@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.
Files changed (91) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +80 -13
  3. package/dist/cli/commandCatalog.js +30 -5
  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/projectCommands.js +581 -2
  9. package/dist/commands/taskCommands.js +159 -172
  10. package/dist/commands/taskCompletionGate.js +29 -54
  11. package/dist/commands/taskContextCommand.js +55 -11
  12. package/dist/commands/taskInputCommands.js +8 -5
  13. package/dist/commands/taskIntegrationCommands.js +2 -1
  14. package/dist/commands/taskNextActionCommand.js +56 -6
  15. package/dist/commands/taskOverviewCommand.js +17 -31
  16. package/dist/commands/taskRoleRuntimeStatus.js +24 -0
  17. package/dist/commands/taskWorkspaceCommands.js +5 -2
  18. package/dist/config/configCatalog.js +2 -2
  19. package/dist/config/yuiConfig.js +3 -7
  20. package/dist/context/wakeNotification.js +20 -5
  21. package/dist/controller/agentRuntimeObserver.js +247 -51
  22. package/dist/controller/clientRuntime.js +1 -1
  23. package/dist/controller/controller.js +8 -12
  24. package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
  25. package/dist/controller/runtime.js +3 -3
  26. package/dist/controller/runtimeEventInbox.js +17 -7
  27. package/dist/controller/runtimeEventProcessor.js +12 -19
  28. package/dist/controller/runtimeObservationHook.js +45 -0
  29. package/dist/execution/resourceBroker.js +5 -4
  30. package/dist/executor/agentExecutor.js +4 -4
  31. package/dist/executor/effectiveLaunch.js +8 -48
  32. package/dist/executor/fileRoleLaunchPlanner.js +36 -16
  33. package/dist/executor/workspacePreflightClassification.js +23 -2
  34. package/dist/interaction/operatorPresentation.js +33 -89
  35. package/dist/lifecycle/exactRunTerminalization.js +1 -2
  36. package/dist/lifecycle/taskRoleSessionReset.js +5 -13
  37. package/dist/observability/orchestrationMetrics.js +8 -3
  38. package/dist/profile/agentProfile.js +1 -1
  39. package/dist/release/cliHomeReleaseFence.js +123 -0
  40. package/dist/repository/checkoutSwap.js +61 -0
  41. package/dist/repository/gitWorkspace.js +36 -0
  42. package/dist/repository/project.js +53 -3
  43. package/dist/repository/taskWorkspacePreparer.js +176 -60
  44. package/dist/resources/sqliteResourceRegistry.js +1 -1
  45. package/dist/review/deltaRecheck.js +12 -51
  46. package/dist/review/reviewAcceptance.js +26 -0
  47. package/dist/review/reviewConfig.js +0 -31
  48. package/dist/review/reviewDecision.js +113 -0
  49. package/dist/review/reviewOutcomeClassifier.js +1 -1
  50. package/dist/review/reviewRound.js +1 -1
  51. package/dist/review/reviewerAvailability.js +69 -0
  52. package/dist/runtime/agentDriverObservation.js +24 -10
  53. package/dist/runtime/builtinAgentDrivers.js +4 -3
  54. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  55. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  56. package/dist/runtime/index.js +1 -0
  57. package/dist/runtime/runtimeObservation.js +13 -0
  58. package/dist/runtime/runtimeProjection.js +16 -25
  59. package/dist/runtime/sessionTokenMetrics.js +181 -0
  60. package/dist/runtime/structuredProviderHost.js +7 -1
  61. package/dist/scheduler/activeRoleRunDelivery.js +4 -4
  62. package/dist/scheduler/activeTaskProgress.js +7 -6
  63. package/dist/scheduler/leaderWakeupProcessor.js +3 -5
  64. package/dist/scheduler/operatorEvent.js +34 -0
  65. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  66. package/dist/scheduler/roleRunStall.js +0 -14
  67. package/dist/scheduler/taskExecutionProjection.js +97 -76
  68. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  69. package/dist/storage/migration/productionRegistry.js +467 -0
  70. package/dist/storage/sqliteSchema.js +18 -8
  71. package/dist/storage/sqliteStore.js +61 -21
  72. package/dist/storage/storeRpc.js +0 -1
  73. package/dist/storage/taskStore.js +47 -31
  74. package/dist/storage/upgrade/recordVersions.js +3 -2
  75. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  76. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  77. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  78. package/dist/task/completionReadiness.js +10 -6
  79. package/dist/task/nextAction.js +82 -24
  80. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  81. package/dist/web/assets/client/components.js +13 -3
  82. package/dist/web/assets/client/i18n.js +6 -2
  83. package/dist/web/webSnapshot.js +4 -0
  84. package/i18n/README.zh-CN.md +2 -2
  85. package/package.json +1 -1
  86. package/skills/yui-leader/SKILL.md +53 -16
  87. package/skills/yui-operator/SKILL.md +9 -0
  88. package/skills/yui-reviewer/SKILL.md +28 -7
  89. package/dist/context/sessionContextBudget.js +0 -71
  90. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  91. package/dist/scheduler/operatorNotification.js +0 -59
