@basein/runner 0.2.12 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +540 -369
  2. package/dist/auth/client.d.ts +65 -3
  3. package/dist/auth/client.js +305 -53
  4. package/dist/bin/bir-hooks.d.ts +72 -31
  5. package/dist/bin/bir-hooks.js +176 -284
  6. package/dist/bin/bir-proxy.d.ts +17 -2
  7. package/dist/bin/bir-proxy.js +222 -28
  8. package/dist/bin/bir-scenario.d.ts +61 -21
  9. package/dist/bin/bir-scenario.js +593 -122
  10. package/dist/bin/bir.d.ts +12 -6
  11. package/dist/bin/bir.js +2935 -489
  12. package/dist/bin/investigate.js +40 -15
  13. package/dist/bin/scenario-edit.d.ts +7 -1
  14. package/dist/bin/scenario-edit.js +43 -21
  15. package/dist/bin/scenario-liveness.d.ts +194 -0
  16. package/dist/bin/scenario-liveness.js +459 -0
  17. package/dist/bin/scenario-stderr.d.ts +43 -0
  18. package/dist/bin/scenario-stderr.js +76 -0
  19. package/dist/bin/service-read.d.ts +88 -0
  20. package/dist/bin/service-read.js +128 -0
  21. package/dist/bin/setup.d.ts +19 -0
  22. package/dist/bin/setup.js +146 -20
  23. package/dist/config/adapters/claude-code.d.ts +92 -14
  24. package/dist/config/adapters/claude-code.js +291 -101
  25. package/dist/config/adapters/generic.d.ts +4 -1
  26. package/dist/config/adapters/generic.js +13 -14
  27. package/dist/config/generate.d.ts +392 -14
  28. package/dist/config/generate.js +666 -19
  29. package/dist/config/mcp-approval.d.ts +76 -0
  30. package/dist/config/mcp-approval.js +210 -0
  31. package/dist/config/node-path.d.ts +42 -0
  32. package/dist/config/node-path.js +102 -0
  33. package/dist/config/overrides.d.ts +62 -0
  34. package/dist/config/overrides.js +136 -0
  35. package/dist/config/resolve.d.ts +70 -8
  36. package/dist/config/resolve.js +186 -15
  37. package/dist/control/client.d.ts +22 -0
  38. package/dist/control/client.js +26 -7
  39. package/dist/control/correlation.d.ts +23 -5
  40. package/dist/control/correlation.js +0 -0
  41. package/dist/control/daemon.d.ts +400 -18
  42. package/dist/control/daemon.js +1105 -102
  43. package/dist/control/discovery.d.ts +77 -5
  44. package/dist/control/discovery.js +186 -11
  45. package/dist/control/ensure-hook.d.ts +61 -0
  46. package/dist/control/ensure-hook.js +160 -8
  47. package/dist/control/paths.d.ts +26 -0
  48. package/dist/control/paths.js +34 -0
  49. package/dist/control/pin-guard.d.ts +83 -0
  50. package/dist/control/pin-guard.js +534 -0
  51. package/dist/control/recorder-keeper.d.ts +229 -0
  52. package/dist/control/recorder-keeper.js +721 -0
  53. package/dist/control/recorder-main.d.ts +73 -0
  54. package/dist/control/recorder-main.js +778 -0
  55. package/dist/control/server.d.ts +707 -31
  56. package/dist/control/server.js +2786 -302
  57. package/dist/control/transcript.d.ts +21 -1
  58. package/dist/control/transcript.js +378 -112
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +5 -1
  61. package/dist/jsonrpc/framing.d.ts +76 -12
  62. package/dist/jsonrpc/framing.js +216 -59
  63. package/dist/proxy/intercept.d.ts +49 -5
  64. package/dist/proxy/intercept.js +188 -59
  65. package/dist/proxy/relay.d.ts +16 -0
  66. package/dist/proxy/relay.js +77 -5
  67. package/dist/proxy/revive.d.ts +122 -0
  68. package/dist/proxy/revive.js +251 -0
  69. package/dist/proxy/session.d.ts +222 -14
  70. package/dist/proxy/session.js +708 -99
  71. package/dist/record/recorder.d.ts +51 -3
  72. package/dist/record/recorder.js +3 -0
  73. package/dist/record/redact.d.ts +103 -9
  74. package/dist/record/redact.js +659 -30
  75. package/dist/record/remote-recorder.d.ts +327 -32
  76. package/dist/record/remote-recorder.js +1517 -187
  77. package/dist/record/switchable-recorder.d.ts +126 -0
  78. package/dist/record/switchable-recorder.js +351 -0
  79. package/dist/record/truncate.d.ts +40 -1
  80. package/dist/record/truncate.js +111 -4
  81. package/dist/replay/bash-channel.d.ts +35 -0
  82. package/dist/replay/bash-channel.js +133 -0
  83. package/dist/replay/bundle.d.ts +10 -0
  84. package/dist/replay/bundle.js +10 -0
  85. package/dist/replay/controller.d.ts +369 -20
  86. package/dist/replay/controller.js +1176 -107
  87. package/dist/replay/derive.d.ts +1 -0
  88. package/dist/replay/derive.js +81 -24
  89. package/dist/replay/executor.d.ts +51 -16
  90. package/dist/replay/executor.js +114 -41
  91. package/dist/replay/handover.d.ts +54 -1
  92. package/dist/replay/handover.js +55 -5
  93. package/dist/replay/logic.d.ts +62 -10
  94. package/dist/replay/logic.js +140 -22
  95. package/dist/replay/plan.d.ts +201 -27
  96. package/dist/replay/plan.js +581 -141
  97. package/dist/replay/safety.d.ts +72 -0
  98. package/dist/replay/safety.js +303 -0
  99. package/dist/replay/source-run.d.ts +10 -0
  100. package/dist/replay/source-run.js +80 -35
  101. package/dist/replay/targets.d.ts +15 -1
  102. package/dist/replay/targets.js +62 -4
  103. package/dist/replay/tool-error.d.ts +15 -1
  104. package/dist/replay/tool-error.js +33 -1
  105. package/dist/replay/types.d.ts +7 -4
  106. package/dist/upstream/client.d.ts +56 -3
  107. package/dist/upstream/client.js +61 -4
  108. package/dist/upstream/headers.d.ts +43 -0
  109. package/dist/upstream/headers.js +90 -0
  110. package/dist/upstream/http-client.d.ts +213 -22
  111. package/dist/upstream/http-client.js +1034 -168
  112. package/dist/upstream/http-transport.d.ts +143 -0
  113. package/dist/upstream/http-transport.js +391 -0
  114. package/dist/upstream/lazy-client.d.ts +8 -0
  115. package/dist/upstream/lazy-client.js +29 -1
  116. package/dist/upstream/sse.d.ts +78 -0
  117. package/dist/upstream/sse.js +189 -0
  118. package/dist/upstream/stdio-client.d.ts +61 -1
  119. package/dist/upstream/stdio-client.js +356 -33
  120. package/dist/util/atomic-write.d.ts +29 -0
  121. package/dist/util/atomic-write.js +96 -0
  122. package/dist/util/journal.d.ts +15 -0
  123. package/dist/util/journal.js +48 -3
  124. package/dist/util/log.d.ts +10 -0
  125. package/dist/util/log.js +28 -1
  126. package/dist/util/off-switch.d.ts +46 -0
  127. package/dist/util/off-switch.js +91 -0
  128. package/dist/util/service-health.d.ts +133 -0
  129. package/dist/util/service-health.js +217 -0
  130. package/docs/BaseInstRunner.md +694 -621
  131. package/docs/calculatedReplay.md +1625 -1304
  132. package/docs/calculatedReplayGuide.md +1212 -1087
  133. package/docs/installRun.md +1013 -772
  134. package/docs/loginWeb.md +627 -607
  135. package/docs/mcpmark.md +49 -16
  136. package/docs/my-first-sample.md +569 -545
  137. package/docs/quickstart.md +384 -311
  138. package/docs/t-bench.md +406 -394
  139. package/package.json +58 -57
  140. package/production.md +1371 -0
  141. package/scripts/install.ps1 +97 -6
  142. package/scripts/install.sh +104 -7
