Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
267 commits
Select commit Hold shift + click to select a range
6dad39a
fix(native-chat): every journal append reaches the chats that are open
brennanb2025 Sep 25, 2026
97bf3bb
test(native-chat): an epoch replacement reaches the open chat
brennanb2025 Sep 25, 2026
427dc8d
test(native-chat): each row reaches an open chat once, and a live han…
brennanb2025 Sep 25, 2026
2bd44f5
test(native-chat): give the legacy-lease store test a tab id so the b…
brennanb2025 Sep 25, 2026
f79eb9b
test(worktree-activation): restore the OMP surfaced-agent resume test
brennanb2025 Sep 25, 2026
50d2e33
perf(native-chat): a publish behind a delivered commit reads nothing
brennanb2025 Sep 25, 2026
bd72b13
test(native-chat): state why the teardown test's fake journal is safe…
brennanb2025 Sep 25, 2026
fa9b76a
docs(native-chat): say mutation admission checks only the writer lease
brennanb2025 Sep 25, 2026
5435973
docs(native-chat): drop the send rebase from comments that still desc…
brennanb2025 Sep 25, 2026
8fd4558
Merge #22811 into the PR 1 base
brennanb2025 Sep 25, 2026
12dde37
Merge #22812 into the PR 1 base
brennanb2025 Sep 25, 2026
c753e8a
fix(native-chat): a message is accepted, then delivered
brennanb2025 Sep 25, 2026
82f9bfd
fix(native-chat): settle queued messages only for the child that ended
brennanb2025 Sep 25, 2026
f3e52d2
fix(native-chat): an adoption that fails to import keeps the conversa…
brennanb2025 Sep 25, 2026
29a340b
perf(native-chat): the recovering open reads the journal once
brennanb2025 Sep 25, 2026
c7a652f
fix(native-chat): an attach that fails after indexing its child leave…
brennanb2025 Sep 25, 2026
34d0bb0
fix(native-chat): a withdrawn message shows no error, and a rejection…
brennanb2025 Sep 25, 2026
46b6472
fix(orchestration): a structured worker whose agent outlasts the prea…
brennanb2025 Sep 25, 2026
283819d
fix(native-chat): a message rejected while its chat was closed reads …
brennanb2025 Sep 25, 2026
40dabe8
test(orchestration): name why the readiness settlement fakes are cast
brennanb2025 Sep 25, 2026
31444bf
fix(native-chat): keep each pane's own fence on frames so a failed re…
brennanb2025 Sep 25, 2026
494d8c7
docs(native-chat): drop the fence from the admission the send effects…
brennanb2025 Sep 25, 2026
0e74529
docs(native-chat): give the fence move on release the reason that sti…
brennanb2025 Sep 25, 2026
6c16f35
docs(native-chat): stop citing a write fence check in launch and mail…
brennanb2025 Sep 25, 2026
d2f646b
refactor(native-chat): the provider child is its own record
brennanb2025 Sep 25, 2026
25fa38a
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-wri…
brennanb2025 Sep 25, 2026
aa56b50
fix(native-chat): the delivery loop alone settles a message its start…
brennanb2025 Sep 25, 2026
d23cca4
Merge #22812 and main into PR 1
brennanb2025 Sep 25, 2026
4993696
refactor(native-chat): a stopped child ends on the one reading of its…
brennanb2025 Sep 25, 2026
a2ad8f2
feat(native-chat): the host says it accepts a send before any agent h…
brennanb2025 Sep 25, 2026
48def96
refactor(native-chat): an attach never opens a journal of its own
brennanb2025 Sep 25, 2026
da65d57
fix(native-chat): a moved fence resends nothing on a host that accept…
brennanb2025 Sep 25, 2026
0184a3d
refactor(native-chat): a child's end says whether the user or the hos…
brennanb2025 Sep 25, 2026
12577d8
fix(native-chat): a chat whose only work is a queued message is not o…
brennanb2025 Sep 25, 2026
e751c7e
fix(native-chat): the conversation outlives its agent
brennanb2025 Sep 25, 2026
a59eed9
test(native-chat): type the queued-message fixtures in the resume-off…
brennanb2025 Sep 25, 2026
ccaf412
fix(native-chat): a start that dies while a message waits on it is th…
brennanb2025 Sep 25, 2026
9123568
fix(native-chat): a request that failed reads as failed
brennanb2025 Sep 25, 2026
0efca93
docs(native-chat): say what an attach's open conversation and unconfi…
brennanb2025 Sep 25, 2026
36067f3
test(native-chat): a verdict change republishes the mobile status pro…
brennanb2025 Sep 25, 2026
a47a902
refactor(native-chat): the store's retention trigger keeps its flag c…
brennanb2025 Sep 25, 2026
69540f8
Merge remote-tracking branch 'origin/brennanb2025/chat-accept-then-de…
brennanb2025 Sep 25, 2026
918091d
test(native-chat): a user message the provider journaled keeps its se…
brennanb2025 Sep 25, 2026
74d6506
test(native-chat): pin what a failed start settles, and what a resume…
brennanb2025 Sep 25, 2026
9c671f1
test(native-chat): the failed-start pins fail on what the message bec…
brennanb2025 Sep 25, 2026
eaf1436
fix(native-chat): a restart offer ends when the chat's agent starts a…
brennanb2025 Sep 25, 2026
5b93c44
fix(runtime): end a transcript stream when its client unsubscribes
brennanb2025 Sep 25, 2026
40f3d35
fix(native-chat): a late provider-session update keeps a failed recov…
brennanb2025 Sep 25, 2026
80ccb33
test(orchestration): the preamble's host stub is typed, not cast
brennanb2025 Sep 25, 2026
97a5321
test(native-chat): the terminal-bell check asserts the renamed verdic…
brennanb2025 Sep 25, 2026
4c67270
Merge remote-tracking branch 'origin/brennanb2025/chat-accept-then-de…
brennanb2025 Sep 25, 2026
7805005
Merge remote-tracking branch 'origin/brennanb2025/chat-accept-then-de…
brennanb2025 Sep 25, 2026
dfe35eb
fix(native-chat): a failed turn ranks like a completion for attention
brennanb2025 Sep 25, 2026
3d3c52b
Merge remote-tracking branch 'origin/brennanb2025/chat-failed-reads-f…
brennanb2025 Sep 25, 2026
0a3f255
fix(native-chat): one fact ends a restart offer: the chat moved on si…
brennanb2025 Sep 25, 2026
4c45865
test(native-chat): an older build reads the restart offer this build …
brennanb2025 Sep 25, 2026
1887bef
fix(native-chat): read a restart offer against where the journal stoo…
brennanb2025 Sep 26, 2026
a862d71
test(native-chat): wait for the listing's retire write before reading…
brennanb2025 Sep 26, 2026
8caf8d7
refactor(native-chat): every journal row states which turn it belongs to
brennanb2025 Sep 26, 2026
0871b27
fix(native-chat): keep the terminal-backed chat's read error over its…
brennanb2025 Sep 26, 2026
6cde2a7
fix(native-chat): a start retries the exit settlement a failed journa…
brennanb2025 Sep 26, 2026
a96f475
fix(native-chat): a failed main agent reads failed while its subagent…
brennanb2025 Sep 26, 2026
9530e8d
perf(native-chat): answer the owner check without opening the chat
brennanb2025 Sep 26, 2026
2f39e1b
fix(native-chat): a read waiting on the session lock opens nothing on…
brennanb2025 Sep 26, 2026
b35ee58
test(native-chat): pin stated turn scopes, the upcast of unscoped row…
brennanb2025 Sep 26, 2026
d4d9d71
fix(native-chat): /compact is a message the chat sends, run as a turn…
brennanb2025 Sep 26, 2026
4ad06ca
fix(native-chat): read a failed resume's chat before calling it retry…
brennanb2025 Sep 26, 2026
1aaf776
test(native-chat): type the provider event sink the settlement test r…
brennanb2025 Sep 26, 2026
9d9a8ae
docs(native-chat): the worktree ps outcome comment no longer claims o…
brennanb2025 Sep 26, 2026
daaf0cf
fix(native-chat): say the structured read keeps trying only where it …
brennanb2025 Sep 26, 2026
20318fe
fix(native-chat): rows group under the turn their record names, not t…
brennanb2025 Sep 26, 2026
42307c8
test(native-chat): await the send's settlement instead of polling for…
brennanb2025 Sep 26, 2026
48cdb21
docs(native-chat): the status-store listing rule names provider-journ…
brennanb2025 Sep 26, 2026
9d9dd98
fix(native-chat): a restart offer resumes any time after the quit, an…
brennanb2025 Sep 26, 2026
e7458c1
fix(native-chat): a /compact is not a request the sidebar, notificati…
brennanb2025 Sep 26, 2026
bab2cef
fix(orchestration): route no mail to a structured worker its orchestr…
brennanb2025 Sep 26, 2026
75f5607
fix(native-chat): a failed retry names the user's prompt, not Orca's …
brennanb2025 Sep 26, 2026
50a3316
test(native-chat): pin what a conversation command's admission refuse…
brennanb2025 Sep 26, 2026
cf8e2fe
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 26, 2026
15b2efa
Merge remote-tracking branch 'origin/brennanb2025/chat-failed-reads-f…
brennanb2025 Sep 26, 2026
09e359f
test(native-chat): tests merged from the base state which turn their …
brennanb2025 Sep 26, 2026
bbec1b6
fix(native-chat): a refused send notifies failed through the completi…
brennanb2025 Sep 26, 2026
7a5e202
fix(orchestration): read the released row optionally, as the authorit…
brennanb2025 Sep 26, 2026
33c42e3
chore(native-chat): one import per module and no unexplained casts in…
brennanb2025 Sep 26, 2026
3e561ee
test(claude): pin which turn a Claude row joins, including a subagent…
brennanb2025 Sep 26, 2026
c0ea360
fix(native-chat): the status bar drops a restart offer the chat moved…
brennanb2025 Sep 26, 2026
60826fc
Merge remote-tracking branch 'origin/brennanb2025/chat-failed-reads-f…
brennanb2025 Sep 26, 2026
b8526e4
fix(native-chat): a refused steer is read from the turn its handover …
brennanb2025 Sep 26, 2026
b680294
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 26, 2026
e6dadd1
Merge main into PR 1
brennanb2025 Sep 26, 2026
a96d3fc
fix(mobile): the native-chat controller contract carries the turn jou…
brennanb2025 Sep 26, 2026
8cbdd44
fix(native-chat): the live turn is the running turn, not the newest u…
brennanb2025 Sep 26, 2026
4e391e1
test(native-chat): a turn the provider opened keeps its own clock
brennanb2025 Sep 26, 2026
70736fd
fix(native-chat): a running turn no message opened draws its status o…
brennanb2025 Sep 26, 2026
a6c7153
fix(native-chat): every copy of a row carries the main agent's own st…
brennanb2025 Sep 26, 2026
7b6eafb
test(native-chat): pin the worktree ps verdict across host and phone …
brennanb2025 Sep 26, 2026
a73c33a
test(mobile): name the parity table's row for its role
brennanb2025 Sep 26, 2026
f6d9c8a
Merge PR 1's latest head (main merged in) into the conversation-outli…
brennanb2025 Sep 26, 2026
e10362b
test(native-chat): a roster of idle or finished children does not kee…
brennanb2025 Sep 26, 2026
159b971
fix(native-chat): a request that settles while the user is asked some…
brennanb2025 Sep 26, 2026
20978d6
fix(orchestration): a task dispatched into a resting structured worke…
brennanb2025 Sep 26, 2026
ff274a7
fix(native-chat): a command's wait ends when its child does
brennanb2025 Sep 26, 2026
eed0954
fix(native-chat): keep refusing sends during a command on an older host
brennanb2025 Sep 26, 2026
ed8dd9f
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 26, 2026
b7ebbdb
Merge remote-tracking branch 'origin/brennanb2025/chat-failed-reads-f…
brennanb2025 Sep 26, 2026
742bda7
docs(native-chat): comments stop describing the hold this PR removed
brennanb2025 Sep 26, 2026
93ee58a
fix(native-chat): the completion says when the user is being asked
brennanb2025 Sep 26, 2026
b36d188
fix(native-chat): a restart offer keeps the start its own continuatio…
brennanb2025 Sep 26, 2026
7214f65
fix(native-chat): a rewound turn still names the message that opened it
brennanb2025 Sep 26, 2026
26fc397
fix(native-chat): Stop ends only the command it names
brennanb2025 Sep 26, 2026
d242e5e
Merge remote-tracking branch 'origin/brennanb2025/chat-accept-then-de…
brennanb2025 Sep 26, 2026
ca3e68f
fix(native-chat): an agent gets a full idle window after its owed wor…
brennanb2025 Sep 26, 2026
194fc33
test(claude): the options-read fixture runs a live child
brennanb2025 Sep 26, 2026
86d7365
test(native-chat): host tests reach its collaborators through a typed…
brennanb2025 Sep 26, 2026
e17c80e
fix(worktree-status): a departed agent's failure yields to live work …
brennanb2025 Sep 26, 2026
fb53224
refactor(orchestration): one owner answers a structured worker's custody
brennanb2025 Sep 26, 2026
ccb0610
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 26, 2026
e6922a5
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 27, 2026
1c48bf1
Merge remote-tracking branch 'origin/brennanb2025/chat-failed-reads-f…
brennanb2025 Sep 27, 2026
d7ce43a
refactor(orchestration): owed work is an open dispatch on the worker'…
brennanb2025 Sep 27, 2026
6374f35
Merge main into PR 1
brennanb2025 Sep 27, 2026
0f89353
docs(agent-status): a departed agent's failure ranks below live work …
brennanb2025 Sep 27, 2026
724b961
Merge main into PR 1
brennanb2025 Sep 27, 2026
9fa4452
fix(native-chat): a restart offer knows its continuations by a tag in…
brennanb2025 Sep 27, 2026
4d36cd3
Merge remote-tracking branch 'origin/brennanb2025/chat-accept-then-de…
brennanb2025 Sep 27, 2026
8e37dc9
test(runtime): the legacy-worker reveal test judges its stale snapsho…
brennanb2025 Sep 27, 2026
6545e9c
fix(native-chat): a message held behind /compact is drawn where it wa…
brennanb2025 Sep 27, 2026
8eef3fe
fix(native-chat): a phone's send during /compact answers without wait…
brennanb2025 Sep 27, 2026
7f8eda9
perf(native-chat): a rewind places provider items with one pass over …
brennanb2025 Sep 27, 2026
c39151d
fix(native-chat): a view never restarts a chat whose last start failed
brennanb2025 Sep 27, 2026
14dc4db
fix(native-chat): a message waiting behind /compact is drawn after it…
brennanb2025 Sep 27, 2026
897768d
Merge PR 1's latest head (main merged in) into the conversation-outli…
brennanb2025 Sep 27, 2026
be2d4e1
fix(native-chat): the idle sweep reads owed work every tick
brennanb2025 Sep 27, 2026
309777a
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 27, 2026
0fc424a
fix(native-chat): a continuation handed to the agent stays sent
brennanb2025 Sep 27, 2026
9613178
test(native-chat): start the child the loop waits on with an attach, …
brennanb2025 Sep 27, 2026
0224459
fix(native-chat): settle a gone generation's turn wherever a conversa…
brennanb2025 Sep 27, 2026
2276cef
test(native-chat): prove the next child's start settles the turn an e…
brennanb2025 Sep 27, 2026
9275869
test(native-chat): count a failed start's rows by row, not by text
brennanb2025 Sep 27, 2026
8fea3f0
test(cross-version): load the phone row readers without mobile's tool…
brennanb2025 Sep 27, 2026
8c966c7
test(cross-version): keep the checkout path-guard message and justify…
brennanb2025 Sep 27, 2026
1474464
fix(native-chat): a command ends only by its own provider answer or i…
brennanb2025 Sep 27, 2026
6a316b9
fix(native-chat): a command's settle answers its message before endin…
brennanb2025 Sep 27, 2026
09e94b8
fix(native-chat): "Worked for" counts from the handover, not the send
brennanb2025 Sep 27, 2026
158ca0a
test(native-chat): give the failed-start and stale-turn waits a loade…
brennanb2025 Sep 27, 2026
43e5c9f
test(native-chat): the interrupted create's own retry continues again
brennanb2025 Sep 27, 2026
301762e
docs(native-chat): three comments that still had views starting agents
brennanb2025 Sep 27, 2026
8f66a25
test(native-chat): pin the open's and the send's start and row counts…
brennanb2025 Sep 27, 2026
61cd62e
fix(native-chat): a second Stop on a command ends its child; one comp…
brennanb2025 Sep 27, 2026
dcc85e6
Merge PR 1's latest head into the conversation-outlives-agent branch
brennanb2025 Sep 27, 2026
da2ba05
Merge PR 1's latest head (its view-start test pinned at open and send…
brennanb2025 Sep 27, 2026
2a21363
test(native-chat): a reader's open settles the turn a failed exit set…
brennanb2025 Sep 27, 2026
0217612
test(native-chat): the view-start test's starting window outlasts two…
brennanb2025 Sep 27, 2026
ad04c3b
fix(native-chat): settle a gone generation's turn at every open but a…
brennanb2025 Sep 27, 2026
05b8b93
test(native-chat): hold the create's start open until the views bind
brennanb2025 Sep 27, 2026
e463c6a
refactor(native-chat): the provider's translator ends a command's tur…
brennanb2025 Sep 27, 2026
6bf458f
test(native-chat): pin a command's end to real /compact frames and to…
brennanb2025 Sep 27, 2026
320a4ff
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 27, 2026
065d98f
Merge remote-tracking branch 'origin/brennanb2025/chat-failed-reads-f…
brennanb2025 Sep 27, 2026
dc4b176
test(native-chat): tests merged from the base state which turn their …
brennanb2025 Sep 27, 2026
a72cec4
refactor(native-chat): drop the child-end waiter nothing waits on
brennanb2025 Sep 27, 2026
199e2d6
fix(native-chat): a command holds the queue only while its child runs it
brennanb2025 Sep 27, 2026
446d65f
fix(native-chat): a Claude /compact succeeds only on its compaction b…
brennanb2025 Sep 27, 2026
d5716f3
Merge main into PR 1
brennanb2025 Sep 27, 2026
f92abe2
fix(native-chat): a Claude child's exit says why the turn it ended st…
brennanb2025 Sep 27, 2026
3ea60f2
Merge remote-tracking branch 'origin/brennanb2025/chat-accept-then-de…
brennanb2025 Sep 27, 2026
4bf6080
fix(native-chat): a message waiting behind /compact draws below its l…
brennanb2025 Sep 27, 2026
ea4dbfb
fix(native-chat): only a running command holds a message below its li…
brennanb2025 Sep 27, 2026
740ed1f
Merge main into PR 1
brennanb2025 Sep 28, 2026
f74a642
Merge PR 1's latest head (main merged in) into the conversation-outli…
brennanb2025 Sep 28, 2026
fb34994
test(codex): the claim test names its notification params as a record
brennanb2025 Sep 28, 2026
69ff6df
test(native-chat): a read that reaches a crashed chat before the star…
brennanb2025 Sep 28, 2026
7002b1b
refactor(native-chat): drop the composer's second error formatter
brennanb2025 Sep 28, 2026
cb5ce82
test(native-chat): pin the reason on a message rejected while its cha…
brennanb2025 Sep 28, 2026
cec2d77
Merge PR 1's latest head into the conversation-outlives-agent branch
brennanb2025 Sep 28, 2026
f4963ad
docs(native-chat): drop the removed dispatch hold from six comments
brennanb2025 Sep 28, 2026
fcad779
Merge PR 1 into #22944
brennanb2025 Sep 28, 2026
5479e31
test(native-chat): rest the owner-status chat through the idle sweep,…
brennanb2025 Sep 28, 2026
57735e6
fix(native-chat): show the structured pane's retrying line when a rea…
brennanb2025 Sep 28, 2026
5b9379d
fix(native-chat): a send the provider never received after a restart …
brennanb2025 Sep 28, 2026
fd6b1f4
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 28, 2026
e9e2b3a
Merge remote-tracking branch 'origin/brennanb2025/chat-failed-reads-f…
brennanb2025 Sep 28, 2026
f7639bf
fix(native-chat): a failed Codex compaction's late completion writes …
brennanb2025 Sep 28, 2026
3e789fc
test(native-chat): the mid-command exit case resumes its next child a…
brennanb2025 Sep 28, 2026
9da7cd0
fix(native-chat): a /clear that never committed no longer locks the chat
brennanb2025 Sep 28, 2026
ea99997
fix(native-chat): a Codex rewind the provider did not keep no longer …
brennanb2025 Sep 28, 2026
f1bf5b4
test(native-chat): a clear retried after a crash starts no second rep…
brennanb2025 Sep 28, 2026
d89674a
chore(native-chat): the clear rerun comment claims only the stable re…
brennanb2025 Sep 28, 2026
1b2f025
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-con…
brennanb2025 Sep 28, 2026
4cd918b
test(native-chat): wait for a send's background start before the refu…
brennanb2025 Sep 28, 2026
97e7925
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-con…
brennanb2025 Sep 28, 2026
aceb39f
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-con…
brennanb2025 Sep 28, 2026
e6aaca2
fix(native-chat): a start a message waited on gets one failure row, t…
brennanb2025 Sep 28, 2026
7913e6f
Merge #22835's final head into #23059
brennanb2025 Sep 28, 2026
537d795
Merge origin/main (#22835 squash, #23502, #23537) into #23059
brennanb2025 Sep 28, 2026
ebbb88c
Merge origin/main (#23560, #23520, #23109) into #23059
brennanb2025 Sep 28, 2026
436d177
Merge origin/main (#23546) into #23059
brennanb2025 Sep 28, 2026
d6d4b29
Merge #23059's head (main merged after #22835) into #23524
brennanb2025 Sep 28, 2026
066b4ee
Merge origin/main (#23116) into #23059
brennanb2025 Sep 28, 2026
90b68d3
fix(native-chat): a /compact whose start failed says to run /compact …
brennanb2025 Sep 28, 2026
acdd559
Merge origin/main (#23608, #23682, #23456, #23605, #23618, #23702, #2…
brennanb2025 Sep 29, 2026
7b9fb3f
fix(native-chat): a Codex /compact ends only on its turn's completion…
brennanb2025 Sep 29, 2026
caa5be6
test(native-chat): main's crash-turn test states its row's turn, and …
brennanb2025 Sep 29, 2026
1e6c37d
test(native-chat): main's new journal tests state each row's turn
brennanb2025 Sep 29, 2026
9c847cb
Merge origin/main (#23573, #23621, #23801, #23802, #23799, #23765, #2…
brennanb2025 Sep 29, 2026
bda9ff1
Merge origin/main (#23808) into #23059
brennanb2025 Sep 29, 2026
077454c
Merge origin/main (#23671, #23026, #22090, #23816, #23815, #23499, #2…
brennanb2025 Sep 29, 2026
321a58a
fix(native-chat): draw a queued turn's message after the earlier turn…
brennanb2025 Sep 29, 2026
7b2f2d9
test(native-chat): run #23026's Stop tests against #23059's command t…
brennanb2025 Sep 29, 2026
898daae
fix(native-chat): typecheck main's Stop and restore-grouping code aga…
brennanb2025 Sep 29, 2026
54b147e
Merge #23059's head (898daaef37c) into #23524
brennanb2025 Sep 29, 2026
8a44a4e
fix(native-chat): say a /clear cut off by a restart left the chat unc…
brennanb2025 Sep 29, 2026
84fc7ff
fix(native-chat): a retried /clear finishes onto the conversation its…
brennanb2025 Sep 29, 2026
8995771
refactor(native-chat): stop recording that Codex acknowledged a rewind
brennanb2025 Sep 29, 2026
1e087ec
fix(native-chat): a /clear retried under a new operation id finishes …
brennanb2025 Sep 29, 2026
b67389a
fix(native-chat): a /clear retry never repeats a failed start or wait…
brennanb2025 Sep 29, 2026
4974af9
fix(native-chat): a /clear retry finishes only a replacement that sta…
brennanb2025 Sep 29, 2026
dc4ca58
Merge origin/main (d60f999f94b, #23059 squashed) into #23524
brennanb2025 Sep 29, 2026
9d3f676
refactor(native-chat): a record's identity fields are built in one place
brennanb2025 Sep 29, 2026
3bffed3
feat(native-chat): the store commits a /clear and its new conversatio…
brennanb2025 Sep 29, 2026
990bff2
fix(native-chat): /clear starts nothing; the new chat's first message…
brennanb2025 Sep 29, 2026
8b0f189
test(native-chat): drop the /clear retry wording test; no start runs …
brennanb2025 Sep 29, 2026
c9b008b
Merge origin/main (28073327354, #23923) into brennanb2025/chat-clear-…
brennanb2025 Sep 29, 2026
66e9658
test(native-chat): another window and a phone read a /clear's replace…
brennanb2025 Sep 29, 2026
7da6f8d
test(native-chat): a never-started replacement closes as settled
brennanb2025 Sep 29, 2026
a6534ad
Merge #23524's head (c9b008b873e) into the /clear lazy-start branch
brennanb2025 Sep 29, 2026
f26e803
fix(native-chat): /clear settles an interrupted Codex rewind the way …
brennanb2025 Sep 29, 2026
d57545b
fix(native-chat): a chat whose agent is not running keeps its `/` com…
brennanb2025 Sep 29, 2026
386f986
Merge origin/main (29c49aec31b) into brennanb2025/clear-starts-on-fir…
brennanb2025 Sep 29, 2026
9199f97
test(native-chat): queued drafts around /clear follow what a /clear n…
brennanb2025 Sep 29, 2026
18624fb
fix(native-chat): /clear opens the new conversation under its own lock
brennanb2025 Sep 29, 2026
5b6cd57
Merge origin/main (a57863ba167) into brennanb2025/clear-starts-on-fir…
brennanb2025 Sep 29, 2026
85a12cb
test(native-chat): queued drafts reach a /clear replacement that neve…
brennanb2025 Sep 29, 2026
4581395
fix(native-chat): /clear stops the old agent before it records the clear
brennanb2025 Sep 30, 2026
b0ccff4
fix(native-chat): a Claude chat at rest reads its `/` menu from Claud…
brennanb2025 Sep 30, 2026
e338bca
fix(native-chat): a Claude chat at rest keeps /model, /effort, /clear…
brennanb2025 Sep 30, 2026
7251f4e
fix(native-chat): an at-rest `/` scan keeps any newer answer and neve…
brennanb2025 Sep 30, 2026
d6c17bd
fix(native-chat): a failed at-rest `/` scan no longer discards an old…
brennanb2025 Sep 30, 2026
ee31bbe
Merge remote-tracking branch 'origin/main' into brennanb2025/clear-fi…
brennanb2025 Sep 30, 2026
d22b8cb
test(native-chat): open the at-rest command host on main's shared jou…
brennanb2025 Sep 30, 2026
d4e7d53
Merge origin/main (f37a84993e8) into #23935
brennanb2025 Sep 30, 2026
59c1445
Merge remote-tracking branch 'origin/main' into brennanb2025/clear-fi…
brennanb2025 Sep 30, 2026
9366ef7
refactor(agent-session): the transaction queue opens the session stor…
brennanb2025 Sep 30, 2026
36708c2
Merge origin/main (b4b708c2c4c) into #23935
brennanb2025 Sep 30, 2026
7508af3
test: open the store on the journal database and pass the close cause…
brennanb2025 Sep 30, 2026
86acc5e
Merge remote-tracking branch 'origin/main' into brennanb2025/clear-st…
brennanb2025 Sep 30, 2026
e3619c2
refactor(native-chat): a provider's at-rest commands are one adapter …
brennanb2025 Sep 30, 2026
58e63a1
test: the startup-reconcile tab close passes the close cause main now…
brennanb2025 Sep 30, 2026
10d019f
Merge main into PR 1 (after #24231)
brennanb2025 Oct 1, 2026
62d506a
Merge main into PR 1
brennanb2025 Oct 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions src/main/claude/claude-at-rest-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { join } from 'node:path'
import type { AgentSessionRecord } from '../../shared/agent-session-record'
import type { AgentSessionSlashCommand } from '../../shared/agent-session-wire'
import { structuredSlashCommands } from '../../shared/structured-agent-session-composer'
import { discoverSkills } from '../skills/discovery'
import { scanClaudeCommandFolders } from './claude-command-folder-scan'
import { supportsClaudeStructuredLocation } from './claude-structured-location-support'

/** How long one scan answers for a workspace and account before the next read scans again. */
export const CLAUDE_AT_REST_COMMANDS_TTL_MS = 10_000
/** A scan still unanswered this long no longer holds the next one off, so a hung folder can't
* freeze the menu. At most two run at once, so a folder that stays hung holds no more. */
const SCAN_OVERDUE_MS = 30_000
const MAX_SCANS_IN_FLIGHT = 2

export type ClaudeAtRestCommandsDeps = {
resolveWorkspacePath: (workspaceId: string) => Promise<string>
now?: () => number
discover?: typeof discoverSkills
}

type Entry = {
commands?: AgentSessionSlashCommand[]
/** When the kept answer, or the last failure, landed. */
scannedAt: number
lastStartedAt: number
inFlight: number
/** Numbers each scan, so only an answer newer than the one kept replaces it. */
started: number
kept: number
}

/**
* The `/` surface of a Claude chat whose Claude is not running, read from the folders Claude reads
* its own from on the host that runs it: the account's and the workspace's command folders and
* skills, beside the commands the chat's menu always offers. Nothing until the first scan lands;
* `onChange` fires when a scan finds something different.
*/
export class ClaudeAtRestCommandCatalog {
private readonly entries = new Map<string, Entry>()
private readonly listeners = new Set<() => void>()

constructor(private readonly deps: ClaudeAtRestCommandsDeps) {}

read = (record: AgentSessionRecord): AgentSessionSlashCommand[] | undefined => {
// Another host's folders are only readable there, so this host never answers for them.
if (record.provider !== 'claude' || !supportsClaudeStructuredLocation(record.location)) {
return undefined
}
const key = JSON.stringify([record.location.workspaceId, record.accountHome.path])
let entry = this.entries.get(key)
if (!entry) {
entry = {
scannedAt: Number.NEGATIVE_INFINITY,
lastStartedAt: Number.NEGATIVE_INFINITY,
inFlight: 0,
started: 0,
kept: 0
}
this.entries.set(key, entry)
}
const now = this.now()
const due =
entry.inFlight === 0
? now - entry.scannedAt >= CLAUDE_AT_REST_COMMANDS_TTL_MS
: entry.inFlight < MAX_SCANS_IN_FLIGHT && now - entry.lastStartedAt >= SCAN_OVERDUE_MS

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.

If two scans hang, this key is frozen forever. inFlight is decremented only in .finally, which runs when scan() settles, and scan() has no timeout — so once both slots are occupied inFlight < MAX_SCANS_IN_FLIGHT is permanently false, due never becomes true again, and the / surface stays at its last value until relaunch. This contradicts the comment above ("a hung folder can't freeze the menu") and the PR body's "a scan that has not answered in 30 seconds is given up on".

Technical details
# Two hung at-rest scans permanently freeze one workspace/account key

## Affected sites
- `src/main/claude/claude-at-rest-commands.ts:63-69` — the `due` predicate: `entry.inFlight === 0 ? TTL : entry.inFlight < MAX_SCANS_IN_FLIGHT && ...`
- `src/main/claude/claude-at-rest-commands.ts:90-109` — `scan()` is un-raced (`resolveWorkspacePath`, `discoverSkills`, `opendir`, `readFile`) and `inFlight` is only released in `.finally`

## Required outcome
- A scan that never settles must not permanently hold its in-flight slot: after `SCAN_OVERDUE_MS` a hung scan should be abandoned (or the overdue branch should ignore it) so a later read can still start a fresh scan and refresh the list.

## Suggested approach (optional)
- Race `scan(record)` against a timeout that rejects after `SCAN_OVERDUE_MS`, or track each in-flight scan's start time and let the overdue branch start a new scan regardless of how many are hung (bounded by a hard cap on concurrent scans).
- Either way, make the module comment at lines 11-12 match the real behavior.

if (due) {
this.scanInto(entry, record)
}
return entry.commands
}

onChange = (listener: () => void): (() => void) => {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}

private scanInto(entry: Entry, record: AgentSessionRecord): void {
const scan = ++entry.started
entry.lastStartedAt = this.now()
entry.inFlight += 1
const newest = () => {
if (scan < entry.kept) {
return false
}
entry.kept = scan
entry.scannedAt = this.now()
return true
}
void this.scan(record)
.then(
(commands) => {
if (!newest() || JSON.stringify(commands) === JSON.stringify(entry.commands)) {
return
}
entry.commands = commands
for (const listener of this.listeners) {
listener()
}
},
(error: unknown) => {
// Waits out the window before trying again, but leaves room for an older scan's answer.
entry.scannedAt = this.now()
console.warn('[claude] reading the at-rest `/` commands failed:', error)
}
)
.finally(() => {
entry.inFlight -= 1
})
}

private async scan(record: AgentSessionRecord): Promise<AgentSessionSlashCommand[]> {
const cwd = await this.deps.resolveWorkspacePath(record.location.workspaceId)
const account = record.accountHome.path
const [custom, discovered] = await Promise.all([
scanClaudeCommandFolders([join(cwd, '.claude', 'commands'), join(account, 'commands')]),
(this.deps.discover ?? discoverSkills)({
repos: [],
cwd,
providerRootOverrides: { claude: join(account, 'skills') }
})
])
// What the menu offers a Claude chat at rest without a list, so reading one loses none of it.
const builtIn = structuredSlashCommands(['clear', 'compact'], 'claude').map(
(command): AgentSessionSlashCommand => ({
name: command.name,
kind: 'command',
...(command.description ? { description: command.description } : {})
})
)
const names = new Set(builtIn.map((command) => command.name))
const skills = discovered.skills
.filter((skill) => skill.providers.includes('claude'))
.map((skill): AgentSessionSlashCommand => ({
name: skill.name,
kind: 'skill',
...(skill.description ? { description: skill.description } : {})
}))
return [...builtIn, ...custom.filter((command) => !names.has(command.name)), ...skills]
}

private now(): number {
return this.deps.now?.() ?? Date.now()
}
}
88 changes: 88 additions & 0 deletions src/main/claude/claude-command-folder-scan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { Dirent } from 'node:fs'
import { opendir, readFile } from 'node:fs/promises'
import { join, relative, sep } from 'node:path'
import type { AgentSessionSlashCommand } from '../../shared/agent-session-wire'
import { summarizeSkillMarkdown } from '../../shared/skill-metadata'

const MARKDOWN_EXTENSION = '.md'
const MAX_SCAN_DEPTH = 24
const MAX_SCAN_ENTRIES = 1_000
const MAX_COMMAND_FILE_BYTES = 256 * 1024

type ScanBudget = { remainingEntries: number }

/** A folder that cannot be read holds no commands we can offer, so it is skipped, not an error. */
async function readEntries(path: string, budget: ScanBudget): Promise<Dirent[]> {
try {
const entries: Dirent[] = []
for await (const entry of await opendir(path)) {
if (budget.remainingEntries === 0) {
break
}
budget.remainingEntries -= 1
entries.push(entry)
}
return entries.sort((left, right) => left.name.localeCompare(right.name))
} catch {
return []
}
}

async function walkMarkdown(
path: string,
depth: number,
budget: ScanBudget,
found: string[]
): Promise<void> {
if (depth > MAX_SCAN_DEPTH || budget.remainingEntries === 0) {
return
}
for (const entry of await readEntries(path, budget)) {
// Links are not followed: a command folder is the user's own files, not a way out of it.
if (entry.isSymbolicLink()) {
continue
}
const entryPath = join(path, entry.name)
if (entry.isDirectory()) {
await walkMarkdown(entryPath, depth + 1, budget, found)
} else if (entry.isFile() && entry.name.endsWith(MARKDOWN_EXTENSION)) {
found.push(entryPath)
}
}
}

async function describe(filePath: string): Promise<string | undefined> {
try {
const markdown = await readFile(filePath, 'utf8')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Oversized command files exhaust memory

When an at-rest Claude chat scans a workspace containing a large .claude/commands/*.md file, readFile loads the entire file before applying the 256 KiB limit. Opening the chat can therefore allocate memory proportional to the file’s size and potentially exhaust the host process. Bound the read itself.

How this was verified: An at-rest menu read reaches the workspace command scanner, which reads each Markdown file in full before truncating its contents.

return (
summarizeSkillMarkdown(markdown.slice(0, MAX_COMMAND_FILE_BYTES)).description ?? undefined
)
Comment on lines +54 to +59

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.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Limit the read before parsing the command file.

describe calls readFile on the whole file. It truncates the file only after the read. MAX_COMMAND_FILE_BYTES therefore does not limit memory or I/O. For example, a large .md file under .claude/commands in a cloned repository is read completely on every scan, and a scan can repeat every 10 s. slice also counts UTF-16 code units, not bytes. Open the file and read at most MAX_COMMAND_FILE_BYTES bytes.

Proposed fix
-import { opendir, readFile } from 'node:fs/promises'
+import { open, opendir } from 'node:fs/promises'
@@
 async function describe(filePath: string): Promise<string | undefined> {
+  const handle = await open(filePath, 'r').catch(() => null)
+  if (!handle) {
+    return undefined
+  }
   try {
-    const markdown = await readFile(filePath, 'utf8')
-    return (
-      summarizeSkillMarkdown(markdown.slice(0, MAX_COMMAND_FILE_BYTES)).description ?? undefined
-    )
+    const buffer = Buffer.alloc(MAX_COMMAND_FILE_BYTES)
+    const { bytesRead } = await handle.read(buffer, 0, MAX_COMMAND_FILE_BYTES, 0)
+    return summarizeSkillMarkdown(buffer.subarray(0, bytesRead).toString('utf8')).description ?? undefined
   } catch {
     return undefined
+  } finally {
+    await handle.close()
   }
 }

} catch {
return undefined
}
}

/**
* Claude's custom commands in `roots`: every `*.md` below each folder, named by its path from
* the folder with `:` between segments (`frontend/test.md` is `/frontend:test`). The first root
* to name a command wins, so list the closest folder first.
*/
export async function scanClaudeCommandFolders(
roots: readonly string[]
): Promise<AgentSessionSlashCommand[]> {
const budget = { remainingEntries: MAX_SCAN_ENTRIES }
const commands = new Map<string, AgentSessionSlashCommand>()
for (const root of roots) {
const files: string[] = []
await walkMarkdown(root, 0, budget, files)
for (const file of files) {
const name = relative(root, file).slice(0, -MARKDOWN_EXTENSION.length).split(sep).join(':')
if (commands.has(name)) {
continue
}
const description = await describe(file)
commands.set(name, { name, kind: 'command', ...(description ? { description } : {}) })
}
}
return [...commands.values()]
}
2 changes: 2 additions & 0 deletions src/main/claude/claude-structured-session-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda
private readonly exitLifecycle: ClaudeExitLifecycle

constructor(private readonly deps: ClaudeStructuredSessionAdapterDeps) {
this.atRestCommands = deps.atRestCommands
this.exitLifecycle = {
sessions: this.sessions,
exits: this.exits,
Expand Down Expand Up @@ -232,6 +233,7 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda
this.sessions.has(sessionId) ? { supportsTaskStop: true, supportsStopAll: true } : undefined
readCommands: NonNullable<StructuredAgentSessionAdapter['readCommands']> = (sessionId) =>
this.sessions.get(sessionId)?.commands.commands
readonly atRestCommands: ClaudeStructuredSessionAdapterDeps['atRestCommands']
holdsDispatch = (sessionId: string): boolean => {
const session = this.sessions.get(sessionId)
return session ? claudeHoldsDispatch(session) : false
Expand Down
3 changes: 3 additions & 0 deletions src/main/claude/claude-structured-session-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type { AgentChildWorkEvidence } from '../../shared/agent-status-child-wor
import type { ClaudeBackgroundTaskTracker } from './claude-background-task-tracker'
import type { ClaudeChildWorkDecoder } from './claude-child-work-decoder'
import type { ClaudeSlashCommandCatalog } from './claude-slash-command-catalog'
import type { ClaudeAtRestCommandCatalog } from './claude-at-rest-commands'
import type { ClaudeSessionStartup } from './claude-structured-session-startup-state'

export type ClaudeAuthDiagnostic = {
Expand Down Expand Up @@ -93,6 +94,8 @@ export type ClaudeLateDispatchOutcome =
| { clientMessageId: string; state: 'unknown'; reason: string }

export type ClaudeStructuredSessionAdapterDeps = {
/** The `/` surface of a chat whose Claude is not running. */
atRestCommands?: ClaudeAtRestCommandCatalog
resolveLaunch: (input: {
identity: AgentSessionJournalIdentity
}) => Promise<ClaudeStructuredLaunch>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { SubmissionRejectionFact } from '../../../shared/agent-session-failure'
import type { StructuredAgentSessionAtRestCommands } from './structured-agent-session-at-rest-commands'
import type { AgentSessionJournalIdentity } from '../../../shared/agent-session-journal-types'
import type {
AgentSessionAccountHome,
Expand Down Expand Up @@ -121,6 +122,16 @@ export class StructuredAgentSessionAdapterRouter implements StructuredAgentSessi
readCommands: NonNullable<StructuredAgentSessionAdapter['readCommands']> = (sessionId) =>
this.liveOwnerOrNull(sessionId)?.readCommands?.(sessionId)

atRestCommands: StructuredAgentSessionAtRestCommands = {
read: (record) => this.adapters[record.provider].atRestCommands?.read(record),
onChange: (listener) => {
const stops = Object.values(this.adapters).flatMap((adapter) =>
adapter.atRestCommands ? [adapter.atRestCommands.onChange(listener)] : []
)
return () => stops.forEach((stop) => stop())
}
}

answerPrompt: StructuredAgentSessionAdapter['answerPrompt'] = (input) =>
this.owner(input.sessionId).answerPrompt(input)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
AgentSessionExecutionLocation,
AgentSessionProcessIdentity
} from '../../../shared/agent-session-record'
import type { StructuredAgentSessionAtRestCommands } from './structured-agent-session-at-rest-commands'
import type {
AgentSessionOptionsResult,
AgentSessionSlashCommand,
Expand Down Expand Up @@ -347,6 +348,8 @@ export type StructuredAgentSessionAdapter = {
/** The `/` surface the running provider reports for itself. Undefined when the
* provider never reports one, which is what keeps the client on its catalog. */
readCommands?(sessionId: string): AgentSessionSlashCommand[] | undefined
/** The `/` surface of a chat whose agent is not running; absent when the provider has none. */
atRestCommands?: StructuredAgentSessionAtRestCommands
/** Claims the live callback, builds the provider reply, commits the journal CAS while that claim is
* held, then answers it. A reply that cannot be built throws `AgentSessionPromptAnswerRejectedError`
* before the commit. A prompt cancel claims the same callback, so only one operation can commit. */
Expand Down
Loading
Loading