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.
@@ -34,7 +34,7 @@ import {
34
34
  turnFromChatResponse,
35
35
  wanderGradeCounts,
36
36
  wanderPlannerInstructions
37
- } from "./archymedes-h2x8ct7s.mjs";
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 path25 from "path";
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 randomUUID3 } from "node:crypto";
125
- import path12 from "node:path";
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_${randomUUID()}`,
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 digest = raw.length >= DEDUPE_MIN_CHARS ? createHash("sha256").update(raw).digest("hex") : undefined;
627
- const alreadySent = digest ? sentResults.get(digest) : undefined;
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 (digest && !alreadySent)
642
- sentResults.set(digest, { toolName: call.name, path: artifact?.path });
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 { promises as fs } from "node:fs";
665
- import path from "node:path";
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 ?? 0, stdout, stderr }));
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 fs.mkdir(path.dirname(this.indexFile), { recursive: true }).catch(() => {
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
- const checkpoint = this.latest();
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 result = await this.git(["diff", "--stat", checkpoint.tree, "--", ...EXCLUDE_ARCHYMEDES], { cwd: this.root });
742
- return result.exitCode === 0 ? result.stdout.trim() : "";
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 createHash2 } from "node:crypto";
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 (const character of command) {
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
- escaped = true;
785
- hasToken = true;
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 path2 = typeof args.path === "string" ? args.path : "";
1093
- if (SENSITIVE_PATH.test(path2))
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(path2))
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 createHash2("sha256").update(action).digest("hex");
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 fs2 } from "node:fs";
1508
+ import { promises as fs3 } from "node:fs";
1246
1509
  import os from "node:os";
1247
- import path2 from "node:path";
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 path2.resolve(environment.ARCHYMEDES_CONFIG_DIR);
1524
+ return path3.resolve(environment.ARCHYMEDES_CONFIG_DIR);
1262
1525
  if (platform === "win32")
1263
- return path2.join(environment.APPDATA?.trim() || path2.join(os.homedir(), "AppData", "Roaming"), "Archymedes");
1526
+ return path3.join(environment.APPDATA?.trim() || path3.join(os.homedir(), "AppData", "Roaming"), "Archymedes");
1264
1527
  if (platform === "darwin")
1265
- return path2.join(os.homedir(), "Library", "Application Support", "Archymedes");
1266
- return path2.join(environment.XDG_CONFIG_HOME?.trim() || path2.join(os.homedir(), ".config"), "archymedes");
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" ? path2.join(root, ".archymedes", "memory.md") : path2.join(archymedesConfigDirectory(environment), "memory.md");
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 fs2.readFile(memoryFile(scope, root, environment), "utf8"), scope);
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 fs2.mkdir(path2.dirname(file), { recursive: true, mode: 448 });
1589
+ await fs3.mkdir(path3.dirname(file), { recursive: true, mode: 448 });
1327
1590
  const temporary = `${file}.${process.pid}.tmp`;
1328
- await fs2.writeFile(temporary, formatMemoryFile(entries), { encoding: "utf8", mode: 384 });
1329
- await fs2.rename(temporary, file);
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 createHash3 } from "node:crypto";
1527
- import { promises as fs4 } from "node:fs";
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 path4 from "node:path";
1792
+ import path5 from "node:path";
1530
1793
 
1531
1794
  // packages/core/src/cli/workspace.ts
1532
- import { promises as fs3 } from "node:fs";
1533
- import path3 from "node:path";
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 = path3.resolve(root);
1550
- const resolved = path3.resolve(absoluteRoot, candidate);
1551
- if (resolved !== absoluteRoot && !resolved.startsWith(absoluteRoot + path3.sep)) {
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 fs3.realpath(path3.resolve(root)).catch(() => path3.resolve(root));
1559
- const real = await fs3.realpath(resolved).catch(() => null);
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 + path3.sep)) {
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 = path3.relative(path3.resolve(root), absolute);
1569
- return relative === "" ? "." : relative.split(path3.sep).join("/");
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 fs3.stat(absolute).catch((error) => {
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 fs3.readFile(absolute);
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, path3.dirname(candidate));
1617
- await fs3.mkdir(path3.dirname(absolute), { recursive: true });
1618
- await fs3.writeFile(absolute, content, "utf8");
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 = path3.resolve(root);
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 fs3.readdir(directory, { withFileTypes: true }) };
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 = path3.join(directory, entry.name);
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 fs3.stat(entry.absolute);
2000
+ const stat = await fs4.stat(entry.absolute);
1738
2001
  if (stat.size > limits.maxReadBytes)
1739
2002
  return [];
1740
- buffer = await fs3.readFile(entry.absolute);
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 = path4.resolve(root);
2492
- const temporaryRoot = path4.resolve(os3.tmpdir());
2493
- let current = path4.resolve(root);
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 fs4.stat(path4.join(current, ".git")).then(() => true).catch(() => false))
2758
+ if (current !== temporaryRoot && await fs5.stat(path5.join(current, ".git")).then(() => true).catch(() => false))
2496
2759
  return current;
2497
- const parent = path4.dirname(current);
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 = path4.resolve(root);; current = path4.dirname(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 = path4.join(directory, candidate);
2517
- const text = await fs4.readFile(file, "utf8");
2518
- discovered.push({ file, relative: path4.relative(boundary, file).split(path4.sep).join("/") || candidate, text });
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: createHash3("sha256").update(item.text).digest("hex"),
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 fs4.readdir(root, { withFileTypes: true });
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 fs4.readFile(path4.join(root, "package.json"), "utf8"));
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 fs4.readFile(path4.join(boundary, ".git", "HEAD"), "utf8");
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 createHash4 } from "node:crypto";
2707
- import { promises as fs5 } from "node:fs";
2708
- import path5 from "node:path";
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 digest = createHash4("sha256").update(value).digest("hex");
3091
+ const digest2 = createHash5("sha256").update(value).digest("hex");
2741
3092
  return `${redacted.slice(0, 4000)}
