@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.
- package/CHANGELOG.md +56 -1
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +57 -2
- package/dist/types/config/settings.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +14 -9
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +11 -2
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
- package/dist/types/sdk/host/control/operations.d.ts +1 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +11 -11
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
- package/dist/types/session/session-manager.d.ts +34 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +39 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/resource-gc.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +1 -0
- package/src/cli.ts +48 -2
- package/src/commands/sdk.ts +91 -7
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +31 -7
- package/src/config/model-registry.ts +4 -2
- package/src/config/settings-schema.ts +61 -2
- package/src/config/settings.ts +8 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +14 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
- package/src/gjc-runtime/deep-interview-draft.ts +2 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
- package/src/gjc-runtime/deep-interview-state.ts +25 -3
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/ralplan-runtime.ts +252 -1
- package/src/gjc-runtime/state-runtime.ts +48 -11
- package/src/gjc-runtime/state-writer.ts +1 -1
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/modes/acp/acp-agent.ts +554 -60
- package/src/modes/acp/acp-event-mapper.ts +93 -6
- package/src/modes/components/model-selector.ts +15 -1
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive-mode.ts +65 -26
- package/src/modes/runtime-init.ts +1 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +82 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +222 -27
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +1191 -164
- package/src/sdk/host/control/dispatch.ts +2 -0
- package/src/sdk/host/control/operations.ts +1 -0
- package/src/sdk/host/host.ts +2 -2
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +1 -0
- package/src/sdk/session.ts +24 -1
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +290 -132
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +173 -127
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/session-manager.ts +608 -25
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +152 -42
- package/src/task/index.ts +2 -0
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +25 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/resource-gc.ts +496 -120
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- 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(
|
|
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) {
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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(
|
|
23
|
-
await fs.chmod(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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;
|