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.
- package/README.md +64 -0
- package/dist/api-image-agent.d.ts +32 -0
- package/dist/api-image-agent.js +132 -0
- package/dist/api-image-output.d.ts +8 -0
- package/dist/api-image-output.js +41 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.js +153 -0
- package/dist/auth.d.ts +58 -0
- package/dist/auth.js +273 -0
- package/dist/backend/adapter.d.ts +35 -0
- package/dist/backend/adapter.js +439 -0
- package/dist/backend/capabilities.d.ts +23 -0
- package/dist/backend/capabilities.js +75 -0
- package/dist/backend/diagnostics.d.ts +9 -0
- package/dist/backend/diagnostics.js +80 -0
- package/dist/backend/index.d.ts +6 -0
- package/dist/backend/index.js +7 -0
- package/dist/backend/models.d.ts +15 -0
- package/dist/backend/models.js +43 -0
- package/dist/backend/paths.d.ts +6 -0
- package/dist/backend/paths.js +69 -0
- package/dist/backend/stdio.d.ts +66 -0
- package/dist/backend/stdio.js +172 -0
- package/dist/backend/types.d.ts +90 -0
- package/dist/backend/types.js +11 -0
- package/dist/backup-api.d.ts +10 -0
- package/dist/backup-api.js +40 -0
- package/dist/blob-store.d.ts +40 -0
- package/dist/blob-store.js +218 -0
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.js +63 -0
- package/dist/cli-output.d.ts +4 -0
- package/dist/cli-output.js +130 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +284 -0
- package/dist/client-pairing.d.ts +8 -0
- package/dist/client-pairing.js +30 -0
- package/dist/directories.d.ts +14 -0
- package/dist/directories.js +34 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +7 -0
- package/dist/events.d.ts +22 -0
- package/dist/events.js +155 -0
- package/dist/execution-chains.d.ts +24 -0
- package/dist/execution-chains.js +231 -0
- package/dist/execution-settings.d.ts +10 -0
- package/dist/execution-settings.js +38 -0
- package/dist/graph-document-history.d.ts +43 -0
- package/dist/graph-document-history.js +275 -0
- package/dist/graph-files.d.ts +14 -0
- package/dist/graph-files.js +59 -0
- package/dist/graph-transfer.d.ts +53 -0
- package/dist/graph-transfer.js +501 -0
- package/dist/graphs.d.ts +68 -0
- package/dist/graphs.js +771 -0
- package/dist/history.d.ts +20 -0
- package/dist/history.js +61 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +42 -0
- package/dist/image-credentials.d.ts +11 -0
- package/dist/image-credentials.js +130 -0
- package/dist/image-providers.d.ts +51 -0
- package/dist/image-providers.js +271 -0
- package/dist/input-materialization.d.ts +17 -0
- package/dist/input-materialization.js +95 -0
- package/dist/inputs.d.ts +74 -0
- package/dist/inputs.js +301 -0
- package/dist/local-control.d.ts +13 -0
- package/dist/local-control.js +37 -0
- package/dist/network.d.ts +11 -0
- package/dist/network.js +48 -0
- package/dist/openai-image-driver.d.ts +42 -0
- package/dist/openai-image-driver.js +368 -0
- package/dist/operations/backups.d.ts +40 -0
- package/dist/operations/backups.js +391 -0
- package/dist/operations/capacity.d.ts +7 -0
- package/dist/operations/capacity.js +35 -0
- package/dist/operations/cli-context.d.ts +7 -0
- package/dist/operations/cli-context.js +58 -0
- package/dist/operations/daemon.d.ts +36 -0
- package/dist/operations/daemon.js +159 -0
- package/dist/operations/files.d.ts +12 -0
- package/dist/operations/files.js +84 -0
- package/dist/operations/lifecycle.d.ts +28 -0
- package/dist/operations/lifecycle.js +155 -0
- package/dist/operations/local-admin.d.ts +6 -0
- package/dist/operations/local-admin.js +38 -0
- package/dist/operations/logs.d.ts +16 -0
- package/dist/operations/logs.js +37 -0
- package/dist/operations/plugins.d.ts +10 -0
- package/dist/operations/plugins.js +11 -0
- package/dist/operations/snapshot-worker.d.ts +1 -0
- package/dist/operations/snapshot-worker.js +18 -0
- package/dist/operations/snapshot.d.ts +1 -0
- package/dist/operations/snapshot.js +14 -0
- package/dist/operations/update.d.ts +26 -0
- package/dist/operations/update.js +216 -0
- package/dist/persistence/asset-business-schema.d.ts +2 -0
- package/dist/persistence/asset-business-schema.js +36 -0
- package/dist/persistence/database.d.ts +7 -0
- package/dist/persistence/database.js +91 -0
- package/dist/persistence/plugin-schema.d.ts +9 -0
- package/dist/persistence/plugin-schema.js +38 -0
- package/dist/persistence/repositories.d.ts +20 -0
- package/dist/persistence/repositories.js +106 -0
- package/dist/persistence/resource-schema.d.ts +2 -0
- package/dist/persistence/resource-schema.js +44 -0
- package/dist/persistence/schema.d.ts +6 -0
- package/dist/persistence/schema.js +236 -0
- package/dist/persistence/workflow-schema.d.ts +2 -0
- package/dist/persistence/workflow-schema.js +19 -0
- package/dist/plugin-api.d.ts +10 -0
- package/dist/plugin-api.js +40 -0
- package/dist/plugin-management.d.ts +36 -0
- package/dist/plugin-management.js +112 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.js +239 -0
- package/dist/project-discovery.d.ts +3 -0
- package/dist/project-discovery.js +57 -0
- package/dist/project-files-api.d.ts +16 -0
- package/dist/project-files-api.js +243 -0
- package/dist/project-files.d.ts +97 -0
- package/dist/project-files.js +557 -0
- package/dist/project-lock.d.ts +4 -0
- package/dist/project-lock.js +35 -0
- package/dist/project-probe-worker.d.ts +1 -0
- package/dist/project-probe-worker.js +17 -0
- package/dist/project-probe.d.ts +22 -0
- package/dist/project-probe.js +67 -0
- package/dist/projects.d.ts +29 -0
- package/dist/projects.js +219 -0
- package/dist/publication.d.ts +31 -0
- package/dist/publication.js +642 -0
- package/dist/resource-api.d.ts +15 -0
- package/dist/resource-api.js +256 -0
- package/dist/resources-orphans.d.ts +41 -0
- package/dist/resources-orphans.js +235 -0
- package/dist/resources.d.ts +246 -0
- package/dist/resources.js +714 -0
- package/dist/run-api.d.ts +10 -0
- package/dist/run-api.js +308 -0
- package/dist/run-context.d.ts +54 -0
- package/dist/run-context.js +137 -0
- package/dist/runs.d.ts +82 -0
- package/dist/runs.js +506 -0
- package/dist/runtime-backend.d.ts +24 -0
- package/dist/runtime-backend.js +214 -0
- package/dist/runtime.d.ts +97 -0
- package/dist/runtime.js +553 -0
- package/dist/scheduler.d.ts +85 -0
- package/dist/scheduler.js +344 -0
- package/dist/service.d.ts +29 -0
- package/dist/service.js +515 -0
- package/dist/skills-api.d.ts +18 -0
- package/dist/skills-api.js +120 -0
- package/dist/storage-api.d.ts +21 -0
- package/dist/storage-api.js +92 -0
- package/dist/transfer-api.d.ts +10 -0
- package/dist/transfer-api.js +60 -0
- package/dist/vendor/protocol/contracts.typecheck.d.ts +1 -0
- package/dist/vendor/protocol/contracts.typecheck.js +35 -0
- package/dist/vendor/protocol/execution-chain.d.ts +6 -0
- package/dist/vendor/protocol/execution-chain.js +26 -0
- package/dist/vendor/protocol/graph-binary.d.ts +15 -0
- package/dist/vendor/protocol/graph-binary.js +121 -0
- package/dist/vendor/protocol/graph-title.d.ts +2 -0
- package/dist/vendor/protocol/graph-title.js +23 -0
- package/dist/vendor/protocol/index.d.ts +617 -0
- package/dist/vendor/protocol/index.js +20 -0
- package/dist/vendor/protocol/pairing.d.ts +29 -0
- package/dist/vendor/protocol/pairing.js +71 -0
- package/dist/vendor/protocol/preview.d.ts +2 -0
- package/dist/vendor/protocol/preview.js +9 -0
- package/dist/vendor/protocol/resource-mime.d.ts +2 -0
- package/dist/vendor/protocol/resource-mime.js +35 -0
- package/dist/video-thumbnail.d.ts +2 -0
- package/dist/video-thumbnail.js +34 -0
- package/package.json +9 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isTerminalRunStatus } from "./vendor/protocol/index.js";
|
|
3
|
+
import { ServiceError } from './errors.js';
|
|
4
|
+
import { Runs } from './runs.js';
|
|
5
|
+
export class Scheduler {
|
|
6
|
+
runs;
|
|
7
|
+
backend;
|
|
8
|
+
options;
|
|
9
|
+
epoch;
|
|
10
|
+
jobs = new Map();
|
|
11
|
+
releases = new Map();
|
|
12
|
+
lockAcquisitions = new Map();
|
|
13
|
+
pendingReplies = new Set();
|
|
14
|
+
replyBuffers = new Map();
|
|
15
|
+
contendedProjects = new Set();
|
|
16
|
+
publicationRetries = new Map();
|
|
17
|
+
ticking = false;
|
|
18
|
+
dispatched = false;
|
|
19
|
+
get isUnstarted() { return !this.dispatched && !this.ticking && !this.jobs.size && !this.releases.size && !this.lockAcquisitions.size; }
|
|
20
|
+
constructor(runs, backend, options = {}) {
|
|
21
|
+
this.runs = runs;
|
|
22
|
+
this.backend = backend;
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.epoch = options.epoch ?? randomUUID();
|
|
25
|
+
}
|
|
26
|
+
context(id) { return { epoch: this.epoch, emit: event => this.emit(id, event) }; }
|
|
27
|
+
owned(id) { return this.runs.db.prepare('SELECT epoch FROM run_runtime WHERE run_id=?').get(id)?.['epoch'] === this.epoch; }
|
|
28
|
+
lockScope(run) {
|
|
29
|
+
const runtime = this.runs.runtime(run.id);
|
|
30
|
+
if (runtime.details.kind !== 'execution' || !this.options.acquireProjectLock)
|
|
31
|
+
return undefined;
|
|
32
|
+
const canonicalPath = String(runtime.details.canonicalPath);
|
|
33
|
+
// The kernel lock excludes other runtimes, not independent runs in this
|
|
34
|
+
// scheduler. Concurrent runs (including different batches) share it.
|
|
35
|
+
return { key: canonicalPath, canonicalPath };
|
|
36
|
+
}
|
|
37
|
+
activeProjectRuns(canonicalPath) {
|
|
38
|
+
return Boolean(this.runs.db.prepare(`SELECT 1 FROM occupancy o JOIN run_runtime rt ON rt.run_id=o.run_id
|
|
39
|
+
WHERE rt.epoch=? AND json_extract(rt.details,'$.kind')='execution'
|
|
40
|
+
AND json_extract(rt.details,'$.canonicalPath')=? LIMIT 1`).get(this.epoch, canonicalPath));
|
|
41
|
+
}
|
|
42
|
+
release(id) {
|
|
43
|
+
const run = this.runs.get(id), scope = this.lockScope(run);
|
|
44
|
+
if (!scope || this.activeProjectRuns(scope.canonicalPath))
|
|
45
|
+
return;
|
|
46
|
+
const release = this.releases.get(scope.key);
|
|
47
|
+
if (release) {
|
|
48
|
+
release();
|
|
49
|
+
this.releases.delete(scope.key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
launch(key, work) {
|
|
53
|
+
if (this.jobs.has(key))
|
|
54
|
+
return;
|
|
55
|
+
const job = Promise.resolve().then(work).catch(error => {
|
|
56
|
+
const id = key.split(':')[1];
|
|
57
|
+
if (id && this.owned(id))
|
|
58
|
+
this.runs.record(id, 'scheduler.error', { message: error instanceof Error ? error.message : String(error) });
|
|
59
|
+
}).finally(() => { this.jobs.delete(key); });
|
|
60
|
+
this.jobs.set(key, job);
|
|
61
|
+
}
|
|
62
|
+
deferPublication(id, error) {
|
|
63
|
+
if (!this.owned(id))
|
|
64
|
+
return;
|
|
65
|
+
const now = this.options.now?.() ?? Date.now();
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
const previous = this.publicationRetries.get(id);
|
|
68
|
+
const attempts = Math.min((previous?.attempts ?? 0) + 1, 16);
|
|
69
|
+
const recordedAt = previous?.recordedAt ?? now;
|
|
70
|
+
if (!previous || previous.message !== message || now - previous.recordedAt >= 30_000) {
|
|
71
|
+
this.runs.record(id, 'scheduler.error', { message });
|
|
72
|
+
}
|
|
73
|
+
this.publicationRetries.set(id, {
|
|
74
|
+
attempts,
|
|
75
|
+
nextAt: now + Math.min(30_000, 250 * 2 ** (attempts - 1)),
|
|
76
|
+
message,
|
|
77
|
+
recordedAt: !previous || previous.message !== message || now - previous.recordedAt >= 30_000 ? now : recordedAt,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
uncertain(id, error) {
|
|
81
|
+
if (!this.owned(id))
|
|
82
|
+
return;
|
|
83
|
+
const run = this.runs.get(id);
|
|
84
|
+
if (isTerminalRunStatus(run.status) || ['reconciling', 'finalizing'].includes(run.status))
|
|
85
|
+
return;
|
|
86
|
+
this.runs.transition(id, this.runs.runtime(id), 'reconciling', { uncertainty: error instanceof Error ? error.message : String(error) });
|
|
87
|
+
}
|
|
88
|
+
/** Epoch fencing plus state checks discard late success after cancellation/recovery. */
|
|
89
|
+
emit(id, event) {
|
|
90
|
+
if (!this.owned(id))
|
|
91
|
+
return;
|
|
92
|
+
const run = this.runs.get(id);
|
|
93
|
+
const runtime = this.runs.runtime(id);
|
|
94
|
+
if (isTerminalRunStatus(run.status))
|
|
95
|
+
return;
|
|
96
|
+
if (runtime.details.cancelRequested === true && event.type !== 'stopped' && event.type !== 'progress')
|
|
97
|
+
return;
|
|
98
|
+
const buffer = this.replyBuffers.get(id);
|
|
99
|
+
if (buffer && ['started', 'completed'].includes(event.type)) {
|
|
100
|
+
buffer.push(event);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
switch (event.type) {
|
|
104
|
+
case 'started':
|
|
105
|
+
if (run.status === 'preparing')
|
|
106
|
+
this.runs.transition(id, runtime, 'running', { backendIdentity: event.identity ?? {} });
|
|
107
|
+
break;
|
|
108
|
+
case 'progress':
|
|
109
|
+
this.runs.record(id, 'backend.progress', event.payload);
|
|
110
|
+
break;
|
|
111
|
+
case 'question':
|
|
112
|
+
case 'approval':
|
|
113
|
+
if (['running', 'waiting_answer', 'waiting_approval'].includes(run.status))
|
|
114
|
+
this.runs.openInteraction(id, runtime, event.type === 'question' ? 'question' : 'approval', event.payload, event.id);
|
|
115
|
+
break;
|
|
116
|
+
case 'completed':
|
|
117
|
+
if (this.runs.activeInteraction(id))
|
|
118
|
+
this.runs.transition(id, runtime, 'reconciling', { uncertainty: 'Backend completed with unresolved independent actions', unresolvedCompletion: true });
|
|
119
|
+
else if (run.status === 'running')
|
|
120
|
+
this.runs.transition(id, runtime, 'agent_completed', { result: event.result });
|
|
121
|
+
break;
|
|
122
|
+
case 'failed':
|
|
123
|
+
if (['preparing', 'running', 'waiting_answer', 'waiting_approval'].includes(run.status))
|
|
124
|
+
this.runs.transition(id, runtime, 'failed', { error: event.error });
|
|
125
|
+
break;
|
|
126
|
+
case 'stopped':
|
|
127
|
+
if (run.status === 'cancelling')
|
|
128
|
+
this.runs.transition(id, runtime, 'cancelled');
|
|
129
|
+
else if (run.status === 'reconciling' && runtime.details.cancelRequested === true) {
|
|
130
|
+
this.runs.transition(id, runtime, 'cancelling');
|
|
131
|
+
this.runs.transition(id, this.runs.runtime(id), 'cancelled');
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (isTerminalRunStatus(this.runs.get(id).status))
|
|
136
|
+
this.release(id);
|
|
137
|
+
}
|
|
138
|
+
async start(run) {
|
|
139
|
+
try {
|
|
140
|
+
if (!await this.ensureProjectLock(run)) {
|
|
141
|
+
if (!this.owned(run.id))
|
|
142
|
+
return;
|
|
143
|
+
if (this.runs.get(run.id).status === 'cancelling')
|
|
144
|
+
this.emit(run.id, { type: 'stopped' });
|
|
145
|
+
else if (this.runs.get(run.id).status === 'preparing')
|
|
146
|
+
this.runs.requeueUnstarted(run.id, this.runs.runtime(run.id));
|
|
147
|
+
this.contendedProjects.add(run.projectId);
|
|
148
|
+
// Refill immediately even with capacity=1; retry this project next tick.
|
|
149
|
+
this.dispatch();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!this.owned(run.id) || this.runs.get(run.id).status !== 'preparing') {
|
|
153
|
+
if (this.owned(run.id) && this.runs.get(run.id).status === 'cancelling')
|
|
154
|
+
this.emit(run.id, { type: 'stopped' });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.runs.markBackendLaunching(run.id, this.runs.runtime(run.id));
|
|
158
|
+
await this.backend.start(this.runs.get(run.id), this.runs.snapshot(run.id), this.context(run.id));
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
this.uncertain(run.id, error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
dispatch() {
|
|
165
|
+
let claimed;
|
|
166
|
+
while ((claimed = this.runs.claimNext(this.epoch, [...this.contendedProjects]))) {
|
|
167
|
+
const run = claimed;
|
|
168
|
+
if (this.jobs.has('start:' + run.id)) {
|
|
169
|
+
this.runs.requeueUnstarted(run.id, this.runs.runtime(run.id));
|
|
170
|
+
this.contendedProjects.add(run.projectId);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
this.launch('start:' + run.id, () => this.start(run));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async cancel(run) {
|
|
177
|
+
// A start call may still be acquiring a lock or spawning. Do not confirm stop
|
|
178
|
+
// until that call settles, otherwise it could launch after capacity is freed.
|
|
179
|
+
const start = this.jobs.get('start:' + run.id);
|
|
180
|
+
if (start)
|
|
181
|
+
return;
|
|
182
|
+
if (this.runs.runtime(run.id).details.backendLaunchAttempted !== true) {
|
|
183
|
+
this.emit(run.id, { type: 'stopped' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
if (await this.backend.cancel(run, this.context(run.id)) === 'confirmed')
|
|
188
|
+
this.emit(run.id, { type: 'stopped' });
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
this.uncertain(run.id, error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async reconcile(run) {
|
|
195
|
+
try {
|
|
196
|
+
if (!await this.ensureProjectLock(run))
|
|
197
|
+
return;
|
|
198
|
+
if (!this.backend.reconcile)
|
|
199
|
+
return;
|
|
200
|
+
const result = await this.backend.reconcile(run, this.context(run.id));
|
|
201
|
+
if (!this.owned(run.id) || this.runs.get(run.id).status !== 'reconciling')
|
|
202
|
+
return;
|
|
203
|
+
const runtime = this.runs.runtime(run.id);
|
|
204
|
+
if (result === 'interrupted') {
|
|
205
|
+
if (runtime.details.cancelRequested === true) {
|
|
206
|
+
this.runs.transition(run.id, runtime, 'cancelling');
|
|
207
|
+
this.emit(run.id, { type: 'stopped' });
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
this.runs.transition(run.id, runtime, 'interrupted');
|
|
211
|
+
this.release(run.id);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (result === 'resumed' && runtime.details.unresolvedCompletion !== true)
|
|
215
|
+
this.runs.transition(run.id, runtime, runtime.details.cancelRequested === true ? 'cancelling' : 'running');
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
this.runs.record(run.id, 'reconcile.error', { message: String(error) });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async ensureProjectLock(run) {
|
|
222
|
+
if (!this.owned(run.id) || isTerminalRunStatus(this.runs.get(run.id).status))
|
|
223
|
+
return false;
|
|
224
|
+
const scope = this.lockScope(run);
|
|
225
|
+
if (!scope || this.releases.has(scope.key))
|
|
226
|
+
return true;
|
|
227
|
+
let acquisition = this.lockAcquisitions.get(scope.key);
|
|
228
|
+
if (!acquisition) {
|
|
229
|
+
acquisition = (async () => {
|
|
230
|
+
const release = await this.options.acquireProjectLock(run, scope.canonicalPath);
|
|
231
|
+
if (!release)
|
|
232
|
+
return false;
|
|
233
|
+
const needed = this.activeProjectRuns(scope.canonicalPath);
|
|
234
|
+
if (!needed) {
|
|
235
|
+
release();
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
this.releases.set(scope.key, release);
|
|
239
|
+
return true;
|
|
240
|
+
})().finally(() => { this.lockAcquisitions.delete(scope.key); });
|
|
241
|
+
this.lockAcquisitions.set(scope.key, acquisition);
|
|
242
|
+
}
|
|
243
|
+
return acquisition;
|
|
244
|
+
}
|
|
245
|
+
async deliverReply(run, interaction, answer) {
|
|
246
|
+
if (!this.backend.reply || this.pendingReplies.has(interaction.id))
|
|
247
|
+
return;
|
|
248
|
+
this.pendingReplies.add(interaction.id);
|
|
249
|
+
const token = this.runs.runtime(run.id);
|
|
250
|
+
// Some adapters synchronously emit the next action before their reply promise
|
|
251
|
+
// resolves. Buffer those callbacks until delivery is acknowledged, rather than
|
|
252
|
+
// dropping completion/questions while the durable Run still says waiting.
|
|
253
|
+
const events = [];
|
|
254
|
+
this.replyBuffers.set(run.id, events);
|
|
255
|
+
try {
|
|
256
|
+
await this.backend.reply(run, interaction, answer, this.context(run.id));
|
|
257
|
+
if (!this.owned(run.id))
|
|
258
|
+
return;
|
|
259
|
+
const status = this.runs.get(run.id).status;
|
|
260
|
+
if (['waiting_answer', 'waiting_approval'].includes(status))
|
|
261
|
+
this.runs.acknowledgeReply(run.id, token.epoch, interaction.id);
|
|
262
|
+
this.replyBuffers.delete(run.id);
|
|
263
|
+
for (const event of events)
|
|
264
|
+
this.emit(run.id, event);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
this.uncertain(run.id, error);
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
this.replyBuffers.delete(run.id);
|
|
271
|
+
this.pendingReplies.delete(interaction.id);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** Nonblocking dispatch. Host calls on events/timer; it never awaits a full turn. */
|
|
275
|
+
async tick() {
|
|
276
|
+
if (this.runs.db.isTransaction)
|
|
277
|
+
throw new ServiceError('CONFLICT', 'Scheduler cannot dispatch inside a database transaction');
|
|
278
|
+
this.dispatched = true;
|
|
279
|
+
if (this.ticking)
|
|
280
|
+
return;
|
|
281
|
+
this.ticking = true;
|
|
282
|
+
this.contendedProjects.clear();
|
|
283
|
+
try {
|
|
284
|
+
for (const run of this.runs.list()) {
|
|
285
|
+
if (!this.owned(run.id))
|
|
286
|
+
continue;
|
|
287
|
+
if (isTerminalRunStatus(run.status)) {
|
|
288
|
+
this.publicationRetries.delete(run.id);
|
|
289
|
+
this.release(run.id);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (run.status === 'cancelling')
|
|
293
|
+
this.launch('cancel:' + run.id, () => this.cancel(run));
|
|
294
|
+
if (run.status === 'reconciling')
|
|
295
|
+
this.launch('reconcile:' + run.id, () => this.reconcile(run));
|
|
296
|
+
if (run.status === 'agent_completed')
|
|
297
|
+
this.runs.transition(run.id, this.runs.runtime(run.id), 'finalizing');
|
|
298
|
+
const publicationRetry = this.publicationRetries.get(run.id);
|
|
299
|
+
const publicationReady = !publicationRetry || (this.options.now?.() ?? Date.now()) >= publicationRetry.nextAt;
|
|
300
|
+
if (this.runs.get(run.id).status === 'finalizing' && this.options.publish && publicationReady)
|
|
301
|
+
this.launch('publish:' + run.id, async () => {
|
|
302
|
+
if (!await this.ensureProjectLock(run))
|
|
303
|
+
return;
|
|
304
|
+
if (!this.owned(run.id) || this.runs.get(run.id).status !== 'finalizing')
|
|
305
|
+
return;
|
|
306
|
+
try {
|
|
307
|
+
await this.options.publish(this.runs.get(run.id), this.runs.runtime(run.id));
|
|
308
|
+
this.publicationRetries.delete(run.id);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
// Invalid output is permanent. Transport/filesystem faults remain
|
|
312
|
+
// finalizing for manifest-based recovery instead of backend reruns.
|
|
313
|
+
const permanent = (error instanceof ServiceError && ['INVALID_REQUEST', 'INPUT_BLOCKED', 'UNSUPPORTED_MEDIA_TYPE', 'PAYLOAD_TOO_LARGE', 'NOT_FOUND', 'PROJECT_INACTIVE'].includes(error.code)) || error instanceof SyntaxError || (error instanceof Error && ['ENOENT', 'ELOOP', 'ENOTDIR'].includes(String(error.code)));
|
|
314
|
+
if (permanent && this.owned(run.id) && this.runs.get(run.id).status === 'finalizing') {
|
|
315
|
+
this.publicationRetries.delete(run.id);
|
|
316
|
+
this.runs.transition(run.id, this.runs.runtime(run.id), 'failed', { error: error instanceof Error ? error.message : 'Invalid output', failurePhase: 'publication' });
|
|
317
|
+
}
|
|
318
|
+
else
|
|
319
|
+
this.deferPublication(run.id, error);
|
|
320
|
+
}
|
|
321
|
+
if (isTerminalRunStatus(this.runs.get(run.id).status))
|
|
322
|
+
this.release(run.id);
|
|
323
|
+
});
|
|
324
|
+
if (['waiting_answer', 'waiting_approval'].includes(run.status)) {
|
|
325
|
+
const interaction = this.runs.activeInteraction(run.id);
|
|
326
|
+
if (interaction?.status === 'answered') {
|
|
327
|
+
const row = this.runs.db.prepare('SELECT answer FROM interactions WHERE id=?').get(interaction.id);
|
|
328
|
+
this.launch('reply:' + run.id, () => this.deliverReply(run, interaction, JSON.parse(String(row.answer))));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
this.dispatch();
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
this.ticking = false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Only invoke after host verifies previous instance lost ownership. */
|
|
339
|
+
async recover() { this.runs.recover(this.epoch); await this.tick(); }
|
|
340
|
+
/** Test/controlled-adapter drain; not used by production request handlers. */
|
|
341
|
+
async settled() { while (this.jobs.size)
|
|
342
|
+
await Promise.all([...this.jobs.values()]); }
|
|
343
|
+
}
|
|
344
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ServiceInfo } from "./vendor/protocol/index.js";
|
|
2
|
+
import { Auth } from "./auth.js";
|
|
3
|
+
import { Projects } from "./projects.js";
|
|
4
|
+
import { ProjectProbe } from "./project-probe.js";
|
|
5
|
+
import { BusinessApi } from "./api.js";
|
|
6
|
+
import { Events } from "./events.js";
|
|
7
|
+
import { WorkflowRuntime, type WorkflowRuntimeOptions } from "./runtime.js";
|
|
8
|
+
export declare function createService(dataDir?: string, options?: {
|
|
9
|
+
now?: () => number;
|
|
10
|
+
codexHome?: string;
|
|
11
|
+
projectProbe?: ProjectProbe;
|
|
12
|
+
runtime?: WorkflowRuntimeOptions;
|
|
13
|
+
npmInstallation?: () => Promise<boolean>;
|
|
14
|
+
latestNpmVersion?: () => Promise<string>;
|
|
15
|
+
updateRuntime?: (onExit: () => void) => Promise<void>;
|
|
16
|
+
}): {
|
|
17
|
+
server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
|
|
18
|
+
db: import("node:sqlite").DatabaseSync;
|
|
19
|
+
directories: import("./directories.js").DataDirectories;
|
|
20
|
+
info: ServiceInfo;
|
|
21
|
+
auth: Auth;
|
|
22
|
+
projects: Projects;
|
|
23
|
+
api: BusinessApi;
|
|
24
|
+
events: Events;
|
|
25
|
+
runtime: WorkflowRuntime;
|
|
26
|
+
instanceId: `${string}-${string}-${string}-${string}-${string}`;
|
|
27
|
+
close: () => Promise<void>;
|
|
28
|
+
abortStartup: () => Promise<void>;
|
|
29
|
+
};
|