2742
- ...[${redacted.length - 4000} chars omitted; original sha256=${digest}]`;
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 createHash4("sha256").update(JSON.stringify(envelope)).digest("hex");
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 path5.join(root, ".archymedes", "events", `${sessionId}.jsonl`);
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 fs5.readFile(file, "utf8");
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
- const { events, text, completeText } = await readJournalFile(this.root, this.sessionId);
2819
- this.sequence = events.length;
2820
- this.previousHash = events.at(-1)?.hash ?? JOURNAL_GENESIS_HASH;
2821
- const file = eventJournalPath(this.root, this.sessionId);
2822
- await fs5.mkdir(path5.dirname(file), { recursive: true });
2823
- if (text.length !== completeText.length)
2824
- await fs5.truncate(file, Buffer.byteLength(completeText, "utf8"));
2825
- this.handle = await fs5.open(file, "a", 384);
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
- this.initialized ??= this.initialize();
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
- await this.flush();
2857
- await this.handle?.close();
2858
- this.handle = null;
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 createHash5, randomUUID as randomUUID2 } from "node:crypto";
2864
- import { promises as fs6 } from "node:fs";
2865
- import path6 from "node:path";
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 path6.join(root, ".archymedes", "sessions");
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 createHash5("sha256").update(JSON.stringify(canonicalize2(record))).digest("hex");
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 fs6.open(lockFile, "wx", 384);
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 fs6.unlink(lockFile).catch(() => {
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 fs6.stat(lockFile).catch(() => null);
3284
+ const stat = await fs8.stat(lockFile).catch(() => null);
2908
3285
  if (stat && Date.now() - stat.mtimeMs > 30000) {
2909
- await fs6.unlink(lockFile).catch(() => {
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 fs6.mkdir(directory, { recursive: true });
2923
- const file = path6.join(directory, `${record.id}.json`);
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}.${randomUUID2()}.tmp`;
3303
+ const temporary = `${file}.${process.pid}.${randomUUID5()}.tmp`;
2927
3304
  try {
2928
- const fileExists = await fs6.stat(file).then(() => true).catch(() => false);
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 fs6.open(temporary, "wx", 384);
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 fs6.rename(temporary, file);
2952
- const directoryHandle = await fs6.open(directory, "r").catch(() => null);
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 fs6.unlink(temporary).catch(() => {
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 fs6.readFile(path6.join(sessionDirectory(root), `${id}.json`), "utf8"));
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) => fs6.realpath(path6.resolve(candidate)).catch(() => path6.resolve(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.rejectAllPending(error));
3226
- this.child.on("exit", (code) => this.rejectAllPending(new Error(`MCP server '${config.id}' exited (code ${code})`)));
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.rejectAllPending(new Error(`MCP server '${this.config.id}' connection closed`));
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 path8 from "node:path";
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 path7 from "node:path";
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 = path7.resolve(this.root, relativeDirectory);
5941
- const relative = path7.relative(path7.resolve(this.root), directory);
5942
- if (relative.startsWith("..") || path7.isAbsolute(relative))
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(path9, options = {}) {
6076
- return readTextFile(this.root, path9, { ...options, limits: this.limits });
6465
+ readFile(path11, options = {}) {
6466
+ return readTextFile(this.root, path11, { ...options, limits: this.limits });
6077
6467
  }
6078
- writeFile(path9, content) {
6079
- return writeTextFile(this.root, path9, content, this.limits);
6468
+ writeFile(path11, content) {
6469
+ return writeTextFile(this.root, path11, content, this.limits);
6080
6470
  }
6081
- editFile(path9, oldText, newText, options = {}) {
6082
- return editTextFile(this.root, path9, oldText, newText, { ...options, limits: this.limits });
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 = path8.resolve(this.root, prefix);
6104
- if (!base.startsWith(path8.resolve(this.root)))
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 createHash6 } from "node:crypto";
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 digest = createHash6("sha256").update(content).digest("hex").slice(0, 12);
6150
- return `${ARTIFACT_DIRECTORY}/${safeToolName(toolName)}-${digest}.txt`;
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 path9 = artifactPathFor(input.toolName, input.content);
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(path9)) {
6168
- await this.workspace.writeFile(path9, body);
6169
- this.written.add(path9);
6557
+ if (!this.written.has(path11)) {
6558
+ await this.workspace.writeFile(path11, body);
6559
+ this.written.add(path11);
6170
6560
  }
6171
- return { path: path9, bytes: full, lines: countLines(input.content), elided };
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 path11 from "node:path";
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 path9 from "node:path";
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 = path9.resolve(environment.ARCHYMEDES_STATE_BINARY);
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 = path9.join(path9.dirname(manifest), "bin", process.platform === "win32" ? "archymedes-state.exe" : "archymedes-state");
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 = path9.resolve(process.cwd(), "packages", "archymedes-state", "target", profile, filename);
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 createHash7, createPublicKey, verify as verifySignature } from "node:crypto";
6405
- import { promises as fs7 } from "node:fs";
6406
- import path10 from "node:path";
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 path10.join(archymedesConfigDirectory(environment), "defender-brain");
6802
+ return path12.join(archymedesConfigDirectory(environment), "defender-brain");
6413
6803
  }
6414
6804
  function statePath(environment) {
6415
- return path10.join(feedRoot(environment), "feed-state.json");
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 fs7.readFile(statePath(environment), "utf8"));
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 fs7.mkdir(path10.dirname(file), { recursive: true });
6915
+ await fs9.mkdir(path12.dirname(file), { recursive: true });
6526
6916
  const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
6527
- await fs7.writeFile(temporary, `${JSON.stringify(value, null, 2)}
6917
+ await fs9.writeFile(temporary, `${JSON.stringify(value, null, 2)}
6528
6918
  `, { encoding: "utf8", mode: 384 });
6529
- await fs7.rename(temporary, file);
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 = path10.join(feedRoot(environment), "replicas", current.activeDigest);
6925
+ const directory = path12.join(feedRoot(environment), "replicas", current.activeDigest);
6536
6926
  try {
6537
- await fs7.access(path10.join(directory, DEFENDER_FEED_FILENAME));
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 (createHash7("sha256").update(bytes).digest("hex") !== manifest.corpus.sha256)
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 = path10.join(feedRoot(options.environment), "replicas", manifest.corpus.sha256);
6605
- await fs7.mkdir(replica, { recursive: true });
6606
- const corpusFile = path10.join(replica, DEFENDER_FEED_FILENAME);
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 fs7.access(corpusFile);
6998
+ await fs9.access(corpusFile);
6609
6999
  } catch {
6610
7000
  const temporary = `${corpusFile}.${process.pid}.${Date.now()}.tmp`;
6611
- await fs7.writeFile(temporary, bytes, { mode: 384, flag: "wx" });
7001
+ await fs9.writeFile(temporary, bytes, { mode: 384, flag: "wx" });
6612
7002
  try {
6613
- await fs7.rename(temporary, corpusFile);
7003
+ await fs9.rename(temporary, corpusFile);
6614
7004
  } catch (error) {
6615
- await fs7.rm(temporary, { force: true });
7005
+ await fs9.rm(temporary, { force: true });
6616
7006
  try {
6617
- await fs7.access(corpusFile);
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 = path11.dirname(fileURLToPath(moduleUrl));
7033
+ const directory = path13.dirname(fileURLToPath(moduleUrl));
6644
7034
  return [
6645
- path11.join(directory, "defender-knowledge"),
6646
- path11.resolve(directory, "../../../archymedes-state/defender-knowledge")
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(path12.join(options.root, ".archymedes", "security-brain"));
6797
- this.checkpoints = new CheckpointStore(options.root, path12.join(options.root, ".archymedes", "checkpoint-index"), options.git);
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(path13, options = {}) {
6887
- return this.workspace.readFile(path13, options);
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_${randomUUID3()}`,
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
- const turnId = `turn_${randomUUID3()}`;
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 randomUUID4 } from "node:crypto";
7485
- import path13 from "node:path";
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(path13.join(options.root, ".archymedes", "security-brain"));
7541
- this.checkpoints = new CheckpointStore(options.root, path13.join(options.root, ".archymedes", "checkpoint-index"), options.git);
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(path14, options = {}) {
7631
- return this.workspace.readFile(path14, options);
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_${randomUUID4()}`,
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
- const turnId = `turn_${randomUUID4()}`;
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 randomUUID5 } from "node:crypto";
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_${randomUUID5()}`;
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(path14, body, timeoutMs) {
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}${path14}`, {
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 ${path14} failed (${response.status}): ${text.slice(0, 240)}`);
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 randomUUID6 } from "node:crypto";
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 path14 = typeof args.path === "string" ? args.path : undefined;
11164
- if (!path14)
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: path14, content: args.content };
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: path14, oldText: args.oldText, newText: args.newText };
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_${randomUUID6()}`;
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_${randomUUID6()}`,
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_${randomUUID6()}`) {
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 createHash8, randomUUID as randomUUID7 } from "node:crypto";
11545
- import { promises as fs8 } from "node:fs";
11546
- import path14 from "node:path";
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 path14.join(root, ".archymedes", "sessions");
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 createHash8("sha256").update(JSON.stringify(canonicalize3(record))).digest("hex");
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 fs8.readFile(path14.join(sessionDirectory2(root), `${id}.json`), "utf8"));
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) => fs8.realpath(path14.resolve(candidate)).catch(() => path14.resolve(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 fs8.readdir(sessionDirectory2(root));
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(path15, body, timeoutMs) {
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}${path15}`, {
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 ${path15} failed (${response.status}): ${text.slice(0, 240)}`);
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 path15 from "node:path";
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(path16, options = {}) {
12068
- return readTextFile(this.root, path16, { ...options, limits: this.limits });
12652
+ readFile(path18, options = {}) {
12653
+ return readTextFile(this.root, path18, { ...options, limits: this.limits });
12069
12654
  }
12070
- writeFile(path16, content) {
12071
- return writeTextFile(this.root, path16, content, this.limits);
12655
+ writeFile(path18, content) {
12656
+ return writeTextFile(this.root, path18, content, this.limits);
12072
12657
  }
12073
- editFile(path16, oldText, newText, options = {}) {
12074
- return editTextFile(this.root, path16, oldText, newText, { ...options, limits: this.limits });
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 = path15.resolve(this.root, prefix);
12096
- if (!base.startsWith(path15.resolve(this.root)))
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(path16, options = {}) {
12169
- const absolute = this.absolute(path16);
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(path16, content) {
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(path16);
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(path16, oldText, newText, options = {}) {
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(path16);
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(path16, updated);
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 path16 of await workspace.glob("**/*")) {
12927
+ for (const path18 of await workspace.glob("**/*")) {
12343
12928
  try {
12344
- const file = await workspace.readFile(path16);
12345
- await writeTextFile(localRoot, path16, file.content, limits);
12346
- written.push(displayPath(localRoot, `${localRoot}/${path16}`));
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(path16);
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
- var INSTRUMENT = [" ╭───╮ ", " ╭─┴───┴─╮ ", " │ │ ", " ├── ● ──┤ ", " ╰─┬───┬─╯ ", " ╰───╯ "];
13081
- var ASCII_INSTRUMENT = [" .---. ", " .-+---+-. ", " | | ", " +-- O --+ ", " '-+---+-' ", " '---' "];
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 instrument = ascii ? ASCII_INSTRUMENT : INSTRUMENT;
13104
- const artWidth = Math.max(...instrument.map(visibleWidth));
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 instrument.map((line, index) => {
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 path16 = asString(args.path) ?? "";
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 path16;
14390
- return limit === undefined ? `${path16}:${offset}+` : `${path16}:${offset}-${offset + limit - 1}`;
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(([path16, delta]) => ({ path: path16, added: delta.added, removed: delta.removed })),
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 fs9 } from "node:fs";
16214
- import path16 from "node:path";
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: path16.basename(record.root)
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 = path16.join(record.root, ".archymedes", "exports");
16256
- await fs9.mkdir(directory, { recursive: true, mode: 448 });
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 = path16.join(directory, `${record.id}-${format}.${extension}`);
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 fs9.writeFile(file, body, { encoding: "utf8", mode: 384 });
16262
- await fs9.chmod(file, 384).catch(() => {
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.0.0",
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 fs10 } from "node:fs";
16944
- import path17 from "node:path";
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 path17.join(archymedesConfigDirectory(environment), "update-check.json");
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 fs10.readFile(stateFile(environment), "utf8"));
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 fs10.mkdir(path17.dirname(file), { recursive: true });
17009
- await fs10.writeFile(file, `${JSON.stringify(state, null, 2)}
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 fs11 } from "node:fs";
17904
- import os4 from "node:os";
17905
- import path18 from "node:path";
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 path18.resolve(environment.ARCHYMEDES_CONFIG_DIR);
18730
+ return path20.resolve(environment.ARCHYMEDES_CONFIG_DIR);
18090
18731
  if (platform === "win32")
