@sema-agent/core 7.53.0 → 7.54.1

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 (249) hide show
  1. package/CHANGELOG.md +291 -1
  2. package/LICENSE +3 -1
  3. package/dist/agents/peer-session-drain.d.ts +0 -1
  4. package/dist/agents/peer-session-drain.js +0 -2
  5. package/dist/agents/send-message-tool.js +3 -4
  6. package/dist/agents/subagent.d.ts +3 -5
  7. package/dist/agents/subagent.js +18 -11
  8. package/dist/agents/tool-filter.d.ts +7 -4
  9. package/dist/agents/tool-filter.js +7 -3
  10. package/dist/core/a2a.js +4 -2
  11. package/dist/core/allow-entry.d.ts +1 -0
  12. package/dist/core/allow-entry.js +1 -0
  13. package/dist/core/approval-object.d.ts +87 -0
  14. package/dist/core/approval-object.js +85 -0
  15. package/dist/core/arg-summary.d.ts +3 -2
  16. package/dist/core/arg-summary.js +17 -11
  17. package/dist/core/ask-origin.d.ts +43 -27
  18. package/dist/core/ask-origin.js +20 -11
  19. package/dist/core/ask-question.d.ts +0 -1
  20. package/dist/core/ask-question.js +4 -67
  21. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  22. package/dist/core/auto-compaction.d.ts +49 -50
  23. package/dist/core/auto-compaction.js +41 -51
  24. package/dist/core/auto-mode-prompt.d.ts +4 -2
  25. package/dist/core/auto-mode-prompt.js +4 -3
  26. package/dist/core/canonical-json.d.ts +69 -0
  27. package/dist/core/canonical-json.js +130 -8
  28. package/dist/core/checkpoint-requirements.d.ts +13 -7
  29. package/dist/core/checkpoint-requirements.js +4 -1
  30. package/dist/core/checkpoint-store.d.ts +33 -18
  31. package/dist/core/checkpoint-store.js +5 -4
  32. package/dist/core/child-clamp-seats.d.ts +45 -6
  33. package/dist/core/child-clamp-seats.js +56 -4
  34. package/dist/core/consolidate-scope.d.ts +1 -1
  35. package/dist/core/cross-boundary-taint.d.ts +213 -0
  36. package/dist/core/cross-boundary-taint.js +196 -0
  37. package/dist/core/decision-receipt.d.ts +44 -1
  38. package/dist/core/decision-receipt.js +16 -0
  39. package/dist/core/effective-path-target.d.ts +3 -0
  40. package/dist/core/effective-path-target.js +4 -0
  41. package/dist/core/engine-notice.d.ts +34 -2
  42. package/dist/core/engine-notice.js +17 -1
  43. package/dist/core/engine-storage-backend.d.ts +73 -0
  44. package/dist/core/engine-storage-backend.js +1 -0
  45. package/dist/core/file-history-store.d.ts +6 -2
  46. package/dist/core/file-history-store.js +1 -0
  47. package/dist/core/gate-fold.js +10 -2
  48. package/dist/core/gate-lanes.js +30 -21
  49. package/dist/core/gate-outcome.d.ts +7 -3
  50. package/dist/core/governance-codes.d.ts +7 -2
  51. package/dist/core/governance-codes.js +6 -0
  52. package/dist/core/hooks.d.ts +14 -14
  53. package/dist/core/hooks.js +1 -2
  54. package/dist/core/locked-config.d.ts +28 -1
  55. package/dist/core/locked-config.js +30 -3
  56. package/dist/core/lsp.d.ts +1 -1
  57. package/dist/core/mcp-server-spec.d.ts +66 -9
  58. package/dist/core/mcp-server-spec.js +52 -19
  59. package/dist/core/mcp.js +5 -4
  60. package/dist/core/memory-engine/engine.js +2 -1
  61. package/dist/core/memory-engine/export-bundle.d.ts +1 -1
  62. package/dist/core/memory-engine/export-bundle.js +1 -1
  63. package/dist/core/memory-engine/file-backend.d.ts +2 -6
  64. package/dist/core/memory-engine/file-backend.js +1 -12
  65. package/dist/core/memory.d.ts +3 -3
  66. package/dist/core/mode-policy.d.ts +13 -8
  67. package/dist/core/mode-policy.js +3 -2
  68. package/dist/core/mods/kernel/receive.d.ts +2 -1
  69. package/dist/core/mods/kernel/receive.js +7 -2
  70. package/dist/core/parent-spec-seat.d.ts +10 -13
  71. package/dist/core/parent-spec-seat.js +14 -6
  72. package/dist/core/pause-registry.d.ts +35 -1
  73. package/dist/core/pause-registry.js +10 -0
  74. package/dist/core/permission-rule-consent.d.ts +18 -5
  75. package/dist/core/permission-rule-consent.js +13 -8
  76. package/dist/core/permission-rule-model.d.ts +29 -6
  77. package/dist/core/permission-rule-model.js +6 -1
  78. package/dist/core/permission-rule-org.d.ts +37 -0
  79. package/dist/core/permission-rule-org.js +34 -42
  80. package/dist/core/permission-rule-session.js +2 -2
  81. package/dist/core/permission-rule-store.d.ts +8 -0
  82. package/dist/core/permission-rule-store.js +1 -0
  83. package/dist/core/permission-rule-sync.js +3 -2
  84. package/dist/core/read-only-shell.d.ts +1 -1
  85. package/dist/core/read-only-shell.js +30 -9
  86. package/dist/core/remote-env.d.ts +41 -1
  87. package/dist/core/remote-env.js +18 -0
  88. package/dist/core/resume-startup-cause.d.ts +18 -0
  89. package/dist/core/resume-startup-cause.js +12 -0
  90. package/dist/core/runner/approval-feedback.d.ts +46 -0
  91. package/dist/core/runner/approval-feedback.js +41 -0
  92. package/dist/core/runner/compaction-call-options.d.ts +3 -3
  93. package/dist/core/runner/compaction-call-options.js +2 -2
  94. package/dist/core/runner/contracts.d.ts +33 -41
  95. package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
  96. package/dist/core/runner/cross-boundary-lane.js +190 -0
  97. package/dist/core/runner/decide-continuation.d.ts +4 -4
  98. package/dist/core/runner/decide-continuation.js +3 -1
  99. package/dist/core/runner/gate-exit.d.ts +32 -12
  100. package/dist/core/runner/gate-exit.js +75 -33
  101. package/dist/core/runner/inherited-ask-grants.js +2 -2
  102. package/dist/core/runner/person-path-rows.d.ts +2 -2
  103. package/dist/core/runner/prepare-acquire-reconcile.d.ts +7 -9
  104. package/dist/core/runner/prepare-acquire-reconcile.js +8 -6
  105. package/dist/core/runner/prepare-ask-lane.js +3 -3
  106. package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
  107. package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
  108. package/dist/core/runner/prepare-config-doors.d.ts +12 -15
  109. package/dist/core/runner/prepare-config-doors.js +8 -3
  110. package/dist/core/runner/prepare-context-lane.js +6 -3
  111. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  112. package/dist/core/runner/prepare-delegation-surface.js +2 -3
  113. package/dist/core/runner/prepare-execution-env.d.ts +1 -2
  114. package/dist/core/runner/prepare-execution-env.js +2 -1
  115. package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
  116. package/dist/core/runner/prepare-gate-stations.js +4 -3
  117. package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
  118. package/dist/core/runner/prepare-hands-readface.js +1 -1
  119. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  120. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  121. package/dist/core/runner/prepare-park-ask.d.ts +1 -1
  122. package/dist/core/runner/prepare-park-ask.js +9 -3
  123. package/dist/core/runner/prepare-policy-chain.js +2 -7
  124. package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
  125. package/dist/core/runner/prepare-safety-scan.js +34 -27
  126. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
  127. package/dist/core/runner/prepare-suspend-saga.js +6 -2
  128. package/dist/core/runner/prepare-task.js +13 -10
  129. package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
  130. package/dist/core/runner/prepare-turn-wiring.js +14 -49
  131. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  132. package/dist/core/runner/prepare-wiring-manifest.js +2 -2
  133. package/dist/core/runner/prepare-workspace-restore.js +2 -2
  134. package/dist/core/runner/resume-admission.js +9 -9
  135. package/dist/core/runner/resume-apply.js +4 -4
  136. package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
  137. package/dist/core/runner/resume-policy-outcome.js +18 -15
  138. package/dist/core/runner/resume-preflight.js +2 -2
  139. package/dist/core/runner/resume-review-outcome.js +3 -3
  140. package/dist/core/runner/run-leg.js +57 -33
  141. package/dist/core/runner/run-settle-and-teardown.js +2 -0
  142. package/dist/core/runner/run-terminal-adoption.js +15 -4
  143. package/dist/core/runner/session-file-state-replay.d.ts +5 -2
  144. package/dist/core/runner/session-file-state-replay.js +3 -2
  145. package/dist/core/runner/stream-steer-verb.js +2 -2
  146. package/dist/core/runner/tool-face-overlay.d.ts +24 -6
  147. package/dist/core/runner/tool-face-overlay.js +24 -15
  148. package/dist/core/runner-deps.d.ts +9 -9
  149. package/dist/core/session-store.d.ts +35 -16
  150. package/dist/core/session-store.js +20 -12
  151. package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
  152. package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
  153. package/dist/core/shell-wrapper-table.d.ts +5 -3
  154. package/dist/core/shell-wrapper-table.js +1 -1
  155. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  156. package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
  157. package/dist/core/store-contracts/session-store-contract.js +683 -0
  158. package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
  159. package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
  160. package/dist/core/task-event.d.ts +6 -4
  161. package/dist/core/task-registry-agent.js +1 -0
  162. package/dist/core/task-registry-shared.d.ts +5 -0
  163. package/dist/core/task-registry.d.ts +7 -0
  164. package/dist/core/task-registry.js +17 -0
  165. package/dist/core/task-result.d.ts +1 -1
  166. package/dist/core/task-spec.d.ts +6 -6
  167. package/dist/core/tighten-task-spec.d.ts +6 -4
  168. package/dist/core/tighten-task-spec.js +2 -2
  169. package/dist/core/tool-catalog-entries.d.ts +1 -1
  170. package/dist/core/tool-catalog-entries.js +52 -52
  171. package/dist/core/tool-face-reader.d.ts +49 -0
  172. package/dist/core/tool-face-reader.js +94 -0
  173. package/dist/core/tool-face.d.ts +17 -8
  174. package/dist/core/tool-face.js +1 -0
  175. package/dist/core/tool-policy.d.ts +64 -23
  176. package/dist/core/tool-policy.js +62 -56
  177. package/dist/core/tool-roster.d.ts +46 -28
  178. package/dist/core/tool-roster.js +91 -14
  179. package/dist/core/tool-spec-members.js +2 -2
  180. package/dist/core/tool-spec.d.ts +20 -4
  181. package/dist/core/tools.js +2 -0
  182. package/dist/core/trace.d.ts +24 -2
  183. package/dist/core/types.d.ts +2 -2
  184. package/dist/core/types.js +1 -1
  185. package/dist/core/untrusted-text.d.ts +7 -0
  186. package/dist/core/untrusted-text.js +3 -8
  187. package/dist/core/user-attachment.d.ts +43 -0
  188. package/dist/core/user-attachment.js +92 -15
  189. package/dist/engine/compaction/compaction.d.ts +2 -2
  190. package/dist/engine/compaction/utils.d.ts +2 -2
  191. package/dist/engine/harness/types.d.ts +7 -4
  192. package/dist/engine/llm/types.d.ts +7 -2
  193. package/dist/engine/llm/types.js +2 -1
  194. package/dist/engine/session/parent-chain-guard.d.ts +10 -0
  195. package/dist/engine/session/parent-chain-guard.js +10 -0
  196. package/dist/engine/session/repo-utils.d.ts +5 -0
  197. package/dist/engine/session/repo-utils.js +9 -0
  198. package/dist/engine/session/storage-base.js +5 -2
  199. package/dist/index.d.ts +23 -9
  200. package/dist/index.js +19 -8
  201. package/dist/internal/harness.d.ts +1 -0
  202. package/dist/internal/harness.js +1 -0
  203. package/dist/internal/llm.d.ts +1 -1
  204. package/dist/internal/llm.js +1 -1
  205. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  206. package/dist/orchestration/run-workflow-tool.js +1 -0
  207. package/dist/orchestration/workflow-governance.d.ts +1 -1
  208. package/dist/orchestration/workflow.d.ts +3 -0
  209. package/dist/orchestration/workflow.js +2 -1
  210. package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
  211. package/dist/prompt-assembly/catalog/schema.js +3 -0
  212. package/dist/prompt-assembly/overlay.js +2 -2
  213. package/dist/prompt-assembly/select.js +2 -2
  214. package/dist/prompts/default.d.ts +1 -1
  215. package/dist/shell/lex.d.ts +87 -8
  216. package/dist/shell/lex.js +283 -49
  217. package/dist/stores/file/index.d.ts +8 -6
  218. package/dist/tools/artifact/artifact-tool.js +10 -0
  219. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  220. package/dist/tools/fs/bash-program-position.js +3 -10
  221. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  222. package/dist/tools/fs/fs-bash.d.ts +3 -3
  223. package/dist/tools/fs/fs-bash.js +72 -101
  224. package/dist/tools/fs/fs-read.d.ts +12 -0
  225. package/dist/tools/fs/fs-read.js +10 -9
  226. package/dist/tools/fs/fs-shared.d.ts +27 -31
  227. package/dist/tools/fs/fs-shared.js +12 -15
  228. package/dist/tools/fs/fs-write.js +1 -1
  229. package/dist/tools/fs/index.js +2 -2
  230. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  231. package/dist/tools/fs/read-deny-home.js +5 -4
  232. package/dist/tools/fs/read-deny.d.ts +16 -3
  233. package/dist/tools/fs/read-deny.js +12 -9
  234. package/dist/tools/fs/safety.d.ts +7 -6
  235. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  236. package/dist/tools/fs/shell-read-bindings.js +15 -16
  237. package/dist/tools/fs/shell-read-device.js +1 -1
  238. package/dist/tools/fs/shell-read-plan.js +36 -47
  239. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  240. package/dist/tools/fs/shell-read-word.js +2 -20
  241. package/dist/tools/fs/shell-reentry-text.js +1 -1
  242. package/dist/tools/web.d.ts +2 -2
  243. package/package.json +4 -1
  244. package/test/export-surface.snapshot.json +5654 -5520
  245. package/wire-changes/core@7.53.0.json +30 -8
  246. package/wire-changes/core@7.54.0.json +2870 -0
  247. package/wire-changes/core@7.54.1.json +37 -0
  248. package/dist/core/shell-scan.d.ts +0 -60
  249. package/dist/core/shell-scan.js +0 -183
