@zq-silk/yui 0.15.12 → 0.16.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 (236) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/ARCHITECTURE.zh-CN.md +3 -2
  3. package/dist/artifacts/artifactCommitLock.js +16 -55
  4. package/dist/artifacts/taskArtifactRepository.js +3 -3
  5. package/dist/cli/agentConfigurationPicker.js +1 -1
  6. package/dist/cli/commandCatalog.js +57 -41
  7. package/dist/cli/completion.js +18 -18
  8. package/dist/cli/completionWizard.js +1 -1
  9. package/dist/cli/dynamicCompletion.js +1 -1
  10. package/dist/cli/interactionPolicy.js +7 -3
  11. package/dist/cli/invocationAuthority.js +64 -0
  12. package/dist/cli/managedDiagnostics.js +1 -1
  13. package/dist/cli/parseRepeatable.js +35 -0
  14. package/dist/cli/updatePorts.js +26 -5
  15. package/dist/cli.js +1280 -1243
  16. package/dist/commands/agentCommands.js +4 -4
  17. package/dist/commands/capabilityCommands.js +1 -1
  18. package/dist/commands/configCommands.js +6 -57
  19. package/dist/commands/configOverview.js +2 -2
  20. package/dist/commands/durableJobCommands.js +12 -6
  21. package/dist/commands/globalRoleCommands.js +53 -29
  22. package/dist/commands/grantCommands.js +0 -3
  23. package/dist/commands/jobCommands.js +1 -4
  24. package/dist/commands/operatorCommands.js +3 -3
  25. package/dist/commands/projectCommands.js +1 -10
  26. package/dist/commands/resourcesCommands.js +10 -40
  27. package/dist/commands/roleConfiguration.js +2 -6
  28. package/dist/commands/taskActivationCommands.js +4 -8
  29. package/dist/commands/taskAgentCapabilities.js +9 -0
  30. package/dist/commands/taskCommandSupport.js +155 -0
  31. package/dist/commands/taskCommandTypes.js +1 -0
  32. package/dist/commands/taskCommands.js +271 -710
  33. package/dist/commands/taskCompletionGate.js +1 -1
  34. package/dist/commands/taskExecutionCommands.js +1 -1
  35. package/dist/commands/taskFactCommands.js +325 -0
  36. package/dist/commands/taskInputCommands.js +12 -42
  37. package/dist/commands/taskIntegrationCommands.js +17 -57
  38. package/dist/commands/taskNextActionCommand.js +7 -6
  39. package/dist/commands/taskPublicationAdoptCommand.js +3 -3
  40. package/dist/commands/taskPublicationCommands.js +1 -1
  41. package/dist/commands/taskPublicationVerifyCommand.js +1 -1
  42. package/dist/commands/taskRemoteDeliveryCommand.js +2 -94
  43. package/dist/commands/taskUpstreamCommands.js +19 -8
  44. package/dist/commands/workflowCommands.js +6 -1
  45. package/dist/completion/completionInstaller.js +26 -26
  46. package/dist/completion/completionState.js +26 -26
  47. package/dist/completion/fileCompletionManager.js +6 -11
  48. package/dist/config/configCatalog.js +0 -2
  49. package/dist/config/timeZone.js +14 -0
  50. package/dist/config/yuiConfig.js +0 -29
  51. package/dist/context/runContextPack.js +2 -3
  52. package/dist/context/taskCatalog.js +3 -5
  53. package/dist/context/taskContext.js +139 -151
  54. package/dist/context/wakeRunReferences.js +11 -0
  55. package/dist/controller/agentCapabilities.js +58 -0
  56. package/dist/controller/agentRuntimeObserver.js +6 -11
  57. package/dist/controller/clientRuntime.js +22 -14
  58. package/dist/controller/controller.js +9 -22
  59. package/dist/controller/fileSchedulerStoreAdapter.js +117 -212
  60. package/dist/controller/jobClient.js +4 -4
  61. package/dist/controller/jobControl.js +60 -25
  62. package/dist/controller/jobSupervisor.js +4 -4
  63. package/dist/controller/providerRetryDelivery.js +7 -7
  64. package/dist/controller/runtime.js +51 -44
  65. package/dist/controller/runtimeEventProcessor.js +0 -5
  66. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  67. package/dist/controller/taskAgentError.js +35 -0
  68. package/dist/coordination/workMailboxQueue.js +2 -24
  69. package/dist/core/controllerClient.js +2 -2
  70. package/dist/core/fileLockOwner.js +74 -0
  71. package/dist/decision/decision.js +17 -0
  72. package/dist/doctor/doctor.js +18 -9
  73. package/dist/event/taskEvent.js +12 -0
  74. package/dist/execution/workItemExecutionProjection.js +2 -2
  75. package/dist/executor/agentCapabilityConfig.js +43 -0
  76. package/dist/executor/agentConfigurationCatalog.js +49 -11
  77. package/dist/executor/agentExecutor.js +5 -8
  78. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  79. package/dist/executor/taskAgentCapabilities.js +64 -0
  80. package/dist/integration/deliveryObligation.js +1 -69
  81. package/dist/integration/gitIntegrationService.js +60 -84
  82. package/dist/integration/integrationAttempt.js +3 -14
  83. package/dist/integration/integrationSourceApplication.js +4 -12
  84. package/dist/integration/manifestTags.js +2 -2
  85. package/dist/job/durableJob.js +4 -25
  86. package/dist/job/jobAssignmentScope.js +22 -0
  87. package/dist/job/jobOperation.js +16 -0
  88. package/dist/job/jobRunner.js +2 -1
  89. package/dist/job/stepDirectory.js +15 -0
  90. package/dist/kernel/builtinCapabilities.js +15 -11
  91. package/dist/kernel/kernelPorts.js +1 -17
  92. package/dist/lifecycle/exactRunTerminalization.js +1 -9
  93. package/dist/message/inputControlResolution.js +3 -4
  94. package/dist/message/message.js +17 -20
  95. package/dist/message/messageContinuation.js +1 -1
  96. package/dist/milestone/milestone.js +11 -0
  97. package/dist/observability/orchestrationMetrics.js +24 -1
  98. package/dist/observability/runtimeIdentity.js +1 -17
  99. package/dist/output/agentConfigurationPresentation.js +2 -0
  100. package/dist/output/timePresentation.js +1 -14
  101. package/dist/plugins/pluginService.js +12 -4
  102. package/dist/release/releaseWorkflowPorts.js +45 -96
  103. package/dist/release/runtimeRelease.js +9 -15
  104. package/dist/repository/projectMaintenanceLock.js +23 -64
  105. package/dist/repository/taskBaseFreshness.js +1 -1
  106. package/dist/repository/taskWorkspaceCoordinator.js +13 -22
  107. package/dist/repository/taskWorkspacePreparer.js +29 -25
  108. package/dist/repository/workspaceCleanupInspection.js +5 -5
  109. package/dist/resources/autoResourceGc.js +4 -77
  110. package/dist/resources/liveReferences.js +7 -54
  111. package/dist/resources/resourceDiscovery.js +5 -118
  112. package/dist/resources/resourceGc.js +233 -181
  113. package/dist/resources/resourceRegistrar.js +5 -4
  114. package/dist/resources/resourceRegistry.js +3 -37
  115. package/dist/resources/resourceRegistryStore.js +13 -36
  116. package/dist/resources/sqliteResourceRegistry.js +43 -26
  117. package/dist/review/deltaRecheck.js +1 -1
  118. package/dist/review/reviewAcceptance.js +1 -1
  119. package/dist/review/reviewDecision.js +1 -1
  120. package/dist/review/reviewRound.js +8 -7
  121. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  122. package/dist/runtime/acpProtocol.js +4 -51
  123. package/dist/runtime/acpSession.js +10 -75
  124. package/dist/runtime/acpSessionConfiguration.js +1 -8
  125. package/dist/runtime/agentEndpoint.js +3 -2
  126. package/dist/runtime/agentFailureContext.js +43 -0
  127. package/dist/runtime/agentHost.js +15 -15
  128. package/dist/runtime/codexInteractiveHost.js +2 -2
  129. package/dist/runtime/index.js +0 -1
  130. package/dist/runtime/managedCaller.js +21 -1
  131. package/dist/runtime/providerErrors.js +38 -0
  132. package/dist/runtime/runtimeObservation.js +0 -5
  133. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  134. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  135. package/dist/runtime/structuredProviderHost.js +4 -41
  136. package/dist/runtime/tmuxAdapters.js +6 -8
  137. package/dist/scheduler/activeRoleRunDelivery.js +7 -7
  138. package/dist/scheduler/leaderWakeupProcessor.js +19 -6
  139. package/dist/scheduler/operatorEvent.js +12 -20
  140. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  141. package/dist/scheduler/ports.js +5 -8
  142. package/dist/scheduler/roleRunLiveness.js +4 -4
  143. package/dist/scheduler/roleRunStall.js +16 -23
  144. package/dist/scheduler/taskExecutionProjection.js +25 -33
  145. package/dist/scheduler/taskObservabilityProjection.js +0 -1
  146. package/dist/scheduler/taskWake.js +5 -10
  147. package/dist/scheduler/wakeReason.js +2 -0
  148. package/dist/scheduler/wakeupQueue.js +2 -4
  149. package/dist/setup/setupCommand.js +1 -1
  150. package/dist/storage/contextRecords.js +106 -0
  151. package/dist/storage/currentTaskStore.js +1 -1
  152. package/dist/storage/homeLayout.js +13 -17
  153. package/dist/storage/migrations/agentFailureContext.js +22 -0
  154. package/dist/storage/migrations/currentInputContract.js +86 -0
  155. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  156. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  157. package/dist/storage/migrations/integrationContinuation.js +5 -4
  158. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  159. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  160. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  161. package/dist/storage/migrations/verificationPolicy.js +74 -0
  162. package/dist/storage/migrations/workItemHistory.js +46 -0
  163. package/dist/storage/persistenceWorker.js +12 -4
  164. package/dist/storage/recordValidation.js +87 -0
  165. package/dist/storage/sqliteSchema.js +142 -1
  166. package/dist/storage/sqliteStore.js +156 -162
  167. package/dist/storage/storageSchema.js +2 -14
  168. package/dist/storage/storageVersions.js +1 -1
  169. package/dist/storage/storeRpc.js +15 -7
  170. package/dist/storage/taskStore.js +3 -7
  171. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  172. package/dist/task/completionReadiness.js +5 -24
  173. package/dist/task/draftPlan.js +0 -53
  174. package/dist/task/nextAction.js +7 -13
  175. package/dist/task/remoteDeliveryService.js +89 -0
  176. package/dist/task/taskActivation.js +2 -25
  177. package/dist/task/taskActivationService.js +0 -2
  178. package/dist/task/taskRecordReference.js +0 -1
  179. package/dist/task/taskSubmission.js +3 -14
  180. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  181. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  182. package/dist/telemetry/telemetryWiring.js +4 -3
  183. package/dist/tmux/tmuxManager.js +29 -20
  184. package/dist/verification/gateArtifact.js +15 -24
  185. package/dist/verification/gateArtifactStore.js +14 -7
  186. package/dist/verification/verificationGateService.js +29 -88
  187. package/dist/verification/verificationPlan.js +27 -72
  188. package/dist/web/assets/client/app.js +1 -1
  189. package/dist/web/assets/client/taskSurface.js +4 -4
  190. package/dist/web/webServer.js +4 -6
  191. package/dist/web/webSnapshot.js +3 -83
  192. package/dist/web/webTaskSurface.js +17 -46
  193. package/dist/workItem/workItem.js +1 -12
  194. package/docs/architecture/capabilities-and-resources.md +14 -2
  195. package/docs/architecture/capabilities-and-resources.zh-CN.md +11 -1
  196. package/docs/managed-turn-and-session-runtime.md +83 -10
  197. package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -11
  198. package/docs/plugin-sdk.md +4 -2
  199. package/docs/provider-runtime.md +3 -1
  200. package/docs/provider-runtime.zh-CN.md +4 -2
  201. package/docs/release-workflow.md +190 -7
  202. package/docs/release-workflow.zh-CN.md +139 -5
  203. package/docs/roles-and-configuration.md +29 -0
  204. package/docs/roles-and-configuration.zh-CN.md +21 -0
  205. package/docs/sqlite-control-plane-design.md +16 -3
  206. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  207. package/docs/task-dag-semantics.md +3 -3
  208. package/docs/task-dag-semantics.zh-CN.md +2 -2
  209. package/docs/task-delivery.md +91 -5
  210. package/docs/task-delivery.zh-CN.md +72 -4
  211. package/docs/task-discovery.md +10 -11
  212. package/docs/task-discovery.zh-CN.md +8 -9
  213. package/docs/testing/verification-levels.md +145 -9
  214. package/docs/testing/verification-levels.zh-CN.md +103 -8
  215. package/package.json +1 -1
  216. package/skills/yui-leader/SKILL.md +5 -0
  217. package/skills/yui-leader/references/integration.md +9 -5
  218. package/skills/yui-leader/references/planning.md +9 -3
  219. package/skills/yui-operator/SKILL.md +15 -10
  220. package/skills/yui-runtime/SKILL.md +6 -6
  221. package/skills/yui-runtime/references/recovery.md +47 -0
  222. package/dist/agent/agentRegistry.js +0 -10
  223. package/dist/agentRun/runIdentity.js +0 -22
  224. package/dist/commands/deliveryGuardPreflight.js +0 -30
  225. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  226. package/dist/commands/taskOverviewCommand.js +0 -377
  227. package/dist/completion/completionWizard.js +0 -125
  228. package/dist/context/dispatchContext.js +0 -110
  229. package/dist/context/wakeNotification.js +0 -144
  230. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  231. package/dist/integration/integrationQueueEntry.js +0 -191
  232. package/dist/integration/integrationQueueService.js +0 -810
  233. package/dist/release/fakeReleasePorts.js +0 -55
  234. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  235. package/dist/task/deliveryGuard.js +0 -226
  236. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -28,46 +28,44 @@
