@gajae-code/coding-agent 0.11.10 → 0.12.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 (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -68,17 +68,24 @@ type ConnectionTask = {
68
68
 
69
69
  const STARTUP_TIMEOUT_MS = 250;
70
70
  const STARTUP_TIMEOUT_GRACE_MS = 500;
71
+ /**
72
+ * Default ceiling on how long `discoverAndConnect` waits for a server batch to
73
+ * come up. Deliberately short: a config with a large `timeout` must not be able
74
+ * to hang ordinary startup. ACP lifecycle launches carry their own, larger
75
+ * budget derived from the readiness deadline (see `maxStartupTimeoutMs`).
76
+ */
71
77
  const MAX_STARTUP_TIMEOUT_MS = 1_750;
72
78
 
73
- function resolveStartupTimeoutMs(configs: MCPServerConfig[]): number {
79
+ export function resolveStartupTimeoutMs(configs: MCPServerConfig[], maxStartupTimeoutMs?: number): number {
80
+ const ceiling =
81
+ typeof maxStartupTimeoutMs === "number" && Number.isFinite(maxStartupTimeoutMs) && maxStartupTimeoutMs > 0
82
+ ? Math.max(STARTUP_TIMEOUT_MS, maxStartupTimeoutMs)
83
+ : MAX_STARTUP_TIMEOUT_MS;
74
84
  const configuredTimeouts = configs
75
85
  .map(config => config.timeout)
76
86
  .filter((timeout): timeout is number => typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0);
77
87
  if (configuredTimeouts.length === 0) return STARTUP_TIMEOUT_MS;
78
- return Math.min(
79
- MAX_STARTUP_TIMEOUT_MS,
80
- Math.max(STARTUP_TIMEOUT_MS, Math.max(...configuredTimeouts) + STARTUP_TIMEOUT_GRACE_MS),
81
- );
88
+ return Math.min(ceiling, Math.max(STARTUP_TIMEOUT_MS, Math.max(...configuredTimeouts) + STARTUP_TIMEOUT_GRACE_MS));
82
89
  }
83
90
 
84
91
  function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
@@ -185,6 +192,13 @@ export interface MCPDiscoverOptions {
185
192
  export interface MCPManagerOptions {
186
193
  /** Restrict this instance to tools from an explicit MCP config. */
187
194
  toolsOnly?: boolean;
195
+ /**
196
+ * Ceiling for the startup wait, in milliseconds. Only ACP lifecycle launches
197
+ * set this, so a slow ACP MCP handshake gets the readiness budget while every
198
+ * other consumer keeps the short default. Non-positive or non-finite values
199
+ * are ignored and the default applies.
200
+ */
201
+ maxStartupTimeoutMs?: number;
188
202
  }
189
203
 
190
204
  /**
@@ -269,12 +283,15 @@ export class MCPManager {
269
283
  );
270
284
  }
271
285
 
286
+ readonly #maxStartupTimeoutMs: number | undefined;
287
+
272
288
  constructor(
273
289
  private cwd: string,
274
290
  private toolCache: MCPToolCache | null = null,
275
291
  options: MCPManagerOptions = {},
276
292
  ) {
277
293
  this.#toolsOnly = options.toolsOnly === true;
294
+ this.#maxStartupTimeoutMs = options.maxStartupTimeoutMs;
278
295
  }
279
296
 
280
297
  isToolsOnly(): boolean {
@@ -633,7 +650,10 @@ export class MCPManager {
633
650
  }
634
651
 
635
652
  if (connectionTasks.length > 0) {
636
- const startupTimeoutMs = resolveStartupTimeoutMs(connectionTasks.map(task => task.config));
653
+ const startupTimeoutMs = resolveStartupTimeoutMs(
654
+ connectionTasks.map(task => task.config),
655
+ this.#maxStartupTimeoutMs,
656
+ );
637
657
  const firstUnexpectedFailure = Promise.withResolvers<{ reason: unknown }>();
638
658
  if (this.#toolsOnly) {
639
659
  for (const task of connectionTasks) {
@@ -2,9 +2,18 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { isEnoent, logger } from "@gajae-code/utils";
4
4
  import { getAgentDir } from "@gajae-code/utils/dirs";
5
+ import { $credentialEnv } from "@gajae-code/utils/env";
5
6
 
6
7
  const SMITHERY_AUTH_FILENAME = "smithery.json";
7
- const SMITHERY_URL = process.env.SMITHERY_URL || "https://smithery.ai";
8
+ /**
9
+ * Smithery web origin, from trusted environment sources only.
10
+ *
11
+ * This origin serves the CLI auth session and the verification URL the user is
12
+ * sent to, so whatever can set it can drive the login flow. `$env` merges the
13
+ * caller's `cwd/.env` into `process.env`, so reading it there would let
14
+ * repository content redirect that flow.
15
+ */
16
+ const SMITHERY_URL = $credentialEnv("SMITHERY_URL") || "https://smithery.ai";
8
17
 
9
18
  type SmitheryCliAuthSession = {
10
19
  sessionId: string;
@@ -31,6 +40,11 @@ function normalizeApiKey(value: string | undefined): string | undefined {
31
40
  return trimmed.length > 0 ? trimmed : undefined;
32
41
  }
33
42
 
43
+ /** Test seam: the Smithery origin and env API key as resolved from trusted env. */
44
+ export function resolveSmitheryEnvForTest(): { url: string; apiKey: string | undefined } {
45
+ return { url: SMITHERY_URL, apiKey: normalizeApiKey($credentialEnv("SMITHERY_API_KEY")) };
46
+ }
47
+
34
48
  export function getSmitheryLoginUrl(): string {
35
49
  return SMITHERY_URL;
36
50
  }
@@ -62,7 +76,8 @@ export async function pollSmitheryCliAuthSession(
62
76
  }
63
77
 
64
78
  export async function getSmitheryApiKey(): Promise<string | undefined> {
65
- const envKey = normalizeApiKey(process.env.SMITHERY_API_KEY);
79
+ // Trusted sources only: this key authenticates every Smithery API call.
80
+ const envKey = normalizeApiKey($credentialEnv("SMITHERY_API_KEY"));
66
81
  if (envKey) return envKey;
67
82
 
68
83
  const authPath = getSmitheryAuthPath();
@@ -1,4 +1,14 @@
1
- const SMITHERY_API_BASE_URL = (process.env.SMITHERY_API_URL || "https://api.smithery.ai").replace(/\/+$/, "");
1
+ import { $credentialEnv } from "@gajae-code/utils/env";
2
+
3
+ /**
4
+ * Smithery API base, from trusted environment sources only.
5
+ *
6
+ * Every request to this base carries `Authorization: Bearer <apiKey>` and the
7
+ * `/connect` routes return the connection records the agent consumes, so
8
+ * whatever can set it receives the key and supplies those records. `$env` merges
9
+ * the caller's `cwd/.env` into `process.env`.
10
+ */
11
+ const SMITHERY_API_BASE_URL = ($credentialEnv("SMITHERY_API_URL") || "https://api.smithery.ai").replace(/\/+$/, "");
2
12
 
3
13
  export class SmitheryConnectError extends Error {
4
14
  status: number;
@@ -179,7 +179,14 @@ export class AcpSdkAdapter {
179
179
  this.#assertGenericDisposition("global", operation);
180
180
  if (isLifecycleOperation(operation) && !idempotencyKey)
181
181
  throw new AcpSdkAdapterError("invalid_input", "idempotencyKey is required for lifecycle operations.");
182
- return await this.#client.global(operation, input, { idempotencyKey });
182
+ const readinessTimeoutMs =
183
+ typeof input.readinessTimeoutMs === "number" && Number.isSafeInteger(input.readinessTimeoutMs)
184
+ ? input.readinessTimeoutMs
185
+ : undefined;
186
+ return await this.#client.global(operation, input, {
187
+ idempotencyKey,
188
+ ...(readinessTimeoutMs ? { timeoutMs: readinessTimeoutMs + 1_000 } : {}),
189
+ });
183
190
  }
184
191
 
185
192
  async sdkControl(params: { operation: string; input?: JsonObject }): Promise<unknown> {
@@ -0,0 +1,40 @@
1
+ export const ACP_FINAL_TEXT_LIMIT = 100_000;
2
+
3
+ export interface BoundedAcpFinalText {
4
+ text: string;
5
+ truncated: boolean;
6
+ }
7
+
8
+ export type AcpFinalTextResolution =
9
+ | { kind: "none"; final: BoundedAcpFinalText }
10
+ | { kind: "emit"; final: BoundedAcpFinalText; text: string }
11
+ | { kind: "divergent"; final: BoundedAcpFinalText };
12
+
13
+ export function boundAcpFinalText(value: string): BoundedAcpFinalText {
14
+ if (value.length <= ACP_FINAL_TEXT_LIMIT) return { text: value, truncated: false };
15
+ let text = value.slice(0, ACP_FINAL_TEXT_LIMIT);
16
+ const finalCodeUnit = text.charCodeAt(text.length - 1);
17
+ if (finalCodeUnit >= 0xd800 && finalCodeUnit <= 0xdbff) text = text.slice(0, -1);
18
+ return { text, truncated: true };
19
+ }
20
+
21
+ export function acpFinalTextFromMessage(message: unknown): BoundedAcpFinalText {
22
+ const content = (message as { content?: unknown } | null | undefined)?.content;
23
+ if (typeof content === "string") return boundAcpFinalText(content);
24
+ if (!Array.isArray(content)) return { text: "", truncated: false };
25
+ const parts: string[] = [];
26
+ for (const block of content) {
27
+ if (!block || typeof block !== "object" || (block as { type?: unknown }).type !== "text") continue;
28
+ const text = (block as { text?: unknown }).text;
29
+ if (typeof text === "string") parts.push(text);
30
+ }
31
+ return boundAcpFinalText(parts.join(""));
32
+ }
33
+
34
+ export function resolveAcpFinalText(streamed: string, finalText: string): AcpFinalTextResolution {
35
+ const final = boundAcpFinalText(finalText);
36
+ if (!final.text || streamed === final.text || streamed.includes(final.text)) return { kind: "none", final };
37
+ if (!streamed) return { kind: "emit", final, text: final.text };
38
+ if (final.text.startsWith(streamed)) return { kind: "emit", final, text: final.text.slice(streamed.length) };
39
+ return { kind: "divergent", final };
40
+ }
@@ -0,0 +1,26 @@
1
+ export const ACP_MCP_REQUEST_TIMEOUT_MS = 30_000;
2
+ export const ACP_MCP_LIFECYCLE_TIMEOUT_MS = ACP_MCP_REQUEST_TIMEOUT_MS + 500;
3
+ /**
4
+ * Slice of the readiness budget reserved for the work that follows MCP startup
5
+ * (session wiring, marker publication). The ACP MCP startup ceiling is the
6
+ * remaining time to `semanticReadyDeadlineAt` minus this headroom, so a slow
7
+ * MCP handshake cannot consume the whole readiness window.
8
+ */
9
+ export const ACP_MCP_STARTUP_HEADROOM_MS = 250;
10
+
11
+ export interface SessionLifecycleMcpStdioServer {
12
+ type?: "stdio";
13
+ name: string;
14
+ command: string;
15
+ args: string[];
16
+ env?: Record<string, string>;
17
+ }
18
+
19
+ export interface SessionLifecycleMcpRemoteServer {
20
+ type: "http" | "sse";
21
+ name: string;
22
+ url: string;
23
+ headers?: Record<string, string>;
24
+ }
25
+
26
+ export type SessionLifecycleMcpServer = SessionLifecycleMcpStdioServer | SessionLifecycleMcpRemoteServer;
@@ -5,7 +5,7 @@ import * as fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import type { NativeExactUnlinkResult } from "@gajae-code/natives";
7
7
  import * as native from "@gajae-code/natives";
8
- import { resolveEquivalentPath } from "@gajae-code/utils";
8
+ import { $credentialEnv, resolveEquivalentPath } from "@gajae-code/utils";
9
9
 
10
10
  import {
11
11
  isModelProfileError,
@@ -29,6 +29,7 @@ import {
29
29
  type VerifiedSessionDeleteResult,
30
30
  type VerifiedSessionDeleteTarget,
31
31
  } from "../../session/session-storage";
32
+ import type { SessionLifecycleMcpServer } from "../acp/mcp";
32
33
  import { SdkClient, SdkClientError } from "../client/client";
33
34
  import {
34
35
  type LogicalSessionCandidate,
@@ -215,6 +216,7 @@ export interface SessionLifecycleLaunchRequest {
215
216
  /** Broker-issued effect marker which the child echoes only after host readiness. */
216
217
  effectMarker?: string;
217
218
  modelPreset?: string;
219
+ mcpServers?: SessionLifecycleMcpServer[];
218
220
  worktree?: SessionLifecycleWorktreeTarget;
219
221
  receivedAt: number;
220
222
  requestedReadinessTimeoutMs: number;
@@ -248,6 +250,77 @@ function hasValidTranscriptAuthority(path: unknown, identity: unknown): path is
248
250
  return typeof path === "string" && path.length > 0 && isSessionLifecycleTranscriptIdentity(identity);
249
251
  }
250
252
 
253
+ function isSessionLifecycleMcpServer(value: unknown): value is SessionLifecycleMcpServer {
254
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
255
+ const server = value as Record<string, unknown>;
256
+ if (server.type === "http" || server.type === "sse") {
257
+ if (
258
+ !Object.keys(server).every(key => key === "type" || key === "name" || key === "url" || key === "headers") ||
259
+ typeof server.name !== "string" ||
260
+ !/^[A-Za-z0-9_.-]{1,100}$/.test(server.name) ||
261
+ typeof server.url !== "string" ||
262
+ server.url.length > 8_192
263
+ )
264
+ return false;
265
+ try {
266
+ const url = new URL(server.url);
267
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
268
+ } catch {
269
+ return false;
270
+ }
271
+ if (server.headers === undefined) return true;
272
+ if (typeof server.headers !== "object" || server.headers === null || Array.isArray(server.headers)) return false;
273
+ const headers = server.headers as Record<string, unknown>;
274
+ return (
275
+ Object.keys(headers).length <= 100 &&
276
+ Object.entries(headers).every(
277
+ ([name, headerValue]) =>
278
+ name.length > 0 &&
279
+ name.length <= 256 &&
280
+ !name.includes("\r") &&
281
+ !name.includes("\n") &&
282
+ typeof headerValue === "string" &&
283
+ headerValue.length <= 8_192 &&
284
+ !headerValue.includes("\r") &&
285
+ !headerValue.includes("\n"),
286
+ )
287
+ );
288
+ }
289
+ const env = server.env;
290
+ return (
291
+ Object.keys(server).every(
292
+ key => key === "type" || key === "name" || key === "command" || key === "args" || key === "env",
293
+ ) &&
294
+ (server.type === undefined || server.type === "stdio") &&
295
+ typeof server.name === "string" &&
296
+ /^[A-Za-z0-9_.-]{1,100}$/.test(server.name) &&
297
+ typeof server.command === "string" &&
298
+ server.command.length <= 4_096 &&
299
+ path.isAbsolute(server.command) &&
300
+ Array.isArray(server.args) &&
301
+ server.args.length <= 100 &&
302
+ server.args.every(argument => typeof argument === "string" && argument.length <= 8_192) &&
303
+ (env === undefined ||
304
+ (typeof env === "object" &&
305
+ env !== null &&
306
+ !Array.isArray(env) &&
307
+ Object.keys(env).length <= 100 &&
308
+ Object.entries(env).every(
309
+ ([name, envValue]) =>
310
+ /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && typeof envValue === "string" && envValue.length <= 32_768,
311
+ )))
312
+ );
313
+ }
314
+
315
+ function isSessionLifecycleMcpServers(value: unknown): value is SessionLifecycleMcpServer[] {
316
+ return (
317
+ Array.isArray(value) &&
318
+ value.length <= 64 &&
319
+ value.every(isSessionLifecycleMcpServer) &&
320
+ new Set(value.map(server => server.name)).size === value.length
321
+ );
322
+ }
323
+
251
324
  export function readSessionLifecycleLaunchRequest(
252
325
  value: string | undefined,
253
326
  now = Date.now(),
@@ -278,6 +351,7 @@ export function readSessionLifecycleLaunchRequest(
278
351
  (request.effectMarker !== undefined &&
279
352
  (typeof request.effectMarker !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(request.effectMarker))) ||
280
353
  (request.modelPreset !== undefined && (typeof request.modelPreset !== "string" || !request.modelPreset)) ||
354
+ (request.mcpServers !== undefined && !isSessionLifecycleMcpServers(request.mcpServers)) ||
281
355
  !hasValidLifecycleDeadlines(
282
356
  {
283
357
  receivedAt: request.receivedAt as number,
@@ -310,6 +384,7 @@ type SessionLaunch = {
310
384
  sessionPath?: string;
311
385
  sessionIdentity?: SessionLifecycleTranscriptIdentity;
312
386
  modelPreset?: string;
387
+ mcpServers?: SessionLifecycleMcpServer[];
313
388
  worktree?: SessionLifecycleWorktreeTarget;
314
389
  worktreePlan?: GjcLaunchWorktreePlan;
315
390
  };
@@ -631,12 +706,31 @@ async function reconcileReadyScope(broker: Broker, id: string, scope: string | u
631
706
  });
632
707
  }
633
708
 
709
+ /**
710
+ * Operator override for the session-host command, resolved from trusted
711
+ * environment sources only.
712
+ *
713
+ * The result is spawned directly, so whatever can set it chooses which binary
714
+ * the broker runs. `$env` merges the caller's `cwd/.env` into `process.env`, so
715
+ * reading it there would let repository content replace the session host;
716
+ * resolve it the same way provider credentials are (launching shell plus
717
+ * GJC/user-owned `.env` files, never the project `.env`).
718
+ */
719
+ function sdkSessionCommandOverride(): { file: string; args: string[] } | undefined {
720
+ const configured = $credentialEnv("GJC_SDK_SESSION_COMMAND");
721
+ if (!configured) return undefined;
722
+ const [file, ...args] = configured.trim().split(/\s+/);
723
+ return file ? { file, args } : undefined;
724
+ }
725
+
726
+ /** Test seam: the session-host command override as resolved from trusted env. */
727
+ export function sdkSessionCommandOverrideForTest(): { file: string; args: string[] } | undefined {
728
+ return sdkSessionCommandOverride();
729
+ }
730
+
634
731
  function command(broker: Broker): LifecycleCommand {
635
- const configured = process.env.GJC_SDK_SESSION_COMMAND;
636
- if (configured) {
637
- const [file, ...args] = configured.trim().split(/\s+/);
638
- if (file) return { file, args };
639
- }
732
+ const configured = sdkSessionCommandOverride();
733
+ if (configured) return configured;
640
734
  return lifecycleCommandResolversForTest.get(broker)?.() ?? resolveSdkInternalSpawnCommand("session-host-internal");
641
735
  }
642
736
 
@@ -2299,9 +2393,12 @@ async function launchInput(
2299
2393
  if (input.modelPreset !== undefined && (typeof input.modelPreset !== "string" || input.modelPreset.length === 0))
2300
2394
  return fail("invalid_input", "modelPreset must be a non-empty exact profile ID.");
2301
2395
  const modelPreset = text(input.modelPreset);
2396
+ if (input.mcpServers !== undefined && !isSessionLifecycleMcpServers(input.mcpServers))
2397
+ return fail("invalid_input", "mcpServers must contain unique valid stdio, HTTP, or SSE server definitions.");
2398
+ const mcpServers = input.mcpServers as SessionLifecycleMcpServer[] | undefined;
2302
2399
 
2303
2400
  if (operation === "session.create")
2304
- return { id: randomUUID(), cwd, root: resolvedRoot, modelPreset, worktree, worktreePlan };
2401
+ return { id: randomUUID(), cwd, root: resolvedRoot, modelPreset, mcpServers, worktree, worktreePlan };
2305
2402
  if (operation === "session.resume") {
2306
2403
  if (!requested) return fail("invalid_input", "sessionId is required to resume a saved session.");
2307
2404
  const savedPath = text(input.sessionPath);
@@ -2315,6 +2412,7 @@ async function launchInput(
2315
2412
  sessionPath: saved.path,
2316
2413
  sessionIdentity: saved.identity,
2317
2414
  modelPreset,
2415
+ mcpServers,
2318
2416
  worktree,
2319
2417
  worktreePlan,
2320
2418
  };
@@ -2336,6 +2434,7 @@ async function launchInput(
2336
2434
  sourceSessionIdentity: source.identity,
2337
2435
  sourceCwd,
2338
2436
  modelPreset,
2437
+ mcpServers,
2339
2438
  worktree,
2340
2439
  worktreePlan,
2341
2440
  };
@@ -2805,6 +2904,7 @@ async function executeLifecycleResponse(
2805
2904
  ...(launch.sessionPath ? { sessionPath: launch.sessionPath } : {}),
2806
2905
  ...(launch.sessionIdentity ? { sessionIdentity: launch.sessionIdentity } : {}),
2807
2906
  ...(launch.modelPreset ? { modelPreset: launch.modelPreset } : {}),
2907
+ ...(launch.mcpServers ? { mcpServers: launch.mcpServers } : {}),
2808
2908
  ...(launch.worktree ? { worktree: launch.worktree } : {}),
2809
2909
  };
2810
2910
  let child: ChildProcess | undefined;
@@ -67,7 +67,7 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
67
67
  idleTimeoutMs: 60_000,
68
68
  rich: { enabled: true },
69
69
  richDraft: { enabled: false },
70
- toolActivity: { enabled: true },
70
+ toolActivity: { enabled: false },
71
71
  topics: { nameTemplate: undefined },
72
72
  btw: { enabled: true },
73
73
  streaming: { enabled: true },
@@ -102,7 +102,7 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
102
102
  idleTimeoutMs: 60_000,
103
103
  rich: { enabled: true },
104
104
  richDraft: { enabled: false },
105
- toolActivity: { enabled: true },
105
+ toolActivity: { enabled: false },
106
106
  topics: { nameTemplate: undefined },
107
107
  btw: { enabled: true },
108
108
  streaming: { enabled: true },
@@ -34,11 +34,12 @@ export type ChatDaemonAction = "stop" | "reload";
34
34
  * concrete detached quarantine plus a proven-absent canonical lock pathname —
35
35
  * when deleting an observed owner-lock lease. Generation 12 refreshes retained
36
36
  * native path and process authority semantics. Generation 13 rejects special
37
- * files before retained native authority opens.
37
+ * files before retained native authority opens. Generation 14 reloads shared
38
+ * chat daemons after notification configuration parsing changes.
38
39
  */
39
40
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
40
- discord: 13,
41
- slack: 13,
41
+ discord: 14,
42
+ slack: 14,
42
43
  };
43
44
 
44
45
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {
@@ -170,7 +170,7 @@ export function parseNotificationSettingsSnapshot(rawConfig?: unknown): Notifica
170
170
  enabled: notificationSettingsBoolean(richDraft.enabled, false),
171
171
  },
172
172
  toolActivity: {
173
- enabled: notificationSettingsBoolean(toolActivity.enabled, true),
173
+ enabled: notificationSettingsBoolean(toolActivity.enabled, false),
174
174
  },
175
175
  streaming: {
176
176
  enabled: notificationSettingsBoolean(streaming.enabled, true),
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Ordering seam for an in-band SDK identity transition.
3
+ *
4
+ * A request arrives through predecessor A, so a successful terminal response cannot
5
+ * be published until successor B is ready. Native builds may provide a true
6
+ * send-only connection lease; source/dev builds use the conservative fallback in
7
+ * which the predecessor server remains alive until the terminal write completes.
8
+ */
9
+
10
+ export type TerminalSendOutcome = "written" | "disconnected" | "write_failed";
11
+
12
+ /** Fence/capability hooks may return false to reject; undefined means ok. */
13
+ export type IdentityControlGateResult = boolean | undefined;
14
+ export type IdentityControlGate = () => IdentityControlGateResult | Promise<IdentityControlGateResult>;
15
+
16
+ export interface IdentityControlSuccessPathInput {
17
+ /** Fence predecessor inbound work. The fence must not close its response writer. */
18
+ fence: IdentityControlGate;
19
+ /** Optional explicit proof that the predecessor writer is still usable. */
20
+ ensurePredecessorSendCapable?: IdentityControlGate;
21
+ /** Start successor B and resolve only after its public endpoint is ready. */
22
+ startSuccessor: () => Promise<void>;
23
+ /** Write and flush either the success or non-success terminal response. */
24
+ sendTerminal: () => Promise<TerminalSendOutcome>;
25
+ /** Release predecessor A after the terminal response has settled. */
26
+ stopPredecessor: () => Promise<void>;
27
+ /** Set only when a caller cannot use the retained-writer fallback. */
28
+ requireNativeControlDrain?: boolean;
29
+ }
30
+
31
+ export interface IdentityControlTerminalPathInput {
32
+ fenceInbound: IdentityControlGate;
33
+ ensurePredecessorSendCapable?: IdentityControlGate;
34
+ startSuccessorReady: () => Promise<void>;
35
+ sendTerminal: () => Promise<TerminalSendOutcome>;
36
+ stopPredecessor: () => Promise<void>;
37
+ requireNativeControlDrain?: boolean;
38
+ }
39
+
40
+ /**
41
+ * Probe the loaded native addon, never an environment flag or a TypeScript shim.
42
+ * Older/source installs intentionally report false so callers can select the
43
+ * retained-writer fallback or fail closed when detach is mandatory.
44
+ */
45
+ export function isNativeControlDrainAvailable(): boolean {
46
+ try {
47
+ // Keep the addon lazy: importing the bus must continue to work without a
48
+ // compiled native artifact (tests and source distributions do that often).
49
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
50
+ const natives = require("@gajae-code/natives") as {
51
+ NotificationServer?: {
52
+ prototype?: Record<string, unknown>;
53
+ };
54
+ };
55
+ const prototype = natives.NotificationServer?.prototype;
56
+ return (
57
+ typeof prototype?.acquireControlDrain === "function" &&
58
+ typeof prototype?.sendTerminalAndWait === "function" &&
59
+ typeof prototype?.closeControlDrain === "function"
60
+ );
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Run the only safe ordering for a successful identity-control response.
68
+ *
69
+ * The `finally` is deliberately around both successor startup and terminal
70
+ * delivery: a failed startup must not leave the fenced predecessor alive, and a
71
+ * failed/disconnected terminal write must still release it. When native detach
72
+ * is unavailable the caller is expected to keep A's server up (not call
73
+ * `stopAndWait`) until `sendTerminal` settles.
74
+ */
75
+ export async function runIdentityControlSuccessPath(
76
+ input: IdentityControlSuccessPathInput,
77
+ ): Promise<TerminalSendOutcome> {
78
+ if (input.requireNativeControlDrain && !isNativeControlDrainAvailable())
79
+ throw new Error("SDK identity control requires the native control-drain lease.");
80
+
81
+ if ((await input.fence()) === false) throw new Error("SDK identity control predecessor fence was not acquired.");
82
+ if ((await input.ensurePredecessorSendCapable?.()) === false)
83
+ throw new Error("SDK identity control predecessor writer is not send-capable.");
84
+ try {
85
+ await input.startSuccessor();
86
+ return await input.sendTerminal();
87
+ } finally {
88
+ await input.stopPredecessor();
89
+ }
90
+ }
91
+
92
+ /** Compatibility spelling used by the host wiring design notes. */
93
+ export async function runIdentityControlTerminalPath(
94
+ input: IdentityControlTerminalPathInput,
95
+ ): Promise<TerminalSendOutcome> {
96
+ return runIdentityControlSuccessPath({
97
+ fence: input.fenceInbound,
98
+ ensurePredecessorSendCapable: input.ensurePredecessorSendCapable,
99
+ startSuccessor: input.startSuccessorReady,
100
+ sendTerminal: input.sendTerminal,
101
+ stopPredecessor: input.stopPredecessor,
102
+ requireNativeControlDrain: input.requireNativeControlDrain,
103
+ });
104
+ }