openworkgraph 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +64 -0
  2. package/dist/api-image-agent.d.ts +32 -0
  3. package/dist/api-image-agent.js +132 -0
  4. package/dist/api-image-output.d.ts +8 -0
  5. package/dist/api-image-output.js +41 -0
  6. package/dist/api.d.ts +35 -0
  7. package/dist/api.js +153 -0
  8. package/dist/auth.d.ts +58 -0
  9. package/dist/auth.js +273 -0
  10. package/dist/backend/adapter.d.ts +35 -0
  11. package/dist/backend/adapter.js +439 -0
  12. package/dist/backend/capabilities.d.ts +23 -0
  13. package/dist/backend/capabilities.js +75 -0
  14. package/dist/backend/diagnostics.d.ts +9 -0
  15. package/dist/backend/diagnostics.js +80 -0
  16. package/dist/backend/index.d.ts +6 -0
  17. package/dist/backend/index.js +7 -0
  18. package/dist/backend/models.d.ts +15 -0
  19. package/dist/backend/models.js +43 -0
  20. package/dist/backend/paths.d.ts +6 -0
  21. package/dist/backend/paths.js +69 -0
  22. package/dist/backend/stdio.d.ts +66 -0
  23. package/dist/backend/stdio.js +172 -0
  24. package/dist/backend/types.d.ts +90 -0
  25. package/dist/backend/types.js +11 -0
  26. package/dist/backup-api.d.ts +10 -0
  27. package/dist/backup-api.js +40 -0
  28. package/dist/blob-store.d.ts +40 -0
  29. package/dist/blob-store.js +218 -0
  30. package/dist/cli-help.d.ts +1 -0
  31. package/dist/cli-help.js +63 -0
  32. package/dist/cli-output.d.ts +4 -0
  33. package/dist/cli-output.js +130 -0
  34. package/dist/cli.d.ts +2 -0
  35. package/dist/cli.js +284 -0
  36. package/dist/client-pairing.d.ts +8 -0
  37. package/dist/client-pairing.js +30 -0
  38. package/dist/directories.d.ts +14 -0
  39. package/dist/directories.js +34 -0
  40. package/dist/errors.d.ts +12 -0
  41. package/dist/errors.js +7 -0
  42. package/dist/events.d.ts +22 -0
  43. package/dist/events.js +155 -0
  44. package/dist/execution-chains.d.ts +24 -0
  45. package/dist/execution-chains.js +231 -0
  46. package/dist/execution-settings.d.ts +10 -0
  47. package/dist/execution-settings.js +38 -0
  48. package/dist/graph-document-history.d.ts +43 -0
  49. package/dist/graph-document-history.js +275 -0
  50. package/dist/graph-files.d.ts +14 -0
  51. package/dist/graph-files.js +59 -0
  52. package/dist/graph-transfer.d.ts +53 -0
  53. package/dist/graph-transfer.js +501 -0
  54. package/dist/graphs.d.ts +68 -0
  55. package/dist/graphs.js +771 -0
  56. package/dist/history.d.ts +20 -0
  57. package/dist/history.js +61 -0
  58. package/dist/http.d.ts +2 -0
  59. package/dist/http.js +42 -0
  60. package/dist/image-credentials.d.ts +11 -0
  61. package/dist/image-credentials.js +130 -0
  62. package/dist/image-providers.d.ts +51 -0
  63. package/dist/image-providers.js +271 -0
  64. package/dist/input-materialization.d.ts +17 -0
  65. package/dist/input-materialization.js +95 -0
  66. package/dist/inputs.d.ts +74 -0
  67. package/dist/inputs.js +301 -0
  68. package/dist/local-control.d.ts +13 -0
  69. package/dist/local-control.js +37 -0
  70. package/dist/network.d.ts +11 -0
  71. package/dist/network.js +48 -0
  72. package/dist/openai-image-driver.d.ts +42 -0
  73. package/dist/openai-image-driver.js +368 -0
  74. package/dist/operations/backups.d.ts +40 -0
  75. package/dist/operations/backups.js +391 -0
  76. package/dist/operations/capacity.d.ts +7 -0
  77. package/dist/operations/capacity.js +35 -0
  78. package/dist/operations/cli-context.d.ts +7 -0
  79. package/dist/operations/cli-context.js +58 -0
  80. package/dist/operations/daemon.d.ts +36 -0
  81. package/dist/operations/daemon.js +159 -0
  82. package/dist/operations/files.d.ts +12 -0
  83. package/dist/operations/files.js +84 -0
  84. package/dist/operations/lifecycle.d.ts +28 -0
  85. package/dist/operations/lifecycle.js +155 -0
  86. package/dist/operations/local-admin.d.ts +6 -0
  87. package/dist/operations/local-admin.js +38 -0
  88. package/dist/operations/logs.d.ts +16 -0
  89. package/dist/operations/logs.js +37 -0
  90. package/dist/operations/plugins.d.ts +10 -0
  91. package/dist/operations/plugins.js +11 -0
  92. package/dist/operations/snapshot-worker.d.ts +1 -0
  93. package/dist/operations/snapshot-worker.js +18 -0
  94. package/dist/operations/snapshot.d.ts +1 -0
  95. package/dist/operations/snapshot.js +14 -0
  96. package/dist/operations/update.d.ts +26 -0
  97. package/dist/operations/update.js +216 -0
  98. package/dist/persistence/asset-business-schema.d.ts +2 -0
  99. package/dist/persistence/asset-business-schema.js +36 -0
  100. package/dist/persistence/database.d.ts +7 -0
  101. package/dist/persistence/database.js +91 -0
  102. package/dist/persistence/plugin-schema.d.ts +9 -0
  103. package/dist/persistence/plugin-schema.js +38 -0
  104. package/dist/persistence/repositories.d.ts +20 -0
  105. package/dist/persistence/repositories.js +106 -0
  106. package/dist/persistence/resource-schema.d.ts +2 -0
  107. package/dist/persistence/resource-schema.js +44 -0
  108. package/dist/persistence/schema.d.ts +6 -0
  109. package/dist/persistence/schema.js +236 -0
  110. package/dist/persistence/workflow-schema.d.ts +2 -0
  111. package/dist/persistence/workflow-schema.js +19 -0
  112. package/dist/plugin-api.d.ts +10 -0
  113. package/dist/plugin-api.js +40 -0
  114. package/dist/plugin-management.d.ts +36 -0
  115. package/dist/plugin-management.js +112 -0
  116. package/dist/plugins.d.ts +55 -0
  117. package/dist/plugins.js +239 -0
  118. package/dist/project-discovery.d.ts +3 -0
  119. package/dist/project-discovery.js +57 -0
  120. package/dist/project-files-api.d.ts +16 -0
  121. package/dist/project-files-api.js +243 -0
  122. package/dist/project-files.d.ts +97 -0
  123. package/dist/project-files.js +557 -0
  124. package/dist/project-lock.d.ts +4 -0
  125. package/dist/project-lock.js +35 -0
  126. package/dist/project-probe-worker.d.ts +1 -0
  127. package/dist/project-probe-worker.js +17 -0
  128. package/dist/project-probe.d.ts +22 -0
  129. package/dist/project-probe.js +67 -0
  130. package/dist/projects.d.ts +29 -0
  131. package/dist/projects.js +219 -0
  132. package/dist/publication.d.ts +31 -0
  133. package/dist/publication.js +642 -0
  134. package/dist/resource-api.d.ts +15 -0
  135. package/dist/resource-api.js +256 -0
  136. package/dist/resources-orphans.d.ts +41 -0
  137. package/dist/resources-orphans.js +235 -0
  138. package/dist/resources.d.ts +246 -0
  139. package/dist/resources.js +714 -0
  140. package/dist/run-api.d.ts +10 -0
  141. package/dist/run-api.js +308 -0
  142. package/dist/run-context.d.ts +54 -0
  143. package/dist/run-context.js +137 -0
  144. package/dist/runs.d.ts +82 -0
  145. package/dist/runs.js +506 -0
  146. package/dist/runtime-backend.d.ts +24 -0
  147. package/dist/runtime-backend.js +214 -0
  148. package/dist/runtime.d.ts +97 -0
  149. package/dist/runtime.js +553 -0
  150. package/dist/scheduler.d.ts +85 -0
  151. package/dist/scheduler.js +344 -0
  152. package/dist/service.d.ts +29 -0
  153. package/dist/service.js +515 -0
  154. package/dist/skills-api.d.ts +18 -0
  155. package/dist/skills-api.js +120 -0
  156. package/dist/storage-api.d.ts +21 -0
  157. package/dist/storage-api.js +92 -0
  158. package/dist/transfer-api.d.ts +10 -0
  159. package/dist/transfer-api.js +60 -0
  160. package/dist/vendor/protocol/contracts.typecheck.d.ts +1 -0
  161. package/dist/vendor/protocol/contracts.typecheck.js +35 -0
  162. package/dist/vendor/protocol/execution-chain.d.ts +6 -0
  163. package/dist/vendor/protocol/execution-chain.js +26 -0
  164. package/dist/vendor/protocol/graph-binary.d.ts +15 -0
  165. package/dist/vendor/protocol/graph-binary.js +121 -0
  166. package/dist/vendor/protocol/graph-title.d.ts +2 -0
  167. package/dist/vendor/protocol/graph-title.js +23 -0
  168. package/dist/vendor/protocol/index.d.ts +617 -0
  169. package/dist/vendor/protocol/index.js +20 -0
  170. package/dist/vendor/protocol/pairing.d.ts +29 -0
  171. package/dist/vendor/protocol/pairing.js +71 -0
  172. package/dist/vendor/protocol/preview.d.ts +2 -0
  173. package/dist/vendor/protocol/preview.js +9 -0
  174. package/dist/vendor/protocol/resource-mime.d.ts +2 -0
  175. package/dist/vendor/protocol/resource-mime.js +35 -0
  176. package/dist/video-thumbnail.d.ts +2 -0
  177. package/dist/video-thumbnail.js +34 -0
  178. package/package.json +9 -0
