@zq-silk/yui 0.6.9 → 0.6.11

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 (56) hide show
  1. package/README.md +10 -3
  2. package/dist/cli/commandCatalog.js +1 -1
  3. package/dist/commands/taskCommands.js +62 -27
  4. package/dist/commands/taskContextCommand.js +27 -1
  5. package/dist/commands/taskRoleRuntimeStatus.js +17 -6
  6. package/dist/controller/agentRuntimeObserver.js +6 -3
  7. package/dist/controller/controller.js +29 -47
  8. package/dist/controller/fileSchedulerStoreAdapter.js +546 -255
  9. package/dist/controller/runtime.js +8 -1
  10. package/dist/controller/runtimeEventInbox.js +16 -5
  11. package/dist/controller/runtimeHookRunFence.js +51 -5
  12. package/dist/controller/runtimeObservationHook.js +8 -2
  13. package/dist/coordination/workMailbox.js +408 -28
  14. package/dist/coordination/workMailboxQueue.js +12 -10
  15. package/dist/executor/agentExecutor.js +102 -94
  16. package/dist/executor/executorRegistry.js +47 -2
  17. package/dist/executor/fileRoleLaunchPlanner.js +4 -2
  18. package/dist/lifecycle/exactRunTerminalization.js +1 -7
  19. package/dist/repository/taskWorkspaceCoordinator.js +9 -4
  20. package/dist/runtime/agentDriver.js +83 -4
  21. package/dist/runtime/agentDriverObservation.js +25 -10
  22. package/dist/runtime/builtinAgentDrivers.js +168 -18
  23. package/dist/runtime/codexAppServerRuntime.js +355 -0
  24. package/dist/runtime/continuationManager.js +117 -0
  25. package/dist/runtime/index.js +2 -0
  26. package/dist/runtime/lifecycleReservation.js +4 -3
  27. package/dist/runtime/promptEnvelope.js +14 -3
  28. package/dist/runtime/providerContinuation.js +225 -0
  29. package/dist/runtime/providerContinuationReconciliationService.js +172 -0
  30. package/dist/runtime/providerRuntimeIdentity.js +232 -0
  31. package/dist/runtime/providerRuntimeReconciler.js +166 -0
  32. package/dist/runtime/runtimeContinuationProjection.js +34 -0
  33. package/dist/runtime/runtimeObservation.js +198 -9
  34. package/dist/runtime/runtimeProjection.js +162 -7
  35. package/dist/scheduler/activeRoleRunDelivery.js +314 -1
  36. package/dist/scheduler/leaderWakeupProcessor.js +1 -1
  37. package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
  38. package/dist/scheduler/roleRunLiveness.js +8 -7
  39. package/dist/scheduler/roleRunStall.js +4 -2
  40. package/dist/scheduler/taskExecutionProjection.js +2 -2
  41. package/dist/storage/migration/productionRegistry.js +474 -1
  42. package/dist/storage/sqliteSchema.js +102 -21
  43. package/dist/storage/sqliteStore.js +52 -110
  44. package/dist/storage/storageVersions.js +1 -1
  45. package/dist/storage/storeRpc.js +0 -1
  46. package/dist/storage/taskStore.js +40 -53
  47. package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
  48. package/dist/web/assets/client/app.js +1 -1
  49. package/dist/web/assets/client/components.js +233 -4
  50. package/dist/web/assets/client/i18n.js +166 -2
  51. package/dist/web/assets/client/view.js +30 -13
  52. package/dist/web/assets/styles/cards.js +62 -0
  53. package/dist/web/assets/styles/widgets.js +1 -0
  54. package/dist/web/webSnapshot.js +11 -2
  55. package/package.json +1 -1
  56. package/skills/yui-leader/SKILL.md +14 -9
@@ -1,6 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { hasRecentTurnId, rememberRecentTurnId, validatePendingTurnCompletion, validateRecentTurnIds } from "./turnCompletion.js";
3
3
  import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
