@zq-silk/yui 0.6.2 → 0.6.4

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 (63) hide show
  1. package/ARCHITECTURE.md +28 -4
  2. package/README.md +60 -97
  3. package/dist/agent/argumentPolicy.js +1 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +1 -0
  5. package/dist/cli/commandCatalog.js +19 -9
  6. package/dist/cli/interactionPolicy.js +4 -2
  7. package/dist/cli.js +77 -32
  8. package/dist/commands/taskCommands.js +46 -11
  9. package/dist/commands/taskContextCommand.js +1 -1
  10. package/dist/commands/taskRoleRuntimeStatus.js +170 -10
  11. package/dist/controller/agentRuntimeObserver.js +210 -0
  12. package/dist/controller/clientRuntime.js +3 -21
  13. package/dist/controller/controller.js +47 -7
  14. package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
  15. package/dist/controller/runtime.js +9 -3
  16. package/dist/controller/runtimeEventInbox.js +49 -295
  17. package/dist/controller/runtimeEventProcessor.js +184 -321
  18. package/dist/controller/runtimeHookRunFence.js +226 -0
  19. package/dist/controller/runtimeLaunchCoordinator.js +91 -26
  20. package/dist/controller/runtimeObservationHook.js +112 -0
  21. package/dist/core/controllerServer.js +5 -0
  22. package/dist/executor/agentAdapter.js +18 -3
  23. package/dist/executor/fileRoleLaunchPlanner.js +64 -15
  24. package/dist/executor/managedClaudeRunner.js +121 -0
  25. package/dist/observability/executionAudit.js +6 -3
  26. package/dist/repository/taskWorkspacePreparer.js +1 -4
  27. package/dist/run/providerRetryConfig.js +8 -3
  28. package/dist/runtime/agentDriver.js +229 -0
  29. package/dist/runtime/agentDriverObservation.js +57 -0
  30. package/dist/runtime/builtinAgentDrivers.js +235 -0
  31. package/dist/runtime/builtinTranscriptObserver.js +290 -0
  32. package/dist/runtime/builtinTranscriptUsage.js +97 -0
  33. package/dist/runtime/exactControlPlane.js +2 -2
  34. package/dist/runtime/index.js +1 -1
  35. package/dist/runtime/ports.js +12 -1
  36. package/dist/runtime/runtimeObservation.js +297 -0
  37. package/dist/runtime/runtimeProjection.js +277 -0
  38. package/dist/runtime/sessionTerminationGuard.js +78 -22
  39. package/dist/runtime/tmuxAdapters.js +35 -0
  40. package/dist/scheduler/activeRoleRunDelivery.js +28 -13
  41. package/dist/scheduler/leaderWakeupProcessor.js +21 -2
  42. package/dist/scheduler/roleRunLiveness.js +2 -2
  43. package/dist/scheduler/roleRunStall.js +62 -114
  44. package/dist/storage/migration/productionRegistry.js +85 -0
  45. package/dist/storage/sqliteStore.js +3 -3
  46. package/dist/storage/storageVersions.js +1 -1
  47. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +2 -1
  48. package/dist/storage/upgrade/sqliteStateMigration.js +123 -0
  49. package/dist/telemetry/sqliteTelemetryStore.js +0 -28
  50. package/dist/telemetry/telemetryCompaction.js +1 -0
  51. package/dist/telemetry/telemetryConfig.js +4 -5
  52. package/dist/tmux/tmuxManager.js +136 -22
  53. package/dist/web/assets/client/view.js +1 -1
  54. package/dist/web/tmuxWebTerminal.js +17 -12
  55. package/dist/web/webSnapshot.js +1 -1
  56. package/dist/worktree/managedWorkspace.js +14 -0
  57. package/i18n/README.zh-CN.md +12 -7
  58. package/package.json +1 -1
  59. package/dist/controller/claudeLifecycleHook.js +0 -203
  60. package/dist/controller/codexLifecycleHook.js +0 -108
  61. package/dist/controller/providerHookRunFence.js +0 -156
  62. package/dist/lifecycle/providerLifecycleMapping.js +0 -190
  63. package/dist/telemetry/telemetryRouter.js +0 -32