@@ -8,55 +8,181 @@
8
8
  * PATCH /recordings/runs/:id/steps/:stepId
9
9
  * POST /recordings/runs/:id/finish
10
10
  *
11
- * Four properties matter, and each is load-bearing:
11
+ * The properties that matter, each load-bearing:
12
12
  * - **Client-generated ids** (`run_`/`step_` uuids) so callers get an id
13
13
  * synchronously and never await the network on the hot path.
14
- * - **One ordered promise chain per run**, so steps arrive in the order they
15
- * were recorded even though every send is fire-and-forget.
16
- * - **Best-effort.** A failed send is logged and dropped, never retried inline
17
- * and never thrown at the caller (§10).
18
- * - **One token refresh on 401**, then one retry. Sessions outlive access tokens.
14
+ * - **One ordered lane per run**, so steps arrive in the order they were
15
+ * recorded even though every send is fire-and-forget.
16
+ * - **Best-effort, and invisible when the service is not there.** A failed
17
+ * send is logged once per run and dropped, never thrown at the caller
18
+ * (§10). Every request has a deadline (`BIR_SERVICE_TIMEOUT_MS`), every
19
+ * answer or its absence is reported to the process-wide health verdict
20
+ * ({@link serviceHealth}), and while that verdict says the service is
21
+ * cooling down the calls a hook waits on — `getMatch`, `runCreated`,
22
+ * `matchIntent` — answer "no match" at once instead of making the user wait,
23
+ * and `flush` returns at once, leaving the rest to drain in the background.
24
+ * A run's create that fails its first attempt answers "no match" at once
25
+ * too; its retry is for the recording, never for a hook.
26
+ * - **One probe, not a stampede.** While the breaker is half-open, one
27
+ * queued send finds out for all the lanes; the rest wait for its verdict
28
+ * and then follow it a moment apart, not all at once.
29
+ * - **Bounded.** The lanes are the real backlog (the hook server's step queue
30
+ * drains instantly into them), so they carry the bound: past
31
+ * {@link DEFAULT_MAX_PENDING_SENDS} sends or {@link DEFAULT_MAX_PENDING_BYTES}
32
+ * the oldest unsent *step* is dropped and counted — creates and finishes go
33
+ * last, so the service is not left with runs that never end.
34
+ * - **One token refresh on 401**, shared by every send that meets the same
35
+ * expiry, then one retry. Sessions outlive access tokens.
36
+ * - **It follows credentials.json.** `bir logout` removes the file; a
37
+ * recorder that loaded its session from it stops sending within seconds,
38
+ * rather than recording for the rest of the access token's life.
39
+ * - **It redacts and caps every free-text field it sends** — the prompt, the
40
+ * answer, reasoning, errors — as the last line of defence. Structured tool
41
+ * payloads are redacted by their producers; text is only ever seen here.
19
42
  *
