@sema-agent/core 5.65.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +18 -0
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +2 -1
  33. package/dist/core/governance-codes.js +14 -0
  34. package/dist/core/hooks.d.ts +39 -7
  35. package/dist/core/hooks.js +38 -21
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +3 -0
  45. package/dist/core/memory-engine/engine.d.ts +328 -15
  46. package/dist/core/memory-engine/engine.js +355 -29
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +64 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +20 -0
  65. package/dist/core/permission-rule-consent.js +12 -3
  66. package/dist/core/permission-rule-model.d.ts +67 -7
  67. package/dist/core/permission-rule-model.js +53 -7
  68. package/dist/core/permission-rule-store.js +15 -10
  69. package/dist/core/permission-rule-sync.js +15 -11
  70. package/dist/core/retention-policy.d.ts +9 -0
  71. package/dist/core/retention-policy.js +5 -2
  72. package/dist/core/retention.d.ts +13 -2
  73. package/dist/core/runner/assemble-result.d.ts +19 -1
  74. package/dist/core/runner/assemble-result.js +17 -2
  75. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  76. package/dist/core/runner/compaction-call-options.js +3 -0
  77. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  78. package/dist/core/runner/memory-capture-optout.js +53 -0
  79. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  80. package/dist/core/runner/prepare-config-doors.js +16 -0
  81. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  82. package/dist/core/runner/prepare-hands-readface.js +99 -7
  83. package/dist/core/runner/prepare-memory.d.ts +88 -0
  84. package/dist/core/runner/prepare-memory.js +305 -24
  85. package/dist/core/runner/prepare-task.d.ts +141 -1
  86. package/dist/core/runner/prepare-task.js +443 -79
  87. package/dist/core/runner/runtask.d.ts +9 -20
  88. package/dist/core/runner/runtask.js +133 -96
  89. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  90. package/dist/core/runner/session-file-state-replay.js +52 -1
  91. package/dist/core/runner/tool-disclosure.js +2 -1
  92. package/dist/core/runner/turn-attachments.d.ts +22 -12
  93. package/dist/core/session-store.d.ts +1 -1
  94. package/dist/core/session-store.js +6 -1
  95. package/dist/core/session.d.ts +34 -1
  96. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  97. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  98. package/dist/core/task-registry-shared.js +11 -1
  99. package/dist/core/tool-errors.js +1 -0
  100. package/dist/core/tool-policy.d.ts +172 -1
  101. package/dist/core/tool-policy.js +32 -1
  102. package/dist/core/tool-result-store.js +2 -1
  103. package/dist/core/trace.d.ts +24 -0
  104. package/dist/core/types.d.ts +784 -89
  105. package/dist/core/types.js +4 -3
  106. package/dist/core/untrusted-text.d.ts +1 -1
  107. package/dist/core/untrusted-text.js +8 -0
  108. package/dist/core/workflow-run-store-contract.js +6 -2
  109. package/dist/core/workflow-run-store.d.ts +4 -1
  110. package/dist/engine/compaction/compaction.d.ts +88 -10
  111. package/dist/engine/compaction/compaction.js +109 -30
  112. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  113. package/dist/engine/execution-env/node-execution-env.js +28 -0
  114. package/dist/engine/harness/agent-harness.d.ts +52 -1
  115. package/dist/engine/harness/agent-harness.js +36 -1
  116. package/dist/engine/harness/types.d.ts +26 -1
  117. package/dist/engine/llm/types.d.ts +50 -4
  118. package/dist/engine/loop/agent-loop.d.ts +5 -1
  119. package/dist/engine/loop/agent-loop.js +25 -0
  120. package/dist/engine/loop/types.d.ts +19 -0
  121. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  122. package/dist/engine/session/session.js +1 -1
  123. package/dist/index.d.ts +18 -8
  124. package/dist/index.js +14 -6
  125. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  126. package/dist/orchestration/run-workflow-tool.js +22 -3
  127. package/dist/orchestration/workflow-governance.d.ts +59 -1
  128. package/dist/orchestration/workflow-governance.js +61 -8
  129. package/dist/orchestration/workflow-meta.d.ts +4 -2
  130. package/dist/orchestration/workflow-primitives.js +56 -13
  131. package/dist/orchestration/workflow-types.d.ts +78 -2
  132. package/dist/orchestration/workflow.d.ts +20 -0
  133. package/dist/orchestration/workflow.js +163 -14
  134. package/dist/prompt-assembly/event-registry.js +1 -1
  135. package/dist/prompts/default.d.ts +7 -7
  136. package/dist/stores/file/file-history-store.d.ts +368 -0
  137. package/dist/stores/file/file-history-store.js +1248 -0
  138. package/dist/stores/file/index.d.ts +22 -13
  139. package/dist/stores/file/index.js +4 -4
  140. package/dist/stores/file/permission-rule-store.js +1 -0
  141. package/dist/stores/file/strategy-store.d.ts +3 -3
  142. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  143. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  144. package/dist/tools/fs/fs-bash.js +9 -5
  145. package/dist/tools/fs/fs-shared.d.ts +52 -1
  146. package/dist/tools/fs/fs-shared.js +14 -0
  147. package/dist/tools/fs/fs-write.d.ts +5 -5
  148. package/dist/tools/fs/fs-write.js +71 -14
  149. package/dist/tools/fs/index.d.ts +6 -1
  150. package/dist/tools/fs/index.js +1 -1
  151. package/dist/tools/web.js +2 -1
  152. package/package.json +5 -1
  153. package/test/export-surface.snapshot.json +155 -23
  154. package/dist/core/file-snapshot-store.d.ts +0 -165
  155. package/dist/core/file-snapshot-store.js +0 -259
  156. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  157. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  158. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  159. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