@@ -1,6 +1,5 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { isDeepStrictEqual } from "node:util";
2
- import { routeProviderProgress } from "../telemetry/telemetryRouter.js";
3
- import { NullTelemetrySink } from "../telemetry/telemetryStore.js";
4
3
  import { activeLiveRoleAgentSession, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, recordObservedTaskRoleCompletion, recordRoleAgentSession, rememberRoleAgentCompletedTurn, settleTaskRoleCompletion, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
5
4
  import { createPendingTurnCompletion, hasRecentTurnId } from "../executor/turnCompletion.js";
6
5
  import { createTaskEvent } from "../event/taskEvent.js";
@@ -13,13 +12,12 @@ import { classifyProviderError, isRetryableProviderErrorClass } from "../lifecyc
13
12
  import { providerRetryIsDue, scheduleProviderRetry } from "../run/providerRetry.js";
14
13
  import { providerRetryConfig } from "../run/providerRetryConfig.js";
15
14
  import { terminalizeExactTaskRun } from "../lifecycle/exactRunTerminalization.js";
16
- import { foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
17
- import { mapNativeLifecycleSignal } from "../lifecycle/providerLifecycleMapping.js";
15
+ import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
18
16
  import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
19
17
  import { createLeaderRecoveryNotification, createLeaderStallNotification } from "../scheduler/operatorNotification.js";
20
18
  import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
21
19
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
22
- import { foldRunProgressFacts, latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, isRoleRunStalled, clearMatchingLeaderStallAttention, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT, RUN_RESOURCE_SUPPRESSED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
20
+ import { foldRunProgressFacts, latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, clearMatchingLeaderStallAttention, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
23
21
  import { currentWorkItemExecutionGroup, updateWorkItemExecutionGroup, updateWorkItemStatus } from "../workItem/workItem.js";
24
22
  import { recordExecutionLaneResult } from "../execution/executionGroup.js";
25
23
  import { formatAgentRunReceiptId, formatTaskRecordReference } from "../task/taskRecordReference.js";
@@ -27,21 +25,221 @@ import { bindExecution, claimPending, completeProcessing, releaseProcessing } fr
27
25
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
28
26
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_LAUNCH_RESERVED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
29
27
  import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
30
- const NULL_TELEMETRY_SINK = new NullTelemetrySink();
28
+ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
29
+ import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
31
30
  /** Maps the authoritative FileTaskStore records to the scheduler's narrow port. */
32
31
  export class FileSchedulerStoreAdapter {
33
32
  store;
34
33
  telemetry;
35
- /**
36
- * @param telemetry Optional sidecar wiring (Issue 09). Absent or `legacy`
37
- * keeps master behavior (semantic progress events only); `dual` also
38
- * upserts progress to the sidecar; `bounded` routes progress to the
39
- * sidecar only and feeds the scheduler projection synthesized latest
40
- * progress from the reader.
41
- */
42
- constructor(store, telemetry = null) {
34
+ drivers;
35
+ /** Diagnostic telemetry is optional and never participates in runtime truth. */
36
+ constructor(store, telemetry = null, drivers = builtinAgentDriverRegistry()) {
43
37
  this.store = store;
44
38
  this.telemetry = telemetry;
39
+ this.drivers = drivers;
40
+ }
41
+ /**
42
+ * Sole provider-independent ingress for structured runtime state. Driver
43
+ * mapping has already happened before this boundary; this method validates
44
+ * the exact durable Run/session fence, applies the small workflow-relevant
45
+ * transitions, and retains the canonical observation for status projection.
46
+ */
47
+ observeRuntimeObservation(raw, now = new Date()) {
48
+ const input = createRuntimeObservation(raw);
49
+ const taskId = input.fence.taskId;
50
+ if (taskId === undefined)
51
+ return "obsolete";
52
+ if (Date.parse(input.receivedAt) > now.getTime()) {
53
+ return this.recordObsoleteCanonicalObservation(input, "received-at-in-future", now);
54
+ }
55
+ const adapterId = this.adapterForRuntimeObservation(input);
56
+ if (adapterId === null) {
57
+ return this.recordObsoleteCanonicalObservation(input, "driver-or-run-mismatch", now);
58
+ }
59
+ let outcome;
60
+ switch (input.kind) {
61
+ case "session.started":
62
+ case "session.ready":
63
+ outcome = this.observeRuntimeSession(input, adapterId, now);
64
+ break;
65
+ case "turn.accepted":
66
+ outcome = this.observeRuntimePromptAccepted(input, adapterId, now);
67
+ break;
68
+ case "turn.completed": {
69
+ const completed = {
70
+ taskId,
71
+ roleName: input.fence.roleName,
72
+ agentId: input.fence.agentId,
73
+ adapterId,
74
+ launchId: input.fence.launchId,
75
+ nativeSessionId: input.fence.nativeSessionId,
76
+ turnId: input.fence.nativeTurnId,
77
+ runId: input.fence.runId,
78
+ summary: input.payload.summary ?? "Agent turn completed without a workflow outcome."
79
+ };
80
+ const classification = this.classifyRuntimeTurnCompleted(completed);
81
+ if (classification !== "apply")
82
+ return classification;
83
+ const result = this.observeRuntimeTurnCompleted(completed, now);
84
+ outcome = result.disposition === "obsolete" ? "obsolete" : "applied";
85
+ break;
86
+ }
87
+ case "turn.failed": {
88
+ const failureEvidence = input.payload.failure;
89
+ const failure = {
90
+ eventId: input.eventId,
91
+ eventType: input.kind,
92
+ taskId,
93
+ roleName: input.fence.roleName,
94
+ agentId: input.fence.agentId,
95
+ adapterId,
96
+ launchId: input.fence.launchId,
97
+ nativeSessionId: input.fence.nativeSessionId,
98
+ runId: input.fence.runId,
99
+ error: failureEvidence.code,
100
+ ...(failureEvidence.details === undefined
101
+ ? {}
102
+ : { errorDetails: failureEvidence.details }),
103
+ ...(failureEvidence.lastOutput === undefined
104
+ ? {}
105
+ : { lastAssistantMessage: failureEvidence.lastOutput })
106
+ };
107
+ if (this.classifyRuntimeTurnFailed(failure) !== "apply")
108
+ return "obsolete";
109
+ outcome = this.observeRuntimeTurnFailed(failure, now).disposition === "applied"
110
+ ? "applied"
111
+ : "obsolete";
112
+ break;
113
+ }
114
+ case "operation.started":
115
+ case "operation.completed":
116
+ case "operation.failed":
117
+ case "turn.waiting":
118
+ case "turn.cancelled":
119
+ case "activity.observed":
120
+ case "observer.health":
121
+ outcome = this.validateCanonicalRunObservation(input, now);
122
+ break;
123
+ case "session.ended":
124
+ case "session.failed":
125
+ outcome = this.validateCanonicalSessionObservation(input, now);
126
+ break;
127
+ case "host.observed":
128
+ outcome = "obsolete";
129
+ break;
130
+ default:
131
+ outcome = "obsolete";
132
+ }
133
+ if (outcome === "applied")
134
+ this.persistRuntimeObservation(input, now);
135
+ return outcome;
136
+ }
137
+ adapterForRuntimeObservation(input) {
138
+ const taskId = input.fence.taskId;
139
+ const runId = input.fence.runId;
140
+ if (taskId === undefined || runId === undefined)
141
+ return null;
142
+ const run = this.store.getAgentRun(taskId, runId);
143
+ if (run === null
144
+ || run.roleName !== input.fence.roleName
145
+ || run.effective.agentId !== input.fence.agentId)
146
+ return null;
147
+ try {
148
+ return this.drivers.requireByAdapterId(run.effective.adapterId).id === input.fence.driverId
149
+ ? run.effective.adapterId
150
+ : null;
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ }
156
+ validateCanonicalRunObservation(input, now) {
157
+ return this.store.transaction((store) => {
158
+ if (store.listEvents(input.fence.taskId).some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
159
+ && event.payload.eventId === input.eventId)))
160
+ return "applied";
161
+ const run = store.getAgentRun(input.fence.taskId, input.fence.runId);
162
+ const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
163
+ const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
164
+ const session = sessions?.sessions[input.fence.agentId];
165
+ const requiresActive = input.kind !== "turn.cancelled";
166
+ const valid = run !== null
167
+ && (!requiresActive || (run.status === "active" && active?.id === run.id))
168
+ && run.roleName === input.fence.roleName
169
+ && run.effective.agentId === input.fence.agentId
170
+ && this.drivers.requireByAdapterId(run.effective.adapterId).id === input.fence.driverId
171
+ && session?.launchId === input.fence.launchId
172
+ && session.nativeSessionId === input.fence.nativeSessionId
173
+ && input.fence.sessionGenerationId === input.fence.launchId
174
+ && input.fence.receiptId === formatAgentRunReceiptId(input.fence.taskId, run.id)
175
+ && (run.deliveredAt !== undefined || input.kind === "turn.cancelled");
176
+ if (!valid) {
177
+ recordCanonicalObservationObsolete(store, input, "runtime-fence-not-current", now);
178
+ return "obsolete";
179
+ }
180
+ return "applied";
181
+ });
182
+ }
183
+ validateCanonicalSessionObservation(input, now) {
184
+ return this.store.transaction((store) => {
185
+ const run = store.getAgentRun(input.fence.taskId, input.fence.runId);
186
+ const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
187
+ const session = sessions?.sessions[input.fence.agentId];
188
+ if (run === null
189
+ || run.roleName !== input.fence.roleName
190
+ || run.effective.agentId !== input.fence.agentId
191
+ || this.drivers.requireByAdapterId(run.effective.adapterId).id !== input.fence.driverId
192
+ || input.fence.receiptId !== formatAgentRunReceiptId(input.fence.taskId, run.id)
193
+ || session?.launchId !== input.fence.launchId
194
+ || session.nativeSessionId !== input.fence.nativeSessionId
195
+ || input.fence.sessionGenerationId !== input.fence.launchId) {
196
+ recordCanonicalObservationObsolete(store, input, "runtime-session-not-current", now);
197
+ return "obsolete";
198
+ }
199
+ const status = input.kind === "session.failed" ? "broken" : "stopped";
200
+ store.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(sessions, input.fence.agentId, status, now));
201
+ return "applied";
202
+ });
203
+ }
204
+ persistRuntimeObservation(input, now) {
205
+ this.store.transaction((store) => {
206
+ const taskId = input.fence.taskId;
207
+ const events = store.listEvents(taskId);
208
+ if (events.some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
209
+ && event.payload.eventId === input.eventId)))
210
+ return;
211
+ if (usageSnapshotIsSuperseded(events, input))
212
+ return;
213
+ const confirmedActivity = confirmedUsageActivityObservation(events, input);
214
+ const removable = [
215
+ ...compactedRuntimeObservationIds(events, input),
216
+ ...(confirmedActivity === null
217
+ ? []
218
+ : compactedRuntimeObservationIds(events, confirmedActivity))
219
+ ];
220
+ if (removable.length > 0)
221
+ store.removeEvents(taskId, removable);
222
+ store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(input), now));
223
+ if (confirmedActivity !== null) {
224
+ store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(confirmedActivity), now));
225
+ }
226
+ });
227
+ if (this.telemetry !== null && input.fence.runId !== undefined) {
228
+ try {
229
+ this.telemetry.sink.observe(runtimeObservationTelemetryEntry(input));
230
+ }
231
+ catch {
232
+ // Runtime telemetry is diagnostic; the compact durable state snapshot
233
+ // above remains authoritative when a sidecar is unavailable.
234
+ }
235
+ }
236
+ }
237
+ recordObsoleteCanonicalObservation(input, reason, now) {
238
+ const taskId = input.fence.taskId;
239
+ if (taskId === undefined || this.store.getTask(taskId) === null)
240
+ return "obsolete";
241
+ this.store.transaction((store) => recordCanonicalObservationObsolete(store, input, reason, now));
242
+ return "obsolete";
45
243
  }
