From f779de473429b438f740302d953f66ff06f0b0b0 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 10:43:17 -0400 Subject: [PATCH 01/12] update: a quiet launch offer and background installs that keep work running The launch check now raises an offer in the keys line, counted down live from ten seconds, and installs the release in the background when it expires. alt+n or /update skip skips that exact release; /update never or the settings row turns the automatic road off; auto off still names the release on one dim line. Nothing is modal, no plain letter is bound, and no install ever quits, restarts or stops a turn, a task or an engine. /update typed by hand installs on the same coordinator and says the same thing. The lock, the on-disk sha256-tied record and the duplicate/stale-downgrade refusal live inside codeupdate.Install, so the chat surface and `codeaf update` are one acquirer. A duplicate is a quiet no-op; only a named tag may roll back. Package-managed or unwritable installs are refused before any download. Manual pages the engine lane flagged are respelled to the new takeover contract. CODEAF_UPDATE_DEMO raises labelled render fixtures and scripts/update-drive.sh captures them at 80 and 120 columns with no network. docs/design/background-updates.md is the design; the change entry carries the unreleased placeholder number 1789 for root to correct. --- cmd/codeaf/chatv3_surface.go | 30 +- cmd/codeaf/chatv3_surface_test.go | 2 +- cmd/codeaf/update.go | 15 + .../unreleased/1789-background-updates.md | 34 ++ docs/design/background-updates.md | 119 ++++++ internal/config/settings.go | 55 +++ internal/manual/chat/commands.md | 6 +- .../manual/chat/running-from-the-terminal.md | 50 ++- .../manual/chat/running-on-another-machine.md | 19 +- .../manual/chat/staying-on-that-machine.md | 22 +- internal/tui3/app.go | 32 +- internal/tui3/commands.go | 4 +- internal/tui3/conversation_open.go | 3 - internal/tui3/home.go | 3 - internal/tui3/homeexchange.go | 3 - internal/tui3/hometip.go | 10 +- internal/tui3/input.go | 10 +- internal/tui3/render.go | 12 +- internal/tui3/settings.go | 15 + internal/tui3/steer.go | 7 + internal/tui3/tui3.go | 12 +- internal/tui3/updatecmd.go | 199 ++++++--- internal/tui3/updatecmd_test.go | 125 +++--- internal/tui3/updatedemo.go | 91 ++++ internal/tui3/updateoffer.go | 403 ++++++++++++++++++ internal/tui3/updateoffer_test.go | 376 ++++++++++++++++ internal/tui3/updateplatform.go | 7 + internal/update/auto.go | 401 +++++++++++++++++ internal/update/auto_test.go | 234 ++++++++++ internal/update/check.go | 2 +- internal/update/dirwrite_unix.go | 21 + internal/update/dirwrite_windows.go | 28 ++ internal/update/install.go | 39 ++ internal/update/lock_unix.go | 20 + internal/update/lock_windows.go | 25 ++ internal/update/update_test.go | 2 +- scripts/update-drive.sh | 91 ++++ 37 files changed, 2355 insertions(+), 172 deletions(-) create mode 100644 docs/changes/unreleased/1789-background-updates.md create mode 100644 docs/design/background-updates.md create mode 100644 internal/tui3/updatedemo.go create mode 100644 internal/tui3/updateoffer.go create mode 100644 internal/tui3/updateoffer_test.go create mode 100644 internal/update/auto.go create mode 100644 internal/update/auto_test.go create mode 100644 internal/update/dirwrite_unix.go create mode 100644 internal/update/dirwrite_windows.go create mode 100644 internal/update/lock_unix.go create mode 100644 internal/update/lock_windows.go create mode 100755 scripts/update-drive.sh diff --git a/cmd/codeaf/chatv3_surface.go b/cmd/codeaf/chatv3_surface.go index 809f6d3f61..d00e3540c7 100644 --- a/cmd/codeaf/chatv3_surface.go +++ b/cmd/codeaf/chatv3_surface.go @@ -6,6 +6,7 @@ import ( "os" "github.com/Agent-Field/codeaf/internal/buildinfo" + "github.com/Agent-Field/codeaf/internal/config" internalenv "github.com/Agent-Field/codeaf/internal/env" "github.com/Agent-Field/codeaf/internal/tui3" codeupdate "github.com/Agent-Field/codeaf/internal/update" @@ -58,15 +59,35 @@ func runSurface(ctx context.Context, options tui3.Options) error { options.UpdateRunning = revision options.UpdateCurl = curl options.UpdateArgs = surfaceArguments() + // THE AUTOMATIC UPDATER IS WIRED HERE AND NOWHERE ELSE, so all four doors + // that open this surface get one coordinator: one setting, one remembered + // dismissal, one failure count and one install lock per profile. The lock + // is what keeps two terminals from replacing this executable at once, and + // the surface hands back only a release closure so it never touches a file + // itself. + autoUpdate := config.UpdateAutoAt(options.ProfileDir) + options.UpdateAuto = &tui3.UpdateCoordinator{ + Enabled: autoUpdate, + State: func() codeupdate.AutoState { return codeupdate.LoadAutoState(options.ProfileDir) }, + Dismiss: func(tag string) error { return codeupdate.DismissRelease(options.ProfileDir, tag) }, + Failure: func(tag string, failure error) { codeupdate.AutoFailure(options.ProfileDir, tag, failure) }, + Success: func() { codeupdate.AutoSuccess(options.ProfileDir) }, + Disable: func() error { return config.SaveUpdateAuto(options.ProfileDir, false) }, + // A BREW, DISTRO OR NIX FILE, OR ONE IN A FOLDER THIS ACCOUNT CANNOT + // WRITE, is not ours to replace in place. + Refusal: func() string { return codeupdate.InstallRefusal(executable, curl) }, + } options.UpdateCheck = func(check context.Context) (codeupdate.Available, bool) { return codeupdate.CheckLaunch(check, codeupdate.CheckOptions{ Running: revision, ProfileDir: options.ProfileDir, Client: client, - Disabled: internalenv.Get(codeupdate.NoUpdateCheckEnv) == "1", + // OFF MEANS NO REQUEST AT ALL, not a hidden one. The environment + // value keeps its one-launch meaning beside the standing setting. + Disabled: !autoUpdate || internalenv.Get(codeupdate.NoUpdateCheckEnv) == "1", Executable: executable, }) } options.ResolveUpdate = client.Select - options.InstallUpdate = func(install context.Context, release codeupdate.Release) (codeupdate.InstallResult, error) { + options.InstallUpdate = func(install context.Context, options codeupdate.InstallOptions) (codeupdate.InstallResult, error) { // Resolution belongs to the off-frame installation command. A launch may // live on a slow mount, but its first frame never has to wait for that // mount merely because /update exists. @@ -74,7 +95,10 @@ func runSurface(ctx context.Context, options tui3.Options) error { if err != nil { return codeupdate.InstallResult{}, err } - return codeupdate.Install(install, codeupdate.InstallOptions{Client: client, Release: release, Target: target, Curl: curl}) + options.Client = client + options.Target = target + options.Curl = curl + return codeupdate.Install(install, options) } // The byte meter, off unless a developer named a log file (wire.go). It // measures what this surface DRAWS and is therefore as local as the terminal diff --git a/cmd/codeaf/chatv3_surface_test.go b/cmd/codeaf/chatv3_surface_test.go index cf4e35be2a..d4400fc70a 100644 --- a/cmd/codeaf/chatv3_surface_test.go +++ b/cmd/codeaf/chatv3_surface_test.go @@ -151,7 +151,7 @@ func TestRunSurfaceWiresTheDeferredLaunchCheckAndInstallerThroughRealInit(t *tes t.Fatalf("surface exit: %v", err) } - result, err := seen.InstallUpdate(context.Background(), codeupdate.Release{Tag: "v0.2.0", Repository: "Agent-Field/codeaf"}) + result, err := seen.InstallUpdate(context.Background(), codeupdate.InstallOptions{Release: codeupdate.Release{Tag: "v0.2.0", Repository: "Agent-Field/codeaf"}}) if err != nil { t.Fatal(err) } diff --git a/cmd/codeaf/update.go b/cmd/codeaf/update.go index 2d4636855e..ec03d5d500 100644 --- a/cmd/codeaf/update.go +++ b/cmd/codeaf/update.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "io" "os" @@ -107,12 +108,26 @@ func runUpdate(args []string) error { } } + // THE LOCK, THE ON-DISK RE-READ AND EVERY REFUSAL LIVE INSIDE + // [codeupdate.Install], so the command line and the chat surface are one + // acquirer with nothing to keep in step. result, err := codeupdate.Install(context.Background(), codeupdate.InstallOptions{ Client: client, Release: release, Target: target, Curl: curl, + // A NAMED TAG IS A DELIBERATE ROLLBACK; a channel or the default is not. + AllowDowngrade: strings.TrimSpace(choice.Version) != "", }) if err != nil { + var refusal *codeupdate.RefusalError + if errors.As(err, &refusal) { + fmt.Fprintln(updateErr, refusal.Reason) + return exitStatus(2) + } return updateFailure(err, curl) } + if result.Already { + fmt.Fprintf(updateOut, "codeaf: %s is already installed at %s\n", result.Release.Tag, result.Path) + return nil + } fmt.Fprintf(updateOut, "codeaf: installed %s at %s\n", result.Release.Tag, result.Path) if err := updateVersionLine(result.Path, updateOut, updateErr); err != nil { return updateFailure(fmt.Errorf("run the installed codeaf: %w", err), curl) diff --git a/docs/changes/unreleased/1789-background-updates.md b/docs/changes/unreleased/1789-background-updates.md new file mode 100644 index 0000000000..393ec97477 --- /dev/null +++ b/docs/changes/unreleased/1789-background-updates.md @@ -0,0 +1,34 @@ +--- +kind: changed +title: launches offer a new codeaf in a quieter, non-blocking way, and installs run in the background +pr: 1789 +surface: [chat] +invalidates: + - "`/update` used to refuse while a turn or task was running and then quit the program so the door could restart on the new build. It now installs in the background on the same road the automatic updater uses: nothing quits, nothing restarts, and a running turn or task is untouched. The new build is what opens the next time codeaf is started." + - "The launch notice used to be a single dim line whose only action was `/update`. With `update.auto` on (the default) a newer release now raises an offer in the keys line, counted down live from ten seconds, and then installs itself in the background; `alt+n` or `/update skip` skips that exact release, and `/update never` or the settings row turns the automatic road off." + - "A version behind its channel with `update.auto` off still gets one quiet line naming the release and the hand-run road, instead of no check at all." + - "A codeaf owned by Homebrew, Nix or a system package manager, or one in a folder the account cannot write, is refused in place by both the chat and `codeaf update`, and the refusal names the manager and the curl road." + - "The manuals `internal/manual/chat/staying-on-that-machine.md` and `running-on-another-machine.md` said a busy engine from an older build was replaced the moment a newer codeaf opened. They now say the truth the engine lane implements: an older engine holding work is joined and keeps it, and steps aside once it is quiet (or at the next safe launch for a host too old to know the ask); one holding nothing hands over at once; an attached idle window counts as holding work; and another wire, or one too old to be asked, is refused with `codeaf engine --stop --workspace ''` named." + - "One install now runs at a time per executable, across terminals and profiles: the lock is an OS advisory lock keyed on the executable AND IS TAKEN INSIDE the shared installer, so the chat surface and `codeaf update` are one acquirer. Under it a sha256-tied record says what is on disk: a duplicate is a quiet no-op and a stale automatic downgrade is refused, while an explicitly named tag may still roll back." +--- + +The offer waits in the line that already carries the idle keys, so it never +takes the message box, never blocks a keystroke and never binds a plain letter. +While its grace is open it stays visible even over a running turn's own keys by +appending a defer clause; when it expires the download begins in the background. +The sentence names the release, says it installs shortly, and carries the skip +chord and `/update` — both survive an eighty-column window, because the +automatic clause is dropped before either action is. + +The completion wording is exact and quiet: `codeaf installed · this +session keeps running`, with the durable note adding that new windows use the +update while existing engines finish their work first and an attached window +keeps its engine current. It does not promise that a busy engine swaps. A +same-release candidate is a quiet successful no-op rather than an alarming +failure, and only a NAMED tag (`/update `, `update --version `) may +deliberately roll back; an automatic or channel update never does. + +`update.auto`, `auto update` in `/settings`, is on by default. The dismissal and +the failure count live in one small file beside `config.json`, keyed by release +and by executable, so the same release never asks twice and a release that failed +three times stops being tried on its own. diff --git a/docs/design/background-updates.md b/docs/design/background-updates.md new file mode 100644 index 0000000000..c9912398e7 --- /dev/null +++ b/docs/design/background-updates.md @@ -0,0 +1,119 @@ +# Background updates and the launch offer + +*The contract for how codeaf keeps itself current: one coordinator, one offer, +one installer per executable. `internal/update` is the machinery, `internal/tui3` +draws it, and this file says why the shape is what it is.* + +## What a person sees + +On launch, behind the newest release of its own channel, a build with `update.auto` +on raises one sentence in the keys line: + +``` +codeaf v0.9.3 is out · installs in 10s · alt+n skip · /update +``` + +It is not modal, it does not take the message box, and it binds no plain letter. +The count is live — the line repaints once a second from the deadline — so it can +never show a frozen number. After [codeupdate.AutoGrace] the download begins by +itself. The three answers are: + +- `/update` (or `/upgrade`) — install now; an explicit channel or tag wins, and + a NAMED tag is the one deliberate rollback. +- `alt+n`, or `/update skip` — skip this release. It is written down and that + exact tag is never offered again. +- `/update never`, or the `auto update` row in `/settings` — stop installing + automatically. The launch check still runs and still says what is out. + +The three are the ONLY commands the offer prints, and a test extracts every +`/update` token from the hint and the note and checks the parser answers each as +itself rather than as a tag. While the grace is open the offer stays reachable +even over a running turn's own keys, because `footHint` appends a defer clause to +whatever outranks it; `hintShorter` drops that clause first, so a narrow frame +loses the offer and never the turn's keys. A question page owns the frame, so the +grace PAUSES under one instead of counting out invisibly. + +## What a background install does and says + +`/update` typed by hand takes the same road as the automatic one. Both download, +verify sha256, and replace the executable atomically while this process keeps +running the build it started on. Neither quits, neither restarts a conversation, +and neither stops a turn or a task. The completion sentences are: + +``` +checksum matched · installed at (hand-run only) +codeaf installed · this session keeps running +``` + +The durable note adds: `new windows use the update; existing engines finish their +work first, and an attached window keeps its engine current`. That is the engine +lane's contract, stated without over-promising: a file replaced on disk does not +force a running engine to hand over, and an idle line never claims it did. + +A finished line dwells for [updateOfferingDwell] and then folds; the transcript +note is the durable record. A same-release candidate is a quiet successful no-op: +`codeaf is already the build on disk · this session is untouched`. + +There is no restart-on-the-spot action. A new build runs at the next launch. + +## The coordinator's memory + +One small file in the profile, `update-state.json`: + +- the tag a person dismissed, with the moment, so a newer tag is a new question; +- the tag that last failed, how many times, and when, which becomes a backoff + (doubling from 6h) and stops after three failures. + +`internal/config`'s `update.auto` row is the standing switch, default on. The +offer's own opt-out writes through the same writer the settings row does. + +## One installer per executable + +The lock is an OS advisory lock on `.install.lock`, keyed on the +resolved target and not on a profile: two profiles can share one codeaf file, and +it is the file that must not be replaced twice at once. A process that dies +releases the lock in the kernel, so nothing is ever taken over by guessing at an +age. + +THE LOCK IS TAKEN INSIDE `codeupdate.Install` AND NOWHERE ELSE, so the chat +surface and `codeaf update` are one acquirer with nothing to keep in step. Under +the lock the installer reads the record, which is trusted only while the file +still hashes to the tag — a binary replaced by curl, a package manager or a +person reads as unknown: + +- a candidate already on disk is a quiet successful no-op (`Already`); +- a candidate older than what is on disk is refused UNLESS the person named that + exact tag, which is a deliberate rollback (`AllowDowngrade`); +- a target another process is mid-install on is refused with `another codeaf is + already installing an update`. + +Channel builds are ordered by the release's published moment, the same +`CompareChannelBuilds` the launch check uses, so two same-day dev builds are +ordered by fact rather than by guess. + +## What it refuses + +- A source or unstamped build: `this codeaf was built from source · rebuild with + make build, or install a release: `. +- An unsupported platform: the installer's own error, recorded as a failure. +- A checksum that does not match, or a missing checksum: the installer's error; + the original file is untouched because the download lands in a temporary file + and only a verified body is moved into place. +- A file owned by Homebrew, Nix or a system package manager, or one in a folder + this account cannot write: refused in place, with the manager and the curl road + named, before anything is downloaded. + +## Render fixtures + +`CODEAF_UPDATE_DEMO` names one state so a capture can be taken without waiting +out a grace or paying for a download: `offer`, `downloading`, `ready`, +`ready-working`, `manual`, `off`, `failure`. Every case first clears the updater +seams, so a fixture cannot make a request or replace a file, and each says at the +top of the transcript that it is a fixture. `scripts/update-drive.sh` captures +them at 80 and 120 columns with the real binary. + +These are deterministic render fixtures, labelled in the frame itself, and they +clear every updater seam so a capture machine needs no network and cannot install +anything. They are not evidence that a background install preserves a real turn; +that continuity is REQUIRED VALIDATION to be recorded from the E2E suite before +this is treated as proved — a fixture shows the surface, never the continuity. diff --git a/internal/config/settings.go b/internal/config/settings.go index b6bafccde1..213a12b46f 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -280,6 +280,15 @@ const ( // because a person who has silenced the tips has said they know the surface, // and being told about features is the same conversation. KeyHints = "ui.hints" + // KeyUpdateAuto is whether codeaf checks for a new release at launch and + // installs it in the background on its own. ON is the default: the check is + // one small request after the first frame, and a person who does not want it + // has a row, an environment variable and one chord on the offer itself. Off + // means no launch request and no background install at all; `/update` still + // installs a release on request, because a person who typed it has said what + // they want. It is a boolean because there is no third answer between + // "keep me current" and "leave me alone until I ask". + KeyUpdateAuto = "update.auto" // KeyWork controls whether completed turn machinery starts folded or open. KeyWork = "ui.work" // KeyIcons selects the step icon repertoire independently of colour. @@ -1184,6 +1193,12 @@ var OperatorEnvPins = []string{ // give the page real questions. A row offering to persist it would be // offering to open a fixture over somebody's conversation every launch. "CODEAF_QUESTION_DEMO", + // CODEAF_UPDATE_DEMO names one of the update offer's fixture states + // (internal/tui3/updatedemo.go), so the offer can be SEEN at every stage + // without waiting out a grace or paying for a download. Same terms as the + // question fixture beside it: no key, no command, no row, and it disables + // every updater hook so a capture machine needs no network at all. + "CODEAF_UPDATE_DEMO", // CODEAF_SUITE_DIRLOCK_PATH names the older directory lock a heavy suite // takes beside its file lock, so that a checkout too old to know about the // file lock can still see the box is busy (scripts/one-suite.sh, #1307). It @@ -1286,6 +1301,8 @@ var OperatorEnvPins = []string{ // shape, under `make demo-home`'s terms. A row offering to persist a // fixture would put a demo question in front of a person every morning. "CODEAF_QUESTION_DEMO", + // And the update offer's own fixture states (internal/tui3/updatedemo.go). + "CODEAF_UPDATE_DEMO", // CODEAF_TASK_BELT sends a task worker BACK to the older node belt // (internal/session's bashbelt.go, docs/design/worker-harness/DESIGN.md). // The bash belt is the shipped default, so the variable is an escape @@ -1480,6 +1497,13 @@ const ( // otherwise. The tips retire themselves the moment each is acted on, so the // default costs a veteran one line per gesture they already know, once. DefaultHints = true + + // DefaultUpdateAuto keeps a profile that has never said otherwise on the + // newest codeaf. The whole cost is one small release check after the first + // frame, and the offer that follows waits in the keys line rather than over + // the box — it never blocks a keystroke, and a person who would rather + // decide for themselves turns it off once, from the offer or the sheet. + DefaultUpdateAuto = true ) // Setting is one row: what it is called, what it reads now, and what happens @@ -2642,6 +2666,19 @@ func (s *Settings) build() []Setting { return writeProfileValue(dir, KeyHints, !disabled) }, }, + // THE ROW READS THE SAME WAY UP AS ITS KEY, unlike the hints row above: + // `update.auto` on means codeaf keeps itself current, and `auto update` on + // is that sentence. The offer's own "stop asking" writes through + // [SaveUpdateAuto], which is this row's writer by another door. + Setting{ + Key: KeyUpdateAuto, Category: CategoryInterface, Kind: SettingBool, + Label: "auto update", + Hint: "check for a new codeaf at launch and install it in the background. " + + "The offer waits in the keys line for about ten seconds; it never blocks a keystroke. " + + "Off, nothing is checked or downloaded until you run /update.", + read: func() string { return formatBool(UpdateAutoAt(dir)) }, + write: func(raw string) error { return writeBool(dir, KeyUpdateAuto, raw) }, + }, Setting{ Key: KeyAttributionModel, Category: CategoryInterface, Kind: SettingBool, Label: "model in commits", Env: "CODEAF_ATTRIBUTION_MODEL", @@ -3315,6 +3352,24 @@ func HintsAt(profileDir string) bool { return DefaultHints } +// UpdateAutoAt resolves whether codeaf checks for and installs a new release on +// its own, default on. A row that will not parse reads as the default rather +// than as off, for [HintsAt]'s reason: a garbled row must not silently stop a +// person's codeaf from being kept current. +func UpdateAutoAt(profileDir string) bool { + if value, ok := persistedBool(profileDir, KeyUpdateAuto); ok { + return value + } + return DefaultUpdateAuto +} + +// SaveUpdateAuto writes the row from a door that is not the sheet — the offer's +// own "stop asking" answer — through the same writer the row's own does, so +// there is one validation and one place the value lives. +func SaveUpdateAuto(profileDir string, on bool) error { + return writeBool(profileDir, KeyUpdateAuto, formatBool(on)) +} + // DocumentEngineAt resolves the document-reading rung. func DocumentEngineAt(profileDir string) (string, error) { if raw := strings.TrimSpace(env.Get("CODEAF_DOC_ENGINE")); raw != "" { diff --git a/internal/manual/chat/commands.md b/internal/manual/chat/commands.md index d2dacba65a..4955d8888d 100644 --- a/internal/manual/chat/commands.md +++ b/internal/manual/chat/commands.md @@ -332,8 +332,10 @@ Canonical word, the other words it answers to, its argument form, and what it do | `/cache` | — | `clean` | asks first, then deletes the cache to free disk — confirm with `/cache clean now` | | `/cache` | — | `clean now` | deletes the shared build cache now | | `/debug` | — | — | keeps the full record of **this conversation** from here on, and says which folder it goes to | -| `/update` | `/upgrade` | — | installs the newest stable release and restarts this conversation on it | -| `/update` | `/upgrade` | `` | installs that channel's newest release or one exact tag, then restarts this conversation on it | +| `/update` | `/upgrade` | — | installs the newest stable release in the background for the next launch | +| `/update` | `/upgrade` | `` | installs that channel's newest release or one exact tag for the next launch | +| `/update skip` | `/upgrade skip` | — | answers an open update offer not now, for that release only | +| `/update never` | `/upgrade never` | — | turns `auto update` off; the setting row does the same | | `/copy` | — | — | enters copy mode (also ctrl+b) | | `/select` | — | — | hands the pointer back to the terminal (also ctrl+s) | | `/export` | `/save` | — | writes the whole conversation to a file | diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index 4e86279e24..019f1a14b3 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -3,19 +3,43 @@ ## Is there a newer version — update a dev build — keep a dev build up to date — latest dev — /update — why does it say this every time I start At launch, a stable, dev or staging build behind the newest release of its own -channel gets one dim line: `codeaf is out · you have · /update -installs it and restarts · or: `. A release candidate gets that line -when its stable line is published. An equal or ahead build gets no line. Source -and unstamped builds make no launch request, and their `/update` answers `this -codeaf was built from source · rebuild with make build, or install a release: -curl -fsSL https://agentfield.ai/get/codeaf | bash`. - -`/update` downloads the release, checks its sha256, replaces this executable and -restarts the same conversation; `/upgrade` is its alias. With no channel word, -a dev or staging build selects its own channel; stable and rc select stable. An -explicit channel or tag wins. Finish a running turn or task first. An ahead dev -build refuses with `this codeaf is , ahead of the newest dev — -/update installs it anyway`; naming the tag is the deliberate downgrade. +channel is told about it, and with `auto update` on (the default) the install +begins on its own after a brief pause: the sentence `codeaf is out · +installs in 10s · skip · /update` waits in the keys line, counted +down from about ten seconds. It never blocks a keystroke. `/update` while it +is up installs immediately; the skip chord, or `/update skip`, answers that one +release and it is never offered again; `/update never` turns `auto update` +off, and so does the row in `/settings`. A release whose automatic install failed +three times stops being tried on its own. + +A release candidate gets the notice when its stable line is published. An equal +or ahead build gets nothing. Source and unstamped builds make no launch request, +and their `/update` answers `this codeaf was built from source · rebuild with +make build, or install a release: curl -fsSL https://agentfield.ai/get/codeaf | +bash`. A codeaf owned by Homebrew, Nix or the system package manager, or one in a +folder this account cannot write, is never replaced in place: the notice says so +and names the manager and the curl line. + +With `auto update` off, the launch check still runs and still says `codeaf +is out · you have · /update installs it for the next launch`, but +nothing is downloaded until you ask. + +`/update` downloads the release, checks its sha256 and replaces this executable +IN THE BACKGROUND; `/upgrade` is its alias. It never quits, never restarts the +conversation and never stops a running turn or task — the session keeps the build +it started on, and the new one is what opens the next time codeaf is started. +`/update` [skip|never] answers an open offer. With no channel word, a dev or +staging build selects its own channel; stable and rc select stable. An explicit +channel or tag wins. An ahead dev build refuses with `this codeaf is , +ahead of the newest dev — /update installs it anyway`; naming +the tag is the deliberate downgrade, and it is the ONLY road that rolls a file +back. An automatic or channel update never installs an older build than the one +on disk, and a release already on disk is a quiet no-op. If the file is owned by +Homebrew, Nix or the system package manager, or is in a folder this account +cannot write, neither road replaces it in place. One install runs at a time per executable: +two terminals, or two profiles sharing one codeaf, cannot replace it at once, and +a window that resolves a release another terminal has already superseded installs +nothing. Set `CODEAF_NO_UPDATE_CHECK=1` to skip only the launch check. Dev and staging answers are cached beside `config.json` for one hour in `update-check.dev.json` diff --git a/internal/manual/chat/running-on-another-machine.md b/internal/manual/chat/running-on-another-machine.md index fc5db0aacc..59ddf253c8 100644 --- a/internal/manual/chat/running-on-another-machine.md +++ b/internal/manual/chat/running-on-another-machine.md @@ -164,20 +164,25 @@ So before it hands your window over, `codeaf engine` asks whatever is already ho workspace which build it is. The check asks which source the running engine was built from, so building the same commit twice is the same build and your window attaches to it without a word; a build from a changed working tree, or one too old to report a stamp at all, counts -as another build. Three things can be true: +as another build. Four things can be true: - **It is this build.** Your window attaches to it exactly as before. This is the ordinary case, and it costs one question on a local socket. -- **It is an older build** — built earlier, from any file, or too old to answer the - question at all. It is replaced, busy or not: it closes its conversations, flushes their - transcripts, and a fresh one starts from the binary that is on disk now. The window says - `replaced the older engine on spark (pid , , ) — this build holds the - workspace now`. +- **It is an older build, holding work.** Your window joins it and your work carries on. The + old process keeps what it was holding and steps aside at its first quiet moment; a host + built before this ask ignores the note, so for one of those the line says the work is kept + and **your next safe launch once the work is done and every window has left** opens from + the new build. An attached idle window counts as holding work, so close it before the + handover. +- **It is an older build, holding nothing.** It hands the workspace over at once and the + window says `replaced the older engine on spark (pid , , ) — this build + holds the workspace now`. - **It is a newer build.** Your window joins it. If its wire is one this binary cannot speak, the connection is refused, naming this binary as the older one and `codeaf engine --status` as the way to see which is newer. -Only if the older engine will not go is the connection refused, in these words: +An older engine on another wire, or one too old to be asked at all, is refused, in these +words: ``` engine: spark is still holding this conversation on an older codeaf — run codeaf engine --stop --workspace /home/you/project on spark diff --git a/internal/manual/chat/staying-on-that-machine.md b/internal/manual/chat/staying-on-that-machine.md index 05994955e5..c7707d7051 100644 --- a/internal/manual/chat/staying-on-that-machine.md +++ b/internal/manual/chat/staying-on-that-machine.md @@ -471,17 +471,21 @@ A plain `codeaf` does not run your conversation inside the window — the sessio in a process of its own, so it survives the terminal closing. That process also outlives the build that started it. -**An engine from an older build is replaced the moment a newer codeaf opens in that -workspace**, busy or not, and you are told in one line which process that was: +**An older engine holding work is joined, and steps aside at its first quiet moment.** Your +window attaches and your work carries on; the old process keeps what it was holding and is +asked to retire once nothing is in flight. If it was holding nothing, the workspace is handed +over at once and you are told in one line: ``` replaced the older engine on (pid 4242, a1b2c3d4 built 2026-09-21 09:00, /home/you/.codeaf/bin/devaf) — this build holds the workspace now ``` -Its conversations are closed properly on the way out — transcripts flushed; a reply it was -in the middle of stops where it is and keeps what it had written — and they reopen on the -new build. Windows that were on it reconnect to the new one. `codeaf engine --daemon` does -the same and prints the same line. +Retiring closes its conversations properly — transcripts flushed; a reply it was in the +middle of stops where it is and keeps what it had written — and they reopen on the new build +at the next connection. **An attached idle window counts as holding work**, so the handover +waits until you close it; only a stalled window whose link is gone is ignored. A host built +before this ask ignores it, so for one of those the old process keeps its work and **your +next safe launch — after the work is done and every window has left — opens from this build**. When both ends support restart detection, that reconnect retires the old reply channels before displaying new replies. If an answer was still open, the window says @@ -492,8 +496,10 @@ loss to the same running engine still resumes its existing answer. "Older" is when the build was made, whichever file it runs from: another binary built two days ago is older, and so is one too old to say. **A newer engine is never replaced** by an older codeaf — that window joins it — and two copies of one build never take the slot from -each other. It used to be the other way round: an older engine holding work was left in -place, and a fresh `codeaf engine --daemon` exited without a word. +each other. An older engine on another wire, or one too old to be asked at all, is refused +with `codeaf engine --stop --workspace ''` named, and nothing is signalled behind your +back. It used to be the other way round for a busy engine: it was killed and replaced, which +is the rule this page no longer states. ## What still does not work, even though the session stays open diff --git a/internal/tui3/app.go b/internal/tui3/app.go index bec20cb803..e5379d84b6 100644 --- a/internal/tui3/app.go +++ b/internal/tui3/app.go @@ -950,12 +950,22 @@ type app struct { resumed bool updateCheck func(context.Context) (codeupdate.Available, bool) resolveUpdate func(context.Context, codeupdate.Choice) (codeupdate.Release, error) - installUpdate func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) + installUpdate func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) updateRunning string updateCurl string updateArgs []string - updateActive bool - restart *codeupdate.Plan + // updateInFlight is any install running, hand-run or background, and it is + // the guard against a second installer in this window. It stops nothing + // else: an install never owns a turn on this surface. + updateInFlight bool + // updateAuto is the durable half of the automatic updater: the setting, the + // remembered dismissal, the failure count and the cross-terminal lock. Nil + // means this surface has no automatic updater and the launch check is the + // one dim note it always was. + updateAuto *UpdateCoordinator + // offer is the launch offer's state machine (updateoffer.go). + offer updateOffer + restart *codeupdate.Plan // THE OPENROUTER BALANCE (credits.go). readCredits is the door's reader and // nil on every surface that cannot ask; the rest is what the loop keeps so // the keys row never reads a file or scans a catalog while it draws. @@ -2965,6 +2975,7 @@ func newApp(ctx context.Context, opts Options) *app { updateRunning: strings.TrimSpace(opts.UpdateRunning), updateCurl: strings.TrimSpace(opts.UpdateCurl), updateArgs: append([]string(nil), opts.UpdateArgs...), + updateAuto: opts.UpdateAuto, restart: opts.Restart, models: opts.Models, modelsForService: opts.ModelsForService, @@ -3268,6 +3279,9 @@ func newApp(ctx context.Context, opts Options) *app { // person presses; it exists so that the page a question opens into can be // looked at on a real screen before anything raises a real one. a.openDemoQuestion(env) + // AND THE UPDATE OFFER'S OWN, for the same reason and on the same terms + // (updatedemo.go). + a.openDemoUpdate(env) // AND THE TERMINAL'S TITLE IS READ ONCE THE SURFACE KNOWS WHERE IT OPENED, // which is only now — home, a picker or a conversation. [app.Init] sends it, // and from then on [app.retitle] sends it again only when it moves. @@ -5553,6 +5567,15 @@ func (a *app) route(msg tea.Msg) (tea.Model, tea.Cmd) { case updateCheckMsg: return a, a.tookUpdateCheck(msg) + + case updateOfferTickMsg: + return a, a.tookOfferTick() + + case updateGraceMsg: + return a, a.tookUpdateGrace() + + case offerSettleMsg: + return a, a.tookOfferSettle() case creditWakeMsg: if a.creditRecordPending.Swap(false) { a.refreshCreditWarnings() @@ -5567,9 +5590,6 @@ func (a *app) route(msg tea.Msg) (tea.Model, tea.Cmd) { case updateInstallMsg: return a, a.tookUpdateInstall(msg) - case updateRestartMsg: - return a, a.quit() - case frameMsg: return a, a.paint() } diff --git a/internal/tui3/commands.go b/internal/tui3/commands.go index f6502f2233..cf9a4835e8 100644 --- a/internal/tui3/commands.go +++ b/internal/tui3/commands.go @@ -474,8 +474,8 @@ var commands = []command{ // twice in a day, and a row inserted higher would push a daily command // behind a scroll. {name: "debug", desc: "keep the full record of this conversation · says where it goes"}, - {name: "update", desc: "install the newest codeaf and restart on it", alias: []string{"upgrade"}}, - {name: "update", args: "", desc: "…that channel or exact tag, then restart"}, + {name: "update", desc: "install the newest codeaf for the next launch", alias: []string{"upgrade"}}, + {name: "update", args: "", desc: "…that channel or exact tag; skip or never answer an open offer"}, {name: "help", desc: "this list", alias: []string{"?"}}, {name: "quit", desc: "close this conversation", alias: []string{"exit", "q"}}, } diff --git a/internal/tui3/conversation_open.go b/internal/tui3/conversation_open.go index 1661a33de9..91fdaaa3e2 100644 --- a/internal/tui3/conversation_open.go +++ b/internal/tui3/conversation_open.go @@ -104,9 +104,6 @@ func (a *app) renewLater(say func(string), after func() tea.Cmd) tea.Cmd { } func (a *app) homeStartLater(text, place string) tea.Cmd { - if (strings.TrimSpace(text) != "" || place != "" || len(a.home.chips) > 0) && a.updateStopsTurn() { - return nil - } say := func(text string) { a.home.say(text, "") } if !a.canStart() { say(newUnavailableWord) diff --git a/internal/tui3/home.go b/internal/tui3/home.go index d130166e50..4bf8352be4 100644 --- a/internal/tui3/home.go +++ b/internal/tui3/home.go @@ -3615,9 +3615,6 @@ func (a *app) homeStartWithProject(text, place string) tea.Cmd { } func (a *app) homeStartWithProjectNow(text, place string) tea.Cmd { - if (strings.TrimSpace(text) != "" || place != "" || len(a.home.chips) > 0) && a.updateStopsTurn() { - return nil - } if !a.canStart() { a.home.say(newUnavailableWord, "") return nil diff --git a/internal/tui3/homeexchange.go b/internal/tui3/homeexchange.go index e56b9dd2c0..479bbc174c 100644 --- a/internal/tui3/homeexchange.go +++ b/internal/tui3/homeexchange.go @@ -974,9 +974,6 @@ func (a *app) askHereWith(text string, orders ErrandOrders) tea.Cmd { } // The door was found, whatever it answers below (notice.go). a.noticeEvent(eventAsked) - if a.updateStopsTurn() { - return nil - } h := &a.home if a.errand == nil { h.say(homeAskUnavailableWord, "") diff --git a/internal/tui3/hometip.go b/internal/tui3/hometip.go index f25e79c583..f6e0ccb2e4 100644 --- a/internal/tui3/hometip.go +++ b/internal/tui3/hometip.go @@ -92,7 +92,15 @@ func (a *app) homeFootLine(width int, pal palette) string { if warning != "" { room -= ansi.StringWidth(warning) + 1 } - hint := hintFit(a.placeHint(), room) + // THE UPDATE OFFER TAKES THE KEYS ROW WHERE IT IS UP, on home as in a + // conversation: a release waiting on an answer must be answerable from + // wherever the person actually is (updateoffer.go). + hint := "" + if offer := a.updateOfferHint(room); offer != "" { + hint = offer + } else { + hint = hintFit(a.placeHint(), room) + } line := " " + paintHint(hint, pal, pal.dim) used := 1 + ansi.StringWidth(hint) before := used diff --git a/internal/tui3/input.go b/internal/tui3/input.go index 28348b314b..9933e88174 100644 --- a/internal/tui3/input.go +++ b/internal/tui3/input.go @@ -434,6 +434,13 @@ func (a *app) key(msg tea.KeyPressMsg) tea.Cmd { } } + // THE UPDATE OFFER'S THREE CHORDS, read above every place and below every + // modal. They are chords and never letters: an offer answered by `y` is an + // offer that eats the first word of somebody's sentence (updateoffer.go). + if cmd, taken := a.updateOfferKey(msg); taken { + return cmd + } + // THE CHIP'S CHORD IS READ ABOVE EVERY PLACE, because the chip is drawn on // every page and a door that only opened from the conversation would be a // door that is not there wherever a person is actually standing when the @@ -1463,9 +1470,6 @@ func (a *app) enterLine() tea.Cmd { // empty message — "what is this?" is often the picture itself — so the two // tests below both ask about the tray as well as about the words. held := len(a.chips) > 0 - if !strings.HasPrefix(line, "/") && (line != "" || held) && a.updateStopsTurn() { - return nil - } // An empty draft with a call selected is a reader, not a typist: enter // opens what ↑/↓ picked out. A draft of any length is a sentence, and a // sentence wins. diff --git a/internal/tui3/render.go b/internal/tui3/render.go index 57568e1fc9..aa1d85e657 100644 --- a/internal/tui3/render.go +++ b/internal/tui3/render.go @@ -3854,13 +3854,13 @@ func (a *app) footHint(width int) string { return "" } if hint := a.hintWord(); hint != "" { - return hint + return a.withUpdateOffer(hint) } // A WORD OF THE HEAD UNDER THE POINTER says what it opens and its key // (topnav.go's [app.headHint]), under a state's own keys and over every // resting sentence. if hint := a.headHint(); hint != "" { - return hint + return a.withUpdateOffer(hint) } // AND UNDER THE STATES, BUT OVER EVERY TIP AND DOOR: THE CHORD THAT DID NOT // ARRIVE. A Mac whose Option key is composing accents answers the switcher's @@ -3871,7 +3871,13 @@ func (a *app) footHint(width int) string { // all on every other terminal (chords.go's [app.chordWatch] arms it only on a // Mac, and only after a chord was actually aimed and missed). if a.chordLost && a.chords.meta == chordMetaWord { - return a.chords.chordShortWords() + return a.withUpdateOffer(a.chords.chordShortWords()) + } + // AND UNDER THOSE, THE UPDATE OFFER. While its grace is open it appends its + // defer clause to whatever outranks it (above), so the chance to say "not + // now" survives a running turn; at rest it takes the slot outright. + if hint := a.updateOfferHint(width); hint != "" { + return hint } return a.idleHint() } diff --git a/internal/tui3/settings.go b/internal/tui3/settings.go index e184d2ba38..51b66cadf0 100644 --- a/internal/tui3/settings.go +++ b/internal/tui3/settings.go @@ -622,6 +622,13 @@ var settingUI = map[string]settingMeta{ // NO `about` OF ITS OWN: the line under the row is the registry's hint, // the owner's own wording (2026-09-24), said once ([settingMetaFor]). }, + // THE UPDATE ROW READS THE WAY UP ITS KEY DOES, unlike the hints row above: + // on means codeaf keeps itself current. It sits on Display with the other + // choices about what this surface does on its own. + config.KeyUpdateAuto: { + tab: tabDisplay, label: "auto update", widget: widgetToggle, + // NO `about`: the registry's own hint is the sentence, said once. + }, config.KeySplitPct: { tab: tabDisplay, label: "chat width", widget: widgetText, about: "the chat pane's share of the frame while the task rail is open.", @@ -2652,6 +2659,14 @@ func (a *app) applySetting(item sheetItem, raw string) { // banked rule through, and it rebuilds the policy from these same three rows. note := "" switch item.row.Key { + case config.KeyUpdateAuto: + // THE LIVE SESSION SEES THE ROW IT JUST CHANGED. The grace re-reads + // this flag, so turning it off in the panel must reach the running + // window and not only the file. + if a.updateAuto != nil { + a.updateAuto.Enabled = config.UpdateAutoAt(a.profileDir) + } + a.touch() case config.LaneSettingKey(talkSlot), config.LaneBorrowKey(talkSlot): a.laneRowChanged() case config.KeyLaneGuard: diff --git a/internal/tui3/steer.go b/internal/tui3/steer.go index 3305ab10f2..f3aff627b1 100644 --- a/internal/tui3/steer.go +++ b/internal/tui3/steer.go @@ -490,6 +490,13 @@ func (a *app) runHint() string { // Idle controls yield from the left to commands and home; running hints keep // their existing priority from the left. Every step removes at least one clause. func (a *app) hintShorter(slot string) string { + // THE UPDATE OFFER'S DEFER CLAUSE IS DROPPED FIRST. It is appended to a + // running turn's own keys, and the generic rule below cannot shorten a line + // that extends the run hint — it would delete the whole row, taking the + // turn's keys with it. Here the offer's clause goes and the turn's keys stay. + if full := a.runHint(); full != "" && strings.HasPrefix(slot, full+hintSegment) { + return full + } idle := a.idleHint() if slot != "" && (slot == idle || strings.HasSuffix(idle, hintSegment+slot)) { parts := strings.Split(slot, hintSegment) diff --git a/internal/tui3/tui3.go b/internal/tui3/tui3.go index 5a8c986ad0..a326e9870e 100644 --- a/internal/tui3/tui3.go +++ b/internal/tui3/tui3.go @@ -457,9 +457,12 @@ type Options struct { ImplicitTalk bool // ResolveUpdate and InstallUpdate are the two off-frame halves of /update. // Keeping selection separate lets the surface name the tag before the - // download begins. Nil leaves the command with an honest refusal. + // download begins. InstallUpdate takes the whole InstallOptions so the + // surface can say whether a person asked for an exact tag — the one case + // where replacing the file with an older release is deliberate. Nil leaves + // the command with an honest refusal. ResolveUpdate func(context.Context, codeupdate.Choice) (codeupdate.Release, error) - InstallUpdate func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) + InstallUpdate func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) // UpdateRunning is the exact revision of this process, without build-time // decoration. UpdateArgs are its original arguments. Restart is the slot the // surface fills before quitting and the door reads after the terminal is back. @@ -467,6 +470,11 @@ type Options struct { UpdateCurl string UpdateArgs []string Restart *codeupdate.Plan + // UpdateAuto is the automatic updater's durable half: whether it is on, the + // release this profile already answered, the failure count that stops a + // retry loop, and the cross-terminal install lock. Nil leaves the launch + // notice as a one-line note with no offer behind it. + UpdateAuto *UpdateCoordinator // Memory is the durable memory store behind the memory place. Nil means the // place is unavailable; the live door passes the same store it gave the diff --git a/internal/tui3/updatecmd.go b/internal/tui3/updatecmd.go index ab38fe5605..9061ad6d06 100644 --- a/internal/tui3/updatecmd.go +++ b/internal/tui3/updatecmd.go @@ -3,7 +3,6 @@ package tui3 import ( "context" "strings" - "time" tea "charm.land/bubbletea/v2" @@ -19,29 +18,15 @@ type updateResolveMsg struct { release codeupdate.Release choice codeupdate.Choice err error + // auto is true when the offer's own road raised this, in the background and + // for a release the person did not have to ask for. + auto bool } type updateInstallMsg struct { result codeupdate.InstallResult err error -} - -type updateRestartMsg struct{} - -const updateTurnRefusal = "finish the update before starting a turn" - -// updateStopsTurn keeps every turn-starting door behind the update that owns -// the next process start. Home also receives the same words on its visible -// message line because its full-page frame hides conversation notes. -func (a *app) updateStopsTurn() bool { - if !a.updateActive { - return false - } - a.note(updateTurnRefusal) - if a.at(pageHome) { - a.home.say(updateTurnRefusal, "") - } - return true + auto bool } // checkForUpdate starts after the app is ready to draw, so a slow or absent @@ -56,19 +41,63 @@ func (a *app) checkForUpdate() tea.Cmd { } } +// tookUpdateCheck folds the launch check. OFF still names the release, because +// knowing is not acting; ON raises the offer and its countdown. func (a *app) tookUpdateCheck(message updateCheckMsg) tea.Cmd { - if message.show { + if !message.show { + return nil + } + if a.updateAuto == nil { a.note(message.available.Notice()) + return nil } - return nil + if refusal := a.updateRefusal(); refusal != "" { + a.note(quietUpdateNotice(message.available)) + if a.updateAuto.Enabled { + a.note(refusal) + } + return nil + } + if !a.updateAuto.Enabled { + a.note(quietUpdateNotice(message.available)) + return nil + } + tag := strings.TrimSpace(message.available.Latest) + state := codeupdate.AutoState{} + if a.updateAuto.State != nil { + state = a.updateAuto.State() + } + if !codeupdate.ShouldOffer(state, tag, a.now()) { + return nil + } + a.offer.raise(tag, a.updateRunning, a.now()) + a.note(a.updateOfferNote()) + return a.offerTick() } +// runUpdateCommand is the typed road, on the same coordinator as the offer. It +// never interrupts anything: an install begun here replaces the file in the +// background, and a turn or a task may keep running throughout. func (a *app) runUpdateCommand(argument string) tea.Cmd { - if a.state == stateWorking || len(a.runningNodes()) != 0 { - a.note("finish the running turn or task before updating codeaf") + argument = strings.TrimSpace(argument) + switch argument { + case "skip": + switch { + case a.offer.offering(): + a.skipUpdateOffer() + case a.offer.active(): + // AN INSTALL ALREADY RUNNING IS NOT OURS TO CANCEL, and saying it + // was skipped would be a lie the download immediately contradicts. + a.note("an install is already running · it finishes in the background · /update never stops the next one") + default: + a.note("there is no update offer to skip · /update installs one for the next launch when you want it") + } + return nil + case "never": + a.declineUpdateOffer() return nil } - if a.updateActive { + if a.updateInFlight { a.note("an update is already running") return nil } @@ -80,14 +109,26 @@ func (a *app) runUpdateCommand(argument string) tea.Cmd { a.note("this codeaf was built from source · rebuild with make build, or install a release: " + codeupdate.CurlCommand) return nil } - choice := updateChoice(argument, a.updateRunning) + if refusal := a.updateRefusal(); refusal != "" { + a.note(refusal) + return nil + } + // BARE `/update` WHILE THE OFFER IS UP is the offer's own "now". + if argument == "" && a.offer.offering() { + return a.updateNowFromOffer() + } + a.offer.clear() + return a.beginUpdate(updateChoice(argument, a.updateRunning), false) +} + +// beginUpdate captures the resolver and leaves the loop. Every road installs in +// the background; `auto` only changes what the completion says. +func (a *app) beginUpdate(choice codeupdate.Choice, auto bool) tea.Cmd { resolve := a.resolveUpdate - // The mark is set before the command leaves the loop. A second /update or a - // new turn can therefore never enter while release selection is off-frame. - a.updateActive = true + a.updateInFlight = true return func() tea.Msg { release, err := resolve(context.Background(), choice) - return updateResolveMsg{release: release, choice: choice, err: err} + return updateResolveMsg{release: release, choice: choice, err: err, auto: auto} } } @@ -107,11 +148,11 @@ func updateChoice(argument, running string) codeupdate.Choice { func (a *app) tookUpdateResolve(message updateResolveMsg) tea.Cmd { if message.err != nil { - a.updateFailed(message.err) - return nil + return a.updateStopped(message.auto, message.choice.Version, message.err) } if message.release.Tag == a.updateRunning { - a.updateActive = false + a.updateInFlight = false + a.offer.clear() a.note("you are on the newest codeaf, " + message.release.Tag) return nil } @@ -125,42 +166,98 @@ func (a *app) tookUpdateResolve(message updateResolveMsg) tea.Cmd { if channel == "" { channel = "stable" } - a.updateActive = false + a.updateInFlight = false + a.offer.clear() a.note("this codeaf is " + a.updateRunning + ", ahead of the newest " + channel + " " + message.release.Tag + " — /update " + message.release.Tag + " installs it anyway") return nil } } - a.note("downloading codeaf " + message.release.Tag + " for " + runtimePlatform() + "…") + // The lock, the on-disk re-read and the duplicate/downgrade refusal all live + // inside [codeupdate.Install], so the chat surface has no second acquirer to + // keep in step with the command line. + a.offer.downloading(message.auto) + a.offer.tag = strings.TrimSpace(message.release.Tag) + a.note(a.updateOfferNote()) install := a.installUpdate + release := message.release + // ONLY A NAMED TAG IS A DELIBERATE ROLLBACK. A channel or a bare /update + // follows the release line forward, so a file another window advanced is + // never stepped back by it. + allowDowngrade := !message.auto && strings.TrimSpace(message.choice.Version) != "" return func() tea.Msg { - result, err := install(context.Background(), message.release) - return updateInstallMsg{result: result, err: err} + result, err := install(context.Background(), codeupdate.InstallOptions{ + Release: release, AllowDowngrade: allowDowngrade, + }) + return updateInstallMsg{result: result, err: err, auto: message.auto} } } -func runtimePlatform() string { - return runtimeGOOS + "/" + runtimeGOARCH -} - +// tookUpdateInstall finishes one install. Both roads say the same thing: the +// file is replaced, this process keeps the build it started on, and a later +// launch opens the new one. func (a *app) tookUpdateInstall(message updateInstallMsg) tea.Cmd { + a.updateInFlight = false if message.err != nil { - a.updateFailed(message.err) + return a.updateStopped(message.auto, message.result.Release.Tag, message.err) + } + if message.result.Already { + // ALREADY INSTALLED IS SUCCESS, SAID QUIETLY: the build on disk is the + // one this window was about to write, so there is nothing to alarm. + tag := strings.TrimSpace(message.result.Release.Tag) + a.offer.clear() + a.note("codeaf " + tag + " is already the build on disk · this session is untouched") return nil } + tag := strings.TrimSpace(message.result.Release.Tag) + if tag == "" { + tag = a.offer.tag + } + a.offer.ready(a.now()) + a.offer.tag = tag + if a.updateAuto != nil && a.updateAuto.Success != nil { + a.updateAuto.Success() + } + if message.auto { + // THE QUIET ROAD: one line, no checksum, no path, no curl script. Those + // belong to a person who asked for diagnostics. + a.note(a.updateOfferNote()) + return a.offerSettleTick() + } a.note("checksum matched · installed at " + message.result.Path) - a.note("restarting on " + message.result.Release.Tag + "…") - *a.restart = codeupdate.Plan{ - Path: message.result.Path, - Args: codeupdate.RestartArgs(a.updateArgs, a.file), - } - // The final two notes get one frame of their own before the program quits. - // Quitting in this same update would restore the terminal before Bubble Tea - // had painted the checksum and restart lines at all. - return surfaceTick(frameInterval, func(time.Time) tea.Msg { return updateRestartMsg{} }) + a.note(a.updateOfferNote()) + return a.offerSettleTick() +} + +// updateStopped is the one completion a failed install has, on either road. A +// background failure names the error and the manual road in ONE line and keeps +// the old version running; a hand-run one may carry the curl road, because the +// person asked for the details. +func (a *app) updateStopped(auto bool, tag string, err error) tea.Cmd { + a.updateInFlight = false + if reason, refused := refusalReason(err); refused { + // A REFUSAL IS NOT A FAILURE: nothing was downloaded, nothing is + // retried, and the release's automatic attempts are untouched. + a.offer.clear() + a.note(reason) + return nil + } + tag = strings.TrimSpace(tag) + if auto { + if a.updateAuto != nil && a.updateAuto.Failure != nil && tag != "" { + a.updateAuto.Failure(tag, err) + } + a.offer.failed(a.now()) + a.offer.tag = tag + a.note("codeaf " + tag + " could not be installed · this version keeps running · " + err.Error() + " · /update retries") + return a.offerSettleTick() + } + a.offer.clear() + a.note("could not update codeaf: " + err.Error()) + a.note("install a release with: " + a.updateCurlLine()) + return nil } func (a *app) updateFailed(err error) { - a.updateActive = false a.note("could not update codeaf: " + err.Error()) a.note("install a release with: " + a.updateCurlLine()) } diff --git a/internal/tui3/updatecmd_test.go b/internal/tui3/updatecmd_test.go index f031b8f532..92faccb650 100644 --- a/internal/tui3/updatecmd_test.go +++ b/internal/tui3/updatecmd_test.go @@ -25,32 +25,37 @@ func TestC1LaunchAvailabilityBecomesOneTranscriptNote(t *testing.T) { a := newTestApp(&fakeAgent{model: "test/model"}) available := codeupdate.Available{Latest: "v0.2.0", Running: "v0.1.1"} drive(t, a, updateCheckMsg{available: available, show: true}) - line := "codeaf v0.2.0 is out · you have v0.1.1 · /update installs it and restarts · or: " + codeupdate.CurlCommand + line := "codeaf v0.2.0 is out · you have v0.1.1 · /update installs it for the next launch · or: " + codeupdate.CurlCommand if got := strings.Count(updateNotes(a), line); got != 1 { t.Fatalf("launch note count = %d, want 1:\n%s", got, updateNotes(a)) } } -// TestOneUpdateOwnsTheSurfaceUntilItFinishes proves that a second installer -// and a home-page turn cannot begin while release selection is off-frame. -func TestOneUpdateOwnsTheSurfaceUntilItFinishes(t *testing.T) { +// TestASecondUpdateIsRefusedWhileWorkContinues proves one install at a time is +// enforced, and that an install in flight blocks NOTHING else: a turn started +// from home while the download runs is exactly what a background update must +// allow. (The old name of this test was "OneUpdateOwnsTheSurface": it does not.) +func TestASecondUpdateIsRefusedWhileWorkContinues(t *testing.T) { agent := &fakeAgent{model: "test/model"} a := newApp(context.Background(), Options{ Agent: agent, Workspace: "/tmp/lab", UpdateRunning: "v0.1.1", Restart: &codeupdate.Plan{}, ResolveUpdate: func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { return codeupdate.Release{Tag: "v0.2.0"}, nil }, - InstallUpdate: func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) { + InstallUpdate: func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) { return codeupdate.InstallResult{}, nil }, }) first := a.slash("/update") - if first == nil || !a.updateActive { - t.Fatalf("first command = %v, active = %t", first, a.updateActive) + if first == nil || !a.updateInFlight { + t.Fatalf("first command = %v, in flight = %t", first, a.updateInFlight) } if second := a.slash("/upgrade"); second != nil { t.Fatal("the overlapping update returned a command") } + if strings.Count(updateNotes(a), "an update is already running") != 1 { + t.Fatalf("notes do not contain the refusal once:\n%s", updateNotes(a)) + } started := 0 a.start = func(string) (Conversation, error) { started++ @@ -60,25 +65,18 @@ func TestOneUpdateOwnsTheSurfaceUntilItFinishes(t *testing.T) { a.home.box.setText("start another turn") a.home.build() drive(t, a, key("enter")) - if started != 0 { - t.Fatalf("home enter started %d conversations while the update was running", started) - } - if len(agent.sent) != 0 || a.home.box.String() != "start another turn" { - t.Fatalf("sent = %q home draft = %q", agent.sent, a.home.box.String()) - } - for _, want := range []string{"an update is already running", "finish the update before starting a turn"} { - if strings.Count(updateNotes(a), want) != 1 { - t.Fatalf("notes do not contain %q once:\n%s", want, updateNotes(a)) - } + if started != 1 { + t.Fatalf("home enter started %d conversations while an install was in flight", started) } } -// TestAnUpdateOwnsHomesAskHereRoad proves that the home-page chord cannot -// start an errand beneath the restart that an update is preparing. -func TestAnUpdateOwnsHomesAskHereRoad(t *testing.T) { +// TestABackgroundInstallLeavesTheAskHereRoadOpen proves a running download is +// not a lock on the surface: the home chord still starts an errand, which the +// old blocking design refused. +func TestABackgroundInstallLeavesTheAskHereRoadOpen(t *testing.T) { called := 0 a := newTestApp(&fakeAgent{model: "test/model"}) - a.updateActive = true + a.updateInFlight = true a.standingRoot = t.TempDir() a.errand = func(ErrandOrders) (Agent, error) { called++ @@ -92,12 +90,8 @@ func TestAnUpdateOwnsHomesAskHereRoad(t *testing.T) { t.Fatal("the first ask-here chord did not open its composer") } drive(t, a, key("alt+enter")) - - if called != 0 || len(a.exchanges) != 0 { - t.Fatalf("errand calls = %d exchanges = %d", called, len(a.exchanges)) - } - if got := strings.Count(updateNotes(a), "finish the update before starting a turn"); got != 1 { - t.Fatalf("update refusal count = %d, want 1:\n%s", got, updateNotes(a)) + if called == 0 { + t.Fatal("the install in flight stopped home's ask-here road") } } @@ -130,15 +124,15 @@ func TestC6UpdateOnTheNewestBuildDoesNotInstallOrQuit(t *testing.T) { ResolveUpdate: func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { return codeupdate.Release{Tag: "v0.2.0"}, nil }, - InstallUpdate: func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) { + InstallUpdate: func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) { installed = true return codeupdate.InstallResult{}, nil }, }) command := a.slash("/update") drive(t, a, command()) - if installed || a.updateActive || restart.Path != "" || !strings.Contains(updateNotes(a), "you are on the newest codeaf, v0.2.0") { - t.Fatalf("installed = %t active = %t restart = %+v notes:\n%s", installed, a.updateActive, restart, updateNotes(a)) + if installed || a.updateInFlight || restart.Path != "" || !strings.Contains(updateNotes(a), "you are on the newest codeaf, v0.2.0") { + t.Fatalf("installed = %t in flight = %t restart = %+v notes:\n%s", installed, a.updateInFlight, restart, updateNotes(a)) } } @@ -153,7 +147,7 @@ func TestC15ChatUpdateRefusesAnImplicitDowngradeAndAnExactTagInstalls(t *testing ResolveUpdate: func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { return codeupdate.Release{Tag: "v0.2.0"}, nil }, - InstallUpdate: func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) { + InstallUpdate: func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) { installed = true return codeupdate.InstallResult{}, nil }, @@ -161,8 +155,8 @@ func TestC15ChatUpdateRefusesAnImplicitDowngradeAndAnExactTagInstalls(t *testing command := a.slash("/update") drive(t, a, command()) want := "this codeaf is v0.3.0, ahead of the newest stable v0.2.0 — /update v0.2.0 installs it anyway" - if installed || a.updateActive || restart.Path != "" || !strings.Contains(updateNotes(a), want) { - t.Fatalf("installed = %t active = %t restart = %+v notes:\n%s", installed, a.updateActive, restart, updateNotes(a)) + if installed || a.updateInFlight || restart.Path != "" || !strings.Contains(updateNotes(a), want) { + t.Fatalf("installed = %t in flight = %t restart = %+v notes:\n%s", installed, a.updateInFlight, restart, updateNotes(a)) } }) @@ -178,14 +172,17 @@ func TestC15ChatUpdateRefusesAnImplicitDowngradeAndAnExactTagInstalls(t *testing } return codeupdate.Release{Tag: "v0.2.0"}, nil }, - InstallUpdate: func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) { + InstallUpdate: func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) { installed = true return codeupdate.InstallResult{Release: codeupdate.Release{Tag: "v0.2.0"}, Path: "/tmp/codeaf"}, nil }, }) command := a.slash("/update v0.2.0") drive(t, a, command()) - if !installed || restart.Path != "/tmp/codeaf" { + // A HAND-RUN INSTALL IS A BACKGROUND INSTALL: the file is replaced and + // the session keeps running the build it started on, so no restart plan + // is armed at all. + if !installed || restart.Path != "" || !strings.Contains(updateNotes(a), "codeaf v0.2.0 installed") { t.Fatalf("installed = %t restart = %+v notes:\n%s", installed, restart, updateNotes(a)) } }) @@ -206,8 +203,10 @@ func TestUpdateCommandListNamesItsOptionalArgument(t *testing.T) { } } -// TestC7UpdateInstallReturnsTheRestartPlanForThisTranscript proves C7. -func TestC7UpdateInstallReturnsTheRestartPlanForThisTranscript(t *testing.T) { +// TestC7UpdateInstallReplacesTheFileAndKeepsThisSession proves C7 under the +// new law: the file is replaced in the background, the session keeps the build +// it started on, and NO restart is armed. +func TestC7UpdateInstallReplacesTheFileAndKeepsThisSession(t *testing.T) { restart := &codeupdate.Plan{} a := newApp(context.Background(), Options{ Agent: &fakeAgent{model: "test/model"}, Workspace: "/tmp/lab", SessionFile: "/tmp/this.jsonl", @@ -215,16 +214,16 @@ func TestC7UpdateInstallReturnsTheRestartPlanForThisTranscript(t *testing.T) { ResolveUpdate: func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { return codeupdate.Release{Tag: "v0.2.0"}, nil }, - InstallUpdate: func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) { + InstallUpdate: func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) { return codeupdate.InstallResult{Release: codeupdate.Release{Tag: "v0.2.0"}, Path: "/tmp/codeaf"}, nil }, }) command := a.slash("/update") drive(t, a, command()) - if restart.Path != "/tmp/codeaf" || strings.Join(restart.Args, " ") != "chat --model x --session /tmp/this.jsonl" { - t.Fatalf("restart = %+v", restart) + if restart.Path != "" { + t.Fatalf("a background install armed a restart: %+v", restart) } - for _, want := range []string{"downloading codeaf v0.2.0", "checksum matched · installed at /tmp/codeaf", "restarting on v0.2.0"} { + for _, want := range []string{"updating codeaf in the background", "checksum matched \u00b7 installed at /tmp/codeaf", "codeaf v0.2.0 installed"} { if !strings.Contains(updateNotes(a), want) { t.Fatalf("notes do not contain %q:\n%s", want, updateNotes(a)) } @@ -239,30 +238,33 @@ func TestC7UpdateFailureNamesTheCauseAndLeavesNoRestart(t *testing.T) { ResolveUpdate: func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { return codeupdate.Release{Tag: "v0.2.0"}, nil }, - InstallUpdate: func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) { + InstallUpdate: func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) { return codeupdate.InstallResult{}, errors.New("the checksum did not match") }, }) drive(t, a, a.slash("/update")()) - if a.updateActive || restart.Path != "" || !strings.Contains(updateNotes(a), "the checksum did not match") || !strings.Contains(updateNotes(a), codeupdate.CurlCommand) { - t.Fatalf("active = %t restart = %+v notes:\n%s", a.updateActive, restart, updateNotes(a)) + if a.updateInFlight || restart.Path != "" || !strings.Contains(updateNotes(a), "the checksum did not match") || !strings.Contains(updateNotes(a), codeupdate.CurlCommand) { + t.Fatalf("in flight = %t restart = %+v notes:\n%s", a.updateInFlight, restart, updateNotes(a)) } } // TestC8UpdateRefusesWhileATurnOrTaskIsRunning proves C8. -func TestC8UpdateRefusesWhileATurnOrTaskIsRunning(t *testing.T) { +// TestC8AnUpdateInstallsWhileATurnOrTaskIsRunning proves C8 under the new law: +// an install never interrupts work and never refuses because work is happening. +// The file is replaced in the background and the turn or task is untouched. +func TestC8AnUpdateInstallsWhileATurnOrTaskIsRunning(t *testing.T) { for _, kind := range []string{"turn", "task"} { t.Run(kind, func(t *testing.T) { - resolved := false + installed := 0 a := newTestApp(&fakeAgent{model: "test/model"}) a.updateRunning = "v0.1.1" a.restart = &codeupdate.Plan{} a.resolveUpdate = func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { - resolved = true - return codeupdate.Release{}, nil + return codeupdate.Release{Tag: "v0.2.0"}, nil } - a.installUpdate = func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) { - return codeupdate.InstallResult{}, nil + a.installUpdate = func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) { + installed++ + return codeupdate.InstallResult{Release: codeupdate.Release{Tag: "v0.2.0"}, Path: "/tmp/codeaf"}, nil } if kind == "turn" { a.state = stateWorking @@ -270,11 +272,16 @@ func TestC8UpdateRefusesWhileATurnOrTaskIsRunning(t *testing.T) { a.tasks = map[uint64]*taskNode{1: {id: 1, state: session.TaskRunning}} a.taskOrder = []uint64{1} } - if command := a.slash("/update"); command != nil { - t.Fatal("the refusal returned a command") + command := a.slash("/update") + if command == nil { + t.Fatal("an update was refused because work was running") + } + drive(t, a, command()) + if installed != 1 || a.updateInFlight || a.restart.Path != "" { + t.Fatalf("installed = %d in flight = %t restart = %+v notes:\n%s", installed, a.updateInFlight, a.restart, updateNotes(a)) } - if resolved || strings.Count(updateNotes(a), "finish the running turn or task before updating codeaf") != 1 { - t.Fatalf("resolved = %t notes:\n%s", resolved, updateNotes(a)) + if strings.Contains(updateNotes(a), "finish the running turn or task") { + t.Fatalf("the old refusal is still spoken:\n%s", updateNotes(a)) } }) } @@ -304,7 +311,7 @@ func TestV6UpdateChoiceFollowsTheRunningBuildUnlessOverridden(t *testing.T) { got = choice return codeupdate.Release{Tag: row.running}, nil }, - InstallUpdate: func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) { + InstallUpdate: func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) { return codeupdate.InstallResult{}, nil }, }) @@ -336,9 +343,9 @@ func TestV7ChatUpdateRefusesAnAheadChannelBuildUnlessTheTagIsNamed(t *testing.T) } return codeupdate.Release{Tag: newest}, nil }, - InstallUpdate: func(_ context.Context, release codeupdate.Release) (codeupdate.InstallResult, error) { + InstallUpdate: func(_ context.Context, options codeupdate.InstallOptions) (codeupdate.InstallResult, error) { installed++ - return codeupdate.InstallResult{Release: release, Path: "/tmp/devaf"}, nil + return codeupdate.InstallResult{Release: options.Release, Path: "/tmp/devaf"}, nil }, }) } @@ -365,7 +372,7 @@ func TestV8UpdateNotesUseTheRightCurlLine(t *testing.T) { ResolveUpdate: func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { return codeupdate.Release{}, errors.New("release service is away") }, - InstallUpdate: func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) { + InstallUpdate: func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) { return codeupdate.InstallResult{}, nil }, }) @@ -378,7 +385,7 @@ func TestV8UpdateNotesUseTheRightCurlLine(t *testing.T) { Agent: &fakeAgent{model: "test/model"}, Workspace: "/tmp/lab", UpdateRunning: "deadbeef", UpdateCurl: devafCurl, Restart: &codeupdate.Plan{}, ResolveUpdate: func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { return codeupdate.Release{}, nil }, - InstallUpdate: func(context.Context, codeupdate.Release) (codeupdate.InstallResult, error) { + InstallUpdate: func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) { return codeupdate.InstallResult{}, nil }, }) diff --git a/internal/tui3/updatedemo.go b/internal/tui3/updatedemo.go new file mode 100644 index 0000000000..df9f04982f --- /dev/null +++ b/internal/tui3/updatedemo.go @@ -0,0 +1,91 @@ +package tui3 + +import ( + "strings" + + "github.com/Agent-Field/codeaf/internal/session" + codeupdate "github.com/Agent-Field/codeaf/internal/update" +) + +// Deterministic render fixtures for the update offer, on the same terms as the +// question page's (questiondemo.go): a case named in the environment raises the +// REAL state machine so a screenshot can be taken without waiting out a grace +// or paying for a download. It is not a person-facing feature, there is no key +// or command that reaches it, and it writes nothing. +// +// EVERY CASE DISABLES THE UPDATER'S HOOKS, so no fixture can make a request or +// replace a file: they exist to draw the surface, and a capture harness must be +// able to run them on a machine with no network at all. +const updateDemoEnv = "CODEAF_UPDATE_DEMO" + +// openDemoUpdate raises a fixture where the environment names one. +func (a *app) openDemoUpdate(env func(string) string) { + if env == nil { + return + } + name := strings.TrimSpace(env(updateDemoEnv)) + if name == "" { + return + } + // A FIXTURE MAY NOT TOUCH THE WORLD. Clearing every updater seam first means + // even a case that looks like a download cannot start one. + a.updateCheck = nil + a.resolveUpdate = nil + a.installUpdate = nil + a.updateAuto = nil + + const released, running = "v0.9.3", "v0.9.2" + a.updateRunning = running + a.updateCurl = updateFallbackCurl + // THE GREETING IS SPENT, exactly as it is on a second launch: the welcome + // stands where the keys row would, and a fixture whose subject IS that row + // would otherwise be a picture of the greeting instead. + a.welcome = welcome{spent: true} + + switch name { + case "offer": + a.offer.raise(released, running, a.now()) + a.note(a.updateOfferNote()) + case "downloading": + a.offer.downloading(false) + a.offer.tag = released + a.note(a.updateOfferNote()) + case "ready": + a.offer.ready(a.now()) + a.offer.tag = released + a.note(a.updateOfferNote()) + case "ready-working": + // A REPRESENTATIVE RUNNING TURN, drawn from the same events a real one + // sends: a question asked, a tool begun, an answer arriving, and a draft + // still in the box. Nothing here is a promise — the offer is simply + // raised over work that is genuinely in progress. + a.turn = 1 + a.state = stateWorking + a.entries = append(a.entries, entry{kind: entryUser, text: "tighten the retry loop in the sync client"}) + a.ingest(session.Event{Kind: session.EventToolBegin, Tool: "bash", CallID: "fixture-call"}) + a.ingest(session.Event{Kind: session.EventTextDelta, Text: "Reading the retry loop now."}) + a.input.setText("and keep the backoff capped") + a.offer.ready(a.now()) + a.offer.tag = released + a.note(a.updateOfferNote()) + case "manual": + a.offer.downloading(true) + a.offer.tag = released + a.note(a.updateOfferNote()) + case "off": + a.note(quietUpdateNotice(codeupdate.Available{Latest: released, Running: running})) + case "failure": + a.offer.failed(a.now()) + a.offer.tag = released + a.note("codeaf " + released + " could not be installed · the checksum did not match") + a.note("/update tries again when you ask · or install a release with: " + updateFallbackCurl) + } + // EVERY CAPTURE SAYS WHAT IT IS, written LAST so it is the line still on + // screen whichever state the fixture drew. A frame taken from a fixture can + // therefore never be mistaken for a scene a person reached. + a.note("fixture · codeaf update states · CODEAF_UPDATE_DEMO=" + name + " · no network, nothing installed") +} + +// updateFallbackCurl is the road a capture names when no door has handed this +// surface its own line; the real door always does (cmd/codeaf's runSurface). +const updateFallbackCurl = "curl -fsSL https://agentfield.ai/get/codeaf | bash" diff --git a/internal/tui3/updateoffer.go b/internal/tui3/updateoffer.go new file mode 100644 index 0000000000..a248845612 --- /dev/null +++ b/internal/tui3/updateoffer.go @@ -0,0 +1,403 @@ +package tui3 + +import ( + "errors" + "strings" + "time" + + tea "charm.land/bubbletea/v2" + "github.com/charmbracelet/x/ansi" + + codeupdate "github.com/Agent-Field/codeaf/internal/update" +) + +// The launch offer. Three properties hold it together: +// +// - It is one sentence in the line that already carries the idle keys, and its +// only key is a chord. Nothing is modal, no plain letter is bound, and it +// never takes the message box. +// - Auto update is on by default, so doing nothing answers it: the install +// starts after [codeupdate.AutoGrace], counted down in the line. +// - It never restarts anything. The file is replaced in the background; this +// process, its turns and its engines keep running. `/update` typed by hand +// installs the same way. +// +// [updateOffer] is a pure state machine; updatedemo.go raises it deterministically +// for captures. +type UpdateCoordinator struct { + // Enabled is the live value of the `update.auto` row. OFF means nothing is + // downloaded on its own; the launch check still says what is out. + Enabled bool + // State reads the remembered dismissal, failure count and backoff. + State func() codeupdate.AutoState + // Dismiss records "not now" for one exact release. + Dismiss func(tag string) error + // Failure records a failed automatic install and its words. + Failure func(tag string, err error) + // Success clears the failure count belonging to a release now installed. + Success func() + // Disable writes the `update.auto` row off. + Disable func() error + // Refusal names why this executable cannot be replaced in place, or "". + Refusal func() string +} + +// updateSkipKey is the offer's one action: "not now", said about one exact +// release. Install-now is `/update`, discoverable in the command list and in +// the offer's own sentence, and turning the updater off is a settings row. +const updateSkipKey = chordAltWord + "n" + +// updateOfferingDwell is how long a finished or failed install keeps its line +// before the keys row settles back to the ordinary idle keys. The transcript +// note is the durable record; the footer is a moment, not a billboard. +const updateOfferingDwell = 8 * time.Second + +type updatePhase uint8 + +const ( + updateIdle updatePhase = iota + // updateOffering is the grace window: the release is known, the automatic + // answer has not fired, and either the person or the clock will answer. + updateOffering + updateDownloading + // updateReady is an install that finished. A later safe launch opens it. + updateReady + // updateFailed is an automatic install that did not finish; the failure is + // remembered so the same release is not retried every launch. + updateFailed +) + +// updateOffer is the launch offer's whole state. +type updateOffer struct { + phase updatePhase + tag string + running string + deadline time.Time + // settleAt is when the ready or failed line folds away. + settleAt time.Time + // manual is true when a person typed the road rather than letting the offer + // run. It changes the words, not the behaviour. + manual bool +} + +func (o updateOffer) active() bool { return o.phase != updateIdle } + +// raise opens the grace window for a release the launch check found. +func (o *updateOffer) raise(tag, running string, now time.Time) { + o.phase = updateOffering + o.tag = strings.TrimSpace(tag) + o.running = strings.TrimSpace(running) + o.deadline = now.Add(codeupdate.AutoGrace) + o.settleAt = time.Time{} + o.manual = false +} + +func (o *updateOffer) expire() { + if o.phase == updateOffering { + o.phase = updateDownloading + } +} + +func (o *updateOffer) downloading(manual bool) { + o.phase = updateDownloading + o.settleAt = time.Time{} + o.manual = manual +} + +// ready marks an install that landed, and starts the dwell that folds its line. +func (o *updateOffer) ready(now time.Time) { + o.phase = updateReady + o.manual = false + o.settleAt = now.Add(updateOfferingDwell) +} + +// failed marks an install that did not land, with the same dwell. +func (o *updateOffer) failed(now time.Time) { + o.phase = updateFailed + o.manual = false + o.settleAt = now.Add(updateOfferingDwell) +} + +func (o *updateOffer) clear() { *o = updateOffer{} } + +func (o updateOffer) offering() bool { return o.phase == updateOffering } + +// remaining is the countdown shown while the grace is open. +func (o updateOffer) remaining(now time.Time) time.Duration { + if !o.offering() { + return 0 + } + left := o.deadline.Sub(now) + if left < 0 { + return 0 + } + return left.Round(time.Second) +} + +// ── WHAT THE SURFACE SAYS ─────────────────────────────────────────────────── + +// updateOfferHint is the one sentence the offer puts in the keys line: plain +// text at the hint slot's own weight, no colour, no icon. +func (a *app) updateOfferHint(width int) string { + o := a.offer + switch o.phase { + case updateOffering: + lead := "codeaf " + o.tag + " is out" + count := "installs in " + spellGrace(o.remaining(a.now())) + skip := a.chords.say(updateSkipKey) + " skip" + now := "/update" + // THE ACTIONS SURVIVE A NARROW WINDOW: the countdown clause is dropped + // before either action is. + for _, clauses := range [][]string{ + {lead, count, skip, now}, + {lead, skip, now}, + {lead, count, skip}, + {lead, skip}, + {lead, now}, + {lead}, + } { + line := strings.Join(clauses, " · ") + if width > 0 && ansi.StringWidth(line) > width { + continue + } + return line + } + return lead + case updateDownloading: + return "Updating codeaf in the background · keep working" + case updateReady: + return "codeaf " + o.tag + " installed · this session keeps running" + case updateFailed: + return "codeaf " + o.tag + " could not be installed · /update retries" + } + return "" +} + +// spellGrace prints a short count for the keys line. +func spellGrace(left time.Duration) string { + seconds := int(left / time.Second) + if seconds < 0 { + seconds = 0 + } + if seconds <= 1 { + return "1s" + } + return itoa(seconds) + "s" +} + +// updateOfferDefer is the clause appended to a running turn's own keys while +// the grace is open, so the chance to defer survives work in the conversation. +func (a *app) updateOfferDefer() string { + if !a.offer.offering() { + return "" + } + return " · /update skip" +} + +// withUpdateOffer folds the defer clause into whatever the keys line was going +// to say. It adds nothing unless the grace is open. +func (a *app) withUpdateOffer(hint string) string { + return hint + a.updateOfferDefer() +} + +// updateOfferNote is the transcript record: ONE quiet line per meaningful state, +// said once. The footer may fold away; this is what remains. +func (a *app) updateOfferNote() string { + o := a.offer + switch o.phase { + case updateOffering: + shown := o.running + if shown == "" { + shown = "an unstamped build" + } + return "codeaf " + o.tag + " is out · you have " + shown + " · it installs on its own shortly · " + + a.chords.say(updateSkipKey) + " skips · /update installs it" + case updateDownloading: + return "updating codeaf in the background · this session keeps running" + case updateReady: + return "codeaf " + o.tag + " installed · this session keeps running · new windows use the update; existing engines finish their work first, and an attached window keeps its engine current" + case updateFailed: + return "codeaf " + o.tag + " could not be installed · this version keeps running · /update retries" + } + return "" +} + +// quietUpdateNotice is what a launch says when a release is out but the +// automatic updater is off: knowing is still worth one dim line, acting is not +// offered until it is asked for. +func quietUpdateNotice(available codeupdate.Available) string { + running := strings.TrimSpace(available.Running) + if running == "" { + running = "an unstamped build" + } + return "codeaf " + available.Latest + " is out · you have " + running + " · /update installs it for the next launch" +} + +// ── THE ANSWERS ───────────────────────────────────────────────────────────── + +// updateOfferKey reads the offer's one chord and lets every other key past. It +// answers only while the grace is open: alt+n cannot cancel a download already +// running, and pretending otherwise would be the same lie as a dead /update +// skip. +func (a *app) updateOfferKey(msg tea.KeyPressMsg) (tea.Cmd, bool) { + if !a.offer.offering() || msg.String() != updateSkipKey { + return nil, false + } + a.skipUpdateOffer() + return nil, true +} + +// updateNowFromOffer is `/update` while the grace is open: install now rather +// than waiting the countdown out. +func (a *app) updateNowFromOffer() tea.Cmd { + if !a.offer.offering() { + return nil + } + tag := a.offer.tag + a.offer.clear() + a.note("installing codeaf " + tag + " now") + return a.beginUpdate(updateChoice(tag, a.updateRunning), true) +} + +// skipUpdateOffer answers this release "not now", and only while it is being +// offered. The answer is written down, so the tag is never offered again. +func (a *app) skipUpdateOffer() { + if !a.offer.offering() { + return + } + tag := a.offer.tag + a.offer.clear() + if a.updateAuto != nil && a.updateAuto.Dismiss != nil && tag != "" { + _ = a.updateAuto.Dismiss(tag) + } + a.note("codeaf " + tag + " will not be offered again · this release only · /update installs it for the next launch whenever you want") +} + +// declineUpdateOffer turns the automatic updater off for good, reached from the +// settings row and from `/update never`. +func (a *app) declineUpdateOffer() { + a.offer.clear() + if a.updateAuto != nil && a.updateAuto.Disable != nil { + if err := a.updateAuto.Disable(); err != nil { + a.note("could not turn auto update off: " + err.Error()) + return + } + a.updateAuto.Enabled = false + } + a.note("auto update is off · /settings turns it back on · /update still installs a release for the next launch when you ask") +} + +// updateRefusal is why this executable may not be replaced in place, or "". +func (a *app) updateRefusal() string { + if a.updateAuto == nil || a.updateAuto.Refusal == nil { + return "" + } + return strings.TrimSpace(a.updateAuto.Refusal()) +} + +// ── THE CLOCK ─────────────────────────────────────────────────────────────── + +// updateOfferTickMsg is the countdown's own beat, one second apart. +type updateOfferTickMsg struct{} + +// updateGraceMsg is the deadline reached. +type updateGraceMsg struct{} + +// offerSettleMsg folds a ready or failed line away. +type offerSettleMsg struct{} + +func (a *app) offerTick() tea.Cmd { + if !a.offer.offering() { + return nil + } + wait := time.Until(a.offer.deadline) + if wait <= 0 { + return surfaceTick(0, func(time.Time) tea.Msg { return updateGraceMsg{} }) + } + if wait > time.Second { + wait = time.Second + } + return surfaceTick(wait, func(time.Time) tea.Msg { return updateOfferTickMsg{} }) +} + +// tookOfferTick repaints one second of the countdown, and pauses the grace +// while a question page owns the frame: the offer cannot be seen there, so the +// clock does not run out invisibly under it. +func (a *app) tookOfferTick() tea.Cmd { + if !a.offer.offering() { + return nil + } + if a.questionRoomOpen() { + a.offer.deadline = a.now().Add(codeupdate.AutoGrace) + return a.offerTick() + } + if !a.now().Before(a.offer.deadline) { + return a.tookUpdateGrace() + } + a.touch() + return a.offerTick() +} + +// tookUpdateGrace fires the automatic answer. It RE-READS the live setting and +// the persisted memory first: a person who turned auto update off during the +// window, or another window that dismissed the same tag, must not be installed +// over by a decision this session made ten seconds ago. +func (a *app) tookUpdateGrace() tea.Cmd { + if !a.offer.offering() { + return nil + } + tag := a.offer.tag + if a.updateAuto == nil || !a.updateAuto.Enabled { + a.offer.clear() + a.note("auto update is off · nothing was installed · /update installs codeaf " + tag + " when you want it") + return nil + } + if a.updateAuto.State != nil { + state := a.updateAuto.State() + if !codeupdate.ShouldOffer(state, tag, a.now()) { + a.offer.clear() + a.note("codeaf " + tag + " will not be installed on its own · /update installs it when you want it") + return nil + } + } + if refusal := a.updateRefusal(); refusal != "" { + a.offer.clear() + a.note(refusal) + return nil + } + a.offer.expire() + a.note(a.updateOfferNote()) + return a.beginUpdate(updateChoice(tag, a.updateRunning), true) +} + +// offerSettle schedules the fold of a finished line and clears it when due. +func (a *app) offerSettleTick() tea.Cmd { + if a.offer.settleAt.IsZero() { + return nil + } + wait := time.Until(a.offer.settleAt) + if wait < 0 { + wait = 0 + } + return surfaceTick(wait, func(time.Time) tea.Msg { return offerSettleMsg{} }) +} + +func (a *app) tookOfferSettle() tea.Cmd { + if a.offer.phase != updateReady && a.offer.phase != updateFailed { + return nil + } + a.offer.clear() + a.touch() + return nil +} + +// updateRefusalFrom is the message a typed or automatic install failure +// produces, and whether it is a refusal (which is never retried) rather than a +// failed download. +func refusalReason(err error) (string, bool) { + var refusal *codeupdate.RefusalError + if errors.As(err, &refusal) { + return refusal.Reason, true + } + return "", false +} diff --git a/internal/tui3/updateoffer_test.go b/internal/tui3/updateoffer_test.go new file mode 100644 index 0000000000..51060e9e34 --- /dev/null +++ b/internal/tui3/updateoffer_test.go @@ -0,0 +1,376 @@ +package tui3 + +import ( + "context" + "strings" + "testing" + "time" + + codeupdate "github.com/Agent-Field/codeaf/internal/update" +) + +func offerAuto(state *codeupdate.AutoState) *UpdateCoordinator { + return &UpdateCoordinator{ + Enabled: true, + State: func() codeupdate.AutoState { return *state }, + Dismiss: func(tag string) error { state.DismissedRelease = tag; return nil }, + Failure: func(tag string, err error) { state.FailedRelease = tag; state.Failures++ }, + Success: func() { state.FailedRelease = ""; state.Failures = 0 }, + Disable: func() error { return nil }, + } +} + +type offerLab struct { + app *app + installed int + result codeupdate.InstallResult + failWith error + allow *bool +} + +func newOfferLab(t *testing.T, coordinator *UpdateCoordinator) *offerLab { + t.Helper() + lab := &offerLab{allow: new(bool)} + lab.app = newApp(context.Background(), Options{ + Agent: &fakeAgent{model: "test/model"}, Workspace: "/tmp/lab", UpdateRunning: "v0.9.2", + Restart: &codeupdate.Plan{}, UpdateAuto: coordinator, + ResolveUpdate: func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { + return codeupdate.Release{Tag: "v0.9.3"}, nil + }, + InstallUpdate: func(_ context.Context, options codeupdate.InstallOptions) (codeupdate.InstallResult, error) { + *lab.allow = options.AllowDowngrade + if lab.failWith != nil { + return codeupdate.InstallResult{}, lab.failWith + } + lab.installed++ + if lab.result.Release.Tag != "" || lab.result.Already { + return lab.result, nil + } + return codeupdate.InstallResult{Release: options.Release, Path: "/tmp/codeaf"}, nil + }, + }) + return lab +} + +func offerCheck(tag string) updateCheckMsg { + return updateCheckMsg{available: codeupdate.Available{Latest: tag, Running: "v0.9.2"}, show: true} +} + +// TestLaunchOfferInstallsInBackgroundAfterTheGrace proves the default road. +func TestLaunchOfferInstallsInBackgroundAfterTheGrace(t *testing.T) { + state := codeupdate.AutoState{} + lab := newOfferLab(t, offerAuto(&state)) + a := lab.app + drive(t, a, offerCheck("v0.9.3")) + if !a.offer.offering() || lab.installed != 0 { + t.Fatalf("offer = %+v installed = %d", a.offer, lab.installed) + } + drive(t, a, updateGraceMsg{}) + if lab.installed != 1 || a.updateInFlight || a.restart.Path != "" { + t.Fatalf("installed = %d in flight = %t restart = %+v", lab.installed, a.updateInFlight, a.restart) + } + if !strings.Contains(updateNotes(a), "codeaf v0.9.3 installed") { + t.Fatalf("the completion was not said:\n%s", updateNotes(a)) + } + if !a.offer.active() || a.offer.phase != updateReady { + t.Fatalf("the offer did not settle ready: %+v", a.offer) + } + // M2: the ready line folds away; the durable note remains. + drive(t, a, offerSettleMsg{}) + if a.offer.active() { + t.Fatalf("the ready line never folded: %+v", a.offer) + } + if !strings.Contains(updateNotes(a), "codeaf v0.9.3 installed") { + t.Fatalf("the note did not survive the fold:\n%s", updateNotes(a)) + } +} + +// TestSkipOnlyAnswersTheOfferingRelease proves H2 and the wording. +func TestSkipOnlyAnswersTheOfferingRelease(t *testing.T) { + state := codeupdate.AutoState{} + lab := newOfferLab(t, offerAuto(&state)) + a := lab.app + drive(t, a, offerCheck("v0.9.3")) + drive(t, a, key("alt+n")) + if lab.installed != 0 || a.offer.active() || state.DismissedRelease != "v0.9.3" { + t.Fatalf("installed = %d offer = %+v dismissed = %q", lab.installed, a.offer, state.DismissedRelease) + } + if !strings.Contains(updateNotes(a), "will not be offered again") { + t.Fatalf("the skip note is unclear:\n%s", updateNotes(a)) + } +} + +// TestSkipCannotCancelARunningInstall proves the honest answer while a download +// is already in flight. +func TestSkipCannotCancelARunningInstall(t *testing.T) { + state := codeupdate.AutoState{} + lab := newOfferLab(t, offerAuto(&state)) + a := lab.app + drive(t, a, offerCheck("v0.9.3")) + // The grace expiring starts the install; the seam answers on the next drive. + drive(t, a, updateGraceMsg{}) + if lab.installed == 0 { + t.Fatal("the background install did not run") + } + // A person types skip afterwards: it must not claim to have cancelled. + command := a.slash("/update skip") + _ = command + if !strings.Contains(updateNotes(a), "already running") { + t.Fatalf("skip lied about a running install:\n%s", updateNotes(a)) + } +} + +// TestAutoOffShowsOneQuietNoticeAndNeverInstalls proves the quiet indicator. +func TestAutoOffShowsOneQuietNoticeAndNeverInstalls(t *testing.T) { + coordinator := offerAuto(&codeupdate.AutoState{}) + coordinator.Enabled = false + lab := newOfferLab(t, coordinator) + drive(t, lab.app, offerCheck("v0.9.3")) + if lab.app.offer.active() || lab.installed != 0 { + t.Fatalf("offer = %+v installed = %d with auto off", lab.app.offer, lab.installed) + } + if !strings.Contains(updateNotes(lab.app), "codeaf v0.9.3 is out") || !strings.Contains(updateNotes(lab.app), "/update installs it for the next launch") { + t.Fatalf("the quiet notice is missing:\n%s", updateNotes(lab.app)) + } +} + +// TestGraceRechecksTheLiveSettingAndMemory proves H1: a setting turned off, or a +// dismissal written by another window, inside the window must stop the install. +func TestGraceRechecksTheLiveSettingAndMemory(t *testing.T) { + t.Run("setting turned off", func(t *testing.T) { + coordinator := offerAuto(&codeupdate.AutoState{}) + lab := newOfferLab(t, coordinator) + drive(t, lab.app, offerCheck("v0.9.3")) + coordinator.Enabled = false + drive(t, lab.app, updateGraceMsg{}) + if lab.installed != 0 || lab.app.offer.active() { + t.Fatalf("installed %d after the setting went off", lab.installed) + } + if !strings.Contains(updateNotes(lab.app), "auto update is off") { + t.Fatalf("the refusal was silent:\n%s", updateNotes(lab.app)) + } + }) + t.Run("dismissed elsewhere", func(t *testing.T) { + state := codeupdate.AutoState{} + lab := newOfferLab(t, offerAuto(&state)) + drive(t, lab.app, offerCheck("v0.9.3")) + // Another window writes the dismissal while our grace runs. + state.DismissedRelease = "v0.9.3" + drive(t, lab.app, updateGraceMsg{}) + if lab.installed != 0 || lab.app.offer.active() { + t.Fatalf("installed %d after another window dismissed it", lab.installed) + } + if !strings.Contains(updateNotes(lab.app), "will not be installed on its own") { + t.Fatalf("the refusal was silent:\n%s", updateNotes(lab.app)) + } + }) +} + +// TestOfferHintCountsDownAndKeepsItsActionsAtEightyColumns proves M3/M4 on the +// footer the person actually reads. +func TestOfferHintCountsDownAndKeepsItsActionsAtEightyColumns(t *testing.T) { + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + a := lab.app + fixed := time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC) + a.clock = func() time.Time { return fixed } + a.offer.raise("v0.9.3", "v0.9.2", fixed) + line := a.updateOfferHint(80) + for _, want := range []string{a.chords.say(updateSkipKey), "/update", "10s"} { + if !strings.Contains(line, want) { + t.Fatalf("the 80-column offer dropped %q:\n%s", want, line) + } + } + // The count is live, not frozen: three seconds later it reads 7s. + a.clock = func() time.Time { return fixed.Add(3 * time.Second) } + if later := a.updateOfferHint(80); !strings.Contains(later, "7s") { + t.Fatalf("the countdown did not move:\n%s", later) + } + // The opening note names the grace once, for the record. + if note := a.updateOfferNote(); !strings.Contains(note, "installs on its own shortly") { + t.Fatalf("the note lost the grace:\n%s", note) + } +} + +// TestTheDeferClauseIsDroppedBeforeTheTurnsOwnKeys proves M3. +func TestTheDeferClauseIsDroppedBeforeTheTurnsOwnKeys(t *testing.T) { + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + a := lab.app + a.offer.raise("v0.9.3", "v0.9.2", a.now()) + composed := a.withUpdateOffer("esc interrupt · ctrl+g backgrounds") + if !strings.Contains(composed, "/update skip") { + t.Fatalf("the defer clause is not composed:\n%s", composed) + } + shortened := a.hintShorter(composed) + if shortened != a.runHint() { + t.Fatalf("shortening dropped more than the offer clause: %q", shortened) + } + if strings.Contains(shortened, "/update skip") { + t.Fatalf("the offer clause was not the first to go: %q", shortened) + } +} + +// TestPlainLettersNeverAnswerTheOffer proves the composer keeps its typing. +func TestPlainLettersNeverAnswerTheOffer(t *testing.T) { + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + a := lab.app + drive(t, a, offerCheck("v0.9.3")) + drive(t, a, key("y")) + drive(t, a, key("n")) + if got := a.input.String(); got != "yn" { + t.Fatalf("the offer ate the typing: %q", got) + } + if !a.offer.offering() || lab.installed != 0 { + t.Fatalf("a letter answered the offer: %+v installed = %d", a.offer, lab.installed) + } +} + +// TestARefusalIsQuietAndNeverCounted proves the typed refusal path. +func TestARefusalIsQuietAndNeverCounted(t *testing.T) { + state := codeupdate.AutoState{} + lab := newOfferLab(t, offerAuto(&state)) + lab.failWith = &codeupdate.RefusalError{Reason: "codeaf v0.9.4 is already the build on disk, newer than v0.9.3"} + drive(t, lab.app, offerCheck("v0.9.3")) + drive(t, lab.app, updateGraceMsg{}) + if state.Failures != 0 || state.FailedRelease != "" { + t.Fatalf("a refusal was counted as a failure: %+v", state) + } + if !strings.Contains(updateNotes(lab.app), "newer than v0.9.3") { + t.Fatalf("the refusal was not said:\n%s", updateNotes(lab.app)) + } +} + +// TestAnAlreadyInstalledReleaseIsQuietSuccess proves the no-op wording. +func TestAnAlreadyInstalledReleaseIsQuietSuccess(t *testing.T) { + state := codeupdate.AutoState{} + lab := newOfferLab(t, offerAuto(&state)) + lab.result = codeupdate.InstallResult{Release: codeupdate.Release{Tag: "v0.9.3"}, Path: "/tmp/codeaf", Already: true} + drive(t, lab.app, offerCheck("v0.9.3")) + drive(t, lab.app, updateGraceMsg{}) + if !strings.Contains(updateNotes(lab.app), "already the build on disk") { + t.Fatalf("the no-op was not said:\n%s", updateNotes(lab.app)) + } + if lab.app.offer.active() { + t.Fatalf("a no-op raised an offer: %+v", lab.app.offer) + } +} + +// TestOnlyANamedTagAllowsADowngrade proves the automatic road never rolls back. +func TestOnlyANamedTagAllowsADowngrade(t *testing.T) { + // The automatic road: no downgrade permission. + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + drive(t, lab.app, offerCheck("v0.9.3")) + drive(t, lab.app, updateGraceMsg{}) + if *lab.allow { + t.Fatal("the automatic road asked to be allowed to downgrade") + } + // A typed exact tag: the person asked for that release by name. + named := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + named.app.resolveUpdate = func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { + return codeupdate.Release{Tag: "v0.9.0"}, nil + } + drive(t, named.app, named.app.slash("/update v0.9.0")()) + if !*named.allow || named.installed != 1 { + t.Fatalf("an explicit tag did not allow its own rollback: allow = %t installed = %d", *named.allow, named.installed) + } +} + +// TestThePackagedInstallIsRefusedBeforeAnyOffer proves the ownership exclusion. +func TestThePackagedInstallIsRefusedBeforeAnyOffer(t *testing.T) { + coordinator := offerAuto(&codeupdate.AutoState{}) + coordinator.Refusal = func() string { return "this codeaf is managed by Homebrew" } + lab := newOfferLab(t, coordinator) + drive(t, lab.app, offerCheck("v0.9.3")) + if lab.app.offer.active() || lab.installed != 0 { + t.Fatalf("offer = %+v installed = %d for a managed file", lab.app.offer, lab.installed) + } + if command := lab.app.slash("/update"); command != nil || lab.installed != 0 { + t.Fatalf("the typed road installed a managed file: %v", command) + } + if !strings.Contains(updateNotes(lab.app), "managed by Homebrew") { + t.Fatalf("the refusal was not said:\n%s", updateNotes(lab.app)) + } +} + +// TestUpdateDemoFixturesDrawEveryState is the deterministic render entrypoint. +func TestUpdateDemoFixturesDrawEveryState(t *testing.T) { + for _, name := range []string{"offer", "downloading", "ready", "ready-working", "manual", "off", "failure"} { + t.Run(name, func(t *testing.T) { + a := newTestApp(&fakeAgent{model: "test/model"}) + a.openDemoUpdate(func(key string) string { + if key == updateDemoEnv { + return name + } + return "" + }) + if a.updateCheck != nil || a.resolveUpdate != nil || a.installUpdate != nil || a.updateAuto != nil { + t.Fatal("a fixture left an updater hook armed") + } + if !strings.Contains(updateNotes(a), "fixture · codeaf update states") || + !strings.Contains(updateNotes(a), "CODEAF_UPDATE_DEMO="+name) { + t.Fatalf("the fixture is not labelled:\n%s", updateNotes(a)) + } + switch name { + case "offer": + if !a.offer.offering() || !strings.Contains(a.updateOfferHint(120), "is out") { + t.Fatalf("offer fixture = %+v", a.offer) + } + home := plain(a.homeFootLine(80, a.pal)) + for _, want := range []string{a.chords.say(updateSkipKey), "/update"} { + if !strings.Contains(home, want) { + t.Fatalf("home's 80-column keys row dropped %q: %q", want, home) + } + } + case "downloading", "manual": + if a.offer.phase != updateDownloading || (name == "manual") != a.offer.manual { + t.Fatalf("download fixture = %+v", a.offer) + } + case "ready": + if a.offer.phase != updateReady || !strings.Contains(updateNotes(a), "installed") { + t.Fatalf("ready fixture = %+v", a.offer) + } + case "ready-working": + if a.offer.phase != updateReady || a.state != stateWorking || a.input.String() == "" { + t.Fatalf("working fixture state = %v offer = %+v draft = %q", a.state, a.offer, a.input.String()) + } + case "off": + if a.offer.active() || !strings.Contains(updateNotes(a), "is out") { + t.Fatalf("off fixture = %+v", a.offer) + } + case "failure": + if a.offer.phase != updateFailed { + t.Fatalf("failure fixture = %+v", a.offer) + } + } + }) + } +} + +// TestEveryUpdateCommandWeShowIsParsed guards the copy: a hint or note may only +// print commands the parser accepts as themselves rather than as a tag. +func TestEveryUpdateCommandWeShowIsParsed(t *testing.T) { + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + a := lab.app + a.offer.raise("v0.9.3", "v0.9.2", a.now()) + text := strings.Join([]string{ + a.updateOfferHint(120), + a.updateOfferNote(), + a.withUpdateOffer("esc interrupt"), + quietUpdateNotice(codeupdate.Available{Latest: "v0.9.3", Running: "v0.9.2"}), + }, "\n") + for _, field := range strings.Fields(text) { + field = strings.Trim(field, "·,;") + if !strings.HasPrefix(field, "/update") { + continue + } + argument := strings.TrimSpace(strings.TrimPrefix(field, "/update")) + switch argument { + case "", "skip", "never": + // The three forms the parser answers as themselves. + default: + t.Fatalf("the surface prints %q, which the parser would read as a tag", field) + } + } + if !strings.Contains(text, "/update") { + t.Fatal("no /update road is shown at all") + } +} diff --git a/internal/tui3/updateplatform.go b/internal/tui3/updateplatform.go index e8bc8347d8..cb916a28c7 100644 --- a/internal/tui3/updateplatform.go +++ b/internal/tui3/updateplatform.go @@ -6,3 +6,10 @@ var ( runtimeGOOS = runtime.GOOS runtimeGOARCH = runtime.GOARCH ) + +// runtimePlatform names the build a release download would fetch, as the +// installer spells it (`linux/amd64`). It is one function because the offer's +// line and its note must spell the platform identically. +func runtimePlatform() string { + return runtimeGOOS + "/" + runtimeGOARCH +} diff --git a/internal/update/auto.go b/internal/update/auto.go new file mode 100644 index 0000000000..e1d2efcd9c --- /dev/null +++ b/internal/update/auto.go @@ -0,0 +1,401 @@ +package update + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/Agent-Field/codeaf/internal/config" +) + +// The auto updater's durable memory: one small file in the profile holding the +// release a person dismissed, the release that failed and how often, and the +// backoff after that failure. An unreadable file reads as empty state, because +// the worst case of forgetting is one repeated offer. +const ( + // AutoGrace is how long a launch offer waits before the automatic answer. + // Nothing is blocked while it runs. + AutoGrace = 10 * time.Second + // maxAutoFailures stops automatic retries of one release. Manual /update + // ignores it. + maxAutoFailures = 3 + autoRetryBase = 6 * time.Hour + autoRetryCap = 72 * time.Hour + + autoStateName = "update-state.json" +) + +// AutoState is what the background updater remembers between launches. +type AutoState struct { + // DismissedRelease is the tag a person said no to, by chord or by command. + DismissedRelease string `json:"dismissed_release,omitempty"` + DismissedAt time.Time `json:"dismissed_at,omitempty"` + // FailedRelease, Failures and LastAttemptAt bound automatic retries. + FailedRelease string `json:"failed_release,omitempty"` + Failures int `json:"failures,omitempty"` + LastAttemptAt time.Time `json:"last_attempt_at,omitempty"` + LastError string `json:"last_error,omitempty"` +} + +// Dismissed reports whether this exact release was answered "not now". +func (s AutoState) Dismissed(tag string) bool { + tag = strings.TrimSpace(tag) + return tag != "" && tag == strings.TrimSpace(s.DismissedRelease) +} + +// Blocked reports whether automatic installation of this release has been tried +// too many times already. +func (s AutoState) Blocked(tag string) bool { + tag = strings.TrimSpace(tag) + return tag != "" && tag == strings.TrimSpace(s.FailedRelease) && s.Failures >= maxAutoFailures +} + +// RetryDue reports whether a failed release may be tried again now. +func (s AutoState) RetryDue(tag string, now time.Time) bool { + tag = strings.TrimSpace(tag) + if tag == "" || tag != strings.TrimSpace(s.FailedRelease) || s.Failures == 0 { + return true + } + if s.LastAttemptAt.IsZero() { + return true + } + return !now.Before(s.LastAttemptAt.Add(autoBackoff(s.Failures))) +} + +// autoBackoff is the quiet period after a failed automatic install. +func autoBackoff(failures int) time.Duration { + if failures < 1 { + return 0 + } + backoff := autoRetryBase + for i := 1; i < failures; i++ { + backoff *= 2 + } + if backoff > autoRetryCap { + return autoRetryCap + } + return backoff +} + +// ShouldOffer reports whether the automatic updater may offer this release now: +// not dismissed, attempts not exhausted, and any backoff passed. A manual door +// ignores all three. +func ShouldOffer(state AutoState, tag string, now time.Time) bool { + tag = strings.TrimSpace(tag) + if tag == "" { + return false + } + return !state.Dismissed(tag) && !state.Blocked(tag) && state.RetryDue(tag, now) +} + +// AutoStatePath names the coordinator's file in the profile. +func AutoStatePath(profileDir string) string { + return config.ProfilePath(profileDir, autoStateName) +} + +// LoadAutoState reads the coordinator's file; a missing or corrupt one is empty. +func LoadAutoState(profileDir string) AutoState { + raw, err := os.ReadFile(AutoStatePath(profileDir)) + if err != nil { + return AutoState{} + } + var state AutoState + if json.Unmarshal(raw, &state) != nil { + return AutoState{} + } + return state +} + +// SaveAutoState writes the coordinator's file atomically. +func SaveAutoState(profileDir string, state AutoState) error { + path := AutoStatePath(profileDir) + if strings.TrimSpace(path) == "" { + return nil + } + raw, err := json.Marshal(state) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return err + } + temporary, err := os.CreateTemp(filepath.Dir(path), ".update-state-*.json") + if err != nil { + return err + } + temporaryPath := temporary.Name() + keep := false + defer func() { + if !keep { + _ = os.Remove(temporaryPath) + } + }() + if err := temporary.Chmod(0o600); err != nil { + _ = temporary.Close() + return err + } + if _, err := temporary.Write(append(raw, '\n')); err != nil { + _ = temporary.Close() + return err + } + if err := temporary.Close(); err != nil { + return err + } + if err := os.Rename(temporaryPath, path); err != nil { + return err + } + keep = true + return nil +} + +// DismissRelease records "not now" for one exact release. +func DismissRelease(profileDir, tag string) error { + tag = strings.TrimSpace(tag) + if tag == "" { + return errors.New("cannot dismiss an unnamed release") + } + state := LoadAutoState(profileDir) + state.DismissedRelease = tag + state.DismissedAt = time.Now() + return SaveAutoState(profileDir, state) +} + +// AutoFailure records one failed automatic install of a release. +func AutoFailure(profileDir, tag string, err error) AutoState { + tag = strings.TrimSpace(tag) + state := LoadAutoState(profileDir) + if strings.TrimSpace(state.FailedRelease) != tag { + state.FailedRelease = tag + state.Failures = 0 + } + state.Failures++ + state.LastAttemptAt = time.Now() + state.LastError = "" + if err != nil { + state.LastError = err.Error() + } + _ = SaveAutoState(profileDir, state) + return state +} + +// AutoSuccess clears the failure belonging to a release now installed. +func AutoSuccess(profileDir string) AutoState { + state := LoadAutoState(profileDir) + state.FailedRelease = "" + state.Failures = 0 + state.LastAttemptAt = time.Time{} + state.LastError = "" + _ = SaveAutoState(profileDir, state) + return state +} + +// ── ONE INSTALLER PER EXECUTABLE ──────────────────────────────────────────── +// +// The lock is keyed on the executable target, not on a profile: two profiles +// can share one codeaf file and it is the file that must not be replaced twice +// at once. It is an OS advisory lock, so a process that dies releases it and +// nothing has to be taken over by age. +// +// The lock file also carries a record of the last successful install: the tag, +// the sha256 of the bytes written and when the release was published. The +// sha256 is what makes the record TRUSTWORTHY — it is believed only while the +// file still hashes to it, so a binary replaced by curl, a package manager or a +// person reads as unknown rather than as some older tag. + +// ErrInstallInFlight means another process holds the target's lock. +var ErrInstallInFlight = errors.New("another codeaf is already installing an update") + +// RefusalError is a typed refusal: this target will not be replaced in place, +// and Reason is the sentence to show. It is not a download or write failure, so +// it is never retried and never counted against a release's automatic attempts. +type RefusalError struct { + Reason string +} + +func (e *RefusalError) Error() string { return e.Reason } + +// InstallLock is a held lock on one executable target. +type InstallLock struct { + target string + file *os.File +} + +type installRecord struct { + Tag string `json:"tag,omitempty"` + SHA256 string `json:"sha256,omitempty"` + PublishedAt time.Time `json:"published_at,omitempty"` + InstalledAt time.Time `json:"installed_at,omitempty"` +} + +// installLockPath names the lock-and-record file for a target. It sits beside +// the executable so a target directory that is not writable (which would fail +// the install anyway) cannot be the reason two installers run; the file is left +// in place after release, because unlinking it would let a fresh opener create +// a second inode and hold a second lock. +func installLockPath(target string) string { + target = filepath.Clean(strings.TrimSpace(target)) + return target + ".install.lock" +} + +// TryTargetLock claims the lock for one executable target, returning +// ErrInstallInFlight when another process holds it. +func TryTargetLock(target string) (*InstallLock, error) { + target = strings.TrimSpace(target) + if target == "" { + return nil, errors.New("the running executable path is empty") + } + file, err := os.OpenFile(installLockPath(target), os.O_CREATE|os.O_RDWR, 0o600) + if err != nil { + return nil, err + } + if !lockFileExclusive(file) { + _ = file.Close() + return nil, ErrInstallInFlight + } + return &InstallLock{target: target, file: file}, nil +} + +// Record reads the last install this target recorded, and reports false unless +// the record still matches the file on disk. A tag written by bytes that are no +// longer there is not a fact about the file. +func (l *InstallLock) Record() (installRecord, bool) { + if l == nil || l.file == nil { + return installRecord{}, false + } + raw, err := os.ReadFile(l.file.Name()) + if err != nil { + return installRecord{}, false + } + var record installRecord + if json.Unmarshal(raw, &record) != nil || record.Tag == "" || record.SHA256 == "" { + return installRecord{}, false + } + actual, err := sha256File(l.target) + if err != nil || !strings.EqualFold(actual, record.SHA256) { + return installRecord{}, false + } + return record, true +} + +// RecordInstalled writes what this target now holds. The caller holds the lock +// and has just written those bytes. +func (l *InstallLock) RecordInstalled(tag, digest string, published time.Time) { + if l == nil || l.file == nil { + return + } + record := installRecord{ + Tag: strings.TrimSpace(tag), SHA256: strings.ToLower(strings.TrimSpace(digest)), + PublishedAt: published.UTC(), InstalledAt: time.Now().UTC(), + } + raw, err := json.Marshal(record) + if err != nil { + return + } + if _, err := l.file.Seek(0, 0); err != nil { + return + } + if err := l.file.Truncate(0); err != nil { + return + } + if _, err := l.file.Write(append(raw, '\n')); err != nil { + return + } + _ = l.file.Sync() +} + +// Release drops the lock. It is safe on a nil lock and safe to call twice. +func (l *InstallLock) Release() { + if l == nil || l.file == nil { + return + } + unlockFile(l.file) + _ = l.file.Close() + l.file = nil +} + +func sha256File(path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", err + } + defer file.Close() + sum := sha256.New() + if _, err := io.Copy(sum, file); err != nil { + return "", err + } + return hex.EncodeToString(sum.Sum(nil)), nil +} + +// recordSupersedes reports whether the build on disk is newer than the release +// this process resolved. It uses PUBLISHED ordering where the facts exist — the +// same CompareChannelBuilds the launch check uses — and falls back to the tag +// grammar only when it must; two tags it cannot order are not guessed at. +func recordSupersedes(record installRecord, release Release) bool { + installed, candidate := strings.TrimSpace(record.Tag), strings.TrimSpace(release.Tag) + if installed == "" || candidate == "" { + return false + } + if installed == candidate { + return true + } + installedChannel, installedDate, installedOK := ParseChannel(installed) + candidateChannel, candidateDate, candidateOK := ParseChannel(candidate) + if installedOK && candidateOK && installedChannel == candidateChannel { + if !record.PublishedAt.IsZero() && !release.PublishedAt.IsZero() { + comparison, ok := CompareChannelBuilds(installed, record.PublishedAt, candidate, release.PublishedAt) + return ok && comparison > 0 + } + return installedDate > candidateDate + } + if comparison, ok := CompareSemverTags(installed, candidate); ok { + return comparison > 0 + } + return false +} + +// InstallRefusal names why this target may not be replaced in place — a package +// manager owns it, or its directory is not writable — or "" when an ordinary +// install may proceed. The curl line is the road the refusal offers instead. +func InstallRefusal(target, curl string) string { + target = strings.TrimSpace(target) + if manager := PackageManaged(target); manager != "" { + return "this codeaf is managed by " + manager + " · update it there, or install a release with: " + curl + } + if dir := filepath.Dir(target); dir != "" && !dirWritable(dir) { + return "this codeaf is in a folder this account cannot write · reinstall it as that owner, or with: " + curl + } + return "" +} + +// PackageManaged names the package manager that owns a path, or "" when the +// path is an ordinary install this program may replace. A brew, distro or nix +// install must go through its manager, not through an in-place replacement that +// the next upgrade would quietly undo. +func PackageManaged(target string) string { + path := filepath.ToSlash(filepath.Clean(strings.TrimSpace(target))) + if path == "" || path == "/" { + return "" + } + prefixes := []struct{ prefix, manager string }{ + {"/opt/homebrew/", "Homebrew"}, + {"/usr/local/Cellar/", "Homebrew"}, + {"/nix/store/", "Nix"}, + {"/usr/bin/", "the system package manager"}, + {"/usr/sbin/", "the system package manager"}, + {"/bin/", "the system package manager"}, + {"/sbin/", "the system package manager"}, + {"/usr/lib/", "the system package manager"}, + } + for _, row := range prefixes { + if strings.HasPrefix(path, row.prefix) { + return row.manager + } + } + return "" +} diff --git a/internal/update/auto_test.go b/internal/update/auto_test.go new file mode 100644 index 0000000000..b6b3b7623e --- /dev/null +++ b/internal/update/auto_test.go @@ -0,0 +1,234 @@ +package update + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "os" + "path/filepath" + "testing" + "time" +) + +// TestDismissalAnswersOneReleaseAndNotTheNext is the heart of "never nags". +func TestDismissalAnswersOneReleaseAndNotTheNext(t *testing.T) { + dir := t.TempDir() + if err := DismissRelease(dir, "v0.2.0"); err != nil { + t.Fatalf("dismiss: %v", err) + } + state := LoadAutoState(dir) + if !state.Dismissed("v0.2.0") { + t.Fatal("the dismissed release is not remembered") + } + if state.Dismissed("v0.3.0") { + t.Fatal("a dismissal answered a release it was never about") + } + now := time.Now() + if ShouldOffer(state, "v0.2.0", now) { + t.Fatal("the same release was offered again") + } + if !ShouldOffer(state, "v0.3.0", now) { + t.Fatal("a newer release was not offered") + } +} + +// TestFailuresThrottleAndThenStop proves the bounded retries. +func TestFailuresThrottleAndThenStop(t *testing.T) { + dir := t.TempDir() + now := time.Now() + failure := errors.New("the release host is away") + + for attempt := 1; attempt <= maxAutoFailures; attempt++ { + state := AutoFailure(dir, "v0.2.0", failure) + if state.Failures != attempt { + t.Fatalf("attempt %d recorded %d failures", attempt, state.Failures) + } + } + state := LoadAutoState(dir) + if !state.Blocked("v0.2.0") { + t.Fatal("the exhausted release is still not blocked") + } + if ShouldOffer(state, "v0.2.0", now) { + t.Fatal("a release that failed out is still offered") + } + // A different release starts its own count, and a success clears the whole + // memory so the next release is tried cleanly. + if state.Blocked("v0.3.0") { + t.Fatal("one release's failures blocked another") + } + cleared := AutoSuccess(dir) + if cleared.Failures != 0 || cleared.Blocked("v0.2.0") { + t.Fatalf("success left %+v", cleared) + } +} + +// TestBackoffGrowsAndCaps proves the quiet period after a failure. +func TestBackoffGrowsAndCaps(t *testing.T) { + if got := autoBackoff(1); got != autoRetryBase { + t.Fatalf("first backoff = %s", got) + } + if got := autoBackoff(2); got != 2*autoRetryBase { + t.Fatalf("second backoff = %s", got) + } + if got := autoBackoff(12); got != autoRetryCap { + t.Fatalf("capped backoff = %s", got) + } + now := time.Now() + state := AutoState{FailedRelease: "v0.2.0", Failures: 1, LastAttemptAt: now} + if state.RetryDue("v0.2.0", now.Add(time.Minute)) { + t.Fatal("a fresh failure was due again immediately") + } + if !state.RetryDue("v0.2.0", now.Add(autoRetryBase+time.Minute)) { + t.Fatal("a failure was never due again") + } + if !state.RetryDue("v0.3.0", now) { + t.Fatal("an unrelated release inherited a backoff") + } +} + +// TestCorruptStateReadsAsFresh proves a damaged file never breaks a launch. +func TestCorruptStateReadsAsFresh(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(AutoStatePath(dir), []byte("{not json"), 0o600); err != nil { + t.Fatal(err) + } + if state := LoadAutoState(dir); state.Dismissed("v0.2.0") || state.Failures != 0 { + t.Fatalf("corrupt state = %+v", state) + } +} + +// TestInstallLockIsOnePerExecutable proves the cross-terminal, cross-profile +// deduplication, the record of what is on disk, and that a released lock frees +// the target at once. There is no stale-takeover case to test: the OS releases +// the lock when its holder dies. +func TestInstallLockIsOnePerExecutable(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "codeaf") + first, err := TryTargetLock(target) + if err != nil { + t.Fatalf("first lock: %v", err) + } + if _, err := TryTargetLock(target); !errors.Is(err, ErrInstallInFlight) { + t.Fatalf("second lock error = %v, want in flight", err) + } + // The record lives inside the lock and is trusted only while the file + // still hashes to it. + digest := sha256.Sum256([]byte("installed bytes")) + hexDigest := hex.EncodeToString(digest[:]) + if err := os.WriteFile(target, []byte("installed bytes"), 0o755); err != nil { + t.Fatal(err) + } + first.RecordInstalled("v0.2.0", hexDigest, time.Now()) + if record, ok := first.Record(); !ok || record.Tag != "v0.2.0" { + t.Fatalf("record = %+v, ok = %t", record, ok) + } + first.Release() + second, err := TryTargetLock(target) + if err != nil { + t.Fatalf("relock: %v", err) + } + if record, ok := second.Record(); !ok || record.Tag != "v0.2.0" { + t.Fatalf("the record did not survive release: %+v, ok = %t", record, ok) + } + // AN EXTERNALLY REPLACED FILE INVALIDATES THE RECORD. + if err := os.WriteFile(target, []byte("someone else's build"), 0o755); err != nil { + t.Fatal(err) + } + if record, ok := second.Record(); ok { + t.Fatalf("a replaced file still trusted %+v", record) + } + second.Release() + + // A different target is a different lock and a different record. + other := filepath.Join(dir, "devaf") + otherLock, err := TryTargetLock(other) + if err != nil { + t.Fatalf("unrelated target: %v", err) + } + if _, ok := otherLock.Record(); ok { + t.Fatal("a fresh target trusted a record") + } + otherLock.Release() +} + +// TestRecordSupersedesUsesPublishedOrdering proves the stale-caller guard and +// the same-day dev case M1 asked about. +func TestRecordSupersedesUsesPublishedOrdering(t *testing.T) { + older := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC) + newer := older.Add(6 * time.Hour) + for _, row := range []struct { + name string + record installRecord + release Release + want bool + }{ + {"stable newer", installRecord{Tag: "v0.3.0"}, Release{Tag: "v0.2.0"}, true}, + {"same tag", installRecord{Tag: "v0.2.0"}, Release{Tag: "v0.2.0"}, true}, + {"stable older", installRecord{Tag: "v0.2.0"}, Release{Tag: "v0.3.0"}, false}, + {"same-day dev, later publish", installRecord{Tag: "dev-20261006-bbbbbbbbbbbb", PublishedAt: newer}, Release{Tag: "dev-20261006-aaaaaaaaaaaa", PublishedAt: older}, true}, + {"same-day dev, earlier publish", installRecord{Tag: "dev-20261006-aaaaaaaaaaaa", PublishedAt: older}, Release{Tag: "dev-20261006-bbbbbbbbbbbb", PublishedAt: newer}, false}, + {"unorderable", installRecord{Tag: "dev-20261006-aaaaaaaaaaaa"}, Release{Tag: "staging-20261007-bbbbbbbbbbbb"}, false}, + } { + t.Run(row.name, func(t *testing.T) { + if got := recordSupersedes(row.record, row.release); got != row.want { + t.Fatalf("recordSupersedes(%+v, %s) = %t", row.record, row.release.Tag, got) + } + }) + } +} + +// TestPackageManagedNamesTheOwner proves the exclusion list. +func TestPackageManagedNamesTheOwner(t *testing.T) { + for _, row := range []struct{ path, want string }{ + {"/opt/homebrew/bin/codeaf", "Homebrew"}, + {"/nix/store/abc-codeaf/bin/codeaf", "Nix"}, + {"/usr/bin/codeaf", "the system package manager"}, + {"/home/somebody/.local/bin/codeaf", ""}, + {"/tmp/codeaf", ""}, + } { + if got := PackageManaged(row.path); got != row.want { + t.Errorf("PackageManaged(%q) = %q, want %q", row.path, got, row.want) + } + } +} + +// TestInstallRefusesADuplicateAndAStaleDowngrade proves H4/M1 at the shared +// installer: the record is tied to the bytes on disk, a duplicate is a quiet +// no-op, an automatic stale downgrade is refused, and a NAMED tag may roll back. +func TestInstallRefusesADuplicateAndAStaleDowngrade(t *testing.T) { + asset := []byte("codeaf v0.9.3 bytes") + digest := sha256.Sum256(asset) + server, _ := servedRelease(t, asset, hex.EncodeToString(digest[:])) + defer server.Close() + target := filepath.Join(t.TempDir(), "codeaf") + + install := func(tag string, allow bool) (InstallResult, error) { + return Install(context.Background(), InstallOptions{ + Client: releaseClient(server, tag), Release: Release{Tag: tag, Repository: primaryRepository}, + Target: target, Curl: CurlCommand, AllowDowngrade: allow, + }) + } + if _, err := install("v0.9.3", false); err != nil { + t.Fatalf("first install: %v", err) + } + // Same release again: SUCCESS, and nothing to alarm about. + again, err := install("v0.9.3", false) + if err != nil || !again.Already { + t.Fatalf("duplicate install = %+v, %v", again, err) + } + // An automatic older candidate is refused... + if _, err := install("v0.9.2", false); err == nil { + t.Fatal("an automatic downgrade was allowed") + } else { + var refusal *RefusalError + if !errors.As(err, &refusal) { + t.Fatalf("downgrade error = %v, want a refusal", err) + } + } + // ...but a person naming the tag is doing a rollback and it proceeds. + rolled, err := install("v0.9.2", true) + if err != nil || rolled.Already { + t.Fatalf("named rollback = %+v, %v", rolled, err) + } +} diff --git a/internal/update/check.go b/internal/update/check.go index 8e7f9430f1..6c193fa29f 100644 --- a/internal/update/check.go +++ b/internal/update/check.go @@ -64,7 +64,7 @@ func (a Available) Notice() string { if curl == "" { curl = CurlCommand } - return "codeaf " + a.Latest + " is out · you have " + a.Running + " · /update installs it and restarts · or: " + curl + return "codeaf " + a.Latest + " is out · you have " + a.Running + " · /update installs it for the next launch · or: " + curl } type checkCache struct { diff --git a/internal/update/dirwrite_unix.go b/internal/update/dirwrite_unix.go new file mode 100644 index 0000000000..c54c60bcb8 --- /dev/null +++ b/internal/update/dirwrite_unix.go @@ -0,0 +1,21 @@ +//go:build !windows + +package update + +import ( + "os" + + "golang.org/x/sys/unix" +) + +// dirWritable reports whether this account may create files in dir, which is +// the same permission an in-place replacement needs. +func dirWritable(dir string) bool { + // A DIRECTORIES THAT DOES NOT EXIST IS NOT A PERMISSION VERDICT: the install + // itself will fail with its own error, and calling it a refusal would name + // the wrong reason. + if _, err := os.Stat(dir); err != nil { + return true + } + return unix.Access(dir, unix.W_OK) == nil +} diff --git a/internal/update/dirwrite_windows.go b/internal/update/dirwrite_windows.go new file mode 100644 index 0000000000..611c90dd3d --- /dev/null +++ b/internal/update/dirwrite_windows.go @@ -0,0 +1,28 @@ +//go:build windows + +package update + +import ( + "os" + + "golang.org/x/sys/windows" +) + +// dirWritable reports whether this account may create files in dir, the +// permission an in-place replacement needs. A failed create in an unwritable +// folder leaves nothing behind, so a probe is the honest answer here. +func dirWritable(dir string) bool { + if _, err := os.Stat(dir); err != nil { + return true + } + file, err := windows.CreateFile( + windows.StringToUTF16Ptr(dir+`\.codeaf-writecheck`), + windows.GENERIC_WRITE, 0, nil, windows.CREATE_ALWAYS, windows.FILE_ATTRIBUTE_TEMPORARY, 0, + ) + if err != nil { + return false + } + _ = windows.CloseHandle(file) + _ = windows.DeleteFile(windows.StringToUTF16Ptr(dir + `\.codeaf-writecheck`)) + return true +} diff --git a/internal/update/install.go b/internal/update/install.go index eaecd1e65d..2b21844d33 100644 --- a/internal/update/install.go +++ b/internal/update/install.go @@ -23,12 +23,20 @@ type InstallOptions struct { Curl string GOOS string GOARCH string + // AllowDowngrade says the PERSON chose this exact release by name — `/update + // ` or `update --version ` — and a rollback is what they asked + // for. An automatic or channel-following update never sets it, so a stale + // caller cannot silently downgrade a file another window advanced. + AllowDowngrade bool } // InstallResult is the release and executable path that are ready to restart. type InstallResult struct { Release Release Path string + // Already is true when the target was already the requested release: a + // successful no-op, not a failure, and not something to alarm anyone about. + Already bool } // Install downloads, checks, and atomically replaces one executable. @@ -67,6 +75,34 @@ func Install(ctx context.Context, options InstallOptions) (InstallResult, error) if release.Repository == "" { release.Repository = primaryRepository } + // ONE ACQUIRER, INSIDE THE SHARED INSTALLER. Every road that replaces this + // file — the chat surface's background install and `codeaf update` alike — + // comes through here, so the lock and the on-disk re-read cannot be + // forgotten by a caller and cannot race a second caller that took its own. + if refusal := InstallRefusal(target, curl); refusal != "" { + return InstallResult{}, &RefusalError{Reason: refusal} + } + lock, lockErr := TryTargetLock(target) + if lockErr != nil { + if errors.Is(lockErr, ErrInstallInFlight) { + return InstallResult{}, &RefusalError{Reason: "another codeaf is already installing an update"} + } + return InstallResult{}, fmt.Errorf("cannot replace %s: %w; install a release with: %s", target, lockErr, curl) + } + defer lock.Release() + // UNDER THE LOCK, WHAT IS ON DISK IS WHAT COUNTS. The record is trusted only + // while the file still hashes to it, so an externally replaced binary reads + // as unknown and a candidate newer than it is not refused on a stale fact. + if record, ok := lock.Record(); ok { + if record.Tag == release.Tag { + // ALREADY THERE IS SUCCESS. Nothing is downloaded, nothing is + // written, and the caller is told so it can stay quiet about it. + return InstallResult{Release: release, Path: target, Already: true}, nil + } + if recordSupersedes(record, release) && !options.AllowDowngrade { + return InstallResult{}, &RefusalError{Reason: "codeaf " + record.Tag + " is already the build on disk, newer than " + release.Tag} + } + } var ( body []byte checksums []byte @@ -115,6 +151,9 @@ func Install(ctx context.Context, options InstallOptions) (InstallResult, error) if err := replaceExecutable(temporary, target); err != nil { return InstallResult{}, fmt.Errorf("cannot replace %s: %w; install a release with: %s", target, err, curl) } + // The record names the bytes just written, so the next caller can tell what + // is on disk from what it believes is on disk. + lock.RecordInstalled(release.Tag, actual, release.PublishedAt) return InstallResult{Release: release, Path: target}, nil } diff --git a/internal/update/lock_unix.go b/internal/update/lock_unix.go new file mode 100644 index 0000000000..a9d86d3f59 --- /dev/null +++ b/internal/update/lock_unix.go @@ -0,0 +1,20 @@ +//go:build !windows + +package update + +import ( + "os" + + "golang.org/x/sys/unix" +) + +// lockFileExclusive takes the target's lock without blocking. The kernel holds +// it for the life of the process, so a crashed installer can never wedge a +// future one and no watchdog has to guess when to take a lock over. +func lockFileExclusive(file *os.File) bool { + return unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB) == nil +} + +func unlockFile(file *os.File) { + _ = unix.Flock(int(file.Fd()), unix.LOCK_UN) +} diff --git a/internal/update/lock_windows.go b/internal/update/lock_windows.go new file mode 100644 index 0000000000..717251d15a --- /dev/null +++ b/internal/update/lock_windows.go @@ -0,0 +1,25 @@ +//go:build windows + +package update + +import ( + "os" + + "golang.org/x/sys/windows" +) + +// lockFileExclusive takes the target's lock without blocking, on the same terms +// as the unix door: held by the OS until the handle closes, so a dead installer +// releases it and nothing has to be taken over by age. +func lockFileExclusive(file *os.File) bool { + overlapped := new(windows.Overlapped) + return windows.LockFileEx( + windows.Handle(file.Fd()), + windows.LOCKFILE_EXCLUSIVE_LOCK|windows.LOCKFILE_FAIL_IMMEDIATELY, + 0, 1, 0, overlapped, + ) == nil +} + +func unlockFile(file *os.File) { + _ = windows.UnlockFileEx(windows.Handle(file.Fd()), 0, 1, 0, new(windows.Overlapped)) +} diff --git a/internal/update/update_test.go b/internal/update/update_test.go index bfd022eb34..2e20dc904a 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -127,7 +127,7 @@ func TestV3LaunchNoticeForChannelBuilds(t *testing.T) { t.Fatalf("requests = %q, want one ending in %q", paths, row.wantPath) } if row.name == "older dev" { - want := "codeaf " + newDev + " is out · you have " + oldDev + " · /update installs it and restarts · or: curl -fsSL https://agentfield.ai/get/devaf | bash" + want := "codeaf " + newDev + " is out · you have " + oldDev + " · /update installs it for the next launch · or: curl -fsSL https://agentfield.ai/get/devaf | bash" if got := answer.Notice(); got != want { t.Fatalf("notice = %q, want %q", got, want) } diff --git a/scripts/update-drive.sh b/scripts/update-drive.sh new file mode 100755 index 0000000000..db2ec9982b --- /dev/null +++ b/scripts/update-drive.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Capture the update offer's deterministic fixtures from THIS worktree's binary. +# +# scripts/update-drive.sh # the whole matrix +# scripts/update-drive.sh offer 80 # one case at one width +# +# Every fixture disables the updater's hooks (internal/tui3/updatedemo.go), and +# this drive adds to that: a home and workspace made fresh under mktemp, a +# private tmux server, an EMPTY inherited environment, and `chat --no-host` so +# no session host is left behind. Nothing here reaches a release host, installs +# anything, or writes outside the temporary directory, which is removed on exit. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BIN="${UPDATE_BIN:-$ROOT/bin/codeaf}" +OUT="${UPDATE_OUT:-$ROOT/docs/design/background-updates-frames}" +CASES=(offer downloading ready ready-working manual off failure) +WIDTHS=(80 120) +if [ "${1:-}" = "--all" ]; then + shift + CASES=(offer downloading ready ready-working manual off failure) + WIDTHS=(80 120) +fi + +[ -x "$BIN" ] || { echo "no binary at $BIN — run make build" >&2; exit 1; } +command -v tmux >/dev/null || { echo "tmux is required for a screen drive" >&2; exit 1; } + +TMP="$(mktemp -d "${TMPDIR:-/tmp}/codeaf-update-drive.XXXXXX")" +SOCK="codeaf-update-$$" +cleanup() { + tmux -L "$SOCK" kill-server 2>/dev/null || true + rm -rf "$TMP" +} +trap cleanup EXIT + +HOME_DIR="$TMP/home" +WORK="$TMP/work/project" +mkdir -p "$HOME_DIR" "$WORK" "$OUT" +cat >"$HOME_DIR/config.json" <<'JSON' +{ + "setup_seen_at": "2026-10-07T00:00:00Z", + "api_key": "demo-key-not-used", + "model.talk": "deepseek/deepseek-v4.1-flash", + "models.tiers.high": "deepseek/deepseek-v4.1-flash", + "models.tiers.worker": "deepseek/deepseek-v4.1-flash", + "models.tiers.reflex": "deepseek/deepseek-v4.1-flash", + "budget.daily_usd": "20", + "ui.mouse": "on" +} +JSON + +# capture +capture() { + local case="$1" cols="$2" + local session="update-$case-$cols" file="$OUT/$case-${cols}c.txt" + tmux -L "$SOCK" kill-session -t "$session" 2>/dev/null || true + tmux -L "$SOCK" new-session -d -s "$session" -x "$cols" -y 40 \ + "cd $(printf '%q' "$WORK") && env -i \ + HOME=$(printf '%q' "$HOME_DIR") \ + CODEAF_HOME=$(printf '%q' "$HOME_DIR") \ + CODEAF_PROFILE_DIR=$(printf '%q' "$HOME_DIR") \ + CODEAF_NO_UPDATE_CHECK=1 \ + TERM=xterm-256color \ + LANG=C.UTF-8 \ + PATH=/usr/bin:/bin \ + CODEAF_UPDATE_DEMO=$(printf '%q' "$case") \ + $(printf '%q' "$BIN") chat --no-host" + sleep 3 + tmux -L "$SOCK" capture-pane -p -t "$session" >"$file" + tmux -L "$SOCK" capture-pane -p -e -t "$session" >"$OUT/$case-${cols}c.ansi" + tmux -L "$SOCK" kill-session -t "$session" 2>/dev/null || true + # A CAPTURE THAT DID NOT DRAW THE FIXTURE IS A FAILURE, not a file. Every + # fixture writes its own name last, so this checks the real surface ran. + grep -q "CODEAF_UPDATE_DEMO=$case" "$file" || { + echo "capture $case at ${cols}c did not draw the fixture:" >&2 + cat "$file" >&2 + exit 1 + } + echo "$file" +} + +if [ $# -gt 0 ]; then + capture "$1" "${2:-80}" + exit 0 +fi + +for case in "${CASES[@]}"; do + for cols in "${WIDTHS[@]}"; do + capture "$case" "$cols" + done +done From 7a231534d425621059f8681ff0fe8c6ec25f205d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 10:30:39 -0400 Subject: [PATCH 02/12] engine: an older engine keeps its work and lets go when it is quiet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What was true: a newer build connecting to a workspace replaced an engine from an older build whenever it arrived, busy or not, through enginehost.Stop — which asks with Anyway (do not ask the host whether it may go) and signals the pid for a host too old to answer. It ended turns people were watching, and it was two round trips: ask what is there, then kill it, with a window free to attach in the gap. What is true now: - An older engine holding anything — a turn, an unanswered question, handed-off work, an attached window — is JOINED, and asked with the additive WhoIs.StandDownWhenIdle to retire at its first quiet moment. - An older engine holding nothing is retired through enginehost.Retire with no Anyway: the host decides under the same lock that admits a conversation, so there is no status-then-kill race. - A host on another wire is refused in words while it is busy, retired when it is not; one too old to answer at all is refused rather than signalled. codeaf engine --stop / --stop-all stay the only force in the tree. - HostSelf.StandDownWhenIdle acknowledges the note, so the busy line only promises a quiet-moment swap to a host that understood it; an installed older build gets the honest sentence instead. - openChatV3Local keeps a takeover refusal as its own type (hostRefusal) rather than hostUnreachable, so a launch refuses instead of opening a second writer beside the journal the engine holds. - A structural law keeps the takeover road free of enginehost.Stop, migrations and store.Open. Focused checks: internal/enginehost (whole package, and -race on the lifecycle tests), internal/remote whois/idle/hold suites, and the engine and host suites in cmd/codeaf all pass. Command list and limits: the engine update plan in the external evidence directory. --- cmd/codeaf/chatv3_local.go | 31 +- cmd/codeaf/chatv3_local_test.go | 2 +- cmd/codeaf/engine.go | 200 +++++++--- cmd/codeaf/engine_stale_test.go | 488 ++++++++++++++++++++----- cmd/codeaf/engine_takeover_law_test.go | 75 ++++ internal/enginehost/enginehost.go | 10 + internal/enginehost/host.go | 48 ++- internal/enginehost/standdown_test.go | 391 ++++++++++++++++++++ internal/remote/whois.go | 38 +- 9 files changed, 1114 insertions(+), 169 deletions(-) create mode 100644 cmd/codeaf/engine_takeover_law_test.go create mode 100644 internal/enginehost/standdown_test.go diff --git a/cmd/codeaf/chatv3_local.go b/cmd/codeaf/chatv3_local.go index d6a143ae63..d4963b1c88 100644 --- a/cmd/codeaf/chatv3_local.go +++ b/cmd/codeaf/chatv3_local.go @@ -123,6 +123,9 @@ func (l *localLink) dial() (io.ReadWriteCloser, error) { if err != nil { return nil, err } + // A staleHost refusal is passed through UNTOUCHED: openChatV3Local reads it + // back out of the dial's error to decide that this launch may not fall back + // to a conversation in this process. note, err := clearStaleEngineHost(l.workspace) if err != nil { return nil, err @@ -149,6 +152,16 @@ type hostUnreachable struct{ reason string } func (h *hostUnreachable) Error() string { return h.reason } +// hostRefusal is an engine on this machine that holds the workspace and will not +// give it up: a busy one on another wire, one holding work, or one too old to be +// asked. It is deliberately NOT [hostUnreachable], because the floor is not +// available here — that engine holds the conversation's journal, so opening the +// conversation in this process would be a second writer beside it. The launch +// refuses in the host's own words instead (openChatV3Local states the whole of it). +type hostRefusal struct{ sentence string } + +func (h *hostRefusal) Error() string { return h.sentence } + // hostFallbackReason is the clause a person reads after "this conversation // opened in this terminal instead, and ends with it: ". The two host failures // written in machinery words are translated here, at the surface that knows @@ -206,9 +219,19 @@ func openChatV3Local(launch localLaunch) error { client, err = remote.Roam("", hello, remote.Roaming{Dial: link.dial}) } if err != nil { + // AN ENGINE THAT HOLDS THE WORKSPACE IS NOT AN UNREACHABLE ONE. The + // in-process floor is right for a machine where no host could be + // reached or started — and wrong here, because this host holds the + // conversation's journal: opening it in this process as well would be a + // second writer beside it. So the sentence is the whole answer and the + // launch refuses (staleHost is the takeover rule's own refusal). + var stale *staleHost + if errors.As(err, &stale) { + return &hostRefusal{sentence: stale.reason} + } // A host that cannot be reached or started is not the end of the // launch: the in-process door is the floor, and the reason travels with - // the fallback so a stale host's own sentence is still read. + // the fallback so nothing is swallowed. return &hostUnreachable{reason: hostFallbackReason(err)} } closeClient := func() { _ = client.Close() } @@ -428,9 +451,9 @@ func localDoors(options *tui3.Options, welcome remote.Welcome, settings config.C // point at. // // The three launches that do not: one the engine answered (err is nil), one it -// refused for any other reason — unreachable, a wrong wire version, a stale -// host, all of which are the in-process door's own floor — and `--once`, which -// has no screen to land an offer on and says the sentence instead +// refused for any other reason — unreachable, a wrong wire version, and a stale +// host, which is a refusal rather than a fallback — and `--once`, which has no +// screen to land an offer on and says the sentence instead // ([sessionHeldElsewhereSentence]). func localAskAgainAfterRefusal(launch localLaunch, err error) (string, bool) { if err == nil || launch.once != "" || !hostHeldRefusal(err) { diff --git a/cmd/codeaf/chatv3_local_test.go b/cmd/codeaf/chatv3_local_test.go index 3fed579832..a040c45132 100644 --- a/cmd/codeaf/chatv3_local_test.go +++ b/cmd/codeaf/chatv3_local_test.go @@ -693,7 +693,7 @@ func TestALongStatePathIsSaidInWordsAndNotInTheEnginesOwn(t *testing.T) { t.Fatalf("the %s speaks in machinery words: %q", name, said) } } - written := busyEngineHostSentence(true) + written := busyEngineHostSentence("/home/somebody/api", true) if got := hostFallbackReason(errors.New(written)); got != written { t.Fatalf("a sentence already written for a person became %q, want %q", got, written) } diff --git a/cmd/codeaf/engine.go b/cmd/codeaf/engine.go index 0968b59de0..2577e8011c 100644 --- a/cmd/codeaf/engine.go +++ b/cmd/codeaf/engine.go @@ -268,26 +268,22 @@ func clearStaleEngineHost(workspace string) (string, error) { } // clearStaleEngineHostAs is the question and what is done with the answer. A nil -// error means "go ahead and attach": nothing is holding this workspace, what is -// holding it is this build or a newer one, or what was holding it was older and -// has been replaced — in which case the string is the one line saying so. +// error means "go ahead and attach"; what was holding the workspace was this +// build, a newer one, or an older one that let go — and then the string is the +// one line saying so. // -// ── THE RULE: THE OLDER ENGINE GIVES UP THE SLOT ───────────────────────────── -// -// On 2026-09-23 a new `codeaf engine --daemon` exited without a word because a -// two-day-old engine, started from a different binary, held the workspace. The -// windows went on talking to it, and conversations it had open refused the new -// build with "open in another window". The file-replaced retirement -// (internal/enginehost's binary.go) never fired, because nothing had replaced -// ITS file: a different file was simply newer. -// -// So an engine from an OLDER build is replaced whenever a newer one arrives, -// busy or not, and the window says so in one line. It is not asked first: an -// older engine is stale by definition, the conversations it holds are closed -// properly on the way out (every journal flushed; a turn it catches stops where -// it is and keeps its partial reply, as ctrl+c does), and the next connection — -// this one — reopens them on the current build. Windows that were attached to it -// see their connection end and redial onto the replacement. +// AN OLDER ENGINE HOLDS ONTO ITS WORK AND LETS GO WHEN IT IS QUIET. A turn +// running, a question waiting, a task handed off, a window attached: all of it +// is somebody's work, and a newer binary connecting is no reason to end it. So +// an older engine holding anything is JOINED, and asked with +// [enginehost.StandDownWhenIdle] to retire at its first quiet moment. One +// holding nothing is RETIRED — through the host's own admission check +// ([enginehost.Retire] with no Anyway), never a signal, which is what makes the +// decision atomic with admitting a conversation. A host on another wire cannot +// be joined, so it is refused in words while it is busy and retired when it is +// not; one too old to answer at all cannot say whether it is holding work, so it +// is refused too rather than signalled. The explicit `codeaf engine --stop` is +// the only force left, and it is a person's own. // // WHAT MAKES TWO BUILDS THE SAME ONE IS THE SOURCE AND THE FILE. A host of this // source running from this file (or one that does not say which file) is this @@ -295,15 +291,10 @@ func clearStaleEngineHost(workspace string) (string, error) { // unless an environment change can retire it without ending anybody's work. // // WHICH IS OLDER IS THE BUILD MOMENT AND NOTHING ELSE, and a tie is never a -// replacement. That is what makes the rule converge: two windows on two builds -// agree on which one is older, so the newer engine is never replaced by a window -// of the older build, and two copies of one binary cannot take the slot from -// each other forever. A host too old to answer the question at all is older than -// everything. -// -// A NEWER ENGINE IS JOINED when it speaks this build's wire, and refused in words -// when it does not — the older half of that pair is this binary, and the -// sentence names it. +// replacement, so two windows on two builds agree and the slot cannot go back +// and forth. A NEWER ENGINE IS JOINED when it speaks this build's wire and +// refused in words when it does not — the older half of that pair is this +// binary, and the sentence names it. func clearStaleEngineHostAs(workspace string, me engineBuild) (string, error) { held, err := enginehost.Inspect(workspace) if err != nil { @@ -331,18 +322,76 @@ func clearStaleEngineHostAs(workspace string, me engineBuild) (string, error) { } return "", &staleHost{reason: newerEngineHostSentence(hostWorkspace(host, workspace))} } - // OLDER: replaced. [enginehost.Stop] asks it to stand down regardless of - // what it holds, and ends a host too old to be asked with the signal every - // build of the host has answered by flushing and exiting. - went, err := enginehost.Stop(workspace) - if err != nil { - return "", &staleHost{reason: staleEngineHostSentence(host.Busy, hostWorkspace(host, workspace))} + // Too old to answer at all: it cannot say whether it is holding work, so + // nothing here may decide for it. Words, and the explicit stop. + if !held.Answered { + return "", &staleHost{reason: unknownOlderEngineHostSentence(hostWorkspace(host, workspace))} + } + if host.Version != remote.Version { + return olderEngineOnAnotherWire(workspace, held) + } + return olderEngineOnThisWire(workspace, held) +} + +// olderEngineOnThisWire is an older engine that speaks this build's protocol: +// the ordinary road. Busy means joined, idle means asked to go. +func olderEngineOnThisWire(workspace string, held enginehost.Holder) (string, error) { + host := held.Self + if host.Busy { + return waitingEngineHostNote(workspace, held), nil + } + err := enginehost.Retire(workspace, false) + switch { + case err == nil: + return replacedEngineHostSentence(held), nil + case nothingHolds(workspace): + // It went on its own between the question and the ask: the caller's + // attach starts a host from this build. + return "", nil + case errors.Is(err, enginehost.ErrHostBusy): + // The host decided, and its answer is the one that counts: something + // arrived between the two questions, so this build joins it rather than + // ending the conversation that arrived. + return waitingEngineHostNote(workspace, held), nil + default: + return "", &staleHost{reason: staleEngineHostSentence(false, hostWorkspace(host, workspace))} } - if !went { - // It went on its own between the question and the stop. +} + +// waitingEngineHostNote tells the host a newer build is waiting for the slot and +// writes the one line the surface shows. The line is chosen by the host's own +// answer: a host too old to have heard of the note steps aside only when asked, +// and the sentence says that instead of promising a quiet moment it cannot take. +func waitingEngineHostNote(workspace string, held enginehost.Holder) string { + self, err := enginehost.StandDownWhenIdle(workspace) + return busyEngineHostSentence(hostWorkspace(held.Self, workspace), err == nil && self.StandDownWhenIdle) +} + +// olderEngineOnAnotherWire is an older engine this build cannot speak to: it +// cannot be joined, so while it holds anything the honest answer is a sentence +// and the explicit stop, and when it holds nothing it is asked to go exactly as +// the same-wire one is. Nothing on this road signals anything. +func olderEngineOnAnotherWire(workspace string, held enginehost.Holder) (string, error) { + host := held.Self + if host.Busy { + return "", &staleHost{reason: otherProtocolEngineHostSentence(hostWorkspace(host, workspace))} + } + err := enginehost.Retire(workspace, false) + switch { + case err == nil: + return replacedEngineHostSentence(held), nil + case nothingHolds(workspace): return "", nil + default: + return "", &staleHost{reason: otherProtocolEngineHostSentence(hostWorkspace(host, workspace))} } - return replacedEngineHostSentence(held), nil +} + +// nothingHolds reports that no host is answering this workspace any more, which +// is what a host that retired on its own leaves behind. +func nothingHolds(workspace string) bool { + _, err := enginehost.Inspect(workspace) + return errors.Is(err, enginehost.ErrNothingHolding) } // differentEngineEnvironmentSentence names the host's workspace so the command @@ -423,6 +472,48 @@ func staleEngineHostSentence(busy bool, workspace string) string { return fmt.Sprintf("engine: %s is still holding this conversation on an older codeaf — run %s on %s", name, stop, name) } +// unknownOlderEngineHostSentence is what a person reads when the process holding +// their conversation is older than the version exchange itself: it refused the +// only question there is, so this build cannot tell whether it is mid-turn, +// holding a window or doing nothing at all. +// +// SO IT IS NOT ENDED. The one thing that still ends a build this old is a person +// saying stop — which [enginehost.Stop] reaches by signalling what it cannot +// ask, and which is the command the sentence names. +func unknownOlderEngineHostSentence(workspace string) string { + name := remote.MachineName() + if strings.TrimSpace(name) == "" { + name = "that machine" + } + stop := engineStopWords(workspace) + return fmt.Sprintf("engine: %s is holding this conversation on an older codeaf that cannot say what it is or what it is doing, and this build will not end work it cannot see — run %s on %s if you want it stopped", name, stop, name) +} + +// otherProtocolEngineHostSentence is the refusal for an older engine on a wire +// this build cannot speak to while it is holding something: there is nothing to +// join and nothing here will end it. It has to read well in a window of the +// newer build and in a terminal somebody just typed `codeaf` into, which is why +// it names both the work and the way out. +func otherProtocolEngineHostSentence(workspace string) string { + name := remote.MachineName() + if strings.TrimSpace(name) == "" { + name = "that machine" + } + stop := engineStopWords(workspace) + return fmt.Sprintf("engine: %s is holding this conversation on a codeaf that speaks a different protocol, and something is still going in it — let that finish, or run %s on %s", name, stop, name) +} + +// engineStopWords is the command a refusal tells a person to run: the explicit +// stop, naming and quoting the workspace so a folder with a space in it survives +// being copied out of a sentence and pasted into a shell. +func engineStopWords(workspace string) string { + stop := "codeaf engine --stop" + if workspace = strings.TrimSpace(workspace); workspace != "" { + stop += " --workspace " + shellQuote(workspace) + } + return stop +} + // newerEngineHostSentence is the refusal the other way round: the engine holding // this workspace is a NEWER codeaf on a wire this binary cannot speak, so the // binary that is behind is this one. Nothing is stopped — the newer engine is @@ -455,20 +546,25 @@ func hostWorkspace(host remote.HostSelf, asked string) string { return asked } -// busyEngineHostSentence is the one line a window reads when it is attached to -// an engine one build behind that could not be let go of. Under the takeover -// rule this door no longer leaves such an engine in place; the sentence stays -// because a window of THIS build can still meet it from an older build's door, -// and the hosted surface quotes its voice (internal/tui3's newsSilenceNote). -func busyEngineHostSentence(busy bool) string { +// busyEngineHostSentence is the one line a window reads when it joined an engine +// one build behind that is holding something. It is the ordinary line of the +// takeover rule rather than a leftover: an older engine with work in flight is +// joined, not replaced, and the hosted surface quotes this voice for the same +// fact (internal/tui3's newsSilenceNote). +// +// THE PROMISE IS MADE ONLY WHERE IT IS KEPT. "Picks up this build once it goes +// quiet" is [enginehost.StandDownWhenIdle], and a host older than that field +// never heard of it: waits says whether the host acknowledged the request, and +// the other sentence says what actually gets a person onto this build. +func busyEngineHostSentence(workspace string, waits bool) string { name := remote.MachineName() if strings.TrimSpace(name) == "" { name = "this machine" } - if busy { - return fmt.Sprintf("the engine on %s is an older codeaf and is still holding work — it picks up this build the moment it goes quiet", name) + if waits { + return fmt.Sprintf("the engine on %s is an older codeaf and is still holding work — it keeps that work and picks up this build once it goes quiet", name) } - return fmt.Sprintf("the engine on %s is an older codeaf — it is holding this conversation and picks up this build the moment you leave it", name) + return fmt.Sprintf("the engine on %s is an older codeaf and is still holding work — it keeps that work, and your next launch once the work is done opens from this build", name) } // replacedEngineHostSentence is the ONE LINE a takeover owes: which engine was @@ -729,12 +825,12 @@ func runEngineHost(workspaceFlag, sessionFlag string) error { if err != nil { return err } - // AN OLDER ENGINE IN THE SLOT IS REPLACED, NOT DEFERRED TO. This door used - // to find the lock taken and exit without a word, which is right when the - // holder is this build and was the whole defect when it was a two-day-old - // engine from another binary (the takeover rule: [clearStaleEngineHostAs]). - // Stderr is the person's terminal when they typed this, and the host's log - // when a window spawned it — the one line belongs in either. + // THE SLOT IS SETTLED BEFORE THIS PROCESS BECOMES THE HOST, so an older + // engine is either joined (holding work) or asked to go (holding nothing) + // and this door starts a host of this build rather than exiting into a + // lock it cannot take ([clearStaleEngineHostAs]). Stderr is the person's + // terminal when they typed this and the host's log when a window spawned + // it, so the one line belongs in either. note, err := clearStaleEngineHost(workspace) if err != nil { return err diff --git a/cmd/codeaf/engine_stale_test.go b/cmd/codeaf/engine_stale_test.go index 83ed4b5a98..73d4542e8a 100644 --- a/cmd/codeaf/engine_stale_test.go +++ b/cmd/codeaf/engine_stale_test.go @@ -14,11 +14,11 @@ import ( "bufio" "encoding/json" "errors" - "fmt" "net" "os" "os/exec" "os/signal" + "path/filepath" "strings" "sync" "syscall" @@ -30,20 +30,48 @@ import ( "github.com/Agent-Field/codeaf/internal/remote" ) +// standInOptions are the two things an ordinary stand-in cannot model, and +// each of them is a moment a real host can genuinely be in. +type standInOptions struct { + // busyAfter makes the Nth question onward answer busy: a host that looked + // idle when it was asked what it is and is holding something by the time + // the stand-down arrives. It is the race the rule must lose gracefully. + busyAfter int + // vanishAfter makes the socket go after the Nth answer: a host that retired + // on its own between the question and the stand-down. + vanishAfter int + // ignoresWhenIdle is a host built before the note existed: it answers the + // question without acknowledging it, exactly as an installed older build + // does, and cannot retire at a quiet moment nobody told it about. + ignoresWhenIdle bool +} + // standInHost answers the version exchange with whatever it was told to say, // and takes down its own socket when it agrees to retire — which is what a real // host's shutdown looks like from the outside. +// +// IT KEEPS EVERY QUESTION IT WAS ASKED, because part of what is under test is +// WHICH question arrived: an older engine holding work is asked to let go when +// it is quiet and must never be asked to go anyway. type standInHost struct { - self remote.HostSelf - // older is a build from before the exchange: it refuses the question the - // way every one of them always has. + self remote.HostSelf older bool + opts standInOptions + + mu sync.Mutex + asks []remote.WhoIs + retired bool listener net.Listener once sync.Once } func standIn(t *testing.T, workspace string, self remote.HostSelf, older bool) *standInHost { + t.Helper() + return standInAs(t, workspace, self, older, standInOptions{}) +} + +func standInAs(t *testing.T, workspace string, self remote.HostSelf, older bool, opts standInOptions) *standInHost { t.Helper() // A real host makes the directory it listens in ([enginehost.Run] takes Dir // before SocketPath), because naming the socket makes nothing — asking @@ -59,7 +87,7 @@ func standIn(t *testing.T, workspace string, self remote.HostSelf, older bool) * if err != nil { t.Fatalf("listen on the socket: %v", err) } - host := &standInHost{self: self, older: older, listener: listener} + host := &standInHost{self: self, older: older, opts: opts, listener: listener} t.Cleanup(host.close) go host.serve() return host @@ -96,17 +124,62 @@ func (h *standInHost) answer(conn net.Conn) { var ask remote.WhoIs _ = json.Unmarshal(frame.Payload, &ask) + h.mu.Lock() + h.asks = append(h.asks, ask) + count := len(h.asks) + busy := h.self.Busy || (h.opts.busyAfter > 0 && count >= h.opts.busyAfter) + retired := h.retired + h.mu.Unlock() + self := h.self - if ask.StandDown && (ask.Anyway || !self.Busy) { + self.Busy = busy + self.StandDownWhenIdle = ask.StandDownWhenIdle && !h.opts.ignoresWhenIdle + if retired { + self.Retiring = true + } + if ask.StandDown && (ask.Anyway || !busy) { self.Retiring = true + h.mu.Lock() + h.retired = true + h.mu.Unlock() } answer, _ := json.Marshal(remote.Frame{Kind: "whoami", Payload: mustStandInJSON(self)}) _, _ = conn.Write(append(answer, '\n')) - if self.Retiring { + if self.Retiring || (h.opts.vanishAfter > 0 && count >= h.opts.vanishAfter) { h.close() } } +// questions is every question this stand-in was asked, oldest first. +func (h *standInHost) questions() []remote.WhoIs { + h.mu.Lock() + defer h.mu.Unlock() + return append([]remote.WhoIs(nil), h.asks...) +} + +// waitingSaid reports that a newer build asked this host for the slot without +// asking it to end anything. +func (h *standInHost) waitingSaid() bool { + for _, ask := range h.questions() { + if ask.StandDownWhenIdle { + return true + } + } + return false +} + +// forced reports that something asked this host to go regardless of what it was +// holding, which is a person's own `codeaf engine --stop` and must never be +// this door. +func (h *standInHost) forced() bool { + for _, ask := range h.questions() { + if ask.StandDown && ask.Anyway { + return true + } + } + return false +} + func (h *standInHost) close() { h.once.Do(func() { _ = h.listener.Close() @@ -137,7 +210,7 @@ func shortEngineHome(t *testing.T) { t.Setenv("CODEAF_HOME", root) } -// ── the takeover rule: the older engine gives up the slot ─────────────────── +// ── the takeover rule: work is kept, and the slot is handed over quietly ──── // window is a build of this binary at a moment, for the tests that need two // builds of one source and a test binary that is linked once. @@ -150,6 +223,16 @@ var ( later = earlier.Add(48 * time.Hour) ) +// hostAnswers is whether anything is still listening on this workspace's socket. +func hostAnswers(workspace string) bool { + conn, err := enginehost.Dial(workspace) + if err != nil { + return false + } + _ = conn.Close() + return true +} + func TestAHostOfThisBuildIsSplicedOntoWithoutAWord(t *testing.T) { shortEngineHome(t) workspace := "/home/somebody/api" @@ -162,91 +245,241 @@ func TestAHostOfThisBuildIsSplicedOntoWithoutAWord(t *testing.T) { if note != "" { t.Fatalf("a host of this build owed a sentence: %q", note) } - if conn, err := enginehost.Dial(workspace); err != nil { - t.Fatalf("a host of this build was asked to go: %v", err) - } else { - _ = conn.Close() + if !hostAnswers(workspace) { + t.Fatal("a host of this build was asked to go") } } -// THE CASE THIS RULE WAS WRITTEN FOR (2026-09-23): an engine two days older, -// from another binary, HOLDING WORK, was deferred to — the new daemon exited -// without a word and every window kept talking to the old one. Now it is -// replaced, and the window is told in one line which process that was. -func TestAnOlderEngineHoldingWorkIsReplacedAndNamed(t *testing.T) { +// THE CASE THIS RULE WAS WRITTEN FOR (2026-09-23), turned right way up. An +// engine two days older, from another binary, HOLDING WORK, used to be ended +// whenever a newer window connected — a turn somebody was watching closed +// because a rebuild happened. It is joined now, it keeps everything it has, and +// it is asked to let go at its first quiet moment. +func TestAnOlderEngineHoldingWorkKeepsItAndIsJoined(t *testing.T) { shortEngineHome(t) workspace := "/home/somebody/api" - standIn(t, workspace, remote.HostSelf{ + host := standIn(t, workspace, remote.HostSelf{ Version: remote.Version, Build: "two-days-ago", Busy: true, PID: 4242, Binary: "/home/somebody/.codeaf/bin/devaf", Revision: "a1b2c3d4 built 2026-09-21 09:00", - BuiltAt: earlier, Surfaces: 1, Conversations: 3, + BuiltAt: earlier, Surfaces: 1, Conversations: 3, Workspace: workspace, }, false) note, err := clearStaleEngineHostAs(workspace, window("today", later)) if err != nil { - t.Fatalf("an older engine holding work was refused rather than replaced: %v", err) + t.Fatalf("an older engine holding work was refused rather than joined: %v", err) + } + for _, want := range []string{"older codeaf", "still holding work", "picks up this build once it goes quiet"} { + if !strings.Contains(note, want) { + t.Fatalf("the busy line %q does not say %q", note, want) + } + } + // NOBODY'S WORK ENDED AND THE PROCESS IS STILL THERE. + if !hostAnswers(workspace) { + t.Fatal("an older engine holding work was taken down") + } + // AND THE SLOT IS ASKED FOR, QUIETLY: the host is told a newer build is + // waiting, which is what makes the swap happen at its own first quiet + // moment rather than half an hour later. + if !host.waitingSaid() { + t.Fatal("the older engine was not asked to let go when it is quiet") + } + if host.forced() { + t.Fatal("an older engine holding work was asked to go regardless of it") + } +} + +// THE OTHER HALF OF THE SAME LINE: the older engine was holding nothing, so it +// is asked to go — through the host's own admission check, never a signal — and +// the next connection starts a host from the binary on disk. +func TestAnOlderEngineHoldingNothingIsRetiredWithoutBeingKilled(t *testing.T) { + shortEngineHome(t) + workspace := "/home/somebody/api" + host := standIn(t, workspace, remote.HostSelf{ + Version: remote.Version, Build: "two-days-ago", BuiltAt: earlier, + PID: 4242, Binary: "/home/somebody/.codeaf/bin/devaf", Revision: "a1b2c3d4 built 2026-09-21 09:00", + }, false) + + note, err := clearStaleEngineHostAs(workspace, window("today", later)) + if err != nil { + t.Fatalf("an idle older engine was refused rather than retired: %v", err) } for _, want := range []string{"replaced the older engine", "pid 4242", "a1b2c3d4", "/home/somebody/.codeaf/bin/devaf", "this build holds the workspace now"} { if !strings.Contains(note, want) { - t.Fatalf("the takeover line %q does not say %q", note, want) + t.Fatalf("the handover line %q does not say %q", note, want) } } - if conn, err := enginehost.Dial(workspace); err == nil { - _ = conn.Close() - t.Fatal("the older engine was still answering after the takeover") + if hostAnswers(workspace) { + t.Fatal("the older engine was still answering after the handover") + } + // ATOMICITY IS IN THE QUESTION THAT WAS ASKED. The stand-down went with + // Anyway FALSE, so the host measured what it was holding under its own lock + // and agreed; a kill, or an Anyway, would have decided for it. + asked := host.questions() + if len(asked) < 2 || !asked[len(asked)-1].StandDown || asked[len(asked)-1].Anyway { + t.Fatalf("the stand-down was not the host's own decision: %+v", asked) + } +} + +// THE RACE THE RULE HAS TO LOSE GRACEFULLY: the host looked idle when it was +// asked what it was, and it is holding something by the time the stand-down +// arrives. The second question is the one that counts, the host refuses, and +// this build joins it instead of killing it — which is exactly what a +// status-then-kill pair used to get wrong. +func TestAnOlderEngineThatRefusedTheStandDownKeepsItsWork(t *testing.T) { + shortEngineHome(t) + workspace := "/home/somebody/api" + host := standInAs(t, workspace, remote.HostSelf{ + Version: remote.Version, Build: "two-days-ago", BuiltAt: earlier, Workspace: workspace, + }, false, standInOptions{busyAfter: 2}) + + note, err := clearStaleEngineHostAs(workspace, window("today", later)) + if err != nil { + t.Fatalf("a host that kept its work was refused rather than joined: %v", err) + } + if !strings.Contains(note, "older codeaf") || !strings.Contains(note, "still holding work") { + t.Fatalf("the busy line %q does not say what happened", note) + } + if !hostAnswers(workspace) { + t.Fatal("a host that refused the stand-down was ended anyway") + } + if host.forced() { + t.Fatal("a host that refused the stand-down was asked to go regardless of it") + } + if !host.waitingSaid() { + t.Fatal("the host that refused was not asked to let go when it is quiet") + } +} + +// AND A HOST OLDER THAN THE NOTE IS NOT PROMISED IT. The ask is additive, so an +// already-installed older build ignores it and cannot retire at a quiet moment +// nobody told it about — which the acknowledgement distinguishes, so the line a +// person reads says what actually gets them onto this build. +func TestAnOldHostThatIgnoresTheNoteIsNotPromisedAQuietMoment(t *testing.T) { + shortEngineHome(t) + workspace := "/home/somebody/api" + host := standInAs(t, workspace, remote.HostSelf{ + Version: remote.Version, Build: "two-days-ago", BuiltAt: earlier, Busy: true, Workspace: workspace, + }, false, standInOptions{ignoresWhenIdle: true}) + + note, err := clearStaleEngineHostAs(workspace, window("today", later)) + if err != nil { + t.Fatalf("an old host was refused rather than joined: %v", err) + } + if strings.Contains(note, "once it goes quiet") { + t.Fatalf("the line promises a quiet-moment handover this host was never told about: %q", note) + } + for _, want := range []string{"older codeaf", "still holding work", "your next launch once the work is done"} { + if !strings.Contains(note, want) { + t.Fatalf("the line %q does not say %q", note, want) + } + } + if !hostAnswers(workspace) || host.forced() { + t.Fatal("an old host that cannot step aside on its own was ended rather than kept") + } + if !host.waitingSaid() { + t.Fatal("the note was not even delivered, so a newer host could not use it") + } +} + +// THE LAUNCH ITSELF REFUSES, and that is the whole of this test: an engine +// holding the workspace on a wire this build cannot speak must NOT fall through +// to a conversation in this process, where it would be a second writer beside +// the journal the engine is holding. +func TestALaunchRefusesAnIncompatibleBusyEngineRatherThanFallingBack(t *testing.T) { + shortEngineHome(t) + workspace := t.TempDir() + standIn(t, workspace, remote.HostSelf{ + Version: remote.Version - 1, Busy: true, Build: "ancient", BuiltAt: earlier, Workspace: workspace, + }, false) + + err := openChatV3Local(localLaunch{workspace: workspace}) + if err == nil { + t.Fatal("a launch against a busy engine on another wire came back with nothing to say") + } + var unreachable *hostUnreachable + if errors.As(err, &unreachable) { + t.Fatalf("the launch fell back to an in-process conversation beside a live engine: %q", unreachable.reason) + } + var refusal *hostRefusal + if !errors.As(err, &refusal) { + t.Fatalf("the launch answered %v, want the host's own refusal", err) + } + if !strings.Contains(refusal.sentence, "different protocol") || !strings.Contains(refusal.sentence, "engine --stop") { + t.Fatalf("the refusal %q does not say what is holding it and the way out", refusal.sentence) + } + // AND NOTHING WAS OPENED IN THIS PROCESS: no project tree, no journal. + if entries, readErr := os.ReadDir(workspace); readErr != nil || len(entries) != 0 { + t.Fatalf("the refused launch left something in the workspace: %v, %v", entries, readErr) + } + if _, statErr := os.Stat(filepath.Join(os.Getenv("CODEAF_HOME"), "v3", "projects")); statErr == nil { + t.Fatal("the refused launch opened a conversation in this process") } } -// Another wire and older is the same answer: replaced, not refused. -func TestAnOlderEngineOnAnotherWireIsReplaced(t *testing.T) { +// A BUILD WITH NO STAMP IS OLDER THAN EVERY STAMP, and older is not a reason to +// end somebody's turn: it is joined like any other build this wire can speak. +func TestAnOlderBuildWithNoStampHoldingWorkIsJoined(t *testing.T) { shortEngineHome(t) workspace := "/home/somebody/api" - standIn(t, workspace, remote.HostSelf{Version: remote.Version - 1, Busy: true}, false) + host := standIn(t, workspace, remote.HostSelf{Version: remote.Version, Build: "previous-build", Busy: true}, false) note, err := clearStaleEngineHost(workspace) if err != nil { - t.Fatalf("an older engine on another wire was refused: %v", err) + t.Fatalf("a stampless older build holding work was refused: %v", err) } - if !strings.Contains(note, "replaced the older engine") { - t.Fatalf("the takeover said nothing: %q", note) + if !strings.Contains(note, "older codeaf") { + t.Fatalf("the notice did not say which engine this is: %q", note) } - if conn, err := enginehost.Dial(workspace); err == nil { - _ = conn.Close() - t.Fatal("the older engine was still answering") + if strings.Contains(note, "--stop") { + t.Fatalf("the notice sends somebody off to stop something by hand: %q", note) + } + if !hostAnswers(workspace) || !host.waitingSaid() || host.forced() { + t.Fatal("a stampless older build was ended rather than joined") } } -// Idle or busy makes no difference to WHETHER an older engine is replaced, and -// a stamp it does not carry reads as older than every stamp. -func TestASameProtocolOlderBuildIsReplacedBusyOrNot(t *testing.T) { - for _, tc := range []struct { - name string - stamp string - busy bool - }{ - {"idle", "previous-build", false}, - {"busy", "previous-build", true}, - {"no-stamp", "", false}, - } { - t.Run(tc.name, func(t *testing.T) { - shortEngineHome(t) - workspace := "/home/somebody/api" - standIn(t, workspace, remote.HostSelf{Version: remote.Version, Build: tc.stamp, Busy: tc.busy}, false) - note, err := clearStaleEngineHost(workspace) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(note, "replaced the older engine") { - t.Fatalf("the notice did not say an older engine was replaced: %q", note) - } - if strings.Contains(note, "--stop") { - t.Fatalf("the notice sends somebody off to stop something by hand: %q", note) - } - if conn, err := enginehost.Dial(workspace); err == nil { - _ = conn.Close() - t.Fatal("stale same-protocol host still answers") - } - }) +// A SECOND WINDOW MEETS THE SAME OLDER ENGINE AND THE TWO OF THEM DO NOTHING TO +// IT: no kill, no replacement, one engine still holding the one conversation. +// This is the half of #1788 that is about windows rather than turns — two +// terminals opening codeaf must not race each other into ending anything. +func TestTwoWindowsMeetingAnOlderEngineHoldingWorkBothJoinIt(t *testing.T) { + shortEngineHome(t) + workspace := "/home/somebody/api" + host := standIn(t, workspace, remote.HostSelf{ + Version: remote.Version, Build: "two-days-ago", BuiltAt: earlier, Busy: true, Workspace: workspace, + }, false) + + for opened := 1; opened <= 2; opened++ { + note, err := clearStaleEngineHostAs(workspace, window("today", later)) + if err != nil { + t.Fatalf("window %d was refused: %v", opened, err) + } + if !strings.Contains(note, "older codeaf") { + t.Fatalf("window %d was not told which engine it is on: %q", opened, note) + } + } + if !hostAnswers(workspace) { + t.Fatal("two windows ended the engine they joined") + } + if host.forced() { + t.Fatal("two windows asked a busy engine to go regardless of its work") + } +} + +// AND A HOST THAT RETIRED ON ITS OWN BETWEEN THE QUESTION AND THE STAND-DOWN IS +// NOT A FAILURE: nothing is holding the workspace, so the launch attaches and +// starts a host from this build. It is the failed-replacement road. +func TestAnIdleOlderEngineThatWentAwayLetsTheLaunchStartItsOwn(t *testing.T) { + shortEngineHome(t) + workspace := "/home/somebody/api" + standInAs(t, workspace, remote.HostSelf{Version: remote.Version, Build: "two-days-ago", BuiltAt: earlier}, false, standInOptions{vanishAfter: 1}) + + note, err := clearStaleEngineHostAs(workspace, window("today", later)) + if err != nil || note != "" { + t.Fatalf("a host that had already gone answered %q, %v — want a quiet go-ahead", note, err) + } + if hostAnswers(workspace) { + t.Fatal("something is still holding the workspace after it vanished") } } @@ -265,10 +498,8 @@ func TestANewerEngineIsJoinedAndNeverReplacedByAnOlderWindow(t *testing.T) { if note != "" { t.Fatalf("joining a newer engine owed no sentence, said %q", note) } - if conn, err := enginehost.Dial(workspace); err != nil { - t.Fatalf("an older window took the slot from a newer engine: %v", err) - } else { - _ = conn.Close() + if !hostAnswers(workspace) { + t.Fatal("an older window took the slot from a newer engine") } } @@ -287,10 +518,63 @@ func TestANewerEngineOnAnotherWireIsRefusedAndLeftAlone(t *testing.T) { if !strings.Contains(stale.reason, "newer codeaf") || !strings.Contains(stale.reason, "codeaf engine --status --workspace "+workspace) { t.Fatalf("the refusal does not say this binary is the older one and how to see which is newer: %q", stale.reason) } - if conn, err := enginehost.Dial(workspace); err != nil { - t.Fatalf("a newer engine was taken down: %v", err) - } else { - _ = conn.Close() + if !hostAnswers(workspace) { + t.Fatal("a newer engine was taken down") + } +} + +// AN OLDER ENGINE ON ANOTHER WIRE, HOLDING WORK, IS REFUSED AND NEVER KILLED. +// This build cannot speak to it, so there is nothing to join; and the work it +// is holding is the reason there is nothing to end either. A person is told +// which command says so. +func TestAnOlderEngineOnAnotherWireHoldingWorkIsRefusedNotKilled(t *testing.T) { + shortEngineHome(t) + workspace := "/home/somebody/api" + host := standIn(t, workspace, remote.HostSelf{ + Version: remote.Version - 1, Busy: true, Build: "ancient", BuiltAt: earlier, Workspace: workspace, + }, false) + + _, err := clearStaleEngineHostAs(workspace, window("today", later)) + var stale *staleHost + if !errors.As(err, &stale) { + t.Fatalf("an older engine on another wire holding work answered %v, want a refusal", err) + } + // AND THE COMMAND IS COPY-PASTEABLE: the folder is quoted, because a path + // with a space in it typed out of a sentence has to be one argument. + for _, want := range []string{"different protocol", "engine --stop", "--workspace " + shellQuote(workspace)} { + if !strings.Contains(stale.reason, want) { + t.Fatalf("the refusal %q does not say %q", stale.reason, want) + } + } + if !hostAnswers(workspace) { + t.Fatal("an older engine on another wire was taken down while holding work") + } + if host.forced() { + t.Fatal("an older engine on another wire was asked to go regardless of its work") + } +} + +// AND AN OLDER ENGINE ON ANOTHER WIRE THAT IS HOLDING NOTHING IS ASKED TO GO, +// on the host's own terms: it cannot be joined, so the slot has to change hands +// for the launch to work at all. +func TestAnOlderEngineOnAnotherWireHoldingNothingIsRetired(t *testing.T) { + shortEngineHome(t) + workspace := "/home/somebody/api" + host := standIn(t, workspace, remote.HostSelf{Version: remote.Version - 1, Build: "ancient", BuiltAt: earlier}, false) + + note, err := clearStaleEngineHostAs(workspace, window("today", later)) + if err != nil { + t.Fatalf("an idle older engine on another wire was refused: %v", err) + } + if !strings.Contains(note, "replaced the older engine") { + t.Fatalf("the handover said nothing: %q", note) + } + if hostAnswers(workspace) { + t.Fatal("the older engine was still answering after the handover") + } + asked := host.questions() + if len(asked) == 0 || !asked[len(asked)-1].StandDown || asked[len(asked)-1].Anyway { + t.Fatalf("the stand-down was not the host's own decision: %+v", asked) } } @@ -310,16 +594,14 @@ func TestAHostBuiltFromTheSameSourceAnotherMinuteIsSplicedOnto(t *testing.T) { if note != "" { t.Fatalf("a host built from the same source owed a sentence: %q", note) } - if conn, err := enginehost.Dial(workspace); err != nil { - t.Fatalf("the matching host was asked to retire: %v", err) - } else { - _ = conn.Close() + if !hostAnswers(workspace) { + t.Fatal("the matching host was asked to retire") } } -// SAME SOURCE, ANOTHER FILE, OLDER: replaced — `~/.codeaf/bin/devaf` and -// `bin/codeaf` built from one commit two days apart are two builds. -func TestTheSameSourceFromAnOlderOtherFileIsReplaced(t *testing.T) { +// SAME SOURCE, ANOTHER FILE, OLDER, HOLDING NOTHING: retired — `~/.codeaf/bin/devaf` +// and `bin/codeaf` built from one commit two days apart are two builds. +func TestTheSameSourceFromAnOlderOtherFileIsRetired(t *testing.T) { shortEngineHome(t) workspace := "/home/somebody/api" standIn(t, workspace, remote.HostSelf{Version: remote.Version, Build: "c85e10a19", Binary: "/somewhere/else/devaf", BuiltAt: earlier}, false) @@ -329,7 +611,7 @@ func TestTheSameSourceFromAnOlderOtherFileIsReplaced(t *testing.T) { t.Fatal(err) } if !strings.Contains(note, "replaced the older engine") { - t.Fatalf("an older copy from another file was not replaced: %q", note) + t.Fatalf("an older copy from another file was not retired: %q", note) } } @@ -344,10 +626,8 @@ func TestTwoCopiesOfOneBuildDoNotTakeTheSlotFromEachOther(t *testing.T) { if err != nil || note != "" { t.Fatalf("a copy of the same build was replaced (%q, %v)", note, err) } - if conn, err := enginehost.Dial(workspace); err != nil { - t.Fatalf("the copy was asked to go: %v", err) - } else { - _ = conn.Close() + if !hostAnswers(workspace) { + t.Fatal("the copy was asked to go") } } @@ -402,12 +682,13 @@ func TestHelperOldEngineHost(t *testing.T) { } } -// The build that trapped somebody for real cannot be asked anything at all, and -// it used to be left in place with a sentence telling the person to stop it by -// hand. It is older than everything, so it is replaced: the kernel names the -// process on the other end of its socket and it is sent the signal it has -// always answered by flushing and exiting. -func TestAnEngineTooOldToBeAskedIsReplacedThroughItsPid(t *testing.T) { +// THE BUILD THAT TRAPPED SOMEBODY FOR REAL cannot be asked anything at all: it +// says nothing about what it holds or what it is doing. It used to be signalled +// through the pid on its socket, which is a turn somebody may have been watching +// ended by a rebuild. It is refused in words now — and the words are true, +// because the one thing that still ends it is a person typing the stop, which is +// the same test's second half. +func TestAnEngineTooOldToBeAskedIsRefusedAndTheExplicitStopStillEndsIt(t *testing.T) { shortEngineHome(t) workspace := "/home/somebody/api" if _, err := enginehost.Dir(workspace); err != nil { @@ -454,17 +735,32 @@ func TestAnEngineTooOldToBeAskedIsReplacedThroughItsPid(t *testing.T) { t.Fatalf("status of a too-old engine: %+v, %v — want unanswered, pid %d", held, err, pid) } - note, err := clearStaleEngineHost(workspace) - if err != nil { - t.Fatalf("a too-old engine was refused rather than replaced: %v", err) + _, err = clearStaleEngineHost(workspace) + var stale *staleHost + if !errors.As(err, &stale) { + t.Fatalf("a too-old engine answered %v, want a refusal in words", err) + } + for _, want := range []string{"cannot say what it is", "engine --stop", "--workspace " + shellQuote(workspace)} { + if !strings.Contains(stale.reason, want) { + t.Fatalf("the refusal %q does not say %q", stale.reason, want) + } } - if !strings.Contains(note, "replaced the older engine") || !strings.Contains(note, fmt.Sprintf("pid %d", pid)) { - t.Fatalf("the takeover line did not name the process it replaced: %q", note) + select { + case <-exited: + t.Fatal("a too-old engine was ended without being asked") + case <-time.After(300 * time.Millisecond): + } + + // AND THE EXPLICIT STOP IS UNCHANGED: the pid road is still there for the + // one caller a person is. `codeaf engine --stop` is [enginehost.Stop]. + stopped, err := enginehost.Stop(workspace) + if err != nil || !stopped { + t.Fatalf("the explicit stop answered %v, %v — want the host ended", stopped, err) } select { case <-exited: case <-time.After(10 * time.Second): - t.Fatal("the old engine was not ended") + t.Fatal("the explicit stop did not end the old engine") } } diff --git a/cmd/codeaf/engine_takeover_law_test.go b/cmd/codeaf/engine_takeover_law_test.go new file mode 100644 index 0000000000..dc8083c540 --- /dev/null +++ b/cmd/codeaf/engine_takeover_law_test.go @@ -0,0 +1,75 @@ +package main + +// A LAW ABOUT THE TWO ROADS THAT MEET AN ENGINE ALREADY HOLDING A WORKSPACE: +// [clearStaleEngineHost] in engine.go and the local dial in chatv3_local.go. +// +// An engine may be ended from here only by ASKING it (the host decides, under +// its own lock, whether it may go) and the road may not MIGRATE anything on the +// way to that question. The first is what keeps a turn, a waiting question and +// handed-off work alive when a newer build connects; the second is what keeps a +// connecting client from rewriting state the engine it is about to join is still +// reading — a shared store or layout that changes under a running engine is a +// protocol break, and the wire version is where that has to be said (see the +// engine-update plan). +// +// It is structural because the road is short and the temptation is a line: a +// pid-signal for the host that will not go, a migration called "just to be safe". +// The one signal in the tree is `codeaf engine --stop`, which is a person saying +// so, and it lives in the two doors named below. + +import ( + "go/ast" + "go/parser" + "go/token" + "testing" +) + +func TestTheTakeoverRoadAsksTheEngineAndMigratesNothing(t *testing.T) { + // The two doors a person types, and the only callers of the explicit stop. + personDoors := map[string]bool{"runEngineStop": true, "runEngineStopAll": true} + forbidden := map[string]string{ + "enginehost.Stop": "the only force is a person's own `codeaf engine --stop`", + "migrateV3Layout": "a launch may not move a running engine's data", + "migrateV3Tree": "a launch may not move a running engine's data", + "store.Open": "a launch may not open shared storage before it has a host", + } + for _, file := range []string{"engine.go", "chatv3_local.go"} { + parsed, err := parser.ParseFile(token.NewFileSet(), file, nil, 0) + if err != nil { + t.Fatalf("parse %s: %v", file, err) + } + enclosing := "" + ast.Inspect(parsed, func(node ast.Node) bool { + switch one := node.(type) { + case *ast.FuncDecl: + enclosing = one.Name.Name + case *ast.CallExpr: + called := calledName(one.Fun) + why, banned := forbidden[called] + if !banned { + return true + } + if called == "enginehost.Stop" && personDoors[enclosing] { + return true + } + t.Errorf("%s: %s calls %s — %s", file, enclosing, called, why) + } + return true + }) + } +} + +// calledName is the name a call was made under: "pkg.Func" for a selector and +// "Func" for a bare identifier, "" for anything else (a call through a field, a +// variable, an interface — which this law cannot read and does not guess at). +func calledName(fun ast.Expr) string { + switch one := fun.(type) { + case *ast.Ident: + return one.Name + case *ast.SelectorExpr: + if pkg, ok := one.X.(*ast.Ident); ok { + return pkg.Name + "." + one.Sel.Name + } + } + return "" +} diff --git a/internal/enginehost/enginehost.go b/internal/enginehost/enginehost.go index 86bb464dff..0c34b3b7dc 100644 --- a/internal/enginehost/enginehost.go +++ b/internal/enginehost/enginehost.go @@ -320,6 +320,16 @@ func Retire(workspace string, anyway bool) error { return retire(workspace, remote.WhoIs{StandDown: true, Anyway: anyway}) } +// StandDownWhenIdle asks this workspace's host for the slot WITHOUT asking it to +// end anything: the host records that a newer build is waiting and retires at +// its first quiet moment rather than holding on through its idle policy. It is +// the busy half of a replacement and it is not a kill (cmd/codeaf's takeover +// rule); the answer's [remote.HostSelf.StandDownWhenIdle] says whether this host +// understood the request or is older than it. +func StandDownWhenIdle(workspace string) (remote.HostSelf, error) { + return Ask(workspace, remote.WhoIs{StandDownWhenIdle: true}) +} + // RetireForEnvironment asks only idle work to stand down, without allowing a // disconnected window's watch grace to preserve the environment it just left. // NEVER ANYWAY: actual windows, turns, questions and background work still refuse. diff --git a/internal/enginehost/host.go b/internal/enginehost/host.go index a90c490237..6dbce943b8 100644 --- a/internal/enginehost/host.go +++ b/internal/enginehost/host.go @@ -18,6 +18,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "syscall" "time" @@ -60,10 +61,20 @@ var ErrHostRunning = errors.New("engine host: another host already holds this wo // writes it. var sessionIdle = 30 * time.Minute -const ( - hostIdle = 2 * time.Minute - sweepEvery = 30 * time.Second -) +const hostIdle = 2 * time.Minute + +// sweepEvery is how often the policy above looks: a product's half-minute, or a +// test's override, since a test watching a host take its quiet moment should not +// wait out a clock. It is atomic because one test sets it while another test's +// host may still be starting its own ticker; nothing in the product writes it. +var sweepEvery atomic.Int64 + +func sweepSpan() time.Duration { + if span := time.Duration(sweepEvery.Load()); span > 0 { + return span + } + return 30 * time.Second +} // The two numbers a stand-down is measured in. // @@ -149,6 +160,10 @@ type Host struct { // closes the door on new conversations, because a conversation opened into // a process that is leaving is one the person watches vanish. retiring bool + // wanted is a newer build asking for this slot without asking anybody to + // stop: the host keeps its work and retires at its first quiet moment + // ([Host.sweepOnce]) rather than waiting out the idle policy. + wanted bool // quiet is when the host last had nothing to do, and zero while it has // something. It is what [hostIdle] is measured against. quiet time.Time @@ -503,6 +518,13 @@ func (h *Host) whois(ask remote.WhoIs) remote.HostSelf { // anything is measured, so that the measurement is right. h.probes++ self := remote.HostSelf{Workspace: h.workspace, Busy: !h.idleWithWatchGraceLocked(ask.StandDown && ask.IgnoreWatchGrace)} + // Recorded under the lock that is about to measure whether this host may + // go. It is a note and never a verdict: nothing about the work in flight + // changes, and the answer acknowledges it so the asker can tell this host + // apart from one too old to have heard of it. + if ask.StandDownWhenIdle { + h.wanted, self.StandDownWhenIdle = true, true + } // AND WHAT THIS PROCESS IS, for `codeaf engine --status` and for the door // deciding which of two builds is the older one. The counts are read under // the same lock as busy, so the three never disagree with each other. @@ -590,7 +612,7 @@ func (h *Host) standDown() { // sweep is the idle policy, run on a clock: retire the conversations nobody // wants any more, and then retire the host when there is nothing left to hold. func (h *Host) sweep() { - ticker := time.NewTicker(sweepEvery) + ticker := time.NewTicker(sweepSpan()) defer ticker.Stop() for { select { @@ -648,7 +670,18 @@ func (h *Host) sweepOnce() bool { leaving := h.live == 0 && len(h.sessions) == 0 && !h.quiet.IsZero() && time.Since(h.quiet) > hostIdle replaced := !leaving && h.binary.replaced() && h.idleLocked() - if replaced { + // THE QUIET MOMENT A NEWER BUILD ASKED FOR, decided under the same lock the + // retirement is set under — so a conversation that arrived since the note + // keeps this host alive as surely as it keeps it from being asked to go. + // + // It ignores the watch grace, as an environment replacement does + // ([remote.WhoIs.IgnoreWatchGrace]): that grace covers a stalled window + // about to redial, and a redial is what this swap is safe under, because + // the client at the other end is whatever binary is on disk now. Nothing + // real is ignored — a surface, a turn, a waiting question, handed-off + // work each still holds this host. + waited := !leaving && !replaced && h.wanted && h.idleWithWatchGraceLocked(true) + if replaced || waited { // The door closes under the same lock that decided, so a surface // arriving in the moment between here and the stop is refused rather // than handed a conversation that is about to end. @@ -659,6 +692,9 @@ func (h *Host) sweepOnce() bool { if replaced { h.note("the file this host was started from has been replaced; retiring so the next connection starts the current one") } + if waited { + h.note("a newer codeaf asked for this workspace; it is holding nothing now, so the next connection starts from the current build") + } return leaving } diff --git a/internal/enginehost/standdown_test.go b/internal/enginehost/standdown_test.go new file mode 100644 index 0000000000..e2c0205df1 --- /dev/null +++ b/internal/enginehost/standdown_test.go @@ -0,0 +1,391 @@ +package enginehost + +// standdown_test.go is the half of the takeover rule that lives on the host: +// a newer build may ask for this slot WITHOUT ending anybody's work, and the +// host decides when the moment has come. +// +// Three things are under test and they are the same three facts stated three +// ways: the note is a note (nothing stops while something is happening), the +// decision is the host's own (work arriving cancels it, under one lock), and the +// slot really does change hands once the work is gone — which is the promise +// every surface line about "picks up this build once it goes quiet" makes. + +import ( + "context" + "errors" + "sync" + "testing" + "time" + + "github.com/Agent-Field/codeaf/internal/remote" + "github.com/Agent-Field/codeaf/internal/session" +) + +// quietSweeps makes the host's policy look often enough for a test to watch what +// it decides, and puts it back whatever the test did. +func quietSweeps(t *testing.T, every time.Duration) { + t.Helper() + was := sweepEvery.Load() + sweepEvery.Store(int64(every)) + t.Cleanup(func() { sweepEvery.Store(was) }) +} + +// THE NOTE IS A NOTE. A host that is asked to let go when idle does not stop +// while it is holding anything — an attached surface is work, exactly as a turn +// or an unanswered question is. +func TestAStandDownWhenIdleWaitsForTheWorkInFlight(t *testing.T) { + h := stubHost(t, "/home/somebody/api") + h.dir = t.TempDir() + if _, err := h.open(remote.Hello{Version: remote.Version}); err != nil { + t.Fatalf("open: %v", err) + } + // Somebody is attached, and this very question is the other connection the + // host is holding: BEING ASKED IS NOT WORK, so the two are counted side by + // side and only the window is left over. + h.live = 2 + if self := h.whois(remote.WhoIs{StandDownWhenIdle: true}); self.Retiring { + t.Fatal("a host with a window on it agreed to go when it was only asked to wait") + } + if h.sweepOnce() { + t.Fatal("the policy retired a host that was still holding a window") + } + if h.retiring { + t.Fatal("the host closed its door while it was still holding work") + } + + // AND THE WINDOW GOES. Now the same note is the whole of what stands between + // this host and the newer build on disk. + h.live = 1 + if !h.sweepOnce() { + t.Fatal("the host did not take its quiet moment") + } + if !h.retiring { + t.Fatal("the host decided to leave without closing the door to new conversations") + } +} + +// THE DOOR CLOSES UNDER THE SAME LOCK THAT DECIDED, which is the atomicity the +// whole rule rests on: a conversation arriving in the moment between the +// decision and the stop is refused rather than handed to a process that is +// leaving. +func TestAPastAndRefusedWindowIsRefusedRatherThanHandedAConversation(t *testing.T) { + h := stubHost(t, "/home/somebody/api") + h.dir = t.TempDir() + + if self := h.whois(remote.WhoIs{StandDownWhenIdle: true}); self.Retiring { + t.Fatal("the note was read as a command") + } + if !h.sweepOnce() { + t.Fatal("an idle host that was asked to wait did not take its quiet moment") + } + if _, err := h.open(remote.Hello{Version: remote.Version}); err == nil { + t.Fatal("a conversation was opened into a host that is leaving") + } +} + +// THE NOTE IS DELIVERED, AND IT IS NOT A COMMAND: a real host answers it, keeps +// serving, and is not retiring a moment later. +func TestAStandDownWhenIdleIsDeliveredAndEndsNothingOnTheSpot(t *testing.T) { + shortHome(t) + workspace := "/home/somebody/api" + liveHost(t, workspace) + + if _, err := StandDownWhenIdle(workspace); err != nil { + t.Fatalf("ask the host to let go when it is quiet: %v", err) + } + self, err := Ask(workspace, remote.WhoIs{}) + if err != nil { + t.Fatalf("the host stopped answering: %v", err) + } + if self.Retiring { + t.Fatal("being asked to wait was answered as agreeing to go") + } + if self.Busy { + t.Fatal("the host called itself busy with nothing but the question on it") + } +} + +// THE WHOLE PROMISE, ON A REAL HOST: a window is on it, a newer build asks for +// the slot, the window leaves, and the host takes its own quiet moment. Nothing +// was killed; the host ended itself, which is the one ending that flushes every +// journal on the way out. +func TestAnOlderHostLetsGoAtItsFirstQuietMomentAfterBeingAsked(t *testing.T) { + shortHome(t) + quietSweeps(t, 20*time.Millisecond) + workspace := "/home/somebody/api" + liveHost(t, workspace) + + conn, err := Dial(workspace) + if err != nil { + t.Fatalf("dial the host: %v", err) + } + surface, err := remote.Dial(conn, "test", remote.Hello{Version: remote.Version}) + if err != nil { + t.Fatalf("handshake: %v", err) + } + + if _, err := StandDownWhenIdle(workspace); err != nil { + t.Fatalf("ask the host to let go when it is quiet: %v", err) + } + // It is holding a window, so several of its own sweeps go by and it stays. + time.Sleep(200 * time.Millisecond) + if _, err := Ask(workspace, remote.WhoIs{}); err != nil { + t.Fatalf("the host left with a window still on it: %v", err) + } + + // The window goes, and the slot is handed over without anything being + // signalled or forced. + _ = surface.Close() + deadline := time.Now().Add(5 * time.Second) + for { + if _, err := Ask(workspace, remote.WhoIs{}); err != nil { + break + } + if time.Now().After(deadline) { + t.Fatal("the host kept the slot after the window left") + } + time.Sleep(20 * time.Millisecond) + } +} + +// AND NOTHING A WINDOW ASKS CAN END THE WORK BESIDE IT. Many asks arriving while +// a surface is attached — the note and the stand-down both — leave the host +// holding exactly what it held, refusing every one of them. This is the race a +// status-then-kill pair used to lose, run many times over. +func TestManyAsksAtOnceNeverEndTheWorkInFlight(t *testing.T) { + shortHome(t) + workspace := "/home/somebody/api" + liveHost(t, workspace) + + conn, err := Dial(workspace) + if err != nil { + t.Fatalf("dial the host: %v", err) + } + surface, err := remote.Dial(conn, "test", remote.Hello{Version: remote.Version}) + if err != nil { + t.Fatalf("handshake: %v", err) + } + defer surface.Close() + + var waiters sync.WaitGroup + for i := 0; i < 16; i++ { + waiters.Add(1) + go func() { + defer waiters.Done() + if _, err := StandDownWhenIdle(workspace); err != nil { + t.Errorf("a note was refused: %v", err) + } + if err := Retire(workspace, false); !errors.Is(err, ErrHostBusy) { + t.Errorf("a stand-down against a busy host answered %v, want ErrHostBusy", err) + } + }() + } + waiters.Wait() + + self, err := Ask(workspace, remote.WhoIs{}) + if err != nil { + t.Fatalf("the host stopped answering: %v", err) + } + if !self.Busy || self.Retiring { + t.Fatalf("after many asks the host said busy=%v retiring=%v, want busy and staying", self.Busy, self.Retiring) + } +} + +// ── the two kinds of work a host holds without a window ───────────────────── + +// questionAgent stops on a question: its one turn asks for a bash command and +// ends, leaving the card waiting for somebody to answer it — and says so through +// the same door the real session does ([session.Question]), which is what the +// room reconciles against. +type questionAgent struct { + stubAgent + mu sync.Mutex + open bool +} + +func (a *questionAgent) Submit(context.Context, string) (<-chan session.Event, error) { + a.mu.Lock() + a.open = true + a.mu.Unlock() + events := make(chan session.Event, 1) + events <- session.Event{Kind: session.EventConsentRequest, ID: 7, Tool: "bash", CallID: "call-1"} + close(events) + return events, nil +} + +func (a *questionAgent) OpenQuestions() []session.Question { + a.mu.Lock() + defer a.mu.Unlock() + if !a.open { + return nil + } + return []session.Question{{ID: 7, Kind: session.QuestionConsent, Ask: session.AskPermission, Head: "run this command?"}} +} + +// settled is somebody answering the card, or its turn being over: either way the +// engine has no question open. +func (a *questionAgent) settled() { + a.mu.Lock() + a.open = false + a.mu.Unlock() +} + +// handoffAgent is a conversation that handed work off: the turn that started it +// is long over and the work is still going, which is what [WorkingNow] says. +type handoffAgent struct { + stubAgent + mu sync.Mutex + running bool +} + +func (a *handoffAgent) WorkingNow() []session.WorkNode { + a.mu.Lock() + defer a.mu.Unlock() + if !a.running { + return nil + } + return []session.WorkNode{{ID: "job-1", Title: "the handed-off job"}} +} + +func (a *handoffAgent) finished() { + a.mu.Lock() + a.running = false + a.mu.Unlock() +} + +// liveAgentHost is [liveHost] with a conversation of the test's own, so a test +// can put the host in a state that needs an agent rather than a socket. +func liveAgentHost(t *testing.T, workspace string, agent remote.WrappedAgent) { + t.Helper() + stopped := make(chan error, 1) + go func() { + stopped <- Run(workspace, Options{Boot: func(remote.Hello) (*remote.Engine, error) { + return &remote.Engine{Agent: agent, Workspace: workspace}, nil + }}) + }() + waitForHostQuietly(t, workspace) + t.Cleanup(func() { + _ = Retire(workspace, true) + select { + case <-stopped: + case <-time.After(5 * time.Second): + t.Error("the host did not exit") + } + }) +} + +// goneWithin waits for nothing to be holding the workspace any more. +func goneWithin(t *testing.T, workspace string, within time.Duration) { + t.Helper() + deadline := time.Now().Add(within) + for { + if _, err := Ask(workspace, remote.WhoIs{}); err != nil { + return + } + if time.Now().After(deadline) { + t.Fatal("the host kept the workspace") + } + time.Sleep(20 * time.Millisecond) + } +} + +// AN UNANSWERED QUESTION HOLDS THE ENGINE, and answering it is what lets the +// newer build in. The card is the case that a status-then-kill rule got worst: +// the turn is over, so a host that only counted turns read as idle, and the +// person's question was closed with the process that was waiting on it. +func TestAnUnansweredQuestionHoldsTheEngineAndItsAnswerLetsItGo(t *testing.T) { + shortHome(t) + quietSweeps(t, 20*time.Millisecond) + workspace := "/home/somebody/api" + agent := &questionAgent{} + liveAgentHost(t, workspace, agent) + + conn, err := Dial(workspace) + if err != nil { + t.Fatalf("dial the host: %v", err) + } + client, err := remote.Dial(conn, "test", remote.Hello{Version: remote.Version}) + if err != nil { + t.Fatalf("handshake: %v", err) + } + events, err := client.Agent().Submit(t.Context(), "ask me something") + if err != nil { + t.Fatalf("submit: %v", err) + } + deadline := time.After(10 * time.Second) +drain: + for { + select { + case event, ok := <-events: + if !ok { + break drain + } + if event.Kind == session.EventConsentRequest { + break drain + } + case <-deadline: + t.Fatal("the question never reached the surface") + } + } + _ = client.Close() + + // A QUESTION, NOT THE WATCH GRACE: asked with the grace ignored, the host + // still says it is holding something. + self, err := Ask(workspace, remote.WhoIs{StandDown: true, IgnoreWatchGrace: true}) + if err != nil || !self.Busy { + t.Fatalf("a host holding an unanswered question said busy=%v: %v", self.Busy, err) + } + if err := Retire(workspace, false); !errors.Is(err, ErrHostBusy) { + t.Fatalf("a stand-down against a waiting question answered %v, want ErrHostBusy", err) + } + // AND THE NOTE CANNOT END IT EITHER, for several of its own sweeps. + if _, err := StandDownWhenIdle(workspace); err != nil { + t.Fatalf("ask the host to let go when quiet: %v", err) + } + time.Sleep(200 * time.Millisecond) + if _, err := Ask(workspace, remote.WhoIs{}); err != nil { + t.Fatalf("the host left with a question nobody had answered: %v", err) + } + + // THE ANSWER IS WHAT OPENS THE QUIET MOMENT. + agent.settled() + goneWithin(t, workspace, 5*time.Second) +} + +// WORK HANDED OFF OUTLIVES ITS TURN AND ITS WINDOW, and there is no window here +// at all: a task running on after the turn ended is not an idle engine, so the +// newer build waits for it rather than taking it down with the process. +func TestWorkHandedOffHoldsTheEngineAndItsEndLetsItGo(t *testing.T) { + shortHome(t) + quietSweeps(t, 20*time.Millisecond) + workspace := "/home/somebody/api" + agent := &handoffAgent{running: true} + liveAgentHost(t, workspace, agent) + + // A conversation has to exist for work to have been handed off from: it is + // the same road an ordinary window takes, and it closes again — the job + // outlives both the turn that started it and the window that watched it. + conn, err := Dial(workspace) + if err != nil { + t.Fatalf("dial the host: %v", err) + } + client, err := remote.Dial(conn, "test", remote.Hello{Version: remote.Version}) + if err != nil { + t.Fatalf("handshake: %v", err) + } + _ = client.Close() + + if err := Retire(workspace, false); !errors.Is(err, ErrHostBusy) { + t.Fatalf("a stand-down against handed-off work answered %v, want ErrHostBusy", err) + } + if _, err := StandDownWhenIdle(workspace); err != nil { + t.Fatalf("ask the host to let go when quiet: %v", err) + } + time.Sleep(200 * time.Millisecond) + if _, err := Ask(workspace, remote.WhoIs{}); err != nil { + t.Fatalf("the host left with work it had handed off: %v", err) + } + + agent.finished() + goneWithin(t, workspace, 5*time.Second) +} diff --git a/internal/remote/whois.go b/internal/remote/whois.go index f7ff85500e..172c3e6192 100644 --- a/internal/remote/whois.go +++ b/internal/remote/whois.go @@ -19,22 +19,26 @@ package remote // person to update a machine they had just updated. The two halves were the // same build. The half in the middle was not. // -// So the splice stopped being a blind copy of bytes. Before it hands the -// surface over, `codeaf engine` asks the socket what it is, and a host that -// answers with another protocol is retired and replaced rather than attached -// to. THE QUESTION IS ASKED BEFORE THE HELLO, on a connection that never +// So the splice stopped being a blind copy of bytes: `codeaf engine` asks the +// socket what it is and acts on the answer. An older host holding work is +// joined and asked — [WhoIs.StandDownWhenIdle] — to let go when it is quiet; one +// holding nothing is stood down. Nothing on this road kills a busy engine any +// more, and cmd/codeaf's takeover rule has the whole table. +// +// THE QUESTION IS ASKED BEFORE THE HELLO, on a connection that never // becomes a surface, because a hello would open a conversation — which is the // expensive, journal-locking thing this exchange exists to avoid doing twice. // // ── AND WHY IT IS SAFE TO ASK A HOST TO LEAVE ─────────────────────────────── // // The host answers the question about itself, so it is the host that decides. -// A conversation with a surface attached, a turn running or a question waiting -// is WORK IN FLIGHT and a host holding any of it says [HostSelf.Busy] and stays -// exactly where it is; the door then refuses in words that say so. Nothing here -// can end somebody else's turn by accident, because nothing here does the -// ending — [WhoIs.StandDown] is a request, and the only process that knows -// whether it may be granted is the one being asked. +// Work in flight — a surface, a turn, a waiting question — reads [HostSelf.Busy] +// and the door joins it rather than ending anything, because nothing here does +// the ending: StandDown is a request and StandDownWhenIdle is a note. The +// decision and the admission happen under one lock (internal/enginehost's +// [Host.whois] and [Host.open]), so a host that agreed to go refuses the +// conversation arriving a moment later and one that admitted a conversation +// refuses to go. import ( "bufio" @@ -61,6 +65,14 @@ type WhoIs struct { // A stalled window may redial onto the replacement, with every journal // flushed and reopened, exactly as an older-build replacement already does. IgnoreWatchGrace bool `json:"ignoreWatchGrace,omitempty"` + // StandDownWhenIdle asks for the slot WITHOUT ending anybody's work: the + // host keeps everything it is holding and retires at its first quiet + // moment, instead of waiting out its idle policy (cmd/codeaf's takeover + // rule). It is a note and not a command — the answer never carries + // Retiring because of it. A host older than this field ignores it, as it + // ignores every field it does not know, so the answer says whether it was + // understood ([HostSelf.StandDownWhenIdle]). + StandDownWhenIdle bool `json:"standDownWhenIdle,omitempty"` // Anyway asks for the retirement even with work in flight, and there is // exactly one caller: a person typing `codeaf engine --stop` on the machine // itself, who has been told what is running and said stop anyway. A turn @@ -93,6 +105,12 @@ type HostSelf struct { // name it. Workspace string `json:"workspace,omitempty"` + // StandDownWhenIdle says THIS HOST UNDERSTOOD the request of that name and + // has recorded it. Absence is false: a host older than the field ignored + // the ask and cannot retire at a quiet moment nobody told it about, so the + // door that asked reads this before it promises anything. + StandDownWhenIdle bool `json:"standDownWhenIdle,omitempty"` + // ── WHAT A PERSON TYPING `codeaf engine --status` IS TOLD ────────────── // // The five fields below are the process's account of itself, and they From 68b2e74d8f34100a650358b1f7e77119feed4ee4 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 11:04:17 -0400 Subject: [PATCH 03/12] update: the offer answers the row that is really there, and one installer per real file What was true: the countdown re-read the setting it had captured when the window started, so a row turned off in another window was installed over anyway; the launch check was suppressed whenever `update.auto` was off, so the quiet line the change entry promised was never drawn; the bare `/update` that answered an offer carried the offered tag into the installer as if the person had named it, so a file another window had advanced could be rolled back; an explicit tag equal to this process's own stamp never reached the installer, whose record is the only thing that knows what the file holds. What is true now: - The automatic road asks the PROFILE, live, on every countdown beat, at the deadline and AGAIN after the resolver answers: a row turned off, or a release skipped, in another window stops this one before anything is downloaded. `/update` typed by hand stays the person's own answer and is not re-checked. - The offer's own clause survives a narrow width over a running turn: the turn's clauses are shortened first, so the one control the countdown has is never the first thing dropped. The clock WAITS wherever the offer cannot be read or answered (a question page, /settings and the other places, the first-run sheet, a team's card, the provider panel, the setting panel's own boxes) instead of spending the grace invisibly. - The launch check runs with `update.auto` off: the release is named on one dim line and never installed on its own. Only CODEAF_NO_UPDATE_CHECK silences the request. A production runSurface test proves the request is made and the file is untouched with the row off. - Intent is carried, not inferred: only a tag a person NAMED may roll back. A named tag reaches the installer even when it matches the running stamp, and a bare `/update`, a channel or the automatic road never steps the file back. - Skip is per release and says so; a skip the profile could not save is reported instead of claimed. `/update never` says an install already running finishes. - The ready note names what is true without the engine lane's vocabulary: new windows open the updated app, and work already running keeps its current engine until all its windows and work close. - The install target is canonicalized before the lock, the refusal, the temporary file and the replace, so an alias of a held target answers in flight and a package-managed file reached through a symlink is still refused. Two new tests hold real locks across two processes and through a symlinked folder. - The `update.auto` row is registered in the config reader table and the profile key ledger, so the derivation law stops failing on the row the previous commit added. - The `failure` render fixture goes through the production handler with a synthetic error, and `scripts/update-drive.sh` also captures the REAL /settings page with the auto update row on (default) and off. Captures run with an isolated home, an empty environment, `--no-host`, the launch check off, a loopback provider endpoint, and remove only the directory they made. Focused checks: internal/update (whole package), internal/config, internal/namelaw, internal/manual, the update/offer tests in internal/tui3, the Manual tests, the update and surface tests in cmd/codeaf, and `make build`. The credits_test failure in internal/tui3 was reproduced on the integrated base and is untouched. --- cmd/codeaf/chatv3_surface.go | 26 +- cmd/codeaf/chatv3_surface_test.go | 126 +++++++ .../unreleased/1789-background-updates.md | 30 +- .../downloading-120c.ansi | 40 +++ .../downloading-120c.txt | 40 +++ .../downloading-80c.ansi | 40 +++ .../downloading-80c.txt | 40 +++ .../failure-120c.ansi | 40 +++ .../failure-120c.txt | 40 +++ .../failure-80c.ansi | 40 +++ .../background-updates-frames/failure-80c.txt | 40 +++ .../manual-120c.ansi | 40 +++ .../background-updates-frames/manual-120c.txt | 40 +++ .../background-updates-frames/manual-80c.ansi | 40 +++ .../background-updates-frames/manual-80c.txt | 40 +++ .../background-updates-frames/off-120c.ansi | 40 +++ .../background-updates-frames/off-120c.txt | 40 +++ .../background-updates-frames/off-80c.ansi | 40 +++ .../background-updates-frames/off-80c.txt | 40 +++ .../background-updates-frames/offer-120c.ansi | 40 +++ .../background-updates-frames/offer-120c.txt | 40 +++ .../background-updates-frames/offer-80c.ansi | 40 +++ .../background-updates-frames/offer-80c.txt | 40 +++ .../background-updates-frames/ready-120c.ansi | 40 +++ .../background-updates-frames/ready-120c.txt | 40 +++ .../background-updates-frames/ready-80c.ansi | 40 +++ .../background-updates-frames/ready-80c.txt | 40 +++ .../ready-working-120c.ansi | 40 +++ .../ready-working-120c.txt | 40 +++ .../ready-working-80c.ansi | 40 +++ .../ready-working-80c.txt | 40 +++ .../settings-auto-off-120c.ansi | 40 +++ .../settings-auto-off-120c.txt | 40 +++ .../settings-auto-off-80c.ansi | 40 +++ .../settings-auto-off-80c.txt | 40 +++ .../settings-auto-on-120c.ansi | 40 +++ .../settings-auto-on-120c.txt | 40 +++ .../settings-auto-on-80c.ansi | 40 +++ .../settings-auto-on-80c.txt | 40 +++ docs/design/background-updates.md | 83 ++++- internal/config/derivation_test.go | 5 + internal/config/testdata/profile-keys.ledger | 1 + .../manual/chat/running-from-the-terminal.md | 12 +- internal/tui3/app.go | 5 +- internal/tui3/steer.go | 26 +- internal/tui3/tui3.go | 10 +- internal/tui3/updatecmd.go | 62 +++- internal/tui3/updatedemo.go | 15 +- internal/tui3/updateoffer.go | 173 ++++++++-- internal/tui3/updateoffer_test.go | 326 +++++++++++++++++- internal/update/auto.go | 48 ++- internal/update/auto_test.go | 157 +++++++++ internal/update/install.go | 6 +- scripts/update-drive.sh | 188 +++++++--- 54 files changed, 2584 insertions(+), 155 deletions(-) create mode 100644 docs/design/background-updates-frames/downloading-120c.ansi create mode 100644 docs/design/background-updates-frames/downloading-120c.txt create mode 100644 docs/design/background-updates-frames/downloading-80c.ansi create mode 100644 docs/design/background-updates-frames/downloading-80c.txt create mode 100644 docs/design/background-updates-frames/failure-120c.ansi create mode 100644 docs/design/background-updates-frames/failure-120c.txt create mode 100644 docs/design/background-updates-frames/failure-80c.ansi create mode 100644 docs/design/background-updates-frames/failure-80c.txt create mode 100644 docs/design/background-updates-frames/manual-120c.ansi create mode 100644 docs/design/background-updates-frames/manual-120c.txt create mode 100644 docs/design/background-updates-frames/manual-80c.ansi create mode 100644 docs/design/background-updates-frames/manual-80c.txt create mode 100644 docs/design/background-updates-frames/off-120c.ansi create mode 100644 docs/design/background-updates-frames/off-120c.txt create mode 100644 docs/design/background-updates-frames/off-80c.ansi create mode 100644 docs/design/background-updates-frames/off-80c.txt create mode 100644 docs/design/background-updates-frames/offer-120c.ansi create mode 100644 docs/design/background-updates-frames/offer-120c.txt create mode 100644 docs/design/background-updates-frames/offer-80c.ansi create mode 100644 docs/design/background-updates-frames/offer-80c.txt create mode 100644 docs/design/background-updates-frames/ready-120c.ansi create mode 100644 docs/design/background-updates-frames/ready-120c.txt create mode 100644 docs/design/background-updates-frames/ready-80c.ansi create mode 100644 docs/design/background-updates-frames/ready-80c.txt create mode 100644 docs/design/background-updates-frames/ready-working-120c.ansi create mode 100644 docs/design/background-updates-frames/ready-working-120c.txt create mode 100644 docs/design/background-updates-frames/ready-working-80c.ansi create mode 100644 docs/design/background-updates-frames/ready-working-80c.txt create mode 100644 docs/design/background-updates-frames/settings-auto-off-120c.ansi create mode 100644 docs/design/background-updates-frames/settings-auto-off-120c.txt create mode 100644 docs/design/background-updates-frames/settings-auto-off-80c.ansi create mode 100644 docs/design/background-updates-frames/settings-auto-off-80c.txt create mode 100644 docs/design/background-updates-frames/settings-auto-on-120c.ansi create mode 100644 docs/design/background-updates-frames/settings-auto-on-120c.txt create mode 100644 docs/design/background-updates-frames/settings-auto-on-80c.ansi create mode 100644 docs/design/background-updates-frames/settings-auto-on-80c.txt diff --git a/cmd/codeaf/chatv3_surface.go b/cmd/codeaf/chatv3_surface.go index d00e3540c7..bc2ef7d242 100644 --- a/cmd/codeaf/chatv3_surface.go +++ b/cmd/codeaf/chatv3_surface.go @@ -68,11 +68,15 @@ func runSurface(ctx context.Context, options tui3.Options) error { autoUpdate := config.UpdateAutoAt(options.ProfileDir) options.UpdateAuto = &tui3.UpdateCoordinator{ Enabled: autoUpdate, - State: func() codeupdate.AutoState { return codeupdate.LoadAutoState(options.ProfileDir) }, - Dismiss: func(tag string) error { return codeupdate.DismissRelease(options.ProfileDir, tag) }, - Failure: func(tag string, failure error) { codeupdate.AutoFailure(options.ProfileDir, tag, failure) }, - Success: func() { codeupdate.AutoSuccess(options.ProfileDir) }, - Disable: func() error { return config.SaveUpdateAuto(options.ProfileDir, false) }, + // THE LIVE READER IS THE PROFILE ITSELF, and it is what the countdown + // and the deadline ask: /settings updates this window's Enabled, while + // a row changed in ANOTHER window is only visible by reading it again. + EnabledLive: func() bool { return config.UpdateAutoAt(options.ProfileDir) }, + State: func() codeupdate.AutoState { return codeupdate.LoadAutoState(options.ProfileDir) }, + Dismiss: func(tag string) error { return codeupdate.DismissRelease(options.ProfileDir, tag) }, + Failure: func(tag string, failure error) { codeupdate.AutoFailure(options.ProfileDir, tag, failure) }, + Success: func() { codeupdate.AutoSuccess(options.ProfileDir) }, + Disable: func() error { return config.SaveUpdateAuto(options.ProfileDir, false) }, // A BREW, DISTRO OR NIX FILE, OR ONE IN A FOLDER THIS ACCOUNT CANNOT // WRITE, is not ours to replace in place. Refusal: func() string { return codeupdate.InstallRefusal(executable, curl) }, @@ -80,9 +84,15 @@ func runSurface(ctx context.Context, options tui3.Options) error { options.UpdateCheck = func(check context.Context) (codeupdate.Available, bool) { return codeupdate.CheckLaunch(check, codeupdate.CheckOptions{ Running: revision, ProfileDir: options.ProfileDir, Client: client, - // OFF MEANS NO REQUEST AT ALL, not a hidden one. The environment - // value keeps its one-launch meaning beside the standing setting. - Disabled: !autoUpdate || internalenv.Get(codeupdate.NoUpdateCheckEnv) == "1", + // OFF STILL LOOKS, ONCE, AND ONLY THE ENVIRONMENT SILENCES THE + // REQUEST. `update.auto` off means codeaf never installs on its + // own and the surface says what is out on one dim line; the + // check itself is a read, not an act, and a person who turned the + // row off still deserves to know a release exists. Only + // CODEAF_NO_UPDATE_CHECK, which is a shell's decision to make no + // request at all, takes the look away — the launch check's own + // [codeupdate.CheckOptions.Disabled] contract. + Disabled: internalenv.Get(codeupdate.NoUpdateCheckEnv) == "1", Executable: executable, }) } diff --git a/cmd/codeaf/chatv3_surface_test.go b/cmd/codeaf/chatv3_surface_test.go index d4400fc70a..d0507cf1aa 100644 --- a/cmd/codeaf/chatv3_surface_test.go +++ b/cmd/codeaf/chatv3_surface_test.go @@ -15,6 +15,7 @@ import ( "runtime" "strings" "sync" + "sync/atomic" "testing" "time" @@ -344,3 +345,128 @@ func TestTheCrashLogAndTheRunningLogAreOneFileUnderAProfile(t *testing.T) { t.Fatalf("the sentence on screen names %q, not the file both wrote: %s", stderr.String(), want) } } + +// TestRunSurfaceChecksWithAutoOffAndNeverInstalls is the production regression +// for the wiring the fixture tests cannot see: `update.auto` OFF must still make +// the ONE launch request and say what is out (the quiet notice), while the +// surface never offers a countdown and never replaces the file by itself. Only +// CODEAF_NO_UPDATE_CHECK takes the look away. +// +// It drives [runSurface] itself, so the assertion is about the closure this door +// hands the surface rather than about a seam a test built for itself. +func TestRunSurfaceChecksWithAutoOffAndNeverInstalls(t *testing.T) { + asset := []byte("new executable") + digest := sha256.Sum256(asset) + var releases atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + switch { + case strings.HasSuffix(request.URL.Path, "/releases/latest"): + releases.Add(1) + fmt.Fprint(w, `{"tag_name":"v0.2.0"}`) + case strings.HasSuffix(request.URL.Path, "/checksums.txt"): + fmt.Fprintf(w, "%x codeaf-%s-%s\n", digest, runtime.GOOS, runtime.GOARCH) + default: + _, _ = w.Write(asset) + } + })) + defer server.Close() + + target := filepath.Join(t.TempDir(), "codeaf") + if err := os.WriteFile(target, []byte("old executable"), 0o755); err != nil { + t.Fatal(err) + } + oldRun := runSurfaceProgram + oldClient, oldExecutable := surfaceUpdateClient, surfaceExecutable + oldRunningExecutable := surfaceRunningExecutable + oldRevision, oldArguments := surfaceRevision, surfaceArguments + t.Cleanup(func() { + runSurfaceProgram = oldRun + surfaceUpdateClient, surfaceExecutable = oldClient, oldExecutable + surfaceRunningExecutable = oldRunningExecutable + surfaceRevision, surfaceArguments = oldRevision, oldArguments + }) + frame := &surfaceFrameWriter{ready: make(chan struct{})} + input, inputWriter := io.Pipe() + defer inputWriter.Close() + optionsSeen := make(chan tui3.Options, 1) + runSurfaceProgram = func(ctx context.Context, options tui3.Options) error { + optionsSeen <- options + options.Input = input + options.Output = frame + options.Width, options.Height = 80, 24 + return tui3.Run(ctx, options) + } + surfaceUpdateClient = func(revision string, timeout time.Duration) *codeupdate.Client { + return &codeupdate.Client{HTTP: server.Client(), APIBase: server.URL, DownloadBase: server.URL, Revision: revision} + } + surfaceExecutable = func(executable func() (string, error)) (string, error) { return target, nil } + surfaceRunningExecutable = func() (string, error) { return target, nil } + surfaceRevision = func() string { return "v0.1.1" } + surfaceArguments = func() []string { return []string{"chat"} } + + profile := t.TempDir() + if err := config.SaveUpdateAuto(profile, false); err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + finished := make(chan error, 1) + go func() { + finished <- runSurface(ctx, tui3.Options{ + Agent: &quietAgent{}, Workspace: "/tmp/lab", ProfileDir: profile, SessionFile: "/tmp/this.jsonl", + }) + }() + seen := <-optionsSeen + if seen.UpdateAuto == nil || seen.UpdateAuto.Enabled || seen.UpdateAuto.EnabledLive == nil { + cancel() + t.Fatalf("auto off did not wire an off coordinator with a live reader: %+v", seen.UpdateAuto) + } + + // THE ONE LOOK HAPPENS AND NAMES THE RELEASE. The offer's own countdown + // never appears, because the automatic road is off. + deadline := time.Now().Add(5 * time.Second) + for !strings.Contains(frame.String(), "is out") && time.Now().Before(deadline) { + time.Sleep(20 * time.Millisecond) + } + if !strings.Contains(frame.String(), "v0.2.0 is out") { + cancel() + t.Fatalf("the launch check did not say what is out with auto off:\n%s", frame.String()) + } + if got := releases.Load(); got != 1 { + cancel() + t.Fatalf("release check requests = %d, want exactly 1", got) + } + if strings.Contains(frame.String(), "installs in") { + cancel() + t.Fatalf("auto off offered a countdown:\n%s", frame.String()) + } + installed, err := os.ReadFile(target) + if err != nil || string(installed) != "old executable" { + cancel() + t.Fatalf("auto off replaced the executable: %q, %v", installed, err) + } + // AND THE COORDINATOR READS THE ROW FROM THE PROFILE, not from the value + // captured at wiring: another window's change is what the grace rechecks. + if err := config.SaveUpdateAuto(profile, true); err != nil { + t.Fatal(err) + } + if !seen.UpdateAuto.EnabledLive() { + cancel() + t.Fatal("the live reader did not see another window turn auto update on") + } + if err := config.SaveUpdateAuto(profile, false); err != nil { + t.Fatal(err) + } + cancel() + if err := <-finished; err != nil && !errors.Is(err, context.Canceled) { + t.Fatalf("surface exit: %v", err) + } + + // THE ENVIRONMENT IS THE ONLY THING THAT SILENCES THE LOOK. + t.Setenv(codeupdate.NoUpdateCheckEnv, "1") + if available, show := seen.UpdateCheck(context.Background()); show || available.Latest != "" { + t.Fatalf("CODEAF_NO_UPDATE_CHECK did not silence the check: %+v show=%t", available, show) + } + if got := releases.Load(); got != 1 { + t.Fatalf("a silenced check still made %d requests", got) + } +} diff --git a/docs/changes/unreleased/1789-background-updates.md b/docs/changes/unreleased/1789-background-updates.md index 393ec97477..5d29ff7573 100644 --- a/docs/changes/unreleased/1789-background-updates.md +++ b/docs/changes/unreleased/1789-background-updates.md @@ -6,10 +6,11 @@ surface: [chat] invalidates: - "`/update` used to refuse while a turn or task was running and then quit the program so the door could restart on the new build. It now installs in the background on the same road the automatic updater uses: nothing quits, nothing restarts, and a running turn or task is untouched. The new build is what opens the next time codeaf is started." - "The launch notice used to be a single dim line whose only action was `/update`. With `update.auto` on (the default) a newer release now raises an offer in the keys line, counted down live from ten seconds, and then installs itself in the background; `alt+n` or `/update skip` skips that exact release, and `/update never` or the settings row turns the automatic road off." - - "A version behind its channel with `update.auto` off still gets one quiet line naming the release and the hand-run road, instead of no check at all." + - "A version behind its channel with `update.auto` off still gets one quiet line naming the release and the hand-run road, instead of no check at all — and the launch request really is made, because the standing row no longer suppresses it; only `CODEAF_NO_UPDATE_CHECK=1` does that." - "A codeaf owned by Homebrew, Nix or a system package manager, or one in a folder the account cannot write, is refused in place by both the chat and `codeaf update`, and the refusal names the manager and the curl road." - "The manuals `internal/manual/chat/staying-on-that-machine.md` and `running-on-another-machine.md` said a busy engine from an older build was replaced the moment a newer codeaf opened. They now say the truth the engine lane implements: an older engine holding work is joined and keeps it, and steps aside once it is quiet (or at the next safe launch for a host too old to know the ask); one holding nothing hands over at once; an attached idle window counts as holding work; and another wire, or one too old to be asked, is refused with `codeaf engine --stop --workspace ''` named." - - "One install now runs at a time per executable, across terminals and profiles: the lock is an OS advisory lock keyed on the executable AND IS TAKEN INSIDE the shared installer, so the chat surface and `codeaf update` are one acquirer. Under it a sha256-tied record says what is on disk: a duplicate is a quiet no-op and a stale automatic downgrade is refused, while an explicitly named tag may still roll back." + - "One install now runs at a time per executable, across terminals and profiles: the lock is an OS advisory lock keyed on the executable AND IS TAKEN INSIDE the shared installer, so the chat surface and `codeaf update` are one acquirer. The target is canonicalized first — absolute, cleaned, symlinks resolved — so a relative path or a symlinked folder is the same target and serializes against it, and the ownership refusal answers about the file the replacement would land on. Under the lock a sha256-tied record says what is on disk: a duplicate is a quiet no-op and a stale automatic downgrade is refused, while an explicitly named tag may still roll back." + - "Only a tag a person named may install an older build. The bare `/update` that answers an open offer installs the release the offer is about and no longer carries rollback intent, and a named tag reaches the installer even when it matches the running process's own stamp, because the stamp is not the file on disk." --- The offer waits in the line that already carries the idle keys, so it never @@ -17,16 +18,29 @@ takes the message box, never blocks a keystroke and never binds a plain letter. While its grace is open it stays visible even over a running turn's own keys by appending a defer clause; when it expires the download begins in the background. The sentence names the release, says it installs shortly, and carries the skip -chord and `/update` — both survive an eighty-column window, because the -automatic clause is dropped before either action is. +chord and `/update` — both survive an eighty-column window, and over a running +turn at a narrow width the TURN's clauses are shortened first so the countdown +never loses the one control it has. + +The clock also WAITS wherever the offer cannot be read or answered — a question +page, `/settings` and the other places, the first-run sheet, a team's card, the +provider panel, the settings panel's own boxes — instead of spending the ten +seconds invisibly, and it ends early when the row is turned off or the release is +skipped in another window. The row is re-read from the profile on every beat, at +the deadline, and again before a resolved release is downloaded, so a stale +decision never installs over a newer one; `/update` typed by hand is the person's +own answer and is not re-checked. The completion wording is exact and quiet: `codeaf installed · this -session keeps running`, with the durable note adding that new windows use the -update while existing engines finish their work first and an attached window -keeps its engine current. It does not promise that a busy engine swaps. A +session keeps running`, with the durable note adding that new windows open the +updated app while work already running keeps its current engine until all its +windows and work close. It does not promise that a busy engine swaps. A same-release candidate is a quiet successful no-op rather than an alarming failure, and only a NAMED tag (`/update `, `update --version `) may -deliberately roll back; an automatic or channel update never does. +deliberately roll back; an automatic or channel update never does. Skipping is +per release — that exact tag is never offered again and a later one is a new +question — and a skip the profile could not save says so rather than claiming a +durable answer it did not write. `update.auto`, `auto update` in `/settings`, is on by default. The dismissal and the failure count live in one small file beside `config.json`, keyed by release diff --git a/docs/design/background-updates-frames/downloading-120c.ansi b/docs/design/background-updates-frames/downloading-120c.ansi new file mode 100644 index 0000000000..2dca83f1ff --- /dev/null +++ b/docs/design/background-updates-frames/downloading-120c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/downloading/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/da0b5f30d42af71c/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/downloading/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/da0b5f30d42af71c/transcript.jsonl\project/da0b5f30d42af71c/transcript.jsonl]8;;\ │  + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · updating codeaf in the background · this session keeps running │  + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=downloading · no network, │  +  nothing installed │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────────────────────────────────────────────  $0.00 idle ─ + ›  + + + Updating codeaf in the background · keep working project: codeaf diff --git a/docs/design/background-updates-frames/downloading-120c.txt b/docs/design/background-updates-frames/downloading-120c.txt new file mode 100644 index 0000000000..2abac723a0 --- /dev/null +++ b/docs/design/background-updates-frames/downloading-120c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task + project/da0b5f30d42af71c/transcript.jsonl │ + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · updating codeaf in the background · this session keeps running │ + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=downloading · no network, │ + nothing installed │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────────────────────────────────────────────── $0.00 idle ─ + › + + + Updating codeaf in the background · keep working project: codeaf diff --git a/docs/design/background-updates-frames/downloading-80c.ansi b/docs/design/background-updates-frames/downloading-80c.ansi new file mode 100644 index 0000000000..80ccf4d713 --- /dev/null +++ b/docs/design/background-updates-frames/downloading-80c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────── + + + · updating codeaf in the background · this session keeps running + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=downloading · no +  network, nothing installed + + + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────  $0.00 idle ─ + ›  + + + Updating codeaf in the background · keep working project: codeaf diff --git a/docs/design/background-updates-frames/downloading-80c.txt b/docs/design/background-updates-frames/downloading-80c.txt new file mode 100644 index 0000000000..224bd90dfe --- /dev/null +++ b/docs/design/background-updates-frames/downloading-80c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────── + + + · updating codeaf in the background · this session keeps running + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=downloading · no + network, nothing installed + + + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────── $0.00 idle ─ + › + + + Updating codeaf in the background · keep working project: codeaf diff --git a/docs/design/background-updates-frames/failure-120c.ansi b/docs/design/background-updates-frames/failure-120c.ansi new file mode 100644 index 0000000000..23054a41c8 --- /dev/null +++ b/docs/design/background-updates-frames/failure-120c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/failure/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/29cd71f28969ce31/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/failure/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/29cd71f28969ce31/transcript.jsonl\project/29cd71f28969ce31/transcript.jsonl]8;;\ │  + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · codeaf v0.9.3 could not be installed · this version keeps running · the checksum did│  +  not match · /update retries │  + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=failure · no network, nothing │  +  installed │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────────────────────────────────────────────  $0.00 idle ─ + ›  + + + codeaf v0.9.3 could not be installed · /update retries project: codeaf diff --git a/docs/design/background-updates-frames/failure-120c.txt b/docs/design/background-updates-frames/failure-120c.txt new file mode 100644 index 0000000000..7c11584e73 --- /dev/null +++ b/docs/design/background-updates-frames/failure-120c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task + project/29cd71f28969ce31/transcript.jsonl │ + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · codeaf v0.9.3 could not be installed · this version keeps running · the checksum did│ + not match · /update retries │ + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=failure · no network, nothing │ + installed │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────────────────────────────────────────────── $0.00 idle ─ + › + + + codeaf v0.9.3 could not be installed · /update retries project: codeaf diff --git a/docs/design/background-updates-frames/failure-80c.ansi b/docs/design/background-updates-frames/failure-80c.ansi new file mode 100644 index 0000000000..dc3b647fe3 --- /dev/null +++ b/docs/design/background-updates-frames/failure-80c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────── + + + · codeaf v0.9.3 could not be installed · this version keeps running · the +  checksum did not match · /update retries + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=failure · no network, +  nothing installed + + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────  $0.00 idle ─ + ›  + + + codeaf v0.9.3 could not be installed · /update retries project: codeaf diff --git a/docs/design/background-updates-frames/failure-80c.txt b/docs/design/background-updates-frames/failure-80c.txt new file mode 100644 index 0000000000..25015284a1 --- /dev/null +++ b/docs/design/background-updates-frames/failure-80c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────── + + + · codeaf v0.9.3 could not be installed · this version keeps running · the + checksum did not match · /update retries + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=failure · no network, + nothing installed + + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────── $0.00 idle ─ + › + + + codeaf v0.9.3 could not be installed · /update retries project: codeaf diff --git a/docs/design/background-updates-frames/manual-120c.ansi b/docs/design/background-updates-frames/manual-120c.ansi new file mode 100644 index 0000000000..c14be08f86 --- /dev/null +++ b/docs/design/background-updates-frames/manual-120c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/manual/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/8fb8092a9bb3f78b/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/manual/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/8fb8092a9bb3f78b/transcript.jsonl\project/8fb8092a9bb3f78b/transcript.jsonl]8;;\ │  + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · updating codeaf in the background · this session keeps running │  + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=manual · no network, nothing │  +  installed │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────────────────────────────────────────────  $0.00 idle ─ + ›  + + + Updating codeaf in the background · keep working project: codeaf diff --git a/docs/design/background-updates-frames/manual-120c.txt b/docs/design/background-updates-frames/manual-120c.txt new file mode 100644 index 0000000000..9bd4ee3178 --- /dev/null +++ b/docs/design/background-updates-frames/manual-120c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task + project/8fb8092a9bb3f78b/transcript.jsonl │ + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · updating codeaf in the background · this session keeps running │ + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=manual · no network, nothing │ + installed │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────────────────────────────────────────────── $0.00 idle ─ + › + + + Updating codeaf in the background · keep working project: codeaf diff --git a/docs/design/background-updates-frames/manual-80c.ansi b/docs/design/background-updates-frames/manual-80c.ansi new file mode 100644 index 0000000000..334d6e08cf --- /dev/null +++ b/docs/design/background-updates-frames/manual-80c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────── + + + · updating codeaf in the background · this session keeps running + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=manual · no network, +  nothing installed + + + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────  $0.00 idle ─ + ›  + + + Updating codeaf in the background · keep working project: codeaf diff --git a/docs/design/background-updates-frames/manual-80c.txt b/docs/design/background-updates-frames/manual-80c.txt new file mode 100644 index 0000000000..53a5282757 --- /dev/null +++ b/docs/design/background-updates-frames/manual-80c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────── + + + · updating codeaf in the background · this session keeps running + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=manual · no network, + nothing installed + + + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────── $0.00 idle ─ + › + + + Updating codeaf in the background · keep working project: codeaf diff --git a/docs/design/background-updates-frames/off-120c.ansi b/docs/design/background-updates-frames/off-120c.ansi new file mode 100644 index 0000000000..0b5c9c49f2 --- /dev/null +++ b/docs/design/background-updates-frames/off-120c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/off/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/2092e8222d70aa5b/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/off/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/2092e8222d70aa5b/transcript.jsonl\project/2092e8222d70aa5b/transcript.jsonl]8;;\ │  + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · codeaf v0.9.3 is out · you have v0.9.2 · /update installs it for the next launch │  + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=off · no network, nothing │  +  installed │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────────────────────────────────────────────  $0.00 idle ─ + ›  + + + alt+e effort · alt+a approvals · / commands · space space home project: codeaf diff --git a/docs/design/background-updates-frames/off-120c.txt b/docs/design/background-updates-frames/off-120c.txt new file mode 100644 index 0000000000..b6eaa2b2a0 --- /dev/null +++ b/docs/design/background-updates-frames/off-120c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task + project/2092e8222d70aa5b/transcript.jsonl │ + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · codeaf v0.9.3 is out · you have v0.9.2 · /update installs it for the next launch │ + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=off · no network, nothing │ + installed │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────────────────────────────────────────────── $0.00 idle ─ + › + + + alt+e effort · alt+a approvals · / commands · space space home project: codeaf diff --git a/docs/design/background-updates-frames/off-80c.ansi b/docs/design/background-updates-frames/off-80c.ansi new file mode 100644 index 0000000000..0e0b880440 --- /dev/null +++ b/docs/design/background-updates-frames/off-80c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────── + + + · codeaf v0.9.3 is out · you have v0.9.2 · /update installs it for the next +  launch + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=off · no network, +  nothing installed + + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────  $0.00 idle ─ + ›  + + + alt+e effort · alt+a approvals · / commands · space space home project: cod… diff --git a/docs/design/background-updates-frames/off-80c.txt b/docs/design/background-updates-frames/off-80c.txt new file mode 100644 index 0000000000..764a753a92 --- /dev/null +++ b/docs/design/background-updates-frames/off-80c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────── + + + · codeaf v0.9.3 is out · you have v0.9.2 · /update installs it for the next + launch + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=off · no network, + nothing installed + + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────── $0.00 idle ─ + › + + + alt+e effort · alt+a approvals · / commands · space space home project: cod… diff --git a/docs/design/background-updates-frames/offer-120c.ansi b/docs/design/background-updates-frames/offer-120c.ansi new file mode 100644 index 0000000000..3ccbf12780 --- /dev/null +++ b/docs/design/background-updates-frames/offer-120c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/offer/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/fc39dcd5baf27c1b/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/offer/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/fc39dcd5baf27c1b/transcript.jsonl\project/fc39dcd5baf27c1b/transcript.jsonl]8;;\ │  + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · codeaf v0.9.3 is out · you have v0.9.2 · it installs on its own shortly · alt+n │  +  skips · /update installs it │  + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=offer · no network, nothing │  +  installed │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────────────────────────────────────────────  $0.00 idle ─ + ›  + + + codeaf v0.9.3 is out · installs in 10s · alt+n skip · /update project: codeaf diff --git a/docs/design/background-updates-frames/offer-120c.txt b/docs/design/background-updates-frames/offer-120c.txt new file mode 100644 index 0000000000..787caacc41 --- /dev/null +++ b/docs/design/background-updates-frames/offer-120c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task + project/fc39dcd5baf27c1b/transcript.jsonl │ + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · codeaf v0.9.3 is out · you have v0.9.2 · it installs on its own shortly · alt+n │ + skips · /update installs it │ + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=offer · no network, nothing │ + installed │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────────────────────────────────────────────── $0.00 idle ─ + › + + + codeaf v0.9.3 is out · installs in 10s · alt+n skip · /update project: codeaf diff --git a/docs/design/background-updates-frames/offer-80c.ansi b/docs/design/background-updates-frames/offer-80c.ansi new file mode 100644 index 0000000000..12b2ab6ebf --- /dev/null +++ b/docs/design/background-updates-frames/offer-80c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────── + + + · codeaf v0.9.3 is out · you have v0.9.2 · it installs on its own shortly · +  alt+n skips · /update installs it + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=offer · no network, +  nothing installed + + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────  $0.00 idle ─ + ›  + + + codeaf v0.9.3 is out · installs in 10s · alt+n skip · /update project: code… diff --git a/docs/design/background-updates-frames/offer-80c.txt b/docs/design/background-updates-frames/offer-80c.txt new file mode 100644 index 0000000000..6aac8e7f94 --- /dev/null +++ b/docs/design/background-updates-frames/offer-80c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────── + + + · codeaf v0.9.3 is out · you have v0.9.2 · it installs on its own shortly · + alt+n skips · /update installs it + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=offer · no network, + nothing installed + + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────── $0.00 idle ─ + › + + + codeaf v0.9.3 is out · installs in 10s · alt+n skip · /update project: code… diff --git a/docs/design/background-updates-frames/ready-120c.ansi b/docs/design/background-updates-frames/ready-120c.ansi new file mode 100644 index 0000000000..cda0b4def2 --- /dev/null +++ b/docs/design/background-updates-frames/ready-120c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/ready/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/3aff42a745e45c6d/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/ready/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/3aff42a745e45c6d/transcript.jsonl\project/3aff42a745e45c6d/transcript.jsonl]8;;\ │  + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · codeaf v0.9.3 installed · this session keeps running · new windows open the updated │  +  app; work already running keeps its current engine until all its windows and work │  +  close │  + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=ready · no network, nothing │  +  installed │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────────────────────────────────────────────  $0.00 idle ─ + ›  + + + codeaf v0.9.3 installed · this session keeps running project: codeaf diff --git a/docs/design/background-updates-frames/ready-120c.txt b/docs/design/background-updates-frames/ready-120c.txt new file mode 100644 index 0000000000..d6f835b2db --- /dev/null +++ b/docs/design/background-updates-frames/ready-120c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task + project/3aff42a745e45c6d/transcript.jsonl │ + · esc interrupts · ctrl+c quits · ? for help │ + /standing + · codeaf v0.9.3 installed · this session keeps running · new windows open the updated │ + app; work already running keeps its current engine until all its windows and work │ + close │ + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=ready · no network, nothing │ + installed │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────────────────────────────────────────────── $0.00 idle ─ + › + + + codeaf v0.9.3 installed · this session keeps running project: codeaf diff --git a/docs/design/background-updates-frames/ready-80c.ansi b/docs/design/background-updates-frames/ready-80c.ansi new file mode 100644 index 0000000000..fd760712a7 --- /dev/null +++ b/docs/design/background-updates-frames/ready-80c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +  +  +──────────────────────────────────────────────────────────────────────────────── + + + · codeaf v0.9.3 installed · this session keeps running · new windows open +  the updated app; work already running keeps its current engine until all +  its windows and work close + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=ready · no network, +  nothing installed + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ─────────────────  $0.00 idle ─ + ›  + + + codeaf v0.9.3 installed · this session keeps running project: codeaf diff --git a/docs/design/background-updates-frames/ready-80c.txt b/docs/design/background-updates-frames/ready-80c.txt new file mode 100644 index 0000000000..b8df0e34ba --- /dev/null +++ b/docs/design/background-updates-frames/ready-80c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + + +──────────────────────────────────────────────────────────────────────────────── + + + · codeaf v0.9.3 installed · this session keeps running · new windows open + the updated app; work already running keeps its current engine until all + its windows and work close + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=ready · no network, + nothing installed + + + + + + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ───────────────── $0.00 idle ─ + › + + + codeaf v0.9.3 installed · this session keeps running project: codeaf diff --git a/docs/design/background-updates-frames/ready-working-120c.ansi b/docs/design/background-updates-frames/ready-working-120c.ansi new file mode 100644 index 0000000000..f3545e48c3 --- /dev/null +++ b/docs/design/background-updates-frames/ready-working-120c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +   ◐ tighten the retry loop in the sync client ×  + ▦ All  +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/ready-working/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/8c7285c6057fd684/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/ready-working/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/8c7285c6057fd684/transcript.jsonl\project/8c7285c6057fd684/transcript.jsonl]8;;\ │  + · esc interrupts · ctrl+c quits · ? for help │ + /standing + │  + › tighten the retry loop in the sync client │  + │  +  running 1 command │  +  Reading the retry loop now │  + │  + · codeaf v0.9.3 installed · this session keeps running · new windows open the updated │  +  app; work already running keeps its current engine until all its windows and work │  +  close │  + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=ready-working · no network, │  +  nothing installed │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + │  + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ────────────────────────────────────────────────────  $0.00 ⠋ working ─ + › and keep the backoff capped + + + enter steers it in · ctrl+g backgrounds · esc interrupt project: codeaf diff --git a/docs/design/background-updates-frames/ready-working-120c.txt b/docs/design/background-updates-frames/ready-working-120c.txt new file mode 100644 index 0000000000..90920655e2 --- /dev/null +++ b/docs/design/background-updates-frames/ready-working-120c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + ◐ tighten the retry loop in the sync client × + ▦ All +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + │ Tasks 0 alt+l + · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task + project/8c7285c6057fd684/transcript.jsonl │ + · esc interrupts · ctrl+c quits · ? for help │ + /standing + │ + › tighten the retry loop in the sync client │ + │ +  running 1 command │ +  Reading the retry loop now │ + │ + · codeaf v0.9.3 installed · this session keeps running · new windows open the updated │ + app; work already running keeps its current engine until all its windows and work │ + close │ + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=ready-working · no network, │ + nothing installed │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ──────────────────────────────────────────────────── $0.00 ⠋ working ─ + › and keep the backoff capped + + + enter steers it in · ctrl+g backgrounds · esc interrupt project: codeaf diff --git a/docs/design/background-updates-frames/ready-working-80c.ansi b/docs/design/background-updates-frames/ready-working-80c.ansi new file mode 100644 index 0000000000..0774621d3e --- /dev/null +++ b/docs/design/background-updates-frames/ready-working-80c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + +   ◐ tighten the retry loop in the sync client ×  + ▦ All  +──────────────────────────────────────────────────────────────────────────────── + + + › tighten the retry loop in the sync client + +  running 1 command +  Reading the retry loop now + + · codeaf v0.9.3 installed · this session keeps running · new windows open +  the updated app; work already running keeps its current engine until all +  its windows and work close + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=ready-working · no +  network, nothing installed + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ────────────  $0.00 ⠋ working ─ + › and keep the backoff capped + + + enter steers it in · ctrl+g backgrounds · esc interrupt project: codeaf diff --git a/docs/design/background-updates-frames/ready-working-80c.txt b/docs/design/background-updates-frames/ready-working-80c.txt new file mode 100644 index 0000000000..cb3ea3f6d2 --- /dev/null +++ b/docs/design/background-updates-frames/ready-working-80c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + + ◐ tighten the retry loop in the sync client × + ▦ All +──────────────────────────────────────────────────────────────────────────────── + + + › tighten the retry loop in the sync client + +  running 1 command +  Reading the retry loop now + + · codeaf v0.9.3 installed · this session keeps running · new windows open + the updated app; work already running keeps its current engine until all + its windows and work close + · fixture · codeaf update states · CODEAF_UPDATE_DEMO=ready-working · no + network, nothing installed + + + + + + + + + + + + + + + + + + + +─ deepseek/deepseek-v4.1-flash:auto · ◇ YOLO ──────────── $0.00 ⠋ working ─ + › and keep the backoff capped + + + enter steers it in · ctrl+g backgrounds · esc interrupt project: codeaf diff --git a/docs/design/background-updates-frames/settings-auto-off-120c.ansi b/docs/design/background-updates-frames/settings-auto-off-120c.ansi new file mode 100644 index 0000000000..da1624cc8c --- /dev/null +++ b/docs/design/background-updates-frames/settings-auto-off-120c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams chats sessions spend  settings  wed 11:08am + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams   Providers   Connections  + + › type to search + mouse on + timestamps footers + turn work fold + step icons auto + task column on + quick switch on + input history on + telemetry on + keep drafts on + auto update • off + check for a new codeaf at launch and install it in the background. + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/background-updates-frames/settings-auto-off-120c.txt b/docs/design/background-updates-frames/settings-auto-off-120c.txt new file mode 100644 index 0000000000..c9970e72fc --- /dev/null +++ b/docs/design/background-updates-frames/settings-auto-off-120c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:08am + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + mouse on + timestamps footers + turn work fold + step icons auto + task column on + quick switch on + input history on + telemetry on + keep drafts on + auto update • off + check for a new codeaf at launch and install it in the background. + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/background-updates-frames/settings-auto-off-80c.ansi b/docs/design/background-updates-frames/settings-auto-off-80c.ansi new file mode 100644 index 0000000000..6d2e66f50e --- /dev/null +++ b/docs/design/background-updates-frames/settings-auto-off-80c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams chats sessions spend  settings  wed 11:08am + +──────────────────────────────────────────────────────────────────────────────── + +  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams  … + + › type to search + mouse on + timestamps footers + turn work fold + step icons auto + task column on + quick switch on + input history on + telemetry on + keep drafts on + auto update • off + check for a new codeaf at launch and install it in the background. + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/background-updates-frames/settings-auto-off-80c.txt b/docs/design/background-updates-frames/settings-auto-off-80c.txt new file mode 100644 index 0000000000..9b2ac2df2a --- /dev/null +++ b/docs/design/background-updates-frames/settings-auto-off-80c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:08am + +──────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams … + + › type to search + mouse on + timestamps footers + turn work fold + step icons auto + task column on + quick switch on + input history on + telemetry on + keep drafts on + auto update • off + check for a new codeaf at launch and install it in the background. + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/background-updates-frames/settings-auto-on-120c.ansi b/docs/design/background-updates-frames/settings-auto-on-120c.ansi new file mode 100644 index 0000000000..0714d9db71 --- /dev/null +++ b/docs/design/background-updates-frames/settings-auto-on-120c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams chats sessions spend  settings  wed 11:08am + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams   Providers   Connections  + + › type to search + mouse on + timestamps footers + turn work fold + step icons auto + task column on + quick switch on + input history on + telemetry on + keep drafts on + auto update on + check for a new codeaf at launch and install it in the background. + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/background-updates-frames/settings-auto-on-120c.txt b/docs/design/background-updates-frames/settings-auto-on-120c.txt new file mode 100644 index 0000000000..ad7263273a --- /dev/null +++ b/docs/design/background-updates-frames/settings-auto-on-120c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:08am + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams Providers Connections + + › type to search + mouse on + timestamps footers + turn work fold + step icons auto + task column on + quick switch on + input history on + telemetry on + keep drafts on + auto update on + check for a new codeaf at launch and install it in the background. + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─────────────────────────────────────────── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/background-updates-frames/settings-auto-on-80c.ansi b/docs/design/background-updates-frames/settings-auto-on-80c.ansi new file mode 100644 index 0000000000..6f0f5c6333 --- /dev/null +++ b/docs/design/background-updates-frames/settings-auto-on-80c.ansi @@ -0,0 +1,40 @@ + >● codeaf  home teams chats sessions spend  settings  wed 11:07am + +──────────────────────────────────────────────────────────────────────────────── + +  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams  … + + › type to search + mouse on + timestamps footers + turn work fold + step icons auto + task column on + quick switch on + input history on + telemetry on + keep drafts on + auto update on + check for a new codeaf at launch and install it in the background. + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/background-updates-frames/settings-auto-on-80c.txt b/docs/design/background-updates-frames/settings-auto-on-80c.txt new file mode 100644 index 0000000000..00d10a18f5 --- /dev/null +++ b/docs/design/background-updates-frames/settings-auto-on-80c.txt @@ -0,0 +1,40 @@ + >● codeaf home teams chats sessions spend settings wed 11:07am + +──────────────────────────────────────────────────────────────────────────────── + + Session Context Workspace Display Spending Safety Tasks Teams … + + › type to search + mouse on + timestamps footers + turn work fold + step icons auto + task column on + quick switch on + input history on + telemetry on + keep drafts on + auto update on + check for a new codeaf at launch and install it in the background. + + + + + + + + + + + + + + + + + + + + +─ saved to your profile · a project's own .codeaf/config.json is a hand edit ─── + ↑↓ move · ←→ tabs · enter change · type to search · tab next place · esc close diff --git a/docs/design/background-updates.md b/docs/design/background-updates.md index c9912398e7..a94c44de56 100644 --- a/docs/design/background-updates.md +++ b/docs/design/background-updates.md @@ -18,20 +18,42 @@ The count is live — the line repaints once a second from the deadline — so i never show a frozen number. After [codeupdate.AutoGrace] the download begins by itself. The three answers are: -- `/update` (or `/upgrade`) — install now; an explicit channel or tag wins, and - a NAMED tag is the one deliberate rollback. -- `alt+n`, or `/update skip` — skip this release. It is written down and that - exact tag is never offered again. +- `/update` (or `/upgrade`) — install now. Typed bare while the offer is up it + installs the release the offer is about, and it is NOT a rollback: the tag is + the surface's, not the person's. An explicit channel or tag wins, and only a + NAMED tag is the deliberate rollback. +- `alt+n`, or `/update skip` — skip this release. It is written down for that + exact tag and that tag is never offered again; a newer release is a new + question. If the write itself fails, the note says so instead of claiming a + skip that was not saved — this window still stops offering it. - `/update never`, or the `auto update` row in `/settings` — stop installing - automatically. The launch check still runs and still says what is out. + automatically. It does not cancel an install already downloading and does not + say it did. The launch check still runs and still says what is out: with the + row off the one request is still made and the release is named on one dim + line. Only `CODEAF_NO_UPDATE_CHECK=1` takes the look away. The three are the ONLY commands the offer prints, and a test extracts every `/update` token from the hint and the note and checks the parser answers each as itself rather than as a tag. While the grace is open the offer stays reachable even over a running turn's own keys, because `footHint` appends a defer clause to -whatever outranks it; `hintShorter` drops that clause first, so a narrow frame -loses the offer and never the turn's keys. A question page owns the frame, so the -grace PAUSES under one instead of counting out invisibly. +whatever outranks it; `hintShorter` shortens the TURN's own clauses first and +keeps the offer's, so a narrow frame under a countdown never loses the one +control that countdown has — the offer's clause goes only when the turn is down +to its last key. + +The clock PAUSES wherever the offer cannot be read or answered: a question page, +every place but home and the conversation (whose keys row is the offer's own — +`/settings`, memory, standing, spend), the first-run sheet, a team's card, the +move picker, the switcher's menu, the wall, the provider panel, and the settings +panel's own value box and model picker. The list is `[app.key]`'s rung order +above the offer's chord, and a test pins its scope in both directions: a running +turn and a plain conversation MUST keep counting. + +The row is read LIVE. `/settings` updates the running window's flag, and a row +turned off — or a release skipped — in ANOTHER window is seen because the +coordinator re-reads the profile on every countdown beat, at the deadline, and +again after the resolver answers and before anything is downloaded. A stale +ten-second-old decision never installs over a newer one. ## What a background install does and says @@ -45,10 +67,11 @@ checksum matched · installed at (hand-run only) codeaf installed · this session keeps running ``` -The durable note adds: `new windows use the update; existing engines finish their -work first, and an attached window keeps its engine current`. That is the engine -lane's contract, stated without over-promising: a file replaced on disk does not -force a running engine to hand over, and an idle line never claims it did. +The durable note says what is true without the engine lane's vocabulary: `new +windows open the updated app; work already running keeps its current engine until +all its windows and work close`. A file replaced on disk does not force a +running engine to hand over, so the line never claims it did, and it no longer +leaves "current" to be guessed at. A finished line dwells for [updateOfferingDwell] and then folds; the transcript note is the durable record. A same-release candidate is a quiet successful no-op: @@ -70,10 +93,18 @@ offer's own opt-out writes through the same writer the settings row does. ## One installer per executable The lock is an OS advisory lock on `.install.lock`, keyed on the -resolved target and not on a profile: two profiles can share one codeaf file, and -it is the file that must not be replaced twice at once. A process that dies -releases the lock in the kernel, so nothing is ever taken over by guessing at an -age. +target and not on a profile: two profiles can share one codeaf file, and it is +the file that must not be replaced twice at once. A process that dies releases +the lock in the kernel, so nothing is ever taken over by guessing at an age. + +THE TARGET IS CANONICALIZED BEFORE THE LOCK, THE REFUSAL OR THE TEMPORARY FILE +IS NAMED: absolute, cleaned, and resolved through symlinks (the directory too, +for a target that does not exist yet). A relative path, a `..`, or a symlinked +folder is therefore the SAME target as the real one, so holding an alias +serializes against an installer using the real path — proved by a cross-process +test and by a symlinked-folder test inside `internal/update`. The ownership +screen (`InstallRefusal`) canonicalizes the same way, so a package-managed file +reached through a symlink is still refused instead of answering "ordinary". THE LOCK IS TAKEN INSIDE `codeupdate.Install` AND NOWHERE ELSE, so the chat surface and `codeaf update` are one acquirer with nothing to keep in step. Under @@ -83,7 +114,13 @@ person reads as unknown: - a candidate already on disk is a quiet successful no-op (`Already`); - a candidate older than what is on disk is refused UNLESS the person named that - exact tag, which is a deliberate rollback (`AllowDowngrade`); + exact tag, which is a deliberate rollback (`AllowDowngrade`). A channel, the + automatic road, and the bare `/update` that answers an offer all follow the + release line forward, so a file another window advanced is never stepped back; +- a NAMED tag reaches the installer even when it matches the PROCESS's own + revision stamp, because the stamp is not the file on disk — another window may + have advanced the file while this one kept working, and a person asking for + that exact release is asking for that exact file; - a target another process is mid-install on is refused with `another codeaf is already installing an update`. @@ -110,7 +147,17 @@ out a grace or paying for a download: `offer`, `downloading`, `ready`, `ready-working`, `manual`, `off`, `failure`. Every case first clears the updater seams, so a fixture cannot make a request or replace a file, and each says at the top of the transcript that it is a fixture. `scripts/update-drive.sh` captures -them at 80 and 120 columns with the real binary. +them at 80 and 120 columns with the real binary, and captures the REAL `/settings` +page beside them (`settings-auto-on`, `settings-auto-off`): those two open the +settings place through `/settings`, walk the tabs to Display and the cursor onto +the row with the panel's own arrow keys, and the capture is only kept once the +row's OWN hint is the sentence on screen — so what is drawn is the registry's +row reading the profile's value, not whatever a fuzzy search happened to put +first. The off capture's profile is the only thing that differs, and it carries +the sheet's own changed marker. Every capture runs with an empty environment, an isolated +home, a private tmux server, `--no-host`, `CODEAF_NO_UPDATE_CHECK=1` and a +loopback `CODEAF_BASE_URL`, so no capture can reach a release host or a provider, +and the drive removes only the directory it made. These are deterministic render fixtures, labelled in the frame itself, and they clear every updater seam so a capture machine needs no network and cannot install diff --git a/internal/config/derivation_test.go b/internal/config/derivation_test.go index 0c438a432a..7d57759f67 100644 --- a/internal/config/derivation_test.go +++ b/internal/config/derivation_test.go @@ -153,6 +153,11 @@ var settingReaders = map[string]string{ // The hints row names its accessor too: the surface reads it at boot and at // every turn end, beside the mouse row (internal/tui3's notice.go). KeyHints: "HintsAt", + // The auto-update row is read outside this package by the surface's + // live reader (cmd/codeaf's runSurface hands [config.UpdateAutoAt] to the + // coordinator), which is exactly the "does anything use it" proof this + // table asks for: the countdown re-reads the row through it. + KeyUpdateAuto: "UpdateAutoAt", // The two surface rows name their own KEY, because that is now what the // far side touches: they resolve through the project layer // (ProjectBoolAt), which takes the row by name and calls diff --git a/internal/config/testdata/profile-keys.ledger b/internal/config/testdata/profile-keys.ledger index 8a19b7c865..25fb63b8aa 100644 --- a/internal/config/testdata/profile-keys.ledger +++ b/internal/config/testdata/profile-keys.ledger @@ -115,6 +115,7 @@ ui.quick_switch ui.task_column ui.timestamps ui.work +update.auto vision_model work.workers working_set_tokens diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index 019f1a14b3..b2d3895d91 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -7,9 +7,12 @@ channel is told about it, and with `auto update` on (the default) the install begins on its own after a brief pause: the sentence `codeaf is out · installs in 10s · skip · /update` waits in the keys line, counted down from about ten seconds. It never blocks a keystroke. `/update` while it -is up installs immediately; the skip chord, or `/update skip`, answers that one -release and it is never offered again; `/update never` turns `auto update` -off, and so does the row in `/settings`. A release whose automatic install failed +is up installs immediately — that is the release the offer is about, not a +rollback; the skip chord, or `/update skip`, answers that one release, it is +never offered again, and a later release is a new question; `/update never` turns +`auto update` off, and so does the row in `/settings`. Turning it off does not +cancel an install already downloading, and the launch check still makes its one +request and names what is out. A release whose automatic install failed three times stops being tried on its own. A release candidate gets the notice when its stable line is published. An equal @@ -33,7 +36,8 @@ staging build selects its own channel; stable and rc select stable. An explicit channel or tag wins. An ahead dev build refuses with `this codeaf is , ahead of the newest dev — /update installs it anyway`; naming the tag is the deliberate downgrade, and it is the ONLY road that rolls a file -back. An automatic or channel update never installs an older build than the one +back — a bare `/update` you type while an offer is up installs what the offer is +about and never steps the file backwards. An automatic or channel update never installs an older build than the one on disk, and a release already on disk is a quiet no-op. If the file is owned by Homebrew, Nix or the system package manager, or is in a folder this account cannot write, neither road replaces it in place. One install runs at a time per executable: diff --git a/internal/tui3/app.go b/internal/tui3/app.go index e5379d84b6..3f1a28518d 100644 --- a/internal/tui3/app.go +++ b/internal/tui3/app.go @@ -964,7 +964,10 @@ type app struct { // one dim note it always was. updateAuto *UpdateCoordinator // offer is the launch offer's state machine (updateoffer.go). - offer updateOffer + offer updateOffer + // restart is the plan a quitting surface used to fill for the door. Nothing + // on this surface quits for an update any more, so it is read by no code + // here; it stays a field because the door hands one in (tui3.Options). restart *codeupdate.Plan // THE OPENROUTER BALANCE (credits.go). readCredits is the door's reader and // nil on every surface that cannot ask; the rest is what the loop keeps so diff --git a/internal/tui3/steer.go b/internal/tui3/steer.go index f3aff627b1..ed6238a7ac 100644 --- a/internal/tui3/steer.go +++ b/internal/tui3/steer.go @@ -490,10 +490,28 @@ func (a *app) runHint() string { // Idle controls yield from the left to commands and home; running hints keep // their existing priority from the left. Every step removes at least one clause. func (a *app) hintShorter(slot string) string { - // THE UPDATE OFFER'S DEFER CLAUSE IS DROPPED FIRST. It is appended to a - // running turn's own keys, and the generic rule below cannot shorten a line - // that extends the run hint — it would delete the whole row, taking the - // turn's keys with it. Here the offer's clause goes and the turn's keys stay. + // THE OFFER'S OWN CLAUSE SURVIVES THE TURN'S. It is appended to a running + // turn's keys, and the generic rule below cannot shorten a line that extends + // the run hint without deleting the whole row. So while the grace is open the + // line is shortened FROM THE TURN'S LEFT and the offer keeps its clause: with + // a ten-second countdown running, "not now" is the one thing a person still + // has a moment to say, and dropping it first would take away the only control + // the countdown has. + // + // WHEN THE TURN IS DOWN TO ITS LAST CLAUSE, the offer's clause is what goes, + // because a keys row cannot be shorter than the key that stops the turn — + // and the row must always shorten, or a narrow frame would spin here. + if full := a.runHint(); full != "" && strings.HasSuffix(slot, hintSegment+updateOfferDeferWords) && a.offer.offering() { + base := strings.TrimSuffix(slot, hintSegment+updateOfferDeferWords) + if base != "" && strings.HasSuffix(full, base) { + if parts := strings.Split(base, hintSegment); len(parts) > 1 { + return strings.Join(parts[1:], hintSegment) + hintSegment + updateOfferDeferWords + } + } + return base + } + // A DEFER CLAUSE LEFT OVER FROM A FINISHED OFFER FOLDS BACK to the turn's own + // keys rather than being shortened away with the line that carries them. if full := a.runHint(); full != "" && strings.HasPrefix(slot, full+hintSegment) { return full } diff --git a/internal/tui3/tui3.go b/internal/tui3/tui3.go index a326e9870e..7f774fb1df 100644 --- a/internal/tui3/tui3.go +++ b/internal/tui3/tui3.go @@ -464,8 +464,14 @@ type Options struct { ResolveUpdate func(context.Context, codeupdate.Choice) (codeupdate.Release, error) InstallUpdate func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error) // UpdateRunning is the exact revision of this process, without build-time - // decoration. UpdateArgs are its original arguments. Restart is the slot the - // surface fills before quitting and the door reads after the terminal is back. + // decoration. UpdateArgs are its original arguments. + // + // Restart is the plan a quitting surface used to fill for the door to read + // after the terminal was back. NO INSTALL FILLS IT NOW: an install replaces + // the file and the next launch opens it, so nothing on this surface quits for + // an update. The field is still handed in by the four doors because the + // engine lane reads the same plan, and it is empty on every road that does + // not restart. UpdateRunning string UpdateCurl string UpdateArgs []string diff --git a/internal/tui3/updatecmd.go b/internal/tui3/updatecmd.go index 9061ad6d06..0e35fc1ef4 100644 --- a/internal/tui3/updatecmd.go +++ b/internal/tui3/updatecmd.go @@ -21,6 +21,19 @@ type updateResolveMsg struct { // auto is true when the offer's own road raised this, in the background and // for a release the person did not have to ask for. auto bool + // named is true when a PERSON named what to install — `/update v0.9.0`, or + // a channel by hand. It is the only thing that makes replacing the file with + // an older release deliberate, and the only road that reaches the installer + // when its tag is the build already running. A tag THIS WINDOW pinned — the + // release the offer was about, used for a bare `/update` during the + // countdown — is not a person naming anything, which is why the intent is + // carried here and never derived from the choice's own tag. + named bool +} + +// namedTag is whether the person named an exact release by hand. +func (m updateResolveMsg) namedTag() bool { + return m.named && strings.TrimSpace(m.choice.Version) != "" } type updateInstallMsg struct { @@ -101,7 +114,10 @@ func (a *app) runUpdateCommand(argument string) tea.Cmd { a.note("an update is already running") return nil } - if a.resolveUpdate == nil || a.installUpdate == nil || a.restart == nil { + // NO RESTART PLAN IS ASKED FOR. An install replaces the file and the next + // launch opens it, so the ability to update here rests on the two doors + // that do the work and on nothing else. + if a.resolveUpdate == nil || a.installUpdate == nil { a.note("this window cannot update codeaf here · install a release with: " + a.updateCurlLine()) return nil } @@ -118,17 +134,21 @@ func (a *app) runUpdateCommand(argument string) tea.Cmd { return a.updateNowFromOffer() } a.offer.clear() - return a.beginUpdate(updateChoice(argument, a.updateRunning), false) + // A PERSON NAMED IT: the argument is a channel or an exact tag. That is the + // intent [updateResolveMsg.named] carries, and a bare `/update` is the + // default road that names nothing. + return a.beginUpdate(updateChoice(argument, a.updateRunning), false, argument != "") } // beginUpdate captures the resolver and leaves the loop. Every road installs in -// the background; `auto` only changes what the completion says. -func (a *app) beginUpdate(choice codeupdate.Choice, auto bool) tea.Cmd { +// the background; `auto` and `named` only change what the completion says and +// whether a rollback is deliberate. +func (a *app) beginUpdate(choice codeupdate.Choice, auto, named bool) tea.Cmd { resolve := a.resolveUpdate a.updateInFlight = true return func() tea.Msg { release, err := resolve(context.Background(), choice) - return updateResolveMsg{release: release, choice: choice, err: err, auto: auto} + return updateResolveMsg{release: release, choice: choice, err: err, auto: auto, named: named} } } @@ -150,7 +170,26 @@ func (a *app) tookUpdateResolve(message updateResolveMsg) tea.Cmd { if message.err != nil { return a.updateStopped(message.auto, message.choice.Version, message.err) } - if message.release.Tag == a.updateRunning { + // THE AUTOMATIC ROAD ASKS ONCE MORE, HERE. The resolver is off-frame, so a + // row turned off or a tag another window dismissed can land between the + // deadline and the bytes: the decision is asked again before anything is + // downloaded. A command a person typed is theirs and is not re-checked. + if message.auto { + if withdrawn := a.updateAutoWithdrawn(message.release.Tag); withdrawn != "" { + a.updateInFlight = false + a.offer.clear() + a.note(withdrawn) + return nil + } + } + // AN EXPLICIT TAG DOES NOT STOP HERE. `a.updateRunning` is the stamp of this + // PROCESS; the file on disk can be something else entirely, because another + // window installed a newer build while this one kept working. `/update ` + // is a person asking for that exact file, so it reaches the shared + // installer, which is idempotent when the disk already holds it and refuses + // a silent rollback otherwise. The automatic road and the default channel + // keep the quiet no-op, where the stamp really is the answer. + if message.release.Tag == a.updateRunning && !message.namedTag() { a.updateInFlight = false a.offer.clear() a.note("you are on the newest codeaf, " + message.release.Tag) @@ -175,15 +214,16 @@ func (a *app) tookUpdateResolve(message updateResolveMsg) tea.Cmd { // The lock, the on-disk re-read and the duplicate/downgrade refusal all live // inside [codeupdate.Install], so the chat surface has no second acquirer to // keep in step with the command line. - a.offer.downloading(message.auto) + a.offer.downloading() a.offer.tag = strings.TrimSpace(message.release.Tag) a.note(a.updateOfferNote()) install := a.installUpdate release := message.release - // ONLY A NAMED TAG IS A DELIBERATE ROLLBACK. A channel or a bare /update - // follows the release line forward, so a file another window advanced is - // never stepped back by it. - allowDowngrade := !message.auto && strings.TrimSpace(message.choice.Version) != "" + // ONLY A PERSON NAMING AN EXACT TAG IS A DELIBERATE ROLLBACK. A channel, a + // bare `/update`, the automatic road and the bare `/update` that answers an + // open offer all follow the release line forward, so a file another window + // advanced is never stepped back by them. + allowDowngrade := message.namedTag() return func() tea.Msg { result, err := install(context.Background(), codeupdate.InstallOptions{ Release: release, AllowDowngrade: allowDowngrade, diff --git a/internal/tui3/updatedemo.go b/internal/tui3/updatedemo.go index df9f04982f..b8104b9e6a 100644 --- a/internal/tui3/updatedemo.go +++ b/internal/tui3/updatedemo.go @@ -1,6 +1,7 @@ package tui3 import ( + "errors" "strings" "github.com/Agent-Field/codeaf/internal/session" @@ -47,7 +48,7 @@ func (a *app) openDemoUpdate(env func(string) string) { a.offer.raise(released, running, a.now()) a.note(a.updateOfferNote()) case "downloading": - a.offer.downloading(false) + a.offer.downloading() a.offer.tag = released a.note(a.updateOfferNote()) case "ready": @@ -69,16 +70,18 @@ func (a *app) openDemoUpdate(env func(string) string) { a.offer.tag = released a.note(a.updateOfferNote()) case "manual": - a.offer.downloading(true) + a.offer.downloading() a.offer.tag = released a.note(a.updateOfferNote()) case "off": a.note(quietUpdateNotice(codeupdate.Available{Latest: released, Running: running})) case "failure": - a.offer.failed(a.now()) - a.offer.tag = released - a.note("codeaf " + released + " could not be installed · the checksum did not match") - a.note("/update tries again when you ask · or install a release with: " + updateFallbackCurl) + // THE PRODUCTION COMPLETION, DRAWN BY THE PRODUCTION HANDLER. A + // synthetic error goes through [app.updateStopped] on the automatic + // road, so the frame shows the sentence a real failed background + // install shows rather than a copy of it kept here: the fixture cannot + // promise more than the surface says. + a.updateStopped(true, released, errors.New("the checksum did not match")) } // EVERY CAPTURE SAYS WHAT IT IS, written LAST so it is the line still on // screen whichever state the fixture drew. A frame taken from a fixture can diff --git a/internal/tui3/updateoffer.go b/internal/tui3/updateoffer.go index a248845612..e5c5ff8f8f 100644 --- a/internal/tui3/updateoffer.go +++ b/internal/tui3/updateoffer.go @@ -25,9 +25,15 @@ import ( // [updateOffer] is a pure state machine; updatedemo.go raises it deterministically // for captures. type UpdateCoordinator struct { - // Enabled is the live value of the `update.auto` row. OFF means nothing is - // downloaded on its own; the launch check still says what is out. + // Enabled is the value of the `update.auto` row read when this window wired + // its coordinator. OFF means nothing is downloaded on its own; the launch + // check still says what is out. Enabled bool + // EnabledLive re-reads the row from the profile, and it is the answer the + // AUTOMATIC road acts on: a row changed in /settings updates this window's + // Enabled, and a row changed in ANOTHER window is only visible here. Nil + // falls back to Enabled. + EnabledLive func() bool // State reads the remembered dismissal, failure count and backoff. State func() codeupdate.AutoState // Dismiss records "not now" for one exact release. @@ -75,13 +81,49 @@ type updateOffer struct { deadline time.Time // settleAt is when the ready or failed line folds away. settleAt time.Time - // manual is true when a person typed the road rather than letting the offer - // run. It changes the words, not the behaviour. - manual bool } func (o updateOffer) active() bool { return o.phase != updateIdle } +// updateAutoEnabledNow is the LIVE answer to whether the automatic road may +// still spend, read from the profile where production supplied a reader and +// from the captured flag otherwise. +func (a *app) updateAutoEnabledNow() bool { + if a.updateAuto == nil { + return false + } + if a.updateAuto.EnabledLive != nil { + return a.updateAuto.EnabledLive() + } + return a.updateAuto.Enabled +} + +// updateAutoMemory is the remembered answer — the dismissal, the failure +// count and its backoff — read fresh, because another window writes those too. +func (a *app) updateAutoMemory() codeupdate.AutoState { + if a.updateAuto == nil || a.updateAuto.State == nil { + return codeupdate.AutoState{} + } + return a.updateAuto.State() +} + +// updateAutoWithdrawn is the ONE question the automatic road asks before it +// spends anything, and the sentence to say when the answer is no. It is asked +// at the deadline, on every countdown beat, and AGAIN after the resolver +// answers: a person turning the row off or another window dismissing the tag +// can land in any of those gaps, and the decision is not this window's to keep +// once it is stale. +func (a *app) updateAutoWithdrawn(tag string) string { + tag = strings.TrimSpace(tag) + if !a.updateAutoEnabledNow() { + return "auto update is off · nothing was installed · /update installs codeaf " + tag + " when you want it" + } + if !codeupdate.ShouldOffer(a.updateAutoMemory(), tag, a.now()) { + return "codeaf " + tag + " will not be installed on its own · /update installs it when you want it" + } + return "" +} + // raise opens the grace window for a release the launch check found. func (o *updateOffer) raise(tag, running string, now time.Time) { o.phase = updateOffering @@ -89,7 +131,6 @@ func (o *updateOffer) raise(tag, running string, now time.Time) { o.running = strings.TrimSpace(running) o.deadline = now.Add(codeupdate.AutoGrace) o.settleAt = time.Time{} - o.manual = false } func (o *updateOffer) expire() { @@ -98,23 +139,24 @@ func (o *updateOffer) expire() { } } -func (o *updateOffer) downloading(manual bool) { +// downloading marks the phase: the file is being fetched. WHICH ROAD ASKED is +// not part of this state — the completion's words come from the message that +// carried the install, and a second flag here would be a spelling of `auto` +// that could only drift from it. +func (o *updateOffer) downloading() { o.phase = updateDownloading o.settleAt = time.Time{} - o.manual = manual } // ready marks an install that landed, and starts the dwell that folds its line. func (o *updateOffer) ready(now time.Time) { o.phase = updateReady - o.manual = false o.settleAt = now.Add(updateOfferingDwell) } // failed marks an install that did not land, with the same dwell. func (o *updateOffer) failed(now time.Time) { o.phase = updateFailed - o.manual = false o.settleAt = now.Add(updateOfferingDwell) } @@ -185,13 +227,17 @@ func spellGrace(left time.Duration) string { return itoa(seconds) + "s" } +// updateOfferDeferWords is the defer clause's own words, spelled once so the +// shortener recognises exactly what this appended (steer.go's [app.hintShorter]). +const updateOfferDeferWords = "/update skip" + // updateOfferDefer is the clause appended to a running turn's own keys while // the grace is open, so the chance to defer survives work in the conversation. func (a *app) updateOfferDefer() string { if !a.offer.offering() { return "" } - return " · /update skip" + return hintSegment + updateOfferDeferWords } // withUpdateOffer folds the defer clause into whatever the keys line was going @@ -215,7 +261,7 @@ func (a *app) updateOfferNote() string { case updateDownloading: return "updating codeaf in the background · this session keeps running" case updateReady: - return "codeaf " + o.tag + " installed · this session keeps running · new windows use the update; existing engines finish their work first, and an attached window keeps its engine current" + return "codeaf " + o.tag + " installed · this session keeps running · new windows open the updated app; work already running keeps its current engine until all its windows and work close" case updateFailed: return "codeaf " + o.tag + " could not be installed · this version keeps running · /update retries" } @@ -249,6 +295,11 @@ func (a *app) updateOfferKey(msg tea.KeyPressMsg) (tea.Cmd, bool) { // updateNowFromOffer is `/update` while the grace is open: install now rather // than waiting the countdown out. +// +// IT IS THE MANUAL ROAD. A person typed it, so it is not re-checked against a +// row another window may have just turned off, and its completion carries the +// same checksum line every hand-run install does — the offer simply answered +// early. func (a *app) updateNowFromOffer() tea.Cmd { if !a.offer.offering() { return nil @@ -256,11 +307,15 @@ func (a *app) updateNowFromOffer() tea.Cmd { tag := a.offer.tag a.offer.clear() a.note("installing codeaf " + tag + " now") - return a.beginUpdate(updateChoice(tag, a.updateRunning), true) + // THE TAG IS OURS, NOT THEIRS: the person typed bare `/update`, so this + // install may not roll the file back to the candidate this window pinned. + return a.beginUpdate(updateChoice(tag, a.updateRunning), false, false) } // skipUpdateOffer answers this release "not now", and only while it is being -// offered. The answer is written down, so the tag is never offered again. +// offered. The answer is written down for that EXACT tag, so this release is +// never offered again — and a note that claimed more than the write proved +// would be the surface lying about a file. func (a *app) skipUpdateOffer() { if !a.offer.offering() { return @@ -268,14 +323,25 @@ func (a *app) skipUpdateOffer() { tag := a.offer.tag a.offer.clear() if a.updateAuto != nil && a.updateAuto.Dismiss != nil && tag != "" { - _ = a.updateAuto.Dismiss(tag) + if err := a.updateAuto.Dismiss(tag); err != nil { + // THIS WINDOW KEEPS ITS ANSWER either way: the offer and its + // countdown are gone. Only the note changes, because only the + // durable half is in doubt. + a.note("skipped codeaf " + tag + " for this window · could not save the skip: " + err.Error() + " · it may be offered again at the next launch") + return + } } - a.note("codeaf " + tag + " will not be offered again · this release only · /update installs it for the next launch whenever you want") + a.note("skipped codeaf " + tag + " · newer releases will still be offered · /update installs it anytime") } -// declineUpdateOffer turns the automatic updater off for good, reached from the -// settings row and from `/update never`. +// declineUpdateOffer turns the automatic updater off for good, reached from +// `/update never`. +// +// IT DOES NOT STOP AN INSTALL ALREADY RUNNING, and it does not say it did: the +// road is about the next release, so a download in flight finishes and is +// reported by the completion that owns its line. func (a *app) declineUpdateOffer() { + finishing := a.updateInFlight a.offer.clear() if a.updateAuto != nil && a.updateAuto.Disable != nil { if err := a.updateAuto.Disable(); err != nil { @@ -284,6 +350,10 @@ func (a *app) declineUpdateOffer() { } a.updateAuto.Enabled = false } + if finishing { + a.note("auto update is off · the install already running finishes · /settings turns it back on · /update still installs a release when you ask") + return + } a.note("auto update is off · /settings turns it back on · /update still installs a release for the next launch when you ask") } @@ -320,17 +390,53 @@ func (a *app) offerTick() tea.Cmd { return surfaceTick(wait, func(time.Time) tea.Msg { return updateOfferTickMsg{} }) } -// tookOfferTick repaints one second of the countdown, and pauses the grace -// while a question page owns the frame: the offer cannot be seen there, so the -// clock does not run out invisibly under it. +// updateOfferHidden reports whether something other than the keys row owns the +// keyboard right now, so the countdown cannot be read and its chord cannot be +// answered. The clock WAITS there rather than spending the grace invisibly. +// +// THE LIST IS [app.key]'s RUNG ORDER AND NOT A GUESS. Every layer that takes the +// whole keyboard above [app.updateOfferKey] — a team's card, the move picker, +// the switcher's menu, the map's fold menu, the wall, the first-run sheet, a +// rail plan still typing itself — a question page, every place but home and the +// conversation (whose foot is the offer's own: /settings, memory, standing, +// spend, a team's page, the chats switcher), and the two dialogs inside a place +// that own every key: the settings sheet's value box and model picker, and the +// provider panel's key box. +// +// A RUNNING TURN IS NOT ON THIS LIST. There the offer's defer clause is drawn +// under the turn's own keys and its chord still works, which is exactly where +// the countdown must keep running (steer.go's [app.hintShorter] keeps that +// clause while it does). +func (a *app) updateOfferHidden() bool { + if a.questionRoomOpen() || a.setup.open || a.railPlanPending.id != "" { + return true + } + if a.teamMenu.on || a.navMore.on || a.tsheet.on || a.tmove.on || a.wall.on { + return true + } + if pl := a.showing(); pl != nil && pl.id() != pageHome { + return true + } + return a.sheetLayerOwnsKeys() || a.addPanel.open +} + +// tookOfferTick repaints one second of the countdown. It pauses where the offer +// cannot be read, and it ENDS the grace the moment the automatic road has been +// answered elsewhere, rather than counting ten seconds out for an answer that +// is no longer wanted. func (a *app) tookOfferTick() tea.Cmd { if !a.offer.offering() { return nil } - if a.questionRoomOpen() { + if a.updateOfferHidden() { a.offer.deadline = a.now().Add(codeupdate.AutoGrace) return a.offerTick() } + if withdrawn := a.updateAutoWithdrawn(a.offer.tag); withdrawn != "" { + a.offer.clear() + a.note(withdrawn) + return nil + } if !a.now().Before(a.offer.deadline) { return a.tookUpdateGrace() } @@ -338,28 +444,21 @@ func (a *app) tookOfferTick() tea.Cmd { return a.offerTick() } -// tookUpdateGrace fires the automatic answer. It RE-READS the live setting and -// the persisted memory first: a person who turned auto update off during the -// window, or another window that dismissed the same tag, must not be installed -// over by a decision this session made ten seconds ago. +// tookUpdateGrace fires the automatic answer at the deadline. It asks the same +// live question the countdown asks on every beat ([app.updateAutoWithdrawn]): +// a person who turned auto update off during the window, or another window +// that dismissed the same tag, must not be installed over by a decision this +// session made ten seconds ago. func (a *app) tookUpdateGrace() tea.Cmd { if !a.offer.offering() { return nil } tag := a.offer.tag - if a.updateAuto == nil || !a.updateAuto.Enabled { + if withdrawn := a.updateAutoWithdrawn(tag); withdrawn != "" { a.offer.clear() - a.note("auto update is off · nothing was installed · /update installs codeaf " + tag + " when you want it") + a.note(withdrawn) return nil } - if a.updateAuto.State != nil { - state := a.updateAuto.State() - if !codeupdate.ShouldOffer(state, tag, a.now()) { - a.offer.clear() - a.note("codeaf " + tag + " will not be installed on its own · /update installs it when you want it") - return nil - } - } if refusal := a.updateRefusal(); refusal != "" { a.offer.clear() a.note(refusal) @@ -367,7 +466,7 @@ func (a *app) tookUpdateGrace() tea.Cmd { } a.offer.expire() a.note(a.updateOfferNote()) - return a.beginUpdate(updateChoice(tag, a.updateRunning), true) + return a.beginUpdate(updateChoice(tag, a.updateRunning), true, false) } // offerSettle schedules the fold of a finished line and clears it when due. diff --git a/internal/tui3/updateoffer_test.go b/internal/tui3/updateoffer_test.go index 51060e9e34..d3c9fbf7cb 100644 --- a/internal/tui3/updateoffer_test.go +++ b/internal/tui3/updateoffer_test.go @@ -2,10 +2,18 @@ package tui3 import ( "context" + "crypto/sha256" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "runtime" "strings" "testing" "time" + "github.com/Agent-Field/codeaf/internal/config" codeupdate "github.com/Agent-Field/codeaf/internal/update" ) @@ -95,7 +103,8 @@ func TestSkipOnlyAnswersTheOfferingRelease(t *testing.T) { if lab.installed != 0 || a.offer.active() || state.DismissedRelease != "v0.9.3" { t.Fatalf("installed = %d offer = %+v dismissed = %q", lab.installed, a.offer, state.DismissedRelease) } - if !strings.Contains(updateNotes(a), "will not be offered again") { + if !strings.Contains(updateNotes(a), "skipped codeaf v0.9.3") || + !strings.Contains(updateNotes(a), "newer releases will still be offered") { t.Fatalf("the skip note is unclear:\n%s", updateNotes(a)) } } @@ -191,22 +200,50 @@ func TestOfferHintCountsDownAndKeepsItsActionsAtEightyColumns(t *testing.T) { } } -// TestTheDeferClauseIsDroppedBeforeTheTurnsOwnKeys proves M3. -func TestTheDeferClauseIsDroppedBeforeTheTurnsOwnKeys(t *testing.T) { +// TestTheWorkingOfferKeepsItsSkipWhileTheCountdownRuns proves M3/M4 over a +// running turn: at a narrow width the OFFER's clause survives the shortening and +// what is given up first is the turn's leftmost clause. A countdown that ran +// with its only control shortened away would be a ten-second timer nobody could +// answer. +func TestTheWorkingOfferKeepsItsSkipWhileTheCountdownRuns(t *testing.T) { lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) a := lab.app + a.state = stateWorking + a.turn = 1 + a.input.setText("keep going") a.offer.raise("v0.9.3", "v0.9.2", a.now()) - composed := a.withUpdateOffer("esc interrupt · ctrl+g backgrounds") - if !strings.Contains(composed, "/update skip") { + full := a.runHint() + if full == "" || !strings.Contains(full, "esc interrupt") { + t.Fatalf("the working hint is not the turn's own: %q", full) + } + composed := a.withUpdateOffer(full) + if !strings.Contains(composed, updateOfferDeferWords) { t.Fatalf("the defer clause is not composed:\n%s", composed) } shortened := a.hintShorter(composed) - if shortened != a.runHint() { - t.Fatalf("shortening dropped more than the offer clause: %q", shortened) - } - if strings.Contains(shortened, "/update skip") { - t.Fatalf("the offer clause was not the first to go: %q", shortened) + if !strings.Contains(shortened, updateOfferDeferWords) { + t.Fatalf("the offer's skip was dropped while the countdown ran:\n%s", shortened) + } + if len(shortened) >= len(composed) { + t.Fatalf("the line did not shorten: %q", shortened) + } + if !strings.Contains(shortened, "esc interrupt") { + t.Fatalf("the key that stops the turn went before the offer's clause: %q", shortened) + } + // SHORTENING ALWAYS MAKES PROGRESS, so a narrow frame cannot spin here, and + // the offer's clause is the last thing to go before the turn's own key. + line := shortened + for steps := 0; steps < 12; steps++ { + next := a.hintShorter(line) + if next == line { + t.Fatalf("shortening stopped making progress at %q", line) + } + line = next + if line == "" { + return + } } + t.Fatalf("shortening never emptied the row: %q", line) } // TestPlainLettersNeverAnswerTheOffer proves the composer keeps its typing. @@ -321,7 +358,9 @@ func TestUpdateDemoFixturesDrawEveryState(t *testing.T) { } } case "downloading", "manual": - if a.offer.phase != updateDownloading || (name == "manual") != a.offer.manual { + // BOTH ROADS DRAW THE SAME LINE; the case name is what tells + // the frame which one it raised (the label is written last). + if a.offer.phase != updateDownloading { t.Fatalf("download fixture = %+v", a.offer) } case "ready": @@ -374,3 +413,268 @@ func TestEveryUpdateCommandWeShowIsParsed(t *testing.T) { t.Fatal("no /update road is shown at all") } } + +// productionAuto wires the automatic updater the way cmd/codeaf does: every +// answer comes out of the PROFILE, so two windows in one test share one row, +// one remembered dismissal and one install lock. A mirror of these callbacks +// would prove nothing about the two-window case. +func productionAuto(profile string) *UpdateCoordinator { + return &UpdateCoordinator{ + Enabled: config.UpdateAutoAt(profile), + EnabledLive: func() bool { return config.UpdateAutoAt(profile) }, + State: func() codeupdate.AutoState { return codeupdate.LoadAutoState(profile) }, + Dismiss: func(tag string) error { return codeupdate.DismissRelease(profile, tag) }, + Failure: func(tag string, failure error) { codeupdate.AutoFailure(profile, tag, failure) }, + Success: func() { codeupdate.AutoSuccess(profile) }, + Disable: func() error { return config.SaveUpdateAuto(profile, false) }, + } +} + +// TestTwoWindowsSharingAProfileAnswerTheAutomaticRoad is the shared-profile +// regression: two windows on ONE profile, wired the way production wires them, +// so a row turned off \u2014 or a release skipped \u2014 in the other window stops THIS +// window's countdown before it downloads anything. +func TestTwoWindowsSharingAProfileAnswerTheAutomaticRoad(t *testing.T) { + t.Run("the other window turns the row off", func(t *testing.T) { + profile := t.TempDir() + lab := newOfferLab(t, productionAuto(profile)) + drive(t, lab.app, offerCheck("v0.9.3")) + if !lab.app.offer.offering() { + t.Fatal("the launch check raised no offer") + } + // The other window's /settings, or `/update never` there. + if err := config.SaveUpdateAuto(profile, false); err != nil { + t.Fatal(err) + } + drive(t, lab.app, updateGraceMsg{}) + if lab.installed != 0 || lab.app.offer.active() { + t.Fatalf("the other window's row was ignored: installed = %d offer = %+v", lab.installed, lab.app.offer) + } + if !strings.Contains(updateNotes(lab.app), "auto update is off") { + t.Fatalf("the withdrawal was silent:\n%s", updateNotes(lab.app)) + } + }) + t.Run("the other window skips this release", func(t *testing.T) { + profile := t.TempDir() + lab := newOfferLab(t, productionAuto(profile)) + drive(t, lab.app, offerCheck("v0.9.3")) + if !lab.app.offer.offering() { + t.Fatal("the launch check raised no offer") + } + if err := codeupdate.DismissRelease(profile, "v0.9.3"); err != nil { + t.Fatal(err) + } + // The countdown's own beat, which asks the same live question. + drive(t, lab.app, updateOfferTickMsg{}) + if lab.installed != 0 || lab.app.offer.active() { + t.Fatalf("the other window's dismissal was ignored: installed = %d offer = %+v", lab.installed, lab.app.offer) + } + if !strings.Contains(updateNotes(lab.app), "will not be installed on its own") { + t.Fatalf("the withdrawal was silent:\n%s", updateNotes(lab.app)) + } + }) + t.Run("the row goes off while the resolver is out", func(t *testing.T) { + profile := t.TempDir() + lab := newOfferLab(t, productionAuto(profile)) + a := lab.app + drive(t, a, offerCheck("v0.9.3")) + command := a.tookUpdateGrace() + if command == nil || !a.updateInFlight { + t.Fatalf("the deadline did not start the resolver: cmd = %v in flight = %t", command, a.updateInFlight) + } + // The resolver is off-frame; the other window answers in the gap. + if err := config.SaveUpdateAuto(profile, false); err != nil { + t.Fatal(err) + } + if cmd := a.tookUpdateResolve(updateResolveMsg{ + release: codeupdate.Release{Tag: "v0.9.3"}, auto: true, + }); cmd != nil { + t.Fatalf("a withdrawn automatic resolve returned work: %v", cmd) + } + if lab.installed != 0 || a.updateInFlight || a.offer.active() { + t.Fatalf("installed = %d in flight = %t offer = %+v", lab.installed, a.updateInFlight, a.offer) + } + if !strings.Contains(updateNotes(a), "auto update is off") { + t.Fatalf("the withdrawal was silent:\n%s", updateNotes(a)) + } + }) + t.Run("this window's own row writes the file the reader reads", func(t *testing.T) { + profile := t.TempDir() + lab := newOfferLab(t, productionAuto(profile)) + if !lab.app.updateAutoEnabledNow() { + t.Fatal("the default row did not read as on") + } + if cmd := lab.app.runUpdateCommand("never"); cmd != nil { + t.Fatalf("/update never returned work: %v", cmd) + } + if config.UpdateAutoAt(profile) || lab.app.updateAutoEnabledNow() { + t.Fatalf("the offer's /update never did not reach the row: file = %t live = %t", + config.UpdateAutoAt(profile), lab.app.updateAutoEnabledNow()) + } + }) +} + +// TestTheGraceWaitsWhereTheOfferCannotBeAnswered proves the pause's scope: a +// page or layer that owns the keyboard must not consume the ten seconds +// invisibly, while the conversation, home and a RUNNING TURN must keep counting +// (the running turn is the whole reason the countdown exists). +func TestTheGraceWaitsWhereTheOfferCannotBeAnswered(t *testing.T) { + hidden := []struct { + name string + seat func(*app) + }{ + {"the settings page", func(a *app) { a.raisePlace(pageSettings) }}, + {"the memory page", func(a *app) { a.raisePlace(pageMemory) }}, + {"the standing page", func(a *app) { a.raisePlace(pageStanding) }}, + {"the first-run sheet", func(a *app) { a.setup.open = true }}, + {"a team's card", func(a *app) { a.tsheet.on = true }}, + {"the move picker", func(a *app) { a.tmove.on = true }}, + {"the wall", func(a *app) { a.wall.on = true }}, + {"the team menu", func(a *app) { a.teamMenu.on = true }}, + {"the provider panel", func(a *app) { a.addPanel.open = true }}, + {"a question page", func(a *app) { a.qroom = &questionRoom{} }}, + } + for _, row := range hidden { + t.Run("waits under "+row.name, func(t *testing.T) { + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + a := lab.app + a.offer.raise("v0.9.3", "v0.9.2", a.now()) + row.seat(a) + a.offer.deadline = a.now().Add(-time.Second) + if cmd := a.tookOfferTick(); cmd == nil || !a.offer.offering() { + t.Fatalf("the grace was spent under %s: cmd = %v offer = %+v", row.name, cmd, a.offer) + } + if !a.offer.deadline.After(a.now()) { + t.Fatalf("the deadline was not moved on under %s: %s", row.name, a.offer.deadline) + } + if lab.installed != 0 { + t.Fatalf("%s installed a release", row.name) + } + }) + } + counting := []struct { + name string + seat func(*app) + }{ + {"the conversation", func(a *app) {}}, + {"home", func(a *app) { a.raisePlace(pageHome) }}, + {"a running turn", func(a *app) { a.state = stateWorking; a.turn = 1; a.input.setText("work") }}, + } + for _, row := range counting { + t.Run("counts over "+row.name, func(t *testing.T) { + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + a := lab.app + a.offer.raise("v0.9.3", "v0.9.2", a.now()) + row.seat(a) + a.offer.deadline = a.now().Add(-time.Second) + if cmd := a.tookOfferTick(); cmd == nil || !a.updateInFlight { + t.Fatalf("the deadline did not fire over %s: cmd = %v in flight = %t", row.name, cmd, a.updateInFlight) + } + }) + } +} + +// TestBareUpdateDuringTheOfferDoesNotRollBackAFileAnotherWindowAdvanced is the +// /update resolution boundary, exercised through the REAL installer: the offer +// is about v0.9.3 while the file on disk already holds v0.9.4, because another +// window installed it. A bare `/update` \u2014 the offer's own "now", which pinned +// the offered tag itself \u2014 must not step the disk back; `/update v0.9.3` typed +// by hand may, because that is a person naming the release. +func TestBareUpdateDuringTheOfferDoesNotRollBackAFileAnotherWindowAdvanced(t *testing.T) { + older, newer := []byte("v0.9.3 bytes"), []byte("v0.9.4 bytes") + olderSum, newerSum := sha256.Sum256(older), sha256.Sum256(newer) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + asset, sum := newer, newerSum + if strings.Contains(request.URL.Path, "v0.9.3") { + asset, sum = older, olderSum + } + switch { + case strings.HasSuffix(request.URL.Path, "/checksums.txt"): + fmt.Fprintf(w, "%x codeaf-%s-%s\n", sum, runtime.GOOS, runtime.GOARCH) + case strings.HasSuffix(request.URL.Path, "/codeaf-"+runtime.GOOS+"-"+runtime.GOARCH): + _, _ = w.Write(asset) + default: + http.NotFound(w, request) + } + })) + defer server.Close() + client := &codeupdate.Client{HTTP: server.Client(), APIBase: server.URL, DownloadBase: server.URL} + + // labFor gives a window whose installer is the SHARED one, writing to a + // temporary executable that another window has already advanced to v0.9.4. + labFor := func(t *testing.T, running string) (*offerLab, string) { + t.Helper() + target := filepath.Join(t.TempDir(), "codeaf") + if err := os.WriteFile(target, []byte("something older"), 0o755); err != nil { + t.Fatal(err) + } + if _, err := codeupdate.Install(context.Background(), codeupdate.InstallOptions{ + Client: client, Release: codeupdate.Release{Tag: "v0.9.4"}, Target: target, + }); err != nil { + t.Fatalf("the other window's install: %v", err) + } + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + lab.app.updateRunning = running + lab.app.resolveUpdate = func(_ context.Context, choice codeupdate.Choice) (codeupdate.Release, error) { + tag := "v0.9.3" + if strings.TrimSpace(choice.Version) != "" { + tag = strings.TrimSpace(choice.Version) + } + return codeupdate.Release{Tag: tag}, nil + } + lab.app.installUpdate = func(_ context.Context, options codeupdate.InstallOptions) (codeupdate.InstallResult, error) { + options.Client = client + options.Target = target + return codeupdate.Install(context.Background(), options) + } + return lab, target + } + + t.Run("bare /update answered from the offer", func(t *testing.T) { + lab, target := labFor(t, "v0.9.2") + drive(t, lab.app, offerCheck("v0.9.3")) + if !lab.app.offer.offering() { + t.Fatal("the launch check raised no offer") + } + drive(t, lab.app, lab.app.slash("/update")()) + if onDisk, err := os.ReadFile(target); err != nil || string(onDisk) != string(newer) { + t.Fatalf("bare /update rolled the file back: %q, %v", onDisk, err) + } + if !strings.Contains(updateNotes(lab.app), "newer than v0.9.3") { + t.Fatalf("the refusal was not said:\n%s", updateNotes(lab.app)) + } + }) + + t.Run("the same release named by hand", func(t *testing.T) { + lab, target := labFor(t, "v0.9.2") + if cmd := lab.app.slash("/update v0.9.3"); cmd == nil { + t.Fatal("/update v0.9.3 built no command") + } else { + drive(t, lab.app, cmd()) + } + if onDisk, err := os.ReadFile(target); err != nil || string(onDisk) != string(older) { + t.Fatalf("a named tag did not reach the installer: %q, %v", onDisk, err) + } + if !strings.Contains(updateNotes(lab.app), "checksum matched") { + t.Fatalf("the hand-run completion was not said:\n%s", updateNotes(lab.app)) + } + }) + + t.Run("a named tag equal to the running stamp", func(t *testing.T) { + // THIS WINDOW RUNS v0.9.3 AND THE FILE HOLDS v0.9.4: the stamp is not + // the disk, so the request must reach the installer instead of the + // quiet "newest" no-op. + lab, target := labFor(t, "v0.9.3") + if cmd := lab.app.slash("/update v0.9.3"); cmd == nil { + t.Fatal("/update v0.9.3 built no command") + } else { + drive(t, lab.app, cmd()) + } + if onDisk, err := os.ReadFile(target); err != nil || string(onDisk) != string(older) { + t.Fatalf("a named tag equal to the running stamp never reached the installer: %q, %v", onDisk, err) + } + if strings.Contains(updateNotes(lab.app), "you are on the newest codeaf") { + t.Fatalf("the process stamp was mistaken for the file on disk:\n%s", updateNotes(lab.app)) + } + }) +} diff --git a/internal/update/auto.go b/internal/update/auto.go index e1d2efcd9c..356dd4821c 100644 --- a/internal/update/auto.go +++ b/internal/update/auto.go @@ -233,20 +233,53 @@ type installRecord struct { InstalledAt time.Time `json:"installed_at,omitempty"` } +// canonicalTarget resolves a target to the ABSOLUTE, LINK-FREE path whose file +// the installer will actually replace, so that two windows naming one +// executable through different spellings — a relative path, a symlinked +// folder, `..` — hold ONE lock and serialize against each other. It is +// [ExecutableTarget]'s answer, applied again here because the installer also +// runs for a target a caller handed in directly (`codeaf update`, a test). +func canonicalTarget(target string) string { + target = strings.TrimSpace(target) + if target == "" { + return "" + } + absolute, err := filepath.Abs(target) + if err != nil { + absolute = filepath.Clean(target) + } + absolute = filepath.Clean(absolute) + // THE WHOLE PATH FIRST: an executable that is already there is resolved + // exactly as [ExecutableTarget] resolves it, which is the file the + // replacement will land on. + if resolved, err := filepath.EvalSymlinks(absolute); err == nil { + return filepath.Clean(resolved) + } + // THE DIRECTORY OTHERWISE: a target that does not exist yet cannot be + // resolved itself, but its folder can, and two aliases of one folder must + // still point at one lock file. + dir, base := filepath.Split(absolute) + if resolved, err := filepath.EvalSymlinks(filepath.Clean(dir)); err == nil { + return filepath.Join(resolved, base) + } + return absolute +} + // installLockPath names the lock-and-record file for a target. It sits beside // the executable so a target directory that is not writable (which would fail // the install anyway) cannot be the reason two installers run; the file is left // in place after release, because unlinking it would let a fresh opener create // a second inode and hold a second lock. func installLockPath(target string) string { - target = filepath.Clean(strings.TrimSpace(target)) - return target + ".install.lock" + return canonicalTarget(target) + ".install.lock" } // TryTargetLock claims the lock for one executable target, returning -// ErrInstallInFlight when another process holds it. +// ErrInstallInFlight when another process holds it. The target is canonicalized +// first, so an alias of a file already held answers in flight rather than +// opening a second lock file beside a path nothing else names. func TryTargetLock(target string) (*InstallLock, error) { - target = strings.TrimSpace(target) + target = canonicalTarget(target) if target == "" { return nil, errors.New("the running executable path is empty") } @@ -362,8 +395,13 @@ func recordSupersedes(record installRecord, release Release) bool { // InstallRefusal names why this target may not be replaced in place — a package // manager owns it, or its directory is not writable — or "" when an ordinary // install may proceed. The curl line is the road the refusal offers instead. +// +// THE TARGET IS CANONICALIZED FIRST, exactly as [Install] canonicalizes it: a +// launcher that hands in a symlink (or a folder reached through one) must answer +// about the file the replacement would land on, or a managed install answers +// "ordinary" while the installer refuses it a moment later. func InstallRefusal(target, curl string) string { - target = strings.TrimSpace(target) + target = canonicalTarget(target) if manager := PackageManaged(target); manager != "" { return "this codeaf is managed by " + manager + " · update it there, or install a release with: " + curl } diff --git a/internal/update/auto_test.go b/internal/update/auto_test.go index b6b3b7623e..95a3a77ade 100644 --- a/internal/update/auto_test.go +++ b/internal/update/auto_test.go @@ -5,8 +5,12 @@ import ( "crypto/sha256" "encoding/hex" "errors" + "fmt" "os" + "os/exec" "path/filepath" + "runtime" + "strings" "testing" "time" ) @@ -232,3 +236,156 @@ func TestInstallRefusesADuplicateAndAStaleDowngrade(t *testing.T) { t.Fatalf("named rollback = %+v, %v", rolled, err) } } + +// TestTargetLockAnswersOneTargetThroughEverySpelling proves the canonicalization +// the whole duplicate/downgrade guard rests on: a relative path, an absolute +// path and a path through a symlinked folder are ONE target, so the second +// spelling answers "in flight" rather than opening a second lock file beside a +// path nothing else names. +func TestTargetLockAnswersOneTargetThroughEverySpelling(t *testing.T) { + root := t.TempDir() + realDir := filepath.Join(root, "real") + if err := os.MkdirAll(realDir, 0o755); err != nil { + t.Fatal(err) + } + target := filepath.Join(realDir, "codeaf") + if err := os.WriteFile(target, []byte("build"), 0o755); err != nil { + t.Fatal(err) + } + held, err := TryTargetLock(target) + if err != nil { + t.Fatalf("lock the real path: %v", err) + } + defer held.Release() + + spellings := map[string]string{"absolute": target, "dirty": realDir + "/./codeaf"} + if rel, err := filepath.Rel(mustGetwd(t), target); err == nil && !filepath.IsAbs(rel) { + spellings["relative"] = rel + } + if runtime.GOOS != "windows" { + alias := filepath.Join(root, "alias") + if err := os.Symlink(realDir, alias); err != nil { + t.Fatalf("symlink the folder: %v", err) + } + spellings["symlinked folder"] = filepath.Join(alias, "codeaf") + } + for name, spelling := range spellings { + t.Run(name, func(t *testing.T) { + second, err := TryTargetLock(spelling) + if !errors.Is(err, ErrInstallInFlight) { + t.Fatalf("TryTargetLock(%q) = %v, want in flight", spelling, err) + } + if second != nil { + t.Fatal("an alias of a held target handed back a lock") + } + }) + } +} + +func mustGetwd(t *testing.T) string { + t.Helper() + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + return wd +} + +// TestTargetLockSerialisesTwoRealProcesses is the cross-process half: a second +// PROCESS, with its own file descriptors and its own address space, must find +// the lock already held. The child is this test binary run again, so nothing is +// simulated about it (see [TestTargetLockSecondProcessHelper]). +func TestTargetLockSerialisesTwoRealProcesses(t *testing.T) { + if os.Getenv(targetLockHelperEnv) != "" { + t.Skip("helper process") + } + dir := t.TempDir() + target := filepath.Join(dir, "codeaf") + if err := os.WriteFile(target, []byte("build"), 0o755); err != nil { + t.Fatal(err) + } + held, err := TryTargetLock(target) + if err != nil { + t.Fatalf("hold the target: %v", err) + } + + child := exec.Command(os.Args[0], "-test.run=^TestTargetLockSecondProcessHelper$", "-test.v") + child.Env = append(os.Environ(), targetLockHelperEnv+"="+target) + output, runErr := child.CombinedOutput() + if runErr != nil { + t.Fatalf("the child exited %v, want 0 (in flight):\n%s", runErr, output) + } + held.Release() + + // AND WITH THE LOCK DROPPED, the same child takes it: the first answer was + // the other process's lock and not a child that cannot lock at all. + child = exec.Command(os.Args[0], "-test.run=^TestTargetLockSecondProcessHelper$", "-test.v") + child.Env = append(os.Environ(), targetLockHelperEnv+"="+target) + output, runErr = child.CombinedOutput() + if runErr == nil { + t.Fatalf("the child took a released lock with no error, so the guard proved nothing:\n%s", output) + } +} + +// targetLockHelperEnv carries one target to the child process above. +const targetLockHelperEnv = "CODEAF_UPDATE_LOCK_HELPER_TARGET" + +// TestTargetLockSecondProcessHelper is the child half of the test above. It +// exits 0 when the target is already held by another process, and EXITS +// NON-ZERO when it could take the lock, which the parent reads as "the lock did +// not serialize". +func TestTargetLockSecondProcessHelper(t *testing.T) { + targetPtr := strings.TrimSpace(os.Getenv(targetLockHelperEnv)) + if targetPtr == "" { + t.Skip("not the helper") + } + lock, err := TryTargetLock(targetPtr) + switch { + case errors.Is(err, ErrInstallInFlight): + os.Exit(0) + case err != nil: + fmt.Fprintf(os.Stderr, "helper lock error: %v\n", err) + os.Exit(1) + } + lock.Release() + fmt.Fprintln(os.Stderr, "helper took the lock") + os.Exit(2) +} + +// TestInstallRefusalFollowsASymlinkedTarget proves the ownership screen answers +// about the file the replacement lands on, not about the alias that named it: a +// launcher holding the running executable as a symlink into a folder this +// account cannot write is refused, where the alias's own (writable) folder would +// have answered "ordinary" and left the installer to refuse it a moment later. +func TestInstallRefusalFollowsASymlinkedTarget(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlinked folders need a privilege this test cannot assume") + } + if os.Geteuid() == 0 { + t.Skip("root may write into any folder, so there is no refusal to see") + } + root := t.TempDir() + realDir := filepath.Join(root, "real") + if err := os.MkdirAll(realDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(realDir, "codeaf"), []byte("build"), 0o755); err != nil { + t.Fatal(err) + } + alias := filepath.Join(root, "alias") + if err := os.Symlink(realDir, alias); err != nil { + t.Fatal(err) + } + if !dirWritable(alias) { + t.Fatal("the alias's own folder is not writable; this test would prove nothing") + } + if err := os.Chmod(realDir, 0o500); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chmod(realDir, 0o755) }) + + refusal := InstallRefusal(filepath.Join(alias, "codeaf"), "curl -fsSL https://example.invalid/codeaf | bash") + if refusal == "" || !strings.Contains(refusal, "cannot write") { + t.Fatalf("a symlinked target in an unwritable folder = %q, want the write refusal", refusal) + } +} diff --git a/internal/update/install.go b/internal/update/install.go index 2b21844d33..07b7dab417 100644 --- a/internal/update/install.go +++ b/internal/update/install.go @@ -44,7 +44,11 @@ func Install(ctx context.Context, options InstallOptions) (InstallResult, error) if options.Client == nil { return InstallResult{}, errors.New("no release client is available") } - target := strings.TrimSpace(options.Target) + // THE TARGET IS CANONICALIZED ONCE, HERE, BEFORE ANYTHING IS DECIDED FROM + // IT: the refusal, the lock, the temporary file beside it and the atomic + // replace all name the SAME absolute, link-free path, so a caller handing + // in a relative path or a symlinked folder is not a second target. + target := canonicalTarget(options.Target) curl := strings.TrimSpace(options.Curl) if curl == "" { curl = CurlCommand diff --git a/scripts/update-drive.sh b/scripts/update-drive.sh index db2ec9982b..58b5129dd3 100755 --- a/scripts/update-drive.sh +++ b/scripts/update-drive.sh @@ -1,14 +1,25 @@ #!/usr/bin/env bash -# Capture the update offer's deterministic fixtures from THIS worktree's binary. +# Capture the update offer's deterministic fixtures AND the real settings row +# from THIS worktree's binary. # -# scripts/update-drive.sh # the whole matrix -# scripts/update-drive.sh offer 80 # one case at one width +# scripts/update-drive.sh # the whole matrix +# scripts/update-drive.sh offer 80 # one offer fixture at one width +# scripts/update-drive.sh settings-off 120 # the real /settings page, row off # -# Every fixture disables the updater's hooks (internal/tui3/updatedemo.go), and -# this drive adds to that: a home and workspace made fresh under mktemp, a -# private tmux server, an EMPTY inherited environment, and `chat --no-host` so -# no session host is left behind. Nothing here reaches a release host, installs -# anything, or writes outside the temporary directory, which is removed on exit. +# The offer fixtures raise the REAL state machine with every updater hook +# disabled (internal/tui3/updatedemo.go), so no capture can make a request or +# replace a file. The settings captures are the OTHER kind and are labelled as +# such: they open the REAL settings page through /settings and type into its +# search box, so the row drawn is the registry's own row reading the profile's +# own value \u2014 there is no fixture state in them at all. +# +# This drive adds to both: a home and workspace made fresh under mktemp, a +# private tmux server, an EMPTY inherited environment, `chat --no-host` so no +# session host is left behind, CODEAF_NO_UPDATE_CHECK=1 so the launch check makes +# no request, and CODEAF_BASE_URL pointed at a loopback address so ANY provider +# or catalog call a surface might start lands on this machine and nowhere else. +# No real key, profile or engine is read or written and nothing outside the +# temporary directory is touched. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -16,59 +27,90 @@ BIN="${UPDATE_BIN:-$ROOT/bin/codeaf}" OUT="${UPDATE_OUT:-$ROOT/docs/design/background-updates-frames}" CASES=(offer downloading ready ready-working manual off failure) WIDTHS=(80 120) -if [ "${1:-}" = "--all" ]; then - shift - CASES=(offer downloading ready ready-working manual off failure) - WIDTHS=(80 120) -fi -[ -x "$BIN" ] || { echo "no binary at $BIN — run make build" >&2; exit 1; } +[ -x "$BIN" ] || { echo "no binary at $BIN \u2014 run make build" >&2; exit 1; } command -v tmux >/dev/null || { echo "tmux is required for a screen drive" >&2; exit 1; } -TMP="$(mktemp -d "${TMPDIR:-/tmp}/codeaf-update-drive.XXXXXX")" +TMP_PREFIX="${TMPDIR:-/tmp}/codeaf-update-drive." +TMP="$(mktemp -d "$TMP_PREFIX"XXXXXX)" SOCK="codeaf-update-$$" +# CLEANUP REMOVES ONLY WHAT THIS SCRIPT MADE. The home, the workspace and the +# profile all live under $TMP, which mktemp just created under that prefix; a +# user-supplied UPDATE_OUT is written into and never deleted. cleanup() { tmux -L "$SOCK" kill-server 2>/dev/null || true - rm -rf "$TMP" + case "$TMP" in + "$TMP_PREFIX"*) rm -rf "$TMP" ;; + *) echo "refusing to remove $TMP" >&2 ;; + esac } trap cleanup EXIT -HOME_DIR="$TMP/home" WORK="$TMP/work/project" -mkdir -p "$HOME_DIR" "$WORK" "$OUT" -cat >"$HOME_DIR/config.json" <<'JSON' +mkdir -p "$WORK" "$OUT" + +# profile — write the demo profile. `auto` is left UNSET for the +# default-on capture (so the page shows the default, not a value this script +# wrote) and is the JSON word false for the off one. Every model tier the +# surface can resolve is named exactly, so no catalog lookup is ever needed. +profile() { + local dir="$1" auto="${2:-}" tail=' "ui.mouse": "on"' + mkdir -p "$dir" + if [ -n "$auto" ]; then + tail=" \"ui.mouse\": \"on\", + \"update.auto\": $auto" + fi + cat >"$dir/config.json" < -capture() { - local case="$1" cols="$2" - local session="update-$case-$cols" file="$OUT/$case-${cols}c.txt" +# launch \u2014 start the surface in a private +# tmux server with an empty environment. +launch() { + local session="$1" cols="$2" home_dir="$3" demo="$4" demo_env="" + [ "$demo" != "-" ] && demo_env="CODEAF_UPDATE_DEMO=$(printf '%q' "$demo")" tmux -L "$SOCK" kill-session -t "$session" 2>/dev/null || true tmux -L "$SOCK" new-session -d -s "$session" -x "$cols" -y 40 \ "cd $(printf '%q' "$WORK") && env -i \ - HOME=$(printf '%q' "$HOME_DIR") \ - CODEAF_HOME=$(printf '%q' "$HOME_DIR") \ - CODEAF_PROFILE_DIR=$(printf '%q' "$HOME_DIR") \ + HOME=$(printf '%q' "$home_dir") \ + CODEAF_HOME=$(printf '%q' "$home_dir") \ + CODEAF_PROFILE_DIR=$(printf '%q' "$home_dir") \ CODEAF_NO_UPDATE_CHECK=1 \ + CODEAF_BASE_URL=http://127.0.0.1:9 \ TERM=xterm-256color \ LANG=C.UTF-8 \ PATH=/usr/bin:/bin \ - CODEAF_UPDATE_DEMO=$(printf '%q' "$case") \ + $demo_env \ $(printf '%q' "$BIN") chat --no-host" sleep 3 +} + +# grab \u2014 keep both the plain text and the painted capture. +grab() { + local session="$1" file="$2" tmux -L "$SOCK" capture-pane -p -t "$session" >"$file" - tmux -L "$SOCK" capture-pane -p -e -t "$session" >"$OUT/$case-${cols}c.ansi" + tmux -L "$SOCK" capture-pane -p -e -t "$session" >"${file%.txt}.ansi" tmux -L "$SOCK" kill-session -t "$session" 2>/dev/null || true +} + +# capture \u2014 one offer fixture at one width. +capture() { + local case="$1" cols="$2" + local session="update-$case-$cols" file="$OUT/$case-${cols}c.txt" home_dir="$TMP/home/$case" + profile "$home_dir" + launch "$session" "$cols" "$home_dir" "$case" + grab "$session" "$file" # A CAPTURE THAT DID NOT DRAW THE FIXTURE IS A FAILURE, not a file. Every # fixture writes its own name last, so this checks the real surface ran. grep -q "CODEAF_UPDATE_DEMO=$case" "$file" || { @@ -79,13 +121,81 @@ capture() { echo "$file" } -if [ $# -gt 0 ]; then - capture "$1" "${2:-80}" - exit 0 -fi - -for case in "${CASES[@]}"; do - for cols in "${WIDTHS[@]}"; do - capture "$case" "$cols" +# capture_settings \u2014 the REAL /settings page with the real +# `auto update` row, reached the way a person reaches it: the command, the +# arrow keys that walk the tabs to Display, and the down key that walks the +# cursor onto the row. The row is confirmed selected by its OWN hint being on +# screen, not by the label merely existing somewhere in the frame. +capture_settings() { + local state="$1" cols="$2" + local session="settings-$state-$cols" file="$OUT/settings-auto-$state-${cols}c.txt" + local home_dir="$TMP/home/settings-$state" auto="" + [ "$state" = "off" ] && auto="false" + profile "$home_dir" "$auto" + launch "$session" "$cols" "$home_dir" - + tmux -L "$SOCK" send-keys -t "$session" -l "/settings" + tmux -L "$SOCK" send-keys -t "$session" Enter + sleep 2 + # THE DISPLAY TAB, BY THE PANEL'S OWN ARROW KEY: Session, Context, Workspace, + # Display. The panel's type-to-search is deliberately NOT used here — it ranks + # fuzzy matches from every tab, so the row under the cursor is usually the + # wrong one and the sentence drawn under it explains somebody else's setting. + tmux -L "$SOCK" send-keys -t "$session" Right + tmux -L "$SOCK" send-keys -t "$session" Right + tmux -L "$SOCK" send-keys -t "$session" Right + sleep 2 + local landed="" + for _ in $(seq 1 24); do + # THE ROW'S OWN HINT IS THE PROOF THE CURSOR IS ON IT: the sheet draws the + # hint only for the row under the cursor, and this row's hint opens with + # these words. + if tmux -L "$SOCK" capture-pane -p -t "$session" | grep -q "check for a new codeaf at launch"; then + landed=yes + break + fi + tmux -L "$SOCK" send-keys -t "$session" Down + sleep 0.4 done -done + if [ -z "$landed" ]; then + echo "the settings capture at ${cols}c never selected the auto update row:" >&2 + tmux -L "$SOCK" capture-pane -p -t "$session" >&2 + tmux -L "$SOCK" kill-session -t "$session" 2>/dev/null || true + exit 1 + fi + grab "$session" "$file" + # THE ROW ITSELF IS THE CHECK, not the fact that something drew: the registry's + # label, the sentence the registry wrote for it, and the value its reader + # resolved. A capture without all three is a failure. + grep -q "check for a new codeaf at launch" "$file" || { + echo "the settings capture at ${cols}c did not draw the row's own hint:" >&2 + cat "$file" >&2 + exit 1 + } + grep -q "auto update" "$file" || { + echo "the settings capture at ${cols}c did not draw the auto update row:" >&2 + cat "$file" >&2 + exit 1 + } + grep -q "auto update.*$state" "$file" || { + echo "the settings capture at ${cols}c did not read $state:" >&2 + cat "$file" >&2 + exit 1 + } + echo "$file" +} + +case "${1:-}" in + --all) shift + for case in "${CASES[@]}"; do for cols in "${WIDTHS[@]}"; do capture "$case" "$cols"; done; done + for state in on off; do for cols in "${WIDTHS[@]}"; do capture_settings "$state" "$cols"; done; done + ;; + "") for case in "${CASES[@]}"; do for cols in "${WIDTHS[@]}"; do capture "$case" "$cols"; done; done + for state in on off; do for cols in "${WIDTHS[@]}"; do capture_settings "$state" "$cols"; done; done + ;; + settings) for state in on off; do for cols in "${WIDTHS[@]}"; do capture_settings "$state" "$cols"; done; done + ;; + settings-on|settings-off) capture_settings "${1#settings-}" "${2:-80}" + ;; + *) capture "$1" "${2:-80}" + ;; +esac From af832334d21de8b4771c70fcaccd84e2605da10a Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 11:10:36 -0400 Subject: [PATCH 04/12] update: the lock reads its own record through the handle that holds it InstallLock.Record opened the record file a second time to read it back. On Windows our own exclusive byte-range lock makes that second handle unable to read the region until it is unlocked, so the record written by the lock holder was the one thing the holder could not read; on every platform it was a second look-up of a name another process may be replacing. The record is now read from the held descriptor through a bounded io.NewSectionReader, which does not disturb the write cursor RecordInstalled uses. Focused checks: internal/update (whole package, with the new read-through test covering a second write after a read, a replaced file, corrupt bytes and the read bound), `go vet` and a GOOS=windows compile check for internal/update and cmd/codeaf. No Windows runtime claim: the reference is the LockFileEx contract in the comment, and the run is a compile check on Spark. --- internal/update/auto.go | 17 ++++++++- internal/update/auto_test.go | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/internal/update/auto.go b/internal/update/auto.go index 356dd4821c..4e4b18f85b 100644 --- a/internal/update/auto.go +++ b/internal/update/auto.go @@ -294,14 +294,29 @@ func TryTargetLock(target string) (*InstallLock, error) { return &InstallLock{target: target, file: file}, nil } +// maxInstallRecordBytes bounds the record read: the file holds one small JSON +// object and nothing else, so a corrupted or replaced file is not read whole +// into memory. +const maxInstallRecordBytes = 4 << 10 + // Record reads the last install this target recorded, and reports false unless // the record still matches the file on disk. A tag written by bytes that are no // longer there is not a fact about the file. +// +// THE BYTES COME THROUGH THE HELD HANDLE, and the read does not move its cursor +// (io.NewSectionReader). A second handle would be a second look-up of a name +// another process may be replacing, and on Windows it would be unreadable to the +// process that locked it: LockFileEx's own documentation says that a process +// which locks a region with one handle — which is what [lockFileExclusive] +// does — cannot access that region through a second handle until it unlocks it +// (learn.microsoft.com/windows/win32/api/fileapi/nf-fileapi-lockfileex). A record +// written by the lock holder would then be the one thing it could not read, so +// the record is read from the handle that holds the lock. func (l *InstallLock) Record() (installRecord, bool) { if l == nil || l.file == nil { return installRecord{}, false } - raw, err := os.ReadFile(l.file.Name()) + raw, err := io.ReadAll(io.NewSectionReader(l.file, 0, maxInstallRecordBytes)) if err != nil { return installRecord{}, false } diff --git a/internal/update/auto_test.go b/internal/update/auto_test.go index 95a3a77ade..4729d8a27b 100644 --- a/internal/update/auto_test.go +++ b/internal/update/auto_test.go @@ -389,3 +389,74 @@ func TestInstallRefusalFollowsASymlinkedTarget(t *testing.T) { t.Fatalf("a symlinked target in an unwritable folder = %q, want the write refusal", refusal) } } + +// TestRecordReadsThroughTheHeldHandle proves the record is read on the handle +// the installer already holds — the property Windows' LockFileEx makes a +// requirement rather than a preference (see [InstallLock.Record]). Reading must not disturb that handle's write +// cursor (a second RecordInstalled after a read must land at offset zero, not +// append), no second handle is opened to a name another process may replace, and +// what the read cannot make sense of reads as NO record rather than as bytes. +func TestRecordReadsThroughTheHeldHandle(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "codeaf") + if err := os.WriteFile(target, []byte("first bytes"), 0o755); err != nil { + t.Fatal(err) + } + lock, err := TryTargetLock(target) + if err != nil { + t.Fatalf("lock: %v", err) + } + defer lock.Release() + + digest := func(body string) string { + sum := sha256.Sum256([]byte(body)) + return hex.EncodeToString(sum[:]) + } + lock.RecordInstalled("v0.1.0", digest("first bytes"), time.Now()) + if record, ok := lock.Record(); !ok || record.Tag != "v0.1.0" { + t.Fatalf("the record just written = %+v, ok = %t", record, ok) + } + + // A SECOND WRITE AFTER A READ IS STILL THE WHOLE FILE. + if err := os.WriteFile(target, []byte("second bytes"), 0o755); err != nil { + t.Fatal(err) + } + lock.RecordInstalled("v0.2.0", digest("second bytes"), time.Now()) + record, ok := lock.Record() + if !ok || record.Tag != "v0.2.0" { + t.Fatalf("the record after a read = %+v, ok = %t", record, ok) + } + + // A FILE THAT NO LONGER MATCHES IS NOT A FACT ABOUT THE FILE. + if err := os.WriteFile(target, []byte("somebody else's build"), 0o755); err != nil { + t.Fatal(err) + } + if record, ok := lock.Record(); ok { + t.Fatalf("a replaced file still trusted %+v", record) + } + + // NOTHING THE READ CANNOT PARSE IS A RECORD. + if err := lock.file.Truncate(0); err != nil { + t.Fatal(err) + } + if _, err := lock.file.WriteAt([]byte("{not json"), 0); err != nil { + t.Fatal(err) + } + if record, ok := lock.Record(); ok { + t.Fatalf("a corrupt record file trusted %+v", record) + } + // AND IT IS READ BOUNDED: the object the installer writes sits at offset + // zero, so bytes beyond the ceiling are not read whole and cannot turn into + // a trusted record. + valid := fmt.Sprintf(`{"tag":"v0.9.9","sha256":"%s"}`, digest("somebody else's build")) + padded := strings.Repeat(" ", maxInstallRecordBytes*2) + valid + if err := lock.file.Truncate(0); err != nil { + t.Fatal(err) + } + if _, err := lock.file.WriteAt([]byte(padded), 0); err != nil { + t.Fatal(err) + } + if record, ok := lock.Record(); ok { + t.Fatalf("bytes past the read bound were trusted as %+v", record) + } +} From 6ac1c47d22a800703d5851e4719d374475b837ed Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 11:17:32 -0400 Subject: [PATCH 05/12] update: the offer's visibility is a place's own answer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The launch offer's countdown asked "am I standing on home?" by comparing the page id itself (updateoffer.go), which is the switch LAW 1 forbids outside the registry, and newOfferLab built its surface with no profile of its own (testprofilelaw_test.go), so an earlier test's marker decided what it saw. The visibility question is now the registry's: place.showsOffer says whether a place draws the offer's own keys row (pages.go), false by default and true on home, whose foot draws the offer in place of the resting hint (hometip.go), so a room added later answers it on the day it is registered. The conversation answers by not being a place at all. newOfferLab mints a profile with t.TempDir(); the two-window shared-profile cases keep their own shared profile through the Coordinator's callbacks, as they did. Focused checks on Spark: the two failing laws alone (TestNothingSwitchesOnAPageIdOutsideTheRegistry, TestEveryHelperThatBuildsASurfaceNamesAProfile) pass; ./scripts/laws.sh is green; the update tests in internal/tui3 (TestLaunchOffer…, TestTwoWindows…, TestTheGraceWaitsWhereTheOfferCannotBeAnswered, TestBareUpdate…, and the every-place laws) pass; internal/update whole; go vet ./internal/tui3. No credit tests touched; the root full gate diagnoses those separately. --- internal/tui3/pages.go | 17 +++++++++++++++++ internal/tui3/place_home.go | 5 +++++ internal/tui3/updateoffer.go | 6 +++++- internal/tui3/updateoffer_test.go | 6 +++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/internal/tui3/pages.go b/internal/tui3/pages.go index 1e32e167f4..f12c98e524 100644 --- a/internal/tui3/pages.go +++ b/internal/tui3/pages.go @@ -133,6 +133,18 @@ type place interface { // line naming four keys nobody has, so the way out has to be a target rather // than a legend. bar(a *app, width int) (string, placeHit, bool) + // showsOffer is whether THIS PLACE DRAWS THE OFFER'S KEYS ROW — the line a + // launch release speaks in ([app.updateOfferHint]) — rather than a foot of + // its own. The countdown reads it to know whether its sentence can be read + // and its chord answered where the person is standing (updateoffer.go's + // [app.updateOfferHidden]). + // + // ONLY HOME ANSWERS TRUE, and the conversation answers by not being a place + // at all ([app.showing] is nil there). Every other place's keys row is its + // own sentence about the row under its cursor ([place.hint]), drawn by + // [placeFrame], and the offer is not on it — so its ten seconds are not + // spent behind a line the person cannot read. + showsOffer(a *app) bool // ownFrame is the ONE ESCAPE from [placeFrame], and exactly two places take // it. Home unpacks a second hit map — the pane a row shares with an errand — // and below sixty columns is an inbox and a sheet rather than a list @@ -316,6 +328,11 @@ func (placeBase) remote(a *app) string { return "" } func (placeBase) bar(a *app, width int) (string, placeHit, bool) { return "", nil, false } + +// showsOffer is NO UNLESS A PLACE SAYS OTHERWISE: a place that has not told the +// registry its foot is the offer's draws a foot of its own, and the countdown +// waits there rather than spending its ten seconds invisibly ([place.showsOffer]). +func (placeBase) showsOffer(a *app) bool { return false } func (placeBase) ownFrame(a *app, width, height int) ([]string, []placeHit, int, int, bool) { return nil, nil, 0, 0, false } diff --git a/internal/tui3/place_home.go b/internal/tui3/place_home.go index 45063aeb8d..5d837c66f4 100644 --- a/internal/tui3/place_home.go +++ b/internal/tui3/place_home.go @@ -522,6 +522,11 @@ func (placeHome) about() string { return "what wants you and what is running" } func (placeHome) hint(a *app) string { return a.homeHint() } +// showsOffer is YES: home draws the offer's own keys row where it is up, in +// place of the resting hint ([app.homeFootLine], hometip.go), so a release +// waiting on an answer is answerable from home as it is from a conversation. +func (placeHome) showsOffer(a *app) bool { return true } + // changed is ZERO AND THAT IS THE DESIGN. Home is where the "since you left" // ledger is DRAWN, in sentences that say what happened and open the place it // happened in — so a digit on its tab would be the same news said twice, once diff --git a/internal/tui3/updateoffer.go b/internal/tui3/updateoffer.go index e5c5ff8f8f..84738f8953 100644 --- a/internal/tui3/updateoffer.go +++ b/internal/tui3/updateoffer.go @@ -414,7 +414,11 @@ func (a *app) updateOfferHidden() bool { if a.teamMenu.on || a.navMore.on || a.tsheet.on || a.tmove.on || a.wall.on { return true } - if pl := a.showing(); pl != nil && pl.id() != pageHome { + // A PLACE WITH A FOOT OF ITS OWN IS NOT SHOWING THE OFFER. The question is + // the registry's and not this file's: a place says whether its keys row is + // the offer's ([place.showsOffer]), so a room added later answers it on the + // day it is registered rather than on the day somebody remembers this list. + if pl := a.showing(); pl != nil && !pl.showsOffer(a) { return true } return a.sheetLayerOwnsKeys() || a.addPanel.open diff --git a/internal/tui3/updateoffer_test.go b/internal/tui3/updateoffer_test.go index d3c9fbf7cb..4b1b83496a 100644 --- a/internal/tui3/updateoffer_test.go +++ b/internal/tui3/updateoffer_test.go @@ -39,9 +39,13 @@ type offerLab struct { func newOfferLab(t *testing.T, coordinator *UpdateCoordinator) *offerLab { t.Helper() lab := &offerLab{allow: new(bool)} + // A PROFILE OF ITS OWN, so this lab never sees a marker another test wrote + // (testprofilelaw_test.go). The two-window tests share their profile through + // the Coordinator's own callbacks ([productionAuto]) and write the file the + // other window writes, so no second surface needs the same directory here. lab.app = newApp(context.Background(), Options{ Agent: &fakeAgent{model: "test/model"}, Workspace: "/tmp/lab", UpdateRunning: "v0.9.2", - Restart: &codeupdate.Plan{}, UpdateAuto: coordinator, + Restart: &codeupdate.Plan{}, UpdateAuto: coordinator, ProfileDir: t.TempDir(), ResolveUpdate: func(context.Context, codeupdate.Choice) (codeupdate.Release, error) { return codeupdate.Release{Tag: "v0.9.3"}, nil }, From 014994f3d71917fed53987d034784cdd6c2fb561 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 11:22:23 -0400 Subject: [PATCH 06/12] update: a failed install keeps its tag, and a pinned tag keeps its moment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three gaps the frozen review verified, none of them a new surface. P2-1, the lost failure tag: every error out of codeupdate.Install carries an empty InstallResult, so tookUpdateInstall read "" from the result and updateStopped recorded no AutoFailure — no count, no backoff, no three-strike stop, and the line drew `codeaf could not be installed`. The requested tag is now carried on updateInstallMsg from the resolve (which also stops the resolve from blanking the offer's own tag) and read from the message, the result and last the offer on BOTH the error and the success roads. P2-2, the settings row told the truth backwards: `update.auto` off stops installing, not checking. The hint and the KeyUpdateAuto comment now say off installs nothing until asked while the launch check still runs and names a release on one dim line, and name CODEAF_NO_UPDATE_CHECK=1 as the only switch that makes no request. (The manual and the design already said this; only the config surface contradicted them.) P2-3, the pinned tag carried no publish moment: the automatic and offer roads resolve an exact tag, Select answers it without the API, and the record was written with a zero PublishedAt — so recordSupersedes fell to the same-day date comparison and a stale same-day dev/staging offer could write over a newer file another window had installed, AllowDowngrade false notwithstanding. The launch check's LatestPublished now rides on the offer (updateOffer.published) into Choice.Published and out as Release.PublishedAt, and recordSupersedes refuses a same-day same-channel pair when either moment is unknown instead of guessing permissive. Exact-tag rollback is untouched: a named tag still sets AllowDowngrade, and a pinned resolve still asks no API, so `--version ` and `/update ` install what they name with the assets alone. Focused checks on Spark: internal/update whole (9.9s) plus the new pinned-moment test and the extended recordSupersedes table; internal/tui3 update/offer set including the two new tests (P2-1 through the REAL installer against a host that answers nothing, asserting the exact tag, the count, the persisted LastError, the backoff and the block at three; P2-3 through the REAL Select and Install on two windows of one target, asserting the pinned resolve carried 10:00 and dev-b's file survived); cmd/codeaf update/version suites; internal/config; ./scripts/laws.sh green; go vet and gofmt clean. Credits tests untouched. The change entry now states which version is which (the offer names the release, the status row and the note name the build you are on) and the limits of activation: an install replaces the file only, the running session keeps its build until it is restarted, and the automatic road moves forward only. --- .../unreleased/1789-background-updates.md | 13 ++ internal/config/settings.go | 22 +- internal/tui3/updatecmd.go | 55 ++++- internal/tui3/updateoffer.go | 22 +- internal/tui3/updateoffer_test.go | 197 ++++++++++++++++++ internal/update/auto.go | 20 +- internal/update/auto_test.go | 11 + internal/update/client.go | 14 +- internal/update/update_test.go | 34 +++ 9 files changed, 363 insertions(+), 25 deletions(-) diff --git a/docs/changes/unreleased/1789-background-updates.md b/docs/changes/unreleased/1789-background-updates.md index 5d29ff7573..ca2be167c4 100644 --- a/docs/changes/unreleased/1789-background-updates.md +++ b/docs/changes/unreleased/1789-background-updates.md @@ -46,3 +46,16 @@ durable answer it did not write. the failure count live in one small file beside `config.json`, keyed by release and by executable, so the same release never asks twice and a release that failed three times stops being tried on its own. + +WHICH VERSION IS WHICH IS ON THE SCREEN. The offer's line names the release that +is out; the build this window is RUNNING is the one the status row and `/status` +report, and the note the offer writes names both (`codeaf is out \u00b7 you +have `). An install only replaces the file: nothing about the running +session changes, and the new build is what opens the next time codeaf is started +\u2014 so a window can be current and behind its channel at the same time, on +purpose. The automatic road moves forward only: a duplicate is a quiet no-op, an +older build is refused unless a person names the tag, and two same-day dev or +staging builds whose published moment is not known are refused rather than +guessed at. With `update.auto` off the launch check still runs and says the same +two versions in one quiet line; `CODEAF_NO_UPDATE_CHECK=1` is what makes no +request at all. diff --git a/internal/config/settings.go b/internal/config/settings.go index 213a12b46f..2270c7d629 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -280,14 +280,17 @@ const ( // because a person who has silenced the tips has said they know the surface, // and being told about features is the same conversation. KeyHints = "ui.hints" - // KeyUpdateAuto is whether codeaf checks for a new release at launch and - // installs it in the background on its own. ON is the default: the check is - // one small request after the first frame, and a person who does not want it - // has a row, an environment variable and one chord on the offer itself. Off - // means no launch request and no background install at all; `/update` still - // installs a release on request, because a person who typed it has said what - // they want. It is a boolean because there is no third answer between - // "keep me current" and "leave me alone until I ask". + // KeyUpdateAuto is whether codeaf INSTALLS a new release in the background on + // its own. ON is the default: the check is one small request after the first + // frame, and a person who does not want it has a row, an environment variable + // and one chord on the offer itself. OFF means nothing is installed until the + // person asks — `/update` still installs a release on request, because a + // person who typed it has said what they want — but the LAUNCH CHECK STILL + // RUNS and still names the release on one dim line, because knowing is not + // acting (cmd/codeaf's surface wires the check separately from this row). + // CODEAF_NO_UPDATE_CHECK=1 is the switch that makes no request at all. It is a + // boolean because there is no third answer between "keep me current" and + // "leave me alone until I ask". KeyUpdateAuto = "update.auto" // KeyWork controls whether completed turn machinery starts folded or open. KeyWork = "ui.work" @@ -2675,7 +2678,8 @@ func (s *Settings) build() []Setting { Label: "auto update", Hint: "check for a new codeaf at launch and install it in the background. " + "The offer waits in the keys line for about ten seconds; it never blocks a keystroke. " + - "Off, nothing is checked or downloaded until you run /update.", + "Off, nothing is installed until you run /update; codeaf still checks once and names a new release " + + "(CODEAF_NO_UPDATE_CHECK=1 makes no request at all).", read: func() string { return formatBool(UpdateAutoAt(dir)) }, write: func(raw string) error { return writeBool(dir, KeyUpdateAuto, raw) }, }, diff --git a/internal/tui3/updatecmd.go b/internal/tui3/updatecmd.go index 0e35fc1ef4..638fb7b85c 100644 --- a/internal/tui3/updatecmd.go +++ b/internal/tui3/updatecmd.go @@ -40,6 +40,14 @@ type updateInstallMsg struct { result codeupdate.InstallResult err error auto bool + // tag is the release THIS WINDOW asked for. The installer answers an empty + // [codeupdate.InstallResult] on every error ([codeupdate.Install]), so a + // failure tag read from the result alone would be "", and the automatic + // road would record no failure and name no release. It is carried from the + // resolve, which is the last place both the answer and the offer's tag are + // in hand ([app.tookUpdateInstall] falls back to the offer for a message + // built before this field existed). + tag string } // checkForUpdate starts after the app is ready to draw, so a slow or absent @@ -84,6 +92,10 @@ func (a *app) tookUpdateCheck(message updateCheckMsg) tea.Cmd { return nil } a.offer.raise(tag, a.updateRunning, a.now()) + // THE MOMENT IS KEPT WITH THE TAG, because the automatic road resolves the + // tag itself rather than the channel and the install record needs the fact + // (see [updateOffer.published] and [app.pinnedChoice]). + a.offer.published = message.available.LatestPublished a.note(a.updateOfferNote()) return a.offerTick() } @@ -152,6 +164,17 @@ func (a *app) beginUpdate(choice codeupdate.Choice, auto, named bool) tea.Cmd { } } +// pinnedChoice is the request for ONE exact tag this window is holding, with the +// publish moment the launch check gave it ([updateOffer.published]). It is the +// automatic road's own answer and bare `/update` answered from an open offer: +// both pin a tag, both install with AllowDowngrade false, so both must hand the +// installer the fact that orders two same-day channel builds. +func (a *app) pinnedChoice(tag string) codeupdate.Choice { + choice := updateChoice(tag, a.updateRunning) + choice.Published = a.offer.published + return choice +} + func updateChoice(argument, running string) codeupdate.Choice { argument = strings.TrimSpace(argument) switch argument { @@ -214,8 +237,16 @@ func (a *app) tookUpdateResolve(message updateResolveMsg) tea.Cmd { // The lock, the on-disk re-read and the duplicate/downgrade refusal all live // inside [codeupdate.Install], so the chat surface has no second acquirer to // keep in step with the command line. + // THE TAG IS COMPUTED ONCE, AND THE OFFER KEEPS IT WHEN THE RESOLVE COULD + // NOT. A resolver that answers a bare tag (the pinned road) always names + // one; a resolver that errored may not, and the offer's own tag is the last + // fact either road has. + tag := strings.TrimSpace(message.release.Tag) + if tag == "" { + tag = strings.TrimSpace(a.offer.tag) + } a.offer.downloading() - a.offer.tag = strings.TrimSpace(message.release.Tag) + a.offer.tag = tag a.note(a.updateOfferNote()) install := a.installUpdate release := message.release @@ -228,7 +259,7 @@ func (a *app) tookUpdateResolve(message updateResolveMsg) tea.Cmd { result, err := install(context.Background(), codeupdate.InstallOptions{ Release: release, AllowDowngrade: allowDowngrade, }) - return updateInstallMsg{result: result, err: err, auto: message.auto} + return updateInstallMsg{result: result, err: err, auto: message.auto, tag: tag} } } @@ -237,21 +268,29 @@ func (a *app) tookUpdateResolve(message updateResolveMsg) tea.Cmd { // launch opens the new one. func (a *app) tookUpdateInstall(message updateInstallMsg) tea.Cmd { a.updateInFlight = false + // THE TAG SURVIVES A FAILURE. Every error out of the installer carries an + // empty result, so the tag this window asked for is read from the message, + // then the result, and last from the offer that raised it — never from the + // empty result alone. Without this the automatic road recorded no failure + // and drew `codeaf could not be installed`: no count, no backoff, and no + // release named in the line a person reads. + tag := strings.TrimSpace(message.tag) + if tag == "" { + tag = strings.TrimSpace(message.result.Release.Tag) + } + if tag == "" { + tag = strings.TrimSpace(a.offer.tag) + } if message.err != nil { - return a.updateStopped(message.auto, message.result.Release.Tag, message.err) + return a.updateStopped(message.auto, tag, message.err) } if message.result.Already { // ALREADY INSTALLED IS SUCCESS, SAID QUIETLY: the build on disk is the // one this window was about to write, so there is nothing to alarm. - tag := strings.TrimSpace(message.result.Release.Tag) a.offer.clear() a.note("codeaf " + tag + " is already the build on disk · this session is untouched") return nil } - tag := strings.TrimSpace(message.result.Release.Tag) - if tag == "" { - tag = a.offer.tag - } a.offer.ready(a.now()) a.offer.tag = tag if a.updateAuto != nil && a.updateAuto.Success != nil { diff --git a/internal/tui3/updateoffer.go b/internal/tui3/updateoffer.go index 84738f8953..9602dcbf4e 100644 --- a/internal/tui3/updateoffer.go +++ b/internal/tui3/updateoffer.go @@ -75,10 +75,17 @@ const ( // updateOffer is the launch offer's whole state. type updateOffer struct { - phase updatePhase - tag string - running string - deadline time.Time + phase updatePhase + tag string + running string + // published is the moment the launch check said this exact release was + // published, kept so the pinned resolve can hand it to the installer + // ([codeupdate.Choice.Published]): an install record written without it + // cannot order two same-day dev or staging builds, and a stale one would be + // written over a newer file on disk. A raise with no check behind it leaves + // it zero, which the installer treats as "unknown" rather than as old. + published time.Time + deadline time.Time // settleAt is when the ready or failed line folds away. settleAt time.Time } @@ -305,11 +312,14 @@ func (a *app) updateNowFromOffer() tea.Cmd { return nil } tag := a.offer.tag + // THE MOMENT IS TAKEN BEFORE THE OFFER IS CLEARED, because the pinned + // resolve still has to name it (see [app.pinnedChoice]). + choice := a.pinnedChoice(a.offer.tag) a.offer.clear() a.note("installing codeaf " + tag + " now") // THE TAG IS OURS, NOT THEIRS: the person typed bare `/update`, so this // install may not roll the file back to the candidate this window pinned. - return a.beginUpdate(updateChoice(tag, a.updateRunning), false, false) + return a.beginUpdate(choice, false, false) } // skipUpdateOffer answers this release "not now", and only while it is being @@ -470,7 +480,7 @@ func (a *app) tookUpdateGrace() tea.Cmd { } a.offer.expire() a.note(a.updateOfferNote()) - return a.beginUpdate(updateChoice(tag, a.updateRunning), true, false) + return a.beginUpdate(a.pinnedChoice(tag), true, false) } // offerSettle schedules the fold of a finished line and clears it when due. diff --git a/internal/tui3/updateoffer_test.go b/internal/tui3/updateoffer_test.go index 4b1b83496a..9965dcd9b4 100644 --- a/internal/tui3/updateoffer_test.go +++ b/internal/tui3/updateoffer_test.go @@ -3,6 +3,7 @@ package tui3 import ( "context" "crypto/sha256" + "errors" "fmt" "net/http" "net/http/httptest" @@ -682,3 +683,199 @@ func TestBareUpdateDuringTheOfferDoesNotRollBackAFileAnotherWindowAdvanced(t *te } }) } + +// TestAFailedAutomaticInstallKeepsItsTagAndStopsAfterThree is P2-1 through the +// REAL installer: every error it answers carries an empty +// [codeupdate.InstallResult], so the tag has to survive in the surface or the +// automatic road records no failure against the release — no count, no backoff, +// and a line naming nobody. The release host here answers nothing, which is +// exactly where the empty result comes from. +// +// THE WALL CLOCK IS COMPRESSED, NOT THE COUNTER. Between attempts the test +// clears LastAttemptAt in the profile so the backoff window has passed, the way +// three launches on three days would; the failure count, the release it belongs +// to and the block at the end are the production ones. +func TestAFailedAutomaticInstallKeepsItsTagAndStopsAfterThree(t *testing.T) { + profile := t.TempDir() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + http.NotFound(w, request) + })) + defer server.Close() + client := &codeupdate.Client{HTTP: server.Client(), APIBase: server.URL, DownloadBase: server.URL} + target := filepath.Join(t.TempDir(), "codeaf") + if err := os.WriteFile(target, []byte("the running build"), 0o755); err != nil { + t.Fatal(err) + } + const tag = "v0.9.3" + lab := newOfferLab(t, productionAuto(profile)) + a := lab.app + var asked []string + a.resolveUpdate = client.Select + a.installUpdate = func(install context.Context, options codeupdate.InstallOptions) (codeupdate.InstallResult, error) { + options.Client = client + options.Target = target + asked = append(asked, strings.TrimSpace(options.Release.Tag)) + result, err := codeupdate.Install(install, options) + if err == nil { + return result, errors.New("the release host answered an install it should not have") + } + // THE INSTALLER'S OWN CONTRACT, ASSERTED HERE: a failure names no + // release, so a surface reading the result alone loses the tag. + if result.Release.Tag != "" { + return result, fmt.Errorf("the installer returned a tagged result on an error: %+v", result) + } + return result, err + } + // THE FIRST ATTEMPT IS THE PRODUCTION ONE: the launch check raises the offer + // and the grace runs out. + drive(t, a, offerCheck(tag)) + if !a.offer.offering() { + t.Fatal("the launch check raised no offer") + } + a.offer.deadline = a.now().Add(-time.Second) + drive(t, a, updateOfferTickMsg{}) + state := codeupdate.LoadAutoState(profile) + if state.FailedRelease != tag || state.Failures != 1 || state.LastError == "" { + t.Fatalf("the failed release was not remembered: %+v", state) + } + if !strings.Contains(updateNotes(a), "codeaf "+tag+" could not be installed") { + t.Fatalf("the failure line did not name the release:\n%s", updateNotes(a)) + } + // THE BACKOFF IS REAL: the same check on the next launch does not offer it. + if err := codeupdate.SaveAutoState(profile, state); err != nil { + t.Fatal(err) + } + drive(t, a, offerCheck(tag)) + if a.offer.offering() { + t.Fatal("a release inside its failure backoff was offered again") + } + // TWO MORE FAILURES, the way later launches reach the same release, exhaust + // the count. + for attempt := 2; attempt <= 3; attempt++ { + state := codeupdate.LoadAutoState(profile) + state.LastAttemptAt = time.Time{} // the backoff window has passed + if err := codeupdate.SaveAutoState(profile, state); err != nil { + t.Fatal(err) + } + drive(t, a, offerCheck(tag)) + if !a.offer.offering() { + t.Fatalf("attempt %d was not offered after its backoff", attempt) + } + a.offer.deadline = a.now().Add(-time.Second) + drive(t, a, updateOfferTickMsg{}) + if got := codeupdate.LoadAutoState(profile).Failures; got != attempt { + t.Fatalf("attempt %d recorded %d failures", attempt, got) + } + } + final := codeupdate.LoadAutoState(profile) + if !final.Blocked(tag) || codeupdate.ShouldOffer(final, tag, a.now()) { + t.Fatalf("the exhausted release is still offered: %+v", final) + } + drive(t, a, offerCheck(tag)) + if a.offer.offering() { + t.Fatal("a release that failed out was offered again") + } + if len(asked) != 3 { + t.Fatalf("the installer was asked for %d releases, want 3", len(asked)) + } + for _, requested := range asked { + if requested != tag { + t.Fatalf("an attempt carried %q, want %q", requested, tag) + } + } +} + +// TestASameDayDevWindowDoesNotStepBackTheBuildAnotherWindowInstalled is P2-3 end +// to end through the REAL resolver and the REAL installer, on the automatic +// road: window A installs dev-b at 11:00 (the record keeps that moment), and +// window B's stale check — cached before b was published — offers dev-a at +// 10:00. AllowDowngrade is false there, so the published ordering the record +// now carries refuses the rollback instead of writing the older file. +func TestASameDayDevWindowDoesNotStepBackTheBuildAnotherWindowInstalled(t *testing.T) { + const aTag, bTag = "dev-20261006-aaaaaaaaaaaa", "dev-20261006-bbbbbbbbbbbb" + publishedA := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) + publishedB := publishedA.Add(time.Hour) + aBytes, bBytes := []byte("dev-a bytes"), []byte("dev-b bytes") + aSum, bSum := sha256.Sum256(aBytes), sha256.Sum256(bBytes) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + switch { + case strings.Contains(request.URL.Path, "/releases?") || strings.HasSuffix(request.URL.Path, "/releases"): + fmt.Fprintf(w, `[{"tag_name":%q,"published_at":%q},{"tag_name":%q,"published_at":%q}]`, + aTag, publishedA.Format(time.RFC3339), bTag, publishedB.Format(time.RFC3339)) + case strings.HasSuffix(request.URL.Path, "/checksums.txt"): + sum := bSum + if strings.Contains(request.URL.Path, aTag) { + sum = aSum + } + fmt.Fprintf(w, "%x codeaf-%s-%s\n", sum, runtime.GOOS, runtime.GOARCH) + case strings.Contains(request.URL.Path, "/releases/download/"): + asset := bBytes + if strings.Contains(request.URL.Path, aTag) { + asset = aBytes + } + _, _ = w.Write(asset) + default: + http.NotFound(w, request) + } + })) + defer server.Close() + client := &codeupdate.Client{HTTP: server.Client(), APIBase: server.URL, DownloadBase: server.URL} + + target := filepath.Join(t.TempDir(), "codeaf") + if err := os.WriteFile(target, []byte("a before-this-morning build"), 0o755); err != nil { + t.Fatal(err) + } + // WINDOW A: the ordinary channel road (what `codeaf update --dev` and a bare + // /update resolve), so the install record is written with the API's own + // publish moment for dev-b. + releaseB, err := client.Select(context.Background(), codeupdate.Choice{Channel: "dev", Running: aTag}) + if err != nil || releaseB.Tag != bTag || !releaseB.PublishedAt.Equal(publishedB) { + t.Fatalf("the channel resolve = %+v, %v", releaseB, err) + } + if _, err := codeupdate.Install(context.Background(), codeupdate.InstallOptions{ + Client: client, Release: releaseB, Target: target, + }); err != nil { + t.Fatalf("window A's install: %v", err) + } + + // WINDOW B: a surface holding the STALE check, with production's own seams + // pointed at the one shared executable. + profile := t.TempDir() + lab := newOfferLab(t, productionAuto(profile)) + a := lab.app + var resolved codeupdate.Release + a.resolveUpdate = client.Select + a.installUpdate = func(install context.Context, options codeupdate.InstallOptions) (codeupdate.InstallResult, error) { + options.Client = client + options.Target = target + resolved = options.Release + return codeupdate.Install(install, options) + } + drive(t, a, updateCheckMsg{available: codeupdate.Available{ + Latest: aTag, Running: bTag, LatestPublished: publishedA, RunningPublished: publishedB, + }, show: true}) + if !a.offer.offering() { + t.Fatal("the stale check raised no offer") + } + a.offer.deadline = a.now().Add(-time.Second) + drive(t, a, updateOfferTickMsg{}) + + if resolved.Tag != aTag { + t.Fatalf("the resolver was asked for %q", resolved.Tag) + } + // THE MOMENT RODE WITH THE PINNED TAG. Without it the record and the + // candidate could not be ordered under the lock and the older build would + // have been written over the newer one. + if !resolved.PublishedAt.Equal(publishedA) { + t.Fatalf("the pinned resolve carried published_at %s, want %s", resolved.PublishedAt, publishedA) + } + if onDisk, err := os.ReadFile(target); err != nil || string(onDisk) != string(bBytes) { + t.Fatalf("the older same-day build was written: %q, %v", onDisk, err) + } + if !strings.Contains(updateNotes(a), "newer than "+aTag) { + t.Fatalf("the refusal was not said:\n%s", updateNotes(a)) + } + if state := codeupdate.LoadAutoState(profile); state.Failures != 0 { + t.Fatalf("a refusal was counted as a failure: %+v", state) + } +} diff --git a/internal/update/auto.go b/internal/update/auto.go index 4e4b18f85b..4dbde37cd7 100644 --- a/internal/update/auto.go +++ b/internal/update/auto.go @@ -384,6 +384,14 @@ func sha256File(path string) (string, error) { // this process resolved. It uses PUBLISHED ordering where the facts exist — the // same CompareChannelBuilds the launch check uses — and falls back to the tag // grammar only when it must; two tags it cannot order are not guessed at. +// +// TWO SAME-DAY CHANNEL BUILDS WITH AN UNKNOWN MOMENT ARE ONE OF THE TAGS IT +// CANNOT ORDER. The date inside them is equal and says nothing about which came +// second, so the permissive answer — proceed — would step a file another +// window advanced back to a stale same-day dev or staging build. The build on +// disk therefore wins; a person who means to roll back names the tag, which +// sets [InstallOptions.AllowDowngrade] and never comes through here for an +// answer. func recordSupersedes(record installRecord, release Release) bool { installed, candidate := strings.TrimSpace(record.Tag), strings.TrimSpace(release.Tag) if installed == "" || candidate == "" { @@ -399,7 +407,17 @@ func recordSupersedes(record installRecord, release Release) bool { comparison, ok := CompareChannelBuilds(installed, record.PublishedAt, candidate, release.PublishedAt) return ok && comparison > 0 } - return installedDate > candidateDate + if installedDate != candidateDate { + // THE DATE IS A FACT WHEN THEY DIFFER: the older-dated build is the + // older one whatever the clock said on the day. + return installedDate > candidateDate + } + // SAME DAY, AND ONE OF THE TWO MOMENTS IS UNKNOWN. There is no fact + // left to order them with, and guessing would be permissive in exactly + // the case this refusal exists for ([codeupdate.Choice.Published] is + // how the pinned road hands the moment over, so a correct install + // carries it). + return true } if comparison, ok := CompareSemverTags(installed, candidate); ok { return comparison > 0 diff --git a/internal/update/auto_test.go b/internal/update/auto_test.go index 4729d8a27b..d5a772ad78 100644 --- a/internal/update/auto_test.go +++ b/internal/update/auto_test.go @@ -173,6 +173,17 @@ func TestRecordSupersedesUsesPublishedOrdering(t *testing.T) { {"same-day dev, later publish", installRecord{Tag: "dev-20261006-bbbbbbbbbbbb", PublishedAt: newer}, Release{Tag: "dev-20261006-aaaaaaaaaaaa", PublishedAt: older}, true}, {"same-day dev, earlier publish", installRecord{Tag: "dev-20261006-aaaaaaaaaaaa", PublishedAt: older}, Release{Tag: "dev-20261006-bbbbbbbbbbbb", PublishedAt: newer}, false}, {"unorderable", installRecord{Tag: "dev-20261006-aaaaaaaaaaaa"}, Release{Tag: "staging-20261007-bbbbbbbbbbbb"}, false}, + // SAME DAY AND ONE MOMENT UNKNOWN IS NOT A FACT TO ORDER BY. The + // permissive answer would step a file another window advanced back to a + // stale same-day build, so the record wins in BOTH directions and the + // caller must name a tag to roll back (auto.go's [recordSupersedes]). + {"same-day dev, record's moment unknown", installRecord{Tag: "dev-20261006-bbbbbbbbbbbb"}, Release{Tag: "dev-20261006-aaaaaaaaaaaa", PublishedAt: older}, true}, + {"same-day dev, candidate's moment unknown", installRecord{Tag: "dev-20261006-bbbbbbbbbbbb", PublishedAt: newer}, Release{Tag: "dev-20261006-aaaaaaaaaaaa"}, true}, + {"same-day dev, both moments unknown", installRecord{Tag: "dev-20261006-bbbbbbbbbbbb"}, Release{Tag: "dev-20261006-aaaaaaaaaaaa"}, true}, + // DIFFERENT DATES ARE A FACT whatever the moments say, in both channels + // that carry them. + {"dev one day newer", installRecord{Tag: "dev-20261007-aaaaaaaaaaaa"}, Release{Tag: "dev-20261006-bbbbbbbbbbbb"}, true}, + {"dev one day older", installRecord{Tag: "dev-20261006-aaaaaaaaaaaa"}, Release{Tag: "dev-20261007-bbbbbbbbbbbb"}, false}, } { t.Run(row.name, func(t *testing.T) { if got := recordSupersedes(row.record, row.release); got != row.want { diff --git a/internal/update/client.go b/internal/update/client.go index f6c6d654a1..e8f8174ddd 100644 --- a/internal/update/client.go +++ b/internal/update/client.go @@ -304,6 +304,18 @@ type Choice struct { Channel string Version string Running string + // Published is THE MOMENT THE CALLER ALREADY KNOWS this exact release was + // published, and it means something only beside Version. + // + // A PINNED TAG IS RESOLVED WITHOUT THE API, so the release it answers would + // carry no publish moment at all — and the install record written from it + // could not be ordered against the file on disk, which is how a stale + // same-day dev or staging build gets written over a newer one + // ([recordSupersedes]). The road that pins has the fact already: the launch + // check read it from the API and the offer kept it, so it rides here rather + // than being guessed at under the lock. A caller resolving a CHANNEL leaves + // it zero, because the API's own answer is the fact on that road. + Published time.Time } // Release is one selected GitHub release and the repository that answered. @@ -333,7 +345,7 @@ func (c *Client) Select(ctx context.Context, choice Choice) (Release, error) { if Kind(version) == "other" { return Release{}, fmt.Errorf("%q is not a codeaf release tag", version) } - return Release{Tag: version, Repository: primaryRepository}, nil + return Release{Tag: version, Repository: primaryRepository, PublishedAt: choice.Published}, nil } channel := strings.TrimSpace(choice.Channel) if channel == "" { diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 2e20dc904a..7f00f55d8d 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -923,3 +923,37 @@ func TestC14RestartArgumentsReopenTheSameConversation(t *testing.T) { }) } } + +// TestSelectingAPinnedTagCarriesTheCallersPublishMoment proves the pinned road's +// half of the resolution contract: an exact tag is answered WITHOUT the API, and +// the moment the caller already knows rides into the Release. The install record +// is written from that moment, and without it two same-day dev or staging builds +// could not be ordered under the lock (auto.go's [recordSupersedes]) — the guard +// that stops a stale offer from writing an older same-day build over a newer +// file another window installed. +func TestSelectingAPinnedTagCarriesTheCallersPublishMoment(t *testing.T) { + published := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) + // THE ADDRESS IS UNREACHABLE ON PURPOSE: a pinned tag is resolved from the + // request alone, and a test that needed a server here would be proving a + // contract the pinned road does not have. + client := &Client{APIBase: "http://127.0.0.1:1", DownloadBase: "http://127.0.0.1:1"} + release, err := client.Select(context.Background(), Choice{ + Version: "dev-20261006-aaaaaaaaaaaa", Running: "dev-20261006-bbbbbbbbbbbb", Published: published, + }) + if err != nil { + t.Fatal(err) + } + if release.Tag != "dev-20261006-aaaaaaaaaaaa" || release.Repository != primaryRepository { + t.Fatalf("pinned resolve = %+v", release) + } + if !release.PublishedAt.Equal(published) { + t.Fatalf("pinned resolve carried published_at %s, want %s", release.PublishedAt, published) + } + // AND NOTHING IS INVENTED WHEN THE CALLER HAS NOTHING. A named tag with no + // known moment answers zero, which the installer reads as unknown rather + // than as old. + blank, err := (&Client{}).Select(context.Background(), Choice{Version: "v0.2.0"}) + if err != nil || blank.Tag != "v0.2.0" || !blank.PublishedAt.IsZero() { + t.Fatalf("unnamed moment = %+v, %v", blank, err) + } +} From 3888ddc5acbbb1fc140182fdf4bc652518ddf0db Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 11:34:44 -0400 Subject: [PATCH 07/12] update: a saved skip survives another window's write, and the auto update row moves to Workspace The coordinator's memory is a read-modify-write of one file, and two windows could interleave it: a skip pressed in one while another finished an install read the state before the skip landed and saved it back without the dismissal, so the release the person declined was offered again and installed itself. Every change to update-state.json now goes through one mutateAutoState that holds an in-process mutex AND the profile's OS advisory lock on a stable `.lock` that is never unlinked, with a bounded wait so the surface cannot hang. DismissRelease already reports a failed write instead of claiming a saved skip; AutoFailure and AutoSuccess keep their signatures and no longer claim a write they did not make, and AutoSuccess still never clears a dismissal. A cross-process regression drives the real functions from two processes and proves the skip and the failure both survive the interleaving. The `auto update` row (`update.auto`) moves from the Display tab to Workspace, beside `background checks`, and reads its category with them: keeping this installation current is what codeaf does on this machine on its own, not how the surface draws itself. The key, label and default are unchanged, and the first sentence of its hint now says what it does and what off means. The settings capture script walks to Workspace and asserts the new sentence; the manual, design and change entry say the same. The change entry also loses two literal escape sequences, its claim that the status ROW reports the running build (it is the `build` row of `/status`, the build holding the session), and the internal "engine lane" wording. The design page gains a maintainer note: shared storage breaking changes must bump the engine wire compatibility version, and joining an older engine is safe only within that contract. --- .../unreleased/1789-background-updates.md | 9 +- docs/design/background-updates.md | 20 +- internal/config/settings.go | 31 +-- internal/config/settings_test.go | 56 +++++ .../manual/chat/running-from-the-terminal.md | 2 +- internal/tui3/settings.go | 15 +- internal/tui3/settingunit_test.go | 20 ++ internal/update/auto.go | 182 ++++++++++++-- internal/update/auto_test.go | 222 ++++++++++++++++++ scripts/update-drive.sh | 16 +- 10 files changed, 510 insertions(+), 63 deletions(-) diff --git a/docs/changes/unreleased/1789-background-updates.md b/docs/changes/unreleased/1789-background-updates.md index ca2be167c4..4a2c5afe7f 100644 --- a/docs/changes/unreleased/1789-background-updates.md +++ b/docs/changes/unreleased/1789-background-updates.md @@ -8,9 +8,10 @@ invalidates: - "The launch notice used to be a single dim line whose only action was `/update`. With `update.auto` on (the default) a newer release now raises an offer in the keys line, counted down live from ten seconds, and then installs itself in the background; `alt+n` or `/update skip` skips that exact release, and `/update never` or the settings row turns the automatic road off." - "A version behind its channel with `update.auto` off still gets one quiet line naming the release and the hand-run road, instead of no check at all — and the launch request really is made, because the standing row no longer suppresses it; only `CODEAF_NO_UPDATE_CHECK=1` does that." - "A codeaf owned by Homebrew, Nix or a system package manager, or one in a folder the account cannot write, is refused in place by both the chat and `codeaf update`, and the refusal names the manager and the curl road." - - "The manuals `internal/manual/chat/staying-on-that-machine.md` and `running-on-another-machine.md` said a busy engine from an older build was replaced the moment a newer codeaf opened. They now say the truth the engine lane implements: an older engine holding work is joined and keeps it, and steps aside once it is quiet (or at the next safe launch for a host too old to know the ask); one holding nothing hands over at once; an attached idle window counts as holding work; and another wire, or one too old to be asked, is refused with `codeaf engine --stop --workspace ''` named." + - "The manuals `internal/manual/chat/staying-on-that-machine.md` and `running-on-another-machine.md` said a busy engine from an older build was replaced the moment a newer codeaf opened. They now say the truth the engine implements: an older engine holding work is joined and keeps it, and steps aside once it is quiet (or at the next safe launch for a host too old to know the ask); one holding nothing hands over at once; an attached idle window counts as holding work; and another wire, or one too old to be asked, is refused with `codeaf engine --stop --workspace ''` named." - "One install now runs at a time per executable, across terminals and profiles: the lock is an OS advisory lock keyed on the executable AND IS TAKEN INSIDE the shared installer, so the chat surface and `codeaf update` are one acquirer. The target is canonicalized first — absolute, cleaned, symlinks resolved — so a relative path or a symlinked folder is the same target and serializes against it, and the ownership refusal answers about the file the replacement would land on. Under the lock a sha256-tied record says what is on disk: a duplicate is a quiet no-op and a stale automatic downgrade is refused, while an explicitly named tag may still roll back." - "Only a tag a person named may install an older build. The bare `/update` that answers an open offer installs the release the offer is about and no longer carries rollback intent, and a named tag reaches the installer even when it matches the running process's own stamp, because the stamp is not the file on disk." + - "The `auto update` row in `/settings` (`update.auto`) moved from the Display tab to the Workspace tab, beside `background checks`: keeping this installation current is what codeaf does on this machine on its own, and Display is where the surface's own appearance and typing live. The key, the label and the default are unchanged; only the tab it is read under moved." --- The offer waits in the line that already carries the idle keys, so it never @@ -48,11 +49,11 @@ and by executable, so the same release never asks twice and a release that faile three times stops being tried on its own. WHICH VERSION IS WHICH IS ON THE SCREEN. The offer's line names the release that -is out; the build this window is RUNNING is the one the status row and `/status` -report, and the note the offer writes names both (`codeaf is out \u00b7 you +is out; the build holding this session is the one `/status` reports (its `build` +row), and the note the offer writes names both (`codeaf is out · you have `). An install only replaces the file: nothing about the running session changes, and the new build is what opens the next time codeaf is started -\u2014 so a window can be current and behind its channel at the same time, on +— so a window can be current and behind its channel at the same time, on purpose. The automatic road moves forward only: a duplicate is a quiet no-op, an older build is refused unless a person names the tag, and two same-day dev or staging builds whose published moment is not known are refused rather than diff --git a/docs/design/background-updates.md b/docs/design/background-updates.md index a94c44de56..3597f8e7dd 100644 --- a/docs/design/background-updates.md +++ b/docs/design/background-updates.md @@ -26,7 +26,8 @@ itself. The three answers are: exact tag and that tag is never offered again; a newer release is a new question. If the write itself fails, the note says so instead of claiming a skip that was not saved — this window still stops offering it. -- `/update never`, or the `auto update` row in `/settings` — stop installing +- `/update never`, or the `auto update` row on `/settings`' **Workspace** tab + (beside `background checks`) — stop installing automatically. It does not cancel an install already downloading and does not say it did. The launch check still runs and still says what is out: with the row off the one request is still made and the release is named on one dim @@ -67,7 +68,7 @@ checksum matched · installed at (hand-run only) codeaf installed · this session keeps running ``` -The durable note says what is true without the engine lane's vocabulary: `new +The durable note says what is true without the engine's internal vocabulary: `new windows open the updated app; work already running keeps its current engine until all its windows and work close`. A file replaced on disk does not force a running engine to hand over, so the line never claims it did, and it no longer @@ -149,7 +150,7 @@ seams, so a fixture cannot make a request or replace a file, and each says at th top of the transcript that it is a fixture. `scripts/update-drive.sh` captures them at 80 and 120 columns with the real binary, and captures the REAL `/settings` page beside them (`settings-auto-on`, `settings-auto-off`): those two open the -settings place through `/settings`, walk the tabs to Display and the cursor onto +settings place through `/settings`, walk the tabs to Workspace and the cursor onto the row with the panel's own arrow keys, and the capture is only kept once the row's OWN hint is the sentence on screen — so what is drawn is the registry's row reading the profile's value, not whatever a fuzzy search happened to put @@ -164,3 +165,16 @@ clear every updater seam so a capture machine needs no network and cannot instal anything. They are not evidence that a background install preserves a real turn; that continuity is REQUIRED VALIDATION to be recorded from the E2E suite before this is treated as proved — a fixture shows the surface, never the continuity. + +## Maintainer note: shared storage is part of the wire contract + +The files this feature shares between windows and between builds — +`update-state.json`, the `.install.lock` and the install record it +carries — are read across codeaf versions, and the state root may live on a +machine a surface only reaches over the engine wire. A change that BREAKS their +storage contract (renaming or retyping a field, or changing what an existing one +means) MUST bump the engine wire compatibility version (`remote.Version`): the +door refuses a mismatch, and that refusal is the only honest answer when a newer +surface would misread an older engine's storage. Joining an older engine is safe +only WITHIN that compatibility contract — adding an optional field or reading +one the older build already wrote is fine; redefining what is there is not. diff --git a/internal/config/settings.go b/internal/config/settings.go index 2270c7d629..0fa12a6b9c 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -2560,6 +2560,23 @@ func (s *Settings) build() []Setting { read: func() string { return strconv.Itoa(TenureAfterAt(dir)) }, write: func(raw string) error { return writeTenure(dir, raw) }, }, + // THE ROW READS THE SAME WAY UP AS ITS KEY, unlike the hints row above: + // `update.auto` on means codeaf keeps itself current, and `auto update` on + // is that sentence. It is the second row about what this machine does on + // its own, so it stands beside `background checks` rather than among the + // rows about how the surface draws itself. The offer's own "stop asking" + // writes through [SaveUpdateAuto], which is this row's writer by another + // door. + Setting{ + Key: KeyUpdateAuto, Category: CategoryPractice, Kind: SettingBool, + Label: "auto update", + Hint: "keeps this codeaf installation current in the background. " + + "The offer waits in the keys line for about ten seconds; it never blocks a keystroke. " + + "Off, installation is manual — nothing is installed until you run /update — and codeaf " + + "still checks once and names a new release (CODEAF_NO_UPDATE_CHECK=1 makes no request at all).", + read: func() string { return formatBool(UpdateAutoAt(dir)) }, + write: func(raw string) error { return writeBool(dir, KeyUpdateAuto, raw) }, + }, ) // THE SECOND ROW THAT IS NOT ALWAYS BUILT, on the divider's law below and @@ -2669,20 +2686,6 @@ func (s *Settings) build() []Setting { return writeProfileValue(dir, KeyHints, !disabled) }, }, - // THE ROW READS THE SAME WAY UP AS ITS KEY, unlike the hints row above: - // `update.auto` on means codeaf keeps itself current, and `auto update` on - // is that sentence. The offer's own "stop asking" writes through - // [SaveUpdateAuto], which is this row's writer by another door. - Setting{ - Key: KeyUpdateAuto, Category: CategoryInterface, Kind: SettingBool, - Label: "auto update", - Hint: "check for a new codeaf at launch and install it in the background. " + - "The offer waits in the keys line for about ten seconds; it never blocks a keystroke. " + - "Off, nothing is installed until you run /update; codeaf still checks once and names a new release " + - "(CODEAF_NO_UPDATE_CHECK=1 makes no request at all).", - read: func() string { return formatBool(UpdateAutoAt(dir)) }, - write: func(raw string) error { return writeBool(dir, KeyUpdateAuto, raw) }, - }, Setting{ Key: KeyAttributionModel, Category: CategoryInterface, Kind: SettingBool, Label: "model in commits", Env: "CODEAF_ATTRIBUTION_MODEL", diff --git a/internal/config/settings_test.go b/internal/config/settings_test.go index cf46aab9c0..97d5433adb 100644 --- a/internal/config/settings_test.go +++ b/internal/config/settings_test.go @@ -901,6 +901,62 @@ func mustRow(t *testing.T, rows *Settings, key string) Setting { // the timer's own definition on disk, so a person who removed the agent by hand // is told `off` in the one place they went to check — and turning the row is // what installs and removes it. +// THE AUTO UPDATE ROW IS READ NEXT TO BACKGROUND CHECKS, on the same +// machine-and-project reading and the same category: both answer what codeaf +// does HERE on its own, rather than how the surface draws itself. It used to be +// filed under the interface group and drawn on the Display tab, which is about +// appearance and typing; a person keeping an installation current looks where +// the machine's own work is kept. +func TestTheAutoUpdateRowReadsBesideBackgroundChecks(t *testing.T) { + dir := t.TempDir() + home := t.TempDir() + program := filepath.Join(t.TempDir(), "codeaf") + if err := os.WriteFile(program, []byte("#!/bin/sh\n"), 0o755); err != nil { + t.Fatalf("seed: %v", err) + } + timer, err := standing.NewWatch(standing.WatchOptions{ + Platform: "darwin", HomeDir: home, Executable: program, UID: 501, + Runner: quietRunner{}, + }) + if err != nil { + t.Fatalf("NewWatch: %v", err) + } + registry := NewSettings(SettingsOptions{ProfileDir: dir, BackgroundChecks: timer}) + updateAt, checksAt := -1, -1 + for at, row := range registry.Rows() { + switch row.Key { + case KeyUpdateAuto: + updateAt = at + if row.Category != CategoryPractice { + t.Fatalf("auto update is filed under %q, want %q", row.Category, CategoryPractice) + } + case KeyStandingBackground: + checksAt = at + } + } + if updateAt < 0 || checksAt < 0 { + t.Fatalf("both rows must be in the registry: auto update at %d, background checks at %d", updateAt, checksAt) + } + if updateAt+1 != checksAt { + t.Fatalf("auto update stands at %d and background checks at %d, want them adjacent", updateAt, checksAt) + } + // THE FIRST SENTENCE SAYS WHAT THE ROW DOES, and the rest says what off + // means: manual installation, while the one launch check still runs. + row, ok := registry.Row(KeyUpdateAuto) + if !ok { + t.Fatal("the registry has no auto update row") + } + for _, want := range []string{ + "keeps this codeaf installation current in the background.", + "Off, installation is manual", + "still checks once", + } { + if !strings.Contains(row.Hint, want) { + t.Fatalf("the hint does not say %q: %s", want, row.Hint) + } + } +} + func TestTheBackgroundChecksRowReadsTheTimerAndTurnsIt(t *testing.T) { dir := t.TempDir() home := t.TempDir() diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index b2d3895d91..f2130b0e46 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -10,7 +10,7 @@ down from about ten seconds. It never blocks a keystroke. `/update` while it is up installs immediately — that is the release the offer is about, not a rollback; the skip chord, or `/update skip`, answers that one release, it is never offered again, and a later release is a new question; `/update never` turns -`auto update` off, and so does the row in `/settings`. Turning it off does not +`auto update` off, and so does the `auto update` row on `/settings`' **Workspace** tab. Turning it off does not cancel an install already downloading, and the launch check still makes its one request and names what is out. A release whose automatic install failed three times stops being tried on its own. diff --git a/internal/tui3/settings.go b/internal/tui3/settings.go index 51b66cadf0..3243b30004 100644 --- a/internal/tui3/settings.go +++ b/internal/tui3/settings.go @@ -554,6 +554,14 @@ var settingUI = map[string]settingMeta{ everyWord(standing.Interval) + " with no window open. " + "Off checks only while one is.", }, + // THE UPDATE ROW READS THE WAY UP ITS KEY DOES, unlike the hints row above: + // on means codeaf keeps itself current. It sits on Workspace beside + // `background checks`, because both answer what this MACHINE does on its own + // rather than how the surface draws itself. + config.KeyUpdateAuto: { + tab: tabWorkspace, label: "auto update", widget: widgetToggle, + // NO `about`: the registry's own hint is the sentence, said once. + }, // THE SIGNATURE HAS NO ROW, only the model's name inside it: codeaf always // signs the commits and pull requests it writes, and what a person may // choose is whether the `Assisted-by` line says which model it was. @@ -622,13 +630,6 @@ var settingUI = map[string]settingMeta{ // NO `about` OF ITS OWN: the line under the row is the registry's hint, // the owner's own wording (2026-09-24), said once ([settingMetaFor]). }, - // THE UPDATE ROW READS THE WAY UP ITS KEY DOES, unlike the hints row above: - // on means codeaf keeps itself current. It sits on Display with the other - // choices about what this surface does on its own. - config.KeyUpdateAuto: { - tab: tabDisplay, label: "auto update", widget: widgetToggle, - // NO `about`: the registry's own hint is the sentence, said once. - }, config.KeySplitPct: { tab: tabDisplay, label: "chat width", widget: widgetText, about: "the chat pane's share of the frame while the task rail is open.", diff --git a/internal/tui3/settingunit_test.go b/internal/tui3/settingunit_test.go index dd24caea54..c6cad1e687 100644 --- a/internal/tui3/settingunit_test.go +++ b/internal/tui3/settingunit_test.go @@ -136,6 +136,26 @@ func TestTheSshRowsAreOnTheTabAboutReachingAnotherMachine(t *testing.T) { } } +// THE AUTO UPDATE ROW IS ON THE TAB ABOUT THIS MACHINE, NOT ABOUT THE SURFACE. +// +// It sat on Display with the appearance-and-typing choices, while the question +// it answers — keep this installation current — is the same kind of +// question `background checks` answers, and that row is on Workspace. The key, +// the label and the default are unchanged; only the tab it is read under moved. +func TestTheAutoUpdateRowIsOnTheTabAboutThisMachine(t *testing.T) { + meta, ok := settingUI[config.KeyUpdateAuto] + if !ok { + t.Fatal("the auto update row has no place on the panel at all") + } + if meta.tab != tabWorkspace { + t.Fatalf("auto update is drawn under %q\n drawn: %s tab · %s\n want: %s tab · %s", + meta.tab, meta.tab, meta.label, tabWorkspace, meta.label) + } + if meta.label != "auto update" { + t.Fatalf("the label moved to %q, want %q", meta.label, "auto update") + } +} + // THE TAB STRIP FOLLOWS THE CURSOR, AT EVERY WIDTH THIS SURFACE IS DRAWN AT. // // It was built from the first chip and cut on the right, so at eighty columns diff --git a/internal/update/auto.go b/internal/update/auto.go index 4dbde37cd7..fa3cc82f85 100644 --- a/internal/update/auto.go +++ b/internal/update/auto.go @@ -5,13 +5,16 @@ import ( "encoding/hex" "encoding/json" "errors" + "fmt" "io" "os" "path/filepath" "strings" + "sync" "time" "github.com/Agent-Field/codeaf/internal/config" + "github.com/Agent-Field/codeaf/internal/filelock" ) // The auto updater's durable memory: one small file in the profile holding the @@ -99,9 +102,110 @@ func AutoStatePath(profileDir string) string { return config.ProfilePath(profileDir, autoStateName) } +// ── ONE WRITER PER PROFILE ───────────────────────────────────────────────── +// +// The remembered dismissal and failure count are one file, and changing it is a +// read-modify-write: read the state, change one field, write it back. Two codeaf +// processes that interleave that way — a skip pressed in one window while +// another finishes an install — would let the later writer save a state it read +// before the other wrote, silently dropping a person's skip (or a failure +// count). Both halves are therefore taken under TWO locks at once: +// +// - an in-process mutex, because flock and LockFileEx coordinate per open file +// description and this package must also serialize two goroutines of ONE +// process; +// - the profile's OS advisory lock on a STABLE `.lock` +// beside the file, never unlinked, so the hold lives in the kernel's lock on +// one inode and a crashed writer releases it instead of leaving a file to be +// reclaimed by age. +// +// The wait is BOUNDED: a lock held by a process wedged on a slow mount cannot +// hang the surface that pressed skip. A failed lock or write is REPORTED by +// [DismissRelease] rather than dressed up as a saved answer; the automatic +// [AutoFailure] and [AutoSuccess] keep their signatures and simply never claim a +// write they did not make. + +// autoStateLockWait bounds how long a state write waits for another process to +// release the profile's lock, so a write never hangs a chat forever. +const autoStateLockWait = 2 * time.Second + +// autoStateMu serializes the read-modify-write inside one process; the file lock +// below does the same across processes. It is a plain mutex and not an attempt at +// the file lock from the same handle, because the file lock's contract is one +// hold per open file description and a second acquisition in the same process +// must queue rather than deadlock. +var autoStateMu sync.Mutex + +// lockAutoState takes the profile's cross-process lock for update-state.json, +// waiting at most [autoStateLockWait]. It reuses [filelock], the same +// cross-platform advisory lock every other codeaf file coordination uses, and +// the lock file is a stable path that is never unlinked — a fresh opener must +// find the same inode the holder locked, not create a second one. +func lockAutoState(statePath string) (*os.File, error) { + lockPath := statePath + ".lock" + file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600) + if err != nil { + return nil, fmt.Errorf("open update state lock: %w", err) + } + deadline := time.Now().Add(autoStateLockWait) + for { + err = filelock.Lock(file, true, true) + if err == nil { + return file, nil + } + if !filelock.IsBusy(err) { + _ = file.Close() + return nil, fmt.Errorf("lock update state: %w", err) + } + remaining := time.Until(deadline) + if remaining <= 0 { + _ = file.Close() + return nil, fmt.Errorf("lock update state: timed out after %s", autoStateLockWait) + } + pause := 10 * time.Millisecond + if remaining < pause { + pause = remaining + } + time.Sleep(pause) + } +} + +// mutateAutoState is the ONE read-modify-write of the coordinator's file, and +// every change to it goes through here under both locks. It returns the state it +// wrote, or the error that kept the write from landing — and a caller may not +// report a change the returned error denies. +func mutateAutoState(profileDir string, mutate func(*AutoState)) (AutoState, error) { + path := AutoStatePath(profileDir) + autoStateMu.Lock() + defer autoStateMu.Unlock() + // The lock file sits beside the state file, so the folder has to exist before + // the lock can be opened on a profile that has never written one. + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return AutoState{}, err + } + lock, err := lockAutoState(path) + if err != nil { + return AutoState{}, err + } + defer func() { _ = lock.Close() }() + state := loadAutoStateFile(path) + mutate(&state) + if err := saveAutoStateFile(path, state); err != nil { + return AutoState{}, err + } + return state, nil +} + // LoadAutoState reads the coordinator's file; a missing or corrupt one is empty. func LoadAutoState(profileDir string) AutoState { - raw, err := os.ReadFile(AutoStatePath(profileDir)) + return loadAutoStateFile(AutoStatePath(profileDir)) +} + +// loadAutoStateFile is the raw read. It is safe without the lock because a +// writer lands its whole file with one rename: a reader sees the version before +// or the version after, never a half-written one. +func loadAutoStateFile(path string) AutoState { + raw, err := os.ReadFile(path) if err != nil { return AutoState{} } @@ -112,9 +216,17 @@ func LoadAutoState(profileDir string) AutoState { return state } -// SaveAutoState writes the coordinator's file atomically. +// SaveAutoState writes the coordinator's file atomically. It is the raw write +// and does NOT take the profile lock: a caller with a read-modify-write to make +// goes through [mutateAutoState], which holds both locks across the read and the +// write. func SaveAutoState(profileDir string, state AutoState) error { - path := AutoStatePath(profileDir) + return saveAutoStateFile(AutoStatePath(profileDir), state) +} + +// saveAutoStateFile writes one state with a temporary file and a rename, so a +// reader or a crash never observes a partial file. +func saveAutoStateFile(path string, state AutoState) error { if strings.TrimSpace(path) == "" { return nil } @@ -154,44 +266,62 @@ func SaveAutoState(profileDir string, state AutoState) error { return nil } -// DismissRelease records "not now" for one exact release. +// DismissRelease records "not now" for one exact release. A lock or write that +// failed is returned, because the surface may not report a skip it did not save. func DismissRelease(profileDir, tag string) error { tag = strings.TrimSpace(tag) if tag == "" { return errors.New("cannot dismiss an unnamed release") } - state := LoadAutoState(profileDir) - state.DismissedRelease = tag - state.DismissedAt = time.Now() - return SaveAutoState(profileDir, state) + _, err := mutateAutoState(profileDir, func(state *AutoState) { + state.DismissedRelease = tag + state.DismissedAt = time.Now() + }) + return err } -// AutoFailure records one failed automatic install of a release. +// AutoFailure records one failed automatic install of a release. Its signature +// is unchanged — the caller only wants the note on the surface — but a write +// that did not land returns what the file really holds rather than a fabricated +// count, and a failure recorded by another window is not clobbered: the whole +// state is read and rewritten under the lock. func AutoFailure(profileDir, tag string, err error) AutoState { tag = strings.TrimSpace(tag) - state := LoadAutoState(profileDir) - if strings.TrimSpace(state.FailedRelease) != tag { - state.FailedRelease = tag - state.Failures = 0 - } - state.Failures++ - state.LastAttemptAt = time.Now() - state.LastError = "" - if err != nil { - state.LastError = err.Error() + state, saveErr := mutateAutoState(profileDir, func(state *AutoState) { + if strings.TrimSpace(state.FailedRelease) != tag { + state.FailedRelease = tag + state.Failures = 0 + } + state.Failures++ + state.LastAttemptAt = time.Now() + state.LastError = "" + if err != nil { + state.LastError = err.Error() + } + }) + if saveErr != nil { + return LoadAutoState(profileDir) } - _ = SaveAutoState(profileDir, state) return state } // AutoSuccess clears the failure belonging to a release now installed. +// +// IT DOES NOT CLEAR A DISMISSAL. The person's "not now" for another tag is a +// separate decision and survives — the read-modify-write reads the whole state +// and empties only the failure fields — and doing so under the profile lock is +// what keeps a concurrent skip from being erased by a success that read the file +// a moment too early. func AutoSuccess(profileDir string) AutoState { - state := LoadAutoState(profileDir) - state.FailedRelease = "" - state.Failures = 0 - state.LastAttemptAt = time.Time{} - state.LastError = "" - _ = SaveAutoState(profileDir, state) + state, saveErr := mutateAutoState(profileDir, func(state *AutoState) { + state.FailedRelease = "" + state.Failures = 0 + state.LastAttemptAt = time.Time{} + state.LastError = "" + }) + if saveErr != nil { + return LoadAutoState(profileDir) + } return state } diff --git a/internal/update/auto_test.go b/internal/update/auto_test.go index d5a772ad78..7c828b7ae4 100644 --- a/internal/update/auto_test.go +++ b/internal/update/auto_test.go @@ -471,3 +471,225 @@ func TestRecordReadsThroughTheHeldHandle(t *testing.T) { t.Fatalf("bytes past the read bound were trusted as %+v", record) } } + +// TestAutoSuccessKeepsASkipItFound proves the merge AutoSuccess performs is over +// the WHOLE state: a failure is cleared and a person's "not now" for another tag +// is left standing, which is the field a lost read-modify-write used to erase. +func TestAutoSuccessKeepsASkipItFound(t *testing.T) { + dir := t.TempDir() + if err := DismissRelease(dir, "v0.9.9"); err != nil { + t.Fatalf("dismiss: %v", err) + } + if state := AutoFailure(dir, "v0.9.8", errors.New("the release host is away")); state.Failures != 1 { + t.Fatalf("failure was not recorded: %+v", state) + } + state := AutoSuccess(dir) + if !state.Dismissed("v0.9.9") { + t.Fatalf("a success erased the saved skip: %+v", state) + } + if state.FailedRelease != "" || state.Failures != 0 || !state.LastAttemptAt.IsZero() || state.LastError != "" { + t.Fatalf("a success did not clear the failure: %+v", state) + } + // And it is the file, not only the returned value, that still holds the skip. + if !LoadAutoState(dir).Dismissed("v0.9.9") { + t.Fatal("the skip is not in the file after the success") + } +} + +// autoStateHelperEnv carries the answer a child process must record to the +// helper below; autoStateHelperMarker names a file the child touches once it is +// started, so the parent can tell "queued on the lock" from "still loading". +const ( + autoStateHelperEnv = "CODEAF_UPDATE_STATE_HELPER" + autoStateHelperMarker = "CODEAF_UPDATE_STATE_HELPER_MARKER" +) + +// TestConcurrentSkipAndFailureSurviveInterleaving is the cross-process half of +// the fix: one window (this process) holds the state lock in the middle of its +// skip while a second REAL process records a success or a failure. The child +// must WAIT, and once released it must merge its change with the skip rather +// than save a state it read before the skip landed. +// +// The child is this test binary run again with an environment switch, so nothing +// about the second process is simulated — it has its own address space and its +// own file descriptors, exactly as two terminals do. +func TestConcurrentSkipAndFailureSurviveInterleaving(t *testing.T) { + if os.Getenv(autoStateHelperEnv) != "" { + t.Skip("helper process") + } + for _, mode := range []string{"success", "failure"} { + t.Run(mode, func(t *testing.T) { + dir := t.TempDir() + statePath := AutoStatePath(dir) + marker := filepath.Join(t.TempDir(), "started") + + // W1's read-modify-write is in progress: it holds the profile lock. + held, err := lockAutoState(statePath) + if err != nil { + t.Fatalf("hold the state lock: %v", err) + } + released := false + defer func() { + if !released { + _ = held.Close() + } + }() + + child := exec.Command(os.Args[0], "-test.run=^TestAutoStateSecondProcessHelper$", "-test.v") + child.Env = append(os.Environ(), + autoStateHelperEnv+"="+mode+":"+dir, + autoStateHelperMarker+"="+marker) + if err := child.Start(); err != nil { + t.Fatalf("start the second process: %v", err) + } + done := make(chan error, 1) + go func() { done <- child.Wait() }() + + // WAIT FOR THE CHILD TO BE PAST ITS STARTUP and about to write, so the + // next check observes it queued on the lock rather than still loading. + waitForFile(t, marker, 5*time.Second) + select { + case err := <-done: + t.Fatalf("the second process finished (%v) while the state lock was held, "+ + "so its read-modify-write did not serialize", err) + case <-time.After(300 * time.Millisecond): + } + + // W1 completes its skip while W2 waits. + state := LoadAutoState(dir) + state.DismissedRelease = "v0.9.9" + state.DismissedAt = time.Now() + if err := SaveAutoState(dir, state); err != nil { + t.Fatalf("write the dismissal: %v", err) + } + _ = held.Close() + released = true + + select { + case err := <-done: + if err != nil { + t.Fatalf("the second process exited %v, want 0", err) + } + case <-time.After(10 * time.Second): + t.Fatal("the second process never finished after the lock was released") + } + final := LoadAutoState(dir) + if !final.Dismissed("v0.9.9") { + t.Fatalf("the skip did not survive the concurrent %s: %+v", mode, final) + } + switch mode { + case "success": + if final.FailedRelease != "" || final.Failures != 0 { + t.Fatalf("a concurrent success did not clear the failure: %+v", final) + } + case "failure": + if final.FailedRelease != "v0.9.8" || final.Failures != 1 { + t.Fatalf("the concurrent failure was erased by the skip: %+v", final) + } + } + }) + } +} + +// TestAutoStateSecondProcessHelper is the child half of the test above: it +// touches its marker, then records the answer the parent named while the parent +// holds the lock. It exits 0 on success so the parent reads a clean Wait. +func TestAutoStateSecondProcessHelper(t *testing.T) { + spec := strings.TrimSpace(os.Getenv(autoStateHelperEnv)) + if spec == "" { + t.Skip("not the helper") + } + mode, dir, ok := strings.Cut(spec, ":") + if !ok || dir == "" { + fmt.Fprintln(os.Stderr, "helper spec malformed") + os.Exit(1) + } + if marker := strings.TrimSpace(os.Getenv(autoStateHelperMarker)); marker != "" { + if err := os.WriteFile(marker, []byte("ready"), 0o600); err != nil { + fmt.Fprintf(os.Stderr, "helper marker: %v\n", err) + os.Exit(1) + } + } + switch mode { + case "success": + AutoSuccess(dir) + case "failure": + AutoFailure(dir, "v0.9.8", errors.New("the release host is away")) + default: + fmt.Fprintf(os.Stderr, "helper mode %q\n", mode) + os.Exit(1) + } + os.Exit(0) +} + +// TestStateWritesSerializeInsideOneProcess proves the second lock: two +// goroutines of ONE process cannot be inside the read-modify-write together, +// whatever the platform's file lock does with two handles on one description. +func TestStateWritesSerializeInsideOneProcess(t *testing.T) { + dir := t.TempDir() + autoStateMu.Lock() + done := make(chan struct{}) + go func() { + if err := DismissRelease(dir, "v0.9.9"); err != nil { + t.Errorf("dismiss: %v", err) + } + close(done) + }() + select { + case <-done: + autoStateMu.Unlock() + t.Fatal("a state write finished while another goroutine held the in-process lock") + case <-time.After(200 * time.Millisecond): + } + autoStateMu.Unlock() + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("the state write never finished after the in-process lock was released") + } + if !LoadAutoState(dir).Dismissed("v0.9.9") { + t.Fatal("the dismissal did not land") + } +} + +// TestStateLockIsAStableUnlinkedPath proves the lock is one inode beside the +// state file and stays there: unlinking it on release would let a fresh opener +// create a second file and hold a second lock. +func TestStateLockIsAStableUnlinkedPath(t *testing.T) { + dir := t.TempDir() + statePath := AutoStatePath(dir) + lockPath := statePath + ".lock" + first, err := lockAutoState(statePath) + if err != nil { + t.Fatalf("lock: %v", err) + } + if _, err := os.Stat(lockPath); err != nil { + t.Fatalf("the lock is not at the stable path %s: %v", lockPath, err) + } + if err := first.Close(); err != nil { + t.Fatalf("release: %v", err) + } + if _, err := os.Stat(lockPath); err != nil { + t.Fatalf("the lock file was removed on release: %v", err) + } + second, err := lockAutoState(statePath) + if err != nil { + t.Fatalf("relock the stable path: %v", err) + } + _ = second.Close() +} + +// waitForFile waits until path exists, failing the test if it never does. +func waitForFile(t *testing.T, path string, within time.Duration) { + t.Helper() + deadline := time.Now().Add(within) + for { + if _, err := os.Stat(path); err == nil { + return + } + if time.Now().After(deadline) { + t.Fatalf("the second process never signalled it had started: %s", path) + } + time.Sleep(10 * time.Millisecond) + } +} diff --git a/scripts/update-drive.sh b/scripts/update-drive.sh index 58b5129dd3..99447cc198 100755 --- a/scripts/update-drive.sh +++ b/scripts/update-drive.sh @@ -123,7 +123,7 @@ capture() { # capture_settings \u2014 the REAL /settings page with the real # `auto update` row, reached the way a person reaches it: the command, the -# arrow keys that walk the tabs to Display, and the down key that walks the +# arrow keys that walk the tabs to Workspace, and the down key that walks the # cursor onto the row. The row is confirmed selected by its OWN hint being on # screen, not by the label merely existing somewhere in the frame. capture_settings() { @@ -136,11 +136,11 @@ capture_settings() { tmux -L "$SOCK" send-keys -t "$session" -l "/settings" tmux -L "$SOCK" send-keys -t "$session" Enter sleep 2 - # THE DISPLAY TAB, BY THE PANEL'S OWN ARROW KEY: Session, Context, Workspace, - # Display. The panel's type-to-search is deliberately NOT used here — it ranks - # fuzzy matches from every tab, so the row under the cursor is usually the - # wrong one and the sentence drawn under it explains somebody else's setting. - tmux -L "$SOCK" send-keys -t "$session" Right + # THE WORKSPACE TAB, BY THE PANEL'S OWN ARROW KEY: Session, Context, + # Workspace. The panel's type-to-search is deliberately NOT used here — it + # ranks fuzzy matches from every tab, so the row under the cursor is usually + # the wrong one and the sentence drawn under it explains somebody else's + # setting. tmux -L "$SOCK" send-keys -t "$session" Right tmux -L "$SOCK" send-keys -t "$session" Right sleep 2 @@ -149,7 +149,7 @@ capture_settings() { # THE ROW'S OWN HINT IS THE PROOF THE CURSOR IS ON IT: the sheet draws the # hint only for the row under the cursor, and this row's hint opens with # these words. - if tmux -L "$SOCK" capture-pane -p -t "$session" | grep -q "check for a new codeaf at launch"; then + if tmux -L "$SOCK" capture-pane -p -t "$session" | grep -q "keeps this codeaf installation current in the background"; then landed=yes break fi @@ -166,7 +166,7 @@ capture_settings() { # THE ROW ITSELF IS THE CHECK, not the fact that something drew: the registry's # label, the sentence the registry wrote for it, and the value its reader # resolved. A capture without all three is a failure. - grep -q "check for a new codeaf at launch" "$file" || { + grep -q "keeps this codeaf installation current in the background" "$file" || { echo "the settings capture at ${cols}c did not draw the row's own hint:" >&2 cat "$file" >&2 exit 1 From 8db7e3be7590621f304b0620abdde74457f1a50d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 11:42:57 -0400 Subject: [PATCH 08/12] docs: show background updates and Workspace settings at two terminal widths --- .../unreleased/1789-background-updates.md | 4 +-- .../background-updates-frames/README.md | 19 +++++++++++ .../downloading-120c.ansi | 6 ++-- .../downloading-120c.png | Bin 0 -> 42764 bytes .../downloading-120c.txt | 6 ++-- .../downloading-80c.ansi | 2 +- .../downloading-80c.png | Bin 0 -> 26793 bytes .../downloading-80c.txt | 2 +- .../failure-120c.ansi | 6 ++-- .../failure-120c.png | Bin 0 -> 46623 bytes .../failure-120c.txt | 6 ++-- .../failure-80c.ansi | 2 +- .../background-updates-frames/failure-80c.png | Bin 0 -> 30708 bytes .../background-updates-frames/failure-80c.txt | 2 +- .../manual-120c.ansi | 6 ++-- .../background-updates-frames/manual-120c.png | Bin 0 -> 42396 bytes .../background-updates-frames/manual-120c.txt | 6 ++-- .../background-updates-frames/manual-80c.ansi | 2 +- .../background-updates-frames/manual-80c.png | Bin 0 -> 26342 bytes .../background-updates-frames/manual-80c.txt | 2 +- .../background-updates-frames/off-120c.ansi | 6 ++-- .../background-updates-frames/off-120c.png | Bin 0 -> 44084 bytes .../background-updates-frames/off-120c.txt | 6 ++-- .../background-updates-frames/off-80c.ansi | 2 +- .../background-updates-frames/off-80c.png | Bin 0 -> 28513 bytes .../background-updates-frames/off-80c.txt | 2 +- .../background-updates-frames/offer-120c.ansi | 6 ++-- .../background-updates-frames/offer-120c.png | Bin 0 -> 46760 bytes .../background-updates-frames/offer-120c.txt | 6 ++-- .../background-updates-frames/offer-80c.ansi | 2 +- .../background-updates-frames/offer-80c.png | Bin 0 -> 30573 bytes .../background-updates-frames/offer-80c.txt | 2 +- .../background-updates-frames/ready-120c.ansi | 6 ++-- .../background-updates-frames/ready-120c.png | Bin 0 -> 48470 bytes .../background-updates-frames/ready-120c.txt | 6 ++-- .../background-updates-frames/ready-80c.ansi | 2 +- .../background-updates-frames/ready-80c.png | Bin 0 -> 32878 bytes .../background-updates-frames/ready-80c.txt | 2 +- .../ready-working-120c.ansi | 6 ++-- .../ready-working-120c.png | Bin 0 -> 58472 bytes .../ready-working-120c.txt | 6 ++-- .../ready-working-80c.ansi | 2 +- .../ready-working-80c.png | Bin 0 -> 42268 bytes .../ready-working-80c.txt | 2 +- .../settings-auto-off-120c.ansi | 32 +++++++++--------- .../settings-auto-off-120c.png | Bin 0 -> 57727 bytes .../settings-auto-off-120c.txt | 30 ++++++++-------- .../settings-auto-off-80c.ansi | 32 +++++++++--------- .../settings-auto-off-80c.png | Bin 0 -> 52412 bytes .../settings-auto-off-80c.txt | 30 ++++++++-------- .../settings-auto-on-120c.ansi | 32 +++++++++--------- .../settings-auto-on-120c.png | Bin 0 -> 57653 bytes .../settings-auto-on-120c.txt | 30 ++++++++-------- .../settings-auto-on-80c.ansi | 32 +++++++++--------- .../settings-auto-on-80c.png | Bin 0 -> 52433 bytes .../settings-auto-on-80c.txt | 30 ++++++++-------- 56 files changed, 201 insertions(+), 182 deletions(-) create mode 100644 docs/design/background-updates-frames/README.md create mode 100644 docs/design/background-updates-frames/downloading-120c.png create mode 100644 docs/design/background-updates-frames/downloading-80c.png create mode 100644 docs/design/background-updates-frames/failure-120c.png create mode 100644 docs/design/background-updates-frames/failure-80c.png create mode 100644 docs/design/background-updates-frames/manual-120c.png create mode 100644 docs/design/background-updates-frames/manual-80c.png create mode 100644 docs/design/background-updates-frames/off-120c.png create mode 100644 docs/design/background-updates-frames/off-80c.png create mode 100644 docs/design/background-updates-frames/offer-120c.png create mode 100644 docs/design/background-updates-frames/offer-80c.png create mode 100644 docs/design/background-updates-frames/ready-120c.png create mode 100644 docs/design/background-updates-frames/ready-80c.png create mode 100644 docs/design/background-updates-frames/ready-working-120c.png create mode 100644 docs/design/background-updates-frames/ready-working-80c.png create mode 100644 docs/design/background-updates-frames/settings-auto-off-120c.png create mode 100644 docs/design/background-updates-frames/settings-auto-off-80c.png create mode 100644 docs/design/background-updates-frames/settings-auto-on-120c.png create mode 100644 docs/design/background-updates-frames/settings-auto-on-80c.png diff --git a/docs/changes/unreleased/1789-background-updates.md b/docs/changes/unreleased/1789-background-updates.md index 4a2c5afe7f..9e8270f7a0 100644 --- a/docs/changes/unreleased/1789-background-updates.md +++ b/docs/changes/unreleased/1789-background-updates.md @@ -53,8 +53,8 @@ is out; the build holding this session is the one `/status` reports (its `build` row), and the note the offer writes names both (`codeaf is out · you have `). An install only replaces the file: nothing about the running session changes, and the new build is what opens the next time codeaf is started -— so a window can be current and behind its channel at the same time, on -purpose. The automatic road moves forward only: a duplicate is a quiet no-op, an +— so a newly opened app can be up to date while its shared engine finishes work +on an older build. The automatic road moves forward only: a duplicate is a quiet no-op, an older build is refused unless a person names the tag, and two same-day dev or staging builds whose published moment is not known are refused rather than guessed at. With `update.auto` off the launch check still runs and says the same diff --git a/docs/design/background-updates-frames/README.md b/docs/design/background-updates-frames/README.md new file mode 100644 index 0000000000..4fd5f1a3bb --- /dev/null +++ b/docs/design/background-updates-frames/README.md @@ -0,0 +1,19 @@ +# Background update screens + +Captured from `3a99582adbc2a001193ec3db4cd58a132fe00830` with `make build` and `scripts/update-drive.sh` on Spark. Each state is shown at 80 and 120 columns, 40 rows. PNGs were rendered from the captured ANSI cells with agg and ffmpeg using JetBrainsMono Nerd Font Mono; no UI text was added to the images. + +The seven update states use deterministic, explicitly labelled render fixtures and synthetic release labels. They do not contact a provider, download an update or prove runtime continuity. The two settings states operate the actual settings page and saved setting in isolated profiles. Every capture uses an isolated home and private terminal server. + +| State | 80 columns | 120 columns | +| --- | --- | --- | +| Launch offer | [View](offer-80c.png) | [View](offer-120c.png) | +| Background download | [View](downloading-80c.png) | [View](downloading-120c.png) | +| Installed | [View](ready-80c.png) | [View](ready-120c.png) | +| Installed while work continues | [View](ready-working-80c.png) | [View](ready-working-120c.png) | +| Manual update | [View](manual-80c.png) | [View](manual-120c.png) | +| Automatic installation off | [View](off-80c.png) | [View](off-120c.png) | +| Failed install | [View](failure-80c.png) | [View](failure-120c.png) | +| Workspace setting on | [View](settings-auto-on-80c.png) | [View](settings-auto-on-120c.png) | +| Workspace setting off | [View](settings-auto-off-80c.png) | [View](settings-auto-off-120c.png) | + +The `.txt` and `.ansi` siblings preserve the terminal capture. Live model continuity is validated separately; screenshots alone do not establish that work survives installation. diff --git a/docs/design/background-updates-frames/downloading-120c.ansi b/docs/design/background-updates-frames/downloading-120c.ansi index 2dca83f1ff..04a80be715 100644 --- a/docs/design/background-updates-frames/downloading-120c.ansi +++ b/docs/design/background-updates-frames/downloading-120c.ansi @@ -1,11 +1,11 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am  +  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  │ Tasks 0 alt+l - · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/downloading/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/da0b5f30d42af71c/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task -  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/downloading/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/da0b5f30d42af71c/transcript.jsonl\project/da0b5f30d42af71c/transcript.jsonl]8;;\ │  + · resumed ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/downloading/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/40ac74e307af5a1c/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/downloading/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/40ac74e307af5a1c/transcript.jsonl\project/40ac74e307af5a1c/transcript.jsonl]8;;\ │  · esc interrupts · ctrl+c quits · ? for help │ + /standing · updating codeaf in the background · this session keeps running │  · fixture · codeaf update states · CODEAF_UPDATE_DEMO=downloading · no network, │  diff --git a/docs/design/background-updates-frames/downloading-120c.png b/docs/design/background-updates-frames/downloading-120c.png new file mode 100644 index 0000000000000000000000000000000000000000..08ea3acee112ef567d4026afd88cc942f80d22fe GIT binary patch literal 42764 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● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ Tasks 0 alt+l - · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task - project/da0b5f30d42af71c/transcript.jsonl │ + · resumed ~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work- │ + /task + project/40ac74e307af5a1c/transcript.jsonl │ · esc interrupts · ctrl+c quits · ? for help │ + /standing · updating codeaf in the background · this session keeps running │ · fixture · codeaf update states · CODEAF_UPDATE_DEMO=downloading · no network, │ diff --git a/docs/design/background-updates-frames/downloading-80c.ansi b/docs/design/background-updates-frames/downloading-80c.ansi index 80ccf4d713..fe45eda96d 100644 --- a/docs/design/background-updates-frames/downloading-80c.ansi +++ b/docs/design/background-updates-frames/downloading-80c.ansi @@ -1,4 +1,4 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am  +  ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/downloading-80c.png b/docs/design/background-updates-frames/downloading-80c.png new file mode 100644 index 0000000000000000000000000000000000000000..e529bb93854056c4621996bc9cd152a3254abd8d GIT binary patch literal 26793 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 SaU%F8sVjEQKh%B?fASx(jM=pS literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/downloading-80c.txt b/docs/design/background-updates-frames/downloading-80c.txt index 224bd90dfe..6c5aaf7275 100644 --- a/docs/design/background-updates-frames/downloading-80c.txt +++ b/docs/design/background-updates-frames/downloading-80c.txt @@ -1,4 +1,4 @@ - >● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am + ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/failure-120c.ansi b/docs/design/background-updates-frames/failure-120c.ansi index 23054a41c8..662ca6d16a 100644 --- a/docs/design/background-updates-frames/failure-120c.ansi +++ b/docs/design/background-updates-frames/failure-120c.ansi @@ -1,11 +1,11 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am  +  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  │ Tasks 0 alt+l - · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/failure/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/29cd71f28969ce31/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task -  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/failure/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/29cd71f28969ce31/transcript.jsonl\project/29cd71f28969ce31/transcript.jsonl]8;;\ │  + · resumed ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/failure/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/df48843f632fbbf6/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/failure/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/df48843f632fbbf6/transcript.jsonl\project/df48843f632fbbf6/transcript.jsonl]8;;\ │  · esc interrupts · ctrl+c quits · ? for help │ + /standing · codeaf v0.9.3 could not be installed · this version keeps running · the checksum did│   not match · /update retries │  diff --git a/docs/design/background-updates-frames/failure-120c.png b/docs/design/background-updates-frames/failure-120c.png new file mode 100644 index 0000000000000000000000000000000000000000..b900840a2e88be3c75a46e78c796d15976dd490b GIT binary patch literal 46623 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%0AcucFi*kjmlA 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 literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/failure-120c.txt b/docs/design/background-updates-frames/failure-120c.txt index 7c11584e73..a65569c143 100644 --- a/docs/design/background-updates-frames/failure-120c.txt +++ b/docs/design/background-updates-frames/failure-120c.txt @@ -1,11 +1,11 @@ - >● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ Tasks 0 alt+l - · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task - project/29cd71f28969ce31/transcript.jsonl │ + · resumed ~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work- │ + /task + project/df48843f632fbbf6/transcript.jsonl │ · esc interrupts · ctrl+c quits · ? for help │ + /standing · codeaf v0.9.3 could not be installed · this version keeps running · the checksum did│ not match · /update retries │ diff --git a/docs/design/background-updates-frames/failure-80c.ansi b/docs/design/background-updates-frames/failure-80c.ansi index dc3b647fe3..7bc5084108 100644 --- a/docs/design/background-updates-frames/failure-80c.ansi +++ b/docs/design/background-updates-frames/failure-80c.ansi @@ -1,4 +1,4 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am  +  ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/failure-80c.png b/docs/design/background-updates-frames/failure-80c.png new file mode 100644 index 0000000000000000000000000000000000000000..df440c0211e160865b0b5b4878b30bc4ce863677 GIT binary patch literal 30708 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%0DI|4^?9fo=NJUw={)`IjlVFzDt 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● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am + ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/manual-120c.ansi b/docs/design/background-updates-frames/manual-120c.ansi index c14be08f86..021ec0a7a9 100644 --- a/docs/design/background-updates-frames/manual-120c.ansi +++ b/docs/design/background-updates-frames/manual-120c.ansi @@ -1,11 +1,11 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am  +  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  │ Tasks 0 alt+l - · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/manual/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/8fb8092a9bb3f78b/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task -  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/manual/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/8fb8092a9bb3f78b/transcript.jsonl\project/8fb8092a9bb3f78b/transcript.jsonl]8;;\ │  + · resumed ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/manual/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/c06a7baf9b01e2ea/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/manual/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/c06a7baf9b01e2ea/transcript.jsonl\project/c06a7baf9b01e2ea/transcript.jsonl]8;;\ │  · esc interrupts · ctrl+c quits · ? for help │ + /standing · updating codeaf in the background · this session keeps running │  · fixture · codeaf update states · CODEAF_UPDATE_DEMO=manual · no network, nothing │  diff --git a/docs/design/background-updates-frames/manual-120c.png b/docs/design/background-updates-frames/manual-120c.png new file mode 100644 index 0000000000000000000000000000000000000000..4d40e31694bb89d59e7f27d4fab9d8f6032b7bdb GIT binary patch literal 42396 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● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ Tasks 0 alt+l - · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task - project/8fb8092a9bb3f78b/transcript.jsonl │ + · resumed ~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work- │ + /task + project/c06a7baf9b01e2ea/transcript.jsonl │ · esc interrupts · ctrl+c quits · ? for help │ + /standing · updating codeaf in the background · this session keeps running │ · fixture · codeaf update states · CODEAF_UPDATE_DEMO=manual · no network, nothing │ diff --git a/docs/design/background-updates-frames/manual-80c.ansi b/docs/design/background-updates-frames/manual-80c.ansi index 334d6e08cf..1d75ebcb90 100644 --- a/docs/design/background-updates-frames/manual-80c.ansi +++ b/docs/design/background-updates-frames/manual-80c.ansi @@ -1,4 +1,4 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am  +  ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/manual-80c.png b/docs/design/background-updates-frames/manual-80c.png new file mode 100644 index 0000000000000000000000000000000000000000..b68aef3484fabee506e07f3f807e3df282b653c8 GIT binary patch literal 26342 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 UXZ!m$msGmqaP`OP9}xHd6Sv^pVE_OC literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/manual-80c.txt b/docs/design/background-updates-frames/manual-80c.txt index 53a5282757..f059077bc4 100644 --- a/docs/design/background-updates-frames/manual-80c.txt +++ b/docs/design/background-updates-frames/manual-80c.txt @@ -1,4 +1,4 @@ - >● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am + ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/off-120c.ansi b/docs/design/background-updates-frames/off-120c.ansi index 0b5c9c49f2..61c421ac8e 100644 --- a/docs/design/background-updates-frames/off-120c.ansi +++ b/docs/design/background-updates-frames/off-120c.ansi @@ -1,11 +1,11 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am  +  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  │ Tasks 0 alt+l - · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/off/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/2092e8222d70aa5b/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task -  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/off/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/2092e8222d70aa5b/transcript.jsonl\project/2092e8222d70aa5b/transcript.jsonl]8;;\ │  + · resumed ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/off/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/eca78e9f47048bf6/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/off/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/eca78e9f47048bf6/transcript.jsonl\project/eca78e9f47048bf6/transcript.jsonl]8;;\ │  · esc interrupts · ctrl+c quits · ? for help │ + /standing · codeaf v0.9.3 is out · you have v0.9.2 · /update installs it for the next launch │  · fixture · codeaf update states · CODEAF_UPDATE_DEMO=off · no network, nothing │  diff --git a/docs/design/background-updates-frames/off-120c.png b/docs/design/background-updates-frames/off-120c.png new file mode 100644 index 0000000000000000000000000000000000000000..1893f3d24a8b14563367e2d9e6cb5e60def72569 GIT binary patch literal 44084 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 literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/off-120c.txt b/docs/design/background-updates-frames/off-120c.txt index b6eaa2b2a0..156b505e54 100644 --- a/docs/design/background-updates-frames/off-120c.txt +++ b/docs/design/background-updates-frames/off-120c.txt @@ -1,11 +1,11 @@ - >● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ Tasks 0 alt+l - · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task - project/2092e8222d70aa5b/transcript.jsonl │ + · resumed ~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work- │ + /task + project/eca78e9f47048bf6/transcript.jsonl │ · esc interrupts · ctrl+c quits · ? for help │ + /standing · codeaf v0.9.3 is out · you have v0.9.2 · /update installs it for the next launch │ · fixture · codeaf update states · CODEAF_UPDATE_DEMO=off · no network, nothing │ diff --git a/docs/design/background-updates-frames/off-80c.ansi b/docs/design/background-updates-frames/off-80c.ansi index 0e0b880440..423c342c90 100644 --- a/docs/design/background-updates-frames/off-80c.ansi +++ b/docs/design/background-updates-frames/off-80c.ansi @@ -1,4 +1,4 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am  +  ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/off-80c.png b/docs/design/background-updates-frames/off-80c.png new file mode 100644 index 0000000000000000000000000000000000000000..8fe20e6fd2034d4252722daa06745c30fad63ab6 GIT binary patch literal 28513 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● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am + ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/offer-120c.ansi b/docs/design/background-updates-frames/offer-120c.ansi index 3ccbf12780..de697c512a 100644 --- a/docs/design/background-updates-frames/offer-120c.ansi +++ b/docs/design/background-updates-frames/offer-120c.ansi @@ -1,11 +1,11 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:35am  +  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  │ Tasks 0 alt+l - · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/offer/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/fc39dcd5baf27c1b/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task -  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/offer/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/fc39dcd5baf27c1b/transcript.jsonl\project/fc39dcd5baf27c1b/transcript.jsonl]8;;\ │  + · resumed ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/offer/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/1b1b35b0e38fb714/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/offer/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/1b1b35b0e38fb714/transcript.jsonl\project/1b1b35b0e38fb714/transcript.jsonl]8;;\ │  · esc interrupts · ctrl+c quits · ? for help │ + /standing · codeaf v0.9.3 is out · you have v0.9.2 · it installs on its own shortly · alt+n │   skips · /update installs it │  diff --git a/docs/design/background-updates-frames/offer-120c.png b/docs/design/background-updates-frames/offer-120c.png new file mode 100644 index 0000000000000000000000000000000000000000..fb6edc31096a449f2599847ac6721c7008b25a9b GIT binary patch literal 46760 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 RA6xO=Pka4RcPb?1{{X6LCbR$m literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/offer-120c.txt b/docs/design/background-updates-frames/offer-120c.txt index 787caacc41..70e9ee7c84 100644 --- a/docs/design/background-updates-frames/offer-120c.txt +++ b/docs/design/background-updates-frames/offer-120c.txt @@ -1,11 +1,11 @@ - >● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:35am + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ Tasks 0 alt+l - · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task - project/fc39dcd5baf27c1b/transcript.jsonl │ + · resumed ~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work- │ + /task + project/1b1b35b0e38fb714/transcript.jsonl │ · esc interrupts · ctrl+c quits · ? for help │ + /standing · codeaf v0.9.3 is out · you have v0.9.2 · it installs on its own shortly · alt+n │ skips · /update installs it │ diff --git a/docs/design/background-updates-frames/offer-80c.ansi b/docs/design/background-updates-frames/offer-80c.ansi index 12b2ab6ebf..e3f5a1aa04 100644 --- a/docs/design/background-updates-frames/offer-80c.ansi +++ b/docs/design/background-updates-frames/offer-80c.ansi @@ -1,4 +1,4 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:35am  +  ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/offer-80c.png b/docs/design/background-updates-frames/offer-80c.png new file mode 100644 index 0000000000000000000000000000000000000000..afc71ad9a8537c3298be129706a4fc5bc20d872c GIT binary patch literal 30573 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�gEvZaDwgvtzvh=|T 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 Yvt{b2dk7-{h*Y?E*7;1`>44<_0u6Ofx&QzG literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/offer-80c.txt b/docs/design/background-updates-frames/offer-80c.txt index 6aac8e7f94..62cc864550 100644 --- a/docs/design/background-updates-frames/offer-80c.txt +++ b/docs/design/background-updates-frames/offer-80c.txt @@ -1,4 +1,4 @@ - >● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:35am + ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/ready-120c.ansi b/docs/design/background-updates-frames/ready-120c.ansi index cda0b4def2..e6a1bc3086 100644 --- a/docs/design/background-updates-frames/ready-120c.ansi +++ b/docs/design/background-updates-frames/ready-120c.ansi @@ -1,11 +1,11 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am  +  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  │ Tasks 0 alt+l - · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/ready/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/3aff42a745e45c6d/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task -  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/ready/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/3aff42a745e45c6d/transcript.jsonl\project/3aff42a745e45c6d/transcript.jsonl]8;;\ │  + · resumed ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/ready/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/d4f5d3f94e3f7ea2/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/ready/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/d4f5d3f94e3f7ea2/transcript.jsonl\project/d4f5d3f94e3f7ea2/transcript.jsonl]8;;\ │  · esc interrupts · ctrl+c quits · ? for help │ + /standing · codeaf v0.9.3 installed · this session keeps running · new windows open the updated │   app; work already running keeps its current engine until all its windows and work │  diff --git a/docs/design/background-updates-frames/ready-120c.png b/docs/design/background-updates-frames/ready-120c.png new file mode 100644 index 0000000000000000000000000000000000000000..7c6cdd259cf63a01f54c84d91db2a4d60aea35b8 GIT binary patch literal 48470 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 literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/ready-120c.txt b/docs/design/background-updates-frames/ready-120c.txt index d6f835b2db..86902a227e 100644 --- a/docs/design/background-updates-frames/ready-120c.txt +++ b/docs/design/background-updates-frames/ready-120c.txt @@ -1,11 +1,11 @@ - >● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ Tasks 0 alt+l - · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task - project/3aff42a745e45c6d/transcript.jsonl │ + · resumed ~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work- │ + /task + project/d4f5d3f94e3f7ea2/transcript.jsonl │ · esc interrupts · ctrl+c quits · ? for help │ + /standing · codeaf v0.9.3 installed · this session keeps running · new windows open the updated │ app; work already running keeps its current engine until all its windows and work │ diff --git a/docs/design/background-updates-frames/ready-80c.ansi b/docs/design/background-updates-frames/ready-80c.ansi index fd760712a7..a612a0b45a 100644 --- a/docs/design/background-updates-frames/ready-80c.ansi +++ b/docs/design/background-updates-frames/ready-80c.ansi @@ -1,4 +1,4 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am  +  ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/ready-80c.png b/docs/design/background-updates-frames/ready-80c.png new file mode 100644 index 0000000000000000000000000000000000000000..6cf94ba818f1f265f91633fd626c28f06d348a7e GIT binary patch literal 32878 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%0AJ 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 KjuVc0#r{8e+LuTG literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/ready-80c.txt b/docs/design/background-updates-frames/ready-80c.txt index b8df0e34ba..f29b12afe0 100644 --- a/docs/design/background-updates-frames/ready-80c.txt +++ b/docs/design/background-updates-frames/ready-80c.txt @@ -1,4 +1,4 @@ - >● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am + ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/ready-working-120c.ansi b/docs/design/background-updates-frames/ready-working-120c.ansi index f3545e48c3..a97d290759 100644 --- a/docs/design/background-updates-frames/ready-working-120c.ansi +++ b/docs/design/background-updates-frames/ready-working-120c.ansi @@ -1,11 +1,11 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am   ◐ tighten the retry loop in the sync client ×  + ▦ All  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  │ Tasks 0 alt+l - · resumed ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/ready-working/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/8c7285c6057fd684/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-]8;;\ │ + /task -  ]8;;file:///tmp/codeaf-update-drive.BPWtpZ/home/ready-working/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work-project/8c7285c6057fd684/transcript.jsonl\project/8c7285c6057fd684/transcript.jsonl]8;;\ │  + · resumed ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/ready-working/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/da72b3aea7f2b4f4/transcript.jsonl\~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-]8;;\ │ + /task +  ]8;;file:///tmp/codeaf-update-drive.YtHivL/home/ready-working/v3/projects/-tmp-codeaf-update-drive.YtHivL-work-project/da72b3aea7f2b4f4/transcript.jsonl\project/da72b3aea7f2b4f4/transcript.jsonl]8;;\ │  · esc interrupts · ctrl+c quits · ? for help │ + /standing │  › tighten the retry loop in the sync client │  diff --git a/docs/design/background-updates-frames/ready-working-120c.png b/docs/design/background-updates-frames/ready-working-120c.png new file mode 100644 index 0000000000000000000000000000000000000000..3cc57bf1050dd73e1e479c5bcd503236c6ab7a9c GIT binary patch literal 58472 zcmeFZcT|&Gv@fb#q)11%NEfA8=pao3DhdW7ASy@)0U<&_N@yXVfS@AarUro^N>OP_ zPauS%AT`oUfQS%U=m`V}<$ZDQea?CBoj1lC_l|MzKbOH6kp8W?=9+8%%E}8f6GQG} z!p9CBI>ZgWdENZbp`$W~4zX~svjX=JNrR%mACAX2t^9z$%%8)DqJjm18+?br*RS3U z%3K_0^od#`R~UPw$FE?aDXwkUVXRfE95IpkUzw{-nJ33cqNnB+c1>lIXJ-&>!%Id@SY$DFl^7An?9=SPOp}M8Y}j%YQQ`v$AGBt^cW`4RratuyD)$gw4_mr}pU^hnYu3mv z4%^#L-*MQVsDd$?h7^Z}8>h4A%SB%#R7c&Y8g9g626lDy%27- zy7F$w_3SBaluNySw}bT!5owg1mIPusRSInFRnD5k5@}gt`%wY8>kHprCwG7T;xcMW z*IRwOG5oZdB8r@G!QE1<+gkrh5Yr6GMbAtcS};aKe>koWmn+(qo;EBnp-)iRC`!JG z`Y}B6b#Hw`f8qCB`W;n-jk6D^;l=vxH@w3|BHJ?PX>r7DB~)^Yn-XRVo+btE4Jl_y ziVERBGy$ilE{tt+9&c_*gu40AaYTy!>sbe6F?Pz?#<~DQEh!$z@V1}lQn38?wsAV8 zF#A*3Mxnz!5@RB}nl+i0th3vnl$GN$NAyc4&S z9i(ABYhnZsWVb?h#%33`RV#2p{bh3b-pesLwUos7n|IAx7X84_lNL>vuz9@OoT!Lo*Max(cSz$ z`Ci-pg^xDU)UNc@b??EwlAIFEY(%eL=Q~Q^^7>E}<$K`ASpFjkg?cQOLEAF z-u78OrHaaQ+f&ezNJG~eF9brf;`SQ0Gv+mU&YU(OyofC!0*h}LNb!Z@>Iv%(9dyuo zx^^e=3$+E#i93=Zd4`ueH|A%aaav)aW$8e35{C$J#?+lYMs+LHm-KjIy@+=Y#{X1Lg=A>&jm5$hJ7lx9)#0PH%Oe)N$MKIRH?(&%)yBU#7ku&>)(5h4|J4fmLi^dk+$7&S^S{_5j<9ItlTmmZ9TUV z!guK&hsimzgKWvl{s`e{c$z{s=Po;^bB*6F@DD8l{rF7u4UGcnA7GI{weP+3Z9S?7 zYmyo(`>W9z@Y%C=mif}JJu_0btB$4@o`9G~d#c8r7dC;#skftEr@;!8zg2`rLo=HD z&abdv*y3_+txz*cM?dx&e;R8o%~t5psgmL`XX)jkNXOtD$6?~+%H=jaxh*V*Q3IVLk}hd1tS&6{0L+_Bri zeZO=iL@t*h*&XxuoIhnK3J4h3%=OO;E_}WiVGtr_j|+jrLax=P+3Py6xkUkq>Bg?X zG0+=4M88|N3OwFY;pPyMH>iPn^;+5gS)6nq>*$geJ(fl6)v)d#jU^3#Jx()ARI0|9 zP}&!E*3bCo7MY-R`m9SJ%@tAKJ&!k@W1Nmo(t~epy@fwaMnHNSayTEUzO9>`D7MOR z8S^Xi$JnC$CQV9PmJ%k0oz$ex9D*DX+Lr)vp`xMlRwoZjf<)f*31}NMOI>SHLrJl} zbo|Rel4Bi(P(---CqmQH(U4*}6)!fJ}uG2icDPLGoAGzTq zHu}eU@MQ$@;P_(Ck+q&yG~Q=yQK#;tT-fe<+QR%9lnZto;hiZFpB>~Ze@-W8uDY7{ zdBb19*M(G8*w1BahdLv)g70{3jm_5I!MEbqgnK25wlFuqA21GZJm>r1r>^~8 z-pCmnYd1|^lVe^+-A9q#%CVnF(j5s)zfO6l9xg4w{WRKyc?B0QMOy0Cs&lvgLN!jC zZHVael9z$!!BZ(L_ERD6V0vEzNzh&EI_+LLl&#XH332USvDkbQ>vY;@dEv#L$Fb4cpyFQfGs+Hs=(Hx2RqJ-@|u>^%G7zWFl^IvG*cN%5tq&280eEEMFsten0% zDm9kd*%~Tr{ZA=gvT?&kWF-1sZPR+#vr+4^J#V&5+ZcA>A}*DC`*jq;w4O_hwXn;? zd?MYCc2p_oZrRZYfAVtg_5N$joL@E2^*GKc9d?OIMr?|Ix?H#>j@vNF3<`sd20iIW zEv}_FjXII`Whp~_ME`FdEJ5?@4{El`Sd%dB#$`*i=Pns%G9d*oRui2T8RE!yqgdF= z=x2{>Ums?q1q&=2V8nHO>-KftA9EIt0AGXbn+8+E#>C^7M0Rvj-|MAh9NQ+Bh8FLv zHuF;2HC+XZC z@Ji{A)P?6?tU0eGn4IaHniDpw5Z)V5dGww%L%q6ACWSQ2ix>J7rl_x!e#dR9^5K=S zfP}UiLoje+?iE?J2!(5EX!~321_a$}D$#ZY^aK|*7)`*ld5_208g0?#!6j&#ra`%b zxf9DJlks2Bii;7K=+VSBBYYAwOEIK zkc@*4!`jYFf5O?z)lzOQD4`bMc%={Uy)!435G9oc!1?WhMPK{5gyVx8Y8Iq4#7w#hAXRV<9!Zn>bO zB0hMW8YelU-#+e;*LoEUn!Dq6@=u5Y7KjDjR4KjO+fp7S`YEi`NJ;DC>?0c`%p4Zz zw@;kH?*1CFT*&E&vbK$V%nJ_r!x!Hc#K!}aE9+=}jlAY0?@C9NkfeM`sg73@1s~_K zgbzE1raBGR21wXmG>!Ho`8G{@~RFaOaNRBG~=E%ZX*keS!D#xK#n1@yT$ zQrB*!Ch;7FMQ44CI|<H}<<}FJp|A!u@Vy#!2 ze^g|e`Ml>L&wxk9V#>EeSV~y1Yr^!D=}EONXt1w(pF!|(nO>W+;KDEL5#H&JiHu_v zM-$ICwb9Xf!$L;l+WpALAK;mt)r{P$iMqQo!dLBh@shhaYkVCASFgIKt8ewf&$z!^ z|J-?hznBk_3Oq%_(*yBbzvxI`9`9eL%!>?#pPRmsakKlV-@oLK65q(*e*7}A?46Zg2@(&(bl7u27iLg7uke0P?Aj$WE&NfP5#qTxYQsjZn)eQOs0&h=vc&H7j*vc+r zylDv@zmV=t(pFuGA-O!$;Vc36hHTnu27B(Pcvup%;&=#&hpFD(GbvcnmY}Dr7lftt zTPJ;$n!lc0)ohWL;EYqm&c;-7OSaa&A!uUBVj? zu^r7L!*N#jXyQ`?-Aw=iiI<4!*KVib8MI_B7WJ`mWwA422eL(Agz)1v3BjE%DD-)r z6AO14P4_j#m{57&BIxG23D7Ouy6(CM znt^g|b2(#qjEmJ?DV-XD)?v@fFDeNA%LPp08r(jF&wC)dk4n#?ZVas?l3N3OLIob* zX5}}kI%=Oc8wVwS!+r%NocEh)!@d%N@Z7`yh0yH=I>fNFFhvz)h=<@NC;2sN(%)U# z$2pu~0*;;d>#%&+X~jP@BOuX2s$zt*lPi4$T#c8tuaNj^)7}M+-cvapuh)RG6LPk! zvjL<$za$i{S4X*03O&;3I5zZ`Y;tw+5#BsW(cDn>^Ya5Y(^rKezWZB`cKH7>t(!FF zok$kArXqjUE2^H0RFW2qt7DhH`Z}cJXp-~?M_Ww|MYJ1EQQ$C_;4ILQ^>pLa{E5O6 zbq4y;r(;^U1-C-0mRis&KUHEoXOD;q&XF3oJ*CzJ$7 z$DM+hE2u@BFRgj17gzLa0>M1c&N2!8#bR8@~&tO+$Y+5 z-`e8#iu(_P=cdhJojFhZmuAJ2de*$}O<5ATxL1zJ_$<&WGnPft zS(6;ywVt|+l_#zy zCR*}Q*WS&nzH8*4;gW?rqJ$(Zrqc+07%KA|3AyrCP5Lk@tTW znnd%cfT&m>`IbFyjyiUUj6Q(~kl*56vGpGP*M|~L7Y8r#Uek%tY*Nd2!&)sXgB4p! zVpeijV3ZT2*Pb$F6e)C6a+L7RYh@|(-_xe9~{&SA=rOi0soYhg;yN-GU3@e)$K#1o3y5M#>AceSTfvD9_$2qWhNG(_?;zx288V&S#EUuEAokl2w@8owvrYzQ8 z9Bd{n;OOCYxZM%&aoWVBrLGM=O`Az`bQic-1K0PlG=*12Z|i5|f(U6VVB;Bm|E<@F zuoXnOJ#uR@1~+SX%1q$c=dmSXphZlAs00WlXE$`C-D)UK{JR`u|DD3h7^d7HYvM!W z^nZH|&Q?0BKhF;Q^AcOqLzm*oJnvYcP=0J_c3e&L3Uzc^Qx5v1Z0V- zr(C)P5T^wX*GRB1Qtf9ojA*a%buRCk>0g3`dh?}le}@gjgYAxs1D{n~%9`bN#N)H_#V z^mZjSKI?`|e2%BhO?&4btXtMssoO>Id)KW!#Obo}h<*?ccCmzS=2?D{IM`Z2lwO>> z7qT>38G?p;qbZcD1oT5f?|n91f-jGlc0ZN}V>EQBn#-vAqUEAxYTG&*vA;MP=D~^e zOC?o%-Lheuh$Y|;EW>eLxDS^6g9p2_2P${0Ae_!&>q*)Dj9AyTvxi;R(g~sYa15p| zf8+JUkAD4jdS`n1#upG}6LR_CEK)RiF!F{3+q|})xa&v}uih3PDwpv#S3EWKqMoiY z%u}#WH6eT}CcC@BE{~cOAeY=Z|wj^0X|G_M3v?4gL64qM3@J!I5#9_*|+Ie<=q5jro3O#rUK$5OUlic+7 zHn7z$v`iiUnWYzf!c6`eDpX{v1fE&~_zAB^L3sdMVt6E;m87E|>Hd6%i~4@;6i8cN zElT!i63gK(S5>5%wee62s}%RoAVs@uYH9u8D4*9wlE0WyM4WEpbcM>f@e*6;xtpdb zGupXyScl!d9B?mO6CkQKJk%QcmfqebZiZx+p-z70v#5miwcT;^8g;<*8GxQI-Ml@6 zY#JPM4tPHGwnAKPNAu8z=h9dRPTc(Dmjia;ZJ!Sn;;G3OC!Kbt?qKcY@ZOpuZuLNi z>oqR?D^%yMQX4NvG6(U`NvUe%7ur3unxYi!gFhqMswYAUc|KT~?vq!7iInnfLrg&LN|?diS3_-~VX-gpxRD z@j#t0@V{goP7fqmaoz#;dtAEu8Dd16JBtboTTH81EH7axzpeX)r&BNF>X-dI%IiXZ zJL6P`GCNE!5^SnF7}XUP?%;9p;<8wd0J!3RfpEqP%Fumte_cs;e}mv|E0 z`?6U~OhQ)Z`~3{QH71w)qEF;MvZ(DS!b^!GL@0U8yNo7oXUBb8_yi1h?;ZcxNk}gr zq#$kr1S7sKFPZIj_)q}d6eZhRbPX|znsquVUACo(DHq4npeQjX1MAiuIlTfPLQfl> zDxKq|42G!P(iGoG{+0`62cNxi%OOGB9~yL9Wyu;;W8X4g;lmlF%cyc8M(gnSE-)kX zcw(BGtabPlZdT@*4QAuBOKpiQ#`mlvV{1p{f!&9T$mV%nYL=Ul7HvCdf{t>@#?#if z;iO8WKA0<5zL75PG%dw~05%m4{c-0+Ct_3#9aMwgN38s1Nx@5uBI@8FzxQ|f$P`W) zS#Gwh6JLJ>IiI?_ygMrdF*)Y^&S~T%L_JLeM0TH}TMboBe*Wk`zVn|0_2{k7trp-W zR%qPICa19`^$i}Di-BTbSFJ7dV*;TZ<=!&8vXHdATkW%whX5DI)Y4^mhB7eE_ys4g_ffxg7lrM_*QY)%mU zAjZ@w5;$nTDqV#KZz+MdZmxdExg-Fb@bU}D-t?t?vbVjP!@6*vDW{B3RhOC9vLc6g zs6OiZ@k(O82D5|dRk@FGEE~Cx2IlfeH0RFPwj#E5+N@EEe3E|EIPdDv1=b`jVoIUZ zlezL;tB5vmZ%hK;sjmm~lY@Z{=R9->R>|uGexd zHcmGg0aOLa_(<@nGOS1NR~;r1%Og`Tz}##cZ>saUKv`dR`k5>JJw_FP6PmsMz1T)SJHZ%_&v%l@ z*JDwVxK>ub@u-OM@*cYk$7-;nDX8oLapKX?a^!|js|O9`@Ep`fHV+8tPl!0s zT)-cHKk~SDh%SyZ!<*b3+3kc?>zUyg&EXD+mw{>j?(aY`J>AYN1HIn+Qx2M($K7#v zRP%k~as3r`(+6+-Z`#vjoWj(kg5%&)uO{~`sq8X-gDJAwhVMn7X6+#~ZdPM*kw@FQ zaaAEf+a#lkuNsj?APR3=y=A_r(jyB$KX=XIbK(#d35Pmtde|WBJ{oZYKY%x zJ*d(M=;0#Upc{@ot%uJv#J-Nj@^z&9XE#Eb<=}&}vVRT6PA;+_j6PBV)Pmm|ZVsVU z3qY6F%x34bzf_detLaINF;42o=2pt%H$!t)9V9j$@e)$PkFtxXy47VmC1Fh6fDAA%0cr$4?iw}%{0Bt%33B<_4aL7I){l8F{6C({Rzoqxc6r~kIirPWW3tDA;eeqxYH-JYT#fDh=EyEvLM-LTY#shwwO?r zELyHF!PdttjgkIhLb|3aP3+4+ljFHJWt1IqA8S{nS3UU_{WaiAR{4ZpB_Xsn{#oxz zBr_Z&j@Ys}N}TIeB_ZCLoc$mAi9i3+Zt>XdAc>mRN$|F$19_eC*W{?qZvN7qX&AL& zfGIZ@udK0nU+W?qY~xGDlgi|O%4IRT(Dus2Mk$=pTwX6wL*|@oM7h5Qb3wB-B2wn0 z`vw})eEHv8&st!Zy>u0L%0<6#L&KgU85G96!Qa4gc-a2S1vq%i#Y&2Yodvq>;!i1D zGDjN4K|${9hJ@qMT|LcD7yu{F&l;^G@a7EiF)vPr!Bd`WPK$nL(sSgMQ$ehKw$-hL z^ZDeeo`swlZvbR;L^vi=iGbWZID5Ita9O&olCyM8F&q#Ysls~wzjc4;S?hU66(vYK z`Z?D(o2t*H!34!7a5R5!t$Pn8KTa|lmTy{R@5-7~2z@`G-$eqjO6s?)Kv=Q5_sG3h z1o@D0X1^Ou9#kWjP@!eIlJtTz^4%zk1vt65rpRSKj#1rGPO z!ctpSnU0YJF-_a4Q{OpQCBmk!fW!_I9&Bp?<6sU*xS=k__c)gTpTLQD4D%i5wT!Hm zi3iizp#-tZ_wZprkEt-|Lm0`%~#r!+^``48)aN@*5!ag;vFnf1e7M{?~ zgp-Aeb#%6Y{H|K>H=DbKrT1121{^H)7$by*?caFzw{#KN;Ex42PN(75D|g_jSIur~ zlWYORU)rH!$0|NrUmAu4wpUKlP_NuF*3AZ47!fO|c<;B-q%L5};;019*^ffAYdqat zm7!mfF&77D;m8Et+j!HJ?=J!|GrSbp zc3HP7Z-2i~_6;gYG$d7POm=`_~#6U_)1Q(MxB1dGcJTw2U{QnB)1lJiSsTf$h? z$DJ*5HAS*NDq1;LJGXKAX{6yG_eOjUiS|fmZwv#c#yOVF-^t$Jy+tuoA=yHez^m;@ zH4Z0knM(f0V*=Oew`WoY9bb#5bP+mLQPcLtbqVbITJckrRZipotVwwc@KL6r>#CHd zD^1|9xYK=(0y^Y3Y*R`OA@}m1{>qV!FS#Yy=+dd0KUEH+{B@aRJK=0DA|!cwV+%7& z807VRc|~cNSIb{1eO;UhW)r1ge9ml1$}So7zJl#4Zn9UiQg4;3T8u9s3FcM4P2}?( z4KaP=T>>>FhXq;M&z+G!D}L&RciW6SFZ&wz-^<5b5qOKW>s}MGny2R+xBO}MA(7yj z-#psRI0@-Ct`l^#tHM!$t83=czU#S1+1oRtK*6v}?cz&ji(cKymE|%ne1c8{5*u?q zNRQEb@p$C#CVFE(;Unzw=ARoEy18Zi(z2U>cgu;nhhK?7^63#STLE0S8FW&YVLA@Y!8w4EY_r`DE=UH_(r8Ywd+Ph`w{J+Ysy!@PsPMrxOHb1cL3I(^@332 zj;Kjuzw5%*aZ}wpmjm~k`W+L|sg|Yo#c1u&x{$?&N#Vu^+Ff>m-XHY|oTYX}*)?K8 zHQhy&tnyvaf2*Y9qGO`CWt}14&C}ca4tsE9xB>e0xO#GiM}ajKFWt0gSqxSFYJ>0YNZ&wSf;5w+;TTzk~Ni%dihi8?BykX%j*Tdw|}55I0Qf=Y3Td^xCvZcf7id! z0*a`)#=ncf5vbwg|JJ=HKox`kLp?(e{m;Beq0zz z1GB9Bj*f0f>&&xKLU9eR6v&KepYx*C0Z9B=X~%`nbyCnv#SfI`H!5c-ex+3+ zR!Co7lf5U(3hnokOvNZ0+aLzdK(@EvWMyomVz``5)pE&1&{Wp~KvEj61pATlvj&(6 za8UaS?$`;pyAi9|q5+$*%R1qJ+E z)mAnY&e2yquvlF^t06qS`(G}2`}UJV&s|%6FhFJGRE+)7{%B-j@*RQde5UR!-|V2^ zJf+9+^5fOVrF($5pGmI*w)I|33wcy#00{TN<2ZrZ+v2M95o6^NnPPu2O%X^*K#xbO z@d!0N-X9qH1QcLb8rl20zWw)3goK>U=?bnE-If(BqQi)GO}wlqwOD+@s8LGK!iO09 zkav6*t!I<%F}mF6p@h*?zb`Fcc<-2uUr7N_FS;@sT}-*w0HS?6#Qo37KLxkO=r5eg z=Ka-dw+7~W^Ew6w3WpXZ^wbssgP0;$$c%DlYLWsj3NWVa@G~W4a_9l6;E_s(Om-56yzP zia_69BE664q?9@ot(0EVROwg`T+*%CBzNt*8ExaHSuz~3057_jGnI?TD&vv!?$u7cpcR<9SejVYem+vS71HLDQRq_Z zOR(___p=%8ZafDHUaRx z*_j78tyeW_sA=OTaGk*YYsg80_I1HM8VkC%emBv8y$SoxfA>>#FiR3l@~8vEj`xw4 z-;5`20_Ma;90uZ>W=$+b%Ec}@JIqX6&hyyje{AM(&Q!QUEda1l**j^$FDh~Om4~Ag zCI6s;QpW`26d5yoa0R~6h_*!SjtWII?h9c=JRiCF)z5>HJ-EcK7L+}|tzoZk-7VdE zZCUsGJ#2||t0ffl{jXYHqXpx6WqbdL8hZ==8I|$2BsXiy$wn^C)-O5{gF)PVAxfsb zvZ%}|yixlFfY+xuF{gm0lZ}}8wFM#END33Z)`memS2@)2(2DW%CAuT>t}IOAlw+bGAH{)o%p6l@~xTJlx4+1KfAqQ#QU&SG6IBsrEnHp3rY=S%?wI?&O6yP8 ztnZEWxMq+a3n!mRQn}U{Ii+Z?PyMA@UAMzSULJKocE|L!19C~~a%ATEh4GRxs>K~k zr59s<0FRvfT;kAu!NKanHA?}A39uyO&RtC{Twic3U5OloJ&W0noVGHh}7>JFH34BtPj!m*ur@ye~6(M@Y2Cxjk)WItz!Z58W)q z8+99E;1`99&e!Wb99ICp`ExeaYPJ#nFx26zeB{<&vi2FRpO-T!(CN^j6kF=FJv|LQ zD_tCKBZJDUuHq!;*UXWBX~9CH9{y?)TS#KeY_69JwX z@@!C1w9lY%TJN?SD56V(TjS_cx=vAXp|@vf-(%5YFnt!^RJ{bpvcfQKe#lJ!h-75j z@93DN`NlUV!o@D2I_-vBj{z)kx@LUMR<7Kyy&BUVIY3AYme)ot-eF~qkr|RMfS6R4 z2eL9SE%oZhF=yCZfp1`nWgFcVtdx!j^(m2QaFuwxJZ0O(+f*b-&wx}14my_IQoC=J zpgp(O1rvNjw#C{dsucp;#z#rO*@*!1r$E^KxzE6C&|Yf2KCggU^Y_EJ&1DnVRWtIzPg zckdmesm3GF=A@-xL)eFyRJrh9`f6dSo~7GSlVb+M6)|l7<7_Zk|Hn%Bd!y8I z0b16vabW8df0-r%nL$r6#tBP4q>Xq61^kPWK~iW;Rz zB5L-PgTL)B8#sc&0~F|Lsus$&k^%*bBsJz*nA6V8hctAQ$%Xg!AzPb1N1xg>G}R7! z_DVKSAw;jlSL|HnT*zs98bCFYRM?UV13>anXTS%>S~d41lJV^;A7}K<)zi;|b-$Sw z-sYX&2bh^Dn;8JVr#@}WO}#b*ssN~(mZ~|AvtW_P+2j9TOzi&5z5Z?ZJekVrkS1Cfqzjy$YX&$A zCg&{D0C?bCJU2{5gshI&y`?KT8y0G#i5qZ^VvjyQ<7r)>96 zPHoJ}N?v~nU8(c%fCFp_{KI2%d&&ZnZv`~CdMFxjPZ#OcB4^*%stF&0LkRHi!SFg={RBZ zzgL44rqfOX?ls?WXL-6R^_UE1g^1lC!JYT$dYgCm=m0SMa4-R!HG}tMg)M)}NR^lz zsS+d8`KH5yxu|)qKgA@7WvhF`1x4n8<2ctxyu&#i>Vp{@i*FQw!q=%W-M?aU>4c5U zawOZr+i@UQb0+~-0U(;%#LsBv+%{fOj9%(_zcBpej>g6(COJQT_SNL`sN3H4!FkK@ zmdE&+P@#o(9h4xbWA5V(LgkDeJfIk2MF2NSO$w1sF)dgD%}Xm3gN7Ro{bL%m;VHj0 z?T+B>6siVNH~N7epoNS5tc1efV!9VC+tXfq_Nr+S@0EGa+F5iVPvYccVf5Fo9X~wd zRTsJ{oHdxk47A=;00GB;L@*3>kaw0&a6VcVWSE_9R>B2#!{6BtjfYy9*r0Tm>TYB0 zJnxi;nxM7&tYfK^Y&XAh`gbI*VHu>VNM4VhkY;NVe z_Wdpbu-neuHuI1>uWV#n{c+?j~n^{CsnDsb(?V2 z%)w4JBc1K$gQFiRS(!3MzhQk$(^cfT*Q=HuHP`t9hjE_zdJi5REVg2kIeLzT*HeNK zW-gTCX}Mf4_Nj<{yFqEL#?-kkI&I5$!~ zAAB)><&+6Y&T_DtXG=E>!=p8|c|faKR3vUm5Xnv2T6yHy{aQ2#TT>DI)mGh~|`aRPUoPp^j`>9Em zu3!Nb$by}qdqdTwDChcG0ETc}cdkHttGH-z!+N1-b$&^no~PtBOOCL%M=HHt+9Rbl zdP_VPn6v?C_5-!<3_9zpD=wq@vcoM8PY*`#S;TOa+S$^h$UvsZC&$LTds5~$`tZp; zz@fzm_)zv{kWoZ=szSYB6Or{WYx2G(z#Ky;Z!VL7lYPEp(`XIgSXi=$YX3_!<>FYH z1x%#L#O{4q9#vk;d)mwLcq6&OQDrvNwv6|o@;oy;q)8mW`5C6trN)#j0F`Y-U)xgw znZ;PSbVOA67tIk|5X%@-c2T#FEy!Dco9u5Lt+T!}2t!;Rf|~1V&UBcyRm7Zk%4DHioc`W8 z5GY#z&!7gw(JKjvPkDVkr^$d}D>9uCy*4uQxu?c$yh8Sb=_B^mP+9OSap1N0ozWV% zy*bIW&3NM*ikHx$8TAtZ3E-KlTh|+hXql)=!EthEW5vyngSurvxM1-{7f=0B zn^J%Qq%Zw&y(bLmF7)K^4?4$A$^HeM1&(+Fd(j=daeT%Y)u63jp1YJCG@@_~8nWec z09k(a9m&b2{$L`Pq=s|YXM zUDZR$N}H7VAM+2(Uj6iON#~w+GU9`y@1N1WX~`bhIb+P&{I+5|`dr}m` zy$0pgc1JwXe`KzYihe{4g;w<4k+riF#zKr0(f*_CT5ssxHi)fubnE3I`QI z?tSM5j@?$VIMKvC9t14{&~=_!rP}_fQqq%u5<9k|7-{Mh3%}7sYRhY&2mf-vjMtoH zKAA1Yv746E-6~DCUeEfW;h$eHQxS6sFi(v9t){ezzvI8jchyw$4?*#{29K^5KQDcq zhzt38`-}4-XdlHgC}>dWqogP`Pty2E@^+c-?szEs(LRH>jR=T{Dcx1PD_Hd|$}Fnj zf-20N2lQ(F!FQh_b-vZl+-j*K55>o>2vr|VQZ~&m5EN&R!#1h^apjNPD%e%pyI;Cb z)#fsK9hFlFb@>B_+at*WC43Wn!>gDobR=Mpto4pLS2(kIi<1$3fe3DG`fo;^?JPFp z(3u4NWuID?y7@znS(gMiB#R|7j}Mn@Wt=X>*lEp9_4-+ga`rf@R|`((QhlJ9nJ`%z zx6HgxN5a|6n0KB+e>jAA-b~DG`_Do>_QfxJ+)=6lLtE>BJAWDR`Tn~mH zD&o!IAl}tm+uo^vN54mmG&f=%!|$#wdc*Lv9@cOfQ3v$!)c%#Rsr>1>zkuT=%@+q{ zmzCQYOxfcIzN(;j7Oml1eKCF;j577B^#yW(ar#C>Tfi54Ek-WopZy#e2}VzMZ>uAN z1Y9lupMh}sF#c}nsG2*pLSgE)j&OYk)~mOuh!i`!eIHm6s{a)E=G=+%&XZ2;%8T*# zU|LO%hACB&qvLdl?kz-6LdDDf_w(P0G%0tU*s>_!gV?2@uhtlN1%1 zQe56QB~;zB#Sz2@tS{LJzCh#jOD7>S0xV2*wjp~f@2X+9Nkp?yl_Z3{i9r@)Q+AMFgeksPX3;tGm^Bo0WsWgFR9qVR!MM(0;c)`);83h-&q= z_kRHa6izcyUEk12qFBI^?)}of&8&oO7~1DcmiA6(+cWP%DX_@Mr=v+3)r+5owgLA_ zmrki`tw9|6Eqwz2eq%4VI!zk*QkB0^fbJJ6@O>fQRPJZu-0X{wNbP6A?QJB&^Lb=_ zXniYV>y0iYarb!xvqGT73VcTO)D8#WJa%1E8&hOveuoJ1=?;JzD&={*6t^;i8Eot_ zzx6Iy9H$?12aT?L)F?H?YBy>30=R^%lMo|ou1N4km8XMRYU2aIJOCLFUNMPmgH&Gw zjjhrA;K;Xu%SBWyV8rpGa4029Mzr-W1?svp8!dLg13NwOl9~L5DV@$?9RYEVl+Me` z0!$41Eq2IUBb9N$2)8_#)!W6xc}tn_Zrq=CsCFyzz#h2_99K5j7=8Wq7GQIk6$|t( z6&cUo3J)`<^E^PyPV6;@SRsd~C5WnVHUE|AkE+Fe2Kp$)c;G0)-QO_aHm&o^9r)Q) z=v*`@)1hw}lu(+Xn4PzVnC~S6+N%fLRJ#7e=-}jNrN_XF2s{0kn!f2sOltGwVhh_s(|Zrh~GABnzRxB9DNLnk=f*HtJm)U%_B z;+q}v{qxeyYfAnR!HN4C4$;%~yBpp^rK+a!FFFpWrJrD?S*V~_WY(k@1-zXp@N*#K_So^utB zu4r8k_U_p*x0V4^J^l}TUVJJ7H+}Ug6n9(0`@>t+q7u|{InHuiKb3g%`?c5L`{~^Y zBAlwGO@GdO({b1t)u1TNE*$LO5qd(ej=OM;AK@N^%8oK+)$Hq9KI`9-(3#K=t>8W2 ze8|Acns;3^P5g|fAh&0S5c3pxf=yvw%RmB3` zzEswv&Lp^$<2%tHc^u&;R7eRKWf9s|X4`aV^C-D?4m(yLZeAU%IN=rofR)d|q&t)u zG3ue#=8t+?=E%^uu%Km~$hMq#Mcvk}MvSRbzjv6E0m)GXSl!<@K6AsP{NyGBR)THu z?Ee>g?-|x)*0znJBFLkNBQw%LRGRc6NJ&r`3sS`ykq#nYq!U941V&Mq8Kg)FAWep* zBSpHQiYNgB1PKr!frvn22!s+Kgi!X1I`8|u$G7*fzhi&z-hcM@_a-FweXX^wa$e_k z-u&F-OcjZ$LW#kUfGLws%%7}1VvSKrs61 z2`vD$iGWZXKfolQt_8VYKIkx7WT873WP;{nW`~-HLbeBtPTZc7h54H~bEFBSN?YQk zS8k?ROR>j1vsuc5lD!1$y8cV-1fRg%H#u&$hAynH?LYhKRz_sNvO<^G;da}SD)HHM#9>3RswOzwZ}}x?OHp)0Z2#mFZ|Wkar-(^yZIVp z%2DQZbl=2ocL%cl=y5ERY1|HLpDYt;{{w~F=Q$Md?tO^lS#)k=NJeh51-=%fR~7^D zsi)}Pyat>`vYcHJQHB`0JZ{pKiD{U68h506VH-+JB~)I++@Ly^MWFhOUTM%xxXx05 z#u2_yZ%+JT#k!*zIMKi^{a~m@aksl0X2GAPcdTg2yeMLn^FA$B;(?_6fxn119!j^! zb@bL1rY<||OLEnI}2R*<39WNhE{-X?z4~a3?5KxhO z7k%<}0x@M>D-mwbYPtNSm*E`L<*PuenD^H_3)FsmuIZ#3qgolfT^FFA?4IiTvQo}o z>CqnwCzkx01I`ur_K+hjb=vi!X0f-RSoO49Pg8x1QhirTBkD4X?0s1c&{aLa=nDu1 z2KqE}p>ee`GGO*k*BfcY5afYde_iW`EQr7Dr{%Eg6JDi7+8uT{Y^XF&?L4NqU&+u- zZ|mytn!>pA4f0EMRHEkT3wcyA3npkM--hRnw%v5h|Cqz|k}^Bz;KRIrIWuPNa|?4s zziM*=;MQQAz~i-_DLdECX|m2e6|@Q!fdGudZn%0L%@#{6_Vh$YTI%UO#~+_nZwoqbWT-X+e+pYo;Zmbzwh9DOqYEu|27*!c*o=VBo zUnC9enK*=KbV$MF=)OEXD|-D*o$oK`iwAnv0!U{pk?JlX{=bAIPU>fh(U*w_B zKD>H$=q&XI%hI^l*xRdd#g{#Z_e#F*YtLc9`$|OF;Dw{?Qa!Lb!odIj-2EYJ!zT)g zI@_%yJe%*MCE=#;qO?1Fq+_NMV$hHfnOIzf6*Iesk-MwRuB(9^{>i1dFg`@{be3}* z&{#hjF8Kp$3b){ZYb-B{@8}sWtq7IDyE?DI8xO6n-GdjK#Jp z0%?^~=8=Kwgw8;)F!h2O^AS;>SF-F6ow?#x7L3#M>yHcs2zTPIoxS35_|Wwu;Q5Js)qIAPli2p?jiH> zlsd?=W}~eBEPt9*g{**v9VTY6xQ4C@I08UfVO5a@IJdx|+?PF^#xx(HeLW-%^B~7@ zGWjV-@bx__BF!;m{LE>H0x5s*WGHVtyc=Xo5ifO`Y1&6`|h&0T!^-)5;S%1!{phAPw9#$6ZxY=6Q!{eIjR3`=Zu$9iAJkbCOX{ir9- z;S0AW7^=leU9)+$2X3RpcLKCjCi>iKl;Xs6-lY#d35tF8WT|>A z;bFcb|A~iB^j>A>Us^AnFeunqd>t7H5!oyIK$dErG8ScP{fD@sk&y6QR)bzvd1D}L zXY&*;>Q;Wg66TuXY7^!jJ!IklHQ^7XV3?Y6r^yYJ0ce-igNo;F-=@WcjoGPI0AyVJF9eJq9!`Ds_o?7i=_~ z`Dac`Bg*?j$L(@oW`zFIs>NIY@`I&}b*3AVm&P&FBef(tU%=ZaTxSdG=1a4eKS;@c z9x?hG^^rs(W3B=tJI7V)o0J^?=^1J3!Dul1Z z&d^{Kjn1Z-nWs`cvq2NJ_*8h#JzueYu_D_qPcV;9W)xopU888vAx(EXm00r%BRmMd z{aDaa3hsXj9kyYe7a0~mY%OB@)-9m6^Gf=~s+>AM-VeNzuZ+;n8vdIFj@D3=8eHqe z)S%RvxLDCEI+l5nAp_jPq@hbZYUuw+@Bd$^55F>u|EC8D{`W6^6Ak^pAHy*0K#4G` zzBTLOi%n!}B~#ac!r0n(WYtMJdbV~Xa&fl_Ii0-gJ;gWyGfH+4?%4#>4x3;D6$b!9 z4kiMdHUcKnDt7y`cm7?Bv!K6@yp7cyNgQ*r$(QkS$!vAzrgxlnSl|BX{?)lYrM9M} zwWo~{GPP!-v5C=YYRwKwk1mc@d6{AX9K@R*8D2miP-M$S)CW9VSpnfz#^<|_8>={)eUp?)6hL>l5xW0M<;Hb)zGtGjl6^5i-vpLVW470(wV zu-s#*T$j%EtQ{b}SOw*7$GrHe@F`Mn%o7^LFUt~sK2pv8@@Ni7nZDfID%kafs3&?z zcAYD}blXgn0sDUL?06WpqZv0iop!;1~56k{-ye0Jq7 zWV?Lr)oNBAJ6LPAT5C9XX>f9Sq(pP)m8(AE?bevwJhpA^)tzQA{ZJ{_x0@%oRWn)1 zn9QiS|FPF0EC0>W|UYVSKWRl4VwpA>eS z^ofp2S~D-h{pW-KiF)5sg<4yjd zy;4NGY#SZ0bpf@M@%8gPb^dF|dm9!}TrhAMAtz`ichF2)usm=p<-(oI+1EtIV#+TY z`I|%fCMtOt&;;XJK(OB`gvQPe9H_RnYp0^1OnA~0FewY@;;BvQziRzacG&ItWc%cS zNFd7vnzB%{sV@lK7e8Dr%H{l^_1;%62~g?!yI^`hlaQ>La#fG>PRZE^ByMp~M34Mg z$C|Cwh12h8Lbty-j5x=2M$8?9sK4+*;f^ak{zK>Z@8)jF z&y;wh_KCk>YaQLw=ujpTxek#h&L6=l5=; za6S#w;S3LEsFhJ*h_sdnmEzO9*%t<~&_S9f4{y4`oAp9C^|~K*+pdZMTFa59Zp6;= zst7PS(pN1}YT1On4Y~+X`DO3c9f$|cDVb*yePpN(l?ph`u}V9CwoftWPdUzGp*T$; z#WHo%pMU!i@L@i_n}!DvufxFfAu_7G|K0^?S%>IedB9Ygt7-^UT;Fi(62Q;oEB?% zBg?O%-pZ6F+J@L=owe%GHom822sX;^)4$20(tj|0eq_AbdBA}`i5#edIvvJyCmL~o z*1A;VJ)3VSM^NO7HV4ASdHy8MM2?ov3nL+4qoIYU<>AD*<5Tj9*aM=3e7)e9Zu1Ri zc>d}`582goe=Qr+Uyw^Yy6X^$UZOjL7HI;yi8s)csBxw^k>xhVX;i2)p`D7TOkj(F_R)Aj`YY?oOQ8x6BhaRS) z)D9t}UqK(?@%CV5$(s;mYTK|RAxabTT zGT^U6mwdi0ZJ<1x++2^GdEzpHt}q5o92e%sNAz9VKwjMQeF@Jt*Nx@A|BKXuI3J zJYbL)=l|!zoYaBZDC+VPlKw~x-Z<;_L|;|Bj$zoM3nM*MN)^xIc35PUgy4Z$MaSsJ~!(C#{Wd$g+DQ#oBt z?>AQh{e0C7N)vRhmifkI!Q8W=YM%9(z4H{d@92@QSI-V^uK$5tw$Fm zD9!(BVG;k!)vK);Di?)pSlnf!c{`C_6XJqq$0}ANr(^7+3yx@cHQXK_CX~}&)BGLcVJH%D=cwggzH@R9%ztarm9aeL)jbcTVP1rN zuI#wI<6^LI*5qz>sEdQ&LB+=YMoR->e5gz>q`u01rKilzxw&o3Z46!0##qOdZr?=e zQFjTif}1AQ2}xe9e-1T=m_t6+xx|I+<2&c}{ri)^Y=K#}OT|IO+LnhudpTGXXk+lO zGAe%>iQ26G#i+qO#2Pa^x_c_NXSi_X=5O@c^9K9VnwkIA8hBfsyP8i_29R12j!4V= zQums2vxw>;Pn%r1^r@K#=6rl-A^d542p|2d+_=B;(mXK|jcWe*;`RU`{TZ*5n=?T_UU|eqwA+z!^Wy>Ug zU2lJy&`7UTHz7x}<3_|ZTU#Kl)is)+n+7y zK%IiLi&*N~zZ6-!!6F|Xx+ePf4i50%-1!asg#Ip}oxrM9G z`)Qo*@TQ}aX!__vm21cCF+ml4&4kxekS5%Yo?aN&v*F`&kKsM#gPuoJfwU*KfAgCu z{HW$}^Mh)ZTEU1~cHk*zi@?sI=^$cZhZiSjemqi#VZUxBWQT=_;`B2z@sDFa&|8pg z8OF1=qj2dy=21&UHH}tnUlu4W+Exfmb!(nNOGhrbCI)RV279eemn$qu`|nX%Zfcjll3%q?mOJvYfEv0v_PB<}{UO)uxhb2h><|UpZiDc_O{2ZMNr2M z`o`=Mal{+D+wHriFBz0=YzO;t|8;z8L(@t}#T3P#`|0gVIhiPw$gcwNzg%EeYgX=L z##V^|1BilWo!Iw-rT3SRy_kneyL8iSIb(d)!&^ePCL`FJM}|(N_vrvh1dgeF^41jj zd;{DGZ0A#c*tNRuY`#}YLQbVVez&)kp0+J zsRGpUg1kvzy9KpXLoyWw5fANwYV3hx<(+&jJH}TG>Hb9_DlvLm6Zq zo)!2OHyRQ6ulG8SBP9n<)%D;d=3J1!D4vV@GDR6+|54^Djdb;Xs+gCiO@3^b5wqPMAK{bFc+g{MFtJnBwi7tA(A%C}+Gor#b&0Rp+JfrLzg9DC_f;nx za>%cw{W9v?)6~F>$JoWx%<~UryN$}@7oF|PsA}!?hp3fiNFg5#0fC(VgVovO_qPbV zUs#bP{%CkHSfY5coFoV@m640jZWzA56_e?T0~9uDleb$&J3bf!jZa6#jJN;7$M-4v ztACNQRFoDMG*Wn#Es$Z6-}qW3%ThcYFKU#I@6{2#d#e1(;FI4@obTEQB-GaOTV(4O zj+DYg!_${^+Hq042u6$)`i!u_(ZO{2#%l-v>J9aRs$}X=5FXxdD{BG>v!MJDWu^z6 z!JWpb6};bwNKj{t*T^}}Eb3CyFH$*lA5&qjrYvfuggV=Z?jR$S<+tnF(D=-UEcQY( zfV4-ehsXRZ)}3|&CZJim3w4Jdmyu=-|E@sz4m5dxiGytWf%pSxpqWy@y6v zGe43Vq4-*@AcI34L(kS8a&Z&0wD!sYyd=W2=MQyC#2kg(ML(81?Eqd_xtB7#0pUL( zqTXwf+|~M}x2CI;lc^TNv}GDHQr^HPERt*5?R;t%L&w~JKSLlIh8O)h&vxe-FKjqe z5$JJ7ua+*_Tg;6%Ao_QD@FjQ>$3ot}Re-@ejF=`(JX#}Y~f$$Ea|VM4_rk%==|xdL~*eh;Nr z*yopEU>qe0%K#^03D+Zu--=+Ao)SFn8``tt^q|L(VY`=kbTmVLXBvhEZX2zWwZ7qO zpGmcDenAUSA-WXElk6I)e)bY=1)35y6dfy}lcEopM#ZvxzfGU~=CEh#u(yS7>!gQl zaZ;E5;ujM_9R2fDii21RXd5j=+$Yd+T4V&F@}MH_)!?lGTp);BIS=w{TC6(o8u%<} z((J4Oqqh`c+V^Yz;#r`J7_zr4cudV9+Ct0FN9L*~aVSBDk={OmaC;BPi0K{oA{gt_ zWLRVC!@Kk&YYJc^gesh~=F8qrh1(<%auaIWhawT=eG+`X)t>(zoX8tCk-FZ>RJ{!n zR##E9?{-`EE(jBjr<8l@u1`ISHqD@6CEm7@0A;52093q?2`|(>}f+{qKwB@Su&~fPs9G z0rv48#)bGH-w^513`7>IbmHh!Yl^M&gXA>n-d6RLvDLnJNB@!q;0HJpeZu>}+cYj~ zIgA$ZhY_|LhaF^kC%zgVsv5|)zLJeUdL)yBJZZr9X6(D^Z=brU99SYNm@0H|$ilw> z^7qj$D@u4P{k^zD!Rlk__S}=EN_x`mH&2eX4wkZUferh0RGRyg@O`*CA+H*ZlFYfb z2R)8Kqeo@y=i`sT%{qXh_jLhv#*tZtvtu@6G1FqF7fyH}7yK9NbVDf4!XqyF{- z2AuNMr)q=@O@;MpO^`Qe{F4X?x%a{q*vnLsRmVl4lU*I|Z{Hr3^?HuT_BMLh(x=PF zbWNpi-;5(!cXsig=eRVe;LCAQx)V+AI;~wtpa00o8M1i!vB8D0H`~%!_E zHMVXAQKwlt&7DK%QIdhrxTpgTTkgVD+o$+iH;cdl%YuT|CsKT${D8L~h8*d*eQHnz zq7arxEd6xi{(HTY@YQNsd;xG7Qx_kUK1^aDUuHC}qUl2ieF<8h{A}o6*%6sbca8ix zerfzpqz2#X$J6{RvM;?ILP0!rY7VO;EZc4^Bh?*<7_LVp+BT?Xr7`0?!{{-ib7=By z&WzQk5-1wc-;h*hu4SMeJDg3QK1j7TuG}ZIA6K2SYr6FXp3qoQA5iaSU)lpzW!NK~ zzpeZ!aOfi4ADC}iytbRviTZ-@Xk9)SWZi7F=fr;`5UCe?YmHB~8sP?KP$EBg#|&A7 zOt6mFsPeL7EQpM}9OcO&qO6D@erd$SEhtmIZZ%`@EO%~MDYNxQ>PN7R`Ob!dMFnXJ z$VKiQ|C%a*yx^SR|GxOYXb|aYY(bRA+F)nAo5+AH!XhGAdbpw%*S7z{*L8V#a;?ja zA|+KtvcwF&{WcL_VC>3m=|U5Bln}vthFL$CR?@|r*5s!5H$0@gcL-qznvR5 zZF8N#zLFNi@r7ZlTIi&#Pw_E!_tlfo#n zthIzT;u7Jm8zQv8e|VDN$a9taX_uL{!uW^22D=E~n>&oa7UQ$*vILjR?3AU=wMBc= z?N!2DQL?EV8nrf8@4*d&edHYsc0YT?X_1l>xU)TBhS$Th=%#H^RnC9%-^r#?+PCID zt$EvP?s5`6oOn09ISr3#v%gdV4g8msQ|Uy!XIphnt%z)0j^wDQ<=a?VR+Zc9xT@MJ zqh`GR-9?1@x9Z?z*za*ZNOkXQ`it;X($mTP4$KnbWZNQ31iEo=!^GoLDo62mX-Tar zGUc}j+Jp#u^i>bMIDq^bv$!BWb}d2F0(Yz^oksx zw-xyI*A1If@pAEW_%-r9kG_y-nWtNXS6i0N0d2{U;AnB#yieujz8A25ouzBtItCR! z32Lgiyy*NjJThj5J`!0_vIQ_ji5>bQi%#0uue_4)8x{b2qwWxY$lR^*@R$MjK$~6L zSk;SzLI~gt?m)ypBNT14N^KDp^9`cJ#dShquNA+x#ZEB&nP0Z7c$q~5Hy zG~ow6dPyFh;2YVqGlx)X=rf?b#zS{Q5RB0ZkEeCldJQ$&NhN;l3a@7BaOd!LLCg+k zvR;Xl&uQNn^6kWPZozvnZHP8F+n<4XHoJL-7FAc=f|PEw7|4SUXj=@paL+2STjCo1 zh|U)Hc=-Bxi@ZDn+E1P2=_~iIuRHhP?JtXP$C9gI--dsVZC@awB1M7HV77TA}D zQTYd%IhhnWjVMmDb2pZd;n9Z)r-o(w?5)po6|dbQ`^|oKDebqV>FLzwQu^4L3Bei- zi> z)4D${%Bj>}r3ypiptnbExoBm>LmSR8^rO*Ca*h9(;PgVTo=U8r8#D(RS9E#ebnlt7 zq}*o;w36JMA~5kVb17=z`&tn(Zj7l3xbqG(E4IgN`96pqGix`m@+q_JhCDpGk;qWxK77{^Ib5-2NUgP!jB%K0+Wp)CjKOYdg<065n~W+2ppfLEa}e4l63+e=#jJ>QDJuBZ6ZE+p>~V32w!d@wbgSj3$>WG&geYD0xr0b;JEEZVOx?ysC75l*9G0z(-^~zW0Hh8r) z`w>f5?i%73YufLYcHHKys2O+>G%cxUe;ibqgz&0yQHhPpm+H+&yzGup>gd^MvP&uo z$#)Am3H6CtGde~aXdJ#D99TOXixNoVnqd1UDgogt>ch=}Od+F?1sj<@Jgh*0-`552 zLTiRm8iq3!7CQZDMWYLI!DJ1?umSLvH^Z<6Zmu-9?b~q}h*#&jTj#k9p~*KIu%pmJ z-%Iw=JvZJK>HQs+=N1q&>e3CrHt>l1M;X)gU9VJJ8UB(Q-mWk4-MxIFrY-_{>df>( z&+NdsvcyO|bVR!QJvFg)=ihO4ULWr{5S<^w?RVU1EFE;gTmDl51o#cUDFRjgM0XnL|sKm{OD4^H-)A{$03s(~fcvFCBi_3zs7+HiIU*=W$Q5j~vk??@f133Q@1<3jyU zSD{&38r+wq5A877mWCyluSZ8Otc4xR1a=n}S(egN#TCegLFR5e$@v&XKeJPAqKk<# zY`F8~^W*Ii#?4$HL`Ue|k+iX&ds_)KiSWVj#+N0s-r{%m)*o$}6;~@#7_vCsxs_SY z&|pm*vmAJ#X)zuTAGoYe8kv6Wa*vrCoaAtiInFXPTt^AJ4XsnW!OOk;x$fpUFH6)h z6@JMtP&auYRQ7jwogpnt@djM&%|8)hQ;1#*`%(k`lCsGS46Q$*V2rF)inf zwisJ4iD=c#e9(;dvAiTP;PRlqW}6`J{%n?IV2|Eag8Sr|tjBsLHM_Xb%$1MqgVDVP z+d28wAMXwbGgb@fj5#|A*$gSRNvDe0BfCcZM>TNCVUSdVSEr(+7LTIYxkXP5AJ6+4y zHr!D&Xr4V-xz+NIs~E^zEDh~}^3YB=nY?iKMLdwIlR~U7HLiR?=oXjMt-t16wr)s*_n$O{hUTpUgUi z#*BsPdXbZ?acmUdz~6$3S>!TLAw|M7arPaxu1>e!#yFU5>9uR6DtI5mKiq3feuhx^ zXtb5z7U_-5)_iS;;^o{xbC&SkxW*FsHoOOMdSw#JQuK5KLfO$OSpGT{8aZV~;M8mi z#fkcqd_4eq)UljHY|d*5DOl3o4q|y799YkAbN- zG-^=N5*VghBP;i)rEL5t0Jk8PF1<%STILL@Aym%a9X~E1U(F29^33GO!nMYlT)5DH zNWrw`+~y?1tlUVK+-dO#JqrFk;5}B_^&+tAHx2ZA}-T_9-Ob~s`CqcJTfw1tYJPDEgubG zjOMzpWF&+U&)|QOifLn#QBNQAgtTQrudA~l{T;Un5j&Z_$jRs68CM)!qwVLWn&b=N z^Ewn1e_Cr?$YN1Ts9`{)$ZUXNSYDiah#c`fMJ=X-6GSn?fF-3Jo`sgK7kF>hXn z^=`02e#qmlofn- zyHuuG)MHx|)%HT{7H%Ksx;|Yfr7LVfYR8|Y)@qCu&R+TJeoN0vR$PeEV1I*U0px~c z8bflDEi3IxFKH?cxQM(tle1(Ul25ySfFU(J>s}w3Ulz9bdMz&;Ou{ z+UM4$Vp*hl-ZDDV121u_mihK3H7Cvqox`DetC`x!ZMjJv%?^9paYZ?72*~E7#;t8I zyT$`AVZBmTnwnh}MTCFfnrNE*k0bhjKfnB>Jc69FhV;HH45R z5ZLDm>)<*t zlC4jesf8&MiUNKD)(>%2VW1J}v~oJ8*`lRAT2~FSjVTyz1ERg5L4?*|?9}@b8%#nnb0d>rkT9^xJqw6Sqn;j> zqpT_NfARKTy`VXtdv!>s5z1O3K2sCln}Zf<)p(T-@0%JEZ2sYEyJpXC@AcO|NP>o`>c&L&2m}f#uDTH_IrAv>}gFeuV#0l zZ+8>^tYw+VjuZH7+P1m=iSYn@;@S64sC@lIYSHo*$3Ffb`%yw*{zi?(KIHtiYJqf% zB|4n7LWx@gRicRx6~mS3#y&%@vbz% z5r;`ZZ1m*W29%R(m6=OcQ2sfsc*8S$!@q1TxFz11fh`FU%u zGa2eM@j4hT{&&cBvlMv0I>&vDdpW<=pkGyN2U+z)HY{di3OmxD(|AKJYKcDhMh0Ky zHU62`J8D9%jn56_72H^C;Z9kT(h|1trn^yr+q%xm4Ay6hDq&O8ZEWVEZ=cELN6?V* zlBwNVY;Uh_+8X8h(zXdUrWMegjRcRIHoXk2T%ZVE+K9dNh+9emasZr+=EP-}hF~Cu zM4gNRd_Wz1Usbw2xj?;i3Sux#pkCH;Ny_WOPCokw3#buUX&%+_ha7gC8zsY>@R#xVkzQ+T&34IB9Hm5h zCG3T#MKaNlt7;}$MzFHN1ydN3hWfP>czK5B`UD%gW#v#WEm%RzLKP%aWd$wP&nGA# zLP^2K?5u@|$vcJtv@gL){u7Prvu8BoQE@1BB7Sss5$<9`Fsv*=edIbeSF~R5gB3M2 zL`FIrJI?vxSH^v0?u-e(*Cs2if~GTQT56;i94ehkN!X5Dj4^TkLb%d>(Ks5IL7r2e5?PFLgZR=_7cEn&m~2@R1)nc|d_BNoZiKv@;gy+F!9VZNzU2f>SY~}hmizXu z;e5lAF?Mb>hi5;E+cclb%7sU*ot5hc>&iawJErlXmXt)oVEw9CmvEu|Bh%8Pra-4& z-z@{~zqpQ7YZW*Ottr10?rgz*Mby@9{I(#sDn<-T|-#GS@R~q7j}%0(-oIxH&q@mhMIM(O#q1J#eAwhTaYJVa$St$xHrYm99h# z+KBYB1vz}55Lys)^qbR?Deg#-`kmbUbATm$D&q5tX0fg1R|(6M*7Z%}PMX9qJE(7V zL*~O@V9x0RSK@v-wc5A1lIRoXFta8 z3n!7Ul$fw8?nPTmtgaX)T(sjcXXqeN$ zWljv#LL}a`^`~_NVbasZf@8%9O{!0y-pFzR>uA^pcsNcXXlXa@VMVc#F?B{WPP@5^ z)s^b|Rv$7@-OMDV9ac=h@Qhaw=UzkPga>e6c{M++tQTI$L+j6x7v@lTIoEw6GwhR% zf*cn`k-T1xkfK<(!9AE>aYx3%{$DVkGB3S5sJT7c*4`fGsn>M>4ltTYl@8@yseD?X z2I2VCtPFwcaWU!gQgm1iQ=rmh+N();c1(L3uY)IFNtCiG>Ci^|4CM|3OMyxh2*pJB z>u6M=5feXpmq*6g(nm7dRT$}Uv#Fo_%q7$?u>8>?dL6vX$!CCfm;*LHyUXGK(qaBXt@8s58*H`FsDvj#84bY;a{b zWyVN8==l;1%_l+q#wyXsk3*9%;55IU&=<9>y)Q4zoCOnWO7aZk7X;r~?*~2mdkyZx zii-%dwt$L!Y{l>G6>?8ol<1 z$c3s~neHKZcCISKn)&MG6f_#)?;lQ$Fq zxBRRmw$<%JiHh8wG)k9Ou!y@;r+2ou)^o-0U6g9h)XAL5=NSC8yTqmXko0bBj|ui(I%&D|up4X;B75dky|1)bzE40Z|p{Zw>;Lo2;Lw z1mvl$xNXFG@WZ&^BcOD|tbO9u+91qrGDG^9jFt~IF7UOpSY}h8jZv0g>-T44%p?DE zy?m9~Lkr{fn16|x z@4p|JjF4e}5|42WP{ZO6`WYT}OsYb|Xrr+TSR$x`Z<*&!G_*BK*K&;l>j1R=?T{N{ z{rfPJ1JyH*l%_3I1tYb_XxLDL+92Z803fr5x-iG_=KwwXKR1=Fq#&$#hJ@{IjLUh@ za|qQ?hry#{DcCo(Jo# z4J!>i_}E2K`iV|Y|C^z}V!+I<)~gHA&5W~_L_G;i$n>*UsnY8TF-u$NrOeXkoo&F1 zlvh4Y=~lfEk#jZT!Wc1}Zh>F3<-l<((Jx7d+JOg%3u>d@ybEX~=1OU$2ja>yLvq?SCGM=||4^ z7dlXGwTSRydj0Th`Zct}Wm!DSM#|`_|Kmb`tyD)e*hefhAFgA@o?ZRVQ+CyC zm3eV@U{JZI@d07Vq2z}rt`Zon-KEZCv#Q6=>1C9p21{c!j1|U33V4Bps;H(<=iZ%h zv*Qq>ngEH>YqI?LvKJUsjBo5oh~vO?#P%YujRq})=dKIq#M=O zf(mFKM^VkIuk+xoc`w(#?^T#F_I>WU2i~)^q?IsqDUZZl__Y$w_?EN+Wu>GkXd$R| z0N1akXz2GWC<{h}VMC^$NTs+0c!viSFH6O{Cf91T9>S4MgEG`Y4In8704g1y%(sT^f7`{e9;p~q;Ohg4tueD{U&()bN<1>DQr7%T zGJZQ^ZBuITFv#P9_4xSS?E6+qo9q!W-OP0d1j$c(coY~=3ao+>&ZTv2J!52zL8%I$ zw6%WiXg4pS&OS^bz-R(#$8;^YTAxvoh2=S@mUm3}<8xyvIqJ ziekBe;*Zxhe`1>J)V!tE+-Jae>Y5v`VFrr$TFbBZ=lqFR5^k=H6FV#02|qG(XIkrXO`zOtBQ_06O`h&iA^D z50ivc%NU!KgDYDdhEYk*qwjz)hsVp(qO&qEq(_>O%GtcLm_kl2^pLId71GTy5UK_Zv`S|ZQ zrp=!g5qcY4ke^I9i~tmXDT%a*F0W{Vyv6qxnO}SS6mgLs0MYgzQt$r5ko0CR+1Xm9 zg6LodWgj2kE7R`D1M-?{`_A?fxqG>; zM2RL>z-6`KhX^4PQM&#}2k43*1TbRwkpH9~wsha!4dmEP$6x|F;HyfX$UfrJmAY{=^K@XHrnY~o4jJ=k3FBf2J!cL70BaD zdvJNc%UQhZF0*ScFU(XLT42ou%PEY#RN7NV*T3!q&P&kcFwE73H=Orpdfu$^6Ge(+gG{?`R$` zq7|>vvdIu$N!@;Bg$u1$Geu|=-Yty*WihSI4+qFi;=Q+|EkQC}drqq-_KTi#=g8Q^ zw~%Gro%pMUULX*9K97!(l++PwR+4|Y^WGJbL0(=VB#Z0 zSQ)FkJWg|SmO{QP@p0soKeW?qSf?!sM%=w6L3h@9vh z#SN>#*TLIdh9nxsO?kX6fB4_uE4I1G<{O0V6{3HaNwS9OgF5L2n% zb{?e&(P>8;?mVWxRzj{Tlh0dIpG{eD3acgCxsrOHt)2bi;3Ima*vGZOSDd+7<BXxIR@G>;EC zvlmu=k=~Q?+smk9vt1D3v}M~$y+itE6ycsadT(N2U((xc%udFn#YJXo2qzAO+r*mS z+KpQnf$YbDXrL{G@z~^L2EqmWrLf|kkyNjzI(2FWGO*R#+4O}cm#zm~IoujkTGQs{ z%0+&hY&L@|qQN-ja@`m+Du1Cn1(S~2(I8<0V!U?Kb-HzBJ;%@bsZ@Eiwd=XVi_kUr zInmF0edkihpvZ}w4`>OMVzGd zj@H~&kHxSYo8-`=6$r+Vq-E*Wia~@%P{6#XY_Xqia3>m1|I&+2vQ`aoMtHTQfdUbVdXZRg_-SG*VJ)|+R; z$WJ%cbu!hIxzO>;w6?A~GVPHB$c8c!)8biKdhcEK8>(PKe16&f|T(nj#g!Yn^7Z07d z?Rpr3WKAlKh=dKdn#X$J7*J`>T4o0qrHR$?z}GqX8w?ouQwe+>??6U*kh2Qm)~6|w zL7`jQNKSS9onoyZvd`YYkb$L$D3vnBR7sOs`jy%bWc@H-vfow4RX$A_0{hF!9kY?cTqMRu38byJ$ukJ;bF z)_TTPF_|IRdcT4QzlB{WA2AYgvTr)FKh&kbBY6P&Z=2H#iz4Zc{bPR*E-@F3iNbL+ZHXhq`&;R!7H5d0$`Q=JlS8Do04rAqN2)ZoB?&h zHlJN=&`aP(^paa^Hpa|!8!J1)o6AuzM)S8DH$Ls#%p9OmS_)95Z%tkC&p-px5iv7X zFkl1uDEBx@pY_EWbmmo(FIui_pg0t=nxoyC;?6 zYe#qprrzvfC0A9_!L6^fF|gcZK3N7A;6T%g+wKU))fP^=$NcuFzUGZvT18q-sbU~* zsbjcryZdMK;>+jF@}L-(>d^vt_hi*-DW_*4WhIf&R@Hv()o^OLN{WQP{P=6+S8Ecl z4L3)uQGo;m#*T%LbELi>*HyCr(-aqqb>%rX(A!%K8SXuC?r734HP5>8E1pZk(q4e^}z0*3jh^;p%`yp*e zOPiD9kA@k3rf^kD%h-bq#xcV^hxWCH7EZUVdN_#1=2@~pBk1-om#G(q6ZX^p7q75;7-;6o$?3 z0{uB3S2mClu4xs1Se=GZLh23i-$r)JT8xA%bkF`Qj%bpn|8k0t7}kZ^+i0Qf!Q{-~ zfUrSy8t-Xy1aTL3)CB~C_@t7BI52d0reDZnZ93*ZpKdWj$LG)ifdfZz_ICNtac2y;0NG>84phUIN*lQ8*t z8I?pE%>?|v4p)NL@b(R$G<#{Wbnfy{_@n2XA_RFX`jXs4^dYrLF#p%hx*Be<99$dl z=}KCvUAb+64iO#Z&On9!6mh&zrZw+VrDD!0!$wpY@R2SH;Dat7wlGnXI+gEzrSDnW%+%pT@m)(vT^-e7}k z6I_vfmJP-z_&`&HPr>ToEswHBByntY>wR07&HT$QP$i6#b3iyo)}+I*RXt{XeV?g1 zF~TXDUCPjs+qLFRm2?TkA^(|(Xz|BQ3rqa2gS@8%{s}Ya7#la}L$T`G-qNh`*iS)+ zd=>25af*c$C#O1?mH`+)+N{6`1VtKpyJ#-JT3=KCT8_(k6^}+(OHu)zFsM;=7A?4} zyf4hKjFS=)l7fa|HD-Uyv&8#SM(QfnC#4>o8`hlhPxdRHwV{Ar-NtEv62ksI(7-{W z<`-J-)(NBpmS0n4ju#kO8gk<*qB-#Jew4;SrM4cM?L5oS-#ak)Y%to`SGB_LY^7cd z>C>eY149I>Oxm#h&VAV@dOG1LMNo;N=+LuwL$cOv|HcB;pC2r+9Mgp5>-2wvjgU5H zPu^p%f1r`CLf0Dugnq*6ih?rTB6BWnCB7E9Y)zcR8U{zF?LSG28Bg2IHUYlf)0zsA zgY-n7Oi!X7#)nb&==^<`bj}AOb7dF9BMRir(EKO6j*-XO;exol?)z+GZ_6+skJ{_w6q0IM3sb8AbrCL@3o%% zI}i$L+m2HkJ*M!Tb;>03?bKO)d((z!^ISd0p7m{CDf{WsHlCk#^I2D4Jq+(W)fpwN zRHvGP4oLyGEl_Db072cvP|5G_x6VX4YPqzjvmE7RE~kx@gQ;%tq*d3%pg5ml{f-Nz zpzqS%*}o6}@fZ2=rV%g#5TKc&2UPa1&T&uP$-Npkcj*Xu&p_S}HYZwsvufybDS+Z; z`ix&{;7^u_ZBkBs?Xt>fS$VQ^{Ze+bYxYyyFs;P>D$ho0RxXyVEx-)t5YB0-)&77X zS$P3^-ttH+^|WmG@aiwgUQ%9wlJxvnFR8m$vFhW{c%6jb-ljlYO6?BaFFsZc+C#QJ zu)>j#O>It=pV8@AocX?xdFGtc_4ZR^{N*czyn>PA|eT2;k66rwGc4^38g;y#1-_U zw-2GkW*Y)~0HhQEh97=MNL)$b7`}9wC9xpyK9?~t={@~LFS-JKNoDL6!_RxRvaBS1e3m&NA z+#KpV$Mnn0c=-86&&1X*4L>+S%YT8=Et2?kKY7DY%8|5E&uv5s=$c>K1_j6KBeK18 zMqtGbxTbxRk|T3X_U6Jr-#_03#71!6{l{ncJpr&}tWFo)R<0Pw9A$%k74$ZKlqSL& zA}mTyU#_yb=2CSj#wGdTHm?0SVM~3xb30v~V1M&!n%IR`&qBOuCHzDIaq&EPwf)^T z(Pe|G(e2v+x8?7P6yOOs2asZEL@++xnY9OQNKEaVf*Z?7=RljQNfBWs{s zvjMudBs9gWPZqD|!TWWMs##jKEWn;&tB=5vK(CvK$of}4p3KRLiPP?xnhn&wvDVq0 zTutcV#9nZ@(E zHuEn#ZtsxIuV~U9itKq)aSZ9WPWd+HE4`fHh9nTrO`R?!N`JCtQl&tr^_FqOtF91k zCqRGG%E8E{>8?4F?9$_u0V7zBH-)ceCOf_>4KSnb@fAe|4f?ig6&q}6`%QxdGMGF# z5A-Hn65V6j>}jq==gm3J);f=MzRwf*@CE2hl>4Z!nH*NZypjLC)VPz4{YBKfBX)zy`v!b&Eyr2~{jSMmnQ&#SGv+dJos zJM~7Mg%7WVp$y1f^@%Vn>G;Y$3kqAi$lo@W5B_6`>A#;9;Rr7a`aFH1e6N*1p?P0* zSgwZ7hQXcj)x}VW5t*6i4T^a-n&^1wiG_6rnZ--A`?b}#OI*-PbcTE?I7R}uF*w5A zZg@S*z*up=;^p}Mcmww8M3ZC_waDMs0|*Mt1t5U7&^wdG#2C;^u2Z2nf?UtfG1=ia zwltHCJD9k&z|~x|EnpL3t4tjimy;iKbh`hCo7&TodVQ`Hsr3%Yka^r7Uy8)gr z@%48O3~0@OpWg9J(_x~sz6z57*ZsF-%Kt#!;s1Gs;-7bZq3D-DV<9C~%OMfnRydt$6ctqAj z{MPleQp$FJp1xJ`2SV|5<1H!W&^-$VLpy6+-{yPC>oyPvi}+Ja#BMK908Rj*G!WJv+$O`7 ze{s?00a5oOz;*!J>0cl%T>nCo`RA?wSl}NE{9}RtaSMpt9{A#r{*Qkxq`W1|Q*I8e zUO)wmE1YiAW>>j!rk3Eo?q3|Th!T$-CL}KiXhQhRYzh+

x=qX9a}eHaF!wFZ2RI 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 izBB~<54`e(JFd;Xd● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am ◐ tighten the retry loop in the sync client × + ▦ All ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ Tasks 0 alt+l - · resumed ~/v3/projects/-tmp-codeaf-update-drive.BPWtpZ-work- │ + /task - project/8c7285c6057fd684/transcript.jsonl │ + · resumed ~/v3/projects/-tmp-codeaf-update-drive.YtHivL-work- │ + /task + project/da72b3aea7f2b4f4/transcript.jsonl │ · esc interrupts · ctrl+c quits · ? for help │ + /standing │ › tighten the retry loop in the sync client │ diff --git a/docs/design/background-updates-frames/ready-working-80c.ansi b/docs/design/background-updates-frames/ready-working-80c.ansi index 0774621d3e..b76a589434 100644 --- a/docs/design/background-updates-frames/ready-working-80c.ansi +++ b/docs/design/background-updates-frames/ready-working-80c.ansi @@ -1,4 +1,4 @@ - >● codeaf  home teams  chats  sessions spend settings  wed 11:07am + >● codeaf  home teams  chats  sessions spend settings  wed 11:36am   ◐ tighten the retry loop in the sync client ×  + ▦ All  ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/ready-working-80c.png b/docs/design/background-updates-frames/ready-working-80c.png new file mode 100644 index 0000000000000000000000000000000000000000..d88ea2f94de65fefbe07e3be1badeb314bab3bef GIT binary patch literal 42268 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

Z)$vx=3QlcOfJjXNM^bA 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 b@9gVgQJlS&-k%05fjyYN`=4&~!Ds#l1#R+Z literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/ready-working-80c.txt b/docs/design/background-updates-frames/ready-working-80c.txt index cb3ea3f6d2..f9a793c33a 100644 --- a/docs/design/background-updates-frames/ready-working-80c.txt +++ b/docs/design/background-updates-frames/ready-working-80c.txt @@ -1,4 +1,4 @@ - >● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am ◐ tighten the retry loop in the sync client × + ▦ All ──────────────────────────────────────────────────────────────────────────────── diff --git a/docs/design/background-updates-frames/settings-auto-off-120c.ansi b/docs/design/background-updates-frames/settings-auto-off-120c.ansi index da1624cc8c..e79972e328 100644 --- a/docs/design/background-updates-frames/settings-auto-off-120c.ansi +++ b/docs/design/background-updates-frames/settings-auto-off-120c.ansi @@ -1,25 +1,25 @@ - >● codeaf  home teams chats sessions spend  settings  wed 11:08am + >● codeaf  home teams chats sessions spend  settings  wed 11:37am ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams   Providers   Connections  +  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams   Providers   Connections  › type to search - mouse on - timestamps footers - turn work fold - step icons auto - task column on - quick switch on - input history on - telemetry on - keep drafts on + google sign-in id not set + google sign-in secret not set + slack sign-in id not set + quiet before practice 20m + arrival brief after 4h + tenure after 3 clean firings  auto update • off - check for a new codeaf at launch and install it in the background. - - - - + keeps this codeaf installation current in the background. + background checks off + disable hints off + model in commits on + ssh reuse 300s + ssh heartbeat 3s + ssh missed heartbeats 3 + ssh traffic lowdelay diff --git a/docs/design/background-updates-frames/settings-auto-off-120c.png b/docs/design/background-updates-frames/settings-auto-off-120c.png new file mode 100644 index 0000000000000000000000000000000000000000..808e086c06f550ffa3f3fb1cd9449208de740159 GIT binary patch literal 57727 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 Lf4%scZODHEU1zDr literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/settings-auto-off-120c.txt b/docs/design/background-updates-frames/settings-auto-off-120c.txt index c9970e72fc..5b2e614c7c 100644 --- a/docs/design/background-updates-frames/settings-auto-off-120c.txt +++ b/docs/design/background-updates-frames/settings-auto-off-120c.txt @@ -1,25 +1,25 @@ - >● codeaf home teams chats sessions spend settings wed 11:08am + >● codeaf home teams chats sessions spend settings wed 11:37am ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Session Context Workspace Display Spending Safety Tasks Teams Providers Connections › type to search - mouse on - timestamps footers - turn work fold - step icons auto - task column on - quick switch on - input history on - telemetry on - keep drafts on + google sign-in id not set + google sign-in secret not set + slack sign-in id not set + quiet before practice 20m + arrival brief after 4h + tenure after 3 clean firings auto update • off - check for a new codeaf at launch and install it in the background. - - - - + keeps this codeaf installation current in the background. + background checks off + disable hints off + model in commits on + ssh reuse 300s + ssh heartbeat 3s + ssh missed heartbeats 3 + ssh traffic lowdelay diff --git a/docs/design/background-updates-frames/settings-auto-off-80c.ansi b/docs/design/background-updates-frames/settings-auto-off-80c.ansi index 6d2e66f50e..3847ea051b 100644 --- a/docs/design/background-updates-frames/settings-auto-off-80c.ansi +++ b/docs/design/background-updates-frames/settings-auto-off-80c.ansi @@ -1,25 +1,25 @@ - >● codeaf  home teams chats sessions spend  settings  wed 11:08am + >● codeaf  home teams chats sessions spend  settings  wed 11:37am ──────────────────────────────────────────────────────────────────────────────── -  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams  … +  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams  …  › type to search - mouse on - timestamps footers - turn work fold - step icons auto - task column on - quick switch on - input history on - telemetry on - keep drafts on + google sign-in id not set + google sign-in secret not set + slack sign-in id not set + quiet before practice 20m + arrival brief after 4h + tenure after 3 clean firings  auto update • off - check for a new codeaf at launch and install it in the background. - - - - + keeps this codeaf installation current in the background. + background checks off + disable hints off + model in commits on + ssh reuse 300s + ssh heartbeat 3s + ssh missed heartbeats 3 + ssh traffic lowdelay diff --git a/docs/design/background-updates-frames/settings-auto-off-80c.png b/docs/design/background-updates-frames/settings-auto-off-80c.png new file mode 100644 index 0000000000000000000000000000000000000000..6c701ec438759d2e103ce5ce2cde657411c9cd18 GIT binary patch literal 52412 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● codeaf home teams chats sessions spend settings wed 11:08am + >● codeaf home teams chats sessions spend settings wed 11:37am ──────────────────────────────────────────────────────────────────────────────── Session Context Workspace Display Spending Safety Tasks Teams … › type to search - mouse on - timestamps footers - turn work fold - step icons auto - task column on - quick switch on - input history on - telemetry on - keep drafts on + google sign-in id not set + google sign-in secret not set + slack sign-in id not set + quiet before practice 20m + arrival brief after 4h + tenure after 3 clean firings auto update • off - check for a new codeaf at launch and install it in the background. - - - - + keeps this codeaf installation current in the background. + background checks off + disable hints off + model in commits on + ssh reuse 300s + ssh heartbeat 3s + ssh missed heartbeats 3 + ssh traffic lowdelay diff --git a/docs/design/background-updates-frames/settings-auto-on-120c.ansi b/docs/design/background-updates-frames/settings-auto-on-120c.ansi index 0714d9db71..e1504b763b 100644 --- a/docs/design/background-updates-frames/settings-auto-on-120c.ansi +++ b/docs/design/background-updates-frames/settings-auto-on-120c.ansi @@ -1,25 +1,25 @@ - >● codeaf  home teams chats sessions spend  settings  wed 11:08am + >● codeaf  home teams chats sessions spend  settings  wed 11:36am ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams   Providers   Connections  +  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams   Providers   Connections  › type to search - mouse on - timestamps footers - turn work fold - step icons auto - task column on - quick switch on - input history on - telemetry on - keep drafts on + google sign-in id not set + google sign-in secret not set + slack sign-in id not set + quiet before practice 20m + arrival brief after 4h + tenure after 3 clean firings  auto update on - check for a new codeaf at launch and install it in the background. - - - - + keeps this codeaf installation current in the background. + background checks off + disable hints off + model in commits on + ssh reuse 300s + ssh heartbeat 3s + ssh missed heartbeats 3 + ssh traffic lowdelay diff --git a/docs/design/background-updates-frames/settings-auto-on-120c.png b/docs/design/background-updates-frames/settings-auto-on-120c.png new file mode 100644 index 0000000000000000000000000000000000000000..c2abe64dabe7797505001c6bd3cd7ce365d3a144 GIT binary patch literal 57653 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

-8-c1cA5C&(WiaL`Qf>rnv2P6_3@fP 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 literal 0 HcmV?d00001 diff --git a/docs/design/background-updates-frames/settings-auto-on-120c.txt b/docs/design/background-updates-frames/settings-auto-on-120c.txt index ad7263273a..a95d5b3b75 100644 --- a/docs/design/background-updates-frames/settings-auto-on-120c.txt +++ b/docs/design/background-updates-frames/settings-auto-on-120c.txt @@ -1,25 +1,25 @@ - >● codeaf home teams chats sessions spend settings wed 11:08am + >● codeaf home teams chats sessions spend settings wed 11:36am ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Session Context Workspace Display Spending Safety Tasks Teams Providers Connections › type to search - mouse on - timestamps footers - turn work fold - step icons auto - task column on - quick switch on - input history on - telemetry on - keep drafts on + google sign-in id not set + google sign-in secret not set + slack sign-in id not set + quiet before practice 20m + arrival brief after 4h + tenure after 3 clean firings auto update on - check for a new codeaf at launch and install it in the background. - - - - + keeps this codeaf installation current in the background. + background checks off + disable hints off + model in commits on + ssh reuse 300s + ssh heartbeat 3s + ssh missed heartbeats 3 + ssh traffic lowdelay diff --git a/docs/design/background-updates-frames/settings-auto-on-80c.ansi b/docs/design/background-updates-frames/settings-auto-on-80c.ansi index 6f0f5c6333..d791c6a241 100644 --- a/docs/design/background-updates-frames/settings-auto-on-80c.ansi +++ b/docs/design/background-updates-frames/settings-auto-on-80c.ansi @@ -1,25 +1,25 @@ - >● codeaf  home teams chats sessions spend  settings  wed 11:07am + >● codeaf  home teams chats sessions spend  settings  wed 11:36am ──────────────────────────────────────────────────────────────────────────────── -  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams  … +  Session   Context   Workspace   Display   Spending   Safety   Tasks   Teams  …  › type to search - mouse on - timestamps footers - turn work fold - step icons auto - task column on - quick switch on - input history on - telemetry on - keep drafts on + google sign-in id not set + google sign-in secret not set + slack sign-in id not set + quiet before practice 20m + arrival brief after 4h + tenure after 3 clean firings  auto update on - check for a new codeaf at launch and install it in the background. - - - - + keeps this codeaf installation current in the background. + background checks off + disable hints off + model in commits on + ssh reuse 300s + ssh heartbeat 3s + ssh missed heartbeats 3 + ssh traffic lowdelay diff --git a/docs/design/background-updates-frames/settings-auto-on-80c.png b/docs/design/background-updates-frames/settings-auto-on-80c.png new file mode 100644 index 0000000000000000000000000000000000000000..21f3375c4e2f99c0626782f2afd3fb5b09a34518 GIT binary patch literal 52433 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 OB● codeaf home teams chats sessions spend settings wed 11:07am + >● codeaf home teams chats sessions spend settings wed 11:36am ──────────────────────────────────────────────────────────────────────────────── Session Context Workspace Display Spending Safety Tasks Teams … › type to search - mouse on - timestamps footers - turn work fold - step icons auto - task column on - quick switch on - input history on - telemetry on - keep drafts on + google sign-in id not set + google sign-in secret not set + slack sign-in id not set + quiet before practice 20m + arrival brief after 4h + tenure after 3 clean firings auto update on - check for a new codeaf at launch and install it in the background. - - - - + keeps this codeaf installation current in the background. + background checks off + disable hints off + model in commits on + ssh reuse 300s + ssh heartbeat 3s + ssh missed heartbeats 3 + ssh traffic lowdelay From 7561079f4577fab29593a870ccffd2be2e1b436f Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 11:44:02 -0400 Subject: [PATCH 09/12] docs: associate background update change entry with PR 1790 --- .../{1789-background-updates.md => 1790-background-updates.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/changes/unreleased/{1789-background-updates.md => 1790-background-updates.md} (99%) diff --git a/docs/changes/unreleased/1789-background-updates.md b/docs/changes/unreleased/1790-background-updates.md similarity index 99% rename from docs/changes/unreleased/1789-background-updates.md rename to docs/changes/unreleased/1790-background-updates.md index 9e8270f7a0..d31c99be1e 100644 --- a/docs/changes/unreleased/1789-background-updates.md +++ b/docs/changes/unreleased/1790-background-updates.md @@ -1,7 +1,7 @@ --- kind: changed title: launches offer a new codeaf in a quieter, non-blocking way, and installs run in the background -pr: 1789 +pr: 1790 surface: [chat] invalidates: - "`/update` used to refuse while a turn or task was running and then quit the program so the door could restart on the new build. It now installs in the background on the same road the automatic updater uses: nothing quits, nothing restarts, and a running turn or task is untouched. The new build is what opens the next time codeaf is started." From 263478b7f951d78292a7886f9788e3d8ddec0758 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 12:03:38 -0400 Subject: [PATCH 10/12] update: a named tag keeps its moment, and the grace waits under the sheets Four review findings on the background-update change (PR 1790). P2-1: a hand-named tag (or `update --version `) resolved without the API, so the install record it wrote carried no publish moment and a genuinely-newer SAME-DAY dev or staging build was then refused as if the disk build were newer. The shared installer now makes ONE best-effort, bounded `Client.TagPublished` lookup for a dev or staging tag whose moment is unknown - for the release being installed and for a dateless record already on disk - so a dateless record no longer blocks a candidate whose own moment proves it is newer. The lookup is skipped for stable and rc (release order is the tag itself) and for a deliberate named rollback, its failure is never fatal, and nothing is invented: when the facts cannot order two same-day builds the refusal says "their release order could not be confirmed" instead of claiming either is newer, and still names `/update ` as the escape. A dev or staging cache entry written without its moment is refreshed once (and the real timestamp persisted); when the API cannot answer, the cached offer still stands. `Select`'s pinned-tag promise is untouched. P2-2: the grace pause list omitted the model picker, the thinking ladder, the session picker (whose keys take the slot the countdown is read from), the folder chooser, and the phone tier's status sheet and tool detail. The countdown now waits under every one of them, and a test asserts the whole window is given back when it resumes and that the offer's own control stays on the row wherever the clock keeps running. P2-5: the change entry said the dismissal/failure state was keyed by release AND executable; it is per profile, and only the install lock is keyed by executable. The design note's pause list is corrected to match. P2-3/P2-4: `runtimePlatform` was dead and is removed. The door's detach-then-restart plumbing is RETAINED (the shared launch hands one empty plan to four doors and C7/C14 pin that road) with the false "the engine lane reads the same plan" comment replaced by what is actually true; see the summary for why it is kept rather than deleted. --- .../unreleased/1790-background-updates.md | 13 +- docs/design/background-updates.md | 20 +- internal/tui3/tui3.go | 8 +- internal/tui3/updateoffer.go | 29 +- internal/tui3/updateoffer_test.go | 85 +++++- internal/tui3/updateplatform.go | 15 - internal/update/auto.go | 55 +++- internal/update/check.go | 37 ++- internal/update/client.go | 41 +++ internal/update/install.go | 51 +++- internal/update/update_test.go | 283 ++++++++++++++++++ 11 files changed, 574 insertions(+), 63 deletions(-) delete mode 100644 internal/tui3/updateplatform.go diff --git a/docs/changes/unreleased/1790-background-updates.md b/docs/changes/unreleased/1790-background-updates.md index d31c99be1e..d8e5536bb8 100644 --- a/docs/changes/unreleased/1790-background-updates.md +++ b/docs/changes/unreleased/1790-background-updates.md @@ -44,9 +44,10 @@ question — and a skip the profile could not save says so rather than claiming durable answer it did not write. `update.auto`, `auto update` in `/settings`, is on by default. The dismissal and -the failure count live in one small file beside `config.json`, keyed by release -and by executable, so the same release never asks twice and a release that failed -three times stops being tried on its own. +the failure count live in one small PROFILE file beside `config.json`, keyed by +release, so the same release never asks twice and a release that failed three +times stops being tried on its own. (The install lock, not this file, is the one +keyed by the executable.) WHICH VERSION IS WHICH IS ON THE SCREEN. The offer's line names the release that is out; the build holding this session is the one `/status` reports (its `build` @@ -57,6 +58,10 @@ session changes, and the new build is what opens the next time codeaf is started on an older build. The automatic road moves forward only: a duplicate is a quiet no-op, an older build is refused unless a person names the tag, and two same-day dev or staging builds whose published moment is not known are refused rather than -guessed at. With `update.auto` off the launch check still runs and says the same +guessed at — with a sentence that says the order could not be confirmed rather +than calling either one newer. A dev or staging tag resolved without the API +looks its own publish moment up once, best-effort, so a hand-installed same-day +build is still ordered by fact instead of blocking the next one, and an offline +or unindexed tag installs without it. With `update.auto` off the launch check still runs and says the same two versions in one quiet line; `CODEAF_NO_UPDATE_CHECK=1` is what makes no request at all. diff --git a/docs/design/background-updates.md b/docs/design/background-updates.md index 3597f8e7dd..8812afeaf7 100644 --- a/docs/design/background-updates.md +++ b/docs/design/background-updates.md @@ -45,10 +45,14 @@ to its last key. The clock PAUSES wherever the offer cannot be read or answered: a question page, every place but home and the conversation (whose keys row is the offer's own — `/settings`, memory, standing, spend), the first-run sheet, a team's card, the -move picker, the switcher's menu, the wall, the provider panel, and the settings -panel's own value box and model picker. The list is `[app.key]`'s rung order -above the offer's chord, and a test pins its scope in both directions: a running -turn and a plain conversation MUST keep counting. +move picker, the switcher's menu, the wall, the provider panel, the settings +panel's own value box and model picker, the model picker and the thinking ladder +and the session picker (whose keys take the offer's slot in the conversation), +and the sheets that replace the frame whole — the folder chooser and, at phone +width, the status sheet and the tool detail. The list is the keyboard states that +take the keys slot or the frame away from the offer, and a test pins its scope in +both directions: a running turn and a plain conversation MUST keep counting, with +the offer's control on the keys row. The row is read LIVE. `/settings` updates the running window's flag, and a row turned off — or a release skipped — in ANOTHER window is seen because the @@ -127,7 +131,13 @@ person reads as unknown: Channel builds are ordered by the release's published moment, the same `CompareChannelBuilds` the launch check uses, so two same-day dev builds are -ordered by fact rather than by guess. +ordered by fact rather than by guess. A dev or staging tag a person named — which +`Select` answers without the API — gets ONE best-effort moment lookup before its +record is written, and a record that was written without one is looked up before +the ordering is decided, so a genuinely-newer same-day build is not blocked by a +dateless record. When no moment can be had the disk build is kept and the refusal +says the order **could not be confirmed** rather than calling it newer; a named +tag on the refusal line is the escape. ## What it refuses diff --git a/internal/tui3/tui3.go b/internal/tui3/tui3.go index 7f774fb1df..cb65d91a2f 100644 --- a/internal/tui3/tui3.go +++ b/internal/tui3/tui3.go @@ -469,9 +469,11 @@ type Options struct { // Restart is the plan a quitting surface used to fill for the door to read // after the terminal was back. NO INSTALL FILLS IT NOW: an install replaces // the file and the next launch opens it, so nothing on this surface quits for - // an update. The field is still handed in by the four doors because the - // engine lane reads the same plan, and it is empty on every road that does - // not restart. + // an update, and no code here reads the plan. It is handed in by the four + // doors so the shared launch keeps one empty plan rather than four, and it is + // RETIRED IN PLACE rather than deleted while the door's own detach-then- + // restart road (cmd/codeaf/chatv3.go's [finishChatRestart]) is still written + // and tested — see the change summary for why that road is kept. UpdateRunning string UpdateCurl string UpdateArgs []string diff --git a/internal/tui3/updateoffer.go b/internal/tui3/updateoffer.go index 9602dcbf4e..13f56358d1 100644 --- a/internal/tui3/updateoffer.go +++ b/internal/tui3/updateoffer.go @@ -413,10 +413,22 @@ func (a *app) offerTick() tea.Cmd { // that own every key: the settings sheet's value box and model picker, and the // provider panel's key box. // +// AND THE SHEETS THAT TAKE THE KEYS SLOT OR THE WHOLE FRAME ARE ON IT TOO. The +// three typed lists put their own sentence in the slot the countdown is READ +// from — [app.hintWord] outranks [app.updateOfferHint], so the release and the +// seconds are not drawn and only the defer clause survives beside them — the +// folder chooser shades the frame it keeps underneath (view.go's +// [app.contextModalShowing]), and the phone tier's status sheet and tool detail +// replace the frame outright. In all of them the countdown cannot be watched, +// and the clock WAITS rather than running down out of sight. A door added beside +// these should ask its own question here. +// // A RUNNING TURN IS NOT ON THIS LIST. There the offer's defer clause is drawn // under the turn's own keys and its chord still works, which is exactly where // the countdown must keep running (steer.go's [app.hintShorter] keeps that -// clause while it does). +// clause while it does): keeping work moving and staying current at the same +// time is the whole reason the grace exists. The list is the states that TAKE +// the slot away from the running conversation, not the conversation's own work. func (a *app) updateOfferHidden() bool { if a.questionRoomOpen() || a.setup.open || a.railPlanPending.id != "" { return true @@ -431,6 +443,21 @@ func (a *app) updateOfferHidden() bool { if pl := a.showing(); pl != nil && !pl.showsOffer(a) { return true } + // THE INPUT LINE'S OWN OVERLAYS take the keys slot with their own sentence: + // the model picker, the thinking ladder and the session picker all answer + // through [app.hintWord], which outranks [app.updateOfferHint] — so the + // release and the seconds are replaced, and only the defer clause rides + // beside their keys. + if a.pick.open || a.effPick.open || a.roster.open { + return true + } + // AND THE FRAME-TAKING SHEETS: the context chooser draws over what stays + // underneath, and the phone tier's status sheet and tool detail are pages of + // their own ([app.contextModalShowing], [app.deckShowing], + // [app.expandShowing]). + if a.contextModalShowing() || a.deckShowing() || a.expandShowing() { + return true + } return a.sheetLayerOwnsKeys() || a.addPanel.open } diff --git a/internal/tui3/updateoffer_test.go b/internal/tui3/updateoffer_test.go index 9965dcd9b4..9c9a66a1e4 100644 --- a/internal/tui3/updateoffer_test.go +++ b/internal/tui3/updateoffer_test.go @@ -538,6 +538,14 @@ func TestTheGraceWaitsWhereTheOfferCannotBeAnswered(t *testing.T) { {"the team menu", func(a *app) { a.teamMenu.on = true }}, {"the provider panel", func(a *app) { a.addPanel.open = true }}, {"a question page", func(a *app) { a.qroom = &questionRoom{} }}, + // THE SHEETS THE FOOT REPLACES THE OFFER'S SENTENCE WITH, or that take + // the frame whole (the phone tier's status sheet and tool detail). + {"the model picker", func(a *app) { a.pick.open = true }}, + {"the thinking ladder", func(a *app) { a.effPick.open = true }}, + {"the session picker", func(a *app) { a.roster.open = true }}, + {"the folder chooser", func(a *app) { a.folder.open = true }}, + {"the status sheet", func(a *app) { a.width = phoneWidth; a.deck.open = true }}, + {"the tool detail", func(a *app) { a.width = phoneWidth; a.expand.open = true }}, } for _, row := range hidden { t.Run("waits under "+row.name, func(t *testing.T) { @@ -549,8 +557,11 @@ func TestTheGraceWaitsWhereTheOfferCannotBeAnswered(t *testing.T) { if cmd := a.tookOfferTick(); cmd == nil || !a.offer.offering() { t.Fatalf("the grace was spent under %s: cmd = %v offer = %+v", row.name, cmd, a.offer) } - if !a.offer.deadline.After(a.now()) { - t.Fatalf("the deadline was not moved on under %s: %s", row.name, a.offer.deadline) + // THE GRACE IS GIVEN BACK WHOLE, not merely kept alive: the clock + // restarts at the full window from the moment the offer is readable + // again, so a person who leaves the sheet gets the whole countdown. + if left := a.offer.deadline.Sub(a.now()); left <= 0 || left > codeupdate.AutoGrace || left < codeupdate.AutoGrace-time.Second { + t.Fatalf("the grace did not restart under %s: %s left, want about %s", row.name, left, codeupdate.AutoGrace) } if lab.installed != 0 { t.Fatalf("%s installed a release", row.name) @@ -560,10 +571,11 @@ func TestTheGraceWaitsWhereTheOfferCannotBeAnswered(t *testing.T) { counting := []struct { name string seat func(*app) + keys func(*app, int) string }{ - {"the conversation", func(a *app) {}}, - {"home", func(a *app) { a.raisePlace(pageHome) }}, - {"a running turn", func(a *app) { a.state = stateWorking; a.turn = 1; a.input.setText("work") }}, + {"the conversation", func(a *app) {}, func(a *app, width int) string { return a.footHint(width) }}, + {"home", func(a *app) { a.raisePlace(pageHome) }, func(a *app, width int) string { return a.homeFootLine(width, a.pal) }}, + {"a running turn", func(a *app) { a.state = stateWorking; a.turn = 1; a.input.setText("work") }, func(a *app, width int) string { return a.footHint(width) }}, } for _, row := range counting { t.Run("counts over "+row.name, func(t *testing.T) { @@ -571,6 +583,12 @@ func TestTheGraceWaitsWhereTheOfferCannotBeAnswered(t *testing.T) { a := lab.app a.offer.raise("v0.9.3", "v0.9.2", a.now()) row.seat(a) + // THE CONTROL STAYS ON THE ROW WHERE THE CLOCK KEEPS RUNNING: a + // countdown that cannot be skipped is a countdown a person cannot + // answer, which is the whole reason the pause list exists. + if hint := row.keys(a, 200); !strings.Contains(hint, "skip") { + t.Fatalf("the offer's control left the keys row over %s: %q", row.name, hint) + } a.offer.deadline = a.now().Add(-time.Second) if cmd := a.tookOfferTick(); cmd == nil || !a.updateInFlight { t.Fatalf("the deadline did not fire over %s: cmd = %v in flight = %t", row.name, cmd, a.updateInFlight) @@ -879,3 +897,60 @@ func TestASameDayDevWindowDoesNotStepBackTheBuildAnotherWindowInstalled(t *testi t.Fatalf("a refusal was counted as a failure: %+v", state) } } + +// TestATypedNamedTagRecordsTheLookedUpPublishMoment proves the MANUAL named road +// end to end through the surface's own wiring: `/update ` resolves a +// PINNED tag without the API, the shared installer looks the tag's publish +// moment up once (best-effort) and writes it into the record \u2014 which is the fact +// a later same-day channel update needs to move forward safely. +func TestATypedNamedTagRecordsTheLookedUpPublishMoment(t *testing.T) { + const running = "dev-20261007-aaaaaaaaaaaa" + publishedAt := time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC) + asset := []byte("dev early bytes") + digest := sha256.Sum256(asset) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + switch { + case strings.Contains(request.URL.Path, "/releases/tags/"): + fmt.Fprintf(w, `{"tag_name":%q,"published_at":%q}`, running, publishedAt.Format(time.RFC3339)) + case strings.HasSuffix(request.URL.Path, "/checksums.txt"): + fmt.Fprintf(w, "%x codeaf-%s-%s\n", digest, runtime.GOOS, runtime.GOARCH) + case strings.Contains(request.URL.Path, "/releases/download/"): + _, _ = w.Write(asset) + default: + http.NotFound(w, request) + } + })) + defer server.Close() + client := &codeupdate.Client{HTTP: server.Client(), APIBase: server.URL, DownloadBase: server.URL, Revision: running} + target := filepath.Join(t.TempDir(), "codeaf") + + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + a := lab.app + a.updateRunning = running + a.resolveUpdate = client.Select + a.installUpdate = func(install context.Context, options codeupdate.InstallOptions) (codeupdate.InstallResult, error) { + options.Client = client + options.Target = target + return codeupdate.Install(install, options) + } + command := a.runUpdateCommand(running) + if command == nil { + t.Fatal("the typed named tag was refused") + } + drive(t, a, command()) + if a.updateInFlight { + t.Fatal("the named install never finished") + } + if !strings.Contains(updateNotes(a), "checksum matched") { + t.Fatalf("the manual completion was not said:\n%s", updateNotes(a)) + } + lock, err := codeupdate.TryTargetLock(target) + if err != nil { + t.Fatal(err) + } + record, ok := lock.Record() + lock.Release() + if !ok || record.Tag != running || !record.PublishedAt.Equal(publishedAt) { + t.Fatalf("the typed install recorded %+v, ok = %t; want the looked-up moment %s", record, ok, publishedAt) + } +} diff --git a/internal/tui3/updateplatform.go b/internal/tui3/updateplatform.go deleted file mode 100644 index cb916a28c7..0000000000 --- a/internal/tui3/updateplatform.go +++ /dev/null @@ -1,15 +0,0 @@ -package tui3 - -import "runtime" - -var ( - runtimeGOOS = runtime.GOOS - runtimeGOARCH = runtime.GOARCH -) - -// runtimePlatform names the build a release download would fetch, as the -// installer spells it (`linux/amd64`). It is one function because the offer's -// line and its note must spell the platform identically. -func runtimePlatform() string { - return runtimeGOOS + "/" + runtimeGOARCH -} diff --git a/internal/update/auto.go b/internal/update/auto.go index fa3cc82f85..7d5bc820a3 100644 --- a/internal/update/auto.go +++ b/internal/update/auto.go @@ -510,49 +510,72 @@ func sha256File(path string) (string, error) { return hex.EncodeToString(sum.Sum(nil)), nil } +// channelBuildNeedsMoment reports whether a tag carries the dev or staging +// channel whose BUILDS are ordered by their publish moment. Stable and release +// candidate tags carry their own order, so nothing has to be looked up for them. +func channelBuildNeedsMoment(tag string) bool { + channel := Kind(tag) + return channel == "dev" || channel == "staging" +} + // recordSupersedes reports whether the build on disk is newer than the release -// this process resolved. It uses PUBLISHED ordering where the facts exist — the -// same CompareChannelBuilds the launch check uses — and falls back to the tag +// this process resolved. It is [recordCompare]'s verdict alone, and it is the +// installer's guard against a stale caller stepping the file back. +func recordSupersedes(record installRecord, release Release) bool { + supersedes, _ := recordCompare(record, release) + return supersedes +} + +// recordCompare orders the build on disk against the release this process +// resolved, and it says whether that answer is a FACT or the conservative +// same-day tie. It uses PUBLISHED ordering where the facts exist — the same +// CompareChannelBuilds the launch check uses — and falls back to the tag // grammar only when it must; two tags it cannot order are not guessed at. // // TWO SAME-DAY CHANNEL BUILDS WITH AN UNKNOWN MOMENT ARE ONE OF THE TAGS IT // CANNOT ORDER. The date inside them is equal and says nothing about which came // second, so the permissive answer — proceed — would step a file another // window advanced back to a stale same-day dev or staging build. The build on -// disk therefore wins; a person who means to roll back names the tag, which -// sets [InstallOptions.AllowDowngrade] and never comes through here for an -// answer. -func recordSupersedes(record installRecord, release Release) bool { +// disk therefore wins (supersedes) while certain is FALSE, so the refusal can +// say what is really known instead of claiming the disk build is newer. A +// person who means to roll back names the tag, which sets +// [InstallOptions.AllowDowngrade] and never comes through here for an answer. +// +// THE INSTALLER FILLS THE MOMENTS BEFORE IT ASKS, with ONE best-effort +// [Client.TagPublished] lookup for a tag whose moment is unknown, so a +// same-day candidate that really is newer is not blocked by a record written +// before the moment was kept (install.go). +func recordCompare(record installRecord, release Release) (supersedes, certain bool) { installed, candidate := strings.TrimSpace(record.Tag), strings.TrimSpace(release.Tag) if installed == "" || candidate == "" { - return false + return false, false } if installed == candidate { - return true + return true, true } installedChannel, installedDate, installedOK := ParseChannel(installed) candidateChannel, candidateDate, candidateOK := ParseChannel(candidate) if installedOK && candidateOK && installedChannel == candidateChannel { if !record.PublishedAt.IsZero() && !release.PublishedAt.IsZero() { comparison, ok := CompareChannelBuilds(installed, record.PublishedAt, candidate, release.PublishedAt) - return ok && comparison > 0 + return ok && comparison > 0, ok } if installedDate != candidateDate { // THE DATE IS A FACT WHEN THEY DIFFER: the older-dated build is the // older one whatever the clock said on the day. - return installedDate > candidateDate + return installedDate > candidateDate, true } // SAME DAY, AND ONE OF THE TWO MOMENTS IS UNKNOWN. There is no fact // left to order them with, and guessing would be permissive in exactly - // the case this refusal exists for ([codeupdate.Choice.Published] is - // how the pinned road hands the moment over, so a correct install - // carries it). - return true + // the case this refusal exists for ([codeupdate.Choice.Published] and + // [Client.TagPublished] are how the pinned and named roads hand the + // moment over). + return true, false } if comparison, ok := CompareSemverTags(installed, candidate); ok { - return comparison > 0 + return comparison > 0, true } - return false + return false, false } // InstallRefusal names why this target may not be replaced in place — a package diff --git a/internal/update/check.go b/internal/update/check.go index 6c193fa29f..3fa701c902 100644 --- a/internal/update/check.go +++ b/internal/update/check.go @@ -109,21 +109,44 @@ func CheckLaunch(ctx context.Context, options CheckOptions) (Available, bool) { now = options.Now } path := config.ProfilePath(options.ProfileDir, cacheName) - if cached, ok := loadCheckCache(path); ok && cached.Running == running { + cached, cachedOK := loadCheckCache(path) + cachedForRunning := cachedOK && cached.Running == running + if cachedForRunning { age := now().Sub(cached.CheckedAt) - if age >= 0 && age < lifetime { - answer := Available{ - Latest: cached.Latest, Running: running, Curl: curl, - LatestPublished: cached.LatestPublished, RunningPublished: cached.RunningPublished, - } - return answer, answer.Newer() + cachedForRunning = age >= 0 && age < lifetime + } + // A DEV OR STAGING ANSWER IS ONLY AS GOOD AS ITS MOMENT. That is the one + // channel whose comparison orders two BUILDS BY THE MOMENT rather than by + // the tag, so a cache a pre-PR codeaf wrote — or any answer that arrived + // without a timestamp — cannot order a same-day pair at all. It is + // REFRESHED rather than reused whole: an entry with no fact behind it must + // not decide the offer for the rest of its lifetime. The stable and rc + // channels order by the tag itself, so a missing moment costs them nothing + // and they keep their cache untouched. + needsMoment := (channel == "dev" || channel == "staging") && cached.LatestPublished.IsZero() + if cachedForRunning && !needsMoment { + answer := Available{ + Latest: cached.Latest, Running: running, Curl: curl, + LatestPublished: cached.LatestPublished, RunningPublished: cached.RunningPublished, } + return answer, answer.Newer() } if options.Client == nil { return Available{}, false } release, err := options.Client.Check(ctx, Choice{Channel: channel, Running: running}) if err != nil { + // THE REFRESH IS BEST-EFFORT. A release the API cannot answer for must + // not turn a known offer into no offer at all: a usable cache is still + // the answer, moment or no moment, and the installer resolves the + // moment it needs when it writes the record. + if cachedForRunning { + answer := Available{ + Latest: cached.Latest, Running: running, Curl: curl, + LatestPublished: cached.LatestPublished, RunningPublished: cached.RunningPublished, + } + return answer, answer.Newer() + } return Available{}, false } answer := Available{ diff --git a/internal/update/client.go b/internal/update/client.go index e8f8174ddd..7d91acc00a 100644 --- a/internal/update/client.go +++ b/internal/update/client.go @@ -366,6 +366,47 @@ func (c *Client) Select(ctx context.Context, choice Choice) (Release, error) { return Release{}, errors.New("no release repository answered") } +// TagPublished asks the release API for ONE exact tag's publish moment, and it +// is BEST-EFFORT BY CONTRACT. The moment is what the install record carries and +// what orders two same-day dev or staging builds ([recordSupersedes]), so a +// NAMED tag — which [Select] answers without the API — is worth one bounded +// lookup before its install writes that record. A tag the API cannot answer for +// — offline, rate-limited, deleted, or a release the API has not indexed — +// leaves the moment UNKNOWN, which is never guessed at and never fails the +// install: the file is still there to fetch, and a person naming it means it. +func (c *Client) TagPublished(ctx context.Context, tag string) (time.Time, bool) { + tag = strings.TrimSpace(tag) + if c == nil || tag == "" || !installNamePattern.MatchString(tag) { + return time.Time{}, false + } + // AN EMPTY API BASE IS NO API TO ASK. A caller that built a client for + // downloads alone (or a test's stub) reads as unknown rather than as a + // relative address nothing can serve. + if strings.TrimSpace(c.APIBase) == "" { + return time.Time{}, false + } + for index, repository := range []string{primaryRepository, legacyRepository} { // legacy-name + rawURL := strings.TrimRight(c.APIBase, "/") + "/repos/" + repository + "/releases/tags/" + url.PathEscape(tag) + body, err := c.get(ctx, rawURL, "application/vnd.github+json", true, "release API") + if err != nil { + if isStatus(err, http.StatusNotFound) && index == 0 { + continue + } + return time.Time{}, false + } + var row apiRelease + if json.Unmarshal(body, &row) != nil || strings.TrimSpace(row.TagName) != tag { + return time.Time{}, false + } + moment := row.stamp() + if moment.IsZero() { + return time.Time{}, false + } + return moment, true + } + return time.Time{}, false +} + func (c *Client) selectRepository(ctx context.Context, repository, channel, running string) (Release, error) { suffix := "releases/latest" if channel != "stable" { diff --git a/internal/update/install.go b/internal/update/install.go index 07b7dab417..7abb701004 100644 --- a/internal/update/install.go +++ b/internal/update/install.go @@ -97,14 +97,51 @@ func Install(ctx context.Context, options InstallOptions) (InstallResult, error) // UNDER THE LOCK, WHAT IS ON DISK IS WHAT COUNTS. The record is trusted only // while the file still hashes to it, so an externally replaced binary reads // as unknown and a candidate newer than it is not refused on a stale fact. - if record, ok := lock.Record(); ok { - if record.Tag == release.Tag { - // ALREADY THERE IS SUCCESS. Nothing is downloaded, nothing is - // written, and the caller is told so it can stay quiet about it. - return InstallResult{Release: release, Path: target, Already: true}, nil + record, recorded := lock.Record() + if recorded && record.Tag == release.Tag { + // ALREADY THERE IS SUCCESS. Nothing is downloaded, nothing is written, + // and the caller is told so it can stay quiet about it. + return InstallResult{Release: release, Path: target, Already: true}, nil + } + // ONE BEST-EFFORT MOMENT FOR THE RELEASE BEING INSTALLED, AND ONLY WHERE IT + // IS THE FACT THAT ORDERS: a NAMED dev or staging tag is resolved without + // the API ([Client.Select]), so the record written from it would carry no + // moment at all — and a same-day channel build whose record is dateless + // blocks the next genuinely-newer one for the rest of the day + // ([recordCompare]). Stable and rc releases order by the tag itself, so they + // ask the release API for nothing. The lookup is bounded by the client's api + // window and its failure is not fatal: an unknown moment is never guessed + // at, and a person naming a tag still gets that exact file. + if release.PublishedAt.IsZero() && channelBuildNeedsMoment(release.Tag) { + if moment, found := options.Client.TagPublished(ctx, release.Tag); found { + release.PublishedAt = moment } - if recordSupersedes(record, release) && !options.AllowDowngrade { - return InstallResult{}, &RefusalError{Reason: "codeaf " + record.Tag + " is already the build on disk, newer than " + release.Tag} + } + if recorded && !options.AllowDowngrade { + // AND ONE FOR THE RECORD, for the same reason read the other way: a + // record a pre-PR codeaf or an offline install wrote without a moment + // must not block a candidate whose own moment proves it is newer. The + // lookup is skipped when the person deliberately named a tag — a rollback + // is not a question about which build is ahead — and when the two tags + // are not one channel pair, because only a same-channel pair is ordered + // by the moment at all. + if record.PublishedAt.IsZero() && channelBuildNeedsMoment(record.Tag) && Kind(record.Tag) == Kind(release.Tag) { + if moment, found := options.Client.TagPublished(ctx, record.Tag); found { + record.PublishedAt = moment + } + } + if supersedes, certain := recordCompare(record, release); supersedes { + reason := "codeaf " + record.Tag + " is already the build on disk" + if certain { + reason += ", newer than " + release.Tag + } else { + // NEVER CLAIM AN ORDER THE FACTS DO NOT CARRY, and never blame + // one side for a moment nobody has: the two builds share a day + // and their order could not be confirmed, so the disk build is + // kept without calling it newer. + reason += " · their release order could not be confirmed, so " + release.Tag + " was not written over it · /update " + release.Tag + " installs that exact tag" + } + return InstallResult{}, &RefusalError{Reason: reason} } } var ( diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 7f00f55d8d..c34e8b9e8c 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "encoding/hex" + "errors" "fmt" "net/http" "net/http/httptest" @@ -957,3 +958,285 @@ func TestSelectingAPinnedTagCarriesTheCallersPublishMoment(t *testing.T) { t.Fatalf("unnamed moment = %+v, %v", blank, err) } } + +// sameDayFixture is one channel build a test server serves: the bytes an install +// writes and the moment the release API reports for the tag. +type sameDayFixture struct { + asset []byte + at time.Time +} + +// sameDayReleaseServer serves one channel's release list, each tag's metadata and +// each tag's asset and checksums, so a test can drive the REAL [Client.Select] +// and [Install] end to end rather than hand-built releases. +func sameDayReleaseServer(t *testing.T, fixtures map[string]sameDayFixture) *httptest.Server { + t.Helper() + tagOf := func(path string) string { + _, rest, _ := strings.Cut(path, "/download/") + tag, _, _ := strings.Cut(rest, "/") + return tag + } + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.Contains(r.URL.Path, "/releases/tags/"): + tag := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] + fixture, ok := fixtures[tag] + if !ok { + http.NotFound(w, r) + return + } + fmt.Fprintf(w, `{"tag_name":%q,"published_at":%q}`, tag, fixture.at.Format(time.RFC3339)) + case strings.HasSuffix(r.URL.Path, "/releases"): + rows := make([]string, 0, len(fixtures)) + for tag, fixture := range fixtures { + rows = append(rows, fmt.Sprintf(`{"tag_name":%q,"published_at":%q}`, tag, fixture.at.Format(time.RFC3339))) + } + fmt.Fprint(w, "["+strings.Join(rows, ",")+"]") + case strings.HasSuffix(r.URL.Path, "/checksums.txt"): + fixture, ok := fixtures[tagOf(r.URL.Path)] + if !ok { + http.NotFound(w, r) + return + } + sum := sha256.Sum256(fixture.asset) + fmt.Fprintf(w, "%x codeaf-%s-%s\n", sum, runtime.GOOS, runtime.GOARCH) + case strings.Contains(r.URL.Path, "/releases/download/"): + fixture, ok := fixtures[tagOf(r.URL.Path)] + if !ok { + http.NotFound(w, r) + return + } + _, _ = w.Write(fixture.asset) + default: + http.NotFound(w, r) + } + })) +} + +// TestAHandInstalledSameDayBuildKeepsTheChannelMovingForward proves the P2-1 fix +// through the REAL resolver and installer: a PERSON naming a same-day dev tag +// writes an install record that carries that tag's publish moment (the one +// best-effort lookup the installer makes), so the next genuinely-newer same-day +// channel build is installed rather than refused as if the disk build were +// newer, and the stale reverse is still refused \u2014 WITH the fact that says so. +func TestAHandInstalledSameDayBuildKeepsTheChannelMovingForward(t *testing.T) { + const ( + earlyDev = "dev-20261007-aaaaaaaaaaaa" + lateDev = "dev-20261007-bbbbbbbbbbbb" + ) + earlyAt := time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC) + lateAt := earlyAt.Add(2 * time.Hour) + earlyBytes, lateBytes := []byte("dev early bytes"), []byte("dev late bytes") + server := sameDayReleaseServer(t, map[string]sameDayFixture{ + earlyDev: {asset: earlyBytes, at: earlyAt}, + lateDev: {asset: lateBytes, at: lateAt}, + }) + defer server.Close() + client := releaseClient(server, "dev-20261006-000000000000") + target := filepath.Join(t.TempDir(), "codeaf") + + // A PERSON NAMES THE EARLIER TAG. Select answers a pinned tag WITHOUT the + // API, so the moment the record needs can only come from the installer. + named, err := client.Select(context.Background(), Choice{Version: earlyDev}) + if err != nil { + t.Fatal(err) + } + if !named.PublishedAt.IsZero() { + t.Fatalf("the pinned select invented a moment: %s", named.PublishedAt) + } + if _, err := Install(context.Background(), InstallOptions{ + Client: client, Release: named, Target: target, AllowDowngrade: true, + }); err != nil { + t.Fatalf("the named install: %v", err) + } + lock, err := TryTargetLock(target) + if err != nil { + t.Fatal(err) + } + record, ok := lock.Record() + lock.Release() + if !ok || record.Tag != earlyDev { + t.Fatalf("record = %+v, ok = %t", record, ok) + } + if !record.PublishedAt.Equal(earlyAt) { + t.Fatalf("the named install recorded %s, want the looked-up moment %s", record.PublishedAt, earlyAt) + } + + // THE AUTOMATIC ROAD THEN FINDS THE NEWER SAME-DAY BUILD, and the record's + // moment orders it as older, so the candidate installs. + candidate, err := client.Select(context.Background(), Choice{Channel: "dev", Running: earlyDev}) + if err != nil { + t.Fatal(err) + } + if candidate.Tag != lateDev || !candidate.PublishedAt.Equal(lateAt) { + t.Fatalf("candidate = %+v", candidate) + } + if _, err := Install(context.Background(), InstallOptions{ + Client: client, Release: candidate, Target: target, + }); err != nil { + t.Fatalf("the same-day forward install was refused: %v", err) + } + if onDisk, err := os.ReadFile(target); err != nil || string(onDisk) != string(lateBytes) { + t.Fatalf("on disk after the forward install = %q, %v", onDisk, err) + } + + // AND THE STALE REVERSE IS REFUSED, naming the fact it really has. + _, err = Install(context.Background(), InstallOptions{ + Client: client, + Release: Release{Tag: earlyDev, Repository: primaryRepository, PublishedAt: earlyAt}, + Target: target, + }) + var refusal *RefusalError + if !errors.As(err, &refusal) || !strings.Contains(refusal.Reason, "newer than "+earlyDev) { + t.Fatalf("stale reverse = %v", err) + } + if onDisk, err := os.ReadFile(target); err != nil || string(onDisk) != string(lateBytes) { + t.Fatalf("the stale reverse wrote the disk: %q, %v", onDisk, err) + } +} + +// TestANamedTagInstallsOfflineAndSaysUnknownRatherThanNewer proves the second +// half of P2-1: a NAMED exact tag installs when the release API cannot answer at +// all (the lookup is best-effort and never blocks the asset), the record it +// writes is honestly moment-less, and a later same-day candidate is then kept +// back with a sentence that says the moment is unknown instead of claiming the +// disk build is newer. +func TestANamedTagInstallsOfflineAndSaysUnknownRatherThanNewer(t *testing.T) { + const ( + earlyDev = "dev-20261007-aaaaaaaaaaaa" + lateDev = "dev-20261007-bbbbbbbbbbbb" + ) + asset := []byte("dev early bytes") + digest := sha256.Sum256(asset) + server, _ := servedRelease(t, asset, hex.EncodeToString(digest[:])) + defer server.Close() + // THE API IS UNREACHABLE AND THE DOWNLOAD HOST IS NOT: the lookup is the + // only thing that fails. + client := &Client{HTTP: server.Client(), APIBase: "http://127.0.0.1:1", DownloadBase: server.URL, Revision: earlyDev} + target := filepath.Join(t.TempDir(), "codeaf") + + release, err := client.Select(context.Background(), Choice{Version: earlyDev}) + if err != nil { + t.Fatal(err) + } + if _, err := Install(context.Background(), InstallOptions{ + Client: client, Release: release, Target: target, AllowDowngrade: true, + }); err != nil { + t.Fatalf("an offline named install failed: %v", err) + } + lock, err := TryTargetLock(target) + if err != nil { + t.Fatal(err) + } + record, ok := lock.Record() + lock.Release() + if !ok || record.Tag != earlyDev || !record.PublishedAt.IsZero() { + t.Fatalf("offline record = %+v, ok = %t; the moment must be honestly unknown", record, ok) + } + + // THE NEXT SAME-DAY CANDIDATE IS KEPT, AND SAID TO BE UNKNOWN-NOT-NEWER. + lateAt := time.Date(2026, 10, 7, 11, 0, 0, 0, time.UTC) + _, err = Install(context.Background(), InstallOptions{ + Client: client, + Release: Release{Tag: lateDev, Repository: primaryRepository, PublishedAt: lateAt}, + Target: target, + }) + var refusal *RefusalError + if !errors.As(err, &refusal) { + t.Fatalf("the unknown-moment candidate = %v, want a refusal", err) + } + if strings.Contains(refusal.Reason, "newer than") || !strings.Contains(refusal.Reason, "release order could not be confirmed") { + t.Fatalf("the refusal claimed an order it does not have: %q", refusal.Reason) + } + // THE EXPLICIT NAMED-TAG ESCAPE STAYS ON THE SENTENCE: the way to install + // the release a person means is named rather than left to be guessed. + if !strings.Contains(refusal.Reason, "/update "+lateDev) { + t.Fatalf("the refusal left off the named escape: %q", refusal.Reason) + } + if onDisk, err := os.ReadFile(target); err != nil || string(onDisk) != string(asset) { + t.Fatalf("the unknown-moment candidate wrote the disk: %q, %v", onDisk, err) + } +} + +// TestALegacyDevCacheWithoutItsMomentIsRefreshedAndStillOffersOffline proves the +// check-cache half of P2-1. A dev cache written before the moment was kept +// cannot order two same-day builds, so it is refreshed once and the REAL +// timestamp is persisted; a stable cache is left alone because its channel +// orders by the tag; and when the API cannot answer the cached offer still +// stands rather than vanishing. +func TestALegacyDevCacheWithoutItsMomentIsRefreshedAndStillOffersOffline(t *testing.T) { + const ( + runningDev = "dev-20261007-aaaaaaaaaaaa" + latestDev = "dev-20261007-bbbbbbbbbbbb" + ) + publishedAt := time.Date(2026, 10, 7, 11, 0, 0, 0, time.UTC) + now := publishedAt.Add(time.Minute) + var hits atomic.Int64 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + hits.Add(1) + fmt.Fprintf(w, `[{"tag_name":%q,"published_at":%q},{"tag_name":%q,"published_at":%q}]`, + runningDev, publishedAt.Add(-2*time.Hour).Format(time.RFC3339), + latestDev, publishedAt.Format(time.RFC3339)) + })) + defer server.Close() + client := releaseClient(server, runningDev) + clock := func() time.Time { return now } + + // A DEV CACHE A PRE-PR CODEAF WROTE: the latest tag and the checked moment, + // no published moment. + profile := t.TempDir() + path := filepath.Join(profile, "update-check.dev.json") + if err := saveCheckCache(path, checkCache{CheckedAt: now.Add(-time.Minute), Latest: latestDev, Running: runningDev}); err != nil { + t.Fatal(err) + } + answer, show := CheckLaunch(context.Background(), CheckOptions{Running: runningDev, ProfileDir: profile, Client: client, Now: clock}) + if !show || answer.Latest != latestDev { + t.Fatalf("the refreshed answer = %+v, %t", answer, show) + } + if !answer.LatestPublished.Equal(publishedAt) { + t.Fatalf("the refreshed answer carried moment %s, want %s", answer.LatestPublished, publishedAt) + } + if hits.Load() != 1 { + t.Fatalf("the metadata-less dev cache made %d requests, want the one refresh", hits.Load()) + } + persisted, ok := loadCheckCache(path) + if !ok || !persisted.LatestPublished.Equal(publishedAt) { + t.Fatalf("the refresh did not persist the timestamp: %+v, %t", persisted, ok) + } + // THE SECOND LAUNCH USES THE REFRESHED CACHE, which now carries the fact. + if _, show := CheckLaunch(context.Background(), CheckOptions{Running: runningDev, ProfileDir: profile, Client: client, Now: clock}); !show { + t.Fatal("the refreshed cache did not answer") + } + if hits.Load() != 1 { + t.Fatalf("a cache WITH its moment still asked %d times", hits.Load()) + } + + // A STABLE CACHE WITHOUT A MOMENT IS NOT REFRESHED: stable orders by tag. + stableProfile := t.TempDir() + stablePath := filepath.Join(stableProfile, "update-check.json") + if err := saveCheckCache(stablePath, checkCache{CheckedAt: now.Add(-time.Minute), Latest: "v0.2.0", Running: "v0.1.0"}); err != nil { + t.Fatal(err) + } + before := hits.Load() + stableAnswer, stableShow := CheckLaunch(context.Background(), CheckOptions{Running: "v0.1.0", ProfileDir: stableProfile, Client: client, Now: clock}) + if !stableShow || stableAnswer.Latest != "v0.2.0" { + t.Fatalf("the stable cache answer = %+v, %t", stableAnswer, stableShow) + } + if hits.Load() != before { + t.Fatalf("a stable cache asked the API %d times", hits.Load()-before) + } + + // AND WHEN THE API CANNOT ANSWER, THE OFFER STANDS. The unreachable API + // makes the refresh fail; the moment stays unknown and the installer will + // resolve it when it writes the record. + offline := &Client{HTTP: server.Client(), APIBase: "http://127.0.0.1:1", DownloadBase: server.URL, Revision: runningDev} + offlineProfile := t.TempDir() + if err := saveCheckCache(filepath.Join(offlineProfile, "update-check.dev.json"), + checkCache{CheckedAt: now.Add(-time.Minute), Latest: latestDev, Running: runningDev}); err != nil { + t.Fatal(err) + } + offlineAnswer, offlineShow := CheckLaunch(context.Background(), CheckOptions{Running: runningDev, ProfileDir: offlineProfile, Client: offline, Now: clock}) + if !offlineShow || offlineAnswer.Latest != latestDev { + t.Fatalf("the offline fallback lost the offer: %+v, %t", offlineAnswer, offlineShow) + } +} From 63cb1b347e94f9471f9079e9f4c40f4dccbc8b31 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 12:21:55 -0400 Subject: [PATCH 11/12] update: the offer's clock waits behind the conversation-delete confirmation Rebase integration fix (PR 1750 Teams brought in the conversation-delete sheet). Its key handler is read at the top of the loop, before updateOfferKey, so alt+n never reaches the offer while the confirmation stands and the countdown could run down and install behind a sheet the person cannot answer from. updateOfferHidden now pauses on a.cdelete.on. The existing grace table gains the sheet, asserting the grace does not fire, the full window is given back and nothing installs; a new focused test drives alt+n with the sheet up to pin the premise that the loop eats the chord. The design note's pause list names the confirmation. No other code changed. Focused checks on Spark: go test ./internal/tui3/ -run 'Update|Offer|Grace|Delete' and ./internal/namelaw ./internal/e2e - ok; gofmt/vet/build clean. --- docs/design/background-updates.md | 19 +++++++++-------- internal/tui3/updateoffer.go | 8 ++++++++ internal/tui3/updateoffer_test.go | 34 +++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/docs/design/background-updates.md b/docs/design/background-updates.md index 8812afeaf7..35dec228d6 100644 --- a/docs/design/background-updates.md +++ b/docs/design/background-updates.md @@ -44,15 +44,16 @@ to its last key. The clock PAUSES wherever the offer cannot be read or answered: a question page, every place but home and the conversation (whose keys row is the offer's own — -`/settings`, memory, standing, spend), the first-run sheet, a team's card, the -move picker, the switcher's menu, the wall, the provider panel, the settings -panel's own value box and model picker, the model picker and the thinking ladder -and the session picker (whose keys take the offer's slot in the conversation), -and the sheets that replace the frame whole — the folder chooser and, at phone -width, the status sheet and the tool detail. The list is the keyboard states that -take the keys slot or the frame away from the offer, and a test pins its scope in -both directions: a running turn and a plain conversation MUST keep counting, with -the offer's control on the keys row. +`/settings`, memory, standing, spend), the first-run sheet, the +conversation-delete confirmation (which reads every keypress before the offer's +chord), a team's card, the move picker, the switcher's menu, the wall, the +provider panel, the settings panel's own value box and model picker, the model +picker and the thinking ladder and the session picker (whose keys take the +offer's slot in the conversation), and the sheets that replace the frame whole — +the folder chooser and, at phone width, the status sheet and the tool detail. The +list is the keyboard states that take the keys slot or the frame away from the +offer, and a test pins its scope in both directions: a running turn and a plain +conversation MUST keep counting, with the offer's control on the keys row. The row is read LIVE. `/settings` updates the running window's flag, and a row turned off — or a release skipped — in ANOTHER window is seen because the diff --git a/internal/tui3/updateoffer.go b/internal/tui3/updateoffer.go index 13f56358d1..5fa3b55872 100644 --- a/internal/tui3/updateoffer.go +++ b/internal/tui3/updateoffer.go @@ -430,6 +430,14 @@ func (a *app) offerTick() tea.Cmd { // time is the whole reason the grace exists. The list is the states that TAKE // the slot away from the running conversation, not the conversation's own work. func (a *app) updateOfferHidden() bool { + // THE CONVERSATION-DELETE CONFIRMATION READS EVERY KEYPRESS AT THE TOP OF + // THE LOOP — above this file's own chord — so alt+n never reaches the offer + // while the card stands, and the countdown would run down behind a sheet + // the person cannot answer from (conversationdelete.go's + // [app.conversationDeleteKey]). + if a.cdelete.on { + return true + } if a.questionRoomOpen() || a.setup.open || a.railPlanPending.id != "" { return true } diff --git a/internal/tui3/updateoffer_test.go b/internal/tui3/updateoffer_test.go index 9c9a66a1e4..dc5a959909 100644 --- a/internal/tui3/updateoffer_test.go +++ b/internal/tui3/updateoffer_test.go @@ -538,6 +538,10 @@ func TestTheGraceWaitsWhereTheOfferCannotBeAnswered(t *testing.T) { {"the team menu", func(a *app) { a.teamMenu.on = true }}, {"the provider panel", func(a *app) { a.addPanel.open = true }}, {"a question page", func(a *app) { a.qroom = &questionRoom{} }}, + // THE CONFIRMATION SHEET READS THE KEY BEFORE THE OFFER'S CHORD, so it + // is the one state whose pause is decided by the loop rather than by the + // foot (app.go's tea.KeyPressMsg arm). + {"the conversation-delete sheet", func(a *app) { a.cdelete.on = true }}, // THE SHEETS THE FOOT REPLACES THE OFFER'S SENTENCE WITH, or that take // the frame whole (the phone tier's status sheet and tool detail). {"the model picker", func(a *app) { a.pick.open = true }}, @@ -597,6 +601,36 @@ func TestTheGraceWaitsWhereTheOfferCannotBeAnswered(t *testing.T) { } } +// TestTheDeleteConfirmationEatsTheOfferChordAndTheGraceWaits pins the premise +// of the pause above it: the conversation-delete sheet reads EVERY keypress at +// the top of the loop, before the offer's own chord is ever consulted, so alt+n +// cannot answer the offer while the card stands — and the countdown must wait +// rather than install behind a sheet the person is answering. +func TestTheDeleteConfirmationEatsTheOfferChordAndTheGraceWaits(t *testing.T) { + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + a := lab.app + a.offer.raise("v0.9.3", "v0.9.2", a.now()) + a.cdelete.on = true + drive(t, a, key("alt+n")) + if !a.offer.offering() { + t.Fatalf("the delete sheet did not eat the offer's chord: %+v", a.offer) + } + if !a.cdelete.on { + t.Fatal("the skip chord closed the delete confirmation") + } + if lab.installed != 0 { + t.Fatal("a release installed behind the delete confirmation") + } + // THE GRACE WAITS AND IS GIVEN BACK WHOLE, and the deadline never fires. + a.offer.deadline = a.now().Add(-time.Second) + if cmd := a.tookOfferTick(); cmd == nil || !a.offer.offering() || lab.installed != 0 { + t.Fatalf("the grace fired behind the delete confirmation: cmd = %v offer = %+v installed = %d", cmd, a.offer, lab.installed) + } + if left := a.offer.deadline.Sub(a.now()); left <= 0 || left > codeupdate.AutoGrace || left < codeupdate.AutoGrace-time.Second { + t.Fatalf("the grace did not restart under the delete confirmation: %s left, want about %s", left, codeupdate.AutoGrace) + } +} + // TestBareUpdateDuringTheOfferDoesNotRollBackAFileAnotherWindowAdvanced is the // /update resolution boundary, exercised through the REAL installer: the offer // is about v0.9.3 while the file on disk already holds v0.9.4, because another From 0ba10fca643e5e8e3b77cb9f8fa5e1bc62238670 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Wed, 7 Oct 2026 13:01:05 -0400 Subject: [PATCH 12/12] update: honest skip and refusal lines for the launch offer Two surface-truth bugs in the background-update messages: - `/update skip` tested `offer.active()`, which is true through the few seconds a FINISHED (`updateReady`) or FAILED (`updateFailed`) install keeps its line on screen, while `updateInFlight` was already false. It said "an install is already running" over a line that read "installed" or "could not be installed". The predicate is now the install, not the offer; the ready/failed dwell answers "there is no update offer to skip". - A managed or unwritable executable suppressed the refusal whenever `update.auto` was off, so the launch only advised `/update` -- the road the refusal says is answered with itself. The refusal is now always said, on both settings, and the notice printed beside it drops the `/update installs it for the next launch` clause rather than offering a road the product knows it will refuse. Regression coverage genuinely models running vs finished/failed: the running test starts a real in-flight install and never completes it, and new tests pin the finished and failed dwells and the refused launch with auto on and off. Manual wording matches. Channel intent (dev->dev, staging->staging, stable/rc->stable) and the executable target are untouched. --- .../manual/chat/running-from-the-terminal.md | 4 +- internal/tui3/updatecmd.go | 17 +++- internal/tui3/updateoffer.go | 16 +++- internal/tui3/updateoffer_test.go | 93 +++++++++++++++++-- 4 files changed, 113 insertions(+), 17 deletions(-) diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index f2130b0e46..4e32bbb4a2 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -20,8 +20,8 @@ or ahead build gets nothing. Source and unstamped builds make no launch request, and their `/update` answers `this codeaf was built from source · rebuild with make build, or install a release: curl -fsSL https://agentfield.ai/get/codeaf | bash`. A codeaf owned by Homebrew, Nix or the system package manager, or one in a -folder this account cannot write, is never replaced in place: the notice says so -and names the manager and the curl line. +folder this account cannot write, is never replaced in place: with `auto update` +on or off the launch names the manager and the curl line, not `/update`. With `auto update` off, the launch check still runs and still says `codeaf is out · you have · /update installs it for the next launch`, but diff --git a/internal/tui3/updatecmd.go b/internal/tui3/updatecmd.go index 638fb7b85c..2ecb0bb110 100644 --- a/internal/tui3/updatecmd.go +++ b/internal/tui3/updatecmd.go @@ -72,11 +72,14 @@ func (a *app) tookUpdateCheck(message updateCheckMsg) tea.Cmd { a.note(message.available.Notice()) return nil } + // A MANAGED OR UNWRITABLE FILE IS TOLD THE RELEASE IS OUT AND THEN WHY IT + // CANNOT BE REPLACED, ON BOTH SETTINGS. The row's own state does not change + // what is true of the file, and the `/update` clause is left off the notice + // because the refusal below it has already said that road answers with + // itself — advice the product knows it will refuse is not guidance. if refusal := a.updateRefusal(); refusal != "" { - a.note(quietUpdateNotice(message.available)) - if a.updateAuto.Enabled { - a.note(refusal) - } + a.note(refusedUpdateNotice(message.available)) + a.note(refusal) return nil } if !a.updateAuto.Enabled { @@ -110,9 +113,13 @@ func (a *app) runUpdateCommand(argument string) tea.Cmd { switch { case a.offer.offering(): a.skipUpdateOffer() - case a.offer.active(): + case a.updateInFlight: // AN INSTALL ALREADY RUNNING IS NOT OURS TO CANCEL, and saying it // was skipped would be a lie the download immediately contradicts. + // THE PREDICATE IS THE INSTALL, NOT THE OFFER: a finished or failed + // install keeps its line on screen for a few seconds + // ([updateOfferingDwell]), and `offer.active()` is true through that + // dwell even though nothing is running to interrupt. a.note("an install is already running · it finishes in the background · /update never stops the next one") default: a.note("there is no update offer to skip · /update installs one for the next launch when you want it") diff --git a/internal/tui3/updateoffer.go b/internal/tui3/updateoffer.go index 5fa3b55872..48604d111e 100644 --- a/internal/tui3/updateoffer.go +++ b/internal/tui3/updateoffer.go @@ -279,11 +279,25 @@ func (a *app) updateOfferNote() string { // automatic updater is off: knowing is still worth one dim line, acting is not // offered until it is asked for. func quietUpdateNotice(available codeupdate.Available) string { + return updateAheadNotice(available) + " · /update installs it for the next launch" +} + +// refusedUpdateNotice names the same release for an executable this account may +// not replace in place. IT STOPS BEFORE THE `/update` CLAUSE ON PURPOSE: the +// refusal said right after it that the in-place road answers with itself, and +// offering a road the product knows it will refuse is not guidance. +func refusedUpdateNotice(available codeupdate.Available) string { + return updateAheadNotice(available) +} + +// updateAheadNotice is the head both launch notices open with: what is out and +// what this build is. The two roads differ only in the clause that follows. +func updateAheadNotice(available codeupdate.Available) string { running := strings.TrimSpace(available.Running) if running == "" { running = "an unstamped build" } - return "codeaf " + available.Latest + " is out · you have " + running + " · /update installs it for the next launch" + return "codeaf " + available.Latest + " is out · you have " + running } // ── THE ANSWERS ───────────────────────────────────────────────────────────── diff --git a/internal/tui3/updateoffer_test.go b/internal/tui3/updateoffer_test.go index dc5a959909..b22f032381 100644 --- a/internal/tui3/updateoffer_test.go +++ b/internal/tui3/updateoffer_test.go @@ -115,23 +115,98 @@ func TestSkipOnlyAnswersTheOfferingRelease(t *testing.T) { } // TestSkipCannotCancelARunningInstall proves the honest answer while a download -// is already in flight. +// is GENUINELY in flight. `/update` leaves the loop: the returned command is the +// resolver and the download, and NOT running it is exactly the state a person +// types into while the bytes are being fetched off-frame. (The old shape drove +// the grace to completion first, so the install had already finished and the +// assertion held for the wrong reason.) func TestSkipCannotCancelARunningInstall(t *testing.T) { state := codeupdate.AutoState{} lab := newOfferLab(t, offerAuto(&state)) a := lab.app - drive(t, a, offerCheck("v0.9.3")) - // The grace expiring starts the install; the seam answers on the next drive. - drive(t, a, updateGraceMsg{}) - if lab.installed == 0 { - t.Fatal("the background install did not run") + command := a.slash("/update") + if command == nil || !a.updateInFlight { + t.Fatalf("the update did not start: command = %t in flight = %t", command != nil, a.updateInFlight) } // A person types skip afterwards: it must not claim to have cancelled. - command := a.slash("/update skip") - _ = command - if !strings.Contains(updateNotes(a), "already running") { + a.slash("/update skip") + if !strings.Contains(updateNotes(a), "an install is already running \u00b7 it finishes in the background") { t.Fatalf("skip lied about a running install:\n%s", updateNotes(a)) } + if a.offer.active() { + t.Fatalf("skip raised or kept an offer while an install ran: %+v", a.offer) + } +} + +// TestSkipOnAFinishedOrFailedDwellSaysThereIsNoOffer proves finding 1: the few +// seconds a finished or failed install keeps its line on screen are NOT an +// install in flight, and `/update skip` says there is no offer rather than +// calling an install that already landed "already running". +func TestSkipOnAFinishedOrFailedDwellSaysThereIsNoOffer(t *testing.T) { + t.Run("finished", func(t *testing.T) { + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + a := lab.app + drive(t, a, offerCheck("v0.9.3")) + drive(t, a, updateGraceMsg{}) + if lab.installed != 1 || a.updateInFlight || a.offer.phase != updateReady { + t.Fatalf("the install did not land in its dwell: installed = %d in flight = %t offer = %+v", lab.installed, a.updateInFlight, a.offer) + } + a.slash("/update skip") + if strings.Contains(updateNotes(a), "an install is already running") { + t.Fatalf("a finished install was called running:\n%s", updateNotes(a)) + } + if !strings.Contains(updateNotes(a), "there is no update offer to skip") { + t.Fatalf("the finished dwell did not get the no-offer answer:\n%s", updateNotes(a)) + } + }) + t.Run("failed", func(t *testing.T) { + lab := newOfferLab(t, offerAuto(&codeupdate.AutoState{})) + lab.failWith = errors.New("the checksum did not match") + a := lab.app + drive(t, a, offerCheck("v0.9.3")) + drive(t, a, updateGraceMsg{}) + if a.updateInFlight || a.offer.phase != updateFailed { + t.Fatalf("the failure did not land in its dwell: in flight = %t offer = %+v", a.updateInFlight, a.offer) + } + a.slash("/update skip") + if strings.Contains(updateNotes(a), "an install is already running") { + t.Fatalf("a failed install was called running:\n%s", updateNotes(a)) + } + if !strings.Contains(updateNotes(a), "there is no update offer to skip") { + t.Fatalf("the failed dwell did not get the no-offer answer:\n%s", updateNotes(a)) + } + }) +} + +// TestARefusedLaunchNamesTheReleaseOnBothSettings proves finding 2: a managed or +// unwritable executable is told what is out and why it cannot be replaced, with +// `update.auto` ON or OFF, and the notice never sends a person to `/update` that +// the refusal it is printed beside has already said will not work. +func TestARefusedLaunchNamesTheReleaseOnBothSettings(t *testing.T) { + const refusal = "this codeaf is managed by Homebrew \u00b7 update it there, or install a release with: curl -fsSL https://agentfield.ai/get/codeaf | bash" + for _, enabled := range []bool{true, false} { + name := "auto on" + if !enabled { + name = "auto off" + } + t.Run(name, func(t *testing.T) { + coordinator := offerAuto(&codeupdate.AutoState{}) + coordinator.Enabled = enabled + coordinator.Refusal = func() string { return refusal } + lab := newOfferLab(t, coordinator) + drive(t, lab.app, offerCheck("v0.9.3")) + notes := updateNotes(lab.app) + if lab.app.offer.active() || lab.installed != 0 { + t.Fatalf("offer = %+v installed = %d for a managed file", lab.app.offer, lab.installed) + } + if !strings.Contains(notes, "codeaf v0.9.3 is out \u00b7 you have v0.9.2") || !strings.Contains(notes, refusal) { + t.Fatalf("the release or the refusal was not said:\n%s", notes) + } + if strings.Contains(notes, "/update installs it") { + t.Fatalf("a refused file was told /update installs it:\n%s", notes) + } + }) + } } // TestAutoOffShowsOneQuietNoticeAndNeverInstalls proves the quiet indicator.