@tomflow/proflow-task-store-sqlite 0.1.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.
@@ -0,0 +1,894 @@
1
+ const fixedAgentPackageRefs = new Set([
2
+ "@tomflow/proflow-agent-product",
3
+ "@tomflow/proflow-agent-controller-dev",
4
+ "@tomflow/proflow-agent-test-ops",
5
+ ]);
6
+ function tableColumns(database, table) {
7
+ return new Set(database.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
8
+ }
9
+ function resolveLegacyAgentPackageRef(legacyRoleRef, context) {
10
+ if (fixedAgentPackageRefs.has(legacyRoleRef))
11
+ return legacyRoleRef;
12
+ const mapped = context.legacyRoleMap?.[legacyRoleRef];
13
+ if (mapped && fixedAgentPackageRefs.has(mapped))
14
+ return mapped;
15
+ const descriptor = context.legacyRoleBindings?.[legacyRoleRef];
16
+ if (descriptor && fixedAgentPackageRefs.has(descriptor.agentPackageRef))
17
+ return descriptor.agentPackageRef;
18
+ throw new Error(`LEGACY_TASK_ROLE_MAPPING_REQUIRED:${legacyRoleRef}`);
19
+ }
20
+ function tableExists(database, table) {
21
+ return Boolean(database
22
+ .prepare("SELECT 1 AS present FROM sqlite_master WHERE type='table' AND name=?")
23
+ .get(table));
24
+ }
25
+ function tableInfo(database, table) {
26
+ return database.prepare(`PRAGMA table_info(${table})`).all();
27
+ }
28
+ function columnInfo(database, table, column) {
29
+ return tableInfo(database, table).find((item) => item.name === column);
30
+ }
31
+ function resolveLegacyRoleBinding(legacyRoleRef, agentPackageRef, context) {
32
+ const descriptor = context.legacyRoleBindings?.[legacyRoleRef] ??
33
+ context.legacyRoleBindings?.[agentPackageRef];
34
+ if (!descriptor)
35
+ throw new Error(`LEGACY_TASK_BINDING_MAPPING_REQUIRED:${legacyRoleRef}`);
36
+ if (descriptor.agentPackageRef !== agentPackageRef)
37
+ throw new Error(`LEGACY_TASK_BINDING_MAPPING_CONFLICT:${legacyRoleRef}`);
38
+ if (!fixedAgentPackageRefs.has(descriptor.agentPackageRef))
39
+ throw new Error(`LEGACY_TASK_ROLE_MAPPING_REQUIRED:${legacyRoleRef}`);
40
+ if (!descriptor.roleRef)
41
+ throw new Error(`LEGACY_TASK_BINDING_MAPPING_REQUIRED:${legacyRoleRef}`);
42
+ return descriptor;
43
+ }
44
+ function canonicalizeLegacyNodes(database, context) {
45
+ if (!tableExists(database, "nodes"))
46
+ throw new Error("LEGACY_TASK_SCHEMA_UNSUPPORTED:nodes");
47
+ let columns = tableColumns(database, "nodes");
48
+ if (!columns.has("required_role_ref") &&
49
+ !columns.has("required_agent_package_ref"))
50
+ throw new Error("LEGACY_TASK_SCHEMA_UNSUPPORTED:nodes");
51
+ const legacyRows = database
52
+ .prepare(columns.has("required_role_ref")
53
+ ? "SELECT node_id, task_id, required_role_ref AS legacy_ref, required_agent_package_ref AS current_ref, worker_ref FROM nodes".replace(", required_agent_package_ref AS current_ref", columns.has("required_agent_package_ref")
54
+ ? ", required_agent_package_ref AS current_ref"
55
+ : ", NULL AS current_ref")
56
+ : "SELECT node_id, task_id, required_agent_package_ref AS legacy_ref, required_agent_package_ref AS current_ref, worker_ref FROM nodes")
57
+ .all();
58
+ const resolved = legacyRows.map((row) => {
59
+ const sourceRef = row.current_ref && fixedAgentPackageRefs.has(row.current_ref)
60
+ ? row.current_ref
61
+ : row.legacy_ref;
62
+ if (!sourceRef)
63
+ throw new Error("LEGACY_TASK_SCHEMA_UPGRADE_INCOMPLETE:nodes");
64
+ return {
65
+ nodeId: row.node_id,
66
+ taskId: row.task_id,
67
+ legacyRoleRef: row.legacy_ref ?? sourceRef,
68
+ agentPackageRef: resolveLegacyAgentPackageRef(sourceRef, context),
69
+ workerRef: row.worker_ref,
70
+ };
71
+ });
72
+ if (columns.has("required_role_ref")) {
73
+ const updateLegacy = database.prepare("UPDATE nodes SET required_role_ref=? WHERE node_id=?");
74
+ for (const row of resolved)
75
+ updateLegacy.run(row.agentPackageRef, row.nodeId);
76
+ if (columns.has("required_agent_package_ref"))
77
+ database.exec("ALTER TABLE nodes DROP COLUMN required_agent_package_ref");
78
+ database.exec("ALTER TABLE nodes RENAME COLUMN required_role_ref TO required_agent_package_ref");
79
+ columns = tableColumns(database, "nodes");
80
+ }
81
+ else {
82
+ const updateCanonical = database.prepare("UPDATE nodes SET required_agent_package_ref=? WHERE node_id=?");
83
+ for (const row of resolved)
84
+ updateCanonical.run(row.agentPackageRef, row.nodeId);
85
+ }
86
+ const requiredPackageColumn = columnInfo(database, "nodes", "required_agent_package_ref");
87
+ if (requiredPackageColumn?.notnull !== 1 || columns.has("required_role_ref"))
88
+ throw new Error("LEGACY_TASK_SCHEMA_UPGRADE_INCOMPLETE:nodes_constraints");
89
+ return resolved.map(({ nodeId: _nodeId, ...row }) => row);
90
+ }
91
+ function createCanonicalTaskRoleBindings(database) {
92
+ database.exec(`
93
+ CREATE TABLE task_role_bindings (
94
+ task_id TEXT NOT NULL,
95
+ agent_package_ref TEXT NOT NULL,
96
+ role_ref TEXT NOT NULL,
97
+ worker_ref TEXT,
98
+ conversation_locator TEXT,
99
+ version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
100
+ created_at TEXT NOT NULL,
101
+ updated_at TEXT NOT NULL,
102
+ PRIMARY KEY (task_id, agent_package_ref),
103
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
104
+ );`);
105
+ }
106
+ function canonicalizeLegacyBindings(database, context, nodeFacts) {
107
+ if (!tableExists(database, "task_role_bindings")) {
108
+ createCanonicalTaskRoleBindings(database);
109
+ const tasks = database
110
+ .prepare("SELECT task_id, created_at, updated_at FROM tasks ORDER BY task_id")
111
+ .all();
112
+ const insert = database.prepare("INSERT INTO task_role_bindings(task_id,agent_package_ref,role_ref,worker_ref,conversation_locator,version,created_at,updated_at) VALUES (?,?,?,?,?,1,?,?)");
113
+ for (const task of tasks) {
114
+ for (const agentPackageRef of fixedAgentPackageRefs) {
115
+ const facts = nodeFacts.filter((fact) => fact.taskId === task.task_id &&
116
+ fact.agentPackageRef === agentPackageRef);
117
+ const legacyRoleRefs = new Set(facts.map((fact) => fact.legacyRoleRef));
118
+ if (legacyRoleRefs.size > 1)
119
+ throw new Error("LEGACY_TASK_BINDING_AMBIGUOUS:role");
120
+ const workerRefs = new Set(facts
121
+ .map((fact) => fact.workerRef)
122
+ .filter((workerRef) => workerRef !== null));
123
+ if (workerRefs.size > 1)
124
+ throw new Error("LEGACY_TASK_BINDING_AMBIGUOUS:worker");
125
+ const legacyRoleRef = [...legacyRoleRefs][0] ?? agentPackageRef;
126
+ const descriptor = resolveLegacyRoleBinding(legacyRoleRef, agentPackageRef, context);
127
+ insert.run(task.task_id, agentPackageRef, descriptor.roleRef, [...workerRefs][0] ?? null, descriptor.conversationLocator ?? null, task.created_at, task.updated_at);
128
+ }
129
+ }
130
+ return;
131
+ }
132
+ const info = tableInfo(database, "task_role_bindings");
133
+ const names = new Set(info.map((item) => item.name));
134
+ const agentPackage = info.find((item) => item.name === "agent_package_ref");
135
+ const taskId = info.find((item) => item.name === "task_id");
136
+ const canonical = names.has("conversation_locator") &&
137
+ agentPackage?.notnull === 1 &&
138
+ agentPackage.pk === 2 &&
139
+ taskId?.pk === 1;
140
+ if (canonical) {
141
+ const rows = database
142
+ .prepare("SELECT task_id,agent_package_ref,role_ref FROM task_role_bindings")
143
+ .all();
144
+ for (const row of rows)
145
+ if (!fixedAgentPackageRefs.has(row.agent_package_ref))
146
+ throw new Error(`LEGACY_TASK_ROLE_MAPPING_REQUIRED:${row.agent_package_ref || row.role_ref}`);
147
+ return;
148
+ }
149
+ const hasAgentPackage = names.has("agent_package_ref");
150
+ const hasConversationLocator = names.has("conversation_locator");
151
+ const rows = database
152
+ .prepare(`SELECT task_id, role_ref, worker_ref, version, created_at, updated_at, ${hasAgentPackage ? "agent_package_ref" : "NULL"} AS agent_package_ref, ${hasConversationLocator ? "conversation_locator" : "NULL"} AS conversation_locator FROM task_role_bindings`)
153
+ .all();
154
+ database.exec("ALTER TABLE task_role_bindings RENAME TO task_role_bindings_legacy_v3");
155
+ createCanonicalTaskRoleBindings(database);
156
+ const insert = database.prepare("INSERT INTO task_role_bindings(task_id,agent_package_ref,role_ref,worker_ref,conversation_locator,version,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)");
157
+ for (const row of rows) {
158
+ const sourceRef = row.agent_package_ref ?? row.role_ref;
159
+ let packageRef;
160
+ try {
161
+ packageRef = resolveLegacyAgentPackageRef(sourceRef, context);
162
+ }
163
+ catch {
164
+ packageRef = resolveLegacyAgentPackageRef(row.role_ref, context);
165
+ }
166
+ insert.run(row.task_id, packageRef, row.role_ref, row.worker_ref, row.conversation_locator, row.version, row.created_at, row.updated_at);
167
+ }
168
+ database.exec("DROP TABLE task_role_bindings_legacy_v3");
169
+ }
170
+ function isHistorical20260810Shape(database) {
171
+ if (!tableExists(database, "nodes"))
172
+ return false;
173
+ const nodeColumns = tableColumns(database, "nodes");
174
+ return (nodeColumns.has("required_role_ref") &&
175
+ !nodeColumns.has("required_agent_package_ref") &&
176
+ !tableExists(database, "task_role_bindings"));
177
+ }
178
+ function rebuildHistorical20260810Schema(database, context) {
179
+ const requiredHistoricalTables = [
180
+ "task_groups",
181
+ "tasks",
182
+ "nodes",
183
+ "node_execution_history",
184
+ "task_documents",
185
+ "task_messages",
186
+ "task_events",
187
+ "idempotency_records",
188
+ ];
189
+ for (const table of requiredHistoricalTables)
190
+ if (!tableExists(database, table))
191
+ throw new Error(`LEGACY_TASK_SCHEMA_UNSUPPORTED:${table}`);
192
+ const historicalNodes = database
193
+ .prepare("SELECT node_id,task_id,required_role_ref,worker_ref FROM nodes ORDER BY task_id,sequence_no,node_id")
194
+ .all();
195
+ const nodeFacts = historicalNodes.map((row) => ({
196
+ nodeId: row.node_id,
197
+ taskId: row.task_id,
198
+ legacyRoleRef: row.required_role_ref,
199
+ agentPackageRef: resolveLegacyAgentPackageRef(row.required_role_ref, context),
200
+ workerRef: row.worker_ref,
201
+ }));
202
+ database.exec(`
203
+ CREATE TABLE task_groups_v3 (
204
+ task_group_id TEXT PRIMARY KEY,
205
+ title TEXT NOT NULL,
206
+ objective TEXT,
207
+ status TEXT NOT NULL CHECK (status IN ('READY','ACTIVE','SUCCEEDED')),
208
+ max_active_tasks INTEGER NOT NULL DEFAULT 1 CHECK (max_active_tasks = 1),
209
+ version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
210
+ created_by_ref TEXT NOT NULL,
211
+ created_at TEXT NOT NULL,
212
+ updated_at TEXT NOT NULL
213
+ );
214
+ CREATE TABLE tasks_v3 (
215
+ task_id TEXT PRIMARY KEY,
216
+ task_group_id TEXT,
217
+ sequence_no INTEGER,
218
+ title TEXT NOT NULL,
219
+ objective TEXT NOT NULL,
220
+ status TEXT NOT NULL CHECK (status IN ('PENDING','READY','ACTIVE','WAITING','FAILED','PAUSED','SUCCEEDED','TERMINATED')),
221
+ version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
222
+ plan_version INTEGER NOT NULL DEFAULT 1,
223
+ current_node_id TEXT,
224
+ created_by_ref TEXT NOT NULL,
225
+ created_at TEXT NOT NULL,
226
+ started_at TEXT,
227
+ completed_at TEXT,
228
+ updated_at TEXT NOT NULL,
229
+ FOREIGN KEY (task_group_id) REFERENCES task_groups_v3(task_group_id),
230
+ UNIQUE(task_group_id, sequence_no)
231
+ );
232
+ CREATE TABLE nodes_v3 (
233
+ node_id TEXT PRIMARY KEY,
234
+ task_id TEXT NOT NULL,
235
+ sequence_no INTEGER NOT NULL,
236
+ title TEXT NOT NULL,
237
+ objective TEXT NOT NULL,
238
+ status TEXT NOT NULL CHECK (status IN ('PENDING','READY','IN_PROGRESS','WAITING','FAILED','SUCCEEDED','TERMINATED')),
239
+ version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
240
+ run_no INTEGER NOT NULL DEFAULT 1 CHECK (run_no >= 1),
241
+ required_agent_package_ref TEXT NOT NULL,
242
+ worker_ref TEXT,
243
+ input_documents_json TEXT NOT NULL DEFAULT '[]',
244
+ output_documents_json TEXT NOT NULL DEFAULT '[]',
245
+ result_summary TEXT,
246
+ error_code TEXT,
247
+ error_message TEXT,
248
+ error_retryable INTEGER,
249
+ started_at TEXT,
250
+ completed_at TEXT,
251
+ updated_at TEXT NOT NULL,
252
+ FOREIGN KEY (task_id) REFERENCES tasks_v3(task_id) ON DELETE CASCADE,
253
+ UNIQUE(task_id, sequence_no)
254
+ );
255
+ CREATE TABLE task_role_bindings_v3 (
256
+ task_id TEXT NOT NULL,
257
+ agent_package_ref TEXT NOT NULL,
258
+ role_ref TEXT NOT NULL,
259
+ worker_ref TEXT,
260
+ conversation_locator TEXT,
261
+ version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
262
+ created_at TEXT NOT NULL,
263
+ updated_at TEXT NOT NULL,
264
+ PRIMARY KEY (task_id, agent_package_ref),
265
+ FOREIGN KEY (task_id) REFERENCES tasks_v3(task_id) ON DELETE CASCADE
266
+ );
267
+ CREATE TABLE node_execution_history_v3 (
268
+ execution_id INTEGER PRIMARY KEY AUTOINCREMENT,
269
+ task_id TEXT NOT NULL,
270
+ node_id TEXT NOT NULL,
271
+ run_no INTEGER NOT NULL,
272
+ worker_ref TEXT,
273
+ final_status TEXT NOT NULL,
274
+ result_summary TEXT,
275
+ error_code TEXT,
276
+ error_message TEXT,
277
+ error_retryable INTEGER,
278
+ input_documents_json TEXT NOT NULL DEFAULT '[]',
279
+ output_documents_json TEXT NOT NULL DEFAULT '[]',
280
+ started_at TEXT,
281
+ ended_at TEXT NOT NULL,
282
+ FOREIGN KEY (task_id) REFERENCES tasks_v3(task_id),
283
+ FOREIGN KEY (node_id) REFERENCES nodes_v3(node_id),
284
+ UNIQUE(node_id, run_no)
285
+ );
286
+ CREATE TABLE task_documents_v3 (
287
+ task_id TEXT NOT NULL,
288
+ document_type TEXT NOT NULL,
289
+ source_node_id TEXT,
290
+ file_path TEXT NOT NULL,
291
+ content_hash TEXT NOT NULL,
292
+ updated_by_ref TEXT NOT NULL,
293
+ updated_at TEXT NOT NULL,
294
+ PRIMARY KEY (task_id, document_type),
295
+ FOREIGN KEY (task_id) REFERENCES tasks_v3(task_id) ON DELETE CASCADE
296
+ );
297
+ CREATE TABLE task_messages_v3 (
298
+ message_id TEXT PRIMARY KEY,
299
+ task_id TEXT NOT NULL,
300
+ node_id TEXT,
301
+ message_type TEXT NOT NULL,
302
+ reason_code TEXT NOT NULL,
303
+ message TEXT NOT NULL,
304
+ related_ref TEXT,
305
+ status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','ACKNOWLEDGED')),
306
+ created_by_ref TEXT NOT NULL,
307
+ created_at TEXT NOT NULL,
308
+ acknowledged_by_ref TEXT,
309
+ acknowledged_at TEXT,
310
+ resolution TEXT,
311
+ FOREIGN KEY (task_id) REFERENCES tasks_v3(task_id) ON DELETE CASCADE
312
+ );
313
+ CREATE TABLE task_events_v3 (
314
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
315
+ task_id TEXT NOT NULL,
316
+ node_id TEXT,
317
+ event_type TEXT NOT NULL,
318
+ actor_ref TEXT NOT NULL,
319
+ task_version INTEGER,
320
+ node_version INTEGER,
321
+ payload_json TEXT,
322
+ created_at TEXT NOT NULL,
323
+ FOREIGN KEY (task_id) REFERENCES tasks_v3(task_id) ON DELETE CASCADE
324
+ );
325
+ CREATE TABLE idempotency_records_v3 (
326
+ idempotency_key TEXT PRIMARY KEY,
327
+ operation TEXT NOT NULL,
328
+ request_hash TEXT NOT NULL,
329
+ response_json TEXT NOT NULL,
330
+ created_at TEXT NOT NULL
331
+ );
332
+ `);
333
+ database.exec(`
334
+ INSERT INTO task_groups_v3 SELECT * FROM task_groups;
335
+ INSERT INTO tasks_v3 SELECT * FROM tasks;
336
+ `);
337
+ const insertNode = database.prepare(`
338
+ INSERT INTO nodes_v3(
339
+ node_id,task_id,sequence_no,title,objective,status,version,run_no,
340
+ required_agent_package_ref,worker_ref,input_documents_json,output_documents_json,
341
+ result_summary,error_code,error_message,error_retryable,started_at,completed_at,updated_at
342
+ )
343
+ SELECT node_id,task_id,sequence_no,title,objective,status,version,run_no,?,worker_ref,
344
+ input_documents_json,output_documents_json,result_summary,error_code,error_message,
345
+ error_retryable,started_at,completed_at,updated_at
346
+ FROM nodes WHERE node_id=?`);
347
+ for (const fact of nodeFacts)
348
+ insertNode.run(fact.agentPackageRef, fact.nodeId);
349
+ const tasks = database
350
+ .prepare("SELECT task_id,created_at,updated_at FROM tasks ORDER BY task_id")
351
+ .all();
352
+ const insertBinding = database.prepare("INSERT INTO task_role_bindings_v3(task_id,agent_package_ref,role_ref,worker_ref,conversation_locator,version,created_at,updated_at) VALUES (?,?,?,?,?,1,?,?)");
353
+ for (const task of tasks) {
354
+ for (const agentPackageRef of fixedAgentPackageRefs) {
355
+ const facts = nodeFacts.filter((fact) => fact.taskId === task.task_id &&
356
+ fact.agentPackageRef === agentPackageRef);
357
+ const legacyRoleRefs = new Set(facts.map((fact) => fact.legacyRoleRef));
358
+ if (legacyRoleRefs.size > 1)
359
+ throw new Error("LEGACY_TASK_BINDING_AMBIGUOUS:role");
360
+ const workerRefs = new Set(facts
361
+ .map((fact) => fact.workerRef)
362
+ .filter((workerRef) => workerRef !== null));
363
+ if (workerRefs.size > 1)
364
+ throw new Error("LEGACY_TASK_BINDING_AMBIGUOUS:worker");
365
+ const legacyRoleRef = [...legacyRoleRefs][0] ?? agentPackageRef;
366
+ const descriptor = resolveLegacyRoleBinding(legacyRoleRef, agentPackageRef, context);
367
+ insertBinding.run(task.task_id, agentPackageRef, descriptor.roleRef, [...workerRefs][0] ?? null, descriptor.conversationLocator ?? null, task.created_at, task.updated_at);
368
+ }
369
+ }
370
+ database.exec(`
371
+ INSERT INTO node_execution_history_v3 SELECT * FROM node_execution_history;
372
+ INSERT INTO task_documents_v3 SELECT * FROM task_documents;
373
+ INSERT INTO task_messages_v3 SELECT * FROM task_messages;
374
+ INSERT INTO task_events_v3 SELECT * FROM task_events;
375
+ INSERT INTO idempotency_records_v3 SELECT * FROM idempotency_records;
376
+
377
+ DROP TABLE task_events;
378
+ DROP TABLE task_messages;
379
+ DROP TABLE task_documents;
380
+ DROP TABLE node_execution_history;
381
+ DROP TABLE nodes;
382
+ DROP TABLE tasks;
383
+ DROP TABLE task_groups;
384
+ DROP TABLE idempotency_records;
385
+
386
+ ALTER TABLE task_groups_v3 RENAME TO task_groups;
387
+ ALTER TABLE tasks_v3 RENAME TO tasks;
388
+ ALTER TABLE nodes_v3 RENAME TO nodes;
389
+ ALTER TABLE task_role_bindings_v3 RENAME TO task_role_bindings;
390
+ ALTER TABLE node_execution_history_v3 RENAME TO node_execution_history;
391
+ ALTER TABLE task_documents_v3 RENAME TO task_documents;
392
+ ALTER TABLE task_messages_v3 RENAME TO task_messages;
393
+ ALTER TABLE task_events_v3 RENAME TO task_events;
394
+ ALTER TABLE idempotency_records_v3 RENAME TO idempotency_records;
395
+
396
+ CREATE INDEX idx_tasks_group_sequence ON tasks(task_group_id, sequence_no);
397
+ CREATE INDEX idx_tasks_status ON tasks(status);
398
+ CREATE INDEX idx_nodes_task_sequence ON nodes(task_id, sequence_no);
399
+ CREATE INDEX idx_nodes_task_status ON nodes(task_id, status);
400
+ CREATE INDEX idx_messages_pending ON task_messages(status, created_at);
401
+ CREATE INDEX idx_events_task ON task_events(task_id, event_id);
402
+ `);
403
+ }
404
+ function tableSql(database, table) {
405
+ const row = database
406
+ .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name=?")
407
+ .get(table);
408
+ return (row?.sql ?? "").replace(/\s+/g, " ").trim().toUpperCase();
409
+ }
410
+ function hasUniqueColumns(database, table, columns) {
411
+ const indexes = database
412
+ .prepare(`PRAGMA index_list(${table})`)
413
+ .all();
414
+ return indexes.some((index) => {
415
+ if (index.unique !== 1)
416
+ return false;
417
+ const names = database.prepare(`PRAGMA index_info(${index.name})`).all()
418
+ .sort((left, right) => left.seqno - right.seqno)
419
+ .map((item) => item.name);
420
+ return (names.length === columns.length &&
421
+ names.every((name, i) => name === columns[i]));
422
+ });
423
+ }
424
+ function applyTaskSchemaCompatibilityV3(database, context) {
425
+ if (isHistorical20260810Shape(database))
426
+ rebuildHistorical20260810Schema(database, context);
427
+ else {
428
+ const nodeFacts = canonicalizeLegacyNodes(database, context);
429
+ canonicalizeLegacyBindings(database, context, nodeFacts);
430
+ }
431
+ const nodePackage = columnInfo(database, "nodes", "required_agent_package_ref");
432
+ const bindingInfo = tableInfo(database, "task_role_bindings");
433
+ const bindingPackage = bindingInfo.find((item) => item.name === "agent_package_ref");
434
+ const bindingTask = bindingInfo.find((item) => item.name === "task_id");
435
+ if (nodePackage?.notnull !== 1 ||
436
+ bindingPackage?.notnull !== 1 ||
437
+ bindingPackage.pk !== 2 ||
438
+ bindingTask?.pk !== 1 ||
439
+ !bindingInfo.some((item) => item.name === "conversation_locator"))
440
+ throw new Error("LEGACY_TASK_SCHEMA_UPGRADE_INCOMPLETE:canonical_shape");
441
+ }
442
+ function verifyTaskSchemaCompatibilityV3(database) {
443
+ const issues = [];
444
+ const expectedNodeColumns = new Set([
445
+ "node_id",
446
+ "task_id",
447
+ "sequence_no",
448
+ "title",
449
+ "objective",
450
+ "status",
451
+ "version",
452
+ "run_no",
453
+ "required_agent_package_ref",
454
+ "worker_ref",
455
+ "input_documents_json",
456
+ "output_documents_json",
457
+ "result_summary",
458
+ "error_code",
459
+ "error_message",
460
+ "error_retryable",
461
+ "started_at",
462
+ "completed_at",
463
+ "updated_at",
464
+ ]);
465
+ const expectedBindingColumns = new Set([
466
+ "task_id",
467
+ "agent_package_ref",
468
+ "role_ref",
469
+ "worker_ref",
470
+ "conversation_locator",
471
+ "version",
472
+ "created_at",
473
+ "updated_at",
474
+ ]);
475
+ if (!tableExists(database, "nodes"))
476
+ issues.push("nodes:missing");
477
+ else {
478
+ const info = tableInfo(database, "nodes");
479
+ const names = new Set(info.map((item) => item.name));
480
+ if (names.size !== expectedNodeColumns.size ||
481
+ [...expectedNodeColumns].some((name) => !names.has(name)))
482
+ issues.push("nodes:column_shape_mismatch");
483
+ const required = info.find((item) => item.name === "required_agent_package_ref");
484
+ if (!required)
485
+ issues.push("nodes.required_agent_package_ref:missing");
486
+ else if (required.notnull !== 1)
487
+ issues.push("nodes.required_agent_package_ref:not_null_required");
488
+ if (names.has("required_role_ref"))
489
+ issues.push("nodes.required_role_ref:legacy_column_present");
490
+ if (required) {
491
+ const invalid = database
492
+ .prepare(`SELECT COUNT(*) AS count FROM nodes WHERE required_agent_package_ref NOT IN (${[
493
+ ...fixedAgentPackageRefs,
494
+ ]
495
+ .map(() => "?")
496
+ .join(",")})`)
497
+ .get(...fixedAgentPackageRefs);
498
+ if (Number(invalid.count) !== 0)
499
+ issues.push("nodes.required_agent_package_ref:invalid_value");
500
+ }
501
+ }
502
+ if (!tableExists(database, "task_role_bindings"))
503
+ issues.push("task_role_bindings:missing");
504
+ else {
505
+ const info = tableInfo(database, "task_role_bindings");
506
+ const names = new Set(info.map((item) => item.name));
507
+ if (names.size !== expectedBindingColumns.size ||
508
+ [...expectedBindingColumns].some((name) => !names.has(name)))
509
+ issues.push("task_role_bindings:column_shape_mismatch");
510
+ const taskId = info.find((item) => item.name === "task_id");
511
+ const packageRef = info.find((item) => item.name === "agent_package_ref");
512
+ const roleRef = info.find((item) => item.name === "role_ref");
513
+ if (taskId?.pk !== 1)
514
+ issues.push("task_role_bindings.task_id:pk_order");
515
+ if (!packageRef)
516
+ issues.push("task_role_bindings.agent_package_ref:missing");
517
+ else {
518
+ if (packageRef.notnull !== 1)
519
+ issues.push("task_role_bindings.agent_package_ref:not_null_required");
520
+ if (packageRef.pk !== 2)
521
+ issues.push("task_role_bindings.agent_package_ref:pk_order");
522
+ }
523
+ if (roleRef?.notnull !== 1)
524
+ issues.push("task_role_bindings.role_ref:not_null_required");
525
+ for (const column of ["version", "created_at", "updated_at"]) {
526
+ if (info.find((item) => item.name === column)?.notnull !== 1)
527
+ issues.push(`task_role_bindings.${column}:not_null_required`);
528
+ }
529
+ if (!names.has("conversation_locator"))
530
+ issues.push("task_role_bindings.conversation_locator:missing");
531
+ if (packageRef) {
532
+ const invalid = database
533
+ .prepare(`SELECT COUNT(*) AS count FROM task_role_bindings WHERE agent_package_ref NOT IN (${[
534
+ ...fixedAgentPackageRefs,
535
+ ]
536
+ .map(() => "?")
537
+ .join(",")})`)
538
+ .get(...fixedAgentPackageRefs);
539
+ if (Number(invalid.count) !== 0)
540
+ issues.push("task_role_bindings.agent_package_ref:invalid_value");
541
+ }
542
+ }
543
+ const expectedColumnsByTable = {
544
+ task_groups: [
545
+ "task_group_id",
546
+ "title",
547
+ "objective",
548
+ "status",
549
+ "max_active_tasks",
550
+ "version",
551
+ "created_by_ref",
552
+ "created_at",
553
+ "updated_at",
554
+ ],
555
+ tasks: [
556
+ "task_id",
557
+ "task_group_id",
558
+ "sequence_no",
559
+ "title",
560
+ "objective",
561
+ "status",
562
+ "version",
563
+ "plan_version",
564
+ "current_node_id",
565
+ "created_by_ref",
566
+ "created_at",
567
+ "started_at",
568
+ "completed_at",
569
+ "updated_at",
570
+ ],
571
+ node_execution_history: [
572
+ "execution_id",
573
+ "task_id",
574
+ "node_id",
575
+ "run_no",
576
+ "worker_ref",
577
+ "final_status",
578
+ "result_summary",
579
+ "error_code",
580
+ "error_message",
581
+ "error_retryable",
582
+ "input_documents_json",
583
+ "output_documents_json",
584
+ "started_at",
585
+ "ended_at",
586
+ ],
587
+ task_documents: [
588
+ "task_id",
589
+ "document_type",
590
+ "source_node_id",
591
+ "file_path",
592
+ "content_hash",
593
+ "updated_by_ref",
594
+ "updated_at",
595
+ ],
596
+ task_messages: [
597
+ "message_id",
598
+ "task_id",
599
+ "node_id",
600
+ "message_type",
601
+ "reason_code",
602
+ "message",
603
+ "related_ref",
604
+ "status",
605
+ "created_by_ref",
606
+ "created_at",
607
+ "acknowledged_by_ref",
608
+ "acknowledged_at",
609
+ "resolution",
610
+ ],
611
+ task_events: [
612
+ "event_id",
613
+ "task_id",
614
+ "node_id",
615
+ "event_type",
616
+ "actor_ref",
617
+ "task_version",
618
+ "node_version",
619
+ "payload_json",
620
+ "created_at",
621
+ ],
622
+ idempotency_records: [
623
+ "idempotency_key",
624
+ "operation",
625
+ "request_hash",
626
+ "response_json",
627
+ "created_at",
628
+ ],
629
+ };
630
+ const requiredNotNullByTable = {
631
+ task_groups: [
632
+ "title",
633
+ "status",
634
+ "max_active_tasks",
635
+ "version",
636
+ "created_by_ref",
637
+ "created_at",
638
+ "updated_at",
639
+ ],
640
+ tasks: [
641
+ "title",
642
+ "objective",
643
+ "status",
644
+ "version",
645
+ "plan_version",
646
+ "created_by_ref",
647
+ "created_at",
648
+ "updated_at",
649
+ ],
650
+ node_execution_history: [
651
+ "task_id",
652
+ "node_id",
653
+ "run_no",
654
+ "final_status",
655
+ "input_documents_json",
656
+ "output_documents_json",
657
+ "ended_at",
658
+ ],
659
+ task_documents: [
660
+ "task_id",
661
+ "document_type",
662
+ "file_path",
663
+ "content_hash",
664
+ "updated_by_ref",
665
+ "updated_at",
666
+ ],
667
+ task_messages: [
668
+ "task_id",
669
+ "message_type",
670
+ "reason_code",
671
+ "message",
672
+ "status",
673
+ "created_by_ref",
674
+ "created_at",
675
+ ],
676
+ task_events: ["task_id", "event_type", "actor_ref", "created_at"],
677
+ idempotency_records: [
678
+ "operation",
679
+ "request_hash",
680
+ "response_json",
681
+ "created_at",
682
+ ],
683
+ };
684
+ for (const [table, expectedColumns] of Object.entries(expectedColumnsByTable)) {
685
+ if (!tableExists(database, table)) {
686
+ issues.push(`${table}:missing`);
687
+ continue;
688
+ }
689
+ const info = tableInfo(database, table);
690
+ const names = new Set(info.map((item) => item.name));
691
+ if (names.size !== expectedColumns.length ||
692
+ expectedColumns.some((name) => !names.has(name)))
693
+ issues.push(`${table}:column_shape_mismatch`);
694
+ for (const column of requiredNotNullByTable[table] ?? [])
695
+ if (info.find((item) => item.name === column)?.notnull !== 1)
696
+ issues.push(`${table}.${column}:not_null_required`);
697
+ }
698
+ if (!hasUniqueColumns(database, "tasks", ["task_group_id", "sequence_no"]))
699
+ issues.push("tasks:group_sequence_unique_required");
700
+ if (!hasUniqueColumns(database, "nodes", ["task_id", "sequence_no"]))
701
+ issues.push("nodes:task_sequence_unique_required");
702
+ if (!hasUniqueColumns(database, "node_execution_history", ["node_id", "run_no"]))
703
+ issues.push("node_execution_history:node_run_unique_required");
704
+ const requiredSqlFragments = {
705
+ task_groups: [
706
+ "CHECK (STATUS IN ('READY','ACTIVE','SUCCEEDED'))",
707
+ "CHECK (MAX_ACTIVE_TASKS = 1)",
708
+ "CHECK (VERSION >= 1)",
709
+ ],
710
+ tasks: [
711
+ "CHECK (STATUS IN ('PENDING','READY','ACTIVE','WAITING','FAILED','PAUSED','SUCCEEDED','TERMINATED'))",
712
+ "CHECK (VERSION >= 1)",
713
+ ],
714
+ nodes: [
715
+ "CHECK (STATUS IN ('PENDING','READY','IN_PROGRESS','WAITING','FAILED','SUCCEEDED','TERMINATED'))",
716
+ "CHECK (VERSION >= 1)",
717
+ "CHECK (RUN_NO >= 1)",
718
+ ],
719
+ task_role_bindings: ["CHECK (VERSION >= 1)"],
720
+ task_messages: ["CHECK (STATUS IN ('PENDING','ACKNOWLEDGED'))"],
721
+ };
722
+ for (const [table, fragments] of Object.entries(requiredSqlFragments)) {
723
+ const sql = tableSql(database, table);
724
+ for (const fragment of fragments)
725
+ if (!sql.includes(fragment))
726
+ issues.push(`${table}:constraint_missing:${fragment}`);
727
+ }
728
+ const foreignKeyViolations = database
729
+ .prepare("PRAGMA foreign_key_check")
730
+ .all();
731
+ if (foreignKeyViolations.length > 0)
732
+ issues.push(`foreign_key_check:${foreignKeyViolations.length}`);
733
+ return issues;
734
+ }
735
+ export const taskMigrations = [
736
+ {
737
+ version: 1,
738
+ name: "task_core",
739
+ identity: "task_core.current.20260814",
740
+ sql: `
741
+ CREATE TABLE task_groups (
742
+ task_group_id TEXT PRIMARY KEY,
743
+ title TEXT NOT NULL,
744
+ objective TEXT,
745
+ status TEXT NOT NULL CHECK (status IN ('READY','ACTIVE','SUCCEEDED')),
746
+ max_active_tasks INTEGER NOT NULL DEFAULT 1 CHECK (max_active_tasks = 1),
747
+ version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
748
+ created_by_ref TEXT NOT NULL,
749
+ created_at TEXT NOT NULL,
750
+ updated_at TEXT NOT NULL
751
+ );
752
+ CREATE TABLE tasks (
753
+ task_id TEXT PRIMARY KEY,
754
+ task_group_id TEXT,
755
+ sequence_no INTEGER,
756
+ title TEXT NOT NULL,
757
+ objective TEXT NOT NULL,
758
+ status TEXT NOT NULL CHECK (status IN ('PENDING','READY','ACTIVE','WAITING','FAILED','PAUSED','SUCCEEDED','TERMINATED')),
759
+ version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
760
+ plan_version INTEGER NOT NULL DEFAULT 1,
761
+ current_node_id TEXT,
762
+ created_by_ref TEXT NOT NULL,
763
+ created_at TEXT NOT NULL,
764
+ started_at TEXT,
765
+ completed_at TEXT,
766
+ updated_at TEXT NOT NULL,
767
+ FOREIGN KEY (task_group_id) REFERENCES task_groups(task_group_id),
768
+ UNIQUE(task_group_id, sequence_no)
769
+ );
770
+ CREATE TABLE nodes (
771
+ node_id TEXT PRIMARY KEY,
772
+ task_id TEXT NOT NULL,
773
+ sequence_no INTEGER NOT NULL,
774
+ title TEXT NOT NULL,
775
+ objective TEXT NOT NULL,
776
+ status TEXT NOT NULL CHECK (status IN ('PENDING','READY','IN_PROGRESS','WAITING','FAILED','SUCCEEDED','TERMINATED')),
777
+ version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
778
+ run_no INTEGER NOT NULL DEFAULT 1 CHECK (run_no >= 1),
779
+ required_agent_package_ref TEXT NOT NULL,
780
+ worker_ref TEXT,
781
+ input_documents_json TEXT NOT NULL DEFAULT '[]',
782
+ output_documents_json TEXT NOT NULL DEFAULT '[]',
783
+ result_summary TEXT,
784
+ error_code TEXT,
785
+ error_message TEXT,
786
+ error_retryable INTEGER,
787
+ started_at TEXT,
788
+ completed_at TEXT,
789
+ updated_at TEXT NOT NULL,
790
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE,
791
+ UNIQUE(task_id, sequence_no)
792
+ );
793
+ CREATE TABLE task_role_bindings (
794
+ task_id TEXT NOT NULL,
795
+ agent_package_ref TEXT NOT NULL,
796
+ role_ref TEXT NOT NULL,
797
+ worker_ref TEXT,
798
+ conversation_locator TEXT,
799
+ version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
800
+ created_at TEXT NOT NULL,
801
+ updated_at TEXT NOT NULL,
802
+ PRIMARY KEY (task_id, agent_package_ref),
803
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
804
+ );
805
+ CREATE TABLE node_execution_history (
806
+ execution_id INTEGER PRIMARY KEY AUTOINCREMENT,
807
+ task_id TEXT NOT NULL,
808
+ node_id TEXT NOT NULL,
809
+ run_no INTEGER NOT NULL,
810
+ worker_ref TEXT,
811
+ final_status TEXT NOT NULL,
812
+ result_summary TEXT,
813
+ error_code TEXT,
814
+ error_message TEXT,
815
+ error_retryable INTEGER,
816
+ input_documents_json TEXT NOT NULL DEFAULT '[]',
817
+ output_documents_json TEXT NOT NULL DEFAULT '[]',
818
+ started_at TEXT,
819
+ ended_at TEXT NOT NULL,
820
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id),
821
+ FOREIGN KEY (node_id) REFERENCES nodes(node_id),
822
+ UNIQUE(node_id, run_no)
823
+ );
824
+ CREATE INDEX idx_tasks_group_sequence ON tasks(task_group_id, sequence_no);
825
+ CREATE INDEX idx_tasks_status ON tasks(status);
826
+ CREATE INDEX idx_nodes_task_sequence ON nodes(task_id, sequence_no);
827
+ CREATE INDEX idx_nodes_task_status ON nodes(task_id, status);
828
+ `,
829
+ },
830
+ {
831
+ version: 2,
832
+ name: "task_documents_audit_idempotency",
833
+ identity: "task_documents_audit_idempotency.current.20260814",
834
+ sql: `
835
+ CREATE TABLE task_documents (
836
+ task_id TEXT NOT NULL,
837
+ document_type TEXT NOT NULL,
838
+ source_node_id TEXT,
839
+ file_path TEXT NOT NULL,
840
+ content_hash TEXT NOT NULL,
841
+ updated_by_ref TEXT NOT NULL,
842
+ updated_at TEXT NOT NULL,
843
+ PRIMARY KEY (task_id, document_type),
844
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
845
+ );
846
+ CREATE TABLE task_messages (
847
+ message_id TEXT PRIMARY KEY,
848
+ task_id TEXT NOT NULL,
849
+ node_id TEXT,
850
+ message_type TEXT NOT NULL,
851
+ reason_code TEXT NOT NULL,
852
+ message TEXT NOT NULL,
853
+ related_ref TEXT,
854
+ status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','ACKNOWLEDGED')),
855
+ created_by_ref TEXT NOT NULL,
856
+ created_at TEXT NOT NULL,
857
+ acknowledged_by_ref TEXT,
858
+ acknowledged_at TEXT,
859
+ resolution TEXT,
860
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
861
+ );
862
+ CREATE TABLE task_events (
863
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
864
+ task_id TEXT NOT NULL,
865
+ node_id TEXT,
866
+ event_type TEXT NOT NULL,
867
+ actor_ref TEXT NOT NULL,
868
+ task_version INTEGER,
869
+ node_version INTEGER,
870
+ payload_json TEXT,
871
+ created_at TEXT NOT NULL,
872
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
873
+ );
874
+ CREATE TABLE idempotency_records (
875
+ idempotency_key TEXT PRIMARY KEY,
876
+ operation TEXT NOT NULL,
877
+ request_hash TEXT NOT NULL,
878
+ response_json TEXT NOT NULL,
879
+ created_at TEXT NOT NULL
880
+ );
881
+ CREATE INDEX idx_messages_pending ON task_messages(status, created_at);
882
+ CREATE INDEX idx_events_task ON task_events(task_id, event_id);
883
+ `,
884
+ },
885
+ {
886
+ version: 3,
887
+ name: "task_schema_compatibility_v3",
888
+ identity: "task_schema_compatibility_v3.20260815",
889
+ coversLegacyVersions: [1, 2],
890
+ sql: "",
891
+ apply: applyTaskSchemaCompatibilityV3,
892
+ verify: verifyTaskSchemaCompatibilityV3,
893
+ },
894
+ ];