@zq-silk/yui 0.6.16 → 0.7.1
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/coordination/workMailbox.js +25 -22
- 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 +48 -4
- 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
|
@@ -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;
|
|
@@ -27,7 +27,7 @@ export const SQLITE_LAYOUT_VERSION = 7;
|
|
|
27
27
|
/** The aggregate version of the normalized SQLite schema. */
|
|
28
28
|
export const SQLITE_AGGREGATE_VERSION = 1;
|
|
29
29
|
/** The current schema migration version. */
|
|
30
|
-
export const SQLITE_SCHEMA_VERSION =
|
|
30
|
+
export const SQLITE_SCHEMA_VERSION = 17;
|
|
31
31
|
/** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
|
|
32
32
|
export const TELEMETRY_KEEP_PER_GENERATION = 200;
|
|
33
33
|
export const TELEMETRY_RUN_CAP = 50_000;
|
|
@@ -911,6 +911,25 @@ CREATE TABLE IF NOT EXISTS task_wakes (
|
|
|
911
911
|
);
|
|
912
912
|
CREATE INDEX IF NOT EXISTS idx_task_wakes_seq ON task_wakes(task_id, seq);
|
|
913
913
|
`;
|
|
914
|
+
/** Migration 17: immutable, Task-scoped ContextSnapshot records. */
|
|
915
|
+
const MIGRATION_17_SQL = `
|
|
916
|
+
CREATE TABLE IF NOT EXISTS context_snapshots (
|
|
917
|
+
task_id TEXT NOT NULL,
|
|
918
|
+
snapshot_id TEXT NOT NULL,
|
|
919
|
+
scope TEXT NOT NULL CHECK (scope IN ('task','workitem','stage')),
|
|
920
|
+
scope_ref TEXT,
|
|
921
|
+
sequence INTEGER NOT NULL CHECK (sequence > 0),
|
|
922
|
+
digest TEXT NOT NULL CHECK (length(digest) = 64),
|
|
923
|
+
payload TEXT NOT NULL,
|
|
924
|
+
frozen_at TEXT NOT NULL,
|
|
925
|
+
PRIMARY KEY (task_id, snapshot_id),
|
|
926
|
+
FOREIGN KEY (task_id) REFERENCES tasks_catalog(task_id) ON DELETE CASCADE,
|
|
927
|
+
CHECK ((scope = 'task' AND scope_ref IS NULL) OR (scope <> 'task' AND scope_ref IS NOT NULL))
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_context_snapshots_scope_sequence
|
|
931
|
+
ON context_snapshots(task_id, scope, COALESCE(scope_ref, ''), sequence);
|
|
932
|
+
`;
|
|
914
933
|
const MIGRATIONS = [
|
|
915
934
|
{ version: 1, axis: "layout", sql: MIGRATION_1_SQL },
|
|
916
935
|
{ version: 2, axis: "record", recordKind: "durableJob+capability-grant+release-workflow", sql: MIGRATION_2_SQL },
|
|
@@ -927,7 +946,8 @@ const MIGRATIONS = [
|
|
|
927
946
|
{ version: 13, axis: "layout", sql: MIGRATION_13_SQL },
|
|
928
947
|
{ version: 14, axis: "record", recordKind: "workMailbox", sql: MIGRATION_14_SQL },
|
|
929
948
|
{ version: 15, axis: "record", recordKind: "publicationReference", sql: MIGRATION_15_SQL },
|
|
930
|
-
{ version: 16, axis: "record", recordKind: "taskWake", sql: MIGRATION_16_SQL }
|
|
949
|
+
{ version: 16, axis: "record", recordKind: "taskWake", sql: MIGRATION_16_SQL },
|
|
950
|
+
{ version: 17, axis: "record", recordKind: "contextSnapshot", sql: MIGRATION_17_SQL }
|
|
931
951
|
];
|
|
932
952
|
/** Current hot-path indexes whose absence would invalidate a current Home. */
|
|
933
953
|
const REQUIRED_SCHEMA_INDEXES = [
|
|
@@ -1169,6 +1189,7 @@ export const SQLITE_SCHEMA_TABLES = [
|
|
|
1169
1189
|
"role_session_sets",
|
|
1170
1190
|
"work_items",
|
|
1171
1191
|
"work_item_candidates",
|
|
1192
|
+
"context_snapshots",
|
|
1172
1193
|
"agent_runs",
|
|
1173
1194
|
"active_runs",
|
|
1174
1195
|
"review_rounds",
|
|
@@ -39,6 +39,7 @@ import { join } from "node:path";
|
|
|
39
39
|
import { isDeepStrictEqual } from "node:util";
|
|
40
40
|
import Database from "better-sqlite3";
|
|
41
41
|
import { consumePendingBatch, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
|
|
42
|
+
import { validateContextSnapshot } from "../context/contextSnapshot.js";
|
|
42
43
|
import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
|
|
43
44
|
import { validateReviewFinding } from "../review/reviewFinding.js";
|
|
44
45
|
import { reviewFindingLedgerMode } from "../review/reviewFindingLedger.js";
|
|
@@ -1418,6 +1419,37 @@ export class SqliteTaskStore {
|
|
|
1418
1419
|
return Object.freeze({ retained, deleted: toDelete.length });
|
|
1419
1420
|
});
|
|
1420
1421
|
}
|
|
1422
|
+
// -- context snapshots ------------------------------------------------------
|
|
1423
|
+
nextContextSnapshotId(taskId) {
|
|
1424
|
+
return this.#nextTaskRecordId(taskId, "contextSnapshot");
|
|
1425
|
+
}
|
|
1426
|
+
getContextSnapshot(taskId, snapshotId) {
|
|
1427
|
+
return this.#getPayload("context_snapshots", "task_id = ? AND snapshot_id = ?", [taskId, snapshotId]);
|
|
1428
|
+
}
|
|
1429
|
+
listContextSnapshots(taskId) {
|
|
1430
|
+
return this.#sortById(this.#listPayload("context_snapshots", "task_id = ?", [taskId]), (snapshot) => snapshot.id);
|
|
1431
|
+
}
|
|
1432
|
+
saveContextSnapshot(snapshot) {
|
|
1433
|
+
const stored = validateContextSnapshot(snapshot);
|
|
1434
|
+
this.#requireTask(stored.taskId);
|
|
1435
|
+
const existing = this.getContextSnapshot(stored.taskId, stored.id);
|
|
1436
|
+
if (existing !== null) {
|
|
1437
|
+
if (!isDeepStrictEqual(existing, stored)) {
|
|
1438
|
+
throw new StorageRecordError(`Context Snapshot is immutable: ${stored.id}.`);
|
|
1439
|
+
}
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
const duplicate = this.#db.prepare(`SELECT snapshot_id FROM context_snapshots
|
|
1443
|
+
WHERE task_id = ? AND scope = ? AND COALESCE(scope_ref, '') = COALESCE(?, '') AND sequence = ?`).get(stored.taskId, stored.scope, stored.scopeRef ?? null, stored.sequence);
|
|
1444
|
+
if (duplicate !== undefined) {
|
|
1445
|
+
throw new StorageRecordError(`Context Snapshot sequence already exists: ${stored.taskId}/${stored.scope}/${stored.sequence}.`);
|
|
1446
|
+
}
|
|
1447
|
+
this.#mutate(() => {
|
|
1448
|
+
this.#db.prepare(`INSERT INTO context_snapshots
|
|
1449
|
+
(task_id, snapshot_id, scope, scope_ref, sequence, digest, payload, frozen_at)
|
|
1450
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(stored.taskId, stored.id, stored.scope, stored.scopeRef ?? null, stored.sequence, stored.digest, this.#json(stored), stored.frozenAt);
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1421
1453
|
// -- agent runs -------------------------------------------------------------
|
|
1422
1454
|
nextAgentRunId(taskId) { return this.#nextTaskRecordId(taskId, "agentRun"); }
|
|
1423
1455
|
peekNextAgentRunId(taskId) { return this.#peekTaskRecordId(taskId, "agentRun"); }
|
|
@@ -1451,13 +1483,18 @@ export class SqliteTaskStore {
|
|
|
1451
1483
|
? ""
|
|
1452
1484
|
: ` AND tc.task_id IN (${selectedTaskIds.map(() => "?").join(", ")})`;
|
|
1453
1485
|
const rows = this.#db.prepare(`SELECT DISTINCT ar.task_id AS taskId, ar.run_id AS runId, ar.role_name AS roleName,
|
|
1454
|
-
json_extract(ar.payload, '$.providerRetry.
|
|
1486
|
+
json_extract(ar.payload, '$.providerRetry.state') AS state,
|
|
1487
|
+
CASE json_extract(ar.payload, '$.providerRetry.state')
|
|
1488
|
+
WHEN 'scheduled' THEN json_extract(ar.payload, '$.providerRetry.nextAttemptAt')
|
|
1489
|
+
ELSE json_extract(ar.payload, '$.providerRetry.episodeDeadlineAt')
|
|
1490
|
+
END AS dueAt
|
|
1455
1491
|
FROM tasks_catalog tc INDEXED BY idx_tasks_active
|
|
1456
1492
|
JOIN active_runs ap ON ap.task_id = tc.task_id
|
|
1457
1493
|
JOIN agent_runs ar ON ar.task_id = ap.task_id AND ar.run_id = ap.run_id
|
|
1458
1494
|
WHERE tc.is_active = 1
|
|
1459
1495
|
AND ar.status = 'active'
|
|
1460
|
-
AND json_extract(ar.payload, '$.providerRetry.
|
|
1496
|
+
AND json_extract(ar.payload, '$.providerRetry.state') IN
|
|
1497
|
+
('scheduled', 'dispatching', 'awaiting-progress')${taskPredicate}`).all(...(selectedTaskIds ?? []));
|
|
1461
1498
|
return rows.sort((left, right) => (numericCompare(left.taskId, right.taskId)
|
|
1462
1499
|
|| numericCompare(left.roleName, right.roleName)
|
|
1463
1500
|
|| numericCompare(left.runId, right.runId)));
|
|
@@ -1886,13 +1923,20 @@ export class SqliteTaskStore {
|
|
|
1886
1923
|
return rows.map((row) => this.#rowToMailbox(row));
|
|
1887
1924
|
}
|
|
1888
1925
|
saveWorkMailbox(mailbox) {
|
|
1889
|
-
|
|
1926
|
+
let validated;
|
|
1927
|
+
try {
|
|
1928
|
+
validated = validateWorkMailbox(mailbox);
|
|
1929
|
+
}
|
|
1930
|
+
catch (error) {
|
|
1931
|
+
throw new StorageRecordError(error instanceof Error ? error.message : String(error));
|
|
1932
|
+
}
|
|
1933
|
+
const cols = this.#mailboxCols(validated.target);
|
|
1890
1934
|
this.#mutate(() => {
|
|
1891
1935
|
this.#db.prepare(`INSERT INTO mailboxes (target_kind, task_id, role_name, target_key, next_sequence, processing, pending, input_delivery)
|
|
1892
1936
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1893
1937
|
ON CONFLICT(target_key) DO UPDATE SET next_sequence = excluded.next_sequence,
|
|
1894
1938
|
processing = excluded.processing, pending = excluded.pending,
|
|
1895
|
-
input_delivery = excluded.input_delivery`).run(cols.targetKind, cols.taskId, cols.roleName, cols.targetKey,
|
|
1939
|
+
input_delivery = excluded.input_delivery`).run(cols.targetKind, cols.taskId, cols.roleName, cols.targetKey, validated.nextSequence, validated.processing === null ? null : this.#json(validated.processing), this.#json(validated.pending), validated.inputDelivery === null ? null : this.#json(validated.inputDelivery));
|
|
1896
1940
|
});
|
|
1897
1941
|
}
|
|
1898
1942
|
removeWorkMailbox(target) {
|