@gajae-code/coding-agent 0.5.2 → 0.5.4

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 (99) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/async/job-manager.d.ts +6 -0
  3. package/dist/types/config/model-profiles.d.ts +10 -0
  4. package/dist/types/dap/client.d.ts +2 -1
  5. package/dist/types/edit/read-file.d.ts +6 -0
  6. package/dist/types/eval/js/context-manager.d.ts +3 -0
  7. package/dist/types/eval/js/executor.d.ts +1 -0
  8. package/dist/types/exec/bash-executor.d.ts +2 -0
  9. package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
  10. package/dist/types/lsp/types.d.ts +2 -0
  11. package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
  12. package/dist/types/modes/components/model-selector.d.ts +2 -0
  13. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  14. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
  15. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  16. package/dist/types/modes/interactive-mode.d.ts +1 -0
  17. package/dist/types/modes/types.d.ts +1 -0
  18. package/dist/types/runtime/process-lifecycle.d.ts +108 -0
  19. package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
  20. package/dist/types/runtime-mcp/types.d.ts +2 -0
  21. package/dist/types/session/agent-session.d.ts +29 -1
  22. package/dist/types/session/artifacts.d.ts +4 -1
  23. package/dist/types/session/streaming-output.d.ts +12 -0
  24. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
  25. package/dist/types/tools/bash.d.ts +1 -0
  26. package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
  27. package/dist/types/tools/sqlite-reader.d.ts +2 -1
  28. package/dist/types/web/search/providers/codex.d.ts +4 -4
  29. package/package.json +7 -7
  30. package/src/async/job-manager.ts +181 -43
  31. package/src/config/file-lock.ts +9 -1
  32. package/src/config/model-profile-activation.ts +71 -3
  33. package/src/config/model-profiles.ts +39 -14
  34. package/src/dap/client.ts +105 -64
  35. package/src/dap/session.ts +44 -7
  36. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -2
  37. package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -2
  38. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
  39. package/src/edit/read-file.ts +19 -1
  40. package/src/eval/js/context-manager.ts +228 -65
  41. package/src/eval/js/executor.ts +2 -0
  42. package/src/eval/js/index.ts +1 -0
  43. package/src/eval/js/worker-core.ts +10 -6
  44. package/src/eval/py/executor.ts +68 -19
  45. package/src/eval/py/kernel.ts +46 -22
  46. package/src/eval/py/runner.py +68 -14
  47. package/src/exec/bash-executor.ts +49 -13
  48. package/src/gjc-runtime/deep-interview-runtime.ts +14 -13
  49. package/src/gjc-runtime/ralplan-runtime.ts +10 -0
  50. package/src/gjc-runtime/state-runtime.ts +73 -0
  51. package/src/gjc-runtime/tmux-gc.ts +86 -37
  52. package/src/gjc-runtime/tmux-sessions.ts +44 -6
  53. package/src/gjc-runtime/ultragoal-runtime.ts +8 -4
  54. package/src/internal-urls/artifact-protocol.ts +10 -1
  55. package/src/internal-urls/docs-index.generated.ts +2 -2
  56. package/src/lsp/client.ts +64 -26
  57. package/src/lsp/index.ts +2 -1
  58. package/src/lsp/lspmux.ts +33 -9
  59. package/src/lsp/types.ts +2 -0
  60. package/src/modes/bridge/bridge-mode.ts +21 -0
  61. package/src/modes/components/assistant-message.ts +10 -2
  62. package/src/modes/components/bash-execution.ts +5 -1
  63. package/src/modes/components/eval-execution.ts +5 -1
  64. package/src/modes/components/model-selector.ts +34 -2
  65. package/src/modes/components/oauth-selector.ts +5 -0
  66. package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
  67. package/src/modes/components/skill-message.ts +24 -16
  68. package/src/modes/components/tool-execution.ts +6 -0
  69. package/src/modes/controllers/extension-ui-controller.ts +33 -6
  70. package/src/modes/controllers/input-controller.ts +19 -0
  71. package/src/modes/controllers/selector-controller.ts +6 -1
  72. package/src/modes/interactive-mode.ts +13 -0
  73. package/src/modes/types.ts +1 -0
  74. package/src/modes/utils/ui-helpers.ts +5 -2
  75. package/src/prompts/agents/executor.md +1 -1
  76. package/src/runtime/process-lifecycle.ts +400 -0
  77. package/src/runtime-mcp/manager.ts +164 -50
  78. package/src/runtime-mcp/transports/http.ts +12 -11
  79. package/src/runtime-mcp/transports/stdio.ts +64 -38
  80. package/src/runtime-mcp/types.ts +3 -0
  81. package/src/sdk.ts +27 -0
  82. package/src/session/agent-session.ts +271 -25
  83. package/src/session/artifacts.ts +17 -2
  84. package/src/session/blob-store.ts +36 -2
  85. package/src/session/session-manager.ts +29 -13
  86. package/src/session/streaming-output.ts +95 -3
  87. package/src/setup/model-onboarding-guidance.ts +10 -3
  88. package/src/skill-state/active-state.ts +79 -7
  89. package/src/slash-commands/builtin-registry.ts +30 -3
  90. package/src/slash-commands/helpers/fast-status-report.ts +111 -0
  91. package/src/tools/archive-reader.ts +10 -1
  92. package/src/tools/bash.ts +11 -4
  93. package/src/tools/browser/registry.ts +17 -1
  94. package/src/tools/browser/tab-supervisor.ts +22 -0
  95. package/src/tools/browser.ts +38 -4
  96. package/src/tools/cron.ts +2 -6
  97. package/src/tools/read.ts +11 -12
  98. package/src/tools/sqlite-reader.ts +19 -5
  99. package/src/web/search/providers/codex.ts +6 -5
