@zq-silk/yui 0.6.16 → 0.7.0

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 (69) hide show
  1. package/dist/cli/commandCatalog.js +3 -7
  2. package/dist/cli.js +12 -33
  3. package/dist/commands/executionAuditCommands.js +19 -0
  4. package/dist/commands/globalRoleCommands.js +70 -0
  5. package/dist/commands/taskActor.js +3 -2
  6. package/dist/commands/taskCommands.js +160 -41
  7. package/dist/commands/taskContextCommand.js +1 -1
  8. package/dist/commands/taskInputCommands.js +3 -2
  9. package/dist/commands/taskRoleRuntimeStatus.js +3 -3
  10. package/dist/context/contextSnapshot.js +228 -0
  11. package/dist/context/roleSessionContext.js +3 -1
  12. package/dist/context/runContextContract.js +162 -0
  13. package/dist/context/runContextPack.js +322 -0
  14. package/dist/context/sessionBootstrapManifest.js +81 -0
  15. package/dist/context/sessionProtocolIdentity.js +23 -0
  16. package/dist/controller/agentRuntimeObserver.js +6 -1
  17. package/dist/controller/controller.js +4 -3
  18. package/dist/controller/fileSchedulerStoreAdapter.js +446 -145
  19. package/dist/controller/jobControl.js +2 -1
  20. package/dist/controller/runtime.js +83 -0
  21. package/dist/controller/runtimeHookRunFence.js +6 -2
  22. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  23. package/dist/executor/agentAdapter.js +7 -2
  24. package/dist/executor/agentExecutor.js +23 -0
  25. package/dist/executor/effectiveLaunch.js +24 -0
  26. package/dist/executor/executorRegistry.js +7 -1
  27. package/dist/executor/fileRoleLaunchPlanner.js +73 -27
  28. package/dist/lifecycle/exactRunTerminalization.js +2 -3
  29. package/dist/lifecycle/providerErrorClass.js +8 -3
  30. package/dist/observability/executionAudit.js +87 -2
  31. package/dist/repository/taskWorkspacePreparer.js +2 -2
  32. package/dist/run/agentRun.js +101 -16
  33. package/dist/run/providerRetry.js +167 -56
  34. package/dist/run/providerRetryConfig.js +5 -1
  35. package/dist/run/runControlRequest.js +50 -0
  36. package/dist/runtime/agentDriver.js +47 -0
  37. package/dist/runtime/agentHost.js +327 -0
  38. package/dist/runtime/builtinAgentDrivers.js +23 -1
  39. package/dist/runtime/builtinTranscriptObserver.js +4 -0
  40. package/dist/runtime/builtinTranscriptUsage.js +2 -0
  41. package/dist/runtime/exactControlPlane.js +2 -2
  42. package/dist/runtime/globalProcessExitStore.js +38 -0
  43. package/dist/runtime/launchBroker.js +95 -0
  44. package/dist/runtime/processExitObservation.js +60 -0
  45. package/dist/runtime/runtimeBinding.js +6 -0
  46. package/dist/runtime/runtimeObservation.js +27 -6
  47. package/dist/runtime/runtimeProjection.js +6 -3
  48. package/dist/runtime/runtimeStopReceipt.js +42 -0
  49. package/dist/runtime/sessionTerminationGuard.js +13 -0
  50. package/dist/runtime/tmuxAdapters.js +203 -220
  51. package/dist/scheduler/activeRoleRunDelivery.js +24 -3
  52. package/dist/scheduler/leaderWakeupProcessor.js +18 -60
  53. package/dist/scheduler/roleRunLiveness.js +61 -27
  54. package/dist/storage/migration/productionRegistry.js +264 -0
  55. package/dist/storage/sqliteSchema.js +23 -2
  56. package/dist/storage/sqliteStore.js +39 -2
  57. package/dist/storage/taskStore.js +54 -5
  58. package/dist/storage/upgrade/recordVersions.js +3 -1
  59. package/dist/storage/upgrade/sqliteStateMigration.js +10 -0
  60. package/dist/task/taskRecordReference.js +1 -0
  61. package/dist/tmux/tmuxManager.js +15 -4
  62. package/dist/web/assets/client/components.js +1 -1
  63. package/package.json +1 -1
  64. package/skills/yui-leader/SKILL.md +10 -5
  65. package/skills/yui-operator/SKILL.md +4 -0
  66. package/skills/yui-reviewer/SKILL.md +4 -0
  67. package/skills/yui-runtime/SKILL.md +61 -0
  68. package/skills/yui-worker/SKILL.md +82 -218
  69. package/dist/executor/managedClaudeRunner.js +0 -121
@@ -9,7 +9,7 @@ import { StorageConflictError } from "../storage/taskStore.js";
9
9
  import { bindTaskWorkspaceIdentity } from "../task/task.js";
10
10
  import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
