@zq-silk/yui 0.6.8 → 0.6.10

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.
Files changed (58) hide show
  1. package/README.md +10 -3
  2. package/dist/cli/commandCatalog.js +1 -1
  3. package/dist/commands/taskCommands.js +63 -28
  4. package/dist/commands/taskContextCommand.js +27 -1
  5. package/dist/commands/taskRoleRuntimeStatus.js +17 -6
  6. package/dist/controller/agentRuntimeObserver.js +6 -3
  7. package/dist/controller/controller.js +29 -47
  8. package/dist/controller/fileSchedulerStoreAdapter.js +572 -258
  9. package/dist/controller/runtime.js +8 -1
  10. package/dist/controller/runtimeEventInbox.js +16 -5
  11. package/dist/controller/runtimeHookRunFence.js +51 -5
  12. package/dist/controller/runtimeObservationHook.js +8 -2
  13. package/dist/coordination/workMailbox.js +408 -28
  14. package/dist/coordination/workMailboxQueue.js +12 -10
  15. package/dist/executor/agentExecutor.js +101 -94
  16. package/dist/executor/executorRegistry.js +47 -2
  17. package/dist/executor/fileRoleLaunchPlanner.js +4 -2
  18. package/dist/lifecycle/exactRunTerminalization.js +1 -7
  19. package/dist/repository/taskWorkspaceCoordinator.js +9 -4
  20. package/dist/runtime/agentDriver.js +83 -4
  21. package/dist/runtime/agentDriverObservation.js +25 -10
  22. package/dist/runtime/builtinAgentDrivers.js +168 -18
  23. package/dist/runtime/codexAppServerRuntime.js +355 -0
  24. package/dist/runtime/continuationManager.js +117 -0
  25. package/dist/runtime/index.js +2 -0
  26. package/dist/runtime/lifecycleReservation.js +4 -3
  27. package/dist/runtime/promptEnvelope.js +14 -3
  28. package/dist/runtime/providerContinuation.js +225 -0
  29. package/dist/runtime/providerContinuationReconciliationService.js +172 -0
  30. package/dist/runtime/providerRuntimeIdentity.js +232 -0
  31. package/dist/runtime/providerRuntimeReconciler.js +166 -0
  32. package/dist/runtime/runtimeContinuationProjection.js +34 -0
  33. package/dist/runtime/runtimeObservation.js +217 -6
  34. package/dist/runtime/runtimeProjection.js +172 -11
  35. package/dist/scheduler/activeRoleRunDelivery.js +314 -1
  36. package/dist/scheduler/leaderWakeupProcessor.js +2 -1
  37. package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
  38. package/dist/scheduler/roleRunLiveness.js +8 -7
  39. package/dist/scheduler/roleRunStall.js +4 -2
  40. package/dist/scheduler/taskExecutionProjection.js +2 -2
  41. package/dist/storage/migration/productionRegistry.js +474 -1
  42. package/dist/storage/sqliteSchema.js +102 -21
  43. package/dist/storage/sqliteStore.js +52 -110
  44. package/dist/storage/storageVersions.js +1 -1
  45. package/dist/storage/storeRpc.js +0 -1
  46. package/dist/storage/taskStore.js +40 -53
  47. package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
  48. package/dist/task/nextAction.js +1 -1
  49. package/dist/web/assets/client/app.js +1 -1
  50. package/dist/web/assets/client/components.js +233 -4
  51. package/dist/web/assets/client/i18n.js +166 -2
  52. package/dist/web/assets/client/view.js +30 -13
  53. package/dist/web/assets/styles/cards.js +62 -0
  54. package/dist/web/assets/styles/widgets.js +1 -0
  55. package/dist/web/webSnapshot.js +11 -2
  56. package/package.json +1 -1
  57. package/skills/yui-leader/SKILL.md +33 -24
  58. package/skills/yui-operator/SKILL.md +7 -5
