@gajae-code/coding-agent 0.11.9 → 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 (150) hide show
  1. package/CHANGELOG.md +56 -1
  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/model-registry.d.ts +2 -0
  6. package/dist/types/config/settings-schema.d.ts +57 -2
  7. package/dist/types/config/settings.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  9. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  12. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  13. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  14. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  15. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  16. package/dist/types/modes/components/welcome.d.ts +1 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
  18. package/dist/types/modes/interactive-mode.d.ts +5 -1
  19. package/dist/types/runtime/memory-domain.d.ts +9 -0
  20. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  21. package/dist/types/runtime/memory-guard.d.ts +39 -0
  22. package/dist/types/runtime/memory-limit.d.ts +11 -0
  23. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  24. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  25. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  26. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  28. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  29. package/dist/types/sdk/bus/index.d.ts +11 -2
  30. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  31. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  32. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
  33. package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
  34. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  36. package/dist/types/sdk/session.d.ts +7 -0
  37. package/dist/types/sdk/startup-capability.d.ts +6 -0
  38. package/dist/types/session/agent-session.d.ts +11 -11
  39. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  40. package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
  41. package/dist/types/session/session-manager.d.ts +34 -0
  42. package/dist/types/task/executor.d.ts +6 -0
  43. package/dist/types/task/provider-retry-status.d.ts +13 -0
  44. package/dist/types/task/types.d.ts +39 -0
  45. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  46. package/dist/types/tools/index.d.ts +3 -1
  47. package/dist/types/tools/resource-gc.d.ts +19 -0
  48. package/package.json +8 -8
  49. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  50. package/scripts/run-test-manifest.ts +6 -4
  51. package/src/cli/args.ts +14 -8
  52. package/src/cli/fast-help.ts +1 -1
  53. package/src/cli/notify-cli.ts +1 -0
  54. package/src/cli.ts +48 -2
  55. package/src/commands/sdk.ts +91 -7
  56. package/src/config/model-profile-activation.ts +1 -0
  57. package/src/config/model-profile-contract.ts +13 -0
  58. package/src/config/model-profiles.ts +31 -7
  59. package/src/config/model-registry.ts +4 -2
  60. package/src/config/settings-schema.ts +61 -2
  61. package/src/config/settings.ts +8 -0
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  64. package/src/eval/js/tool-bridge.ts +1 -1
  65. package/src/extensibility/extensions/runner.ts +4 -0
  66. package/src/extensibility/extensions/types.ts +14 -0
  67. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  68. package/src/gjc-runtime/deep-interview-draft.ts +2 -1
  69. package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
  70. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  71. package/src/gjc-runtime/linux-proc.ts +70 -36
  72. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  73. package/src/gjc-runtime/state-runtime.ts +48 -11
  74. package/src/gjc-runtime/state-writer.ts +1 -1
  75. package/src/hooks/skill-state.ts +23 -1
  76. package/src/internal-urls/docs-index.generated.ts +7 -7
  77. package/src/internal-urls/local-protocol.ts +23 -4
  78. package/src/modes/acp/acp-agent.ts +554 -60
  79. package/src/modes/acp/acp-event-mapper.ts +93 -6
  80. package/src/modes/components/model-selector.ts +15 -1
  81. package/src/modes/components/notifications-settings-editor.ts +1 -1
  82. package/src/modes/components/queue-pane.ts +55 -9
  83. package/src/modes/components/queued-message-selector.ts +53 -10
  84. package/src/modes/components/tool-status-header.ts +1 -4
  85. package/src/modes/components/welcome.ts +18 -3
  86. package/src/modes/controllers/command-controller.ts +4 -0
  87. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  88. package/src/modes/controllers/input-controller.ts +44 -3
  89. package/src/modes/controllers/selector-controller.ts +38 -8
  90. package/src/modes/interactive-mode.ts +65 -26
  91. package/src/modes/runtime-init.ts +1 -0
  92. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  93. package/src/prompts/agents/architect.md +8 -1
  94. package/src/prompts/agents/critic.md +8 -1
  95. package/src/prompts/agents/executor.md +1 -1
  96. package/src/prompts/agents/planner.md +8 -1
  97. package/src/runtime/memory-domain.ts +66 -0
  98. package/src/runtime/memory-guard-contract.ts +73 -0
  99. package/src/runtime/memory-guard.ts +292 -0
  100. package/src/runtime/memory-limit.ts +50 -0
  101. package/src/runtime-mcp/manager.ts +26 -6
  102. package/src/sdk/acp/adapter.ts +8 -1
  103. package/src/sdk/acp/final-text.ts +40 -0
  104. package/src/sdk/acp/mcp.ts +26 -0
  105. package/src/sdk/broker/identity.ts +22 -12
  106. package/src/sdk/broker/lifecycle.ts +82 -1
  107. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  108. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  109. package/src/sdk/bus/config.ts +1 -1
  110. package/src/sdk/bus/daemon-paths.ts +2 -0
  111. package/src/sdk/bus/index.ts +222 -27
  112. package/src/sdk/bus/notification-service.ts +19 -5
  113. package/src/sdk/bus/operator-runtime.ts +31 -5
  114. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  115. package/src/sdk/bus/telegram-cli.ts +6 -13
  116. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  117. package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
  118. package/src/sdk/bus/telegram-daemon-control.ts +14 -5
  119. package/src/sdk/bus/telegram-daemon.ts +1191 -164
  120. package/src/sdk/host/control/dispatch.ts +2 -0
  121. package/src/sdk/host/control/operations.ts +1 -0
  122. package/src/sdk/host/host.ts +2 -2
  123. package/src/sdk/lifecycle-session.ts +19 -3
  124. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  125. package/src/sdk/protocol/operation-registry.ts +1 -0
  126. package/src/sdk/session.ts +24 -1
  127. package/src/sdk/startup-capability.ts +6 -0
  128. package/src/session/agent-session.ts +290 -132
  129. package/src/session/fallback-chain-controller.ts +47 -8
  130. package/src/session/internal/managed-session-scope.ts +173 -127
  131. package/src/session/internal/native-publish-outcome.ts +8 -3
  132. package/src/session/session-manager.ts +608 -25
  133. package/src/skill-state/workflow-mutation-guard.ts +41 -7
  134. package/src/task/executor.ts +152 -42
  135. package/src/task/index.ts +2 -0
  136. package/src/task/provider-retry-status.ts +88 -0
  137. package/src/task/render.ts +45 -26
  138. package/src/task/types.ts +25 -11
  139. package/src/task/ultragoal-redteam-activation.ts +75 -0
  140. package/src/tools/bash-allowed-prefixes.ts +45 -1
  141. package/src/tools/index.ts +3 -1
  142. package/src/tools/resource-gc.ts +496 -120
  143. package/src/tools/subagent-render.ts +74 -16
  144. package/src/tools/subagent.ts +2 -0
  145. package/src/workflow/workflow-intent-diff.ts +4 -1
  146. package/vendor/insane-search/MANIFEST.json +4 -1
  147. package/vendor/insane-search/engine/bias_check.py +5 -0
  148. package/vendor/insane-search/engine/fetch_chain.py +67 -1
  149. package/vendor/insane-search/engine/transport.py +13 -2
  150. package/vendor/insane-search/engine/validators.py +4 -1
