Skip to content

Merge app/v3 into workstreams/main (fourth catch-up) - #2338

Merged
AbirAbbas merged 5 commits into
workstreams/mainfrom
workstreams/sync4-app-v3
Nov 4, 2026
Merged

AbirAbbas merged 5 commits into
workstreams/mainfrom
workstreams/sync4-app-v3

Conversation

@AbirAbbas

Copy link
Copy Markdown
Collaborator

Summary

Brings the Workstreams integration branch up to app/v3 (4 commits behind: #2285, #2330, #2314, #2331). A merge, not a rebase: no commit on either side is rewritten.

  • One conflict, in cmd/codeaf/app.go: the appBackend struct keeps the client-tools field beside app/v3's plugin pause, Ollama and auth fields.
  • internal/appwire/server.go (the client-tools and plugins hello features are both optional and sit side by side), internal/session/connect.go and docs/design/app-v3/PROTOCOL.md merged line-disjoint.
  • The changelog entries come from the merged app/v3 commits; this PR adds none of its own.

How it lands

This PR runs the gate only. It is not squash-merged: a squash would flatten app/v3's commits into one and leave app/v3 out of the branch's history, so every later sync would replay them. Once the gate is green (or red only on known baseline failures, confirmed against a fresh app/v3 run), workstreams/main is fast-forwarded to this branch's merge commit and this PR closes as merged.

Validation

  • PR gate: light gate + touched packages. Baseline reds are listed in a comment once the gate finishes.

🤖 Generated with Claude Code

ZeroPoint95 and others added 5 commits November 3, 2026 13:29
#2285)

* pairing: a failure says its sentence once, and a finished pairing drops its check number

- B2: the failure sentence is the pairing's error only; it is no longer
  also appended as a status line, so the desktop sheet shows it once.
- B3a: finish() clears Words with the code and link, so no check number
  stays on screen after pairing succeeds, fails or is cancelled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* sync: log why a chat could not be shared, once per chat and reason (T7)

The sharing pass kept only that a chat failed and showed one sentence;
nothing recorded the cause. Each failure now logs the chat and its error
the first time that reason appears, so retries do not flood the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tui: an approval that timed out says it may still have arrived (T2)

"no answer from your devices — try again" blamed devices that take no part
and invited a retry the service would refuse. A timed-out answer now says
the sync service did not answer and to check /devices first; a timed-out
lookup says the sync service did not answer. The manual quotes both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* pair: the terminal approval keys sit on their own line (T9)

`codeaf pair approve` printed the check number and "a / d" on one line, so
the typed answer ran into the question. The keys now follow on their own
line as "a approve · d decline:", matching the chat card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* manual: correct how a removed computer rejoins and where older chats are prepared (D1)

- A removed computer comes back with `codeaf pair --replace`; plain
  `codeaf pair` does not bring it back.
- Older chats are prepared in the background only by the terminal chat,
  not by the desktop app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* app: a removed computer reads the removal and "codeaf pair --replace", not "wireauth: device revoked" (B11)

- Engine errors that are the revoked-device refusal reach the app as
  chatlist.Removed instead of the wire sentinel's text, so the chats area
  no longer shows "wireauth: device revoked".
- Removed now says "run codeaf pair --replace to bring it back": plain
  `codeaf pair` does not bring a removed computer back (B1). Notices
  stored with the earlier wording are still recognised as a removal.
- The manual quotes the new sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* sync: a failed continue logs its reason, and the app test pins the take failure's Detail (B5a)

- The "continue <id> phases … outcome=failed" line now ends with the
  error as reason="…", so chat.log names why a move failed.
- appTakeError already carries the cause in Detail; the S5 take-failure
  test now checks it reaches the app over the wire and is not the generic
  sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* pairing: the code instruction says dashes are optional (F4)

The engine already reads a join code with or without dashes and spaces
(pair.ReadJoinCode, covered by TestReadCodeShapes). The instruction now
says so: "and type 42-715-302 (dashes optional)". Copy pins and the
desktop-sync manual quote follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* manual: restore the fresh-computer --replace exception, scoped so it cannot contradict rejoining (D1 follow-up)

A computer whose chats hold only startup headers still pairs without
--replace when it first joins (identity.Joinable). A removed computer
already holds the identity, so pair.adopt stops at "already joined"
before that check and only --replace brings it back. The sentence now
says both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* pairing: a removed computer pairs back from the app (B1)

A removed computer keeps the identity and its revoked device key, so
pairing it again ended "already paired; nothing changed" and left it
removed. Only --replace minted a new key, and the app never sends it.

- pair.Joining gains Revoked. When it is set and the grant is the same
  identity, adopt renews the device key as Replace does. With the same
  identity nothing local is given up; a different set of devices still
  follows the normal rules.
- The app's use_code and request_link ask the directory whether this
  computer's own device is refused (ownDeviceRevoked) and set Revoked.
- New TestB1AppRePairsARemovedComputer covers code and link re-pairing
  after removal (new key, same identity, chat kept, old key still
  removed, restart requested) and an unremoved computer still being told
  nothing changed.
- The manual says the app brings a removed computer back on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tui: the approval timeout says "sync", not "the sync service" (T2 follow-up)

The word law (#1508) keeps "service" out of person-facing text, and
TestNoPersonFacingStringInThisSurfaceSaysLane caught approve.go. Both
timeout sentences now begin "sync did not answer in time", as the other
sync sentences do, and the manual quotes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* approve: an approval whose reply was lost still pairs, and its link stops asking (T1, T3)

- After an approve times out or is refused as already answered, the door
  reads the request again. Approved for this device and listed in this
  computer's own fleet counts as success: the solo marker ends and the card
  closes as approved.
- Still waiting means nothing arrived: the screen says the plain retry.
  Unknown keeps the "may still have arrived" sentence.
- Reopening the link of an approval that landed says the device is already
  paired and ends solo, instead of asking to approve again.
- The device record gets its own 8 s wait, and an answer's overall wait is
  20 s, so a slow first write cannot spend the approve's time.
- Tests: a slow relay that approves but loses the reply (both computers end
  up paired), one that never hears the approval (the retry pairs), and a
  reopened link after a lost approval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* devices: a roster read's own last-seen stamp no longer publishes devices.changed (B8b)

A hosted relay stamps the asking computer's last seen on every request, so
each roster read moved this computer's stamp and devices.changed went out.
The app answers that event with another read, and the two kept each other
going while a window was open: constant relay traffic, and the Computers
page reloading every few seconds.

publish now judges a change with this computer's last seen, and that of any
computer online now, left out; neither is shown. The event still carries
the current stamps, and an offline computer's new last seen or a change in
who is online still publishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* changes: entry for the pairing QA fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tui: a declined answer that timed out is the plain retry, not "check /devices" (T2 follow-up)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* app: a removed computer's refusal names no chat and points at Pair a computer (B11 follow-up)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* manual: rejoining keeps chats syncing and memories and settings in place (D1 follow-up)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* changes: the lost-reply fix is the terminal screen's, and the app's removal remedy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* changes: re-adding a removed computer is not finished (B1)

Two-computer testing on 2026-11-03 still showed neither computer listing
the other after re-pairing a removed one; the entry no longer says it
comes back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test(factory): expose hold lost with heartbeat reply

Keep a red regression at the real HTTP Hold/heartbeat/worker seam. Drop
one heartbeat reply only after the factory drains its hold. The 30-second
check has already started, so job completion cannot explain the failure.
The runner remains unpaused after 249 successful subsequent heartbeats.
Leave production code and TestAPauseAndResumeReachTheRunner unchanged.

The lease forgets Hold at lease/signals.go:115 before serve/workers.go:523
writes its reply. A transport failure gives worker/worker.go:538 no signal,
so Runner.Hold/setPaused and the paused log are never reached. Subsequent
heartbeats have no hold to redeliver. Repeating Hold(true) is also a no-op
because lease/doors.go:276 already sees the factory's paused flag.

Validation: the new regression fails with GOMAXPROCS=1 and -race.
The original -race -count=200 loop reproduced four missing-pause failures
and one separate resume failure. A CPU-loaded throwaway characterization
observed 81 consecutive lost holds, all still running without a paused log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): capture pause worker and factory reports

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): require heartbeat hold redelivery and resume precedence

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(factory): redeliver wanted hold on every heartbeat

