@sema-agent/core 7.16.0 → 7.17.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 (207) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/dist/agents/builtin-agents.d.ts +3 -3
  3. package/dist/agents/cross-session-envelope.d.ts +0 -1
  4. package/dist/agents/cross-session-envelope.js +0 -1
  5. package/dist/agents/peer-notices.d.ts +0 -9
  6. package/dist/agents/peer-notices.js +0 -63
  7. package/dist/agents/subagent.js +7 -15
  8. package/dist/brain/request-params.d.ts +2 -2
  9. package/dist/config/defaults.d.ts +0 -7
  10. package/dist/config/defaults.js +0 -2
  11. package/dist/core/artifact-host.d.ts +218 -0
  12. package/dist/core/artifact-host.js +92 -0
  13. package/dist/core/ask-origin.d.ts +29 -3
  14. package/dist/core/ask-origin.js +17 -9
  15. package/dist/core/ask-question.d.ts +25 -0
  16. package/dist/core/ask-question.js +4 -0
  17. package/dist/core/ask-unresolvable-notice.d.ts +30 -21
  18. package/dist/core/ask-unresolvable-notice.js +16 -8
  19. package/dist/core/auto-compaction.d.ts +10 -10
  20. package/dist/core/auto-mode-arming.d.ts +2 -2
  21. package/dist/core/background-agent-store.d.ts +5 -5
  22. package/dist/core/background-shell.d.ts +1 -1
  23. package/dist/core/checkpoint-execution-record.d.ts +0 -3
  24. package/dist/core/checkpoint-execution-record.js +0 -4
  25. package/dist/core/checkpoint-store.d.ts +71 -99
  26. package/dist/core/checkpoint-store.js +9 -16
  27. package/dist/core/context-edit.d.ts +36 -104
  28. package/dist/core/context-edit.js +37 -60
  29. package/dist/core/effective-path-target.d.ts +68 -8
  30. package/dist/core/effective-path-target.js +42 -30
  31. package/dist/core/engine-notice.d.ts +46 -32
  32. package/dist/core/env-budget.d.ts +31 -0
  33. package/dist/core/env-budget.js +39 -0
  34. package/dist/core/exec-output-tail.d.ts +5 -5
  35. package/dist/core/file-history-store.js +3 -6
  36. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  37. package/dist/core/fs-write-gate-policy.js +3 -3
  38. package/dist/core/gate-lanes.js +9 -15
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +2 -0
  41. package/dist/core/hands-band.d.ts +1 -1
  42. package/dist/core/hooks.d.ts +18 -17
  43. package/dist/core/lsp-session.d.ts +1 -1
  44. package/dist/core/lsp.d.ts +1 -1
  45. package/dist/core/lsp.js +5 -4
  46. package/dist/core/mcp.d.ts +5 -5
  47. package/dist/core/mcp.js +4 -48
  48. package/dist/core/memory-engine/engine.js +2 -1
  49. package/dist/core/memory-engine/layout.d.ts +18 -6
  50. package/dist/core/memory-engine/layout.js +40 -21
  51. package/dist/core/memory-recall.d.ts +0 -8
  52. package/dist/core/memory.d.ts +1 -1
  53. package/dist/core/oracle-isolation.js +2 -2
  54. package/dist/core/parent-spec-seat.d.ts +66 -0
  55. package/dist/core/parent-spec-seat.js +47 -0
  56. package/dist/core/permission-rule-model.d.ts +11 -6
  57. package/dist/core/permission-rule-model.js +53 -37
  58. package/dist/core/permission-rules.js +8 -3
  59. package/dist/core/physical-path.d.ts +37 -0
  60. package/dist/core/physical-path.js +30 -0
  61. package/dist/core/protocol-naming.d.ts +2 -2
  62. package/dist/core/remote-env.d.ts +1 -1
  63. package/dist/core/retired-keys.d.ts +19 -0
  64. package/dist/core/retired-keys.js +15 -0
  65. package/dist/core/runner/active-skill-scope.js +3 -3
  66. package/dist/core/runner/assemble-result.d.ts +15 -13
  67. package/dist/core/runner/assemble-result.js +5 -4
  68. package/dist/core/runner/compaction-call-options.d.ts +25 -89
  69. package/dist/core/runner/contracts.d.ts +11 -10
  70. package/dist/core/runner/gate-exit.js +15 -1
  71. package/dist/core/runner/halt-attribution.d.ts +35 -0
  72. package/dist/core/runner/halt-attribution.js +16 -0
  73. package/dist/core/runner/memory-consolidation.d.ts +0 -2
  74. package/dist/core/runner/memory-consolidation.js +0 -1
  75. package/dist/core/runner/prepare-artifact.d.ts +52 -0
  76. package/dist/core/runner/prepare-artifact.js +63 -0
  77. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  78. package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
  79. package/dist/core/runner/prepare-config-doors.d.ts +3 -3
  80. package/dist/core/runner/prepare-config-doors.js +5 -2
  81. package/dist/core/runner/prepare-context-lane.d.ts +2 -2
  82. package/dist/core/runner/prepare-context-lane.js +2 -13
  83. package/dist/core/runner/prepare-file-history.d.ts +1 -1
  84. package/dist/core/runner/prepare-file-history.js +3 -52
  85. package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
  86. package/dist/core/runner/prepare-hands-readface.js +3 -2
  87. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
  88. package/dist/core/runner/prepare-inherited-gate.js +7 -8
  89. package/dist/core/runner/prepare-memory.d.ts +4 -6
  90. package/dist/core/runner/prepare-memory.js +4 -2
  91. package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
  92. package/dist/core/runner/prepare-policy-chain.js +15 -30
  93. package/dist/core/runner/prepare-prompt-assembly.js +1 -1
  94. package/dist/core/runner/prepare-question-face.js +2 -1
  95. package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
  96. package/dist/core/runner/prepare-suspend-saga.js +0 -2
  97. package/dist/core/runner/prepare-task.js +12 -11
  98. package/dist/core/runner/prepare-wiring-manifest.js +9 -7
  99. package/dist/core/runner/resume-admission.js +2 -1
  100. package/dist/core/runner/resume-internals-and-config.js +4 -0
  101. package/dist/core/runner/retired-runner-deps.d.ts +3 -0
  102. package/dist/core/runner/retired-runner-deps.js +7 -0
  103. package/dist/core/runner/run-harness-handlers.js +5 -3
  104. package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
  105. package/dist/core/runner/run-settle-and-teardown.js +3 -1
  106. package/dist/core/runner/session-file-state-replay.js +3 -3
  107. package/dist/core/runner/session-rule-policy.js +3 -4
  108. package/dist/core/runner/stream-halt-verbs.js +1 -1
  109. package/dist/core/runner/stream-steer-verb.js +1 -4
  110. package/dist/core/runner/workspace-path.js +3 -0
  111. package/dist/core/runner-deps.d.ts +30 -49
  112. package/dist/core/safe-notify.d.ts +1 -1
  113. package/dist/core/sensitive-path-policy.d.ts +1 -1
  114. package/dist/core/sensitive-path-policy.js +11 -12
  115. package/dist/core/session.d.ts +2 -2
  116. package/dist/core/skill-tool-specifier.d.ts +1 -1
  117. package/dist/core/skills-directory.d.ts +1 -1
  118. package/dist/core/skills-directory.js +4 -3
  119. package/dist/core/spec-contract.js +5 -4
  120. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  121. package/dist/core/strategy-store.d.ts +8 -11
  122. package/dist/core/task-event.d.ts +20 -17
  123. package/dist/core/task-limits.d.ts +1 -1
  124. package/dist/core/task-notification.d.ts +5 -5
  125. package/dist/core/task-registry-agent.d.ts +5 -5
  126. package/dist/core/task-registry-monitor.d.ts +2 -2
  127. package/dist/core/task-registry-shared.d.ts +32 -28
  128. package/dist/core/task-registry-shared.js +4 -13
  129. package/dist/core/task-registry.d.ts +8 -8
  130. package/dist/core/task-result.d.ts +37 -37
  131. package/dist/core/task-spec.d.ts +5 -5
  132. package/dist/core/task-stream.d.ts +16 -11
  133. package/dist/core/tool-catalog-entries.d.ts +1 -0
  134. package/dist/core/tool-catalog-entries.js +6 -1
  135. package/dist/core/tool-conformance.d.ts +0 -2
  136. package/dist/core/tool-conformance.js +1 -3
  137. package/dist/core/tool-errors.d.ts +5 -0
  138. package/dist/core/tool-errors.js +1 -0
  139. package/dist/core/tool-policy.d.ts +66 -22
  140. package/dist/core/tool-policy.js +31 -5
  141. package/dist/core/tool-result-budget.d.ts +1 -1
  142. package/dist/core/tool-result-store.d.ts +11 -11
  143. package/dist/core/tool-spec.d.ts +34 -38
  144. package/dist/core/tools.d.ts +2 -2
  145. package/dist/core/trace.d.ts +12 -15
  146. package/dist/core/untrusted-text.d.ts +1 -1
  147. package/dist/core/wiring-manifest.d.ts +4 -3
  148. package/dist/core/workflow-journal-store.d.ts +11 -11
  149. package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
  150. package/dist/engine/execution-env/node-execution-env.js +8 -5
  151. package/dist/engine/harness/agent-harness.d.ts +47 -4
  152. package/dist/engine/harness/agent-harness.js +52 -9
  153. package/dist/engine/loop/agent-loop.js +15 -8
  154. package/dist/engine/loop/types.d.ts +5 -0
  155. package/dist/index.d.ts +20 -14
  156. package/dist/index.js +16 -10
  157. package/dist/internal/harness.d.ts +1 -0
  158. package/dist/orchestration/run-spec.d.ts +3 -0
  159. package/dist/orchestration/run-spec.js +3 -3
  160. package/dist/orchestration/run-workflow-tool.d.ts +12 -6
  161. package/dist/orchestration/run-workflow-tool.js +23 -5
  162. package/dist/orchestration/workflow-primitives.d.ts +4 -2
  163. package/dist/orchestration/workflow-primitives.js +1 -4
  164. package/dist/orchestration/workflow-script-store.js +9 -25
  165. package/dist/orchestration/workflow-types.d.ts +34 -4
  166. package/dist/orchestration/workflow-types.js +2 -0
  167. package/dist/orchestration/workflow.d.ts +39 -7
  168. package/dist/orchestration/workflow.js +237 -113
  169. package/dist/prompt-assembly/assemble.d.ts +2 -2
  170. package/dist/prompt-assembly/assemble.js +9 -9
  171. package/dist/prompt-assembly/composer.d.ts +6 -7
  172. package/dist/prompt-assembly/composer.js +10 -10
  173. package/dist/prompt-assembly/epoch.js +1 -1
  174. package/dist/prompt-assembly/event-registry.d.ts +0 -21
  175. package/dist/prompt-assembly/packs/sema-default.js +30 -30
  176. package/dist/prompt-assembly/types.d.ts +14 -14
  177. package/dist/prompts/coordinator.d.ts +0 -13
  178. package/dist/prompts/coordinator.js +0 -3
  179. package/dist/stores/cc/task-list-store.js +2 -10
  180. package/dist/stores/file/checkpoint-store.js +1 -5
  181. package/dist/stores/file/file-history-store.js +2 -2
  182. package/dist/stores/file/fs-atomic.d.ts +15 -18
  183. package/dist/stores/file/fs-atomic.js +4 -14
  184. package/dist/stores/file/mailbox-store.d.ts +7 -11
  185. package/dist/stores/file/mailbox-store.js +4 -11
  186. package/dist/stores/file/permission-rule-store.d.ts +4 -35
  187. package/dist/stores/file/permission-rule-store.js +4 -148
  188. package/dist/tools/artifact/artifact-text.d.ts +62 -0
  189. package/dist/tools/artifact/artifact-text.js +68 -0
  190. package/dist/tools/artifact/artifact-tool.d.ts +52 -0
  191. package/dist/tools/artifact/artifact-tool.js +351 -0
  192. package/dist/tools/artifact/local-stub.d.ts +26 -0
  193. package/dist/tools/artifact/local-stub.js +297 -0
  194. package/dist/tools/fs/bash-readonly-classifier.d.ts +90 -8
  195. package/dist/tools/fs/bash-readonly-classifier.js +294 -70
  196. package/dist/tools/fs/fs-bash.d.ts +14 -10
  197. package/dist/tools/fs/fs-bash.js +50 -33
  198. package/dist/tools/fs/fs-search-tools.js +5 -2
  199. package/dist/tools/fs/fs-shared.d.ts +2 -0
  200. package/dist/tools/fs/fs-shared.js +3 -11
  201. package/dist/tools/fs/index.d.ts +4 -0
  202. package/dist/tools/fs/index.js +4 -1
  203. package/dist/tools/fs/safety.d.ts +117 -1
  204. package/dist/tools/fs/safety.js +131 -31
  205. package/dist/tools/fs/search.js +8 -6
  206. package/package.json +2 -1
  207. package/test/export-surface.snapshot.json +119 -39