11
11
  import { createManagedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
12
- import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
12
+ import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
13
13
  import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
14
14
  import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
15
15
  import { generateTaskWorkspaceIdentity, isLegacyTaskRef, taskArchiveRef, taskMainBranch, taskWorkspaceRefSegment, taskWorkspaceRefSegmentFromIdentity, TASK_WORKSPACE_TOKEN_PATTERN, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
@@ -2519,7 +2519,7 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
2519
2519
  || sessions.inFlight === null
2520
2520
  || sessions.inFlight.agentId !== role.activeAgentId
2521
2521
  || sessions.inFlight.runId !== run.id
2522
- || sessions.inFlight.receiptId !== formatAgentRunReceiptId(taskId, run.id)
2522
+ || sessions.inFlight.receiptId !== agentRunDeliveryReceiptId(run)
2523
2523
  || session === undefined
2524
2524
  || session.agentId !== role.activeAgentId
2525
2525
  || session.adapterId !== run.effective.adapterId
@@ -1,20 +1,42 @@
1
1
  import { validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
2
- import { validateTaskRecordReference } from "../task/taskRecordReference.js";
3
- import { markProviderRetryInFlight, validateAgentRunProviderRetry } from "./providerRetry.js";
2
+ import { formatAgentRunReceiptId, validateTaskRecordReference } from "../task/taskRecordReference.js";
3
+ import { prepareProviderRetryDispatch, validateAgentRunProviderRetry } from "./providerRetry.js";
4
4
  import { validateYieldReceipt } from "./yieldReceipt.js";
5
+ import { validateAgentRunControlRequest } from "./runControlRequest.js";
5
6
  import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
6
- export function createAgentRun(id, taskId, roleName, mode, input, now, context) {
7
+ import { createRunAssignment, createRunBootstrapEnvelope, validateRunAssignment, validateRunBootstrapEnvelope } from "../context/runContextContract.js";
8
+ export function createAgentRun(id, taskId, roleName, mode, assignment, now, context) {
7
9
  if (mode !== "new" && mode !== "resume") {
8
10
  throw new Error(`Agent run dispatch mode is invalid: ${mode}.`);
9
11
  }
10
12
  const timestamp = now.toISOString();
13
+ const normalizedAssignment = typeof assignment === "string"
14
+ ? createRunAssignment({
15
+ runId: id,
16
+ roleName,
17
+ purpose: context.purpose ?? "execution",
18
+ action: context.purpose === "review"
19
+ ? "review-round"
20
+ : context.workItemId === undefined ? "leader-wake" : "execute-work-item",
21
+ subject: {
22
+ taskId,
23
+ ...(context.workItemId === undefined ? {} : { workItemId: context.workItemId }),
24
+ ...(context.purpose === "review"
25
+ ? { reviewRoundId: context.reviewRoundId ?? `legacy-${id}` }
26
+ : context.reviewRoundId === undefined ? {} : { reviewRoundId: context.reviewRoundId })
27
+ },
28
+ directive: assignment,
29
+ deltaRefIds: []
30
+ })
31
+ : validateRunAssignment(assignment);
11
32
  return {
12
- schemaVersion: 7,
33
+ schemaVersion: 9,
13
34
  id: requireSafeIdentity(id, "Agent run id"),
14
35
  taskId: requireSafeIdentity(taskId, "Task id"),
15
36
  roleName: requireSafeIdentity(roleName, "Role name"),
16
37
  mode,
17
- input: requireText(input, "Agent run input"),
38
+ assignment: normalizedAssignment,
39
+ bootstrapEnvelope: createRunBootstrapEnvelope(normalizedAssignment),
18
40
  purpose: context.purpose ?? "execution",
19
41
  ...(context.workItemId === undefined
20
42
  ? {}
@@ -40,6 +62,22 @@ export function createAgentRun(id, taskId, roleName, mode, input, now, context)
40
62
  export function isActiveAgentRun(run) {
41
63
  return run.status === "active";
42
64
  }
65
+ /** Binds a freshly created, not-yet-persisted Run to its frozen Context. */
66
+ export function withAgentRunContextSnapshot(run, snapshot, deltaRefIds = []) {
67
+ if (run.status !== "active" || run.pushedAt !== undefined || run.deliveredAt !== undefined) {
68
+ throw new Error(`Cannot bind Context Snapshot after Run delivery: ${run.id}.`);
69
+ }
70
+ const assignment = validateRunAssignment(Object.freeze({
71
+ ...run.assignment,
72
+ contextSnapshotRef: snapshot,
73
+ deltaRefIds
74
+ }));
75
+ return validateAgentRun(Object.freeze({
76
+ ...run,
77
+ assignment,
78
+ bootstrapEnvelope: createRunBootstrapEnvelope(assignment)
79
+ }));
80
+ }
43
81
  export function markAgentRunPushed(run, now) {
44
82
  if (run.status !== "active") {
45
83
  throw new Error(`Cannot push a terminal Agent run: ${run.id}.`);
@@ -66,14 +104,23 @@ export function markAgentRunDelivered(run, now) {
66
104
  };
67
105
  }
68
106
  export function validateAgentRun(run) {
69
- if (run.schemaVersion !== 7)
70
- throw new Error("Agent run must use schemaVersion 7.");
107
+ if (run.schemaVersion !== 9)
108
+ throw new Error("Agent run must use schemaVersion 9.");
71
109
  validateTaskRecordReference({ taskId: run.taskId, localId: run.id }, "agentRun");
72
110
  requireSafeIdentity(run.roleName, "Role name");
73
111
  if (run.mode !== "new" && run.mode !== "resume") {
74
112
  throw new Error(`Agent run dispatch mode is invalid: ${String(run.mode)}.`);
75
113
  }
76
- requireText(run.input, "Agent run input");
114
+ validateRunAssignment(run.assignment);
115
+ validateRunBootstrapEnvelope(run.bootstrapEnvelope);
116
+ if (run.assignment.runId !== run.id || run.assignment.roleName !== run.roleName
117
+ || run.assignment.subject.taskId !== run.taskId) {
118
+ throw new Error("Agent run Assignment identity does not match the Run.");
119
+ }
120
+ if (JSON.stringify(createRunBootstrapEnvelope(run.assignment))
121
+ !== JSON.stringify(run.bootstrapEnvelope)) {
122
+ throw new Error("Agent run Bootstrap Envelope does not match its Assignment.");
123
+ }
77
124
  if (!["execution", "review"].includes(run.purpose)) {
78
125
  throw new Error(`Agent run purpose is invalid: ${String(run.purpose)}.`);
79
126
  }
@@ -186,6 +233,12 @@ export function validateAgentRun(run) {
186
233
  throw new Error("Agent run deliveredAt requires a prior pushedAt.");
187
234
  }
188
235
  }
236
+ if (run.deliveryReceiptId !== undefined) {
237
+ requireText(run.deliveryReceiptId, "Agent run deliveryReceiptId");
238
+ if (run.deliveryReceiptId.length > 256) {
239
+ throw new Error("Agent run deliveryReceiptId exceeds 256 characters.");
240
+ }
241
+ }
189
242
  if (run.status === "active") {
190
243
  if (run.summary !== undefined || run.endedAt !== undefined) {
191
244
  throw new Error("An active Agent run cannot have terminal metadata.");
@@ -219,6 +272,12 @@ export function validateAgentRun(run) {
219
272
  throw new Error("A terminal Agent run cannot carry a providerRetry projection.");
220
273
  }
221
274
  }
275
+ if (run.controlRequest !== undefined) {
276
+ validateAgentRunControlRequest(run.controlRequest);
277
+ if (run.status !== "active") {
278
+ throw new Error("A terminal Agent run cannot carry a controlRequest projection.");
279
+ }
280
+ }
222
281
  if (run.yieldReceipt !== undefined) {
223
282
  validateYieldReceipt(run.yieldReceipt);
224
283
  if (run.status !== "yielded") {
@@ -250,6 +309,8 @@ function finishAgentRun(run, status, summary, now) {
250
309
  if (terminal.providerRetry !== undefined) {
251
310
  delete terminal.providerRetry;
252
311
  }
312
+ if (terminal.controlRequest !== undefined)
313
+ delete terminal.controlRequest;
253
314
  return terminal;
254
315
  }
255
316
  /**
@@ -264,23 +325,47 @@ export function withProviderRetry(run, retry) {
264
325
  }
265
326
  /**
266
327
  * Reopens an active retry-waiting Run for another in-place attempt on its
267
- * original Native Session: transport markers are reset so the existing
268
- * delivery path re-pushes the exact same input, the dispatch mode becomes
269
- * `resume`, and the projection is marked in-flight.
328
+ * original Native Session. The original Run transport/acceptance timestamps
329
+ * remain historical facts; the new delivery receipt and `dispatching` retry
330
+ * state make the delivery path send only the short continuation envelope.
270
331
  */
271
- export function reopenRunForProviderRetry(run, now) {
332
+ export function reopenRunForProviderRetry(run, receiptId, now) {
272
333
  if (run.status !== "active" || run.providerRetry === undefined) {
273
334
  throw new Error(`Agent run is not waiting for a provider retry: ${run.id}.`);
274
335
  }
275
336
  const timestamp = now.toISOString();
276
- // Omit the transport markers entirely (not `undefined`, which the file
277
- // store rejects) so the delivery path sees an un-pushed Run.
278
- const { providerRetry, pushedAt: _pushedAt, deliveredAt: _deliveredAt, ...rest } = run;
337
+ const { providerRetry, ...rest } = run;
279
338
  return validateAgentRun({
280
339
  ...rest,
281
340
  mode: "resume",
341
+ deliveryReceiptId: receiptId,
282
342
  updatedAt: timestamp,
283
- providerRetry: markProviderRetryInFlight(providerRetry, now)
343
+ providerRetry: prepareProviderRetryDispatch(providerRetry, receiptId, now)
344
+ });
345
+ }
346
+ /** Any exact correlated provider progress ends the active consecutive-failure episode. */
347
+ export function clearProviderRetryOnProgress(run) {
348
+ if (run.providerRetry === undefined)
349
+ return run;
350
+ const { providerRetry: _providerRetry, ...rest } = run;
351
+ return validateAgentRun(rest);
352
+ }
353
+ export function agentRunDeliveryReceiptId(run) {
354
+ return run.deliveryReceiptId
355
+ ?? run.controlRequest?.receiptId
356
+ ?? run.providerRetry?.lastRetryReceiptId
357
+ ?? formatAgentRunReceiptId(run.taskId, run.id);
358
+ }
359
+ export function withAgentRunControlRequest(run, request, now) {
360
+ if (run.status !== "active" || run.providerRetry !== undefined) {
361
+ throw new Error(`Agent run cannot accept a control request: ${run.id}.`);
362
+ }
363
+ return validateAgentRun({
364
+ ...run,
365
+ mode: "resume",
366
+ deliveryReceiptId: request.receiptId,
367
+ controlRequest: validateAgentRunControlRequest(request),
368
+ updatedAt: now.toISOString()
284
369
  });
285
370
  }
286
371
  /** Attaches the Issue 04 yield receipt to a yielded Run. */
@@ -1,24 +1,17 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
2
- export const PROVIDER_RETRY_BASE_DELAY_MS = 1_000;
3
- export const PROVIDER_RETRY_MAX_DELAY_MS = 60_000;
4
- /**
5
- * Default total budget for one in-place retry lineage. The window starts at
6
- * the first classified failure and bounds the total wall-clock time the Run
7
- * may stay in `provider-retrying`; once it elapses the Run terminalizes with
8
- * one structured failure instead of looping.
9
- */
10
- export const PROVIDER_RETRY_MAX_WINDOW_MS = 600_000;
11
- /**
12
- * Bounded exponential backoff. The delay is capped in interval so a hot loop
13
- * can never occupy the control plane; the total retry lineage is bounded by
14
- * {@link PROVIDER_RETRY_MAX_WINDOW_MS} (see {@link providerRetryBudgetExhausted}).
15
- */
16
- export function nextProviderRetryDelayMs(attempt) {
17
- if (!Number.isSafeInteger(attempt) || attempt < 1) {
18
- throw new Error(`Provider retry attempt must be a positive integer: ${String(attempt)}.`);
3
+ export const PROVIDER_RETRY_DELAYS_MS = Object.freeze([2_000, 5_000, 15_000]);
4
+ export const PROVIDER_RETRY_MAX_DISPATCHES = PROVIDER_RETRY_DELAYS_MS.length;
5
+ export const PROVIDER_RETRY_EPISODE_WINDOW_MS = 600_000;
6
+ /** Durable configuration keeps the historical name for compatibility. */
7
+ export const PROVIDER_RETRY_MAX_WINDOW_MS = PROVIDER_RETRY_EPISODE_WINDOW_MS;
8
+ export function nextProviderRetryDelayMs(retryIndex) {
9
+ if (!Number.isSafeInteger(retryIndex)
10
+ || retryIndex < 1
11
+ || retryIndex > PROVIDER_RETRY_MAX_DISPATCHES) {
12
+ throw new Error(`Provider retry index is out of range: ${String(retryIndex)}.`);
19
13
  }
20
- const exponent = Math.min(attempt - 1, 10);
21
- return Math.min(PROVIDER_RETRY_MAX_DELAY_MS, PROVIDER_RETRY_BASE_DELAY_MS * (2 ** exponent));
14
+ return PROVIDER_RETRY_DELAYS_MS[retryIndex - 1];
22
15
  }
23
16
  /**
24
17
  * True when the retry lineage has used its total wall-clock budget. The
@@ -29,74 +22,192 @@ export function providerRetryBudgetExhausted(value, now, maxWindowMs = PROVIDER_
29
22
  if (!Number.isSafeInteger(maxWindowMs) || maxWindowMs <= 0) {
30
23
  throw new Error(`Provider retry max window must be a positive integer: ${String(maxWindowMs)}.`);
31
24
  }
32
- return Date.parse(value.firstFailureAt) + maxWindowMs <= now.getTime();
25
+ return Math.min(Date.parse(value.episodeDeadlineAt), Date.parse(value.firstFailureAt) + maxWindowMs) <= now.getTime();
33
26
  }
34
27
  export function validateAgentRunProviderRetry(value) {
35
- if (value.schemaVersion !== 1) {
36
- throw new Error("Agent run providerRetry must use schemaVersion 1.");
28
+ if (value.schemaVersion !== 2) {
29
+ throw new Error("Agent run providerRetry must use schemaVersion 2.");
30
+ }
31
+ if (!["scheduled", "dispatching", "awaiting-progress", "blocked"].includes(value.state)) {
32
+ throw new Error("Agent run providerRetry state is invalid.");
37
33
  }
38
- if (!Number.isSafeInteger(value.attempt) || value.attempt < 1) {
39
- throw new Error("Agent run providerRetry attempt must be a positive integer.");
34
+ requireIdentity(value.episodeId, "Agent run providerRetry episodeId");
35
+ requireIdentity(value.failureEventId, "Agent run providerRetry failureEventId");
36
+ if (value.policyVersion !== 1)
37
+ throw new Error("Provider retry policy version is invalid.");
38
+ if (!Number.isSafeInteger(value.consecutiveFailures) || value.consecutiveFailures < 1) {
39
+ throw new Error("Agent run providerRetry consecutiveFailures must be positive.");
40
+ }
41
+ if (!Number.isSafeInteger(value.dispatchedRetries)
42
+ || value.dispatchedRetries < 0
43
+ || value.dispatchedRetries > value.maxRetries) {
44
+ throw new Error("Agent run providerRetry dispatchedRetries is invalid.");
45
+ }
46
+ if (value.maxRetries !== PROVIDER_RETRY_MAX_DISPATCHES) {
47
+ throw new Error("Agent run providerRetry maxRetries is invalid.");
40
48
  }
41
49
  requireTimestamp(value.firstFailureAt, "Agent run providerRetry firstFailureAt");
42
50
  requireTimestamp(value.lastFailureAt, "Agent run providerRetry lastFailureAt");
51
+ requireTimestamp(value.episodeDeadlineAt, "Agent run providerRetry episodeDeadlineAt");
43
52
  if (Date.parse(value.lastFailureAt) < Date.parse(value.firstFailureAt)) {
44
- throw new Error("Agent run providerRetry lastFailureAt must not precede firstFailureAt.");
53
+ throw new Error("Agent run providerRetry lastFailureAt precedes firstFailureAt.");
54
+ }
55
+ if (Date.parse(value.episodeDeadlineAt) <= Date.parse(value.firstFailureAt)) {
56
+ throw new Error("Agent run providerRetry deadline must follow firstFailureAt.");
57
+ }
58
+ if ((value.state === "scheduled") !== (value.nextAttemptAt !== undefined)) {
59
+ throw new Error("Only a scheduled providerRetry may carry nextAttemptAt.");
45
60
  }
46
61
  if (value.nextAttemptAt !== undefined) {
47
62
  requireTimestamp(value.nextAttemptAt, "Agent run providerRetry nextAttemptAt");
63
+ if (Date.parse(value.nextAttemptAt) > Date.parse(value.episodeDeadlineAt)) {
64
+ throw new Error("Agent run providerRetry next attempt exceeds its episode deadline.");
65
+ }
48
66
  }
49
- if (value.launchId !== undefined) {
50
- requireIdentity(value.launchId, "Agent run providerRetry launchId");
67
+ if ((value.state === "dispatching" || value.state === "awaiting-progress")
68
+ !== (value.lastRetryReceiptId !== undefined)) {
69
+ throw new Error("An in-flight providerRetry requires one retry receipt identity.");
70
+ }
71
+ if (value.lastRetryReceiptId !== undefined) {
72
+ requireIdentity(value.lastRetryReceiptId, "Agent run providerRetry receipt id");
51
73
  }
74
+ if (value.launchId !== undefined)
75
+ requireIdentity(value.launchId, "Agent run providerRetry launchId");
52
76
  if (value.nativeSessionId !== undefined) {
53
77
  requireIdentity(value.nativeSessionId, "Agent run providerRetry nativeSessionId");
54
78
  }
79
+ if (value.failedNativeTurnId !== undefined) {
80
+ requireIdentity(value.failedNativeTurnId, "Agent run providerRetry failed native Turn id");
81
+ }
55
82
  requireText(value.lastErrorSummary, "Agent run providerRetry lastErrorSummary");
56
83
  return value;
57
84
  }
58
- /**
59
- * Records one more transient failure on a Run lineage. A fresh lineage starts
60
- * at attempt 1; an existing projection increments its attempt and keeps the
61
- * original `firstFailureAt`.
62
- */
85
+ /** Advance one failure episode without ever changing the native Session. */
63
86
  export function scheduleProviderRetry(previous, input, now) {
64
- const timestamp = now.toISOString();
65
- const attempt = (previous?.attempt ?? 0) + 1;
66
- const scheduleNextAttempt = input.scheduleNextAttempt ?? true;
67
- return validateAgentRunProviderRetry({
68
- schemaVersion: 1,
87
+ const at = now.toISOString();
88
+ const firstFailureAt = previous?.firstFailureAt ?? at;
89
+ const episodeDeadlineAt = previous?.episodeDeadlineAt
90
+ ?? new Date(now.getTime() + PROVIDER_RETRY_EPISODE_WINDOW_MS).toISOString();
91
+ if (now.getTime() >= Date.parse(episodeDeadlineAt)) {
92
+ return Object.freeze({ outcome: "exhausted", reason: "window" });
93
+ }
94
+ const consecutiveFailures = (previous?.consecutiveFailures ?? 0) + 1;
95
+ const dispatchedRetries = previous?.dispatchedRetries ?? 0;
96
+ const schedule = input.scheduleNextAttempt ?? true;
97
+ if (schedule && dispatchedRetries >= PROVIDER_RETRY_MAX_DISPATCHES) {
98
+ return Object.freeze({ outcome: "exhausted", reason: "attempts" });
99
+ }
100
+ const state = schedule ? "scheduled" : "blocked";
101
+ const retryAfterMs = input.retryAfterMs;
102
+ if (retryAfterMs !== undefined && (!Number.isSafeInteger(retryAfterMs) || retryAfterMs <= 0)) {
103
+ throw new Error("Provider retry Retry-After must be a positive safe integer.");
104
+ }
105
+ const delayMs = schedule
106
+ ? Math.max(nextProviderRetryDelayMs(dispatchedRetries + 1), retryAfterMs ?? 0)
107
+ : undefined;
108
+ const nextAttemptAt = delayMs === undefined
109
+ ? undefined
110
+ : new Date(now.getTime() + delayMs).toISOString();
111
+ if (nextAttemptAt !== undefined && Date.parse(nextAttemptAt) > Date.parse(episodeDeadlineAt)) {
112
+ return Object.freeze({
113
+ outcome: "exhausted",
114
+ reason: retryAfterMs === undefined ? "window" : "retry-after-window"
115
+ });
116
+ }
117
+ const retry = validateAgentRunProviderRetry({
118
+ schemaVersion: 2,
119
+ episodeId: previous?.episodeId ?? createHash("sha256")
120
+ .update(`${firstFailureAt}\0${input.nativeSessionId ?? "unknown"}\0${input.launchId ?? "unknown"}`)
121
+ .digest("hex"),
122
+ failureEventId: requireIdentity(input.failureEventId, "Provider failure event id"),
123
+ policyVersion: 1,
124
+ state,
69
125
  errorClass: input.errorClass,
70
- attempt,
71
- firstFailureAt: previous?.firstFailureAt ?? timestamp,
72
- lastFailureAt: timestamp,
73
- ...(scheduleNextAttempt
74
- ? {
75
- nextAttemptAt: new Date(now.getTime() + nextProviderRetryDelayMs(attempt)).toISOString()
76
- }
77
- : {}),
126
+ consecutiveFailures,
127
+ dispatchedRetries,
128
+ maxRetries: PROVIDER_RETRY_MAX_DISPATCHES,
129
+ firstFailureAt,
130
+ lastFailureAt: at,
131
+ episodeDeadlineAt,
132
+ ...(nextAttemptAt === undefined ? {} : { nextAttemptAt }),
78
133
  ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
79
134
  ...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
135
+ ...(input.failedNativeTurnId === undefined
136
+ ? {}
137
+ : { failedNativeTurnId: input.failedNativeTurnId }),
80
138
  lastErrorSummary: input.lastErrorSummary
81
139
  });
140
+ return Object.freeze({ outcome: schedule ? "scheduled" : "blocked", retry });
82
141
  }
83
- /**
84
- * Marks the projection in-flight: the retry has been dispatched and no
85
- * `nextAttemptAt` is due until the next outcome arrives.
86
- */
87
- export function markProviderRetryInFlight(value, now) {
88
- if (value.nextAttemptAt === undefined)
142
+ /** Mark that one short continuation request was dispatched and now awaits any correlated progress. */
143
+ export function prepareProviderRetryDispatch(value, receiptId, now) {
144
+ if (value.state !== "scheduled" || value.nextAttemptAt === undefined)
89
145
  return value;
90
- // Omit `nextAttemptAt` entirely so the projection is durably in-flight
91
- // until the next outcome.
146
+ if (now.getTime() > Date.parse(value.episodeDeadlineAt)) {
147
+ throw new Error("Provider retry episode expired before dispatch.");
148
+ }
92
149
  const { nextAttemptAt: _nextAttemptAt, ...rest } = value;
93
150
  return validateAgentRunProviderRetry({
94
151
  ...rest,
95
- lastFailureAt: now.toISOString()
152
+ state: "dispatching",
153
+ lastRetryReceiptId: requireIdentity(receiptId, "Provider retry receipt id")
154
+ });
155
+ }
156
+ export function markProviderRetryDispatched(value) {
157
+ if (value.state !== "dispatching")
158
+ return value;
159
+ return validateAgentRunProviderRetry({
160
+ ...value,
161
+ state: "awaiting-progress",
162
+ dispatchedRetries: value.dispatchedRetries + 1
96
163
  });
97
164
  }
98
- /** True when the projection is due for another attempt at `now`. */
99
165
  export function providerRetryIsDue(value, now) {
100
- return value.nextAttemptAt !== undefined
166
+ return value.state === "scheduled"
167
+ && value.nextAttemptAt !== undefined
101
168
  && Date.parse(value.nextAttemptAt) <= now.getTime();
102
169
  }
170
+ /**
171
+ * Next Controller wake for an active automatic retry episode. Scheduled
172
+ * retries wake to dispatch; an in-flight retry wakes only at the episode
173
+ * deadline so silence cannot strand the Run forever. Blocked states are
174
+ * intentionally excluded because they require native/user evidence rather
175
+ * than an automatic lifecycle transition.
176
+ */
177
+ export function providerRetryWakeAt(value) {
178
+ if (value.state === "scheduled")
179
+ return value.nextAttemptAt ?? null;
180
+ if (value.state === "dispatching" || value.state === "awaiting-progress") {
181
+ return value.episodeDeadlineAt;
182
+ }
183
+ return null;
184
+ }
185
+ /** Delay a control-plane recovery gap without changing either failure counter. */
186
+ export function deferProviderRetry(value, now) {
187
+ const deadline = Date.parse(value.episodeDeadlineAt);
188
+ if (now.getTime() >= deadline)
189
+ return null;
190
+ const { lastRetryReceiptId: _receipt, nextAttemptAt: _next, ...rest } = value;
191
+ return validateAgentRunProviderRetry({
192
+ ...rest,
193
+ state: "scheduled",
194
+ nextAttemptAt: new Date(Math.min(now.getTime() + 15_000, deadline)).toISOString()
195
+ });
196
+ }
197
+ /** Short recovery instruction; it cannot repeat the Task Assignment body. */
198
+ export function serializeProviderRetryEnvelope(input) {
199
+ validateAgentRunProviderRetry(input.retry);
200
+ const retryOrdinal = input.retry.state === "dispatching"
201
+ ? input.retry.dispatchedRetries + 1
202
+ : input.retry.dispatchedRetries;
203
+ return [
204
+ "Yui managed in-Session continuation retry.",
205
+ `task=${requireIdentity(input.taskId, "Provider retry task id")} run=${requireIdentity(input.runId, "Provider retry run id")} role=${requireIdentity(input.roleName, "Provider retry role")}`,
206
+ `episode=${input.retry.episodeId} retry=${retryOrdinal}/${PROVIDER_RETRY_MAX_DISPATCHES} receipt=${input.retry.lastRetryReceiptId ?? "pending"}`,
207
+ `failureEvent=${input.retry.failureEventId}`,
208
+ ...(input.retry.failedNativeTurnId === undefined
209
+ ? []
210
+ : [`retryOfTurn=${input.retry.failedNativeTurnId}`]),
211
+ "Continue the existing native conversation from its latest accepted state. Do not repeat completed work; load exact Run deltas if needed."
212
+ ].join("\n");
213
+ }
@@ -17,5 +17,9 @@ export function providerRetryConfig(config) {
17
17
  /** Whether the adapter has in-place retry enabled in the given mode. */
18
18
  export function providerRetryEnabledForAdapter(config, adapterId, mode) {
19
19
  return config.mode === mode
20
- && config.adapters.includes(adapterId);
20
+ && providerRetryAdapterEnabled(config, adapterId);
21
+ }
22
+ /** Default admission is capability-driven, not a hard-coded Provider list. */
23
+ export function providerRetryAdapterEnabled(config, adapterId) {
24
+ return config.adapters === "all-capable" || config.adapters.includes(adapterId);
21
25
  }
@@ -0,0 +1,50 @@
1
+ import { createHash } from "node:crypto";
2
+ import { requireIdentity, requireTimestamp } from "../domain/validation.js";
3
+ export const WORKFLOW_OUTCOME_REQUEST_TIMEOUT_MS = 30_000;
4
+ export function createWorkflowOutcomeRequest(input) {
5
+ const requestedAt = input.now.toISOString();
6
+ return validateAgentRunControlRequest({
7
+ schemaVersion: 1,
8
+ kind: "workflow-outcome",
9
+ requestId: createHash("sha256")
10
+ .update(`${input.runId}\0${input.nativeTurnId}\0${requestedAt}`)
11
+ .digest("hex"),
12
+ state: "dispatching",
13
+ nativeTurnId: input.nativeTurnId,
14
+ receiptId: `workflow-outcome-${input.runId}-${input.nativeTurnId}`,
15
+ requestedAt,
16
+ deadlineAt: new Date(input.now.getTime() + WORKFLOW_OUTCOME_REQUEST_TIMEOUT_MS).toISOString()
17
+ });
18
+ }
19
+ export function validateAgentRunControlRequest(value) {
20
+ if (value.schemaVersion !== 1 || value.kind !== "workflow-outcome") {
21
+ throw new Error("Agent Run control request is unsupported.");
22
+ }
23
+ if (value.state !== "dispatching" && value.state !== "awaiting-outcome") {
24
+ throw new Error("Agent Run control request state is invalid.");
25
+ }
26
+ requireIdentity(value.requestId, "Agent Run control request id");
27
+ requireIdentity(value.nativeTurnId, "Agent Run control native Turn id");
28
+ requireIdentity(value.receiptId, "Agent Run control receipt id");
29
+ requireTimestamp(value.requestedAt, "Agent Run control requestedAt");
30
+ requireTimestamp(value.deadlineAt, "Agent Run control deadlineAt");
31
+ if (Date.parse(value.deadlineAt) <= Date.parse(value.requestedAt)) {
32
+ throw new Error("Agent Run control deadline must follow requestedAt.");
33
+ }
34
+ return value;
35
+ }
36
+ export function markWorkflowOutcomeRequestDispatched(value) {
37
+ validateAgentRunControlRequest(value);
38
+ return value.state === "dispatching"
39
+ ? validateAgentRunControlRequest({ ...value, state: "awaiting-outcome" })
40
+ : value;
41
+ }
42
+ export function serializeWorkflowOutcomeRequestEnvelope(input) {
43
+ validateAgentRunControlRequest(input.request);
44
+ return [
45
+ "Yui managed workflow-outcome request for the existing Run.",
46
+ `task=${requireIdentity(input.taskId, "Task id")} run=${requireIdentity(input.runId, "Run id")} role=${requireIdentity(input.roleName, "Role name")}`,
47
+ `request=${input.request.requestId} completedTurn=${input.request.nativeTurnId}`,
48
+ "Record the required Yui workflow outcome now. Do not replay the Assignment or repeat completed work."
49
+ ].join("\n");
50
+ }
@@ -41,6 +41,36 @@ export function validateAgentDriverCapabilities(input) {
41
41
  throw new Error(`Agent Driver control capability ${name} must be boolean.`);
42
42
  }
43
43
  }
44
+ const lifecycle = input.lifecycle;
45
+ if (lifecycle === null || typeof lifecycle !== "object" || Array.isArray(lifecycle)) {
46
+ throw new Error("Agent Driver lifecycle capabilities must be an object.");
47
+ }
48
+ if (lifecycle.host !== "persistent") {
49
+ throw new Error("Managed Agent Host lifecycle must be persistent.");
50
+ }
51
+ if (lifecycle.providerProcess !== "persistent" && lifecycle.providerProcess !== "per-turn") {
52
+ throw new Error("Provider process lifecycle capability is invalid.");
53
+ }
54
+ if (lifecycle.nativeConversationResume !== "exact"
55
+ && lifecycle.nativeConversationResume !== "unsupported") {
56
+ throw new Error("Native conversation resume capability is invalid.");
57
+ }
58
+ if (!["automatic", "native-explicit", "unsupported", "unknown"].includes(lifecycle.compaction)) {
59
+ throw new Error("Native compaction capability is invalid.");
60
+ }
61
+ if (lifecycle.compactionEvents !== "exact" && lifecycle.compactionEvents !== "unavailable") {
62
+ throw new Error("Native compaction event capability is invalid.");
63
+ }
64
+ if (!["per-request", "remaining-context", "cumulative-only", "unavailable"].includes(lifecycle.contextUsage)) {
65
+ throw new Error("Context usage capability is invalid.");
66
+ }
67
+ if (typeof lifecycle.inSessionContinuation !== "boolean") {
68
+ throw new Error("In-Session continuation capability must be boolean.");
69
+ }
70
+ if (lifecycle.deliveryDeduplication !== "exact"
71
+ && lifecycle.deliveryDeduplication !== "unsupported") {
72
+ throw new Error("Delivery deduplication capability is invalid.");
73
+ }
44
74
  const observation = input.observation;
45
75
  if (observation === null || typeof observation !== "object" || Array.isArray(observation)) {
46
76
  throw new Error("Agent Driver observation capabilities must be an object.");
@@ -92,6 +122,16 @@ export function validateAgentDriverCapabilities(input) {
92
122
  }
93
123
  return Object.freeze({
94
124
  surfaces: Object.freeze(surfaces),
125
+ lifecycle: Object.freeze({
126
+ host: lifecycle.host,
127
+ providerProcess: lifecycle.providerProcess,
128
+ nativeConversationResume: lifecycle.nativeConversationResume,
129
+ compaction: lifecycle.compaction,
130
+ compactionEvents: lifecycle.compactionEvents,
131
+ contextUsage: lifecycle.contextUsage,
132
+ inSessionContinuation: lifecycle.inSessionContinuation,
133
+ deliveryDeduplication: lifecycle.deliveryDeduplication
134
+ }),
95
135
  control: Object.freeze({
96
136
  start: control.start,
97
137
  resume: control.resume,
@@ -149,6 +189,13 @@ export function managedRuntimeAdmission(capabilities) {
149
189
  if (actual.observation.promptAcceptance !== "exact"
150
190
  || actual.input.acceptance !== "exact")
151
191
  missing.push("exact-prompt-acceptance");
192
+ if (actual.lifecycle.host !== "persistent")
193
+ missing.push("persistent-agent-host");
194
+ if (actual.lifecycle.nativeConversationResume !== "exact") {
195
+ missing.push("exact-native-conversation-resume");
196
+ }
197
+ if (!actual.lifecycle.inSessionContinuation)
198
+ missing.push("in-session-continuation");
152
199
  if (actual.observation.turnLifecycle !== "exact")
153
200
  missing.push("exact-turn-lifecycle");
154
201
  return missing.length === 0