@zq-silk/yui 0.11.3 → 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.
Files changed (86) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +36 -13
  3. package/dist/cli/commandCatalog.js +2 -4
  4. package/dist/cli.js +105 -43
  5. package/dist/commands/configCommands.js +12 -45
  6. package/dist/commands/executionAuditCommands.js +1 -1
  7. package/dist/commands/jobCommands.js +4 -10
  8. package/dist/commands/taskCommands.js +154 -171
  9. package/dist/commands/taskCompletionGate.js +29 -54
  10. package/dist/commands/taskContextCommand.js +55 -11
  11. package/dist/commands/taskInputCommands.js +8 -5
  12. package/dist/commands/taskNextActionCommand.js +56 -6
  13. package/dist/commands/taskOverviewCommand.js +17 -31
  14. package/dist/commands/taskRoleRuntimeStatus.js +24 -0
  15. package/dist/commands/taskWorkspaceCommands.js +5 -2
  16. package/dist/config/configCatalog.js +2 -2
  17. package/dist/config/yuiConfig.js +3 -7
  18. package/dist/context/wakeNotification.js +20 -5
  19. package/dist/controller/agentRuntimeObserver.js +247 -51
  20. package/dist/controller/clientRuntime.js +1 -1
  21. package/dist/controller/controller.js +8 -12
  22. package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
  23. package/dist/controller/runtime.js +3 -3
  24. package/dist/controller/runtimeEventInbox.js +17 -7
  25. package/dist/controller/runtimeEventProcessor.js +12 -19
  26. package/dist/controller/runtimeObservationHook.js +45 -0
  27. package/dist/execution/resourceBroker.js +5 -4
  28. package/dist/executor/agentExecutor.js +4 -4
  29. package/dist/executor/effectiveLaunch.js +8 -48
  30. package/dist/executor/fileRoleLaunchPlanner.js +36 -16
  31. package/dist/executor/workspacePreflightClassification.js +23 -2
  32. package/dist/interaction/operatorPresentation.js +33 -89
  33. package/dist/lifecycle/exactRunTerminalization.js +1 -2
  34. package/dist/lifecycle/taskRoleSessionReset.js +5 -13
  35. package/dist/observability/orchestrationMetrics.js +8 -3
  36. package/dist/profile/agentProfile.js +1 -1
  37. package/dist/release/cliHomeReleaseFence.js +123 -0
  38. package/dist/repository/taskWorkspacePreparer.js +176 -60
  39. package/dist/resources/sqliteResourceRegistry.js +1 -1
  40. package/dist/review/deltaRecheck.js +12 -51
  41. package/dist/review/reviewAcceptance.js +26 -0
  42. package/dist/review/reviewConfig.js +0 -31
  43. package/dist/review/reviewDecision.js +113 -0
  44. package/dist/review/reviewOutcomeClassifier.js +1 -1
  45. package/dist/review/reviewRound.js +1 -1
  46. package/dist/review/reviewerAvailability.js +69 -0
  47. package/dist/runtime/agentDriverObservation.js +24 -10
  48. package/dist/runtime/builtinAgentDrivers.js +4 -3
  49. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  50. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  51. package/dist/runtime/index.js +1 -0
  52. package/dist/runtime/runtimeObservation.js +13 -0
  53. package/dist/runtime/runtimeProjection.js +16 -25
  54. package/dist/runtime/sessionTokenMetrics.js +181 -0
  55. package/dist/runtime/structuredProviderHost.js +7 -1
  56. package/dist/scheduler/activeRoleRunDelivery.js +4 -4
  57. package/dist/scheduler/activeTaskProgress.js +7 -6
  58. package/dist/scheduler/leaderWakeupProcessor.js +3 -5
  59. package/dist/scheduler/operatorEvent.js +34 -0
  60. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  61. package/dist/scheduler/roleRunStall.js +0 -14
  62. package/dist/scheduler/taskExecutionProjection.js +97 -76
  63. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  64. package/dist/storage/migration/productionRegistry.js +379 -0
  65. package/dist/storage/sqliteSchema.js +18 -8
  66. package/dist/storage/sqliteStore.js +24 -20
  67. package/dist/storage/storeRpc.js +0 -1
  68. package/dist/storage/taskStore.js +15 -30
  69. package/dist/storage/upgrade/recordVersions.js +3 -2
  70. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  71. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  72. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  73. package/dist/task/completionReadiness.js +10 -6
  74. package/dist/task/nextAction.js +82 -24
  75. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  76. package/dist/web/assets/client/components.js +13 -3
  77. package/dist/web/assets/client/i18n.js +6 -2
  78. package/dist/web/webSnapshot.js +4 -0
  79. package/i18n/README.zh-CN.md +2 -2
  80. package/package.json +1 -1
  81. package/skills/yui-leader/SKILL.md +53 -16
  82. package/skills/yui-operator/SKILL.md +9 -0
  83. package/skills/yui-reviewer/SKILL.md +28 -7
  84. package/dist/context/sessionContextBudget.js +0 -71
  85. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  86. 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 (leaderFailure, operatorNotification).
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 pending migrations idempotently inside transactions.
1141
+ * Apply or validate schema migrations without letting an ordinary open mutate
1142
+ * an existing authoritative database.
1138
1143
  *