Read the current pause intent on every live drain. Resume queues a fresh lease and suppresses an older uncollected hold; ordinary drains and the wire vocabulary remain compatible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): expose pause log lost when cancelled drive finishes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): retain worker release snapshots for pause diagnosis

Under CPU load the natural pause test failed eight times in sixty runs: the hold arrived, reports carried only the check log, and Done successfully released the lease. The factory remained paused and resumable despite the raw running state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(factory): save pause before the drive can cancel its context

The held drive exits through settleSpend, which cancels its context. Publishing c.paused before entering move allowed that exit to reject the pause log write. Change the control within the admitted store write so the log survives and the worker flushes it before releasing its lease.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): format pause regressions and document live hold delivery

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): bound concurrent hold and spending answer regression

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): schedule spending answer before item-locked hold

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): use runner clock for lock scheduling

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): require atomic saved holds at step boundaries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(factory): persist holds outside the loop lock before cancelling

Serialize pause transitions with round admission. Store callbacks never acquire the loop lock, so spending answers cannot deadlock with holds. Failed writes leave the control and execution unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): hold only the first save in the step boundary regression

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): capture the first hold retention hook caller

Instrument the existing failing boundary test before changing the fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(factory): block the hold save after its preliminary read

The first-hook goroutine stack showed Hold at loop.go:2149 calling Store.Get and retainOutput at store.go:219, before setPaused acquires pauseMu. Arm the fixture through the runner clock inside setPaused so the blocker intercepts the pause write. Preserve every boundary, persistence, cancellation and failed-save assertion; remove temporary stack logging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: record the hold boundary fixture correction

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: keep the boundary fixture note with the existing hold change

