Skip to content

refactor(runtime-host): split execution composition - #4352

Open
WangzJi wants to merge 821 commits into
apache:mainfrom
WangzJi:feat/issue-3911-execution-composition
Open

WangzJi wants to merge 821 commits into
apache:mainfrom
WangzJi:feat/issue-3911-execution-composition

Conversation

@WangzJi

@WangzJi WangzJi commented Aug 31, 2026 •

Copy link
Copy Markdown
Member

Summary

  • reduce the public Runtime Host execution composition root from 2,064 lines to 42 lines
  • isolate sandbox, history, tool, and Agent Graph composition ownership and lifecycle handling
  • preserve startup, recovery, drain, and aggregate cleanup behavior

Fixes #3911

Verification

  • npm --workspace @maka/runtime-host run build
  • npm --workspace @maka/runtime-host run typecheck
  • focused composition regression tests: 13 passed
  • new composition module tests: 8 passed
  • Biome lint, ASF license header check, and git diff --check passed
  • changed composition modules introduce no new relative-import cycle
  • full Runtime Host suite attempted locally: 1,448 passed, 12 skipped, and 1 environment-only failure because the Electron binary was not installed

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex implemented the composition refactor, added focused tests, and ran the reported validation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@WangzJi
WangzJi force-pushed the feat/issue-3911-execution-composition branch from 7d22f23 to efc8cd3 Compare August 31, 2026 11:47
@github-actions github-actions Bot added effort/XL Under 2500 readable lines effort/XXL Over 2500 readable lines and removed effort/XL Under 2500 readable lines labels Aug 31, 2026
Astro-Han and others added 26 commits September 20, 2026 17:52
* refactor(ui): keep execution details in the conversation flow

Keep the process disclosure and running/settled lifecycle, but remove the outer card, height caps, nested scrolling, fade measurement and zoom state. Supporting activity should not need a second viewport or an extra action to read everything after expanding it.

Trade a bounded long-process view for one transcript scroll path. Tool and reasoning details, answer identity, selection and footer status stay unchanged. Native disclosure motion retains overflow: clip without a scroll container.

Remove two zoom-only unit tests and the zoom story. Extend the existing cold-scroll story to require full process layout; remove nested-scroller avoidance and the oversized-answer workaround from geometry coverage.

Verified UI build/tests, UI and Storybook typechecks, format/lint, Astryx inventory, 11 focused Electron/CDP story renders and all three geometry scenes with --assert-stable.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated-by: Devin

* fix(ui): restore elapsed timing to the process disclosure

Removing the bounded card alone preserved the earlier move of all timing to the footer. Keep live elapsed time beside the current process, then replace it with the recorded total duration on settlement. The last assistant segment owns this once; pure replies keep footer timing, and settled status and finish timestamps remain below the answer.

Reuse the existing clock and duration copy rather than introducing another timer or persisted state. Retry and observation-loss gates still suppress false activity. Existing localized footer labels now accept omitted duration so the header and footer do not repeat it.

Extend the existing lifecycle regression: one footer clock hands off to the process, advances from the same start time, then freezes at recorded duration without remounting the answer. Confirmed red before implementation and green after; updated steering, failure, locale, pure-reply and browser coverage. UI suite, typechecks, format/lint and 13 focused Electron/CDP renders pass.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated-by: Devin

* feat(ui): fold activity batches between process commentary

Keep the timed whole-turn disclosure while reducing the rows it exposes: consecutive thinking and tools share one compact batch, and commentary remains outside each batch. Astryx Collapsible owns the trigger and visibility; existing reasoning and tool details remain the second reveal, with tool calls rendered individually instead of introducing another tool-group layer.

Derive batches from the existing process projection and anchor them to preceding text. Stable wrappers retain reader choices as items stream, neighboring tools disappear or the outer turn folds. Uniform batch wrappers also handle the initial single item without changing detail identity when more work arrives. Visible summaries carry current or latest action and failure evidence; hidden details stop animating. Removed an unnecessary grouping memo after verifying behavior without it.

Extend existing production-path tests rather than adding another suite. Grouping regression failed before the change; streamed boundaries, detail identity, redaction, observation loss and timing now pass in the 531-test UI suite. The existing expanded-process story opens a mixed batch and a real Read result, then checks collapsing and answer selection. Thirteen Electron/CDP story renders, three geometry scenes, typechecks, format/lint and surface inventory pass.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated-by: Devin

* revert(ui): defer activity batch presentation

Revert 5836cb8 at the user request: mixed activity batches need more design work. Keep this PR focused on the previous timed inline process disclosure rather than expanding its interaction scope.

The resulting tree exactly matches 98c0d8a: live timing in the process header, recorded duration on completion, original reasoning/tool rows, no outer card or nested scrolling. Verified 531 UI tests, format/lint, Storybook typecheck/build, surface inventory and 13 focused Electron/CDP renders.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated-by: Devin

* test(ui): drop the ScrollableArea stand-ins the revert orphaned

The Proxy getComputedStyle stubs, auto overflow values, and non-zero
client-box defines were added so Astryx useScrollableArea could measure
the capped process body. With that primitive gone, the comments describe a
dependency nothing consumes. Restore the pre-5511 minimal forms: a plain
visible-overflow stand-in for the transcript wheel-routing walk, and a
parameterless domRoot without geometry defines. reachesTranscript reads
overflowY/overscrollBehaviorY and treats visible nodes as pass-through,
unchanged. 51 ui and 99 desktop harness-consumer tests pass.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated-by: Devin

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Replace the historical SQLite fixture with SQL while preserving migration coverage, reject SQLite binaries from source candidates, clarify bundled attribution, and document release review and replacement steps.

Generated-by: Codex
Remove the composer footer's Git-branch chip and the read pipeline that fed it (apache#5487): the chip was that pipeline's only consumer, so nothing is left stranded.

The branch is ambient session state the agent owns, not a parameter of the send, so the composer's control row is the wrong home for a read-only value there — and it was the one item on that row drawn as a hand-rolled span instead of an Astryx primitive. The workbar's Review face names the current branch once apache#5120 lands; this lands first so the two never show the same fact twice.

Migration: none. Between this merge and apache#5120's there is no surface naming the branch — a deliberate gap over duplicating the readout. apache#5120 needs a trivial rebase on git-review-main.ts and packages/core/src/git-review.ts to drop the readGitBranch / GitBranchReadResult it inherited from apache#5487.

Refs apache#2171, apache#5487

Generated-by: Maka
…pache#5541)

* fix(ui): keep steering a timeline boundary when its step continues

A steering_message parked in pendingSteering until an event opening a new
step claimed it. Wire order lets the same stepId keep emitting after a
steering, so the row was never claimed: post-steering content merged into
the pre-steering group and the steering rendered at the timeline tail.

The projection now records a steering's position at arrival as a boundary
slice in the step list instead of deferring it to a claim rule. Content
after a boundary resolves only to slices past it; tool events for an
existing row stay on the slice holding it. Settled steering rows the live
stream missed splice into the live order by timestamp rather than trailing.

Generated-by: Devin

* fix(ui): resolve completions to their message slice across steering boundaries

A *_complete event finalizes an existing message row, it is not new content.
Routing it through the boundary check appended a fresh post-boundary slice
while supersedesKind stripped the pre-boundary one, flipping
[text, steering] to [steering, text]. Resolve completions to the last slice
of the message wherever it sits, trim the complete full text to the
post-boundary portion of a continuation slice, and mark earlier slices
complete instead of stripping them. replaySafeDelta now always tracks the
source end offset so continuation baselines exist for offset-less streams.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): guard completion slicing behind a verified rendered prefix

A completion's full text shares the delta stream's coordinates only when it
extends them; the runtime adopts a provider reasoningSummaryText when it
diverges from the accumulated deltas, so slicing at continuedEndOffset cut
real content or blanked the slice. Compare the payload against the
concatenated text earlier slices already render and trim only that verified
prefix — mismatch lands the payload whole, the same overlap-check the host
projector applies to deltas. Two regression tests pin the divergent and
shorter-than-offset cases.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…pache#5536)

* fix(desktop): settle stale-epoch local messages instead of replaying

A Message dispatched in a Host Epoch that is no longer running could never
leave the local outbox. The store forbids retargeting the immutable
originHostEpoch, so retries replay the original submit, and the running Host
answers `outcome_unknown` for any identity without durable proof. The copy
looped between `unknown` and another doomed submit, holding the Session's
delivery order behind it, while neither cancel nor reconcile could end it.

Resolve the identity's real fate from the Host's existing
`turn.message.execution.query` instead: durable receipts and steering proofs
settle it as accepted, a surviving queued admission yields to the Host queue,
and a cancelled or absent identity becomes an explicit non-delivery that
releases the queue and offers a removable local copy. An unanswered query
stays unresolved and retried, so a Host that is still recovering is never
read as never delivered.

Generated-by: Maka

* fix(runtime-host): report non-admission positively in the execution query

Review [P1] on apache#5536: `#resolveMessageExecution` returns `{ kind: 'recovering' }`
when no receipt, steering proof, cancellation tombstone, or pending admission
names an identity, and `queryMessageExecutions` dropped that case from the
result. The desktop then read the omission as "never admitted" and retired the
local copy, so the change's own safety promise — an absent answer must never
let a user resend a Message the Host may already own — held only for a thrown
failure, not for the path that actually produces "no answer".

Give the union an explicit `not_admitted` state and emit it when the identity is
traceless and the Host holds no in-flight submit for it. Silence then carries
exactly one meaning — "cannot say yet" — and the desktop retires a copy only on
positive evidence (`cancelled` or `not_admitted`), keeping an omitted identity
unresolved and retried.

`not_admitted` is a claim that no epoch ever admitted the identity, so it is
only sound while no admission write can be in flight. A stale epoch's submit
returns `outcome_unknown` before it can commit, which covers the coordinator's
own path, but WorkHub writes admission rows without an in-memory submit to
observe. The query now enters the Session admission gate the way its sibling
`readMessageExecutionDisposition` already does, so the read cannot race an
admission write. A settable `failed` state that `wake()` never revisits made
that race uncorrectable, which is why the read has to be atomic rather than
merely conservative.

Adds the `not_admitted` wire state, so the compatibility epoch moves 168 -> 169;
epoch-168 peers reject the new state as an invalid frame.

Generated-by: Maka

* fix(desktop): retire not_admitted side-chat slots and name observer outcomes

Review [P2] on apache#5536: `not_admitted` is positive proof a Message can never
execute, but `reconcilePendingMessageExecutions` matched it in neither branch,
so the identity was neither dropped from `pendingUserMessagesRef` nor used to
release the Composer's admission slot. Recovery was asymmetric: `cancelled`
cleared the slot automatically, `not_admitted` left the user pressing Stop to
clear a slot the Host had already proven dead.

Fold `not_admitted` into the terminal branch and rename the set to `retired`,
which is what the two states now mean together. Only an omitted identity still
means the Host cannot say, and it keeps its slot.

The session observer mapped `not_admitted` to `retracted` only by falling
through a pre-existing `owned ? ... : 'retracted'` ternary. Name both retracting
states so a later addition cannot silently inherit that outcome, and cover the
state in the parameterized observer test.

Generated-by: Maka
…he#5544)

Remove public Nightly download promotion and retire the built-in OpenCode Free provider while preserving stored connections and conversation history. New installations require an explicit model connection; cancelling CLI setup exits cleanly.

Remove obsolete free-tier metadata and adapt session and package fixtures to the explicit-model setup. Validated by main CI, four-platform installed CLI checks, and released State Root compatibility.

Generated-by: Codex
…pache#5543)