@@ -14,21 +14,9 @@ import { SHELL_CONTROL_PREFIXES, type LexedWord } from "../../shell/lex.js";
14
14
  export declare const NOT_AUTO_ALLOWED = "\u2014 not auto-allowed";
15
15
  /** argv[0] as the program bash will resolve it: `/usr/bin/cat` and `cat` are one name. */
16
16
  export declare function programBasename(word: string): string;
17
- /**
18
- * The `NAME=` / `NAME+=` prefix as WRITTEN in a whitespace-split token. This is NOT the position reader's grammar
19
- * — the reader reads the lexer's {@link LexedWord.assignment} fact — but the one reader that still walks the
20
- * command TEXT rather than its words (the activity summary) reads it here. It retires with it.
21
- */
22
- export declare const ASSIGNMENT_PREFIX: RegExp;
23
- /** The prefix a whitespace-split token BEGINS with, or `undefined` — the text readers' view of
24
- * {@link ASSIGNMENT_PREFIX}; `name` is the variable before any subscript, `settlesWholeName` false for an
25
- * append and for a subscript. */
26
- export declare function writtenAssignmentPrefix(token: string): {
27
- readonly prefix: string;
28
- readonly name: string;
29
- readonly appends: boolean;
30
- readonly settlesWholeName: boolean;
31
- } | undefined;
17
+ /** A launcher's own `NAME=value` operand (`env`, `sudo`): any `=`-bearing non-option word, read DEQUOTED — the launcher's
18
+ * grammar, not the shell's (`env 'F[1=2' cmd` sets `F[1`). The one spelling of it. */
19
+ export declare function isLauncherAssignmentOperand(folded: string): boolean;
32
20
  /** The names a segment's COMMAND ENVIRONMENT sets for the program it starts: the assignment words in front of the
33
21
  * program (the lexer's {@link LexedWord.assignment} fact, a launcher's `NAME=value` operand included) — or `all` where
34
22
  * a launcher in front of it may set, clear or replace the environment (`env -u` / `-i`, `sudo`, `su`, …). */
