@bitkyc08/opencodex 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +126 -10
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +143 -17
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/init.ts +129 -102
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +14 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +60 -14
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +179 -13
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +97 -25
- package/src/types.ts +39 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type ReadMcpResourceExecArgs,
|
|
23
23
|
} from "./gen/agent_pb";
|
|
24
24
|
import type { McpCallResult } from "./mcp-manager";
|
|
25
|
-
import { CursorMcpManager } from "./mcp-manager";
|
|
25
|
+
import { CursorMcpManager, decodedBase64Length } from "./mcp-manager";
|
|
26
26
|
import { errorText } from "./native-exec-common";
|
|
27
27
|
import type { CursorNativeToolDeps } from "./native-exec-tools";
|
|
28
28
|
import { encodeCursorInputSchema } from "./tool-definitions";
|
|
@@ -62,7 +62,7 @@ export function mcpDepsFromManager(manager: CursorMcpManager): CursorNativeToolD
|
|
|
62
62
|
}
|
|
63
63
|
const result = await manager.callTool(name, decodeMcpArgs(args.args));
|
|
64
64
|
return create(McpResultSchema, {
|
|
65
|
-
result: { case: "success", value: create(McpSuccessSchema, { isError: result.isError, content: toContentItems(result) }) },
|
|
65
|
+
result: { case: "success", value: create(McpSuccessSchema, { isError: result.isError, content: toContentItems(result, manager) }) },
|
|
66
66
|
});
|
|
67
67
|
} catch (err) {
|
|
68
68
|
return create(McpResultSchema, { result: { case: "error", value: create(McpErrorSchema, { error: errorText(err) }) } });
|
|
@@ -112,7 +112,9 @@ function decodeMcpArgs(raw: { [key: string]: Uint8Array }): Record<string, unkno
|
|
|
112
112
|
return decodeCursorArgsMap(raw);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function toContentItems(result: McpCallResult): McpToolResultContentItem[] {
|
|
115
|
+
function toContentItems(result: McpCallResult, manager: CursorMcpManager): McpToolResultContentItem[] {
|
|
116
|
+
const decodedBytes = result.content.reduce((sum, block) => sum + (block.type === "image" && typeof block.data === "string" ? decodedBase64Length(block.data) : 0), 0);
|
|
117
|
+
manager.assertDecodedResultBudget(result, decodedBytes);
|
|
116
118
|
return result.content.map(block => {
|
|
117
119
|
// Promote real image payloads to McpImageContent so the model receives the actual bytes,
|
|
118
120
|
// not a "[image]" placeholder. Non-text, non-image blocks (audio/resource) still degrade
|
|
@@ -19,9 +19,67 @@ import {
|
|
|
19
19
|
type ExecServerMessage,
|
|
20
20
|
} from "./gen/agent_pb";
|
|
21
21
|
import { errorText, execBytes, execStreamCloseBytes } from "./native-exec-common";
|
|
22
|
+
import {
|
|
23
|
+
createAdmissionGate,
|
|
24
|
+
type AdmissionLease,
|
|
25
|
+
type AdmissionMetrics,
|
|
26
|
+
} from "../../lib/admission";
|
|
27
|
+
|
|
28
|
+
export const CURSOR_BACKGROUND_SHELL_MAX_LIVE = 8;
|
|
29
|
+
export const CURSOR_BACKGROUND_SHELL_IDLE_MS = 5 * 60_000;
|
|
30
|
+
export const CURSOR_BACKGROUND_SHELL_ABSOLUTE_MS = 30 * 60_000;
|
|
31
|
+
export const CURSOR_BACKGROUND_SHELL_TERM_GRACE_MS = 2_000;
|
|
32
|
+
|
|
33
|
+
type BackgroundShellTerminationReason = "session_close" | "idle" | "absolute" | "shutdown";
|
|
34
|
+
type BackgroundShellTimer = ReturnType<typeof setTimeout>;
|
|
35
|
+
|
|
36
|
+
export interface BackgroundShellTerminationReport {
|
|
37
|
+
attempted: number;
|
|
38
|
+
closed: number;
|
|
39
|
+
unresolved: number;
|
|
40
|
+
killFailures: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BackgroundShellRuntime {
|
|
44
|
+
spawn: typeof spawn;
|
|
45
|
+
now(): number;
|
|
46
|
+
setTimer(callback: () => void, delayMs: number): BackgroundShellTimer;
|
|
47
|
+
clearTimer(timer: BackgroundShellTimer): void;
|
|
48
|
+
kill(child: ChildProcessWithoutNullStreams, signal?: NodeJS.Signals): boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface BackgroundShellEntry {
|
|
52
|
+
shellId: number;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
child: ChildProcessWithoutNullStreams;
|
|
55
|
+
admissionLease: AdmissionLease;
|
|
56
|
+
outputLength: number;
|
|
57
|
+
startedAt: number;
|
|
58
|
+
lastActivityAt: number;
|
|
59
|
+
idleTimer?: BackgroundShellTimer;
|
|
60
|
+
absoluteTimer?: BackgroundShellTimer;
|
|
61
|
+
terminating: Promise<BackgroundShellTerminationReport> | null;
|
|
62
|
+
closePromise: Promise<void>;
|
|
63
|
+
confirmClose(): void;
|
|
64
|
+
}
|
|
22
65
|
|
|
23
|
-
const
|
|
66
|
+
const defaultBackgroundShellRuntime: BackgroundShellRuntime = {
|
|
67
|
+
spawn,
|
|
68
|
+
now: () => Date.now(),
|
|
69
|
+
setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
70
|
+
clearTimer: timer => clearTimeout(timer),
|
|
71
|
+
kill: (child, signal) => child.kill(signal),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let backgroundShellRuntime = defaultBackgroundShellRuntime;
|
|
75
|
+
let backgroundShellGate = createAdmissionGate("cursor_background_shells", CURSOR_BACKGROUND_SHELL_MAX_LIVE);
|
|
76
|
+
const backgroundShells = new Map<number, BackgroundShellEntry>();
|
|
24
77
|
let nextShellId = 1;
|
|
78
|
+
let backgroundShellShutdownStarted = false;
|
|
79
|
+
let idleTerminations = 0;
|
|
80
|
+
let absoluteTerminations = 0;
|
|
81
|
+
let unresolvedKills = 0;
|
|
82
|
+
let killFailures = 0;
|
|
25
83
|
|
|
26
84
|
/** Rejection text when Cursor-native shell is denied by policy (issue #604). */
|
|
27
85
|
export function nativeShellDisabledMessage(): string {
|
|
@@ -212,23 +270,242 @@ export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessa
|
|
|
212
270
|
}));
|
|
213
271
|
}
|
|
214
272
|
|
|
215
|
-
|
|
273
|
+
function backgroundShellSpawnError(
|
|
274
|
+
execMsg: ExecServerMessage,
|
|
275
|
+
command: string,
|
|
276
|
+
workingDirectory: string,
|
|
277
|
+
error: string,
|
|
278
|
+
): Uint8Array {
|
|
279
|
+
return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, {
|
|
280
|
+
result: {
|
|
281
|
+
case: "error",
|
|
282
|
+
value: create(BackgroundShellSpawnErrorSchema, { command, workingDirectory, error }),
|
|
283
|
+
},
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function clearBackgroundShellTimers(entry: BackgroundShellEntry): void {
|
|
288
|
+
if (entry.idleTimer) backgroundShellRuntime.clearTimer(entry.idleTimer);
|
|
289
|
+
if (entry.absoluteTimer) backgroundShellRuntime.clearTimer(entry.absoluteTimer);
|
|
290
|
+
entry.idleTimer = undefined;
|
|
291
|
+
entry.absoluteTimer = undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function confirmBackgroundShellClose(entry: BackgroundShellEntry): void {
|
|
295
|
+
if (backgroundShells.get(entry.shellId) !== entry) return;
|
|
296
|
+
clearBackgroundShellTimers(entry);
|
|
297
|
+
backgroundShells.delete(entry.shellId);
|
|
298
|
+
entry.admissionLease.release();
|
|
299
|
+
entry.confirmClose();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function unrefTimer(timer: BackgroundShellTimer): void {
|
|
303
|
+
timer.unref?.();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function armBackgroundShellIdleTimer(entry: BackgroundShellEntry): void {
|
|
307
|
+
if (entry.idleTimer) backgroundShellRuntime.clearTimer(entry.idleTimer);
|
|
308
|
+
entry.idleTimer = backgroundShellRuntime.setTimer(() => {
|
|
309
|
+
entry.idleTimer = undefined;
|
|
310
|
+
void terminateBackgroundShell(entry, "idle");
|
|
311
|
+
}, CURSOR_BACKGROUND_SHELL_IDLE_MS);
|
|
312
|
+
unrefTimer(entry.idleTimer);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function armBackgroundShellAbsoluteTimer(entry: BackgroundShellEntry): void {
|
|
316
|
+
entry.absoluteTimer = backgroundShellRuntime.setTimer(() => {
|
|
317
|
+
entry.absoluteTimer = undefined;
|
|
318
|
+
void terminateBackgroundShell(entry, "absolute");
|
|
319
|
+
}, CURSOR_BACKGROUND_SHELL_ABSOLUTE_MS);
|
|
320
|
+
unrefTimer(entry.absoluteTimer);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function noteBackgroundShellActivity(entry: BackgroundShellEntry, bytes = 0): void {
|
|
324
|
+
if (backgroundShells.get(entry.shellId) !== entry) return;
|
|
325
|
+
entry.outputLength += bytes;
|
|
326
|
+
entry.lastActivityAt = backgroundShellRuntime.now();
|
|
327
|
+
// Once termination has begun the lifecycle timers are cleared for good:
|
|
328
|
+
// late pipe output must update byte accounting only, never re-arm idle,
|
|
329
|
+
// or a quarantined shell gets a fresh five-minute timer that can fire a
|
|
330
|
+
// second kill attempt and distort the lifecycle counters.
|
|
331
|
+
if (!entry.terminating) armBackgroundShellIdleTimer(entry);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function waitForBackgroundShellClose(entry: BackgroundShellEntry): Promise<boolean> {
|
|
335
|
+
if (backgroundShells.get(entry.shellId) !== entry) return Promise.resolve(true);
|
|
336
|
+
return new Promise(resolveWait => {
|
|
337
|
+
let settled = false;
|
|
338
|
+
// Deliberately REF'D: this is the bounded kill-grace wait that shutdown
|
|
339
|
+
// drain awaits. Bun on Windows / under `bun test --isolate` can starve
|
|
340
|
+
// unref'd timers when a pending promise is the only other work, which
|
|
341
|
+
// would leave drainAndShutdown waiting forever (same class as oauth
|
|
342
|
+
// serializeMutation wait timers). The timer self-clears within the
|
|
343
|
+
// 2-second grace window (or earlier on close), so a ref cannot keep the
|
|
344
|
+
// process alive beyond that bound.
|
|
345
|
+
const timer = backgroundShellRuntime.setTimer(() => {
|
|
346
|
+
if (settled) return;
|
|
347
|
+
settled = true;
|
|
348
|
+
resolveWait(false);
|
|
349
|
+
}, CURSOR_BACKGROUND_SHELL_TERM_GRACE_MS);
|
|
350
|
+
void entry.closePromise.then(() => {
|
|
351
|
+
if (settled) return;
|
|
352
|
+
settled = true;
|
|
353
|
+
backgroundShellRuntime.clearTimer(timer);
|
|
354
|
+
resolveWait(true);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function tryKillBackgroundShell(entry: BackgroundShellEntry, signal?: NodeJS.Signals): boolean {
|
|
360
|
+
try {
|
|
361
|
+
return backgroundShellRuntime.kill(entry.child, signal);
|
|
362
|
+
} catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function terminateBackgroundShell(
|
|
368
|
+
entry: BackgroundShellEntry,
|
|
369
|
+
reason: BackgroundShellTerminationReason,
|
|
370
|
+
): Promise<BackgroundShellTerminationReport> {
|
|
371
|
+
if (entry.terminating) return entry.terminating;
|
|
372
|
+
if (backgroundShells.get(entry.shellId) !== entry) {
|
|
373
|
+
return { attempted: 0, closed: 0, unresolved: 0, killFailures: 0 };
|
|
374
|
+
}
|
|
375
|
+
if (reason === "idle") idleTerminations += 1;
|
|
376
|
+
if (reason === "absolute") absoluteTerminations += 1;
|
|
377
|
+
entry.terminating = (async () => {
|
|
378
|
+
clearBackgroundShellTimers(entry);
|
|
379
|
+
try { entry.child.stdin.end(); } catch { /* best-effort direct-child shutdown */ }
|
|
380
|
+
try { entry.child.stdout.resume(); } catch { /* keep draining when supported */ }
|
|
381
|
+
try { entry.child.stderr.resume(); } catch { /* keep draining when supported */ }
|
|
382
|
+
|
|
383
|
+
let attemptKillFailures = 0;
|
|
384
|
+
if (!tryKillBackgroundShell(entry)) attemptKillFailures += 1;
|
|
385
|
+
let closed = await waitForBackgroundShellClose(entry);
|
|
386
|
+
if (!closed && backgroundShells.get(entry.shellId) !== entry) closed = true;
|
|
387
|
+
if (!closed) {
|
|
388
|
+
if (!tryKillBackgroundShell(entry, "SIGKILL")) attemptKillFailures += 1;
|
|
389
|
+
closed = await waitForBackgroundShellClose(entry);
|
|
390
|
+
if (!closed && backgroundShells.get(entry.shellId) !== entry) closed = true;
|
|
391
|
+
}
|
|
392
|
+
killFailures += attemptKillFailures;
|
|
393
|
+
if (!closed) unresolvedKills += 1;
|
|
394
|
+
return {
|
|
395
|
+
attempted: 1,
|
|
396
|
+
closed: closed ? 1 : 0,
|
|
397
|
+
unresolved: closed ? 0 : 1,
|
|
398
|
+
killFailures: attemptKillFailures,
|
|
399
|
+
};
|
|
400
|
+
})();
|
|
401
|
+
try {
|
|
402
|
+
return await entry.terminating;
|
|
403
|
+
} finally {
|
|
404
|
+
if (backgroundShells.get(entry.shellId) === entry) entry.terminating = null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function sumTerminationReports(reports: readonly BackgroundShellTerminationReport[]): BackgroundShellTerminationReport {
|
|
409
|
+
return reports.reduce((total, report) => ({
|
|
410
|
+
attempted: total.attempted + report.attempted,
|
|
411
|
+
closed: total.closed + report.closed,
|
|
412
|
+
unresolved: total.unresolved + report.unresolved,
|
|
413
|
+
killFailures: total.killFailures + report.killFailures,
|
|
414
|
+
}), { attempted: 0, closed: 0, unresolved: 0, killFailures: 0 });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function beginBackgroundShellShutdown(): void {
|
|
418
|
+
backgroundShellShutdownStarted = true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function backgroundShellAdmissionMetrics(): Readonly<AdmissionMetrics> {
|
|
422
|
+
return backgroundShellGate.metrics();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function backgroundShellLifecycleMetrics(): {
|
|
426
|
+
idleTerminations: number;
|
|
427
|
+
absoluteTerminations: number;
|
|
428
|
+
unresolvedKills: number;
|
|
429
|
+
killFailures: number;
|
|
430
|
+
} {
|
|
431
|
+
return { idleTerminations, absoluteTerminations, unresolvedKills, killFailures };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function terminateBackgroundShellsForSession(
|
|
435
|
+
sessionId: string,
|
|
436
|
+
): Promise<BackgroundShellTerminationReport> {
|
|
437
|
+
const owned = [...backgroundShells.values()].filter(entry => entry.sessionId === sessionId);
|
|
438
|
+
return sumTerminationReports(await Promise.all(owned.map(entry => terminateBackgroundShell(entry, "session_close"))));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export async function terminateAllBackgroundShells(): Promise<BackgroundShellTerminationReport> {
|
|
442
|
+
const entries = [...backgroundShells.values()];
|
|
443
|
+
return sumTerminationReports(await Promise.all(entries.map(entry => terminateBackgroundShell(entry, "shutdown"))));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function setBackgroundShellRuntimeForTests(runtime: Partial<BackgroundShellRuntime>): void {
|
|
447
|
+
backgroundShellRuntime = { ...defaultBackgroundShellRuntime, ...runtime };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export async function resetBackgroundShellStateForTests(): Promise<void> {
|
|
451
|
+
const report = await terminateAllBackgroundShells();
|
|
452
|
+
if (report.unresolved !== 0 || backgroundShells.size !== 0 || backgroundShellGate.metrics().active !== 0) {
|
|
453
|
+
throw new Error("cannot reset background shell state before every direct child confirms close");
|
|
454
|
+
}
|
|
455
|
+
backgroundShellRuntime = defaultBackgroundShellRuntime;
|
|
456
|
+
backgroundShellGate = createAdmissionGate("cursor_background_shells", CURSOR_BACKGROUND_SHELL_MAX_LIVE);
|
|
457
|
+
nextShellId = 1;
|
|
458
|
+
backgroundShellShutdownStarted = false;
|
|
459
|
+
idleTerminations = 0;
|
|
460
|
+
absoluteTerminations = 0;
|
|
461
|
+
unresolvedKills = 0;
|
|
462
|
+
killFailures = 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function backgroundShellSpawnExec(execMsg: ExecServerMessage, sessionId: string): Uint8Array {
|
|
216
466
|
if (execMsg.message.case !== "backgroundShellSpawnArgs") throw new Error("invalid background shell exec");
|
|
217
467
|
const args = execMsg.message.value;
|
|
218
468
|
const cwd = resolve(args.workingDirectory || process.cwd());
|
|
469
|
+
if (!sessionId) return backgroundShellSpawnError(execMsg, args.command, cwd, "background shell session owner required");
|
|
470
|
+
if (backgroundShellShutdownStarted) return backgroundShellSpawnError(execMsg, args.command, cwd, "background shell shutdown in progress");
|
|
471
|
+
const admissionLease = backgroundShellGate.tryAcquire();
|
|
472
|
+
if (!admissionLease) return backgroundShellSpawnError(execMsg, args.command, cwd, "background shell limit reached");
|
|
473
|
+
let child: ChildProcessWithoutNullStreams;
|
|
219
474
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
475
|
+
child = backgroundShellRuntime.spawn(args.command, { cwd, shell: true });
|
|
476
|
+
} catch (err) {
|
|
477
|
+
admissionLease.release();
|
|
478
|
+
return backgroundShellSpawnError(execMsg, args.command, cwd, errorText(err));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const shellId = nextShellId++;
|
|
482
|
+
let confirmClose!: () => void;
|
|
483
|
+
const closePromise = new Promise<void>(resolveClose => { confirmClose = resolveClose; });
|
|
484
|
+
const now = backgroundShellRuntime.now();
|
|
485
|
+
const entry: BackgroundShellEntry = {
|
|
486
|
+
shellId,
|
|
487
|
+
sessionId,
|
|
488
|
+
child,
|
|
489
|
+
admissionLease,
|
|
490
|
+
outputLength: 0,
|
|
491
|
+
startedAt: now,
|
|
492
|
+
lastActivityAt: now,
|
|
493
|
+
terminating: null,
|
|
494
|
+
closePromise,
|
|
495
|
+
confirmClose,
|
|
496
|
+
};
|
|
497
|
+
try {
|
|
498
|
+
child.on("close", () => confirmBackgroundShellClose(entry));
|
|
499
|
+
child.on("error", () => { /* error alone never confirms direct-child termination */ });
|
|
500
|
+
backgroundShells.set(shellId, entry);
|
|
223
501
|
child.stdout.on("data", chunk => {
|
|
224
|
-
|
|
225
|
-
if (shell) shell.outputLength += Buffer.byteLength(String(chunk));
|
|
502
|
+
noteBackgroundShellActivity(entry, Buffer.byteLength(String(chunk)));
|
|
226
503
|
});
|
|
227
504
|
child.stderr.on("data", chunk => {
|
|
228
|
-
|
|
229
|
-
if (shell) shell.outputLength += Buffer.byteLength(String(chunk));
|
|
505
|
+
noteBackgroundShellActivity(entry, Buffer.byteLength(String(chunk)));
|
|
230
506
|
});
|
|
231
|
-
|
|
507
|
+
armBackgroundShellIdleTimer(entry);
|
|
508
|
+
armBackgroundShellAbsoluteTimer(entry);
|
|
232
509
|
return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, {
|
|
233
510
|
result: {
|
|
234
511
|
case: "success",
|
|
@@ -236,9 +513,8 @@ export function backgroundShellSpawnExec(execMsg: ExecServerMessage): Uint8Array
|
|
|
236
513
|
},
|
|
237
514
|
}));
|
|
238
515
|
} catch (err) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}));
|
|
516
|
+
void terminateBackgroundShell(entry, "session_close");
|
|
517
|
+
return backgroundShellSpawnError(execMsg, args.command, cwd, errorText(err));
|
|
242
518
|
}
|
|
243
519
|
}
|
|
244
520
|
|
|
@@ -249,7 +525,7 @@ export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage):
|
|
|
249
525
|
}));
|
|
250
526
|
}
|
|
251
527
|
|
|
252
|
-
export function writeShellStdinExec(execMsg: ExecServerMessage): Uint8Array {
|
|
528
|
+
export function writeShellStdinExec(execMsg: ExecServerMessage, sessionId: string): Uint8Array {
|
|
253
529
|
if (execMsg.message.case !== "writeShellStdinArgs") throw new Error("invalid shell stdin exec");
|
|
254
530
|
const args = execMsg.message.value;
|
|
255
531
|
const shell = backgroundShells.get(args.shellId);
|
|
@@ -258,8 +534,14 @@ export function writeShellStdinExec(execMsg: ExecServerMessage): Uint8Array {
|
|
|
258
534
|
result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: `Unknown shell id ${args.shellId}` }) },
|
|
259
535
|
}));
|
|
260
536
|
}
|
|
537
|
+
if (shell.sessionId !== sessionId) {
|
|
538
|
+
return execBytes(execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, {
|
|
539
|
+
result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: "shell belongs to another session" }) },
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
261
542
|
const before = shell.outputLength;
|
|
262
543
|
shell.child.stdin.write(args.chars);
|
|
544
|
+
noteBackgroundShellActivity(shell);
|
|
263
545
|
return execBytes(execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, {
|
|
264
546
|
result: { case: "success", value: create(WriteShellStdinSuccessSchema, { shellId: args.shellId, terminalFileLengthBeforeInputWritten: before }) },
|
|
265
547
|
}));
|