@basein/runner 0.3.21 → 0.3.23

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 (50) hide show
  1. package/dist/bin/bir-hooks.d.ts +12 -16
  2. package/dist/bin/bir-hooks.js +12 -16
  3. package/dist/bin/bir-scenario.js +1 -1
  4. package/dist/bin/bir.js +1 -21
  5. package/dist/bin/investigate.d.ts +0 -9
  6. package/dist/bin/investigate.js +14 -42
  7. package/dist/bin/scenario-edit.js +13 -44
  8. package/dist/control/pin-guard.d.ts +16 -0
  9. package/dist/control/pin-guard.js +36 -0
  10. package/dist/control/recorder-main.d.ts +0 -15
  11. package/dist/control/recorder-main.js +7 -39
  12. package/dist/control/server.d.ts +81 -131
  13. package/dist/control/server.js +460 -968
  14. package/dist/record/recorder.d.ts +47 -138
  15. package/dist/record/recorder.js +0 -3
  16. package/dist/record/remote-recorder.d.ts +1 -9
  17. package/dist/record/remote-recorder.js +11 -49
  18. package/dist/record/switchable-recorder.js +0 -2
  19. package/dist/replay/bundle.d.ts +2 -2
  20. package/dist/replay/bundle.js +3 -3
  21. package/dist/replay/controller.d.ts +75 -124
  22. package/dist/replay/controller.js +207 -529
  23. package/dist/replay/derive.d.ts +0 -10
  24. package/dist/replay/derive.js +0 -3
  25. package/dist/replay/handover.d.ts +2 -37
  26. package/dist/replay/handover.js +38 -106
  27. package/dist/replay/logic.d.ts +0 -11
  28. package/dist/replay/logic.js +0 -24
  29. package/dist/replay/plan.d.ts +43 -163
  30. package/dist/replay/plan.js +144 -556
  31. package/dist/replay/safety.d.ts +4 -5
  32. package/dist/replay/safety.js +7 -7
  33. package/dist/replay/source-run.d.ts +15 -24
  34. package/dist/replay/source-run.js +25 -58
  35. package/dist/replay/targets.d.ts +11 -21
  36. package/dist/replay/targets.js +30 -69
  37. package/dist/replay/types.d.ts +9 -96
  38. package/dist/replay/types.js +3 -16
  39. package/dist/util/service-headers.d.ts +1 -2
  40. package/dist/util/service-headers.js +1 -2
  41. package/docs/calculatedReplay.md +17 -74
  42. package/docs/calculatedReplayGuide.md +10 -30
  43. package/package.json +1 -1
  44. package/production.md +5 -35
  45. package/dist/replay/condition-words.d.ts +0 -28
  46. package/dist/replay/condition-words.js +0 -59
  47. package/dist/replay/flatten.d.ts +0 -161
  48. package/dist/replay/flatten.js +0 -222
  49. package/dist/replay/reads-only.d.ts +0 -29
  50. package/dist/replay/reads-only.js +0 -261
@@ -50,13 +50,9 @@ export function isReadyScenario(value) {
50
50
  s.steps.every(readyStep));
51
51
  }
52
52
  /**
53
- * One step of a ready chain (segmented.md 10.6 step 1).
54
- *
55
- * A call row carries no tool of its own, so the old test — a string `toolName`
56
- * and a string `toolInputLogic` — would decline every chain that contains one.
57
- * A call is ready when it says it cannot run (the plan stops in front of it and
58
- * the agent takes over there) or when it carries a segment whose own steps pass
59
- * this same test. A call row with neither is a payload we cannot run.
53
+ * One step of a ready chain: a tool and the logic that computes its input.
54
+ * Anything else — a row with no tool, as an older service could send — is a
55
+ * payload this runner cannot run, and the plan is not armed.
60
56
  */
61
57
  function readyStep(step) {
62
58
  if (!step)
@@ -65,15 +61,6 @@ function readyStep(step) {
65
61
  // not armed rather than run with the step always in, or always out.
66
62
  if (step.when !== undefined && step.when !== null && !isStepTest(step.when))
67
63
  return false;
68
- if (step.kind === "segment") {
69
- if (step.unusable)
70
- return true;
71
- const segment = step.segment;
72
- return (!!segment &&
73
- Array.isArray(segment.steps) &&
74
- segment.steps.length > 0 &&
75
- segment.steps.every(readyStep));
76
- }
77
64
  return typeof step.toolName === "string" && typeof step.toolInputLogic === "string";
78
65
  }
79
66
  //# sourceMappingURL=types.js.map
@@ -5,8 +5,7 @@
5
5
  *
6
6
  * - **1** — stops in front of a step that carries `stop`, and reads
7
7
  * `withheld: "runner_update_required"` on a match.
8
- * - **2** — runs a step (or a sub-task's call row) only when its test, `when`,
9
- * says so: leaves it out (`not_taken`) or stops in front of it
8
+ * - **2** — runs a step only when its test, `when`, says so: leaves it out (`not_taken`) or stops in front of it
10
9
  * (`other_branch`) when the test says no, and stops when the test cannot
11
10
  * answer (stage `when_logic`). The service's autocorrect.md AC5: a plan
12
11
  * holding a test is handed only to a runner that names contract 2 or newer,
@@ -22,8 +22,7 @@ import { packageVersion } from "./version.js";
22
22
  *
23
23
  * - **1** — stops in front of a step that carries `stop`, and reads
24
24
  * `withheld: "runner_update_required"` on a match.
25
- * - **2** — runs a step (or a sub-task's call row) only when its test, `when`,
26
- * says so: leaves it out (`not_taken`) or stops in front of it
25
+ * - **2** — runs a step only when its test, `when`, says so: leaves it out (`not_taken`) or stops in front of it
27
26
  * (`other_branch`) when the test says no, and stops when the test cannot
28
27
  * answer (stage `when_logic`). The service's autocorrect.md AC5: a plan
29
28
  * holding a test is handed only to a runner that names contract 2 or newer,
@@ -231,8 +231,8 @@ more than one score — a same-request question under its threshold, and a secon
231
231
  prompt's task line. Re-scoring that on this side declined matches the server had made (a checked
232
232
  0.85, a task line at 0.91). **A match is the decision to use the scenario**, so every hand-out
233
233
  steers; what a plan cannot do — a parked first step, a tool nothing reaches, a target the prompt
234
- never named — the gates after it still stop. `BIR_MIN_STEER_SIMILARITY` and
235
- `BIR_MIN_SEGMENT_STEER_SIMILARITY` are ignored.
234
+ never named — the gates after it still stop. `BIR_MIN_STEER_SIMILARITY` is
235
+ ignored.
236
236
 
237
237
  `not_steered` as the floor is not a failure code. It is the **control group**: the prompt matched,
238
238
  the agent ran the task the ordinary way, and the cost of doing so is a fresh measurement of what this
@@ -754,30 +754,6 @@ about it:
754
754
  It ends with the remaining planned steps and the parameters derived for this request, within 4 000
755
755
  characters.
756
756
 
757
- **A plan's decision (0.3.19)** has a note of its own. The service serves a step with
758
- `stop: {kind: "decision"}` where the task has an "if … otherwise" (its segmentsbreakdown.md). The
759
- plan runs only the steps before it, in steer and direct mode and when a divergence's bundle runs up
760
- to it, and hands the task back. The note opens *"[BaseInstRunner calculated replay] The calculated
761
- scenario for this request ran the steps before a decision, and stopped there."*, says which steps
762
- ran as above, and ends *"This request asks for a decision: “…”. Decide from the results above
763
- which way applies, then continue the task, calling tools as needed."* (*below* in a direct run's
764
- answer, *above and below* after a divergence; *"This task has a decision to make here."* when the
765
- request has no such sentence). The quote is the live request's own sentences with a word for a
766
- condition, found with the service's word list (`if`, `otherwise`, `else`, `unless`, `only when`,
767
- `only if`, `in case`, `whether`, and the Hebrew ones), at most 600 characters
768
- ([`condition-words.ts`](../src/replay/condition-words.ts)). The note has no intent line, no
769
- remaining planned steps and no parameters, and never the condition the service keeps (`stop.why`):
770
- each names, or hints at, the half the recording took. The directive at the prompt leaves the intent
771
- out for the same reason, and says the task comes back *"at a decision"*. The report is `fell_back`
772
- (`failed` when no step ran) with `fallbackKind: "decision"`; the decision step has no verdict, so
773
- nothing counts against it. A runner up to 0.3.18 reads the unknown kind as `known_bad_step`.
774
-
775
- A plan whose chain has a decision keeps these protections when it stops *earlier* for another
776
- reason — a parked or failed step, a judgement, the budget. Its note is the ordinary one above, but
777
- with no intent line and no parameters, and its remaining planned steps end before the decision. Its
778
- directive leaves the intent out too. An agent sent on to the recorded half's steps would start that
779
- half's segment, and the plan would have decided after all.
780
-
781
757
  With `BIR_REPLAY_RECORDED_OUTPUTS=1`, a step that could not be *sent* (a built-in, no proxy, off the
782
758
  allowlist) is served its **recorded output** instead (§8.1), marked `(recorded)` in the bundle so the