@@ -49,24 +49,14 @@ export declare const COMPACTION_TRIGGER_BUFFER_TOKENS = 13000;
49
49
  */
50
50
  export declare function contextEditFrontier(window: number): number;
51
51
  /**
52
- * design/374which CLEARING MACHINE `clearStaleToolResults` runs.
52
+ * The `RunnerDeps.microCompact.machine` vocabulary ONE clearing machine, switchable:
53
53
  *
54
- * - `"cc"` (default since the slice-3 flip): the CC 2.1.223 `yId` machine — keep 5, a
55
- * ≥20000-token minimum-savings gate (below it the pass is a byte-identical no-op), ONE deep
56
- * clear of everything beyond the keep window, the window base counts already-cleared/offloaded
57
- * occurrences (they hold their keep slots), and CC's plain marker bytes.
58
- * - `"legacy"`: the pre-374 sema machine, kept as the explicit compatibility opt-out keep 3, no
59
- * minimum-savings gate, incremental oldest-first clearing that STOPS once the budget is met,
60
- * legacy marker bytes. In the request pipeline the opt-out also keeps the pre-374 BACKSTOP
61
- * ORDER (guard trim as the ordinary second line) — see the guard-chain note in prepare-task's
62
- * context hook.
63
- *
64
- * The pre-flip P-form warning (design/374 r2-B⑨/C-5: "cc" + the OLD message-dropping guard-trim
65
- * backstop = the 20k gate in front of the only reduction) is RESOLVED, not merely re-worded: the
66
- * slice-3 fallback re-ordering shipped in the same version as this default (blocking-point machine
67
- * re-run when the frontier pass is off → in-turn forced compaction behind the adopt seam → trim
68
- * demoted to the disaster-only last resort), so the default configuration never hands an
69
- * under-20k refusal straight to a message-dropping trim.
54
+ * - `"cc"` (default): the CC 2.1.223 `yId` machine — keep 5, a ≥20000-token minimum-savings gate
55
+ * (below it the pass is a byte-identical no-op), ONE deep clear of everything beyond the keep
56
+ * window, the window base counts already-cleared/offloaded occurrences (they hold their keep
57
+ * slots), and CC's plain marker bytes.
58
+ * - `"off"`: no proactive frontier clearing; the machine gets its one shot at the blocking point
59
+ * of the guard chain instead (trace trigger `"blocking"`).
70
60
  *
71
61
  * WHOLE-MACHINE CROSS-CHECK against the 2.1.250 corpus (design/374 slice 4 — the "the knob number
72
62
  * was copied, the machine was not" disease is checked by naming the machine's parts, not the one
@@ -93,39 +83,28 @@ export declare function contextEditFrontier(window: number): number;
93
83
  * keeps a PROACTIVE frontier trigger CC's client does not have, anchored to Anthropic's
94
84
  * platform context-management policy rather than to CC-CLI — registered divergence D-1, and
95
85
  * re-stated at {@link clearStaleToolResults}'s ANCHOR paragraph.
86
+ *
87
+ * A closed set (`docs/CLOSED-SETS.md`): the knob door quotes it, so a word outside it is refused by
88
+ * name rather than folded to the default.
96
89
  */
