@basein/runner 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,6 +61,8 @@ the browser — the OAuth device grant, the same shape `gh auth login` uses. It
61
61
  never handles a password, so it works for accounts that only sign in with
62
62
  Google, and the link can be opened on any device, which is what makes it usable
63
63
  on a headless runner. `--no-browser` prints the link without opening one;
64
+ `--token <value>` redeems a one-time setup token made on the console's *Set up
65
+ the runner* page, with no browser step at all (service 2026-09 or newer);
64
66
  `--password` is the deprecated email-and-password prompt, removed next release.
65
67
 
66
68
  Check it:
@@ -199,8 +201,8 @@ what to do when they register, so there is one switch and no second copy of it:
199
201
  | `BIR_REPLAY=1` | Enable replay. Nothing below matters until it is set. |
200
202
  | `BIR_REPLAY_ALLOW_SERVERS` | Server keys eligible for **direct** execution. Unset means every wrapped server; setting it is the recommendation. |
201
203
  | `BIR_MIN_STEER_SIMILARITY` | Minimum match similarity to replay (default `0.92`, above the service's own `0.9` detection threshold). |
202
- | `ANTHROPIC_API_KEY` | Enables parameter derivation. Without it, replay uses the scenario's recorded sample values — free, and often still correct. |
203
- | `BIR_DERIVE_MODEL` | Derivation model (default `claude-haiku-4-5-20251001`). |
204
+ | `ANTHROPIC_API_KEY` | **Optional.** Working out what a new request is about — which fleet, which file, which date — is done for you by the service on its own key, as long as you are signed in. Set this only to keep that reading on your machine, on your key. Signed out *and* unset, a scenario whose values change between requests is declined rather than replayed on stale ones. |
205
+ | `BIR_DERIVE_MODEL` | Derivation model, when you set a key of your own (default `claude-haiku-4-5-20251001`). |
204
206
  | `BIR_MATCH_BUDGET_MS` | How long the prompt hook waits for a match (default `2500`). |
205
207
  | `BIR_DERIVE_BUDGET_MS` | How long the first `PreToolUse` waits for parameters (default `8000`). |
206
208
  | `BIR_REPLAY_BUDGET_MS` / `BIR_STEP_TIMEOUT_MS` | Whole-plan and per-step ceilings for direct execution (default `120000` / `60000`). |
@@ -200,6 +200,18 @@ export interface DeviceLoginOptions {
200
200
  * that a script may read, and the code block is not it.
201
201
  */
202
202
  export declare function deviceLogin(authUrl: string, opts?: DeviceLoginOptions): Promise<AuthSession | undefined>;
203
+ /**
204
+ * Sign in with a setup token and cache the session.
205
+ *
206
+ * A setup token is a device code the console minted already approved for the
207
+ * person who was signed in there (`POST /auth/device/setup-token`), so this is
208
+ * the browser flow with the waiting removed: one redemption, no polling. It is
209
+ * the secret that opens a session, so it is never printed or logged here, for
210
+ * the same reason a device code never is.
211
+ */
212
+ export declare function tokenLogin(authUrl: string, token: string, opts?: {
213
+ fetchImpl?: typeof fetch;
214
+ }): Promise<AuthSession>;
203
215
  /**
204
216
  * Sign in with an email and a password.
205
217
  *
@@ -517,6 +517,47 @@ export async function deviceLogin(authUrl, opts = {}) {
517
517
  saveCredentials(session);
518
518
  return session;
519
519
  }
520
+ /**
521
+ * Sign in with a setup token and cache the session.
522
+ *
523
+ * A setup token is a device code the console minted already approved for the
524
+ * person who was signed in there (`POST /auth/device/setup-token`), so this is
525
+ * the browser flow with the waiting removed: one redemption, no polling. It is
526
+ * the secret that opens a session, so it is never printed or logged here, for
527
+ * the same reason a device code never is.
528
+ */
529
+ export async function tokenLogin(authUrl, token, opts = {}) {
530
+ const fetchImpl = opts.fetchImpl ?? fetch;
531
+ const res = await fetchImpl(`${authUrl}/auth/device/token`, {
532
+ method: "POST",
533
+ headers: { "content-type": "application/json" },
534
+ body: JSON.stringify({ deviceCode: token.trim() }),
535
+ });
536
+ if (res.status === 404)
537
+ throw new DeviceFlowUnsupported();
538
+ if (res.ok) {
539
+ const session = toSession((await res.json()));
540
+ saveCredentials(session);
541
+ logLine("auth.token_login", { url: authUrl });
542
+ return session;
543
+ }
544
+ const body = (await res.json().catch(() => ({})));
545
+ switch (body.error) {
546
+ case "expired_token":
547
+ throw new DeviceFlowAborted("this setup token has expired or was already used — make a new one on the console's " +
548
+ "Set up the runner page and paste it within ten minutes", "expired_token");
549
+ case "invalid_grant":
550
+ case "authorization_pending":
551
+ case "slow_down":
552
+ case "access_denied":
553
+ // A pending or unknown code is not a setup token: the console only hands
554
+ // out codes that are already approved.
555
+ throw new DeviceFlowAborted("that is not a valid setup token — copy the whole token from the console's " +
556
+ "Set up the runner page", "access_denied");
557
+ default:
558
+ throw new Error(`sign-in failed: ${body.error ?? `HTTP ${res.status}`}`);
559
+ }
560
+ }
520
561
  /**
521
562
  * A name for this machine, shown on the approval page.
522
563
  *
@@ -27,12 +27,27 @@
27
27
  * BIR_REPLAY_ALLOW_SERVERS comma-separated server keys eligible for *direct*
28
28
  * execution. Unset means every wrapped server
29
29
  * BIR_MIN_STEER_SIMILARITY minimum match similarity to replay (default 0.92)
30
- * ANTHROPIC_API_KEY enables parameter derivation. Without it, replay
31
- * uses the scenario's recorded sample values — free,
32
- * and often still correct
30
+ * ANTHROPIC_API_KEY OPTIONAL. Derivation — reading what this turn acts
31
+ * on — is done by the service for a signed-in
32
+ * runner. Set this only to keep that reading on this
33
+ * machine; signed out and unset, a scenario with a
34
+ * target does not run (segmented.md R-PARAM-5)
33
35
  * BIR_DERIVE_MODEL derivation model (default claude-haiku-4-5-…)
34
36
  * BIR_MATCH_BUDGET_MS prompt-hook match wait (default 2500)
35
37
  * BIR_DERIVE_BUDGET_MS first PreToolUse derivation wait (default 8000)
38
+ * BIR_DERIVE_RECENT_RESULTS how many of this prompt's tool results derivation
39
+ * reads, so a target named by an earlier step can be
40
+ * found (default 5; 0 off)
41
+ * BIR_INTENT_SOURCES which text a step's intent may come from
42
+ * (default text,thinking,tool)
43
+ * BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40).
44
+ * Separate from BIR_INTENT_MATCHES_PER_TURN: arming
45
+ * is a claim on the turn, counting hits is not
46
+ * BIR_SEGMENT_ARM 1 lets a handed-out segment actually run mid-task.
47
+ * Unset is observe-only: what would have armed is
48
+ * logged and nothing is replaced
49
+ * BIR_MIN_SEGMENT_STEER_SIMILARITY minimum similarity to steer a segment
50
+ * (default 0.95)
36
51
  * BIR_REPLAY_BUDGET_MS whole-plan ceiling, direct mode (default 120000)
37
52
  * BIR_STEP_TIMEOUT_MS one direct tools/call (default 60000)
38
53
  *
@@ -27,12 +27,27 @@
27
27
  * BIR_REPLAY_ALLOW_SERVERS comma-separated server keys eligible for *direct*
28
28
  * execution. Unset means every wrapped server
29
29
  * BIR_MIN_STEER_SIMILARITY minimum match similarity to replay (default 0.92)
30
- * ANTHROPIC_API_KEY enables parameter derivation. Without it, replay
31
- * uses the scenario's recorded sample values — free,
32
- * and often still correct
30
+ * ANTHROPIC_API_KEY OPTIONAL. Derivation — reading what this turn acts
31
+ * on — is done by the service for a signed-in
32
+ * runner. Set this only to keep that reading on this
33
+ * machine; signed out and unset, a scenario with a
34
+ * target does not run (segmented.md R-PARAM-5)
33
35
  * BIR_DERIVE_MODEL derivation model (default claude-haiku-4-5-…)
34
36
  * BIR_MATCH_BUDGET_MS prompt-hook match wait (default 2500)
35
37
  * BIR_DERIVE_BUDGET_MS first PreToolUse derivation wait (default 8000)
38
+ * BIR_DERIVE_RECENT_RESULTS how many of this prompt's tool results derivation
39
+ * reads, so a target named by an earlier step can be
40
+ * found (default 5; 0 off)
41
+ * BIR_INTENT_SOURCES which text a step's intent may come from
42
+ * (default text,thinking,tool)
43
+ * BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40).
44
+ * Separate from BIR_INTENT_MATCHES_PER_TURN: arming
45
+ * is a claim on the turn, counting hits is not
46
+ * BIR_SEGMENT_ARM 1 lets a handed-out segment actually run mid-task.
47
+ * Unset is observe-only: what would have armed is
48
+ * logged and nothing is replaced
49
+ * BIR_MIN_SEGMENT_STEER_SIMILARITY minimum similarity to steer a segment
50
+ * (default 0.95)
36
51
  * BIR_REPLAY_BUDGET_MS whole-plan ceiling, direct mode (default 120000)
37
52
  * BIR_STEP_TIMEOUT_MS one direct tools/call (default 60000)
38
53
  *
@@ -93,6 +108,29 @@ function positiveInt(value, fallback) {
93
108
  const n = Number(value);
94
109
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
95
110
  }
111
+ /** Like {@link positiveInt}, but 0 is a meaningful setting rather than unset. */
112
+ function nonNegativeInt(value, fallback) {
113
+ const n = Number(value);
114
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
115
+ }
116
+ /**
117
+ * `BIR_INTENT_SOURCES` — which text a step's intent may be read from
118
+ * (segmented.md R-INTENT-3). Unset or unparseable means all three.
119
+ *
120
+ * It governs only what is *sent*. Turning every source off does not stop a
121
+ * probe: the request still goes out with an empty text and the server builds a
122
+ * line from the tool name and arguments (R-HIT-4).
123
+ */
124
+ function parseIntentSources(value) {
125
+ if (value === undefined)
126
+ return undefined;
127
+ const allowed = ["text", "thinking", "tool"];
128
+ const wanted = value
129
+ .split(",")
130
+ .map((s) => s.trim().toLowerCase())
131
+ .filter((s) => allowed.includes(s));
132
+ return new Set(wanted);
133
+ }
96
134
  /**
97
135
  * Assemble the replay configuration (docs/calculatedReplay.md §5.3 of the guide).
98
136
  *
@@ -116,6 +154,25 @@ function buildReplayOptions(auth) {
116
154
  planMs: positiveInt(process.env.BIR_REPLAY_BUDGET_MS, 120_000),
117
155
  stepMs: positiveInt(process.env.BIR_STEP_TIMEOUT_MS, 60_000),
118
156
  },
157
+ // Intent matching in the ReAct loop (fallbk.md §Runner 4). On with replay;
158
+ // BIR_INTENT_MATCH=0 turns it off on its own.
159
+ intentMatch: {
160
+ enabled: process.env.BIR_INTENT_MATCH !== "0",
161
+ // 4 000, not 1 500: a request whose best segment lands in the not-clear
162
+ // band has one live question to ask under the server's own 2 500 ms
163
+ // attempt, and must still answer inside the hook's 30 s (segmented.md
164
+ // R-HIT-11).
165
+ budgetMs: positiveInt(process.env.BIR_INTENT_MATCH_BUDGET_MS, 4_000),
166
+ maxPerTurn: positiveInt(process.env.BIR_INTENT_MATCHES_PER_TURN, 3),
167
+ maxRequestsPerTurn: positiveInt(process.env.BIR_INTENT_REQUESTS_PER_TURN, 40),
168
+ },
169
+ // Observe-only until an operator has read the logs and turned it on
170
+ // (segmented.md R-OUT-10, R-LIFE-7). Arming is never the way to find out.
171
+ segmentArm: process.env.BIR_SEGMENT_ARM === "1",
172
+ minSegmentSimilarity: (() => {
173
+ const n = Number(process.env.BIR_MIN_SEGMENT_STEER_SIMILARITY);
174
+ return Number.isFinite(n) ? n : 0.95;
175
+ })(),
119
176
  authUrl: auth.baseUrl,
120
177
  // Read late, not captured: `RemoteRecorder` refreshes the access token as the
121
178
  // session outlives it, and a snapshot taken here would go stale mid-run.
@@ -127,7 +184,14 @@ function buildReplayOptions(auth) {
127
184
  logLine("replay.enabled", {
128
185
  minSimilarity: opts.minSimilarity,
129
186
  allowServers: allowServers ? [...allowServers].join(",") : "(all wrapped)",
130
- derive: process.env.ANTHROPIC_API_KEY ? "anthropic" : "recorded sample values",
187
+ // Who reads what this turn acts on (segmented.md R-PARAM-5). Named at
188
+ // startup because the third state is a replay that silently never runs.
189
+ derive: process.env.ANTHROPIC_API_KEY
190
+ ? "this machine (ANTHROPIC_API_KEY)"
191
+ : auth.session && auth.baseUrl
192
+ ? "the service"
193
+ : "recorded sample values — sign in, or a scenario with a target will not run",
194
+ intentMatch: opts.intentMatch?.enabled ? "on" : "off",
131
195
  why: "matched prompts will run their calculated scenario — steered steps are auto-approved",
132
196
  });
133
197
  }
@@ -152,6 +216,8 @@ async function main() {
152
216
  host: { app: "claude-code" },
153
217
  correlationDecision: process.env.BIR_CORRELATION_DECISION === "ask" ? "ask" : "allow",
154
218
  noCorrelation: process.env.BIR_NO_CORRELATION === "1",
219
+ deriveRecentResults: nonNegativeInt(process.env.BIR_DERIVE_RECENT_RESULTS, 5),
220
+ intentSources: parseIntentSources(process.env.BIR_INTENT_SOURCES),
155
221
  replay: buildReplayOptions(auth),
156
222
  });
157
223
  const address = await server.listen();
package/dist/bin/bir.js CHANGED
@@ -28,7 +28,7 @@ import { isScenarioServer, isWrapped, PACKAGE_NAME, readSidecar, scenarioEntry,
28
28
  import { isRemote, resolveServers } from "../config/resolve.js";
29
29
  import { buildHooksBlock, claudeCodePaths, fileForScope, installHooks, readTextOrNull, setServerEntry, sha256, uninstallHooks, } from "../config/adapters/claude-code.js";
30
30
  import { readGenericServers, setGenericServerEntry } from "../config/adapters/generic.js";
31
- import { AUTH_URL_HINT, DeviceFlowAborted, DeviceFlowUnsupported, authenticate, deviceLogin, describeAuthService, legacyPasswordLogin, logout, normalizeAuthUrl, resolveAuthUrl, } from "../auth/client.js";
31
+ import { AUTH_URL_HINT, DeviceFlowAborted, DeviceFlowUnsupported, authenticate, deviceLogin, describeAuthService, legacyPasswordLogin, tokenLogin, logout, normalizeAuthUrl, resolveAuthUrl, } from "../auth/client.js";
32
32
  import { DEFAULT_CONTROL_PORT } from "../control/server.js";
33
33
  import { errText } from "../util/log.js";
34
34
  import { packageVersion } from "../util/version.js";
@@ -67,6 +67,7 @@ Options:
67
67
  --json machine-readable output for status / doctor
68
68
  --dry replay against recorded outputs only; run no real tools
69
69
  --no-browser login: print the link and code, open nothing (SSH, headless)
70
+ --token <value> login: redeem a one-time setup token from the console (no browser)
70
71
  --password login: use the old email/password prompt (deprecated)`);
71
72
  process.exit(code);
72
73
  }
@@ -115,6 +116,9 @@ function parseArgs(argv) {
115
116
  case "--no-browser":
116
117
  args.browser = false;
117
118
  break;
119
+ case "--token":
120
+ args.token = argv[++i] ?? "";
121
+ break;
118
122
  case "--config":
119
123
  args.configPath = argv[++i];
120
124
  break;
@@ -540,6 +544,20 @@ async function doctor(args) {
540
544
  else {
541
545
  notes.push(`recording to ${String(health.authUrl ?? "the configured BaseIn service")}`);
542
546
  }
547
+ // Whether a recurring sub-task the service found may actually run mid-task
548
+ // (segmented.md R-LIFE-8). Observe-only is the default and must be visible
549
+ // without reading a log: an operator turns arming on by reading what *would*
550
+ // have armed, and needs to know which of the two states this machine is in.
551
+ notes.push(health.segmentArm === true
552
+ ? "recurring sub-tasks MAY run mid-task (BIR_SEGMENT_ARM=1)"
553
+ : "recurring sub-tasks are observed only — nothing is replaced (BIR_SEGMENT_ARM unset)");
554
+ const sessions = health.sessions ?? [];
555
+ const probes = sessions.reduce((n, s) => n + (s.intent?.probes ?? 0), 0);
556
+ const armed = sessions.reduce((n, s) => n + (s.intent?.armed ?? 0), 0);
557
+ if (sessions.length > 0) {
558
+ notes.push(`mid-turn matching this session: ${probes} probe${probes === 1 ? "" : "s"}, ` +
559
+ `${armed} armed`);
560
+ }
543
561
  }
544
562
  else if (!resolveAuthUrl()) {
545
563
  // No control server to ask, and nothing in this shell either. That is a
@@ -582,7 +600,20 @@ async function doctor(args) {
582
600
  if (replay?.enabled) {
583
601
  out(`Replay : ON servers=${replay.allowServers?.join(",") || "(all wrapped)"} ` +
584
602
  `minSimilarity=${replay.minSimilarity ?? "?"}`);
585
- out(` derive=${replay.deriveKey ? "anthropic" : "recorded sample values (no ANTHROPIC_API_KEY)"}`);
603
+ // An older control server sends only `deriveKey`; read it as the two states
604
+ // it could describe then.
605
+ const via = replay.deriveVia ?? (replay.deriveKey ? "key" : "samples");
606
+ const derive = via === "key"
607
+ ? "this machine (ANTHROPIC_API_KEY)"
608
+ : via === "service"
609
+ ? "the service (no key needed here)"
610
+ : "recorded sample values — scenarios with a target will NOT run";
611
+ out(` derive=${derive}`);
612
+ if (via === "samples") {
613
+ notes.push("nothing can read what this turn acts on: sign in with `bir login` so the " +
614
+ "service derives, or set ANTHROPIC_API_KEY to derive here. Until then a " +
615
+ "matched scenario with a target is declined and the agent does the task");
616
+ }
586
617
  const idle = wantWrapped.filter((n) => !(replay.pollingProxies ?? []).includes(n));
587
618
  if (idle.length > 0) {
588
619
  notes.push(`these proxies are not polling for replay work: ${idle.join(", ")} — ` +
@@ -902,7 +933,29 @@ async function main() {
902
933
  return 1;
903
934
  }
904
935
  let session;
905
- if (args.password) {
936
+ if (args.token !== undefined) {
937
+ // A setup token from the console: already approved for one account, so
938
+ // there is no page to open and nothing to wait for.
939
+ if (!args.token.trim()) {
940
+ process.stderr.write("[bir] --token needs the token itself: bir login --token <token>\n");
941
+ return 1;
942
+ }
943
+ try {
944
+ session = await tokenLogin(baseUrl, args.token);
945
+ }
946
+ catch (err) {
947
+ if (err instanceof DeviceFlowAborted) {
948
+ process.stderr.write(`[bir] ${err.message}\n`);
949
+ return 1;
950
+ }
951
+ if (err instanceof DeviceFlowUnsupported) {
952
+ process.stderr.write("[bir] this service does not offer setup tokens; run `bir login` without --token.\n");
953
+ return 1;
954
+ }
955
+ throw err;
956
+ }
957
+ }
958
+ else if (args.password) {
906
959
  process.stderr.write("[bir] --password is deprecated and will be removed in the next release. " +
907
960
  "Browser sign-in works for every account, including Google-only ones.\n");
908
961
  session = await legacyPasswordLogin(baseUrl);
@@ -23,6 +23,7 @@
23
23
  * token on every route, and never echoes the token back. A loopback port that
24
24
  * accepts unauthenticated step reports is a local exfiltration channel.
25
25
  */
26
+ import { type IntentSource } from "./transcript.js";
26
27
  import { type Recorder } from "../record/recorder.js";
27
28
  import { type ReplayOptions } from "../replay/controller.js";
28
29
  /** The subset of the Claude Code hook payload we consume (all optional, all defensive). */
@@ -41,6 +42,13 @@ export interface HookPayload {
41
42
  }
42
43
  export interface ControlServerOptions {
43
44
  recorder: Recorder;
45
+ /**
46
+ * Tool results of the current prompt handed to derivation
47
+ * (`BIR_DERIVE_RECENT_RESULTS`, segmented.md R-PARAM-2). Default 5; 0 off.
48
+ */
49
+ deriveRecentResults?: number;
50
+ /** `BIR_INTENT_SOURCES` — which intent rungs may be sent (R-INTENT-3). */
51
+ intentSources?: ReadonlySet<IntentSource>;
44
52
  /** Session working directory — keys the discovery file. */
45
53
  cwd: string;
46
54
  /** Preferred port; 0 (or a busy port) falls back to an ephemeral one. */
@@ -107,6 +115,18 @@ export declare class ControlServer {
107
115
  private readonly pendingSeals;
108
116
  /** Calculated replay. Inert unless `opts.replay.enabled`. */
109
117
  private readonly replay;
118
+ /**
119
+ * How many of this prompt's tool results derivation may read
120
+ * (`BIR_DERIVE_RECENT_RESULTS`, segmented.md R-PARAM-2). A target is often
121
+ * named by an earlier step's output and never by the prompt.
122
+ */
123
+ private readonly deriveRecentResults;
124
+ /**
125
+ * Which rungs of R-INTENT-1 the runner may send (`BIR_INTENT_SOURCES`). It
126
+ * governs only what is *sent*: with every source off, the probe still goes out
127
+ * with an empty text and the server builds the tool line (R-HIT-4).
128
+ */
129
+ private readonly intentSources;
110
130
  constructor(opts: ControlServerOptions);
111
131
  listen(): Promise<ControlServerAddress>;
112
132
  close(): Promise<void>;
@@ -167,6 +187,8 @@ export declare class ControlServer {
167
187
  * false` and zero rather than a number that is confidently wrong.
168
188
  */
169
189
  private runCost;
190
+ /** Transcript cost since `mark`; unmeasured (and zero) without one. */
191
+ private costSince;
170
192
  private reportExecution;
171
193
  /** Seal the run *and* wait for everything queued to reach the service. */
172
194
  private finalizeRun;
@@ -183,6 +205,67 @@ export declare class ControlServer {
183
205
  * record `tool_selected`. The hook is this step's only observer.
184
206
  */
185
207
  private onToolPre;
208
+ /** A controller decision as a `PreToolUse` answer, or undefined to carry on. */
209
+ private preToolAnswer;
210
+ /**
211
+ * Recordings this turn must not count a step hit against (segmented.md
212
+ * R-HIT-7).
213
+ *
214
+ * A repeat of a recording's own prompt is *prompt* recurrence, counted as
215
+ * `iterations`, never step recurrence. Without this, three prompt repeats
216
+ * would reach `SEGMENT_MIN_HITS` on every step of the whole recording and the
217
+ * detector would build a whole-run-sized segment beside the whole-run
218
+ * scenario.
219
+ */
220
+ private excludeRunsFor;
221
+ /**
222
+ * Per recording a plan of this turn came from, how far that plan got
223
+ * (segmented.md R-OUT-6).
224
+ *
225
+ * The server uses it to refuse a segment that starts *before* where this turn
226
+ * already is: re-running steps the turn has just done is worse than not
227
+ * replaying at all. The largest position per recording wins, and a plan
228
+ * declined for anything but a known-bad first step reports nothing — it ran
229
+ * none of the recording, so it blocks none of it.
230
+ *
231
+ * Rebuilt on every probe rather than cached: a plan that hands over between
232
+ * two calls changes the answer.
233
+ */
234
+ private ranThroughFor;
235
+ /**
236
+ * Intent matching in the ReAct loop (fallbk.md §Runner 4, segmented.md 10.3).
237
+ *
238
+ * Called only while the model is driving. Every unsteered, non-housekeeping
239
+ * tool call is sent: the service counts it as a hit on whatever recorded step
240
+ * it matches, and may hand back a scenario or a segment to run in its place.
241
+ *
242
+ * A call made with **no reasoning at all** still probes (R-HIT-4): the server
243
+ * builds an intent from the tool name and arguments, and a step the agent took
244
+ * without narrating it is exactly as much a recurrence as one it explained.
245
+ * Every failure is a miss.
246
+ */
247
+ private tryIntentMatch;
248
+ /**
249
+ * Schedule a fragment once a matched turn's plan has handed over (fallbk.md
250
+ * D7). Only a turn that is not already recording needs one: an unmatched turn
251
+ * whose intent-armed plan handed over is recording the model's work anyway.
252
+ */
253
+ private noteHandover;
254
+ /**
255
+ * Open the fragment run and record from this call on (fallbk.md D7).
256
+ *
257
+ * The run id is swapped in place: steps allocated before belonged to a turn
258
+ * the service never created a run for, and nothing of theirs may land in the
259
+ * fragment — so pending correlations are latched and built-ins forgotten.
260
+ */
261
+ private openFragment;
262
+ /** Resolve with `p`, or null once `ms` passes or it rejects. */
263
+ private withinBudget;
264
+ /**
265
+ * `PostToolUse`, and where a hand-over noticed while threading this call's
266
+ * output reaches the model at once, as `additionalContext` (fallbk.md D4).
267
+ */
268
+ private onToolPost;
186
269
  /**
187
270
  * `PostToolUse` / `PostToolUseFailure`. For a built-in this closes the pair.
188
271
  * For a correlated MCP call it normally does nothing — the proxy owns that
@@ -190,7 +273,7 @@ export declare class ControlServer {
190
273
  * or the call never reached it), the hook's own view is recorded after
191
274
  * {@link PROXY_REPORT_GRACE_MS} rather than the step being lost entirely.
192
275
  */
193
- private onToolPost;
276
+ private toolPost;
194
277
  private errorText;
195
278
  /**
196
279
  * `SubagentStart` / `SubagentStop`. These carry `agent_id`, which is the only