@zq-silk/yui 0.16.1 → 0.16.2

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 (114) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +3 -0
  4. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  5. package/dist/agentRun/agentRun.js +7 -17
  6. package/dist/cli/agentConfigurationPicker.js +24 -21
  7. package/dist/cli/commandCatalog.js +18 -10
  8. package/dist/cli/roleWizard.js +41 -30
  9. package/dist/cli/updateCommand.js +35 -13
  10. package/dist/cli/updateOrchestrator.js +73 -33
  11. package/dist/cli/updatePorts.js +40 -17
  12. package/dist/cli.js +35 -26
  13. package/dist/commands/controllerCommands.js +1 -1
  14. package/dist/commands/executionAuditCommands.js +2 -2
  15. package/dist/commands/projectCommands.js +1 -1
  16. package/dist/commands/releaseCommands.js +9 -38
  17. package/dist/commands/taskCommands.js +84 -81
  18. package/dist/commands/taskCompletionGate.js +0 -117
  19. package/dist/commands/taskRoleRuntimeStatus.js +1 -1
  20. package/dist/commands/taskUpstreamCommands.js +93 -54
  21. package/dist/context/runContextPack.js +30 -54
  22. package/dist/context/runInputContract.js +9 -0
  23. package/dist/context/sessionBootstrapManifest.js +11 -19
  24. package/dist/controller/agentHostObservation.js +4 -2
  25. package/dist/controller/controller.js +8 -6
  26. package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
  27. package/dist/controller/globalInputDelivery.js +28 -7
  28. package/dist/controller/globalRuntimeAttention.js +34 -0
  29. package/dist/controller/operatorNotification.js +31 -0
  30. package/dist/controller/providerRetryAdmission.js +3 -1
  31. package/dist/controller/providerRetryDelivery.js +10 -3
  32. package/dist/controller/runtime.js +1 -27
  33. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  34. package/dist/controller/structuredProviderObservation.js +27 -0
  35. package/dist/controller/updateReconciliation.js +72 -20
  36. package/dist/doctor/doctor.js +2 -1
  37. package/dist/executor/agentAdapter.js +28 -96
  38. package/dist/executor/agentConfigurationCatalog.js +13 -68
  39. package/dist/executor/agentConfigurationFields.js +120 -0
  40. package/dist/executor/agentConfigurationProbe.js +55 -80
  41. package/dist/executor/agentExecutor.js +5 -2
  42. package/dist/executor/executorRegistry.js +0 -9
  43. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  44. package/dist/interaction/operatorPresentation.js +2 -1
  45. package/dist/message/messageContinuation.js +8 -9
  46. package/dist/observability/executionAudit.js +1 -10
  47. package/dist/observability/runtimeIdentity.js +0 -23
  48. package/dist/output/agentConfigurationPresentation.js +4 -2
  49. package/dist/release/releaseHandover.js +2 -2
  50. package/dist/release/releaseIdempotencyStore.js +0 -23
  51. package/dist/release/releaseWorkflowPorts.js +8 -4
  52. package/dist/repository/gitWorkspace.js +66 -19
  53. package/dist/repository/taskWorkspacePreparer.js +75 -68
  54. package/dist/resources/resourceRegistry.js +90 -44
  55. package/dist/resources/sqliteResourceRegistry.js +3 -5
  56. package/dist/runtime/acpSession.js +21 -5
  57. package/dist/runtime/agentEndpoint.js +4 -1
  58. package/dist/runtime/agentHost.js +22 -62
  59. package/dist/runtime/agentHostCleanup.js +85 -0
  60. package/dist/runtime/agentHostProtocol.js +1 -1
  61. package/dist/runtime/builtinAgentDrivers.js +46 -119
  62. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  63. package/dist/runtime/codexAppServerRuntime.js +55 -89
  64. package/dist/runtime/jsonLineChannel.js +35 -7
  65. package/dist/runtime/providerContinuationReconciliationService.js +42 -3
  66. package/dist/runtime/providerRetry.js +15 -0
  67. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  68. package/dist/runtime/structuredProviderHost.js +106 -81
  69. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  70. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  71. package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
  72. package/dist/task/nextAction.js +33 -23
  73. package/dist/task/task.js +2 -1
  74. package/dist/web/assets/assetManifest.js +2 -0
  75. package/dist/web/assets/client/app.js +72 -3
  76. package/dist/web/assets/client/components.js +2 -1
  77. package/dist/web/assets/client/i18n.js +2 -0
  78. package/dist/web/assets/client/taskSummary.js +345 -0
  79. package/dist/web/assets/client/taskSurface.js +44 -26
  80. package/dist/web/assets/client/view.js +33 -0
  81. package/dist/web/assets/shell.js +1 -0
  82. package/dist/web/assets/styles/cards.js +21 -0
  83. package/dist/web/webServer.js +39 -5
  84. package/dist/web/webSessions.js +165 -0
  85. package/dist/web/webSnapshot.js +23 -0
  86. package/dist/web/webTaskSurface.js +23 -0
  87. package/docs/managed-turn-and-session-runtime.md +31 -0
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
  89. package/docs/observability/README.md +47 -0
  90. package/docs/observability/README.zh-CN.md +37 -0
  91. package/docs/project-refresh.md +9 -0
  92. package/docs/project-refresh.zh-CN.md +8 -0
  93. package/docs/provider-retry.md +35 -0
  94. package/docs/release-workflow.md +124 -20
  95. package/docs/release-workflow.zh-CN.md +103 -14
  96. package/docs/roles-and-configuration.md +30 -0
  97. package/docs/roles-and-configuration.zh-CN.md +20 -0
  98. package/docs/task-delivery.md +109 -0
  99. package/docs/task-delivery.zh-CN.md +80 -0
  100. package/docs/task-discovery.md +9 -0
  101. package/docs/task-discovery.zh-CN.md +6 -0
  102. package/docs/testing/verification-levels.md +29 -11
  103. package/docs/testing/verification-levels.zh-CN.md +25 -7
  104. package/i18n/README.zh-CN.md +2 -0
  105. package/package.json +1 -1
  106. package/skills/yui-leader/SKILL.md +11 -0
  107. package/skills/yui-leader/references/execution.md +22 -0
  108. package/skills/yui-leader/references/planning.md +6 -2
  109. package/skills/yui-operator/SKILL.md +45 -13
  110. package/skills/yui-operator/references/task-delivery.md +99 -0
  111. package/skills/yui-runtime/SKILL.md +5 -3
  112. package/skills/yui-runtime/references/publication.md +56 -5
  113. package/skills/yui-runtime/references/recovery.md +12 -2
  114. package/dist/runtime/agentHostCompatibility.js +0 -127