97
- export type ContextEditMachine = "legacy" | "cc";
90
+ export declare const MICRO_COMPACT_MACHINES: readonly ["off", "cc"];
91
+ export type MicroCompactMachine = (typeof MICRO_COMPACT_MACHINES)[number];
98
92
  /**
99
- * How many most-recent CANDIDATE tool results keep their content when the stale-result pass runs
100
- * per machine (design/374: the two values govern DIFFERENT machines, so they are separate
101
- * constants, not one adjudicated number):
102
- *
103
- * - LEGACY machine (the explicit opt-out): 3. The measured objection to 5 stands FOR THIS MACHINE
104
- * (probed on a 200k model, 140k usage anchor, five 36k-char Bash results: at 3 the pass clears
105
- * two and lands under the 177k guard; at 5 nothing is clearable and the guard trim cannot
106
- * recover the batch either) — on the legacy machine the keep window is the only lever between
107
- * the frontier and the message-dropping trim, so widening it disables the whole defense in
108
- * exactly the terminal-batch shape that needs it. This is why the slice-3 "keep default 3→5"
109
- * flip rides the MACHINE default, not this constant: painting 5 onto the opt-out machine would
110
- * recreate the probed failure inside the escape hatch.
111
- * - CC machine (the default since slice 3): 5 (2.1.223 `uAp`; 2.1.250 `var k = 5` at the call site
112
- * cli250.js:58375, and 2.1.245 carries the same value — zero drift across 245→250; same value
113
- * again on CC 88's `timeBasedMCConfig`, i.e. stable across all three corpus generations, and it
114
- * is a hard-coded constant upstream, not a setting). The #384 adjudication adopted 5
115
- * CONDITIONAL on the whole machine coming with it (20k gate + deep clear + re-ordered
116
- * backstops); the cc machine carries that condition, and slice 3's backstop re-ordering
117
- * completed it — 5 is now the effective default keep, and the 200k probe above is answered by
118
- * the machine (§3.5: the gate correctly refuses the no-win clear and the in-turn forced
119
- * compaction owns the request instead of a message-dropping trim).
93
+ * How many most-recent CANDIDATE tool results keep their content when the stale-result pass runs:
94
+ * 5 (CC 2.1.223 `uAp`; 2.1.250 `var k = 5` at the call site cli250.js:58375, and 2.1.245 carries
95
+ * the same value zero drift across 245→250; same value again on CC 88's `timeBasedMCConfig`, i.e.
96
+ * stable across all three corpus generations, and it is a hard-coded constant upstream, not a
97
+ * setting). The #384 adjudication adopted 5 CONDITIONAL on the whole machine coming with it (20k
98
+ * gate + deep clear + re-ordered backstops); the machine carries that condition 3.5: the gate
99
+ * refuses a no-win clear and the in-turn forced compaction owns the request instead of a
100
+ * message-dropping trim).
120
101
  */
121
- export declare const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
122
102
  export declare const CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS = 5;
123
103
  /** design/374 §3.1 — CC's minimum-savings gate (`vUs` :397773; 2.1.250 `Mnn` cli250.js:41714, same
124
104
  * value): the cc machine refuses to clear at
125
105
  * all unless the structural estimate of what WOULD be cleared reaches this many tokens. Rationale
126
106
  * (independent of "CC does it"): every clear breaks the provider prefix cache once, so a clear that
127
- * saves less than the re-prefill it causes is a net loss; the gate prices that trade. The LEGACY
128
- * machine has no such gate (1 token over budget clears) — that difference is machine-defining. */
107
+ * saves less than the re-prefill it causes is a net loss; the gate prices that trade. */
129
108
  export declare const MIN_CLEAR_SAVINGS_TOKENS = 20000;
130
109
  /** Minimum kept results — CC `EUs` :397710 `Math.max(1, keepRecent)`. See
131
110
  * {@link ContextEditOptions.keepRecentToolResults} for why both degenerate ends are unusable. */
