@gajae-code/coding-agent 0.11.10 → 0.11.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 (111) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/cli.d.ts +8 -1
  3. package/dist/types/config/model-profile-activation.d.ts +1 -0
  4. package/dist/types/config/model-profile-contract.d.ts +2 -0
  5. package/dist/types/config/settings-schema.d.ts +57 -2
  6. package/dist/types/config/settings.d.ts +1 -0
  7. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  8. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  9. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  10. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  11. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  12. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  13. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  14. package/dist/types/modes/components/welcome.d.ts +1 -0
  15. package/dist/types/modes/interactive-mode.d.ts +5 -1
  16. package/dist/types/runtime/memory-domain.d.ts +9 -0
  17. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  18. package/dist/types/runtime/memory-guard.d.ts +39 -0
  19. package/dist/types/runtime/memory-limit.d.ts +11 -0
  20. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  21. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  22. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  23. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  24. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  25. package/dist/types/sdk/bus/index.d.ts +3 -1
  26. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  29. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  30. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  31. package/dist/types/sdk/startup-capability.d.ts +6 -0
  32. package/dist/types/session/agent-session.d.ts +9 -11
  33. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  34. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  35. package/dist/types/session/session-manager.d.ts +34 -0
  36. package/dist/types/task/executor.d.ts +6 -0
  37. package/dist/types/task/types.d.ts +36 -0
  38. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  39. package/dist/types/tools/resource-gc.d.ts +19 -0
  40. package/package.json +8 -8
  41. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  42. package/scripts/run-test-manifest.ts +6 -4
  43. package/src/cli/args.ts +14 -8
  44. package/src/cli/fast-help.ts +1 -1
  45. package/src/cli.ts +48 -2
  46. package/src/commands/sdk.ts +91 -7
  47. package/src/config/model-profile-activation.ts +1 -0
  48. package/src/config/model-profile-contract.ts +13 -0
  49. package/src/config/model-profiles.ts +24 -0
  50. package/src/config/settings-schema.ts +61 -2
  51. package/src/config/settings.ts +8 -0
  52. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  53. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  54. package/src/extensibility/extensions/runner.ts +4 -0
  55. package/src/extensibility/extensions/types.ts +14 -0
  56. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  57. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  58. package/src/gjc-runtime/linux-proc.ts +70 -36
  59. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  60. package/src/gjc-runtime/state-runtime.ts +48 -11
  61. package/src/internal-urls/docs-index.generated.ts +4 -4
  62. package/src/internal-urls/local-protocol.ts +23 -4
  63. package/src/modes/acp/acp-agent.ts +554 -60
  64. package/src/modes/acp/acp-event-mapper.ts +93 -6
  65. package/src/modes/components/model-selector.ts +15 -1
  66. package/src/modes/components/notifications-settings-editor.ts +1 -1
  67. package/src/modes/components/queue-pane.ts +55 -9
  68. package/src/modes/components/queued-message-selector.ts +53 -10
  69. package/src/modes/components/tool-status-header.ts +1 -4
  70. package/src/modes/components/welcome.ts +18 -3
  71. package/src/modes/controllers/command-controller.ts +4 -0
  72. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  73. package/src/modes/controllers/input-controller.ts +44 -3
  74. package/src/modes/interactive-mode.ts +65 -26
  75. package/src/modes/runtime-init.ts +1 -0
  76. package/src/prompts/agents/executor.md +1 -1
  77. package/src/runtime/memory-domain.ts +66 -0
  78. package/src/runtime/memory-guard-contract.ts +73 -0
  79. package/src/runtime/memory-guard.ts +292 -0
  80. package/src/runtime/memory-limit.ts +50 -0
  81. package/src/runtime-mcp/manager.ts +26 -6
  82. package/src/sdk/acp/adapter.ts +8 -1
  83. package/src/sdk/acp/final-text.ts +40 -0
  84. package/src/sdk/acp/mcp.ts +26 -0
  85. package/src/sdk/broker/lifecycle.ts +82 -1
  86. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  87. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  88. package/src/sdk/bus/config.ts +1 -1
  89. package/src/sdk/bus/index.ts +162 -22
  90. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  91. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  92. package/src/sdk/bus/telegram-daemon.ts +531 -53
  93. package/src/sdk/host/control/dispatch.ts +2 -0
  94. package/src/sdk/host/control/operations.ts +1 -0
  95. package/src/sdk/host/host.ts +2 -2
  96. package/src/sdk/lifecycle-session.ts +19 -3
  97. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  98. package/src/sdk/protocol/operation-registry.ts +1 -0
  99. package/src/sdk/session.ts +12 -1
  100. package/src/sdk/startup-capability.ts +6 -0
  101. package/src/session/agent-session.ts +256 -142
  102. package/src/session/fallback-chain-controller.ts +47 -8
  103. package/src/session/internal/managed-session-scope.ts +40 -10
  104. package/src/session/internal/native-publish-outcome.ts +8 -3
  105. package/src/session/session-manager.ts +608 -25
  106. package/src/task/executor.ts +11 -3
  107. package/src/task/index.ts +2 -0
  108. package/src/task/types.ts +22 -11
  109. package/src/task/ultragoal-redteam-activation.ts +75 -0
  110. package/src/tools/resource-gc.ts +496 -120
  111. package/src/workflow/workflow-intent-diff.ts +4 -1
@@ -1,3 +1,4 @@
1
+ import type { WindowsJobMemoryProbeResult } from "@gajae-code/natives";
1
2
  import type { Settings } from "../config/settings";
