archymedes-cli 2.0.0 → 2.1.0
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/dist/archymedes-main.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
turnFromChatResponse,
|
|
35
35
|
wanderGradeCounts,
|
|
36
36
|
wanderPlannerInstructions
|
|
37
|
-
} from "./archymedes-
|
|
37
|
+
} from "./archymedes-76z6n643.mjs";
|
|
38
38
|
import {
|
|
39
39
|
BrailleCanvas,
|
|
40
40
|
StreamSeries,
|
|
@@ -116,16 +116,242 @@ import {
|
|
|
116
116
|
|
|
117
117
|
// packages/archymedes-cli/src/archymedes.ts
|
|
118
118
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
119
|
-
import
|
|
119
|
+
import path27 from "path";
|
|
120
120
|
import { realpathSync } from "fs";
|
|
121
121
|
import { pathToFileURL } from "url";
|
|
122
122
|
|
|
123
|
+
// packages/core/src/cli/hosted-recovery.ts
|
|
124
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
125
|
+
import { promises as fs } from "node:fs";
|
|
126
|
+
import path from "node:path";
|
|
127
|
+
function digest(value) {
|
|
128
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
129
|
+
}
|
|
130
|
+
function savedRequest(request) {
|
|
131
|
+
return JSON.parse(JSON.stringify({
|
|
132
|
+
requestId: request.requestId ?? `cli_${randomUUID()}`,
|
|
133
|
+
messages: request.messages,
|
|
134
|
+
tools: request.tools,
|
|
135
|
+
maxOutputTokens: request.maxOutputTokens,
|
|
136
|
+
safetyIdentifier: request.safetyIdentifier,
|
|
137
|
+
...request.effort ? { effort: request.effort } : {}
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
function validateBatch(value, sessionId) {
|
|
141
|
+
const batch = value;
|
|
142
|
+
if (!batch || batch.version !== 1 || batch.sessionId !== sessionId || typeof batch.id !== "string" || typeof batch.scope !== "string" || !Number.isSafeInteger(batch.baseTotalRwf) || batch.baseTotalRwf < 0 || !Array.isArray(batch.entries)) {
|
|
143
|
+
throw new Error("Invalid hosted recovery snapshot; preserve the file for inspection.");
|
|
144
|
+
}
|
|
145
|
+
const ids = new Set;
|
|
146
|
+
for (const entry of batch.entries) {
|
|
147
|
+
if (!entry || !entry.request || typeof entry.request.requestId !== "string" || ids.has(entry.request.requestId) || !Array.isArray(entry.request.messages) || !Array.isArray(entry.request.tools) || digest(entry.request) !== entry.fingerprint || !["pending", "completed", "failed", "ambiguous", "cancelled"].includes(entry.state) || typeof entry.kind !== "string" || !entry.prices || entry.state === "completed" && !entry.response) {
|
|
148
|
+
throw new Error("Invalid hosted recovery request; refusing to replay changed content.");
|
|
149
|
+
}
|
|
150
|
+
ids.add(entry.request.requestId);
|
|
151
|
+
}
|
|
152
|
+
return batch;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
class HostedRecoveryStore {
|
|
156
|
+
sessionId;
|
|
157
|
+
batch;
|
|
158
|
+
tail = Promise.resolve();
|
|
159
|
+
file;
|
|
160
|
+
constructor(root, sessionId) {
|
|
161
|
+
this.sessionId = sessionId;
|
|
162
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(sessionId))
|
|
163
|
+
throw new Error("Unsafe recovery session id");
|
|
164
|
+
this.file = path.join(root, ".archymedes", "recovery", `${sessionId}.json`);
|
|
165
|
+
}
|
|
166
|
+
async read() {
|
|
167
|
+
let text;
|
|
168
|
+
try {
|
|
169
|
+
text = await fs.readFile(this.file, "utf8");
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error.code === "ENOENT")
|
|
172
|
+
return;
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
const parsed = JSON.parse(text);
|
|
176
|
+
if (digest(parsed.batch) !== parsed.integrity)
|
|
177
|
+
throw new Error("Hosted recovery snapshot integrity check failed");
|
|
178
|
+
return validateBatch(parsed.batch, this.sessionId);
|
|
179
|
+
}
|
|
180
|
+
async write(batch) {
|
|
181
|
+
await fs.mkdir(path.dirname(this.file), { recursive: true, mode: 448 });
|
|
182
|
+
const temporary = `${this.file}.${randomUUID()}.tmp`;
|
|
183
|
+
try {
|
|
184
|
+
const handle = await fs.open(temporary, "wx", 384);
|
|
185
|
+
try {
|
|
186
|
+
await handle.writeFile(JSON.stringify({ batch, integrity: digest(batch) }));
|
|
187
|
+
await handle.sync();
|
|
188
|
+
} finally {
|
|
189
|
+
await handle.close();
|
|
190
|
+
}
|
|
191
|
+
await fs.rename(temporary, this.file);
|
|
192
|
+
const directory = await fs.open(path.dirname(this.file), "r").catch(() => null);
|
|
193
|
+
if (directory) {
|
|
194
|
+
try {
|
|
195
|
+
await directory.sync();
|
|
196
|
+
} finally {
|
|
197
|
+
await directory.close();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
await fs.unlink(temporary).catch(() => {
|
|
202
|
+
return;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
wrap(provider, session, prices, kind) {
|
|
207
|
+
if (!provider.recoveryScope)
|
|
208
|
+
return provider;
|
|
209
|
+
return {
|
|
210
|
+
capabilities: provider.capabilities,
|
|
211
|
+
complete: (request) => {
|
|
212
|
+
const operation = this.tail.then(() => this.complete(provider, session(), prices, kind, request));
|
|
213
|
+
this.tail = operation.catch(() => {
|
|
214
|
+
return;
|
|
215
|
+
});
|
|
216
|
+
return operation;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async complete(provider, session, prices, kind, request) {
|
|
221
|
+
request.signal?.throwIfAborted();
|
|
222
|
+
if (!this.batch) {
|
|
223
|
+
if (await this.read())
|
|
224
|
+
throw new Error("Recover the interrupted hosted request before starting new work.");
|
|
225
|
+
this.batch = { version: 1, id: randomUUID(), sessionId: this.sessionId, scope: provider.recoveryScope, baseTotalRwf: session.totalRwf, entries: [] };
|
|
226
|
+
}
|
|
227
|
+
if (this.batch.scope !== provider.recoveryScope)
|
|
228
|
+
throw new Error("Hosted recovery provider configuration changed");
|
|
229
|
+
const saved = savedRequest(request);
|
|
230
|
+
let entry = this.batch.entries.find((item) => item.request.requestId === saved.requestId);
|
|
231
|
+
if (entry && entry.fingerprint !== digest(saved))
|
|
232
|
+
throw new Error("Hosted request identity reused with changed content");
|
|
233
|
+
if (!entry) {
|
|
234
|
+
entry = { request: saved, fingerprint: digest(saved), kind, prices: { ...prices }, state: "pending" };
|
|
235
|
+
this.batch.entries.push(entry);
|
|
236
|
+
await this.write(this.batch);
|
|
237
|
+
}
|
|
238
|
+
await this.write(this.batch);
|
|
239
|
+
return this.dispatch(provider, this.batch, entry, request.signal);
|
|
240
|
+
}
|
|
241
|
+
async dispatch(provider, batch, entry, signal, recoveryOnly = false) {
|
|
242
|
+
if (entry.response) {
|
|
243
|
+
await this.write(batch);
|
|
244
|
+
return entry.response;
|
|
245
|
+
}
|
|
246
|
+
if (entry.state === "failed")
|
|
247
|
+
throw new Error("Hosted request was previously confirmed failed");
|
|
248
|
+
if (!recoveryOnly && signal?.aborted) {
|
|
249
|
+
entry.state = "failed";
|
|
250
|
+
await this.write(batch);
|
|
251
|
+
signal.throwIfAborted();
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const method = recoveryOnly && provider.recoverComplete ? provider.recoverComplete : provider.complete;
|
|
255
|
+
const response = await method.call(provider, { ...entry.request, signal });
|
|
256
|
+
entry.response = JSON.parse(JSON.stringify(response));
|
|
257
|
+
entry.state = "completed";
|
|
258
|
+
await this.write(batch);
|
|
259
|
+
return response;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (!entry.response) {
|
|
262
|
+
entry.state = error?.code === "request_previously_failed" ? "failed" : signal?.aborted ? "cancelled" : "ambiguous";
|
|
263
|
+
await this.write(batch);
|
|
264
|
+
}
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
responses() {
|
|
269
|
+
return (this.batch?.entries ?? []).flatMap((entry) => entry.response ? [{ requestId: entry.request.requestId, turn: entry.response }] : []);
|
|
270
|
+
}
|
|
271
|
+
settledBatchId() {
|
|
272
|
+
return this.batch?.entries.every((entry) => entry.state === "completed" || entry.state === "failed") ? this.batch.id : undefined;
|
|
273
|
+
}
|
|
274
|
+
async cleanup(committedId) {
|
|
275
|
+
const batch = this.batch ?? await this.read();
|
|
276
|
+
if (batch && batch.id === committedId) {
|
|
277
|
+
await fs.unlink(this.file).catch((error) => {
|
|
278
|
+
if (error.code !== "ENOENT")
|
|
279
|
+
throw error;
|
|
280
|
+
});
|
|
281
|
+
this.batch = undefined;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async recover(provider, session, signal) {
|
|
285
|
+
const batch = await this.read();
|
|
286
|
+
if (!batch)
|
|
287
|
+
return null;
|
|
288
|
+
if (session.hostedRecoveryBatchId === batch.id) {
|
|
289
|
+
await this.cleanup(batch.id);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
if (!provider.recoveryScope || batch.scope !== provider.recoveryScope) {
|
|
293
|
+
throw new Error("An interrupted hosted request belongs to a different provider, credential or routing configuration. Resume with the original configuration to recover it.");
|
|
294
|
+
}
|
|
295
|
+
for (const entry of batch.entries) {
|
|
296
|
+
if (entry.state === "failed" || entry.response)
|
|
297
|
+
continue;
|
|
298
|
+
try {
|
|
299
|
+
await this.dispatch(provider, batch, entry, signal, true);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error?.code === "request_previously_failed")
|
|
302
|
+
continue;
|
|
303
|
+
const failure = new Error(`Hosted request ${entry.request.requestId} remains unresolved. Its identity is preserved; resume again to reconcile it.`, { cause: error });
|
|
304
|
+
throw failure;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
this.batch = batch;
|
|
308
|
+
const main = batch.entries.filter((entry) => entry.kind === "main").at(-1);
|
|
309
|
+
const messages = main ? [...main.request.messages] : [...session.messages];
|
|
310
|
+
if (main?.response) {
|
|
311
|
+
const response = main.response;
|
|
312
|
+
if (response.toolCalls.length) {
|
|
313
|
+
messages.push({ role: "assistant", content: response.content, toolCalls: response.toolCalls });
|
|
314
|
+
for (const call of response.toolCalls)
|
|
315
|
+
messages.push({
|
|
316
|
+
role: "tool",
|
|
317
|
+
toolCallId: call.id,
|
|
318
|
+
name: call.name,
|
|
319
|
+
content: "Interrupted execution: the tool outcome is unknown. Do not repeat this action without inspecting the current state. Recovery did not replay the tool."
|
|
320
|
+
});
|
|
321
|
+
} else
|
|
322
|
+
messages.push({ role: "assistant", content: response.content || response.refusal || "Recovered hosted response with no text." });
|
|
323
|
+
}
|
|
324
|
+
for (const entry of batch.entries.filter((item) => item.kind !== "main" && item.response)) {
|
|
325
|
+
messages.push({ role: "assistant", content: `[Recovered ${entry.kind.startsWith("delegate") ? "delegated work" : "compaction"}]
|
|
326
|
+
${entry.response.content}` });
|
|
327
|
+
}
|
|
328
|
+
messages.push({ role: "user", internal: true, content: "The previous run was interrupted. Hosted responses and usage were recovered. Tool execution may be incomplete; inspect the workspace and verify before repeating side effects or claiming completion." });
|
|
329
|
+
const groups = new Map;
|
|
330
|
+
for (const entry of batch.entries)
|
|
331
|
+
if (entry.response) {
|
|
332
|
+
const group = groups.get(entry.kind) ?? { input: 0, output: 0, prices: entry.prices };
|
|
333
|
+
group.input += entry.response.usage.inputTokens;
|
|
334
|
+
group.output += entry.response.usage.outputTokens;
|
|
335
|
+
groups.set(entry.kind, group);
|
|
336
|
+
}
|
|
337
|
+
const cost = [...groups.values()].reduce((sum, group) => sum + priceActualModelUsage(group.input, group.output, group.prices), 0);
|
|
338
|
+
return {
|
|
339
|
+
...session,
|
|
340
|
+
messages,
|
|
341
|
+
totalRwf: batch.baseTotalRwf + cost,
|
|
342
|
+
hostedRecoveryBatchId: batch.id,
|
|
343
|
+
routingReceipts: mergeRoutingReceipts(session.routingReceipts, batch.entries.flatMap((entry) => entry.response?.routingReceipt ? [{ ...entry.response.routingReceipt, taskId: entry.response.routingReceipt.taskId ?? entry.request.requestId }] : []))
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
123
348
|
// packages/core/src/cli/agent.ts
|
|
124
|
-
import { randomUUID as
|
|
125
|
-
import
|
|
349
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
350
|
+
import path14 from "node:path";
|
|
351
|
+
import { access as access3 } from "node:fs/promises";
|
|
126
352
|
|
|
127
353
|
// packages/core/src/agent-runtime.ts
|
|
128
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
354
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
129
355
|
function agentMessagePromptParts(message) {
|
|
130
356
|
const parts = [message.content ?? ""];
|
|
131
357
|
if ("toolCalls" in message && Array.isArray(message.toolCalls)) {
|
|
@@ -436,7 +662,7 @@ class BoundedAgentRuntime {
|
|
|
436
662
|
if (maximumOutputTokens < 1)
|
|
437
663
|
return stop("iteration_limit", "Run reached its approved model budget before another provider call.", iteration - 1);
|
|
438
664
|
const modelRequest = {
|
|
439
|
-
requestId: `cli_${
|
|
665
|
+
requestId: `cli_${randomUUID2()}`,
|
|
440
666
|
...request.effort ? { effort: request.effort } : {},
|
|
441
667
|
messages: [...messages],
|
|
442
668
|
tools: definitions,
|
|
@@ -493,7 +719,7 @@ class BoundedAgentRuntime {
|
|
|
493
719
|
actualModelRwf = priceActualModelUsage(usage.inputTokens, usage.outputTokens, this.dependencies.prices);
|
|
494
720
|
if (actualModelRwf > request.modelReservationRwf)
|
|
495
721
|
throw new Error("Actual model usage exceeds the reserved model budget");
|
|
496
|
-
await this.dependencies.control.persistEvent({ type: "model_turn", iteration, responseId: turn.responseId, model: turn.model, toolCallCount: turn.toolCalls.length, usage: turn.usage });
|
|
722
|
+
await this.dependencies.control.persistEvent({ type: "model_turn", requestId: modelRequest.requestId, iteration, responseId: turn.responseId, model: turn.model, toolCallCount: turn.toolCalls.length, usage: turn.usage });
|
|
497
723
|
if (turn.finishReason === "refusal")
|
|
498
724
|
return stop("blocked", turn.refusal?.trim() || "Model refused the task.", iteration);
|
|
499
725
|
if (turn.finishReason === "length") {
|
|
@@ -623,8 +849,8 @@ class BoundedAgentRuntime {
|
|
|
623
849
|
let artifact;
|
|
624
850
|
let content = truncate(raw, budget);
|
|
625
851
|
const withinTwiceBudget = raw.length <= budget * 2 && raw.length <= remaining;
|
|
626
|
-
const
|
|
627
|
-
const alreadySent =
|
|
852
|
+
const digest2 = raw.length >= DEDUPE_MIN_CHARS ? createHash2("sha256").update(raw).digest("hex") : undefined;
|
|
853
|
+
const alreadySent = digest2 ? sentResults.get(digest2) : undefined;
|
|
628
854
|
if (alreadySent) {
|
|
629
855
|
content = `[Identical to the earlier ${alreadySent.toolName} result in this conversation${alreadySent.path ? `, saved at ${alreadySent.path}` : ""}. Unchanged since then; re-read it there if you need it again.]`;
|
|
630
856
|
} else if (withinTwiceBudget) {
|
|
@@ -638,8 +864,8 @@ class BoundedAgentRuntime {
|
|
|
638
864
|
content = evictedToolResult(raw, stored, Math.min(budget, Math.max(EXCERPT_FLOOR, Math.round(budget * EXCERPT_SHARE))));
|
|
639
865
|
}
|
|
640
866
|
}
|
|
641
|
-
if (
|
|
642
|
-
sentResults.set(
|
|
867
|
+
if (digest2 && !alreadySent)
|
|
868
|
+
sentResults.set(digest2, { toolName: call.name, path: artifact?.path });
|
|
643
869
|
totalToolResultChars += content.length;
|
|
644
870
|
iterationChars += content.length;
|
|
645
871
|
messages.push({ role: "tool", content, toolCallId: call.id, name: call.name });
|
|
@@ -661,8 +887,9 @@ class BoundedAgentRuntime {
|
|
|
661
887
|
|
|
662
888
|
// packages/core/src/cli/checkpoints.ts
|
|
663
889
|
import { spawn } from "node:child_process";
|
|
664
|
-
import {
|
|
665
|
-
import
|
|
890
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
891
|
+
import { promises as fs2 } from "node:fs";
|
|
892
|
+
import path2 from "node:path";
|
|
666
893
|
var EXCLUDE_ARCHYMEDES = [".", ":(exclude).archymedes"];
|
|
667
894
|
var runGit = (args, options) => new Promise((resolve) => {
|
|
668
895
|
const child = spawn("git", args, {
|
|
@@ -679,7 +906,7 @@ var runGit = (args, options) => new Promise((resolve) => {
|
|
|
679
906
|
stderr += String(chunk);
|
|
680
907
|
});
|
|
681
908
|
child.on("error", (error) => resolve({ exitCode: 127, stdout, stderr: error.message }));
|
|
682
|
-
child.on("close", (code) => resolve({ exitCode: code ??
|
|
909
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
683
910
|
});
|
|
684
911
|
|
|
685
912
|
class CheckpointStore {
|
|
@@ -700,7 +927,7 @@ class CheckpointStore {
|
|
|
700
927
|
return result.exitCode === 0;
|
|
701
928
|
}
|
|
702
929
|
async capture(label, turnId, messageCount) {
|
|
703
|
-
await
|
|
930
|
+
await fs2.mkdir(path2.dirname(this.indexFile), { recursive: true }).catch(() => {
|
|
704
931
|
return;
|
|
705
932
|
});
|
|
706
933
|
const env = { GIT_INDEX_FILE: this.indexFile };
|
|
@@ -728,26 +955,38 @@ class CheckpointStore {
|
|
|
728
955
|
return this.checkpoints[this.checkpoints.length - 1];
|
|
729
956
|
}
|
|
730
957
|
async diffPatch() {
|
|
731
|
-
|
|
732
|
-
if (!checkpoint)
|
|
733
|
-
return "";
|
|
734
|
-
const result = await this.git(["diff", "--unified=3", "--no-color", checkpoint.tree, "--", ...EXCLUDE_ARCHYMEDES], { cwd: this.root });
|
|
735
|
-
return result.exitCode === 0 ? result.stdout : "";
|
|
958
|
+
return this.diff(["--unified=3", "--no-color"]);
|
|
736
959
|
}
|
|
737
960
|
async diffStat() {
|
|
961
|
+
return (await this.diff(["--stat"])).trim();
|
|
962
|
+
}
|
|
963
|
+
async diff(format) {
|
|
738
964
|
const checkpoint = this.latest();
|
|
739
965
|
if (!checkpoint)
|
|
740
966
|
return "";
|
|
741
|
-
const
|
|
742
|
-
|
|
967
|
+
const index = `${this.indexFile}.diff-${randomUUID3()}`;
|
|
968
|
+
const options = { cwd: this.root, env: { GIT_INDEX_FILE: index } };
|
|
969
|
+
try {
|
|
970
|
+
if ((await this.git(["read-tree", checkpoint.tree], options)).exitCode !== 0)
|
|
971
|
+
return "";
|
|
972
|
+
if ((await this.git(["add", "--all", "--", ...EXCLUDE_ARCHYMEDES], options)).exitCode !== 0)
|
|
973
|
+
return "";
|
|
974
|
+
const result = await this.git(["diff", "--cached", ...format, checkpoint.tree, "--", ...EXCLUDE_ARCHYMEDES], options);
|
|
975
|
+
return result.exitCode === 0 ? result.stdout : "";
|
|
976
|
+
} finally {
|
|
977
|
+
await fs2.unlink(index).catch(() => {
|
|
978
|
+
return;
|
|
979
|
+
});
|
|
980
|
+
}
|
|
743
981
|
}
|
|
744
982
|
}
|
|
745
983
|
|
|
746
984
|
// packages/core/src/cli/permissions.ts
|
|
747
|
-
import { createHash as
|
|
985
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
748
986
|
|
|
749
987
|
// packages/core/src/cli/command.ts
|
|
750
988
|
import { spawn as spawn2 } from "node:child_process";
|
|
989
|
+
import { constants } from "node:os";
|
|
751
990
|
var ARCHYMEDES_CREDENTIAL_ENV_NAMES = [
|
|
752
991
|
"ANTHROPIC_API_KEY",
|
|
753
992
|
"OPENAI_API_KEY",
|
|
@@ -773,7 +1012,8 @@ function tokenizeCommand(command, platform = process.platform) {
|
|
|
773
1012
|
let quote = null;
|
|
774
1013
|
let escaped = false;
|
|
775
1014
|
let hasToken = false;
|
|
776
|
-
for (
|
|
1015
|
+
for (let index = 0;index < command.length; index++) {
|
|
1016
|
+
const character = command[index];
|
|
777
1017
|
if (escaped) {
|
|
778
1018
|
current += character;
|
|
779
1019
|
escaped = false;
|
|
@@ -781,8 +1021,16 @@ function tokenizeCommand(command, platform = process.platform) {
|
|
|
781
1021
|
continue;
|
|
782
1022
|
}
|
|
783
1023
|
if (character === "\\" && quote !== "'" && platform !== "win32") {
|
|
784
|
-
|
|
785
|
-
|
|
1024
|
+
if (quote === '"' && !["$", "`", '"', "\\", `
|
|
1025
|
+
`].includes(command[index + 1])) {
|
|
1026
|
+
current += character;
|
|
1027
|
+
} else if (command[index + 1] === `
|
|
1028
|
+
`) {
|
|
1029
|
+
index++;
|
|
1030
|
+
} else {
|
|
1031
|
+
escaped = true;
|
|
1032
|
+
hasToken = true;
|
|
1033
|
+
}
|
|
786
1034
|
continue;
|
|
787
1035
|
}
|
|
788
1036
|
if (quote) {
|
|
@@ -864,11 +1112,15 @@ function isFindDelete(command) {
|
|
|
864
1112
|
}
|
|
865
1113
|
var SHELL_METACHARACTERS = /[|&;><`$()]|\|\||&&/;
|
|
866
1114
|
function hasShellSyntax(command) {
|
|
1115
|
+
if (/^\s*(?:[A-Za-z_][A-Za-z0-9_]*=|(?:cd|export|unset|set|source|\.|umask|alias|unalias|read|eval|exec|exit|return)\s)/.test(command))
|
|
1116
|
+
return true;
|
|
867
1117
|
let quote = null;
|
|
868
1118
|
let escaped = false;
|
|
1119
|
+
let tokenStart = true;
|
|
869
1120
|
for (const character of command) {
|
|
870
1121
|
if (escaped) {
|
|
871
1122
|
escaped = false;
|
|
1123
|
+
tokenStart = false;
|
|
872
1124
|
continue;
|
|
873
1125
|
}
|
|
874
1126
|
if (character === "\\" && quote !== "'") {
|
|
@@ -878,14 +1130,23 @@ function hasShellSyntax(command) {
|
|
|
878
1130
|
if (quote) {
|
|
879
1131
|
if (character === quote)
|
|
880
1132
|
quote = null;
|
|
1133
|
+
else if (quote === '"' && (character === "$" || character === "`"))
|
|
1134
|
+
return true;
|
|
881
1135
|
continue;
|
|
882
1136
|
}
|
|
883
1137
|
if (character === '"' || character === "'") {
|
|
884
1138
|
quote = character;
|
|
1139
|
+
tokenStart = false;
|
|
885
1140
|
continue;
|
|
886
1141
|
}
|
|
1142
|
+
if (character === `
|
|
1143
|
+
` || /[*?\[]/.test(character))
|
|
1144
|
+
return true;
|
|
1145
|
+
if (tokenStart && (character === "~" || character === "#"))
|
|
1146
|
+
return true;
|
|
887
1147
|
if (SHELL_METACHARACTERS.test(character))
|
|
888
1148
|
return true;
|
|
1149
|
+
tokenStart = /\s/.test(character);
|
|
889
1150
|
}
|
|
890
1151
|
return false;
|
|
891
1152
|
}
|
|
@@ -951,6 +1212,8 @@ var runLocalCommand = async (command, options) => {
|
|
|
951
1212
|
argv = [...CONTAINMENT_ARGS, ...inner];
|
|
952
1213
|
throughShell = false;
|
|
953
1214
|
}
|
|
1215
|
+
if (options.signal?.aborted)
|
|
1216
|
+
return { exitCode: 130, stdout: "", stderr: "Command cancelled before start." };
|
|
954
1217
|
return new Promise((resolve) => {
|
|
955
1218
|
const child = spawn2(program, argv, {
|
|
956
1219
|
cwd: options.cwd,
|
|
@@ -1029,8 +1292,8 @@ Command exceeded the ${maximumOutputBytes}-byte output limit and was killed.`;
|
|
|
1029
1292
|
stderr += error.message;
|
|
1030
1293
|
finish(127);
|
|
1031
1294
|
});
|
|
1032
|
-
child.on("close", (code) => {
|
|
1033
|
-
closedCode = code;
|
|
1295
|
+
child.on("close", (code, signal) => {
|
|
1296
|
+
closedCode = code ?? (signal ? 128 + (constants.signals[signal] ?? 1) : 1);
|
|
1034
1297
|
finishWhenDrained();
|
|
1035
1298
|
});
|
|
1036
1299
|
abort = () => {
|
|
@@ -1089,10 +1352,10 @@ function assessToolSafety(call, tool) {
|
|
|
1089
1352
|
add("destructive", "recursive, forced file removal");
|
|
1090
1353
|
if (isFindDelete(command))
|
|
1091
1354
|
add("destructive", "find with -delete, as thorough a wipe as rm -rf");
|
|
1092
|
-
const
|
|
1093
|
-
if (SENSITIVE_PATH.test(
|
|
1355
|
+
const path3 = typeof args.path === "string" ? args.path : "";
|
|
1356
|
+
if (SENSITIVE_PATH.test(path3))
|
|
1094
1357
|
add("credentials", "sensitive credential or environment file");
|
|
1095
|
-
if (PRODUCTION_PATH.test(
|
|
1358
|
+
if (PRODUCTION_PATH.test(path3))
|
|
1096
1359
|
add("production", "production configuration file");
|
|
1097
1360
|
const content = [args.content, args.replacement, args.newText].filter((value) => typeof value === "string").join(`
|
|
1098
1361
|
`);
|
|
@@ -1158,7 +1421,7 @@ function actionDigest(call, tool) {
|
|
|
1158
1421
|
provenance: tool.provenance ?? { kind: "built-in" },
|
|
1159
1422
|
arguments: call.arguments ?? {}
|
|
1160
1423
|
}));
|
|
1161
|
-
return
|
|
1424
|
+
return createHash3("sha256").update(action).digest("hex");
|
|
1162
1425
|
}
|
|
1163
1426
|
class PermissionLedger {
|
|
1164
1427
|
mode;
|
|
@@ -1242,9 +1505,9 @@ function describeToolCall(call, tool) {
|
|
|
1242
1505
|
}
|
|
1243
1506
|
|
|
1244
1507
|
// packages/core/src/cli/memory.ts
|
|
1245
|
-
import { promises as
|
|
1508
|
+
import { promises as fs3 } from "node:fs";
|
|
1246
1509
|
import os from "node:os";
|
|
1247
|
-
import
|
|
1510
|
+
import path3 from "node:path";
|
|
1248
1511
|
var MEMORY_LIMITS = { project: 6000, user: 2500 };
|
|
1249
1512
|
var DEFAULT_RECALL_CHARS = 2200;
|
|
1250
1513
|
var MEMORY_HEADER = "# Archymedes memory";
|
|
@@ -1258,15 +1521,15 @@ var PREAMBLE = [
|
|
|
1258
1521
|
`);
|
|
1259
1522
|
function archymedesConfigDirectory(environment, platform = process.platform) {
|
|
1260
1523
|
if (environment.ARCHYMEDES_CONFIG_DIR?.trim())
|
|
1261
|
-
return
|
|
1524
|
+
return path3.resolve(environment.ARCHYMEDES_CONFIG_DIR);
|
|
1262
1525
|
if (platform === "win32")
|
|
1263
|
-
return
|
|
1526
|
+
return path3.join(environment.APPDATA?.trim() || path3.join(os.homedir(), "AppData", "Roaming"), "Archymedes");
|
|
1264
1527
|
if (platform === "darwin")
|
|
1265
|
-
return
|
|
1266
|
-
return
|
|
1528
|
+
return path3.join(os.homedir(), "Library", "Application Support", "Archymedes");
|
|
1529
|
+
return path3.join(environment.XDG_CONFIG_HOME?.trim() || path3.join(os.homedir(), ".config"), "archymedes");
|
|
1267
1530
|
}
|
|
1268
1531
|
function memoryFile(scope, root, environment) {
|
|
1269
|
-
return scope === "project" ?
|
|
1532
|
+
return scope === "project" ? path3.join(root, ".archymedes", "memory.md") : path3.join(archymedesConfigDirectory(environment), "memory.md");
|
|
1270
1533
|
}
|
|
1271
1534
|
function parseMemoryFile(contents, scope) {
|
|
1272
1535
|
const entries = [];
|
|
@@ -1312,7 +1575,7 @@ function validateMemoryText(text) {
|
|
|
1312
1575
|
}
|
|
1313
1576
|
async function readScope(scope, root, environment) {
|
|
1314
1577
|
try {
|
|
1315
|
-
return parseMemoryFile(await
|
|
1578
|
+
return parseMemoryFile(await fs3.readFile(memoryFile(scope, root, environment), "utf8"), scope);
|
|
1316
1579
|
} catch {
|
|
1317
1580
|
return [];
|
|
1318
1581
|
}
|
|
@@ -1323,10 +1586,10 @@ async function loadMemories(root, environment) {
|
|
|
1323
1586
|
}
|
|
1324
1587
|
async function writeScope(scope, entries, root, environment) {
|
|
1325
1588
|
const file = memoryFile(scope, root, environment);
|
|
1326
|
-
await
|
|
1589
|
+
await fs3.mkdir(path3.dirname(file), { recursive: true, mode: 448 });
|
|
1327
1590
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
1328
|
-
await
|
|
1329
|
-
await
|
|
1591
|
+
await fs3.writeFile(temporary, formatMemoryFile(entries), { encoding: "utf8", mode: 384 });
|
|
1592
|
+
await fs3.rename(temporary, file);
|
|
1330
1593
|
return file;
|
|
1331
1594
|
}
|
|
1332
1595
|
async function addMemory(scope, text, root, environment, options = {}) {
|
|
@@ -1523,14 +1786,14 @@ function describeEnvironment(report) {
|
|
|
1523
1786
|
}
|
|
1524
1787
|
|
|
1525
1788
|
// packages/core/src/cli/prompt.ts
|
|
1526
|
-
import { createHash as
|
|
1527
|
-
import { promises as
|
|
1789
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1790
|
+
import { promises as fs5 } from "node:fs";
|
|
1528
1791
|
import os3 from "node:os";
|
|
1529
|
-
import
|
|
1792
|
+
import path5 from "node:path";
|
|
1530
1793
|
|
|
1531
1794
|
// packages/core/src/cli/workspace.ts
|
|
1532
|
-
import { promises as
|
|
1533
|
-
import
|
|
1795
|
+
import { promises as fs4 } from "node:fs";
|
|
1796
|
+
import path4 from "node:path";
|
|
1534
1797
|
var DEFAULT_WORKSPACE_LIMITS = {
|
|
1535
1798
|
maxReadBytes: 512000,
|
|
1536
1799
|
maxWriteBytes: 512000,
|
|
@@ -1546,27 +1809,27 @@ class WorkspaceViolation extends Error {
|
|
|
1546
1809
|
function resolveInWorkspace(root, candidate) {
|
|
1547
1810
|
if (typeof candidate !== "string" || !candidate.trim())
|
|
1548
1811
|
throw new WorkspaceViolation("path must be a non-empty string");
|
|
1549
|
-
const absoluteRoot =
|
|
1550
|
-
const resolved =
|
|
1551
|
-
if (resolved !== absoluteRoot && !resolved.startsWith(absoluteRoot +
|
|
1812
|
+
const absoluteRoot = path4.resolve(root);
|
|
1813
|
+
const resolved = path4.resolve(absoluteRoot, candidate);
|
|
1814
|
+
if (resolved !== absoluteRoot && !resolved.startsWith(absoluteRoot + path4.sep)) {
|
|
1552
1815
|
throw new WorkspaceViolation(`path escapes the workspace root: ${candidate}`);
|
|
1553
1816
|
}
|
|
1554
1817
|
return resolved;
|
|
1555
1818
|
}
|
|
1556
1819
|
async function realPathWithin(root, candidate) {
|
|
1557
1820
|
const resolved = resolveInWorkspace(root, candidate);
|
|
1558
|
-
const absoluteRoot = await
|
|
1559
|
-
const real = await
|
|
1821
|
+
const absoluteRoot = await fs4.realpath(path4.resolve(root)).catch(() => path4.resolve(root));
|
|
1822
|
+
const real = await fs4.realpath(resolved).catch(() => null);
|
|
1560
1823
|
if (real === null)
|
|
1561
1824
|
return resolved;
|
|
1562
|
-
if (real !== absoluteRoot && !real.startsWith(absoluteRoot +
|
|
1825
|
+
if (real !== absoluteRoot && !real.startsWith(absoluteRoot + path4.sep)) {
|
|
1563
1826
|
throw new WorkspaceViolation(`path resolves outside the workspace root: ${candidate}`);
|
|
1564
1827
|
}
|
|
1565
1828
|
return resolved;
|
|
1566
1829
|
}
|
|
1567
1830
|
function displayPath(root, absolute) {
|
|
1568
|
-
const relative =
|
|
1569
|
-
return relative === "" ? "." : relative.split(
|
|
1831
|
+
const relative = path4.relative(path4.resolve(root), absolute);
|
|
1832
|
+
return relative === "" ? "." : relative.split(path4.sep).join("/");
|
|
1570
1833
|
}
|
|
1571
1834
|
var BINARY_SNIFF_BYTES = 4096;
|
|
1572
1835
|
function looksBinary(buffer) {
|
|
@@ -1575,7 +1838,7 @@ function looksBinary(buffer) {
|
|
|
1575
1838
|
async function readTextFile(root, candidate, options = {}) {
|
|
1576
1839
|
const limits = options.limits ?? DEFAULT_WORKSPACE_LIMITS;
|
|
1577
1840
|
const absolute = await realPathWithin(root, candidate);
|
|
1578
|
-
const stat = await
|
|
1841
|
+
const stat = await fs4.stat(absolute).catch((error) => {
|
|
1579
1842
|
if (error.code === "ENOENT")
|
|
1580
1843
|
throw new WorkspaceViolation(`${displayPath(root, absolute)} does not exist`);
|
|
1581
1844
|
throw error;
|
|
@@ -1585,7 +1848,7 @@ async function readTextFile(root, candidate, options = {}) {
|
|
|
1585
1848
|
if (stat.size > limits.maxReadBytes) {
|
|
1586
1849
|
throw new WorkspaceViolation(`${displayPath(root, absolute)} is ${stat.size} bytes, above the ${limits.maxReadBytes}-byte read limit`);
|
|
1587
1850
|
}
|
|
1588
|
-
const buffer = await
|
|
1851
|
+
const buffer = await fs4.readFile(absolute);
|
|
1589
1852
|
if (looksBinary(buffer))
|
|
1590
1853
|
throw new WorkspaceViolation(`${displayPath(root, absolute)} looks like a binary file`);
|
|
1591
1854
|
const text = buffer.toString("utf8");
|
|
@@ -1613,9 +1876,9 @@ async function writeTextFile(root, candidate, content, limits = DEFAULT_WORKSPAC
|
|
|
1613
1876
|
if (bytes > limits.maxWriteBytes)
|
|
1614
1877
|
throw new WorkspaceViolation(`content is ${bytes} bytes, above the ${limits.maxWriteBytes}-byte write limit`);
|
|
1615
1878
|
const absolute = resolveInWorkspace(root, candidate);
|
|
1616
|
-
await realPathWithin(root,
|
|
1617
|
-
await
|
|
1618
|
-
await
|
|
1879
|
+
await realPathWithin(root, path4.dirname(candidate));
|
|
1880
|
+
await fs4.mkdir(path4.dirname(absolute), { recursive: true });
|
|
1881
|
+
await fs4.writeFile(absolute, content, "utf8");
|
|
1619
1882
|
return { path: displayPath(root, absolute), bytesWritten: bytes };
|
|
1620
1883
|
}
|
|
1621
1884
|
async function editTextFile(root, candidate, oldText, newText, options = {}) {
|
|
@@ -1639,7 +1902,7 @@ async function editTextFile(root, candidate, oldText, newText, options = {}) {
|
|
|
1639
1902
|
var WALK_CONCURRENCY = 32;
|
|
1640
1903
|
var GREP_CONCURRENCY = 32;
|
|
1641
1904
|
async function* walkWorkspace(root, limits = DEFAULT_WORKSPACE_LIMITS, maxEntries = 20000) {
|
|
1642
|
-
const absoluteRoot =
|
|
1905
|
+
const absoluteRoot = path4.resolve(root);
|
|
1643
1906
|
const ignored = new Set(limits.ignoredDirectories);
|
|
1644
1907
|
let level = [absoluteRoot];
|
|
1645
1908
|
let seen = 0;
|
|
@@ -1649,7 +1912,7 @@ async function* walkWorkspace(root, limits = DEFAULT_WORKSPACE_LIMITS, maxEntrie
|
|
|
1649
1912
|
const batch = level.slice(start, start + WALK_CONCURRENCY);
|
|
1650
1913
|
const reads = await Promise.all(batch.map(async (directory) => {
|
|
1651
1914
|
try {
|
|
1652
|
-
return { directory, entries: await
|
|
1915
|
+
return { directory, entries: await fs4.readdir(directory, { withFileTypes: true }) };
|
|
1653
1916
|
} catch {
|
|
1654
1917
|
return { directory, entries: [] };
|
|
1655
1918
|
}
|
|
@@ -1658,7 +1921,7 @@ async function* walkWorkspace(root, limits = DEFAULT_WORKSPACE_LIMITS, maxEntrie
|
|
|
1658
1921
|
for (const entry of entries) {
|
|
1659
1922
|
if (seen >= maxEntries)
|
|
1660
1923
|
return;
|
|
1661
|
-
const absolute =
|
|
1924
|
+
const absolute = path4.join(directory, entry.name);
|
|
1662
1925
|
const isDirectory = entry.isDirectory();
|
|
1663
1926
|
if (isDirectory && ignored.has(entry.name))
|
|
1664
1927
|
continue;
|
|
@@ -1734,10 +1997,10 @@ async function grepWorkspace(root, query, options = {}) {
|
|
|
1734
1997
|
const scan = async (entry) => {
|
|
1735
1998
|
let buffer;
|
|
1736
1999
|
try {
|
|
1737
|
-
const stat = await
|
|
2000
|
+
const stat = await fs4.stat(entry.absolute);
|
|
1738
2001
|
if (stat.size > limits.maxReadBytes)
|
|
1739
2002
|
return [];
|
|
1740
|
-
buffer = await
|
|
2003
|
+
buffer = await fs4.readFile(entry.absolute);
|
|
1741
2004
|
} catch {
|
|
1742
2005
|
return [];
|
|
1743
2006
|
}
|
|
@@ -2488,13 +2751,13 @@ function playbookFor(id) {
|
|
|
2488
2751
|
// packages/core/src/cli/prompt.ts
|
|
2489
2752
|
var INSTRUCTION_FILES = ["AGENTS.override.md", "ARCHYMEDES.md", "AGENTS.md", "CLAUDE.md", ".archymedesrules"];
|
|
2490
2753
|
async function instructionBoundary(root) {
|
|
2491
|
-
const requestedRoot =
|
|
2492
|
-
const temporaryRoot =
|
|
2493
|
-
let current =
|
|
2754
|
+
const requestedRoot = path5.resolve(root);
|
|
2755
|
+
const temporaryRoot = path5.resolve(os3.tmpdir());
|
|
2756
|
+
let current = path5.resolve(root);
|
|
2494
2757
|
while (true) {
|
|
2495
|
-
if (current !== temporaryRoot && await
|
|
2758
|
+
if (current !== temporaryRoot && await fs5.stat(path5.join(current, ".git")).then(() => true).catch(() => false))
|
|
2496
2759
|
return current;
|
|
2497
|
-
const parent =
|
|
2760
|
+
const parent = path5.dirname(current);
|
|
2498
2761
|
if (current === temporaryRoot || parent === current)
|
|
2499
2762
|
return requestedRoot;
|
|
2500
2763
|
current = parent;
|
|
@@ -2504,7 +2767,7 @@ async function collectProjectContext(root, maxInstructionChars = 8000) {
|
|
|
2504
2767
|
const context = { root, instructions: null, instructionsFile: null, instructionSources: [], layout: [], packageScripts: [], gitBranch: null };
|
|
2505
2768
|
const boundary = await instructionBoundary(root);
|
|
2506
2769
|
const directories = [];
|
|
2507
|
-
for (let current =
|
|
2770
|
+
for (let current = path5.resolve(root);; current = path5.dirname(current)) {
|
|
2508
2771
|
directories.unshift(current);
|
|
2509
2772
|
if (current === boundary)
|
|
2510
2773
|
break;
|
|
@@ -2513,9 +2776,9 @@ async function collectProjectContext(root, maxInstructionChars = 8000) {
|
|
|
2513
2776
|
for (const directory of directories) {
|
|
2514
2777
|
for (const candidate of INSTRUCTION_FILES) {
|
|
2515
2778
|
try {
|
|
2516
|
-
const file =
|
|
2517
|
-
const text = await
|
|
2518
|
-
discovered.push({ file, relative:
|
|
2779
|
+
const file = path5.join(directory, candidate);
|
|
2780
|
+
const text = await fs5.readFile(file, "utf8");
|
|
2781
|
+
discovered.push({ file, relative: path5.relative(boundary, file).split(path5.sep).join("/") || candidate, text });
|
|
2519
2782
|
break;
|
|
2520
2783
|
} catch {}
|
|
2521
2784
|
}
|
|
@@ -2529,7 +2792,7 @@ async function collectProjectContext(root, maxInstructionChars = 8000) {
|
|
|
2529
2792
|
selected.unshift({
|
|
2530
2793
|
path: item.relative,
|
|
2531
2794
|
content,
|
|
2532
|
-
sha256:
|
|
2795
|
+
sha256: createHash4("sha256").update(item.text).digest("hex"),
|
|
2533
2796
|
truncated: content.length < item.text.length
|
|
2534
2797
|
});
|
|
2535
2798
|
remaining -= content.length;
|
|
@@ -2543,16 +2806,16 @@ ${source.content}`).join(`
|
|
|
2543
2806
|
context.instructionSources = selected.map(({ path: sourcePath, sha256, truncated }) => ({ path: sourcePath, sha256, truncated }));
|
|
2544
2807
|
}
|
|
2545
2808
|
try {
|
|
2546
|
-
const entries = await
|
|
2809
|
+
const entries = await fs5.readdir(root, { withFileTypes: true });
|
|
2547
2810
|
const ignored = new Set([...DEFAULT_WORKSPACE_LIMITS.ignoredDirectories ?? [], "coverage", "test-results", "tmp", ".turbo"]);
|
|
2548
2811
|
context.layout = entries.filter((entry) => !entry.name.startsWith(".") || entry.name === ".github").filter((entry) => !(entry.isDirectory() && ignored.has(entry.name))).map((entry) => entry.isDirectory() ? `${entry.name}/` : entry.name).sort().slice(0, 60);
|
|
2549
2812
|
} catch {}
|
|
2550
2813
|
try {
|
|
2551
|
-
const manifest = JSON.parse(await
|
|
2814
|
+
const manifest = JSON.parse(await fs5.readFile(path5.join(root, "package.json"), "utf8"));
|
|
2552
2815
|
context.packageScripts = Object.keys(manifest.scripts ?? {}).slice(0, 30);
|
|
2553
2816
|
} catch {}
|
|
2554
2817
|
try {
|
|
2555
|
-
const head = await
|
|
2818
|
+
const head = await fs5.readFile(path5.join(boundary, ".git", "HEAD"), "utf8");
|
|
2556
2819
|
const match = head.match(/ref:\s*refs\/heads\/(.+)/);
|
|
2557
2820
|
context.gitBranch = match?.[1]?.trim() ?? null;
|
|
2558
2821
|
} catch {}
|
|
@@ -2703,9 +2966,97 @@ ${context.instructions.trim()}`);
|
|
|
2703
2966
|
}
|
|
2704
2967
|
|
|
2705
2968
|
// packages/core/src/cli/protocol.ts
|
|
2706
|
-
import { createHash as
|
|
2707
|
-
import { promises as
|
|
2708
|
-
import
|
|
2969
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
2970
|
+
import { promises as fs7 } from "node:fs";
|
|
2971
|
+
import path7 from "node:path";
|
|
2972
|
+
|
|
2973
|
+
// packages/core/src/cli/file-ownership.ts
|
|
2974
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
2975
|
+
import { promises as fs6 } from "node:fs";
|
|
2976
|
+
import os4 from "node:os";
|
|
2977
|
+
import path6 from "node:path";
|
|
2978
|
+
async function processStart(pid) {
|
|
2979
|
+
if (process.platform !== "linux")
|
|
2980
|
+
return;
|
|
2981
|
+
try {
|
|
2982
|
+
const stat = await fs6.readFile(`/proc/${pid}/stat`, "utf8");
|
|
2983
|
+
const boot = await fs6.readFile("/proc/sys/kernel/random/boot_id", "utf8");
|
|
2984
|
+
return `${boot.trim()}:${stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]}`;
|
|
2985
|
+
} catch {
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
async function readOwner(file) {
|
|
2990
|
+
try {
|
|
2991
|
+
const owner = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
2992
|
+
if (!owner || typeof owner.token !== "string" || !Number.isSafeInteger(owner.pid) || owner.pid < 1 || typeof owner.host !== "string") {
|
|
2993
|
+
throw new Error(`Invalid ownership record: ${file}`);
|
|
2994
|
+
}
|
|
2995
|
+
return owner;
|
|
2996
|
+
} catch (error) {
|
|
2997
|
+
if (error.code === "ENOENT")
|
|
2998
|
+
return null;
|
|
2999
|
+
throw error;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
async function isAlive(owner) {
|
|
3003
|
+
if (owner.host !== os4.hostname())
|
|
3004
|
+
return true;
|
|
3005
|
+
try {
|
|
3006
|
+
process.kill(owner.pid, 0);
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
return error.code !== "ESRCH";
|
|
3009
|
+
}
|
|
3010
|
+
const started = await processStart(owner.pid);
|
|
3011
|
+
return !owner.started || !started || owner.started === started;
|
|
3012
|
+
}
|
|
3013
|
+
async function acquireFileOwnership(file, depth = 0) {
|
|
3014
|
+
if (depth > 8)
|
|
3015
|
+
throw new Error(`Ownership recovery requires inspection: ${file}`);
|
|
3016
|
+
await fs6.mkdir(path6.dirname(file), { recursive: true });
|
|
3017
|
+
const owner = { token: randomUUID4(), pid: process.pid, host: os4.hostname(), started: await processStart(process.pid) };
|
|
3018
|
+
const candidate = `${file}.${owner.token}.tmp`;
|
|
3019
|
+
const handle = await fs6.open(candidate, "wx", 384);
|
|
3020
|
+
try {
|
|
3021
|
+
await handle.writeFile(JSON.stringify(owner));
|
|
3022
|
+
await handle.sync();
|
|
3023
|
+
} finally {
|
|
3024
|
+
await handle.close();
|
|
3025
|
+
}
|
|
3026
|
+
try {
|
|
3027
|
+
for (let attempt = 0;attempt < 8; attempt++) {
|
|
3028
|
+
try {
|
|
3029
|
+
await fs6.link(candidate, file);
|
|
3030
|
+
return async () => {
|
|
3031
|
+
if ((await readOwner(file))?.token === owner.token)
|
|
3032
|
+
await fs6.unlink(file);
|
|
3033
|
+
};
|
|
3034
|
+
} catch (error) {
|
|
3035
|
+
if (error.code !== "EEXIST")
|
|
3036
|
+
throw error;
|
|
3037
|
+
}
|
|
3038
|
+
const current = await readOwner(file);
|
|
3039
|
+
if (!current)
|
|
3040
|
+
continue;
|
|
3041
|
+
if (await isAlive(current))
|
|
3042
|
+
throw new Error(`Session is already owned by process ${current.pid}; close or detach the other writer first.`);
|
|
3043
|
+
const release = await acquireFileOwnership(`${file}.reap`, depth + 1);
|
|
3044
|
+
try {
|
|
3045
|
+
if ((await readOwner(file))?.token === current.token)
|
|
3046
|
+
await fs6.unlink(file);
|
|
3047
|
+
} finally {
|
|
3048
|
+
await release();
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
throw new Error(`Could not acquire session ownership: ${file}`);
|
|
3052
|
+
} finally {
|
|
3053
|
+
await fs6.unlink(candidate).catch(() => {
|
|
3054
|
+
return;
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// packages/core/src/cli/protocol.ts
|
|
2709
3060
|
var ARCHYMEDES_PROTOCOL_VERSION = 1;
|
|
2710
3061
|
var JOURNAL_GENESIS_HASH = "0".repeat(64);
|
|
2711
3062
|
var TRANSITIONS = {
|
|
@@ -2737,9 +3088,9 @@ function boundedJournalValue(value, key = "") {
|
|
|
2737
3088
|
const redacted = redactJournalString(value);
|
|
2738
3089
|
if (redacted.length <= 4000)
|
|
2739
3090
|
return redacted;
|
|
2740
|
-
const
|
|
3091
|
+
const digest2 = createHash5("sha256").update(value).digest("hex");
|
|
2741
3092
|
return `${redacted.slice(0, 4000)}
|
|
2742
|
-
...[${redacted.length - 4000} chars omitted; original sha256=${
|
|
3093
|
+
...[${redacted.length - 4000} chars omitted; original sha256=${digest2}]`;
|
|
2743
3094
|
}
|
|
2744
3095
|
if (Array.isArray(value))
|
|
2745
3096
|
return value.map((item) => boundedJournalValue(item));
|
|
@@ -2761,12 +3112,12 @@ function runtimeEventForJournal(event) {
|
|
|
2761
3112
|
return event;
|
|
2762
3113
|
}
|
|
2763
3114
|
function hashEnvelope(envelope) {
|
|
2764
|
-
return
|
|
3115
|
+
return createHash5("sha256").update(JSON.stringify(envelope)).digest("hex");
|
|
2765
3116
|
}
|
|
2766
3117
|
function eventJournalPath(root, sessionId) {
|
|
2767
3118
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(sessionId))
|
|
2768
3119
|
throw new Error("Session id contains unsafe characters");
|
|
2769
|
-
return
|
|
3120
|
+
return path7.join(root, ".archymedes", "events", `${sessionId}.jsonl`);
|
|
2770
3121
|
}
|
|
2771
3122
|
async function readEventJournal(root, sessionId) {
|
|
2772
3123
|
return (await readJournalFile(root, sessionId)).events;
|
|
@@ -2775,7 +3126,7 @@ async function readJournalFile(root, sessionId) {
|
|
|
2775
3126
|
const file = eventJournalPath(root, sessionId);
|
|
2776
3127
|
let text;
|
|
2777
3128
|
try {
|
|
2778
|
-
text = await
|
|
3129
|
+
text = await fs7.readFile(file, "utf8");
|
|
2779
3130
|
} catch (error) {
|
|
2780
3131
|
if (error.code === "ENOENT")
|
|
2781
3132
|
return { events: [], text: "", completeText: "" };
|
|
@@ -2806,6 +3157,8 @@ class EventJournal {
|
|
|
2806
3157
|
root;
|
|
2807
3158
|
sessionId;
|
|
2808
3159
|
handle = null;
|
|
3160
|
+
releaseOwnership;
|
|
3161
|
+
closed = false;
|
|
2809
3162
|
sequence = 0;
|
|
2810
3163
|
previousHash = JOURNAL_GENESIS_HASH;
|
|
2811
3164
|
initialized = null;
|
|
@@ -2815,20 +3168,34 @@ class EventJournal {
|
|
|
2815
3168
|
this.sessionId = sessionId;
|
|
2816
3169
|
}
|
|
2817
3170
|
async initialize() {
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
await
|
|
2825
|
-
|
|
3171
|
+
this.releaseOwnership = await acquireFileOwnership(`${eventJournalPath(this.root, this.sessionId)}.owner`);
|
|
3172
|
+
try {
|
|
3173
|
+
const { events, text, completeText } = await readJournalFile(this.root, this.sessionId);
|
|
3174
|
+
this.sequence = events.length;
|
|
3175
|
+
this.previousHash = events.at(-1)?.hash ?? JOURNAL_GENESIS_HASH;
|
|
3176
|
+
const file = eventJournalPath(this.root, this.sessionId);
|
|
3177
|
+
await fs7.mkdir(path7.dirname(file), { recursive: true });
|
|
3178
|
+
if (text.length !== completeText.length)
|
|
3179
|
+
await fs7.truncate(file, Buffer.byteLength(completeText, "utf8"));
|
|
3180
|
+
this.handle = await fs7.open(file, "a", 384);
|
|
3181
|
+
} catch (error) {
|
|
3182
|
+
await this.releaseOwnership();
|
|
3183
|
+
this.releaseOwnership = undefined;
|
|
3184
|
+
throw error;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
async open() {
|
|
3188
|
+
if (this.closed)
|
|
3189
|
+
throw new Error("Session journal is closed");
|
|
3190
|
+
this.initialized ??= this.initialize();
|
|
3191
|
+
await this.initialized;
|
|
2826
3192
|
}
|
|
2827
3193
|
append(payload, options = {}) {
|
|
3194
|
+
if (this.closed)
|
|
3195
|
+
return Promise.reject(new Error("Session journal is closed"));
|
|
2828
3196
|
let written;
|
|
2829
3197
|
const operation = this.tail.then(async () => {
|
|
2830
|
-
|
|
2831
|
-
await this.initialized;
|
|
3198
|
+
await this.open();
|
|
2832
3199
|
const withoutHash = {
|
|
2833
3200
|
protocolVersion: ARCHYMEDES_PROTOCOL_VERSION,
|
|
2834
3201
|
sequence: this.sequence + 1,
|
|
@@ -2853,19 +3220,29 @@ class EventJournal {
|
|
|
2853
3220
|
await this.handle?.sync();
|
|
2854
3221
|
}
|
|
2855
3222
|
async close() {
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
3223
|
+
try {
|
|
3224
|
+
await this.initialized;
|
|
3225
|
+
await this.flush();
|
|
3226
|
+
} finally {
|
|
3227
|
+
this.closed = true;
|
|
3228
|
+
try {
|
|
3229
|
+
await this.handle?.close();
|
|
3230
|
+
} finally {
|
|
3231
|
+
this.handle = null;
|
|
3232
|
+
await this.releaseOwnership?.();
|
|
3233
|
+
this.releaseOwnership = undefined;
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
2859
3236
|
}
|
|
2860
3237
|
}
|
|
2861
3238
|
|
|
2862
3239
|
// packages/core/src/cli/session.ts
|
|
2863
|
-
import { createHash as
|
|
2864
|
-
import { promises as
|
|
2865
|
-
import
|
|
3240
|
+
import { createHash as createHash6, randomUUID as randomUUID5 } from "node:crypto";
|
|
3241
|
+
import { promises as fs8 } from "node:fs";
|
|
3242
|
+
import path8 from "node:path";
|
|
2866
3243
|
var SESSION_SCHEMA_VERSION = 2;
|
|
2867
3244
|
function sessionDirectory(root) {
|
|
2868
|
-
return
|
|
3245
|
+
return path8.join(root, ".archymedes", "sessions");
|
|
2869
3246
|
}
|
|
2870
3247
|
function newSessionId(now = new Date) {
|
|
2871
3248
|
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
@@ -2887,26 +3264,26 @@ function canonicalize2(value) {
|
|
|
2887
3264
|
return null;
|
|
2888
3265
|
}
|
|
2889
3266
|
function integrityFor(record) {
|
|
2890
|
-
return
|
|
3267
|
+
return createHash6("sha256").update(JSON.stringify(canonicalize2(record))).digest("hex");
|
|
2891
3268
|
}
|
|
2892
3269
|
async function acquireSessionLock(lockFile) {
|
|
2893
3270
|
for (let attempt = 0;attempt < 40; attempt += 1) {
|
|
2894
3271
|
try {
|
|
2895
|
-
const handle = await
|
|
3272
|
+
const handle = await fs8.open(lockFile, "wx", 384);
|
|
2896
3273
|
await handle.writeFile(`${process.pid}
|
|
2897
3274
|
`, "utf8");
|
|
2898
3275
|
await handle.close();
|
|
2899
3276
|
return async () => {
|
|
2900
|
-
await
|
|
3277
|
+
await fs8.unlink(lockFile).catch(() => {
|
|
2901
3278
|
return;
|
|
2902
3279
|
});
|
|
2903
3280
|
};
|
|
2904
3281
|
} catch (error) {
|
|
2905
3282
|
if (error.code !== "EEXIST")
|
|
2906
3283
|
throw error;
|
|
2907
|
-
const stat = await
|
|
3284
|
+
const stat = await fs8.stat(lockFile).catch(() => null);
|
|
2908
3285
|
if (stat && Date.now() - stat.mtimeMs > 30000) {
|
|
2909
|
-
await
|
|
3286
|
+
await fs8.unlink(lockFile).catch(() => {
|
|
2910
3287
|
return;
|
|
2911
3288
|
});
|
|
2912
3289
|
continue;
|
|
@@ -2919,13 +3296,13 @@ async function acquireSessionLock(lockFile) {
|
|
|
2919
3296
|
async function saveSession(record) {
|
|
2920
3297
|
assertSessionId(record.id);
|
|
2921
3298
|
const directory = sessionDirectory(record.root);
|
|
2922
|
-
await
|
|
2923
|
-
const file =
|
|
3299
|
+
await fs8.mkdir(directory, { recursive: true });
|
|
3300
|
+
const file = path8.join(directory, `${record.id}.json`);
|
|
2924
3301
|
const lockFile = `${file}.lock`;
|
|
2925
3302
|
const release = await acquireSessionLock(lockFile);
|
|
2926
|
-
const temporary = `${file}.${process.pid}.${
|
|
3303
|
+
const temporary = `${file}.${process.pid}.${randomUUID5()}.tmp`;
|
|
2927
3304
|
try {
|
|
2928
|
-
const fileExists = await
|
|
3305
|
+
const fileExists = await fs8.stat(file).then(() => true).catch(() => false);
|
|
2929
3306
|
const current = await loadSession(record.root, record.id);
|
|
2930
3307
|
if (fileExists && !current)
|
|
2931
3308
|
throw new Error("Existing session is corrupt or incompatible; refusing to overwrite it");
|
|
@@ -2940,7 +3317,7 @@ async function saveSession(record) {
|
|
|
2940
3317
|
};
|
|
2941
3318
|
delete withoutIntegrity.integrity;
|
|
2942
3319
|
const next = { ...withoutIntegrity, integrity: integrityFor(withoutIntegrity) };
|
|
2943
|
-
const handle = await
|
|
3320
|
+
const handle = await fs8.open(temporary, "wx", 384);
|
|
2944
3321
|
try {
|
|
2945
3322
|
await handle.writeFile(`${JSON.stringify(next, null, 2)}
|
|
2946
3323
|
`, "utf8");
|
|
@@ -2948,8 +3325,8 @@ async function saveSession(record) {
|
|
|
2948
3325
|
} finally {
|
|
2949
3326
|
await handle.close();
|
|
2950
3327
|
}
|
|
2951
|
-
await
|
|
2952
|
-
const directoryHandle = await
|
|
3328
|
+
await fs8.rename(temporary, file);
|
|
3329
|
+
const directoryHandle = await fs8.open(directory, "r").catch(() => null);
|
|
2953
3330
|
if (directoryHandle) {
|
|
2954
3331
|
await directoryHandle.sync().catch(() => {
|
|
2955
3332
|
return;
|
|
@@ -2959,7 +3336,7 @@ async function saveSession(record) {
|
|
|
2959
3336
|
Object.assign(record, next);
|
|
2960
3337
|
return file;
|
|
2961
3338
|
} finally {
|
|
2962
|
-
await
|
|
3339
|
+
await fs8.unlink(temporary).catch(() => {
|
|
2963
3340
|
return;
|
|
2964
3341
|
});
|
|
2965
3342
|
await release();
|
|
@@ -2968,10 +3345,10 @@ async function saveSession(record) {
|
|
|
2968
3345
|
async function loadSession(root, id) {
|
|
2969
3346
|
try {
|
|
2970
3347
|
assertSessionId(id);
|
|
2971
|
-
const parsed = JSON.parse(await
|
|
3348
|
+
const parsed = JSON.parse(await fs8.readFile(path8.join(sessionDirectory(root), `${id}.json`), "utf8"));
|
|
2972
3349
|
if (!parsed || typeof parsed !== "object" || parsed.id !== id || typeof parsed.root !== "string")
|
|
2973
3350
|
return null;
|
|
2974
|
-
const [storedRoot, requestedRoot] = await Promise.all([parsed.root, root].map(async (candidate) =>
|
|
3351
|
+
const [storedRoot, requestedRoot] = await Promise.all([parsed.root, root].map(async (candidate) => fs8.realpath(path8.resolve(candidate)).catch(() => path8.resolve(candidate))));
|
|
2975
3352
|
const rootsMatch = process.platform === "win32" ? storedRoot.toLocaleLowerCase("en-US") === requestedRoot.toLocaleLowerCase("en-US") : storedRoot === requestedRoot;
|
|
2976
3353
|
if (!rootsMatch)
|
|
2977
3354
|
return null;
|
|
@@ -3214,6 +3591,7 @@ class McpConnection {
|
|
|
3214
3591
|
nextId = 1;
|
|
3215
3592
|
pending = new Map;
|
|
3216
3593
|
initializePromise = null;
|
|
3594
|
+
closedError;
|
|
3217
3595
|
toolsPromise;
|
|
3218
3596
|
constructor(config, requestTimeoutMs = REQUEST_TIMEOUT_MS) {
|
|
3219
3597
|
this.config = config;
|
|
@@ -3222,11 +3600,17 @@ class McpConnection {
|
|
|
3222
3600
|
env: { ...sanitizeCommandEnvironment(process.env), ...config.env },
|
|
3223
3601
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3224
3602
|
});
|
|
3225
|
-
this.child.on("error", (error) => this.
|
|
3226
|
-
this.child.on("
|
|
3603
|
+
this.child.on("error", (error) => this.fail(error));
|
|
3604
|
+
this.child.stdin.on("error", (error) => this.fail(error));
|
|
3605
|
+
this.child.on("close", (code) => this.fail(new Error(`MCP server '${config.id}' exited (code ${code})`)));
|
|
3606
|
+
this.child.stderr.resume();
|
|
3227
3607
|
const lines = createInterface({ input: this.child.stdout });
|
|
3228
3608
|
lines.on("line", (line) => this.handleLine(line));
|
|
3229
3609
|
}
|
|
3610
|
+
fail(error) {
|
|
3611
|
+
this.closedError ??= error;
|
|
3612
|
+
this.rejectAllPending(this.closedError);
|
|
3613
|
+
}
|
|
3230
3614
|
rejectAllPending(error) {
|
|
3231
3615
|
for (const { reject } of this.pending.values())
|
|
3232
3616
|
reject(error);
|
|
@@ -3258,6 +3642,8 @@ class McpConnection {
|
|
|
3258
3642
|
waiting.resolve(parsed.result);
|
|
3259
3643
|
}
|
|
3260
3644
|
request(method, params) {
|
|
3645
|
+
if (this.closedError)
|
|
3646
|
+
return Promise.reject(this.closedError);
|
|
3261
3647
|
const id = this.nextId++;
|
|
3262
3648
|
const line = `${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
3263
3649
|
`;
|
|
@@ -3280,10 +3666,14 @@ class McpConnection {
|
|
|
3280
3666
|
});
|
|
3281
3667
|
}
|
|
3282
3668
|
notify(method, params) {
|
|
3669
|
+
if (this.closedError)
|
|
3670
|
+
throw this.closedError;
|
|
3283
3671
|
this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
|
|
3284
3672
|
`);
|
|
3285
3673
|
}
|
|
3286
3674
|
initialize() {
|
|
3675
|
+
if (this.closedError)
|
|
3676
|
+
return Promise.reject(this.closedError);
|
|
3287
3677
|
this.initializePromise ??= (async () => {
|
|
3288
3678
|
await this.request("initialize", {
|
|
3289
3679
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
@@ -3320,7 +3710,7 @@ class McpConnection {
|
|
|
3320
3710
|
return { content: text || "(no output)", isError: result.isError };
|
|
3321
3711
|
}
|
|
3322
3712
|
close() {
|
|
3323
|
-
this.
|
|
3713
|
+
this.fail(new Error(`MCP server '${this.config.id}' connection closed`));
|
|
3324
3714
|
this.child.kill();
|
|
3325
3715
|
}
|
|
3326
3716
|
}
|
|
@@ -5790,7 +6180,7 @@ class CostLedger {
|
|
|
5790
6180
|
}
|
|
5791
6181
|
|
|
5792
6182
|
// packages/core/src/cli/backends.ts
|
|
5793
|
-
import
|
|
6183
|
+
import path10 from "node:path";
|
|
5794
6184
|
|
|
5795
6185
|
// packages/core/src/sandbox-search.ts
|
|
5796
6186
|
var DEFAULT_IGNORED_DIRECTORIES = [".git", "node_modules", ".next", "dist", "build", "target", "__pycache__", ".venv", "venv", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".turbo", "vendor", ".archymedes"];
|
|
@@ -5871,7 +6261,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
5871
6261
|
import { stat } from "node:fs/promises";
|
|
5872
6262
|
import http from "node:http";
|
|
5873
6263
|
import net from "node:net";
|
|
5874
|
-
import
|
|
6264
|
+
import path9 from "node:path";
|
|
5875
6265
|
var MAX_LOG_BYTES = 128 * 1024;
|
|
5876
6266
|
var DEFAULT_READY_TIMEOUT_MS = 20000;
|
|
5877
6267
|
function appendBounded(current, chunk) {
|
|
@@ -5937,9 +6327,9 @@ class LocalApplicationSupervisor {
|
|
|
5937
6327
|
if (await portIsOpen(request.port))
|
|
5938
6328
|
throw new Error(`Port ${request.port} is already in use by another process.`);
|
|
5939
6329
|
const relativeDirectory = request.directory?.trim() || ".";
|
|
5940
|
-
const directory =
|
|
5941
|
-
const relative =
|
|
5942
|
-
if (relative.startsWith("..") ||
|
|
6330
|
+
const directory = path9.resolve(this.root, relativeDirectory);
|
|
6331
|
+
const relative = path9.relative(path9.resolve(this.root), directory);
|
|
6332
|
+
if (relative.startsWith("..") || path9.isAbsolute(relative))
|
|
5943
6333
|
throw new Error("directory must stay inside the project workspace");
|
|
5944
6334
|
const info = await stat(directory).catch(() => null);
|
|
5945
6335
|
if (!info?.isDirectory())
|
|
@@ -6072,14 +6462,14 @@ class LocalWorkspace {
|
|
|
6072
6462
|
get label() {
|
|
6073
6463
|
return this.root;
|
|
6074
6464
|
}
|
|
6075
|
-
readFile(
|
|
6076
|
-
return readTextFile(this.root,
|
|
6465
|
+
readFile(path11, options = {}) {
|
|
6466
|
+
return readTextFile(this.root, path11, { ...options, limits: this.limits });
|
|
6077
6467
|
}
|
|
6078
|
-
writeFile(
|
|
6079
|
-
return writeTextFile(this.root,
|
|
6468
|
+
writeFile(path11, content) {
|
|
6469
|
+
return writeTextFile(this.root, path11, content, this.limits);
|
|
6080
6470
|
}
|
|
6081
|
-
editFile(
|
|
6082
|
-
return editTextFile(this.root,
|
|
6471
|
+
editFile(path11, oldText, newText, options = {}) {
|
|
6472
|
+
return editTextFile(this.root, path11, oldText, newText, { ...options, limits: this.limits });
|
|
6083
6473
|
}
|
|
6084
6474
|
async list(prefix, depth) {
|
|
6085
6475
|
const entries = [];
|
|
@@ -6100,8 +6490,8 @@ class LocalWorkspace {
|
|
|
6100
6490
|
}
|
|
6101
6491
|
async listConfigFiles(prefix) {
|
|
6102
6492
|
const files = [];
|
|
6103
|
-
const base =
|
|
6104
|
-
if (!base.startsWith(
|
|
6493
|
+
const base = path10.resolve(this.root, prefix);
|
|
6494
|
+
if (!base.startsWith(path10.resolve(this.root)))
|
|
6105
6495
|
return [];
|
|
6106
6496
|
for await (const entry of walkWorkspace(base, { ...this.limits, ignoredDirectories: [] }, CONFIG_FILE_SCAN_LIMIT)) {
|
|
6107
6497
|
if (!entry.isDirectory)
|
|
@@ -6138,7 +6528,7 @@ class LocalWorkspace {
|
|
|
6138
6528
|
}
|
|
6139
6529
|
|
|
6140
6530
|
// packages/core/src/cli/artifacts.ts
|
|
6141
|
-
import { createHash as
|
|
6531
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
6142
6532
|
var ARTIFACT_DIRECTORY = ".archymedes/artifacts";
|
|
6143
6533
|
var MAX_ARTIFACT_BYTES = 480000;
|
|
6144
6534
|
function safeToolName(toolName) {
|
|
@@ -6146,8 +6536,8 @@ function safeToolName(toolName) {
|
|
|
6146
6536
|
return cleaned || "tool";
|
|
6147
6537
|
}
|
|
6148
6538
|
function artifactPathFor(toolName, content) {
|
|
6149
|
-
const
|
|
6150
|
-
return `${ARTIFACT_DIRECTORY}/${safeToolName(toolName)}-${
|
|
6539
|
+
const digest2 = createHash7("sha256").update(content).digest("hex").slice(0, 12);
|
|
6540
|
+
return `${ARTIFACT_DIRECTORY}/${safeToolName(toolName)}-${digest2}.txt`;
|
|
6151
6541
|
}
|
|
6152
6542
|
|
|
6153
6543
|
class WorkspaceArtifactStore {
|
|
@@ -6159,16 +6549,16 @@ class WorkspaceArtifactStore {
|
|
|
6159
6549
|
this.maxBytes = maxBytes;
|
|
6160
6550
|
}
|
|
6161
6551
|
async put(input) {
|
|
6162
|
-
const
|
|
6552
|
+
const path11 = artifactPathFor(input.toolName, input.content);
|
|
6163
6553
|
const full = Buffer.byteLength(input.content, "utf8");
|
|
6164
6554
|
const elided = full > this.maxBytes;
|
|
6165
6555
|
const body = elided ? `${input.content.slice(0, this.maxBytes)}
|
|
6166
6556
|
...[artifact truncated at ${this.maxBytes} bytes; the tool produced ${full}]` : input.content;
|
|
6167
|
-
if (!this.written.has(
|
|
6168
|
-
await this.workspace.writeFile(
|
|
6169
|
-
this.written.add(
|
|
6557
|
+
if (!this.written.has(path11)) {
|
|
6558
|
+
await this.workspace.writeFile(path11, body);
|
|
6559
|
+
this.written.add(path11);
|
|
6170
6560
|
}
|
|
6171
|
-
return { path:
|
|
6561
|
+
return { path: path11, bytes: full, lines: countLines(input.content), elided };
|
|
6172
6562
|
}
|
|
6173
6563
|
}
|
|
6174
6564
|
function countLines(content) {
|
|
@@ -6183,14 +6573,14 @@ function countLines(content) {
|
|
|
6183
6573
|
|
|
6184
6574
|
// packages/core/src/cli/defender-brain.ts
|
|
6185
6575
|
import { access as access2 } from "node:fs/promises";
|
|
6186
|
-
import
|
|
6576
|
+
import path13 from "node:path";
|
|
6187
6577
|
import { fileURLToPath } from "node:url";
|
|
6188
6578
|
|
|
6189
6579
|
// packages/core/src/cli/state-client.ts
|
|
6190
6580
|
import { spawn as spawn5 } from "node:child_process";
|
|
6191
6581
|
import { access } from "node:fs/promises";
|
|
6192
6582
|
import { createRequire } from "node:module";
|
|
6193
|
-
import
|
|
6583
|
+
import path11 from "node:path";
|
|
6194
6584
|
import { createInterface as createInterface2 } from "node:readline";
|
|
6195
6585
|
var ARCHYMEDES_STATE_PROTOCOL_VERSION = 1;
|
|
6196
6586
|
|
|
@@ -6235,7 +6625,7 @@ async function executable(pathname) {
|
|
|
6235
6625
|
}
|
|
6236
6626
|
async function resolveArchymedesStateBinary(environment = process.env) {
|
|
6237
6627
|
if (environment.ARCHYMEDES_STATE_BINARY) {
|
|
6238
|
-
const explicit =
|
|
6628
|
+
const explicit = path11.resolve(environment.ARCHYMEDES_STATE_BINARY);
|
|
6239
6629
|
if (!await executable(explicit))
|
|
6240
6630
|
throw new Error(`ARCHYMEDES_STATE_BINARY is not executable: ${explicit}`);
|
|
6241
6631
|
return explicit;
|
|
@@ -6245,14 +6635,14 @@ async function resolveArchymedesStateBinary(environment = process.env) {
|
|
|
6245
6635
|
try {
|
|
6246
6636
|
const require2 = createRequire(import.meta.url);
|
|
6247
6637
|
const manifest = require2.resolve(`${packageName}/package.json`);
|
|
6248
|
-
const candidate =
|
|
6638
|
+
const candidate = path11.join(path11.dirname(manifest), "bin", process.platform === "win32" ? "archymedes-state.exe" : "archymedes-state");
|
|
6249
6639
|
if (await executable(candidate))
|
|
6250
6640
|
return candidate;
|
|
6251
6641
|
} catch {}
|
|
6252
6642
|
}
|
|
6253
6643
|
const filename = process.platform === "win32" ? "archymedes-state.exe" : "archymedes-state";
|
|
6254
6644
|
for (const profile of ["release", "debug"]) {
|
|
6255
|
-
const candidate =
|
|
6645
|
+
const candidate = path11.resolve(process.cwd(), "packages", "archymedes-state", "target", profile, filename);
|
|
6256
6646
|
if (await executable(candidate))
|
|
6257
6647
|
return candidate;
|
|
6258
6648
|
}
|
|
@@ -6401,25 +6791,25 @@ async function tryConnectArchymedesState(options = {}) {
|
|
|
6401
6791
|
}
|
|
6402
6792
|
|
|
6403
6793
|
// packages/core/src/cli/defender-feed.ts
|
|
6404
|
-
import { createHash as
|
|
6405
|
-
import { promises as
|
|
6406
|
-
import
|
|
6794
|
+
import { createHash as createHash8, createPublicKey, verify as verifySignature } from "node:crypto";
|
|
6795
|
+
import { promises as fs9 } from "node:fs";
|
|
6796
|
+
import path12 from "node:path";
|
|
6407
6797
|
var DEFENDER_FEED_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
6408
6798
|
var DEFENDER_FEED_MAX_BYTES = 8 * 1024 * 1024;
|
|
6409
6799
|
var DEFENDER_FEED_MAX_MANIFEST_BYTES = 64 * 1024;
|
|
6410
6800
|
var DEFENDER_FEED_FILENAME = "knowledge-v1.jsonl";
|
|
6411
6801
|
function feedRoot(environment) {
|
|
6412
|
-
return
|
|
6802
|
+
return path12.join(archymedesConfigDirectory(environment), "defender-brain");
|
|
6413
6803
|
}
|
|
6414
6804
|
function statePath(environment) {
|
|
6415
|
-
return
|
|
6805
|
+
return path12.join(feedRoot(environment), "feed-state.json");
|
|
6416
6806
|
}
|
|
6417
6807
|
function validDigest(value) {
|
|
6418
6808
|
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
6419
6809
|
}
|
|
6420
6810
|
async function readDefenderFeedState(environment) {
|
|
6421
6811
|
try {
|
|
6422
|
-
const value = JSON.parse(await
|
|
6812
|
+
const value = JSON.parse(await fs9.readFile(statePath(environment), "utf8"));
|
|
6423
6813
|
if (value.schemaVersion !== 1 || !Number.isFinite(value.lastCheckedAt))
|
|
6424
6814
|
return null;
|
|
6425
6815
|
return {
|
|
@@ -6522,19 +6912,19 @@ async function boundedResponseBody(response, maximum, label) {
|
|
|
6522
6912
|
return Buffer.concat(chunks, total);
|
|
6523
6913
|
}
|
|
6524
6914
|
async function atomicJson(file, value) {
|
|
6525
|
-
await
|
|
6915
|
+
await fs9.mkdir(path12.dirname(file), { recursive: true });
|
|
6526
6916
|
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
6527
|
-
await
|
|
6917
|
+
await fs9.writeFile(temporary, `${JSON.stringify(value, null, 2)}
|
|
6528
6918
|
`, { encoding: "utf8", mode: 384 });
|
|
6529
|
-
await
|
|
6919
|
+
await fs9.rename(temporary, file);
|
|
6530
6920
|
}
|
|
6531
6921
|
async function activeDirectory(environment, state) {
|
|
6532
6922
|
const current = state ?? await readDefenderFeedState(environment);
|
|
6533
6923
|
if (!current?.activeDigest)
|
|
6534
6924
|
return;
|
|
6535
|
-
const directory =
|
|
6925
|
+
const directory = path12.join(feedRoot(environment), "replicas", current.activeDigest);
|
|
6536
6926
|
try {
|
|
6537
|
-
await
|
|
6927
|
+
await fs9.access(path12.join(directory, DEFENDER_FEED_FILENAME));
|
|
6538
6928
|
return directory;
|
|
6539
6929
|
} catch {
|
|
6540
6930
|
return;
|
|
@@ -6595,26 +6985,26 @@ async function refreshDefenderFeed(options) {
|
|
|
6595
6985
|
const bytes = await boundedResponseBody(corpusResponse, manifest.corpus.bytes, "corpus");
|
|
6596
6986
|
if (bytes.byteLength !== manifest.corpus.bytes)
|
|
6597
6987
|
throw new Error("corpus byte count does not match manifest");
|
|
6598
|
-
if (
|
|
6988
|
+
if (createHash8("sha256").update(bytes).digest("hex") !== manifest.corpus.sha256)
|
|
6599
6989
|
throw new Error("corpus digest does not match manifest");
|
|
6600
6990
|
const records = bytes.toString("utf8").split(`
|
|
6601
6991
|
`).filter((line) => line.trim()).length;
|
|
6602
6992
|
if (records !== manifest.corpus.records)
|
|
6603
6993
|
throw new Error("corpus record count does not match manifest");
|
|
6604
|
-
const replica =
|
|
6605
|
-
await
|
|
6606
|
-
const corpusFile =
|
|
6994
|
+
const replica = path12.join(feedRoot(options.environment), "replicas", manifest.corpus.sha256);
|
|
6995
|
+
await fs9.mkdir(replica, { recursive: true });
|
|
6996
|
+
const corpusFile = path12.join(replica, DEFENDER_FEED_FILENAME);
|
|
6607
6997
|
try {
|
|
6608
|
-
await
|
|
6998
|
+
await fs9.access(corpusFile);
|
|
6609
6999
|
} catch {
|
|
6610
7000
|
const temporary = `${corpusFile}.${process.pid}.${Date.now()}.tmp`;
|
|
6611
|
-
await
|
|
7001
|
+
await fs9.writeFile(temporary, bytes, { mode: 384, flag: "wx" });
|
|
6612
7002
|
try {
|
|
6613
|
-
await
|
|
7003
|
+
await fs9.rename(temporary, corpusFile);
|
|
6614
7004
|
} catch (error) {
|
|
6615
|
-
await
|
|
7005
|
+
await fs9.rm(temporary, { force: true });
|
|
6616
7006
|
try {
|
|
6617
|
-
await
|
|
7007
|
+
await fs9.access(corpusFile);
|
|
6618
7008
|
} catch {
|
|
6619
7009
|
throw error;
|
|
6620
7010
|
}
|
|
@@ -6640,10 +7030,10 @@ async function existingDirectory(candidates) {
|
|
|
6640
7030
|
return null;
|
|
6641
7031
|
}
|
|
6642
7032
|
function defenderKnowledgeCandidates(moduleUrl = import.meta.url) {
|
|
6643
|
-
const directory =
|
|
7033
|
+
const directory = path13.dirname(fileURLToPath(moduleUrl));
|
|
6644
7034
|
return [
|
|
6645
|
-
|
|
6646
|
-
|
|
7035
|
+
path13.join(directory, "defender-knowledge"),
|
|
7036
|
+
path13.resolve(directory, "../../../archymedes-state/defender-knowledge")
|
|
6647
7037
|
];
|
|
6648
7038
|
}
|
|
6649
7039
|
|
|
@@ -6758,6 +7148,8 @@ class ArchymedesAgent {
|
|
|
6758
7148
|
workspace;
|
|
6759
7149
|
permissions;
|
|
6760
7150
|
checkpoints;
|
|
7151
|
+
recovery;
|
|
7152
|
+
ownershipReady;
|
|
6761
7153
|
artifacts;
|
|
6762
7154
|
budgets;
|
|
6763
7155
|
messages = [];
|
|
@@ -6793,8 +7185,8 @@ class ArchymedesAgent {
|
|
|
6793
7185
|
this.workspace = options.workspace ?? new LocalWorkspace(options.root, options.limits);
|
|
6794
7186
|
this.nestedInstructions = new NestedInstructionTracker(this.workspace);
|
|
6795
7187
|
this.artifacts = new WorkspaceArtifactStore(this.workspace);
|
|
6796
|
-
this.defenderBrain = new DefenderBrain(
|
|
6797
|
-
this.checkpoints = new CheckpointStore(options.root,
|
|
7188
|
+
this.defenderBrain = new DefenderBrain(path14.join(options.root, ".archymedes", "security-brain"));
|
|
7189
|
+
this.checkpoints = new CheckpointStore(options.root, path14.join(options.root, ".archymedes", `checkpoint-index-${randomUUID6()}`), options.git);
|
|
6798
7190
|
this.session = {
|
|
6799
7191
|
schemaVersion: 2,
|
|
6800
7192
|
revision: 0,
|
|
@@ -6810,6 +7202,7 @@ class ArchymedesAgent {
|
|
|
6810
7202
|
totalRwf: 0
|
|
6811
7203
|
};
|
|
6812
7204
|
this.journal = new EventJournal(options.root, this.session.id);
|
|
7205
|
+
this.recovery = new HostedRecoveryStore(options.root, this.session.id);
|
|
6813
7206
|
this.permissions = new PermissionLedger(options.mode, async (request) => {
|
|
6814
7207
|
const turnId = this.activeTurnId ?? "turn_unbound";
|
|
6815
7208
|
await this.activeTransition?.("waiting_approval", true);
|
|
@@ -6844,6 +7237,8 @@ class ArchymedesAgent {
|
|
|
6844
7237
|
return structuredClone(this.session);
|
|
6845
7238
|
}
|
|
6846
7239
|
resume(record) {
|
|
7240
|
+
if (this.ownershipReady)
|
|
7241
|
+
throw new Error("Relinquish the current agent before resuming another session");
|
|
6847
7242
|
this.session = { ...record, routingReceipts: mergeRoutingReceipts(record.routingReceipts), mode: this.options.mode };
|
|
6848
7243
|
this.messages = [...record.messages];
|
|
6849
7244
|
this.recalledMemoryKeys.clear();
|
|
@@ -6852,6 +7247,62 @@ class ArchymedesAgent {
|
|
|
6852
7247
|
this.openingObjective = record.messages.find((message) => message.role === "user" && !message.internal && !message.content.startsWith(STANDING_CONSTRAINTS_HEADING) && !message.content.startsWith("[Earlier conversation, summarized]"))?.content ?? (record.title === "Untitled session" ? null : record.title);
|
|
6853
7248
|
this.permissions.restore(record.approvals ?? {});
|
|
6854
7249
|
this.journal = new EventJournal(this.options.root, record.id);
|
|
7250
|
+
this.recovery = new HostedRecoveryStore(this.options.root, record.id);
|
|
7251
|
+
}
|
|
7252
|
+
async acquireOwnership() {
|
|
7253
|
+
this.ownershipReady ??= (async () => {
|
|
7254
|
+
await this.journal.open();
|
|
7255
|
+
const current = await loadSession(this.options.root, this.session.id);
|
|
7256
|
+
const exists = current || await access3(path14.join(this.options.root, ".archymedes", "sessions", `${this.session.id}.json`)).then(() => true).catch((error) => {
|
|
7257
|
+
if (error.code === "ENOENT")
|
|
7258
|
+
return false;
|
|
7259
|
+
throw error;
|
|
7260
|
+
});
|
|
7261
|
+
if (!current && (exists || this.session.revision > 0)) {
|
|
7262
|
+
await this.journal.close();
|
|
7263
|
+
throw new Error("Session snapshot is missing, corrupt or incompatible; refusing to start work.");
|
|
7264
|
+
}
|
|
7265
|
+
if (current && current.revision !== this.session.revision) {
|
|
7266
|
+
await this.journal.close();
|
|
7267
|
+
throw new Error("Session changed since it was loaded; resume it again before writing.");
|
|
7268
|
+
}
|
|
7269
|
+
})();
|
|
7270
|
+
await this.ownershipReady;
|
|
7271
|
+
}
|
|
7272
|
+
async recordHostedUsage() {
|
|
7273
|
+
const responses = this.recovery.responses();
|
|
7274
|
+
if (!responses.length)
|
|
7275
|
+
return;
|
|
7276
|
+
const events = await readEventJournal(this.options.root, this.session.id);
|
|
7277
|
+
const recorded = new Set(events.flatMap(({ payload }) => payload.type === "runtime" && payload.event.type === "model_turn" && payload.event.requestId ? [payload.event.requestId] : []));
|
|
7278
|
+
for (const { requestId, turn } of responses) {
|
|
7279
|
+
if (recorded.has(requestId))
|
|
7280
|
+
continue;
|
|
7281
|
+
await this.journal.append({ type: "runtime", turnId: `recovered_${requestId}`, event: {
|
|
7282
|
+
type: "model_turn",
|
|
7283
|
+
requestId,
|
|
7284
|
+
iteration: 0,
|
|
7285
|
+
responseId: turn.responseId,
|
|
7286
|
+
model: turn.model,
|
|
7287
|
+
toolCallCount: turn.toolCalls.length,
|
|
7288
|
+
usage: turn.usage
|
|
7289
|
+
} }, { durable: true });
|
|
7290
|
+
recorded.add(requestId);
|
|
7291
|
+
}
|
|
7292
|
+
}
|
|
7293
|
+
async recoverPending(signal) {
|
|
7294
|
+
if (this.turnAbort && signal !== this.turnAbort.signal)
|
|
7295
|
+
throw new Error("Cannot recover while a turn is running");
|
|
7296
|
+
await this.acquireOwnership();
|
|
7297
|
+
const recovered = await this.recovery.recover(this.options.model, this.session, signal);
|
|
7298
|
+
if (!recovered)
|
|
7299
|
+
return false;
|
|
7300
|
+
await this.recordHostedUsage();
|
|
7301
|
+
await saveSession(recovered);
|
|
7302
|
+
this.session = recovered;
|
|
7303
|
+
this.messages = [...recovered.messages];
|
|
7304
|
+
await this.recovery.cleanup(recovered.hostedRecoveryBatchId);
|
|
7305
|
+
return true;
|
|
6855
7306
|
}
|
|
6856
7307
|
cancel() {
|
|
6857
7308
|
this.cancelled = true;
|
|
@@ -6883,8 +7334,8 @@ class ArchymedesAgent {
|
|
|
6883
7334
|
listFiles(pattern = "**/*") {
|
|
6884
7335
|
return this.workspace.glob(pattern);
|
|
6885
7336
|
}
|
|
6886
|
-
readFile(
|
|
6887
|
-
return this.workspace.readFile(
|
|
7337
|
+
readFile(path15, options = {}) {
|
|
7338
|
+
return this.workspace.readFile(path15, options);
|
|
6888
7339
|
}
|
|
6889
7340
|
scanSecrets(include) {
|
|
6890
7341
|
return scanWorkspaceForSecrets(this.workspace, include);
|
|
@@ -6974,6 +7425,9 @@ class ArchymedesAgent {
|
|
|
6974
7425
|
return { initialInputTokens, toolCount: scoped.length };
|
|
6975
7426
|
}
|
|
6976
7427
|
async undo(scope = "both") {
|
|
7428
|
+
if (this.turnAbort)
|
|
7429
|
+
throw new Error("Cannot undo while a turn is running");
|
|
7430
|
+
await this.acquireOwnership();
|
|
6977
7431
|
const checkpoint = this.checkpoints.latest();
|
|
6978
7432
|
if (!checkpoint)
|
|
6979
7433
|
return;
|
|
@@ -6995,7 +7449,7 @@ class ArchymedesAgent {
|
|
|
6995
7449
|
const systemPrompt = buildArchymedesSystemPrompt(context, this.options.mode, subTools.map((tool) => tool.name), this.workspace, await this.loadEnvironment());
|
|
6996
7450
|
const reservation = Math.max(0, Math.min(remainingRwf(), this.budgets.maxRwf / 2));
|
|
6997
7451
|
const runtime = new BoundedAgentRuntime({
|
|
6998
|
-
model: this.options.model,
|
|
7452
|
+
model: this.recovery.wrap(this.options.model, () => this.session, this.options.prices, `delegate:${randomUUID6()}`),
|
|
6999
7453
|
tools: subTools,
|
|
7000
7454
|
prices: this.options.prices,
|
|
7001
7455
|
artifacts: this.artifacts,
|
|
@@ -7009,7 +7463,7 @@ class ArchymedesAgent {
|
|
|
7009
7463
|
const result = await runtime.execute({
|
|
7010
7464
|
taskId: `${this.session.id}_delegate`,
|
|
7011
7465
|
runId: this.session.id,
|
|
7012
|
-
stepId: `delegate_${
|
|
7466
|
+
stepId: `delegate_${randomUUID6()}`,
|
|
7013
7467
|
objective: task,
|
|
7014
7468
|
history: [],
|
|
7015
7469
|
systemPrompt,
|
|
@@ -7036,10 +7490,19 @@ class ArchymedesAgent {
|
|
|
7036
7490
|
async send(objective) {
|
|
7037
7491
|
if (!objective.trim())
|
|
7038
7492
|
throw new Error("A request is required");
|
|
7493
|
+
if (this.turnAbort)
|
|
7494
|
+
throw new Error("A turn is already running in this session");
|
|
7039
7495
|
this.cancelled = false;
|
|
7040
7496
|
const turnAbort = new AbortController;
|
|
7041
7497
|
this.turnAbort = turnAbort;
|
|
7042
|
-
|
|
7498
|
+
try {
|
|
7499
|
+
await this.recoverPending(turnAbort.signal);
|
|
7500
|
+
turnAbort.signal.throwIfAborted();
|
|
7501
|
+
} catch (error) {
|
|
7502
|
+
this.turnAbort = null;
|
|
7503
|
+
throw error;
|
|
7504
|
+
}
|
|
7505
|
+
const turnId = `turn_${randomUUID6()}`;
|
|
7043
7506
|
this.activeTurnId = turnId;
|
|
7044
7507
|
let turnStatus = "queued";
|
|
7045
7508
|
const transition = async (to, durable = false) => {
|
|
@@ -7049,6 +7512,10 @@ class ArchymedesAgent {
|
|
|
7049
7512
|
};
|
|
7050
7513
|
this.activeTransition = transition;
|
|
7051
7514
|
try {
|
|
7515
|
+
if (this.options.model.recoveryScope) {
|
|
7516
|
+
this.session.title = this.session.title === "Untitled session" ? titleFromObjective(objective) : this.session.title;
|
|
7517
|
+
await saveSession(this.session);
|
|
7518
|
+
}
|
|
7052
7519
|
await transition("running");
|
|
7053
7520
|
this.openingObjective ??= objective;
|
|
7054
7521
|
this.context = await collectProjectContext(this.options.root);
|
|
@@ -7090,7 +7557,7 @@ ${objective}` : objective;
|
|
|
7090
7557
|
const compaction = await this.compactIfNeeded(turnId, objective, turnAbort.signal);
|
|
7091
7558
|
compactionActualRwf = compaction.actualRwf;
|
|
7092
7559
|
const runtime = new BoundedAgentRuntime({
|
|
7093
|
-
model: this.options.model,
|
|
7560
|
+
model: this.recovery.wrap(this.options.model, () => this.session, this.options.prices, "main"),
|
|
7094
7561
|
tools: scoped,
|
|
7095
7562
|
prices: this.options.prices,
|
|
7096
7563
|
artifacts: this.artifacts,
|
|
@@ -7135,12 +7602,15 @@ ${objective}` : objective;
|
|
|
7135
7602
|
recalledMemoryKeys: [...this.recalledMemoryKeys],
|
|
7136
7603
|
approvals: this.permissions.snapshot(),
|
|
7137
7604
|
totalRwf: this.session.totalRwf + combinedRwf,
|
|
7138
|
-
routingReceipts: mergeRoutingReceipts(this.session.routingReceipts, result.routingReceipts),
|
|
7605
|
+
routingReceipts: mergeRoutingReceipts(this.session.routingReceipts, result.routingReceipts, this.recovery.responses().flatMap(({ requestId, turn }) => turn.routingReceipt ? [{ ...turn.routingReceipt, taskId: turn.routingReceipt.taskId ?? requestId }] : [])),
|
|
7606
|
+
hostedRecoveryBatchId: this.recovery.settledBatchId() ?? this.session.hostedRecoveryBatchId,
|
|
7139
7607
|
updatedAt: Date.now()
|
|
7140
7608
|
};
|
|
7141
7609
|
const terminalStatus = runtimeStatusToTurnStatus(result.status);
|
|
7142
7610
|
await transition(terminalStatus, true);
|
|
7611
|
+
await this.recordHostedUsage();
|
|
7143
7612
|
await saveSession(this.session);
|
|
7613
|
+
await this.recovery.cleanup(this.session.hostedRecoveryBatchId);
|
|
7144
7614
|
return { ...result, usage: combinedUsage, actualModelRwf: combinedRwf, checkpoint };
|
|
7145
7615
|
} catch (error) {
|
|
7146
7616
|
if (isActiveTurnStatus(turnStatus)) {
|
|
@@ -7166,7 +7636,7 @@ ${objective}` : objective;
|
|
|
7166
7636
|
const maximumOutputTokens = affordableOutputTokens([...plan.toSummarize.map((message) => message.content), COMPACTION_INSTRUCTION], Math.min(this.budgets.maxOutputTokens, 4000), this.budgets.maxRwf, this.options.prices);
|
|
7167
7637
|
if (maximumOutputTokens < 1)
|
|
7168
7638
|
return { usage: emptyModelUsage(), actualRwf: 0 };
|
|
7169
|
-
const turn = await this.options.model.complete({
|
|
7639
|
+
const turn = await this.recovery.wrap(this.options.model, () => this.session, this.options.prices, "compaction").complete({
|
|
7170
7640
|
effort: "low",
|
|
7171
7641
|
messages: [...plan.toSummarize, { role: "user", content: COMPACTION_INSTRUCTION }],
|
|
7172
7642
|
tools: [],
|
|
@@ -7481,8 +7951,9 @@ function toolCallTitle(toolName, argumentsValue) {
|
|
|
7481
7951
|
}
|
|
7482
7952
|
|
|
7483
7953
|
// packages/core/src/cli/agent.ts
|
|
7484
|
-
import { randomUUID as
|
|
7485
|
-
import
|
|
7954
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
7955
|
+
import path15 from "node:path";
|
|
7956
|
+
import { access as access4 } from "node:fs/promises";
|
|
7486
7957
|
var DEFAULT_ARCHYMEDES_BUDGETS2 = {
|
|
7487
7958
|
maxIterations: 100,
|
|
7488
7959
|
maxToolCalls: 500,
|
|
@@ -7502,6 +7973,8 @@ class ArchymedesAgent2 {
|
|
|
7502
7973
|
workspace;
|
|
7503
7974
|
permissions;
|
|
7504
7975
|
checkpoints;
|
|
7976
|
+
recovery;
|
|
7977
|
+
ownershipReady;
|
|
7505
7978
|
artifacts;
|
|
7506
7979
|
budgets;
|
|
7507
7980
|
messages = [];
|
|
@@ -7537,8 +8010,8 @@ class ArchymedesAgent2 {
|
|
|
7537
8010
|
this.workspace = options.workspace ?? new LocalWorkspace(options.root, options.limits);
|
|
7538
8011
|
this.nestedInstructions = new NestedInstructionTracker(this.workspace);
|
|
7539
8012
|
this.artifacts = new WorkspaceArtifactStore(this.workspace);
|
|
7540
|
-
this.defenderBrain = new DefenderBrain(
|
|
7541
|
-
this.checkpoints = new CheckpointStore(options.root,
|
|
8013
|
+
this.defenderBrain = new DefenderBrain(path15.join(options.root, ".archymedes", "security-brain"));
|
|
8014
|
+
this.checkpoints = new CheckpointStore(options.root, path15.join(options.root, ".archymedes", `checkpoint-index-${randomUUID7()}`), options.git);
|
|
7542
8015
|
this.session = {
|
|
7543
8016
|
schemaVersion: 2,
|
|
7544
8017
|
revision: 0,
|
|
@@ -7554,6 +8027,7 @@ class ArchymedesAgent2 {
|
|
|
7554
8027
|
totalRwf: 0
|
|
7555
8028
|
};
|
|
7556
8029
|
this.journal = new EventJournal(options.root, this.session.id);
|
|
8030
|
+
this.recovery = new HostedRecoveryStore(options.root, this.session.id);
|
|
7557
8031
|
this.permissions = new PermissionLedger(options.mode, async (request) => {
|
|
7558
8032
|
const turnId = this.activeTurnId ?? "turn_unbound";
|
|
7559
8033
|
await this.activeTransition?.("waiting_approval", true);
|
|
@@ -7588,6 +8062,8 @@ class ArchymedesAgent2 {
|
|
|
7588
8062
|
return structuredClone(this.session);
|
|
7589
8063
|
}
|
|
7590
8064
|
resume(record) {
|
|
8065
|
+
if (this.ownershipReady)
|
|
8066
|
+
throw new Error("Relinquish the current agent before resuming another session");
|
|
7591
8067
|
this.session = { ...record, routingReceipts: mergeRoutingReceipts(record.routingReceipts), mode: this.options.mode };
|
|
7592
8068
|
this.messages = [...record.messages];
|
|
7593
8069
|
this.recalledMemoryKeys.clear();
|
|
@@ -7596,6 +8072,62 @@ class ArchymedesAgent2 {
|
|
|
7596
8072
|
this.openingObjective = record.messages.find((message) => message.role === "user" && !message.internal && !message.content.startsWith(STANDING_CONSTRAINTS_HEADING) && !message.content.startsWith("[Earlier conversation, summarized]"))?.content ?? (record.title === "Untitled session" ? null : record.title);
|
|
7597
8073
|
this.permissions.restore(record.approvals ?? {});
|
|
7598
8074
|
this.journal = new EventJournal(this.options.root, record.id);
|
|
8075
|
+
this.recovery = new HostedRecoveryStore(this.options.root, record.id);
|
|
8076
|
+
}
|
|
8077
|
+
async acquireOwnership() {
|
|
8078
|
+
this.ownershipReady ??= (async () => {
|
|
8079
|
+
await this.journal.open();
|
|
8080
|
+
const current = await loadSession(this.options.root, this.session.id);
|
|
8081
|
+
const exists = current || await access4(path15.join(this.options.root, ".archymedes", "sessions", `${this.session.id}.json`)).then(() => true).catch((error) => {
|
|
8082
|
+
if (error.code === "ENOENT")
|
|
8083
|
+
return false;
|
|
8084
|
+
throw error;
|
|
8085
|
+
});
|
|
8086
|
+
if (!current && (exists || this.session.revision > 0)) {
|
|
8087
|
+
await this.journal.close();
|
|
8088
|
+
throw new Error("Session snapshot is missing, corrupt or incompatible; refusing to start work.");
|
|
8089
|
+
}
|
|
8090
|
+
if (current && current.revision !== this.session.revision) {
|
|
8091
|
+
await this.journal.close();
|
|
8092
|
+
throw new Error("Session changed since it was loaded; resume it again before writing.");
|
|
8093
|
+
}
|
|
8094
|
+
})();
|
|
8095
|
+
await this.ownershipReady;
|
|
8096
|
+
}
|
|
8097
|
+
async recordHostedUsage() {
|
|
8098
|
+
const responses = this.recovery.responses();
|
|
8099
|
+
if (!responses.length)
|
|
8100
|
+
return;
|
|
8101
|
+
const events = await readEventJournal(this.options.root, this.session.id);
|
|
8102
|
+
const recorded = new Set(events.flatMap(({ payload }) => payload.type === "runtime" && payload.event.type === "model_turn" && payload.event.requestId ? [payload.event.requestId] : []));
|
|
8103
|
+
for (const { requestId, turn } of responses) {
|
|
8104
|
+
if (recorded.has(requestId))
|
|
8105
|
+
continue;
|
|
8106
|
+
await this.journal.append({ type: "runtime", turnId: `recovered_${requestId}`, event: {
|
|
8107
|
+
type: "model_turn",
|
|
8108
|
+
requestId,
|
|
8109
|
+
iteration: 0,
|
|
8110
|
+
responseId: turn.responseId,
|
|
8111
|
+
model: turn.model,
|
|
8112
|
+
toolCallCount: turn.toolCalls.length,
|
|
8113
|
+
usage: turn.usage
|
|
8114
|
+
} }, { durable: true });
|
|
8115
|
+
recorded.add(requestId);
|
|
8116
|
+
}
|
|
8117
|
+
}
|
|
8118
|
+
async recoverPending(signal) {
|
|
8119
|
+
if (this.turnAbort && signal !== this.turnAbort.signal)
|
|
8120
|
+
throw new Error("Cannot recover while a turn is running");
|
|
8121
|
+
await this.acquireOwnership();
|
|
8122
|
+
const recovered = await this.recovery.recover(this.options.model, this.session, signal);
|
|
8123
|
+
if (!recovered)
|
|
8124
|
+
return false;
|
|
8125
|
+
await this.recordHostedUsage();
|
|
8126
|
+
await saveSession(recovered);
|
|
8127
|
+
this.session = recovered;
|
|
8128
|
+
this.messages = [...recovered.messages];
|
|
8129
|
+
await this.recovery.cleanup(recovered.hostedRecoveryBatchId);
|
|
8130
|
+
return true;
|
|
7599
8131
|
}
|
|
7600
8132
|
cancel() {
|
|
7601
8133
|
this.cancelled = true;
|
|
@@ -7627,8 +8159,8 @@ class ArchymedesAgent2 {
|
|
|
7627
8159
|
listFiles(pattern = "**/*") {
|
|
7628
8160
|
return this.workspace.glob(pattern);
|
|
7629
8161
|
}
|
|
7630
|
-
readFile(
|
|
7631
|
-
return this.workspace.readFile(
|
|
8162
|
+
readFile(path16, options = {}) {
|
|
8163
|
+
return this.workspace.readFile(path16, options);
|
|
7632
8164
|
}
|
|
7633
8165
|
scanSecrets(include) {
|
|
7634
8166
|
return scanWorkspaceForSecrets(this.workspace, include);
|
|
@@ -7718,6 +8250,9 @@ class ArchymedesAgent2 {
|
|
|
7718
8250
|
return { initialInputTokens, toolCount: scoped.length };
|
|
7719
8251
|
}
|
|
7720
8252
|
async undo(scope = "both") {
|
|
8253
|
+
if (this.turnAbort)
|
|
8254
|
+
throw new Error("Cannot undo while a turn is running");
|
|
8255
|
+
await this.acquireOwnership();
|
|
7721
8256
|
const checkpoint = this.checkpoints.latest();
|
|
7722
8257
|
if (!checkpoint)
|
|
7723
8258
|
return;
|
|
@@ -7739,7 +8274,7 @@ class ArchymedesAgent2 {
|
|
|
7739
8274
|
const systemPrompt = buildArchymedesSystemPrompt(context, this.options.mode, subTools.map((tool) => tool.name), this.workspace, await this.loadEnvironment());
|
|
7740
8275
|
const reservation = Math.max(0, Math.min(remainingRwf(), this.budgets.maxRwf / 2));
|
|
7741
8276
|
const runtime = new BoundedAgentRuntime({
|
|
7742
|
-
model: this.options.model,
|
|
8277
|
+
model: this.recovery.wrap(this.options.model, () => this.session, this.options.prices, `delegate:${randomUUID7()}`),
|
|
7743
8278
|
tools: subTools,
|
|
7744
8279
|
prices: this.options.prices,
|
|
7745
8280
|
artifacts: this.artifacts,
|
|
@@ -7753,7 +8288,7 @@ class ArchymedesAgent2 {
|
|
|
7753
8288
|
const result = await runtime.execute({
|
|
7754
8289
|
taskId: `${this.session.id}_delegate`,
|
|
7755
8290
|
runId: this.session.id,
|
|
7756
|
-
stepId: `delegate_${
|
|
8291
|
+
stepId: `delegate_${randomUUID7()}`,
|
|
7757
8292
|
objective: task,
|
|
7758
8293
|
history: [],
|
|
7759
8294
|
systemPrompt,
|
|
@@ -7780,10 +8315,19 @@ class ArchymedesAgent2 {
|
|
|
7780
8315
|
async send(objective) {
|
|
7781
8316
|
if (!objective.trim())
|
|
7782
8317
|
throw new Error("A request is required");
|
|
8318
|
+
if (this.turnAbort)
|
|
8319
|
+
throw new Error("A turn is already running in this session");
|
|
7783
8320
|
this.cancelled = false;
|
|
7784
8321
|
const turnAbort = new AbortController;
|
|
7785
8322
|
this.turnAbort = turnAbort;
|
|
7786
|
-
|
|
8323
|
+
try {
|
|
8324
|
+
await this.recoverPending(turnAbort.signal);
|
|
8325
|
+
turnAbort.signal.throwIfAborted();
|
|
8326
|
+
} catch (error) {
|
|
8327
|
+
this.turnAbort = null;
|
|
8328
|
+
throw error;
|
|
8329
|
+
}
|
|
8330
|
+
const turnId = `turn_${randomUUID7()}`;
|
|
7787
8331
|
this.activeTurnId = turnId;
|
|
7788
8332
|
let turnStatus = "queued";
|
|
7789
8333
|
const transition = async (to, durable = false) => {
|
|
@@ -7793,6 +8337,10 @@ class ArchymedesAgent2 {
|
|
|
7793
8337
|
};
|
|
7794
8338
|
this.activeTransition = transition;
|
|
7795
8339
|
try {
|
|
8340
|
+
if (this.options.model.recoveryScope) {
|
|
8341
|
+
this.session.title = this.session.title === "Untitled session" ? titleFromObjective(objective) : this.session.title;
|
|
8342
|
+
await saveSession(this.session);
|
|
8343
|
+
}
|
|
7796
8344
|
await transition("running");
|
|
7797
8345
|
this.openingObjective ??= objective;
|
|
7798
8346
|
this.context = await collectProjectContext(this.options.root);
|
|
@@ -7834,7 +8382,7 @@ ${objective}` : objective;
|
|
|
7834
8382
|
const compaction = await this.compactIfNeeded(turnId, objective, turnAbort.signal);
|
|
7835
8383
|
compactionActualRwf = compaction.actualRwf;
|
|
7836
8384
|
const runtime = new BoundedAgentRuntime({
|
|
7837
|
-
model: this.options.model,
|
|
8385
|
+
model: this.recovery.wrap(this.options.model, () => this.session, this.options.prices, "main"),
|
|
7838
8386
|
tools: scoped,
|
|
7839
8387
|
prices: this.options.prices,
|
|
7840
8388
|
artifacts: this.artifacts,
|
|
@@ -7879,12 +8427,15 @@ ${objective}` : objective;
|
|
|
7879
8427
|
recalledMemoryKeys: [...this.recalledMemoryKeys],
|
|
7880
8428
|
approvals: this.permissions.snapshot(),
|
|
7881
8429
|
totalRwf: this.session.totalRwf + combinedRwf,
|
|
7882
|
-
routingReceipts: mergeRoutingReceipts(this.session.routingReceipts, result.routingReceipts),
|
|
8430
|
+
routingReceipts: mergeRoutingReceipts(this.session.routingReceipts, result.routingReceipts, this.recovery.responses().flatMap(({ requestId, turn }) => turn.routingReceipt ? [{ ...turn.routingReceipt, taskId: turn.routingReceipt.taskId ?? requestId }] : [])),
|
|
8431
|
+
hostedRecoveryBatchId: this.recovery.settledBatchId() ?? this.session.hostedRecoveryBatchId,
|
|
7883
8432
|
updatedAt: Date.now()
|
|
7884
8433
|
};
|
|
7885
8434
|
const terminalStatus = runtimeStatusToTurnStatus2(result.status);
|
|
7886
8435
|
await transition(terminalStatus, true);
|
|
8436
|
+
await this.recordHostedUsage();
|
|
7887
8437
|
await saveSession(this.session);
|
|
8438
|
+
await this.recovery.cleanup(this.session.hostedRecoveryBatchId);
|
|
7888
8439
|
return { ...result, usage: combinedUsage, actualModelRwf: combinedRwf, checkpoint };
|
|
7889
8440
|
} catch (error) {
|
|
7890
8441
|
if (isActiveTurnStatus2(turnStatus)) {
|
|
@@ -7910,7 +8461,7 @@ ${objective}` : objective;
|
|
|
7910
8461
|
const maximumOutputTokens = affordableOutputTokens([...plan.toSummarize.map((message) => message.content), COMPACTION_INSTRUCTION], Math.min(this.budgets.maxOutputTokens, 4000), this.budgets.maxRwf, this.options.prices);
|
|
7911
8462
|
if (maximumOutputTokens < 1)
|
|
7912
8463
|
return { usage: emptyModelUsage2(), actualRwf: 0 };
|
|
7913
|
-
const turn = await this.options.model.complete({
|
|
8464
|
+
const turn = await this.recovery.wrap(this.options.model, () => this.session, this.options.prices, "compaction").complete({
|
|
7914
8465
|
effort: "low",
|
|
7915
8466
|
messages: [...plan.toSummarize, { role: "user", content: COMPACTION_INSTRUCTION }],
|
|
7916
8467
|
tools: [],
|
|
@@ -8248,7 +8799,7 @@ class AnthropicAgentTurnProvider {
|
|
|
8248
8799
|
}
|
|
8249
8800
|
|
|
8250
8801
|
// packages/core/src/providers/archymedes-cloud-agent.ts
|
|
8251
|
-
import { randomUUID as
|
|
8802
|
+
import { createHash as createHash9, randomUUID as randomUUID8 } from "node:crypto";
|
|
8252
8803
|
|
|
8253
8804
|
// packages/core/src/providers/routing-plan.ts
|
|
8254
8805
|
function isRecord(value) {
|
|
@@ -8379,6 +8930,7 @@ class ArchymedesCloudError extends Error {
|
|
|
8379
8930
|
class ArchymedesCloudTurnProvider {
|
|
8380
8931
|
options;
|
|
8381
8932
|
capabilities;
|
|
8933
|
+
recoveryScope;
|
|
8382
8934
|
fetchImpl;
|
|
8383
8935
|
completionUrl;
|
|
8384
8936
|
planUrl;
|
|
@@ -8422,6 +8974,18 @@ class ArchymedesCloudTurnProvider {
|
|
|
8422
8974
|
this.planUrl = base.endsWith("/v1") ? `${base}/routes/plan` : `${base}/v1/routes/plan`;
|
|
8423
8975
|
this.balanceUrl = base.endsWith("/v1") ? `${base}/credits/balance` : `${base}/v1/credits/balance`;
|
|
8424
8976
|
this.capabilities = capabilitiesFor(this.model);
|
|
8977
|
+
this.recoveryScope = createHash9("sha256").update(JSON.stringify({
|
|
8978
|
+
version: 1,
|
|
8979
|
+
endpoint: this.completionUrl,
|
|
8980
|
+
token: options.token,
|
|
8981
|
+
model: this.model,
|
|
8982
|
+
maximum: this.maximumMicros,
|
|
8983
|
+
currency: this.currency,
|
|
8984
|
+
policy: this.dataPolicy,
|
|
8985
|
+
quality: this.qualityFloor,
|
|
8986
|
+
kind: this.taskKind,
|
|
8987
|
+
region: options.region
|
|
8988
|
+
})).digest("hex");
|
|
8425
8989
|
}
|
|
8426
8990
|
async plan(input) {
|
|
8427
8991
|
if (!Number.isSafeInteger(input.estimatedInputTokens) || input.estimatedInputTokens < 1) {
|
|
@@ -8485,9 +9049,15 @@ class ArchymedesCloudTurnProvider {
|
|
|
8485
9049
|
return parseCreditBalance(body);
|
|
8486
9050
|
}
|
|
8487
9051
|
async complete(request) {
|
|
9052
|
+
return this.exchange(request, false);
|
|
9053
|
+
}
|
|
9054
|
+
async recoverComplete(request) {
|
|
9055
|
+
return this.exchange(request, true);
|
|
9056
|
+
}
|
|
9057
|
+
async exchange(request, recoveryOnly) {
|
|
8488
9058
|
if (!request.safetyIdentifier.trim())
|
|
8489
9059
|
throw new Error("safetyIdentifier is required");
|
|
8490
|
-
const taskId = request.requestId ?? `cli_${
|
|
9060
|
+
const taskId = request.requestId ?? `cli_${randomUUID8()}`;
|
|
8491
9061
|
if (!/^[A-Za-z0-9_-]{1,160}$/.test(taskId))
|
|
8492
9062
|
throw new Error("requestId must contain 1 to 160 letters, numbers, underscores or hyphens");
|
|
8493
9063
|
request.signal?.throwIfAborted();
|
|
@@ -8495,7 +9065,7 @@ class ArchymedesCloudTurnProvider {
|
|
|
8495
9065
|
AbortSignal.timeout(this.options.timeoutMs ?? 180000),
|
|
8496
9066
|
...request.signal ? [request.signal] : []
|
|
8497
9067
|
]);
|
|
8498
|
-
const response = await this.fetchImpl(this.completionUrl, {
|
|
9068
|
+
const response = await this.fetchImpl(recoveryOnly ? `${this.completionUrl}/recover` : this.completionUrl, {
|
|
8499
9069
|
method: "POST",
|
|
8500
9070
|
headers: {
|
|
8501
9071
|
authorization: `Bearer ${this.options.token}`,
|
|
@@ -8768,11 +9338,11 @@ class ExaSearchClient {
|
|
|
8768
9338
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
8769
9339
|
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
8770
9340
|
}
|
|
8771
|
-
async post(
|
|
9341
|
+
async post(path16, body, timeoutMs) {
|
|
8772
9342
|
const controller = new AbortController;
|
|
8773
9343
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
8774
9344
|
try {
|
|
8775
|
-
const response = await this.fetchImpl(`${this.baseUrl}${
|
|
9345
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path16}`, {
|
|
8776
9346
|
method: "POST",
|
|
8777
9347
|
headers: {
|
|
8778
9348
|
"content-type": "application/json",
|
|
@@ -8783,7 +9353,7 @@ class ExaSearchClient {
|
|
|
8783
9353
|
});
|
|
8784
9354
|
if (!response.ok) {
|
|
8785
9355
|
const text = await response.text();
|
|
8786
|
-
throw new Error(`Exa ${
|
|
9356
|
+
throw new Error(`Exa ${path16} failed (${response.status}): ${text.slice(0, 240)}`);
|
|
8787
9357
|
}
|
|
8788
9358
|
return await response.json();
|
|
8789
9359
|
} finally {
|
|
@@ -11156,18 +11726,18 @@ async function askModelForSuggestions(model, signals, context = {}) {
|
|
|
11156
11726
|
}
|
|
11157
11727
|
|
|
11158
11728
|
// packages/core/src/cli/daemon.ts
|
|
11159
|
-
import { randomUUID as
|
|
11729
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
11160
11730
|
var ARCHYMEDES_DAEMON_PROTOCOL_VERSION = 1;
|
|
11161
11731
|
function previewFor(request) {
|
|
11162
11732
|
const args = request.call.arguments ?? {};
|
|
11163
|
-
const
|
|
11164
|
-
if (!
|
|
11733
|
+
const path16 = typeof args.path === "string" ? args.path : undefined;
|
|
11734
|
+
if (!path16)
|
|
11165
11735
|
return;
|
|
11166
11736
|
if (request.tool.name === "write_file" && typeof args.content === "string") {
|
|
11167
|
-
return { toolName: "write_file", path:
|
|
11737
|
+
return { toolName: "write_file", path: path16, content: args.content };
|
|
11168
11738
|
}
|
|
11169
11739
|
if (request.tool.name === "edit_file" && typeof args.oldText === "string" && typeof args.newText === "string") {
|
|
11170
|
-
return { toolName: "edit_file", path:
|
|
11740
|
+
return { toolName: "edit_file", path: path16, oldText: args.oldText, newText: args.newText };
|
|
11171
11741
|
}
|
|
11172
11742
|
return;
|
|
11173
11743
|
}
|
|
@@ -11184,7 +11754,7 @@ class ArchymedesSessionDaemon {
|
|
|
11184
11754
|
sessions = new Map;
|
|
11185
11755
|
pendingApprovals = new Map;
|
|
11186
11756
|
connect(options = {}) {
|
|
11187
|
-
const id = options.id ?? `client_${
|
|
11757
|
+
const id = options.id ?? `client_${randomUUID9()}`;
|
|
11188
11758
|
assertSafeId(id, "Client id");
|
|
11189
11759
|
if (this.clients.has(id))
|
|
11190
11760
|
throw new Error(`Daemon client already connected: ${id}`);
|
|
@@ -11203,7 +11773,7 @@ class ArchymedesSessionDaemon {
|
|
|
11203
11773
|
}
|
|
11204
11774
|
async requestApproval(sessionId, request) {
|
|
11205
11775
|
const publicRequest = {
|
|
11206
|
-
id: `approval_${
|
|
11776
|
+
id: `approval_${randomUUID9()}`,
|
|
11207
11777
|
sessionId,
|
|
11208
11778
|
summary: request.summary,
|
|
11209
11779
|
actionDigest: request.actionDigest,
|
|
@@ -11241,8 +11811,17 @@ class ArchymedesSessionDaemon {
|
|
|
11241
11811
|
onEvent: (event) => this.publish(sessionId, { type: "agent_event", sessionId, event }),
|
|
11242
11812
|
approve: (request) => this.requestApproval(sessionId, request)
|
|
11243
11813
|
});
|
|
11244
|
-
if (record)
|
|
11814
|
+
if (record) {
|
|
11245
11815
|
agent.resume(record);
|
|
11816
|
+
try {
|
|
11817
|
+
await agent.recoverPending();
|
|
11818
|
+
} catch (error) {
|
|
11819
|
+
await agent.relinquish().catch(() => {
|
|
11820
|
+
return;
|
|
11821
|
+
});
|
|
11822
|
+
throw error;
|
|
11823
|
+
}
|
|
11824
|
+
}
|
|
11246
11825
|
sessionId = agent.sessionId;
|
|
11247
11826
|
const live = {
|
|
11248
11827
|
agent,
|
|
@@ -11360,6 +11939,9 @@ class ArchymedesSessionDaemon {
|
|
|
11360
11939
|
inspectTools(clientId, sessionId) {
|
|
11361
11940
|
return this.requireAttached(clientId, sessionId).agent.inspectTools();
|
|
11362
11941
|
}
|
|
11942
|
+
snapshot(clientId, sessionId) {
|
|
11943
|
+
return this.requireAttached(clientId, sessionId).agent.snapshot();
|
|
11944
|
+
}
|
|
11363
11945
|
routingReceipts(clientId, sessionId) {
|
|
11364
11946
|
return this.requireAttached(clientId, sessionId).agent.routingReceipts;
|
|
11365
11947
|
}
|
|
@@ -11453,13 +12035,16 @@ class ArchymedesDaemonClient {
|
|
|
11453
12035
|
this.info = info;
|
|
11454
12036
|
return info;
|
|
11455
12037
|
}
|
|
12038
|
+
snapshot() {
|
|
12039
|
+
return this.daemon.snapshot(this.id, this.sessionId);
|
|
12040
|
+
}
|
|
11456
12041
|
get routingReceipts() {
|
|
11457
12042
|
return this.daemon.routingReceipts(this.id, this.sessionId);
|
|
11458
12043
|
}
|
|
11459
12044
|
planRoute(objective, signal) {
|
|
11460
12045
|
return this.daemon.planRoute(this.id, this.sessionId, objective, signal);
|
|
11461
12046
|
}
|
|
11462
|
-
send(objective, commandId = `turn_${
|
|
12047
|
+
send(objective, commandId = `turn_${randomUUID9()}`) {
|
|
11463
12048
|
return this.daemon.send(this.id, this.sessionId, objective, commandId);
|
|
11464
12049
|
}
|
|
11465
12050
|
cancel() {
|
|
@@ -11541,12 +12126,12 @@ function assessTaskSafety(objective) {
|
|
|
11541
12126
|
}
|
|
11542
12127
|
|
|
11543
12128
|
// packages/core/src/cli/session.ts
|
|
11544
|
-
import { createHash as
|
|
11545
|
-
import { promises as
|
|
11546
|
-
import
|
|
12129
|
+
import { createHash as createHash10, randomUUID as randomUUID10 } from "node:crypto";
|
|
12130
|
+
import { promises as fs10 } from "node:fs";
|
|
12131
|
+
import path16 from "node:path";
|
|
11547
12132
|
var SESSION_SCHEMA_VERSION2 = 2;
|
|
11548
12133
|
function sessionDirectory2(root) {
|
|
11549
|
-
return
|
|
12134
|
+
return path16.join(root, ".archymedes", "sessions");
|
|
11550
12135
|
}
|
|
11551
12136
|
function assertSessionId2(id) {
|
|
11552
12137
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id) || id === "." || id === "..") {
|
|
@@ -11564,15 +12149,15 @@ function canonicalize3(value) {
|
|
|
11564
12149
|
return null;
|
|
11565
12150
|
}
|
|
11566
12151
|
function integrityFor2(record) {
|
|
11567
|
-
return
|
|
12152
|
+
return createHash10("sha256").update(JSON.stringify(canonicalize3(record))).digest("hex");
|
|
11568
12153
|
}
|
|
11569
12154
|
async function loadSession2(root, id) {
|
|
11570
12155
|
try {
|
|
11571
12156
|
assertSessionId2(id);
|
|
11572
|
-
const parsed = JSON.parse(await
|
|
12157
|
+
const parsed = JSON.parse(await fs10.readFile(path16.join(sessionDirectory2(root), `${id}.json`), "utf8"));
|
|
11573
12158
|
if (!parsed || typeof parsed !== "object" || parsed.id !== id || typeof parsed.root !== "string")
|
|
11574
12159
|
return null;
|
|
11575
|
-
const [storedRoot, requestedRoot] = await Promise.all([parsed.root, root].map(async (candidate) =>
|
|
12160
|
+
const [storedRoot, requestedRoot] = await Promise.all([parsed.root, root].map(async (candidate) => fs10.realpath(path16.resolve(candidate)).catch(() => path16.resolve(candidate))));
|
|
11576
12161
|
const rootsMatch = process.platform === "win32" ? storedRoot.toLocaleLowerCase("en-US") === requestedRoot.toLocaleLowerCase("en-US") : storedRoot === requestedRoot;
|
|
11577
12162
|
if (!rootsMatch)
|
|
11578
12163
|
return null;
|
|
@@ -11599,7 +12184,7 @@ async function loadSession2(root, id) {
|
|
|
11599
12184
|
}
|
|
11600
12185
|
async function listSessions(root, limit = 20) {
|
|
11601
12186
|
try {
|
|
11602
|
-
const files = await
|
|
12187
|
+
const files = await fs10.readdir(sessionDirectory2(root));
|
|
11603
12188
|
const records = await Promise.all(files.filter((file) => file.endsWith(".json")).map(async (file) => {
|
|
11604
12189
|
try {
|
|
11605
12190
|
const record = await loadSession2(root, file.slice(0, -5));
|
|
@@ -11926,11 +12511,11 @@ class ExaSearchClient2 {
|
|
|
11926
12511
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
11927
12512
|
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
11928
12513
|
}
|
|
11929
|
-
async post(
|
|
12514
|
+
async post(path17, body, timeoutMs) {
|
|
11930
12515
|
const controller = new AbortController;
|
|
11931
12516
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
11932
12517
|
try {
|
|
11933
|
-
const response = await this.fetchImpl(`${this.baseUrl}${
|
|
12518
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path17}`, {
|
|
11934
12519
|
method: "POST",
|
|
11935
12520
|
headers: {
|
|
11936
12521
|
"content-type": "application/json",
|
|
@@ -11941,7 +12526,7 @@ class ExaSearchClient2 {
|
|
|
11941
12526
|
});
|
|
11942
12527
|
if (!response.ok) {
|
|
11943
12528
|
const text = await response.text();
|
|
11944
|
-
throw new Error(`Exa ${
|
|
12529
|
+
throw new Error(`Exa ${path17} failed (${response.status}): ${text.slice(0, 240)}`);
|
|
11945
12530
|
}
|
|
11946
12531
|
return await response.json();
|
|
11947
12532
|
} finally {
|
|
@@ -12044,7 +12629,7 @@ function createExaClient2(environment, fetchImpl) {
|
|
|
12044
12629
|
}
|
|
12045
12630
|
|
|
12046
12631
|
// packages/core/src/cli/backends.ts
|
|
12047
|
-
import
|
|
12632
|
+
import path17 from "node:path";
|
|
12048
12633
|
var CONFIG_FILE_SCAN_LIMIT2 = 500;
|
|
12049
12634
|
|
|
12050
12635
|
class LocalWorkspace2 {
|
|
@@ -12064,14 +12649,14 @@ class LocalWorkspace2 {
|
|
|
12064
12649
|
get label() {
|
|
12065
12650
|
return this.root;
|
|
12066
12651
|
}
|
|
12067
|
-
readFile(
|
|
12068
|
-
return readTextFile(this.root,
|
|
12652
|
+
readFile(path18, options = {}) {
|
|
12653
|
+
return readTextFile(this.root, path18, { ...options, limits: this.limits });
|
|
12069
12654
|
}
|
|
12070
|
-
writeFile(
|
|
12071
|
-
return writeTextFile(this.root,
|
|
12655
|
+
writeFile(path18, content) {
|
|
12656
|
+
return writeTextFile(this.root, path18, content, this.limits);
|
|
12072
12657
|
}
|
|
12073
|
-
editFile(
|
|
12074
|
-
return editTextFile(this.root,
|
|
12658
|
+
editFile(path18, oldText, newText, options = {}) {
|
|
12659
|
+
return editTextFile(this.root, path18, oldText, newText, { ...options, limits: this.limits });
|
|
12075
12660
|
}
|
|
12076
12661
|
async list(prefix, depth) {
|
|
12077
12662
|
const entries = [];
|
|
@@ -12092,8 +12677,8 @@ class LocalWorkspace2 {
|
|
|
12092
12677
|
}
|
|
12093
12678
|
async listConfigFiles(prefix) {
|
|
12094
12679
|
const files = [];
|
|
12095
|
-
const base =
|
|
12096
|
-
if (!base.startsWith(
|
|
12680
|
+
const base = path17.resolve(this.root, prefix);
|
|
12681
|
+
if (!base.startsWith(path17.resolve(this.root)))
|
|
12097
12682
|
return [];
|
|
12098
12683
|
for await (const entry of walkWorkspace(base, { ...this.limits, ignoredDirectories: [] }, CONFIG_FILE_SCAN_LIMIT2)) {
|
|
12099
12684
|
if (!entry.isDirectory)
|
|
@@ -12165,8 +12750,8 @@ class SandboxWorkspace {
|
|
|
12165
12750
|
relative(absolute) {
|
|
12166
12751
|
return absolute.startsWith(`${this.options.workspaceRoot}/`) ? absolute.slice(this.options.workspaceRoot.length + 1) : absolute;
|
|
12167
12752
|
}
|
|
12168
|
-
async readFile(
|
|
12169
|
-
const absolute = this.absolute(
|
|
12753
|
+
async readFile(path18, options = {}) {
|
|
12754
|
+
const absolute = this.absolute(path18);
|
|
12170
12755
|
const text = await this.options.sandbox.readFile(this.options.sandboxId, absolute);
|
|
12171
12756
|
if (looksBinary(Buffer.from(text.slice(0, 4096), "utf8")))
|
|
12172
12757
|
throw new WorkspaceViolation(`${this.relative(absolute)} looks like a binary file`);
|
|
@@ -12189,24 +12774,24 @@ class SandboxWorkspace {
|
|
|
12189
12774
|
truncated: startLine > 1 || options.limit !== undefined && startLine - 1 + options.limit < lines.length
|
|
12190
12775
|
};
|
|
12191
12776
|
}
|
|
12192
|
-
async writeFile(
|
|
12777
|
+
async writeFile(path18, content) {
|
|
12193
12778
|
if (typeof content !== "string")
|
|
12194
12779
|
throw new WorkspaceViolation("content must be a string");
|
|
12195
12780
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
12196
12781
|
if (bytes > this.limits.maxWriteBytes)
|
|
12197
12782
|
throw new WorkspaceViolation(`content is ${bytes} bytes, above the ${this.limits.maxWriteBytes}-byte write limit`);
|
|
12198
|
-
const absolute = this.absolute(
|
|
12783
|
+
const absolute = this.absolute(path18);
|
|
12199
12784
|
await this.options.sandbox.writeFile(this.options.sandboxId, absolute, content);
|
|
12200
12785
|
return { path: this.relative(absolute), bytesWritten: bytes };
|
|
12201
12786
|
}
|
|
12202
|
-
async editFile(
|
|
12787
|
+
async editFile(path18, oldText, newText, options = {}) {
|
|
12203
12788
|
if (typeof oldText !== "string" || oldText === "")
|
|
12204
12789
|
throw new WorkspaceViolation("oldText must be a non-empty string");
|
|
12205
12790
|
if (typeof newText !== "string")
|
|
12206
12791
|
throw new WorkspaceViolation("newText must be a string");
|
|
12207
12792
|
if (oldText === newText)
|
|
12208
12793
|
throw new WorkspaceViolation("oldText and newText are identical");
|
|
12209
|
-
const existing = await this.readFile(
|
|
12794
|
+
const existing = await this.readFile(path18);
|
|
12210
12795
|
const occurrences = existing.content.split(oldText).length - 1;
|
|
12211
12796
|
if (occurrences === 0)
|
|
12212
12797
|
throw new WorkspaceViolation(`oldText was not found in ${existing.path}`);
|
|
@@ -12214,7 +12799,7 @@ class SandboxWorkspace {
|
|
|
12214
12799
|
throw new WorkspaceViolation(`oldText appears ${occurrences} times in ${existing.path}; include more surrounding context or set replaceAll`);
|
|
12215
12800
|
}
|
|
12216
12801
|
const updated = options.replaceAll ? existing.content.split(oldText).join(newText) : existing.content.replace(oldText, newText);
|
|
12217
|
-
await this.writeFile(
|
|
12802
|
+
await this.writeFile(path18, updated);
|
|
12218
12803
|
return { path: existing.path, replacements: options.replaceAll ? occurrences : 1 };
|
|
12219
12804
|
}
|
|
12220
12805
|
async allFiles() {
|
|
@@ -12339,13 +12924,13 @@ async function downloadProject(workspace, localRoot, options = {}) {
|
|
|
12339
12924
|
const limits = options.limits ?? DEFAULT_WORKSPACE_LIMITS;
|
|
12340
12925
|
const written = [];
|
|
12341
12926
|
const failed2 = [];
|
|
12342
|
-
for (const
|
|
12927
|
+
for (const path18 of await workspace.glob("**/*")) {
|
|
12343
12928
|
try {
|
|
12344
|
-
const file = await workspace.readFile(
|
|
12345
|
-
await writeTextFile(localRoot,
|
|
12346
|
-
written.push(displayPath(localRoot, `${localRoot}/${
|
|
12929
|
+
const file = await workspace.readFile(path18);
|
|
12930
|
+
await writeTextFile(localRoot, path18, file.content, limits);
|
|
12931
|
+
written.push(displayPath(localRoot, `${localRoot}/${path18}`));
|
|
12347
12932
|
} catch {
|
|
12348
|
-
failed2.push(
|
|
12933
|
+
failed2.push(path18);
|
|
12349
12934
|
}
|
|
12350
12935
|
}
|
|
12351
12936
|
return { written, failed: failed2 };
|
|
@@ -13077,21 +13662,77 @@ function detectColorDepth(environment, isTTY) {
|
|
|
13077
13662
|
var STAR_GLYPHS2 = UNICODE_GLYPHS.starGlyphs;
|
|
13078
13663
|
|
|
13079
13664
|
// packages/archymedes-cli/src/identity.ts
|
|
13080
|
-
|
|
13081
|
-
|
|
13665
|
+
function renderGeometry(angle = 0, ascii = false) {
|
|
13666
|
+
const width = 23;
|
|
13667
|
+
const height = 11;
|
|
13668
|
+
const cells = Array.from({ length: height }, () => Array(width).fill(" "));
|
|
13669
|
+
const vertices = [[0, 1.15, 0], [-1, -0.7, 0.65], [1, -0.7, 0.65], [0, -0.7, -1]];
|
|
13670
|
+
const project = ([x, y, z], scale) => {
|
|
13671
|
+
const yaw = angle + 0.25;
|
|
13672
|
+
const rx = (x * Math.cos(yaw) + z * Math.sin(yaw)) * scale;
|
|
13673
|
+
const rz = (-x * Math.sin(yaw) + z * Math.cos(yaw)) * scale;
|
|
13674
|
+
const ry = y * scale;
|
|
13675
|
+
return [Math.round(11 + rx * 9), Math.round(5 - (ry * 0.94 - rz * 0.34) * 4)];
|
|
13676
|
+
};
|
|
13677
|
+
const outer = vertices.map((point) => project(point, 1));
|
|
13678
|
+
const inner = vertices.map((point) => project(point, 0.43));
|
|
13679
|
+
const line = (a, b, faint = false) => {
|
|
13680
|
+
const dx = b[0] - a[0];
|
|
13681
|
+
const dy = b[1] - a[1];
|
|
13682
|
+
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
|
13683
|
+
const glyph = faint ? "." : Math.abs(dx) > Math.abs(dy) * 2 ? "-" : dx === 0 ? "|" : dx * dy > 0 ? "\\" : "/";
|
|
13684
|
+
for (let step = 1;step < steps; step++) {
|
|
13685
|
+
const x = Math.round(a[0] + dx * step / steps);
|
|
13686
|
+
const y = Math.round(a[1] + dy * step / steps);
|
|
13687
|
+
if (cells[y]?.[x] !== undefined && (!faint || cells[y][x] === " "))
|
|
13688
|
+
cells[y][x] = glyph;
|
|
13689
|
+
}
|
|
13690
|
+
};
|
|
13691
|
+
for (let i = 0;i < 4; i++)
|
|
13692
|
+
line(outer[i], inner[i], true);
|
|
13693
|
+
for (const points of [outer, inner]) {
|
|
13694
|
+
for (let i = 0;i < 4; i++)
|
|
13695
|
+
for (let j = i + 1;j < 4; j++)
|
|
13696
|
+
line(points[i], points[j], j === 3);
|
|
13697
|
+
}
|
|
13698
|
+
for (const [x, y] of [...outer, ...inner])
|
|
13699
|
+
cells[y][x] = ascii ? "+" : "◇";
|
|
13700
|
+
return cells.map((row) => row.join(""));
|
|
13701
|
+
}
|
|
13702
|
+
async function writeIdentity(options, output, motion) {
|
|
13703
|
+
output.write(`${renderIdentity(options)}
|
|
13704
|
+
`);
|
|
13705
|
+
if (!motion.enabled || motion.signal?.aborted || options.width < 64 || options.rows < 24)
|
|
13706
|
+
return;
|
|
13707
|
+
const frames = 12;
|
|
13708
|
+
for (let frame = 1;frame <= frames; frame++) {
|
|
13709
|
+
await new Promise((resolve) => setTimeout(resolve, 45));
|
|
13710
|
+
if (motion.signal?.aborted)
|
|
13711
|
+
return;
|
|
13712
|
+
const size = motion.size();
|
|
13713
|
+
if (size.width !== options.width || size.rows !== options.rows)
|
|
13714
|
+
return;
|
|
13715
|
+
const rendered = renderIdentity({ ...options, angle: Math.sin(frame / frames * Math.PI) * 0.65 });
|
|
13716
|
+
output.write(`\x1B[11F${rendered.split(`
|
|
13717
|
+
`).map((line) => `\x1B[2K${line}`).join(`
|
|
13718
|
+
`)}
|
|
13719
|
+
`);
|
|
13720
|
+
}
|
|
13721
|
+
}
|
|
13082
13722
|
function renderIdentity(options) {
|
|
13083
13723
|
const { palette } = options;
|
|
13084
13724
|
const glyphs = options.glyphs ?? UNICODE_GLYPHS;
|
|
13085
13725
|
const width = Math.max(1, Math.floor(options.width));
|
|
13086
13726
|
const ascii = glyphs.boxHorizontal === ASCII_GLYPHS.boxHorizontal;
|
|
13087
|
-
const mark = ascii ? "
|
|
13727
|
+
const mark = ascii ? "/\\" : "△";
|
|
13088
13728
|
const text = [
|
|
13089
13729
|
`ARCHYMEDES ${options.version}`,
|
|
13090
13730
|
"A place to think. The tools to build.",
|
|
13091
13731
|
"",
|
|
13092
13732
|
`${options.mode} ${glyphs.middot} ${options.workspace}`,
|
|
13093
13733
|
options.model,
|
|
13094
|
-
`/help ${glyphs.middot} /guide ${glyphs.middot} /workspace
|
|
13734
|
+
`/help ${glyphs.middot} /guide ${glyphs.middot} /workspace`,
|
|
13735
|
+
""
|
|
13095
13736
|
];
|
|
13096
13737
|
const codes = [palette.primary + BOLD, palette.text, "", palette.secondary, palette.muted, palette.primary];
|
|
13097
13738
|
const clean = (value) => value.replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
|
|
@@ -13100,13 +13741,13 @@ function renderIdentity(options) {
|
|
|
13100
13741
|
return lines.map((line, index) => paint(clipTo(clean(line), width, glyphs), index === 0 ? codes[0] : palette.muted, palette.depth)).join(`
|
|
13101
13742
|
`);
|
|
13102
13743
|
}
|
|
13103
|
-
const
|
|
13104
|
-
const artWidth = Math.max(...
|
|
13744
|
+
const geometry = renderGeometry(options.angle, ascii);
|
|
13745
|
+
const artWidth = Math.max(...geometry.map(visibleWidth));
|
|
13105
13746
|
const gap = 3;
|
|
13106
13747
|
const margin = 2;
|
|
13107
|
-
return
|
|
13748
|
+
return geometry.map((line, index) => {
|
|
13108
13749
|
const drawing = line + " ".repeat(artWidth - visibleWidth(line));
|
|
13109
|
-
return " ".repeat(margin) + paint(drawing, palette.primary, palette.depth) + " ".repeat(gap) + paint(clipTo(clean(text[index]), width - margin - artWidth - gap, glyphs), codes[index], palette.depth);
|
|
13750
|
+
return " ".repeat(margin) + paint(drawing, palette.primary, palette.depth) + " ".repeat(gap) + paint(clipTo(clean(text[index] ?? ""), width - margin - artWidth - gap, glyphs), codes[index] ?? "", palette.depth);
|
|
13110
13751
|
}).join(`
|
|
13111
13752
|
`);
|
|
13112
13753
|
}
|
|
@@ -14382,12 +15023,12 @@ function countOf(value) {
|
|
|
14382
15023
|
function describeToolCall2(toolName, args) {
|
|
14383
15024
|
switch (toolName) {
|
|
14384
15025
|
case "read_file": {
|
|
14385
|
-
const
|
|
15026
|
+
const path18 = asString(args.path) ?? "";
|
|
14386
15027
|
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
14387
15028
|
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
14388
15029
|
if (offset === undefined)
|
|
14389
|
-
return
|
|
14390
|
-
return limit === undefined ? `${
|
|
15030
|
+
return path18;
|
|
15031
|
+
return limit === undefined ? `${path18}:${offset}+` : `${path18}:${offset}-${offset + limit - 1}`;
|
|
14391
15032
|
}
|
|
14392
15033
|
case "write_file":
|
|
14393
15034
|
case "edit_file":
|
|
@@ -15980,7 +16621,7 @@ function renderTask(context) {
|
|
|
15980
16621
|
return renderTaskView({
|
|
15981
16622
|
request: context.request,
|
|
15982
16623
|
plan: context.plan.map((todo) => ({ text: todo.text, status: todo.status })),
|
|
15983
|
-
files: [...context.files].sort(([a], [b]) => a.localeCompare(b)).map(([
|
|
16624
|
+
files: [...context.files].sort(([a], [b]) => a.localeCompare(b)).map(([path18, delta]) => ({ path: path18, added: delta.added, removed: delta.removed })),
|
|
15984
16625
|
checks: context.checks.map(([kind, passed]) => ({ kind, passed })),
|
|
15985
16626
|
lastTurnStatus: context.turnsTaken ? context.lastTurnStatus : undefined
|
|
15986
16627
|
}, context.style);
|
|
@@ -16210,8 +16851,8 @@ function fallbackSetting(preference) {
|
|
|
16210
16851
|
}
|
|
16211
16852
|
|
|
16212
16853
|
// packages/archymedes-cli/src/session-export.ts
|
|
16213
|
-
import { promises as
|
|
16214
|
-
import
|
|
16854
|
+
import { promises as fs11 } from "node:fs";
|
|
16855
|
+
import path18 from "node:path";
|
|
16215
16856
|
function redactText(value) {
|
|
16216
16857
|
let redacted = value.replace(/-----BEGIN (?:RSA |EC |OPENSSH |PGP |DSA )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |PGP |DSA )?PRIVATE KEY-----/g, "[REDACTED PRIVATE KEY]").replace(/([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^@\s/]+)@/gi, "$1[REDACTED]@");
|
|
16217
16858
|
for (const pattern of SECRET_PATTERNS)
|
|
@@ -16237,7 +16878,7 @@ function sessionExportPayload(record, format) {
|
|
|
16237
16878
|
updatedAt: new Date(record.updatedAt).toISOString(),
|
|
16238
16879
|
mode: record.mode,
|
|
16239
16880
|
totalRwf: record.totalRwf,
|
|
16240
|
-
workspace:
|
|
16881
|
+
workspace: path18.basename(record.root)
|
|
16241
16882
|
};
|
|
16242
16883
|
return format === "support" ? { ...base, messageCount: messages.length, recentMessages: messages.slice(-20) } : { ...base, messages };
|
|
16243
16884
|
}
|
|
@@ -16252,14 +16893,14 @@ function markdownExport(record) {
|
|
|
16252
16893
|
`;
|
|
16253
16894
|
}
|
|
16254
16895
|
async function exportSession(record, format) {
|
|
16255
|
-
const directory =
|
|
16256
|
-
await
|
|
16896
|
+
const directory = path18.join(record.root, ".archymedes", "exports");
|
|
16897
|
+
await fs11.mkdir(directory, { recursive: true, mode: 448 });
|
|
16257
16898
|
const extension = format === "markdown" ? "md" : "json";
|
|
16258
|
-
const file =
|
|
16899
|
+
const file = path18.join(directory, `${record.id}-${format}.${extension}`);
|
|
16259
16900
|
const body = format === "markdown" ? markdownExport(record) : `${JSON.stringify(sessionExportPayload(record, format), null, 2)}
|
|
16260
16901
|
`;
|
|
16261
|
-
await
|
|
16262
|
-
await
|
|
16902
|
+
await fs11.writeFile(file, body, { encoding: "utf8", mode: 384 });
|
|
16903
|
+
await fs11.chmod(file, 384).catch(() => {
|
|
16263
16904
|
return;
|
|
16264
16905
|
});
|
|
16265
16906
|
return file;
|
|
@@ -16660,7 +17301,7 @@ import { createInterface as createInterface3 } from "node:readline/promises";
|
|
|
16660
17301
|
// packages/archymedes-cli/package.json
|
|
16661
17302
|
var package_default = {
|
|
16662
17303
|
name: "archymedes-cli",
|
|
16663
|
-
version: "2.
|
|
17304
|
+
version: "2.1.0",
|
|
16664
17305
|
description: "Archymedes CLI — a coding agent that shows its work. Runs against your working tree or an isolated remote sandbox, with guarded approvals, git checkpoints, and exact, measured cost accounting.",
|
|
16665
17306
|
keywords: [
|
|
16666
17307
|
"cli",
|
|
@@ -16940,8 +17581,8 @@ async function runSelfUpdate(options = {}) {
|
|
|
16940
17581
|
}
|
|
16941
17582
|
|
|
16942
17583
|
// packages/archymedes-cli/src/auto-update.ts
|
|
16943
|
-
import { promises as
|
|
16944
|
-
import
|
|
17584
|
+
import { promises as fs12 } from "node:fs";
|
|
17585
|
+
import path19 from "node:path";
|
|
16945
17586
|
var AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
16946
17587
|
function isContinuousIntegration(environment) {
|
|
16947
17588
|
return ["CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "JENKINS_URL", "TEAMCITY_VERSION"].some((name) => Boolean(environment[name]?.trim()));
|
|
@@ -16986,11 +17627,11 @@ function isNewer(candidate, current) {
|
|
|
16986
17627
|
return false;
|
|
16987
17628
|
}
|
|
16988
17629
|
function stateFile(environment) {
|
|
16989
|
-
return
|
|
17630
|
+
return path19.join(archymedesConfigDirectory(environment), "update-check.json");
|
|
16990
17631
|
}
|
|
16991
17632
|
async function readAutoUpdateState(environment) {
|
|
16992
17633
|
try {
|
|
16993
|
-
const parsed = JSON.parse(await
|
|
17634
|
+
const parsed = JSON.parse(await fs12.readFile(stateFile(environment), "utf8"));
|
|
16994
17635
|
if (typeof parsed.lastCheckedAt !== "number" || !Number.isFinite(parsed.lastCheckedAt))
|
|
16995
17636
|
return null;
|
|
16996
17637
|
return {
|
|
@@ -17005,8 +17646,8 @@ async function readAutoUpdateState(environment) {
|
|
|
17005
17646
|
async function writeAutoUpdateState(environment, state) {
|
|
17006
17647
|
try {
|
|
17007
17648
|
const file = stateFile(environment);
|
|
17008
|
-
await
|
|
17009
|
-
await
|
|
17649
|
+
await fs12.mkdir(path19.dirname(file), { recursive: true });
|
|
17650
|
+
await fs12.writeFile(file, `${JSON.stringify(state, null, 2)}
|
|
17010
17651
|
`, "utf8");
|
|
17011
17652
|
} catch {}
|
|
17012
17653
|
}
|
|
@@ -17900,9 +18541,9 @@ function renderReliabilityStatus(width, separator, snapshot = BUNDLED_RELIABILIT
|
|
|
17900
18541
|
}
|
|
17901
18542
|
|
|
17902
18543
|
// packages/archymedes-cli/src/settings.ts
|
|
17903
|
-
import { promises as
|
|
17904
|
-
import
|
|
17905
|
-
import
|
|
18544
|
+
import { promises as fs13 } from "node:fs";
|
|
18545
|
+
import os5 from "node:os";
|
|
18546
|
+
import path20 from "node:path";
|
|
17906
18547
|
|
|
17907
18548
|
// packages/archymedes-cli/src/local-currency.ts
|
|
17908
18549
|
var COUNTRY_CURRENCIES2 = {
|
|
@@ -18086,15 +18727,15 @@ var SETTING_FIELDS = [
|
|
|
18086
18727
|
];
|
|
18087
18728
|
function settingsDirectory(environment = process.env, platform = process.platform) {
|
|
18088
18729
|
if (environment.ARCHYMEDES_CONFIG_DIR?.trim())
|
|
18089
|
-
return
|
|
18730
|
+
return path20.resolve(environment.ARCHYMEDES_CONFIG_DIR);
|
|
18090
18731
|
if (platform === "win32")
|
|
18091
|
-
return
|
|
18732
|
+
return path20.join(environment.APPDATA?.trim() || path20.join(os5.homedir(), "AppData", "Roaming"), "Archymedes");
|
|
18092
18733
|
if (platform === "darwin")
|
|
18093
|
-
return
|
|
18094
|
-
return
|
|
18734
|
+
return path20.join(os5.homedir(), "Library", "Application Support", "Archymedes");
|
|
18735
|
+
return path20.join(environment.XDG_CONFIG_HOME?.trim() || path20.join(os5.homedir(), ".config"), "archymedes");
|
|
18095
18736
|
}
|
|
18096
18737
|
function settingsFile(environment = process.env, platform = process.platform) {
|
|
18097
|
-
return
|
|
18738
|
+
return path20.join(settingsDirectory(environment, platform), "settings.json");
|
|
18098
18739
|
}
|
|
18099
18740
|
function cleanSettings(value) {
|
|
18100
18741
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -18110,7 +18751,7 @@ function cleanSettings(value) {
|
|
|
18110
18751
|
}
|
|
18111
18752
|
async function loadSettings(environment = process.env, platform = process.platform) {
|
|
18112
18753
|
try {
|
|
18113
|
-
return cleanSettings(JSON.parse(await
|
|
18754
|
+
return cleanSettings(JSON.parse(await fs13.readFile(settingsFile(environment, platform), "utf8")));
|
|
18114
18755
|
} catch {
|
|
18115
18756
|
return {};
|
|
18116
18757
|
}
|
|
@@ -18118,15 +18759,15 @@ async function loadSettings(environment = process.env, platform = process.platfo
|
|
|
18118
18759
|
async function saveSettings(settings, environment = process.env, platform = process.platform) {
|
|
18119
18760
|
const directory = settingsDirectory(environment, platform);
|
|
18120
18761
|
const file = settingsFile(environment, platform);
|
|
18121
|
-
await
|
|
18762
|
+
await fs13.mkdir(directory, { recursive: true, mode: 448 });
|
|
18122
18763
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
18123
|
-
await
|
|
18764
|
+
await fs13.writeFile(temporary, `${JSON.stringify(cleanSettings(settings), null, 2)}
|
|
18124
18765
|
`, { encoding: "utf8", mode: 384 });
|
|
18125
|
-
await
|
|
18766
|
+
await fs13.chmod(temporary, 384).catch(() => {
|
|
18126
18767
|
return;
|
|
18127
18768
|
});
|
|
18128
|
-
await
|
|
18129
|
-
await
|
|
18769
|
+
await fs13.rename(temporary, file);
|
|
18770
|
+
await fs13.chmod(file, 384).catch(() => {
|
|
18130
18771
|
return;
|
|
18131
18772
|
});
|
|
18132
18773
|
return file;
|
|
@@ -18331,12 +18972,12 @@ Archymedes ${controlLabel(language, "settings")}
|
|
|
18331
18972
|
}
|
|
18332
18973
|
|
|
18333
18974
|
// packages/archymedes-cli/src/history.ts
|
|
18334
|
-
import { promises as
|
|
18335
|
-
import
|
|
18975
|
+
import { promises as fs14 } from "node:fs";
|
|
18976
|
+
import path22 from "node:path";
|
|
18336
18977
|
|
|
18337
18978
|
// packages/archymedes-cli/src/settings.ts
|
|
18338
|
-
import
|
|
18339
|
-
import
|
|
18979
|
+
import os6 from "node:os";
|
|
18980
|
+
import path21 from "node:path";
|
|
18340
18981
|
var LANGUAGE_NAMES2 = {
|
|
18341
18982
|
en: "English",
|
|
18342
18983
|
zh: "中文",
|
|
@@ -18470,12 +19111,12 @@ var SETTING_FIELDS2 = [
|
|
|
18470
19111
|
];
|
|
18471
19112
|
function settingsDirectory2(environment = process.env, platform = process.platform) {
|
|
18472
19113
|
if (environment.ARCHYMEDES_CONFIG_DIR?.trim())
|
|
18473
|
-
return
|
|
19114
|
+
return path21.resolve(environment.ARCHYMEDES_CONFIG_DIR);
|
|
18474
19115
|
if (platform === "win32")
|
|
18475
|
-
return
|
|
19116
|
+
return path21.join(environment.APPDATA?.trim() || path21.join(os6.homedir(), "AppData", "Roaming"), "Archymedes");
|
|
18476
19117
|
if (platform === "darwin")
|
|
18477
|
-
return
|
|
18478
|
-
return
|
|
19118
|
+
return path21.join(os6.homedir(), "Library", "Application Support", "Archymedes");
|
|
19119
|
+
return path21.join(environment.XDG_CONFIG_HOME?.trim() || path21.join(os6.homedir(), ".config"), "archymedes");
|
|
18479
19120
|
}
|
|
18480
19121
|
var PROVIDER_REQUIRED_NAMES2 = new Set(PROVIDER_IDS.flatMap((id) => PROVIDER_INFO[id].requires));
|
|
18481
19122
|
var PROVIDER_KEY_FIELDS2 = SETTING_FIELDS2.filter((field) => PROVIDER_REQUIRED_NAMES2.has(field.key));
|
|
@@ -18484,7 +19125,7 @@ var PROVIDER_KEY_FIELDS2 = SETTING_FIELDS2.filter((field) => PROVIDER_REQUIRED_N
|
|
|
18484
19125
|
var MAX_HISTORY = 200;
|
|
18485
19126
|
var SENSITIVE = /(?:api[_-]?key|authorization|password|secret|token)\s*[:=]/i;
|
|
18486
19127
|
function historyFile(environment) {
|
|
18487
|
-
return
|
|
19128
|
+
return path22.join(settingsDirectory2(environment), "history.json");
|
|
18488
19129
|
}
|
|
18489
19130
|
function sanitizeHistory(lines) {
|
|
18490
19131
|
const result = [];
|
|
@@ -18501,7 +19142,7 @@ function sanitizeHistory(lines) {
|
|
|
18501
19142
|
}
|
|
18502
19143
|
async function loadHistory(environment) {
|
|
18503
19144
|
try {
|
|
18504
|
-
const parsed = JSON.parse(await
|
|
19145
|
+
const parsed = JSON.parse(await fs14.readFile(historyFile(environment), "utf8"));
|
|
18505
19146
|
return Array.isArray(parsed) ? sanitizeHistory(parsed.filter((item) => typeof item === "string")) : [];
|
|
18506
19147
|
} catch {
|
|
18507
19148
|
return [];
|
|
@@ -18509,12 +19150,12 @@ async function loadHistory(environment) {
|
|
|
18509
19150
|
}
|
|
18510
19151
|
async function saveHistory(lines, environment) {
|
|
18511
19152
|
const file = historyFile(environment);
|
|
18512
|
-
await
|
|
19153
|
+
await fs14.mkdir(path22.dirname(file), { recursive: true, mode: 448 });
|
|
18513
19154
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
18514
|
-
await
|
|
19155
|
+
await fs14.writeFile(temporary, `${JSON.stringify(sanitizeHistory(lines), null, 2)}
|
|
18515
19156
|
`, { encoding: "utf8", mode: 384 });
|
|
18516
|
-
await
|
|
18517
|
-
await
|
|
19157
|
+
await fs14.rename(temporary, file);
|
|
19158
|
+
await fs14.chmod(file, 384).catch(() => {
|
|
18518
19159
|
return;
|
|
18519
19160
|
});
|
|
18520
19161
|
}
|
|
@@ -18962,8 +19603,8 @@ function paintSegments2(text, segments, colour, depth) {
|
|
|
18962
19603
|
}
|
|
18963
19604
|
|
|
18964
19605
|
// packages/core/src/providers/model-fetch.ts
|
|
18965
|
-
import { promises as
|
|
18966
|
-
import
|
|
19606
|
+
import { promises as fs15 } from "node:fs";
|
|
19607
|
+
import path23 from "node:path";
|
|
18967
19608
|
async function fetchProviderModels(provider, environment, fetchImpl, timeoutMs = 4000) {
|
|
18968
19609
|
const endpoint = modelsEndpoint(provider, environment);
|
|
18969
19610
|
if (!endpoint)
|
|
@@ -18983,7 +19624,7 @@ async function fetchProviderModels(provider, environment, fetchImpl, timeoutMs =
|
|
|
18983
19624
|
}
|
|
18984
19625
|
var MODEL_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
18985
19626
|
function modelCacheFile(environment) {
|
|
18986
|
-
return
|
|
19627
|
+
return path23.join(archymedesConfigDirectory(environment), "models.json");
|
|
18987
19628
|
}
|
|
18988
19629
|
function isCacheFresh(cache, now = Date.now(), ttlMs = MODEL_CACHE_TTL_MS) {
|
|
18989
19630
|
if (!cache)
|
|
@@ -18992,7 +19633,7 @@ function isCacheFresh(cache, now = Date.now(), ttlMs = MODEL_CACHE_TTL_MS) {
|
|
|
18992
19633
|
}
|
|
18993
19634
|
async function readModelCache(environment) {
|
|
18994
19635
|
try {
|
|
18995
|
-
const parsed = JSON.parse(await
|
|
19636
|
+
const parsed = JSON.parse(await fs15.readFile(modelCacheFile(environment), "utf8"));
|
|
18996
19637
|
return typeof parsed?.fetchedAt === "number" && parsed.models ? parsed : undefined;
|
|
18997
19638
|
} catch {
|
|
18998
19639
|
return;
|
|
@@ -19001,11 +19642,11 @@ async function readModelCache(environment) {
|
|
|
19001
19642
|
async function writeModelCache(environment, cache) {
|
|
19002
19643
|
const file = modelCacheFile(environment);
|
|
19003
19644
|
try {
|
|
19004
|
-
await
|
|
19645
|
+
await fs15.mkdir(path23.dirname(file), { recursive: true });
|
|
19005
19646
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
19006
|
-
await
|
|
19647
|
+
await fs15.writeFile(temporary, `${JSON.stringify(cache, null, 2)}
|
|
19007
19648
|
`, { mode: 384 });
|
|
19008
|
-
await
|
|
19649
|
+
await fs15.rename(temporary, file);
|
|
19009
19650
|
} catch {}
|
|
19010
19651
|
}
|
|
19011
19652
|
async function loadLiveModels(providers, environment, options = {}) {
|
|
@@ -19899,23 +20540,23 @@ function parseThemeCommand(input) {
|
|
|
19899
20540
|
}
|
|
19900
20541
|
|
|
19901
20542
|
// packages/archymedes-cli/src/theme-files.ts
|
|
19902
|
-
import { promises as
|
|
19903
|
-
import
|
|
20543
|
+
import { promises as fs16 } from "node:fs";
|
|
20544
|
+
import path24 from "node:path";
|
|
19904
20545
|
function themeDirectory(scope, root, environment) {
|
|
19905
|
-
return scope === "project" ?
|
|
20546
|
+
return scope === "project" ? path24.join(root, ".archymedes", "themes") : path24.join(settingsDirectory2(environment), "themes");
|
|
19906
20547
|
}
|
|
19907
20548
|
async function readThemeDirectory(directory, source) {
|
|
19908
20549
|
let entries;
|
|
19909
20550
|
try {
|
|
19910
|
-
entries = await
|
|
20551
|
+
entries = await fs16.readdir(directory);
|
|
19911
20552
|
} catch {
|
|
19912
20553
|
return [];
|
|
19913
20554
|
}
|
|
19914
20555
|
const themes = [];
|
|
19915
20556
|
for (const entry of entries.filter((name) => name.endsWith(".tss")).sort()) {
|
|
19916
|
-
const file =
|
|
20557
|
+
const file = path24.join(directory, entry);
|
|
19917
20558
|
try {
|
|
19918
|
-
const contents = await
|
|
20559
|
+
const contents = await fs16.readFile(file, "utf8");
|
|
19919
20560
|
for (const theme of parseThemeSource(contents))
|
|
19920
20561
|
themes.push({ ...theme, source, file });
|
|
19921
20562
|
} catch {}
|
|
@@ -20216,18 +20857,20 @@ function describe(job, now) {
|
|
|
20216
20857
|
return job.cadence ? `${job.cadence} schedule` : "";
|
|
20217
20858
|
}
|
|
20218
20859
|
// packages/core/src/cli/job-store.ts
|
|
20219
|
-
import { promises as
|
|
20220
|
-
import
|
|
20860
|
+
import { promises as fs17 } from "node:fs";
|
|
20861
|
+
import path25 from "node:path";
|
|
20221
20862
|
function jobStoreFile(root) {
|
|
20222
|
-
return
|
|
20863
|
+
return path25.join(root, ".archymedes", "jobs.json");
|
|
20223
20864
|
}
|
|
20224
20865
|
function jobLogPath(root, id) {
|
|
20225
|
-
return
|
|
20866
|
+
return path25.join(root, ".archymedes", "jobs", `${id}.log`);
|
|
20226
20867
|
}
|
|
20227
20868
|
async function readStore(file) {
|
|
20228
20869
|
try {
|
|
20229
|
-
const parsed = JSON.parse(await
|
|
20230
|
-
|
|
20870
|
+
const parsed = JSON.parse(await fs17.readFile(file, "utf8"));
|
|
20871
|
+
if (!Array.isArray(parsed?.jobs))
|
|
20872
|
+
throw new Error('expected an object with a "jobs" array');
|
|
20873
|
+
return { jobs: parsed.jobs };
|
|
20231
20874
|
} catch (error) {
|
|
20232
20875
|
if (error.code === "ENOENT")
|
|
20233
20876
|
return emptyStore();
|
|
@@ -20237,21 +20880,21 @@ async function readStore(file) {
|
|
|
20237
20880
|
async function acquireLock(lockFile) {
|
|
20238
20881
|
for (let attempt = 0;attempt < 80; attempt += 1) {
|
|
20239
20882
|
try {
|
|
20240
|
-
const handle = await
|
|
20883
|
+
const handle = await fs17.open(lockFile, "wx", 384);
|
|
20241
20884
|
await handle.writeFile(`${process.pid}
|
|
20242
20885
|
`, "utf8");
|
|
20243
20886
|
await handle.close();
|
|
20244
20887
|
return async () => {
|
|
20245
|
-
await
|
|
20888
|
+
await fs17.unlink(lockFile).catch(() => {
|
|
20246
20889
|
return;
|
|
20247
20890
|
});
|
|
20248
20891
|
};
|
|
20249
20892
|
} catch (error) {
|
|
20250
20893
|
if (error.code !== "EEXIST")
|
|
20251
20894
|
throw error;
|
|
20252
|
-
const stat2 = await
|
|
20895
|
+
const stat2 = await fs17.stat(lockFile).catch(() => null);
|
|
20253
20896
|
if (stat2 && Date.now() - stat2.mtimeMs > 1e4) {
|
|
20254
|
-
await
|
|
20897
|
+
await fs17.unlink(lockFile).catch(() => {
|
|
20255
20898
|
return;
|
|
20256
20899
|
});
|
|
20257
20900
|
continue;
|
|
@@ -20264,13 +20907,13 @@ async function acquireLock(lockFile) {
|
|
|
20264
20907
|
async function withJobs(root, mutate) {
|
|
20265
20908
|
const file = jobStoreFile(root);
|
|
20266
20909
|
const lockFile = `${file}.lock`;
|
|
20267
|
-
await
|
|
20910
|
+
await fs17.mkdir(path25.dirname(file), { recursive: true });
|
|
20268
20911
|
const release = await acquireLock(lockFile);
|
|
20269
20912
|
try {
|
|
20270
20913
|
const current = await readStore(file);
|
|
20271
20914
|
const { store, result } = mutate(current, Date.now());
|
|
20272
20915
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
20273
|
-
const handle = await
|
|
20916
|
+
const handle = await fs17.open(temporary, "w", 384);
|
|
20274
20917
|
try {
|
|
20275
20918
|
await handle.writeFile(`${JSON.stringify(store, null, 2)}
|
|
20276
20919
|
`, "utf8");
|
|
@@ -20278,7 +20921,7 @@ async function withJobs(root, mutate) {
|
|
|
20278
20921
|
} finally {
|
|
20279
20922
|
await handle.close();
|
|
20280
20923
|
}
|
|
20281
|
-
await
|
|
20924
|
+
await fs17.rename(temporary, file);
|
|
20282
20925
|
return result;
|
|
20283
20926
|
} finally {
|
|
20284
20927
|
await release();
|
|
@@ -20353,13 +20996,13 @@ function newJobId(now = new Date) {
|
|
|
20353
20996
|
}
|
|
20354
20997
|
async function appendJobLog(root, id, line) {
|
|
20355
20998
|
const file = jobLogPath(root, id);
|
|
20356
|
-
await
|
|
20357
|
-
await
|
|
20999
|
+
await fs17.mkdir(path25.dirname(file), { recursive: true });
|
|
21000
|
+
await fs17.appendFile(file, `${line}
|
|
20358
21001
|
`, "utf8");
|
|
20359
21002
|
}
|
|
20360
21003
|
async function readJobLog(root, id, fromByte = 0) {
|
|
20361
21004
|
try {
|
|
20362
|
-
const handle = await
|
|
21005
|
+
const handle = await fs17.open(jobLogPath(root, id), "r");
|
|
20363
21006
|
try {
|
|
20364
21007
|
const stat2 = await handle.stat();
|
|
20365
21008
|
if (fromByte >= stat2.size)
|
|
@@ -20377,7 +21020,7 @@ async function readJobLog(root, id, fromByte = 0) {
|
|
|
20377
21020
|
}
|
|
20378
21021
|
}
|
|
20379
21022
|
// packages/archymedes-cli/src/job-worker.ts
|
|
20380
|
-
import
|
|
21023
|
+
import os7 from "node:os";
|
|
20381
21024
|
// packages/archymedes-cli/src/transcript.ts
|
|
20382
21025
|
function truncateMiddle2(value, maximum) {
|
|
20383
21026
|
const clean = value.replace(/\s+/g, " ").trim();
|
|
@@ -20398,12 +21041,12 @@ function countOf2(value) {
|
|
|
20398
21041
|
function describeToolCall3(toolName, args) {
|
|
20399
21042
|
switch (toolName) {
|
|
20400
21043
|
case "read_file": {
|
|
20401
|
-
const
|
|
21044
|
+
const path26 = asString2(args.path) ?? "";
|
|
20402
21045
|
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
20403
21046
|
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
20404
21047
|
if (offset === undefined)
|
|
20405
|
-
return
|
|
20406
|
-
return limit === undefined ? `${
|
|
21048
|
+
return path26;
|
|
21049
|
+
return limit === undefined ? `${path26}:${offset}+` : `${path26}:${offset}-${offset + limit - 1}`;
|
|
20407
21050
|
}
|
|
20408
21051
|
case "write_file":
|
|
20409
21052
|
case "edit_file":
|
|
@@ -20613,7 +21256,7 @@ function resolveWanderJobTopic(objective, seed) {
|
|
|
20613
21256
|
}
|
|
20614
21257
|
|
|
20615
21258
|
// packages/archymedes-cli/src/job-worker.ts
|
|
20616
|
-
function workerId(hostname =
|
|
21259
|
+
function workerId(hostname = os7.hostname(), pid = process.pid) {
|
|
20617
21260
|
return `${hostname}:${pid}`;
|
|
20618
21261
|
}
|
|
20619
21262
|
var DEFAULT_LEASE_MS = 4 * 60000;
|
|
@@ -21050,17 +21693,17 @@ function renderTools(view, style = plain) {
|
|
|
21050
21693
|
|
|
21051
21694
|
// packages/archymedes-cli/src/voice.ts
|
|
21052
21695
|
import { spawn as spawn6 } from "node:child_process";
|
|
21053
|
-
import { promises as
|
|
21054
|
-
import
|
|
21055
|
-
import
|
|
21696
|
+
import { promises as fs18 } from "node:fs";
|
|
21697
|
+
import os8 from "node:os";
|
|
21698
|
+
import path26 from "node:path";
|
|
21056
21699
|
function recordingArgs(outputFile, platform = process.platform, device) {
|
|
21057
21700
|
const head = ["-hide_banner", "-loglevel", "error", "-y"];
|
|
21058
21701
|
const input = platform === "darwin" ? ["-f", "avfoundation", "-i", device?.trim() || ":0"] : platform === "win32" ? ["-f", "dshow", "-i", `audio=${device?.trim() || "default"}`] : ["-f", "pulse", "-i", device?.trim() || "default"];
|
|
21059
21702
|
return [...head, ...input, "-ac", "1", "-ar", "16000", outputFile];
|
|
21060
21703
|
}
|
|
21061
21704
|
async function startRecording(environment, platform = process.platform, spawnImpl = spawn6) {
|
|
21062
|
-
const directory = await
|
|
21063
|
-
const file =
|
|
21705
|
+
const directory = await fs18.mkdtemp(path26.join(os8.tmpdir(), "archymedes-voice-"));
|
|
21706
|
+
const file = path26.join(directory, "prompt.wav");
|
|
21064
21707
|
const executable2 = environment.ARCHYMEDES_FFMPEG_PATH?.trim() || "ffmpeg";
|
|
21065
21708
|
const child = spawnImpl(executable2, recordingArgs(file, platform, environment.VOICE_INPUT_DEVICE), {
|
|
21066
21709
|
shell: false,
|
|
@@ -21084,7 +21727,7 @@ async function startRecording(environment, platform = process.platform, spawnImp
|
|
|
21084
21727
|
}
|
|
21085
21728
|
});
|
|
21086
21729
|
}).catch(async (error) => {
|
|
21087
|
-
await
|
|
21730
|
+
await fs18.rm(directory, { recursive: true, force: true });
|
|
21088
21731
|
throw error;
|
|
21089
21732
|
});
|
|
21090
21733
|
return {
|
|
@@ -21109,13 +21752,13 @@ async function transcribeAudio(file, environment, fetchImpl = fetch) {
|
|
|
21109
21752
|
const apiKey = environment.OPENAI_API_KEY?.trim();
|
|
21110
21753
|
if (!apiKey)
|
|
21111
21754
|
throw new Error("Voice input needs OPENAI_API_KEY. Add it in archymedes settings or set it in your environment.");
|
|
21112
|
-
const bytes = await
|
|
21755
|
+
const bytes = await fs18.readFile(file);
|
|
21113
21756
|
if (bytes.byteLength === 0)
|
|
21114
21757
|
throw new Error("The audio recording is empty.");
|
|
21115
21758
|
if (bytes.byteLength > 25 * 1024 * 1024)
|
|
21116
21759
|
throw new Error("Audio must be 25 MB or smaller.");
|
|
21117
21760
|
const form = new FormData;
|
|
21118
|
-
form.append("file", new Blob([bytes]),
|
|
21761
|
+
form.append("file", new Blob([bytes]), path26.basename(file));
|
|
21119
21762
|
form.append("model", environment.VOICE_MODEL?.trim() || "gpt-4o-mini-transcribe");
|
|
21120
21763
|
form.append("response_format", "json");
|
|
21121
21764
|
const response = await fetchImpl(transcriptionEndpoint(environment), {
|
|
@@ -21132,7 +21775,7 @@ async function transcribeAudio(file, environment, fetchImpl = fetch) {
|
|
|
21132
21775
|
return result.text.trim();
|
|
21133
21776
|
}
|
|
21134
21777
|
async function removeRecording(file) {
|
|
21135
|
-
await
|
|
21778
|
+
await fs18.rm(path26.dirname(file), { recursive: true, force: true });
|
|
21136
21779
|
}
|
|
21137
21780
|
|
|
21138
21781
|
// packages/archymedes-cli/src/i18n.ts
|
|
@@ -23040,7 +23683,7 @@ function parseArgs(argv) {
|
|
|
23040
23683
|
} else
|
|
23041
23684
|
parsed.resume = "latest";
|
|
23042
23685
|
} else if (argument === "--cwd") {
|
|
23043
|
-
parsed.root =
|
|
23686
|
+
parsed.root = path27.resolve(argv[index + 1] ?? ".");
|
|
23044
23687
|
index += 1;
|
|
23045
23688
|
} else if (argument === "--sandbox") {
|
|
23046
23689
|
const value = argv[index + 1];
|
|
@@ -23258,22 +23901,22 @@ function writeFoldable(label, rendered) {
|
|
|
23258
23901
|
}
|
|
23259
23902
|
}
|
|
23260
23903
|
function renderWrittenCode(toolName, args) {
|
|
23261
|
-
const
|
|
23262
|
-
if (!
|
|
23904
|
+
const path28 = typeof args.path === "string" ? args.path : "";
|
|
23905
|
+
if (!path28)
|
|
23263
23906
|
return;
|
|
23264
23907
|
const style_ = sectionStyle();
|
|
23265
23908
|
if (toolName === "write_file") {
|
|
23266
23909
|
const content = typeof args.content === "string" ? args.content : "";
|
|
23267
23910
|
if (content === "")
|
|
23268
23911
|
return;
|
|
23269
|
-
writeFoldable(
|
|
23912
|
+
writeFoldable(path28, renderFileChange({ path: path28, kind: "write", content }, style_, { maxLines: FOLD_AFTER_LINES }));
|
|
23270
23913
|
return;
|
|
23271
23914
|
}
|
|
23272
23915
|
const before = typeof args.oldText === "string" ? args.oldText : "";
|
|
23273
23916
|
const after = typeof args.newText === "string" ? args.newText : "";
|
|
23274
23917
|
if (before === "" && after === "")
|
|
23275
23918
|
return;
|
|
23276
|
-
writeFoldable(
|
|
23919
|
+
writeFoldable(path28, renderFileChange({ path: path28, kind: "edit", before, after }, style_, { maxLines: FOLD_AFTER_LINES }));
|
|
23277
23920
|
}
|
|
23278
23921
|
function renderCommandOutput(content) {
|
|
23279
23922
|
const style_ = sectionStyle();
|
|
@@ -23404,9 +24047,9 @@ ${rule2(sectionStyle(), { label: "tool activity", tone: "neutral" })}
|
|
|
23404
24047
|
if (runtime.toolName === "write_file" || runtime.toolName === "edit_file") {
|
|
23405
24048
|
toolLines.forget();
|
|
23406
24049
|
renderWrittenCode(runtime.toolName, pending.arguments);
|
|
23407
|
-
const
|
|
23408
|
-
if (
|
|
23409
|
-
touchedFiles.add(
|
|
24050
|
+
const path28 = typeof pending.arguments.path === "string" ? pending.arguments.path : undefined;
|
|
24051
|
+
if (path28)
|
|
24052
|
+
touchedFiles.add(path28);
|
|
23410
24053
|
let addedDelta = 0;
|
|
23411
24054
|
let removedDelta = 0;
|
|
23412
24055
|
if (runtime.toolName === "write_file") {
|
|
@@ -23423,9 +24066,9 @@ ${rule2(sectionStyle(), { label: "tool activity", tone: "neutral" })}
|
|
|
23423
24066
|
}
|
|
23424
24067
|
turnLineDelta.added += addedDelta;
|
|
23425
24068
|
turnLineDelta.removed += removedDelta;
|
|
23426
|
-
if (
|
|
23427
|
-
const prior = sessionFiles.get(
|
|
23428
|
-
sessionFiles.set(
|
|
24069
|
+
if (path28) {
|
|
24070
|
+
const prior = sessionFiles.get(path28) ?? { added: 0, removed: 0 };
|
|
24071
|
+
sessionFiles.set(path28, { added: prior.added + addedDelta, removed: prior.removed + removedDelta });
|
|
23429
24072
|
}
|
|
23430
24073
|
} else if (runtime.toolName === "run_command") {
|
|
23431
24074
|
toolLines.forget();
|
|
@@ -23626,7 +24269,7 @@ async function spawnJobWorker(root, jobId) {
|
|
|
23626
24269
|
const { spawn: spawn7 } = await import("child_process");
|
|
23627
24270
|
const { openSync, mkdirSync } = await import("fs");
|
|
23628
24271
|
const logFile = jobLogPath(root, jobId);
|
|
23629
|
-
mkdirSync(
|
|
24272
|
+
mkdirSync(path27.dirname(logFile), { recursive: true });
|
|
23630
24273
|
const fd = openSync(logFile, "a");
|
|
23631
24274
|
try {
|
|
23632
24275
|
const child = spawn7(process.execPath, [process.argv[1], "--archymedes-job-worker", root, jobId], {
|
|
@@ -24079,7 +24722,7 @@ Setup cancelled.
|
|
|
24079
24722
|
const minutes = Math.max(1, Math.min(args.sandboxMinutes, 60));
|
|
24080
24723
|
if (request.backend === "e2b") {
|
|
24081
24724
|
const { findWorkspacePreset } = await import("./sandbox-templates-8sd1psez.mjs");
|
|
24082
|
-
const { createE2BProvider } = await import("./factory-
|
|
24725
|
+
const { createE2BProvider } = await import("./factory-96yy6mxr.mjs");
|
|
24083
24726
|
const preset = findWorkspacePreset(request.preset ?? args.preset);
|
|
24084
24727
|
const sandbox = createE2BProvider(environment, preset.templateAlias);
|
|
24085
24728
|
if (!sandbox)
|
|
@@ -24105,7 +24748,7 @@ Setup cancelled.
|
|
|
24105
24748
|
return { workspace: created };
|
|
24106
24749
|
}
|
|
24107
24750
|
if (request.backend === "docker") {
|
|
24108
|
-
const { createDockerProvider } = await import("./factory-
|
|
24751
|
+
const { createDockerProvider } = await import("./factory-96yy6mxr.mjs");
|
|
24109
24752
|
const image = request.dockerImage || args.dockerImage || environment.DOCKER_CODING_IMAGE;
|
|
24110
24753
|
const sandbox = createDockerProvider({ ...environment, DOCKER_CODING_IMAGE: image });
|
|
24111
24754
|
if (!sandbox)
|
|
@@ -24314,7 +24957,7 @@ Setup cancelled.
|
|
|
24314
24957
|
`);
|
|
24315
24958
|
}
|
|
24316
24959
|
const firstSink = new TabSink(terminalStream2, { live: true });
|
|
24317
|
-
tabs.adopt(
|
|
24960
|
+
tabs.adopt(path27.basename(args.root) || "archymedes", {
|
|
24318
24961
|
agent,
|
|
24319
24962
|
ledger,
|
|
24320
24963
|
mode,
|
|
@@ -24525,11 +25168,11 @@ Setup cancelled.
|
|
|
24525
25168
|
if (record.mode && !args.modeExplicit)
|
|
24526
25169
|
mode = record.mode;
|
|
24527
25170
|
agent = await openClient(record);
|
|
24528
|
-
await carryResumedSpend(
|
|
25171
|
+
await carryResumedSpend(agent.snapshot());
|
|
24529
25172
|
out.write(style.dim(`Resumed ${record.id} \u2014 ${record.title}
|
|
24530
25173
|
`));
|
|
24531
25174
|
if (interactive && !args.prompt)
|
|
24532
|
-
out.write(`${renderReplay(
|
|
25175
|
+
out.write(`${renderReplay(agent.snapshot(), sectionStyle(), { turns: 2 })}
|
|
24533
25176
|
`);
|
|
24534
25177
|
} else if (args.resume === "latest") {
|
|
24535
25178
|
out.write(style.yellow(`No matching session; starting a new one.
|
|
@@ -24997,7 +25640,7 @@ ${style.yellow(`Stopped at the ${formatMoney2(fromUnits2(args.budget, display))}
|
|
|
24997
25640
|
});
|
|
24998
25641
|
const ran = await runTurn(args.prompt);
|
|
24999
25642
|
if (ran && workspace.kind === "e2b") {
|
|
25000
|
-
const destination =
|
|
25643
|
+
const destination = path27.resolve(args.root, "archymedes-pull");
|
|
25001
25644
|
const pulled = await downloadProject(workspace, destination);
|
|
25002
25645
|
out.write(style.dim(` pulled ${pulled.written.length} files into ${destination}
|
|
25003
25646
|
`));
|
|
@@ -25007,18 +25650,30 @@ ${style.yellow(`Stopped at the ${formatMoney2(fromUnits2(args.budget, display))}
|
|
|
25007
25650
|
exitCleanly();
|
|
25008
25651
|
return args.json ? exitCodeForStatus(lastTurnStatus) : ran ? 0 : 1;
|
|
25009
25652
|
}
|
|
25010
|
-
const where = workspace.kind === "e2b" ? `sandbox ${workspace.label.split(":")[1]}` :
|
|
25011
|
-
|
|
25012
|
-
|
|
25013
|
-
|
|
25014
|
-
|
|
25015
|
-
|
|
25016
|
-
|
|
25017
|
-
|
|
25018
|
-
|
|
25019
|
-
|
|
25020
|
-
|
|
25021
|
-
|
|
25653
|
+
const where = workspace.kind === "e2b" ? `sandbox ${workspace.label.split(":")[1]}` : path27.basename(args.root);
|
|
25654
|
+
const identityMotion = new AbortController;
|
|
25655
|
+
const stopIdentityMotion = () => identityMotion.abort();
|
|
25656
|
+
if (ttyMode)
|
|
25657
|
+
process.stdin.on("data", stopIdentityMotion);
|
|
25658
|
+
try {
|
|
25659
|
+
await writeIdentity({
|
|
25660
|
+
width: process.stdout.columns ?? 80,
|
|
25661
|
+
rows: process.stdout.rows ?? 24,
|
|
25662
|
+
version: ARCHYMEDES_CLI_VERSION,
|
|
25663
|
+
workspace: where,
|
|
25664
|
+
model: `${spec.label} ${resolvedModelId}`,
|
|
25665
|
+
mode,
|
|
25666
|
+
palette,
|
|
25667
|
+
glyphs
|
|
25668
|
+
}, out, {
|
|
25669
|
+
enabled: ttyMode && !readline.line && environment.TERM !== "dumb" && environment.NO_COLOR === undefined && environment.ARCHYMEDES_NO_MOTION !== "1",
|
|
25670
|
+
signal: identityMotion.signal,
|
|
25671
|
+
size: () => ({ width: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 })
|
|
25672
|
+
});
|
|
25673
|
+
} finally {
|
|
25674
|
+
if (ttyMode)
|
|
25675
|
+
process.stdin.off("data", stopIdentityMotion);
|
|
25676
|
+
}
|
|
25022
25677
|
const context = [
|
|
25023
25678
|
renderReliabilityStatus(999, glyphs.middot),
|
|
25024
25679
|
`costs ${display}${preference.countryCode ? ` ${glyphs.middot} location ${preference.countryCode}` : ""} (${preference.source === "location" ? "auto-detected" : preference.source})`
|
|
@@ -25046,7 +25701,7 @@ ${style.yellow(`Stopped at the ${formatMoney2(fromUnits2(args.budget, display))}
|
|
|
25046
25701
|
`);
|
|
25047
25702
|
}
|
|
25048
25703
|
if (interactive && !args.prompt) {
|
|
25049
|
-
const starters = renderStarters(navContext(), sectionStyle(),
|
|
25704
|
+
const starters = renderStarters(navContext(), sectionStyle(), path27.basename(args.root));
|
|
25050
25705
|
if (starters)
|
|
25051
25706
|
out.write(`
|
|
25052
25707
|
${starters}
|
|
@@ -25870,8 +26525,8 @@ ${note4(`you ${glyphs.middot} ${files.user}`, style_)}
|
|
|
25870
26525
|
rows: process.stdout.rows ?? 24,
|
|
25871
26526
|
paths: projectFiles,
|
|
25872
26527
|
palette,
|
|
25873
|
-
readFile: async (
|
|
25874
|
-
const result = await workspace.readFile(
|
|
26528
|
+
readFile: async (path28) => {
|
|
26529
|
+
const result = await workspace.readFile(path28, { limit: 200 });
|
|
25875
26530
|
return { content: result.content, totalLines: result.totalLines, truncated: result.truncated };
|
|
25876
26531
|
}
|
|
25877
26532
|
});
|
|
@@ -26056,7 +26711,7 @@ ${note4(`you ${glyphs.middot} ${files.user}`, style_)}
|
|
|
26056
26711
|
if (record.mode)
|
|
26057
26712
|
mode = record.mode;
|
|
26058
26713
|
agent = await openClient(record);
|
|
26059
|
-
await carryResumedSpend(
|
|
26714
|
+
await carryResumedSpend(agent.snapshot());
|
|
26060
26715
|
expandables.clear();
|
|
26061
26716
|
out.write(`${renderReplay(record, style_, { turns: 2 })}
|
|
26062
26717
|
`);
|
|
@@ -26563,7 +27218,7 @@ ${renderKeyboardShortcuts(language)}
|
|
|
26563
27218
|
`));
|
|
26564
27219
|
continue;
|
|
26565
27220
|
}
|
|
26566
|
-
const destination =
|
|
27221
|
+
const destination = path27.resolve(args.root, input.split(/\s+/)[1] ?? "archymedes-pull");
|
|
26567
27222
|
const pulled = await downloadProject(workspace, destination);
|
|
26568
27223
|
out.write(style.green(` pulled ${pulled.written.length} files into ${destination}
|
|
26569
27224
|
`));
|
|
@@ -26584,7 +27239,7 @@ ${renderKeyboardShortcuts(language)}
|
|
|
26584
27239
|
}
|
|
26585
27240
|
if (input.startsWith("/voice")) {
|
|
26586
27241
|
const supplied = input.slice("/voice".length).trim();
|
|
26587
|
-
let audioFile = supplied ?
|
|
27242
|
+
let audioFile = supplied ? path27.resolve(args.root, supplied) : "";
|
|
26588
27243
|
let temporary = false;
|
|
26589
27244
|
try {
|
|
26590
27245
|
if (!audioFile) {
|