openworkgraph 0.3.0 → 0.3.2

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.
@@ -2,222 +2,222 @@ import { RESOURCE_SCHEMA } from './resource-schema.js';
2
2
  import { WORKFLOW_SCHEMA } from './workflow-schema.js';
3
3
  import { ASSET_BUSINESS_SCHEMA } from './asset-business-schema.js';
4
4
  import { PLUGIN_SCHEMA } from './plugin-schema.js';
5
- export const MIGRATIONS = [{ version: 1, sql: `
6
- CREATE TABLE identity (singleton INTEGER PRIMARY KEY CHECK(singleton = 1), service_id TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL) STRICT;
7
- CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL CHECK(json_valid(value))) STRICT;
8
- CREATE TABLE sessions (id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, browser_name TEXT NOT NULL, paired_at INTEGER NOT NULL, last_used_at INTEGER NOT NULL, revoked_at INTEGER) STRICT;
9
- CREATE TABLE pairing_codes (code_hash TEXT PRIMARY KEY, expires_at INTEGER NOT NULL, consumed_at INTEGER) STRICT;
10
- CREATE TABLE projects (id TEXT PRIMARY KEY, canonical_path TEXT NOT NULL UNIQUE, name TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('active','inactive')), is_git INTEGER NOT NULL CHECK(is_git IN (0,1))) STRICT;
11
- CREATE TABLE graphs (id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id), title TEXT NOT NULL, archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)), execution_revision INTEGER NOT NULL DEFAULT 0 CHECK(execution_revision >= 0), layout_revision INTEGER NOT NULL DEFAULT 0 CHECK(layout_revision >= 0), UNIQUE(id,project_id)) STRICT;
12
- CREATE TRIGGER graph_binding_immutable BEFORE UPDATE OF project_id ON graphs WHEN NEW.project_id != OLD.project_id BEGIN SELECT RAISE(ABORT,'graph binding immutable'); END;
13
- CREATE TABLE nodes (id TEXT PRIMARY KEY, graph_id TEXT NOT NULL REFERENCES graphs(id), type TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version > 0), current_version INTEGER NOT NULL CHECK(current_version > 0), x REAL NOT NULL, y REAL NOT NULL, read_only INTEGER NOT NULL DEFAULT 0 CHECK(read_only IN (0,1)), UNIQUE(id,graph_id), FOREIGN KEY(id,current_version) REFERENCES node_versions(node_id,version) DEFERRABLE INITIALLY DEFERRED) STRICT;
14
- CREATE TABLE node_versions (node_id TEXT NOT NULL REFERENCES nodes(id) DEFERRABLE INITIALLY DEFERRED, version INTEGER NOT NULL CHECK(version > 0), content TEXT NOT NULL CHECK(json_valid(content)), PRIMARY KEY(node_id,version)) STRICT;
15
- CREATE TRIGGER node_version_immutable BEFORE UPDATE ON node_versions BEGIN SELECT RAISE(ABORT,'immutable node version'); END;
16
- CREATE TABLE edges (id TEXT PRIMARY KEY, graph_id TEXT NOT NULL REFERENCES graphs(id), source_id TEXT NOT NULL, target_id TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('reference','delivery')), CHECK(source_id != target_id), UNIQUE(graph_id,source_id,target_id), FOREIGN KEY(source_id,graph_id) REFERENCES nodes(id,graph_id), FOREIGN KEY(target_id,graph_id) REFERENCES nodes(id,graph_id)) STRICT;
17
- CREATE TABLE blobs (sha256 TEXT PRIMARY KEY CHECK(length(sha256) = 64), bytes INTEGER NOT NULL CHECK(bytes >= 0)) STRICT;
18
- CREATE TABLE assets (id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id), shared INTEGER NOT NULL DEFAULT 0 CHECK(shared IN (0,1)), deleted_at INTEGER, unreferenced_since INTEGER) STRICT;
19
- CREATE TABLE asset_versions (asset_id TEXT NOT NULL REFERENCES assets(id), version INTEGER NOT NULL CHECK(version > 0), sha256 TEXT NOT NULL REFERENCES blobs(sha256), mime TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('processing','ready','failed')), representation_version INTEGER, PRIMARY KEY(asset_id,version)) STRICT;
20
- CREATE TRIGGER asset_version_content_immutable BEFORE UPDATE OF sha256,mime,asset_id,version ON asset_versions BEGIN SELECT RAISE(ABORT,'immutable asset version'); END;
21
- CREATE TABLE runs (sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, project_id TEXT NOT NULL, graph_id TEXT NOT NULL, node_id TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('execution','text_generation','image_generation')), status TEXT NOT NULL CHECK(status IN ('accepted','queued','preparing','running','waiting_answer','waiting_approval','agent_completed','finalizing','cancelling','reconciling','paused_restore','succeeded','failed','cancelled','interrupted')), input_digest TEXT NOT NULL, created_at TEXT NOT NULL, history_state TEXT NOT NULL DEFAULT 'retained' CHECK(history_state IN ('retained','cleared')), FOREIGN KEY(graph_id,project_id) REFERENCES graphs(id,project_id), FOREIGN KEY(node_id,graph_id) REFERENCES nodes(id,graph_id)) STRICT;
22
- CREATE UNIQUE INDEX one_active_run_per_node ON runs(node_id) WHERE status NOT IN ('succeeded','failed','cancelled','interrupted');
23
- CREATE TABLE snapshots (run_id TEXT PRIMARY KEY REFERENCES runs(id), input_digest TEXT NOT NULL, payload TEXT NOT NULL CHECK(json_valid(payload))) STRICT;
24
- CREATE TRIGGER snapshot_immutable BEFORE UPDATE ON snapshots BEGIN SELECT RAISE(ABORT,'immutable input snapshot'); END;
25
- CREATE TABLE outputs (run_id TEXT NOT NULL REFERENCES runs(id), output_key TEXT NOT NULL, asset_id TEXT NOT NULL, asset_version INTEGER NOT NULL, node_id TEXT REFERENCES nodes(id), PRIMARY KEY(run_id,output_key), FOREIGN KEY(asset_id,asset_version) REFERENCES asset_versions(asset_id,version)) STRICT;
26
- CREATE TABLE asset_references (id TEXT PRIMARY KEY, asset_id TEXT NOT NULL, asset_version INTEGER NOT NULL, node_id TEXT REFERENCES nodes(id), run_id TEXT REFERENCES runs(id), CHECK((node_id IS NOT NULL) != (run_id IS NOT NULL)), FOREIGN KEY(asset_id,asset_version) REFERENCES asset_versions(asset_id,version)) STRICT;
27
- CREATE INDEX asset_reference_version ON asset_references(asset_id,asset_version);
28
- CREATE TABLE events (sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, type TEXT NOT NULL, project_id TEXT REFERENCES projects(id), graph_id TEXT REFERENCES graphs(id), entity_id TEXT NOT NULL, revision INTEGER NOT NULL CHECK(revision >= 0), occurred_at TEXT NOT NULL, payload TEXT NOT NULL CHECK(json_valid(payload))) STRICT;
29
- CREATE TABLE interactions (id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(id), epoch TEXT NOT NULL, version INTEGER NOT NULL CHECK(version > 0), kind TEXT NOT NULL CHECK(kind IN ('question','approval')), status TEXT NOT NULL CHECK(status IN ('pending','answered','expired')), payload TEXT NOT NULL CHECK(json_valid(payload)), answer TEXT CHECK(answer IS NULL OR json_valid(answer))) STRICT;
30
- CREATE TABLE occupancy (run_id TEXT PRIMARY KEY REFERENCES runs(id), slot INTEGER NOT NULL UNIQUE CHECK(slot >= 0), project_id TEXT UNIQUE REFERENCES projects(id)) STRICT;
31
- CREATE TABLE idempotency (scope TEXT NOT NULL, key TEXT NOT NULL, request_hash TEXT NOT NULL, response TEXT NOT NULL CHECK(json_valid(response)), PRIMARY KEY(scope,key)) STRICT;
32
- CREATE TABLE plugin_contracts (type TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version > 0), api_version TEXT NOT NULL, contract TEXT NOT NULL CHECK(json_valid(contract)), PRIMARY KEY(type,schema_version)) STRICT;
33
- CREATE TABLE backups (id TEXT PRIMARY KEY, state TEXT NOT NULL CHECK(state IN ('creating','ready','failed')), created_at TEXT NOT NULL, bytes INTEGER, sha256 TEXT, path TEXT NOT NULL) STRICT;
34
- ` }, { version: 2, sql: `
35
- CREATE INDEX runs_project_sequence ON runs(project_id,sequence);
36
- CREATE INDEX events_graph_sequence ON events(graph_id,sequence);
37
- CREATE INDEX sessions_last_used ON sessions(last_used_at);
38
- INSERT INTO settings(key,value) VALUES ('capacity','4'), ('modelDefaults','null');
39
- ` }, { version: 3, sql: `
40
- CREATE TABLE trusted_origins (origin TEXT PRIMARY KEY, created_at INTEGER NOT NULL) STRICT;
41
- ALTER TABLE pairing_codes ADD COLUMN origin TEXT NOT NULL DEFAULT '';
42
- ALTER TABLE sessions ADD COLUMN origin TEXT NOT NULL DEFAULT '';
43
- ` }, { version: 4, sql: RESOURCE_SCHEMA }, { version: 5, sql: WORKFLOW_SCHEMA }, { version: 6, sql: ASSET_BUSINESS_SCHEMA }, { version: 7, sql: PLUGIN_SCHEMA }, { version: 8, sql: `
44
- ALTER TABLE graphs ADD COLUMN trashed INTEGER NOT NULL DEFAULT 0 CHECK(trashed IN (0,1));
45
- ALTER TABLE idempotency ADD COLUMN invalidated INTEGER NOT NULL DEFAULT 0 CHECK(invalidated IN (0,1));
46
- CREATE TABLE run_file_deletions (run_id TEXT PRIMARY KEY, completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0,1))) STRICT;
47
- ` }, { version: 9, sql: `
48
- ALTER TABLE nodes ADD COLUMN width REAL CHECK(width IS NULL OR (width>0 AND width<=1000000000));
49
- ALTER TABLE nodes ADD COLUMN height REAL CHECK(height IS NULL OR (height>0 AND height<=1000000000));
50
- CREATE TABLE IF NOT EXISTS run_file_deletions (run_id TEXT PRIMARY KEY, completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0,1))) STRICT;
51
- ` }, { version: 10, sql: `
52
- ALTER TABLE pairing_codes ADD COLUMN client_code TEXT;
53
- ` }, { version: 11, sql: `
54
- -- Historical nodes have no recoverable creation time. Freeze their insertion
55
- -- order before nodes created after this migration.
56
- ALTER TABLE nodes ADD COLUMN created_at INTEGER NOT NULL DEFAULT 0 CHECK(created_at >= 0);
57
- ALTER TABLE nodes ADD COLUMN creation_order INTEGER NOT NULL DEFAULT 0 CHECK(creation_order >= 0);
58
- UPDATE nodes SET creation_order=rowid;
59
- CREATE UNIQUE INDEX nodes_creation_order ON nodes(creation_order);
60
- CREATE INDEX nodes_graph_creation ON nodes(graph_id,created_at,creation_order);
61
- ` }, { version: 12, sql: `
62
- CREATE TABLE image_providers (id TEXT PRIMARY KEY, name TEXT NOT NULL, driver TEXT NOT NULL CHECK(driver IN ('openai')), endpoint TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 0 CHECK(enabled IN (0,1)), revision INTEGER NOT NULL CHECK(revision > 0), credential_revision INTEGER, deleted INTEGER NOT NULL DEFAULT 0 CHECK(deleted IN (0,1))) STRICT;
63
- CREATE TABLE image_provider_models (provider_id TEXT NOT NULL REFERENCES image_providers(id), id TEXT NOT NULL, name TEXT NOT NULL, modes TEXT NOT NULL CHECK(json_valid(modes)), formats TEXT NOT NULL CHECK(json_valid(formats)), sizes TEXT NOT NULL CHECK(json_valid(sizes)), qualities TEXT NOT NULL CHECK(json_valid(qualities)), is_default INTEGER NOT NULL DEFAULT 0 CHECK(is_default IN (0,1)), verified_at TEXT, PRIMARY KEY(provider_id,id)) STRICT;
64
- CREATE UNIQUE INDEX image_provider_default_model ON image_provider_models(provider_id) WHERE is_default=1;
65
- ` }, { version: 13, sql: `
66
- CREATE TABLE image_provider_config_versions (provider_id TEXT NOT NULL REFERENCES image_providers(id), revision INTEGER NOT NULL CHECK(revision > 0), payload TEXT NOT NULL CHECK(json_valid(payload)), PRIMARY KEY(provider_id,revision)) STRICT;
67
- CREATE TRIGGER image_provider_config_immutable BEFORE UPDATE ON image_provider_config_versions BEGIN SELECT RAISE(ABORT,'immutable image provider configuration'); END;
68
- INSERT INTO image_provider_config_versions(provider_id,revision,payload)
69
- SELECT p.id,p.revision,json_object('endpoint',p.endpoint,'driver',p.driver,'models',
70
- coalesce((SELECT json_group_array(json_object('id',m.id,'name',m.name,'modes',json(m.modes),'formats',json(m.formats),'sizes',json(m.sizes),'qualities',json(m.qualities),'isDefault',json(m.is_default)))
71
- FROM image_provider_models m WHERE m.provider_id=p.id),json('[]')))
72
- FROM image_providers p;
73
- ` }, { version: 14, sql: `
74
- ALTER TABLE image_providers ADD COLUMN credential_counter INTEGER NOT NULL DEFAULT 0 CHECK(credential_counter >= 0);
75
- UPDATE image_providers SET credential_counter=coalesce(credential_revision,0);
76
- CREATE TABLE image_provider_revoked_credentials (provider_id TEXT NOT NULL REFERENCES image_providers(id), revision INTEGER NOT NULL CHECK(revision > 0), PRIMARY KEY(provider_id,revision)) STRICT;
77
- ` }, { version: 15, sql: `
78
- ALTER TABLE nodes ADD COLUMN undo_expires_at INTEGER;
79
- CREATE INDEX nodes_undo_expiry ON nodes(undo_expires_at) WHERE undo_expires_at IS NOT NULL;
80
- ` }, { version: 16, sql: `
81
- CREATE TABLE execution_outputs (
82
- node_id TEXT PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
83
- execution_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
84
- CHECK(node_id != execution_node_id)
85
- ) STRICT;
86
- CREATE INDEX execution_outputs_owner ON execution_outputs(execution_node_id);
87
- -- Infer only service originals backed by an execution Run.
88
- INSERT INTO execution_outputs(node_id,execution_node_id)
89
- SELECT n.id,r.node_id FROM nodes n JOIN node_versions v ON v.node_id=n.id AND v.version=n.current_version
90
- JOIN runs r ON r.id=json_extract(v.content,'$.runId') AND r.graph_id=n.graph_id AND r.kind='execution'
91
- JOIN nodes owner ON owner.id=r.node_id AND owner.deleted=0
92
- WHERE n.deleted=0 AND n.read_only_reason='original' AND n.id!=r.node_id;
93
- INSERT OR IGNORE INTO execution_outputs(node_id,execution_node_id)
94
- SELECT e.target_id,e.source_id FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON t.id=e.target_id
95
- WHERE e.kind='delivery' AND s.type='execution' AND s.deleted=0 AND t.deleted=0;
96
- -- Only remove legacy artifact-to-artifact references within the same Run.
97
- DELETE FROM edges WHERE id IN (
98
- SELECT e.id FROM edges e JOIN execution_outputs s ON s.node_id=e.source_id
99
- JOIN execution_outputs t ON t.node_id=e.target_id AND t.execution_node_id=s.execution_node_id
100
- JOIN nodes sn ON sn.id=s.node_id JOIN node_versions sv ON sv.node_id=sn.id AND sv.version=sn.current_version
101
- JOIN nodes tn ON tn.id=t.node_id JOIN node_versions tv ON tv.node_id=tn.id AND tv.version=tn.current_version
102
- WHERE e.kind='reference' AND sn.type='document' AND tn.type='image'
103
- AND sn.read_only_reason='original' AND tn.read_only_reason='original'
104
- AND json_extract(sv.content,'$.mime')='text/markdown'
105
- AND json_extract(sv.content,'$.runId')=json_extract(tv.content,'$.runId')
106
- );
107
- INSERT INTO edges(id,graph_id,source_id,target_id,kind)
108
- SELECT 'output-'||lower(hex(randomblob(16))),n.graph_id,o.execution_node_id,o.node_id,'delivery'
109
- FROM execution_outputs o JOIN nodes n ON n.id=o.node_id
110
- WHERE NOT EXISTS(SELECT 1 FROM edges e WHERE e.source_id=o.execution_node_id AND e.target_id=o.node_id);
111
- CREATE TRIGGER register_execution_output AFTER INSERT ON edges WHEN NEW.kind='delivery'
112
- BEGIN INSERT INTO execution_outputs(node_id,execution_node_id) VALUES(NEW.target_id,NEW.source_id)
113
- ON CONFLICT(node_id) DO UPDATE SET execution_node_id=excluded.execution_node_id; END;
114
- UPDATE graphs SET execution_revision=execution_revision+1 WHERE id IN
115
- (SELECT n.graph_id FROM execution_outputs o JOIN nodes n ON n.id=o.node_id);
116
- UPDATE idempotency SET invalidated=1 WHERE scope LIKE '%:graph.command:%';
117
- ` }, { version: 17, sql: `
118
- CREATE TABLE project_file_bindings (
119
- node_id TEXT PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
120
- service_id TEXT NOT NULL,
121
- project_id TEXT NOT NULL REFERENCES projects(id),
122
- relative_path TEXT NOT NULL CHECK(relative_path != ''),
123
- UNIQUE(project_id,relative_path,node_id)
124
- ) STRICT;
125
- CREATE INDEX project_file_bindings_path ON project_file_bindings(project_id,relative_path);
126
- CREATE TABLE project_file_observations (
127
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
128
- relative_path TEXT NOT NULL CHECK(relative_path != ''),
129
- state TEXT NOT NULL CHECK(state IN ('available','missing','unavailable')),
130
- name TEXT NOT NULL,
131
- mime TEXT,
132
- bytes INTEGER CHECK(bytes IS NULL OR bytes >= 0),
133
- change_token TEXT,
134
- observed_at INTEGER NOT NULL CHECK(observed_at >= 0),
135
- PRIMARY KEY(project_id,relative_path)
136
- ) STRICT;
137
- CREATE TABLE project_file_outputs (
138
- run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
139
- output_key TEXT NOT NULL,
140
- project_id TEXT NOT NULL REFERENCES projects(id),
141
- relative_path TEXT NOT NULL CHECK(relative_path != ''),
142
- bytes INTEGER NOT NULL CHECK(bytes >= 0),
143
- sha256 TEXT NOT NULL CHECK(length(sha256) = 64),
144
- PRIMARY KEY(run_id,output_key),
145
- UNIQUE(run_id,relative_path)
146
- ) STRICT;
147
- CREATE INDEX project_file_outputs_path ON project_file_outputs(project_id,relative_path);
148
-
149
- -- Keep the current-source index correct for every writer of node_versions,
150
- -- including imports, generation acceptance, undo/restore and future commands.
151
- CREATE TRIGGER project_file_binding_on_version_insert AFTER INSERT ON node_versions
152
- WHEN (SELECT current_version FROM nodes WHERE id=NEW.node_id)=NEW.version
153
- BEGIN
154
- DELETE FROM project_file_bindings WHERE node_id=NEW.node_id;
155
- INSERT INTO project_file_bindings(node_id,service_id,project_id,relative_path)
156
- SELECT NEW.node_id,json_extract(NEW.content,'$.source.serviceId'),json_extract(NEW.content,'$.source.projectId'),json_extract(NEW.content,'$.source.relativePath')
157
- WHERE json_extract(NEW.content,'$.source.kind')='project-file';
158
- END;
159
- CREATE TRIGGER project_file_binding_on_current_version AFTER UPDATE OF current_version ON nodes
160
- BEGIN
161
- DELETE FROM project_file_bindings WHERE node_id=NEW.id;
162
- INSERT INTO project_file_bindings(node_id,service_id,project_id,relative_path)
163
- SELECT NEW.id,json_extract(v.content,'$.source.serviceId'),json_extract(v.content,'$.source.projectId'),json_extract(v.content,'$.source.relativePath')
164
- FROM node_versions v WHERE v.node_id=NEW.id AND v.version=NEW.current_version
165
- AND json_extract(v.content,'$.source.kind')='project-file';
166
- END;
167
- INSERT INTO project_file_bindings(node_id,service_id,project_id,relative_path)
168
- SELECT n.id,json_extract(v.content,'$.source.serviceId'),json_extract(v.content,'$.source.projectId'),json_extract(v.content,'$.source.relativePath')
169
- FROM nodes n JOIN node_versions v ON v.node_id=n.id AND v.version=n.current_version
170
- JOIN graphs g ON g.id=n.graph_id
171
- WHERE json_extract(v.content,'$.source.kind')='project-file'
172
- AND json_extract(v.content,'$.source.projectId')=g.project_id;
173
- ` }, { version: 18, sql: `
174
- CREATE TABLE edges_next (id TEXT PRIMARY KEY, graph_id TEXT NOT NULL REFERENCES graphs(id), source_id TEXT NOT NULL, target_id TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('reference','execution','delivery')), CHECK(source_id != target_id), UNIQUE(graph_id,source_id,target_id), FOREIGN KEY(source_id,graph_id) REFERENCES nodes(id,graph_id), FOREIGN KEY(target_id,graph_id) REFERENCES nodes(id,graph_id)) STRICT;
175
- INSERT INTO edges_next SELECT * FROM edges;
176
- DROP TRIGGER register_execution_output;
177
- DROP TABLE edges;
178
- ALTER TABLE edges_next RENAME TO edges;
179
- CREATE INDEX execution_edges_source ON edges(graph_id,source_id) WHERE kind='execution';
180
- CREATE TRIGGER register_execution_output AFTER INSERT ON edges WHEN NEW.kind='delivery'
181
- BEGIN INSERT INTO execution_outputs(node_id,execution_node_id) VALUES(NEW.target_id,NEW.source_id)
182
- ON CONFLICT(node_id) DO UPDATE SET execution_node_id=excluded.execution_node_id; END;
183
- ` }, { version: 19, sql: `
184
- CREATE TABLE execution_input_snapshots (run_id TEXT PRIMARY KEY REFERENCES runs(id),payload TEXT NOT NULL CHECK(json_valid(payload))) STRICT;
185
- CREATE TRIGGER execution_input_snapshot_immutable BEFORE UPDATE ON execution_input_snapshots BEGIN SELECT RAISE(ABORT,'immutable execution input snapshot'); END;
186
- ` }, { version: 20, sql: `
187
- CREATE TABLE run_notifications (
188
- run_id TEXT PRIMARY KEY REFERENCES runs(id) ON DELETE CASCADE,
189
- status TEXT NOT NULL CHECK(status IN ('succeeded','failed','waiting_answer','waiting_approval','interrupted')),
190
- revision INTEGER NOT NULL CHECK(revision > 0),
191
- created_at TEXT NOT NULL,
192
- read_at TEXT
193
- ) STRICT;
194
- CREATE INDEX run_notifications_unread ON run_notifications(read_at,created_at) WHERE read_at IS NULL;
195
- INSERT INTO run_notifications(run_id,status,revision,created_at,read_at)
196
- SELECT id,status,1,created_at,NULL FROM runs WHERE status IN ('waiting_answer','waiting_approval');
197
- ` }, { version: 21, sql: `
198
- UPDATE settings SET value='4' WHERE key='capacity' AND value='2';
199
- ` }, { version: 22, sql: `
200
- CREATE TABLE graph_document_versions (
201
- id INTEGER PRIMARY KEY AUTOINCREMENT,
202
- graph_id TEXT NOT NULL REFERENCES graphs(id) ON DELETE CASCADE,
203
- document TEXT NOT NULL CHECK(json_valid(document))
204
- ) STRICT;
205
- CREATE INDEX graph_document_versions_graph ON graph_document_versions(graph_id,id);
206
- CREATE TABLE graph_document_heads (
207
- graph_id TEXT PRIMARY KEY REFERENCES graphs(id) ON DELETE CASCADE,
208
- cursor INTEGER NOT NULL REFERENCES graph_document_versions(id),
209
- execution_revision INTEGER NOT NULL,
210
- layout_revision INTEGER NOT NULL,
211
- run_sequence INTEGER NOT NULL,
212
- expires_at INTEGER NOT NULL
213
- ) STRICT;
214
- CREATE TABLE graph_document_resources (
215
- version_id INTEGER NOT NULL REFERENCES graph_document_versions(id) ON DELETE CASCADE,
216
- resource_id TEXT NOT NULL,
217
- resource_version INTEGER NOT NULL,
218
- PRIMARY KEY(version_id,resource_id,resource_version),
219
- FOREIGN KEY(resource_id,resource_version) REFERENCES canvas_resource_versions(resource_id,version)
220
- ) STRICT;
5
+ export const MIGRATIONS = [{ version: 1, sql: `
6
+ CREATE TABLE identity (singleton INTEGER PRIMARY KEY CHECK(singleton = 1), service_id TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL) STRICT;
7
+ CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL CHECK(json_valid(value))) STRICT;
8
+ CREATE TABLE sessions (id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, browser_name TEXT NOT NULL, paired_at INTEGER NOT NULL, last_used_at INTEGER NOT NULL, revoked_at INTEGER) STRICT;
9
+ CREATE TABLE pairing_codes (code_hash TEXT PRIMARY KEY, expires_at INTEGER NOT NULL, consumed_at INTEGER) STRICT;
10
+ CREATE TABLE projects (id TEXT PRIMARY KEY, canonical_path TEXT NOT NULL UNIQUE, name TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('active','inactive')), is_git INTEGER NOT NULL CHECK(is_git IN (0,1))) STRICT;
11
+ CREATE TABLE graphs (id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id), title TEXT NOT NULL, archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)), execution_revision INTEGER NOT NULL DEFAULT 0 CHECK(execution_revision >= 0), layout_revision INTEGER NOT NULL DEFAULT 0 CHECK(layout_revision >= 0), UNIQUE(id,project_id)) STRICT;
12
+ CREATE TRIGGER graph_binding_immutable BEFORE UPDATE OF project_id ON graphs WHEN NEW.project_id != OLD.project_id BEGIN SELECT RAISE(ABORT,'graph binding immutable'); END;
13
+ CREATE TABLE nodes (id TEXT PRIMARY KEY, graph_id TEXT NOT NULL REFERENCES graphs(id), type TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version > 0), current_version INTEGER NOT NULL CHECK(current_version > 0), x REAL NOT NULL, y REAL NOT NULL, read_only INTEGER NOT NULL DEFAULT 0 CHECK(read_only IN (0,1)), UNIQUE(id,graph_id), FOREIGN KEY(id,current_version) REFERENCES node_versions(node_id,version) DEFERRABLE INITIALLY DEFERRED) STRICT;
14
+ CREATE TABLE node_versions (node_id TEXT NOT NULL REFERENCES nodes(id) DEFERRABLE INITIALLY DEFERRED, version INTEGER NOT NULL CHECK(version > 0), content TEXT NOT NULL CHECK(json_valid(content)), PRIMARY KEY(node_id,version)) STRICT;
15
+ CREATE TRIGGER node_version_immutable BEFORE UPDATE ON node_versions BEGIN SELECT RAISE(ABORT,'immutable node version'); END;
16
+ CREATE TABLE edges (id TEXT PRIMARY KEY, graph_id TEXT NOT NULL REFERENCES graphs(id), source_id TEXT NOT NULL, target_id TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('reference','delivery')), CHECK(source_id != target_id), UNIQUE(graph_id,source_id,target_id), FOREIGN KEY(source_id,graph_id) REFERENCES nodes(id,graph_id), FOREIGN KEY(target_id,graph_id) REFERENCES nodes(id,graph_id)) STRICT;
17
+ CREATE TABLE blobs (sha256 TEXT PRIMARY KEY CHECK(length(sha256) = 64), bytes INTEGER NOT NULL CHECK(bytes >= 0)) STRICT;
18
+ CREATE TABLE assets (id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id), shared INTEGER NOT NULL DEFAULT 0 CHECK(shared IN (0,1)), deleted_at INTEGER, unreferenced_since INTEGER) STRICT;
19
+ CREATE TABLE asset_versions (asset_id TEXT NOT NULL REFERENCES assets(id), version INTEGER NOT NULL CHECK(version > 0), sha256 TEXT NOT NULL REFERENCES blobs(sha256), mime TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('processing','ready','failed')), representation_version INTEGER, PRIMARY KEY(asset_id,version)) STRICT;
20
+ CREATE TRIGGER asset_version_content_immutable BEFORE UPDATE OF sha256,mime,asset_id,version ON asset_versions BEGIN SELECT RAISE(ABORT,'immutable asset version'); END;
21
+ CREATE TABLE runs (sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, project_id TEXT NOT NULL, graph_id TEXT NOT NULL, node_id TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('execution','text_generation','image_generation')), status TEXT NOT NULL CHECK(status IN ('accepted','queued','preparing','running','waiting_answer','waiting_approval','agent_completed','finalizing','cancelling','reconciling','paused_restore','succeeded','failed','cancelled','interrupted')), input_digest TEXT NOT NULL, created_at TEXT NOT NULL, history_state TEXT NOT NULL DEFAULT 'retained' CHECK(history_state IN ('retained','cleared')), FOREIGN KEY(graph_id,project_id) REFERENCES graphs(id,project_id), FOREIGN KEY(node_id,graph_id) REFERENCES nodes(id,graph_id)) STRICT;
22
+ CREATE UNIQUE INDEX one_active_run_per_node ON runs(node_id) WHERE status NOT IN ('succeeded','failed','cancelled','interrupted');
23
+ CREATE TABLE snapshots (run_id TEXT PRIMARY KEY REFERENCES runs(id), input_digest TEXT NOT NULL, payload TEXT NOT NULL CHECK(json_valid(payload))) STRICT;
24
+ CREATE TRIGGER snapshot_immutable BEFORE UPDATE ON snapshots BEGIN SELECT RAISE(ABORT,'immutable input snapshot'); END;
25
+ CREATE TABLE outputs (run_id TEXT NOT NULL REFERENCES runs(id), output_key TEXT NOT NULL, asset_id TEXT NOT NULL, asset_version INTEGER NOT NULL, node_id TEXT REFERENCES nodes(id), PRIMARY KEY(run_id,output_key), FOREIGN KEY(asset_id,asset_version) REFERENCES asset_versions(asset_id,version)) STRICT;
26
+ CREATE TABLE asset_references (id TEXT PRIMARY KEY, asset_id TEXT NOT NULL, asset_version INTEGER NOT NULL, node_id TEXT REFERENCES nodes(id), run_id TEXT REFERENCES runs(id), CHECK((node_id IS NOT NULL) != (run_id IS NOT NULL)), FOREIGN KEY(asset_id,asset_version) REFERENCES asset_versions(asset_id,version)) STRICT;
27
+ CREATE INDEX asset_reference_version ON asset_references(asset_id,asset_version);
28
+ CREATE TABLE events (sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, type TEXT NOT NULL, project_id TEXT REFERENCES projects(id), graph_id TEXT REFERENCES graphs(id), entity_id TEXT NOT NULL, revision INTEGER NOT NULL CHECK(revision >= 0), occurred_at TEXT NOT NULL, payload TEXT NOT NULL CHECK(json_valid(payload))) STRICT;
29
+ CREATE TABLE interactions (id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(id), epoch TEXT NOT NULL, version INTEGER NOT NULL CHECK(version > 0), kind TEXT NOT NULL CHECK(kind IN ('question','approval')), status TEXT NOT NULL CHECK(status IN ('pending','answered','expired')), payload TEXT NOT NULL CHECK(json_valid(payload)), answer TEXT CHECK(answer IS NULL OR json_valid(answer))) STRICT;
30
+ CREATE TABLE occupancy (run_id TEXT PRIMARY KEY REFERENCES runs(id), slot INTEGER NOT NULL UNIQUE CHECK(slot >= 0), project_id TEXT UNIQUE REFERENCES projects(id)) STRICT;
31
+ CREATE TABLE idempotency (scope TEXT NOT NULL, key TEXT NOT NULL, request_hash TEXT NOT NULL, response TEXT NOT NULL CHECK(json_valid(response)), PRIMARY KEY(scope,key)) STRICT;
32
+ CREATE TABLE plugin_contracts (type TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version > 0), api_version TEXT NOT NULL, contract TEXT NOT NULL CHECK(json_valid(contract)), PRIMARY KEY(type,schema_version)) STRICT;
33
+ CREATE TABLE backups (id TEXT PRIMARY KEY, state TEXT NOT NULL CHECK(state IN ('creating','ready','failed')), created_at TEXT NOT NULL, bytes INTEGER, sha256 TEXT, path TEXT NOT NULL) STRICT;
34
+ ` }, { version: 2, sql: `
35
+ CREATE INDEX runs_project_sequence ON runs(project_id,sequence);
36
+ CREATE INDEX events_graph_sequence ON events(graph_id,sequence);
37
+ CREATE INDEX sessions_last_used ON sessions(last_used_at);
38
+ INSERT INTO settings(key,value) VALUES ('capacity','4'), ('modelDefaults','null');
39
+ ` }, { version: 3, sql: `
40
+ CREATE TABLE trusted_origins (origin TEXT PRIMARY KEY, created_at INTEGER NOT NULL) STRICT;
41
+ ALTER TABLE pairing_codes ADD COLUMN origin TEXT NOT NULL DEFAULT '';
42
+ ALTER TABLE sessions ADD COLUMN origin TEXT NOT NULL DEFAULT '';
43
+ ` }, { version: 4, sql: RESOURCE_SCHEMA }, { version: 5, sql: WORKFLOW_SCHEMA }, { version: 6, sql: ASSET_BUSINESS_SCHEMA }, { version: 7, sql: PLUGIN_SCHEMA }, { version: 8, sql: `
44
+ ALTER TABLE graphs ADD COLUMN trashed INTEGER NOT NULL DEFAULT 0 CHECK(trashed IN (0,1));
45
+ ALTER TABLE idempotency ADD COLUMN invalidated INTEGER NOT NULL DEFAULT 0 CHECK(invalidated IN (0,1));
46
+ CREATE TABLE run_file_deletions (run_id TEXT PRIMARY KEY, completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0,1))) STRICT;
47
+ ` }, { version: 9, sql: `
48
+ ALTER TABLE nodes ADD COLUMN width REAL CHECK(width IS NULL OR (width>0 AND width<=1000000000));
49
+ ALTER TABLE nodes ADD COLUMN height REAL CHECK(height IS NULL OR (height>0 AND height<=1000000000));
50
+ CREATE TABLE IF NOT EXISTS run_file_deletions (run_id TEXT PRIMARY KEY, completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0,1))) STRICT;
51
+ ` }, { version: 10, sql: `
52
+ ALTER TABLE pairing_codes ADD COLUMN client_code TEXT;
53
+ ` }, { version: 11, sql: `
54
+ -- Historical nodes have no recoverable creation time. Freeze their insertion
55
+ -- order before nodes created after this migration.
56
+ ALTER TABLE nodes ADD COLUMN created_at INTEGER NOT NULL DEFAULT 0 CHECK(created_at >= 0);
57
+ ALTER TABLE nodes ADD COLUMN creation_order INTEGER NOT NULL DEFAULT 0 CHECK(creation_order >= 0);
58
+ UPDATE nodes SET creation_order=rowid;
59
+ CREATE UNIQUE INDEX nodes_creation_order ON nodes(creation_order);
60
+ CREATE INDEX nodes_graph_creation ON nodes(graph_id,created_at,creation_order);
61
+ ` }, { version: 12, sql: `
62
+ CREATE TABLE image_providers (id TEXT PRIMARY KEY, name TEXT NOT NULL, driver TEXT NOT NULL CHECK(driver IN ('openai')), endpoint TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 0 CHECK(enabled IN (0,1)), revision INTEGER NOT NULL CHECK(revision > 0), credential_revision INTEGER, deleted INTEGER NOT NULL DEFAULT 0 CHECK(deleted IN (0,1))) STRICT;
63
+ CREATE TABLE image_provider_models (provider_id TEXT NOT NULL REFERENCES image_providers(id), id TEXT NOT NULL, name TEXT NOT NULL, modes TEXT NOT NULL CHECK(json_valid(modes)), formats TEXT NOT NULL CHECK(json_valid(formats)), sizes TEXT NOT NULL CHECK(json_valid(sizes)), qualities TEXT NOT NULL CHECK(json_valid(qualities)), is_default INTEGER NOT NULL DEFAULT 0 CHECK(is_default IN (0,1)), verified_at TEXT, PRIMARY KEY(provider_id,id)) STRICT;
64
+ CREATE UNIQUE INDEX image_provider_default_model ON image_provider_models(provider_id) WHERE is_default=1;
65
+ ` }, { version: 13, sql: `
66
+ CREATE TABLE image_provider_config_versions (provider_id TEXT NOT NULL REFERENCES image_providers(id), revision INTEGER NOT NULL CHECK(revision > 0), payload TEXT NOT NULL CHECK(json_valid(payload)), PRIMARY KEY(provider_id,revision)) STRICT;
67
+ CREATE TRIGGER image_provider_config_immutable BEFORE UPDATE ON image_provider_config_versions BEGIN SELECT RAISE(ABORT,'immutable image provider configuration'); END;
68
+ INSERT INTO image_provider_config_versions(provider_id,revision,payload)
69
+ SELECT p.id,p.revision,json_object('endpoint',p.endpoint,'driver',p.driver,'models',
70
+ coalesce((SELECT json_group_array(json_object('id',m.id,'name',m.name,'modes',json(m.modes),'formats',json(m.formats),'sizes',json(m.sizes),'qualities',json(m.qualities),'isDefault',json(m.is_default)))
71
+ FROM image_provider_models m WHERE m.provider_id=p.id),json('[]')))
72
+ FROM image_providers p;
73
+ ` }, { version: 14, sql: `
74
+ ALTER TABLE image_providers ADD COLUMN credential_counter INTEGER NOT NULL DEFAULT 0 CHECK(credential_counter >= 0);
75
+ UPDATE image_providers SET credential_counter=coalesce(credential_revision,0);
76
+ CREATE TABLE image_provider_revoked_credentials (provider_id TEXT NOT NULL REFERENCES image_providers(id), revision INTEGER NOT NULL CHECK(revision > 0), PRIMARY KEY(provider_id,revision)) STRICT;
77
+ ` }, { version: 15, sql: `
78
+ ALTER TABLE nodes ADD COLUMN undo_expires_at INTEGER;
79
+ CREATE INDEX nodes_undo_expiry ON nodes(undo_expires_at) WHERE undo_expires_at IS NOT NULL;
80
+ ` }, { version: 16, sql: `
81
+ CREATE TABLE execution_outputs (
82
+ node_id TEXT PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
83
+ execution_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
84
+ CHECK(node_id != execution_node_id)
85
+ ) STRICT;
86
+ CREATE INDEX execution_outputs_owner ON execution_outputs(execution_node_id);
87
+ -- Infer only service originals backed by an execution Run.
88
+ INSERT INTO execution_outputs(node_id,execution_node_id)
89
+ SELECT n.id,r.node_id FROM nodes n JOIN node_versions v ON v.node_id=n.id AND v.version=n.current_version
90
+ JOIN runs r ON r.id=json_extract(v.content,'$.runId') AND r.graph_id=n.graph_id AND r.kind='execution'
91
+ JOIN nodes owner ON owner.id=r.node_id AND owner.deleted=0
92
+ WHERE n.deleted=0 AND n.read_only_reason='original' AND n.id!=r.node_id;
93
+ INSERT OR IGNORE INTO execution_outputs(node_id,execution_node_id)
94
+ SELECT e.target_id,e.source_id FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON t.id=e.target_id
95
+ WHERE e.kind='delivery' AND s.type='execution' AND s.deleted=0 AND t.deleted=0;
96
+ -- Only remove legacy artifact-to-artifact references within the same Run.
97
+ DELETE FROM edges WHERE id IN (
98
+ SELECT e.id FROM edges e JOIN execution_outputs s ON s.node_id=e.source_id
99
+ JOIN execution_outputs t ON t.node_id=e.target_id AND t.execution_node_id=s.execution_node_id
100
+ JOIN nodes sn ON sn.id=s.node_id JOIN node_versions sv ON sv.node_id=sn.id AND sv.version=sn.current_version
101
+ JOIN nodes tn ON tn.id=t.node_id JOIN node_versions tv ON tv.node_id=tn.id AND tv.version=tn.current_version
102
+ WHERE e.kind='reference' AND sn.type='document' AND tn.type='image'
103
+ AND sn.read_only_reason='original' AND tn.read_only_reason='original'
104
+ AND json_extract(sv.content,'$.mime')='text/markdown'
105
+ AND json_extract(sv.content,'$.runId')=json_extract(tv.content,'$.runId')
106
+ );
107
+ INSERT INTO edges(id,graph_id,source_id,target_id,kind)
108
+ SELECT 'output-'||lower(hex(randomblob(16))),n.graph_id,o.execution_node_id,o.node_id,'delivery'
109
+ FROM execution_outputs o JOIN nodes n ON n.id=o.node_id
110
+ WHERE NOT EXISTS(SELECT 1 FROM edges e WHERE e.source_id=o.execution_node_id AND e.target_id=o.node_id);
111
+ CREATE TRIGGER register_execution_output AFTER INSERT ON edges WHEN NEW.kind='delivery'
112
+ BEGIN INSERT INTO execution_outputs(node_id,execution_node_id) VALUES(NEW.target_id,NEW.source_id)
113
+ ON CONFLICT(node_id) DO UPDATE SET execution_node_id=excluded.execution_node_id; END;
114
+ UPDATE graphs SET execution_revision=execution_revision+1 WHERE id IN
115
+ (SELECT n.graph_id FROM execution_outputs o JOIN nodes n ON n.id=o.node_id);
116
+ UPDATE idempotency SET invalidated=1 WHERE scope LIKE '%:graph.command:%';
117
+ ` }, { version: 17, sql: `
118
+ CREATE TABLE project_file_bindings (
119
+ node_id TEXT PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
120
+ service_id TEXT NOT NULL,
121
+ project_id TEXT NOT NULL REFERENCES projects(id),
122
+ relative_path TEXT NOT NULL CHECK(relative_path != ''),
123
+ UNIQUE(project_id,relative_path,node_id)
124
+ ) STRICT;
125
+ CREATE INDEX project_file_bindings_path ON project_file_bindings(project_id,relative_path);
126
+ CREATE TABLE project_file_observations (
127
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
128
+ relative_path TEXT NOT NULL CHECK(relative_path != ''),
129
+ state TEXT NOT NULL CHECK(state IN ('available','missing','unavailable')),
130
+ name TEXT NOT NULL,
131
+ mime TEXT,
132
+ bytes INTEGER CHECK(bytes IS NULL OR bytes >= 0),
133
+ change_token TEXT,
134
+ observed_at INTEGER NOT NULL CHECK(observed_at >= 0),
135
+ PRIMARY KEY(project_id,relative_path)
136
+ ) STRICT;
137
+ CREATE TABLE project_file_outputs (
138
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
139
+ output_key TEXT NOT NULL,
140
+ project_id TEXT NOT NULL REFERENCES projects(id),
141
+ relative_path TEXT NOT NULL CHECK(relative_path != ''),
142
+ bytes INTEGER NOT NULL CHECK(bytes >= 0),
143
+ sha256 TEXT NOT NULL CHECK(length(sha256) = 64),
144
+ PRIMARY KEY(run_id,output_key),
145
+ UNIQUE(run_id,relative_path)
146
+ ) STRICT;
147
+ CREATE INDEX project_file_outputs_path ON project_file_outputs(project_id,relative_path);
148
+
149
+ -- Keep the current-source index correct for every writer of node_versions,
150
+ -- including imports, generation acceptance, undo/restore and future commands.
151
+ CREATE TRIGGER project_file_binding_on_version_insert AFTER INSERT ON node_versions
152
+ WHEN (SELECT current_version FROM nodes WHERE id=NEW.node_id)=NEW.version
153
+ BEGIN
154
+ DELETE FROM project_file_bindings WHERE node_id=NEW.node_id;
155
+ INSERT INTO project_file_bindings(node_id,service_id,project_id,relative_path)
156
+ SELECT NEW.node_id,json_extract(NEW.content,'$.source.serviceId'),json_extract(NEW.content,'$.source.projectId'),json_extract(NEW.content,'$.source.relativePath')
157
+ WHERE json_extract(NEW.content,'$.source.kind')='project-file';
158
+ END;
159
+ CREATE TRIGGER project_file_binding_on_current_version AFTER UPDATE OF current_version ON nodes
160
+ BEGIN
161
+ DELETE FROM project_file_bindings WHERE node_id=NEW.id;
162
+ INSERT INTO project_file_bindings(node_id,service_id,project_id,relative_path)
163
+ SELECT NEW.id,json_extract(v.content,'$.source.serviceId'),json_extract(v.content,'$.source.projectId'),json_extract(v.content,'$.source.relativePath')
164
+ FROM node_versions v WHERE v.node_id=NEW.id AND v.version=NEW.current_version
165
+ AND json_extract(v.content,'$.source.kind')='project-file';
166
+ END;
167
+ INSERT INTO project_file_bindings(node_id,service_id,project_id,relative_path)
168
+ SELECT n.id,json_extract(v.content,'$.source.serviceId'),json_extract(v.content,'$.source.projectId'),json_extract(v.content,'$.source.relativePath')
169
+ FROM nodes n JOIN node_versions v ON v.node_id=n.id AND v.version=n.current_version
170
+ JOIN graphs g ON g.id=n.graph_id
171
+ WHERE json_extract(v.content,'$.source.kind')='project-file'
172
+ AND json_extract(v.content,'$.source.projectId')=g.project_id;
173
+ ` }, { version: 18, sql: `
174
+ CREATE TABLE edges_next (id TEXT PRIMARY KEY, graph_id TEXT NOT NULL REFERENCES graphs(id), source_id TEXT NOT NULL, target_id TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('reference','execution','delivery')), CHECK(source_id != target_id), UNIQUE(graph_id,source_id,target_id), FOREIGN KEY(source_id,graph_id) REFERENCES nodes(id,graph_id), FOREIGN KEY(target_id,graph_id) REFERENCES nodes(id,graph_id)) STRICT;
175
+ INSERT INTO edges_next SELECT * FROM edges;
176
+ DROP TRIGGER register_execution_output;
177
+ DROP TABLE edges;
178
+ ALTER TABLE edges_next RENAME TO edges;
179
+ CREATE INDEX execution_edges_source ON edges(graph_id,source_id) WHERE kind='execution';
180
+ CREATE TRIGGER register_execution_output AFTER INSERT ON edges WHEN NEW.kind='delivery'
181
+ BEGIN INSERT INTO execution_outputs(node_id,execution_node_id) VALUES(NEW.target_id,NEW.source_id)
182
+ ON CONFLICT(node_id) DO UPDATE SET execution_node_id=excluded.execution_node_id; END;
183
+ ` }, { version: 19, sql: `
184
+ CREATE TABLE execution_input_snapshots (run_id TEXT PRIMARY KEY REFERENCES runs(id),payload TEXT NOT NULL CHECK(json_valid(payload))) STRICT;
185
+ CREATE TRIGGER execution_input_snapshot_immutable BEFORE UPDATE ON execution_input_snapshots BEGIN SELECT RAISE(ABORT,'immutable execution input snapshot'); END;
186
+ ` }, { version: 20, sql: `
187
+ CREATE TABLE run_notifications (
188
+ run_id TEXT PRIMARY KEY REFERENCES runs(id) ON DELETE CASCADE,
189
+ status TEXT NOT NULL CHECK(status IN ('succeeded','failed','waiting_answer','waiting_approval','interrupted')),
190
+ revision INTEGER NOT NULL CHECK(revision > 0),
191
+ created_at TEXT NOT NULL,
192
+ read_at TEXT
193
+ ) STRICT;
194
+ CREATE INDEX run_notifications_unread ON run_notifications(read_at,created_at) WHERE read_at IS NULL;
195
+ INSERT INTO run_notifications(run_id,status,revision,created_at,read_at)
196
+ SELECT id,status,1,created_at,NULL FROM runs WHERE status IN ('waiting_answer','waiting_approval');
197
+ ` }, { version: 21, sql: `
198
+ UPDATE settings SET value='4' WHERE key='capacity' AND value='2';
199
+ ` }, { version: 22, sql: `
200
+ CREATE TABLE graph_document_versions (
201
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
202
+ graph_id TEXT NOT NULL REFERENCES graphs(id) ON DELETE CASCADE,
203
+ document TEXT NOT NULL CHECK(json_valid(document))
204
+ ) STRICT;
205
+ CREATE INDEX graph_document_versions_graph ON graph_document_versions(graph_id,id);
206
+ CREATE TABLE graph_document_heads (
207
+ graph_id TEXT PRIMARY KEY REFERENCES graphs(id) ON DELETE CASCADE,
208
+ cursor INTEGER NOT NULL REFERENCES graph_document_versions(id),
209
+ execution_revision INTEGER NOT NULL,
210
+ layout_revision INTEGER NOT NULL,
211
+ run_sequence INTEGER NOT NULL,
212
+ expires_at INTEGER NOT NULL
213
+ ) STRICT;
214
+ CREATE TABLE graph_document_resources (
215
+ version_id INTEGER NOT NULL REFERENCES graph_document_versions(id) ON DELETE CASCADE,
216
+ resource_id TEXT NOT NULL,
217
+ resource_version INTEGER NOT NULL,
218
+ PRIMARY KEY(version_id,resource_id,resource_version),
219
+ FOREIGN KEY(resource_id,resource_version) REFERENCES canvas_resource_versions(resource_id,version)
220
+ ) STRICT;
221
221
  CREATE INDEX graph_document_resources_resource ON graph_document_resources(resource_id);
