@sema-agent/core 7.23.1 → 7.23.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/agents/agent-transcript-tool.js +2 -1
  3. package/dist/agents/cascade.js +2 -1
  4. package/dist/agents/list-agents-tool.js +2 -1
  5. package/dist/agents/observer.js +3 -2
  6. package/dist/agents/peer-session-drain.js +2 -1
  7. package/dist/agents/retain-ledger.js +2 -1
  8. package/dist/agents/send-message-tool.js +11 -10
  9. package/dist/agents/subagent.js +18 -17
  10. package/dist/agents/teacher.js +5 -4
  11. package/dist/brain/circuit-breaker.js +2 -1
  12. package/dist/brain/degrading.js +2 -1
  13. package/dist/brain/errors.js +24 -15
  14. package/dist/brain/failover.js +2 -1
  15. package/dist/brain/stream-engine.js +2 -1
  16. package/dist/core/a2a.js +4 -3
  17. package/dist/core/artifact-host.js +2 -1
  18. package/dist/core/ask-question.js +3 -2
  19. package/dist/core/auto-promote.js +3 -2
  20. package/dist/core/gate-bounded-await.js +2 -1
  21. package/dist/core/gate-fold.js +4 -1
  22. package/dist/core/gate-lanes.js +4 -2
  23. package/dist/core/git-worktree-env.js +6 -5
  24. package/dist/core/hooks.js +4 -4
  25. package/dist/core/lsp-session.js +3 -2
  26. package/dist/core/mcp.js +4 -3
  27. package/dist/core/memory-admission.js +2 -1
  28. package/dist/core/memory-engine/consolidation-driver.js +5 -4
  29. package/dist/core/memory-engine/consolidation.js +2 -1
  30. package/dist/core/memory-engine/delegation-settlement.js +4 -3
  31. package/dist/core/memory-engine/distiller.js +8 -4
  32. package/dist/core/memory-engine/engine.js +63 -55
  33. package/dist/core/memory-engine/file-backend.js +11 -10
  34. package/dist/core/memory-engine/layout.js +5 -4
  35. package/dist/core/memory-engine/session-incarnation.js +2 -1
  36. package/dist/core/memory.js +2 -1
  37. package/dist/core/oracle-isolation.js +3 -2
  38. package/dist/core/permission-rule-store.js +2 -1
  39. package/dist/core/runner/announce-once-ledger.js +2 -1
  40. package/dist/core/runner/compaction-seams.js +3 -2
  41. package/dist/core/runner/gate-exit.d.ts +5 -0
  42. package/dist/core/runner/git-leg-delivery.js +4 -3
  43. package/dist/core/runner/git-status-frame.js +2 -1
  44. package/dist/core/runner/mcp-redial.js +2 -1
  45. package/dist/core/runner/memory-consolidation-driver.js +2 -1
  46. package/dist/core/runner/permission-rule-lanes.js +3 -2
  47. package/dist/core/runner/prepare-artifact.js +2 -1
  48. package/dist/core/runner/prepare-ask-lane.js +2 -1
  49. package/dist/core/runner/prepare-caps-and-workflow.js +4 -3
  50. package/dist/core/runner/prepare-inherited-gate.js +2 -1
  51. package/dist/core/runner/prepare-memory-engine-session.js +2 -1
  52. package/dist/core/runner/prepare-memory.js +5 -4
  53. package/dist/core/runner/prepare-policy-chain.js +3 -2
  54. package/dist/core/runner/prepare-project-context.js +3 -2
  55. package/dist/core/runner/prepare-protocol-tools.js +3 -2
  56. package/dist/core/runner/prepare-suspend-saga.js +2 -1
  57. package/dist/core/runner/prepare-task.js +6 -5
  58. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  59. package/dist/core/runner/resume-apply.js +5 -4
  60. package/dist/core/runner/resume-preflight.js +3 -2
  61. package/dist/core/runner/run-git-lane.js +4 -3
  62. package/dist/core/runner/run-leg.js +4 -3
  63. package/dist/core/runner/run-recovery-lanes.js +3 -2
  64. package/dist/core/runner/run-settle-and-teardown.js +2 -1
  65. package/dist/core/runner/run-stop-and-final-verify.js +2 -1
  66. package/dist/core/runner/run-telemetry-and-budget-seats.js +2 -1
  67. package/dist/core/runner/run-terminal-adoption.js +6 -5
  68. package/dist/core/runner/run-turn-boundary.js +5 -4
  69. package/dist/core/runner/runtask.js +2 -1
  70. package/dist/core/runner/stream-settle-backstop.js +5 -4
  71. package/dist/core/runner/stream-steer-verb.js +2 -1
  72. package/dist/core/runner/strict-output-schema.js +2 -1
  73. package/dist/core/runner/teardown-bounded.js +2 -1
  74. package/dist/core/safe-notify.js +2 -1
  75. package/dist/core/skills-directory.js +2 -1
  76. package/dist/core/strategy-store.js +2 -1
  77. package/dist/core/task-registry-agent.js +2 -1
  78. package/dist/core/task-registry-monitor.js +2 -1
  79. package/dist/core/task-registry-workflow.js +2 -1
  80. package/dist/core/task-registry.js +3 -2
  81. package/dist/core/thrown-value.d.ts +79 -0
  82. package/dist/core/thrown-value.js +56 -0
  83. package/dist/core/tool-errors.d.ts +0 -6
  84. package/dist/core/tool-errors.js +36 -14
  85. package/dist/core/tool-model-gate.js +2 -1
  86. package/dist/core/tool-policy.js +6 -13
  87. package/dist/core/tool-result-store.js +2 -1
  88. package/dist/core/tool-spec.d.ts +12 -0
  89. package/dist/core/write-protect.js +3 -2
  90. package/dist/engine/compaction/compaction.js +3 -4
  91. package/dist/engine/harness/agent-harness.js +2 -1
  92. package/dist/engine/harness/types.js +8 -7
  93. package/dist/engine/loop/agent-loop.js +7 -6
  94. package/dist/engine/lsp/node-lsp-manager.js +5 -4
  95. package/dist/engine/lsp/stdio-lsp-transport.js +12 -5
  96. package/dist/orchestration/dev-vm-script-runner.js +2 -1
  97. package/dist/orchestration/run-workflow-tool.js +7 -6
  98. package/dist/orchestration/workflow-governance.js +2 -1
  99. package/dist/orchestration/workflow-sandbox-conformance.js +3 -2
  100. package/dist/orchestration/workflow-script-store.js +2 -1
  101. package/dist/orchestration/workflow.js +15 -14
  102. package/dist/server/http.js +3 -2
  103. package/dist/stores/file/adoption/adopt.js +3 -2
  104. package/dist/stores/file/fs-atomic.js +2 -1
  105. package/dist/stores/file/permission-rule-adopt.js +2 -1
  106. package/dist/stores/file/permission-rule-store.js +2 -1
  107. package/dist/stores/file/session-store.js +4 -3
  108. package/dist/stores/file/strategy-store.js +7 -6
  109. package/dist/tools/artifact/artifact-tool.js +3 -2
  110. package/dist/tools/artifact/conformance.js +4 -3
  111. package/dist/tools/fs/bash-readonly-classifier.d.ts +20 -9
  112. package/dist/tools/fs/bash-readonly-classifier.js +7 -3
  113. package/dist/tools/fs/fs-bash.d.ts +16 -10
  114. package/dist/tools/fs/fs-bash.js +84 -35
  115. package/dist/tools/fs/fs-write.js +3 -2
  116. package/dist/tools/fs/safety.js +2 -1
  117. package/dist/tools/fs/search.js +3 -2
  118. package/dist/tools/monitor.js +3 -2
  119. package/dist/tools/sql.js +2 -1
  120. package/dist/tools/web.js +5 -15
  121. package/dist/tools/worktree.js +12 -9
  122. package/package.json +2 -1
  123. package/wire-changes/core@7.23.2.json +42 -0
  124. package/wire-changes/core@7.23.3.json +25 -0
