@rallycry/conveyor-agent 10.0.5 → 10.1.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/dist/{chunk-S72TAMGA.js → chunk-DAHP6JPD.js} +759 -87
- package/dist/chunk-DAHP6JPD.js.map +1 -0
- package/dist/cli.js +78 -31
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/runtime/entrypoint.sh +58 -27
- package/dist/chunk-S72TAMGA.js.map +0 -1
|
@@ -186,6 +186,7 @@ var AgentConnection = class _AgentConnection {
|
|
|
186
186
|
apiKeyUpdateCallback = null;
|
|
187
187
|
pullBranchCallback = null;
|
|
188
188
|
finalizeSnapshotCallback = null;
|
|
189
|
+
runStartCommandCallback = null;
|
|
189
190
|
earlyPullBranches = [];
|
|
190
191
|
// PTY relay (S5 terminal). Single-slot callbacks, set per PtySession run.
|
|
191
192
|
ptyInputCallback = null;
|
|
@@ -211,6 +212,20 @@ var AgentConnection = class _AgentConnection {
|
|
|
211
212
|
// the model to retry the tool, which is the safe place to decide idempotency.
|
|
212
213
|
static CALL_CONNECT_WAIT_MS = 2e4;
|
|
213
214
|
static CALL_ACK_TIMEOUT_MS = 3e4;
|
|
215
|
+
// ── Proactive socket recycle ───────────────────────────────────────────
|
|
216
|
+
// Cloud Run severs every WebSocket at its request timeout (3600s is the
|
|
217
|
+
// platform ceiling), so a socket that lives past ~60 minutes is killed at a
|
|
218
|
+
// random moment — historically mid-tool-call, which let the spawned CLI
|
|
219
|
+
// abandon its MCP session. Recycle the transport at a QUIET moment (no
|
|
220
|
+
// in-flight RPC) before the platform deadline instead: an engine-level close
|
|
221
|
+
// looks like a transport drop, so Socket.IO's auto-reconnect and the
|
|
222
|
+
// io "reconnect" → reconnectToSession() recovery path run unchanged. Jitter
|
|
223
|
+
// keeps a fleet of pods from recycling in one thundering herd.
|
|
224
|
+
static SOCKET_RECYCLE_BASE_MS = 52 * 60 * 1e3;
|
|
225
|
+
static SOCKET_RECYCLE_JITTER_MS = 4 * 60 * 1e3;
|
|
226
|
+
static SOCKET_RECYCLE_BUSY_POLL_MS = 15e3;
|
|
227
|
+
recycleTimer = null;
|
|
228
|
+
pendingCalls = 0;
|
|
214
229
|
async call(method, payload) {
|
|
215
230
|
const socket = this.socket;
|
|
216
231
|
if (!socket) {
|
|
@@ -218,10 +233,46 @@ var AgentConnection = class _AgentConnection {
|
|
|
218
233
|
`Not connected (method: ${String(method)}, session: ${this.config.sessionId})`
|
|
219
234
|
);
|
|
220
235
|
}
|
|
221
|
-
|
|
222
|
-
|
|
236
|
+
this.pendingCalls++;
|
|
237
|
+
try {
|
|
238
|
+
if (!socket.connected) {
|
|
239
|
+
await this.waitForConnected(socket, _AgentConnection.CALL_CONNECT_WAIT_MS, String(method));
|
|
240
|
+
}
|
|
241
|
+
return await this.emitWithAck(socket, method, payload);
|
|
242
|
+
} finally {
|
|
243
|
+
this.pendingCalls--;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** (Re)arm the recycle timer — called on every successful (re)connect. */
|
|
247
|
+
scheduleSocketRecycle() {
|
|
248
|
+
this.clearSocketRecycle();
|
|
249
|
+
const delay = _AgentConnection.SOCKET_RECYCLE_BASE_MS + Math.random() * _AgentConnection.SOCKET_RECYCLE_JITTER_MS;
|
|
250
|
+
this.armRecycleTimer(delay);
|
|
251
|
+
}
|
|
252
|
+
clearSocketRecycle() {
|
|
253
|
+
if (this.recycleTimer) {
|
|
254
|
+
clearTimeout(this.recycleTimer);
|
|
255
|
+
this.recycleTimer = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
armRecycleTimer(delay) {
|
|
259
|
+
this.recycleTimer = setTimeout(() => {
|
|
260
|
+
this.recycleTimer = null;
|
|
261
|
+
this.attemptSocketRecycle();
|
|
262
|
+
}, delay);
|
|
263
|
+
this.recycleTimer.unref?.();
|
|
264
|
+
}
|
|
265
|
+
attemptSocketRecycle() {
|
|
266
|
+
const socket = this.socket;
|
|
267
|
+
if (!socket?.connected) return;
|
|
268
|
+
if (this.pendingCalls > 0) {
|
|
269
|
+
this.armRecycleTimer(_AgentConnection.SOCKET_RECYCLE_BUSY_POLL_MS);
|
|
270
|
+
return;
|
|
223
271
|
}
|
|
224
|
-
|
|
272
|
+
process.stderr.write(
|
|
273
|
+
"[conveyor-agent] Recycling socket ahead of the platform request timeout\n"
|
|
274
|
+
);
|
|
275
|
+
socket.io.engine?.close?.();
|
|
225
276
|
}
|
|
226
277
|
/** Resolve once `socket` reports connected, or reject after `timeoutMs`. */
|
|
227
278
|
waitForConnected(socket, timeoutMs, method) {
|
|
@@ -343,6 +394,9 @@ var AgentConnection = class _AgentConnection {
|
|
|
343
394
|
this.socket.on("session:finalizeSnapshot", () => {
|
|
344
395
|
this.finalizeSnapshotCallback?.();
|
|
345
396
|
});
|
|
397
|
+
this.socket.on("session:runStartCommand", () => {
|
|
398
|
+
this.runStartCommandCallback?.();
|
|
399
|
+
});
|
|
346
400
|
this.socket.on("pty:input", (data) => {
|
|
347
401
|
if (data.sessionId && data.sessionId !== this.config.sessionId) return;
|
|
348
402
|
this.ptyInputCallback?.(data.data);
|
|
@@ -353,6 +407,7 @@ var AgentConnection = class _AgentConnection {
|
|
|
353
407
|
});
|
|
354
408
|
this.socket.on("connect", () => {
|
|
355
409
|
process.stderr.write("[conveyor-agent] Socket connected\n");
|
|
410
|
+
this.scheduleSocketRecycle();
|
|
356
411
|
if (!settled) {
|
|
357
412
|
settled = true;
|
|
358
413
|
resolve();
|
|
@@ -400,6 +455,7 @@ var AgentConnection = class _AgentConnection {
|
|
|
400
455
|
}
|
|
401
456
|
disconnect() {
|
|
402
457
|
this.stopProactiveTokenRefresh();
|
|
458
|
+
this.clearSocketRecycle();
|
|
403
459
|
void this.flushEvents();
|
|
404
460
|
if (this.socket) {
|
|
405
461
|
this.socket.io.reconnection(false);
|
|
@@ -568,6 +624,9 @@ var AgentConnection = class _AgentConnection {
|
|
|
568
624
|
onFinalizeSnapshot(callback) {
|
|
569
625
|
this.finalizeSnapshotCallback = callback;
|
|
570
626
|
}
|
|
627
|
+
onRunStartCommand(callback) {
|
|
628
|
+
this.runStartCommandCallback = callback;
|
|
629
|
+
}
|
|
571
630
|
// ── PTY relay (S5 Connected-TUI terminal) ──────────────────────────
|
|
572
631
|
/**
|
|
573
632
|
* Forward a raw chunk of terminal output to the S2 relay (fire-and-forget).
|
|
@@ -583,6 +642,17 @@ var AgentConnection = class _AgentConnection {
|
|
|
583
642
|
}).catch(() => {
|
|
584
643
|
});
|
|
585
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Report that the interactive CLI process for this session has died and no
|
|
647
|
+
* respawn is imminent (fire-and-forget). The server clears the scrollback
|
|
648
|
+
* ring and broadcasts pty:ended so clients hide the Connected-TUI tab. Old
|
|
649
|
+
* servers that don't know the method reject harmlessly.
|
|
650
|
+
*/
|
|
651
|
+
sendPtyEnded() {
|
|
652
|
+
if (!this.socket) return;
|
|
653
|
+
void this.call("ptyEnded", { sessionId: this.config.sessionId }).catch(() => {
|
|
654
|
+
});
|
|
655
|
+
}
|
|
586
656
|
/** Subscribe to relayed keystrokes. Returns an unsubscribe fn. */
|
|
587
657
|
onPtyInput(handler) {
|
|
588
658
|
this.ptyInputCallback = handler;
|
|
@@ -2486,6 +2556,9 @@ var GetUiCliHistoryRequestSchema = z.object({
|
|
|
2486
2556
|
var GetActivePtySessionRequestSchema = z.object({
|
|
2487
2557
|
taskId: z.string()
|
|
2488
2558
|
});
|
|
2559
|
+
var ListActivePtySessionsRequestSchema = z.object({
|
|
2560
|
+
taskId: z.string()
|
|
2561
|
+
});
|
|
2489
2562
|
var SendSoftStopRequestSchema = z.object({
|
|
2490
2563
|
taskId: z.string()
|
|
2491
2564
|
});
|
|
@@ -2548,6 +2621,9 @@ var PtyOutputRequestSchema = z.object({
|
|
|
2548
2621
|
cols: z.number().int().positive().max(PTY_MAX_DIMENSION).optional(),
|
|
2549
2622
|
rows: z.number().int().positive().max(PTY_MAX_DIMENSION).optional()
|
|
2550
2623
|
});
|
|
2624
|
+
var PtyEndedRequestSchema = z.object({
|
|
2625
|
+
sessionId: z.string()
|
|
2626
|
+
});
|
|
2551
2627
|
var PtyInputRequestSchema = z.object({
|
|
2552
2628
|
sessionId: z.string(),
|
|
2553
2629
|
data: z.string().max(PTY_FRAME_MAX_CHARS)
|
|
@@ -2866,6 +2942,7 @@ var PM_CHAT_HISTORY_LIMIT = 40;
|
|
|
2866
2942
|
var AGENT_CHAT_HISTORY_FETCH_LIMIT = Math.max(TASK_CHAT_HISTORY_LIMIT, PM_CHAT_HISTORY_LIMIT) + 10;
|
|
2867
2943
|
|
|
2868
2944
|
// src/runner/mode-controller.ts
|
|
2945
|
+
var FRESH_AUTO_STATUSES = /* @__PURE__ */ new Set(["Planning", "Open"]);
|
|
2869
2946
|
var ModeController = class {
|
|
2870
2947
|
_mode;
|
|
2871
2948
|
_hasExitedPlanMode = false;
|
|
@@ -2949,12 +3026,25 @@ var ModeController = class {
|
|
|
2949
3026
|
}
|
|
2950
3027
|
return this._mode;
|
|
2951
3028
|
}
|
|
2952
|
-
/**
|
|
3029
|
+
/**
|
|
3030
|
+
* A FRESH auto task runs the read-only plan turn first (`--permission-mode plan`):
|
|
3031
|
+
* resolveInitialMode leaves it in `auto` with `_hasExitedPlanMode = false`, and
|
|
3032
|
+
* once the agent finishes it calls ExitPlanMode → building. Only tasks that have
|
|
3033
|
+
* already progressed past the fresh stage bypass planning and build immediately
|
|
3034
|
+
* with `--dangerously-skip-permissions`:
|
|
3035
|
+
*
|
|
3036
|
+
* - a build already underway (status past `Planning`/`Open`),
|
|
3037
|
+
* - a PR already open (safety net if status lags),
|
|
3038
|
+
* - a release (booted `InProgress`), or
|
|
3039
|
+
* - a pod restart mid-build (DB `agentMode` stays `"auto"` but status advanced).
|
|
3040
|
+
*
|
|
3041
|
+
* Runtime Build-after-Discovery never reaches this gate as `auto` — the server's
|
|
3042
|
+
* `resolveModeToSend` rewrites it to `"building"` for any non-`Planning` task.
|
|
3043
|
+
*/
|
|
2953
3044
|
canBypassPlanning(context) {
|
|
2954
3045
|
if (this._mode !== "auto") return false;
|
|
2955
|
-
if (context.status
|
|
2956
|
-
|
|
2957
|
-
return false;
|
|
3046
|
+
if (!FRESH_AUTO_STATUSES.has(context.status)) return true;
|
|
3047
|
+
return !!context.githubPRUrl;
|
|
2958
3048
|
}
|
|
2959
3049
|
// ── Mode transitions ───────────────────────────────────────────────
|
|
2960
3050
|
/** Handle mode change from server */
|
|
@@ -3017,7 +3107,8 @@ var DEFAULT_LIFECYCLE_CONFIG = {
|
|
|
3017
3107
|
dormantTimeoutMs: 60 * 60 * 1e3,
|
|
3018
3108
|
heartbeatIntervalMs: 3e4,
|
|
3019
3109
|
tokenRefreshIntervalMs: 45 * 60 * 1e3,
|
|
3020
|
-
gitFlushIntervalMs: 2 * 60 * 1e3
|
|
3110
|
+
gitFlushIntervalMs: 2 * 60 * 1e3,
|
|
3111
|
+
usageSampleIntervalMs: 5 * 60 * 1e3
|
|
3021
3112
|
};
|
|
3022
3113
|
var Lifecycle = class {
|
|
3023
3114
|
config;
|
|
@@ -3028,6 +3119,7 @@ var Lifecycle = class {
|
|
|
3028
3119
|
idleCheckInterval = null;
|
|
3029
3120
|
dormantTimer = null;
|
|
3030
3121
|
gitFlushTimer = null;
|
|
3122
|
+
usageSampleTimer = null;
|
|
3031
3123
|
constructor(config, callbacks) {
|
|
3032
3124
|
this.config = config;
|
|
3033
3125
|
this.callbacks = callbacks;
|
|
@@ -3073,6 +3165,21 @@ var Lifecycle = class {
|
|
|
3073
3165
|
this.gitFlushTimer = null;
|
|
3074
3166
|
}
|
|
3075
3167
|
}
|
|
3168
|
+
// ── Claude key usage sampling ─────────────────────────────────────
|
|
3169
|
+
startUsageSample() {
|
|
3170
|
+
this.stopUsageSample();
|
|
3171
|
+
if (this.config.usageSampleIntervalMs <= 0) return;
|
|
3172
|
+
this.callbacks.onUsageSample();
|
|
3173
|
+
this.usageSampleTimer = setInterval(() => {
|
|
3174
|
+
this.callbacks.onUsageSample();
|
|
3175
|
+
}, this.config.usageSampleIntervalMs);
|
|
3176
|
+
}
|
|
3177
|
+
stopUsageSample() {
|
|
3178
|
+
if (this.usageSampleTimer) {
|
|
3179
|
+
clearInterval(this.usageSampleTimer);
|
|
3180
|
+
this.usageSampleTimer = null;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3076
3183
|
// ── Idle timer ─────────────────────────────────────────────────────
|
|
3077
3184
|
startIdleTimer() {
|
|
3078
3185
|
this.clearIdleTimers();
|
|
@@ -3109,6 +3216,7 @@ var Lifecycle = class {
|
|
|
3109
3216
|
this.stopHeartbeat();
|
|
3110
3217
|
this.stopTokenRefresh();
|
|
3111
3218
|
this.stopGitFlush();
|
|
3219
|
+
this.stopUsageSample();
|
|
3112
3220
|
this.clearIdleTimers();
|
|
3113
3221
|
this.cancelDormantTimer();
|
|
3114
3222
|
}
|
|
@@ -3168,7 +3276,7 @@ var ClaudeCodeHarness = class {
|
|
|
3168
3276
|
};
|
|
3169
3277
|
|
|
3170
3278
|
// src/harness/pty/session.ts
|
|
3171
|
-
import { mkdtemp as mkdtemp2, mkdir as mkdir2, rm as rm4
|
|
3279
|
+
import { mkdtemp as mkdtemp2, mkdir as mkdir2, rm as rm4 } from "fs/promises";
|
|
3172
3280
|
import { tmpdir as tmpdir4 } from "os";
|
|
3173
3281
|
import { join as join4, dirname } from "path";
|
|
3174
3282
|
|
|
@@ -3549,6 +3657,14 @@ function buildSpawnArgs(input) {
|
|
|
3549
3657
|
}
|
|
3550
3658
|
return args;
|
|
3551
3659
|
}
|
|
3660
|
+
function spawnOptionsFingerprint(input) {
|
|
3661
|
+
return JSON.stringify([
|
|
3662
|
+
input.model,
|
|
3663
|
+
input.permissionMode,
|
|
3664
|
+
input.appendSystemPrompt ?? "",
|
|
3665
|
+
input.cwd
|
|
3666
|
+
]);
|
|
3667
|
+
}
|
|
3552
3668
|
var ANSI_CSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[ -/]*[@-~]`, "g");
|
|
3553
3669
|
function cleanTerminalOutput(raw, maxChars = 1200) {
|
|
3554
3670
|
const noAnsi = raw.replace(ANSI_CSI, "");
|
|
@@ -3641,6 +3757,7 @@ import { join as join3 } from "path";
|
|
|
3641
3757
|
import { randomBytes } from "crypto";
|
|
3642
3758
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3643
3759
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3760
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
3644
3761
|
|
|
3645
3762
|
// src/harness/pty/mcp-server.ts
|
|
3646
3763
|
var PtyMcpServer = class {
|
|
@@ -3662,6 +3779,7 @@ var PtyMcpServer = class {
|
|
|
3662
3779
|
|
|
3663
3780
|
// src/harness/pty/tool-server.ts
|
|
3664
3781
|
var LOOPBACK = "127.0.0.1";
|
|
3782
|
+
var MAX_SESSIONS = 16;
|
|
3665
3783
|
var PtyToolServer = class {
|
|
3666
3784
|
constructor(name, tools) {
|
|
3667
3785
|
this.name = name;
|
|
@@ -3670,22 +3788,11 @@ var PtyToolServer = class {
|
|
|
3670
3788
|
name;
|
|
3671
3789
|
tools;
|
|
3672
3790
|
http = null;
|
|
3673
|
-
|
|
3674
|
-
mcp = null;
|
|
3791
|
+
sessions = /* @__PURE__ */ new Map();
|
|
3675
3792
|
token = randomBytes(24).toString("base64url");
|
|
3676
3793
|
async start() {
|
|
3677
|
-
const mcp = new McpServer({ name: this.name, version: "1.0.0" });
|
|
3678
|
-
const register = mcp.tool.bind(mcp);
|
|
3679
|
-
for (const tool2 of this.tools) {
|
|
3680
|
-
register(tool2.name, tool2.description, tool2.schema, (args) => tool2.handler(args));
|
|
3681
|
-
}
|
|
3682
|
-
const transport = new StreamableHTTPServerTransport({
|
|
3683
|
-
sessionIdGenerator: () => randomBytes(16).toString("hex"),
|
|
3684
|
-
enableJsonResponse: true
|
|
3685
|
-
});
|
|
3686
|
-
await mcp.connect(transport);
|
|
3687
3794
|
const server = createServer2((req, res) => {
|
|
3688
|
-
void this.handle(req, res
|
|
3795
|
+
void this.handle(req, res);
|
|
3689
3796
|
});
|
|
3690
3797
|
await new Promise((resolve, reject) => {
|
|
3691
3798
|
server.once("error", reject);
|
|
@@ -3694,33 +3801,84 @@ var PtyToolServer = class {
|
|
|
3694
3801
|
const address = server.address();
|
|
3695
3802
|
const port = address && typeof address === "object" ? address.port : 0;
|
|
3696
3803
|
this.http = server;
|
|
3697
|
-
this.transport = transport;
|
|
3698
|
-
this.mcp = mcp;
|
|
3699
3804
|
return { url: `http://${LOOPBACK}:${port}/mcp`, token: this.token };
|
|
3700
3805
|
}
|
|
3701
|
-
|
|
3806
|
+
/** Fresh McpServer with every tool registered — one per MCP session. */
|
|
3807
|
+
buildMcpServer() {
|
|
3808
|
+
const mcp = new McpServer({ name: this.name, version: "1.0.0" });
|
|
3809
|
+
const register = mcp.tool.bind(mcp);
|
|
3810
|
+
for (const tool2 of this.tools) {
|
|
3811
|
+
register(tool2.name, tool2.description, tool2.schema, (args) => tool2.handler(args));
|
|
3812
|
+
}
|
|
3813
|
+
return mcp;
|
|
3814
|
+
}
|
|
3815
|
+
async handle(req, res) {
|
|
3702
3816
|
if (req.headers.authorization !== `Bearer ${this.token}`) {
|
|
3703
3817
|
res.writeHead(401).end();
|
|
3704
3818
|
return;
|
|
3705
3819
|
}
|
|
3706
3820
|
try {
|
|
3707
|
-
|
|
3821
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
3822
|
+
if (typeof sessionId === "string") {
|
|
3823
|
+
const session = this.sessions.get(sessionId);
|
|
3824
|
+
if (!session) {
|
|
3825
|
+
jsonRpcError(res, 404, -32001, "Session not found");
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
await session.transport.handleRequest(req, res);
|
|
3829
|
+
return;
|
|
3830
|
+
}
|
|
3831
|
+
if (req.method !== "POST") {
|
|
3832
|
+
jsonRpcError(res, 400, -32600, "Bad Request: Mcp-Session-Id header is required");
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
const body = await readJsonBody(req);
|
|
3836
|
+
const messages = Array.isArray(body) ? body : [body];
|
|
3837
|
+
if (!messages.some((m) => isInitializeRequest(m))) {
|
|
3838
|
+
jsonRpcError(res, 400, -32600, "Bad Request: Mcp-Session-Id header is required");
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3841
|
+
await this.openSession(req, res, body);
|
|
3708
3842
|
} catch {
|
|
3709
3843
|
if (res.headersSent) res.end();
|
|
3710
3844
|
else res.writeHead(500).end();
|
|
3711
3845
|
}
|
|
3712
3846
|
}
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3847
|
+
/** Connect a fresh transport + McpServer pair and serve the initialize. */
|
|
3848
|
+
async openSession(req, res, parsedBody) {
|
|
3849
|
+
const mcp = this.buildMcpServer();
|
|
3850
|
+
const transport = new StreamableHTTPServerTransport({
|
|
3851
|
+
sessionIdGenerator: () => randomBytes(16).toString("hex"),
|
|
3852
|
+
enableJsonResponse: true,
|
|
3853
|
+
onsessioninitialized: (sid) => {
|
|
3854
|
+
this.sessions.set(sid, { transport, mcp });
|
|
3855
|
+
this.evictOverflow(sid);
|
|
3856
|
+
}
|
|
3857
|
+
});
|
|
3858
|
+
transport.onclose = () => {
|
|
3859
|
+
const sid = transport.sessionId;
|
|
3860
|
+
if (sid && this.sessions.get(sid)?.transport === transport) {
|
|
3861
|
+
this.sessions.delete(sid);
|
|
3862
|
+
}
|
|
3863
|
+
};
|
|
3864
|
+
await mcp.connect(transport);
|
|
3865
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
3866
|
+
}
|
|
3867
|
+
/** Drop oldest abandoned sessions past MAX_SESSIONS (never the newest). */
|
|
3868
|
+
evictOverflow(currentSid) {
|
|
3869
|
+
for (const [sid, session] of this.sessions) {
|
|
3870
|
+
if (this.sessions.size <= MAX_SESSIONS) break;
|
|
3871
|
+
if (sid === currentSid) continue;
|
|
3872
|
+
this.sessions.delete(sid);
|
|
3873
|
+
void closeSession(session);
|
|
3717
3874
|
}
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3875
|
+
}
|
|
3876
|
+
async close() {
|
|
3877
|
+
const sessions = [...this.sessions.values()];
|
|
3878
|
+
this.sessions.clear();
|
|
3879
|
+
for (const session of sessions) {
|
|
3880
|
+
await closeSession(session);
|
|
3722
3881
|
}
|
|
3723
|
-
this.mcp = null;
|
|
3724
3882
|
const http = this.http;
|
|
3725
3883
|
this.http = null;
|
|
3726
3884
|
if (http) {
|
|
@@ -3730,6 +3888,27 @@ var PtyToolServer = class {
|
|
|
3730
3888
|
}
|
|
3731
3889
|
}
|
|
3732
3890
|
};
|
|
3891
|
+
async function closeSession(session) {
|
|
3892
|
+
try {
|
|
3893
|
+
await session.transport.close();
|
|
3894
|
+
} catch {
|
|
3895
|
+
}
|
|
3896
|
+
try {
|
|
3897
|
+
await session.mcp.close();
|
|
3898
|
+
} catch {
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
function jsonRpcError(res, status, code, message) {
|
|
3902
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
3903
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code, message }, id: null }));
|
|
3904
|
+
}
|
|
3905
|
+
async function readJsonBody(req) {
|
|
3906
|
+
const chunks = [];
|
|
3907
|
+
for await (const chunk of req) {
|
|
3908
|
+
chunks.push(chunk);
|
|
3909
|
+
}
|
|
3910
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
3911
|
+
}
|
|
3733
3912
|
async function startToolServers(mcpServers, tempDir) {
|
|
3734
3913
|
const servers = [];
|
|
3735
3914
|
const config = {};
|
|
@@ -3747,8 +3926,23 @@ async function startToolServers(mcpServers, tempDir) {
|
|
|
3747
3926
|
return { servers, mcpConfigPath };
|
|
3748
3927
|
}
|
|
3749
3928
|
|
|
3750
|
-
// src/harness/pty/
|
|
3929
|
+
// src/harness/pty/pty-support.ts
|
|
3930
|
+
import { stat as stat3 } from "fs/promises";
|
|
3751
3931
|
var MAX_DIAGNOSTIC_OUTPUT = 4e3;
|
|
3932
|
+
var MAX_BETWEEN_TURN_BUFFER = 500;
|
|
3933
|
+
var SUBMIT_SETTLE_MS = 300;
|
|
3934
|
+
var SUBMIT_NUDGE_INTERVAL_MS = 2e3;
|
|
3935
|
+
var SUBMIT_NUDGE_MAX_PRESSES = 5;
|
|
3936
|
+
var SUBMIT_NUDGE_SLOW_INTERVAL_MS = 5e3;
|
|
3937
|
+
var SUBMIT_NUDGE_WINDOW_MS = 9e4;
|
|
3938
|
+
function turnOptionsFrom(options) {
|
|
3939
|
+
return {
|
|
3940
|
+
canUseTool: options.canUseTool,
|
|
3941
|
+
promptDelivery: options.promptDelivery,
|
|
3942
|
+
planDialogAutoAccept: options.planDialogAutoAccept,
|
|
3943
|
+
abortController: options.abortController
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3752
3946
|
function isRecord2(value) {
|
|
3753
3947
|
return typeof value === "object" && value !== null;
|
|
3754
3948
|
}
|
|
@@ -3771,16 +3965,13 @@ function inheritedEnv(socketPath) {
|
|
|
3771
3965
|
if (typeof value === "string") env[key] = value;
|
|
3772
3966
|
}
|
|
3773
3967
|
env.CONVEYOR_HOOK_SOCKET = socketPath;
|
|
3968
|
+
env.MCP_TIMEOUT ??= "60000";
|
|
3969
|
+
env.MCP_TOOL_TIMEOUT ??= "180000";
|
|
3774
3970
|
return env;
|
|
3775
3971
|
}
|
|
3776
3972
|
function buildPromptBytes(text) {
|
|
3777
3973
|
return `\x1B[200~${text}\x1B[201~`;
|
|
3778
3974
|
}
|
|
3779
|
-
var SUBMIT_SETTLE_MS = 300;
|
|
3780
|
-
var SUBMIT_NUDGE_INTERVAL_MS = 2e3;
|
|
3781
|
-
var SUBMIT_NUDGE_MAX_PRESSES = 5;
|
|
3782
|
-
var SUBMIT_NUDGE_SLOW_INTERVAL_MS = 5e3;
|
|
3783
|
-
var SUBMIT_NUDGE_WINDOW_MS = 9e4;
|
|
3784
3975
|
function sleep3(ms) {
|
|
3785
3976
|
return new Promise((resolve) => {
|
|
3786
3977
|
setTimeout(resolve, ms);
|
|
@@ -3812,18 +4003,41 @@ function parseUserQuestions(input) {
|
|
|
3812
4003
|
}
|
|
3813
4004
|
return questions;
|
|
3814
4005
|
}
|
|
4006
|
+
|
|
4007
|
+
// src/harness/pty/session.ts
|
|
3815
4008
|
var PtySession = class {
|
|
3816
4009
|
constructor(prompt, options, resume, bridge) {
|
|
3817
|
-
this.prompt = prompt;
|
|
3818
4010
|
this.options = options;
|
|
3819
4011
|
this.resume = resume;
|
|
3820
4012
|
this.bridge = bridge;
|
|
4013
|
+
this.turnPrompt = prompt;
|
|
4014
|
+
this.turn = turnOptionsFrom(options);
|
|
3821
4015
|
}
|
|
3822
|
-
prompt;
|
|
3823
4016
|
options;
|
|
3824
4017
|
resume;
|
|
3825
4018
|
bridge;
|
|
3826
|
-
|
|
4019
|
+
// The current turn's event stream. Null between turns (process parked) — the
|
|
4020
|
+
// process outlives any single turn, so the queue is per-turn, not per-process.
|
|
4021
|
+
activeQueue = null;
|
|
4022
|
+
// The drain generator for the current turn, captured at turn-start and bound
|
|
4023
|
+
// to that turn's queue instance. events() returns THIS, not a fresh drain of
|
|
4024
|
+
// activeQueue — a turn can end (closing + nulling activeQueue) synchronously
|
|
4025
|
+
// within beginPassiveTurn (a buffered result) before the consumer calls
|
|
4026
|
+
// events(), and the consumer must still drain the already-buffered records.
|
|
4027
|
+
turnStream = null;
|
|
4028
|
+
// Transcript events that arrive with no turn draining (parked window). A
|
|
4029
|
+
// passive turn replays these; beginTurn drops them.
|
|
4030
|
+
betweenTurnBuffer = [];
|
|
4031
|
+
// Leading-edge guard so a burst of parked activity fires onPassiveActivity
|
|
4032
|
+
// once, not per event. Reset when a turn (re)starts.
|
|
4033
|
+
passiveSignaled = false;
|
|
4034
|
+
passiveListener = null;
|
|
4035
|
+
// Set once the CLI process actually exits (distinct from _toreDown, which is
|
|
4036
|
+
// our own teardown). Blocks reuse of a dead process.
|
|
4037
|
+
exited = false;
|
|
4038
|
+
// Whether the turn that just ended did so via a clean transcript `result`
|
|
4039
|
+
// (eligible for parking) vs. an abort/error (must teardown).
|
|
4040
|
+
lastTurnCleanResult = false;
|
|
3827
4041
|
socket = null;
|
|
3828
4042
|
tailer = null;
|
|
3829
4043
|
pty = null;
|
|
@@ -3852,27 +4066,173 @@ var PtySession = class {
|
|
|
3852
4066
|
// Submit-nudge state (see armSubmitNudge).
|
|
3853
4067
|
pendingSubmitNudge = false;
|
|
3854
4068
|
submitNudgeTimer = null;
|
|
4069
|
+
// Per-turn state: the prompt to feed and the per-turn options subset. Both
|
|
4070
|
+
// start from the constructor args (turn 1) and are replaced by beginTurn.
|
|
4071
|
+
turnPrompt;
|
|
4072
|
+
turn;
|
|
3855
4073
|
onIdle(listener) {
|
|
3856
4074
|
this.idleListeners.push(listener);
|
|
3857
4075
|
}
|
|
3858
4076
|
onExit(listener) {
|
|
3859
4077
|
this.exitListeners.push(listener);
|
|
3860
4078
|
}
|
|
4079
|
+
/** Subscribe to parked-window transcript activity (human typed into the idle
|
|
4080
|
+
* TUI). Returns unsubscribe. Only one listener is retained. */
|
|
4081
|
+
onPassiveActivity(listener) {
|
|
4082
|
+
this.passiveListener = listener;
|
|
4083
|
+
return () => {
|
|
4084
|
+
if (this.passiveListener === listener) this.passiveListener = null;
|
|
4085
|
+
};
|
|
4086
|
+
}
|
|
3861
4087
|
get isToreDown() {
|
|
3862
4088
|
return this._toreDown;
|
|
3863
4089
|
}
|
|
4090
|
+
/** True once the underlying CLI process has exited. */
|
|
4091
|
+
get hasExited() {
|
|
4092
|
+
return this.exited;
|
|
4093
|
+
}
|
|
4094
|
+
/** True when the last turn ended via a clean transcript result (parkable). */
|
|
4095
|
+
get endedCleanly() {
|
|
4096
|
+
return this.lastTurnCleanResult;
|
|
4097
|
+
}
|
|
4098
|
+
/** The deterministic session UUID this process is bound to. */
|
|
4099
|
+
get sessionUuid() {
|
|
4100
|
+
return this.resume ?? this.options.sessionId;
|
|
4101
|
+
}
|
|
4102
|
+
/** Fingerprint of the spawn-time options a reused process cannot change. */
|
|
4103
|
+
get spawnFingerprint() {
|
|
4104
|
+
return spawnOptionsFingerprint({
|
|
4105
|
+
model: this.options.model,
|
|
4106
|
+
permissionMode: this.options.permissionMode,
|
|
4107
|
+
...this.options.appendSystemPrompt ? { appendSystemPrompt: this.options.appendSystemPrompt } : {},
|
|
4108
|
+
cwd: this.options.cwd
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
/**
|
|
4112
|
+
* Whether this parked process can serve a turn wanting `resume`/`fingerprint`.
|
|
4113
|
+
* Requires: live (not torn down / not exited), idle (no turn draining), a
|
|
4114
|
+
* matching resume target, and matching spawn options.
|
|
4115
|
+
*/
|
|
4116
|
+
canReuse(resume, fingerprint) {
|
|
4117
|
+
return !this._toreDown && !this.exited && this.activeQueue === null && resume !== void 0 && resume === this.sessionUuid && fingerprint === this.spawnFingerprint;
|
|
4118
|
+
}
|
|
3864
4119
|
get hookSocketPath() {
|
|
3865
4120
|
return this.socket ? this.socket.socketPath : null;
|
|
3866
4121
|
}
|
|
3867
4122
|
events() {
|
|
3868
|
-
|
|
4123
|
+
if (!this.turnStream) {
|
|
4124
|
+
const empty = new AsyncEventQueue();
|
|
4125
|
+
empty.close();
|
|
4126
|
+
return empty.drain();
|
|
4127
|
+
}
|
|
4128
|
+
return this.turnStream;
|
|
4129
|
+
}
|
|
4130
|
+
/**
|
|
4131
|
+
* Route a harness event to the current turn's queue, or — when no turn is
|
|
4132
|
+
* draining (the process is parked) — into the between-turn buffer, firing the
|
|
4133
|
+
* passive-activity signal once per parked window.
|
|
4134
|
+
*/
|
|
4135
|
+
pushEvent(event) {
|
|
4136
|
+
if (this.activeQueue) {
|
|
4137
|
+
this.activeQueue.push(event);
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
this.betweenTurnBuffer.push(event);
|
|
4141
|
+
if (this.betweenTurnBuffer.length > MAX_BETWEEN_TURN_BUFFER) {
|
|
4142
|
+
this.betweenTurnBuffer.shift();
|
|
4143
|
+
}
|
|
4144
|
+
if (!this.passiveSignaled) {
|
|
4145
|
+
this.passiveSignaled = true;
|
|
4146
|
+
this.passiveListener?.();
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
/**
|
|
4150
|
+
* Begin a follow-up turn on the ALREADY-RUNNING process: fresh per-turn state
|
|
4151
|
+
* and queue, rewire the abort listener to this turn's controller, then feed
|
|
4152
|
+
* the prompt into the live pty. The caller must have verified canReuse().
|
|
4153
|
+
*/
|
|
4154
|
+
async beginTurn(prompt, options) {
|
|
4155
|
+
this.turnPrompt = prompt;
|
|
4156
|
+
this.turn = turnOptionsFrom(options);
|
|
4157
|
+
this.resetForTurn();
|
|
4158
|
+
this.betweenTurnBuffer = [];
|
|
4159
|
+
this.activeQueue = new AsyncEventQueue();
|
|
4160
|
+
this.turnStream = this.activeQueue.drain();
|
|
4161
|
+
if (!this.rearmAbort()) return;
|
|
4162
|
+
await this.feedPrompt();
|
|
4163
|
+
}
|
|
4164
|
+
/**
|
|
4165
|
+
* Begin a PASSIVE turn: a human typed into the parked TUI and the CLI is
|
|
4166
|
+
* producing transcript records with no prompt from us. Replay the buffered
|
|
4167
|
+
* records into a fresh queue (a buffered `result` ends the turn at once) and
|
|
4168
|
+
* let live records continue to flow.
|
|
4169
|
+
*/
|
|
4170
|
+
beginPassiveTurn(turn) {
|
|
4171
|
+
this.turn = turn;
|
|
4172
|
+
this.resetForTurn();
|
|
4173
|
+
const buffered = this.betweenTurnBuffer;
|
|
4174
|
+
this.betweenTurnBuffer = [];
|
|
4175
|
+
this.activeQueue = new AsyncEventQueue();
|
|
4176
|
+
this.turnStream = this.activeQueue.drain();
|
|
4177
|
+
if (!this.rearmAbort()) return;
|
|
4178
|
+
for (const event of buffered) {
|
|
4179
|
+
this.activeQueue?.push(event);
|
|
4180
|
+
if (event.type === "result") this.endTurn(true);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
/** Reset per-turn state shared by beginTurn/beginPassiveTurn. */
|
|
4184
|
+
resetForTurn() {
|
|
4185
|
+
this.sawResult = false;
|
|
4186
|
+
this.lastTurnCleanResult = false;
|
|
4187
|
+
this.passiveSignaled = false;
|
|
4188
|
+
this.disarmSubmitNudge();
|
|
4189
|
+
this.disarmPlanDialogAutoAccept();
|
|
4190
|
+
}
|
|
4191
|
+
/**
|
|
4192
|
+
* (Re)register the abort→teardown listener on the current turn's controller,
|
|
4193
|
+
* removing any prior one. Returns false (after tearing down) when the signal
|
|
4194
|
+
* is already aborted, so callers can bail.
|
|
4195
|
+
*/
|
|
4196
|
+
rearmAbort() {
|
|
4197
|
+
if (this.abortHandler && this.turn.abortController) {
|
|
4198
|
+
this.turn.abortController.signal.removeEventListener("abort", this.abortHandler);
|
|
4199
|
+
}
|
|
4200
|
+
this.abortHandler = null;
|
|
4201
|
+
const signal = this.turn.abortController?.signal;
|
|
4202
|
+
if (!signal) return true;
|
|
4203
|
+
if (signal.aborted) {
|
|
4204
|
+
void this.teardown();
|
|
4205
|
+
return false;
|
|
4206
|
+
}
|
|
4207
|
+
this.abortHandler = () => {
|
|
4208
|
+
void this.teardown();
|
|
4209
|
+
};
|
|
4210
|
+
signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
4211
|
+
return true;
|
|
4212
|
+
}
|
|
4213
|
+
/**
|
|
4214
|
+
* End the current turn WITHOUT killing the process: close+null the queue so
|
|
4215
|
+
* the consumer's generator completes, and unhook the (now-finished) turn's
|
|
4216
|
+
* abort listener so a later abort of that controller can't teardown a process
|
|
4217
|
+
* we intend to reuse. The process stays alive (parked) for the next turn.
|
|
4218
|
+
*/
|
|
4219
|
+
endTurn(clean) {
|
|
4220
|
+
this.lastTurnCleanResult = clean;
|
|
4221
|
+
this.activeQueue?.close();
|
|
4222
|
+
this.activeQueue = null;
|
|
4223
|
+
if (this.abortHandler && this.turn.abortController) {
|
|
4224
|
+
this.turn.abortController.signal.removeEventListener("abort", this.abortHandler);
|
|
4225
|
+
this.abortHandler = null;
|
|
4226
|
+
}
|
|
3869
4227
|
}
|
|
3870
4228
|
async start() {
|
|
3871
4229
|
const sessionId = this.resume ?? this.options.sessionId;
|
|
3872
4230
|
if (!sessionId) {
|
|
3873
4231
|
throw new Error("PtySession requires options.sessionId or a resume target");
|
|
3874
4232
|
}
|
|
3875
|
-
|
|
4233
|
+
this.activeQueue = new AsyncEventQueue();
|
|
4234
|
+
this.turnStream = this.activeQueue.drain();
|
|
4235
|
+
const signal = this.turn.abortController?.signal;
|
|
3876
4236
|
if (signal?.aborted) {
|
|
3877
4237
|
await this.teardown();
|
|
3878
4238
|
return;
|
|
@@ -3952,7 +4312,7 @@ var PtySession = class {
|
|
|
3952
4312
|
this.unsubResize?.();
|
|
3953
4313
|
this.unsubResize = null;
|
|
3954
4314
|
if (this.abortHandler) {
|
|
3955
|
-
this.
|
|
4315
|
+
this.turn.abortController?.signal.removeEventListener("abort", this.abortHandler);
|
|
3956
4316
|
this.abortHandler = null;
|
|
3957
4317
|
}
|
|
3958
4318
|
try {
|
|
@@ -3976,7 +4336,8 @@ var PtySession = class {
|
|
|
3976
4336
|
}
|
|
3977
4337
|
this.toolServers = [];
|
|
3978
4338
|
this.mcpConfigPath = null;
|
|
3979
|
-
this.
|
|
4339
|
+
this.activeQueue?.close();
|
|
4340
|
+
this.activeQueue = null;
|
|
3980
4341
|
if (this.tempDir) {
|
|
3981
4342
|
await rm4(this.tempDir, { recursive: true, force: true });
|
|
3982
4343
|
this.tempDir = "";
|
|
@@ -4037,18 +4398,18 @@ var PtySession = class {
|
|
|
4037
4398
|
}
|
|
4038
4399
|
async deliverPrompt(text) {
|
|
4039
4400
|
this.writeStdin(buildPromptBytes(text));
|
|
4040
|
-
if (this.
|
|
4401
|
+
if (this.turn.promptDelivery === "prefill") return;
|
|
4041
4402
|
await sleep3(SUBMIT_SETTLE_MS);
|
|
4042
4403
|
if (this._toreDown) return;
|
|
4043
4404
|
this.writeStdin("\r");
|
|
4044
4405
|
this.armSubmitNudge();
|
|
4045
4406
|
}
|
|
4046
4407
|
async feedPrompt() {
|
|
4047
|
-
if (typeof this.
|
|
4048
|
-
await this.deliverPrompt(this.
|
|
4408
|
+
if (typeof this.turnPrompt === "string") {
|
|
4409
|
+
await this.deliverPrompt(this.turnPrompt);
|
|
4049
4410
|
return;
|
|
4050
4411
|
}
|
|
4051
|
-
for await (const message of this.
|
|
4412
|
+
for await (const message of this.turnPrompt) {
|
|
4052
4413
|
const content = message.message.content;
|
|
4053
4414
|
const text = typeof content === "string" ? content : JSON.stringify(content);
|
|
4054
4415
|
await this.deliverPrompt(text);
|
|
@@ -4096,7 +4457,7 @@ var PtySession = class {
|
|
|
4096
4457
|
}
|
|
4097
4458
|
}
|
|
4098
4459
|
handleProgress(progress) {
|
|
4099
|
-
this.
|
|
4460
|
+
this.pushEvent({
|
|
4100
4461
|
type: "tool_progress",
|
|
4101
4462
|
...progress.tool_name === void 0 ? {} : { tool_name: progress.tool_name },
|
|
4102
4463
|
...progress.elapsed_time_seconds === void 0 ? {} : { elapsed_time_seconds: progress.elapsed_time_seconds }
|
|
@@ -4112,10 +4473,13 @@ var PtySession = class {
|
|
|
4112
4473
|
async handlePreToolUse(request) {
|
|
4113
4474
|
if (request.tool_name === "AskUserQuestion") {
|
|
4114
4475
|
this.disarmSubmitNudge();
|
|
4115
|
-
this.
|
|
4476
|
+
this.pushEvent({
|
|
4477
|
+
type: "user_question",
|
|
4478
|
+
questions: parseUserQuestions(request.tool_input)
|
|
4479
|
+
});
|
|
4116
4480
|
return { decision: "allow" };
|
|
4117
4481
|
}
|
|
4118
|
-
const canUseTool = this.
|
|
4482
|
+
const canUseTool = this.turn.canUseTool;
|
|
4119
4483
|
let verdict;
|
|
4120
4484
|
if (canUseTool) {
|
|
4121
4485
|
const result = await canUseTool(request.tool_name, request.tool_input);
|
|
@@ -4123,7 +4487,7 @@ var PtySession = class {
|
|
|
4123
4487
|
} else {
|
|
4124
4488
|
verdict = { decision: "allow" };
|
|
4125
4489
|
}
|
|
4126
|
-
if (verdict.decision === "allow" && request.tool_name === "ExitPlanMode" && this.
|
|
4490
|
+
if (verdict.decision === "allow" && request.tool_name === "ExitPlanMode" && this.turn.planDialogAutoAccept) {
|
|
4127
4491
|
this.armPlanDialogAutoAccept();
|
|
4128
4492
|
}
|
|
4129
4493
|
return verdict;
|
|
@@ -4159,29 +4523,31 @@ var PtySession = class {
|
|
|
4159
4523
|
handleTranscriptEvent(event) {
|
|
4160
4524
|
if (this.pendingPlanApproval) this.disarmPlanDialogAutoAccept();
|
|
4161
4525
|
if (this.pendingSubmitNudge) this.disarmSubmitNudge();
|
|
4162
|
-
this.
|
|
4526
|
+
this.pushEvent(event);
|
|
4163
4527
|
if (event.type === "result") {
|
|
4164
4528
|
this.sawResult = true;
|
|
4165
4529
|
for (const listener of this.idleListeners) listener();
|
|
4166
|
-
this.
|
|
4530
|
+
this.endTurn(true);
|
|
4167
4531
|
}
|
|
4168
4532
|
}
|
|
4169
4533
|
async finalizeOnExit(exitCode) {
|
|
4170
4534
|
this.coalescer?.flush();
|
|
4535
|
+
this.exited = true;
|
|
4171
4536
|
if (this._toreDown) return;
|
|
4172
4537
|
if (this.tailer) {
|
|
4173
4538
|
this.tailer.close();
|
|
4174
4539
|
await this.tailer.flush();
|
|
4175
4540
|
}
|
|
4176
|
-
if (!this.sawResult) {
|
|
4177
|
-
this.
|
|
4541
|
+
if (!this.sawResult && this.activeQueue) {
|
|
4542
|
+
this.activeQueue.push({
|
|
4178
4543
|
type: "result",
|
|
4179
4544
|
subtype: "error",
|
|
4180
4545
|
errors: buildExitErrors(exitCode, this.recentOutput, resolveClaudeBinary())
|
|
4181
4546
|
});
|
|
4182
4547
|
}
|
|
4183
4548
|
for (const listener of this.exitListeners) listener(exitCode);
|
|
4184
|
-
this.
|
|
4549
|
+
this.activeQueue?.close();
|
|
4550
|
+
this.activeQueue = null;
|
|
4185
4551
|
}
|
|
4186
4552
|
};
|
|
4187
4553
|
|
|
@@ -4351,6 +4717,15 @@ async function removeConveyorCredentials(env = process.env) {
|
|
|
4351
4717
|
}
|
|
4352
4718
|
|
|
4353
4719
|
// src/harness/pty/index.ts
|
|
4720
|
+
var ENDED_GRACE_MS = 4e3;
|
|
4721
|
+
function fingerprintOf(options) {
|
|
4722
|
+
return spawnOptionsFingerprint({
|
|
4723
|
+
model: options.model,
|
|
4724
|
+
permissionMode: options.permissionMode,
|
|
4725
|
+
...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
|
|
4726
|
+
cwd: options.cwd
|
|
4727
|
+
});
|
|
4728
|
+
}
|
|
4354
4729
|
var PtyHarness = class {
|
|
4355
4730
|
/**
|
|
4356
4731
|
* `bridge` relays raw terminal I/O to/from the S2 server (and on to the S5
|
|
@@ -4363,34 +4738,143 @@ var PtyHarness = class {
|
|
|
4363
4738
|
bridge;
|
|
4364
4739
|
/** The session currently streaming events, if any — repaint target. */
|
|
4365
4740
|
activeSession = null;
|
|
4741
|
+
/** A completed-but-alive session kept for the next turn (keep-alive). */
|
|
4742
|
+
parked = null;
|
|
4743
|
+
/** Pending "process died" → `pty:ended` timer (grace window). */
|
|
4744
|
+
endedTimer = null;
|
|
4745
|
+
/** Passive-activity subscriber, re-attached to whichever session is parked. */
|
|
4746
|
+
passiveHandler = null;
|
|
4366
4747
|
/**
|
|
4367
|
-
* Wiggle the live pty's size so the CLI repaints its whole screen. No-op
|
|
4368
|
-
*
|
|
4369
|
-
* the
|
|
4370
|
-
* and the Connected-TUI terminal reappears.
|
|
4748
|
+
* Wiggle the live pty's size so the CLI repaints its whole screen. No-op on
|
|
4749
|
+
* the SDK harness. Falls back to the parked session so an API reconnect while
|
|
4750
|
+
* the CLI is idle still re-seeds the server's scrollback ring.
|
|
4371
4751
|
*/
|
|
4372
4752
|
forceRepaint() {
|
|
4373
|
-
this.activeSession?.forceRepaint();
|
|
4753
|
+
(this.activeSession ?? this.parked)?.forceRepaint();
|
|
4374
4754
|
}
|
|
4375
4755
|
async *executeQuery(opts) {
|
|
4376
|
-
const
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
this.
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4756
|
+
const want = opts.resume ?? opts.options.resume;
|
|
4757
|
+
const fingerprint = fingerprintOf(opts.options);
|
|
4758
|
+
let session;
|
|
4759
|
+
if (this.parked?.canReuse(want, fingerprint)) {
|
|
4760
|
+
session = this.parked;
|
|
4761
|
+
this.parked = null;
|
|
4762
|
+
this.cancelEndedTimer();
|
|
4763
|
+
await session.beginTurn(opts.prompt, opts.options);
|
|
4764
|
+
} else {
|
|
4765
|
+
if (this.parked) {
|
|
4766
|
+
const stale = this.parked;
|
|
4767
|
+
this.parked = null;
|
|
4768
|
+
this.cancelEndedTimer();
|
|
4769
|
+
await stale.teardown();
|
|
4770
|
+
}
|
|
4771
|
+
session = new PtySession(opts.prompt, opts.options, want, this.bridge);
|
|
4772
|
+
await ensureClaudeCredentials();
|
|
4773
|
+
await ensureClaudeOnboarding(process.env, opts.options.cwd);
|
|
4774
|
+
session.onExit(() => this.handleSessionExit(session));
|
|
4775
|
+
await session.start();
|
|
4776
|
+
}
|
|
4777
|
+
yield* this.drain(session);
|
|
4778
|
+
}
|
|
4779
|
+
/**
|
|
4780
|
+
* PTY-only: subscribe to "the parked CLI produced transcript activity with no
|
|
4781
|
+
* query running" (a human typed into the idle Connected-TUI). Attaches to the
|
|
4782
|
+
* currently-parked session and to any session parked later.
|
|
4783
|
+
*/
|
|
4784
|
+
onPassiveActivity(handler) {
|
|
4785
|
+
this.passiveHandler = handler;
|
|
4786
|
+
const unsubParked = this.parked?.onPassiveActivity(handler);
|
|
4787
|
+
return () => {
|
|
4788
|
+
if (this.passiveHandler === handler) this.passiveHandler = null;
|
|
4789
|
+
unsubParked?.();
|
|
4790
|
+
};
|
|
4791
|
+
}
|
|
4792
|
+
/**
|
|
4793
|
+
* PTY-only: drain a passive turn against the parked process — the human
|
|
4794
|
+
* already submitted in the TUI, so no prompt is fed. Empty stream when nothing
|
|
4795
|
+
* is parked.
|
|
4796
|
+
*/
|
|
4797
|
+
async *executePassiveTurn(options) {
|
|
4798
|
+
const session = this.parked;
|
|
4799
|
+
if (!session) return;
|
|
4800
|
+
this.parked = null;
|
|
4801
|
+
this.cancelEndedTimer();
|
|
4802
|
+
session.beginPassiveTurn(options);
|
|
4803
|
+
yield* this.drain(session);
|
|
4804
|
+
}
|
|
4805
|
+
/**
|
|
4806
|
+
* Shared query/passive-turn drain: stream the session's events, then either
|
|
4807
|
+
* park it (clean result → keep the process for the next turn) or tear it down
|
|
4808
|
+
* (abort/error/early-return → real death, schedule the ended signal).
|
|
4809
|
+
*/
|
|
4810
|
+
async *drain(session) {
|
|
4385
4811
|
this.activeSession = session;
|
|
4812
|
+
let clean = false;
|
|
4386
4813
|
try {
|
|
4387
4814
|
for await (const event of session.events()) {
|
|
4388
4815
|
yield event;
|
|
4389
4816
|
}
|
|
4817
|
+
clean = session.endedCleanly && !session.isToreDown && !session.hasExited;
|
|
4390
4818
|
} finally {
|
|
4391
4819
|
if (this.activeSession === session) this.activeSession = null;
|
|
4392
|
-
|
|
4820
|
+
if (clean) {
|
|
4821
|
+
this.park(session);
|
|
4822
|
+
} else {
|
|
4823
|
+
await session.teardown();
|
|
4824
|
+
this.scheduleEnded();
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
/** Park a live, idle session for reuse and (re)wire the passive listener. */
|
|
4829
|
+
park(session) {
|
|
4830
|
+
this.parked = session;
|
|
4831
|
+
if (this.passiveHandler) session.onPassiveActivity(this.passiveHandler);
|
|
4832
|
+
}
|
|
4833
|
+
/**
|
|
4834
|
+
* Parked/active process exited on its own (crash). A mid-turn death is
|
|
4835
|
+
* handled by drain()'s finally; here we only act on a death while PARKED —
|
|
4836
|
+
* tear down the husk and report ended immediately (no respawn is coming).
|
|
4837
|
+
*/
|
|
4838
|
+
handleSessionExit(session) {
|
|
4839
|
+
if (this.parked !== session) return;
|
|
4840
|
+
this.parked = null;
|
|
4841
|
+
void session.teardown();
|
|
4842
|
+
this.notifyEnded();
|
|
4843
|
+
}
|
|
4844
|
+
scheduleEnded() {
|
|
4845
|
+
if (this.endedTimer) return;
|
|
4846
|
+
this.endedTimer = setTimeout(() => {
|
|
4847
|
+
this.endedTimer = null;
|
|
4848
|
+
this.notifyEnded();
|
|
4849
|
+
}, ENDED_GRACE_MS);
|
|
4850
|
+
}
|
|
4851
|
+
cancelEndedTimer() {
|
|
4852
|
+
if (this.endedTimer) {
|
|
4853
|
+
clearTimeout(this.endedTimer);
|
|
4854
|
+
this.endedTimer = null;
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
notifyEnded() {
|
|
4858
|
+
this.cancelEndedTimer();
|
|
4859
|
+
this.bridge?.sendEnded?.();
|
|
4860
|
+
}
|
|
4861
|
+
/**
|
|
4862
|
+
* Tear down every live process (parked + active) and signal ended. Called by
|
|
4863
|
+
* the runner on stop/shutdown so the parked CLI never outlives the session and
|
|
4864
|
+
* the Connected-TUI tab hides promptly.
|
|
4865
|
+
*/
|
|
4866
|
+
async dispose() {
|
|
4867
|
+
this.cancelEndedTimer();
|
|
4868
|
+
const toKill = [this.parked, this.activeSession].filter((s) => s !== null);
|
|
4869
|
+
this.parked = null;
|
|
4870
|
+
this.activeSession = null;
|
|
4871
|
+
for (const session of toKill) {
|
|
4872
|
+
try {
|
|
4873
|
+
await session.teardown();
|
|
4874
|
+
} catch {
|
|
4875
|
+
}
|
|
4393
4876
|
}
|
|
4877
|
+
if (toKill.length > 0) this.notifyEnded();
|
|
4394
4878
|
}
|
|
4395
4879
|
createMcpServer(config) {
|
|
4396
4880
|
return new PtyMcpServer(config.name, config.tools);
|
|
@@ -8042,6 +8526,18 @@ async function runPrefilledFollowUp(host, context, options, resume, followUpCont
|
|
|
8042
8526
|
});
|
|
8043
8527
|
await trackAndRun(host, context, queryOptions, agentQuery);
|
|
8044
8528
|
}
|
|
8529
|
+
async function runPassiveTurn(host, context) {
|
|
8530
|
+
if (host.isStopped()) return;
|
|
8531
|
+
if (!host.harness.executePassiveTurn) return;
|
|
8532
|
+
const options = buildQueryOptions(host, context);
|
|
8533
|
+
const passiveQuery = host.harness.executePassiveTurn(options);
|
|
8534
|
+
host.activeQuery = passiveQuery;
|
|
8535
|
+
try {
|
|
8536
|
+
await processEvents(passiveQuery, context, host);
|
|
8537
|
+
} finally {
|
|
8538
|
+
host.activeQuery = null;
|
|
8539
|
+
}
|
|
8540
|
+
}
|
|
8045
8541
|
async function trackAndRun(host, context, options, agentQuery) {
|
|
8046
8542
|
if (host.harnessKind === "pty" && options.promptDelivery !== "prefill") {
|
|
8047
8543
|
agentQuery = watchForParkedTui(agentQuery, host);
|
|
@@ -8377,6 +8873,7 @@ function resolveHarnessKind() {
|
|
|
8377
8873
|
function buildPtyBridge(connection) {
|
|
8378
8874
|
return {
|
|
8379
8875
|
sendOutput: (data, dims) => connection.sendPtyOutput(data, dims),
|
|
8876
|
+
sendEnded: () => connection.sendPtyEnded(),
|
|
8380
8877
|
onInput: (handler) => connection.onPtyInput(handler),
|
|
8381
8878
|
onResize: (handler) => connection.onPtyResize(handler)
|
|
8382
8879
|
};
|
|
@@ -8448,6 +8945,24 @@ var QueryBridge = class {
|
|
|
8448
8945
|
resume() {
|
|
8449
8946
|
this._stopped = false;
|
|
8450
8947
|
}
|
|
8948
|
+
/**
|
|
8949
|
+
* Tear down any parked/active CLI process the harness is keeping alive between
|
|
8950
|
+
* turns (PTY keep-alive). Called by SessionRunner on stop/shutdown so the
|
|
8951
|
+
* process never outlives the session and the Connected-TUI tab hides. No-op on
|
|
8952
|
+
* the SDK harness.
|
|
8953
|
+
*/
|
|
8954
|
+
async dispose() {
|
|
8955
|
+
await this.harness.dispose?.();
|
|
8956
|
+
}
|
|
8957
|
+
/**
|
|
8958
|
+
* Subscribe to "a human typed into the parked, idle TUI" (PTY keep-alive).
|
|
8959
|
+
* SessionRunner uses this to wake a passive turn. Returns unsubscribe; no-op
|
|
8960
|
+
* (returns a noop unsubscribe) on the SDK harness.
|
|
8961
|
+
*/
|
|
8962
|
+
onPassiveActivity(handler) {
|
|
8963
|
+
return this.harness.onPassiveActivity?.(handler) ?? (() => {
|
|
8964
|
+
});
|
|
8965
|
+
}
|
|
8451
8966
|
/**
|
|
8452
8967
|
* How the INITIAL instructions for this task would be delivered — "prefill"
|
|
8453
8968
|
* means the TUI input is pre-filled but unsubmitted, awaiting the human.
|
|
@@ -8496,6 +9011,34 @@ var QueryBridge = class {
|
|
|
8496
9011
|
this._abortController = null;
|
|
8497
9012
|
}
|
|
8498
9013
|
}
|
|
9014
|
+
/**
|
|
9015
|
+
* Drive a passive turn: the parked CLI is producing transcript records because
|
|
9016
|
+
* a human typed into the idle Connected-TUI. Mirrors execute()'s setup (fresh
|
|
9017
|
+
* abort controller so a superseding chat message can abort, host construction,
|
|
9018
|
+
* error classification) but runs the promptless passive drain instead of a
|
|
9019
|
+
* full query.
|
|
9020
|
+
*/
|
|
9021
|
+
async executePassive(context) {
|
|
9022
|
+
this._stopped = false;
|
|
9023
|
+
this._wasRateLimited = false;
|
|
9024
|
+
this._abortController = new AbortController();
|
|
9025
|
+
const host = this.buildHost();
|
|
9026
|
+
try {
|
|
9027
|
+
await runPassiveTurn(host, context);
|
|
9028
|
+
} catch (err) {
|
|
9029
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9030
|
+
const isAbort = this._stopped || /abort/i.test(msg);
|
|
9031
|
+
if (isAbort) {
|
|
9032
|
+
logger3.info("Passive turn stopped", { error: msg });
|
|
9033
|
+
} else {
|
|
9034
|
+
logger3.error("Passive turn failed", { error: msg });
|
|
9035
|
+
this.connection.sendEvent({ type: "error", message: msg });
|
|
9036
|
+
}
|
|
9037
|
+
} finally {
|
|
9038
|
+
this.mode.pendingModeRestart = false;
|
|
9039
|
+
this._abortController = null;
|
|
9040
|
+
}
|
|
9041
|
+
}
|
|
8499
9042
|
// ── QueryHost construction ──────────────────────────────────────────
|
|
8500
9043
|
buildHost() {
|
|
8501
9044
|
const bridge = this;
|
|
@@ -8615,6 +9158,80 @@ function readAgentVersion() {
|
|
|
8615
9158
|
return null;
|
|
8616
9159
|
}
|
|
8617
9160
|
|
|
9161
|
+
// src/execution/usage-sampler.ts
|
|
9162
|
+
var logger4 = createServiceLogger("usage-sampler");
|
|
9163
|
+
var OAUTH_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
9164
|
+
var OAUTH_USAGE_BETA = "oauth-2025-04-20";
|
|
9165
|
+
var KNOWN_RATE_LIMIT_TYPES = [
|
|
9166
|
+
"five_hour",
|
|
9167
|
+
"seven_day",
|
|
9168
|
+
"seven_day_opus",
|
|
9169
|
+
"seven_day_sonnet"
|
|
9170
|
+
];
|
|
9171
|
+
function normalizeUtilization(raw) {
|
|
9172
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
|
|
9173
|
+
const fraction = raw > 1 ? raw / 100 : raw;
|
|
9174
|
+
return Math.max(0, Math.min(1, fraction));
|
|
9175
|
+
}
|
|
9176
|
+
function extractBucket(value) {
|
|
9177
|
+
if (typeof value === "number") {
|
|
9178
|
+
const utilization = normalizeUtilization(value);
|
|
9179
|
+
return utilization === null ? null : { utilization, status: "allowed" };
|
|
9180
|
+
}
|
|
9181
|
+
if (value && typeof value === "object") {
|
|
9182
|
+
const obj = value;
|
|
9183
|
+
const utilization = normalizeUtilization(
|
|
9184
|
+
obj.utilization ?? obj.used ?? obj.percent ?? obj.percentage
|
|
9185
|
+
);
|
|
9186
|
+
if (utilization === null) return null;
|
|
9187
|
+
const status = typeof obj.status === "string" ? obj.status : "allowed";
|
|
9188
|
+
return { utilization, status };
|
|
9189
|
+
}
|
|
9190
|
+
return null;
|
|
9191
|
+
}
|
|
9192
|
+
function mapUsageResponse(data) {
|
|
9193
|
+
if (!data || typeof data !== "object") return [];
|
|
9194
|
+
const root = data;
|
|
9195
|
+
const candidates = [root];
|
|
9196
|
+
for (const key of ["usage", "rate_limits", "rateLimits"]) {
|
|
9197
|
+
const nested = root[key];
|
|
9198
|
+
if (nested && typeof nested === "object") {
|
|
9199
|
+
candidates.push(nested);
|
|
9200
|
+
}
|
|
9201
|
+
}
|
|
9202
|
+
for (const container of candidates) {
|
|
9203
|
+
const samples = [];
|
|
9204
|
+
for (const type of KNOWN_RATE_LIMIT_TYPES) {
|
|
9205
|
+
const bucket = extractBucket(container[type]);
|
|
9206
|
+
if (bucket) samples.push({ rateLimitType: type, ...bucket });
|
|
9207
|
+
}
|
|
9208
|
+
if (samples.length > 0) return samples;
|
|
9209
|
+
}
|
|
9210
|
+
return [];
|
|
9211
|
+
}
|
|
9212
|
+
async function sampleKeyUsage(token) {
|
|
9213
|
+
if (!token) return [];
|
|
9214
|
+
try {
|
|
9215
|
+
const res = await fetch(OAUTH_USAGE_URL, {
|
|
9216
|
+
headers: {
|
|
9217
|
+
Authorization: `Bearer ${token}`,
|
|
9218
|
+
"anthropic-beta": OAUTH_USAGE_BETA
|
|
9219
|
+
}
|
|
9220
|
+
});
|
|
9221
|
+
if (!res.ok) {
|
|
9222
|
+
logger4.info("OAuth usage endpoint returned non-200", { status: res.status });
|
|
9223
|
+
return [];
|
|
9224
|
+
}
|
|
9225
|
+
const data = await res.json();
|
|
9226
|
+
return mapUsageResponse(data);
|
|
9227
|
+
} catch (error) {
|
|
9228
|
+
logger4.info("OAuth usage sample failed", {
|
|
9229
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9230
|
+
});
|
|
9231
|
+
return [];
|
|
9232
|
+
}
|
|
9233
|
+
}
|
|
9234
|
+
|
|
8618
9235
|
// src/runner/port-discovery.ts
|
|
8619
9236
|
import { readFile as readFile4 } from "fs/promises";
|
|
8620
9237
|
import { execFile } from "child_process";
|
|
@@ -8913,7 +9530,8 @@ var SessionRunner = class _SessionRunner {
|
|
|
8913
9530
|
}
|
|
8914
9531
|
},
|
|
8915
9532
|
onTokenRefresh: () => void this.refreshGithubToken(),
|
|
8916
|
-
onGitFlush: () => void this.periodicGitFlush()
|
|
9533
|
+
onGitFlush: () => void this.periodicGitFlush(),
|
|
9534
|
+
onUsageSample: () => void this.sampleAndReportKeyUsage()
|
|
8917
9535
|
});
|
|
8918
9536
|
}
|
|
8919
9537
|
get state() {
|
|
@@ -8939,6 +9557,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
8939
9557
|
this.wireConnectionCallbacks();
|
|
8940
9558
|
this.lifecycle.startHeartbeat();
|
|
8941
9559
|
this.lifecycle.startTokenRefresh();
|
|
9560
|
+
this.lifecycle.startUsageSample();
|
|
8942
9561
|
const { pendingMessages: serverMessages } = await this.connection.call("connectAgent", {
|
|
8943
9562
|
sessionId: this.sessionId
|
|
8944
9563
|
});
|
|
@@ -9095,16 +9714,21 @@ var SessionRunner = class _SessionRunner {
|
|
|
9095
9714
|
break;
|
|
9096
9715
|
}
|
|
9097
9716
|
this.interrupted = false;
|
|
9098
|
-
|
|
9099
|
-
type: "user_message",
|
|
9100
|
-
content: msg.content,
|
|
9101
|
-
userId: msg.userId
|
|
9102
|
-
});
|
|
9103
|
-
if (this.prefillEligible(msg)) {
|
|
9104
|
-
await this.runPrefilledMessage(msg, this.mode.effectiveMode);
|
|
9105
|
-
} else {
|
|
9717
|
+
if (msg.source === "pty_passive") {
|
|
9106
9718
|
await this.setState("running");
|
|
9107
|
-
await this.
|
|
9719
|
+
await this.executePassive();
|
|
9720
|
+
} else {
|
|
9721
|
+
await this.callbacks.onEvent({
|
|
9722
|
+
type: "user_message",
|
|
9723
|
+
content: msg.content,
|
|
9724
|
+
userId: msg.userId
|
|
9725
|
+
});
|
|
9726
|
+
if (this.prefillEligible(msg)) {
|
|
9727
|
+
await this.runPrefilledMessage(msg, this.mode.effectiveMode);
|
|
9728
|
+
} else {
|
|
9729
|
+
await this.setState("running");
|
|
9730
|
+
await this.executeQuery(msg.content);
|
|
9731
|
+
}
|
|
9108
9732
|
}
|
|
9109
9733
|
if (this.queryBridge?.isDiscoveryCompleted) {
|
|
9110
9734
|
process.stderr.write(
|
|
@@ -9331,6 +9955,28 @@ var SessionRunner = class _SessionRunner {
|
|
|
9331
9955
|
throw err;
|
|
9332
9956
|
}
|
|
9333
9957
|
}
|
|
9958
|
+
/**
|
|
9959
|
+
* A human typed into the parked, idle Connected-TUI (keep-alive PTY). Wake the
|
|
9960
|
+
* core loop with a sentinel so it drains a passive turn. No-op once stopped.
|
|
9961
|
+
*/
|
|
9962
|
+
onPassiveTuiActivity() {
|
|
9963
|
+
if (this.stopped) return;
|
|
9964
|
+
this.injectMessage({ content: "", userId: "human", source: "pty_passive" });
|
|
9965
|
+
}
|
|
9966
|
+
/** Run queryBridge.executePassive, swallowing abort errors from stop/softStop. */
|
|
9967
|
+
async executePassive() {
|
|
9968
|
+
if (!this.fullContext || !this.queryBridge) return;
|
|
9969
|
+
this.agentSpokeThisTurn = false;
|
|
9970
|
+
try {
|
|
9971
|
+
await this.queryBridge.executePassive(this.fullContext);
|
|
9972
|
+
} catch (err) {
|
|
9973
|
+
if (this.interrupted || this.stopped) {
|
|
9974
|
+
process.stderr.write("[conveyor-agent] Passive turn aborted by stop/softStop signal\n");
|
|
9975
|
+
return;
|
|
9976
|
+
}
|
|
9977
|
+
throw err;
|
|
9978
|
+
}
|
|
9979
|
+
}
|
|
9334
9980
|
// ── Stop / soft-stop ───────────────────────────────────────────────
|
|
9335
9981
|
/** Periodic best-effort WIP commit + push during normal agent execution.
|
|
9336
9982
|
* Covers ungraceful pod termination (OOMKilled, node crash/eviction) where
|
|
@@ -9365,6 +10011,24 @@ var SessionRunner = class _SessionRunner {
|
|
|
9365
10011
|
this.periodicFlushInFlight = false;
|
|
9366
10012
|
}
|
|
9367
10013
|
}
|
|
10014
|
+
/** Sample the running Claude subscription key's rate-limit utilization from the
|
|
10015
|
+
* Anthropic OAuth usage endpoint and report it as `rate_limit_update` events.
|
|
10016
|
+
* The API (`persistRateLimitSnapshot`) attributes them to the key this session
|
|
10017
|
+
* launched under, keeping User Settings + the PtY-tab usage widget fresh and
|
|
10018
|
+
* `selectBestKey` rotation honest. Best-effort — never throws, no-op when the
|
|
10019
|
+
* pod has no OAuth token (e.g. API-key projects). */
|
|
10020
|
+
async sampleAndReportKeyUsage() {
|
|
10021
|
+
if (this.stopped) return;
|
|
10022
|
+
const samples = await sampleKeyUsage(process.env.CLAUDE_CODE_OAUTH_TOKEN);
|
|
10023
|
+
for (const sample of samples) {
|
|
10024
|
+
this.connection.sendEvent({
|
|
10025
|
+
type: "rate_limit_update",
|
|
10026
|
+
rateLimitType: sample.rateLimitType,
|
|
10027
|
+
utilization: sample.utilization,
|
|
10028
|
+
status: sample.status
|
|
10029
|
+
});
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
9368
10032
|
/** PUT the WorkPreservation snapshot tar to the bundle's capability URL
|
|
9369
10033
|
* when this pod is v3 (CONVEYOR_SNAPSHOT_UPLOAD_URL set). Never throws. */
|
|
9370
10034
|
async uploadGcsSnapshotIfConfigured() {
|
|
@@ -9415,6 +10079,8 @@ var SessionRunner = class _SessionRunner {
|
|
|
9415
10079
|
stop() {
|
|
9416
10080
|
this.stopped = true;
|
|
9417
10081
|
this.queryBridge?.stop();
|
|
10082
|
+
void this.queryBridge?.dispose().catch(() => {
|
|
10083
|
+
});
|
|
9418
10084
|
this.portDiscovery?.stop();
|
|
9419
10085
|
this.lifecycle.destroy();
|
|
9420
10086
|
this.connection.disconnect();
|
|
@@ -9602,6 +10268,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
9602
10268
|
this.connection.emitModeChanged(newMode);
|
|
9603
10269
|
this.softStop();
|
|
9604
10270
|
};
|
|
10271
|
+
bridge.onPassiveActivity(() => this.onPassiveTuiActivity());
|
|
9605
10272
|
return bridge;
|
|
9606
10273
|
}
|
|
9607
10274
|
// ── Private helpers ────────────────────────────────────────────────
|
|
@@ -9718,6 +10385,10 @@ var SessionRunner = class _SessionRunner {
|
|
|
9718
10385
|
this.connection.sendEvent({ type: "shutdown", reason: finalState });
|
|
9719
10386
|
this.portDiscovery?.stop();
|
|
9720
10387
|
this.lifecycle.destroy();
|
|
10388
|
+
try {
|
|
10389
|
+
await this.queryBridge?.dispose();
|
|
10390
|
+
} catch {
|
|
10391
|
+
}
|
|
9721
10392
|
try {
|
|
9722
10393
|
await this.setState(finalState);
|
|
9723
10394
|
} catch {
|
|
@@ -9897,6 +10568,7 @@ export {
|
|
|
9897
10568
|
flushPendingChanges,
|
|
9898
10569
|
pushToOrigin,
|
|
9899
10570
|
PlanSync,
|
|
10571
|
+
sampleKeyUsage,
|
|
9900
10572
|
awaitGitReady,
|
|
9901
10573
|
uploadSnapshotToGcs,
|
|
9902
10574
|
captureSnapshot,
|
|
@@ -9913,4 +10585,4 @@ export {
|
|
|
9913
10585
|
runStartCommand,
|
|
9914
10586
|
unshallowRepo
|
|
9915
10587
|
};
|
|
9916
|
-
//# sourceMappingURL=chunk-
|
|
10588
|
+
//# sourceMappingURL=chunk-DAHP6JPD.js.map
|