@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.
- package/README.md +540 -369
- package/dist/auth/client.d.ts +65 -3
- package/dist/auth/client.js +305 -53
- package/dist/bin/bir-hooks.d.ts +72 -31
- package/dist/bin/bir-hooks.js +176 -284
- package/dist/bin/bir-proxy.d.ts +17 -2
- package/dist/bin/bir-proxy.js +222 -28
- package/dist/bin/bir-scenario.d.ts +61 -21
- package/dist/bin/bir-scenario.js +593 -122
- package/dist/bin/bir.d.ts +12 -6
- package/dist/bin/bir.js +2935 -489
- package/dist/bin/investigate.js +40 -15
- package/dist/bin/scenario-edit.d.ts +7 -1
- package/dist/bin/scenario-edit.js +43 -21
- package/dist/bin/scenario-liveness.d.ts +194 -0
- package/dist/bin/scenario-liveness.js +459 -0
- package/dist/bin/scenario-stderr.d.ts +43 -0
- package/dist/bin/scenario-stderr.js +76 -0
- package/dist/bin/service-read.d.ts +88 -0
- package/dist/bin/service-read.js +128 -0
- package/dist/bin/setup.d.ts +19 -0
- package/dist/bin/setup.js +146 -20
- package/dist/config/adapters/claude-code.d.ts +92 -14
- package/dist/config/adapters/claude-code.js +291 -101
- package/dist/config/adapters/generic.d.ts +4 -1
- package/dist/config/adapters/generic.js +13 -14
- package/dist/config/generate.d.ts +392 -14
- package/dist/config/generate.js +666 -19
- package/dist/config/mcp-approval.d.ts +76 -0
- package/dist/config/mcp-approval.js +210 -0
- package/dist/config/node-path.d.ts +42 -0
- package/dist/config/node-path.js +102 -0
- package/dist/config/overrides.d.ts +62 -0
- package/dist/config/overrides.js +136 -0
- package/dist/config/resolve.d.ts +70 -8
- package/dist/config/resolve.js +186 -15
- package/dist/control/client.d.ts +22 -0
- package/dist/control/client.js +26 -7
- package/dist/control/correlation.d.ts +23 -5
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +400 -18
- package/dist/control/daemon.js +1105 -102
- package/dist/control/discovery.d.ts +77 -5
- package/dist/control/discovery.js +186 -11
- package/dist/control/ensure-hook.d.ts +61 -0
- package/dist/control/ensure-hook.js +160 -8
- package/dist/control/paths.d.ts +26 -0
- package/dist/control/paths.js +34 -0
- package/dist/control/pin-guard.d.ts +83 -0
- package/dist/control/pin-guard.js +534 -0
- package/dist/control/recorder-keeper.d.ts +229 -0
- package/dist/control/recorder-keeper.js +721 -0
- package/dist/control/recorder-main.d.ts +73 -0
- package/dist/control/recorder-main.js +778 -0
- package/dist/control/server.d.ts +707 -31
- package/dist/control/server.js +2786 -302
- package/dist/control/transcript.d.ts +21 -1
- package/dist/control/transcript.js +378 -112
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/jsonrpc/framing.d.ts +76 -12
- package/dist/jsonrpc/framing.js +216 -59
- package/dist/proxy/intercept.d.ts +49 -5
- package/dist/proxy/intercept.js +188 -59
- package/dist/proxy/relay.d.ts +16 -0
- package/dist/proxy/relay.js +77 -5
- package/dist/proxy/revive.d.ts +122 -0
- package/dist/proxy/revive.js +251 -0
- package/dist/proxy/session.d.ts +222 -14
- package/dist/proxy/session.js +708 -99
- package/dist/record/recorder.d.ts +51 -3
- package/dist/record/recorder.js +3 -0
- package/dist/record/redact.d.ts +103 -9
- package/dist/record/redact.js +659 -30
- package/dist/record/remote-recorder.d.ts +327 -32
- package/dist/record/remote-recorder.js +1517 -187
- package/dist/record/switchable-recorder.d.ts +126 -0
- package/dist/record/switchable-recorder.js +351 -0
- package/dist/record/truncate.d.ts +40 -1
- package/dist/record/truncate.js +111 -4
- package/dist/replay/bash-channel.d.ts +35 -0
- package/dist/replay/bash-channel.js +133 -0
- package/dist/replay/bundle.d.ts +10 -0
- package/dist/replay/bundle.js +10 -0
- package/dist/replay/controller.d.ts +369 -20
- package/dist/replay/controller.js +1176 -107
- package/dist/replay/derive.d.ts +1 -0
- package/dist/replay/derive.js +81 -24
- package/dist/replay/executor.d.ts +51 -16
- package/dist/replay/executor.js +114 -41
- package/dist/replay/handover.d.ts +54 -1
- package/dist/replay/handover.js +55 -5
- package/dist/replay/logic.d.ts +62 -10
- package/dist/replay/logic.js +140 -22
- package/dist/replay/plan.d.ts +201 -27
- package/dist/replay/plan.js +581 -141
- package/dist/replay/safety.d.ts +72 -0
- package/dist/replay/safety.js +303 -0
- package/dist/replay/source-run.d.ts +10 -0
- package/dist/replay/source-run.js +80 -35
- package/dist/replay/targets.d.ts +15 -1
- package/dist/replay/targets.js +62 -4
- package/dist/replay/tool-error.d.ts +15 -1
- package/dist/replay/tool-error.js +33 -1
- package/dist/replay/types.d.ts +7 -4
- package/dist/upstream/client.d.ts +56 -3
- package/dist/upstream/client.js +61 -4
- package/dist/upstream/headers.d.ts +43 -0
- package/dist/upstream/headers.js +90 -0
- package/dist/upstream/http-client.d.ts +213 -22
- package/dist/upstream/http-client.js +1034 -168
- package/dist/upstream/http-transport.d.ts +143 -0
- package/dist/upstream/http-transport.js +391 -0
- package/dist/upstream/lazy-client.d.ts +8 -0
- package/dist/upstream/lazy-client.js +29 -1
- package/dist/upstream/sse.d.ts +78 -0
- package/dist/upstream/sse.js +189 -0
- package/dist/upstream/stdio-client.d.ts +61 -1
- package/dist/upstream/stdio-client.js +356 -33
- package/dist/util/atomic-write.d.ts +29 -0
- package/dist/util/atomic-write.js +96 -0
- package/dist/util/journal.d.ts +15 -0
- package/dist/util/journal.js +48 -3
- package/dist/util/log.d.ts +10 -0
- package/dist/util/log.js +28 -1
- package/dist/util/off-switch.d.ts +46 -0
- package/dist/util/off-switch.js +91 -0
- package/dist/util/service-health.d.ts +133 -0
- package/dist/util/service-health.js +217 -0
- package/docs/BaseInstRunner.md +694 -621
- package/docs/calculatedReplay.md +1625 -1304
- package/docs/calculatedReplayGuide.md +1212 -1087
- package/docs/installRun.md +1013 -772
- package/docs/loginWeb.md +627 -607
- package/docs/mcpmark.md +49 -16
- package/docs/my-first-sample.md +569 -545
- package/docs/quickstart.md +384 -311
- package/docs/t-bench.md +406 -394
- package/package.json +58 -57
- package/production.md +1371 -0
- package/scripts/install.ps1 +97 -6
- 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
|
-
*
|
|
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
|
|
15
|
-
*
|
|
16
|
-
* - **Best-effort
|
|
17
|
-
* and never thrown at the caller
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
38
|
-
private readonly
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
private
|
|
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
|
-
*
|
|
45
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
88
|
-
* `flush` waits for it
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|