@@ -23,6 +23,7 @@ import { compileOutputSchema } from "../core/runner/strict-output-schema.js";
23
23
  import { WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentBlockedError, WorkflowAgentParkedError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowMaxAgentsError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } from "./workflow-types.js";
24
24
  import { WorkflowScriptError } from "./workflow-meta.js";
25
25
  import { STALL_CONNECT_MS, STALL_FIRST_TOKEN_MS } from "../brain/timeout.js";
26
+ import { asThrownError, thrownText } from "../core/thrown-value.js";
26
27
  export * from "./workflow-types.js";
27
28
  const MAX_TRANSCRIPT_CHARS = 4000;
28
29
  const WORKFLOW_RESULT_MAX = 4000;
@@ -193,7 +194,7 @@ function foldFanOutSettled(settled, lane, errorsOut, emitRunLog) {
193
194
  }
194
195
  rethrowIfMaxAgents(s.e);
195
196
  errorsOut?.push(workflowFanOutSlotError(s.i, s.e));
196
- emitRunLog(`${lane}[${s.i}] failed: ${boundedRedactedSummary(s.e instanceof Error ? s.e.message : String(s.e), 300)}`);
197
+ emitRunLog(`${lane}[${s.i}] failed: ${boundedRedactedSummary(thrownText(s.e), 300)}`);
197
198
  return null;
198
199
  });
199
200
  if (dropped > 0)
@@ -203,7 +204,7 @@ function foldFanOutSettled(settled, lane, errorsOut, emitRunLog) {
203
204
  function workflowFanOutSlotError(index, e) {
204
205
  const code = e?.code;
205
206
  const kind = typeof code === "string" && code.length > 0 ? code : e instanceof Error ? e.name : "Error";
206
- return { index, kind, message: boundedRedactedSummary(e instanceof Error ? e.message : String(e), 300) };
207
+ return { index, kind, message: boundedRedactedSummary(thrownText(e), 300) };
207
208
  }
208
209
  export class WorkflowResultTooLargeError extends Error {
209
210
  size;
@@ -841,7 +842,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
841
842
  };
842
843
  const parkSeat = opts.checkpointStore;
843
844
  const parkedByOrdinal = [];
844
- const errText = (err) => (err instanceof Error ? err.message : String(err));
845
+ const errText = (err) => (thrownText(err));
845
846
  const withoutToken = (text, token) => (token === "" ? text : text.split(token).join("<token>"));
846
847
  const parkBindingBroken = (ordinal, why) => new WorkflowParkRefusal("workflow.park_binding_broken", `workflow resume: ordinal ${ordinal}'s park cannot be bound to its checkpoint (${why}) — the recorded park is inconsistent. Decide the checkpoint through the deployment's approval channel and start a fresh run.`, { ordinal });
847
848
  const readParkTruth = async (seat, key, ordinal) => {
@@ -980,7 +981,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
980
981
  }
981
982
  catch (err) {
982
983
  if (dbg)
983
- console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} TOMBSTONE-ERROR ${err instanceof Error ? err.message : String(err)}`);
984
+ console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} TOMBSTONE-ERROR ${thrownText(err)}`);
984
985
  }
