maka-agent 0.2.0-dev.31.20260913 → 0.2.0-dev.32.20260914

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 (71) hide show
  1. package/dist/pi-transcript.js +3 -4
  2. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  3. package/node_modules/@maka/core/dist/agent-graph-schedule.js +15 -4
  4. package/node_modules/@maka/core/dist/executor-id.js +22 -0
  5. package/node_modules/@maka/core/dist/external-session.js +18 -0
  6. package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -13
  7. package/node_modules/@maka/core/dist/runtime-event.js +19 -3
  8. package/node_modules/@maka/core/dist/session-send-projection.js +6 -3
  9. package/node_modules/@maka/core/dist/session.js +8 -2
  10. package/node_modules/@maka/core/dist/shell-run-result.js +1 -0
  11. package/node_modules/@maka/core/dist/shell-run.js +4 -0
  12. package/node_modules/@maka/core/dist/work-board.js +94 -1
  13. package/node_modules/@maka/core/package.json +1 -0
  14. package/node_modules/@maka/eval/dist/fleet-simulation.js +270 -0
  15. package/node_modules/@maka/eval/dist/fleet-store.js +243 -0
  16. package/node_modules/@maka/eval/dist/fleet-worker.js +125 -0
  17. package/node_modules/@maka/eval/dist/fleet.js +329 -0
  18. package/node_modules/@maka/eval/dist/index.js +3 -0
  19. package/node_modules/@maka/runtime/dist/agent-run.js +2 -17
  20. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +6 -2
  21. package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +36 -24
  22. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +257 -330
  23. package/node_modules/@maka/runtime/dist/background-task-health-tool.js +104 -0
  24. package/node_modules/@maka/runtime/dist/history-compaction.js +3 -1
  25. package/node_modules/@maka/runtime/dist/local-web-fetch.js +1 -1
  26. package/node_modules/@maka/runtime/dist/model-adapter.js +62 -53
  27. package/node_modules/@maka/runtime/dist/model-history.js +1 -5
  28. package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +311 -0
  29. package/node_modules/@maka/runtime/dist/plugin-executor-service.js +344 -0
  30. package/node_modules/@maka/runtime/dist/provider-error-classification.js +7 -1
  31. package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +35 -8
  32. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +7 -1
  33. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +1 -0
  34. package/node_modules/@maka/runtime/dist/runtime-invocation-route.js +49 -0
  35. package/node_modules/@maka/runtime/dist/runtime-kernel.js +72 -150
  36. package/node_modules/@maka/runtime/dist/runtime-read-model.js +0 -2
  37. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -0
  38. package/node_modules/@maka/runtime/dist/session-manager.js +100 -58
  39. package/node_modules/@maka/runtime/dist/shell-run-manager.js +9 -3
  40. package/node_modules/@maka/runtime/dist/shell-run-tool-result.js +1 -0
  41. package/node_modules/@maka/runtime/dist/stream-graph-schedule-reconcile.js +1 -0
  42. package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +26 -4
  43. package/node_modules/@maka/runtime/dist/subagent-tools.js +7 -0
  44. package/node_modules/@maka/runtime/dist/tool-runtime.js +45 -466
  45. package/node_modules/@maka/runtime/package.json +3 -0
  46. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +7 -2
  47. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +1 -0
  48. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +24 -3
  49. package/node_modules/@maka/runtime-host/dist/protocol/index.js +4 -1
  50. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +42 -5
  51. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +31 -3
  52. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +6 -0
  53. package/node_modules/@maka/runtime-host/dist/server/child-agent-composition.js +1 -0
  54. package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +1 -22
  55. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +54 -8
  56. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +14 -1
  57. package/node_modules/@maka/runtime-host/dist/server/host-session-availability.js +10 -2
  58. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
  59. package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
  60. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +56 -16
  61. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +4 -3
  62. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +4 -0
  63. package/node_modules/@maka/runtime-host/dist/server/web-fetch-tool.js +37 -1
  64. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +450 -234
  65. package/node_modules/@maka/storage/dist/claude-code-transcript-lineage.js +106 -77
  66. package/node_modules/@maka/storage/dist/legacy-run-header.js +2 -2
  67. package/node_modules/@maka/storage/dist/model-call-usage-sql.js +1 -1
  68. package/node_modules/@maka/storage/dist/session-store.js +12 -2
  69. package/node_modules/@maka/storage/dist/sqlite-long-term-memory-store.js +86 -12
  70. package/node_modules/@maka/storage/dist/work-board-store.js +40 -1
  71. package/package.json +1 -1