4
+ import { validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
5
+ import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
4
6
  export function createRoleSessionSet(owner, activeAgentId, now) {
5
7
  const base = {
6
8
  owner: normalizeOwner(owner),
@@ -12,9 +14,9 @@ export function createRoleSessionSet(owner, activeAgentId, now) {
12
14
  ? { ...base, schemaVersion: 3 }
13
15
  : {
14
16
  ...base,
15
- schemaVersion: 4,
17
+ schemaVersion: 5,
16
18
  inFlight: null,
17
- pendingTurnCompletion: null
19
+ providerBinding: null
18
20
  };
19
21
  }
20
22
  export function activeRoleAgentSession(set) {
@@ -157,8 +159,7 @@ export function updateRoleAgentSessionStatus(set, agentId, status, now) {
157
159
  }
158
160
  export function retireTaskRoleSessionsForWorkspace(set, now) {
159
161
  validateRoleSessionSet(set);
160
- if (set.inFlight !== null
161
- || set.pendingTurnCompletion !== null) {
162
+ if (set.inFlight !== null) {
162
163
  throw new Error("Cannot retire a Task Role session with unsettled Run state.");
163
164
  }
164
165
  const live = Object.values(set.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
@@ -183,7 +184,7 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
183
184
  */
184
185
  export function retireConfirmedAbsentInactiveTaskRolePlaceholders(set, now) {
185
186
  validateRoleSessionSet(set);
186
- if (set.inFlight !== null || set.pendingTurnCompletion !== null) {
187
+ if (set.inFlight !== null) {
187
188
  throw new Error("Cannot retire a Task Role placeholder with unsettled Run state.");
188
189
  }
189
190
  const timestamp = now.toISOString();
@@ -278,9 +279,6 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
278
279
  if (normalized.agentId !== set.activeAgentId) {
279
280
  throw new Error("Task Role Run Agent does not match the active Agent.");
280
281
  }
281
- if (set.pendingTurnCompletion !== null) {
282
- throw new Error("Task Role session set has an unsettled Turn completion.");
283
- }
284
282
  if (set.inFlight !== null) {
285
283
  if (sameRunFence(set.inFlight, normalized))
286
284
  return set;
@@ -294,6 +292,38 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
294
292
  };
295
293
  return validateRoleSessionSet(updated);
296
294
  }
295
+ export function bindTaskRoleProviderRuntime(set, binding, updatedAt) {
296
+ validateRoleSessionSet(set);
297
+ const normalized = validateProviderRuntimeBinding(binding);
298
+ if (set.inFlight === null || set.inFlight.runId !== normalized.runId) {
299
+ throw new Error("Provider Runtime Binding does not match the in-flight Run.");
300
+ }
301
+ if (set.providerBinding !== null) {
302
+ if (JSON.stringify(set.providerBinding) === JSON.stringify(normalized))
303
+ return set;
304
+ throw new Error("Task Role already has a Provider Runtime Binding.");
305
+ }
306
+ return validateRoleSessionSet({
307
+ ...set,
308
+ providerBinding: normalized,
309
+ updatedAt: requireDate(updatedAt, "Provider Runtime Binding timestamp")
310
+ });
311
+ }
312
+ export function updateTaskRoleProviderRuntime(set, binding, updatedAt) {
313
+ validateRoleSessionSet(set);
314
+ const normalized = validateProviderRuntimeBinding(binding);
315
+ if (set.providerBinding === null
316
+ || normalized.runId !== set.providerBinding.runId
317
+ || normalized.providerNamespace !== set.providerBinding.providerNamespace
318
+ || normalized.accountScope !== set.providerBinding.accountScope) {
319
+ throw new Error("Provider Runtime Binding identity cannot change in place.");
320
+ }
321
+ return validateRoleSessionSet({
322
+ ...set,
323
+ providerBinding: normalized,
324
+ updatedAt: requireDate(updatedAt, "Provider Runtime Binding timestamp")
325
+ });
326
+ }
297
327
  export function markTaskRoleRunPushed(set, fence, pushedAt) {
298
328
  validateRoleSessionSet(set);
299
329
  assertTaskRoleSessionSet(set);
@@ -332,38 +362,55 @@ export function markTaskRoleRunDelivered(set, fence, deliveredAt) {
332
362
  };
333
363
  return validateRoleSessionSet(updated);
334
364
  }
335
- export function recordObservedTaskRoleCompletion(set, completion) {
365
+ /**
366
+ * Records a provider Turn boundary without changing the durable Yui Run.
367
+ *
368
+ * A native session may finish one foreground Turn while provider-owned
369
+ * subagents, mailbox work, or later user corrections still belong to the same
370
+ * application-level Run. Only an explicit Yui workflow outcome may clear the
371
+ * Run fence; this transition merely makes the native session available for a
372
+ * subsequent input and remembers the provider Turn idempotently.
373
+ */
374
+ export function recordTaskRoleTurnBoundary(set, input, completedAt) {
336
375
  validateRoleSessionSet(set);
337
376
  assertTaskRoleSessionSet(set);
338
- const observed = validatePendingTurnCompletion(completion);
339
- assertCompletionOwner(set, observed);
340
- const session = set.sessions[observed.agentId];
341
- if (session !== undefined && session.nativeSessionId !== observed.nativeSessionId) {
342
- throw new Error("Observed Turn native session does not match the Role Agent session.");
377
+ const agentId = requireSafeIdentity(input.agentId, "Agent id");
378
+ const nativeSessionId = requireText(input.nativeSessionId, "Native session id");
379
+ const turnId = requireSafeIdentity(input.turnId, "Turn id");
380
+ const session = set.sessions[agentId];
381
+ if (session === undefined || session.nativeSessionId !== nativeSessionId) {
382
+ throw new Error("Completed Turn has no matching Role Agent native session.");
343
383
  }
344
- if (session?.recentCompletedTurnIds.includes(observed.turnId) === true)
384
+ if (session.recentCompletedTurnIds.includes(turnId))
345
385
  return set;
346
- if (set.pendingTurnCompletion !== null) {
347
- if (samePendingTurnCompletion(set.pendingTurnCompletion, observed))
348
- return set;
349
- throw new Error("Task Role session set already has a pending Turn completion.");
350
- }
351
- const inFlight = set.inFlight;
352
- if (inFlight === null) {
353
- throw new Error("Observed Turn has no matching in-flight Run.");
354
- }
355
- if (inFlight.agentId !== observed.agentId || inFlight.runId !== observed.runId) {
356
- throw new Error("Observed Turn Run does not match the in-flight Run.");
386
+ if (set.inFlight !== null && set.inFlight.agentId !== agentId) {
387
+ throw new Error("Completed Turn Agent does not match the in-flight Run.");
357
388
  }
358
- if (inFlight.pushedAt === undefined) {
359
- throw new Error("Observed Turn Run must be pushed before completion is recorded.");
360
- }
361
- const updated = {
389
+ const timestamp = requireDate(completedAt, "Turn completedAt");
390
+ const status = session.status === "stopped" || session.status === "broken"
391
+ ? session.status
392
+ : "ready";
393
+ return validateRoleSessionSet({
362
394
  ...set,
363
- pendingTurnCompletion: observed,
364
- updatedAt: observed.observedAt
365
- };
366
- return validateRoleSessionSet(updated);
395
+ sessions: {
396
+ ...set.sessions,
397
+ [agentId]: {
398
+ ...session,
399
+ status,
400
+ recentCompletedTurnIds: rememberRecentTurnId(session.recentCompletedTurnIds, turnId),
401
+ updatedAt: timestamp
402
+ }
403
+ },
404
+ updatedAt: timestamp
405
+ });
406
+ }
407
+ export function recordObservedTaskRoleCompletion(set, completion) {
408
+ const observed = validatePendingTurnCompletion(completion);
409
+ return recordTaskRoleTurnBoundary(set, {
410
+ agentId: observed.agentId,
411
+ nativeSessionId: observed.nativeSessionId,
412
+ turnId: observed.turnId
413
+ }, new Date(observed.observedAt));
367
414
  }
368
415
  export function clearTaskRoleRun(set, fence, clearedAt) {
369
416
  validateRoleSessionSet(set);
@@ -373,23 +420,10 @@ export function clearTaskRoleRun(set, fence, clearedAt) {
373
420
  if (Date.parse(timestamp) < Date.parse(inFlight.preparedAt)) {
374
421
  throw new Error("Task Role Run clearedAt must not be earlier than preparedAt.");
375
422
  }
376
- const pending = set.pendingTurnCompletion;
377
- const session = set.sessions[inFlight.agentId];
378
- const sessions = pending !== null && session !== undefined
379
- ? {
380
- ...set.sessions,
381
- [inFlight.agentId]: {
382
- ...session,
383
- recentCompletedTurnIds: rememberRecentTurnId(session.recentCompletedTurnIds, pending.turnId),
384
- updatedAt: timestamp
385
- }
386
- }
387
- : set.sessions;
388
423
  const updated = {
389
424
  ...set,
390
- sessions,
391
425
  inFlight: null,
392
- pendingTurnCompletion: null,
426
+ providerBinding: null,
393
427
  updatedAt: timestamp
394
428
  };
395
429
  return validateRoleSessionSet(updated);
@@ -402,18 +436,9 @@ export function clearTaskRoleRun(set, fence, clearedAt) {
402
436
  export function terminalizeTaskRoleRunSession(set, fence, terminalAt) {
403
437
  validateRoleSessionSet(set);
404
438
  const inFlight = set.inFlight;
405
- const pending = set.pendingTurnCompletion;
406
- let updated = pending !== null
407
- && pending.agentId === fence.agentId
408
- && pending.runId === fence.runId
409
- ? settleTaskRoleCompletion(set, {
410
- agentId: fence.agentId,
411
- runId: fence.runId,
412
- turnId: pending.turnId
413
- }, terminalAt)
414
- : inFlight === null
415
- ? set
416
- : clearTaskRoleRun(set, fence, terminalAt);
439
+ let updated = inFlight === null
440
+ ? set
441
+ : clearTaskRoleRun(set, fence, terminalAt);
417
442
  const session = updated.sessions[updated.activeAgentId];
418
443
  if (session?.status === "running") {
419
444
  updated = updateRoleAgentSessionStatus(updated, updated.activeAgentId, "ready", terminalAt);
@@ -441,7 +466,7 @@ export function resetTaskRoleSession(set, now) {
441
466
  sessions,
442
467
  ...(history === undefined ? {} : { history }),
443
468
  inFlight: null,
444
- pendingTurnCompletion: null,
469
+ providerBinding: null,
445
470
  updatedAt: timestamp
446
471
  });
447
472
  }
@@ -451,38 +476,27 @@ export function settleTaskRoleCompletion(set, expected, settledAt) {
451
476
  const agentId = requireSafeIdentity(expected.agentId, "Agent id");
452
477
  const runId = requireSafeIdentity(expected.runId, "Run id");
453
478
  const turnId = requireSafeIdentity(expected.turnId, "Turn id");
454
- const pending = set.pendingTurnCompletion;
455
- if (pending === null)
456
- throw new Error("Task Role session set has no pending Turn completion.");
457
- if (pending.agentId !== agentId
458
- || pending.runId !== runId
459
- || pending.turnId !== turnId) {
460
- throw new Error("Pending Turn completion does not match the expected Turn.");
461
- }
462
479
  const inFlight = set.inFlight;
463
480
  if (inFlight === null || inFlight.agentId !== agentId || inFlight.runId !== runId) {
464
481
  throw new Error("Pending Turn completion does not match the in-flight Run.");
465
482
  }
466
483
  const session = set.sessions[agentId];
467
- if (session === undefined || session.nativeSessionId !== pending.nativeSessionId) {
468
- throw new Error("Pending Turn completion has no matching Role Agent native session.");
484
+ if (session === undefined || !session.recentCompletedTurnIds.includes(turnId)) {
485
+ throw new Error("Turn completion has not been observed for the Role Agent session.");
469
486
  }
470
487
  const timestamp = requireDate(settledAt, "Turn settledAt");
471
- if (Date.parse(timestamp) < Date.parse(pending.observedAt)) {
472
- throw new Error("Turn settledAt must not be earlier than observedAt.");
473
- }
474
488
  const updated = {
475
489
  ...set,
476
490
  sessions: {
477
491
  ...set.sessions,
478
492
  [agentId]: {
479
493
  ...session,
480
- recentCompletedTurnIds: rememberRecentTurnId(session.recentCompletedTurnIds, pending.turnId),
494
+ recentCompletedTurnIds: session.recentCompletedTurnIds,
481
495
  updatedAt: timestamp
482
496
  }
483
497
  },
484
498
  inFlight: null,
485
- pendingTurnCompletion: null,
499
+ providerBinding: null,
486
500
  updatedAt: timestamp
487
501
  };
488
502
  return validateRoleSessionSet(updated);
@@ -498,7 +512,7 @@ export function validateRoleSessionSet(set) {
498
512
  throw new Error("Global Role session set schema version is invalid.");
499
513
  }
500
514
  if (Object.hasOwn(set, "inFlight")
501
- || Object.hasOwn(set, "pendingTurnCompletion")) {
515
+ || Object.hasOwn(set, "providerBinding")) {
502
516
  throw new Error("Global Role session set must not contain Task Role lifecycle fields.");
503
517
  }
504
518
  const history = set.history;
@@ -513,11 +527,11 @@ export function validateRoleSessionSet(set) {
513
527
  }
514
528
  }
515
529
  else {
516
- if (set.schemaVersion !== 4) {
530
+ if (set.schemaVersion !== 5) {
517
531
  throw new Error("Task Role session set schema version is invalid.");
518
532
  }
519
533
  if (!Object.hasOwn(set, "inFlight")
520
- || !Object.hasOwn(set, "pendingTurnCompletion")) {
534
+ || !Object.hasOwn(set, "providerBinding")) {
521
535
  throw new Error("Task Role session set must contain its Turn fence.");
522
536
  }
523
537
  const taskSet = set;
@@ -546,28 +560,22 @@ export function validateRoleSessionSet(set) {
546
560
  const inFlight = taskSet.inFlight === null
547
561
  ? null
548
562
  : validateTaskRoleInFlight(taskSet.inFlight);
549
- const pending = taskSet.pendingTurnCompletion === null
563
+ const providerBinding = taskSet.providerBinding === null
550
564
  ? null
551
- : validatePendingTurnCompletion(taskSet.pendingTurnCompletion);
552
- if (inFlight === null && pending !== null) {
553
- throw new Error("Pending Turn completion requires an in-flight Run.");
554
- }
565
+ : validateProviderRuntimeBinding(taskSet.providerBinding);
555
566
  if (inFlight !== null && inFlight.agentId !== set.activeAgentId) {
556
567
  throw new Error("Task Role in-flight Run Agent must be active.");
557
568
  }
558
- if (pending !== null) {
559
- assertCompletionOwner(taskSet, pending);
560
- if (inFlight?.agentId !== pending.agentId
561
- || inFlight.runId !== pending.runId
562
- || inFlight.pushedAt === undefined) {
563
- throw new Error("Pending Turn completion must match a pushed in-flight Run.");
569
+ if (providerBinding !== null) {
570
+ if (inFlight === null || providerBinding.runId !== inFlight.runId) {
571
+ throw new Error("Provider Runtime Binding must match the in-flight Run.");
564
572
  }
565
- const session = taskSet.sessions[pending.agentId];
573
+ const session = taskSet.sessions[inFlight.agentId];
566
574
  if (session === undefined) {
567
- throw new Error("Pending Turn completion has no Role Agent session.");
575
+ throw new Error("Provider Runtime Binding has no active Role Agent session.");
568
576
  }
569
- if (session.nativeSessionId !== pending.nativeSessionId) {
570
- throw new Error("Pending Turn native session does not match the Role Agent session.");
577
+ if (providerBinding.providerNamespace !== builtinDriverIdForAdapter(session.adapterId)) {
578
+ throw new Error("Provider Runtime Binding namespace does not match the Agent adapter.");
571
579
  }
572
580
  }
573
581
  }
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { createPromptEnvelope, createSessionLaunchRequest } from "../runtime/index.js";
2
+ import { createPromptEnvelope, createRuntimeBinding, createSessionLaunchRequest } from "../runtime/index.js";
3
+ import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
3
4
  /**
4
5
  * rr13/test: Test-only liveness seam. Integration tests that spawn a real
5
6
  * Controller subprocess cannot inject a fake TmuxDeliveryPort, and a saved
@@ -25,6 +26,9 @@ export class ExecutorRegistry {
25
26
  this.readiness = readiness;
26
27
  this.runtimePorts = runtimePorts;
27
28
  }
29
+ canRouteProviderInput(adapterId) {
30
+ return adapterId === "codex" && this.runtimePorts?.providerInputRouting !== undefined;
31
+ }
28
32
  async prepareRoleSession(input) {
29
33
  if (input.mode === "resume" && !hasText(input.nativeSessionId)) {
30
34
  throw new Error("Role session resume requires a native session id.");
@@ -173,7 +177,7 @@ export class ExecutorRegistry {
173
177
  envelope: createPromptEnvelope({
174
178
  id: input.receiptId,
175
179
  source: {
176
- kind: "agent-run",
180
+ kind: input.receiptId === formatAgentRunReceiptId(input.delivery.prepared.taskId, runId) ? "agent-run" : "run-input",
177
181
  taskId: input.delivery.prepared.taskId,
178
182
  localId: runId
179
183
  },
@@ -192,6 +196,47 @@ export class ExecutorRegistry {
192
196
  }
193
197
  return outcome;
194
198
  }
199
+ async routeProviderInput(input) {
200
+ const prepared = this.requirePrepared(input.delivery.prepared);
201
+ if (prepared.binding === undefined || this.runtimePorts?.providerInputRouting === undefined) {
202
+ return "unavailable";
203
+ }
204
+ try {
205
+ return await this.runtimePorts.providerInputRouting.route({
206
+ binding: prepared.binding,
207
+ attemptId: input.attemptId,
208
+ mode: input.mode,
209
+ text: input.text,
210
+ fence: input.fence
211
+ });
212
+ }
213
+ finally {
214
+ // A routed mutation is fenced by its durable inputDelivery, not by this
215
+ // process-local preparation. Never let a later Turn reuse a cached
216
+ // Activation binding after this attempt (including an unknown result).
217
+ this.#prepared.delete(input.delivery.prepared.deliveryId);
218
+ }
219
+ }
220
+ async reconcileProviderInput(input) {
221
+ if (this.runtimePorts?.providerInputRouting === undefined) {
222
+ return "unavailable";
223
+ }
224
+ return this.runtimePorts.providerInputRouting.reconcile({
225
+ binding: createRuntimeBinding({
226
+ id: `metadata:${input.taskId}:${input.roleName}:${input.launchId}`,
227
+ launchId: input.launchId,
228
+ owner: { scope: "task", taskId: input.taskId, roleName: input.roleName },
229
+ agentId: input.agentId,
230
+ adapterId: input.adapterId,
231
+ hostRef: "metadata-only",
232
+ hostCreated: false,
233
+ nativeSessionId: input.nativeSessionId
234
+ }),
235
+ attemptId: input.attemptId,
236
+ mode: input.mode,
237
+ fence: input.fence
238
+ });
239
+ }
195
240
  async notifyOperatorInputOnce(input) {
196
241
  const probe = this.readiness(input.adapterId, "operator");
197
242
  return this.tmux.sendRoleInputOnceIfReadyAsync === undefined
@@ -384,7 +384,8 @@ export class FileRoleLaunchPlanner {
384
384
  args = addCodexLifecycleHooks(args, launchMode, this.#cliPath);
385
385
  // End option parsing before the opaque prompt so a wakeup beginning
386
386
  // with '-' can never be reinterpreted as a Codex CLI flag.
387
- args.push("--", managedRun.input);
387
+ if (managedRun.pushedAt === undefined)
388
+ args.push("--", managedRun.input);
388
389
  }
389
390
  session = launchMode === "resume"
390
391
  ? readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective)
@@ -503,7 +504,8 @@ export class FileRoleLaunchPlanner {
503
504
  },
504
505
  launch: scopedLaunch,
505
506
  session,
506
- ...((binding.adapterId === "codex" || managedClaudeRun) && input.runId !== undefined
507
+ ...((binding.adapterId === "codex" && managedRun?.pushedAt === undefined
508
+ || managedClaudeRun) && input.runId !== undefined
507
509
  ? { initialPromptRunId: input.runId }
508
510
  : {})
509
511
  };
@@ -253,7 +253,7 @@ export function terminalizeExactTaskRun(store, input, now) {
253
253
  }
254
254
  else {
255
255
  const mailboxSettlement = settleExactWorkExecution(store, roleTarget, { type: "run", taskId: run.taskId, id: run.id });
256
- if (mailboxSettlement === "absent" && isReview) {
256
+ if (mailboxSettlement === "absent" && isReview && run.deliveredAt === undefined) {
257
257
  return obsolete(run, "review-mailbox-missing");
258
258
  }
259
259
  }
@@ -545,12 +545,6 @@ function matchesSessionFence(sessions, input) {
545
545
  if (session?.nativeSessionId !== input.nativeSessionId)
546
546
  return false;
547
547
  }
548
- const pending = sessions.pendingTurnCompletion;
549
- if (pending !== null && (pending.agentId !== input.agentId
550
- || pending.runId !== input.runId
551
- || (input.nativeSessionId !== undefined
552
- && pending.nativeSessionId !== input.nativeSessionId)))
553
- return false;
554
548
  const inFlight = sessions.inFlight;
555
549
  return inFlight === null || (inFlight.agentId === input.agentId
556
550
  && inFlight.runId === input.runId
@@ -1,6 +1,7 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { retireConfirmedAbsentInactiveTaskRolePlaceholders } from "../executor/agentExecutor.js";
3
3
  import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
4
+ import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
4
5
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
5
6
  import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
6
7
  import { WorkspaceCleanupBlockedError } from "./taskWorkspacePreparer.js";
@@ -148,6 +149,7 @@ export class TaskWorkspaceCoordinator {
148
149
  if (task.status !== "completed" && task.status !== "retired") {
149
150
  throw new Error(`Task must be completed or retired before archive cleanup: ${task.id}.`);
150
151
  }
152
+ this.#assertNoProviderContinuationWriters(task.id);
151
153
  const managedWorkspaces = [...this.store.listManagedWorkspaces(task.id)]
152
154
  .sort((left, right) => managedWorkspaceKey(left.owner)
153
155
  .localeCompare(managedWorkspaceKey(right.owner)));
@@ -314,6 +316,13 @@ export class TaskWorkspaceCoordinator {
314
316
  if (current === null || !isDeepStrictEqual(current, expected)) {
315
317
  throw new WorkspaceCleanupBlockedError("task-changed", `task:${expected.id}`, true, `Task changed during archive cleanup: ${expected.id}.`);
316
318
  }
319
+ this.#assertNoProviderContinuationWriters(expected.id);
320
+ }
321
+ #assertNoProviderContinuationWriters(taskId) {
322
+ const blockers = blockingProviderContinuations(this.store.listEvents(taskId));
323
+ if (blockers.length > 0) {
324
+ throw new WorkspaceCleanupBlockedError("active-run", `task:${taskId}`, true, `Task has Provider continuations that may still write its Workspace: ${taskId}.`);
325
+ }
317
326
  }
318
327
  #assertWorkItemRuntimeQuiescent(item) {
319
328
  const activeRun = this.store.listAgentRuns(item.taskId)
@@ -367,10 +376,6 @@ export class TaskWorkspaceCoordinator {
367
376
  if (sessions?.inFlight !== null && sessions?.inFlight !== undefined) {
368
377
  throw new Error(`Role has unsettled Run state: ${taskId}/${roleName}.`);
369
378
  }
370
- if (sessions?.pendingTurnCompletion !== null
371
- && sessions?.pendingTurnCompletion !== undefined) {
372
- throw new Error(`Role has unsettled Run state: ${taskId}/${roleName}.`);
373
- }
374
379
  if (this.store.getWorkMailbox !== undefined && hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
375
380
  throw new Error(`Role has unsettled runtime lifecycle state: ${taskId}/${roleName}.`);
376
381
  }
@@ -13,6 +13,18 @@ const DELIVERY_MODES = [
13
13
  "best-effort",
14
14
  "host-only"
15
15
  ];
16
+ function requireCapabilityObject(value, label) {
17
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
18
+ throw new Error(`Agent Driver ${label} capabilities must be an object.`);
19
+ }
20
+ return value;
21
+ }
22
+ function evidenceQuality(value, label) {
23
+ if (value !== "exact" && value !== "partial" && value !== "unavailable") {
24
+ throw new Error(`${label} capability is invalid.`);
25
+ }
26
+ return value;
27
+ }
16
28
  export function validateAgentDriverCapabilities(input) {
17
29
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
18
30
  throw new Error("Agent Driver capabilities must be an object.");
@@ -56,6 +68,28 @@ export function validateAgentDriverCapabilities(input) {
56
68
  if (!DELIVERY_MODES.includes(observation.delivery)) {
57
69
  throw new Error("Agent Driver delivery capability is invalid.");
58
70
  }
71
+ const conversation = requireCapabilityObject(input.conversation, "conversation");
72
+ evidenceQuality(conversation.persistentIdentity, "Provider Conversation identity");
73
+ evidenceQuality(conversation.readback, "Provider Conversation readback");
74
+ if (typeof conversation.crossProcessResume !== "boolean") {
75
+ throw new Error("Agent Driver Conversation resume capability must be boolean.");
76
+ }
77
+ const inputRouting = requireCapabilityObject(input.input, "input");
78
+ if (typeof inputRouting.startTurn !== "boolean"
79
+ || !["fenced", "unavailable"].includes(String(inputRouting.steer))
80
+ || !["fenced", "unavailable"].includes(String(inputRouting.inject))
81
+ || !["exact", "unavailable"].includes(String(inputRouting.acceptance))
82
+ || !["exact", "unavailable"].includes(String(inputRouting.idempotency))) {
83
+ throw new Error("Agent Driver input routing capabilities are invalid.");
84
+ }
85
+ const descendants = requireCapabilityObject(input.descendants, "descendants");
86
+ evidenceQuality(descendants.lineage, "Provider descendant lineage");
87
+ evidenceQuality(descendants.detachedQuery, "Provider detached descendant query");
88
+ evidenceQuality(descendants.resultRouting, "Provider descendant result routing");
89
+ const bounded = requireCapabilityObject(input.bounded, "bounded");
90
+ if (typeof bounded.structuredTerminal !== "boolean") {
91
+ throw new Error("Agent Driver bounded terminal capability must be boolean.");
92
+ }
59
93
  return Object.freeze({
60
94
  surfaces: Object.freeze(surfaces),
61
95
  control: Object.freeze({
@@ -65,6 +99,24 @@ export function validateAgentDriverCapabilities(input) {
65
99
  interrupt: control.interrupt,
66
100
  stop: control.stop
67
101
  }),
102
+ conversation: Object.freeze({
103
+ persistentIdentity: conversation.persistentIdentity,
104
+ crossProcessResume: conversation.crossProcessResume,
105
+ readback: conversation.readback
106
+ }),
107
+ input: Object.freeze({
108
+ startTurn: inputRouting.startTurn,
109
+ steer: inputRouting.steer,
110
+ inject: inputRouting.inject,
111
+ acceptance: inputRouting.acceptance,
112
+ idempotency: inputRouting.idempotency
113
+ }),
114
+ descendants: Object.freeze({
115
+ lineage: descendants.lineage,
116
+ detachedQuery: descendants.detachedQuery,
117
+ resultRouting: descendants.resultRouting
118
+ }),
119
+ bounded: Object.freeze({ structuredTerminal: bounded.structuredTerminal }),
68
120
  observation: Object.freeze({
69
121
  sessionIdentity: observation.sessionIdentity,
70
122
  sessionBootstrap: observation.sessionBootstrap,
@@ -83,17 +135,19 @@ export function managedRuntimeAdmission(capabilities) {
83
135
  const missing = [];
84
136
  if (!actual.control.start)
85
137
  missing.push("start");
86
- if (!actual.control.resume)
138
+ if (!actual.control.resume || !actual.conversation.crossProcessResume)
87
139
  missing.push("resume");
88
- if (!actual.control.sendTurn)
140
+ if (!actual.control.sendTurn || !actual.input.startTurn)
89
141
  missing.push("send-turn");
90
142
  if (!actual.control.interrupt)
91
143
  missing.push("interrupt");
92
144
  if (!actual.control.stop)
93
145
  missing.push("stop");
94
- if (actual.observation.sessionIdentity !== "exact")
146
+ if (actual.observation.sessionIdentity !== "exact"
147
+ || actual.conversation.persistentIdentity !== "exact")
95
148
  missing.push("exact-session-identity");
96
- if (actual.observation.promptAcceptance !== "exact")
149
+ if (actual.observation.promptAcceptance !== "exact"
150
+ || actual.input.acceptance !== "exact")
97
151
  missing.push("exact-prompt-acceptance");
98
152
  if (actual.observation.turnLifecycle !== "exact")
99
153
  missing.push("exact-turn-lifecycle");
@@ -101,6 +155,17 @@ export function managedRuntimeAdmission(capabilities) {
101
155
  ? Object.freeze({ admitted: true })
102
156
  : Object.freeze({ admitted: false, missing: Object.freeze(missing) });
103
157
  }
158
+ export function boundedRuntimeAdmission(capabilities) {
159
+ const actual = validateAgentDriverCapabilities(capabilities);
160
+ const missing = [];
161
+ if (!actual.control.start)
162
+ missing.push("start");
163
+ if (!actual.bounded.structuredTerminal)
164
+ missing.push("structured-terminal");
165
+ return missing.length === 0
166
+ ? Object.freeze({ admitted: true })
167
+ : Object.freeze({ admitted: false, missing: Object.freeze(missing) });
168
+ }
104
169
  export class AgentDriverRegistry {
105
170
  #drivers = new Map();
106
171
  #driverIdsByAdapter = new Map();
@@ -194,8 +259,22 @@ export function normalizeAgentDriverHookClassification(input) {
194
259
  if (input.terminal !== undefined && typeof input.terminal !== "boolean") {
195
260
  throw new Error("Agent Driver Hook terminal classification is invalid.");
196
261
  }
262
+ if (input.continuationId !== undefined) {
263
+ requireText(input.continuationId, "Agent Driver Hook continuation id");
264
+ if (!Number.isSafeInteger(input.continuationGeneration)
265
+ || input.continuationGeneration < 1) {
266
+ throw new Error("Agent Driver Hook continuation generation is invalid.");
267
+ }
268
+ }
269
+ else if (input.continuationGeneration !== undefined) {
270
+ throw new Error("Agent Driver Hook continuation generation requires an id.");
271
+ }
197
272
  return Object.freeze({
198
273
  ...(input.startupSession === undefined ? {} : { startupSession: input.startupSession }),
274
+ ...(input.continuationId === undefined ? {} : {
275
+ continuationId: input.continuationId,
276
+ continuationGeneration: input.continuationGeneration
277
+ }),
199
278
  terminal: input.terminal ?? false
200
279
  });
201
280
  }