@zq-silk/yui 0.16.2 → 1.0.0-alpha

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 (111) hide show
  1. package/README.md +6 -4
  2. package/dist/agent/agent.js +4 -9
  3. package/dist/agent/executionComponents.js +4 -4
  4. package/dist/agentRun/agentRun.js +8 -8
  5. package/dist/artifacts/managedGit.js +3 -56
  6. package/dist/brief/taskBrief.js +3 -3
  7. package/dist/cli/updateOrchestrator.js +2 -13
  8. package/dist/cli/updatePorts.js +36 -69
  9. package/dist/cli/upgradeCommand.js +4 -8
  10. package/dist/cli.js +4 -7
  11. package/dist/commands/controllerCommands.js +1 -1
  12. package/dist/commands/taskCommands.js +8 -1
  13. package/dist/commands/taskRoleRuntimeStatus.js +1 -1
  14. package/dist/context/runInputContract.js +1 -1
  15. package/dist/controller/agentRuntimeObserver.js +4 -4
  16. package/dist/controller/clientRuntime.js +19 -44
  17. package/dist/controller/controller.js +16 -4
  18. package/dist/controller/fileSchedulerStoreAdapter.js +6 -6
  19. package/dist/controller/globalInputDelivery.js +3 -1
  20. package/dist/controller/jobSupervisor.js +3 -3
  21. package/dist/controller/providerRetryDelivery.js +128 -118
  22. package/dist/controller/runtime.js +1 -1
  23. package/dist/controller/structuredProviderObservation.js +1 -1
  24. package/dist/coordination/workMailbox.js +4 -4
  25. package/dist/core/controllerIdentity.js +25 -0
  26. package/dist/core/controllerProcessIdentity.js +2 -2
  27. package/dist/core/controllerServer.js +4 -2
  28. package/dist/core/protocol.js +1 -1
  29. package/dist/domain/validation.js +6 -0
  30. package/dist/event/taskEvent.js +3 -3
  31. package/dist/execution/workItemExecution.js +2 -2
  32. package/dist/executor/agentExecutor.js +6 -6
  33. package/dist/executor/effectiveLaunch.js +3 -3
  34. package/dist/grant/capabilityGrant.js +2 -2
  35. package/dist/input/inputRequest.js +4 -4
  36. package/dist/integration/changeSet.js +3 -3
  37. package/dist/integration/integrationAttempt.js +3 -3
  38. package/dist/integration/integrationSourceApplication.js +1 -1
  39. package/dist/job/durableJob.js +1 -1
  40. package/dist/job/jobRunner.js +2 -2
  41. package/dist/message/message.js +8 -6
  42. package/dist/milestone/milestone.js +3 -3
  43. package/dist/profile/agentProfile.js +3 -3
  44. package/dist/release/runtimeRelease.js +9 -7
  45. package/dist/repository/project.js +3 -3
  46. package/dist/resources/liveReferences.js +1 -1
  47. package/dist/resources/sqliteResourceRegistry.js +2 -2
  48. package/dist/review/reviewRound.js +5 -5
  49. package/dist/role/role.js +7 -8
  50. package/dist/runtime/acpProtocol.js +2 -3
  51. package/dist/runtime/agentDriverObservation.js +1 -1
  52. package/dist/runtime/agentHost.js +3 -3
  53. package/dist/runtime/agentHostProtocol.js +2 -2
  54. package/dist/runtime/codexInteractiveHost.js +1 -1
  55. package/dist/runtime/launchBroker.js +1 -1
  56. package/dist/runtime/processExitObservation.js +1 -1
  57. package/dist/runtime/providerContinuationReconciliationService.js +84 -75
  58. package/dist/runtime/providerRuntimeIdentity.js +3 -3
  59. package/dist/runtime/runtimeCoherence.js +7 -3
  60. package/dist/runtime/runtimeObservation.js +3 -3
  61. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  62. package/dist/runtime/taskRuntimeIsolation.js +3 -3
  63. package/dist/runtime/tmuxAdapters.js +3 -3
  64. package/dist/scheduler/taskWake.js +1 -1
  65. package/dist/storage/baselineSchema.js +606 -0
  66. package/dist/storage/homeLayout.js +5 -16
  67. package/dist/storage/recordValidation.js +16 -4
  68. package/dist/storage/sqliteSchema.js +106 -1741
  69. package/dist/storage/sqliteStore.js +16 -14
  70. package/dist/storage/storageSchema.js +8 -7
  71. package/dist/storage/storageVersions.js +23 -16
  72. package/dist/storage/taskStore.js +3 -29
  73. package/dist/storage/upgrade/upgradeOrchestrator.js +14 -102
  74. package/dist/task/task.js +15 -7
  75. package/dist/task/taskActivation.js +5 -4
  76. package/dist/telemetry/sqliteTelemetryStore.js +2 -2
  77. package/dist/verification/gateArtifact.js +5 -3
  78. package/dist/verification/verificationPlan.js +6 -7
  79. package/dist/workItem/workItem.js +6 -6
  80. package/dist/workspace/cleanupInspection.js +1 -9
  81. package/dist/worktree/managedWorkspace.js +3 -3
  82. package/docs/managed-turn-and-session-runtime.md +26 -14
  83. package/docs/managed-turn-and-session-runtime.zh-CN.md +18 -10
  84. package/docs/release-workflow.md +52 -300
  85. package/docs/release-workflow.zh-CN.md +41 -234
  86. package/docs/sqlite-control-plane-design.md +48 -289
  87. package/docs/sqlite-control-plane-design.zh-CN.md +37 -53
  88. package/docs/storage-baseline.md +132 -0
  89. package/docs/storage-baseline.zh-CN.md +106 -0
  90. package/docs/task-delivery.md +3 -4
  91. package/docs/task-delivery.zh-CN.md +3 -3
  92. package/docs/testing/verification-levels.md +40 -180
  93. package/docs/testing/verification-levels.zh-CN.md +27 -131
  94. package/i18n/README.zh-CN.md +5 -4
  95. package/package.json +1 -1
  96. package/dist/storage/migrations/agentFailureContext.js +0 -22
  97. package/dist/storage/migrations/agentRunContract.js +0 -159
  98. package/dist/storage/migrations/artifactsToGit.js +0 -338
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +0 -963
  100. package/dist/storage/migrations/currentInputContract.js +0 -86
  101. package/dist/storage/migrations/currentRuntimeContract.js +0 -228
  102. package/dist/storage/migrations/historicalVerificationPlan.js +0 -35
  103. package/dist/storage/migrations/integrationContinuation.js +0 -105
  104. package/dist/storage/migrations/narrowAgentFailureContext.js +0 -65
  105. package/dist/storage/migrations/notificationOnlyWakes.js +0 -74
  106. package/dist/storage/migrations/removeRuntimeGeneration.js +0 -207
  107. package/dist/storage/migrations/submitIntent.js +0 -126
  108. package/dist/storage/migrations/unifyHomeLayout.js +0 -925
  109. package/dist/storage/migrations/verificationPlanV1.js +0 -162
  110. package/dist/storage/migrations/verificationPolicy.js +0 -74
  111. package/dist/storage/migrations/workItemHistory.js +0 -46
@@ -1,1765 +1,130 @@
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 (`home_meta` keeps identity/revision).
12
- *
13
- * Record payloads are stored two ways, per §4: typed columns for fields that are
14
- * queried/filtered/used-for-CAS, and a `payload` JSON column holding the full
15
- * current record. Record-local `schemaVersion` tags remain validation guards;
16
- * they are not independent Home compatibility axes. Any historical payload
17
- * rewrite belongs to the ordered Home migration that introduced the new shape.
18
- *
19
- * The migration runner is append-only and idempotent: it records one ordered
20
- * Home version in `schema_migrations`. Re-running an already-current database
21
- * is a no-op; a crash mid-upgrade rolls the whole migration transaction back.
22
- */
23
1
  import { createHash } from "node:crypto";
