@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
@@ -18,7 +18,8 @@ import type { OnAsk } from "../tool-policy.js";
18
18
  import { type ArtifactPlane } from "../artifact-host.js";
19
19
  import { RosterBuilder } from "../tool-roster.js";
20
20
  import type { AgentTool, RunnerDeps } from "../types.js";
21
- import type { ReadDenyMatcher, ContainmentFace } from "../../tools/fs/index.js";
21
+ import type { ContainmentFace } from "../../tools/fs/index.js";
22
+ import type { PlacedReadDenyMatcher } from "../../tools/fs/read-deny-home.js";
22
23
  import { type ArtifactStoreRef } from "./read-root-grant.js";
23
24
  import type { Prepared } from "./contracts.js";
24
25
  export interface PrepareArtifactInput {
@@ -46,7 +47,7 @@ export interface PrepareArtifactInput {
46
47
  * live set: an in-flight read-root grant widens it). */
47
48
  additionalRootsCanonical: () => readonly string[];
48
49
  /** borrowed-readonly — the read-deny matcher in force for the run, when any. */
49
- readDenyMatcher: ReadDenyMatcher | undefined;
50
+ readDenyMatcher: PlacedReadDenyMatcher | undefined;
50
51
  /** borrowed-readonly — the resolved read face (`"open"` lifts root containment for reads). */
51
52
  resolvedReadFace: ContainmentFace | undefined;
52
53
  /** borrowed-readonly — the tracked-cwd cell; `current` is read per call as the relative-path base. */
@@ -2,21 +2,23 @@
2
2
  * PrepareTask's CONTEXT-LANE phase, verbatim from the driver: the edit/guard budgets, the
3
3
  * microCompact machine state with the trigger-window clamp's one loud outlet, the model's chars-per-token coordinate,
4
4
  * and the `context` handler registered on the harness — heal → ledger replay → aggregate tool-result and media caps →
5
- * frontier clear → the guard chain (arms A/B/C) → the git-frame guard → the orphan belt → the lossy-projection flag →
6
- * the projection seat. The four module-level helpers only this lane called moved with it (the machine-state builder,
7
- * the frontier-clear recorder, the observation-buffer flush, the guard chain with its two types). The interface is the
8
- * dependency list the segment had implicitly.
5
+ * frontier clear → the guard chain (arms A/B/C) → the git-frame guard → the lossy-projection flag → the projection
6
+ * seat — and the harness's sink for the provider boundary's tool-turn repairs (`engine/llm/tool-turns.ts`, the one
7
+ * law every request builder reads): the run's `context.tool_turn_repaired` seat, and the lossy flag's second half —
8
+ * a request the law left recorded content out of is lossy too. The four module-level helpers only this lane called
9
+ * moved with it (the machine-state builder, the frontier-clear recorder, the observation-buffer flush, the guard chain
10
+ * with its two types). The interface is the dependency list the segment had implicitly.
9
11
  *
10
12
  * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (the handler it registers is async and
11
13
  * runs per provider request), so the phase adds no yield between the gate stations before it and the turn wiring after
12
14
  * it. Read-stability of the host handles: {@link RunInternals} (`@contract prepare.deps-read-stable`).
13
15
  *
14
- * REGISTRATION SIDE EFFECT, OWNED: this phase registers the ONE `context` handler on the harness; the harness owns the
15
- * listener for the run's life (it is never unregistered — the harness is per prepare). No other station registers on
16
- * that hook.
16
+ * REGISTRATION SIDE EFFECT, OWNED: this phase registers the ONE `context` handler on the harness and sets its
17
+ * `onToolTurnRepaired` sink; the harness owns both for the run's life (never unregistered — the harness is per
18
+ * prepare). No other station registers on that hook or sets that sink.
17
19
  */
18
20
  import { type AgentHarness, type AgentMessage } from "../../internal/harness.js";
19
- import type { Model } from "../../internal/llm.js";
21
+ import { type Model } from "../../internal/llm.js";
20
22
  import { type ClearedProjectionLedger, type MicroCompactMachine, type OccurrenceFingerprint, type OccurrenceIndex } from "../context-edit.js";
21
23
  import type { ContextWindowOverlay } from "../context-settings.js";
22
24
  import { type ToolResultStore } from "../tool-result-store.js";
