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,553 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { PROTOCOL_VERSION, SERVICE_VERSION, isTerminalRunStatus } from "./vendor/protocol/index.js";
4
+ import { BlobStore } from './blob-store.js';
5
+ import { Graphs } from './graphs.js';
6
+ import { GraphFiles } from './graph-files.js';
7
+ import { Resources } from './resources.js';
8
+ import { PluginRegistry } from './plugins.js';
9
+ import { InputPreparation } from './inputs.js';
10
+ import { ModelDefaultsStore, freezeModelSelection } from './backend/models.js';
11
+ import { CodexBackendAdapter } from './backend/adapter.js';
12
+ import { connect, listModels } from './backend/capabilities.js';
13
+ import { describeCodexFailure } from './backend/diagnostics.js';
14
+ import { StdioRpc } from './backend/stdio.js';
15
+ import { Runs } from './runs.js';
16
+ import { ExecutionChains } from './execution-chains.js';
17
+ import { History } from './history.js';
18
+ import { Scheduler } from './scheduler.js';
19
+ import { Publication } from './publication.js';
20
+ import { RuntimeBackend, backendReply } from './runtime-backend.js';
21
+ import { acquireProjectLock } from './project-lock.js';
22
+ import { ProjectProbe } from './project-probe.js';
23
+ import { ProjectFiles } from './project-files.js';
24
+ import { atomic } from './persistence/database.js';
25
+ import { canonicalJson } from './persistence/repositories.js';
26
+ import { ServiceError } from './errors.js';
27
+ import { ExecutionSettingsStore } from './execution-settings.js';
28
+ import { ImageProviders } from './image-providers.js';
29
+ import { ImageCredentials } from './image-credentials.js';
30
+ import { ApiImageAgent, RoutedImageBackend } from './api-image-agent.js';
31
+ import { planOpenAiImage, discoverOpenAiModels } from './openai-image-driver.js';
32
+ class RuntimeRuns extends Runs {
33
+ available;
34
+ constructor(db, serviceId, dependencies, available) {
35
+ super(db, serviceId, dependencies);
36
+ this.available = available;
37
+ }
38
+ claimNext(epoch, excludedProjects = [], eligible) {
39
+ // Capability discovery must never turn a persisted queue into failed/reconciling work.
40
+ return super.claimNext(epoch, excludedProjects, (run, snapshot) => this.available(run, snapshot) && (!eligible || eligible(run, snapshot)));
41
+ }
42
+ }
43
+ const capability = (status, reason, verifiedAt = null) => ({ status, reason, verifiedAt });
44
+ const resourceKey = (resource) => JSON.stringify([resource.resourceId, resource.version, resource.sha256, resource.representationVersion]);
45
+ export class WorkflowRuntime {
46
+ db;
47
+ serviceId;
48
+ directories;
49
+ options;
50
+ blobs;
51
+ graphs;
52
+ resources;
53
+ plugins;
54
+ inputs;
55
+ models;
56
+ runs;
57
+ history;
58
+ scheduler;
59
+ publication;
60
+ executionChains;
61
+ graphFiles;
62
+ executionSettings;
63
+ imageProviders;
64
+ imageCredentials;
65
+ probe = new ProjectProbe();
66
+ adapter;
67
+ availableModels;
68
+ timer;
69
+ starting;
70
+ catalogRefresh;
71
+ catalogRpc;
72
+ recovering = false;
73
+ closed = false;
74
+ closing = false;
75
+ stopping = false;
76
+ constructor(db, serviceId, directories, options = {}) {
77
+ this.db = db;
78
+ this.serviceId = serviceId;
79
+ this.directories = directories;
80
+ this.options = options;
81
+ this.availableModels = structuredClone(options.models ?? []);
82
+ this.blobs = new BlobStore(directories.blobs);
83
+ this.resources = new Resources(db, this.blobs);
84
+ this.graphs = new Graphs(db, serviceId);
85
+ this.plugins = new PluginRegistry(db);
86
+ this.graphs.history.backfillActiveGates();
87
+ this.graphFiles = new GraphFiles(db, directories.runs);
88
+ this.executionSettings = new ExecutionSettingsStore(db);
89
+ this.imageCredentials = new ImageCredentials(directories, serviceId);
90
+ this.imageProviders = new ImageProviders(db, this.imageCredentials);
91
+ this.inputs = new InputPreparation(db, { plugins: this.plugins, readRepresentation: resource => this.resources.readPreparedRepresentation(resource) });
92
+ this.models = new ModelDefaultsStore(db);
93
+ this.runs = new RuntimeRuns(db, serviceId, { freeze: () => { throw new ServiceError('INPUT_BLOCKED', 'Use prepareSubmission outside the auth transaction'); }, ...(!options.backend ? { validateReply: (interaction, answer) => { backendReply(interaction, answer); } } : {}) }, (_run, snapshot) => snapshot.imageRoute?.type === 'api'
94
+ ? this.imageProviders.credentialAvailable(snapshot.imageRoute.providerId, snapshot.imageRoute.credentialRevision)
95
+ : this.executionCapability.status === 'available');
96
+ this.history = new History(db);
97
+ this.publication = new Publication(db, directories, this.resources, this.graphs, this.runs);
98
+ this.executionChains = new ExecutionChains(this.runs, this.graphs, this.resources);
99
+ this.adapter = options.backend ? undefined : new CodexBackendAdapter(options.backendOptions);
100
+ const codexBackend = options.backend ?? new RuntimeBackend(this.runs, directories, this.adapter, this.probe);
101
+ const backend = new RoutedImageBackend(codexBackend, new ApiImageAgent(this.runs, this.resources, this.imageProviders, this.imageCredentials, directories), this.runs);
102
+ this.scheduler = new Scheduler(this.runs, backend, { publish: (run, token) => this.publication.publish(run, token), acquireProjectLock: (_run, path) => acquireProjectLock(path) });
103
+ }
104
+ get catalog() { return structuredClone(this.availableModels); }
105
+ /** Catalog reads require only the wire handshake, never login or execution self-checks. */
106
+ async refreshModels() {
107
+ if (this.closed || this.closing)
108
+ throw new ServiceError('MAINTENANCE', '运行时正在关闭。');
109
+ if (this.options.backend || this.options.enableBackend === false)
110
+ return;
111
+ if (this.catalogRefresh)
112
+ return this.catalogRefresh;
113
+ this.catalogRefresh = (async () => {
114
+ const options = this.options.backendOptions ?? {};
115
+ let rpc;
116
+ let stage = 'initialize';
117
+ try {
118
+ rpc = this.catalogRpc = new StdioRpc({ cwd: this.directories.root, ...(options.executable ? { executable: options.executable } : {}), ...(options.args ? { args: options.args } : {}), ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}), ...(options.initializeTimeoutMs !== undefined ? { initializeTimeoutMs: options.initializeTimeoutMs } : {}) });
119
+ await connect(rpc);
120
+ stage = 'model/list';
121
+ const models = await listModels(rpc);
122
+ if (this.closed || this.closing)
123
+ throw new ServiceError('MAINTENANCE', '运行时正在关闭。');
124
+ this.availableModels = models;
125
+ }
126
+ catch (error) {
127
+ this.availableModels = [];
128
+ if (error instanceof ServiceError)
129
+ throw error;
130
+ const failure = describeCodexFailure(error, rpc, stage);
131
+ throw new ServiceError('MODEL_UNAVAILABLE', failure.message, { details: failure.details, retryable: failure.retryable });
132
+ }
133
+ finally {
134
+ rpc?.close();
135
+ this.catalogRpc = undefined;
136
+ }
137
+ })();
138
+ try {
139
+ await this.catalogRefresh;
140
+ }
141
+ finally {
142
+ this.catalogRefresh = undefined;
143
+ }
144
+ }
145
+ get executionCapability() {
146
+ if (this.closed || this.closing)
147
+ return capability('unavailable', 'Runtime is closing; accepted tasks are retained');
148
+ if (this.options.backend)
149
+ return capability('available', 'Host-injected controlled backend; not evidence of real Codex execution');
150
+ if (this.options.enableBackend === false)
151
+ return capability('unavailable', 'Backend discovery explicitly disabled by host');
152
+ return capability('available', '执行由 Codex app-server 提供,沙盒权限使用工作空间设置;提交时检查模型,认证由 Codex 自身处理');
153
+ }
154
+ get imageGenerationCapability() {
155
+ const execution = this.executionCapability;
156
+ if (this.options.backend || execution.status !== 'available')
157
+ return this.imageProviders.list().some(item => item.enabled && item.credentialConfigured && item.models.length) ? capability('available', '已配置的 API 生图服务商可执行,Codex 图片路径不可用。') : execution;
158
+ return capability('available', '图片生成由 Codex 内置 imagegen 执行;成功后校验实际图片文件并发布到工作图。');
159
+ }
160
+ get capabilities() {
161
+ const local = capability('available', 'Durable local business implementation; authenticated HTTP integration required');
162
+ return { pairing: local, projects: local, graphs: local, assets: local, execution: this.executionCapability, imageGeneration: this.imageGenerationCapability, events: local, backups: capability('unknown', 'Backup availability is supplied by host operations integration'), projectFiles: local, projectFileReferences: local };
163
+ }
164
+ get serviceInfo() {
165
+ const modes = ['text', 'image', 'text_image'];
166
+ const codex = this.executionCapability;
167
+ const imageRoutes = [{ route: { type: 'codex' }, modes: { text: codex, image: codex, text_image: codex } }];
168
+ for (const provider of this.imageProviders.list())
169
+ for (const model of provider.models) {
170
+ const state = Object.fromEntries(modes.map(mode => [mode, !provider.enabled ? capability('unavailable', '服务商已停用。')
171
+ : !provider.credentialConfigured ? capability('unavailable', '本机凭据未配置或已撤销。')
172
+ : !model.modes.includes(mode) ? capability('unavailable', '模型未声明支持 ' + mode + ' 输入。')
173
+ : capability('available', '此模型已从服务商目录启用;实际生成结果由服务商响应确认。')]));
174
+ imageRoutes.push({ route: { type: 'api', providerId: provider.id, modelId: model.id }, modes: state });
175
+ }
176
+ return { serviceId: this.serviceId, version: SERVICE_VERSION, protocolVersion: PROTOCOL_VERSION, capabilities: this.capabilities, imageRoutes };
177
+ }
178
+ /** An explicitly initiated, potentially billable request. No catalog probe or fixture marks availability. */
179
+ async discoverImageModels(providerId, expectedRevision) {
180
+ await this.imageProviders.refreshCredentials();
181
+ const provider = this.imageProviders.list().find(item => item.id === providerId);
182
+ if (!provider || provider.revision !== expectedRevision)
183
+ throw new ServiceError('REVISION_CONFLICT', '服务商配置已变化,请刷新后重试。');
184
+ if (!provider.enabled)
185
+ throw new ServiceError('MODEL_UNAVAILABLE', '服务商已停用。');
186
+ if (!provider.credentialConfigured || !provider.credentialRevision)
187
+ throw new ServiceError('MODEL_UNAVAILABLE', '请先保存服务商 API Key。');
188
+ const credentialRevision = provider.credentialRevision;
189
+ const secret = await this.imageCredentials.read(provider.id, credentialRevision);
190
+ if (!secret)
191
+ throw new ServiceError('MODEL_UNAVAILABLE', '服务商 API Key 在本机不可用。');
192
+ const discovered = await discoverOpenAiModels(provider.endpoint, secret, AbortSignal.timeout(30000));
193
+ await this.imageProviders.refreshCredentials();
194
+ const current = this.imageProviders.list().find(item => item.id === provider.id);
195
+ if (!current || current.revision !== expectedRevision || current.credentialRevision !== credentialRevision || !current.credentialConfigured)
196
+ throw new ServiceError('REVISION_CONFLICT', '服务商或 API Key 在拉取模型期间发生变化。');
197
+ const selected = new Set(current.models.map(item => item.id));
198
+ return { models: discovered.map(item => ({ ...item, selected: selected.has(item.id) })), revision: current.revision };
199
+ }
200
+ requireAvailable(request) {
201
+ const state = request.kind === 'image_generation' && request.imageRoute?.type === 'api' ? this.imageGenerationCapability : this.executionCapability;
202
+ if (state.status !== 'available')
203
+ throw new ServiceError('NOT_IMPLEMENTED', state.reason);
204
+ }
205
+ /** Returns a request-scoped synchronous commit closure; no shared mutable cache. */
206
+ async prepareSubmission(input) {
207
+ if (input.kind !== 'execution')
208
+ return this.prepareSingleSubmission(input);
209
+ const graph = this.graphs.snapshot(input);
210
+ if (!graph.edges.some(e => e.kind === 'execution' && (e.sourceId === input.nodeId || e.targetId === input.nodeId)))
211
+ return this.prepareSingleSubmission(input, false);
212
+ const plan = this.executionChains.plan(graph, input.nodeId);
213
+ const outgoing = graph.edges.some(e => e.kind === 'execution' && e.sourceId === input.nodeId);
214
+ const commit = await this.prepareSingleSubmission(input);
215
+ const auto = !outgoing && !plan.requiresConfirmation && !plan.nodes.some(n => n.required && n.missingPrompt);
216
+ const members = auto ? await this.executionChains.prepareMembers(graph, plan, this.executionChains.selection(plan, plan.initialNodeIds), undefined, request => this.prepareSingleSubmission(request)) : undefined;
217
+ return principal => atomic(this.db, () => {
218
+ const run = this.runs.get(commit(principal).id);
219
+ if (run.status !== 'accepted' || run.executionStart === 'dependencies')
220
+ return run;
221
+ if (members)
222
+ return members(principal, run);
223
+ this.executionChains.setMode(run.id, outgoing ? 'manual' : 'confirm');
224
+ return this.runs.get(run.id);
225
+ });
226
+ }
227
+ async prepareExecutionStart(id, nodeIds, expectedExecutionRevision) {
228
+ const run = this.runs.get(id);
229
+ if (run.status !== 'accepted' || !['manual', 'confirm'].includes(run.executionStart ?? ''))
230
+ throw new ServiceError('CONFLICT', '任务已开始或不可启动。');
231
+ const graph = this.graphs.snapshot(run), plan = this.executionChains.plan(graph, run.nodeId, id);
232
+ if (plan.executionRevision !== expectedExecutionRevision)
233
+ throw new ServiceError('REVISION_CONFLICT', '工作图已变化,请重新选择执行深度。');
234
+ const members = await this.executionChains.prepareMembers(graph, plan, this.executionChains.selection(plan, nodeIds), id, request => this.prepareSingleSubmission(request));
235
+ return (principal, key) => this.runs.repo.idempotent(principal + ':run.start:' + id, key, { nodeIds, expectedExecutionRevision }, () => members(principal));
236
+ }
237
+ async prepareSingleSubmission(input, holdExecution = true) {
238
+ if (this.db.isTransaction)
239
+ throw new ServiceError('CONFLICT', 'Preflight must precede the auth transaction');
240
+ const request = structuredClone(input);
241
+ if (request.imageRoute && request.kind !== 'image_generation')
242
+ throw new ServiceError('INVALID_REQUEST', 'Only image tasks can select an image route');
243
+ let apiRoute;
244
+ if (request.kind === 'image_generation') {
245
+ if (request.imageRoute?.type === 'api') {
246
+ if (request.modelOverride)
247
+ throw new ServiceError('INVALID_REQUEST', 'API image route cannot use a Codex model');
248
+ const selected = request.imageRoute;
249
+ await this.imageProviders.refreshCredentials();
250
+ const provider = this.imageProviders.list().find(item => item.id === selected.providerId);
251
+ if (!provider || !provider.enabled)
252
+ throw new ServiceError('MODEL_UNAVAILABLE', '所选生图服务商不存在或已停用。');
253
+ const model = provider.models.find(item => item.id === selected.modelId);
254
+ if (!model)
255
+ throw new ServiceError('MODEL_UNAVAILABLE', '所选生图模型不存在。');
256
+ if (!provider.credentialConfigured || !provider.credentialRevision)
257
+ throw new ServiceError('MODEL_UNAVAILABLE', '服务商凭据未配置或本机不可用。');
258
+ apiRoute = { ...selected, configRevision: provider.revision, credentialRevision: provider.credentialRevision };
259
+ }
260
+ else {
261
+ if (request.imageRoute && request.imageRoute.type !== 'codex')
262
+ throw new ServiceError('INVALID_REQUEST', 'Invalid image route');
263
+ request.imageRoute = { type: 'codex', ...(request.imageRoute?.options ? { options: request.imageRoute.options } : {}) };
264
+ }
265
+ }
266
+ if (!apiRoute) {
267
+ this.requireAvailable(request);
268
+ await this.refreshModels();
269
+ }
270
+ const executionSettings = apiRoute ? null : this.executionSettings.get();
271
+ if (request.serviceId !== this.serviceId)
272
+ throw new ServiceError('SERVICE_MISMATCH', 'Wrong service');
273
+ const project = this.db.prepare('SELECT canonical_path,state FROM projects WHERE id=?').get(request.projectId);
274
+ if (!project)
275
+ throw new ServiceError('NOT_FOUND', 'Project not found');
276
+ if (project.state !== 'active')
277
+ throw new ServiceError('PROJECT_INACTIVE', 'Project is inactive');
278
+ const path = String(project.canonical_path);
279
+ const inspected = await this.probe.inspect([path]);
280
+ if (inspected(path).canonicalPath !== path)
281
+ throw new ServiceError('PROJECT_UNAVAILABLE', 'Stored project path no longer resolves to itself');
282
+ const versions = new Map();
283
+ // Discover only direct input resources using the same declarative mappings.
284
+ const discovery = new InputPreparation(this.db, { plugins: this.plugins, readRepresentation: resource => { versions.set(resourceKey(resource), resource); return { state: 'ready', resourceSha256: resource.sha256, representationVersion: resource.representationVersion, text: null, contentHash: null }; } });
285
+ const preview = discovery.inputPreview(request.graphId, request.nodeId, !!apiRoute);
286
+ if (preview.executionRevision !== request.expectedExecutionRevision)
287
+ throw new ServiceError('REVISION_CONFLICT', 'Graph changed before preflight');
288
+ const unsupportedProjectFile = preview.issues.find(item => item.reason.startsWith('API 生图不能读取项目文件引用'));
289
+ if (unsupportedProjectFile)
290
+ throw new ServiceError(unsupportedProjectFile.code, unsupportedProjectFile.reason);
291
+ if (preview.projectFiles.length) {
292
+ const states = await new ProjectFiles(this.db).stat(request.projectId, preview.projectFiles.map(item => item.relativePath));
293
+ const unavailable = states.find(item => item.state !== 'available');
294
+ if (unavailable)
295
+ throw new ServiceError(unavailable.state === 'missing' ? 'INPUT_BLOCKED' : 'PROJECT_UNAVAILABLE', '项目文件引用当前不可读:' + unavailable.path);
296
+ }
297
+ if (apiRoute) {
298
+ const hasImage = preview.resources.some(item => item.kind === 'image' && item.resource);
299
+ const mode = hasImage ? (preview.prompt.trim() ? 'text_image' : 'image') : 'text';
300
+ const frozen = this.imageProviders.frozen(apiRoute.providerId, apiRoute.configRevision, apiRoute.modelId);
301
+ if (!frozen.model.modes.includes(mode))
302
+ throw new ServiceError('INPUT_BLOCKED', '所选模型不支持 ' + mode + ' 输入。');
303
+ const options = apiRoute.options;
304
+ if ((options?.size && options.size !== 'auto' && !frozen.model.sizes.includes(options.size) && !frozen.model.sizes.includes('auto')) || (options?.quality && options.quality !== 'auto' && !frozen.model.qualities.includes(options.quality) && !frozen.model.qualities.includes('auto')) || (options?.outputFormat && !frozen.model.formats.includes(options.outputFormat)))
305
+ throw new ServiceError('INPUT_BLOCKED', '所选模型不支持图片参数。');
306
+ }
307
+ const prepared = new Map();
308
+ const fileIds = new Set(preview.resources.filter(item => item.kind === 'file').map(item => item.resource?.resourceId));
309
+ for (const [key, resource] of versions) {
310
+ let text = null;
311
+ if (resource.mime.startsWith('image/') || fileIds.has(resource.resourceId))
312
+ await this.resources.readContent(request, 'canvas', resource.resourceId, resource.version);
313
+ else {
314
+ const representation = await this.resources.representation(request, 'canvas', resource.resourceId, resource.version);
315
+ if (representation.state !== 'ready' || (resource.representationVersion !== null && representation.version !== resource.representationVersion))
316
+ throw new ServiceError('INPUT_BLOCKED', representation.reason ?? 'Required immutable representation unavailable');
317
+ text = representation.text;
318
+ }
319
+ prepared.set(key, Object.freeze({ state: 'ready', resourceSha256: resource.sha256, representationVersion: resource.representationVersion, text, contentHash: text === null ? null : createHash('sha256').update(text).digest('hex') }));
320
+ }
321
+ const scopedInputs = new InputPreparation(this.db, { plugins: this.plugins, readRepresentation: resource => prepared.get(resourceKey(resource)) ?? null });
322
+ const scopedPreview = scopedInputs.inputPreview(request.graphId, request.nodeId, !!apiRoute);
323
+ if (apiRoute) {
324
+ const references = [];
325
+ for (const envelope of scopedPreview.resources) {
326
+ if (envelope.kind !== 'image' || !envelope.resource)
327
+ continue;
328
+ const content = await this.resources.readContent(request, 'canvas', envelope.resource.resourceId, envelope.resource.version);
329
+ if (!['image/png', 'image/jpeg', 'image/webp'].includes(content.mime))
330
+ throw new ServiceError('INPUT_BLOCKED', '参考图格式不支持。');
331
+ references.push({ bytes: content.bytes, mime: content.mime });
332
+ }
333
+ const frozen = this.imageProviders.frozen(apiRoute.providerId, apiRoute.configRevision, apiRoute.modelId);
334
+ planOpenAiImage({ route: apiRoute, model: frozen.model, prompt: scopedPreview.prompt, references: scopedPreview.resources, images: references });
335
+ }
336
+ const scoped = new Runs(this.db, this.serviceId, {
337
+ validate: (_request, context) => {
338
+ if (apiRoute) {
339
+ const current = this.imageProviders.list().find(item => item.id === apiRoute.providerId);
340
+ if (!current?.enabled || current.revision !== apiRoute.configRevision || current.credentialRevision !== apiRoute.credentialRevision || !this.imageProviders.credentialAvailable(apiRoute.providerId, apiRoute.credentialRevision))
341
+ throw new ServiceError('REVISION_CONFLICT', '生图服务商或凭据配置已变化,请重新提交。');
342
+ }
343
+ else {
344
+ this.requireAvailable(request);
345
+ if (this.executionSettings.get().revision !== executionSettings.revision)
346
+ throw new ServiceError('REVISION_CONFLICT', '权限设置已变化,请重新提交任务。');
347
+ context.sandboxMode = executionSettings.sandboxMode;
348
+ try {
349
+ this.models.freeze(request.modelOverride, this.availableModels);
350
+ }
351
+ catch {
352
+ throw new ServiceError('MODEL_UNAVAILABLE', 'Selected model/effort is unavailable; choose explicitly');
353
+ }
354
+ }
355
+ if (context.canonicalPath !== path)
356
+ throw new ServiceError('REVISION_CONFLICT', 'Project path changed while preparing inputs');
357
+ },
358
+ freeze: () => scopedInputs.build(request.graphId, request.nodeId, apiRoute
359
+ ? { expectedExecutionRevision: request.expectedExecutionRevision, imageRoute: apiRoute }
360
+ : { expectedExecutionRevision: request.expectedExecutionRevision, model: this.models.freeze(request.modelOverride, this.availableModels), ...(request.kind === 'image_generation' ? { imageRoute: request.imageRoute?.type === 'codex' ? request.imageRoute : { type: 'codex' } } : {}) }),
361
+ });
362
+ return principal => scoped.submit(request, principal, request.kind === 'execution' && holdExecution);
363
+ }
364
+ async prepareContinuation(id) {
365
+ if (this.db.isTransaction)
366
+ throw new ServiceError('CONFLICT', 'Continuation preflight must precede auth transaction');
367
+ const run = this.runs.get(id);
368
+ const token = this.runs.runtime(id);
369
+ const replay = (work) => (principal, key) => this.runs.repo.idempotent(principal + ':run.continue:' + id, key, { id }, () => work());
370
+ if (run.status !== 'paused_restore')
371
+ return replay(() => { throw new ServiceError('CONFLICT', 'Only explicitly paused restored Runs can continue'); });
372
+ const snapshot = this.runs.snapshot(id);
373
+ const snapshotJson = canonicalJson(snapshot);
374
+ const api = snapshot.imageRoute?.type === 'api' ? snapshot.imageRoute : null;
375
+ if (api) {
376
+ await this.imageProviders.refreshCredentials();
377
+ if (!this.imageProviders.credentialAvailable(api.providerId, api.credentialRevision))
378
+ throw new ServiceError('MODEL_UNAVAILABLE', '冻结凭据已撤销或本机不可用。');
379
+ }
380
+ else {
381
+ this.requireAvailable({ kind: token.details.kind });
382
+ await this.refreshModels();
383
+ }
384
+ const project = this.db.prepare('SELECT state,canonical_path FROM projects WHERE id=?').get(run.projectId);
385
+ if (!project || project.state !== 'active')
386
+ throw new ServiceError('PROJECT_INACTIVE', 'Restored project is inactive');
387
+ const path = String(project.canonical_path);
388
+ if (path !== token.details.canonicalPath)
389
+ throw new ServiceError('PROJECT_UNAVAILABLE', 'Restored Run cannot be reassigned to another project path');
390
+ const inspect = await this.probe.inspect([path]);
391
+ if (inspect(path).canonicalPath !== path)
392
+ throw new ServiceError('PROJECT_UNAVAILABLE', 'Restored project path no longer resolves to itself');
393
+ if ((snapshot.projectFiles ?? []).length) {
394
+ const states = await new ProjectFiles(this.db).stat(run.projectId, snapshot.projectFiles.map(item => item.relativePath));
395
+ const unavailable = states.find(item => item.state !== 'available');
396
+ if (unavailable)
397
+ throw new ServiceError(unavailable.state === 'missing' ? 'INPUT_BLOCKED' : 'PROJECT_UNAVAILABLE', '项目文件引用当前不可读:' + unavailable.path);
398
+ }
399
+ const validateFrozen = () => {
400
+ const hasImage = snapshot.resources.some(item => item.kind === 'image' && item.resource) || (snapshot.projectFiles ?? []).some(item => item.kind === 'image');
401
+ if (snapshot.inputDigest !== run.inputDigest || (!snapshot.prompt.trim() && !(token.details.kind === 'image_generation' && hasImage)))
402
+ throw new ServiceError('INPUT_BLOCKED', 'Invalid restored input snapshot');
403
+ if (api) {
404
+ if (snapshot.model || snapshot.inputMode !== (snapshot.resources.some(item => item.kind === 'image' && item.resource) ? (snapshot.prompt.trim() ? 'text_image' : 'image') : 'text'))
405
+ throw new ServiceError('INPUT_BLOCKED', '冻结 API 输入模式无效。');
406
+ this.imageProviders.frozen(api.providerId, api.configRevision, api.modelId);
407
+ if (this.db.prepare('SELECT 1 FROM image_provider_revoked_credentials WHERE provider_id=? AND revision=?').get(api.providerId, api.credentialRevision))
408
+ throw new ServiceError('MODEL_UNAVAILABLE', '冻结凭据已撤销。');
409
+ return;
410
+ }
411
+ if (!snapshot.model)
412
+ throw new ServiceError('INPUT_BLOCKED', 'Codex model is missing from restored snapshot');
413
+ try {
414
+ const frozen = freezeModelSelection(snapshot.model, this.availableModels);
415
+ if (canonicalJson(frozen) !== canonicalJson(snapshot.model))
416
+ throw Error('Changed model defaults');
417
+ }
418
+ catch {
419
+ throw new ServiceError('MODEL_UNAVAILABLE', 'Frozen model/effort unavailable; no substitution is permitted');
420
+ }
421
+ };
422
+ validateFrozen();
423
+ for (const envelope of snapshot.resources) {
424
+ const resource = envelope.resource;
425
+ if (!resource)
426
+ continue;
427
+ const metadata = this.resources.readCanvasVersion(run, resource.resourceId, resource.version);
428
+ if (metadata.sha256 !== resource.sha256 || metadata.bytes !== resource.bytes || metadata.representationVersion !== resource.representationVersion)
429
+ throw new ServiceError('INPUT_BLOCKED', 'Frozen resource version changed');
430
+ await this.resources.readContent(run, 'canvas', resource.resourceId, resource.version);
431
+ }
432
+ return replay(() => {
433
+ if (this.runs.repo.setting('acceptingRuns') !== true)
434
+ throw new ServiceError('MAINTENANCE', 'Service is not accepting continuations');
435
+ if (api) {
436
+ if (!this.imageProviders.credentialAvailable(api.providerId, api.credentialRevision))
437
+ throw new ServiceError('MODEL_UNAVAILABLE', '冻结凭据不可用。');
438
+ }
439
+ else
440
+ this.requireAvailable({ kind: token.details.kind });
441
+ validateFrozen();
442
+ const current = this.runs.runtime(id);
443
+ const target = this.db.prepare('SELECT p.state,p.canonical_path,g.archived,g.trashed,n.deleted,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(run.projectId, run.graphId, run.nodeId);
444
+ if (!target || target.state !== 'active' || target.canonical_path !== path || target.archived || target.trashed || target.deleted || target.read_only)
445
+ throw new ServiceError('PROJECT_UNAVAILABLE', 'Restored target changed or is read-only');
446
+ if (current.epoch !== token.epoch || current.revision !== token.revision || canonicalJson(this.runs.snapshot(id)) !== snapshotJson)
447
+ throw new ServiceError('REVISION_CONFLICT', 'Restored Run changed during preflight');
448
+ const pendingChain = token.details.executionStart && !this.db.prepare('SELECT 1 FROM execution_input_snapshots WHERE run_id=?').get(id);
449
+ return this.runs.transition(id, token, pendingChain ? 'accepted' : 'queued');
450
+ });
451
+ }
452
+ get ready() { return this.starting ?? this.start(); }
453
+ active() { return this.runs.list().filter(run => !isTerminalRunStatus(run.status) && !['queued', 'accepted', 'paused_restore'].includes(run.status)); }
454
+ async pump() {
455
+ if (this.closed)
456
+ return;
457
+ if (!this.closing)
458
+ await this.executionChains.pump();
459
+ await this.scheduler.tick();
460
+ if (this.closing && !this.active().length)
461
+ this.finishClose();
462
+ }
463
+ async start() {
464
+ if (this.closed || this.closing)
465
+ throw new ServiceError('MAINTENANCE', 'Runtime closed');
466
+ if (this.starting)
467
+ return this.starting;
468
+ this.starting = (async () => {
469
+ if (this.options.enableBackend && !this.options.backend) {
470
+ await this.refreshModels().catch(() => { });
471
+ }
472
+ await this.imageProviders.refreshCredentials();
473
+ if (this.closed || this.closing)
474
+ return;
475
+ await this.graphFiles.drain();
476
+ if (!this.stopping)
477
+ atomic(this.db, () => this.db.prepare("UPDATE settings SET value='true' WHERE key='acceptingRuns'").run());
478
+ this.timer = setInterval(() => { void this.pump().catch(() => { }); }, 100);
479
+ this.timer.unref();
480
+ await this.pump();
481
+ })();
482
+ return this.starting;
483
+ }
484
+ /** Host must hold its service-root instance lock before transferring epochs. */
485
+ async recover() {
486
+ if (this.closed || this.closing)
487
+ throw new ServiceError('MAINTENANCE', 'Runtime closed');
488
+ this.recovering = true;
489
+ atomic(this.db, () => {
490
+ if (this.runs.repo.setting('backupRestorePending') === true) {
491
+ this.db.prepare("DELETE FROM settings WHERE key='backupRestorePending'").run();
492
+ this.runs.recover(this.scheduler.epoch);
493
+ }
494
+ else
495
+ this.runs.abandonOnRestart(this.scheduler.epoch);
496
+ });
497
+ await this.start();
498
+ }
499
+ /** Bind/startup failure only: never drain or mutate persisted service state. */
500
+ disposeUnstarted() {
501
+ if (this.starting || this.recovering || this.timer || !this.scheduler.isUnstarted || this.closing || this.stopping)
502
+ throw new ServiceError('CONFLICT', 'Runtime has already entered its lifecycle');
503
+ if (this.closed)
504
+ return;
505
+ this.closed = true;
506
+ this.executionChains.stop();
507
+ this.probe.close();
508
+ }
509
+ stopAccepting() { this.stopping = true; this.executionChains.stop(); atomic(this.db, () => this.db.prepare("UPDATE settings SET value='false' WHERE key='acceptingRuns'").run()); }
510
+ /** A host shutdown cancels durable queued work as well as active responders. */
511
+ stopRuns() {
512
+ this.stopAccepting();
513
+ for (const run of this.runs.list()) {
514
+ if (!isTerminalRunStatus(run.status))
515
+ this.runs.cancel(run.id, randomUUID(), 'host');
516
+ }
517
+ }
518
+ async drain(options = {}) {
519
+ this.stopAccepting();
520
+ while (this.active().length) {
521
+ if (options.interrupt)
522
+ for (const run of this.active())
523
+ this.runs.cancel(run.id, 'runtime-drain', 'host');
524
+ await this.pump();
525
+ await delay(25);
526
+ }
527
+ await this.scheduler.settled();
528
+ }
529
+ finishClose() {
530
+ if (this.closed)
531
+ return;
532
+ this.closed = true;
533
+ if (this.timer)
534
+ clearInterval(this.timer);
535
+ this.timer = undefined;
536
+ this.probe.close();
537
+ this.catalogRpc?.close();
538
+ // No adapter.close(): disconnecting unresolved child processes is not cancellation.
539
+ }
540
+ close() {
541
+ if (this.closed)
542
+ return;
543
+ this.stopAccepting();
544
+ this.closing = true;
545
+ if (!this.active().length)
546
+ this.finishClose();
547
+ else if (!this.timer) {
548
+ this.timer = setInterval(() => { void this.pump().catch(() => { }); }, 100);
549
+ this.timer.unref();
550
+ }
551
+ }
552
+ }
553
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,85 @@
1
+ import type { InputSnapshot, Interaction, Json, Run } from "./vendor/protocol/index.js";
2
+ import { Runs } from './runs.js';
3
+ import type { RunToken } from './runs.js';
4
+ export type BackendEvent = {
5
+ type: 'started';
6
+ identity?: {
7
+ [key: string]: Json;
8
+ };
9
+ } | {
10
+ type: 'progress';
11
+ payload: Json;
12
+ } | {
13
+ type: 'question' | 'approval';
14
+ id: string;
15
+ payload: Json;
16
+ } | {
17
+ type: 'completed';
18
+ result: Json;
19
+ } | {
20
+ type: 'failed';
21
+ error: string;
22
+ } | {
23
+ type: 'stopped';
24
+ };
25
+ export interface BackendContext {
26
+ epoch: string;
27
+ emit: (event: BackendEvent) => void;
28
+ }
29
+ export type ReconcileResult = 'unknown' | 'interrupted' | 'resumed';
30
+ /** A rejected transport call is NOT proof that a backend stopped. */
31
+ export interface SchedulerBackend {
32
+ start(run: Run, snapshot: InputSnapshot, context: BackendContext): Promise<void>;
33
+ cancel(run: Run, context: BackendContext): Promise<'confirmed' | 'unknown'>;
34
+ reply?(run: Run, interaction: Interaction, answer: Json, context: BackendContext): Promise<void>;
35
+ reconcile?(run: Run, context: BackendContext): Promise<ReconcileResult>;
36
+ }
37
+ export interface SchedulerOptions {
38
+ epoch?: string;
39
+ /** Injectable monotonic-enough wall clock for bounded publication retries. */
40
+ now?: () => number;
41
+ /** Must publish and transition finalizing -> succeeded in the SAME database txn.
42
+ * Replayed after crashes; manifest/hash/publication-key validation belongs here. */
43
+ publish?: (run: Run, token: RunToken) => Promise<void> | void;
44
+ /** Lifetime lock, released only after durable terminal state. Undefined means busy. */
45
+ acquireProjectLock?: (run: Run, canonicalPath: string) => Promise<(() => void) | undefined>;
46
+ }
47
+ export declare class Scheduler {
48
+ readonly runs: Runs;
49
+ readonly backend: SchedulerBackend;
50
+ readonly options: SchedulerOptions;
51
+ readonly epoch: string;
52
+ private readonly jobs;
53
+ private readonly releases;
54
+ private readonly lockAcquisitions;
55
+ private readonly pendingReplies;
56
+ private readonly replyBuffers;
57
+ private readonly contendedProjects;
58
+ private readonly publicationRetries;
59
+ private ticking;
60
+ private dispatched;
61
+ get isUnstarted(): boolean;
62
+ constructor(runs: Runs, backend: SchedulerBackend, options?: SchedulerOptions);
63
+ private context;
64
+ private owned;
65
+ private lockScope;
66
+ private activeProjectRuns;
67
+ private release;
68
+ private launch;
69
+ private deferPublication;
70
+ private uncertain;
71
+ /** Epoch fencing plus state checks discard late success after cancellation/recovery. */
72
+ private emit;
73
+ private start;
74
+ private dispatch;
75
+ private cancel;
76
+ private reconcile;
77
+ private ensureProjectLock;
78
+ private deliverReply;
79
+ /** Nonblocking dispatch. Host calls on events/timer; it never awaits a full turn. */
80
+ tick(): Promise<void>;
81
+ /** Only invoke after host verifies previous instance lost ownership. */
82
+ recover(): Promise<void>;
83
+ /** Test/controlled-adapter drain; not used by production request handlers. */
84
+ settled(): Promise<void>;
85
+ }