@sema-agent/core 7.18.0 → 7.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +276 -2
  2. package/dist/agents/subagent.js +12 -20
  3. package/dist/agents/teacher.js +2 -5
  4. package/dist/agents/verify.js +21 -10
  5. package/dist/brain/anthropic.js +8 -3
  6. package/dist/brain/open-responses.js +12 -4
  7. package/dist/brain/openai.js +7 -2
  8. package/dist/brain/reasoning.d.ts +13 -13
  9. package/dist/brain/reasoning.js +2 -2
  10. package/dist/brain/stream-engine.d.ts +4 -0
  11. package/dist/brain/stream-engine.js +6 -1
  12. package/dist/brain/stream-shared.d.ts +3 -0
  13. package/dist/brain/stream-shared.js +1 -0
  14. package/dist/core/ask-origin.d.ts +16 -0
  15. package/dist/core/ask-origin.js +8 -0
  16. package/dist/core/auto-mode-arming.d.ts +6 -0
  17. package/dist/core/auto-mode-arming.js +5 -5
  18. package/dist/core/auto-mode-defaults.d.ts +43 -15
  19. package/dist/core/auto-mode-defaults.js +4 -1
  20. package/dist/core/auto-mode.d.ts +74 -26
  21. package/dist/core/auto-mode.js +80 -11
  22. package/dist/core/checkpoint-store.d.ts +7 -0
  23. package/dist/core/checkpoint-store.js +2 -2
  24. package/dist/core/child-clamp-seats.d.ts +79 -0
  25. package/dist/core/child-clamp-seats.js +29 -0
  26. package/dist/core/engine-notice.d.ts +89 -41
  27. package/dist/core/fs-write-gate-policy.d.ts +17 -9
  28. package/dist/core/fs-write-gate-policy.js +51 -4
  29. package/dist/core/gate-bounded-await.d.ts +38 -0
  30. package/dist/core/gate-bounded-await.js +53 -0
  31. package/dist/core/gate-fold.js +20 -48
  32. package/dist/core/gate-lanes.js +17 -9
  33. package/dist/core/gate-outcome.d.ts +34 -6
  34. package/dist/core/gate-outcome.js +7 -0
  35. package/dist/core/governance-codes.d.ts +16 -1
  36. package/dist/core/governance-codes.js +18 -0
  37. package/dist/core/hooks.d.ts +8 -8
  38. package/dist/core/memory-engine/engine.d.ts +14 -9
  39. package/dist/core/memory-engine/engine.js +21 -8
  40. package/dist/core/memory-engine/layout.d.ts +31 -0
  41. package/dist/core/memory-engine/layout.js +68 -3
  42. package/dist/core/memory-engine/types.d.ts +24 -0
  43. package/dist/core/memory.d.ts +19 -0
  44. package/dist/core/memory.js +12 -0
  45. package/dist/core/read-root-candidate.d.ts +12 -0
  46. package/dist/core/read-root-candidate.js +24 -0
  47. package/dist/core/remote-env.d.ts +32 -0
  48. package/dist/core/remote-env.js +4 -1
  49. package/dist/core/roles.d.ts +9 -1
  50. package/dist/core/roles.js +4 -0
  51. package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
  52. package/dist/core/runner/advertised-writable-dirs.js +51 -3
  53. package/dist/core/runner/announce-once-ledger.d.ts +15 -0
  54. package/dist/core/runner/announce-once-ledger.js +8 -0
  55. package/dist/core/runner/classifier-ladder.d.ts +154 -11
  56. package/dist/core/runner/classifier-ladder.js +170 -10
  57. package/dist/core/runner/contracts.d.ts +46 -1
  58. package/dist/core/runner/denial-limit-arms.d.ts +14 -1
  59. package/dist/core/runner/denial-limit-arms.js +7 -6
  60. package/dist/core/runner/env-capability-census.d.ts +30 -0
  61. package/dist/core/runner/env-capability-census.js +38 -0
  62. package/dist/core/runner/forward-sink.d.ts +32 -0
  63. package/dist/core/runner/forward-sink.js +41 -0
  64. package/dist/core/runner/gate-exit.d.ts +4 -0
  65. package/dist/core/runner/gate-exit.js +4 -1
  66. package/dist/core/runner/git-status-frame.d.ts +0 -9
  67. package/dist/core/runner/listing-fold.d.ts +57 -0
  68. package/dist/core/runner/listing-fold.js +75 -0
  69. package/dist/core/runner/prepare-ask-lane.js +1 -0
  70. package/dist/core/runner/prepare-caps-and-workflow.js +20 -31
  71. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  72. package/dist/core/runner/prepare-config-doors.js +8 -10
  73. package/dist/core/runner/prepare-execution-env.d.ts +1 -1
  74. package/dist/core/runner/prepare-execution-env.js +2 -0
  75. package/dist/core/runner/prepare-gate-stations.d.ts +8 -0
  76. package/dist/core/runner/prepare-gate-stations.js +6 -2
  77. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  78. package/dist/core/runner/prepare-hands-readface.js +3 -5
  79. package/dist/core/runner/prepare-listings.d.ts +3 -0
  80. package/dist/core/runner/prepare-listings.js +4 -2
  81. package/dist/core/runner/prepare-memory.js +7 -1
  82. package/dist/core/runner/prepare-policy-chain.js +5 -5
  83. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  84. package/dist/core/runner/prepare-project-context.js +3 -2
  85. package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
  86. package/dist/core/runner/prepare-run-refs.d.ts +4 -2
  87. package/dist/core/runner/prepare-run-refs.js +5 -4
  88. package/dist/core/runner/prepare-task.js +8 -6
  89. package/dist/core/runner/resume-apply.js +3 -3
  90. package/dist/core/runner/run-clock-and-content.js +3 -7
  91. package/dist/core/runner/run-harness-handlers.js +10 -10
  92. package/dist/core/runner/run-leg.js +31 -16
  93. package/dist/core/runner/run-settle-and-teardown.js +2 -5
  94. package/dist/core/runner/run-turn-boundary.js +44 -25
  95. package/dist/core/runner/runtask.js +2 -1
  96. package/dist/core/runner/synthetic-tools.d.ts +41 -29
  97. package/dist/core/runner/synthetic-tools.js +25 -40
  98. package/dist/core/runner/task-root-canonical.d.ts +45 -0
  99. package/dist/core/runner/task-root-canonical.js +18 -0
  100. package/dist/core/runner/turn-attachments.d.ts +160 -53
  101. package/dist/core/runner/turn-attachments.js +153 -51
  102. package/dist/core/safe-notify.d.ts +5 -0
  103. package/dist/core/safe-notify.js +24 -2
  104. package/dist/core/sensitive-path-policy.d.ts +84 -17
  105. package/dist/core/sensitive-path-policy.js +37 -19
  106. package/dist/core/skill-listing-budget.d.ts +106 -0
  107. package/dist/core/skill-listing-budget.js +57 -0
  108. package/dist/core/skills-directory.js +17 -10
  109. package/dist/core/task-registry.js +2 -1
  110. package/dist/core/task-spec.d.ts +29 -2
  111. package/dist/core/tool-policy.d.ts +31 -5
  112. package/dist/core/tool-policy.js +45 -32
  113. package/dist/core/tool-spec.d.ts +37 -4
  114. package/dist/core/types.d.ts +1 -1
  115. package/dist/core/write-protect.d.ts +105 -20
  116. package/dist/core/write-protect.js +89 -32
  117. package/dist/index.d.ts +8 -7
  118. package/dist/index.js +6 -5
  119. package/dist/orchestration/workflow-primitives.js +10 -20
  120. package/dist/stores/file/memory-store.d.ts +5 -2
  121. package/dist/stores/file/memory-store.js +8 -4
  122. package/dist/tools/fs/bash-readonly-classifier.d.ts +40 -2
  123. package/dist/tools/fs/bash-readonly-classifier.js +376 -41
  124. package/dist/tools/fs/fs-bash.d.ts +1 -1
  125. package/dist/tools/fs/fs-bash.js +19 -28
  126. package/dist/tools/fs/fs-shared.d.ts +22 -0
  127. package/dist/tools/fs/fs-shared.js +30 -0
  128. package/dist/tools/fs/index.d.ts +1 -1
  129. package/dist/tools/fs/index.js +1 -1
  130. package/dist/tools/fs/read-deny.d.ts +4 -3
  131. package/dist/tools/fs/safety.d.ts +15 -0
  132. package/dist/tools/fs/safety.js +7 -0
  133. package/package.json +2 -1
  134. package/test/export-surface.snapshot.json +31 -5
  135. package/wire-changes/core@7.18.0.json +1273 -0
  136. package/wire-changes/core@7.19.0.json +457 -0
  137. package/wire-changes/core@7.20.0.json +568 -0
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { isAbsolute } from "node:path";
3
3
  import { applyParentSpecSeat, parentSpecSeatOfCtx } from "../core/parent-spec-seat.js";
