@basein/runner 0.2.12 → 0.3.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 (142) hide show
  1. package/README.md +540 -369
  2. package/dist/auth/client.d.ts +65 -3
  3. package/dist/auth/client.js +305 -53
  4. package/dist/bin/bir-hooks.d.ts +72 -31
  5. package/dist/bin/bir-hooks.js +176 -284
  6. package/dist/bin/bir-proxy.d.ts +17 -2
  7. package/dist/bin/bir-proxy.js +222 -28
  8. package/dist/bin/bir-scenario.d.ts +61 -21
  9. package/dist/bin/bir-scenario.js +593 -122
  10. package/dist/bin/bir.d.ts +12 -6
  11. package/dist/bin/bir.js +2935 -489
  12. package/dist/bin/investigate.js +40 -15
  13. package/dist/bin/scenario-edit.d.ts +7 -1
  14. package/dist/bin/scenario-edit.js +43 -21
  15. package/dist/bin/scenario-liveness.d.ts +194 -0
  16. package/dist/bin/scenario-liveness.js +459 -0
  17. package/dist/bin/scenario-stderr.d.ts +43 -0
  18. package/dist/bin/scenario-stderr.js +76 -0
  19. package/dist/bin/service-read.d.ts +88 -0
  20. package/dist/bin/service-read.js +128 -0
  21. package/dist/bin/setup.d.ts +19 -0
  22. package/dist/bin/setup.js +146 -20
  23. package/dist/config/adapters/claude-code.d.ts +92 -14
  24. package/dist/config/adapters/claude-code.js +291 -101
  25. package/dist/config/adapters/generic.d.ts +4 -1
  26. package/dist/config/adapters/generic.js +13 -14
  27. package/dist/config/generate.d.ts +392 -14
  28. package/dist/config/generate.js +666 -19
  29. package/dist/config/mcp-approval.d.ts +76 -0
  30. package/dist/config/mcp-approval.js +210 -0
  31. package/dist/config/node-path.d.ts +42 -0
  32. package/dist/config/node-path.js +102 -0
  33. package/dist/config/overrides.d.ts +62 -0
  34. package/dist/config/overrides.js +136 -0
  35. package/dist/config/resolve.d.ts +70 -8
  36. package/dist/config/resolve.js +186 -15
  37. package/dist/control/client.d.ts +22 -0
  38. package/dist/control/client.js +26 -7
  39. package/dist/control/correlation.d.ts +23 -5
  40. package/dist/control/correlation.js +0 -0
  41. package/dist/control/daemon.d.ts +400 -18
  42. package/dist/control/daemon.js +1105 -102
  43. package/dist/control/discovery.d.ts +77 -5
  44. package/dist/control/discovery.js +186 -11
  45. package/dist/control/ensure-hook.d.ts +61 -0
  46. package/dist/control/ensure-hook.js +160 -8
  47. package/dist/control/paths.d.ts +26 -0
  48. package/dist/control/paths.js +34 -0
  49. package/dist/control/pin-guard.d.ts +83 -0
  50. package/dist/control/pin-guard.js +534 -0
  51. package/dist/control/recorder-keeper.d.ts +229 -0
  52. package/dist/control/recorder-keeper.js +721 -0
  53. package/dist/control/recorder-main.d.ts +73 -0
  54. package/dist/control/recorder-main.js +778 -0
  55. package/dist/control/server.d.ts +707 -31
  56. package/dist/control/server.js +2786 -302
  57. package/dist/control/transcript.d.ts +21 -1
  58. package/dist/control/transcript.js +378 -112
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +5 -1
  61. package/dist/jsonrpc/framing.d.ts +76 -12
  62. package/dist/jsonrpc/framing.js +216 -59
  63. package/dist/proxy/intercept.d.ts +49 -5
  64. package/dist/proxy/intercept.js +188 -59
  65. package/dist/proxy/relay.d.ts +16 -0
  66. package/dist/proxy/relay.js +77 -5
  67. package/dist/proxy/revive.d.ts +122 -0
  68. package/dist/proxy/revive.js +251 -0
  69. package/dist/proxy/session.d.ts +222 -14
  70. package/dist/proxy/session.js +708 -99
  71. package/dist/record/recorder.d.ts +51 -3
  72. package/dist/record/recorder.js +3 -0
  73. package/dist/record/redact.d.ts +103 -9
  74. package/dist/record/redact.js +659 -30
  75. package/dist/record/remote-recorder.d.ts +327 -32
  76. package/dist/record/remote-recorder.js +1517 -187
  77. package/dist/record/switchable-recorder.d.ts +126 -0
  78. package/dist/record/switchable-recorder.js +351 -0
  79. package/dist/record/truncate.d.ts +40 -1
  80. package/dist/record/truncate.js +111 -4
  81. package/dist/replay/bash-channel.d.ts +35 -0
  82. package/dist/replay/bash-channel.js +133 -0
  83. package/dist/replay/bundle.d.ts +10 -0
  84. package/dist/replay/bundle.js +10 -0
  85. package/dist/replay/controller.d.ts +369 -20
  86. package/dist/replay/controller.js +1176 -107
  87. package/dist/replay/derive.d.ts +1 -0
  88. package/dist/replay/derive.js +81 -24
  89. package/dist/replay/executor.d.ts +51 -16
  90. package/dist/replay/executor.js +114 -41
  91. package/dist/replay/handover.d.ts +54 -1
  92. package/dist/replay/handover.js +55 -5
  93. package/dist/replay/logic.d.ts +62 -10
  94. package/dist/replay/logic.js +140 -22
  95. package/dist/replay/plan.d.ts +201 -27
  96. package/dist/replay/plan.js +581 -141
  97. package/dist/replay/safety.d.ts +72 -0
  98. package/dist/replay/safety.js +303 -0
  99. package/dist/replay/source-run.d.ts +10 -0
  100. package/dist/replay/source-run.js +80 -35
  101. package/dist/replay/targets.d.ts +15 -1
  102. package/dist/replay/targets.js +62 -4
  103. package/dist/replay/tool-error.d.ts +15 -1
  104. package/dist/replay/tool-error.js +33 -1
  105. package/dist/replay/types.d.ts +7 -4
  106. package/dist/upstream/client.d.ts +56 -3
  107. package/dist/upstream/client.js +61 -4
  108. package/dist/upstream/headers.d.ts +43 -0
  109. package/dist/upstream/headers.js +90 -0
  110. package/dist/upstream/http-client.d.ts +213 -22
  111. package/dist/upstream/http-client.js +1034 -168
  112. package/dist/upstream/http-transport.d.ts +143 -0
  113. package/dist/upstream/http-transport.js +391 -0
  114. package/dist/upstream/lazy-client.d.ts +8 -0
  115. package/dist/upstream/lazy-client.js +29 -1
  116. package/dist/upstream/sse.d.ts +78 -0
  117. package/dist/upstream/sse.js +189 -0
  118. package/dist/upstream/stdio-client.d.ts +61 -1
  119. package/dist/upstream/stdio-client.js +356 -33
  120. package/dist/util/atomic-write.d.ts +29 -0
  121. package/dist/util/atomic-write.js +96 -0
  122. package/dist/util/journal.d.ts +15 -0
  123. package/dist/util/journal.js +48 -3
  124. package/dist/util/log.d.ts +10 -0
  125. package/dist/util/log.js +28 -1
  126. package/dist/util/off-switch.d.ts +46 -0
  127. package/dist/util/off-switch.js +91 -0
  128. package/dist/util/service-health.d.ts +133 -0
  129. package/dist/util/service-health.js +217 -0
  130. package/docs/BaseInstRunner.md +694 -621
  131. package/docs/calculatedReplay.md +1625 -1304
  132. package/docs/calculatedReplayGuide.md +1212 -1087
  133. package/docs/installRun.md +1013 -772
  134. package/docs/loginWeb.md +627 -607
  135. package/docs/mcpmark.md +49 -16
  136. package/docs/my-first-sample.md +569 -545
  137. package/docs/quickstart.md +384 -311
  138. package/docs/t-bench.md +406 -394
  139. package/package.json +58 -57
  140. package/production.md +1371 -0
  141. package/scripts/install.ps1 +97 -6
  142. package/scripts/install.sh +104 -7
