@sema-agent/server 7.145.0 → 7.146.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 (275) hide show
  1. package/MIGRATION.md +492 -0
  2. package/USAGE.md +218 -47
  3. package/deploy/facts/CHANGES.json +25 -0
  4. package/deploy/facts/README.md +57 -0
  5. package/deploy/facts/lanes.json +54 -0
  6. package/deploy/facts/lanes.schema.json +57 -0
  7. package/deploy/facts/sandbox-rbac.json +113 -0
  8. package/deploy/facts/sandbox-rbac.schema.json +74 -0
  9. package/deploy/facts/sealed-key.json +9 -0
  10. package/deploy/facts/sealed-key.schema.json +52 -0
  11. package/deploy/sema-up/chart/templates/_helpers.tpl +19 -0
  12. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +47 -7
  13. package/deploy/sema-up/chart/templates/server-v4pro.yaml +4 -0
  14. package/deploy/sema-up/chart/templates/server.yaml +4 -0
  15. package/deploy/sema-up/chart/values.yaml +7 -0
  16. package/deploy/sema-up/stack/sema-stack-cluster.yaml +30 -14
  17. package/dist/adoption/import-limits.d.ts +6 -0
  18. package/dist/adoption/import-limits.js +7 -0
  19. package/dist/adoption/import.d.ts +62 -0
  20. package/dist/adoption/import.js +107 -0
  21. package/dist/adoption/wire.d.ts +52 -0
  22. package/dist/adoption/wire.js +9 -0
  23. package/dist/approval-ask-machine.d.ts +2 -2
  24. package/dist/approval-card.d.ts +3 -12
  25. package/dist/approval-card.js +0 -4
  26. package/dist/approval-gate-currency.d.ts +1 -1
  27. package/dist/approval-gate-currency.js +1 -1
  28. package/dist/approval-reconciler.d.ts +1 -1
  29. package/dist/boot/deployment-mods.d.ts +11 -1
  30. package/dist/boot/deployment-mods.js +15 -2
  31. package/dist/boot/device-lane.js +2 -0
  32. package/dist/boot/engine-lease.d.ts +193 -21
  33. package/dist/boot/engine-lease.js +146 -20
  34. package/dist/boot/execution-env.d.ts +4 -0
  35. package/dist/boot/execution-env.js +12 -0
  36. package/dist/boot/exit-boundary.d.ts +50 -0
  37. package/dist/boot/exit-boundary.js +32 -0
  38. package/dist/boot/exit-last.d.ts +20 -4
  39. package/dist/boot/exit-last.js +21 -4
  40. package/dist/boot/governance-seams.js +1 -0
  41. package/dist/boot/memory-consolidation.d.ts +29 -17
  42. package/dist/boot/memory-consolidation.js +26 -15
  43. package/dist/boot/reapers.d.ts +12 -4
  44. package/dist/boot/reapers.js +39 -15
  45. package/dist/boot/resolve-spec.d.ts +2 -0
  46. package/dist/boot/resolve-spec.js +38 -15
  47. package/dist/boot/runner-deps.js +1 -1
  48. package/dist/boot/session-faces.js +3 -1
  49. package/dist/boot/shutdown.js +27 -9
  50. package/dist/boot/stage-01-config.js +1 -1
  51. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  52. package/dist/boot/stage-03-store-backend.js +2 -2
  53. package/dist/boot/stage-04-stores.d.ts +1 -0
  54. package/dist/boot/stage-05-execution-env.d.ts +3 -0
  55. package/dist/boot/stage-05-execution-env.js +3 -3
  56. package/dist/boot/stage-06-runners.d.ts +3 -0
  57. package/dist/boot/stage-06-runners.js +11 -10
  58. package/dist/boot/stage-07-capability-layer.d.ts +3 -0
  59. package/dist/boot/stage-07-capability-layer.js +7 -2
  60. package/dist/boot/stage-08-reapers.d.ts +3 -0
  61. package/dist/boot/stage-08-reapers.js +3 -3
  62. package/dist/boot/stage-09-leader.d.ts +3 -0
  63. package/dist/boot/stage-10-http-server.d.ts +3 -0
  64. package/dist/boot/stage-10-http-server.js +3 -2
  65. package/dist/boot/stores.d.ts +1 -0
  66. package/dist/boot/stores.js +35 -19
  67. package/dist/boot/workflow-orchestration.d.ts +1 -1
  68. package/dist/boot/workflow-orchestration.js +2 -1
  69. package/dist/capabilities/composite-members.d.ts +3 -1
  70. package/dist/capabilities/composite-members.js +3 -1
  71. package/dist/capabilities/scenarios.d.ts +2 -2
  72. package/dist/capability-facts.d.ts +18 -1
  73. package/dist/capability-facts.js +25 -7
  74. package/dist/checkpoint-ctx-seal-m28.d.ts +1 -1
  75. package/dist/config-catalog.js +13 -4
  76. package/dist/config-provider.d.ts +2 -2
  77. package/dist/config-types.d.ts +43 -4
  78. package/dist/config.d.ts +6 -1
  79. package/dist/config.js +67 -11
  80. package/dist/deleted-session-ids.d.ts +19 -0
  81. package/dist/deleted-session-ids.js +26 -0
  82. package/dist/deploy-facts.d.ts +155 -0
  83. package/dist/deploy-facts.js +94 -0
  84. package/dist/device-ws-hub.d.ts +48 -3
  85. package/dist/device-ws-hub.js +48 -2
  86. package/dist/error-code-family.d.ts +35 -0
  87. package/dist/error-code-family.js +26 -0
  88. package/dist/fleet/drain-entry.d.ts +39 -3
  89. package/dist/fleet/drain-entry.js +6 -0
  90. package/dist/fleet/fleet-bus.d.ts +2 -1
  91. package/dist/hooks/branch-transcript.d.ts +1 -1
  92. package/dist/hooks/branch-transcript.js +1 -0
  93. package/dist/hooks/classic-stations.d.ts +25 -8
  94. package/dist/hooks/classic-stations.js +29 -3
  95. package/dist/hooks/hook-runner.d.ts +33 -16
  96. package/dist/hooks/hook-runner.js +166 -73
  97. package/dist/hooks/hook-source.d.ts +14 -2
  98. package/dist/hooks/hook-source.js +8 -0
  99. package/dist/http/active-run-conflict.d.ts +1 -1
  100. package/dist/http/active-run-conflict.js +1 -1
  101. package/dist/http/admission.js +11 -1
  102. package/dist/http/checkpoint-ctx.d.ts +59 -71
  103. package/dist/http/checkpoint-ctx.js +34 -40
  104. package/dist/http/dispatch.js +18 -2
  105. package/dist/http/durable-run-leg.d.ts +5 -0
  106. package/dist/http/durable-run-leg.js +1 -0
  107. package/dist/http/execution-context.d.ts +4 -0
  108. package/dist/http/execution-context.js +9 -0
  109. package/dist/http/health-self-report.d.ts +6 -2
  110. package/dist/http/health-self-report.js +3 -2
  111. package/dist/http/memory-compliance-failure-reply.d.ts +1 -1
  112. package/dist/http/memory-compliance-failure-reply.js +2 -15
  113. package/dist/http/memory-control-plane-corrupt-reply.d.ts +25 -0
  114. package/dist/http/memory-control-plane-corrupt-reply.js +19 -0
  115. package/dist/http/principal-gate.d.ts +35 -12
  116. package/dist/http/principal-gate.js +16 -12
  117. package/dist/http/request-guards.d.ts +1 -1
  118. package/dist/http/request-guards.js +5 -5
  119. package/dist/http/resume-legs.d.ts +22 -0
  120. package/dist/http/resume-legs.js +26 -17
  121. package/dist/http/route-ctx.d.ts +11 -10
  122. package/dist/http/route-table.js +2 -0
  123. package/dist/http/routes/adoption.d.ts +11 -0
  124. package/dist/http/routes/adoption.js +80 -6
  125. package/dist/http/routes/agents-roster.js +1 -0
  126. package/dist/http/routes/approvals-assistant.js +2 -1
  127. package/dist/http/routes/background.js +1 -0
  128. package/dist/http/routes/capabilities.js +4 -1
  129. package/dist/http/routes/diagnostics.d.ts +9 -14
  130. package/dist/http/routes/diagnostics.js +10 -20
  131. package/dist/http/routes/engine.d.ts +31 -0
  132. package/dist/http/routes/engine.js +81 -0
  133. package/dist/http/routes/memory-bundle.js +3 -0
  134. package/dist/http/routes/memory-consolidation.js +3 -5
  135. package/dist/http/routes/memory-origin.d.ts +1 -1
  136. package/dist/http/routes/memory-origin.js +2 -4
  137. package/dist/http/routes/memory-policy.js +5 -1
  138. package/dist/http/routes/observability.js +3 -6
  139. package/dist/http/routes/runs.js +7 -3
  140. package/dist/http/routes/session-sync.js +12 -25
  141. package/dist/http/routes/sessions-list.d.ts +1 -1
  142. package/dist/http/routes/sessions.js +4 -0
  143. package/dist/http/routes/tasks.js +23 -5
  144. package/dist/http/routes/trace-usage.js +1 -1
  145. package/dist/http/run-concurrency-cap.d.ts +86 -0
  146. package/dist/http/run-concurrency-cap.js +54 -0
  147. package/dist/http/server.d.ts +15 -12
  148. package/dist/http/server.js +18 -3
  149. package/dist/http/stored-body-replay.d.ts +9 -6
  150. package/dist/http/stored-body-replay.js +19 -5
  151. package/dist/http/wire-types.d.ts +8 -3
  152. package/dist/json-body-limit.d.ts +8 -0
  153. package/dist/json-body-limit.js +2 -0
  154. package/dist/leader/diffout.d.ts +1 -1
  155. package/dist/local-data-root.d.ts +2 -2
  156. package/dist/memory-operator-faces.d.ts +9 -4
  157. package/dist/memory-scope-stores.d.ts +9 -7
  158. package/dist/memory-scope-stores.js +3 -14
  159. package/dist/memory-scope.d.ts +36 -0
  160. package/dist/memory-scope.js +32 -1
  161. package/dist/net-address.d.ts +53 -0
  162. package/dist/net-address.js +111 -0
  163. package/dist/observability/fail-open.d.ts +14 -10
  164. package/dist/observability/fail-open.js +14 -10
  165. package/dist/orchestration/workflow-notify-journal.js +2 -3
  166. package/dist/org-delivery-gate.js +1 -1
  167. package/dist/peer-directory.d.ts +6 -12
  168. package/dist/peer-directory.js +5 -1
  169. package/dist/plugins/caching-session-store.d.ts +3 -1
  170. package/dist/plugins/caching-session-store.js +3 -0
  171. package/dist/plugins/checkpoint-store-face.d.ts +426 -0
  172. package/dist/plugins/checkpoint-store-face.js +152 -0
  173. package/dist/plugins/file-run-store.d.ts +0 -6
  174. package/dist/plugins/file-run-store.js +1 -12
  175. package/dist/plugins/fork-routing-session-store.d.ts +6 -2
  176. package/dist/plugins/fork-routing-session-store.js +2 -2
  177. package/dist/plugins/imported-layer-root-retirement.d.ts +10 -8
  178. package/dist/plugins/local-checkpoint-store.d.ts +2 -2
  179. package/dist/plugins/local-checkpoint-store.js +2 -1
  180. package/dist/plugins/local-session-store.d.ts +28 -5
  181. package/dist/plugins/local-session-store.js +32 -8
  182. package/dist/plugins/memory-engine-pg.d.ts +29 -6
  183. package/dist/plugins/memory-engine-pg.js +96 -214
  184. package/dist/plugins/memory-engine-sql-apply.d.ts +121 -0
  185. package/dist/plugins/memory-engine-sql-apply.js +358 -0
  186. package/dist/plugins/memory-engine-tidb.d.ts +35 -9
  187. package/dist/plugins/memory-engine-tidb.js +97 -227
  188. package/dist/plugins/memory-key-guards.d.ts +1 -1
  189. package/dist/plugins/memory-origin-law.d.ts +3 -3
  190. package/dist/plugins/one-time-migrations.d.ts +32 -56
  191. package/dist/plugins/one-time-migrations.js +25 -117
  192. package/dist/plugins/permission-rule-store-file.d.ts +17 -3
  193. package/dist/plugins/permission-rule-store-file.js +8 -1
  194. package/dist/plugins/pg-pool.js +1 -1
  195. package/dist/plugins/pg-session-storage.d.ts +17 -4
  196. package/dist/plugins/pg-session-storage.js +8 -4
  197. package/dist/plugins/run-store-sql.js +2 -2
  198. package/dist/plugins/schema-ensure-legs.js +2 -2
  199. package/dist/plugins/session-fork.d.ts +31 -9
  200. package/dist/plugins/session-fork.js +3 -2
  201. package/dist/plugins/session-placement.d.ts +4 -2
  202. package/dist/plugins/sql-json-column.d.ts +5 -1
  203. package/dist/plugins/store-backend.d.ts +11 -8
  204. package/dist/plugins/store-backend.js +8 -7
  205. package/dist/plugins/store-contracts.d.ts +1 -11
  206. package/dist/plugins/store-contracts.js +0 -9
  207. package/dist/plugins/store-sweep.d.ts +63 -0
  208. package/dist/plugins/store-sweep.js +48 -0
  209. package/dist/plugins/tidb-pool.js +1 -1
  210. package/dist/plugins/tidb-session-store.d.ts +16 -3
  211. package/dist/plugins/tidb-session-store.js +8 -4
  212. package/dist/prompt-screen.d.ts +6 -9
  213. package/dist/prompt-screen.js +2 -5
  214. package/dist/rules-consent.d.ts +20 -27
  215. package/dist/rules-consent.js +82 -6
  216. package/dist/run-cancel-context.d.ts +4 -1
  217. package/dist/run-cancel-context.js +2 -1
  218. package/dist/runs.js +1 -1
  219. package/dist/sandbox-egress-policy.js +6 -11
  220. package/dist/sandbox-rbac-rules.d.ts +88 -0
  221. package/dist/sandbox-rbac-rules.js +80 -0
  222. package/dist/sealed-key-layout.d.ts +17 -0
  223. package/dist/sealed-key-layout.js +4 -0
  224. package/dist/sealed-key.js +5 -5
  225. package/dist/security.d.ts +38 -6
  226. package/dist/security.js +9 -2
  227. package/dist/session-notify-door.d.ts +5 -3
  228. package/dist/session-resident-background.d.ts +57 -0
  229. package/dist/session-resident-background.js +20 -0
  230. package/dist/session-sync-kernel.d.ts +53 -14
  231. package/dist/session-sync-kernel.js +70 -1
  232. package/dist/session-sync.d.ts +5 -3
  233. package/dist/session-workspace.d.ts +1 -0
  234. package/dist/session-workspace.js +1 -0
  235. package/dist/sighup-idle.d.ts +9 -7
  236. package/dist/sighup-idle.js +4 -5
  237. package/dist/spec-fields.d.ts +16 -0
  238. package/dist/spec-fields.js +7 -0
  239. package/dist/stored-request-env-credentials-m38.d.ts +3 -0
  240. package/dist/stored-request-env-credentials-m38.js +8 -0
  241. package/dist/stored-request-prompt-profile-m30.d.ts +1 -1
  242. package/dist/{plugins → stores-sql}/checkpoint-store-sql.d.ts +13 -427
  243. package/dist/{plugins → stores-sql}/checkpoint-store-sql.js +33 -194
  244. package/dist/{plugins → stores-sql}/mailbox-store-sql.d.ts +2 -2
  245. package/dist/{plugins → stores-sql}/mailbox-store-sql.js +4 -4
  246. package/dist/{plugins → stores-sql}/roster-store-sql.d.ts +2 -2
  247. package/dist/{plugins → stores-sql}/roster-store-sql.js +1 -1
  248. package/dist/{plugins → stores-sql}/tool-result-store-sql.d.ts +2 -2
  249. package/dist/{plugins → stores-sql}/tool-result-store-sql.js +6 -6
  250. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.d.ts +7 -12
  251. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.js +3 -8
  252. package/dist/task-settings.d.ts +59 -4
  253. package/dist/task-settings.js +50 -4
  254. package/dist/tool-approval.d.ts +2 -2
  255. package/dist/trace/core-keyset-guard.d.ts +8 -7
  256. package/dist/trace/engine-notice-wire.d.ts +1 -1
  257. package/dist/trace/engine-notice-wire.js +2 -0
  258. package/dist/trace/exit-seat.d.ts +6 -0
  259. package/dist/trace/exit-seat.js +1 -0
  260. package/dist/trace/project.js +9 -0
  261. package/dist/trace/redact.d.ts +13 -2
  262. package/dist/trace/redact.js +305 -24
  263. package/dist/trace/self-minted-wire.d.ts +326 -8
  264. package/dist/trace/self-minted-wire.js +125 -7
  265. package/dist/trace/wire-projection-faces.d.ts +1 -1
  266. package/dist/trace/wire-projection-faces.js +1 -1
  267. package/dist/usage-analytics.d.ts +4 -0
  268. package/package.json +12 -4
  269. package/scripts/drain-check-local.mjs +8 -29
  270. package/scripts/drain-check.mysql.sql +3 -24
  271. package/scripts/drain-check.pg.sql +3 -24
  272. package/skills/update-config.md +1 -1
  273. package/wire-changes/server@7.146.0.json +3221 -0
  274. package/dist/approval-batch-census.d.ts +0 -27
  275. package/dist/approval-batch-census.js +0 -27