20
43
  * SIMILAR-PROMPT HITS. `POST /recordings/runs` may answer `{ id: null, matched }`,
21
44
  * meaning the server recognised the prompt and did **not** create a run. Every
22
45
  * later step post for that id would then 404, so callers must stop recording —
23
- * {@link RemoteRecorder.getMatch} is how they find out. v1 only stops; replay is v2.
46
+ * {@link RemoteRecorder.getMatch} is how they find out — and this recorder skips
47
+ * them itself too, for the caller that stopped waiting before the answer came.
24
48
  */
25
49
  import { type AuthSession } from "../auth/client.js";
26
- import type { ExecutionReport, IntentMatchAnswer, IntentMatchRequest, Recorder, RunCreation, RunMatch, RunMetrics, StartRunOptions } from "./recorder.js";
50
+ import { type ServiceHealth } from "../util/service-health.js";
51
+ import type { ExecutionReport, FlushOptions, IntentMatchAnswer, IntentMatchOptions, IntentMatchRequest, Recorder, RecorderHealth, RecorderHealthReporter, RunCreation, RunMatch, RunMetrics, StartRunOptions } from "./recorder.js";
52
+ /**
53
+ * Caps for the free text this recorder sends, in UTF-8 bytes. Tool payloads
54
+ * arrive capped at 64 KiB by their producers; text never passed through one,
55
+ * so a pasted 3 MB log used to go out whole — once as the run's input, again
56
+ * in the execution report — and spend the prompt's match budget uploading.
57
+ */
58
+ export declare const TEXT_CAPS: {
59
+ /** The prompt: the run's `input`, and an execution report's `prompt`. */
60
+ readonly prompt: number;
61
+ /** The final answer, and the run's `finalOutput`. */
62
+ readonly answer: number;
63
+ /** A step's written reasoning. */
64
+ readonly context: number;
65
+ /** A step's thinking text, sent only to be embedded. */
66
+ readonly intent: number;
67
+ /** A tool's error text when it is not JSON. */
68
+ readonly toolError: number;
69
+ /** An execution report's error lines, rendered in a web page. */
70
+ readonly reportError: number;
71
+ };
72
+ /** Queued sends past which the oldest unsent step is dropped (`BIR_RECORDER_MAX_PENDING`). */
73
+ export declare const DEFAULT_MAX_PENDING_SENDS = 500;
74
+ /** Queued bytes past which the same happens (`BIR_RECORDER_MAX_PENDING_BYTES`). */
75
+ export declare const DEFAULT_MAX_PENDING_BYTES: number;
76
+ /** How long a probe waits when its caller did not say. Its answer is useless after a hook's budget. */
77
+ export declare const PROBE_TIMEOUT_MS = 5000;
78
+ /**
79
+ * How long {@link RemoteRecorder.flush} waits when its caller did not say
80
+ * (`BIR_FLUSH_TIMEOUT_MS`). Every caller is a hook or a shutdown, and a
81
+ * healthy service drains a turn's last few sends in well under a second.
82
+ */
83
+ export declare const DEFAULT_FLUSH_TIMEOUT_MS = 2000;
84
+ /** A send that did not succeed, with the status the service answered, if it answered. */
85
+ export declare class ServiceError extends Error {
86
+ readonly status?: number | undefined;
87
+ constructor(message: string, status?: number | undefined);
88
+ }
27
89
  export interface RemoteRecorderOptions {
28
90
  baseUrl: string;
29
91
  session: AuthSession;
30
92
  /** Value for the run's `framework` column. Defaults to `baseinstrunner`. */
31
93
  framework?: string;
94
+ /** The health verdict to report to. Defaults to the process-wide one; tests inject their own. */
95
+ health?: ServiceHealth;
96
+ /** Backlog bound, in sends. Defaults to `BIR_RECORDER_MAX_PENDING` or 500. */
97
+ maxPendingSends?: number;
98
+ /** Backlog bound, in bytes. Defaults to `BIR_RECORDER_MAX_PENDING_BYTES` or 32 MiB. */
99
+ maxPendingBytes?: number;
100
+ /** How often credentials.json is looked at. Tests shorten it. */
101
+ credentialsCheckMs?: number;
102
+ /** How long a refused refresh waits for a sibling's session before pausing. Tests shorten it. */
103
+ authRecheckMs?: number;
104
+ /** While paused as `auth_rejected`, how often the refresh is tried again. Tests shorten it. */
105
+ authRetryMs?: number;
106
+ /** The longest one queued send waits for a failing service ({@link COOLDOWN_WAIT_MAX_MS}). Tests shorten it. */
107
+ cooldownWaitMaxMs?: number;
32
108
  }
