@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
@@ -104,22 +104,18 @@
104
104
  * found (default 5; 0 off)
105
105
  * BIR_INTENT_SOURCES which text a step's intent may come from
106
106
  * (default text,thinking,tool)
107
- * BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40).
108
- * Separate from BIR_INTENT_MATCHES_PER_TURN: arming
109
- * is a claim on the turn, counting hits is not
110
- * BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 4000).
111
- * Not a wait of its own: PreToolUse waits for a
112
- * probe only while arming is on (the default from
113
- * 0.3.19; off with BIR_SEGMENT_ARM=0 or
114
- * BIR_INTENT_ARM=0), and then at most
115
- * BIR_INTENT_ARM_BUDGET_MS (default 500)
116
- * BIR_SEGMENT_ARM unset or 1 (the default from 0.3.19): a segment
117
- * the service hands out runs mid-task — one found
118
- * by repeated work, or a half after a plan's
119
- * decision — and so may a whole plan whose intent
120
- * a step matches (BIR_INTENT_ARM follows it unless
121
- * set). 0 is observe-only: what would have armed
122
- * is logged and nothing is replaced
107
+ * BIR_INTENT_ARM 1 lets a whole plan whose intent matches the
108
+ * agent's reasoning mid-turn arm in the place of the
109
+ * call. Unset (or 0) is off: no probe is sent and
110
+ * nothing arms mid-turn
111
+ * BIR_INTENT_ARM_BUDGET_MS how long a call waits for that probe (default 500)
112
+ * BIR_CONTINUATIONS 0 stops asking, after a plan handed over, for the
113
+ * plan of the work recorded after an earlier
114
+ * hand-over at that step (default on: one request
115
+ * per hand-over; it runs only when the agent's next
116
+ * call is its first step)
117
+ * BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40)
118
+ * BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 1500)
123
119
  * BIR_REPLAY_BUDGET_MS whole-plan ceiling, direct mode (unset: the
124
120
  * controller's 100000, BIR_REPLAY_PLAN_BUDGET_MS)
125
121
  * BIR_STEP_TIMEOUT_MS one direct tools/call (default 60000)
@@ -104,22 +104,18 @@
104
104
  * found (default 5; 0 off)
105
105
  * BIR_INTENT_SOURCES which text a step's intent may come from
106
106
  * (default text,thinking,tool)
107
- * BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40).
108
- * Separate from BIR_INTENT_MATCHES_PER_TURN: arming
109
- * is a claim on the turn, counting hits is not
110
- * BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 4000).
111
- * Not a wait of its own: PreToolUse waits for a
112
- * probe only while arming is on (the default from
113
- * 0.3.19; off with BIR_SEGMENT_ARM=0 or
114
- * BIR_INTENT_ARM=0), and then at most
115
- * BIR_INTENT_ARM_BUDGET_MS (default 500)
116
- * BIR_SEGMENT_ARM unset or 1 (the default from 0.3.19): a segment
117
- * the service hands out runs mid-task — one found
118
- * by repeated work, or a half after a plan's
119
- * decision — and so may a whole plan whose intent
120
- * a step matches (BIR_INTENT_ARM follows it unless
121
- * set). 0 is observe-only: what would have armed
122
- * is logged and nothing is replaced
107
+ * BIR_INTENT_ARM 1 lets a whole plan whose intent matches the
108
+ * agent's reasoning mid-turn arm in the place of the
109
+ * call. Unset (or 0) is off: no probe is sent and
110
+ * nothing arms mid-turn
111
+ * BIR_INTENT_ARM_BUDGET_MS how long a call waits for that probe (default 500)
112
+ * BIR_CONTINUATIONS 0 stops asking, after a plan handed over, for the
113
+ * plan of the work recorded after an earlier
114
+ * hand-over at that step (default on: one request
115
+ * per hand-over; it runs only when the agent's next
116
+ * call is its first step)
117
+ * BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40)
118
+ * BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 1500)
123
119
  * BIR_REPLAY_BUDGET_MS whole-plan ceiling, direct mode (unset: the
124
120
  * controller's 100000, BIR_REPLAY_PLAN_BUDGET_MS)
125
121
  * BIR_STEP_TIMEOUT_MS one direct tools/call (default 60000)
@@ -488,7 +488,7 @@ class BadArguments extends Error {
488
488
  }
489
489
  const TARGET = {
490
490
  type: "string",
491
- description: "A scn_ id, or the run_ id of a whole-run scenario (a sub-task scenario has only its scn_ id) — as investigate and scenario_show print them.",
491
+ description: "A scn_ id, or the run_ id of the recording it was calculated from — as investigate and scenario_show print them.",
492
492
  };
493
493
  const STEP = {
494
494
  type: "integer",
package/dist/bin/bir.js CHANGED
@@ -114,7 +114,7 @@ Commands:
114
114
  a scenario: intent, params, steps (--step: just that one)
115
115
  scenario calc <runId|scnId> [--force [--discard-edits]]
116
116
  calculate (or re-derive) a run's scenario; a scn_ id
117
- (a sub-task's too) is recalculated in place, with --force
117
+ is recalculated in place, with --force
118
118
  scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
119
119
  try a change to one step against the recording; saves nothing
120
120
  scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
@@ -3118,20 +3118,6 @@ async function doctor(args) {
3118
3118
  "For headless runs (`claude -p`) allow them in permissions.allow or --allowedTools, or set " +
3119
3119
  "BIR_CORRELATION_DECISION=allow where the recorder starts");
3120
3120
  }
3121
- // Whether a segment the service hands out may run mid-task (segmented.md
3122
- // R-LIFE-8). On by default from 0.3.19 (the service's segmentsbreakdown.md
3123
- // SB-21); either state must be visible without reading a log, and an older
3124
- // recorder still running here keeps the old default until it is replaced.
3125
- // A segment starts only where the probe is waited on, so `intentArm: false`
3126
- // (BIR_INTENT_ARM=0) starts nothing mid-task, segments included; a
3127
- // recorder older than that field says nothing of it.
3128
- notes.push(health.segmentArm === true && health.intentArm !== false
3129
- ? "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)"
3130
- : health.segmentArm === true
3131
- ? "segments are observed only — nothing starts mid-task (BIR_INTENT_ARM=0 where the recorder started)"
3132
- : theirs && theirs !== VERSION
3133
- ? `segments are observed only — this recorder (${theirs}) is older than this bir, which runs them by default: \`bir up --restart\``
3134
- : `segments are observed only — nothing is replaced (BIR_SEGMENT_ARM=0 where the recorder started)${health.intentArm === true ? "; whole plans may still start mid-task (BIR_INTENT_ARM=1)" : ""}`);
3135
3121
  const sessions = health.sessions ?? [];
3136
3122
  const probes = sessions.reduce((n, s) => n + (s.intent?.probes ?? 0), 0);
3137
3123
  const armed = sessions.reduce((n, s) => n + (s.intent?.armed ?? 0), 0);
@@ -3721,12 +3707,6 @@ async function replayCommand(args) {
3721
3707
  }
3722
3708
  out(`response ${JSON.stringify(result.responseModel ?? {})}`);
3723
3709
  out(`steps ${result.steps ?? 0}${result.partial ? " (partial — budget reached)" : ""}`);
3724
- // A plan's decision (the service's segmentsbreakdown.md SB-6): the steps
3725
- // from there are the half the recording took, so the run stops before them.
3726
- if (result.stopped?.reason === "decision") {
3727
- out(`stopped before step ${result.stopped.step}${result.stopped.tool ? ` (${result.stopped.tool})` : ""}: ` +
3728
- "the plan's decision, which the agent makes — the steps from there did not run");
3729
- }
3730
3710
  return 0;
3731
3711
  }
3732
3712
  // ── wrap ───────────────────────────────────────────────────────────────────
@@ -245,13 +245,6 @@ interface ServiceInvestigation {
245
245
  runId?: string | null;
246
246
  } | null;
247
247
  corrections?: ServiceCorrection[];
248
- /**
249
- * The step the plan stops in front of so that the agent decides, and the
250
- * condition in words (the service's segmentsbreakdown.md SB-6). Null when
251
- * the plan has none; absent from an older service.
252
- */
253
- decisionStep?: number | null;
254
- decisionText?: string | null;
255
248
  } | null;
