@zq-silk/yui 0.6.8 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +10 -3
  2. package/dist/cli/commandCatalog.js +1 -1
  3. package/dist/commands/taskCommands.js +63 -28
  4. package/dist/commands/taskContextCommand.js +27 -1
  5. package/dist/commands/taskRoleRuntimeStatus.js +17 -6
  6. package/dist/controller/agentRuntimeObserver.js +6 -3
  7. package/dist/controller/controller.js +29 -47
  8. package/dist/controller/fileSchedulerStoreAdapter.js +572 -258
  9. package/dist/controller/runtime.js +8 -1
  10. package/dist/controller/runtimeEventInbox.js +16 -5
  11. package/dist/controller/runtimeHookRunFence.js +51 -5
  12. package/dist/controller/runtimeObservationHook.js +8 -2
  13. package/dist/coordination/workMailbox.js +408 -28
  14. package/dist/coordination/workMailboxQueue.js +12 -10
  15. package/dist/executor/agentExecutor.js +101 -94
  16. package/dist/executor/executorRegistry.js +47 -2
  17. package/dist/executor/fileRoleLaunchPlanner.js +4 -2
  18. package/dist/lifecycle/exactRunTerminalization.js +1 -7
  19. package/dist/repository/taskWorkspaceCoordinator.js +9 -4
  20. package/dist/runtime/agentDriver.js +83 -4
  21. package/dist/runtime/agentDriverObservation.js +25 -10
  22. package/dist/runtime/builtinAgentDrivers.js +168 -18
  23. package/dist/runtime/codexAppServerRuntime.js +355 -0
  24. package/dist/runtime/continuationManager.js +117 -0
  25. package/dist/runtime/index.js +2 -0
  26. package/dist/runtime/lifecycleReservation.js +4 -3
  27. package/dist/runtime/promptEnvelope.js +14 -3
  28. package/dist/runtime/providerContinuation.js +225 -0
  29. package/dist/runtime/providerContinuationReconciliationService.js +172 -0
  30. package/dist/runtime/providerRuntimeIdentity.js +232 -0
  31. package/dist/runtime/providerRuntimeReconciler.js +166 -0
  32. package/dist/runtime/runtimeContinuationProjection.js +34 -0
  33. package/dist/runtime/runtimeObservation.js +217 -6
  34. package/dist/runtime/runtimeProjection.js +172 -11
  35. package/dist/scheduler/activeRoleRunDelivery.js +314 -1
  36. package/dist/scheduler/leaderWakeupProcessor.js +2 -1
  37. package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
  38. package/dist/scheduler/roleRunLiveness.js +8 -7
  39. package/dist/scheduler/roleRunStall.js +4 -2
  40. package/dist/scheduler/taskExecutionProjection.js +2 -2
  41. package/dist/storage/migration/productionRegistry.js +474 -1
  42. package/dist/storage/sqliteSchema.js +102 -21
  43. package/dist/storage/sqliteStore.js +52 -110
  44. package/dist/storage/storageVersions.js +1 -1
  45. package/dist/storage/storeRpc.js +0 -1
  46. package/dist/storage/taskStore.js +40 -53
  47. package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
  48. package/dist/task/nextAction.js +1 -1
  49. package/dist/web/assets/client/app.js +1 -1
  50. package/dist/web/assets/client/components.js +233 -4
  51. package/dist/web/assets/client/i18n.js +166 -2
  52. package/dist/web/assets/client/view.js +30 -13
  53. package/dist/web/assets/styles/cards.js +62 -0
  54. package/dist/web/assets/styles/widgets.js +1 -0
  55. package/dist/web/webSnapshot.js +11 -2
  56. package/package.json +1 -1
  57. package/skills/yui-leader/SKILL.md +33 -24
  58. package/skills/yui-operator/SKILL.md +7 -5