@@ -133,26 +112,10 @@ export declare const MIN_KEEP_RECENT_TOOL_RESULTS = 1;
133
112
  export interface ContextEditOptions {
134
113
  /** Start clearing once estimated context tokens exceed this. */
135
114
  budgetTokens: number;
136
- /**
137
- * design/374 — which clearing machine runs; see {@link ContextEditMachine}. Default `"cc"`
138
- * (the slice-3 default flip); pass `"legacy"` explicitly for the pre-374 machine, byte-identical.
139
- */
140
- machine?: ContextEditMachine;
141
- /**
142
- * Mixed-configuration protection (design/374 r2-Md⑪ family): make the LEGACY machine recognize
143
- * the CC marker forms as already-cleared, so a ledger-replayed CC marker is never re-cleared
144
- * into a legacy marker (destroying its ref/media disclosure). Wired by the pipeline whenever a
145
- * cc-minting knob is on (machine "cc" — where it is implied — or the rejection-recovery arm over
146
- * a legacy frontier machine). Default false — which since the slice-3 flip is the posture of the
147
- * explicit OPT-OUT machine with MC-R also off (C15; the shipped default machine is "cc", where
148
- * this option is moot): in that pure opt-out, tool output that merely SPELLS the CC marker is
149
- * untrusted bytes and stays clearable exactly as pre-374.
150
- */
151
- recognizeCcMarkers?: boolean;
152
115
  /**
153
116
  * Fired AT MOST ONCE per pass, only when the pass cleared at least one result: the cleared
154
- * indexes (into the returned array) and the pass's savings estimate (cc machine: the structural
155
- * minimum-savings estimate it gated on; legacy machine: the accounted structural delta). The
117
+ * indexes (into the returned array) and the pass's savings estimate (the structural
118
+ * minimum-savings estimate it gated on). The
156
119
  * request-projection ledger (design/374 §3.2.6) records cleared occurrences through this seat —
157
120
  * observation only, a throwing callback is swallowed and never fails the request build.
158
121
  */
@@ -164,9 +127,7 @@ export interface ContextEditOptions {
164
127
  }) => void;
165
128
  /**
166
129
  * Always keep the content of this many most-recent CANDIDATE tool results (candidates = results
167
- * from {@link compactableTools}). Default: the selected machine's own default —
168
- * {@link DEFAULT_KEEP_RECENT_TOOL_RESULTS} = 3 (legacy) / {@link CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS}
169
- * = 5 (cc); see those constants for why the two machines carry different numbers.
130
+ * from {@link compactableTools}). Default {@link CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS} = 5.
170
131
  *
171
132
  * Floored at 1, which IS CC's rule (`EUs` `Math.max(1, keepRecent)`), whose own source note gives
172
133
  * the reason: 0 leaves the model with zero working tool context, and in CC's `slice(-0)` spelling
@@ -190,20 +151,16 @@ export interface ContextEditOptions {
190
151
  persist: (toolCallId: string, fullText: string) => string;
191
152
  };
192
153
  /**
193
- * design/123 D3 (= fable-M5) — ANCHORED accounting start: `estimateContextTokens(messages,
194
- * charsPerToken).tokens` for the SAME message array. When set, the trigger gate AND the internal
195
- * bookkeeping start from this usage-anchored value instead of the pure structural sum (which
196
- * underestimates code by ~25% and CJK by 60–75% — the defense would self-certify and pass
197
- * over-window requests). Bookkeeping semantics (pinned): clearing a message AFTER the usage anchor
198
- * decrements by its structural delta; clearing a message AT/BEFORE the anchor decrements ZERO —
199
- * the anchor usage was billed with that content included and does not shrink on a request-view
200
- * edit (the clear only pays off on the NEXT request, after a fresh anchor lands), so the defense
201
- * keeps clearing deeper until the anchored total is met or nothing clearable remains.
202
- * Absent ⇒ legacy structural gate + accounting (byte-identical behavior).
154
+ * design/123 D3 (= fable-M5) — ANCHORED trigger: `estimateContextTokens(messages,
155
+ * charsPerToken).tokens` for the SAME message array. When set, the trigger gate reads this
156
+ * usage-anchored value instead of the pure structural sum (which underestimates code by ~25% and
157
+ * CJK by 60–75% — the defense would self-certify and pass over-window requests). The minimum-
158
+ * savings gate stays CC's structural form either way (design/374 D-5). Absent the structural
159
+ * sum triggers.
203
160
  */
204
161
  anchoredTotalTokens?: number;
205
162
  /** design/123 D2 — structural coefficient (`Model.charsPerToken`); default 3 (design/374 slice 4;
206
- * it was 4 before). Note this does NOT reach the cc machine's ≥20k gate, which is CC-exactly
163
+ * it was 4 before). Note this does NOT reach the ≥20k gate, which is CC-exactly
207
164
  * hard-coded to 4 — see {@link structuralClearSavings}. */
208
165
  charsPerToken?: number;
209
166
  /**
@@ -215,13 +172,11 @@ export interface ContextEditOptions {
215
172
  compactableTools?: ReadonlySet<string>;
216
173
  /**
217
174
  * design/374 (RB-212 durable half): the PRE-CAP counterpart array of `messages`, position-aligned
218
- * (the pipeline's aggregate caps replace in place, never drop). When present, the CC machine
175
+ * (the pipeline's aggregate caps replace in place, never drop). When present, the machine
219
176
  * composes each clear's marker + offload text from `clearSource[idx]` — the same occurrence
220
177
  * bytes the cleared-projection ledger repaints the marker onto at the next session rebuild — so
221
178
  * the durable marker discloses the occurrence's REAL media and the page-back ref resolves to its
222
- * real text rather than to a cap-stripped copy. Ignored by the legacy machine (its clears are
223
- * request-local and correctly describe the projection they were handed — default bytes do not
224
- * move). An entry that is not a tool result, or that carries a different `toolCallId` than the
179
+ * real text rather than to a cap-stripped copy. An entry that is not a tool result, or that carries a different `toolCallId` than the
225
180
  * target, is ignored per index (composition falls back to the projection copy — a misaligned
226
181
  * source must never mis-attribute content).
227
182
  */