222
222
  ` }, { version: 23, sql: `
223
223
  ALTER TABLE graphs ADD COLUMN updated_at TEXT;
@@ -1,19 +1,19 @@
1
1
  /** Additive workflow storage. Deletion keeps Run identity/FK records as tombstones. */
2
- export const WORKFLOW_SCHEMA = `
3
- ALTER TABLE nodes ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0 CHECK(deleted IN (0,1));
4
- CREATE TABLE node_resource_history (
5
- node_id TEXT NOT NULL, node_version INTEGER NOT NULL, resource_id TEXT NOT NULL, resource_version INTEGER NOT NULL,
6
- PRIMARY KEY(node_id,node_version,resource_id,resource_version),
7
- FOREIGN KEY(node_id,node_version) REFERENCES node_versions(node_id,version),
8
- FOREIGN KEY(resource_id,resource_version) REFERENCES canvas_resource_versions(resource_id,version)
9
- ) STRICT;
10
- CREATE TABLE graph_groups (id TEXT PRIMARY KEY,graph_id TEXT NOT NULL REFERENCES graphs(id),run_id TEXT REFERENCES runs(id),title TEXT NOT NULL) STRICT;
11
- CREATE TABLE group_members (group_id TEXT NOT NULL REFERENCES graph_groups(id),node_id TEXT NOT NULL REFERENCES nodes(id),PRIMARY KEY(group_id,node_id)) STRICT;
12
- CREATE TABLE run_runtime (run_id TEXT PRIMARY KEY REFERENCES runs(id),epoch TEXT NOT NULL,revision INTEGER NOT NULL DEFAULT 0,details TEXT NOT NULL CHECK(json_valid(details))) STRICT;
13
- CREATE TABLE run_process_records (id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES runs(id),occurred_at TEXT NOT NULL,kind TEXT NOT NULL,payload TEXT NOT NULL CHECK(json_valid(payload))) STRICT;
14
- CREATE TABLE generation_candidates (run_id TEXT PRIMARY KEY REFERENCES runs(id),node_id TEXT NOT NULL REFERENCES nodes(id),base_version INTEGER NOT NULL,content TEXT NOT NULL CHECK(json_valid(content)),state TEXT NOT NULL CHECK(state IN ('pending','accepted','discarded'))) STRICT;
15
- CREATE TABLE publication_manifests (run_id TEXT PRIMARY KEY REFERENCES runs(id),manifest TEXT NOT NULL CHECK(json_valid(manifest)),sha256 TEXT NOT NULL) STRICT;
16
- CREATE TRIGGER immutable_publication_manifest BEFORE UPDATE ON publication_manifests BEGIN SELECT RAISE(ABORT,'publication manifest is immutable'); END;
17
- INSERT INTO settings(key,value) VALUES('eventFloor','0'),('acceptingRuns','true');
2
+ export const WORKFLOW_SCHEMA = `
3
+ ALTER TABLE nodes ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0 CHECK(deleted IN (0,1));
4
+ CREATE TABLE node_resource_history (
5
+ node_id TEXT NOT NULL, node_version INTEGER NOT NULL, resource_id TEXT NOT NULL, resource_version INTEGER NOT NULL,
6
+ PRIMARY KEY(node_id,node_version,resource_id,resource_version),
7
+ FOREIGN KEY(node_id,node_version) REFERENCES node_versions(node_id,version),
8
+ FOREIGN KEY(resource_id,resource_version) REFERENCES canvas_resource_versions(resource_id,version)
9
+ ) STRICT;
10
+ CREATE TABLE graph_groups (id TEXT PRIMARY KEY,graph_id TEXT NOT NULL REFERENCES graphs(id),run_id TEXT REFERENCES runs(id),title TEXT NOT NULL) STRICT;
11
+ CREATE TABLE group_members (group_id TEXT NOT NULL REFERENCES graph_groups(id),node_id TEXT NOT NULL REFERENCES nodes(id),PRIMARY KEY(group_id,node_id)) STRICT;
12
+ CREATE TABLE run_runtime (run_id TEXT PRIMARY KEY REFERENCES runs(id),epoch TEXT NOT NULL,revision INTEGER NOT NULL DEFAULT 0,details TEXT NOT NULL CHECK(json_valid(details))) STRICT;
13
+ CREATE TABLE run_process_records (id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES runs(id),occurred_at TEXT NOT NULL,kind TEXT NOT NULL,payload TEXT NOT NULL CHECK(json_valid(payload))) STRICT;
14
+ CREATE TABLE generation_candidates (run_id TEXT PRIMARY KEY REFERENCES runs(id),node_id TEXT NOT NULL REFERENCES nodes(id),base_version INTEGER NOT NULL,content TEXT NOT NULL CHECK(json_valid(content)),state TEXT NOT NULL CHECK(state IN ('pending','accepted','discarded'))) STRICT;
15
+ CREATE TABLE publication_manifests (run_id TEXT PRIMARY KEY REFERENCES runs(id),manifest TEXT NOT NULL CHECK(json_valid(manifest)),sha256 TEXT NOT NULL) STRICT;
16
+ CREATE TRIGGER immutable_publication_manifest BEFORE UPDATE ON publication_manifests BEGIN SELECT RAISE(ABORT,'publication manifest is immutable'); END;
17
+ INSERT INTO settings(key,value) VALUES('eventFloor','0'),('acceptingRuns','true');
18
18
  `;
19
19
  //# sourceMappingURL=workflow-schema.js.map
package/dist/runs.js CHANGED
@@ -116,7 +116,7 @@ export class Runs {
116
116
  refreshNotification(run) {
117
117
  if (!notificationStatuses.has(run.status))
118
118
  return;
119
- this.db.prepare(`INSERT INTO run_notifications(run_id,status,revision,created_at,read_at) VALUES(?,?,1,?,NULL)
119
+ this.db.prepare(`INSERT INTO run_notifications(run_id,status,revision,created_at,read_at) VALUES(?,?,1,?,NULL)
120
120
  ON CONFLICT(run_id) DO UPDATE SET status=excluded.status,revision=run_notifications.revision+1,created_at=excluded.created_at,read_at=NULL`).run(run.id, run.status, new Date(this.now()).toISOString());