* chore(deps): bump @astryxdesign/* from 0.6.1 to 0.6.2

0.6.2 adds an opt-in Markdown math renderer (components.math), a TextInput
IME fix for CJK input, ChatToolCalls a11y announcements, and a localized
Spinner label.

Upstream reworked Markdown.js parse plumbing for the math option
(mathParseOptions / hasMathRenderer cache reset), so the settledText /
transformSource patch hunks were re-applied by hand onto the new shape;
all other patched files were byte-identical between 0.6.1 and 0.6.2.

Generated-by: Devin

* refactor(ui): render Markdown math through upstream components.math

Replace the private-use TOKEN transport with Astryx 0.6.2's official
math pipeline: components.math renders parsed math nodes through the
same KaTeX configuration, and transformSource now translates the host
delimiters into upstream grammar instead of smuggling tokens past the
parser.

\(…\) inline math is emitted as `$…$` guarded by zero-width spaces so
adjacent digits or dollars cannot trip upstream's currency checks, and
`\[…\]`/`$$…$$` emit `$$` lines that repeat the enclosing quote/list
margin so containers still own them. Bare `$` outside code and link
destinations is escaped, preserving the "shell variables and currency
stay literal" contract that upstream's bare-$ pairing would otherwise
break. Display math on a table-row line degrades to an inline span so
`$$` lines never split a row.

The regenerated patch adds one upstream fix: trimStreamingArtifacts'
unclosed-marker scans ([, *, ~~) now skip completed inline math spans;
previously a last-line formula containing those characters was trimmed
or auto-closed as Markdown.

Generated-by: Devin
* feat(mcp): forward tools/call progress to Host

Wire SDK onprogress through McpClientManager.callTool into the CLI
capability provider's existing Host progress hook. Drop incomplete
or invalid step counts so a noisy server cannot fail the tool.

Refs apache#5069

Generated-by: Pi

* fix(mcp): harden forwarded tool progress

* fix(mcp): accept emitProgress on tools/call progress

Desktop in-process MCP tools pass McpToolProvider.emitProgress into
McpClientManager.callTool. Only onProgress requested a progress token,
so local Desktop tools stayed frozen while TUI/sidecar forwarded steps.

Refs apache#5069

Generated-by: Pi
Point client_uri at the Apache product homepage and set software_id
to the running clientName so TUI is no longer registered as desktop.

Refs apache#5072

Generated-by: Pi
…ache#5532)

* perf(desktop): commit the session catalog at the granularity of change

sessions:changed already carries the changed row's id, but the shell
answered every hint with a full sessions.list() and committed fresh row
objects, so one background session's event stream invalidated every row
reference and re-rendered the whole AppShell tree each commit (apache#5441).

- Add sessions.get (host session.catalog.query{kind:'get'} -> IPC ->
  preload) with the same runningTurnIds merge and pendingCleanup filter
  as sessions.list.
- handleSessionChange folds same-id hints into one sessions.get per row;
  a failed row read falls back to a deduped full refresh instead of
  evicting the row. Membership changes still take the full-list path.
- commitSessions reconciles by id and commitPatch upserts one row:
  published references change iff values change, and a stale snapshot
  never regresses a row patched to a newer revision.
- AppShell subscribes at the granularity it displays (count, active row,
  membership set, the two draft rows); the rail, archive/tasks pages,
  turn-request inbox, palette and setting-intent read the catalog where
  they consume it instead of through a shell-carried sessions array.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(desktop): observe a side chat only while its panel is visible

A retained Side Conversation (PR apache#5440) kept its fork observer alive
across session switches: mounted-but-hidden panels still received every
fork event, ran the live-turn reducers, and re-rendered a transcript no
one could see - ~981 DOM mutations/s and ~440 task ms/s per hidden
running panel in measurement.

The fork's observation period is now the panel's interest period:
QuoteCompanionPanel already receives active = visible && selected; the
hook releases the observer when it goes false and re-seeds through the
existing lost-subscription recovery path when it returns (seeded events
replay, then readSettledMessages reconciles the durable transcript).
commitFork resolves send readiness without an observer when inactive.

While unobserved no new turn can start - the panel is the fork's only
writer - so the tab activity indicator can only freeze at "running",
which a returning re-seed corrects.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ui): reconcile transcript timeline and fold entries at item granularity

A live turn rebuilt its whole timeline per event, so every memoized entry
re-rendered on each delta even when only the streaming tail moved. Extend
the turn-level identity contract one level down: reconcileTimelineItems
hands back the previous object for every timeline item whose value is
unchanged, keyed by timelineItemKey so mid-timeline inserts (steering)
do not shift the comparison. reconcileFoldedEntries does the same for
foldTimeline's output, matching processing folds by their stable anchor
id and children identity; it also refuses to return a stale array when
entries leave the fold.

With item identity carried through, TurnTimelineEntry and ProcessingBlock
become memo boundaries and the settled prefix of a long turn — dozens of
tool rows and reasoning blocks — no longer re-renders per token.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(desktop): dedupe onboarding snapshot emits and serialize pulls

sessions:changed fired per background message event, and every event
drove a full getSnapshot IPC (4+ parallel queries in main) plus a
setSnapshot(newObject) that re-rendered AppShellContent at event rate.

Two changes on the same publish-iff-value-changed invariant:

- setSnapshot now publishes only when the render projection changes.
  `sessions` is excluded from the projection: it is boot-time seed data
  (the session catalog is the live authority) whose rows churn per
  event; everything onboarding UI renders — state, milestones,
  connections, defaultSlug, chatModelChoices, sessionSendOutcomes —
  still propagates. Call-time refs stay unconditionally fresh.

- pulls are serialized: an invalidation while one is in flight sets a
  dirty bit and collapses into a single follow-up, so IPC rate tracks
  pull latency instead of event rate. The inflight clear lives inside
  the loop so no microtask window can swallow an invalidation.

Measured under ~124 sessions:changed/s: AppShell-level chrome writes
drop out of the hot path entirely once combined with the palette
subscription sink.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(desktop): sink the palette session subscription into its consumer

AppShellContent subscribed selectPaletteSessions at the shell level even
though the data only feeds command-palette session rows. Background
session churn (activityAt reorder, isFlagged flips) emitted a new
visibleSessions per commit and re-rendered the whole shell subtree —
~650 DOM attribute writes/s under ~124 sessions:changed/s.

The subscription now lives inside useAppShellCommands in the overlay
layer, where the list is consumed; commandOptions carries
hiddenSessionIds instead of a materialized session array. While the
palette is closed the selector is a constant-empty function, so catalog
churn cannot emit at all — the residual 'other' bucket under background
steering drops to ~23 mutations/s, all real status-dot updates.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(desktop): sweep session retirement against the committed catalog

`handleSessionChange` fed the fetch result of a `sessions:changed` event
into `retiredSessionIds`, whose contract is the complete catalog. On the
single-row path the result is one row, so every background update retired
the selected session and cleared its transcript.

The sweep now reads `sessionsRef`, which mirrors the catalog after commit;
both refresh promises resolve after their commit, so the timing is safe.
The handler moves to a leaf module so the regression test can exercise the
production code path without mounting the hook.

Reported by kabi-sol on PR apache#5532.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(desktop): sink the stale-session selector into the rail provider

The shell subscribed `selectStaleSessionIds` only to pass the set through
to `SessionNavigationProvider` — a whole-tree scope for rail-only state,
and a new call site the apache#4109 hook gate rejects. The provider now selects
it from the catalog it already owns, taking `sessionSendOutcomes` as the
prop instead.

The three selector calls that remain in the shell body — session count
and the two revision-draft rows — are ones the shell genuinely reads, so
they are recorded in the hooks inventory rather than moved.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(desktop): publish the catalog only when its content changed

`commitSessions`/`commitPatch` bumped `revision` and replaced the snapshot
even when every row was reused, so a no-op event still notified every
subscriber. Now a commit that reuses all rows returns early — except the
first commit, since revision 0 means "no authoritative observation" and an
empty list is still one.

The row equality check also switches from `summaryValuesEqual` to the
fail-closed `valuesEqual` shared with `@maka/ui`, so a non-plain field can
never compare equal by walking zero keys.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(desktop): make the onboarding dedup key exhaustive

`onboardingSnapshotProjectionEqual` compared a hand-maintained field list,
so a new `OnboardingSnapshot` field would silently drop out of the dedup
key and stop publishing. A `satisfies` witness over
`Exclude<keyof OnboardingSnapshot, 'sessions'>` makes a missing field a
compile error.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(desktop): share the session-id set comparator across features

sessionIdSetsEqual lived in the conversation feature, so the navigation
provider importing it crossed the feature boundary the renderer
architecture ledger forbids. It is a leaf comparison over session ids —
move it to src/shared/ where both features can reach it.

Regenerates the architecture ledger for the new/changed files on this
branch (session-change-effects module, the catalog selectors' new call
sites, sessions.get bridge path).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(desktop): move session-catalog infrastructure into contracts

The strict-base ratchet forbids new feature-to-legacy edges and any
capability growth in legacy files, so the catalog machinery moves to the
layers that own it: external-store state, selectors and change effects
live under application/contracts/session-catalog, and the
window.maka.sessions patch drain lives on the platform adapter. Root
modules keep their import surface as re-export shims; every feature now
imports the contracts paths, which deletes all 15 budgeted
feature-to-legacy catalog edges rather than adding new ones.

Subscriptions sit at their consumption points: the command palette
selects its session rows internally, the archived-tasks page receives
sessions through a render-prop component, and the revision-draft watch
runs inside CatalogRowWatch. AppShell gains no hooks — its inventory
drops to 36 hooks / 60 call sites — and no legacy file grows tokens,
hooks, bridge paths or dependencies relative to the merge base.

* chore(desktop): drop legacy catalog shims and dead exports

Knip flagged five root re-export shims whose last consumers can all
point at application/contracts/session-catalog directly, so the
compatibility layer is no longer earning its keep. Repoint the six
remaining importers and delete the shim files.

Also remove selectSessionCount/selectCatalogRevision (no consumers;
the revision fence reads catalog.getState() directly) and drop
sessionMatchesRail from the session-navigation barrel while keeping
the testing.ts export the controller test uses.

* fix(desktop): fence revision-draft retirement against catalog admission lag

CatalogRowWatch reported a just-created revision owner as absent the
moment sessionIds flipped, and the shell retired the draft on that
absence — clearing the edited text and silently failing the send. The
owner's created-row read is asynchronous, so absence at watch-switch
time is admission lag, not removal.

The watch now distinguishes three states per id: observed, removed by a
targeted read (commitPatch(id, null) tombstones it — a list omission
cannot testify about a row it predates), and pending. Retirement only
fires for observed-or-removed rows, keeping genuine deletion cleanup
intact.

A patch-admitted row could still be evicted by a list snapshot taken
before the admission, so commitSessions gains an observedAtRevision
fence: rows confirmed after the snapshot's observation point survive
the commit, the membership-level analogue of the per-row staleness
fence. The refresher stamps each fetch with the catalog revision at
issue time.

The composed sequence — owner switch before admission, tombstone,
stale list, authoritative eviction — is pinned in
session-change-retirement.test.ts.

* fix(desktop): keep locally-owned Sessions out of targeted catalog reads

sessions.get can only answer for Host-owned rows, while the merged list
also carries pending Sessions the local store still owns. Emitting a
sessions:changed event with a pending Session id routed it through the
row-level drain, whose sessions.get returned null and evicted the row.
The sweep then retired the selected Session and cleared the transcript,
leaving the shell on the new-task hero after every first send.

The local-change emitter now drops the row id while the store holds the
creation intent, so pending changes refresh the merged list; admission
clears the marker and the targeted path resumes. On the renderer side, a
row-level read retires only its own tombstoned id: an unrelated event can
no longer read a still-uncommitted selection as deletion.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…pache#5471)

* fix(desktop): latch transcript replica read failures instead of re-arming

A transcript page-read rejection used to re-arm unconditionally: advance()'s
finally queued another catch-up whenever the announced watermark still led the
durable one, so a replica bound to a dead subscription spun an unbounded
microtask rejection storm — observed pinning a Maka main process near 75% CPU
and starving IPC.

Runtime Host read failures are permanent for the subscription a replica is
bound to, so the first one is now latched: later advance() calls and
transcript reads reject with that same error — ahead of the generic closed
state — and only a settled catch-up re-arms, once, to cover a watermark that
moved while it ran. Non-subscription failures still propagate unlatched so a
transient page read can retry on the next announced frame, and renderer
delivery failures keep their existing path and never latch.

advance() also loses its explicit target: the subscription already records the
newest announced watermark before handing out the frame, so the replica reads
it from the handle rather than mirroring it in a second field. Recovery stays
with the subscription owner, which replaces the replica or the subscription
by its existing classification.

Generated-by: Devin

* fix(desktop): dispatch subscription pump failures before the close handshake

A frame handler rejection inside the pump's for-await only reached the catch
block after the loop's implicit return(), which awaits the subscription's
close acknowledgement — under a rejection storm that handshake never lands,
so failure handling never ran and the failed attempt's replica stayed armed.

Frame errors are now dispatched inside the loop: the attempt is failed and
replacement or terminal teardown is kicked off before the iterator is left,
so teardown no longer queues behind the close handshake.

Generated-by: Devin

* refactor(desktop): reseed evicted transcript replicas on the live subscription

Eviction recovery used to replace the whole subscription — a new handshake,
a new replica, and retiring-subscription frame buffering — even though the
subscription was still alive and only the durable tail was missing.
Reseeding reads the current tail at the live watermark on the same handle,
installs it through the same path as prepare(), and lets advance() cover
whatever was committed during the fetch. The owner swaps attempt.replica
only while the attempt is unchanged, so a concurrent recovery is never
displaced; consumers reset through the existing generation mechanism.

Deletes refresh(), the retiring phase, pendingFrames buffering, and the
MAX_PENDING_* constants. The two owner-level tests that exercised refresh
directly are replaced by reseed tests covering same-subscription rebuild,
catch-up across the fetch window, and supersession by recovery.

Generated-by: Devin

* test(desktop): deduplicate subscription test fixtures

runtimeHostSessionFixture now defaults transcript and events to empty, and
hosts the shared AsyncFrameQueue, continuitySnapshot, and transcriptPage
that several test files each defined locally. Removes the empty-transcript
literals and three duplicate helper definitions.

Generated-by: Devin

* fix(desktop): commit reseeded transcript replicas inside the owner's swap

The reseed swapped attempt.replica and closed the evicted replica while
the observer's state still referenced it, installing the new one a
microtask later. A transcript_advanced landing in the gap called
advance() on a closed replica, whose rejection classified as terminal —
the failure class this branch exists to eliminate. A recovery interposing
in the same gap closed the freshly swapped replica and left state.replica
and attempt.replica permanently diverged.

The observer's install now runs as an installReseededReplica dep inside
the owner's staleness check, so state.replica moves before the evicted
replica closes — the same atomicity activate() has. The failed attempt
detaches before the first teardown await so a reseed cannot swap onto a
corpse, and discard()/trimDurable() check residency before liveness since
recovery windows legitimately leave closed replicas in state.

A reseed read failure also had nowhere to go: session.transcript.page
not_found — the class recovery exists for — propagated raw and stranded
the session on a dead handle. It now routes through #failAttempt exactly
like a pump-frame failure, and onChange no longer caches snapshots from
replicas that were never installed.

Adds the coverage the audit surfaced: recovery on a failed reseed,
concurrent-reseed serialization with byte-balance assertions on
superseded builds, evicted-replica advance and mid-flight watermark
tests, and an observer-level evict-to-reopen test behind a
transcriptGlobalCacheMaxBytes dep.

Generated-by: Devin

* docs(desktop): state the installed-replica liveness invariant

state.replica can legitimately hold a non-resident or closed replica —
eviction and recovery windows both produce that — but the invariant was
implicit, so each consumer encoded its own assumption (discard() and
trimDurable() asserted liveness first, which killed unrelated sessions
under cache pressure). Writing the legal state set and the single swap
authority down where the pointer lives.

Generated-by: Devin

* fix(desktop): route the reseed commit's catch-up through the attempt

The install callback ran consumer resets and the budget pass before
moving state.replica, diverging from activate()'s pointer-first order
and undercounting the new replica's bytes in that pass. The pointer now
moves right after the only throwing steps: a throw still leaves the
state on the evicted replica for the owner to close, while resets and
accounting see the installed replica.

The post-commit catch-up moves out of the callback and into the owner,
where its rejection takes the same #failAttempt path as a pump-frame
failure. Swallowing it inside the install left a latched-dead replica
reporting resident on a quiet stream.

Generated-by: Devin

* fix(desktop): close the reseed survival gaps found by adversarial review

- feed the surviving projector the reseeded tail: rows that went durable
  while a replica was evicted never reached its durable-message map, so
  steering suppression and message admissions stayed stale until the next
  subscription recovery rebuilt the projector
- report resident=false for a latched replica: a dead read model now takes
  the same reseed/recovery path as an evicted one, and ack/read gates stop
  throwing the latched error into unrelated IPC
- let the pump's authoritative error land before reporting a masked
  'connection_closed' from a racing reseed fetch — a recoverable close
  could otherwise be inverted into a terminal teardown
- wait out in-flight recovery before a losing reseed returns, so callers
  pick up the installed replica instead of erroring on the closed one
- protect the just-installed replica from its own install-time budget pass
- drop dead watermark bookkeeping on evicted replicas — nothing reads a
  husk's durableThrough now that reseed always builds a new replica

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(desktop): classify masked reads by the subscription's death certificate

The fourth adversarial round found two real defects in the previous fixes:

- The setImmediate deferral only covered the case where the pump's report
  was already queued; a pump blocked inside acceptFrame's round-trip still
  lost to the masked connection_closed. The subscription already knows its
  own death reason — expose terminalError/closedReason and let #failAttempt
  prefer that certificate, which heals the reseed, acceptFrame, and
  post-commit paths deterministically and makes the deferral unnecessary.
- The latch caught every RuntimeHostOperationError, so a transient blip on
  the swallowed post-settle re-arm became a sticky terminal on the next
  frame — a regression over main's silent retry. Narrow the latch to
  failures that prove the subscription or its transcript context is dead.

Also assertTranscriptReadable now throws the recorded terminal error when
one exists, so every transcript reader sees the real failure.

Tests: the masked-close classification test was vacuous (the pump's chain
is strictly shorter, so it could never exercise the race) — it is now
driven by the certificate, plus a terminal-removal variant. Add coverage
for loser-branch terminal propagation, transient operation failures
staying unlatched, acknowledgeTranscriptTail on a latched replica, and a
latched replica reseeding through recovery end to end.

Generated-by: Devin

* fix(desktop): harden subscription death classification under adversarial review

- Record the Host close reason before the queue offer so a full client
  queue cannot discard session_removed/access_revoked as slow_consumer.
- Prefer closedReason unconditionally over terminalError: the Host's own
  death statement is recorded first, so it always outranks the
  synthesized connection_closed mask a dying transport stores later.
- Normalize subscription.open/not_found at the terminal boundary to the
  removed-session path so a recovery losing to a deletion still emits
  sessions:changed deleted instead of a generic observation error.
- Exempt TranscriptCacheCapacityError from attempt failure: the rolled-
  back charge is not a subscription failure, and tearing a healthy
  subscription down under memory pressure frees nothing. #failAttempt
  now reports whether it consumed the failure so the pump keeps
  consuming absorbed errors instead of exiting unconditionally.
- Keep throwing steps before the pointer move in activate() and release
  prep bytes before clearing the adoption flag, matching the install
  contract.
- Add the required certificate members to the CLI test fakes (the new
  interface members broke the packages/cli build) and cover
  terminalError/closedReason on the real subscription client.

Generated-by: Devin

* fix(desktop): invert the failure channel's default to absorb, not death

The classification at #failAttempt was "unclassified error = terminal",
so every new failure shape reaching the channel needed an exemption or
it tore a healthy subscription down — the capacity RangeError, transient
Host operation errors through the pump, and the next undiscovered shape.
The invariant is now stated at the boundary: an attempt dies only on
positive evidence of subscription death — a recoverable subscription
failure or a subscription-scoped error — and unclassified errors are
absorbed, but only for read-only callers whose work is safe to retry on
the next frame (a catch-up's watermark is still ahead). Non-transcript
frame failures may be committed mutations that will not be replayed, so
they still die loudly. #failAttempt reports whether it consumed the
failure so the pump keeps consuming absorbed errors.

- Drop TranscriptCacheCapacityError: the whitelist makes a dedicated
  exemption type unnecessary.
- Make terminalError/closedReason optional on the public
  RuntimeHostSessionSubscription — they are owner introspection, not
  contract, and fakes that never produce a certificate need not declare
  them. Reverts the four CLI test-fake additions.
- Keep the certificate recording order (reason before the queue offer),
  closedReason's unconditional precedence, the open/not_found deleted
  normalization, and the activate/adopt ordering fixes.

Generated-by: Devin

* refactor(desktop): ablate redundant diff from the replica change

Reverts adoptResidentAccounting to the flag-then-release order: the
release can only throw on a prior accounting corruption, which a retry
cannot repair, so guarding it was dead weight.

Drops the generic-Error no-latch test — its protection is strictly
subsumed by the transient-operation-error case, which sits on the
instanceof boundary and additionally fails if the latch stops checking
the operation code.

Generated-by: Devin

* refactor(desktop): collapse the death certificate into one authority

The second ablation pass removed the pieces the audit proved derivable:
- one certificate getter (`deathCause`) replaces the terminalError/
  closedReason pair; the reason-to-error taxonomy moves to the client
  package where the recording order lives
- SubscriptionAttempt.phase was the candidate pointer restated; the pump
  and failAttempt read #candidate directly
- the reseed catch's instanceof pre-filter was a third classifier; every
  error now goes through failAttempt and only unclassified ones reach the
  caller
- reseedTranscriptReplica returns void; both callers re-read state
- owner deps no longer take a clock it never read
- the evicted replica's watermark fallback was dead: throughSequence null
  already means the newest page

Two dominated owner tests were removed and replaced by the missing half
of the absorb contract: a committed non-transcript frame failure must
still terminate the attempt.

Generated-by: Devin

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…an (apache#5531)

* feat(core): carry the anchor's transcript index on a Recall passage

A Recall passage names its anchor by message id, which is enough to identify
the message but not to point at it: the transcript reader addresses rows by
sequence, so a caller holding only an id has to page the transcript to find
the anchor. That is the whole cost a UI pays when it wants to open a result
and scroll to it.

`buildPassage` already locates the anchor with `findIndex` over the same
transcript it assembles the passage from, so the index is in hand; carry it
as `sequence`, the coordinate `runThreadSearch` already reports as
`SearchResultTarget.sequence`. `expandRecallPassage` rebuilds through the
same function, so a widened passage keeps the coordinate.

Purely additive: no caller changes, and the field is defined on every
passage that exists because `buildPassage` returns undefined when the anchor
is not found.

Generated-by: Claude Code <noreply@anthropic.com>

* refactor(core): move the transcript-search primitives out of thread-search

Recall and the Agent's global history search both fold transcript text and
classify what they matched, so they import `foldForMatch`,
`MAX_SESSIONS_SCANNED`, and `threadSearchMatchKind` from `thread-search.ts`.
That put a second retrieval implementation's home on the path of two
surfaces that do not perform a thread search, and it is about to become a
dangling import once the Desktop's thread-search lane is replaced.

Move exactly those three to `transcript-search.ts` — pure functions and one
constant, no storage, no privacy state, no search envelope — and have
`thread-search.ts` re-export them so its published surface is unchanged.
`recall` and the Host composition import the new module directly.

No behavior change: recall's suite and the thread-search text-projection
suite both pass unchanged.

Generated-by: Claude Code <noreply@anthropic.com>

* feat(runtime-host): serve recall over the protocol as recall.query

Recall has only been reachable by the model, through the `Recall` tool. Its
implementation lives in the Host because the corpus does: the Session manager,
the distilled-fact store, and the material fetch are all Host-owned, so a
Client cannot perform a recall without dragging every transcript across the
boundary.

Expose it as `recall.query`. The coordinator holds no retrieval logic — it
calls `runRecall` and projects the answer — and it is constructed from the
same `recallDeps` the model's tools use, so a Client search and a model recall
cannot diverge in what they can see. No turn is excluded here: a tool call
runs inside a turn and must not echo that turn back, but a Client search is
not inside one.

The call this replaces, the Desktop's `runThreadSearch`, scanned up to 200
Sessions and ranked by substring order. Recall narrows candidates, ranks with
BM25, and returns distilled facts and context-carrying passages alongside the
hits. Its passages now carry the anchor's transcript index, which is what a
Client needs to scroll to a result.

Compatible extension, declared rather than bumped: a peer that predates
`recall.query` never sends or receives one, so every existing frame decodes
unchanged.

Generated-by: Claude Code <noreply@anthropic.com>

* feat(desktop): search history through recall instead of a local scan

The Search modal answered by pulling every Session's transcript into the
main process and scanning it: up to 200 Sessions per page, ranked by
substring order, returning a flat list of matched lines. Recall already
answers the same question better — it narrows candidates in storage, ranks
with BM25, and returns context-carrying passages — and it runs where the
corpus lives.

Switch the lane end to end. `search:recall` replaces `search:thread`; the
preload fan-out keeps its shape, asking every ready Owner Host and
interleaving the answers, because a score from one Host's corpus is not
comparable to another's and the merge must not pretend otherwise. Each Host
scans only its own history, which is also why this stops hauling transcripts
across the boundary.

The modal keeps its shell, its cancellation semantics, and its navigation —
a passage carries its anchor's transcript index, so a click still lands on
the turn that matched.

One user-visible change worth naming: recall matches literal terms, OR
combined, so a typed phrase is split into its distinct words. A multi-word
query ranks passages containing more of them higher rather than requiring
the exact phrase.

Delete the old lane: `runThreadSearch`, its main-process IPC, its preload
fan-out, and their tests. `foldForMatch`, `MAX_SESSIONS_SCANNED`, and
`threadSearchMatchKind` move to `transcript-search.ts` because recall and
the Agent's global history search still share them.

Generated-by: Claude Code <noreply@anthropic.com>

* test(search): cover the recall lane, and fix the cancellation it caught

The replacement deleted four test files and added one, which left every new
module untested: the IPC relay, the multi-Host merge, the query-to-terms
split, and the coordinator's projection. That is the wrong trade — the old
lane's tests were the only thing pinning behavior these modules now own.

Cover them, and keep the assertions on what each layer decides rather than on
the framework it runs in:

  - the IPC relay: what it refuses, what it answers when the Host is gone, and
    that an abandoned request stops being ours to wait on;
  - the merge: interleaving, partial failure, and that a score from one corpus
    is never compared against another's;
  - the query split: a phrase becomes its distinct terms, capped and deduped;
  - the coordinator: end-to-end over Host deps, the wire projection, and that
    a Client search excludes no turn because it is not inside one.

Writing the cancellation test found a real defect: the handler marked a
boolean and then awaited the Host, so a cancelled search held the IPC channel
open until a Host answered a question nobody was waiting for. Race the read
against the abort instead, and keep the reconnect policy's rethrow visible
rather than swallowing it.

Generated-by: Claude Code <noreply@anthropic.com>
Report + per-task CSV for the three-arm file-editing tool contract experiment on Terminal-Bench 2.1 with deepseek-v4-flash: str_replace_editor vs @deepseek-ai/dsh-tool-fs vs a repo-authored apply_patch plugin. No detectable difference (56/56/53 of 86 scored tasks); the report states this as a failure to detect, not equivalence. Docs only.

Generated-by: Claude Code
…n projects

Merged after review. The Settings remote naming inconsistency is tracked as a non-blocking follow-up comment.
Remove the standalone Deep Research workflow while preserving historical chats, reports, stored events, and access credential compatibility.

Generated-by: Codex
…ache#5550)

* perf(desktop): stop republishing catalog bookkeeping to the shell

Row-level catalog patches still republished the whole active row to the
shell on every event: flag/unflag, unread and preview bookkeeping all
committed a fresh row object, and the observation channel re-sent a fresh
execution projection per frame. Each republish re-rendered the chat
surface — measured at ~2.7k rendered fibers per flag toggle, mostly
astryx controls rewriting DOM attributes.

- Gate the shell's whole-row reads with a field-aware equality that skips
  rail-only bookkeeping (activity, unread, flag, preview, revision,
  timestamps, subagent runtime) and compares every other field by value;
  fields added later republish until proven rail-only, so the failure
  direction is an extra render rather than a stale value.
- Compare execution projections by value before publishing: the channel
  resends an equivalent object on unrelated metadata events.
- Feed composer mentions from the renderer catalog instead of a full
  sessions.list() IPC per sessions:changed; drop the now-unused list and
  subscribeChanges ports from the conversation service surface.
- Subscribe the browser panel only while visible and reseed via getState
  on the way back; keep artifact poll results identity-stable when the
  list is unchanged.

Measured (Electron + react commit probe, 8 flag toggles on the active
row): 73 commits / ~22k rendered fibers -> 20 commits / 372 fibers; the
row diff stays isFlagged+revision and remaining renders are the rail's
own flag/selection update.

Generated-by: Devin

* perf(desktop): resolve review findings — export the row equality, mount the catalog above services

- Export shellSessionRowEqual through the conversation feature's testing
  surface; the test no longer deep-imports the controller file.
- Give the session catalog a React context mounted once in composition
  (createDesktopFeatureServices owns the instance): providers read it via
  useSessionCatalogController instead of a prop drilled through app-shell,
  keeping app-shell.tsx byte-identical to the ratchet baseline.
- Pin the wiring, not just the predicate: a mounted selectSessionById
  subscriber survives a rail-only patch and re-renders on a name change;
  setExecution holds publication for a fresh-but-equal projection; a field
  outside the rail-only list fails closed.
- The slash-menu story drives its session-update refresh through a catalog
  commitPatch (thinkingLevel bump) instead of a subscribed 'updated' event.

Generated-by: Devin

* test(desktop): mount the session catalog provider in the workspace identity test

useAppShellSessionWorkspace reads the catalog from SessionCatalogContext
now, so the probe needs the provider the composition mounts in
production.

Generated-by: Devin
…pache#5565)

`skill-draft-lifecycle` fails on most branches right now, including main, with
"Skill 调用失败,消息未发送" never becoming visible. Two independent races sit
between disabling the Skill and pressing Enter, and each one makes the rejection
the journey asserts never render:

- The toggle goes through the raw `window.maka.skills.setEnabled` bridge, not
  the Skills page, so nothing re-fetches the composer's `/` source. Until
  Runtime's projection drops the Skill it is still invocable, the send resolves
  it, and the send succeeds.
- Picking the Skill left the `/` menu open. While the editable reports
  `aria-expanded`, the composer swallows Enter as menu acceptance rather than
  sending, so the draft is never submitted at all.

Wait for both before pressing Enter: the editable back to `aria-expanded="false"`,
and the Skill gone from `listInvocable`. The second uses a new helper beside the
existing `waitForInvocableSkills`, which already polls the same authoritative
projection for the opposite transition.

Generated-by: Maka
* chore(provider): remove the Command Code GO provider

Command Code GO did not use a published API. It sent requests to the official
CLI's private `/alpha/generate` endpoint behind the same identity headers that
CLI presents (`x-command-code-version`, `x-cli-environment`, `x-taste-learning`
and friends), and its account-usage card read the same `/alpha/*` endpoints with
the same headers. The provider was withdrawn from the registry for that reason.

Removing the provider takes its whole surface with it, because everything else
existed only to serve it:

- the `commandcode-cli` Runtime adapter, its `/alpha/generate` wire, and the
  CLI identity headers it forged;
- the browser-assisted sign-in (`commandcode-browser-login`,
  `commandcode-login-ipc-main`, its renderer flow and section) that minted a key
  through the CLI's own loopback login;
- the account-usage read end to end: the `connection.usage.read` operation and
  its Host coordinator, the storage `connection_usage` ticket machinery, the
  `read_usage` ProviderAuthAction, the runtime fetch/parse, the core report
  types, and the settings usage card;
- the transport-acknowledgement gate, which existed only to tell a user that
  one provider presented another client's identity, and so is now an empty set.

The ordinary `commandcode` provider stays: it is a plain API-key connection over
the published Provider API, and it keeps the shared reasoning-effort table.

The `connection.usage.read` operation is gone, so the compatibility epoch moves
169 -> 170; a peer older than this epoch may still advertise or submit it.

Generated-by: Maka

* fix(provider): retire Command Code GO instead of removing it

Review [P2] on apache#5545: deleting the entry outright leaves an existing
connection *unknown* rather than *retired*. `isRetiredProvider` answers false
for a type no longer in the registry, so the readiness guard never fires, a
connection with a default model and an enabled model list reports `ready: true`,
and the send is admitted only to throw "Unknown provider type" deep in model
construction — the exact failure `connection-readiness.ts` documents above that
guard.

Keep the entry with `retired: true` and `runtimeAdapter: unavailable`, the
pattern apache#5544 used for OpenCode Free. A stored row stays identifiable and
displayable, readiness answers `provider_retired`, and the user lands on the
existing `blocked:all_connections_retired` state with its "Add a model
connection" call to action. `provider-catalog-contract` already pins the
retired set and asserts each entry keeps no Runtime adapter and stays out of
the add catalog, so this restores that entry rather than adding new machinery.

Also regenerate the Astryx surface inventory, which still listed the two files
this PR deletes and failed the CI step ahead of Typecheck.

Generated-by: Maka

* chore(desktop): drop the browser-login copy left by the GO removal

The sign-in section that read `browserLogin` is deleted, so its three locale
blocks — including the "sign in with your Command Code account" title and the
port-range failure text — are unreachable. Remove them with it.

* fix(runtime-host): release the retired usage grant

CI on this branch failed in `Qualify durable state against the published
baseline`:

  The released access credential carries grants this build cannot account for:
  connection.usage.read

Removing an operation leaves its grant in every credential a released build
already wrote, and `unresolvedPersistedGrants` reports a stored grant that the
protocol no longer defines and no migration entry names. The released fixture
carries `connection.usage.read`, so the forward roll refused it.

Add the `release` entry, which drops the grant on decode and records that its
authority went nowhere — the same treatment `turn.regenerate` and
`execution.inspect.resolve` got when they were removed. Without it the grant
survives in the record and is reported as unaccounted for on every read.

Generated-by: Maka
…he#5494)

* fix(runtime-host): stop progress views from inheriting attention fields

The handoff progress projection spread the previous attention view, so
stale fields like reason "retry_required" surfaced during an active
update — exactly the misleading staging + retry_required combination
reported in the diagnostics of issue 5476. Split HostHandoffView into
a discriminated union (attention carries reason, progress carries
phase) and build progress views explicitly instead of spreading.

Refs apache#5476 apache#5488

Generated-by: Devin

* feat(desktop): create the main window before Local Host reconciliation

Desktop startup awaited the Local Runtime Host connect/handoff chain
before building the window or registering IPC, so first paint was gated
on connect, generation checks, and on a version mismatch the full
managed update. The manager already registers its reconnecting IPC
router on construction, so construct it synchronously, create the main
window and local caches immediately, and run start() in the background.

A failed first connect now degrades the Local target to unavailable,
the same semantics post-start fatals already used, instead of closing
the router and quitting; retryLocalStart re-drives it in place with a
fresh epoch so the profile entry and local session cache survive, and
profile enablement routes the recovery dialog retry to it. The boot
no longer opens a launch progress window; the handoff surface still
opens on demand until the in-window surface replaces it.

Refs apache#5488

Generated-by: Devin

* refactor(desktop): mount the renderer without prefetching the onboarding snapshot

The pre-mount prefetch (retry + 2.5s timeout + prop plumbing + the
workHub bypass) existed only to skip a transient loading frame. Mount
immediately instead: the .maka-preload skeleton already covers the
load gap, useOnboardingSnapshot pulls after mount and re-pulls on
sessions:changed / connections:event, and a failed pull now falls back
to the empty-chat surface instead of suppressing it forever.

Generated-by: Devin

* feat(desktop): surface Runtime Host handoff attention inside the main window

Replaces the standalone startup/progress window with an in-window
attention surface. Host reconciliation now runs silently in the
background; progress stays invisible and only views that need a user
decision (attention state) are pushed to the renderer, which renders
them as a required dialog with the existing copy-diagnostics action.

- New runtime-host-handoff-surface publishes the current handoff view
  over IPC and routes renderer decisions back to the handoff submit.
- New preload bridge (current/subscribe/decide) plus a renderer
  overlay mounted above AppShell; localized copy follows the existing
  UiCatalog pattern.
- Deletes startup-progress-window.ts, startup-presentation.ts, their
  test, the onShow hook, the duplicated onUpdateProgress option (the
  onProgress callback already carries the same phases), and the
  renderer-architecture allowlist entry.
- Exports the formatted handoff presentation type so the main-process
  surface and renderer share one contract.

Generated-by: Devin

* refactor(runtime-host): drop the pre-dev.9 file-lock compatibility half-layer

Managed updates no longer interoperate with Host operators older than
dev.9, so the lease machinery that supervised a legacy child's
directory lock goes away:

- withLegacyFileUpdateLockLease and its .supervised marker producer are
  deleted; withProcessLifetimeFileUpdateLock keeps recovering stale
  .supervised + .lock directory pairs left behind by already-shipped
  builds and still refuses to steal a live legacy directory lock.
- withRuntimeHostManagedServiceLegacyOperatorLeases and the update
  command's lock-protocol probe, inheritedFds stdio plumbing, and
  RuntimeHostOperatorInvocation are deleted; retire now runs the
  current operator directly. The process-lifetime-lock-v1 capability
  stays in the operator echo so older updaters still detect current
  operators.

Trade-off to call out in review: upgrading FROM a pre-dev.9 Host is no
longer a supported path; the retire loses the crash-safety umbrella the
inherited leases provided for that case.

Generated-by: Devin

* test(desktop): cover the Runtime Host handoff overlay in Storybook

Exercise the in-window attention surface the same way the bridge drives
it in production: replacement consent clicks through to decide(), a
retry-exhausted view offers only cancel, and a progress view mounts
nothing.

Generated-by: Devin

* fix(desktop): put the handoff copy-diagnostics action on its own line

Text defaults to inline display, which pulled the ghost button onto the
last line of the detail paragraph.

Generated-by: Devin

* fix(desktop): keep background startup alive when the Work Board schema is unverified

A degraded Local start resolves instead of rejecting, so the continuation
still reaches registerWorkBoardIpc — where require_current throws without
a Host-verified schema. The throw aborted the rest of the chain, skipping
guest-session restore, interrupted-setup recovery, and remote profile
startup. Registration is now isolated and retried when a Local target
reaches ready, so a recovered Host completes the board setup too.

Generated-by: Devin

* fix(desktop): deliver the newest handoff payload when mounting the overlay

The snapshot fetch and the push subscription were in flight together; a
publish landing between them was overwritten by the older current()
response. Subscribe first and let the push win.

Generated-by: Devin

* refactor(desktop): source the handoff payload type from the bridge contract

The surface re-declared the wire shape it sends; the bridge contract is
already the single declaration both sides import.

Generated-by: Devin

* fix(desktop): mount the handoff overlay inside the locale providers

useUiLocale() throws without LocaleProvider, which lives inside
LegacyAppShell — mounting the overlay at Theme level crashed the renderer
root on first paint and the window never reported ready. Mounting it in
the provider subtree also scopes the dialog to the main surface instead
of the floating WorkHub window. Catches added for the snapshot fetch and
the clipboard write, which reject when the document is unfocused.

Generated-by: Devin

* fix(desktop): keep a pending handoff decision visible behind silent updates

Every update claimed the visible slot, so a concurrent handoff's progress
view could displace a pending attention decision — indefinitely for
manualRecheck views that never republish. Only attention views own the
slot now, recency-ordered, and the presentation locale resolves per
publish instead of once at boot. Unit coverage for the arbitration and
the decide fencing, plus a source guard pinning the overlay mount inside
the locale providers.

Generated-by: Devin

* fix(desktop): offer the default-Host recovery prompt for a failed Local start

A permanently failed Local Host left the app alive but stranded: the
recovery offer skipped the Local profile outright, so retryLocalStart()
was unreachable even though the whole retry machinery behind it was
already wired. Local failures now enter the same default-Host recovery
loop, with a two-button prompt (Retry / Keep Offline) since "Use Local"
is meaningless when Local itself is the one that failed.

Generated-by: Devin

* refactor(desktop): move the handoff overlay into the runtime-host-management zone

A new flat renderer file is forbidden growth under the renderer
architecture check and --strict-base rejects it outright, so the overlay
moves into the existing Runtime Host feature zone: a `handoff` port on
RuntimeHostManagementServices carries current/subscribe/decide, the
platform adapter owns the only window.maka access, and the component
consumes services through context — which also removes the provider-free
window.maka?.runtimeHostHandoff probe that silently no-oped whenever the
bridge was not up yet. Clipboard goes through the port like every other
copy action in the feature, and a copied toast restores the feedback the
retired startup window had. The stories wrap the real services provider
and adapter around a fake bridge channel instead of stubbing the
component's data source, and the architecture ledger is regenerated.

Generated-by: Devin

* refactor(renderer): drop the prefetch-era snapshot getters

getSessions/getConnections/getDefaultSlug and the refs feeding them were
seeding plumbing for the removed onboarding prefetch; nothing calls them
any more.

Generated-by: Devin

* refactor(desktop): activate remote profiles without waiting on Local reconciliation

startEnabledProfiles only drives remote targets, but it sat behind the
Local Host's whole start lifecycle — a handoff parked on a user decision
held every enabled remote profile hostage for the session.

Generated-by: Devin

* fix(desktop): raise the main window when a handoff asks for a decision

The retired startup window called focus() on every attention view; the
in-window surface must keep that pull or a required decision waits
silently behind a minimized window. One raise per revision keeps repeat
updates of the on-screen decision from stealing focus.

Generated-by: Devin

* fix(desktop): treat pre-ready Host reads as pending, not failed

With first paint ahead of Host readiness, mount-time refreshes now race
the Host coming up. A getDefault() rejection meant "still connecting",
but every background read reported it as a failure: five startup toasts,
a stuck memory pill, stale shell settings, and an onboarding snapshot
error that cascaded into a bogus connection-refresh error.

Gate background refresh reporting on default-Host resolvability and lean
on the existing ready-transition re-fire for recovery. Mutations and
post-ready failures still surface errors. The onboarding poller defers
the same transient rejection via a message check — importing the probe
would add a forbidden dependency edge in the architecture ledger.

Verified end-to-end on a real Electron boot: first paint ~170ms,
renderer mounted ~750ms, Host ready ~1.1s, zero startup error toasts,
and the onboarding provider list hydrates on the ready transition.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(desktop): reveal the window on the first painted frame

The window stayed hidden until the renderer's first React commit, so the
designed .maka-preload loading surface — meant to be the loading UI —
was never visible and perceived startup was bounded by React mount
(~750-940ms). Electron's ready-to-show fires as soon as the skeleton has
painted; routing it through the existing reveal gate shows the window
~500ms earlier while keeping inactive/hidden modes, deferred focus, and
the notifyRendererReady/fallback backstops intact. Perceived startup now
matches the UI-first goal: the window appears with the loading surface
while Runtime Host reconciliation continues in the background.

Generated-by: Devin

* feat(desktop): fire the window before the Runtime Host module graph

runtime-host-boot evaluates ~1100 compiled files before its first
statement, so window creation could not start until ~400ms after the app
was ready. early-window.ts holds the light slice the window actually
needs — storage root, settings, locale, diagnostics, the window
controller, the quit coordinator — and fires createWindow as soon as it
exists; main.ts imports it first so the remaining Runtime Host graph
loads while the renderer is already navigating. Window creation moves
from ~400ms to ~210ms on this machine.

The login-shell PATH probe now starts at module top and is awaited only
where a child process is spawned (Local Host start, remote profiles,
MCP) — it no longer sits serially ahead of the window (~100ms on a real
launch; e2e fixtures skip it as before). Independent small reads (client
instance id, Runtime Host startup) run in parallel.

window:notifyRendererReady is not a Host-scoped channel: it is
registered on ipcMain by early-window so the renderer's first commit
cannot outrun scoped-router registration. The quit coordinator and the
window controller keep their exact lifecycle semantics — before-quit
aborts in-flight creation, close hooks and diagnostics reach the Runtime
Host through boot-context late bindings.

Generated-by: Devin

* feat(desktop): show the window at construction in active runs

The previous commit deferred the Runtime Host module graph until the
window existed, but the window still stayed hidden until ready-to-show:
the user-visible surface arrived at ~500ms while the OS window itself
could have been on screen ~160ms earlier with its theme-matched
backgroundColor reading as a launch surface.

- `show: revealMode === 'active'` — active runs display the native
  window at construction; the persisted appearance still picks the
  right backgroundColor so the first visible frame is theme-correct.
  Hidden/inactive e2e modes keep show:false and the reveal gate.
- Saved-bounds/mkdir/appearance reads run in parallel ahead of the
  constructor; serialized they cost ~200ms of prelude.
- A maximized session restores via maximize() directly in active mode
  (the window is already shown, so the reveal-gate deferral no longer
  applies); hidden/inactive runs still defer to markReady.
- The firstWindowConstructed boundary moves from construction to the
  native 'show' event: the heavy module graph may evaluate once the
  window is on screen without starving the display path, and the
  launch-settle fallback keeps hidden/failed runs unblocked.

Generated-by: Devin

* feat(desktop): replace the preload skeleton with a brand surface

The fake app-frame shimmer (a card with two gradient bars) read as a
broken half-rendered UI rather than a launch screen. Codex's cold start
shows the better pattern: a theme-matched window with a centered mark.

- `.maka-preload` becomes the traced wordmark (the same MAKA_WORDMARK_PATH
  the hero and dock icon use) at --maka-brand on the theme background,
  with a slow opacity breath and a reduced-motion opt-out.
- `body` gets the hardcoded theme background (#ffffff / #1c1d21) so the
  native window's `backgroundColor` hands off to the first frame with no
  colour step.
- The renderer entry contract allowlists the three presentational tags
  the inline mark needs (svg/g/path); navigation and execution vectors
  stay closed via the single-module script check and the on*= scan.

Generated-by: Devin

* feat(desktop): hold the launch surface until the app reports a usable frame

The launch overlay moves out of #root into a fixed fullscreen layer, so
React mounts underneath it instead of replacing it. AppShell drops it once
the bootstrap snapshot resolves and no session view or transcript read is
still in flight, which removes the bare-vibrancy window, the partial
shell, and the skeleton beats from the startup sequence. main.tsx arms an
8s failsafe so a wedged read can never strand the logo. The window returns
to show:false — ready-to-show now reveals the first painted frame, which
is the launch surface itself, and restoring maximized state defers to
markReady in every reveal mode.

Generated-by: Devin

* fix(desktop): drop stale runtime host handoff publications

Locale resolution makes each publication asynchronous, so a payload
computed for a superseded or closed handoff could land after the newer
state and resurrect the attention modal. Fence each publish with a
monotonic ticket invalidated by update and close.

Generated-by: Devin

* fix(desktop): stop deferring onboarding errors once the host settles

The identity-unavailable deferral kept the snapshot pending forever when
the default Host never acquired an identity, hiding the composer
indefinitely. Gate the deferral on the authoritative profile readiness:
only 'connecting'/'reconnecting' means still pending; 'unavailable',
'disabled' or a missing entry now surfaces the error so the shell exits
its loading state.

Generated-by: Devin

* fix(desktop): anchor the boot barrier on the first painted frame

firstWindowConstructed could resolve at launch-settle before the first
frame was composited, letting the Runtime Host module graph contend with
the paint it was meant to follow. Resolve on ready-to-show instead — it
fires at the first frame for every reveal mode, including hidden runs
that never emit 'show'; launch-settle stays as the fallback resolver.

Generated-by: Devin

* fix(cli): reject operators that predate capability reporting

The status probe dropped its capabilityRequest when the legacy lease
path was removed, so a pre-lifetime-lock operator now passes the probe
and gets retired without the advisory lease it needs. Restore the
request and refuse operators that cannot echo it, so the failure is an
explicit unsupported-operator error instead of a lock-free retirement.

Generated-by: Devin

* fix(desktop): poke open WorkBoard panels once their IPC registers

workBoard:list rejects with "No handler registered" while the Local Host
is still connecting, and the panel's error state never self-heals
because nothing re-triggers the load. Emit workBoard:changed after
registration so a mounted panel reloads itself.

Generated-by: Devin

* fix(desktop): route launch-surface dismissal through bootstrap subscriptions

The hooks gate counts call sites in the shell body: the standalone
useEffect pushed AppShellContent past its inventory. Move the dismissal
into useAppShellBootstrapSubscriptions where the other bootstrap
reactions already live, and record the useLayoutEffect shrink from the
prefetch-era seeding removal.

Generated-by: Devin

* fix(desktop,cli): close residual host-readiness and repair-path gaps

- Onboarding: subscribe Host profile changes as a snapshot invalidation,
  so a deferred pending re-pulls when the default Host settles
  unavailable instead of pinning the skeleton forever.
- Update probe: emit the unsupported-operator refusal directly instead of
  throwing through the outer catch — the catch degraded it to "operator
  unavailable" under repair mode (defeating the capability guard) and the
  exactTargetObserved remap flattened it to a generic update_incomplete.

Generated-by: Devin

* chore(desktop): add missing ASF header to boot-context

Generated-by: Devin

* fix(desktop): let installUpdate proceed while the local Host is still starting

Deferring Host boot behind first paint made installUpdate reachable during
startup: retireOwnedLocalHost's 5s admission deadline expires mid-launch and
the retire throws, surfacing as install_failed in the Windows autoupdate
check. quitAndInstall follows immediately and the launch-owner guard closes
a half-started Host on exit, so an unquiesced Host must not fail the
install — same degrade the quit path already applies.

Generated-by: Devin

* fix(desktop): gate renderer IPC on the boot registration pass

First paint now precedes the Runtime Host module graph, so invokes fired
during renderer startup hit "No handler registered" instead of waiting for
their handler. Park every preload invoke behind app:bootstrapReady — which
main resolves once runtime-host-boot's top-level registration pass has
run — and defer the module-level browser:document-ready send the same
way, since a send dropped before its ipcMain.on exists never retries. The
gate fails open so a call's outcome is never altered, covers every invoke
site and future channels without enumerating them, and keeps
window:notifyRendererReady plus the diagnostics channels immediate.

The launch overlay was owned by AppShellContent, so the WorkHub surface
never dismissed it; drop launch-surface.ts and the onboarding-derived
launchSurfaceReady gate in favor of CSS-only dismissal keyed on #root's
first child, which fires on either surface with no renderer debt. Host
identity unavailability while the default Host is still connecting now
pends in preload's activeRuntimeHostRef on runtime-host-profiles:changed
instead of round-tripping a deferral through the renderer, so the
onboarding hook no longer needs the profiles bridge.

Generated-by: Devin

* chore(desktop): regenerate astryx surface inventory for the handoff overlay

Generated-by: Devin

* fix(desktop): drop redundant shell-settings refresh on Host ready

The preload identity deferral turns settings.get() into a pending read
while the default Host is connecting, so the ready-transition re-pull no
longer has a stale partial state to repair — and the legacy effects file
must stay at its base token budget.

Generated-by: Devin

* chore(desktop): drop dead barrel re-exports for handoff types

Generated-by: Devin

* test(desktop): surface app consoles on e2e failure

Attaches captured main/renderer console output to failed specs so CI
failures carry the evidence instead of needing a local repro.

Generated-by: Devin

* fix(desktop): keep revision send alive past a slow transcript open

prepareRevisionSend read the revised Session's transcript with a 480ms
budget that also covers transcripts.open, which waits on Host admission
and legitimately takes far longer under load. When it lapsed, the catch
ran rollback — whose navigation bumps the selection revision — and only
then checked selectionIsCurrent, which was therefore always stale, so
the failure was discarded without a toast and the send vanished.

A transcript replica that is still opening must not gate the send: the
Session view fills from its own consumer and the write boundary is Host
admission. tolerateOpenTimeout reports such an open as unsettled instead
of failing; real open errors and caller aborts still propagate. Failure
feedback now runs before rollback so rollback's own navigation cannot
invalidate it.

Generated-by: Devin

* fix(desktop): land e2e fixture workbar writes after Session activation

The Workbar keys collapse state per Session and withRightCollapsed drops
any write issued while its reducer has not activated a Session yet.
applyE2eFixture ran setActiveId and immediately dispatched the collapse
and openTool writes, which the reducer processed before the render-phase
activate-session landed — under UI-first startup the fixture is now the
first activation, so both writes were eaten and the audited surfaces
(.maka-session-workbar, .maka-browser-panel) stayed collapsed forever.

Set the selection before awaiting refreshSessions: the IPC round trip
lets React commit the activation, and a direct setWorkbarCollapsed
replaces the stale rightCollapsed snapshot + toggle, which could flip
the wrong direction when read late.

Generated-by: Devin

* fix(desktop): register the Host-change waiter before probing readiness

activeRuntimeHostRef awaited runtime-host-profiles:getSnapshot before
installing its profiles:changed waiter, so a transition push landing
inside the probe's round trip fired an empty waiter set and the read
parked forever. Install the waiter first and cancel it when the probe
shows the Host settled.

Generated-by: Devin

* fix(desktop): activate the e2e fixture Session only once catalog-observed

A runtime-host-profiles change runs a retire sweep that drops the active
Session when the committed catalog lacks it. With the Host still
starting, that sweep can land before the seeded fixture row reaches the
catalog, retiring the Session the fixture just activated and collapsing
the workbar the alignment audit waits on. Gate activation on catalog
membership — the same constraint a real selection has.

Generated-by: Devin

* refactor(desktop): move the fixture catalog wait into session-catalog-state

The committed fixture carries the wait inline plus a structural dep type,
which pushes app-shell-e2e-fixture past its legacyAppShell token budget
(750 vs 637). Export waitForCatalogSession from the contracts module
(which the ratchet does not price), collapse the workbarTab union check
and the sidebarSection if-chain into a lookup, and the file lands at 623.

Generated-by: Devin

* fix(desktop): drop the redundant transcript read from revision sends

prepareRevisionSend opened a second transcript consumer to verify the
replica before allowing the send. The Session view's own consumer already
loads history, and the write boundary is Host admission — so the read
existed only to span the window until commitTranscript made the new
Session the send target. With the Host reconnecting that open parks on
the router's candidate wait (~15s), and the tolerateOpenTimeout escape
still could not settle: cancelOpen is a noop until the open delivers its
close handle, and closing it never rejects the parked invoke, so the
deadline waited the full router timeout anyway (review 5263634692).

The send now takes the target explicitly via SendOptions.targetSessionId
(from the prepared draft), so neither the read nor the commit barrier is
needed: the read, the flag, and the preparation AbortController plumbing
all go away. onFollowLatest is skipped for an explicit target — a fresh
Session has no reading position to prepare — and isSurfaceVisible keeps
gating feedback on the committed view. The revision tests assert the
preparation never opens another transcript consumer.

Generated-by: Devin

* chore(desktop): refresh the astryx surface inventory

session-catalog-state gained the waitForCatalogSession export.

Generated-by: Devin

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(desktop): settle queue admissions before editing in side-chat spec

The spec pressed Enter on the third follow-up and clicked edit on the
first within ~100ms. beginEdit captures the queue revision at click time,
so when the third entry's Host admission landed inside that window the
update carried a stale expectedQueueRevision and was correctly rejected
with operation_conflict. The failed commit leaves the edit textarea open,
which replaces the row's queue-text span — the queue never reordered; the
first entry was simply hidden behind its own edit box while the error
toast reported the conflict.

Wait for the third entry's edit button to become enabled — enabled marks
Host-owned 'queued' state — before opening the edit, matching the spec's
own note that optimistic appearance does not settle send admission.

Generated-by: Devin

* fix(desktop): own the native menu through its popup lifetime

Two CI runs died mid-assertion after closePopup(): the main window's CDP
session closed because the native menu teardown crashed the process.

popupNativeMenu never retained the Menu it built, so a JS wrapper
collected while its popup is open can crash the native close path
(electron#20737 family). Hold each open menu until its popup callback
reports it closed.

The spec also closed the popup unconditionally. On Linux a popup can
auto-dismiss after window resizes — this spec resizes three times first —
and closing an already-dead popup hits the same teardown crash.
aria-expanded tracks the popup IPC resolution, so only call closePopup
while it still reports the menu open.

Renderer crashes previously left no artifact evidence; the fixture now
logs every page crash, including the restarted window's, into the error
context.

Generated-by: Devin

* test(desktop): move queue row semantics to component tests

The side-chat E2E asserted ComposerMessageQueue contracts through a real
Electron window: the revision captured at edit click, a rejected
stale-revision update leaving the row in edit mode (the misread that hid
the edited row behind its own textarea), and drag reorder passing the
Host-owned id list. All three are renderer-owned and now run in
packages/ui against the real component with stubbed callbacks.

Mutation-checked: closing edit mode unconditionally on a rejected update
fails the stale-revision test.

Generated-by: Devin

* test(desktop): migrate skill-draft lifecycle coverage to action seams

The deleted E2E asserted renderer-owned contracts: a refused send keeps
the draft, a retry reuses the already-prepared child instead of forking
another revision, and cancel restores the pre-edit draft text (Skill
token included). They now run deterministically against
createAppShellRevisionActions with a stubbed bridge, plus the blocked-
Skill toast through createAppShellChatActions. The transcript-settlement
gate that once raced the deferred React handoff was removed upstream in
apache#5494, which already covers it with a no-second-transcript-open test.

The success-path draft cleanup stays inline in sendWithAttachments —
untested glue, same as before — rather than earning a new seam here.

Mutation-checked: removing the retry early-return and restoring the
wrong draft text each fail their test.

Generated-by: Devin
The streaming label animated background-position linearly, which Blink
cannot composite — every frame repainted the glyph-clipped gradient for
the whole turn (~36% tree CPU measured on one label). steps(60) keeps
the same sweep at ~15fps for ~9%.

Cuts from auditing the same surface: the ejected ChatReasoning drops
props Maka never passes (duration, controlled expansion, theme
reflections nothing selects) and the eight shimmer atoms collapse into
one semantic .maka-reasoning-shimmer class; data-deep-thinking had no
readers.

Generated-by: Devin

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…pache#5566)

* fix(desktop): stop cached transcript previews offering earlier history

transcripts.open replays the locally cached tail before the live history
answer so the previous content is visible while the Host reads. That
snapshot carries the replica's tail bound as hasOlder, so the view
rendered a load-earlier control for a generation that cannot serve the
read (loadEarlier no-ops on cached:), then replaced it wholesale once
the live answer arrived — the "load earlier history" flash on every
session switch.

A cached generation is provisional by contract and every live-only
affordance already gates on it. Close the two leaks: range() reports
hasOlder only for generations that can answer earlier reads, and the
reading-position restore waits for the live answer instead of
concluding a bookmarked Turn is unreachable from the cached tail.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated-by: Devin

* fix(desktop): publish session transcripts once, behind a fade swap

Switching sessions used to paint the locally cached tail first — a
prefix-truncated copy whose hasOlder flag rendered a dead load-earlier
control — then replace it wholesale when the live answer arrived. The
cache now stands in only when the live open fails, which also removes a
per-open session-local disk read. The switch keeps the previous
transcript inert and dimmed while the live read is in flight, then fades
the new transcript in on its keyed remount.

Generated-by: Devin

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…he#5561)

The process disclosure's ::details-content animation grid had an implicit auto column track, which grows to the item's max-content: one unbreakable descendant inside .maka-processing-body widened the body past the 800px reading measure while the details' contain:paint boundary clipped every sibling at the column edge — the uniform mid-column text clipping seen in the release transcript. Pin the column to minmax(0, 1fr), give the sequence the 4px inline padding where the tool rows' intentional overhang lands, narrow the body's clip to the block axis the wipe needs, refresh the stale ProcessingBlock comment, and assert the contract in the disclosure story. Also drops dead transcript column declarations proven removable by live CSSOM ablation.
)

* perf(runtime-host): baseline session copy cost

* chore: normalize ASF headers

* fix(runtime-host): stabilize session copy benchmark

---------

Co-authored-by: likun <kunli@alva.xyz>
…pache#5534)

* fix(runtime): make subagent spawn selection explicit and recoverable

Expose callable catalog arguments, ignore inactive selector fields only in explicit modes, and provide actionable errors without changing legacy routing or parallel scheduling.

Generated-by: Codex

* fix(runtime): align missing selector guidance with spawn mode

Report the selected identity field when an explicit spawn mode lacks its selector, retaining legacy guidance for callers without a mode. Cover recovery with and without an inactive selector present.

Generated-by: Codex

* fix(runtime): tighten subagent recovery guidance
chihumyum and others added 25 commits October 2, 2026 13:12
AppShell owned context compaction: the running-notice presentation, the
/compact bridge call and the hand-off of the Host's terminal outcome.

Move all three into the Conversation owner. The controller creates the
one presentation, compactSession calls the injected sessions.compact
service, and ConversationLifecycle hands the outcome to the same
presentation. AppShell keeps only the stable compactSession command until
composer submission moves. The workspace-unavailable classifier moves to
application/contracts.

Refs apache#4582

Generated-by: Claude Code
* fix(desktop): default Workbar entry to the titlebar

Generated-by: OpenAI Codex

* test(desktop): cover Workbar entry modes in native WorkHub

Generated-by: OpenAI Codex
* feat(desktop): fit segmented switches to their content

A segmented control that spans its whole container became the heaviest
object around it. The Session rail's 按时间 / 按项目 switch outweighed 新任务
and read as one more nav row, and the import/export settings page stacked
two full-width bars under its title.

Give each switch content width and put it at the trailing end of the
heading for the area it governs:

- Session rail: a 任务 heading row with the grouping switch on its right,
  still in the sticky top region so it never scrolls away.
- 导入/导出任务: the source switch moves into the 来源 section header's
  action slot; the import/export mode switch keeps its place but hugs
  its segments.
- 远程接入 (quick-onboarding providers): the 快捷接入 / 手动配置 switch moves
  into the 接入方式 section header's action slot at size sm.

Generated-by: Claude Code (Claude Opus 5.5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(desktop): regenerate the Astryx surface inventory

session-list-panel.tsx now imports Text for the grouping heading.

Generated-by: Claude Code (Claude Opus 5.5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…nd retract (apache#5910)

* feat(cli): bind Tab and Shift+Left for TUI queue and retract

The TUI's queue (Alt+Enter) and retract (Alt+Up) chords are intercepted
by Windows Terminal and never reach the app on macOS Terminal/iTerm,
leaving both actions unavailable there. Add the macOS/Windows-safe pair
Codex ships for the same actions: Tab queues a followup while a turn
runs (only with a non-empty draft and no completion popup open, so the
editor keeps owning Tab for completion and idle input), and Shift+Left
retracts queued messages back into the editor. The Alt chords stay as
aliases, and /help plus the pending-bar hint name both routes in all
three locales.

Refs apache#3538

Generated-by: Maka

* refactor(cli): drop the Alt queue/retract chords and fix review failures

Follow-up to the first commit: the Alt chords conflict on macOS and
Windows terminals rather than merely being awkward, so they are removed
instead of kept as aliases. Tab (queue, during a turn) and Shift+Left
(retract) are now the only bindings. Queue retraction needs no separate
key — queue.retract returns both steering and followup queues.

Also fixes the two CI failures called out in review: the cross-locale
keybinding-token invariant (tokens are now 'Tab' and 'Shift+Left' in all
locales) and the pending-queue platform-render test (no alt token left
to rewrite, so darwin and linux render identically).

Refs apache#3538

Generated-by: Maka
…5904)

When the Runtime Host's resume planner confirms the latest interrupted
Turn can resume, the composer replaces Send with Resume while the draft
is empty; typing or staging context brings Send back. Availability comes
from the authoritative read-only turn.resume.query preview, surfaced
through a new sessions:queryResumeLatest IPC and tracked per session in
useShellResume, so the offer stays fail-closed: the click still re-runs
the full safe-boundary admission and can only ever park.

Generated-by: Maka (k3-256k)
…5794) (apache#5847)

A1 promises lossless layout refactors, but nothing in the workflow shows whether a change moved pixels. This adds the local verification loop: one script shoots the chat-surface story set in light and dark with a fixed clock and disabled animations, and compare reports changed pixels per story with diff images.

Deliberately not a CI gate and no baseline images — captures are local artifacts for the refactor's PR evidence.

Generated-by: Devin

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…euse (apache#5931)

The compactHistory fast path returned already_compacted on a raw-prefix
match alone, while the pre-send gate rejects the same checkpoint with
effective_history_changed. Fold the covered span through the current
projection transitions and require the pinned effectiveSourceDigest to
still match; on drift, fall through to the planner's roll-forward gate,
which discards the stale checkpoint and re-summarizes the current
effective view (apache#5929).

Generated-by: Devin

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…n-footer resume button (apache#5927)

The review-round suppression on apache#5904 hid the send-slot Resume offer for a
Turn the user just stopped and relegated resume to a small ghost button in
the stopped Turn's footer. That kills the primary use case: stopping a Turn
mid-reply and then deciding to continue it is exactly when the user looks at
the send slot, and the footer button was never covered by a test or story.

Restore the original apache#5903 behavior: after a manual Stop, with an empty
draft, the composer send slot offers Resume like any other resumable
interruption. The accidental-restart hazard the suppression guarded against
is narrow now: the slot only renders Resume after a fresh ready answer from
the planner, which requires the stop's terminal transition to have
completed, so a click during the settle window lands on a disabled Send; a
resumed Turn can simply be stopped again.

The tracker keeps the unconditional wins from the review rounds: stale-offer
retraction while a re-read is in flight, and refresh on Host rebound.

Fixes apache#5923

Generated-by: Maka (k3-256k)
…ache#5934)

* chore(desktop): check R2 root symbol uses and retained root hooks

The renderer architecture checker gains the rules apache#4582 needs to close
R2 by numbers.

rootSymbolUses (M0): a generated record, per feature public entry, of
the runtime symbols each root zone takes from it: appShell (the AppShell
family), composition, and bootstrap (bootstrap/ plus the guarded
main.tsx and app.tsx). Uses are attributed through named and default
imports, static namespace members including JSX members, and re-exports
followed through any module until a feature public entry. Namespace
escapes, wildcard or namespace re-exports over an entry, and runtime
import() or require of an entry are violations. Against the base the
record may only shrink, except for an export the same change adds to the
entry, measured on the materialized base tree, or a use moving one way
out of appShell into composition or bootstrap. The CLI lists each
admitted use.

Retained root hooks (M5): the renderer README now has one row per call
site the AppShell hook gate allows, naming its consumer, owner, allowed
capability and either the root reason or the removal module. The checker
reads the gate's ALLOWED literal without running or editing it and fails
when an entry has no row, when the row count differs from the gate
count, or when a row names a hook the gate no longer lists.

--report prints the M3/M5 completion measures: AppShell-family bridge
references and action factories, the Conversation transitional rows,
hook-gate entries without a row, and root symbol uses per zone.

Seven feature exports that only tests or Storybook read move from public
entries to testing.ts (or, for the WorkHub coordination lifecycle, to
its application contract). The README lists the remaining non-assembly
root exports outside Conversation with their consumer and removal module.

Generated-by: Claude Opus 5.5

* chore(desktop): compare root symbol growth by binding and bind table rows to calls

Review follow-ups on the R2 enforcement rules.

Root symbol admission now compares bindings, not export names. Each
public export resolves to its declaring module and local name, so
re-exporting an existing binding under a new alias no longer counts as a
new public export.

The AppShell closure stays outside the root zones, but its feature-entry
uses are now visible: --report lists them per file, and a --base run
prints each one a change adds, reported and never ratcheted.

Where a hook-gate entry has several call sites, each retained-root row
must name an identifier of exactly one of those calls in app-shell.tsx,
and no two rows may name the same call. The two useEffect rows now name
setWorkHubEnabled and defaultHostConnections.

Generated-by: Claude Opus 5.5
…ssion below the shell (apache#5935)

* refactor(desktop): own task readiness below the Composer

Move the task-readiness snapshot, its refresh revision and request fence
from AppShell into a persistent TaskReadinessProvider in Conversation,
mounted beside ComposerStagingProvider. The two Host reads reach the
feature through an injected TaskReadinessServices port and a Desktop
adapter. The transcript surface reads the notice through
TaskReadinessNoticeConsumer instead of receiving it as props.

AppShell now passes only the request projection, targets, refresh key
and workspace-picker command. The legacy use-task-submission-readiness
hook and the task-readiness-notice re-export are retired, and the public
entry no longer exports the notice derivation.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): let Session Settings own new-task mode choices

The new chat's Plan toggle, orchestration value and permission choice
were AppShell state (two useState calls and useNewTaskChoice).
SessionSettingsProvider already decided between the selected Session and
the new task for permission writes, so it now holds all three. The shell
reads them through the existing useSessionSettingIntent hook and writes
them through setNewTaskPlanMode, setNewTaskOrchestrationMode and
clearNewTaskPermissionChoice.

useNewTaskChoice moves to application contracts, because Session
Settings and the Conversation chat-model hook both use it.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): own Composer submission below the shell

Move the send-pending flag, the edit-and-resend draft and the submit,
follow-up and interaction-answer paths out of AppShellContent into a
persistent ComposerSubmissionProvider in Conversation. The provider
assembles the chat and revision actions, the staged follow-up and
createRevisionAwareOnSend. ConversationComposerRegion reads the provider
in the Composer slot. The shell keeps one stable command handle,
beginEditUserMessage, and supplies only commands it already owns:
navigation, catalog refresh, Workbar side chat and form answers, and
the selected Session's orchestration write.

The Host calls (submitMessage, newTasks.create, sessions.remove,
reviseBeforeTurn, abandonSessionCopy, and the sandbox-boundary and
question answers) go through ComposerSubmissionServices and a Desktop
adapter. Shared helpers move to application contracts or into the
feature. The submit construction and unused exports leave the public
entry, and the transitional adapter loses the commands only the moved
actions used.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): move Stop, Turn branch and local recovery under the Composer owner

The Stop and Turn-branch actions move from AppShell-family legacy files
into ComposerSubmissionProvider. Their Host calls, sessions.stop and
branchFromTurn, now go through ComposerSubmissionServices. The Composer
slot reads the published Session's Stop claim and receives onStop/stop
from the owner; Stop still notes the stopped Turn for the resume offer.
The shell's command handle gains handleTurnFooterAction and receives
the Turn-action pending registry as a port, because the shell still
renders that registry's mask.

SessionLocalMessages, the local delivery-recovery reader, now mounts
inside the owner for the published Session. Its recovery policy is
unchanged. The transitional adapter drops the Stop claim, the transient
add/remove commands and captureSelection, and the README's matching
transitional row is removed.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): address apache#5935 review follow-ups

- Move the missing-working-directory toast into
  application/contracts/session-workspace-errors.ts. Contracts cannot
  import copy catalogs, so callers pass the copy. The Conversation copy
  is deleted, and the chat, revision and Turn actions call the contract.
- Pin the steering Turn id: an owner test sends with followUpMode
  "steer" while a Host Turn runs and checks that the pending row
  carries that Turn's id before the Host answers.
- TaskReadinessProvider takes the recovery Session id and the stable
  recovery and Add Project commands instead of a closure built on every
  shell render, and memoizes the picker action. A shell render with the
  same facts no longer republishes the notice context.

Refs apache#4582

Generated-by: Claude Opus 5.5
…pache#5936)

* refactor(desktop): read transcript attachment bytes through the Conversation port

AppShell passed window.maka.attachments.readBytes to the transcript as a
prop. Declare readBytes on Conversation's attachment port
(ComposerStagingServices), whose Desktop adapter already returns the
bridge's attachments namespace, and have StagedQuoteChatView, the
feature's transcript ChatView, take the reader from it. The reader
contracts of ChatMessageSurface and StagedQuoteChatView drop the prop,
so AppShell can no longer supply one; app-shell.tsx goes from five
direct bridge paths to four.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): own WorkHub enablement in an application authority

AppShell read the client WorkHub switch from the Desktop bridge into its
own state and passed it to Workbar, the session rail and the WorkHub dock.
Create a WorkHub enablement authority in application/contracts beside the
WorkHub workspace contract, with its Desktop source injected at
composition like the session catalog. Workbar, the rail and the dock read
it directly; the rail and WorkHub's main-window navigation check it again
before opening WorkHub. WorkHubEnablementWatch hands AppShell only the
on/off edges for the navigation it still owns (workHubActive and the
destination).

AppShell loses settings.getClient, settings.subscribeClientChanged, the
workHubEnabled state, its ref and its effect.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): own onboarding in an application authority

AppShell owned the onboarding snapshot: the poller, its invalidation
subscriptions, the refresh after Settings closes and the bridge write
behind "skip setup". Move the poller into an onboarding authority in
application/contracts, created at composition with a Desktop source
(the former onboarding-snapshot-bridge, now also writing the skip
milestone on the default Host). The session rail reads per-Session send
outcomes from it directly. AppShell receives a read-only projection plus
refresh and skip through OnboardingProjectionRoot, the same render-prop
pattern as the other shell roots, and derives first-run gating, the
default-Host connection seed and the activation candidate from it.

A failed read is now a flag. The localized error text was never shown,
so nothing that might carry paths or tokens is kept. The snapshot types
move to src/shared so the application layer can name them; preload
re-exports them unchanged.

AppShell loses onboarding.setMilestone, useOnboardingSnapshot and the
sessionSendOutcomes prop it threaded to the rail.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): give the session catalog its Desktop source

The shell's catalog refresh called window.maka.sessions.list itself.
The session catalog now takes a SessionCatalogSource (full lists and the
change feed), injected at composition from the Desktop session catalog
adapter, and the shell refreshes through catalog.source. A catalog built
without a source, as in tests and stories, is detached.

use-app-shell-session-list.ts loses its sessions.list bridge path.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): move the root lifecycle's Desktop events behind injected sources

app-shell-effects.ts subscribed to seven Desktop bridge paths directly:
app.info for the document's platform tag, the window menu, connection
events, Host profile changes, Session changes and both settings-change
feeds. The reactions stay a root application lifecycle, since they
refresh several regions at once, but the environment leaves the shell:

- ShellLifecycleSources (application/contracts/shell-lifecycle.ts) is
  supplied at composition by a Desktop adapter that also writes the
  data-os tag.
- ShellLifecycleSubscriptions is the one subscriber. It takes Session
  changes from the session catalog's own source.
- useAppShellBootstrapSubscriptions keeps the startup refreshes, the
  hotkeys and the mounted flag, and returns the handlers.

Every handler now reads the latest render. The two settings-change
handlers used to capture the first render's connection refresh; they now
refresh the current connection projections, as the Host-change handler
already did.

app-shell-effects.ts loses all seven bridge paths and one effect.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): read turn landmarks through Conversation services

AppShell handed ConversationLifecycle an inline
window.maka.sessions.listTurnLandmarks reader. Declare listTurnLandmarks
on ConversationServices.sessions; the Desktop adapter already spreads the
bridge's sessions namespace, so it supplies the function unchanged. The
lifecycle passes it to the reading-position controller, and its prop is
gone, so AppShell can no longer supply a reader. The controller never
listed the reader as an effect dependency, so its identity becoming
stable changes nothing.

app-shell.tsx loses its last direct bridge path.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): require the root authorities' providers and test the lifecycle handlers

Review follow-up for apache#5936:

- The onboarding, WorkHub-enablement and shell-lifecycle contexts throw
  when their provider is missing, as createServicesContext does, instead
  of falling back to inert values that would, for example, hold the
  first-run gate closed. A catalog built without a source still accepts
  commits but fails when read through that source. Tests pass explicit
  fakes.
- The event-to-reaction mapping moves into the pure
  createShellLifecycleHandlers beside ShellLifecycleHandlers, so it is
  unit-tested without loading app-shell-effects.ts.
- AppShell imports WorkHubEnablementWatch from its contract; the WorkHub
  feature entry no longer re-exports it.

Refs apache#4582

Generated-by: Claude Opus 5.5
…ktop calls to their owners (apache#5937)

* refactor(desktop): route legacy diagnostics reports through the diagnostics feature

The Error Boundary, About and the command palette still called
window.maka.diagnostics.copyReport from legacy renderer files after
apache#5892 moved the root's diagnostics into features/diagnostics. Each now
receives one fixed-surface command from the feature's services instead:

- the Error Boundary takes copyRendererCrashReport from an optional
  RendererCrashReportConsumer; without Desktop composition it still
  renders its fallback and copies its own browser report;
- About takes copyManualReport from ManualDiagnosticReportConsumer;
- AppShell takes the same command through that consumer and passes it to
  the palette in its command options.

The Desktop adapter fixes the manual and renderer_crash surfaces and
forwards the same fields as before. DiagnosticReportToastProvider now
reads its action's words from the shared shell catalog itself, so
AppShell no longer reads shell copy to hand them over.

No renderer file outside platform/desktop references the diagnostics
bridge any more; the ledger drops six bridge paths, and re-adding any of
them is new bridgePaths debt.

Refs apache#4582

Generated-by: Claude Code

* refactor(desktop): retire the AppShell project actions into Task Entry

createAppShellProjectActions built twelve project actions for AppShell,
but nine of them (add, select, select-no-project, prepare, prepare-default,
relink, rename, archive, restore) had no production caller: the rail and
the new-task flows have gone through taskEntry.commands since apache#5527.
Delete them with their helpers, the project-picker state and refs that
only addProject used, and the onProjectSelected callback that only those
actions fired.

The two live actions move to Task Entry, whose README already names it
the Task Entry / Workspace owner:

- TaskEntryServices gains folders.openProjectFolder(sessionId?) and
  openWorkspaceFolder(). The Desktop adapter opens a task's folder through
  the task and anything else on the default Runtime Host, and names the
  task or Host profile a refusal or failure belongs to.
- The controller exposes openProjectFolder / openWorkspaceFolder commands
  and reports failures in the same shell copy, targets and
  workspace-unavailable notice as before. TaskEntryError may now carry a
  sessionId instead of a profileId.
- AppShell hands taskEntry.commands to the titlebar and the palette.

refreshProjects stays with the workspace projection in
use-project-context.ts, inlined on runOnDefaultRuntimeHost.
app-shell-project-actions.ts, its workspace-projection ownership entry
and the now unused openPathActionErrorMessage are removed.

Refs apache#4582

Generated-by: Claude Code

* refactor(desktop): give the command palette's Desktop actions an overlays port

Five command-palette rows still called the Desktop bridge from
app-shell-command-actions.ts: testing a connection, making it the
default, testing the network proxy, opening the local memory file and
saving the conversation to a file. They now reach Desktop through the
palette owner, features/overlays:

- OverlaysServices gains a palette port (OverlayPaletteActions) whose
  Desktop adapter makes the same bridge calls with the same arguments,
  including the slug form of connections.test/setDefault, which is a
  different IPC channel from Connection Settings' identity form.
- The overlays projection hands the port to AppShell as paletteActions,
  and AppShell passes it in the command options. The rows keep their
  default-Host resolution, toasts and failure copy.

app-shell-command-actions.ts no longer references window.maka; together
with the diagnostics report, its six bridge paths are gone. The overlays
entry-surface pin now lists OverlayPaletteActions for that file.

Refs apache#4582

Generated-by: Claude Code

* refactor(desktop): share the open-path copy helpers and keep the titlebar opener stable

Review follow-ups for apache#5937:

- openPathFailureCopy and openPathActionLabel move into the shell copy
  catalog beside the strings they read. Task Entry's folderOpenFailure
  now calls them instead of its own copy of the lookup, and open-path.ts
  re-exports them for Settings and the artifact pane. An application
  contract cannot hold them: the application zone may not import copy
  catalogs. The renderer's unused OpenPathKey / OpenPathFailureReason
  aliases go with the move.
- AppShell's openProjectFolder is a useCallback over taskEntry.commands
  and the owner session, so the titlebar no longer receives a new
  onOpenFolder on every commit.

Refs apache#4582

Generated-by: Claude Code
* feat(acp): add mode selection and scoped catalog lifecycle

Generated-by: Codex

* fix(acp): preserve executor configuration authority

Generated-by: Codex

* fix(protocol): validate optional executor catalog fields

Generated-by: Codex

* fix(acp): reconcile reviewed configuration races

Generated-by: Codex

* fix(acp): preserve confirmed selection across agent drift

Apply merged configuration once when opening a fresh Session, so a model change may remove an obsolete mode.

Generated-by: Codex

* fix(acp): keep saved selection across agent drift

Generated-by: Codex

* docs(acp): reconcile official agent acceptance findings

Generated-by: Codex

* fix(desktop): select extracted ACP directory on macOS

Record authenticated Desktop acceptance and post-restart continuation.

Generated-by: Codex

* fix(acp): validate mode after model selection

Generated-by: Codex

* docs(acp): record real directory refresh isolation

Generated-by: Codex

* docs(acp): record second project desktop catalog

Generated-by: Codex

* docs(acp): reconcile second project acceptance summary

Generated-by: Codex

* docs(acp): record desktop cross-project acceptance

Generated-by: Codex

* docs(acp): complete repeatable PR 5826 acceptance procedure

Generated-by: Codex

* docs(acp): record inconclusive eligibility recheck

Generated-by: Codex

* fix(acp): advance mode catalog protocol epoch beyond main

Generated-by: Codex

* docs(acp): record successful official execution recheck

Generated-by: Codex

* docs: close official Agent Desktop acceptance

Generated-by: Codex

* fix(acp): restore model before dependent mode on rollback

Preserve the retained Session and durable selection when a model change exposes different mode candidates. Cover stale and invalid modes, explicit mode restoration, and same-Session prompt retry.

Generated-by: Codex

* fix(ui): block sends until executor mode is confirmed

Aggregate mode and model configuration pending state into the Composer send gate. Preserve drafts until confirmation settles and release the gate when the selector unmounts.

Generated-by: Codex

* fix(acp): isolate draft catalog probes from project sessions

Generated-by: Codex

* fix(acp): validate program selection and consolidate protocol history

Generated-by: Codex

* chore(test): sync Windows program selection skip inventory

Generated-by: Codex

* fix(acp): reuse leased catalog paths and restart invalidated waiters

Provide stable namespaced scratch directories through Plugin storage and reuse existing native file leases across runtime instances. Keep leases through probe cleanup and recover residue after owner exit. Redirect catalog waiters by revision without retrying genuine failures.

Generated-by: Codex

* docs(acp): record stable probe and preceding desktop acceptance

Record official Agent checks at 4fefdc1, production Plugin bundle integration, 286 affected tests and negative regressions. Keep Desktop 1-4 and historical 5-9 evidence scoped to their tested implementations.

Generated-by: Codex

* test(acp): cover catalog refresh and mode configuration boundaries

Generated-by: Codex

* fix(desktop): preserve executor controls during first send

Show Retry only when the executor catalog is unavailable or has an error. Carry the selected catalog into locally pending sessions, defer Host inspection until admission, and preserve the selected model and mode in the pending summary.

Generated-by: Codex

* fix(test): import executor composer through conversation API

Generated-by: Codex

* fix(acp): reject unavailable explicit modes before prompting

Require confirmation of every explicit and saved configuration value after model selection. Preserve model-only mode side effects by keeping the inherited mode out of the Host provider request. Cover retained-session drift, explicit-mode rollback, and Host patch intent.

Generated-by: Codex

* fix(acp): recover stale modes and restore catalog refresh
…nds (apache#5951)

`e2e/session-workbar.spec.ts:179` ("Terminal survives navigation and reload…") is flaky on main. It always fails at line 224: after `page.reload()`, the owner Session's right Workbar comes back collapsed, so the terminal region never appears. This is a product bug, not a harness problem. A reload can drop every other Session's per-Session Workbar visibility.

The cause is a race at startup. After a reload, a `sessions:changed` event from a Session whose turn is still finishing (the test does not wait for the replacement Session's turn to end) is read by the patch drain. `catalog.commitPatch` commits it before `bootstrapSessions()`'s `sessions.list()` does. `commitPatch` moves the catalog `revision` off 0, and `selectAuthoritativeSessionIds` read any `revision > 0` as a membership observation. It therefore published a one-row set. `useWorkbarLayoutState` dispatched `retain-sessions` with it. No Session is active yet at that point, so every other Session's `collapsedBySession` entry was dropped, and the right-visibility effect persisted the loss to `maka-session-workbar-collapsed-v2`. The rate went from 9/40 at 8ad836c to 19/40 at 255ae23 (Fisher p = 0.034) but the root cause is older. Row patches have been able to land before the list since apache#5532. The range 8ad836c..255ae23 changes nothing on the catalog, Workbar layout, main, preload or runtime path, so apache#5934/apache#5935 only moved the timing.


Generated-by: Claude Opus 5.5
After apache#5934–apache#5937, `npm run check:renderer-architecture -- --report` still listed two retained-root rows scheduled for M5. M5's fourth item, "remove migrated legacy exceptions, public raw-state exports and redundant props/helpers", and its fifth, "document every retained root projection/lifecycle with its actual consumer, owner and allowed capability", were also open. This PR closes all three, in five commits:

| Commit | What changes | Who loses what |
|---|---|---|
| `61cad0bcc` | `OnboardingConnectionSeed`, a render-null watch beside the onboarding authority, seeds the default Host's connection projection from each accepted snapshot, or asks it to refresh when onboarding cannot be read. | `AppShellContent` loses its last `useEffect` and the M5 row for it. |
| `e485fa592` | `useAppShellProjectContext` stops returning the default Host's project list, the Local Host's projects and the raw selected id. It also drops the Local Host project subscription (`projects.getLocalSnapshot`, `subscribeLocalChanges`), which nothing has read since apache#5937 moved project mutations to Task Entry. Its row moves from "M5" to "application lifecycle" (see below). | `use-project-context.ts` loses 2 bridge paths and 1 effect. |
| `eae997042` | Props nobody reads are removed (details below). A type-level test pins the trimmed contracts. | AppShell stops computing 3 chat-model values and importing `ProviderLogo`. |

Generated-by: Claude Opus 5.5
Fixes apache#5870

Testing an MCP connection while its detail dialog is open now shows a readable, dismissible result above the backdrop. Astryx's shared toast viewport uses a stable portal host inside the active native modal, then opens its popover; raising `z-index` or reopening a popover outside the modal leaves the notification obscured or inert. The host moves back when the dialog closes without remounting toast rows. Environments without native popover and modal-selector support retain the inline viewport.

The change extends the existing Astryx dependency patch (source and distributed JavaScript), documents its removal condition, and adds browser hit-testing, keyboard-focus and dismissal assertions to the existing MCP detail story.

| Before | After |
| --- | --- |
| ![Toast blurred behind dialog](https://raw.githubusercontent.com/liuxiaocs7/maka/6c14d18dce9e1f6b513553fb5f0f9a6b59bfbb03/before.png) | ![Toast readable above dialog](https://raw.githubusercontent.com/liuxiaocs7/maka/6c14d18dce9e1f6b513553fb5f0f9a6b59bfbb03/after-light.png) |

[Dark-mode capture](https://raw.githubusercontent.com/liuxiaocs7/maka/6c14d18dce9e1f6b513553fb5f0f9a6b59bfbb03/after-dark.png)

Generated-by: Codex
Recall reconstructs each candidate Session before ranking passages. That reconstruction issued `4N + 2` SQL reads for N runs and repeatedly decoded the same event payloads. With 20,000 Turns and ten returned passages, the query took **1,603.63 ms** and a complete message read issued **80,002 SQL reads / 160,000 JSON parses**.

Add a storage batch snapshot that reads openings, events, lightweight ordinals and partial presentation in one SQLite read transaction. Reuse decoded opening/terminal events and preserve the existing projection, ordering, legacy-opening and corruption checks. Wire the capability through the production storage facades; stores without it retain the individual-reader path. Resolve corpus-count fallback before reading transcripts so unavailable counts no longer make Recall reread candidates.

Fixes apache#5877

Refs apache#5368, apache#5531 and apache#4677.

Generated-by: Codex
…very below the shell (R2 M3) (apache#5954)

* refactor(desktop): replace Conversation's export-star lines with explicit exports

The public entry re-exported six modules wholesale. Production code
outside the feature uses three of their names: `useShellChatModel` and
`SessionHealthNoticeView` (AppShell and the chat surfaces) and
`executorComposerProps` (AppShell). Only those stay public. The names
that only tests read move to `testing.ts`.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): give Copy and Save a Conversation export command

Copy and Save read the published messages through the transitional
adapter's `readMessages` and rendered them in the command palette.

The Conversation controller now owns `renderPublishedConversation`. It
renders the published range as Markdown when called, and the palette
receives that export, not the messages. `conversation-markdown` moves
into the feature. The adapter drops `readMessages`, and the
corresponding row leaves the Conversation README's transitional table.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): replace the shell's Composer handle with named edits

AppShell held the main Composer's editor handle in 16 places and passed
it to Workbar and Session Collaboration. Those callers could read
drafts back and write any draft key.

Conversation now builds `ComposerEditingCommands` from its own handle:
- appendText, replaceText, focus and openModelPicker for the visible
  draft;
- seedDraft for Work Board and discardDraft for a Guest's settled turn
  request;
- claimVisibleDraft for Module Hub's later append, which stays current
  only while the same editor is mounted.

None of these reads a draft back. The shell's queue surface carries
these intents and the plate's entry actions, and nothing else.
`ConversationComposerRegion` attaches the handle to the Composer
itself. Workbar and Session Collaboration take the two keyed intents
structurally.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): move Turn marks and the resume offer into the submission owner

AppShell called three hooks for the Turn footer and the resume offer:
`useTurnActionRegistry` held the pending marks and lent them to the
submission owner through its shell port, `useAppShellTurnPresentation`
derived the footer from them, and `useShellResume` built the banner and
send-slot resume actions.

The submission owner now holds the marks and the resume instance.
`ConversationTranscriptRegion` derives the Turn presentation from the
owner's marks (Branch is still withheld from a shared Session) and
injects the banner's resume action; `ConversationComposerRegion`
injects the send-slot offer. One resume instance stays behind both. The
shell's command handle gains `clearPendingTurnActions` for Session
teardown and Host changes, a no-op while the owner is unmounted.

The two resume actions are memoized, so the owner's reader keeps its
identity while nothing the actions show changes.

The hook gate and the retained-root table drop the three rows; the
legacy registry file moves under Conversation.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): read the displayed Session's Turn in the regions that render it

AppShell subscribed to the displayed Session's load, Turn summary,
interaction and queue through `useAppShellSessionUiReads`, derived
live-turn flags with `useShellLiveTurn`, and computed every control a
running Turn holds in its own render body. Each Turn boundary therefore
re-rendered the whole shell.

Each region now reads what it renders:
- `ConversationComposerRegion` reads the Turn summary, the queue and
  the owner Session's interaction. It derives Stop, the mode,
  permission and goal reasons, the model-switch gate, the executor
  picker's hold and the slash commands (`/compact` is withheld while a
  Turn runs). The shell passes only the catalog row's arrival and
  status and the executor selection.
- `ConversationTranscriptRegion` injects the running Turn's activity
  and holds the health notice's model picker for the same Turn. The
  shell's `useShellChatModel` keeps the status half of that gate.
- `ConversationActivityConsumer` feeds the custom pet whether an
  observable Turn runs and whether the owner Session waits on an
  answer; `petActivityForSession` maps that onto the pack state.
- `ConversationHomeSurface` renders the main column and marks it as
  the home surface when the shell's empty-transcript condition holds
  and there is no live Turn content and no failed load.

The gate logic moves unchanged into `composerTurnGates` and
`liveTurnFlags`. The two legacy hooks, their gate entries and
retained-root rows, and the Conversation README's last transitional
row are gone, together with the `chatTurnActivity`,
`executorComposerProps` and `desktopSlashCommandPresentation` exports.

Refs apache#4582

Generated-by: Claude Opus 5.5

* fix(desktop): give a withdrawn send's staged context back to the Session it left

Editing a queued steering entry or a cancelled local message restores
its text to the editor's keyed draft, and was meant to restore its
attachments, directory references and quotes through the queue's
`draftContextRestorer` slot. apache#5747 removed the only line that filled
that slot when it moved the shell's staging calls, so since then the
text came back and the staged context was dropped.

Staging now offers `restoreContext(draftKey, context)` on its command
handle, a no-op while no owner is mounted, and the Composer submission
owner binds the queue's slot to it. The restore stays keyed by the
Session the send left, so it lands there even after navigation. A
message whose outcome is unknown keeps its identity: it offers only the
Host check, bound to its Session and Message.

Refs apache#4582

Generated-by: Claude Opus 5.5

* docs(desktop): retain the last three M3 root hooks with their cross-region reasons

`useActiveExecutionBoundary`, `useSessionSettingIntent` and
`useShellChatModel` were the last retained-root rows scheduled for M3.
Each serves several regions the root composes, so each stays at the
root with the `cross-region command` reason and its full consumer list:
- the execution boundary feeds the Composer's permission control and
  unreadable notice, the palette's permission-mode command, the Session
  Collaboration dialog gate and the health notice's picker gate, and is
  reloaded by the submission owner and Conversation lifecycle;
- the setting overlay feeds the Composer's model, thinking and mode
  controls, the transcript's model picker, the palette's permission-mode
  command, new-task creation and Session teardown;
- the model selection feeds the Composer's model and executor pickers,
  the transcript's labels and health notice, the staging vision gate,
  the readiness and new-task submission model and Workbar.

The exports table's remaining M3 rows (the workspace picker, guest Turn
requests and the skill catalog boundary) are other features' projections
into the Composer; a feature cannot import another, so the root composes
them and they stay. The guest row no longer mentions the Composer ref,
which it lost to the named edits.

Refs apache#4582

Generated-by: Claude Opus 5.5

* refactor(desktop): narrow the transcript's submission read and type its picker gate

Review follow-ups on apache#5954:
- The Turn-action registry clears its timers and marks when its owner
  unmounts. The shell's unmount call reached an already-unbound handle,
  so it is removed, and with it the no-argument "clear every Session"
  form: `clearPendingTurnActions(sessionId)` now needs a Session.
- The transcript and the activity reader read a narrow Turn reader
  (displayed and owner Session, pending marks, the banner's resume
  action). A send-pending or edit-draft change no longer repaints the
  transcript.
- The health notice's picker gate is a typed region input,
  `localInteractionAvailable`, instead of a prop read through a cast.

Refs apache#4582

Generated-by: Claude Opus 5.5
Align the top-right settings menu to the trigger's end edge and add real-browser geometry regression coverage for narrow and regular desktop viewports.

Generated-by: Maka (GPT-6.1)
…#5928)

The quote chip, turn footer and lineage badges restyled Astryx Buttons
through dedicated product classes that repainted chrome ghost buttons
already own — or rebuilt the focus ring as an inset shadow because the
chat scrollport clips the outward one.

Drop the per-Button hooks and reach the same surface through the
extension points the components publish:

- `.maka-quote-chip .astryx-button` keeps only the product delta
  (supporting type, muted ink, shrink-to-clip sizing); the expanded
  state now reads the button's own `aria-expanded="true"`.
- `.maka-turn-lineage-row` sets `--_button-radius` — the derived var
  Astryx's own Thumbnail/Carousel retune — and the color rules scope
  through `.astryx-button[data-direction]`.
- `.maka-turn-footer` and `.maka-message-meta` set
  `--focus-outline-offset` so the component's own outline draws inside
  the border box instead of a replacement inset shadow.

`markerVariants` drops its `lineage-badge` and `footer-action` variants;
the remaining ones only ever mark product composition.

Adds a TurnView lineage story so the badges' resting chrome and the
retuned footer ring get pixel coverage; its play holds focus on the
footer branch action, so the retuned ring is in the frame. Before/after
run on the new base (42 shots, light+dark — the 20-story chat surface
set plus regenerated-conversation's lineage badges): all identical.
The one intentional visual delta remains the focused footer ring —
inset box-shadow in --focus-ring → native outline in
--focus-outline-color (= --accent-solid, the contrast-clearing tier) —
pinned by the story's computed-style assertions.

Refs apache#5793

Generated-by: Devin (devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ion (apache#5866)

Fixes apache#5865

Setting a custom relay with no enabled chat model as default currently produces a generic error. The action now guides the user through choosing a model:

- Switch directly when one chat model is enabled; prompt when several are enabled.
- When none are enabled, explicitly select a model and confirm **Enable and set as default**. An empty inventory offers discovery or manual entry without silently enabling the first result.
- Enable the selected model and change the default in one catalog commit. Cancellation or a rejected commit retains the original default, with actionable errors for stale or unavailable selections.

Production components/styles rendered in Chromium with a synthetic connection and stubbed bridge:

| Before | After |
| --- | --- |
| ![Generic failure when no model is enabled](https://raw.githubusercontent.com/liuxiaocs7/maka/353614c8fabaa6dd6dafe42d7aea16b700ef0ba9/before.png) | ![Explicitly enable Model Beta and set the connection as default](https://raw.githubusercontent.com/liuxiaocs7/maka/353614c8fabaa6dd6dafe42d7aea16b700ef0ba9/after.png) |

<details>
<summary>Empty model inventory</summary>

![Fetch models or add one manually](https://raw.githubusercontent.com/liuxiaocs7/maka/353614c8fabaa6dd6dafe42d7aea16b700ef0ba9/empty.png)

</details>

Generated-by: Codex
Preserve the current persistence, Recall, plugin, and transcript event wiring
when integrating the execution composition split with main.

Keep graph coordinator drain running when supervisor wake drain fails, and
cover persistence ownership, lifecycle ordering, and subscription cleanup.

Generated-by: OpenAI Codex

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Combined review of head 954442bb from two independent review passes (behaviour preservation and adversarial breakage), synthesized by the review lead. Verdict: COMMENT, no P0–P2, P3s only.

Status. Against current main (merge-base 3597abe8) the real change is 11 files, +4347/-3598; GitHub's larger figure comes from the merge commits. test CI passes, the PR merges cleanly, and no runtime-host protocol files change, so no epoch claim is needed.

Behaviour. Every piece moved out of the original execution-composition.ts (sandbox/workspace filesystem adapter, tools, agent-graph construction, history readers and subscriptions) is logically identical to the original. Construction order, agent-graph recovery, drain order and close order (supervisor wake → client → coordinator → store) are preserved. All old exports are re-exported, so every importer in packages/ still resolves, including the lazy-import test. There are no new import cycles and no services constructed twice. Locally, runtime-host typecheck and build pass, the new tests plus execution-composition pass 55/55, and nearby suites pass 151/152. The one failure also fails on main here because bwrap isn't installed.

P3

  • execution-composition.ts:37: "Fixes #3911" is met only by moving the 3,380-line body into execution-domain-composition.ts; about 590 lines went into new modules, and agent-graph construction is still in the big file. Consider "Refs #3911". Keeping the old file as a pass-through also hides the rename from git, so the diff reads as a near-total rewrite. Renaming in place, or splitting into a pure-move PR and an extraction PR, would make this much easier to review.
  • execution-domain-composition.ts:3122: startup-failure cleanup now also closes and awaits the supervisor wake and coordinator. It's safe, arguably a fix, but not disclosed under "no behaviour change", and no test drives a startup failure through the new modules.
  • agent-graph-composition.ts:92: multiple drain/close failures now arrive as a nested aggregate error instead of separate entries.
  • tool-composition.ts:130: readMessages is no longer async and resolves the session manager before the abort check.
  • tool-composition.ts:196: childHostTools is returned but unused.
  • Stale docs (not in the diff): docs/architecture/runtime-host-architecture.md:133 and its .zh-CN.md twin, packages/runtime/README.md:43, and the comment at packages/core/src/session.ts:362 still point to execution-composition.ts.

Comment thread packages/runtime-host/src/server/execution-composition.ts
Comment thread packages/runtime-host/src/server/execution-domain-composition.ts Outdated
Comment thread packages/runtime-host/src/server/agent-graph-composition.ts Outdated
Comment thread packages/runtime-host/src/server/tool-composition.ts Outdated
Comment thread packages/runtime-host/src/server/tool-composition.ts Outdated
…ntics

Restore abort-first asynchronous Recall history reads and expose individual
Agent Graph lifecycle hooks to preserve error ordering without added nesting.
Cover late startup cleanup and history listener release, remove the unused
returned child tool catalog, and clarify composition implementation links.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Incremental review of head 293e8445 against our previous review of 954442bb. Verdict: COMMENT. There are no P0–P2 findings, and two P3s remain, both about scope and disclosure.

Status. Main has not moved: both heads share merge-base 3597abe8. The delta is the two new commits (23fb5900 preserving cancellation and cleanup semantics, and 293e8445 aligning the storage writer guard). Against main the real change is 16 files, +4566/-3603. test CI passes on 293e8445, GitHub reports the PR as mergeable (blocked only on review), and no runtime-host protocol files change, so no epoch claim is needed.

Fixed since 954442b

  • Nested aggregate errors: RuntimeHostAgentGraphComposition now exposes drainHooks/closeHooks arrays, so the module runner aggregates each authority's failure flat again, as on base. A new test pins the flat six-error shape and the single attempt per hook.
  • readMessages: it is async again and takes a getSessionManager getter that is resolved after the abort check, which restores the base ordering. Tests cover abort-before-acquire and asynchronous rejection when the getter throws.
  • childHostTools is no longer returned. It is still used internally to keep recall tools away from child agents, which is correct.
  • Stale doc pointers in the architecture docs (EN/zh-CN), packages/runtime/README.md, and the core/session.ts comment now name execution-domain-composition.ts.
  • Startup-failure cleanup now has a test. A late failure after all graph bindings proves that wake, client, and coordinator close in order, that startup awaits them, that close failures aggregate as [startup, wake, coordinator], and that history listeners are detached before storage closes.
  • The storage writer-ownership guard now exempts execution-domain-composition.ts instead of the wrapper. The wrapper constructs no writers, so the guard still holds.

Still open (P3)

  • execution-composition.ts:37: the body still says Fixes #3911 and "2,064 → 42 lines". The 3,382-line body only moved to execution-domain-composition.ts, and the pass-through still hides the rename from git. Suggest Refs #3911.
  • execution-domain-composition.ts:3121: the startup-failure path now closing wake and coordinator is tested, but the PR body still answers "No" to behaviour change. One line under Summary would cover it.

export { runtimeHostFilesystemWorkerRuntime } from './sandbox-composition.js';

/** Public composition root for the Runtime Host execution domain assembly. */
export function createExecutionRuntimeHostComposition(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 (scope, still open; no runtime impact): the PR body still says Fixes #3911 and "2,064 → 42 lines". On base 3597abe8 the file is 3,617 lines, and the assembly now lives in execution-domain-composition.ts (3,382 lines), so merging would auto-close #3911 while the bottleneck remains under a new name. Suggest Refs #3911, or more extraction before claiming the fix.

} catch (closeError) {
errors.push(closeError);
}
for (const closeGraphAuthority of agentGraph?.closeHooks ?? []) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 (disclosure, partly addressed): thanks for the new late-startup-failure test. It pins this path well. The behaviour still differs from base, though: base closed only graphClient and graphControlStore here, and this now also closes and awaits the supervisor wake and coordinator. The PR body still ticks "No" for behaviour change. A one-line note under Summary would make that accurate.

@github-actions github-actions Bot removed the stale No qualifying activity within the lifecycle policy window label Oct 8, 2026

This branch has not been deployed

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

Labels

effort/XXL Over 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(runtime-host): decompose execution-composition.ts (1840 lines, 40+ imports)