@agentex/agent 0.0.36 → 0.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,176 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.37 — Turn liveness and background-task identity (Claude)
4
+
5
+ Claude Code starts turns by itself. When a background task finishes, the CLI
6
+ enqueues the notification as user input, which opens a fresh turn with no host
7
+ involvement. A host that tracks "is the agent working" from its own `send()`
8
+ cannot see those turns — `send()` already resolved — so the session reads as
9
+ finished while the agent is visibly working. Verified against Claude Code
10
+ 2.1.241, where one user message produced two `result` events with a
11
+ self-started turn between them.
12
+
13
+ ### Added
14
+
15
+ - **`turn_start` StreamEvent.** Pairs with `result`, which closes a turn.
16
+ Carries `trigger: "send" | "resume"` — `resume` meaning the provider opened
17
+ the turn on its own. Emitted for host-initiated turns too, so `turn_start` →
18
+ `result` describes turn liveness straight off the stream rather than by
19
+ inference from dispatch.
20
+
21
+ It deliberately does not name the background task behind a `resume`. Claude
22
+ delivers a task's result and opens the turn as two unlinked records, and with
23
+ several tasks in flight the pairing is not recoverable from the wire; every
24
+ attempt to infer it produced a plausible id that was sometimes wrong.
25
+ Correlate through `background_task.report` and `toolUseId`, which the
26
+ provider does state.
27
+
28
+ Attribution is exact, not inferred. A host message carries a uuid and a
29
+ provider-initiated continuation does not: every dequeued input with a uuid
30
+ emits `command_lifecycle` naming it, while the task-notification continuation
31
+ is enqueued without one. So a turn opened by a `started` naming an
32
+ outstanding message *is* that message's turn. A build that has never emitted
33
+ `command_lifecycle` falls back to the oldest unclaimed send; once one has
34
+ been seen, the fallback is disabled, because guessing there would mislabel
35
+ twice.
36
+
37
+ - **`turn_end` StreamEvent.** Every `turn_start` is followed by exactly one,
38
+ carrying the same `turnId` and a `reason`. `result` cannot serve as the close
39
+ signal on its own: a message the CLI cancels, discards, or refuses opens a
40
+ turn and produces no result, so a host pairing `turn_start` with `result`
41
+ would stay busy forever on those paths. `result` remains the outcome payload
42
+ and is ordered before the `turn_end` that follows it.
43
+
44
+ - **`background_task.report`.** The task's delivered output (`summary`,
45
+ `outputFile`, `usage`), present only on the event that hands the result
46
+ back. Claude emits *both* `task_updated` and `task_notification` for a single
47
+ completion; they are different records, not duplicates, and only the
48
+ notification carries the result. Collapsing them into one indistinguishable
49
+ `phase: "completed"` event made hosts render every finished task twice, once
50
+ with its report and once empty. A task that was stopped or killed delivered
51
+ nothing and carries no report. Codex applies the identical rule at every
52
+ emitter, so "one row per delivered result" holds across providers.
53
+
54
+ - **`background_task.toolUseId`.** The tool call that launched the task — the
55
+ same id Claude writes into a subagent's `meta.json`, and the only structured
56
+ task-to-tool_call link the wire provides. It was being discarded.
57
+
58
+ ### Changed
59
+
60
+ - Turn, command, and task state is now reduced from the ordered wire in one
61
+ place, rather than held in mutable fields updated across `await` boundaries.
62
+ Each turn's settlement batch is captured synchronously before any suspension
63
+ point, so a turn that opens while an earlier handler is still draining cannot
64
+ have its sends resolved by that handler — the failure that made two
65
+ back-to-back results resolve both callers with the first result.
66
+
67
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
68
+ dequeued while a turn is running joins that turn instead of starting its own.
69
+ Modelling one command per turn left every coalesced message permanently
70
+ unsettled.
71
+
72
+ ### Fixed
73
+
74
+ - A background task's `taskType` and `description` survive its whole lifetime.
75
+ `task_started` is the only record that names them; the patches and the
76
+ completion notification that follow identify the task by id alone, so every
77
+ finished subagent normalized to `taskType: "unknown"` and hosts rendered
78
+ "Background task completed" for what was plainly a subagent. A completion for
79
+ a task the session never saw start still reports `unknown` rather than
80
+ guessing. The cache is bounded at 512 entries, oldest evicted.
81
+
82
+ - A detached subagent's own output no longer drives the parent session. Claude
83
+ streams a child's assistant text, thinking, and tool calls onto the parent
84
+ stream while the root turn is already over. Anything carrying
85
+ `parent_tool_use_id` is the child working, not the session: it does not open
86
+ a turn, does not move `session.state`, and a child's permission prompt no
87
+ longer parks the parent in `waiting_for_approval` with nothing able to clear
88
+ it.
89
+
90
+ - `session.state` and `turn_start`/`result` agree. A host turn took `thinking`
91
+ from `send()`; a provider-initiated one had nothing to set it, so `state`
92
+ read `idle` for the whole head of every resume turn while `turn_start` had
93
+ already fired.
94
+
95
+ - Turn settling is synchronous with the `result` line. The CLI can flush a
96
+ result and the next turn's opening line in one chunk; deferring the close
97
+ until the event chain drained swallowed the following `turn_start`, and
98
+ deferring the turn's send-resolvers with it let the next turn mistake them
99
+ for its own. The resolvers are appended to a settling list, never assigned
100
+ over — overwriting discarded the earlier turn's resolvers outright, hanging
101
+ that caller's `send()` and deadlocking `drain()` behind it.
102
+
103
+ - Background-task state is tracked from the wire, not from the delivery path.
104
+ It was maintained inside event dispatch, which only runs when a host
105
+ subscribed, so `session.state`, `drain()`, and turn attribution silently
106
+ degraded for a host that reads the session without an `onEvent` handler.
107
+
108
+ - A send resolves with its own turn. `_pendingResults` was a flat queue drained
109
+ by whatever `result` landed next, so a follow-up the CLI had merely queued
110
+ resolved against a turn that never contained it — the host then read
111
+ "finished" for a message still waiting to run. Each entry now carries its
112
+ command uuid and settles only when the turn that claimed it completes. A
113
+ terminal `command_lifecycle` settles a message the CLI retires without
114
+ running, which previously had nothing to settle it at all.
115
+
116
+ - `drain()` waits for provider-initiated turns and for running subagents.
117
+ `_inFlight` only tracks turns the host dispatched, so draining during the gap
118
+ between a root result and the resume turn that follows it — measured at 8-11
119
+ seconds live — SIGTERM'd the CLI and killed the child outright. Background
120
+ *processes* are deliberately excluded: a dev server started with
121
+ `run_in_background` may never exit, and the contract is "let the agent's work
122
+ settle", not "outlive whatever it launched". Bounded by a deadline so a
123
+ wedged turn cannot hang the drain.
124
+
125
+ It also holds across the gap between a task's result being delivered and the
126
+ resume turn it triggers — 24ms for a subagent, 71ms for a background process.
127
+ The task is no longer live by then, so waiting on live tasks alone left
128
+ `drain()` landing in that window and killing the very turn it was extended to
129
+ protect. The delivery record is the provider stating a turn is coming, so it
130
+ is used as one; the wait is released by the next turn to open or close.
131
+
132
+ - Turns opened by a command that ends in `refused`, `discarded`, or `cancelled`
133
+ are closed by that record. Those states never produce a `result`, so nothing
134
+ else would ever close them and the session pinned as working with no path
135
+ back.
136
+
137
+ - Wire lines arriving after `close()` are ignored, and `close()` rejects sends
138
+ still waiting on a turn instead of stranding the caller forever.
139
+
140
+ - `turn_start` carries the `eventId` of the line that opened it, and a minimal
141
+ `raw`. A resume turn is headed by `system/init`, whose payload runs to ~5KB
142
+ of tools, skills, plugins, and MCP config; echoing it whole made hosts that
143
+ persist `raw` pay that for every resume, twice.
144
+
145
+ ### Compatibility
146
+
147
+ - Additive at the type level. `turn_start` is a new event type — consumers with
148
+ exhaustive `switch` statements over `StreamEvent` will need a case or a
149
+ default. `report` and `toolUseId` are new required fields on
150
+ `background_task`; every in-tree provider sets them, and both are `null`
151
+ where the provider reports nothing.
152
+ - No behavior change for OpenCode, Cursor, or any other provider. Codex gains
153
+ `report`/`toolUseId` and is otherwise untouched; only the Claude provider
154
+ emits `turn_start`.
155
+
3
156
  ## 0.0.36 — OpenCode empty-turn follow-ups