783
759
  model is never told a stale result is fresh. A timeout or an upstream error never is.
@@ -916,8 +892,7 @@ requests* — which is what §7.3's ordering rule protects.
916
892
  Built 2026-10-23 for the service's autocorrect.md AC5; not yet released. A task can have two
917
893
  halves: "if engaged users went over the threshold, book a meeting; otherwise send a note". A
918
894
  recording shows one half, so its plan knows one half. The service's correction adds the other half
919
- and a test on each step that belongs to only one of them. A served step, and a sub-task's call
920
- row, may carry:
895
+ and a test on each step that belongs to only one of them. A served step may carry:
921
896
 
922
897
  ```ts
923
898
  when?: { logic: string; otherwise: "skip" | "hand_over"; text?: string | null } | null
@@ -926,16 +901,12 @@ when?: { logic: string; otherwise: "skip" | "hand_over"; text?: string | null }
926
901
 
927
902
  The test runs in the same sandbox as the other logic (`evalWhenLogic`, `src/replay/logic.ts`). It is
928
903
  decided when its step comes due, with what is known then: the request's values and the results of
929
- the steps before it. A call row's test is decided once, in the caller's scope, before the sub-task
930
- starts — where its first step *or its callee's* is: a sub-task whose chain begins with a call
931
- begins with that call's steps, so nothing of it runs before its test says it is needed. A sub-task
932
- is entered there too, before its callee, whose mapping reads its parameters, and left after the
933
- last step of it or its callee (`frameEdges`, `src/replay/flatten.ts`).
904
+ the steps before it.
934
905
 
935
906
  | The test says | What happens |
936
907
  |---|---|
937
908
  | `true` | The step runs. |
938
- | `false`, `otherwise: "skip"` | Nothing is sent. The step is reported `not_taken` and the plan goes on. A skipped call row leaves out its whole sub-task and is reported `not_taken` under its own index. |
909
+ | `false`, `otherwise: "skip"` | Nothing is sent. The step is reported `not_taken` and the plan goes on. |
939
910
  | `false`, `otherwise: "hand_over"` | The plan stops in front of the step and hands the rest of the task to the model, saying the steps from here are only for when `text` (`fallbackKind: other_branch`). |
940
911
  | throws, or not exactly `true` or `false` | The plan stops: the step is `failed` at stage `when_logic` (`fallbackKind: step_failed`). The model is told the plan could not tell whether this request needs the step, and decides itself. Nothing is guessed. |
941
912
 
@@ -943,24 +914,18 @@ A plan whose tests left out every step it came to, and ran none, hands the whole
943
914
  (`other_branch`, step 0). Its report is `failed` with `fallbackKind: other_branch`, and it is sent
944
915
  although it cost nothing: it tells the service the request took the half the plan does not know.
945
916
 
946
- A step left out that is the last of a sub-task still hands the sub-task's results back to its
947
- caller. When the body that maps them throws, the plan stops there: the call row is `failed` at stage
948
- `tool_output_logic`, and the note says the sub-task could not hand back its results — never that
949
- the step left out ran.
950
-
951
917
  - **Direct mode** decides the tests in its loop. The bundle gives a left-out step its own heading,
952
918
  `--- Step 5: tool (not needed) ---`, and says it is only for when `text`, so the model does not run
953
919
  it itself.
954
920
  - **Steer mode** settles the tests after each call. The `PostToolUse` answer names the next step
955
- this request does not need, and the step to call instead. A call to a left-out tool — every tool
956
- of a sub-task left out whole — is answered in its place with the same words, at most twice, and
921
+ this request does not need, and the step to call instead. A call to a left-out tool is answered in its place with the same words, at most twice, and
957
922
  does not end the plan. When the left-out step and the next one share a tool and the note has not
958
923
  reached the model yet, a call the pin guard (§7) finds is not the next step gets the note in its
959
924
  place too, so the step left out never runs because the note was late. A note naming a step to call
960
925
  next is dropped when the plan retires — a divergence, a hand-over — since the plan no longer has
961
926
  that step; only "the sequence is complete" outlives a plan that finished.
962
- - **The divergence bundle** (§8) decides the tests the same way, going on from the sub-task the
963
- cursor is inside, with what that sub-task made so far. A `hand_over` stop ends the bundle there,
927
+ - **The divergence bundle** (§8) decides the tests the same way, going on from where the
928
+ cursor is, with what the plan made so far. A `hand_over` stop ends the bundle there,
964
929
  and the turn is booked `fell_back` when a step did its work (`failed` when none did) — a hand-over,
965
930
  as in the other modes, never `diverged`, so the service takes the agent's work after it as that
966
931
  half's evidence.
@@ -1040,16 +1005,10 @@ note replaces the call, in direct mode `run_scenario` answers with it). The deri
1040
1005
  itself times out after max(`BIR_SERVICE_TIMEOUT_MS`, `BIR_DERIVE_BUDGET_MS`). None of these waits
1041
1006
  happens while the service is cooling down after a failure: the turn simply runs normally.
1042
1007
 
1043
- **Intent arming mid-turn is on by default from 0.3.19.** An intent probe is sent from `PreToolUse`
1044
- for hit counting. While arming is on — `BIR_SEGMENT_ARM` unset or `1`, the recorder's default from
1045
- 0.3.19, or `BIR_INTENT_ARM=1` — the probe may arm a plan in that call's place, and the hook waits at
1046
- most `BIR_INTENT_ARM_BUDGET_MS` (**500**) for it and 1.5 s more for arming. With `BIR_SEGMENT_ARM=0`
1047
- (and no `BIR_INTENT_ARM=1`), or `BIR_INTENT_ARM=0`, it is never waited on. Up to 0.3.18 arming was
1048
- opt-in: a segment runs only mid-task, and a plan that stops at its decision hands the rest of the
1049
- task to segments of its halves, so 0.3.19 turns it on. A probe asks the service to hand segments out
1050
- (`acceptSegments`) only where one can arm: segment and intent arming on, in Claude Code. Any other
1051
- probe — `BIR_INTENT_ARM=0`, or a session of another host — gets what would have armed instead, and
1052
- it is logged (`replay.segment_would_arm`).
1008
+ **Intent arming mid-turn is opt-in.** With `BIR_INTENT_ARM=1`, a `PreToolUse` whose call the agent
1009
+ explained sends an intent probe, and a whole plan it matches may arm in that call's place; the hook
1010
+ waits at most `BIR_INTENT_ARM_BUDGET_MS` (**500**) for the probe and 1.5 s for arming. Unset (or
1011
+ `0`), no probe is sent and nothing arms mid-turn.
1053
1012
 
1054
1013
  `UserPromptSubmit` must stay non-`async`: an async hook is fire-and-forget and cannot return
1055
1014
  `additionalContext` at all.
@@ -1081,8 +1040,7 @@ stopped at a step that broke (a logic failure, a tool error, a call sent and nev
1081
1040
  recorded stand-in (`BIR_REPLAY_RECORDED_OUTPUTS=1`) is never work done: it counts toward neither
1082
1041
  `stepsPinned` nor the upgrade, and a direct run that used one is `diverged` at best. A call that ran
1083
1042
  beside a pinned one hands over as `fell_back` or `failed` (§7), and so does a divergence whose
1084
- bundle stops in front of the other half of an "if … otherwise" (§9.2), or in front of the plan's
1085
- decision (§8). A direct run cancelled by its caller
1043
+ bundle stops in front of the other half of an "if … otherwise" (§9.2). A direct run cancelled by its caller
1086
1044
  (Esc, or `bir-scenario`'s own timeout) books `fell_back` or `failed` with `measured: false`, never
1087
1045
  `diverged`: that turn's cost says nothing about what the task costs.
1088
1046
 
@@ -1191,10 +1149,6 @@ a replay that sent 3600.
1191
1149
  has no output, and neither has a call the host says failed without a result. The exception is a
1192
1150
  step its host stopped itself (§7.4): its verdict keeps what the host handed back, and after a
1193
1151
  changed call it has no input, because only the host knows the arguments that call ran with.
1194
- - **Only for the reported scenario's own steps.** A step inside a called segment is reported with
1195
- its verdict and its `scenarioId`, without payloads: the console still files verdicts by step
1196
- number alone (the service's plan-services.md, W1.9), and a called segment numbers its steps from
1197
- zero too.
1198
1152
 
1199
1153
  `ok` and `recorded` are passes (`recorded` with an asterisk: the values came from the source run,
1200
1154
  which happens only with `BIR_REPLAY_RECORDED_OUTPUTS=1` and in `bir replay`). `skipped` produced no
@@ -1222,8 +1176,7 @@ nothing knows which step the stack trace belonged to. In steer mode there is no
1222
1176
  the controller records the verdict at both hooks: `PreToolUse` for an input-logic throw, `PostToolUse`
1223
1177
  for the pass or the output-logic throw — and keeps the pinned arguments on the pin, so the verdict
1224
1178
  `PostToolUse` writes can say what the call ran with. `ReplayState.stepResults` is keyed by the
1225
- scenario that owns the step and its index, so the second word on a step replaces the first and a
1226
- called segment's step 0 never replaces the caller's.
1179
+ step's index, so the second word on a step replaces the first.
1227
1180
 
1228
1181
  One consequence for the rule that a costless decline is not worth sending (`buildReport`, and the
1229
1182
  service's matching `202 { recorded: false }`): it now *is* worth sending when a step broke. No cost
@@ -1293,15 +1246,6 @@ tokens and every model in the list has a price; with no usage at all, the line s
1293
1246
  `why="the host sent no usage"`. Such a host hands over one number for the whole run, so with more
1294
1247
  than one plan in one run none of them is measured.
1295
1248
 
1296
- **A plan stopped at its decision is billed up to the stop (0.3.19).** When a step of it ran, its
1297
- hand-over takes a usage mark, and its report's window ends there: the agent's decision and the half
1298
- it took are the agent's own, not the plan's (the service's segmentsbreakdown.md SB-26). This holds
1299
- for a plan the prompt started, which gets a way to take a mark only when its stop is a decision, and
1300
- for one an intent match started, whose window already ended at its own retire. Every other plan
1301
- keeps its window — a plan of the prompt is billed for the whole turn up to the next plan, so the
1302
- answer it was asked for stays on its bill — and every other hand-over still takes no mark. A
1303
- decision reached before any step ran takes no mark either: its report is a sample of the whole turn.
1304
-
1305
1249
  ---
1306
1250
 
1307
1251
  ## 12. What the replayed turn records
@@ -1786,12 +1730,12 @@ bir replay status|on|off|allow <a,b>|allow all
1786
1730
 
1787
1731
  bir scenario list GET /recordings/runs runs, iterations, scenario state
1788
1732
  bir scenario show <runId> GET /recordings/runs/:id/scenario intent, params, steps, logic
1789
- bir scenario show <scnId> GET /scenarios/:id the same, by scenario id (segments too)
1733
+ bir scenario show <scnId> GET /scenarios/:id the same, by scenario id
1790
1734
  [--step <n>: one step, with scenarioId and chainRevision first]
1791
1735
  bir scenario calc <runId> POST /recordings/runs/:id/calculate [--force to re-derive in place]
1792
1736
  [--force --discard-edits: re-derive a plan with hand edits]
1793
1737
  bir scenario calc <scnId> --force
1794
- POST /scenarios/:id/recalculate recalculate in place (segments too)
1738
+ POST /scenarios/:id/recalculate recalculate in place
1795
1739
  [--discard-edits: sends {discardEdits: true}]
1796
1740
  bir scenario replay <scnId> --prompt "…" [--dry]
1797
1741
 
@@ -1817,8 +1761,7 @@ are where `bir` changes a scenario by hand, so the rules are strict:
1817
1761
  `--force --note "<why>"`. `check` and `edit` run the same check; `check` never writes. When the
1818
1762
  check had any problem, a forced save is recorded as forced, and `bir investigate` keeps saying so.
1819
1763
  - `<id>` is a `run_` id, resolved to its whole-run scenario with `GET /recordings/runs/:id/scenario`,
1820
- or a `scn_` id, used as it is. A sub-task scenario has only its `scn_` id; for a run with only
1821
- sub-task scenarios, `bir` lists their `scn_` ids. Logic goes in with `--input-logic` /
1764
+ or a `scn_` id, used as it is. Logic goes in with `--input-logic` /
1822
1765
  `--output-logic` from a file, or `-` for stdin — never inline, because shell quoting mangles
1823
1766
  JavaScript. `edit` also takes `--freeze`, `--unfreeze`, `--note` and `--revision <n>` (the
1824
1767
  `expectedRevision` of the body).
@@ -158,8 +158,7 @@ Calculation is a background job. Poll `show` until `state` leaves `calculating`:
158
158
  `409 scenario_exists` means this run already has one. Re-derive it in place — keeping the id and its
159
159
  replay history — with `bir scenario calc run_… --force`. A `run_` id reaches only the run's
160
160
  whole-run scenario. From the next version (not yet released), `bir scenario calc scn_… --force`
161
- does the same by the scenario's own id, a sub-task scenario included; it is the only way to
162
- recalculate a sub-task scenario from `bir`.
161
+ does the same by the scenario's own id.
163
162
 
164
163
  To change one step instead, without recalculating, see §9.2. A plan with steps edited by hand is
165
164
  not rebuilt unless you add `--discard-edits`, which replaces those edits. They stay in
@@ -330,7 +329,6 @@ Recording : yes
330
329
  · the recorder runs in the background (pid 41820); its audit log is ~/.baseinstrunner/logs/<key>.log
331
330
  · the BaseIn service last answered the recorder 12 s ago
332
331
  · wrapped MCP calls are not approved by the runner: Claude Code asks as it would without it. For headless runs (`claude -p`) allow them in permissions.allow or --allowedTools, or set BIR_CORRELATION_DECISION=allow where the recorder starts
333
- · segments run mid-task — recurring sub-tasks, and the halves after a plan's decision — and so may whole plans (the default; BIR_SEGMENT_ARM=0 turns this off)
334
332
 
335
333
  No problems found.
336
334
  ```