@@ -28,9 +30,9 @@ import { type ToolRosterDeltaSeat } from "../tool-roster.js";
28
30
  export interface PrepareContextLaneInput {
29
31
  /** borrowed-readonly — the task model: the budgets, the trigger-window geometry and `charsPerToken` are read off it. */
30
32
  model: Model;
31
- /** borrowed-readonly — the deployment seats, whole: `onNotice` (the clamp outlet, the offload-loss announcement), `tracer`
32
- * (the cap / clear / trim frames), `mediaByteCapBytes` and `onMediaStripped` (the media cap), `onError` (the belt line, the
33
- * git-frame degrade) — the guard chain and the machine-state builder read through the whole record. */
33
+ /** borrowed-readonly — the deployment seats, whole: `onNotice` (the clamp outlet, the offload-loss announcement, the
34
+ * tool-turn repair seat), `tracer` (the cap / clear / trim frames), `mediaByteCapBytes` and `onMediaStripped` (the media
35
+ * cap), `onError` (the git-frame degrade) — the guard chain and the machine-state builder read through the whole record. */
34
36
  deps: RunnerDeps;
35
37
  /** borrowed-readonly — the acquired session id (the offload persist key; every disclosure's attribution). */
36
38
  sessionId: string;
@@ -47,8 +49,9 @@ export interface PrepareContextLaneInput {
47
49
  machine: MicroCompactMachine;
48
50
  clearOnRejection: boolean;
49
51
  };
50
- /** borrowed-mutable — the built harness. Writer here: `on("context", …)` — the ONE registration of the request-build
51
- * handler (the harness owns the listener; nothing unregisters it). */
52
+ /** borrowed-mutable — the built harness. Writers here: `on("context", …)` — the ONE registration of the request-build
53
+ * handler (the harness owns the listener; nothing unregisters it) — and `onToolTurnRepaired`, the provider boundary's
54
+ * repair sink. */
52
55
  harness: AgentHarness;
53
56
  /** borrowed-mutable — the turn's halt fact (`Prepared.batchHaltRef`). Writer here: the handler CLEARS it at every new
54
57
  * provider request (a new request means the halted boundary was legitimately passed). The judgment site sets it. */
@@ -60,8 +63,9 @@ export interface PrepareContextLaneInput {
60
63
  postureFrameRef: {
61
64
  protectedText?: string;
62
65
  };
63
- /** borrowed-mutable — the per-request lossy-projection flag. Writer here: the handler re-derives it at every request
64
- * build; the provider-seam recording shim reads it immediately after. */
66
+ /** borrowed-mutable — the per-request lossy-projection flag. Writers here: the handler re-derives it at every request
67
+ * build, and the repair sink raises it when the provider boundary's law left recorded content out of the same request
68
+ * (the boundary runs after the handler, before the recording shim); the shim reads it immediately after. */
65
69
  requestLossyRef: {
66
70
  current: boolean;
67
71
  };
@@ -1,6 +1,7 @@
1
1
  import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens } from "../../internal/harness.js";
2
+ import { TOOL_TURN_REPAIR_DROPS } from "../../internal/llm.js";
2
3
  import { clearStaleToolResults, createClearedProjectionLedger, dropEmptyFailureAssistants, editBudget, replayClearedProjection, resolveTriggerWindow } from "../context-edit.js";
3
- import { dropOrphanToolResults, guardBudget, insertTrimNotice, protectGitFrame, trimToBudget } from "../context-guard.js";
4
+ import { guardBudget, insertTrimNotice, protectGitFrame, trimToBudget } from "../context-guard.js";
4
5
  import { AGGREGATE_MEDIA_BUDGET_BYTES, capAggregateMediaBytes } from "../media-byte-cap.js";
5
6
  import { capAggregateToolResults } from "../tool-result-budget.js";
6
7
  import { createOffloadPersist } from "../tool-result-store.js";
@@ -9,6 +10,7 @@ import { deliverEngineNotice } from "../types.js";
9
10
  import { applyGitFrameGuard } from "./git-status-frame.js";
10
11
  import { applyContextShrinkToReadFileState } from "../../tools/fs/index.js";
11
12
  import { rosterToolName } from "../tool-roster.js";
13
+ import { createToolTurnRepairSeat } from "./tool-turn-repair-seat.js";
12
14
  export function prepareContextLane(input) {
13
15
  const { model, deps, sessionId, runId, hostTaskId, offloadStore, toolRosterDeltas, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, readFileState } = input;
14
16
  const toolNameOf = (recorded) => rosterToolName(toolRosterDeltas.current, recorded);
@@ -20,6 +22,12 @@ export function prepareContextLane(input) {
20
22
  };
21
23
  const microCompact = buildMicroCompactState(deps, model, sessionId, runId, offloadStore, microCompactKnob, contextOverlay());
22
24
  const charsPerToken = model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
25
+ const announceRepairs = createToolTurnRepairSeat((notice) => deps.onNotice?.(notice), { sessionId, runId });
26
+ harness.onToolTurnRepaired = (repairs) => {
27
+ if (repairs.some((repair) => TOOL_TURN_REPAIR_DROPS[repair.form]))
28
+ requestLossyRef.current = true;
29
+ announceRepairs(repairs);
30
+ };
23
31
  harness.on("context", async ({ messages, recheck, signal }) => {
24
32
  batchHaltRef.current = undefined;
25
33
  const pendingProjectionObservations = [];
@@ -97,17 +105,6 @@ export function prepareContextLane(input) {
97
105
  if (kept.action === "kept" || kept.action === "reinserted")
98
106
  trimmed = kept.messages;
99
107
  }
100
- const swept = dropOrphanToolResults(trimmed);
101
- if (swept.dropped.length > 0) {
102
- try {
103
- deps.onError?.(new Error(`context belt: dropped ${swept.dropped.length} orphan toolResult(s) whose tool-call assistant was not in the request view — ` +
104
- swept.dropped
105
- .map((d) => `toolCallId=${d.toolCallId} at index ${d.index} (prev=${d.prevRole}, next=${d.nextRole})`)
106
- .join("; ")), { phase: "hook", sessionId });
107
- }
108
- catch {
109
- }
110
- }
111
108
  requestLossyRef.current =
112
109
  replayed !== healed ||
113
110
  capped !== replayed ||
@@ -115,10 +112,9 @@ export function prepareContextLane(input) {
115
112
  edited !== mediaCapped ||
116
113
  working !== edited ||
117
114
  trimDroppedMessages ||
118
- swept.dropped.length > 0 ||
119
115
  gitStatusRef.overBudgetShrunk === true;
120
- microCompact.projectionRef.current = { messages: swept.messages, keyOf: replay.index.keyOf };
121
- return { messages: swept.messages };
116
+ microCompact.projectionRef.current = { messages: trimmed, keyOf: replay.index.keyOf };
117
+ return { messages: trimmed };
122
118
  }
123
119
  finally {
124
120
  flushProjectionObservations(pendingProjectionObservations);
@@ -102,7 +102,7 @@ export function prepareGateStations(input) {
102
102
  const gate = directDeny("compliance");
103
103
  gateOutcomes.set(e.toolCallId, gate);
104
104
  if (notifyPermissionDenied) {
105
- await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: complianceDeny, gate, identity: hookIdentity });
105
+ await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: complianceDeny, gate, identity: hookIdentity, permissionMode: permissionMode.observed });
106
106
  }
107
107
  return { block: true, reason: formatHookFeedback(complianceDeny, reminderMark), preToolContext: [] };
108
108
  }
@@ -115,7 +115,7 @@ export function prepareGateStations(input) {
115
115
  ...(e.signal !== undefined ? { callSignal: e.signal } : {}),
116
116
  identity: hookIdentity,
117
117
  reminderMark,
118
- preToolUse: ownGatePreToolUse,
118
+ ...(ownGatePreToolUse !== undefined ? { preToolUse: (name, args, ctx) => ownGatePreToolUse(name, args, { ...ctx, permissionMode: permissionMode.observed }) } : {}),
119
119
  hookTimeoutMs,
120
120
  ...(handsCwdRef !== undefined ? { trackedCwd: () => handsCwdRef.current } : {}),
121
121
  ...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
@@ -131,7 +131,7 @@ export function prepareGateStations(input) {
131
131
  irreversibility: tierSeat?.value,
132
132
  reversibilityProbe: tierSeat?.read(reversibilityProbes),
133
133
  abortSignal: abortController.signal,
134
- ...(notifyPermissionDenied ? { permissionDenied: notifyPermissionDenied } : {}),
134
+ ...(notifyPermissionDenied ? { permissionDenied: (p) => notifyPermissionDenied({ ...p, permissionMode: permissionMode.observed }) } : {}),
135
135
  onHookError: notifyHookError,
136
136
  shellGated: tierSeat?.shellGated(shellGatedTools, toolMarkSources) === true,
137
137
  ...(autoModeDecider.current !== undefined ? { autoMode: { decider: autoModeDecider.current, ...(autoModeDenialTracking.current !== undefined ? { denialTracking: autoModeDenialTracking.current } : {}), unavailable: classifierUnavailableSeat({ station: "leg", ...(autoModeDecider.current.timeoutMs !== undefined ? { budgetMs: autoModeDecider.current.timeoutMs } : {}), approvalSeat: askReachesPerson(askRoute), onNotice: deps.onNotice, sessionId, runId }) } } : {}),
@@ -263,7 +263,7 @@ export function prepareGateStations(input) {
263
263
  isInterrupt: abortController.signal.aborted || spec.signal?.aborted === true,
264
264
  content: e.content.map((c) => ({ ...c })),
265
265
  details: clonedDetails,
266
- }, { toolCallId: e.toolCallId, toolName: failedName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
266
+ }, { toolCallId: e.toolCallId, toolName: failedName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity, permissionMode: permissionMode.observed }));
267
267
  if (failureSeat.expired) {
268
268
  notifyOwnHookCrash(hookSeatExpiredError("postToolUseFailure", hookTimeoutMs, failureSeat.cause, "the observation was abandoned and its additionalContext dropped; the failure result itself is unchanged"));
269
269
  }
@@ -276,7 +276,7 @@ export function prepareGateStations(input) {
276
276
  }
277
277
  else if (hooks?.postToolUse) {
278
278
  const ranName = wireOf("hook.postToolUse", post);
279
- const successSeat = await runHookSeat("postToolUse", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUse(ranName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: ranName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
279
+ const successSeat = await runHookSeat("postToolUse", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUse(ranName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: ranName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity, permissionMode: permissionMode.observed }));
280
280
  if (successSeat.expired) {
281
281
  notifyOwnHookCrash(hookSeatExpiredError("postToolUse", hookTimeoutMs, successSeat.cause, "the seat was abandoned; the tool result is delivered to the model UNMODIFIED (no updatedOutput, no additionalContext)"));
282
282
  }
@@ -1,5 +1,6 @@
1
- import { type BeforeWriteHook, type CwdRef, type ReadDenyMatcher, type PathRuleDenyRow, type ContainmentFace } from "../../tools/fs/index.js";
1
+ import { type BeforeWriteHook, type CwdRef, type PathRuleDenyRow, type ContainmentFace } from "../../tools/fs/index.js";
2
2
  import { type ReadFileState } from "../../tools/fs/safety.js";
3
+ import { type PlacedReadDenyMatcher } from "../../tools/fs/read-deny-home.js";
3
4
  import { type ShellReadAdmission, type ShellReadCheck } from "../../tools/fs/shell-read-check.js";
4
5
  import type { ShellReadBoundary } from "../../tools/fs/shell-read-judge.js";
5
6
  import type { RunnerDeps, ShellGateDoctrine, TaskSpec } from "../types.js";
@@ -254,8 +255,8 @@ export interface PrepareHandsReadFaceResult {
254
255
  * reader applies the same containment the hands tools do. Undefined ⇔ hands-less. */
255
256
  attachmentRootCanonical: string | undefined;
256
257
  /** owned — the ONE compiled deny judge per prepare (built-ins ∪ deployment ∪
257
- * task additions). Undefined ⇔ hands-less (its consumers only exist with hands). */
258
- readDenyMatcher: ReadDenyMatcher | undefined;
258
+ * task additions), carrying the home views of its placement. Undefined ⇔ hands-less (its consumers only exist with hands). */
259
+ readDenyMatcher: PlacedReadDenyMatcher | undefined;
259
260
  /** owned — the shell-seat PROVENANCE set: the shell-running tools whose tier the engine's shell seat (read
260
261
  * boundary + shape tier + shellGate floor) is the SOLE POSITIVE source of — no explicit deployment mark on that name.
261
262
  * The one seat every consumer reads (risk grading, the gate's tighten attribution, the persisted-rule mandate
@@ -313,6 +313,7 @@ export async function prepareHandsMount(input) {
313
313
  tools: shellReadTools,
314
314
  seatOf: () => (handsReadOnly ? "readonly" : "full_shell"),
315
315
  boundaryOf: () => readonlyBoundary() ?? shellReadBoundary(), rootsCanonical: true,
316
+ homeViews: readDenyPlacement.matcher.homeViews,
316
317
  });
317
318
  }
318
319
  if (!handsReadOnly) {
@@ -36,7 +36,7 @@ export function prepareInheritedGate(input) {
36
36
  if (modeSeed.migrated !== undefined) {
37
37
  deliverEngineNotice(deps.onNotice, {
38
38
  code: "resume.legacy_mode_seed_migrated",
39
- message: `the resumed checkpoint row carried pre-mode keys (${modeSeed.migrated.droppedKeys.join(", ")}); read once into ${modeSeed.migrated.permissionMode !== undefined ? `permissionMode: "${modeSeed.migrated.permissionMode}"` : "no mode of its own (the chain's ceiling stands)"}${modeSeed.shellGate !== undefined ? ' with the shell floor "always" kept' : ""} — the next suspend writes the one key; this migration is kept through 7.37.x and removed in 7.38.0, which refuses a row still carrying these keys (resume or abandon such rows before upgrading)`,
39
+ message: `the resumed checkpoint row carried pre-mode keys (${modeSeed.migrated.droppedKeys.join(", ")}); read once into ${modeSeed.migrated.permissionMode !== undefined ? `permissionMode: "${modeSeed.migrated.permissionMode}"` : "no mode of its own (the chain's ceiling stands)"}${modeSeed.shellGate !== undefined ? ' with the shell floor "always" kept' : ""} — the next suspend writes the one key; this migration is kept through 7.38.x and removed in 7.39.0, which refuses a row still carrying these keys (resume or abandon such rows before upgrading)`,
40
40
  detail: { migrated: { ...(modeSeed.migrated.permissionMode !== undefined ? { permissionMode: modeSeed.migrated.permissionMode } : {}), droppedKeys: [...modeSeed.migrated.droppedKeys] }, sessionId, runId },
41
41
  });
42
42
  }
@@ -14,7 +14,7 @@ import { createActiveSkillScopePolicy } from "./active-skill-scope.js";
14
14
  import { arbitrateByMode, askClassMembersOf, askMandateMember } from "../ask-origin.js";
15
15
  import { createModeGatePolicy, mountedToolDeclaresReadOnly } from "../mode-policy.js";
16
16
  const ancestorFacts = (ask) => ({ ...(ask ?? {}), ancestorResolved: true });
17
- import { headlessDenyAtFold, headlessDenyAtRecheck, INHERITED_STATION_FACTS, inheritedAskCarry, judgeInheritedClassifier, lateAskSettlementObserver, settleDenialLimitFallback } from "./denial-limit-arms.js";
17
+ import { headlessDenyAtFold, headlessDenyAtRecheck, INHERITED_STATION_FACTS, inheritedAskCarry, inheritedAskFallbackMessage, judgeInheritedClassifier, lateAskSettlementObserver, settleDenialLimitFallback } from "./denial-limit-arms.js";
18
18
  import { settleReadRootGrant } from "./read-root-grant.js";
19
19
  import { askGrantShapeOf } from "./inherited-ask-grants.js";
20
20
  import { createPermissionRuleLanes, inheritedAskRuleEvidence, createRuleOffersOf } from "./permission-rule-lanes.js";
@@ -24,8 +24,8 @@ import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js";
24
24
  export async function preparePolicyChain(input) {
25
25
  const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
26
26
  const ancestorArbitrations = new Map();
27
- const arbitrateInherited = (ask, toolCallId, classification) => {
28
- const verdict = arbitrateByMode(permissionMode.current, classification, { ...(ask.origin !== undefined ? { origin: ask.origin } : {}), ...(ask.mandate !== undefined ? { mandate: ask.mandate } : {}) });
27
+ const arbitrateInherited = (ask, toolCallId, judged) => {
28
+ const verdict = arbitrateByMode(permissionMode.current, judged.classification, { origin: judged.origin, ...askMandateMember(ancestorFacts(judged.ask.action === "ask" ? judged.ask : undefined)) });
29
29
  if (verdict.kind === "ask" || verdict.kind === "accepted")
30
30
  return undefined;
31
31
  ancestorArbitrations.set(toolCallId, verdict);
@@ -300,7 +300,7 @@ export async function preparePolicyChain(input) {
300
300
  const seat = seatMarksOf(call);
301
301
  return {
302
302
  ...INHERITED_STATION_FACTS,
303
- mandateMarks: { egress: seat.egress, shellGated: seat.shellGated, ...(seat.tier !== undefined ? { irreversibility: seat.tier } : {}) },
303
+ mandateMarks: { egress: seat.egress, shellGated: false, ...(seat.tier === "always" && !seat.shellGated ? { irreversibility: "always" } : {}) },
304
304
  ...(seat.sources.size > 0 ? { toolMarkSources: seat.sources } : {}),
305
305
  ...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}),
306
306
  };
@@ -382,7 +382,7 @@ export async function preparePolicyChain(input) {
382
382
  return vetoed(judged.result);
383
383
  const { ask: editAsk, fallback: editFallback, mintedHere: editMintedHere } = judged;
384
384
  re = editAsk;
385
- const editArbitrated = arbitrateInherited(re, creq.toolCallId, judged.classification);
385
+ const editArbitrated = arbitrateInherited(re, creq.toolCallId, judged);
386
386
  if (editArbitrated !== undefined) {
387
387
  const out = editArbitrated.action === "allow" ? { action: "allow", updatedInput: editArgs } : editArbitrated;
388
388
  carryPrecedingSettlement(settledBy, out);
@@ -393,7 +393,7 @@ export async function preparePolicyChain(input) {
393
393
  toolCallId: creq.toolCallId,
394
394
  args: editArgs,
395
395
  ...ruleOffersOf(creq, editArgs, ancestorFacts(re)),
396
- message: re.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
396
+ message: re.message ?? inheritedAskFallbackMessage(creq.toolName),
397
397
  ...askSourceIdentity(),
398
398
  ...riskAxesOf(creq),
399
399
  ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
@@ -463,7 +463,7 @@ export async function preparePolicyChain(input) {
463
463
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
464
464
  return { action: "allow" };
465
465
  }
466
- const arbitrated = arbitrateInherited(inheritedAsk, creq.toolCallId, judged.classification);
466
+ const arbitrated = arbitrateInherited(inheritedAsk, creq.toolCallId, judged);
467
467
  if (arbitrated !== undefined)
468
468
  return arbitrated;
469
469
  const presentedArgs = creq.args;
@@ -473,7 +473,7 @@ export async function preparePolicyChain(input) {
473
473
  toolCallId: creq.toolCallId,
474
474
  args: presentedArgs,
475
475
  ...ruleOffersOf(creq, presentedArgs, ancestorFacts(inheritedAsk.action === "ask" ? inheritedAsk : undefined)),
476
- message: inheritedAsk.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
476
+ message: inheritedAsk.message ?? inheritedAskFallbackMessage(creq.toolName),
477
477
  ...askSourceIdentity(),
478
478
  ...riskAxesOf(creq),
479
479
  ...(inheritedAsk.action === "ask" && inheritedAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
@@ -537,7 +537,7 @@ export async function preparePolicyChain(input) {
537
537
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
538
538
  return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
539
539
  }
540
- const arbitrated = arbitrateInherited(inheritedAsk.action === "ask" ? { ...inheritedAsk, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) } : inheritedAsk, creq.toolCallId, judged.classification);
540
+ const arbitrated = arbitrateInherited(inheritedAsk.action === "ask" ? { ...inheritedAsk, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) } : inheritedAsk, creq.toolCallId, judged);
541
541
  if (arbitrated !== undefined)
542
542
  return arbitrated;
543
543
  const askT0 = now();
@@ -546,7 +546,7 @@ export async function preparePolicyChain(input) {
546
546
  toolCallId: creq.toolCallId,
547
547
  args: presentedArgs,
548
548
  ...ruleOffersOf(creq, presentedArgs, ancestorFacts(inheritedAsk.action === "ask" ? inheritedAsk : undefined)),
549
- message: inheritedAsk.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
549
+ message: inheritedAsk.message ?? inheritedAskFallbackMessage(creq.toolName),
550
550
  ...askSourceIdentity(),
551
551
  ...riskAxesOf(creq),
552
552
  ...(inheritedAsk.action === "ask" && inheritedAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
@@ -10,7 +10,7 @@ import { type ToolPolicy } from "../tool-policy.js";
10
10
  import type { RunnerDeps, TaskSpec } from "../types.js";
11
11
  import type { CwdRef } from "../../tools/fs/fs-shared.js";
12
12
  import { type ContainmentFace } from "../../tools/fs/index.js";
13
- import type { ReadDenyMatcher } from "../../tools/fs/read-deny.js";
13
+ import type { PlacedReadDenyMatcher } from "../../tools/fs/read-deny-home.js";
14
14
  import { type ReadFileState } from "../../tools/fs/safety.js";
15
15
  import type { Prepared } from "./contracts.js";
16
16
  export interface PrepareTurnWiringInput {
@@ -70,7 +70,7 @@ export interface PrepareTurnWiringInput {
70
70
  /** borrowed-readonly — the canonical extra read roots (the fence's root set). */
71
71
  additionalReadRootsCanonical: ReadonlyArray<string>;
72
72
  /** borrowed-readonly — the compiled read-deny judge, or undefined (the reader's withheld arm). */
73
- readDenyMatcher: ReadDenyMatcher | undefined;
73
+ readDenyMatcher: PlacedReadDenyMatcher | undefined;
74
74
  /** borrowed-readonly — the resolved read face, or undefined (the fence's ninth argument). */
75
75
  resolvedReadFace: ContainmentFace | undefined;
76
76
  /** borrowed-readonly — the tracked-cwd seat, read per call by the kept-tail normalizer (never written here). */
@@ -12,12 +12,14 @@ import { hasVerifiableStructureSignal } from "./grounding-signal.js";
12
12
  import { batchResponseDigest, toolEndBodyFrom } from "./tool-end-body.js";
13
13
  import { createResponseModelSeat } from "./response-model-seat.js";
14
14
  import { createToolCallIdFoldSeat } from "./tool-call-id-fold-seat.js";
15
+ import { createToolTurnIncompleteSeat } from "./tool-turn-repair-seat.js";
15
16
  import { wireOf } from "../tool-name-readings.js";
16
17
  import { rosterToolName } from "../tool-roster.js";
17
18
  export function createHarnessHandlers(input) {
18
19
  const { prepared, stats, rs, deps } = input;
19
20
  const responseModelSeat = createResponseModelSeat(deps.runnerHooks.onNotice, { sessionId: prepared.sessionId, runId: prepared.runId });
20
21
  const toolCallIdFoldSeat = createToolCallIdFoldSeat(deps.runnerHooks.onNotice, { sessionId: prepared.sessionId, runId: prepared.runId });
22
+ const toolTurnIncompleteSeat = createToolTurnIncompleteSeat(deps.runnerHooks.onNotice, { sessionId: prepared.sessionId, runId: prepared.runId });
21
23
  const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs, internalsNotifier } = deps;
22
24
  let lastWorkspaceCwd = prepared.cwdRef?.current;
23
25
  const announceWorkspaceMove = () => {
@@ -152,6 +154,7 @@ export function createHarnessHandlers(input) {
152
154
  if (m.role === "assistant") {
153
155
  rs.turn.turnResponseModel = responseModelSeat.observe(m);
154
156
  toolCallIdFoldSeat.observe(m);
157
+ toolTurnIncompleteSeat.observe(m);
155
158
  }
156
159
  if (m.role === "assistant") {
157
160
  const rep = m.repetition;
@@ -12,7 +12,7 @@ export interface RunIdentityWiringInput {
12
12
  * task_notification frames. */
13
13
  queue: PushQueue<TaskEvent>;
14
14
  /** borrowed-mutable — the leg's prepared seat: the harness sinks (`onUndrainedEngineNotes`, `engineInjectionsHeld`,
15
- * `onUndrainedUserInputs`, `onEngineNoteConsumed`, `onToolTurnRepaired`) are installed, `nextTurn` is appended to; the ids, the identity
15
+ * `onUndrainedUserInputs`, `onEngineNoteConsumed`) are installed, `nextTurn` is appended to; the ids, the identity
16
16
  * envelope, the hold refs, the memory session and the listing / roster faces are read. */
17
17
  prepared: Prepared;
18
18
  /** borrowed-readonly — the trusted run-scoped channel: `parentToolCallId`, `onNotifyInjectorReady`. */
@@ -3,7 +3,6 @@ import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js"
3
3
  import { recordFrameReceipt, toolResultReceiptKey } from "../frame-settlement.js";
4
4
  import { discloseDroppedPending, renderTaskNotificationXml, taskNotificationDedupKey } from "../task-notification.js";
5
5
  import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
6
- import { createToolTurnRepairSeat } from "./tool-turn-repair-seat.js";
7
6
  export function runIdentityWiring(input) {
8
7
  const { spec, queue, prepared, internals, taskIdRef, onReady, manualCompactRef, notifyRef, captureOptOutRef, mcpRedialRef, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef, notificationLane, pendingSessionNotifications, runner } = input;
9
8
  notificationLane.harness = prepared.harness;
@@ -85,7 +84,6 @@ export function runIdentityWiring(input) {
85
84
  if (key !== undefined)
86
85
  recordFrameReceipt(seam, key, carrier.kind === "tool_result" ? carrier.content : undefined, entryId);
87
86
  };
88
- prepared.harness.onToolTurnRepaired = createToolTurnRepairSeat((notice) => runner.deps.onNotice?.(notice), { sessionId: prepared.sessionId, runId: prepared.runId });
89
87
  if (notifyRef)
90
88
  notifyRef.inject = injectTaskNotification;
91
89
  if (captureOptOutRef && prepared.memoryEngineSession?.captureOptOut !== undefined) {
@@ -157,7 +157,7 @@ export async function runLeg(input) {
157
157
  let outcome;
158
158
  try {
159
159
  const seat = await runHookSeat("userPromptSubmit", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => parkedScreen(steer.text, {
160
- identity: prepared.hookIdentity,
160
+ identity: prepared.hookIdentity, permissionMode: prepared.permissionMode.observed,
161
161
  signal: sig,
162
162
  source: "parked_redelivery",
163
163
  ...(steer.inputId !== undefined ? { inputId: steer.inputId } : {}),
@@ -283,7 +283,7 @@ export async function runLeg(input) {
283
283
  const userPromptSubmit = (spec.hooks ?? runner.deps.hooks)?.userPromptSubmit;
284
284
  if (userPromptSubmit) {
285
285
  try {
286
- const promptSeat = await runHookSeat("userPromptSubmit", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => userPromptSubmit(spec.objective, { identity: prepared.hookIdentity, signal: sig, source: "objective", ...(objectiveActor !== undefined ? { actor: snapshotActorAssertion(objectiveActor) } : {}) }));
286
+ const promptSeat = await runHookSeat("userPromptSubmit", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => userPromptSubmit(spec.objective, { identity: prepared.hookIdentity, permissionMode: prepared.permissionMode.observed, signal: sig, source: "objective", ...(objectiveActor !== undefined ? { actor: snapshotActorAssertion(objectiveActor) } : {}) }));
287
287
  if (promptSeat.expired) {
288
288
  if (promptSeat.cause === "timeout") {
289
289
  try {
@@ -110,6 +110,7 @@ export function runStopAndFinalVerify(input) {
110
110
  getBranch: () => prepared.session.getBranch(),
111
111
  backgroundTasks: () => prepared.listBackgroundTasks(),
112
112
  identity: prepared.hookIdentity,
113
+ permissionMode: prepared.permissionMode.observed,
113
114
  signal: sig,
114
115
  }));
115
116
  if (stopSeat.expired) {
@@ -469,7 +469,7 @@ export function createTurnBoundary(input) {
469
469
  const injectedThisTurn = finalVerifyInjectedThisTurn ? "final_verification" : undefined;
470
470
  if (!boundarySteered && !prepared.abortController.signal.aborted) {
471
471
  try {
472
- const batchSeat = await runHookSeat("postToolBatch", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }, (sig) => postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined, { identity: prepared.hookIdentity, signal: sig }));
472
+ const batchSeat = await runHookSeat("postToolBatch", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }, (sig) => postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined, { identity: prepared.hookIdentity, permissionMode: prepared.permissionMode.observed, signal: sig }));
473
473
  if (batchSeat.expired) {
474
474
  runnerHooks.onError?.(hookSeatExpiredError("postToolBatch", prepared.hookTimeoutMs, batchSeat.cause, "the boundary observation was abandoned and its additionalContext dropped; the turn boundary itself is unchanged"), { phase: "hook", sessionId: prepared.sessionId });
475
475
  }
@@ -20,10 +20,19 @@ export interface LegModeCeiling {
20
20
  /** The spawning leg's live mode while it runs; the session's current leg's after it (`default` before any leg of
21
21
  * the session committed — no child can read it then). */
22
22
  readonly current: PermissionMode;
23
+ /** The same word read without starting anything (`PermissionModeController.observed` on the leg it reads). Optional on
24
+ * the TYPE only: the engine's ceilings always answer it, and a reader falls to `current` when a hand-built ceiling (a
25
+ * host's literal, a fixture) carries none — so the member's arrival is additive for every constructor of this shape. */
26
+ readonly observed?: PermissionMode;
23
27
  readonly sessionId: string;
24
28
  /** A child leg holds the ceiling until `signal` aborts (its own leg's end): each move of the ceiling re-judges it. */
25
29
  hold(holder: ModeCeilingHolder, signal: AbortSignal): void;
26
30
  }
31
+ /** A leg's mode seat as a ceiling reads it: the decision read and the observation read. */
32
+ type LegModeSeat = {
33
+ readonly current: PermissionMode;
34
+ readonly observed: PermissionMode;
35
+ };
27
36
  /**
28
37
  * The seats of one Runner, keyed by session id and held WEAKLY: a seat lives while a leg ceiling holds it, and the
29
38
  * next leg of the session finds the same object as long as any child still reads it. In-process only — a session
@@ -38,11 +47,10 @@ export declare class SessionModeSeats {
38
47
  * session's current leg) and its end (once the leg has STOPPED — its loop settled, not merely cancelled: a cancelled
39
48
  * leg whose tool is still pending keeps answering its own children).
40
49
  */
41
- legCeiling(sessionId: string, leg: {
42
- readonly current: PermissionMode;
43
- }): {
50
+ legCeiling(sessionId: string, leg: LegModeSeat): {
44
51
  ceiling: LegModeCeiling;
45
52
  commit: () => void;
46
53
  end: () => void;
47
54
  };
48
55
  }
56
+ export {};
@@ -8,6 +8,9 @@ class Seat {
8
8
  get current() {
9
9
  return this.leg?.current ?? "default";
10
10
  }
11
+ get observed() {
12
+ return this.leg?.observed ?? "default";
13
+ }
11
14
  addHolder(holder, via, signal) {
12
15
  if (signal.aborted)
13
16
  return;
@@ -36,6 +39,9 @@ class LegCeiling {
36
39
  get current() {
37
40
  return this.live ? this.leg.current : this.seat.current;
38
41
  }
42
+ get observed() {
43
+ return this.live ? this.leg.observed : (this.seat.observed ?? this.seat.current);
44
+ }
39
45
  hold(holder, signal) {
40
46
  this.seat.addHolder(holder, this, signal);
41
47
  }
@@ -136,11 +136,10 @@ export function createSessionRulePolicy(rules, opts) {
136
136
  nameSets: [{ ...(seed.toolDeny?.length ? { deny: [...seed.toolDeny] } : {}), ...(seed.toolAllow?.length ? { allow: [...seed.toolAllow] } : {}) }],
137
137
  async check(req, signal) {
138
138
  const { toolDeny, toolAllow, toolDenyCovering, toolAllowCovering, cmdPolicy, allowDirs } = compile(await readRules(req.toolCallId, signal));
139
- const toolName = req.toolName;
140
139
  const listed = (set) => heldBy("policy.sessionRules", set, req);
141
- if (listed(toolDeny) || namespacedCoveringHit(toolDenyCovering, toolName))
140
+ if (listed(toolDeny) || namespacedCoveringHit(toolDenyCovering, req))
142
141
  return deny(req.toolName);
143
- if (toolAllow && !listed(toolAllow) && !namespacedCoveringHit(toolAllowCovering, toolName)) {
142
+ if (toolAllow && !listed(toolAllow) && !namespacedCoveringHit(toolAllowCovering, req)) {
144
143
  return deny(req.toolName, "it is not in the session-rule allowlist");
145
144
  }
146
145
  if (cmdPolicy) {
@@ -94,7 +94,7 @@ export function streamSteerVerb(input) {
94
94
  if (screen !== undefined) {
95
95
  let decision;
96
96
  try {
97
- const seat = await runHookSeat("userPromptSubmit", { timeoutMs: h.hookTimeoutMs, signal: h.abortController.signal, abortEnds: true }, (sig) => screen(text, { identity: h.hookIdentity, signal: sig, source: "steer", inputId: effectiveInputId, ...(actor !== undefined ? { actor: snapshotActorAssertion(actor) } : {}) }));
97
+ const seat = await runHookSeat("userPromptSubmit", { timeoutMs: h.hookTimeoutMs, signal: h.abortController.signal, abortEnds: true }, (sig) => screen(text, { identity: h.hookIdentity, permissionMode: h.permissionMode.observed, signal: sig, source: "steer", inputId: effectiveInputId, ...(actor !== undefined ? { actor: snapshotActorAssertion(actor) } : {}) }));
98
98
  if (seat.expired) {
99
99
  if (seat.cause === "timeout") {
100
100
  try {
@@ -1,8 +1,13 @@
1
1
  import { deliverEngineNotice } from "../engine-notice.js";
2
- import { providerToolCallId, toolCallIdFoldReason } from "../../brain/tool-call-id.js";
2
+ import { providerToolCallId, toolCallFoldReason } from "../../brain/tool-call-id.js";
3
3
  function isCall(block) {
4
4
  return typeof block === "object" && block !== null && block.type === "toolCall" && typeof block.id === "string";
5
5
  }
6
+ const REASON_CLAUSE = {
7
+ charset: () => "named a tool call with an id outside the shape the engine keys calls under (a control byte, a space, a non-ASCII character or a `~`)",
8
+ length: (wireId) => `named a tool call with an id outside the shape the engine keys calls under (${wireId.length} characters, over 128)`,
9
+ duplicate: () => "gave two tool calls in one response the same id, and every provider requires a request's call ids to be unique",
10
+ };
6
11
  export function createToolCallIdFoldSeat(onNotice, ids) {
7
12
  let announced = false;
8
13
  return {
@@ -10,19 +15,21 @@ export function createToolCallIdFoldSeat(onNotice, ids) {
10
15
  if (announced || !Array.isArray(message.content))
11
16
  return;
12
17
  for (const call of message.content.filter(isCall)) {
13
- const wireId = providerToolCallId(call);
14
- const reason = wireId === call.id ? undefined : toolCallIdFoldReason(wireId);
18
+ const reason = toolCallFoldReason(call);
15
19
  if (reason === undefined)
16
20
  continue;
17
21
  announced = true;
22
+ const wireId = providerToolCallId(call);
18
23
  deliverEngineNotice(onNotice, {
19
24
  code: "route.tool_call_id_folded",
20
- message: `The provider named a tool call with an id outside the shape the engine keys calls under ` +
21
- `(${reason === "length" ? `${wireId.length} characters, over 128` : "a control byte, a space, a non-ASCII character or a `~`"}). ` +
22
- `The engine keys it as "${call.id}" on every face — frames, cards, transcript, park rows — and hands the ` +
23
- `provider its own id back for the call and its result, so the run is unchanged. Once per run; later calls ` +
24
- `with such ids are folded the same way without another notice.`,
25
- detail: { toolCallId: call.id, rawLength: wireId.length, reason, sessionId: ids.sessionId, runId: ids.runId },
25
+ message: `The provider ${REASON_CLAUSE[reason](wireId)}. ` +
26
+ (reason === "duplicate"
27
+ ? `The engine keys the later call as "${call.id}" on every face — frames, cards, transcript, park rows — and hands ` +
28
+ `the provider a distinct id for it and its result, so both calls run and both results reach the model. `
29
+ : `The engine keys it as "${call.id}" on every face — frames, cards, transcript, park rows — and hands the ` +
30
+ `provider its own id back for the call and its result, so the run is unchanged. `) +
31
+ `Once per run; later calls with such ids are folded the same way without another notice.`,
32
+ detail: { toolCallId: call.id, ...(reason === "duplicate" ? {} : { rawLength: wireId.length }), reason, sessionId: ids.sessionId, runId: ids.runId },
26
33
  });
27
34
  return;
28
35
  }
@@ -1,8 +1,18 @@
1
1
  import type { EngineNotice } from "../engine-notice.js";
2
- import type { ToolTurnRepair } from "../../internal/llm.js";
2
+ import { type ToolTurnIncompleteCause, type ToolTurnRepair } from "../../internal/llm.js";
3
3
  /** The notice's attribution ids — the same pair, for the same reason, as the fold seat's. */
4
4
  export interface ToolTurnRepairSeatIds {
5
5
  sessionId: string;
6
6
  runId: string;
7
7
  }
8
+ /** The person's line for one cause — once per run per cause, whichever observer saw it first. */
9
+ export interface ToolTurnIncompleteSeat {
10
+ /** Announce `count` items of `cause` (and, for a missing result, the calls) unless this run already said it. */
11
+ announce(cause: ToolTurnIncompleteCause, count: number, toolCallIds?: readonly string[]): void;
12
+ /** Observe one assistant message: announces each stream drop it reports (`streamDropsOf`). */
13
+ observe(message: {
14
+ readonly diagnostics?: unknown;
15
+ }): void;
16
+ }
17
+ export declare function createToolTurnIncompleteSeat(onNotice: ((notice: EngineNotice) => void) | undefined, ids: ToolTurnRepairSeatIds): ToolTurnIncompleteSeat;
8
18
  export declare function createToolTurnRepairSeat(onNotice: ((notice: EngineNotice) => void) | undefined, ids: ToolTurnRepairSeatIds): (repairs: readonly ToolTurnRepair[]) => void;