46
244
  /**
47
245
  * Revision-scoped read projection. Keyed by the store's durable revision;
@@ -53,24 +251,14 @@ export class FileSchedulerStoreAdapter {
53
251
  const revision = typeof this.store.getStateRevision === "function"
54
252
  ? this.store.getStateRevision()
55
253
  : Number.NaN;
56
- const telemetryRevision = this.telemetry?.mode === "bounded"
57
- ? this.telemetry.reader.revision()
58
- : 0;
59
254
  if (this.#readProjection === null
60
- || this.#readProjection.revision !== revision
61
- || this.#readProjection.telemetryRevision !== telemetryRevision) {
62
- this.#readProjection = { revision, telemetryRevision, tasks: new Map() };
255
+ || this.#readProjection.revision !== revision) {
256
+ this.#readProjection = { revision, tasks: new Map() };
63
257
  }
64
258
  const tasks = this.#readProjection.tasks;
65
259
  let task = tasks.get(taskId);
66
260
  if (task === undefined) {
67
- const semanticEvents = this.store.listEvents(taskId);
68
- // In bounded mode progress lives only in the sidecar; merge the latest
69
- // observation per Run so liveness/stall folds keep working. In legacy
70
- // and dual modes semantic history already carries progress.
71
- const events = this.telemetry?.mode === "bounded"
72
- ? [...semanticEvents, ...this.telemetry.reader.latestProgressEvents(taskId)]
73
- : semanticEvents;
261
+ const events = this.store.listEvents(taskId);
74
262
  task = {
75
263
  events,
76
264
  runFacts: foldRunProgressFacts(events),
@@ -301,58 +489,24 @@ export class FileSchedulerStoreAdapter {
301
489
  return existing && !recovered ? "already-recorded" : "recorded";
302
490
  });
303
491
  }
304
- recordRoleRunResourceSuppression(input) {
305
- return this.store.transaction((store) => {
306
- const task = store.getTask(input.taskId);
307
- const run = store.getActiveAgentRun(input.taskId, input.roleName);
308
- if (task === null
309
- || task.status !== "active"
310
- || run === null
311
- || run.id !== input.runId
312
- || run.status !== "active")
313
- return "state-changed";
314
- const session = store.getRoleSession(input.taskId, input.roleName);
315
- if (session === null
316
- || session.agentId !== input.agentId
317
- || session.adapterId !== input.adapterId
318
- || session.status === "stopped"
319
- || session.status === "broken"
320
- || session.nativeSessionId !== input.nativeSessionId
321
- || session.launchId !== input.launchId)
322
- return "state-changed";
323
- const existing = store.listEvents(task.id).some((event) => (event.type === RUN_RESOURCE_SUPPRESSED_EVENT
324
- && event.payload.runId === run.id
325
- && event.payload.roleName === input.roleName
326
- && event.payload.progressAt === input.progressAt));
327
- if (existing)
328
- return "already-recorded";
329
- store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RESOURCE_SUPPRESSED_EVENT, {
330
- runId: run.id,
331
- roleName: input.roleName,
332
- progressAt: input.progressAt,
333
- observedAt: input.observedAt,
334
- kind: "advisory-resource"
335
- }, input.now));
336
- return "recorded";
337
- });
338
- }
339
492
  hasInFlightTurn(taskId, roleName) {
340
493
  const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
341
494
  return sessions !== null && sessions.inFlight !== null;
342
495
  }
343
496
  isRoleGenerationProviderReady(input) {
344
- // A provider-ready fact for this exact generation is proven by a folded
345
- // runtime.provider-session-lifecycle event with preInputReady true and a
346
- // matching agent/launch/nativeSession fence. This reads durable folded truth
497
+ // A session.ready observation for this exact generation is the durable
498
+ // pre-input fence. This reads folded Driver truth
347
499
  // only — no liveness, screen, or pane/PID inference. Served from the
348
500
  // revision projection: a stale miss only repeats an idempotent first push.
349
- return this.listEvents(input.taskId).some((event) => (event.type === "runtime.provider-session-lifecycle"
350
- && event.payload.roleName === input.roleName
351
- && event.payload.agentId === input.agentId
352
- && event.payload.preInputReady === "true"
353
- && (input.launchId === undefined || event.payload.launchId === input.launchId)
354
- && (input.nativeSessionId === undefined
355
- || event.payload.nativeSessionId === input.nativeSessionId)));
501
+ return this.listEvents(input.taskId).some((event) => {
502
+ const observation = runtimeObservationFromTaskEvent(event);
503
+ return observation?.kind === "session.ready"
504
+ && observation.fence.roleName === input.roleName
505
+ && observation.fence.agentId === input.agentId
506
+ && (input.launchId === undefined || observation.fence.launchId === input.launchId)
507
+ && (input.nativeSessionId === undefined
508
+ || observation.fence.nativeSessionId === input.nativeSessionId);
509
+ });
356
510
  }
357
511
  peekNextAgentRunId(taskId) {
358
512
  return this.store.peekNextAgentRunId(taskId);
@@ -995,7 +1149,7 @@ export class FileSchedulerStoreAdapter {
995
1149
  if (hasRuntimeCleanupObligation(mailbox)) {
996
1150
  throw new Error("Runtime cleanup is still pending.");
997
1151
  }
998
- // A synchronous provider lifecycle Hook may bind a fresh native Session
1152
+ // A synchronous Driver Hook may bind a fresh native Session
999
1153
  // and complete the reservation while the host start call is still
1000
1154
  // unwinding. Preserve that exact hook-won generation instead of treating
1001
1155
  // the already-settled reservation as a launch failure.
@@ -1151,7 +1305,7 @@ export class FileSchedulerStoreAdapter {
1151
1305
  }
1152
1306
  // Provider completion is not a transport acknowledgement. Retain the
1153
1307
  // immutable Hook until the exact pane receipt independently reconciles
1154
- // run.pushed; never infer transport from provider progress/terminal facts.
1308
+ // run.pushed; never infer transport from Driver activity/terminal facts.
1155
1309
  return sessions.inFlight.pushedAt === undefined ? "deferred" : "apply";
1156
1310
  }
1157
1311
  observeRuntimeTurnCompleted(input, now = new Date()) {
@@ -1251,23 +1405,9 @@ export class FileSchedulerStoreAdapter {
1251
1405
  observedAt: now,
1252
1406
  dueAt: new Date(now.getTime() + 2_000)
1253
1407
  });
1254
- const wasStalled = isRoleRunStalled(store.listEvents(task.id), fence.runId);
1255
- store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_PROGRESS_EVENT, {
1256
- runId: fence.runId,
1257
- roleName: role.name,
1258
- kind: "native-turn",
1259
- turnId: input.turnId,
1260
- note: boundedRuntimeNote(input.summary)
1261
- }, now));
1262
- if (wasStalled) {
1263
- store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RECOVERED_EVENT, {
1264
- runId: fence.runId,
1265
- roleName: role.name,
1266
- progressAt: now.toISOString(),
1267
- kind: "native-turn"
1268
- }, now));
1269
- }
1270
- clearMatchingLeaderStallAttention(store, task.id, fence.runId);
1408
+ // Runtime completion is not workflow progress. The exact Run remains
1409
+ // open for the short outcome grace period so a concurrently committed
1410
+ // yui yield/block/complete action can win; otherwise the contract fails.
1271
1411
  sessions = recordObservedTaskRoleCompletion(sessions, completion);
1272
1412
  const active = store.getActiveAgentRun(task.id, role.name);
1273
1413
  if (active === null || active.id !== fence.runId || active.status !== "active") {
@@ -1391,53 +1531,7 @@ export class FileSchedulerStoreAdapter {
1391
1531
  || (input.expectedRunId !== undefined && active.id !== input.expectedRunId)) {
1392
1532
  return { session };
1393
1533
  }
1394
- if (role.name !== "leader" || active.workItemId !== undefined) {
1395
- const summary = `Role turn completed without yui task run yield. Last assistant message: ${input.summary}`;
1396
- const result = terminalizeExactTaskRun(store, {
1397
- taskId: task.id,
1398
- roleName: role.name,
1399
- agentId: active.effective.agentId,
1400
- runId: active.id,
1401
- receiptId: formatAgentRunReceiptId(task.id, active.id),
1402
- nativeSessionId: input.nativeSessionId,
1403
- ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1404
- outcome: { status: "failed", summary }
1405
- }, now);
1406
- if (result.disposition !== "applied" || result.run === null) {
1407
- throw new Error(`Role Run changed during native Turn completion: ${active.id}/${result.reason}.`);
1408
- }
1409
- const terminal = result.run;
1410
- if (active.purpose === "execution" && active.workItemId !== undefined) {
1411
- const item = store.getWorkItem(task.id, active.workItemId);
1412
- if (item !== null && !["completed", "failed", "retired"].includes(item.status)) {
1413
- store.saveWorkItem(task.id, updateWorkItemStatus(item, "failed", now, summary));
1414
- }
1415
- }
1416
- store.saveRole(task.id, updateRoleStatus(role, "idle", now));
1417
- clearMatchingLeaderStallAttention(store, task.id, terminal.id);
1418
- enqueueWork(store, { kind: "role", taskId: task.id, roleName: "leader" }, "role-run-failed", now, [
1419
- { type: "run", taskId: task.id, id: terminal.id },
1420
- ...(terminal.workItemId === undefined
1421
- ? []
1422
- : [{
1423
- type: "work-item",
1424
- taskId: task.id,
1425
- id: terminal.workItemId
1426
- }])
1427
- ]);
1428
- return { session, finalizedRunId: terminal.id };
1429
- }
1430
- const leaderTarget = { kind: "role", taskId: task.id, roleName: role.name };
1431
- const leaderMailbox = store.getWorkMailbox(leaderTarget);
1432
- const recoveryTurn = leaderMailbox?.processing?.executionRef?.type === "run"
1433
- && leaderMailbox.processing.executionRef.taskId === task.id
1434
- && leaderMailbox.processing.executionRef.id === active.id
1435
- && leaderMailbox.processing.batch.reasons.includes("leader-turn-unclosed");
1436
- const quiescent = leaderMailbox?.pending === null
1437
- && !store.listRoles(task.id).some((candidate) => (candidate.name !== "leader"
1438
- && store.getActiveAgentRun(task.id, candidate.name) !== null))
1439
- && !store.listWorkItems(task.id).some((item) => item.status === "running")
1440
- && !store.listInputRequests(task.id).some((request) => request.status === "open");
1534
+ const summary = `Agent turn completed without a Yui workflow outcome. Last assistant message: ${input.summary}`;
1441
1535
  const result = terminalizeExactTaskRun(store, {
1442
1536
  taskId: task.id,
1443
1537
  roleName: role.name,
@@ -1446,38 +1540,45 @@ export class FileSchedulerStoreAdapter {
1446
1540
  receiptId: formatAgentRunReceiptId(task.id, active.id),
1447
1541
  nativeSessionId: input.nativeSessionId,
1448
1542
  ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1449
- outcome: { status: "yielded", summary: input.summary }
1543
+ outcome: { status: "failed", summary }
1450
1544
  }, now);
1451
1545
  if (result.disposition !== "applied" || result.run === null) {
1452
- throw new Error(`Leader Run changed during native Turn completion: ${active.id}/${result.reason}.`);
1546
+ throw new Error(`Role Run changed during native Turn completion: ${active.id}/${result.reason}.`);
1453
1547
  }
1454
1548
  const terminal = result.run;
1455
- if (input.origin === "recovery") {
1456
- store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RECOVERED_EVENT, { runId: terminal.id, roleName: role.name, summary: input.summary }, now));
1549
+ const leaderWorkflowRun = role.name === "leader" && active.workItemId === undefined;
1550
+ if (active.purpose === "execution" && active.workItemId !== undefined) {
1551
+ const item = store.getWorkItem(task.id, active.workItemId);
1552
+ if (item !== null && !["completed", "failed", "retired"].includes(item.status)) {
1553
+ store.saveWorkItem(task.id, updateWorkItemStatus(item, "failed", now, summary));
1554
+ }
1457
1555
  }
1556
+ store.saveRole(task.id, updateRoleStatus(role, leaderWorkflowRun ? "failed" : "idle", now));
1458
1557
  clearMatchingLeaderStallAttention(store, task.id, terminal.id);
1459
- if (quiescent) {
1460
- if (recoveryTurn) {
1461
- const message = "Leader ended a recovery Turn without completing, blocking, or continuing the Task.";
1462
- store.saveLeaderFailure(recordLeaderFailure(task.id, input.nativeSessionId, message, now, store.getLeaderFailure(task.id)));
1463
- store.saveOperatorNotification(createLeaderRecoveryNotification(task.id, message, now, store.getOperatorNotification(task.id)));
1464
- store.saveRole(task.id, updateRoleStatus(role, "failed", now));
1465
- enqueueWork(store, { kind: "operator" }, "leader-recovery-failed", now, [
1466
- { type: "task", id: task.id },
1467
- { type: "run", taskId: task.id, id: terminal.id }
1468
- ]);
1469
- }
1470
- else {
1471
- enqueueWork(store, leaderTarget, "leader-turn-unclosed", now, [
1472
- { type: "task", id: task.id },
1473
- { type: "run", taskId: task.id, id: terminal.id }
1474
- ]);
1475
- }
1558
+ if (!leaderWorkflowRun) {
1559
+ enqueueWork(store, { kind: "role", taskId: task.id, roleName: "leader" }, "role-run-failed", now, [
1560
+ { type: "run", taskId: task.id, id: terminal.id },
1561
+ ...(terminal.workItemId === undefined
1562
+ ? []
1563
+ : [{
1564
+ type: "work-item",
1565
+ taskId: task.id,
1566
+ id: terminal.workItemId
1567
+ }])
1568
+ ]);
1569
+ }
1570
+ else {
1571
+ store.saveLeaderFailure(recordLeaderFailure(task.id, input.nativeSessionId, summary, now, store.getLeaderFailure(task.id)));
1572
+ store.saveOperatorNotification(createLeaderRecoveryNotification(task.id, summary, now, store.getOperatorNotification(task.id)));
1573
+ enqueueWork(store, { kind: "operator" }, "leader-run-failed", now, [
1574
+ { type: "task", id: task.id },
1575
+ { type: "run", taskId: task.id, id: terminal.id }
1576
+ ]);
1476
1577
  }
1477
1578
  return { session, finalizedRunId: terminal.id };
1478
1579
  });
1479
1580
  }
1480
- classifyClaudeStopFailureEvent(input) {
1581
+ classifyRuntimeTurnFailed(input) {
1481
1582
  const task = this.store.getTask(input.taskId);
1482
1583
  if (task?.status !== "active")
1483
1584
  return "obsolete";
@@ -1491,8 +1592,7 @@ export class FileSchedulerStoreAdapter {
1491
1592
  || active?.id !== run.id
1492
1593
  || run.roleName !== input.roleName
1493
1594
  || run.effective.agentId !== input.agentId
1494
- || run.effective.adapterId !== "claude"
1495
- || input.adapterId !== "claude")
1595
+ || run.effective.adapterId !== input.adapterId)
1496
1596
  return "obsolete";
1497
1597
  const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
1498
1598
  const session = sessions?.sessions[input.agentId];
@@ -1505,14 +1605,14 @@ export class FileSchedulerStoreAdapter {
1505
1605
  return "obsolete";
1506
1606
  return "apply";
1507
1607
  }
1508
- observeClaudeStopFailureEvent(input, now = new Date()) {
1608
+ observeRuntimeTurnFailed(input, now = new Date()) {
1509
1609
  return this.store.transaction((store) => {
1510
1610
  const before = store.getAgentRun(input.taskId, input.runId);
1511
1611
  if (before === null) {
1512
1612
  recordObsoleteRuntimeEvent(store, input, "run-missing", now);
1513
1613
  return { disposition: "obsolete", runId: input.runId };
1514
1614
  }
1515
- const summary = claudeStopFailureSummary(input);
1615
+ const summary = runtimeTurnFailureSummary(input);
1516
1616
  // Issue 04: a classified transient Provider failure keeps the exact Run
1517
1617
  // and Native Session and is retried in place instead of terminalizing.
1518
1618
  const retryDecision = this.providerRetryDecision(store, input, summary, now);
@@ -1553,7 +1653,7 @@ export class FileSchedulerStoreAdapter {
1553
1653
  if (role !== null) {
1554
1654
  store.saveRole(input.taskId, updateRoleStatus(role, input.roleName === "leader" ? "failed" : "idle", now));
1555
1655
  }
1556
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.claude-stop-failure", {
1656
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.turn-failed", {
1557
1657
  eventId: input.eventId,
1558
1658
  runId: input.runId,
1559
1659
  roleName: input.roleName,
@@ -1790,206 +1890,119 @@ export class FileSchedulerStoreAdapter {
1790
1890
  return reopened;
1791
1891
  }
1792
1892
  /**
1793
- * Folds a provider SessionStart lifecycle fact
1794
- * through the canonical contract. Session-started/ready never advance Run
1795
- * acceptance; this only records generation facts and binds a discovered native
1796
- * id under the exact launch fence. Idempotent replays are no-ops.
1893
+ * Applies already-normalized Session observations. Driver-specific startup
1894
+ * names and source variants have terminated before this boundary.
1797
1895
  */