@@ -374,8 +372,8 @@ so there is exactly one switch and it cannot get out of step with itself.
374
372
  | `BIR_BUILTIN_TOOLS` | `Read,Glob,Grep,Edit,Write,Bash` | Built-ins the runner executes itself during a replay, from the session's own project directory (which folders the file tools may touch: `BIR_PATH_ALLOW`). A chain whose only non-MCP steps are on this list arms `direct`. A runner-side step never reaches the host's permission prompts, so leave off any tool you want the model to call itself; `Read,Glob,Grep` is the read-only set (the default up to 0.3.7). `none`: every built-in step is steered in the session. Claude Code sessions only: in a LangChain application a tool with one of these names is the application's own, and the runner runs none (§6.5) |
375
373
  | `BIR_BASH_ALLOW` | *(unset: any command)* | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset means any command; `none` means no command, and `Bash` steps are steered in the session. A plan's Bash steps share a working directory, exactly as the host's own Bash tool does — variables and functions do not persist, also as the host's. A call with `run_in_background`, or with its own `timeout` longer than the step's clock (`BIR_STEP_TIMEOUT_MS`), is never started on the runner: the plan hands over in front of it |
376
374
  | `BIR_PATH_ALLOW` | *(unset: any folder, from 0.3.21)* | Folders a runner-side `Read`, `Glob`, `Grep`, `Edit` or `Write` may touch (`*` for all). Unset means any path, as the agent's own tools reach it — a plan that reads the agent's memory file under `~/.claude` runs. `project` is the session's project folder alone (the rule up to 0.3.20); add folders after a comma, `~` for the home folder: `project,~/.claude`. Under a list, a path outside it, symlinks included, is refused and the plan hands over in front of it. `none` means no folder, and the file steps are steered in the session. `Bash` is `BIR_BASH_ALLOW`'s |
