@zq-silk/yui 0.14.1 → 0.15.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 (185) hide show
  1. package/ARCHITECTURE.md +91 -47
  2. package/README.md +275 -145
  3. package/dist/agent/managedRuntimeEnvironment.js +5 -8
  4. package/dist/cli/commandCatalog.js +74 -109
  5. package/dist/cli/helpRenderer.js +1 -2
  6. package/dist/cli/interactionCandidates.js +10 -0
  7. package/dist/cli/interactionPolicy.js +76 -37
  8. package/dist/cli/roleWizard.js +5 -6
  9. package/dist/cli/updateCommand.js +17 -9
  10. package/dist/cli/updateOrchestrator.js +87 -19
  11. package/dist/cli/updatePorts.js +79 -13
  12. package/dist/cli/upgradeCommand.js +105 -23
  13. package/dist/cli.js +268 -231
  14. package/dist/commands/agentCommands.js +13 -0
  15. package/dist/commands/configCommands.js +7 -19
  16. package/dist/commands/configOverview.js +3 -2
  17. package/dist/commands/controllerCommands.js +1 -1
  18. package/dist/commands/durableJobCommands.js +1 -1
  19. package/dist/commands/executionAuditCommands.js +4 -9
  20. package/dist/commands/globalRoleCommands.js +17 -0
  21. package/dist/commands/profileCommands.js +200 -30
  22. package/dist/commands/sessionCommands.js +1 -1
  23. package/dist/commands/taskActor.js +29 -127
  24. package/dist/commands/taskChangeSetCommands.js +1 -3
  25. package/dist/commands/taskCommands.js +1771 -2107
  26. package/dist/commands/taskCompletionGate.js +27 -103
  27. package/dist/commands/taskContextCommand.js +47 -80
  28. package/dist/commands/taskExecutionCommands.js +9 -27
  29. package/dist/commands/taskInputCommands.js +10 -10
  30. package/dist/commands/taskIntegrationCommands.js +69 -61
  31. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  32. package/dist/commands/taskNextActionCommand.js +8 -29
  33. package/dist/commands/taskOverviewCommand.js +25 -3
  34. package/dist/commands/taskPublicationCommands.js +151 -85
  35. package/dist/commands/taskPublicationVerifyCommand.js +181 -0
  36. package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
  37. package/dist/commands/taskRoleRuntimeStatus.js +5 -6
  38. package/dist/commands/taskUpstreamCommands.js +79 -95
  39. package/dist/config/configCatalog.js +1 -1
  40. package/dist/context/contextSnapshot.js +17 -2
  41. package/dist/context/sourceTurnContext.js +30 -0
  42. package/dist/context/turnContextPack.js +107 -48
  43. package/dist/context/turnInputContract.js +11 -4
  44. package/dist/context/wakeNotification.js +84 -22
  45. package/dist/controller/agentRuntimeObserver.js +9 -12
  46. package/dist/controller/clientRuntime.js +13 -13
  47. package/dist/controller/controller.js +11 -11
  48. package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
  49. package/dist/controller/jobControl.js +11 -10
  50. package/dist/controller/resourceInventory.js +6 -6
  51. package/dist/controller/resourceInventoryLinux.js +2 -2
  52. package/dist/controller/runtime.js +18 -61
  53. package/dist/controller/runtimeEventInbox.js +57 -34
  54. package/dist/controller/runtimeEventProcessor.js +36 -96
  55. package/dist/controller/runtimeHookTurnFence.js +33 -57
  56. package/dist/controller/runtimeLaunchCoordinator.js +44 -40
  57. package/dist/controller/runtimeObservationHook.js +18 -12
  58. package/dist/controller/sessionNotify.js +43 -28
  59. package/dist/controller/sessionOwnerReconciliation.js +21 -21
  60. package/dist/controller/structuredProviderObservation.js +13 -16
  61. package/dist/coordination/workMailboxQueue.js +132 -0
  62. package/dist/core/controllerServer.js +6 -5
  63. package/dist/doctor/doctor.js +37 -14
  64. package/dist/domain/agentResultTransport.js +60 -0
  65. package/dist/execution/executionHealth.js +111 -118
  66. package/dist/execution/reviewMainTurn.js +161 -0
  67. package/dist/execution/workItemExecution.js +454 -0
  68. package/dist/execution/workItemExecutionProjection.js +309 -0
  69. package/dist/execution/workItemMainTurn.js +164 -0
  70. package/dist/executor/agentExecutor.js +19 -19
  71. package/dist/executor/executorRegistry.js +7 -28
  72. package/dist/executor/fileRoleLaunchPlanner.js +16 -82
  73. package/dist/external/githubPublicationVerifier.js +114 -0
  74. package/dist/external/gitlabPublicationVerifier.js +158 -0
  75. package/dist/external/pinnedCommandRunner.js +73 -0
  76. package/dist/integration/changeSetManifest.js +0 -3
  77. package/dist/integration/deliveryObligation.js +43 -6
  78. package/dist/integration/gitIntegrationService.js +284 -101
  79. package/dist/integration/integrationAttempt.js +64 -14
  80. package/dist/integration/integrationQueueEntry.js +5 -35
  81. package/dist/integration/integrationQueueService.js +65 -194
  82. package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
  83. package/dist/lifecycle/exactTurnTerminalization.js +100 -97
  84. package/dist/message/message.js +16 -0
  85. package/dist/observability/executionAudit.js +4 -25
  86. package/dist/observability/faultClassification.js +58 -70
  87. package/dist/observability/orchestrationMetrics.js +8 -50
  88. package/dist/observability/runtimeIdentity.js +48 -50
  89. package/dist/profile/agentProfile.js +60 -15
  90. package/dist/profile/agentProfileRuntime.js +100 -0
  91. package/dist/release/releaseWorkflowPorts.js +5 -68
  92. package/dist/release/runtimeRelease.js +9 -1
  93. package/dist/repository/executionLaneGitSnapshot.js +80 -0
  94. package/dist/repository/gitWorkspace.js +9 -67
  95. package/dist/repository/taskBaseFreshness.js +3 -1
  96. package/dist/repository/taskWorkspaceCoordinator.js +32 -4
  97. package/dist/repository/taskWorkspacePreparer.js +252 -411
  98. package/dist/resources/liveReferences.js +6 -55
  99. package/dist/resources/resourceDiscovery.js +1 -30
  100. package/dist/resources/resourceRegistrar.js +0 -23
  101. package/dist/review/deltaRecheck.js +8 -65
  102. package/dist/review/reviewAcceptance.js +22 -19
  103. package/dist/review/reviewConfig.js +0 -10
  104. package/dist/review/reviewDecision.js +2 -2
  105. package/dist/review/reviewRound.js +147 -273
  106. package/dist/review/reviewerAvailability.js +13 -23
  107. package/dist/role/role.js +3 -0
  108. package/dist/runtime/agentDriverObservation.js +1 -1
  109. package/dist/runtime/agentHost.js +51 -51
  110. package/dist/runtime/builtinAgentDrivers.js +9 -2
  111. package/dist/runtime/codexAppServerRuntime.js +23 -5
  112. package/dist/runtime/exactControlPlane.js +22 -296
  113. package/dist/runtime/firstProgressAdvisory.js +1 -1
  114. package/dist/runtime/index.js +1 -1
  115. package/dist/runtime/launchBroker.js +11 -11
  116. package/dist/runtime/lifecycleReservation.js +2 -2
  117. package/dist/runtime/managedCaller.js +111 -0
  118. package/dist/runtime/ports.js +16 -3
  119. package/dist/runtime/preallocatedNativeSession.js +2 -2
  120. package/dist/runtime/processExitObservation.js +3 -2
  121. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  122. package/dist/runtime/runtimeBinding.js +1 -1
  123. package/dist/runtime/runtimeObservation.js +33 -16
  124. package/dist/runtime/runtimeSessionCandidate.js +2 -2
  125. package/dist/runtime/runtimeStopReceipt.js +11 -11
  126. package/dist/runtime/sessionLaunchRequest.js +2 -2
  127. package/dist/runtime/sessionOwnerIdentity.js +9 -9
  128. package/dist/runtime/sessionOwnerRegistry.js +13 -13
  129. package/dist/runtime/sessionReconciliation.js +4 -4
  130. package/dist/runtime/sessionTerminationGuard.js +9 -9
  131. package/dist/runtime/sessionTokenMetrics.js +5 -7
  132. package/dist/runtime/structuredProviderHost.js +5 -5
  133. package/dist/runtime/taskRuntimeIsolation.js +24 -24
  134. package/dist/runtime/tmuxAdapters.js +32 -30
  135. package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
  136. package/dist/scheduler/leaderWakeupProcessor.js +78 -55
  137. package/dist/scheduler/roleTurnLiveness.js +6 -6
  138. package/dist/scheduler/roleTurnStall.js +8 -9
  139. package/dist/scheduler/taskExecutionProjection.js +19 -41
  140. package/dist/scheduler/taskObservabilityProjection.js +75 -122
  141. package/dist/storage/currentTaskStore.js +6 -4
  142. package/dist/storage/sqliteSchema.js +135 -80
  143. package/dist/storage/sqliteStore.js +69 -52
  144. package/dist/storage/storageSchema.js +92 -230
  145. package/dist/storage/storageVersions.js +12 -13
  146. package/dist/storage/taskStore.js +8 -8
  147. package/dist/storage/upgrade/upgradeOrchestrator.js +276 -85
  148. package/dist/task/completionReadiness.js +17 -98
  149. package/dist/task/deliveryGuard.js +9 -11
  150. package/dist/task/draftPlan.js +142 -0
  151. package/dist/task/nextAction.js +179 -477
  152. package/dist/task/publicationVerification.js +1 -0
  153. package/dist/task/remoteDelivery.js +233 -0
  154. package/dist/task/task.js +108 -4
  155. package/dist/task/taskRecordReference.js +0 -1
  156. package/dist/turn/turn.js +109 -16
  157. package/dist/verification/verificationGateService.js +0 -26
  158. package/dist/version.js +3 -3
  159. package/dist/web/assets/client/components.js +99 -121
  160. package/dist/web/assets/client/i18n.js +136 -58
  161. package/dist/web/assets/client/view.js +44 -0
  162. package/dist/web/assets/styles/cards.js +3 -15
  163. package/dist/web/assets/styles/widgets.js +4 -0
  164. package/dist/web/webSnapshot.js +29 -14
  165. package/dist/workItem/dependencyGate.js +34 -0
  166. package/dist/workItem/workItem.js +99 -266
  167. package/dist/workspace/workItemChangeSetManager.js +15 -59
  168. package/docs/task-local-identity.md +9 -9
  169. package/i18n/README.zh-CN.md +189 -74
  170. package/package.json +1 -1
  171. package/skills/yui-leader/SKILL.md +272 -782
  172. package/skills/yui-operator/SKILL.md +190 -365
  173. package/skills/yui-reviewer/SKILL.md +40 -33
  174. package/skills/yui-runtime/SKILL.md +22 -0
  175. package/skills/yui-worker/SKILL.md +19 -6
  176. package/dist/execution/candidateConvergence.js +0 -623
  177. package/dist/execution/executionGroup.js +0 -854
  178. package/dist/execution/resourceBroker.js +0 -426
  179. package/dist/executor/launchPlan.js +0 -16
  180. package/dist/review/reviewFinding.js +0 -240
  181. package/dist/review/reviewFindingLedger.js +0 -550
  182. package/dist/review/reviewOutcomeClassifier.js +0 -255
  183. package/dist/scheduler/resourceQueueProjection.js +0 -71
  184. package/dist/storage/upgrade/recordVersions.js +0 -82
  185. package/dist/task/repairWave.js +0 -150