@@ -0,0 +1,270 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { pathToFileURL } from 'node:url';
20
+ import { summarizeFleet } from './fleet.js';
21
+ import { FleetCoordinator, MemoryFleetPersistence } from './fleet-store.js';
22
+ import { FleetWorker } from './fleet-worker.js';
23
+ /** Replayable virtual-time fault simulation, exercising the real coordinator and worker loop. */
24
+ export async function simulateFleet(seed) {
25
+ if (!Number.isSafeInteger(seed) || seed < 0 || seed > 0xffffffff)
26
+ throw new Error('seed must be uint32');
27
+ let randomState = seed;
28
+ const random = () => {
29
+ randomState = (Math.imul(1664525, randomState) + 1013904223) >>> 0;
30
+ return randomState;
31
+ };
32
+ const spec = {
33
+ schemaVersion: 'maka.eval.v1',
34
+ id: 'fleet-simulation',
35
+ benchmark: { id: 'mock-benchmark', version: '1', config: {} },
36
+ executor: { kind: 'mock', config: {} },
37
+ execution: { maxConcurrentTaskGroups: 3 },
38
+ subjects: ['a', 'b', 'c'].map((id) => ({ id, kind: 'external', credentials: [], config: {} })),
39
+ tasks: Array.from({ length: 20 }, (_, index) => ({
40
+ id: `task-${index}`,
41
+ input: 'mock',
42
+ config: {},
43
+ })),
44
+ repetitions: 2,
45
+ budget: {},
46
+ verifier: {},
47
+ };
48
+ const policy = {
49
+ runId: `seed-${seed}`,
50
+ environmentId: 'mock-manifest-v1',
51
+ groupCpus: 6,
52
+ groupMemoryMb: 1024,
53
+ leaseMs: 12,
54
+ recoveryGraceMs: 15,
55
+ retryBackoffMs: 2,
56
+ maxAttemptsPerCell: 20,
57
+ };
58
+ let now = 0;
59
+ const persistence = new MemoryFleetPersistence();
60
+ let coordinator = await FleetCoordinator.open({ persistence, spec, policy, now: () => now });
61
+ const trace = [];
62
+ let order = 0;
63
+ const events = [];
64
+ let crashed = false;
65
+ const lost = new Set();
66
+ const committed = new Map();
67
+ const workers = Array.from({ length: 3 }, (_, index) => {
68
+ const id = `vm-${index}/boot-1`;
69
+ const disconnected = () => (now >= 55 && now < 100) || (index === 0 && now >= 8 && now < 38);
70
+ const transport = {
71
+ async command(command) {
72
+ if (disconnected())
73
+ throw new SimulatedLinkError();
74
+ const roll = random();
75
+ if (now < 240 && roll % 31 === 0) {
76
+ trace.push(`${now} ${id} ${command.kind} dropped-before`);
77
+ throw new SimulatedLinkError();
78
+ }
79
+ const reply = await coordinator.command(command);
80
+ const once = `${id}/${command.kind}`;
81
+ // Guaranteed lost claim/report acknowledgements, plus seeded background faults.
82
+ if (now < 240 &&
83
+ ((['claim', 'report'].includes(command.kind) && !lost.has(once)) || roll % 37 === 0)) {
84
+ lost.add(once);
85
+ trace.push(`${now} ${id} ${command.kind} dropped-after`);
86
+ throw new SimulatedLinkError();
87
+ }
88
+ return reply;
89
+ },
90
+ async putArtifact(bytes) {
91
+ if (disconnected())
92
+ throw new SimulatedLinkError();
93
+ return coordinator.putArtifact(bytes);
94
+ },
95
+ };
96
+ return new FleetWorker({
97
+ id,
98
+ environmentId: policy.environmentId,
99
+ cpus: 6,
100
+ memoryMb: 1024,
101
+ groupSlots: 1,
102
+ }, transport, (work, emit) => new Promise((resolve) => {
103
+ trace.push(`${now} ${id} start ${work.assignment.id} cells=${work.cells.length}`);
104
+ let remaining = work.cells.length;
105
+ if (!remaining) {
106
+ resolve();
107
+ return;
108
+ }
109
+ for (const cell of work.cells) {
110
+ const delay = 2 + (random() % 19);
111
+ events.push({
112
+ at: now + delay,
113
+ order: order++,
114
+ action: () => {
115
+ if (index === 2 && crashed)
116
+ return;
117
+ const output = simulationOutput();
118
+ // Valid outcomes with absent metering must remain selectable.
119
+ if (random() % 5 === 0) {
120
+ output.report.usage = 'missing';
121
+ output.report.result = {
122
+ ...output.report.result,
123
+ usage: null,
124
+ costUsd: null,
125
+ status: 'indeterminate',
126
+ };
127
+ }
128
+ emit(cell.attemptId, output);
129
+ trace.push(`${now} ${id} completed ${cell.attemptId}`);
130
+ if (--remaining === 0)
131
+ resolve();
132
+ },
133
+ });
134
+ }
135
+ }));
136
+ });
137
+ for (now = 0; now < 3000; now++) {
138
+ if (now === 20) {
139
+ crashed = true;
140
+ trace.push(`${now} vm-2 process-crash`);
141
+ }
142
+ if (now === 55) {
143
+ await coordinator.command({ kind: 'pause' });
144
+ trace.push(`${now} coordinator-offline`);
145
+ }
146
+ if (now === 100) {
147
+ await coordinator.close();
148
+ coordinator = await FleetCoordinator.open({ persistence, spec, policy, now: () => now });
149
+ trace.push(`${now} coordinator-restarted`);
150
+ }
151
+ events.sort((a, b) => a.at - b.at || a.order - b.order);
152
+ while (events[0]?.at <= now)
153
+ events.shift().action();
154
+ // Drain the finite execute/catch/finally chain, without timers or real sleeps.
155
+ for (let i = 0; i < 8; i++)
156
+ await Promise.resolve();
157
+ for (const [index, worker] of workers.entries()) {
158
+ if (index === 2 && crashed)
159
+ continue;
160
+ try {
161
+ await worker.poll();
162
+ }
163
+ catch (error) {
164
+ if (!(error instanceof SimulatedLinkError))
165
+ throw error;
166
+ }
167
+ }
168
+ await coordinator.command({ kind: 'tick' });
169
+ const state = await coordinator.snapshot();
170
+ try {
171
+ assertFleetInvariants(state);
172
+ for (const [id, evidence] of committed) {
173
+ const attempt = state.attempts.find((a) => a.id === id);
174
+ if (!attempt || JSON.stringify(attempt) !== evidence)
175
+ throw new Error('committed result changed or disappeared');
176
+ }
177
+ for (const attempt of state.attempts) {
178
+ if (attempt.disposition === 'selected')
179
+ committed.set(attempt.id, JSON.stringify(attempt));
180
+ }
181
+ }
182
+ catch (error) {
183
+ throw new Error(`simulation invariant failed: seed=${seed} tick=${now}\n${trace.slice(-30).join('\n')}`, { cause: error });
184
+ }
185
+ if (summarizeFleet(state).settled)
186
+ return { seed, elapsedTicks: now, trace, state, summary: summarizeFleet(state) };
187
+ }
188
+ throw new Error(`simulation did not settle: seed=${seed}\n${trace.slice(-30).join('\n')}`);
189
+ }
190
+ /** Cross-cutting safety oracle, checked after every simulated tick. */
191
+ export function assertFleetInvariants(state) {
192
+ const fail = (message) => {
193
+ throw new Error(`fleet invariant: ${message}`);
194
+ };
195
+ const active = state.assignments.filter((a) => a.status === 'active');
196
+ if (active.length > state.spec.execution.maxConcurrentTaskGroups)
197
+ fail('global capacity');
198
+ if (new Set(active.map((a) => a.groupId)).size !== active.length)
199
+ fail('group ownership');
200
+ for (const worker of state.workers) {
201
+ if (active.filter((a) => a.workerId === worker.description.id).length >
202
+ worker.description.groupSlots)
203
+ fail('worker capacity');
204
+ }
205
+ for (const cellId of state.groups.flatMap((g) => g.cellIds)) {
206
+ const attempts = state.attempts.filter((a) => a.cellId === cellId);
207
+ if (attempts.filter((a) => a.disposition === 'selected').length > 1)
208
+ fail('duplicate result');
209
+ if (attempts.length > state.policy.maxAttemptsPerCell)
210
+ fail('retry budget');
211
+ if (attempts.some((a, i) => a.sequence !== i + 1))
212
+ fail('attempt ordering');
213
+ }
214
+ if (new Set(state.attempts.map((a) => a.id)).size !== state.attempts.length)
215
+ fail('attempt identity');
216
+ for (const assignment of state.assignments) {
217
+ const group = state.groups.find((g) => g.id === assignment.groupId);
218
+ if (!group ||
219
+ assignment.attemptIds.some((id) => {
220
+ const attempt = state.attempts.find((a) => a.id === id);
221
+ return (!attempt ||
222
+ attempt.assignmentId !== assignment.id ||
223
+ !group.cellIds.includes(attempt.cellId));
224
+ }))
225
+ fail('assignment membership');
226
+ }
227
+ for (const attempt of state.attempts.filter((a) => a.disposition === 'selected')) {
228
+ if (!attempt.report ||
229
+ attempt.report.verification !== 'valid' ||
230
+ attempt.report.cleanup !== 'confirmed' ||
231
+ attempt.report.environmentFailure !== null ||
232
+ !['completed', 'subject_failed'].includes(attempt.report.execution))
233
+ fail('selected evidence');
234
+ }
235
+ }
236
+ export function simulationOutput() {
237
+ return {
238
+ report: {
239
+ result: {
240
+ status: 'completed',
241
+ score: 1,
242
+ usage: {
243
+ inputTokens: 1,
244
+ outputTokens: 1,
245
+ cacheReadTokens: 0,
246
+ cacheWriteTokens: 0,
247
+ reasoningTokens: 0,
248
+ totalTokens: 2,
249
+ },
250
+ costUsd: 0.01,
251
+ durationMs: 1,
252
+ failureReason: null,
253
+ artifacts: [],
254
+ },
255
+ execution: 'completed',
256
+ verification: 'valid',
257
+ usage: 'complete',
258
+ cleanup: 'confirmed',
259
+ environmentFailure: null,
260
+ },
261
+ artifacts: [new TextEncoder().encode('mock subject artifact')],
262
+ };
263
+ }
264
+ class SimulatedLinkError extends Error {
265
+ }
266
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
267
+ const seed = Number(process.argv[2] ?? 1);
268
+ const result = await simulateFleet(seed);
269
+ process.stdout.write(`${JSON.stringify({ seed, elapsedTicks: result.elapsedTicks, ...result.summary, trace: result.trace }, null, 2)}\n`);
270
+ }
@@ -0,0 +1,243 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { createHash, randomUUID } from 'node:crypto';
20
+ import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
21
+ import { join } from 'node:path';
22
+ import { stableJsonStringify } from '@maka/core/canonical-json';
23
+ import { createFleetState, transitionFleet, } from './fleet.js';
24
+ export class FleetCoordinator {
25
+ persistence;
26
+ now;
27
+ static #owners = new WeakSet();
28
+ #tail = Promise.resolve();
29
+ #closed = false;
30
+ constructor(persistence, now) {
31
+ this.persistence = persistence;
32
+ this.now = now;
33
+ }
34
+ static async open(input) {
35
+ if (FleetCoordinator.#owners.has(input.persistence))
36
+ throw new Error('fleet persistence already has a coordinator');
37
+ FleetCoordinator.#owners.add(input.persistence);
38
+ try {
39
+ const initial = createFleetState(input.spec, input.policy);
40
+ const existing = await input.persistence.load();
41
+ if (existing &&
42
+ (existing.version !== initial.version ||
43
+ stableJsonStringify(existing.spec) !== stableJsonStringify(initial.spec) ||
44
+ stableJsonStringify(existing.policy) !== stableJsonStringify(initial.policy))) {
45
+ throw new Error('fleet run identity differs');
46
+ }
47
+ if (!existing)
48
+ await input.persistence.save(initial);
49
+ const coordinator = new FleetCoordinator(input.persistence, input.now);
50
+ await coordinator.command({ kind: 'recover' });
51
+ return coordinator;
52
+ }
53
+ catch (error) {
54
+ FleetCoordinator.#owners.delete(input.persistence);
55
+ throw error;
56
+ }
57
+ }
58
+ command(command) {
59
+ if (this.#closed)
60
+ return Promise.reject(new Error('coordinator closed'));
61
+ const captured = structuredClone(command);
62
+ const operation = this.#tail.then(async () => {
63
+ const state = await this.persistence.load();
64
+ if (!state)
65
+ throw new Error('fleet state missing');
66
+ if (captured.kind === 'report') {
67
+ for (const artifact of captured.report.artifacts)
68
+ await this.persistence.getArtifact(artifact);
69
+ }
70
+ const next = transitionFleet(state, captured, this.now());
71
+ await this.persistence.save(next.state);
72
+ return next.reply;
73
+ });
74
+ this.#tail = operation.catch(() => undefined);
75
+ return operation;
76
+ }
77
+ async snapshot() {
78
+ await this.#tail;
79
+ const state = await this.persistence.load();
80
+ if (!state)
81
+ throw new Error('fleet state missing');
82
+ return state;
83
+ }
84
+ putArtifact(bytes) {
85
+ if (this.#closed)
86
+ return Promise.reject(new Error('coordinator closed'));
87
+ return this.persistence.putArtifact(bytes);
88
+ }
89
+ /** Stop accepting commands and drain commits before closing persistence or replacing this owner. */
90
+ async close() {
91
+ if (this.#closed) {
92
+ await this.#tail;
93
+ return;
94
+ }
95
+ this.#closed = true;
96
+ await this.#tail;
97
+ FleetCoordinator.#owners.delete(this.persistence);
98
+ }
99
+ }
100
+ export class MemoryFleetPersistence {
101
+ #state = null;
102
+ #artifacts = new Map();
103
+ async load() {
104
+ return structuredClone(this.#state);
105
+ }
106
+ async save(state) {
107
+ this.#state = structuredClone(state);
108
+ }
109
+ async putArtifact(bytes) {
110
+ const ref = reference(bytes);
111
+ this.#artifacts.set(ref.sha256, Uint8Array.from(bytes));
112
+ return ref;
113
+ }
114
+ async getArtifact(ref) {
115
+ validateReference(ref);
116
+ const bytes = this.#artifacts.get(ref.sha256);
117
+ if (!bytes)
118
+ throw new Error('artifact missing');
119
+ verifyArtifact(ref, bytes);
120
+ return Uint8Array.from(bytes);
121
+ }
122
+ }
123
+ /** Local coordinator storage. A stale writer lock requires explicit operator recovery. */
124
+ export class FileFleetPersistence {
125
+ root;
126
+ release;
127
+ #tail = Promise.resolve();
128
+ #closing;
129
+ constructor(root, release) {
130
+ this.root = root;
131
+ this.release = release;
132
+ }
133
+ static async open(root) {
134
+ await mkdir(root, { recursive: true });
135
+ const lockPath = join(root, '.writer.lock');
136
+ const lock = await open(lockPath, 'wx', 0o600);
137
+ let closed = false;
138
+ return new FileFleetPersistence(root, async () => {
139
+ if (closed)
140
+ return;
141
+ closed = true;
142
+ await lock.close();
143
+ await unlink(lockPath);
144
+ });
145
+ }
146
+ async close() {
147
+ this.#closing ??= this.#tail.then(() => this.release());
148
+ await this.#closing;
149
+ }
150
+ #operation(operation) {
151
+ if (this.#closing)
152
+ return Promise.reject(new Error('fleet persistence closed'));
153
+ const running = this.#tail.then(operation);
154
+ this.#tail = running.catch(() => undefined);
155
+ return running;
156
+ }
157
+ load() {
158
+ return this.#operation(() => this.#load());
159
+ }
160
+ async #load() {
161
+ let raw;
162
+ try {
163
+ raw = await readFile(join(this.root, 'state.json'), 'utf8');
164
+ }
165
+ catch (error) {
166
+ if (error.code === 'ENOENT')
167
+ return null;
168
+ throw error;
169
+ }
170
+ const envelope = JSON.parse(raw);
171
+ if (digest(Buffer.from(stableJsonStringify(envelope.state))) !== envelope.sha256) {
172
+ throw new Error('fleet state checksum mismatch');
173
+ }
174
+ if (envelope.state.version !== 'maka.eval.fleet.v1')
175
+ throw new Error('unsupported fleet state');
176
+ return envelope.state;
177
+ }
178
+ async save(state) {
179
+ const canonical = stableJsonStringify(state);
180
+ const bytes = Buffer.from(JSON.stringify({ sha256: digest(Buffer.from(canonical)), state }));
181
+ await this.#operation(() => atomicWrite(this.root, 'state.json', bytes));
182
+ }
183
+ async putArtifact(bytes) {
184
+ const captured = Uint8Array.from(bytes);
185
+ return this.#operation(async () => {
186
+ const ref = reference(captured);
187
+ const directory = join(this.root, 'artifacts');
188
+ await mkdir(directory, { recursive: true });
189
+ await atomicWrite(directory, ref.sha256, captured);
190
+ return ref;
191
+ });
192
+ }
193
+ async getArtifact(ref) {
194
+ validateReference(ref);
195
+ const captured = { ...ref };
196
+ return this.#operation(async () => {
197
+ const bytes = await readFile(join(this.root, 'artifacts', captured.sha256));
198
+ verifyArtifact(captured, bytes);
199
+ return bytes;
200
+ });
201
+ }
202
+ }
203
+ async function atomicWrite(directory, name, bytes) {
204
+ const temporary = join(directory, `.${name}.${randomUUID()}.tmp`);
205
+ const file = await open(temporary, 'wx', 0o600);
206
+ try {
207
+ await file.writeFile(bytes);
208
+ await file.sync();
209
+ await file.close();
210
+ await rename(temporary, join(directory, name));
211
+ if (process.platform !== 'win32') {
212
+ const parent = await open(directory, 'r');
213
+ try {
214
+ await parent.sync();
215
+ }
216
+ finally {
217
+ await parent.close();
218
+ }
219
+ }
220
+ }
221
+ finally {
222
+ await file.close().catch(() => undefined);
223
+ await unlink(temporary).catch((error) => {
224
+ if (error.code !== 'ENOENT')
225
+ throw error;
226
+ });
227
+ }
228
+ }
229
+ function digest(bytes) {
230
+ return createHash('sha256').update(bytes).digest('hex');
231
+ }
232
+ function reference(bytes) {
233
+ return { sha256: digest(bytes), bytes: bytes.byteLength };
234
+ }
235
+ function validateReference(ref) {
236
+ if (!/^[a-f0-9]{64}$/.test(ref.sha256) || !Number.isSafeInteger(ref.bytes) || ref.bytes < 0) {
237
+ throw new Error('invalid artifact reference');
238
+ }
239
+ }
240
+ function verifyArtifact(ref, bytes) {
241
+ if (bytes.byteLength !== ref.bytes || digest(bytes) !== ref.sha256)
242
+ throw new Error('artifact checksum mismatch');
243
+ }
@@ -0,0 +1,125 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ /**
20
+ * Transport-independent worker loop. Its outbox survives disconnection, not process death.
21
+ * The caller drives polling; no wall clock, timers, networking or cloud SDK is hidden here.
22
+ */
23
+ export class FleetWorker {
24
+ description;
25
+ transport;
26
+ execute;
27
+ #runs = new Map();
28
+ #outbox = new Map();
29
+ #polling;
30
+ constructor(description, transport, execute) {
31
+ this.description = description;
32
+ this.transport = transport;
33
+ this.execute = execute;
34
+ }
35
+ poll() {
36
+ this.#polling ??= this.#poll().finally(() => {
37
+ this.#polling = undefined;
38
+ });
39
+ return this.#polling;
40
+ }
41
+ get bufferedReports() {
42
+ return this.#outbox.size;
43
+ }
44
+ get activeGroups() {
45
+ return this.#runs.size;
46
+ }
47
+ async #poll() {
48
+ await this.transport.command({ kind: 'register', worker: this.description });
49
+ const heartbeat = await this.transport.command({
50
+ kind: 'heartbeat',
51
+ workerId: this.description.id,
52
+ assignmentIds: [...this.#runs.keys()],
53
+ });
54
+ for (const work of heartbeat.work ?? [])
55
+ this.#adopt(work);
56
+ for (const [attemptId, output] of this.#outbox) {
57
+ const artifacts = [];
58
+ for (const bytes of output.artifacts)
59
+ artifacts.push(await this.transport.putArtifact(bytes));
60
+ await this.transport.command({
61
+ kind: 'report',
62
+ workerId: this.description.id,
63
+ attemptId,
64
+ report: { ...output.report, artifacts },
65
+ });
66
+ this.#outbox.delete(attemptId);
67
+ }
68
+ for (const [assignmentId, run] of this.#runs) {
69
+ if (!run.settled || run.work.cells.some((cell) => this.#outbox.has(cell.attemptId)))
70
+ continue;
71
+ await this.transport.command({ kind: 'finish', workerId: this.description.id, assignmentId });
72
+ this.#runs.delete(assignmentId);
73
+ }
74
+ while (this.#runs.size < this.description.groupSlots) {
75
+ const reply = await this.transport.command({ kind: 'claim', workerId: this.description.id });
76
+ if (!reply.assignment)
77
+ break;
78
+ for (const work of reply.work ?? [])
79
+ this.#adopt(work);
80
+ }
81
+ }
82
+ #adopt(work) {
83
+ if (this.#runs.has(work.assignment.id))
84
+ return;
85
+ const run = { work, settled: false, emitted: new Set() };
86
+ this.#runs.set(work.assignment.id, run);
87
+ const emit = (attemptId, output) => {
88
+ if (!work.cells.some((cell) => cell.attemptId === attemptId) || run.emitted.has(attemptId)) {
89
+ throw new Error('executor emitted an unknown or duplicate attempt');
90
+ }
91
+ run.emitted.add(attemptId);
92
+ this.#outbox.set(attemptId, structuredClone(output));
93
+ };
94
+ // A failed local operation is an execution uncertainty, not a provider classification.
95
+ void Promise.resolve()
96
+ .then(() => this.execute(work, emit))
97
+ .catch(() => undefined)
98
+ .finally(() => {
99
+ for (const cell of work.cells) {
100
+ if (run.emitted.has(cell.attemptId))
101
+ continue;
102
+ emit(cell.attemptId, {
103
+ report: {
104
+ result: {
105
+ status: 'infra_failed',
106
+ score: null,
107
+ usage: null,
108
+ costUsd: null,
109
+ durationMs: 0,
110
+ failureReason: 'worker execution ended without a cell report',
111
+ artifacts: [],
112
+ },
113
+ execution: 'unknown',
114
+ verification: 'not_run',
115
+ usage: 'missing',
116
+ cleanup: 'unknown',
117
+ environmentFailure: null,
118
+ },
119
+ artifacts: [],
120
+ });
121
+ }
122
+ run.settled = true;
123
+ });
124
+ }
125
+ }