@@ -0,0 +1,231 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { executionOrder, isTerminalRunStatus } from "./vendor/protocol/index.js";
3
+ import { Runs } from './runs.js';
4
+ import { Graphs } from './graphs.js';
5
+ import { Resources } from './resources.js';
6
+ import { atomic } from './persistence/database.js';
7
+ import { canonicalJson } from './persistence/repositories.js';
8
+ import { ServiceError } from './errors.js';
9
+ /** Accepted runs are durable submissions. Only confirmed, dependency-ready runs enter the queue. */
10
+ export class ExecutionChains {
11
+ runs;
12
+ graphs;
13
+ resources;
14
+ pumping = false;
15
+ stopped = false;
16
+ stop() { this.stopped = true; }
17
+ constructor(runs, graphs, resources) {
18
+ this.runs = runs;
19
+ this.graphs = graphs;
20
+ this.resources = resources;
21
+ }
22
+ get db() { return this.runs.db; }
23
+ histories(graph, nodeId, exclude) {
24
+ return this.db.prepare('SELECT id FROM runs WHERE project_id=? AND graph_id=? AND node_id=? ORDER BY sequence').all(graph.projectId, graph.graphId, nodeId).filter(row => row.id !== exclude).map(row => this.runs.get(String(row.id)));
25
+ }
26
+ plan(graph, targetId, exclude) {
27
+ const edges = graph.edges.filter(e => e.kind === 'execution');
28
+ if (!executionOrder(edges))
29
+ throw new ServiceError('INVALID_EDGE', '执行依赖存在环。');
30
+ if (graph.nodes.find(n => n.id === targetId)?.type !== 'execution')
31
+ throw new ServiceError('INVALID_REQUEST', '只有执行节点支持串联调度。');
32
+ const ids = new Set([targetId]), queue = [targetId];
33
+ for (let i = 0; i < queue.length; i++)
34
+ for (const e of edges)
35
+ if (e.targetId === queue[i] && !ids.has(e.sourceId)) {
36
+ ids.add(e.sourceId);
37
+ queue.push(e.sourceId);
38
+ }
39
+ const nodes = graph.nodes.filter(n => ids.has(n.id)).map(n => {
40
+ const history = this.histories(graph, n.id, exclude);
41
+ const content = n.content && typeof n.content === 'object' && !Array.isArray(n.content) ? n.content : {};
42
+ // Already submitted runs use their frozen prompt, not subsequent node edits.
43
+ const submitted = n.id === targetId && exclude || history.find(r => r.status === 'accepted' && ['manual', 'confirm'].includes(r.executionStart ?? ''))?.id;
44
+ const prompt = submitted ? this.runs.snapshot(submitted).prompt : content.prompt;
45
+ return { id: n.id, title: typeof content.title === 'string' ? content.title : n.id, required: n.id === targetId || !history.some(r => r.status === 'succeeded'), hasRun: history.some(r => r.status !== 'accepted'), missingPrompt: typeof prompt !== 'string' || !prompt.trim() };
46
+ });
47
+ const requiresConfirmation = nodes.some(n => n.hasRun) && nodes.length > 1;
48
+ return { targetId, executionRevision: graph.executionRevision, nodes, edges: edges.filter(e => ids.has(e.sourceId) && ids.has(e.targetId)), initialNodeIds: nodes.filter(n => n.required).map(n => n.id), requiresConfirmation };
49
+ }
50
+ forRun(id) { const run = this.runs.get(id); return this.plan(this.graphs.snapshot(run), run.nodeId, id); }
51
+ cancel(id, key, principal) {
52
+ this.runs.repo.idempotent(principal + ':chain.cancel:' + id, key, { id }, () => {
53
+ const target = this.runs.get(id);
54
+ const runtime = this.runs.runtime(id);
55
+ if (runtime.details.chainBatch !== id)
56
+ throw new ServiceError('INVALID_REQUEST', '请从本轮串联主任务停止整体调度。');
57
+ const members = this.db.prepare("SELECT r.id FROM runs r JOIN run_runtime rt ON rt.run_id=r.id WHERE r.project_id=? AND r.graph_id=? AND json_extract(rt.details,'$.chainBatch')=?").all(target.projectId, target.graphId, id);
58
+ this.setMode(id, runtime.details.executionStart, { chainStopRequested: true });
59
+ for (const member of members) {
60
+ const memberId = String(member.id);
61
+ if (isTerminalRunStatus(this.runs.get(memberId).status))
62
+ continue;
63
+ this.runs.cancel(memberId, key, principal + ':chain:' + id);
64
+ this.runs.record(memberId, 'workflow', { message: '主任务已请求停止本轮串联调度。', targetRunId: id });
65
+ }
66
+ return { id };
67
+ });
68
+ return this.runs.get(id);
69
+ }
70
+ selection(plan, seeds) {
71
+ if (!Array.isArray(seeds) || seeds.length > plan.nodes.length || seeds.some(id => typeof id !== 'string' || !plan.nodes.some(n => n.id === id)))
72
+ throw new ServiceError('INVALID_REQUEST', '执行范围包含无关节点。');
73
+ const selected = new Set([...seeds, ...plan.nodes.filter(n => n.required).map(n => n.id)]);
74
+ const queue = [...selected];
75
+ for (let i = 0; i < queue.length; i++)
76
+ for (const e of plan.edges)
77
+ if (e.sourceId === queue[i] && !selected.has(e.targetId)) {
78
+ selected.add(e.targetId);
79
+ queue.push(e.targetId);
80
+ }
81
+ const order = executionOrder(plan.edges);
82
+ if (!order.includes(plan.targetId))
83
+ order.push(plan.targetId);
84
+ return order.filter(id => selected.has(id));
85
+ }
86
+ setMode(id, executionStart, extra = {}) {
87
+ const runtime = this.runs.runtime(id);
88
+ this.db.prepare('UPDATE run_runtime SET details=?,revision=revision+1 WHERE run_id=?').run(canonicalJson({ ...runtime.details, ...extra, executionStart: executionStart }), id);
89
+ const run = this.runs.get(id);
90
+ this.runs.repo.appendEvent({ eventId: randomUUID(), type: 'run.changed', projectId: run.projectId, graphId: run.graphId, entityId: id, revision: runtime.revision + 1, occurredAt: new Date().toISOString(), payload: run });
91
+ }
92
+ /** Prepare every missing run before entering the auth transaction. No partial batch on failure. */
93
+ async prepareMembers(graph, plan, selected, targetRunId, prepare) {
94
+ const missing = plan.nodes.filter(n => selected.includes(n.id) && n.missingPrompt);
95
+ if (missing.length)
96
+ throw new ServiceError('INPUT_BLOCKED', '请先填写以下节点的提示词:' + missing.map(n => n.title).join('、'));
97
+ const members = new Map();
98
+ const commits = new Map();
99
+ for (const id of selected) {
100
+ if (id === plan.targetId && targetRunId) {
101
+ members.set(id, targetRunId);
102
+ continue;
103
+ }
104
+ const active = this.histories(graph, id).find(r => !isTerminalRunStatus(r.status));
105
+ if (active) {
106
+ if (active.status !== 'accepted' || !['manual', 'confirm'].includes(active.executionStart ?? ''))
107
+ throw new ServiceError('ACTIVE_RUN', '所选节点已在其他执行计划中运行。');
108
+ members.set(id, active.id);
109
+ }
110
+ else {
111
+ const node = graph.nodes.find(n => n.id === id);
112
+ const content = node.content && typeof node.content === 'object' && !Array.isArray(node.content) ? node.content : {};
113
+ commits.set(id, await prepare({ serviceId: graph.serviceId, projectId: graph.projectId, graphId: graph.graphId, nodeId: id, kind: 'execution', idempotencyKey: randomUUID(), expectedExecutionRevision: graph.executionRevision, ...(content.modelOverride ? { modelOverride: content.modelOverride } : {}) }));
114
+ }
115
+ }
116
+ const reused = new Map();
117
+ for (const e of plan.edges)
118
+ if (selected.includes(e.targetId) && !selected.includes(e.sourceId)) {
119
+ const success = this.histories(graph, e.sourceId).filter(r => r.status === 'succeeded').at(-1);
120
+ if (!success)
121
+ throw new ServiceError('INPUT_BLOCKED', '前驱没有可复用的成功结果,请扩大执行范围。');
122
+ reused.set(e.sourceId, success.id);
123
+ }
124
+ return (principal, target) => {
125
+ const committedMembers = new Map(members);
126
+ if (this.graphs.snapshot(graph).executionRevision !== plan.executionRevision)
127
+ throw new ServiceError('REVISION_CONFLICT', '工作图已变化,请重新确认执行范围。');
128
+ if (target)
129
+ committedMembers.set(plan.targetId, target.id);
130
+ for (const [id, commit] of commits)
131
+ if (!committedMembers.has(id))
132
+ committedMembers.set(id, commit(principal).id);
133
+ for (const id of committedMembers.values()) {
134
+ const run = this.runs.get(id);
135
+ if (run.status !== 'accepted' || !['manual', 'confirm'].includes(run.executionStart ?? ''))
136
+ throw new ServiceError('CONFLICT', '任务已开始或已取消,请刷新。');
137
+ }
138
+ const batch = committedMembers.get(plan.targetId);
139
+ for (const [nodeId, id] of committedMembers) {
140
+ const dependencies = plan.edges.filter(e => e.targetId === nodeId).map(e => committedMembers.get(e.sourceId) ?? reused.get(e.sourceId));
141
+ this.setMode(id, 'dependencies', { chainBatch: batch, chainDependencies: dependencies });
142
+ this.runs.record(id, 'workflow', { message: '执行范围已确认,等待前驱交付。', targetRunId: batch, predecessorRunIds: dependencies });
143
+ }
144
+ return this.runs.get(batch);
145
+ };
146
+ }
147
+ async pump() {
148
+ if (this.stopped || this.pumping || this.runs.repo.setting('acceptingRuns') !== true)
149
+ return;
150
+ this.pumping = true;
151
+ try {
152
+ for (const run of this.runs.list().filter(r => r.status === 'accepted' && r.executionStart === 'dependencies')) {
153
+ if (this.runs.get(run.id).status !== 'accepted')
154
+ continue;
155
+ const runtime = this.runs.runtime(run.id);
156
+ const dependencies = runtime.details.chainDependencies.map(id => this.runs.get(id));
157
+ const failed = dependencies.find(r => isTerminalRunStatus(r.status) && r.status !== 'succeeded');
158
+ if (failed) {
159
+ this.runs.transition(run.id, runtime, 'failed', { error: '前驱任务未成功,已停止下游执行。', failedPredecessorRunId: failed.id });
160
+ continue;
161
+ }
162
+ if (dependencies.some(r => r.status !== 'succeeded'))
163
+ continue;
164
+ try {
165
+ const snapshot = structuredClone(this.runs.snapshot(run.id));
166
+ for (const predecessor of dependencies) {
167
+ const manifestRow = this.db.prepare('SELECT manifest FROM publication_manifests WHERE run_id=?').get(predecessor.id);
168
+ let structuredKeys;
169
+ if (manifestRow) {
170
+ try {
171
+ const parsed = JSON.parse(String(manifestRow['manifest']));
172
+ const keys = (parsed.outputs ?? []).filter(item => item.role === 'workgraph-node' && typeof item.outputKey === 'string').map(item => String(item.outputKey));
173
+ if (keys.length)
174
+ structuredKeys = new Set(keys);
175
+ }
176
+ catch { /* Persisted publication validation owns corruption handling. */ }
177
+ }
178
+ const outputRows = this.db.prepare('SELECT o.output_key,o.resource_id,o.resource_version FROM canvas_outputs o WHERE o.run_id=? ORDER BY o.output_key').all(predecessor.id);
179
+ const outputs = structuredKeys ? outputRows.filter(output => structuredKeys.has(String(output.output_key))) : outputRows;
180
+ if (!outputs.length)
181
+ throw new ServiceError('INPUT_BLOCKED', '前驱交付结果不可读取。');
182
+ for (const output of outputs) {
183
+ const resource = { ...this.resources.readCanvasVersion(run, String(output.resource_id), Number(output.resource_version)), resourceId: String(output.resource_id) };
184
+ const bytes = await this.resources.readContent(run, 'canvas', resource.resourceId, resource.version);
185
+ if (this.stopped)
186
+ return;
187
+ const text = resource.mime.startsWith('text/') ? bytes.bytes.toString('utf8') : null;
188
+ snapshot.resources.push({ kind: text !== null ? 'document' : resource.mime.startsWith('image/') ? 'image' : 'file', sourceNodeIds: [predecessor.nodeId], text, resource });
189
+ }
190
+ const files = this.db.prepare('SELECT relative_path FROM project_file_outputs WHERE run_id=? ORDER BY output_key').all(predecessor.id);
191
+ snapshot.projectFiles ??= [];
192
+ for (const file of files)
193
+ snapshot.projectFiles.push({ kind: 'file', relativePath: String(file.relative_path), sourceNodeIds: [predecessor.nodeId], edgeIds: [] });
194
+ }
195
+ const textBytes = snapshot.resources.reduce((sum, r) => sum + Buffer.byteLength(r.text ?? ''), Buffer.byteLength(snapshot.prompt));
196
+ const resourceBytes = snapshot.resources.reduce((sum, r) => sum + (r.resource?.bytes ?? 0), 0);
197
+ if (snapshot.resources.length > 64 || textBytes > 33554432 || resourceBytes > 134217728 || snapshot.resources.some(r => Buffer.byteLength(r.text ?? '') > 8388608 || (r.resource?.bytes ?? 0) > 52428800))
198
+ throw new ServiceError('INPUT_BUDGET_EXCEEDED', '串联输入超过预算。');
199
+ const { inputDigest: _old, ...body } = snapshot;
200
+ snapshot.inputDigest = createHash('sha256').update(canonicalJson({ baseInputDigest: _old, predecessorRunIds: dependencies.map(r => r.id), ...body })).digest('hex');
201
+ atomic(this.db, () => {
202
+ const current = this.runs.get(run.id);
203
+ if (current.status !== 'accepted' || this.runs.runtime(run.id).revision !== runtime.revision)
204
+ return;
205
+ this.graphs.scope(run);
206
+ this.graphs.retireExecutionOutputs(run, run.nodeId);
207
+ this.db.prepare('INSERT INTO execution_input_snapshots(run_id,payload) VALUES(?,?)').run(run.id, canonicalJson(snapshot));
208
+ this.db.prepare('UPDATE runs SET input_digest=? WHERE id=?').run(snapshot.inputDigest, run.id);
209
+ for (const envelope of snapshot.resources)
210
+ if (envelope.resource) {
211
+ const r = envelope.resource;
212
+ if (!this.db.prepare("SELECT 1 FROM canvas_resource_references WHERE run_id=? AND resource_id=? AND resource_version=? AND owner_kind='snapshot'").get(run.id, r.resourceId, r.version))
213
+ this.db.prepare("INSERT INTO canvas_resource_references(id,resource_id,resource_version,graph_id,owner_kind,run_id) VALUES(?,?,?,?,'snapshot',?)").run(randomUUID(), r.resourceId, r.version, run.graphId, run.id);
214
+ }
215
+ this.runs.transition(run.id, runtime, 'queued');
216
+ });
217
+ }
218
+ catch (error) {
219
+ if (this.stopped)
220
+ return;
221
+ if (this.runs.get(run.id).status === 'accepted')
222
+ this.runs.transition(run.id, this.runs.runtime(run.id), 'failed', { error: error instanceof Error ? error.message : String(error) });
223
+ }
224
+ }
225
+ }
226
+ finally {
227
+ this.pumping = false;
228
+ }
229
+ }
230
+ }
231
+ //# sourceMappingURL=execution-chains.js.map
@@ -0,0 +1,10 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ExecutionSettings, SandboxMode } from "./vendor/protocol/index.js";
3
+ export declare function sandboxMode(value: unknown): SandboxMode;
4
+ /** Runtime-local settings, persisted with CAS. Codex enforces their permissions. */
5
+ export declare class ExecutionSettingsStore {
6
+ private readonly db;
7
+ constructor(db: DatabaseSync);
8
+ get(): ExecutionSettings;
9
+ set(mode: unknown, expectedRevision: unknown): ExecutionSettings;
10
+ }
@@ -0,0 +1,38 @@
1
+ import { ServiceError } from './errors.js';
2
+ export function sandboxMode(value) {
3
+ if (value !== 'read-only' && value !== 'workspace-write' && value !== 'danger-full-access')
4
+ throw new ServiceError('INVALID_REQUEST', '请选择有效的沙盒权限范围。');
5
+ return value;
6
+ }
7
+ /** Runtime-local settings, persisted with CAS. Codex enforces their permissions. */
8
+ export class ExecutionSettingsStore {
9
+ db;
10
+ constructor(db) {
11
+ this.db = db;
12
+ }
13
+ get() {
14
+ const row = this.db.prepare("SELECT value FROM settings WHERE key='executionSettings'").get();
15
+ if (!row)
16
+ return { revision: 0, sandboxMode: 'workspace-write' };
17
+ const value = JSON.parse(String(row.value));
18
+ if (!Number.isSafeInteger(value.revision) || value.revision < 1)
19
+ throw new ServiceError('MAINTENANCE', '运行时权限设置损坏。');
20
+ return { revision: value.revision, sandboxMode: sandboxMode(value.sandboxMode) };
21
+ }
22
+ set(mode, expectedRevision) {
23
+ const selected = sandboxMode(mode);
24
+ if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0 || expectedRevision >= Number.MAX_SAFE_INTEGER)
25
+ throw new ServiceError('INVALID_REQUEST', '权限设置修订号无效。');
26
+ const current = this.get();
27
+ if (current.revision !== expectedRevision)
28
+ throw new ServiceError('REVISION_CONFLICT', '权限设置已变化,请刷新后重试。');
29
+ const next = { revision: current.revision + 1, sandboxMode: selected };
30
+ const result = current.revision === 0
31
+ ? this.db.prepare("INSERT OR IGNORE INTO settings(key,value) VALUES('executionSettings',?)").run(JSON.stringify(next))
32
+ : this.db.prepare("UPDATE settings SET value=? WHERE key='executionSettings' AND json_extract(value,'$.revision')=?").run(JSON.stringify(next), current.revision);
33
+ if (result.changes !== 1)
34
+ throw new ServiceError('REVISION_CONFLICT', '权限设置已变化,请刷新后重试。');
35
+ return next;
36
+ }
37
+ }
38
+ //# sourceMappingURL=execution-settings.js.map
@@ -0,0 +1,43 @@
1
+ import type { DatabaseSync, SQLInputValue } from 'node:sqlite';
2
+ import { type GraphDocumentHistory, type GraphHistoryTravel, type GraphScope, type Json } from "./vendor/protocol/index.js";
3
+ import type { Graphs } from './graphs.js';
4
+ type Row = Record<string, SQLInputValue>;
5
+ interface Document {
6
+ title: string;
7
+ nodes: Row[];
8
+ edges: Row[];
9
+ groups: Row[];
10
+ members: Row[];
11
+ outputs: Row[];
12
+ }
13
+ interface Checkpoint {
14
+ document: Document;
15
+ execution: number;
16
+ layout: number;
17
+ run: number;
18
+ }
19
+ /** One shared document timeline per graph. Only graph command transactions append versions. */
20
+ export declare class GraphDocumentVersions {
21
+ private graphs;
22
+ private db;
23
+ constructor(graphs: Graphs, db?: DatabaseSync);
24
+ private runSequence;
25
+ private active;
26
+ private head;
27
+ private valid;
28
+ state(scope: GraphScope): GraphDocumentHistory;
29
+ capture(scope: GraphScope): Checkpoint;
30
+ clear(id: string): void;
31
+ private append;
32
+ private anchorActiveRuns;
33
+ /** Upgrade an already-running graph from the pre-gate history format. */
34
+ backfillActiveGates(): void;
35
+ /** A submitted Run is a one-way gate anchored after the last editable version. */
36
+ recordRunSubmission(scope: GraphScope, runId: string, before: Checkpoint): void;
37
+ /** Run publication may advance graph revisions outside the user command queue. */
38
+ completeRun(scope: GraphScope, runId: string, published?: boolean): void;
39
+ record(scope: GraphScope, before: Checkpoint, boundary?: boolean): void;
40
+ travel(request: GraphHistoryTravel, principal: string): Json;
41
+ private restore;
42
+ }
43
+ export {};
@@ -0,0 +1,275 @@
1
+ import { TERMINAL_RUN_STATUSES } from "./vendor/protocol/index.js";
2
+ import { resourceLinks } from './graphs.js';
3
+ import { canonicalJson } from './persistence/repositories.js';
4
+ import { ServiceError } from './errors.js';
5
+ const TTL = 24 * 60 * 60 * 1000;
6
+ const MAX_VERSIONS = 51;
7
+ const MAX_BYTES = 64 * 1024 * 1024;
8
+ const empty = () => ({ cursor: null, canUndo: false, canRedo: false, expiresAt: null });
9
+ /** One shared document timeline per graph. Only graph command transactions append versions. */
10
+ export class GraphDocumentVersions {
11
+ graphs;
12
+ db;
13
+ constructor(graphs, db = graphs.db) {
14
+ this.graphs = graphs;
15
+ this.db = db;
16
+ }
17
+ runSequence(id) {
18
+ return Number(this.db.prepare('SELECT coalesce(max(sequence),0) AS n FROM runs WHERE graph_id=?').get(id)['n']);
19
+ }
20
+ active(id) {
21
+ return !!this.db.prepare('SELECT 1 FROM runs WHERE graph_id=? AND status NOT IN (' + TERMINAL_RUN_STATUSES.map(() => '?').join(',') + ') UNION ALL SELECT 1 FROM occupancy o JOIN runs r ON r.id=o.run_id WHERE r.graph_id=? LIMIT 1').get(id, ...TERMINAL_RUN_STATUSES, id);
22
+ }
23
+ head(id) { return this.db.prepare('SELECT * FROM graph_document_heads WHERE graph_id=?').get(id); }
24
+ valid(scope) {
25
+ const graph = this.graphs.scope(scope), head = this.head(scope.graphId);
26
+ return head && Number(head['expires_at']) > Date.now() && !graph['archived'] && !graph['trashed']
27
+ && head['execution_revision'] === graph['execution_revision'] && head['layout_revision'] === graph['layout_revision']
28
+ && head['run_sequence'] === this.runSequence(scope.graphId) ? head : undefined;
29
+ }
30
+ state(scope) {
31
+ const head = this.valid(scope);
32
+ if (!head)
33
+ return empty();
34
+ const cursor = Number(head['cursor']);
35
+ const gates = this.db.prepare('SELECT g.run_id,r.status FROM graph_document_run_gates g JOIN runs r ON r.id=g.run_id WHERE g.graph_id=? AND g.cursor=? ORDER BY r.sequence').all(scope.graphId, cursor);
36
+ if (this.active(scope.graphId) && !this.db.prepare('SELECT 1 FROM graph_document_run_gates WHERE graph_id=? LIMIT 1').get(scope.graphId))
37
+ return empty();
38
+ return { cursor: String(cursor), expiresAt: Number(head['expires_at']), undoRunIds: gates.filter(g => !TERMINAL_RUN_STATUSES.includes(String(g['status']))).map(g => String(g['run_id'])),
39
+ canUndo: gates.length > 0 || !!this.db.prepare('SELECT 1 FROM graph_document_versions WHERE graph_id=? AND id<? LIMIT 1').get(scope.graphId, cursor),
40
+ canRedo: !!this.db.prepare('SELECT 1 FROM graph_document_versions WHERE graph_id=? AND id>? LIMIT 1').get(scope.graphId, cursor) };
41
+ }
42
+ capture(scope) {
43
+ const graph = this.graphs.scope(scope), id = scope.graphId;
44
+ const document = {
45
+ title: String(graph['title']),
46
+ nodes: this.db.prepare('SELECT n.id,n.type,n.schema_version,n.x,n.y,n.width,n.height,n.read_only,n.read_only_reason,n.deleted,v.content FROM nodes n JOIN node_versions v ON v.node_id=n.id AND v.version=n.current_version WHERE n.graph_id=? AND (n.deleted=0 OR EXISTS(SELECT 1 FROM execution_outputs o WHERE o.node_id=n.id)) ORDER BY n.creation_order').all(id),
47
+ edges: this.db.prepare('SELECT id,source_id,target_id,kind FROM edges WHERE graph_id=? ORDER BY id').all(id),
48
+ groups: this.db.prepare('SELECT id,run_id,title FROM graph_groups WHERE graph_id=? ORDER BY id').all(id),
49
+ members: this.db.prepare('SELECT m.group_id,m.node_id FROM group_members m JOIN graph_groups g ON g.id=m.group_id WHERE g.graph_id=? ORDER BY m.group_id,m.node_id').all(id),
50
+ outputs: this.db.prepare('SELECT o.node_id,o.execution_node_id FROM execution_outputs o JOIN nodes n ON n.id=o.node_id WHERE n.graph_id=? ORDER BY o.node_id').all(id),
51
+ };
52
+ return { document, execution: Number(graph['execution_revision']), layout: Number(graph['layout_revision']), run: this.runSequence(id) };
53
+ }
54
+ clear(id) {
55
+ this.db.prepare('DELETE FROM graph_document_heads WHERE graph_id=?').run(id);
56
+ this.db.prepare('DELETE FROM graph_document_versions WHERE graph_id=?').run(id);
57
+ }
58
+ append(scope, document) {
59
+ const id = Number(this.db.prepare('INSERT INTO graph_document_versions(graph_id,document) VALUES(?,?)').run(scope.graphId, canonicalJson(document)).lastInsertRowid);
60
+ for (const node of document.nodes)
61
+ for (const link of resourceLinks(JSON.parse(String(node['content'])))) {
62
+ this.db.prepare('INSERT OR IGNORE INTO graph_document_resources(version_id,resource_id,resource_version) VALUES(?,?,?)').run(id, link.resourceId, link.version);
63
+ }
64
+ return id;
65
+ }
66
+ anchorActiveRuns(graphId, cursor, excludeRunId) {
67
+ const statuses = TERMINAL_RUN_STATUSES.map(() => '?').join(',');
68
+ this.db.prepare('INSERT OR IGNORE INTO graph_document_run_gates(run_id,graph_id,cursor) '
69
+ + 'SELECT r.id,r.graph_id,? FROM runs r WHERE r.graph_id=? AND r.status NOT IN (' + statuses + ') '
70
+ + 'AND r.id<>? AND NOT EXISTS(SELECT 1 FROM graph_document_run_gates g WHERE g.run_id=r.id)').run(cursor, graphId, ...TERMINAL_RUN_STATUSES, excludeRunId ?? '');
71
+ }
72
+ /** Upgrade an already-running graph from the pre-gate history format. */
73
+ backfillActiveGates() {
74
+ const rows = this.db.prepare('SELECT r.id,r.graph_id,r.project_id FROM runs r WHERE r.status NOT IN (' + TERMINAL_RUN_STATUSES.map(() => '?').join(',') + ') AND NOT EXISTS(SELECT 1 FROM graph_document_run_gates g WHERE g.run_id=r.id) ORDER BY r.sequence').all(...TERMINAL_RUN_STATUSES);
75
+ for (const row of rows) {
76
+ const scope = { serviceId: this.graphs.serviceId, projectId: String(row['project_id']), graphId: String(row['graph_id']) };
77
+ this.db.exec('SAVEPOINT history_gate_backfill');
78
+ try {
79
+ const head = this.head(scope.graphId);
80
+ const graph = this.graphs.scope(scope);
81
+ if (!head || Number(head['expires_at']) <= Date.now() || head['execution_revision'] !== graph['execution_revision'] || head['layout_revision'] !== graph['layout_revision']) {
82
+ this.clear(scope.graphId);
83
+ const snapshot = this.capture(scope);
84
+ const cursor = this.append(scope, snapshot.document);
85
+ this.db.prepare('INSERT INTO graph_document_heads VALUES(?,?,?,?,?,?)').run(scope.graphId, cursor, snapshot.execution, snapshot.layout, snapshot.run, Date.now() + TTL);
86
+ }
87
+ else {
88
+ // Legacy submissions advanced runs.sequence without updating the existing history head.
89
+ this.db.prepare('UPDATE graph_document_heads SET run_sequence=? WHERE graph_id=?').run(this.runSequence(scope.graphId), scope.graphId);
90
+ }
91
+ this.db.prepare('INSERT OR IGNORE INTO graph_document_run_gates(run_id,graph_id,cursor) VALUES(?,?,?)').run(row['id'], scope.graphId, this.head(scope.graphId)['cursor']);
92
+ this.db.exec('RELEASE history_gate_backfill');
93
+ }
94
+ catch (error) {
95
+ this.db.exec('ROLLBACK TO history_gate_backfill; RELEASE history_gate_backfill');
96
+ throw error;
97
+ }
98
+ }
99
+ }
100
+ /** A submitted Run is a one-way gate anchored after the last editable version. */
101
+ recordRunSubmission(scope, runId, before) {
102
+ if (!this.db.isTransaction)
103
+ throw Error('Run history gate requires the submission transaction');
104
+ const id = scope.graphId, after = this.capture(scope);
105
+ let head = this.head(id);
106
+ if (!head || Number(head['expires_at']) <= Date.now() || head['execution_revision'] !== before.execution || head['layout_revision'] !== before.layout || head['run_sequence'] !== before.run) {
107
+ this.clear(id);
108
+ const baseline = this.append(scope, before.document);
109
+ this.db.prepare('INSERT INTO graph_document_heads VALUES(?,?,?,?,?,?)').run(id, baseline, before.execution, before.layout, before.run, Date.now() + TTL);
110
+ head = this.head(id);
111
+ }
112
+ let cursor = Number(head['cursor']);
113
+ // A history reset may have cascaded an older active Run's gate. Re-anchor it
114
+ // before this submission's own document change, which gets a separate gate.
115
+ this.anchorActiveRuns(id, cursor, runId);
116
+ this.db.prepare('DELETE FROM graph_document_versions WHERE graph_id=? AND id>?').run(id, cursor);
117
+ if (canonicalJson(before.document) !== canonicalJson(after.document)) {
118
+ cursor = this.append(scope, after.document);
119
+ }
120
+ this.db.prepare('INSERT INTO graph_document_run_gates(run_id,graph_id,cursor) VALUES(?,?,?)').run(runId, id, cursor);
121
+ this.db.prepare('UPDATE graph_document_heads SET cursor=?,execution_revision=?,layout_revision=?,run_sequence=? WHERE graph_id=?').run(cursor, after.execution, after.layout, after.run, id);
122
+ }
123
+ /** Run publication may advance graph revisions outside the user command queue. */
124
+ completeRun(scope, runId, published = false) {
125
+ if (!this.db.isTransaction)
126
+ throw Error('Run history settlement requires a transaction');
127
+ const gate = this.db.prepare('SELECT cursor FROM graph_document_run_gates WHERE graph_id=? AND run_id=?').get(scope.graphId, runId);
128
+ if (!gate)
129
+ return;
130
+ const head = this.head(scope.graphId);
131
+ if (!head)
132
+ return;
133
+ const current = this.capture(scope);
134
+ if (!published)
135
+ return;
136
+ const stored = this.db.prepare('SELECT document FROM graph_document_versions WHERE id=? AND graph_id=?').get(head['cursor'], scope.graphId);
137
+ if (!stored)
138
+ return;
139
+ const before = JSON.parse(String(stored['document']));
140
+ const after = current.document;
141
+ const collections = [
142
+ { field: 'nodes', key: row => String(row['id']) }, { field: 'edges', key: row => String(row['id']) },
143
+ { field: 'groups', key: row => String(row['id']) }, { field: 'members', key: row => JSON.stringify([row['group_id'], row['node_id']]) },
144
+ { field: 'outputs', key: row => String(row['node_id']) },
145
+ ];
146
+ for (const version of this.db.prepare('SELECT id,document FROM graph_document_versions WHERE graph_id=?').all(scope.graphId)) {
147
+ const document = JSON.parse(String(version['document']));
148
+ if (before.title !== after.title)
149
+ document.title = after.title;
150
+ for (const { field, key } of collections) {
151
+ const oldRows = new Map(before[field].map(row => [key(row), row]));
152
+ const nextRows = new Map(after[field].map(row => [key(row), row]));
153
+ const changed = new Set([...oldRows.keys(), ...nextRows.keys()].filter(id => canonicalJson((oldRows.get(id) ?? null)) !== canonicalJson((nextRows.get(id) ?? null))));
154
+ document[field] = document[field].filter(row => !changed.has(key(row)) || nextRows.has(key(row))).map(row => {
155
+ const id = key(row), old = oldRows.get(id), next = nextRows.get(id);
156
+ if (!changed.has(id) || !old || !next)
157
+ return row;
158
+ const merged = { ...row };
159
+ for (const [name, value] of Object.entries(next))
160
+ if (canonicalJson((old[name] ?? null)) !== canonicalJson((value ?? null)))
161
+ merged[name] = value;
162
+ return merged;
163
+ });
164
+ for (const [id, row] of nextRows)
165
+ if (changed.has(id) && !document[field].some(item => key(item) === id))
166
+ document[field].push(row);
167
+ }
168
+ this.db.prepare('UPDATE graph_document_versions SET document=? WHERE id=?').run(canonicalJson(document), version['id']);
169
+ this.db.prepare('DELETE FROM graph_document_resources WHERE version_id=?').run(version['id']);
170
+ for (const node of document.nodes)
171
+ for (const link of resourceLinks(JSON.parse(String(node['content']))))
172
+ this.db.prepare('INSERT OR IGNORE INTO graph_document_resources(version_id,resource_id,resource_version) VALUES(?,?,?)').run(version['id'], link.resourceId, link.version);
173
+ }
174
+ this.db.prepare('UPDATE graph_document_heads SET execution_revision=?,layout_revision=?,run_sequence=? WHERE graph_id=?').run(current.execution, current.layout, current.run, scope.graphId);
175
+ }
176
+ record(scope, before, boundary = false) {
177
+ if (!this.db.isTransaction)
178
+ throw Error('Document history requires the command transaction');
179
+ const after = this.capture(scope), id = scope.graphId;
180
+ if (boundary) {
181
+ this.clear(id);
182
+ return;
183
+ }
184
+ let head = this.head(id);
185
+ if (!head || Number(head['expires_at']) <= Date.now() || head['execution_revision'] !== before.execution || head['layout_revision'] !== before.layout || head['run_sequence'] !== before.run) {
186
+ this.clear(id);
187
+ const baseline = this.append(scope, before.document);
188
+ this.db.prepare('INSERT INTO graph_document_heads VALUES(?,?,?,?,?,?)').run(id, baseline, before.execution, before.layout, before.run, Date.now() + TTL);
189
+ head = this.head(id);
190
+ }
191
+ let cursor = Number(head['cursor']);
192
+ // Keep active Runs gated even when a stale head required a new baseline.
193
+ // Also repair a missing gate before appending the current user command.
194
+ this.anchorActiveRuns(id, cursor);
195
+ if (canonicalJson(before.document) !== canonicalJson(after.document)) {
196
+ this.db.prepare('DELETE FROM graph_document_versions WHERE graph_id=? AND id>?').run(id, cursor);
197
+ cursor = this.append(scope, after.document);
198
+ }
199
+ this.db.prepare('UPDATE graph_document_heads SET cursor=?,execution_revision=?,layout_revision=?,run_sequence=? WHERE graph_id=?').run(cursor, after.execution, after.layout, after.run, id);
200
+ const versions = this.db.prepare('SELECT id,length(CAST(document AS BLOB)) AS bytes FROM graph_document_versions WHERE graph_id=? ORDER BY id DESC').all(id);
201
+ let bytes = 0;
202
+ for (let i = 0; i < versions.length; i++) {
203
+ const v = versions[i];
204
+ bytes += Number(v['bytes']);
205
+ if ((i >= MAX_VERSIONS || bytes > MAX_BYTES) && Number(v['id']) < cursor)
206
+ this.db.prepare('DELETE FROM graph_document_versions WHERE id=?').run(v['id']);
207
+ }
208
+ }
209
+ travel(request, principal) {
210
+ if (!['undo', 'redo'].includes(request.direction) || typeof request.expectedCursor !== 'string' || !request.expectedCursor || !Number.isSafeInteger(request.expectedExecutionRevision) || !Number.isSafeInteger(request.expectedLayoutRevision))
211
+ throw new ServiceError('INVALID_REQUEST', '撤销重做需要方向、历史游标和整数修订号。');
212
+ return this.graphs.repo.idempotent(principal + ':graph.history:' + request.graphId, request.idempotencyKey, request, () => {
213
+ const graph = this.graphs.writable(request);
214
+ const head = this.valid(request);
215
+ if (this.active(request.graphId) && !this.db.prepare('SELECT 1 FROM graph_document_run_gates WHERE graph_id=? LIMIT 1').get(request.graphId))
216
+ throw new ServiceError('ACTIVE_RUN', '图中存在正在运行的任务,请先等待任务结束。');
217
+ if (!head || String(head['cursor']) !== request.expectedCursor || graph['execution_revision'] !== request.expectedExecutionRevision || graph['layout_revision'] !== request.expectedLayoutRevision)
218
+ throw new ServiceError('REVISION_CONFLICT', '工作图或历史版本已变化,请读取最新状态后重试。');
219
+ const gates = request.direction === 'undo' ? this.db.prepare('SELECT g.run_id,r.status FROM graph_document_run_gates g JOIN runs r ON r.id=g.run_id WHERE g.graph_id=? AND g.cursor=?').all(request.graphId, Number(head['cursor'])) : [];
220
+ if (gates.some(g => !TERMINAL_RUN_STATUSES.includes(String(g['status']))))
221
+ throw new ServiceError('ACTIVE_RUN', '请先确认停止正在运行的任务,并等待运行时完成收尾。');
222
+ const target = this.db.prepare('SELECT id,document FROM graph_document_versions WHERE graph_id=? AND id' + (request.direction === 'undo' ? '<' : '>') + '? ORDER BY id ' + (request.direction === 'undo' ? 'DESC' : 'ASC') + ' LIMIT 1').get(request.graphId, Number(head['cursor']));
223
+ if (!target && !gates.length)
224
+ throw new ServiceError('CONFLICT', request.direction === 'undo' ? '没有可撤销操作。' : '没有可重做操作。');
225
+ if (gates.length)
226
+ this.db.prepare('DELETE FROM graph_document_run_gates WHERE graph_id=? AND cursor=?').run(request.graphId, Number(head['cursor']));
227
+ if (!target)
228
+ return this.graphs.snapshot(request);
229
+ this.restore(request, JSON.parse(String(target['document'])));
230
+ this.db.prepare('UPDATE graphs SET execution_revision=execution_revision+1,layout_revision=layout_revision+1 WHERE id=?').run(request.graphId);
231
+ this.db.prepare('UPDATE graph_document_heads SET cursor=?,execution_revision=?,layout_revision=? WHERE graph_id=?').run(target['id'], request.expectedExecutionRevision + 1, request.expectedLayoutRevision + 1, request.graphId);
232
+ this.graphs.event(request);
233
+ return this.graphs.snapshot(request);
234
+ });
235
+ }
236
+ restore(scope, doc) {
237
+ const id = scope.graphId;
238
+ // Stable node identities preserve selection and Run foreign keys. Body versions never rewind.
239
+ this.db.prepare('DELETE FROM edges WHERE graph_id=?').run(id);
240
+ this.db.prepare('DELETE FROM group_members WHERE group_id IN (SELECT id FROM graph_groups WHERE graph_id=?)').run(id);
241
+ this.db.prepare('DELETE FROM graph_groups WHERE graph_id=?').run(id);
242
+ this.db.prepare('DELETE FROM execution_outputs WHERE node_id IN (SELECT id FROM nodes WHERE graph_id=?)').run(id);
243
+ this.db.prepare("DELETE FROM canvas_resource_references WHERE graph_id=? AND owner_kind='node'").run(id);
244
+ const live = new Set(doc.nodes.map(n => String(n['id'])));
245
+ for (const n of this.db.prepare('SELECT id FROM nodes WHERE graph_id=? AND deleted=0').all(id))
246
+ if (!live.has(String(n['id']))) {
247
+ this.db.prepare('UPDATE nodes SET deleted=1,undo_expires_at=NULL WHERE id=?').run(n['id']);
248
+ this.db.prepare('DELETE FROM node_resource_history WHERE node_id=?').run(n['id']);
249
+ }
250
+ for (const n of doc.nodes) {
251
+ const current = this.db.prepare('SELECT n.*,v.content FROM nodes n JOIN node_versions v ON v.node_id=n.id AND v.version=n.current_version WHERE n.id=? AND n.graph_id=?').get(n['id'], id);
252
+ if (!current)
253
+ throw new ServiceError('CONFLICT', '历史节点已不存在,无法恢复。');
254
+ let version = Number(current['current_version']);
255
+ if (current['content'] !== n['content'] || current['type'] !== n['type'] || current['deleted'] !== n['deleted']) {
256
+ version = Number(this.db.prepare('SELECT max(version)+1 AS n FROM node_versions WHERE node_id=?').get(n['id'])['n']);
257
+ this.db.prepare('INSERT INTO node_versions(node_id,version,content) VALUES(?,?,?)').run(n['id'], version, n['content']);
258
+ }
259
+ this.db.prepare('UPDATE nodes SET type=?,schema_version=?,current_version=?,x=?,y=?,width=?,height=?,read_only=?,read_only_reason=?,deleted=?,undo_expires_at=NULL WHERE id=?').run(n['type'], n['schema_version'], version, n['x'], n['y'], n['width'], n['height'], n['read_only'], n['read_only_reason'], n['deleted'], n['id']);
260
+ this.graphs.retainResources(scope, String(n['id']), version, JSON.parse(String(n['content'])));
261
+ if (n['deleted'])
262
+ this.db.prepare("DELETE FROM canvas_resource_references WHERE owner_kind='node' AND node_id=?").run(n['id']);
263
+ }
264
+ for (const g of doc.groups)
265
+ this.db.prepare('INSERT INTO graph_groups(id,graph_id,run_id,title) VALUES(?,?,?,?)').run(g['id'], id, g['run_id'], g['title']);
266
+ for (const m of doc.members)
267
+ this.db.prepare('INSERT INTO group_members(group_id,node_id) VALUES(?,?)').run(m['group_id'], m['node_id']);
268
+ for (const o of doc.outputs)
269
+ this.db.prepare('INSERT INTO execution_outputs(node_id,execution_node_id) VALUES(?,?)').run(o['node_id'], o['execution_node_id']);
270
+ for (const e of doc.edges)
271
+ this.db.prepare('INSERT INTO edges(id,graph_id,source_id,target_id,kind) VALUES(?,?,?,?,?)').run(e['id'], id, e['source_id'], e['target_id'], e['kind']);
272
+ this.db.prepare('UPDATE graphs SET title=? WHERE id=?').run(doc.title, id);
273
+ }
274
+ }
275
+ //# sourceMappingURL=graph-document-history.js.map
@@ -0,0 +1,14 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ /** Only explicit graph purges enqueue IDs. Never enumerate user projects or
3
+ * infer orphan ownership. Retain failed entries across restarts for retry. */
4
+ export declare class GraphFiles {
5
+ readonly db: DatabaseSync;
6
+ readonly runsDirectory: string;
7
+ private pending;
8
+ constructor(db: DatabaseSync, runsDirectory: string);
9
+ drain(): Promise<{
10
+ removed: number;
11
+ failed: number;
12
+ }>;
13
+ private work;
14
+ }