377
- | `BIR_SEGMENT_ARM` | *(unset: on, from 0.3.19)* | A segment the service hands out runs mid-task: a recurring sub-task, or a half of a task after a plan's decision. `0` is observe-only: what would have armed is logged (`replay.segment_would_arm`) and nothing is replaced. Up to 0.3.18 observe-only was the default. A recorder started before an update keeps its old default until `bir up --restart` |
378
- | `BIR_INTENT_ARM` | *(unset)* | `1` lets a match found mid-turn arm a plan in the calling tool's place, which means waiting for it (`BIR_INTENT_ARM_BUDGET_MS`, 500); `0` never waits, and then nothing starts mid-task, segments included: what would have armed is logged; unset follows `BIR_SEGMENT_ARM`, so from 0.3.19 it is on unless `BIR_SEGMENT_ARM=0` |
375
+ | `BIR_INTENT_ARM` | *(unset: off)* | `1` lets a whole plan whose intent matches the agent's reasoning mid-turn arm in the calling tool's place, which means waiting for it (`BIR_INTENT_ARM_BUDGET_MS`, 500). Unset or `0`: off — no probe is sent and nothing arms mid-turn, as up to 0.3.18 |
376
+ | `BIR_CONTINUATIONS` | *(unset: on, from 0.3.23)* | After a plan hands the task to the agent at a step, the agent's first call asks the service for the plan of the work recorded after an earlier hand-over at that step — once per hand-over, waiting 1.5 s at most for the answer and up to 7 s for its values — and runs it in that call's place only when the call is its first step (the same tool, on the same thing). Its saving is counted on the plan that handed over. `0`: off, and the agent's work is recorded as before. `BIR_INTENT_MATCH=0` turns it off too |
379
377
  | `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
380
378
 
381
379
  The `bir` server reads its own, from the environment Claude Code starts it with:
@@ -498,11 +496,7 @@ Everything replay decides is one line. `grep` for these:
498
496
  | `replay.step` | One direct step executed |
499
497
  | `replay.step_not_taken` | A step's test said this request does not need it, and nothing was sent for it ([calculatedReplay.md](calculatedReplay.md) §9.2). `plan.armed … tests=` lists the tested steps by their place in the plan: `4:hand_over,11:skip` |
500
498
  | `replay.diverge` / `replay.compose` | Off-script, and the recovery |
501
- | `replay.handover` | The plan stopped and handed the rest of the task to the model; `kind=` and `step=` say where. `kind=decision` (0.3.19) is a plan that ran the steps before its decision and stopped there, as planned (`via=divergence` when a divergence's bundle ran up to it) |
502
- | `run.fragment … decision=true` / `run.fragment_dropped` | After a plan's decision: the agent's own work from its next call is recorded as a fragment, from which the service makes a calculated scenario of that way / a plan the intent check armed, or a next scenario, took that call instead, so nothing is recorded as the agent's own |
503
- | `replay.next_ready` / `replay.next_hit` / `replay.next_miss` | 0.3.20: the calculated scenarios that can follow a plan's decision, armed while the agent decided / the agent's call was one's first step, value for value, so it runs in the call's place / none was used: `reason=other_values` (the same first tool with other values), `own_way` (a call that may change something and starts none of them), `not_ready` (the one that starts with that tool was not armed in time), `calls_limit` (5 calls started none), `turn_ended`. A call that only reads ends nothing |
504
- | `run.fragment_closed` | 0.3.20: a scenario that follows the decision took over after the agent's own looks had opened the fragment; the fragment ends there, with what the agent did itself |
505
- | `replay.decision … code=would_repeat` | 0.3.20: a plan an intent match armed after the agent's call would have made that call again (the same tool that may change something, or for a shell the same command), so it was not started (never twice) |
499
+ | `replay.handover` | The plan stopped and handed the rest of the task to the model; `kind=` and `step=` say where |
506
500
  | `replay.vetoed` | A direct plan retired without running anything: `run_scenario` was not the first call, or never arrived |
507
501
  | `replay.run_ambiguous` | Two sessions had `run_scenario` pending and the request named neither; nothing ran |
508
502
  | `replay.cancelled` / `replay.note_kept` | A direct run's caller went away (Esc); no further step started, and the note is kept for the model's next call |
@@ -587,11 +581,6 @@ A recorded output that stood in for a step (`BIR_REPLAY_RECORDED_OUTPUTS=1`) is
587
581
  work done. A direct run you cancel (Esc) is booked `fell_back` or `failed` with `measured: false`:
588
582
  a turn cut short says nothing about what the task costs.
589
583
 
590
- A plan that stops at its decision (runner 0.3.19 or newer) is booked `fell_back` with
591
- `fallbackKind: decision`, or `failed` when no step ran. Nothing is counted against its steps, and
592
- when a step ran its cost ends at the stop: the agent's decision and the half it took are not on the
593
- plan's bill.
594
-
595
584
  A turn's cost is **measured** only when its tokens were found and every model of the turn has a
596
585
  price in the runner's table. A model with no price used to count as $0, and that zero was called
597
586
  measured. Now such a turn is not measured, in Claude Code as in any host, and a `run.unmeasured`
@@ -670,14 +659,12 @@ Code's pool, as before.
670
659
  | `run_scenario` answers "The prepared results are not available for this request. Do the task normally with your tools." | The plan could not be delivered — a recorder that does not answer, a timeout, an HTTP error — and the model does the task itself | The `scenario.answered` line on the `bir` server's stderr has the real reason. `BIR_SCENARIO_DEBUG=1` in that server's environment hands the raw reason to the model as an error, for debugging |
671
660
  | A hand-over note says a step "was sent but did not answer … may or may not have completed" | The step timed out, or its upstream went away mid-call; nothing after it ran | Check whether its work happened before running it again — the model is told the same. Repeats: look at that upstream, or raise `BIR_STEP_TIMEOUT_MS` |
672
661
  | A hand-over note says "a step never runs on another session's upstream" | Two Claude Code sessions are open in the project, and this one has not yet made a call of its own to that server, so bir cannot tell its proxy from the other session's. A direct plan is told apart by the process that started both the `bir` server and the session's proxies, so for one this means an older proxy or `bir` server (restart the session after an update), or a server started through a wrapper such as a shell or `npx` | Expected: the model calls the tool itself. Once the session's own call to the server has gone through, later replays use its proxy |
673
- | A hand-over note says the plan "ran the steps before a decision, and stopped there" | The service split the plan at its "if … otherwise" (runner 0.3.19 or newer): the plan runs the steps both ways need, then the agent decides which way applies. The note quotes the request's own sentences with the condition | Expected. The agent's next step may start a segment of the way it chose (`replay.intent_armed … kind=segment`); with `BIR_SEGMENT_ARM=0` it does that part itself. `bir investigate` shows the run as `fell_back · decision` |
674
- | A hand-over note says a step "did not run: it has failed before", and the step never failed | A runner up to 0.3.18 met a plan's decision, a stop it does not know. It stops there too, but its note names a failure and lists the remaining steps | Update the runner (`bir update`, then `bir up --restart`) |
675
662
  | A hand-over note says a step "was not run by the host", or "ran with arguments a person chose, not the scenario's" | A LangChain application (§6.5). A person said no to the step, or answered in the tool's place, or a call limit blocked the call (`host_rejected`); or a person changed the call before it ran (`host_edited`) | Expected, and no fault of the plan: the step is booked `skipped`, the plan stops there and the model takes over. It is told not to run the step again — after a refusal, unless the user asks. §7 has what the report says |
676
663
  | `recorder.send_failed … error="HTTP 409 … invalid_ticket"` | The execution report's ticket was already redeemed, or belongs to another scenario. Or the report came more than an hour after its plan was handed out | Harmless if the body says `duplicate: true`. Otherwise a rollover raced `SessionEnd`; the server books once. A report that came too late is not booked; the service as built (not deployed yet) takes a report for 24 hours |
677
664
  | `recorder.send_failed … error="HTTP 429 …"` | The account is over one of the service's limits, and the answer is `429` with `Retry-After`. The limits are built on the service and not deployed yet; each is a setting there. By default: 120 runs a minute · 3,000 steps a minute (a tool call is two steps) · 10 title requests a minute · 20 requests in any one hour for Calculate, Prefer and Recalculate together (`bir scenario calc` then prints a line that begins `Could not start calculation (HTTP 429`) | What it costs: a create, a step or a report is sent once more, at most 2 s later, then dropped. A dropped step is a hole in its recording. A run whose create got the 429 runs without its plan. A 429 that the second try did not cure counts as the service being unwell: for 5 to 60 seconds no prompt of that recorder waits for a match. An intent probe over its own limit (1,500 a minute) is answered with no match, never with `429` |
678
665
  | `savedUsd` looks impossible | Pricing drift between the runner's table and the server's | Compare `PRICING_VERSION` on both sides. Design §11.3 documents a known live drift between two existing copies |
679
666
  | The hand-over note says a step "did not run: its input needs a judgement" | The service marked the step non-deterministic when it calculated the plan: the generated logic could not compute its input from the prompt or from earlier outputs, so the recorded value was a copy (the service's plan-services design, D8 kind 3 — it lives in the BaseIn repository, not this one). `nondeterministicWhy: by_hand` means someone marked it with `bir scenario edit --freeze` | If the code only has a slip, fix the step yourself: `bir scenario show scn_… --step <n>`, write the input logic to a file, then `bir scenario edit scn_… --step <n> --input-logic <file>` — the service checks it against the recording before it saves it (§9.2, not yet released). Of the edits, only `--unfreeze` lifts a mark made by hand (`by_hand`); a code edit, even a forced one, leaves it in place. A real judgement — a choice with no fixed rule — cannot be written as code: the agent makes that choice and finishes the task; a model step for it is planned (W3.1). The journal's `plan.armed` line lists the step under `stops=` |
680
- | A parked step ran again after an hour, or a plan was recalculated by itself | The service's grace period (D8): a parked step is served with `stop: null` once an hour for a day and a success clears its count; if it keeps failing, or its tool is gone, the next repeat of the prompt is recorded in full and the plan is calculated again from it | Nothing: that is the repair. `bir investigate` says which it is — a step still being retried, a parked step whose repair is due, or a plan being calculated again. To repair now by hand: `bir scenario calc <runId> --force` for a whole-run plan; from the next version, `bir scenario calc <scnId> --force` works for any plan, a sub-task one included. A plan with steps edited by hand is not repaired by itself: the parked step stays parked, and `bir investigate` says the repair was skipped. Then fix the step (§9.2); or take the edits back with `bir scenario undo`, once per edit, and it is repaired as usual; or recalculate now and drop the edits: `bir scenario calc <scnId> --force --discard-edits` |
667
+ | A parked step ran again after an hour, or a plan was recalculated by itself | The service's grace period (D8): a parked step is served with `stop: null` once an hour for a day and a success clears its count; if it keeps failing, or its tool is gone, the next repeat of the prompt is recorded in full and the plan is calculated again from it | Nothing: that is the repair. `bir investigate` says which it is — a step still being retried, a parked step whose repair is due, or a plan being calculated again. To repair now by hand: `bir scenario calc <runId> --force` for a whole-run plan; from the next version, `bir scenario calc <scnId> --force` works by the plan's own id. A plan with steps edited by hand is not repaired by itself: the parked step stays parked, and `bir investigate` says the repair was skipped. Then fix the step (§9.2); or take the edits back with `bir scenario undo`, once per edit, and it is repaired as usual; or recalculate now and drop the edits: `bir scenario calc <scnId> --force --discard-edits` |
681
668
  | `sessionCostUsd` grows every prompt in a session | The per-run usage watermark is missing or not taken | Design §11.4. Reports without a mark must carry `measured: false` |
682
669
  | `run.unmeasured` in the log, and `bir investigate` shows the report as `UNMEASURED` | The turn's cost is not measured. The run opened before any hook carried a transcript path, or the runner has no price for a model of the turn (`models=` names it). In a LangChain application: the adapter sent no usage, the run made no model call, or a model has no price | Update the runner when it has no price for the model. The report is booked as not measured. A model with no price used to count as $0 and measured (§7) |
683
670
  | Everything works, nothing is saved | A replayed turn is deliberately not recorded (design §12) | Correct. The matched run stays canonical and its `iterations` is bumped |
@@ -803,8 +790,7 @@ bir scenario editing on|off|status
803
790
  ```
804
791
 
805
792
  A `run_` id finds the run's whole-run scenario. A `scn_` id (what `bir investigate` prints) is used
806
- as it is. A sub-task scenario has only a `scn_` id. When a run has only sub-task scenarios, `bir`
807
- lists their `scn_` ids instead of guessing. `--step` is the `stepIndex` that `show` prints; it
793
+ as it is. `--step` is the `stepIndex` that `show` prints; it
808
794
  starts at 0. `--revision <n>` saves only if the plan is still at revision `n`: the `chainRevision`
809
795
  at the top of `show --step`. `--json` on `check`, `edit`, `edits` or `undo` prints the service's
810
796
  answer unchanged. Exit codes: `0` checked OK, saved or undone · `1` refused, not found, or a
@@ -833,8 +819,7 @@ code in its own process, with the global scope and only a time limit, where `fet
833
819
  do exist. So read every change before it is saved — yours and Claude's.
834
820
 
835
821
  **Only a step's code and its mark can be changed — and a parameter's kind (below, *A value no prompt names*).**
836
- Not the parameters' sample values, not the logic that builds the final answer. Only tool steps: a
837
- sub-task call row belongs to the service.
822
+ Not the parameters' sample values, not the logic that builds the final answer.
838
823
 
839
824
  #### The fix, step by step
840
825
 
@@ -1031,8 +1016,6 @@ call with a mistake you do not want repeated. Then no correct code can reproduce
1031
1016
  as a `warn`.
1032
1017
  - A forced save removes a mark **the calculation** set, unless you also pass `--freeze`. A mark
1033
1018
  made **by hand** (`--freeze`) stays. A save lifts it only with `--unfreeze`.
1034
- - A step after a sub-task call row can only be saved with `--force`: the check cannot rebuild the
1035
- outputs that step reads.
1036
1019
 
1037
1020
  #### Mark or unmark a step by hand
1038
1021
 
@@ -1170,13 +1153,11 @@ Not recalculated: steps 3, 4 were edited by hand, and a recalculation rebuilds e
1170
1153
  ```
1171
1154
 
1172
1155
  A `run_` id reaches only the run's whole-run scenario. `bir scenario calc <scnId> --force`
1173
- recalculates any scenario in place, by its own id, a sub-task scenario included. Add
1156
+ recalculates any scenario in place, by its own id. Add
1174
1157
  `--discard-edits` when it has hand edits. A `scn_` id without `--force` is refused before anything
1175
1158
  is sent: a scenario that exists can only be recalculated.
1176
1159
 
1177
- The console's *Recalculate* button (on a sub-task's scenario) is refused too while a plan has hand
1178
- edits; the console does not discard them. The service's own rebuilds — the repair of a parked step
1179
- (§9) and the adoption of a sub-task — skip a plan with hand edits. `bir investigate` says that the
1160
+ The service's own rebuild — the repair of a parked step (§9) — skips a plan with hand edits. `bir investigate` says that the
1180
1161
  service does not rebuild the plan on its own, and warns when it skipped a repair