24
- import { REMOVE_RUNTIME_GENERATION_SQL, removeRuntimeGenerationRecords } from "./migrations/removeRuntimeGeneration.js";
25
- import { migrateAgentRunContract } from "./migrations/agentRunContract.js";
26
- import { migrateArtifactsToGit } from "./migrations/artifactsToGit.js";
27
- import { migrateIntegrationContinuation } from "./migrations/integrationContinuation.js";
28
- import { migrateNotificationOnlyWakes } from "./migrations/notificationOnlyWakes.js";
29
- import { migrateWorkItemHistory } from "./migrations/workItemHistory.js";
30
- import { migrateVerificationPolicy } from "./migrations/verificationPolicy.js";
31
- import { migrateCurrentInputContract } from "./migrations/currentInputContract.js";
32
- import { CURRENT_RUNTIME_CONTRACT_SQL, migrateCurrentRuntimeContract } from "./migrations/currentRuntimeContract.js";
33
- import { migrateAgentFailureContext } from "./migrations/agentFailureContext.js";
34
- import { migrateNarrowAgentFailureContext } from "./migrations/narrowAgentFailureContext.js";
35
- import { UNIFY_HOME_LAYOUT_SQL, migrateUnifyHomeLayout } from "./migrations/unifyHomeLayout.js";
36
- import { COLLAPSE_WORKTREE_LAYOUT_SQL, migrateCollapseWorktreeLayout } from "./migrations/collapseWorktreeLayout.js";
37
- import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
38
- /** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
2
+ import Database from "better-sqlite3";
3
+ import { BASELINE_SCHEMA_SQL, SQLITE_SCHEMA_TABLES } from "./baselineSchema.js";
4
+ import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION, STORAGE_FORMAT, isMinorStorageUpgrade, isStorageVersion, storageVersionParts } from "./storageVersions.js";
5
+ export { BASELINE_SCHEMA_SQL, SQLITE_SCHEMA_TABLES };
39
6
  export const TELEMETRY_KEEP_PER_RUN = 200;
40
7
  export const TELEMETRY_RUN_CAP = 50_000;
41
- /**
42
- * Version 1 migration: creates every table and index.
43
- *
44
- * `synchronous`/`foreign_keys`/`busy_timeout` are per-connection PRAGMAs set by
45
- * the store; `journal_mode=WAL` is a persistent database property set on open.
46
- * The migration itself only contains schema objects.
47
- *
48
- * This is the Yui 0.15.0 / storage-version-1 baseline. Future releases append
49
- * migrations after it so fresh and upgraded databases converge on the same
50
- * current contract.
51
- */
52
- const BASELINE_CORE_SQL = `
53
- -- Global catalog and coordination (§4.1) -------------------------------------
54
-
55
- CREATE TABLE IF NOT EXISTS home_meta (
56
- id INTEGER PRIMARY KEY CHECK (id = 1),
57
- home_identity TEXT NOT NULL,
58
- revision INTEGER NOT NULL,
59
- created_at TEXT NOT NULL,
60
- updated_at TEXT NOT NULL
61
- );
62
-
63
- CREATE TABLE IF NOT EXISTS config (
64
- id INTEGER PRIMARY KEY CHECK (id = 1),
65
- payload TEXT NOT NULL,
66
- updated_at TEXT NOT NULL
67
- );
68
-
69
- CREATE TABLE IF NOT EXISTS configured_agents (
70
- id TEXT PRIMARY KEY,
71
- payload TEXT NOT NULL,
72
- updated_at TEXT NOT NULL
73
- );
74
-
75
- CREATE TABLE IF NOT EXISTS agent_profiles (
76
- id TEXT PRIMARY KEY,
77
- payload TEXT NOT NULL,
78
- updated_at TEXT NOT NULL
79
- );
80
-
81
- CREATE TABLE IF NOT EXISTS projects (
82
- id TEXT PRIMARY KEY,
83
- name TEXT NOT NULL,
84
- path TEXT NOT NULL,
85
- payload TEXT NOT NULL,
86
- created_at TEXT NOT NULL,
87
- updated_at TEXT NOT NULL
88
- );
89
-
90
- CREATE TABLE IF NOT EXISTS global_roles (
91
- name TEXT PRIMARY KEY,
92
- payload TEXT NOT NULL,
93
- updated_at TEXT NOT NULL
94
- );
95
-
96
- CREATE TABLE IF NOT EXISTS global_role_session_sets (
97
- name TEXT PRIMARY KEY,
98
- payload TEXT NOT NULL,
99
- updated_at TEXT NOT NULL,
100
- FOREIGN KEY (name) REFERENCES global_roles(name)
101
- );
102
-
103
- -- Global record ID high-water marks (§5.3).
104
- CREATE TABLE IF NOT EXISTS global_sequences (
105
- name TEXT PRIMARY KEY,
106
- high_water INTEGER NOT NULL
107
- );
108
-
109
- -- Task catalog: the global active index and lifecycle lookup.
110
- CREATE TABLE IF NOT EXISTS tasks_catalog (
111
- task_id TEXT PRIMARY KEY,
112
- status TEXT NOT NULL,
113
- lifecycle TEXT NOT NULL,
114
- is_active INTEGER NOT NULL CHECK (is_active IN (0,1)),
115
- created_at TEXT NOT NULL,
116
- updated_at TEXT NOT NULL
117
- );
118
- CREATE INDEX IF NOT EXISTS idx_tasks_active ON tasks_catalog(is_active) WHERE is_active = 1;
119
-
120
- -- Session/Workspace ownership is global (workspaces outlive task activity).
121
- CREATE TABLE IF NOT EXISTS managed_workspaces (
122
- owner_kind TEXT NOT NULL CHECK (owner_kind IN
123
- ('task','work-item','review-round','integration-attempt','execution-lane')),
124
- owner_id TEXT NOT NULL,
125
- task_id TEXT,
126
- path TEXT NOT NULL,
127
- payload TEXT NOT NULL,
128
- status TEXT NOT NULL,
129
- created_at TEXT NOT NULL,
130
- updated_at TEXT NOT NULL,
131
- PRIMARY KEY (owner_kind, owner_id)
132
- );
133
- CREATE INDEX IF NOT EXISTS idx_workspaces_task ON managed_workspaces(task_id);
134
-
135
- -- Per-task record ID high-water marks (replaces StoredTask.idHighWaterMarks).
136
- CREATE TABLE IF NOT EXISTS id_sequences (
137
- task_id TEXT NOT NULL,
138
- kind TEXT NOT NULL,
139
- high_water INTEGER NOT NULL,
140
- PRIMARY KEY (task_id, kind)
141
- );
142
-
143
- -- Cross-task coordination: Project locks and the Integration queue.
144
- CREATE TABLE IF NOT EXISTS coordination_locks (
145
- lock_key TEXT PRIMARY KEY,
146
- holder_task TEXT NOT NULL,
147
- holder_ref TEXT NOT NULL,
148
- acquired_at TEXT NOT NULL,
149
- expires_at TEXT
150
- );
151
-
152
- CREATE TABLE IF NOT EXISTS integration_queue (
153
- queue_id TEXT PRIMARY KEY,
154
- task_id TEXT NOT NULL,
155
- project_id TEXT NOT NULL,
156
- change_set TEXT NOT NULL,
157
- status TEXT NOT NULL,
158
- payload TEXT NOT NULL,
159
- created_at TEXT NOT NULL,
160
- updated_at TEXT NOT NULL
161
- );
162
- CREATE INDEX IF NOT EXISTS idx_integration_queue_status ON integration_queue(status, created_at);
163
-
164
- -- Durable outbox (§5.4): UNIQUE(request_id) makes cross-task effects exactly-once.
165
- CREATE TABLE IF NOT EXISTS outbox (
166
- outbox_id INTEGER PRIMARY KEY,
167
- request_id TEXT NOT NULL UNIQUE,
168
- command TEXT NOT NULL,
169
- state TEXT NOT NULL DEFAULT 'pending',
170
- created_at TEXT NOT NULL,
171
- applied_at TEXT
172
- );
173
- CREATE INDEX IF NOT EXISTS idx_outbox_state ON outbox(state, created_at);
174
-
175
- -- Mailboxes (§4.2): per-target ordering and exactly-once signals. -------------
176
-
177
- CREATE TABLE IF NOT EXISTS mailboxes (
178
- mailbox_id INTEGER PRIMARY KEY,
179
- target_kind TEXT NOT NULL CHECK (target_kind IN
180
- ('task','role','role-runtime','global-role-runtime','operator')),
181
- task_id TEXT,
182
- role_name TEXT,
183
- -- The stable mailboxTargetKey string carries the real uniqueness: the column
184
- -- UNIQUE below cannot, because NULL task_id/role_name (the 'operator' and
185
- -- 'global-role-runtime' targets) are distinct under SQL NULL semantics.
186
- target_key TEXT NOT NULL,
187
- next_sequence INTEGER NOT NULL,
188
- processing TEXT,
189
- pending TEXT,
190
- recent_dedupe_keys TEXT NOT NULL DEFAULT '[]',
191
- UNIQUE (target_kind, task_id, role_name)
192
- );
193
- CREATE UNIQUE INDEX IF NOT EXISTS idx_mailboxes_target_key
194
- ON mailboxes(target_key);
195
- CREATE INDEX IF NOT EXISTS idx_mailboxes_ready
196
- ON mailboxes(target_key)
197
- WHERE processing IS NOT NULL OR json_type(pending) <> 'null';
198
-
199
- -- Task-partitioned tables (§4.3). Every task-local read constrains task_id. ----
200
-
201
- CREATE TABLE IF NOT EXISTS task_records (
202
- task_id TEXT PRIMARY KEY,
203
- payload TEXT NOT NULL,
204
- brief TEXT,
205
- updated_at TEXT NOT NULL,
206
- FOREIGN KEY (task_id) REFERENCES tasks_catalog(task_id)
207
- );
208
-
209
- CREATE TABLE IF NOT EXISTS task_roles (
210
- task_id TEXT NOT NULL,
211
- role_name TEXT NOT NULL,
212
- payload TEXT NOT NULL,
213
- updated_at TEXT NOT NULL,
214
- PRIMARY KEY (task_id, role_name)
215
- );
216
-
217
- CREATE TABLE IF NOT EXISTS role_session_sets (
218
- task_id TEXT NOT NULL,
219
- role_name TEXT NOT NULL,
220
- payload TEXT NOT NULL,
221
- updated_at TEXT NOT NULL,
222
- PRIMARY KEY (task_id, role_name)
223
- );
224
-
225
- CREATE TABLE IF NOT EXISTS work_items (
226
- task_id TEXT NOT NULL,
227
- work_item_id TEXT NOT NULL,
228
- status TEXT NOT NULL,
229
- payload TEXT NOT NULL,
230
- updated_at TEXT NOT NULL,
231
- PRIMARY KEY (task_id, work_item_id)
232
- );
233
- CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(task_id, status);
234
-
235
- CREATE TABLE IF NOT EXISTS work_item_candidates (
236
- task_id TEXT NOT NULL,
237
- candidate_id TEXT NOT NULL,
238
- work_item_id TEXT NOT NULL,
239
- payload TEXT NOT NULL,
240
- created_at TEXT NOT NULL,
241
- PRIMARY KEY (task_id, candidate_id)
242
- );
243
-
244
- CREATE TABLE IF NOT EXISTS turns (
245
- task_id TEXT NOT NULL,
246
- turn_id TEXT NOT NULL,
247
- role_name TEXT NOT NULL,
248
- status TEXT NOT NULL,
249
- payload TEXT NOT NULL,
250
- updated_at TEXT NOT NULL,
251
- PRIMARY KEY (task_id, turn_id)
252
- );
253
- CREATE INDEX IF NOT EXISTS idx_turns_role_status ON turns(task_id, role_name, status);
254
-
255
- -- Active-Turn pointers (getActiveTurn / execution-lane Turns).
256
- CREATE TABLE IF NOT EXISTS active_turns (
257
- task_id TEXT NOT NULL,
258
- pointer TEXT NOT NULL,
259
- turn_id TEXT NOT NULL,
260
- payload TEXT NOT NULL,
261
- updated_at TEXT NOT NULL,
262
- PRIMARY KEY (task_id, pointer)
263
- );
264
-
265
- CREATE TABLE IF NOT EXISTS review_rounds (
266
- task_id TEXT NOT NULL,
267
- review_round_id TEXT NOT NULL,
268
- status TEXT NOT NULL,
269
- payload TEXT NOT NULL,
270
- updated_at TEXT NOT NULL,
271
- PRIMARY KEY (task_id, review_round_id)
272
- );
273
-
274
- CREATE TABLE IF NOT EXISTS change_sets (
275
- task_id TEXT NOT NULL,
276
- change_set_id TEXT NOT NULL,
277
- project_id TEXT NOT NULL,
278
- head_sha TEXT NOT NULL,
279
- payload TEXT NOT NULL,
280
- created_at TEXT NOT NULL,
281
- PRIMARY KEY (task_id, change_set_id)
282
- );
283
- CREATE INDEX IF NOT EXISTS idx_change_sets_project ON change_sets(task_id, project_id);
284
-
285
- CREATE TABLE IF NOT EXISTS integration_attempts (
286
- task_id TEXT NOT NULL,
287
- integration_id TEXT NOT NULL,
288
- status TEXT NOT NULL,
289
- payload TEXT NOT NULL,
290
- updated_at TEXT NOT NULL,
291
- PRIMARY KEY (task_id, integration_id)
292
- );
293
-
294
- CREATE TABLE IF NOT EXISTS messages (
295
- task_id TEXT NOT NULL,
296
- message_id TEXT NOT NULL,
297
- seq INTEGER NOT NULL,
298
- payload TEXT NOT NULL,
299
- created_at TEXT NOT NULL,
300
- PRIMARY KEY (task_id, message_id)
301
- );
302
- CREATE INDEX IF NOT EXISTS idx_messages_seq ON messages(task_id, seq);
303
-
304
- CREATE TABLE IF NOT EXISTS input_requests (
305
- task_id TEXT NOT NULL,
306
- input_id TEXT NOT NULL,
307
- status TEXT NOT NULL,
308
- blocks TEXT,
309
- payload TEXT NOT NULL,
310
- created_at TEXT NOT NULL,
311
- updated_at TEXT NOT NULL,
312
- PRIMARY KEY (task_id, input_id)
313
- );
314
- CREATE INDEX IF NOT EXISTS idx_input_open ON input_requests(task_id, status) WHERE status <> 'resolved';
315
- CREATE INDEX IF NOT EXISTS idx_input_requests_open_hot
316
- ON input_requests(task_id, input_id)
317
- WHERE status = 'open';
318
-
319
- CREATE TABLE IF NOT EXISTS decisions (
320
- task_id TEXT NOT NULL,
321
- decision_id TEXT NOT NULL,
322
- payload TEXT NOT NULL,
323
- created_at TEXT NOT NULL,
324
- PRIMARY KEY (task_id, decision_id)
325
- );
326
-
327
- CREATE TABLE IF NOT EXISTS milestones (
328
- task_id TEXT NOT NULL,
329
- milestone_id TEXT NOT NULL,
330
- payload TEXT NOT NULL,
331
- created_at TEXT NOT NULL,
332
- PRIMARY KEY (task_id, milestone_id)
333
- );
334
-
335
- -- Terminal/semantic events: retained individually, never pruned.
336
- CREATE TABLE IF NOT EXISTS events (
337
- task_id TEXT NOT NULL,
338
- event_id TEXT NOT NULL,
339
- type TEXT NOT NULL,
340
- occurred_at TEXT NOT NULL,
341
- payload TEXT NOT NULL,
342
- PRIMARY KEY (task_id, event_id)
343
- );
344
- CREATE INDEX IF NOT EXISTS idx_events_type_time ON events(task_id, type, occurred_at);
345
-
346
- -- Per-task scheduler projections (leaderFailure, operatorNotification).
347
- CREATE TABLE IF NOT EXISTS task_projections (
348
- task_id TEXT NOT NULL,
349
- kind TEXT NOT NULL CHECK (kind IN ('leader-failure','operator-notification')),
350
- payload TEXT,
351
- updated_at TEXT NOT NULL,
352
- PRIMARY KEY (task_id, kind)
353
- );
354
-
355
- -- Telemetry (§4.4): bounded, latest-per-key. WITHOUT ROWID, PK is the key.
356
- CREATE TABLE IF NOT EXISTS telemetry (
357
- task_id TEXT NOT NULL,
358
- role_name TEXT NOT NULL,
359
- turn_id TEXT NOT NULL,
360
- generation TEXT NOT NULL,
361
- progress_id TEXT NOT NULL,
362
- sequence INTEGER,
363
- payload TEXT NOT NULL,
364
- received_at TEXT NOT NULL,
365
- PRIMARY KEY (task_id, role_name, turn_id, generation, progress_id)
366
- ) WITHOUT ROWID;
367
- CREATE INDEX IF NOT EXISTS idx_telemetry_turn ON telemetry(task_id, turn_id);
368
- `;
369
- /** Task-scoped execution and release record families. */
370
- const BASELINE_JOB_AND_RELEASE_SQL = `
371
- CREATE TABLE IF NOT EXISTS durable_jobs (
372
- job_id TEXT NOT NULL,
373
- task_id TEXT NOT NULL,
374
- idempotency_key TEXT,
375
- status TEXT NOT NULL,
376
- payload TEXT NOT NULL,
377
- created_at TEXT NOT NULL,
378
- updated_at TEXT NOT NULL,
379
- PRIMARY KEY (task_id, job_id)
380
- );
381
- CREATE INDEX IF NOT EXISTS idx_durable_jobs_task ON durable_jobs(task_id);
382
- CREATE INDEX IF NOT EXISTS idx_durable_jobs_status ON durable_jobs(status);
383
- CREATE UNIQUE INDEX IF NOT EXISTS idx_durable_jobs_idempotency
384
- ON durable_jobs(task_id, idempotency_key) WHERE idempotency_key IS NOT NULL;
385
- CREATE TABLE IF NOT EXISTS capability_grants (
386
- task_id TEXT NOT NULL,
387
- grant_id TEXT NOT NULL,
388
- payload TEXT NOT NULL,
389
- updated_at TEXT NOT NULL,
390
- PRIMARY KEY (task_id, grant_id)
391
- );
392
-
393
- CREATE TABLE IF NOT EXISTS release_workflows (
394
- task_id TEXT NOT NULL,
395
- workflow_id TEXT NOT NULL,
396
- payload TEXT NOT NULL,
397
- updated_at TEXT NOT NULL,
398
- PRIMARY KEY (task_id, workflow_id)
399
- );
400
- `;
401
- /** Durable caller-key hashes used to verify DurableJob ownership. */
402
- const BASELINE_JOB_CALLER_SQL = `
403
- CREATE TABLE IF NOT EXISTS job_caller_key_hashes (
404
- task_id TEXT NOT NULL,
405
- role_name TEXT NOT NULL,
406
- agent_id TEXT NOT NULL,
407
- hash TEXT NOT NULL,
408
- updated_at TEXT NOT NULL,
409
- PRIMARY KEY (task_id, role_name, agent_id)
410
- );
411
- `;
412
- /**
413
- * The `telemetry` table stores the bounded latest-per-key progress window; this companion
414
- * table holds the authoritative per-Turn/generation summary (count, first/last,
415
- * max sequence, error count) so aggregates stay accurate after the window is
416
- * pruned. Triggers maintain it on telemetry INSERT/UPDATE; DELETE intentionally
417
- * leaves it untouched because pruned rows were still observed.
418
- */
419
- const BASELINE_TELEMETRY_AGGREGATE_SQL = `
420
- CREATE TABLE IF NOT EXISTS telemetry_aggregate (
421
- task_id TEXT NOT NULL,
422
- role_name TEXT NOT NULL,
423
- turn_id TEXT NOT NULL,
424
- generation TEXT NOT NULL,
425
- first_at TEXT NOT NULL,
426
- last_at TEXT NOT NULL,
427
- count INTEGER NOT NULL,
428
- max_sequence INTEGER,
429
- error_count INTEGER NOT NULL DEFAULT 0,
430
- updated_at TEXT NOT NULL,
431
- PRIMARY KEY (task_id, role_name, turn_id, generation)
432
- ) WITHOUT ROWID;
433
-
434
- CREATE TRIGGER IF NOT EXISTS telemetry_ai AFTER INSERT ON telemetry
435
- BEGIN
436
- INSERT INTO telemetry_aggregate
437
- (task_id, role_name, turn_id, generation, first_at, last_at, count, max_sequence, error_count, updated_at)
438
- VALUES
439
- (NEW.task_id, NEW.role_name, NEW.turn_id, NEW.generation, NEW.received_at, NEW.received_at, 1, NEW.sequence,
440
- CASE WHEN json_valid(NEW.payload)
441
- AND (COALESCE(json_extract(NEW.payload, '$.error'), '') <> ''
442
- OR COALESCE(json_extract(NEW.payload, '$.errorKind'), '') <> '')
443
- THEN 1 ELSE 0 END,
444
- NEW.received_at)
445
- ON CONFLICT(task_id, role_name, turn_id, generation) DO UPDATE SET
446
- first_at = MIN(telemetry_aggregate.first_at, excluded.first_at),
447
- last_at = MAX(telemetry_aggregate.last_at, excluded.last_at),
448
- count = telemetry_aggregate.count + 1,
449
- max_sequence = CASE
450
- WHEN excluded.max_sequence IS NOT NULL
451
- AND (telemetry_aggregate.max_sequence IS NULL OR excluded.max_sequence > telemetry_aggregate.max_sequence)
452
- THEN excluded.max_sequence ELSE telemetry_aggregate.max_sequence END,
453
- error_count = telemetry_aggregate.error_count + excluded.error_count,
454
- updated_at = excluded.updated_at;
455
- END;
456
-
457
- CREATE TRIGGER IF NOT EXISTS telemetry_au AFTER UPDATE ON telemetry
458
- BEGIN
459
- UPDATE telemetry_aggregate SET
460
- last_at = CASE WHEN NEW.received_at > last_at THEN NEW.received_at ELSE last_at END,
461
- max_sequence = CASE
462
- WHEN NEW.sequence IS NOT NULL AND (max_sequence IS NULL OR NEW.sequence > max_sequence)
463
- THEN NEW.sequence ELSE max_sequence END,
464
- updated_at = NEW.received_at
465
- WHERE task_id = NEW.task_id AND role_name = NEW.role_name
466
- AND turn_id = NEW.turn_id AND generation = NEW.generation;
467
- END;
468
- `;
469
- /**
470
- * Session owner physical identity records.
471
- *
472
- * One row per runtime generation, keyed by runtime generation id. The payload column
473
- * stores the full current JSON record; typed columns support the
474
- * reconciliation queries (task/role lookup, PID liveness).
475
- */
476
- const BASELINE_SESSION_OWNER_SQL = `
477
- CREATE TABLE IF NOT EXISTS session_owners (
478
- launch_id TEXT PRIMARY KEY,
479
- scope TEXT NOT NULL CHECK (scope IN ('task','global')),
480
- task_id TEXT,
481
- role_name TEXT NOT NULL,
482
- agent_id TEXT NOT NULL,
483
- native_session_id TEXT,
484
- provider_root_pid INTEGER,
485
- payload TEXT NOT NULL,
486
- recorded_at TEXT NOT NULL
487
- );
488
- CREATE INDEX IF NOT EXISTS idx_session_owners_task
489
- ON session_owners(task_id, role_name);
490
- `;
491
- /**
492
- * Resource GC registry.
493
- *
494
- * GC-owned table for resource lifecycle records. The registry is GC's own
495
- * state. Records are stored as full current JSON in `payload`, with
496
- * typed columns for the fields GC queries (disposition, kind, task_id).
497
- */
498
- const BASELINE_RESOURCE_REGISTRY_SQL = `
499
- CREATE TABLE IF NOT EXISTS resource_registry (
500
- id TEXT PRIMARY KEY,
501
- kind TEXT NOT NULL,
502
- path TEXT NOT NULL,
503
- disposition TEXT NOT NULL,
504
- task_id TEXT,
505
- payload TEXT NOT NULL,
506
- created_at TEXT NOT NULL,
507
- updated_at TEXT NOT NULL
508
- );
509
- CREATE INDEX IF NOT EXISTS idx_resource_registry_disposition
510
- ON resource_registry(disposition);
511
- CREATE INDEX IF NOT EXISTS idx_resource_registry_task
512
- ON resource_registry(task_id);
513
- `;
514
- /**
515
- * Content-addressed GateArtifact storage. Content-addressed gate
516
- * evidence records with per-step logs stored as BLOBs. The artifact key is
517
- * the SHA-256 of the identity tuple (Project + commit + plan digest +
518
- * toolchain digest + L2 boundary), so the same tuple always maps to one row.
519
- * Typed columns support the reuse lookup paths (exact-commit L2 search,
520
- * Project-level prune) without scanning payloads.
521
- */
522
- const BASELINE_GATE_ARTIFACT_SQL = `
523
- CREATE TABLE IF NOT EXISTS gate_artifacts (
524
- key TEXT PRIMARY KEY,
525
- project_id TEXT NOT NULL,
526
- level TEXT NOT NULL CHECK (level IN ('L1','L2')),
527
- commit_sha TEXT NOT NULL,
528
- plan_digest TEXT NOT NULL,
529
- toolchain_digest TEXT NOT NULL,
530
- target_ref TEXT,
531
- status TEXT NOT NULL CHECK (status IN ('incomplete','complete')),
532
- outcome TEXT NOT NULL CHECK (outcome IN ('unknown','succeeded','failed')),
533
- payload TEXT NOT NULL,
534
- created_at TEXT NOT NULL,
535
- completed_at TEXT,
536
- last_used_at TEXT NOT NULL
537
- );
538
-
539
- CREATE INDEX IF NOT EXISTS idx_gate_artifacts_project_commit
540
- ON gate_artifacts(project_id, commit_sha);
541
-
542
- CREATE INDEX IF NOT EXISTS idx_gate_artifacts_project_last_used
543
- ON gate_artifacts(project_id, last_used_at);
544
-
545
- CREATE TABLE IF NOT EXISTS gate_artifact_logs (
546
- artifact_key TEXT NOT NULL,
547
- step_name TEXT NOT NULL,
548
- log_content BLOB NOT NULL,
549
- log_digest TEXT NOT NULL,
550
- log_bytes INTEGER NOT NULL,
551
- PRIMARY KEY (artifact_key, step_name),
552
- FOREIGN KEY (artifact_key) REFERENCES gate_artifacts(key) ON DELETE CASCADE
553
- );
554
- `;
555
- /** Bounded current-Session projection for runtime cleanup. */
556
- const BASELINE_RUNTIME_SESSION_SQL = `
557
- CREATE TABLE IF NOT EXISTS runtime_session_candidates (
558
- scope TEXT NOT NULL CHECK (scope IN ('task','global')),
559
- task_id TEXT NOT NULL,
560
- role_name TEXT NOT NULL,
561
- agent_id TEXT NOT NULL,
562
- adapter_id TEXT NOT NULL,
563
- native_session_id TEXT NOT NULL,
564
- launch_id TEXT,
565
- session_updated_at TEXT NOT NULL,
566
- cleanup_required INTEGER NOT NULL CHECK (cleanup_required IN (0,1)),
567
- PRIMARY KEY (scope, task_id, role_name),
568
- CHECK (
569
- (scope = 'task' AND length(task_id) > 0)
570
- OR (scope = 'global' AND task_id = '')
571
- ),
572
- CHECK (
573
- cleanup_required = CASE WHEN launch_id IS NOT NULL THEN 1 ELSE 0 END
574
- )
575
- );
576
-
577
- CREATE INDEX IF NOT EXISTS idx_runtime_session_cleanup_required
578
- ON runtime_session_candidates(scope, task_id, role_name)
579
- WHERE cleanup_required = 1;
580
- `;
581
- /**
582
- * External publication evidence. Records are immutable;
583
- * a corrected MR/PR state appends a superseding record with the same
584
- * external_key, so only the unsuperseded root is globally unique.
585
- */
586
- const BASELINE_PUBLICATION_REFERENCE_SQL = `
587
- CREATE TABLE IF NOT EXISTS publication_references (
588
- task_id TEXT NOT NULL,
589
- publication_id TEXT NOT NULL,
590
- project_id TEXT NOT NULL,
591
- provider TEXT NOT NULL,
592
- repository TEXT NOT NULL,
593
- external_kind TEXT NOT NULL,
594
- external_id TEXT NOT NULL,
595
- external_key TEXT NOT NULL,
596
- state TEXT NOT NULL,
597
- verification TEXT NOT NULL,
598
- external_url TEXT,
599
- title TEXT,
600
- source_branch TEXT,
601
- target_branch TEXT,
602
- local_commit TEXT,
603
- remote_commit TEXT,
604
- supersedes TEXT,
605
- payload TEXT NOT NULL,
606
- merged_at TEXT,
607
- created_at TEXT NOT NULL,
608
- PRIMARY KEY (task_id, publication_id)
609
- );
610
- CREATE INDEX IF NOT EXISTS idx_publication_references_task
611
- ON publication_references(task_id);
612
- CREATE INDEX IF NOT EXISTS idx_publication_references_external
613
- ON publication_references(external_key);
614
- CREATE UNIQUE INDEX IF NOT EXISTS idx_publication_references_external_root
615
- ON publication_references(external_key) WHERE supersedes IS NULL;
616
- `;
617
- const BASELINE_TASK_WAKE_SQL = `
618
- -- Durable Leader wake ledger (Issue 04 long-term design). A wake is a
619
- -- notification envelope, not a context dump: the record holds the aggregated
620
- -- reason tags and the delta window; the Agent reads delta content on demand.
621
- CREATE TABLE IF NOT EXISTS task_wakes (
622
- task_id TEXT NOT NULL,
623
- wake_id TEXT NOT NULL,
624
- seq INTEGER NOT NULL,
625
- status TEXT NOT NULL CHECK (status IN ('dispatched','consumed')),
626
- turn_id TEXT,
627
- from_cursor TEXT NOT NULL,
628
- to_cursor TEXT NOT NULL,
629
- reasons TEXT NOT NULL,
630
- payload TEXT NOT NULL,
631
- created_at TEXT NOT NULL,
632
- consumed_at TEXT,
633
- PRIMARY KEY (task_id, wake_id)
634
- );
635
- CREATE INDEX IF NOT EXISTS idx_task_wakes_seq ON task_wakes(task_id, seq);
636
- `;
637
- /** Immutable, Task-scoped ContextSnapshot records. */
638
- const BASELINE_CONTEXT_SNAPSHOT_SQL = `
639
- CREATE TABLE IF NOT EXISTS context_snapshots (
640
- task_id TEXT NOT NULL,
641
- snapshot_id TEXT NOT NULL,
642
- scope TEXT NOT NULL CHECK (scope IN ('task','workitem','stage')),
643
- scope_ref TEXT,
644
- sequence INTEGER NOT NULL CHECK (sequence > 0),
645
- digest TEXT NOT NULL CHECK (length(digest) = 64),
646
- payload TEXT NOT NULL,
647
- frozen_at TEXT NOT NULL,
648
- PRIMARY KEY (task_id, snapshot_id),
649
- FOREIGN KEY (task_id) REFERENCES tasks_catalog(task_id) ON DELETE CASCADE,
650
- CHECK ((scope = 'task' AND scope_ref IS NULL) OR (scope <> 'task' AND scope_ref IS NOT NULL))
651
- );
652
-
653
- CREATE UNIQUE INDEX IF NOT EXISTS idx_context_snapshots_scope_sequence
654
- ON context_snapshots(task_id, scope, COALESCE(scope_ref, ''), sequence);
655
- `;
656
- /**
657
- * The Global-owned Message store (decision-3 §9/§11). It extends the Message
658
- * store to a Global owner exactly as `global_role_session_sets` parallels
659
- * `role_session_sets`: keyed by the Global Role `name` with an FK to
660
- * `global_roles`, never by a fabricated `task_id`, and never a new private
661
- * queue. Ids are minted from the existing `global_sequences` counter, so a
662
- * Global input has an explicit owner and an authorizable reference without
663
- * reusing Task record shapes or Task permissions.
664
- */
665
- const GLOBAL_ROLE_MESSAGE_SQL = `
666
- CREATE TABLE IF NOT EXISTS global_role_messages (
667
- name TEXT NOT NULL,
668
- message_id TEXT NOT NULL,
669
- seq INTEGER NOT NULL,
670
- payload TEXT NOT NULL,
671
- created_at TEXT NOT NULL,
672
- PRIMARY KEY (name, message_id),
673
- FOREIGN KEY (name) REFERENCES global_roles(name)
674
- );
675
- CREATE INDEX IF NOT EXISTS idx_global_role_messages_seq ON global_role_messages(name, seq);
676
- `;
677
- const MIGRATION_1_SQL = [
678
- BASELINE_CORE_SQL,
679
- BASELINE_JOB_AND_RELEASE_SQL,
680
- BASELINE_JOB_CALLER_SQL,
681
- BASELINE_TELEMETRY_AGGREGATE_SQL,
682
- BASELINE_SESSION_OWNER_SQL,
683
- BASELINE_RESOURCE_REGISTRY_SQL,
684
- BASELINE_GATE_ARTIFACT_SQL,
685
- BASELINE_RUNTIME_SESSION_SQL,
686
- BASELINE_PUBLICATION_REFERENCE_SQL,
687
- BASELINE_TASK_WAKE_SQL,
688
- BASELINE_CONTEXT_SNAPSHOT_SQL
689
- ].join("\n");
690
- /** Released migrations are append-only and must never be rewritten. */
691
- const MIGRATIONS = Object.freeze([
692
- {
693
- version: 1,
694
- name: "v0.15.0-baseline",
695
- introducedIn: "0.15.0",
696
- sql: MIGRATION_1_SQL
697
- },
698
- {
699
- version: 2,
700
- name: "job-operation-facts",
701
- introducedIn: "0.15.2",
702
- // Historical records never carried caller identity or external effect
703
- // evidence. Preserve that uncertainty rather than inventing attribution.
704
- // No old executable implementation or runtime dual-read is needed.
705
- sql: `
706
- UPDATE durable_jobs SET payload = json_set(payload,
707
- '$.schemaVersion', 2,
708
- '$.operation', json_object(
709
- 'requestId', json_extract(payload, '$.idempotencyKey'),
710
- 'inputDigest', json_extract(payload, '$.idempotencyKey'),
711
- 'actorId', 'historical:unrecorded',
712
- 'authorityRef', 'historical:unrecorded',
713
- 'targetId', json_extract(payload, '$.workspace'),
714
- 'capability', 'job.start',
715
- 'implementation', json_object('id', 'yui:job-runner', 'generation', '1'),
716
- 'effect', 'possible',
717
- 'receiptRefs', json('[]'),
718
- 'partialResultRefs', json('[]')
719
- )
720
- );
721
- CREATE UNIQUE INDEX idx_durable_jobs_request
722
- ON durable_jobs(task_id, json_extract(payload, '$.operation.actorId'),
723
- json_extract(payload, '$.operation.requestId'));
724
- `
725
- },
726
- {
727
- version: 3,
728
- name: "exact-attempt-result-identity",
729
- introducedIn: "0.15.5",
730
- // Append after the released T01 migration without changing its checksum.
731
- // Accepted inputs/results can use an exact attempt without a native Turn id.
732
- // Preserve all valid historical records; never repair failed Turns or logs.
733
- sql: "SELECT 1; -- exact attempt identity without a fabricated native Turn id"
734
- },
735
- {
736
- version: 4,
737
- name: "session-and-process-identity",
738
- introducedIn: "0.15.8",
739
- sql: REMOVE_RUNTIME_GENERATION_SQL,
740
- migrateData: removeRuntimeGenerationRecords
741
- },
742
- {
743
- version: 5,
744
- name: "project-resource-artifacts",
745
- introducedIn: "0.15.8",
746
- sql: `
747
- CREATE TABLE artifacts (
748
- task_id TEXT NOT NULL REFERENCES tasks_catalog(task_id),
749
- id TEXT NOT NULL,
750
- payload TEXT NOT NULL,
751
- PRIMARY KEY (task_id, id)
752
- );
753
- CREATE TABLE local_resources (
754
- id TEXT PRIMARY KEY,
755
- canonical_identity TEXT NOT NULL UNIQUE,
756
- payload TEXT NOT NULL
757
- );
758
- CREATE TABLE environment_preparations (
759
- task_id TEXT NOT NULL REFERENCES tasks_catalog(task_id),
760
- id TEXT NOT NULL,
761
- payload TEXT NOT NULL,
762
- PRIMARY KEY (task_id, id)
763
- );
764
- UPDATE projects SET payload = json_set(payload,
765
- '$.schemaVersion', 6, '$.resourceRefs', json('[]'),
766
- '$.defaultCapabilityProviders', json('{}'));
767
- `
768
- },
769
- {
770
- version: 6,
771
- name: "session-endpoint-implementation",
772
- introducedIn: "0.15.8",
773
- // Valid earlier Sessions used these two built-in protocols. Generation 1
774
- // retains those codecs and identities behind the new execution boundary.
775
- // Preserve native IDs, effective snapshots and all historical results.
776
- sql: `
777
- UPDATE role_session_sets SET payload = json_set(payload, '$.sessions', json((
778
- SELECT json_group_object(key, json_set(value, '$.schemaVersion', 6,
779
- '$.endpointImplementation', json_object(
780
- 'id', 'yui.agent-endpoint.' || json_extract(value, '$.adapterId'), 'generation', '1')))
781
- FROM json_each(payload, '$.sessions')
782
- )));
783
- UPDATE role_session_sets SET payload = json_set(payload, '$.history', json((
784
- SELECT json_group_array(json_set(value, '$.schemaVersion', 6,
785
- '$.endpointImplementation', json_object(
786
- 'id', 'yui.agent-endpoint.' || json_extract(value, '$.adapterId'), 'generation', '1')))
787
- FROM json_each(payload, '$.history')
788
- ))) WHERE json_type(payload, '$.history') = 'array';
789
- UPDATE global_role_session_sets SET payload = json_set(payload, '$.sessions', json((
790
- SELECT json_group_object(key, json_set(value, '$.schemaVersion', 6,
791
- '$.endpointImplementation', json_object(
792
- 'id', 'yui.agent-endpoint.' || json_extract(value, '$.adapterId'), 'generation', '1')))
793
- FROM json_each(payload, '$.sessions')
794
- )));
795
- UPDATE global_role_session_sets SET payload = json_set(payload, '$.history', json((
796
- SELECT json_group_object(key, json_set(value, '$.schemaVersion', 6,
797
- '$.endpointImplementation', json_object(
798
- 'id', 'yui.agent-endpoint.' || json_extract(value, '$.adapterId'), 'generation', '1')))
799
- FROM json_each(payload, '$.history')
800
- ))) WHERE json_type(payload, '$.history') = 'object';
801
- `
802
- },
803
- {
804
- version: 7,
805
- name: "task-facts-and-explicit-acceptance",
806
- introducedIn: "0.15.8",
807
- // Retain the complete retirement metadata/events and diagnostic facts.
808
- // Only the public lifecycle changes; ordinary stores read one shape.
809
- sql: `
810
- UPDATE task_records SET payload = json_set(payload, '$.status', 'cancelled')
811
- WHERE json_extract(payload, '$.status') = 'retired';
812
- UPDATE task_records SET payload = json_set(payload, '$.retirementIsolation', json('true'))
813
- WHERE json_extract(payload, '$.retiredAt') IS NOT NULL;
814
- UPDATE tasks_catalog SET status = 'cancelled' WHERE status = 'retired';
815
- UPDATE tasks_catalog SET lifecycle = 'cancelled' WHERE lifecycle = 'retired';
816
- UPDATE task_records SET brief = json_set(brief, '$.revision', 1) WHERE brief IS NOT NULL;
817
- UPDATE work_items SET payload = json_set(payload,
818
- '$.historicalState', json_patch(json('{}'), json_object(
819
- 'status', json_extract(payload, '$.status'),
820
- 'outcome', json_extract(payload, '$.outcome'),
821
- 'endedAt', json_extract(payload, '$.endedAt'))),
822
- '$.status', CASE status WHEN 'completed' THEN 'accepted' WHEN 'retired' THEN 'retired' ELSE 'open' END);
823
- UPDATE work_items SET payload = json_remove(payload, '$.outcome', '$.endedAt')
824
- WHERE json_extract(payload, '$.status') = 'open';
825
- UPDATE work_items SET status = json_extract(payload, '$.status');
826
- UPDATE work_items SET payload = json_set(payload, '$.currentCandidateId',
827
- json_extract(payload, '$.candidates[#-1].id'))
828
- WHERE json_extract(payload, '$.historicalState.status') = 'awaiting_acceptance';
829
- UPDATE work_items SET payload = json_set(payload, '$.acceptedCandidateId',
830
- json_extract(payload, '$.candidates[#-1].id'))
831
- WHERE status = 'accepted' AND json_array_length(payload, '$.candidates') > 0;
832
- `
833
- },
834
- {
835
- version: 8,
836
- name: "event-owned-edit-history",
837
- introducedIn: "0.15.8",
838
- // Move embedded histories to one immutable import event per Task.
839
- // Existing events stay byte-identical; imports supply otherwise missing
840
- // historical fields and are evidence only, never executable lifecycle input.
841
- // Candidates may now carry optional fixed Artifact refs; absent refs remain
842
- // valid for historical Candidates, so this payload addition needs no backfill.
843
- sql: `
844
- CREATE TEMP TABLE migrated_edit_history AS
845
- SELECT records.task_id,
846
- COALESCE(sequences.high_water, 0) + 1 AS sequence,
847
- strftime('%Y-%m-%dT%H:%M:%fZ', 'now') AS imported_at,
848
- json_object(
849
- 'sourceStorageVersion', '7',
850
- 'taskOutcomes', '' || COALESCE(json_extract(records.payload, '$.outcomeHistory'), '[]'),
851
- 'workAcceptances', '' || COALESCE((
852
- SELECT json_group_array(json_object(
853
- 'workItemId', work_item_id,
854
- 'history', json_extract(payload, '$.acceptanceHistory')))
855
- FROM (SELECT * FROM work_items
856
- WHERE task_id = records.task_id
857
- AND json_array_length(payload, '$.acceptanceHistory') > 0
858
- ORDER BY work_item_id)
859
- ), '[]')
860
- ) AS history
861
- FROM task_records AS records
862
- LEFT JOIN id_sequences AS sequences
863
- ON sequences.task_id = records.task_id AND sequences.kind = 'event'
864
- WHERE json_array_length(records.payload, '$.outcomeHistory') > 0
865
- OR EXISTS (SELECT 1 FROM work_items
866
- WHERE task_id = records.task_id AND json_array_length(payload, '$.acceptanceHistory') > 0);
867
-
868
- INSERT INTO events (task_id, event_id, type, occurred_at, payload)
869
- SELECT task_id, 'event-' || sequence, 'history.imported', imported_at,
870
- json_object('schemaVersion', 2, 'id', 'event-' || sequence,
871
- 'taskId', task_id, 'type', 'history.imported',
872
- 'createdAt', imported_at, 'payload', json(history))
873
- FROM migrated_edit_history;
874
- INSERT INTO id_sequences (task_id, kind, high_water)
875
- SELECT task_id, 'event', sequence FROM migrated_edit_history WHERE true
876
- ON CONFLICT(task_id, kind) DO UPDATE SET high_water = excluded.high_water;
877
- DROP TABLE migrated_edit_history;
878
-
879
- UPDATE task_records SET payload = json_remove(payload, '$.outcomeHistory'),
880
- brief = CASE WHEN brief IS NULL THEN NULL ELSE json_remove(brief, '$.revision') END;
881
- UPDATE work_items SET payload = json_remove(payload, '$.acceptanceHistory');
882
- `
883
- },
884
- {
885
- version: 9,
886
- name: "adopted-agent-execution-environment",
887
- introducedIn: "0.15.8",
888
- // Optional Role selection and immutable Session/Turn effective snapshot.
889
- // Existing records deliberately keep their managed-workspace execution;
890
- // adopted preparations must never be inferred as an automatic binding.
891
- sql: "SELECT 1; -- explicit adopted execution environment; absent means managed workspace"
892
- },
893
- {
894
- version: 10,
895
- name: "plugin-validation-evidence",
896
- introducedIn: "0.15.8",
897
- sql: `
898
- CREATE TABLE plugin_validations (
899
- task_id TEXT NOT NULL REFERENCES tasks_catalog(task_id),
900
- id TEXT NOT NULL,
901
- payload TEXT NOT NULL,
902
- PRIMARY KEY (task_id, id)
903
- );
904
- `
905
- },
906
- {
907
- version: 11,
908
- name: "plugin-enable-intent",
909
- introducedIn: "0.15.8",
910
- // A v10 validation proves no enable/disable choice. Preserve it unchanged;
911
- // never infer desired configuration from historical reports or processes.
912
- sql: `
913
- CREATE TABLE plugin_intents (
914
- task_id TEXT NOT NULL REFERENCES tasks_catalog(task_id),
915
- plugin_id TEXT NOT NULL,
916
- payload TEXT NOT NULL,
917
- PRIMARY KEY (task_id, plugin_id)
918
- );
919
- `
920
- },
921
- {
922
- // Renumbered from 10 to 12 when this branch adopted upstream master: PR320
923
- // took 10 and 11 while this work was in review. Both are already merged, so
924
- // they own those numbers; this migration appends after them rather than
925
- // competing for a number, and no released entry is touched.
926
- version: 12,
927
- name: "draft-planning-and-deferred-activation",
928
- // Same unreleased release as migrations 4-11: this does not bump the version.
929
- introducedIn: "0.15.8",
930
- // One current-version migration for everything this change adds. No existing
931
- // record is rewritten and no payload is backfilled:
932
- //
933
- // - the optional Task activation request: absent means activation was
934
- // never explicitly requested, so a Draft is never inferred to have
935
- // requested it;
936
- // - its bounded settled-request history: absent means no request ever
937
- // reached a terminal disposition on this Task. This is a display
938
- // projection, not the authority: whether an explicitly cancelled or
939
- // adopted requestId may be replayed is decided from the durable
940
- // activation event ledger (never compacted), so eviction from this
941
- // bounded list never resurrects a decided outcome — per-request state
942
- // inside the Task that owns it, not a second scheduling or operation
943
- // ledger;
944
- // - the planning Turn purpose: historical Turns keep their execution or
945
- // review purpose untouched.
946
- //
947
- // The partial indexes bound Draft planning selection and pending-activation
948
- // recovery to the few rows that qualify, so no execution phase scans Task
949
- // history to find them. Creating an index adds no row and rewrites no
950
- // payload.
951
- sql: `
952
- -- explicit activation request, its bounded settled history, and planning Turn
953
- -- purpose; absent means activation was never requested
954
- CREATE INDEX IF NOT EXISTS idx_turns_planning_active ON turns(task_id, turn_id)
955
- WHERE status = 'active' AND json_extract(payload, '$.purpose') = 'planning';
956
- CREATE INDEX IF NOT EXISTS idx_tasks_activation_pending ON task_records(task_id)
957
- WHERE json_extract(payload, '$.activationRequest.disposition') = 'pending';
958
- `
959
- },
960
- {
961
- version: 13,
962
- name: "session-authority-and-execution-admission",
963
- introducedIn: "0.15.9",
964
- sql: "SELECT 1; -- AgentRun contract, notification admission and Session authority",
965
- migrateData: migrateAgentRunContract
966
- },
967
- {
968
- version: 14,
969
- name: "acp-session-workspace-configuration",
970
- introducedIn: "0.15.10",
971
- // ACP is a new legal adapter/configuration value, including its optional
972
- // additional workspace roots. The v13 baseline has no ACP bindings; existing
973
- // Codex/Claude configuration and Session history remain valid unchanged.
974
- // This widens the persistent contract without rewriting payloads or earlier
975
- // migration checksums. Workspace delivery is negotiated at initialize, not
976
- // stored as another configuration authority.
977
- sql: "SELECT 1; -- ACP bindings may carry additionalDirectories; history stays valid"
978
- },
979
- {
980
- version: 15,
981
- name: "agent-execution-component",
982
- introducedIn: "0.15.10",
983
- // Which product executes is now its own recorded fact, separate from the
984
- // connection plan that reaches it. The plan keeps its `adapterId` name and
985
- // its meaning; only the product identity is new.
986
- //
987
- // The backfill is a total function of the stored plan, never of a command
988
- // string. `codex` and `claude` each have exactly one component, so those
989
- // rows gain their true value. Every `acp` row becomes `unknown-acp-agent`:
990
- // a v14 Home cannot say which product answered, and an executable named
991
- // `claude-agent-acp` is not evidence that it was the Claude Agent SDK. The
992
- // unidentified value is the honest one and stays correctable by hand.
993
- //
994
- // Sessions keep their own recorded component, so a Session started before
995
- // this migration continues against exactly the implementation it began on.
996
- //
997
- // Every persisted effective snapshot must be reached, not only the ones on
998
- // Session sets: `validateEffectiveLaunchSnapshot` demands schemaVersion 4,
999
- // and the upgrade verifier replays it over Runs, WorkItem ExecutionLanes
1000
- // and ReviewRound ExecutionLanes too. A Home holding any of those would
1001
- // otherwise fail the upgrade and roll back. The list below is the
1002
- // verifier's own reachable set, not a scan for fields that look similar.
1003
- sql: `
1004
- UPDATE configured_agents SET payload = json_set(payload,
1005
- '$.schemaVersion', 3,
1006
- '$.component', CASE json_extract(payload, '$.adapterId')
1007
- WHEN 'codex' THEN 'codex-cli'
1008
- WHEN 'claude' THEN 'claude-code-cli'
1009
- ELSE 'unknown-acp-agent' END)
1010
- WHERE json_extract(payload, '$.component') IS NULL;
1011
-
1012
- UPDATE global_roles SET payload = json_set(payload, '$.agentBindings', json((
1013
- SELECT json_group_object(key, json_set(value, '$.component',
1014
- CASE json_extract(value, '$.adapterId')
1015
- WHEN 'codex' THEN 'codex-cli'
1016
- WHEN 'claude' THEN 'claude-code-cli'
1017
- ELSE 'unknown-acp-agent' END))
1018
- FROM json_each(payload, '$.agentBindings')
1019
- ))) WHERE json_type(payload, '$.agentBindings') = 'object';
1020
-
1021
- UPDATE task_roles SET payload = json_set(payload, '$.agentBindings', json((
1022
- SELECT json_group_object(key, json_set(value, '$.component',
1023
- CASE json_extract(value, '$.adapterId')
1024
- WHEN 'codex' THEN 'codex-cli'
1025
- WHEN 'claude' THEN 'claude-code-cli'
1026
- ELSE 'unknown-acp-agent' END))
1027
- FROM json_each(payload, '$.agentBindings')
1028
- ))) WHERE json_type(payload, '$.agentBindings') = 'object';
1029
-
1030
- UPDATE global_role_session_sets SET payload = json_set(payload, '$.sessions', json((
1031
- SELECT json_group_object(key, json_set(value, '$.effective.schemaVersion', 4,
1032
- '$.effective.component', CASE json_extract(value, '$.effective.adapterId')
1033
- WHEN 'codex' THEN 'codex-cli'
1034
- WHEN 'claude' THEN 'claude-code-cli'
1035
- ELSE 'unknown-acp-agent' END))
1036
- FROM json_each(payload, '$.sessions')
1037
- ))) WHERE json_type(payload, '$.sessions') = 'object';
1038
- UPDATE global_role_session_sets SET payload = json_set(payload, '$.history', json((
1039
- SELECT json_group_object(key, json_set(value, '$.effective.schemaVersion', 4,
1040
- '$.effective.component', CASE json_extract(value, '$.effective.adapterId')
1041
- WHEN 'codex' THEN 'codex-cli'
1042
- WHEN 'claude' THEN 'claude-code-cli'
1043
- ELSE 'unknown-acp-agent' END))
1044
- FROM json_each(payload, '$.history')
1045
- ))) WHERE json_type(payload, '$.history') = 'object';
1046
-
1047
- UPDATE role_session_sets SET payload = json_set(payload, '$.sessions', json((
1048
- SELECT json_group_object(key, json_set(value, '$.effective.schemaVersion', 4,
1049
- '$.effective.component', CASE json_extract(value, '$.effective.adapterId')
1050
- WHEN 'codex' THEN 'codex-cli'
1051
- WHEN 'claude' THEN 'claude-code-cli'
1052
- ELSE 'unknown-acp-agent' END))
1053
- FROM json_each(payload, '$.sessions')
1054
- ))) WHERE json_type(payload, '$.sessions') = 'object';
1055
- UPDATE role_session_sets SET payload = json_set(payload, '$.history', json((
1056
- SELECT json_group_array(json_set(value, '$.effective.schemaVersion', 4,
1057
- '$.effective.component', CASE json_extract(value, '$.effective.adapterId')
1058
- WHEN 'codex' THEN 'codex-cli'
1059
- WHEN 'claude' THEN 'claude-code-cli'
1060
- ELSE 'unknown-acp-agent' END))
1061
- FROM json_each(payload, '$.history')
1062
- ))) WHERE json_type(payload, '$.history') = 'array';
1063
-
1064
- -- A Turn records the launch it actually ran on, one snapshot per row.
1065
- UPDATE turns SET payload = json_set(payload,
1066
- '$.effective.schemaVersion', 4,
1067
- '$.effective.component', CASE json_extract(payload, '$.effective.adapterId')
1068
- WHEN 'codex' THEN 'codex-cli'
1069
- WHEN 'claude' THEN 'claude-code-cli'
1070
- ELSE 'unknown-acp-agent' END)
1071
- WHERE json_type(payload, '$.effective') = 'object';
1072
-
1073
- -- ExecutionLane launch facts are frozen per Lane, inside an array of Groups
1074
- -- that each hold an array of Lanes. A Lane that was never dispatched has no
1075
- -- effective at all, and must keep that absence: json_set would otherwise
1076
- -- create a partial snapshot the Lane validator rejects as incomplete.
1077
- UPDATE work_items SET payload = json_set(payload, '$.executionGroups', json((
1078
- SELECT json_group_array(json_set(grp.value, '$.lanes', json((
1079
- SELECT json_group_array(CASE
1080
- WHEN json_type(lane.value, '$.effective') = 'object'
1081
- THEN json_set(lane.value, '$.effective.schemaVersion', 4,
1082
- '$.effective.component', CASE json_extract(lane.value, '$.effective.adapterId')
1083
- WHEN 'codex' THEN 'codex-cli'
1084
- WHEN 'claude' THEN 'claude-code-cli'
1085
- ELSE 'unknown-acp-agent' END)
1086
- ELSE lane.value END)
1087
- FROM json_each(grp.value, '$.lanes') AS lane
1088
- ))))
1089
- FROM json_each(payload, '$.executionGroups') AS grp
1090
- ))) WHERE json_type(payload, '$.executionGroups') = 'array';
1091
-
1092
- -- A ReviewRound holds at most one Group, so only the Lane array nests here.
1093
- UPDATE review_rounds SET payload = json_set(payload, '$.executionGroup.lanes', json((
1094
- SELECT json_group_array(CASE
1095
- WHEN json_type(lane.value, '$.effective') = 'object'
1096
- THEN json_set(lane.value, '$.effective.schemaVersion', 4,
1097
- '$.effective.component', CASE json_extract(lane.value, '$.effective.adapterId')
1098
- WHEN 'codex' THEN 'codex-cli'
1099
- WHEN 'claude' THEN 'claude-code-cli'
1100
- ELSE 'unknown-acp-agent' END)
1101
- ELSE lane.value END)
1102
- FROM json_each(payload, '$.executionGroup.lanes') AS lane
1103
- ))) WHERE json_type(payload, '$.executionGroup.lanes') = 'array';
1104
- `
1105
- },
1106
- {
1107
- version: 16,
1108
- name: "acp-session-run-configuration",
1109
- introducedIn: "0.15.10",
1110
- // An ACP Role binding may now carry a model, a reasoning effort and a
1111
- // permission strategy beyond `default`, because Yui's ACP client implements
1112
- // `session/set_config_option` and pushes those values to the Session before
1113
- // it prompts. Previously the adapter rejected all three, so no stored
1114
- // payload can contain them.
1115
- //
1116
- // This widens the contract without rewriting anything, and the absence of a
1117
- // payload update is the substantive decision rather than an omission. A v10
1118
- // ACP binding holds `permission.strategy = "default"`, which keeps exactly
1119
- // the meaning it always had: Yui sends no mode, so the Agent's own default
1120
- // stands. Rewriting those rows to `bypass` — or to any newly expressible
1121
- // value — would grant authority the user never chose, on Homes whose owner
1122
- // did nothing but upgrade. `bypass` is reachable only by asking for it.
1123
- //
1124
- // Effective launch snapshots need no change either. They already carry
1125
- // optional `model` and `effort` on the shared base, and their ACP permission
1126
- // is the same object the adapter canonicalizes, so a historical snapshot
1127
- // still validates unchanged at schemaVersion 4 across Sessions, Session
1128
- // history, Turns, WorkItem ExecutionLanes and ReviewRound ExecutionLanes.
1129
- // A frozen snapshot therefore keeps describing the launch it actually ran,
1130
- // which is what makes replaying old history honest.
1131
- sql: "SELECT 1; -- ACP bindings may carry model/effort and a chosen "
1132
- + "permission mode; existing `default` bindings keep their meaning"
1133
- },
1134
- {
1135
- version: 17,
1136
- name: "session-origin-input-requests",
1137
- introducedIn: "0.15.8",
1138
- // Widen requester provenance: notifications originate in a current Leader
1139
- // Session without an AgentRun. Existing schemaVersion-3 requests keep their
1140
- // exact Run/Session origin, including frozen historical Context values.
1141
- // No guessed native identity or history rewrite is needed.
1142
- sql: "SELECT 1; -- InputRequest requester.runId is optional when an exact nativeSessionId is present"
1143
- },
1144
- {
1145
- version: 18,
1146
- name: "replaceable-session-process-custody",
1147
- introducedIn: "0.15.8",
1148
- // Existing Session/Run history remains unchanged. Runtime mailboxes may
1149
- // now retain explicit replacement intent; OS owner records may identify
1150
- // the dedicated execution child, independently of a disposable Host.
1151
- sql: "SELECT 1; -- Replacement intent, independent retained native control evidence/process custody, and optional fixed TaskWake refs"
1152
- },
1153
- {
1154
- version: 19,
1155
- name: "task-artifacts-local-git",
1156
- introducedIn: "0.16.0",
1157
- // Retire the DB-owned immutable Artifact store: file/directory artifacts now
1158
- // live in a per-Task local Git repository, referenced by a self-certifying
1159
- // `commit + relativePath`. The whole rewrite is payload work that must READ
1160
- // the `artifacts` table before it is dropped, so it runs entirely in
1161
- // `migrateData` (which executes after this `sql`) — the table is dropped
1162
- // there, last, once its rows have been moved. Requirement A's independent,
1163
- // deterministic submit-intent backfill (events + id_sequences only) is the
1164
- // final step inside that same transaction. This `sql` is intentionally a
1165
- // no-op: dropping the table here would destroy the rows before they move.
1166
- //
1167
- // The 18->19 contract also DECLARES three optional `messages.payload` fields
1168
- // added by Requirement A — `intent` (record|discuss|develop), an optional
1169
- // client idempotency `submissionKey`, and a frozen `submissionReceipt`
1170
- // disposition. They add no column and no table and are only ever written
1171
- // going forward (absent `intent` reads as discuss; absent key is keyless;
1172
- // absent receipt is a pre-19 message), so NO historical row is rewritten and
1173
- // no key or receipt is ever fabricated for old data — declaring them here
1174
- // satisfies "optional still requires a migration declaration".
1175
- sql: "SELECT 1; -- artifacts move to per-Task local Git; see migrateArtifactsToGit",
1176
- migrateData: migrateArtifactsToGit
1177
- },
1178
- {
1179
- version: 20,
1180
- name: "integration-conflict-continuation",
1181
- introducedIn: "0.16.0",
1182
- // Widen Integration status with conflicted; declare optional sourceProgress
1183
- // (Git cursor/reflog action) and checkInputDigest. Exact old bound FF Job
1184
- // receipts supply these facts; unprovable history remains unchanged.
1185
- // Frozen Context and events are never rewritten.
1186
- sql: "SELECT 1; -- Integration Git progress and exact check admission",
1187
- migrateData: migrateIntegrationContinuation
1188
- },
1189
- {
1190
- version: 21,
1191
- name: "force-archive-independent-cleanup",
1192
- introducedIn: "0.16.0",
1193
- // Declare optional task.archived force/cleanup/warnings/retainedResources
1194
- // audit fields and task.archive-cleanup resource/status/detail/paths events;
1195
- // runtime.event-obsolete may retain the full originalEvent as source evidence.
1196
- // Archived Tasks may retain exact runtime/workspace/mailbox ownership.
1197
- // Existing archive, completion and Publication history is unchanged.
1198
- sql: "SELECT 1; -- archive intent and cleanup evidence remain separate facts"
1199
- },
1200
- {
1201
- version: 22,
1202
- name: "controller-owned-agent-host-ingress",
1203
- introducedIn: "0.16.0",
1204
- // Add optional `host` source metadata to the stable Inbox v1 observation
1205
- // envelope. Such facts contain no resolved Run: current Controller alone
1206
- // validates ownership and commits observations, process custody and native
1207
- // account locations. Existing Inbox facts and domain history stay intact.
1208
- // Host status capabilities/transport diagnostics are live, not stored.
1209
- sql: "SELECT 1; -- Controller-owned Host ingress; no historical data rewrite"
1210
- },
1211
- {
1212
- version: 23,
1213
- name: "unified-message-input-control",
1214
- introducedIn: "0.16.1",
1215
- // Widen the Message payload with an optional durable input action
1216
- // (queue/steer) plus its stable requestId, and an optional interrupt-then
1217
- // handoff claim (requestId + exact target AgentRun). Existing messages carry
1218
- // neither field and keep their current queue semantics and delivery history;
1219
- // no native Turn identity is synthesized and no history is rewritten. The
1220
- // same contiguous migration adds the Global-owned Message store
1221
- // (global_role_messages), extending the Message store to a Global owner
1222
- // exactly as global_role_session_sets parallels role_session_sets: an
1223
- // explicit Global owner and authorizable reference, no fabricated Task and
1224
- // no new private global queue (decision-3 §9/§11).
1225
- sql: `SELECT 1; -- Message input actions, exact Session delivery pins, provider receipts and interrupt-then claims; Global Session provider bindings and native interrupt evidence\n${GLOBAL_ROLE_MESSAGE_SQL}`
1226
- },
1227
- {
1228
- version: 24,
1229
- name: "unify-home-layout",
1230
- introducedIn: "0.15.9",
1231
- // Unify every Yui self-managed path under a single canonical YUI_HOME. The
1232
- // managed Git worktrees move from the out-of-Home `defaultWorkspace` into
1233
- // `<home>/workspaces/{worktree,tasks}` and the provider runtimes from the
1234
- // `<home>.task-runtimes` sibling into `<home>/runtime/task-runtimes`. Only
1235
- // the durable Git worktree tree is physically relocated, by COPY (never
1236
- // rename-away): its replica is digest-verified against the source and
1237
- // atomically published while the original is PRESERVED as the rollback
1238
- // anchor; the regenerable task views and disposable runtimes are pointer
1239
- // rewrites only. The copied worktrees are then repaired, and only the
1240
- // persisted pointers the runtime trusts as live are rewritten. Frozen
1241
- // evidence (Context snapshots, terminal Run Git evidence, candidate
1242
- // snapshots, terminal Jobs, reports) and self-healing/self-rediscovered
1243
- // records (Task cwd, active Role workspaces, views, resource registry) are
1244
- // left intact; a queued/running Job bound under a relocating root is refused
1245
- // rather than silently moved. The data/filesystem transform is the
1246
- // substantive step; the SQL is inert.
1247
- sql: UNIFY_HOME_LAYOUT_SQL,
1248
- migrateData: migrateUnifyHomeLayout
1249
- },
1250
- {
1251
- version: 25,
1252
- name: "collapse-worktree-layout",
1253
- introducedIn: "0.15.9",
1254
- // Collapse the two-layer managed workspace layout into a single layer of real
1255
- // Git worktrees addressed by Task/owner and Project. Each managed worktree
1256
- // moves from its per-Project physical root
1257
- // (`<home>/workspaces/worktree/<projectName>/<taskKey>/<roleKey>`) to the
1258
- // location the v19 symlink view used to point at
1259
- // (`<home>/workspaces/tasks/<taskId>/<owner>/<projectDirectory>`), so the
1260
- // logical entry path and the physical worktree become the same directory and
1261
- // the symlink-view indirection is gone; an integration attempt moves under a
1262
- // new `tasks/<taskId>/integrations/<id>/` owner root, addressed by the bound
1263
- // Project directory rather than the Project name. Only the durable Git
1264
- // worktrees are physically relocated, by COPY (never rename-away): each
1265
- // replica is digest-verified against the source and atomically published over
1266
- // the regenerable view symlink while the original is PRESERVED as the
1267
- // rollback anchor. The copied worktrees are then repaired, and only the
1268
- // persisted pointers the runtime trusts as live are rewritten. Frozen
1269
- // evidence and self-healing records are left intact; a queued/running Job
1270
- // bound under a relocating worktree is refused rather than silently moved.
1271
- // The data/filesystem transform is the substantive step; the SQL is inert.
1272
- sql: COLLAPSE_WORKTREE_LAYOUT_SQL,
1273
- migrateData: migrateCollapseWorktreeLayout
1274
- },
1275
- {
1276
- version: 26,
1277
- name: "publication-candidate-adoption",
1278
- introducedIn: "0.15.12",
1279
- // Add optional Publication.headCommit (the observed PR/MR source head) and
1280
- // publication.candidate-adopted events binding publicationId, completionEventId,
1281
- // projectId, acceptedCommit/localCommit, acceptedTree/candidateTree, diffDigest,
1282
- // acceptance and by, optionally integrationId/integrationDigest. Derived
1283
- // task.archived mergeCoverage may also be uncovered or unverified.
1284
- // No stored delivery index and no inferred adoption or verification:
1285
- // valid old Publication, completion and archive payloads remain byte-identical.
1286
- sql: "SELECT 1; -- fixed acceptance-to-publication evidence; preserve all historical facts"
1287
- },
1288
- {
1289
- version: 27,
1290
- name: "bounded-provider-input-retry",
1291
- introducedIn: "0.16.0",
1292
- // Optional owned input/recovery records on the existing Provider binding;
1293
- // positive Driver transient evidence on StandardAgentError. No historical
1294
- // failures are scanned, scheduled, reclassified or replayed by upgrading.
1295
- sql: `
1296
- -- ProviderTurn.input/retrySupported/failure; ProviderRuntimeBinding.retry/retryHistory/retryDisabled;
1297
- -- optional error retryable and retryAfterMs facts. Index existing records only.
1298
- CREATE INDEX idx_task_provider_retry ON role_session_sets(
1299
- json_extract(payload, '$.providerBinding.retry.status')
1300
- ) WHERE json_extract(payload, '$.providerBinding.retry.status') IN ('waiting', 'in-flight');
1301
- CREATE INDEX idx_global_provider_retry ON global_role_session_sets(
1302
- json_extract(payload, '$.providerBinding.retry.status')
1303
- ) WHERE json_extract(payload, '$.providerBinding.retry.status') IN ('waiting', 'in-flight');
1304
- `
1305
- },
1306
- {
1307
- version: 28,
1308
- name: "current-contract-only-dispatch",
1309
- introducedIn: "0.16.0",
1310
- // Freeze the one valid earlier singleton dispatch shape here, not in the
1311
- // normal runtime. Preserve the full input batch and sequence; only replace
1312
- // its transport dedupe identity. Merged/ambiguous batches, processing
1313
- // claims, original Messages, results and external-effect evidence remain
1314
- // untouched. This step does not accept, replay or complete any execution.
1315
- sql: `
1316
- UPDATE mailboxes
1317
- SET pending = json_set(pending,
1318
- '$.sources', json_array('turn-dispatch'),
1319
- '$.dedupeKeys', json_array('role-turn:' || task_id || ':' || role_name || ':' ||
1320
- (SELECT json_extract(value, '$.id') FROM json_each(mailboxes.pending, '$.refs')
1321
- WHERE json_extract(value, '$.type') = 'run'
1322
- AND json_extract(value, '$.taskId') = mailboxes.task_id)))
1323
- WHERE target_kind = 'role' AND role_name <> 'leader' AND pending IS NOT NULL
1324
- AND json_extract(pending, '$.requestCount') = 1
1325
- AND json_array_length(pending, '$.sources') = 1
1326
- AND json_extract(pending, '$.sources[0]') = 'yui'
1327
- AND EXISTS (SELECT 1 FROM json_each(mailboxes.pending, '$.reasons')
1328
- WHERE value IN ('turn-dispatched', 'turn-retried', 'review-requested',
1329
- 'workitem-synthesis-ready', 'review-synthesis-ready'))
1330
- AND (SELECT count(*) FROM json_each(mailboxes.pending, '$.refs')
1331
- WHERE json_extract(value, '$.type') = 'run') = 1
1332
- AND EXISTS (SELECT 1 FROM json_each(mailboxes.pending, '$.refs')
1333
- WHERE json_extract(value, '$.type') = 'run'
1334
- AND json_extract(value, '$.taskId') = mailboxes.task_id);
1335
- `
1336
- },
1337
- {
1338
- version: 29,
1339
- name: "agent-ordered-atomic-integration",
1340
- introducedIn: "0.16.0",
1341
- // Retire the scheduler, not its user's intent. Freeze each complete queue
1342
- // payload in ordinary Task history before dropping the active table.
1343
- // Existing Integrations, Jobs and WorkItem delivery evidence remain exact;
1344
- // no entry is interpreted as committed or automatically replayed.
1345
- sql: `
1346
- CREATE TEMP TABLE retired_integration_queue AS
1347
- SELECT q.*,
1348
- strftime('%Y-%m-%dT%H:%M:%fZ', 'now') AS retired_at,
1349
- max(
1350
- coalesce((SELECT high_water FROM id_sequences
1351
- WHERE task_id = q.task_id AND kind = 'event'), 0),
1352
- coalesce((SELECT max(CAST(substr(event_id, 7) AS INTEGER)) FROM events
1353
- WHERE task_id = q.task_id), 0)
1354
- ) + row_number() OVER (
1355
- PARTITION BY q.task_id ORDER BY q.created_at,
1356
- CAST(substr(q.queue_id, length('integration-queue-') + 1) AS INTEGER)
1357
- ) AS event_sequence
1358
- FROM integration_queue AS q;
1359
-
1360
- INSERT INTO events(task_id, event_id, type, occurred_at, payload)
1361
- SELECT task_id, 'event-' || event_sequence, 'integration.queue-retired', retired_at,
1362
- json_object(
1363
- 'schemaVersion', 2, 'id', 'event-' || event_sequence, 'taskId', task_id,
1364
- 'type', 'integration.queue-retired', 'createdAt', retired_at,
1365
- 'payload', json_object('queueId', queue_id, 'record', payload,
1366
- 'disposition', 'retired-without-replay')
1367
- )
1368
- FROM retired_integration_queue;
1369
-
1370
- INSERT INTO id_sequences(task_id, kind, high_water)
1371
- SELECT task_id, 'event', max(event_sequence)
1372
- FROM retired_integration_queue GROUP BY task_id
1373
- ON CONFLICT(task_id, kind) DO UPDATE SET high_water = excluded.high_water;
1374
-
1375
- DROP TABLE retired_integration_queue;
1376
- DROP TABLE integration_queue;
1377
- DELETE FROM id_sequences WHERE kind = 'integrationQueue';
1378
- `
1379
- },
1380
- {
1381
- version: 30,
1382
- name: "notification-only-wakes",
1383
- introducedIn: "0.16.0",
1384
- sql: "SELECT 1; -- Retire Run-linked wakes as audit events; notification schema 2; explicit Global providerBinding",
1385
- migrateData: migrateNotificationOnlyWakes
1386
- },
1387
- {
1388
- version: 31,
1389
- name: "explicit-review-scope",
1390
- introducedIn: "0.16.0",
1391
- sql: `
1392
- UPDATE review_rounds
1393
- SET payload = json_set(payload, '$.scope', 'work-item')
1394
- WHERE json_type(payload, '$.scope') IS NULL
1395
- OR json_type(payload, '$.scope') = 'null';
1396
- `
1397
- },
1398
- {
1399
- version: 32,
1400
- name: "work-item-history-as-events",
1401
- introducedIn: "0.16.0",
1402
- sql: "SELECT 1; -- Archive original WorkItem payloads before removing historicalState",
1403
- migrateData: migrateWorkItemHistory
1404
- },
1405
- {
1406
- version: 33,
1407
- name: "advisory-and-verification-policy",
1408
- introducedIn: "0.16.0",
1409
- sql: "SELECT 1; -- Retire rollout modes/shadow metrics; explicit rerun intent; verification proof v3",
1410
- migrateData: migrateVerificationPolicy
1411
- },
1412
- {
1413
- version: 34,
1414
- name: "current-input-contract",
1415
- introducedIn: "0.16.0",
1416
- sql: "SELECT 1; -- Archive retired Message wake policies and activation origin gates",
1417
- migrateData: migrateCurrentInputContract
1418
- },
1419
- {
1420
- version: 35,
1421
- name: "current-verification-and-owner-contract",
1422
- introducedIn: "0.16.0",
1423
- sql: CURRENT_RUNTIME_CONTRACT_SQL,
1424
- migrateData: migrateCurrentRuntimeContract
1425
- },
1426
- {
1427
- version: 36,
1428
- name: "agent-failure-configuration-context",
1429
- introducedIn: "0.16.0",
1430
- sql: "SELECT 1; -- Preserve historical errors; absent original launch configuration is explicitly unavailable",
1431
- migrateData: migrateAgentFailureContext
1432
- },
1433
- {
1434
- version: 37,
1435
- name: "narrow-agent-failure-context",
1436
- introducedIn: "0.16.0",
1437
- sql: "SELECT 1; -- Archive full v36 failure snapshots; retain only native metadata selectors",
1438
- migrateData: migrateNarrowAgentFailureContext
8
+ export const CURRENT_SCHEMA_CHECKSUM = createHash("sha256").update(BASELINE_SCHEMA_SQL).digest("hex");
9
+ export class SqliteSchemaError extends Error {
10
+ code;
11
+ constructor(detail, code = "STORAGE_SCHEMA_INVALID") {
12
+ super(`SQLite storage schema cannot be opened: ${detail}`);
13
+ this.code = code;
1439
14
  }
1440
- ]);
1441
- for (let index = 0; index < MIGRATIONS.length; index += 1) {
1442
- const expectedVersion = MIN_SUPPORTED_STORAGE_VERSION + index;
1443
- if (MIGRATIONS[index]?.version !== expectedVersion) {
1444
- throw new Error(`Storage migration registry must be contiguous from `
1445
- + `${MIN_SUPPORTED_STORAGE_VERSION}; missing version ${expectedVersion}.`);
1446
- }
1447
- }
1448
- if (MIGRATIONS.at(-1)?.version !== CURRENT_STORAGE_VERSION) {
1449
- throw new Error(`Storage migration registry head ${String(MIGRATIONS.at(-1)?.version)} does not match `
1450
- + `CURRENT_STORAGE_VERSION ${CURRENT_STORAGE_VERSION}.`);
1451
- }
1452
- /** Current hot-path indexes whose absence would invalidate a current Home. */
1453
- const REQUIRED_SCHEMA_INDEXES = [
1454
- "idx_mailboxes_ready",
1455
- "idx_input_requests_open_hot",
1456
- "idx_turns_planning_active",
1457
- "idx_tasks_activation_pending"
1458
- ];
1459
- function checksum(sql) {
1460
- return createHash("sha256").update(sql).digest("hex");
1461
15
  }
1462
- /**
1463
- * A SQLite Home is only safe to open when its migration ledger proves exactly
1464
- * which schema definition was applied. The ledger is durable metadata, not a
1465
- * best-effort cache: a missing row, a changed checksum, or an unknown version
1466
- * must stop startup before any pending schema step is applied.
1467
- */
1468
- export class SqliteSchemaMigrationError extends Error {
1469
- constructor(detail, subject = "metadata") {
1470
- super(`SQLite schema migration ${subject} is invalid: ${detail}`);
1471
- this.name = "SqliteSchemaMigrationError";
16
+ // Only explicit, contiguous minor changes in this baseline may be added here.
17
+ // The initial 1.0 release has no upgrade steps and carries no older format code.
18
+ const MINOR_UPGRADES = Object.freeze([]);
19
+ export function storageMinorUpgradePlan(from) {
20
+ if (from === CURRENT_STORAGE_VERSION)
21
+ return [];
22
+ if (!isMinorStorageUpgrade(from, CURRENT_STORAGE_VERSION))
23
+ return null;
24
+ const result = [];
25
+ let current = from;
26
+ while (current !== CURRENT_STORAGE_VERSION) {
27
+ const step = MINOR_UPGRADES.find(candidate => candidate.fromVersion === current);
28
+ if (step === undefined || !isMinorStorageUpgrade(step.fromVersion, step.toVersion)
29
+ || storageVersionParts(step.toVersion).minor !== storageVersionParts(current).minor + 1)
30
+ return null;
31
+ result.push(step);
32
+ current = step.toVersion;
1472
33
  }
34
+ return result;
35
+ }
36
+ function objects(db) {
37
+ return db.prepare(`SELECT type,name,sql FROM sqlite_master
38
+ WHERE name NOT GLOB 'sqlite_*' ORDER BY type,name`).all();
1473
39
  }
1474
- const SCHEMA_MIGRATIONS_SQL = `
1475
- CREATE TABLE schema_migrations (
1476
- version INTEGER PRIMARY KEY,
1477
- name TEXT NOT NULL,
1478
- applied_at TEXT NOT NULL,
1479
- checksum TEXT NOT NULL
1480
- )
1481
- `;
1482
- /**
1483
- * Admit the migration ledger only for a genuinely empty SQLite database.
1484
- * Recreating an absent ledger on top of existing Yui tables would make the
1485
- * migration runner mistake a live Home for a fresh one and replay destructive
1486
- * layout migrations. A database with any sqlite_master object is therefore
1487
- * diagnosed as corrupt/partially initialized and left untouched.
1488
- */
1489
- function ensureMigrationLedger(db, mode) {
1490
- const objects = db.prepare("SELECT type, name FROM sqlite_master WHERE name IS NOT NULL").all();
1491
- const ledger = objects.find(({ name }) => name === "schema_migrations");
1492
- if (ledger === undefined) {
1493
- if (mode === "validate") {
1494
- throw new SqliteSchemaMigrationError("schema_migrations ledger is missing from an existing database");
40
+ let expectedObjects;
41
+ function validateSchemaObjects(db) {
42
+ if (expectedObjects === undefined) {
43
+ const reference = new Database(":memory:");
44
+ try {
45
+ reference.exec(BASELINE_SCHEMA_SQL);
46
+ expectedObjects = objects(reference);
1495
47
  }
1496
- if (objects.length !== 0) {
1497
- throw new SqliteSchemaMigrationError("schema_migrations ledger is missing from a non-empty database");
48
+ finally {
49
+ reference.close();
1498
50
  }
1499
- db.exec(SCHEMA_MIGRATIONS_SQL);
1500
- return true;
1501
- }
1502
- if (ledger.type !== "table") {
1503
- throw new SqliteSchemaMigrationError(`schema_migrations has type ${String(ledger.type)} instead of table`);
1504
51
  }
1505
- return false;
1506
- }
1507
- function validateMigrationLedgerColumns(db) {
1508
- const columns = new Set(db.prepare("PRAGMA table_info(schema_migrations)").all()
1509
- .flatMap(({ name }) => typeof name === "string" ? [name] : []));
1510
- if (columns.size === 4
1511
- && ["version", "name", "applied_at", "checksum"].every((name) => columns.has(name))) {
1512
- return;
52
+ const actual = objects(db);
53
+ for (const expected of expectedObjects) {
54
+ const found = actual.find(object => object.type === expected.type && object.name === expected.name);
55
+ if (found?.sql !== expected.sql)
56
+ throw new SqliteSchemaError(`required ${expected.type} '${expected.name}' differs from the current definition.`);
1513
57
  }
1514
- throw new SqliteSchemaMigrationError("schema_migrations columns do not match the storage-version-1 ledger");
58
+ if (actual.length !== expectedObjects.length)
59
+ throw new SqliteSchemaError("unexpected schema objects; preserve the database for diagnosis.");
1515
60
  }
1516
- /** Validate the applied linear prefix and return its current head. */
1517
- function validateAppliedMigrations(db, ledgerWasCreated) {
1518
- validateMigrationLedgerColumns(db);
1519
- const expected = new Map(MIGRATIONS.map((migration) => [migration.version, migration]));
1520
- const rows = db.prepare("SELECT version, name, checksum FROM schema_migrations ORDER BY version").all();
1521
- if (rows.length === 0 && !ledgerWasCreated) {
1522
- throw new SqliteSchemaMigrationError("schema_migrations ledger is empty in an existing database");
61
+ /** Read the one format identity. Missing metadata never authorizes initialization. */
62
+ export function inspectSqliteSchema(db) {
63
+ const table = db.prepare("SELECT type FROM sqlite_master WHERE name='storage_schema'").get();
64
+ if (table?.type !== "table") {
65
+ throw new SqliteSchemaError("storage_schema is missing: this is not the current baseline. Use the independent conversion tool for a different format.", "STORAGE_FORMAT_UNSUPPORTED");
1523
66
  }
1524
- const applied = new Set();
1525
- for (const row of rows) {
1526
- if (!Number.isInteger(row.version) || row.version < 1) {
1527
- throw new SqliteSchemaMigrationError(`invalid migration version ${String(row.version)}`);
1528
- }
1529
- const version = row.version;
1530
- const migration = expected.get(version);
1531
- if (applied.has(version)) {
1532
- throw new SqliteSchemaMigrationError(`duplicate migration version ${version}`);
1533
- }
1534
- applied.add(version);
1535
- if (migration === undefined) {
1536
- if (version <= CURRENT_STORAGE_VERSION) {
1537
- throw new SqliteSchemaMigrationError(`unknown migration version ${version}`);
1538
- }
1539
- if (typeof row.name !== "string" || row.name.length === 0
1540
- || typeof row.checksum !== "string" || row.checksum.length === 0) {
1541
- throw new SqliteSchemaMigrationError(`future migration ${version} metadata is invalid`);
1542
- }
1543
- continue;
1544
- }
1545
- if (row.name !== migration.name) {
1546
- throw new SqliteSchemaMigrationError(`migration ${version} name ${String(row.name)} does not match ${migration.name}`);
1547
- }
1548
- const expectedChecksum = checksum(migration.sql);
1549
- if (row.checksum !== expectedChecksum) {
1550
- throw new SqliteSchemaMigrationError(`migration ${version} checksum ${String(row.checksum)} does not match current definition`);
1551
- }
67
+ const rows = db.prepare("SELECT * FROM storage_schema").all();
68
+ const row = rows[0];
69
+ const version = row === undefined ? "" : `${row.major}.${row.minor}`;
70
+ if (rows.length !== 1 || row?.id !== 1 || row.format !== STORAGE_FORMAT
71
+ || !Number.isSafeInteger(row.major) || !Number.isSafeInteger(row.minor)
72
+ || !isStorageVersion(version) || !/^[a-f0-9]{64}$/u.test(row.checksum)
73
+ || !Number.isFinite(Date.parse(row.created_at))) {
74
+ throw new SqliteSchemaError("invalid baseline identity.");
1552
75
  }
1553
- const versions = [...applied].sort((left, right) => left - right);
1554
- for (let index = 0; index < versions.length; index += 1) {
1555
- const expectedVersion = MIN_SUPPORTED_STORAGE_VERSION + index;
1556
- if (versions[index] !== expectedVersion) {
1557
- throw new SqliteSchemaMigrationError(`migration ledger has a gap before version ${versions[index]}`);
1558
- }
76
+ const plan = storageMinorUpgradePlan(version);
77
+ const expectedChecksum = version === CURRENT_STORAGE_VERSION
78
+ ? CURRENT_SCHEMA_CHECKSUM : plan?.[0]?.sourceChecksum;
79
+ if (expectedChecksum !== undefined && row.checksum !== expectedChecksum) {
80
+ throw new SqliteSchemaError("baseline checksum does not match its version.");
1559
81
  }
1560
- const currentVersion = versions.at(-1) ?? 0;
1561
- const head = rows.at(-1);
82
+ if (version === CURRENT_STORAGE_VERSION)
83
+ validateSchemaObjects(db);
1562
84
  return {
1563
- versions: applied,
1564
- currentVersion,
1565
- currentChecksum: typeof head?.checksum === "string" ? head.checksum : ""
85
+ currentVersion: version, currentChecksum: row.checksum,
86
+ targetVersion: CURRENT_STORAGE_VERSION, targetChecksum: CURRENT_SCHEMA_CHECKSUM,
87
+ minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
88
+ pendingVersions: plan?.map(step => step.toVersion) ?? []
1566
89
  };
1567
90
  }
1568
- /**
1569
- * Validate the physical objects promised by the migration ledger. Ledger
1570
- * rows can be forged independently of SQLite's schema, so a complete and
1571
- * checksummed ledger is not enough to authorize startup when an object was
1572
- * manually removed or replaced.
1573
- */
1574
- function validateSchemaObjects(db) {
1575
- const objects = new Map(db.prepare("SELECT type, name FROM sqlite_master WHERE name IS NOT NULL").all().flatMap(({ type, name }) => (typeof type === "string" && typeof name === "string" ? [[name, type]] : [])));
1576
- for (const table of SQLITE_SCHEMA_TABLES) {
1577
- if (objects.get(table) !== "table") {
1578
- throw new SqliteSchemaMigrationError(`required table '${table}' is missing or has the wrong type`, "schema object");
1579
- }
1580
- }
1581
- for (const index of REQUIRED_SCHEMA_INDEXES) {
1582
- if (objects.get(index) !== "index") {
1583
- throw new SqliteSchemaMigrationError(`required index '${index}' is missing or has the wrong type`, "schema object");
1584
- }
1585
- }
1586
- const homeMetaColumns = db.prepare("PRAGMA table_info(home_meta)").all().map(({ name }) => name);
1587
- const expectedHomeMetaColumns = [
1588
- "id",
1589
- "home_identity",
1590
- "revision",
1591
- "created_at",
1592
- "updated_at"
1593
- ];
1594
- if (homeMetaColumns.length !== expectedHomeMetaColumns.length
1595
- || homeMetaColumns.some((name, index) => name !== expectedHomeMetaColumns[index])) {
1596
- throw new SqliteSchemaMigrationError("home_meta columns do not match the current storage contract", "schema object");
91
+ export function validateSqliteSchema(db) {
92
+ const state = inspectSqliteSchema(db);
93
+ if (state.currentVersion !== CURRENT_STORAGE_VERSION) {
94
+ throw new SqliteSchemaError(`storage ${state.currentVersion} is not current ${CURRENT_STORAGE_VERSION}; ordinary opens never convert data.`);
1597
95
  }
96
+ return state;
1598
97
  }
1599
- /** Return the one linear upgrade path from a supported source to this release. */
1600
- export function storageMigrationPlan(currentVersion) {
1601
- if (!Number.isInteger(currentVersion)
1602
- || currentVersion < MIN_SUPPORTED_STORAGE_VERSION
1603
- || currentVersion > CURRENT_STORAGE_VERSION) {
1604
- return null;
1605
- }
1606
- return MIGRATIONS
1607
- .filter(({ version }) => version > currentVersion)
1608
- .map(({ version, name, introducedIn }) => ({
1609
- fromVersion: version - 1,
1610
- toVersion: version,
1611
- name,
1612
- introducedIn
1613
- }));
1614
- }
1615
- /** Inspect a recognized migration prefix without changing it. */
1616
- export function inspectSqliteSchemaMigrations(db) {
1617
- const ledgerWasCreated = ensureMigrationLedger(db, "validate");
1618
- const applied = validateAppliedMigrations(db, ledgerWasCreated);
1619
- if (applied.currentVersion > CURRENT_STORAGE_VERSION) {
1620
- return {
1621
- currentVersion: applied.currentVersion,
1622
- currentChecksum: applied.currentChecksum,
1623
- targetVersion: CURRENT_STORAGE_VERSION,
1624
- minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
1625
- targetChecksum: checksum(MIGRATIONS.at(-1).sql),
1626
- pendingVersions: []
1627
- };
1628
- }
1629
- const pendingVersions = MIGRATIONS
1630
- .filter((migration) => !applied.versions.has(migration.version))
1631
- .map((migration) => migration.version);
1632
- if (pendingVersions.length === 0)
1633
- validateSchemaObjects(db);
1634
- const target = MIGRATIONS.at(-1);
1635
- if (applied.currentVersion === 0) {
1636
- throw new SqliteSchemaMigrationError("schema_migrations ledger has no current head");
98
+ /** Fresh initialization, never a replay of previous schema definitions. */
99
+ export function initializeSqliteSchema(db) {
100
+ if (db.prepare("SELECT 1 FROM sqlite_master LIMIT 1").get() !== undefined) {
101
+ throw new SqliteSchemaError("initialization requires an empty database.");
1637
102
  }
1638
- return {
1639
- currentVersion: applied.currentVersion,
1640
- currentChecksum: applied.currentChecksum,
1641
- targetVersion: CURRENT_STORAGE_VERSION,
1642
- minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
1643
- targetChecksum: checksum(target.sql),
1644
- pendingVersions
1645
- };
103
+ const { major, minor } = storageVersionParts(CURRENT_STORAGE_VERSION);
104
+ return db.transaction(() => {
105
+ db.exec(BASELINE_SCHEMA_SQL);
106
+ db.prepare("INSERT INTO storage_schema VALUES (1,?,?,?,?,?)")
107
+ .run(STORAGE_FORMAT, major, minor, CURRENT_SCHEMA_CHECKSUM, new Date().toISOString());
108
+ return validateSqliteSchema(db);
109
+ })();
1646
110
  }
1647
- /**
1648
- * Apply or validate schema migrations without letting an ordinary open mutate
1649
- * an existing authoritative database.
1650
- *
1651
- * In `apply` mode every pending DDL/data step and every ledger row runs in one
1652
- * outer transaction. The database therefore advances to the release version
1653
- * as one commit or remains entirely at its previous version. `validate` mode
1654
- * rejects a pending version before executing any migration.
1655
- */
1656
- export function migrateSqliteSchema(db, options) {
1657
- const migrate = () => {
1658
- const ledgerWasCreated = ensureMigrationLedger(db, options.mode);
1659
- // Validate the complete ledger before touching any pending migration. This
1660
- // prevents a manually altered or partially recorded ledger from silently
1661
- // skipping a later schema/data step.
1662
- const applied = validateAppliedMigrations(db, ledgerWasCreated);
1663
- if (applied.currentVersion > CURRENT_STORAGE_VERSION) {
1664
- throw new SqliteSchemaMigrationError(`storage version ${applied.currentVersion} is newer than supported `
1665
- + `${CURRENT_STORAGE_VERSION}`, "admission");
1666
- }
1667
- if (applied.currentVersion !== 0
1668
- && applied.currentVersion < MIN_SUPPORTED_STORAGE_VERSION) {
1669
- throw new SqliteSchemaMigrationError(`storage version ${applied.currentVersion} is older than the minimum supported `
1670
- + `${MIN_SUPPORTED_STORAGE_VERSION}`, "admission");
1671
- }
1672
- // Production callers omit `throughVersion`, so the effective target is head
1673
- // and behavior is unchanged; a partial target is honored only in apply mode.
1674
- const effectiveTarget = options.mode === "apply" && options.throughVersion !== undefined
1675
- ? options.throughVersion
1676
- : CURRENT_STORAGE_VERSION;
1677
- const pending = MIGRATIONS.filter((migration) => !applied.versions.has(migration.version) && migration.version <= effectiveTarget);
1678
- if (!ledgerWasCreated && pending.length > 0 && options.mode === "validate") {
1679
- throw new SqliteSchemaMigrationError(`Storage version ${applied.currentVersion} requires an explicit upgrade to `
1680
- + `${CURRENT_STORAGE_VERSION}`, "admission");
1681
- }
1682
- // Bound the applied steps to `throughVersion` (default and clamp: head), so a
1683
- // migration test can stop at one step's intermediate state while production
1684
- // always advances to head. The validate-mode guard above still fires on the
1685
- // FULL pending set — the bound never lets an ordinary open partially migrate.
1686
- const targetVersion = Math.min(options.throughVersion ?? CURRENT_STORAGE_VERSION, CURRENT_STORAGE_VERSION);
1687
- const toApply = pending.filter((migration) => migration.version <= targetVersion);
1688
- const newlyApplied = [];
1689
- for (const migration of toApply) {
1690
- db.exec(migration.sql);
1691
- migration.migrateData?.(db);
1692
- const appliedAt = new Date().toISOString();
1693
- db.prepare(`INSERT INTO schema_migrations (version, name, applied_at, checksum)
1694
- VALUES (?, ?, ?, ?)`).run(migration.version, migration.name, appliedAt, checksum(migration.sql));
1695
- newlyApplied.push(migration.version);
111
+ /** Called only after an explicit minor upgrade has obtained its maintenance fence. */
112
+ export function applySqliteMinorUpgrades(db) {
113
+ db.transaction(() => {
114
+ const before = inspectSqliteSchema(db);
115
+ const plan = storageMinorUpgradePlan(before.currentVersion);
116
+ if (plan === null)
117
+ throw new SqliteSchemaError("cross-major or unknown upgrades require an independent explicit converter.");
118
+ for (const step of plan) {
119
+ const state = inspectSqliteSchema(db);
120
+ if (state.currentVersion !== step.fromVersion || state.currentChecksum !== step.sourceChecksum) {
121
+ throw new SqliteSchemaError("minor upgrade source changed.");
122
+ }
123
+ db.exec(step.sql);
124
+ const { major, minor } = storageVersionParts(step.toVersion);
125
+ db.prepare("UPDATE storage_schema SET major=?,minor=?,checksum=? WHERE id=1")
126
+ .run(major, minor, step.targetChecksum);
1696
127
  }
1697
- // The table/index inventory describes the HEAD shape; only assert it once the
1698
- // migration has actually advanced to head (a deliberate partial target stops
1699
- // earlier and is validated when the real upgrade later completes it).
1700
- if (effectiveTarget >= CURRENT_STORAGE_VERSION)
1701
- validateSchemaObjects(db);
1702
- // The true head reached: the highest already-applied version or, when this
1703
- // run advanced the ledger, the last step it committed. Bounded runs report
1704
- // the intermediate head; an unbounded run reports CURRENT_STORAGE_VERSION.
1705
- const head = newlyApplied.at(-1) ?? applied.currentVersion;
1706
- return { newlyApplied, head };
1707
- };
1708
- const outcome = options.mode === "apply" && !db.inTransaction
1709
- ? db.transaction(migrate)()
1710
- : migrate();
1711
- return { applied: outcome.newlyApplied, version: outcome.head };
128
+ validateSqliteSchema(db);
129
+ })();
1712
130
  }
1713
- /** The names of every table the schema creates (for tests/introspection). */
1714
- export const SQLITE_SCHEMA_TABLES = [
1715
- "storage_migration_archive",
1716
- "plugin_intents",
1717
- "plugin_validations",
1718
- "local_resources",
1719
- "environment_preparations",
1720
- "schema_migrations",
1721
- "home_meta",
1722
- "config",
1723
- "configured_agents",
1724
- "agent_profiles",
1725
- "projects",
1726
- "global_roles",
1727
- "global_role_session_sets",
1728
- "global_role_messages",
1729
- "global_sequences",
1730
- "tasks_catalog",
1731
- "managed_workspaces",
1732
- "id_sequences",
1733
- "coordination_locks",
1734
- "durable_jobs",
1735
- "outbox",
1736
- "mailboxes",
1737
- "task_records",
1738
- "task_roles",
1739
- "role_session_sets",
1740
- "work_items",
1741
- "work_item_candidates",
1742
- "context_snapshots",
1743
- "turns",
1744
- "active_turns",
1745
- "review_rounds",
1746
- "change_sets",
1747
- "integration_attempts",
1748
- "messages",
1749
- "input_requests",
1750
- "decisions",
1751
- "milestones",
1752
- "events",
1753
- "task_projections",
1754
- "telemetry",
1755
- "telemetry_aggregate",
1756
- "capability_grants",
1757
- "release_workflows",
1758
- "publication_references",
1759
- "task_wakes",
1760
- "session_owners",
1761
- "runtime_session_candidates",
1762
- "resource_registry",
1763
- "gate_artifacts",
1764
- "gate_artifact_logs"
1765
- ];