@@ -6,6 +6,10 @@ const KINDS = [
6
6
  "session.ready",
7
7
  "session.ended",
8
8
  "session.failed",
9
+ "conversation.observed",
10
+ "activation.started",
11
+ "activation.ended",
12
+ "activation.failed",
9
13
  "turn.accepted",
10
14
  "turn.waiting",
11
15
  "turn.completed",
@@ -14,6 +18,12 @@ const KINDS = [
14
18
  "operation.started",
15
19
  "operation.completed",
16
20
  "operation.failed",
21
+ "native-work.snapshot",
22
+ "continuation.started",
23
+ "continuation.reported",
24
+ "continuation.settled",
25
+ "input.accepted",
26
+ "input.delivery-unknown",
17
27
  "activity.observed",
18
28
  "observer.health"
19
29
  ];
@@ -37,6 +47,11 @@ const RUN_SCOPED = new Set([
37
47
  "activity.observed",
38
48
  "observer.health"
39
49
  ]);
50
+ const CONTINUATION_SCOPED = new Set([
51
+ "continuation.started",
52
+ "continuation.reported",
53
+ "continuation.settled"
54
+ ]);
40
55
  const PROVIDER_STATE = new Set([
41
56
  "session.started",
42
57
  "session.ready",
@@ -52,8 +67,8 @@ const PROVIDER_STATE = new Set([
52
67
  "operation.failed"
53
68
  ]);
54
69
  export function createRuntimeObservation(input) {
55
- if (input.schemaVersion !== 1)
56
- throw new Error("Runtime observation schemaVersion must be 1.");
70
+ if (input.schemaVersion !== 2)
71
+ throw new Error("Runtime observation schemaVersion must be 2.");
57
72
  if (!KINDS.includes(input.kind))
58
73
  throw new Error("Runtime observation kind is invalid.");
59
74
  if (!AUTHORITIES.includes(input.authority))
@@ -76,6 +91,22 @@ export function createRuntimeObservation(input) {
76
91
  if (RUN_SCOPED.has(input.kind) && fence.nativeTurnId === undefined) {
77
92
  throw new Error(`${input.kind} requires nativeTurnId.`);
78
93
  }
94
+ if ((input.kind.startsWith("activation.") || CONTINUATION_SCOPED.has(input.kind)
95
+ || input.kind === "native-work.snapshot")
96
+ && fence.activationId === undefined) {
97
+ throw new Error(`${input.kind} requires activationId.`);
98
+ }
99
+ if ((input.kind === "conversation.observed" || input.kind.startsWith("activation.")
100
+ || CONTINUATION_SCOPED.has(input.kind) || input.kind === "native-work.snapshot")
101
+ && fence.conversationId === undefined) {
102
+ throw new Error(`${input.kind} requires conversationId.`);
103
+ }
104
+ if (CONTINUATION_SCOPED.has(input.kind) && fence.continuationId === undefined) {
105
+ throw new Error(`${input.kind} requires continuationId.`);
106
+ }
107
+ if (CONTINUATION_SCOPED.has(input.kind) && fence.continuationGeneration === undefined) {
108
+ throw new Error(`${input.kind} requires continuationGeneration.`);
109
+ }
79
110
  const payload = normalizePayload(input.kind, input.payload);
80
111
  const sequence = input.sequence;
81
112
  if (sequence !== undefined && (!Number.isSafeInteger(sequence) || sequence < 0)) {
@@ -86,8 +117,9 @@ export function createRuntimeObservation(input) {
86
117
  throw new Error("Runtime observation ordinal must be a non-negative safe integer.");
87
118
  }
88
119
  return Object.freeze({
89
- schemaVersion: 1,
120
+ schemaVersion: 2,
90
121
  eventId: requireIdentity(input.eventId, "Runtime observation event id"),
122
+ semanticKey: requireIdentity(input.semanticKey, "Runtime observation semantic key"),
91
123
  kind: input.kind,
92
124
  authority: input.authority,
93
125
  receivedAt: requireTimestamp(input.receivedAt, "Runtime observation receivedAt"),
@@ -109,6 +141,11 @@ export function runtimeObservationFenceMatches(expected, actual) {
109
141
  "driverId",
110
142
  "launchId",
111
143
  "sessionGenerationId",
144
+ "conversationId",
145
+ "activationId",
146
+ "continuationId",
147
+ "continuationGeneration",
148
+ "parentContinuationId",
112
149
  "nativeSessionId",
113
150
  "nativeTurnId",
114
151
  "receiptId"
@@ -118,11 +155,35 @@ export function runtimeObservationFenceMatches(expected, actual) {
118
155
  }
119
156
  return true;
120
157
  }
158
+ /**
159
+ * Matches observations that belong to one durable Run/session generation.
160
+ * A provider may advance its native Turn while background subagents from an
161
+ * earlier Turn are still active and later mailbox activations use their own
162
+ * exactly-once receipt, so nativeTurnId and receiptId are intentionally
163
+ * excluded. Exact acceptance still validates both fields before persistence.
164
+ */
165
+ export function runtimeObservationRunFenceMatches(expected, actual) {
166
+ for (const field of [
167
+ "taskId",
168
+ "roleName",
169
+ "runId",
170
+ "agentId",
171
+ "driverId",
172
+ "launchId",
173
+ "sessionGenerationId",
174
+ "nativeSessionId"
175
+ ]) {
176
+ if (expected[field] !== actual[field])
177
+ return false;
178
+ }
179
+ return true;
180
+ }
121
181
  /** Compact TaskEvent payload used for durable state-boundary observations. */
122
182
  export function runtimeObservationTaskEventPayload(input) {
123
183
  const observation = createRuntimeObservation(input);
124
184
  return Object.freeze({
125
185
  eventId: observation.eventId,
186
+ semanticKey: observation.semanticKey,
126
187
  roleName: observation.fence.roleName,
127
188
  agentId: observation.fence.agentId,
128
189
  driverId: observation.fence.driverId,
@@ -165,7 +226,26 @@ function normalizeFence(input) {
165
226
  : { nativeTurnId: requireIdentity(input.nativeTurnId, "Native Turn id") }),
166
227
  ...(input.receiptId === undefined
167
228
  ? {}
168
- : { receiptId: requireIdentity(input.receiptId, "Receipt id") })
229
+ : { receiptId: requireIdentity(input.receiptId, "Receipt id") }),
230
+ ...(input.conversationId === undefined
231
+ ? {}
232
+ : { conversationId: requireIdentity(input.conversationId, "Provider Conversation id") }),
233
+ ...(input.activationId === undefined
234
+ ? {}
235
+ : { activationId: requireIdentity(input.activationId, "Provider Activation id") }),
236
+ ...(input.continuationId === undefined
237
+ ? {}
238
+ : { continuationId: requireIdentity(input.continuationId, "Provider Continuation id") }),
239
+ ...(input.continuationGeneration === undefined
240
+ ? {}
241
+ : {
242
+ continuationGeneration: requireNonNegativeInteger(input.continuationGeneration, 1, "Provider Continuation generation")
243
+ }),
244
+ ...(input.parentContinuationId === undefined
245
+ ? {}
246
+ : {
247
+ parentContinuationId: requireIdentity(input.parentContinuationId, "Parent Provider Continuation id")
248
+ })
169
249
  });
170
250
  }
171
251
  function normalizePayload(kind, input) {
@@ -208,6 +288,41 @@ function normalizePayload(kind, input) {
208
288
  if (kind === "turn.failed" && input.failure === undefined) {
209
289
  throw new Error("turn.failed requires normalized failure evidence.");
210
290
  }
291
+ if (kind === "conversation.observed"
292
+ && !["unknown", "recoverable", "unrecoverable"].includes(input.recoverability ?? "")) {
293
+ throw new Error("conversation.observed requires recoverability.");
294
+ }
295
+ if (kind === "native-work.snapshot") {
296
+ if (typeof input.snapshotComplete !== "boolean"
297
+ || !["exact", "partial", "unavailable"].includes(input.observationQuality ?? "")) {
298
+ throw new Error("native-work.snapshot requires completeness and observation quality.");
299
+ }
300
+ if (input.snapshotComplete && input.observationQuality !== "exact") {
301
+ throw new Error("A complete native-work.snapshot requires exact observation quality.");
302
+ }
303
+ }
304
+ if (kind.startsWith("continuation.")) {
305
+ if (!["active", "quiescent", "unknown"].includes(input.execution ?? "")) {
306
+ throw new Error(`${kind} requires continuation execution.`);
307
+ }
308
+ if (!["pending", "succeeded", "failed", "cancelled", "unknown"].includes(input.outcome ?? "")) {
309
+ throw new Error(`${kind} requires continuation outcome.`);
310
+ }
311
+ if (input.attachment !== "attached" && input.attachment !== "detached") {
312
+ throw new Error(`${kind} requires continuation attachment.`);
313
+ }
314
+ if (!["exact", "partial", "unavailable"].includes(input.observationQuality ?? "")) {
315
+ throw new Error(`${kind} requires continuation observation quality.`);
316
+ }
317
+ if (typeof input.mayWriteWorkspace !== "boolean") {
318
+ throw new Error(`${kind} requires mayWriteWorkspace.`);
319
+ }
320
+ if (kind === "continuation.reported")
321
+ requireIdentity(input.reportId, "Provider report id");
322
+ if (kind === "continuation.settled" && input.observationQuality !== "exact") {
323
+ throw new Error("continuation.settled requires exact observation quality.");
324
+ }
325
+ }
211
326
  const failure = input.failure === undefined
212
327
  ? undefined
213
328
  : normalizeFailure(input.failure);
@@ -233,9 +348,92 @@ function normalizePayload(kind, input) {
233
348
  ? {}
234
349
  : { observerDetail: requireText(input.observerDetail, "Runtime observer detail") }),
235
350
  ...(failure === undefined ? {} : { failure }),
236
- ...(input.summary === undefined ? {} : { summary: requireText(input.summary, "Runtime summary") })
351
+ ...(input.summary === undefined ? {} : { summary: requireText(input.summary, "Runtime summary") }),
352
+ ...(input.execution === undefined ? {} : { execution: input.execution }),
353
+ ...(input.outcome === undefined ? {} : { outcome: input.outcome }),
354
+ ...(input.attachment === undefined ? {} : { attachment: input.attachment }),
355
+ ...(input.observationQuality === undefined
356
+ ? {}
357
+ : { observationQuality: input.observationQuality }),
358
+ ...(input.mayWriteWorkspace === undefined
359
+ ? {}
360
+ : { mayWriteWorkspace: input.mayWriteWorkspace }),
361
+ ...(input.resultRef === undefined
362
+ ? {}
363
+ : { resultRef: requireIdentity(input.resultRef, "Provider result ref") }),
364
+ ...(input.reportId === undefined
365
+ ? {}
366
+ : { reportId: requireIdentity(input.reportId, "Provider report id") }),
367
+ ...(input.providerDeliveryRef === undefined
368
+ ? {}
369
+ : {
370
+ providerDeliveryRef: requireIdentity(input.providerDeliveryRef, "Provider delivery ref")
371
+ }),
372
+ ...(input.snapshotComplete === undefined
373
+ ? {}
374
+ : { snapshotComplete: input.snapshotComplete }),
375
+ ...(input.recoverability === undefined
376
+ ? {}
377
+ : { recoverability: input.recoverability })
237
378
  });
238
379
  }
380
+ export function runtimeObservationSemanticKey(input) {
381
+ const fence = input.fence;
382
+ const continuationIdentity = [
383
+ fence.driverId,
384
+ fence.agentId,
385
+ fence.conversationId ?? fence.nativeSessionId ?? fence.launchId,
386
+ fence.activationId ?? fence.launchId,
387
+ fence.continuationId ?? "none",
388
+ fence.continuationGeneration ?? "none"
389
+ ];
390
+ // Terminal boundaries are semantic facts. Providers may replay them with a
391
+ // fresh transport sequence after reconnect, so terminal identity must win
392
+ // over occurrence ordering or one SessionEnd storm becomes many facts.
393
+ if (["session.ended", "session.failed", "activation.ended", "activation.failed",
394
+ "turn.completed", "turn.failed", "turn.cancelled", "continuation.settled"].includes(input.kind)) {
395
+ return [
396
+ "terminal",
397
+ ...continuationIdentity,
398
+ fence.continuationId ?? fence.nativeTurnId ?? "none",
399
+ input.kind,
400
+ input.payload?.outcome ?? input.payload?.failure?.code ?? "terminal",
401
+ input.kind === "continuation.settled" ? input.payload?.resultRef ?? "none" : "none",
402
+ input.kind === "turn.failed" && input.payload?.failure?.runTerminal === true
403
+ ? "run-terminal"
404
+ : "turn-terminal"
405
+ ].join(":");
406
+ }
407
+ if (input.kind === "continuation.reported") {
408
+ return ["continuation-report", ...continuationIdentity, input.payload?.reportId ?? "missing"]
409
+ .join(":");
410
+ }
411
+ if (input.kind === "continuation.started") {
412
+ return [
413
+ "continuation-state",
414
+ ...continuationIdentity,
415
+ input.payload?.execution ?? "unknown",
416
+ input.payload?.attachment ?? "unknown",
417
+ input.payload?.observationQuality ?? "unknown",
418
+ input.payload?.mayWriteWorkspace === true ? "writer" : "read-only",
419
+ input.payload?.outcome ?? "unknown",
420
+ input.payload?.resultRef ?? "none"
421
+ ].join(":");
422
+ }
423
+ if (input.sequence !== undefined) {
424
+ return [
425
+ "provider-sequence",
426
+ fence.driverId,
427
+ fence.conversationId ?? fence.nativeSessionId ?? fence.launchId,
428
+ fence.activationId ?? fence.launchId,
429
+ fence.continuationId ?? fence.nativeTurnId ?? "none",
430
+ fence.continuationGeneration ?? "none",
431
+ input.sequence,
432
+ input.kind
433
+ ].join(":");
434
+ }
435
+ return `provider-event:${requireIdentity(input.eventId, "Runtime observation event id")}`;
436
+ }
239
437
  function normalizeObserverSource(input) {
240
438
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
241
439
  throw new Error("Runtime observer source must be an object.");
@@ -261,9 +459,17 @@ function normalizeFailure(input) {
261
459
  : { details: requireText(input.details, "Runtime failure details") }),
262
460
  ...(input.lastOutput === undefined
263
461
  ? {}
264
- : { lastOutput: requireText(input.lastOutput, "Runtime failure last output") })
462
+ : { lastOutput: requireText(input.lastOutput, "Runtime failure last output") }),
463
+ ...(input.runTerminal === undefined
464
+ ? {}
465
+ : { runTerminal: requireBoolean(input.runTerminal, "Runtime failure runTerminal") })
265
466
  });
266
467
  }
468
+ function requireBoolean(value, label) {
469
+ if (typeof value !== "boolean")
470
+ throw new Error(`${label} must be boolean.`);
471
+ return value;
472
+ }
267
473
  function validateUsage(input) {
268
474
  for (const [name, value] of Object.entries(input)) {
269
475
  if (!Number.isSafeInteger(value) || value < 0) {
@@ -280,6 +486,11 @@ function requireTimestamp(value, label) {
280
486
  throw new Error(`${label} is invalid.`);
281
487
  return timestamp;
282
488
  }
489
+ function requireNonNegativeInteger(value, minimum, label) {
490
+ if (!Number.isSafeInteger(value) || value < minimum)
491
+ throw new Error(`${label} is invalid.`);
492
+ return value;
493
+ }
283
494
  function requireIdentity(value, label) {
284
495
  const identity = requireText(value, label);
285
496
  if (identity.includes("/../") || identity === "__proto__")
@@ -1,4 +1,4 @@
1
- import { createRuntimeObservation, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
1
+ import { createRuntimeObservation, runtimeObservationFromTaskEvent, runtimeObservationRunFenceMatches } from "./runtimeObservation.js";
2
2
  export function createRuntimeProjection(fence, createdAt) {
3
3
  const timestamp = requireTimestamp(createdAt);
4
4
  return Object.freeze({
@@ -6,6 +6,14 @@ export function createRuntimeProjection(fence, createdAt) {
6
6
  host: "unknown",
7
7
  session: "unknown",
8
8
  turn: "none",
9
+ conversation: "unknown",
10
+ activation: "none",
11
+ continuations: Object.freeze({}),
12
+ inputDelivery: "none",
13
+ runActivity: "starting",
14
+ health: "reconciling",
15
+ waitingOn: Object.freeze([]),
16
+ attention: Object.freeze([]),
9
17
  operations: Object.freeze({}),
10
18
  activity: Object.freeze({ kind: "none" }),
11
19
  observer: Object.freeze({ status: "unknown" }),
@@ -20,7 +28,7 @@ export function createRuntimeProjection(fence, createdAt) {
20
28
  export function projectRuntimeTaskEvents(fence, createdAt, events) {
21
29
  const observations = events
22
30
  .map(runtimeObservationFromTaskEvent)
23
- .filter((event) => (event !== null && runtimeObservationFenceMatches(fence, event.fence)))
31
+ .filter((event) => (event !== null && runtimeObservationRunFenceMatches(fence, event.fence)))
24
32
  .sort((left, right) => (left.receivedAt.localeCompare(right.receivedAt)
25
33
  || (left.sequence ?? -1) - (right.sequence ?? -1)
26
34
  || (left.ordinal ?? -1) - (right.ordinal ?? -1)
@@ -29,7 +37,7 @@ export function projectRuntimeTaskEvents(fence, createdAt, events) {
29
37
  }
30
38
  export function projectRuntimeObservation(current, raw) {
31
39
  const event = createRuntimeObservation(raw);
32
- if (!runtimeObservationFenceMatches(current.fence, event.fence)) {
40
+ if (!runtimeObservationRunFenceMatches(current.fence, event.fence)) {
33
41
  throw new Error("Runtime observation fence does not match the projection.");
34
42
  }
35
43
  const at = event.receivedAt;
@@ -40,24 +48,36 @@ export function projectRuntimeObservation(current, raw) {
40
48
  stateSince: at
41
49
  });
42
50
  case "session.started":
43
- return next(current, { session: "started", stateSince: at });
51
+ return next(current, {
52
+ session: "started",
53
+ conversation: "recoverable",
54
+ activation: "active",
55
+ stateSince: at
56
+ });
44
57
  case "session.ready":
45
58
  return next(current, { session: "ready", stateSince: at });
46
59
  case "session.ended":
47
60
  return next(current, {
48
61
  session: "ended",
62
+ activation: "ended",
49
63
  turn: terminalTurn(current.turn),
50
- operations: Object.freeze({}),
64
+ // A parent provider Session ending is independent from native child
65
+ // operations already observed under the Run. Keep those children
66
+ // visible until their own terminal facts arrive; host/session loss is
67
+ // health evidence, not proof that child work or the Yui Run ended.
68
+ operations: activeSubagentOperations(current.operations),
51
69
  stateSince: at
52
70
  });
53
71
  case "session.failed":
54
72
  return next(current, {
55
73
  session: "failed",
74
+ activation: "failed",
56
75
  turn: current.turn === "none" ? "failed" : terminalTurn(current.turn, "failed"),
57
- operations: Object.freeze({}),
76
+ operations: activeSubagentOperations(current.operations),
58
77
  stateSince: at
59
78
  });
60
79
  case "turn.accepted":
80
+ case "input.accepted":
61
81
  return withActivity(next(current, {
62
82
  session: "active",
63
83
  turn: "accepted",
@@ -79,7 +99,10 @@ export function projectRuntimeObservation(current, raw) {
79
99
  turn: "completed",
80
100
  waitingReason: undefined,
81
101
  waitId: undefined,
82
- operations: Object.freeze({}),
102
+ // Tool operations belong to the completed native Turn. Provider-owned
103
+ // background subagents may legitimately outlive it and wake later
104
+ // native Turns inside the same durable Yui Run.
105
+ operations: activeSubagentOperations(current.operations),
83
106
  stateSince: at
84
107
  }), "provider", at);
85
108
  case "turn.failed":
@@ -104,7 +127,9 @@ export function projectRuntimeObservation(current, raw) {
104
127
  const operationId = event.payload.operationId;
105
128
  const operation = event.payload.operation;
106
129
  return withActivity(next(current, {
107
- session: "active",
130
+ session: current.session === "ended" || current.session === "failed"
131
+ ? current.session
132
+ : "active",
108
133
  turn: current.turn === "waiting" ? "accepted" : current.turn,
109
134
  waitingReason: undefined,
110
135
  waitId: undefined,
@@ -120,7 +145,9 @@ export function projectRuntimeObservation(current, raw) {
120
145
  const operations = { ...current.operations };
121
146
  delete operations[event.payload.operationId];
122
147
  return withActivity(next(current, {
123
- session: "active",
148
+ session: current.session === "ended" || current.session === "failed"
149
+ ? current.session
150
+ : "active",
124
151
  turn: current.turn === "waiting" ? "accepted" : current.turn,
125
152
  waitingReason: undefined,
126
153
  waitId: undefined,
@@ -151,6 +178,82 @@ export function projectRuntimeObservation(current, raw) {
151
178
  : { detail: event.payload.observerDetail })
152
179
  })
153
180
  });
181
+ case "conversation.observed":
182
+ return next(current, {
183
+ conversation: event.payload.recoverability === "unrecoverable"
184
+ ? "unrecoverable"
185
+ : event.payload.recoverability === "recoverable" ? "recoverable" : "unknown"
186
+ });
187
+ case "activation.started":
188
+ return next(current, { activation: "active", stateSince: at });
189
+ case "activation.ended":
190
+ return next(current, { activation: "ended", stateSince: at });
191
+ case "activation.failed":
192
+ return next(current, { activation: "failed", stateSince: at });
193
+ case "continuation.started":
194
+ case "continuation.reported":
195
+ case "continuation.settled": {
196
+ const id = [
197
+ event.fence.activationId,
198
+ event.fence.continuationId,
199
+ event.fence.continuationGeneration
200
+ ].join("/");
201
+ const existing = current.continuations[id];
202
+ if (event.kind === "continuation.reported") {
203
+ // A report is an attachment, not lifecycle evidence. When it races
204
+ // ahead of continuation.started, retain a conservative writer-owned
205
+ // stub until structured start/settlement metadata arrives.
206
+ const reported = existing ?? Object.freeze({
207
+ execution: "unknown",
208
+ outcome: "pending",
209
+ attachment: event.payload.attachment ?? "detached",
210
+ observation: "unavailable",
211
+ mayWriteWorkspace: true,
212
+ identityConflict: false
213
+ });
214
+ return next(current, {
215
+ continuations: Object.freeze({
216
+ ...current.continuations,
217
+ [id]: Object.freeze({
218
+ ...reported,
219
+ ...(event.payload.resultRef === undefined
220
+ ? {}
221
+ : { resultRef: event.payload.resultRef })
222
+ })
223
+ }),
224
+ stateSince: at
225
+ });
226
+ }
227
+ const settled = existing?.execution === "quiescent" && existing.observation === "exact";
228
+ const conflicts = settled && (event.payload.execution !== "quiescent"
229
+ || event.payload.outcome !== existing.outcome);
230
+ return next(current, {
231
+ continuations: Object.freeze({
232
+ ...current.continuations,
233
+ [id]: Object.freeze(conflicts
234
+ ? { ...existing, identityConflict: true }
235
+ : {
236
+ execution: event.payload.execution,
237
+ outcome: event.payload.outcome,
238
+ attachment: event.payload.attachment,
239
+ observation: event.payload.observationQuality,
240
+ mayWriteWorkspace: event.payload.observationQuality === "exact"
241
+ ? event.payload.mayWriteWorkspace
242
+ : (existing?.mayWriteWorkspace ?? false)
243
+ || event.payload.mayWriteWorkspace,
244
+ identityConflict: existing?.identityConflict ?? false,
245
+ ...(event.payload.resultRef === undefined
246
+ ? existing?.resultRef === undefined ? {} : { resultRef: existing.resultRef }
247
+ : { resultRef: event.payload.resultRef })
248
+ })
249
+ }),
250
+ stateSince: at
251
+ });
252
+ }
253
+ case "input.delivery-unknown":
254
+ return next(current, { inputDelivery: "delivery-unknown", stateSince: at });
255
+ case "native-work.snapshot":
256
+ return next(current, {});
154
257
  default:
155
258
  return current;
156
259
  }
@@ -173,12 +276,27 @@ export function completeRuntimeWorkflow(current, completedAt) {
173
276
  })
174
277
  });
175
278
  }
279
+ export function projectRuntimeMailbox(current, mailbox) {
280
+ if (mailbox?.inputDelivery === undefined || mailbox.inputDelivery === null) {
281
+ return next(current, { inputDelivery: "none" });
282
+ }
283
+ return next(current, {
284
+ inputDelivery: mailbox.inputDelivery.status === "delivery-unknown"
285
+ ? "delivery-unknown"
286
+ : "dispatching"
287
+ });
288
+ }
176
289
  export function runtimeDisplayStatus(current) {
177
290
  if (current.session === "failed")
178
291
  return "broken";
292
+ const operation = dominantOperation(current.operations);
293
+ // Provider-owned child work can outlive the parent Session. Surface the
294
+ // child as active while retaining host/session health orthogonally in the
295
+ // projection instead of collapsing both facts into "stopped".
296
+ if (operation === "subagent")
297
+ return "subagent-active";
179
298
  if (current.host === "exited" || current.session === "ended")
180
299
  return "stopped";
181
- const operation = dominantOperation(current.operations);
182
300
  if (operation !== null)
183
301
  return `${operation}-active`;
184
302
  if (current.turn === "waiting")
@@ -236,7 +354,47 @@ function next(current, patch) {
236
354
  }
237
355
  if (patch.waitId === undefined && Object.hasOwn(patch, "waitId"))
238
356
  delete copy.waitId;
239
- return Object.freeze(copy);
357
+ const continuations = Object.entries(copy.continuations);
358
+ const waitingNative = continuations.filter(([, continuation]) => (continuation.execution === "active" || continuation.execution === "unknown"));
359
+ const activeNativeOperations = Object.entries(copy.operations).filter(([, operation]) => (operation.kind === "subagent"));
360
+ const waitingOn = [
361
+ ...waitingNative.map(([id]) => `native:${id}`),
362
+ ...activeNativeOperations.map(([id]) => `native-operation:${id}`)
363
+ ];
364
+ const attention = [
365
+ ...(copy.inputDelivery === "delivery-unknown" ? ["delivery-unknown"] : []),
366
+ ...continuations.flatMap(([id, continuation]) => (continuation.identityConflict ? [`identity-conflict:${id}`]
367
+ : continuation.observation === "unavailable" ? [`continuation-unresolved:${id}`]
368
+ : [])),
369
+ ...(copy.conversation === "unrecoverable" ? ["leader-decision-needed"] : []),
370
+ ...(copy.host === "exited" || copy.session === "ended" || copy.session === "failed"
371
+ ? ["runtime-unobservable"]
372
+ : [])
373
+ ];
374
+ const health = copy.observer.status === "unavailable"
375
+ || waitingNative.some(([, continuation]) => continuation.observation === "unavailable")
376
+ || copy.host === "exited"
377
+ || copy.session === "ended"
378
+ || copy.session === "failed"
379
+ ? "unobservable"
380
+ : copy.inputDelivery === "dispatching"
381
+ || (copy.activation !== "active" && waitingNative.length > 0)
382
+ ? "reconciling"
383
+ : "healthy";
384
+ const runActivity = copy.inputDelivery === "dispatching"
385
+ || copy.turn === "accepted"
386
+ || copy.session === "active"
387
+ ? "running"
388
+ : waitingNative.length > 0 || activeNativeOperations.length > 0 ? "waiting"
389
+ : copy.session === "unknown" || copy.session === "started" ? "starting"
390
+ : "parked";
391
+ return Object.freeze({
392
+ ...copy,
393
+ waitingOn: Object.freeze(waitingOn),
394
+ attention: Object.freeze([...new Set(attention)]),
395
+ health,
396
+ runActivity
397
+ });
240
398
  }
241
399
  function usageAdvanced(previous, current) {
242
400
  // A first cumulative snapshot may contain history from a resumed native
@@ -261,6 +419,9 @@ function dominantOperation(operations) {
261
419
  return "model";
262
420
  return null;
263
421
  }
422
+ function activeSubagentOperations(operations) {
423
+ return Object.freeze(Object.fromEntries(Object.entries(operations).filter(([, operation]) => operation.kind === "subagent")));
424
+ }
264
425
  function terminalTurn(current, fallback = "cancelled") {
265
426
  return current === "completed" || current === "failed" || current === "cancelled"
266
427
  ? current