Repository navigation
Keep codeaf current with quiet background updates that preserve running work - #1790
Merged
Merged
Conversation
…unning 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.
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.
…ller 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.
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.
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.
…ment 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 <tag>` and `/update <tag>` 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.
…date 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 `<update-state.json>.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.
…heets Four review findings on the background-update change (PR 1790). P2-1: a hand-named tag (or `update --version <tag>`) 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 <tag>` 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.
santoshkumarradha
force-pushed
the
seamless-update-20261007
branch
from
October 7, 2026 16:16
70de2ec to
263478b
Compare
…mation 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.
santoshkumarradha
marked this pull request as ready for review
October 7, 2026 16:31
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Keeps codeaf current without interrupting a conversation. A newer release gets a small, nonmodal offer in the existing footer; after ten visible seconds it downloads and installs in the background. Chats, drafts, questions and tasks remain the main interface.
Fixes #1788. Targets
dev.alt+nor/update skip; turn automatic installation off in settings or with/update never./updateuses the same background installer. Completion briefly appears in the footer and leaves a concise note; failures preserve the current executable and use bounded automatic retries.“New window” means a newly launched CLI process, not a new in-app chat tab. The new executable and the running engine are different: installation does not migrate live turns. Existing windows continue on their current build; new windows use the installed app and may temporarily share an older compatible engine. Older engines without the new retirement request transition at the next safe launch.
Screens
These are actual TUI renders in isolated profiles. Release labels in the seven update states are deterministic fixtures; the settings captures operate the real settings page. They show appearance, not proof of runtime continuity.
Launch offer · 80 columns
Installed while work continues · 120 columns
Settings → Workspace · automatic updates on
Settings → Workspace · automatic updates off
Failed installation · current version continues
All 18 captures, raw frames and provenance.
How it was checked
Live-test source:
63cb1b347e94f9471f9079e9f4c40f4dccbc8b31, rebased ontodevatbaa2d7f0fda7466fd787841274170bd56a4148aa(including the Teams changes). All full and live acceptance ran on Spark; all live model calls and parallel model reviews useddeepseek/deepseek-v4.1-flash.make pr-ready/updateduring real model workHigh-level examples exercised:
The live updater runs use two synthetic release stamps on the live-test source named above and a loopback server that serves the actual binary and checksum. They exercise real TUI/installer/engine paths, not published release packaging. The separate released-engine run uses a byte-identical isolated copy of
dev-20261007-0dbeccf575ff.Evidence: Spark job
20261007-162309-001791-seamless-integrated-final-validationran the full sequence on the live-test source named above. The independent fix and integration review ran in parallel as20261007-162310-001792-seamless-delete-fix-review. The final integration regression also drives the real skip key with the new conversation-delete sheet open and proves that its countdown waits.Final code review follow-up:
0ba10fca643e5e8e3b77cb9f8fa5e1bc62238670fixes two message defects:/update skipno longer calls a finished or failed install “running”, and managed/unwritable installs show the correct guidance with automatic updates on or off. Focused Spark regressions passed; a fresh independent DeepSeek review verified both fixes and default channel selection (20261007-170243-001797-merge-fix-verification-review). This follow-up changes messages and their predicate, tests, and manual; it changes no installer or engine path. E2E was not rerun at the owner’s request. Finalmake pr-readypassed on that exact follow-up commit, with every selected test passing on its first run and all GitHub checks green. No tests were suppressed. Spark job20261007-170157-001796-seamless-merge-gate.No experimental build has been installed into the owner's Blackmac or Spark devaf profile. Windows locking was cross-compiled; Windows runtime continuity and macOS runtime continuity are not established by the Linux acceptance run.
Published release verification
After merge, Spark job
20261007-171749-001799-published-curl-reviewtested the actual public releasedev-20261007-4fd9cfaa0fb6(merge commit4fd9cfaa0fb63b1c78db5aa6ebe9479e512d1640) on Linux/arm64. The publiccurl -fsSL https://agentfield.ai/get/devaf | bashinstaller was exercised in isolated homes with--no-modify-path --no-startpassed viabash -s --.devaf, and matched the public SHA256 checksum.devaf update --checkcorrectly reported the newest dev build.deepseek/deepseek-v4.1-flashchat. No update offer appeared while already current./get/codeafscript was inspected and defaults to stable/codeaf. No stable installation was performed.No newer public release existed, so this smoke run does not prove an automatic countdown into a subsequent public release; that behavior remains supported by the earlier isolated live tests. The test needed a shorter private state path to fit the Unix socket length limit. Evidence:
published-curl/assertions.jsonunder the Spark evidence root, all required assertions passed.Checklist
docs/changes/unreleased/1790-background-updates.md.