1798
- observeProviderSessionLifecycle(input, now = new Date()) {
1896
+ observeRuntimeSession(input, adapterId, now) {
1799
1897
  return this.store.transaction((store) => {
1800
- const decision = this.foldProviderSignal(store, {
1801
- kind: "native-session-start",
1802
- ...(input.sessionSource === undefined ? {} : { sessionSource: input.sessionSource }),
1803
- fence: providerFence(input)
1804
- }, input.runId);
1898
+ const event = createCanonicalLifecycleEvent({
1899
+ phase: input.kind === "session.ready" ? "provider-ready" : "provider-session-started",
1900
+ source: "provider-native",
1901
+ evidence: "provider-native-durable",
1902
+ ...(input.kind === "session.ready"
1903
+ ? {
1904
+ preInputReady: true,
1905
+ readinessVariant: `${input.fence.driverId}:session.ready`
1906
+ }
1907
+ : {}),
1908
+ fence: runtimeObservationLifecycleFence(input, adapterId)
1909
+ });
1910
+ const decision = this.foldRuntimeLifecycleEvent(store, event, input);
1805
1911
  switch (decision.kind) {
1806
1912
  case "obsolete":
1807
- recordCanonicalObsolete(store, input, "native-session-lifecycle", decision.reason, now);
1913
+ recordCanonicalObservationObsolete(store, input, decision.reason, now);
1808
1914
  return "obsolete";
1809
1915
  case "deferred":
1810
1916
  return "deferred";
1811
1917
  case "idempotent":
1812
1918
  return "applied";
1813
- case "apply":
1919
+ case "apply": {
1814
1920
  if (decision.outcome.outcome === "mark-ready"
1815
- && input.adapterId === "claude"
1816
- && input.sessionSource === "startup"
1817
- && store.getTaskRoleSessionSet(input.taskId, input.roleName)
1818
- ?.sessions[input.agentId] === undefined) {
1819
- // Claude emits startup readiness after native process creation but
1820
- // before the Controller projects its preallocated Session. Keep
1821
- // the immutable fact queued until that exact Session is durable.
1822
- return "deferred";
1921
+ && store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName)
1922
+ ?.sessions[input.fence.agentId] === undefined) {
1923
+ return preallocatedRuntimeReadyAwaitingProjection(store, input, this.drivers)
1924
+ ? "applied"
1925
+ : "deferred";
1823
1926
  }
1824
1927
  if (decision.outcome.outcome === "bind-native-session") {
1825
- const role = store.getRole(input.taskId, input.roleName);
1826
- const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
1827
- const run = input.runId === undefined
1828
- ? null
1829
- : store.getAgentRun(input.taskId, input.runId);
1928
+ const taskId = input.fence.taskId;
1929
+ const role = store.getRole(taskId, input.fence.roleName);
1930
+ const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
1931
+ const run = store.getAgentRun(taskId, input.fence.runId);
1830
1932
  if (role === null || sessions === null || run === null) {
1831
- recordCanonicalObsolete(store, input, "native-session-lifecycle", "bind-state-missing", now);
1933
+ recordCanonicalObservationObsolete(store, input, "bind-state-missing", now);
1832
1934
  return "obsolete";
1833
1935
  }
1834
1936
  const bound = recordRoleAgentSession(sessions, {
1835
- agentId: input.agentId,
1836
- adapterId: input.adapterId,
1937
+ agentId: input.fence.agentId,
1938
+ adapterId,
1837
1939
  nativeSessionId: decision.outcome.nativeSessionId,
1838
- launchId: input.launchId,
1940
+ launchId: input.fence.launchId,
1839
1941
  policy: "fixed",
1840
1942
  status: "running",
1841
1943
  effective: run.effective
1842
1944
  }, now);
1843
1945
  store.saveTaskRoleSessionSet(bound);
1844
- // Codex 0.145 emits SessionStart from inside run_turn(input), so a
1845
- // runtime-discovered bind is durable evidence that this Run's
1846
- // initial transport input already crossed the provider boundary.
1847
- // Preserve that truth before clearing the launch reservation: a
1848
- // restarted Controller must not allocate a second generation or
1849
- // push the same first input again. UserPromptSubmit remains the
1850
- // sole acceptance/delivered transition.
1851
- if (input.adapterId === "codex" && run.pushedAt === undefined) {
1946
+ const driver = this.drivers.require(input.fence.driverId);
1947
+ if (driver.capabilities.observation.sessionBootstrap === "discovered"
1948
+ && run.pushedAt === undefined) {
1852
1949
  const pushed = markAgentRunPushed(run, now);
1853
1950
  store.saveAgentRun(pushed);
1854
1951
  markTaskRoleRunPushedInFlight(store, role, pushed, now);
1855
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "run.pushed", runLaunchEventPayload(pushed), now));
1952
+ store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "run.pushed", runLaunchEventPayload(pushed), now));
1856
1953
  }
1857
- completeRuntimeHookReservation(store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, input.launchId);
1954
+ completeRuntimeHookReservation(store, { scope: "task", taskId, roleName: input.fence.roleName }, input.fence.launchId);
1858
1955
  }
1859
- // Session-started/ready/bind facts are recorded provider-neutrally as
1860
- // an auditable event. The
1861
- // payload carries the generation fence + preInputReady so the fresh
1862
- // push gate can read the exact provider-ready fact for this generation.
1863
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-session-lifecycle", {
1864
- eventId: input.eventId,
1865
- roleName: input.roleName,
1866
- agentId: input.agentId,
1867
- adapterId: input.adapterId,
1868
- launchId: input.launchId,
1869
- nativeSessionId: input.nativeSessionId,
1870
- outcome: decision.outcome.outcome,
1871
- preInputReady: (decision.outcome.outcome === "mark-ready"
1872
- && decision.outcome.preInputReady) ? "true" : "false",
1873
- ...(input.runId === undefined ? {} : { runId: input.runId })
1874
- }, now));
1875
1956
  return "applied";
