@oh-my-pi/pi-coding-agent 15.13.2 → 15.13.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 +22 -0
- package/dist/cli.js +147 -122
- package/dist/types/config/settings-schema.d.ts +31 -0
- package/dist/types/eval/js/context-manager.d.ts +15 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/session/unexpected-stop-classifier.d.ts +13 -0
- package/dist/types/stt/asr-client.d.ts +1 -1
- package/dist/types/tiny/title-client.d.ts +1 -1
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/tts/tts-client.d.ts +1 -1
- package/dist/types/utils/thinking-display.d.ts +1 -17
- package/package.json +12 -12
- package/src/cli.ts +25 -12
- package/src/config/model-registry.ts +6 -2
- package/src/config/settings-schema.ts +25 -0
- package/src/eval/__tests__/agent-bridge.test.ts +106 -46
- package/src/eval/__tests__/js-context-manager.test.ts +12 -2
- package/src/eval/js/context-manager.ts +40 -3
- package/src/eval/js/worker-entry.ts +7 -0
- package/src/export/html/template.js +18 -22
- package/src/internal-urls/docs-index.generated.ts +5 -3
- package/src/main.ts +15 -5
- package/src/modes/acp/acp-agent.ts +2 -2
- package/src/modes/acp/acp-event-mapper.ts +2 -2
- package/src/modes/components/agent-hub.ts +31 -7
- package/src/modes/components/assistant-message.ts +24 -15
- package/src/modes/components/snapcompact-shape-preview-doc.md +2 -2
- package/src/modes/components/snapcompact-shape-preview.ts +2 -2
- package/src/modes/components/tree-selector.ts +3 -2
- package/src/modes/controllers/event-controller.ts +3 -3
- package/src/modes/controllers/input-controller.ts +7 -1
- package/src/modes/controllers/streaming-reveal.ts +4 -4
- package/src/modes/interactive-mode.ts +2 -0
- package/src/modes/types.ts +6 -0
- package/src/modes/utils/ui-helpers.ts +3 -3
- package/src/prompts/agents/oracle.md +0 -1
- package/src/prompts/agents/reviewer.md +0 -1
- package/src/prompts/system/unexpected-stop-classifier.md +17 -0
- package/src/prompts/system/unexpected-stop-retry.md +4 -0
- package/src/session/agent-session.ts +164 -10
- package/src/session/session-dump-format.ts +8 -19
- package/src/session/unexpected-stop-classifier.ts +129 -0
- package/src/stt/asr-client.ts +1 -1
- package/src/tiny/title-client.ts +1 -1
- package/src/tools/browser/tab-supervisor.ts +1 -1
- package/src/tools/browser/tab-worker-entry.ts +12 -4
- package/src/tools/job.ts +1 -0
- package/src/tts/tts-client.ts +1 -1
- package/src/utils/thinking-display.ts +8 -34
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "bun:test";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
2
|
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { Settings } from "../../config/settings";
|
|
4
4
|
import type { ToolSession } from "../../tools";
|
|
5
|
-
import { disposeAllVmContexts } from "../js/context-manager";
|
|
5
|
+
import { disposeAllVmContexts, setWorkerCloseTimeoutMsForTests } from "../js/context-manager";
|
|
6
6
|
import { executeJs } from "../js/executor";
|
|
7
7
|
|
|
8
8
|
const originalWorker = globalThis.Worker;
|
|
@@ -180,8 +180,18 @@ function installFakeWorker(stats: FakeWorkerStats, behavior: FakeWorkerBehavior)
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
describe("JavaScript eval worker lifecycle", () => {
|
|
183
|
+
let restoreCloseTimeoutMs = 0;
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
// Shrink the graceful-close grace period so the "close acked but the worker
|
|
186
|
+
// never exits -> force terminate" contract is proven without a real 1s wait.
|
|
187
|
+
restoreCloseTimeoutMs = setWorkerCloseTimeoutMsForTests(1);
|
|
188
|
+
});
|
|
189
|
+
|
|
183
190
|
afterEach(async () => {
|
|
191
|
+
// Dispose while the shrunk timeout is still active so a hung worker's afterEach
|
|
192
|
+
// close also force-terminates instantly, then restore the production default.
|
|
184
193
|
await disposeAllVmContexts();
|
|
194
|
+
setWorkerCloseTimeoutMsForTests(restoreCloseTimeoutMs);
|
|
185
195
|
Object.defineProperty(globalThis, "Worker", {
|
|
186
196
|
configurable: true,
|
|
187
197
|
writable: true,
|
|
@@ -60,6 +60,22 @@ const resettingSessions = new Map<string, Promise<void>>();
|
|
|
60
60
|
// SIGILL/SIGSEGV. Callers that pass a larger per-cell budget still dominate.
|
|
61
61
|
const WORKER_INIT_TIMEOUT_MS = 15_000;
|
|
62
62
|
const WORKER_CLOSE_TIMEOUT_MS = 1_000;
|
|
63
|
+
// Active graceful-close grace period before a worker that ack'd `close` but never
|
|
64
|
+
// emitted its `close` event is force-terminated. Defaults to the production floor;
|
|
65
|
+
// tests override it (and restore it) to exercise the close-timeout -> terminate
|
|
66
|
+
// path without a real wall-clock wait.
|
|
67
|
+
let workerCloseTimeoutMs: number = WORKER_CLOSE_TIMEOUT_MS;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Test-only seam: override the graceful-close grace period (ms). Returns the
|
|
71
|
+
* previous value so callers can restore it. Production always uses
|
|
72
|
+
* {@link WORKER_CLOSE_TIMEOUT_MS}; never call this outside tests.
|
|
73
|
+
*/
|
|
74
|
+
export function setWorkerCloseTimeoutMsForTests(ms: number): number {
|
|
75
|
+
const previous = workerCloseTimeoutMs;
|
|
76
|
+
workerCloseTimeoutMs = ms;
|
|
77
|
+
return previous;
|
|
78
|
+
}
|
|
63
79
|
|
|
64
80
|
export async function executeInVmContext(options: {
|
|
65
81
|
sessionKey: string;
|
|
@@ -125,6 +141,27 @@ export async function disposeAllVmContexts(): Promise<void> {
|
|
|
125
141
|
await Promise.all(all.map(session => killSession(session, new ToolError("JS context disposed"), { force: false })));
|
|
126
142
|
}
|
|
127
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Smoke probe: spawn the JS eval worker through the worker-host entry and prove
|
|
146
|
+
* it answers the `init` handshake on a real worker thread (not the inline
|
|
147
|
+
* fallback). Catches the silent worker-load and init-message-drop regressions
|
|
148
|
+
* that otherwise strand every cell on the init timeout in a distribution build —
|
|
149
|
+
* the failure mode that motivated `installWorkerInbox`. Wired into
|
|
150
|
+
* `omp --smoke-test` so binary / source / tarball installs all exercise it.
|
|
151
|
+
*/
|
|
152
|
+
export async function smokeTestJsEvalWorker(): Promise<void> {
|
|
153
|
+
const worker = spawnJsWorker();
|
|
154
|
+
const session: JsSession = { sessionKey: "smoke", worker, state: "alive", pending: new Map() };
|
|
155
|
+
try {
|
|
156
|
+
await initWorker(session, { cwd: process.cwd(), sessionId: "smoke" }, WORKER_INIT_TIMEOUT_MS);
|
|
157
|
+
if (worker.mode !== "worker") {
|
|
158
|
+
throw new Error("JS eval worker smoke fell back to the inline worker (real worker failed to start)");
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
await worker.terminate().catch(() => undefined);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
128
165
|
async function runOnce(
|
|
129
166
|
session: JsSession,
|
|
130
167
|
options: {
|
|
@@ -431,7 +468,7 @@ function spawnJsWorker(): WorkerHandle {
|
|
|
431
468
|
try {
|
|
432
469
|
const hostEntry = workerHostEntry();
|
|
433
470
|
const worker = hostEntry
|
|
434
|
-
? new Worker(hostEntry, { type: "module", argv: ["
|
|
471
|
+
? new Worker(hostEntry, { type: "module", argv: ["__omp_worker_js_eval"] })
|
|
435
472
|
: new Worker(new URL("./worker-entry.ts", import.meta.url).href, { type: "module" });
|
|
436
473
|
return wrapBunWorker(worker);
|
|
437
474
|
} catch (err) {
|
|
@@ -492,7 +529,7 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
|
|
|
492
529
|
finishIfClosed();
|
|
493
530
|
});
|
|
494
531
|
worker.addEventListener("close", onClose);
|
|
495
|
-
timeout = setTimeout(() => finish(false),
|
|
532
|
+
timeout = setTimeout(() => finish(false), workerCloseTimeoutMs);
|
|
496
533
|
worker.postMessage({ type: "close" } satisfies WorkerInbound);
|
|
497
534
|
return await closed;
|
|
498
535
|
},
|
|
@@ -557,7 +594,7 @@ function spawnInlineWorker(): WorkerHandle {
|
|
|
557
594
|
if (msg.type === "closed") finish(true);
|
|
558
595
|
});
|
|
559
596
|
this.send({ type: "close" });
|
|
560
|
-
timeout = setTimeout(() => finish(false),
|
|
597
|
+
timeout = setTimeout(() => finish(false), workerCloseTimeoutMs);
|
|
561
598
|
return await closed;
|
|
562
599
|
},
|
|
563
600
|
async terminate() {
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
|
|
2
3
|
import { WorkerCore } from "./worker-core";
|
|
3
4
|
import type { Transport, WorkerInbound, WorkerOutbound } from "./worker-protocol";
|
|
4
5
|
|
|
5
6
|
if (!parentPort) throw new Error("js worker-entry: missing parentPort");
|
|
6
7
|
|
|
7
8
|
const port = parentPort;
|
|
9
|
+
// When the CLI host pre-buffered messages (it imports this module dynamically),
|
|
10
|
+
// bind that inbox so the parent's already-delivered `init` is replayed. Loaded
|
|
11
|
+
// directly (test/SDK fallback), this module's top-level runs synchronously at
|
|
12
|
+
// worker start, so the direct `parentPort.on` below wins the flush on its own.
|
|
13
|
+
const inbox = consumeWorkerInbox();
|
|
8
14
|
const transport: Transport = {
|
|
9
15
|
send: (msg: WorkerOutbound) => port.postMessage(msg),
|
|
10
16
|
onMessage: handler => {
|
|
17
|
+
if (inbox) return inbox.bind(data => handler(data as WorkerInbound));
|
|
11
18
|
const wrap = (data: unknown): void => handler(data as WorkerInbound);
|
|
12
19
|
port.on("message", wrap);
|
|
13
20
|
return () => port.off("message", wrap);
|
|
@@ -278,10 +278,12 @@
|
|
|
278
278
|
let searchQuery = '';
|
|
279
279
|
|
|
280
280
|
function hasTextContent(content) {
|
|
281
|
-
if (typeof content === 'string') return content
|
|
281
|
+
if (typeof content === 'string') return Boolean(canonicalizeMessage(content));
|
|
282
282
|
if (Array.isArray(content)) {
|
|
283
283
|
for (const c of content) {
|
|
284
|
-
if (c.type === 'text' && c.text
|
|
284
|
+
if (c.type === 'text' && c.text) {
|
|
285
|
+
if (canonicalizeMessage(c.text)) return true;
|
|
286
|
+
}
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
289
|
return false;
|
|
@@ -450,24 +452,16 @@
|
|
|
450
452
|
return div.innerHTML;
|
|
451
453
|
}
|
|
452
454
|
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
455
|
+
function canonicalizeMessage(text) {
|
|
456
|
+
if (!text) return '';
|
|
457
|
+
const trimmed = text.trim();
|
|
458
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
459
|
+
const code = trimmed.charCodeAt(i);
|
|
460
|
+
if (code !== 0x2e && code !== 0x2026 && code !== 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) {
|
|
461
|
+
return trimmed;
|
|
460
462
|
}
|
|
461
|
-
if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) continue;
|
|
462
|
-
return false;
|
|
463
463
|
}
|
|
464
|
-
return
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function visibleThinkingText(block) {
|
|
468
|
-
const text = block.thinking.trim();
|
|
469
|
-
if (!text) return '';
|
|
470
|
-
return isDotOnlyThinking(text) ? '' : text;
|
|
464
|
+
return '';
|
|
471
465
|
}
|
|
472
466
|
|
|
473
467
|
/**
|
|
@@ -1074,10 +1068,13 @@
|
|
|
1074
1068
|
let html = `<div class="assistant-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
|
1075
1069
|
|
|
1076
1070
|
for (const block of msg.content) {
|
|
1077
|
-
if (block.type === 'text'
|
|
1078
|
-
|
|
1071
|
+
if (block.type === 'text') {
|
|
1072
|
+
const canon = canonicalizeMessage(block.text);
|
|
1073
|
+
if (canon) {
|
|
1074
|
+
html += `<div class="assistant-text markdown-content">${safeMarkedParse(block.text)}</div>`;
|
|
1075
|
+
}
|
|
1079
1076
|
} else if (block.type === 'thinking') {
|
|
1080
|
-
const thinking =
|
|
1077
|
+
const thinking = canonicalizeMessage(block.thinking);
|
|
1081
1078
|
if (!thinking) continue;
|
|
1082
1079
|
html += `<div class="thinking-block">
|
|
1083
1080
|
<div class="thinking-text">${escapeHtml(thinking)}</div>
|
|
@@ -1085,7 +1082,6 @@
|
|
|
1085
1082
|
</div>`;
|
|
1086
1083
|
}
|
|
1087
1084
|
}
|
|
1088
|
-
|
|
1089
1085
|
for (const block of msg.content) {
|
|
1090
1086
|
if (block.type === 'toolCall') {
|
|
1091
1087
|
html += renderToolCall(block, sctx);
|