@basein/runner 0.2.3 → 0.2.5
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/dist/control/client.d.ts +13 -1
- package/dist/control/client.js +7 -4
- package/dist/control/correlation.d.ts +2 -0
- package/dist/control/server.d.ts +8 -0
- package/dist/control/server.js +48 -2
- package/dist/proxy/session.d.ts +2 -0
- package/dist/proxy/session.js +8 -1
- package/dist/replay/controller.d.ts +6 -0
- package/dist/replay/controller.js +5 -5
- package/dist/replay/executor.d.ts +82 -11
- package/dist/replay/executor.js +232 -39
- package/dist/replay/tool-error.d.ts +10 -9
- package/dist/replay/tool-error.js +13 -21
- package/docs/mcpmark.md +752 -752
- package/package.json +1 -1
package/dist/control/client.d.ts
CHANGED
|
@@ -23,11 +23,23 @@ export interface ProxyWork {
|
|
|
23
23
|
arguments: unknown;
|
|
24
24
|
timeoutMs: number;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Who a proxy is, sent with every register, poll and step report. The control
|
|
28
|
+
* server uses it to run a session's replay steps on that session's own upstream
|
|
29
|
+
* rather than on whichever proxy polled first (see replay/executor.ts).
|
|
30
|
+
*/
|
|
31
|
+
export interface ProxyIdentity {
|
|
32
|
+
proxyId: string;
|
|
33
|
+
/** Process start, epoch ms. */
|
|
34
|
+
startedAt: number;
|
|
35
|
+
pid: number;
|
|
36
|
+
}
|
|
26
37
|
export declare class ControlClient {
|
|
27
38
|
private readonly url;
|
|
28
39
|
private readonly token;
|
|
29
40
|
private readonly timeoutMs;
|
|
30
|
-
|
|
41
|
+
private readonly identity?;
|
|
42
|
+
constructor(url: string, token: string, timeoutMs?: number, identity?: ProxyIdentity);
|
|
31
43
|
register(info: {
|
|
32
44
|
serverName: string;
|
|
33
45
|
pid: number;
|
package/dist/control/client.js
CHANGED
|
@@ -10,18 +10,21 @@ export class ControlClient {
|
|
|
10
10
|
url;
|
|
11
11
|
token;
|
|
12
12
|
timeoutMs;
|
|
13
|
-
|
|
13
|
+
identity;
|
|
14
|
+
constructor(url, token, timeoutMs = 5_000, identity) {
|
|
14
15
|
this.url = url.replace(/\/+$/, "");
|
|
15
16
|
this.token = token;
|
|
16
17
|
this.timeoutMs = timeoutMs;
|
|
18
|
+
this.identity = identity;
|
|
17
19
|
}
|
|
18
20
|
async register(info) {
|
|
19
|
-
const body = await this.post("/proxy/register", info);
|
|
21
|
+
const body = await this.post("/proxy/register", { ...this.identity, ...info });
|
|
20
22
|
return body;
|
|
21
23
|
}
|
|
22
24
|
/** Report one completed MCP call. Resolves false when the send was dropped. */
|
|
23
25
|
async report(step) {
|
|
24
|
-
|
|
26
|
+
const body = this.identity ? { ...step, proxyId: this.identity.proxyId } : step;
|
|
27
|
+
return (await this.post("/proxy/step", body)) !== undefined;
|
|
25
28
|
}
|
|
26
29
|
async health() {
|
|
27
30
|
return (await this.request("GET", "/health"));
|
|
@@ -36,7 +39,7 @@ export class ControlClient {
|
|
|
36
39
|
* with a small delay so a dead server is not busy-looped.
|
|
37
40
|
*/
|
|
38
41
|
async poll(serverName, holdMs) {
|
|
39
|
-
const body = (await this.request("POST", "/proxy/poll", { serverName, holdMs }, holdMs + 10_000));
|
|
42
|
+
const body = (await this.request("POST", "/proxy/poll", { ...this.identity, serverName, holdMs }, holdMs + 10_000));
|
|
40
43
|
return body?.work;
|
|
41
44
|
}
|
|
42
45
|
/** Hand back one dispatched call's result, or the reason it could not run. */
|
|
@@ -49,6 +49,8 @@ export interface ProxyStepReport {
|
|
|
49
49
|
/** Epoch ms. */
|
|
50
50
|
startedAt: number;
|
|
51
51
|
durationMs: number;
|
|
52
|
+
/** The reporting proxy, added by its control client. Absent from older proxies. */
|
|
53
|
+
proxyId?: string;
|
|
52
54
|
}
|
|
53
55
|
export declare function newCallId(): string;
|
|
54
56
|
/**
|
package/dist/control/server.d.ts
CHANGED
|
@@ -328,6 +328,14 @@ export declare class ControlServer {
|
|
|
328
328
|
* hooks produces and is a complete MCP step in its own right.
|
|
329
329
|
*/
|
|
330
330
|
private onProxyStep;
|
|
331
|
+
/**
|
|
332
|
+
* Tie the reporting proxy to the session whose hook minted the step's call id,
|
|
333
|
+
* so that session's replay steps run on this proxy's upstream and not on
|
|
334
|
+
* another session's (replay/executor.ts). Searches every session, not just the
|
|
335
|
+
* one {@link sessionForProxyStep} records into: with two sessions open, that
|
|
336
|
+
* guess is exactly what cannot be trusted.
|
|
337
|
+
*/
|
|
338
|
+
private learnProxySession;
|
|
331
339
|
/**
|
|
332
340
|
* Which session a proxy's step belongs to.
|
|
333
341
|
*
|
package/dist/control/server.js
CHANGED
|
@@ -443,6 +443,7 @@ export class ControlServer {
|
|
|
443
443
|
const state = await this.replay.arm(match, prompt || run.input, this.wrapped, "prompt", {
|
|
444
444
|
recentResults: recentToolResults(session.transcriptPath, this.deriveRecentResults),
|
|
445
445
|
});
|
|
446
|
+
state.sessionId = session.sessionId;
|
|
446
447
|
run.replay = state;
|
|
447
448
|
run.replays.push(state);
|
|
448
449
|
return this.replay.directiveFor(state);
|
|
@@ -714,6 +715,15 @@ export class ControlServer {
|
|
|
714
715
|
logDetail("tool.pre.skipped", { run: run.runId, tool: toolName, why: "host housekeeping" });
|
|
715
716
|
return {};
|
|
716
717
|
}
|
|
718
|
+
// `run_scenario` with no live plan does nothing but answer "no scenario is
|
|
719
|
+
// armed". Recorded, that answer is a failed tool call, and a recording with
|
|
720
|
+
// one is never picked for calculation — observed in production on every
|
|
721
|
+
// first run in a project with the scenario server installed. With a live
|
|
722
|
+
// plan it is still recorded below, tagged `pinnedBy`.
|
|
723
|
+
if (this.replay.isDirectTool(toolName) && (!run.replay?.plan || run.replay.retired)) {
|
|
724
|
+
logDetail("tool.pre.skipped", { run: run.runId, tool: toolName, why: "no scenario armed" });
|
|
725
|
+
return {};
|
|
726
|
+
}
|
|
717
727
|
const agentId = payload.agent_id ?? session.agentId;
|
|
718
728
|
// The real intent, read from every line sharing this message's id
|
|
719
729
|
// (segmented.md R-INTENT-3). `context` stays the *written* text only, with
|
|
@@ -1077,6 +1087,7 @@ export class ControlServer {
|
|
|
1077
1087
|
liveCall: { toolName, toolInput },
|
|
1078
1088
|
recentResults: recentToolResults(session.transcriptPath, this.deriveRecentResults),
|
|
1079
1089
|
});
|
|
1090
|
+
state.sessionId = session.sessionId;
|
|
1080
1091
|
if (!state.plan) {
|
|
1081
1092
|
// A declined intent hit reports nothing: the turn's cost is not a
|
|
1082
1093
|
// measurement of that scenario's task, so it is no baseline sample either.
|
|
@@ -1234,6 +1245,10 @@ export class ControlServer {
|
|
|
1234
1245
|
// `tool_response` with no `tool_selected` before it.
|
|
1235
1246
|
if (isHousekeeping(toolName))
|
|
1236
1247
|
return {};
|
|
1248
|
+
// Nor for an unarmed `run_scenario`. The plan may have retired since the
|
|
1249
|
+
// pre hook, so the test is whether that hook opened a step, not the plan.
|
|
1250
|
+
if (this.replay.isDirectTool(toolName) && !run.builtIns.has(toolUseId))
|
|
1251
|
+
return {};
|
|
1237
1252
|
const failed = payload.hook_event_name === "PostToolUseFailure" || payload.error !== undefined;
|
|
1238
1253
|
// ── replay threading (docs/calculatedReplay.md §7.2) ─────────────────────
|
|
1239
1254
|
// `toolOutputLogic` was authored against the bytes that were *recorded* for
|
|
@@ -1353,6 +1368,8 @@ export class ControlServer {
|
|
|
1353
1368
|
async onSessionEnd(payload) {
|
|
1354
1369
|
const session = this.ensureSession(payload);
|
|
1355
1370
|
await this.finalizeRun(session);
|
|
1371
|
+
// Its proxies outlive it after a `/clear`, and serve the next session.
|
|
1372
|
+
this.replay.work.releaseSession(session.sessionId);
|
|
1356
1373
|
return {};
|
|
1357
1374
|
}
|
|
1358
1375
|
// ── proxy routes ─────────────────────────────────────────────────────────
|
|
@@ -1368,7 +1385,12 @@ export class ControlServer {
|
|
|
1368
1385
|
at: Date.now(),
|
|
1369
1386
|
version: proxyVersion,
|
|
1370
1387
|
});
|
|
1371
|
-
logLine("proxy.registered", {
|
|
1388
|
+
logLine("proxy.registered", {
|
|
1389
|
+
server: serverName,
|
|
1390
|
+
pid: body.pid,
|
|
1391
|
+
version: proxyVersion,
|
|
1392
|
+
proxy: typeof body.proxyId === "string" ? body.proxyId.slice(-8) : undefined,
|
|
1393
|
+
});
|
|
1372
1394
|
// A half-upgraded machine is the update failure that looks like success:
|
|
1373
1395
|
// `npm i -g` replaced the package, but a config still points at an older
|
|
1374
1396
|
// copy, or this hooks process predates the upgrade and was never
|
|
@@ -1413,7 +1435,13 @@ export class ControlServer {
|
|
|
1413
1435
|
if (!this.replay.enabled)
|
|
1414
1436
|
return {};
|
|
1415
1437
|
const hold = Number(body.holdMs);
|
|
1416
|
-
const
|
|
1438
|
+
const startedAt = Number(body.startedAt);
|
|
1439
|
+
const pid = Number(body.pid);
|
|
1440
|
+
const work = await this.replay.work.waitForWork(serverName, Number.isFinite(hold) && hold > 0 ? Math.min(hold, POLL_HOLD_MS) : POLL_HOLD_MS, signal, {
|
|
1441
|
+
proxyId: typeof body.proxyId === "string" ? body.proxyId : undefined,
|
|
1442
|
+
startedAt: Number.isFinite(startedAt) ? startedAt : undefined,
|
|
1443
|
+
pid: Number.isFinite(pid) ? pid : undefined,
|
|
1444
|
+
});
|
|
1417
1445
|
return work ? { work } : {};
|
|
1418
1446
|
}
|
|
1419
1447
|
/** `POST /proxy/result` — the answer to one dispatched `tools/call`. */
|
|
@@ -1478,6 +1506,7 @@ export class ControlServer {
|
|
|
1478
1506
|
* hooks produces and is a complete MCP step in its own right.
|
|
1479
1507
|
*/
|
|
1480
1508
|
onProxyStep(report) {
|
|
1509
|
+
this.learnProxySession(report);
|
|
1481
1510
|
const session = this.sessionForProxyStep();
|
|
1482
1511
|
const run = this.ensureRun(session);
|
|
1483
1512
|
this.wrapped.add(report.serverName);
|
|
@@ -1534,6 +1563,23 @@ export class ControlServer {
|
|
|
1534
1563
|
});
|
|
1535
1564
|
return { stepIndex: pair.selected, merged: false };
|
|
1536
1565
|
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Tie the reporting proxy to the session whose hook minted the step's call id,
|
|
1568
|
+
* so that session's replay steps run on this proxy's upstream and not on
|
|
1569
|
+
* another session's (replay/executor.ts). Searches every session, not just the
|
|
1570
|
+
* one {@link sessionForProxyStep} records into: with two sessions open, that
|
|
1571
|
+
* guess is exactly what cannot be trusted.
|
|
1572
|
+
*/
|
|
1573
|
+
learnProxySession(report) {
|
|
1574
|
+
if (!report.proxyId || !report.callId)
|
|
1575
|
+
return;
|
|
1576
|
+
for (const session of this.sessions.values()) {
|
|
1577
|
+
if (session.run?.correlations.has(report.callId)) {
|
|
1578
|
+
this.replay.work.bindProxy(report.proxyId, session.sessionId);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1537
1583
|
/**
|
|
1538
1584
|
* Which session a proxy's step belongs to.
|
|
1539
1585
|
*
|
package/dist/proxy/session.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface ProxySessionOptions {
|
|
|
47
47
|
}
|
|
48
48
|
export declare class ProxySession {
|
|
49
49
|
readonly serverName: string;
|
|
50
|
+
/** This proxy process, as the control server tells proxies apart (replay/executor.ts). */
|
|
51
|
+
readonly proxyId: string;
|
|
50
52
|
private readonly opts;
|
|
51
53
|
private tierValue;
|
|
52
54
|
private control?;
|
package/dist/proxy/session.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* flushed into whichever owner wins. Nothing is lost to the race, and nothing
|
|
22
22
|
* blocks: relaying never waits on this.
|
|
23
23
|
*/
|
|
24
|
+
import { randomUUID } from "node:crypto";
|
|
24
25
|
import { hostname } from "node:os";
|
|
25
26
|
import { ControlClient } from "../control/client.js";
|
|
26
27
|
import { resolveControl } from "../control/discovery.js";
|
|
@@ -38,6 +39,8 @@ import { packageVersion } from "../util/version.js";
|
|
|
38
39
|
export const DISCOVERY_WINDOW_MS = 5_000;
|
|
39
40
|
export class ProxySession {
|
|
40
41
|
serverName;
|
|
42
|
+
/** This proxy process, as the control server tells proxies apart (replay/executor.ts). */
|
|
43
|
+
proxyId = "birproxy_" + randomUUID();
|
|
41
44
|
opts;
|
|
42
45
|
tierValue = "pending";
|
|
43
46
|
control;
|
|
@@ -100,7 +103,11 @@ export class ProxySession {
|
|
|
100
103
|
await this.becomeStandalone(`no control server within ${window}ms`);
|
|
101
104
|
return;
|
|
102
105
|
}
|
|
103
|
-
const client = new ControlClient(found.url, found.token
|
|
106
|
+
const client = new ControlClient(found.url, found.token, undefined, {
|
|
107
|
+
proxyId: this.proxyId,
|
|
108
|
+
startedAt: this.startedAt,
|
|
109
|
+
pid: process.pid,
|
|
110
|
+
});
|
|
104
111
|
const registered = await client.register({
|
|
105
112
|
serverName: this.serverName,
|
|
106
113
|
pid: process.pid,
|
|
@@ -158,6 +158,12 @@ export interface ReplayState {
|
|
|
158
158
|
retired: boolean;
|
|
159
159
|
/** What armed it: the prompt, or a ReAct iteration's intent (fallbk.md). */
|
|
160
160
|
armedBy: "prompt" | "intent";
|
|
161
|
+
/**
|
|
162
|
+
* The Claude Code session this plan serves, set by the control server. Its
|
|
163
|
+
* steps run on that session's own proxies (executor.ts). Absent for `bir
|
|
164
|
+
* replay`, which has no session and takes the newest proxy.
|
|
165
|
+
*/
|
|
166
|
+
sessionId?: string;
|
|
161
167
|
/**
|
|
162
168
|
* Which kind of row was handed out (segmented.md R-OUT-7). A `segment` is a
|
|
163
169
|
* named sub-task of a recording rather than a whole task, and it is judged
|
|
@@ -616,7 +616,7 @@ export class ReplayController {
|
|
|
616
616
|
const deadline = Date.now() + this.budgets.planMs;
|
|
617
617
|
let result;
|
|
618
618
|
try {
|
|
619
|
-
result = await plan.runToCompletion(this.executeStep(), state.sourceRun ? (entry) => state.sourceRun.outputFor(entry) : undefined, this.observeStep(state), deadline, MAX_REPLAY_REASON, { stopOnFailure: true });
|
|
619
|
+
result = await plan.runToCompletion(this.executeStep(state), state.sourceRun ? (entry) => state.sourceRun.outputFor(entry) : undefined, this.observeStep(state), deadline, MAX_REPLAY_REASON, { stopOnFailure: true });
|
|
620
620
|
}
|
|
621
621
|
catch (err) {
|
|
622
622
|
// The scenario's own logic failed. Retire and let the model do the work.
|
|
@@ -889,7 +889,7 @@ export class ReplayController {
|
|
|
889
889
|
}
|
|
890
890
|
const trace = [];
|
|
891
891
|
try {
|
|
892
|
-
const result = await plan.runToCompletion(this.executeStep(), state.sourceRun ? (entry) => state.sourceRun.outputFor(entry) : undefined, (info) => {
|
|
892
|
+
const result = await plan.runToCompletion(this.executeStep(state), state.sourceRun ? (entry) => state.sourceRun.outputFor(entry) : undefined, (info) => {
|
|
893
893
|
this.observeStep(state)(info);
|
|
894
894
|
trace.push({
|
|
895
895
|
step: info.step.stepIndex,
|
|
@@ -1023,7 +1023,7 @@ export class ReplayController {
|
|
|
1023
1023
|
this.retire(state, undefined);
|
|
1024
1024
|
let composed;
|
|
1025
1025
|
try {
|
|
1026
|
-
composed = await plan.composeBundle(MAX_REPLAY_REASON, this.executeStep(), state.sourceRun ? (step) => state.sourceRun.outputFor(step) : undefined, this.observeStep(state),
|
|
1026
|
+
composed = await plan.composeBundle(MAX_REPLAY_REASON, this.executeStep(state), state.sourceRun ? (step) => state.sourceRun.outputFor(step) : undefined, this.observeStep(state),
|
|
1027
1027
|
// A parked step ends the bundle short of the task (fallbk.md D3).
|
|
1028
1028
|
{ handover: plan.stopsEarly() });
|
|
1029
1029
|
}
|
|
@@ -1175,13 +1175,13 @@ export class ReplayController {
|
|
|
1175
1175
|
* under which a recorded output may stand in. A tool that ran and failed
|
|
1176
1176
|
* resolves with its failure as the response, exactly as it would in a session.
|
|
1177
1177
|
*/
|
|
1178
|
-
executeStep() {
|
|
1178
|
+
executeStep(state) {
|
|
1179
1179
|
return async (step, input) => {
|
|
1180
1180
|
const mcp = parseQualifiedName(step.toolName ?? "");
|
|
1181
1181
|
if (!mcp) {
|
|
1182
1182
|
throw new Error(`${step.toolName} is not an MCP tool — it can only run in the session`);
|
|
1183
1183
|
}
|
|
1184
|
-
const result = await this.work.call(mcp.serverName, mcp.toolName, input, this.budgets.stepMs);
|
|
1184
|
+
const result = await this.work.call(mcp.serverName, mcp.toolName, input, this.budgets.stepMs, { sessionId: state.sessionId });
|
|
1185
1185
|
// Serialize exactly as the proxy records it, or `toolOutputLogic` — which
|
|
1186
1186
|
// was authored against that shape — silently derives nothing (§7.2).
|
|
1187
1187
|
return serializeCapped(redact(result));
|
|
@@ -18,6 +18,36 @@
|
|
|
18
18
|
* A proxy that never polls (an old build, or one started without `BIR_REPLAY`)
|
|
19
19
|
* is not an error: `call` rejects with `no_proxy`, and the caller falls back to
|
|
20
20
|
* the step's recorded output.
|
|
21
|
+
*
|
|
22
|
+
* WHICH PROXY. Several proxies can poll for the same server at once — one per
|
|
23
|
+
* Claude Code session in the project, and a session's proxy outlives a
|
|
24
|
+
* `bir-hooks` restart by simply re-polling. They are NOT interchangeable: each
|
|
25
|
+
* holds its own upstream, and another session's upstream may be in any state (a
|
|
26
|
+
* database connection a benchmark reset killed, a browser on another page).
|
|
27
|
+
* Handing a step to "whoever is parked first" therefore ran it on the oldest
|
|
28
|
+
* proxy in the project, which is precisely the one most likely to be stale. So
|
|
29
|
+
* every proxy polls under its own id, and a step goes to, in order:
|
|
30
|
+
*
|
|
31
|
+
* 1. a proxy bound to the step's session — learned from a correlated step the
|
|
32
|
+
* proxy reported, or from an earlier step of this session it ran;
|
|
33
|
+
* 2. otherwise the newest unbound proxy (latest process start) — a session's
|
|
34
|
+
* proxies are spawned when it starts, so the newest is the best guess for
|
|
35
|
+
* a session that has not called the server yet;
|
|
36
|
+
* 3. otherwise NOBODY. A proxy bound to another session is never used for
|
|
37
|
+
* this one: the step resolves as a tool error, so the plan stops and hands
|
|
38
|
+
* over to the agent, who calls the tool on its own upstream. It does not
|
|
39
|
+
* reject — a rejection lets a recorded output stand in, or skips the step
|
|
40
|
+
* and runs the rest of the plan past it.
|
|
41
|
+
*
|
|
42
|
+
* A session's bindings are released when it ends (a `/clear` starts a new
|
|
43
|
+
* session on the same proxies). Legacy proxies, which send no id, share one
|
|
44
|
+
* record per server and are never bound: they stay a pool anyone may use.
|
|
45
|
+
*
|
|
46
|
+
* `bir replay` has no session, and takes the newest proxy, bound or not.
|
|
47
|
+
*
|
|
48
|
+
* The choice is made against proxies that are *present*, parked or not, and the
|
|
49
|
+
* work is then held for that proxy if it is mid-round-trip — never handed to a
|
|
50
|
+
* different one that happens to be parked.
|
|
21
51
|
*/
|
|
22
52
|
/** One dispatched tool call, as the proxy receives it. */
|
|
23
53
|
export interface Work {
|
|
@@ -27,30 +57,47 @@ export interface Work {
|
|
|
27
57
|
arguments: unknown;
|
|
28
58
|
timeoutMs: number;
|
|
29
59
|
}
|
|
60
|
+
/** Who a dispatched step is for — decides which proxy runs it. */
|
|
61
|
+
export interface WorkRoute {
|
|
62
|
+
/** The Claude Code session whose plan this step belongs to. */
|
|
63
|
+
sessionId?: string;
|
|
64
|
+
}
|
|
65
|
+
/** What a polling proxy says about itself. All optional: older proxies send none. */
|
|
66
|
+
export interface PollerIdentity {
|
|
67
|
+
/** Stable for the proxy process's lifetime. */
|
|
68
|
+
proxyId?: string;
|
|
69
|
+
/** When the proxy process started, epoch ms — "newest" is judged by this. */
|
|
70
|
+
startedAt?: number;
|
|
71
|
+
pid?: number;
|
|
72
|
+
}
|
|
30
73
|
/** Rejection reason when no proxy is polling for a server. */
|
|
31
74
|
export declare const NO_PROXY = "no_proxy";
|
|
32
75
|
export declare class ProxyWorkQueue {
|
|
33
|
-
/** Work
|
|
76
|
+
/** Work held for a proxy that was mid-round-trip when it was dispatched, per proxy id. */
|
|
34
77
|
private readonly pending;
|
|
35
78
|
/** Pollers currently parked, per server. */
|
|
36
79
|
private readonly waiters;
|
|
37
80
|
/** Work handed out and awaiting a result. */
|
|
38
81
|
private readonly inFlight;
|
|
39
|
-
/**
|
|
40
|
-
private readonly
|
|
82
|
+
/** Every proxy that has polled, by id. A proxy between polls is still present. */
|
|
83
|
+
private readonly proxies;
|
|
41
84
|
private closed;
|
|
42
85
|
/** Servers a proxy is currently serving — what `bir doctor` reports. */
|
|
43
86
|
pollingServers(): string[];
|
|
44
87
|
/** True when a proxy for `serverName` is available to take work. */
|
|
45
88
|
hasPoller(serverName: string): boolean;
|
|
46
|
-
/**
|
|
47
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Record that `proxyId` serves `sessionId`: it reported a call whose id the
|
|
91
|
+
* session's own hook minted. Proof, so it replaces any earlier binding — a
|
|
92
|
+
* `/clear` starts a new session on the same proxies.
|
|
93
|
+
*/
|
|
94
|
+
bindProxy(proxyId: string, sessionId: string): void;
|
|
48
95
|
/**
|
|
49
96
|
* `POST /proxy/poll`. Resolves with work, or with `undefined` at the poll
|
|
50
|
-
* deadline so the proxy re-polls. Work
|
|
51
|
-
* polls is handed over immediately.
|
|
97
|
+
* deadline so the proxy re-polls. Work held for this proxy while it was
|
|
98
|
+
* between polls is handed over immediately.
|
|
52
99
|
*/
|
|
53
|
-
waitForWork(serverName: string, pollDeadlineMs: number, signal?: AbortSignal): Promise<Work | undefined>;
|
|
100
|
+
waitForWork(serverName: string, pollDeadlineMs: number, signal?: AbortSignal, who?: PollerIdentity): Promise<Work | undefined>;
|
|
54
101
|
/** `POST /proxy/result`. Unknown ids are ignored — a late result after a timeout. */
|
|
55
102
|
complete(workId: string, result?: unknown, error?: string): boolean;
|
|
56
103
|
/**
|
|
@@ -61,16 +108,40 @@ export declare class ProxyWorkQueue {
|
|
|
61
108
|
* "could not be run here", which is exactly the condition under which the
|
|
62
109
|
* caller may substitute a recorded output.
|
|
63
110
|
*/
|
|
64
|
-
call(serverName: string, toolName: string, args: unknown, timeoutMs: number): Promise<unknown>;
|
|
111
|
+
call(serverName: string, toolName: string, args: unknown, timeoutMs: number, route?: WorkRoute): Promise<unknown>;
|
|
65
112
|
/** Remove queued work that timed out, so a later poll never gets stale work. */
|
|
66
113
|
private dropPending;
|
|
67
114
|
/** Fail everything in flight and release every poller. */
|
|
68
115
|
close(): void;
|
|
116
|
+
/** Note a poll: create the proxy's record on first sight, refresh it after. */
|
|
117
|
+
private touch;
|
|
118
|
+
/** Parked now, or polled recently enough to be mid-round-trip. */
|
|
119
|
+
private isLive;
|
|
120
|
+
/** Live proxies for a server, newest first. */
|
|
121
|
+
private present;
|
|
122
|
+
/**
|
|
123
|
+
* The proxy a step for `sessionId` should run on — see the file comment.
|
|
124
|
+
* Undefined when no proxy is live at all; `other_sessions` when every live
|
|
125
|
+
* one belongs to a different session.
|
|
126
|
+
*/
|
|
127
|
+
private pickProxy;
|
|
69
128
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
129
|
+
* Free every proxy bound to `sessionId`: the session ended. After a `/clear`
|
|
130
|
+
* the same proxies serve the next session, which must be able to pick them.
|
|
131
|
+
*/
|
|
132
|
+
releaseSession(sessionId: string): void;
|
|
133
|
+
/**
|
|
134
|
+
* A live parked poller of `proxyId`. Waiters whose request has already gone
|
|
135
|
+
* are discarded rather than handed work they can never run.
|
|
72
136
|
*/
|
|
73
137
|
private takeWaiter;
|
|
138
|
+
/**
|
|
139
|
+
* A proxy whose poll request closed has, almost always, exited. Forget it
|
|
140
|
+
* unless another of its polls is still parked, so it is never picked for a
|
|
141
|
+
* minute after it died. A proxy that merely lost one request re-registers on
|
|
142
|
+
* its next poll.
|
|
143
|
+
*/
|
|
144
|
+
private forgetIfGone;
|
|
74
145
|
private removeWaiter;
|
|
75
146
|
/** Drop a waiter's timer and abort listener. Idempotent. */
|
|
76
147
|
private detach;
|