1181
1162
  (`repair_skipped_edits`). After `--discard-edits` the old edits stay readable, but they cannot be
1182
1163
  undone (`edit_replaced`).
@@ -1348,12 +1329,11 @@ Parameter database: target → setting (revision 4). Undo: bir scenario undo scn
1348
1329
  | A `later` step no longer reproduces | The change broke a step after it — usually output logic that stopped keeping a value a later step reads | Keep every key the later steps read |
1349
1330
  | `too costly to check` | The check used up its 5 seconds of sandbox time (`EDIT_CHECK_BUDGET_MS`). Usually code that reads or keeps a great many values, so the check has to run it very many times | Make the code cheaper to run, then check again |
1350
1331
  | `Nothing to change: the step already has this code and this mark.` | The code and the mark you gave are what the step already has (`nothing_to_change`) | Nothing to do. `check` says `Nothing would change` for the same case |
1351
- | `not_a_tool_step` | The index is a sub-task call row, not a tool step | Call rows cannot be edited; the service writes them |
1352
1332
  | `note_required` | `--force` without `--note` | Add `--note "why"` |
1353
1333
  | `revision_changed` | The plan changed since you read it: another edit, or a recalculation | `show` it again, check again, save again |
1354
1334
  | `not_ready` or `scenario_calculating` | The scenario is not `ready`, or a recalculation is running | Wait for it to finish |