@@ -8,26 +8,20 @@
8
8
  *
9
9
  * - `global_sequences` (§5.3): global record ID high-water marks.
10
10
  * - `outbox` (§5.4): durable outbox with `UNIQUE(request_id)` for exactly-once.
11
- * - `config`: the `YuiConfig` singleton (the design keeps `home_meta` for
12
- * identity/revision/versions; config is a separate singleton).
11
+ * - `config`: the `YuiConfig` singleton (`home_meta` keeps identity/revision).
13
12
  *
14
13
  * Record payloads are stored two ways, per §4: typed columns for fields that are
15
14
  * queried/filtered/used-for-CAS, and a `payload` JSON column holding the full
16
- * versioned record (including its family `schemaVersion`). The record axis of
17
- * the three-axis versioning is therefore unchanged.
15
+ * current record. Record-local `schemaVersion` tags remain validation guards;
16
+ * they are not independent Home compatibility axes. Any historical payload
17
+ * rewrite belongs to the ordered Home migration that introduced the new shape.
18
18
  *
19
- * The migration runner is idempotent: it records applied versions in
20
- * `schema_migrations` and uses `CREATE TABLE IF NOT EXISTS`, so re-running on an
21
- * already-current database is a no-op and a crash mid-migration is rolled back
22
- * by SQLite (DDL is transactional) and re-applied on the next open.
19
+ * The migration runner is append-only and idempotent: it records one ordered
20
+ * Home version in `schema_migrations`. Re-running an already-current database
21
+ * is a no-op; a crash mid-upgrade rolls the whole migration transaction back.
23
22
  */
