@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.
Files changed (175) hide show
  1. package/README.md +24 -0
  2. package/bin/ocx.mjs +32 -4
  3. package/gui/dist/assets/index-CHwf3tTD.css +1 -0
  4. package/gui/dist/assets/index-u5eFOv2y.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-image-normalize.ts +114 -20
  8. package/src/adapters/anthropic.ts +126 -10
  9. package/src/adapters/azure.ts +3 -3
  10. package/src/adapters/base.ts +7 -3
  11. package/src/adapters/cursor/discovery.ts +14 -4
  12. package/src/adapters/cursor/effort-map.ts +18 -6
  13. package/src/adapters/cursor/framing.ts +102 -27
  14. package/src/adapters/cursor/kv-store.ts +30 -3
  15. package/src/adapters/cursor/live-models.ts +22 -2
  16. package/src/adapters/cursor/live-transport.ts +245 -49
  17. package/src/adapters/cursor/mcp-manager.ts +105 -8
  18. package/src/adapters/cursor/native-exec-mcp.ts +5 -3
  19. package/src/adapters/cursor/native-exec-shell.ts +296 -14
  20. package/src/adapters/cursor/native-exec.ts +381 -33
  21. package/src/adapters/cursor/protobuf-events.ts +28 -1
  22. package/src/adapters/cursor/protobuf-request.ts +71 -39
  23. package/src/adapters/cursor/request-builder.ts +2 -2
  24. package/src/adapters/cursor/transport.ts +2 -0
  25. package/src/adapters/cursor.ts +13 -2
  26. package/src/adapters/google-antigravity-replay.ts +184 -17
  27. package/src/adapters/google.ts +58 -8
  28. package/src/adapters/kiro-thinking.ts +23 -9
  29. package/src/adapters/kiro-tools.ts +49 -18
  30. package/src/adapters/kiro.ts +377 -133
  31. package/src/adapters/mimo-free.ts +36 -4
  32. package/src/adapters/openai-chat.ts +143 -17
  33. package/src/adapters/openai-responses.ts +130 -14
  34. package/src/adapters/run-turn-queue.ts +7 -1
  35. package/src/bridge.ts +466 -69
  36. package/src/chat/outbound.ts +144 -38
  37. package/src/claude/inbound-debug.ts +53 -8
  38. package/src/claude/outbound.ts +224 -38
  39. package/src/cli/agent-driven.ts +34 -1
  40. package/src/cli/catalog-prewarm.ts +5 -2
  41. package/src/cli/claude-desktop.ts +2 -2
  42. package/src/cli/doctor.ts +12 -0
  43. package/src/cli/export-command.ts +187 -0
  44. package/src/cli/help.ts +11 -0
  45. package/src/cli/index.ts +13 -3
  46. package/src/cli/init.ts +129 -102
  47. package/src/cli/opencode.ts +36 -151
  48. package/src/cli/star-prompt.ts +13 -4
  49. package/src/cli/status-oauth.ts +12 -2
  50. package/src/clients/config-export.ts +377 -0
  51. package/src/codex/account-runtime-state.ts +19 -1
  52. package/src/codex/account-store.ts +162 -82
  53. package/src/codex/auth-api.ts +467 -159
  54. package/src/codex/auth-context.ts +15 -2
  55. package/src/codex/catalog/aggregation.ts +15 -0
  56. package/src/codex/catalog/effort.ts +16 -6
  57. package/src/codex/catalog/metadata.ts +6 -0
  58. package/src/codex/catalog/parsing.ts +3 -1
  59. package/src/codex/catalog/provider-fetch.ts +29 -0
  60. package/src/codex/catalog/sync.ts +64 -7
  61. package/src/codex/catalog.ts +2 -2
  62. package/src/codex/inject.ts +5 -5
  63. package/src/codex/main-account-cache.ts +8 -1
  64. package/src/codex/model-cache.ts +81 -2
  65. package/src/codex/pool-rotation.ts +39 -0
  66. package/src/codex/project-config-warnings.ts +12 -1
  67. package/src/codex/quota.ts +35 -3
  68. package/src/codex/routing.ts +46 -1
  69. package/src/codex/shim.ts +10 -4
  70. package/src/codex/subagent-model-fallback.ts +12 -0
  71. package/src/codex/websocket-registry.ts +27 -0
  72. package/src/combos/failover.ts +31 -1
  73. package/src/combos/request.ts +9 -0
  74. package/src/combos/resolve.ts +60 -4
  75. package/src/combos/types.ts +12 -0
  76. package/src/config.ts +510 -55
  77. package/src/github/star-state.ts +13 -1
  78. package/src/images/fulfill.ts +39 -1
  79. package/src/images/loop.ts +52 -12
  80. package/src/lib/admission.ts +83 -0
  81. package/src/lib/app-owned-memory-stores.ts +173 -0
  82. package/src/lib/app-owned-memory.ts +265 -0
  83. package/src/lib/bun-stream-caps.ts +31 -7
  84. package/src/lib/config-ownership.ts +33 -0
  85. package/src/lib/crash-guard.ts +65 -5
  86. package/src/lib/debug-log-buffer.ts +47 -6
  87. package/src/lib/destination-policy.ts +12 -1
  88. package/src/lib/errors.ts +3 -0
  89. package/src/lib/gcp-adc.ts +40 -2
  90. package/src/lib/injection-debug-log.ts +26 -2
  91. package/src/lib/provider-outbound.ts +3 -0
  92. package/src/lib/sidecar-tracker.ts +5 -2
  93. package/src/lib/sse-decoder.ts +257 -37
  94. package/src/lib/state-store-registrations.ts +109 -0
  95. package/src/lib/state-store-sweeper.ts +184 -0
  96. package/src/lib/translator-budget.ts +356 -0
  97. package/src/lib/windows-secret-acl.ts +33 -12
  98. package/src/lib/winsw.ts +14 -1
  99. package/src/oauth/anthropic-routing.ts +31 -7
  100. package/src/oauth/google-antigravity.ts +2 -1
  101. package/src/oauth/health.ts +30 -12
  102. package/src/oauth/index.ts +127 -23
  103. package/src/oauth/kiro-credentials.ts +72 -1
  104. package/src/oauth/kiro.ts +23 -4
  105. package/src/oauth/store.ts +165 -18
  106. package/src/oauth/token-guardian.ts +43 -4
  107. package/src/oauth/types.ts +2 -1
  108. package/src/providers/base-url-choices.ts +10 -0
  109. package/src/providers/derive.ts +12 -0
  110. package/src/providers/free-directory.ts +4 -1
  111. package/src/providers/key-failover.ts +12 -0
  112. package/src/providers/openai-sidecar.ts +4 -1
  113. package/src/providers/quota.ts +68 -7
  114. package/src/providers/registry.ts +279 -3
  115. package/src/responses/parser.ts +5 -1
  116. package/src/responses/spill-store.ts +394 -0
  117. package/src/responses/state.ts +520 -102
  118. package/src/router.ts +18 -1
  119. package/src/server/adapter-resolve.ts +20 -3
  120. package/src/server/auth-cors.ts +121 -28
  121. package/src/server/chat-completions.ts +57 -12
  122. package/src/server/claude-messages.ts +85 -13
  123. package/src/server/index.ts +242 -100
  124. package/src/server/lifecycle.ts +155 -25
  125. package/src/server/management/agent-settings-routes.ts +79 -36
  126. package/src/server/management/api-key-usage.ts +167 -0
  127. package/src/server/management/body.ts +35 -0
  128. package/src/server/management/combo-routes.ts +5 -1
  129. package/src/server/management/config-routes.ts +42 -12
  130. package/src/server/management/logs-usage-routes.ts +41 -21
  131. package/src/server/management/model-routes.ts +188 -54
  132. package/src/server/management/oauth-account-routes.ts +115 -26
  133. package/src/server/management/provider-routes.ts +56 -6
  134. package/src/server/management/shared.ts +16 -3
  135. package/src/server/management/sidebar-routes.ts +50 -1
  136. package/src/server/management/system-restart.ts +13 -6
  137. package/src/server/management/system-routes.ts +15 -3
  138. package/src/server/management/usage-summary-cache.ts +86 -0
  139. package/src/server/management-api.ts +39 -5
  140. package/src/server/management-auth.ts +65 -14
  141. package/src/server/port-reclaim.ts +58 -12
  142. package/src/server/ports.ts +2 -0
  143. package/src/server/proxy-liveness.ts +60 -14
  144. package/src/server/relay-eager.ts +20 -4
  145. package/src/server/relay.ts +548 -154
  146. package/src/server/request-decompress.ts +51 -4
  147. package/src/server/request-log.ts +134 -15
  148. package/src/server/responses/collaboration.ts +15 -4
  149. package/src/server/responses/compact.ts +3 -0
  150. package/src/server/responses/core.ts +241 -66
  151. package/src/server/responses-image-gen-repair.ts +19 -5
  152. package/src/server/responses-item-id-repair.ts +23 -5
  153. package/src/server/sse-payload-rewrite.ts +71 -12
  154. package/src/server/startup-health-cache.ts +14 -1
  155. package/src/server/system-env.ts +8 -1
  156. package/src/server/windows-tcp-drop.ts +15 -5
  157. package/src/server/ws-bridge.ts +25 -0
  158. package/src/service.ts +179 -13
  159. package/src/storage/policy-job.ts +93 -23
  160. package/src/storage/policy-worker.ts +6 -0
  161. package/src/storage/restore-job.ts +62 -16
  162. package/src/storage/restore-worker.ts +6 -0
  163. package/src/storage/storage-mutation-coordinator.ts +36 -6
  164. package/src/storage/worker-lifecycle.ts +181 -47
  165. package/src/tray/windows.ts +97 -25
  166. package/src/types.ts +39 -6
  167. package/src/update/index.ts +24 -5
  168. package/src/update/job.ts +598 -73
  169. package/src/usage/log.ts +115 -17
  170. package/src/usage/summary.ts +67 -2
  171. package/src/vision/index.ts +112 -22
  172. package/src/web-search/loop.ts +38 -6
  173. package/src/web-search/progress-stream.ts +14 -3
  174. package/gui/dist/assets/index-BDjpkcRN.js +0 -67
  175. 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 backgroundShells = new Map<number, { child: ChildProcessWithoutNullStreams; outputLength: number }>();
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
- export function backgroundShellSpawnExec(execMsg: ExecServerMessage): Uint8Array {
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
- const child = spawn(args.command, { cwd, shell: true });
221
- const shellId = nextShellId++;
222
- backgroundShells.set(shellId, { child, outputLength: 0 });
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
- const shell = backgroundShells.get(shellId);
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
- const shell = backgroundShells.get(shellId);
229
- if (shell) shell.outputLength += Buffer.byteLength(String(chunk));
505
+ noteBackgroundShellActivity(entry, Buffer.byteLength(String(chunk)));
230
506
  });
231
- child.on("close", () => backgroundShells.delete(shellId));
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
- return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, {
240
- result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: errorText(err) }) },
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
  }));