28
28
  * `telemetry` scoped by its primary key — it never rewrites global
29
29
  * state and never touches another Task's rows (§4.4).
30
30
  *
31
- * The in-process store is phase 1 of §6. It does not re-run the heavy record
32
- * validators on write (the domain layer that constructs records already does,
33
- * and the design places record validation in the persistence worker, phase 2);
34
- * it performs the same cheap structural checks the file store relies on
35
- * (identity presence, taskId matching, referential lookups).
31
+ * The same current record validators apply to direct and worker-backed access,
32
+ * including writes, reads and explicit Home diagnostics. There is no weaker
33
+ * in-process contract or deferred validation phase.
36
34
  */
35
+ import Database from "better-sqlite3";
37
36
  import { existsSync, mkdirSync } from "node:fs";
38
37
  import { join } from "node:path";
39
38
  import { isDeepStrictEqual } from "node:util";
40
- import Database from "better-sqlite3";
41
- import { queryTaskCatalog } from "./taskCatalog.js";
42
- import { validatePluginValidation } from "../plugins/pluginPackage.js";
43
- import { validatePluginIntent, validatePluginIntentFailure } from "../plugins/pluginIntent.js";
44
- import { validateLocalResource, validateEnvironmentPreparation } from "../resources/projectResource.js";
45
- import { validateConfiguredAgent } from "../agent/agent.js";
39
+ import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
46
40
  import { validateTaskBrief } from "../brief/taskBrief.js";