2
+ import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, translateImportedWildcardRule, } from "./permission-rule-model.js";
3
3
  import { errText, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
4
4
  export class InMemoryRuleApprovalRecordStore {
5
5
  rows = new Map();
@@ -538,6 +538,7 @@ export async function prepareCcImport(opts) {
538
538
  const owner = resolveCallerOwner(opts.principal, opts.owner, "prepareCcImport");
539
539
  const candidates = [];
540
540
  const skipped = [];
541
+ const translated = [];
541
542
  const layers = [];
542
543
  const denyAskBuckets = { deny: 0, ask: 0 };
543
544
  for (const layer of opts.layers) {
@@ -614,17 +615,25 @@ export async function prepareCcImport(opts) {
614
615
  skipped.push({ rule: entry, reason });
615
616
  continue;
616
617
  }
617
- const parsed = parseAllowRuleText(entry);
618
+ const wildcard = translateImportedWildcardRule(entry);
619
+ if (wildcard !== undefined && "skip" in wildcard) {
620
+ skipped.push({ rule: entry, reason: wildcard.skip });
621
+ continue;
622
+ }
623
+ const text = wildcard?.rule ?? entry;
624
+ const parsed = parseAllowRuleText(text);
618
625
  if ("reject" in parsed) {
619
626
  skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
620
627
  continue;
621
628
  }
622
629
  if (candidates.some((c) => c.rule === parsed.rule.rule && sameScope(c.scope, scope)))
623
630
  continue;
631
+ if (wildcard !== undefined)
632
+ translated.push({ from: entry, to: parsed.rule.rule, scope });
624
633
  candidates.push({ rule: parsed.rule.rule, scope });
625
634
  }
626
635
  }
627
- const preview = { candidates, skipped, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } };
636
+ const preview = { candidates, skipped, translated, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } };
628
637
  if (candidates.length === 0)
629
638
  return { preview };
