@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.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 +91 -0
- package/dist/cli.js +3556 -3504
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +3 -3
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
- package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
- package/dist/types/goals/guided-setup.d.ts +12 -0
- package/dist/types/internal-urls/history-protocol.d.ts +3 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -0
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +10 -0
- package/dist/types/modes/components/dynamic-border.d.ts +5 -3
- package/dist/types/modes/components/login-dialog.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +0 -2
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/session/messages.d.ts +15 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +0 -2
- package/dist/types/tools/read.d.ts +0 -4
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +917 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +270 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/autoresearch/dashboard.ts +2 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/config-cli.ts +15 -3
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +10 -0
- package/src/cursor.ts +2 -0
- package/src/discovery/claude-plugins.ts +9 -3
- package/src/discovery/omp-plugins.ts +6 -2
- package/src/discovery/substitute-plugin-root.ts +32 -0
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/prelude-agent.test.ts +20 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/backend.ts +3 -3
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +72 -0
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/prelude.py +28 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/exec/bash-executor.ts +30 -43
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/extensions/wrapper.ts +18 -18
- package/src/extensibility/plugins/bun-git-cache.ts +91 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
- package/src/extensibility/plugins/manager.ts +28 -7
- package/src/goals/guided-setup.ts +29 -1
- package/src/internal-urls/history-protocol.ts +95 -15
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/internal-urls/registry-helpers.ts +50 -1
- package/src/launch/broker.ts +38 -25
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/oauth-discovery.ts +20 -1
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
- package/src/modes/components/agent-dashboard.ts +2 -2
- package/src/modes/components/agent-hub.ts +15 -2
- package/src/modes/components/agent-transcript-viewer.ts +2 -2
- package/src/modes/components/chat-transcript-builder.ts +26 -4
- package/src/modes/components/dynamic-border.ts +9 -6
- package/src/modes/components/extensions/extension-list.ts +2 -2
- package/src/modes/components/hook-selector.ts +10 -4
- package/src/modes/components/login-dialog.ts +5 -0
- package/src/modes/components/mcp-add-wizard.ts +5 -0
- package/src/modes/components/plan-review-overlay.ts +11 -11
- package/src/modes/components/read-tool-group.ts +1 -8
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/input-controller.ts +4 -2
- package/src/modes/controllers/mcp-command-controller.ts +6 -7
- package/src/modes/controllers/selector-controller.ts +24 -5
- package/src/modes/controllers/todo-command-controller.ts +18 -14
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/prompt-action-autocomplete.ts +6 -1
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/types.ts +1 -1
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/grep.md +1 -2
- package/src/prompts/tools/read.md +2 -4
- package/src/sdk.ts +61 -34
- package/src/session/agent-session.ts +283 -39
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/messages.test.ts +66 -0
- package/src/session/messages.ts +37 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/system-prompt.test.ts +36 -0
- package/src/system-prompt.ts +1 -1
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser/registry.ts +17 -3
- package/src/tools/eval.ts +14 -9
- package/src/tools/gh.ts +3 -1
- package/src/tools/grep.ts +5 -45
- package/src/tools/path-utils.ts +7 -1
- package/src/tools/read.ts +23 -74
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/utils/title-generator.ts +10 -6
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
- package/src/web/search/providers/perplexity-auth.ts +20 -11
- package/src/web/search/providers/perplexity.ts +14 -2
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
3
5
|
import { Settings } from "../../config/settings";
|
|
4
6
|
import type { ToolSession } from "../../tools";
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
disposeAllVmContexts,
|
|
9
|
+
setJsEvalWorkerThreadForTests,
|
|
10
|
+
setWorkerCloseTimeoutMsForTests,
|
|
11
|
+
} from "../js/context-manager";
|
|
6
12
|
import { executeJs } from "../js/executor";
|
|
7
13
|
|
|
8
14
|
const originalWorker = globalThis.Worker;
|
|
@@ -181,7 +187,9 @@ function installFakeWorker(stats: FakeWorkerStats, behavior: FakeWorkerBehavior)
|
|
|
181
187
|
|
|
182
188
|
describe("JavaScript eval worker lifecycle", () => {
|
|
183
189
|
let restoreCloseTimeoutMs = 0;
|
|
190
|
+
let restoreWorkerThread = false;
|
|
184
191
|
beforeEach(() => {
|
|
192
|
+
restoreWorkerThread = setJsEvalWorkerThreadForTests(true);
|
|
185
193
|
// Shrink the graceful-close grace period so the "close acked but the worker
|
|
186
194
|
// never exits -> force terminate" contract is proven without a real 1s wait.
|
|
187
195
|
restoreCloseTimeoutMs = setWorkerCloseTimeoutMsForTests(1);
|
|
@@ -197,6 +205,7 @@ describe("JavaScript eval worker lifecycle", () => {
|
|
|
197
205
|
writable: true,
|
|
198
206
|
value: originalWorker,
|
|
199
207
|
});
|
|
208
|
+
setJsEvalWorkerThreadForTests(restoreWorkerThread);
|
|
200
209
|
});
|
|
201
210
|
|
|
202
211
|
it("exits a real worker on graceful close even with ref'ed user handles", async () => {
|
|
@@ -271,6 +280,73 @@ describe("JavaScript eval worker lifecycle", () => {
|
|
|
271
280
|
expect(stats.terminateCalls).toBe(1);
|
|
272
281
|
});
|
|
273
282
|
|
|
283
|
+
it("falls back to a Bun Worker when the subprocess cannot spawn", async () => {
|
|
284
|
+
using tempDir = TempDir.createSync("@omp-js-spawn-fallback-");
|
|
285
|
+
// Exercise the production ladder (process -> worker -> inline), not the
|
|
286
|
+
// worker-thread test seam the surrounding describe enables.
|
|
287
|
+
setJsEvalWorkerThreadForTests(false);
|
|
288
|
+
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
|
|
289
|
+
installFakeWorker(stats, { exitOnClose: true, settleRuns: true });
|
|
290
|
+
const originalSpawn = Bun.spawn;
|
|
291
|
+
let spawnAttempts = 0;
|
|
292
|
+
Bun.spawn = ((): never => {
|
|
293
|
+
spawnAttempts++;
|
|
294
|
+
throw new Error("subprocess spawn unavailable");
|
|
295
|
+
}) as unknown as typeof Bun.spawn;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const session = makeSession(tempDir.path());
|
|
299
|
+
const sessionId = `js-spawn-fallback:${crypto.randomUUID()}`;
|
|
300
|
+
// The fake Worker settles runs without executing the cell, so an empty
|
|
301
|
+
// output proves the middle rung handled it — the inline fallback would
|
|
302
|
+
// have actually evaluated the expression and printed 42.
|
|
303
|
+
const result = await executeJs("return String(6 * 7);", { cwd: tempDir.path(), sessionId, session });
|
|
304
|
+
expect(result.exitCode).toBe(0);
|
|
305
|
+
expect(result.output.trim()).toBe("");
|
|
306
|
+
expect(spawnAttempts).toBe(1);
|
|
307
|
+
} finally {
|
|
308
|
+
Bun.spawn = originalSpawn;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("falls back to a Bun Worker when the subprocess fails during initialization", async () => {
|
|
313
|
+
using tempDir = TempDir.createSync("@omp-js-init-fallback-");
|
|
314
|
+
// Exercise the production ladder (process -> worker -> inline), not the
|
|
315
|
+
// worker-thread test seam the surrounding describe enables.
|
|
316
|
+
setJsEvalWorkerThreadForTests(false);
|
|
317
|
+
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
|
|
318
|
+
installFakeWorker(stats, { exitOnClose: true, settleRuns: true });
|
|
319
|
+
const originalSpawn = Bun.spawn;
|
|
320
|
+
let spawnAttempts = 0;
|
|
321
|
+
Bun.spawn = ((options: unknown) => {
|
|
322
|
+
spawnAttempts++;
|
|
323
|
+
const spawnOptions = options as {
|
|
324
|
+
onExit?: (proc: unknown, exitCode: number | null, signalCode: string | null) => void;
|
|
325
|
+
};
|
|
326
|
+
const fakeProcess = {
|
|
327
|
+
send: () => undefined,
|
|
328
|
+
kill: () => undefined,
|
|
329
|
+
unref: () => undefined,
|
|
330
|
+
};
|
|
331
|
+
queueMicrotask(() => spawnOptions.onExit?.(fakeProcess, 1, null));
|
|
332
|
+
return fakeProcess;
|
|
333
|
+
}) as unknown as typeof Bun.spawn;
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const session = makeSession(tempDir.path());
|
|
337
|
+
const sessionId = `js-init-fallback:${crypto.randomUUID()}`;
|
|
338
|
+
// The fake Worker settles runs without executing the cell, so empty
|
|
339
|
+
// output proves the middle rung handled the retry. Inline execution
|
|
340
|
+
// would evaluate the expression and print 42.
|
|
341
|
+
const result = await executeJs("return String(6 * 7);", { cwd: tempDir.path(), sessionId, session });
|
|
342
|
+
expect(result.exitCode).toBe(0);
|
|
343
|
+
expect(result.output.trim()).toBe("");
|
|
344
|
+
expect(spawnAttempts).toBe(1);
|
|
345
|
+
} finally {
|
|
346
|
+
Bun.spawn = originalSpawn;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
274
350
|
it("falls back to the inline worker when the spawned worker errors during startup", async () => {
|
|
275
351
|
using tempDir = TempDir.createSync("@omp-js-worker-error-");
|
|
276
352
|
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
|
|
@@ -289,3 +365,84 @@ describe("JavaScript eval worker lifecycle", () => {
|
|
|
289
365
|
expect(stats.terminateCalls).toBe(1);
|
|
290
366
|
});
|
|
291
367
|
});
|
|
368
|
+
|
|
369
|
+
describe.skipIf(process.platform === "win32")("JavaScript eval process isolation", () => {
|
|
370
|
+
afterEach(async () => {
|
|
371
|
+
await disposeAllVmContexts();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("runs spawned commands in the isolated POSIX process group", async () => {
|
|
375
|
+
using tempDir = TempDir.createSync("@omp-js-process-isolation-");
|
|
376
|
+
const session = makeSession(tempDir.path());
|
|
377
|
+
const evalSessionId = `js-isolation:${crypto.randomUUID()}`;
|
|
378
|
+
const result = await executeJs(
|
|
379
|
+
[
|
|
380
|
+
`const child = Bun.spawn(["/bin/sh", "-c", 'pgid=$(ps -o pgid= -p $$); printf "%s %s\\n" "$pgid" "$PPID"'], { stdout: "pipe" });`,
|
|
381
|
+
"return await new Response(child.stdout).text();",
|
|
382
|
+
].join("\n"),
|
|
383
|
+
{ cwd: tempDir.path(), sessionId: evalSessionId, session },
|
|
384
|
+
);
|
|
385
|
+
const [processGroupId, parentProcessId] = result.output.trim().split(/\s+/).map(Number);
|
|
386
|
+
expect(parentProcessId).not.toBe(process.pid);
|
|
387
|
+
expect(processGroupId).toBe(parentProcessId);
|
|
388
|
+
|
|
389
|
+
await executeJs("var saved = 41; function increment(value) { return value + 1; }", {
|
|
390
|
+
cwd: tempDir.path(),
|
|
391
|
+
sessionId: evalSessionId,
|
|
392
|
+
session,
|
|
393
|
+
});
|
|
394
|
+
const reused = await executeJs("return increment(saved);", {
|
|
395
|
+
cwd: tempDir.path(),
|
|
396
|
+
sessionId: evalSessionId,
|
|
397
|
+
session,
|
|
398
|
+
});
|
|
399
|
+
expect(reused.output.trim()).toBe("42");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("mirrors the session cwd onto the subprocess's real cwd", async () => {
|
|
403
|
+
using tempDir = TempDir.createSync("@omp-js-process-cwd-");
|
|
404
|
+
const session = makeSession(tempDir.path());
|
|
405
|
+
const evalSessionId = `js-cwd:${crypto.randomUUID()}`;
|
|
406
|
+
const result = await executeJs("return process.cwd();", {
|
|
407
|
+
cwd: tempDir.path(),
|
|
408
|
+
sessionId: evalSessionId,
|
|
409
|
+
session,
|
|
410
|
+
});
|
|
411
|
+
// process.chdir resolves symlinks (macOS tempdirs live under /var ->
|
|
412
|
+
// /private/var), so compare physical paths.
|
|
413
|
+
expect(result.output.trim()).toBe(fs.realpathSync(tempDir.path()));
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("still runs cells when the session cwd does not exist", async () => {
|
|
417
|
+
using tempDir = TempDir.createSync("@omp-js-process-cwd-missing-");
|
|
418
|
+
const missingCwd = path.join(tempDir.path(), "deleted");
|
|
419
|
+
const session = makeSession(missingCwd);
|
|
420
|
+
const result = await executeJs("return String(6 * 7);", {
|
|
421
|
+
cwd: missingCwd,
|
|
422
|
+
sessionId: `js-cwd-missing:${crypto.randomUUID()}`,
|
|
423
|
+
session,
|
|
424
|
+
});
|
|
425
|
+
expect(result.exitCode).toBe(0);
|
|
426
|
+
expect(result.output.trim()).toBe("42");
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("keeps the isolated process alive after a stackless floated rejection", async () => {
|
|
430
|
+
using tempDir = TempDir.createSync("@omp-js-process-rejection-");
|
|
431
|
+
const session = makeSession(tempDir.path());
|
|
432
|
+
const evalSessionId = `js-rejection:${crypto.randomUUID()}`;
|
|
433
|
+
const rejected = await executeJs(
|
|
434
|
+
'var savedAfterRejection = 41; Promise.reject("stackless rejection"); await Bun.sleep(10);',
|
|
435
|
+
{ cwd: tempDir.path(), sessionId: evalSessionId, session },
|
|
436
|
+
);
|
|
437
|
+
expect(rejected.exitCode).toBe(1);
|
|
438
|
+
expect(rejected.output).toContain("Unhandled rejection (missing await?): stackless rejection");
|
|
439
|
+
|
|
440
|
+
const reused = await executeJs("return savedAfterRejection + 1;", {
|
|
441
|
+
cwd: tempDir.path(),
|
|
442
|
+
sessionId: evalSessionId,
|
|
443
|
+
session,
|
|
444
|
+
});
|
|
445
|
+
expect(reused.exitCode).toBe(0);
|
|
446
|
+
expect(reused.output.trim()).toBe("42");
|
|
447
|
+
});
|
|
448
|
+
});
|
|
@@ -3,9 +3,21 @@ import {
|
|
|
3
3
|
__resetWindowsConsoleProbeCache,
|
|
4
4
|
consoleAttachedViaTTY,
|
|
5
5
|
hostHasInheritableConsole,
|
|
6
|
+
shouldDetachKernel,
|
|
6
7
|
shouldHideKernelWindow,
|
|
7
8
|
} from "../py/spawn-options";
|
|
8
9
|
|
|
10
|
+
describe("shouldDetachKernel", () => {
|
|
11
|
+
it("starts POSIX kernels in a new session", () => {
|
|
12
|
+
expect(shouldDetachKernel("darwin")).toBe(true);
|
|
13
|
+
expect(shouldDetachKernel("linux")).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("leaves Windows console inheritance to windowsHide", () => {
|
|
17
|
+
expect(shouldDetachKernel("win32")).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
9
21
|
/**
|
|
10
22
|
* `shouldHideKernelWindow` decides whether the long-lived Python kernel
|
|
11
23
|
* subprocess is spawned with `windowsHide: true`. On Windows, Bun maps that
|
|
@@ -105,3 +105,23 @@ describe("eval js agent() handle", () => {
|
|
|
105
105
|
expect("branchName" in node).toBe(false);
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
describe("eval js read() URI delegation", () => {
|
|
110
|
+
it("appends line selectors to delegated URI paths", async () => {
|
|
111
|
+
const calls: Array<{ name: string; args: unknown }> = [];
|
|
112
|
+
const sandbox = loadPrelude(async (name, args) => {
|
|
113
|
+
calls.push({ name, args });
|
|
114
|
+
return { text: "resource contents" };
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result = await vm.runInContext(`read("mcp://server/resource", { offset: 10, limit: 5 })`, sandbox);
|
|
118
|
+
|
|
119
|
+
expect(result).toBe("resource contents");
|
|
120
|
+
expect(calls).toEqual([
|
|
121
|
+
{
|
|
122
|
+
name: "read",
|
|
123
|
+
args: { path: "mcp://server/resource:10-14" },
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { expect, it } from "bun:test";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
4
|
+
|
|
5
|
+
it("imports the JS process entry without loading dotenv before profile bootstrap", async () => {
|
|
6
|
+
using tempDir = TempDir.createSync("@omp-js-process-import-");
|
|
7
|
+
await Bun.write(path.join(tempDir.path(), ".env"), "OMP_PROCESS_ENTRY_ENV_PROBE=loaded-too-early\n");
|
|
8
|
+
const env = Object.fromEntries(
|
|
9
|
+
Object.entries(process.env).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
|
10
|
+
);
|
|
11
|
+
delete env.OMP_PROCESS_ENTRY_ENV_PROBE;
|
|
12
|
+
env.HOME = tempDir.path();
|
|
13
|
+
const fixture = path.resolve(import.meta.dir, "../../../test/fixtures/js-process-entry-import.ts");
|
|
14
|
+
const proc = Bun.spawn([process.execPath, fixture], {
|
|
15
|
+
env,
|
|
16
|
+
stdout: "pipe",
|
|
17
|
+
stderr: "pipe",
|
|
18
|
+
});
|
|
19
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
20
|
+
proc.exited,
|
|
21
|
+
new Response(proc.stdout).text(),
|
|
22
|
+
new Response(proc.stderr).text(),
|
|
23
|
+
]);
|
|
24
|
+
expect(exitCode).toBe(0);
|
|
25
|
+
expect(stdout).toBe("");
|
|
26
|
+
expect(stderr).toBe("");
|
|
27
|
+
});
|
package/src/eval/agent-bridge.ts
CHANGED
|
@@ -449,130 +449,135 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
449
449
|
// runtime is parked waiting for the result, and the cell timeout must
|
|
450
450
|
// not abort us mid-cherry-pick or mid-nested-commit. The clock restarts
|
|
451
451
|
// only after we hand control back to the runtime.
|
|
452
|
-
const { result, mergeSummary, changesApplied } = await withBridgeTimeoutPause(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
const preferredBackend = isIsolated ? parseIsolationMode(isolationMode) : undefined;
|
|
463
|
-
|
|
464
|
-
const result = await (async () => {
|
|
465
|
-
if (!isolationContext) {
|
|
466
|
-
return taskExecutor.runSubprocess(baseRunOptions);
|
|
467
|
-
}
|
|
468
|
-
const taskStart = Date.now();
|
|
469
|
-
return runIsolatedSubprocess({
|
|
470
|
-
baseOptions: baseRunOptions,
|
|
471
|
-
context: isolationContext,
|
|
472
|
-
preferredBackend,
|
|
473
|
-
agentId: id,
|
|
474
|
-
mergeMode,
|
|
475
|
-
artifactsDir,
|
|
476
|
-
description: trimToUndefined(parsed.label),
|
|
477
|
-
buildCommitMessage,
|
|
478
|
-
buildFailureResult: err => {
|
|
452
|
+
const { result, mergeSummary, changesApplied } = await withBridgeTimeoutPause(
|
|
453
|
+
options.emitStatus,
|
|
454
|
+
async () => {
|
|
455
|
+
let isolationContext: IsolationContext | null = null;
|
|
456
|
+
if (isIsolated) {
|
|
457
|
+
try {
|
|
458
|
+
isolationContext = await prepareIsolationContext(options.session.cwd);
|
|
459
|
+
} catch (err) {
|
|
479
460
|
const message = err instanceof Error ? err.message : String(err);
|
|
480
|
-
|
|
481
|
-
index: 0,
|
|
482
|
-
id,
|
|
483
|
-
agent: effectiveAgent.name,
|
|
484
|
-
agentSource: effectiveAgent.source,
|
|
485
|
-
task: renderSubagentPrompt(assignment),
|
|
486
|
-
assignment,
|
|
487
|
-
description: trimToUndefined(parsed.label),
|
|
488
|
-
exitCode: 1,
|
|
489
|
-
output: "",
|
|
490
|
-
stderr: message,
|
|
491
|
-
truncated: false,
|
|
492
|
-
durationMs: Date.now() - taskStart,
|
|
493
|
-
tokens: 0,
|
|
494
|
-
requests: 0,
|
|
495
|
-
modelOverride,
|
|
496
|
-
error: message,
|
|
497
|
-
};
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
})();
|
|
501
|
-
|
|
502
|
-
if (result.exitCode !== 0 || result.error || result.aborted) {
|
|
503
|
-
const failureMessage = buildSubagentFailureMessage(agentName, result);
|
|
504
|
-
const recoveryHint = isIsolated ? await buildIsolationRecoveryHint(result, artifactsDir) : "";
|
|
505
|
-
throw new ToolError(`${failureMessage}${recoveryHint}`);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
let mergeSummary = "";
|
|
509
|
-
let changesApplied: boolean | null = null;
|
|
510
|
-
if (isIsolated && isolationContext) {
|
|
511
|
-
if (applyChanges) {
|
|
512
|
-
const outcome = await mergeIsolatedChanges({
|
|
513
|
-
result,
|
|
514
|
-
repoRoot: isolationContext.repoRoot,
|
|
515
|
-
mergeMode,
|
|
516
|
-
});
|
|
517
|
-
mergeSummary = outcome.summary;
|
|
518
|
-
changesApplied = outcome.changesApplied;
|
|
519
|
-
if (outcome.changesApplied === false) {
|
|
520
|
-
const summaryText = outcome.summary.trim();
|
|
521
|
-
const recoveryHint = await buildIsolationRecoveryHint(result, artifactsDir);
|
|
522
|
-
throw new ToolError(
|
|
523
|
-
`agent() isolated apply failed for ${result.id}${summaryText ? `: ${summaryText}` : ""}${recoveryHint}`,
|
|
524
|
-
);
|
|
461
|
+
throw new ToolError(`Isolated agent() execution requires a git repository. ${message}`);
|
|
525
462
|
}
|
|
463
|
+
}
|
|
464
|
+
const preferredBackend = isIsolated ? parseIsolationMode(isolationMode) : undefined;
|
|
526
465
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
466
|
+
const result = await (async () => {
|
|
467
|
+
if (!isolationContext) {
|
|
468
|
+
return taskExecutor.runSubprocess(baseRunOptions);
|
|
469
|
+
}
|
|
470
|
+
const taskStart = Date.now();
|
|
471
|
+
return runIsolatedSubprocess({
|
|
472
|
+
baseOptions: baseRunOptions,
|
|
473
|
+
context: isolationContext,
|
|
474
|
+
preferredBackend,
|
|
475
|
+
agentId: id,
|
|
530
476
|
mergeMode,
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
477
|
+
artifactsDir,
|
|
478
|
+
description: trimToUndefined(parsed.label),
|
|
479
|
+
buildCommitMessage,
|
|
480
|
+
buildFailureResult: err => {
|
|
481
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
482
|
+
return {
|
|
483
|
+
index: 0,
|
|
484
|
+
id,
|
|
485
|
+
agent: effectiveAgent.name,
|
|
486
|
+
agentSource: effectiveAgent.source,
|
|
487
|
+
task: renderSubagentPrompt(assignment),
|
|
488
|
+
assignment,
|
|
489
|
+
description: trimToUndefined(parsed.label),
|
|
490
|
+
exitCode: 1,
|
|
491
|
+
output: "",
|
|
492
|
+
stderr: message,
|
|
493
|
+
truncated: false,
|
|
494
|
+
durationMs: Date.now() - taskStart,
|
|
495
|
+
tokens: 0,
|
|
496
|
+
requests: 0,
|
|
497
|
+
modelOverride,
|
|
498
|
+
error: message,
|
|
499
|
+
};
|
|
500
|
+
},
|
|
534
501
|
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
mergeSummary =
|
|
502
|
+
})();
|
|
503
|
+
|
|
504
|
+
if (result.exitCode !== 0 || result.error || result.aborted) {
|
|
505
|
+
const failureMessage = buildSubagentFailureMessage(agentName, result);
|
|
506
|
+
const recoveryHint = isIsolated ? await buildIsolationRecoveryHint(result, artifactsDir) : "";
|
|
507
|
+
throw new ToolError(`${failureMessage}${recoveryHint}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let mergeSummary = "";
|
|
511
|
+
let changesApplied: boolean | null = null;
|
|
512
|
+
if (isIsolated && isolationContext) {
|
|
513
|
+
if (applyChanges) {
|
|
514
|
+
const outcome = await mergeIsolatedChanges({
|
|
515
|
+
result,
|
|
516
|
+
repoRoot: isolationContext.repoRoot,
|
|
517
|
+
mergeMode,
|
|
518
|
+
});
|
|
519
|
+
mergeSummary = outcome.summary;
|
|
520
|
+
changesApplied = outcome.changesApplied;
|
|
521
|
+
if (outcome.changesApplied === false) {
|
|
522
|
+
const summaryText = outcome.summary.trim();
|
|
523
|
+
const recoveryHint = await buildIsolationRecoveryHint(result, artifactsDir);
|
|
524
|
+
throw new ToolError(
|
|
525
|
+
`agent() isolated apply failed for ${result.id}${summaryText ? `: ${summaryText}` : ""}${recoveryHint}`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const nestedSummary = await applyEligibleNestedPatches({
|
|
530
|
+
result,
|
|
531
|
+
repoRoot: isolationContext.repoRoot,
|
|
532
|
+
mergeMode,
|
|
533
|
+
changesApplied: outcome.changesApplied,
|
|
534
|
+
mergedBranchForNestedPatches: outcome.mergedBranchForNestedPatches,
|
|
535
|
+
commitMessage: buildCommitMessage(),
|
|
536
|
+
});
|
|
537
|
+
mergeSummary += nestedSummary;
|
|
538
|
+
if (structured && nestedSummary.trim()) {
|
|
539
|
+
const recoveryHint = await buildIsolationRecoveryHint(
|
|
540
|
+
{ ...result, patchPath: undefined, branchName: undefined },
|
|
541
|
+
artifactsDir,
|
|
542
|
+
);
|
|
543
|
+
throw new ToolError(
|
|
544
|
+
`agent() isolated nested patch apply failed for ${result.id}: ${plainIsolationSummary(nestedSummary)}${recoveryHint}`,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
} else if (result.branchName) {
|
|
548
|
+
mergeSummary = `\n\nIsolation: changes captured on branch \`${result.branchName}\` (apply=false). Not merged.`;
|
|
549
|
+
} else if (result.patchPath) {
|
|
550
|
+
mergeSummary = `\n\nIsolation: changes captured at \`${result.patchPath}\` (apply=false). Not applied.`;
|
|
553
551
|
} else {
|
|
554
|
-
|
|
552
|
+
const nestedPatches = result.nestedPatches ?? [];
|
|
553
|
+
if (nestedPatches.length > 0) {
|
|
554
|
+
mergeSummary = `\n\nIsolation: changes captured for ${nestedPatches.length} nested repositor${nestedPatches.length === 1 ? "y" : "ies"} (apply=false). Not applied.`;
|
|
555
|
+
} else {
|
|
556
|
+
mergeSummary = "\n\nIsolation: no changes captured.";
|
|
557
|
+
}
|
|
555
558
|
}
|
|
556
559
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
560
|
+
|
|
561
|
+
// Clean up the temp artifacts dir we created for this call only when the
|
|
562
|
+
// caller will not need files from it later. Keep it when the runtime helper
|
|
563
|
+
// will return an `agent://` handle (the `.md`/`.jsonl` backing files live
|
|
564
|
+
// here) and on `apply=false` (`changesApplied === null`) where the caller
|
|
565
|
+
// consumes `details.patchPath` / `details.branchName` /
|
|
566
|
+
// `details.nestedPatches` out of band. Failed isolated applies throw
|
|
567
|
+
// earlier with a recovery hint, so they never reach this gate.
|
|
568
|
+
const shouldCleanupTempArtifacts =
|
|
569
|
+
tempArtifactsDir && !parsed.handle && (!isIsolated || changesApplied === true);
|
|
570
|
+
if (shouldCleanupTempArtifacts) {
|
|
571
|
+
await fs.rm(artifactsDir, { recursive: true, force: true });
|
|
572
|
+
unregisterArtifactsDir?.();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
options.session.recordEvalSubagentUsage?.(result.usage?.output ?? 0);
|
|
576
|
+
|
|
577
|
+
return { result, mergeSummary, changesApplied };
|
|
578
|
+
},
|
|
579
|
+
{ deferExternalAbort: true },
|
|
580
|
+
);
|
|
576
581
|
|
|
577
582
|
return {
|
|
578
583
|
text: structured ? result.output : result.output + mergeSummary,
|
package/src/eval/backend.ts
CHANGED
|
@@ -15,10 +15,10 @@ export interface ExecutorBackendExecOptions {
|
|
|
15
15
|
* driven entirely by `signal`, which the eval tool arms as a watchdog that
|
|
16
16
|
* pauses on bridge timeout-control status events and fires a `TimeoutError`
|
|
17
17
|
* reason only while the Python/JS runtime owns control. Backends use this
|
|
18
|
-
* value only for timeout-annotation text and as cold-start headroom;
|
|
19
|
-
* NOT derive a competing wall-clock timer from it.
|
|
18
|
+
* value only for timeout-annotation text and as cold-start headroom; undefined
|
|
19
|
+
* disables the cell timeout. Backends MUST NOT derive a competing wall-clock timer from it.
|
|
20
20
|
*/
|
|
21
|
-
idleTimeoutMs
|
|
21
|
+
idleTimeoutMs?: number;
|
|
22
22
|
reset: boolean;
|
|
23
23
|
onChunk: (chunk: string) => void;
|
|
24
24
|
/**
|
|
@@ -26,6 +26,15 @@ export function isEvalTimeoutControlEvent(event: JsStatusEvent): boolean {
|
|
|
26
26
|
return event.op === EVAL_TIMEOUT_PAUSE_OP || event.op === EVAL_TIMEOUT_RESUME_OP;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Optional behavior for a timeout pause around a host bridge call. */
|
|
30
|
+
export interface BridgeTimeoutPauseOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Marks the pause as an `agent()` call whose already-started work must finish
|
|
33
|
+
* before an external eval abort reaches the kernel.
|
|
34
|
+
*/
|
|
35
|
+
deferExternalAbort?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
/**
|
|
30
39
|
* Run {@link operation} while suspending the eval watchdog through
|
|
31
40
|
* {@link emitStatus}. A no-op wrapper when no status sink is wired.
|
|
@@ -33,12 +42,21 @@ export function isEvalTimeoutControlEvent(event: JsStatusEvent): boolean {
|
|
|
33
42
|
export async function withBridgeTimeoutPause<T>(
|
|
34
43
|
emitStatus: ((event: JsStatusEvent) => void) | undefined,
|
|
35
44
|
operation: () => Promise<T>,
|
|
45
|
+
options?: BridgeTimeoutPauseOptions,
|
|
36
46
|
): Promise<T> {
|
|
37
47
|
if (!emitStatus) return operation();
|
|
38
|
-
emitStatus(
|
|
48
|
+
emitStatus(
|
|
49
|
+
options?.deferExternalAbort
|
|
50
|
+
? { op: EVAL_TIMEOUT_PAUSE_OP, deferExternalAbort: true }
|
|
51
|
+
: { op: EVAL_TIMEOUT_PAUSE_OP },
|
|
52
|
+
);
|
|
39
53
|
try {
|
|
40
54
|
return await operation();
|
|
41
55
|
} finally {
|
|
42
|
-
emitStatus(
|
|
56
|
+
emitStatus(
|
|
57
|
+
options?.deferExternalAbort
|
|
58
|
+
? { op: EVAL_TIMEOUT_RESUME_OP, deferExternalAbort: true }
|
|
59
|
+
: { op: EVAL_TIMEOUT_RESUME_OP },
|
|
60
|
+
);
|
|
43
61
|
}
|
|
44
62
|
}
|