Skip to content

shell: delegation threads, a line each over the chat an agent asked from - #779

Merged
futurepaul merged 19 commits into
masterfrom
claude/shell-threads
Oct 31, 2026
Merged

futurepaul merged 19 commits into
masterfrom
claude/shell-threads

Conversation

@futurepaul

@futurepaul futurepaul commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Step 3 of agents and threads (Paul, 2026-10-30): delegation threads in the shell. It stacks on the stub-asks PR, which lets the local lanes test asks end to end.

What the person sees

  • When one of their agents asks another (fragment ask --return-to, as Hermes' fragment_ask and the stub's ask do), the chat it asked from shows one line per thread above the chat: the asked agent's face and name, the first line of what was asked, its state, how long it has run, and Stop while it runs.
  • The states are Asked, Working, Needs you (a card of the asked agent's waits for the person), Done, Stopped, and Lost (any other end, with the error as its title).
  • A line opens the pair chat, the thread's transcript, which is an ordinary chat. Its transcript is never shown inline.
  • Pair chats leave the chat list. A pair chat is one of theirs with exactly two of their agents, labelled as fragment ask labels a pair. A person's own group chats stay listed.
  • A line stays until its answer has come. That means the answer record (an:<asked turn>) is on the asker's tasks, and the asker's turn of it has ended in the asking chat. So a thread that's stopped or lost is always shown, never dropped.
  • The asking chat's row says "Waiting on Rowan" in place of its time while a thread runs.
  • Stop posts the asked agent's owner's stop command in the pair chat (stop:<turn>). The person's own Stop there would belong to its asker, an agent, and be ignored.

No new platform state. A thread's state comes from the pair chat's listed records, live on the list socket. The question, its time, its return_to and its turn's end come from the pair chat's newest records (before=, 100 of chat and 400 of work), read again whenever its row changes at all. A row holds each member's newest record and is sent at most once in 3 s, so a start or end may be past before a row says it. An ended thread's answer is looked for (the asker's newest 100 tasks records, the asking chat's 400 work records) when the asking chat's row changes, or another of its threads is newly seen to end.

Tests

  • cell/shell-tests/threads.test.mjs (run by cargo xtask check on the pinned Node, node --test): the real createThreads against rows published only when the test says, each holding a poster's newest record:
  • shell-ui threads_ui: the first agent asks the Reader through the stub's ask:
    • Asked plainly: the answer comes back as the asker's own turn, and no line stays.
    • Asked for a card: one line (Reader, the question, Needs you, the time, Stop); the row waits on the Reader; the pair chat is out of the list; the line opens the pair chat.
    • Stop: the line says Stopped until the answer is in, then goes.
    • Cut by a restart (the computer lever's kill): the line says Lost until the answer is in, then goes.

Complexity ledger

  • Added: cell/shell/threads.js (~200 lines, its own module), a #threads strip (index.html, ~15 lines of CSS), and two hooks in chat-list.js (hide pair chats; "Waiting on").
  • No platform change, no new state, no new timers but a 30 s redraw of the elapsed time while a thread runs.
  • Reads: a pair chat's two record reads each time its row is sent while its agent works (a kind change, or the minute's resend: LISTED_FRESH_MS).
  • Test: cell/shell-tests/threads.test.mjs (~130 lines), on Settings: Add agent (a name and a role), and each agent's role #763's node --test step.
  • Duplicated: pairLabel mirrors the CLI's pair_label (cli/src/ask.rs); the lane's asks keep the two in agreement end to end.

🤖 Generated with Claude Code

futurepaul and others added 7 commits October 30, 2026 17:57
Our Hermes image reads an agent's status for its title and its role
(`agentRole`, its fragment's `meta.role`) in one read, and tells it its
role between its name and its owner's home ("Your role is chief of
staff: what your owner made you for, as it is now."), none when it has
none, so an edit reaches its next turn. Its profile.yaml description,
what teammates read it does in Hermes' roster, is its role, else its
job's first line. Its job never copies the role.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Several agents, one UI for any number (Paul, 2026-10-30): each chat's
row is two lines, its agent first (its face, a group's stacked; its name;
its role's tag; what it is doing there, else when), then the chat's own
name. "New chat with [agent]" names the open chat's agent, else the main
one; with more than one agent its chip lists them all, the pick for that
chat alone. The launcher's start row follows the same rule, with a pick
beside it. The launcher's button is a search and its key, beside the
brand. The main agent's pill and its menu go: what it is doing is on
its chats' rows, its profile and screen in a chat's ⋯, its name and
picture in Settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings' Agents adds an agent with a name and a role, made as the
first run makes its agent (one shared `makeAgent`): its own chat opens
at once with the making's steps, and it says hello first. Its job names
no role; its role is its fragment.json's meta.role, which its runtime
tells it. Each agent's row has a Role field, written and put live as its
name is. Add is offered while the computer runs fewer than 16 agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat rows now say what their agent is doing, so a row whose agent
finished its work while the list changed is rightly drawn again; every
row that still says what it said stays the same element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stub image builds the fragment CLI by our Hermes image's own stage
(cached by the same ids; xtask's guard reads both), its context now the
repo's root as Hermes' is. Its script runtime's `ask <agent> <words>`
runs `fragment ask --return-to <this chat>` as the agent acting for its
owner, as Hermes' fragment_ask does, so the local lanes test an ask end
to end on the real CLI path: the computers lane's lead asks its second
agent, the question in their pair chat, its answer back as the lead's
own turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When one of the person's agents asks another (`fragment ask --return-to`),
the chat it asked from shows one line per thread: the asked agent, the
first line of what was asked, its state (Asked, Working, Needs you, Done,
Stopped, Lost), how long, and Stop (the owner's `stop` in the pair chat).
A line opens the pair chat, its transcript; pair chats leave the chat
list. A line stays until its answer has come (its `an:<asked turn>` on
the asker's tasks, and the asker's turn of it ended in the asking chat),
so a thread stopped or lost is said, never dropped; the asking chat's row
waits on the asked agent meanwhile. Nothing new is kept: a pair chat's
listed records, and its newest records read when its row changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@futurepaul
futurepaul added this pull request to stack #764 October 31, 2026 00:44

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fail for now at bde93fd1ba22ced21b3622cc10528a5120870be7, reviewing this layer against #778 at 4194934e61d2df8dda17d6443f76226f3a692cfd.

The separate shell module and existing public APIs fit this feature. However, the cache invalidation assumes more ordering/history than listed records provide.

Blocking:

  1. Coalesced work updates can leave a running thread at Asked, without Stop. cell/shell/threads.js:62 includes only turn.start and turn.end in the signature. Listed records contain each poster's newest record, and publication coalesces changes over three seconds (docs/api.md, Listed records). If the question is read before its turn starts, and the next published row already contains turn.step or turn.prompt, the signature remains unchanged. refresh skips the work read, the cached thread has no turn, and stateOf keeps returning Asked even while an approval card waits. Including the latest work state in invalidation must account for starts/ends that were superseded before publication; the list is not an event stream.

  2. A newly discovered ended thread can inherit another thread's completed answer check. cell/shell/threads.js:107-123 keys answerChecks only by the asking chat's signature, not the turns being checked. With two asks returning to the same chat, the cached pair read can contain only the first end. If the asking chat publishes both completed answer turns before the pair publishes its second end, the pair signature still matches its cached read. It checks only the first ended thread and caches that asking-chat signature. When the next pair update discovers the second end, the asking chat is unchanged, so its answer is never checked. The second line stays Done (or Stopped/Lost) until unrelated activity or a page reload. Newly discovered ended turns need their own answer check even when that chat's signature is already cached.

I reproduced both against this head's actual createThreads using mocked public-API responses on the repository's pinned Node: prompt update produced no reread and remained Asked; delayed discovery of the second end left a completed line across repeated refreshes. Please cover these coalesced-update and interleaved-read cases alongside the existing sequential e2e cases.

Validation: the focused reproduction and module syntax check ran locally. CI check and images check pass; e2e shards and real-Hermes Docker are still pending. Full e2e was not run locally. The external engineering-style document named by AGENTS.md is absent on this machine.

futurepaul and others added 2 commits October 30, 2026 19:51
The stub's CLI stage builds its dependencies first, from the manifests
and the lock alone (our crates empty sources), so a change to the CLI,
core or proto rebuilds only our crates; its target is the layers' own, so
CI's BuildKit cache keeps the dependencies' build. Unoptimized (the
stub's agents make a few calls with it): a third of the build time, a
binary of 17 MB. xtask's guard reads its dependency-first form.

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

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fail for now at d6955c6deecb8f3ee3ed6fc39b64fa009365c1f8, reviewing this layer against #778 at e2e0c7ddf06efa5b1c0135e7d764252380a15601.

Neither blocker from my previous review is fixed. This head merges base-layer Docker/build changes; the shell implementation and its tests are unchanged.

Blocking:

  1. Coalesced work updates still leave running threads at Asked without Stop. cell/shell/threads.js:62 ignores work records other than turn.start and turn.end. If the question is cached before its turn starts and the next listed row already contains turn.prompt or turn.step, the signature does not change. refresh skips the read, and stateOf returns Asked because the cached turn is absent. Listed rows retain each poster's newest record and combine changes over three seconds (docs/api.md, Listed records), so observing every start is not guaranteed. Invalidate from the latest work state even when it supersedes a start/end.

  2. Newly discovered ended threads still inherit an earlier answer check. cell/shell/threads.js:107-123 caches checks only by return-chat signature. When two asks return to one chat, that chat can publish both completed answer turns while the pair cache contains only the first end. Checking that first thread caches the return-chat signature. When the pair later publishes the second end, the unchanged return-chat signature skips checking its answer, leaving its completed line visible until unrelated activity or reload. Include newly discovered ended turns in the check boundary even when the return-chat signature is unchanged.

Please cover coalesced work updates and interleaved pair/return-chat publication, alongside the sequential e2e cases. The separate module and existing public APIs remain an appropriate scope for this feature; no additional complexity objection.

Validation: rechecked the current-head source and listed-record contract, and confirmed the affected implementation/tests are byte-for-byte unchanged from the earlier review. I did not rerun the earlier mocked reproductions or full e2e. Current CI check, image checks, Docker and e2e shards are pending. The external engineering-style document is unavailable on this machine.

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fail for now at d075a6a38894401cd51fafa6e254dcf6292b4476, against #778 at 1b14f3cc0c5bbfc46d4eeb0c88b4e8a9a158935d.

Neither previous blocker is fixed. The only tree change since my last verdict is in docs/computers.md; shell code and tests are unchanged.

Blocking:

  1. Coalesced work updates leave running threads at Asked without Stop. cell/shell/threads.js:62 filters the invalidation signature to turn.start/turn.end. When the question is cached before its turn starts and the next listed row already contains turn.prompt or turn.step, the signature stays unchanged, the work read is skipped, and the missing cached turn keeps the line at Asked. Listed records keep each poster’s newest record and coalesce publication over three seconds (docs/api.md, Listed records). Invalidation must handle work states that supersede starts/ends before publication.

  2. Newly discovered ended threads inherit an earlier answer check. cell/shell/threads.js:107-123 keys answer checks only by return-chat signature. With two asks returning to one chat, both answer turns can finish and publish while the pair cache contains only the first end. Checking that first thread caches the return-chat signature; discovering the second end later does not invalidate it. The completed second line remains until unrelated activity or reload. Newly discovered ended turns must receive their own check even when the return-chat signature is unchanged.

Please cover coalesced work updates and interleaved pair/return-chat publication. The separate module and public-API approach remain appropriate; no additional complexity objection.

Validation: rechecked both source paths and the listed-record contract at this head, and verified shell implementation/tests are unchanged from the previously reviewed head. I did not rerun the earlier reproductions or full e2e. CI check, image checks, Docker and e2e shards remain pending. The external engineering-style document named by AGENTS.md is unavailable on this machine.

…read's answer

Listed records are each member's newest, sent at most once in 3 s, so a
turn's start or end may be past before a row says it (#779 review, 1): a
pair chat is read again whenever its row changes. An asking chat's answers
are looked for again when another of its threads is seen to end, even if
its own row is unchanged (#779 review, 2).

cargo xtask check runs the repo's *.test.mjs on the pinned Node's own
runner; cell/shell-tests/threads.test.mjs covers both cases (each failed
before this change).

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

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pass for this stack layer at ea17a8fb3dbbef67a5514e88cbc7b1ab94ebe070, against #778 at 1b14f3cc0c5bbfc46d4eeb0c88b4e8a9a158935d.

Both previous blockers are fixed: pair reads now invalidate on coalesced work updates beyond start/end, and answer checks include the ended turns being checked, so a newly discovered end cannot inherit an earlier check. The regression tests exercise both publication orderings.

The separate shell module and existing public APIs remain appropriate for delegation threads. The small Node test integration uses the existing file discovery and pinned runtime without adding dependencies. I see no remaining blocking regression in this layer.

Validation: all four tests in cell/shell-tests/threads.test.mjs pass locally on pinned Node v24.22.1. CI check passes; image checks, real-Hermes Docker and all four e2e shards remain pending. Full check/e2e were not run locally. The external engineering-style document named by AGENTS.md is unavailable on this machine.

futurepaul and others added 3 commits October 30, 2026 20:47
Add kept its name and role in memory alone, and made its chat before its
agent, so a reload in between left a chat nothing tied to them (#763
review). Add now makes its agent first, its title, job, tier and role
written, then its chat: once its chat exists, all Add was asked for is on
the platform. A page's start (resumeFirstRun, once no first run is owed)
finds one of their chats, <label>-chat, with no message yet, whose agent
<label> is not in it, and makes it on from there; in it but quiet, its
hello is posted again (the same record). Cut before its chat, Add again
with that name makes on the agent it made.

cargo xtask check runs the repo's *.test.mjs on the pinned Node's own
runner; cell/shell-tests/onboarding.test.mjs cuts each step, reloads, and
checks one agent with its role in one chat (all seven reload cases failed
before this change).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	xtask/src/js_syntax.rs
@futurepaul

Copy link
Copy Markdown
Collaborator Author

Thanks, both reproduced, and both fixed in ea17a8f. They reach this layer through 3bff24a, which merges #763's fix up the stack.

  1. Coalesced work updates leave a running thread at Asked. Fixed at the root: a pair chat is read again whenever its row changes at all, meaning any listed record (channel, principal, kind, at) and previewAt, not only turn.start/turn.end. A row holds each poster's newest record, so any change at all may hide a start or an end. The cost is the pair chat's two reads per send of its row while its agent works: a kind change, or the minute's resend (LISTED_FRESH_MS), at most once in 3 s.

  2. A newly ended thread inherits an earlier answer check. An asking chat's check is now keyed by its row's signature and the ended turns waiting on it (checkOf). So a thread newly seen to end gets its own look for its answer, even when that chat's row hasn't changed.

Tests: cell/shell-tests/threads.test.mjs runs the real createThreads against a world whose rows are published only when the test says, each holding a poster's newest record. cargo xtask check now runs the repo's *.test.mjs with the pinned Node's own runner (node --test).

  • The question is read; then its turn's start and a turn.step (or turn.prompt) go out in one row. The line says Working (or Needs you). Before the fix it stayed at Asked.
  • Two asks return to one chat. That chat's row is sent with both answers in before the pair's row says the second ended. The second line goes once its end is read, and stays gone. Before the fix it stayed at Done.

Both fail on bde93fd's threads.js and pass now. The sequential e2e cases (threads_ui) are unchanged.

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pass for this stack layer at 3bff24a77475d5cd331708f433a88bb2a3f3c609, against the incorporated #778 commit dce4c235015bd82bf6587593d29171a5b305f38d (that base branch has since advanced).

Both earlier blockers remain fixed: coalesced work updates invalidate pair reads, and newly discovered ended turns receive an answer check. The thread implementation is unchanged from my approved head ea17a8fb3dbbef67a5514e88cbc7b1ab94ebe070. This merge brings in the lower layer's Add-agent recovery and combines the two shell test suites correctly. The separate module and existing public APIs remain appropriate; I found no new blocking regression in this layer.

Validation: all 14 onboarding/thread tests pass locally on pinned Node v24.22.1. CI check, images check, real-Hermes Docker, and all four e2e shards are pending. Full check/e2e were not run locally. The external engineering-style document named by AGENTS.md is unavailable on this machine.

# Conflicts:
#	cell/shell-tests/onboarding.test.mjs
#	cell/shell/chat-list.js
#	cell/shell/onboarding.js
#	cell/shell/shell.css
#	cell/shell/shell.js
#	crates/e2e/src/lanes/shell.rs
#	xtask/src/js_syntax.rs

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pass for this stack layer at c58cd3e1f470264104672de9958baa006753af73, against #778 at 6feb34f2b43f494475164bf3f4827dd3eef6b63b.

Both earlier blockers remain fixed: coalesced work updates invalidate pair reads, and newly discovered ended turns receive an answer check. The thread implementation and regression tests are unchanged from the previously approved head. I checked this merge's integration with the updated shell and base-layer changes and found no new blocking regression in this layer. The separate module and existing public APIs remain appropriate; no additional complexity concern.

Validation: all 15 shell onboarding/thread tests pass locally on pinned Node v24.22.1. CI check, images check and all four e2e shards are pending. Full check/e2e were not run locally. The external engineering-style document named by AGENTS.md is unavailable on this machine.

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pass for this stack layer at 75625fd166e153daae10e1c62da96d073962e608, against #778 at b201eff549c575e6bd883a00bb518cbe50be1210.

Both earlier blockers remain fixed: coalesced work updates invalidate pair reads, and newly discovered ended turns receive an answer check. The shell implementation and regression tests are unchanged from my previously approved head. I checked the merged integration and found no new blocking regression in this layer. The separate module and existing public APIs remain appropriate; no additional complexity concern.

Validation: all 15 shell onboarding/thread tests pass locally on pinned Node v24.22.1; CI check passes. The images check fails in simplex::run::tests::no_unsettled_cancel_is_let_go (43 cancels retained versus 64 expected), in bridge code inherited unchanged from the base; this layer changes neither the image workspace nor its workflow. Real-Hermes Docker and all four e2e shards remain pending. Full check/e2e were not run locally. The external engineering-style document named by AGENTS.md is unavailable on this machine.

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pass for this stack layer at 6a61bf14166ab7a2402493211ff869c048f3dd47, against #778 at edb1daf8bb72e684fdb836abf8a2a4e61871eebb.

Both earlier blockers remain fixed: coalesced work updates invalidate pair reads, and newly discovered ended turns receive an answer check. The thread implementation and regression tests are unchanged from my previously approved head. I checked the merge integration with the updated shell layout and e2e code and found no new blocking regression in this layer. The separate module and existing public APIs remain appropriate; no additional complexity concern.

Validation: all 15 shell onboarding/thread tests pass locally on pinned Node v24.22.1. CI check, images check, real-Hermes Docker and all four e2e shards remain pending. Full check/e2e were not run locally. The external engineering-style document named by AGENTS.md is unavailable on this machine.

Base automatically changed from claude/stub-asks to master October 31, 2026 05:48
@futurepaul
futurepaul requested a review from alexlwn123 October 31, 2026 07:12
@futurepaul

Copy link
Copy Markdown
Collaborator Author

Merged master in (019df47) after #763 and #778 merged, since the Session Archive had landed. Two conflicts, both additive, both sides kept:

  • cell/shell/shell.js: the chat list's data keeps master's chatTitle (the sessions title) and adds the threads hooks (isThread, waitingOn, renderThreads). load() calls both threads.refresh() and sessionArchive.load().
  • crates/templates/src/lib.rs: SHELL_PUBLISHED lists both session-archive.js and threads.js (47).

No threads code changed. cargo xtask check passes, including both *.test.mjs files (15/15).

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pass at 019df471add4409e7935377647757a5f2936042e, against master at 90fa6b883aee5fd73bc7e3479f62b1626c7d16f5.

Both earlier blockers remain fixed: coalesced work updates invalidate pair reads, and newly discovered ended turns receive an answer check. The thread module and regression tests are unchanged from my previously approved head. I checked the merge integration with the Session Archive, updated chat-row state handling, release assets and e2e code and found no new blocking regression. The separate module and existing public APIs remain appropriate; no additional complexity concern.

Validation: all 15 shell onboarding/thread tests pass locally on pinned Node v24.22.1; CI check passes. Images check, real-Hermes Docker and all four e2e shards remain pending. Full check/e2e were not run locally. The external engineering-style document named by AGENTS.md is unavailable on this machine.

@futurepaul
futurepaul added this pull request to the merge queue Oct 31, 2026
Merged via the queue into master with commit 135a142 Oct 31, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants