@gajae-code/coding-agent 0.12.8 → 0.12.11

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 (51) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/types/cli/notify-cli.d.ts +1 -0
  3. package/dist/types/commands/sdk.d.ts +23 -0
  4. package/dist/types/discovery/agents-md.d.ts +11 -1
  5. package/dist/types/modes/types.d.ts +6 -0
  6. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
  7. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  8. package/dist/types/sdk/bus/notification-service.d.ts +23 -0
  9. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -3
  10. package/dist/types/session/internal/managed-session-storage.d.ts +20 -2
  11. package/dist/types/session/internal/native-publish-outcome.d.ts +2 -2
  12. package/dist/types/session/session-manager.d.ts +17 -0
  13. package/dist/types/session/session-storage.d.ts +1 -0
  14. package/dist/types/system-prompt.d.ts +12 -1
  15. package/dist/types/tools/composer-bash-policy.d.ts +9 -1
  16. package/dist/types/tools/computer.d.ts +35 -2
  17. package/dist/types/tools/todo-write.d.ts +2 -0
  18. package/package.json +8 -8
  19. package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
  20. package/src/cli/notify-cli.ts +12 -6
  21. package/src/commands/sdk.ts +54 -0
  22. package/src/config/model-profiles.ts +8 -0
  23. package/src/config/model-registry.ts +4 -1
  24. package/src/discovery/agents-md.ts +76 -19
  25. package/src/gjc-runtime/psmux-detect.ts +12 -16
  26. package/src/gjc-runtime/tmux-sessions.ts +8 -1
  27. package/src/internal-urls/docs-index.generated.ts +6 -5
  28. package/src/modes/components/extensions/inspector-panel.ts +9 -5
  29. package/src/modes/controllers/selector-controller.ts +61 -43
  30. package/src/modes/print-mode.ts +3 -0
  31. package/src/modes/types.ts +6 -0
  32. package/src/prompts/system/system-prompt.md +1 -0
  33. package/src/prompts/tools/computer.md +6 -1
  34. package/src/sdk/bus/chat-daemon-control.ts +5 -3
  35. package/src/sdk/bus/daemon-paths.ts +2 -0
  36. package/src/sdk/bus/notification-service.ts +140 -7
  37. package/src/sdk/bus/telegram-daemon-contract.ts +4 -3
  38. package/src/sdk/session.ts +24 -7
  39. package/src/session/agent-session.ts +176 -34
  40. package/src/session/internal/managed-session-storage.ts +280 -248
  41. package/src/session/internal/native-publish-outcome.ts +12 -2
  42. package/src/session/session-manager.ts +140 -6
  43. package/src/session/session-storage.ts +3 -0
  44. package/src/slash-commands/builtin-registry.ts +35 -11
  45. package/src/system-prompt.ts +28 -15
  46. package/src/tools/bash.ts +3 -1
  47. package/src/tools/composer-bash-policy.ts +21 -5
  48. package/src/tools/computer.ts +322 -45
  49. package/src/tools/todo-write.ts +29 -11
  50. package/src/utils/command-args.ts +19 -30
  51. package/src/utils/lang-from-path.ts +2 -4
@@ -9,6 +9,7 @@ import { applyStartupModelProfiles, createSessionManager } from "../main";
9
9
  import { initializeExtensions } from "../modes/runtime-init";
10
10
  import { ACP_MCP_REQUEST_TIMEOUT_MS, ACP_MCP_STARTUP_HEADROOM_MS } from "../sdk/acp/mcp";
11
11
  import { Broker } from "../sdk/broker/broker";
12
+ import { readBrokerDiscovery } from "../sdk/broker/discovery";
12
13
  import { completeBrokerProcess } from "../sdk/broker/internal";
13
14
  import {
14
15
  type LifecycleTranscriptEvidence,
@@ -58,6 +59,54 @@ export async function lifecycleArgs(
58
59
  };
59
60
  }
60
61
 