256
249
  executions?: Array<{
257
250
  id: string;
@@ -272,8 +265,6 @@ interface ServiceInvestigation {
272
265
  status: string;
273
266
  error?: string;
274
267
  }> | null;
275
- /** Where a `fell_back` run handed over, and why (`decision`, …): null for any other run. */
276
- fallbackKind?: string | null;
277
268
  /** The repeat the agent redid the task in, and its own calls there (AC1). */
278
269
  redoneByRunId?: string | null;
279
270
  redoneOwnCalls?: number | null;
@@ -35,7 +35,6 @@ const INCIDENTS = new Set([
35
35
  "replay.arm_failed",
36
36
  "replay.derive_failed",
37
37
  "replay.compose_failed",
38
- "replay.flatten_failed",
39
38
  "replay.thread_failed",
40
39
  "replay.step_failed",
41
40
  "replay.failed",
@@ -203,8 +202,8 @@ export function findTurn(turns, id) {
203
202
  /** The runner's own gate vocabulary, with the cause and the fix (guide §9). */
204
203
  export const DECLINES = {
205
204
  replay_disabled: {
206
- cause: "calculated replay was off in this bir-hooks (BIR_REPLAY=0), or a sub-task was handed out to a recorder that only observes sub-tasks (BIR_SEGMENT_ARM=0, or a recorder older than 0.3.19).",
207
- fix: "start bir-hooks with BIR_REPLAY=1 (the default); for sub-tasks leave BIR_SEGMENT_ARM unset (on from 0.3.19), then `bir up --restart`.",
205
+ cause: "calculated replay was off in this bir-hooks (BIR_REPLAY=0).",
206
+ fix: "start bir-hooks with BIR_REPLAY=1 (the default).",
208
207
  },
209
208
  not_ready: {
210
209
  cause: "the matched recording had no ready scenario at that moment.",
@@ -222,16 +221,12 @@ export const DECLINES = {
222
221
  },
223
222
  known_bad_first_step: {
224
223
  cause: "the chain's first step is parked after repeated failures.",
225
- fix: "wait: the service retries it once an hour for a day, then repairs the plan from the next run of the prompt — unless the plan has steps edited by hand, or is a sub-task, which it does not repair on its own. To repair now: `bir scenario calc <scnId> --force` (add --discard-edits to drop hand edits), or fix the step: `bir scenario edit <scnId> --step 0 --input-logic <file>`. `bir investigate <scnId>` names the scenario id.",
224
+ fix: "wait: the service retries it once an hour for a day, then repairs the plan from the next run of the prompt — unless the plan has steps edited by hand, which it does not repair on its own. To repair now: `bir scenario calc <scnId> --force` (add --discard-edits to drop hand edits), or fix the step: `bir scenario edit <scnId> --step 0 --input-logic <file>`. `bir investigate <scnId>` names the scenario id.",
226
225
  },
227
226
  nondeterministic_first_step: {
228
227
  cause: "the chain's first step needs a judgement the calculation could not write code for, so the plan cannot start.",
229
228
  fix: "if its code only has a slip, fix the step yourself: read it with `bir scenario show <scnId> --step 0`, write the input logic to a file, and run `bir scenario edit <scnId> --step 0 --input-logic <file>`; the service checks it against the recording before it saves it. A real judgement — a choice with no fixed rule — stays with the agent until a model step exists (plan-services.md W3.1).",
230
229
  },
231
- unusable_first_step: {
232
- cause: "the first step calls a sub-task that is gone, switched off or stale.",
233
- fix: "enable or recalculate that sub-task, or recalculate this scenario.",
234
- },
235
230
  missing_target: {
236
231
  cause: "the plan needs a target — a value a prompt must name, such as a file, a table or a region — and the prompt did not name it. A replay never takes a target from the recording.",
237
232
  fix: 'name it in the prompt. If the value is the same every time — a database, a host, a working folder — it is a setting, not a target: `bir scenario edit <scnId> --param <name> --kind setting --note "why"` lets the runner use its recorded value when a prompt names none (`bir scenario show <scnId>` lists the parameters and their kinds).',
@@ -246,11 +241,6 @@ export const DECLINES = {
246
241
  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.",
247
242
  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.",
248
243
  },
249
- // Nor this one: the control server's own rule (the service's offer1.md O8).
250
- would_repeat: {
251
- cause: "a plan the intent check matched would have started after the agent's call, not in its place, and its steps call that same tool: it would have made the call a second time — an e-mail sent twice (issue fx2). Never twice: it was not started, and the agent went on.",
252
- fix: "nothing to fix in the plan. A scenario starts in the call's place only when the agent's call is its first step, value for value; one recorded in the order the agent works in (the agent's own run after a decision becomes one) is what starts there.",
253
- },
254
244
  // Nor is this one: the match itself was late, so nothing was armed at all.
255
245
  match_late: {
256
246
  cause: "the service's answer to the match landed too late: after the prompt hook had stopped waiting for it (3 s, BIR_MATCH_BUDGET_MS); while prompts were not waiting on the match at all — after one slow answer they stop waiting for a few minutes (`replay.match_gate state=holding`); or with the hook not waiting from the start, because a failure to reach the service was still on record (a `service.degraded` line, perhaps hours before, and no `service.recovered` until after this prompt) or the match request's first attempt failed. The agent did the task itself.",
@@ -264,10 +254,6 @@ export const DECLINES = {
264
254
  cause: "the service matched the prompt and handed over no plan: this runner is older than its plans need.",
265
255
  fix: "update the runner (`bir update`), then run `bir setup` in each project that uses it; `bir doctor` says when the service is satisfied.",
266
256
  },
267
- flatten_failed: {
268
- cause: "the chain could not be flattened — a sub-task it calls no longer resolves.",
269
- fix: "recalculate the scenario.",
270
- },
271
257
  // The gates of a host that is not Claude Code (a LangChain application).
272
258
  host_records_only: {
273
259
  cause: "the runner in that application's folder is older than replay for LangChain needs, so the LangChain package only records.",
@@ -439,11 +425,9 @@ export function localFindings(turn) {
439
425
  }
440
426
  for (const i of turn.incidents) {
441
427
  const fields = Object.fromEntries(Object.entries(i).filter(([k]) => k !== "at" && k !== "event"));
442
- // A stop at the plan's decision is the plan working as split: no fault.
443
- const decisionStop = i.event === "replay.handover" && i.kind === "decision";
444
428
  out.push({
445
429
  code: i.event.replace(/\./g, "_"),
446
- severity: i.event.endsWith("_failed") || i.event === "replay.failed" ? "problem" : decisionStop ? "info" : "warn",
430
+ severity: i.event.endsWith("_failed") || i.event === "replay.failed" ? "problem" : "warn",
447
431
  title: `${i.event}${i.error ? `: ${String(i.error)}` : ""}`,
448
432
  cause: Object.entries(fields)
449
433
  .map(([k, v]) => `${k}=${String(v)}`)
@@ -454,15 +438,13 @@ export function localFindings(turn) {
454
438
  : "the turn's cost could not be read from the transcript, or the runner has no price for the model: update it. Its report is unmeasured; run bir-hooks where the Claude Code transcript is readable."
455
439
  : i.event === "replay.handover" && i.kind === "other_branch"
456
440
  ? "this request was the other half of the task the plan holds (a step's test said so), and the agent did it; that run is how the service learns the other half."
457
- : decisionStop
458
- ? "nothing to fix: the plan stopped at its decision, as planned. It ran the steps before it, and the agent chose the way. A segment of the half it chose may have run next (a `replay.intent_armed` line with kind segment); a `run.fragment` line with decision=true is the agent's own work, from which the service makes the other half."
459
- : i.event === "replay.diverge" || i.event === "replay.handover"
460
- ? "the model left the script or the runner handed the turn back; the service section shows what the fallback cost."
461
- : i.event === "run.incomplete"
462
- ? "the recording is not the whole run, so it is never calculated and no longer catches its prompt; run the task again to record it whole."
463
- : i.event === "run.host_failed"
464
- ? "the application's run failed; its recording is closed with that error and never becomes a plan."
465
- : "see the guide's troubleshooting table for this line.",
441
+ : i.event === "replay.diverge" || i.event === "replay.handover"
442
+ ? "the model left the script or the runner handed the turn back; the service section shows what the fallback cost."
443
+ : i.event === "run.incomplete"
444
+ ? "the recording is not the whole run, so it is never calculated and no longer catches its prompt; run the task again to record it whole."
445
+ : i.event === "run.host_failed"
446
+ ? "the application's run failed; its recording is closed with that error and never becomes a plan."
447
+ : "see the guide's troubleshooting table for this line.",
466
448
  });
467
449
  }
468
450
  const r = turn.reported;
@@ -545,12 +527,6 @@ export function savedPct(turn) {
545
527
  return baseline > 0 ? r.savedUsd / baseline : null;
546
528
  }
547
529
  const short = (id) => (id ? id.slice(0, 13) : "—");
548
- /**
549
- * An execution's outcome, and where it handed the task over when the service
550
- * says (`fell_back · decision`). The service sends `fallbackKind: null` for a
551
- * run that did not hand over, and an older one sends nothing.
552
- */
553
- const outcomeOf = (e) => typeof e.fallbackKind === "string" && e.fallbackKind !== "" ? `${e.outcome} · ${e.fallbackKind}` : e.outcome;
554
530
  /** 4624 → "4,624", whatever the machine's locale. */
555
531
  const count = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
556
532
  const money = (v) => (v == null ? "—" : usd(v));
@@ -653,10 +629,6 @@ export function renderService(out, s, viewerIsOwner) {
653
629
  if (sc.held) {
654
630
  out(` HELD since ${when(sc.held.at ?? undefined)}${sc.held.reason ? ` (${sc.held.reason})` : ""}: not handed out — the agent does the task until the plan is corrected or let go${sc.held.runId ? `; the redo is ${sc.held.runId}` : ""}`);
655
631
  }
656
- if (typeof sc.decisionStep === "number") {
657
- const text = typeof sc.decisionText === "string" ? sc.decisionText.replace(/\s+/g, " ").trim() : "";
658
- out(` decision at step ${sc.decisionStep}: the plan stops in front of it, and the agent decides${text ? ` (${text.length > 160 ? `${text.slice(0, 160)}…` : text})` : ""}`);
659
- }
660
632
  for (const line of stepLines(sc.steps))
661
633
  out(` ${line}`);
662
634
  const correction = newestCorrection(sc.corrections);
@@ -674,7 +646,7 @@ export function renderService(out, s, viewerIsOwner) {
674
646
  if (ex.length > 0) {
675
647
  out(` executions (${ex.length}, newest first)`);
676
648
  for (const e of ex.slice(0, 10)) {
677
- out(` ${when(e.createdAt)} ${short(e.id)} ${outcomeOf(e).padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
649
+ out(` ${when(e.createdAt)} ${short(e.id)} ${e.outcome.padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
678
650
  const failed = e.steps?.filter((st) => st.status === "failed") ?? [];
679
651
  for (const st of failed)
680
652
  out(` step ${st.stepIndex} ${st.toolName ?? ""} failed${st.error ? `: ${st.error.slice(0, 120)}` : ""}`);
@@ -702,7 +674,7 @@ function stepLines(steps) {
702
674
  if (st.correctionId)
703
675
  parts.push(`${st.correctionKind ?? "changed"} by correction ${st.correctionId}`);
704
676
  if (parts.length > 0)
705
- lines.push(`step ${st.stepIndex} ${st.toolName ?? "(a sub-task)"}: ${parts.join("; ")}`);
677
+ lines.push(`step ${st.stepIndex} ${st.toolName ?? "?"}: ${parts.join("; ")}`);
706
678
  }
707
679
  return lines;
708
680
  }
@@ -809,7 +781,7 @@ export async function investigateCommand(args, deps) {
809
781
  if (rows.length === 0)
810
782
  out(" none yet");
811
783
  for (const e of rows) {
812
- out(` ${when(e.createdAt)} ${e.id} ${outcomeOf(e).padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
784
+ out(` ${when(e.createdAt)} ${e.id} ${e.outcome.padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
813
785
  out(` scenario ${e.scenarioId}${e.runId ? ` run ${e.runId}` : ""}${e.intent ? ` "${e.intent.slice(0, 60)}"` : ""}`);
814
786
  }
815
787
  out();
@@ -100,9 +100,8 @@ export async function scenarioEditCommand(args, deps) {
100
100
  }
101
101
  /**
102
102
  * The raw JSON, as it always was — scripts and the guide read it. A `scn_` id
103
- * goes to `GET /scenarios/:id`, which also serves sub-task scenarios (they have
104
- * no run of their own); `--step` narrows the print to that one step object,
105
- * with the scenario's id and `chainRevision` put in front of it. The revision
103
+ * goes to `GET /scenarios/:id`; `--step` narrows the print to that one step
104
+ * object, with the scenario's id and `chainRevision` put in front of it. The revision
106
105
  * is a scenario's, not a step's, and it is what `edit --revision` (and the MCP
107
106
  * `scenario_edit`'s `revision`) takes: a step printed without it sent the model
108
107
  * that had just read the step off to guess one (C20).
@@ -130,7 +129,7 @@ async function show(args, deps) {
130
129
  const served = reply.body;
131
130
  const scenario = served?.scenario;
132
131
  if (!scenario)
133
- return noWholeRunScenario(target, served?.segments, args, deps);
132
+ return noScenario(target, args, deps);
134
133
  if (!Array.isArray(scenario.steps)) {
135
134
  out(`${scenario.id ?? target} is ${scenario.state ?? "not ready"}, so it has no steps to show yet.`);
136
135
  return 1;
@@ -510,7 +509,7 @@ function inputPhrase(status, verifiable) {
510
509
  case "not_checked":
511
510
  return verifiable
512
511
  ? "not checked: the recorded call is plain text, not an object"
513
- : "not checked: the step comes after a sub-task call, whose outputs the check cannot rebuild";
512
+ : "not checked: the recording holds no call to check this step against";
514
513
  case "differs":
515
514
  return "differs from the recorded call";
516
515
  case "throws":
@@ -955,12 +954,10 @@ function paramUndone(answer, editId, target, args, deps) {
955
954
  * step from the recording, so the service refuses `--force` on a plan with
956
955
  * hand edits (D4) unless `--discard-edits` says to throw them away.
957
956
  *
958
- * A `run_` id posts `/recordings/runs/:id/calculate`, which reaches only the
959
- * run's **whole-run** scenario. A `scn_` id posts `/scenarios/:id/recalculate`
960
- * — the only way to recalculate a sub-task (segment) scenario from here, and
961
- * so the only way to take a segment's hand edits back once undo cannot (C12).
962
- * A scenario id always names one that exists, so it is always a
963
- * recalculation in place, and asks for `--force` as the run form does.
957
+ * A `run_` id posts `/recordings/runs/:id/calculate`. A `scn_` id posts
958
+ * `/scenarios/:id/recalculate`. A scenario id always names one that exists,
959
+ * so it is always a recalculation in place, and asks for `--force` as the run
960
+ * form does.
964
961
  */
965
962
  async function calc(args, deps) {
966
963
  const { out } = deps;
@@ -1106,28 +1103,13 @@ function localFailure(args, deps, code, text, extra = {}) {
1106
1103
  deps.out(args.json ? jsonText({ error: code, details: text, ...extra }) : text);
1107
1104
  return 1;
1108
1105
  }
1109
- /**
1110
- * A run with no whole-run scenario. When sub-task (segment) scenarios were
1111
- * calculated from it, they are listed with their `scn_` ids — the only ids
1112
- * they have — instead of suggesting a calculation, which would start a new
1113
- * whole-run plan beside them rather than reach the one the person meant (C21).
1114
- */
1115
- function noWholeRunScenario(target, segments, args, deps) {
1116
- const listed = (Array.isArray(segments) ? segments : []).filter((s) => typeof s?.id === "string");
1117
- if (listed.length === 0) {
1118
- return localFailure(args, deps, "no_scenario", `${target} has no calculated scenario yet — \`bir scenario calc ${target}\` first.`);
1119
- }
1120
- const range = (s) => typeof s.stepFrom === "number" && typeof s.stepTo === "number" ? ` steps ${s.stepFrom}–${s.stepTo} of the recording` : "";
1121
- const text = [
1122
- `${target} has no whole-run scenario, only sub-task scenarios — and a sub-task scenario is addressed by its own scn_ id:`,
1123
- ...listed.map((s) => ` ${s.id}${range(s)}`),
1124
- ].join("\n");
1125
- return localFailure(args, deps, "no_whole_run_scenario", text, { segments: listed.map((s) => s.id) });
1106
+ /** A run with no calculated scenario yet. */
1107
+ function noScenario(target, args, deps) {
1108
+ return localFailure(args, deps, "no_scenario", `${target} has no calculated scenario yet — \`bir scenario calc ${target}\` first.`);
1126
1109
  }
1127
1110
  /**
1128
1111
  * A `run_` id is the recording's; the routes that change a scenario take its
1129
- * `scn_` id (D7). A `run_` id reaches only the run's **whole-run** scenario; a
1130
- * `scn_` id is used as it is — sub-task scenarios have nothing else.
1112
+ * `scn_` id (D7), and a `scn_` id is used as it is.
1131
1113
  */
1132
1114
  async function resolveTarget(target, args, deps, clock = new ReadClock()) {
1133
1115
  if (target.startsWith("scn_"))
@@ -1145,7 +1127,7 @@ async function resolveTarget(target, args, deps, clock = new ReadClock()) {
1145
1127
  const served = reply.body;
1146
1128
  const id = served?.scenario?.id;
1147
1129
  if (typeof id !== "string")
1148
- return { code: noWholeRunScenario(target, served?.segments, args, deps) };
1130
+ return { code: noScenario(target, args, deps) };
1149
1131
  return { id };
1150
1132
  }
1151
1133
  /**
@@ -1199,9 +1181,6 @@ function explain(reply, args, deps, e) {
1199
1181
  out(`${e.verb} (invalid_input): the service found the request invalid${details ? ` — ${details}` : ""}.`);
1200
1182
  return 1;
1201
1183
  }
1202
- case "not_a_tool_step":
1203
- out(`${e.verb} (not_a_tool_step): step ${e.step ?? "?"} is a sub-task call row, not a tool step. Call rows belong to the service; only tool steps can be edited.`);
1204
- return 1;
1205
1184
  case "nothing_to_change":
1206
1185
  // The empty command is refused on this side before any request, so a
1207
1186
  // service `nothing_to_change` always means the change is already the
@@ -1231,16 +1210,6 @@ function explain(reply, args, deps, e) {
1231
1210
  // newer entry puts the step back as this one left it, and then this one
1232
1211
  // can be undone. It used to loop, when undo only toggled (C23).
1233
1212
  const latest = typeof body.latestEditId === "string" ? body.latestEditId : "<the newer edit>";
1234
- if (latest.startsWith("ssplit_")) {
1235
- // The plan was split at its decision since (the service's
1236
- // segmentsbreakdown.md). No `--edit` takes a split back: its Undo is
1237
- // on the recording page's Split card, and only then can this go.
1238
- const what = aboutCorrection ? "correction" : "edit";
1239
- out(`${e.verb} (not_latest_edit): the plan was split at its decision after that ${what} (${latest}). ` +
1240
- `It cannot be taken back while the split is in use: undo the split first (Undo on the Split card of the plan's recording page), then this ${what}:`);
1241
- out(` bir scenario undo ${target} --edit ${e.editId ?? "<this edit>"}`);
1242
- return 1;
1243
- }
1244
1213
  out(aboutCorrection
1245
1214
  ? `${e.verb} (not_latest_edit): the plan has changed since that correction — ${latest} changed it after. Undo the newer one first, then this one:`
1246
1215
  : `${e.verb} (not_latest_edit): the ${aboutParam ? "parameter" : "step"} has changed since that edit — ${latest} changed it after. Undo the newer one first, then this one:`);
@@ -85,4 +85,20 @@ export declare function commandShape(command: string, depth?: number, escape?: s
85
85
  * only the general rule is applied there.
86
86
  */
87
87
  export declare function pinCompatible(toolName: string, modelArgs: unknown, pinnedArgs: unknown, otherHost?: boolean): PinVerdict;
88
+ /**
89
+ * Whether the model's own call is a continuation's first step: the same call,
90
+ * not only the same kind of call (the service's fallbk.md Part 3).
91
+ *
92
+ * Stricter than {@link pinCompatible}, on purpose. A pin takes a call the
93
+ * model was told to make — the directive named the step — and one argument in
94
+ * common is enough to know it is that call. A continuation starts on a call
95
+ * nobody told the model to make: after a hand-over, the call is the model's
96
+ * own choice of what to do next, and the plan may run only when that choice
97
+ * is the one the recording made. Looking up another person, restarting
98
+ * another worker, reading another file is another choice. So the call must
99
+ * pass the pin guard, share at least one argument with the step, and hold
100
+ * the same value in every argument they share — text compared without
101
+ * surrounding space or letter case, anything else exactly.
102
+ */
103
+ export declare function sameFirstCall(toolName: string, modelArgs: unknown, stepArgs: unknown): PinVerdict;
88
104
  //# sourceMappingURL=pin-guard.d.ts.map
@@ -545,4 +545,40 @@ export function pinCompatible(toolName, modelArgs, pinnedArgs, otherHost = false
545
545
  ? { ok: true }
546
546
  : { ok: false, why: "it shares no argument with the step" };
547
547
  }
548
+ /**
549
+ * Whether the model's own call is a continuation's first step: the same call,
550
+ * not only the same kind of call (the service's fallbk.md Part 3).
551
+ *
552
+ * Stricter than {@link pinCompatible}, on purpose. A pin takes a call the
553
+ * model was told to make — the directive named the step — and one argument in
554
+ * common is enough to know it is that call. A continuation starts on a call
555
+ * nobody told the model to make: after a hand-over, the call is the model's
556
+ * own choice of what to do next, and the plan may run only when that choice
557
+ * is the one the recording made. Looking up another person, restarting
558
+ * another worker, reading another file is another choice. So the call must
559
+ * pass the pin guard, share at least one argument with the step, and hold
560
+ * the same value in every argument they share — text compared without
561
+ * surrounding space or letter case, anything else exactly.
562
+ */
563
+ export function sameFirstCall(toolName, modelArgs, stepArgs) {
564
+ const verdict = pinCompatible(toolName, modelArgs, stepArgs);
565
+ if (!verdict.ok)
566
+ return verdict;
567
+ const model = record(modelArgs);
568
+ const step = record(stepArgs);
569
+ const shared = Object.keys(model).filter((key) => Object.prototype.hasOwnProperty.call(step, key));
570
+ if (shared.length === 0) {
571
+ return stableStringify(model) === stableStringify(step)
572
+ ? { ok: true }
573
+ : { ok: false, why: "it shares no argument with the step" };
574
+ }
575
+ const differs = shared.find((key) => !sameValue(model[key], step[key]));
576
+ return differs === undefined ? { ok: true } : { ok: false, why: `its ${differs} is not the step's` };
577
+ }
578
+ function sameValue(a, b) {
579
+ if (typeof a === "string" && typeof b === "string") {
580
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
581
+ }
582
+ return stableStringify(a) === stableStringify(b);
583
+ }
548
584
  //# sourceMappingURL=pin-guard.js.map
@@ -52,21 +52,6 @@ export declare function derivationKey(daemon?: boolean): {
52
52
  apiKey?: string;
53
53
  source?: string;
54
54
  };
55
- /**
56
- * `BIR_SEGMENT_ARM`: whether a segment the service hands out may run mid-task.
57
- * On unless it is `0`, from runner 0.3.19 (the service's segmentsbreakdown.md
58
- * SB-21): segments found by repeated work, and the halves after a plan's
59
- * decision. `0` is observe-only: what would have armed is logged, and nothing
60
- * is replaced. While it is on, whole plans may start mid-task by intent too:
61
- * `BIR_INTENT_ARM` follows it unless that is set.
62
- */
63
- export declare function segmentArmFromEnv(env?: NodeJS.ProcessEnv): boolean;
64
- /**
65
- * What the `replay.enabled` startup line says segments do. A segment starts
66
- * only where the probe is waited on, so `BIR_INTENT_ARM=0` starts nothing
67
- * mid-task, segments included — the rule `bir doctor` words from /health.
68
- */
69
- export declare function segmentsAtStartup(segmentArm: boolean, env?: NodeJS.ProcessEnv): string;
70
55
  /** Where the two replay switches came from, for `/health` and `bir doctor`. */
71
56
  export type ReplaySource = "env" | "sidecar" | "default";
72
57
  /** What a recorder still serves (K4, `ControlServer.activity()`). */
@@ -85,9 +85,8 @@ function wrappedServersFor(cwd) {
85
85
  * `BIR_INTENT_SOURCES` — which text a step's intent may be read from
86
86
  * (segmented.md R-INTENT-3). Unset or unparseable means all three.
87
87
  *
88
- * It governs only what is *sent*. Turning every source off does not stop a
89
- * probe: the request still goes out with an empty text and the server builds a
90
- * line from the tool name and arguments (R-HIT-4).
88
+ * It governs only what is *sent*. With every source off, a step goes out with
89
+ * no text, and no intent probe is sent for it.
91
90
  */
92
91
  function parseIntentSources(value) {
93
92
  if (value === undefined)
@@ -152,33 +151,12 @@ export function derivationKey(daemon = process.env[DAEMON_ENV] === "1") {
152
151
  }
153
152
  return {};
154
153
  }
155
- /**
156
- * `BIR_SEGMENT_ARM`: whether a segment the service hands out may run mid-task.
157
- * On unless it is `0`, from runner 0.3.19 (the service's segmentsbreakdown.md
158
- * SB-21): segments found by repeated work, and the halves after a plan's
159
- * decision. `0` is observe-only: what would have armed is logged, and nothing
160
- * is replaced. While it is on, whole plans may start mid-task by intent too:
161
- * `BIR_INTENT_ARM` follows it unless that is set.
162
- */
163
- export function segmentArmFromEnv(env = process.env) {
164
- return env.BIR_SEGMENT_ARM !== "0";
165
- }
166
- /**
167
- * What the `replay.enabled` startup line says segments do. A segment starts
168
- * only where the probe is waited on, so `BIR_INTENT_ARM=0` starts nothing
169
- * mid-task, segments included — the rule `bir doctor` words from /health.
170
- */
171
- export function segmentsAtStartup(segmentArm, env = process.env) {
172
- if (!segmentArm)
173
- return "observed only (BIR_SEGMENT_ARM=0)";
174
- return env.BIR_INTENT_ARM === "0" ? "observed only (BIR_INTENT_ARM=0)" : "run mid-task";
175
- }
176
154
  /**
177
155
  * Assemble the replay configuration (docs/calculatedReplay.md §5.3 of the guide).
178
156
  *
179
157
  * There is no steer threshold: a match the service hands out is steered
180
158
  * (controller.ts, gate 4). A recorder started with `BIR_MIN_STEER_SIMILARITY`
181
- * or `BIR_MIN_SEGMENT_STEER_SIMILARITY` set ignores them.
159
+ * set ignores it.
182
160
  */
183
161
  function buildReplayOptions(keeper, policy, key) {
184
162
  // The environment still wins — an operator at a terminal is the most
@@ -227,22 +205,14 @@ function buildReplayOptions(keeper, policy, key) {
227
205
  // BIR_INTENT_MATCH=0 turns it off on its own.
228
206
  intentMatch: {
229
207
  enabled: process.env.BIR_INTENT_MATCH !== "0",
230
- // The probe's own ceiling, not a wait on the hook's path: `PreToolUse`
231
- // waits for it only while arming is on — the default, unless
232
- // BIR_SEGMENT_ARM=0 or BIR_INTENT_ARM=0 — and then at most
208
+ // The probe's own ceiling, not a wait on the hook's path: a probe is sent
209
+ // only with BIR_INTENT_ARM=1, and `PreToolUse` waits for it at most
233
210
  // BIR_INTENT_ARM_BUDGET_MS (500 ms), never while the service is failing
234
- // (control/server.ts); otherwise it answers without it (hit counting
235
- // only). 4 000 leaves a request whose best segment lands in the
236
- // not-clear band its one live question under the service's own 2 500 ms
237
- // attempt (segmented.md R-HIT-11).
238
- budgetMs: positiveInt(process.env.BIR_INTENT_MATCH_BUDGET_MS, 4_000),
211
+ // (control/server.ts).
212
+ budgetMs: positiveInt(process.env.BIR_INTENT_MATCH_BUDGET_MS, 1_500),
239
213
  maxPerTurn: positiveInt(process.env.BIR_INTENT_MATCHES_PER_TURN, 3),
240
214
  maxRequestsPerTurn: positiveInt(process.env.BIR_INTENT_REQUESTS_PER_TURN, 40),
241
215
  },
242
- // On by default from 0.3.19 (the service's segmentsbreakdown.md SB-21): a
243
- // plan that stops at its decision hands the rest to segments of each half,
244
- // and they run only mid-task. BIR_SEGMENT_ARM=0 brings back observe-only.
245
- segmentArm: segmentArmFromEnv(),
246
216
  // Both read live, never captured (C5). The recorder signs in after it
247
217
  // listens, refreshes the token as the session outlives it, and may sign
248
218
  // in or out while it runs; replay reads whatever holds *now*. The old
@@ -282,8 +252,6 @@ function buildReplayOptions(keeper, policy, key) {
282
252
  ? "the service"
283
253
  : "recorded sample values — sign in, or a scenario with a target will not run"),
284
254
  intentMatch: opts.intentMatch?.enabled ? "on" : "off",
285
- // Whether a segment the service hands out runs, or is only logged.
286
- segments: segmentsAtStartup(opts.segmentArm === true),
287
255
  source,
288
256
  why: "a prompt that matches a calculated scenario runs it: the scenario's own steps run without a " +
289
257
  "permission prompt of their own (never in plan mode); every other tool call is asked about as " +