18091
- return path18.join(environment.APPDATA?.trim() || path18.join(os4.homedir(), "AppData", "Roaming"), "Archymedes");
18732
+ return path20.join(environment.APPDATA?.trim() || path20.join(os5.homedir(), "AppData", "Roaming"), "Archymedes");
18092
18733
  if (platform === "darwin")
18093
- return path18.join(os4.homedir(), "Library", "Application Support", "Archymedes");
18094
- return path18.join(environment.XDG_CONFIG_HOME?.trim() || path18.join(os4.homedir(), ".config"), "archymedes");
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 path18.join(settingsDirectory(environment, platform), "settings.json");
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 fs11.readFile(settingsFile(environment, platform), "utf8")));
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 fs11.mkdir(directory, { recursive: true, mode: 448 });
18762
+ await fs13.mkdir(directory, { recursive: true, mode: 448 });
18122
18763
  const temporary = `${file}.${process.pid}.tmp`;
18123
- await fs11.writeFile(temporary, `${JSON.stringify(cleanSettings(settings), null, 2)}
18764
+ await fs13.writeFile(temporary, `${JSON.stringify(cleanSettings(settings), null, 2)}
18124
18765
  `, { encoding: "utf8", mode: 384 });
18125
- await fs11.chmod(temporary, 384).catch(() => {
18766
+ await fs13.chmod(temporary, 384).catch(() => {
18126
18767
  return;
18127
18768
  });
18128
- await fs11.rename(temporary, file);
18129
- await fs11.chmod(file, 384).catch(() => {
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 fs12 } from "node:fs";
18335
- import path20 from "node:path";
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 os5 from "node:os";
18339
- import path19 from "node:path";
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 path19.resolve(environment.ARCHYMEDES_CONFIG_DIR);
19114
+ return path21.resolve(environment.ARCHYMEDES_CONFIG_DIR);
18474
19115
  if (platform === "win32")
18475
- return path19.join(environment.APPDATA?.trim() || path19.join(os5.homedir(), "AppData", "Roaming"), "Archymedes");
19116
+ return path21.join(environment.APPDATA?.trim() || path21.join(os6.homedir(), "AppData", "Roaming"), "Archymedes");
18476
19117
  if (platform === "darwin")
18477
- return path19.join(os5.homedir(), "Library", "Application Support", "Archymedes");
18478
- return path19.join(environment.XDG_CONFIG_HOME?.trim() || path19.join(os5.homedir(), ".config"), "archymedes");
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 path20.join(settingsDirectory2(environment), "history.json");
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 fs12.readFile(historyFile(environment), "utf8"));
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 fs12.mkdir(path20.dirname(file), { recursive: true, mode: 448 });
19153
+ await fs14.mkdir(path22.dirname(file), { recursive: true, mode: 448 });
18513
19154
  const temporary = `${file}.${process.pid}.tmp`;
18514
- await fs12.writeFile(temporary, `${JSON.stringify(sanitizeHistory(lines), null, 2)}
19155
+ await fs14.writeFile(temporary, `${JSON.stringify(sanitizeHistory(lines), null, 2)}
18515
19156
  `, { encoding: "utf8", mode: 384 });
18516
- await fs12.rename(temporary, file);
18517
- await fs12.chmod(file, 384).catch(() => {
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 fs13 } from "node:fs";
18966
- import path21 from "node:path";
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 path21.join(archymedesConfigDirectory(environment), "models.json");
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 fs13.readFile(modelCacheFile(environment), "utf8"));
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 fs13.mkdir(path21.dirname(file), { recursive: true });
19645
+ await fs15.mkdir(path23.dirname(file), { recursive: true });
19005
19646
  const temporary = `${file}.${process.pid}.tmp`;
19006
- await fs13.writeFile(temporary, `${JSON.stringify(cache, null, 2)}
19647
+ await fs15.writeFile(temporary, `${JSON.stringify(cache, null, 2)}
19007
19648
  `, { mode: 384 });
19008
- await fs13.rename(temporary, file);
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 fs14 } from "node:fs";
19903
- import path22 from "node:path";
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" ? path22.join(root, ".archymedes", "themes") : path22.join(settingsDirectory2(environment), "themes");
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 fs14.readdir(directory);
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 = path22.join(directory, entry);
20557
+ const file = path24.join(directory, entry);
19917
20558
  try {
19918
- const contents = await fs14.readFile(file, "utf8");
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 fs15 } from "node:fs";
20220
- import path23 from "node:path";
20860
+ import { promises as fs17 } from "node:fs";
20861
+ import path25 from "node:path";
20221
20862
  function jobStoreFile(root) {
20222
- return path23.join(root, ".archymedes", "jobs.json");
20863
+ return path25.join(root, ".archymedes", "jobs.json");
20223
20864
  }
20224
20865
  function jobLogPath(root, id) {
20225
- return path23.join(root, ".archymedes", "jobs", `${id}.log`);
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 fs15.readFile(file, "utf8"));
20230
- return Array.isArray(parsed?.jobs) ? { jobs: parsed.jobs } : emptyStore();
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 fs15.open(lockFile, "wx", 384);
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 fs15.unlink(lockFile).catch(() => {
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 fs15.stat(lockFile).catch(() => null);
20895
+ const stat2 = await fs17.stat(lockFile).catch(() => null);
20253
20896
  if (stat2 && Date.now() - stat2.mtimeMs > 1e4) {
20254
- await fs15.unlink(lockFile).catch(() => {
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 fs15.mkdir(path23.dirname(file), { recursive: true });
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 fs15.open(temporary, "w", 384);
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 fs15.rename(temporary, file);
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 fs15.mkdir(path23.dirname(file), { recursive: true });
20357
- await fs15.appendFile(file, `${line}
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 fs15.open(jobLogPath(root, id), "r");
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 os6 from "node:os";
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 path24 = asString2(args.path) ?? "";
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 path24;
20406
- return limit === undefined ? `${path24}:${offset}+` : `${path24}:${offset}-${offset + limit - 1}`;
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 = os6.hostname(), pid = process.pid) {
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 fs16 } from "node:fs";
21054
- import os7 from "node:os";
21055
- import path24 from "node:path";
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 fs16.mkdtemp(path24.join(os7.tmpdir(), "archymedes-voice-"));
21063
- const file = path24.join(directory, "prompt.wav");
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 fs16.rm(directory, { recursive: true, force: true });
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 fs16.readFile(file);
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]), path24.basename(file));
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 fs16.rm(path24.dirname(file), { recursive: true, force: true });
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 = path25.resolve(argv[index + 1] ?? ".");
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 path26 = typeof args.path === "string" ? args.path : "";
23262
- if (!path26)
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(path26, renderFileChange({ path: path26, kind: "write", content }, style_, { maxLines: FOLD_AFTER_LINES }));
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(path26, renderFileChange({ path: path26, kind: "edit", before, after }, style_, { maxLines: FOLD_AFTER_LINES }));
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 path26 = typeof pending.arguments.path === "string" ? pending.arguments.path : undefined;
23408
- if (path26)
23409
- touchedFiles.add(path26);
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 (path26) {
23427
- const prior = sessionFiles.get(path26) ?? { added: 0, removed: 0 };
23428
- sessionFiles.set(path26, { added: prior.added + addedDelta, removed: prior.removed + removedDelta });
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(path25.dirname(logFile), { recursive: true });
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-0z4he0ev.mjs");
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-0z4he0ev.mjs");
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(path25.basename(args.root) || "archymedes", {
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(record);
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(record, sectionStyle(), { turns: 2 })}
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 = path25.resolve(args.root, "archymedes-pull");
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]}` : path25.basename(args.root);
25011
- out.write(`${renderIdentity({
25012
- width: process.stdout.columns ?? 80,
25013
- rows: process.stdout.rows ?? 24,
25014
- version: ARCHYMEDES_CLI_VERSION,
25015
- workspace: where,
25016
- model: `${spec.label} ${resolvedModelId}`,
25017
- mode,
25018
- palette,
25019
- glyphs
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(), path25.basename(args.root));
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 (path26) => {
25874
- const result = await workspace.readFile(path26, { limit: 200 });
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(record);
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 = path25.resolve(args.root, input.split(/\s+/)[1] ?? "archymedes-pull");
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 ? path25.resolve(args.root, supplied) : "";
27242
+ let audioFile = supplied ? path27.resolve(args.root, supplied) : "";
26588
27243
  let temporary = false;
26589
27244
  try {
26590
27245
  if (!audioFile) {