@@ -1,6 +1,7 @@
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";
4
5
  export function createRoleSessionSet(owner, activeAgentId, now) {
5
6
  const base = {
6
7
  owner: normalizeOwner(owner),
@@ -12,9 +13,9 @@ export function createRoleSessionSet(owner, activeAgentId, now) {
12
13
  ? { ...base, schemaVersion: 3 }
13
14
  : {
14
15
  ...base,
15
- schemaVersion: 4,
16
+ schemaVersion: 5,
16
17
  inFlight: null,
17
- pendingTurnCompletion: null
18
+ providerBinding: null
18
19
  };
19
20
  }
20
21
  export function activeRoleAgentSession(set) {
@@ -157,8 +158,7 @@ export function updateRoleAgentSessionStatus(set, agentId, status, now) {
157
158
  }
158
159
  export function retireTaskRoleSessionsForWorkspace(set, now) {
159
160
  validateRoleSessionSet(set);
160
- if (set.inFlight !== null
161
- || set.pendingTurnCompletion !== null) {
161
+ if (set.inFlight !== null) {
162
162
  throw new Error("Cannot retire a Task Role session with unsettled Run state.");
163
163
  }
164
164
  const live = Object.values(set.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
@@ -183,7 +183,7 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
183
183
  */
184
184
  export function retireConfirmedAbsentInactiveTaskRolePlaceholders(set, now) {
185
185
  validateRoleSessionSet(set);
186
- if (set.inFlight !== null || set.pendingTurnCompletion !== null) {
186
+ if (set.inFlight !== null) {
187
187
  throw new Error("Cannot retire a Task Role placeholder with unsettled Run state.");
188
188
  }
189
189
  const timestamp = now.toISOString();
@@ -278,9 +278,6 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
278
278
  if (normalized.agentId !== set.activeAgentId) {
279
279
  throw new Error("Task Role Run Agent does not match the active Agent.");
280
280
  }
281
- if (set.pendingTurnCompletion !== null) {
282
- throw new Error("Task Role session set has an unsettled Turn completion.");
283
- }
284
281
  if (set.inFlight !== null) {
285
282
  if (sameRunFence(set.inFlight, normalized))
286
283
  return set;
@@ -294,6 +291,38 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
294
291
  };
295
292
  return validateRoleSessionSet(updated);
296
293
  }
294
+ export function bindTaskRoleProviderRuntime(set, binding, updatedAt) {
295
+ validateRoleSessionSet(set);
296
+ const normalized = validateProviderRuntimeBinding(binding);
297
+ if (set.inFlight === null || set.inFlight.runId !== normalized.runId) {
298
+ throw new Error("Provider Runtime Binding does not match the in-flight Run.");
299
+ }
300
+ if (set.providerBinding !== null) {
301
+ if (JSON.stringify(set.providerBinding) === JSON.stringify(normalized))
302
+ return set;
303
+ throw new Error("Task Role already has a Provider Runtime Binding.");
304
+ }
305
+ return validateRoleSessionSet({
306
+ ...set,
307
+ providerBinding: normalized,
308
+ updatedAt: requireDate(updatedAt, "Provider Runtime Binding timestamp")
309
+ });
310
+ }
311
+ export function updateTaskRoleProviderRuntime(set, binding, updatedAt) {
312
+ validateRoleSessionSet(set);
313
+ const normalized = validateProviderRuntimeBinding(binding);
314
+ if (set.providerBinding === null
315
+ || normalized.runId !== set.providerBinding.runId
316
+ || normalized.providerNamespace !== set.providerBinding.providerNamespace
317
+ || normalized.accountScope !== set.providerBinding.accountScope) {
318
+ throw new Error("Provider Runtime Binding identity cannot change in place.");
319
+ }
320
+ return validateRoleSessionSet({
321
+ ...set,
322
+ providerBinding: normalized,
323
+ updatedAt: requireDate(updatedAt, "Provider Runtime Binding timestamp")
324
+ });
325
+ }
297
326
  export function markTaskRoleRunPushed(set, fence, pushedAt) {
298
327
  validateRoleSessionSet(set);
299
328
  assertTaskRoleSessionSet(set);
@@ -332,38 +361,55 @@ export function markTaskRoleRunDelivered(set, fence, deliveredAt) {
332
361
  };
333
362
  return validateRoleSessionSet(updated);
334
363
  }
335
- export function recordObservedTaskRoleCompletion(set, completion) {
364
+ /**
365
+ * Records a provider Turn boundary without changing the durable Yui Run.
366
+ *
367
+ * A native session may finish one foreground Turn while provider-owned
368
+ * subagents, mailbox work, or later user corrections still belong to the same
369
+ * application-level Run. Only an explicit Yui workflow outcome may clear the
370
+ * Run fence; this transition merely makes the native session available for a
371
+ * subsequent input and remembers the provider Turn idempotently.
372
+ */
373
+ export function recordTaskRoleTurnBoundary(set, input, completedAt) {
336
374
  validateRoleSessionSet(set);
337
375
  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.");
376
+ const agentId = requireSafeIdentity(input.agentId, "Agent id");
377
+ const nativeSessionId = requireText(input.nativeSessionId, "Native session id");
378
+ const turnId = requireSafeIdentity(input.turnId, "Turn id");
379
+ const session = set.sessions[agentId];
380
+ if (session === undefined || session.nativeSessionId !== nativeSessionId) {
381
+ throw new Error("Completed Turn has no matching Role Agent native session.");
343
382
  }
344
- if (session?.recentCompletedTurnIds.includes(observed.turnId) === true)
383
+ if (session.recentCompletedTurnIds.includes(turnId))
345
384
  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.");
385
+ if (set.inFlight !== null && set.inFlight.agentId !== agentId) {
386
+ throw new Error("Completed Turn Agent does not match the in-flight Run.");
357
387
  }
358
- if (inFlight.pushedAt === undefined) {
359
- throw new Error("Observed Turn Run must be pushed before completion is recorded.");
360
- }
361
- const updated = {
388
+ const timestamp = requireDate(completedAt, "Turn completedAt");
389
+ const status = session.status === "stopped" || session.status === "broken"
390
+ ? session.status
391
+ : "ready";
392
+ return validateRoleSessionSet({
362
393
  ...set,
363
- pendingTurnCompletion: observed,
364
- updatedAt: observed.observedAt
365
- };
366
- return validateRoleSessionSet(updated);
394
+ sessions: {
395
+ ...set.sessions,
396
+ [agentId]: {
397
+ ...session,
398
+ status,
399
+ recentCompletedTurnIds: rememberRecentTurnId(session.recentCompletedTurnIds, turnId),
400
+ updatedAt: timestamp
401
+ }
402
+ },
403
+ updatedAt: timestamp
404
+ });
405
+ }
406
+ export function recordObservedTaskRoleCompletion(set, completion) {
407
+ const observed = validatePendingTurnCompletion(completion);
408
+ return recordTaskRoleTurnBoundary(set, {
409
+ agentId: observed.agentId,
410
+ nativeSessionId: observed.nativeSessionId,
411
+ turnId: observed.turnId
412
+ }, new Date(observed.observedAt));
367
413
  }
368
414
  export function clearTaskRoleRun(set, fence, clearedAt) {
369
415
  validateRoleSessionSet(set);
@@ -373,23 +419,10 @@ export function clearTaskRoleRun(set, fence, clearedAt) {
373
419
  if (Date.parse(timestamp) < Date.parse(inFlight.preparedAt)) {
374
420
  throw new Error("Task Role Run clearedAt must not be earlier than preparedAt.");
375
421
  }
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
422
  const updated = {
389
423
  ...set,
390
- sessions,
391
424
  inFlight: null,
392
- pendingTurnCompletion: null,
425
+ providerBinding: null,
393
426
  updatedAt: timestamp
394
427
  };
395
428
  return validateRoleSessionSet(updated);
@@ -402,18 +435,9 @@ export function clearTaskRoleRun(set, fence, clearedAt) {
402
435
  export function terminalizeTaskRoleRunSession(set, fence, terminalAt) {
403
436
  validateRoleSessionSet(set);
404
437
  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);
438
+ let updated = inFlight === null
439
+ ? set
440
+ : clearTaskRoleRun(set, fence, terminalAt);
417
441
  const session = updated.sessions[updated.activeAgentId];
418
442
  if (session?.status === "running") {
419
443
  updated = updateRoleAgentSessionStatus(updated, updated.activeAgentId, "ready", terminalAt);
@@ -441,7 +465,7 @@ export function resetTaskRoleSession(set, now) {
441
465
  sessions,
442
466
  ...(history === undefined ? {} : { history }),
443
467
  inFlight: null,
444
- pendingTurnCompletion: null,
468
+ providerBinding: null,
445
469
  updatedAt: timestamp
446
470
  });
447
471
  }
@@ -451,38 +475,27 @@ export function settleTaskRoleCompletion(set, expected, settledAt) {
451
475
  const agentId = requireSafeIdentity(expected.agentId, "Agent id");
452
476
  const runId = requireSafeIdentity(expected.runId, "Run id");
453
477
  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
478
  const inFlight = set.inFlight;
463
479
  if (inFlight === null || inFlight.agentId !== agentId || inFlight.runId !== runId) {
464
480
  throw new Error("Pending Turn completion does not match the in-flight Run.");
465
481
  }
466
482
  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.");
483
+ if (session === undefined || !session.recentCompletedTurnIds.includes(turnId)) {
484
+ throw new Error("Turn completion has not been observed for the Role Agent session.");
469
485
  }
470
486
  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
487
  const updated = {
475
488
  ...set,
476
489
  sessions: {
477
490
  ...set.sessions,
478
491
  [agentId]: {
479
492
  ...session,
480
- recentCompletedTurnIds: rememberRecentTurnId(session.recentCompletedTurnIds, pending.turnId),
493
+ recentCompletedTurnIds: session.recentCompletedTurnIds,
481
494
  updatedAt: timestamp
482
495
  }
483
496
  },
484
497
  inFlight: null,
485
- pendingTurnCompletion: null,
498
+ providerBinding: null,
486
499
  updatedAt: timestamp
487
500
  };
488
501
  return validateRoleSessionSet(updated);
@@ -498,7 +511,7 @@ export function validateRoleSessionSet(set) {
498
511
  throw new Error("Global Role session set schema version is invalid.");
499
512
  }
500
513
  if (Object.hasOwn(set, "inFlight")
501
- || Object.hasOwn(set, "pendingTurnCompletion")) {
514
+ || Object.hasOwn(set, "providerBinding")) {
502
515
  throw new Error("Global Role session set must not contain Task Role lifecycle fields.");
503
516
  }
504
517
  const history = set.history;
@@ -513,11 +526,11 @@ export function validateRoleSessionSet(set) {
513
526
  }
514
527
  }
515
528
  else {
516
- if (set.schemaVersion !== 4) {
529
+ if (set.schemaVersion !== 5) {
517
530
  throw new Error("Task Role session set schema version is invalid.");
518
531
  }
519
532
  if (!Object.hasOwn(set, "inFlight")
520
- || !Object.hasOwn(set, "pendingTurnCompletion")) {
533
+ || !Object.hasOwn(set, "providerBinding")) {
521
534
  throw new Error("Task Role session set must contain its Turn fence.");
522
535
  }
523
536
  const taskSet = set;
@@ -546,28 +559,22 @@ export function validateRoleSessionSet(set) {
546
559
  const inFlight = taskSet.inFlight === null
547
560
  ? null
548
561
  : validateTaskRoleInFlight(taskSet.inFlight);
549
- const pending = taskSet.pendingTurnCompletion === null
562
+ const providerBinding = taskSet.providerBinding === null
550
563
  ? null
551
- : validatePendingTurnCompletion(taskSet.pendingTurnCompletion);
552
- if (inFlight === null && pending !== null) {
553
- throw new Error("Pending Turn completion requires an in-flight Run.");
554
- }
564
+ : validateProviderRuntimeBinding(taskSet.providerBinding);
555
565
  if (inFlight !== null && inFlight.agentId !== set.activeAgentId) {
556
566
  throw new Error("Task Role in-flight Run Agent must be active.");
557
567
  }
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.");
568
+ if (providerBinding !== null) {
569
+ if (inFlight === null || providerBinding.runId !== inFlight.runId) {
570
+ throw new Error("Provider Runtime Binding must match the in-flight Run.");
564
571
  }
565
- const session = taskSet.sessions[pending.agentId];
572
+ const session = taskSet.sessions[inFlight.agentId];
566
573
  if (session === undefined) {
567
- throw new Error("Pending Turn completion has no Role Agent session.");
574
+ throw new Error("Provider Runtime Binding has no active Role Agent session.");
568
575
  }
569
- if (session.nativeSessionId !== pending.nativeSessionId) {
570
- throw new Error("Pending Turn native session does not match the Role Agent session.");
576
+ if (providerBinding.providerNamespace !== session.adapterId) {
577
+ throw new Error("Provider Runtime Binding namespace does not match the Agent adapter.");
571
578
  }
572
579
  }
573
580
  }
@@ -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
  }