@basein/runner 0.3.10 → 0.3.11
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.
- package/README.md +2 -2
- package/dist/bin/investigate.js +3 -3
- package/dist/config/adapters/claude-code.d.ts +1 -1
- package/dist/config/adapters/claude-code.js +2 -2
- package/dist/control/server.d.ts +5 -0
- package/dist/control/server.js +25 -8
- package/dist/replay/controller.d.ts +8 -1
- package/docs/calculatedReplay.md +1 -1
- package/docs/calculatedReplayGuide.md +1 -1
- package/package.json +1 -1
- package/production.md +5 -5
package/README.md
CHANGED
|
@@ -488,7 +488,7 @@ what to do when they register, so there is one switch and no second copy of it:
|
|
|
488
488
|
| `ANTHROPIC_API_KEY` | Not used by the background recorder, which has Claude Code's environment and would bill your own key — unless `BIR_USE_ANTHROPIC_API_KEY=1`. A `bir-hooks` run by hand in a terminal uses that terminal's key. |
|
|
489
489
|
| `BIR_DERIVE_MODEL` | Derivation model, when you set a key of your own (default `claude-haiku-4-5-20251001`). |
|
|
490
490
|
| `BIR_MATCH_BUDGET_MS` | How long the prompt hook waits for a match (default `3000`). After a match slower than `BIR_MATCH_SLOW_MS` (default `2000`), or one that timed out, prompts stop waiting for 5 min, doubling up to 30 min; a fast match ends that. |
|
|
491
|
-
| `BIR_DERIVE_BUDGET_MS` / `BIR_HOOK_DERIVE_BUDGET_MS` | How long a direct run, and a hook, waits for parameters (default `8000` / `8000`; a hook waits the smaller, and the prompt hook never past its own
|
|
491
|
+
| `BIR_DERIVE_BUDGET_MS` / `BIR_HOOK_DERIVE_BUDGET_MS` | How long a direct run, and a hook, waits for parameters (default `8000` / `8000`; a hook waits the smaller, and the prompt hook never past its own 7.5 s). |
|
|
492
492
|
| `BIR_PRETOOL_REPLAY_BUDGET_MS` | Everything replay does inside one `PreToolUse` (default `9000`, at most `10000`). |
|
|
493
493
|
| `BIR_REPLAY_BUDGET_MS` / `BIR_STEP_TIMEOUT_MS` | Whole-plan and per-step ceilings for direct execution (default `100000` / `60000`; `BIR_REPLAY_PLAN_BUDGET_MS` also sets the first). A direct run's parameter wait plus its plan are held under 270 s. |
|
|
494
494
|
| `BIR_LOGIC_TIMEOUT_MS` | How long one scenario logic body may run before it is stopped and the plan hands over (default `1000`, at most `10000`). |
|
|
@@ -525,7 +525,7 @@ The governing rule: **a host session never fails because of BaseInstRunner.**
|
|
|
525
525
|
| No recorder running at session start | the SessionStart hook starts one in the background; it always exits 0 and never holds the start longer than about 11.5 s |
|
|
526
526
|
| Control server unreachable at startup | steps buffered ≤ 10 s, then Tier 2 (passthrough: nothing); the proxy keeps looking every 15 s and binds when one answers |
|
|
527
527
|
| Control server dies mid-session | the proxy records to a Tier 2 run of its own (flagged lossy), starts the recorder again in the background (not after `bir down`), and binds to it once it answers |
|
|
528
|
-
| A hook the recorder cannot answer in time | every route has a hard deadline (prompt
|
|
528
|
+
| A hook the recorder cannot answer in time | every route has a hard deadline (prompt 8.5 s, `PreToolUse` 12 s, the others 1–2 s) and answers `{}`; so does a hook with a stale token or a malformed body |
|
|
529
529
|
| BaseIn service slow or down | hooks stop waiting on it after one failure (a cool-down of 5 s, doubling to 60 s); sends go on in the background, retried once only when they cannot have arrived (429, 503, a refused connection); past 500 queued sends or 32 MiB the oldest steps are dropped |
|
|
530
530
|
| BaseIn service answers `429`: the account is over one of its limits | the send — a run's create, a step, a report — goes once more, at most 2 s later, and is then dropped, with `recorder.send_failed … HTTP 429` on the log (one line per run). A dropped step is a hole in its recording; a run whose create was refused runs without its plan; a `429` the second try did not cure starts the cool-down above. The limits are settings of the service, per account — by default 120 runs and 3,000 steps a minute (a tool call is two steps), 10 title requests a minute, and 20 Calculate, Prefer and Recalculate requests in any one hour — and are built there, not deployed yet |
|
|
531
531
|
| Upstream fails to start, or exits | calls in flight are answered (`MCP server "<name>" is not available`, `isError: true` for a `tools/call`), then `bir-proxy` exits with the server's own status, so Claude Code shows the server as failed, as it would unwrapped. `BIR_PROXY_STAY_UP=1` keeps the proxy up answering that instead |
|
package/dist/bin/investigate.js
CHANGED
|
@@ -214,8 +214,8 @@ export const DECLINES = {
|
|
|
214
214
|
// Not a gate of the ladder: the control server's own verdict, when arming
|
|
215
215
|
// outlived the hook that would have announced the plan.
|
|
216
216
|
arm_timeout: {
|
|
217
|
-
cause: "the plan was not ready before the hook that would have announced it had to answer: the prompt hook waits
|
|
218
|
-
fix: "run the prompt again. If it keeps happening the reading is slow: the recorder's log (`bir status` names the file) shows when its `replay.derived` line landed. BIR_HOOK_DERIVE_BUDGET_MS does not help here, because the hook's own limit was reached first. To read values on this machine instead of asking the service, set BIR_DERIVE_API_KEY where Claude Code starts.",
|
|
217
|
+
cause: "the plan was not ready before the hook that would have announced it had to answer: the prompt hook waits 7.5 s in all, match included, and a tool call that starts a plan mid-task waits 1.5 s. What arming waits for is the reading of the prompt's values (the derivation). A plan that was not announced is never run.",
|
|
218
|
+
fix: "run the prompt again. If it keeps happening the reading is slow: the recorder's log (`bir status` names the file) shows when its `replay.derived` line landed, and a dry replay shows what the service's reading took, under `reading` (`bir scenario replay <scnId> --prompt \"…\" --dry --json`). BIR_HOOK_DERIVE_BUDGET_MS does not help here, because the hook's own limit was reached first. To read values on this machine instead of asking the service, set BIR_DERIVE_API_KEY where Claude Code starts.",
|
|
219
219
|
},
|
|
220
220
|
runner_update_required: {
|
|
221
221
|
cause: "the service matched the prompt and handed over no plan: this runner is older than its plans need.",
|
|
@@ -287,7 +287,7 @@ function readingDecline(code, why) {
|
|
|
287
287
|
return {
|
|
288
288
|
cause: `the plan needs a target, a value read from the prompt, and that reading (the derivation) had not answered when the runner stopped waiting for it: its wait is ${waitedMs} ms, and none while the service is cooling down after a slow or failed call. ` +
|
|
289
289
|
"Whether the prompt names the target was never learned. A replay never takes a target from the recording.",
|
|
290
|
-
fix: "nothing in the prompt needs to change: run it again. If it keeps happening the reading is slower than the wait, and the recorder's log (`bir status` names the file) has its `replay.derived` line after this decline. Give it longer: set BIR_HOOK_DERIVE_BUDGET_MS where the recorder starts, then `bir up --restart`. The prompt hook stops
|
|
290
|
+
fix: "nothing in the prompt needs to change: run it again. If it keeps happening the reading is slower than the wait, and the recorder's log (`bir status` names the file) has its `replay.derived` line after this decline. Give it longer: set BIR_HOOK_DERIVE_BUDGET_MS where the recorder starts, then `bir up --restart`. The prompt hook stops 7.5 s after the prompt arrived, whatever is set.",
|
|
291
291
|
};
|
|
292
292
|
}
|
|
293
293
|
// Gate 7 writes `<reason>: target <name>`, and `no_derive_key` itself as the
|
|
@@ -182,7 +182,7 @@ export declare function setLocalEntries(claudeJson: string, keys: string[], name
|
|
|
182
182
|
* to its `timeout`, so each number here is how long a slow or blackholed
|
|
183
183
|
* service could make a person wait — at every prompt, every tool call, every
|
|
184
184
|
* `/exit`. The control server keeps its own per-route deadlines well inside
|
|
185
|
-
* them (prompt ≤
|
|
185
|
+
* them (prompt ≤ 8.5 s, tool/pre ≤ 12 s, tool/post and stop ≤ 2 s, end ≤ 1 s,
|
|
186
186
|
* start ≤ 2 s) and answers `{}` on overrun, so these are the second line of
|
|
187
187
|
* defence, not a budget to spend. SessionEnd matters twice: Claude Code's own
|
|
188
188
|
* default for it is 1.5 s, and whatever is written here RAISES that — the 30 it
|
|
@@ -448,7 +448,7 @@ export function setLocalEntries(claudeJson, keys, name, entry, shouldTouch = ()
|
|
|
448
448
|
* to its `timeout`, so each number here is how long a slow or blackholed
|
|
449
449
|
* service could make a person wait — at every prompt, every tool call, every
|
|
450
450
|
* `/exit`. The control server keeps its own per-route deadlines well inside
|
|
451
|
-
* them (prompt ≤
|
|
451
|
+
* them (prompt ≤ 8.5 s, tool/pre ≤ 12 s, tool/post and stop ≤ 2 s, end ≤ 1 s,
|
|
452
452
|
* start ≤ 2 s) and answers `{}` on overrun, so these are the second line of
|
|
453
453
|
* defence, not a budget to spend. SessionEnd matters twice: Claude Code's own
|
|
454
454
|
* default for it is 1.5 s, and whatever is written here RAISES that — the 30 it
|
|
@@ -460,7 +460,7 @@ export const HOOK_ROUTES = [
|
|
|
460
460
|
{ event: "SessionStart", route: "/session/start", matcher: "", timeout: 5 },
|
|
461
461
|
// Awaits the similar-prompt match and the derivation before it can return a
|
|
462
462
|
// steering directive (docs/calculatedReplay.md §10); the server caps the
|
|
463
|
-
// whole route at
|
|
463
|
+
// whole route at 8.5 s.
|
|
464
464
|
//
|
|
465
465
|
// It must NEVER become `async`. An async hook is fire-and-forget, so it cannot
|
|
466
466
|
// return `additionalContext` at all — replay would silently never arm.
|
package/dist/control/server.d.ts
CHANGED
|
@@ -712,6 +712,11 @@ export declare class ControlServer {
|
|
|
712
712
|
* told of it, so it must never steer or diverge a call: it is kept only as
|
|
713
713
|
* what it now is — a matched turn the agent does itself, which still owes the
|
|
714
714
|
* ledger its baseline sample (§11.1) — exactly like a declined match.
|
|
715
|
+
*
|
|
716
|
+
* `why` is the code the report carries when the plan was only late. Without
|
|
717
|
+
* it the service got a matched turn that was not steered and no reason, and
|
|
718
|
+
* its console could only say that the runner had given none. A plan a gate
|
|
719
|
+
* declined keeps the gate's own code.
|
|
715
720
|
*/
|
|
716
721
|
private shelve;
|
|
717
722
|
/**
|
package/dist/control/server.js
CHANGED
|
@@ -64,7 +64,9 @@ const PROXY_REPORT_GRACE_MS = 1_500;
|
|
|
64
64
|
*/
|
|
65
65
|
const HOOK_DEADLINE_MS = {
|
|
66
66
|
"/session/start": 2_000,
|
|
67
|
-
|
|
67
|
+
// A second past {@link PROMPT_WAIT_MS}, and a second and a half inside the
|
|
68
|
+
// 10 s Claude Code gives the hook.
|
|
69
|
+
"/session/prompt": 8_500,
|
|
68
70
|
"/tool/pre": 12_000,
|
|
69
71
|
"/tool/post": 2_000,
|
|
70
72
|
"/session/agent": 2_000,
|
|
@@ -79,10 +81,19 @@ const PREVIEW_RESERVE_MS = 500;
|
|
|
79
81
|
/**
|
|
80
82
|
* What `UserPromptSubmit` may spend on replay: the similar-prompt match plus
|
|
81
83
|
* any derivation a matched scenario with a target needs before its directive
|
|
82
|
-
* can go out (segmented.md R-PARAM-4). Below the route's
|
|
83
|
-
* decision to steer is always taken here and never by the guard.
|
|
84
|
+
* can go out (segmented.md R-PARAM-4). Below the route's 8.5 s deadline, so
|
|
85
|
+
* the decision to steer is always taken here and never by the guard.
|
|
86
|
+
*
|
|
87
|
+
* LONGER THAN THE SERVICE MAY TAKE TO READ, ON PURPOSE. The service gives a
|
|
88
|
+
* reading 6 s (its RUNNER_DERIVE_TIMEOUT_MS) and answers either way, so with
|
|
89
|
+
* the match and the round trip an answer can be 6.5 s after the prompt. At
|
|
90
|
+
* 6 s this wait ended before that answer could arrive: on 2026-10-08 a
|
|
91
|
+
* reading that took 6.0 s armed its plan 0.3 s after the prompt had been
|
|
92
|
+
* answered, and a good 20-step plan was thrown away (`arm_timeout`) for a
|
|
93
|
+
* turn the agent then spent 39 s on. The wait is only ever this long while a
|
|
94
|
+
* matched plan's values are still being read.
|
|
84
95
|
*/
|
|
85
|
-
const PROMPT_WAIT_MS =
|
|
96
|
+
const PROMPT_WAIT_MS = 7_500;
|
|
86
97
|
/**
|
|
87
98
|
* How long a `PreToolUse` waits for the intent probe when that probe may arm a
|
|
88
99
|
* plan in the call's place (`BIR_INTENT_ARM_BUDGET_MS`). A clear match or a
|
|
@@ -1768,12 +1779,12 @@ export class ControlServer {
|
|
|
1768
1779
|
});
|
|
1769
1780
|
}
|
|
1770
1781
|
if (state) {
|
|
1771
|
-
this.shelve(session, run, state);
|
|
1782
|
+
this.shelve(session, run, state, "arm_unannounced");
|
|
1772
1783
|
}
|
|
1773
1784
|
else {
|
|
1774
1785
|
void arming.then((late) => {
|
|
1775
1786
|
if (late)
|
|
1776
|
-
this.shelve(session, run, late);
|
|
1787
|
+
this.shelve(session, run, late, "arm_timeout");
|
|
1777
1788
|
});
|
|
1778
1789
|
}
|
|
1779
1790
|
return undefined;
|
|
@@ -2128,7 +2139,7 @@ export class ControlServer {
|
|
|
2128
2139
|
scenario: state.scenarioId ?? undefined,
|
|
2129
2140
|
why: "the prompt was answered without this plan's directive — retired unannounced, so never run",
|
|
2130
2141
|
});
|
|
2131
|
-
this.shelve(session, run, state);
|
|
2142
|
+
this.shelve(session, run, state, "arm_unannounced");
|
|
2132
2143
|
}
|
|
2133
2144
|
}
|
|
2134
2145
|
/**
|
|
@@ -2136,14 +2147,20 @@ export class ControlServer {
|
|
|
2136
2147
|
* told of it, so it must never steer or diverge a call: it is kept only as
|
|
2137
2148
|
* what it now is — a matched turn the agent does itself, which still owes the
|
|
2138
2149
|
* ledger its baseline sample (§11.1) — exactly like a declined match.
|
|
2150
|
+
*
|
|
2151
|
+
* `why` is the code the report carries when the plan was only late. Without
|
|
2152
|
+
* it the service got a matched turn that was not steered and no reason, and
|
|
2153
|
+
* its console could only say that the runner had given none. A plan a gate
|
|
2154
|
+
* declined keeps the gate's own code.
|
|
2139
2155
|
*/
|
|
2140
|
-
shelve(session, run, late) {
|
|
2156
|
+
shelve(session, run, late, why) {
|
|
2141
2157
|
if (late.plan && !late.retired) {
|
|
2142
2158
|
logDetail("replay.shelved", {
|
|
2143
2159
|
run: run.runId,
|
|
2144
2160
|
scenario: late.scenarioId ?? undefined,
|
|
2145
2161
|
why: "armed without its directive reaching the model — retired, never steers",
|
|
2146
2162
|
});
|
|
2163
|
+
late.declined ??= why;
|
|
2147
2164
|
}
|
|
2148
2165
|
late.sessionId = session.sessionId;
|
|
2149
2166
|
late.plan = undefined;
|
|
@@ -507,7 +507,14 @@ export type DeclineCode = "replay_disabled" | "not_ready" | "flatten_failed" | "
|
|
|
507
507
|
/** The session's host only records: its adapter asked for no plan with the prompt. */
|
|
508
508
|
| "host_records_only"
|
|
509
509
|
/** The plan names a tool the host's agent does not hold. */
|
|
510
|
-
| "host_tool_missing"
|
|
510
|
+
| "host_tool_missing"
|
|
511
|
+
/**
|
|
512
|
+
* No gate said no: the plan was good, and late. Not ready when the prompt
|
|
513
|
+
* had to be answered (`arm_timeout`), or ready just after it had been
|
|
514
|
+
* (`arm_unannounced`). Set by the control server, where a prompt's wait
|
|
515
|
+
* ends, so the report can say why a matched turn was not steered.
|
|
516
|
+
*/
|
|
517
|
+
| "arm_timeout" | "arm_unannounced";
|
|
511
518
|
/** What `PreToolUse` should do about this call. */
|
|
512
519
|
export type PreToolAction =
|
|
513
520
|
/** Pin the arguments and let the real tool run. */
|
package/docs/calculatedReplay.md
CHANGED
|
@@ -878,7 +878,7 @@ The hook is on the user's critical path. Every await here is a stall the user wa
|
|
|
878
878
|
|
|
879
879
|
| Moment | Route | Hook timeout | Answered by | What may be awaited | Budget |
|
|
880
880
|
|---|---|---|---|---|---|
|
|
881
|
-
| `UserPromptSubmit` | `/session/prompt` | 10 s |
|
|
881
|
+
| `UserPromptSubmit` | `/session/prompt` | 10 s | 8.5 s | the match; for a scenario with a target, its derivation; a steer plan's argument preview | match `BIR_MATCH_BUDGET_MS` = **3000**; match and arming together ≤ **7.5 s**; derivation ≤ `BIR_HOOK_DERIVE_BUDGET_MS` = **8000**, cut short by the 7.5 s |
|
|
882
882
|
| `PreToolUse` | `/tool/pre` | 15 s | 12 s | `plan.ready()` (derivation); on a divergence, the remaining steps | derivation ≤ min(`BIR_DERIVE_BUDGET_MS`, `BIR_HOOK_DERIVE_BUDGET_MS`) = **8000**; all replay work ≤ `BIR_PRETOOL_REPLAY_BUDGET_MS` = **9000** (held at ≤ 10 000) |
|
|
883
883
|
| `run_scenario` | `/scenario/run` | `bir-scenario`'s own, **290 s** (`BIR_SCENARIO_RUN_TIMEOUT_MS`) | — | `ready()` + N steps | derivation `BIR_DERIVE_BUDGET_MS` = **8000**; plan `BIR_REPLAY_PLAN_BUDGET_MS` = **100000** (`BIR_REPLAY_BUDGET_MS` overrides it where set); per step `BIR_STEP_TIMEOUT_MS` = **60000**, clipped to what the plan has left |
|
|
884
884
|
| `PostToolUse` | `/tool/post` | 5 s | 2 s | nothing | — |
|
|
@@ -638,7 +638,7 @@ Code's pool, as before.
|
|
|
638
638
|
| No `run.matched`, ever | Prompt below the server's `SIMILARITY_THRESHOLD` | Rephrase closer, or lower it server-side. Confirm the original run is in the list — a sub-`RECORDING_MIN_ACTIONS` run is never embedded |
|
|
639
639
|
| `run.matched` but no `plan.armed` | A gate declined | Read the `replay.decision` line's `why=`. Ranked by frequency: replay off — `bir replay off`, `BIR_REPLAY=0`, or never switched on here (a `bir install` without `--replay`; `bir replay status` says which) · scenario not `ready` · similarity below threshold · no step is executable |
|
|
640
640
|
| `replay.decision … code=match_timeout` or `code=service_unhealthy` on many prompts | The service answered the match slower than `BIR_MATCH_BUDGET_MS` (3 s), or is failing and in its cool-down | Nothing breaks: those turns run normally, and after a slow match prompts stop waiting on it for a while by themselves (`replay.match_gate`). `bir doctor` shows the service's state. Raise `BIR_MATCH_BUDGET_MS` only if the service is reliably just over it — every prompt pays it |
|
|
641
|
-
| `replay.decision … code=arm_timeout`, `code=missing_target … derivation did not answer within …ms`, or `code=no_derive_key why="service: …"`, on a prompt that names its values | The reading of the prompt's values (the derivation) was too slow for the hook, or failed on the service. A hook waits `BIR_HOOK_DERIVE_BUDGET_MS` (8 s) for it, and the prompt hook never past
|
|
641
|
+
| `replay.decision … code=arm_timeout`, `code=missing_target … derivation did not answer within …ms`, or `code=no_derive_key why="service: …"`, on a prompt that names its values | The reading of the prompt's values (the derivation) was too slow for the hook, or failed on the service. A hook waits `BIR_HOOK_DERIVE_BUDGET_MS` (8 s) for it, and the prompt hook never past 7.5 s from the prompt, match included; the service gives one reading `RUNNER_DERIVE_TIMEOUT_MS` (6 s) and then answers `service: error` or `service: timeout`, so its answer arrives inside the prompt's wait either way | Nothing to change in the prompt, and nothing to sign in to: that turn ran normally. The `replay.derived` line that follows says when the reading landed and what it cost. If it is slow every time, the service's reading model is the cause; `BIR_DERIVE_API_KEY` where Claude Code starts moves the reading to this machine |
|
|
642
642
|
| `replay.decision … code=no_delivery_tool` | A direct plan, and no running `bir` server has told the recorder it serves `run_scenario`: the server is missing, disabled or failed in `/mcp`, was not added (a subdirectory or worktree of a repository), or the session was started with `BIR_OFF=1` | `/mcp` shows whether `bir` is connected. `bir install --replay` adds it, in the project's own directory; then start a new session |
|
|
643
643
|
| `replay.vetoed … the model's first call was …, not run_scenario` | A direct plan, and the model reached for another tool first — usually because it has no `run_scenario` | As for `no_delivery_tool`. Nothing ran; the model did the task |
|
|
644
644
|
| `replay.decision … code=host_records_only` | A LangChain application (§6.5) whose adapter asked for no plan with its prompt: it only records. `bir investigate` gives the cause: the runner in that application's folder is older than replay for LangChain needs, so the LangChain package only records | In `bir investigate`'s words: nothing to do while replay for LangChain is not released. After that: update the runner. Until then the model does the task, and the run counts as a sample of the usual cost |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basein/runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "A recording MCP proxy: sits between any MCP client and its MCP servers, executes each call on the client's behalf, and records the run as a reusable BaseIn scenario.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/production.md
CHANGED
|
@@ -122,7 +122,7 @@ In practice: the runner must never be the reason a Claude Code session is slow,
|
|
|
122
122
|
|---|---|---|---|---|---|
|
|
123
123
|
| SessionStart: command hook `bir-hooks ensure` | the payload on stdin (≤ 2 s); finding or starting the recorder (local only, never the service); relaying the payload (≤ 2.5 s); a read-only stale-copy check (only with ≥ 3 s left) | warm about 0.1 s (Node start-up); cold about 0.7 s on Windows, once per project per boot | 11.5 s (11 s budget plus a 0.5 s hard exit). npx installs pass `--budget-ms 8000`, after npx's own start-up | `/session/start`: 2 s | 15 s (30) |
|
|
124
124
|
| SessionStart: HTTP form (older installs only; `bir install` now always writes the command hook, and `bir setup` replaces the HTTP one) | nothing | milliseconds | 2 s | 2 s | 5 s (10) |
|
|
125
|
-
| UserPromptSubmit | replay on: the similar-prompt match (≤ 3 s; skipped while the breaker is open or the match gate holds). For a matched scenario: arming, derivation (budget 8 s, cut short by this hook's
|
|
125
|
+
| UserPromptSubmit | replay on: the similar-prompt match (≤ 3 s; skipped while the breaker is open or the match gate holds). For a matched scenario: arming, derivation (budget 8 s, cut short by this hook's 7.5 s) and the directive's argument preview (≤ 1.2 s), all inside 7.5 s. Replay off: nothing | about 17 ms with an instant local service; plus one service round trip (about 0.3-0.5 s) with replay on | 7.5 s, only for a matched scenario whose values are still being read; 3 s on the first prompt of a hang | 8.5 s | 10 s (15) |
|
|
126
126
|
| PreToolUse | nothing, unless a replay plan steers this turn. Then: in-hook steps (≤ 9 s, `BIR_PRETOOL_REPLAY_BUDGET_MS`), derivation at the first call (≤ 8 s), the previous pinned step (≤ 1.75 s), and after a hand-over the new fragment run's create (≤ 0.3 s, breaker-gated). Intent arming only when opted in (≤ 0.5 s probe + 1.5 s arming) | 2-5 ms | 12 s, replay only | 12 s | 15 s (30) |
|
|
127
127
|
| PostToolUse, PostToolUseFailure | nothing: the proxy's report is joined in the background | 1-2 ms | 2 s | 2 s | 5 s (30) |
|
|
128
128
|
| SubagentStart, SubagentStop | nothing | not measured (bookkeeping only) | 2 s | 2 s | 3 s (5) |
|
|
@@ -135,7 +135,7 @@ Hard deadlines are in `HOOK_DEADLINE_MS` (`src/control/server.ts`). The timeouts
|
|
|
135
135
|
|
|
136
136
|
- **What setup does.** It signs in, gives the folder an entry of its own in `installed.json` (a fixed port, a token and `hosts: ["langchain"]`), stores replay on (off with `--no-replay`), starts the recorder in the background and prints the two lines of Python to add to the agent.
|
|
137
137
|
- **What it leaves alone.** It writes nothing under `.claude/` and nothing into `~/.claude.json`: no hooks, no wrapped servers, no `bir` MCP server. A folder that is already set up for Claude Code keeps that set-up as it is, and only its entry notes the host.
|
|
138
|
-
- **Deadlines.** The server deadlines above are set per route, so they hold for the adapter's requests too: `/session/prompt`
|
|
138
|
+
- **Deadlines.** The server deadlines above are set per route, so they hold for the adapter's requests too: `/session/prompt` 8.5 s, `/tool/pre` 12 s, `/tool/post` and `/session/stop` 2 s. The timeouts `bir install` writes are Claude Code's own.
|
|
139
139
|
|
|
140
140
|
The fields the adapter adds to each route, and what the recorder does for such a session, are in `docs/BaseInstRunner.md`, Phase 5. The how-to is `langChainGuide.md` in the repository's root; it is not in the npm package. The runner's part is all in this package. The Python package is not on PyPI, and the service's part is built and not deployed yet.
|
|
141
141
|
|
|
@@ -370,7 +370,7 @@ Over 2-minute runs with one turn every 8 s (92 hooks):
|
|
|
370
370
|
|
|
371
371
|
**Deadlines and fail-open answers.**
|
|
372
372
|
|
|
373
|
-
- **Deadlines.** Hard per-route deadlines apply: start 2 s, prompt
|
|
373
|
+
- **Deadlines.** Hard per-route deadlines apply: start 2 s, prompt 8.5 s, pre 12 s, post, agent and stop 2 s, end 1 s. Past a deadline the route answers `{}`, or an honest note mid-replay.
|
|
374
374
|
- **Fail-open answers.** Stale or wrong tokens, unknown routes, non-POST requests, bodies over 64 MB (read and discarded) and non-object bodies all get `200 {}`. They are counted in `/health.hooks`. Non-hook routes keep 401, 404 and 405.
|
|
375
375
|
- **Event name.** Every answer's `hookEventName` matches the event that fired. The final review found a PostToolUseFailure answered as PostToolUse, which Claude Code rejects with a visible hook error that dumps the runner's JSON.
|
|
376
376
|
- **Token.** The project's current token is read from `installed.json`: re-read at most every 3 s, and only when the file changed. A reinstall on the same port needs no recorder restart.
|
|
@@ -379,7 +379,7 @@ Over 2-minute runs with one turn every 8 s (92 hooks):
|
|
|
379
379
|
|
|
380
380
|
- The match waits up to 3 s, and only while the breaker allows it.
|
|
381
381
|
- The match gate: over 2 s, prompts stop waiting for 5 min, doubling to 30 min.
|
|
382
|
-
- Arming a matched scenario, including derivation and the directive's argument preview, stays inside 6 s.
|
|
382
|
+
- Arming a matched scenario, including derivation and the directive's argument preview, stays inside 7.5 s. That is longer than the service may take to read a prompt's values (6 s, its `RUNNER_DERIVE_TIMEOUT_MS`) plus the match and the round trip: at 6 s the hook gave up before the service could answer, and a good plan was thrown away (`arm_timeout`).
|
|
383
383
|
- A plan that is ready only after the prompt was answered is retired unannounced, so it never steers (`replay.decision code=arm_unannounced` or `arm_timeout`).
|
|
384
384
|
- With replay off, the prompt never waits.
|
|
385
385
|
- The prompt after an interrupted turn, and SessionEnd, seal synchronously and drain in the background. They used to wait for the whole backlog to reach the service (0.5 s healthy, over 30 s hung).
|
|
@@ -601,7 +601,7 @@ Truncation is always marked with the original size.
|
|
|
601
601
|
|
|
602
602
|
**Budgets.**
|
|
603
603
|
|
|
604
|
-
- The hook derivation wait is 8 s (the prompt hook stops at its own
|
|
604
|
+
- The hook derivation wait is 8 s (the prompt hook stops at its own 7.5 s). All replay work in one PreToolUse is capped at 9 s (maximum 10 s).
|
|
605
605
|
- The direct plan budget is 100 s (was 120 s). Direct runs are clamped so that derivation, plan and slack stay within 270 s, under `run_scenario`'s 290 s client timeout.
|
|
606
606
|
- The worst case by budget is 120 s: 8 s of derivation, 100 s of plan, and 12 s of slack.
|
|
607
607
|
|