2
3
  import { type TabGcSnapshot } from "./browser/tab-supervisor";
3
4
  export interface BrowserGcPolicy {
@@ -16,7 +17,10 @@ export declare function resolveSweepIntervalMs(settings: Settings): number;
16
17
  /** Injectable seams so the controller is fully testable without real browsers/filesystem/RSS. */
17
18
  export interface ResourceGcDeps {
18
19
  now: () => number;
20
+ monotonicNow: () => number;
19
21
  rssBytes: () => number;
22
+ memorySnapshot: () => Promise<MemoryPressureSnapshot>;
23
+ runGc: () => void;
20
24
  logWarn: (msg: string, meta?: Record<string, unknown>) => void;
21
25
  listTabs: () => TabGcSnapshot[];
22
26
  releaseTab: (name: string, policy: {
@@ -43,6 +47,21 @@ export interface ResourceGcRegistration {
43
47
  */
44
48
  export declare function registerResourceGcSession(reg: ResourceGcRegistration): () => void;
45
49
  export declare function sweepOnce(d?: ResourceGcDeps): Promise<void>;
50
+ export interface MemoryPressureDomain {
51
+ hardCapBytes: number;
52
+ totalUsageBytes: number;
53
+ source: MemoryPressureSnapshot["source"];
54
+ }
55
+ export interface MemoryPressureSnapshot {
56
+ hardCapBytes: number;
57
+ totalUsageBytes: number;
58
+ parentBytes: number;
59
+ source: "host" | "linux_cgroup_v2" | "linux_cgroup_v1" | "windows_job" | "windows_process_job_limit";
60
+ domains?: MemoryPressureDomain[];
61
+ }
62
+ export declare function __sampleLinuxCgroupHierarchyForTest(mountInfo: string, membership: string, fsType: "cgroup" | "cgroup2", hostBytes: number, parentBytes: number): Promise<MemoryPressureSnapshot | null>;
63
+ export declare function __sampleWindowsJobMemoryForTest(hostBytes: number, parentBytes: number, probeResult?: WindowsJobMemoryProbeResult): MemoryPressureSnapshot | null;
64
+ export declare function __selectMemoryPressureDomainForTest(snapshot: MemoryPressureSnapshot, policyLimitBytes: number | null): MemoryPressureSnapshot;
46
65
  export declare function __setResourceGcDepsForTest(overrides: Partial<ResourceGcDeps>): void;
47
66
  export declare function __setResourceGcSchedulerNowForTest(now: () => number): void;
48
67
  export declare function __runResourceGcTickForTest(): Promise<void>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.11.10",
4
+ "version": "0.11.11",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -53,13 +53,13 @@
53
53
  "@agentclientprotocol/sdk": "1.2.1",
54
54
  "@babel/parser": "^7.29.3",
55
55
  "@mozilla/readability": "^0.6.0",
56
- "@gajae-code/stats": "0.11.10",
57
- "@gajae-code/agent-core": "0.11.10",
58
- "@gajae-code/ai": "0.11.10",
59
- "@gajae-code/bridge-client": "0.11.10",
60
- "@gajae-code/natives": "0.11.10",
61
- "@gajae-code/tui": "0.11.10",
62
- "@gajae-code/utils": "0.11.10",
56
+ "@gajae-code/stats": "0.11.11",
57
+ "@gajae-code/agent-core": "0.11.11",
58
+ "@gajae-code/ai": "0.11.11",
59
+ "@gajae-code/bridge-client": "0.11.11",
60
+ "@gajae-code/natives": "0.11.11",
61
+ "@gajae-code/tui": "0.11.11",
62
+ "@gajae-code/utils": "0.11.11",
63
63
  "@puppeteer/browsers": "^2.13.0",
64
64
  "@types/turndown": "5.0.6",
65
65
  "@xterm/headless": "^6.0.0",
@@ -86,6 +86,7 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
86
86
  "agent_session:refreshMCPTools": "internal accessor/plumbing, not a user-facing control seam",
87
87
  "agent_session:refreshGjcSubskillTools": "internal accessor/plumbing, not a user-facing control seam",
88
88
  "agent_session:buildDisplaySessionContext": "internal accessor/plumbing, not a user-facing control seam",
89
+ "agent_session:buildPreparedDisplaySessionContext": "internal accessor/plumbing, not a user-facing control seam",
89
90
  "agent_session:convertMessagesToLlm": "internal accessor/plumbing, not a user-facing control seam",
90
91
  "agent_session:prepareSimpleStreamOptions": "internal accessor/plumbing, not a user-facing control seam",
91
92
  "agent_session:getPlanModeState": "internal accessor/plumbing, not a user-facing control seam",
@@ -198,6 +199,7 @@ const SEAM_TO_SDK: Readonly<Record<string, string>> = {
198
199
  "agent_session:setSessionName": "session.rename",
199
200
  "agent_session:setModel": "model.set",
200
201
  "agent_session:setDefaultModelSelection": "model.set",
202
+ "agent_session:activateModelProfileForControl": "model.profile.set",
201
203
  "agent_session:cycleModel": "model.cycle",
202
204
  "agent_session:setThinkingLevel": "thinking.set",
203
205
  "agent_session:cycleThinkingLevel": "thinking.cycle",
@@ -36,6 +36,8 @@ try {
36
36
  function validateRowReceipts(rows: ManifestAdapterRow[]): void {
37
37
  const ids = new Set<string>();
38
38
  const counts = new Map<string, number>();
39
+ // Each operation has one receipt, plus the dedicated config.patch secret-input receipt.
40
+ const expectedRowsPerAdapter = OPERATIONS.length + 1;
39
41
  for (const row of rows) {
40
42
  if (ids.has(row.adapterTestId)) throw new Error(`Duplicate adapter test ID: ${row.adapterTestId}`);
41
43
  ids.add(row.adapterTestId);
@@ -60,11 +62,11 @@ function validateRowReceipts(rows: ManifestAdapterRow[]): void {
60
62
  }
61
63
  counts.set(row.adapter, (counts.get(row.adapter) ?? 0) + 1);
62
64
  }
63
- if (rows.length !== ADAPTERS.length * 93)
64
- throw new Error(`Expected ${ADAPTERS.length * 93} adapter rows; received ${rows.length}.`);
65
+ if (rows.length !== ADAPTERS.length * expectedRowsPerAdapter)
66
+ throw new Error(`Expected ${ADAPTERS.length * expectedRowsPerAdapter} adapter rows; received ${rows.length}.`);
65
67
  for (const adapter of ADAPTERS) {
66
- if (counts.get(adapter) !== 93)
67
- throw new Error(`Expected 93 ${adapter} rows; received ${counts.get(adapter) ?? 0}.`);
68
+ if (counts.get(adapter) !== expectedRowsPerAdapter)
69
+ throw new Error(`Expected ${expectedRowsPerAdapter} ${adapter} rows; received ${counts.get(adapter) ?? 0}.`);
68
70
  }
69
71
  process.stdout.write(
70
72
  `manifest check: ${rows.length} row receipts (${[...counts].map(([adapter, count]) => `${adapter}=${count}`).join(", ")})\n`,
package/src/cli/args.ts CHANGED
@@ -205,17 +205,23 @@ export function parseArgs(args: string[]): Args {
205
205
  }
206
206
  }
207
207
  result.tools = validTools;
208
- } else if (arg === "--thinking" && i + 1 < args.length) {
208
+ } else if (arg === "--thinking") {
209
+ // Match --credential / --mcp-config: a missing value or a following flag is
210
+ // a usage error, not a silent no-op / accidental consumption of `-p`.
211
+ const next = args[i + 1];
212
+ if (!next || next.startsWith("-")) {
213
+ throw new CliParseError(`--thinking requires <level> (${THINKING_EFFORTS.join(", ")})`);
214
+ }
209
215
  const rawThinking = args[++i];
210
216
  const thinking = parseEffort(rawThinking);
211
- if (thinking !== undefined) {
212
- result.thinking = thinking;
213
- } else {
214
- logger.warn("Invalid thinking level passed to --thinking", {
215
- level: rawThinking,
216
- validThinkingLevels: THINKING_EFFORTS,
217
- });
217
+ if (thinking === undefined) {
218
+ // Fail closed: a silent ignore left users believing `ultra` (or any typo)
219
+ // was applied. Help / Flags.options advertise the real Effort enum.
220
+ throw new CliParseError(
221
+ `Invalid --thinking level "${rawThinking}". Expected one of: ${THINKING_EFFORTS.join(", ")}`,
222
+ );
218
223
  }
224
+ result.thinking = thinking;
219
225
  } else if (arg === "--print" || arg === "-p") {
220
226
  result.print = true;
221
227
  } else if (arg === "--export" && i + 1 < args.length) {
@@ -84,7 +84,7 @@ Environment Variables:
84
84
  GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
85
85
  GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
86
86
  GJC_TMUX_PROFILE - Apply GJC tmux scroll/mouse/clipboard profile to --tmux sessions (set 0/off to skip)
87
- GJC_MOUSE - Mouse-wheel scroll in --tmux sessions (set 0/off to let the host terminal scroll)
87
+ GJC_MOUSE - Apply tmux copy-mode mouse capture in --tmux sessions (set 0/off to skip)
88
88
 
89
89
  For complete environment variable reference, see:
90
90
  docs/environment-variables.md
package/src/cli.ts CHANGED
@@ -5,8 +5,10 @@
5
5
  * lightweight CLI runner from pi-utils.
6
6
  */
7
7
  import "@gajae-code/utils/postmortem";
8
+ import { THINKING_EFFORTS } from "@gajae-code/ai";
8
9
  import { Args, type CliConfig, Command, type CommandEntry, Flags, run } from "@gajae-code/utils/cli";
9
10
  import { APP_NAME, formatBunRuntimeError, MIN_BUN_VERSION, VERSION } from "@gajae-code/utils/dirs";
11
+ import { loadNative as loadNativeBindings } from "../../natives/native/loader-state.js";
10
12
  import { runFixtureReport } from "./cli/fixture-report";
11
13
  import { admitManagedOwnerBeforeCli, completeManagedOwnerRecovery } from "./gjc-runtime/managed-owner-admission";
12
14
  import {
@@ -174,6 +176,46 @@ async function runChatDaemonInternalFastPath(argv: string[]): Promise<void> {
174
176
  await runChatDaemonInternal(action === "discord-internal" ? "discord" : "slack", argv.slice(2));
175
177
  }
176
178
 
179
+ type MemoryGuardNativeSmokeLoad = () => Record<string, unknown>;
180
+ type WindowsJobMemoryProbeResult = Record<string, unknown> & { kind: string };
181
+ type MemoryGuardNativeSmokeReceipt = {
182
+ api: "memory_guard_windows_job_probe_v1";
183
+ source: "pi_natives";
184
+ result: WindowsJobMemoryProbeResult;
185
+ };
186
+
187
+ export function isMemoryGuardNativeSmokeFastPath(argv: readonly string[]): boolean {
188
+ return (
189
+ argv.length === 3 && argv[0] === "internal" && argv[1] === "memory-guard-native-smoke" && argv[2] === "--json"
190
+ );
191
+ }
192
+
193
+ function parseWindowsJobMemoryProbeResult(value: unknown): WindowsJobMemoryProbeResult {
194
+ if (!value || typeof value !== "object") {
195
+ throw new Error("memory-guard-native-smoke: native probe returned a non-object result");
196
+ }
197
+ const result = value as Record<string, unknown>;
198
+ if (typeof result.kind !== "string") {
199
+ throw new Error("memory-guard-native-smoke: native probe result is missing a string kind tag");
200
+ }
201
+ return result as WindowsJobMemoryProbeResult;
202
+ }
203
+
204
+ export function runMemoryGuardNativeSmokeFastPath(
205
+ options: { loadNative?: MemoryGuardNativeSmokeLoad; writeStdout?: (text: string) => void } = {},
206
+ ): void {
207
+ const probe = (options.loadNative ?? loadNativeBindings)().probeWindowsJobMemory;
208
+ if (typeof probe !== "function") {
209
+ throw new Error("memory-guard-native-smoke: probeWindowsJobMemory export missing from native addon");
210
+ }
211
+ const receipt: MemoryGuardNativeSmokeReceipt = {
212
+ api: "memory_guard_windows_job_probe_v1",
213
+ source: "pi_natives",
214
+ result: parseWindowsJobMemoryProbeResult((probe as () => unknown)()),
215
+ };
216
+ (options.writeStdout ?? (text => process.stdout.write(text)))(`${JSON.stringify(receipt)}\n`);
217
+ }
218
+
177
219
  function rootFixtureArg(argv: string[]): { present: boolean; id: string | undefined } {
178
220
  for (let i = 0; i < argv.length; i++) {
179
221
  const arg = argv[i];
@@ -244,8 +286,8 @@ export class RootHelpCommand extends Command {
244
286
  tmux: Flags.boolean({ description: "Launch interactive startup inside tmux" }),
245
287
  tools: Flags.string({ description: "Comma-separated list of tools to enable (default: all)" }),
246
288
  thinking: Flags.string({
247
- description: "Set thinking level: ultra, high, medium, low",
248
- options: ["ultra", "high", "medium", "low"],
289
+ description: `Set thinking level: ${THINKING_EFFORTS.join(", ")}`,
290
+ options: [...THINKING_EFFORTS],
249
291
  }),
250
292
  hook: Flags.string({ description: "Load a hook/extension file (can be used multiple times)", multiple: true }),
251
293
  extension: Flags.string({
@@ -382,6 +424,10 @@ export async function runCli(argv: string[]): Promise<void> {
382
424
  }
383
425
  // Re-exec could not be spawned; fall through and run in this process.
384
426
  }
427
+ if (isMemoryGuardNativeSmokeFastPath(argv)) {
428
+ runMemoryGuardNativeSmokeFastPath();
429
+ return;
430
+ }
385
431
  if (isTmuxOwnerIsolationCliArgv(argv)) {
386
432
  await runTmuxOwnerIsolationCliFromStdin();
387
433
  return;
@@ -1,11 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
3
4
  import * as path from "node:path";
4
5
  import { Args, CliParseError, Command, Flags, renderCommandHelp } from "@gajae-code/utils/cli";
5
6
  import type { Args as ParsedArgs } from "../cli/args";
6
7
  import { Settings } from "../config/settings";
7
8
  import { applyStartupModelProfiles, createSessionManager } from "../main";
8
9
  import { initializeExtensions } from "../modes/runtime-init";
10
+ import { ACP_MCP_REQUEST_TIMEOUT_MS, ACP_MCP_STARTUP_HEADROOM_MS } from "../sdk/acp/mcp";
9
11
  import { Broker } from "../sdk/broker/broker";
10
12
  import { completeBrokerProcess } from "../sdk/broker/internal";
11
13
  import {
@@ -285,17 +287,99 @@ export async function runSessionHost(
285
287
  throw new Error("SDK startup did not complete before readiness cutoff.");
286
288
  }
287
289
 
288
- let opened: { parsed: ParsedArgs; sessionManager: SessionManager | undefined };
289
- let created: CreateLifecycleAgentSessionResult;
290
- try {
291
- opened = await openLifecycleSessionManager(request, cwd, agentDir);
292
- created = await createLifecycleAgentSession({ cwd, agentDir, sessionManager: opened.sessionManager });
293
- } catch (error) {
290
+ // Inlined rather than extracted to a helper: TypeScript's definite-assignment
291
+ // analysis does not see a `Promise<never>` helper as terminating, so hoisting
292
+ // this would make `opened`/`created` "used before assigned" below.
293
+ const registrationFailure = async (error: unknown): Promise<SdkStartupFailure> => {
294
294
  const rollback = new SdkStartupRollbackTracker();
295
295
  rollback.recordAbsent();
296
296
  const failure = normalizeSdkStartupFailure("registration", "failed", error);
297
297
  await writeFailure(failure, rollback.result);
298
- throw failure;
298
+ return failure;
299
+ };
300
+
301
+ let opened: { parsed: ParsedArgs; sessionManager: SessionManager | undefined };
302
+ let created: CreateLifecycleAgentSessionResult;
303
+ let mcpConfigDirectory: string | undefined;
304
+ try {
305
+ let mcpConfigPath: string | undefined;
306
+ try {
307
+ opened = await openLifecycleSessionManager(request, cwd, agentDir);
308
+ if (request.mcpServers && request.mcpServers.length > 0) {
309
+ mcpConfigDirectory = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "gjc-acp-mcp-")));
310
+ mcpConfigPath = path.join(mcpConfigDirectory, "mcp.json");
311
+ await Bun.write(
312
+ mcpConfigPath,
313
+ JSON.stringify({
314
+ mcpServers: Object.fromEntries(
315
+ request.mcpServers.map(server => [
316
+ server.name,
317
+ "url" in server
318
+ ? {
319
+ type: server.type,
320
+ url: server.url,
321
+ ...(server.headers ? { headers: server.headers } : {}),
322
+ timeout: ACP_MCP_REQUEST_TIMEOUT_MS,
323
+ }
324
+ : {
325
+ type: "stdio",
326
+ command: server.command,
327
+ args: server.args,
328
+ ...(server.env ? { env: server.env } : {}),
329
+ noInheritEnv: true,
330
+ timeout: ACP_MCP_REQUEST_TIMEOUT_MS,
331
+ },
332
+ ]),
333
+ ),
334
+ }),
335
+ );
336
+ }
337
+ } catch (error) {
338
+ throw await registrationFailure(error);
339
+ }
340
+
341
+ // The longer MCP startup ceiling is scoped to ACP lifecycle launches only:
342
+ // it applies when this request actually carried `mcpServers`. Ordinary
343
+ // CLI/SDK `mcpConfigPath` consumers keep the manager's short default.
344
+ //
345
+ // This recheck deliberately sits OUTSIDE the registration catch above.
346
+ // Inside it, the throw would be caught, reclassified as
347
+ // `registration`/`failed`, and written a second time, losing the
348
+ // `startup`/`pending` outcome the readiness cutoff is supposed to report.
349
+ // Session-manager open and MCP config write already consumed part of the
350
+ // budget, so re-read the clock here rather than reusing the earlier check.
351
+ let mcpStartupTimeoutMs: number | undefined;
352
+ if (mcpConfigPath !== undefined) {
353
+ const remaining = request.semanticReadyDeadlineAt - now() - ACP_MCP_STARTUP_HEADROOM_MS;
354
+ if (remaining <= 0) {
355
+ const absent = new SdkStartupRollbackTracker();
356
+ absent.recordAbsent();
357
+ await writeFailure(
358
+ {
359
+ phase: "startup",
360
+ reason: "pending",
361
+ message: "SDK startup did not complete before readiness cutoff.",
362
+ },
363
+ absent.result,
364
+ );
365
+ throw new Error("SDK startup did not complete before readiness cutoff.");
366
+ }
367
+ mcpStartupTimeoutMs = remaining;
368
+ }
369
+
370
+ try {
371
+ created = await createLifecycleAgentSession({
372
+ cwd,
373
+ agentDir,
374
+ sessionManager: opened.sessionManager,
375
+ ...(mcpConfigPath ? { mcpConfigPath } : {}),
376
+ ...(mcpStartupTimeoutMs !== undefined ? { mcpStartupTimeoutMs } : {}),
377
+ });
378
+ } catch (error) {
379
+ throw await registrationFailure(error);
380
+ }
381
+ } finally {
382
+ if (mcpConfigDirectory) await fs.rm(mcpConfigDirectory, { recursive: true, force: true });
299
383
  }
300
384
  const { parsed } = opened;
301
385
  if ("failure" in created) {
@@ -222,6 +222,7 @@ export function materializeActiveModelProfileAssignments(options: MaterializeMod
222
222
  }
223
223
 
224
224
  export class ModelProfileCredentialError extends Error {
225
+ readonly code = "authentication_failed";
225
226
  readonly profileLabel: string;
226
227
  readonly providers: readonly string[];
227
228
 
@@ -10,6 +10,7 @@ export interface ModelProfileCatalogItem {
10
10
  id: string;
11
11
  displayName: string;
12
12
  source: "builtin" | "configured";
13
+ available?: boolean;
13
14
  }
14
15
 
15
16
  export interface UnknownModelProfileDetails {
@@ -124,6 +125,18 @@ export function projectModelProfileCatalog(
124
125
  .sort((left, right) => left.id.localeCompare(right.id));
125
126
  }
126
127
 
128
+ export function isModelProfileProviderAvailable(
129
+ profile: ModelProfileDefinition,
130
+ authenticatedProviders: ReadonlySet<string>,
131
+ ): boolean {
132
+ const alternativeGroups = profile.alternativeProviderGroups ?? [];
133
+ const alternativeProviders = new Set(alternativeGroups.flat());
134
+ for (const provider of profile.requiredProviders) {
135
+ if (!alternativeProviders.has(provider) && !authenticatedProviders.has(provider)) return false;
136
+ }
137
+ return alternativeGroups.every(group => group.some(provider => authenticatedProviders.has(provider)));
138
+ }
139
+
127
140
  export function isModelProfileError(value: unknown): value is {
128
141
  code: ModelProfileErrorCode;
129
142
  message: string;
@@ -207,6 +207,27 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
207
207
  critic: "xai/grok-4.3:xhigh",
208
208
  architect: "xai/grok-4.3:xhigh",
209
209
  }),
210
+ profile("grok-45-eco", ["xai"], {
211
+ default: "xai/grok-4.5:low",
212
+ executor: "xai/grok-4.5:minimal",
213
+ planner: "xai/grok-4.5:low",
214
+ critic: "xai/grok-4.5:medium",
215
+ architect: "xai/grok-4.5:high",
216
+ }),
217
+ profile("grok-45-medium", ["xai"], {
218
+ default: "xai/grok-4.5:medium",
219
+ executor: "xai/grok-4.5:low",
220
+ planner: "xai/grok-4.5:medium",
221
+ critic: "xai/grok-4.5:high",
222
+ architect: "xai/grok-4.5:high",
223
+ }),
224
+ profile("grok-45-pro", ["xai"], {
225
+ default: "xai/grok-4.5:high",
226
+ executor: "xai/grok-4.5:medium",
227
+ planner: "xai/grok-4.5:high",
228
+ critic: "xai/grok-4.5:high",
229
+ architect: "xai/grok-4.5:high",
230
+ }),
210
231
  profile("grok-build-pro", ["grok-build"], {
211
232
  default: "grok-build/grok-composer-2.5-fast",
212
233
  executor: "grok-build/grok-build",
@@ -321,6 +342,9 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
321
342
  "grok-eco": { displayName: "Grok Eco", providerGroup: "GROK" },
322
343
  "grok-medium": { displayName: "Grok Medium", providerGroup: "GROK" },
323
344
  "grok-pro": { displayName: "Grok Pro", providerGroup: "GROK" },
345
+ "grok-45-eco": { displayName: "Grok 4.5 Eco", providerGroup: "GROK" },
346
+ "grok-45-medium": { displayName: "Grok 4.5 Medium", providerGroup: "GROK" },
347
+ "grok-45-pro": { displayName: "Grok 4.5 Pro", providerGroup: "GROK" },
324
348
  "grok-build-pro": { displayName: "Grok Build Pro", providerGroup: "GROK" },
325
349
  "cursor-eco": { displayName: "Cursor Eco", providerGroup: "CURSOR" },
326
350
  "cursor-medium": { displayName: "Cursor Medium", providerGroup: "CURSOR" },
@@ -309,7 +309,7 @@ export const SETTINGS_SCHEMA = {
309
309
  },
310
310
  "notifications.telegram.toolActivity.enabled": {
311
311
  type: "boolean",
312
- default: true,
312
+ default: false,
313
313
  ui: {
314
314
  tab: "notifications",
315
315
  label: "Telegram Tool Activity",
@@ -501,6 +501,11 @@ export const SETTINGS_SCHEMA = {
501
501
  default: 0.05,
502
502
  validate: (value: number) => Number.isFinite(value) && value > 0 && value <= 1,
503
503
  },
504
+ "gjc.ralplan.maxIterations": {
505
+ type: "number",
506
+ default: 5,
507
+ validate: (value: number) => Number.isInteger(value) && value >= 1 && value <= 20,
508
+ },
504
509
 
505
510
  // ────────────────────────────────────────────────────────────────────────
506
511
  // Appearance
@@ -1244,7 +1249,8 @@ export const SETTINGS_SCHEMA = {
1244
1249
  ui: {
1245
1250
  tab: "interaction",
1246
1251
  label: "Mouse Support",
1247
- description: "Enable SGR mouse wheel scrolling and overlay row selection. Disabled in tmux and screen.",
1252
+ description:
1253
+ "Enable GJC session scrolling, drag-to-copy text selection, and overlay row selection with the mouse. Disabled by default to preserve native terminal or tmux scrollback and selection.",
1248
1254
  },
1249
1255
  },
1250
1256
  // Conversation flow
@@ -2653,6 +2659,47 @@ export const SETTINGS_SCHEMA = {
2653
2659
  description: "How often the resource GC sweeps browser tabs and stale screenshot directories.",
2654
2660
  },
2655
2661
  },
2662
+ "memoryGuard.enabled": {
2663
+ type: "boolean",
2664
+ default: false,
2665
+ },
2666
+ "memoryGuard.checkIntervalMs": {
2667
+ type: "number",
2668
+ default: 30_000,
2669
+ validate: (value: number) => Number.isFinite(value) && value > 0,
2670
+ },
2671
+ "memoryGuard.gcThresholdPercent": {
2672
+ type: "number",
2673
+ default: 70,
2674
+ validate: (value: number) => Number.isFinite(value) && value >= 0 && value <= 100,
2675
+ },
2676
+ "memoryGuard.restartThresholdPercent": {
2677
+ type: "number",
2678
+ default: 85,
2679
+ validate: (value: number) => Number.isFinite(value) && value >= 0 && value <= 100,
2680
+ },
2681
+ "memoryGuard.restartThresholdWindowMs": {
2682
+ type: "number",
2683
+ default: 90_000,
2684
+ validate: (value: number) => Number.isFinite(value) && value > 0,
2685
+ },
2686
+ "memoryGuard.cooldownMs": {
2687
+ type: "number",
2688
+ default: 600_000,
2689
+ validate: (value: number) => Number.isFinite(value) && value >= 0,
2690
+ },
2691
+ "memoryGuard.parentReserveMb": {
2692
+ type: "number",
2693
+ default: 1024,
2694
+ validate: (value: number) =>
2695
+ Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER / (1024 * 1024),
2696
+ },
2697
+ "memoryGuard.policyLimitMb": {
2698
+ type: "number",
2699
+ default: 0,
2700
+ validate: (value: number) =>
2701
+ Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER / (1024 * 1024),
2702
+ },
2656
2703
 
2657
2704
  "computer.enabled": {
2658
2705
  type: "boolean",
@@ -3844,6 +3891,17 @@ export interface ShellMinimizerSettings {
3844
3891
  maxCaptureBytes: number;
3845
3892
  }
3846
3893
 
3894
+ export interface MemoryGuardSettings {
3895
+ enabled: boolean;
3896
+ checkIntervalMs: number;
3897
+ gcThresholdPercent: number;
3898
+ restartThresholdPercent: number;
3899
+ restartThresholdWindowMs: number;
3900
+ cooldownMs: number;
3901
+ parentReserveMb: number;
3902
+ policyLimitMb: number;
3903
+ }
3904
+
3847
3905
  export interface NotificationsSettings {
3848
3906
  enabled: boolean;
3849
3907
  telegram: {
@@ -3902,6 +3960,7 @@ export interface GroupTypeMap {
3902
3960
  statusLine: StatusLineSettings;
3903
3961
  thinkingBudgets: ThinkingBudgetsSettings;
3904
3962
  stt: SttSettings;
3963
+ memoryGuard: MemoryGuardSettings;
3905
3964
  modelRoles: Record<string, ModelSelectorValue>;
3906
3965
  modelTags: ModelTagsSettings;
3907
3966
  cycleOrder: string[];
@@ -363,6 +363,7 @@ export class Settings implements NotificationSettingsReader {
363
363
  /** Pending debounced ordinary save; its queue slot is reserved immediately. */
364
364
  #saveTimer?: NodeJS.Timeout;
365
365
  #savePromise?: Promise<void>;
366
+ #changeListeners = new Set<(path: SettingPath) => void>();
366
367
  #pendingSaveSlot?: PendingSaveSlot;
367
368
 
368
369
  /** Legacy fallback migration warnings emitted once per settings instance. */
@@ -522,6 +523,11 @@ export class Settings implements NotificationSettingsReader {
522
523
  return structuredClone(this.#schemaReport);
523
524
  }
524
525
 
526
+ onChanged(listener: (path: SettingPath) => void): () => void {
527
+ this.#changeListeners.add(listener);
528
+ return () => this.#changeListeners.delete(listener);
529
+ }
530
+
525
531
  /**
526
532
  * Set a setting value (sync).
527
533
  * Updates global settings and reserves its background persistence slot before
@@ -555,6 +561,7 @@ export class Settings implements NotificationSettingsReader {
555
561
 
556
562
  const hook = SETTING_HOOKS[path];
557
563
  if (hook) hook(value, prev);
564
+ for (const listener of this.#changeListeners) listener(path);
558
565
  }
559
566
 
560
567
  /**
@@ -579,6 +586,7 @@ export class Settings implements NotificationSettingsReader {
579
586
 
580
587
  const hook = SETTING_HOOKS[path];
581
588
  if (hook) hook(this.get(path), prev);
589
+ for (const listener of this.#changeListeners) listener(path);
582
590
  }
583
591
 
584
592
  /**
@@ -67,14 +67,15 @@ The consensus workflow:
67
67
  - **Plan-only Critic lane**: independently check quality, principle-option consistency, alternatives, risks, acceptance criteria, and verification; when the plan is thin, request concrete expansion rather than only defects. Persist with `gjc ralplan --write --stage critic --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json`, then return receipt/path plus `OKAY`/`ITERATE`/`REJECT`.
68
68
  - **Sequential fallback**: if Critic must evaluate Architect findings, verdict, antithesis, tradeoffs, synthesis, status, or any Architect-produced artifact, await the Architect result before issuing that Architect-dependent Critic pass.
69
69
  4. **Review join gate**: before consensus, revision, reconciliation, finalization, or approval, verify both Architect and Critic receipts/verdicts exist for the same Planner artifact/pass (`path`, `sha256`, `stage_n`). A non-`CLEAR` Architect verdict, non-`APPROVE` Architect decision, or any non-`OKAY` Critic verdict routes back to Planner revision; do not finalize from only one review lane.
70
- 5. **Re-review loop** (max 5 iterations): Any non-`OKAY` Critic verdict (`ITERATE` or `REJECT`) or Architect result that is not `CLEAR`/`APPROVE` MUST run the same full closed loop:
70
+ 5. **Re-review loop** (max 5 iterations; **runtime-enforced**): Any non-`OKAY` Critic verdict (`ITERATE` or `REJECT`) or Architect result that is not `CLEAR`/`APPROVE` MUST run the same full closed loop:
71
71
  a. Collect Architect + Critic feedback
72
72
  b. Revise the plan by resuming the SAME persisted Planner subagent with consolidated Architect + Critic feedback (see **Persisted Planner** below); fall back to a fresh Planner spawn only per the fallback routing table
73
73
  c. Return to the review fan-out or sequential fallback path above
74
74
  - Persist each Planner revision with `gjc ralplan --write --stage revision --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json` before re-review, then pass the receipt/path forward instead of duplicating the full revision markdown in the parent conversation.
75
75
  d. Re-join Architect and Critic verdicts for the same revised Planner artifact/pass
76
76
  e. Repeat this loop until Critic returns `OKAY` **and** Architect is `CLEAR`/`APPROVE` for the same Planner artifact/pass, or 5 iterations are reached
77
- f. If 5 iterations are reached without Critic `OKAY` plus Architect `CLEAR`/`APPROVE`, present the best version to the user
77
+ f. If 5 iterations are reached without Critic `OKAY` plus Architect `CLEAR`/`APPROVE`, **stop opening further planner/revision passes**. Present the best version to the user (interactive) or surface `PLANNING-STUCK` (headless). Do **not** auto-start implementation.
78
+ g. **Runtime budget (#3165):** native `gjc ralplan --write` refuses a new `planner`/`revision` that would open consensus iteration **> max** (default **5**, overridable via `gjc.ralplan.maxIterations` in project/user `.gjc/settings.json`, integer 1..20). Cap uses the same iteration definition as the HUD (`planner`/`revision` openers in `index.jsonl`). Overflow exits **3**, prints operator-visible **`PLANNING-STUCK`** on stdout (and stderr detail; JSON includes `planning_stuck: true`), and still allows `architect`/`critic` within an already-opened pass plus `post-interview`/`adr`/`final` so the best plan can be escalated to `pending approval` without auto-execution. A new `--run-id` starts a fresh budget.
78
79
  6. **Post-ralplan interview** (intent reconciliation gate): After the review join gate has both Critic `OKAY` and Architect `CLEAR`/`APPROVE` for the same Planner artifact/pass, and before the plan is finalized, reconcile the consensus plan against the user's actual intent. The goal is to make sure ralplan did not silently bake in assumptions that conflict with what the user wants.
79
80
  a. **Collect open items** from the run: every assumption the Planner/Architect/Critic resolved by assumption rather than by stated fact, every ambiguity flagged during review, and every decision the loop made without explicit user input. Source these from the persisted `planner`/`architect`/`critic`/`revision` stage artifacts, not from memory.
80
81
  b. **Cross-check prior context for conflicts**: glob `.gjc/_session-{sessionid}/specs/deep-interview-*.md` and other prior specs/plans/context relevant by topic. For each, list points where the consensus plan contradicts, weakens, or expands beyond a previously crystallized decision, constraint, or non-goal. Cite the conflicting artifact and line/section.
@@ -103,6 +104,24 @@ The consensus workflow:
103
104
 
104
105
  > **Important:** Architect and Critic MAY run in the same parallel batch only for the plan-only Critic lane after Planner persistence. Any Architect-dependent Critic pass MUST remain sequential: await Architect before issuing Critic, then apply the same review join gate before consensus.
105
106
 
107
+ ## Consensus iteration cap (operator contract)
108
+
109
+ - Default max consensus iterations: **5** (`gjc.ralplan.maxIterations`).
110
+ - On cap: exit code **3**, marker **`PLANNING-STUCK`** (stdout), no silent re-loop, no automatic ultragoal/team handoff. Opener budget is `max(index.jsonl openers, on-disk stage-*-{planner,revision}.md count)` so a missing/empty/malformed ledger cannot fail open after prior openers.
111
+ - Headless/CI: treat `PLANNING-STUCK` / exit 3 as terminal planning failure for orchestration/watchdogs.
112
+ - Interactive: present best existing plan via the final approval gate; residual critic findings stay as caveats.
113
+ - Override example (project `.gjc/settings.json`):
114
+
115
+ ```json
116
+ {
117
+ "gjc": {
118
+ "ralplan": {
119
+ "maxIterations": 3
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
106
125
 
107
126
  Follow this ralplan-internal consensus workflow for consensus mode details.
108
127
 
@@ -279,7 +279,7 @@ An ultragoal story cannot be checkpointed `complete` until the active agent has
279
279
  - architecture-side: system boundaries, layering, data/control flow, operational risks.
280
280
  - product-side: user-visible behavior, acceptance criteria, edge cases, regressions.
281
281
  - code-side: maintainability, tests, integration points, and unsafe shortcuts.
282
- 5. Delegate an `executor` QA/red-team lane to build and run the e2e/read-teaming QA suite appropriate for the story. This lane must try to break the change, not just confirm the happy path. It must start from the approved plan/spec/acceptance criteria, then user-facing contracts, and only then implementation code as supporting evidence. Plan/code mismatches are blockers, not items to paper over with implementation intent.
282
+ 5. Delegate an `executor` QA/red-team lane with typed `executionMode: "ultragoal-red-team"` (preferred) — or assignment text that explicitly labels Ultragoal completion QA/red-team — to build and run the e2e/red-teaming QA suite appropriate for the story. A bare `executorQa` field-name mention is not enough to activate the mode. This lane must try to break the change, not just confirm the happy path. It must start from the approved plan/spec/acceptance criteria, then user-facing contracts, and only then implementation code as supporting evidence. Plan/code mismatches are blockers, not items to paper over with implementation intent.
283
283
  6. The executor QA/red-team lane must prove evidence by the real surface under test:
284
284
  - GUI/web surfaces require a valid automation transcript plus a non-uniform screenshot. Bare `inlineEvidence` text or typed receipts never prove live GUI/web execution.
285
285
  - CLI surfaces require runtime argv replay: `schemaVersion: 1`, `kind: "cli-replay"`, `replaySafe: true`, an allowlisted argv `command`, and replayed output validation. The complete field-by-field replay schema, command allowlist, and `replayExempt` audit contract are specified once in the "For CLI replay artifacts" paragraph below the quality-gate JSON; follow it exactly.