@@ -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. */
@@ -596,7 +609,43 @@ export class SqliteTaskStore {
596
609
  return this.#getPayload("projects", "id = ?", [id]);
597
610
  }
598
611
  removeProject(id) {
599
- return this.#mutate(() => this.#db.prepare("DELETE FROM projects WHERE id = ?").run(id).changes > 0);
612
+ return this.#mutate(() => {
613
+ if (this.listTasks().some((task) => task.projectBindings.some((binding) => binding.projectId === id))) {
614
+ throw new StorageRecordError(`Project is still used by a Task: ${id}`);
615
+ }
616
+ return this.#db.prepare("DELETE FROM projects WHERE id = ?").run(id).changes > 0;
617
+ });
618
+ }
619
+ summarizeProjectReferences(projectId) {
620
+ const boundTasks = this.listTasks().filter((task) => task.projectBindings.some((binding) => binding.projectId === projectId));
621
+ const activeTasks = boundTasks.filter((task) => task.status === "active");
622
+ const unresolvedWorkItemRefs = [];
623
+ const activeRunRefs = [];
624
+ const unresolvedIntegrationRefs = [];
625
+ for (const task of activeTasks) {
626
+ for (const workItem of this.listWorkItems(task.id)) {
627
+ if (workItem.status !== "completed" && workItem.status !== "retired") {
628
+ unresolvedWorkItemRefs.push(`${task.id}/${workItem.id}`);
629
+ }
630
+ }
631
+ for (const run of this.listAgentRuns(task.id)) {
632
+ if (run.status === "active")
633
+ activeRunRefs.push(`${task.id}/${run.id}`);
634
+ }
635
+ for (const attempt of this.listIntegrationAttempts(task.id)) {
636
+ if (attempt.status === "running" || attempt.status === "blocked") {
637
+ unresolvedIntegrationRefs.push(`${task.id}/${attempt.id}`);
638
+ }
639
+ }
640
+ }
641
+ return {
642
+ projectId,
643
+ boundTaskIds: boundTasks.map(({ id }) => id),
644
+ activeTaskIds: activeTasks.map(({ id }) => id),
645
+ unresolvedWorkItemRefs,
646
+ activeRunRefs,
647
+ unresolvedIntegrationRefs
648
+ };
600
649
  }
601
650
  // -- agent profiles ---------------------------------------------------------