1957
+ }
1876
1958
  }
1877
1959
  });
1878
1960
  }
1879
- /**
1880
- * Folds a provider UserPromptSubmit acceptance fact. This is the ONLY path
1881
- * that may advance a Run to accepted/delivered, and only after an exact
1882
- * transport receipt has independently committed run.pushed.
1883
- */
1884
- observeProviderPromptAccepted(input, now = new Date()) {
1961
+ /** Provider acceptance is canonical before storage and remains receipt-fenced. */
1962
+ observeRuntimePromptAccepted(input, adapterId, now) {
1885
1963
  return this.store.transaction((store) => {
1886
- const decision = this.foldProviderSignal(store, {
1887
- kind: "native-prompt-submit",
1888
- fence: providerFence(input)
1889
- }, input.runId);
1964
+ const event = createCanonicalLifecycleEvent({
1965
+ phase: "provider-accepted",
1966
+ source: "provider-native",
1967
+ evidence: "provider-native-durable",
1968
+ fence: runtimeObservationLifecycleFence(input, adapterId)
1969
+ });
1970
+ const decision = this.foldRuntimeLifecycleEvent(store, event, input);
1890
1971
  if (decision.kind === "obsolete") {
1891
- recordCanonicalObsolete(store, input, "native-prompt-accepted", decision.reason, now);
1972
+ recordCanonicalObservationObsolete(store, input, decision.reason, now);
1892
1973
  return "obsolete";
1893
1974
  }
1894
1975
  if (decision.kind === "deferred")
1895
1976
  return "deferred";
1896
1977
  if (decision.kind === "idempotent")
1897
1978
  return "applied";
1898
- // decision.kind === "apply" && outcome advance-accepted
1899
- const active = store.getActiveAgentRun(input.taskId, input.roleName);
1900
- if (active === null || active.id !== input.runId || active.status !== "active") {
1901
- recordCanonicalObsolete(store, input, "native-prompt-accepted", "run-not-active", now);
1979
+ const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
1980
+ if (active === null
1981
+ || active.id !== input.fence.runId
1982
+ || active.status !== "active") {
1983
+ recordCanonicalObservationObsolete(store, input, "run-not-active", now);
1902
1984
  return "obsolete";
1903
1985
  }
1904
- const role = store.getRole(input.taskId, input.roleName);
1986
+ const role = store.getRole(input.fence.taskId, input.fence.roleName);
1905
1987
  if (role === null) {
1906
- recordCanonicalObsolete(store, input, "native-prompt-accepted", "role-missing", now);
1988
+ recordCanonicalObservationObsolete(store, input, "role-missing", now);
1907
1989
  return "obsolete";
1908
1990
  }
1909
1991
  if (active.deliveredAt === undefined) {
1910
1992
  const delivered = markAgentRunDelivered(active, now);
1911
1993
  store.saveAgentRun(delivered);
1912
1994
  markTaskRoleRunDeliveredInFlight(store, role, delivered, now);
1913
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "run.delivered", runLaunchEventPayload(delivered), now));
1995
+ store.saveEvent(input.fence.taskId, createTaskEvent(store.nextEventId(input.fence.taskId), input.fence.taskId, "run.delivered", runLaunchEventPayload(delivered), now));
1914
1996
  }
1915
1997
  return "applied";
1916
1998
  });
1917
1999
  }