1139
- * Each migration runs in its own transaction: the DDL and the
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 next open re-applies cleanly. Re-running on a current
1142
- * database performs no work (every version is already recorded).
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))
@@ -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 { 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") {
@@ -143,16 +143,29 @@ export class SqliteTaskStore {
143
143
  this.#migration = _options.migration ?? false;
144
144
  mkdirSync(rootDir, { recursive: true, mode: 0o700 });
145
145
  const filename = _options.databaseFilename ?? "yui.db";
146
- this.#db = new Database(join(rootDir, filename));
147
- // §4.1 / §9: WAL, no fsync weakening, FKs on, busy timeout for CLI contention.
148
- this.#db.pragma("journal_mode = WAL");
149
- this.#db.pragma("synchronous = FULL");
150
- this.#db.pragma("foreign_keys = ON");
151
- this.#db.pragma("busy_timeout = 5000");
152
- this.#db.pragma("wal_autocheckpoint = 1000");
153
- migrateSqliteSchema(this.#db);
154
- this.#seedHomeMeta();
155
- this.#seedConfig();
146
+ const databasePath = join(rootDir, filename);
147
+ const databaseExisted = existsSync(databasePath);
148
+ if (this.#migration && filename === "yui.db") {
149
+ throw new SqliteSchemaMigrationError("offline migration mode cannot open the authoritative yui.db", "admission");
150
+ }
151
+ this.#db = new Database(databasePath);
152
+ try {
153
+ // §4.1 / §9: WAL, no fsync weakening, FKs on, busy timeout for CLI contention.
154
+ this.#db.pragma("journal_mode = WAL");
155
+ this.#db.pragma("synchronous = FULL");
156
+ this.#db.pragma("foreign_keys = ON");
157
+ this.#db.pragma("busy_timeout = 5000");
158
+ this.#db.pragma("wal_autocheckpoint = 1000");
159
+ migrateSqliteSchema(this.#db, {
160
+ mode: this.#migration || !databaseExisted ? "apply" : "validate"
161
+ });
162
+ this.#seedHomeMeta();
163
+ this.#seedConfig();
164
+ }
165
+ catch (error) {
166
+ this.#db.close();
167
+ throw error;
168
+ }
156
169
  }
157
170
  rootDirectory() { return this.#rootDir; }
158
171
  /** Close the underlying database connection. */
@@ -2048,15 +2061,6 @@ export class SqliteTaskStore {
2048
2061
  clearLeaderFailure(taskId) {
2049
2062
  this.#clearProjection(taskId, "leader-failure");
2050
2063
  }
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
2064
  // -- pending wakeups (leader-role work-mailbox projection, mirrors taskStore.ts) --
2061
2065
  getPendingWakeup(taskId) {
2062
2066
  return pendingWakeupProjection(this.getWorkMailbox({ kind: "role", taskId, roleName: "leader" }));
@@ -97,7 +97,6 @@ const READ_ONLY_STORE_METHODS = new Set([
97
97
  "getPendingWakeup",
98
98
  "listPendingWakeups",
99
99
  "getLeaderFailure",
100
- "getOperatorNotification",
101
100
  "listTelemetry",
102
101
  "countTelemetry",
103
102
  "hasOutboxEntry",
@@ -31,7 +31,6 @@ import { validateIntegrationQueueEntry } from "../integration/integrationQueueEn
31
31
  import { CURRENT_DURABLE_JOB_SCHEMA_VERSION, validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
32
32
  import { validateGlobalRole, validateTaskRole } from "../role/role.js";
33
33
  import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../scheduler/leaderFailure.js";
34
- import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../scheduler/operatorNotification.js";
35
34
  import { CURRENT_TASK_WAKE_SCHEMA_VERSION, validateTaskWake } from "../scheduler/taskWake.js";
36
35
  import { validateTask } from "../task/task.js";
37
36
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
@@ -46,7 +45,7 @@ import { CURRENT_AGGREGATE_SCHEMA_VERSION, requireCompatibleStorageSchema, requi
46
45
  export const STORAGE_STATE_FILE = "state.json";
47
46
  /** The root StorageState schema is the persisted aggregate document version. */
48
47
  export const CURRENT_STORAGE_STATE_SCHEMA_VERSION = CURRENT_AGGREGATE_SCHEMA_VERSION;
49
- export const CURRENT_CONFIG_SCHEMA_VERSION = 2;
48
+ export const CURRENT_CONFIG_SCHEMA_VERSION = 3;
50
49
  export const CURRENT_HOME_IDENTITY_SCHEMA_VERSION = 1;
51
50
  export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
52
51
  /**
@@ -113,7 +112,7 @@ export function executionLaneActiveRunKeyParts(key) {
113
112
  }
114
113
  }
115
114
  /** The schema version of each persisted `state.json#/tasks/*` aggregate. */
116
- export const CURRENT_STORED_TASK_SCHEMA_VERSION = 17;
115
+ export const CURRENT_STORED_TASK_SCHEMA_VERSION = 18;
117
116
  /**
118
117
  * Persisted nested-record versions consumed by this store's strict parser.
119
118
  * Keep these named at the storage boundary so the upgrade record-axis map can
@@ -1730,22 +1729,16 @@ export class FileTaskStore {
1730
1729
  this.saveWorkMailbox(consumePendingBatch(mailbox, "normal"));
1731
1730
  }
1732
1731
  getLeaderFailure(taskId) { return optional(this.#state().tasks[taskId]?.leaderFailure ?? undefined); }
1733
- saveLeaderFailure(value) { this.#saveSingleton(value.taskId, "leaderFailure", value, "Leader failure"); }
1734
- clearLeaderFailure(taskId) { this.#clearSingleton(taskId, "leaderFailure"); }
1735
- getOperatorNotification(taskId) { return optional(this.#state().tasks[taskId]?.operatorNotification ?? undefined); }
1736
- saveOperatorNotification(value) { this.#saveSingleton(value.taskId, "operatorNotification", value, "Operator notification"); }
1737
- clearOperatorNotification(taskId) { this.#clearSingleton(taskId, "operatorNotification"); }
1738
- #saveSingleton(taskId, key, value, label) {
1739
- const schemaVersion = key === "leaderFailure"
1740
- ? CURRENT_LEADER_FAILURE_SCHEMA_VERSION
1741
- : CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION;
1742
- const stored = identified(value, schemaVersion, "taskId", taskId, label);
1743
- this.#requireTaskForWrite(taskId);
1744
- this.#mutate((state) => { state.tasks[taskId][key] = stored; });
1732
+ saveLeaderFailure(value) {
1733
+ const stored = identified(value, CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "taskId", value.taskId, "Leader failure");
1734
+ this.#requireTaskForWrite(value.taskId);
1735
+ this.#mutate((state) => { state.tasks[value.taskId].leaderFailure = stored; });
1745
1736
  }
1746
- #clearSingleton(key, field) {
1747
- this.#mutate((state) => { if (state.tasks[key] !== undefined)
1748
- state.tasks[key][field] = null; });
1737
+ clearLeaderFailure(taskId) {
1738
+ this.#mutate((state) => {
1739
+ if (state.tasks[taskId] !== undefined)
1740
+ state.tasks[taskId].leaderFailure = null;
1741
+ });
1749
1742
  }
1750
1743
  #saveTaskRecord(taskId, key, value, label) {
1751
1744
  const record = versioned(value, CURRENT_MESSAGE_SCHEMA_VERSION, label);
@@ -2014,8 +2007,7 @@ function emptyStoredTask(task) {
2014
2007
  capabilityGrants: {},
2015
2008
  releaseWorkflows: {},
2016
2009
  publicationReferences: {},
2017
- leaderFailure: null,
2018
- operatorNotification: null
2010
+ leaderFailure: null
2019
2011
  };
2020
2012
  }
2021
2013
  function emptyTaskIdHighWaterMarks() {
@@ -2233,8 +2225,7 @@ function parseStoredTask(value, taskId) {
2233
2225
  "capabilityGrants",
2234
2226
  "releaseWorkflows",
2235
2227
  "publicationReferences",
2236
- "leaderFailure",
2237
- "operatorNotification"
2228
+ "leaderFailure"
2238
2229
  ], `Task aggregate ${taskId}`);
2239
2230
  parseMap(aggregate.changeSets, (record, key) => {
2240
2231
  const changeSet = identifiedChangeSet(record, key);
@@ -2434,14 +2425,8 @@ function parseStoredTask(value, taskId) {
2434
2425
  }
2435
2426
  return reference;
2436
2427
  }, "publicationReferences");
2437
- for (const [key, label] of [["leaderFailure", "Leader failure"], ["operatorNotification", "Operator notification"]]) {
2438
- const record = aggregate[key];
2439
- if (record !== null) {
2440
- const schemaVersion = key === "leaderFailure"
2441
- ? CURRENT_LEADER_FAILURE_SCHEMA_VERSION
2442
- : CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION;
2443
- identified(record, schemaVersion, "taskId", taskId, label);
2444
- }
2428
+ if (aggregate.leaderFailure !== null) {
2429
+ identified(aggregate.leaderFailure, CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "taskId", taskId, "Leader failure");
2445
2430
  }
2446
2431
  validateTaskIdHighWaterCoverage(aggregate, taskId);
2447
2432
  return aggregate;
@@ -18,7 +18,6 @@
18
18
  import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
19
19
  import { CURRENT_AGENT_RUN_SCHEMA_VERSION, CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, CURRENT_CHANGE_SET_SCHEMA_VERSION, CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION, CURRENT_DECISION_SCHEMA_VERSION, CURRENT_EVENT_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, CURRENT_MESSAGE_SCHEMA_VERSION, CURRENT_MILESTONE_SCHEMA_VERSION, CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, CURRENT_PROJECT_SCHEMA_VERSION, CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, CURRENT_STORED_TASK_SCHEMA_VERSION, CURRENT_TASK_BRIEF_SCHEMA_VERSION, CURRENT_TASK_ROLE_SCHEMA_VERSION, CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_TASK_SCHEMA_VERSION, CURRENT_WORK_ITEM_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION } from "../taskStore.js";
20
20
  import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../../scheduler/leaderFailure.js";
21
- import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../../scheduler/operatorNotification.js";
22
21
  import { CURRENT_TASK_WAKE_SCHEMA_VERSION } from "../../scheduler/taskWake.js";
23
22
  import { CURRENT_DURABLE_JOB_SCHEMA_VERSION } from "../../job/durableJob.js";
24
23
  function descriptor(version, path) {
@@ -118,7 +117,9 @@ function getCurrentRecordDescriptors() {
118
117
  releaseWorkflow: descriptor(CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, "state.json#/tasks/*/releaseWorkflows"),
119
118
  publicationReference: descriptor(CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, "state.json#/tasks/*/publicationReferences"),
120
119
  leaderFailure: descriptor(CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "state.json#/tasks/*/leaderFailure"),
121
- operatorNotification: descriptor(CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION, "state.json#/tasks/*/operatorNotification"),
120
+ // Historical family retained in the version graph so StoredTask v17
121
+ // Homes can migrate; current v18 aggregates never store this field.
122
+ operatorNotification: descriptor(1, "state.json#/tasks/*/operatorNotification"),
122
123
  workMailbox: descriptor(CURRENT_WORK_MAILBOX_SCHEMA_VERSION, "state.json#/mailboxes")
123
124
  });
124
125
  }