@@ -5,15 +5,8 @@ export const NOT_AUTO_ALLOWED = "— not auto-allowed";
5
5
  export function programBasename(word) {
6
6
  return word.includes("/") ? (word.split("/").pop() ?? word) : word;
7
7
  }
8
- export const ASSIGNMENT_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]\s]*\])?\+?=/;
9
- export function writtenAssignmentPrefix(token) {
10
- const prefix = ASSIGNMENT_PREFIX.exec(token)?.[0];
11
- if (prefix === undefined)
12
- return undefined;
13
- const appends = prefix.endsWith("+=");
14
- const written = prefix.slice(0, prefix.length - (appends ? 2 : 1));
15
- const bracket = written.indexOf("[");
16
- return { prefix, name: bracket < 0 ? written : written.slice(0, bracket), appends, settlesWholeName: !appends && bracket < 0 };
8
+ export function isLauncherAssignmentOperand(folded) {
9
+ return !folded.startsWith("-") && folded.includes("=");
17
10
  }
18
11
  const ENVIRONMENT_KEEPING_LAUNCHERS = new Set(["nice", "nohup", "time", "timeout", "stdbuf", "ionice", "chrt", "taskset", "command", "builtin", "caffeinate"]);
19
12
  export function commandEnvironmentNames(argv) {
@@ -123,7 +116,7 @@ export function readProgramPosition(words) {
123
116
  owedOperands -= 1;
124
117
  continue;
125
118
  }
126
- if (lastRow?.assignmentOperands === true && !t.startsWith("-") && t.includes("="))
119
+ if (lastRow?.assignmentOperands === true && isLauncherAssignmentOperand(t))
127
120
  continue;
128
121
  if (lastRow?.shellCommand !== undefined && !t.startsWith("-") && selectsShell(launchers[launchers.length - 1], j))
129
122
  return chain(undefined, shellRefusal(launchers[launchers.length - 1].index, j));
@@ -41,6 +41,8 @@ function expandsBeyondText(w) {
41
41
  const k = p.expansion?.kind;
42
42
  if (k === "parameter" || k === "command" || k === "process" || k === "arithmetic")
43
43
  return true;
44
+ if (p.expansion?.inner !== undefined)
45
+ return true;
44
46
  if (k === "tilde" && p.expansion?.body !== "~")
45
47
  return true;
46
48
  if (p.literal !== undefined && (p.quotedBy === undefined || p.quotedBy === '"') && /[$`]/.test(p.literal))
@@ -36,9 +36,9 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
36
36
  */
37
37
  export { FULL_SHELL_CONTRACT_ID } from "../../core/tool-catalog-entries.js";
38
38
  /** The CC-verbatim exit-1 interpretation for `command`, or undefined when exit 1 means a real error.
39
- * Conservative parse: last `;`/`&&`/`||`/newline statement → last `|` pipeline segment → leading
40
- * command name (env-assignments skipped, path prefix stripped); `git grep`/`git diff` special-cased
41
- * (CC `cLp`). Exported for its unit tests. */
39
+ * Read off the lexer's segments: the last one (the last `;`/`&&`/`||`/newline statement's last `|` piece) → its
40
+ * written command word past the leading assignments bash strips, path prefix stripped;
41
+ * `git grep`/`git diff` special-cased (CC `cLp`). Exported for its unit tests. */
42
42
  export declare function bashExitOneInterpretation(command: string): string | undefined;
43
43
  export declare function canAutoBackground(command: string, depth?: number): boolean;
44
44
  /** One observation of a running command, handed to {@link runShell}'s caller once per
@@ -18,6 +18,8 @@ import { backgroundLifetimeNote } from "./fs-bash-background-text.js";
18
18
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, } from "./fs-shared.js";
19
19
  import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed } from "./bash-readonly-classifier.js";
20
20
  import { toolFace } from "../../core/tool-catalog-entries.js";
21
+ import { argvOf, compoundValueHidesRun, expansionHidesRun, expansionsInHeredocBody, lexShell } from "../../shell/lex.js";
22
+ import { arithmeticAssignedNames } from "./shell-read-bindings.js";
21
23
  import { thrownText } from "../../core/thrown-value.js";
22
24
  function shellCommandOf(args) {
23
25
  const command = args?.command;
@@ -55,19 +57,15 @@ const EXIT1_INTERPRETATION = {
55
57
  "[": "Condition is false",
56
58
  };
57
59
  export function bashExitOneInterpretation(command) {
58
- const stmts = command.split(/\n|;|&&|\|\|/);
59
- let lastStmt = "";
60
- for (let i = stmts.length - 1; i >= 0; i--) {
61
- if (stmts[i].trim().length > 0) {
62
- lastStmt = stmts[i];
63
- break;
64
- }
65
- }
66
- const lastSeg = lastStmt.split("|").at(-1)?.trim() ?? "";
67
- const tokens = lastSeg.split(/\s+/).filter(Boolean);
60
+ const lexed = lexShell(command);
61
+ if ("parseFailure" in lexed)
62
+ return undefined;
63
+ const last = [...lexed.segments].reverse().find((s) => s.tokens.length > 0);
64
+ const argv = last === undefined ? [] : argvOf(last);
68
65
  let i = 0;
69
- while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i]))
66
+ while (i < argv.length && argv[i].assignment?.bare === true && argv[i].atCommandPosition)
70
67
  i++;
68
+ const tokens = argv.map((w) => w.folded);
71
69
  const name = tokens[i]?.split("/").pop() ?? "";
72
70
  if (name === "git") {
73
71
  for (let j = i + 1; j < tokens.length; j++) {
@@ -103,31 +101,41 @@ function dataUriImageFromStdout(stdout) {
103
101
  return undefined;
104
102
  }
105
103
  const AUTO_BACKGROUND_BLOCKED_FIRST_WORDS = new Set(["sleep"]);
106
- function stripAutoBackgroundWrapperPrefixes(segment) {
107
- let s = segment.trim();
108
- const wrappers = [
109
- /^timeout\s+(?:-[A-Za-z]\S*\s+)*\d+(?:\.\d+)?[smhd]?\s+/,
110
- /^time\s+/,
111
- /^nice(?:\s+-n?\s*-?\d+)?\s+/,
112
- /^nohup\s+/,
113
- /^command\s+/,
114
- /^builtin\s+/,
115
- /^stdbuf(?:\s+-[ioe]\S+)+\s+/,
116
- /^noglob\s+/,
117
- /^[A-Za-z_][A-Za-z0-9_]*=\S+\s+/,
118
- ];
119
- let changed = true;
120
- while (changed) {
121
- changed = false;
122
- for (const re of wrappers) {
123
- const stripped = s.replace(re, "");
124
- if (stripped !== s) {
125
- s = stripped.trim();
126
- changed = true;
127
- }
104
+ function pastAutoBackgroundWrapperPrefixes(words) {
105
+ const folded = words.map((w) => w.folded);
106
+ let i = 0;
107
+ for (;;) {
108
+ const w = folded[i];
109
+ let next = i;
110
+ if (words[i]?.assignment?.bare === true && words[i].atCommandPosition && words[i].assignment.value.raw !== "")
111
+ next = i + 1;
112
+ else if (w === "time" || w === "nohup" || w === "command" || w === "builtin" || w === "noglob")
113
+ next = i + 1;
114
+ else if (w === "timeout") {
115
+ let j = i + 1;
116
+ while (/^-[A-Za-z]/.test(folded[j] ?? ""))
117
+ j++;
118
+ if (/^\d+(?:\.\d+)?[smhd]?$/.test(folded[j] ?? ""))
119
+ next = j + 1;
120
+ }
121
+ else if (w === "nice") {
122
+ next = i + 1;
123
+ if (folded[next] === "-n" && /^-?\d+$/.test(folded[next + 1] ?? ""))
124
+ next += 2;
125
+ else if (/^-n?-?\d+$/.test(folded[next] ?? ""))
126
+ next += 1;
127
+ }
128
+ else if (w === "stdbuf") {
129
+ let j = i + 1;
130
+ while (/^-[ioe]./.test(folded[j] ?? ""))
131
+ j++;
132
+ if (j > i + 1)
133
+ next = j;
128
134
  }
135
+ if (next === i || next >= folded.length)
136
+ return words.slice(i);
137
+ i = next;
129
138
  }
130
- return s;
131
139
  }
132
140
  function commandBasename(token) {
133
141
  const idx = token.lastIndexOf("/");
@@ -141,84 +149,47 @@ function commandStringPayload(tokens) {
141
149
  if (!COMMAND_STRING_SHELLS.has(word))
142
150
  return undefined;
143
151
  const flagIdx = tokens.findIndex((t, i) => i > 0 && /^-[A-Za-z]*c[A-Za-z]*$/.test(t));
144
- return flagIdx === -1 ? undefined : tokens.slice(flagIdx + 1).join(" ");
145
- }
146
- function unquotePayload(payload) {
147
- const s = payload.trim();
148
- const ansiC = /^\$'([\s\S]*)'$/.exec(s);
149
- if (ansiC)
150
- return (ansiC[1] ?? "").replace(/\\n/g, "\n").replace(/\\t/g, "\t");
151
- const quoted = /^(['"])([\s\S]*)\1$/.exec(s);
152
- return quoted ? (quoted[2] ?? "") : s;
152
+ return flagIdx === -1 ? undefined : tokens[flagIdx + 1];
153
153
  }
154
154
  const AUTO_BACKGROUND_MAX_WRAPPER_DEPTH = 3;
155
- function splitTopLevelStatements(command) {
156
- const out = [];
157
- let cur = "";
158
- let quote;
159
- for (let i = 0; i < command.length; i++) {
160
- const c = command[i];
161
- if (quote !== undefined) {
162
- cur += c;
163
- if (c === quote)
164
- quote = undefined;
165
- continue;
166
- }
167
- if (c === '"' || c === "'") {
168
- quote = c;
169
- cur += c;
170
- continue;
171
- }
172
- if (c === ";" || c === "\n" || c === "|" || (c === "&" && command[i + 1] === "&")) {
173
- if ((c === "&" || c === "|") && command[i + 1] === c)
174
- i++;
175
- out.push(cur);
176
- cur = "";
177
- continue;
178
- }
179
- if (c === "\r" && command[i + 1] === "\n") {
180
- out.push(cur);
181
- cur = "";
182
- i++;
183
- continue;
184
- }
185
- cur += c;
186
- }
187
- out.push(cur);
188
- return out.map((s) => s.trim()).filter((s) => s.length > 0);
155
+ function arithmeticInFront(text, depth) {
156
+ return arithmeticAssignedNames(text).length > 0 || (expansionsInHeredocBody(text)?.some((x) => x.unresolved !== undefined || x.expansion.kind === "command" || (x.expansion.kind === "arithmetic" ? depth >= AUTO_BACKGROUND_MAX_WRAPPER_DEPTH || arithmeticInFront(x.expansion.body ?? "", depth + 1) : expansionHidesRun(x.expansion))) ?? true);
189
157
  }
190
158
  export function canAutoBackground(command, depth = 0) {
191
- if (depth < AUTO_BACKGROUND_MAX_WRAPPER_DEPTH) {
192
- for (const stmt of splitTopLevelStatements(command)) {
193
- const tokens = stripAutoBackgroundWrapperPrefixes(stmt).split(/\s+/);
159
+ const lexed = lexShell(command);
160
+ if ("parseFailure" in lexed)
161
+ return false;
162
+ const segments = lexed.segments;
163
+ const hidden = (e) => e !== undefined && e.kind !== "command" && e.kind !== "process" && e.kind !== "arithmetic" && expansionHidesRun(e);
164
+ const words = segments.flatMap((s) => s.tokens.flatMap((t) => (t.kind === "word" ? [t] : t.kind === "redirect" && t.target !== undefined ? [t.target] : [])));
165
+ if (words.some((w) => w.unresolved === "parameter_text" || compoundValueHidesRun(w) || w.expansions.some((p) => hidden(p.expansion))))
166
+ return false;
167
+ const heredocs = segments.flatMap((s) => s.tokens.flatMap((t) => (t.kind === "heredoc" && !t.quoted ? [expansionsInHeredocBody(t.body)] : [])));
168
+ if (heredocs.some((b) => b === undefined || b.some((x) => x.unresolved !== undefined || hidden(x.expansion))))
169
+ return false;
170
+ const arithmetic = [...words.flatMap((w) => w.expansions), ...heredocs.flatMap((b) => b ?? [])].flatMap((p) => (p.expansion?.kind === "arithmetic" ? [p.expansion.body ?? ""] : []));
171
+ const bodies = [...segments.flatMap((s) => s.substitutions), ...heredocs.flatMap((b) => (b ?? []).flatMap((x) => (x.expansion.kind === "command" ? [x.expansion.body ?? ""] : [])))].filter(((left) => (b) => { const k = left.indexOf(b); if (k < 0)
172
+ return true; left.splice(k, 1); return false; })([...arithmetic]));
173
+ if ((bodies.length > 0 && depth >= AUTO_BACKGROUND_MAX_WRAPPER_DEPTH) || bodies.some((b) => !canAutoBackground(b, depth + 1)) || arithmetic.some((b) => arithmeticInFront(b, depth + 1)))
174
+ return false;
175
+ const runs = segments.map((s) => pastAutoBackgroundWrapperPrefixes(argvOf(s)).map((w) => w.folded)).filter((t) => t.length > 0);
176
+ if (runs.length === 0)
177
+ return true;
178
+ for (const tokens of runs) {
179
+ const word = commandBasename(tokens[0]);
180
+ if (word === "git")
181
+ return false;
182
+ if (depth < AUTO_BACKGROUND_MAX_WRAPPER_DEPTH) {
194
183
  for (let i = 0; i < tokens.length; i++) {
195
- const stmtPayload = commandStringPayload(tokens.slice(i));
196
- if (stmtPayload !== undefined && stmtPayload.trim() !== "" && !canAutoBackground(unquotePayload(stmtPayload), depth + 1))
184
+ const payload = commandStringPayload(tokens.slice(i));
185
+ if (payload !== undefined && payload.trim() !== "" && !canAutoBackground(payload, depth + 1))
197
186
  return false;
198
187
  }
199
188
  }
200
- }
201
- const segments = command
202
- .split(/&&|\|\||;|\||\r?\n/)
203
- .map((s) => s.trim())
204
- .filter((s) => s.length > 0);
205
- if (segments.length === 0)
206
- return true;
207
- for (const seg of segments) {
208
- const tokens = stripAutoBackgroundWrapperPrefixes(seg).split(/\s+/);
209
- const word = commandBasename(tokens[0] ?? "");
210
- if (word === "git")
211
- return false;
212
- const payload = depth < AUTO_BACKGROUND_MAX_WRAPPER_DEPTH ? commandStringPayload(tokens) : undefined;
213
- if (payload !== undefined && payload.trim() !== "" && !canAutoBackground(unquotePayload(payload), depth + 1))
214
- return false;
215
189
  if (word === "xargs" && tokens.slice(1).some((t) => commandBasename(t) === "git"))
216
190
  return false;
217
191
  }
218
- const firstWord = commandBasename(stripAutoBackgroundWrapperPrefixes(segments[0]).split(/\s+/)[0] ?? "");
219
- if (AUTO_BACKGROUND_BLOCKED_FIRST_WORDS.has(firstWord))
220
- return false;
221
- return true;
192
+ return !AUTO_BACKGROUND_BLOCKED_FIRST_WORDS.has(commandBasename(runs[0][0]));
222
193
  }
223
194
  function msTimeoutToRequestedSec(timeoutMs) {
224
195
  if (timeoutMs === undefined || !Number.isFinite(timeoutMs))
@@ -155,6 +155,18 @@ export interface ReadForModelOptions {
155
155
  key: string;
156
156
  object: BoundFileObject;
157
157
  };
158
+ /** The read state the two dedup answers (unchanged since the last read, already in context) are judged against —
159
+ * absent ⇒ the reader's own live state. A carrier that delivers the read into a context other than the live one
160
+ * passes the state that context will hold (a compaction's restore: what the landed summary leaves); such a read is
161
+ * not held back by the disclosure throttle either (the disclosure the window remembers left with that context). */
162
+ dedupAgainst?: ReadonlyMap<string, ReadEntry>;
163
+ /** A carrier that presents the read as file data (an attachment frame) takes a PDF as a reference — its size and page
164
+ * count, no pipeline run — since the Read projection interleaves engine text with the file's. A bound read always does. */
165
+ pdfAsReference?: boolean;
166
+ /** The canonical key the carrier's judgment of this path landed on, for a carrier that judged it without holding the
167
+ * object (a bound read carries it as `bound.key`): a path whose final resolution is elsewhere is refused, the refusal
168
+ * marked `details.type: "target_moved"` so a frame says it in the attachment's own words. */
169
+ judgedKey?: string;
158
170
  }
159
171
  export interface ModelFileReaderOptions {
160
172
  env: ExecutionEnv;
@@ -30,8 +30,8 @@ export function createModelFileReader(opts) {
30
30
  refusal: envelope,
31
31
  effects: NO_EFFECTS(),
32
32
  });
33
- const disclosureOf = (segments, outlet, key) => {
34
- const plan = planReminderDisclosure({ segments, mark: reminderMark, outlet, defuseExactMark: false, throttle: { key, windows } });
33
+ const disclosureOf = (segments, outlet, key, throttled = true) => {
34
+ const plan = planReminderDisclosure({ segments, mark: reminderMark, outlet, defuseExactMark: false, ...(throttled ? { throttle: { key, windows } } : {}) });
35
35
  const verdict = plan.verdict;
36
36
  return { ...(verdict.reminder !== undefined || verdict.envelopeTags !== undefined ? { note: { kind: "disclosure", verdict } } : {}), effects: plan.effects };
37
37
  };
@@ -57,8 +57,9 @@ export function createModelFileReader(opts) {
57
57
  const r = await resolve(path, ctx);
58
58
  if (!r.ok)
59
59
  return refuse(errorResult(violationText("Read", r.violation), violationDetails(r.violation)));
60
- if (opts?.bound !== undefined && r.key !== opts.bound.key) {
61
- return refuse(errorResult(`Error (Read): "${path.slice(0, 300)}" resolved to a different file while it was being checked, so it was not read.`));
60
+ const judgedKey = opts?.bound?.key ?? opts?.judgedKey;
61
+ if (judgedKey !== undefined && r.key !== judgedKey) {
62
+ return refuse(errorResult(`Error (Read): "${path.slice(0, 300)}" resolved to a different file while it was being checked, so it was not read.`, { type: "target_moved" }));
62
63
  }
63
64
  const bound = opts?.bound;
64
65
  const readBytes = (key, laneCap) => bound === undefined ? env.readBinaryFile(key, ctx.signal) : bound.object.readBytes(laneCap, ctx.signal);
@@ -114,7 +115,7 @@ export function createModelFileReader(opts) {
114
115
  };
115
116
  }
116
117
  const pdfOf = async (preRead) => {
117
- if (bound !== undefined) {
118
+ if (bound !== undefined || opts?.pdfAsReference === true) {
118
119
  if (!info.ok)
119
120
  return refuse(errorResult(`Error (Read): cannot size PDF "${path.slice(0, 300)}": ${info.error.message.slice(0, 200)}`), "pdf");
120
121
  let held = preRead;
@@ -232,7 +233,7 @@ export function createModelFileReader(opts) {
232
233
  ` cat "${path}" | jq '.cells | length' # Count total cells\n` +
233
234
  ` cat "${path}" | jq '.cells[] | select(.cell_type=="code") | .source' # All code sources`), "notebook");
234
235
  }
235
- const prevNb = state.get(r.key);
236
+ const prevNb = (opts?.dedupAgainst ?? state).get(r.key);
236
237
  if (prevNb?.seededFromContext && !prevNb.isPartialView && prevNb.hash === hash) {
237
238
  return unchanged({ kind: "already_in_context", key: r.key }, "notebook", "already-in-context", 1, total, total);
238
239
  }
@@ -242,7 +243,7 @@ export function createModelFileReader(opts) {
242
243
  const observation = { key: r.key, entry: { hash, totalLines: countLines(content), truncated: false, view: { start: 1, end: total } } };
243
244
  if (rendered.blocks.length === 0)
244
245
  notes.push({ kind: "notebook_no_cells" });
245
- const disclosure = disclosureOf(rendered.blocks.filter((b) => b.type === "text").map((b) => b.text), "notebook", r.key);
246
+ const disclosure = disclosureOf(rendered.blocks.filter((b) => b.type === "text").map((b) => b.text), "notebook", r.key, opts?.dedupAgainst === undefined);
246
247
  if (disclosure.note !== undefined)
247
248
  notes.push(disclosure.note);
248
249
  return {
@@ -292,7 +293,7 @@ export function createModelFileReader(opts) {
292
293
  }
293
294
  }
294
295
  const truncated = start > 1 || end < total || partial !== undefined;
295
- const prev = state.get(r.key);
296
+ const prev = (opts?.dedupAgainst ?? state).get(r.key);
296
297
  if (total > 0 && prev?.seededFromContext && !prev.isPartialView && start === 1 && effLimit === undefined && prev.hash === hash) {
297
298
  return unchanged({ kind: "already_in_context", key: r.key }, "text", "already-in-context", 1, total, total);
298
299
  }
@@ -309,7 +310,7 @@ export function createModelFileReader(opts) {
309
310
  notes.push(partial);
310
311
  else if (truncated)
311
312
  notes.push({ kind: "range", path, start, end, total });
312
- const disclosure = disclosureOf([body], "read", r.key);
313
+ const disclosure = disclosureOf([body], "read", r.key, opts?.dedupAgainst === undefined);
313
314
  if (disclosure.note !== undefined)
314
315
  notes.push(disclosure.note);
315
316
  return {
@@ -1,6 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import type { ExecutionEnv } from "../../internal/harness-types.js";
3
- import { type FsViolation, type ReadFileState } from "./safety.js";
3
+ import { type FsViolation, type ReadEntry, type ReadFileState } from "./safety.js";
4
4
  import { type DecodedTextFile } from "./encoding.js";
5
5
  import { type HomeViewsCarrier } from "./read-deny-home.js";
6
6
  import { type ImageDownsampler } from "../../core/mcp.js";
@@ -426,8 +426,10 @@ export declare function seedReadFileStateFromContext(state: ReadFileState, key:
426
426
  * the context, which a continuation (or a compaction inside it) cannot promise.
427
427
  * - no `seededFromContext`: that flag means the text lives in the system-prompt lane and survives
428
428
  * compaction. This text lives in the transcript and does not.
429
+ * A Read result's text carries that result's call id (`transmission`), as the live read's entry does, so a trim that
430
+ * drops the result retires the entry; a Write's post-condition rode no result body and carries none.
429
431
  */
430
- export declare function seedReadFileStateFromTranscript(state: ReadFileState, key: string, content: string, lastReadAt: number): void;
432
+ export declare function seedReadFileStateFromTranscript(state: ReadFileState, key: string, content: string, lastReadAt: number, transmission?: string): void;
431
433
  /**
432
434
  * Copy a SESSION-inherited read-file state into a new task's map, under the new task's
433
435
  * own containment judgment (CC parity: readFileState is session-scoped — minted once per REPL session,
@@ -459,30 +461,28 @@ export declare function inheritSessionReadFileState(target: ReadFileState, inher
459
461
  * Two deliberate divergences from CC's bare clear(), each from sema's own structure:
460
462
  * - SEEDED entries survive: their content lives in the system-prompt lane, which compaction never touches,
461
463
  * so their dedup remains CORRECT (CC re-seeds the same family right after its clear — equivalent end state).
462
- * - Files re-attached WHOLE into the summary re-register (content genuinely re-entered the context — same
463
- * principle as CC re-registering restored file attachments); a TRUNCATED attachment does not (a partial
464
- * view must not answer a whole-file read with a stub).
465
- * The read-before-edit gate tightens as a consequence, matching CC: after a compaction the model must
466
- * re-Read a summarized-away file before editing it, instead of blind-editing from a stale recollection.
467
- *
468
- * A THIRD category besides seeded/re-attached: a file whose most recent Read is still visible
469
- * verbatim in the compaction's KEPT tail is deliberately left OUT of
470
- * `attachedComplete` (re-attaching it would just duplicate content already in context), but its
471
- * existing read-state entry must survive unmodified for the SAME reason a seeded entry does: the
472
- * model's view of it did not change. Wiping it breaks two things for exactly these files — the
473
- * read-before-edit gate would reject an Edit the model could visibly justify from its own kept-tail
474
- * Read, and a next Read would miss the dedup stub and retransmit the whole file, recreating the exact
475
- * waste this dedup exists to remove. `preserveKeys` is the caller's kept-tail set
476
- * (see `maybeCompact`'s `candidateFiles` computation) — passed as keys already in `state`'s own
477
- * coordinate (canonicalized the same way the Read/Edit/Write tools populate it), so a plain lookup
478
- * suffices; no re-hash/re-register needed since the entry's content genuinely has not changed.
464
+ * - A file still visible in the compaction's KEPT tail (its read or its attachment frame is there) survives
465
+ * unmodified, for the same reason: the model's view of it did not change. Wiping it would make the
466
+ * read-before-edit gate reject an Edit the model can visibly justify, and the next Read retransmit the file.
467
+ * `preserveKeys` is that set, in `state`'s own coordinate.
468
+ * A file the compaction restores re-registers through its restore frame's own delivery (the reader's commit,
469
+ * after this clear), never here. The read-before-edit gate tightens as a consequence, matching CC: after a
470
+ * compaction the model must re-Read a summarized-away file before editing it.
471
+ */
472
+ /**
473
+ * What the read state holds once a landed summary has taken its reads out of view — the ONE answer to "which entries
474
+ * survive a compaction": the seeded entries and `preserveKeys`. The summarized clear applies it to the live state; the
475
+ * restore's reads dedup against it (the context they deliver into is the one the summary leaves).
479
476
  */
477
+ export declare function readFileStateAfterSummary(state: ReadonlyMap<string, ReadEntry>, preserveKeys: ReadonlyArray<string>): ReadFileState;
480
478
  /**
481
- * The SECOND way a transmission leaves the model's view, and why this function is named for context
479
+ * The OTHER ways a transmission leaves the model's view, and why this function is named for context
482
480
  * SHRINK rather than compaction: the request-layer stale-result pass replaces an old tool result's
483
- * content with a cleared marker — no summary, no re-attachment, nothing changed in the session — and
484
- * for a Read result the effect on the entry is identical: the body its dedup stub points at is no
485
- * longer anywhere the model can see, so the entry must retire with it.
481
+ * content with a cleared marker, and the request guard's last-resort trim drops old messages — a Read's
482
+ * result or an attachment frame among them — from the request; no summary, no re-attachment, nothing changed in the
483
+ * session — and for a read the effect on the entry is identical: the body its dedup stub points at is no
484
+ * longer anywhere the model can see, so the entry must retire with it. A transmission is named by the
485
+ * tool call that carried it, or by the attachment frame (`attachmentTransmissionId`).
486
486
  *
487
487
  * Selection is by the TRANSMISSION, not by path — an entry a LATER Read re-transmitted carries that
488
488
  * later call's id and so survives a clear of the earlier one, which a path-keyed retirement could not
@@ -494,19 +494,15 @@ export declare function inheritSessionReadFileState(target: ReadFileState, inher
494
494
  * carries the id of the last Read that transmitted it, so if that result was cleared its id is in the list.
495
495
  */
496
496
  export type ReadFileStateShrink =
497
- /** A landed compaction: `attachedComplete` re-entered the context whole; `preserveKeys` stayed visible in the kept tail. */
497
+ /** A landed compaction: everything but {@link readFileStateAfterSummary} retires (`preserveKeys` stayed visible in the kept tail). */
498
498
  {
499
499
  kind: "summarized";
500
- attachedComplete: ReadonlyArray<{
501
- path: string;
502
- content: string;
503
- }>;
504
500
  preserveKeys?: ReadonlyArray<string>;
505
501
  }
506
- /** A request-layer stale-result clear: these tool calls' results are markers now. Nothing re-entered. */
502
+ /** These transmissions left the request (a stale-result clear's markers, a trim's dropped messages). Nothing re-entered. */
507
503
  | {
508
- kind: "tool-results-cleared";
509
- clearedToolCallIds: ReadonlyArray<string>;
504
+ kind: "transmissions-gone";
505
+ transmissions: ReadonlyArray<string>;
510
506
  };
511
507
  export declare function applyContextShrinkToReadFileState(state: ReadFileState, shrink: ReadFileStateShrink): void;
512
508
  /** CC 2.1.198 verbatim skeleton: the ENOENT error body for a
@@ -320,9 +320,9 @@ export function isReadDedupStubResult(resultText) {
320
320
  export function seedReadFileStateFromContext(state, key, content) {
321
321
  state.set(key, { hash: sha256(normalizeFileText(content)), totalLines: countLines(content), truncated: false, lastReadAt: Date.now(), seededFromContext: true });
322
322
  }
323
- export function seedReadFileStateFromTranscript(state, key, content, lastReadAt) {
323
+ export function seedReadFileStateFromTranscript(state, key, content, lastReadAt, transmission) {
324
324
  const text = normalizeFileText(content);
325
- state.set(key, { hash: sha256(text), totalLines: countLines(text), truncated: false, lastReadAt });
325
+ state.set(key, { hash: sha256(text), totalLines: countLines(text), truncated: false, lastReadAt, ...(transmission !== undefined ? { transmittedByToolCallId: transmission } : {}) });
326
326
  }
327
327
  export async function inheritSessionReadFileState(target, inherited, judgeKey) {
328
328
  for (const [key, entry] of inherited) {
@@ -333,28 +333,25 @@ export async function inheritSessionReadFileState(target, inherited, judgeKey) {
333
333
  target.set(judged, entry);
334
334
  }
335
335
  }
336
+ export function readFileStateAfterSummary(state, preserveKeys) {
337
+ const preserve = new Set(preserveKeys);
338
+ return new Map([...state].filter(([k, v]) => v.seededFromContext === true || preserve.has(k)));
339
+ }
336
340
  export function applyContextShrinkToReadFileState(state, shrink) {
337
- if (shrink.kind === "tool-results-cleared") {
338
- if (shrink.clearedToolCallIds.length === 0)
341
+ if (shrink.kind === "transmissions-gone") {
342
+ if (shrink.transmissions.length === 0)
339
343
  return;
340
- const cleared = new Set(shrink.clearedToolCallIds);
344
+ const cleared = new Set(shrink.transmissions);
341
345
  for (const [k, v] of [...state]) {
342
346
  if (v.transmittedByToolCallId !== undefined && cleared.has(v.transmittedByToolCallId))
343
347
  state.delete(k);
344
348
  }
345
349
  return;
346
350
  }
347
- const { attachedComplete, preserveKeys = [] } = shrink;
348
- const preserve = new Set(preserveKeys);
349
- for (const [k, v] of [...state]) {
350
- if (v.seededFromContext !== true && !preserve.has(k))
351
+ const surviving = readFileStateAfterSummary(state, shrink.preserveKeys ?? []);
352
+ for (const k of [...state.keys()])
353
+ if (!surviving.has(k))
351
354
  state.delete(k);
352
- }
353
- for (const f of attachedComplete) {
354
- const totalLines = countLines(f.content);
355
- const displayLines = f.content.length === 0 ? 0 : f.content.split("\n").length;
356
- state.set(f.path, { hash: sha256(f.content), totalLines, truncated: false, view: { start: 1, end: displayLines }, lastReadAt: Date.now() });
357
- }
358
355
  }
359
356
  export async function enoentMessage(env, key, cwd, signal, readDeny) {
360
357
  let msg = `File does not exist. Note: your current working directory is ${cwd}.`;
@@ -40,7 +40,7 @@ async function envFinalWrite(env, key, content, signal, opts) {
40
40
  fireEdited();
41
41
  return r;
42
42
  }
43
- const NOTHING_WAS_WRITTEN = new Set(["already_exists", "precondition_failed"]);
43
+ const NOTHING_WAS_WRITTEN = new Set(["already_exists", "precondition_failed", "target_unavailable"]);
44
44
  async function writeThroughEnv(env, key, content, signal, opts) {
45
45
  if (env.writeFileGuarded !== undefined) {
46
46
  const r = await env.writeFileGuarded(key, content, { canonicalPath: key, ...(opts?.exclusive === true ? { exclusive: true } : {}) }, signal);
@@ -14,7 +14,7 @@ export * from "./read-deny.js";
14
14
  export { readDenyHomesOf } from "./read-deny-home.js";
15
15
  import { BASH_READONLY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
16
16
  export { BASH_CLASSIFY_EXTRA_ALLOW, bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
17
- import { checkedReadDenyHomes, compileReadDeny, compileWriteDeny, isRootedReadDenyPattern, readDenyBuiltinConfigOf, readDenyPatternRun } from "./read-deny.js";
17
+ import { checkedReadDenyHomes, compileReadDeny, compileWriteDeny, readDenyBuiltinConfigOf, readDenyRowAnchor } from "./read-deny.js";
18
18
  import { withGateTimeout } from "../../core/gate-bounded-await.js";
19
19
  import { carriedReadFaceOf, carryHomeViews, homeViewsAt, placeReadDenyAtHome, placementViewsOf, readDenyPlacedLater, sayPlacementLost } from "./read-deny-home.js";
20
20
  import { deploymentFaceClampNotice, resolveContainmentFace } from "./containment-face.js";
@@ -142,7 +142,7 @@ function bandOwnTables(env, opts) {
142
142
  const carriedViews = carried ? placementViewsOf(opts.readDenyHomes) : undefined;
143
143
  if (carried && carriedViews === undefined)
144
144
  sayPlacementLost();
145
- const placesNothing = !unplaced.entries.some((e) => readDenyPatternRun(e.pattern).homeAnchored || isRootedReadDenyPattern(e.pattern));
145
+ const placesNothing = !unplaced.entries.some((e) => readDenyRowAnchor(e.pattern) !== "none");
146
146
  const late = carried || placesNothing ? undefined : readDenyPlacedLater(carryHomeViews(unplaced, homeViewsAt(env, [])), () => placeReadDenyAtHome(env, compileAt, readDenyHomes));
147
147
  const readDeny = late?.matcher ??
148
148
  (carried
@@ -41,7 +41,7 @@ export declare function readDenyLinksOf(env: ExecutionEnv, entries: readonly {
41
41
  /**
42
42
  * Which rows one placement reads, and where their heads are spelled:
43
43
  * · `home` — the home-anchored rows (`~/…`), under each home; the row's run anchors it there;
44
- * · `root` — the ROOTED rows (`/app/config-lkg.json`, {@link isRootedReadDenyPattern}), under the filesystem root; the run
44
+ * · `root` — the ROOTED rows (`/app/config-lkg.json`, {@link readDenyRowAnchor} `root`), under the filesystem root; the run
45
45
  * anchors it there. A root is no home: read whether or not a home answered;
46
46
  * · `unanchored` — the rows that open with the configuration directory's name (`~/.sema/…`, {@link agentConfigDirTail})
47
47
  * under each spelling of the root the host declares for that directory (`RunnerDeps.agentConfigDir`): the head BELOW
@@ -4,7 +4,7 @@ import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "../../core/gate-
4
4
  import { thrownText } from "../../core/thrown-value.js";
5
5
  import { agentConfigDirTail } from "../../core/agent-config-dir.js";
6
6
  import { validatePath } from "../../core/effective-path-target.js";
7
- import { isRootedReadDenyPattern, readDenyPatternRun, readDenyRunHead } from "./read-deny.js";
7
+ import { readDenyPatternRun, readDenyRowAnchor, readDenyRunHead } from "./read-deny.js";
8
8
  export const AUXILIARY_HOME_PROBE_MS = 2_000;
9
9
  const said = new Set();
10
10
  function sayOnce(kind, line) {
@@ -126,13 +126,14 @@ function anchoredRowsOf(entries, anchor) {
126
126
  return rows;
127
127
  }
128
128
  function rowHeadOf(pattern, caseSensitive, anchor) {
129
+ const rowAnchor = readDenyRowAnchor(pattern);
129
130
  if (anchor.at === "root") {
130
- const head = isRootedReadDenyPattern(pattern) ? literalHeadOf(pattern) : "";
131
+ const head = rowAnchor === "root" ? literalHeadOf(pattern) : "";
131
132
  return head.length > 0 ? head : undefined;
132
133
  }
133
- const { homeAnchored, run } = readDenyPatternRun(pattern);
134
- if (!homeAnchored)
134
+ if (rowAnchor !== "home")
135
135
  return undefined;
136
+ const { run } = readDenyPatternRun(pattern);
136
137
  if (anchor.at === "unanchored") {
137
138
  const tail = agentConfigDirTail(run, caseSensitive);
138
139
  return tail === undefined ? undefined : literalHeadOf(tail);