@zq-silk/yui 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/agentConfigurationPicker.js +1 -1
- package/dist/cli/commandCatalog.js +2 -2
- package/dist/commands/taskWorkspaceCommands.js +11 -4
- package/dist/controller/controller.js +11 -5
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/resourceInventoryRpc.js +85 -0
- package/dist/controller/resourceInventoryWorker.js +50 -0
- package/dist/controller/runtime.js +82 -16
- package/dist/controller/runtimeEventProcessor.js +268 -0
- package/dist/core/boundedRpc.js +475 -0
- package/dist/repository/taskWorkspacePreparer.js +10 -7
- package/dist/setup/setupCommand.js +27 -4
- package/dist/storage/migration/productionRegistry.js +33 -0
- package/dist/storage/persistenceWorker.js +194 -0
- package/dist/storage/sqliteSchema.js +444 -0
- package/dist/storage/sqliteStore.js +1222 -0
- package/dist/storage/storageVersions.js +9 -2
- package/dist/storage/storeRpc.js +277 -0
- package/dist/storage/taskStore.js +9 -0
- package/dist/storage/upgrade/homeClassification.js +38 -11
- package/dist/storage/upgrade/sqliteMigrationTarget.js +299 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +457 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +41 -15
- package/package.json +2 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence Worker Thread (task-21, work-item-5).
|
|
3
|
+
*
|
|
4
|
+
* The worker owns the {@link SqliteTaskStore} connection: one writer connection
|
|
5
|
+
* (single-writer, `BEGIN IMMEDIATE`) plus a small read pool (separate WAL
|
|
6
|
+
* connections that never take the write lock, §3.2). The main thread never
|
|
7
|
+
* touches the db; it sends commands over a `MessageChannel` port and receives
|
|
8
|
+
* results.
|
|
9
|
+
*
|
|
10
|
+
* The port handshake, dispatch, cancellation observation, and error
|
|
11
|
+
* serialization are provided by the shared `core/boundedRpc` worker host
|
|
12
|
+
* (`runRpcWorker`); this module supplies the storage dialect:
|
|
13
|
+
*
|
|
14
|
+
* - `call` ......... a `TaskStore` method. Reads use the read pool; writes
|
|
15
|
+
* run on the writer inside a transaction and are made
|
|
16
|
+
* idempotent via the durable outbox (§5.4): a retried
|
|
17
|
+
* write after a crash returns `already-applied`.
|
|
18
|
+
* - `transaction` .. an ordered command batch inside one
|
|
19
|
+
* `BEGIN IMMEDIATE … COMMIT` on the writer (§3.2),
|
|
20
|
+
* yielding between commands so a `cancel` can roll back an
|
|
21
|
+
* open transaction (§3.1). Committed batches are not undone.
|
|
22
|
+
* - `observer` ..... a controller observer method hosted by the worker (the
|
|
23
|
+
* `FileSchedulerStoreAdapter` folds run here, off the main
|
|
24
|
+
* event loop).
|
|
25
|
+
* - `expectedRevision` enforces the global revision CAS in the same txn
|
|
26
|
+
* (§5.3); a mismatch fails with `StorageConflictError`.
|
|
27
|
+
*
|
|
28
|
+
* `synchronous=FULL` is never weakened; the worker opens the store with the same
|
|
29
|
+
* pragmas as the in-process path (WAL, FULL, busy_timeout).
|
|
30
|
+
*/
|
|
31
|
+
import { runRpcWorker } from "../core/boundedRpc.js";
|
|
32
|
+
import { SqliteTaskStore } from "./sqliteStore.js";
|
|
33
|
+
import { StorageRecordError } from "./taskStore.js";
|
|
34
|
+
// -- method invocation -------------------------------------------------------
|
|
35
|
+
function invokeMethod(target, method, args) {
|
|
36
|
+
const fn = target[method];
|
|
37
|
+
if (typeof fn !== "function") {
|
|
38
|
+
throw new StorageRecordError(`Unknown store method: ${method}`);
|
|
39
|
+
}
|
|
40
|
+
return fn.apply(target, args);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sentinel returned by a handler when the durable outbox already records the
|
|
44
|
+
* request's effect (§5.4). The host's `result` builder maps it to an
|
|
45
|
+
* `already-applied` response. Store results are plain JSON data, so a Symbol
|
|
46
|
+
* sentinel can never collide with a real result.
|
|
47
|
+
*/
|
|
48
|
+
const ALREADY_APPLIED = Symbol("already-applied");
|
|
49
|
+
const state = {
|
|
50
|
+
writer: undefined,
|
|
51
|
+
readers: [],
|
|
52
|
+
readIndex: 0,
|
|
53
|
+
observer: undefined,
|
|
54
|
+
cancelled: new Set()
|
|
55
|
+
};
|
|
56
|
+
function nextReader() {
|
|
57
|
+
const reader = state.readers[state.readIndex % state.readers.length];
|
|
58
|
+
state.readIndex += 1;
|
|
59
|
+
return reader;
|
|
60
|
+
}
|
|
61
|
+
// -- request handlers --------------------------------------------------------
|
|
62
|
+
async function handleInit(request) {
|
|
63
|
+
const writer = new SqliteTaskStore(request.home);
|
|
64
|
+
const poolSize = request.readPoolSize ?? 4;
|
|
65
|
+
const readers = [];
|
|
66
|
+
for (let index = 0; index < poolSize; index += 1) {
|
|
67
|
+
readers.push(new SqliteTaskStore(request.home));
|
|
68
|
+
}
|
|
69
|
+
state.writer = writer;
|
|
70
|
+
state.readers = readers;
|
|
71
|
+
if (request.observerModule !== undefined) {
|
|
72
|
+
const module = (await import(request.observerModule));
|
|
73
|
+
const Adapter = module.FileSchedulerStoreAdapter;
|
|
74
|
+
if (Adapter === undefined) {
|
|
75
|
+
throw new Error(`Observer module ${request.observerModule} does not export FileSchedulerStoreAdapter.`);
|
|
76
|
+
}
|
|
77
|
+
const host = new Adapter(writer);
|
|
78
|
+
state.observer = {
|
|
79
|
+
invoke(method, args) {
|
|
80
|
+
return invokeMethod(host, method, args);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function handleCall(request) {
|
|
86
|
+
const { requestId, method, args, readOnly } = request;
|
|
87
|
+
if (readOnly) {
|
|
88
|
+
return invokeMethod(nextReader(), method, args);
|
|
89
|
+
}
|
|
90
|
+
// Write. With a requestId, make it idempotent via the durable outbox
|
|
91
|
+
// (§5.4): skip if already committed, otherwise record the effect in the
|
|
92
|
+
// same transaction. The worker is single-threaded, so the check and the
|
|
93
|
+
// write are race-free; the UNIQUE constraint is the backstop.
|
|
94
|
+
const writer = state.writer;
|
|
95
|
+
if (writer === undefined)
|
|
96
|
+
throw new Error("Worker not initialized.");
|
|
97
|
+
if (writer.hasOutboxEntry(requestId)) {
|
|
98
|
+
return ALREADY_APPLIED;
|
|
99
|
+
}
|
|
100
|
+
return writer.transaction((store) => invokeMethod(store, method, args), { requestId });
|
|
101
|
+
}
|
|
102
|
+
async function handleTransaction(request) {
|
|
103
|
+
const { requestId, commands, expectedRevision } = request;
|
|
104
|
+
const writer = state.writer;
|
|
105
|
+
if (writer === undefined) {
|
|
106
|
+
throw new Error("Worker not initialized.");
|
|
107
|
+
}
|
|
108
|
+
// Idempotent replay: a batch that committed before a crash is not re-run.
|
|
109
|
+
if (writer.hasOutboxEntry(requestId)) {
|
|
110
|
+
return ALREADY_APPLIED;
|
|
111
|
+
}
|
|
112
|
+
const shouldCancel = () => state.cancelled.has(requestId);
|
|
113
|
+
try {
|
|
114
|
+
return await writer.transactionAsyncBatch(commands, {
|
|
115
|
+
requestId,
|
|
116
|
+
...(expectedRevision === undefined ? {} : { expectedRevision }),
|
|
117
|
+
shouldCancel
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
state.cancelled.delete(requestId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function handleObserver(request) {
|
|
125
|
+
if (state.observer === undefined) {
|
|
126
|
+
throw new Error("Worker has no observer host (observerModule not provided at init).");
|
|
127
|
+
}
|
|
128
|
+
return state.observer.invoke(request.method, request.args);
|
|
129
|
+
}
|
|
130
|
+
// -- worker host --------------------------------------------------------------
|
|
131
|
+
runRpcWorker({
|
|
132
|
+
serial: true,
|
|
133
|
+
kindOf: (request) => {
|
|
134
|
+
switch (request.kind) {
|
|
135
|
+
case "init":
|
|
136
|
+
return "init";
|
|
137
|
+
case "cancel":
|
|
138
|
+
return "cancel";
|
|
139
|
+
case "shutdown":
|
|
140
|
+
return "shutdown";
|
|
141
|
+
case "call":
|
|
142
|
+
case "transaction":
|
|
143
|
+
case "observer":
|
|
144
|
+
return "request";
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
requestIdOf: (request) => {
|
|
148
|
+
switch (request.kind) {
|
|
149
|
+
case "call":
|
|
150
|
+
case "transaction":
|
|
151
|
+
case "observer":
|
|
152
|
+
case "cancel":
|
|
153
|
+
return request.requestId;
|
|
154
|
+
case "init":
|
|
155
|
+
case "shutdown":
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
init: (request) => handleInit(request),
|
|
160
|
+
handle: (request) => {
|
|
161
|
+
switch (request.kind) {
|
|
162
|
+
case "call":
|
|
163
|
+
return handleCall(request);
|
|
164
|
+
case "transaction":
|
|
165
|
+
return handleTransaction(request);
|
|
166
|
+
case "observer":
|
|
167
|
+
return handleObserver(request);
|
|
168
|
+
case "init":
|
|
169
|
+
case "cancel":
|
|
170
|
+
case "shutdown":
|
|
171
|
+
throw new Error(`Unexpected request kind for handle: ${request.kind}`);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
cancel: (requestId) => {
|
|
175
|
+
state.cancelled.add(requestId);
|
|
176
|
+
},
|
|
177
|
+
shutdown: async () => {
|
|
178
|
+
for (const reader of state.readers) {
|
|
179
|
+
try {
|
|
180
|
+
reader.close();
|
|
181
|
+
}
|
|
182
|
+
catch { /* already closed */ }
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
state.writer?.close();
|
|
186
|
+
}
|
|
187
|
+
catch { /* already closed */ }
|
|
188
|
+
},
|
|
189
|
+
ready: () => ({ kind: "ready" }),
|
|
190
|
+
result: (requestId, value) => (value === ALREADY_APPLIED
|
|
191
|
+
? { kind: "already-applied", requestId }
|
|
192
|
+
: { kind: "result", requestId, result: value }),
|
|
193
|
+
error: (requestId, error) => ({ kind: "error", requestId, error })
|
|
194
|
+
});
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite WAL control-plane schema and migration runner (task-21, work-item-3).
|
|
3
|
+
*
|
|
4
|
+
* This is the normalized, `task_id`-partitioned schema that replaces the single
|
|
5
|
+
* aggregate `state.json` document. It implements the logical schema from
|
|
6
|
+
* `docs/sqlite-control-plane-design.md` §4 (31 tables) plus the cross-task
|
|
7
|
+
* coordination tables the design references in §5:
|
|
8
|
+
*
|
|
9
|
+
* - `global_sequences` (§5.3): global record ID high-water marks.
|
|
10
|
+
* - `outbox` (§5.4): durable outbox with `UNIQUE(request_id)` for exactly-once.
|
|
11
|
+
* - `config`: the `YuiConfig` singleton (the design keeps `home_meta` for
|
|
12
|
+
* identity/revision/versions; config is a separate singleton).
|
|
13
|
+
*
|
|
14
|
+
* Record payloads are stored two ways, per §4: typed columns for fields that are
|
|
15
|
+
* queried/filtered/used-for-CAS, and a `payload` JSON column holding the full
|
|
16
|
+
* versioned record (including its family `schemaVersion`). The record axis of
|
|
17
|
+
* the three-axis versioning is therefore unchanged.
|
|
18
|
+
*
|
|
19
|
+
* The migration runner is idempotent: it records applied versions in
|
|
20
|
+
* `schema_migrations` and uses `CREATE TABLE IF NOT EXISTS`, so re-running on an
|
|
21
|
+
* already-current database is a no-op and a crash mid-migration is rolled back
|
|
22
|
+
* by SQLite (DDL is transactional) and re-applied on the next open.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
/** The SQLite layout version (state.json layout 6 -> SQLite layout 7, per §8.1). */
|
|
26
|
+
export const SQLITE_LAYOUT_VERSION = 7;
|
|
27
|
+
/** The aggregate version of the normalized SQLite schema. */
|
|
28
|
+
export const SQLITE_AGGREGATE_VERSION = 1;
|
|
29
|
+
/** The current schema migration version. */
|
|
30
|
+
export const SQLITE_SCHEMA_VERSION = 1;
|
|
31
|
+
/** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
|
|
32
|
+
export const TELEMETRY_KEEP_PER_GENERATION = 200;
|
|
33
|
+
export const TELEMETRY_RUN_CAP = 50_000;
|
|
34
|
+
/**
|
|
35
|
+
* Version 1 migration: creates every table and index.
|
|
36
|
+
*
|
|
37
|
+
* `synchronous`/`foreign_keys`/`busy_timeout` are per-connection PRAGMAs set by
|
|
38
|
+
* the store; `journal_mode=WAL` is a persistent database property set on open.
|
|
39
|
+
* The migration itself only contains schema objects.
|
|
40
|
+
*/
|
|
41
|
+
const MIGRATION_1_SQL = `
|
|
42
|
+
-- Global catalog and coordination (§4.1) -------------------------------------
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS home_meta (
|
|
45
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
46
|
+
home_identity TEXT NOT NULL,
|
|
47
|
+
revision INTEGER NOT NULL,
|
|
48
|
+
layout_version INTEGER NOT NULL,
|
|
49
|
+
aggregate_version INTEGER NOT NULL,
|
|
50
|
+
created_at TEXT NOT NULL,
|
|
51
|
+
updated_at TEXT NOT NULL
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
55
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
56
|
+
payload TEXT NOT NULL,
|
|
57
|
+
updated_at TEXT NOT NULL
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS configured_agents (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
payload TEXT NOT NULL,
|
|
63
|
+
updated_at TEXT NOT NULL
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS agent_profiles (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
payload TEXT NOT NULL,
|
|
69
|
+
updated_at TEXT NOT NULL
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
name TEXT NOT NULL,
|
|
75
|
+
path TEXT NOT NULL,
|
|
76
|
+
payload TEXT NOT NULL,
|
|
77
|
+
created_at TEXT NOT NULL,
|
|
78
|
+
updated_at TEXT NOT NULL
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS global_roles (
|
|
82
|
+
name TEXT PRIMARY KEY,
|
|
83
|
+
payload TEXT NOT NULL,
|
|
84
|
+
updated_at TEXT NOT NULL
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS global_role_session_sets (
|
|
88
|
+
name TEXT PRIMARY KEY,
|
|
89
|
+
payload TEXT NOT NULL,
|
|
90
|
+
updated_at TEXT NOT NULL,
|
|
91
|
+
FOREIGN KEY (name) REFERENCES global_roles(name)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
-- Global record ID high-water marks (§5.3).
|
|
95
|
+
CREATE TABLE IF NOT EXISTS global_sequences (
|
|
96
|
+
name TEXT PRIMARY KEY,
|
|
97
|
+
high_water INTEGER NOT NULL
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
-- Task catalog: the global active index and lifecycle lookup.
|
|
101
|
+
CREATE TABLE IF NOT EXISTS tasks_catalog (
|
|
102
|
+
task_id TEXT PRIMARY KEY,
|
|
103
|
+
status TEXT NOT NULL,
|
|
104
|
+
lifecycle TEXT NOT NULL,
|
|
105
|
+
is_active INTEGER NOT NULL CHECK (is_active IN (0,1)),
|
|
106
|
+
created_at TEXT NOT NULL,
|
|
107
|
+
updated_at TEXT NOT NULL
|
|
108
|
+
);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_active ON tasks_catalog(is_active) WHERE is_active = 1;
|
|
110
|
+
|
|
111
|
+
-- Session/Workspace ownership is global (workspaces outlive task activity).
|
|
112
|
+
CREATE TABLE IF NOT EXISTS managed_workspaces (
|
|
113
|
+
owner_kind TEXT NOT NULL CHECK (owner_kind IN
|
|
114
|
+
('task','work-item','review-round','integration-attempt','execution-lane')),
|
|
115
|
+
owner_id TEXT NOT NULL,
|
|
116
|
+
task_id TEXT,
|
|
117
|
+
path TEXT NOT NULL,
|
|
118
|
+
payload TEXT NOT NULL,
|
|
119
|
+
status TEXT NOT NULL,
|
|
120
|
+
created_at TEXT NOT NULL,
|
|
121
|
+
updated_at TEXT NOT NULL,
|
|
122
|
+
PRIMARY KEY (owner_kind, owner_id)
|
|
123
|
+
);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_workspaces_task ON managed_workspaces(task_id);
|
|
125
|
+
|
|
126
|
+
-- Per-task record ID high-water marks (replaces StoredTask.idHighWaterMarks).
|
|
127
|
+
CREATE TABLE IF NOT EXISTS id_sequences (
|
|
128
|
+
task_id TEXT NOT NULL,
|
|
129
|
+
kind TEXT NOT NULL,
|
|
130
|
+
high_water INTEGER NOT NULL,
|
|
131
|
+
PRIMARY KEY (task_id, kind)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
-- Cross-task coordination: Project locks and the Integration queue.
|
|
135
|
+
CREATE TABLE IF NOT EXISTS coordination_locks (
|
|
136
|
+
lock_key TEXT PRIMARY KEY,
|
|
137
|
+
holder_task TEXT NOT NULL,
|
|
138
|
+
holder_ref TEXT NOT NULL,
|
|
139
|
+
acquired_at TEXT NOT NULL,
|
|
140
|
+
expires_at TEXT
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS integration_queue (
|
|
144
|
+
queue_id TEXT PRIMARY KEY,
|
|
145
|
+
task_id TEXT NOT NULL,
|
|
146
|
+
project_id TEXT NOT NULL,
|
|
147
|
+
change_set TEXT NOT NULL,
|
|
148
|
+
status TEXT NOT NULL,
|
|
149
|
+
payload TEXT NOT NULL,
|
|
150
|
+
created_at TEXT NOT NULL,
|
|
151
|
+
updated_at TEXT NOT NULL
|
|
152
|
+
);
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_integration_queue_status ON integration_queue(status, created_at);
|
|
154
|
+
|
|
155
|
+
-- Durable outbox (§5.4): UNIQUE(request_id) makes cross-task effects exactly-once.
|
|
156
|
+
CREATE TABLE IF NOT EXISTS outbox (
|
|
157
|
+
outbox_id INTEGER PRIMARY KEY,
|
|
158
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
159
|
+
command TEXT NOT NULL,
|
|
160
|
+
state TEXT NOT NULL DEFAULT 'pending',
|
|
161
|
+
created_at TEXT NOT NULL,
|
|
162
|
+
applied_at TEXT
|
|
163
|
+
);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_outbox_state ON outbox(state, created_at);
|
|
165
|
+
|
|
166
|
+
-- Mailboxes (§4.2): per-target ordering and exactly-once signals. -------------
|
|
167
|
+
|
|
168
|
+
CREATE TABLE IF NOT EXISTS mailboxes (
|
|
169
|
+
mailbox_id INTEGER PRIMARY KEY,
|
|
170
|
+
target_kind TEXT NOT NULL CHECK (target_kind IN
|
|
171
|
+
('task','role','role-runtime','global-role-runtime','operator')),
|
|
172
|
+
task_id TEXT,
|
|
173
|
+
role_name TEXT,
|
|
174
|
+
-- The stable mailboxTargetKey string carries the real uniqueness: the column
|
|
175
|
+
-- UNIQUE below cannot, because NULL task_id/role_name (the 'operator' and
|
|
176
|
+
-- 'global-role-runtime' targets) are distinct under SQL NULL semantics.
|
|
177
|
+
target_key TEXT NOT NULL,
|
|
178
|
+
next_sequence INTEGER NOT NULL,
|
|
179
|
+
processing TEXT,
|
|
180
|
+
pending TEXT,
|
|
181
|
+
UNIQUE (target_kind, task_id, role_name)
|
|
182
|
+
);
|
|
183
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_mailboxes_target_key
|
|
184
|
+
ON mailboxes(target_key);
|
|
185
|
+
|
|
186
|
+
-- Signals are the durable append log; (mailbox, sequence) is the exactly-once key.
|
|
187
|
+
CREATE TABLE IF NOT EXISTS mailbox_signals (
|
|
188
|
+
mailbox_id INTEGER NOT NULL,
|
|
189
|
+
sequence INTEGER NOT NULL,
|
|
190
|
+
reason TEXT NOT NULL,
|
|
191
|
+
ref_type TEXT,
|
|
192
|
+
ref_task_id TEXT,
|
|
193
|
+
ref_id TEXT,
|
|
194
|
+
occurred_at TEXT NOT NULL,
|
|
195
|
+
request_id TEXT NOT NULL,
|
|
196
|
+
PRIMARY KEY (mailbox_id, sequence),
|
|
197
|
+
FOREIGN KEY (mailbox_id) REFERENCES mailboxes(mailbox_id)
|
|
198
|
+
);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_mailbox_signals_request ON mailbox_signals(request_id);
|
|
200
|
+
|
|
201
|
+
-- Task-partitioned tables (§4.3). Every task-local read constrains task_id. ----
|
|
202
|
+
|
|
203
|
+
CREATE TABLE IF NOT EXISTS task_records (
|
|
204
|
+
task_id TEXT PRIMARY KEY,
|
|
205
|
+
payload TEXT NOT NULL,
|
|
206
|
+
brief TEXT,
|
|
207
|
+
updated_at TEXT NOT NULL,
|
|
208
|
+
FOREIGN KEY (task_id) REFERENCES tasks_catalog(task_id)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
CREATE TABLE IF NOT EXISTS task_roles (
|
|
212
|
+
task_id TEXT NOT NULL,
|
|
213
|
+
role_name TEXT NOT NULL,
|
|
214
|
+
payload TEXT NOT NULL,
|
|
215
|
+
updated_at TEXT NOT NULL,
|
|
216
|
+
PRIMARY KEY (task_id, role_name)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
CREATE TABLE IF NOT EXISTS role_session_sets (
|
|
220
|
+
task_id TEXT NOT NULL,
|
|
221
|
+
role_name TEXT NOT NULL,
|
|
222
|
+
payload TEXT NOT NULL,
|
|
223
|
+
updated_at TEXT NOT NULL,
|
|
224
|
+
PRIMARY KEY (task_id, role_name)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
CREATE TABLE IF NOT EXISTS work_items (
|
|
228
|
+
task_id TEXT NOT NULL,
|
|
229
|
+
work_item_id TEXT NOT NULL,
|
|
230
|
+
status TEXT NOT NULL,
|
|
231
|
+
payload TEXT NOT NULL,
|
|
232
|
+
updated_at TEXT NOT NULL,
|
|
233
|
+
PRIMARY KEY (task_id, work_item_id)
|
|
234
|
+
);
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(task_id, status);
|
|
236
|
+
|
|
237
|
+
CREATE TABLE IF NOT EXISTS work_item_candidates (
|
|
238
|
+
task_id TEXT NOT NULL,
|
|
239
|
+
candidate_id TEXT NOT NULL,
|
|
240
|
+
work_item_id TEXT NOT NULL,
|
|
241
|
+
payload TEXT NOT NULL,
|
|
242
|
+
created_at TEXT NOT NULL,
|
|
243
|
+
PRIMARY KEY (task_id, candidate_id)
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
CREATE TABLE IF NOT EXISTS agent_runs (
|
|
247
|
+
task_id TEXT NOT NULL,
|
|
248
|
+
run_id TEXT NOT NULL,
|
|
249
|
+
role_name TEXT NOT NULL,
|
|
250
|
+
status TEXT NOT NULL,
|
|
251
|
+
payload TEXT NOT NULL,
|
|
252
|
+
updated_at TEXT NOT NULL,
|
|
253
|
+
PRIMARY KEY (task_id, run_id)
|
|
254
|
+
);
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_role_status ON agent_runs(task_id, role_name, status);
|
|
256
|
+
|
|
257
|
+
-- Active-run pointers (getActiveAgentRun / execution-lane runs).
|
|
258
|
+
CREATE TABLE IF NOT EXISTS active_runs (
|
|
259
|
+
task_id TEXT NOT NULL,
|
|
260
|
+
pointer TEXT NOT NULL,
|
|
261
|
+
run_id TEXT NOT NULL,
|
|
262
|
+
payload TEXT NOT NULL,
|
|
263
|
+
updated_at TEXT NOT NULL,
|
|
264
|
+
PRIMARY KEY (task_id, pointer)
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
CREATE TABLE IF NOT EXISTS review_rounds (
|
|
268
|
+
task_id TEXT NOT NULL,
|
|
269
|
+
review_round_id TEXT NOT NULL,
|
|
270
|
+
status TEXT NOT NULL,
|
|
271
|
+
payload TEXT NOT NULL,
|
|
272
|
+
updated_at TEXT NOT NULL,
|
|
273
|
+
PRIMARY KEY (task_id, review_round_id)
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
CREATE TABLE IF NOT EXISTS change_sets (
|
|
277
|
+
task_id TEXT NOT NULL,
|
|
278
|
+
change_set_id TEXT NOT NULL,
|
|
279
|
+
project_id TEXT NOT NULL,
|
|
280
|
+
head_sha TEXT NOT NULL,
|
|
281
|
+
payload TEXT NOT NULL,
|
|
282
|
+
created_at TEXT NOT NULL,
|
|
283
|
+
PRIMARY KEY (task_id, change_set_id)
|
|
284
|
+
);
|
|
285
|
+
CREATE INDEX IF NOT EXISTS idx_change_sets_project ON change_sets(task_id, project_id);
|
|
286
|
+
|
|
287
|
+
CREATE TABLE IF NOT EXISTS integration_attempts (
|
|
288
|
+
task_id TEXT NOT NULL,
|
|
289
|
+
integration_id TEXT NOT NULL,
|
|
290
|
+
status TEXT NOT NULL,
|
|
291
|
+
payload TEXT NOT NULL,
|
|
292
|
+
updated_at TEXT NOT NULL,
|
|
293
|
+
PRIMARY KEY (task_id, integration_id)
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
297
|
+
task_id TEXT NOT NULL,
|
|
298
|
+
message_id TEXT NOT NULL,
|
|
299
|
+
seq INTEGER NOT NULL,
|
|
300
|
+
payload TEXT NOT NULL,
|
|
301
|
+
created_at TEXT NOT NULL,
|
|
302
|
+
PRIMARY KEY (task_id, message_id)
|
|
303
|
+
);
|
|
304
|
+
CREATE INDEX IF NOT EXISTS idx_messages_seq ON messages(task_id, seq);
|
|
305
|
+
|
|
306
|
+
CREATE TABLE IF NOT EXISTS input_requests (
|
|
307
|
+
task_id TEXT NOT NULL,
|
|
308
|
+
input_id TEXT NOT NULL,
|
|
309
|
+
status TEXT NOT NULL,
|
|
310
|
+
blocks TEXT,
|
|
311
|
+
payload TEXT NOT NULL,
|
|
312
|
+
created_at TEXT NOT NULL,
|
|
313
|
+
updated_at TEXT NOT NULL,
|
|
314
|
+
PRIMARY KEY (task_id, input_id)
|
|
315
|
+
);
|
|
316
|
+
CREATE INDEX IF NOT EXISTS idx_input_open ON input_requests(task_id, status) WHERE status <> 'resolved';
|
|
317
|
+
|
|
318
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
319
|
+
task_id TEXT NOT NULL,
|
|
320
|
+
decision_id TEXT NOT NULL,
|
|
321
|
+
payload TEXT NOT NULL,
|
|
322
|
+
created_at TEXT NOT NULL,
|
|
323
|
+
PRIMARY KEY (task_id, decision_id)
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
CREATE TABLE IF NOT EXISTS milestones (
|
|
327
|
+
task_id TEXT NOT NULL,
|
|
328
|
+
milestone_id TEXT NOT NULL,
|
|
329
|
+
payload TEXT NOT NULL,
|
|
330
|
+
created_at TEXT NOT NULL,
|
|
331
|
+
PRIMARY KEY (task_id, milestone_id)
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
-- Terminal/semantic events: retained individually, never pruned.
|
|
335
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
336
|
+
task_id TEXT NOT NULL,
|
|
337
|
+
event_id TEXT NOT NULL,
|
|
338
|
+
type TEXT NOT NULL,
|
|
339
|
+
occurred_at TEXT NOT NULL,
|
|
340
|
+
payload TEXT NOT NULL,
|
|
341
|
+
PRIMARY KEY (task_id, event_id)
|
|
342
|
+
);
|
|
343
|
+
CREATE INDEX IF NOT EXISTS idx_events_type_time ON events(task_id, type, occurred_at);
|
|
344
|
+
|
|
345
|
+
-- Per-task scheduler projections (leaderFailure, operatorNotification).
|
|
346
|
+
CREATE TABLE IF NOT EXISTS task_projections (
|
|
347
|
+
task_id TEXT NOT NULL,
|
|
348
|
+
kind TEXT NOT NULL CHECK (kind IN ('leader-failure','operator-notification')),
|
|
349
|
+
payload TEXT,
|
|
350
|
+
updated_at TEXT NOT NULL,
|
|
351
|
+
PRIMARY KEY (task_id, kind)
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
-- Telemetry (§4.4): bounded, latest-per-key. WITHOUT ROWID, PK is the key.
|
|
355
|
+
CREATE TABLE IF NOT EXISTS telemetry (
|
|
356
|
+
task_id TEXT NOT NULL,
|
|
357
|
+
role_name TEXT NOT NULL,
|
|
358
|
+
run_id TEXT NOT NULL,
|
|
359
|
+
generation TEXT NOT NULL,
|
|
360
|
+
progress_id TEXT NOT NULL,
|
|
361
|
+
sequence INTEGER,
|
|
362
|
+
payload TEXT NOT NULL,
|
|
363
|
+
received_at TEXT NOT NULL,
|
|
364
|
+
PRIMARY KEY (task_id, role_name, run_id, generation, progress_id)
|
|
365
|
+
) WITHOUT ROWID;
|
|
366
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_run ON telemetry(task_id, run_id);
|
|
367
|
+
`;
|
|
368
|
+
const MIGRATIONS = [
|
|
369
|
+
{ version: 1, axis: "layout", sql: MIGRATION_1_SQL }
|
|
370
|
+
];
|
|
371
|
+
function checksum(sql) {
|
|
372
|
+
return createHash("sha256").update(sql).digest("hex");
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Apply pending migrations idempotently inside transactions.
|
|
376
|
+
*
|
|
377
|
+
* Each migration runs in its own transaction: the DDL and the
|
|
378
|
+
* `schema_migrations` bookkeeping commit atomically, so a crash mid-migration
|
|
379
|
+
* rolls back and the next open re-applies cleanly. Re-running on a current
|
|
380
|
+
* database performs no work (every version is already recorded).
|
|
381
|
+
*/
|
|
382
|
+
export function migrateSqliteSchema(db) {
|
|
383
|
+
db.exec(`
|
|
384
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
385
|
+
version INTEGER PRIMARY KEY,
|
|
386
|
+
axis TEXT NOT NULL CHECK (axis IN ('layout','aggregate','record')),
|
|
387
|
+
record_kind TEXT,
|
|
388
|
+
applied_at TEXT NOT NULL,
|
|
389
|
+
checksum TEXT NOT NULL
|
|
390
|
+
)
|
|
391
|
+
`);
|
|
392
|
+
const applied = new Set(db.prepare("SELECT version FROM schema_migrations").all().map((row) => row.version));
|
|
393
|
+
const newlyApplied = [];
|
|
394
|
+
for (const migration of MIGRATIONS) {
|
|
395
|
+
if (applied.has(migration.version))
|
|
396
|
+
continue;
|
|
397
|
+
const apply = db.transaction(() => {
|
|
398
|
+
db.exec(migration.sql);
|
|
399
|
+
db.prepare(`INSERT INTO schema_migrations (version, axis, record_kind, applied_at, checksum)
|
|
400
|
+
VALUES (?, ?, ?, ?, ?)`).run(migration.version, migration.axis, migration.recordKind ?? null, new Date().toISOString(), checksum(migration.sql));
|
|
401
|
+
});
|
|
402
|
+
apply();
|
|
403
|
+
newlyApplied.push(migration.version);
|
|
404
|
+
}
|
|
405
|
+
const version = MIGRATIONS.reduce((max, m) => Math.max(max, m.version), 0);
|
|
406
|
+
return { applied: newlyApplied, version };
|
|
407
|
+
}
|
|
408
|
+
/** The names of every table the schema creates (for tests/introspection). */
|
|
409
|
+
export const SQLITE_SCHEMA_TABLES = [
|
|
410
|
+
"schema_migrations",
|
|
411
|
+
"home_meta",
|
|
412
|
+
"config",
|
|
413
|
+
"configured_agents",
|
|
414
|
+
"agent_profiles",
|
|
415
|
+
"projects",
|
|
416
|
+
"global_roles",
|
|
417
|
+
"global_role_session_sets",
|
|
418
|
+
"global_sequences",
|
|
419
|
+
"tasks_catalog",
|
|
420
|
+
"managed_workspaces",
|
|
421
|
+
"id_sequences",
|
|
422
|
+
"coordination_locks",
|
|
423
|
+
"integration_queue",
|
|
424
|
+
"outbox",
|
|
425
|
+
"mailboxes",
|
|
426
|
+
"mailbox_signals",
|
|
427
|
+
"task_records",
|
|
428
|
+
"task_roles",
|
|
429
|
+
"role_session_sets",
|
|
430
|
+
"work_items",
|
|
431
|
+
"work_item_candidates",
|
|
432
|
+
"agent_runs",
|
|
433
|
+
"active_runs",
|
|
434
|
+
"review_rounds",
|
|
435
|
+
"change_sets",
|
|
436
|
+
"integration_attempts",
|
|
437
|
+
"messages",
|
|
438
|
+
"input_requests",
|
|
439
|
+
"decisions",
|
|
440
|
+
"milestones",
|
|
441
|
+
"events",
|
|
442
|
+
"task_projections",
|
|
443
|
+
"telemetry"
|
|
444
|
+
];
|