62
+ /**
63
+ * How long a session host tolerates the complete absence of a live broker
64
+ * publication before treating itself as orphaned. Hosts intentionally survive
65
+ * broker restarts (a replacement broker republishes discovery within seconds),
66
+ * so this must comfortably exceed a restart window while still bounding the
67
+ * lifetime of hosts whose broker is gone for good — otherwise every crashed or
68
+ * torn-down broker leaks a detached multi-hundred-megabyte host forever.
69
+ */
70
+ export const SESSION_HOST_BROKER_ABSENCE_GRACE_MS = 10 * 60_000;
71
+ const SESSION_HOST_BROKER_POLL_MS = 15_000;
72
+
73
+ /**
74
+ * Resolves only once no live broker publication has been observable in
75
+ * `agentDir` for the full grace window. A reappearing broker (including a
76
+ * replacement with a different pid) resets the window; an unreadable
77
+ * publication is not proof of orphanhood but accrues against the same bound.
78
+ */
79
+ export async function watchSessionHostBrokerLiveness(deps: {
80
+ agentDir: string;
81
+ now?: () => number;
82
+ sleep?: (ms: number) => Promise<void>;
83
+ readDiscovery?: (agentDir: string) => Promise<unknown>;
84
+ graceMs?: number;
85
+ pollMs?: number;
86
+ }): Promise<void> {
87
+ const now = deps.now ?? Date.now;
88
+ const sleep = deps.sleep ?? (async ms => await Bun.sleep(ms));
89
+ const readDiscovery = deps.readDiscovery ?? readBrokerDiscovery;
90
+ const graceMs = deps.graceMs ?? SESSION_HOST_BROKER_ABSENCE_GRACE_MS;
91
+ const pollMs = deps.pollMs ?? SESSION_HOST_BROKER_POLL_MS;
92
+ let absentSince: number | null = null;
93
+ for (;;) {
94
+ let live: unknown = null;
95
+ try {
96
+ live = await readDiscovery(deps.agentDir);
97
+ } catch {
98
+ // Transient read failures are ambiguity, not proof of orphanhood.
99
+ }
100
+ if (live) {
101
+ absentSince = null;
102
+ } else {
103
+ absentSince ??= now();
104
+ if (now() - absentSince >= graceMs) return;
105
+ }
106
+ await sleep(pollMs);
107
+ }
108
+ }
109
+
61
110
  type LifecycleTranscriptSource = {
62
111
  cwd: string;
63
112
  path: string;
@@ -505,6 +554,11 @@ export async function runSessionHost(
505
554
  }
506
555
  process.once("SIGTERM", stop);
507
556
  process.once("SIGINT", stop);
557
+ // A detached host whose broker is gone for good would otherwise live (and
558
+ // hold its session's memory) forever; reap it through the same graceful
559
+ // teardown a SIGTERM would take once the bounded absence grace elapses.
560
+ await watchSessionHostBrokerLiveness({ agentDir });
561
+ stop();
508
562
  await new Promise<void>(() => {});
509
563
  }
510
564
 
@@ -291,6 +291,13 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
291
291
  architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
292
292
  critic: "alibaba-token-plan/glm-5.2:high",
293
293
  }),