@@ -240,28 +195,6 @@ export interface ContextEditOptions {
240
195
  * excluded one layer later, at `convertToLlm`, by its own three-key identity (`isSyntheticApiErrorMessage`).
241
196
  */
242
197
  export declare function dropEmptyFailureAssistants(messages: AgentMessage[]): AgentMessage[];
243
- /**
244
- * Context-editing — the lightest, safest reduction: when context exceeds the budget, replace the
245
- * CONTENT of older tool-result messages with a short marker, keeping the turn structure
246
- * (tool_call/tool_result pairing) intact; the most recent `keepRecentToolResults` CANDIDATE
247
- * results keep their content. Request-only and non-destructive: returns a new array, the durable
248
- * session keeps the full results (the request-projection ledger, not this function, is what makes
249
- * a clear DECISION durable across requests — see `replayClearedProjection`).
250
- *
251
- * ANCHOR (design/374 片4 re-anchoring): this proactive frontier hook is the client-side form of
252
- * Anthropic's PLATFORM context-management policy (`clear_tool_uses_20250919` — same
253
- * trigger/keep/clear-at-least construction), NOT a CC-CLI parity claim: CC 2.1.x's client
254
- * keep-recent clear lives exclusively on its `context_hint` REJECTION leg (default-gated off), a
255
- * machine whose sema form is the MC-R arm (`planRejectionClears` + the runner's recovery chain).
256
- * The per-mechanism CC anchors that DO hold (the COMPACTABLE_TOOLS set, the candidate-scoped keep
257
- * window, the floor, and the whole `machine: "cc"` clearing form) are cited at their sites.
258
- * (An earlier note here cited an unverified "~29% improvement" figure — removed: no first-party
259
- * source in the corpus; the machine's case rests on the measured mechanics, not that number.)
260
- *
261
- * Clear ORDER is machine-dependent: legacy clears oldest-first and stops at the budget; the cc
262
- * machine deep-clears everything beyond the keep window behind the ≥20k gate (see
263
- * {@link ContextEditMachine}).
264
- */
265
198
  export declare function clearStaleToolResults(messages: AgentMessage[], opts: ContextEditOptions): AgentMessage[];
266
199
  /** design/374 slice 1b (X5) — the resolved trigger-side window and whether the clamp repaired it. */
267
200
  export interface TriggerWindowResolution {
@@ -368,11 +301,10 @@ export type RejectionClearPlan = {
368
301
  };
369
302
  /**
370
303
  * design/374 §3.2 (slice 2) — plan the MC-R rejection clear over the REJECTED projection: the same
371
- * cc machine (keep 5 default, window base counts cleared occurrences, five-form exclusion, Sby
304
+ * machine (keep 5 default, window base counts cleared occurrences, five-form exclusion, Sby
372
305
  * savings, ≥20k gate) with the TRIGGER replaced by the rejection itself — the provider already said
373
- * the input does not fit, so there is no budget coordinate to compare against (which is also why an
374
- * enabled MC-R always runs the cc form regardless of the frontier machine knob: the legacy
375
- * incremental form is defined by "stop when the budget is met" and has no meaning here).
306
+ * the input does not fit, so there is no budget coordinate to compare against (an enabled MC-R runs
307
+ * even with the frontier machine `"off"`).
376
308
  * `keyOf` threads occurrence identity (the ledger is the ONLY way a rejection-arm clear survives
377
309
  * the session rebuild — §1.2's adoption chain discards the in-memory view); a candidate the index
378
310
  * cannot key is skipped from BOTH the clear set and the savings (never promise savings the ledger
@@ -1,11 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "../internal/harness.js";
2
+ import { DEFAULT_CHARS_PER_TOKEN, estimateTokens } from "../internal/harness.js";
3
3
  import { isToolResult } from "./message-utils.js";
4
4
  import { PERSISTED_OUTPUT_PREFIX, offloadPagebackHint } from "./tool-result-store.js";
5
+ import { assertNoRetiredKeys } from "./retired-keys.js";
5
6
  import { compactableToolNames } from "./tool-registry.js";
6
- const CLEARED_MARKER = "[tool result cleared to save context]";
7
7
  const CC_CLEARED_MARKER = "[Old tool result content cleared]";
8
- const LEGACY_CLEARED_EXTENDED_PREFIX = "[tool result cleared to save context —";
9
8
  const CC_CLEARED_EXTENDED_PREFIX = "[Old tool result content cleared —";
10
9
  const refNote = (ref) => offloadPagebackHint(ref, "cleared");
11
10
  const mediaNote = (blocks) => {
@@ -22,10 +21,9 @@ const mediaNote = (blocks) => {
22
21
  const breakdown = [...byType].map(([t, c]) => `${c} ${t}`).join(", ");
23
22
  return `${n} attachments (${breakdown}) no longer visible after this clear`;
24
23
  };
25
- const clearedMarker = (notes, machine) => {
24
+ const clearedMarker = (notes) => {
26
25
  const present = notes.filter((n) => n !== undefined);
27
- const [plain, extendedPrefix] = machine === "cc" ? [CC_CLEARED_MARKER, CC_CLEARED_EXTENDED_PREFIX] : [CLEARED_MARKER, LEGACY_CLEARED_EXTENDED_PREFIX];
28
- return present.length > 0 ? `${extendedPrefix} ${present.join("; ")}]` : plain;
26
+ return present.length > 0 ? `${CC_CLEARED_EXTENDED_PREFIX} ${present.join("; ")}]` : CC_CLEARED_MARKER;
29
27
  };
30
28
  export const EDIT_FRACTION = 0.7;
31
29
  export const CONTEXT_OUTPUT_RESERVE_TOKENS = 20000;
@@ -33,7 +31,7 @@ export const COMPACTION_TRIGGER_BUFFER_TOKENS = 13000;
33
31
  export function contextEditFrontier(window) {
34
32
  return Math.max(window - (CONTEXT_OUTPUT_RESERVE_TOKENS + COMPACTION_TRIGGER_BUFFER_TOKENS), Math.floor(window * EDIT_FRACTION));
35
33
  }
36
- export const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
34
+ export const MICRO_COMPACT_MACHINES = ["off", "cc"];
37
35
  export const CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS = 5;
38
36
  export const MIN_CLEAR_SAVINGS_TOKENS = 20000;
39
37
  const MEDIA_BLOCK_SAVINGS_TOKENS = 2000;
@@ -55,16 +53,9 @@ function soleMarkerText(m) {
55
53
  const text = content[0].text;
56
54
  return typeof text === "string" ? text : undefined;
57
55
  }
58
- function isCleared(m, recognizeCcForms = false) {
56
+ function isCleared(m) {
59
57
  const text = soleMarkerText(m);
60
- if (text === undefined)
61
- return false;
62
- if (text === CLEARED_MARKER || text.startsWith(LEGACY_CLEARED_EXTENDED_PREFIX))
63
- return true;
64
- return recognizeCcForms && (text === CC_CLEARED_MARKER || text.startsWith(CC_CLEARED_EXTENDED_PREFIX));
65
- }
66
- function isCcCleared(m) {
67
- if (isCleared(m, true))
58
+ if (text !== undefined && (text === CC_CLEARED_MARKER || text.startsWith(CC_CLEARED_EXTENDED_PREFIX)))
68
59
  return true;
69
60
  const content = m.content;
70
61
  if (!Array.isArray(content) || content.length === 0)
@@ -97,7 +88,7 @@ function structuralClearSavings(target) {
97
88
  }
98
89
  return saved;
99
90
  }
100
- function clearOneResult(target, offload, machine) {
91
+ function clearOneResult(target, offload) {
101
92
  const rawContent = Array.isArray(target.content) ? target.content : [];
102
93
  let ref;
103
94
  if (offload) {
@@ -115,7 +106,7 @@ function clearOneResult(target, offload, machine) {
115
106
  }
116
107
  }
117
108
  const mediaBlocks = rawContent.filter((c) => c?.type !== "text");
118
- const marker = clearedMarker([ref, mediaBlocks.length > 0 ? mediaNote(mediaBlocks) : undefined], machine);
109
+ const marker = clearedMarker([ref, mediaBlocks.length > 0 ? mediaNote(mediaBlocks) : undefined]);
119
110
  return { ...target, content: [{ type: "text", text: marker }] };
120
111
  }
121
112
  function reportCleared(onCleared, clears, tokensSavedEstimate) {
@@ -127,18 +118,34 @@ function reportCleared(onCleared, clears, tokensSavedEstimate) {
127
118
  catch {
128
119
  }
129
120
  }
121
+ const CONTEXT_EDIT_OPTION_KEYS = new Set([
122
+ "budgetTokens",
123
+ "onCleared",
124
+ "keepRecentToolResults",
125
+ "offload",
126
+ "anchoredTotalTokens",
127
+ "charsPerToken",
128
+ "compactableTools",
129
+ "clearSource",
130
+ ]);
131
+ const RETIRED_CONTEXT_EDIT_OPTION_KEYS = {
132
+ machine: 'the pass runs one machine (the CC form); the frontier on/off switch is `RunnerDeps.microCompact.machine: "off" | "cc"`',
133
+ recognizeCcMarkers: "the cleared-marker forms are always recognized",
134
+ };
130
135
  export function clearStaleToolResults(messages, opts) {
131
- const cpt = opts.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
132
- const machine = opts.machine ?? "cc";
133
- if (machine !== "legacy" && machine !== "cc") {
134
- throw new TypeError(`machine must be "legacy" | "cc" (got ${String(machine)}); omit it for the default "cc"`);
136
+ assertNoRetiredKeys(opts, RETIRED_CONTEXT_EDIT_OPTION_KEYS, "clearStaleToolResults option");
137
+ for (const key of Object.keys(opts)) {
138
+ if (CONTEXT_EDIT_OPTION_KEYS.has(key) || Reflect.get(opts, key) === undefined)
139
+ continue;
140
+ const replacement = RETIRED_CONTEXT_EDIT_OPTION_KEYS[key];
141
+ throw new TypeError(`clearStaleToolResults: option "${key}" is not one this pass reads` +
142
+ (replacement !== undefined ? ` — it is retired: ${replacement}` : "") +
143
+ `. Legal keys: ${[...CONTEXT_EDIT_OPTION_KEYS].join(", ")}. Refused rather than ignored: a silently dropped option reads to the caller as in force.`);
135
144
  }
136
- const anchored = opts.anchoredTotalTokens !== undefined;
145
+ const cpt = opts.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
137
146
  let total;
138
- let anchorIdx = -1;
139
- if (anchored) {
147
+ if (opts.anchoredTotalTokens !== undefined) {
140
148
  total = opts.anchoredTotalTokens;
141
- anchorIdx = estimateContextTokens(messages, cpt).lastUsageIndex ?? -1;
142
149
  }
143
150
  else {
144
151
  total = 0;
@@ -149,41 +156,11 @@ export function clearStaleToolResults(messages, opts) {
149
156
  if (total <= opts.budgetTokens) {
150
157
  return messages;
151
158
  }
152
- if (machine === "cc") {
153
- return clearStaleToolResultsCc(messages, opts);
154
- }
155
- const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults, DEFAULT_KEEP_RECENT_TOOL_RESULTS);
156
- const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
157
- const toolResultCandidates = messages.flatMap((m, i) => isToolResult(m) && !isCleared(m, opts.recognizeCcMarkers === true) && compactable.has(m.toolName) ? [{ idx: i, target: m }] : []);
158
- const clearable = toolResultCandidates.slice(0, Math.max(0, toolResultCandidates.length - keep));
159
- if (clearable.length === 0) {
160
- return messages;
161
- }
162
- const out = messages.slice();
163
- let current = total;
164
- const clears = [];
165
- let savedEstimate = 0;
166
- for (const { idx, target } of clearable) {
167
- if (current <= opts.budgetTokens) {
168
- break;
169
- }
170
- const before = estimateTokens(target, cpt);
171
- const cleared = clearOneResult(target, opts.offload, "legacy");
172
- out[idx] = cleared;
173
- clears.push({ index: idx });
174
- const delta = before - estimateTokens(cleared, cpt);
175
- savedEstimate += delta;
176
- current -= idx > anchorIdx ? delta : 0;
177
- }
178
- reportCleared(opts.onCleared, clears, savedEstimate);
179
- return out;
180
- }
181
- function clearStaleToolResultsCc(messages, opts) {
182
159
  const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults, CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS);
183
160
  const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
184
161
  const candidates = messages.flatMap((m, i) => isToolResult(m) && compactable.has(m.toolName) ? [{ idx: i, target: m }] : []);
185
162
  const beyondKeep = candidates.slice(0, Math.max(0, candidates.length - keep));
186
- const clearable = beyondKeep.filter(({ target }) => !isCcCleared(target));
163
+ const clearable = beyondKeep.filter(({ target }) => !isCleared(target));
187
164
  if (clearable.length === 0) {
188
165
  return messages;
189
166
  }
@@ -196,7 +173,7 @@ function clearStaleToolResultsCc(messages, opts) {
196
173
  for (const { idx, target } of clearable) {
197
174
  const src = opts.clearSource?.[idx];
198
175
  const source = src !== undefined && isToolResult(src) && src.toolCallId === target.toolCallId ? src : target;
199
- out[idx] = clearOneResult(source, opts.offload, "cc");
176
+ out[idx] = clearOneResult(source, opts.offload);
200
177
  clears.push({ index: idx });
201
178
  }
202
179
  reportCleared(opts.onCleared, clears, tokensSaved);
@@ -248,7 +225,7 @@ export function planRejectionClears(messages, opts) {
248
225
  const candidates = messages.flatMap((m) => (isToolResult(m) && compactable.has(m.toolName) ? [m] : []));
249
226
  const beyondKeep = candidates.slice(0, Math.max(0, candidates.length - keep));
250
227
  const clearable = beyondKeep.flatMap((m) => {
251
- if (isCcCleared(m))
228
+ if (isCleared(m))
252
229
  return [];
253
230
  const at = opts.keyOf(m);
254
231
  return at === undefined ? [] : [{ m, at }];
@@ -260,7 +237,7 @@ export function planRejectionClears(messages, opts) {
260
237
  return { declined: "below_min_savings" };
261
238
  const cleared = clearable.map(({ m, at }) => {
262
239
  const source = at.occurrence !== undefined && isToolResult(at.occurrence) ? at.occurrence : m;
263
- const clearedMsg = clearOneResult(source, opts.offload, "cc");
240
+ const clearedMsg = clearOneResult(source, opts.offload);
264
241
  return { key: at.key, groupCount: at.groupCount, marker: clearedMsg.content[0]?.text ?? CC_CLEARED_MARKER, fp: at.fp };
265
242
  });
266
243
  return { cleared, tokensSavedEstimate };
@@ -1,7 +1,46 @@
1
+ /**
2
+ * The ONE resolution of a call's EFFECTIVE PATH TARGET — the absolute path a path-targeting tool will
3
+ * actually open — derived from the tool's OWN declaration ({@link ToolCallPathTarget}'s `base` / `absent` /
4
+ * `patternParam`) rather than guessed, separately, by every fence.
5
+ *
6
+ * ## Why this module exists
7
+ *
8
+ * The permission-rule lanes resolved a call's path with one hard-coded rule (`liveCwd ?? root` for a
9
+ * relative spelling, "no verdict" for an absent slot) while each tool resolved its own target its own way.
10
+ * The three shapes that rule got wrong were all silent — a rule that names the very directory the tool
11
+ * reads simply never fires:
12
+ *
13
+ * · Grep/Glob with NO `path` search the whole ROOT; the lanes read the absent slot as "no path", so a
14
+ * deny on the root reached nothing;
15
+ * · Glob with an ABSOLUTE `pattern` and no `path` searches the base the pattern carries
16
+ * (`/proj/secret/**` ⇒ `/proj/secret`); the lanes never looked at `pattern`;
17
+ * · Grep/Glob resolve a RELATIVE `path` against the ROOT (their describes say so, and both tools hand
18
+ * `rootCanonical` to the resolver), while the lanes resolved it against the live `cd` — so after a
19
+ * `cd` into a sibling directory the lane judged a path the tool would never open.
20
+ *
21
+ * The fix is not a fourth arm in the lanes: it is that "which base does this tool resolve against, and what
22
+ * does it target when the slot is empty" is a FACT OF THE TOOL, so the tool declares it once and both
23
+ * sides read the declaration. {@link pathTargetBaseOf} is the shared SELECTION (which base), and each side
24
+ * then applies its own identity to the selected base — lexical-normal here (the rule family's one path
25
+ * identity, zero IO), canonical (symlink-resolving, IO) inside the tool.
26
+ *
27
+ * ## What it does NOT decide
28
+ *
29
+ * Nothing here judges anything: it answers "which absolute path is this call about". Containment, the
30
+ * sensitive-path deny face and the rule precedence walk are unchanged and still run where they ran.
31
+ * `undefined` means "this call has no path this reading can locate" — the documented fail-toward-asking
32
+ * posture of both lanes (no refusal on the tightening side, no grant on the allow side).
33
+ *
34
+ * RepoMap shares the search tools' root-relative `path` semantics but declares NO path target at all (no
35
+ * rule family reaches it, and the rule grammar is derived from the catalog's path targets), so it has no
36
+ * declaration to read; the day it declares one, it reads the same selector as everything else.
37
+ */
38
+ import { type PathFamily, type PathIdentity, type Win32NamespaceRefusal } from "../tools/fs/safety.js";
1
39
  import type { ToolCallPathTarget } from "./tool-face.js";
2
40
  import { type ToolCallLike } from "./tool-registry.js";
3
41
  /** The bases of ONE CALL a target resolves against: the task root, and the live tracked working directory
4
- * (absent when no tracker moved or the caller keeps none). Facts of the call, never of the tool. */
42
+ * (absent when no tracker moved or the caller keeps none). Facts of the call, never of the tool. The
43
+ * tree's FAMILY is read from them once per call ({@link pathFamilyOf}). */
5
44
  export interface ToolPathBases {
6
45
  readonly root?: string | undefined;
7
46
  readonly cwd?: string | undefined;
@@ -17,22 +56,43 @@ export declare function pathTargetBaseOf(pt: Pick<ToolCallPathTarget, "base"> |
17
56
  * design/116 批④ #8 (CC 2.1.198 RWp, bundle :332987) — decompose an ABSOLUTE glob pattern into a base
18
57
  * directory + relative pattern: cut at the last separator before the first glob metachar (`* ? [ {`); a
19
58
  * pattern with no metachar splits dirname/basename. `undefined` for a relative pattern (the caller keeps it
20
- * as-is) or when no usable base can be derived.
59
+ * as-is) or when no usable base can be derived; a namespace refusal ({@link win32NamespaceScreen}, run
60
+ * BEFORE the metachar scan so `\\?\` is a prefix, never a `?` wildcard) is answered by its word.
61
+ * Absoluteness and the separator set are the FAMILY's: on a POSIX tree `C:\\x\\*` is a relative pattern.
21
62
  *
22
63
  * Lives HERE rather than beside the search engine because two readers need the identical cut: Glob's own
23
64
  * scope resolution and the path fences that must know which directory an absolute pattern really searches.
24
65
  */
25
- export declare function splitAbsoluteGlobPattern(pattern: string): {
66
+ export declare function splitAbsoluteGlobPattern(pattern: string, family: PathFamily | undefined): {
26
67
  baseDir: string;
27
68
  relativePattern: string;
69
+ } | {
70
+ refused: Win32NamespaceRefusal;
71
+ } | undefined;
72
+ /**
73
+ * The reading of a call's path slot under ONE declaration: `path` = the absolute target's identity; `unreadable`
74
+ * = the slot spells a target but the base it needs is not a usable base of this tree (fed to TIGHTENING rows
75
+ * only, as the ask a person clears — never to the allow arm); `refused` = a namespace refusal of the
76
+ * spelling itself (a rule speaks about no such spelling: no verdict, no ask — the fs throat delivers the
77
+ * refusal with the same word); `undefined` = no target in this reading.
78
+ */
79
+ export type PathTargetReading = {
80
+ readonly kind: "path";
81
+ readonly identity: PathIdentity;
82
+ } | {
83
+ readonly kind: "unreadable";
84
+ readonly reason: string;
85
+ } | {
86
+ readonly kind: "refused";
87
+ readonly reason: Win32NamespaceRefusal;
28
88
  } | undefined;
29
89
  /**
30
- * The absolute, LEXICAL-NORMAL path this call is about under ONE reading of its path slot, or `undefined`
31
- * when that reading locates none. `pt` is the reading (design/388 B6/B17: the PROTECTIVE target for a
32
- * tightening fence, the DECLARED target for a grant) — this function never picks between the two.
90
+ * The absolute target this call is about under ONE reading of its path slot, as a {@link PathTargetReading}.
91
+ * `pt` is the reading (design/388 B6/B17: the PROTECTIVE target for a tightening fence, the DECLARED target
92
+ * for a grant) — this function never picks between the two.
33
93
  *
34
94
  * The order is the tool's own:
35
- * ① a spelling in the slot wins — absolute as spelled, relative against {@link pathTargetBaseOf}'s base;
95
+ * ① a spelling in the slot wins — fully qualified as spelled, otherwise against {@link pathTargetBaseOf}'s base;
36
96
  * ② otherwise, when the declaration says an absent slot means `none`, the call is about no path;
37
97
  * ③ otherwise a `patternParam`'s ABSOLUTE pattern supplies its own base directory;
38
98
  * ④ otherwise the base itself (the tool searches all of it).
@@ -40,4 +100,4 @@ export declare function splitAbsoluteGlobPattern(pattern: string): {
40
100
  * A base the deployment did not supply yields `undefined`: a relative spelling with nothing to resolve
41
101
  * against names no file, and claiming it names `/<spelling>` was a fabrication no fence should judge.
42
102
  */
43
- export declare function effectivePathTargetOf(call: ToolCallLike, pt: ToolCallPathTarget | undefined, bases: ToolPathBases): string | undefined;
103
+ export declare function effectivePathTargetOf(call: ToolCallLike, pt: ToolCallPathTarget | undefined, bases: ToolPathBases): PathTargetReading;
@@ -1,34 +1,26 @@
1
- import { isAbsolutePathForm } from "../tools/fs/safety.js";
2
- import { lexicalNormalAbsolutePathOf } from "./permission-rule-model.js";
1
+ import { isAbsoluteForFamily, joinForFamily, pathFamilyOf, pathIdentityOf, win32NamespaceScreen } from "../tools/fs/safety.js";
3
2
  import { pathTargetValue } from "./tool-registry.js";
4
3
  export function pathTargetBaseOf(pt, bases) {
5
4
  return (pt?.base ?? "cwd") === "root" ? bases.root : (bases.cwd ?? bases.root);
6
5
  }
7
- export function splitAbsoluteGlobPattern(pattern) {
8
- if (!isAbsolutePathForm(pattern))
6
+ export function splitAbsoluteGlobPattern(pattern, family) {
7
+ const screened = win32NamespaceScreen(pattern, family);
8
+ if (!screened.ok)
9
+ return { refused: screened.refuse };
10
+ const spelled = screened.spelled;
11
+ if (!isAbsoluteForFamily(family, spelled))
9
12
  return undefined;
10
- const m = /[*?[{]/.exec(pattern);
11
- if (!m) {
12
- const i = Math.max(pattern.lastIndexOf("/"), pattern.lastIndexOf("\\"));
13
- if (i <= 0)
14
- return undefined;
15
- return { baseDir: pattern.slice(0, i), relativePattern: pattern.slice(i + 1) };
16
- }
17
- const prefix = pattern.slice(0, m.index);
18
- const cut = Math.max(prefix.lastIndexOf("/"), prefix.lastIndexOf("\\"));
19
- if (cut === -1)
13
+ const lastSep = (s) => (family === "win" ? Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\")) : s.lastIndexOf("/"));
14
+ const m = /[*?[{]/.exec(spelled);
15
+ const cut = m ? lastSep(spelled.slice(0, m.index)) : lastSep(spelled);
16
+ if (cut <= 0 && (m === null || cut === -1))
20
17
  return undefined;
21
- let baseDir = pattern.slice(0, cut);
22
- if (baseDir === "" && cut === 0)
18
+ let baseDir = spelled.slice(0, cut);
19
+ if (baseDir === "")
23
20
  baseDir = "/";
24
21
  if (/^[A-Za-z]:$/.test(baseDir))
25
- baseDir = baseDir + pattern[cut];
26
- if (!baseDir)
27
- return undefined;
28
- return { baseDir, relativePattern: pattern.slice(cut + 1) };
29
- }
30
- function under(base, rest) {
31
- return `${base === "/" ? "" : base}/${rest}`;
22
+ baseDir = baseDir + spelled[cut];
23
+ return { baseDir, relativePattern: spelled.slice(cut + 1) };
32
24
  }
33
25
  export function effectivePathTargetOf(call, pt, bases) {
34
26
  if (pt === undefined)
@@ -41,16 +33,36 @@ export function effectivePathTargetOf(call, pt, bases) {
41
33
  return undefined;
42
34
  if (pt.patternParam !== undefined) {
43
35
  const pattern = pathTargetValue(call.args, { param: pt.patternParam });
44
- const split = pattern === undefined ? undefined : splitAbsoluteGlobPattern(pattern);
36
+ const split = pattern === undefined ? undefined : splitAbsoluteGlobPattern(pattern, pathFamilyOf(bases));
37
+ if (split !== undefined && "refused" in split)
38
+ return { kind: "refused", reason: split.refused };
45
39
  if (split !== undefined)
46
40
  return resolveSpelling(split.baseDir, declaredBase, bases);
47
41
  }
48
- return declaredBase === undefined ? undefined : lexicalNormalAbsolutePathOf(declaredBase);
42
+ if (declaredBase === undefined)
43
+ return undefined;
44
+ const family = pathFamilyOf(bases);
45
+ return isAbsoluteForFamily(family, declaredBase) ? readingOf(pathIdentityOf(declaredBase, family, bases.root ?? bases.cwd)) : unreadableBase(declaredBase);
49
46
  }
50
47
  function resolveSpelling(spelled, declaredBase, bases) {
51
- if (spelled.startsWith("/"))
52
- return lexicalNormalAbsolutePathOf(spelled);
53
- const landsOn = isAbsolutePathForm(spelled) ? pathTargetBaseOf(undefined, bases) : declaredBase;
54
- const normalBase = landsOn === undefined ? undefined : lexicalNormalAbsolutePathOf(landsOn);
55
- return normalBase === undefined ? undefined : lexicalNormalAbsolutePathOf(under(normalBase, spelled));
48
+ const family = pathFamilyOf(bases);
49
+ const screened = win32NamespaceScreen(spelled, family);
50
+ if (!screened.ok)
51
+ return { kind: "refused", reason: screened.refuse };
52
+ const tree = bases.root ?? bases.cwd;
53
+ if (isAbsoluteForFamily(family, screened.spelled))
54
+ return readingOf(pathIdentityOf(screened.spelled, family, tree));
55
+ if (declaredBase === undefined || family === undefined)
56
+ return undefined;
57
+ if (!isAbsoluteForFamily(family, declaredBase))
58
+ return unreadableBase(declaredBase);
59
+ return readingOf(pathIdentityOf(joinForFamily(family, declaredBase, screened.spelled), family, tree));
60
+ }
61
+ function readingOf(r) {
62
+ if (r.ok)
63
+ return { kind: "path", identity: r.identity };
64
+ return r.reason === "relative" || r.reason === "unanchored" ? undefined : { kind: "refused", reason: r.reason };
65
+ }
66
+ function unreadableBase(base) {
67
+ return { kind: "unreadable", reason: `the base this call resolves against (${JSON.stringify(base)}) is not a fully-qualified path of this tree's family` };
56
68
  }