@gajae-code/coding-agent 0.13.0 → 0.13.2
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 +34 -0
- package/dist/types/config/file-lock.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +17 -5
- package/dist/types/config/settings.d.ts +10 -1
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/eval/py/kernel.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/runtime/process-lifecycle.d.ts +12 -31
- package/dist/types/sdk/broker/identity.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
- package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
- package/dist/types/sdk/host/query/cursor.d.ts +4 -0
- package/dist/types/sdk/host/query/handlers.d.ts +6 -0
- package/dist/types/sdk/host/session-runtime.d.ts +7 -6
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/turn-result.d.ts +37 -0
- package/dist/types/session/agent-session.d.ts +8 -11
- package/dist/types/session/blob-store.d.ts +6 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +13 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
- package/dist/types/task/executor.d.ts +4 -7
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/utils/edit-mode.d.ts +69 -0
- package/package.json +8 -8
- package/scripts/generate-hotkeys-docs.ts +8 -6
- package/src/commands/sdk.ts +23 -2
- package/src/config/file-lock.ts +25 -5
- package/src/config/keybindings.ts +4 -2
- package/src/config/model-registry.ts +20 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +29 -22
- package/src/config/settings.ts +42 -10
- package/src/coordinator-mcp/server.ts +87 -106
- package/src/edit/index.ts +5 -19
- package/src/edit/renderer.ts +3 -2
- package/src/edit/streaming.ts +28 -15
- package/src/eval/py/executor.ts +108 -10
- package/src/eval/py/kernel.ts +86 -12
- package/src/eval/py/runtime.ts +200 -16
- package/src/exec/bash-executor.ts +36 -37
- package/src/gjc-runtime/gc-runtime.ts +25 -3
- package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
- package/src/gjc-runtime/session-state-sidecar.ts +110 -4
- package/src/gjc-runtime/tmux-sessions.ts +34 -5
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/modes/acp/acp-agent.ts +10 -2
- package/src/modes/components/model-selector.ts +28 -8
- package/src/modes/controllers/plan-mode-controller.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/modes/interactive-mode.ts +4 -0
- package/src/runtime/process-lifecycle.ts +126 -27
- package/src/runtime-mcp/transports/stdio.ts +11 -6
- package/src/sdk/acp/adapter.ts +18 -10
- package/src/sdk/broker/broker.ts +55 -4
- package/src/sdk/broker/ensure.ts +10 -7
- package/src/sdk/broker/identity.ts +10 -0
- package/src/sdk/broker/lifecycle-ledger.ts +56 -6
- package/src/sdk/broker/lifecycle.ts +53 -10
- package/src/sdk/broker/process-incarnation.ts +6 -1
- package/src/sdk/broker/session-index.ts +28 -11
- package/src/sdk/broker/startup-failure.ts +101 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
- package/src/sdk/bus/conversation-store.ts +171 -50
- package/src/sdk/bus/index.ts +101 -47
- package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
- package/src/sdk/bus/notification-service.ts +3 -3
- package/src/sdk/bus/reconciliation-store.ts +16 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -2
- package/src/sdk/bus/telegram-daemon.ts +151 -41
- package/src/sdk/bus/topic-registry.ts +38 -9
- package/src/sdk/cli/session-cli.ts +20 -7
- package/src/sdk/client/discovery.ts +23 -2
- package/src/sdk/host/query/cursor.ts +4 -0
- package/src/sdk/host/query/handlers.ts +99 -100
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +98 -1
- package/src/sdk/host/session-runtime.ts +57 -15
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
- package/src/sdk/host/websocket-transport.ts +54 -30
- package/src/sdk/index.ts +1 -0
- package/src/sdk/mcp/server.ts +19 -6
- package/src/sdk/protocol/operation-inventory.generated.json +1 -1
- package/src/sdk/protocol/operation-registry.ts +8 -12
- package/src/sdk/session.ts +5 -14
- package/src/sdk/turn-result.ts +53 -0
- package/src/session/agent-session.ts +112 -69
- package/src/session/blob-store.ts +91 -21
- package/src/session/internal/managed-session-storage.ts +136 -21
- package/src/session/session-manager.ts +408 -81
- package/src/session/session-storage.ts +68 -10
- package/src/slash-commands/builtin-registry.ts +43 -5
- package/src/slash-commands/helpers/fast-status-report.ts +24 -8
- package/src/task/executor.ts +25 -8
- package/src/task/index.ts +5 -4
- package/src/tools/bash.ts +11 -9
- package/src/tools/index.ts +1 -1
- package/src/tools/read.ts +12 -11
- package/src/tools/search.ts +7 -7
- package/src/utils/edit-mode.ts +180 -9
- package/src/web/search/providers/insane.ts +55 -1
package/src/edit/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import { prompt } from "@gajae-code/utils";
|
|
3
3
|
import type * as z from "zod/v4";
|
|
4
4
|
import {
|
|
5
5
|
executeHashlineSingle,
|
|
@@ -22,7 +22,7 @@ import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
|
22
22
|
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
23
23
|
import type { ToolSession } from "../tools";
|
|
24
24
|
import { VimTool, vimSchema } from "../tools/vim";
|
|
25
|
-
import { type EditMode,
|
|
25
|
+
import { type EditMode, resolveEditMode, resolveForcedEnvEditMode } from "../utils/edit-mode";
|
|
26
26
|
import type { VimToolDetails } from "../vim/types";
|
|
27
27
|
import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
|
|
28
28
|
import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
@@ -132,19 +132,6 @@ type EditModeDefinition = {
|
|
|
132
132
|
) => Promise<AgentToolResult<EditToolResultDetails, TInput>>;
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
-
function resolveConfiguredEditMode(rawEditMode: string): EditMode | undefined {
|
|
136
|
-
if (!rawEditMode || rawEditMode === "auto") {
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const editMode = normalizeEditMode(rawEditMode);
|
|
141
|
-
if (!editMode) {
|
|
142
|
-
throw new Error(`Invalid PI_EDIT_VARIANT: ${rawEditMode}`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return editMode;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
135
|
function resolveAllowFuzzy(session: ToolSession, rawValue: string): boolean {
|
|
149
136
|
switch (rawValue) {
|
|
150
137
|
case "true":
|
|
@@ -347,15 +334,15 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
347
334
|
readonly #allowFuzzy: boolean;
|
|
348
335
|
readonly #fuzzyThreshold: number;
|
|
349
336
|
readonly #writethrough: WritethroughCallback;
|
|
350
|
-
readonly #editMode?: EditMode;
|
|
351
337
|
readonly #vimTool: VimTool;
|
|
352
338
|
readonly #pendingDeferredFetches = new Map<string, AbortController>();
|
|
353
339
|
|
|
354
340
|
constructor(private readonly session: ToolSession) {
|
|
355
341
|
const { PI_EDIT_FUZZY: editFuzzy = "auto", PI_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto" } = Bun.env;
|
|
356
|
-
const envEditVariant = $pickenv("GJC_EDIT_VARIANT", "PI_EDIT_VARIANT") ?? "auto";
|
|
357
342
|
|
|
358
|
-
|
|
343
|
+
// Fail fast on an invalid forced env variant; the shared resolver owns
|
|
344
|
+
// env precedence at resolution time (no separate cache here).
|
|
345
|
+
resolveForcedEnvEditMode();
|
|
359
346
|
this.#allowFuzzy = resolveAllowFuzzy(session, editFuzzy);
|
|
360
347
|
this.#fuzzyThreshold = resolveFuzzyThreshold(session, editFuzzyThreshold);
|
|
361
348
|
this.#writethrough = createEditWritethrough(session);
|
|
@@ -363,7 +350,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
363
350
|
}
|
|
364
351
|
|
|
365
352
|
get mode(): EditMode {
|
|
366
|
-
if (this.#editMode) return this.#editMode;
|
|
367
353
|
return resolveEditMode(this.session);
|
|
368
354
|
}
|
|
369
355
|
|
package/src/edit/renderer.ts
CHANGED
|
@@ -356,8 +356,8 @@ export const editToolRenderer = {
|
|
|
356
356
|
});
|
|
357
357
|
const firstEdit = Array.isArray(editArgs.edits) && editArgs.edits.length > 0 ? editArgs.edits[0] : undefined;
|
|
358
358
|
const rawPath = inventory.paths[0] ?? "";
|
|
359
|
-
const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move;
|
|
360
|
-
const op = editArgs.op || firstEdit?.op || inventory.
|
|
359
|
+
const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move || inventory.rename;
|
|
360
|
+
const op = editArgs.op || firstEdit?.op || inventory.op;
|
|
361
361
|
const { description } = formatEditDescription(rawPath, uiTheme, options.expanded ? { rename } : undefined);
|
|
362
362
|
const spinner =
|
|
363
363
|
options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
|
|
@@ -416,6 +416,7 @@ function resolveCompletedEditIdentity(
|
|
|
416
416
|
const firstEdit = args?.edits?.[0];
|
|
417
417
|
const inventory = getEditRequestTargetInventory(args, editMode, { isPartial });
|
|
418
418
|
const detailsPath = details && "path" in details ? details.path : undefined;
|
|
419
|
+
|
|
419
420
|
return {
|
|
420
421
|
path: detailsPath ?? args?.file_path ?? args?.path ?? firstEdit?.path ?? inventory.paths[0] ?? "",
|
|
421
422
|
op: details?.op ?? args?.op ?? firstEdit?.op,
|
package/src/edit/streaming.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { replaceTabs, truncateToWidth } from "../tools/render-utils";
|
|
|
31
31
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
32
32
|
import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
|
|
33
33
|
import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
|
|
34
|
-
import { computePatchDiff, type
|
|
34
|
+
import { computePatchDiff, type PatchEditEntry } from "./modes/patch";
|
|
35
35
|
import type { ReplaceEditEntry } from "./modes/replace";
|
|
36
36
|
|
|
37
37
|
export interface PerFileDiffPreview {
|
|
@@ -76,11 +76,11 @@ export interface EditStreamingStrategy<Args = unknown> {
|
|
|
76
76
|
}
|
|
77
77
|
export interface EditRequestTargetInventory {
|
|
78
78
|
paths: string[];
|
|
79
|
-
/** Operation of the first target (Create/Delete) when derivable from the
|
|
80
|
-
* request payload (apply_patch/hashline free-form envelopes carry op on the
|
|
81
|
-
* parsed entry, not on the top-level args). */
|
|
82
|
-
firstOp?: Operation;
|
|
83
79
|
parseError?: string;
|
|
80
|
+
/** First envelope entry operation, for free-form modes that carry no structured `edits`. */
|
|
81
|
+
op?: PatchEditEntry["op"];
|
|
82
|
+
/** First envelope entry rename target, for free-form modes that carry no structured `edits`. */
|
|
83
|
+
rename?: string;
|
|
84
84
|
}
|
|
85
85
|
const MISSING_APPLY_PATCH_END_ERROR = `The last line of the patch must be '${END_PATCH_MARKER}'`;
|
|
86
86
|
|
|
@@ -135,6 +135,19 @@ function getHashlineTargetPaths(input: string): string[] {
|
|
|
135
135
|
return paths.filter(Boolean);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* `apply_patch` carries operation metadata inside the envelope rather than in structured `edits`,
|
|
140
|
+
* so the collapsed card has to read op/rename off the first parsed entry to stay accurate while live.
|
|
141
|
+
*/
|
|
142
|
+
function applyPatchEntryInventory(entries: readonly ApplyPatchEntry[]): EditRequestTargetInventory {
|
|
143
|
+
const first = entries[0];
|
|
144
|
+
return {
|
|
145
|
+
paths: orderedDistinctPaths(entries.map(entry => entry.path)),
|
|
146
|
+
op: first?.op,
|
|
147
|
+
rename: first?.rename,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
export function getEditRequestTargetInventory(
|
|
139
152
|
args: unknown,
|
|
140
153
|
editMode: EditMode | undefined,
|
|
@@ -154,22 +167,22 @@ export function getEditRequestTargetInventory(
|
|
|
154
167
|
if (editMode === "hashline") {
|
|
155
168
|
const input = typeof values.input === "string" ? values.input : "";
|
|
156
169
|
const headerPaths = getHashlineTargetPaths(input);
|
|
157
|
-
|
|
170
|
+
// `path` is only a fallback for headerless input; explicit §PATH headers are the full target set.
|
|
171
|
+
if (headerPaths.length > 0) return { paths: orderedDistinctPaths(headerPaths) };
|
|
172
|
+
return { paths: orderedDistinctPaths([topLevelPath]) };
|
|
158
173
|
}
|
|
159
174
|
|
|
160
|
-
|
|
175
|
+
// Legacy call sites render the shared edit card without threading `renderContext.editMode`;
|
|
176
|
+
// a free-form `input` string is only ever an apply_patch envelope there.
|
|
177
|
+
if (editMode === "apply_patch" || (editMode === undefined && typeof values.input === "string")) {
|
|
161
178
|
const input = typeof values.input === "string" ? values.input : "";
|
|
162
179
|
try {
|
|
163
|
-
|
|
164
|
-
return { paths: orderedDistinctPaths(entries.map(entry => entry.path)), firstOp: entries[0]?.op };
|
|
180
|
+
return applyPatchEntryInventory(expandApplyPatchToEntries({ input }));
|
|
165
181
|
} catch (err) {
|
|
166
182
|
const parseError = err instanceof Error ? err.message : String(err);
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return { paths, firstOp: previewEntries[0]?.op };
|
|
171
|
-
}
|
|
172
|
-
return { paths, firstOp: previewEntries[0]?.op, parseError };
|
|
183
|
+
const inventory = applyPatchEntryInventory(expandApplyPatchToPreviewEntries({ input }));
|
|
184
|
+
if (options.isPartial && parseError === MISSING_APPLY_PATCH_END_ERROR) return inventory;
|
|
185
|
+
return { ...inventory, parseError };
|
|
173
186
|
}
|
|
174
187
|
}
|
|
175
188
|
|
package/src/eval/py/executor.ts
CHANGED
|
@@ -116,6 +116,7 @@ interface PythonSession {
|
|
|
116
116
|
interface InitializingPythonSession {
|
|
117
117
|
sessionId: string;
|
|
118
118
|
promise: Promise<PythonSession>;
|
|
119
|
+
cancelled?: PythonExecutionCancelledError;
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
let pythonResourceCleanupRegistered = false;
|
|
@@ -126,6 +127,14 @@ function ensurePythonResourceCleanup(): void {
|
|
|
126
127
|
registerResourceOwner("python-kernel-sessions", disposeAllKernelSessions);
|
|
127
128
|
}
|
|
128
129
|
const sessions = new Map<string, PythonSession | InitializingPythonSession>();
|
|
130
|
+
const retiringKernels = new Set<PythonKernel>();
|
|
131
|
+
|
|
132
|
+
async function shutdownOrRetainKernel(kernel: PythonKernel): Promise<void> {
|
|
133
|
+
const result = await kernel.shutdown().catch(() => undefined);
|
|
134
|
+
if (result?.confirmed) return;
|
|
135
|
+
retiringKernels.add(kernel);
|
|
136
|
+
logger.warn("Python kernel shutdown not confirmed", { kernelId: kernel.id });
|
|
137
|
+
}
|
|
129
138
|
|
|
130
139
|
function isInitializingSession(
|
|
131
140
|
session: PythonSession | InitializingPythonSession,
|
|
@@ -312,9 +321,23 @@ function attachOwner(session: PythonSession, sessionId: string, ownerId: string
|
|
|
312
321
|
async function acquireSession(sessionId: string, cwd: string, options: PythonExecutorOptions): Promise<PythonSession> {
|
|
313
322
|
const existing = sessions.get(sessionId);
|
|
314
323
|
if (existing) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
let session: PythonSession;
|
|
325
|
+
if (isInitializingSession(existing)) {
|
|
326
|
+
try {
|
|
327
|
+
session = await waitForPromiseWithCancellation(existing.promise, options);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
const remainingMs = getRemainingTimeoutMs(options.deadlineMs);
|
|
330
|
+
if (
|
|
331
|
+
isCancellationError(error) &&
|
|
332
|
+
!options.signal?.aborted &&
|
|
333
|
+
(remainingMs === undefined || remainingMs > 0)
|
|
334
|
+
)
|
|
335
|
+
return await acquireSession(sessionId, cwd, options);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
session = existing;
|
|
340
|
+
}
|
|
318
341
|
attachOwner(session, sessionId, options.kernelOwnerId);
|
|
319
342
|
ensurePythonResourceCleanup();
|
|
320
343
|
return session;
|
|
@@ -324,9 +347,13 @@ async function acquireSession(sessionId: string, cwd: string, options: PythonExe
|
|
|
324
347
|
sessionId,
|
|
325
348
|
promise: Promise.resolve().then(async () => {
|
|
326
349
|
const kernel = await startKernel(cwd, options);
|
|
350
|
+
if (initializing.cancelled) {
|
|
351
|
+
await shutdownOrRetainKernel(kernel);
|
|
352
|
+
throw initializing.cancelled;
|
|
353
|
+
}
|
|
327
354
|
const current = sessions.get(sessionId);
|
|
328
355
|
if (current !== initializing) {
|
|
329
|
-
await kernel
|
|
356
|
+
await shutdownOrRetainKernel(kernel);
|
|
330
357
|
const winner = current
|
|
331
358
|
? isInitializingSession(current)
|
|
332
359
|
? await waitForPromiseWithCancellation(current.promise, options)
|
|
@@ -348,13 +375,49 @@ async function acquireSession(sessionId: string, cwd: string, options: PythonExe
|
|
|
348
375
|
}),
|
|
349
376
|
};
|
|
350
377
|
sessions.set(sessionId, initializing);
|
|
378
|
+
ensurePythonResourceCleanup();
|
|
379
|
+
let cancellationTimer: NodeJS.Timeout | undefined;
|
|
380
|
+
const retireCancelledInitialization = (timedOut: boolean): void => {
|
|
381
|
+
if (initializing.cancelled) return;
|
|
382
|
+
initializing.cancelled = new PythonExecutionCancelledError(timedOut);
|
|
383
|
+
if (sessions.get(sessionId) === initializing) sessions.delete(sessionId);
|
|
384
|
+
};
|
|
385
|
+
const onAbort = (): void => {
|
|
386
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
387
|
+
retireCancelledInitialization(isTimedOutCancellation(options.signal?.reason, options.signal));
|
|
388
|
+
};
|
|
389
|
+
if (options.signal?.aborted) {
|
|
390
|
+
onAbort();
|
|
391
|
+
} else if (options.signal) {
|
|
392
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
393
|
+
}
|
|
394
|
+
const remainingMs = getRemainingTimeoutMs(options.deadlineMs);
|
|
395
|
+
if (remainingMs !== undefined) {
|
|
396
|
+
if (remainingMs <= 0) {
|
|
397
|
+
retireCancelledInitialization(true);
|
|
398
|
+
} else {
|
|
399
|
+
cancellationTimer = setTimeout(() => retireCancelledInitialization(true), remainingMs);
|
|
400
|
+
cancellationTimer.unref();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
void initializing.promise
|
|
404
|
+
.finally(() => {
|
|
405
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
406
|
+
if (cancellationTimer) clearTimeout(cancellationTimer);
|
|
407
|
+
})
|
|
408
|
+
.catch(() => undefined);
|
|
351
409
|
try {
|
|
352
410
|
const session = await waitForPromiseWithCancellation(initializing.promise, options);
|
|
353
411
|
attachOwner(session, sessionId, options.kernelOwnerId);
|
|
354
412
|
ensurePythonResourceCleanup();
|
|
355
413
|
return session;
|
|
356
414
|
} catch (err) {
|
|
357
|
-
if (
|
|
415
|
+
if (isCancellationError(err)) {
|
|
416
|
+
retireCancelledInitialization(isTimedOutCancellation(err, options.signal));
|
|
417
|
+
await initializing.promise.catch(() => undefined);
|
|
418
|
+
} else if (sessions.get(sessionId) === initializing) {
|
|
419
|
+
sessions.delete(sessionId);
|
|
420
|
+
}
|
|
358
421
|
throw err;
|
|
359
422
|
}
|
|
360
423
|
}
|
|
@@ -429,6 +492,8 @@ async function runQueued<T>(
|
|
|
429
492
|
|
|
430
493
|
export async function disposeAllKernelSessions(): Promise<void> {
|
|
431
494
|
const all = [...sessions.entries()];
|
|
495
|
+
const retiring = [...retiringKernels];
|
|
496
|
+
retiringKernels.clear();
|
|
432
497
|
for (const [id, session] of all) {
|
|
433
498
|
if (sessions.get(id) === session) sessions.delete(id);
|
|
434
499
|
}
|
|
@@ -450,6 +515,16 @@ export async function disposeAllKernelSessions(): Promise<void> {
|
|
|
450
515
|
logger.warn("Python kernel shutdown not confirmed", { sessionId: id, reason });
|
|
451
516
|
if (!sessions.has(id)) sessions.set(id, session);
|
|
452
517
|
}
|
|
518
|
+
const retiringResults = await Promise.allSettled(retiring.map(kernel => kernel.shutdown()));
|
|
519
|
+
for (let i = 0; i < retiring.length; i += 1) {
|
|
520
|
+
const result = retiringResults[i];
|
|
521
|
+
if (result.status === "fulfilled" && result.value.confirmed) continue;
|
|
522
|
+
retiringKernels.add(retiring[i]);
|
|
523
|
+
logger.warn("Python kernel shutdown not confirmed", {
|
|
524
|
+
kernelId: retiring[i].id,
|
|
525
|
+
reason: result.status === "rejected" ? result.reason : "not confirmed",
|
|
526
|
+
});
|
|
527
|
+
}
|
|
453
528
|
}
|
|
454
529
|
|
|
455
530
|
export async function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
@@ -596,10 +671,10 @@ async function executeWithKernel(
|
|
|
596
671
|
}
|
|
597
672
|
|
|
598
673
|
async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions): Promise<void> {
|
|
599
|
-
const availability = await
|
|
600
|
-
|
|
601
|
-
options,
|
|
602
|
-
);
|
|
674
|
+
const availability = await checkPythonKernelAvailability(cwd, options.runtimeOptions, {
|
|
675
|
+
signal: options.signal,
|
|
676
|
+
deadlineMs: options.deadlineMs,
|
|
677
|
+
});
|
|
603
678
|
if (!availability.ok) {
|
|
604
679
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
605
680
|
}
|
|
@@ -683,11 +758,28 @@ export async function executePythonWithKernel(
|
|
|
683
758
|
export async function executePython(code: string, options?: PythonExecutorOptions): Promise<PythonResult> {
|
|
684
759
|
const cwd = options?.cwd ?? getProjectDir();
|
|
685
760
|
const deadlineMs = getExecutionDeadlineMs(options);
|
|
761
|
+
const deadlineController = deadlineMs === undefined ? undefined : new AbortController();
|
|
762
|
+
const combinedController = deadlineController && options?.signal ? new AbortController() : undefined;
|
|
763
|
+
const forwardAbort = (): void => combinedController?.abort(options?.signal?.reason);
|
|
764
|
+
if (combinedController) {
|
|
765
|
+
if (options?.signal?.aborted) forwardAbort();
|
|
766
|
+
else options?.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
767
|
+
}
|
|
768
|
+
const forwardDeadlineAbort = (): void => combinedController?.abort(deadlineController?.signal.reason);
|
|
769
|
+
if (combinedController && deadlineController)
|
|
770
|
+
deadlineController.signal.addEventListener("abort", forwardDeadlineAbort, { once: true });
|
|
771
|
+
const signal = combinedController?.signal ?? deadlineController?.signal ?? options?.signal;
|
|
772
|
+
const remainingMs = getRemainingTimeoutMs(deadlineMs);
|
|
773
|
+
const deadlineTimer =
|
|
774
|
+
deadlineController && remainingMs !== undefined
|
|
775
|
+
? setTimeout(() => deadlineController.abort(new PythonExecutionCancelledError(true)), Math.max(0, remainingMs))
|
|
776
|
+
: undefined;
|
|
777
|
+
deadlineTimer?.unref();
|
|
686
778
|
const executionOptions: PythonExecutorOptions = {
|
|
687
779
|
...(options ?? {}),
|
|
780
|
+
signal,
|
|
688
781
|
deadlineMs,
|
|
689
782
|
};
|
|
690
|
-
|
|
691
783
|
try {
|
|
692
784
|
requireRemainingTimeoutMs(deadlineMs);
|
|
693
785
|
if (executionOptions.signal?.aborted) {
|
|
@@ -708,5 +800,11 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
708
800
|
return createCancelledPythonResult(isTimedOutCancellation(err, executionOptions.signal));
|
|
709
801
|
}
|
|
710
802
|
throw err;
|
|
803
|
+
} finally {
|
|
804
|
+
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
805
|
+
if (combinedController) {
|
|
806
|
+
options?.signal?.removeEventListener("abort", forwardAbort);
|
|
807
|
+
deadlineController?.signal.removeEventListener("abort", forwardDeadlineAbort);
|
|
808
|
+
}
|
|
711
809
|
}
|
|
712
810
|
}
|
package/src/eval/py/kernel.ts
CHANGED
|
@@ -10,13 +10,17 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { $env, isBunTestRuntime, logger, Snowflake } from "@gajae-code/utils";
|
|
12
12
|
import type { Subprocess } from "bun";
|
|
13
|
-
import { $ } from "bun";
|
|
14
13
|
import { Settings } from "../../config/settings";
|
|
15
14
|
import { type KernelDisplayOutput, renderKernelDisplay } from "./display";
|
|
16
15
|
import { resolvePythonIpcTrace, resolvePythonSkipCheck } from "./env";
|
|
17
16
|
import { PYTHON_PRELUDE } from "./prelude";
|
|
18
17
|
import { ensureRunnerScript } from "./runner-artifact";
|
|
19
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
ensurePythonRuntime,
|
|
20
|
+
filterEnv,
|
|
21
|
+
type PythonRuntimeLifecycleOptions,
|
|
22
|
+
type PythonRuntimeOptions,
|
|
23
|
+
} from "./runtime";
|
|
20
24
|
|
|
21
25
|
export type { KernelDisplayOutput, PythonStatusEvent } from "./display";
|
|
22
26
|
export { renderKernelDisplay } from "./display";
|
|
@@ -103,32 +107,98 @@ function throwIfAborted(signal: AbortSignal | undefined, fallbackReason: string)
|
|
|
103
107
|
throw createAbortError("AbortError", typeof reason === "string" ? reason : fallbackReason);
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
async function waitForLifecycle<T>(
|
|
111
|
+
value: Promise<T>,
|
|
112
|
+
lifecycle: PythonRuntimeLifecycleOptions | undefined,
|
|
113
|
+
message: string,
|
|
114
|
+
): Promise<T> {
|
|
115
|
+
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
116
|
+
const cleanups: Array<() => void> = [];
|
|
117
|
+
const finish = (callback: () => void): void => {
|
|
118
|
+
while (cleanups.length > 0) cleanups.pop()?.();
|
|
119
|
+
callback();
|
|
120
|
+
};
|
|
121
|
+
if (lifecycle?.signal?.aborted) {
|
|
122
|
+
return Promise.reject(lifecycle.signal.reason ?? createAbortError("AbortError", message));
|
|
123
|
+
}
|
|
124
|
+
if (lifecycle?.signal) {
|
|
125
|
+
const onAbort = (): void =>
|
|
126
|
+
finish(() => reject(lifecycle.signal?.reason ?? createAbortError("AbortError", message)));
|
|
127
|
+
lifecycle.signal.addEventListener("abort", onAbort, { once: true });
|
|
128
|
+
cleanups.push(() => lifecycle.signal?.removeEventListener("abort", onAbort));
|
|
129
|
+
}
|
|
130
|
+
const remainingMs = getRemainingTimeMs(lifecycle?.deadlineMs);
|
|
131
|
+
if (remainingMs !== undefined) {
|
|
132
|
+
if (remainingMs <= 0) {
|
|
133
|
+
finish(() => reject(createAbortError("TimeoutError", `${message} timed out`)));
|
|
134
|
+
} else {
|
|
135
|
+
const timer = setTimeout(
|
|
136
|
+
() => finish(() => reject(createAbortError("TimeoutError", `${message} timed out`))),
|
|
137
|
+
remainingMs,
|
|
138
|
+
);
|
|
139
|
+
timer.unref();
|
|
140
|
+
cleanups.push(() => clearTimeout(timer));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
void value.then(
|
|
144
|
+
result => finish(() => resolve(result)),
|
|
145
|
+
error => finish(() => reject(error)),
|
|
146
|
+
);
|
|
147
|
+
return await promise;
|
|
148
|
+
}
|
|
149
|
+
|
|
106
150
|
export async function checkPythonKernelAvailability(
|
|
107
151
|
cwd: string,
|
|
108
152
|
runtimeOptions?: PythonRuntimeOptions,
|
|
153
|
+
lifecycle?: PythonRuntimeLifecycleOptions,
|
|
109
154
|
): Promise<PythonKernelAvailability> {
|
|
110
155
|
if (isBunTestRuntime() || resolvePythonSkipCheck($env)) {
|
|
111
156
|
return { ok: true };
|
|
112
157
|
}
|
|
158
|
+
throwIfAborted(lifecycle?.signal, "Python kernel availability check aborted");
|
|
159
|
+
if (lifecycle?.deadlineMs !== undefined && lifecycle.deadlineMs <= Date.now()) {
|
|
160
|
+
throw createAbortError("TimeoutError", "Python kernel availability check timed out");
|
|
161
|
+
}
|
|
113
162
|
try {
|
|
114
|
-
const settings = await Settings.init();
|
|
163
|
+
const settings = await waitForLifecycle(Settings.init(), lifecycle, "Python kernel settings initialization");
|
|
115
164
|
const { env } = settings.getShellConfig();
|
|
116
165
|
const baseEnv = filterEnv(env);
|
|
117
|
-
const runtime = await ensurePythonRuntime(cwd, baseEnv, runtimeOptions);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
166
|
+
const runtime = await ensurePythonRuntime(cwd, baseEnv, runtimeOptions, lifecycle);
|
|
167
|
+
const probeEnv: Record<string, string> = {};
|
|
168
|
+
for (const [key, value] of Object.entries(runtime.env)) {
|
|
169
|
+
if (typeof value === "string") probeEnv[key] = value;
|
|
170
|
+
}
|
|
171
|
+
const remainingMs = getRemainingTimeMs(lifecycle?.deadlineMs);
|
|
172
|
+
if (remainingMs !== undefined && remainingMs <= 0) {
|
|
173
|
+
throw createAbortError("TimeoutError", "Python kernel availability check timed out");
|
|
174
|
+
}
|
|
175
|
+
const probe = Bun.spawn([runtime.pythonPath, "-c", "import sys;sys.exit(0)"], {
|
|
176
|
+
cwd,
|
|
177
|
+
env: probeEnv,
|
|
178
|
+
stdout: "ignore",
|
|
179
|
+
stderr: "ignore",
|
|
180
|
+
windowsHide: true,
|
|
181
|
+
signal: lifecycle?.signal,
|
|
182
|
+
timeout: remainingMs,
|
|
183
|
+
});
|
|
184
|
+
const exitCode = await probe.exited;
|
|
185
|
+
throwIfAborted(lifecycle?.signal, "Python kernel availability check aborted");
|
|
186
|
+
if (lifecycle?.deadlineMs !== undefined && lifecycle.deadlineMs <= Date.now()) {
|
|
187
|
+
throw createAbortError("TimeoutError", "Python kernel availability check timed out");
|
|
188
|
+
}
|
|
189
|
+
if (exitCode === 0) {
|
|
124
190
|
return { ok: true, pythonPath: runtime.pythonPath };
|
|
125
191
|
}
|
|
126
192
|
return {
|
|
127
193
|
ok: false,
|
|
128
194
|
pythonPath: runtime.pythonPath,
|
|
129
|
-
reason: `Python interpreter at ${runtime.pythonPath} returned exit code ${
|
|
195
|
+
reason: `Python interpreter at ${runtime.pythonPath} returned exit code ${exitCode}`,
|
|
130
196
|
};
|
|
131
197
|
} catch (err) {
|
|
198
|
+
throwIfAborted(lifecycle?.signal, "Python kernel availability check aborted");
|
|
199
|
+
if (lifecycle?.deadlineMs !== undefined && lifecycle.deadlineMs <= Date.now()) {
|
|
200
|
+
throw createAbortError("TimeoutError", "Python kernel availability check timed out");
|
|
201
|
+
}
|
|
132
202
|
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
133
203
|
}
|
|
134
204
|
}
|
|
@@ -186,6 +256,7 @@ export class PythonKernel {
|
|
|
186
256
|
checkPythonKernelAvailability,
|
|
187
257
|
options.cwd,
|
|
188
258
|
options.runtimeOptions,
|
|
259
|
+
{ signal: options.signal, deadlineMs: options.deadlineMs },
|
|
189
260
|
);
|
|
190
261
|
if (!availability.ok) {
|
|
191
262
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
@@ -194,7 +265,10 @@ export class PythonKernel {
|
|
|
194
265
|
const settings = await Settings.init();
|
|
195
266
|
const { env: shellEnv } = settings.getShellConfig();
|
|
196
267
|
const baseEnv = filterEnv(shellEnv);
|
|
197
|
-
const runtime = await ensurePythonRuntime(options.cwd, baseEnv, options.runtimeOptions
|
|
268
|
+
const runtime = await ensurePythonRuntime(options.cwd, baseEnv, options.runtimeOptions, {
|
|
269
|
+
signal: options.signal,
|
|
270
|
+
deadlineMs: options.deadlineMs,
|
|
271
|
+
});
|
|
198
272
|
const spawnEnv: Record<string, string> = {};
|
|
199
273
|
for (const [key, value] of Object.entries(runtime.env)) {
|
|
200
274
|
if (typeof value === "string") spawnEnv[key] = value;
|