@basein/runner 0.3.24 → 0.3.26
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 +27 -26
- package/dist/auth/client.d.ts +4 -1
- package/dist/auth/client.js +83 -14
- package/dist/bin/bir-hooks.js +4 -2
- package/dist/bin/bir.js +153 -37
- package/dist/bin/investigate.js +5 -0
- package/dist/bin/scenario-edit.d.ts +5 -0
- package/dist/bin/scenario-edit.js +17 -1
- package/dist/config/adapters/claude-code.d.ts +51 -7
- package/dist/config/adapters/claude-code.js +105 -18
- package/dist/config/generate.d.ts +61 -13
- package/dist/config/generate.js +108 -20
- package/dist/config/resolve.d.ts +5 -0
- package/dist/config/resolve.js +7 -0
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +28 -0
- package/dist/control/daemon.js +62 -0
- package/dist/control/paths.d.ts +19 -1
- package/dist/control/paths.js +20 -2
- package/dist/control/pin-guard.js +8 -2
- package/dist/control/recorder-main.js +17 -5
- package/dist/control/server.d.ts +63 -9
- package/dist/control/server.js +350 -62
- package/dist/control/transcript.d.ts +64 -3
- package/dist/control/transcript.js +318 -37
- package/dist/jsonrpc/framing.d.ts +18 -3
- package/dist/jsonrpc/framing.js +57 -4
- package/dist/proxy/intercept.d.ts +4 -0
- package/dist/proxy/intercept.js +4 -0
- package/dist/proxy/relay.d.ts +13 -0
- package/dist/proxy/relay.js +12 -1
- package/dist/proxy/session.d.ts +8 -1
- package/dist/proxy/session.js +67 -10
- package/dist/record/recorder.d.ts +6 -1
- package/dist/record/redact.d.ts +18 -4
- package/dist/record/redact.js +538 -79
- package/dist/record/remote-recorder.d.ts +26 -5
- package/dist/record/remote-recorder.js +174 -18
- package/dist/record/truncate.js +3 -1
- package/dist/replay/bash-channel.d.ts +7 -1
- package/dist/replay/bash-channel.js +9 -3
- package/dist/replay/builtins.d.ts +14 -0
- package/dist/replay/builtins.js +460 -95
- package/dist/replay/bundle.d.ts +80 -0
- package/dist/replay/bundle.js +455 -24
- package/dist/replay/controller.d.ts +64 -1
- package/dist/replay/controller.js +238 -38
- package/dist/replay/derive.js +46 -4
- package/dist/replay/handover.d.ts +13 -0
- package/dist/replay/handover.js +18 -0
- package/dist/replay/plan.d.ts +15 -0
- package/dist/replay/plan.js +48 -30
- package/dist/replay/pricing.d.ts +61 -11
- package/dist/replay/pricing.js +135 -42
- package/dist/replay/safety.js +43 -5
- package/dist/upstream/http-client.d.ts +6 -3
- package/dist/upstream/http-client.js +54 -13
- package/dist/upstream/stdio-client.d.ts +7 -0
- package/dist/upstream/stdio-client.js +1 -1
- package/dist/util/atomic-write.d.ts +9 -1
- package/dist/util/atomic-write.js +9 -6
- package/dist/util/service-health.d.ts +7 -0
- package/dist/util/service-health.js +10 -3
- package/docs/calculatedReplay.md +32 -0
- package/package.json +1 -1
- package/production.md +2 -2
- package/scripts/install.ps1 +14 -2
|
@@ -41,12 +41,14 @@
|
|
|
41
41
|
* `initialize` got a 405 and the server showed as failed every session).
|
|
42
42
|
* A stream that ends — cleanly or not — is noticed, the requests riding it
|
|
43
43
|
* are answered, and it is re-established with backoff; the new session is
|
|
44
|
-
* re-initialised with the host's own `initialize` before anything else
|
|
44
|
+
* re-initialised with the host's own `initialize` before anything else,
|
|
45
|
+
* and a stream whose re-initialisation fails is a reconnect that failed.
|
|
45
46
|
* 4. **Session expiry is survivable.** A 404 to a request that carried a
|
|
46
47
|
* session means the server forgot us (a redeploy, an idle timeout). The
|
|
47
48
|
* spec says start a new session; the proxy does it for the host — replays
|
|
48
49
|
* the host's cached `initialize` under a `bir:` id, sends `initialized`,
|
|
49
|
-
* and retries the request once.
|
|
50
|
+
* and retries the request once. A replay that fails is tried again when
|
|
51
|
+
* the next request meets the same 404.
|
|
50
52
|
* 5. **No timeout shorter than the host's.** The transport (http-transport.ts)
|
|
51
53
|
* bounds the connect only; how long a tool may take is Claude Code's call,
|
|
52
54
|
* and its cancellation reaches the server immediately (rule 2). What no
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
* recovery, a session the server forgot, and a stream that keeps dropping as
|
|
73
75
|
* soon as it opens (once an hour per server at most).
|
|
74
76
|
*/
|
|
75
|
-
import { readFrames } from "../jsonrpc/framing.js";
|
|
77
|
+
import { readFrames, receivedText, rememberText } from "../jsonrpc/framing.js";
|
|
76
78
|
import { paramsObject } from "../jsonrpc/types.js";
|
|
77
79
|
import { logDetail, logEvery, logLine, logOnce, errText } from "../util/log.js";
|
|
78
80
|
import { BaseUpstreamClient, BIR_ID_PREFIX, isBirId } from "./client.js";
|
|
@@ -341,7 +343,11 @@ export class HttpUpstreamClient extends BaseUpstreamClient {
|
|
|
341
343
|
accept: "application/json, text/event-stream",
|
|
342
344
|
...this.sessionHeaders(method === "initialize"),
|
|
343
345
|
...this.headers,
|
|
344
|
-
},
|
|
346
|
+
},
|
|
347
|
+
// The host's own line when this is the message as the relay read it
|
|
348
|
+
// (jsonrpc/framing.ts): serialised again, an integer past 2^53 in a
|
|
349
|
+
// tool's arguments reached the server rounded.
|
|
350
|
+
receivedText(msg) ?? JSON.stringify(msg), controller.signal);
|
|
345
351
|
}
|
|
346
352
|
catch (err) {
|
|
347
353
|
clearTimeout(abandonTimer);
|
|
@@ -516,8 +522,16 @@ export class HttpUpstreamClient extends BaseUpstreamClient {
|
|
|
516
522
|
if (Array.isArray(parsed))
|
|
517
523
|
for (const item of parsed)
|
|
518
524
|
this.deliver(item);
|
|
519
|
-
else
|
|
525
|
+
else {
|
|
526
|
+
// One message, and this is its text: the relay writes it to the host as
|
|
527
|
+
// the server wrote it rather than as JavaScript read it (framing.ts).
|
|
528
|
+
// Without the white space around it — a body often ends in a newline —
|
|
529
|
+
// and only when it is one line; a batch's members have no text of their
|
|
530
|
+
// own, and are serialised as before.
|
|
531
|
+
if (parsed && typeof parsed === "object")
|
|
532
|
+
rememberText(parsed, raw.trim());
|
|
520
533
|
this.deliver(parsed);
|
|
534
|
+
}
|
|
521
535
|
return true;
|
|
522
536
|
}
|
|
523
537
|
/** Hand one inbound JSON-RPC message to the relay (or to a `bir:` waiter). */
|
|
@@ -601,7 +615,18 @@ export class HttpUpstreamClient extends BaseUpstreamClient {
|
|
|
601
615
|
if (this.transport === "http") {
|
|
602
616
|
this.sessionId = undefined;
|
|
603
617
|
this.resetStandingStream();
|
|
604
|
-
|
|
618
|
+
if (await this.reinitialize())
|
|
619
|
+
return true;
|
|
620
|
+
// No new session: a 503 from an instance still starting, a network
|
|
621
|
+
// error, no answer in time. Left without an id, every later request
|
|
622
|
+
// went out bare, the server answered 400 or 404, and none of that
|
|
623
|
+
// read as "forgotten" any more — errors for the rest of the host's
|
|
624
|
+
// session, with the proxy up. Holding the forgotten id again makes
|
|
625
|
+
// the next request's 404 start another attempt. An id this attempt
|
|
626
|
+
// was handed is dropped with it: its handshake never completed.
|
|
627
|
+
if (!this.dead)
|
|
628
|
+
this.sessionId = staleKey;
|
|
629
|
+
return false;
|
|
605
630
|
}
|
|
606
631
|
// Legacy: the session *is* the event stream. Drop it and open a new one;
|
|
607
632
|
// reconnect() replays the handshake. A reconnect already under way (the
|
|
@@ -634,9 +659,10 @@ export class HttpUpstreamClient extends BaseUpstreamClient {
|
|
|
634
659
|
/**
|
|
635
660
|
* Replay the host's `initialize` (under a `bir:` id) and `initialized`.
|
|
636
661
|
* `news`: say it on stderr when it worked — after a forgotten session or an
|
|
637
|
-
* outage; after a routine reconnect it is detail. A failure is
|
|
662
|
+
* outage; after a routine reconnect it is detail. A failure is said, unless
|
|
663
|
+
* `failureSaid`: it is one more of an outage whose first failure was.
|
|
638
664
|
*/
|
|
639
|
-
async reinitialize(news = true) {
|
|
665
|
+
async reinitialize(news = true, failureSaid = false) {
|
|
640
666
|
if (this.initParams === undefined || this.dead)
|
|
641
667
|
return false;
|
|
642
668
|
this.reinitCount += 1;
|
|
@@ -663,7 +689,10 @@ export class HttpUpstreamClient extends BaseUpstreamClient {
|
|
|
663
689
|
return false;
|
|
664
690
|
if (!reply || !("result" in reply)) {
|
|
665
691
|
const error = reply ? paramsObject(reply.error).message : "no answer";
|
|
666
|
-
logLine("upstream.reinitialize_failed", {
|
|
692
|
+
(failureSaid ? logDetail : logLine)("upstream.reinitialize_failed", {
|
|
693
|
+
server: this.label,
|
|
694
|
+
error: String(error ?? "error"),
|
|
695
|
+
});
|
|
667
696
|
return false;
|
|
668
697
|
}
|
|
669
698
|
this.noteProtocolVersion(reply.result);
|
|
@@ -856,15 +885,27 @@ export class HttpUpstreamClient extends BaseUpstreamClient {
|
|
|
856
885
|
return false;
|
|
857
886
|
}
|
|
858
887
|
const recovered = this.reconnectFailures > 0;
|
|
888
|
+
// A new event stream is a new, uninitialised session: replay the host's
|
|
889
|
+
// handshake before anything else is let through. Until that has worked
|
|
890
|
+
// the reconnect has not: counted as done on the stream alone, a replay
|
|
891
|
+
// the server refused (it was still starting) left every later request
|
|
892
|
+
// posted to a session nobody had initialised, and nothing ever tried
|
|
893
|
+
// again. So the stream is dropped and the attempt counts as failed —
|
|
894
|
+
// backoff, and the next request pulls a new one forward.
|
|
895
|
+
if (this.initParams !== undefined && !(await this.reinitialize(recovered || afterExpiry, recovered))) {
|
|
896
|
+
if (this.dead)
|
|
897
|
+
return false;
|
|
898
|
+
this.reconnectFailures += 1;
|
|
899
|
+
this.onSseStreamLost(this.generation, "re-initialise failed", true);
|
|
900
|
+
this.streamAbort?.abort();
|
|
901
|
+
this.scheduleReconnect();
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
859
904
|
(recovered ? logLine : logDetail)("upstream.stream_reconnected", {
|
|
860
905
|
server: this.label,
|
|
861
906
|
failedAttempts: this.reconnectFailures,
|
|
862
907
|
});
|
|
863
908
|
this.reconnectFailures = 0;
|
|
864
|
-
// A new event stream is a new, uninitialised session: replay the host's
|
|
865
|
-
// handshake before anything else is let through.
|
|
866
|
-
if (this.initParams !== undefined)
|
|
867
|
-
await this.reinitialize(recovered || afterExpiry);
|
|
868
909
|
return true;
|
|
869
910
|
})();
|
|
870
911
|
this.connecting = attempt;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* {@link StdioUpstreamClient.shutdown} closes stdin first (the MCP-spec way to
|
|
23
23
|
* ask a server to exit), waits briefly, and only then kills the whole tree.
|
|
24
24
|
*/
|
|
25
|
+
import { type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
25
26
|
import { type JsonRpcMessage } from "../jsonrpc/types.js";
|
|
26
27
|
import { BaseUpstreamClient } from "./client.js";
|
|
27
28
|
export interface StdioUpstreamOptions {
|
|
@@ -80,6 +81,12 @@ export declare function whichSync(command: string, env?: NodeJS.ProcessEnv): str
|
|
|
80
81
|
* On POSIX this is the identity. On Windows a batch file is wrapped in `cmd.exe`.
|
|
81
82
|
*/
|
|
82
83
|
export declare function resolveCommand(command: string, args: string[], env?: NodeJS.ProcessEnv): ResolvedCommand;
|
|
84
|
+
/**
|
|
85
|
+
* Kill `child` and everything it started. On Windows that takes `taskkill /T`
|
|
86
|
+
* (a plain kill terminates one process: `cmd.exe`, not the server behind it);
|
|
87
|
+
* elsewhere SIGTERM, then SIGKILL. Bounded; never throws.
|
|
88
|
+
*/
|
|
89
|
+
export declare function killTree(child: ChildProcessWithoutNullStreams): Promise<void>;
|
|
83
90
|
export declare class StdioUpstreamClient extends BaseUpstreamClient {
|
|
84
91
|
readonly ready: Promise<void>;
|
|
85
92
|
private child?;
|
|
@@ -161,7 +161,7 @@ function waitForExit(child, ms) {
|
|
|
161
161
|
* (a plain kill terminates one process: `cmd.exe`, not the server behind it);
|
|
162
162
|
* elsewhere SIGTERM, then SIGKILL. Bounded; never throws.
|
|
163
163
|
*/
|
|
164
|
-
async function killTree(child) {
|
|
164
|
+
export async function killTree(child) {
|
|
165
165
|
const pid = child.pid;
|
|
166
166
|
if (pid === undefined)
|
|
167
167
|
return;
|
|
@@ -19,11 +19,19 @@
|
|
|
19
19
|
* - a symlinked target (dotfile managers do this to ~/.claude.json) must keep
|
|
20
20
|
* being a symlink, so the rename goes to the link's real target.
|
|
21
21
|
*
|
|
22
|
-
* The existing file's mode is kept; `mode` applies to a new file
|
|
22
|
+
* The existing file's mode is kept; `mode` applies to a new file, and to an
|
|
23
|
+
* existing one too with `replaceMode`.
|
|
23
24
|
*/
|
|
24
25
|
export interface AtomicWriteOptions {
|
|
25
26
|
/** Mode for a file that does not exist yet. An existing file keeps its own. */
|
|
26
27
|
mode?: number;
|
|
28
|
+
/**
|
|
29
|
+
* `mode` for an existing file as well: a file a credential goes in. Kept,
|
|
30
|
+
* the old file's mode went to the new one, and when the old file was not
|
|
31
|
+
* ours to chmod first (root's 0644, after `sudo claude`), the token went
|
|
32
|
+
* into a file that is ours and readable by every account.
|
|
33
|
+
*/
|
|
34
|
+
replaceMode?: boolean;
|
|
27
35
|
}
|
|
28
36
|
export declare function writeFileAtomic(path: string, data: string | Uint8Array, opts?: AtomicWriteOptions): void;
|
|
29
37
|
//# sourceMappingURL=atomic-write.d.ts.map
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - a symlinked target (dotfile managers do this to ~/.claude.json) must keep
|
|
20
20
|
* being a symlink, so the rename goes to the link's real target.
|
|
21
21
|
*
|
|
22
|
-
* The existing file's mode is kept; `mode` applies to a new file
|
|
22
|
+
* The existing file's mode is kept; `mode` applies to a new file, and to an
|
|
23
|
+
* existing one too with `replaceMode`.
|
|
23
24
|
*/
|
|
24
25
|
import { chmodSync, lstatSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
25
26
|
import { randomBytes } from "node:crypto";
|
|
@@ -42,11 +43,13 @@ function resolveTarget(path) {
|
|
|
42
43
|
export function writeFileAtomic(path, data, opts = {}) {
|
|
43
44
|
const target = resolveTarget(path);
|
|
44
45
|
let mode = opts.mode;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
if (!(opts.replaceMode && mode !== undefined)) {
|
|
47
|
+
try {
|
|
48
|
+
mode = statSync(target).mode & 0o777;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* new file */
|
|
52
|
+
}
|
|
50
53
|
}
|
|
51
54
|
const tmp = join(dirname(target), `.${basename(target)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
52
55
|
try {
|
|
@@ -35,6 +35,13 @@ export declare const MAX_COOLDOWN_MS = 60000;
|
|
|
35
35
|
* and never answers, holding a socket and a chain slot the whole time.
|
|
36
36
|
*/
|
|
37
37
|
export declare const DEFAULT_SERVICE_TIMEOUT_MS = 10000;
|
|
38
|
+
/**
|
|
39
|
+
* The longest delay a Node timer takes. Given more, `setTimeout` warns and
|
|
40
|
+
* waits 1 ms instead — so `BIR_SERVICE_TIMEOUT_MS=3000000000`, set to switch
|
|
41
|
+
* the timeout off, ended every request at once. Every delay that comes from
|
|
42
|
+
* the environment or from a caller is held to this.
|
|
43
|
+
*/
|
|
44
|
+
export declare const MAX_TIMER_MS = 2147483647;
|
|
38
45
|
export declare function serviceTimeoutMs(): number;
|
|
39
46
|
export interface HealthSnapshot {
|
|
40
47
|
/** False while a cool-down is running: hot paths do not wait. */
|
|
@@ -36,9 +36,16 @@ export const MAX_COOLDOWN_MS = 60_000;
|
|
|
36
36
|
* and never answers, holding a socket and a chain slot the whole time.
|
|
37
37
|
*/
|
|
38
38
|
export const DEFAULT_SERVICE_TIMEOUT_MS = 10_000;
|
|
39
|
+
/**
|
|
40
|
+
* The longest delay a Node timer takes. Given more, `setTimeout` warns and
|
|
41
|
+
* waits 1 ms instead — so `BIR_SERVICE_TIMEOUT_MS=3000000000`, set to switch
|
|
42
|
+
* the timeout off, ended every request at once. Every delay that comes from
|
|
43
|
+
* the environment or from a caller is held to this.
|
|
44
|
+
*/
|
|
45
|
+
export const MAX_TIMER_MS = 2_147_483_647;
|
|
39
46
|
export function serviceTimeoutMs() {
|
|
40
47
|
const n = Number(process.env.BIR_SERVICE_TIMEOUT_MS);
|
|
41
|
-
return Number.isFinite(n) && n > 0 ? n : DEFAULT_SERVICE_TIMEOUT_MS;
|
|
48
|
+
return Number.isFinite(n) && n > 0 ? Math.min(n, MAX_TIMER_MS) : DEFAULT_SERVICE_TIMEOUT_MS;
|
|
42
49
|
}
|
|
43
50
|
export class ServiceHealth {
|
|
44
51
|
failures = 0;
|
|
@@ -170,7 +177,7 @@ export async function awaitWithin(pending, budgetMs, what, health = serviceHealt
|
|
|
170
177
|
const winner = await Promise.race([
|
|
171
178
|
pending,
|
|
172
179
|
new Promise((resolve) => {
|
|
173
|
-
timer = setTimeout(() => resolve(timedOut), budgetMs);
|
|
180
|
+
timer = setTimeout(() => resolve(timedOut), Math.min(budgetMs, MAX_TIMER_MS));
|
|
174
181
|
timer.unref?.();
|
|
175
182
|
}),
|
|
176
183
|
]);
|
|
@@ -197,7 +204,7 @@ export async function awaitWithin(pending, budgetMs, what, health = serviceHealt
|
|
|
197
204
|
*/
|
|
198
205
|
export function timeoutSignal(ms = serviceTimeoutMs(), parent) {
|
|
199
206
|
const controller = new AbortController();
|
|
200
|
-
const timer = setTimeout(() => controller.abort(new DOMException(`timed out after ${ms} ms`, "TimeoutError")), ms);
|
|
207
|
+
const timer = setTimeout(() => controller.abort(new DOMException(`timed out after ${ms} ms`, "TimeoutError")), Math.min(ms, MAX_TIMER_MS));
|
|
201
208
|
timer.unref?.();
|
|
202
209
|
if (parent) {
|
|
203
210
|
if (parent.aborted) {
|
package/docs/calculatedReplay.md
CHANGED
|
@@ -771,6 +771,38 @@ Delivery, best channel first. (A direct plan's own results go through `run_scena
|
|
|
771
771
|
(`assembleBundle`/`truncate`, portable verbatim from `payload-executor.ts`). Works for any tool,
|
|
772
772
|
and is read as adversarial — hence last.
|
|
773
773
|
|
|
774
|
+
**How a bundle reads** ([`bundle.ts`](../src/replay/bundle.ts); the same in a direct run's answer,
|
|
775
|
+
§6). The agent pays once to take the bundle in, as new input, and on a long plan that was most of a
|
|
776
|
+
replayed task's bill — so nothing in it repeats what the model already has. Each result is shown as
|
|
777
|
+
its text, not as MCP's `CallToolResult` JSON (the envelope and its second layer of escaping told the
|
|
778
|
+
model nothing), with `(error)` when `isError` is set; a result with anything but text — an image, a
|
|
779
|
+
resource, structured content — is shown as it came. Consecutive calls to one tool share a section,
|
|
780
|
+
`--- Steps a-b: tool (n calls) ---`, with one numbered line per call, `n: input -> result`. Input
|
|
781
|
+
fields with the same value in every call are said once above them (`In every call: {…}`), and so is
|
|
782
|
+
an answer every call got (`Every call's result: …`). A step on its own, and a step left out, keep
|
|
783
|
+
`--- Step n: tool ---`. Nothing is dropped, and each result keeps its fair share of the cap. On
|
|
784
|
+
B2's length tests this made bundles 27% shorter, 33% at 50–60 steps.
|
|
785
|
+
|
|
786
|
+
**A run that went well reads briefly** (`AssembleOptions.brief`, asked for by a direct run, §6). When
|
|
787
|
+
every step ran live and succeeded and the agent is to answer now, its job is to see that the steps
|
|
788
|
+
were right and say what was done — not to read every word of every result. So the header says every
|
|
789
|
+
call succeeded; a result up to 300 characters is shown whole; a longer one that is JSON keeps every
|
|
790
|
+
record and field, and only its text longer than 40 characters is cut, with "…" (`cutLongText`: the
|
|
791
|
+
IDs, dates, senders and names the agent checks the steps against are short; an e-mail's body is
|
|
792
|
+
not). A long result in words is shown whole — it may be the very answer. Showing only the values the
|
|
793
|
+
plan took from a long result was tried first: it left the agent unable to check the plan's choices
|
|
794
|
+
("I couldn't confirm these were the newest") in one reply in six on B2's length tests, and once it
|
|
795
|
+
reported a correct run as wrong. The plan's summary (its response model) is said in a few words
|
|
796
|
+
(short lists whole, long ones counted), in the bundle, above the footer — so `bir-scenario` gets
|
|
797
|
+
`responseModel: {}` and does not append it again — and is left out when it only says it was left
|
|
798
|
+
out. The footer asks for
|
|
799
|
+
the answer in a few sentences: the reply is output, the dearest tokens an agent spends. Anything
|
|
800
|
+
that went wrong — a step that reported an error, a run that stopped short, a recorded stand-in, a
|
|
801
|
+
sub-task the agent carries on with — gets the full bundle above, because that is what the agent
|
|
802
|
+
works from next. For the same reason a direct run's directive (`steeringDirective`) no longer lists
|
|
803
|
+
the planned steps: the model calls none of them, and the answer names every call and says whether
|
|
804
|
+
the run was whole. An operator's `bir replay` and a divergence bundle are always full.
|
|
805
|
+
|
|
774
806
|
**Tier 2 — abort.** Composition itself threw (`replay.compose_failed`). Retire the plan, return
|
|
775
807
|
`{}`, continue as an ordinary turn. Outcome `failed`.
|
|
776
808
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basein/runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.26",
|
|
4
4
|
"description": "A recording MCP proxy: sits between any MCP client and its MCP servers, executes each call on the client's behalf, and records the run as a reusable BaseIn scenario.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/production.md
CHANGED
|
@@ -523,7 +523,7 @@ Over 2-minute runs with one turn every 8 s (92 hooks):
|
|
|
523
523
|
|
|
524
524
|
- **Backlog.** The backlog is bounded: 500 sends or 32 MiB, oldest steps dropped first. A queued send still facing a cool-down after 60 s is dropped unsent, not sent into the outage.
|
|
525
525
|
- **Flush.** A flush waits at most 2 s (`BIR_FLUSH_TIMEOUT_MS`), and not at all while the service is cooling down.
|
|
526
|
-
- **Retries.** A retry happens once, only after 429, 503, or a connection error that proves nothing reached the service. A 409 on the retry counts as "already landed". There is no retry after 502, 504 or a mid-request reset, until the service confirms it deduplicates ids (§10). What a 429 costs is in §3.3. The service's new build answers a report whose write failed with `503 report_not_booked`, so this one retry sends the report once more (built, not deployed yet).
|
|
526
|
+
- **Retries.** A retry happens once, only after 429, 503, or a connection error that proves nothing reached the service. A 409 on the retry counts as "already landed". There is no retry after 502, 504 or a mid-request reset, until the service confirms it deduplicates ids (§10). What a 429 costs is in §3.3. The service's new build answers a report whose write failed with `503 report_not_booked`, so this one retry sends the report once more (built, not deployed yet). Three sends are different, because the service takes a second copy of each without harm: a run's finish and a patch (an update that keeps what it has), and an execution report that carries its ticket (a second copy answers `200 duplicate`). From 0.3.26 each of these goes back to the head of its lane when it fails with no answer or an unhealthy one (502, 504, a reset), up to 3 sends in all, and it is exempt from the 60 s give-up. Before, a run whose finish was lost never ended (never embedded, matched or calculated), and a turn whose report was lost was never booked. Any other 4xx stays final.
|
|
527
527
|
|
|
528
528
|
**Size caps on uploads.**
|
|
529
529
|
|
|
@@ -1174,7 +1174,7 @@ Defaults were checked in `src/`.
|
|
|
1174
1174
|
- **Two sessions in one project.** Direct replays for a session whose own proxy has not yet bound hand over instead of running.
|
|
1175
1175
|
- **Revive gaps.** `bir-scenario` does not revive in its first 12 s; those first hooks are left to the SessionStart hook. A session with no wrapped server and no `bir` server is not revived until `/clear` or a new session. The revive window was not measured with real Claude Code.
|
|
1176
1176
|
- **Direct-run worst case.** The worst case by budget is 120 s: 8 s of derivation, 100 s of plan, and 12 s of slack. It is reached only if every logic body hits its limit, and sits at Claude Code's 2-minute MCP auto-background. Setting `BIR_REPLAY_BUDGET_MS=120000` (the old default) makes it 140 s.
|
|
1177
|
-
- **Retries are conservative.** A
|
|
1177
|
+
- **Retries are conservative.** A step or run create lost to a 502, 504 or mid-request reset is not retried until the service deduplicates ids. (A finish, a patch and a ticketed report are sent up to 3 times from 0.3.26: the service takes a second copy of each without harm, §6.5. The service now answers a repeated run or step id with 409.)
|
|
1178
1178
|
- **Healthy-service prompt cost.** With an instant local service the prompt hook is about 6 ms slower than 0.2.12 (about 17 vs 11 ms median); the cause was not isolated. Under 503 or slow answers the recorder also keeps more idle keep-alive sockets. Both are small, but visible in measurements.
|
|
1179
1179
|
- **A host other than Claude Code is not checked end to end.** What §3.2 describes ran on a bench only: a real LangChain agent against the real recorder, with a stand-in service and a fake model (`examples/langchain-proof/`). A real model, the real service and the production server have not been tried. The service's part is built and not deployed, and the Python package is not on PyPI. What that means for a LangChain application today is in `langChainGuide.md`, part 5.
|
|
1180
1180
|
- **An interrupted or lossy Claude Code run is not marked.** Only a run of another host is finished with an `error` or with the `incomplete` mark today. A Claude Code run that was interrupted or lost steps is finished as before.
|
package/scripts/install.ps1
CHANGED
|
@@ -65,6 +65,11 @@ function Invoke-BirInstall {
|
|
|
65
65
|
# child's environment only, set right before the spawn and removed after.
|
|
66
66
|
$setupToken = $env:BIR_SETUP_TOKEN
|
|
67
67
|
if ($setupToken) { Remove-Item -Path Env:BIR_SETUP_TOKEN -ErrorAction SilentlyContinue }
|
|
68
|
+
# The uninstall switch the same way: read once and taken out of the session,
|
|
69
|
+
# so pasting the ordinary install line later in this window installs, and
|
|
70
|
+
# does not uninstall again.
|
|
71
|
+
$uninstall = ($env:BIR_UNINSTALL -eq '1')
|
|
72
|
+
if ($null -ne $env:BIR_UNINSTALL) { Remove-Item -Path Env:BIR_UNINSTALL -ErrorAction SilentlyContinue }
|
|
68
73
|
|
|
69
74
|
# Inside the function on purpose: under `irm | iex` these would otherwise
|
|
70
75
|
# change the session the person keeps working in.
|
|
@@ -262,7 +267,14 @@ function Invoke-BirInstall {
|
|
|
262
267
|
Write-Note "node v$Version already at $exe"
|
|
263
268
|
return $exe
|
|
264
269
|
}
|
|
265
|
-
|
|
270
|
+
}
|
|
271
|
+
# Half an extraction, a foreign file, or a folder whose node.exe is gone
|
|
272
|
+
# (an antivirus quarantine takes just that): removed, never extracted
|
|
273
|
+
# over. Whenever the folder is there, not only when node.exe is - a
|
|
274
|
+
# Move-Item onto a folder that exists moves the new one INSIDE it, so
|
|
275
|
+
# node.exe never appeared where it is looked for and every retry
|
|
276
|
+
# failed, 80 MB heavier. install.sh removes it the same way.
|
|
277
|
+
if (Test-Path -LiteralPath $nodeDir) {
|
|
266
278
|
if ($dryRun) { Write-Would "remove the unusable $nodeDir" }
|
|
267
279
|
else {
|
|
268
280
|
try { Remove-Item -LiteralPath $nodeDir -Recurse -Force }
|
|
@@ -459,7 +471,7 @@ public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wP
|
|
|
459
471
|
# before its files go. Every wrapped MCP server and every hook names a
|
|
460
472
|
# file under ~\.baseinstrunner; deleting that first would break them
|
|
461
473
|
# all, in every project, with no `bir` left to repair it.
|
|
462
|
-
if ($
|
|
474
|
+
if ($uninstall) {
|
|
463
475
|
Write-Step 'Removing the runner from every project on this machine'
|
|
464
476
|
$birCmd = Join-Path $birHome 'bin\bir.cmd'
|
|
465
477
|
if (-not (Test-Path -LiteralPath $birCmd -PathType Leaf)) {
|