985
986
  return;
986
987
  }
@@ -993,7 +994,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
993
994
  }
994
995
  catch (err) {
995
996
  if (dbg)
996
- console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} APPEND-ERROR ${err instanceof Error ? err.message : String(err)}`);
997
+ console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} APPEND-ERROR ${thrownText(err)}`);
997
998
  throw err;
998
999
  }
999
1000
  };
@@ -1271,7 +1272,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1271
1272
  inheritedModelSnap = opts.defaultModel?.();
1272
1273
  }
1273
1274
  catch (err) {
1274
- inheritedModelError = err instanceof Error ? err : new Error(String(err));
1275
+ inheritedModelError = asThrownError(err);
1275
1276
  }
1276
1277
  }
1277
1278
  const inheritedThinkingSnap = spec.thinking === undefined ? opts.defaultThinking?.() : undefined;
@@ -1781,17 +1782,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1781
1782
  const journaled = journalAppend(callKey, salvaged ?? {
1782
1783
  taskId: callKey,
1783
1784
  sessionId: "",
1784
- result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1785
+ result: boundedRedactedSummary(thrownText(err), 500),
1785
1786
  terminal: {
1786
1787
  kind: "failed",
1787
1788
  ...(err instanceof WorkflowAgentBlockedError ? { code: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } : err instanceof WorkflowBudgetExceededError ? { code: err.code } : {}),
1788
- message: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1789
+ message: boundedRedactedSummary(thrownText(err), 500),
1789
1790
  },
1790
1791
  stats: { turns: 0, tokens: 0, costMicroUsd: 0, usageMissing: true },
1791
1792
  }, label).catch(() => undefined);
1792
1793
  if (!((err instanceof WorkflowBudgetExceededError || err instanceof WorkflowAgentParkedError) && rec.startedAt === undefined))
1793
1794
  await journaled;
1794
- bceTerminal(callKey, "failed", rec.output ?? (err instanceof Error ? err.message : String(err)), rec.sessionId, rec.stats);
1795
+ bceTerminal(callKey, "failed", rec.output ?? (thrownText(err)), rec.sessionId, rec.stats);
1795
1796
  }
1796
1797
  throw err;
1797
1798
  }
@@ -1969,7 +1970,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1969
1970
  }
1970
1971
  recordFailed();
1971
1972
  releaseLeg();
1972
- bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, undefined);
1973
+ bceTerminal(callKey, "failed", thrownText(err), undefined, undefined);
1973
1974
  throw err;
1974
1975
  }
1975
1976
  if (childSessionId !== undefined && !finalized) {
@@ -2012,11 +2013,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
2012
2013
  void journalAppend(callKey, {
2013
2014
  taskId: callKey,
2014
2015
  sessionId: "",
2015
- terminal: { kind: "failed", message: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500) },
2016
- result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
2016
+ terminal: { kind: "failed", message: boundedRedactedSummary(thrownText(err), 500) },
2017
+ result: boundedRedactedSummary(thrownText(err), 500),
2017
2018
  stats: { turns: partialSpend?.turns ?? 0, tokens: partialSpend?.tokens ?? 0, costMicroUsd: partialSpend?.costMicroUsd ?? 0 },
2018
2019
  }).catch(() => undefined);
2019
- bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, rec.stats);
2020
+ bceTerminal(callKey, "failed", thrownText(err), undefined, rec.stats);
2020
2021
  throw err;
2021
2022
  }
2022
2023
  rollbackUsageBeat();
@@ -2377,7 +2378,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
2377
2378
  run.endedAt = now();
2378
2379
  if (parkedLeg !== undefined && !(err instanceof WorkflowAgentParkedError))
2379
2380
  err = parkedError();
2380
- run.error = err instanceof Error ? err.message : String(err);
2381
+ run.error = thrownText(err);
2381
2382
  closeAbandonedAgents(run.endedAt);
