@gajae-code/coding-agent 0.5.1 → 0.5.3
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 +31 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock.d.ts +24 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -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/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- 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/provider-onboarding-selector.d.ts +1 -1
- 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 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- 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 +17 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/session/streaming-output.d.ts +5 -0
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- 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/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +21 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +153 -39
- package/src/cli/args.ts +2 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/setup-cli.ts +138 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +14 -2
- package/src/config/file-lock.ts +63 -13
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +15 -15
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- 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-recorder.ts +40 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ralplan-runtime.ts +174 -12
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/tmux-gc.ts +88 -38
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/owner.ts +3 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +14 -10
- package/src/lsp/client.ts +64 -26
- package/src/lsp/defaults.json +1 -0
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +23 -1
- 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/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +60 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- 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 +5 -0
- package/src/modes/controllers/selector-controller.ts +86 -2
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +132 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +5 -2
- 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 +39 -1
- package/src/session/agent-session.ts +190 -33
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +99 -31
- package/src/session/streaming-output.ts +54 -3
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +1 -1
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/tools/subagent-render.ts +119 -29
- package/src/tools/subagent.ts +147 -7
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
|
@@ -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
|
}
|
package/src/eval/py/kernel.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Speaks NDJSON with `runner.py` over stdin/stdout. One subprocess per kernel
|
|
5
5
|
* instance; sessions reuse a single subprocess across executions. Cancellation
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* sends SIGINT to the runner process group, which raises a real
|
|
7
|
+
* `KeyboardInterrupt` inside user code and any foreground magic subprocess.
|
|
8
|
+
* Shutdown writes `{"type":"exit"}` and escalates to SIGTERM/SIGKILL on
|
|
8
9
|
* timeout.
|
|
9
10
|
*/
|
|
10
11
|
import * as fs from "node:fs";
|
|
@@ -173,6 +174,7 @@ interface PendingExecution {
|
|
|
173
174
|
kernelKilled: boolean;
|
|
174
175
|
settled: boolean;
|
|
175
176
|
escalationTimer?: NodeJS.Timeout;
|
|
177
|
+
finalize?: () => void;
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
export class PythonKernel {
|
|
@@ -227,6 +229,9 @@ export class PythonKernel {
|
|
|
227
229
|
stdout: "pipe",
|
|
228
230
|
stderr: "pipe",
|
|
229
231
|
windowsHide: true,
|
|
232
|
+
// Run as its own session/process-group leader so SIGINT/SIGTERM can be
|
|
233
|
+
// delivered to the whole runner tree (incl. foreground magic subprocesses).
|
|
234
|
+
detached: true,
|
|
230
235
|
});
|
|
231
236
|
kernel.#proc = proc;
|
|
232
237
|
kernel.#stdin = proc.stdin;
|
|
@@ -377,10 +382,30 @@ export class PythonKernel {
|
|
|
377
382
|
|
|
378
383
|
async interrupt(): Promise<void> {
|
|
379
384
|
if (!this.#proc || this.#disposed) return;
|
|
385
|
+
this.#signalProcessGroup("SIGINT");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
#signalProcessGroup(signal: NodeJS.Signals): void {
|
|
389
|
+
const proc = this.#proc;
|
|
390
|
+
if (!proc) return;
|
|
391
|
+
try {
|
|
392
|
+
if (process.platform !== "win32") {
|
|
393
|
+
process.kill(-proc.pid, signal);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
logger.warn("Failed to signal python runner process group", {
|
|
398
|
+
signal,
|
|
399
|
+
error: err instanceof Error ? err.message : String(err),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
380
402
|
try {
|
|
381
|
-
|
|
403
|
+
proc.kill(signal);
|
|
382
404
|
} catch (err) {
|
|
383
|
-
logger.warn("Failed to
|
|
405
|
+
logger.warn("Failed to signal python runner", {
|
|
406
|
+
signal,
|
|
407
|
+
error: err instanceof Error ? err.message : String(err),
|
|
408
|
+
});
|
|
384
409
|
}
|
|
385
410
|
}
|
|
386
411
|
|
|
@@ -412,24 +437,16 @@ export class PythonKernel {
|
|
|
412
437
|
|
|
413
438
|
const exited = this.#waitForExitWithTimeout(timeoutMs);
|
|
414
439
|
let result = await exited;
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
proc.kill("SIGTERM");
|
|
418
|
-
} catch {
|
|
419
|
-
/* ignore */
|
|
420
|
-
}
|
|
440
|
+
if (result === null) {
|
|
441
|
+
this.#signalProcessGroup("SIGTERM");
|
|
421
442
|
result = await this.#waitForExitWithTimeout(timeoutMs);
|
|
422
443
|
}
|
|
423
|
-
if (
|
|
424
|
-
|
|
425
|
-
proc.kill("SIGKILL");
|
|
426
|
-
} catch {
|
|
427
|
-
/* ignore */
|
|
428
|
-
}
|
|
444
|
+
if (result === null) {
|
|
445
|
+
this.#signalProcessGroup("SIGKILL");
|
|
429
446
|
result = await this.#waitForExitWithTimeout(timeoutMs);
|
|
430
447
|
}
|
|
431
448
|
|
|
432
|
-
const confirmed =
|
|
449
|
+
const confirmed = result !== null;
|
|
433
450
|
this.#shutdownConfirmed = confirmed;
|
|
434
451
|
this.#disposed = true;
|
|
435
452
|
return { confirmed };
|
|
@@ -441,17 +458,21 @@ export class PythonKernel {
|
|
|
441
458
|
this.#pending.clear();
|
|
442
459
|
const kernelKilledDefault = options?.kernelKilled ?? false;
|
|
443
460
|
for (const entry of pending) {
|
|
461
|
+
entry.cancelled = true;
|
|
462
|
+
entry.status = "error";
|
|
463
|
+
entry.kernelKilled = entry.kernelKilled || kernelKilledDefault;
|
|
464
|
+
entry.finalize?.();
|
|
444
465
|
if (entry.settled) continue;
|
|
445
466
|
entry.settled = true;
|
|
446
467
|
void entry.options?.onChunk?.(`[kernel] ${reason}\n`);
|
|
447
468
|
entry.resolve({
|
|
448
|
-
status:
|
|
449
|
-
cancelled:
|
|
469
|
+
status: entry.status,
|
|
470
|
+
cancelled: entry.cancelled,
|
|
450
471
|
timedOut: entry.timedOut,
|
|
451
472
|
stdinRequested: entry.stdinRequested,
|
|
452
473
|
executionCount: entry.executionCount,
|
|
453
474
|
error: entry.error,
|
|
454
|
-
kernelKilled: entry.kernelKilled
|
|
475
|
+
kernelKilled: entry.kernelKilled,
|
|
455
476
|
});
|
|
456
477
|
}
|
|
457
478
|
}
|
|
@@ -655,11 +676,14 @@ export class PythonKernel {
|
|
|
655
676
|
#waitForExitWithTimeout(timeoutMs: number): Promise<number | null> {
|
|
656
677
|
if (!this.#exitedPromise) return Promise.resolve(0);
|
|
657
678
|
const exitedPromise = this.#exitedPromise;
|
|
658
|
-
|
|
679
|
+
return new Promise(resolve => {
|
|
659
680
|
const timer = setTimeout(() => resolve(null), Math.max(0, timeoutMs));
|
|
660
681
|
timer.unref?.();
|
|
682
|
+
exitedPromise.then(code => {
|
|
683
|
+
clearTimeout(timer);
|
|
684
|
+
resolve(code);
|
|
685
|
+
});
|
|
661
686
|
});
|
|
662
|
-
return Promise.race([exitedPromise.then(code => code as number | null), timeout]);
|
|
663
687
|
}
|
|
664
688
|
}
|
|
665
689
|
|
package/src/eval/py/runner.py
CHANGED
|
@@ -290,6 +290,44 @@ def _emit_status(op: str, **data: Any) -> None:
|
|
|
290
290
|
_emit({"type": "display", "id": rid, "bundle": bundle})
|
|
291
291
|
|
|
292
292
|
|
|
293
|
+
def _terminate_process_group(proc: subprocess.Popen[Any]) -> None:
|
|
294
|
+
if proc.poll() is not None:
|
|
295
|
+
return
|
|
296
|
+
try:
|
|
297
|
+
if os.name == "posix":
|
|
298
|
+
os.killpg(proc.pid, signal.SIGTERM)
|
|
299
|
+
else:
|
|
300
|
+
proc.terminate()
|
|
301
|
+
except ProcessLookupError:
|
|
302
|
+
return
|
|
303
|
+
except Exception:
|
|
304
|
+
try:
|
|
305
|
+
proc.terminate()
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
try:
|
|
309
|
+
proc.wait(timeout=1)
|
|
310
|
+
return
|
|
311
|
+
except subprocess.TimeoutExpired:
|
|
312
|
+
pass
|
|
313
|
+
try:
|
|
314
|
+
if os.name == "posix":
|
|
315
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
316
|
+
else:
|
|
317
|
+
proc.kill()
|
|
318
|
+
except ProcessLookupError:
|
|
319
|
+
return
|
|
320
|
+
except Exception:
|
|
321
|
+
try:
|
|
322
|
+
proc.kill()
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
try:
|
|
326
|
+
proc.wait(timeout=1)
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
|
|
293
331
|
@line_magic("pip")
|
|
294
332
|
def _magic_pip(args: str) -> None:
|
|
295
333
|
argv = shlex.split(args) if args else ["--help"]
|
|
@@ -300,18 +338,26 @@ def _magic_pip(args: str) -> None:
|
|
|
300
338
|
stderr=subprocess.STDOUT,
|
|
301
339
|
text=True,
|
|
302
340
|
bufsize=1,
|
|
341
|
+
start_new_session=(os.name == "posix"),
|
|
303
342
|
)
|
|
304
343
|
installed_packages: list[str] = []
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
344
|
+
try:
|
|
345
|
+
assert proc.stdout is not None
|
|
346
|
+
try:
|
|
347
|
+
for raw_line in proc.stdout:
|
|
348
|
+
sys.stdout.write(raw_line)
|
|
349
|
+
m = re.search(r"Successfully installed\s+(.+)$", raw_line)
|
|
350
|
+
if m:
|
|
351
|
+
for token in m.group(1).split():
|
|
352
|
+
# Token is name-version; drop the version suffix.
|
|
353
|
+
pkg = token.rsplit("-", 1)[0]
|
|
354
|
+
installed_packages.append(pkg.replace("_", "-"))
|
|
355
|
+
finally:
|
|
356
|
+
proc.stdout.close()
|
|
357
|
+
proc.wait()
|
|
358
|
+
except KeyboardInterrupt:
|
|
359
|
+
_terminate_process_group(proc)
|
|
360
|
+
raise
|
|
315
361
|
if installed_packages:
|
|
316
362
|
import importlib
|
|
317
363
|
|
|
@@ -500,11 +546,19 @@ def _run_shell_body(body: str, *, shell_arg: str) -> int:
|
|
|
500
546
|
stderr=subprocess.STDOUT,
|
|
501
547
|
text=True,
|
|
502
548
|
bufsize=1,
|
|
549
|
+
start_new_session=(os.name == "posix"),
|
|
503
550
|
)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
551
|
+
try:
|
|
552
|
+
assert proc.stdout is not None
|
|
553
|
+
try:
|
|
554
|
+
for raw_line in proc.stdout:
|
|
555
|
+
sys.stdout.write(raw_line)
|
|
556
|
+
finally:
|
|
557
|
+
proc.stdout.close()
|
|
558
|
+
proc.wait()
|
|
559
|
+
except KeyboardInterrupt:
|
|
560
|
+
_terminate_process_group(proc)
|
|
561
|
+
raise
|
|
508
562
|
return proc.returncode
|
|
509
563
|
|
|
510
564
|
|
|
@@ -32,6 +32,8 @@ export interface BashExecutorOptions {
|
|
|
32
32
|
/** Artifact path/id for full output storage */
|
|
33
33
|
artifactPath?: string;
|
|
34
34
|
artifactId?: string;
|
|
35
|
+
/** Execute without retaining a native Shell in the persistent session registry. */
|
|
36
|
+
oneShot?: boolean;
|
|
35
37
|
/**
|
|
36
38
|
* Invoked when the native minimizer rewrote the command's output, giving
|
|
37
39
|
* the caller a chance to persist the lossless original capture (typically
|
|
@@ -59,6 +61,8 @@ export interface BashResult {
|
|
|
59
61
|
|
|
60
62
|
const shellSessions = new Map<string, Shell>();
|
|
61
63
|
const brokenShellSessions = new Set<string>();
|
|
64
|
+
const retiringShellSessions = new Set<Shell>();
|
|
65
|
+
const CANCEL_CLEANUP_WAIT_MS = 250;
|
|
62
66
|
|
|
63
67
|
/** Number of persistent shell sessions currently retained (owner gauge). */
|
|
64
68
|
export function getShellSessionCount(): number {
|
|
@@ -76,10 +80,14 @@ export async function disposeAllShellSessions(): Promise<void> {
|
|
|
76
80
|
// Snapshot and drop strong references up front so concurrent callers cannot
|
|
77
81
|
// reuse a session that is being torn down, then await every native abort so
|
|
78
82
|
// shutdown/signal cleanup does not return before resources are released.
|
|
79
|
-
|
|
83
|
+
// Include retiring shells whose JS call returned after bounded abort cleanup
|
|
84
|
+
// while the native run is still unwinding; they are no longer reusable but
|
|
85
|
+
// remain owned until their run promise settles.
|
|
86
|
+
const sessions = new Set([...shellSessions.values(), ...retiringShellSessions]);
|
|
80
87
|
shellSessions.clear();
|
|
88
|
+
retiringShellSessions.clear();
|
|
81
89
|
brokenShellSessions.clear();
|
|
82
|
-
await Promise.allSettled(sessions.map(session => session.abort()));
|
|
90
|
+
await Promise.allSettled([...sessions].map(session => session.abort()));
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
postmortem.register("bash-executor:shell-sessions", () => disposeAllShellSessions());
|
|
@@ -151,14 +159,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
151
159
|
};
|
|
152
160
|
}
|
|
153
161
|
|
|
162
|
+
const usePersistentShell = options?.oneShot !== true;
|
|
154
163
|
const sessionKey = buildSessionKey(shell, prefix, snapshotPath, shellEnv, options?.sessionKey, minimizer);
|
|
155
|
-
const persistentSessionBroken = brokenShellSessions.has(sessionKey);
|
|
156
|
-
if (persistentSessionBroken) {
|
|
157
|
-
shellSessions.delete(sessionKey);
|
|
158
|
-
}
|
|
164
|
+
const persistentSessionBroken = usePersistentShell && brokenShellSessions.has(sessionKey);
|
|
159
165
|
|
|
160
|
-
let shellSession = persistentSessionBroken ? undefined : shellSessions.get(sessionKey);
|
|
161
|
-
if (!shellSession && !persistentSessionBroken) {
|
|
166
|
+
let shellSession = persistentSessionBroken || !usePersistentShell ? undefined : shellSessions.get(sessionKey);
|
|
167
|
+
if (!shellSession && !persistentSessionBroken && usePersistentShell) {
|
|
162
168
|
shellSession = new Shell({
|
|
163
169
|
sessionEnv: shellEnv,
|
|
164
170
|
snapshotPath: snapshotPath ?? undefined,
|
|
@@ -172,16 +178,29 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
172
178
|
if (!runAbortController.signal.aborted) {
|
|
173
179
|
runAbortController.abort();
|
|
174
180
|
}
|
|
175
|
-
if (shellSession) {
|
|
176
|
-
|
|
177
|
-
void shellSession.abort();
|
|
181
|
+
if (shellSession && !abortPromise) {
|
|
182
|
+
abortPromise = shellSession.abort();
|
|
178
183
|
}
|
|
179
184
|
};
|
|
180
185
|
const abortDeferred = Promise.withResolvers<"abort">();
|
|
186
|
+
let abortPromise: Promise<unknown> | undefined;
|
|
181
187
|
const abortHandler = () => {
|
|
182
188
|
abortCurrentExecution();
|
|
183
189
|
abortDeferred.resolve("abort");
|
|
184
190
|
};
|
|
191
|
+
const awaitAbortCleanup = async (runPromise: Promise<unknown>): Promise<boolean> => {
|
|
192
|
+
const settled = await Promise.race([
|
|
193
|
+
runPromise.then(
|
|
194
|
+
() => true,
|
|
195
|
+
() => true,
|
|
196
|
+
),
|
|
197
|
+
Bun.sleep(CANCEL_CLEANUP_WAIT_MS).then(() => false),
|
|
198
|
+
]);
|
|
199
|
+
if (abortPromise) {
|
|
200
|
+
await Promise.race([abortPromise.catch(() => undefined), Bun.sleep(CANCEL_CLEANUP_WAIT_MS)]);
|
|
201
|
+
}
|
|
202
|
+
return settled;
|
|
203
|
+
};
|
|
185
204
|
if (userSignal) {
|
|
186
205
|
userSignal.addEventListener("abort", abortHandler, { once: true });
|
|
187
206
|
}
|
|
@@ -198,6 +217,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
198
217
|
}
|
|
199
218
|
|
|
200
219
|
let resetSession = false;
|
|
220
|
+
let runSettled = false;
|
|
201
221
|
|
|
202
222
|
try {
|
|
203
223
|
const runPromise = shellSession
|
|
@@ -243,8 +263,24 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
243
263
|
acceptingChunks = false;
|
|
244
264
|
if (shellSession) {
|
|
245
265
|
resetSession = true;
|
|
266
|
+
retiringShellSessions.add(shellSession);
|
|
246
267
|
brokenShellSessions.add(sessionKey);
|
|
247
|
-
|
|
268
|
+
shellSessions.delete(sessionKey);
|
|
269
|
+
runSettled = await awaitAbortCleanup(runPromise);
|
|
270
|
+
if (runSettled) {
|
|
271
|
+
brokenShellSessions.delete(sessionKey);
|
|
272
|
+
retiringShellSessions.delete(shellSession);
|
|
273
|
+
} else {
|
|
274
|
+
void runPromise
|
|
275
|
+
.finally(() => {
|
|
276
|
+
brokenShellSessions.delete(sessionKey);
|
|
277
|
+
retiringShellSessions.delete(shellSession);
|
|
278
|
+
if (shellSessions.get(sessionKey) === shellSession) {
|
|
279
|
+
shellSessions.delete(sessionKey);
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
.catch(() => undefined);
|
|
283
|
+
}
|
|
248
284
|
} else {
|
|
249
285
|
void runPromise.catch(() => undefined);
|
|
250
286
|
}
|
|
@@ -337,7 +373,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
337
373
|
if (userSignal) {
|
|
338
374
|
userSignal.removeEventListener("abort", abortHandler);
|
|
339
375
|
}
|
|
340
|
-
if (resetSession) {
|
|
376
|
+
if (resetSession && runSettled && shellSessions.get(sessionKey) === shellSession) {
|
|
341
377
|
shellSessions.delete(sessionKey);
|
|
342
378
|
}
|
|
343
379
|
}
|
|
@@ -388,6 +388,34 @@ export async function appendOrMergeDeepInterviewRound(
|
|
|
388
388
|
return { action: result.action, record: result.record };
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/**
|
|
392
|
+
* The chronological scored predecessor of the round currently being scored: the
|
|
393
|
+
* scored round with the greatest `round` strictly less than `currentRound`, with
|
|
394
|
+
* the same durable key excluded. Selecting by `round` (not array position) ensures
|
|
395
|
+
* an out-of-order re-score of an earlier round compares against its true prior, never
|
|
396
|
+
* a later ("future") scored round that happens to sit later in the array.
|
|
397
|
+
*
|
|
398
|
+
* Fail-safe: if `currentRound` is not a finite number, or a candidate's `round` is
|
|
399
|
+
* not finite, that comparison is treated as non-matching, so no prior is selected
|
|
400
|
+
* rather than risking a spurious comparison against an unrelated round.
|
|
401
|
+
*/
|
|
402
|
+
function latestPriorScoredRound(
|
|
403
|
+
rounds: readonly DeepInterviewRoundRecord[],
|
|
404
|
+
currentKey: string,
|
|
405
|
+
currentRound: number,
|
|
406
|
+
): DeepInterviewRoundRecord | undefined {
|
|
407
|
+
if (!Number.isFinite(currentRound)) return undefined;
|
|
408
|
+
let prior: DeepInterviewRoundRecord | undefined;
|
|
409
|
+
for (const candidate of rounds) {
|
|
410
|
+
if (candidate.lifecycle !== "scored") continue;
|
|
411
|
+
if (candidate.round_key === currentKey) continue;
|
|
412
|
+
if (!Number.isFinite(candidate.round)) continue;
|
|
413
|
+
if (!(candidate.round < currentRound)) continue;
|
|
414
|
+
if (prior === undefined || candidate.round > prior.round) prior = candidate;
|
|
415
|
+
}
|
|
416
|
+
return prior;
|
|
417
|
+
}
|
|
418
|
+
|
|
391
419
|
/** Merge scoring output into the same round record, transitioning to `scored`. */
|
|
392
420
|
export async function enrichDeepInterviewRoundScoring(
|
|
393
421
|
cwd: string,
|
|
@@ -399,6 +427,18 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
399
427
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
400
428
|
const rounds = readRounds(envelope);
|
|
401
429
|
const { rounds: nextRounds, record } = enrichRoundWithScoring(rounds, { ...input, interviewId });
|
|
430
|
+
// Fail closed: a scored transition that violates the bidirectional invariant
|
|
431
|
+
// (an active trigger that improves the affected dimension or fails to raise
|
|
432
|
+
// overall ambiguity, or a disputed/unresolved trigger lacking a rationale) must
|
|
433
|
+
// never be persisted — storing it lets the interview falsely converge. Validate
|
|
434
|
+
// against the most recent prior scored round before writing any durable state.
|
|
435
|
+
const prior = latestPriorScoredRound(rounds, record.round_key, record.round);
|
|
436
|
+
const validation = validateDeepInterviewScoredTransition(prior, record);
|
|
437
|
+
if (!validation.ok) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`deep-interview scored transition for round ${record.round} is invalid and was refused: ${validation.violations.join("; ")}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
402
442
|
(envelope.state as Record<string, unknown>).rounds = nextRounds;
|
|
403
443
|
(envelope.state as Record<string, unknown>).current_ambiguity = input.ambiguity;
|
|
404
444
|
await persistEnvelope(cwd, statePath, envelope, options.sessionId, "gjc deep-interview score-round");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { safeStderrWrite } from "@gajae-code/utils";
|
|
2
3
|
import type { Args } from "../cli/args";
|
|
3
4
|
import {
|
|
4
5
|
buildGjcTmuxProfileCommands,
|
|
@@ -280,7 +281,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
280
281
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
281
282
|
const failure =
|
|
282
283
|
profile.failures.find(item => item.command.args.includes("@gjc-profile")) ?? profile.failures[0];
|
|
283
|
-
(context.diagnosticWriter ??
|
|
284
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
284
285
|
formatTmuxLaunchDiagnostic("profile tagging failed", failure?.stderr),
|
|
285
286
|
);
|
|
286
287
|
return true;
|
|
@@ -289,8 +290,6 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
289
290
|
if (created.exitCode !== 0) return false;
|
|
290
291
|
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
|
|
291
292
|
if (attached.exitCode === 0) return true;
|
|
292
|
-
(context.diagnosticWriter ??
|
|
293
|
-
formatTmuxLaunchDiagnostic("attach failed", attached.stderr),
|
|
294
|
-
);
|
|
293
|
+
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
295
294
|
return true;
|
|
296
295
|
}
|