@@ -23,17 +23,160 @@ import { type ExecutionOutcome, type ExecutionStepResult, type FallbackKind, typ
23
23
  export declare const DIRECT_TOOL_NAME = "mcp__bir__run_scenario";
24
24
  /** How long a `/proxy/poll` is held open before it answers empty. */
25
25
  export declare const POLL_HOLD_MS = 25000;
26
+ /**
27
+ * The longest a prompt ever waits for a steer plan's parameters so that its
28
+ * directive can preview them ({@link ReplayController.settleForPreview}).
29
+ * `UserPromptSubmit` is on the user's critical path: a preview is worth a
30
+ * little of it, never the derivation's whole budget.
31
+ */
32
+ export declare const PREVIEW_SETTLE_MAX_MS = 1200;
26
33
  export interface ReplayBudgets {
27
34
  /** How long `/session/prompt` waits for the match. */
28
35
  matchMs: number;
29
- /** How long the first `PreToolUse` waits for derivation. */
36
+ /** How long `/scenario/run` waits for derivation, which has no hook timeout. */
30
37
  deriveMs: number;
31
- /** Ceiling on a whole direct plan. */
38
+ /**
39
+ * Ceiling on a whole direct plan. 100 s, so that `deriveMs + planMs` plus the
40
+ * last step's `MIN_STEP_MS` and its logic stays under Claude Code's 2-minute
41
+ * auto-background of an MCP tool call: a backgrounded `run_scenario` leaves
42
+ * the model doing the task while this keeps starting its steps.
43
+ * `BIR_REPLAY_PLAN_BUDGET_MS` (120 000 was the old default; past about
44
+ * 110 000 a long run may be backgrounded).
45
+ */
32
46
  planMs: number;
33
47
  /** Ceiling on one direct `tools/call`. */
34
48
  stepMs: number;
49
+ /**
50
+ * How long a *hook* waits for derivation: arming inside `UserPromptSubmit`
51
+ * (whose whole answer, match included, must land within the control server's
52
+ * prompt deadline) and the first `PreToolUse`. Capped by `deriveMs`.
53
+ * `BIR_HOOK_DERIVE_BUDGET_MS`.
54
+ */
55
+ hookDeriveMs: number;
56
+ /**
57
+ * Everything replay does inside one `PreToolUse`: the wait for the
58
+ * parameters and, on a divergence, executing the rest of the plan. The host
59
+ * gives the hook 15 s and the control server answers by 12 s whatever
60
+ * happens, so this stays below both: a step started after the host stopped
61
+ * waiting is a side effect the model is never told about.
62
+ * `BIR_PRETOOL_REPLAY_BUDGET_MS`, held at or under
63
+ * {@link MAX_PRETOOL_REPLAY_MS} whatever is asked.
64
+ */
65
+ preToolMs: number;
35
66
  }
36
67
  export declare const DEFAULT_BUDGETS: ReplayBudgets;
68
+ /**
69
+ * The most {@link ReplayBudgets.preToolMs} may be: the last step started at the
70
+ * budget's end still gets its {@link MIN_STEP_MS} and the margin before the
71
+ * route's deadline answers the host. Past it, a divergence kept starting steps
72
+ * — writes, unattended — after the model had been told to carry on itself.
73
+ */
74
+ export declare const MAX_PRETOOL_REPLAY_MS: number;
75
+ /** What the caller's hook knows about its own deadline (see {@link ReplayController.preTool}). */
76
+ export interface PreToolOptions {
77
+ /**
78
+ * When the caller answers the host whatever happens (epoch ms). No step is
79
+ * started later than {@link MIN_STEP_MS} and a margin before it.
80
+ */
81
+ answerBy?: number;
82
+ /**
83
+ * Aborted once the host has been answered without this call's action — the
84
+ * route's deadline guard spoke first. No further step is started.
85
+ */
86
+ signal?: AbortSignal;
87
+ }
88
+ /**
89
+ * What the caller knows about how a pinned call ended, beyond its error text
90
+ * ({@link ReplayController.postTool}). Both optional: without them the text
91
+ * and the output are read (`pinFailure`).
92
+ */
93
+ export interface PostToolOptions {
94
+ /**
95
+ * The call was sent and never answered — its proxy abandoned it (the
96
+ * upstream closed or restarted, the host cancelled it) or its answer could
97
+ * not be delivered. It may or may not have done its work.
98
+ */
99
+ uncertain?: boolean;
100
+ /** The call was never sent: its upstream was already gone. Nothing of it happened. */
101
+ notSent?: boolean;
102
+ }
103
+ /**
104
+ * The longest a direct run may take by its budgets, end to end.
105
+ *
106
+ * `bir-scenario` gives `/scenario/run` 290 s (`BIR_SCENARIO_RUN_TIMEOUT_MS`),
107
+ * just under Claude Code's 5-minute idle timeout for an MCP call, and a run
108
+ * that answers after its client stopped waiting is a replay thrown away with
109
+ * its steps already done. So the budgets are held to 270 s, and the defaults
110
+ * sit far below it: 8 s of derivation + 100 s of plan (more only when an
111
+ * operator sets `BIR_REPLAY_BUDGET_MS` or `BIR_REPLAY_PLAN_BUDGET_MS`) + 12 s
112
+ * of slack (one last step's 1 s, six 1 s logic bodies, a 5 s margin —
113
+ * {@link clampDirectRun}) = 120 s.
114
+ *
115
+ * Only `/scenario/run` is held to it. `bir replay` (`runAdHoc`) answers the
116
+ * operator's own command, which no MCP client times out, and keeps the budgets
117
+ * the environment asked for.
118
+ */
119
+ export declare const DIRECT_RUN_CEILING_MS = 270000;
120
+ /**
121
+ * The direct-run budgets, held under {@link DIRECT_RUN_CEILING_MS}.
122
+ *
123
+ * The worst case is `deriveMs + planMs` plus what can happen past the plan's
124
+ * deadline: one last step's {@link MIN_STEP_MS}, {@link LOGIC_BODIES_OUTSIDE_PLAN}
125
+ * logic bodies, and the margin. `stepMs` never adds to it — each step is
126
+ * clipped to what the plan has left. Budgets that would pass the ceiling are
127
+ * brought under it — the derivation wait first to {@link MAX_DIRECT_DERIVE_MS},
128
+ * then the plan — and `clamped` says what changed, for the log.
129
+ */
130
+ export declare function clampDirectRun(budgets: ReplayBudgets, logicMs?: number): {
131
+ budgets: ReplayBudgets;
132
+ clamped?: {
133
+ deriveMs?: [number, number];
134
+ planMs?: [number, number];
135
+ };
136
+ };
137
+ /**
138
+ * What `/scenario/run` answers — {@link ReplayController.runArmed}'s result,
139
+ * sent as the response body as it is. `bir-scenario` renders it (bin/
140
+ * bir-scenario.ts, `toolResult`); these are the shapes it can meet:
141
+ *
142
+ * - **ok** — the plan ran to its end: `{ ok: true, text, responseModel, steps,
143
+ * partial: false }`. `text` is the bundle; `responseModel` what the
144
+ * scenario's own logic built from it (`{}` when it has none).
145
+ * - **partial** — the plan stopped part-way after running steps here: `{ ok:
146
+ * true, text, responseModel: {}, steps, partial }`. `text` opens with the
147
+ * hand-over note (text beginning `[BaseInstRunner calculated replay]`) and
148
+ * carries the bundle of what ran below it. `partial` is true only when the
149
+ * time budget, or the caller leaving, cut it short; a step that broke says
150
+ * so in the note instead.
151
+ * - **hand-over note** — nothing ran here, and the model does the task: `{ ok:
152
+ * false, why: <note>, note: true }`. `why` is written for the model and
153
+ * opens with `[BaseInstRunner calculated replay]`; `note` marks it so, for
154
+ * a client that should pass it through rather than say something of its own.
155
+ * - **no_plan** — nothing is armed for this call, or the plan retired before
156
+ * it ran: `{ ok: false, why: "no_plan" }`. The common case, never an error.
157
+ * - **not run** — the run itself threw, or the scenario's logic did: `{ ok:
158
+ * false, why }` with a short reason for the log (`"not_ready"`, `"the
159
+ * scenario could not be run"`), never written for the model.
160
+ * - **already running** — a second `runArmed` for a plan that is running is
161
+ * handed the first one's promise and so the first one's result, whichever
162
+ * shape it takes: the plan is never driven twice. (At HTTP, the control
163
+ * server answers a second `/scenario/run` for a running plan itself, with
164
+ * `{ ok: false, why: <its ALREADY_RUNNING text> }`, before it reaches here.)
165
+ *
166
+ * Only `ok: true` results carry `text`; only `ok: false` ones carry `why`.
167
+ */
168
+ export interface RunArmedResult {
169
+ ok: boolean;
170
+ why?: string;
171
+ /** Set on a hand-over: `why` is a note written for the model, not a reason for the log. */
172
+ note?: boolean;
173
+ text?: string;
174
+ responseModel?: Record<string, unknown>;
175
+ steps?: number;
176
+ partial?: boolean;
177
+ }
178
+ /** The `why` of a {@link RunArmedResult} with nothing armed to run. */
179
+ export declare const RUN_NO_PLAN = "no_plan";
37
180
  export interface ReplayOptions {
38
181
  /** On unless `BIR_REPLAY=0`. Everything here is inert when false. */
39
182
  enabled: boolean;
@@ -65,6 +208,15 @@ export interface ReplayOptions {
65
208
  fetchImpl?: typeof fetch;
66
209
  /** Anthropic key for derivation. Absent → recorded sample values, free. */
67
210
  apiKey?: string;
211
+ /**
212
+ * Let a step whose tool could not be *sent* here — a built-in during
213
+ * divergence recovery, a server with no proxy — stand on its recorded output,
214
+ * labelled `(recorded)`, instead of stopping the plan there. Off by default:
215
+ * a recorded "INSERT 0 1" is last week's write, not this turn's
216
+ * (plan.ts, `DriveOptions`). `BIR_REPLAY_RECORDED_OUTPUTS=1` when omitted.
217
+ * Never applies to a step that was sent and failed or did not answer.
218
+ */
219
+ recordedFallback?: boolean;
68
220
  /**
69
221
  * Intent matching in the ReAct loop (fallbk.md §Runner 4): while the model is
70
222
  * driving, each tool call's reasoning is matched against scenario intents.
@@ -113,6 +265,14 @@ interface PinnedCall {
113
265
  toolName: string;
114
266
  /** When it was pinned — a steered step's duration is measured across hooks. */
115
267
  pinnedAt: number;
268
+ /**
269
+ * When the host said the call finished (`PostToolUse`), if the control
270
+ * server told us ({@link ReplayController.hostCompleted}). A wrapped step
271
+ * threads only when the proxy's report lands, which can be up to the
272
+ * server's 1.5 s fallback later: until then the *next* call is sequential,
273
+ * not concurrent, and is worth waiting for.
274
+ */
275
+ hostDoneAt?: number;
116
276
  }
117
277
  /**
118
278
  * Per-run replay state. Created on every match — including declined ones,
@@ -130,6 +290,13 @@ export interface ReplayState {
130
290
  plan?: ScenarioReplayPlan;
131
291
  /** `tool_use_id` → the step it pinned. */
132
292
  pinned: Map<string, PinnedCall>;
293
+ /**
294
+ * Pins still in flight when the plan handed over because another call ran
295
+ * beside them. The plan is retired, but those calls were pinned (and
296
+ * approved) under it: their results are still read when they land, so the
297
+ * step's verdict is recorded and the ledger counts work that happened.
298
+ */
299
+ lingering?: Map<string, PinnedCall>;
133
300
  outcome: ExecutionOutcome;
134
301
  deriveCostUsd: number;
135
302
  fallbackCostUsd: number;
@@ -214,6 +381,25 @@ export interface ReplayState {
214
381
  */
215
382
  retiredMark?: unknown;
216
383
  retiredAt?: number;
384
+ /**
385
+ * The one `/scenario/run` executing this plan. A second call — a retry after
386
+ * a client timeout, a double call — waits for its answer rather than driving
387
+ * the same cursor a second time.
388
+ */
389
+ running?: Promise<RunArmedResult>;
390
+ /** The model called `run_scenario` for this plan (its `PreToolUse` was seen). */
391
+ directRequested?: boolean;
392
+ /**
393
+ * The direct run's caller went away mid-run — Esc on `run_scenario`, or its
394
+ * client timing out. The turn it cut short is no measurement of the task, so
395
+ * the execution report says `measured: false`.
396
+ */
397
+ callerCancelled?: boolean;
398
+ /**
399
+ * The derivation is the service's to answer (no key here), so waiting on it
400
+ * is waiting on the service, and obeys its breaker (util/service-health.ts).
401
+ */
402
+ deriveOnService?: boolean;
217
403
  }
218
404
  /**
219
405
  * Why a match did not arm. One code per gate of the ladder, so the caller can
@@ -249,9 +435,26 @@ export type PreToolAction =
249
435
  export declare class ReplayController {
250
436
  readonly enabled: boolean;
251
437
  readonly work: ProxyWorkQueue;
438
+ /** The budgets every path runs on, a direct run's held under {@link DIRECT_RUN_CEILING_MS}. */
252
439
  readonly budgets: ReplayBudgets;
440
+ /**
441
+ * `bir replay`'s derivation wait and plan budget: what the environment asked
442
+ * for, unclamped — no MCP client times that run out (see
443
+ * {@link DIRECT_RUN_CEILING_MS}).
444
+ */
445
+ readonly adHocBudgets: Readonly<Pick<ReplayBudgets, "deriveMs" | "planMs">>;
253
446
  readonly intentMatch: IntentMatchOptions;
254
447
  private readonly opts;
448
+ /** See {@link ReplayOptions.recordedFallback}. */
449
+ private readonly recordedFallback;
450
+ /** Calls waiting for a plan's pins to change (see `awaitPins`). */
451
+ private readonly pinWaiters;
452
+ /**
453
+ * Set the first time the control server reports a host completion
454
+ * ({@link hostCompleted}). From then on a pin the host has not finished is a
455
+ * call really running beside the next one, and nothing waits for it.
456
+ */
457
+ private hostSignals;
255
458
  constructor(opts: ReplayOptions);
256
459
  /**
257
460
  * Whether a handed-out segment may actually arm (segmented.md R-OUT-10).
@@ -279,8 +482,39 @@ export declare class ReplayController {
279
482
  * up with a denominator nobody measured.
280
483
  */
281
484
  arm(match: RunMatchLike, prompt: string, wrapped: ReadonlySet<string>, armedBy?: "prompt" | "intent", ctx?: DeriveContext): Promise<ReplayState>;
282
- /** The directive to inject via `additionalContext`, or undefined when declined. */
283
- directiveFor(state: ReplayState): string | undefined;
485
+ /**
486
+ * The directive to inject via `additionalContext`, or undefined when declined.
487
+ *
488
+ * A steer directive previews each step's arguments (plan.ts,
489
+ * `previewArguments`), which can take up to {@link PREVIEW_BUDGET_MS}.
490
+ * `previewMs` — the time the caller's hook has left — lowers that bound,
491
+ * never raises it; under ~10 ms nothing is previewed and the directive keeps
492
+ * its old wording.
493
+ */
494
+ directiveFor(state: ReplayState, opts?: {
495
+ previewMs?: number;
496
+ }): string | undefined;
497
+ /**
498
+ * Give a steer plan's parameters a short, bounded chance to settle before its
499
+ * directive is written, so {@link directiveFor} can show the arguments the
500
+ * steps run with. Resolves true when they are settled.
501
+ *
502
+ * Without it a derivation the service answers — nearly every one, since a
503
+ * signed-in runner has no key of its own — has never answered by the time
504
+ * `arm` returns (it waits one turn of the loop, on purpose), the directive
505
+ * says "the system supplies the exact arguments", the model writes a guessed
506
+ * command, the pin guard declines it, and the plan retires having run
507
+ * nothing.
508
+ *
509
+ * Only for the steps the pin guard holds to their exact shape — a shell
510
+ * command, a file write (control/pin-guard.ts) — and never longer than
511
+ * {@link PREVIEW_SETTLE_MAX_MS} or `budgetMs`, whichever is less: the caller
512
+ * passes what its prompt hook has left, less what the preview itself needs.
513
+ * Not at all while the service is cooling down. A late answer is not an
514
+ * overrun of anything, so it marks nothing against the service. `arm`
515
+ * itself still never waits (integ-replay R2).
516
+ */
517
+ settleForPreview(state: ReplayState, budgetMs: number): Promise<boolean>;
284
518
  /**
285
519
  * A hand-over note not yet delivered, claimed for delivery (fallbk.md D4).
286
520
  * Returns it once and marks it delivered; undefined when there is none.
@@ -298,10 +532,46 @@ export declare class ReplayController {
298
532
  * Three tiers (§8): pin the expected call; on divergence execute the remainder
299
533
  * and hand it back through the best channel available; if even that fails,
300
534
  * abort to an ordinary turn.
535
+ *
536
+ * `hook` is optional and only ever tightens the work: `answerBy` lowers the
537
+ * deadline to the caller's own, and `signal` stops a divergence from
538
+ * starting another step once the host has been answered without it.
301
539
  */
302
540
  preTool(state: ReplayState, toolName: string, toolUseId: string,
303
541
  /** The call's `tool_input`: whether a `Bash` call is only `bir` is read from it (D13). */
304
- toolInput?: unknown): Promise<PreToolAction>;
542
+ toolInput?: unknown, hook?: PreToolOptions): Promise<PreToolAction>;
543
+ /**
544
+ * A call arrived while another pinned call's output has not threaded yet
545
+ * (see `preTool`). The plan stops here rather than pin a step twice.
546
+ */
547
+ private concurrentCall;
548
+ /**
549
+ * Wait until the plan's outstanding pins thread, or until waiting is no
550
+ * longer worth it (see `preTool`, ONE CALL AT A TIME).
551
+ *
552
+ * Always bounded by the hook's `deadline`. Within it:
553
+ * - every pin finished by the host ({@link hostCompleted}): until
554
+ * {@link PIN_THREAD_WAIT_MS} after the last of them finished — the
555
+ * server's fallback has threaded it by then, if anything will;
556
+ * - the server is known to report host completions and a pin is not
557
+ * finished: a call really running beside this one, which may run for
558
+ * minutes, so only {@link HOST_RACE_MS} in case its completion is racing
559
+ * this hook;
560
+ * - otherwise (a server that reports nothing): {@link PIN_THREAD_WAIT_MS}.
561
+ */
562
+ private awaitPins;
563
+ /** Resolves on the next change to a plan's pins, or after `ms`. */
564
+ private pinChange;
565
+ /** Wake every call waiting on this plan's pins. */
566
+ private notifyPins;
567
+ /**
568
+ * The host finished a call (`PostToolUse` / `PostToolUseFailure`) — whether
569
+ * or not its output has threaded yet. The control server calls this for
570
+ * every pinned call; it is what tells the model's next, sequential call
571
+ * apart from one running beside a pin (see `awaitPins`). Optional: without
572
+ * it every such call waits the bounded {@link PIN_THREAD_WAIT_MS}.
573
+ */
574
+ hostCompleted(state: ReplayState | undefined, toolUseId: string): void;
305
575
  /**
306
576
  * `PostToolUse` for a call this plan pinned.
307
577
  *
@@ -309,26 +579,60 @@ export declare class ReplayController {
309
579
  * wrapped MCP step that is the proxy's whole `CallToolResult`, which is why the
310
580
  * server threads those from `/proxy/step` rather than from the hook (§7.2).
311
581
  * Returns true when the plan is now complete.
312
- */
313
- postTool(state: ReplayState, toolUseId: string, output: string): boolean;
582
+ *
583
+ * `failed` is the host's verdict that the call failed — `PostToolUseFailure`,
584
+ * or a proxy report with `isError` — with its error text when there is one.
585
+ * It outranks anything read from `output`: a built-in's response is Claude
586
+ * Code's rendering and is never judged here, so for an `Edit` whose
587
+ * `old_string` moved since the recording this is the only word there is. A
588
+ * step that failed must never thread as if it worked: the next step would be
589
+ * pinned, and auto-approved, on top of it.
590
+ *
591
+ * Not every failure is one. A call its proxy *abandoned* — the upstream
592
+ * closed, restarted or was cancelled mid-call, reported with `isError` and
593
+ * no result — may well have done its work, and so may one the host says
594
+ * timed out or was interrupted: the note says "may or may not have
595
+ * completed", never "failed", or the model runs a write a second time
596
+ * ({@link pinFailure}). `how` lets the caller say so outright.
597
+ */
598
+ postTool(state: ReplayState, toolUseId: string, output: string, failed?: string, how?: PostToolOptions): boolean;
599
+ /**
600
+ * The result of a call pinned under a plan that has since handed over
601
+ * because another call ran beside it (`concurrentCall`). Nothing threads —
602
+ * the plan is retired and the model is doing the rest — but the step's
603
+ * verdict is recorded, and a step that did its work is counted: the outcome
604
+ * moves from `failed` to `fell_back` when it is the first that did.
605
+ */
606
+ private lateThread;
314
607
  /** Whether a pinned call's output is threaded from the proxy's report (§7.2). */
315
608
  threadsFromProxy(state: ReplayState, toolUseId: string): boolean;
316
- /** True when this call belongs to the active plan. */
609
+ /**
610
+ * True when this call belongs to the plan: pinned by it and not yet
611
+ * threaded — including a pin still in flight when the plan handed over, so
612
+ * its result still reaches `postTool` (see `lateThread`).
613
+ */
317
614
  isPinned(state: ReplayState, toolUseId: string): boolean;
318
615
  /**
319
616
  * `POST /scenario/run` — the whole of a `direct` plan, executed here.
320
617
  *
321
618
  * Every step runs through the proxy that already owns its upstream, so the
322
619
  * model spends nothing beyond the turn that reads the results.
620
+ *
621
+ * `signal` aborts when the caller is gone — `bir-scenario`'s request was
622
+ * cancelled (Esc) or timed out — so no further step is started (C4). The
623
+ * worst case this can take is `deriveMs + planMs` plus one step's
624
+ * `MIN_STEP_MS`, six logic bodies at their full limit and a 5 s margin —
625
+ * 120 s by default, more only for a plan budget an operator raised
626
+ * ({@link clampDirectRun}) — which `bir-scenario`'s own timeout (290 s) must
627
+ * exceed, and which is held under {@link DIRECT_RUN_CEILING_MS} whatever the
628
+ * environment asks for. That bound counts every body at its limit; a run
629
+ * that does not stall ends well inside its plan budget.
630
+ *
631
+ * One run per plan: a second call while the first is running gets the first
632
+ * one's answer, never a second execution of the same steps.
323
633
  */
324
- runArmed(state: ReplayState): Promise<{
325
- ok: boolean;
326
- why?: string;
327
- text?: string;
328
- responseModel?: Record<string, unknown>;
329
- steps?: number;
330
- partial?: boolean;
331
- }>;
634
+ runArmed(state: ReplayState, signal?: AbortSignal): Promise<RunArmedResult>;
635
+ private runArmedOnce;
332
636
  /**
333
637
  * Build the execution report for a sealed run, or undefined when there is
334
638
  * nothing to report.
@@ -383,6 +687,13 @@ export declare class ReplayController {
383
687
  outcome: string;
384
688
  ms: number;
385
689
  }>;
690
+ /** Where the run stopped short of its end, and why, when it did. */
691
+ stopped?: {
692
+ step: number;
693
+ tool: string;
694
+ reason: string;
695
+ error?: string;
696
+ };
386
697
  }>;