121
121
  }
122
122
  record(id, kind, payload) {
@@ -213,13 +213,13 @@ export class Runs {
213
213
  if (!capacity.available)
214
214
  return null;
215
215
  const exclusion = excludedProjectIds.length ? ` AND (r.kind!='execution' OR r.project_id NOT IN (${excludedProjectIds.map(() => '?').join(',')}))` : '';
216
- const rows = this.db.prepare(`
217
- SELECT r.id FROM runs r
218
- JOIN projects p ON p.id=r.project_id
219
- JOIN run_runtime rt ON rt.run_id=r.id
220
- WHERE r.status='queued' AND p.state='active'
221
- AND p.canonical_path=json_extract(rt.details,'$.canonicalPath')
222
- ${exclusion} ORDER BY r.sequence
216
+ const rows = this.db.prepare(`
217
+ SELECT r.id FROM runs r
218
+ JOIN projects p ON p.id=r.project_id
219
+ JOIN run_runtime rt ON rt.run_id=r.id
220
+ WHERE r.status='queued' AND p.state='active'
221
+ AND p.canonical_path=json_extract(rt.details,'$.canonicalPath')
222
+ ${exclusion} ORDER BY r.sequence
223
223
  `).all(...excludedProjectIds);
224
224
  const selected = rows.find(row => {
225
225
  const run = this.get(String(row.id));
package/dist/service.d.ts CHANGED
@@ -11,6 +11,7 @@ export declare function createService(dataDir?: string, options?: {
11
11
  projectProbe?: ProjectProbe;
12
12
  runtime?: WorkflowRuntimeOptions;
13
13
  npmInstallation?: () => Promise<boolean>;
14
+ npxInstallation?: () => Promise<boolean>;
14
15
  latestNpmVersion?: () => Promise<string>;
15
16
  updateRuntime?: (onExit: () => void) => Promise<void>;
16
17
  }): {
package/dist/service.js CHANGED
@@ -58,6 +58,7 @@ export function createService(dataDir, options = {}) {
58
58
  const events = new Events(db, serviceId, auth);
59
59
  const instanceId = randomUUID();
60
60
  const npmInstallation = options.npmInstallation ?? (async () => false);
61
+ const npxInstallation = options.npxInstallation ?? (async () => false);
61
62
  let updateQueued = false;
62
63
  const unavailable = {
63
64
  status: "unavailable",
@@ -202,7 +203,7 @@ export function createService(dataDir, options = {}) {
202
203
  const runtimeInfo = runtime.serviceInfo;
203
204
  send({
204
205
  ...info,
205
- installation: await npmInstallation() ? 'npm-global' : 'other',
206
+ installation: await npmInstallation() ? 'npm-global' : await npxInstallation() ? 'npx' : 'other',
206
207
  imageRoutes: runtimeInfo.imageRoutes,
207
208
  capabilities: {
208
209
  ...info.capabilities,
@@ -470,13 +471,29 @@ export function createService(dataDir, options = {}) {
470
471
  db.prepare("INSERT INTO settings(key,value) VALUES('listener',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(JSON.stringify(listenerInfo(address, serviceId, instanceId)));
471
472
  });
472
473
  let closing;
474
+ async function closeHttpServer() {
475
+ if (!server.listening)
476
+ return;
477
+ await new Promise((resolve, reject) => {
478
+ // Runs and event streams have already drained. A browser's unfinished
479
+ // request must not hold the service's lifetime locks indefinitely.
480
+ const timeout = setTimeout(() => server.closeAllConnections(), 1000);
481
+ timeout.unref();
482
+ server.close(error => {
483
+ clearTimeout(timeout);
484
+ if (error)
485
+ reject(error);
486
+ else
487
+ resolve();
488
+ });
489
+ });
490
+ }
473
491
  async function abortStartup() {
474
492
  runtime.disposeUnstarted();
475
493
  projectProbe.close();
476
494
  events.close();
477
495
  try {
478
- if (server.listening)
479
- await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
496
+ await closeHttpServer();
480
497
  }
481
498
  finally {
482
499
  db.close();
@@ -489,8 +506,7 @@ export function createService(dataDir, options = {}) {
489
506
  projectProbe.close();
490
507
  events.close();
491
508
  try {
492
- if (server.listening)
493
- await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
509
+ await closeHttpServer();
494
510
  db.prepare("DELETE FROM settings WHERE key='listener' AND json_extract(value,'$.instanceId')=?").run(instanceId);
495
511
  }
496
512
  finally {