4
157
 
5
158
  Follow-ups to the OpenCode empty-turn handling in 0.0.35.
6
159
 
160
+ ### Changed
161
+
162
+ - Turn, command, and task state is now reduced from the ordered wire in one
163
+ place, rather than held in mutable fields updated across `await` boundaries.
164
+ Each turn's settlement batch is captured synchronously before any suspension
165
+ point, so a turn that opens while an earlier handler is still draining cannot
166
+ have its sends resolved by that handler — the failure that made two
167
+ back-to-back results resolve both callers with the first result.
168
+
169
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
170
+ dequeued while a turn is running joins that turn instead of starting its own.
171
+ Modelling one command per turn left every coalesced message permanently
172
+ unsettled.
173
+
7
174
  ### Fixed
8
175
 
9
176
  - The empty-turn classifier's "finished" guard now lives in `terminalOutcome`,
@@ -28,6 +195,26 @@ Follow-ups to the OpenCode empty-turn handling in 0.0.35.
28
195
  Codex's handling of the same action, rather than `agent_error` with a
29
196
  JSON-stringified error object.
30
197
 
198
+ - `session.state` no longer follows a detached child. The state machine set
199
+ `thinking` on any assistant line, including a subagent's, so `state` and
200
+ `turn_start`/`result` contradicted each other for the child's entire run —
201
+ measured at 7.6s live — with nothing to clear it if the child was stopped or
202
+ killed.
203
+ - `stream_event` opens a turn. Under `includePartialMessages` the whole
204
+ streamed reply arrived before `turn_start`, which reintroduced the reported
205
+ bug one layer down: output visible while the session still read as finished.
206
+ - A `send()` issued while a turn is settling is no longer classified as
207
+ `resume`. The trigger reads a counter of host messages still awaiting a
208
+ turn; the resolver list cannot answer that, because a turn's resolvers are
209
+ moved off it the moment its `result` is read.
210
+ - `_pendingResumeTaskId` is consumed only by the resume turn it explains. It
211
+ was cleared on every turn open, so a host send landing between a task's
212
+ delivery and its resume turn wiped the attribution.
213
+ - Turn state, the task-fact cache, and the unclaimed-send counter are all
214
+ reset when a session's pending work is rejected (exit, crash, close). A
215
+ `_turnOpen` left set would also suppress the `idle` fallback in
216
+ `handleResult` and pin a dead session as working with no path back.
217
+
31
218
  ### Compatibility