@@ -5,7 +5,8 @@
5
5
  * Messages are newline-delimited JSON.
6
6
  */
7
7
 
8
- import { getProjectDir, ptree, readJsonl, Snowflake } from "@gajae-code/utils";
8
+ import { getProjectDir, readJsonl, Snowflake } from "@gajae-code/utils";
9
+ import { type OwnedProcess, spawnOwnedProcess } from "../../runtime/process-lifecycle";
9
10
  import type {
10
11
  JsonRpcError,
11
12
  JsonRpcMessage,
@@ -24,7 +25,7 @@ import { toJsonRpcError } from "../../runtime-mcp/types";
24
25
  const CLOSE_WAIT_MS = 1_000;
25
26
 
26
27
  export class StdioTransport implements MCPTransport {
27
- #process: ptree.ChildProcess<"pipe"> | null = null;
28
+ #process: OwnedProcess | null = null;
28
29
  #pendingRequests = new Map<
29
30
  string | number,
30
31
  {
@@ -34,6 +35,8 @@ export class StdioTransport implements MCPTransport {
34
35
  >();
35
36
  #connected = false;
36
37
  #readLoop: Promise<void> | null = null;
38
+ #stderrLoop: Promise<void> | null = null;
39
+ #closePromise: Promise<void> | null = null;
37
40
 
38
41
  onClose?: () => void;
39
42
  onError?: (error: Error) => void;
@@ -46,10 +49,17 @@ export class StdioTransport implements MCPTransport {
46
49
  return this.#connected;
47
50
  }
48
51
 
52
+ get closeBeforeReconnect(): true {
53
+ return true;
54
+ }
55
+
49
56
  /**
50
57
  * Start the subprocess and begin reading.
51
58
  */
52
59
  async connect(): Promise<void> {
60
+ if (this.#closePromise) {
61
+ throw new Error("Transport is closing");
62
+ }
53
63
  if (this.#connected) return;
54
64
 
55
65
  const args = this.config.args ?? [];
@@ -58,11 +68,12 @@ export class StdioTransport implements MCPTransport {
58
68
  ...this.config.env,
59
69
  };
60
70
 
61
- this.#process = ptree.spawn([this.config.command, ...args], {
71
+ this.#process = spawnOwnedProcess([this.config.command, ...args], {
62
72
  cwd: this.config.cwd ?? getProjectDir(),
63
73
  env,
64
74
  stdin: "pipe",
65
- stderr: "full",
75
+ gracefulMs: CLOSE_WAIT_MS,
76
+ name: `mcp-stdio:${this.config.command}`,
66
77
  });
67
78
 
68
79
  this.#connected = true;
@@ -71,13 +82,13 @@ export class StdioTransport implements MCPTransport {
71
82
  this.#readLoop = this.#startReadLoop();
72
83
 
73
84
  // Log stderr for debugging
74
- this.#startStderrLoop();
85
+ this.#stderrLoop = this.#startStderrLoop();
75
86
  }
76
87
 
77
88
  async #startReadLoop(): Promise<void> {
78
- if (!this.#process?.stdout) return;
89
+ if (!this.#process?.child.stdout) return;
79
90
  try {
80
- for await (const line of readJsonl(this.#process.stdout)) {
91
+ for await (const line of readJsonl(this.#process.child.stdout)) {
81
92
  if (!this.#connected) break;
82
93
  try {
83
94
  this.#handleMessage(line as JsonRpcMessage);
@@ -95,9 +106,9 @@ export class StdioTransport implements MCPTransport {
95
106
  }
96
107
 
97
108
  async #startStderrLoop(): Promise<void> {
98
- if (!this.#process?.stderr) return;
109
+ if (!this.#process?.child.stderr) return;
99
110
 
100
- const reader = this.#process.stderr.getReader();
111
+ const reader = this.#process.child.stderr.getReader();
101
112
  const decoder = new TextDecoder();
102
113
 
103
114
  try {
@@ -168,26 +179,23 @@ export class StdioTransport implements MCPTransport {
168
179
  }
169
180
  }
170
181
 
182
+ #getStdin(): Bun.FileSink | null {
183
+ const stdin = this.#process?.child.stdin;
184
+ return typeof stdin === "object" && stdin !== null ? stdin : null;
185
+ }
186
+
171
187
  #sendResponse(id: string | number, result?: unknown, error?: JsonRpcError): void {
172
- if (!this.#connected || !this.#process?.stdin) return;
188
+ const stdin = this.#getStdin();
189
+ if (!this.#connected || !stdin) return;
173
190
  const response = error
174
191
  ? { jsonrpc: "2.0" as const, id, error }
175
192
  : { jsonrpc: "2.0" as const, id, result: result ?? {} };
176
- this.#process.stdin.write(`${JSON.stringify(response)}\n`);
177
- this.#process.stdin.flush();
193
+ stdin.write(`${JSON.stringify(response)}\n`);
194
+ stdin.flush();
178
195
  }
179
196
 
180
197
  #handleClose(): void {
181
- if (!this.#connected) return;
182
- this.#connected = false;
183
-
184
- // Reject all pending requests
185
- for (const [, pending] of this.#pendingRequests) {
186
- pending.reject(new Error("Transport closed"));
187
- }
188
- this.#pendingRequests.clear();
189
-
190
- this.onClose?.();
198
+ void this.#closeInternal(true);
191
199
  }
192
200
 
193
201
  async request<T = unknown>(
@@ -195,7 +203,8 @@ export class StdioTransport implements MCPTransport {
195
203
  params?: Record<string, unknown>,
196
204
  options?: MCPRequestOptions,
197
205
  ): Promise<T> {
198
- if (!this.#connected || !this.#process?.stdin) {
206
+ const stdin = this.#getStdin();
207
+ if (!this.#connected || !stdin) {
199
208
  throw new Error("Transport not connected");
200
209
  }
201
210
 
@@ -261,8 +270,8 @@ export class StdioTransport implements MCPTransport {
261
270
  const message = `${JSON.stringify(request)}\n`;
262
271
  try {
263
272
  // Bun's FileSink has write() method directly
264
- this.#process.stdin.write(message);
265
- this.#process.stdin.flush();
273
+ stdin.write(message);
274
+ stdin.flush();
266
275
  } catch (error: unknown) {
267
276
  cleanup();
268
277
  reject(error instanceof Error ? error : new Error(String(error)));
@@ -272,7 +281,8 @@ export class StdioTransport implements MCPTransport {
272
281
  }
273
282
 
274
283
  async notify(method: string, params?: Record<string, unknown>): Promise<void> {
275
- if (!this.#connected || !this.#process?.stdin) {
284
+ const stdin = this.#getStdin();
285
+ if (!this.#connected || !stdin) {
276
286
  throw new Error("Transport not connected");
277
287
  }
278
288
 
@@ -284,35 +294,51 @@ export class StdioTransport implements MCPTransport {
284
294
 
285
295
  const message = `${JSON.stringify(notification)}\n`;
286
296
  // Bun's FileSink has write() method directly
287
- this.#process.stdin.write(message);
288
- this.#process.stdin.flush();
297
+ stdin.write(message);
298
+ stdin.flush();
289
299
  }
290
300
 
291
301
  async close(): Promise<void> {
292
- if (!this.#connected) return;
302
+ await this.#closeInternal(false);
303
+ }
304
+
305
+ #closeInternal(fromReadLoop: boolean): Promise<void> {
306
+ if (this.#closePromise) return this.#closePromise;
307
+ this.#closePromise = this.#finishClose(fromReadLoop).finally(() => {
308
+ this.#closePromise = null;
309
+ });
310
+ return this.#closePromise;
311
+ }
312
+
313
+ async #finishClose(fromReadLoop: boolean): Promise<void> {
314
+ const wasConnected = this.#connected;
293
315
  this.#connected = false;
294
316
 
295
- // Reject pending requests
296
317
  for (const [, pending] of this.#pendingRequests) {
297
318
  pending.reject(new Error("Transport closed"));
298
319
  }
299
320
  this.#pendingRequests.clear();
300
321
 
301
- // Terminate the subprocess tree and keep the handle until exit is observed.
322
+ const stdin = this.#getStdin();
302
323
  const process = this.#process;
324
+ this.#process = null;
303
325
  if (process) {
304
- process.kill();
305
- await Promise.race([process.exited.catch(() => {}), Bun.sleep(CLOSE_WAIT_MS)]);
306
- this.#process = null;
326
+ stdin?.end();
327
+ await process.dispose().catch(() => {});
328
+ await process.awaitExit({ timeoutMs: CLOSE_WAIT_MS }).catch(() => ({ exited: false, code: null }));
307
329
  }
308
330
 
309
- // Wait for read loop to finish
310
- if (this.#readLoop) {
331
+ if (!fromReadLoop && this.#readLoop) {
311
332
  await this.#readLoop.catch(() => {});
312
- this.#readLoop = null;
333
+ }
334
+ this.#readLoop = null;
335
+
336
+ if (this.#stderrLoop) {
337
+ await this.#stderrLoop.catch(() => {});
338
+ this.#stderrLoop = null;
313
339
  }
314
340
 
315
- this.onClose?.();
341
+ if (wasConnected) this.onClose?.();
316
342
  }
317
343
  }
318
344
 
@@ -225,6 +225,9 @@ export interface MCPTransport {
225
225
  /** Close the transport */
226
226
  close(): Promise<void>;
227
227
 
228
+ /** Whether close must finish before reconnect can safely spawn a replacement. */
229
+ readonly closeBeforeReconnect?: boolean;
230
+
228
231
  /** Whether the transport is connected */
229
232
  readonly connected: boolean;
230
233
 
package/src/sdk.ts CHANGED
@@ -52,6 +52,7 @@ import { resolveConfigValue } from "./config/resolve-config-value";
52
52
  import { getEmbeddedDefaultGjcSkills } from "./defaults/gjc-defaults";
53
53
  import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory } from "./defaults/gjc-grok-cli";
54
54
  import { initializeWithSettings } from "./discovery";
55
+ import { disposeAllVmContexts, disposeVmContextsByOwner } from "./eval/js/context-manager";
55
56
  import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
56
57
  import { TtsrManager } from "./export/ttsr";
57
58
  import type { CustomCommandsLoadResult, LoadedCustomCommand } from "./extensibility/custom-commands";
@@ -414,6 +415,7 @@ function getDefaultAgentDir(): string {
414
415
  */
415
416
  export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): Promise<AuthStorage> {
416
417
  const brokerConfig = await resolveAuthBrokerConfig();
418
+ const credentialRankingMode = resolveCredentialRankingMode();
417
419
  if (brokerConfig) {
418
420
  const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
419
421
  const initialResult = await client.fetchSnapshot();
@@ -424,6 +426,7 @@ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir(
424
426
  const storage = new AuthStorage(store, {
425
427
  configValueResolver: resolveConfigValue,
426
428
  sourceLabel: `broker ${brokerConfig.url}`,
429
+ credentialRankingMode,
427
430
  });
428
431
  await storage.reload();
429
432
  return storage;
@@ -432,11 +435,25 @@ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir(
432
435
  const storage = await AuthStorage.create(dbPath, {
433
436
  configValueResolver: resolveConfigValue,
434
437
  sourceLabel: `local ${dbPath}`,
438
+ credentialRankingMode,
435
439
  });
436
440
  await storage.reload();
437
441
  return storage;
438
442
  }
439
443
 
444
+ /**
445
+ * Opt-in multi-account credential ranking mode, read from the
446
+ * `GJC_CREDENTIAL_RANKING_MODE` env var. Unset/unknown → `undefined`, leaving
447
+ * {@link AuthStorage}'s default (`balanced`) untouched. `earliest-reset`
448
+ * switches to earliest-expiry-first selection so soon-to-reset tumbling-window
449
+ * quota is drained before it is lost.
450
+ */
451
+ function resolveCredentialRankingMode(): "balanced" | "earliest-reset" | undefined {
452
+ const raw = process.env.GJC_CREDENTIAL_RANKING_MODE?.trim();
453
+ if (raw === "balanced" || raw === "earliest-reset") return raw;
454
+ return undefined;
455
+ }
456
+
440
457
  /**
441
458
  * Discover extensions from cwd.
442
459
  */
@@ -570,6 +587,14 @@ function registerPythonCleanup(): void {
570
587
  postmortem.register("python-cleanup", disposeAllKernelSessions);
571
588
  }
572
589
 
590
+ let jsVmCleanupRegistered = false;
591
+
592
+ function registerJsVmCleanup(): void {
593
+ if (jsVmCleanupRegistered) return;
594
+ jsVmCleanupRegistered = true;
595
+ postmortem.register("js-vm-cleanup", disposeAllVmContexts);
596
+ }
597
+
573
598
  /**
574
599
  * Resolve whether to enable append-only context mode based on the setting and provider.
575
600
  *
@@ -806,6 +831,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
806
831
 
807
832
  registerSshCleanup();
808
833
  registerPythonCleanup();
834
+ registerJsVmCleanup();
809
835
 
810
836
  // Pin authStorage to modelRegistry.authStorage: ModelRegistry.getApiKey() routes refresh
811
837
  // failures through that instance, so any divergent storage handed to the bridge / mcpManager
@@ -2200,6 +2226,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2200
2226
  } else {
2201
2227
  if (hasRegistered) agentRegistry.unregister(resolvedAgentId);
2202
2228
  await disposeKernelSessionsByOwner(evalKernelOwnerId);
2229
+ await disposeVmContextsByOwner(evalKernelOwnerId);
2203
2230
  }
2204
2231
  } catch (cleanupError) {
2205
2232
  logger.warn("Failed to clean up createAgentSession resources after startup error", {