1355
1335
  | `undo_refused` | The undo would stop a later step from giving its recorded call, or leave the final answer without its values | Fix that later step first. Or undo it on purpose: `--force --note "why"` |
1356
- | `not_latest_edit` | The step has changed since the edit you named: a newer entry changed it. For a correction: the plan has changed since it. When the newer entry is an `ssplit_…` id, the plan was split at its decision after the correction, and a correction cannot be taken back while the split is in use | `bir` prints two commands: undo the newer one first, then yours. For a split, `bir` says to press Undo on the Split card of the plan's recording page first, and prints the command for yours |
1336
+ | `not_latest_edit` | The step has changed since the edit you named: a newer entry changed it. For a correction: the plan has changed since it | `bir` prints two commands: undo the newer one first, then yours |
1357
1337
  | `already_undone` | That edit was undone already | To redo it, undo the undo. `bir` prints the command |
1358
1338
  | `edit_replaced` | A recalculation replaced the plan after that edit | Nothing to undo; the edit is history now |
1359
1339
  | `logic_sandbox_unavailable` | The service could not run step code just now (HTTP 503). Nothing was checked, saved or undone | Try again in a minute |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basein/runner",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
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
@@ -594,31 +594,6 @@ Truncation is always marked with the original size.
594
594
  - An Esc-interrupted turn is never booked as a saving (`measured:false`).
595
595
  - A step that a host other than Claude Code (§3.2) stopped itself has two stop reasons of its own. `host_rejected`: a person said no, or answered in the tool's place, or a call limit blocked the call. The note says "Step N of M (tool) was not run by the host. The result above was written by the host, not by the tool. Do not run the step again unless the user asks." `host_edited`: a person changed the call. The note says "Step N of M (tool) ran with arguments a person chose, not the scenario's. Its result above is real and its side effects are in place. Do not run it again." The report's `fallbackKind` stays `step_failed`.
596
596
 