32
219
 
33
220
  - The incomplete-turn note is emitted as `type: "assistant"` (the only surface a
@@ -39,6 +226,20 @@ Follow-ups to the OpenCode empty-turn handling in 0.0.35.
39
226
 
40
227
  ## 0.0.35 — Codex turn-boundary correctness and model discovery
41
228
 
229
+ ### Changed
230
+
231
+ - Turn, command, and task state is now reduced from the ordered wire in one
232
+ place, rather than held in mutable fields updated across `await` boundaries.
233
+ Each turn's settlement batch is captured synchronously before any suspension
234
+ point, so a turn that opens while an earlier handler is still draining cannot
235
+ have its sends resolved by that handler — the failure that made two
236
+ back-to-back results resolve both callers with the first result.
237
+
238
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
239
+ dequeued while a turn is running joins that turn instead of starting its own.
240
+ Modelling one command per turn left every coalesced message permanently
241
+ unsettled.
242
+
42
243
  ### Fixed
43
244
 
44
245
  - A Codex `send()` issued while the previous turn's result was still being
@@ -183,6 +384,26 @@ Follow-ups to the OpenCode empty-turn handling in 0.0.35.
183
384
  contract test now checks both directions so a provider cannot declare
184
385
  discovery it does not implement, or implement discovery it does not declare.
185
386
 
387
+ - `session.state` no longer follows a detached child. The state machine set
388
+ `thinking` on any assistant line, including a subagent's, so `state` and
389
+ `turn_start`/`result` contradicted each other for the child's entire run —
390
+ measured at 7.6s live — with nothing to clear it if the child was stopped or
391
+ killed.
392
+ - `stream_event` opens a turn. Under `includePartialMessages` the whole
393
+ streamed reply arrived before `turn_start`, which reintroduced the reported
394
+ bug one layer down: output visible while the session still read as finished.
395
+ - A `send()` issued while a turn is settling is no longer classified as
396
+ `resume`. The trigger reads a counter of host messages still awaiting a
397
+ turn; the resolver list cannot answer that, because a turn's resolvers are
398
+ moved off it the moment its `result` is read.
399
+ - `_pendingResumeTaskId` is consumed only by the resume turn it explains. It
400
+ was cleared on every turn open, so a host send landing between a task's
401
+ delivery and its resume turn wiped the attribution.
402
+ - Turn state, the task-fact cache, and the unclaimed-send counter are all
403
+ reset when a session's pending work is rejected (exit, crash, close). A
404
+ `_turnOpen` left set would also suppress the `idle` fallback in
405
+ `handleResult` and pin a dead session as working with no path back.
406
+
186
407
  ### Compatibility
187
408
 
188
409
  - Discovery is curated-list-plus-validation for Claude, not enumeration. A tier
@@ -229,6 +450,20 @@ Follow-ups to the OpenCode empty-turn handling in 0.0.35.
229
450
 
230
451
  ## 0.0.34 - Codex collaboration task lifecycle
231
452
 
453
+ ### Changed
454
+
455
+ - Turn, command, and task state is now reduced from the ordered wire in one
456
+ place, rather than held in mutable fields updated across `await` boundaries.
457
+ Each turn's settlement batch is captured synchronously before any suspension
458
+ point, so a turn that opens while an earlier handler is still draining cannot
459
+ have its sends resolved by that handler — the failure that made two
460
+ back-to-back results resolve both callers with the first result.
461
+
462
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
463
+ dequeued while a turn is running joins that turn instead of starting its own.
464
+ Modelling one command per turn left every coalesced message permanently
465
+ unsettled.
466
+
232
467
  ### Fixed
233
468
 
234
469
  - Codex 0.144 collaboration tool calls now register spawned child threads as
@@ -241,6 +476,26 @@ Follow-ups to the OpenCode empty-turn handling in 0.0.35.
241
476
  child-thread, and reconciliation signals. Session shutdown also closes any
242
477
  still-active child lifecycle instead of leaving a permanent running state.
243
478
 
479
+ - `session.state` no longer follows a detached child. The state machine set
480
+ `thinking` on any assistant line, including a subagent's, so `state` and
481
+ `turn_start`/`result` contradicted each other for the child's entire run —
482
+ measured at 7.6s live — with nothing to clear it if the child was stopped or
483
+ killed.
484
+ - `stream_event` opens a turn. Under `includePartialMessages` the whole
485
+ streamed reply arrived before `turn_start`, which reintroduced the reported
486
+ bug one layer down: output visible while the session still read as finished.
487
+ - A `send()` issued while a turn is settling is no longer classified as
488
+ `resume`. The trigger reads a counter of host messages still awaiting a
489
+ turn; the resolver list cannot answer that, because a turn's resolvers are
490
+ moved off it the moment its `result` is read.
491
+ - `_pendingResumeTaskId` is consumed only by the resume turn it explains. It
492
+ was cleared on every turn open, so a host send landing between a task's
493
+ delivery and its resume turn wiped the attribution.
494
+ - Turn state, the task-fact cache, and the unclaimed-send counter are all
495
+ reset when a session's pending work is rejected (exit, crash, close). A
496
+ `_turnOpen` left set would also suppress the `idle` fallback in
497
+ `handleResult` and pin a dead session as working with no path back.
498
+
244
499
  ### Compatibility
245
500
 
246
501
  - The change is isolated to Codex collaboration events. Claude and the other
@@ -264,6 +519,20 @@ Follow-ups to the OpenCode empty-turn handling in 0.0.35.
264
519
  `getClaudeTaskDetails()` remains available for Claude-native fields and
265
520
  accepts legacy `unknown` task events from older agentex versions.
266
521
 
522
+ ### Changed
523
+
524
+ - Turn, command, and task state is now reduced from the ordered wire in one
525
+ place, rather than held in mutable fields updated across `await` boundaries.
526
+ Each turn's settlement batch is captured synchronously before any suspension
527
+ point, so a turn that opens while an earlier handler is still draining cannot
528
+ have its sends resolved by that handler — the failure that made two
529
+ back-to-back results resolve both callers with the first result.
530
+
531
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
532
+ dequeued while a turn is running joins that turn instead of starting its own.
533
+ Modelling one command per turn left every coalesced message permanently
534
+ unsettled.
535
+
267
536
  ### Fixed
268
537
 
269
538
  - Codex live sessions now track the active root turn and interrupt it with the
@@ -277,6 +546,26 @@ Follow-ups to the OpenCode empty-turn handling in 0.0.35.
277
546
  as `aborted`. Interrupt RPC errors propagate to the caller so hosts can show a
278
547
  failed Stop action instead of reporting false success.
279
548
 
549
+ - `session.state` no longer follows a detached child. The state machine set
550
+ `thinking` on any assistant line, including a subagent's, so `state` and
551
+ `turn_start`/`result` contradicted each other for the child's entire run —
552
+ measured at 7.6s live — with nothing to clear it if the child was stopped or
553
+ killed.
554
+ - `stream_event` opens a turn. Under `includePartialMessages` the whole
555
+ streamed reply arrived before `turn_start`, which reintroduced the reported
556
+ bug one layer down: output visible while the session still read as finished.
557
+ - A `send()` issued while a turn is settling is no longer classified as
558
+ `resume`. The trigger reads a counter of host messages still awaiting a
559
+ turn; the resolver list cannot answer that, because a turn's resolvers are
560
+ moved off it the moment its `result` is read.
561
+ - `_pendingResumeTaskId` is consumed only by the resume turn it explains. It
562
+ was cleared on every turn open, so a host send landing between a task's
563
+ delivery and its resume turn wiped the attribution.
564
+ - Turn state, the task-fact cache, and the unclaimed-send counter are all
565
+ reset when a session's pending work is rejected (exit, crash, close). A
566
+ `_turnOpen` left set would also suppress the `idle` fallback in
567
+ `handleResult` and pin a dead session as working with no path back.
568
+
280
569
  ### Compatibility
281
570
 
282
571
  - Timeout and AbortSignal cancellation remain best-effort and preserve their
@@ -286,6 +575,20 @@ Follow-ups to the OpenCode empty-turn handling in 0.0.35.
286
575
 
287
576
  ## 0.0.32 — Codex root-thread completion isolation
288
577
 
578
+ ### Changed
579
+
580
+ - Turn, command, and task state is now reduced from the ordered wire in one
581
+ place, rather than held in mutable fields updated across `await` boundaries.
582
+ Each turn's settlement batch is captured synchronously before any suspension
583
+ point, so a turn that opens while an earlier handler is still draining cannot
584
+ have its sends resolved by that handler — the failure that made two
585
+ back-to-back results resolve both callers with the first result.
586
+
587
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
588
+ dequeued while a turn is running joins that turn instead of starting its own.
589
+ Modelling one command per turn left every coalesced message permanently
590
+ unsettled.
591
+
289
592
  ### Fixed
290
593
 
291
594
  - Codex live sessions now pin their root thread and ignore notifications from
@@ -302,6 +605,26 @@ Follow-ups to the OpenCode empty-turn handling in 0.0.35.
302
605
  - Live Codex event identity prefers the event's own thread scope while retaining
303
606
  the pinned root id as a compatibility fallback for older unscoped events.
304
607
 
608
+ - `session.state` no longer follows a detached child. The state machine set
609
+ `thinking` on any assistant line, including a subagent's, so `state` and
610
+ `turn_start`/`result` contradicted each other for the child's entire run —
611
+ measured at 7.6s live — with nothing to clear it if the child was stopped or
612
+ killed.
613
+ - `stream_event` opens a turn. Under `includePartialMessages` the whole
614
+ streamed reply arrived before `turn_start`, which reintroduced the reported
615
+ bug one layer down: output visible while the session still read as finished.
616
+ - A `send()` issued while a turn is settling is no longer classified as
617
+ `resume`. The trigger reads a counter of host messages still awaiting a
618
+ turn; the resolver list cannot answer that, because a turn's resolvers are
619
+ moved off it the moment its `result` is read.
620
+ - `_pendingResumeTaskId` is consumed only by the resume turn it explains. It
621
+ was cleared on every turn open, so a host send landing between a task's
622
+ delivery and its resume turn wiped the attribution.
623
+ - Turn state, the task-fact cache, and the unclaimed-send counter are all
624
+ reset when a session's pending work is rejected (exit, crash, close). A
625
+ `_turnOpen` left set would also suppress the `idle` fallback in
626
+ `handleResult` and pin a dead session as working with no path back.
627
+
305
628
  ### Compatibility
306
629
 
307
630
  - Unscoped global Codex notifications continue to flow through. The documented
@@ -355,6 +678,20 @@ Follow-ups to the OpenCode empty-turn handling in 0.0.35.
355
678
 
356
679
  ## 0.0.30 — Complete Claude and Codex host capabilities
357
680
 
681
+ ### Changed
682
+
683
+ - Turn, command, and task state is now reduced from the ordered wire in one
684
+ place, rather than held in mutable fields updated across `await` boundaries.
685
+ Each turn's settlement batch is captured synchronously before any suspension
686
+ point, so a turn that opens while an earlier handler is still draining cannot
687
+ have its sends resolved by that handler — the failure that made two
688
+ back-to-back results resolve both callers with the first result.
689
+
690
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
691
+ dequeued while a turn is running joins that turn instead of starting its own.
692
+ Modelling one command per turn left every coalesced message permanently
693
+ unsettled.
694
+
358
695
  ### Fixed
359
696
 
360
697
  - Claude and Codex now declare the resumability, permission, question, and
@@ -489,6 +826,26 @@ remain source-compatible.
489
826
  - Codex endpoint header names are emitted as quoted TOML path segments, so
490
827
  valid names containing dots cannot become nested configuration keys.
491
828
 
829
+ - `session.state` no longer follows a detached child. The state machine set
830
+ `thinking` on any assistant line, including a subagent's, so `state` and
831
+ `turn_start`/`result` contradicted each other for the child's entire run —
832
+ measured at 7.6s live — with nothing to clear it if the child was stopped or
833
+ killed.
834
+ - `stream_event` opens a turn. Under `includePartialMessages` the whole
835
+ streamed reply arrived before `turn_start`, which reintroduced the reported
836
+ bug one layer down: output visible while the session still read as finished.
837
+ - A `send()` issued while a turn is settling is no longer classified as
838
+ `resume`. The trigger reads a counter of host messages still awaiting a
839
+ turn; the resolver list cannot answer that, because a turn's resolvers are
840
+ moved off it the moment its `result` is read.
841
+ - `_pendingResumeTaskId` is consumed only by the resume turn it explains. It
842
+ was cleared on every turn open, so a host send landing between a task's
843
+ delivery and its resume turn wiped the attribution.
844
+ - Turn state, the task-fact cache, and the unclaimed-send counter are all
845
+ reset when a session's pending work is rejected (exit, crash, close). A
846
+ `_turnOpen` left set would also suppress the `idle` fallback in
847
+ `handleResult` and pin a dead session as working with no path back.
848
+
492
849
  ### Compatibility and limits
493
850
 
494
851
  - OpenCode 1.3.2 is the release-tested server schema. Safe disconnect uses
@@ -505,6 +862,20 @@ remain source-compatible.
505
862
 
506
863
  ## 0.0.27 — Codex session reasoning effort
507
864
 
865
+ ### Changed
866
+
867
+ - Turn, command, and task state is now reduced from the ordered wire in one
868
+ place, rather than held in mutable fields updated across `await` boundaries.
869
+ Each turn's settlement batch is captured synchronously before any suspension
870
+ point, so a turn that opens while an earlier handler is still draining cannot
871
+ have its sends resolved by that handler — the failure that made two
872
+ back-to-back results resolve both callers with the first result.
873
+
874
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
875
+ dequeued while a turn is running joins that turn instead of starting its own.
876
+ Modelling one command per turn left every coalesced message permanently
877
+ unsettled.
878
+
508
879
  ### Fixed
509
880
 
510
881
  - **Codex session reasoning effort.** Multi-turn Codex sessions now forward
@@ -630,6 +1001,20 @@ invisible to callers. `getProvider` stays synchronous; only heavy modules
630
1001
  The loader defaults to the built-in `acpProvider`; `registerAcpFactory` stays
631
1002
  exported and honored as an override hook.
632
1003
 
1004
+ ### Changed
1005
+
1006
+ - Turn, command, and task state is now reduced from the ordered wire in one
1007
+ place, rather than held in mutable fields updated across `await` boundaries.
1008
+ Each turn's settlement batch is captured synchronously before any suspension
1009
+ point, so a turn that opens while an earlier handler is still draining cannot
1010
+ have its sends resolved by that handler — the failure that made two
1011
+ back-to-back results resolve both callers with the first result.
1012
+
1013
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
1014
+ dequeued while a turn is running joins that turn instead of starting its own.
1015
+ Modelling one command per turn left every coalesced message permanently
1016
+ unsettled.
1017
+
633
1018
  ### Fixed
634
1019
 
635
1020
  - **TDZ crash on direct provider import.** `import("@agentex/agent/providers/gemini")`
@@ -693,6 +1078,20 @@ A session-scoped **goal** primitive. Attach a durable objective and the library
693
1078
  - **`GoalController` + reconstruction helpers**, exported for hosts: `goalStateFromEvent`, `latestGoalFromEvents`, `normalizeClaudeGoalAttachment`, `normalizeCodexGoalStatus`, `normalizeCodexGoalRecord`, `createDefaultSentinel`, `parseAssessment`, `isTerminalGoalStatus`, `EMULATED_GOAL_CAPABILITY`, `GOAL_OBJECTIVE_MAX`, `CODEX_GOAL_TOOLS`, plus the `GoalState` / `GoalStatus` / `GoalOptions` / `GoalSentinel` / `SetGoalResult` / `ClearGoalResult` types.
694
1079
  - **Native observability + resume.** Claude writes `goal_status` only to the on-disk transcript (never live stdout), so a native goal session tails its transcript to surface `active`→`met` and restores an unmet goal on `--resume`. Codex rehydrates a durable goal on resume via `thread/goal/get`. Both confirmed live.
695
1080
 
1081
+ ### Changed
1082
+
1083
+ - Turn, command, and task state is now reduced from the ordered wire in one
1084
+ place, rather than held in mutable fields updated across `await` boundaries.
1085
+ Each turn's settlement batch is captured synchronously before any suspension
1086
+ point, so a turn that opens while an earlier handler is still draining cannot
1087
+ have its sends resolved by that handler — the failure that made two
1088
+ back-to-back results resolve both callers with the first result.
1089
+
1090
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
1091
+ dequeued while a turn is running joins that turn instead of starting its own.
1092
+ Modelling one command per turn left every coalesced message permanently
1093
+ unsettled.
1094
+
696
1095
  ### Fixed
697
1096
 
698
1097
  - **Codex failed turns are no longer reported as success.** codex 0.130 signals failure via `turn/completed` with `turn.status: "failed"` (carrying `turn.error.message`), not only `turn/failed` — agentex hardcoded `isError:false`, so a failed turn (e.g. a 4xx from the model API) looked `completed`. The parser + session now detect the failed status and the trailing `error` notification, reporting `status:"failed"` with the error text. Verified live.
@@ -724,6 +1123,20 @@ Additive. The instruction-file twin of `installSkills`: install an orientation b
724
1123
 
725
1124
  Driven by consumer feedback from an embedding host wiring an orchestrator onto agentex sessions. All additive — except the MCP fix, which replaces behavior that never worked in any published version.
726
1125
 
1126
+ ### Changed
1127
+
1128
+ - Turn, command, and task state is now reduced from the ordered wire in one
1129
+ place, rather than held in mutable fields updated across `await` boundaries.
1130
+ Each turn's settlement batch is captured synchronously before any suspension
1131
+ point, so a turn that opens while an earlier handler is still draining cannot
1132
+ have its sends resolved by that handler — the failure that made two
1133
+ back-to-back results resolve both callers with the first result.
1134
+
1135
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
1136
+ dequeued while a turn is running joins that turn instead of starting its own.
1137
+ Modelling one command per turn left every coalesced message permanently
1138
+ unsettled.
1139
+
727
1140
  ### Fixed
728
1141
 
729
1142
  - **`config.mcpServers` actually attaches MCP servers now.** It previously emitted `--mcp-server <name> -- <command>…` — a flag that does not exist in Claude Code 2.x — so any run/session setting the field died instantly with `error: unknown option '--mcp-server'` (verified against claude 2.1.165; the field has never worked in any published version, so there is no behavior to migrate from). The config is now staged as a **mode-0600 JSON file** in a temp dir and passed via the real `--mcp-config <path>`, cleaned up with the run/session (including spawn-failure paths). Secrets never touch argv — http `headers` (bearer tokens) live only in the 0600 file; argv is world-readable via `ps`.
@@ -759,6 +1172,20 @@ A three-tier provider architecture (deep-native · ACP · bespoke). The ACP tier
759
1172
  - **Codex collaboration modes.** `codexProvider.listModes()` discovers Codex's collaboration modes via `collaborationMode/list`; `config.modeId` applies a chosen mode to a fresh `thread/start` (a resumed thread keeps its original mode). `capabilities.modes` is now `true` for Codex.
760
1173
  - **Codex structured questions.** The app-server `requestUserInput` (and legacy `tool/requestUserInput`) server→client request is now bridged to `onUserInputRequest` as an `AskUserQuestion`, with answers mapped back into Codex's `{ answers: { [id]: { answers: [] } } }` shape — Codex sessions can answer questions headlessly.
761
1174
 
1175
+ ### Changed
1176
+
1177
+ - Turn, command, and task state is now reduced from the ordered wire in one
1178
+ place, rather than held in mutable fields updated across `await` boundaries.
1179
+ Each turn's settlement batch is captured synchronously before any suspension
1180
+ point, so a turn that opens while an earlier handler is still draining cannot
1181
+ have its sends resolved by that handler — the failure that made two
1182
+ back-to-back results resolve both callers with the first result.
1183
+
1184
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
1185
+ dequeued while a turn is running joins that turn instead of starting its own.
1186
+ Modelling one command per turn left every coalesced message permanently
1187
+ unsettled.
1188
+
762
1189
  ### Fixed
763
1190
 
764
1191
  - **Codex tool-approval response shape.** Command/file approval requests are now answered with `{ decision: "accept" | "decline" }` (Codex's actual app-server contract) instead of `{ approved: boolean }`, which the app-server did not honor. Tool-permission gating in Codex sessions now works headlessly.
@@ -796,6 +1223,20 @@ Scheduled / fire-and-forget session runs needed three things the SDK pushed onto
796
1223
 
797
1224
  - **`SendHandle`, `SendOptions`, `CancelResult`** are now exported from the package entry point (previously only the `AgentSession` interface was).
798
1225
 
1226
+ ### Changed
1227
+
1228
+ - Turn, command, and task state is now reduced from the ordered wire in one
1229
+ place, rather than held in mutable fields updated across `await` boundaries.
1230
+ Each turn's settlement batch is captured synchronously before any suspension
1231
+ point, so a turn that opens while an earlier handler is still draining cannot
1232
+ have its sends resolved by that handler — the failure that made two
1233
+ back-to-back results resolve both callers with the first result.
1234
+
1235
+ - A turn owns a *set* of command uuids. The CLI coalesces: a host message
1236
+ dequeued while a turn is running joins that turn instead of starting its own.
1237
+ Modelling one command per turn left every coalesced message permanently
1238
+ unsettled.
1239
+
799
1240
  ### Fixed
800
1241
 
801
1242
  - **Session `close()` now honors `ProviderConfig.graceSec`** for the SIGTERM → SIGKILL window (was hardcoded to 5s, ignoring the config field that `execute()` already respected). `drain()` uses the same configurable grace.
package/README.md CHANGED
@@ -382,8 +382,10 @@ Variants:
382
382
  - `tool_result` — Tool returned a result (`toolCallId: string | null`, `toolName: string | null`, `content`, `isError`, `exitCode: number | null`). `toolName` mirrors the matching `tool_call.name` (correlated for you), so you don't need your own `toolCallId → name` cache; null when no preceding `tool_call` was seen on the stream.
383
383
  - `rate_limit` — Provider reported rate-limit state (`status`, `limitType`, `resetAt`, `overageStatus`, `isUsingOverage`)
384
384
  - `permission_mode` — Permission mode change mid-session (`permissionMode: string`). Claude only, e.g., when the user accepts a plan and the session leaves `plan` mode.
385
- - `background_task` — Provider-neutral lifecycle for work that can outlive the root turn (`taskId`, `taskType`, `phase`, `status`, `description`, `summary`, `parentTaskId`). A terminal task event never settles the root turn.
386
- - `result` — Final result (`text`, `costUsd`, `isError`, `stopReason`, `terminalReason`, `numTurns`, `durationMs`)
385
+ - `background_task` — Provider-neutral lifecycle for work that can outlive the root turn (`taskId`, `taskType`, `phase`, `status`, `description`, `summary`, `parentTaskId`, `toolUseId`, `report`). A terminal task event never settles the root turn.
386
+ - `turn_start` — A turn opened (`turnId`, `trigger`). `trigger` is `"send"` when the host dispatched it and `"resume"` when the provider started it on its own — Claude does the latter when a background task's result comes back, opening a turn no `send()` can see. Track "is the agent working" from `turn_start` → `turn_end`, not from your own dispatch. `turnId` is session-local (`turn-1`, `turn-2`, …), not a provider id. `turn_start` deliberately does not name the task behind a `resume`: Claude delivers a task's result and opens the turn as two unlinked records, so with several tasks in flight the pairing is not recoverable from the wire — correlate through `background_task.report` and `toolUseId` instead.
387
+ - `turn_end` — The matching close (`turnId`, `trigger`, `reason`). Exactly one per `turn_start`, carrying the same `turnId`. **Close on this, not on `result`.** `reason: "result"` is a normal completion whose payload arrives as the accompanying `result` event, ordered immediately before this one. `"cancelled" | "discarded" | "refused"` are the CLI's verdicts on a message that opened a turn and produced no result at all, and `"session_closed"` is a turn still open when the session went down. A host pairing `turn_start` with `result` stays busy forever on those four paths.
388
+ - `result` — Final result (`text`, `costUsd`, `isError`, `stopReason`, `terminalReason`, `numTurns`, `durationMs`). The outcome payload, not the close signal — see `turn_end`.
387
389
 
388
390
  Lifecycle events (via `onLifecycle`) report phases: `preparing`, `spawning`, `running`, `waiting_for_input`, `completed`, `cancelled`, `error`.
389
391
 
@@ -396,7 +398,7 @@ Verified live against `claude 2.1.116` and `codex-cli 0.122.0` (2026-04-21). ACP
396
398
  | `sessionId` | `session_id` (UUID, stable across turns + resume) | `thread_id` (UUIDv7, emitted once on `thread.started`, tracked across lines) |
397
399
  | `messageId` | `message.id` (Anthropic API message, e.g. `msg_*`) | v2 app-server: globally unique (`msg_*`, `rs_*`, `call_*`). NDJSON: `item_N` — **turn-local, not globally unique** |
398
400
  | `eventId` | Top-level per-line `uuid` | Synthetic where derivable (see below); else null |
399
- | `turnId` | **null** — Claude doesn't model turns | v2 app-server: native UUIDv7 from `params.turnId`. NDJSON: **null** — no turn id in legacy format |
401
+ | `turnId` | **null** on every event except `turn_start` / `turn_end`, which carry a session-local `turn-N` — Claude has no native turn id | v2 app-server: native UUIDv7 from `params.turnId`. NDJSON: **null** — no turn id in legacy format |
400
402
  | `parentToolCallId` | `parent_tool_use_id` (set for sub-agent messages) | **null** — not emitted |
401
403
  | Tool correlation | `tool_use.id` (`toolu_*`) ↔ `tool_result.tool_use_id`; the library stamps `tool_result.toolName` from the matching call | `item.id` reappears on the same item's `item.completed`; `toolName` set directly from the item type |
402
404
  | `tool_result.exitCode` | **null** (Claude doesn't expose shell exit codes) | `item.exit_code` for `command_execution` |
@@ -550,6 +552,15 @@ first-class `background_task` event. Its normalized contract is:
550
552
  description: string | null;
551
553
  summary: string | null;
552
554
  parentTaskId: string | null;
555
+ // The tool call that launched the task, when the provider reports it.
556
+ toolUseId: string | null;
557
+ // The task's delivered output — present only on the event that hands the
558
+ // result back, null on every state change. See below.
559
+ report: {
560
+ summary: string | null;
561
+ outputFile: string | null;
562
+ usage: { totalTokens: number | null; toolUses: number | null; durationMs: number | null } | null;
563
+ } | null;
553
564
  }
554
565
  ```
555
566
 
@@ -558,6 +569,15 @@ from the active set when `phase === "completed"`. Root turn state is a separate
558
569
  axis. A `result` may arrive while background tasks are still active, and a
559
570
  background task may complete while the root is still running.
560
571
 
572
+ A completion can arrive as more than one event. Claude emits both a state
573
+ patch (`task_updated`) and a result delivery (`task_notification`) for a single
574
+ finished task, and they are different records rather than duplicates: only the
575
+ delivery carries the summary and the output file. Render one row per completion
576
+ by keying on `report !== null` rather than on `phase`. A task that was stopped
577
+ or killed delivered nothing and carries no report, so it produces no row under
578
+ that rule — surface those from the terminal `status` if your UI needs to show
579
+ that a task was cut short.
580
+
561
581
  ```typescript
562
582
  createSession({
563
583
  onEvent(event) {
package/dist/index.d.ts CHANGED
@@ -35,7 +35,7 @@ export { codexLineToStreamEvents } from "./providers/codex/transcript-normalize.
35
35
  export type { GetCodexTranscriptPathOptions, CodexTranscriptLocation, ReadCodexTranscriptOptions, CodexTranscriptYield, CodexTranscriptLine, CodexPeekResult, } from "./providers/codex/transcript.js";
36
36
  export { installSkills, removeSkills, listInstalledSkills, resolveSkillsHome, resolveSkillsWorkspace, resolveNativeSkillsHome, resolveNativeSkillsWorkspace, ensureSkillSymlink, } from "./utils/skills.js";
37
37
  export { commandInventoryFromEvent, discoverSkillCommands, reconcileSkillCommands, formatSlashInvocation, invokeSkill, buildExpandedSkillPrompt, } from "./utils/skill-commands.js";
38
- export type { ProviderModule, ProviderCapabilities, ProviderRuntimeContext, ProviderRuntimeReport, CapabilityStatus, AgentMode, ListModesOptions, ExecutionContext, ExecutionResult, ExecutionStatus, ProviderConfig, ProviderEndpointConfig, McpServerConfig, StreamEvent, SessionCodec, SessionState, TokenUsage, ModelUsage, RateLimitInfo, BaseStreamEventFields, LifecycleEvent, QuotaStatus, QuotaContext, AuthMethod, AuthSource, AuthOption, AuthReport, AuthResolveContext, AuthIdentity, AuthRequiredReason, BackgroundTaskType, BackgroundTaskPhase, BackgroundTaskStatus, BinaryStatus, ProviderModel, ListModelsOptions, ProviderAuthMethod, ProviderAuthFlow, UpstreamProvider, UpstreamProviderManager, SessionContext, AgentSession, SendHandle, SendOptions, CancelResult, StopTaskResult, TurnResult, UserInputRequest, UserInputResponse, ElicitationRequest, ElicitationResponse, HookCallbackRequest, HookCallbackResponse, TranscriptOps, TranscriptYield, TranscriptPeek, FoundTranscript, GoalStatus, GoalBlockedReason, GoalSource, GoalCapability, GoalState, GoalOptions, SetGoalResult, ClearGoalResult, GoalSentinel, GoalSentinelVerdict, GoalSentinelContext, } from "./types.js";
38
+ export type { ProviderModule, ProviderCapabilities, ProviderRuntimeContext, ProviderRuntimeReport, CapabilityStatus, AgentMode, ListModesOptions, ExecutionContext, ExecutionResult, ExecutionStatus, ProviderConfig, ProviderEndpointConfig, McpServerConfig, StreamEvent, SessionCodec, SessionState, TokenUsage, ModelUsage, RateLimitInfo, BaseStreamEventFields, LifecycleEvent, QuotaStatus, QuotaContext, AuthMethod, AuthSource, AuthOption, AuthReport, AuthResolveContext, AuthIdentity, AuthRequiredReason, BackgroundTaskType, BackgroundTaskPhase, BackgroundTaskStatus, BackgroundTaskReport, TurnTrigger, BinaryStatus, ProviderModel, ListModelsOptions, ProviderAuthMethod, ProviderAuthFlow, UpstreamProvider, UpstreamProviderManager, SessionContext, AgentSession, SendHandle, SendOptions, CancelResult, StopTaskResult, TurnResult, UserInputRequest, UserInputResponse, ElicitationRequest, ElicitationResponse, HookCallbackRequest, HookCallbackResponse, TranscriptOps, TranscriptYield, TranscriptPeek, FoundTranscript, GoalStatus, GoalBlockedReason, GoalSource, GoalCapability, GoalState, GoalOptions, SetGoalResult, ClearGoalResult, GoalSentinel, GoalSentinelVerdict, GoalSentinelContext, } from "./types.js";
39
39
  export type { SavedHistoryArchiveState, SavedHistoryDiscoverOptions, SavedHistoryEvent, SavedHistoryOps, SavedHistoryProbeOptions, SavedHistoryProbeResult, SavedHistoryReadOptions, SavedHistorySession, SavedHistoryUserEvent, SavedHistoryYield, LocalHistoryArchiveState, LocalHistoryDiscoverOptions, LocalHistoryErrorCode, LocalHistoryEvent, LocalHistoryFingerprintOptions, LocalHistoryOps, LocalHistoryProbeOptions, LocalHistoryProbeResult, LocalHistoryReadOptions, LocalHistorySession, LocalHistorySourceFingerprint, LocalHistoryUserEvent, LocalHistoryYield, } from "./history/index.js";
40
40
  export { GoalController, EMULATED_GOAL_CAPABILITY, GOAL_OBJECTIVE_MAX, CODEX_GOAL_TOOLS, isTerminalGoalStatus, goalStateFromEvent, latestGoalFromEvents, normalizeClaudeGoalAttachment, normalizeCodexGoalStatus, normalizeCodexGoalRecord, createDefaultSentinel, parseAssessment, } from "./goals/index.js";
41
41
  export type { GoalControllerDeps, GoalStatusEvent, NormalizedGoalFields, } from "./goals/index.js";