4
+ import { childClampSeats } from "../core/child-clamp-seats.js";
4
5
  import { stripSpawnTurnQuestionFace } from "../core/ask-question.js";
5
6
  import { withDelegationProvenance } from "../core/tool-policy.js";
6
7
  import { errorResult } from "../core/tools.js";
@@ -20,6 +21,7 @@ import { SUBAGENT_PROMPT } from "../prompts/default.js";
20
21
  import { hasSessionFork } from "../core/session.js";
21
22
  import { isDurablePause, pauseFamily } from "./suspend-guard.js";
22
23
  import { terminalProjection } from "../core/runner/terminal-projection.js";
24
+ import { forwardSafely } from "../core/runner/forward-sink.js";
23
25
  import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
24
26
  import { createSafeNotifier } from "../core/safe-notify.js";
25
27
  import { boundedRedactedSummary } from "../core/untrusted-egress.js";
@@ -556,13 +558,13 @@ export function createSubagentResume(deps) {
556
558
  sessionId: entry.childSessionId,
557
559
  requireExistingSession: true,
558
560
  ...(deps.currentOnQuestion !== undefined ? { onQuestion: deps.currentOnQuestion } : {}),
559
- ...(entry.specSnapshot.handsReadOnly === true || deps.currentClamps?.handsReadOnly === true ? { handsReadOnly: true } : {}),
560
- ...(entry.specSnapshot.readFace === "roots" || deps.currentClamps?.readFaceRoots === true ? { readFace: "roots" } : {}),
561
- ...(entry.specSnapshot.writeFace === "roots" || deps.currentClamps?.writeFaceRoots === true ? { writeFace: "roots" } : {}),
562
- ...(entry.specSnapshot.readDenyPatterns !== undefined || deps.currentClamps?.readDenyPatterns !== undefined
563
- ? { readDenyPatterns: [...(entry.specSnapshot.readDenyPatterns ?? []), ...(deps.currentClamps?.readDenyPatterns ?? [])] }
564
- : {}),
565
- ...(entry.specSnapshot.interactiveTools === false || deps.currentClamps?.interactiveTools === false ? { interactiveTools: false } : {}),
561
+ ...childClampSeats({
562
+ handsReadOnly: entry.specSnapshot.handsReadOnly === true || deps.currentClamps?.handsReadOnly === true,
563
+ interactiveTools: entry.specSnapshot.interactiveTools === false || deps.currentClamps?.interactiveTools === false ? false : undefined,
564
+ readFace: entry.specSnapshot.readFace === "roots" || deps.currentClamps?.readFaceRoots === true ? "roots" : entry.specSnapshot.readFace,
565
+ writeFace: entry.specSnapshot.writeFace === "roots" || deps.currentClamps?.writeFaceRoots === true ? "roots" : entry.specSnapshot.writeFace,
566
+ readDenyPatterns: [...(entry.specSnapshot.readDenyPatterns ?? []), ...(deps.currentClamps?.readDenyPatterns ?? [])],
567
+ }),
566
568
  ...(entry.specSnapshot.oneShot === true || deps.currentClamps?.oneShot === true ? { oneShot: true } : {}),
567
569
  ...(entry.specSnapshot.memoryPersistenceCapable === false || deps.currentClamps?.memoryPersistenceCapable === false
568
570
  ? { memoryPersistenceCapable: false }
@@ -643,11 +645,7 @@ export function createSubagentResume(deps) {
643
645
  ? {
644
646
  onForwardEvent: (e) => {
645
647
  resumeStepRecorder.record(e);
646
- try {
647
- entry.internalsSnapshot.onForwardEvent?.(deps.taskId !== undefined ? { ...e, bgAgentId: deps.taskId } : e);
648
- }
649
- catch {
650
- }
648
+ forwardSafely(entry.internalsSnapshot, deps.taskId !== undefined ? { ...e, bgAgentId: deps.taskId } : e);
651
649
  reviveActivityBeat(e);
652
650
  if (reviveEmit !== undefined && e.type === "task_progress") {
653
651
  reviveEmit({
@@ -1904,9 +1902,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1904
1902
  ...(ctx.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...ctx.alwaysLoadTools] } : {}),
1905
1903
  ...(ctx.restoreGatedTools !== undefined ? { restoreGatedTools: ctx.restoreGatedTools === true ? true : [...ctx.restoreGatedTools] } : {}),
1906
1904
  ...(ctx.promptProfile !== undefined ? { promptProfile: ctx.promptProfile } : {}),
1907
- ...(ctx.readFace === "roots" ? { readFace: "roots" } : {}),
1908
- ...(ctx.writeFace === "roots" ? { writeFace: "roots" } : {}),
1909
- ...(ctx.readDenyPatterns !== undefined ? { readDenyPatterns: [...ctx.readDenyPatterns] } : {}),
1905
+ ...childClampSeats({ handsReadOnly: ctx.handsReadOnly, interactiveTools: ctx.interactiveTools, readFace: ctx.readFace, writeFace: ctx.writeFace, readDenyPatterns: ctx.readDenyPatterns }),
1910
1906
  enableBlockedReport: false,
1911
1907
  limits: {
1912
1908
  maxTurns: typeof observerDef.maxTurns === "number" && Number.isFinite(observerDef.maxTurns) && observerDef.maxTurns > 0 ? observerDef.maxTurns : 8,
@@ -2160,11 +2156,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2160
2156
  ...(def?.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: def.memoryPersistenceCapable } : {}),
2161
2157
  ...(ctx.checkpointStoreForChildren !== undefined ? { checkpointStore: ctx.checkpointStoreForChildren } : {}),
2162
2158
  ...(childOnAsk !== undefined ? { onAsk: childOnAsk } : {}),
2163
- ...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
2164
- ...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
2165
- ...(ctx.readFace === "roots" ? { readFace: "roots" } : {}),
2166
- ...(ctx.writeFace === "roots" ? { writeFace: "roots" } : {}),
2167
- ...(ctx.readDenyPatterns !== undefined ? { readDenyPatterns: [...ctx.readDenyPatterns] } : {}),
2159
+ ...childClampSeats({ handsReadOnly: ctx.handsReadOnly, interactiveTools: ctx.interactiveTools, readFace: ctx.readFace, writeFace: ctx.writeFace, readDenyPatterns: ctx.readDenyPatterns }),
2168
2160
  ...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
2169
2161
  ...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
2170
2162
  ...(ctx.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...ctx.alwaysLoadTools] } : {}),
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { MAX_STRATEGY_INJECTION_TOTAL_BYTES, MAX_STRATEGY_PROBLEM_BYTES, MAX_STRATEGY_TEXT_BYTES } from "../core/strategy-store.js";
3
3
  import { createSafeNotifier, observeThenableRejection } from "../core/safe-notify.js";
4
+ import { childClampSeats } from "../core/child-clamp-seats.js";
4
5
  import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
5
6
  import { isDefineToolProduct, rebindDefineToolCtx, stampDefineToolBrand } from "../core/tools.js";
6
7
  import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
@@ -255,11 +256,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
255
256
  ...(studentSpec.principal !== undefined ? { principal: studentSpec.principal } : {}),
256
257
  ...(studentSpec.clientContext !== undefined ? { clientContext: studentSpec.clientContext } : {}),
257
258
  ...(studentSpec.promptProfile !== undefined ? { promptProfile: studentSpec.promptProfile } : {}),
258
- ...(studentSpec.handsReadOnly === true ? { handsReadOnly: true } : {}),
259
- ...(studentSpec.readFace === "roots" ? { readFace: "roots" } : {}),
260
- ...(studentSpec.writeFace === "roots" ? { writeFace: "roots" } : {}),
261
- ...(studentSpec.readDenyPatterns !== undefined ? { readDenyPatterns: [...studentSpec.readDenyPatterns] } : {}),
262
- ...(studentSpec.interactiveTools === false ? { interactiveTools: false } : {}),
259
+ ...childClampSeats({ handsReadOnly: studentSpec.handsReadOnly, interactiveTools: studentSpec.interactiveTools, readFace: studentSpec.readFace, writeFace: studentSpec.writeFace, readDenyPatterns: studentSpec.readDenyPatterns }),
263
260
  ...(studentSpec.excludeTools !== undefined
264
261
  ? { excludeTools: Array.isArray(studentSpec.excludeTools) ? [...studentSpec.excludeTools] : studentSpec.excludeTools }
265
262
  : {}),
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { CODE_AGENT_PROMPT } from "../prompts/default.js";
3
3
  import { releaseSession } from "./session-util.js";
4
+ import { childClampSeats } from "../core/child-clamp-seats.js";
4
5
  import { mapNestedSuspend, isDurablePause, pauseFamily } from "./suspend-guard.js";
5
6
  import { delimitUntrusted, sanitizeUntrustedText } from "../core/untrusted-text.js";
6
7
  import { createSafeNotifier } from "../core/safe-notify.js";
@@ -136,17 +137,21 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
136
137
  const runVerifier = async (impl, round) => {
137
138
  refuseUnhonorableInternals(internals, "before_leg");
138
139
  const roundEvidence = round === 1 ? config.evidence : undefined;
139
- const v = await runner.runTask({
140
+ const verifierSpec = {
140
141
  objective: verifierObjective(objective, impl, roundEvidence),
141
142
  systemPrompt: verifierPrompt,
142
143
  ...(config.verifierModel ? { model: config.verifierModel } : { modelRole: "verifier" }),
143
144
  roles: specBase.roles,
144
145
  tools: verifierTools,
145
146
  handsReadOnly: config.verifierHandsReadOnly ?? true,
146
- ...(foldedRootsFace ? { readFace: "roots" } : {}),
147
- ...(foldedRootsWriteFace ? { writeFace: "roots" } : {}),
148
- ...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
149
147
  interactiveTools: false,
148
+ ...childClampSeats({
149
+ handsReadOnly: specBase.handsReadOnly,
150
+ interactiveTools: specBase.interactiveTools,
151
+ readFace: foldedRootsFace ? "roots" : undefined,
152
+ writeFace: foldedRootsWriteFace ? "roots" : undefined,
153
+ readDenyPatterns: foldedReadDeny,
154
+ }),
150
155
  outputSchema: VerdictSchema,
151
156
  enableBlockedReport: false,
152
157
  limits: { ...(specBase.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: specBase.limits.maxWalltimeMs } : {}) },
@@ -167,7 +172,8 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
167
172
  ...(specBase.clientContext !== undefined ? { clientContext: { ...specBase.clientContext } } : {}),
168
173
  ...(specBase.promptProfile !== undefined ? { promptProfile: specBase.promptProfile } : {}),
169
174
  signal: specBase.signal,
170
- }, legInternals());
175
+ };
176
+ const v = await runner.runTask(verifierSpec, legInternals());
171
177
  try {
172
178
  const cost = (v.stats.costMicroUsd ?? 0) + (v.stats.nested?.costMicroUsd ?? 0);
173
179
  return { verdict: v.structuredOutput, cost, ...(isDurablePause(v.terminal) ? { paused: { ...v, terminal: v.terminal } } : {}) };
@@ -241,14 +247,19 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
241
247
  if (config.costCeilingMicroUsd != null && spend >= config.costCeilingMicroUsd)
242
248
  break;
243
249
  refuseUnhonorableInternals(internals, "before_leg");
244
- current = await runner.runTask({
250
+ const fixLegSpec = {
245
251
  ...specBase,
246
252
  sessionId: current.sessionId,
247
253
  objective: fixObjective(outcome.findings),
248
- ...(foldedRootsFace ? { readFace: "roots" } : {}),
249
- ...(foldedRootsWriteFace ? { writeFace: "roots" } : {}),
250
- ...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
251
- }, legInternals());
254
+ ...childClampSeats({
255
+ handsReadOnly: specBase.handsReadOnly,
256
+ interactiveTools: specBase.interactiveTools,
257
+ readFace: foldedRootsFace ? "roots" : specBase.readFace,
258
+ writeFace: foldedRootsWriteFace ? "roots" : specBase.writeFace,
259
+ readDenyPatterns: foldedReadDeny,
260
+ }),
261
+ };
262
+ current = await runner.runTask(fixLegSpec, legInternals());
252
263
  spend += (current.stats.costMicroUsd ?? 0) + (current.stats.nested?.costMicroUsd ?? 0);
253
264
  implAccount.add(current.stats);
254
265
  if (isDurablePause(current.terminal)) {
@@ -2,7 +2,7 @@ import {} from "../internal/llm.js";
2
2
  import { BrainError } from "./errors.js";
3
3
  import { startsWithInputTooLongAnchor } from "./input-too-long.js";
4
4
  import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
5
- import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
5
+ import { STREAM_TORN_WARNING, createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
6
6
  import { mintFallbackToolCallId } from "./tool-call-id.js";
7
7
  import { emitBrainTelemetry, reportReasoningWireFacts } from "./status-sink.js";
8
8
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, modelSupportsVision, sendableImages } from "./media-degrade.js";
@@ -365,6 +365,7 @@ export function createAnthropicBrain(config = {}) {
365
365
  const blocks = new Map();
366
366
  const usageRaw = {};
367
367
  let stopReason = null;
368
+ const terminalFrameSeen = () => stopReason != null;
368
369
  let stopDetails;
369
370
  let nextContentIndex = 0;
370
371
  let malformedFrames = 0;
@@ -532,6 +533,7 @@ export function createAnthropicBrain(config = {}) {
532
533
  hasSubstantiveText,
533
534
  hasCompletedToolCall,
534
535
  hasOnlyThinking: hasThinking && !hasSubstantiveText && !hasCompletedToolCall,
536
+ terminalFrameSeen: terminalFrameSeen(),
535
537
  };
536
538
  },
537
539
  sealForRetry() {
@@ -560,7 +562,7 @@ export function createAnthropicBrain(config = {}) {
560
562
  }
561
563
  handleEvent(evt);
562
564
  },
563
- finalize() {
565
+ finalize(opts) {
564
566
  const finalContent = [];
565
567
  const toolCalls = [];
566
568
  const malformed = [];
@@ -666,6 +668,8 @@ export function createAnthropicBrain(config = {}) {
666
668
  emptyNoFinish ||
667
669
  (toolError !== undefined && noUsableContent) ||
668
670
  (truncatedEmpty && !dynamicCut && !staticReasoningCut);
671
+ const tornWithContent = !errored && opts?.truncated !== true && !terminalFrameSeen() && !noUsableContent && !degenerate;
672
+ const errorMessageOut = tornWithContent ? (errorMessage ? `${errorMessage}\n${STREAM_TORN_WARNING}` : STREAM_TORN_WARNING) : errorMessage;
669
673
  const errorKind = degenerate
670
674
  ? "degenerate"
671
675
  : !safetyCut && contextWindowExceeded
@@ -680,8 +684,9 @@ export function createAnthropicBrain(config = {}) {
680
684
  usage: computeUsage(model, usageRaw),
681
685
  ...(Object.keys(usageRaw).length === 0 ? { usageMissing: true } : {}),
682
686
  timestamp: Date.now(),
683
- ...(errorMessage ? { errorMessage } : {}),
687
+ ...(errorMessageOut ? { errorMessage: errorMessageOut } : {}),
684
688
  ...(errorKind ? { errorKind } : {}),
689
+ ...(tornWithContent ? { partialFinalized: true } : {}),
685
690
  ...(rep.cut !== undefined || rep.spared.length > 0
686
691
  ? { repetition: { ...(rep.cut !== undefined ? { cut: rep.cut } : {}), ...(rep.spared.length > 0 ? { spared: rep.spared } : {}) } }
687
692
  : {}),
@@ -1,7 +1,7 @@
1
1
  import {} from "../internal/llm.js";
2
2
  import { BrainError } from "./errors.js";
3
3
  import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
4
- import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
4
+ import { STREAM_TORN_WARNING, createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
6
  import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
@@ -195,6 +195,7 @@ function responsesErrorCode(code) {
195
195
  }
196
196
  const INCOMPLETE_MAX_OUTPUT_TOKENS = "max_output_tokens";
197
197
  const INCOMPLETE_CONTENT_FILTER = "content_filter";
198
+ const RESPONSE_INCOMPLETE_WARNING = 'The response above may be incomplete: the endpoint reported status "incomplete" with no reason this client recognises.';
198
199
  function closeFunctionCall(acc) {
199
200
  if (acc.closedTc)
200
201
  return acc.closedTc;
@@ -302,6 +303,7 @@ export function createOpenResponsesBrain(config = {}) {
302
303
  let usageRaw;
303
304
  let responseId;
304
305
  let terminalStatus;
306
+ const terminalFrameSeen = () => terminalStatus !== undefined;
305
307
  let incompleteReason;
306
308
  let accumRefusal = "";
307
309
  const capabilityNotes = [];
@@ -572,6 +574,7 @@ export function createOpenResponsesBrain(config = {}) {
572
574
  hasSubstantiveText,
573
575
  hasCompletedToolCall,
574
576
  hasOnlyThinking: hasReasoning && !hasSubstantiveText && !hasCompletedToolCall,
577
+ terminalFrameSeen: terminalFrameSeen(),
575
578
  };
576
579
  },
577
580
  sealForRetry() {
@@ -688,7 +691,8 @@ export function createOpenResponsesBrain(config = {}) {
688
691
  const staticReasoningCut = truncatedEmpty && !dynamicCut && reasoningSeen && options?.staticReasoningCutDowngrade === true;
689
692
  const safetyCut = incompleteReason === INCOMPLETE_CONTENT_FILTER;
690
693
  const refused = accumRefusal.trim().length > 0;
691
- const emptyNoFinish = noUsableContent && terminalStatus === undefined && !degenerate;
694
+ const incompleteUnmapped = terminalStatus === "incomplete" && incompleteReason !== INCOMPLETE_MAX_OUTPUT_TOKENS && incompleteReason !== INCOMPLETE_CONTENT_FILTER;
695
+ const emptyNoFinish = noUsableContent && (!terminalFrameSeen() || incompleteUnmapped) && !degenerate;
692
696
  const maxTokensNote = sentMaxTokens !== undefined
693
697
  ? `effective max_output_tokens=${sentMaxTokens} (from ${sentMaxTokensLane === "options"
694
698
  ? "options.maxTokens"
@@ -708,7 +712,7 @@ export function createOpenResponsesBrain(config = {}) {
708
712
  ? `[length_empty] response truncated at max_output_tokens (${maxTokensNote}) with no answer text — the model spent the whole budget on reasoning; raise the cap, or lower the reasoning effort for this task.`
709
713
  : `[length_empty] response truncated at max_output_tokens (${maxTokensNote}) with no answer text — raise the cap`
710
714
  : emptyNoFinish
711
- ? `[stream_torn] model stream ended with no content and no terminal response event — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
715
+ ? `[stream_torn] model stream ended with no content and ${terminalStatus === undefined ? "no terminal response event" : `a terminal status "${terminalStatus}" with no reason this client recognises (incomplete_details.reason=${incompleteReason ?? "absent"})`} — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
712
716
  : undefined));
713
717
  const errored = degenerate ||
714
718
  safetyCut ||
@@ -716,6 +720,9 @@ export function createOpenResponsesBrain(config = {}) {
716
720
  emptyNoFinish ||
717
721
  (toolError !== undefined && noUsableContent) ||
718
722
  (truncatedEmpty && !dynamicCut && !staticReasoningCut);
723
+ const tornWithContent = !errored && opts?.truncated !== true && (!terminalFrameSeen() || incompleteUnmapped) && !noUsableContent && !degenerate;
724
+ const partialWarning = terminalStatus === undefined ? STREAM_TORN_WARNING : RESPONSE_INCOMPLETE_WARNING;
725
+ const errorMessageOut = tornWithContent ? (errorMessage ? `${errorMessage}\n${partialWarning}` : partialWarning) : errorMessage;
719
726
  const errorKind = degenerate
720
727
  ? "degenerate"
721
728
  : !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut && !staticReasoningCut
@@ -732,8 +739,9 @@ export function createOpenResponsesBrain(config = {}) {
732
739
  ...(usageRaw === undefined ? { usageMissing: true } : {}),
733
740
  timestamp: Date.now(),
734
741
  ...(responseId !== undefined ? { responseId } : {}),
735
- ...(errorMessage ? { errorMessage } : {}),
742
+ ...(errorMessageOut ? { errorMessage: errorMessageOut } : {}),
736
743
  ...(errorKind ? { errorKind } : {}),
744
+ ...(tornWithContent ? { partialFinalized: true } : {}),
737
745
  ...(rep.cut !== undefined || rep.spared.length > 0
738
746
  ? { repetition: { ...(rep.cut !== undefined ? { cut: rep.cut } : {}), ...(rep.spared.length > 0 ? { spared: rep.spared } : {}) } }
739
747
  : {}),
@@ -1,7 +1,7 @@
1
1
  import {} from "../internal/llm.js";
2
2
  import { repairTextToolCalls } from "./tool-call-repair.js";
3
3
  import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
4
- import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
4
+ import { STREAM_TORN_WARNING, createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
6
  import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
@@ -373,6 +373,7 @@ export function createOpenAIBrain(config = {}) {
373
373
  let textIndex = 0;
374
374
  const toolAccum = new Map();
375
375
  let finishReason = null;
376
+ const terminalFrameSeen = () => finishReason != null;
376
377
  let usageRaw;
377
378
  let malformedFrames = 0;
378
379
  let malformedSample;
@@ -438,6 +439,7 @@ export function createOpenAIBrain(config = {}) {
438
439
  hasSubstantiveText,
439
440
  hasCompletedToolCall,
440
441
  hasOnlyThinking: accumReasoning.length > 0 && !hasSubstantiveText && !hasCompletedToolCall,
442
+ terminalFrameSeen: terminalFrameSeen(),
441
443
  };
442
444
  },
443
445
  sealForRetry() {
@@ -619,6 +621,8 @@ export function createOpenAIBrain(config = {}) {
619
621
  emptyNoFinish ||
620
622
  (toolError !== undefined && noUsableContent) ||
621
623
  (truncatedEmpty && !dynamicCut);
624
+ const tornWithContent = !errored && opts?.truncated !== true && !terminalFrameSeen() && !noUsableContent && !degenerate;
625
+ const errorMessageOut = tornWithContent ? (errorMessage ? `${errorMessage}\n${STREAM_TORN_WARNING}` : STREAM_TORN_WARNING) : errorMessage;
622
626
  const errorKind = degenerate
623
627
  ? "degenerate"
624
628
  : !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut
@@ -631,8 +635,9 @@ export function createOpenAIBrain(config = {}) {
631
635
  usage: computeUsage(model, usageRaw),
632
636
  ...(usageRaw === undefined ? { usageMissing: true } : {}),
633
637
  timestamp: Date.now(),
634
- ...(errorMessage ? { errorMessage } : {}),
638
+ ...(errorMessageOut ? { errorMessage: errorMessageOut } : {}),
635
639
  ...(errorKind ? { errorKind } : {}),
640
+ ...(tornWithContent ? { partialFinalized: true } : {}),
636
641
  ...(rep.cut !== undefined || rep.spared.length > 0
637
642
  ? { repetition: { ...(rep.cut !== undefined ? { cut: rep.cut } : {}), ...(rep.spared.length > 0 ? { spared: rep.spared } : {}) } }
638
643
  : {}),
@@ -117,25 +117,25 @@ export interface OffCapabilityModel {
117
117
  * "none" tier), on an endpoint that takes an effort value at all (`supportsReasoningEffort` not
118
118
  * false) — else `undefined`: there is nothing to send. The effort formats have no disable key, and an
119
119
  * effort VALUE is never off (it enables reasoning at that tier), so off on those wires is spelled only
120
- * when the deployment says how. The two appliers and {@link thinkingOffExpressible} read this ONE
120
+ * when the deployment says how. The two appliers and {@link thinkingOffSpelled} read this ONE
121
121
  * function, so what the wire carries and what the capability table claims cannot disagree.
122
122
  */
123
123
  export declare function declaredOffSpelling(model: OffCapabilityModel): string | undefined;
124
124
  /**
125
- * Whether this model's wire can SAY "off". A model that does not reason at all (`reasoning` falsy)
126
- * never thinks, so off is trivially expressible. Otherwise, per wire: the anthropic budget form spells
127
- * `{type:"disabled"}` (the ADAPTIVE form rejects it — those models think on every request); deepseek
128
- * `{type:"disabled"}`, the binary enable keys at `false`, openrouter/together `reasoning:{enabled:false}`
129
- * all say it; the effort-only wires (stock openai completions, the Responses lane) say it only through
130
- * a deployment-declared off spelling ({@link declaredOffSpelling}).
125
+ * Whether the wire will CARRY a disable key for this model — off is SPELLED, never assumed. Per wire: the
126
+ * anthropic budget form spells `{type:"disabled"}` (the ADAPTIVE form rejects it — those models think on
127
+ * every request); deepseek `{type:"disabled"}`, the binary enable keys at `false`, openrouter/together
128
+ * `reasoning:{enabled:false}` all spell it; the effort-only wires (stock openai completions, the Responses
129
+ * lane) spell it only through a deployment-declared off spelling ({@link declaredOffSpelling}).
131
130
  *
132
- * A caller that sizes an output cap for an ANSWER alone (the auto-mode classifier's block-verdict
133
- * contract) applies the cap only when this is true: a model that will think regardless cannot have its
134
- * cap derived from the answer's contract, and keeps its own budget. What this table cannot see is a
135
- * model that IGNORES its wire's disable key (a thinking-only build served through a format that has
136
- * one) — that is a per-model fact the compat declaration does not yet carry.
131
+ * A model whose `reasoning` is FALSY spells NOTHING: every applier returns before writing any key for it, so
132
+ * the request is the gateway's own default — and a default-on gateway thinks regardless (three of four live
133
+ * endpoints, 2026-09-15). This arm used to answer `true` ("a non-reasoning model never thinks"): the engine
134
+ * may size a request by what it SENT, never by what it hopes a gateway does when told nothing. What this
135
+ * table cannot see is a gateway that IGNORES the key it was sent — the classifier's re-ask on a no-verdict
136
+ * reply is the seat that learns that from the reply.
137
137
  */
138
- export declare function thinkingOffExpressible(model: OffCapabilityModel): boolean;
138
+ export declare function thinkingOffSpelled(model: OffCapabilityModel): boolean;
139
139
  /**
140
140
  * OPTIONAL per-request facts for {@link resolveReasoning} — what the wire's budget arm knows at
141
141
  * request build that a per-leg eager resolution cannot: the resolved output cap and whether it is a
@@ -30,9 +30,9 @@ export function declaredOffSpelling(model) {
30
30
  const mapped = model.thinkingLevelMap?.off;
31
31
  return typeof mapped === "string" && mapped !== "" ? mapped : undefined;
32
32
  }
33
- export function thinkingOffExpressible(model) {
33
+ export function thinkingOffSpelled(model) {
34
34
  if (!model.reasoning)
35
- return true;
35
+ return false;
36
36
  const compat = (model.compat ?? {});
37
37
  if (model.api === "anthropic-messages")
38
38
  return compat.thinkingMode !== "adaptive";
@@ -194,11 +194,15 @@ export interface StreamParser {
194
194
  * - `hasOnlyThinking` — thinking streamed but nothing substantive (no non-blank text, no completed
195
195
  * tool call) → tier B (seal + whole-turn retry). A dangling tool-call accumulation alongside the
196
196
  * thinking does not veto this (nothing was admitted; the retry's snapshot replacement drops it).
197
+ * - `terminalFrameSeen` — the provider TERMINATED this stream (openai `finish_reason`, anthropic
198
+ * `message_delta.stop_reason`, responses `response.completed`/`response.incomplete`). A FACT, not a tier:
199
+ * the parser reports it off the same state its own finalize reads, and the tiering owns what it means.
197
200
  */
198
201
  snapshot(): {
199
202
  hasSubstantiveText: boolean;
200
203
  hasCompletedToolCall: boolean;
201
204
  hasOnlyThinking: boolean;
205
+ terminalFrameSeen: boolean;
202
206
  };
203
207
  /**
204
208
  * design/124 §0.5-2 tier B: seal the UI stream before a whole-turn retry — emit `thinking_end` for
@@ -1,4 +1,5 @@
1
1
  import { createAssistantMessageEventStream, } from "../internal/llm.js";
2
+ import { STREAM_TORN_WARNING } from "./stream-shared.js";
2
3
  import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
3
4
  import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
4
5
  import { classifyInputTooLong } from "./input-too-long.js";
@@ -646,11 +647,15 @@ export function runStreamingBrain(args) {
646
647
  return;
647
648
  }
648
649
  const snap = parser.snapshot();
650
+ if (snap.terminalFrameSeen) {
651
+ parser.finalize();
652
+ return;
653
+ }
649
654
  if (snap.hasSubstantiveText || snap.hasCompletedToolCall) {
650
655
  midstreamWarning =
651
656
  failure.kind === "stalled"
652
657
  ? "Response stalled mid-stream. The response above may be incomplete."
653
- : "Connection closed mid-response. The response above may be incomplete.";
658
+ : STREAM_TORN_WARNING;
654
659
  parser.finalize({ truncated: true });
655
660
  return;
656
661
  }
@@ -24,6 +24,9 @@ import { type RepetitionEvent } from "./repetition.js";
24
24
  * means "do not emit a `toolcall_end` for this call"; callers must NOT cache that outcome — finalize retries
25
25
  * the parse against the complete accumulation.
26
26
  */
27
+ /** The CC-verbatim warning stamped beside `partialFinalized` when a stream ended before its close — by the
28
+ * mid-stream promotion (the body threw) and by the parsers' own finalize (content arrived, no finish frame). */
29
+ export declare const STREAM_TORN_WARNING = "Connection closed mid-response. The response above may be incomplete.";
27
30
  export declare function parseStreamedToolArgs(json: string): Record<string, unknown> | undefined;
28
31
  /** Repetition telemetry for ONE stream — the cut event plus the allowance-spared hits. */
29
32
  export interface RepetitionPoll {
@@ -1,4 +1,5 @@
1
1
  import { inspectDegenerate } from "./repetition.js";
2
+ export const STREAM_TORN_WARNING = "Connection closed mid-response. The response above may be incomplete.";
2
3
  export function parseStreamedToolArgs(json) {
3
4
  if (!json)
4
5
  return {};
@@ -133,6 +133,22 @@ export interface AskOriginFacts {
133
133
  export declare function askOriginOf(decision: Extract<PermissionResult, {
134
134
  action: "ask";
135
135
  }>, facts: AskOriginFacts): AskOrigin;
136
+ /**
137
+ * Stamp the ask's origin word — and withdraw the disclosures that speak for the TIGHTEN alone when some
138
+ * layer above it is the reason this call is being asked about. Today that is one member: the read
139
+ * boundary's directory offer, whose claim holds only while the tighten is the whole reason. Every higher
140
+ * rung is a layer with a reason of its own — a standing `ask` row, an unreadable rule store, an org
141
+ * verdict, a hook, the denial-limit fallback — which no directory grant answers, so the ask would stand
142
+ * after the grant and the offer would have promised a clearing it cannot deliver (measured: `cat
143
+ * <out-of-root>` under a standing `Bash(cat:*)` ask rule offered the directory and kept asking after it).
144
+ * It lives at the one derivation rather than the three stamp sites: the withdrawal's input IS the origin
145
+ * word, so a site that stamped without it would be a second, quieter answer to one question.
146
+ */
147
+ export declare function stampAskOrigin<T extends Extract<PermissionResult, {
148
+ action: "ask";
149
+ }>>(decision: T, facts: AskOriginFacts): T & {
150
+ origin: AskOrigin;
151
+ };
136
152
  /**
137
153
  * Which origins IMPLY the `requiresRealApproval` bit, and WHERE that implication is verified — the
138
154
  * consistency table {@link askOriginOf} reads. An ask of a must-carry origin exists so that no automatic
@@ -15,6 +15,14 @@ export function askOriginOf(decision, facts) {
15
15
  }
16
16
  return origin;
17
17
  }
18
+ const TIGHTEN_ONLY_ORIGINS = new Set(["shell_gate_tighten", "safety_tighten"]);
19
+ export function stampAskOrigin(decision, facts) {
20
+ const origin = askOriginOf(decision, facts);
21
+ if (decision.probeReadRootCandidate === undefined || TIGHTEN_ONLY_ORIGINS.has(origin))
22
+ return { ...decision, origin };
23
+ const { probeReadRootCandidate: _withdrawn, ...kept } = decision;
24
+ return { ...kept, origin };
25
+ }
18
26
  function originLadder(decision, facts) {
19
27
  if (facts.contentQuestion)
20
28
  return "content_question";
@@ -180,5 +180,11 @@ export type AutoModeArmingFold = {
180
180
  * nevertheless declare the same epoch — that is a deployment telling us its settings did not move
181
181
  * while its own data says otherwise, and picking either side would be a guess about which one is the
182
182
  * policy in force.
183
+ *
184
+ * @contract auto_mode.arming.absent_timeout_reads_recorded_default — a parked arming's absent `timeoutMs` is
185
+ * read as the default in force when it was recorded (`AUTO_MODE_RECORDED_ABSENT_TIMEOUT_MS`, 15_000 ms), not
186
+ * the default in force when it is redeemed: persisted data is read under the contract it was written under,
187
+ * so a default that moves can never widen an ancestor's bound at redemption; every recipe minted since
188
+ * materialises the knob, so absence dates the recording.
183
189
  */
184
190
  export declare function foldAutoModeArming(recorded: unknown, current: unknown): AutoModeArmingFold;
@@ -1,4 +1,4 @@
1
- import { AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS, } from "./auto-mode-defaults.js";
1
+ import { AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_RECORDED_ABSENT_TIMEOUT_MS, AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS, } from "./auto-mode-defaults.js";
2
2
  export const AUTO_MODE_ARMING_RECIPE_VERSION = 2;
3
3
  const isPlainRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4
4
  function canonicalStrings(v) {
@@ -143,7 +143,7 @@ export function autoModeArmingRecipeOf(face, bind) {
143
143
  ...(face.settingsDenyRules !== undefined ? { settingsDenyRules: face.settingsDenyRules } : {}),
144
144
  ...(face.sessionContext !== undefined ? { sessionContext: face.sessionContext } : {}),
145
145
  ...(face.window !== undefined ? { window: face.window } : {}),
146
- ...(face.timeoutMs !== undefined ? { timeoutMs: face.timeoutMs } : {}),
146
+ timeoutMs: face.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS,
147
147
  ...(face.denialLimit !== undefined ? { denialLimit: face.denialLimit } : {}),
148
148
  ...(face.settingsEpoch !== undefined ? { settingsEpoch: face.settingsEpoch } : {}),
149
149
  ...(face.crossSessionMessagesRule !== undefined ? { crossSessionMessagesRule: face.crossSessionMessagesRule } : {}),
@@ -208,8 +208,8 @@ export function foldAutoModeArming(recorded, current) {
208
208
  "(the strictest decidable answer: the inherited ask flows the original chain to a human)",
209
209
  };
210
210
  }
211
- const minKnob = (a, b, fallback) => Math.min(a ?? fallback, b ?? fallback);
212
- const timeoutMs = minKnob(rec.timeoutMs, cur.timeoutMs, AUTO_MODE_DEFAULT_TIMEOUT_MS);
211
+ const recordedTimeoutMs = rec.timeoutMs ?? AUTO_MODE_RECORDED_ABSENT_TIMEOUT_MS;
212
+ const timeoutMs = Math.min(recordedTimeoutMs, cur.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS);
213
213
  const denialLimit = tightenDenialLimit(rec.denialLimit, cur.denialLimit);
214
214
  const recordedDenial = tightenDenialLimit(rec.denialLimit, rec.denialLimit);
215
215
  return {
@@ -221,7 +221,7 @@ export function foldAutoModeArming(recorded, current) {
221
221
  denialLimit,
222
222
  ...(cur.settingsEpoch !== undefined ? { settingsEpoch: cur.settingsEpoch } : {}),
223
223
  },
224
- tightened: timeoutMs < (rec.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS) ||
224
+ tightened: timeoutMs < recordedTimeoutMs ||
225
225
  denialLimit.maxConsecutive < recordedDenial.maxConsecutive ||
226
226
  denialLimit.maxTotal < recordedDenial.maxTotal ||
227
227
  denialLimit.autoDenyAfterMs !== recordedDenial.autoDenyAfterMs,