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
package/dist/runs.js ADDED
@@ -0,0 +1,506 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { isTerminalRunStatus } from "./vendor/protocol/index.js";
3
+ import { Graphs } from './graphs.js';
4
+ import { ServiceError } from './errors.js';
5
+ import { atomic } from './persistence/database.js';
6
+ import { canonicalJson, Repositories } from './persistence/repositories.js';
7
+ const terminalSql = "'succeeded','failed','cancelled','interrupted'";
8
+ const notificationStatuses = new Set(['succeeded', 'failed', 'waiting_answer', 'waiting_approval', 'interrupted']);
9
+ const transitions = {
10
+ accepted: ['queued', 'cancelled', 'failed'], queued: ['preparing', 'cancelled', 'failed'],
11
+ preparing: ['running', 'cancelling', 'failed', 'reconciling', 'interrupted'],
12
+ running: ['waiting_answer', 'waiting_approval', 'agent_completed', 'cancelling', 'failed', 'reconciling', 'interrupted'],
13
+ waiting_answer: ['running', 'waiting_answer', 'waiting_approval', 'cancelling', 'reconciling', 'interrupted', 'failed'],
14
+ waiting_approval: ['running', 'waiting_answer', 'waiting_approval', 'cancelling', 'reconciling', 'interrupted', 'failed'],
15
+ agent_completed: ['finalizing', 'cancelling', 'reconciling', 'failed'],
16
+ finalizing: ['succeeded', 'failed', 'cancelling'],
17
+ cancelling: ['cancelled', 'reconciling'],
18
+ reconciling: ['running', 'cancelling', 'interrupted', 'agent_completed', 'finalizing'],
19
+ paused_restore: ['accepted', 'queued', 'cancelled'], succeeded: [], failed: [], cancelled: [], interrupted: [],
20
+ };
21
+ function json(value) { return JSON.parse(JSON.stringify(value)); }
22
+ function recordedFailureReason(details) {
23
+ if (typeof details.error !== 'string')
24
+ return undefined;
25
+ const reason = details.error.trim();
26
+ return reason ? reason.slice(0, 2000) : undefined;
27
+ }
28
+ function sync(value) {
29
+ if (value && typeof value.then === 'function')
30
+ throw new Error('Run dependencies must be synchronous');
31
+ return value;
32
+ }
33
+ /** All authoritative changes are short synchronous transactions (safe inside Auth). */
34
+ export class Runs {
35
+ db;
36
+ serviceId;
37
+ dependencies;
38
+ repo;
39
+ now;
40
+ constructor(db, serviceId, dependencies) {
41
+ this.db = db;
42
+ this.serviceId = serviceId;
43
+ this.dependencies = dependencies;
44
+ this.repo = new Repositories(db);
45
+ this.now = dependencies.now ?? Date.now;
46
+ }
47
+ get(id) {
48
+ const row = this.db.prepare('SELECT * FROM runs WHERE id=?').get(id);
49
+ if (!row)
50
+ throw new ServiceError('NOT_FOUND', 'Run not found');
51
+ const details = this.db.prepare('SELECT details FROM run_runtime WHERE run_id=?').get(id);
52
+ const executionDetails = details ? JSON.parse(String(details.details)) : {};
53
+ const executionStart = executionDetails.executionStart;
54
+ const chainBatch = typeof executionDetails.chainBatch === 'string' ? executionDetails.chainBatch : undefined;
55
+ const chainMembers = chainBatch === id ? this.db.prepare("SELECT r.status 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(row.project_id, row.graph_id, id) : [];
56
+ const chainActive = chainMembers.some(member => !isTerminalRunStatus(member.status));
57
+ const chainControl = chainMembers.length ? executionDetails.chainStopRequested ? chainActive ? 'stopping' : 'stopped' : chainActive ? 'active' : undefined : undefined;
58
+ return { serviceId: this.serviceId, projectId: String(row.project_id), graphId: String(row.graph_id), id: String(row.id), nodeId: String(row.node_id), status: row.status, submissionSequence: String(row.sequence), inputDigest: String(row.input_digest), createdAt: String(row.created_at), historyState: row.history_state, ...(row.status === 'accepted' && executionStart ? { executionStart } : {}), ...(chainBatch ? { chainBatch } : {}), ...(chainControl ? { chainControl } : {}) };
59
+ }
60
+ list(projectId) {
61
+ const rows = projectId === undefined ? this.db.prepare('SELECT id FROM runs ORDER BY sequence').all() : this.db.prepare('SELECT id FROM runs WHERE project_id=? ORDER BY sequence').all(projectId);
62
+ return rows.map(row => this.get(String(row.id)));
63
+ }
64
+ notifications() {
65
+ return this.db.prepare(`SELECT n.run_id,n.revision,n.created_at FROM run_notifications n JOIN runs r ON r.id=n.run_id WHERE n.read_at IS NULL AND n.status=r.status ORDER BY n.created_at,n.run_id`).all().map(row => ({
66
+ run: this.get(String(row.run_id)),
67
+ revision: Number(row.revision),
68
+ createdAt: String(row.created_at),
69
+ }));
70
+ }
71
+ readNotification(id, expectedRevision) {
72
+ return atomic(this.db, () => {
73
+ if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 1)
74
+ throw new ServiceError('INVALID_REQUEST', 'Invalid notification revision');
75
+ const row = this.db.prepare('SELECT n.revision,n.read_at,r.project_id,r.graph_id FROM run_notifications n JOIN runs r ON r.id=n.run_id WHERE n.run_id=?').get(id);
76
+ if (!row)
77
+ throw new ServiceError('NOT_FOUND', 'Notification not found');
78
+ if (Number(row.revision) !== expectedRevision)
79
+ throw new ServiceError('REVISION_CONFLICT', 'Notification changed');
80
+ if (row.read_at !== null)
81
+ return { read: false };
82
+ this.db.prepare('UPDATE run_notifications SET read_at=? WHERE run_id=? AND revision=? AND read_at IS NULL').run(new Date(this.now()).toISOString(), id, expectedRevision);
83
+ this.repo.appendEvent({ eventId: randomUUID(), type: 'notification.changed', projectId: String(row.project_id), graphId: String(row.graph_id), entityId: id, revision: expectedRevision, occurredAt: new Date(this.now()).toISOString(), payload: { runId: id, read: true, notificationRevision: expectedRevision } });
84
+ return { read: true };
85
+ });
86
+ }
87
+ readAllNotifications() {
88
+ return atomic(this.db, () => {
89
+ const rows = this.db.prepare('SELECT n.run_id,n.revision FROM run_notifications n JOIN runs r ON r.id=n.run_id WHERE n.read_at IS NULL AND n.status=r.status').all();
90
+ if (!rows.length)
91
+ return { read: 0 };
92
+ this.db.prepare('UPDATE run_notifications SET read_at=? WHERE read_at IS NULL AND EXISTS(SELECT 1 FROM runs r WHERE r.id=run_notifications.run_id AND r.status=run_notifications.status)').run(new Date(this.now()).toISOString());
93
+ const revision = Math.max(...rows.map(row => Number(row.revision)));
94
+ this.repo.appendEvent({ eventId: randomUUID(), type: 'notification.changed', projectId: null, graphId: null, entityId: 'all', revision, occurredAt: new Date(this.now()).toISOString(), payload: { readAll: true, count: rows.length } });
95
+ return { read: rows.length };
96
+ });
97
+ }
98
+ runtime(id) {
99
+ const row = this.db.prepare('SELECT * FROM run_runtime WHERE run_id=?').get(id);
100
+ if (!row)
101
+ throw new ServiceError('NOT_FOUND', 'Run runtime not found');
102
+ return { epoch: String(row.epoch), revision: Number(row.revision), details: JSON.parse(String(row.details)) };
103
+ }
104
+ snapshot(id) {
105
+ const effective = this.db.prepare('SELECT payload FROM execution_input_snapshots WHERE run_id=?').get(id);
106
+ if (effective)
107
+ return JSON.parse(String(effective.payload));
108
+ const row = this.db.prepare('SELECT payload FROM snapshots WHERE run_id=?').get(id);
109
+ if (!row)
110
+ throw new ServiceError('NOT_FOUND', 'Run snapshot not found');
111
+ return JSON.parse(String(row.payload));
112
+ }
113
+ event(run, revision, type = 'run.changed', entityId = run.id, payload = json(run)) {
114
+ this.repo.appendEvent({ eventId: randomUUID(), type, projectId: run.projectId, graphId: run.graphId, entityId, revision, occurredAt: new Date(this.now()).toISOString(), payload });
115
+ }
116
+ refreshNotification(run) {
117
+ if (!notificationStatuses.has(run.status))
118
+ return;
119
+ this.db.prepare(`INSERT INTO run_notifications(run_id,status,revision,created_at,read_at) VALUES(?,?,1,?,NULL)
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
+ }
122
+ record(id, kind, payload) {
123
+ atomic(this.db, () => {
124
+ if (this.get(id).historyState === 'cleared')
125
+ return;
126
+ this.db.prepare('INSERT INTO run_process_records(run_id,occurred_at,kind,payload) VALUES(?,?,?,?)').run(id, new Date(this.now()).toISOString(), kind, canonicalJson(payload));
127
+ });
128
+ }
129
+ submit(request, principal = 'local', holdExecution = false) {
130
+ return this.repo.idempotent(principal + ':run.submit', request.idempotencyKey, json(request), () => {
131
+ if (request.serviceId !== this.serviceId)
132
+ throw new ServiceError('SERVICE_MISMATCH', 'Wrong service');
133
+ if (this.repo.setting('acceptingRuns') !== true)
134
+ throw new ServiceError('MAINTENANCE', 'Service is not accepting runs');
135
+ const row = this.db.prepare('SELECT p.state,p.canonical_path,g.archived,g.trashed,g.execution_revision,n.type,n.deleted,n.current_version,n.read_only FROM projects p JOIN graphs g ON g.project_id=p.id JOIN nodes n ON n.graph_id=g.id WHERE p.id=? AND g.id=? AND n.id=?').get(request.projectId, request.graphId, request.nodeId);
136
+ if (!row)
137
+ throw new ServiceError('NOT_FOUND', 'Run target not found');
138
+ if (row.state !== 'active')
139
+ throw new ServiceError('PROJECT_INACTIVE', 'Project is inactive');
140
+ if (row.archived || row.trashed || row.deleted || row.read_only)
141
+ throw new ServiceError('NODE_LOCKED', 'Run target is archived, deleted or read-only');
142
+ const expectedType = { execution: 'execution', text_generation: 'text', image_generation: 'image' }[request.kind];
143
+ if (!expectedType || row.type !== expectedType)
144
+ throw new ServiceError('INVALID_REQUEST', 'Run kind does not match node type');
145
+ if (!Number.isSafeInteger(request.expectedExecutionRevision) || row.execution_revision !== request.expectedExecutionRevision)
146
+ throw new ServiceError('REVISION_CONFLICT', 'Graph execution revision changed');
147
+ if (this.db.prepare(`SELECT 1 FROM runs WHERE node_id=? AND status NOT IN (${terminalSql})`).get(request.nodeId))
148
+ throw new ServiceError('ACTIVE_RUN', 'Node already has an unfinished run');
149
+ const context = { runId: randomUUID(), projectId: request.projectId, graphId: request.graphId, nodeId: request.nodeId, kind: request.kind, baseVersion: Number(row.current_version), canonicalPath: String(row.canonical_path), publicationVersion: request.kind === 'execution' ? 2 : 1 };
150
+ if (this.dependencies.validate)
151
+ sync(this.dependencies.validate(request, context));
152
+ const snapshot = sync(this.dependencies.freeze(request, context));
153
+ const hasImage = Array.isArray(snapshot?.resources) && snapshot.resources.some(item => item.kind === 'image' && item.resource) || Array.isArray(snapshot?.projectFiles) && snapshot.projectFiles.some(item => item.kind === 'image');
154
+ if (!snapshot || (!snapshot.prompt?.trim() && !(request.kind === 'image_generation' && hasImage)) || !snapshot.inputDigest || snapshot.executionRevision !== request.expectedExecutionRevision || !Array.isArray(snapshot.resources) || snapshot.projectFiles !== undefined && !Array.isArray(snapshot.projectFiles))
155
+ throw new ServiceError('INPUT_BLOCKED', 'Invalid frozen input snapshot');
156
+ const route = request.kind === 'image_generation' ? request.imageRoute ?? { type: 'codex' } : undefined;
157
+ if (route?.type === 'api') {
158
+ if (request.modelOverride)
159
+ throw new ServiceError('INVALID_REQUEST', 'API image runs cannot select a Codex model');
160
+ const frozen = snapshot.imageRoute;
161
+ const mode = hasImage ? (snapshot.prompt.trim() ? 'text_image' : 'image') : 'text';
162
+ if (!frozen || frozen.type !== 'api' || snapshot.model !== undefined || snapshot.inputMode !== mode || frozen.providerId !== route.providerId || frozen.modelId !== route.modelId || canonicalJson((frozen.options ?? null)) !== canonicalJson((route.options ?? null)) || !Number.isSafeInteger(frozen.configRevision) || frozen.configRevision < 1 || !Number.isSafeInteger(frozen.credentialRevision) || frozen.credentialRevision < 1 || Object.keys(frozen).some(key => !['type', 'providerId', 'modelId', 'options', 'configRevision', 'credentialRevision'].includes(key)))
163
+ throw new ServiceError('INPUT_BLOCKED', 'Invalid frozen API image route');
164
+ }
165
+ else if (!snapshot.model?.model || (request.kind === 'image_generation' && canonicalJson(((snapshot.imageRoute?.type === 'codex' ? snapshot.imageRoute.options : undefined) ?? null)) !== canonicalJson(((route?.type === 'codex' ? route.options : undefined) ?? null))))
166
+ throw new ServiceError('INPUT_BLOCKED', 'Invalid frozen Codex model/route');
167
+ const graphHistory = new Graphs(this.db, this.serviceId);
168
+ const historyBefore = graphHistory.history.capture(request);
169
+ if (request.kind === 'execution' && !holdExecution)
170
+ graphHistory.retireExecutionOutputs(request, request.nodeId);
171
+ this.db.prepare('INSERT INTO runs(id,project_id,graph_id,node_id,kind,status,input_digest,created_at) VALUES(?,?,?,?,?,?,?,?)').run(context.runId, request.projectId, request.graphId, request.nodeId, request.kind, 'accepted', snapshot.inputDigest, new Date(this.now()).toISOString());
172
+ graphHistory.history.recordRunSubmission(request, context.runId, historyBefore);
173
+ this.db.prepare('INSERT INTO run_runtime(run_id,epoch,details) VALUES(?,?,?)').run(context.runId, '', canonicalJson(json({ ...context, ...(request.kind === 'execution' ? { chainDependencies: [] } : {}), ...(holdExecution ? { executionStart: 'manual' } : {}) })));
174
+ this.db.prepare('INSERT INTO snapshots(run_id,input_digest,payload) VALUES(?,?,?)').run(context.runId, snapshot.inputDigest, canonicalJson(json(snapshot)));
175
+ const seen = new Set();
176
+ for (const input of snapshot.resources) {
177
+ const resource = input.resource;
178
+ if (!resource)
179
+ continue;
180
+ const key = resource.resourceId + '/' + resource.version;
181
+ if (seen.has(key))
182
+ continue;
183
+ seen.add(key);
184
+ const stored = this.db.prepare('SELECT v.sha256,v.representation_version FROM canvas_resource_versions v JOIN canvas_resources r ON r.id=v.resource_id WHERE v.resource_id=? AND v.version=? AND r.graph_id=?').get(resource.resourceId, resource.version, request.graphId);
185
+ if (!stored || stored.sha256 !== resource.sha256 || stored.representation_version !== resource.representationVersion)
186
+ throw new ServiceError('INPUT_BLOCKED', 'Frozen resource does not match its immutable version');
187
+ this.db.prepare("INSERT INTO canvas_resource_references(id,resource_id,resource_version,graph_id,owner_kind,run_id) VALUES(?,?,?,?,'snapshot',?)").run(randomUUID(), resource.resourceId, resource.version, request.graphId, context.runId);
188
+ }
189
+ this.event(this.get(context.runId), 0);
190
+ if (holdExecution) {
191
+ this.record(context.runId, 'state', { from: 'submitted', to: 'accepted', reason: '任务已提交,等待开始执行。' });
192
+ return json(this.get(context.runId));
193
+ }
194
+ return json(this.transition(context.runId, this.runtime(context.runId), 'queued'));
195
+ });
196
+ }
197
+ capacity() {
198
+ const configured = this.repo.setting('capacity');
199
+ if (typeof configured !== 'number' || !Number.isSafeInteger(configured) || configured < 1)
200
+ throw new ServiceError('MAINTENANCE', 'Invalid scheduler capacity');
201
+ const occupied = Number(this.db.prepare('SELECT COUNT(*) AS n FROM occupancy').get().n);
202
+ return { capacity: configured, occupied, available: Math.max(0, configured - occupied) };
203
+ }
204
+ /** ExecutionChains enqueues only dependency-ready runs. Claim those in persisted
205
+ * order up to capacity, without serializing unrelated nodes by project or batch. */
206
+ claimNext(epoch, excludedProjectIds = [], eligible) {
207
+ if (!epoch)
208
+ throw new ServiceError('INVALID_REQUEST', 'Scheduler epoch required');
209
+ return atomic(this.db, () => {
210
+ if (this.repo.setting('acceptingRuns') !== true)
211
+ return null;
212
+ const capacity = this.capacity();
213
+ if (!capacity.available)
214
+ return null;
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
223
+ `).all(...excludedProjectIds);
224
+ const selected = rows.find(row => {
225
+ const run = this.get(String(row.id));
226
+ return !eligible || eligible(run, this.snapshot(run.id));
227
+ });
228
+ if (!selected)
229
+ return null;
230
+ const id = String(selected.id);
231
+ const runtime = this.runtime(id);
232
+ const slots = new Set(this.db.prepare('SELECT slot FROM occupancy').all().map(item => Number(item.slot)));
233
+ let slot = 0;
234
+ while (slots.has(slot))
235
+ slot++;
236
+ // Keep the legacy nullable project_id column for database compatibility;
237
+ // occupancy now reserves only a runtime slot, never a project-wide slot.
238
+ this.db.prepare('INSERT INTO occupancy(run_id,slot,project_id) VALUES(?,?,NULL)').run(id, slot);
239
+ this.db.prepare('UPDATE run_runtime SET epoch=? WHERE run_id=?').run(epoch, id);
240
+ return this.transition(id, { epoch, revision: runtime.revision }, 'preparing');
241
+ });
242
+ }
243
+ /** Persist the uncertain spawn boundary BEFORE calling an external backend. */
244
+ markBackendLaunching(id, token) {
245
+ return atomic(this.db, () => {
246
+ const runtime = this.runtime(id);
247
+ if (runtime.epoch !== token.epoch || runtime.revision !== token.revision)
248
+ throw new ServiceError('REVISION_CONFLICT', 'Stale launch attempt');
249
+ if (this.get(id).status !== 'preparing' || runtime.details.backendLaunchAttempted === true)
250
+ throw new ServiceError('CONFLICT', 'Run is not eligible to launch');
251
+ this.db.prepare('UPDATE run_runtime SET revision=revision+1,details=? WHERE run_id=?').run(canonicalJson({ ...runtime.details, backendLaunchAttempted: true }), id);
252
+ this.event(this.get(id), runtime.revision + 1);
253
+ return this.runtime(id);
254
+ });
255
+ }
256
+ /** Only a known lock-contention result BEFORE the spawn boundary can requeue.
257
+ * No generic preparing -> queued transition is allowed after uncertain launch. */
258
+ requeueUnstarted(id, token) {
259
+ return atomic(this.db, () => {
260
+ const run = this.get(id);
261
+ const runtime = this.runtime(id);
262
+ if (runtime.epoch !== token.epoch || runtime.revision !== token.revision)
263
+ throw new ServiceError('REVISION_CONFLICT', 'Stale requeue attempt');
264
+ if (run.status !== 'preparing' || runtime.details.backendLaunchAttempted === true || runtime.details.cancelRequested === true)
265
+ throw new ServiceError('CONFLICT', 'Cannot requeue after launch or cancellation');
266
+ this.db.prepare('UPDATE run_runtime SET revision=revision+1 WHERE run_id=?').run(id);
267
+ this.db.prepare("UPDATE runs SET status='queued' WHERE id=?").run(id);
268
+ this.db.prepare('DELETE FROM occupancy WHERE run_id=?').run(id);
269
+ const changed = this.get(id);
270
+ this.event(changed, runtime.revision + 1);
271
+ this.record(id, 'state', { from: 'preparing', to: 'queued', reason: 'project_lock_busy' });
272
+ return changed;
273
+ });
274
+ }
275
+ transition(id, token, next, details = {}) {
276
+ return atomic(this.db, () => {
277
+ const run = this.get(id);
278
+ const runtime = this.runtime(id);
279
+ if (runtime.epoch !== token.epoch || runtime.revision !== token.revision)
280
+ throw new ServiceError('REVISION_CONFLICT', 'Stale run callback');
281
+ if (!transitions[run.status]?.includes(next))
282
+ throw new ServiceError('CONFLICT', `Invalid run transition ${run.status} -> ${next}`);
283
+ if (!['accepted', 'queued', 'paused_restore'].includes(next) && !isTerminalRunStatus(next) && !this.db.prepare('SELECT 1 FROM occupancy WHERE run_id=?').get(id))
284
+ throw new ServiceError('CONFLICT', 'Active states require an atomic capacity claim');
285
+ if (runtime.details.cancelRequested === true && !['cancelling', 'cancelled', 'reconciling'].includes(next))
286
+ throw new ServiceError('CONFLICT', 'Cancellation already accepted');
287
+ if (['kind', 'baseVersion', 'canonicalPath', 'publicationVersion', 'runId', 'projectId', 'graphId', 'nodeId'].some(key => key in details && details[key] !== runtime.details[key]))
288
+ throw new ServiceError('INVALID_REQUEST', 'Frozen runtime identity cannot change');
289
+ const updated = { ...runtime.details, ...details, ...(isTerminalRunStatus(next) ? { completedAt: new Date(this.now()).toISOString() } : {}) };
290
+ const result = this.db.prepare('UPDATE run_runtime SET revision=revision+1,details=? WHERE run_id=? AND epoch=? AND revision=?').run(canonicalJson(updated), id, token.epoch, token.revision);
291
+ if (Number(result.changes) !== 1)
292
+ throw new ServiceError('REVISION_CONFLICT', 'Stale run callback');
293
+ this.db.prepare('UPDATE runs SET status=? WHERE id=?').run(next, id);
294
+ if (isTerminalRunStatus(next))
295
+ this.db.prepare('DELETE FROM occupancy WHERE run_id=?').run(id);
296
+ if (isTerminalRunStatus(next) || next === 'cancelling' || next === 'reconciling')
297
+ this.expireInteractions(id);
298
+ const changed = this.get(id);
299
+ this.refreshNotification(changed);
300
+ this.event(changed, runtime.revision + 1);
301
+ if (isTerminalRunStatus(next)) {
302
+ const paths = this.db.prepare('SELECT relative_path FROM project_file_outputs WHERE run_id=? ORDER BY output_key').all(id).map(row => String(row['relative_path']));
303
+ this.repo.appendEvent({ eventId: randomUUID(), type: 'project.files.changed', projectId: changed.projectId, graphId: null, entityId: changed.projectId, revision: runtime.revision + 1, occurredAt: new Date(this.now()).toISOString(), payload: { runId: id, paths, fullRecheck: true } });
304
+ }
305
+ const reason = next === 'failed' ? recordedFailureReason(details) : undefined;
306
+ this.record(id, 'state', { from: run.status, to: next, ...(reason ? { reason } : {}) });
307
+ return changed;
308
+ });
309
+ }
310
+ cancel(id, key, principal = 'local') {
311
+ return this.repo.idempotent(principal + ':run.cancel:' + id, key, { id }, () => {
312
+ const run = this.get(id);
313
+ if (isTerminalRunStatus(run.status) || run.status === 'cancelling')
314
+ return json(run);
315
+ // The agent has already stopped; invalidate pending publication callbacks locally.
316
+ if (['agent_completed', 'finalizing'].includes(run.status)) {
317
+ this.transition(id, this.runtime(id), 'cancelling', { cancelRequested: true });
318
+ return json(this.transition(id, this.runtime(id), 'cancelled'));
319
+ }
320
+ return json(this.transition(id, this.runtime(id), ['queued', 'accepted', 'paused_restore'].includes(run.status) ? 'cancelled' : 'cancelling', { cancelRequested: true }));
321
+ });
322
+ }
323
+ interaction(id) {
324
+ const row = this.db.prepare('SELECT * FROM interactions WHERE id=?').get(id);
325
+ if (!row)
326
+ throw new ServiceError('NOT_FOUND', 'Interaction not found');
327
+ return { id: String(row.id), runId: String(row.run_id), epoch: String(row.epoch), version: Number(row.version), kind: row.kind, status: row.status, payload: JSON.parse(String(row.payload)) };
328
+ }
329
+ /** Oldest independent action whose reply has not been acknowledged by its responder. */
330
+ activeInteraction(id) {
331
+ const runtime = this.runtime(id);
332
+ const delivered = new Set(Array.isArray(runtime.details.deliveredInteractionIds) ? runtime.details.deliveredInteractionIds : []);
333
+ if (typeof runtime.details.deliveredInteractionId === 'string')
334
+ delivered.add(runtime.details.deliveredInteractionId);
335
+ const row = this.db.prepare("SELECT id FROM interactions WHERE run_id=? AND epoch=? AND status!='expired' ORDER BY rowid").all(id, runtime.epoch).find(row => !delivered.has(String(row.id)));
336
+ return row ? this.interaction(String(row.id)) : null;
337
+ }
338
+ acknowledgeReply(id, epoch, interactionId) {
339
+ return atomic(this.db, () => {
340
+ const runtime = this.runtime(id);
341
+ const run = this.get(id);
342
+ const active = this.activeInteraction(id);
343
+ if (runtime.epoch !== epoch)
344
+ throw new ServiceError('REVISION_CONFLICT', 'Stale responder epoch');
345
+ if (!['waiting_answer', 'waiting_approval'].includes(run.status) || active?.id !== interactionId || active.status !== 'answered')
346
+ throw new ServiceError('ALREADY_HANDLED', 'Action is no longer awaiting delivery');
347
+ const delivered = Array.isArray(runtime.details.deliveredInteractionIds) ? [...runtime.details.deliveredInteractionIds] : [];
348
+ if (typeof runtime.details.deliveredInteractionId === 'string' && !delivered.includes(runtime.details.deliveredInteractionId))
349
+ delivered.push(runtime.details.deliveredInteractionId);
350
+ if (!delivered.includes(interactionId))
351
+ delivered.push(interactionId);
352
+ const next = this.db.prepare("SELECT id,kind FROM interactions WHERE run_id=? AND epoch=? AND status!='expired' ORDER BY rowid").all(id, epoch).find(row => !delivered.includes(String(row.id)));
353
+ return this.transition(id, runtime, next ? (next.kind === 'question' ? 'waiting_answer' : 'waiting_approval') : 'running', { deliveredInteractionIds: delivered, deliveredInteractionId: interactionId, activeInteractionId: next ? String(next.id) : null });
354
+ });
355
+ }
356
+ openInteraction(id, token, kind, payload, interactionId = randomUUID()) {
357
+ return atomic(this.db, () => {
358
+ if (kind !== 'question' && kind !== 'approval')
359
+ throw new ServiceError('INVALID_REQUEST', 'Invalid interaction kind');
360
+ const existing = this.db.prepare('SELECT id FROM interactions WHERE id=?').get(interactionId);
361
+ if (existing) {
362
+ const prior = this.interaction(interactionId);
363
+ if (prior.runId !== id || prior.epoch !== token.epoch || canonicalJson(prior.payload) !== canonicalJson(payload) || prior.kind !== kind)
364
+ throw new ServiceError('CONFLICT', 'Interaction identity reused');
365
+ return prior;
366
+ }
367
+ const run = this.get(id);
368
+ const runtime = this.runtime(id);
369
+ if (runtime.epoch !== token.epoch || runtime.revision !== token.revision)
370
+ throw new ServiceError('REVISION_CONFLICT', 'Stale interaction callback');
371
+ if (!['running', 'waiting_answer', 'waiting_approval'].includes(run.status) || runtime.details.cancelRequested === true)
372
+ throw new ServiceError('CONFLICT', 'Run cannot accept interactions');
373
+ this.db.prepare("INSERT INTO interactions(id,run_id,epoch,version,kind,status,payload) VALUES(?,?,?,1,?,'pending',?)").run(interactionId, id, token.epoch, kind, canonicalJson(payload));
374
+ const active = this.activeInteraction(id);
375
+ this.transition(id, token, active.kind === 'question' ? 'waiting_answer' : 'waiting_approval', { activeInteractionId: active.id });
376
+ const interaction = this.interaction(interactionId);
377
+ this.event(this.get(id), 1, 'interaction.changed', interactionId, json(interaction));
378
+ return interaction;
379
+ });
380
+ }
381
+ expireInteractions(id) {
382
+ const runtime = this.runtime(id);
383
+ const delivered = new Set(Array.isArray(runtime.details.deliveredInteractionIds) ? runtime.details.deliveredInteractionIds : []);
384
+ if (typeof runtime.details.deliveredInteractionId === 'string')
385
+ delivered.add(runtime.details.deliveredInteractionId);
386
+ for (const row of this.db.prepare("SELECT id FROM interactions WHERE run_id=? AND status IN ('pending','answered')").all(id)) {
387
+ const interactionId = String(row.id);
388
+ if (delivered.has(interactionId))
389
+ continue;
390
+ this.db.prepare("UPDATE interactions SET status='expired',version=version+1 WHERE id=?").run(interactionId);
391
+ const interaction = this.interaction(interactionId);
392
+ this.event(this.get(id), interaction.version, 'interaction.changed', interactionId, json(interaction));
393
+ }
394
+ }
395
+ reply(request, sessionId, principal = sessionId) {
396
+ return atomic(this.db, () => {
397
+ const session = this.db.prepare('SELECT revoked_at,last_used_at FROM sessions WHERE id=?').get(sessionId);
398
+ if (!session)
399
+ throw new ServiceError('UNAUTHENTICATED', 'Valid session required');
400
+ if (session.revoked_at !== null)
401
+ throw new ServiceError('SESSION_REVOKED', 'Session revoked');
402
+ if (Number(session.last_used_at) + 30 * 86400000 <= this.now())
403
+ throw new ServiceError('SESSION_EXPIRED', 'Session expired');
404
+ return this.repo.idempotent(principal + ':interaction.reply:' + request.interactionId, request.idempotencyKey, json(request), () => {
405
+ const interaction = this.interaction(request.interactionId);
406
+ const run = this.get(request.runId);
407
+ const runtime = this.runtime(run.id);
408
+ if (interaction.runId !== request.runId || interaction.epoch !== request.epoch || runtime.epoch !== request.epoch)
409
+ throw new ServiceError('ALREADY_HANDLED', 'Stale interaction epoch or run');
410
+ if (interaction.status !== 'pending')
411
+ throw new ServiceError('ALREADY_HANDLED', 'Interaction already handled');
412
+ if (interaction.version !== request.expectedVersion)
413
+ throw new ServiceError('REVISION_CONFLICT', 'Interaction version changed');
414
+ if (this.activeInteraction(run.id)?.id !== interaction.id)
415
+ throw new ServiceError('CONFLICT', 'An earlier independent action must be acknowledged first');
416
+ if (run.status !== (interaction.kind === 'question' ? 'waiting_answer' : 'waiting_approval'))
417
+ throw new ServiceError('ALREADY_HANDLED', 'Run is no longer waiting');
418
+ if (this.dependencies.validateReply)
419
+ sync(this.dependencies.validateReply(interaction, request.answer));
420
+ else {
421
+ const answer = request.answer;
422
+ const structured = answer !== null && typeof answer === 'object' && !Array.isArray(answer) ? answer : null;
423
+ if (interaction.kind === 'approval') {
424
+ if (typeof answer !== 'boolean' && !(structured?.kind === 'approval' && ['accept', 'decline'].includes(String(structured.decision))))
425
+ throw new ServiceError('INVALID_REQUEST', 'Approval requires accept/decline or boolean');
426
+ if (interaction.payload && typeof interaction.payload === 'object' && !Array.isArray(interaction.payload) && interaction.payload.canApprove === false && (answer === true || structured?.decision === 'accept'))
427
+ throw new ServiceError('INVALID_REQUEST', 'This action cannot be approved');
428
+ }
429
+ else {
430
+ const answers = structured?.answers;
431
+ const validStructured = structured?.kind === 'question' && answers !== null && typeof answers === 'object' && !Array.isArray(answers) && Object.keys(answers).length > 0 && Object.values(answers).every(value => Array.isArray(value) && value.length > 0 && value.every(item => typeof item === 'string' && item.trim()));
432
+ if (!(typeof answer === 'string' && answer.trim()) && !validStructured)
433
+ throw new ServiceError('INVALID_REQUEST', 'Question requires nonempty text or structured answers');
434
+ }
435
+ }
436
+ this.db.prepare("UPDATE interactions SET status='answered',version=version+1,answer=? WHERE id=? AND status='pending' AND version=?").run(canonicalJson(request.answer), interaction.id, request.expectedVersion);
437
+ const changed = this.interaction(interaction.id);
438
+ this.event(run, changed.version, 'interaction.changed', changed.id, json(changed));
439
+ this.record(run.id, 'interaction.reply', { interactionId: changed.id, sessionId });
440
+ // Remain waiting/occupied until the original responder acknowledges delivery.
441
+ return json(changed);
442
+ });
443
+ });
444
+ }
445
+ /** Caller must first establish previous service ownership is stale (not just a PID check). */
446
+ recover(epoch) {
447
+ if (!epoch)
448
+ throw new ServiceError('INVALID_REQUEST', 'Recovery epoch required');
449
+ return atomic(this.db, () => {
450
+ for (const run of this.list()) {
451
+ if (isTerminalRunStatus(run.status) || ['queued', 'accepted', 'paused_restore'].includes(run.status))
452
+ continue;
453
+ const runtime = this.runtime(run.id);
454
+ if (runtime.epoch === epoch)
455
+ continue;
456
+ this.db.prepare('UPDATE run_runtime SET epoch=?,revision=revision+1,details=? WHERE run_id=?').run(epoch, canonicalJson({ ...runtime.details, previousEpoch: runtime.epoch, recoveryStatus: run.status }), run.id);
457
+ this.expireInteractions(run.id);
458
+ if (run.status === 'preparing' && runtime.details.backendLaunchAttempted !== true)
459
+ this.transition(run.id, this.runtime(run.id), 'interrupted', { interruptionReason: 'Recovered before backend launch boundary' });
460
+ else if (run.status === 'agent_completed')
461
+ this.transition(run.id, this.runtime(run.id), 'finalizing');
462
+ else if (run.status === 'finalizing')
463
+ this.event(this.get(run.id), runtime.revision + 1);
464
+ else if (run.status === 'reconciling')
465
+ this.event(this.get(run.id), runtime.revision + 1);
466
+ else
467
+ this.transition(run.id, this.runtime(run.id), 'reconciling');
468
+ }
469
+ return this.list().filter(run => !isTerminalRunStatus(run.status));
470
+ });
471
+ }
472
+ /** A new service instance must not replay or retain ownership of old work. */
473
+ abandonOnRestart(epoch) {
474
+ if (!epoch)
475
+ throw new ServiceError('INVALID_REQUEST', 'Recovery epoch required');
476
+ return atomic(this.db, () => {
477
+ const changed = [];
478
+ for (const run of this.list()) {
479
+ if (isTerminalRunStatus(run.status))
480
+ continue;
481
+ const runtime = this.runtime(run.id);
482
+ const status = ['accepted', 'queued', 'paused_restore'].includes(run.status) ? 'cancelled' : 'interrupted';
483
+ const revision = runtime.revision + 1;
484
+ const details = {
485
+ ...runtime.details, previousEpoch: runtime.epoch, recoveryStatus: run.status,
486
+ interruptionReason: 'Service restarted before Run completion',
487
+ completedAt: new Date(this.now()).toISOString(),
488
+ };
489
+ this.db.prepare('UPDATE run_runtime SET epoch=?,revision=?,details=? WHERE run_id=?').run(epoch, revision, canonicalJson(details), run.id);
490
+ this.db.prepare('UPDATE runs SET status=? WHERE id=?').run(status, run.id);
491
+ this.db.prepare('DELETE FROM occupancy WHERE run_id=?').run(run.id);
492
+ this.expireInteractions(run.id);
493
+ const current = this.get(run.id);
494
+ this.refreshNotification(current);
495
+ this.event(current, revision);
496
+ const paths = this.db.prepare('SELECT relative_path FROM project_file_outputs WHERE run_id=? ORDER BY output_key').all(run.id).map(row => String(row['relative_path']));
497
+ this.repo.appendEvent({ eventId: randomUUID(), type: 'project.files.changed', projectId: current.projectId, graphId: null, entityId: current.projectId, revision, occurredAt: new Date(this.now()).toISOString(), payload: { runId: run.id, paths, fullRecheck: true } });
498
+ this.record(run.id, 'state', { from: run.status, to: status, reason: 'service_restart' });
499
+ changed.push(current);
500
+ }
501
+ this.db.prepare('DELETE FROM occupancy WHERE run_id IN (SELECT id FROM runs WHERE status IN (' + terminalSql + '))').run();
502
+ return changed;
503
+ });
504
+ }
505
+ }
506
+ //# sourceMappingURL=runs.js.map
@@ -0,0 +1,24 @@
1
+ import type { InputSnapshot, Interaction, Json, Run } from "./vendor/protocol/index.js";
2
+ import type { BackendAdapter, BackendReply } from './backend/types.js';
3
+ import type { SchedulerBackend, BackendContext, ReconcileResult } from './scheduler.js';
4
+ import { Runs } from './runs.js';
5
+ import { ProjectProbe } from './project-probe.js';
6
+ import { type DataDirectories } from './directories.js';
7
+ import { RunContexts } from './run-context.js';
8
+ export declare function backendReply(interaction: Interaction, answer: Json): BackendReply;
9
+ /** Trusted host bridge. Never accepts browser paths or creates isolation evidence. */
10
+ export declare class RuntimeBackend implements SchedulerBackend {
11
+ readonly runs: Runs;
12
+ readonly dirs: DataDirectories;
13
+ readonly adapter: BackendAdapter;
14
+ readonly probe: ProjectProbe;
15
+ private readonly contexts;
16
+ readonly runContexts: RunContexts;
17
+ constructor(runs: Runs, dirs: DataDirectories, adapter: BackendAdapter, probe?: ProjectProbe);
18
+ private snapshot;
19
+ start(run: Run, snapshot: InputSnapshot, context: BackendContext): Promise<void>;
20
+ queryHistory(runId: string, query: string): string;
21
+ cancel(run: Run, context: BackendContext): Promise<'confirmed' | 'unknown'>;
22
+ reply(run: Run, interaction: Interaction, answer: Json, context: BackendContext): Promise<void>;
23
+ reconcile(run: Run, context: BackendContext): Promise<ReconcileResult>;
24
+ }