@@ -5,16 +5,16 @@ import { homedir, tmpdir } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { createInterface } from "node:readline";
7
7
  import { withSessionContextPointer } from "../context/sessionBootstrapManifest.js";
8
- import { publishStructuredConversationRecoverability, publishStructuredProviderAccepted, publishStructuredProviderActivity, publishStructuredProviderAttachmentExit, publishStructuredProviderConnection, publishStructuredProviderGoal, publishStructuredProviderInputObserved, publishStructuredProviderInputSettlement, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
8
+ import { publishStructuredConversationRecoverability, publishStructuredProviderAccepted, publishStructuredProviderActivity, publishStructuredProviderAttachmentExit, publishStructuredProviderConnection, publishStructuredProviderDiagnostic, publishStructuredProviderGoal, publishStructuredProviderInputObserved, publishStructuredProviderInputSettlement, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
9
9
  import { callController, controllerCallMayHaveApplied, ControllerClientError } from "../core/controllerClient.js";
10
10
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
11
- import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
12
11
  import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
13
12
  import { yuiTmuxServerName, yuiTmuxSessionName } from "../tmux/tmuxManager.js";
14
13
  import { tmuxSocketDirectory } from "../tmux/tmuxSocketEndpoint.js";
15
14
  import { createAgentEndpointOwner } from "./agentEndpointOwnership.js";
16
15
  import { providerDeliveryFailure, providerDeliveryFailureFrom, redactAgentErrorText, serializeAgentErrorRaw } from "./agentError.js";
17
- import { AGENT_HOST_CONTROL_PROTOCOL, AGENT_HOST_EVENT_PROTOCOL } from "./agentHostProtocol.js";
16
+ import { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
17
+ import { closeAgentHostEndpoints } from "./agentHostCleanup.js";
18
18
  import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
19
19
  import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
20
20
  import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
@@ -99,6 +99,12 @@ export async function runAgentHost(input) {
99
99
  let conversationRecoverability = "unknown";
100
100
  let authority;
101
101
  let hostStopRequested = false;
102
+ const reportDiagnostic = async (diagnostic) => {
103
+ const observedPayload = activeRunPayload ?? sessionPayload ?? payload;
104
+ await publishStructuredProviderDiagnostic({
105
+ home: input.home, environment: observedPayload.environment, diagnostic
106
+ });
107
+ };
102
108
  let snapshot = hostSnapshot("idle");
103
109
  let dispatchTail = Promise.resolve();
104
110
  let promptHuman = () => { };
@@ -361,6 +367,9 @@ export async function runAgentHost(input) {
361
367
  handleStarted(event.value, currentPayload);
362
368
  else if (event.type === "terminal")
363
369
  handleTerminal(event.value);
370
+ else if (event.type === "diagnostic")
371
+ void enqueueSerialized(() => reportDiagnostic(event.value))
372
+ .catch(error => process.stderr.write(`Provider diagnostic delivery failed: ${serializeAgentErrorRaw(error)}\n`));
364
373
  else if (event.type === "goal")
365
374
  handleGoal(event.value);
366
375
  else
@@ -617,6 +626,9 @@ export async function runAgentHost(input) {
617
626
  handleStarted(event.value, next);
618
627
  else if (event.type === "terminal")
619
628
  handleTerminal(event.value);
629
+ else if (event.type === "diagnostic")
630
+ void enqueueSerialized(() => reportDiagnostic(event.value))
631
+ .catch(error => process.stderr.write(`Provider diagnostic delivery failed: ${serializeAgentErrorRaw(error)}\n`));
620
632
  else if (event.type === "goal")
621
633
  handleGoal(event.value);
622
634
  else
@@ -1214,55 +1226,13 @@ export async function runAgentHost(input) {
1214
1226
  if (forceKillTimer !== undefined)
1215
1227
  clearTimeout(forceKillTimer);
1216
1228
  humanConsole?.close();
1217
- // Ask the client to exit, then hand back the Session's hold. The reference
1218
- // is only actually returned once the client proves it exited, so the bounded
1219
- // stop below waits on the real dependency instead of an assumed one.
1220
- session?.detach();
1221
- const adapterId = endpointAdapterId;
1222
- await endpointLease?.release();
1223
- endpointLease = undefined;
1224
- if (adapterId !== undefined) {
1225
- // Stop new acquisition and wait a bounded time for what is already
1226
- // running. A timeout reports the references and pending effects that are
1227
- // genuinely still in use; it never escalates to killing a Provider this
1228
- // Endpoint does not own, and it never reports quiescence it cannot prove.
1229
- const drain = await endpointOwner.stop(adapterId, ENDPOINT_DRAIN_TIMEOUT_MS);
1230
- if (!drain.quiescent) {
1231
- const detail = `Endpoint stop ${drain.timedOut ? "timed out" : "returned"} after ${drain.waitedMs}ms`
1232
- + ` (bound ${ENDPOINT_DRAIN_TIMEOUT_MS}ms); ${drain.references} reference(s), `
1233
- + `${drain.opening} opening client(s) and `
1234
- + `${drain.sessions.reduce((total, held) => total + held.pending.length, 0)} pending effect(s) `
1235
- + "may still be in use. Owned client resources are unknown.";
1236
- updateSnapshot(hostSnapshot(snapshot.state, {
1237
- ...definedFields({
1238
- adapterId,
1239
- nativeSessionId: snapshot.nativeSessionId,
1240
- conversationId: snapshot.conversationId
1241
- }),
1242
- ...authorityFields(),
1243
- detail
1244
- }));
1245
- // The snapshot is about to stop being reachable, and a stop this process
1246
- // could not prove quiescent must remain diagnosable afterwards. Written
1247
- // to the Host's own stream, which its pane and logs retain.
1248
- process.stderr.write(`${detail}\n`);
1249
- }
1250
- }
1251
- // Final cleanup is bounded on its own deadline. The bounded stop above may
1252
- // already have given up on a client that never proves exit, and waiting for
1253
- // that same drain again would leave the control socket open and this function
1254
- // never returning. Anything still held is reported, not silently released.
1255
- const remaining = await endpointOwner.close(ENDPOINT_DRAIN_TIMEOUT_MS).catch(() => []);
1256
- const held = remaining.filter((drain) => !drain.quiescent);
1257
- if (held.length > 0) {
1258
- const references = held.reduce((total, drain) => total + drain.references, 0);
1259
- const opening = held.reduce((total, drain) => total + drain.opening, 0);
1260
- process.stderr.write(`Endpoint cleanup returned with ${references} reference(s) and ${opening} opening client(s) `
1261
- + `still held across ${held.length} implementation(s) (bound ${ENDPOINT_DRAIN_TIMEOUT_MS}ms); `
1262
- + "those implementations stay detached and undisposed. Owned client resources are unknown.\n");
1263
- }
1264
- await control.close();
1265
- void sessionPayload;
1229
+ await closeAgentHostEndpoints({
1230
+ owner: endpointOwner, session, lease: endpointLease, adapterId: endpointAdapterId,
1231
+ attemptId: activeRunAttemptId ?? lastTerminal?.attemptId,
1232
+ nativeTurnId: activeNativeTurnId ?? lastTerminal?.nativeTurnId,
1233
+ timeoutMs: ENDPOINT_DRAIN_TIMEOUT_MS,
1234
+ report: reportDiagnostic, closeControl: () => control.close()
1235
+ });
1266
1236
  }
1267
1237
  }
1268
1238
  /** Host-restart-independent OS custody. No secret or launch payload is stored. */
@@ -1497,16 +1467,6 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
1497
1467
  ...result,
1498
1468
  snapshot: {
1499
1469
  ...result.snapshot,
1500
- compatibility: {
1501
- control: AGENT_HOST_CONTROL_PROTOCOL, events: AGENT_HOST_EVENT_PROTOCOL,
1502
- rpc: FILE_TASK_CONTROLLER_PROTOCOL_VERSION, storage: "controller-owned"
1503
- },
1504
- hostProcess: { pid: process.pid, startIdentity: readLinuxProcessIdentity(process.pid).startIdentity },
1505
- owner: {
1506
- scope: payload.environment.YUI_SESSION_SCOPE ?? "task",
1507
- ...(payload.environment.YUI_TASK_ID === undefined ? {} : { taskId: payload.environment.YUI_TASK_ID }),
1508
- roleName: payload.environment.YUI_ROLE ?? "unknown-role"
1509
- },
1510
1470
  ...(payload.environment.YUI_SESSION_SCOPE !== "task" ? {} : {
1511
1471
  eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
1512
1472
  })
@@ -0,0 +1,85 @@
1
+ import { providerDeliveryFailureFrom, serializeAgentErrorRaw } from "./agentError.js";
2
+ /** Finish the Host's owned cleanup steps even when one fails. Neither a failed
3
+ * inspection nor a timeout is proof that an implementation or client drained. */
4
+ export async function closeAgentHostEndpoints(input) {
5
+ const errors = [];
6
+ let lastObservedDrain;
7
+ const report = async (operation, error, drains) => {
8
+ const failure = providerDeliveryFailureFrom(error, {
9
+ phase: "host-stop", inputDisposition: "unknown",
10
+ detail: `Endpoint ${operation}: ${error instanceof Error ? error.message : String(error)}. Owned client resources are unknown.`
11
+ });
12
+ const raw = serializeAgentErrorRaw({
13
+ operation, error, adapterId: input.adapterId, implementation: input.lease?.implementation,
14
+ nativeSessionId: input.session?.nativeSessionId, processInstanceId: input.session?.processInstanceId,
15
+ attemptId: input.attemptId, timeoutMs: input.timeoutMs,
16
+ ...(drains === undefined ? { lastObservedDrain } : { drains }),
17
+ unknown: "Current resource quiescence and pending Provider effects; no input is replayed."
18
+ });
19
+ // The Host stream remains evidence even if its Inbox cannot be written.
20
+ process.stderr.write(`${raw}\n`);
21
+ if (input.session === undefined)
22
+ return;
23
+ try {
24
+ await input.report({
25
+ nativeSessionId: input.session.nativeSessionId,
26
+ ...(input.attemptId === undefined ? {} : { attemptId: input.attemptId }),
27
+ ...(input.nativeTurnId === undefined ? {} : { nativeTurnId: input.nativeTurnId }),
28
+ failure: { ...failure, raw }
29
+ });
30
+ }
31
+ catch (error) {
32
+ errors.push(error);
33
+ process.stderr.write(`Endpoint cleanup diagnostic delivery failed: ${serializeAgentErrorRaw(error)}\n`);
34
+ }
35
+ };
36
+ try {
37
+ try {
38
+ input.session?.detach();
39
+ }
40
+ catch (error) {
41
+ errors.push(error);
42
+ await report("detach failed", error);
43
+ }
44
+ try {
45
+ await input.lease?.release();
46
+ }
47
+ catch (error) {
48
+ errors.push(error);
49
+ await report("lease release failed", error);
50
+ }
51
+ if (input.adapterId !== undefined) {
52
+ try {
53
+ lastObservedDrain = await input.owner.stop(input.adapterId, input.timeoutMs);
54
+ if (!lastObservedDrain.quiescent) {
55
+ await report("stop incomplete", new Error("The bounded stop did not prove quiescence."), [lastObservedDrain]);
56
+ }
57
+ }
58
+ catch (error) {
59
+ errors.push(error);
60
+ await report("stop failed", error);
61
+ }
62
+ }
63
+ try {
64
+ const remaining = await input.owner.close(input.timeoutMs);
65
+ const held = remaining.filter(drain => !drain.quiescent);
66
+ if (held.length > 0)
67
+ await report("close incomplete", new Error("Endpoint references remain held."), held);
68
+ }
69
+ catch (error) {
70
+ errors.push(error);
71
+ await report("close failed", error);
72
+ }
73
+ }
74
+ finally {
75
+ try {
76
+ await input.closeControl();
77
+ }
78
+ catch (error) {
79
+ errors.push(error);
80
+ await report("control close failed", error);
81
+ }
82
+ }
83
+ if (errors.length > 0)
84
+ throw new AggregateError(errors, "Agent Host cleanup failed; resource effects remain unknown.");
85
+ }
@@ -1,5 +1,5 @@
1
1
  import { createSessionOwnerIdentity } from "./sessionOwnerIdentity.js";
2
- /** Independent of CLI/RPC and Home schema. Keep v1 readable while v1 Hosts live. */
2
+ /** Current Host event source contract, independent of Home storage access. */
3
3
  export const AGENT_HOST_EVENT_PROTOCOL = "yui-agent-host-events/v1";
4
4
  export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v5";
5
5
  export function readAgentHostObservationSource(value) {
@@ -93,7 +93,8 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
93
93
  }),
94
94
  runtime: Object.freeze({
95
95
  nativeSessionId: ({ payload }) => (optionalIdentityFrom(payload, ["session_id"])),
96
- nativeTurnId: ({ payload }) => (optionalIdentityFrom(payload, ["prompt_id", "turn_id"])),
96
+ nativeTurnId: ({ hookEventName, payload }) => (optionalIdentityFrom(payload, hookEventName === "MessageDisplay"
97
+ ? ["prompt_id", "turn_id"] : ["prompt_id"])),
97
98
  mapError: mapClaudeAgentError,
98
99
  mapHook: ({ hookEventName, payload, occurrenceId }) => (mapClaudeHook(hookEventName, payload, occurrenceId)),
99
100
  classifyHook: ({ hookEventName, payload }) => Object.freeze({
@@ -102,7 +103,7 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
102
103
  : {}),
103
104
  terminal: isTerminalHook(hookEventName),
104
105
  ...(hookEventName !== "SubagentStop" ? {} : {
105
- continuationId: subagentId(payload),
106
+ continuationId: firstIdentity(payload, ["agent_id"], "Claude subagent id"),
106
107
  })
107
108
  }),
108
109
  observer: Object.freeze({
@@ -295,16 +296,6 @@ export function builtinAgentDriverRegistry() {
295
296
  return registry;
296
297
  }
297
298
  function mapClaudeHook(name, payload, occurrenceId) {
298
- const lifecycle = mapClaudeLifecycleHook(name, payload, occurrenceId);
299
- const goal = claudeGoalObservation(payload, occurrenceId);
300
- if (goal === undefined)
301
- return lifecycle;
302
- return Object.freeze([
303
- ...(Array.isArray(lifecycle) ? lifecycle : [lifecycle]),
304
- goal
305
- ]);
306
- }
307
- function mapClaudeLifecycleHook(name, payload, occurrenceId) {
308
299
  switch (name) {
309
300
  case "SessionStart":
310
301
  return [
@@ -316,15 +307,15 @@ function mapClaudeLifecycleHook(name, payload, occurrenceId) {
316
307
  case "UserPromptSubmit":
317
308
  return mapped("turn.accepted");
318
309
  case "PreToolUse":
319
- return operation("operation.started", "tool", toolId(payload));
310
+ return operation("operation.started", "tool", firstIdentity(payload, ["tool_use_id"], "Claude tool id"));
320
311
  case "PostToolUse":
321
- return operation("operation.completed", "tool", toolId(payload));
312
+ return operation("operation.completed", "tool", firstIdentity(payload, ["tool_use_id"], "Claude tool id"));
322
313
  case "PostToolUseFailure":
323
- return operation("operation.failed", "tool", toolId(payload));
314
+ return operation("operation.failed", "tool", firstIdentity(payload, ["tool_use_id"], "Claude tool id"));
324
315
  case "PermissionRequest":
325
316
  return mapped("turn.waiting", {
326
317
  reason: "permission",
327
- waitId: optionalIdentityFrom(payload, ["tool_use_id", "call_id"]) ?? requireOccurrence(occurrenceId)
318
+ waitId: requireOccurrence(occurrenceId)
328
319
  });
329
320
  case "MessageDisplay": {
330
321
  const messageId = optionalIdentityFrom(payload, ["message_id"]);
@@ -338,48 +329,60 @@ function mapClaudeLifecycleHook(name, payload, occurrenceId) {
338
329
  : `${messageId}:${index ?? "message"}`
339
330
  });
340
331
  }
341
- case "SubagentStart":
332
+ case "SubagentStart": {
333
+ const continuationId = firstIdentity(payload, ["agent_id"], "Claude subagent id");
342
334
  return [
343
- operation("operation.started", "subagent", subagentId(payload)),
344
- continuationObservation("continuation.started", payload, {
335
+ operation("operation.started", "subagent", continuationId),
336
+ mapped("continuation.started", {
345
337
  execution: "active",
346
338
  outcome: "pending",
347
339
  attachment: "attached",
348
340
  observationQuality: "exact",
349
341
  mayWriteWorkspace: true
350
- })
342
+ }, { continuationId })
351
343
  ];
352
- case "SubagentStop":
344
+ }
345
+ case "SubagentStop": {
346
+ const continuationId = firstIdentity(payload, ["agent_id"], "Claude subagent id");
347
+ const summary = claudeSummary(payload);
353
348
  return [
354
- operation("operation.completed", "subagent", subagentId(payload)),
355
- ...(optionalSummary(payload).summary === undefined ? [] : [
356
- continuationObservation("continuation.reported", payload, {
349
+ operation("operation.completed", "subagent", continuationId),
350
+ ...(summary.summary === undefined ? [] : [
351
+ mapped("continuation.reported", {
357
352
  execution: "quiescent",
358
- outcome: "succeeded",
353
+ outcome: "unknown",
359
354
  attachment: "attached",
360
355
  observationQuality: "exact",
361
356
  mayWriteWorkspace: false,
362
- reportId: reportId(payload),
363
- ...optionalSummary(payload)
364
- })
357
+ reportId: continuationId,
358
+ ...summary
359
+ }, { continuationId })
365
360
  ]),
366
- continuationObservation("continuation.settled", payload, {
361
+ mapped("continuation.settled", {
367
362
  execution: "quiescent",
368
- outcome: continuationOutcome(payload),
363
+ // SubagentStop also fires on interrupted queries. Its current
364
+ // payload has no outcome/status field; a report is not success.
365
+ outcome: "unknown",
369
366
  attachment: "attached",
370
367
  observationQuality: "exact",
371
368
  mayWriteWorkspace: false,
372
- ...optionalSummary(payload)
373
- })
369
+ ...summary
370
+ }, { continuationId })
374
371
  ];
375
- case "Stop":
372
+ }
373
+ case "Stop": {
374
+ const complete = claudeBackgroundEmpty(payload);
376
375
  return [
377
376
  mapped("turn.completed", optionalResultOutput(payload)),
378
377
  mapped("native-work.snapshot", {
379
- snapshotComplete: payload.background_tasks_complete === true,
380
- observationQuality: payload.background_tasks_complete === true ? "exact" : "partial"
378
+ // Current Stop fields are optional lists, never the invented
379
+ // background_tasks_complete boolean. Only explicit empty lists
380
+ // prove that no background work or scheduled wake remains.
381
+ snapshotComplete: complete,
382
+ observationQuality: complete ? "exact" : "partial"
381
383
  })
382
384
  ];
385
+ }
383
386
  case "StopFailure":
384
387
  return mapped("turn.failed", claudeFailure(payload));
385
388
  case "SessionEnd":
@@ -391,70 +394,13 @@ function mapClaudeLifecycleHook(name, payload, occurrenceId) {
391
394
  throw new Error(`Claude Code Driver does not support Hook event: ${name}.`);
392
395
  }
393
396
  }
394
- function claudeGoalObservation(payload, occurrenceId) {
395
- if (!Object.hasOwn(payload, "active_goal"))
396
- return undefined;
397
- if (payload.active_goal === null)
398
- return mapped("goal.cleared");
399
- const goal = objectValue(payload.active_goal);
400
- if (goal === undefined)
401
- return undefined;
402
- const objective = textValue(goal.objective);
403
- const status = normalizedGoalStatus(goal.status);
404
- const updatedAt = timestampValue(goal.updated_at ?? goal.updatedAt)
405
- ?? timestampValue(payload.timestamp)
406
- ?? occurrenceTimestamp(occurrenceId);
407
- if (objective === undefined || status === undefined || updatedAt === undefined)
408
- return undefined;
409
- const tokenBudget = positiveIntegerValue(goal.token_budget ?? goal.tokenBudget);
410
- return mapped("goal.updated", {
411
- goalStatus: status,
412
- goalObjective: objective,
413
- goalUpdatedAt: updatedAt,
414
- ...(optionalIdentityFrom(goal, ["turn_id", "turnId"]) === undefined
415
- ? {}
416
- : { goalNativeTurnId: optionalIdentityFrom(goal, ["turn_id", "turnId"]) }),
417
- ...(tokenBudget === undefined ? {} : { goalTokenBudget: tokenBudget })
418
- });
419
- }
420
- function normalizedGoalStatus(value) {
421
- if (value === "active" || value === "paused" || value === "blocked" || value === "complete") {
422
- return value;
423
- }
424
- if (value === "usageLimited" || value === "usage_limited" || value === "usage-limited") {
425
- return "usage-limited";
426
- }
427
- if (value === "budgetLimited" || value === "budget_limited" || value === "budget-limited") {
428
- return "budget-limited";
429
- }
430
- return undefined;
431
- }
432
- function objectValue(value) {
433
- return value !== null && typeof value === "object" && !Array.isArray(value)
434
- ? value
435
- : undefined;
436
- }
437
- function textValue(value) {
438
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
397
+ function claudeSummary(payload) {
398
+ const summary = optionalText(payload.last_assistant_message);
399
+ return summary === undefined ? {} : { summary };
439
400
  }
440
- function positiveIntegerValue(value) {
441
- return Number.isSafeInteger(value) && value > 0 ? value : undefined;
442
- }
443
- function timestampValue(value) {
444
- if (typeof value === "string" && Number.isFinite(Date.parse(value))) {
445
- return new Date(value).toISOString();
446
- }
447
- if (Number.isSafeInteger(value) && value >= 0) {
448
- const epoch = value;
449
- return new Date(epoch < 1_000_000_000_000 ? epoch * 1_000 : epoch).toISOString();
450
- }
451
- return undefined;
452
- }
453
- function occurrenceTimestamp(value) {
454
- const prefix = value?.slice(0, 24);
455
- return prefix !== undefined && Number.isFinite(Date.parse(prefix))
456
- ? new Date(prefix).toISOString()
457
- : undefined;
401
+ function claudeBackgroundEmpty(payload) {
402
+ return Array.isArray(payload.background_tasks) && payload.background_tasks.length === 0
403
+ && Array.isArray(payload.session_crons) && payload.session_crons.length === 0;
458
404
  }
459
405
  function mapCodexHook(name, payload, occurrenceId) {
460
406
  switch (name) {
@@ -531,16 +477,6 @@ function reportId(payload) {
531
477
  return optionalIdentityFrom(payload, ["report_id", "message_id", "agent_id", "subagent_id"])
532
478
  ?? subagentId(payload);
533
479
  }
534
- function continuationOutcome(payload) {
535
- if (payload.cancelled === true || payload.status === "cancelled")
536
- return "cancelled";
537
- if (payload.error !== undefined || payload.status === "failed")
538
- return "failed";
539
- if (payload.status === undefined || payload.status === "completed" || payload.status === "succeeded") {
540
- return "succeeded";
541
- }
542
- return "unknown";
543
- }
544
480
  function operation(kind, operationKind, operationId) {
545
481
  return mapped(kind, { operationId, operation: operationKind });
546
482
  }
@@ -589,11 +525,6 @@ function claudeFailure(payload) {
589
525
  const lastOutput = optionalText(payload.last_assistant_message);
590
526
  const raw = serializeAgentErrorRaw(payload);
591
527
  const classification = mapClaudeAgentError({ message: code, raw });
592
- const retryAfterMs = typeof payload.retry_after_ms === "number"
593
- && Number.isSafeInteger(payload.retry_after_ms)
594
- && payload.retry_after_ms > 0
595
- ? payload.retry_after_ms
596
- : undefined;
597
528
  return {
598
529
  failure: {
599
530
  error: standardAgentError({
@@ -602,13 +533,9 @@ function claudeFailure(payload) {
602
533
  classification,
603
534
  message: code,
604
535
  raw,
605
- inputDisposition: "accepted",
606
- ...(retryAfterMs === undefined ? {} : { retryAfterMs })
536
+ inputDisposition: "accepted"
607
537
  }),
608
- ...(lastOutput === undefined ? {} : { lastOutput }),
609
- ...(payload.run_terminal === true || payload.unrecoverable === true
610
- ? { runTerminal: true }
611
- : {})
538
+ ...(lastOutput === undefined ? {} : { lastOutput })
612
539
  },
613
540
  summary: [
614
541
  "Agent turn failed.",
@@ -418,21 +418,25 @@ function normalizeCursor(input) {
418
418
  if (!Number.isSafeInteger(offset) || offset < 0
419
419
  || typeof remainder !== "string"
420
420
  || state === null || typeof state !== "object" || Array.isArray(state)
421
- || (continuityEpoch !== undefined
422
- && (typeof continuityEpoch !== "string" || continuityEpoch.length === 0))
421
+ || typeof continuityEpoch !== "string" || continuityEpoch.length === 0
423
422
  || (fileFingerprint !== undefined
424
423
  && (typeof fileFingerprint !== "string" || fileFingerprint.length === 0))
425
424
  || (fileCtimeMs !== undefined
426
425
  && (typeof fileCtimeMs !== "number" || !Number.isFinite(fileCtimeMs) || fileCtimeMs < 0))) {
427
- return undefined;
426
+ throw new Error("Transcript observer cursor is invalid or unsupported.");
427
+ }
428
+ // Only our unavailable-before-first-read cursor lacks file metadata. An old
429
+ // partially shaped cursor must not silently reset/replay a healthy source.
430
+ const unstarted = offset === 0 && remainder === "" && Object.keys(state).length === 0
431
+ && continuityEpoch === INITIAL_CONTINUITY_EPOCH;
432
+ if (!unstarted && (fileFingerprint === undefined || fileCtimeMs === undefined)) {
433
+ throw new Error("Transcript observer cursor has no current file identity.");
428
434
  }
429
435
  return Object.freeze({
430
436
  offset: offset,
431
437
  remainder,
432
438
  state: state,
433
- continuityEpoch: typeof continuityEpoch === "string"
434
- ? continuityEpoch
435
- : INITIAL_CONTINUITY_EPOCH,
439
+ continuityEpoch,
436
440
  ...(typeof fileFingerprint === "string" ? { fileFingerprint } : {}),
437
441
  ...(typeof fileCtimeMs === "number" ? { fileCtimeMs } : {})
438
442
  });