597
- **The decision stop (0.3.19).** The service may serve a step with `stop: {kind: "decision"}`: the task has an "if … otherwise" there (the service's segmentsbreakdown.md). The plan never decides:
598
-
599
- - It runs only the steps before the decision, then hands the task back, in steer and direct mode and after a divergence.
600
- - The note says the plan "ran the steps before a decision, and stopped there", says which steps ran, and quotes the request's own sentences with a condition word (`if`, `otherwise`, `unless` and the others the service uses, Hebrew too; at most 600 characters). It ends "Decide from the results above which way applies, then continue the task". It has no intent line, no remaining steps and no parameters, and never the condition the service keeps (`stop.why`): each would hint at the half the recording took. The directive at the prompt leaves the intent out too, and says the task comes back "at a decision".
601
- - A plan whose chain has a decision keeps these protections when it stops earlier for another reason (a parked or failed step, a judgement, the budget). Its note has no intent line and no parameters, and lists the remaining steps only up to the decision. Its directive leaves the intent out. An agent sent on to the recorded half's steps would start that half's segment, and the plan would have decided after all.
602
- - The report is `fell_back` (or `failed` when no step ran) with `fallbackKind: "decision"` and `fallbackStepIndex` the decision step. The decision step gets no verdict, so nothing is counted against it.
603
- - When a step ran, the hand-over takes the plan's usage mark: the plan is billed up to the stop, and the agent's decision and the half it took are not on its bill. This holds for a plan the prompt started and for one an intent match started. Every other hand-over still takes no mark.
604
- - The agent's own next call opens the fragment, marked `metadata.decisionOf = {scenarioId, stepIndex, request}` (`request`, the turn's prompt, redacted and cut to 4,000 characters, from 0.3.20): the service makes a calculated scenario of that way from it, calculated with the request (the service's offer1.md O2). When a plan the intent check armed, or a next scenario (below), takes that call instead, the fragment is dropped (`run.fragment_dropped`).
605
- - `ranThrough` sends one entry per plan, with the plan's own position (the decision step itself for a decision). The service maps it onto the recording.
606
- - `bir replay` stops in front of a decision and says so (`stopped before step N …: the plan's decision`).
607
- - A runner up to 0.3.18 reads the unknown kind as `known_bad_step`: it stops there too, but its note and report name a failure.
608
-
609
- **What can follow a decision (0.3.20, the service's offer1.md).** The first time a way is taken after a decision, the agent does it and its fragment becomes a calculated scenario of that decision. The next time:
610
-
611
- - When a plan stops at its decision, the runner asks the service once, in the background, which calculated scenarios can follow it (`POST /scenarios/:planId/next`, body `{turnId, supportsCalls}`; at most 4, each handed out with its own ticket and `key: "decision"`). An older service, a 404 or any failure is "none". It arms each one while the agent reads the note and decides: its values are read from the turn's request and this turn's results (the reading carries `atDecision: {scenarioId, stepIndex}`), and settled. `replay.next_ready` says how many are ready.
612
- - The agent's next calls are checked against them before the intent probe. A call that is a next scenario's first step, **value for value** (every argument the step's own; the pin guard's "plausibly the step" is not enough), is a hit: it is answered in its place with that scenario's directive (`replay.next_hit`), the agent calls `run_scenario`, and the scenario runs every step, that one included, once. It is booked on its own ticket. A scenario whose first step is inside a sub-task (a call row) can never be the agent's call in place: it is never compared.
613
- - Each scenario is checkable as soon as the service answers; its arm runs on its own. A call waits only for the arm of a scenario that starts with the call's own tool (at most 6 s, `BIR_NEXT_HIT_WAIT_MS`), and not at all for the others. While the service has not answered yet, a call waits for that answer only, at most 1 s.
614
- - When the background reading left a value of the first step empty, the call waits while it is read once more with the agent's call as `liveCall`; the service fills such a value from the call. A value read differently is never read again: an e-mail to carlos is never the recorded e-mail to olga.
615
- - **A call that only reads keeps the set** (by the service's own rule, mirrored in `src/replay/reads-only.ts`: Claude Code's read tools, a name with a reading word and no writing word, a Bash command that only looks). The set ends unused (`replay.next_miss`) at the agent's first call that may change something and is no hit: `other_values` when the call has a scenario's first tool with other values, `own_way` when it starts none of them — the agent is doing a way of its own, and no scenario may start in the middle of it — and `not_ready` when the scenario that starts with that tool was not armed in time. It also ends after 5 calls that start none (`calls_limit`), or at the turn's end. A scenario not armed in time for a call that only reads misses that call only. Unused ones never run and are never reported; the agent's own work is recorded as the fragment, as before.
616
- - A later decision in the same turn starts a new set: what could follow the earlier one is dropped, never checked again.
617
- - A hit after the agent's own looks had opened the fragment ends that fragment there (`run.fragment_closed`, finished with what the agent did itself): the scenario's own calls, pinned in steer mode, are never recorded into it.
618
- - `/health` shows the set per session: `next: {entries, armed, calls, ended}`.
619
-
620
- **Never twice (0.3.20, offer1.md O8).** A plan an intent match arms after the agent's call — not in its place — whose steps call the same tool is not started: it would make that call a second time (issue fx2: the agent's e-mail, then the plan's own). `replay.decision code=would_repeat`. A tool that only reads, by the same rule as above (`crm_search_customers`, `Read`), may repeat. For Bash and PowerShell the call is its command: only a step that would run that very command counts.
621
-
622
597
  **Measured cost.**
623
598
 
624
599
  - A turn's cost is measured only when tokens were found and every model of the turn has a price in the runner's table. This holds for every host, Claude Code too. A model outside the table used to cost $0, and that zero was called measured.
@@ -735,7 +710,7 @@ Truncation is always marked with the original size.
735
710
  | Remote servers without a literal credential header (OAuth, headersHelper, `${VAR}`) are left unwrapped. Earlier unasked wraps are undone. | The proxy has none of Claude Code's OAuth credentials; wrapped, these servers answered 401 to every call. | `--server <name>` (remembered). |
736
711
  | A flag-less install from an installed package writes absolute-path entries and the self-starting SessionStart hook, for `bir wrap` too. | No npx registry round trip per server start (about 1 s), and it works offline. npx installs had nothing to start the recorder. | `--npx`. A checkout, or `bir` run through npx, still writes npx. |
737
712
  | Hook timeouts: SessionStart command 15 s (was 30), HTTP 5 (10), UserPromptSubmit 10 (15), PreToolUse 15 (30), PostToolUse and PostToolUseFailure 5 (30), Subagent 3 (5), Stop 5 (10), SessionEnd 3 (30). | The timeout is the user's worst case during an incident. The server's deadlines sit inside it. | Not configurable; `bir setup` rewrites them. |
738
- | From 0.3.19 segments run mid-task by default (`BIR_SEGMENT_ARM` unset means on), and so may whole plans by intent. An unsteered PreToolUse waits for the intent probe: at most `BIR_INTENT_ARM_BUDGET_MS` (500 ms), and up to 1.5 s more while an armed plan's values are read. From 0.3.0 to 0.3.18 arming was opt-in and the probe was never waited on. | A plan that stops at its decision hands the rest of the task to segments of its halves, and a segment runs only mid-task (the service's segmentsbreakdown.md SB-21). 0.2.12 held every PreToolUse up to 4 s for the probe. | `BIR_SEGMENT_ARM=0` (observe only: what would have armed is logged). `BIR_INTENT_ARM=0` arms nothing mid-task and never waits; its probes then ask for no segment, as in a session of another host, so what would have armed is logged too. |
713
+ | Mid-turn intent arming is opt-in, and no intent probe is sent by default. | 0.2.12 held every PreToolUse up to 4 s for the probe. | `BIR_INTENT_ARM=1` with `BIR_INTENT_ARM_BUDGET_MS=4000`. |
739
714
  | The prompt match budget is 3 s (was 2.5 s), and there is a match gate: over 2 s, prompts stop waiting for 5 min, doubling to 30 min. | A healthy service answers in 300-450 ms. A slower one would otherwise cost every prompt. A false trip costs one hold of replay, never latency. | `BIR_MATCH_BUDGET_MS=2500`. Raising `BIR_MATCH_SLOW_MS` above the budget effectively disables the gate. |
740
715
  | Recorded outputs no longer stand in for a step that produced no result; the plan hands over at that step. | Last week's output presented as this turn's result is wrong data. | `BIR_REPLAY_RECORDED_OUTPUTS=1`, only for steps that could not be sent. `bir replay` keeps the stand-ins. |
741
716
  | The direct plan budget is 100 s (was 120 s), and direct runs are clamped to 270 s. | Keeps a direct run under Claude Code's 2-minute auto-background of an MCP call, and inside `run_scenario`'s client timeout. | `BIR_REPLAY_PLAN_BUDGET_MS=120000` (or `BIR_REPLAY_BUDGET_MS`). |
@@ -870,7 +845,6 @@ Truncation is always marked with the original size.
870
845
  - where recording goes, and as whom;
871
846
  - when the service last answered;
872
847
  - who approves wrapped MCP calls (the recorder's correlation mode);
873
- - whether segments run mid-task (the default from 0.3.19) or are observed only, and whether whole plans may start mid-task too; a recorder older than this `bir` is named, since it keeps its old default until `bir up --restart`;
874
848
  - superseded recorders;
875
849
  - an npx-started scenario server;
876
850
  - other installed projects.
@@ -919,9 +893,7 @@ In a folder that is set up for another host (§3.2), doctor prints `Host
919
893
  | `registeredProxies[]`, `wrappedServers` | proxies that registered (`serverName`, `pid`, `cwd`, `version`) and the wrapped server names |
920
894
  | `hooksVersion`, `pid`, `cwd`, `url` | the recorder's version and identity |
921
895
  | `hostFields` | `2`: how much of another host's protocol (§3.2) this recorder understands. An adapter reads it before it relies on a field: with 1 it records, and from 2 it may also ask for a plan and step through it. A recorder that does not know another host answers no `hostFields`. |
922
- | `segmentArm` | whether segments the service hands out may run mid-task (default from 0.3.19: yes; `BIR_SEGMENT_ARM=0`: observed only) |
923
- | `intentArm` | whether a step's intent may start a plan in its own PreToolUse, which then waits for the probe. It follows `segmentArm` unless `BIR_INTENT_ARM` is set. Absent from a recorder older than 0.3.19. |
924
- | `sessionCount`, `sessions[]` | at most 50, active ones first: `{sessionId, runId, active, idleMs, ended, steps, recording, replay, intent, fragment}`. `replay` includes `armed`, `unannounced`, `running`, `outcome` and `handover` (`{kind, stepIndex}`; `kind` may be `decision`). `fragment` is `{state}`, with `decision: true` for one scheduled at a plan's decision. |
896
+ | `sessionCount`, `sessions[]` | at most 50, active ones first: `{sessionId, runId, active, idleMs, ended, steps, recording, replay, intent, fragment}`. `replay` includes `armed`, `unannounced`, `running`, `outcome` and `handover`. |
925
897
 
926
898
  ### 8.4 Logs and audit lines
927
899
 
@@ -943,7 +915,7 @@ Lines read `[bir] <ISO time> <event> key=value …`, with each field clipped at
943
915
  | Hooks | `control.listening` (port, `recorder=<state>`, `correlation=`). `control.correlation_allow` (auto-approval opted in). `control.hook_deadline`, `control.hook_unauthorized` (a token from another install), `control.hook_unknown`, `control.hook_oversize`, `control.hook_event_mismatch`, `control.route_failed`, `control.drain_abandoned`. |
944
916
  | Recorder lifecycle | `daemon.restart` (replaced an older recorder), `daemon.restart_deferred` (mid-run: next start), `daemon.start_alongside`, `daemon.newer_kept`, `daemon.revived`. `control.port_busy` (holder and fix), `control.already_running`, `control.superseded`, `control.discovery_reclaimed`, `control.log_reopened`, `control.fatal`. `overrides.stale` (a local copy no longer matches its source: run `bir setup`). |
945
917
  | Proxy (MCP log) | `proxy.upstream_gone` (exiting with the server), `proxy.recorder_revive`, `tier.downgrade`, `tier.rebound`, `relay.undeliverable`. `upstream.auth_required` (the remote refused the handshake: credentials), `upstream.needs_sign_in`, `upstream.headers_invalid`, `upstream.proxy_env_ignored` (this Node cannot use HTTPS_PROXY), `upstream.proxy_env_invalid`, `upstream.standing_stream_failed` (at most hourly). Verbose only: `proxy.started`, `tier.bound`, `tier.passthrough`, `run.start` / `run.finish`, `upstream.stream_*`. |
946
- | Replay | `replay.enabled` (at start: servers, threshold, who derives). `plan.armed`. `replay.decision` (also `arm_timeout`, `arm_unannounced`, `no_directive`, `no_delivery_tool`, `missing_target`, `host_records_only`, `host_tool_missing`). `replay.pin`, `replay.pin_declined`, `replay.pin_abandoned`, `replay.pin_overran`, `replay.diverge`, `replay.diverge_overran`, `replay.handover`, `replay.vetoed`, `replay.unsafe_param`, `replay.derive_timeout`, `replay.budget_clamped`, `replay.concurrent_call`, `replay.no_session_proxy`, `replay.run_ambiguous`, `replay.run_abandoned`. A plan's decision (0.3.19): `replay.handover kind=decision` (`via=divergence` when a divergence's bundle ran up to it), `run.fragment … decision=true` (the agent's own work after it is recorded), `run.fragment_dropped` (a plan the intent check armed took the agent's next call, so nothing is recorded as the other half), `replay.adhoc_decision` (`bir replay` stopped in front of it). `replay.enabled` also says `segments=run mid-task`, or `observed only` with `BIR_SEGMENT_ARM=0` or `BIR_INTENT_ARM=0`. |
918
+ | Replay | `replay.enabled` (at start: servers, threshold, who derives). `plan.armed`. `replay.decision` (also `arm_timeout`, `arm_unannounced`, `no_directive`, `no_delivery_tool`, `missing_target`, `host_records_only`, `host_tool_missing`). `replay.pin`, `replay.pin_declined`, `replay.pin_abandoned`, `replay.pin_overran`, `replay.diverge`, `replay.diverge_overran`, `replay.handover`, `replay.vetoed`, `replay.unsafe_param`, `replay.derive_timeout`, `replay.budget_clamped`, `replay.concurrent_call`, `replay.no_session_proxy`, `replay.run_ambiguous`, `replay.run_abandoned`. |
947
919
  | Run cost | `run.unmeasured` (the turn's cost is not measured: `why` gives the reason, and `models` the models that have no price in the runner's table). |
948
920
  | A host other than Claude Code (§3.2) | `run.start … host=langchain` (`bir investigate` reads the host from it, and raises no `steer_mode` finding for such a turn: steer is the full mode there), `plan.armed … mode=steer host=langchain`. `run.finish … ended=<reason>` (the run did not end with an ordinary stop). `run.unmeasured` (`why="the host sent no usage"`, a model with no price, or a run that made no model call). `run.incomplete` (the host says the recording is not the whole run: its finish carries the `incomplete` mark). `run.host_failed` (the host says the run failed: its recording is closed with that error). The service's new build never picks a run with an `error` or that mark for automatic calculation, and such a run no longer catches its prompt, unless its owner preferred it or it already has a plan (built, not deployed yet). `replay.decision verdict=no-steer code=host_records_only` (the adapter asked for no plan with its prompt) and `code=host_tool_missing` (the plan names a tool that the agent which asked does not have). `replay.own_call` (the model made a call of its own while the plan was live: the plan is handed over). `replay.step_host_stopped` (`stop=rejected` or `stop=edited`). `session.host_refused` (a scope that is empty, longer than 64 characters or not a text: nothing of that session is recorded or replayed). |
949
921
  | `bir-scenario` (MCP log) | `scenario.answered` (`ok` means results were delivered; also `partial`, `bound`), `scenario.recorder_revive`, `scenario.switched`, `scenario.hello_failed`, `scenario.tool_timeout`, `scenario.tool_truncated`. |
@@ -1076,13 +1048,12 @@ Defaults were checked in `src/`.
1076
1048
  | `BIR_PATH_ALLOW` | recorder | unset (any folder) | Folders a runner-side `Read`, `Glob`, `Grep`, `Edit` or `Write` may touch (`*` for all). Unset means any path, as the agent's own tools (from 0.3.21; up to 0.3.20 the project directory alone). `project` is the session's project directory, `~` the home folder: `project,~/.claude`. Under a list, a path outside it, symlinks included, is `not_run` (hand-over). `none` means no folder, and file steps are steered. |
1077
1049
  | `BIR_DERIVE_RECENT_RESULTS` | recorder | 5 | How many of this prompt's tool results derivation may read (`0`: none). Never for plans with a command sink, and never for a plan started at the prompt: a prompt just submitted has no results yet, and an earlier prompt's are not its own. So in a session of another host, where a plan starts at the prompt only, none are read. |
1078
1050
  | `BIR_INTENT_MATCH` | recorder | on | `0` turns mid-turn intent probes off. |
1079
- | `BIR_INTENT_MATCH_BUDGET_MS` | recorder | 4000 | One probe's own ceiling. It is not a hook wait. |
1051
+ | `BIR_INTENT_MATCH_BUDGET_MS` | recorder | 1500 | One probe's own ceiling. A probe is sent only with `BIR_INTENT_ARM=1`. |
1080
1052
  | `BIR_INTENT_MATCHES_PER_TURN` | recorder | 3 | Plans intent may arm per turn. |
1081
1053
  | `BIR_INTENT_REQUESTS_PER_TURN` | recorder | 40 | Probes per turn. |
1082
1054
  | `BIR_INTENT_SOURCES` | recorder | `text,thinking,tool` | Which text an intent may be read from. |
1083
- | `BIR_INTENT_ARM` | recorder | unset (follows `BIR_SEGMENT_ARM`, so on from 0.3.19) | `1`: a probe may arm a plan in its own PreToolUse, and that hook waits for it. `0`: never. Never in a session of another host (§3.2), where a probe only counts. |
1055
+ | `BIR_INTENT_ARM` | recorder | unset (off) | `1`: a call the agent explained sends an intent probe, and a whole plan it matches may arm in that call's own PreToolUse, which waits for it. Unset or `0`: off — no probe is sent and nothing arms mid-turn, as up to 0.3.18. Never in a session of another host (§3.2). |
1084
1056
  | `BIR_INTENT_ARM_BUDGET_MS` | recorder | 500 | How long that PreToolUse waits for the probe. |
1085
- | `BIR_SEGMENT_ARM` | recorder | on (from 0.3.19; unset or `1`) | A segment the service hands out runs mid-task: one found by repeated work, or a half after a plan's decision. `0`: observe only, where what would have armed is logged (`replay.segment_would_arm`) and nothing is replaced. Up to 0.3.18 the default was observe only. Not in a session of another host. |
1086
1057
  | `BIR_CONTROL_URL` | proxy, scenario | written into wrapped entries | The project's control server. Together with `BIR_CONTROL_TOKEN` it is the SDK channel and wins over discovery. |
1087
1058
  | `BIR_CONTROL_TOKEN` | proxy, scenario | unset | Bearer token for the SDK channel. |
1088
1059
  | `BIR_PROXY_STAY_UP` | proxy | unset | `1`: keep answering errors after the upstream dies (0.2.12). |
@@ -1140,7 +1111,6 @@ Defaults were checked in `src/`.
1140
1111
  - `langChainGuide.md`, which is in the repository's root and not in the npm package: its "Where it stands" table and its part 5 describe the time before the release.
1141
1112
 
1142
1113
  The install scripts cannot set a folder up for such a host, so there is no one-line install for it.
1143
- 8. **A plan's decision (0.3.19): the service first.** Deploy the service build that accepts `fallbackKind: "decision"` before 0.3.19 is published (the service's segmentsbreakdown.md section 8, step 1). An older service answers such a report 400, and the run's cost and saving are lost. Then test 0.3.18 against a plan with a decision (it stops there, reported as `known_bad_step`). The deployed service makes no split while its `SCENARIO_SPLIT` is false, so run this check on a sandbox or local service started with `SCENARIO_SPLIT=true`, with bir 0.3.18 pointed at it: set `BIR_AUTH_URL` to its address, run `bir login` (with an account on that service), then `bir up --restart` in the folder you test in. bir keeps one session, for the service that issued it: without that sign-in nothing is recorded on the local service, and while bir is signed in there, nothing on this PC is recorded on the deployed one. The service's segmentsbreakdown.md section 8, step 2, has the steps. Then point bir back: set `BIR_AUTH_URL` to the deployed service's address (removing it is not enough, since the local sign-in stored the local address), run `bir login` again, then `bir up --restart` in that folder. Then publish 0.3.19, and only then turn the deployed service's `SCENARIO_SPLIT` on.
1144
1114
 
1145
1115
  **Manual end-to-end checks with the real Claude Code.** These are behaviours of Claude Code itself, which the automated tests simulate but cannot prove. Use the Claude Code version you will support; the approval rules and hook schema were read from 2.1.280.
1146
1116
 
@@ -1,28 +0,0 @@
1
- /**
2
- * condition-words — the request's own words for its decision (the service's
3
- * segmentsbreakdown.md SB-18).
4
- *
5
- * When a plan stops at its decision, the hand-over note quotes the sentences
6
- * of the live request that hold a word for a condition ("if … otherwise"), so
7
- * the agent sees which choice is its own to make. Only the request's words:
8
- * never the condition the service keeps, which is written from the recorded
9
- * run and would hint at the half the recording took.
10
- *
11
- * The word lists are the service's (its src/scenarios/condition.ts), copied
12
- * exactly. Keep the two the same: the service decides which requests may hold
13
- * a condition with them, and this quotes the sentences they find.
14
- */
15
- /** The longest quote a decision note carries, in characters. */
16
- export declare const MAX_DECISION_QUOTE = 600;
17
- /** Whether a text has a word for a condition in it: the service's `mayHoldCondition`. */
18
- export declare function mayHoldCondition(text: string): boolean;
19
- /**
20
- * The sentences of `request` that hold a word for a condition, in order,
21
- * each on one line of single spaces, joined by a space. Cut to at most `max`
22
- * characters, ending in "…" when cut. Null when no sentence holds one.
23
- *
24
- * Lines are sentences of their own: the request is split at line breaks
25
- * first, then at {@link SENTENCE_BREAK}.
26
- */
27
- export declare function conditionSentences(request: string | null | undefined, max?: number): string | null;
28
- //# sourceMappingURL=condition-words.d.ts.map