The changelog gate rejects PR zero in the existing hold entry. Fold this related fixture note into that entry instead of introducing a second rejected fragment; no pull request number has been assigned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: changelog keeps to what a person sees

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* changelog: name the pull request

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ws say a take is running (#2314)

* pairing: the app and codeaf pair approve settle a lost approval reply too

The terminal chat screen's T1/T3 fix now covers the desktop app's
approve_link and codeaf pair approve: an approve that timed out or was
refused as already answered is looked up with settleApproval, so a
device the relay let in counts and the approver ends solo; a reopened
link this computer already approved says already paired and ends solo
instead of failing as already answered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* app: chat rows say when a take of that chat is running

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* changes: one entry for the pairing follow-ups; #2285's entry keeps what ships there

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* changes: number the pairing follow-ups entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* app: a finished take never says taking:false on a chat still elsewhere

The take stopped counting before it held the inventory lock, so a follower
emission in that gap sent the old row with taking:false, which a client that
stopped waiting reads as a take that did not move the chat. It now stops
counting under the lock, and a failed reconcile republishes only a cached row
that already says the chat is here. The protocol text states the guarantee
the code keeps; the change entry lists its docs surface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* app: a take that acquired the chat says it runs until a row says the chat is here

An error after the chat was acquired, or a presence refresh after a failed
reconcile, still republished the row from before the take with taking:false.
The take now records the fence it acquired, and every row older than that
keeps taking:true until a row says the chat is here or a later fence holds
it. The scheduling-dependent lock test is replaced by deterministic ones; the
protocol and change entry say when taking ends, and a reopened link counts
any approval for this account's devices.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* app: republishing a take's taking leaves agents where the take put them

Republishing cached rows for taking also re-placed their local agents, so
after a failed reconcile the agent a take had just put here went back to
the other computer. That republish now changes rows only. A chat that
leaves the list drops its landed take. The round trip checks taking only on
the row that says the chat is here, since a lagging row may still say it.
The protocol and change entry say a here row can carry taking while a take
runs, and that a lost approval reply did let the device join.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* app: a presence change leaves a landed take's agent where the take put it

When a take acquired the chat but its reconcile read failed, the cache
still said the chat was elsewhere, and the next presence-only frame
re-placed the agent the take had put here back on the other computer.
That refresh now skips re-placing agents on rows whose take landed and
no row has yet confirmed.

It skips only those rows rather than every row: presence changes the
offline sentence in `line` and `takeover_line`, which an agent's location
carries, and nothing else pushes that update to a following client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* app: a reading from before a landed take leaves the agent here

readChats re-placed every row's agent, so a follower reading newer than
the cache but older than the take moved the agent agents.take put here
back elsewhere. It now skips rows with a landed take, as presence does;
a row saying the chat is here clears the landed take first and still
places the agent.

agents.take is now driven through its acquired-then-failed paths with
the copy and open stood in (no engine needed): opening the moved chat
fails, and settleTake's reading fails followed by a stale reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* app: only a directory reading drops a landed take whose chat left the list

publishTaking and the presence refresh mark taking on cached rows, which
can be empty while the follower runs; pruning there forgot a take that
acquired the chat, so a later reading from before it moved the agent back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Expose plugin connections through the desktop protocol

* Expose plugin connection dates and provider state

* fix: prevent cancelled plugin saves and scope asks by agent

Keep account lookup and credential lock waits outside the flow mutex. Serialize only the final credential replacement with cancellation so an abandoned browser attempt cannot save later. Scope connection ask ids by agent across plugin, chat and Inbox events.

* manual: the desktop page is called Connectors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: report whether connector keys were checked

Preserve unset key variable names in connection refusals. Expose catalog checkability and whether a connection probe passed without guessing vendor endpoints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: name the pull request in the plugins change entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings the integration branch up to app/v3 (4 commits: pairing QA
fixes for failures, approvals, removed computers and the devices loop; a
person's pause survives a lost heartbeat reply; pairing follow-ups where
app and pair approve settle a lost reply and chat rows say a take is
running; the plugins desktop protocol over the connect layer).

One conflict, in cmd/codeaf/app.go: the appBackend struct keeps the
client-tools field beside app/v3's plugin pause, Ollama and auth fields.
internal/appwire/server.go (both optional hello features),
internal/session/connect.go and docs/design/app-v3/PROTOCOL.md merged
line-disjoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AbirAbbas
AbirAbbas merged commit 211c968 into workstreams/main Nov 4, 2026
5 of 9 checks passed
@AbirAbbas
AbirAbbas deleted the workstreams/sync4-app-v3 branch November 4, 2026 03:13
@AbirAbbas

Copy link
Copy Markdown
Collaborator Author

Landed by fast-forward: workstreams/main is now 211c96805 (this branch's merge commit).

Gate run 41762991761: touched codeaf, touched rest and touched tui3 green. Reds are baseline only, identical on a fresh app/v3 run at d5f07a356:

  • light gate: TestNoRoadInTheMemoryEngineHasMoreEndingsThanTheCeiling, TestNoRoadInTheTaskEngineHasMoreEndingsThanItsLedgerRow (app/v3 PR gate 41763278371, same two).
  • touched session: those two plus TestALandStartsNoAutomaticMaintenance, TestProgramHoldClassifiesEveryRegisteredChatTool, TestTheFixedPrefixStaysUnderItsBudget, TestTheLeanPrefixStaysUnderItsBudget (app/v3 PR gate 41763278371 touched session and Full check 41763281168 shard 0/3, same six).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants