@zq-silk/yui 0.5.3 → 0.6.1

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 (157) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/agentConfigurationPicker.js +1 -1
  4. package/dist/cli/commandCatalog.js +251 -13
  5. package/dist/cli/updateOrchestrator.js +8 -0
  6. package/dist/cli/updatePorts.js +76 -22
  7. package/dist/cli.js +264 -20
  8. package/dist/commands/configCommands.js +83 -9
  9. package/dist/commands/controllerCommands.js +103 -0
  10. package/dist/commands/deliveryGuardPreflight.js +35 -0
  11. package/dist/commands/durableJobCommands.js +231 -0
  12. package/dist/commands/executionAuditCommands.js +193 -0
  13. package/dist/commands/grantCommands.js +374 -0
  14. package/dist/commands/projectCommands.js +119 -81
  15. package/dist/commands/releaseCommands.js +444 -0
  16. package/dist/commands/resourcesCommands.js +274 -0
  17. package/dist/commands/sessionCommands.js +104 -0
  18. package/dist/commands/taskActor.js +117 -0
  19. package/dist/commands/taskChangeSetCommands.js +60 -0
  20. package/dist/commands/taskCommands.js +610 -201
  21. package/dist/commands/taskCompletionGate.js +78 -1
  22. package/dist/commands/taskContextCommand.js +24 -6
  23. package/dist/commands/taskInputCommands.js +1 -1
  24. package/dist/commands/taskIntegrationCommands.js +136 -33
  25. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  26. package/dist/commands/taskNextActionCommand.js +85 -0
  27. package/dist/commands/taskOverlapCommands.js +120 -0
  28. package/dist/commands/taskOverviewCommand.js +36 -8
  29. package/dist/commands/telemetryCommands.js +330 -0
  30. package/dist/commands/workflowCommands.js +415 -0
  31. package/dist/config/yuiConfig.js +60 -0
  32. package/dist/controller/clientRuntime.js +42 -1
  33. package/dist/controller/controller.js +413 -61
  34. package/dist/controller/controllerMain.js +25 -2
  35. package/dist/controller/domainIdentity.js +16 -8
  36. package/dist/controller/ephemeralResourceReaper.js +2 -1
  37. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  38. package/dist/controller/handoverCandidate.js +168 -0
  39. package/dist/controller/jobClient.js +102 -0
  40. package/dist/controller/jobControl.js +613 -0
  41. package/dist/controller/jobSupervisor.js +498 -0
  42. package/dist/controller/providerHookRunFence.js +34 -5
  43. package/dist/controller/resourceCleanupLinux.js +18 -9
  44. package/dist/controller/resourceInventoryLinux.js +90 -39
  45. package/dist/controller/resourceInventoryRpc.js +85 -0
  46. package/dist/controller/resourceInventoryWorker.js +50 -0
  47. package/dist/controller/runtime.js +238 -22
  48. package/dist/controller/runtimeEventInbox.js +234 -57
  49. package/dist/controller/runtimeEventProcessor.js +549 -42
  50. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  51. package/dist/core/boundedRpc.js +475 -0
  52. package/dist/core/controllerServer.js +416 -27
  53. package/dist/core/controllerTelemetry.js +167 -0
  54. package/dist/doctor/doctor.js +113 -16
  55. package/dist/domain/validation.js +9 -0
  56. package/dist/execution/executionGroup.js +40 -3
  57. package/dist/executor/agentExecutor.js +6 -3
  58. package/dist/executor/effectiveLaunch.js +52 -0
  59. package/dist/executor/executorRegistry.js +50 -0
  60. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  61. package/dist/grant/capabilityGrant.js +282 -0
  62. package/dist/integration/changeSet.js +16 -3
  63. package/dist/integration/changeSetManifest.js +46 -0
  64. package/dist/integration/gitIntegrationService.js +528 -147
  65. package/dist/integration/integrationAttempt.js +54 -5
  66. package/dist/integration/integrationQueueEntry.js +221 -0
  67. package/dist/integration/integrationQueueService.js +955 -0
  68. package/dist/integration/manifestTags.js +99 -0
  69. package/dist/integration/overlapDiagnostics.js +211 -0
  70. package/dist/job/durableJob.js +449 -0
  71. package/dist/job/jobRunner.js +350 -0
  72. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  73. package/dist/lifecycle/providerErrorClass.js +126 -0
  74. package/dist/message/message.js +16 -3
  75. package/dist/observability/executionAudit.js +545 -0
  76. package/dist/observability/faultClassification.js +160 -0
  77. package/dist/observability/runtimeIdentity.js +367 -0
  78. package/dist/release/fakeReleasePorts.js +55 -0
  79. package/dist/release/releaseHandover.js +475 -0
  80. package/dist/release/releaseIdempotencyStore.js +165 -0
  81. package/dist/release/releaseWorkflow.js +459 -0
  82. package/dist/release/releaseWorkflowEngine.js +688 -0
  83. package/dist/release/releaseWorkflowPorts.js +1720 -0
  84. package/dist/release/runtimeRelease.js +495 -0
  85. package/dist/release/workflowFileLock.js +218 -0
  86. package/dist/repository/gitWorkspace.js +177 -1
  87. package/dist/repository/projectMaintenanceLock.js +315 -0
  88. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  89. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  90. package/dist/resources/autoResourceGc.js +116 -0
  91. package/dist/resources/liveReferences.js +574 -0
  92. package/dist/resources/resourceDiscovery.js +477 -0
  93. package/dist/resources/resourceGc.js +645 -0
  94. package/dist/resources/resourceRegistrar.js +256 -0
  95. package/dist/resources/resourceRegistry.js +150 -0
  96. package/dist/resources/resourceRegistryStore.js +41 -0
  97. package/dist/resources/resourceTypes.js +42 -0
  98. package/dist/resources/sqliteResourceRegistry.js +111 -0
  99. package/dist/review/reviewConfig.js +10 -0
  100. package/dist/review/reviewFinding.js +240 -0
  101. package/dist/review/reviewFindingLedger.js +545 -0
  102. package/dist/review/reviewOutcomeClassifier.js +61 -0
  103. package/dist/review/reviewRound.js +56 -4
  104. package/dist/run/agentRun.js +80 -4
  105. package/dist/run/providerRetry.js +84 -0
  106. package/dist/run/providerRetryConfig.js +63 -0
  107. package/dist/run/yieldReceipt.js +65 -0
  108. package/dist/runtime/exactControlPlane.js +79 -2
  109. package/dist/runtime/index.js +4 -0
  110. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  111. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  112. package/dist/runtime/sessionReconciliation.js +93 -0
  113. package/dist/runtime/sessionTerminationGuard.js +211 -0
  114. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  115. package/dist/runtime/tmuxAdapters.js +34 -1
  116. package/dist/scheduler/actionability.js +155 -0
  117. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  118. package/dist/scheduler/activeTaskProgress.js +60 -0
  119. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  120. package/dist/scheduler/roleRunStall.js +135 -29
  121. package/dist/scheduler/taskExecutionProjection.js +11 -0
  122. package/dist/setup/setupCommand.js +27 -4
  123. package/dist/storage/compatibleTaskStore.js +112 -5
  124. package/dist/storage/migration/productionRegistry.js +769 -1
  125. package/dist/storage/persistenceWorker.js +194 -0
  126. package/dist/storage/sqliteSchema.js +705 -0
  127. package/dist/storage/sqliteStore.js +1695 -0
  128. package/dist/storage/storageVersions.js +9 -2
  129. package/dist/storage/storeRpc.js +298 -0
  130. package/dist/storage/taskStore.js +982 -21
  131. package/dist/storage/upgrade/homeClassification.js +157 -12
  132. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  133. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  134. package/dist/storage/upgrade/recordVersions.js +10 -1
  135. package/dist/storage/upgrade/sqliteMigrationTarget.js +351 -0
  136. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  137. package/dist/storage/upgrade/sqliteStateMigration.js +713 -0
  138. package/dist/storage/upgrade/upgradeOrchestrator.js +510 -18
  139. package/dist/task/deliveryGuard.js +226 -0
  140. package/dist/task/nextAction.js +343 -0
  141. package/dist/task/repairWave.js +137 -0
  142. package/dist/task/taskRecordReference.js +6 -1
  143. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  144. package/dist/telemetry/telemetryCompaction.js +251 -0
  145. package/dist/telemetry/telemetryConfig.js +64 -0
  146. package/dist/telemetry/telemetryRouter.js +32 -0
  147. package/dist/telemetry/telemetryStore.js +19 -0
  148. package/dist/telemetry/telemetryWiring.js +33 -0
  149. package/dist/tmux/tmuxManager.js +20 -1
  150. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  151. package/dist/verification/gateArtifact.js +216 -0
  152. package/dist/verification/gateArtifactStore.js +87 -0
  153. package/dist/verification/verificationGateService.js +414 -0
  154. package/dist/verification/verificationPlan.js +308 -0
  155. package/dist/workspace/gitChangeSetCapture.js +12 -2
  156. package/dist/workspace/workItemChangeSetManager.js +60 -3
  157. package/package.json +2 -1
@@ -0,0 +1,705 @@
1
+ /**
2
+ * SQLite WAL control-plane schema and migration runner (task-21, work-item-3).
3
+ *
4
+ * This is the normalized, `task_id`-partitioned schema that replaces the single
5
+ * aggregate `state.json` document. It implements the logical schema from
6
+ * `docs/sqlite-control-plane-design.md` §4 (31 tables) plus the cross-task
7
+ * coordination tables the design references in §5:
8
+ *
9
+ * - `global_sequences` (§5.3): global record ID high-water marks.
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).
13
+ *
14
+ * Record payloads are stored two ways, per §4: typed columns for fields that are
15
+ * 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.
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.
23
+ */
24
+ import { createHash } from "node:crypto";
25
+ /** The SQLite layout version (state.json layout 6 -> SQLite layout 7, per §8.1). */
26
+ export const SQLITE_LAYOUT_VERSION = 7;
27
+ /** The aggregate version of the normalized SQLite schema. */
28
+ export const SQLITE_AGGREGATE_VERSION = 1;
29
+ /** The current schema migration version. */
30
+ export const SQLITE_SCHEMA_VERSION = 9;
31
+ /** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
32
+ export const TELEMETRY_KEEP_PER_GENERATION = 200;
33
+ export const TELEMETRY_RUN_CAP = 50_000;
34
+ /**
35
+ * Version 1 migration: creates every table and index.
36
+ *
37
+ * `synchronous`/`foreign_keys`/`busy_timeout` are per-connection PRAGMAs set by
38
+ * the store; `journal_mode=WAL` is a persistent database property set on open.
39
+ * The migration itself only contains schema objects.
40
+ */
41
+ const MIGRATION_1_SQL = `
42
+ -- Global catalog and coordination (§4.1) -------------------------------------
43
+
44
+ CREATE TABLE IF NOT EXISTS home_meta (
45
+ id INTEGER PRIMARY KEY CHECK (id = 1),
46
+ home_identity TEXT NOT NULL,
47
+ revision INTEGER NOT NULL,
48
+ layout_version INTEGER NOT NULL,
49
+ aggregate_version INTEGER NOT NULL,
50
+ created_at TEXT NOT NULL,
51
+ updated_at TEXT NOT NULL
52
+ );
53
+
54
+ CREATE TABLE IF NOT EXISTS config (
55
+ id INTEGER PRIMARY KEY CHECK (id = 1),
56
+ payload TEXT NOT NULL,
57
+ updated_at TEXT NOT NULL
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS configured_agents (
61
+ id TEXT PRIMARY KEY,
62
+ payload TEXT NOT NULL,
63
+ updated_at TEXT NOT NULL
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS agent_profiles (
67
+ id TEXT PRIMARY KEY,
68
+ payload TEXT NOT NULL,
69
+ updated_at TEXT NOT NULL
70
+ );
71
+
72
+ CREATE TABLE IF NOT EXISTS projects (
73
+ id TEXT PRIMARY KEY,
74
+ name TEXT NOT NULL,
75
+ path TEXT NOT NULL,
76
+ payload TEXT NOT NULL,
77
+ created_at TEXT NOT NULL,
78
+ updated_at TEXT NOT NULL
79
+ );
80
+
81
+ CREATE TABLE IF NOT EXISTS global_roles (
82
+ name TEXT PRIMARY KEY,
83
+ payload TEXT NOT NULL,
84
+ updated_at TEXT NOT NULL
85
+ );
86
+
87
+ CREATE TABLE IF NOT EXISTS global_role_session_sets (
88
+ name TEXT PRIMARY KEY,
89
+ payload TEXT NOT NULL,
90
+ updated_at TEXT NOT NULL,
91
+ FOREIGN KEY (name) REFERENCES global_roles(name)
92
+ );
93
+
94
+ -- Global record ID high-water marks (§5.3).
95
+ CREATE TABLE IF NOT EXISTS global_sequences (
96
+ name TEXT PRIMARY KEY,
97
+ high_water INTEGER NOT NULL
98
+ );
99
+
100
+ -- Task catalog: the global active index and lifecycle lookup.
101
+ CREATE TABLE IF NOT EXISTS tasks_catalog (
102
+ task_id TEXT PRIMARY KEY,
103
+ status TEXT NOT NULL,
104
+ lifecycle TEXT NOT NULL,
105
+ is_active INTEGER NOT NULL CHECK (is_active IN (0,1)),
106
+ created_at TEXT NOT NULL,
107
+ updated_at TEXT NOT NULL
108
+ );
109
+ CREATE INDEX IF NOT EXISTS idx_tasks_active ON tasks_catalog(is_active) WHERE is_active = 1;
110
+
111
+ -- Session/Workspace ownership is global (workspaces outlive task activity).
112
+ CREATE TABLE IF NOT EXISTS managed_workspaces (
113
+ owner_kind TEXT NOT NULL CHECK (owner_kind IN
114
+ ('task','work-item','review-round','integration-attempt','execution-lane')),
115
+ owner_id TEXT NOT NULL,
116
+ task_id TEXT,
117
+ path TEXT NOT NULL,
118
+ payload TEXT NOT NULL,
119
+ status TEXT NOT NULL,
120
+ created_at TEXT NOT NULL,
121
+ updated_at TEXT NOT NULL,
122
+ PRIMARY KEY (owner_kind, owner_id)
123
+ );
124
+ CREATE INDEX IF NOT EXISTS idx_workspaces_task ON managed_workspaces(task_id);
125
+
126
+ -- Per-task record ID high-water marks (replaces StoredTask.idHighWaterMarks).
127
+ CREATE TABLE IF NOT EXISTS id_sequences (
128
+ task_id TEXT NOT NULL,
129
+ kind TEXT NOT NULL,
130
+ high_water INTEGER NOT NULL,
131
+ PRIMARY KEY (task_id, kind)
132
+ );
133
+
134
+ -- Cross-task coordination: Project locks and the Integration queue.
135
+ CREATE TABLE IF NOT EXISTS coordination_locks (
136
+ lock_key TEXT PRIMARY KEY,
137
+ holder_task TEXT NOT NULL,
138
+ holder_ref TEXT NOT NULL,
139
+ acquired_at TEXT NOT NULL,
140
+ expires_at TEXT
141
+ );
142
+
143
+ CREATE TABLE IF NOT EXISTS integration_queue (
144
+ queue_id TEXT PRIMARY KEY,
145
+ task_id TEXT NOT NULL,
146
+ project_id TEXT NOT NULL,
147
+ change_set TEXT NOT NULL,
148
+ status TEXT NOT NULL,
149
+ payload TEXT NOT NULL,
150
+ created_at TEXT NOT NULL,
151
+ updated_at TEXT NOT NULL
152
+ );
153
+ CREATE INDEX IF NOT EXISTS idx_integration_queue_status ON integration_queue(status, created_at);
154
+
155
+ -- Durable outbox (§5.4): UNIQUE(request_id) makes cross-task effects exactly-once.
156
+ CREATE TABLE IF NOT EXISTS outbox (
157
+ outbox_id INTEGER PRIMARY KEY,
158
+ request_id TEXT NOT NULL UNIQUE,
159
+ command TEXT NOT NULL,
160
+ state TEXT NOT NULL DEFAULT 'pending',
161
+ created_at TEXT NOT NULL,
162
+ applied_at TEXT
163
+ );
164
+ CREATE INDEX IF NOT EXISTS idx_outbox_state ON outbox(state, created_at);
165
+
166
+ -- Mailboxes (§4.2): per-target ordering and exactly-once signals. -------------
167
+
168
+ CREATE TABLE IF NOT EXISTS mailboxes (
169
+ mailbox_id INTEGER PRIMARY KEY,
170
+ target_kind TEXT NOT NULL CHECK (target_kind IN
171
+ ('task','role','role-runtime','global-role-runtime','operator')),
172
+ task_id TEXT,
173
+ role_name TEXT,
174
+ -- The stable mailboxTargetKey string carries the real uniqueness: the column
175
+ -- UNIQUE below cannot, because NULL task_id/role_name (the 'operator' and
176
+ -- 'global-role-runtime' targets) are distinct under SQL NULL semantics.
177
+ target_key TEXT NOT NULL,
178
+ next_sequence INTEGER NOT NULL,
179
+ processing TEXT,
180
+ pending TEXT,
181
+ UNIQUE (target_kind, task_id, role_name)
182
+ );
183
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_mailboxes_target_key
184
+ ON mailboxes(target_key);
185
+
186
+ -- Signals are the durable append log; (mailbox, sequence) is the exactly-once key.
187
+ CREATE TABLE IF NOT EXISTS mailbox_signals (
188
+ mailbox_id INTEGER NOT NULL,
189
+ sequence INTEGER NOT NULL,
190
+ reason TEXT NOT NULL,
191
+ ref_type TEXT,
192
+ ref_task_id TEXT,
193
+ ref_id TEXT,
194
+ occurred_at TEXT NOT NULL,
195
+ request_id TEXT NOT NULL,
196
+ PRIMARY KEY (mailbox_id, sequence),
197
+ FOREIGN KEY (mailbox_id) REFERENCES mailboxes(mailbox_id)
198
+ );
199
+ CREATE INDEX IF NOT EXISTS idx_mailbox_signals_request ON mailbox_signals(request_id);
200
+
201
+ -- Task-partitioned tables (§4.3). Every task-local read constrains task_id. ----
202
+
203
+ CREATE TABLE IF NOT EXISTS task_records (
204
+ task_id TEXT PRIMARY KEY,
205
+ payload TEXT NOT NULL,
206
+ brief TEXT,
207
+ updated_at TEXT NOT NULL,
208
+ FOREIGN KEY (task_id) REFERENCES tasks_catalog(task_id)
209
+ );
210
+
211
+ CREATE TABLE IF NOT EXISTS task_roles (
212
+ task_id TEXT NOT NULL,
213
+ role_name TEXT NOT NULL,
214
+ payload TEXT NOT NULL,
215
+ updated_at TEXT NOT NULL,
216
+ PRIMARY KEY (task_id, role_name)
217
+ );
218
+
219
+ CREATE TABLE IF NOT EXISTS role_session_sets (
220
+ task_id TEXT NOT NULL,
221
+ role_name TEXT NOT NULL,
222
+ payload TEXT NOT NULL,
223
+ updated_at TEXT NOT NULL,
224
+ PRIMARY KEY (task_id, role_name)
225
+ );
226
+
227
+ CREATE TABLE IF NOT EXISTS work_items (
228
+ task_id TEXT NOT NULL,
229
+ work_item_id TEXT NOT NULL,
230
+ status TEXT NOT NULL,
231
+ payload TEXT NOT NULL,
232
+ updated_at TEXT NOT NULL,
233
+ PRIMARY KEY (task_id, work_item_id)
234
+ );
235
+ CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(task_id, status);
236
+
237
+ CREATE TABLE IF NOT EXISTS work_item_candidates (
238
+ task_id TEXT NOT NULL,
239
+ candidate_id TEXT NOT NULL,
240
+ work_item_id TEXT NOT NULL,
241
+ payload TEXT NOT NULL,
242
+ created_at TEXT NOT NULL,
243
+ PRIMARY KEY (task_id, candidate_id)
244
+ );
245
+
246
+ CREATE TABLE IF NOT EXISTS agent_runs (
247
+ task_id TEXT NOT NULL,
248
+ run_id TEXT NOT NULL,
249
+ role_name TEXT NOT NULL,
250
+ status TEXT NOT NULL,
251
+ payload TEXT NOT NULL,
252
+ updated_at TEXT NOT NULL,
253
+ PRIMARY KEY (task_id, run_id)
254
+ );
255
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_role_status ON agent_runs(task_id, role_name, status);
256
+
257
+ -- Active-run pointers (getActiveAgentRun / execution-lane runs).
258
+ CREATE TABLE IF NOT EXISTS active_runs (
259
+ task_id TEXT NOT NULL,
260
+ pointer TEXT NOT NULL,
261
+ run_id TEXT NOT NULL,
262
+ payload TEXT NOT NULL,
263
+ updated_at TEXT NOT NULL,
264
+ PRIMARY KEY (task_id, pointer)
265
+ );
266
+
267
+ CREATE TABLE IF NOT EXISTS review_rounds (
268
+ task_id TEXT NOT NULL,
269
+ review_round_id TEXT NOT NULL,
270
+ status TEXT NOT NULL,
271
+ payload TEXT NOT NULL,
272
+ updated_at TEXT NOT NULL,
273
+ PRIMARY KEY (task_id, review_round_id)
274
+ );
275
+
276
+ CREATE TABLE IF NOT EXISTS change_sets (
277
+ task_id TEXT NOT NULL,
278
+ change_set_id TEXT NOT NULL,
279
+ project_id TEXT NOT NULL,
280
+ head_sha TEXT NOT NULL,
281
+ payload TEXT NOT NULL,
282
+ created_at TEXT NOT NULL,
283
+ PRIMARY KEY (task_id, change_set_id)
284
+ );
285
+ CREATE INDEX IF NOT EXISTS idx_change_sets_project ON change_sets(task_id, project_id);
286
+
287
+ CREATE TABLE IF NOT EXISTS integration_attempts (
288
+ task_id TEXT NOT NULL,
289
+ integration_id TEXT NOT NULL,
290
+ status TEXT NOT NULL,
291
+ payload TEXT NOT NULL,
292
+ updated_at TEXT NOT NULL,
293
+ PRIMARY KEY (task_id, integration_id)
294
+ );
295
+
296
+ CREATE TABLE IF NOT EXISTS messages (
297
+ task_id TEXT NOT NULL,
298
+ message_id TEXT NOT NULL,
299
+ seq INTEGER NOT NULL,
300
+ payload TEXT NOT NULL,
301
+ created_at TEXT NOT NULL,
302
+ PRIMARY KEY (task_id, message_id)
303
+ );
304
+ CREATE INDEX IF NOT EXISTS idx_messages_seq ON messages(task_id, seq);
305
+
306
+ CREATE TABLE IF NOT EXISTS input_requests (
307
+ task_id TEXT NOT NULL,
308
+ input_id TEXT NOT NULL,
309
+ status TEXT NOT NULL,
310
+ blocks TEXT,
311
+ payload TEXT NOT NULL,
312
+ created_at TEXT NOT NULL,
313
+ updated_at TEXT NOT NULL,
314
+ PRIMARY KEY (task_id, input_id)
315
+ );
316
+ CREATE INDEX IF NOT EXISTS idx_input_open ON input_requests(task_id, status) WHERE status <> 'resolved';
317
+
318
+ CREATE TABLE IF NOT EXISTS decisions (
319
+ task_id TEXT NOT NULL,
320
+ decision_id TEXT NOT NULL,
321
+ payload TEXT NOT NULL,
322
+ created_at TEXT NOT NULL,
323
+ PRIMARY KEY (task_id, decision_id)
324
+ );
325
+
326
+ CREATE TABLE IF NOT EXISTS milestones (
327
+ task_id TEXT NOT NULL,
328
+ milestone_id TEXT NOT NULL,
329
+ payload TEXT NOT NULL,
330
+ created_at TEXT NOT NULL,
331
+ PRIMARY KEY (task_id, milestone_id)
332
+ );
333
+
334
+ -- Terminal/semantic events: retained individually, never pruned.
335
+ CREATE TABLE IF NOT EXISTS events (
336
+ task_id TEXT NOT NULL,
337
+ event_id TEXT NOT NULL,
338
+ type TEXT NOT NULL,
339
+ occurred_at TEXT NOT NULL,
340
+ payload TEXT NOT NULL,
341
+ PRIMARY KEY (task_id, event_id)
342
+ );
343
+ CREATE INDEX IF NOT EXISTS idx_events_type_time ON events(task_id, type, occurred_at);
344
+
345
+ -- Per-task scheduler projections (leaderFailure, operatorNotification).
346
+ CREATE TABLE IF NOT EXISTS task_projections (
347
+ task_id TEXT NOT NULL,
348
+ kind TEXT NOT NULL CHECK (kind IN ('leader-failure','operator-notification')),
349
+ payload TEXT,
350
+ updated_at TEXT NOT NULL,
351
+ PRIMARY KEY (task_id, kind)
352
+ );
353
+
354
+ -- Telemetry (§4.4): bounded, latest-per-key. WITHOUT ROWID, PK is the key.
355
+ CREATE TABLE IF NOT EXISTS telemetry (
356
+ task_id TEXT NOT NULL,
357
+ role_name TEXT NOT NULL,
358
+ run_id TEXT NOT NULL,
359
+ generation TEXT NOT NULL,
360
+ progress_id TEXT NOT NULL,
361
+ sequence INTEGER,
362
+ payload TEXT NOT NULL,
363
+ received_at TEXT NOT NULL,
364
+ PRIMARY KEY (task_id, role_name, run_id, generation, progress_id)
365
+ ) WITHOUT ROWID;
366
+ CREATE INDEX IF NOT EXISTS idx_telemetry_run ON telemetry(task_id, run_id);
367
+ `;
368
+ /**
369
+ * Migration 2: post-baseline task-scoped record families. This single
370
+ * migration creates the DurableJob, CapabilityGrant, and ReleaseWorkflow
371
+ * tables so a fresh database receives the complete merged schema atomically.
372
+ */
373
+ const MIGRATION_2_SQL = `
374
+ CREATE TABLE IF NOT EXISTS durable_jobs (
375
+ job_id TEXT PRIMARY KEY,
376
+ task_id TEXT NOT NULL,
377
+ idempotency_key TEXT,
378
+ status TEXT NOT NULL,
379
+ payload TEXT NOT NULL,
380
+ created_at TEXT NOT NULL,
381
+ updated_at TEXT NOT NULL
382
+ );
383
+ CREATE INDEX IF NOT EXISTS idx_durable_jobs_task ON durable_jobs(task_id);
384
+ CREATE INDEX IF NOT EXISTS idx_durable_jobs_status ON durable_jobs(status);
385
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_durable_jobs_idempotency
386
+ ON durable_jobs(task_id, idempotency_key) WHERE idempotency_key IS NOT NULL;
387
+ CREATE TABLE IF NOT EXISTS capability_grants (
388
+ task_id TEXT NOT NULL,
389
+ grant_id TEXT NOT NULL,
390
+ payload TEXT NOT NULL,
391
+ updated_at TEXT NOT NULL,
392
+ PRIMARY KEY (task_id, grant_id)
393
+ );
394
+
395
+ CREATE TABLE IF NOT EXISTS release_workflows (
396
+ task_id TEXT NOT NULL,
397
+ workflow_id TEXT NOT NULL,
398
+ payload TEXT NOT NULL,
399
+ updated_at TEXT NOT NULL,
400
+ PRIMARY KEY (task_id, workflow_id)
401
+ );
402
+ `;
403
+ /**
404
+ * Migration 3: Job caller key hashes (task-14, rr13). Durable SHA-256 hashes of
405
+ * the caller key bound to each launched Session, used to fail-closed verify a
406
+ * DurableJob's caller against durable Run state.
407
+ */
408
+ const MIGRATION_3_SQL = `
409
+ CREATE TABLE IF NOT EXISTS job_caller_key_hashes (
410
+ task_id TEXT NOT NULL,
411
+ role_name TEXT NOT NULL,
412
+ agent_id TEXT NOT NULL,
413
+ hash TEXT NOT NULL,
414
+ updated_at TEXT NOT NULL,
415
+ PRIMARY KEY (task_id, role_name, agent_id)
416
+ );
417
+ `;
418
+ /**
419
+ * Migration 4: DurableJob IDs are Task-local. Migration 2 accidentally made
420
+ * job_id the global primary key, so two Tasks allocating their first `job-1`
421
+ * could overwrite one another through the upsert path. Rebuild the table with
422
+ * the actual record identity `(task_id, job_id)` and restore its indexes.
423
+ * Never edit migration 2's checksum: existing Homes reach this repair through
424
+ * this adjacent schema migration.
425
+ */
426
+ const MIGRATION_4_SQL = `
427
+ CREATE TABLE durable_jobs_v4 (
428
+ job_id TEXT NOT NULL,
429
+ task_id TEXT NOT NULL,
430
+ idempotency_key TEXT,
431
+ status TEXT NOT NULL,
432
+ payload TEXT NOT NULL,
433
+ created_at TEXT NOT NULL,
434
+ updated_at TEXT NOT NULL,
435
+ PRIMARY KEY (task_id, job_id)
436
+ );
437
+ INSERT INTO durable_jobs_v4
438
+ (job_id, task_id, idempotency_key, status, payload, created_at, updated_at)
439
+ SELECT job_id, task_id, idempotency_key, status, payload, created_at, updated_at
440
+ FROM durable_jobs;
441
+ DROP TABLE durable_jobs;
442
+ ALTER TABLE durable_jobs_v4 RENAME TO durable_jobs;
443
+ CREATE INDEX idx_durable_jobs_task ON durable_jobs(task_id);
444
+ CREATE INDEX idx_durable_jobs_status ON durable_jobs(status);
445
+ CREATE UNIQUE INDEX idx_durable_jobs_idempotency
446
+ ON durable_jobs(task_id, idempotency_key) WHERE idempotency_key IS NOT NULL;
447
+ `;
448
+ /**
449
+ * Migration 5: telemetry aggregate (Issue 09). The `telemetry` table (migration
450
+ * 1, §4.4) stores the bounded latest-per-key progress window; this companion
451
+ * table holds the authoritative per-Run/generation summary (count, first/last,
452
+ * max sequence, error count) so aggregates stay accurate after the window is
453
+ * pruned. Triggers maintain it on telemetry INSERT/UPDATE; DELETE intentionally
454
+ * leaves it untouched because pruned rows were still observed.
455
+ */
456
+ const MIGRATION_5_SQL = `
457
+ CREATE TABLE IF NOT EXISTS telemetry_aggregate (
458
+ task_id TEXT NOT NULL,
459
+ role_name TEXT NOT NULL,
460
+ run_id TEXT NOT NULL,
461
+ generation TEXT NOT NULL,
462
+ first_at TEXT NOT NULL,
463
+ last_at TEXT NOT NULL,
464
+ count INTEGER NOT NULL,
465
+ max_sequence INTEGER,
466
+ error_count INTEGER NOT NULL DEFAULT 0,
467
+ updated_at TEXT NOT NULL,
468
+ PRIMARY KEY (task_id, role_name, run_id, generation)
469
+ ) WITHOUT ROWID;
470
+
471
+ CREATE TRIGGER IF NOT EXISTS telemetry_ai AFTER INSERT ON telemetry
472
+ BEGIN
473
+ INSERT INTO telemetry_aggregate
474
+ (task_id, role_name, run_id, generation, first_at, last_at, count, max_sequence, error_count, updated_at)
475
+ VALUES
476
+ (NEW.task_id, NEW.role_name, NEW.run_id, NEW.generation, NEW.received_at, NEW.received_at, 1, NEW.sequence,
477
+ CASE WHEN json_valid(NEW.payload)
478
+ AND (COALESCE(json_extract(NEW.payload, '$.error'), '') <> ''
479
+ OR COALESCE(json_extract(NEW.payload, '$.errorKind'), '') <> '')
480
+ THEN 1 ELSE 0 END,
481
+ NEW.received_at)
482
+ ON CONFLICT(task_id, role_name, run_id, generation) DO UPDATE SET
483
+ first_at = MIN(telemetry_aggregate.first_at, excluded.first_at),
484
+ last_at = MAX(telemetry_aggregate.last_at, excluded.last_at),
485
+ count = telemetry_aggregate.count + 1,
486
+ max_sequence = CASE
487
+ WHEN excluded.max_sequence IS NOT NULL
488
+ AND (telemetry_aggregate.max_sequence IS NULL OR excluded.max_sequence > telemetry_aggregate.max_sequence)
489
+ THEN excluded.max_sequence ELSE telemetry_aggregate.max_sequence END,
490
+ error_count = telemetry_aggregate.error_count + excluded.error_count,
491
+ updated_at = excluded.updated_at;
492
+ END;
493
+
494
+ CREATE TRIGGER IF NOT EXISTS telemetry_au AFTER UPDATE ON telemetry
495
+ BEGIN
496
+ UPDATE telemetry_aggregate SET
497
+ last_at = CASE WHEN NEW.received_at > last_at THEN NEW.received_at ELSE last_at END,
498
+ max_sequence = CASE
499
+ WHEN NEW.sequence IS NOT NULL AND (max_sequence IS NULL OR NEW.sequence > max_sequence)
500
+ THEN NEW.sequence ELSE max_sequence END,
501
+ updated_at = NEW.received_at
502
+ WHERE task_id = NEW.task_id AND role_name = NEW.role_name
503
+ AND run_id = NEW.run_id AND generation = NEW.generation;
504
+ END;
505
+ `;
506
+ /**
507
+ * Migration 6: Issue 06 ReviewFinding ledger. Cross-Round semantic review
508
+ * findings with stable keys and Leader dispositions. Failed execution attempts
509
+ * never create rows; only completed Rounds feed the ledger.
510
+ */
511
+ const MIGRATION_6_SQL = `
512
+ CREATE TABLE IF NOT EXISTS review_findings (
513
+ task_id TEXT NOT NULL,
514
+ finding_id TEXT NOT NULL,
515
+ stable_key TEXT NOT NULL,
516
+ severity TEXT NOT NULL,
517
+ payload TEXT NOT NULL,
518
+ updated_at TEXT NOT NULL,
519
+ PRIMARY KEY (task_id, finding_id)
520
+ );
521
+ CREATE INDEX IF NOT EXISTS idx_review_findings_task ON review_findings(task_id);
522
+ CREATE INDEX IF NOT EXISTS idx_review_findings_stable_key ON review_findings(task_id, stable_key);
523
+ `;
524
+ /**
525
+ * Version 5 migration: session owner physical identity records (Issue 03).
526
+ *
527
+ * One row per runtime generation, keyed by launch id. The payload column
528
+ * stores the full versioned JSON record; typed columns support the
529
+ * reconciliation queries (task/role lookup, PID liveness).
530
+ */
531
+ const MIGRATION_7_SQL = `
532
+ CREATE TABLE IF NOT EXISTS session_owners (
533
+ launch_id TEXT PRIMARY KEY,
534
+ scope TEXT NOT NULL CHECK (scope IN ('task','global')),
535
+ task_id TEXT,
536
+ role_name TEXT NOT NULL,
537
+ agent_id TEXT NOT NULL,
538
+ native_session_id TEXT,
539
+ provider_root_pid INTEGER,
540
+ payload TEXT NOT NULL,
541
+ recorded_at TEXT NOT NULL
542
+ );
543
+ CREATE INDEX IF NOT EXISTS idx_session_owners_task
544
+ ON session_owners(task_id, role_name);
545
+ `;
546
+ /**
547
+ * Migration 8: Resource GC registry (Issue 10).
548
+ *
549
+ * GC-owned table for resource lifecycle records. The registry is GC's own
550
+ * state — it is not part of the aggregate and never participates in aggregate
551
+ * versioning. Records are stored as full versioned JSON in `payload`, with
552
+ * typed columns for the fields GC queries (disposition, kind, task_id).
553
+ */
554
+ const MIGRATION_8_SQL = `
555
+ CREATE TABLE IF NOT EXISTS resource_registry (
556
+ id TEXT PRIMARY KEY,
557
+ kind TEXT NOT NULL,
558
+ path TEXT NOT NULL,
559
+ disposition TEXT NOT NULL,
560
+ task_id TEXT,
561
+ payload TEXT NOT NULL,
562
+ created_at TEXT NOT NULL,
563
+ updated_at TEXT NOT NULL
564
+ );
565
+ CREATE INDEX IF NOT EXISTS idx_resource_registry_disposition
566
+ ON resource_registry(disposition);
567
+ CREATE INDEX IF NOT EXISTS idx_resource_registry_task
568
+ ON resource_registry(task_id);
569
+ `;
570
+ /**
571
+ * Migration 9: GateArtifact storage (Issue 08). Content-addressed gate
572
+ * evidence records with per-step logs stored as BLOBs. The artifact key is
573
+ * the SHA-256 of the identity tuple (Project + commit + plan digest +
574
+ * toolchain digest + L2 boundary), so the same tuple always maps to one row.
575
+ * Typed columns support the reuse lookup paths (exact-commit L2 search,
576
+ * Project-level prune) without scanning payloads.
577
+ */
578
+ const MIGRATION_9_SQL = `
579
+ CREATE TABLE IF NOT EXISTS gate_artifacts (
580
+ key TEXT PRIMARY KEY,
581
+ project_id TEXT NOT NULL,
582
+ level TEXT NOT NULL CHECK (level IN ('L1','L2')),
583
+ commit_sha TEXT NOT NULL,
584
+ plan_digest TEXT NOT NULL,
585
+ toolchain_digest TEXT NOT NULL,
586
+ target_ref TEXT,
587
+ status TEXT NOT NULL CHECK (status IN ('incomplete','complete')),
588
+ outcome TEXT NOT NULL CHECK (outcome IN ('unknown','succeeded','failed')),
589
+ payload TEXT NOT NULL,
590
+ created_at TEXT NOT NULL,
591
+ completed_at TEXT,
592
+ last_used_at TEXT NOT NULL
593
+ );
594
+
595
+ CREATE INDEX IF NOT EXISTS idx_gate_artifacts_project_commit
596
+ ON gate_artifacts(project_id, commit_sha);
597
+
598
+ CREATE INDEX IF NOT EXISTS idx_gate_artifacts_project_last_used
599
+ ON gate_artifacts(project_id, last_used_at);
600
+
601
+ CREATE TABLE IF NOT EXISTS gate_artifact_logs (
602
+ artifact_key TEXT NOT NULL,
603
+ step_name TEXT NOT NULL,
604
+ log_content BLOB NOT NULL,
605
+ log_digest TEXT NOT NULL,
606
+ log_bytes INTEGER NOT NULL,
607
+ PRIMARY KEY (artifact_key, step_name),
608
+ FOREIGN KEY (artifact_key) REFERENCES gate_artifacts(key) ON DELETE CASCADE
609
+ );
610
+ `;
611
+ const MIGRATIONS = [
612
+ { version: 1, axis: "layout", sql: MIGRATION_1_SQL },
613
+ { version: 2, axis: "record", recordKind: "durableJob+capability-grant+release-workflow", sql: MIGRATION_2_SQL },
614
+ { version: 3, axis: "record", recordKind: "jobCallerKeyHash", sql: MIGRATION_3_SQL },
615
+ { version: 4, axis: "record", recordKind: "durableJob", sql: MIGRATION_4_SQL },
616
+ { version: 5, axis: "record", recordKind: "telemetryAggregate", sql: MIGRATION_5_SQL },
617
+ { version: 6, axis: "record", recordKind: "reviewFinding", sql: MIGRATION_6_SQL },
618
+ { version: 7, axis: "record", recordKind: "sessionOwner", sql: MIGRATION_7_SQL },
619
+ { version: 8, axis: "record", recordKind: "resource-registry", sql: MIGRATION_8_SQL },
620
+ { version: 9, axis: "record", recordKind: "gateArtifact", sql: MIGRATION_9_SQL }
621
+ ];
622
+ function checksum(sql) {
623
+ return createHash("sha256").update(sql).digest("hex");
624
+ }
625
+ /**
626
+ * Apply pending migrations idempotently inside transactions.
627
+ *
628
+ * Each migration runs in its own transaction: the DDL and the
629
+ * `schema_migrations` bookkeeping commit atomically, so a crash mid-migration
630
+ * rolls back and the next open re-applies cleanly. Re-running on a current
631
+ * database performs no work (every version is already recorded).
632
+ */
633
+ export function migrateSqliteSchema(db) {
634
+ db.exec(`
635
+ CREATE TABLE IF NOT EXISTS schema_migrations (
636
+ version INTEGER PRIMARY KEY,
637
+ axis TEXT NOT NULL CHECK (axis IN ('layout','aggregate','record')),
638
+ record_kind TEXT,
639
+ applied_at TEXT NOT NULL,
640
+ checksum TEXT NOT NULL
641
+ )
642
+ `);
643
+ const applied = new Set(db.prepare("SELECT version FROM schema_migrations").all().map((row) => row.version));
644
+ const newlyApplied = [];
645
+ for (const migration of MIGRATIONS) {
646
+ if (applied.has(migration.version))
647
+ continue;
648
+ const apply = db.transaction(() => {
649
+ db.exec(migration.sql);
650
+ db.prepare(`INSERT INTO schema_migrations (version, axis, record_kind, applied_at, checksum)
651
+ VALUES (?, ?, ?, ?, ?)`).run(migration.version, migration.axis, migration.recordKind ?? null, new Date().toISOString(), checksum(migration.sql));
652
+ });
653
+ apply();
654
+ newlyApplied.push(migration.version);
655
+ }
656
+ const version = MIGRATIONS.reduce((max, m) => Math.max(max, m.version), 0);
657
+ return { applied: newlyApplied, version };
658
+ }
659
+ /** The names of every table the schema creates (for tests/introspection). */
660
+ export const SQLITE_SCHEMA_TABLES = [
661
+ "schema_migrations",
662
+ "home_meta",
663
+ "config",
664
+ "configured_agents",
665
+ "agent_profiles",
666
+ "projects",
667
+ "global_roles",
668
+ "global_role_session_sets",
669
+ "global_sequences",
670
+ "tasks_catalog",
671
+ "managed_workspaces",
672
+ "id_sequences",
673
+ "coordination_locks",
674
+ "integration_queue",
675
+ "durable_jobs",
676
+ "job_caller_key_hashes",
677
+ "outbox",
678
+ "mailboxes",
679
+ "mailbox_signals",
680
+ "task_records",
681
+ "task_roles",
682
+ "role_session_sets",
683
+ "work_items",
684
+ "work_item_candidates",
685
+ "agent_runs",
686
+ "active_runs",
687
+ "review_rounds",
688
+ "review_findings",
689
+ "change_sets",
690
+ "integration_attempts",
691
+ "messages",
692
+ "input_requests",
693
+ "decisions",
694
+ "milestones",
695
+ "events",
696
+ "task_projections",
697
+ "telemetry",
698
+ "telemetry_aggregate",
699
+ "capability_grants",
700
+ "release_workflows",
701
+ "session_owners",
702
+ "resource_registry",
703
+ "gate_artifacts",
704
+ "gate_artifact_logs"
705
+ ];