@@ -7,7 +7,7 @@ import { findUrlCredential, formatConfigPath, outgoingPath } from "@sema-agent/s
7
7
  import { httpHookEnvVarsAllowed, httpHookUrlVerdict } from "@sema-agent/settings-schema";
8
8
  import { ENGINE_OWNED_HOOK_EVENTS } from "@sema-agent/settings-schema/hooks";
9
9
  import { describeToolCatalog } from "@sema-agent/core";
10
- import { hangClassic } from "./classic-stations.js";
10
+ import { hangClassic, wrapClassic } from "./classic-stations.js";
11
11
  import { MAX_HOOK_NOTICE_TEXT_CHARS, boundedNoticeText } from "../fleet/fleet-bus.js";
12
12
  import { recordFailOpen } from "../observability/fail-open.js";
13
13
  import { LIMIT_REASONS } from "../limit-refusal.js";
@@ -19,7 +19,7 @@ import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTP
19
19
  import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
20
20
  import { renderBranchTranscript } from "./branch-transcript.js";
21
21
  import { buildRequestUrlCredentialRefusal, urlCredentialHeaderExit } from "../url-credential-refusal.js";
22
- import { HOOK_PHASES, HOOK_SOURCE_VOCABULARY_SENTENCE, hookPhaseOf, readHookGroupSource } from "./hook-source.js";
22
+ import { HOOK_PHASES, HOOK_SOURCE_VOCABULARY_SENTENCE, hookLaneOf, hookPhaseOf, readHookGroupSource } from "./hook-source.js";
23
23
  import { commandLineChars, composeHookEnvLayer, hookPluginChars, planCommandSpawn, readHookArgs, readHookPlugin } from "./command-spawn.js";
