@gajae-code/coding-agent 0.5.2 → 0.5.4
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 +23 -0
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +29 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/streaming-output.d.ts +12 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/web/search/providers/codex.d.ts +4 -4
- package/package.json +7 -7
- package/src/async/job-manager.ts +181 -43
- package/src/config/file-lock.ts +9 -1
- package/src/config/model-profile-activation.ts +71 -3
- package/src/config/model-profiles.ts +39 -14
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-runtime.ts +14 -13
- package/src/gjc-runtime/ralplan-runtime.ts +10 -0
- package/src/gjc-runtime/state-runtime.ts +73 -0
- package/src/gjc-runtime/tmux-gc.ts +86 -37
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-runtime.ts +8 -4
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/client.ts +64 -26
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/modes/bridge/bridge-mode.ts +21 -0
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/model-selector.ts +34 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +19 -0
- package/src/modes/controllers/selector-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +1 -1
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +27 -0
- package/src/session/agent-session.ts +271 -25
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/session-manager.ts +29 -13
- package/src/session/streaming-output.ts +95 -3
- package/src/setup/model-onboarding-guidance.ts +10 -3
- package/src/skill-state/active-state.ts +79 -7
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/registry.ts +17 -1
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +2 -6
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/web/search/providers/codex.ts +6 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isCompiledBinary, logger, Snowflake } from "@gajae-code/utils";
|
|
2
|
+
import { registerResourceOwner } from "../../runtime/process-lifecycle";
|
|
2
3
|
import type { ToolSession } from "../../tools";
|
|
3
4
|
import { ToolAbortError, ToolError } from "../../tools/tool-errors";
|
|
4
5
|
import { callSessionTool, type JsStatusEvent } from "./tool-bridge";
|
|
@@ -43,22 +44,66 @@ interface PendingRun {
|
|
|
43
44
|
settled: boolean;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
interface ReadyDeferred {
|
|
48
|
+
promise: Promise<JsSession>;
|
|
49
|
+
resolve(session: JsSession): void;
|
|
50
|
+
reject(error: Error): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface QueueDeferred {
|
|
54
|
+
promise: Promise<void>;
|
|
55
|
+
resolve(): void;
|
|
56
|
+
reject(error: Error): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
interface JsSession {
|
|
47
60
|
sessionKey: string;
|
|
48
|
-
worker
|
|
49
|
-
state: "alive" | "dead";
|
|
61
|
+
worker?: WorkerHandle;
|
|
62
|
+
state: "starting" | "alive" | "dead";
|
|
63
|
+
ownerId?: string;
|
|
50
64
|
pending: Map<string, PendingRun>;
|
|
51
65
|
queue: Promise<void>;
|
|
66
|
+
queuedWaiters: Set<(error: Error) => void>;
|
|
67
|
+
queueTail: QueueDeferred;
|
|
68
|
+
controllers: Set<AbortController>;
|
|
69
|
+
runSignal?: AbortSignal;
|
|
70
|
+
ready: ReadyDeferred;
|
|
71
|
+
unsubscribe?: () => void;
|
|
72
|
+
unregistered?: () => void;
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
const sessions = new Map<string, JsSession>();
|
|
76
|
+
const sessionWaiters = new Map<string, Set<(error: Error) => void>>();
|
|
77
|
+
let vmResourceCleanupRegistered = false;
|
|
78
|
+
|
|
79
|
+
function ensureVmResourceCleanup(): void {
|
|
80
|
+
if (vmResourceCleanupRegistered) return;
|
|
81
|
+
vmResourceCleanupRegistered = true;
|
|
82
|
+
registerResourceOwner("js-vm-contexts", async () => {
|
|
83
|
+
try {
|
|
84
|
+
await disposeAllVmContexts();
|
|
85
|
+
} finally {
|
|
86
|
+
vmResourceCleanupRegistered = false;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
55
90
|
const READY_TIMEOUT_MS_DEFAULT = 5_000;
|
|
56
91
|
|
|
92
|
+
function getSessionWaiters(sessionKey: string): Set<(error: Error) => void> {
|
|
93
|
+
let waiters = sessionWaiters.get(sessionKey);
|
|
94
|
+
if (!waiters) {
|
|
95
|
+
waiters = new Set();
|
|
96
|
+
sessionWaiters.set(sessionKey, waiters);
|
|
97
|
+
}
|
|
98
|
+
return waiters;
|
|
99
|
+
}
|
|
100
|
+
|
|
57
101
|
export async function executeInVmContext(options: {
|
|
58
102
|
sessionKey: string;
|
|
59
103
|
sessionId: string;
|
|
60
104
|
cwd: string;
|
|
61
105
|
session: ToolSession;
|
|
106
|
+
ownerId?: string;
|
|
62
107
|
reset?: boolean;
|
|
63
108
|
code: string;
|
|
64
109
|
filename: string;
|
|
@@ -68,40 +113,84 @@ export async function executeInVmContext(options: {
|
|
|
68
113
|
if (options.reset) {
|
|
69
114
|
await resetVmContext(options.sessionKey);
|
|
70
115
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
116
|
+
const waiters = getSessionWaiters(options.sessionKey);
|
|
117
|
+
const { promise: contextResetPromise, reject: rejectContextReset } = Promise.withResolvers<never>();
|
|
118
|
+
contextResetPromise.catch(() => undefined);
|
|
119
|
+
waiters.add(rejectContextReset);
|
|
120
|
+
const runPromise = (async (): Promise<{ value: unknown }> => {
|
|
121
|
+
const session = await acquireSession(
|
|
122
|
+
options.sessionKey,
|
|
123
|
+
{ cwd: options.cwd, sessionId: options.sessionId },
|
|
124
|
+
options.ownerId,
|
|
125
|
+
options.timeoutMs,
|
|
126
|
+
);
|
|
127
|
+
return await runQueued(session, () => runOnce(session, options));
|
|
128
|
+
})();
|
|
129
|
+
try {
|
|
130
|
+
return await Promise.race([runPromise, contextResetPromise]);
|
|
131
|
+
} finally {
|
|
132
|
+
waiters.delete(rejectContextReset);
|
|
133
|
+
}
|
|
77
134
|
}
|
|
78
135
|
|
|
79
136
|
export async function resetVmContext(sessionKey: string): Promise<void> {
|
|
80
137
|
const session = sessions.get(sessionKey);
|
|
81
138
|
if (!session) return;
|
|
82
139
|
sessions.delete(sessionKey);
|
|
140
|
+
const waiters = sessionWaiters.get(sessionKey);
|
|
141
|
+
if (waiters) for (const reject of [...waiters]) reject(new ToolError("JS context reset"));
|
|
83
142
|
await killSession(session, new ToolError("JS context reset"));
|
|
84
143
|
}
|
|
85
144
|
|
|
145
|
+
export async function disposeVmContextsByOwner(ownerId: string): Promise<void> {
|
|
146
|
+
const owned = [...sessions.entries()].filter(
|
|
147
|
+
([sessionKey, session]) =>
|
|
148
|
+
session.ownerId === ownerId || sessionKey === ownerId || sessionKey === `js:${ownerId}`,
|
|
149
|
+
);
|
|
150
|
+
for (const [sessionKey, session] of owned) {
|
|
151
|
+
if (sessions.get(sessionKey) === session) sessions.delete(sessionKey);
|
|
152
|
+
}
|
|
153
|
+
await Promise.all(owned.map(([, session]) => killSession(session, new ToolError("JS context disposed"))));
|
|
154
|
+
}
|
|
155
|
+
|
|
86
156
|
export async function disposeAllVmContexts(): Promise<void> {
|
|
87
157
|
const all = [...sessions.values()];
|
|
88
158
|
sessions.clear();
|
|
89
159
|
await Promise.all(all.map(session => killSession(session, new ToolError("JS context disposed"))));
|
|
90
160
|
}
|
|
91
161
|
|
|
162
|
+
export function liveVmContextCount(): number {
|
|
163
|
+
return [...sessions.values()].filter(session => session.state !== "dead").length;
|
|
164
|
+
}
|
|
165
|
+
|
|
92
166
|
async function runQueued<T>(session: JsSession, work: () => Promise<T>): Promise<T> {
|
|
167
|
+
if (session.state !== "alive") throw new ToolError("JS worker is not alive");
|
|
93
168
|
const previous = session.queue;
|
|
94
|
-
const { promise, resolve } = Promise.withResolvers<void>();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
169
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
170
|
+
const queueController = new AbortController();
|
|
171
|
+
const queueItem: QueueDeferred = { promise, resolve, reject };
|
|
172
|
+
const rejectWaiter = (error: Error): void => queueItem.reject(error);
|
|
173
|
+
session.queuedWaiters.add(rejectWaiter);
|
|
174
|
+
session.controllers.add(queueController);
|
|
175
|
+
session.queueTail = queueItem;
|
|
176
|
+
session.queue = (async () => {
|
|
177
|
+
await previous.catch(() => undefined);
|
|
178
|
+
await queueItem.promise;
|
|
179
|
+
})().catch(() => undefined);
|
|
101
180
|
try {
|
|
102
|
-
|
|
181
|
+
await Promise.race([previous.catch(() => undefined), queueItem.promise, abortPromise(queueController.signal)]);
|
|
182
|
+
if (session.runSignal?.aborted) throw reasonToError(session.runSignal.reason, "JS worker is not alive");
|
|
183
|
+
if (session.state !== "alive") throw new ToolError("JS worker is not alive");
|
|
184
|
+
session.queuedWaiters.delete(rejectWaiter);
|
|
185
|
+
return await Promise.race([
|
|
186
|
+
work(),
|
|
187
|
+
queueItem.promise.then(() => new Promise<never>(() => {})),
|
|
188
|
+
abortPromise(queueController.signal),
|
|
189
|
+
]);
|
|
103
190
|
} finally {
|
|
104
|
-
|
|
191
|
+
session.queuedWaiters.delete(rejectWaiter);
|
|
192
|
+
session.controllers.delete(queueController);
|
|
193
|
+
queueItem.resolve();
|
|
105
194
|
}
|
|
106
195
|
}
|
|
107
196
|
|
|
@@ -118,6 +207,7 @@ async function runOnce(
|
|
|
118
207
|
): Promise<{ value: unknown }> {
|
|
119
208
|
const runId = `r-${Snowflake.next()}`;
|
|
120
209
|
const { promise, resolve, reject } = Promise.withResolvers<{ value: unknown }>();
|
|
210
|
+
const sessionSignal = session.runSignal;
|
|
121
211
|
const pending: PendingRun = {
|
|
122
212
|
runId,
|
|
123
213
|
runState: options.runState,
|
|
@@ -145,13 +235,19 @@ async function runOnce(
|
|
|
145
235
|
}
|
|
146
236
|
|
|
147
237
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
238
|
+
if (sessionSignal?.aborted) throw reasonToError(sessionSignal.reason, "JS worker is not alive");
|
|
239
|
+
if (
|
|
240
|
+
!safeSend(session, {
|
|
241
|
+
type: "run",
|
|
242
|
+
runId,
|
|
243
|
+
code: options.code,
|
|
244
|
+
filename: options.filename,
|
|
245
|
+
snapshot: { cwd: options.cwd, sessionId: options.sessionId },
|
|
246
|
+
})
|
|
247
|
+
) {
|
|
248
|
+
settleRunWithError(session, pending, new ToolError("JS worker send failed"));
|
|
249
|
+
return await promise;
|
|
250
|
+
}
|
|
155
251
|
return await promise;
|
|
156
252
|
} finally {
|
|
157
253
|
options.runState.signal?.removeEventListener("abort", onAbort);
|
|
@@ -159,46 +255,76 @@ async function runOnce(
|
|
|
159
255
|
}
|
|
160
256
|
}
|
|
161
257
|
|
|
162
|
-
async function acquireSession(
|
|
258
|
+
async function acquireSession(
|
|
259
|
+
sessionKey: string,
|
|
260
|
+
snapshot: SessionSnapshot,
|
|
261
|
+
ownerId: string | undefined,
|
|
262
|
+
timeoutMs?: number,
|
|
263
|
+
): Promise<JsSession> {
|
|
264
|
+
ensureVmResourceCleanup();
|
|
163
265
|
const existing = sessions.get(sessionKey);
|
|
164
|
-
if (existing && existing.state
|
|
266
|
+
if (existing && existing.state !== "dead") return await existing.ready.promise;
|
|
165
267
|
|
|
166
|
-
const
|
|
268
|
+
const { promise: ready, resolve: resolveSession, reject: rejectSession } = Promise.withResolvers<JsSession>();
|
|
269
|
+
ready.catch(() => undefined);
|
|
167
270
|
const session: JsSession = {
|
|
168
271
|
sessionKey,
|
|
169
|
-
|
|
170
|
-
|
|
272
|
+
state: "starting",
|
|
273
|
+
ownerId,
|
|
171
274
|
pending: new Map(),
|
|
172
275
|
queue: Promise.resolve(),
|
|
276
|
+
queuedWaiters: new Set(),
|
|
277
|
+
queueTail: settledQueueDeferred(),
|
|
278
|
+
controllers: new Set(),
|
|
279
|
+
ready: { promise: ready, resolve: resolveSession, reject: rejectSession },
|
|
173
280
|
};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (!resolved && msg.type === "ready") {
|
|
178
|
-
resolved = true;
|
|
179
|
-
resolveReady();
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (!resolved && msg.type === "init-failed") {
|
|
183
|
-
resolved = true;
|
|
184
|
-
rejectReady(errorFromPayload(msg.error));
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
handleSessionMessage(session, msg);
|
|
188
|
-
});
|
|
281
|
+
sessions.set(sessionKey, session);
|
|
282
|
+
|
|
283
|
+
let worker: WorkerHandle | undefined;
|
|
189
284
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
285
|
+
worker = await spawnJsWorker();
|
|
286
|
+
const current = sessions.get(sessionKey);
|
|
287
|
+
if (current !== session) {
|
|
288
|
+
await worker.terminate().catch(() => undefined);
|
|
289
|
+
return (
|
|
290
|
+
(await current?.ready.promise) ?? Promise.reject(new ToolError("JS context replaced during initialization"))
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
session.worker = worker;
|
|
294
|
+
const { promise: readyPromise, resolve: resolveReady, reject: rejectReady } = Promise.withResolvers<void>();
|
|
295
|
+
let resolved = false;
|
|
296
|
+
session.unsubscribe = worker.onMessage(msg => {
|
|
297
|
+
if (!resolved && msg.type === "ready") {
|
|
298
|
+
resolved = true;
|
|
299
|
+
resolveReady();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!resolved && msg.type === "init-failed") {
|
|
303
|
+
resolved = true;
|
|
304
|
+
rejectReady(errorFromPayload(msg.error));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
handleSessionMessage(session, msg);
|
|
308
|
+
});
|
|
192
309
|
const readyTimeoutMs = Math.max(READY_TIMEOUT_MS_DEFAULT, timeoutMs ?? 0);
|
|
193
310
|
await raceWithTimeout(readyPromise, readyTimeoutMs, "Timed out initializing JS eval worker");
|
|
311
|
+
if (sessions.get(sessionKey) !== session) {
|
|
312
|
+
await killSession(session, new ToolError("JS context replaced during initialization"));
|
|
313
|
+
return (
|
|
314
|
+
(await sessions.get(sessionKey)?.ready.promise) ??
|
|
315
|
+
Promise.reject(new ToolError("JS context replaced during initialization"))
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
worker.send({ type: "init", snapshot });
|
|
319
|
+
session.state = "alive";
|
|
320
|
+
session.ready.resolve(session);
|
|
321
|
+
return session;
|
|
194
322
|
} catch (error) {
|
|
195
|
-
|
|
196
|
-
await
|
|
323
|
+
if (sessions.get(sessionKey) === session) sessions.delete(sessionKey);
|
|
324
|
+
await killSession(session, error instanceof Error ? error : new Error(String(error)));
|
|
325
|
+
session.ready.reject(error instanceof Error ? error : new Error(String(error)));
|
|
197
326
|
throw error;
|
|
198
327
|
}
|
|
199
|
-
worker.send({ type: "init", snapshot });
|
|
200
|
-
sessions.set(sessionKey, session);
|
|
201
|
-
return session;
|
|
202
328
|
}
|
|
203
329
|
|
|
204
330
|
function handleSessionMessage(session: JsSession, msg: WorkerOutbound): void {
|
|
@@ -276,22 +402,54 @@ async function killSessionFor(session: JsSession, error: Error): Promise<void> {
|
|
|
276
402
|
async function killSession(session: JsSession, error: Error): Promise<void> {
|
|
277
403
|
if (session.state === "dead") return;
|
|
278
404
|
session.state = "dead";
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
405
|
+
const unsubscribe = session.unsubscribe;
|
|
406
|
+
session.unsubscribe = undefined;
|
|
407
|
+
unsubscribe?.();
|
|
408
|
+
session.ready.reject(error);
|
|
409
|
+
session.queueTail.reject(error);
|
|
410
|
+
for (const controller of [...session.controllers]) controller.abort(error);
|
|
411
|
+
session.controllers.clear();
|
|
412
|
+
session.runSignal = AbortSignal.abort(error);
|
|
413
|
+
for (const reject of [...session.queuedWaiters]) reject(error);
|
|
414
|
+
session.queuedWaiters.clear();
|
|
415
|
+
for (const pending of [...session.pending.values()]) settleRunWithError(session, pending, error);
|
|
285
416
|
session.pending.clear();
|
|
286
|
-
|
|
417
|
+
void session.worker?.terminate().catch(() => undefined);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function settleRunWithError(session: JsSession, pending: PendingRun, error: Error): void {
|
|
421
|
+
if (pending.settled) return;
|
|
422
|
+
pending.settled = true;
|
|
423
|
+
for (const ctrl of pending.toolCalls.values()) ctrl.abort(error);
|
|
424
|
+
pending.toolCalls.clear();
|
|
425
|
+
session.pending.delete(pending.runId);
|
|
426
|
+
pending.reject(error);
|
|
287
427
|
}
|
|
288
428
|
|
|
289
|
-
function
|
|
290
|
-
|
|
429
|
+
function settledQueueDeferred(): QueueDeferred {
|
|
430
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
431
|
+
resolve();
|
|
432
|
+
return { promise, resolve, reject };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function abortPromise(signal: AbortSignal): Promise<never> {
|
|
436
|
+
if (signal.aborted) return Promise.reject(reasonToError(signal.reason, "JS worker is not alive"));
|
|
437
|
+
const { promise, reject } = Promise.withResolvers<never>();
|
|
438
|
+
const onAbort = (): void => reject(reasonToError(signal.reason, "JS worker is not alive"));
|
|
439
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
440
|
+
promise.finally(() => signal.removeEventListener("abort", onAbort)).catch(() => undefined);
|
|
441
|
+
return promise;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function safeSend(session: JsSession, msg: WorkerInbound): boolean {
|
|
445
|
+
if (session.state !== "alive") return false;
|
|
291
446
|
try {
|
|
292
|
-
session.worker
|
|
447
|
+
session.worker?.send(msg);
|
|
448
|
+
return true;
|
|
293
449
|
} catch (err) {
|
|
294
450
|
logger.debug("js worker send failed", { error: err instanceof Error ? err.message : String(err) });
|
|
451
|
+
void killSessionFor(session, err instanceof Error ? err : new Error(String(err)));
|
|
452
|
+
return false;
|
|
295
453
|
}
|
|
296
454
|
}
|
|
297
455
|
|
|
@@ -352,10 +510,15 @@ async function spawnJsWorker(): Promise<WorkerHandle> {
|
|
|
352
510
|
: new Worker(new URL("./worker-entry.ts", import.meta.url).href, { type: "module" });
|
|
353
511
|
return wrapBunWorker(worker);
|
|
354
512
|
} catch (err) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
513
|
+
if (process.env.GAJAE_CODE_JS_EVAL_INLINE_WORKER === "1") {
|
|
514
|
+
logger.warn("Bun Worker spawn failed; using test-only inline JS eval worker", {
|
|
515
|
+
error: err instanceof Error ? err.message : String(err),
|
|
516
|
+
});
|
|
517
|
+
return spawnInlineWorker();
|
|
518
|
+
}
|
|
519
|
+
throw new ToolError(
|
|
520
|
+
`JS eval worker is unavailable and inline fallback is disabled because it cannot interrupt synchronous user code: ${err instanceof Error ? err.message : String(err)}`,
|
|
521
|
+
);
|
|
359
522
|
}
|
|
360
523
|
}
|
|
361
524
|
|
package/src/eval/js/executor.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface JsExecutorOptions {
|
|
|
10
10
|
onChunk?: (chunk: string) => Promise<void> | void;
|
|
11
11
|
signal?: AbortSignal;
|
|
12
12
|
sessionId: string;
|
|
13
|
+
ownerId?: string;
|
|
13
14
|
reset?: boolean;
|
|
14
15
|
sessionFile?: string;
|
|
15
16
|
artifactPath?: string;
|
|
@@ -68,6 +69,7 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
|
|
|
68
69
|
await executeInVmContext({
|
|
69
70
|
sessionKey: options.sessionId,
|
|
70
71
|
sessionId: options.sessionId,
|
|
72
|
+
ownerId: options.ownerId,
|
|
71
73
|
cwd: options.cwd ?? options.session.cwd,
|
|
72
74
|
session: options.session,
|
|
73
75
|
reset: options.reset,
|
package/src/eval/js/index.ts
CHANGED
|
@@ -12,6 +12,11 @@ interface ActiveRun {
|
|
|
12
12
|
pendingTools: Map<string, PendingTool>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function rejectPendingTools(active: ActiveRun, error: Error): void {
|
|
16
|
+
for (const pending of active.pendingTools.values()) pending.reject(error);
|
|
17
|
+
active.pendingTools.clear();
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
function errorPayload(error: unknown): RunErrorPayload {
|
|
16
21
|
if (error instanceof Error) {
|
|
17
22
|
return {
|
|
@@ -99,7 +104,8 @@ export class WorkerCore {
|
|
|
99
104
|
async #runOne(runId: string, code: string, filename: string, snapshot: SessionSnapshot): Promise<void> {
|
|
100
105
|
const runtime = this.#ensureRuntime(snapshot);
|
|
101
106
|
runtime.setCwd(snapshot.cwd);
|
|
102
|
-
|
|
107
|
+
const active: ActiveRun = { runId, pendingTools: new Map() };
|
|
108
|
+
this.#active = active;
|
|
103
109
|
try {
|
|
104
110
|
const value = await runtime.run(code, filename);
|
|
105
111
|
runtime.displayValue(value);
|
|
@@ -107,7 +113,8 @@ export class WorkerCore {
|
|
|
107
113
|
} catch (error) {
|
|
108
114
|
this.#transport.send({ type: "result", runId, ok: false, error: errorPayload(error) });
|
|
109
115
|
} finally {
|
|
110
|
-
|
|
116
|
+
rejectPendingTools(active, new ToolError("JS run ended"));
|
|
117
|
+
if (this.#active === active) this.#active = null;
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
|
|
@@ -132,10 +139,7 @@ export class WorkerCore {
|
|
|
132
139
|
#close(): void {
|
|
133
140
|
const active = this.#active;
|
|
134
141
|
if (active) {
|
|
135
|
-
|
|
136
|
-
pending.reject(new ToolError("JS worker closed"));
|
|
137
|
-
}
|
|
138
|
-
active.pendingTools.clear();
|
|
142
|
+
rejectPendingTools(active, new ToolError("JS worker closed"));
|
|
139
143
|
}
|
|
140
144
|
this.#active = null;
|
|
141
145
|
this.#runtime = null;
|
package/src/eval/py/executor.ts
CHANGED
|
@@ -108,7 +108,18 @@ interface PythonSession {
|
|
|
108
108
|
queue: Promise<void>;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
interface InitializingPythonSession {
|
|
112
|
+
sessionId: string;
|
|
113
|
+
promise: Promise<PythonSession>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const sessions = new Map<string, PythonSession | InitializingPythonSession>();
|
|
117
|
+
|
|
118
|
+
function isInitializingSession(
|
|
119
|
+
session: PythonSession | InitializingPythonSession,
|
|
120
|
+
): session is InitializingPythonSession {
|
|
121
|
+
return "promise" in session;
|
|
122
|
+
}
|
|
112
123
|
|
|
113
124
|
// ---------------------------------------------------------------------------
|
|
114
125
|
// Cancellation plumbing
|
|
@@ -288,20 +299,48 @@ function attachOwner(session: PythonSession, sessionId: string, ownerId: string
|
|
|
288
299
|
async function acquireSession(sessionId: string, cwd: string, options: PythonExecutorOptions): Promise<PythonSession> {
|
|
289
300
|
const existing = sessions.get(sessionId);
|
|
290
301
|
if (existing) {
|
|
291
|
-
|
|
292
|
-
|
|
302
|
+
const session = isInitializingSession(existing)
|
|
303
|
+
? await waitForPromiseWithCancellation(existing.promise, options)
|
|
304
|
+
: existing;
|
|
305
|
+
attachOwner(session, sessionId, options.kernelOwnerId);
|
|
306
|
+
return session;
|
|
293
307
|
}
|
|
294
|
-
|
|
295
|
-
const
|
|
308
|
+
|
|
309
|
+
const initializing: InitializingPythonSession = {
|
|
296
310
|
sessionId,
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
311
|
+
promise: Promise.resolve().then(async () => {
|
|
312
|
+
const kernel = await startKernel(cwd, options);
|
|
313
|
+
const current = sessions.get(sessionId);
|
|
314
|
+
if (current !== initializing) {
|
|
315
|
+
await kernel.shutdown().catch(() => undefined);
|
|
316
|
+
const winner = current
|
|
317
|
+
? isInitializingSession(current)
|
|
318
|
+
? await waitForPromiseWithCancellation(current.promise, options)
|
|
319
|
+
: current
|
|
320
|
+
: undefined;
|
|
321
|
+
if (winner) return winner;
|
|
322
|
+
throw new PythonExecutionCancelledError(false);
|
|
323
|
+
}
|
|
324
|
+
const session: PythonSession = {
|
|
325
|
+
sessionId,
|
|
326
|
+
kernel,
|
|
327
|
+
ownerIds: new Set(),
|
|
328
|
+
hasFallbackOwner: false,
|
|
329
|
+
queue: Promise.resolve(),
|
|
330
|
+
};
|
|
331
|
+
sessions.set(sessionId, session);
|
|
332
|
+
return session;
|
|
333
|
+
}),
|
|
301
334
|
};
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
335
|
+
sessions.set(sessionId, initializing);
|
|
336
|
+
try {
|
|
337
|
+
const session = await waitForPromiseWithCancellation(initializing.promise, options);
|
|
338
|
+
attachOwner(session, sessionId, options.kernelOwnerId);
|
|
339
|
+
return session;
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (sessions.get(sessionId) === initializing) sessions.delete(sessionId);
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
305
344
|
}
|
|
306
345
|
|
|
307
346
|
async function replaceSessionKernel(
|
|
@@ -330,7 +369,8 @@ async function resetSession(sessionId: string): Promise<void> {
|
|
|
330
369
|
const existing = sessions.get(sessionId);
|
|
331
370
|
if (!existing) return;
|
|
332
371
|
sessions.delete(sessionId);
|
|
333
|
-
await existing.
|
|
372
|
+
const session = isInitializingSession(existing) ? await existing.promise.catch(() => undefined) : existing;
|
|
373
|
+
await session?.kernel.shutdown().catch(() => undefined);
|
|
334
374
|
}
|
|
335
375
|
|
|
336
376
|
async function runQueued<T>(
|
|
@@ -363,11 +403,20 @@ export async function disposeAllKernelSessions(): Promise<void> {
|
|
|
363
403
|
for (const [id, session] of all) {
|
|
364
404
|
if (sessions.get(id) === session) sessions.delete(id);
|
|
365
405
|
}
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
406
|
+
const resolved = await Promise.all(
|
|
407
|
+
all.map(
|
|
408
|
+
async ([id, entry]) =>
|
|
409
|
+
[id, isInitializingSession(entry) ? await entry.promise.catch(() => undefined) : entry] as const,
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
const shutdownTargets = resolved.filter(
|
|
413
|
+
(entry): entry is readonly [string, PythonSession] => entry[1] !== undefined,
|
|
414
|
+
);
|
|
415
|
+
const results = await Promise.allSettled(shutdownTargets.map(([, session]) => session.kernel.shutdown()));
|
|
416
|
+
for (let i = 0; i < shutdownTargets.length; i += 1) {
|
|
417
|
+
const [id, session] = shutdownTargets[i];
|
|
369
418
|
const result = results[i];
|
|
370
|
-
if (result.status === "fulfilled" && result.value
|
|
419
|
+
if (result.status === "fulfilled" && result.value.confirmed) continue;
|
|
371
420
|
const reason = result.status === "rejected" ? result.reason : "not confirmed";
|
|
372
421
|
logger.warn("Python kernel shutdown not confirmed", { sessionId: id, reason });
|
|
373
422
|
if (!sessions.has(id)) sessions.set(id, session);
|
|
@@ -377,7 +426,7 @@ export async function disposeAllKernelSessions(): Promise<void> {
|
|
|
377
426
|
export async function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
378
427
|
const toShutdown: PythonSession[] = [];
|
|
379
428
|
for (const session of [...sessions.values()]) {
|
|
380
|
-
if (!session.ownerIds.has(ownerId)) continue;
|
|
429
|
+
if (isInitializingSession(session) || !session.ownerIds.has(ownerId)) continue;
|
|
381
430
|
if (session.ownerIds.size === 1) {
|
|
382
431
|
toShutdown.push(session);
|
|
383
432
|
continue;
|
|
@@ -391,7 +440,7 @@ export async function disposeKernelSessionsByOwner(ownerId: string): Promise<voi
|
|
|
391
440
|
for (let i = 0; i < toShutdown.length; i += 1) {
|
|
392
441
|
const session = toShutdown[i];
|
|
393
442
|
const result = results[i];
|
|
394
|
-
if (result.status === "fulfilled" && result.value
|
|
443
|
+
if (result.status === "fulfilled" && result.value.confirmed) {
|
|
395
444
|
session.ownerIds.delete(ownerId);
|
|
396
445
|
continue;
|
|
397
446
|
}
|