Repository navigation
feat(mobile): show a delegated subagent's work in the transcript - #3920
Open
braindeadz wants to merge 1 commit into
Open
braindeadz wants to merge 1 commit into
braindeadz wants to merge 1 commit into
Conversation
…anscript A host keeps a spawned child agent in its own hidden session. The parent transcript only carries the tool row that launched it, and the `subagentSessionId` the host stamps on that row was read by SessionRecordReplica and then dropped, so the child's work was unreachable from the phone and a delegation card could only ever show the spawn call's own outcome. Keep the id on the mapped tool (optional, so an older host is unaffected) and follow the child from a new core-feature coordinator: SubagentBranchCoordinator opens one on-demand child stream per owner, attaches the child's own items as the branch of the item that launched it, re-reads the branch from the host's newest page on a stream gap, caps concurrent child streams at eight with oldest-first eviction, and exposes EnsureSubagentAttached so opening a card asks again for a stream that the cap or a failure released. Child steps are never stored on the device: toPersisted() strips them (withoutChildSteps) and the child is re-read on demand. A child stream failure is reported per card (Ready.subagentFailures, keyed by owner tool call id) rather than replacing a readable conversation with a session-level error: the session stays Ready and the launching item keeps its own status. The card's liveness/status presentation replaces the old boolean subagentFailed(), so a worker that is queued, finishing or calling a tool is no longer printed as done, and a status this client cannot read is drawn as unknown instead of guessed as completed. Following a child is opted in per platform, because it is only half a feature without the card around it: a client that cannot ask for a re-attach and cannot show why a worker was unreadable would keep a released branch's stale prefix and record failures nobody sees. RemoteSessionStore takes followSubagentBranches, off unless a caller asks for it, and AccountStore gained an overload to pass it through; Android is the only caller that passes true, at its single construction site. With the flag off the coordinator is never created, so decorate() is skipped, the published transcript is the host's own item for item, and EnsureSubagentAttached is inert. iOS is therefore behaviourally unchanged -- createSessionStore(scope:) keeps the default, so no decoration, no child streams, no recorded-but-invisible failures and no stale prefix -- and a Swift follow-up can switch it on once the attach trigger and the per-card failure surface are wired there. The only delta that reaches iOS is data-level: a persisted launch row now carries the host's optional subagent_session_id, which the payload codec's ignoreUnknownKeys keeps forward- and backward-compatible. Compile safety holds everywhere: iOS only reads RemoteSessionUiState.Ready and never constructs it, a secondary constructor preserves the previous positional initializer, RemoteToolStatusResponse's new field is optional, and HarmonyOS models the relay payload with its own TypeScript interface. An older desktop that sends no subagent_session_id opens no stream at all and the launch item keeps the plain tool row it has always had. Testing level: AI-assisted implementation, unit, compile and one real-device run. The shared JVM suites pass: 125 tests in core-domain and 557 in core-feature, 27 of them in SubagentBranchCoordinatorTest, which covers both ways a card stops reporting its worker (a real re-read, and steps arriving) and asserts that branches are off by default and leave the transcript untouched. :app:testDebugUnitTest still shows exactly the one failure that official main already has (ProcessPresentationTest.completedWorkSummarizesWithoutHidingLiveOrFailedTools, expected:<3> but was:<1>), which this change neither causes nor fixes. The obsolete ProcessPresentationTest.statusPreservesFailuresInsteadOfCallingThemDone is removed with subagentFailed(), and its coverage is replaced by SubagentLivenessTest. ChatMessageBubbleTest's fixtures now ask for reduced motion, because a running worker's state mark pulses forever and three of those pre-existing fixtures draw one. At revision 7d97a522a the two card instrumented suites were executed on a real Android device and passed: SubagentBranchCardTest 4/4 and SubagentLivenessCardTest 7/7. This revision was not re-run on a device: its added coordinator unit tests, the ChatMessageBubbleTest reduce-motion override and the ConversationView remembered callback are compile-checked only. Nothing was run against a live desktop host. Co-authored-by: Driss <[email protected]>
braindeadz
force-pushed
the
feat/mobile-subagent-branch-cards
branch
from
November 14, 2026 16:31
980e926 to
15a4a7c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A delegated subagent runs in its own host session. The mobile client only ever drew
the tool row that launched it, because the child session id the host stamps on that
row was read by
SessionRecordReplicaand then dropped. This PR keeps that id andfollows the child on demand, so the worker's own steps appear inside the card that
launched it, with the worker's own lifecycle, and a child that cannot be read says so
on its card instead of emptying or failing the conversation.
Fixes #
Type and Areas
Type: Feature (with a UI/UX change to an existing surface).
Areas: mobile Android app, mobile shared core (
core-feature,core-protocol).Shared code only: no Rust core, relay, desktop or web UI changes. The shared follower is
opt-in per platform: Android is the only client that turns it on in this PR, and the
consequences for the other clients are spelled out in
Known limitations. HarmonyOS does not consume the Kotlin module.
Motivation / Impact
The problem. A host spawns a child agent into a separate session. The parent
transcript carries only the launching tool item, and the
subagentSessionIdwritten onthat item was parsed and discarded while mapping the record. Consequences on the phone:
show the spawn call's own status and result preview;
status says nothing about the worker, so a working child was drawn as "Done";
subagentFailed(status)), which foldedevery non-running status into "completed" — a queued, finishing or tool-calling
worker was printed as finished.
What the user sees now.
AgentSpawncard with a status word and a short result previewDesign.
RemoteToolStatusResponsegains an optionalsubagent_session_id;SessionRecordReplicakeeps it on the mapped tool instead of dropping it, and the item stays marked as a
branch.
SubagentBranchCoordinator(new,core-feature) owns the child streams. Discoveryhappens in
decorate(), which is called from exactly one place —render()inRemoteSessionStore— so a child is opened only when the parent transcript names one,never speculatively.
decorate()replaces two things on the owning item and nothing else: the branch items(
branchItems) and the status word (ownerItem).MessageBlockPresentationthendraws the item as the card it already knew how to draw.
turnStatus/childStatus),never from the spawn call's answer, and never from silence: a stream that failed
before publishing anything leaves the launching item's own status standing.
subagentLiveness(status)(new enumSubagentLiveness)instead of the removed boolean
subagentFailed(status).RemoteSessionIntent.EnsureSubagentAttached(ownerToolCallId)is dispatched when a card is first opened (
MessageBlockList→LocalSubagentAttach→
ConversationView), and handled byRemoteSessionStore.ensureSubagentAttached.RemoteSessionStoretakesfollowSubagentBranches(internal constructor, default
false), threaded throughAccountStore.createSessionStore(scope, followSubagentBranches); Android passestrueat its single construction site and every other client keeps the existing overload, so
the coordinator is never even created there.
Compatibility.
SessionRecordReplicamaps a missing/emptysubagentSessionIdtonull;SubagentBranchCoordinator.openBranchstarts withif (childSessionId.isEmpty() || ownerToolCallId.isEmpty()) return, so no stream isever opened; with no branches,
decorate()returnsmessagesunchanged(
if (branches.isEmpty()) return messages); andMessageBlockPresentation.walkroutes an ordinary tool item to
MessageBlock.Tools(the plain tool row) unlessisSubagent(entry)is true. What that preserves is the transcript and the number ofstreams opened — exactly none. What it does not preserve is the card affordance,
because a
task/AgentSpawnrow is already a delegation card on any host (thatpredates this PR): a still-running such row whose worker has no readable branch is
now expandable, and expanding it shows an empty panel and dispatches an attach that
changes nothing. Details and the reason it is kept are in
Known limitations.
RemoteSessionStoretakesfollowSubagentBranches, which stays off unless a caller asks for it. Android is theonly caller that asks:
AccountViewModel, its single construction site, through the newAccountStore.createSessionStore(scope, followSubagentBranches)overload. iOS keepscreateSessionStore(scope:), which leaves the flag at its default, sosubscribeSessionUpdatesnever creates aSubagentBranchCoordinator. iOS thereforedraws and reads exactly what it drew at the base commit: nothing is decorated, no child
stream is opened, no failure is recorded and no branch can go stale.
The one delta that reaches every platform, iOS included and whether or not the flag is
on, is data-level:
SessionRecordReplicanow keeps the host'ssubagent_session_idonthe launch tool, so a persisted launch row gains that one optional field. It is safe in
both directions — the field has a default and the payload codec is
Json { ignoreUnknownKeys = true }, so an older build still restores a row written bythis one. Keeping the flag off there is deliberate: shipping the shared streams to iOS
while leaving out the two affordances that finish them (no
EnsureSubagentAttached, nosubagentFailuressurface) would be a half-working path — a cap-released branch strandedon a stale prefix and a recorded failure nobody can see — so it is off there until the
Swift client is wired. Follow-up: wire the expansion intent and the per-card failure map
into Swift, then pass
followSubagentBranches = trueat the Swift construction site.RemoteSessionUiState.Ready(
as? RemoteSessionUiStateReadyand property access) and never constructs it, neverconstructs
RemoteToolStatusResponse, and never constructsMessageBlock.Subagent;a secondary constructor keeps the previous positional initializer valid, and
RemoteToolStatusResponse's new field is optional with a default. HarmonyOS does notconsume the Kotlin module at all — it models the relay payload with its own TypeScript
interface (
RemoteToolStatusResponseinRemoteModels.ets) — so neither the newfield nor the new intent reaches it.
toPersistedstrips a child's items(
withoutChildSteps) and keeps the child session id on the tool, so a sessionrestored from this device's cache still finds the branch and re-reads it from the
host on demand. This is also the only persistence change this PR makes on any
platform, including iOS with the flag off.
Verification
Environment: Windows, JDK 17, Android SDK, Node 22.23.5, Gradle wrapper from the repo.
No device or emulator is attached for this revision (the two card suites were run on a
device at
7d97a522a).Baseline (untouched checkout at the base commit, before any change):
shared/:./gradlew.bat --console=plain :core-domain:jvmTest :core-feature:jvmTest→ BUILD SUCCESSFUL.
core-domain13 classes / 125 tests / 0 failures;core-feature44 classes / 530 tests / 0 failures.android/:./gradlew.bat --console=plain :app:testDebugUnitTest→ BUILD FAILED withone failing test, 13 classes / 65 tests / 1 failure:
This is a pre-existing disagreement on the base commit, not caused by this PR: the
test expects a
FAILEDtool to break the compact activity summary into three groups,while
ToolCard.foldIntoSummary(ConversationPresentation.kt) acceptsToolPhase.FAILEDas foldable, so the five blocks fold into one group. This PR doesnot touch
foldIntoSummaryand does not fix it.After this change (same commands, same machine):
shared/: BUILD SUCCESSFUL.core-domain13 classes / 125 tests / 0 failures;core-feature45 classes / 557 tests / 0 failures (+27, all from the newSubagentBranchCoordinatorTest;MessageBlockPresentationTest27 tests, 0 failures).android/::app:testDebugUnitTest→ still exactly the same single failure,same assertion message, same test:
ProcessPresentationTest.completedWorkSummarizesWithoutHidingLiveOrFailedToolsjava.lang.AssertionError: expected:<3> but was:<1>(now at line 40, because fourlines were removed above it). 14 classes / 73 tests / 1 failure (+8 net:
SubagentLivenessTest+9,ProcessPresentationTest−1). No test that passed atbaseline fails now, and the pre-existing failure is unchanged.
android/:./gradlew.bat --console=plain :app:compileDebugAndroidTestKotlin→ BUILDSUCCESSFUL for this revision.
SubagentBranchCardTest(4/4) andSubagentLivenessCardTest(7/7) passed on a real Android device at revision 7d97a522a; this revision's changes
to those files' shared inputs and to
ChatMessageBubbleTestare compile-checked only,because no device is attached here.
Repository guards (from the repo root, Node 22.23.5):
node scripts/check-mobile-architecture.mjsnode --test scripts/mobile-client-build.contract.test.mjsnode scripts/mobile-ui-design-system.mjs --checknode scripts/mobile-ui-preview.mjs --check[mobile-ui-preview] Missing canonical design-system asset for /design-system-tokens.css.It requiresdesign-system/packages/*/dist/*.css, which are gitignored build outputs of a package build that this environment does not have (nonode_modules, and no install was performed).node scripts/i18n-audit.mjs[i18n:audit] ERROR Failed to load TypeScript for i18n audit: Cannot find module 'typescript'(nonode_modulesin this environment; nothing was installed).node scripts/audit-frontend-colors.mjs --kind native-mobileFrontend color governance failures: - exclusions[2].path does not exist: src/web-ui/public/monaco-editor. That path is a vendored directory that is absent from the base commit's tree as well; the script is untouched by this PR and validates its exclusion list before filtering by kind.node scripts/check-git-object-sizes.mjs --base upstream/main --head HEADHonest test level. AI-assisted implementation. Verification level: unit, compile, and
one real-device run. The shared JVM suites and
:app:testDebugUnitTestwere re-run for thisrevision. The two card instrumented suites passed on a real Android device at revision
7d97a522a (4/4 and 7/7) and were not re-executed after the amendment; the amendment changed
shared unit tests, a test-only composition local in
ChatMessageBubbleTest, and aremembered callback, not the drawing assertions. Nothing here was run against a livedesktop host, and no screenshot is attached.
Reviewer Notes
Scope of the diff: 24 files — 9 new (
SubagentBranchCoordinator.kt,SubagentBranchCoordinatorTest.kt,SubagentLiveness.kt,SubagentAvatar.kt,SubagentLivenessTest.kt,SubagentBranchCardTest.kt,SubagentLivenessCardTest.kt,values/strings_subagent.xml,values-zh/strings_subagent.xml) and 15 modified(
MessageDtos.kt,SessionRecordReplica.kt,MessageBlockPresentation.kt+MessageBlockPresentationTest.kt,RemoteSessionUiState.kt,RemoteSessionStore.kt,AccountStore.kt,AccountViewModel.kt,ConversationView.kt,MessageBlockList.kt,ProcessPresentation.kt+ProcessPresentationTest.kt,Theme.kt,ChatMessageBubbleTest.kt,AGENTS.md).AccountStore.ktandAccountViewModel.ktarethe opt-in plumbing (
followSubagentBranches);ChatMessageBubbleTest.ktis the onlypre-existing instrumented file touched, and only to keep its fixtures from waiting on the
new endless pulse. No iOS, HarmonyOS, Rust, relay, desktop or web UI file is in the diff —
MobileCoreAdapter.swiftis not modified.New tests and what they cover.
SubagentBranchCoordinatorTest(JVM,core-feature, 27 tests). The path a revieweris most likely to doubt is covered by these exact tests:
aChildSessionIdSurvivesTheParentTranscriptAndMarksTheBranchtheChildsStepsAttachToTheOwnerItemAndItsStatusFollowsTheChildsTurnsaChildTurnThatIsStillTakingItsTurnIsNotReportedAsFinished,aChildTurnThatIsOnlyQueuedIsReportedAsWaitingAndKeepsItsStream,aChildTurnThatIsFinishingIsReportedAsFinishingAndKeepsItsStreamaChildGapReReadsThatChildOnceWithoutDuplicatingItsStepsaChildStreamThatFailsBeforeAnyContentKeepsTheLaunchStatusAndReportsOnce,aChildStreamThatDiesAfterPublishingStepsKeepsThemAndStopsClaimingToBeRunninganUnsupportedChildStreamLeavesTheConversationReadableAndIsReportedOnItsCardtheConcurrencyCapIsRespectedAndAnOpenedCardCanAlwaysReattach,aRunningChildEvictedByTheCapIsFollowedAgainWhenItsCardIsOpenedleavingTheForegroundReleasesChildStreamsAndReturningReattachesThemaCardStopsReportingItsWorkerOnceTheStreamIsReadAgainopeningACardWhoseHostCannotServeChildStreamsKeepsTheExplanationItHasaCardThatIsShowingItsWorkersStepsStopsReportingAFailureswitchingOrEndingTheSessionClosesEveryChildStream,switchingOrClosingTheConversationStopsEveryChildStream,aLateChildErrorFromAClosedSessionLeavesTheNewSessionUntouchedaChildsStepsAreNeverStoredInTheParentsTranscriptsubagentBranchesAreOffByDefaultAndLeaveTheTranscriptUntouchedaHostThatMarksItsOwnSiblingRecordsKeepsThePathItAlwaysUsed,aHostThatStartsPublishingMarkedSiblingsStopsBeingFollowedaChildRecordStampedWithAnotherSessionIsRejectedInsteadOfReducedBoth ways a card stops reporting are covered, at the level where the code lives
(
RemoteSessionStore.ensureSubagentAttached→clearSubagentFailure, andreconcileSubagentFailuresfromrender()): the positive re-read, the no-read casethat must keep the explanation, and the "already drawing steps" case.
SubagentLivenessTest(JVM, app, 9 tests) — the host vocabulary → reading mapping,including that an unreadable status is
UNKNOWNrather than "Done", and that aworker's identity is derived from the delegation and stable (a pinned value guards the
derivation).
SubagentBranchCardTest(instrumented: passed 4/4 on a real device at revision7d97a522a, not re-run after the amendment) — the drawing path:
aDelegationCardShowsTheWorkersStepsWhileTheChildIsStillRunning(steps behind thereader's expansion, and opening the card asks for the worker by its owner tool call
id),
aSettledChildKeepsItsStepsAndShowsTheSettledReading,aDesktopWithoutOnDemandStreamsSaysSoAndInventsNoStepsandaChildStreamThatFailedBeforeAnyContentKeepsTheLaunchStatusAndSaysStepsAreUnavailable(the two "no steps" readings use their own copy and invent no step).
SubagentLivenessCardTest(instrumented: passed 7/7 on a real device at revision7d97a522a, not re-run after the amendment) —
everyReadingIsDrawnInItsOwnWords,aQueuedWorkerIsNeverReportedAsDone,everyReadingHasItsOwnMarkAndNotOnlyItsOwnWord(six distinct marks, compared inpixels),
twoSpawnCardsWithOneTitleStillHaveTwoFaces,theHeaderSaysWhatTappingItDoesAndTheAvatarSaysNothingTwice,aRunningWorkerMovesAndOnlyItsMarkMoves(the pulse is confined to the state mark) andaReaderWhoRemovedAnimationsGetsTheSameCardTwice.Coverage removed and replaced — please read.
ProcessPresentationTest.statusPreservesFailuresInsteadOfCallingThemDoneis deleted because it asserts on
subagentFailed(status), which no longer exists: thecard reads
subagentLiveness(status)instead. That test passed at baseline, so this isa real removal of a passing test, not a deletion of the pre-existing failure (which is a
different test in the same class, still present and still failing). Its coverage is
replaced by
SubagentLivenessTest.aWorkerThatIsStillTakingItsTurnIsNeverReportedAsDone,...aTurnThatIsWrappingUpIsItsOwnReadingAndStillInFlight,...aQueuedWorkerIsWaitingRatherThanRunningOrDoneand...terminalOutcomesKeepTheirOwnReading,which assert the same failure vocabulary (
failed,error,timeout,cancelled,canceled,rejected→ERROR/CANCELLED, notCOMPLETED) plus the states the oldboolean could not express. The remaining three tests in
ProcessPresentationTestareuntouched.
Code paths to look at.
followSubagentBranchesparameter onRemoteSessionStore(internalconstructor, default
false), theif (followSubagentBranches)guard around thecoordinator in
subscribeSessionUpdates(sosubagentsstaysnullandrender()'ssubagents?.decorate(records.messages()) ?: records.messages()is thehost's own transcript), the null-safe
setForeground/finallycalls, the inertensureSubagentAttachedon a null coordinator, the newAccountStore.createSessionStore(scope, followSubagentBranches)overload, andAccountViewModelas the onlytruein the tree.subagentBranchesAreOffByDefaultAndLeaveTheTranscriptUntouchedasserts the consequencewith the default: no attempt at the child, the owner item equal to the un-decorated
replica item (
subagentSessionIdkept, no child items, the host's own status),EnsureSubagentAttachedprovably inert (assertSameon the state value), andReady.subagentFailuresempty even with the child stream rigged to fail.openBranch's early return,decorate'sbranches.isEmpty()earlyreturn,
MessageBlockPresentation.walk'sisSubagent(entry)branch.MAX_CONCURRENT_CHILD_STREAMS,acquireSubscriber,releaseCandidate(settled before working, oldest first),waitForSlot,drainWaiters(newest first),ensureAttached, andRemoteSessionStore.ensureSubagentAttachedwhich only clears a card's failure when a read really happened.
childStreamFailed→onError→RemoteSessionStore.reportSubagentFailure,which copies
Readyand never callshandleFailure;reconcileSubagentFailuresdrops the report once the card is drawing steps.
subscribeSessionUpdatescallssubagents?.endSession()and nulls the field before cancelling the old job; the job's
finallycallsbranches?.endSession()and only clears the fieldif (subagents === branches);childStreamFailedreturns early on an emptyparentSessionIdor a stalesubscriptiontoken;applyChildEventchecks the record'ssession_idagainst thesubscription.
withoutChildStepsintoPersisted.SubagentBranchCoordinator.setForegroundreleases every subscribedchild on the way out, re-attaches non-settled ones on the way back (a settled worker
has nothing left to publish), and is reached from
RemoteSessionStore.setForeground.ConversationViewholds the attach callback in arememberand readsonIntentthroughrememberUpdatedState, so a recompositiondoes not hand every block-list reader a new lambda identity.
The one pre-existing instrumented file this PR touches.
ChatMessageBubbleTestrenders three running delegation cards, and a running worker's state mark now pulses
forever — a compose test cannot idle on a clock that never stops. Its
Bubblehelpertherefore provides
LocalReduceMotionOverride = true, which changes only the movement,not the reading printed on the card. Without it those pre-existing assertions could hang
or time out on a device; that override is compile-checked only in this revision; the file
itself passed on a real device at revision
7d97a522a, before the override was added, sothe fix is reasoned from the compose test contract rather than measured.
Risks worth attacking.
child holds a reduced transcript in memory. The cap and the bound are argued in
comments, not measured on a device.
runningreading (falls back to the launching item's status) — deliberate, but itmeans a card can stop saying "Running" while the worker is in fact still alive.
wider than "an older desktop": any running
task/AgentSpawnrow is already drawnas a delegation card (
MessageBlockPresentation.isSubagentmatches on name), so arunning card whose worker has no readable branch — an older host that names no child
id, a host whose branch was never tracked (over
MAX_TRACKED_CHILD_BRANCHES), or onereleased with nothing published — now offers expansion, expands to an empty panel, and
dispatches an attach that changes nothing. No stream is opened and no failure is
recorded, so nothing is wrong beyond the affordance; see
Known limitations for why it is kept.
EnsureSubagentAttachedis dispatched on the first expansion of any card; on ahealthy host
ensureAttachedreturns false and nothing happens, but the intent isstill sent through the store on every session.
MessageBlockListwith the two compositionlocals supplied directly, not through
ConversationView(the app module may not namecore-domain/core-protocoltypes, which is whatcheck-mobile-architecture.mjsenforces).
ConversationView's wiring of those locals from the state is thereforecompile-checked only — including the
remembered attach callback.completedWorkSummarizesWithoutHidingLiveOrFailedToolsfailure willstill be red in this PR's unit-test run. It is unrelated, and it is not fixed here on
purpose — deciding whether
foldIntoSummaryor the test is wrong is a separatechange.
setForeground(true)deliberately does not re-open a settled worker (it has nothingleft to publish, and spending one of eight lanes on it would starve the ones still
working). The cost: a settled worker that is continued while the phone is in the
background is not followed again by returning to the foreground — it is followed
again when the parent names a new launch for it or when its card is opened.
all, so they gain nothing here (and lose nothing) — see
Known limitations.
Known limitations
The follower is off for every client but Android. Child-stream following is opted in
through
RemoteSessionStore.followSubagentBranches, and Android is the only client thatpasses
true(fromAccountViewModel, viaAccountStore.createSessionStore(scope, followSubagentBranches)). iOS is not modified:MobileCoreAdapter.swiftstill callscreateSessionStore(scope:), so the flag keeps itsdefault, no
SubagentBranchCoordinatoris created, and iOS keeps its previous behaviour —no decoration, no child streams, no recorded-but-invisible failures and no stale prefix (the
only cross-platform delta is the optional
subagent_session_idnow stored on a launch row,described in Child steps are not persisted). That is the intended state for this PR
rather than an end state: following a
worker without a way to re-attach a released branch and without a surface for a per-card
failure would be a half-working path, so the Swift client turns it on when it is ready.
Follow-up: wire the same expansion intent (
EnsureSubagentAttached) and the samesubagentFailuresmap into the Swift client, then passfollowSubagentBranches = truethere. Everything the Swift side needs already exists store-side and is client-agnostic.
An expandable card with nothing to expand. The rule that makes a card openable is
children.isNotEmpty() || text.isNotBlank() || stepsUnavailable != null || (ownerToolCallId.isNotEmpty() && running). The last term exists because a trackedworker that has published nothing yet (the cap can release a branch before its first
record) must still be openable — opening it is the only gesture that re-attaches it.
The card cannot tell "tracked, nothing yet" from "not tracked at all", because a
tracked branch with no items leaves the owner item byte-identical to an untracked one.
So any running delegation row without a branch is now expandable to an empty panel. It
is kept rather than gated because gating it needs a "this owner is tracked" signal
threaded from the coordinator to the card — a new field on the shared presentation plus
plumbing through the coordinator and the card — which is a larger change than this PR.
Gating it here would also remove the only way to re-attach a worker that lost its lane
before it said anything.
Checklist