24
24
  export const MAX_HOOK_ENTRIES_PER_EVENT = 32;
25
25
  export const MAX_HOOK_COMMAND_CHARS = 8_192;
@@ -52,14 +52,27 @@ export const SETTINGS_HOOK_STATIONS = {
52
52
  PreCompact: ["classic.PreCompact"],
53
53
  PostCompact: ["classic.PostCompact"],
54
54
  };
55
- function hangTranslator(on, event, translators) {
55
+ function chainsBelowTaskOn(patterns, station) {
56
+ return patterns.some((p) => p === station || (p.endsWith(".*") && station.startsWith(p.slice(0, -1))));
57
+ }
58
+ function managedTightenOnlyOn(lane, station, belowTaskChainPatterns) {
59
+ return lane === "managed" && chainsBelowTaskOn(belowTaskChainPatterns, station);
60
+ }
61
+ const MANAGED_REWRITE_UNSHOWN_REASON = "a managed PreToolUse hook rewrote this call's input, but this deployment runs its own handler for this call after the request's hooks and that handler never sees a rewrite made above it, so the rewrite cannot be honored and the call was not executed";
62
+ const MANAGED_ALLOW_UNJUDGED_REASON = "a managed PreToolUse hook allowed this call, but this deployment runs its own handler for this call after the request's hooks, and a request hook's allow does not stand in for that handler's judgment, so the call was not executed (the same outcome as the same hook without a managed source)";
63
+ function hangTranslator(on, event, translators, lane) {
56
64
  const translate = translators[event];
57
65
  if (translate === undefined)
58
66
  return;
67
+ const hang = lane === "managed" ? wrapClassic : hangClassic;
59
68
  for (const station of SETTINGS_HOOK_STATIONS[event])
60
- hangClassic(on, station, translate);
69
+ hang(on, station, translate);
61
70
  }
62
- export const SETTINGS_HOOKS_REGISTRATION = "settings.hooks";
71
+ export const SETTINGS_HOOKS_REGISTRATIONS = {
72
+ managed: "settings.hooks.managed",
73
+ unmanaged: "settings.hooks",
74
+ };
75
+ const LANE_CHAIN_ORDER = ["managed", "unmanaged"];
63
76
  export const DEFAULT_SETTINGS_HOOKS_BUDGET_MS = MAX_HOOK_TIMEOUT_SECONDS * 1000;
64
77
  export function settingsHooksBudgetMs(config) {
65
78
  return config.hooksTimeoutMs ?? DEFAULT_SETTINGS_HOOKS_BUDGET_MS;
@@ -650,7 +663,7 @@ function noteVerdictUntrusted(ctx, args) {
650
663
  kind: "hook_verdict_untrusted",
651
664
  event: args.event,
652
665
  reason: args.reason,
653
- lane: hookPhaseOf(args.source) === "unmanaged" ? "unmanaged" : "managed",
666
+ lane: hookLaneOf(args.source),
654
667
  hookName: boundedNoticeText(hookDisplayName(args.entry)),
655
668
  entryType: args.entry.type,
656
669
  ...(args.toolName !== undefined ? { toolName: args.toolName } : {}),
@@ -692,15 +705,22 @@ const VERDICT_GAP_CAUSE = {
692
705
  };
693
706
  function createUnrunLedger(ctx, event, toolName) {
694
707
  let managedVerdictGap;
695
- return {
708
+ let haltNoted = false;
709
+ const ledger = {
710
+ halted(source, entry) {
711
+ if (haltNoted)
712
+ return;
713
+ haltNoted = true;
714
+ ledger.unrun(source, entry, "budget_exhausted", ENGINE_BUDGET_SPENT_DETAIL);
715
+ },
696
716
  untrusted(source, entry, reason) {
697
- if (hookPhaseOf(source) === "unmanaged")
717
+ if (hookLaneOf(source) === "unmanaged")
698
718
  return;
699
719
  managedVerdictGap ??= VERDICT_GAP_CAUSE[reason];
700
720
  noteVerdictUntrusted(ctx, { event, reason, entry, source, toolName });
701
721
  },
702
722
  unrun(source, entry, reason, detail) {
703
- if (source === undefined || hookPhaseOf(source) === "unmanaged")
723
+ if (source === undefined || hookLaneOf(source) === "unmanaged")
704
724
  return;
705
725
  managedVerdictGap ??= VERDICT_GAP_CAUSE[reason];
706
726
  ctx.logger.warn("hook_managed_unrun", { event, reason, source: echoCallerText(source), type: entry.type, ...(toolName !== undefined ? { tool: toolName } : {}) });
@@ -721,6 +741,7 @@ function createUnrunLedger(ctx, event, toolName) {
721
741
  return managedVerdictGap;
722
742
  },
723
743
  };
744
+ return ledger;
724
745
  }
725
746
  function noteExitNonzero(ctx, ledger, event, ran, toolName, detail) {
726
747
  logHookCommandFailed(ctx, event, ran.run);
@@ -766,6 +787,36 @@ function buildHookContextField(parts) {
766
787
  const text = buildHookContext(parts);
767
788
  return text === undefined ? {} : { additionalContext: text };
768
789
  }
790
+ function belowString(below, key) {
791
+ if (typeof below !== "object" || below === null || !Object.hasOwn(below, key))
792
+ return undefined;
793
+ const value = Reflect.get(below, key);
794
+ return typeof value === "string" ? value : undefined;
795
+ }
796
+ function belowMembers(below) {
797
+ return typeof below === "object" && below !== null ? { ...below } : {};
798
+ }
799
+ function withBelowContext(parts, below) {
800
+ const tail = belowString(below, "additionalContext");
801
+ return tail === undefined ? [...parts] : [...parts, tail];
802
+ }
803
+ function readBelowPreToolUse(below) {
804
+ const action = belowString(below, "action");
805
+ const message = belowString(below, "message") ?? "a PreToolUse hook requires approval";
806
+ const cause = belowString(below, "questionCause");
807
+ const rewrite = typeof below === "object" && below !== null && Object.hasOwn(below, "updatedInput") ? Reflect.get(below, "updatedInput") : undefined;
808
+ const additionalContext = belowString(below, "additionalContext");
809
+ const unreadableCause = cause === "verdict_unfinished" ? "verdict_unfinished" : "verdict_unreadable";
810
+ return {
811
+ denied: action === "deny",
812
+ allowed: action === "allow",
813
+ ...(action === "ask" && (cause === undefined || cause === "asked") ? { asked: message } : {}),
814
+ ...(action === "ask" && cause !== undefined && cause !== "asked" ? { unreadable: { message, cause: unreadableCause } } : {}),
815
+ realApproval: action === "ask" && typeof below === "object" && below !== null && Reflect.get(below, "requiresRealApproval") === true,
816
+ ...(rewrite !== null && typeof rewrite === "object" ? { updatedInput: rewrite } : {}),
817
+ ...(additionalContext !== undefined ? { additionalContext } : {}),
818
+ };
819
+ }
769
820
  function spawnPlannedCommand(plan, ctx, stdio) {
770
821
  const env = hookChildEnv(plan.env, true);
771
822
  if (plan.kind === "exec")
@@ -1190,7 +1241,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
1190
1241
  const inputTruncated = boundedToolInputPayload(payload) !== payload;
1191
1242
  const truncatedFor = (entry) => (entry.type === "prompt" || entry.type === "agent" ? llmHookArguments(payload).truncated : inputTruncated);
1192
1243
  const settle = async (entry, onceKey) => {
1193
- if (halt.aborted)
1244
+ if (halt.signal.aborted)
1194
1245
  return { kind: "halted" };
1195
1246
  if (entry.once && onceFired.has(onceKey))
1196
1247
  return { kind: "passed" };
@@ -1234,7 +1285,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
1234
1285
  if (entry.type === "command" && (entry.async || entry.asyncRewake)) {
1235
1286
  if (entry.once)
1236
1287
  onceFired.add(onceKey);
1237
- void fireAsyncCommandHook(entry, payload, ctx, event, halt);
1288
+ void fireAsyncCommandHook(entry, payload, ctx, event, halt.signal);
1238
1289
  return { kind: "passed" };
1239
1290
  }
1240
1291
  if (Date.now() > deadlineAt) {
@@ -1245,12 +1296,14 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
1245
1296
  onceFired.add(onceKey);
1246
1297
  if (entry.type === "prompt" || entry.type === "agent")
1247
1298
  llmEntries++;
1248
- const run = await runRunnableEntry(entry, payload, ctx, event, llmExtra, halt);
1299
+ const run = await runRunnableEntry(entry, payload, ctx, event, llmExtra, halt.signal);
1249
1300
  if (run.aborted !== undefined) {
1250
1301
  if (run.aborted.signal !== undefined)
1251
1302
  ctx.logger.warn("hook_command_killed", { event, type: entry.type, killed: true, signal: run.aborted.signal });
1252
1303
  return { kind: "halted" };
1253
1304
  }
1305
+ if (halt.signal.aborted)
1306
+ return { kind: "halted" };
1254
1307
  if (run.withheld !== undefined) {
1255
1308
  const { list, rule, sources } = run.withheld;
1256
1309
  ctx.logger.warn("hook_http_withheld", { event, url: entry.type === "http" ? buildDisplaySafeUrl(entry.url) : null, list, rule, sources: [...sources] });
@@ -1307,6 +1360,8 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
1307
1360
  case "passed":
1308
1361
  break;
1309
1362
  case "halted":
1363
+ if (isSyncEntry(entry) && halt.budgetSpent())
1364
+ ledger.halted(group.source, entry);
1310
1365
  return results;
1311
1366
  default: {
1312
1367
  const unreachable = fate;
@@ -1317,14 +1372,12 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
1317
1372
  }
1318
1373
  return results;
1319
1374
  }
1320
- function createLaneClocks() {
1321
- const startedAt = {};
1322
- return (phase) => {
1323
- const lane = phase === "unmanaged" ? "unmanaged" : "managed";
1324
- return (startedAt[lane] ??= Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1325
- };
1375
+ function createEventClock() {
1376
+ let deadlineAt;
1377
+ return () => (deadlineAt ??= Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1326
1378
  }
1327
- function hookLanesOf(groups) {
1379
+ const ENGINE_BUDGET_SPENT_DETAIL = "the managed lane's handler ran past its engine budget (HOOKS_TIMEOUT_MS); the managed entries after this one did not run on this event";
1380
+ function hookPhasesOf(groups) {
1328
1381
  const indexed = groups.map((group, index) => ({ group, index, phase: hookPhaseOf(group.source) }));
1329
1382
  return HOOK_PHASES.map((phase) => ({ phase, groups: indexed.filter((g) => g.phase === phase).map(({ group, index }) => ({ group, index })) })).filter((l) => l.groups.length > 0);
1330
1383
  }
@@ -1332,12 +1385,10 @@ function trustRank(phase) {
1332
1385
  return HOOK_PHASES.length - 1 - HOOK_PHASES.indexOf(phase);
1333
1386
  }
1334
1387
  async function runEventLanes(event, groups, matchValue, payload, ctx, onceFired, halt, ledger, llmExtra) {
1335
- const deadlineFor = createLaneClocks();
1388
+ const deadline = createEventClock();
1336
1389
  const ran = [];
1337
- for (const lane of hookLanesOf(groups)) {
1338
- if (halt.aborted)
1339
- break;
1340
- for (const r of await runMatchingEntries(event, lane.groups, matchValue, payload, ctx, onceFired, halt, deadlineFor(lane.phase), ledger, llmExtra))
1390
+ for (const lane of hookPhasesOf(groups)) {
1391
+ for (const r of await runMatchingEntries(event, lane.groups, matchValue, payload, ctx, onceFired, halt, deadline(), ledger, llmExtra))
1341
1392
  ran.push({ ...r, phase: lane.phase });
1342
1393
  }
1343
1394
  return ran.sort((a, b) => trustRank(a.phase) - trustRank(b.phase)).map(({ entry, run, source }) => ({ entry, run, source }));
@@ -1372,12 +1423,26 @@ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, once
1372
1423
  }
1373
1424
  }
1374
1425
  }
1375
- export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1376
- const configured = Object.keys(config).filter((e) => (config[e]?.length ?? 0) > 0);
1377
- const shellOwned = configured.filter((e) => HOOK_EVENT_OWNER[e] === "shell");
1426
+ export function createSettingsHooksRegistrations(config, ctx, budgetMs) {
1427
+ const shellOwned = Object.keys(config).filter((e) => (config[e]?.length ?? 0) > 0 && HOOK_EVENT_OWNER[e] === "shell");
1378
1428
  if (shellOwned.length > 0) {
1379
1429
  ctx.logger.debug("hook_events_shell_owned", { events: shellOwned });
1380
1430
  }
1431
+ return LANE_CHAIN_ORDER.flatMap((lane) => {
1432
+ const registration = createLaneRegistration(lane, laneConfigOf(config, lane), ctx, budgetMs);
1433
+ return registration === undefined ? [] : [registration];
1434
+ });
1435
+ }
1436
+ function laneConfigOf(config, lane) {
1437
+ const out = {};
1438
+ for (const event of Object.keys(config)) {
1439
+ const groups = config[event]?.filter((group) => hookLaneOf(group.source) === lane);
1440
+ if (groups !== undefined && groups.length > 0)
1441
+ out[event] = groups;
1442
+ }
1443
+ return out;
1444
+ }
1445
+ function createLaneRegistration(lane, config, ctx, budgetMs) {
1381
1446
  const lit = (e) => {
1382
1447
  const groups = config[e];
1383
1448
  return groups && groups.length > 0 ? groups : undefined;
@@ -1397,17 +1462,23 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1397
1462
  const onceFired = new Set();
1398
1463
  const translators = {};
1399
1464
  if (pre) {
1400
- translators.PreToolUse = async (e, signal) => {
1465
+ const tightenOnly = managedTightenOnlyOn(lane, "classic.PreToolUse", ctx.belowTaskChainPatterns);
1466
+ translators.PreToolUse = async (e, halt, below) => {
1467
+ const prior = readBelowPreToolUse(below);
1468
+ const realApproval = prior.realApproval ? { requiresRealApproval: true } : {};
1469
+ if (prior.denied)
1470
+ return undefined;
1401
1471
  const toolName = e.tool;
1402
1472
  const ledger = createUnrunLedger(ctx, "PreToolUse", toolName);
1403
- const deadlineFor = createLaneClocks();
1404
- let currentInput = e.input;
1405
- let rewritten = false;
1406
- let asked;
1407
- let allowedFor;
1473
+ const deadline = createEventClock();
1474
+ let currentInput = prior.updatedInput ?? e.input;
1475
+ let rewritten = prior.updatedInput !== undefined;
1476
+ let asked = prior.asked;
1477
+ let spoke = false;
1478
+ let allowedFor = prior.allowed ? { input: currentInput } : undefined;
1408
1479
  let unknownDecision = false;
1409
1480
  const contexts = { unmanaged: [], managedPlugin: [], managedPolicy: [] };
1410
- const contextField = () => buildHookContextField([...HOOK_PHASES].sort((a, b) => trustRank(a) - trustRank(b)).flatMap((p) => contexts[p]));
1481
+ const contextField = () => buildHookContextField([...[...HOOK_PHASES].sort((a, b) => trustRank(a) - trustRank(b)).flatMap((p) => contexts[p]), ...(prior.additionalContext !== undefined ? [prior.additionalContext] : [])]);
1411
1482
  const runPass = async (phase, groups, once) => {
1412
1483
  let rewroteBy;
1413
1484
  for (const laneGroup of groups) {
@@ -1419,7 +1490,7 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1419
1490
  tool_input: groupInput,
1420
1491
  tool_use_id: e.tool_use_id,
1421
1492
  };
1422
- const singles = await runMatchingEntries("PreToolUse", [laneGroup], toolName, payload, ctx, once, signal, deadlineFor(phase), ledger);
1493
+ const singles = await runMatchingEntries("PreToolUse", [laneGroup], toolName, payload, ctx, once, halt, deadline(), ledger);
1423
1494
  for (const ran of singles) {
1424
1495
  const { entry, run } = ran;
1425
1496
  if (run.code === 2) {
@@ -1446,20 +1517,26 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1446
1517
  const hso = out.hookSpecificOutput;
1447
1518
  if (hso !== null && typeof hso === "object" && hso.hookEventName === "PreToolUse") {
1448
1519
  const h = hso;
1449
- if (typeof h.additionalContext === "string")
1520
+ if (typeof h.additionalContext === "string") {
1450
1521
  contexts[phase].push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1522
+ spoke = true;
1523
+ }
1451
1524
  const decision = h.permissionDecision;
1452
1525
  if (decision === "deny") {
1453
1526
  const reason = clipDataText(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "denied by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
1454
1527
  return { deny: { action: "deny", message: reason, ...contextField() } };
1455
1528
  }
1529
+ const proposed = h.updatedInput !== null && typeof h.updatedInput === "object" ? h.updatedInput : undefined;
1530
+ const proposesRewrite = proposed !== undefined && canonicalJsonStringify(proposed) !== canonicalJsonStringify(groupInput);
1531
+ if (tightenOnly && ran.distrust === undefined && (decision === "allow" || (proposesRewrite && (decision === "ask" || decision === undefined)))) {
1532
+ return { deny: { action: "deny", message: proposesRewrite ? MANAGED_REWRITE_UNSHOWN_REASON : MANAGED_ALLOW_UNJUDGED_REASON, ...contextField() } };
1533
+ }
1456
1534
  if (decision === "ask") {
1457
1535
  asked = clipDataText(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "a PreToolUse hook requires approval", MAX_HOOK_FEEDBACK_CHARS);
1536
+ spoke = true;
1458
1537
  }
1459
1538
  if (decision === "allow" || decision === undefined) {
1460
- const proposed = h.updatedInput !== null && typeof h.updatedInput === "object" ? h.updatedInput : undefined;
1461
- const proposesRewrite = proposed !== undefined && canonicalJsonStringify(proposed) !== canonicalJsonStringify(groupInput);
1462
- if (ran.distrust !== undefined && hookPhaseOf(ran.source) === "unmanaged" && (decision === "allow" || proposesRewrite)) {
1539
+ if (ran.distrust !== undefined && hookLaneOf(ran.source) === "unmanaged" && (decision === "allow" || proposesRewrite)) {
1463
1540
  noteVerdictUntrusted(ctx, { event: "PreToolUse", reason: ran.distrust, entry, source: ran.source, toolName });
1464
1541
  }
1465
1542
  const offered = ran.distrust === undefined ? proposed : undefined;
@@ -1468,12 +1545,16 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1468
1545
  currentInput = offered;
1469
1546
  rewritten = true;
1470
1547
  rewroteBy = ran;
1548
+ spoke = true;
1471
1549
  }
1472
- if (decision === "allow" && ran.distrust === undefined)
1550
+ if (decision === "allow" && ran.distrust === undefined) {
1473
1551
  allowedFor = { input: rewrites ? offered : groupInput };
1552
+ spoke = true;
1553
+ }
1474
1554
  }
1475
1555
  else if (decision !== "deny" && decision !== "ask") {
1476
1556
  unknownDecision = true;
1557
+ spoke = true;
1477
1558
  ctx.logger.warn("hook_permission_decision_unknown", {
1478
1559
  event: "PreToolUse",
1479
1560
  decision: seatText("operator-log-text", typeof decision === "string" ? decision : typeof decision),
@@ -1487,7 +1568,7 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1487
1568
  }
1488
1569
  return { rewroteBy };
1489
1570
  };
1490
- for (const lane of hookLanesOf(pre)) {
1571
+ for (const lane of hookPhasesOf(pre)) {
1491
1572
  if (lane.phase === "unmanaged") {
1492
1573
  const pass = await runPass(lane.phase, lane.groups, onceFired);
1493
1574
  if (pass.deny !== undefined)
@@ -1511,38 +1592,45 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1511
1592
  ledger.unrun(second.rewroteBy.source, second.rewroteBy.entry, "rewrite_unsettled", "the managed hooks of this phase rewrote the tool input again after being re-run on their own rewrite; approval required");
1512
1593
  }
1513
1594
  }
1595
+ const managedGap = ledger.managedVerdictGap;
1596
+ if (below !== undefined && !spoke && managedGap === undefined)
1597
+ return undefined;
1514
1598
  if (asked !== undefined) {
1515
- return { action: "ask", message: asked, questionCause: "asked", ...(rewritten ? { updatedInput: currentInput } : {}), ...contextField() };
1599
+ return { action: "ask", message: asked, questionCause: "asked", ...(rewritten ? { updatedInput: currentInput } : {}), ...realApproval, ...contextField() };
1516
1600
  }
1517
- const managedGap = ledger.managedVerdictGap;
1518
1601
  if (managedGap !== undefined) {
1519
1602
  return {
1520
1603
  action: "ask",
1521
1604
  message: "a managed PreToolUse hook did not run to a verdict — approval required",
1522
1605
  questionCause: managedGap,
1523
1606
  ...(rewritten ? { updatedInput: currentInput } : {}),
1607
+ ...realApproval,
1524
1608
  ...contextField(),
1525
1609
  };
1526
1610
  }
1611
+ if (prior.unreadable !== undefined) {
1612
+ return { action: "ask", message: prior.unreadable.message, questionCause: prior.unreadable.cause, ...(rewritten ? { updatedInput: currentInput } : {}), ...realApproval, ...contextField() };
1613
+ }
1527
1614
  if (unknownDecision) {
1528
1615
  return {
1529
1616
  action: "ask",
1530
1617
  message: "a PreToolUse hook returned an unrecognized permissionDecision — approval required",
1531
1618
  questionCause: "verdict_unreadable",
1532
1619
  ...(rewritten ? { updatedInput: currentInput } : {}),
1620
+ ...realApproval,
1533
1621
  ...contextField(),
1534
1622
  };
1535
1623
  }
1536
1624
  const carried = { ...(rewritten ? { updatedInput: currentInput } : {}), ...contextField() };
1537
1625
  if (allowedFor !== undefined && allowedFor.input === currentInput)
1538
1626
  return { action: "allow", ...carried };
1539
- if (rewritten || HOOK_PHASES.some((p) => contexts[p].length > 0))
1627
+ if (rewritten || HOOK_PHASES.some((p) => contexts[p].length > 0) || prior.additionalContext !== undefined)
1540
1628
  return carried;
1541
1629
  return undefined;
1542
1630
  };
1543
1631
  }
1544
1632
  if (post) {
1545
- translators.PostToolUse = async (e, signal) => {
1633
+ translators.PostToolUse = async (e, halt, below) => {
1546
1634
  const toolName = e.tool;
1547
1635
  const input = e.input;
1548
1636
  const output = e.output;
@@ -1559,7 +1647,7 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1559
1647
  tool_use_id: e.tool_use_id,
1560
1648
  };
1561
1649
  const ledger = createUnrunLedger(ctx, "PostToolUse", toolName);
1562
- const singles = await runEventLanes("PostToolUse", post, toolName, payload, ctx, onceFired, signal, ledger);
1650
+ const singles = await runEventLanes("PostToolUse", post, toolName, payload, ctx, onceFired, halt, ledger);
1563
1651
  for (const ran of singles) {
1564
1652
  const { entry, run } = ran;
1565
1653
  if (run.code === 2) {
@@ -1592,12 +1680,13 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1592
1680
  }
1593
1681
  }
1594
1682
  }
1595
- const additionalContext = buildHookContext(contexts);
1596
- return additionalContext === undefined ? undefined : { additionalContext };
1683
+ if (contexts.length === 0)
1684
+ return undefined;
1685
+ return { ...belowMembers(below), ...buildHookContextField(withBelowContext(contexts, below)) };
1597
1686
  };
1598
1687
  }
1599
1688
  if (postFailure) {
1600
- translators.PostToolUseFailure = async (e, signal) => {
1689
+ translators.PostToolUseFailure = async (e, halt, below) => {
1601
1690
  const toolName = e.tool;
1602
1691
  const f = e.failure;
1603
1692
  const contexts = [];
@@ -1611,7 +1700,7 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1611
1700
  is_interrupt: f.isInterrupt,
1612
1701
  };
1613
1702
  const ledger = createUnrunLedger(ctx, "PostToolUseFailure", toolName);
1614
- const singles = await runEventLanes("PostToolUseFailure", postFailure, toolName, payload, ctx, onceFired, signal, ledger);
1703
+ const singles = await runEventLanes("PostToolUseFailure", postFailure, toolName, payload, ctx, onceFired, halt, ledger);
1615
1704
  for (const ran of singles) {
1616
1705
  const { entry, run } = ran;
1617
1706
  if (run.code === 2) {
@@ -1640,12 +1729,13 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1640
1729
  contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1641
1730
  }
1642
1731
  }
1643
- const additionalContext = buildHookContext(contexts);
1644
- return additionalContext === undefined ? undefined : { additionalContext };
1732
+ if (contexts.length === 0)
1733
+ return undefined;
1734
+ return { ...belowMembers(below), ...buildHookContextField(withBelowContext(contexts, below)) };
1645
1735
  };
1646
1736
  }
1647
1737
  if (postBatch) {
1648
- translators.PostToolBatch = async (e, signal) => {
1738
+ translators.PostToolBatch = async (e, halt, below) => {
1649
1739
  const contexts = [];
1650
1740
  const payload = {
1651
1741
  ...basePayload(ctx, e.permissionMode),
@@ -1658,7 +1748,7 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1658
1748
  })),
1659
1749
  };
1660
1750
  const ledger = createUnrunLedger(ctx, "PostToolBatch", undefined);
1661
- const singles = await runEventLanes("PostToolBatch", ignoreMatchers(postBatch), "", payload, ctx, onceFired, signal, ledger);
1751
+ const singles = await runEventLanes("PostToolBatch", ignoreMatchers(postBatch), "", payload, ctx, onceFired, halt, ledger);
1662
1752
  for (const ran of singles) {
1663
1753
  const { entry, run } = ran;
1664
1754
  if (run.code === 2) {
@@ -1683,16 +1773,17 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1683
1773
  if (out.hookSpecificOutput !== undefined)
1684
1774
  ctx.logger.warn("hook_result_unsupported", { event: "PostToolBatch" });
1685
1775
  }
1686
- const additionalContext = buildHookContext(contexts);
1687
- return additionalContext === undefined ? undefined : { additionalContext };
1776
+ if (contexts.length === 0)
1777
+ return undefined;
1778
+ return { ...belowMembers(below), ...buildHookContextField(withBelowContext(contexts, below)) };
1688
1779
  };
1689
1780
  }
1690
1781
  if (promptSubmit) {
1691
- translators.UserPromptSubmit = async (e, signal) => {
1782
+ translators.UserPromptSubmit = async (e, halt, below) => {
1692
1783
  const contexts = [];
1693
1784
  const payload = { ...basePayload(ctx, e.permissionMode), hook_event_name: "UserPromptSubmit", prompt: clipDataText(e.prompt, MAX_TOOL_INPUT_CHARS) };
1694
1785
  const ledger = createUnrunLedger(ctx, "UserPromptSubmit", undefined);
1695
- const singles = await runEventLanes("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, signal, ledger);
1786
+ const singles = await runEventLanes("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, halt, ledger);
1696
1787
  for (const ran of singles) {
1697
1788
  const { entry, run } = ran;
1698
1789
  if (run.code === 2) {
@@ -1723,12 +1814,13 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1723
1814
  contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1724
1815
  }
1725
1816
  }
1726
- const additionalContext = buildHookContext(contexts);
1727
- return additionalContext === undefined ? undefined : { additionalContext };
1817
+ if (contexts.length === 0)
1818
+ return undefined;
1819
+ return { ...belowMembers(below), ...buildHookContextField(withBelowContext(contexts, below)) };
1728
1820
  };
1729
1821
  }
1730
1822
  if (stopEntries) {
1731
- translators.Stop = async (sctx, signal) => {
1823
+ translators.Stop = async (sctx, halt, below) => {
1732
1824
  const payload = { ...basePayload(ctx, sctx.permissionMode), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
1733
1825
  let llmExtra;
1734
1826
  const wantsPrompt = stopEntries.some((g) => g.hooks.some((h) => h.type === "prompt"));
@@ -1766,7 +1858,7 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1766
1858
  const effectiveGroups = llmExtra || !wantsPrompt
1767
1859
  ? stopGroups
1768
1860
  : stopGroups.map((g) => ({ ...g, hooks: g.hooks.filter((h) => h.type !== "prompt") })).filter((g) => g.hooks.length > 0);
1769
- const singles = await runEventLanes("Stop", effectiveGroups, "", payload, ctx, onceFired, signal, ledger, llmExtra);
1861
+ const singles = await runEventLanes("Stop", effectiveGroups, "", payload, ctx, onceFired, halt, ledger, llmExtra);
1770
1862
  let block;
1771
1863
  let sawContinueFalse = false;
1772
1864
  const contexts = [];
@@ -1821,13 +1913,14 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1821
1913
  if (block === undefined && contexts.length === 0)
1822
1914
  return undefined;
1823
1915
  return {
1916
+ ...belowMembers(below),
1824
1917
  ...(block !== undefined ? { block } : {}),
1825
- ...buildHookContextField(contexts),
1918
+ ...buildHookContextField(withBelowContext(contexts, below)),
1826
1919
  };
1827
1920
  };
1828
1921
  }
1829
1922
  if (stopFailureEntries) {
1830
- translators.StopFailure = async (fctx, signal) => {
1923
+ translators.StopFailure = async (fctx, halt) => {
1831
1924
  const turnError = toAssistantTurnError(fctx.errorKind);
1832
1925
  const payload = {
1833
1926
  ...basePayload(ctx, undefined),
@@ -1835,11 +1928,11 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1835
1928
  error: turnError,
1836
1929
  ...(fctx.error ? { error_details: clipDataText(redactSecrets(fctx.error), MAX_HOOK_FEEDBACK_CHARS) } : {}),
1837
1930
  };
1838
- await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired, signal);
1931
+ await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired, halt);
1839
1932
  };
1840
1933
  }
1841
1934
  if (preCompactEntries) {
1842
- translators.PreCompact = async (pctx, signal) => {
1935
+ translators.PreCompact = async (pctx, halt, below) => {
1843
1936
  const payload = {
1844
1937
  ...basePayload(ctx, undefined),
1845
1938
  hook_event_name: "PreCompact",
@@ -1847,11 +1940,11 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1847
1940
  custom_instructions: pctx.customInstructions ?? null,
1848
1941
  };
1849
1942
  const ledger = createUnrunLedger(ctx, "PreCompact", undefined);
1850
- const singles = await runEventLanes("PreCompact", preCompactEntries, pctx.trigger, payload, ctx, onceFired, signal, ledger);
1943
+ const singles = await runEventLanes("PreCompact", preCompactEntries, pctx.trigger, payload, ctx, onceFired, halt, ledger);
1851
1944
  for (const ran of singles) {
1852
1945
  const { entry, run } = ran;
1853
1946
  if (run.code === 2) {
1854
- return { block: clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreCompact hook" };
1947
+ return { ...belowMembers(below), block: clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreCompact hook" };
1855
1948
  }
1856
1949
  if (run.code !== 0) {
1857
1950
  noteExitNonzero(ctx, ledger, "PreCompact", ran);
@@ -1866,31 +1959,31 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
1866
1959
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PreCompact" });
1867
1960
  }
1868
1961
  if (out.decision === "block") {
1869
- return { block: clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a PreCompact hook", MAX_HOOK_FEEDBACK_CHARS) };
1962
+ return { ...belowMembers(below), block: clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a PreCompact hook", MAX_HOOK_FEEDBACK_CHARS) };
1870
1963
  }
1871
1964
  }
1872
1965
  return undefined;
1873
1966
  };
1874
1967
  }
1875
1968
  if (postCompactEntries) {
1876
- translators.PostCompact = async (cctx, signal) => {
1969
+ translators.PostCompact = async (cctx, halt) => {
1877
1970
  const payload = {
1878
1971
  ...basePayload(ctx, undefined),
1879
1972
  hook_event_name: "PostCompact",
1880
1973
  trigger: cctx.trigger,
1881
1974
  compact_summary: clipDataText(cctx.summary, MAX_TOOL_RESPONSE_CHARS),
1882
1975
  };
1883
- await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired, signal);
1976
+ await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired, halt);
1884
1977
  };
1885
1978
  }
1886
1979
  const events = Object.keys(translators);
1887
1980
  return {
1888
- name: SETTINGS_HOOKS_REGISTRATION,
1981
+ name: SETTINGS_HOOKS_REGISTRATIONS[lane],
1889
1982
  uses: { events: events.flatMap((event) => SETTINGS_HOOK_STATIONS[event]), calls: [] },
1890
1983
  budgetMs,
1891
1984
  register(on) {
1892
1985
  for (const event of events)
1893
- hangTranslator(on, event, translators);
1986
+ hangTranslator(on, event, translators, lane);
1894
1987
  },
1895
1988
  };
1896
1989
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 请求面**来源**闭集 —— 一个 hook 组(`settings.hooks[event][i].source`)是谁写的。词表与「托管与否」的映射**只在这里**;
3
- * hooks 执行器按它分车道(`hook-runner.ts` 的三相),将来请求面别处的来源键(MCP 服务器条目)读同一张词表,不另立第二份。
3
+ * hooks 执行器按它分相、分车道(`hook-runner.ts` 的三相 / 两张注册),将来请求面别处的来源键(MCP 服务器条目)读同一张词表,不另立第二份。
4
4
  *
5
5
  * 词表(闭集;集外词 = 形错,受理面整份拒,fail-closed;缺席 = 非托管):
6
6
  * · `managedPolicy` —— 组织策略(托管设置)。**托管**,且是终判相。
@@ -28,7 +28,7 @@ type HookSourcePrefix = keyof typeof HOOK_SOURCE_PREFIX_PHASE;
28
28
  export type HookGroupSource = HookSourceWord | `${HookSourcePrefix}:${string}`;
29
29
  /**
30
30
  * 执行相 —— **固定次序**就是这张数组的次序:非托管 → 托管插件 → 托管策略(终判)。后一相在前一相的最终输入上跑;结果按信任序(倒序)读。
31
- * 托管 = 后两相;非托管与托管各一只事件墙钟(见 `hook-runner.ts`)。
31
+ * 托管 = 后两相;相按 {@link hookLaneOf} 归两条车道(每道一张引擎注册,见 `hook-runner.ts`)。
32
32
  */
33
33
  export declare const HOOK_PHASES: readonly ["unmanaged", "managedPlugin", "managedPolicy"];
34
34
  export type HookPhase = (typeof HOOK_PHASES)[number];
@@ -36,6 +36,18 @@ export type HookPhase = (typeof HOOK_PHASES)[number];
36
36
  export declare const MAX_HOOK_SOURCE_NAME_CHARS = 128;
37
37
  /** 受理面的读法:合形 ⇒ 来源;否则 `undefined`(调用方铸拒)。只认 own 词(`Object.hasOwn`:原型上的名字不是词表成员)。 */
38
38
  export declare function readHookGroupSource(raw: unknown): HookGroupSource | undefined;
39
+ /**
40
+ * 车道 —— **一条车道 = 一张引擎注册**(`hook-runner.ts` 的 `SETTINGS_HOOKS_REGISTRATIONS`):非托管相一道;托管两相(插件相 + 策略相)同一道。
41
+ * 每条车道自带引擎的 handler 预算与本服务的事件墙钟 / LLM 条数 / 账本,两道之间不共用任何一只钟。相 → 道只在这张表(`satisfies`:加一个相不写道 = 编译红)。
42
+ */
43
+ declare const HOOK_PHASE_LANE: {
44
+ readonly unmanaged: "unmanaged";
45
+ readonly managedPlugin: "managed";
46
+ readonly managedPolicy: "managed";
47
+ };
48
+ export type HookLane = (typeof HOOK_PHASE_LANE)[HookPhase];
49
+ /** 来源 → 车道(经相;缺席 ⇒ 非托管)。帧上的 `lane` 位、车道注册的分组都读这一只。 */
50
+ export declare function hookLaneOf(source: HookGroupSource | undefined): HookLane;
39
51
  /** 来源 → 相。缺席 ⇒ 非托管(老包不带 `source`:行为与此前逐字同)。 */
40
52
  export declare function hookPhaseOf(source: HookGroupSource | undefined): HookPhase;
41
53
  /** 拒句里的词表(由两张表派生,不另写一份)。 */
@@ -27,6 +27,14 @@ export function readHookGroupSource(raw) {
27
27
  return undefined;
28
28
  return raw;
29
29
  }
30
+ const HOOK_PHASE_LANE = {
31
+ unmanaged: "unmanaged",
32
+ managedPlugin: "managed",
33
+ managedPolicy: "managed",
34
+ };
35
+ export function hookLaneOf(source) {
36
+ return HOOK_PHASE_LANE[hookPhaseOf(source)];
37
+ }
30
38
  export function hookPhaseOf(source) {
31
39
  if (source === undefined)
32
40
  return "unmanaged";
@@ -16,7 +16,7 @@
16
16
  import { type GovernancePosture } from "../runtime-governance.js";
17
17
  import type { ShellGateDoctrine } from "@sema-agent/core";
18
18
  import { type RunRecord } from "../plugins/store-contracts.js";
19
- import { type PendingRowBinding } from "../plugins/checkpoint-store-sql.js";
19
+ import { type PendingRowBinding } from "../plugins/checkpoint-store-face.js";
20
20
  /** 与 runs.ts/tasks.ts 三个 409 位共享的旧文案(byte-frozen:api-error-text-freeze 门认这句)。 */
21
21
  export declare const ACTIVE_RUN_CONFLICT_BASE_TEXT = "session already has an active run \u2014 POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)";
22
22
  /**
@@ -1,6 +1,6 @@
1
1
  import { governanceOriginOf } from "../runtime-governance.js";
2
2
  import { isParkedRunStatus } from "../plugins/store-contracts.js";
3
- import { LIVE_RUN_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
3
+ import { LIVE_RUN_GATE_KINDS } from "../plugins/checkpoint-store-face.js";
4
4
  import { recordFailOpen } from "../observability/fail-open.js";
5
5
  import { refusalBody } from "./refusal-body.js";
6
6
  import { trustedCheckpointId } from "../checkpoint-identity.js";