@zq-silk/yui 0.11.3 → 0.12.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/ARCHITECTURE.md +23 -4
- package/README.md +44 -22
- package/dist/cli/commandCatalog.js +13 -10
- package/dist/cli/interactionPolicy.js +5 -6
- package/dist/cli/updateCommand.js +3 -1
- package/dist/cli/updateOrchestrator.js +173 -28
- package/dist/cli/updatePorts.js +137 -8
- package/dist/cli/upgradeCommand.js +19 -9
- package/dist/cli.js +156 -57
- package/dist/commands/configCommands.js +13 -46
- package/dist/commands/executionAuditCommands.js +3 -2
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/taskCommands.js +234 -200
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +61 -13
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +55 -7
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +3 -3
- package/dist/config/yuiConfig.js +3 -7
- package/dist/context/wakeNotification.js +20 -5
- package/dist/controller/agentRuntimeObserver.js +247 -51
- package/dist/controller/clientRuntime.js +39 -3
- package/dist/controller/controller.js +31 -27
- package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
- package/dist/controller/runtime.js +58 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +52 -50
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +129 -45
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +69 -5
- package/dist/observability/executionAudit.js +5 -0
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/repository/taskWorkspacePreparer.js +176 -60
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/deltaRecheck.js +12 -51
- package/dist/review/reviewAcceptance.js +26 -0
- package/dist/review/reviewConfig.js +0 -31
- package/dist/review/reviewDecision.js +113 -0
- package/dist/review/reviewOutcomeClassifier.js +1 -1
- package/dist/review/reviewRound.js +1 -1
- package/dist/review/reviewerAvailability.js +69 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/launchBroker.js +12 -0
- package/dist/runtime/processExitOutbox.js +88 -0
- package/dist/runtime/providerRuntimeIdentity.js +29 -1
- package/dist/runtime/runtimeHealthPolicy.js +5 -5
- package/dist/runtime/runtimeObservation.js +28 -0
- package/dist/runtime/runtimeProjection.js +22 -32
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +34 -199
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +42 -138
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +53 -31
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +379 -0
- package/dist/storage/sqliteSchema.js +66 -23
- package/dist/storage/sqliteStore.js +46 -23
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +15 -30
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/recordVersions.js +3 -2
- package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
- package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -58
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/client/components.js +13 -3
- package/dist/web/assets/client/i18n.js +6 -2
- package/dist/web/webSnapshot.js +7 -1
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +60 -20
- package/skills/yui-operator/SKILL.md +13 -4
- package/skills/yui-reviewer/SKILL.md +35 -11
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/lifecycle/taskRoleSessionReset.js +0 -126
- package/dist/scheduler/operatorNotification.js +0 -59
|
@@ -79,6 +79,8 @@ const STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION = 15;
|
|
|
79
79
|
const STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION = 16;
|
|
80
80
|
const STORED_TASK_WAKES_FROM_VERSION = 16;
|
|
81
81
|
const STORED_TASK_WAKES_TO_VERSION = 17;
|
|
82
|
+
const STORED_TASK_EVENT_NOTICES_FROM_VERSION = 17;
|
|
83
|
+
const STORED_TASK_EVENT_NOTICES_TO_VERSION = 18;
|
|
82
84
|
const TASK_WAKE_FROM_VERSION = 0;
|
|
83
85
|
const TASK_WAKE_TO_VERSION = 1;
|
|
84
86
|
const STORED_TASK_PUBLICATION_REFERENCES_FROM_VERSION = 16;
|
|
@@ -97,6 +99,8 @@ const PUBLICATION_REFERENCE_FROM_VERSION = 0;
|
|
|
97
99
|
const PUBLICATION_REFERENCE_TO_VERSION = 1;
|
|
98
100
|
const CONFIG_FROM_VERSION = 1;
|
|
99
101
|
const CONFIG_TO_VERSION = 2;
|
|
102
|
+
const CONFIG_REVIEW_CONTROLS_FROM_VERSION = 2;
|
|
103
|
+
const CONFIG_REVIEW_CONTROLS_TO_VERSION = 3;
|
|
100
104
|
/**
|
|
101
105
|
* Build the authoritative production graph. Transition intent and executable
|
|
102
106
|
* transforms are registered together here; compatible loading and offline
|
|
@@ -146,6 +150,7 @@ export function createProductionStorageRegistry() {
|
|
|
146
150
|
})
|
|
147
151
|
.registerOfflineMigration(projectOwnershipStep())
|
|
148
152
|
.registerOfflineMigration(configV2Step())
|
|
153
|
+
.registerOfflineMigration(configReviewControlsRemovalStep())
|
|
149
154
|
.registerCompatible(projectKnowledgeProposalsStep())
|
|
150
155
|
.registerCompatible(projectLifecycleStep())
|
|
151
156
|
.registerOfflineMigration(taskWorkspaceIdentityStep())
|
|
@@ -180,6 +185,7 @@ export function createProductionStorageRegistry() {
|
|
|
180
185
|
.registerCompatible(storedTaskDurableJobsStep())
|
|
181
186
|
.registerCompatible(storedTaskJobCallerKeyHashesStep())
|
|
182
187
|
.registerCompatible(storedTaskWakesAndPublicationReferencesStep())
|
|
188
|
+
.registerOfflineMigration(storedTaskEventNoticesStep())
|
|
183
189
|
.registerCompatible(taskWakeIntroductionStep())
|
|
184
190
|
.registerCompatible(durableJobIntroductionStep())
|
|
185
191
|
.registerOfflineMigration(capabilityGrantIntroductionStep())
|
|
@@ -246,6 +252,61 @@ function migrateConfigV1ToV2(snapshot) {
|
|
|
246
252
|
}
|
|
247
253
|
};
|
|
248
254
|
}
|
|
255
|
+
/** Removes retired Core controls for the Leader-owned Delta decision. */
|
|
256
|
+
function configReviewControlsRemovalStep() {
|
|
257
|
+
return {
|
|
258
|
+
axis: "record",
|
|
259
|
+
recordKind: "config",
|
|
260
|
+
fromVersion: CONFIG_REVIEW_CONTROLS_FROM_VERSION,
|
|
261
|
+
toVersion: CONFIG_REVIEW_CONTROLS_TO_VERSION,
|
|
262
|
+
preconditions: requireConfigV2,
|
|
263
|
+
transform: migrateConfigV2ToV3,
|
|
264
|
+
declaredEffects: []
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function requireConfigV2(snapshot) {
|
|
268
|
+
const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
269
|
+
if (versions.config !== CONFIG_REVIEW_CONTROLS_FROM_VERSION) {
|
|
270
|
+
throw new Error(`Record config migration requires manifest version ${CONFIG_REVIEW_CONTROLS_FROM_VERSION}.`);
|
|
271
|
+
}
|
|
272
|
+
if (snapshot.state === null)
|
|
273
|
+
return;
|
|
274
|
+
const config = asObject(snapshot.state.config, "Yui config");
|
|
275
|
+
if (config.schemaVersion !== CONFIG_REVIEW_CONTROLS_FROM_VERSION) {
|
|
276
|
+
throw new Error(`Yui config must use schemaVersion ${CONFIG_REVIEW_CONTROLS_FROM_VERSION} before migration.`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function migrateConfigV2ToV3(snapshot) {
|
|
280
|
+
requireConfigV2(snapshot);
|
|
281
|
+
const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
282
|
+
const schemaManifest = {
|
|
283
|
+
...snapshot.schemaManifest,
|
|
284
|
+
recordVersions: { ...versions, config: CONFIG_REVIEW_CONTROLS_TO_VERSION }
|
|
285
|
+
};
|
|
286
|
+
if (snapshot.state === null)
|
|
287
|
+
return { schemaManifest, state: null };
|
|
288
|
+
const config = asObject(snapshot.state.config, "Yui config");
|
|
289
|
+
const review = config.review === undefined
|
|
290
|
+
? undefined
|
|
291
|
+
: asObject(config.review, "Yui review config");
|
|
292
|
+
const migratedReview = review === undefined
|
|
293
|
+
? undefined
|
|
294
|
+
: (() => {
|
|
295
|
+
const { deltaRecheck: _retiredSwitch, deltaRecheckMaxChangedLines: _retiredLineThreshold, deltaRecheckMaxChangedFiles: _retiredFileThreshold, ...retained } = review;
|
|
296
|
+
return retained;
|
|
297
|
+
})();
|
|
298
|
+
return {
|
|
299
|
+
schemaManifest,
|
|
300
|
+
state: {
|
|
301
|
+
...snapshot.state,
|
|
302
|
+
config: {
|
|
303
|
+
...config,
|
|
304
|
+
schemaVersion: CONFIG_REVIEW_CONTROLS_TO_VERSION,
|
|
305
|
+
...(migratedReview === undefined ? {} : { review: migratedReview })
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
249
310
|
function workItemExecutionGroupHistoryStep() {
|
|
250
311
|
return {
|
|
251
312
|
axis: "record",
|
|
@@ -1241,6 +1302,324 @@ function normalizeStoredTaskV16ToV17(snapshot) {
|
|
|
1241
1302
|
state: { ...snapshot.state, tasks: nextTasks }
|
|
1242
1303
|
};
|
|
1243
1304
|
}
|
|
1305
|
+
/** StoredTask v18 retires the mutable OperatorNotification projection. */
|
|
1306
|
+
function storedTaskEventNoticesStep() {
|
|
1307
|
+
return {
|
|
1308
|
+
axis: "record",
|
|
1309
|
+
recordKind: "storedTask",
|
|
1310
|
+
fromVersion: STORED_TASK_EVENT_NOTICES_FROM_VERSION,
|
|
1311
|
+
toVersion: STORED_TASK_EVENT_NOTICES_TO_VERSION,
|
|
1312
|
+
preconditions: requireStoredTaskV17OperatorNoticeShape,
|
|
1313
|
+
transform: normalizeStoredTaskV17ToV18,
|
|
1314
|
+
declaredEffects: ["operator-mailbox-event-references"]
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
const STORED_TASK_V17_FIELDS = [
|
|
1318
|
+
"schemaVersion",
|
|
1319
|
+
"task",
|
|
1320
|
+
"idHighWaterMarks",
|
|
1321
|
+
"brief",
|
|
1322
|
+
"changeSets",
|
|
1323
|
+
"integrationAttempts",
|
|
1324
|
+
"integrationQueue",
|
|
1325
|
+
"durableJobs",
|
|
1326
|
+
"roles",
|
|
1327
|
+
"managedWorkspaces",
|
|
1328
|
+
"roleSessionSets",
|
|
1329
|
+
"jobCallerKeyHashes",
|
|
1330
|
+
"workItems",
|
|
1331
|
+
"contextSnapshots",
|
|
1332
|
+
"agentRuns",
|
|
1333
|
+
"reviewRounds",
|
|
1334
|
+
"activeRuns",
|
|
1335
|
+
"messages",
|
|
1336
|
+
"inputRequests",
|
|
1337
|
+
"decisions",
|
|
1338
|
+
"milestones",
|
|
1339
|
+
"events",
|
|
1340
|
+
"wakes",
|
|
1341
|
+
"capabilityGrants",
|
|
1342
|
+
"releaseWorkflows",
|
|
1343
|
+
"publicationReferences",
|
|
1344
|
+
"leaderFailure",
|
|
1345
|
+
"operatorNotification"
|
|
1346
|
+
];
|
|
1347
|
+
function requireStoredTaskV17OperatorNoticeShape(snapshot) {
|
|
1348
|
+
const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
1349
|
+
if (versions.storedTask !== STORED_TASK_EVENT_NOTICES_FROM_VERSION) {
|
|
1350
|
+
throw new Error(`Record storedTask compatible step requires manifest version ${STORED_TASK_EVENT_NOTICES_FROM_VERSION}.`);
|
|
1351
|
+
}
|
|
1352
|
+
if (snapshot.state === null)
|
|
1353
|
+
return;
|
|
1354
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
1355
|
+
for (const [taskId, rawTask] of Object.entries(tasks)) {
|
|
1356
|
+
const task = asObject(rawTask, `Task aggregate ${taskId}`);
|
|
1357
|
+
if (task.schemaVersion !== STORED_TASK_EVENT_NOTICES_FROM_VERSION) {
|
|
1358
|
+
throw new Error(`Task aggregate ${taskId} must use schemaVersion ${STORED_TASK_EVENT_NOTICES_FROM_VERSION}.`);
|
|
1359
|
+
}
|
|
1360
|
+
const allowed = new Set(STORED_TASK_V17_FIELDS);
|
|
1361
|
+
const unknown = Object.keys(task).find((key) => !allowed.has(key));
|
|
1362
|
+
if (unknown !== undefined) {
|
|
1363
|
+
throw new Error(`Task aggregate ${taskId} has an unknown v17 field: ${unknown}.`);
|
|
1364
|
+
}
|
|
1365
|
+
const missing = STORED_TASK_V17_FIELDS.find((key) => !Object.hasOwn(task, key));
|
|
1366
|
+
if (missing !== undefined) {
|
|
1367
|
+
throw new Error(`Task aggregate ${taskId} is missing v17 field: ${missing}.`);
|
|
1368
|
+
}
|
|
1369
|
+
if (task.operatorNotification !== null) {
|
|
1370
|
+
requireLegacyOperatorNotice(task.operatorNotification, taskId);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function requireLegacyOperatorNotice(value, taskId) {
|
|
1375
|
+
const notice = asObject(value, `Operator notification ${taskId}`);
|
|
1376
|
+
if (notice.schemaVersion !== 1 || notice.taskId !== taskId) {
|
|
1377
|
+
throw new Error(`Operator notification identity is invalid: ${taskId}.`);
|
|
1378
|
+
}
|
|
1379
|
+
if (notice.type === "leader-recovery-failed") {
|
|
1380
|
+
requireLegacyNoticeText(notice.message, `Operator notification message ${taskId}`);
|
|
1381
|
+
}
|
|
1382
|
+
else if (notice.type === "leader-stalled") {
|
|
1383
|
+
requireLegacyNoticeText(notice.message, `Operator notification message ${taskId}`);
|
|
1384
|
+
requireLegacyNoticeText(notice.runId, `Operator notification Run ${taskId}`);
|
|
1385
|
+
requireLegacyNoticeTimestamp(notice.progressAt, `Operator notification progress ${taskId}`);
|
|
1386
|
+
requireLegacyNoticeText(notice.evidenceKey, `Operator notification evidence ${taskId}`);
|
|
1387
|
+
if (notice.classification !== "truly-stalled") {
|
|
1388
|
+
throw new Error(`Operator notification classification is invalid: ${taskId}.`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
else if (notice.type === "task-terminal") {
|
|
1392
|
+
if (notice.status !== "completed" && notice.status !== "retired") {
|
|
1393
|
+
throw new Error(`Operator notification Task status is invalid: ${taskId}.`);
|
|
1394
|
+
}
|
|
1395
|
+
if (!["user", "operator", "leader"].includes(String(notice.by))) {
|
|
1396
|
+
throw new Error(`Operator notification Task actor is invalid: ${taskId}.`);
|
|
1397
|
+
}
|
|
1398
|
+
requireLegacyNoticeText(notice.summary, `Operator notification summary ${taskId}`);
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
throw new Error(`Operator notification type is invalid: ${taskId}.`);
|
|
1402
|
+
}
|
|
1403
|
+
requireLegacyNoticeTimestamp(notice.createdAt, `Operator notification createdAt ${taskId}`);
|
|
1404
|
+
requireLegacyNoticeTimestamp(notice.updatedAt, `Operator notification updatedAt ${taskId}`);
|
|
1405
|
+
}
|
|
1406
|
+
function requireLegacyNoticeText(value, label) {
|
|
1407
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
|
|
1408
|
+
throw new Error(`${label} is invalid.`);
|
|
1409
|
+
}
|
|
1410
|
+
return value;
|
|
1411
|
+
}
|
|
1412
|
+
function requireLegacyNoticeTimestamp(value, label) {
|
|
1413
|
+
const timestamp = requireLegacyNoticeText(value, label);
|
|
1414
|
+
if (!Number.isFinite(Date.parse(timestamp)))
|
|
1415
|
+
throw new Error(`${label} is invalid.`);
|
|
1416
|
+
return timestamp;
|
|
1417
|
+
}
|
|
1418
|
+
function normalizeStoredTaskV17ToV18(snapshot) {
|
|
1419
|
+
requireStoredTaskV17OperatorNoticeShape(snapshot);
|
|
1420
|
+
const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
1421
|
+
const schemaManifest = {
|
|
1422
|
+
...snapshot.schemaManifest,
|
|
1423
|
+
recordVersions: {
|
|
1424
|
+
...versions,
|
|
1425
|
+
storedTask: STORED_TASK_EVENT_NOTICES_TO_VERSION
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
if (snapshot.state === null)
|
|
1429
|
+
return { schemaManifest, state: null };
|
|
1430
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
1431
|
+
const nextTasks = {};
|
|
1432
|
+
const legacyNotices = new Map();
|
|
1433
|
+
for (const [taskId, rawTask] of Object.entries(tasks)) {
|
|
1434
|
+
const task = asObject(rawTask, `Task aggregate ${taskId}`);
|
|
1435
|
+
legacyNotices.set(taskId, task.operatorNotification === null
|
|
1436
|
+
? null
|
|
1437
|
+
: asObject(task.operatorNotification, `Operator notification ${taskId}`));
|
|
1438
|
+
const nextTask = {
|
|
1439
|
+
...task,
|
|
1440
|
+
schemaVersion: STORED_TASK_EVENT_NOTICES_TO_VERSION,
|
|
1441
|
+
events: { ...asObject(task.events, `Task events ${taskId}`) },
|
|
1442
|
+
idHighWaterMarks: {
|
|
1443
|
+
...asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`)
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
delete nextTask.operatorNotification;
|
|
1447
|
+
nextTasks[taskId] = nextTask;
|
|
1448
|
+
}
|
|
1449
|
+
const eventByTask = new Map();
|
|
1450
|
+
const eventRefForTask = (taskId) => {
|
|
1451
|
+
const existing = eventByTask.get(taskId);
|
|
1452
|
+
if (existing !== undefined)
|
|
1453
|
+
return { type: "event", taskId, id: existing };
|
|
1454
|
+
const task = asObject(nextTasks[taskId], `Task aggregate ${taskId}`);
|
|
1455
|
+
const events = asObject(task.events, `Task events ${taskId}`);
|
|
1456
|
+
const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
|
|
1457
|
+
let sequence = Number(marks.event ?? 0) + 1;
|
|
1458
|
+
if (!Number.isSafeInteger(sequence) || sequence < 1) {
|
|
1459
|
+
throw new Error(`Task event high-water mark is invalid: ${taskId}.`);
|
|
1460
|
+
}
|
|
1461
|
+
while (events[`event-${sequence}`] !== undefined)
|
|
1462
|
+
sequence += 1;
|
|
1463
|
+
const eventId = `event-${sequence}`;
|
|
1464
|
+
const notice = legacyNotices.get(taskId) ?? null;
|
|
1465
|
+
const migrated = migratedOperatorNoticeEvent(taskId, eventId, notice, task);
|
|
1466
|
+
events[eventId] = migrated;
|
|
1467
|
+
marks.event = sequence;
|
|
1468
|
+
eventByTask.set(taskId, eventId);
|
|
1469
|
+
return { type: "event", taskId, id: eventId };
|
|
1470
|
+
};
|
|
1471
|
+
const mailboxes = asObject(snapshot.state.mailboxes, "state mailboxes");
|
|
1472
|
+
const nextMailboxes = {};
|
|
1473
|
+
for (const [key, rawMailbox] of Object.entries(mailboxes)) {
|
|
1474
|
+
const mailbox = asObject(rawMailbox, `WorkMailbox ${key}`);
|
|
1475
|
+
const target = asObject(mailbox.target, `WorkMailbox target ${key}`);
|
|
1476
|
+
nextMailboxes[key] = target.kind === "operator"
|
|
1477
|
+
? rewriteLegacyOperatorMailbox(mailbox, key, eventRefForTask)
|
|
1478
|
+
: { ...mailbox };
|
|
1479
|
+
}
|
|
1480
|
+
for (const [taskId, notice] of legacyNotices) {
|
|
1481
|
+
if (notice !== null)
|
|
1482
|
+
eventRefForTask(taskId);
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
schemaManifest,
|
|
1486
|
+
state: { ...snapshot.state, tasks: nextTasks, mailboxes: nextMailboxes }
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
function migratedOperatorNoticeEvent(taskId, eventId, notice, taskAggregate) {
|
|
1490
|
+
const task = asObject(taskAggregate.task, `Task ${taskId}`);
|
|
1491
|
+
const timestamp = String(notice?.updatedAt ?? task.updatedAt ?? task.createdAt ?? "");
|
|
1492
|
+
if (!Number.isFinite(Date.parse(timestamp))) {
|
|
1493
|
+
throw new Error(`Legacy Operator notice timestamp is invalid: ${taskId}.`);
|
|
1494
|
+
}
|
|
1495
|
+
const type = notice?.type;
|
|
1496
|
+
if (notice !== null && type === "task-terminal") {
|
|
1497
|
+
const status = String(notice.status ?? "");
|
|
1498
|
+
if (status !== "completed" && status !== "retired") {
|
|
1499
|
+
throw new Error(`Legacy terminal Operator notice status is invalid: ${taskId}.`);
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
schemaVersion: 2,
|
|
1503
|
+
id: eventId,
|
|
1504
|
+
taskId,
|
|
1505
|
+
type: `task.${status}`,
|
|
1506
|
+
payload: {
|
|
1507
|
+
status,
|
|
1508
|
+
by: String(notice.by ?? "operator"),
|
|
1509
|
+
summary: String(notice.summary ?? "Task reached a terminal state."),
|
|
1510
|
+
migratedFrom: "operator-notification"
|
|
1511
|
+
},
|
|
1512
|
+
createdAt: timestamp
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
if (notice !== null && type === "leader-stalled") {
|
|
1516
|
+
return {
|
|
1517
|
+
schemaVersion: 2,
|
|
1518
|
+
id: eventId,
|
|
1519
|
+
taskId,
|
|
1520
|
+
type: "run.stalled",
|
|
1521
|
+
payload: {
|
|
1522
|
+
runId: String(notice.runId ?? "unknown"),
|
|
1523
|
+
roleName: "leader",
|
|
1524
|
+
progressAt: String(notice.progressAt ?? timestamp),
|
|
1525
|
+
classification: "truly-stalled",
|
|
1526
|
+
evidenceKey: String(notice.evidenceKey ?? "legacy-operator-notification"),
|
|
1527
|
+
migratedFrom: "operator-notification"
|
|
1528
|
+
},
|
|
1529
|
+
createdAt: timestamp
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
return {
|
|
1533
|
+
schemaVersion: 2,
|
|
1534
|
+
id: eventId,
|
|
1535
|
+
taskId,
|
|
1536
|
+
type: "leader.attention-required",
|
|
1537
|
+
payload: {
|
|
1538
|
+
reason: type === "leader-recovery-failed"
|
|
1539
|
+
? "leader-recovery-failed"
|
|
1540
|
+
: "legacy-operator-mailbox",
|
|
1541
|
+
message: String(notice?.message ?? "Inspect the migrated Operator notice."),
|
|
1542
|
+
migratedFrom: notice === null ? "operator-mailbox" : "operator-notification"
|
|
1543
|
+
},
|
|
1544
|
+
createdAt: timestamp
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
function rewriteLegacyOperatorMailbox(mailbox, key, eventRefForTask) {
|
|
1548
|
+
const rewriteRef = (rawRef) => {
|
|
1549
|
+
const ref = asObject(rawRef, `WorkMailbox ${key} entity ref`);
|
|
1550
|
+
if (ref.type === "input" || ref.type === "event")
|
|
1551
|
+
return { ...ref };
|
|
1552
|
+
const taskId = ref.type === "task" ? ref.id : ref.taskId;
|
|
1553
|
+
if (typeof taskId !== "string" || taskId.trim().length === 0) {
|
|
1554
|
+
throw new Error(`Legacy Operator mailbox ref has no Task identity: ${key}.`);
|
|
1555
|
+
}
|
|
1556
|
+
return eventRefForTask(taskId);
|
|
1557
|
+
};
|
|
1558
|
+
const rewriteBatch = (rawBatch, label) => {
|
|
1559
|
+
const batch = asObject(rawBatch, label);
|
|
1560
|
+
if (!Array.isArray(batch.refs))
|
|
1561
|
+
throw new Error(`${label} refs must be an array.`);
|
|
1562
|
+
const sourceRefs = batch.refs.map((rawRef) => asObject(rawRef, `${label} entity ref`));
|
|
1563
|
+
const semanticTaskIds = new Set(sourceRefs.flatMap((ref) => ((ref.type === "input" || ref.type === "event") && typeof ref.taskId === "string"
|
|
1564
|
+
? [ref.taskId]
|
|
1565
|
+
: [])));
|
|
1566
|
+
const refs = [];
|
|
1567
|
+
const seen = new Set();
|
|
1568
|
+
for (const sourceRef of sourceRefs) {
|
|
1569
|
+
const referencedTaskId = sourceRef.type === "task" ? sourceRef.id : sourceRef.taskId;
|
|
1570
|
+
if (sourceRef.type !== "input"
|
|
1571
|
+
&& sourceRef.type !== "event"
|
|
1572
|
+
&& typeof referencedTaskId === "string"
|
|
1573
|
+
&& semanticTaskIds.has(referencedTaskId)) {
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
const ref = rewriteRef(sourceRef);
|
|
1577
|
+
const identity = `${String(ref.type)}\0${String(ref.taskId ?? "")}\0${String(ref.id)}`;
|
|
1578
|
+
if (seen.has(identity))
|
|
1579
|
+
continue;
|
|
1580
|
+
seen.add(identity);
|
|
1581
|
+
refs.push(ref);
|
|
1582
|
+
}
|
|
1583
|
+
return { ...batch, refs };
|
|
1584
|
+
};
|
|
1585
|
+
const rewriteProcessing = (raw) => {
|
|
1586
|
+
if (raw === null)
|
|
1587
|
+
return null;
|
|
1588
|
+
const processing = asObject(raw, `WorkMailbox ${key} processing`);
|
|
1589
|
+
return {
|
|
1590
|
+
...processing,
|
|
1591
|
+
batch: rewriteBatch(processing.batch, `WorkMailbox ${key} processing batch`),
|
|
1592
|
+
...(processing.executionRef === undefined
|
|
1593
|
+
? {}
|
|
1594
|
+
: { executionRef: rewriteRef(processing.executionRef) })
|
|
1595
|
+
};
|
|
1596
|
+
};
|
|
1597
|
+
const rewriteDelivery = (raw) => {
|
|
1598
|
+
if (raw === null)
|
|
1599
|
+
return null;
|
|
1600
|
+
const delivery = asObject(raw, `WorkMailbox ${key} input delivery`);
|
|
1601
|
+
return {
|
|
1602
|
+
...delivery,
|
|
1603
|
+
batch: rewriteBatch(delivery.batch, `WorkMailbox ${key} input delivery batch`),
|
|
1604
|
+
executionRef: rewriteRef(delivery.executionRef)
|
|
1605
|
+
};
|
|
1606
|
+
};
|
|
1607
|
+
const pending = asObject(mailbox.pending, `WorkMailbox ${key} pending lanes`);
|
|
1608
|
+
return {
|
|
1609
|
+
...mailbox,
|
|
1610
|
+
processing: rewriteProcessing(mailbox.processing),
|
|
1611
|
+
pending: {
|
|
1612
|
+
...pending,
|
|
1613
|
+
normal: pending.normal === null
|
|
1614
|
+
? null
|
|
1615
|
+
: rewriteBatch(pending.normal, `WorkMailbox ${key} normal batch`),
|
|
1616
|
+
userCorrection: pending.userCorrection === null
|
|
1617
|
+
? null
|
|
1618
|
+
: rewriteBatch(pending.userCorrection, `WorkMailbox ${key} correction batch`)
|
|
1619
|
+
},
|
|
1620
|
+
inputDelivery: rewriteDelivery(mailbox.inputDelivery)
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1244
1623
|
/**
|
|
1245
1624
|
* The taskWake record family is introduced alongside StoredTask v17. The
|
|
1246
1625
|
* compatible step supplies the empty map and high-water mark; this step only
|
|
@@ -327,7 +327,8 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
327
327
|
);
|
|
328
328
|
CREATE INDEX IF NOT EXISTS idx_events_type_time ON events(task_id, type, occurred_at);
|
|
329
329
|
|
|
330
|
-
-- Per-task scheduler projections
|
|
330
|
+
-- Per-task scheduler projections. operator-notification is a retired legacy
|
|
331
|
+
-- kind retained only so old layout-7 Homes can be read by the upgrade path.
|
|
331
332
|
CREATE TABLE IF NOT EXISTS task_projections (
|
|
332
333
|
task_id TEXT NOT NULL,
|
|
333
334
|
kind TEXT NOT NULL CHECK (kind IN ('leader-failure','operator-notification')),
|
|
@@ -930,6 +931,12 @@ CREATE TABLE IF NOT EXISTS context_snapshots (
|
|
|
930
931
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_context_snapshots_scope_sequence
|
|
931
932
|
ON context_snapshots(task_id, scope, COALESCE(scope_ref, ''), sequence);
|
|
932
933
|
`;
|
|
934
|
+
/**
|
|
935
|
+
* The single forward migration history for current SQLite Homes. New durable
|
|
936
|
+
* layout, aggregate, and record changes belong here and must be expressible as
|
|
937
|
+
* one atomic in-place transaction. The separate logical migration registry is
|
|
938
|
+
* only the bridge from valid pre-SQLite Homes and is allowed to rebuild them.
|
|
939
|
+
*/
|
|
933
940
|
const MIGRATIONS = [
|
|
934
941
|
{ version: 1, axis: "layout", sql: MIGRATION_1_SQL },
|
|
935
942
|
{ version: 2, axis: "record", recordKind: "durableJob+capability-grant+release-workflow", sql: MIGRATION_2_SQL },
|
|
@@ -986,10 +993,13 @@ const SCHEMA_MIGRATIONS_SQL = `
|
|
|
986
993
|
* layout migrations. A database with any sqlite_master object is therefore
|
|
987
994
|
* diagnosed as corrupt/partially initialized and left untouched.
|
|
988
995
|
*/
|
|
989
|
-
function ensureMigrationLedger(db) {
|
|
996
|
+
function ensureMigrationLedger(db, mode) {
|
|
990
997
|
const objects = db.prepare("SELECT type, name FROM sqlite_master WHERE name IS NOT NULL").all();
|
|
991
998
|
const ledger = objects.find(({ name }) => name === "schema_migrations");
|
|
992
999
|
if (ledger === undefined) {
|
|
1000
|
+
if (mode === "validate") {
|
|
1001
|
+
throw new SqliteSchemaMigrationError("schema_migrations ledger is missing from an existing database");
|
|
1002
|
+
}
|
|
993
1003
|
if (objects.length !== 0) {
|
|
994
1004
|
throw new SqliteSchemaMigrationError("schema_migrations ledger is missing from a non-empty database");
|
|
995
1005
|
}
|
|
@@ -1134,34 +1144,67 @@ function validatePendingProjectionBackfillSources(db) {
|
|
|
1134
1144
|
}
|
|
1135
1145
|
}
|
|
1136
1146
|
/**
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
* `schema_migrations` bookkeeping commit atomically, so a crash mid-migration
|
|
1141
|
-
* rolls back and the next open re-applies cleanly. Re-running on a current
|
|
1142
|
-
* database performs no work (every version is already recorded).
|
|
1147
|
+
* Inspect a database's checksummed migration prefix without changing it.
|
|
1148
|
+
* Pending versions are a supported upgrade state; malformed, gapped, changed,
|
|
1149
|
+
* or future ledger entries remain explicit integrity failures.
|
|
1143
1150
|
*/
|
|
1144
|
-
export function
|
|
1145
|
-
const ledgerWasCreated = ensureMigrationLedger(db);
|
|
1146
|
-
// Validate the complete ledger before touching any pending migration. This
|
|
1147
|
-
// prevents a manually altered or partially recorded ledger from silently
|
|
1148
|
-
// skipping the partial-index/projection migrations added after a valid Home.
|
|
1151
|
+
export function inspectSqliteSchemaMigrations(db) {
|
|
1152
|
+
const ledgerWasCreated = ensureMigrationLedger(db, "validate");
|
|
1149
1153
|
const applied = validateAppliedMigrations(db, ledgerWasCreated);
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1154
|
+
const pendingVersions = MIGRATIONS
|
|
1155
|
+
.filter((migration) => !applied.has(migration.version))
|
|
1156
|
+
.map((migration) => migration.version);
|
|
1157
|
+
if (pendingVersions.length === 0)
|
|
1158
|
+
validateSchemaObjects(db);
|
|
1159
|
+
const current = MIGRATIONS[applied.size - 1];
|
|
1160
|
+
const target = MIGRATIONS.at(-1);
|
|
1161
|
+
if (current === undefined) {
|
|
1162
|
+
throw new SqliteSchemaMigrationError("schema_migrations ledger has no current head");
|
|
1163
|
+
}
|
|
1164
|
+
return {
|
|
1165
|
+
currentVersion: applied.size,
|
|
1166
|
+
currentChecksum: checksum(current.sql),
|
|
1167
|
+
targetVersion: SQLITE_SCHEMA_VERSION,
|
|
1168
|
+
targetChecksum: checksum(target.sql),
|
|
1169
|
+
pendingVersions
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Apply or validate schema migrations without letting an ordinary open mutate
|
|
1174
|
+
* an existing authoritative database.
|
|
1175
|
+
*
|
|
1176
|
+
* In `apply` mode every pending DDL/data step and every ledger row runs in one
|
|
1177
|
+
* outer transaction. The database therefore advances to the release version
|
|
1178
|
+
* as one commit or remains entirely at its previous version. `validate` mode
|
|
1179
|
+
* rejects a pending version before executing any migration.
|
|
1180
|
+
*/
|
|
1181
|
+
export function migrateSqliteSchema(db, options) {
|
|
1182
|
+
const migrate = () => {
|
|
1183
|
+
const ledgerWasCreated = ensureMigrationLedger(db, options.mode);
|
|
1184
|
+
// Validate the complete ledger before touching any pending migration. This
|
|
1185
|
+
// prevents a manually altered or partially recorded ledger from silently
|
|
1186
|
+
// skipping a later schema/data step.
|
|
1187
|
+
const applied = validateAppliedMigrations(db, ledgerWasCreated);
|
|
1188
|
+
const pending = MIGRATIONS.filter((migration) => !applied.has(migration.version));
|
|
1189
|
+
if (options.mode === "validate" && pending.length > 0) {
|
|
1190
|
+
throw new SqliteSchemaMigrationError(`pending SQLite schema migration ${pending[0].version}; `
|
|
1191
|
+
+ "run the explicit storage upgrade before opening this database", "admission");
|
|
1192
|
+
}
|
|
1193
|
+
const newlyApplied = [];
|
|
1194
|
+
for (const migration of pending) {
|
|
1155
1195
|
if (migration.version === 13)
|
|
1156
1196
|
validatePendingProjectionBackfillSources(db);
|
|
1157
1197
|
db.exec(migration.sql);
|
|
1158
1198
|
db.prepare(`INSERT INTO schema_migrations (version, axis, record_kind, applied_at, checksum)
|
|
1159
1199
|
VALUES (?, ?, ?, ?, ?)`).run(migration.version, migration.axis, migration.recordKind ?? null, new Date().toISOString(), checksum(migration.sql));
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1200
|
+
newlyApplied.push(migration.version);
|
|
1201
|
+
}
|
|
1202
|
+
validateSchemaObjects(db);
|
|
1203
|
+
return newlyApplied;
|
|
1204
|
+
};
|
|
1205
|
+
const newlyApplied = options.mode === "apply" && !db.inTransaction
|
|
1206
|
+
? db.transaction(migrate)()
|
|
1207
|
+
: migrate();
|
|
1165
1208
|
return { applied: newlyApplied, version: SQLITE_SCHEMA_VERSION };
|
|
1166
1209
|
}
|
|
1167
1210
|
/** The names of every table the schema creates (for tests/introspection). */
|
|
@@ -55,7 +55,7 @@ import { assertHomeWritable } from "./upgradeFence.js";
|
|
|
55
55
|
import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_PENDING_WAKEUP_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, executionLaneActiveRunKeyParts, StorageConflictError, StorageCancelledError, StorageRecordError, FileTaskStore, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, validateYuiConfig } from "./taskStore.js";
|
|
56
56
|
import { publicationExternalKey } from "../task/publicationReference.js";
|
|
57
57
|
import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
|
|
58
|
-
import { migrateSqliteSchema, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_RUN_CAP } from "./sqliteSchema.js";
|
|
58
|
+
import { inspectSqliteSchemaMigrations, migrateSqliteSchema, SqliteSchemaMigrationError, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_RUN_CAP } from "./sqliteSchema.js";
|
|
59
59
|
import { inspectStorageSchema } from "./storageSchema.js";
|
|
60
60
|
/** Read the immutable Home identity without opening a writable Store connection. */
|
|
61
61
|
export function readSqliteHomeIdentity(rootDir, databaseFilename = "yui.db") {
|
|
@@ -136,6 +136,7 @@ export class SqliteTaskStore {
|
|
|
136
136
|
#db;
|
|
137
137
|
#rootDir;
|
|
138
138
|
#migration;
|
|
139
|
+
#openedSchemaHead;
|
|
139
140
|
#inTransaction = false;
|
|
140
141
|
#dirty = false;
|
|
141
142
|
constructor(rootDir, _options = {}) {
|
|
@@ -143,16 +144,34 @@ export class SqliteTaskStore {
|
|
|
143
144
|
this.#migration = _options.migration ?? false;
|
|
144
145
|
mkdirSync(rootDir, { recursive: true, mode: 0o700 });
|
|
145
146
|
const filename = _options.databaseFilename ?? "yui.db";
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.#db
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
147
|
+
const databasePath = join(rootDir, filename);
|
|
148
|
+
const databaseExisted = existsSync(databasePath);
|
|
149
|
+
if (this.#migration && filename === "yui.db") {
|
|
150
|
+
throw new SqliteSchemaMigrationError("offline migration mode cannot open the authoritative yui.db", "admission");
|
|
151
|
+
}
|
|
152
|
+
this.#db = new Database(databasePath);
|
|
153
|
+
try {
|
|
154
|
+
// §4.1 / §9: WAL, no fsync weakening, FKs on, busy timeout for CLI contention.
|
|
155
|
+
this.#db.pragma("journal_mode = WAL");
|
|
156
|
+
this.#db.pragma("synchronous = FULL");
|
|
157
|
+
this.#db.pragma("foreign_keys = ON");
|
|
158
|
+
this.#db.pragma("busy_timeout = 5000");
|
|
159
|
+
this.#db.pragma("wal_autocheckpoint = 1000");
|
|
160
|
+
migrateSqliteSchema(this.#db, {
|
|
161
|
+
mode: this.#migration || !databaseExisted ? "apply" : "validate"
|
|
162
|
+
});
|
|
163
|
+
const schema = inspectSqliteSchemaMigrations(this.#db);
|
|
164
|
+
this.#openedSchemaHead = {
|
|
165
|
+
version: schema.currentVersion,
|
|
166
|
+
checksum: schema.currentChecksum
|
|
167
|
+
};
|
|
168
|
+
this.#seedHomeMeta();
|
|
169
|
+
this.#seedConfig();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
this.#db.close();
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
156
175
|
}
|
|
157
176
|
rootDirectory() { return this.#rootDir; }
|
|
158
177
|
/** Close the underlying database connection. */
|
|
@@ -202,6 +221,13 @@ export class SqliteTaskStore {
|
|
|
202
221
|
if (this.#migration)
|
|
203
222
|
return;
|
|
204
223
|
assertHomeWritable(this.#rootDir);
|
|
224
|
+
const current = this.#db.prepare("SELECT version, checksum FROM schema_migrations ORDER BY version DESC LIMIT 1").get();
|
|
225
|
+
if (current?.version !== this.#openedSchemaHead.version
|
|
226
|
+
|| current.checksum !== this.#openedSchemaHead.checksum) {
|
|
227
|
+
throw new SqliteSchemaMigrationError(`open Store schema head ${this.#openedSchemaHead.version}/${this.#openedSchemaHead.checksum} `
|
|
228
|
+
+ `changed to ${String(current?.version)}/${String(current?.checksum)}; `
|
|
229
|
+
+ "reopen the Store with the active Yui version before writing", "admission");
|
|
230
|
+
}
|
|
205
231
|
}
|
|
206
232
|
#bumpRevision() {
|
|
207
233
|
this.#db.prepare("UPDATE home_meta SET revision = revision + 1, updated_at = ? WHERE id = 1").run(this.#now());
|
|
@@ -217,9 +243,12 @@ export class SqliteTaskStore {
|
|
|
217
243
|
this.#dirty = true;
|
|
218
244
|
return result;
|
|
219
245
|
}
|
|
220
|
-
this.#prepareWrite();
|
|
221
246
|
this.#begin();
|
|
222
247
|
try {
|
|
248
|
+
// Acquire SQLite's write reservation before checking the upgrade fence.
|
|
249
|
+
// A writer that started first commits before the upgrader's final
|
|
250
|
+
// transactional inventory; a writer that starts later sees the fence.
|
|
251
|
+
this.#prepareWrite();
|
|
223
252
|
const result = fn();
|
|
224
253
|
if (!this.#migration) {
|
|
225
254
|
this.#bumpRevision();
|
|
@@ -238,9 +267,12 @@ export class SqliteTaskStore {
|
|
|
238
267
|
return run(this);
|
|
239
268
|
this.#begin();
|
|
240
269
|
try {
|
|
270
|
+
// Pin the Store's validated schema generation before user code runs.
|
|
271
|
+
// This prevents a long-lived pre-upgrade Store from executing old SQL
|
|
272
|
+
// after an in-place migration has committed.
|
|
273
|
+
this.#prepareWrite();
|
|
241
274
|
const result = run(this);
|
|
242
275
|
if (this.#dirty) {
|
|
243
|
-
this.#prepareWrite();
|
|
244
276
|
if (options?.requestId !== undefined) {
|
|
245
277
|
this.#insertOutbox(options.requestId, options.outboxCommand ?? null);
|
|
246
278
|
}
|
|
@@ -265,9 +297,9 @@ export class SqliteTaskStore {
|
|
|
265
297
|
async transactionAsync(execute) {
|
|
266
298
|
if (this.#inTransaction)
|
|
267
299
|
return execute(this);
|
|
268
|
-
this.#prepareWrite();
|
|
269
300
|
this.#begin();
|
|
270
301
|
try {
|
|
302
|
+
this.#prepareWrite();
|
|
271
303
|
const result = await execute(this);
|
|
272
304
|
if (this.#dirty)
|
|
273
305
|
this.#bumpRevision();
|
|
@@ -2048,15 +2080,6 @@ export class SqliteTaskStore {
|
|
|
2048
2080
|
clearLeaderFailure(taskId) {
|
|
2049
2081
|
this.#clearProjection(taskId, "leader-failure");
|
|
2050
2082
|
}
|
|
2051
|
-
getOperatorNotification(taskId) {
|
|
2052
|
-
return this.#getProjection(taskId, "operator-notification");
|
|
2053
|
-
}
|
|
2054
|
-
saveOperatorNotification(notification) {
|
|
2055
|
-
this.#saveProjection(notification.taskId, "operator-notification", notification);
|
|
2056
|
-
}
|
|
2057
|
-
clearOperatorNotification(taskId) {
|
|
2058
|
-
this.#clearProjection(taskId, "operator-notification");
|
|
2059
|
-
}
|
|
2060
2083
|
// -- pending wakeups (leader-role work-mailbox projection, mirrors taskStore.ts) --
|
|
2061
2084
|
getPendingWakeup(taskId) {
|
|
2062
2085
|
return pendingWakeupProjection(this.getWorkMailbox({ kind: "role", taskId, roleName: "leader" }));
|