630
639
  const record = {
@@ -308,7 +308,7 @@ export declare function escapeForDisclosure(value: unknown): string;
308
308
  * transform is LOSSY and NOT injective (two distinct commands can render identically — that is the
309
309
  * price of a readable line). Correlate, match, store and adjudicate on the RAW bytes, which the
310
310
  * carrying seats keep unchanged: this function neither changes what those seats contain nor what any
311
- * of them means. A caller that must instead tell two hostile texts APART wants
311
+ * of them means. A caller that must instead tell two adversarial texts APART wants
312
312
  * {@link escapeForDisclosure} (which escapes rather than folds, so nothing collides) or
313
313
  * {@link hasUnrenderableCharacters} (refuse instead of render).
314
314
  *
@@ -348,6 +348,47 @@ export declare function parseAllowRuleText(text: string, opts?: {
348
348
  };
349
349
  /** The canonical spelling of a rule for a (already folded) command and match form. */
350
350
  export declare function formatAllowRuleText(command: string, match: PersistedRuleMatch): string;
351
+ /**
352
+ * #490 件4 — read ANOTHER product's `Bash(<body> *)` SPACE-STAR spelling as this lane's colon-star
353
+ * prefix rule, for the IMPORT door only.
354
+ *
355
+ * Why a translation is owed at all: the other product mints its prefix suggestions in that spelling
356
+ * (its own classifier reads `X:*` as its earlier prefix form and anything else containing an unescaped star as
357
+ * a wildcard), so a person migrating settings had EVERY prefix rule they had ever approved refused as
358
+ * `unsupported.wildcard` — the import silently emptied out exactly the rules that were the point of it.
359
+ *
360
+ * Translated ONLY for the ONE spelling whose semantics this lane can state exactly: content ending in
361
+ * ` *` with that star the only unescaped one. Upstream compiles a single trailing ` *` into an
362
+ * OPTIONAL suffix (`^body( .*)?$`), which is precisely "the body, or the body followed by a space and
363
+ * more" — this lane's prefix predicate. Any other star placement compiles to a real glob with no rule
364
+ * form here, and a bare `*` is a whole-tool grant that must never become a command rule; both are
365
+ * refused with a reason of their own so the import can DISCLOSE them rather than drop them.
366
+ *
367
+ * WHAT THE TRANSLATED RULE GRANTS, against the source spelling, axis by axis (the direction matters,
368
+ * and it is one-way with one named exception): identical on the bare body, on body-plus-arguments, on
369
+ * the word boundary (`npm run` never reaches `npm run-x`) and on compounds (both sides refuse a chain
370
+ * against a single-command body). NARROWER here on four axes, because upstream normalizes the
371
+ * CANDIDATE before matching and this lane never does: a candidate carrying a redirection, wrapped in
372
+ * `timeout`/`nice`/`nohup`-style words, fed through `xargs`, or prefixed with a `VAR=value` assignment
373
+ * matches there and matches nothing here. WIDER on exactly one: this lane folds runs of whitespace on
374
+ * both sides, so `npm run build` matches the imported rule where upstream's wildcard arm (which does
375
+ * not fold) would refuse it. That axis is NOT introduced by this translation — whitespace folding is
376
+ * this lane's universal reading and already applies to every imported EXACT rule, against an upstream
377
+ * exact arm that is byte-strict — so it is disclosed as a lane property rather than treated as a
378
+ * translation defect. Every other difference runs toward granting less.
379
+ *
380
+ * Returns `undefined` when the text is not a Bash wildcard spelling at all (nothing to translate; the
381
+ * caller proceeds with the text as written), `{ rule }` with the spelling to parse INSTEAD, or
382
+ * `{ skip }` with the reason a star spelling has no form here. Never bypasses the validator: the
383
+ * returned text goes through {@link parseAllowRuleText} exactly like a hand-written one, so the
384
+ * interpreter-head refusal, the control-character screen and the length bound all still apply — a
385
+ * translated `Bash(node *)` is refused `invalid.bare_interpreter_prefix` just as a typed one is.
386
+ */
387
+ export declare function translateImportedWildcardRule(text: string): {
388
+ rule: string;
389
+ } | {
390
+ skip: string;
391
+ } | undefined;
351
392
  /**
352
393
  * Does this rule's command pattern admit `command`?
353
394
  *
@@ -477,9 +518,20 @@ export declare function findAdmittingRule(rules: readonly PersistedAllowRule[],
477
518
  }): readonly PersistedAllowRule[] | undefined;
478
519
  /**
479
520
  * design/375 §5.2 — the per-segment coverage table for `command`: which segments an eligible rule
480
- * already admits, judged with the SAME split, the same MATCH reading and the same eligibility
481
- * predicates as the gate's conjunction arm — one instrument, so the suggestion face and the consent
482
- * prepare can never call a segment covered that the gate would ask about (or vice versa).
521
+ * already admits, judged with the SAME split and the same eligibility predicates as the gate's
522
+ * conjunction arm — one instrument, so the suggestion face and the consent prepare can never call a
523
+ * segment covered that the gate would ask about (or vice versa).
524
+ *
525
+ * The split is the OFFER reading ({@link OFFER_READING}, #490 修①), which is the MATCH reading on
526
+ * every axis but where a redirection is refused — this table exists to align, row for row, with
527
+ * {@link suggestRulesForCommand}'s own segmentation, and a table that stopped one command short of the
528
+ * face it aligns with is a table that face discards (its positional belt), leaving every segment
529
+ * reading UNCOVERED. Each ROW's verdict is still the matcher's, unchanged: `covered` is
530
+ * {@link ruleAdmitsCommand} under the MATCH reading, so a segment carrying a redirection is admitted
531
+ * by nothing and reports `false`. The gate's own conjunction arm keeps the whole-string floor and is
532
+ * not consulted here: for a redirection-bearing compound it has no segments at all, and this table
533
+ * saying "an eligible rule admits `npm test`" is a true statement about that segment either way — it
534
+ * is read only to keep an already-covered segment out of a batch offer, never as an admission.
483
535
  *
484
536
  * TWO SOURCES, two predicates, and the seat labels are the contract:
485
537
  * · `persisted` — rules already in the store, judged {@link eligiblePersisted} ∧ admit: a tombstoned
@@ -632,9 +684,17 @@ export type RuleOffer = {
632
684
  * over-offering; every offered rule still passes the validator at redemption, so an over-offer widens
633
685
  * nothing).
634
686
  *
635
- * Returns an empty array for anything the rule lane cannot speak for (redirections, substitutions,
636
- * subshells, backgrounding) — the card then simply carries no "don't ask again" option, which is the
637
- * honest answer.
687
+ * Returns an empty array for anything the rule lane cannot speak for (substitutions, subshells,
688
+ * backgrounding, an unreadable segment) — the card then simply carries no "don't ask again" option,
689
+ * which is the honest answer.
690
+ *
691
+ * A REDIRECTION is the one member of that list judged per SEGMENT rather than over the whole string
692
+ * (#490 修①, {@link OFFER_READING}): `a && b 2>/dev/null && c` offers a batch for `a` and `c` with
693
+ * `uncoveredSegments: 1`, exactly as the same compound dirtied by a glob always has. A SIMPLE command
694
+ * carrying one still offers nothing — its only segment is the dirty one — and no offer this function
695
+ * mints ever admits the redirection-bearing text: the minting grammar and the matcher keep the
696
+ * whole-string floor, so the compound stays un-admitted and the honest `uncoveredSegments` count says
697
+ * so (design/382 §3.2: neither a sanitized rule text nor a widened matcher is on the table).
638
698
  */
639
699
  export declare function suggestRulesForCommand(command: string, ctx?: {
640
700
  readonly coverage?: readonly SegmentCoverage[];
@@ -1,5 +1,5 @@
1
1
  import { parsePermissionRule } from "./permission-rules.js";
2
- import { parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
2
+ import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
3
3
  import { inlineUntrusted } from "./untrusted-text.js";
4
4
  export const MAX_RULE_TEXT_CHARS = 512;
5
5
  export const BARE_INTERPRETER_NAMES = new Set([
@@ -117,17 +117,22 @@ function reject(code, message) {
117
117
  return { reject: { code, message } };
118
118
  }
119
119
  const RULE_LANE_FLOOR = { pathPrefixedNameIsText: true };
120
- const MATCH_READING = { terminator: "keep", quotedOperatorsAreText: false };
121
- const PROGRAM_RUNS_READING = { terminator: "strip", quotedOperatorsAreText: true };
120
+ const MATCH_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "reject" };
121
+ const PROGRAM_RUNS_READING = { terminator: "strip", quotedOperatorsAreText: true, redirection: "reject" };
122
+ const OFFER_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "tolerate" };
122
123
  function commandBasename(name) {
123
124
  return name.slice(name.lastIndexOf("/") + 1);
124
125
  }
125
126
  function ruleLaneShapeOf(command, reading) {
126
- const split = splitShellCompoundSegments(command, { trailingTerminator: reading.terminator });
127
+ const split = splitShellCompoundSegments(command, { trailingTerminator: reading.terminator, redirection: reading.redirection });
127
128
  if ("reject" in split)
128
129
  return { reject: split.reject };
129
130
  const names = [];
130
131
  for (const segment of split.segments) {
132
+ if (reading.redirection === "tolerate" && carriesShellRedirection(segment)) {
133
+ names.push(undefined);
134
+ continue;
135
+ }
131
136
  const floor = parseLeadingCommandName(segment, { ...RULE_LANE_FLOOR, quotedOperatorsAreText: reading.quotedOperatorsAreText });
132
137
  if ("reject" in floor) {
133
138
  return {
@@ -219,6 +224,45 @@ export function parseAllowRuleText(text, opts) {
219
224
  export function formatAllowRuleText(command, match) {
220
225
  return `Bash(${command}${match === "prefix" ? ":*" : ""})`;
221
226
  }
227
+ function hasUnescapedStar(s) {
228
+ for (let i = 0; i < s.length; i++) {
229
+ if (s[i] !== "*")
230
+ continue;
231
+ let backslashes = 0;
232
+ for (let j = i - 1; j >= 0 && s[j] === "\\"; j--)
233
+ backslashes++;
234
+ if (backslashes % 2 === 0)
235
+ return true;
236
+ }
237
+ return false;
238
+ }
239
+ function isImportedWildcardContent(content) {
240
+ return !content.endsWith(":*") && hasUnescapedStar(content);
241
+ }
242
+ export function translateImportedWildcardRule(text) {
243
+ const parsed = parsePermissionRule(text);
244
+ const content = parsed.ruleContent;
245
+ if (content === undefined || parsed.toolName !== "Bash" || !isImportedWildcardContent(content))
246
+ return undefined;
247
+ const body = /^(.+) \*$/.exec(content)?.[1];
248
+ if (body === undefined || body.trim() === "") {
249
+ return {
250
+ skip: "unsupported.wildcard: only the trailing `<command> *` form has a prefix-rule equivalent here — it stays in the settings file, unimported",
251
+ };
252
+ }
253
+ if (hasUnescapedStar(body)) {
254
+ return {
255
+ skip: "unsupported.wildcard: a `*` before the end of the pattern matches anything at that position, which has no rule form in this version — it stays in the settings file, unimported",
256
+ };
257
+ }
258
+ const bodyShape = ruleLaneShapeOf(body, MATCH_READING);
259
+ if (!("reject" in bodyShape) && bodyShape.segments.length > 1) {
260
+ return {
261
+ skip: "unsupported.wildcard: a `<chain> *` pattern names more than one command, and the trailing-star form has no equivalent for a chain — it stays in the settings file, unimported",
262
+ };
263
+ }
264
+ return { rule: formatAllowRuleText(body, "prefix") };
265
+ }
222
266
  export function ruleAdmitsCommand(rule, command) {
223
267
  return admitsUnder(rule, command, MATCH_READING);
224
268
  }
@@ -297,7 +341,7 @@ export function segmentCoverageOf(command, rules, call) {
297
341
  const folded = foldSpacing(command);
298
342
  if (folded === undefined)
299
343
  return undefined;
300
- const shape = ruleLaneShapeOf(folded, MATCH_READING);
344
+ const shape = ruleLaneShapeOf(folded, OFFER_READING);
301
345
  if ("reject" in shape)
302
346
  return undefined;
303
347
  const proposed = rules.proposed ?? [];
@@ -308,7 +352,7 @@ export function segmentCoverageOf(command, rules, call) {
308
352
  }));
309
353
  }
310
354
  export function suggestRulesForCommand(command, ctx) {
311
- const shape = ruleLaneShapeOf(command, MATCH_READING);
355
+ const shape = ruleLaneShapeOf(command, OFFER_READING);
312
356
  if ("reject" in shape)
313
357
  return [];
314
358
  const folded = foldSpacing(command);
@@ -333,7 +377,7 @@ export function suggestRulesForCommand(command, ctx) {
333
377
  }
334
378
  return offers;
335
379
  }
336
- const foldedShape = ruleLaneShapeOf(folded, MATCH_READING);
380
+ const foldedShape = ruleLaneShapeOf(folded, OFFER_READING);
337
381
  if ("reject" in foldedShape || foldedShape.segments.length !== shape.segments.length)
338
382
  return offers;
339
383
  const segments = foldedShape.segments.map((s) => s.trim());
@@ -341,6 +385,8 @@ export function suggestRulesForCommand(command, ctx) {
341
385
  const alignedTable = table !== undefined && table.length === segments.length && table.every((row, i) => row.segment === segments[i]) ? table : undefined;
342
386
  const coveredAt = (i) => alignedTable?.[i]?.covered === true;
343
387
  const mintSegmentRule = (segment) => {
388
+ if (carriesShellRedirection(segment))
389
+ return undefined;
344
390
  const body = bodyOf(tokensOf(segment));
345
391
  if (body !== undefined) {
346
392
  const parsed = parseAllowRuleText(formatAllowRuleText(body, "prefix"));
@@ -312,7 +312,7 @@ export function applySyncJoin(cur, delta, nowMs) {
312
312
  quarantined,
313
313
  observedVector: delta.observedVector !== undefined ? joinFrontiers(cur.observedVector ?? {}, delta.observedVector) : cur.observedVector,
314
314
  },
315
- report: { droppedInbound, quarantined: quarantinedNow, collectedTombstones, withheldAdds },
315
+ report: structuredClone({ droppedInbound, quarantined: quarantinedNow, collectedTombstones, withheldAdds }),
316
316
  };
317
317
  }
318
318
  export function assertRedemptionNotQuarantined(quarantined, delta) {
@@ -327,11 +327,15 @@ export async function ruleStoreChecksum(payload) {
327
327
  }
328
328
  const REMOVE_MAX_ATTEMPTS = 8;
329
329
  export async function removePersistedRule(opts) {
330
- const owner = typeof opts.principal === "string" ? { kind: "principal", principal: opts.principal } : opts.principal;
331
- if (owner.kind === "local-owner" && opts.provider.forLocalOwner === undefined) {
330
+ const rule = opts.rule;
331
+ const scope = structuredClone(opts.scope);
332
+ const target = opts.principal;
333
+ const owner = typeof target === "string" ? { kind: "principal", principal: target } : structuredClone(target);
334
+ const provider = opts.provider;
335
+ if (owner.kind === "local-owner" && provider.forLocalOwner === undefined) {
332
336
  return { status: "failed", error: "this provider has no local-owner bucket (forLocalOwner is not implemented) — a local-owner rule cannot be removed through it" };
333
337
  }
334
- const store = owner.kind === "local-owner" ? opts.provider.forLocalOwner() : opts.provider.forPrincipal(owner.principal);
338
+ const store = owner.kind === "local-owner" ? provider.forLocalOwner() : provider.forPrincipal(owner.principal);
335
339
  const writer = writerOf(store);
336
340
  if (writer === undefined) {
337
341
  return { status: "failed", error: "the resolved permission-rule store has no write face — rules cannot be removed through it" };
@@ -343,11 +347,11 @@ export async function removePersistedRule(opts) {
343
347
  catch (err) {
344
348
  return { status: "failed", error: `could not read the permission-rule store: ${errText(err)}` };
345
349
  }
346
- const live = snapshot.rules.filter((r) => r.rule === opts.rule && sameScope(r.scope, opts.scope));
350
+ const live = snapshot.rules.filter((r) => r.rule === rule && sameScope(r.scope, scope));
347
351
  const removedDots = live.flatMap((r) => r.adds.map((a) => a.dot));
348
352
  if (removedDots.length === 0)
349
353
  return { status: "no-op", rev: snapshot.rev };
350
- const tombstone = { rule: opts.rule, scope: opts.scope, removedDots, deletedBy: await writer.nextDot() };
354
+ const tombstone = { rule, scope, removedDots, deletedBy: await writer.nextDot() };
351
355
  let expectedRev = snapshot.rev;
352
356
  let lastError = "";
353
357
  for (let attempt = 0; attempt < REMOVE_MAX_ATTEMPTS; attempt++) {
@@ -357,7 +361,7 @@ export async function removePersistedRule(opts) {
357
361
  let stillLive = false;
358
362
  try {
359
363
  const after = await store.list();
360
- stillLive = after.rules.some((r) => r.rule === opts.rule && sameScope(r.scope, opts.scope) && isRuleLive(r));
364
+ stillLive = after.rules.some((r) => r.rule === rule && sameScope(r.scope, scope) && isRuleLive(r));
361
365
  }
362
366
  catch {
363
367
  stillLive = false;
@@ -376,7 +380,7 @@ export async function removePersistedRule(opts) {
376
380
  const after = await store.list();
377
381
  const landed = after.tombstones.some((t) => t.deletedBy.actor === tombstone.deletedBy.actor && t.deletedBy.counter === tombstone.deletedBy.counter);
378
382
  return landed
379
- ? { status: "removed", rev: after.rev, stillLive: after.rules.some((r) => r.rule === opts.rule && sameScope(r.scope, opts.scope) && isRuleLive(r)) }
383
+ ? { status: "removed", rev: after.rev, stillLive: after.rules.some((r) => r.rule === rule && sameScope(r.scope, scope) && isRuleLive(r)) }
380
384
  : { status: "failed", error: `${reason} (read-back confirms no tombstone was written)` };
381
385
  }
382
386
  catch (err) {
@@ -407,8 +411,8 @@ export class InMemoryPermissionRuleStore {
407
411
  }
408
412
  async list() {
409
413
  return {
410
- rules: applyTombstones(this.rules, this.tombstones).map((r) => ({ ...r, adds: r.adds.map((a) => ({ ...a })) })),
411
- tombstones: this.tombstones.map((t) => ({ ...t })),
414
+ rules: structuredClone(applyTombstones(this.rules, this.tombstones)),
415
+ tombstones: structuredClone(this.tombstones),
412
416
  rev: this.rev,
413
417
  };
414
418
  }
@@ -427,6 +431,7 @@ export class InMemoryPermissionRuleStore {
427
431
  quarantined: this.quarantinedRows,
428
432
  }),
429
433
  apply: async (delta, opts) => {
434
+ delta = structuredClone(delta);
430
435
  if (opts.expectedRev !== this.rev)
431
436
  return { conflict: true, rev: this.rev };
432
437
  if (delta.kind === "redemption-add") {
@@ -11,14 +11,18 @@ function refuseLocalOwnerSync() {
11
11
  const SYNC_MAX_ATTEMPTS = 8;
12
12
  const DISCLOSED_REFUSED_TOMBSTONES = 5;
13
13
  export async function syncPermissionRules(opts) {
14
- if (opts.owner !== undefined) {
15
- if (opts.owner.kind === "local-owner")
14
+ const principal = opts.principal;
15
+ const owner = opts.owner;
16
+ const provider = opts.provider;
17
+ const transport = opts.transport;
18
+ if (owner !== undefined) {
19
+ if (owner.kind === "local-owner")
16
20
  refuseLocalOwnerSync();
17
- if (opts.owner.principal !== opts.principal) {
18
- throw new Error(`contradictory identity: principal "${escapeForDisclosure(opts.principal)}" and owner principal "${escapeForDisclosure(opts.owner.principal)}" disagree`);
21
+ if (owner.principal !== principal) {
22
+ throw new Error(`contradictory identity: principal "${escapeForDisclosure(principal)}" and owner principal "${escapeForDisclosure(owner.principal)}" disagree`);
19
23
  }
20
24
  }
21
- if (typeof opts.principal !== "string" || opts.principal === "") {
25
+ if (typeof principal !== "string" || principal === "") {
22
26
  throw new Error("syncPermissionRules requires a verified principal — an unauthenticated deployment has no cloud bucket to sync");
23
27
  }
24
28
  const traceClock = opts.now ?? Date.now;
@@ -30,7 +34,7 @@ export async function syncPermissionRules(opts) {
30
34
  emitTrace(tracer, () => ({
31
35
  kind: "permission.rule_sync_resurrected",
32
36
  version: 1,
33
- principal: opts.principal,
37
+ principal,
34
38
  rule: r.rule,
35
39
  scopeKind: r.scope.kind,
36
40
  ts: traceClock(),
@@ -40,7 +44,7 @@ export async function syncPermissionRules(opts) {
40
44
  emitTrace(tracer, () => ({
41
45
  kind: "permission.rule_sync_dropped",
42
46
  version: 1,
43
- principal: opts.principal,
47
+ principal,
44
48
  rule: d.rule,
45
49
  scopeKind: d.scope.kind,
46
50
  reason: d.reason,
@@ -49,20 +53,20 @@ export async function syncPermissionRules(opts) {
49
53
  }
50
54
  return result;
51
55
  };
52
- const store = opts.provider.forPrincipal(opts.principal);
56
+ const store = provider.forPrincipal(principal);
53
57
  const writer = writerOf(store);
54
58
  if (writer === undefined) {
55
59
  throw new Error("the resolved permission-rule store has no write face — a sync round cannot land through it");
56
60
  }
57
61
  const raw = await writer.readRaw();
58
62
  const request = {
59
- principal: opts.principal,
63
+ principal,
60
64
  replica: raw.actor,
61
65
  state: { rules: raw.rules, tombstones: raw.tombstones },
62
66
  ...(raw.observedVector !== undefined ? { observedVector: raw.observedVector } : {}),
63
67
  };
64
- const rawResponse = await opts.transport(PERMISSION_RULE_SYNC_PATH, request);
65
- const response = parseRuleSyncResponse(rawResponse, opts.principal);
68
+ const rawResponse = await transport(PERMISSION_RULE_SYNC_PATH, request);
69
+ const response = parseRuleSyncResponse(rawResponse, principal);
66
70
  const warnings = [...response.warnings];
67
71
  const dropped = [];
68
72
  const inboundTombstones = [];
@@ -31,6 +31,15 @@ export interface RetentionPolicyKnobs {
31
31
  /** Row-count bound, whole rows (roster's write-time cap). */
32
32
  maxEntries?: number;
33
33
  }
34
+ /**
35
+ * The family's typed refusal, exported so a seam whose bound has its own ADMISSIBLE RANGE refuses in
36
+ * the same grammar under the same code instead of minting a second one. The one in-tree case:
37
+ * file-history's boundary cap requires `>= 1` where the row stores accept `0` ("keep no rows" is a
38
+ * coherent policy for a ledger; for a history whose every commit mints a boundary it is not — the
39
+ * cap would reap the boundary its own commit just published), so that door cannot reuse
40
+ * {@link assertRetentionPolicy}'s "0 or more" wording without telling an operator something false.
41
+ */
42
+ export declare function refuseRetentionPolicy(label: string, knob: string, value: unknown, requirement: string): never;
34
43
  /**
35
44
  * Refuse a malformed retention policy at the door. `undefined` on any field keeps its documented
36
45
  * meaning — that arm is simply not applied — so a deployment that sets nothing is untouched.
@@ -1,8 +1,11 @@
1
- function invalidPolicy(label, knob, value, requirement) {
2
- const e = new Error(`${label}: ${knob} must be ${requirement} (got ${String(value)}) — a retention bound that cannot be evaluated silently decides what to delete instead of bounding it`);
1
+ export function refuseRetentionPolicy(label, knob, value, requirement) {
2
+ const e = new Error(`${label}: ${knob} must be ${requirement} (got ${typeof value === "string" ? JSON.stringify(value) : String(value)}) — a retention bound that cannot be evaluated silently decides what to delete instead of bounding it`);
3
3
  e.code = "config.retention_policy_invalid";
4
4
  throw e;
5
5
  }
6
+ function invalidPolicy(label, knob, value, requirement) {
7
+ refuseRetentionPolicy(label, knob, value, requirement);
8
+ }
6
9
  export function assertRetentionPolicy(label, opts) {
7
10
  if (opts === undefined)
8
11
  return;
@@ -27,7 +27,12 @@ export interface RetentionPolicy {
27
27
  * - **idempotent**: a retried invocation after a crash re-converges (already-deleted rows are
28
28
  * counted, not errors);
29
29
  * - **tombstoned**: deletions leave tombstones so replicas/backups converge instead of resurrecting;
30
- * - **audited**: every destructive call returns a receipt the scheduler persists (append-only).
30
+ * - **audited**: every destructive call is recorded append-only. The IDENTITY of that record —
31
+ * which occurrence, which of the three operations, at what wall clock — belongs to the side that
32
+ * WRITES the row, not to the receipt: an implementation records it in the same transaction as the
33
+ * deletion (recording after the delete would leave an unwitnessed delete on a crash), and it is
34
+ * the only side that knows its own op, sweep round, and policy generation. The receipt below is
35
+ * therefore the CALL's three counts, deliberately not an audit row.
31
36
  */
32
37
  export interface ManagedRetentionCapability {
33
38
  /** Enumerate the retention domains (tenant/scope keys) this store holds data for. */
@@ -54,7 +59,13 @@ export interface ManagedRetentionCapability {
54
59
  cutoffMs: number;
55
60
  }): Promise<RetentionReceipt>;
56
61
  }
57
- /** Audit receipt of one destructive retention call (append-only material for the scheduler). */
62
+ /**
63
+ * The three counts one destructive retention call produced. NOT an audit row and deliberately
64
+ * identity-free (see the capability's `audited` clause): a scheduler that needs to tell two
65
+ * same-shaped calls apart reads its store's audit records, which the implementation writes with its
66
+ * own occurrence id, operation, and timestamp inside the deleting transaction. Adding those here
67
+ * would ask a return value to know its own call context.
68
+ */
58
69
  export interface RetentionReceipt {
59
70
  domain: string;
60
71
  /** Rows destroyed by THIS call (idempotent retries count already-gone rows as 0, not errors). */
@@ -100,6 +100,11 @@ export type BudgetAxis = "tokens" | "cost";
100
100
  */
101
101
  export interface ResultFlags {
102
102
  threw: unknown;
103
+ /** #499 — the run's ENGINE-minted identity (`Prepared.runId`), echoed verbatim on
104
+ * `TaskResult.runId`. Pure pass-through on every terminal: assembly neither mints nor derives it
105
+ * (a caller reaching assembly at all has completed prepare, so the value exists), and a
106
+ * hand-constructed flags bag without one simply produces a result without one. */
107
+ runId?: string;
103
108
  /** design/99 MF-25: the EFFECTIVE (resolved) model id that served the task — echoed on `TaskResult.model`. */
104
109
  model?: string;
105
110
  /** RB-368: some spend was accounted while the serving model had NO configured price table
@@ -118,6 +123,15 @@ export interface ResultFlags {
118
123
  * path the terminal is `completed`, and this is what tells that completion apart from a natural
119
124
  * one — the model did not finish; the person stopped it and the run awaits their direction). */
120
125
  haltedOnUserRejection?: boolean;
126
+ /** design/373 (#504) — a `TaskStream.halt` (the bare user interrupt, CC Esc form) was accepted
127
+ * while the run was live; echoed on `TaskResult.haltedByUser`. Pure pass-through on every
128
+ * terminal (the sibling `haltedOnUserRejection` law: the fact is about the leg that ran) PLUS
129
+ * one status decision it owns (slot 9.7 below): a run whose ONLY stop was this halt reads
130
+ * `"completed"`, never the `!final` failure and never the aborted terminal — whether the halt
131
+ * landed before the model said anything (empty result) or cut a turn whose partial text the
132
+ * brain kept (that text IS the result). The person stopped the run, which is the verb working,
133
+ * not a defect. A REAL abort/limit that also fired still owns the terminal above it. */
134
+ userHalted?: boolean;
121
135
  /** design/174 final-round: call ids of answered-but-never-collected questions, echoed on
122
136
  * `TaskResult.strandedHumanAnswers`. Pure pass-through; empty/absent ⇒ the field is omitted. The
123
137
  * optional `onError` alert is NOT the disclosure — this mandatory result face is. */
@@ -240,7 +254,11 @@ export declare function errorCodeOf(err: unknown): string | undefined;
240
254
  * discriminant guarantees only one is ever set)
241
255
  * 9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
242
256
  * (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
243
- * normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`)
257
+ * normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
258
+ * EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`)
259
+ * 9.7 `userHalted` + (no `final` | the halt's own aborted `final`) → status `"completed"` (#504: a
260
+ * bare user halt — the verb working, not a defect; below the aborts on purpose, a real
261
+ * abort/limit that also fired owns the terminal and the halt rides as the pass-through seat)
244
262
  * 10. no `final` → status `"failed"`
245
263
  * 11. stopReason error → brain `[code]` prefix (or `"conflict"` from the storage-layer flag)
246
264
  * 12. else → `"completed"`
@@ -57,12 +57,18 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
57
57
  const result = text;
58
58
  let errorMessage;
59
59
  let errorCode;
60
+ let apiFailure;
60
61
  let blockedReason;
61
62
  let salvagedOutput;
62
63
  let checkpointToken;
63
64
  let checkpointId;
64
65
  let checkpointGate;
65
66
  let workspaceRestoreMode;
67
+ const haltOwnsAbortedFinal = flags.userHalted === true &&
68
+ final?.stopReason === "aborted" &&
69
+ !flags.abortedLive &&
70
+ flags.abortedForTimeout !== true &&
71
+ flags.abortedForTurns !== true;
66
72
  const isDegenerate = final?.stopReason === "error" && isDegenerateCutMessage(final);
67
73
  if (flags.outputInvalid) {
68
74
  status = "failed";
@@ -127,11 +133,14 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
127
133
  checkpointGate = flags.reviewRef.gate;
128
134
  workspaceRestoreMode = flags.reviewRef.restoreMode;
129
135
  }
130
- else if (flags.abortedLive || final?.stopReason === "aborted") {
136
+ else if ((flags.abortedLive || final?.stopReason === "aborted") && !haltOwnsAbortedFinal) {
131
137
  status = "failed";
132
138
  errorCode = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
133
139
  errorMessage = final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted");
134
140
  }
141
+ else if (flags.userHalted === true && (!final || haltOwnsAbortedFinal)) {
142
+ status = "completed";
143
+ }
135
144
  else if (!final) {
136
145
  status = "failed";
137
146
  errorMessage = "no assistant message produced";
@@ -139,6 +148,12 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
139
148
  else if (final.stopReason === "error") {
140
149
  status = "failed";
141
150
  errorMessage = final.errorMessage ?? "model error";
151
+ if (final.isApiErrorMessage === true) {
152
+ apiFailure = {
153
+ ...(final.apiErrorStatus !== undefined ? { status: final.apiErrorStatus } : {}),
154
+ ...(final.requestId !== undefined ? { requestId: final.requestId } : {}),
155
+ };
156
+ }
142
157
  const lifted = extractErrorCode(errorMessage);
143
158
  if (lifted) {
144
159
  errorCode = lifted;
@@ -165,5 +180,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
165
180
  void _internalCompaction;
166
181
  if (flags.unpricedSpend)
167
182
  delete publicStats.costMicroUsd;
168
- return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
183
+ return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(flags.userHalted === true ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
169
184
  }