1918
- /**
1919
- * Folds provider-native in-turn progress through the canonical lifecycle
1920
- * contract. Progress is an advisory semantic fence only: it never advances
1921
- * Run delivery/acceptance and never reads pane text or process counters.
1922
- */
1923
- observeProviderTurnProgress(input, now = new Date()) {
1924
- return this.store.transaction((store) => {
1925
- const receivedAtMs = Date.parse(input.receivedAt);
1926
- if (!Number.isFinite(receivedAtMs) || receivedAtMs > now.getTime()) {
1927
- recordCanonicalObsolete(store, input, "native-turn-progress", "received-at-invalid", now);
1928
- return "obsolete";
1929
- }
1930
- const decision = this.foldProviderSignal(store, {
1931
- kind: "native-turn-progress",
1932
- ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
1933
- fence: providerFence(input)
1934
- }, input.runId);
1935
- switch (decision.kind) {
1936
- case "obsolete":
1937
- recordCanonicalObsolete(store, input, "native-turn-progress", decision.reason, now);
1938
- return "obsolete";
1939
- case "deferred":
1940
- return "deferred";
1941
- case "idempotent":
1942
- return "applied";
1943
- case "apply":
1944
- if (decision.outcome.outcome !== "advance-progress")
1945
- return "obsolete";
1946
- return routeProviderProgress({
1947
- mode: this.telemetry?.mode ?? "legacy",
1948
- entry: toTelemetryEntry(input),
1949
- semanticExists: this.telemetry?.mode === "bounded"
1950
- ? false
1951
- : store.listEvents(input.taskId).some((event) => (event.type === "runtime.provider-turn-progress"
1952
- && event.payload.eventId === input.eventId)),
1953
- writeSemantic: () => {
1954
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-turn-progress", {
1955
- eventId: input.eventId,
1956
- roleName: input.roleName,
1957
- agentId: input.agentId,
1958
- adapterId: input.adapterId,
1959
- launchId: input.launchId,
1960
- nativeSessionId: input.nativeSessionId,
1961
- runId: input.runId,
1962
- progressId: input.progressId,
1963
- progressAt: input.receivedAt,
1964
- ...(input.sequence === undefined ? {} : { sequence: String(input.sequence) })
1965
- }, now));
1966
- },
1967
- sink: this.telemetry?.sink ?? NULL_TELEMETRY_SINK
1968
- });
1969
- }
1970
- });
1971
- }
1972
- /**
1973
- * Shared projection + canonical fold for provider-native lifecycle signals.
1974
- * Reads current durable Run/session state into a CanonicalRunExpectation,
1975
- * maps the neutral signal through the per-adapter registry, and folds. The
1976
- * fold owns every ordering/identity/evidence decision; this method only reads
1977
- * durable truth and never branches on the provider name.
1978
- */
1979
- foldProviderSignal(store, signal, runId) {
1980
- let event;
1981
- try {
1982
- event = mapNativeLifecycleSignal(signal);
1983
- }
1984
- catch (error) {
1985
- return { kind: "obsolete", reason: error instanceof Error ? error.message : "map-failed" };
1986
- }
1987
- const expectation = this.projectRunExpectation(store, signal.fence, runId);
2000
+ foldRuntimeLifecycleEvent(store, event, observation) {
2001
+ const expectation = this.projectRunExpectation(store, event.fence, observation.fence.runId);
1988
2002
  if (expectation === null) {
1989
- if (preallocatedClaudeStartupAwaitingProjection(store, signal, runId)) {
1990
- return { kind: "deferred", reason: "preallocated-session-not-yet-projected" };
1991
- }
1992
- return { kind: "obsolete", reason: "run-or-role-missing" };
2003
+ return preallocatedRuntimeReadyAwaitingProjection(store, observation, this.drivers)
2004
+ ? { kind: "apply", outcome: { outcome: "mark-ready", preInputReady: true } }
2005
+ : { kind: "obsolete", reason: "run-or-role-missing" };
1993
2006
  }
1994
2007
  const outcome = foldCanonicalLifecycleEvent(event, expectation);
1995
2008
  switch (outcome.outcome) {
@@ -2047,16 +2060,23 @@ export class FileSchedulerStoreAdapter {
2047
2060
  receiptId: inFlight.receiptId,
2048
2061
  ...(boundNativeSessionId === undefined ? {} : { nativeSessionId: boundNativeSessionId })
2049
2062
  };
2050
- const lifecycleEvents = store.listEvents(fence.taskId).filter((event) => (event.type === "runtime.provider-session-lifecycle"
2051
- && event.payload.roleName === fence.roleName
2052
- && event.payload.agentId === run.effective.agentId
2053
- && event.payload.adapterId === run.effective.adapterId
2054
- && event.payload.launchId === launchId
2055
- && event.payload.nativeSessionId === (boundNativeSessionId ?? fence.nativeSessionId)));
2063
+ const driverId = this.drivers.requireByAdapterId(run.effective.adapterId).id;
2064
+ const lifecycleEvents = store.listEvents(fence.taskId).flatMap((event) => {
2065
+ const observation = runtimeObservationFromTaskEvent(event);
2066
+ return observation !== null
2067
+ && (observation.kind === "session.started" || observation.kind === "session.ready")
2068
+ && observation.fence.roleName === fence.roleName
2069
+ && observation.fence.agentId === run.effective.agentId
2070
+ && observation.fence.driverId === driverId
2071
+ && observation.fence.launchId === launchId
2072
+ && observation.fence.nativeSessionId === (boundNativeSessionId ?? fence.nativeSessionId)
2073
+ ? [observation]
2074
+ : [];
2075
+ });
2056
2076
  return {
2057
2077
  fence: expectedFence,
2058
2078
  sessionStarted: lifecycleEvents.length > 0,
2059
- ready: lifecycleEvents.some((event) => event.payload.preInputReady === "true"),
2079
+ ready: lifecycleEvents.some((event) => event.kind === "session.ready"),
2060
2080
  pushed: run.pushedAt !== undefined,
2061
2081
  accepted: run.deliveredAt !== undefined,
2062
2082
  terminal: run.status !== "active",
@@ -2142,9 +2162,9 @@ export class FileSchedulerStoreAdapter {
2142
2162
  return "apply";
2143
2163
  }
2144
2164
  }
2145
- function claudeStopFailureSummary(input) {
2165
+ function runtimeTurnFailureSummary(input) {
2146
2166
  return [
2147
- "Claude StopFailure.",
2167
+ "Agent runtime turn failed.",
2148
2168
  `error: ${input.error}`,
2149
2169
  ...(input.errorDetails === undefined
2150
2170
  ? []
@@ -2154,20 +2174,34 @@ function claudeStopFailureSummary(input) {
2154
2174
  : [`last_assistant_message: ${input.lastAssistantMessage}`])
2155
2175
  ].join("\n");
2156
2176
  }
2157
- function providerFence(input) {
2177
+ function runtimeObservationLifecycleFence(input, adapterId) {
2158
2178
  return {
2159
- taskId: input.taskId,
2160
- roleName: input.roleName,
2161
- agentId: input.agentId,
2162
- adapterId: input.adapterId,
2163
- launchId: input.launchId,
2164
- nativeSessionId: input.nativeSessionId,
2165
- ...(input.runId === undefined ? {} : { runId: input.runId }),
2166
- ...(input.receiptId === undefined ? {} : { receiptId: input.receiptId })
2179
+ taskId: input.fence.taskId,
2180
+ roleName: input.fence.roleName,
2181
+ agentId: input.fence.agentId,
2182
+ adapterId,
2183
+ launchId: input.fence.launchId,
2184
+ ...(input.fence.runId === undefined ? {} : { runId: input.fence.runId }),
2185
+ ...(input.fence.nativeSessionId === undefined
2186
+ ? {}
2187
+ : { nativeSessionId: input.fence.nativeSessionId }),
2188
+ ...(input.fence.receiptId === undefined ? {} : { receiptId: input.fence.receiptId })
2167
2189
  };
2168
2190
  }
2169
- function recordCanonicalObsolete(store, input, eventType, reason, now) {
2170
- recordObsoleteRuntimeEvent(store, { ...input, eventType }, reason, now);
2191
+ function recordCanonicalObservationObsolete(store, input, reason, now) {
2192
+ const taskId = input.fence.taskId;
2193
+ if (taskId === undefined)
2194
+ return;
2195
+ recordObsoleteRuntimeEvent(store, {
2196
+ eventId: input.eventId,
2197
+ eventType: input.kind,
2198
+ taskId,
2199
+ roleName: input.fence.roleName,
2200
+ agentId: input.fence.agentId,
2201
+ ...(input.fence.runId === undefined ? {} : { runId: input.fence.runId }),
2202
+ launchId: input.fence.launchId,
2203
+ nativeSessionId: input.fence.nativeSessionId ?? input.fence.sessionGenerationId
2204
+ }, reason, now);
2171
2205
  }
2172
2206
  function recordObsoleteRuntimeEvent(store, input, reason, now) {
2173
2207
  if (store.listEvents(input.taskId).some((event) => (event.type === "runtime.event-obsolete"
@@ -2219,10 +2253,6 @@ function sessionPreview(value) {
2219
2253
  ? truncated.slice(0, -1)
2220
2254
  : truncated;
2221
2255
  }
2222
- function boundedRuntimeNote(value) {
2223
- const normalized = value.trim().replaceAll(/\s+/gu, " ");
2224
- return normalized.length <= 280 ? normalized : `${normalized.slice(0, 279)}…`;
2225
- }
2226
2256
  function requireRuntimeLaunchReservation(store, owner, launchId) {
2227
2257
  const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
2228
2258
  if (!isRuntimeLaunchReservation(mailbox?.processing, launchId)) {
@@ -2238,50 +2268,43 @@ function runtimeHookMatchesReservation(store, owner, launchId) {
2238
2268
  return launchId !== undefined
2239
2269
  && isRuntimeLaunchReservation(processing, launchId);
2240
2270
  }
2241
- /**
2242
- * Claude may publish SessionStart(startup) synchronously from host.start(),
2243
- * before the Controller can atomically project the returned Session and bind
2244
- * the Run. Preserve that exact immutable fact only while the matching launch
2245
- * reservation is still the durable owner of the active Run. Everything else
2246
- * remains obsolete/fail-closed.
2247
- */
2248
- function preallocatedClaudeStartupAwaitingProjection(store, signal, runId) {
2249
- if (signal.kind !== "native-session-start"
2250
- || signal.sessionSource !== "startup"
2251
- || signal.fence.adapterId !== "claude"
2271
+ function preallocatedRuntimeReadyAwaitingProjection(store, observation, drivers) {
2272
+ const taskId = observation.fence.taskId;
2273
+ const runId = observation.fence.runId;
2274
+ const nativeSessionId = observation.fence.nativeSessionId;
2275
+ const driver = drivers.find(observation.fence.driverId);
2276
+ if (observation.kind !== "session.ready"
2277
+ || driver?.capabilities.observation.sessionBootstrap !== "preallocated"
2278
+ || taskId === undefined
2252
2279
  || runId === undefined
2253
- || signal.fence.runId !== runId
2254
- || signal.fence.launchId === undefined
2255
- || signal.fence.nativeSessionId === undefined)
2280
+ || nativeSessionId === undefined)
2256
2281
  return false;
2257
- const task = store.getTask(signal.fence.taskId);
2258
- const role = store.getRole(signal.fence.taskId, signal.fence.roleName);
2259
- const run = store.getActiveAgentRun(signal.fence.taskId, signal.fence.roleName);
2260
- const sessions = store.getTaskRoleSessionSet(signal.fence.taskId, signal.fence.roleName);
2261
- if (task === null
2262
- || task.status !== "active"
2263
- || role === null
2264
- || role.activeAgentId !== signal.fence.agentId
2265
- || run === null
2266
- || run.id !== runId
2282
+ const task = store.getTask(taskId);
2283
+ const role = store.getRole(taskId, observation.fence.roleName);
2284
+ const run = store.getActiveAgentRun(taskId, observation.fence.roleName);
2285
+ const sessions = store.getTaskRoleSessionSet(taskId, observation.fence.roleName);
2286
+ if (task?.status !== "active"
2287
+ || role?.activeAgentId !== observation.fence.agentId
2288
+ || run?.id !== runId
2267
2289
  || run.status !== "active"
2268
- || run.effective.agentId !== signal.fence.agentId
2269
- || run.effective.adapterId !== "claude"
2290
+ || run.effective.agentId !== observation.fence.agentId
2291
+ || run.effective.adapterId !== driver.adapterId
2292
+ || observation.fence.receiptId !== formatAgentRunReceiptId(taskId, runId)
2270
2293
  || (sessions?.inFlight !== null && sessions?.inFlight !== undefined)
2271
- || sessions?.sessions[signal.fence.agentId] !== undefined)
2294
+ || sessions?.sessions[observation.fence.agentId] !== undefined)
2272
2295
  return false;
2273
2296
  const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
2274
2297
  scope: "task",
2275
- taskId: signal.fence.taskId,
2276
- roleName: signal.fence.roleName
2298
+ taskId,
2299
+ roleName: observation.fence.roleName
2277
2300
  }));
2278
2301
  const executionRef = mailbox?.processing?.executionRef;
2279
- return isRuntimeLaunchReservation(mailbox?.processing, signal.fence.launchId)
2302
+ return isRuntimeLaunchReservation(mailbox?.processing, observation.fence.launchId)
2280
2303
  && !hasRuntimeCleanupObligation(mailbox)
2281
2304
  && executionRef?.type === "run"
2282
- && executionRef.taskId === signal.fence.taskId
2305
+ && executionRef.taskId === taskId
2283
2306
  && executionRef.id === runId
2284
- && signal.fence.nativeSessionId === nativeSessionIdForLaunch(store.rootDirectory(), signal.fence.launchId, signal.fence.agentId, "claude");
2307
+ && nativeSessionId === nativeSessionIdForLaunch(store.rootDirectory(), observation.fence.launchId, observation.fence.agentId, driver.adapterId);
2285
2308
  }
2286
2309
  function completeRuntimeHookReservation(store, owner, launchId) {
2287
2310
  if (launchId === undefined)
@@ -2734,31 +2757,142 @@ function runLaunchEventPayload(run) {
2734
2757
  writeProjectIds: run.effective.writeProjectIds.join(",") || "none"
2735
2758
  };
2736
2759
  }
2737
- /**
2738
- * Maps a provider progress observation to the telemetry sidecar entry. The
2739
- * payload mirrors the semantic event payload so sidecar rows and legacy
2740
- * events carry identical evidence.
2741
- */
2742
- function toTelemetryEntry(input) {
2760
+ function runtimeObservationTelemetryEntry(input) {
2743
2761
  return {
2744
- taskId: input.taskId,
2745
- roleName: input.roleName,
2746
- runId: input.runId,
2747
- generation: input.launchId !== "" ? input.launchId : input.nativeSessionId,
2748
- progressId: input.progressId,
2762
+ taskId: input.fence.taskId,
2763
+ roleName: input.fence.roleName,
2764
+ runId: input.fence.runId,
2765
+ generation: input.fence.sessionGenerationId,
2766
+ progressId: [
2767
+ input.kind,
2768
+ input.payload.operationId ?? input.payload.activity ?? "state"
2769
+ ].join(":"),
2749
2770
  ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
2750
2771
  payload: {
2751
2772
  eventId: input.eventId,
2752
- roleName: input.roleName,
2753
- agentId: input.agentId,
2754
- adapterId: input.adapterId,
2755
- launchId: input.launchId,
2756
- nativeSessionId: input.nativeSessionId,
2757
- runId: input.runId,
2758
- progressId: input.progressId,
2759
- progressAt: input.receivedAt,
2760
- ...(input.sequence === undefined ? {} : { sequence: String(input.sequence) })
2773
+ kind: input.kind,
2774
+ authority: input.authority,
2775
+ driverId: input.fence.driverId,
2776
+ launchId: input.fence.launchId,
2777
+ nativeSessionId: input.fence.nativeSessionId ?? "",
2778
+ nativeTurnId: input.fence.nativeTurnId ?? "",
2779
+ ...(input.payload.operation === undefined
2780
+ ? {}
2781
+ : { operation: input.payload.operation }),
2782
+ ...(input.payload.activity === undefined
2783
+ ? {}
2784
+ : { activity: input.payload.activity }),
2785
+ ...(input.payload.usage === undefined
2786
+ ? {}
2787
+ : { usage: JSON.stringify(input.payload.usage) })
2761
2788
  },
2762
2789
  receivedAt: input.receivedAt
2763
2790
  };
2764
2791
  }
2792
+ /**
2793
+ * Runtime observations are a current-state snapshot, not an append-only
2794
+ * semantic history. Retain active operations, one latest usage baseline, and
2795
+ * one latest confirmed activity boundary so a token stream remains
2796
+ * O(active operations) per Run without losing the delta that was already
2797
+ * proven before an unchanged later snapshot arrived.
2798
+ */
2799
+ function compactedRuntimeObservationIds(events, incoming) {
2800
+ const existing = events.flatMap((event) => {
2801
+ const observation = runtimeObservationFromTaskEvent(event);
2802
+ return observation !== null
2803
+ && runtimeObservationFenceMatches(observation.fence, incoming.fence)
2804
+ ? [{ event, observation }]
2805
+ : [];
2806
+ });
2807
+ const remove = ({ observation }) => {
2808
+ if (incoming.kind === "activity.observed") {
2809
+ return observation.kind === "activity.observed"
2810
+ && (observation.payload.usage === undefined)
2811
+ === (incoming.payload.usage === undefined);
2812
+ }
2813
+ if (incoming.kind === "operation.started") {
2814
+ return (observation.kind === "operation.started"
2815
+ && observation.payload.operationId === incoming.payload.operationId)
2816
+ || observation.kind === "operation.completed"
2817
+ || observation.kind === "operation.failed";
2818
+ }
2819
+ if (incoming.kind === "operation.completed" || incoming.kind === "operation.failed") {
2820
+ return (observation.kind.startsWith("operation.")
2821
+ && observation.payload.operationId === incoming.payload.operationId)
2822
+ || observation.kind === "operation.completed"
2823
+ || observation.kind === "operation.failed";
2824
+ }
2825
+ if (incoming.kind === "turn.waiting")
2826
+ return observation.kind === "turn.waiting";
2827
+ if (incoming.kind === "observer.health") {
2828
+ return observation.kind === "observer.health"
2829
+ && observation.payload.sourceId === incoming.payload.sourceId;
2830
+ }
2831
+ if (["turn.completed", "turn.failed", "turn.cancelled"].includes(incoming.kind)) {
2832
+ return observation.kind.startsWith("operation.")
2833
+ || observation.kind === "turn.waiting"
2834
+ || observation.kind === "turn.completed"
2835
+ || observation.kind === "turn.failed"
2836
+ || observation.kind === "turn.cancelled";
2837
+ }
2838
+ if (incoming.kind === "turn.accepted")
2839
+ return observation.kind === "turn.accepted";
2840
+ if (incoming.kind.startsWith("session."))
2841
+ return observation.kind.startsWith("session.");
2842
+ return false;
2843
+ };
2844
+ return existing.filter(remove).map(({ event }) => event.id);
2845
+ }
2846
+ function confirmedUsageActivityObservation(events, incoming) {
2847
+ const usage = incoming.payload.usage;
2848
+ if (incoming.kind !== "activity.observed" || usage === undefined)
2849
+ return null;
2850
+ const previous = events
2851
+ .map(runtimeObservationFromTaskEvent)
2852
+ .filter((observation) => (observation !== null
2853
+ && observation.kind === "activity.observed"
2854
+ && observation.payload.usage !== undefined
2855
+ && runtimeObservationFenceMatches(observation.fence, incoming.fence)))
2856
+ .sort(compareCanonicalObservationOrder)
2857
+ .at(-1)?.payload.usage;
2858
+ if (previous === undefined
2859
+ || usage.inputTokens + usage.outputTokens
2860
+ <= previous.inputTokens + previous.outputTokens)
2861
+ return null;
2862
+ const digest = createHash("sha256")
2863
+ .update(`confirmed-runtime-activity\0${incoming.eventId}`)
2864
+ .digest("hex");
2865
+ return createRuntimeObservation({
2866
+ schemaVersion: 1,
2867
+ eventId: `runtime-activity-${digest}`,
2868
+ kind: "activity.observed",
2869
+ authority: "controller",
2870
+ receivedAt: incoming.receivedAt,
2871
+ ...(incoming.observedAt === undefined ? {} : { observedAt: incoming.observedAt }),
2872
+ ...(incoming.sequence === undefined ? {} : { sequence: incoming.sequence }),
2873
+ ...(incoming.ordinal === undefined ? {} : { ordinal: incoming.ordinal }),
2874
+ fence: incoming.fence,
2875
+ payload: { activity: incoming.payload.activity ?? "model" }
2876
+ });
2877
+ }
2878
+ function usageSnapshotIsSuperseded(events, incoming) {
2879
+ if (incoming.kind !== "activity.observed" || incoming.payload.usage === undefined) {
2880
+ return false;
2881
+ }
2882
+ return events
2883
+ .map(runtimeObservationFromTaskEvent)
2884
+ .some((observation) => observation !== null
2885
+ && observation.kind === "activity.observed"
2886
+ && observation.payload.usage !== undefined
2887
+ && runtimeObservationFenceMatches(observation.fence, incoming.fence)
2888
+ && observationIsStrictlyNewer(observation, incoming));
2889
+ }
2890
+ function observationIsStrictlyNewer(left, right) {
2891
+ return compareCanonicalObservationOrder(left, right) > 0;
2892
+ }
2893
+ function compareCanonicalObservationOrder(left, right) {
2894
+ return left.receivedAt.localeCompare(right.receivedAt)
2895
+ || (left.sequence ?? -1) - (right.sequence ?? -1)
2896
+ || (left.ordinal ?? -1) - (right.ordinal ?? -1)
2897
+ || left.eventId.localeCompare(right.eventId);
2898
+ }