294
+ profile("alibaba-token-plan-pro", ["alibaba-token-plan"], {
295
+ default: "alibaba-token-plan/qwen3.8-max-preview:medium",
296
+ executor: "alibaba-token-plan/deepseek-v4-flash-0731:max",
297
+ planner: "alibaba-token-plan/glm-5.2:high",
298
+ architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
299
+ critic: "alibaba-token-plan/glm-5.2:xhigh",
300
+ }),
294
301
  profile("alibaba-token-plan-qwenmaxxing", ["alibaba-token-plan"], {
295
302
  default: "alibaba-token-plan/qwen3.8-max-preview:medium",
296
303
  executor: "alibaba-token-plan/qwen3.8-max-preview:low",
@@ -361,6 +368,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
361
368
  "minimax-medium": { displayName: "MiniMax Medium", providerGroup: "MINIMAX" },
362
369
  "minimax-pro": { displayName: "MiniMax Pro", providerGroup: "MINIMAX" },
363
370
  "alibaba-token-plan-balanced": { displayName: "Balanced", providerGroup: "ALIBABA TOKEN PLAN" },
371
+ "alibaba-token-plan-pro": { displayName: "Pro", providerGroup: "ALIBABA TOKEN PLAN" },
364
372
  "alibaba-token-plan-qwenmaxxing": { displayName: "QwenMaxxing", providerGroup: "ALIBABA TOKEN PLAN" },
365
373
  "opus-codex": { displayName: "Opus + Codex", providerGroup: "COMBOS" },
366
374
  "codex-opencodego": { displayName: "Codex + OpenCodeGo", providerGroup: "COMBOS" },
@@ -331,7 +331,10 @@ function validateProviderConfiguration(
331
331
  throw new Error(
332
332
  mode === "runtime-register"
333
333
  ? `Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`
334
- : `Provider ${providerName}: "apiKey" or "apiKeyEnv" is required when defining custom models unless auth is "none".`,
334
+ : `Provider ${providerName}: custom models need a credential source, but none is configured. ` +
335
+ `"auth" only selects the scheme ("auth: apiKey" does not supply a key). ` +
336
+ `Fix by adding "apiKeyEnv: <ENV_VAR>" (recommended) or "apiKey: <literal-key>", ` +
337
+ `or set "auth: none" if the endpoint is genuinely unauthenticated.`,
335
338
  );
336
339
  }
337
340
  }
@@ -5,56 +5,113 @@
5
5
  * This handles AGENTS.md files that live in project root (not in config directories
6
6
  * like .OpenAI code backend/ or .gemini/, which are handled by their respective providers).
7
7
  */
8
+ import * as fs from "node:fs/promises";
8
9
  import * as path from "node:path";
9
10
  import { registerProvider } from "../capability";
10
11
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
11
- import { readFile } from "../capability/fs";
12
12
  import type { LoadContext, LoadResult } from "../capability/types";
13
13
  import { calculateDepth, createSourceMeta } from "./helpers";
14
14
 
15
15
  const PROVIDER_ID = "agents-md";
16
16
  const DISPLAY_NAME = "AGENTS.md";
17
+ // Bound hostile ancestor walks and instruction payloads before they reach prompt assembly.
18
+ const MAX_ANCESTOR_DIRECTORIES = 32;
19
+ const MAX_FILE_BYTES = 64 * 1024;
20
+ const MAX_AGGREGATE_BYTES = 256 * 1024;
21
+ const DIRECTORY_LIMIT_WARNING = "AGENTS.md discovery stopped after scanning 32 ancestor directories.";
22
+ const FILE_LIMIT_WARNING = "Skipped one or more AGENTS.md files that exceed the 64 KiB limit.";
23
+ const AGGREGATE_LIMIT_WARNING = "Skipped one or more AGENTS.md files that exceed the 256 KiB aggregate limit.";
24
+
25
+ export type AgentsMdReader = (
26
+ filePath: string,
27
+ maxBytes: number,
28
+ ) => Promise<{ content: string | null; byteLength: number; tooLarge: boolean }>;
29
+
30
+ async function readBoundedAgentsMdFile(
31
+ filePath: string,
32
+ maxBytes: number,
33
+ ): Promise<{ content: string | null; byteLength: number; tooLarge: boolean }> {
34
+ try {
35
+ const file = await fs.open(filePath, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
36
+ try {
37
+ const stats = await file.stat();
38
+ if (!stats.isFile()) return { content: null, byteLength: 0, tooLarge: false };
39
+ const bytes = Buffer.alloc(maxBytes + 1);
40
+ let bytesRead = 0;
41
+ while (bytesRead < bytes.length) {
42
+ const result = await file.read(bytes, bytesRead, bytes.length - bytesRead, bytesRead);
43
+ if (result.bytesRead === 0) break;
44
+ bytesRead += result.bytesRead;
45
+ }
46
+ if (bytesRead > maxBytes) return { content: null, byteLength: bytesRead, tooLarge: true };
47
+ return {
48
+ content: new TextDecoder().decode(bytes.subarray(0, bytesRead)),
49
+ byteLength: bytesRead,
50
+ tooLarge: false,
51
+ };
52
+ } finally {
53
+ await file.close();
54
+ }
55
+ } catch {
56
+ return { content: null, byteLength: 0, tooLarge: false };
57
+ }
58
+ }
17
59
 
18
60
  /**
19
61
  * Load standalone AGENTS.md files.
20
62
  */
21
- async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
63
+ export async function loadAgentsMd(
64
+ ctx: LoadContext,
65
+ readCandidate: AgentsMdReader = readBoundedAgentsMdFile,
66
+ ): Promise<LoadResult<ContextFile>> {
22
67
  const items: ContextFile[] = [];
23
68
  const warnings: string[] = [];
24
-
25
- // Walk up from cwd looking for AGENTS.md files
26
69
  let current = ctx.cwd;
70
+ let scannedDirectories = 0;
71
+ let aggregateBytes = 0;
72
+ let omittedOversizedFile = false;
73
+ let omittedAggregateFile = false;
27
74
 
28
75
  while (true) {
76
+ scannedDirectories += 1;
29
77
  const candidate = path.join(current, "AGENTS.md");
30
- const content = await readFile(candidate);
31
-
32
- if (content !== null) {
33
- const parent = path.dirname(candidate);
34
- const baseName = parent.split(path.sep).pop() ?? "";
78
+ const parent = path.dirname(candidate);
79
+ const baseName = parent.split(path.sep).pop() ?? "";
35
80
 
36
- if (!baseName.startsWith(".")) {
81
+ if (!baseName.startsWith(".")) {
82
+ const remainingAggregateBytes = MAX_AGGREGATE_BYTES - aggregateBytes;
83
+ const allowedBytes = Math.min(MAX_FILE_BYTES, remainingAggregateBytes);
84
+ const result = await readCandidate(candidate, allowedBytes);
85
+ if (result.tooLarge) {
86
+ if (allowedBytes < MAX_FILE_BYTES) omittedAggregateFile = true;
87
+ else omittedOversizedFile = true;
88
+ } else if (result.content !== null) {
37
89
  const fileDir = path.dirname(candidate);
38
- const calculatedDepth = calculateDepth(ctx.cwd, fileDir, path.sep);
39
-
40
90
  items.push({
41
91
  path: candidate,
42
- content,
92
+ content: result.content,
43
93
  level: "project",
44
- depth: calculatedDepth,
94
+ depth: calculateDepth(ctx.cwd, fileDir, path.sep),
45
95
  _source: createSourceMeta(PROVIDER_ID, candidate, "project"),
46
96
  });
97
+ aggregateBytes += result.byteLength;
47
98
  }
48
99
  }
49
100
 
50
- if (current === (ctx.repoRoot ?? ctx.home)) break; // scanned repo root or home, stop
101
+ const stopDirectory = ctx.repoRoot ?? ctx.home;
102
+ if (current === stopDirectory) break;
103
+ if (scannedDirectories === MAX_ANCESTOR_DIRECTORIES) {
104
+ warnings.push(DIRECTORY_LIMIT_WARNING);
105
+ break;
106
+ }
51
107
 
52
- // Move to parent directory
53
- const parent = path.dirname(current);
54
- if (parent === current) break; // Reached filesystem root
55
- current = parent;
108
+ const parentDirectory = path.dirname(current);
109
+ if (parentDirectory === current) break;
110
+ current = parentDirectory;
56
111
  }
57
112
 
113
+ if (omittedOversizedFile) warnings.push(FILE_LIMIT_WARNING);
114
+ if (omittedAggregateFile) warnings.push(AGGREGATE_LIMIT_WARNING);
58
115
  return { items, warnings };
59
116
  }
60
117
 
@@ -324,25 +324,21 @@ export function resolveGjcTmuxBinary(options: ResolveGjcTmuxBinaryOptions = {}):
324
324
  return { command: explicit, isPsmux, viaExplicitOverride: true };
325
325
  }
326
326
  if (platform === "win32") {
327
- const candidates: Array<{ command: string; identity: string }> = [];
328
- for (const candidate of PSMUX_BINARY_NAMES) {
329
- const executablePath = resolveBinaryPath(candidate);
327
+ for (const command of ["psmux", "pmux"] as const) {
328
+ const executablePath = resolveBinaryPath(command);
330
329
  if (!executablePath) continue;
331
- const isPsmuxAlias = candidate !== "tmux" || detectPsmuxForCommand(candidate, runner);
332
- if (!isPsmuxAlias) continue;
333
- const identity = activeExecutableIdentityResolver(executablePath);
334
- if (!identity)
335
- throw new Error(`gjc_tmux_provider_ambiguous: Windows ${candidate} executable identity is unavailable`);
336
- candidates.push({ command: candidate, identity });
330
+ if (!activeExecutableIdentityResolver(executablePath))
331
+ throw new Error(`gjc_tmux_provider_ambiguous: Windows ${command} executable identity is unavailable`);
332
+ return { command, isPsmux: true, viaExplicitOverride: false };
337
333
  }
338
- if (candidates.length > 0) {
339
- const identities = new Set(candidates.map(candidate => candidate.identity));
340
- if (identities.size !== 1)
341
- throw new Error("gjc_tmux_provider_ambiguous: Windows psmux aliases resolve to different executables");
342
- // The order is the canonical provider spelling, then its compatibility
343
- // aliases. Identity equality above makes PATH-order alias selection safe.
344
- return { command: candidates[0]!.command, isPsmux: true, viaExplicitOverride: false };
334
+ const tmuxPath = resolveBinaryPath("tmux");
335
+ if (tmuxPath) {
336
+ const isPsmux = outputMentionsPsmux(probeVersionOutput(tmuxPath, runner));
337
+ if (isPsmux && !activeExecutableIdentityResolver(tmuxPath))
338
+ throw new Error("gjc_tmux_provider_ambiguous: Windows tmux executable identity is unavailable");
339
+ return { command: "tmux", isPsmux, viaExplicitOverride: false };
345
340
  }
341
+ return { command: "tmux", isPsmux: false, viaExplicitOverride: false };
346
342
  }
347
343
  const tmuxPath = resolveBinaryPath("tmux");
348
344
  if (tmuxPath) {
@@ -228,7 +228,7 @@ function runTmux(
228
228
  );
229
229
  }
230
230
  if (result.exitCode === 0) return result.stdout?.toString() ?? "";
231
- throw new Error(result.stderr?.toString().trim() || `tmux ${args.join(" ")} failed`);
231
+ throw new Error(result.stderr?.toString().trim() || `${tmuxCommand} ${args.join(" ")} failed`);
232
232
  }
233
233
  function normalizeExactTmuxTarget(sessionTarget: string, env: NodeJS.ProcessEnv, kind: "session" | "option"): string {
234
234
  if (sessionTarget.startsWith("$")) return sessionTarget;
@@ -407,6 +407,13 @@ function psmuxAuthorityEnvironments(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv[]
407
407
  if (authorities.length > 0)
408
408
  return authorities.map(authority => environmentForProviderAuthority(env, authority));
409
409
  }
410
+ if (!ambient.viaExplicitOverride && !ambientAvailable) {
411
+ throw new Error(
412
+ "gjc_tmux_provider_unavailable — GJC searched for psmux, pmux, and tmux on PATH. " +
413
+ "Install psmux from https://github.com/psmux/psmux for native Windows support, use WSL with real tmux, " +
414
+ "or set GJC_TMUX_COMMAND (and GJC_PSMUX_COMMAND when selecting a psmux compatibility alias).",
415
+ );
416
+ }
410
417
  }
411
418
  if (ambient.isPsmux) throw new Error("gjc_tmux_provider_authority_unavailable");
412
419
  return [env];