@sema-agent/core 7.37.1 → 7.38.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 (155) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/dist/agents/observer.js +1 -1
  3. package/dist/agents/peer-notices.js +4 -4
  4. package/dist/agents/subagent.js +1 -1
  5. package/dist/agents/team-role.js +2 -2
  6. package/dist/brain/anthropic.js +10 -9
  7. package/dist/brain/errors.d.ts +26 -0
  8. package/dist/brain/errors.js +34 -0
  9. package/dist/brain/input-too-long.d.ts +0 -31
  10. package/dist/brain/input-too-long.js +1 -18
  11. package/dist/brain/open-responses.js +10 -9
  12. package/dist/brain/openai.js +10 -9
  13. package/dist/brain/status-sink.d.ts +1 -1
  14. package/dist/brain/stream-engine.js +10 -2
  15. package/dist/brain/stream-shared.d.ts +11 -3
  16. package/dist/brain/stream-shared.js +4 -2
  17. package/dist/brain/tool-call-id.d.ts +29 -8
  18. package/dist/brain/tool-call-id.js +36 -6
  19. package/dist/brain/tool-call-repair.js +1 -1
  20. package/dist/core/a2a-task-state.js +2 -2
  21. package/dist/core/artifact-host.js +6 -6
  22. package/dist/core/ask-origin.js +6 -6
  23. package/dist/core/ask-unresolvable-notice.js +1 -1
  24. package/dist/core/auto-mode.js +4 -4
  25. package/dist/core/background-agent-store.js +2 -2
  26. package/dist/core/checkpoint-execution-record.js +1 -1
  27. package/dist/core/checkpoint-store.d.ts +2 -2
  28. package/dist/core/checkpoint-store.js +2 -2
  29. package/dist/core/child-clamp-seats.js +3 -3
  30. package/dist/core/compliance.js +1 -1
  31. package/dist/core/context-edit.js +1 -1
  32. package/dist/core/context-guard.d.ts +0 -19
  33. package/dist/core/context-guard.js +0 -38
  34. package/dist/core/decision-receipt.d.ts +27 -1
  35. package/dist/core/decision-receipt.js +176 -1
  36. package/dist/core/engine-notice.d.ts +60 -8
  37. package/dist/core/engine-notice.js +2 -1
  38. package/dist/core/gate-fold.js +3 -2
  39. package/dist/core/gate-outcome.js +4 -4
  40. package/dist/core/governance-codes.d.ts +221 -13
  41. package/dist/core/governance-codes.js +104 -4
  42. package/dist/core/hook-seats.js +2 -2
  43. package/dist/core/hooks.d.ts +21 -2
  44. package/dist/core/hooks.js +4 -1
  45. package/dist/core/mailbox-store.js +3 -3
  46. package/dist/core/mcp-failure.js +4 -4
  47. package/dist/core/mcp-injection-drop.js +1 -1
  48. package/dist/core/memory-engine/consolidation.js +1 -1
  49. package/dist/core/memory-engine/delegation-settlement.d.ts +8 -0
  50. package/dist/core/memory-engine/delegation-settlement.js +7 -0
  51. package/dist/core/memory-engine/engine.d.ts +12 -2
  52. package/dist/core/memory-engine/engine.js +10 -6
  53. package/dist/core/memory-engine/export-bundle.js +1 -1
  54. package/dist/core/memory-engine/layout.d.ts +6 -6
  55. package/dist/core/memory-engine/layout.js +6 -8
  56. package/dist/core/memory-engine/session-write-home.d.ts +30 -0
  57. package/dist/core/memory-engine/session-write-home.js +34 -0
  58. package/dist/core/memory-engine/tools.js +1 -1
  59. package/dist/core/memory-engine/types.js +1 -1
  60. package/dist/core/memory.js +1 -1
  61. package/dist/core/mode-policy.d.ts +10 -1
  62. package/dist/core/mode-policy.js +15 -7
  63. package/dist/core/model-seat.js +1 -1
  64. package/dist/core/permission-rule-model.js +3 -3
  65. package/dist/core/permission-rule-store.js +2 -2
  66. package/dist/core/read-only-shell-table.js +8 -8
  67. package/dist/core/retention.js +2 -2
  68. package/dist/core/runner/assemble-result.js +15 -5
  69. package/dist/core/runner/classifier-ladder.js +1 -1
  70. package/dist/core/runner/denial-limit-arms.d.ts +2 -0
  71. package/dist/core/runner/denial-limit-arms.js +6 -2
  72. package/dist/core/runner/inherited-ask-grants.d.ts +6 -5
  73. package/dist/core/runner/inherited-ask-grants.js +3 -2
  74. package/dist/core/runner/prepare-artifact.d.ts +3 -2
  75. package/dist/core/runner/prepare-context-lane.d.ts +19 -15
  76. package/dist/core/runner/prepare-context-lane.js +11 -15
  77. package/dist/core/runner/prepare-gate-stations.js +5 -5
  78. package/dist/core/runner/prepare-hands-readface.d.ts +4 -3
  79. package/dist/core/runner/prepare-hands-readface.js +1 -0
  80. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  81. package/dist/core/runner/prepare-policy-chain.js +10 -10
  82. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -2
  83. package/dist/core/runner/run-harness-handlers.js +3 -0
  84. package/dist/core/runner/run-identity-wiring.d.ts +1 -1
  85. package/dist/core/runner/run-identity-wiring.js +0 -2
  86. package/dist/core/runner/run-leg.js +2 -2
  87. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  88. package/dist/core/runner/run-turn-boundary.js +1 -1
  89. package/dist/core/runner/session-mode-seats.d.ts +11 -3
  90. package/dist/core/runner/session-mode-seats.js +6 -0
  91. package/dist/core/runner/session-rule-policy.js +2 -3
  92. package/dist/core/runner/stream-steer-verb.js +1 -1
  93. package/dist/core/runner/tool-call-id-fold-seat.js +16 -9
  94. package/dist/core/runner/tool-turn-repair-seat.d.ts +11 -1
  95. package/dist/core/runner/tool-turn-repair-seat.js +35 -0
  96. package/dist/core/runner/turn-attachments.js +1 -1
  97. package/dist/core/runtime-caps.js +1 -1
  98. package/dist/core/safety-axis-vocab.d.ts +5 -0
  99. package/dist/core/safety-axis-vocab.js +5 -0
  100. package/dist/core/session-reconcile.d.ts +9 -7
  101. package/dist/core/session-reconcile.js +21 -1
  102. package/dist/core/shared-memory/types.js +1 -1
  103. package/dist/core/side-query.d.ts +6 -1
  104. package/dist/core/store-contracts/read-back-shape-contract.js +1 -1
  105. package/dist/core/swappable-deps.js +1 -1
  106. package/dist/core/task-notification.js +4 -4
  107. package/dist/core/task-registry-shared.js +1 -1
  108. package/dist/core/terminal-cause.d.ts +19 -0
  109. package/dist/core/tool-face.js +6 -6
  110. package/dist/core/tool-policy.d.ts +17 -11
  111. package/dist/core/tool-policy.js +38 -72
  112. package/dist/core/tool-roster.js +1 -1
  113. package/dist/core/tool-search-setting.js +1 -1
  114. package/dist/core/trace.d.ts +5 -4
  115. package/dist/core/types.d.ts +2 -2
  116. package/dist/core/types.js +1 -1
  117. package/dist/core/wiring-manifest.js +2 -2
  118. package/dist/core/write-protect.js +1 -1
  119. package/dist/engine/harness/agent-harness.js +10 -6
  120. package/dist/engine/llm/tool-turns.d.ts +93 -4
  121. package/dist/engine/llm/tool-turns.js +63 -9
  122. package/dist/engine/llm/types.d.ts +11 -0
  123. package/dist/index.d.ts +5 -3
  124. package/dist/index.js +4 -2
  125. package/dist/internal/llm.d.ts +2 -2
  126. package/dist/internal/llm.js +1 -1
  127. package/dist/orchestration/workflow-governance.js +2 -2
  128. package/dist/orchestration/workflow-types.js +2 -2
  129. package/dist/shell/lex.js +7 -7
  130. package/dist/tools/fs/bash-program-position.js +2 -2
  131. package/dist/tools/fs/fs-shared.d.ts +2 -1
  132. package/dist/tools/fs/fs-shared.js +1 -1
  133. package/dist/tools/fs/index.d.ts +13 -6
  134. package/dist/tools/fs/index.js +12 -4
  135. package/dist/tools/fs/read-deny-home.d.ts +98 -45
  136. package/dist/tools/fs/read-deny-home.js +294 -117
  137. package/dist/tools/fs/read-deny.d.ts +2 -2
  138. package/dist/tools/fs/read-deny.js +2 -2
  139. package/dist/tools/fs/safety.d.ts +2 -0
  140. package/dist/tools/fs/safety.js +2 -2
  141. package/dist/tools/fs/search.d.ts +4 -3
  142. package/dist/tools/fs/search.js +3 -3
  143. package/dist/tools/fs/shell-read-check.d.ts +9 -0
  144. package/dist/tools/fs/shell-read-check.js +3 -2
  145. package/dist/tools/fs/shell-read-establish.d.ts +3 -0
  146. package/dist/tools/fs/shell-read-establish.js +1 -1
  147. package/dist/tools/fs/shell-read-identity.d.ts +4 -3
  148. package/dist/tools/fs/shell-read-identity.js +2 -2
  149. package/dist/tools/fs/shell-read-judge.js +2 -2
  150. package/dist/tools/fs/shell-read-plan.js +2 -2
  151. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  152. package/package.json +1 -1
  153. package/test/export-surface.snapshot.json +17 -1
  154. package/wire-changes/core@7.38.0.json +664 -0
  155. package/wire-changes/core@7.38.1.json +56 -0