@@ -0,0 +1,292 @@
1
+ import type { Settings } from "../config/settings";
2
+ import type {
3
+ MemoryGuardActionTarget,
4
+ MemoryGuardDecision,
5
+ MemoryGuardDomainSnapshot,
6
+ MemoryGuardPolicy,
7
+ MemoryGuardSchedulerState,
8
+ } from "./memory-guard-contract";
9
+
10
+ const BYTES_PER_MB = 1024 * 1024;
11
+ const DEFAULT_CHECK_INTERVAL_MS = 30_000;
12
+
13
+ type ScheduleOwner = { generation: number; token: number };
14
+ type DeferredSchedule = { generation: number; deadline: number };
15
+ type WorkOwner = { generation: number; source: "timer" | "external" };
16
+
17
+ export interface MemoryGuardHostRegistration {
18
+ ownerId: string;
19
+ intervalMs: number;
20
+ }
21
+
22
+ export interface MemoryGuardHostOptions {
23
+ run: () => Promise<void>;
24
+ logDebug?: (message: string, meta?: Record<string, unknown>) => void;
25
+ schedulerNow?: () => number;
26
+ }
27
+
28
+ function normalizePositiveIntervalMs(intervalMs: number): number {
29
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) throw new Error("invalid_interval_ms");
30
+ return intervalMs;
31
+ }
32
+
33
+ function toRatio(percent: number): number {
34
+ return percent / 100;
35
+ }
36
+
37
+ function mbToBytes(value: number): number {
38
+ return Math.round(value * BYTES_PER_MB);
39
+ }
40
+
41
+ function sortTargets(left: MemoryGuardActionTarget, right: MemoryGuardActionTarget): number {
42
+ if (left.excessBytes !== right.excessBytes) return right.excessBytes - left.excessBytes;
43
+ if (left.kind !== right.kind) return left.kind === "host" ? -1 : 1;
44
+ if (left.kind === "worker" && right.kind === "worker") return left.workerId.localeCompare(right.workerId);
45
+ return 0;
46
+ }
47
+
48
+ export function resolveMemoryGuardPolicy(settings: Settings): MemoryGuardPolicy {
49
+ const configuredPolicyLimitMb = settings.get("memoryGuard.policyLimitMb");
50
+ return {
51
+ enabled: settings.get("memoryGuard.enabled"),
52
+ checkIntervalMs: settings.get("memoryGuard.checkIntervalMs"),
53
+ gcThresholdRatio: toRatio(settings.get("memoryGuard.gcThresholdPercent")),
54
+ restartThresholdRatio: toRatio(settings.get("memoryGuard.restartThresholdPercent")),
55
+ restartThresholdWindowMs: settings.get("memoryGuard.restartThresholdWindowMs"),
56
+ cooldownMs: settings.get("memoryGuard.cooldownMs"),
57
+ parentReserveBytes: mbToBytes(settings.get("memoryGuard.parentReserveMb")),
58
+ policyLimitBytes: configuredPolicyLimitMb > 0 ? mbToBytes(configuredPolicyLimitMb) : null,
59
+ };
60
+ }
61
+
62
+ export function chooseMemoryGuardAction(input: {
63
+ domain: MemoryGuardDomainSnapshot;
64
+ hostSupported: boolean;
65
+ workerSupported: (workerId: string) => boolean;
66
+ }): MemoryGuardDecision {
67
+ const candidates: MemoryGuardActionTarget[] = [];
68
+ if (input.hostSupported && input.domain.hostExcessBytes > 0) {
69
+ candidates.push({ kind: "host", excessBytes: input.domain.hostExcessBytes });
70
+ }
71
+ for (const worker of input.domain.workers) {
72
+ if (!worker.accepted || worker.excessBytes <= 0 || !input.workerSupported(worker.workerId)) continue;
73
+ candidates.push({ kind: "worker", workerId: worker.workerId, excessBytes: worker.excessBytes });
74
+ }
75
+ if (candidates.length > 0) {
76
+ candidates.sort(sortTargets);
77
+ return { kind: "execute", target: candidates[0]! };
78
+ }
79
+ const unsupportedExcessExists =
80
+ input.domain.hostExcessBytes > 0 ||
81
+ input.domain.workers.some(worker => worker.accepted && worker.excessBytes > 0);
82
+ if (unsupportedExcessExists) return { kind: "noop", reason: "memory_guard_action_noop_unsupported" };
83
+ return { kind: "noop", reason: "memory_guard_action_noop_unmanaged_or_within_allowance" };
84
+ }
85
+
86
+ export function revalidateMemoryGuardAction(
87
+ previous: Extract<MemoryGuardDecision, { kind: "execute" }>,
88
+ revalidated: MemoryGuardDecision,
89
+ ): MemoryGuardDecision {
90
+ if (revalidated.kind !== "execute")
91
+ return { kind: "revalidated_out", reason: "memory_guard_action_revalidated_out" };
92
+ if (previous.target.kind !== revalidated.target.kind)
93
+ return { kind: "revalidated_out", reason: "memory_guard_action_revalidated_out" };
94
+ if (
95
+ previous.target.kind === "worker" &&
96
+ revalidated.target.kind === "worker" &&
97
+ previous.target.workerId !== revalidated.target.workerId
98
+ ) {
99
+ return { kind: "revalidated_out", reason: "memory_guard_action_revalidated_out" };
100
+ }
101
+ return revalidated;
102
+ }
103
+
104
+ export class MemoryGuardHost {
105
+ #run: () => Promise<void>;
106
+ #logDebug: (message: string, meta?: Record<string, unknown>) => void;
107
+ #defaultSchedulerNow: () => number;
108
+ #schedulerNow: () => number;
109
+ #registrations = new Map<string, { intervalMs: number; nextDueMs: number }>();
110
+ #pendingTimer: NodeJS.Timeout | null = null;
111
+ #pendingDeadline: number | null = null;
112
+ #pendingOwner: ScheduleOwner | null = null;
113
+ #deferredSchedule: DeferredSchedule | null = null;
114
+ #inProgressOwner: WorkOwner | null = null;
115
+ #stopped = false;
116
+ #generation = 0;
117
+ #nextTimerToken = 0;
118
+
119
+ constructor(options: MemoryGuardHostOptions) {
120
+ this.#run = options.run;
121
+ this.#logDebug = options.logDebug ?? (() => undefined);
122
+ this.#defaultSchedulerNow = options.schedulerNow ?? (() => performance.now());
123
+ this.#schedulerNow = this.#defaultSchedulerNow;
124
+ }
125
+
126
+ register(registration: MemoryGuardHostRegistration): () => void {
127
+ const intervalMs = normalizePositiveIntervalMs(registration.intervalMs);
128
+ const now = this.#schedulerNow();
129
+ const existing = this.#registrations.get(registration.ownerId);
130
+ const isNewRegistration = !existing;
131
+ const nextDueMs = existing ? Math.min(existing.nextDueMs, now + intervalMs) : now + intervalMs;
132
+ this.#registrations.set(registration.ownerId, { intervalMs, nextDueMs });
133
+ this.#stopped = false;
134
+ if (isNewRegistration) {
135
+ const deadline = nextDueMs;
136
+ if (
137
+ this.#inProgressOwner?.generation === this.#generation &&
138
+ (this.#pendingDeadline === null || this.#pendingDeadline <= deadline)
139
+ ) {
140
+ this.#deferSchedule(this.#generation, deadline);
141
+ } else {
142
+ this.#requestSchedule(deadline);
143
+ }
144
+ }
145
+ let unregistered = false;
146
+ return () => {
147
+ if (unregistered) return;
148
+ unregistered = true;
149
+ this.#registrations.delete(registration.ownerId);
150
+ if (this.#registrations.size === 0) this.#stop();
151
+ };
152
+ }
153
+
154
+ updateInterval(ownerId: string, intervalMs: number): void {
155
+ const existing = this.#registrations.get(ownerId);
156
+ if (!existing) return;
157
+ const normalized = normalizePositiveIntervalMs(intervalMs);
158
+ if (existing.intervalMs === normalized) return;
159
+ const now = this.#schedulerNow();
160
+ const nextDueMs = now + normalized;
161
+ this.#registrations.set(ownerId, { intervalMs: normalized, nextDueMs });
162
+ if (this.#inProgressOwner) return;
163
+ const nextDeadline = this.#nextDeadlineMs();
164
+ if (this.#pendingDeadline !== null && nextDeadline > this.#pendingDeadline) {
165
+ this.#clearPendingSchedule();
166
+ }
167
+ this.#requestSchedule(nextDeadline);
168
+ }
169
+
170
+ async runTick(generation = this.#generation, source: WorkOwner["source"] = "external"): Promise<void> {
171
+ if (this.#inProgressOwner || this.#registrations.size === 0) return;
172
+ const owner: WorkOwner = { generation, source };
173
+ this.#inProgressOwner = owner;
174
+ try {
175
+ await this.#run();
176
+ } catch (error) {
177
+ this.#logDebug("memory guard tick failed", { error: error instanceof Error ? error.message : String(error) });
178
+ } finally {
179
+ if (this.#inProgressOwner === owner) this.#inProgressOwner = null;
180
+ this.#advanceDueTimes();
181
+ this.#reconcileCurrentSchedule();
182
+ }
183
+ }
184
+
185
+ async runTimerCallbackForTest(owner: { generation: number; token: number }, deadline: number): Promise<void> {
186
+ await this.#handleTimerCallback(owner, deadline);
187
+ }
188
+
189
+ setSchedulerNowForTest(now: () => number): void {
190
+ this.#schedulerNow = now;
191
+ }
192
+
193
+ getStateForTest(): MemoryGuardSchedulerState {
194
+ return {
195
+ timerActive: this.#pendingTimer !== null,
196
+ registrationCount: this.#registrations.size,
197
+ inProgress: this.#inProgressOwner !== null,
198
+ generation: this.#generation,
199
+ pendingDeadline: this.#pendingDeadline,
200
+ pendingOwner: this.#pendingOwner ? { ...this.#pendingOwner } : null,
201
+ deferredDeadline: this.#deferredSchedule?.deadline ?? null,
202
+ deferredGeneration: this.#deferredSchedule?.generation ?? null,
203
+ activeGeneration: this.#inProgressOwner?.generation ?? null,
204
+ };
205
+ }
206
+
207
+ resetForTest(): void {
208
+ this.#stop();
209
+ this.#registrations.clear();
210
+ this.#inProgressOwner = null;
211
+ this.#deferredSchedule = null;
212
+ this.#nextTimerToken = 0;
213
+ this.#schedulerNow = this.#defaultSchedulerNow;
214
+ }
215
+
216
+ #nextDeadlineMs(): number {
217
+ let minDue = Number.POSITIVE_INFINITY;
218
+ for (const reg of this.#registrations.values()) {
219
+ minDue = Math.min(minDue, reg.nextDueMs);
220
+ }
221
+ return Number.isFinite(minDue) ? minDue : this.#schedulerNow() + DEFAULT_CHECK_INTERVAL_MS;
222
+ }
223
+
224
+ #advanceDueTimes(): void {
225
+ const now = this.#schedulerNow();
226
+ for (const reg of this.#registrations.values()) {
227
+ if (reg.nextDueMs <= now) {
228
+ reg.nextDueMs = now + reg.intervalMs;
229
+ }
230
+ }
231
+ }
232
+
233
+ #clearPendingSchedule(): void {
234
+ if (this.#pendingTimer) clearTimeout(this.#pendingTimer);
235
+ this.#pendingTimer = null;
236
+ this.#pendingDeadline = null;
237
+ this.#pendingOwner = null;
238
+ }
239
+
240
+ #requestSchedule(deadline: number): void {
241
+ if (this.#stopped || this.#registrations.size === 0) return;
242
+ if (this.#pendingDeadline !== null && this.#pendingDeadline <= deadline) return;
243
+ this.#clearPendingSchedule();
244
+ const owner = { generation: this.#generation, token: ++this.#nextTimerToken };
245
+ this.#pendingDeadline = deadline;
246
+ this.#pendingOwner = owner;
247
+ this.#pendingTimer = setTimeout(
248
+ () => {
249
+ void this.#handleTimerCallback(owner, deadline);
250
+ },
251
+ Math.max(0, deadline - this.#schedulerNow()),
252
+ );
253
+ this.#pendingTimer.unref?.();
254
+ }
255
+
256
+ #deferSchedule(generation: number, deadline: number): void {
257
+ if (generation !== this.#generation) return;
258
+ if (this.#deferredSchedule?.generation === generation) {
259
+ this.#deferredSchedule.deadline = Math.min(this.#deferredSchedule.deadline, deadline);
260
+ return;
261
+ }
262
+ this.#deferredSchedule = { generation, deadline };
263
+ }
264
+
265
+ async #handleTimerCallback(owner: ScheduleOwner, deadline: number): Promise<void> {
266
+ if (this.#pendingOwner?.generation !== owner.generation || this.#pendingOwner.token !== owner.token) return;
267
+ this.#pendingTimer = null;
268
+ this.#pendingDeadline = null;
269
+ this.#pendingOwner = null;
270
+ if (this.#inProgressOwner) {
271
+ this.#deferSchedule(owner.generation, deadline);
272
+ return;
273
+ }
274
+ await this.runTick(owner.generation, "timer");
275
+ }
276
+
277
+ #reconcileCurrentSchedule(): void {
278
+ if (this.#stopped || this.#registrations.size === 0 || this.#inProgressOwner) return;
279
+ const normalDeadline = this.#nextDeadlineMs();
280
+ const deferredDeadline =
281
+ this.#deferredSchedule?.generation === this.#generation ? this.#deferredSchedule.deadline : null;
282
+ if (deferredDeadline !== null) this.#deferredSchedule = null;
283
+ this.#requestSchedule(deferredDeadline === null ? normalDeadline : Math.min(deferredDeadline, normalDeadline));
284
+ }
285
+
286
+ #stop(): void {
287
+ this.#stopped = true;
288
+ this.#generation++;
289
+ this.#clearPendingSchedule();
290
+ this.#deferredSchedule = null;
291
+ }
292
+ }
@@ -0,0 +1,50 @@
1
+ export interface EffectiveMemoryLimitInput {
2
+ hardCapBytes?: number | null;
3
+ policyLimitBytes?: number | null;
4
+ }
5
+
6
+ export interface EffectiveMemoryLimit {
7
+ hardCapBytes: number | null;
8
+ policyLimitBytes: number | null;
9
+ effectiveBytes: number | null;
10
+ source: "none" | "hard_cap" | "policy_limit" | "hard_cap_and_policy_limit";
11
+ }
12
+
13
+ function normalizePositiveByteCount(value: number | null | undefined): number | null {
14
+ return Number.isSafeInteger(value) && typeof value === "number" && value > 0 ? value : null;
15
+ }
16
+
17
+ export function resolveEffectiveMemoryLimit(input: EffectiveMemoryLimitInput): EffectiveMemoryLimit {
18
+ const hardCapBytes = normalizePositiveByteCount(input.hardCapBytes);
19
+ const policyLimitBytes = normalizePositiveByteCount(input.policyLimitBytes);
20
+ if (hardCapBytes !== null && policyLimitBytes !== null) {
21
+ return {
22
+ hardCapBytes,
23
+ policyLimitBytes,
24
+ effectiveBytes: Math.min(hardCapBytes, policyLimitBytes),
25
+ source: "hard_cap_and_policy_limit",
26
+ };
27
+ }
28
+ if (hardCapBytes !== null) {
29
+ return {
30
+ hardCapBytes,
31
+ policyLimitBytes: null,
32
+ effectiveBytes: hardCapBytes,
33
+ source: "hard_cap",
34
+ };
35
+ }
36
+ if (policyLimitBytes !== null) {
37
+ return {
38
+ hardCapBytes: null,
39
+ policyLimitBytes,
40
+ effectiveBytes: policyLimitBytes,
41
+ source: "policy_limit",
42
+ };
43
+ }
44
+ return {
45
+ hardCapBytes: null,
46
+ policyLimitBytes: null,
47
+ effectiveBytes: null,
48
+ source: "none",
49
+ };
50
+ }
@@ -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) {
@@ -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;
@@ -11,20 +11,30 @@ export function brokerIdentityPath(agentDir: string): string {
11
11
  export async function getBrokerIdentityKey(agentDir: string): Promise<string> {
12
12
  const file = brokerIdentityPath(agentDir);
13
13
  await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
14
- try {
15
- const key = (await fs.readFile(file, "utf8")).trim();
16
- if (/^[0-9a-f]{64}$/i.test(key)) return key;
17
- throw new Error(`Invalid broker identity key at ${file}`);
18
- } catch (error) {
19
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
14
+ while (true) {
15
+ try {
16
+ const key = (await fs.readFile(file, "utf8")).trim();
17
+ if (/^[0-9a-f]{64}$/i.test(key)) return key;
18
+ throw new Error(`Invalid broker identity key at ${file}`);
19
+ } catch (error) {
20
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
21
+ }
22
+
20
23
  const key = randomBytes(32).toString("hex");
24
+ const temporary = path.join(path.dirname(file), `.broker.identity-${randomBytes(16).toString("hex")}`);
21
25
  try {
22
- await fs.writeFile(file, `${key}\n`, { encoding: "utf8", mode: MODE, flag: "wx" });
23
- await fs.chmod(file, MODE);
24
- return key;
25
- } catch (writeError) {
26
- if ((writeError as NodeJS.ErrnoException).code !== "EEXIST") throw writeError;
27
- return getBrokerIdentityKey(agentDir);
26
+ await fs.writeFile(temporary, `${key}\n`, { encoding: "utf8", mode: MODE, flag: "wx" });
27
+ await fs.chmod(temporary, MODE);
28
+ try {
29
+ // Link publishes a fully written key without exposing a partial target.
30
+ // Same-directory hard-link support is required (standard on Linux, macOS, and Windows NTFS).
31
+ await fs.link(temporary, file);
32
+ return key;
33
+ } catch (linkError) {
34
+ if ((linkError as NodeJS.ErrnoException).code !== "EEXIST") throw linkError;
35
+ }
36
+ } finally {
37
+ await fs.rm(temporary, { force: true });
28
38
  }
29
39
  }
30
40
  }
@@ -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
  };
@@ -2299,9 +2374,12 @@ async function launchInput(
2299
2374
  if (input.modelPreset !== undefined && (typeof input.modelPreset !== "string" || input.modelPreset.length === 0))
2300
2375
  return fail("invalid_input", "modelPreset must be a non-empty exact profile ID.");
2301
2376
  const modelPreset = text(input.modelPreset);
2377
+ if (input.mcpServers !== undefined && !isSessionLifecycleMcpServers(input.mcpServers))
2378
+ return fail("invalid_input", "mcpServers must contain unique valid stdio, HTTP, or SSE server definitions.");
2379
+ const mcpServers = input.mcpServers as SessionLifecycleMcpServer[] | undefined;
2302
2380
 
2303
2381
  if (operation === "session.create")
2304
- return { id: randomUUID(), cwd, root: resolvedRoot, modelPreset, worktree, worktreePlan };
2382
+ return { id: randomUUID(), cwd, root: resolvedRoot, modelPreset, mcpServers, worktree, worktreePlan };
2305
2383
  if (operation === "session.resume") {
2306
2384
  if (!requested) return fail("invalid_input", "sessionId is required to resume a saved session.");
2307
2385
  const savedPath = text(input.sessionPath);
@@ -2315,6 +2393,7 @@ async function launchInput(
2315
2393
  sessionPath: saved.path,
2316
2394
  sessionIdentity: saved.identity,
2317
2395
  modelPreset,
2396
+ mcpServers,
2318
2397
  worktree,
2319
2398
  worktreePlan,
2320
2399
  };
@@ -2336,6 +2415,7 @@ async function launchInput(
2336
2415
  sourceSessionIdentity: source.identity,
2337
2416
  sourceCwd,
2338
2417
  modelPreset,
2418
+ mcpServers,
2339
2419
  worktree,
2340
2420
  worktreePlan,
2341
2421
  };
@@ -2805,6 +2885,7 @@ async function executeLifecycleResponse(
2805
2885
  ...(launch.sessionPath ? { sessionPath: launch.sessionPath } : {}),
2806
2886
  ...(launch.sessionIdentity ? { sessionIdentity: launch.sessionIdentity } : {}),
2807
2887
  ...(launch.modelPreset ? { modelPreset: launch.modelPreset } : {}),
2888
+ ...(launch.mcpServers ? { mcpServers: launch.mcpServers } : {}),
2808
2889
  ...(launch.worktree ? { worktree: launch.worktree } : {}),
2809
2890
  };
2810
2891
  let child: ChildProcess | undefined;