@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.
- package/dist/cli/commandCatalog.js +3 -7
- package/dist/cli.js +12 -33
- package/dist/commands/executionAuditCommands.js +19 -0
- package/dist/commands/globalRoleCommands.js +70 -0
- package/dist/commands/taskActor.js +3 -2
- package/dist/commands/taskCommands.js +160 -41
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskInputCommands.js +3 -2
- package/dist/commands/taskRoleRuntimeStatus.js +3 -3
- package/dist/context/contextSnapshot.js +228 -0
- package/dist/context/roleSessionContext.js +3 -1
- package/dist/context/runContextContract.js +162 -0
- package/dist/context/runContextPack.js +322 -0
- package/dist/context/sessionBootstrapManifest.js +81 -0
- package/dist/context/sessionProtocolIdentity.js +23 -0
- package/dist/controller/agentRuntimeObserver.js +6 -1
- package/dist/controller/controller.js +4 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -145
- package/dist/controller/jobControl.js +2 -1
- package/dist/controller/runtime.js +83 -0
- package/dist/controller/runtimeHookRunFence.js +6 -2
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/executor/agentAdapter.js +7 -2
- package/dist/executor/agentExecutor.js +23 -0
- package/dist/executor/effectiveLaunch.js +24 -0
- package/dist/executor/executorRegistry.js +7 -1
- package/dist/executor/fileRoleLaunchPlanner.js +73 -27
- package/dist/lifecycle/exactRunTerminalization.js +2 -3
- package/dist/lifecycle/providerErrorClass.js +8 -3
- package/dist/observability/executionAudit.js +87 -2
- package/dist/repository/taskWorkspacePreparer.js +2 -2
- package/dist/run/agentRun.js +101 -16
- package/dist/run/providerRetry.js +167 -56
- package/dist/run/providerRetryConfig.js +5 -1
- package/dist/run/runControlRequest.js +50 -0
- package/dist/runtime/agentDriver.js +47 -0
- package/dist/runtime/agentHost.js +327 -0
- package/dist/runtime/builtinAgentDrivers.js +23 -1
- package/dist/runtime/builtinTranscriptObserver.js +4 -0
- package/dist/runtime/builtinTranscriptUsage.js +2 -0
- package/dist/runtime/exactControlPlane.js +2 -2
- package/dist/runtime/globalProcessExitStore.js +38 -0
- package/dist/runtime/launchBroker.js +95 -0
- package/dist/runtime/processExitObservation.js +60 -0
- package/dist/runtime/runtimeBinding.js +6 -0
- package/dist/runtime/runtimeObservation.js +27 -6
- package/dist/runtime/runtimeProjection.js +6 -3
- package/dist/runtime/runtimeStopReceipt.js +42 -0
- package/dist/runtime/sessionTerminationGuard.js +13 -0
- package/dist/runtime/tmuxAdapters.js +203 -220
- package/dist/scheduler/activeRoleRunDelivery.js +24 -3
- package/dist/scheduler/leaderWakeupProcessor.js +18 -60
- package/dist/scheduler/roleRunLiveness.js +61 -27
- package/dist/storage/migration/productionRegistry.js +264 -0
- package/dist/storage/sqliteSchema.js +23 -2
- package/dist/storage/sqliteStore.js +39 -2
- package/dist/storage/taskStore.js +54 -5
- package/dist/storage/upgrade/recordVersions.js +3 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +10 -0
- package/dist/task/taskRecordReference.js +1 -0
- package/dist/tmux/tmuxManager.js +15 -4
- package/dist/web/assets/client/components.js +1 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +10 -5
- package/skills/yui-operator/SKILL.md +4 -0
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +61 -0
- package/skills/yui-worker/SKILL.md +82 -218
- 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
|
|
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 =
|
|
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.
|
|
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 {
|
|
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
|
|
170
|
-
|
|
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:
|
|
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,
|
|
494
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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;
|