24
23
  import { createHash } from "node:crypto";
25
- /** The current SQLite layout version. */
26
- export const SQLITE_LAYOUT_VERSION = 8;
27
- /** The aggregate version of the normalized SQLite schema. */
28
- export const SQLITE_AGGREGATE_VERSION = 2;
29
- /** The current schema migration version. */
30
- export const SQLITE_SCHEMA_VERSION = 1;
24
+ import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
31
25
  /** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
32
26
  export const TELEMETRY_KEEP_PER_GENERATION = 200;
33
27
  export const TELEMETRY_TURN_CAP = 50_000;
@@ -38,20 +32,19 @@ export const TELEMETRY_TURN_CAP = 50_000;
38
32
  * the store; `journal_mode=WAL` is a persistent database property set on open.
39
33
  * The migration itself only contains schema objects.
40
34
  *
41
- * This is the direct current baseline for a new Home. Its checksum rejects any
42
- * database initialized from a different physical contract.
35
+ * This is the Yui 0.15.0 / storage-version-1 baseline. Future releases append
36
+ * migrations after it so fresh and upgraded databases converge on the same
37
+ * current contract.
43
38
  */
44
39
  const BASELINE_CORE_SQL = `
45
40
  -- Global catalog and coordination (§4.1) -------------------------------------
46
41
 
47
42
  CREATE TABLE IF NOT EXISTS home_meta (
48
- id INTEGER PRIMARY KEY CHECK (id = 1),
49
- home_identity TEXT NOT NULL,
50
- revision INTEGER NOT NULL,
51
- layout_version INTEGER NOT NULL,
52
- aggregate_version INTEGER NOT NULL,
53
- created_at TEXT NOT NULL,
54
- updated_at TEXT NOT NULL
43
+ id INTEGER PRIMARY KEY CHECK (id = 1),
44
+ home_identity TEXT NOT NULL,
45
+ revision INTEGER NOT NULL,
46
+ created_at TEXT NOT NULL,
47
+ updated_at TEXT NOT NULL
55
48
  );
56
49
 
57
50
  CREATE TABLE IF NOT EXISTS config (
@@ -460,29 +453,11 @@ BEGIN
460
453
  AND turn_id = NEW.turn_id AND generation = NEW.generation;
461
454
  END;
462
455
  `;
463
- /**
464
- * Cross-Round semantic review
465
- * findings with stable keys and Leader dispositions. Failed execution attempts
466
- * never create rows; only completed Rounds feed the ledger.
467
- */
468
- const BASELINE_REVIEW_FINDING_SQL = `
469
- CREATE TABLE IF NOT EXISTS review_findings (
470
- task_id TEXT NOT NULL,
471
- finding_id TEXT NOT NULL,
472
- stable_key TEXT NOT NULL,
473
- severity TEXT NOT NULL,
474
- payload TEXT NOT NULL,
475
- updated_at TEXT NOT NULL,
476
- PRIMARY KEY (task_id, finding_id)
477
- );
478
- CREATE INDEX IF NOT EXISTS idx_review_findings_task ON review_findings(task_id);
479
- CREATE INDEX IF NOT EXISTS idx_review_findings_stable_key ON review_findings(task_id, stable_key);
480
- `;
481
456
  /**
482
457
  * Session owner physical identity records.
483
458
  *
484
- * One row per runtime generation, keyed by launch id. The payload column
485
- * stores the full versioned JSON record; typed columns support the
459
+ * One row per runtime generation, keyed by runtime generation id. The payload column
460
+ * stores the full current JSON record; typed columns support the
486
461
  * reconciliation queries (task/role lookup, PID liveness).
487
462
  */
488
463
  const BASELINE_SESSION_OWNER_SQL = `
@@ -503,9 +478,8 @@ CREATE INDEX IF NOT EXISTS idx_session_owners_task
503
478
  /**
504
479
  * Resource GC registry.
505
480
  *
506
- * GC-owned table for resource lifecycle records. The registry is GC's own
507
- * state — it is not part of the aggregate and never participates in aggregate
508
- * versioning. Records are stored as full versioned JSON in `payload`, with
481
+ * GC-owned table for resource lifecycle records. The registry is GC's own
482
+ * state. Records are stored as full current JSON in `payload`, with
509
483
  * typed columns for the fields GC queries (disposition, kind, task_id).
510
484
  */
511
485
  const BASELINE_RESOURCE_REGISTRY_SQL = `