47
- import { consumePendingBatch, mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
48
41
  import { validateContextSnapshot } from "../context/contextSnapshot.js";
42
+ import { consumePendingBatch, mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
43
+ import { validateDurableJob, validDurableJobTransition } from "../job/durableJob.js";
49
44
  import { validateGlobalRoleMessage } from "../message/message.js";
50
- import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
45
+ import { validatePluginIntent, validatePluginIntentFailure } from "../plugins/pluginIntent.js";
46
+ import { validatePluginValidation } from "../plugins/pluginPackage.js";
47
+ import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
48
+ import { validateProject } from "../repository/project.js";
49
+ import { validateEnvironmentPreparation, validateLocalResource } from "../resources/projectResource.js";
50
+ import { validateReviewRound } from "../review/reviewRound.js";
51
51
  import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
52
52
  import { sessionOwnerProcessKey } from "../runtime/sessionOwnerIdentity.js";
53
- import { validateReviewRound } from "../review/reviewRound.js";
54
- import { validateProject } from "../repository/project.js";
55
- import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
56
- import { validateIntegrationQueueEntry } from "../integration/integrationQueueEntry.js";
57
- import { validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
58
- import { validateGlobalRole, validateTaskRole } from "../role/role.js";
59
53
  import { validateTaskWake } from "../scheduler/taskWake.js";
60
- import { validateTask } from "../task/task.js";
61
54
  import { pendingCompletionMessages } from "../task/completionReadiness.js";
62
- import { operationalTaskRecords, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
55
+ import { publicationExternalKey } from "../task/publicationReference.js";
56
+ import { validateTask } from "../task/task.js";
63
57
  import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
58
+ import { operationalTaskRecords, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
59
+ import { writeTelemetryBatch } from "../telemetry/sqliteTelemetryBatch.js";
60
+ import { STORED_RECORD_TABLES, validateStoredRecord } from "./recordValidation.js";
61
+ import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
64
62
  import { validateWorkItem } from "../workItem/workItem.js";
65
63
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
66
- import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, executionLaneActiveRunKeyParts, StorageConflictError, StorageCancelledError, StorageRecordError, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, pendingWakeupProjection, validateYuiConfig } from "./taskStore.js";
67
- import { publicationExternalKey } from "../task/publicationReference.js";
68
- import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
64
+ import { contextInputReferences, queryContextRecords } from "./contextRecords.js";
69
65
  import { inspectSqliteSchemaMigrations, migrateSqliteSchema, SqliteSchemaMigrationError, TELEMETRY_KEEP_PER_RUN, TELEMETRY_RUN_CAP } from "./sqliteSchema.js";
70
66
  import { StorageSchemaError } from "./storageSchema.js";
67
+ import { queryTaskCatalog } from "./taskCatalog.js";
68
+ import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, executionLaneActiveRunKeyParts, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, pendingWakeupProjection, StorageCancelledError, StorageConflictError, StorageRecordError, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, validateYuiConfig } from "./taskStore.js";
71
69
  /** Read the immutable Home identity without opening a writable Store connection. */
72
70
  export function readSqliteHomeIdentity(rootDir, databaseFilename = "yui.db") {
73
71
  const database = new Database(join(rootDir, databaseFilename), {
@@ -222,7 +220,75 @@ export class SqliteTaskStore {
222
220
  }
223
221
  #now() { return new Date().toISOString(); }
224
222
  #json(value) { return JSON.stringify(value); }
223
+ #recordJson(table, value) {
224
+ validateStoredRecord(table, value);
225
+ return this.#json(value);
226
+ }
227
+ /** Explicit full-Home diagnosis; ordinary reads validate only their records. */
228
+ validateCurrentRecords() {
229
+ this.getConfig();
230
+ for (const table of STORED_RECORD_TABLES) {
231
+ for (const row of this.#db.prepare(`SELECT payload FROM ${table}`).iterate()) {
232
+ this.#validateTaskPayload(table, row.payload);
233
+ }
234
+ }
235
+ for (const row of this.#db.prepare("SELECT brief FROM task_records WHERE brief IS NOT NULL").iterate()) {
236
+ validateTaskBrief(this.#parse(row.brief));
237
+ }
238
+ }
225
239
  #parse(text) { return JSON.parse(text); }
240
+ queryContextRecords(taskId, query) {
241
+ return queryContextRecords(this.#db, taskId, query);
242
+ }
243
+ contextInputReferences(taskId, scope) {
244
+ return contextInputReferences(this.#db, taskId, scope);
245
+ }
246
+ listActiveRuns(taskId) {
247
+ return this.#sortById(this.#listPayload("turns", "task_id = ? AND status = 'active'", [taskId]), run => run.id);
248
+ }
249
+ latestEventSequence(taskId) {
250
+ return this.#db.prepare("SELECT COALESCE(MAX(CAST(substr(event_id, 7) AS INTEGER)), 0) AS seq FROM events WHERE task_id = ?").get(taskId).seq;
251
+ }
252
+ listEventsByType(taskId, types) {
253
+ return this.#sortById(this.#listPayload("events", "task_id = ? AND type IN (SELECT value FROM json_each(?))", [taskId, JSON.stringify(types)]), event => event.id);
254
+ }
255
+ /** Delivery needs original workspace bases, never complete Run reports. */
256
+ listTaskRunWorkspaceBases(taskId) {
257
+ return this.#db.prepare(`SELECT json_object(
258
+ 'id', turn_id, 'createdAt', json_extract(payload, '$.createdAt'),
259
+ 'workspace', json_extract(payload, '$.workspace')) AS payload
260
+ FROM turns WHERE task_id = ? AND json_extract(payload, '$.workspace.owner.type') = 'task'`).all(taskId).map(row => JSON.parse(row.payload));
261
+ }
262
+ /** A pinned read snapshot never reserves the database's single writer slot.
263
+ * Nested reads share an existing transaction; standalone reads also prohibit
264
+ * accidental writes at SQLite's boundary.
265
+ */
266
+ readTransaction(execute) {
267
+ if (this.#inTransaction)
268
+ return execute(this);
269
+ const previous = this.#db.pragma("query_only", { simple: true });
270
+ this.#db.pragma("query_only = ON");
271
+ this.#db.exec("BEGIN");
272
+ this.#inTransaction = true;
273
+ try {
274
+ this.#prepareWrite();
275
+ const result = execute(this);
276
+ this.#commit();
277
+ return result;
278
+ }
279
+ catch (error) {
280
+ this.#rollback();
281
+ throw error;
282
+ }
283
+ finally {
284
+ this.#db.pragma(`query_only = ${previous === 1 ? "ON" : "OFF"}`);
285
+ }
286
+ }
287
+ /** Diagnostic worker write; deliberately outside Task revision/outbox state. */
288
+ flushTelemetry(entries, runCap) {
289
+ this.#prepareWrite();
290
+ writeTelemetryBatch(this.#db, entries, runCap);
291
+ }
226
292
  #begin() {
227
293
  this.#db.exec("BEGIN IMMEDIATE");
228
294
  this.#inTransaction = true;
@@ -536,22 +602,7 @@ export class SqliteTaskStore {
536
602
  }
537
603
  #validateTaskPayload(table, payload) {
538
604
  const record = this.#parse(payload);
539
- if (table === "task_records")
540
- validateTask(record);
541
- if (table === "work_items")
542
- validateWorkItem(record);
543
- // Agents and Roles carry the execution component, and their validators
544
- // refuse a record that lost it. Reading them unchecked defeated that: a
545
- // component-less row reached display and rendered `undefined` as if it
546
- // were a product, and reached launch as a binding nothing had vetted.
547
- if (table === "configured_agents")
548
- validateConfiguredAgent(record);
549
- if (table === "task_roles")
550
- validateTaskRole(record);
551
- if (table === "global_roles")
552
- validateGlobalRole(record);
553
- if (table === "global_role_messages")
554
- validateGlobalRoleMessage(record);
605
+ validateStoredRecord(table, record);
555
606
  return record;
556
607
  }
557
608
  #sortById(rows, idOf) {
@@ -583,12 +634,12 @@ export class SqliteTaskStore {
583
634
  saveConfiguredAgent(agent) {
584
635
  this.#mutate(() => {
585
636
  this.#db.prepare(`INSERT INTO configured_agents (id, payload, updated_at) VALUES (?, ?, ?)
586
- ON CONFLICT(id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(agent.id, this.#json(agent), this.#now());
637
+ ON CONFLICT(id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(agent.id, this.#recordJson("configured_agents", agent), this.#now());
587
638
  });
588
639
  }
589
640
  createConfiguredAgentIfAbsent(agent) {
590
641
  return this.#mutate(() => {
591
- const result = this.#db.prepare("INSERT OR IGNORE INTO configured_agents (id, payload, updated_at) VALUES (?, ?, ?)").run(agent.id, this.#json(agent), this.#now());
642
+ const result = this.#db.prepare("INSERT OR IGNORE INTO configured_agents (id, payload, updated_at) VALUES (?, ?, ?)").run(agent.id, this.#recordJson("configured_agents", agent), this.#now());
592
643
  return result.changes > 0 ? agent : null;
593
644
  });
594
645
  }
@@ -628,7 +679,7 @@ export class SqliteTaskStore {
628
679
  return;
629
680
  }
630
681
  this.#db.prepare("INSERT INTO plugin_validations (task_id, id, payload) VALUES (?, ?, ?)")
631
- .run(validation.taskId, validation.id, this.#json(validation));
682
+ .run(validation.taskId, validation.id, this.#recordJson("plugin_validations", validation));
632
683
  });
633
684
  }
634
685
  savePluginIntent(intent) {
@@ -645,7 +696,7 @@ export class SqliteTaskStore {
645
696
  }
646
697
  this.#db.prepare(`INSERT INTO plugin_intents (task_id, plugin_id, payload) VALUES (?, ?, ?)
647
698
  ON CONFLICT(task_id, plugin_id) DO UPDATE SET payload = excluded.payload`)
648
- .run(intent.taskId, intent.pluginId, this.#json(intent));
699
+ .run(intent.taskId, intent.pluginId, this.#recordJson("plugin_intents", intent));
649
700
  });
650
701
  }
651
702
  getPluginIntent(taskId, pluginId) {
@@ -663,7 +714,7 @@ export class SqliteTaskStore {
663
714
  return false;
664
715
  this.#mutate(() => {
665
716
  this.#db.prepare("UPDATE plugin_intents SET payload = ? WHERE task_id = ? AND plugin_id = ?")
666
- .run(this.#json({ ...current, lastFailure: failure }), taskId, pluginId);
717
+ .run(this.#recordJson("plugin_intents", { ...current, lastFailure: failure }), taskId, pluginId);
667
718
  });
668
719
  return true;
669
720
  });
@@ -682,7 +733,7 @@ export class SqliteTaskStore {
682
733
  return;
683
734
  }
684
735
  this.#db.prepare("INSERT INTO local_resources (id, canonical_identity, payload) VALUES (?, ?, ?)")
685
- .run(resource.id, `${resource.device}:${resource.inode}`, this.#json(resource));
736
+ .run(resource.id, `${resource.device}:${resource.inode}`, this.#recordJson("local_resources", resource));
686
737
  });
687
738
  }
688
739
  getLocalResource(resourceId) {
@@ -710,7 +761,7 @@ export class SqliteTaskStore {
710
761
  }
711
762
  this.#db.prepare(`INSERT INTO environment_preparations (task_id, id, payload) VALUES (?, ?, ?)
712
763
  ON CONFLICT(task_id, id) DO UPDATE SET payload = excluded.payload`)
713
- .run(preparation.taskId, preparation.id, this.#json(preparation));
764
+ .run(preparation.taskId, preparation.id, this.#recordJson("environment_preparations", preparation));
714
765
  });
715
766
  }
716
767
  getEnvironmentPreparation(taskId, preparationId) {
@@ -726,13 +777,13 @@ export class SqliteTaskStore {
726
777
  validateProject(project);
727
778
  this.#mutate(() => {
728
779
  this.#db.prepare(`INSERT INTO projects (id, name, path, payload, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)
729
- ON CONFLICT(id) DO UPDATE SET name = excluded.name, path = excluded.path, payload = excluded.payload, updated_at = excluded.updated_at`).run(project.id, project.name, project.path, this.#json(project), project.createdAt, project.updatedAt);
780
+ ON CONFLICT(id) DO UPDATE SET name = excluded.name, path = excluded.path, payload = excluded.payload, updated_at = excluded.updated_at`).run(project.id, project.name, project.path, this.#recordJson("projects", project), project.createdAt, project.updatedAt);
730
781
  });
731
782
  }
732
783
  createProjectIfAbsent(project) {
733
784
  validateProject(project);
734
785
  return this.#mutate(() => {
735
- const result = this.#db.prepare("INSERT OR IGNORE INTO projects (id, name, path, payload, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)").run(project.id, project.name, project.path, this.#json(project), project.createdAt, project.updatedAt);
786
+ const result = this.#db.prepare("INSERT OR IGNORE INTO projects (id, name, path, payload, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)").run(project.id, project.name, project.path, this.#recordJson("projects", project), project.createdAt, project.updatedAt);
736
787
  return result.changes > 0 ? project : null;
737
788
  });
738
789
  }
@@ -786,12 +837,12 @@ export class SqliteTaskStore {
786
837
  saveAgentProfile(profile) {
787
838
  this.#mutate(() => {
788
839
  this.#db.prepare(`INSERT INTO agent_profiles (id, payload, updated_at) VALUES (?, ?, ?)
789
- ON CONFLICT(id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(profile.id, this.#json(profile), this.#now());
840
+ ON CONFLICT(id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(profile.id, this.#recordJson("agent_profiles", profile), this.#now());
790
841
  });
791
842
  }
792
843
  createAgentProfileIfAbsent(profile) {
793
844
  return this.#mutate(() => {
794
- const result = this.#db.prepare("INSERT OR IGNORE INTO agent_profiles (id, payload, updated_at) VALUES (?, ?, ?)").run(profile.id, this.#json(profile), this.#now());
845
+ const result = this.#db.prepare("INSERT OR IGNORE INTO agent_profiles (id, payload, updated_at) VALUES (?, ?, ?)").run(profile.id, this.#recordJson("agent_profiles", profile), this.#now());
795
846
  return result.changes > 0 ? profile : null;
796
847
  });
797
848
  }
@@ -808,16 +859,16 @@ export class SqliteTaskStore {
808
859
  saveGlobalRole(role) {
809
860
  this.#mutate(() => {
810
861
  this.#db.prepare(`INSERT INTO global_roles (name, payload, updated_at) VALUES (?, ?, ?)
811
- ON CONFLICT(name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(role.name, this.#json(role), this.#now());
862
+ ON CONFLICT(name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(role.name, this.#recordJson("global_roles", role), this.#now());
812
863
  });
813
864
  }
814
865
  saveGlobalRoleWithSessionSet(role, sessions) {
815
866
  this.#mutate(() => {
816
867
  this.#db.prepare(`INSERT INTO global_roles (name, payload, updated_at) VALUES (?, ?, ?)
817
- ON CONFLICT(name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(role.name, this.#json(role), this.#now());
868
+ ON CONFLICT(name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(role.name, this.#recordJson("global_roles", role), this.#now());
818
869
  if (sessions !== null) {
819
870
  this.#db.prepare(`INSERT INTO global_role_session_sets (name, payload, updated_at) VALUES (?, ?, ?)
820
- ON CONFLICT(name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(role.name, this.#json(sessions), this.#now());
871
+ ON CONFLICT(name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(role.name, this.#recordJson("global_role_session_sets", sessions), this.#now());
821
872
  this.#saveRuntimeSessionCandidate(sessions);
822
873
  }
823
874
  else {
@@ -828,7 +879,7 @@ export class SqliteTaskStore {
828
879
  }
829
880
  createGlobalRoleIfAbsent(role) {
830
881
  return this.#mutate(() => {
831
- const result = this.#db.prepare("INSERT OR IGNORE INTO global_roles (name, payload, updated_at) VALUES (?, ?, ?)").run(role.name, this.#json(role), this.#now());
882
+ const result = this.#db.prepare("INSERT OR IGNORE INTO global_roles (name, payload, updated_at) VALUES (?, ?, ?)").run(role.name, this.#recordJson("global_roles", role), this.#now());
832
883
  return result.changes > 0 ? role : null;
833
884
  });
834
885
  }
@@ -855,7 +906,7 @@ export class SqliteTaskStore {
855
906
  saveGlobalRoleSessionSet(sessions) {
856
907
  this.#mutate(() => {
857
908
  this.#db.prepare(`INSERT INTO global_role_session_sets (name, payload, updated_at) VALUES (?, ?, ?)
858
- ON CONFLICT(name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(sessions.owner.roleName, this.#json(sessions), this.#now());
909
+ ON CONFLICT(name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(sessions.owner.roleName, this.#recordJson("global_role_session_sets", sessions), this.#now());
859
910
  this.#saveRuntimeSessionCandidate(sessions);
860
911
  });
861
912
  }
@@ -866,7 +917,7 @@ export class SqliteTaskStore {
866
917
  this.#requireGlobalRole(message.roleName);
867
918
  this.#mutate(() => {
868
919
  const seq = this.#globalMessageSequence(message.id);
869
- this.#db.prepare(`INSERT INTO global_role_messages (name, message_id, seq, payload, created_at) VALUES (?, ?, ?, ?, ?)`).run(message.roleName, message.id, seq, this.#json(message), message.createdAt);
920
+ this.#db.prepare(`INSERT INTO global_role_messages (name, message_id, seq, payload, created_at) VALUES (?, ?, ?, ?, ?)`).run(message.roleName, message.id, seq, this.#recordJson("global_role_messages", message), message.createdAt);
870
921
  });
871
922
  }
872
923
  listGlobalRoleMessages(roleName) {
@@ -877,7 +928,7 @@ export class SqliteTaskStore {
877
928
  this.#requireGlobalRole(message.roleName);
878
929
  this.#mutate(() => {
879
930
  const seq = this.#globalMessageSequence(message.id);
880
- const result = this.#db.prepare(`UPDATE global_role_messages SET seq = ?, payload = ? WHERE name = ? AND message_id = ?`).run(seq, this.#json(message), message.roleName, message.id);
931
+ const result = this.#db.prepare(`UPDATE global_role_messages SET seq = ?, payload = ? WHERE name = ? AND message_id = ?`).run(seq, this.#recordJson("global_role_messages", message), message.roleName, message.id);
881
932
  if (result.changes === 0) {
882
933
  throw new StorageRecordError(`Global message not found: ${message.roleName}/${message.id}`);
883
934
  }
@@ -915,7 +966,7 @@ export class SqliteTaskStore {
915
966
  ON CONFLICT(task_id) DO UPDATE SET status = excluded.status, lifecycle = excluded.lifecycle,
916
967
  is_active = excluded.is_active, updated_at = excluded.updated_at`).run(task.id, task.status, task.status, isActive, task.createdAt, task.updatedAt);
917
968
  this.#db.prepare(`INSERT INTO task_records (task_id, payload, updated_at) VALUES (?, ?, ?)
918
- ON CONFLICT(task_id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(task.id, this.#json(task), this.#now());
969
+ ON CONFLICT(task_id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(task.id, this.#recordJson("task_records", task), this.#now());
919
970
  });
920
971
  }
921
972
  listTasks() {
@@ -960,7 +1011,6 @@ export class SqliteTaskStore {
960
1011
  integrationJobs: this.listDurableJobs(taskId)
961
1012
  .filter(job => job.owner.kind === "integration-attempt")
962
1013
  .map(job => ({ id: job.id, status: job.status })),
963
- integrationQueueEntries: this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id),
964
1014
  reviewRounds: this.#sortById(this.#listPayload("review_rounds", "task_id = ?", [taskId]), (round) => round.id),
965
1015
  reviewConfig: this.getReviewConfig(),
966
1016
  openInputRequests: this.#sortById(this.#listPayload("input_requests", "task_id = ? AND status = 'open'", [taskId]), (request) => request.id),
@@ -980,7 +1030,6 @@ export class SqliteTaskStore {
980
1030
  pendingUserMessages: pendingCompletionMessages(this, taskId),
981
1031
  managedWorkspaces: this.#sortById(this.#listPayload("managed_workspaces", "task_id = ?", [taskId]), (workspace) => managedWorkspaceKey(workspace.owner)),
982
1032
  durableJobs: this.#sortById(this.#listPayload("durable_jobs", "task_id = ?", [taskId]), (job) => job.id),
983
- integrationQueueEntries: this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id),
984
1033
  };
985
1034
  }
986
1035
  /** Task ids flagged active in the catalog projection (the global active index). */
@@ -1045,7 +1094,7 @@ export class SqliteTaskStore {
1045
1094
  this.#db.prepare(`INSERT INTO change_sets (task_id, change_set_id, project_id, head_sha, payload, created_at)
1046
1095
  VALUES (?, ?, ?, ?, ?, ?)
1047
1096
  ON CONFLICT(task_id, change_set_id) DO UPDATE SET project_id = excluded.project_id,
1048
- head_sha = excluded.head_sha, payload = excluded.payload`).run(taskId, changeSet.id, changeSet.projectId, changeSet.headCommit, this.#json(changeSet), changeSet.createdAt);
1097
+ head_sha = excluded.head_sha, payload = excluded.payload`).run(taskId, changeSet.id, changeSet.projectId, changeSet.headCommit, this.#recordJson("change_sets", changeSet), changeSet.createdAt);
1049
1098
  });
1050
1099
  }
1051
1100
  listChangeSets(taskId) {
@@ -1061,10 +1110,16 @@ export class SqliteTaskStore {
1061
1110
  throw new StorageRecordError(`Integration attempt belongs to another Task: ${attempt.taskId}`);
1062
1111
  this.#requireTask(taskId);
1063
1112
  this.#mutate(() => {
1113
+ if (typeof attempt.rerunChecks !== "boolean")
1114
+ throw new StorageRecordError("Integration rerunChecks must be explicit.");
1115
+ const existing = this.getIntegrationAttempt(taskId, attempt.id);
1116
+ if (existing !== null && existing.rerunChecks !== attempt.rerunChecks) {
1117
+ throw new StorageRecordError("Integration check execution intent is immutable; create a new attempt.");
1118
+ }
1064
1119
  this.#db.prepare(`INSERT INTO integration_attempts (task_id, integration_id, status, payload, updated_at)
1065
1120
  VALUES (?, ?, ?, ?, ?)
1066
1121
  ON CONFLICT(task_id, integration_id) DO UPDATE SET status = excluded.status,
1067
- payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, attempt.id, attempt.status, this.#json(attempt), this.#now());
1122
+ payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, attempt.id, attempt.status, this.#recordJson("integration_attempts", attempt), this.#now());
1068
1123
  });
1069
1124
  }
1070
1125
  listIntegrationAttempts(taskId) {
@@ -1073,46 +1128,6 @@ export class SqliteTaskStore {
1073
1128
  getIntegrationAttempt(taskId, integrationId) {
1074
1129
  return this.#getPayload("integration_attempts", "task_id = ? AND integration_id = ?", [taskId, integrationId]);
1075
1130
  }
1076
- // -- integration queue -----------------------------------------------------
1077
- nextIntegrationQueueEntryId(taskId) {
1078
- return this.#nextTaskRecordId(taskId, "integrationQueue");
1079
- }
1080
- saveIntegrationQueueEntry(taskId, entry) {
1081
- if (entry.taskId !== taskId) {
1082
- throw new StorageRecordError(`Integration queue entry belongs to another Task: ${entry.taskId}`);
1083
- }
1084
- validateIntegrationQueueEntry(entry);
1085
- this.#requireTask(taskId);
1086
- const changeSet = this.getChangeSet(taskId, entry.changeSetId);
1087
- if (changeSet === null) {
1088
- throw new StorageRecordError(`Integration queue ChangeSet not found: ${entry.changeSetId}`);
1089
- }
1090
- if (changeSet.projectId !== entry.projectId) {
1091
- throw new StorageRecordError(`Integration queue ChangeSet belongs to another Project: ${entry.changeSetId}`);
1092
- }
1093
- const existing = this.getIntegrationQueueEntry(taskId, entry.id);
1094
- if (existing !== null) {
1095
- if (Date.parse(entry.updatedAt) < Date.parse(existing.updatedAt)) {
1096
- throw new StorageRecordError(`Integration queue entry updatedAt cannot move backwards: ${entry.id}`);
1097
- }
1098
- if (!validIntegrationQueueTransition(existing, entry)) {
1099
- throw new StorageRecordError(`Integration queue entry transition is invalid: ${entry.id}`);
1100
- }
1101
- }
1102
- this.#mutate(() => {
1103
- this.#db.prepare(`INSERT INTO integration_queue (queue_id, task_id, project_id, change_set, status, payload, created_at, updated_at)
1104
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1105
- ON CONFLICT(queue_id) DO UPDATE SET task_id = excluded.task_id,
1106
- project_id = excluded.project_id, change_set = excluded.change_set,
1107
- status = excluded.status, payload = excluded.payload, updated_at = excluded.updated_at`).run(entry.id, taskId, entry.projectId, entry.changeSetId, entry.status, this.#json(entry), entry.createdAt, entry.updatedAt);
1108
- });
1109
- }
1110
- listIntegrationQueueEntries(taskId) {
1111
- return this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id);
1112
- }
1113
- getIntegrationQueueEntry(taskId, entryId) {
1114
- return this.#getPayload("integration_queue", "task_id = ? AND queue_id = ?", [taskId, entryId]);
1115
- }
1116
1131
  // -- durable jobs -----------------------------------------------------------
1117
1132
  nextDurableJobId(taskId) {
1118
1133
  return this.#nextTaskRecordId(taskId, "durableJob");
@@ -1137,7 +1152,7 @@ export class SqliteTaskStore {
1137
1152
  VALUES (?, ?, ?, ?, ?, ?, ?)
1138
1153
  ON CONFLICT(task_id, job_id) DO UPDATE SET idempotency_key = excluded.idempotency_key,
1139
1154
  status = excluded.status,
1140
- payload = excluded.payload, updated_at = excluded.updated_at`).run(job.id, taskId, job.idempotencyKey ?? null, job.status, this.#json(job), job.createdAt, job.updatedAt);
1155
+ payload = excluded.payload, updated_at = excluded.updated_at`).run(job.id, taskId, job.idempotencyKey ?? null, job.status, this.#recordJson("durable_jobs", job), job.createdAt, job.updatedAt);
1141
1156
  });
1142
1157
  }
1143
1158
  listDurableJobs(taskId) {
@@ -1169,12 +1184,12 @@ export class SqliteTaskStore {
1169
1184
  role_name = excluded.role_name, agent_id = excluded.agent_id,
1170
1185
  native_session_id = excluded.native_session_id,
1171
1186
  provider_root_pid = excluded.provider_root_pid,
1172
- payload = excluded.payload, recorded_at = excluded.recorded_at`).run(sessionOwnerProcessKey(identity), owner.scope, owner.scope === "task" ? owner.taskId : null, owner.roleName, identity.agentId, identity.nativeSessionId ?? null, identity.providerRoot.pid, this.#json(identity), identity.recordedAt);
1187
+ payload = excluded.payload, recorded_at = excluded.recorded_at`).run(sessionOwnerProcessKey(identity), owner.scope, owner.scope === "task" ? owner.taskId : null, owner.roleName, identity.agentId, identity.nativeSessionId ?? null, identity.providerRoot.pid, this.#recordJson("session_owners", identity), identity.recordedAt);
1173
1188
  });
1174
1189
  }
1175
1190
  getSessionOwner(processKey) {
1176
1191
  const row = this.#db.prepare("SELECT payload FROM session_owners WHERE process_key = ?").get(processKey);
1177
- return row === undefined ? null : this.#parse(row.payload);
1192
+ return row === undefined ? null : this.#validateTaskPayload("session_owners", row.payload);
1178
1193
  }
1179
1194
  listSessionOwners() {
1180
1195
  return this.#listPayload("session_owners", "1=1", []).sort((left, right) => left.recordedAt.localeCompare(right.recordedAt));
@@ -1195,7 +1210,7 @@ export class SqliteTaskStore {
1195
1210
  this.#requireTask(taskId);
1196
1211
  this.#mutate(() => {
1197
1212
  this.#db.prepare(`INSERT INTO task_roles (task_id, role_name, payload, updated_at) VALUES (?, ?, ?, ?)
1198
- ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, role.name, this.#json(role), this.#now());
1213
+ ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, role.name, this.#recordJson("task_roles", role), this.#now());
1199
1214
  });
1200
1215
  }
1201
1216
  listRoles(taskId) {
@@ -1208,9 +1223,9 @@ export class SqliteTaskStore {
1208
1223
  this.#requireTask(role.name ? role.taskId : sessions.owner.taskId);
1209
1224
  this.#mutate(() => {
1210
1225
  this.#db.prepare(`INSERT INTO task_roles (task_id, role_name, payload, updated_at) VALUES (?, ?, ?, ?)
1211
- ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(sessions.owner.taskId, role.name, this.#json(role), this.#now());
1226
+ ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(sessions.owner.taskId, role.name, this.#recordJson("task_roles", role), this.#now());
1212
1227
  this.#db.prepare(`INSERT INTO role_session_sets (task_id, role_name, payload, updated_at) VALUES (?, ?, ?, ?)
1213
- ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(sessions.owner.taskId, sessions.owner.roleName, this.#json(sessions), this.#now());
1228
+ ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(sessions.owner.taskId, sessions.owner.roleName, this.#recordJson("role_session_sets", sessions), this.#now());
1214
1229
  this.#saveRuntimeSessionCandidate(sessions);
1215
1230
  });
1216
1231
  }
@@ -1236,7 +1251,7 @@ export class SqliteTaskStore {
1236
1251
  this.#db.prepare(`INSERT INTO managed_workspaces (owner_kind, owner_id, task_id, path, payload, status, created_at, updated_at)
1237
1252
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1238
1253
  ON CONFLICT(owner_kind, owner_id) DO UPDATE SET task_id = excluded.task_id, path = excluded.path,
1239
- payload = excluded.payload, status = excluded.status, updated_at = excluded.updated_at`).run(workspace.owner.type, ownerId, taskId, workspace.root, this.#json(workspace), "active", workspace.createdAt, workspace.updatedAt);
1254
+ payload = excluded.payload, status = excluded.status, updated_at = excluded.updated_at`).run(workspace.owner.type, ownerId, taskId, workspace.root, this.#recordJson("managed_workspaces", workspace), "active", workspace.createdAt, workspace.updatedAt);
1240
1255
  });
1241
1256
  }
1242
1257
  listManagedWorkspaces(taskId) {
@@ -1390,7 +1405,7 @@ export class SqliteTaskStore {
1390
1405
  this.#requireTask(taskId);
1391
1406
  this.#mutate(() => {
1392
1407
  this.#db.prepare(`INSERT INTO role_session_sets (task_id, role_name, payload, updated_at) VALUES (?, ?, ?, ?)
1393
- ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, sessions.owner.roleName, this.#json(sessions), this.#now());
1408
+ ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, sessions.owner.roleName, this.#recordJson("role_session_sets", sessions), this.#now());
1394
1409
  this.#saveRuntimeSessionCandidate(sessions);
1395
1410
  });
1396
1411
  }
@@ -1441,7 +1456,7 @@ export class SqliteTaskStore {
1441
1456
  this.#mutate(() => {
1442
1457
  this.#db.prepare(`INSERT INTO work_items (task_id, work_item_id, status, payload, updated_at) VALUES (?, ?, ?, ?, ?)
1443
1458
  ON CONFLICT(task_id, work_item_id) DO UPDATE SET status = excluded.status,
1444
- payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, item.id, item.status, this.#json(item), this.#now());
1459
+ payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, item.id, item.status, this.#recordJson("work_items", item), this.#now());
1445
1460
  });
1446
1461
  }
1447
1462
  // -- capability grants ------------------------------------------------------
@@ -1466,7 +1481,7 @@ export class SqliteTaskStore {
1466
1481
  throw new StorageRecordError(`Capability grant cannot be overwritten: ${taskId}/${stored.id}`);
1467
1482
  }
1468
1483
  this.#db.prepare(`INSERT INTO capability_grants (task_id, grant_id, payload, updated_at) VALUES (?, ?, ?, ?)
1469
- ON CONFLICT(task_id, grant_id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, stored.id, this.#json(stored), this.#now());
1484
+ ON CONFLICT(task_id, grant_id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, stored.id, this.#recordJson("capability_grants", stored), this.#now());
1470
1485
  });
1471
1486
  }
1472
1487
  listCapabilityGrants(taskId) {
@@ -1489,7 +1504,7 @@ export class SqliteTaskStore {
1489
1504
  throw new StorageRecordError(`Release workflow cannot be overwritten: ${taskId}/${stored.id}`);
1490
1505
  }
1491
1506
  this.#db.prepare(`INSERT INTO release_workflows (task_id, workflow_id, payload, updated_at) VALUES (?, ?, ?, ?)
1492
- ON CONFLICT(task_id, workflow_id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, stored.id, this.#json(stored), this.#now());
1507
+ ON CONFLICT(task_id, workflow_id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, stored.id, this.#recordJson("release_workflows", stored), this.#now());
1493
1508
  });
1494
1509
  }
1495
1510
  listReleaseWorkflows(taskId) {
@@ -1536,7 +1551,7 @@ export class SqliteTaskStore {
1536
1551
  remote_commit, supersedes, payload, merged_at, created_at,
1537
1552
  title, source_branch, target_branch)
1538
1553
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1539
- ON CONFLICT(task_id, publication_id) DO UPDATE SET payload = excluded.payload`).run(taskId, stored.id, stored.projectId, stored.provider, stored.repository, stored.externalKind, stored.externalId, key, stored.state, stored.verification, stored.externalUrl ?? null, stored.localCommit ?? null, stored.remoteCommit ?? null, stored.supersedes ?? null, this.#json(stored), stored.mergedAt ?? null, this.#now(), stored.title ?? null, stored.sourceBranch ?? null, stored.targetBranch ?? null);
1554
+ ON CONFLICT(task_id, publication_id) DO UPDATE SET payload = excluded.payload`).run(taskId, stored.id, stored.projectId, stored.provider, stored.repository, stored.externalKind, stored.externalId, key, stored.state, stored.verification, stored.externalUrl ?? null, stored.localCommit ?? null, stored.remoteCommit ?? null, stored.supersedes ?? null, this.#recordJson("publication_references", stored), stored.mergedAt ?? null, this.#now(), stored.title ?? null, stored.sourceBranch ?? null, stored.targetBranch ?? null);
1540
1555
  });
1541
1556
  }
1542
1557
  listPublicationReferences(taskId) {
@@ -1553,7 +1568,7 @@ export class SqliteTaskStore {
1553
1568
  saveGateArtifact(artifact, logs) {
1554
1569
  validateGateArtifact(artifact);
1555
1570
  this.#mutate(() => {
1556
- const targetRef = artifact.boundary?.targetRef ?? null;
1571
+ const targetRef = artifact.boundary.targetRef;
1557
1572
  const completedAt = artifact.completedAt ?? null;
1558
1573
  this.#db.prepare(`INSERT INTO gate_artifacts
1559
1574
  (key, project_id, level, commit_sha, plan_digest, toolchain_digest,
@@ -1569,8 +1584,9 @@ export class SqliteTaskStore {
1569
1584
  status = excluded.status,
1570
1585
  outcome = excluded.outcome,
1571
1586
  payload = excluded.payload,
1587
+ created_at = excluded.created_at,
1572
1588
  completed_at = excluded.completed_at,
1573
- last_used_at = excluded.last_used_at`).run(artifact.key, artifact.projectId, artifact.level, artifact.commit, artifact.planDigest, artifact.toolchainDigest, targetRef, artifact.status, artifact.outcome, this.#json(artifact), artifact.createdAt, completedAt, artifact.lastUsedAt);
1589
+ last_used_at = excluded.last_used_at`).run(artifact.key, artifact.projectId, artifact.level, artifact.commit, artifact.planDigest, artifact.toolchainDigest, targetRef, artifact.status, artifact.outcome, this.#recordJson("gate_artifacts", artifact), artifact.createdAt, completedAt, artifact.lastUsedAt);
1574
1590
  // Replace all logs for this artifact in the same transaction.
1575
1591
  this.#db.prepare("DELETE FROM gate_artifact_logs WHERE artifact_key = ?").run(artifact.key);
1576
1592
  const insertLog = this.#db.prepare(`INSERT INTO gate_artifact_logs (artifact_key, step_name, log_content, log_digest, log_bytes)
@@ -1587,7 +1603,15 @@ export class SqliteTaskStore {
1587
1603
  touchGateArtifact(artifact) {
1588
1604
  validateGateArtifact(artifact);
1589
1605
  this.#mutate(() => {
1590
- const result = this.#db.prepare(`UPDATE gate_artifacts SET payload = ?, last_used_at = ? WHERE key = ?`).run(this.#json(artifact), artifact.lastUsedAt, artifact.key);
1606
+ const current = this.getGateArtifact(artifact.projectId, artifact.key);
1607
+ if (current === null || !isDeepStrictEqual(current, {
1608
+ ...artifact, reuseCount: current.reuseCount, lastUsedAt: current.lastUsedAt
1609
+ })) {
1610
+ throw new StorageConflictError("Gate artifact changed while consuming evidence; retry the exact lookup.");
1611
+ }
1612
+ const updated = { ...current, reuseCount: Math.max(current.reuseCount, artifact.reuseCount),
1613
+ lastUsedAt: Date.parse(artifact.lastUsedAt) > Date.parse(current.lastUsedAt) ? artifact.lastUsedAt : current.lastUsedAt };
1614
+ const result = this.#db.prepare(`UPDATE gate_artifacts SET payload = ?, last_used_at = ? WHERE key = ?`).run(this.#recordJson("gate_artifacts", updated), updated.lastUsedAt, artifact.key);
1591
1615
  if (result.changes === 0) {
1592
1616
  throw new StorageRecordError(`Gate artifact not found for touch: ${artifact.key}`);
1593
1617
  }
@@ -1602,18 +1626,8 @@ export class SqliteTaskStore {
1602
1626
  }
1603
1627
  findL2GateArtifactsForCommit(query) {
1604
1628
  const rows = this.#listPayload("gate_artifacts", `project_id = ? AND commit_sha = ? AND level = 'L2'
1605
- AND plan_digest = ? AND toolchain_digest = ? AND target_ref = ?
1606
- AND status = 'complete' AND outcome = 'succeeded'`, [query.projectId, query.commit, query.planDigest, query.toolchainDigest, query.targetRef]);
1607
- const valid = [];
1608
- for (const row of rows) {
1609
- try {
1610
- valid.push(validateGateArtifact(row));
1611
- }
1612
- catch {
1613
- // Skip corrupt rows; a direct lookup fails closed.
1614
- }
1615
- }
1616
- return valid;
1629
+ AND plan_digest = ? AND toolchain_digest = ? AND target_ref = ?`, [query.projectId, query.commit, query.planDigest, query.toolchainDigest, query.targetRef]);
1630
+ return rows.map(validateGateArtifact);
1617
1631
  }
1618
1632
  getGateArtifactLogs(artifactKey) {
1619
1633
  const rows = this.#db.prepare("SELECT step_name, log_content FROM gate_artifact_logs WHERE artifact_key = ?").all(artifactKey);
@@ -1684,7 +1698,7 @@ export class SqliteTaskStore {
1684
1698
  this.#mutate(() => {
1685
1699
  this.#db.prepare(`INSERT INTO context_snapshots
1686
1700
  (task_id, snapshot_id, scope, scope_ref, sequence, digest, payload, frozen_at)
1687
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(stored.taskId, stored.id, stored.scope, stored.scopeRef ?? null, stored.sequence, stored.digest, this.#json(stored), stored.frozenAt);
1701
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(stored.taskId, stored.id, stored.scope, stored.scopeRef ?? null, stored.sequence, stored.digest, this.#recordJson("context_snapshots", stored), stored.frozenAt);
1688
1702
  });
1689
1703
  }
1690
1704
  // -- turns -----------------------------------------------------------------
@@ -1715,7 +1729,7 @@ export class SqliteTaskStore {
1715
1729
  this.#mutate(() => {
1716
1730
  this.#db.prepare(`INSERT INTO turns (task_id, turn_id, role_name, status, payload, updated_at) VALUES (?, ?, ?, ?, ?, ?)
1717
1731
  ON CONFLICT(task_id, turn_id) DO UPDATE SET role_name = excluded.role_name, status = excluded.status,
1718
- payload = excluded.payload, updated_at = excluded.updated_at`).run(run.taskId, run.id, run.roleName, run.status, this.#json(run), this.#now());
1732
+ payload = excluded.payload, updated_at = excluded.updated_at`).run(run.taskId, run.id, run.roleName, run.status, this.#recordJson("turns", run), this.#now());
1719
1733
  });
1720
1734
  }
1721
1735
  // -- review rounds ----------------------------------------------------------
@@ -1734,7 +1748,7 @@ export class SqliteTaskStore {
1734
1748
  this.#mutate(() => {
1735
1749
  this.#db.prepare(`INSERT INTO review_rounds (task_id, review_round_id, status, payload, updated_at) VALUES (?, ?, ?, ?, ?)
1736
1750
  ON CONFLICT(task_id, review_round_id) DO UPDATE SET status = excluded.status,
1737
- payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, round.id, round.status, this.#json(round), this.#now());
1751
+ payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, round.id, round.status, this.#recordJson("review_rounds", round), this.#now());
1738
1752
  });
1739
1753
  }
1740
1754
  // -- active turns ----------------------------------------------------------
@@ -1922,7 +1936,7 @@ export class SqliteTaskStore {
1922
1936
  this.#requireTask(taskId);
1923
1937
  this.#mutate(() => {
1924
1938
  const seq = this.#idSequence(message.id, "message");
1925
- this.#db.prepare(`INSERT INTO messages (task_id, message_id, seq, payload, created_at) VALUES (?, ?, ?, ?, ?)`).run(taskId, message.id, seq, this.#json(message), message.createdAt);
1939
+ this.#db.prepare(`INSERT INTO messages (task_id, message_id, seq, payload, created_at) VALUES (?, ?, ?, ?, ?)`).run(taskId, message.id, seq, this.#recordJson("messages", message), message.createdAt);
1926
1940
  this.#observeHighWater(taskId, "message", seq);
1927
1941
  });
1928
1942
  }
@@ -1932,7 +1946,7 @@ export class SqliteTaskStore {
1932
1946
  }
1933
1947
  this.#requireTask(taskId);
1934
1948
  this.#mutate(() => {
1935
- const result = this.#db.prepare(`UPDATE messages SET payload = ? WHERE task_id = ? AND message_id = ?`).run(this.#json(message), taskId, message.id);
1949
+ const result = this.#db.prepare(`UPDATE messages SET payload = ? WHERE task_id = ? AND message_id = ?`).run(this.#recordJson("messages", message), taskId, message.id);
1936
1950
  if (result.changes !== 1) {
1937
1951
  throw new StorageRecordError(`Message does not exist: ${taskId}/${message.id}`);
1938
1952
  }
@@ -1949,7 +1963,7 @@ export class SqliteTaskStore {
1949
1963
  this.#requireTask(taskId);
1950
1964
  this.#mutate(() => {
1951
1965
  this.#db.prepare(`INSERT INTO input_requests (task_id, input_id, status, payload, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)
1952
- ON CONFLICT(task_id, input_id) DO UPDATE SET status = excluded.status, payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, request.id, request.status, this.#json(request), request.createdAt, this.#now());
1966
+ ON CONFLICT(task_id, input_id) DO UPDATE SET status = excluded.status, payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, request.id, request.status, this.#recordJson("input_requests", request), request.createdAt, this.#now());
1953
1967
  });
1954
1968
  }
1955
1969
  getInputRequest(taskId, requestId) {
@@ -1980,7 +1994,7 @@ export class SqliteTaskStore {
1980
1994
  this.#requireTask(taskId);
1981
1995
  this.#mutate(() => {
1982
1996
  this.#db.prepare(`INSERT INTO decisions (task_id, decision_id, payload, created_at) VALUES (?, ?, ?, ?)
1983
- ON CONFLICT(task_id, decision_id) DO UPDATE SET payload = excluded.payload`).run(taskId, decision.id, this.#json(decision), decision.createdAt);
1997
+ ON CONFLICT(task_id, decision_id) DO UPDATE SET payload = excluded.payload`).run(taskId, decision.id, this.#recordJson("decisions", decision), decision.createdAt);
1984
1998
  });
1985
1999
  }
1986
2000
  listDecisions(taskId) {
@@ -1997,7 +2011,7 @@ export class SqliteTaskStore {
1997
2011
  this.#requireTask(taskId);
1998
2012
  this.#mutate(() => {
1999
2013
  this.#db.prepare(`INSERT INTO milestones (task_id, milestone_id, payload, created_at) VALUES (?, ?, ?, ?)
2000
- ON CONFLICT(task_id, milestone_id) DO UPDATE SET payload = excluded.payload`).run(taskId, milestone.id, this.#json(milestone), milestone.createdAt);
2014
+ ON CONFLICT(task_id, milestone_id) DO UPDATE SET payload = excluded.payload`).run(taskId, milestone.id, this.#recordJson("milestones", milestone), milestone.createdAt);
2001
2015
  });
2002
2016
  }
2003
2017
  listMilestones(taskId) {
@@ -2015,7 +2029,7 @@ export class SqliteTaskStore {
2015
2029
  this.#mutate(() => {
2016
2030
  const seq = this.#idSequence(event.id, "event");
2017
2031
  // Events are terminal/semantic: retained individually, never pruned (§9).
2018
- this.#db.prepare(`INSERT INTO events (task_id, event_id, type, occurred_at, payload) VALUES (?, ?, ?, ?, ?)`).run(taskId, event.id, event.type, event.createdAt, this.#json(event));
2032
+ this.#db.prepare(`INSERT INTO events (task_id, event_id, type, occurred_at, payload) VALUES (?, ?, ?, ?, ?)`).run(taskId, event.id, event.type, event.createdAt, this.#recordJson("events", event));
2019
2033
  this.#observeHighWater(taskId, "event", seq);
2020
2034
  });
2021
2035
  }
@@ -2046,14 +2060,13 @@ export class SqliteTaskStore {
2046
2060
  this.#mutate(() => {
2047
2061
  const seq = this.#idSequence(wake.id, "taskWake");
2048
2062
  this.#db.prepare(`INSERT INTO task_wakes
2049
- (task_id, wake_id, seq, status, turn_id, from_cursor, to_cursor, reasons, payload, created_at, consumed_at)
2050
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2063
+ (task_id, wake_id, seq, status, from_cursor, to_cursor, reasons, payload, created_at, consumed_at)
2064
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2051
2065
  + ` ON CONFLICT(task_id, wake_id) DO UPDATE SET
2052
2066
  status = excluded.status,
2053
- turn_id = excluded.turn_id,
2054
2067
  reasons = excluded.reasons,
2055
2068
  payload = excluded.payload,
2056
- consumed_at = excluded.consumed_at`).run(taskId, wake.id, wake.seq, wake.status, wake.runId ?? null, wake.fromCursor, wake.toCursor, this.#json([...wake.reasons]), this.#json(wake), wake.createdAt, wake.consumedAt ?? null);
2069
+ consumed_at = excluded.consumed_at`).run(taskId, wake.id, wake.seq, wake.status, wake.fromCursor, wake.toCursor, this.#json([...wake.reasons]), this.#recordJson("task_wakes", wake), wake.createdAt, wake.consumedAt ?? null);
2057
2070
  this.#observeHighWater(taskId, "taskWake", seq);
2058
2071
  });
2059
2072
  }
@@ -2315,25 +2328,6 @@ export class SqliteTaskStore {
2315
2328
  });
2316
2329
  }
2317
2330
  }
2318
- function validIntegrationQueueTransition(before, after) {
2319
- if (before.id !== after.id
2320
- || before.taskId !== after.taskId
2321
- || before.projectId !== after.projectId
2322
- || before.changeSetId !== after.changeSetId
2323
- || before.targetRef !== after.targetRef
2324
- || !isDeepStrictEqual(before.checkCommands, after.checkCommands)
2325
- || !isDeepStrictEqual(before.evidenceRefs, after.evidenceRefs)
2326
- || before.createdAt !== after.createdAt)
2327
- return false;
2328
- const allowed = {
2329
- queued: ["queued", "running", "superseded"],
2330
- running: ["running", "conflicted", "committed"],
2331
- conflicted: ["conflicted", "running", "committed", "queued", "superseded"],
2332
- committed: ["committed"],
2333
- superseded: ["superseded"]
2334
- };
2335
- return allowed[before.status].includes(after.status);
2336
- }
2337
2331
  /** The product storage backend is one current SQLite contract. */
2338
2332
  export function resolveTaskStoreBackendForHome(_home, _env = process.env) {
2339
2333
  return "sqlite";