@zq-silk/yui 0.11.2 → 0.12.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/ARCHITECTURE.md +23 -4
- package/README.md +80 -13
- package/dist/cli/commandCatalog.js +30 -5
- package/dist/cli.js +105 -43
- package/dist/commands/configCommands.js +12 -45
- package/dist/commands/executionAuditCommands.js +1 -1
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/projectCommands.js +581 -2
- package/dist/commands/taskCommands.js +159 -172
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +55 -11
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskIntegrationCommands.js +2 -1
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +24 -0
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +2 -2
- 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 +1 -1
- package/dist/controller/controller.js +8 -12
- package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
- package/dist/controller/runtime.js +3 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +4 -4
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +36 -16
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +1 -2
- package/dist/lifecycle/taskRoleSessionReset.js +5 -13
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/repository/checkoutSwap.js +61 -0
- package/dist/repository/gitWorkspace.js +36 -0
- package/dist/repository/project.js +53 -3
- 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/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/runtimeObservation.js +13 -0
- package/dist/runtime/runtimeProjection.js +16 -25
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/scheduler/activeRoleRunDelivery.js +4 -4
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +3 -5
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +0 -14
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +467 -0
- package/dist/storage/sqliteSchema.js +18 -8
- package/dist/storage/sqliteStore.js +61 -21
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +47 -31
- 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/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -24
- 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 +4 -0
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +53 -16
- package/skills/yui-operator/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +28 -7
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/scheduler/operatorNotification.js +0 -59
|
@@ -18,6 +18,8 @@ const PROJECT_FROM_VERSION = 2;
|
|
|
18
18
|
const PROJECT_TO_VERSION = 3;
|
|
19
19
|
const PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION = 3;
|
|
20
20
|
const PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION = 4;
|
|
21
|
+
const PROJECT_LIFECYCLE_FROM_VERSION = 4;
|
|
22
|
+
const PROJECT_LIFECYCLE_TO_VERSION = 5;
|
|
21
23
|
const TASK_FROM_VERSION = 3;
|
|
22
24
|
const TASK_TO_VERSION = 4;
|
|
23
25
|
const TASK_INTENT_FROM_VERSION = 4;
|
|
@@ -77,6 +79,8 @@ const STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION = 15;
|
|
|
77
79
|
const STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION = 16;
|
|
78
80
|
const STORED_TASK_WAKES_FROM_VERSION = 16;
|
|
79
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;
|
|
80
84
|
const TASK_WAKE_FROM_VERSION = 0;
|
|
81
85
|
const TASK_WAKE_TO_VERSION = 1;
|
|
82
86
|
const STORED_TASK_PUBLICATION_REFERENCES_FROM_VERSION = 16;
|
|
@@ -95,6 +99,8 @@ const PUBLICATION_REFERENCE_FROM_VERSION = 0;
|
|
|
95
99
|
const PUBLICATION_REFERENCE_TO_VERSION = 1;
|
|
96
100
|
const CONFIG_FROM_VERSION = 1;
|
|
97
101
|
const CONFIG_TO_VERSION = 2;
|
|
102
|
+
const CONFIG_REVIEW_CONTROLS_FROM_VERSION = 2;
|
|
103
|
+
const CONFIG_REVIEW_CONTROLS_TO_VERSION = 3;
|
|
98
104
|
/**
|
|
99
105
|
* Build the authoritative production graph. Transition intent and executable
|
|
100
106
|
* transforms are registered together here; compatible loading and offline
|
|
@@ -144,7 +150,9 @@ export function createProductionStorageRegistry() {
|
|
|
144
150
|
})
|
|
145
151
|
.registerOfflineMigration(projectOwnershipStep())
|
|
146
152
|
.registerOfflineMigration(configV2Step())
|
|
153
|
+
.registerOfflineMigration(configReviewControlsRemovalStep())
|
|
147
154
|
.registerCompatible(projectKnowledgeProposalsStep())
|
|
155
|
+
.registerCompatible(projectLifecycleStep())
|
|
148
156
|
.registerOfflineMigration(taskWorkspaceIdentityStep())
|
|
149
157
|
.registerOfflineMigration(taskIntentStep())
|
|
150
158
|
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
|
|
@@ -177,6 +185,7 @@ export function createProductionStorageRegistry() {
|
|
|
177
185
|
.registerCompatible(storedTaskDurableJobsStep())
|
|
178
186
|
.registerCompatible(storedTaskJobCallerKeyHashesStep())
|
|
179
187
|
.registerCompatible(storedTaskWakesAndPublicationReferencesStep())
|
|
188
|
+
.registerOfflineMigration(storedTaskEventNoticesStep())
|
|
180
189
|
.registerCompatible(taskWakeIntroductionStep())
|
|
181
190
|
.registerCompatible(durableJobIntroductionStep())
|
|
182
191
|
.registerOfflineMigration(capabilityGrantIntroductionStep())
|
|
@@ -243,6 +252,61 @@ function migrateConfigV1ToV2(snapshot) {
|
|
|
243
252
|
}
|
|
244
253
|
};
|
|
245
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
|
+
}
|
|
246
310
|
function workItemExecutionGroupHistoryStep() {
|
|
247
311
|
return {
|
|
248
312
|
axis: "record",
|
|
@@ -1238,6 +1302,324 @@ function normalizeStoredTaskV16ToV17(snapshot) {
|
|
|
1238
1302
|
state: { ...snapshot.state, tasks: nextTasks }
|
|
1239
1303
|
};
|
|
1240
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
|
+
}
|
|
1241
1623
|
/**
|
|
1242
1624
|
* The taskWake record family is introduced alongside StoredTask v17. The
|
|
1243
1625
|
* compatible step supplies the empty map and high-water mark; this step only
|
|
@@ -2761,6 +3143,91 @@ function normalizeProjectV3ToV4(snapshot) {
|
|
|
2761
3143
|
state: { ...snapshot.state, projects: nextProjects }
|
|
2762
3144
|
};
|
|
2763
3145
|
}
|
|
3146
|
+
/**
|
|
3147
|
+
* Project v5 adds the lifecycle status (`active`/`retired`) plus the
|
|
3148
|
+
* retirement audit record. Historical Projects are all active; the step only
|
|
3149
|
+
* defaults the new field, so it is a compatible adjacent migration.
|
|
3150
|
+
*/
|
|
3151
|
+
function projectLifecycleStep() {
|
|
3152
|
+
return {
|
|
3153
|
+
kind: "compatible",
|
|
3154
|
+
axis: "record",
|
|
3155
|
+
recordKind: "project",
|
|
3156
|
+
fromVersion: PROJECT_LIFECYCLE_FROM_VERSION,
|
|
3157
|
+
toVersion: PROJECT_LIFECYCLE_TO_VERSION,
|
|
3158
|
+
defaults: [
|
|
3159
|
+
"status defaults to active on every Project"
|
|
3160
|
+
],
|
|
3161
|
+
validateSource: (snapshot) => requireProjectV4Shape(snapshot),
|
|
3162
|
+
normalize: (snapshot) => normalizeProjectV4ToV5(snapshot)
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
function requireProjectV4Shape(snapshot) {
|
|
3166
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
3167
|
+
if (manifestVersions.project !== PROJECT_LIFECYCLE_FROM_VERSION) {
|
|
3168
|
+
throw new Error(`Record project compatible step requires manifest version ${PROJECT_LIFECYCLE_FROM_VERSION}.`);
|
|
3169
|
+
}
|
|
3170
|
+
if (snapshot.state === null)
|
|
3171
|
+
return;
|
|
3172
|
+
const projects = snapshot.state.projects;
|
|
3173
|
+
if (projects === undefined)
|
|
3174
|
+
return;
|
|
3175
|
+
const map = asObject(projects, "project map");
|
|
3176
|
+
const allowed = new Set(PROJECT_V4_FIELDS);
|
|
3177
|
+
for (const [projectId, rawProject] of Object.entries(map)) {
|
|
3178
|
+
const project = asObject(rawProject, `Project ${projectId}`);
|
|
3179
|
+
if (project.schemaVersion !== PROJECT_LIFECYCLE_FROM_VERSION) {
|
|
3180
|
+
throw new Error(`Project ${projectId} must use schemaVersion ${PROJECT_LIFECYCLE_FROM_VERSION}.`);
|
|
3181
|
+
}
|
|
3182
|
+
const unknown = Object.keys(project).find((key) => !allowed.has(key));
|
|
3183
|
+
if (unknown !== undefined) {
|
|
3184
|
+
throw new Error(`Project ${projectId} has an unknown v4 field: ${unknown}.`);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
function normalizeProjectV4ToV5(snapshot) {
|
|
3189
|
+
requireProjectV4Shape(snapshot);
|
|
3190
|
+
const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
|
|
3191
|
+
const schemaManifest = {
|
|
3192
|
+
...snapshot.schemaManifest,
|
|
3193
|
+
recordVersions: { ...manifestVersions, project: PROJECT_LIFECYCLE_TO_VERSION }
|
|
3194
|
+
};
|
|
3195
|
+
if (snapshot.state === null)
|
|
3196
|
+
return { schemaManifest, state: null };
|
|
3197
|
+
const projects = snapshot.state.projects;
|
|
3198
|
+
if (projects === undefined) {
|
|
3199
|
+
return { schemaManifest, state: { ...snapshot.state } };
|
|
3200
|
+
}
|
|
3201
|
+
const map = asObject(projects, "project map");
|
|
3202
|
+
const nextProjects = {};
|
|
3203
|
+
for (const [projectId, rawProject] of Object.entries(map)) {
|
|
3204
|
+
const project = asObject(rawProject, `Project ${projectId}`);
|
|
3205
|
+
nextProjects[projectId] = {
|
|
3206
|
+
...project,
|
|
3207
|
+
schemaVersion: PROJECT_LIFECYCLE_TO_VERSION,
|
|
3208
|
+
status: "active"
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
return {
|
|
3212
|
+
schemaManifest,
|
|
3213
|
+
state: { ...snapshot.state, projects: nextProjects }
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
const PROJECT_V4_FIELDS = [
|
|
3217
|
+
"schemaVersion",
|
|
3218
|
+
"id",
|
|
3219
|
+
"name",
|
|
3220
|
+
"aliases",
|
|
3221
|
+
"path",
|
|
3222
|
+
"ownership",
|
|
3223
|
+
"remoteUrl",
|
|
3224
|
+
"stableBranch",
|
|
3225
|
+
"developmentBranch",
|
|
3226
|
+
"knowledge",
|
|
3227
|
+
"knowledgeProposals",
|
|
3228
|
+
"createdAt",
|
|
3229
|
+
"updatedAt"
|
|
3230
|
+
];
|
|
2764
3231
|
/**
|
|
2765
3232
|
* Task v4 adds the optional durable workspace identity. Historical Tasks have
|
|
2766
3233
|
* no identity; they keep working against their existing (legacy) refs until the
|
|
@@ -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')),
|
|
@@ -986,10 +987,13 @@ const SCHEMA_MIGRATIONS_SQL = `
|
|
|
986
987
|
* layout migrations. A database with any sqlite_master object is therefore
|
|
987
988
|
* diagnosed as corrupt/partially initialized and left untouched.
|
|
988
989
|
*/
|
|
989
|
-
function ensureMigrationLedger(db) {
|
|
990
|
+
function ensureMigrationLedger(db, mode) {
|
|
990
991
|
const objects = db.prepare("SELECT type, name FROM sqlite_master WHERE name IS NOT NULL").all();
|
|
991
992
|
const ledger = objects.find(({ name }) => name === "schema_migrations");
|
|
992
993
|
if (ledger === undefined) {
|
|
994
|
+
if (mode === "validate") {
|
|
995
|
+
throw new SqliteSchemaMigrationError("schema_migrations ledger is missing from an existing database");
|
|
996
|
+
}
|
|
993
997
|
if (objects.length !== 0) {
|
|
994
998
|
throw new SqliteSchemaMigrationError("schema_migrations ledger is missing from a non-empty database");
|
|
995
999
|
}
|
|
@@ -1134,19 +1138,25 @@ function validatePendingProjectionBackfillSources(db) {
|
|
|
1134
1138
|
}
|
|
1135
1139
|
}
|
|
1136
1140
|
/**
|
|
1137
|
-
* Apply
|
|
1141
|
+
* Apply or validate schema migrations without letting an ordinary open mutate
|
|
1142
|
+
* an existing authoritative database.
|
|
1138
1143
|
*
|
|
1139
|
-
*
|
|
1144
|
+
* In `apply` mode each migration runs in its own transaction: the DDL and the
|
|
1140
1145
|
* `schema_migrations` bookkeeping commit atomically, so a crash mid-migration
|
|
1141
|
-
* rolls back and the
|
|
1142
|
-
*
|
|
1146
|
+
* rolls back and the disposable staged database can be rebuilt cleanly.
|
|
1147
|
+
* `validate` mode rejects a pending version before executing any migration.
|
|
1143
1148
|
*/
|
|
1144
|
-
export function migrateSqliteSchema(db) {
|
|
1145
|
-
const ledgerWasCreated = ensureMigrationLedger(db);
|
|
1149
|
+
export function migrateSqliteSchema(db, options) {
|
|
1150
|
+
const ledgerWasCreated = ensureMigrationLedger(db, options.mode);
|
|
1146
1151
|
// Validate the complete ledger before touching any pending migration. This
|
|
1147
1152
|
// prevents a manually altered or partially recorded ledger from silently
|
|
1148
1153
|
// skipping the partial-index/projection migrations added after a valid Home.
|
|
1149
1154
|
const applied = validateAppliedMigrations(db, ledgerWasCreated);
|
|
1155
|
+
const pending = MIGRATIONS.filter((migration) => !applied.has(migration.version));
|
|
1156
|
+
if (options.mode === "validate" && pending.length > 0) {
|
|
1157
|
+
throw new SqliteSchemaMigrationError(`pending SQLite schema migration ${pending[0].version}; `
|
|
1158
|
+
+ "run the offline staged upgrade before opening this database", "admission");
|
|
1159
|
+
}
|
|
1150
1160
|
const newlyApplied = [];
|
|
1151
1161
|
for (const migration of MIGRATIONS) {
|
|
1152
1162
|
if (applied.has(migration.version))
|