@@ -671,7 +645,6 @@ const MIGRATION_1_SQL = [
671
645
  BASELINE_JOB_AND_RELEASE_SQL,
672
646
  BASELINE_JOB_CALLER_SQL,
673
647
  BASELINE_TELEMETRY_AGGREGATE_SQL,
674
- BASELINE_REVIEW_FINDING_SQL,
675
648
  BASELINE_SESSION_OWNER_SQL,
676
649
  BASELINE_RESOURCE_REGISTRY_SQL,
677
650
  BASELINE_GATE_ARTIFACT_SQL,
@@ -680,10 +653,26 @@ const MIGRATION_1_SQL = [
680
653
  BASELINE_TASK_WAKE_SQL,
681
654
  BASELINE_CONTEXT_SNAPSHOT_SQL
682
655
  ].join("\n");
683
- /** Future compatible releases append new migrations after this baseline. */
684
- const MIGRATIONS = [
685
- { version: 1, axis: "layout", sql: MIGRATION_1_SQL }
686
- ];
656
+ /** Released migrations are append-only and must never be rewritten. */
657
+ const MIGRATIONS = Object.freeze([
658
+ {
659
+ version: 1,
660
+ name: "v0.15.0-baseline",
661
+ introducedIn: "0.15.0",
662
+ sql: MIGRATION_1_SQL
663
+ }
664
+ ]);
665
+ for (let index = 0; index < MIGRATIONS.length; index += 1) {
666
+ const expectedVersion = MIN_SUPPORTED_STORAGE_VERSION + index;
667
+ if (MIGRATIONS[index]?.version !== expectedVersion) {
668
+ throw new Error(`Storage migration registry must be contiguous from `
669
+ + `${MIN_SUPPORTED_STORAGE_VERSION}; missing version ${expectedVersion}.`);
670
+ }
671
+ }
672
+ if (MIGRATIONS.at(-1)?.version !== CURRENT_STORAGE_VERSION) {
673
+ throw new Error(`Storage migration registry head ${String(MIGRATIONS.at(-1)?.version)} does not match `
674
+ + `CURRENT_STORAGE_VERSION ${CURRENT_STORAGE_VERSION}.`);
675
+ }
687
676
  /** Current hot-path indexes whose absence would invalidate a current Home. */
688
677
  const REQUIRED_SCHEMA_INDEXES = [
689
678
  "idx_mailboxes_ready",
@@ -707,11 +696,10 @@ export class SqliteSchemaMigrationError extends Error {
707
696
  }
708
697
  const SCHEMA_MIGRATIONS_SQL = `
709
698
  CREATE TABLE schema_migrations (
710
- version INTEGER PRIMARY KEY,
711
- axis TEXT NOT NULL CHECK (axis IN ('layout','aggregate','record')),
712
- record_kind TEXT,
713
- applied_at TEXT NOT NULL,
714
- checksum TEXT NOT NULL
699
+ version INTEGER PRIMARY KEY,
700
+ name TEXT NOT NULL,
701
+ applied_at TEXT NOT NULL,
702
+ checksum TEXT NOT NULL
715
703
  )
716
704
  `;
717
705
  /**
@@ -739,10 +727,20 @@ function ensureMigrationLedger(db, mode) {
739
727
  }
740
728
  return false;
741
729
  }
742
- /** Validate the applied prefix and return its versions for the migration loop. */
730
+ function validateMigrationLedgerColumns(db) {
731
+ const columns = new Set(db.prepare("PRAGMA table_info(schema_migrations)").all()
732
+ .flatMap(({ name }) => typeof name === "string" ? [name] : []));
733
+ if (columns.size === 4
734
+ && ["version", "name", "applied_at", "checksum"].every((name) => columns.has(name))) {
735
+ return;
736
+ }
737
+ throw new SqliteSchemaMigrationError("schema_migrations columns do not match the storage-version-1 ledger");
738
+ }
739
+ /** Validate the applied linear prefix and return its current head. */
743
740
  function validateAppliedMigrations(db, ledgerWasCreated) {
741
+ validateMigrationLedgerColumns(db);
744
742
  const expected = new Map(MIGRATIONS.map((migration) => [migration.version, migration]));
745
- const rows = db.prepare("SELECT version, axis, record_kind, checksum FROM schema_migrations ORDER BY version").all();
743
+ const rows = db.prepare("SELECT version, name, checksum FROM schema_migrations ORDER BY version").all();
746
744
  if (rows.length === 0 && !ledgerWasCreated) {
747
745
  throw new SqliteSchemaMigrationError("schema_migrations ledger is empty in an existing database");
748
746
  }
@@ -753,19 +751,22 @@ function validateAppliedMigrations(db, ledgerWasCreated) {
753
751
  }
754
752
  const version = row.version;
755
753
  const migration = expected.get(version);
756
- if (migration === undefined) {
757
- throw new SqliteSchemaMigrationError(`unknown migration version ${version}`);
758
- }
759
754
  if (applied.has(version)) {
760
755
  throw new SqliteSchemaMigrationError(`duplicate migration version ${version}`);
761
756
  }
762
757
  applied.add(version);
763
- const expectedRecordKind = migration.recordKind ?? null;
764
- if (row.axis !== migration.axis) {
765
- throw new SqliteSchemaMigrationError(`migration ${version} axis ${String(row.axis)} does not match ${migration.axis}`);
758
+ if (migration === undefined) {
759
+ if (version <= CURRENT_STORAGE_VERSION) {
760
+ throw new SqliteSchemaMigrationError(`unknown migration version ${version}`);
761
+ }
762
+ if (typeof row.name !== "string" || row.name.length === 0
763
+ || typeof row.checksum !== "string" || row.checksum.length === 0) {
764
+ throw new SqliteSchemaMigrationError(`future migration ${version} metadata is invalid`);
765
+ }
766
+ continue;
766
767
  }
767
- if (row.record_kind !== expectedRecordKind) {
768
- throw new SqliteSchemaMigrationError(`migration ${version} record_kind ${String(row.record_kind)} does not match ${String(expectedRecordKind)}`);
768
+ if (row.name !== migration.name) {
769
+ throw new SqliteSchemaMigrationError(`migration ${version} name ${String(row.name)} does not match ${migration.name}`);
769
770
  }
770
771
  const expectedChecksum = checksum(migration.sql);
771
772
  if (row.checksum !== expectedChecksum) {
@@ -774,12 +775,18 @@ function validateAppliedMigrations(db, ledgerWasCreated) {
774
775
  }
775
776
  const versions = [...applied].sort((left, right) => left - right);
776
777
  for (let index = 0; index < versions.length; index += 1) {
777
- const expectedVersion = index + 1;
778
+ const expectedVersion = MIN_SUPPORTED_STORAGE_VERSION + index;
778
779
  if (versions[index] !== expectedVersion) {
779
780
  throw new SqliteSchemaMigrationError(`migration ledger has a gap before version ${versions[index]}`);
780
781
  }
781
782
  }
782
- return applied;
783
+ const currentVersion = versions.at(-1) ?? 0;
784
+ const head = rows.at(-1);
785
+ return {
786
+ versions: applied,
787
+ currentVersion,
788
+ currentChecksum: typeof head?.checksum === "string" ? head.checksum : ""
789
+ };
783
790
  }
784
791
  /**
785
792
  * Validate the physical objects promised by the migration ledger. Ledger
@@ -799,25 +806,63 @@ function validateSchemaObjects(db) {
799
806
  throw new SqliteSchemaMigrationError(`required index '${index}' is missing or has the wrong type`, "schema object");
800
807
  }
801
808
  }
809
+ const homeMetaColumns = db.prepare("PRAGMA table_info(home_meta)").all().map(({ name }) => name);
810
+ const expectedHomeMetaColumns = [
811
+ "id",
812
+ "home_identity",
813
+ "revision",
814
+ "created_at",
815
+ "updated_at"
816
+ ];
817
+ if (homeMetaColumns.length !== expectedHomeMetaColumns.length
818
+ || homeMetaColumns.some((name, index) => name !== expectedHomeMetaColumns[index])) {
819
+ throw new SqliteSchemaMigrationError("home_meta columns do not match the current storage contract", "schema object");
820
+ }
821
+ }
822
+ /** Return the one linear upgrade path from a supported source to this release. */
823
+ export function storageMigrationPlan(currentVersion) {
824
+ if (!Number.isInteger(currentVersion)
825
+ || currentVersion < MIN_SUPPORTED_STORAGE_VERSION
826
+ || currentVersion > CURRENT_STORAGE_VERSION) {
827
+ return null;
828
+ }
829
+ return MIGRATIONS
830
+ .filter(({ version }) => version > currentVersion)
831
+ .map(({ version, name, introducedIn }) => ({
832
+ fromVersion: version - 1,
833
+ toVersion: version,
834
+ name,
835
+ introducedIn
836
+ }));
802
837
  }
803
838
  /** Inspect a recognized migration prefix without changing it. */
804
839
  export function inspectSqliteSchemaMigrations(db) {
805
840
  const ledgerWasCreated = ensureMigrationLedger(db, "validate");
806
841
  const applied = validateAppliedMigrations(db, ledgerWasCreated);
842
+ if (applied.currentVersion > CURRENT_STORAGE_VERSION) {
843
+ return {
844
+ currentVersion: applied.currentVersion,
845
+ currentChecksum: applied.currentChecksum,
846
+ targetVersion: CURRENT_STORAGE_VERSION,
847
+ minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
848
+ targetChecksum: checksum(MIGRATIONS.at(-1).sql),
849
+ pendingVersions: []
850
+ };
851
+ }
807
852
  const pendingVersions = MIGRATIONS
808
- .filter((migration) => !applied.has(migration.version))
853
+ .filter((migration) => !applied.versions.has(migration.version))
809
854
  .map((migration) => migration.version);
810
855
  if (pendingVersions.length === 0)
811
856
  validateSchemaObjects(db);
812
- const current = MIGRATIONS[applied.size - 1];
813
857
  const target = MIGRATIONS.at(-1);
814
- if (current === undefined) {
858
+ if (applied.currentVersion === 0) {
815
859
  throw new SqliteSchemaMigrationError("schema_migrations ledger has no current head");
816
860
  }
817
861
  return {
818
- currentVersion: applied.size,
819
- currentChecksum: checksum(current.sql),
820
- targetVersion: SQLITE_SCHEMA_VERSION,
862
+ currentVersion: applied.currentVersion,
863
+ currentChecksum: applied.currentChecksum,
864
+ targetVersion: CURRENT_STORAGE_VERSION,
865
+ minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
821
866
  targetChecksum: checksum(target.sql),
822
867
  pendingVersions
823
868
  };
@@ -838,15 +883,26 @@ export function migrateSqliteSchema(db, options) {
838
883
  // prevents a manually altered or partially recorded ledger from silently
839
884
  // skipping a later schema/data step.
840
885
  const applied = validateAppliedMigrations(db, ledgerWasCreated);
841
- const pending = MIGRATIONS.filter((migration) => !applied.has(migration.version));
886
+ if (applied.currentVersion > CURRENT_STORAGE_VERSION) {
887
+ throw new SqliteSchemaMigrationError(`storage version ${applied.currentVersion} is newer than supported `
888
+ + `${CURRENT_STORAGE_VERSION}`, "admission");
889
+ }
890
+ if (applied.currentVersion !== 0
891
+ && applied.currentVersion < MIN_SUPPORTED_STORAGE_VERSION) {
892
+ throw new SqliteSchemaMigrationError(`storage version ${applied.currentVersion} is older than the minimum supported `
893
+ + `${MIN_SUPPORTED_STORAGE_VERSION}`, "admission");
894
+ }
895
+ const pending = MIGRATIONS.filter((migration) => !applied.versions.has(migration.version));
842
896
  if (!ledgerWasCreated && pending.length > 0 && options.mode === "validate") {
843
- throw new SqliteSchemaMigrationError(`SQLite schema ${applied.size} requires an explicit upgrade to ${SQLITE_SCHEMA_VERSION}`, "admission");
897
+ throw new SqliteSchemaMigrationError(`Storage version ${applied.currentVersion} requires an explicit upgrade to `
898
+ + `${CURRENT_STORAGE_VERSION}`, "admission");
844
899
  }
845
900
  const newlyApplied = [];
846
901
  for (const migration of pending) {
847
902
  db.exec(migration.sql);
848
- db.prepare(`INSERT INTO schema_migrations (version, axis, record_kind, applied_at, checksum)
849
- VALUES (?, ?, ?, ?, ?)`).run(migration.version, migration.axis, migration.recordKind ?? null, new Date().toISOString(), checksum(migration.sql));
903
+ const appliedAt = new Date().toISOString();
904
+ db.prepare(`INSERT INTO schema_migrations (version, name, applied_at, checksum)
905
+ VALUES (?, ?, ?, ?)`).run(migration.version, migration.name, appliedAt, checksum(migration.sql));
850
906
  newlyApplied.push(migration.version);
851
907
  }
852
908
  validateSchemaObjects(db);
@@ -855,7 +911,7 @@ export function migrateSqliteSchema(db, options) {
855
911
  const newlyApplied = options.mode === "apply" && !db.inTransaction
856
912
  ? db.transaction(migrate)()
857
913
  : migrate();
858
- return { applied: newlyApplied, version: SQLITE_SCHEMA_VERSION };
914
+ return { applied: newlyApplied, version: CURRENT_STORAGE_VERSION };
859
915
  }
860
916
  /** The names of every table the schema creates (for tests/introspection). */
861
917
  export const SQLITE_SCHEMA_TABLES = [
@@ -886,7 +942,6 @@ export const SQLITE_SCHEMA_TABLES = [
886
942
  "turns",
887
943
  "active_turns",
888
944
  "review_rounds",
889
- "review_findings",
890
945
  "change_sets",
891
946
  "integration_attempts",
892
947
  "messages",
@@ -17,11 +17,12 @@
17
17
  * on the durable outbox.
18
18
  * - Crash recovery .............. WAL rollback of uncommitted transactions;
19
19
  * outbox replay of committed-but-unacked effects.
20
- * - Record family versioning .... full record (incl. schemaVersion) in payload.
20
+ * - Record validation ........... current record (incl. local schemaVersion)
21
+ * in payload.
21
22
  * - Evidence retention .......... events/review_rounds/change_sets/
22
23
  * integration_attempts are never pruned.
23
24
  *
24
- * Records are stored as full versioned JSON in `payload` columns, with typed
25
+ * Records are stored as full current JSON in `payload` columns, with typed
25
26
  * columns for the fields that are queried/filtered/used-for-CAS (§4). A
26
27
  * high-frequency runtime telemetry observation is a single-row upsert into
27
28
  * `telemetry` scoped by its primary key — it never rewrites global
@@ -40,8 +41,7 @@ import Database from "better-sqlite3";
40
41
  import { consumePendingBatch, mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
41
42
  import { validateContextSnapshot } from "../context/contextSnapshot.js";
42
43
  import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
43
- import { validateReviewFinding } from "../review/reviewFinding.js";
44
- import { reviewFindingLedgerMode } from "../review/reviewFindingLedger.js";
44
+ import { validateReviewRound } from "../review/reviewRound.js";
45
45
  import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
46
46
  import { validateIntegrationQueueEntry } from "../integration/integrationQueueEntry.js";
47
47
  import { validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
@@ -52,7 +52,8 @@ import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
52
52
  import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveTurnKey, executionLaneActiveTurnKeyParts, StorageConflictError, StorageCancelledError, StorageRecordError, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, pendingWakeupProjection, validateYuiConfig } from "./taskStore.js";
53
53
  import { publicationExternalKey } from "../task/publicationReference.js";
54
54
  import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
55
- import { inspectSqliteSchemaMigrations, migrateSqliteSchema, SqliteSchemaMigrationError, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_TURN_CAP } from "./sqliteSchema.js";
55
+ import { inspectSqliteSchemaMigrations, migrateSqliteSchema, SqliteSchemaMigrationError, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_TURN_CAP } from "./sqliteSchema.js";
56
+ import { StorageSchemaError } from "./storageSchema.js";
56
57
  /** Read the immutable Home identity without opening a writable Store connection. */
57
58
  export function readSqliteHomeIdentity(rootDir, databaseFilename = "yui.db") {
58
59
  const database = new Database(join(rootDir, databaseFilename), {
@@ -123,6 +124,10 @@ export class SqliteTaskStore {
123
124
  mkdirSync(rootDir, { recursive: true, mode: 0o700 });
124
125
  const filename = _options.databaseFilename ?? "yui.db";
125
126
  const databasePath = join(rootDir, filename);
127
+ // Remember whether the database file existed before this connection opened
128
+ // it. A store that creates the file is bootstrapping a fresh database and
129
+ // may seed its singleton rows; a store that opens an existing file missing
130
+ // those rows has found corruption, not a bootstrap opportunity.
126
131
  const databaseExisted = existsSync(databasePath);
127
132
  this.#db = new Database(databasePath);
128
133
  try {
@@ -140,8 +145,7 @@ export class SqliteTaskStore {
140
145
  version: schema.currentVersion,
141
146
  checksum: schema.currentChecksum
142
147
  };
143
- this.#seedHomeMeta();
144
- this.#seedConfig();
148
+ this.#ensureSeedRows(databaseExisted);
145
149
  }
146
150
  catch (error) {
147
151
  this.#db.close();
@@ -158,11 +162,40 @@ export class SqliteTaskStore {
158
162
  */
159
163
  databaseHandle() { return this.#db; }
160
164
  // -- transaction primitives -------------------------------------------------
165
+ /**
166
+ * Guarantee the `home_meta` and `config` singleton rows exist.
167
+ *
168
+ * A fresh database created by this connection is bootstrapped with defaults.
169
+ * An existing database that is missing either row is corrupt —
170
+ * truncated, partially migrated, or hand-edited — and must fail closed
171
+ * instead of silently masking the damage with fresh defaults (Issue 01
172
+ * cross-issue handoff: INSERT OR IGNORE could hide migration corruption).
173
+ */
174
+ #ensureSeedRows(databaseExisted) {
175
+ const hasHomeMeta = this.#db.prepare("SELECT 1 FROM home_meta WHERE id = 1").get() !== undefined;
176
+ const hasConfig = this.#db.prepare("SELECT 1 FROM config WHERE id = 1").get() !== undefined;
177
+ if (hasHomeMeta && hasConfig)
178
+ return;
179
+ if (databaseExisted) {
180
+ const missing = [
181
+ !hasHomeMeta ? "home_meta" : null,
182
+ !hasConfig ? "config" : null
183
+ ].filter((value) => value !== null).join(", ");
184
+ throw new StorageSchemaError("STORAGE_SCHEMA_INVALID", `SQLite database is corrupt: required singleton row(s) missing (${missing}). `
185
+ + "The database may be truncated or partially migrated. Restore yui.db from a "
186
+ + "backup, or preserve this Home for diagnosis and initialize a new Home.");
187
+ }
188
+ if (!hasHomeMeta)
189
+ this.#seedHomeMeta();
190
+ if (!hasConfig)
191
+ this.#seedConfig();
192
+ }
161
193
  #seedHomeMeta() {
162
194
  const now = new Date().toISOString();
163
195
  const identity = generateHomeIdentity(new Date());
164
- this.#db.prepare(`INSERT OR IGNORE INTO home_meta (id, home_identity, revision, layout_version, aggregate_version, created_at, updated_at)
165
- VALUES (1, ?, 0, ?, ?, ?, ?)`).run(JSON.stringify(identity), SQLITE_LAYOUT_VERSION, SQLITE_AGGREGATE_VERSION, now, now);
196
+ this.#db.prepare(`INSERT OR IGNORE INTO home_meta
197
+ (id, home_identity, revision, created_at, updated_at)
198
+ VALUES (1, ?, 0, ?, ?)`).run(JSON.stringify(identity), now, now);
166
199
  }
167
200
  #seedConfig() {
168
201
  this.#db.prepare(`INSERT OR IGNORE INTO config (id, payload, updated_at) VALUES (1, ?, ?)`).run(JSON.stringify(DEFAULT_CONFIG), new Date().toISOString());
@@ -731,9 +764,7 @@ export class SqliteTaskStore {
731
764
  activeTurns: runs.filter((run) => run.status === "active"),
732
765
  leaderTurns: runs.filter((run) => run.roleName === "leader"),
733
766
  reviewOutcomeEvidence: {
734
- turns: this.#sortById(this.#listPayload("turns", "task_id = ?", [taskId]).filter((run) => run.purpose === "review"), (run) => run.id),
735
- reviewFindings: this.listReviewFindings(taskId),
736
- events: events.filter((event) => event.type === "review.completed")
767
+ turns: this.#sortById(this.#listPayload("turns", "task_id = ?", [taskId]).filter((run) => run.purpose === "review"), (run) => run.id)
737
768
  }
738
769
  };
739
770
  }
@@ -743,13 +774,9 @@ export class SqliteTaskStore {
743
774
  return null;
744
775
  return {
745
776
  ...base,
746
- turns: operationalTaskRecords(this.listTurns(taskId), this.listEvents(taskId), "turn"),
747
777
  managedWorkspaces: this.#sortById(this.#listPayload("managed_workspaces", "task_id = ?", [taskId]), (workspace) => managedWorkspaceKey(workspace.owner)),
748
778
  durableJobs: this.#sortById(this.#listPayload("durable_jobs", "task_id = ?", [taskId]), (job) => job.id),
749
779
  integrationQueueEntries: this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id),
750
- reviewFindings: this.#sortById(this.#listPayload("review_findings", "task_id = ?", [taskId]), (finding) => finding.id),
751
- reviewFindingLedgerMode: reviewFindingLedgerMode(this.getConfig()),
752
- events: this.#sortById(this.#listPayload("events", "task_id = ?", [taskId]), (event) => event.id)
753
780
  };
754
781
  }
755
782
  /** Task ids flagged active in the catalog projection (the global active index). */
@@ -926,11 +953,11 @@ export class SqliteTaskStore {
926
953
  role_name = excluded.role_name, agent_id = excluded.agent_id,
927
954
  native_session_id = excluded.native_session_id,
928
955
  provider_root_pid = excluded.provider_root_pid,
929
- payload = excluded.payload, recorded_at = excluded.recorded_at`).run(identity.launchId, owner.scope, owner.scope === "task" ? owner.taskId : null, owner.roleName, identity.agentId, identity.nativeSessionId ?? null, identity.providerRoot.pid, this.#json(identity), identity.recordedAt);
956
+ payload = excluded.payload, recorded_at = excluded.recorded_at`).run(identity.runtimeGenerationId, owner.scope, owner.scope === "task" ? owner.taskId : null, owner.roleName, identity.agentId, identity.nativeSessionId ?? null, identity.providerRoot.pid, this.#json(identity), identity.recordedAt);
930
957
  });
931
958
  }
932
- getSessionOwner(launchId) {
933
- const row = this.#db.prepare("SELECT payload FROM session_owners WHERE launch_id = ?").get(launchId);
959
+ getSessionOwner(runtimeGenerationId) {
960
+ const row = this.#db.prepare("SELECT payload FROM session_owners WHERE launch_id = ?").get(runtimeGenerationId);
934
961
  return row === undefined ? null : this.#parse(row.payload);
935
962
  }
936
963
  listSessionOwners() {
@@ -942,9 +969,9 @@ export class SqliteTaskStore {
942
969
  }
943
970
  return this.#listPayload("session_owners", "scope = 'task' AND task_id = ? AND role_name = ?", [owner.taskId, owner.roleName]);
944
971
  }
945
- removeSessionOwner(launchId) {
972
+ removeSessionOwner(runtimeGenerationId) {
946
973
  this.#mutate(() => {
947
- this.#db.prepare("DELETE FROM session_owners WHERE launch_id = ?").run(launchId);
974
+ this.#db.prepare("DELETE FROM session_owners WHERE launch_id = ?").run(runtimeGenerationId);
948
975
  });
949
976
  }
950
977
  // -- task roles -------------------------------------------------------------
@@ -1058,7 +1085,7 @@ export class SqliteTaskStore {
1058
1085
  ? ""
1059
1086
  : ` WHERE ${predicates.join(" AND ")}`;
1060
1087
  const rows = this.#db.prepare(`SELECT scope, task_id, role_name, agent_id, adapter_id,
1061
- native_session_id, launch_id, session_updated_at,
1088
+ native_session_id, launch_id AS runtime_generation_id, session_updated_at,
1062
1089
  cleanup_required
1063
1090
  FROM runtime_session_candidates${where}`).all(...parameters);
1064
1091
  const candidates = rows.map((row) => ({
@@ -1068,7 +1095,7 @@ export class SqliteTaskStore {
1068
1095
  agentId: row.agent_id,
1069
1096
  adapterId: row.adapter_id,
1070
1097
  nativeSessionId: row.native_session_id,
1071
- ...(row.launch_id === null ? {} : { launchId: row.launch_id }),
1098
+ ...(row.runtime_generation_id === null ? {} : { runtimeGenerationId: row.runtime_generation_id }),
1072
1099
  sessionUpdatedAt: row.session_updated_at,
1073
1100
  cleanupRequired: row.cleanup_required === 1
1074
1101
  })).sort(compareRuntimeSessionCandidates);
@@ -1123,7 +1150,7 @@ export class SqliteTaskStore {
1123
1150
  json_extract(active_session, '$.agentId') AS agent_id,
1124
1151
  json_extract(active_session, '$.adapterId') AS adapter_id,
1125
1152
  json_extract(active_session, '$.nativeSessionId') AS native_session_id,
1126
- json_extract(active_session, '$.launchId') AS launch_id,
1153
+ json_extract(active_session, '$.runtimeGenerationId') AS runtime_generation_id,
1127
1154
  CASE
1128
1155
  WHEN json_extract(active_session, '$.status') = 'active'
1129
1156
  THEN 1 ELSE 0
@@ -1131,7 +1158,7 @@ export class SqliteTaskStore {
1131
1158
  json_extract(active_session, '$.updatedAt') AS session_updated_at,
1132
1159
  CASE
1133
1160
  WHEN json_extract(active_session, '$.status') = 'active'
1134
- AND json_type(active_session, '$.launchId') = 'text'
1161
+ AND json_type(active_session, '$.runtimeGenerationId') = 'text'
1135
1162
  THEN 1 ELSE 0
1136
1163
  END AS cleanup_required
1137
1164
  FROM active`).get(...source.parameters);
@@ -1151,7 +1178,7 @@ export class SqliteTaskStore {
1151
1178
  && row.agent_id === candidate.agentId
1152
1179
  && row.adapter_id === candidate.adapterId
1153
1180
  && row.native_session_id === candidate.nativeSessionId
1154
- && (row.launch_id ?? undefined) === candidate.launchId
1181
+ && (row.runtime_generation_id ?? undefined) === candidate.runtimeGenerationId
1155
1182
  && row.is_active === 1
1156
1183
  && row.session_updated_at === candidate.sessionUpdatedAt
1157
1184
  && row.cleanup_required === (candidate.cleanupRequired ? 1 : 0);
@@ -1192,7 +1219,7 @@ export class SqliteTaskStore {
1192
1219
  native_session_id = excluded.native_session_id,
1193
1220
  launch_id = excluded.launch_id,
1194
1221
  session_updated_at = excluded.session_updated_at,
1195
- cleanup_required = excluded.cleanup_required`).run(candidate.owner.scope, taskId, candidate.owner.roleName, candidate.agentId, candidate.adapterId, candidate.nativeSessionId, candidate.launchId ?? null, candidate.sessionUpdatedAt, candidate.cleanupRequired ? 1 : 0);
1222
+ cleanup_required = excluded.cleanup_required`).run(candidate.owner.scope, taskId, candidate.owner.roleName, candidate.agentId, candidate.adapterId, candidate.nativeSessionId, candidate.runtimeGenerationId ?? null, candidate.sessionUpdatedAt, candidate.cleanupRequired ? 1 : 0);
1196
1223
  }
1197
1224
  #deleteRuntimeSessionCandidate(owner) {
1198
1225
  this.#db.prepare(`DELETE FROM runtime_session_candidates
@@ -1295,8 +1322,7 @@ export class SqliteTaskStore {
1295
1322
  if (sameExternal.length > 0) {
1296
1323
  const latest = latestSqlitePublicationReference(sameExternal);
1297
1324
  if (stored.supersedes !== latest.id) {
1298
- throw new StorageRecordError(`Publication external identity conflicts with ${latest.id}; `
1299
- + `explicitly supersede that record: ${key}.`);
1325
+ throw new StorageRecordError(`Publication external identity conflicts with current record ${latest.id}: ${key}.`);
1300
1326
  }
1301
1327
  }
1302
1328
  else if (stored.supersedes !== undefined) {
@@ -1500,6 +1526,7 @@ export class SqliteTaskStore {
1500
1526
  return this.#sortById(this.#listPayload("review_rounds", "task_id = ?", [taskId]), (round) => round.id);
1501
1527
  }
1502
1528
  saveReviewRound(taskId, round) {
1529
+ validateReviewRound(round);
1503
1530
  if (round.taskId !== taskId)
1504
1531
  throw new StorageRecordError(`Review round belongs to another Task: ${round.taskId}`);
1505
1532
  this.#requireTask(taskId);
@@ -1509,27 +1536,6 @@ export class SqliteTaskStore {
1509
1536
  payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, round.id, round.status, this.#json(round), this.#now());
1510
1537
  });
1511
1538
  }
1512
- // -- review findings --------------------------------------------------------
1513
- nextReviewFindingId(taskId) { return this.#nextTaskRecordId(taskId, "reviewFinding"); }
1514
- getReviewFinding(taskId, findingId) {
1515
- return this.#getPayload("review_findings", "task_id = ? AND finding_id = ?", [taskId, findingId]);
1516
- }
1517
- listReviewFindings(taskId) {
1518
- return this.#sortById(this.#listPayload("review_findings", "task_id = ?", [taskId]), (finding) => finding.id);
1519
- }
1520
- saveReviewFinding(taskId, finding) {
1521
- validateReviewFinding(finding);
1522
- if (finding.taskId !== taskId) {
1523
- throw new StorageRecordError(`Review finding belongs to another Task: ${finding.taskId}`);
1524
- }
1525
- this.#requireTask(taskId);
1526
- this.#mutate(() => {
1527
- this.#db.prepare(`INSERT INTO review_findings (task_id, finding_id, stable_key, severity, payload, updated_at)
1528
- VALUES (?, ?, ?, ?, ?, ?)
1529
- ON CONFLICT(task_id, finding_id) DO UPDATE SET stable_key = excluded.stable_key,
1530
- severity = excluded.severity, payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, finding.id, finding.stableKey, finding.severity, this.#json(finding), this.#now());
1531
- });
1532
- }
1533
1539
  // -- active turns ----------------------------------------------------------
1534
1540
  #saveActiveTurn(taskId, pointer, turnId) {
1535
1541
  this.#mutate(() => {
@@ -1721,6 +1727,18 @@ export class SqliteTaskStore {
1721
1727
  this.#observeHighWater(taskId, "message", seq);
1722
1728
  });
1723
1729
  }
1730
+ updateMessage(taskId, message) {
1731
+ if (message.taskId !== taskId) {
1732
+ throw new StorageRecordError(`Message belongs to another Task: ${message.taskId}`);
1733
+ }
1734
+ this.#requireTask(taskId);
1735
+ this.#mutate(() => {
1736
+ const result = this.#db.prepare(`UPDATE messages SET payload = ? WHERE task_id = ? AND message_id = ?`).run(this.#json(message), taskId, message.id);
1737
+ if (result.changes !== 1) {
1738
+ throw new StorageRecordError(`Message does not exist: ${taskId}/${message.id}`);
1739
+ }
1740
+ });
1741
+ }
1724
1742
  listMessages(taskId) {
1725
1743
  return this.#sortById(this.#listPayload("messages", "task_id = ?", [taskId]), (message) => message.id);
1726
1744
  }
@@ -2109,10 +2127,9 @@ function validIntegrationQueueTransition(before, after) {
2109
2127
  || before.createdAt !== after.createdAt)
2110
2128
  return false;
2111
2129
  const allowed = {
2112
- queued: ["queued", "running", "validated", "superseded"],
2130
+ queued: ["queued", "running", "superseded"],
2113
2131
  running: ["running", "conflicted", "committed"],
2114
2132
  conflicted: ["conflicted", "running", "committed", "queued", "superseded"],
2115
- validated: ["validated", "running", "queued", "superseded"],
2116
2133
  committed: ["committed"],
2117
2134
  superseded: ["superseded"]
2118
2135
  };