33
- export declare class RemoteRecorder implements Recorder {
109
+ export declare class RemoteRecorder implements Recorder, RecorderHealthReporter {
34
110
  private readonly baseUrl;
35
111
  private readonly framework;
112
+ private readonly health;
113
+ /** The service these credentials are bound to; never changes for this recorder. */
114
+ private readonly binding;
115
+ private readonly maxPendingSends;
116
+ private readonly maxPendingBytes;
117
+ private readonly credentialsCheckMs;
36
118
  private session;
37
- /** runId → tail of that run's ordered send chain. */
38
- private readonly chains;
39
- /** runId → the similar-prompt outcome of its create call. */
40
- private readonly matches;
41
- /** runId → what the create call said (not created: a hit, nothing to record). */
42
- private readonly created;
119
+ private state;
120
+ private readonly runs;
121
+ private readonly lanes;
122
+ private seq;
123
+ private pendingSends;
124
+ private pendingBytes;
125
+ private droppedSends;
126
+ private failedSends;
127
+ private backlogFull;
128
+ private droppedWhileFull;
129
+ private lastSweep;
130
+ private refreshing?;
131
+ /** A failed refresh is not tried again before this (epoch ms). */
132
+ private refreshRetryAt;
133
+ private readonly authRecheckMs;
134
+ private readonly authRetryMs;
135
+ private readonly cooldownWaitMaxMs;
136
+ /** When the session was last refused or retried while refused. */
137
+ private rejectedAt;
138
+ private lastAheadAttempt;
43
139
  /**
44
- * Execution reports ride their own chain: a matched turn has no run on the
45
- * service (that is what a match means), so there is no run chain to append to.
140
+ * The one request out while the breaker is half-open — a lane's own send,
141
+ * or a hook-requested `GET /health` — and when it started.
46
142
  */
47
- private readonly executionChainKey;
143
+ private probe?;
144
+ private matchIntentOffUntil;
145
+ /** True once this recorder's session is known to be the one in credentials.json. */
146
+ private follow;
147
+ private credMtime;
148
+ private credCheckedAt;
48
149
  constructor(opts: RemoteRecorderOptions);
150
+ /**
151
+ * The access token this recorder is using *now*. It changes on every refresh
152
+ * (and when a sibling process's refresh is adopted), so callers that need the
153
+ * token — replay's service derivation, the source-run fetch — must read it
154
+ * through this each time rather than keep the startup session. Undefined once
155
+ * the session is gone (`bir logout`) or was refused.
156
+ */
157
+ currentAccessToken(): string | undefined;
158
+ /** The backlog and sending state, for `/health` and `bir doctor`. */
159
+ recorderHealth(): RecorderHealth;
49
160
  startRun(input: string, metadata?: Record<string, unknown>, options?: StartRunOptions): string;
50
- /** What the create call said. "Created" for an id this recorder never opened. */
161
+ /**
162
+ * What the create call said. "Created" for an id this recorder never opened,
163
+ * and — without waiting — while the service is cooling down or the session is
164
+ * gone, or once the create's first attempt has failed: a hook must never wait
165
+ * on a service that just failed to answer, nor on the retry after it.
166
+ */
51
167
  runCreated(runId: string): Promise<RunCreation>;
52
168
  /**
53
169
  * Match a ReAct iteration's reasoning against the caller's scenario intents
54
170
  * (fallbk.md §Runner 4). Awaited, not queued: the caller is a `PreToolUse`
55
171
  * holding a budget, and the answer decides what that hook says. A service that
56
- * predates the route (404) or any failure is a miss.
172
+ * predates the route (404) or any failure is a miss — and so is a service in
173
+ * a cool-down, without a request: the probe runs on every tool call, so a
174
+ * dead service must cost it nothing.
175
+ *
176
+ * `opts.signal`/`opts.timeoutMs` end the request when the caller stops
177
+ * waiting, so an abandoned probe does not hold a socket for minutes.
178
+ */
179
+ matchIntent(req: IntentMatchRequest, opts?: IntentMatchOptions): Promise<IntentMatchAnswer | null>;
180
+ /**
181
+ * The similar-prompt match for a run's create call (null = fresh run). Like
182
+ * {@link runCreated}, answers null at once while the service is cooling down,
183
+ * and as soon as the create's first attempt fails — the first prompt of an
184
+ * outage is not held for the retry's sleep (up to 2 s behind a Retry-After).
57
185
  */
58
- matchIntent(req: IntentMatchRequest): Promise<IntentMatchAnswer | null>;
59
- /** Await the similar-prompt match for a run's create call (null = fresh run). */
60
186
  getMatch(runId: string): Promise<RunMatch | null>;
61
187
  recordToolSelected(runId: string, stepIndex: number, data: {
62
188
  toolName: string;
@@ -84,28 +210,161 @@ export declare class RemoteRecorder implements Recorder {
84
210
  /**
85
211
  * Report what a matched turn cost (docs/calculatedReplay.md §11.2).
86
212
  *
87
- * Queued on the run's own chain so it lands after that run's last step and so
88
- * `flush` waits for it — a report is the last thing a matched turn produces.
89
- * Best-effort like everything else here: an unredeemed ticket leaves no
90
- * execution row at all, which loses a saving but can never invent one.
213
+ * Queued on the executions lane — a matched turn has no run on the service —
214
+ * so `flush` waits for it. Best-effort like everything else here: an
215
+ * unredeemed ticket leaves no execution row at all, which loses a saving but
216
+ * can never invent one.
91
217
  *
92
218
  * A doubled report (a retry, `Stop` racing `SessionEnd`) is safe by
93
219
  * construction: the ticket *is* the row's id, so the service books once and
94
- * answers `200 { duplicate: true }`.
220
+ * answers `200 { duplicate: true }`. That is also why a report *with* a
221
+ * ticket is the one kind retried after an ambiguous failure.
95
222
  */
96
223
  reportExecution(report: ExecutionReport): void;
97
- /** Await every queued send for a run, including its execution report. */
98
- flush(runId: string): Promise<void>;
99
- /** Append to this run's ordered chain; failures are logged, not thrown. */
224
+ /**
225
+ * Await every queued send for a run, including its execution report — for at
226
+ * most `opts.timeoutMs` (default {@link DEFAULT_FLUSH_TIMEOUT_MS}, or
227
+ * `BIR_FLUSH_TIMEOUT_MS`), and not at all while the service is cooling down.
228
+ * Either way the sends carry on in the background; a flush behind a dead
229
+ * service must never be what holds a hook or a shutdown.
230
+ *
231
+ * WHY THE BREAKER ENDS THE WAIT. In the common outages — offline, refused,
232
+ * a 5xx — the first send fails in milliseconds and opens the breaker, and
233
+ * every job after it sleeps out the cool-down before it tries (the service
234
+ * is not hammered). A flush that waited for those sleeps paid its whole
235
+ * deadline on every turn of the outage, where the old fail-at-once chain
236
+ * paid tens of milliseconds. Nothing a cooling-down lane holds can land
237
+ * sooner than the cool-down, so the flush stops waiting the moment the
238
+ * breaker opens.
239
+ */
240
+ flush(runId: string, opts?: FlushOptions): Promise<void>;
241
+ private newEntry;
242
+ /** The entry for a run, made on first sight for an id this recorder did not open. */
243
+ private entryFor;
244
+ private enqueueRun;
245
+ /** Append to a lane. Synchronous and never throws; the lane drains after the caller returns. */
100
246
  private enqueue;
247
+ private drain;
248
+ private laneIsIdle;
249
+ /**
250
+ * Wait until every lane in `keys` is idle, the breaker opens ("cooling"), or
251
+ * `timeoutMs` passes — whichever is first. The breaker is polled (every
252
+ * {@link FLUSH_POLL_MS}) rather than subscribed to: the verdict is shared by
253
+ * the whole process and has no listeners, and a poll only runs while a flush
254
+ * is actually waiting.
255
+ */
256
+ private waitForLanes;
257
+ /** Run one job: decide whether it may still go, wait out a cool-down, send. Never throws. */
258
+ private execute;
259
+ private whyNotSend;
260
+ /**
261
+ * A job queued under one account, about to go out after another signed in:
262
+ * its run is closed (the rest would split it across accounts), and a report
263
+ * is dropped with a line. Returns the reason to retire it with.
264
+ */
265
+ private accountChanged;
266
+ /** A job leaves without being sent: counted (unless it was never meant to go), and a create settles. */
267
+ private retire;
268
+ /** Skip everything left for a run, once: its sends would all fail the same way. */
269
+ private markDead;
270
+ private onSendFailed;
271
+ /** At a run's finish: say what did not arrive, if anything did not. */
272
+ private summarize;
273
+ /** Keep the backlog inside its bound: drop the oldest unsent step first. */
274
+ private enforceBound;
275
+ /**
276
+ * The oldest unsent step or patch; failing that, the oldest job of any kind.
277
+ * Steps first because a run without its middle is still a run, and one
278
+ * without its create or finish is a run the service can never close.
279
+ */
280
+ private pickVictim;
281
+ private noteBacklogRecovered;
282
+ /** Forget runs nobody will ask about again, and release matches past their TTL. */
283
+ private sweep;
284
+ /** The breaker is closed: no failure since the last answer. */
285
+ private healthyNow;
286
+ /**
287
+ * May a hook wait on the service right now? Only when it is known to be
288
+ * answering. A hook is never the half-open probe: once the cool-down ends a
289
+ * background `GET /health` (or a queued send) finds out, and the next hook
290
+ * waits again.
291
+ */
292
+ private mayHotWait;
293
+ private probeSoon;
294
+ /** Take the half-open slot. The returned function gives it back, once. */
295
+ private beginProbe;
296
+ /**
297
+ * Is a probe out? One older than any request could take (every attempt has
298
+ * a deadline) is a slot somebody failed to give back, and is not honoured.
299
+ */
300
+ private probeInFlight;
301
+ /** Lanes let go after a successful probe spread over a fraction of the cool-down (at most 2 s). */
302
+ private releaseJitterMs;
303
+ /**
304
+ * Wait until a background send may go.
305
+ *
306
+ * - The breaker is closed: go at once.
307
+ * - It is cooling down: sleep until it ends. A send that has waited
308
+ * {@link COOLDOWN_WAIT_MAX_MS} and still faces more than
309
+ * {@link COOLDOWN_GRACE_MS} of it is given up (`giveUp`), unsent.
310
+ * - It is half-open: ONE lane takes the probe slot and goes (`endProbe`
311
+ * gives the slot back once its send settles); every other lane waits for
312
+ * that verdict. A success lets them go, each after its own short random
313
+ * pause; a failure starts a longer cool-down, and they wait that out.
314
+ *
315
+ * Before this, every lane with a queued send woke at the same moment and
316
+ * sent into the dead service together, once per cool-down.
317
+ */
318
+ private waitForService;
319
+ /** Renew the access token shortly before it expires — in a lane, never on a hook. */
320
+ private refreshAhead;
101
321
  private post;
102
- private patch;
322
+ /** A background send: every lane job is safe to repeat once (client ids, a PATCH, a ticket). */
103
323
  private send;
324
+ /**
325
+ * One request, with a deadline, reported to the health verdict, with the
326
+ * one-refresh-on-401 rule and — for background sends — one retry after a
327
+ * transient failure.
328
+ *
329
+ * WHICH FAILURES ARE RETRIED. Only those that prove the service did not take
330
+ * the request: a connection that never happened or a pooled socket the load
331
+ * balancer had already closed ({@link SAFE_RETRY_CODES}), and a 429 or 503.
332
+ * Not a 502/504 or a reset mid-request — the origin may have the first
333
+ * attempt — not a timeout (a hung service would just cost another deadline)
334
+ * and not while the service is already failing (the breaker's cool-down is
335
+ * the retry then). Every background body carries a client-generated id — the
336
+ * run, the step, the execution ticket — or is a PATCH of one field, so even
337
+ * a repeat whose first attempt did land is recognisable; a 409 on the retry
338
+ * is taken as that.
339
+ *
340
+ * THE ACCOUNT IS FIXED FOR THE REQUEST. A retry or a refresh leaves the event
341
+ * loop, and a hook may sign another account in meanwhile; the next attempt
342
+ * then goes nowhere ({@link AccountChanged}) instead of out under that
343
+ * account's token.
344
+ *
345
+ * EVERY ANSWER SAYS WHEN ITS ATTEMPT WAS SENT. `health.ok(started)`: the late
346
+ * answer of an attempt a hook already gave up on (`slow`) must not end the
347
+ * cool-down that overrun started.
348
+ *
349
+ * `onRetry` runs when the first attempt failed and a retry is about to
350
+ * sleep: whoever waits on this answer on a hook's behalf can stop now.
351
+ */
352
+ private request;
353
+ /**
354
+ * Refresh once for everyone. Concurrent 401s — a run's create, an execution
355
+ * report and a probe crossing the expiry together — used to each spend the
356
+ * rotating refresh token; one won, the others were rejected and their sends
357
+ * (a create: the whole turn) were dropped.
358
+ */
359
+ private refreshSingleFlight;
104
360
  /**
105
361
  * Adopt a newer session written by a sibling process, if there is one.
106
362
  *
107
363
  * Returns true when this recorder now holds a usable token it did not have to
108
- * spend a refresh on.
364
+ * spend a refresh on. Only a session for the same service and the same
365
+ * account is adopted: the binding is what keeps a token from being sent to
366
+ * an address it was not issued for, and an account change is a run boundary
367
+ * ({@link checkCredentials}), never a mid-run swap.
109
368
  */
110
369
  private adoptStoredSession;
111
370
  /**
@@ -121,8 +380,44 @@ export declare class RemoteRecorder implements Recorder {
121
380
  * The fix is to treat the credentials file as the shared source of truth:
122
381
  * check it before spending our token, and check it again if our refresh is
123
382
  * rejected, because the process that beat us has already written a good one.
383
+ * A rejection with nothing to adopt pauses the recorder (`auth_rejected`)
384
+ * instead of paying two round trips on every later send; a new sign-in
385
+ * resumes it, and so does a retry every {@link AUTH_REJECTED_RETRY_MS}.
386
+ *
387
+ * WHAT COUNTS AS A REJECTION. Only the service's own JSON 400/401 — the
388
+ * answer that says this refresh token is spent. A 403 or a web page (a CDN
389
+ * challenge, a portal) says nothing about the token, and pausing on one
390
+ * stopped recording for the rest of a daemon's life. And a rejection is
391
+ * believed only after one more look at the file ~{@link AUTH_RECHECK_MS}
392
+ * later: a sibling that rotated the token a moment ago may not have written
393
+ * it yet. Any other failure is not retried for {@link REFRESH_RETRY_MS}, so a
394
+ * refresh route that keeps failing costs one request per half minute, not
395
+ * one per send.
124
396
  */
125
397
  private refresh;
398
+ /**
399
+ * While `auth_rejected`, try the refresh again now and then, in the
400
+ * background: a refusal the service later takes back (a bad deploy, a
401
+ * clock), or one the recheck missed, must not need a `bir login` to end.
402
+ * Never awaited by the caller — a hook's credentials check starts it.
403
+ */
404
+ private retryRejected;
126
405
  private refreshOverNetwork;
406
+ /** The file now holds this recorder's session: follow it from here on. */
407
+ private noteFileSeen;
408
+ /**
409
+ * Keep in step with credentials.json, cheaply: one `stat` per
410
+ * {@link CREDENTIALS_CHECK_MS}, and a read only when it changed.
411
+ *
412
+ * - gone → `bir logout`: pause (`signed_out`), drop what is queued.
413
+ * - another service → pause (`other_service`): never send there.
414
+ * - another account → adopt it for runs opened from now on; runs already
415
+ * open are closed, so no run is split across two accounts.
416
+ * - newer tokens → adopt them (a sibling refreshed, or `bir login`), and
417
+ * resume if paused.
418
+ */
419
+ private checkCredentials;
420
+ private pause;
421
+ private resume;
127
422
  }
128
423
  //# sourceMappingURL=remote-recorder.d.ts.map