@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.
- package/README.md +5 -0
- package/conformance.json +4 -0
- package/dist/deployment/adapter.d.ts +47 -0
- package/dist/deployment/adapter.js +26 -0
- package/dist/deployment/descriptor.d.ts +51 -0
- package/dist/deployment/descriptor.js +54 -0
- package/dist/src/index.d.ts +24 -0
- package/dist/src/index.js +333 -0
- package/dist/src/migrations.d.ts +20 -0
- package/dist/src/migrations.js +894 -0
- package/package.json +51 -0
- package/proflow.module.json +63 -0
- package/self-install.mjs +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @tomflow/proflow-task-store-sqlite
|
|
2
|
+
|
|
3
|
+
Task Domain 的 `node:sqlite` store、transaction 与 repository adapters。业务状态决策仍由 task-orchestration 拥有。
|
|
4
|
+
|
|
5
|
+
当前持久化包含 TaskRoleBinding 等 Task-owned facts;不把 simple Task start authorization、Browser tab/frame、System Observer assessment、Execution/Collaboration pending mirror 持久化为 Task truth。
|
package/conformance.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const behaviorAdapter: {
|
|
2
|
+
readonly describe: () => {
|
|
3
|
+
result: {
|
|
4
|
+
readonly contract: "deployment.result.v1";
|
|
5
|
+
readonly ok: true;
|
|
6
|
+
readonly status: "SUCCEEDED";
|
|
7
|
+
readonly moduleRef: "task-store-sqlite";
|
|
8
|
+
readonly moduleVersion: "0.1.0";
|
|
9
|
+
};
|
|
10
|
+
observedEffects: never[];
|
|
11
|
+
};
|
|
12
|
+
readonly preflight: () => {
|
|
13
|
+
result: {
|
|
14
|
+
readonly contract: "deployment.result.v1";
|
|
15
|
+
readonly ok: true;
|
|
16
|
+
readonly status: "SUCCEEDED";
|
|
17
|
+
readonly moduleRef: "task-store-sqlite";
|
|
18
|
+
readonly moduleVersion: "0.1.0";
|
|
19
|
+
};
|
|
20
|
+
observedEffects: never[];
|
|
21
|
+
};
|
|
22
|
+
readonly verify: () => {
|
|
23
|
+
result: {
|
|
24
|
+
contract: "deployment.result.v1";
|
|
25
|
+
ok: true;
|
|
26
|
+
status: "SUCCEEDED";
|
|
27
|
+
moduleRef: "task-store-sqlite";
|
|
28
|
+
moduleVersion: "0.1.0";
|
|
29
|
+
checks: {
|
|
30
|
+
id: string;
|
|
31
|
+
status: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
observedEffects: never[];
|
|
36
|
+
};
|
|
37
|
+
readonly doctor: () => {
|
|
38
|
+
result: {
|
|
39
|
+
readonly contract: "deployment.result.v1";
|
|
40
|
+
readonly ok: true;
|
|
41
|
+
readonly status: "SUCCEEDED";
|
|
42
|
+
readonly moduleRef: "task-store-sqlite";
|
|
43
|
+
readonly moduleVersion: "0.1.0";
|
|
44
|
+
};
|
|
45
|
+
observedEffects: never[];
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { descriptor } from "./descriptor.js";
|
|
2
|
+
const result = {
|
|
3
|
+
contract: "deployment.result.v1",
|
|
4
|
+
ok: true,
|
|
5
|
+
status: "SUCCEEDED",
|
|
6
|
+
moduleRef: descriptor.moduleRef,
|
|
7
|
+
moduleVersion: descriptor.moduleVersion,
|
|
8
|
+
};
|
|
9
|
+
export const behaviorAdapter = {
|
|
10
|
+
describe: () => ({ result, observedEffects: [] }),
|
|
11
|
+
preflight: () => ({ result, observedEffects: [] }),
|
|
12
|
+
verify: () => ({
|
|
13
|
+
result: {
|
|
14
|
+
...result,
|
|
15
|
+
checks: [
|
|
16
|
+
{
|
|
17
|
+
id: "sqlite-integrity-pass",
|
|
18
|
+
status: "PASS",
|
|
19
|
+
message: "SQLite integrity verification is available",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
observedEffects: [],
|
|
24
|
+
}),
|
|
25
|
+
doctor: () => ({ result, observedEffects: [] }),
|
|
26
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const descriptor: {
|
|
2
|
+
readonly contract: "module";
|
|
3
|
+
readonly contractVersion: "1.0.0";
|
|
4
|
+
readonly moduleRef: "task-store-sqlite";
|
|
5
|
+
readonly packageName: "@tomflow/proflow-task-store-sqlite";
|
|
6
|
+
readonly moduleVersion: "0.1.0";
|
|
7
|
+
readonly kind: "library";
|
|
8
|
+
readonly templateVersion: "1.0.0";
|
|
9
|
+
readonly platformCompatibility: ">=1.0.0 <2.0.0";
|
|
10
|
+
readonly installClass: "core";
|
|
11
|
+
readonly identity: {
|
|
12
|
+
readonly domain: "task-orchestration";
|
|
13
|
+
readonly summary: "Provides the node:sqlite store and repository adapters for Task-owned structured facts.";
|
|
14
|
+
};
|
|
15
|
+
readonly provides: readonly [];
|
|
16
|
+
readonly requires: readonly [{
|
|
17
|
+
readonly contractRef: "task-orchestration";
|
|
18
|
+
readonly versionRange: ">=1.0.0 <2.0.0";
|
|
19
|
+
}];
|
|
20
|
+
readonly requirements: readonly [{
|
|
21
|
+
readonly kind: "runtime";
|
|
22
|
+
readonly runtime: "node";
|
|
23
|
+
readonly versionRange: ">=24.19.0";
|
|
24
|
+
}];
|
|
25
|
+
readonly configSlots: readonly [{
|
|
26
|
+
readonly key: "databasePath";
|
|
27
|
+
readonly type: "path";
|
|
28
|
+
readonly required: true;
|
|
29
|
+
readonly description: "Task SQLite database path";
|
|
30
|
+
}];
|
|
31
|
+
readonly lifecycle: {
|
|
32
|
+
readonly supported: readonly ["describe", "preflight", "verify", "doctor"];
|
|
33
|
+
};
|
|
34
|
+
readonly verification: {
|
|
35
|
+
readonly checks: readonly [{
|
|
36
|
+
readonly id: "sqlite-integrity-pass";
|
|
37
|
+
readonly description: "SQLite schema and integrity checks pass";
|
|
38
|
+
readonly lifecycle: "verify";
|
|
39
|
+
}];
|
|
40
|
+
};
|
|
41
|
+
readonly effects: readonly [{
|
|
42
|
+
readonly kind: "filesystem";
|
|
43
|
+
readonly description: "Persists Task-owned structured facts in SQLite";
|
|
44
|
+
readonly retention: "preserve";
|
|
45
|
+
}];
|
|
46
|
+
readonly documentation: readonly [{
|
|
47
|
+
readonly id: "overview";
|
|
48
|
+
readonly path: "./README.md";
|
|
49
|
+
readonly description: "Package-owned module overview";
|
|
50
|
+
}];
|
|
51
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const descriptor = {
|
|
2
|
+
contract: "module",
|
|
3
|
+
contractVersion: "1.0.0",
|
|
4
|
+
moduleRef: "task-store-sqlite",
|
|
5
|
+
packageName: "@tomflow/proflow-task-store-sqlite",
|
|
6
|
+
moduleVersion: "0.1.0",
|
|
7
|
+
kind: "library",
|
|
8
|
+
templateVersion: "1.0.0",
|
|
9
|
+
platformCompatibility: ">=1.0.0 <2.0.0",
|
|
10
|
+
installClass: "core",
|
|
11
|
+
identity: {
|
|
12
|
+
domain: "task-orchestration",
|
|
13
|
+
summary: "Provides the node:sqlite store and repository adapters for Task-owned structured facts.",
|
|
14
|
+
},
|
|
15
|
+
provides: [],
|
|
16
|
+
requires: [
|
|
17
|
+
{ contractRef: "task-orchestration", versionRange: ">=1.0.0 <2.0.0" },
|
|
18
|
+
],
|
|
19
|
+
requirements: [
|
|
20
|
+
{ kind: "runtime", runtime: "node", versionRange: ">=24.19.0" },
|
|
21
|
+
],
|
|
22
|
+
configSlots: [
|
|
23
|
+
{
|
|
24
|
+
key: "databasePath",
|
|
25
|
+
type: "path",
|
|
26
|
+
required: true,
|
|
27
|
+
description: "Task SQLite database path",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
lifecycle: { supported: ["describe", "preflight", "verify", "doctor"] },
|
|
31
|
+
verification: {
|
|
32
|
+
checks: [
|
|
33
|
+
{
|
|
34
|
+
id: "sqlite-integrity-pass",
|
|
35
|
+
description: "SQLite schema and integrity checks pass",
|
|
36
|
+
lifecycle: "verify",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
effects: [
|
|
41
|
+
{
|
|
42
|
+
kind: "filesystem",
|
|
43
|
+
description: "Persists Task-owned structured facts in SQLite",
|
|
44
|
+
retention: "preserve",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
documentation: [
|
|
48
|
+
{
|
|
49
|
+
id: "overview",
|
|
50
|
+
path: "./README.md",
|
|
51
|
+
description: "Package-owned module overview",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TaskRepositories, TaskStore } from "@tomflow/proflow-task-orchestration";
|
|
2
|
+
export { type TaskMigration, type TaskMigrationContext, taskMigrations, } from "./migrations.ts";
|
|
3
|
+
export declare class SqliteTaskStore implements TaskStore {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(input: {
|
|
6
|
+
databasePath: string;
|
|
7
|
+
busyTimeoutMs?: number;
|
|
8
|
+
});
|
|
9
|
+
transaction<T>(work: (repositories: TaskRepositories) => T): T;
|
|
10
|
+
read<T>(work: (repositories: TaskRepositories) => T): T;
|
|
11
|
+
close(): void;
|
|
12
|
+
diagnostics(): {
|
|
13
|
+
journalMode: string;
|
|
14
|
+
busyTimeoutMs: number;
|
|
15
|
+
integrity: string;
|
|
16
|
+
};
|
|
17
|
+
inspectCounts(): Record<string, number>;
|
|
18
|
+
inspectSchema(): {
|
|
19
|
+
tables: string[];
|
|
20
|
+
indexes: string[];
|
|
21
|
+
foreignKeys: Record<string, string[]>;
|
|
22
|
+
columns: Record<string, string[]>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
export { taskMigrations, } from "./migrations.js";
|
|
3
|
+
const json = (value) => JSON.stringify(value);
|
|
4
|
+
const text = (value) => value === null || value === undefined ? null : String(value);
|
|
5
|
+
const number = (value) => Number(value);
|
|
6
|
+
const boolean = (value) => value === null || value === undefined ? null : Number(value) === 1;
|
|
7
|
+
const list = (value) => JSON.parse(String(value));
|
|
8
|
+
function group(row) {
|
|
9
|
+
return {
|
|
10
|
+
taskGroupId: String(row.task_group_id),
|
|
11
|
+
title: String(row.title),
|
|
12
|
+
objective: text(row.objective),
|
|
13
|
+
status: row.status,
|
|
14
|
+
maxActiveTasks: 1,
|
|
15
|
+
version: number(row.version),
|
|
16
|
+
createdByRef: String(row.created_by_ref),
|
|
17
|
+
createdAt: String(row.created_at),
|
|
18
|
+
updatedAt: String(row.updated_at),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function task(row) {
|
|
22
|
+
return {
|
|
23
|
+
taskId: String(row.task_id),
|
|
24
|
+
taskGroupId: text(row.task_group_id),
|
|
25
|
+
sequenceNo: row.sequence_no === null ? null : number(row.sequence_no),
|
|
26
|
+
title: String(row.title),
|
|
27
|
+
objective: String(row.objective),
|
|
28
|
+
status: row.status,
|
|
29
|
+
version: number(row.version),
|
|
30
|
+
planVersion: number(row.plan_version),
|
|
31
|
+
currentNodeId: text(row.current_node_id),
|
|
32
|
+
createdByRef: String(row.created_by_ref),
|
|
33
|
+
createdAt: String(row.created_at),
|
|
34
|
+
startedAt: text(row.started_at),
|
|
35
|
+
completedAt: text(row.completed_at),
|
|
36
|
+
updatedAt: String(row.updated_at),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function node(row) {
|
|
40
|
+
return {
|
|
41
|
+
nodeId: String(row.node_id),
|
|
42
|
+
taskId: String(row.task_id),
|
|
43
|
+
sequenceNo: number(row.sequence_no),
|
|
44
|
+
title: String(row.title),
|
|
45
|
+
objective: String(row.objective),
|
|
46
|
+
status: row.status,
|
|
47
|
+
version: number(row.version),
|
|
48
|
+
runNo: number(row.run_no),
|
|
49
|
+
requiredAgentPackageRef: String(row.required_agent_package_ref),
|
|
50
|
+
workerRef: text(row.worker_ref),
|
|
51
|
+
inputDocuments: list(row.input_documents_json),
|
|
52
|
+
outputDocuments: list(row.output_documents_json),
|
|
53
|
+
resultSummary: text(row.result_summary),
|
|
54
|
+
errorCode: text(row.error_code),
|
|
55
|
+
errorMessage: text(row.error_message),
|
|
56
|
+
errorRetryable: boolean(row.error_retryable),
|
|
57
|
+
startedAt: text(row.started_at),
|
|
58
|
+
completedAt: text(row.completed_at),
|
|
59
|
+
updatedAt: String(row.updated_at),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function binding(row) {
|
|
63
|
+
return {
|
|
64
|
+
taskId: String(row.task_id),
|
|
65
|
+
agentPackageRef: String(row.agent_package_ref),
|
|
66
|
+
roleRef: String(row.role_ref),
|
|
67
|
+
workerRef: text(row.worker_ref),
|
|
68
|
+
conversationLocator: text(row.conversation_locator),
|
|
69
|
+
version: number(row.version),
|
|
70
|
+
createdAt: String(row.created_at),
|
|
71
|
+
updatedAt: String(row.updated_at),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function history(row) {
|
|
75
|
+
return {
|
|
76
|
+
executionId: number(row.execution_id),
|
|
77
|
+
taskId: String(row.task_id),
|
|
78
|
+
nodeId: String(row.node_id),
|
|
79
|
+
runNo: number(row.run_no),
|
|
80
|
+
workerRef: text(row.worker_ref),
|
|
81
|
+
finalStatus: row.final_status,
|
|
82
|
+
resultSummary: text(row.result_summary),
|
|
83
|
+
errorCode: text(row.error_code),
|
|
84
|
+
errorMessage: text(row.error_message),
|
|
85
|
+
errorRetryable: boolean(row.error_retryable),
|
|
86
|
+
inputDocuments: list(row.input_documents_json),
|
|
87
|
+
outputDocuments: list(row.output_documents_json),
|
|
88
|
+
startedAt: text(row.started_at),
|
|
89
|
+
endedAt: String(row.ended_at),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function document(row) {
|
|
93
|
+
return {
|
|
94
|
+
taskId: String(row.task_id),
|
|
95
|
+
documentType: String(row.document_type),
|
|
96
|
+
sourceNodeId: text(row.source_node_id),
|
|
97
|
+
filePath: String(row.file_path),
|
|
98
|
+
contentHash: String(row.content_hash),
|
|
99
|
+
updatedByRef: String(row.updated_by_ref),
|
|
100
|
+
updatedAt: String(row.updated_at),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function message(row) {
|
|
104
|
+
return {
|
|
105
|
+
messageId: String(row.message_id),
|
|
106
|
+
taskId: String(row.task_id),
|
|
107
|
+
nodeId: text(row.node_id),
|
|
108
|
+
messageType: String(row.message_type),
|
|
109
|
+
reasonCode: String(row.reason_code),
|
|
110
|
+
message: String(row.message),
|
|
111
|
+
relatedRef: text(row.related_ref),
|
|
112
|
+
status: row.status,
|
|
113
|
+
createdByRef: String(row.created_by_ref),
|
|
114
|
+
createdAt: String(row.created_at),
|
|
115
|
+
acknowledgedByRef: text(row.acknowledged_by_ref),
|
|
116
|
+
acknowledgedAt: text(row.acknowledged_at),
|
|
117
|
+
resolution: text(row.resolution),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function event(row) {
|
|
121
|
+
return {
|
|
122
|
+
eventId: number(row.event_id),
|
|
123
|
+
taskId: String(row.task_id),
|
|
124
|
+
nodeId: text(row.node_id),
|
|
125
|
+
eventType: String(row.event_type),
|
|
126
|
+
actorRef: String(row.actor_ref),
|
|
127
|
+
taskVersion: row.task_version === null ? null : number(row.task_version),
|
|
128
|
+
nodeVersion: row.node_version === null ? null : number(row.node_version),
|
|
129
|
+
payload: row.payload_json === null
|
|
130
|
+
? null
|
|
131
|
+
: JSON.parse(String(row.payload_json)),
|
|
132
|
+
createdAt: String(row.created_at),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function idem(row) {
|
|
136
|
+
return {
|
|
137
|
+
idempotencyKey: String(row.idempotency_key),
|
|
138
|
+
operation: String(row.operation),
|
|
139
|
+
requestHash: String(row.request_hash),
|
|
140
|
+
responseJson: String(row.response_json),
|
|
141
|
+
createdAt: String(row.created_at),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export class SqliteTaskStore {
|
|
145
|
+
#database;
|
|
146
|
+
constructor(input) {
|
|
147
|
+
this.#database = new DatabaseSync(input.databasePath);
|
|
148
|
+
this.#database.exec(`PRAGMA busy_timeout = ${input.busyTimeoutMs ?? 2_500};`);
|
|
149
|
+
this.#database.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;");
|
|
150
|
+
}
|
|
151
|
+
#repositories() {
|
|
152
|
+
const db = this.#database;
|
|
153
|
+
const get = (sql, ...params) => db.prepare(sql).get(...params);
|
|
154
|
+
const all = (sql, ...params) => db.prepare(sql).all(...params);
|
|
155
|
+
return {
|
|
156
|
+
taskGroups: {
|
|
157
|
+
get: (id) => {
|
|
158
|
+
const row = get("SELECT * FROM task_groups WHERE task_group_id = ?", id);
|
|
159
|
+
return row ? group(row) : undefined;
|
|
160
|
+
},
|
|
161
|
+
insert: (v) => {
|
|
162
|
+
db.prepare("INSERT INTO task_groups VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(v.taskGroupId, v.title, v.objective, v.status, v.maxActiveTasks, v.version, v.createdByRef, v.createdAt, v.updatedAt);
|
|
163
|
+
},
|
|
164
|
+
update: (v) => {
|
|
165
|
+
const result = db
|
|
166
|
+
.prepare("UPDATE task_groups SET title=?, objective=?, status=?, max_active_tasks=?, version=?, updated_at=? WHERE task_group_id=? AND version=?")
|
|
167
|
+
.run(v.title, v.objective, v.status, v.maxActiveTasks, v.version, v.updatedAt, v.taskGroupId, v.version - 1);
|
|
168
|
+
if (Number(result.changes) !== 1)
|
|
169
|
+
throw new Error("TASK_GROUP_VERSION_CONFLICT");
|
|
170
|
+
},
|
|
171
|
+
list: () => all("SELECT * FROM task_groups ORDER BY created_at, task_group_id").map(group),
|
|
172
|
+
},
|
|
173
|
+
tasks: {
|
|
174
|
+
get: (id) => {
|
|
175
|
+
const row = get("SELECT * FROM tasks WHERE task_id = ?", id);
|
|
176
|
+
return row ? task(row) : undefined;
|
|
177
|
+
},
|
|
178
|
+
insert: (v) => {
|
|
179
|
+
db.prepare("INSERT INTO tasks(task_id,task_group_id,sequence_no,title,objective,status,version,plan_version,current_node_id,created_by_ref,created_at,started_at,completed_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)").run(v.taskId, v.taskGroupId, v.sequenceNo, v.title, v.objective, v.status, v.version, v.planVersion, v.currentNodeId, v.createdByRef, v.createdAt, v.startedAt, v.completedAt, v.updatedAt);
|
|
180
|
+
},
|
|
181
|
+
update: (v) => {
|
|
182
|
+
const result = db
|
|
183
|
+
.prepare("UPDATE tasks SET status=?, version=?, current_node_id=?, started_at=?, completed_at=?, updated_at=? WHERE task_id=? AND version=?")
|
|
184
|
+
.run(v.status, v.version, v.currentNodeId, v.startedAt, v.completedAt, v.updatedAt, v.taskId, v.version - 1);
|
|
185
|
+
if (Number(result.changes) !== 1)
|
|
186
|
+
throw new Error("TASK_VERSION_CONFLICT");
|
|
187
|
+
},
|
|
188
|
+
list: (groupId) => (groupId === undefined
|
|
189
|
+
? all("SELECT * FROM tasks ORDER BY COALESCE(sequence_no, 2147483647), created_at, task_id")
|
|
190
|
+
: all("SELECT * FROM tasks WHERE task_group_id = ? ORDER BY sequence_no, task_id", groupId)).map(task),
|
|
191
|
+
},
|
|
192
|
+
nodes: {
|
|
193
|
+
get: (id) => {
|
|
194
|
+
const row = get("SELECT * FROM nodes WHERE node_id = ?", id);
|
|
195
|
+
return row ? node(row) : undefined;
|
|
196
|
+
},
|
|
197
|
+
insert: (v) => {
|
|
198
|
+
db.prepare("INSERT INTO nodes(node_id,task_id,sequence_no,title,objective,status,version,run_no,required_agent_package_ref,worker_ref,input_documents_json,output_documents_json,result_summary,error_code,error_message,error_retryable,started_at,completed_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)").run(v.nodeId, v.taskId, v.sequenceNo, v.title, v.objective, v.status, v.version, v.runNo, v.requiredAgentPackageRef, v.workerRef, json(v.inputDocuments), json(v.outputDocuments), v.resultSummary, v.errorCode, v.errorMessage, v.errorRetryable === null ? null : Number(v.errorRetryable), v.startedAt, v.completedAt, v.updatedAt);
|
|
199
|
+
},
|
|
200
|
+
update: (v) => {
|
|
201
|
+
const result = db
|
|
202
|
+
.prepare("UPDATE nodes SET status=?, version=?, run_no=?, worker_ref=?, result_summary=?, error_code=?, error_message=?, error_retryable=?, started_at=?, completed_at=?, updated_at=? WHERE node_id=? AND version=?")
|
|
203
|
+
.run(v.status, v.version, v.runNo, v.workerRef, v.resultSummary, v.errorCode, v.errorMessage, v.errorRetryable === null ? null : Number(v.errorRetryable), v.startedAt, v.completedAt, v.updatedAt, v.nodeId, v.version - 1);
|
|
204
|
+
if (Number(result.changes) !== 1)
|
|
205
|
+
throw new Error("NODE_VERSION_CONFLICT");
|
|
206
|
+
},
|
|
207
|
+
listByTask: (taskId) => all("SELECT * FROM nodes WHERE task_id = ? ORDER BY sequence_no, node_id", taskId).map(node),
|
|
208
|
+
},
|
|
209
|
+
roleBindings: {
|
|
210
|
+
get: (taskId, agentPackageRef) => {
|
|
211
|
+
const row = get("SELECT * FROM task_role_bindings WHERE task_id=? AND agent_package_ref=?", taskId, agentPackageRef);
|
|
212
|
+
return row ? binding(row) : undefined;
|
|
213
|
+
},
|
|
214
|
+
upsert: (v) => {
|
|
215
|
+
db.prepare("INSERT INTO task_role_bindings(task_id,agent_package_ref,role_ref,worker_ref,conversation_locator,version,created_at,updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(task_id, agent_package_ref) DO UPDATE SET role_ref=excluded.role_ref, worker_ref=excluded.worker_ref, conversation_locator=excluded.conversation_locator, version=excluded.version, updated_at=excluded.updated_at").run(v.taskId, v.agentPackageRef, v.roleRef, v.workerRef, v.conversationLocator, v.version, v.createdAt, v.updatedAt);
|
|
216
|
+
},
|
|
217
|
+
listByTask: (taskId) => all("SELECT * FROM task_role_bindings WHERE task_id=? ORDER BY agent_package_ref", taskId).map(binding),
|
|
218
|
+
},
|
|
219
|
+
executionHistory: {
|
|
220
|
+
insert: (v) => {
|
|
221
|
+
db.prepare("INSERT INTO node_execution_history(task_id,node_id,run_no,worker_ref,final_status,result_summary,error_code,error_message,error_retryable,input_documents_json,output_documents_json,started_at,ended_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)").run(v.taskId, v.nodeId, v.runNo, v.workerRef, v.finalStatus, v.resultSummary, v.errorCode, v.errorMessage, v.errorRetryable === null ? null : Number(v.errorRetryable), json(v.inputDocuments), json(v.outputDocuments), v.startedAt, v.endedAt);
|
|
222
|
+
},
|
|
223
|
+
listByTask: (taskId) => all("SELECT * FROM node_execution_history WHERE task_id=? ORDER BY execution_id", taskId).map(history),
|
|
224
|
+
},
|
|
225
|
+
documents: {
|
|
226
|
+
get: (taskId, type) => {
|
|
227
|
+
const row = get("SELECT * FROM task_documents WHERE task_id=? AND document_type=?", taskId, type);
|
|
228
|
+
return row ? document(row) : undefined;
|
|
229
|
+
},
|
|
230
|
+
upsert: (v) => {
|
|
231
|
+
db.prepare("INSERT INTO task_documents VALUES (?,?,?,?,?,?,?) ON CONFLICT(task_id,document_type) DO UPDATE SET source_node_id=excluded.source_node_id,file_path=excluded.file_path,content_hash=excluded.content_hash,updated_by_ref=excluded.updated_by_ref,updated_at=excluded.updated_at").run(v.taskId, v.documentType, v.sourceNodeId, v.filePath, v.contentHash, v.updatedByRef, v.updatedAt);
|
|
232
|
+
},
|
|
233
|
+
listByTask: (taskId) => all("SELECT * FROM task_documents WHERE task_id=? ORDER BY document_type", taskId).map(document),
|
|
234
|
+
},
|
|
235
|
+
messages: {
|
|
236
|
+
get: (id) => {
|
|
237
|
+
const row = get("SELECT * FROM task_messages WHERE message_id=?", id);
|
|
238
|
+
return row ? message(row) : undefined;
|
|
239
|
+
},
|
|
240
|
+
insert: (v) => {
|
|
241
|
+
db.prepare("INSERT INTO task_messages(message_id,task_id,node_id,message_type,reason_code,message,related_ref,status,created_by_ref,created_at,acknowledged_by_ref,acknowledged_at,resolution) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)").run(v.messageId, v.taskId, v.nodeId, v.messageType, v.reasonCode, v.message, v.relatedRef, v.status, v.createdByRef, v.createdAt, v.acknowledgedByRef, v.acknowledgedAt, v.resolution);
|
|
242
|
+
},
|
|
243
|
+
update: (v) => {
|
|
244
|
+
db.prepare("UPDATE task_messages SET status=?,acknowledged_by_ref=?,acknowledged_at=?,resolution=? WHERE message_id=?").run(v.status, v.acknowledgedByRef, v.acknowledgedAt, v.resolution, v.messageId);
|
|
245
|
+
},
|
|
246
|
+
listPending: () => all("SELECT * FROM task_messages WHERE status='PENDING' ORDER BY created_at,message_id").map(message),
|
|
247
|
+
},
|
|
248
|
+
events: {
|
|
249
|
+
insert: (v) => {
|
|
250
|
+
db.prepare("INSERT INTO task_events(task_id,node_id,event_type,actor_ref,task_version,node_version,payload_json,created_at) VALUES (?,?,?,?,?,?,?,?)").run(v.taskId, v.nodeId, v.eventType, v.actorRef, v.taskVersion, v.nodeVersion, v.payload === null ? null : json(v.payload), v.createdAt);
|
|
251
|
+
},
|
|
252
|
+
listByTask: (taskId) => all("SELECT * FROM task_events WHERE task_id=? ORDER BY event_id", taskId).map(event),
|
|
253
|
+
},
|
|
254
|
+
idempotency: {
|
|
255
|
+
get: (key) => {
|
|
256
|
+
const row = get("SELECT * FROM idempotency_records WHERE idempotency_key=?", key);
|
|
257
|
+
return row ? idem(row) : undefined;
|
|
258
|
+
},
|
|
259
|
+
insert: (v) => {
|
|
260
|
+
db.prepare("INSERT INTO idempotency_records VALUES (?,?,?,?,?)").run(v.idempotencyKey, v.operation, v.requestHash, v.responseJson, v.createdAt);
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
transaction(work) {
|
|
266
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
267
|
+
try {
|
|
268
|
+
const result = work(this.#repositories());
|
|
269
|
+
this.#database.exec("COMMIT");
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
this.#database.exec("ROLLBACK");
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
read(work) {
|
|
278
|
+
return work(this.#repositories());
|
|
279
|
+
}
|
|
280
|
+
close() {
|
|
281
|
+
this.#database.close();
|
|
282
|
+
}
|
|
283
|
+
diagnostics() {
|
|
284
|
+
const journal = this.#database.prepare("PRAGMA journal_mode").get();
|
|
285
|
+
const timeout = this.#database.prepare("PRAGMA busy_timeout").get();
|
|
286
|
+
const integrity = this.#database
|
|
287
|
+
.prepare("PRAGMA integrity_check")
|
|
288
|
+
.get();
|
|
289
|
+
return {
|
|
290
|
+
journalMode: journal.journal_mode,
|
|
291
|
+
busyTimeoutMs: timeout.timeout,
|
|
292
|
+
integrity: integrity.integrity_check,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
inspectCounts() {
|
|
296
|
+
const tables = [
|
|
297
|
+
"task_groups",
|
|
298
|
+
"tasks",
|
|
299
|
+
"nodes",
|
|
300
|
+
"task_role_bindings",
|
|
301
|
+
"node_execution_history",
|
|
302
|
+
"task_documents",
|
|
303
|
+
"task_messages",
|
|
304
|
+
"task_events",
|
|
305
|
+
"idempotency_records",
|
|
306
|
+
];
|
|
307
|
+
return Object.fromEntries(tables.map((table) => [
|
|
308
|
+
table,
|
|
309
|
+
Number(this.#database
|
|
310
|
+
.prepare(`SELECT COUNT(*) AS count FROM ${table}`)
|
|
311
|
+
.get().count),
|
|
312
|
+
]));
|
|
313
|
+
}
|
|
314
|
+
inspectSchema() {
|
|
315
|
+
const tables = this.#database
|
|
316
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
|
|
317
|
+
.all().map((row) => row.name);
|
|
318
|
+
const indexes = this.#database
|
|
319
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' ORDER BY name")
|
|
320
|
+
.all().map((row) => row.name);
|
|
321
|
+
const foreignKeys = Object.fromEntries(tables.map((table) => [
|
|
322
|
+
table,
|
|
323
|
+
this.#database
|
|
324
|
+
.prepare(`PRAGMA foreign_key_list(${table})`)
|
|
325
|
+
.all().map((row) => `${row.table}.${row.to}`),
|
|
326
|
+
]));
|
|
327
|
+
const columns = Object.fromEntries(tables.map((table) => [
|
|
328
|
+
table,
|
|
329
|
+
this.#database.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name),
|
|
330
|
+
]));
|
|
331
|
+
return { tables, indexes, foreignKeys, columns };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
export interface LegacyRoleBindingDescriptor {
|
|
3
|
+
agentPackageRef: string;
|
|
4
|
+
roleRef: string;
|
|
5
|
+
conversationLocator?: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface TaskMigrationContext {
|
|
8
|
+
legacyRoleMap?: Readonly<Record<string, string>>;
|
|
9
|
+
legacyRoleBindings?: Readonly<Record<string, LegacyRoleBindingDescriptor>>;
|
|
10
|
+
}
|
|
11
|
+
export interface TaskMigration {
|
|
12
|
+
version: number;
|
|
13
|
+
name: string;
|
|
14
|
+
sql: string;
|
|
15
|
+
identity?: string;
|
|
16
|
+
coversLegacyVersions?: readonly number[];
|
|
17
|
+
apply?: (database: DatabaseSync, context: TaskMigrationContext) => void;
|
|
18
|
+
verify?: (database: DatabaseSync) => readonly string[];
|
|
19
|
+
}
|
|
20
|
+
export declare const taskMigrations: readonly TaskMigration[];
|