@@ -860,13 +860,14 @@ export type TraceEvent = {
860
860
  } | {
861
861
  /** A provider echo field was refused by {@link import("../brain/errors.js").PROVIDER_ECHO_SHAPE} (visible ASCII,
862
862
  * 1–128 characters) and dropped: the response's `request-id` / `x-request-id` header values (`field:
863
- * "requestId"`, read at connect — the id a failure would carry as `apiFailure.requestId`), or the model name
864
- * its frames report (`field: "responseModel"`). One frame per provider response per field; the value itself
865
- * is never carried (metadata-only). A count of these frames is this engine's drop count. */
863
+ * "requestId"`, read at connect — the id a failure would carry as `apiFailure.requestId`), the model name
864
+ * its frames report (`field: "responseModel"`), or the word a refusal's error envelope states (`field:
865
+ * "apiErrorCode"`). One frame per provider response per field; the value itself is never carried
866
+ * (metadata-only). A count of these frames is this engine's drop count. */
866
867
  kind: "brain.echo_dropped";
867
868
  version: 1;
868
869
  taskId: string;
869
- field: "requestId" | "responseModel";
870
+ field: "requestId" | "responseModel" | "apiErrorCode";
870
871
  ts: number;
871
872
  } | {
872
873
  /** C10 — a `preemptSignal` fired on a task that is NOT resource-suspend-eligible: the preempt is a
@@ -29,6 +29,6 @@ export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./runtime-caps.js"
29
29
  export type { BackgroundChildEvent, DelegationLifecycleEvent } from "./delegation-frames.js";
30
30
  export { __resetMalformedDelegationSeatAnnouncement, deliverDelegationLifecycle } from "./delegation-frames.js";
31
31
  export type { ProjectMemoryLoad, ResumePreflightInfo, ResumePreflightVerdict, RunnerDeps } from "./runner-deps.js";
32
- export type { EngineNotice, OwedUserInputAccount } from "./engine-notice.js";
33
- export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, settleOwedUserInput, undrainedUserInputNotices } from "./engine-notice.js";
32
+ export type { EngineNotice, OwedUserInputAccount, UserSteerUndrainedCause } from "./engine-notice.js";
33
+ export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, settleOwedUserInput, undrainedUserInputNotices, USER_STEER_UNDRAINED_CAUSES } from "./engine-notice.js";
34
34
  export type { AgentTool, Model, StreamFn, ThinkingLevel, ToolInputValidationContext, ToolInputVerdict };
@@ -1,4 +1,4 @@
1
1
  export { MODEL_ROLES } from "./model-seat.js";
2
2
  export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./runtime-caps.js";
3
3
  export { __resetMalformedDelegationSeatAnnouncement, deliverDelegationLifecycle } from "./delegation-frames.js";
4
- export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, settleOwedUserInput, undrainedUserInputNotices } from "./engine-notice.js";
4
+ export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, settleOwedUserInput, undrainedUserInputNotices, USER_STEER_UNDRAINED_CAUSES } from "./engine-notice.js";
@@ -5,8 +5,8 @@ import { canonicalize } from "./canonical-json.js";
5
5
  import { resolveCheckpointStore } from "./checkpoint-store.js";
6
6
  import { isLiveQuestionFace } from "./ask-question.js";
7
7
  import { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./runtime-caps.js";
8
- export const AUTO_MODE_ARM_REASONS = ["armed", "no_intent", "no_face", "denied", "resolver_fault"];
9
- export const LSP_LANES = ["tool", "diagnostics", "tool_and_diagnostics"];
8
+ export const AUTO_MODE_ARM_REASONS = Object.freeze(["armed", "no_intent", "no_face", "denied", "resolver_fault"]);
9
+ export const LSP_LANES = Object.freeze(["tool", "diagnostics", "tool_and_diagnostics"]);
10
10
  export function deriveHandsSeam(handsMounted) {
11
11
  return handsMounted ? { mounted: true } : { mounted: false, reason: "no_execution_env" };
12
12
  }
@@ -183,7 +183,7 @@ function relativeUnderDataRoot(path, roots) {
183
183
  }
184
184
  return null;
185
185
  }
186
- export const WRITE_PROTECTION_TARGET_VIEWS = ["spelling-only", "target"];
186
+ export const WRITE_PROTECTION_TARGET_VIEWS = Object.freeze(["spelling-only", "target"]);
187
187
  export function compileWriteProtection(entries, opts) {
188
188
  const rows = resolveWriteProtectedTable(entries);
189
189
  if (rows.length === 0)
@@ -5,7 +5,7 @@ import { normalizeEngineSegments } from "../../core/untrusted-text.js";
5
5
  import { AUTH_CARRIER_NAMES } from "../../brain/request-params.js";
6
6
  import { isProviderEchoShape } from "../../brain/errors.js";
7
7
  import { convertToLlm } from "./messages.js";
8
- import { thrownText } from "../../core/thrown-value.js";
8
+ import { readMember, thrownText } from "../../core/thrown-value.js";
9
9
  import { AgentHarnessError, CompactionError, SessionError, SYNTHETIC_MESSAGE_MODEL, toError, } from "./types.js";
10
10
  const API_FAILURE_RECORD_MAX_CHARS = 2000;
11
11
  function createUserMessage(text, images, provenance) {
@@ -784,7 +784,11 @@ export class AgentHarness {
784
784
  }
785
785
  }
786
786
  async recordApiFailure(failure) {
787
- const raw = failure.errorMessage?.trim();
787
+ const errorMessage = readMember(failure, "errorMessage");
788
+ const apiErrorStatus = readMember(failure, "apiErrorStatus");
789
+ const requestId = readMember(failure, "requestId");
790
+ const errorKind = readMember(failure, "errorKind");
791
+ const raw = typeof errorMessage === "string" ? errorMessage.trim() : undefined;
788
792
  const detail = raw !== undefined && raw.length > API_FAILURE_RECORD_MAX_CHARS
789
793
  ? `${raw.slice(0, API_FAILURE_RECORD_MAX_CHARS)}… (truncated)`
790
794
  : raw;
@@ -795,13 +799,13 @@ export class AgentHarness {
795
799
  provider: failure.provider,
796
800
  model: SYNTHETIC_MESSAGE_MODEL,
797
801
  isApiErrorMessage: true,
798
- ...(failure.apiErrorStatus !== undefined ? { apiErrorStatus: failure.apiErrorStatus } : {}),
799
- ...(typeof failure.requestId === "string" && isProviderEchoShape(failure.requestId) ? { requestId: failure.requestId } : {}),
802
+ ...(typeof apiErrorStatus === "number" ? { apiErrorStatus } : {}),
803
+ ...(typeof requestId === "string" && isProviderEchoShape(requestId) ? { requestId } : {}),
800
804
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
801
805
  usageMissing: true,
802
806
  stopReason: "error",
803
- ...(failure.errorMessage !== undefined ? { errorMessage: failure.errorMessage } : {}),
804
- ...(failure.errorKind !== undefined ? { errorKind: failure.errorKind } : {}),
807
+ ...(typeof errorMessage === "string" ? { errorMessage } : {}),
808
+ ...(errorKind === "length_empty" || errorKind === "degenerate" || errorKind === "input_too_long" ? { errorKind } : {}),
805
809
  timestamp: failure.timestamp,
806
810
  };
807
811
  try {
@@ -1,12 +1,101 @@
1
+ /**
2
+ * The request view's TOOL-TURN LAW — the structure every provider lane requires of a tool turn, restored on the
3
+ * way to the brain (never in the transcript).
4
+ *
5
+ * For every assistant message that carries tool calls:
6
+ * (i) the calls are a CONTIGUOUS TAIL of DISTINCT ids — every block that is not a call and stands after the first
7
+ * call (between two calls or after the last) moves ahead of the calls; blocks ahead of the first call, thinking
8
+ * first, keep their place (a stable partition: non-call blocks in their order, then the calls in theirs); a
9
+ * second call carrying an id the message already used is left out (every lane requires a request's call ids to
10
+ * be unique), so the message keeps the FIRST call per id and every result for that id answers it;
11
+ * (ii) the messages right after it are EXACTLY one result per call — a result recorded later in the transcript
12
+ * (behind a user message, a wake frame, another turn) is moved up into the batch, a second result for the
13
+ * same call is folded into the first (no recorded result is withheld), an interrupted close
14
+ * ({@link isInterruptedClose}) stands in only for a call with no other result (beside a recorded result it
15
+ * is void: it says only that none was recorded), and a call with no result at all is closed with the reconcile
16
+ * family's synthetic interrupted result ({@link interruptedResults}: the same text and `errorKind` a persisted
17
+ * reconcile writes);
18
+ * (iii) every result answers a call — a result with no EARLIER assistant message emitting its id (its call is gone,
19
+ * or it was recorded ahead of its call) answers nothing in this view and is left out. This is the one rule for
20
+ * that shape on every request builder (the harness's request lane and the side query alike).
21
+ *
22
+ * The three lanes carry the same requirement in three spellings — the Anthropic Messages API rejects a `tool_use`
23
+ * followed by any non-`tool_use` block (between two calls as after the last), or whose `tool_result` is not in the
24
+ * very next message; chat completions requires the `tool` messages right after the assistant's `tool_calls`; the
25
+ * Responses API requires a `function_call_output` for every `function_call` — and each brain's own builder only ever
26
+ * merged ADJACENT results. One transcript that
27
+ * breaks the structure (a result recorded late, a note written after the calls) was therefore rejected on every
28
+ * later request of its session. Holding the law here, once, at the provider boundary means the three brains read
29
+ * the canonical form, and a session already carrying a broken turn is repaired on its next request.
30
+ *
31
+ * Attribution (some gateways number calls per request, so an id legitimately recurs across turns): a result belongs
32
+ * to the NEAREST EARLIER assistant message that emitted its id — and to nothing else: its recorded tool name is not a
33
+ * key. That is the persisted reconcile's positional reading (a call is answered by a result standing after it and
34
+ * before the id's next use), so the two readers never disagree about which occurrence a result answers; a name-based
35
+ * reading (7.37.1) reached back past turns the model had already answered.
36
+ * An earlier occurrence of a reused id that has no result before the next use is therefore closed as
37
+ * interrupted (outcome unknown), never handed a later occurrence's success; every result after the last use is that
38
+ * use's (two or more are folded). A result with no earlier call carrying its id is left out (iii). Pure and
39
+ * deterministic (a moved result keeps its object; a synthesized one takes its assistant's timestamp), so the same
40
+ * transcript yields the same request bytes and the prefix cache holds.
41
+ * The close the view synthesizes carries NO tool-effect refinement: the view holds no tool table and no record of
42
+ * which calls started, so every call it closes reads the reconcile family's conservative "outcome unknown — verify"
43
+ * wording, whatever the tool. (The persisted reconcile refines with the run's tool effects — a read-only or
44
+ * idempotent call's close says it may simply be re-issued — and, on a live abort, with the calls it saw start.)
45
+ * Returns the SAME array when nothing needed restoring.
46
+ */
47
+ import type { AssertAllKeysHandled } from "../../core/ask-origin.js";
48
+ import type { AssistantMessageDiagnostic } from "./diagnostics.js";
1
49
  import type { Message } from "./types.js";
2
50
  /** What the law restored — the `forms` of a `context.tool_turn_repaired` notice. */
3
- export declare const TOOL_TURN_REPAIR_FORMS: readonly ["trailing_text", "late_result", "missing_result", "duplicate_result"];
51
+ export declare const TOOL_TURN_REPAIR_FORMS: readonly ["trailing_text", "late_result", "missing_result", "duplicate_result", "duplicate_call", "orphan_result"];
4
52
  export type ToolTurnRepairForm = (typeof TOOL_TURN_REPAIR_FORMS)[number];
5
- /** One restoration on one assistant message. */
53
+ /** Does the form leave recorded content out of the request (a call, a result), rather than move, fold or add it?
54
+ * A request that did is a lossy projection of the session — the harness never seeds a fork summary from it. */
55
+ export declare const TOOL_TURN_REPAIR_DROPS: {
56
+ readonly trailing_text: false;
57
+ readonly late_result: false;
58
+ readonly missing_result: false;
59
+ readonly duplicate_result: false;
60
+ readonly duplicate_call: true;
61
+ readonly orphan_result: true;
62
+ };
63
+ export type DropsTableCoversEveryForm = AssertAllKeysHandled<Exclude<ToolTurnRepairForm, keyof typeof TOOL_TURN_REPAIR_DROPS>>;
64
+ /**
65
+ * Why a tool turn reached the model incomplete — the `task.tool_turn_incomplete` notice's `detail.cause`, the person's
66
+ * view of facts the model is already told in the turn itself. Closed:
67
+ * · `truncated` — calls whose argument bytes were cut before they parsed: dropped, the model asked to re-issue them;
68
+ * · `unnamed` — calls that arrived with no tool name: dropped the same way;
69
+ * · `bad_frame` — provider stream frames that were not valid JSON: dropped, so the response may be missing content;
70
+ * · `missing_result_synthesized` — a call with no recorded result, closed in the request view as interrupted with its
71
+ * outcome unknown (the `missing_result` repair).
72
+ * The first three are a brain's stream facts ({@link StreamDropCause}), stamped on the turn's own message; the fourth is
73
+ * the request view's.
74
+ */
75
+ export declare const TOOL_TURN_INCOMPLETE_CAUSES: readonly ["truncated", "unnamed", "bad_frame", "missing_result_synthesized"];
76
+ export type ToolTurnIncompleteCause = (typeof TOOL_TURN_INCOMPLETE_CAUSES)[number];
77
+ /** The causes a brain's stream parse finds — what a turn's own message can report. */
78
+ export type StreamDropCause = Exclude<ToolTurnIncompleteCause, "missing_result_synthesized">;
79
+ /** One cause a turn's stream dropped content for, and how many calls (or frames) it dropped. */
80
+ export interface StreamDrop {
81
+ readonly cause: StreamDropCause;
82
+ readonly count: number;
83
+ }
84
+ /** Stamp a turn's stream drops on its message (`AssistantMessage.diagnostics`); nothing for a turn that dropped none. */
85
+ export declare function stampStreamDrops(message: {
86
+ diagnostics?: AssistantMessageDiagnostic[];
87
+ }, drops: readonly StreamDrop[]): void;
88
+ /** The stream drops a message reports. A host brain's message is not trusted to be well-formed: an entry counts only
89
+ * with a closed-set cause and a positive integer count; anything else reads as no drop. */
90
+ export declare function streamDropsOf(message: {
91
+ readonly diagnostics?: unknown;
92
+ }): StreamDrop[];
93
+ /** One restoration: on one assistant message, or — for `orphan_result` — on the view as a whole. */
6
94
  export interface ToolTurnRepair {
7
95
  readonly form: ToolTurnRepairForm;
8
- /** The calls concerned, in call order — for `trailing_text` every call of the message; otherwise the calls whose
9
- * result was moved up, synthesized, or folded with a second one. */
96
+ /** The calls concerned, in call order — for `trailing_text` every call of the message; for `duplicate_call` the
97
+ * ids a later call of the message repeated; for `orphan_result` the ids of the results left out, once each, in
98
+ * transcript order; otherwise the calls whose result was moved up, synthesized, or folded with a second one. */
10
99
  readonly toolCallIds: readonly string[];
11
100
  }
12
101
  export interface ToolTurnView {
@@ -1,5 +1,48 @@
1
1
  import { interruptedResultMessage, interruptedResults, isInterruptedClose } from "../../core/session-reconcile.js";
2
- export const TOOL_TURN_REPAIR_FORMS = ["trailing_text", "late_result", "missing_result", "duplicate_result"];
2
+ export const TOOL_TURN_REPAIR_FORMS = Object.freeze(["trailing_text", "late_result", "missing_result", "duplicate_result", "duplicate_call", "orphan_result"]);
3
+ export const TOOL_TURN_REPAIR_DROPS = {
4
+ trailing_text: false,
5
+ late_result: false,
6
+ missing_result: false,
7
+ duplicate_result: false,
8
+ duplicate_call: true,
9
+ orphan_result: true,
10
+ };
11
+ export const TOOL_TURN_INCOMPLETE_CAUSES = Object.freeze(["truncated", "unnamed", "bad_frame", "missing_result_synthesized"]);
12
+ const STREAM_DROPS_DIAGNOSTIC = "stream_drops";
13
+ const STREAM_DROP_CAUSES = new Set(["truncated", "unnamed", "bad_frame"]);
14
+ export function stampStreamDrops(message, drops) {
15
+ if (drops.length === 0)
16
+ return;
17
+ message.diagnostics = [...(message.diagnostics ?? []), { type: STREAM_DROPS_DIAGNOSTIC, timestamp: Date.now(), details: { drops: drops.map(({ cause, count }) => ({ cause, count })) } }];
18
+ }
19
+ export function streamDropsOf(message) {
20
+ const out = [];
21
+ try {
22
+ const diagnostics = message.diagnostics;
23
+ if (!Array.isArray(diagnostics))
24
+ return out;
25
+ const entries = diagnostics;
26
+ for (const entry of entries) {
27
+ if (typeof entry !== "object" || entry === null || entry.type !== STREAM_DROPS_DIAGNOSTIC)
28
+ continue;
29
+ const drops = (entry.details ?? {}).drops;
30
+ if (!Array.isArray(drops))
31
+ continue;
32
+ const items = drops;
33
+ for (const d of items) {
34
+ const cause = d?.cause;
35
+ const count = d?.count;
36
+ if (typeof cause === "string" && STREAM_DROP_CAUSES.has(cause) && typeof count === "number" && Number.isInteger(count) && count > 0)
37
+ out.push({ cause: cause, count });
38
+ }
39
+ }
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ return out;
45
+ }
3
46
  function callsOf(m) {
4
47
  return m.role === "assistant" && Array.isArray(m.content) ? m.content.filter((c) => c?.type === "toolCall") : [];
5
48
  }
@@ -10,6 +53,7 @@ export function ensureToolTurnInvariants(messages) {
10
53
  const sites = new Map();
11
54
  const ownerOf = new Map();
12
55
  const resultsOf = new Map();
56
+ const orphans = new Set();
13
57
  messages.forEach((m, i) => {
14
58
  if (m.role === "assistant") {
15
59
  for (const call of callsOf(m)) {
@@ -23,8 +67,10 @@ export function ensureToolTurnInvariants(messages) {
23
67
  if (m.role !== "toolResult")
24
68
  return;
25
69
  const all = sites.get(m.toolCallId) ?? [];
26
- if (all.length === 0)
70
+ if (all.length === 0) {
71
+ orphans.add(i);
27
72
  return;
73
+ }
28
74
  const owner = all.at(-1);
29
75
  ownerOf.set(i, owner);
30
76
  const batch = resultsOf.get(owner);
@@ -36,7 +82,7 @@ export function ensureToolTurnInvariants(messages) {
36
82
  const repairs = [];
37
83
  const out = [];
38
84
  messages.forEach((m, i) => {
39
- if (ownerOf.has(i))
85
+ if (ownerOf.has(i) || orphans.has(i))
40
86
  return;
41
87
  const calls = callsOf(m);
42
88
  if (calls.length === 0) {
@@ -44,16 +90,22 @@ export function ensureToolTurnInvariants(messages) {
44
90
  return;
45
91
  }
46
92
  const assistant = m;
47
- const firstCall = assistant.content.findIndex((c) => c?.type === "toolCall");
48
- const trails = assistant.content.slice(firstCall).some((c) => c?.type !== "toolCall");
49
- out.push(trails ? { ...assistant, content: [...assistant.content.filter((c) => c?.type !== "toolCall"), ...calls] } : assistant);
50
- if (trails)
51
- repairs.push({ form: "trailing_text", toolCallIds: calls.map((c) => c.id) });
52
93
  const byId = new Map();
53
- for (const call of calls)
94
+ const repeated = [];
95
+ for (const call of calls) {
54
96
  if (!byId.has(call.id))
55
97
  byId.set(call.id, call);
98
+ else if (!repeated.includes(call.id))
99
+ repeated.push(call.id);
100
+ }
56
101
  const ids = [...byId.keys()];
102
+ const firstCall = assistant.content.findIndex((c) => c?.type === "toolCall");
103
+ const trails = assistant.content.slice(firstCall).some((c) => c?.type !== "toolCall");
104
+ out.push(trails || repeated.length > 0 ? { ...assistant, content: [...assistant.content.filter((c) => c?.type !== "toolCall"), ...byId.values()] } : assistant);
105
+ if (trails)
106
+ repairs.push({ form: "trailing_text", toolCallIds: ids });
107
+ if (repeated.length > 0)
108
+ repairs.push({ form: "duplicate_call", toolCallIds: repeated });
57
109
  const recorded = new Map();
58
110
  for (const r of resultsOf.get(i) ?? []) {
59
111
  const id = messages[r].toolCallId;
@@ -84,6 +136,8 @@ export function ensureToolTurnInvariants(messages) {
84
136
  if (duplicate.length > 0)
85
137
  repairs.push({ form: "duplicate_result", toolCallIds: inCallOrder(duplicate) });
86
138
  });
139
+ if (orphans.size > 0)
140
+ repairs.push({ form: "orphan_result", toolCallIds: [...new Set([...orphans].map((i) => messages[i].toolCallId))] });
87
141
  return repairs.length === 0 ? { messages, repairs } : { messages: out, repairs };
88
142
  }
89
143
  function foldResults(first, second) {
@@ -434,6 +434,17 @@ export interface AssistantMessage {
434
434
  * as `0`.
435
435
  */
436
436
  apiErrorStatus?: number;
437
+ /**
438
+ * The provider's own word for a REFUSED request, read structurally from its JSON error envelope (`error.code`, else
439
+ * `error.type`) — e.g. `invalid_request_error`, `context_length_exceeded`. Only meaningful together with
440
+ * {@link isApiErrorMessage}; provider vocabulary, never an engine closed set.
441
+ *
442
+ * PRESENCE: only beside a 4xx {@link apiErrorStatus} (the provider refused the request as sent) whose envelope stated
443
+ * such a word in the provider echo shape (visible ASCII, 1–128 characters). ABSENT otherwise — any other status, a
444
+ * plain-text or torn body, an envelope with neither member, or a word outside the shape (dropped whole, never
445
+ * truncated). The run's `terminal.detail.code` is this value.
446
+ */
447
+ apiErrorCode?: string;
437
448
  diagnostics?: AssistantMessageDiagnostic[];
438
449
  usage: Usage;
439
450
  /** TB 尸检 T1-5: the provider never delivered a usage frame for this message (e.g. a
package/dist/index.d.ts CHANGED
@@ -63,8 +63,10 @@ export { MCP_FAILURE_KINDS, MCP_DELIVERY_VERDICTS, MCP_LIVENESS_STATES, classify
63
63
  export { MCP_INJECTION_DROP_REASONS, MCP_INJECTION_DROP_TEXT, mcpInjectionDroppedNotice, type McpInjectionDropReason, type McpInjectionDropFacts } from "./core/mcp-injection-drop.js";
64
64
  export { askUnresolvableNotice, type AskUnresolvableFacts } from "./core/ask-unresolvable-notice.js";
65
65
  export { ASK_UNRESOLVABLE_CAUSES } from "./core/ask-unresolvable-notice.js";
66
+ export { USER_STEER_UNDRAINED_CAUSES, type UserSteerUndrainedCause } from "./core/engine-notice.js";
67
+ export { TOOL_TURN_INCOMPLETE_CAUSES, type ToolTurnIncompleteCause } from "./internal/llm.js";
66
68
  export { DURABLE_MANDATE_SOURCES } from "./core/tool-policy.js";
67
- export { TERMINAL_CAUSE_IS_REPLAYABLE } from "./core/terminal-cause.js";
69
+ export { TERMINAL_CAUSE_IS_REPLAYABLE, type TerminalFailureDetail } from "./core/terminal-cause.js";
68
70
  export { RESERVED_AGENT_NAMES } from "./agents/subagent.js";
69
71
  export { WORKFLOW_SIZE_GUIDELINE_AGENT_CAPS } from "./orchestration/workflow-size-guideline.js";
70
72
  export { ENGINE_AUTHORITY_ENVELOPE_TAGS } from "./core/untrusted-text.js";
@@ -79,7 +81,7 @@ export { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, WEB_FETCH_TOOL_NAME
79
81
  export { admitMemoryScopes, foldAdmissionFreeze, type OwnOrgAdmissionVerdict, type MemoryAdmissionInput, type MemoryAdmissionOutcome, type MemoryAdmissionVerdict, type MemoryScopeAdmission, type MemoryScopeOrigin, type MemoryScopeRequest, } from "./core/memory-admission.js";
80
82
  export { assertRetentionCapability, RETENTION_CASCADE_DOMAINS, RETENTION_CASCADE_EXEMPT, type ManagedRetentionCapability, type RetentionDeclaration, type RetentionDeclaring, type RetentionPolicy, type RetentionReceipt, } from "./core/retention.js";
81
83
  export { GOVERNANCE_CODES, governanceRetryClass, TOOL_DECLARATION_CODES, type GovernanceCode, type GovernanceRetryClass, type ToolDeclarationCode } from "./core/governance-codes.js";
82
- export { NOTICE_AUDIENCE, noticeAudienceOf, ENGINE_NOTICE_CODES, type EngineNoticeCode, type NoticeAudience } from "./core/governance-codes.js";
84
+ export { NOTICE_AUDIENCE, noticeAudienceOf, ENGINE_NOTICE_CODES, NOTICE_MINTER, type EngineNoticeCode, type NoticeAudience, type NoticeMinter } from "./core/governance-codes.js";
83
85
  export { TtlSessionStore, type TtlSessionStoreOptions, type EvictPolicy } from "./core/session-store.js";
84
86
  export { reconcileInterruptedSession, findOrphanToolCalls, type OrphanToolCall, type ReconcileReport, } from "./core/session-reconcile.js";
85
87
  export { StoredSession, InMemorySessionRepo, InMemorySessionStorage, BaseSessionStorage, leafIdAfterEntry, validateEntriesForImport, StreamingImportValidator, boundedTail, SessionError, isSessionConflict, hasSessionFork, uuidv7, type Session, type SessionStore, type AcquiredSession, type SessionStoreSummary, type SessionStorage, type SessionRepo, type SessionMetadata, type SessionTreeEntry, type SessionWriteOptions, type SessionPlacement, type SessionPlacementRecord, type PlacedSessionRow, } from "./core/session.js";
@@ -191,7 +193,7 @@ export { createAllowDenyPolicy, createApprovalPolicy,
191
193
  * `Parameters<typeof createApprovalPolicy>[0]["approve"]`. */
192
194
  type ApprovalSeatOutcome, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, wrapToolPolicy, decisionText, resolveAsk, toolPolicyNameSets, toolPolicyPathDenyRules, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicyProjection, type ToolPolicyProjectionComponent, type ConstraintChainEntry, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, type AskRuleEvidence, type AskEvidenceAbsence, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
193
195
  export { type ReadRootGrantScreen } from "./core/read-root-grant-screen.js";
194
- export { type AskOrigin, ASK_ORIGINS, isAskOrigin, ORIGIN_IMPLIES_REAL_APPROVAL, PERSISTED_RULE_MANDATES, type PersistedRuleMandate, isPersistedRuleMandate } from "./core/ask-origin.js";
196
+ export { type AskOrigin, ASK_ORIGINS, isAskOrigin, ORIGIN_IMPLIES_REAL_APPROVAL, PERSISTED_RULE_MANDATES, type PersistedRuleMandate, isPersistedRuleMandate, screenAskClassMembers } from "./core/ask-origin.js";
195
197
  export { PERMISSION_MODES, type PermissionMode, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, type AskClass, isAskClass, MODE_ARBITRATION, type ModeArbitrationRow, RELEASABLE_ASK_CLASSES, MODE_RELEASE_REASONS, type ModeReleaseReason, isModeReleaseReason, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, type AskClassification, type AskClassFacts, type ToolMarkSource, arbitrateByMode, type Arbitration, fileCommandOptionsSentence } from "./core/ask-origin.js";
196
198
  export { modePolicyOf, type ModePolicy, PERMISSION_MODE_FOLD_REASONS, type PermissionModeFoldReason, type PermissionModeReceipt, ACCEPT_EDITS_FILE_COMMANDS, modeAcceptsFileCommand } from "./core/mode-policy.js";
197
199
  export { readDenyClassifierRules, renderClassifierDenyRules, type DerivedDenyRule } from "./core/auto-mode-prompt.js";
package/dist/index.js CHANGED
@@ -49,6 +49,8 @@ export { MCP_FAILURE_KINDS, MCP_DELIVERY_VERDICTS, MCP_LIVENESS_STATES, classify
49
49
  export { MCP_INJECTION_DROP_REASONS, MCP_INJECTION_DROP_TEXT, mcpInjectionDroppedNotice } from "./core/mcp-injection-drop.js";
50
50
  export { askUnresolvableNotice } from "./core/ask-unresolvable-notice.js";
51
51
  export { ASK_UNRESOLVABLE_CAUSES } from "./core/ask-unresolvable-notice.js";
52
+ export { USER_STEER_UNDRAINED_CAUSES } from "./core/engine-notice.js";
53
+ export { TOOL_TURN_INCOMPLETE_CAUSES } from "./internal/llm.js";
52
54
  export { DURABLE_MANDATE_SOURCES } from "./core/tool-policy.js";
53
55
  export { TERMINAL_CAUSE_IS_REPLAYABLE } from "./core/terminal-cause.js";
54
56
  export { RESERVED_AGENT_NAMES } from "./agents/subagent.js";
@@ -65,7 +67,7 @@ export { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, WEB_FETCH_TOOL_NAME
65
67
  export { admitMemoryScopes, foldAdmissionFreeze, } from "./core/memory-admission.js";
66
68
  export { assertRetentionCapability, RETENTION_CASCADE_DOMAINS, RETENTION_CASCADE_EXEMPT, } from "./core/retention.js";
67
69
  export { GOVERNANCE_CODES, governanceRetryClass, TOOL_DECLARATION_CODES } from "./core/governance-codes.js";
68
- export { NOTICE_AUDIENCE, noticeAudienceOf, ENGINE_NOTICE_CODES } from "./core/governance-codes.js";
70
+ export { NOTICE_AUDIENCE, noticeAudienceOf, ENGINE_NOTICE_CODES, NOTICE_MINTER } from "./core/governance-codes.js";
69
71
  export { TtlSessionStore } from "./core/session-store.js";
70
72
  export { reconcileInterruptedSession, findOrphanToolCalls, } from "./core/session-reconcile.js";
71
73
  export { StoredSession, InMemorySessionRepo, InMemorySessionStorage, BaseSessionStorage, leafIdAfterEntry, validateEntriesForImport, StreamingImportValidator, boundedTail, SessionError, isSessionConflict, hasSessionFork, uuidv7, } from "./core/session.js";
@@ -156,7 +158,7 @@ export { resolveAutonomousLoopPrompt, AUTONOMOUS_LOOP_PREAMBLE, AUTONOMOUS_LOOP_
156
158
  export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.js";
157
159
  export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, wrapToolPolicy, decisionText, resolveAsk, toolPolicyNameSets, toolPolicyPathDenyRules, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
158
160
  export {} from "./core/read-root-grant-screen.js";
159
- export { ASK_ORIGINS, isAskOrigin, ORIGIN_IMPLIES_REAL_APPROVAL, PERSISTED_RULE_MANDATES, isPersistedRuleMandate } from "./core/ask-origin.js";
161
+ export { ASK_ORIGINS, isAskOrigin, ORIGIN_IMPLIES_REAL_APPROVAL, PERSISTED_RULE_MANDATES, isPersistedRuleMandate, screenAskClassMembers } from "./core/ask-origin.js";
160
162
  export { PERMISSION_MODES, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, isAskClass, MODE_ARBITRATION, RELEASABLE_ASK_CLASSES, MODE_RELEASE_REASONS, isModeReleaseReason, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, arbitrateByMode, fileCommandOptionsSentence } from "./core/ask-origin.js";
161
163
  export { modePolicyOf, PERMISSION_MODE_FOLD_REASONS, ACCEPT_EDITS_FILE_COMMANDS, modeAcceptsFileCommand } from "./core/mode-policy.js";
162
164
  export { readDenyClassifierRules, renderClassifierDenyRules } from "./core/auto-mode-prompt.js";
@@ -4,5 +4,5 @@
4
4
  * design/118 ①②: the layer is now first-party (`src/engine/llm/`, naturalized out of the vendored
5
5
  * llm-core); the facade stays so consumers never notice which side a symbol lives on.
6
6
  */
7
- export { createAssistantMessageEventStream, ensureToolTurnInvariants, snapshotActorAssertion, stripEngineMetadata } from "../engine/llm/index.js";
8
- export type { ActorAssertion, AnthropicMessagesCompat, OpenAICompletionsCompat, OpenAIResponsesCompat, AssistantMessage, AssistantMessageDiagnostic, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, Model, ResilienceOptions, RouteAdjudication, RouteCredential, RouteCredentialSource, RoutePairingConfig, RoutePairingPosture, RouteRefusalCode, RouteRefusalDetail, SimpleStreamOptions, StallTimeouts, StopReason, StreamFn, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage, ToolTurnRepair, Usage, UserMessage, } from "../engine/llm/index.js";
7
+ export { createAssistantMessageEventStream, ensureToolTurnInvariants, snapshotActorAssertion, stampStreamDrops, streamDropsOf, stripEngineMetadata, TOOL_TURN_INCOMPLETE_CAUSES, TOOL_TURN_REPAIR_DROPS } from "../engine/llm/index.js";
8
+ export type { ActorAssertion, AnthropicMessagesCompat, OpenAICompletionsCompat, OpenAIResponsesCompat, AssistantMessage, AssistantMessageDiagnostic, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, Model, ResilienceOptions, RouteAdjudication, RouteCredential, RouteCredentialSource, RoutePairingConfig, RoutePairingPosture, RouteRefusalCode, RouteRefusalDetail, SimpleStreamOptions, StallTimeouts, StopReason, StreamFn, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage, StreamDrop, StreamDropCause, ToolTurnIncompleteCause, ToolTurnRepair, Usage, UserMessage, } from "../engine/llm/index.js";
@@ -1 +1 @@
1
- export { createAssistantMessageEventStream, ensureToolTurnInvariants, snapshotActorAssertion, stripEngineMetadata } from "../engine/llm/index.js";
1
+ export { createAssistantMessageEventStream, ensureToolTurnInvariants, snapshotActorAssertion, stampStreamDrops, streamDropsOf, stripEngineMetadata, TOOL_TURN_INCOMPLETE_CAUSES, TOOL_TURN_REPAIR_DROPS } from "../engine/llm/index.js";
@@ -15,7 +15,7 @@ export class WorkflowModelNotAllowedError extends Error {
15
15
  this.name = "WorkflowModelNotAllowedError";
16
16
  }
17
17
  }
18
- export const WHITELIST_KEYS = [
18
+ export const WHITELIST_KEYS = Object.freeze([
19
19
  "objective",
20
20
  "modelName",
21
21
  "thinking",
@@ -25,7 +25,7 @@ export const WHITELIST_KEYS = [
25
25
  "readFace",
26
26
  "writeFace",
27
27
  "readDenyPatterns",
28
- ];
28
+ ]);
29
29
  const WHITELIST_KEY_SET = new Set(WHITELIST_KEYS);
30
30
  export const SPEC_KEY_ALIASES = new Map([["model", "modelName"]]);
31
31
  export const STRIPPED_KEYS_NOTICE_CODE = "workflow.governance_key_stripped";
@@ -1,5 +1,5 @@
1
- export const PARK_TRUTH_UNREADABLE_REASONS = ["store_threw", "record_missing", "record_not_terminal", "parks_unreadable", "admission_incomplete", "checkpoint_store_threw", "capability_not_armed"];
2
- export const PARK_NOT_PENDING_STATUSES = ["resolved", "expired", "absent"];
1
+ export const PARK_TRUTH_UNREADABLE_REASONS = Object.freeze(["store_threw", "record_missing", "record_not_terminal", "parks_unreadable", "admission_incomplete", "checkpoint_store_threw", "capability_not_armed"]);
2
+ export const PARK_NOT_PENDING_STATUSES = Object.freeze(["resolved", "expired", "absent"]);
3
3
  export class WorkflowBudgetExceededError extends Error {
4
4
  spent;
5
5
  total;
package/dist/shell/lex.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const SHELL_LEX_DIALECT = "bash";
2
- export const QUOTE_KINDS = ["'", '"', "$'", '$"', "\\"];
3
- export const EXPANSION_KINDS = ["parameter", "command", "process", "arithmetic", "tilde", "brace", "glob"];
4
- export const UNRESOLVED_REASONS = ["ansi_c_escape", "locale_string", "parameter_text", "subscript_text", "compound_array_value"];
2
+ export const QUOTE_KINDS = Object.freeze(["'", '"', "$'", '$"', "\\"]);
3
+ export const EXPANSION_KINDS = Object.freeze(["parameter", "command", "process", "arithmetic", "tilde", "brace", "glob"]);
4
+ export const UNRESOLVED_REASONS = Object.freeze(["ansi_c_escape", "locale_string", "parameter_text", "subscript_text", "compound_array_value"]);
5
5
  export const UNRESOLVED_SENTENCE = {
6
6
  ansi_c_escape: "an ANSI-C quoted word carries an escape this lexer does not fold",
7
7
  locale_string: "a locale-translated string carries text this lexer does not read",
@@ -9,7 +9,7 @@ export const UNRESOLVED_SENTENCE = {
9
9
  subscript_text: "a subscript carries text this lexer does not read",
10
10
  compound_array_value: "a compound assignment value is a list this lexer does not read",
11
11
  };
12
- export const PARSE_FAILURE_REASONS = [
12
+ export const PARSE_FAILURE_REASONS = Object.freeze([
13
13
  "unterminated_single_quote",
14
14
  "unterminated_double_quote",
15
15
  "unterminated_ansi_c",
@@ -23,7 +23,7 @@ export const PARSE_FAILURE_REASONS = [
23
23
  "redirect_without_target",
24
24
  "heredoc_without_delimiter",
25
25
  "heredoc_delimiter_expands",
26
- ];
26
+ ]);
27
27
  export const PARSE_FAILURE_SENTENCE = {
28
28
  unterminated_single_quote: "a single-quoted word never closes",
29
29
  unterminated_double_quote: "a double-quoted word never closes",
@@ -39,7 +39,7 @@ export const PARSE_FAILURE_SENTENCE = {
39
39
  heredoc_without_delimiter: "a here-document operator names no delimiter",
40
40
  heredoc_delimiter_expands: "a here-document delimiter is not a plain word",
41
41
  };
42
- export const BASH_ONLY_CONSTRUCTS = [
42
+ export const BASH_ONLY_CONSTRUCTS = Object.freeze([
43
43
  "ansi_c_quoting",
44
44
  "locale_string",
45
45
  "append_assignment",
@@ -50,7 +50,7 @@ export const BASH_ONLY_CONSTRUCTS = [
50
50
  "redirect_both",
51
51
  "pipe_both",
52
52
  "brace_expansion",
53
- ];
53
+ ]);
54
54
  export const ANSI_C_ESCAPES = {
55
55
  a: "\x07",
56
56
  b: "\b",
@@ -89,7 +89,7 @@ function launcherOptionStep(row, tok) {
89
89
  return 1;
90
90
  return undefined;
91
91
  }
92
- export const PROGRAM_POSITION_REFUSAL_KINDS = ["option_at_program_position", "control_syntax_at_program_position"];
92
+ export const PROGRAM_POSITION_REFUSAL_KINDS = Object.freeze(["option_at_program_position", "control_syntax_at_program_position"]);
93
93
  export function readProgramPosition(words) {
94
94
  const folded = words.map((w) => w.folded);
95
95
  const controlWords = [];
@@ -172,7 +172,7 @@ export function argvReentryUnjudged(pos, acceptedTargets) {
172
172
  const runs = pos.program?.basename ?? (pos.runsDefaultProgram ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined);
173
173
  return runs === undefined || !acceptedTargets.has(runs);
174
174
  }
175
- export const SHELL_SCAN_DECLINES = ["unreadable_program_position", "stream_fed_operands"];
175
+ export const SHELL_SCAN_DECLINES = Object.freeze(["unreadable_program_position", "stream_fed_operands"]);
176
176
  export function programPositionRunsShell(pos, shellProgram) {
177
177
  if (pos.refusal !== undefined)
178
178
  return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true;
@@ -2,6 +2,7 @@ import { Type } from "typebox";
2
2
  import type { ExecutionEnv } from "../../internal/harness-types.js";
3
3
  import { type FsViolation, type ReadFileState } from "./safety.js";
4
4
  import { type DecodedTextFile } from "./encoding.js";
5
+ import { type HomeViewsCarrier } from "./read-deny-home.js";
5
6
  import { type ImageDownsampler } from "../../core/mcp.js";
6
7
  import { type EngineNotice } from "../../core/types.js";
7
8
  /**
@@ -543,7 +544,7 @@ export declare function enoentMessage(env: ExecutionEnv, key: string, cwd: strin
543
544
  matchPath(path: string): {
544
545
  pattern: string;
545
546
  } | null;
546
- }): Promise<string>;
547
+ } & HomeViewsCarrier): Promise<string>;
547
548
  /** Per-task mutable working directory shared by the shell and the path-taking fs tools.
548
549
  * Holds the RAW path (never canonicalized): bash `cd` updates `current`, and the fs tools resolve relative
549
550
  * paths against it. Containment is still enforced per-op by resolveKey (canonicalize + within), so a `cd`
@@ -345,7 +345,7 @@ export function applyContextShrinkToReadFileState(state, shrink) {
345
345
  export async function enoentMessage(env, key, cwd, signal, readDeny) {
346
346
  let msg = `File does not exist. Note: your current working directory is ${cwd}.`;
347
347
  const sep = Math.max(key.lastIndexOf("/"), key.lastIndexOf("\\"));
348
- const views = readDeny !== undefined ? homeViewsOf(env, signal) : undefined;
348
+ const views = readDeny !== undefined ? homeViewsOf(env, readDeny, signal) : undefined;
349
349
  const denied = async (path, leaf) => {
350
350
  if (readDeny === undefined || views === undefined)
351
351
  return false;
@@ -186,17 +186,24 @@ export interface HandsToolkitOptions {
186
186
  * time (never silently ignored). Deployment seat ONLY, same clamp argument as the tiers key. */
187
187
  readDenyBuiltinExclude?: readonly string[];
188
188
  /** The spellings of the execution environment's home a home-anchored deny row (`~/…`) is placed under
189
- * ({@link import("./read-deny-home.js").readDenyHomesOf}: the declared home and its canonical spelling). Absolute path
190
- * strings only — any other shape throws at wiring time, naming the seat. The Runner passes the spellings its own judge
191
- * was compiled with. */
189
+ * ({@link import("./read-deny-home.js").readDenyHomesOf}: the declared home and the account home, each with its canonical
190
+ * spelling). Absolute path strings only — any other shape throws at wiring time, naming the seat. The Runner passes the
191
+ * spellings its own judge was compiled with. A list given here is the band's home set as is: the account database is
192
+ * not asked and no home is added (each listed home is anchored at its spelling and, where the environment answers one,
193
+ * its canonical spelling), and the band's home views (a path spelled through a firmlink or a bind mount of a home,
194
+ * judged at the home) read this list and the environment's declared home; an entry that is not the declared home is probed once,
195
+ * under a short bound of its own, and a probe of it that fails costs that entry alone. */
192
196
  readDenyHomes?: readonly string[];
193
197
  /** Where each home-anchored row's literal head leads beyond the home's spellings
194
198
  * ({@link import("./read-deny-home.js").readDenyHomeLinksOf}, keyed by the row's pattern) — the Runner passes the
195
199
  * locations its own judge was compiled with, so the band and the gate judge one placement.
196
200
  * Unless a mount carries both, the band resolves the placement itself the way the Runner does (the homes it carries
197
- * are used as given; {@link import("./read-deny-home.js").placeReadDenyAtHome}), and its read tools (Read / Grep /
198
- * Glob / RepoMap) wait for that placement before they judge anything. A placement that fails leaves the home-anchored
199
- * rows held anywhere. */
201
+ * are its home set — the account is not asked and no home is added;
202
+ * {@link import("./read-deny-home.js").placeReadDenyAtHome}), and its read tools (Read / Grep / Glob / RepoMap) wait
203
+ * for that placement before they judge anything. A placement that fails leaves the home-anchored rows held anywhere.
204
+ * Carrying both is the engine's own handoff of ONE placement — its home views travel with it — so
205
+ * `readDenyHomes` must then be the very list that placement returned: any other list (a copy) holds the home-anchored
206
+ * rows at every location and is named on `console.warn`. */
200
207
  readDenyHomeLinks?: ReadonlyMap<string, readonly string[]>;
201
208
  /** The permission-rule rows of the deny table ({@link import("./read-deny.js").PathRuleDenyRow}, `ToolPolicy.pathDenyRules`):
202
209
  * the read-face rows join the structured read faces' judge beside the segment rows. The Runner passes the leg's own. */