387
698
  /** Release every parked poller. Called at control-server shutdown. */
388
699
  close(): void;
@@ -408,8 +719,18 @@ export declare class ReplayController {
408
719
  *
409
720
  * The plan is retired first: whatever happens next, this turn neither steers
410
721
  * nor re-injects again.
722
+ *
723
+ * This runs inside the diverging call's `PreToolUse`, so `deadline` bounds
724
+ * it: no step is started after it, and none is waited on past it. The bundle
725
+ * then says exactly what ran, and hands the rest over.
411
726
  */
412
727
  private diverge;
728
+ /**
729
+ * The note for a divergence bundle that stopped at a step, recorded as this
730
+ * plan's hand-over (already delivered — it goes out with the bundle), so the
731
+ * model's own work from here is recorded as a fragment.
732
+ */
733
+ private stopNote;
413
734
  /**
414
735
  * Hand the rest of the task to the model (fallbk.md D4/D5).
415
736
  *
@@ -431,11 +752,40 @@ export declare class ReplayController {
431
752
  * The executor handed to a plan: dispatch a step to the proxy that owns its
432
753
  * upstream.
433
754
  *
434
- * Rejecting means "could not be run **here**" — which is the only condition
435
- * under which a recorded output may stand in. A tool that ran and failed
436
- * resolves with its failure as the response, exactly as it would in a session.
755
+ * A tool that ran and failed resolves with its failure as the response,
756
+ * exactly as it would in a session. A rejection is a {@link StepRunError}
757
+ * saying what happened to the call — not sent, sent and unanswered, or
758
+ * refused — because the model is told differently about each (plan.ts).
759
+ *
760
+ * `deadline` is the whole run's: no step is waited on past it (but each gets
761
+ * at least {@link MIN_STEP_MS}). `adHoc` is `bir replay`, which the operator
762
+ * asked for by id and which the allowlist does not gate.
437
763
  */
438
764
  private executeStep;
765
+ /** Where recorded outputs come from, when they may stand in at all. */
766
+ private recordedSource;
767
+ /**
768
+ * Wait for a plan's parameters within `budgetMs`, then judge whether the plan
769
+ * may run on them. Never throws.
770
+ *
771
+ * Out of time, the parameters settle without the derivation
772
+ * (`useFallbackParams`: settings at their samples, targets null) — never on
773
+ * `{}` — and the plan runs only if it needs no target it was not given. A
774
+ * `paramsLogic` that throws, or a value too dangerous to template into a
775
+ * command (safety.ts), stops it. `adHoc` keeps `bir replay`'s contract: the
776
+ * operator named the scenario, so neither judgement applies.
777
+ */
778
+ private readyFor;
779
+ /**
780
+ * Wait for something the derivation decides, within `budgetMs`. Resolves
781
+ * undefined when the time ran out.
782
+ *
783
+ * When the service is the one deriving, this is a service wait on a hot path
784
+ * (C1): it does not wait at all while the service is cooling down, and an
785
+ * overrun is reported to the breaker so the next hook does not wait either.
786
+ * Even a zero wait lets an answer that has already arrived through.
787
+ */
788
+ private waitDerivation;
439
789
  /**
440
790
  * Where a pinned step's output will arrive from — the proxy's report, or the
441
791
  * hook's `tool_response` (§7.2).
@@ -465,7 +815,6 @@ export declare class ReplayController {
465
815
  /** Outcomes only move forward; `not_steered` is the floor (§11.1). */
466
816
  private upgrade;
467
817
  private retire;
468
- private withBudget;
469
818
  }
470
819
  /** The subset of `RunMatch` replay consumes. Structural, so tests need no recorder. */
471
820
  export interface RunMatchLike {