@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
@@ -3,6 +3,7 @@ import { isSchedulerTaskWorkspaceReady } from "./ports.js";
3
3
  import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
4
4
  import { markYuiRunInput } from "../run/runIdentity.js";
5
5
  import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
6
+ import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
6
7
  import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
7
8
  import { RuntimeLaunchError } from "../runtime/ports.js";
8
9
  import { RuntimeLaunchFailure } from "../runtime/launchDiagnostics.js";
@@ -11,6 +12,9 @@ import { mailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js
11
12
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
12
13
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
13
14
  import { hasRuntimeLifecycleWork, RuntimeLifecycleBusyError, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
15
+ import { serializeRunBootstrapEnvelope } from "../context/runContextContract.js";
16
+ import { serializeProviderRetryEnvelope } from "../run/providerRetry.js";
17
+ import { serializeWorkflowOutcomeRequestEnvelope } from "../run/runControlRequest.js";
14
18
  /**
15
19
  * Delivers durable Work AgentRuns before liveness reconciliation. Task command
16
20
  * handlers only record intent; this Controller path is the sole automated
@@ -32,7 +36,10 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
32
36
  // re-push guard keys on pushedAt (transport), not deliveredAt (provider
33
37
  // acceptance): a pushed-but-unaccepted Run must never be pushed twice —
34
38
  // no duplicate Enter while acceptance is still pending.
35
- if (run === null || run.pushedAt !== undefined)
39
+ if (run === null
40
+ || (run.pushedAt !== undefined
41
+ && run.providerRetry?.state !== "dispatching"
42
+ && run.controlRequest?.state !== "dispatching"))
36
43
  continue;
37
44
  const taskWorkspace = store.getTaskWorkspace(task.id);
38
45
  if (!isSchedulerTaskWorkspaceReady(task, taskWorkspace)) {
@@ -64,7 +71,7 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
64
71
  continue;
65
72
  }
66
73
  const existingSession = store.getRoleSession(task.id, role.name, run.effective.agentId);
67
- const receiptId = formatAgentRunReceiptId(task.id, run.id);
74
+ const receiptId = agentRunDeliveryReceiptId(run);
68
75
  const target = { kind: "role", taskId: task.id, roleName: role.name };
69
76
  const claim = store.claimWorkMailbox({
70
77
  target,
@@ -229,7 +236,21 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
229
236
  const outcome = await delivery.sendOnce({
230
237
  delivery: ready,
231
238
  receiptId,
232
- text: run.input
239
+ text: run.controlRequest?.state === "dispatching"
240
+ ? serializeWorkflowOutcomeRequestEnvelope({
241
+ taskId: task.id,
242
+ runId: run.id,
243
+ roleName: role.name,
244
+ request: run.controlRequest
245
+ })
246
+ : run.providerRetry?.state === "dispatching"
247
+ ? serializeProviderRetryEnvelope({
248
+ taskId: task.id,
249
+ runId: run.id,
250
+ roleName: role.name,
251
+ retry: run.providerRetry
252
+ })
253
+ : serializeRunBootstrapEnvelope(run.bootstrapEnvelope)
233
254
  });
234
255
  if (outcome === "busy" || outcome === "unavailable") {
235
256
  results.push({
@@ -1,12 +1,10 @@
1
1
  import { createAgentRun } from "../run/agentRun.js";
2
- import { markYuiRunInput } from "../run/runIdentity.js";
3
- import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
2
+ import { createRunAssignment, serializeRunBootstrapEnvelope } from "../context/runContextContract.js";
4
3
  import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
5
4
  import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
6
5
  import { hasRuntimeLifecycleWork, RuntimeLifecycleBusyError, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
7
6
  import { recordLeaderFailure } from "./leaderFailure.js";
8
7
  import { createLeaderRecoveryNotification } from "./operatorNotification.js";
9
- import { evaluateSessionContextBudget } from "../context/sessionContextBudget.js";
10
8
  import { isSchedulerTaskWorkspaceReady } from "./ports.js";
11
9
  import { RuntimeLaunchError } from "../runtime/ports.js";
12
10
  export async function processLeaderWakeups(store, delivery, now, selection) {
@@ -122,52 +120,21 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
122
120
  && existingSession.status !== "stopped" && existingSession.status !== "broken") {
123
121
  throw new Error(`Leader Session is incompatible with desired effective launch: ${task.id}/${role.name}.`);
124
122
  }
125
- // Issue 04: a native Session generation whose observed per-request
126
- // input peak crossed the hard context budget is retired before dispatch
127
- // so the wake starts a fresh generation instead of waiting for
128
- // provider-side auto-compaction. Durable Task records are the checkpoint;
129
- // the bounded snapshot below re-establishes working context.
130
- let contextBudgetAdvisory;
131
- if (hasNativeSession(existingSession)
132
- && compatibleSession
133
- && existingSession.status !== "stopped"
134
- && existingSession.status !== "broken"
135
- && typeof store.listEvents === "function"
136
- && typeof store.getContextBudget === "function"
137
- && typeof store.rolloverTaskRoleSessionForContextBudget === "function") {
138
- const budget = evaluateSessionContextBudget(store.listEvents(task.id), {
139
- taskId: task.id,
140
- roleName: role.name,
141
- ...(existingSession.nativeSessionId === undefined
142
- ? {}
143
- : { nativeSessionId: existingSession.nativeSessionId }),
144
- ...(existingSession.launchId === undefined
145
- ? {}
146
- : { launchId: existingSession.launchId })
147
- }, store.getContextBudget());
148
- if (budget.state === "hard") {
149
- const rollover = store.rolloverTaskRoleSessionForContextBudget({
150
- taskId: task.id,
151
- roleName: role.name,
152
- peakTokens: budget.peakTokens,
153
- hardTokens: budget.budget.hardTokens,
154
- now
155
- });
156
- if (rollover !== null) {
157
- existingSession = null;
158
- effectiveSession = null;
159
- contextBudgetAdvisory = `Previous Session generation ${rollover.retiredNativeSessionId ?? "unknown"} was retired at the hard context budget (peak ${budget.peakTokens} >= ${budget.budget.hardTokens} tokens). This wake starts a fresh generation; durable Task records preserve all state.`;
160
- }
161
- }
162
- else if (budget.state === "soft") {
163
- contextBudgetAdvisory = `Session context budget advisory: observed per-request input peak ${budget.peakTokens} tokens crossed the soft threshold (${budget.budget.softTokens}). Durable Yui records are the checkpoint; a fresh generation starts automatically at the hard threshold (${budget.budget.hardTokens}).`;
164
- }
165
- }
166
123
  const mode = hasNativeSession(existingSession) && compatibleSession ? "resume" : "new";
167
124
  const runId = store.peekNextAgentRunId(task.id);
168
125
  const wakeEnvelope = resolveLeaderWakeEnvelope(store, task.id);
169
- const input = markYuiRunInput(leaderWakeupInput(task.id, runId, wakeEnvelope, contextBudgetAdvisory), runId, taskRoleSessionTitle(task, role.name));
170
- run = createAgentRun(runId, task.id, role.name, mode, input, now, {
126
+ const contextSnapshot = store.freezeLeaderContextSnapshot?.(task.id, role.name, now);
127
+ const assignment = createRunAssignment({
128
+ runId,
129
+ roleName: role.name,
130
+ purpose: "execution",
131
+ action: "leader-wake",
132
+ subject: { taskId: task.id },
133
+ directive: leaderWakeupInput(task.id, runId, wakeup.reasons),
134
+ ...(contextSnapshot === undefined ? {} : { contextSnapshotRef: contextSnapshot.ref }),
135
+ deltaRefIds: contextSnapshot?.deltaRefIds ?? []
136
+ });
137
+ run = createAgentRun(runId, task.id, role.name, mode, assignment, now, {
171
138
  ...(role.managedWorkspace === undefined
172
139
  ? {}
173
140
  : { workspace: role.managedWorkspace }),
@@ -300,7 +267,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
300
267
  const outcome = await delivery.sendOnce({
301
268
  delivery: ready,
302
269
  receiptId: formatAgentRunReceiptId(task.id, run.id),
303
- text: input
270
+ text: serializeRunBootstrapEnvelope(run.bootstrapEnvelope)
304
271
  });
305
272
  if (outcome === "busy" || outcome === "unavailable") {
306
273
  results.push({
@@ -490,18 +457,9 @@ function resolveLeaderWakeEnvelope(store, taskId) {
490
457
  return null;
491
458
  return store.getTaskWakeEnvelope(taskId);
492
459
  }
493
- function leaderWakeupInput(taskId, runId, wakeEnvelope, contextBudgetAdvisory) {
494
- const lines = [
495
- "Follow the injected yui-leader Skill for this Yui wakeup.",
496
- `Current Leader Run: ${runId}.`,
460
+ function leaderWakeupInput(taskId, runId, reasons) {
461
+ return [
497
462
  `For every Leader decision, milestone, or Work Item lifecycle command that is meaningful progress, carry this exact current-turn assertion on that command: YUI_LEADER_ACTION_RUN_ID=${runId} YUI_LEADER_ACTION_RECEIPT_ID=${formatAgentRunReceiptId(taskId, runId)}. The native Session environment may retain an older YUI_RUN_ID/launch; never copy those values, and never reuse this assertion after the turn changes.`,
498
- ...(contextBudgetAdvisory === undefined ? [] : [contextBudgetAdvisory]),
499
- ...(wakeEnvelope === null
500
- ? [`Read the authoritative context with yui task context ${taskId}.`]
501
- : [
502
- "Wake envelope (Issue 04):",
503
- wakeEnvelope.text
504
- ]),
505
- ];
506
- return lines.join("\n");
463
+ `Wake reasons: ${reasons.join(", ")}. Load exact context for ${taskId}/${runId}.`
464
+ ].join("\n");
507
465
  }
@@ -1,9 +1,6 @@
1
1
  import { selectedSchedulerRoles, selectedActiveSchedulerTasks } from "./ports.js";
2
2
  import { formatTaskRecordReference } from "../task/taskRecordReference.js";
3
- import { queueLeaderWakeup } from "./wakeupQueue.js";
4
- import { wakeReason } from "./wakeReason.js";
5
3
  import { currentRoleRunProgressAt, DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS } from "./roleRunStall.js";
6
- export const EXITED_ROLE_RUN_SUMMARY = "The role's tmux session exited before the run yielded.";
7
4
  /**
8
5
  * Lightweight liveness only. Host absence may fail a Run only before any
9
6
  * prompt bytes were pushed. After push, acceptance may be unknown even when
@@ -11,6 +8,9 @@ export const EXITED_ROLE_RUN_SUMMARY = "The role's tmux session exited before th
11
8
  * evidence, not an application-level outcome. The Run stays active so native
12
9
  * child work or another observer can still contribute facts, while the stall
13
10
  * path raises bounded attention independently.
11
+ * Process liveness is only a recovery signal. An absent pane/Host never proves
12
+ * that the native Session or AgentRun ended; recover the same generation and
13
+ * native identity when possible, otherwise preserve the active Run.
14
14
  */
15
15
  export async function reconcileExitedRoleRuns(store, delivery, now, selection, excludedRunRefs = new Set(), liveStatuses, resourceEvidence, targetedInventory = selection !== undefined && !selection.full) {
16
16
  const failed = [];
@@ -52,7 +52,8 @@ export async function reconcileExitedRoleRuns(store, delivery, now, selection, e
52
52
  && candidates.every(({ task, role }) => liveStatuses.has(`${task.id}\0${role.name}`))
53
53
  ? {
54
54
  statuses: liveStatuses,
55
- resources: resourceEvidence ?? new Map()
55
+ resources: resourceEvidence ?? new Map(),
56
+ hostExits: new Map()
56
57
  }
57
58
  : await inspectRoleStatuses(delivery, candidates, !targetedInventory
58
59
  ? candidates.flatMap(({ task, role, run, session }) => (isResourceCandidate(task, run, now)
@@ -90,27 +91,57 @@ export async function reconcileExitedRoleRuns(store, delivery, now, selection, e
90
91
  throw new Error("Role liveness snapshot is incomplete.");
91
92
  if (status === "present")
92
93
  continue;
93
- const persisted = store.saveExitedRoleRun({
94
- task,
95
- role,
96
- run,
97
- session,
98
- summary: EXITED_ROLE_RUN_SUMMARY,
99
- now
100
- });
101
- if (persisted === "state-changed")
94
+ const hostExit = batchSnapshot.hostExits.get(`${task.id}\0${role.name}`);
95
+ if (hostExit !== undefined) {
96
+ store.saveRoleHostExitObservation?.({
97
+ taskId: task.id,
98
+ roleName: role.name,
99
+ runId: run.id,
100
+ ...(session?.launchId === undefined ? {} : { launchId: session.launchId }),
101
+ ...(session?.nativeSessionId === undefined
102
+ ? {}
103
+ : { nativeSessionId: session.nativeSessionId }),
104
+ ...(hostExit.deadStatus === undefined ? {} : { deadStatus: hostExit.deadStatus }),
105
+ observedAt: now
106
+ });
107
+ }
108
+ if (session?.nativeSessionId === undefined) {
109
+ store.queueTaskProgress(task.id, "host-missing-native-resume-unproven", now);
102
110
  continue;
103
- delivery.forgetPrepared?.({
104
- taskId: task.id,
105
- roleName: role.name,
106
- runId: run.id
107
- });
108
- failed.push(formatTaskRecordReference(task.id, run.id, "agentRun"));
109
- // Compatibility for narrow in-memory/custom ports that predate the
110
- // adapter's atomic failure+wake transition. Production returns
111
- // "failed" and already enqueued this wake in the same transaction.
112
- if (persisted === undefined && task.status === "active") {
113
- queueLeaderWakeup(store, task.id, wakeReason(role.name === "leader" ? "leader-run-failed" : "role-run-failed"), now);
111
+ }
112
+ try {
113
+ delivery.forgetPrepared?.({
114
+ taskId: task.id,
115
+ roleName: role.name,
116
+ runId: run.id
117
+ });
118
+ const recovered = await delivery.prepareRoleSession({
119
+ taskId: task.id,
120
+ roleName: role.name,
121
+ agentId: run.effective.agentId,
122
+ adapterId: run.effective.adapterId,
123
+ effective: run.effective,
124
+ workspace: run.effective.workspace.root,
125
+ ...(run.workspace === undefined ? {} : { managedWorkspace: run.workspace }),
126
+ mode: "resume",
127
+ runId: run.id,
128
+ nativeSessionId: session.nativeSessionId
129
+ });
130
+ store.saveRoleRunPrepared({
131
+ task,
132
+ role,
133
+ run,
134
+ session: recovered.session ?? session,
135
+ ...(recovered.launchId === undefined ? {} : { launchId: recovered.launchId }),
136
+ now
137
+ });
138
+ liveStatuses?.set(`${task.id}\0${role.name}`, "present");
139
+ }
140
+ catch {
141
+ // Absence plus an inconclusive local recovery attempt is still not a
142
+ // native Session death proof. Keep the exact Run active for the next
143
+ // bounded recovery pass and expose the blocked axis separately.
144
+ store.queueTaskProgress(task.id, "host-missing-native-resume-pending", now);
114
145
  }
115
146
  }
116
147
  return failed;
@@ -129,7 +160,7 @@ async function inspectRoleStatuses(delivery, candidates, resourceInputs, targete
129
160
  }
130
161
  statuses.set(key, await delivery.inspectRole(candidate.inspection));
131
162
  }
132
- return { statuses, resources: new Map() };
163
+ return { statuses, resources: new Map(), hostExits: new Map() };
133
164
  }
134
165
  if (delivery.inspectRoles !== undefined) {
135
166
  return exactBatchInventory(await delivery.inspectRoles(candidates.map(({ inspection }) => inspection), resourceInputs), candidates);
@@ -141,12 +172,13 @@ async function inspectRoleStatuses(delivery, candidates, resourceInputs, targete
141
172
  await delivery.inspectRole(candidate.inspection)
142
173
  ]);
143
174
  }
144
- return { statuses: new Map(entries), resources: new Map() };
175
+ return { statuses: new Map(entries), resources: new Map(), hostExits: new Map() };
145
176
  }
146
177
  function exactBatchInventory(batch, candidates) {
147
178
  const expected = new Set(candidates.map(({ task, role }) => `${task.id}\0${role.name}`));
148
179
  const statuses = new Map();
149
180
  const resources = new Map();
181
+ const hostExits = new Map();
150
182
  for (const entry of batch) {
151
183
  const key = `${entry.taskId}\0${entry.roleName}`;
152
184
  if (!expected.has(key) || statuses.has(key)) {
@@ -155,11 +187,13 @@ function exactBatchInventory(batch, candidates) {
155
187
  statuses.set(key, entry.status);
156
188
  if (entry.resource !== undefined)
157
189
  resources.set(key, entry.resource);
190
+ if (entry.hostExit !== undefined)
191
+ hostExits.set(key, entry.hostExit);
158
192
  }
159
193
  if (statuses.size !== expected.size) {
160
194
  throw new Error("Tmux Role batch liveness snapshot is incomplete.");
161
195
  }
162
- return { statuses, resources };
196
+ return { statuses, resources, hostExits };
163
197
  }
164
198
  function isResourceCandidate(task, run, now) {
165
199
  if (task.status !== "active" || run.status !== "active" || run.deliveredAt === undefined) {
@@ -35,6 +35,10 @@ const AGENT_RUN_TO_VERSION = 6;
35
35
  */
36
36
  const AGENT_RUN_OPTIONAL_FIELDS_FROM_VERSION = 6;
37
37
  const AGENT_RUN_OPTIONAL_FIELDS_TO_VERSION = 7;
38
+ const AGENT_RUN_CONTEXT_PROTOCOL_FROM_VERSION = 7;
39
+ const AGENT_RUN_CONTEXT_PROTOCOL_TO_VERSION = 8;
40
+ const AGENT_RUN_RETRY_EPISODE_FROM_VERSION = 8;
41
+ const AGENT_RUN_RETRY_EPISODE_TO_VERSION = 9;
38
42
  const MESSAGE_WAKE_POLICY_FROM_VERSION = 2;
39
43
  const MESSAGE_WAKE_POLICY_TO_VERSION = 3;
40
44
  const REVIEW_ROUND_FROM_VERSION = 2;
@@ -55,6 +59,8 @@ const INTEGRATION_ATTEMPT_GATE_IDENTITY_FROM_VERSION = 3;
55
59
  const INTEGRATION_ATTEMPT_GATE_IDENTITY_TO_VERSION = 4;
56
60
  const INTEGRATION_QUEUE_FROM_VERSION = 0;
57
61
  const INTEGRATION_QUEUE_TO_VERSION = 1;
62
+ const CONTEXT_SNAPSHOT_FROM_VERSION = 0;
63
+ const CONTEXT_SNAPSHOT_TO_VERSION = 1;
58
64
  const STORED_TASK_DURABLE_JOBS_FROM_VERSION = 14;
59
65
  const STORED_TASK_DURABLE_JOBS_TO_VERSION = 15;
60
66
  const STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION = 15;
@@ -130,6 +136,8 @@ export function createProductionStorageRegistry() {
130
136
  .registerOfflineMigration(workItemExecutionGroupHistoryStep())
131
137
  .registerOfflineMigration(recordFamilyStep("agentRun", AGENT_RUN_FROM_VERSION, AGENT_RUN_TO_VERSION, "agentRuns"))
132
138
  .registerOfflineMigration(recordFamilyStep("agentRun", AGENT_RUN_OPTIONAL_FIELDS_FROM_VERSION, AGENT_RUN_OPTIONAL_FIELDS_TO_VERSION, "agentRuns"))
139
+ .registerOfflineMigration(agentRunContextProtocolStep())
140
+ .registerOfflineMigration(agentRunRetryEpisodeStep())
133
141
  .registerOfflineMigration(messageWakePolicyStep())
134
142
  .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_FROM_VERSION, REVIEW_ROUND_TO_VERSION, "reviewRounds"))
135
143
  .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION, REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION, "reviewRounds"))
@@ -140,6 +148,7 @@ export function createProductionStorageRegistry() {
140
148
  .registerOfflineMigration(integrationAttemptSupersededStep())
141
149
  .registerOfflineMigration(recordFamilyStep("integrationAttempt", INTEGRATION_ATTEMPT_GATE_IDENTITY_FROM_VERSION, INTEGRATION_ATTEMPT_GATE_IDENTITY_TO_VERSION, "integrationAttempts"))
142
150
  .registerOfflineMigration(integrationQueueIntroductionStep())
151
+ .registerOfflineMigration(contextSnapshotIntroductionStep())
143
152
  .registerCompatible(storedTaskDurableJobsStep())
144
153
  .registerCompatible(storedTaskJobCallerKeyHashesStep())
145
154
  .registerCompatible(storedTaskWakesAndPublicationReferencesStep())
@@ -725,6 +734,71 @@ function introduceIntegrationQueue(snapshot) {
725
734
  state: { ...snapshot.state, tasks: nextTasks }
726
735
  };
727
736
  }
737
+ /** Introduce the immutable Task-scoped ContextSnapshot record family. */
738
+ function contextSnapshotIntroductionStep() {
739
+ return {
740
+ axis: "record",
741
+ recordKind: "contextSnapshot",
742
+ fromVersion: CONTEXT_SNAPSHOT_FROM_VERSION,
743
+ toVersion: CONTEXT_SNAPSHOT_TO_VERSION,
744
+ introduction: true,
745
+ preconditions: requireContextSnapshotIntroduction,
746
+ transform: introduceContextSnapshots,
747
+ declaredEffects: []
748
+ };
749
+ }
750
+ function requireContextSnapshotIntroduction(snapshot) {
751
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
752
+ const namedVersion = manifestVersions.contextSnapshot;
753
+ if (namedVersion !== undefined && namedVersion !== CONTEXT_SNAPSHOT_FROM_VERSION) {
754
+ throw new Error("Record contextSnapshot introduction requires an absent manifest version or 0.");
755
+ }
756
+ if (snapshot.state === null)
757
+ return;
758
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
759
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
760
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
761
+ if (task.contextSnapshots !== undefined) {
762
+ const records = asObject(task.contextSnapshots, `contextSnapshot map ${taskId}`);
763
+ if (Object.keys(records).length > 0) {
764
+ throw new Error(`Context Snapshot introduction found existing records: ${taskId}.`);
765
+ }
766
+ }
767
+ const marks = asObject(task.idHighWaterMarks ?? {}, `Task id high-water marks ${taskId}`);
768
+ const mark = marks.contextSnapshot;
769
+ if (mark !== undefined && mark !== CONTEXT_SNAPSHOT_FROM_VERSION) {
770
+ throw new Error(`Context Snapshot introduction found a high-water mark: ${taskId}.`);
771
+ }
772
+ }
773
+ }
774
+ function introduceContextSnapshots(snapshot) {
775
+ requireContextSnapshotIntroduction(snapshot);
776
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
777
+ const schemaManifest = {
778
+ ...snapshot.schemaManifest,
779
+ recordVersions: {
780
+ ...manifestVersions,
781
+ contextSnapshot: CONTEXT_SNAPSHOT_TO_VERSION
782
+ }
783
+ };
784
+ if (snapshot.state === null)
785
+ return { schemaManifest, state: null };
786
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
787
+ const nextTasks = {};
788
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
789
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
790
+ const marks = asObject(task.idHighWaterMarks ?? {}, `Task id high-water marks ${taskId}`);
791
+ nextTasks[taskId] = {
792
+ ...task,
793
+ idHighWaterMarks: { ...marks, contextSnapshot: 0 },
794
+ contextSnapshots: {}
795
+ };
796
+ }
797
+ return {
798
+ schemaManifest,
799
+ state: { ...snapshot.state, tasks: nextTasks }
800
+ };
801
+ }
728
802
  function legacyLaneKeyParts(key) {
729
803
  const match = /^lane:([^:]+):([^:]+)$/u.exec(key);
730
804
  if (match === null)
@@ -1411,6 +1485,196 @@ function recordFamilyStep(recordKind, fromVersion, toVersion, taskMapKey) {
1411
1485
  declaredEffects: []
1412
1486
  };
1413
1487
  }
1488
+ function agentRunContextProtocolStep() {
1489
+ return {
1490
+ axis: "record",
1491
+ recordKind: "agentRun",
1492
+ fromVersion: AGENT_RUN_CONTEXT_PROTOCOL_FROM_VERSION,
1493
+ toVersion: AGENT_RUN_CONTEXT_PROTOCOL_TO_VERSION,
1494
+ preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_CONTEXT_PROTOCOL_FROM_VERSION, "agentRuns"),
1495
+ transform: migrateAgentRunsToContextProtocol,
1496
+ declaredEffects: []
1497
+ };
1498
+ }
1499
+ function migrateAgentRunsToContextProtocol(snapshot) {
1500
+ requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_CONTEXT_PROTOCOL_FROM_VERSION, "agentRuns");
1501
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1502
+ const schemaManifest = {
1503
+ ...snapshot.schemaManifest,
1504
+ recordVersions: {
1505
+ ...manifestVersions,
1506
+ agentRun: AGENT_RUN_CONTEXT_PROTOCOL_TO_VERSION
1507
+ }
1508
+ };
1509
+ if (snapshot.state === null)
1510
+ return { schemaManifest, state: null };
1511
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1512
+ const nextTasks = {};
1513
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1514
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
1515
+ const records = asObject(task.agentRuns ?? {}, `agentRun map ${taskId}`);
1516
+ const nextRecords = {};
1517
+ for (const [runId, rawRecord] of Object.entries(records)) {
1518
+ const record = asObject(rawRecord, `agentRun ${taskId}/${runId}`);
1519
+ const { input: rawInput, schemaVersion: _schemaVersion, ...rest } = record;
1520
+ const roleName = requiredMigrationText(record.roleName, "Agent Run roleName");
1521
+ const purpose = record.purpose === "review" ? "review" : "execution";
1522
+ const workItemId = optionalMigrationText(record.workItemId, "Agent Run workItemId");
1523
+ const reviewRoundId = optionalMigrationText(record.reviewRoundId, "Agent Run reviewRoundId");
1524
+ const executionGroupId = optionalMigrationText(record.executionGroupId, "Agent Run executionGroupId");
1525
+ const executionLaneId = optionalMigrationText(record.executionLaneId, "Agent Run executionLaneId");
1526
+ const action = purpose === "review"
1527
+ ? "review-round"
1528
+ : roleName === "leader"
1529
+ ? "leader-wake"
1530
+ : workItemId === undefined
1531
+ ? "lead-task"
1532
+ : "execute-work-item";
1533
+ const subject = {
1534
+ taskId,
1535
+ ...(workItemId === undefined ? {} : { workItemId }),
1536
+ ...(reviewRoundId === undefined ? {} : { reviewRoundId }),
1537
+ ...(executionGroupId === undefined ? {} : { executionGroupId }),
1538
+ ...(executionLaneId === undefined ? {} : { executionLaneId })
1539
+ };
1540
+ const assignment = {
1541
+ schemaVersion: 1,
1542
+ runId,
1543
+ roleName,
1544
+ purpose,
1545
+ action,
1546
+ subject,
1547
+ ...(typeof rawInput === "string" && rawInput.trim().length > 0
1548
+ ? { directive: rawInput }
1549
+ : {}),
1550
+ deltaRefIds: []
1551
+ };
1552
+ nextRecords[runId] = {
1553
+ ...rest,
1554
+ schemaVersion: AGENT_RUN_CONTEXT_PROTOCOL_TO_VERSION,
1555
+ assignment,
1556
+ bootstrapEnvelope: {
1557
+ protocol: "yui-run/v1",
1558
+ runId,
1559
+ roleName,
1560
+ purpose,
1561
+ action,
1562
+ subject,
1563
+ deltaRefIds: []
1564
+ }
1565
+ };
1566
+ }
1567
+ nextTasks[taskId] = { ...task, agentRuns: nextRecords };
1568
+ }
1569
+ return {
1570
+ schemaManifest,
1571
+ state: { ...snapshot.state, tasks: nextTasks }
1572
+ };
1573
+ }
1574
+ function agentRunRetryEpisodeStep() {
1575
+ return {
1576
+ axis: "record",
1577
+ recordKind: "agentRun",
1578
+ fromVersion: AGENT_RUN_RETRY_EPISODE_FROM_VERSION,
1579
+ toVersion: AGENT_RUN_RETRY_EPISODE_TO_VERSION,
1580
+ preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_RETRY_EPISODE_FROM_VERSION, "agentRuns"),
1581
+ transform: migrateAgentRunsToRetryEpisodes,
1582
+ declaredEffects: []
1583
+ };
1584
+ }
1585
+ function migrateAgentRunsToRetryEpisodes(snapshot) {
1586
+ requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_RETRY_EPISODE_FROM_VERSION, "agentRuns");
1587
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1588
+ const schemaManifest = {
1589
+ ...snapshot.schemaManifest,
1590
+ recordVersions: { ...manifestVersions, agentRun: AGENT_RUN_RETRY_EPISODE_TO_VERSION }
1591
+ };
1592
+ if (snapshot.state === null)
1593
+ return { schemaManifest, state: null };
1594
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1595
+ const nextTasks = {};
1596
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1597
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
1598
+ const records = asObject(task.agentRuns ?? {}, `agentRun map ${taskId}`);
1599
+ const nextRecords = {};
1600
+ for (const [runId, rawRecord] of Object.entries(records)) {
1601
+ const record = asObject(rawRecord, `agentRun ${taskId}/${runId}`);
1602
+ const legacyRetry = record.providerRetry === undefined
1603
+ ? undefined
1604
+ : asObject(record.providerRetry, `providerRetry ${taskId}/${runId}`);
1605
+ let providerRetry = undefined;
1606
+ let deliveryReceiptId;
1607
+ if (legacyRetry !== undefined) {
1608
+ const attempt = requiredMigrationPositiveInteger(legacyRetry.attempt, `providerRetry attempt ${taskId}/${runId}`);
1609
+ const firstFailureAt = requiredMigrationTimestamp(legacyRetry.firstFailureAt, `providerRetry firstFailureAt ${taskId}/${runId}`);
1610
+ const lastFailureAt = requiredMigrationTimestamp(legacyRetry.lastFailureAt, `providerRetry lastFailureAt ${taskId}/${runId}`);
1611
+ const nextAttemptAt = optionalMigrationTimestamp(legacyRetry.nextAttemptAt, `providerRetry nextAttemptAt ${taskId}/${runId}`);
1612
+ const policyBlocked = legacyRetry.errorClass === "policy-denied";
1613
+ const deadlineMs = Math.max(Date.parse(firstFailureAt) + 600_000, nextAttemptAt === undefined ? 0 : Date.parse(nextAttemptAt) + 1);
1614
+ deliveryReceiptId = nextAttemptAt !== undefined || policyBlocked
1615
+ ? undefined
1616
+ : `legacy-retry-receipt-${runId}-${attempt}`;
1617
+ providerRetry = {
1618
+ schemaVersion: 2,
1619
+ episodeId: `legacy-retry-${runId}`,
1620
+ failureEventId: `legacy-provider-failure-${runId}-${attempt}`,
1621
+ policyVersion: 1,
1622
+ state: nextAttemptAt !== undefined
1623
+ ? "scheduled"
1624
+ : policyBlocked ? "blocked" : "awaiting-progress",
1625
+ errorClass: legacyRetry.errorClass,
1626
+ consecutiveFailures: attempt,
1627
+ dispatchedRetries: Math.min(3, nextAttemptAt === undefined ? attempt : attempt - 1),
1628
+ maxRetries: 3,
1629
+ firstFailureAt,
1630
+ lastFailureAt,
1631
+ episodeDeadlineAt: new Date(deadlineMs).toISOString(),
1632
+ ...(nextAttemptAt === undefined ? {} : { nextAttemptAt }),
1633
+ ...(deliveryReceiptId === undefined
1634
+ ? {}
1635
+ : { lastRetryReceiptId: deliveryReceiptId }),
1636
+ ...(legacyRetry.launchId === undefined ? {} : { launchId: legacyRetry.launchId }),
1637
+ ...(legacyRetry.nativeSessionId === undefined
1638
+ ? {}
1639
+ : { nativeSessionId: legacyRetry.nativeSessionId }),
1640
+ lastErrorSummary: legacyRetry.lastErrorSummary
1641
+ };
1642
+ }
1643
+ nextRecords[runId] = {
1644
+ ...record,
1645
+ schemaVersion: AGENT_RUN_RETRY_EPISODE_TO_VERSION,
1646
+ ...(deliveryReceiptId === undefined ? {} : { deliveryReceiptId }),
1647
+ ...(providerRetry === undefined ? {} : { providerRetry })
1648
+ };
1649
+ }
1650
+ nextTasks[taskId] = { ...task, agentRuns: nextRecords };
1651
+ }
1652
+ return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
1653
+ }
1654
+ function requiredMigrationPositiveInteger(value, label) {
1655
+ if (!Number.isSafeInteger(value) || value < 1) {
1656
+ throw new Error(`${label} must be a positive integer.`);
1657
+ }
1658
+ return value;
1659
+ }
1660
+ function requiredMigrationTimestamp(value, label) {
1661
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
1662
+ throw new Error(`${label} must be a timestamp.`);
1663
+ }
1664
+ return value;
1665
+ }
1666
+ function optionalMigrationTimestamp(value, label) {
1667
+ return value === undefined ? undefined : requiredMigrationTimestamp(value, label);
1668
+ }
1669
+ function requiredMigrationText(value, label) {
1670
+ if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
1671
+ throw new Error(`${label} is invalid.`);
1672
+ }
1673
+ return value.trim();
1674
+ }
1675
+ function optionalMigrationText(value, label) {
1676
+ return value === undefined ? undefined : requiredMigrationText(value, label);
1677
+ }
1414
1678
  /**
1415
1679
  * The snapshot boundary is nested inside WorkItem/ReviewRound ExecutionGroup
1416
1680
  * results. The parent record versions make that persisted shape explicit;