602
651
  saveAgentProfile(profile) {
@@ -2012,15 +2061,6 @@ export class SqliteTaskStore {
2012
2061
  clearLeaderFailure(taskId) {
2013
2062
  this.#clearProjection(taskId, "leader-failure");
2014
2063
  }
2015
- getOperatorNotification(taskId) {
2016
- return this.#getProjection(taskId, "operator-notification");
2017
- }
2018
- saveOperatorNotification(notification) {
2019
- this.#saveProjection(notification.taskId, "operator-notification", notification);
2020
- }
2021
- clearOperatorNotification(taskId) {
2022
- this.#clearProjection(taskId, "operator-notification");
2023
- }
2024
2064
  // -- pending wakeups (leader-role work-mailbox projection, mirrors taskStore.ts) --
2025
2065
  getPendingWakeup(taskId) {
2026
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
  /**
@@ -58,7 +57,7 @@ export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
58
57
  * below, but are not independent record-axis families.
59
58
  */
60
59
  export const CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION = 2;
61
- export const CURRENT_PROJECT_SCHEMA_VERSION = 4;
60
+ export const CURRENT_PROJECT_SCHEMA_VERSION = 5;
62
61
  export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 2;
63
62
  export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
64
63
  export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 3;
@@ -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
@@ -279,6 +278,37 @@ export class FileTaskStore {
279
278
  return this.#remove((state) => state.projects, id);
280
279
  });
281
280
  }
281
+ summarizeProjectReferences(projectId) {
282
+ const boundTasks = this.listTasks().filter((task) => task.projectBindings.some((binding) => binding.projectId === projectId));
283
+ const activeTasks = boundTasks.filter((task) => task.status === "active");
284
+ const unresolvedWorkItemRefs = [];
285
+ const activeRunRefs = [];
286
+ const unresolvedIntegrationRefs = [];
287
+ for (const task of activeTasks) {
288
+ for (const workItem of this.listWorkItems(task.id)) {
289
+ if (workItem.status !== "completed" && workItem.status !== "retired") {
290
+ unresolvedWorkItemRefs.push(`${task.id}/${workItem.id}`);
291
+ }
292
+ }
293
+ for (const run of this.listAgentRuns(task.id)) {
294
+ if (run.status === "active")
295
+ activeRunRefs.push(`${task.id}/${run.id}`);
296
+ }
297
+ for (const attempt of this.listIntegrationAttempts(task.id)) {
298
+ if (attempt.status === "running" || attempt.status === "blocked") {
299
+ unresolvedIntegrationRefs.push(`${task.id}/${attempt.id}`);
300
+ }
301
+ }
302
+ }
303
+ return {
304
+ projectId,
305
+ boundTaskIds: boundTasks.map(({ id }) => id),
306
+ activeTaskIds: activeTasks.map(({ id }) => id),
307
+ unresolvedWorkItemRefs,
308
+ activeRunRefs,
309
+ unresolvedIntegrationRefs
310
+ };
311
+ }
282
312
  saveAgentProfile(profile) {
283
313
  const stored = identified(profile, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, "id", profile.id, "Agent Profile");
284
314
  validateAgentProfile(stored);
@@ -1699,22 +1729,16 @@ export class FileTaskStore {
1699
1729
  this.saveWorkMailbox(consumePendingBatch(mailbox, "normal"));
1700
1730
  }
1701
1731
  getLeaderFailure(taskId) { return optional(this.#state().tasks[taskId]?.leaderFailure ?? undefined); }
1702
- saveLeaderFailure(value) { this.#saveSingleton(value.taskId, "leaderFailure", value, "Leader failure"); }
1703
- clearLeaderFailure(taskId) { this.#clearSingleton(taskId, "leaderFailure"); }
1704
- getOperatorNotification(taskId) { return optional(this.#state().tasks[taskId]?.operatorNotification ?? undefined); }
1705
- saveOperatorNotification(value) { this.#saveSingleton(value.taskId, "operatorNotification", value, "Operator notification"); }
1706
- clearOperatorNotification(taskId) { this.#clearSingleton(taskId, "operatorNotification"); }
1707
- #saveSingleton(taskId, key, value, label) {
1708
- const schemaVersion = key === "leaderFailure"
1709
- ? CURRENT_LEADER_FAILURE_SCHEMA_VERSION
1710
- : CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION;
1711
- const stored = identified(value, schemaVersion, "taskId", taskId, label);
1712
- this.#requireTaskForWrite(taskId);
1713
- 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; });
1714
1736
  }
1715
- #clearSingleton(key, field) {
1716
- this.#mutate((state) => { if (state.tasks[key] !== undefined)
1717
- 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
+ });
1718
1742
  }
1719
1743
  #saveTaskRecord(taskId, key, value, label) {
1720
1744
  const record = versioned(value, CURRENT_MESSAGE_SCHEMA_VERSION, label);
@@ -1983,8 +2007,7 @@ function emptyStoredTask(task) {
1983
2007
  capabilityGrants: {},
1984
2008
  releaseWorkflows: {},
1985
2009
  publicationReferences: {},
1986
- leaderFailure: null,
1987
- operatorNotification: null
2010
+ leaderFailure: null
1988
2011
  };
1989
2012
  }
1990
2013
  function emptyTaskIdHighWaterMarks() {
@@ -2202,8 +2225,7 @@ function parseStoredTask(value, taskId) {
2202
2225
  "capabilityGrants",
2203
2226
  "releaseWorkflows",
2204
2227
  "publicationReferences",
2205
- "leaderFailure",
2206
- "operatorNotification"
2228
+ "leaderFailure"
2207
2229
  ], `Task aggregate ${taskId}`);
2208
2230
  parseMap(aggregate.changeSets, (record, key) => {
2209
2231
  const changeSet = identifiedChangeSet(record, key);
@@ -2403,14 +2425,8 @@ function parseStoredTask(value, taskId) {
2403
2425
  }
2404
2426
  return reference;
2405
2427
  }, "publicationReferences");
2406
- for (const [key, label] of [["leaderFailure", "Leader failure"], ["operatorNotification", "Operator notification"]]) {
2407
- const record = aggregate[key];
2408
- if (record !== null) {
2409
- const schemaVersion = key === "leaderFailure"
2410
- ? CURRENT_LEADER_FAILURE_SCHEMA_VERSION
2411
- : CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION;
2412
- identified(record, schemaVersion, "taskId", taskId, label);
2413
- }
2428
+ if (aggregate.leaderFailure !== null) {
2429
+ identified(aggregate.leaderFailure, CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "taskId", taskId, "Leader failure");
2414
2430
  }
2415
2431
  validateTaskIdHighWaterCoverage(aggregate, taskId);
2416
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
  }
@@ -149,6 +149,12 @@ export function createSqliteMigrationTarget(options) {
149
149
  }
150
150
  const stamp = now().toISOString().replace(/[:.]/g, "-");
151
151
  let backupPath;
152
+ const schemaPath = join(home, STORAGE_SCHEMA_FILE);
153
+ const sourceSchemaText = readFileSync(schemaPath, "utf8");
154
+ const receiptPath = migrationReceiptPath(home);
155
+ const sourceReceiptText = existsSync(receiptPath)
156
+ ? readFileSync(receiptPath, "utf8")
157
+ : null;
152
158
  // Phase 1: back up any existing yui.db, then promote the sidecar.
153
159
  try {
154
160
  if (existsSync(committedDbPath)) {
@@ -185,7 +191,7 @@ export function createSqliteMigrationTarget(options) {
185
191
  // Phase 2: advance schema.json to layout 7 in the same critical section.
186
192
  try {
187
193
  if (stagedSchemaManifest !== null) {
188
- writeTextFileAtomically(join(home, STORAGE_SCHEMA_FILE), `${JSON.stringify(stagedSchemaManifest, null, 2)}\n`);
194
+ writeTextFileAtomically(schemaPath, `${JSON.stringify(stagedSchemaManifest, null, 2)}\n`);
189
195
  }
190
196
  // Certify the dual-copy state (Issue 01): the 6→7 migration retains
191
197
  // state.json read-only, so the Home legitimately holds both copies.
@@ -204,9 +210,22 @@ export function createSqliteMigrationTarget(options) {
204
210
  }
205
211
  }
206
212
  catch (error) {
207
- // The database is promoted but schema.json could not be advanced.
208
- // Attempt to restore the original database; if that fails, the Home
209
- // is ambiguous and must be recovered manually.
213
+ // A sidecar or receipt writer can fail after an atomic rename. Restore
214
+ // the exact source metadata as well as the database before claiming the
215
+ // layout switch did not commit.
216
+ let rollbackError;
217
+ try {
218
+ writeTextFileAtomically(schemaPath, sourceSchemaText);
219
+ if (sourceReceiptText === null) {
220
+ rmSync(receiptPath, { force: true });
221
+ }
222
+ else {
223
+ writeTextFileAtomically(receiptPath, sourceReceiptText);
224
+ }
225
+ }
226
+ catch (restoreError) {
227
+ rollbackError = restoreError;
228
+ }
210
229
  try {
211
230
  if (backupPath !== undefined && existsSync(backupPath)) {
212
231
  removeSqliteFileSet(committedDbPath);
@@ -216,14 +235,17 @@ export function createSqliteMigrationTarget(options) {
216
235
  removeSqliteFileSet(committedDbPath);
217
236
  }
218
237
  }
219
- catch {
238
+ catch (restoreError) {
239
+ rollbackError ??= restoreError;
240
+ }
241
+ if (rollbackError !== undefined) {
220
242
  throw new AmbiguousSwitchError({
221
243
  homePath: home,
222
244
  backupPath: backupPath ?? committedDbPath,
223
245
  stagingPath: stagedDbPath,
224
- detail: `SQLite database was promoted but schema.json could not be advanced (${messageOf(error)}), ` +
225
- `and the automatic rollback also failed. The database is at ${committedDbPath}; ` +
226
- `recover by advancing schema.json storageVersion to ${latest.layout} or restoring the backup.`
246
+ detail: `SQLite database was promoted but schema.json or its receipt could not be durably advanced (${messageOf(error)}), ` +
247
+ `and the database/metadata rollback also failed (${messageOf(rollbackError)}). ` +
248
+ `Inspect ${committedDbPath}, ${schemaPath}, ${receiptPath}, and the backup before recovery.`
227
249
  });
228
250
  }
229
251
  throw error;
@@ -38,7 +38,7 @@ import { AmbiguousSwitchError } from "../migration/index.js";
38
38
  import { describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspectSourceVersionState, inspectSnapshotVersionState } from "./homeMigrationTarget.js";
39
39
  import { writeSwitchProgress } from "./switchProgress.js";
40
40
  import { moveSqliteFileSet, removeSqliteFileSet } from "./sqliteFileSet.js";
41
- import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, copySqlitePassthroughState, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readStateFromSqlite } from "./sqliteStateMigration.js";
41
+ import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, copySqlitePassthroughState, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readMigrationSourceStateFromSqlite } from "./sqliteStateMigration.js";
42
42
  /** Build the SQLite-backed record-migration target. */
43
43
  export function createSqliteRecordMigrationTarget(options) {
44
44
  const home = options.home;
@@ -70,7 +70,7 @@ export function createSqliteRecordMigrationTarget(options) {
70
70
  readSource() {
71
71
  const manifestRaw = readFileSync(join(home, STORAGE_SCHEMA_FILE), "utf8");
72
72
  const schemaManifest = parseJsonObject(manifestRaw, STORAGE_SCHEMA_FILE);
73
- const state = readStateFromSqlite(home);
73
+ const state = readMigrationSourceStateFromSqlite(home);
74
74
  return Object.freeze({ schemaManifest, state });
75
75
  },
76
76
  writeFreshOutput(snapshot) {
@@ -129,6 +129,8 @@ export function createSqliteRecordMigrationTarget(options) {
129
129
  }
130
130
  const stamp = now().toISOString().replace(/[:.]/g, "-");
131
131
  let backupPath;
132
+ const schemaPath = join(home, STORAGE_SCHEMA_FILE);
133
+ const sourceSchemaText = readFileSync(schemaPath, "utf8");
132
134
  // Phase 1: back up the existing yui.db, then promote the sidecar.
133
135
  try {
134
136
  if (existsSync(committedDbPath)) {
@@ -167,13 +169,20 @@ export function createSqliteRecordMigrationTarget(options) {
167
169
  // critical section. The layout version is unchanged.
168
170
  try {
169
171
  if (stagedSchemaManifest !== null) {
170
- writeTextFileAtomically(join(home, STORAGE_SCHEMA_FILE), `${JSON.stringify(stagedSchemaManifest, null, 2)}\n`);
172
+ writeTextFileAtomically(schemaPath, `${JSON.stringify(stagedSchemaManifest, null, 2)}\n`);
171
173
  }
172
174
  }
173
175
  catch (error) {
174
- // The database is promoted but schema.json could not be advanced.
175
- // Attempt to restore the original database; if that fails, the Home
176
- // is ambiguous and must be recovered manually.
176
+ // A durable writer can report an fsync failure after its atomic rename,
177
+ // so "write threw" does not prove schema.json stayed old. Restore both
178
+ // the exact source sidecar and database before claiming no switch.
179
+ let rollbackError;
180
+ try {
181
+ writeTextFileAtomically(schemaPath, sourceSchemaText);
182
+ }
183
+ catch (restoreError) {
184
+ rollbackError = restoreError;
185
+ }
177
186
  try {
178
187
  if (backupPath !== undefined && existsSync(backupPath)) {
179
188
  removeSqliteFileSet(committedDbPath);
@@ -183,15 +192,18 @@ export function createSqliteRecordMigrationTarget(options) {
183
192
  removeSqliteFileSet(committedDbPath);
184
193
  }
185
194
  }
186
- catch {
195
+ catch (restoreError) {
196
+ rollbackError ??= restoreError;
197
+ }
198
+ if (rollbackError !== undefined) {
187
199
  writeInterruptedMarker(home, backupPath ?? committedDbPath, stagedDbPath, now);
188
200
  throw new AmbiguousSwitchError({
189
201
  homePath: home,
190
202
  backupPath: backupPath ?? committedDbPath,
191
203
  stagingPath: stagedDbPath,
192
- detail: `SQLite database was promoted but schema.json could not be advanced (${messageOf(error)}), ` +
193
- `and the automatic rollback also failed. The database is at ${committedDbPath}; ` +
194
- `recover by advancing schema.json recordVersions or restoring the backup.`
204
+ detail: `SQLite database was promoted but schema.json could not be durably advanced (${messageOf(error)}), ` +
205
+ `and the database/schema rollback also failed (${messageOf(rollbackError)}). ` +
206
+ `Inspect ${committedDbPath}, ${schemaPath}, and the backup before recovery.`
195
207
  });
196
208
  }
197
209
  throw error;
@@ -214,7 +226,7 @@ export function createSqliteRecordMigrationTarget(options) {
214
226
  function readSourceFresh() {
215
227
  const manifestRaw = readFileSync(join(home, STORAGE_SCHEMA_FILE), "utf8");
216
228
  const schemaManifest = parseJsonObject(manifestRaw, STORAGE_SCHEMA_FILE);
217
- const state = readStateFromSqlite(home);
229
+ const state = readMigrationSourceStateFromSqlite(home);
218
230
  return Object.freeze({ schemaManifest, state });
219
231
  }
220
232
  /**
@@ -22,6 +22,7 @@
22
22
  * dropped, duplicated, or mutated record.
23
23
  */
24
24
  import { createHash } from "node:crypto";
25
+ import { existsSync, rmSync } from "node:fs";
25
26
  import { join } from "node:path";
26
27
  import Database from "better-sqlite3";
27
28
  import { mailboxTargetKey } from "../../coordination/workMailbox.js";
@@ -33,6 +34,50 @@ import { CURRENT_STORED_TASK_SCHEMA_VERSION } from "../taskStore.js";
33
34
  export const STAGED_DATABASE_FILENAME = "yui.db.staged";
34
35
  /** The committed database filename. */
35
36
  export const COMMITTED_DATABASE_FILENAME = "yui.db";
37
+ /** Disposable, schema-current snapshot used to reconstruct an older source. */
38
+ export const MIGRATION_SOURCE_DATABASE_FILENAME = "yui.db.migration-source";
39
+ /**
40
+ * Reconstruct a migration snapshot without advancing the authoritative DB.
41
+ *
42
+ * `VACUUM INTO` takes a consistent SQLite snapshot, including committed WAL
43
+ * state, without writing the source. Pending physical migrations are then
44
+ * applied only to that disposable copy before the current reader enumerates
45
+ * record families that may not exist in the older source schema.
46
+ */
47
+ export function readMigrationSourceStateFromSqlite(home) {
48
+ const sourcePath = join(home, COMMITTED_DATABASE_FILENAME);
49
+ const snapshotPath = join(home, MIGRATION_SOURCE_DATABASE_FILENAME);
50
+ if (existsSync(snapshotPath)) {
51
+ throw new Error(`Refusing to overwrite an existing SQLite migration-source snapshot: ${snapshotPath}. `
52
+ + "Discard it and retry.");
53
+ }
54
+ try {
55
+ const source = new Database(sourcePath, { readonly: true });
56
+ try {
57
+ source.prepare("VACUUM INTO ?").run(snapshotPath);
58
+ }
59
+ finally {
60
+ source.close();
61
+ }
62
+ // Physical migration 14 rewrites the WorkMailbox family while adding its
63
+ // current columns. Preserve the source rows before applying it so the
64
+ // logical record migration still sees and transforms v1 exactly once.
65
+ const sourceMailboxes = readWorkMailboxesFromSqlite(snapshotPath);
66
+ const stagedStore = new SqliteTaskStore(home, {
67
+ databaseFilename: MIGRATION_SOURCE_DATABASE_FILENAME,
68
+ migration: true
69
+ });
70
+ stagedStore.close();
71
+ const state = readStateFromSqlite(home, MIGRATION_SOURCE_DATABASE_FILENAME);
72
+ state.mailboxes = sourceMailboxes;
73
+ return state;
74
+ }
75
+ finally {
76
+ rmSync(snapshotPath, { force: true });
77
+ rmSync(`${snapshotPath}-wal`, { force: true });
78
+ rmSync(`${snapshotPath}-shm`, { force: true });
79
+ }
80
+ }
36
81
  /**
37
82
  * Preserve SQLite-owned durable state that intentionally sits outside the
38
83
  * state.json-shaped Task aggregate snapshot. Volatile coordination locks and
@@ -351,9 +396,6 @@ export function populateSqliteFromState(home, state, databaseFilename) {
351
396
  if (stored.leaderFailure !== null) {
352
397
  store.saveLeaderFailure(stored.leaderFailure);
353
398
  }
354
- if (stored.operatorNotification !== null) {
355
- store.saveOperatorNotification(stored.operatorNotification);
356
- }
357
399
  // Per-task ID high-water marks.
358
400
  for (const [kind, highWater] of Object.entries(stored.idHighWaterMarks)) {
359
401
  if (typeof highWater === "number" && Number.isFinite(highWater)) {
@@ -595,6 +637,20 @@ function readWorkMailboxRows(db) {
595
637
  const hasInputDelivery = db.prepare("PRAGMA table_info(mailboxes)").all().some(({ name }) => name === "input_delivery");
596
638
  return db.prepare(`SELECT target_kind, task_id, role_name, next_sequence, processing, pending${hasInputDelivery ? ", input_delivery" : ""} FROM mailboxes ORDER BY target_key`).all();
597
639
  }
640
+ function readWorkMailboxesFromSqlite(dbPath) {
641
+ const db = new Database(dbPath, { readonly: true });
642
+ try {
643
+ const mailboxes = {};
644
+ for (const row of readWorkMailboxRows(db)) {
645
+ const mailbox = rowToMailbox(row);
646
+ mailboxes[mailboxTargetKey(mailbox.target)] = mailbox;
647
+ }
648
+ return mailboxes;
649
+ }
650
+ finally {
651
+ db.close();
652
+ }
653
+ }
598
654
  /**
599
655
  * Compute per-family checksums from the SQLite database. The database is
600
656
  * opened read-only; this is the independent verification read.
@@ -692,8 +748,8 @@ export function verifySqliteChecksums(state, home, databaseFilename) {
692
748
  *
693
749
  * The database is opened read-only; this function never writes.
694
750
  */
695
- export function readStateFromSqlite(home) {
696
- const dbPath = join(home, COMMITTED_DATABASE_FILENAME);
751
+ export function readStateFromSqlite(home, databaseFilename = COMMITTED_DATABASE_FILENAME) {
752
+ const dbPath = join(home, databaseFilename);
697
753
  const db = new Database(dbPath, { readonly: true });
698
754
  try {
699
755
  const manifest = readStorageSchemaManifest(home);
@@ -737,7 +793,7 @@ export function readStateFromSqlite(home) {
737
793
  const tasks = state.tasks;
738
794
  const taskRows = db.prepare("SELECT task_id, payload, brief FROM task_records").all();
739
795
  for (const row of taskRows) {
740
- tasks[row.task_id] = {
796
+ const aggregate = {
741
797
  schemaVersion: storedTaskVersion,
742
798
  task: JSON.parse(row.payload),
743
799
  brief: row.brief === null ? null : JSON.parse(row.brief),
@@ -761,12 +817,14 @@ export function readStateFromSqlite(home) {
761
817
  events: {},
762
818
  wakes: {},
763
819
  leaderFailure: null,
764
- operatorNotification: null,
765
820
  idHighWaterMarks: {},
766
821
  capabilityGrants: {},
767
822
  releaseWorkflows: {},
768
823
  publicationReferences: {}
769
824
  };
825
+ if (storedTaskVersion <= 17)
826
+ aggregate.operatorNotification = null;
827
+ tasks[row.task_id] = aggregate;
770
828
  }
771
829
  // Per-task record families, keyed by their state.json map keys.
772
830
  loadTaskPayloadMap(db, "task_roles", tasks, "roles", (record) => record.name);
@@ -808,7 +866,7 @@ export function readStateFromSqlite(home) {
808
866
  if (row.kind === "leader-failure") {
809
867
  stored.leaderFailure = JSON.parse(row.payload);
810
868
  }
811
- else {
869
+ else if (storedTaskVersion <= 17) {
812
870
  stored.operatorNotification = JSON.parse(row.payload);
813
871
  }
814
872
  }
@@ -4,6 +4,7 @@ import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
4
4
  import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
5
5
  import { reviewFindingLedgerWriteFailedFromEvents } from "../review/reviewFindingLedger.js";
6
6
  import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
7
+ import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
7
8
  import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
8
9
  const ACTIVE_JOB_STATUSES = new Set([
9
10
  "queued",
@@ -22,7 +23,8 @@ export function projectCompletionReadiness(facts, options = {}) {
22
23
  const advisories = [];
23
24
  const { task } = facts;
24
25
  const findingsGate = options.findingsGate ?? true;
25
- const taskFinalReviewRequired = resolveRecordedTaskFinalReviewContract(task.id, facts.workItems, facts.reviewRounds, facts.taskFinalReviewContractEvents) !== undefined;
26
+ const taskFinalReviewContract = resolveRecordedTaskFinalReviewContract(task.id, facts.workItems, facts.reviewRounds, facts.taskFinalReviewContractEvents)?.effective;
27
+ const taskFinalReviewRequired = taskFinalReviewContract !== undefined;
26
28
  // A pending/running Task-final Review must be resumed or blocked first.
27
29
  for (const round of facts.reviewRounds) {
28
30
  if ((round.scope ?? "work-item") !== "task")
@@ -43,11 +45,13 @@ export function projectCompletionReadiness(facts, options = {}) {
43
45
  // escalations remain audit evidence, but a later completed full Review (or
44
46
  // accepted delta) supersedes them instead of blocking completion forever.
45
47
  const latestCompletedTaskReview = facts.reviewRounds
46
- .filter((round) => ((round.scope ?? "work-item") === "task" && isSemanticReviewRound(round, {
47
- listAgentRuns: () => facts.agentRuns,
48
- listReviewFindings: () => facts.reviewFindings,
49
- listEvents: () => facts.events
50
- })))
48
+ .filter((round) => ((round.scope ?? "work-item") === "task"
49
+ && (taskFinalReviewContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalReviewContract))
50
+ && isSemanticReviewRound(round, {
51
+ listAgentRuns: () => facts.agentRuns,
52
+ listReviewFindings: () => facts.reviewFindings,
53
+ listEvents: () => facts.events
54
+ })))
51
55
  .slice()
52
56
  .sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
53
57
  || left.id.localeCompare(right.id, undefined, { numeric: true })))