2382
2383
  {
2383
2384
  let thrownCode;
@@ -1,4 +1,5 @@
1
1
  import http from "node:http";
2
+ import { thrownText } from "../core/thrown-value.js";
2
3
  const DEFAULT_MAX_BODY = 5 * 1024 * 1024;
3
4
  class HttpError extends Error {
4
5
  status;
@@ -25,7 +26,7 @@ async function readJson(req, maxBytes) {
25
26
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
26
27
  }
27
28
  catch (e) {
28
- throw new HttpError(400, `invalid JSON request body: ${e instanceof Error ? e.message : String(e)}`);
29
+ throw new HttpError(400, `invalid JSON request body: ${thrownText(e)}`);
29
30
  }
30
31
  }
31
32
  function setCors(res, origin) {
@@ -47,7 +48,7 @@ export function createTaskServer(opts) {
47
48
  void handle(req, res).catch((err) => {
48
49
  if (!res.headersSent) {
49
50
  const status = err instanceof HttpError ? err.status : 500;
50
- sendJson(res, status, { error: err instanceof Error ? err.message : String(err) });
51
+ sendJson(res, status, { error: thrownText(err) });
51
52
  }
52
53
  else {
53
54
  res.end();
@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { adoptFilePermissionRuleStore } from "../permission-rule-adopt.js";
5
5
  import { BootLock, atomicWriteFile, ensureDir, sanitizeScope } from "../fs-atomic.js";
6
+ import { thrownText } from "../../../core/thrown-value.js";
6
7
  import { AdoptionError, NOT_MIGRATED_BY_DESIGN, readRootAdoptionFile, writeRootAdoptionFile, } from "./marker.js";
7
8
  const CONFIG_ACCOUNT_FILE = "adoption-configs.json";
8
9
  function baselineConfigs(toPrincipal) {
@@ -491,7 +492,7 @@ export async function adoptLocalDataRoot(opts) {
491
492
  catch (err) {
492
493
  if (err instanceof AdoptionError)
493
494
  throw err;
494
- throw new AdoptionError("adoption_rule_leg_refused", `the permission-rule leg refused: ${err instanceof Error ? err.message : String(err)}`);
495
+ throw new AdoptionError("adoption_rule_leg_refused", `the permission-rule leg refused: ${thrownText(err)}`);
495
496
  }
496
497
  if (nested.status === "stalled") {
497
498
  return {
@@ -533,7 +534,7 @@ export async function adoptLocalDataRoot(opts) {
533
534
  return {
534
535
  status: "stalled",
535
536
  phase: 4,
536
- error: `the "${leg.store}" carriage leg failed: ${err instanceof Error ? err.message : String(err)} — resume when it is healthy`,
537
+ error: `the "${leg.store}" carriage leg failed: ${thrownText(err)} — resume when it is healthy`,
537
538
  };
538
539
  }
539
540
  if (rows !== undefined && (!Number.isSafeInteger(rows) || rows < 0)) {
@@ -5,6 +5,7 @@ import { basename, dirname, join, resolve as resolvePath } from "node:path";
5
5
  import { createHash, randomBytes } from "node:crypto";
6
6
  import { execFileSync } from "node:child_process";
7
7
  import { physicalPathOf, physicalPathOfExisting } from "../../core/physical-path.js";
8
+ import { thrownText } from "../../core/thrown-value.js";
8
9
  const SAFE_COMPONENT = /^[A-Za-z0-9_.-]+$/;
9
10
  export function sanitizePathComponent(raw) {
10
11
  if (raw === "" || raw === "." || raw === ".." || !SAFE_COMPONENT.test(raw)) {
@@ -411,7 +412,7 @@ export class BootLock {
411
412
  }
412
413
  catch (err) {
413
414
  throw new FileStoreLockError("store.lock_unreadable", `file store: this process still owns the lock file at ${this.lockPath} from an earlier release that ` +
414
- `could not remove it, and removing it now failed too (${String(err.message)}); ` +
415
+ `could not remove it, and removing it now failed too (${thrownText(err)}); ` +
415
416
  `remove it manually`, this.lockPath, process.pid);
416
417
  }
417
418
  abandonedLocks.delete(this.abandonKey);
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { writerOf } from "../../core/permission-rule-store.js";
4
4
  import { createPermissionRuleStoreProvider } from "../../core/permission-rule-provider.js";
5
5
  import { syncPermissionRules } from "../../core/permission-rule-sync.js";
6
+ import { thrownText } from "../../core/thrown-value.js";
6
7
  import { FileDurableRulePartitionProvider, LOCAL_OWNER_FILE, principalRuleFileName, readRuleAdoptionFile, writeRuleAdoptionFile, } from "./permission-rule-store.js";
7
8
  export async function adoptFilePermissionRuleStore(opts) {
8
9
  if (typeof opts.toPrincipal !== "string" || opts.toPrincipal === "") {
@@ -63,7 +64,7 @@ export async function adoptFilePermissionRuleStore(opts) {
63
64
  });
64
65
  }
65
66
  catch (err) {
66
- return { status: "stalled", phase: 4, error: `the first sync round failed: ${err instanceof Error ? err.message : String(err)}` };
67
+ return { status: "stalled", phase: 4, error: `the first sync round failed: ${thrownText(err)}` };
67
68
  }
68
69
  if (!syncResult.ok) {
69
70
  return {
@@ -6,6 +6,7 @@ import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDel
6
6
  import { canonicalize } from "../../core/canonical-json.js";
7
7
  import { BootLock } from "./fs-atomic.js";
8
8
  import { assertAdoptionBootGate } from "./adoption/marker.js";
9
+ import { thrownText } from "../../core/thrown-value.js";
9
10
  function isRuleFile(v) {
10
11
  const p = v;
11
12
  return typeof p === "object" && p !== null && p.schemaVersion === 4 && Array.isArray(p.rules) && Array.isArray(p.tombstones) && typeof p.rev === "number" && (p.quarantined === undefined || Array.isArray(p.quarantined));
@@ -139,7 +140,7 @@ class FileDurableRulePartition {
139
140
  const e = err;
140
141
  if (e?.code === "ENOENT")
141
142
  return { absent: true };
142
- const why = `could not read ${this.file}: ${e?.message ?? String(err)}; loading zero rules`;
143
+ const why = `could not read ${this.file}: ${thrownText(err)}; loading zero rules`;
143
144
  this.disclose(why);
144
145
  return { unreadable: why };
145
146
  }
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { BaseSessionStorage, StoredSession, SessionError, getEntriesToFork, uuidv7, validateEntriesForImport, } from "../../internal/harness.js";
4
4
  import { canonicalStoreKey, AppendLog, atomicWriteFile, ensureDir, readJsonlRecords, sanitizePathComponent } from "./fs-atomic.js";
5
5
  import { assertAdoptionBootGate } from "./adoption/marker.js";
6
+ import { thrownText } from "../../core/thrown-value.js";
6
7
  function containSinkThenable(r) {
7
8
  if (typeof r?.then === "function") {
8
9
  r.then(undefined, () => {
@@ -171,7 +172,7 @@ export class FileSessionRepo {
171
172
  }
172
173
  catch (err) {
173
174
  if (!(err instanceof SessionError && err.code === "not_found")) {
174
- this.disclose(this.pathFor(id), `session log unreadable (${err instanceof Error ? err.message : String(err)}) — skipped from the listing`);
175
+ this.disclose(this.pathFor(id), `session log unreadable (${thrownText(err)}) — skipped from the listing`);
175
176
  }
176
177
  }
177
178
  }
@@ -200,7 +201,7 @@ export class FileSessionRepo {
200
201
  }
201
202
  catch (err) {
202
203
  if (!(err instanceof SessionError && err.code === "not_found")) {
203
- this.disclose(this.pathFor(id), `session log unreadable (${err instanceof Error ? err.message : String(err)}) — skipped from the placed listing`);
204
+ this.disclose(this.pathFor(id), `session log unreadable (${thrownText(err)}) — skipped from the placed listing`);
204
205
  }
205
206
  continue;
206
207
  }
@@ -238,7 +239,7 @@ export class FileSessionRepo {
238
239
  rmSync(this.pathFor(metadata.id), { force: true });
239
240
  }
240
241
  catch (err) {
241
- throw new SessionError("storage", `session ${metadata.id} could not be deleted: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : undefined);
242
+ throw new SessionError("storage", `session ${metadata.id} could not be deleted: ${thrownText(err)}`, err instanceof Error ? err : undefined);
242
243
  }
243
244
  }
244
245
  async fork(sourceMetadata, options = {}) {
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
4
  import { atomicWriteFile } from "./fs-atomic.js";
5
5
  import { createSafeNotifier, observeThenableRejection } from "../../core/safe-notify.js";
6
+ import { thrownText } from "../../core/thrown-value.js";
6
7
  import { MAX_STRATEGY_INJECTION_TOTAL_BYTES, MAX_STRATEGY_TEXT_BYTES, compileStrategyQuery, resolveCapacityCap, resolveFindLimit, scoreStoredStrategy, storedStrategyShapeIssue, strategyDedupKey, validateStrategyForWrite, } from "../../core/strategy-store.js";
7
8
  function configRefusal(message, code) {
8
9
  const e = new Error(message);
@@ -32,14 +33,14 @@ export class FileStrategyStore {
32
33
  mkdirSync(root, { recursive: true, mode: 0o700 });
33
34
  }
34
35
  catch (err) {
35
- throw configRefusal(`FileStrategyStore: cannot create root ${root}: ${String(err.message ?? err)}`, "config.strategy_root_invalid");
36
+ throw configRefusal(`FileStrategyStore: cannot create root ${root}: ${thrownText(err)}`, "config.strategy_root_invalid");
36
37
  }
37
38
  let st;
38
39
  try {
39
40
  st = statSync(root);
40
41
  }
41
42
  catch (err) {
42
- throw configRefusal(`FileStrategyStore: cannot stat root ${root}: ${String(err.message ?? err)}`, "config.strategy_root_invalid");
43
+ throw configRefusal(`FileStrategyStore: cannot stat root ${root}: ${thrownText(err)}`, "config.strategy_root_invalid");
43
44
  }
44
45
  if (!st.isDirectory()) {
45
46
  throw configRefusal(`FileStrategyStore: root ${root} exists and is not a directory`, "config.strategy_root_invalid");
@@ -145,7 +146,7 @@ export class FileStrategyStore {
145
146
  catch (err) {
146
147
  if (err.code === "ENOENT")
147
148
  continue;
148
- this.incident({ op: "find", error: `unstat-able entry skipped: ${String(err.message ?? err)}`, path: p });
149
+ this.incident({ op: "find", error: `unstat-able entry skipped: ${thrownText(err)}`, path: p });
149
150
  continue;
150
151
  }
151
152
  if (!fst.isFile()) {
@@ -163,7 +164,7 @@ export class FileStrategyStore {
163
164
  catch (err) {
164
165
  if (err.code === "ENOENT")
165
166
  continue;
166
- this.incident({ op: "find", error: `unreadable entry skipped: ${String(err.message ?? err)}`, path: p });
167
+ this.incident({ op: "find", error: `unreadable entry skipped: ${thrownText(err)}`, path: p });
167
168
  continue;
168
169
  }
169
170
  let parsed;
@@ -217,7 +218,7 @@ export class FileStrategyStore {
217
218
  this.incident({ op: "parse", error: `corrupt entry quarantined (${reason})`, path: p });
218
219
  }
219
220
  catch (err) {
220
- this.incident({ op: "parse", error: `corrupt entry skipped (${reason}); quarantine rename failed: ${String(err.message ?? err)}`, path: p });
221
+ this.incident({ op: "parse", error: `corrupt entry skipped (${reason}); quarantine rename failed: ${thrownText(err)}`, path: p });
221
222
  }
222
223
  }
223
224
  writeEntry(dir, path, entry) {
@@ -286,7 +287,7 @@ export class FileStrategyStore {
286
287
  catch (err) {
287
288
  if (err.code === "ENOENT")
288
289
  continue;
289
- this.incident({ op: "evict", error: String(err.message ?? err), id: le.entry.id, path: le.path });
290
+ this.incident({ op: "evict", error: thrownText(err), id: le.entry.id, path: le.path });
290
291
  }
291
292
  }
292
293
  }
@@ -7,6 +7,7 @@ import { errorResult } from "../../core/tools.js";
7
7
  import { inlineUntrusted } from "../../core/untrusted-text.js";
8
8
  import { actionDescription, artifactDescription, PARAM_TEXT, urlDescription } from "./artifact-text.js";
9
9
  import { ARTIFACT_REJECT_BREAKER_THRESHOLD, artifactRejectBannerFor, artifactRejectKeyOf, createArtifactRejectBreaker } from "./reject-breaker.js";
10
+ import { thrownText } from "../../core/thrown-value.js";
10
11
  export const MAX_FILES_PER_PUBLISH = 255;
11
12
  const LIST_LIMIT_DEFAULT = 25;
12
13
  const LIST_LIMIT_MAX = 100;
@@ -93,10 +94,10 @@ function callHost(action, fn, signal) {
93
94
  }
94
95
  catch (err) {
95
96
  signal?.removeEventListener("abort", onAbort);
96
- resolve({ ok: false, kind: "threw", message: err instanceof Error ? err.message : String(err) });
97
+ resolve({ ok: false, kind: "threw", message: thrownText(err) });
97
98
  return;
98
99
  }
99
- p.then((value) => resolve(OUTCOME_SHAPE[action](value) ? { ok: true, value } : { ok: false, kind: "malformed", message: `the ${action} answer was outside the host contract` }), (err) => resolve({ ok: false, kind: "threw", message: err instanceof Error ? err.message : String(err) })).finally(() => signal?.removeEventListener("abort", onAbort));
100
+ p.then((value) => resolve(OUTCOME_SHAPE[action](value) ? { ok: true, value } : { ok: false, kind: "malformed", message: `the ${action} answer was outside the host contract` }), (err) => resolve({ ok: false, kind: "threw", message: thrownText(err) })).finally(() => signal?.removeEventListener("abort", onAbort));
100
101
  });
101
102
  }
102
103
  function buildSchema(caps) {
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { thrownText } from "../../core/thrown-value.js";
2
3
  import { readArtifactHostCapabilities, } from "../../core/artifact-host.js";
3
4
  class Violation extends Error {
4
5
  }
@@ -25,7 +26,7 @@ async function publish(h, r, attemptId) {
25
26
  return await h.host.publish(r, ctxOf(h, attemptId));
26
27
  }
27
28
  catch (err) {
28
- return fail(`publish threw: ${err instanceof Error ? err.message : String(err)}`);
29
+ return fail(`publish threw: ${thrownText(err)}`);
29
30
  }
30
31
  }
31
32
  async function publishOk(h, r, what, attemptId) {
@@ -282,7 +283,7 @@ define({ id: "read.unknown_url_typed", describe: "`read` of a url this host neve
282
283
  out = await h.host.read({ url: FOREIGN_URL }, ctxOf(h));
283
284
  }
284
285
  catch (err) {
285
- return fail(`read threw: ${err instanceof Error ? err.message : String(err)}`);
286
+ return fail(`read threw: ${thrownText(err)}`);
286
287
  }
287
288
  check(!out.ok && (out.code === "not_found" || out.code === "forbidden"), `an unknown url answered ${out.ok ? "ok" : out.code}`);
288
289
  return out.ok ? "" : out.code;
@@ -340,7 +341,7 @@ export async function runArtifactConformance(host, opts = {}) {
340
341
  row = { id: v.id, status: "pass", detail: await v.run(h) };
341
342
  }
342
343
  catch (err) {
343
- row = { id: v.id, status: "fail", detail: err instanceof Violation ? err.message : `threw: ${err instanceof Error ? err.message : String(err)}` };
344
+ row = { id: v.id, status: "fail", detail: err instanceof Violation ? err.message : `threw: ${thrownText(err)}` };
344
345
  }
345
346
  results.push(row);
346
347
  }
@@ -284,10 +284,12 @@ export interface BashReadonlyRootBoundary {
284
284
  * undecided contract. What REMAINS of ① is the form table's stated open set (see the KNOWN OPEN
285
285
  * SET note at {@link RECURSIVE_READ_FORMS}: a traversal verb absent there that a deployment
286
286
  * allowlists classifies by its ordinary form and can auto-run a traversal unjudged);
287
- * ② LEXICAL only — an in-root symlink whose target is a
288
- * guarded path reads as its innocent spelling here, exactly as it does for the containment half
289
- * (the enforcing/canonicalizing recheck is both shell faces' execute leg, via checkedPaths; the
290
- * classify auto-allow lane has no I/O seat by contract). The structured read faces judge BOTH
287
+ * ② LEXICAL only HERE — an in-root symlink whose target is a
288
+ * guarded path reads as its innocent spelling at this seat, exactly as it does for the containment half,
289
+ * because the classify auto-allow lane has no I/O seat by contract. The CANONICAL view is judged by the
290
+ * full shell's execute-time recheck, which asks THIS judge (the same function object, forwarded) where
291
+ * each candidate really lands — a `checkedPaths` literal and a `undecidedPaths` pattern's expansions
292
+ * alike. The `bash_readonly` face wires no deny seat at all, so its own recheck carries nothing. The structured read faces judge BOTH
291
293
  * views (canonical + lexical); what this seat is and is not is stated once, at
292
294
  * `shell.read_boundary.friction_floor`. The `bash_readonly` face deliberately does NOT wire this seat (its
293
295
  * roots containment + command allowlist double gate is the deployment's own read-safety
@@ -527,7 +529,16 @@ export declare function resolveOperandSpellings(base: string | undefined, operan
527
529
  export declare function refusedCommandMayRead(command: string, allow: ReadonlySet<string>, boundary: BashReadonlyRootBoundary | undefined,
528
530
  /** The {@link ShellScanDecline} members THIS SEAT reads as evidence (tables and argument: `fs-bash.ts`); one
529
531
  * the table omits ends the scan with "exempt". */
530
- declinesAreEvidence: ReadonlySet<ShellScanDecline>): boolean;
532
+ declinesAreEvidence: ReadonlySet<ShellScanDecline>): RefusedCommandScan;
533
+ /** What ONE scan of a refused command found — TWO facts, because a seat's table answers only the first. `mayRead`
534
+ * is the whitelist's verdict ("may read a file the boundary never judged"), which a seat turns into a MANDATE.
535
+ * `declined` is the scan's own report that it could not judge where a segment reads AT ALL
536
+ * ({@link SHELL_SCAN_DECLINES}), recorded BEFORE the seat's table and so the SAME on every seat: a seat omitting
537
+ * that decline has ruled only that an ANSWERER may retire the ask, never that nothing happened. */
538
+ export interface RefusedCommandScan {
539
+ readonly mayRead: boolean;
540
+ readonly declined: boolean;
541
+ }
531
542
  /**
532
543
  * The home directory's VARIABLE spellings, SUBSTITUTED with the declared value before a read face segments the
533
544
  * command: `$HOME` / `${HOME}` at the START of a word (an empty quote pair before it included), unquoted or
@@ -645,10 +656,10 @@ export declare const COMMAND_LAUNCHERS_FOR_TEST: ReadonlySet<string>;
645
656
  * base across segments) and dropping it would place every later relative operand against a base bash
646
657
  * has already left. A move the walk cannot model (`pushd`/`popd`) is not silently tolerated either —
647
658
  * see the `paths`-less gated answer below.
648
- * · The CONTAINMENT half only: {@link BashReadonlyRootBoundary.denyMatch} is deliberately NOT passed
649
- * on. The deny judge outranks containment inside the walk and returns early, which would let a
650
- * deny-listed operand HIDE the fact that it also sits outside the roots — the deny demotion is
651
- * unchanged on the full face, and this one keeps answering its own question underneath it.
659
+ * · BOTH halves from ONE extraction, in TWO passes: the CONTAINMENT pass alone is deny-free, because
660
+ * {@link BashReadonlyRootBoundary.denyMatch} outranks containment inside the walk and returns early,
661
+ * which would let a deny-listed operand HIDE the fact that it also sits outside the roots. A SECOND
662
+ * pass over the SAME readers does carry that judge, and is what answers `denied` (see below).
652
663
  * · `face: "open"` is honoured: under the open read face containment is structurally satisfied, an
653
664
  * out-of-roots operand is not a finding, and this gate is silent — a full-read deployment sees no
654
665
  * gate here, by the same one mechanism the face already defines.
@@ -257,7 +257,9 @@ function tokensMoveBase(toks) {
257
257
  }
258
258
  const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
259
259
  export function refusedCommandMayRead(command, allow, boundary, declinesAreEvidence) {
260
- return !refusedCommandExempt(command, allow, boundary, declinesAreEvidence, 0);
260
+ const scan = { declined: false };
261
+ const exempt = refusedCommandExempt(command, allow, boundary, declinesAreEvidence, scan, 0);
262
+ return { mayRead: !exempt, declined: scan.declined };
261
263
  }
262
264
  const STDIN_ONLY_READER_FLAGS = new Map([
263
265
  ["cat", /^-[AbeEnstTuv]+$/],
@@ -279,7 +281,7 @@ function whitelistSegment(segment) {
279
281
  redirects: tokens.filter((t) => t.kind === "redirect"),
280
282
  };
281
283
  }
282
- function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, depth, baseMovedOutside = false) {
284
+ function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, scan, depth, baseMovedOutside = false) {
283
285
  if (depth > 4)
284
286
  return false;
285
287
  const lexed = lexShellText(command, { mode: "lenient" });
@@ -305,6 +307,8 @@ function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, dep
305
307
  if (programPositionRunsShell(pos, SHELL_REENTRY_PROGRAM_RE))
306
308
  return false;
307
309
  const decline = shellScanDeclineOf(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED);
310
+ if (decline !== undefined)
311
+ scan.declined = true;
308
312
  if (decline !== undefined && declinesAreEvidence.has(decline))
309
313
  return false;
310
314
  declined ??= decline;
@@ -322,7 +326,7 @@ function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, dep
322
326
  (walked.undecidedPaths?.length ?? 0) === 0 && (walked.unresolvedOperands?.length ?? 0) === 0 && (walked.recursiveReadPaths?.length ?? 0) === 0;
323
327
  if (fullyGreen)
324
328
  continue;
325
- if (!refusedCommandExempt(body, allow, boundary, declinesAreEvidence, depth + 1, baseMoved))
329
+ if (!refusedCommandExempt(body, allow, boundary, declinesAreEvidence, scan, depth + 1, baseMoved))
326
330
  return false;
327
331
  }
328
332
  const stdinFileJudgedInside = (word) => {
@@ -68,16 +68,22 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
68
68
  * The READ-BOUNDARY seat is what the default/off doctrine and a bypass mode mount ALONE: there "not mandated"
69
69
  * is VOUCHED, so a decline must count as evidence or a read nobody judged runs silently (`<out-of-root> |
70
70
  * xargs cat` at zero asks — a closed P0). It keeps both. The CLASSIFY seat composes the same boundary with the
71
- * SHAPE classifier, which refuses all these shapes anyway: the only question left is whether a stored answer
72
- * may retire the ask. It keeps NEITHER — empty by the argument, not by omission.
73
- * THE COST, stated rather than mitigated: once that ask is retired, an out-of-root or deny-listed read HIDDEN
74
- * inside such a command is not asked again. A command's words are not the argv its programs receive (a stream
75
- * is re-split by its consumer, an unread option may move the working directory first, stdin text is a command
76
- * line nobody parsed), so a scan of them cannot be the guarantee it was claimed to be — three independently
77
- * exploitable shapes measured that. "A read the boundary objects to is never retired by a stored rule" does
78
- * NOT hold on the classify seat for this class, and holds unchanged elsewhere; below it is the execution
79
- * environment's sandbox. One word-level check survives on both seats and is not part of the trade: a token the
80
- * scan DID read naming a path outside the roots still gates the call ({@link classifyOutOfRootReadGate}).
71
+ * SHAPE classifier, which refuses every one of these shapes: what survives is a CARD, and the only question
72
+ * this table answers is who may retire it. It keeps NEITHER — an ANSWERER may (a person, the auto-mode
73
+ * classifier, a person's stored allow rule) — empty by the argument, not by omission.
74
+ *
75
+ * WHAT AN EMPTY TABLE DOES NOT BUY, corrected where it was once claimed: "the shape classifier refuses these
76
+ * shapes anyway, so the only question left is the stored answer" was true of the PROBE and false of the OUTCOME
77
+ * — the allow layer's other members are not that classifier, and both cleared exactly these cards at ZERO asks. An
78
+ * empty table says only "no MANDATE HERE"; that the boundary DECLINED is a separate fact, carried on every ask this
79
+ * seat raises ({@link BoundaryAnswer}'s weaker rung), standing every ANSWERLESS arm down while every answerer's lane stays open.
80
+ * THE COST, stated rather than mitigated: once that ask is retired BY AN ANSWERER, an out-of-root or deny-listed
81
+ * read HIDDEN inside such a command is not asked again. A command's words are not the argv its programs receive (a
82
+ * stream is re-split by its consumer, an unread option may move the working directory first, stdin text is a
83
+ * command line nobody parsed), so a scan of them cannot be the guarantee it was claimed to be. "A read the boundary
84
+ * objects to is never retired by a stored rule" does NOT hold on the classify seat for this class, and holds
85
+ * unchanged elsewhere. One word-level check survives on both seats and is not part of the trade: a token the scan
86
+ * DID read naming a path outside the roots still gates the call ({@link classifyOutOfRootReadGate}).
81
87
  */
82
88
  export declare const BOUNDARY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
83
89
  /** The classify doctrine's table — see above; empty by the argument. */