@oh-my-pi/pi-coding-agent 16.0.6 → 16.0.8
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 +38 -0
- package/dist/cli.js +4760 -12462
- package/dist/types/cli/update-cli.d.ts +11 -0
- package/dist/types/debug/remote-debugger.d.ts +45 -0
- package/dist/types/internal-urls/docs-index.d.ts +19 -0
- package/dist/types/markit/converters/docx.d.ts +6 -0
- package/dist/types/markit/converters/epub.d.ts +15 -0
- package/dist/types/markit/converters/pdf/columns.d.ts +35 -0
- package/dist/types/markit/converters/pdf/extract.d.ts +10 -0
- package/dist/types/markit/converters/pdf/grid.d.ts +25 -0
- package/dist/types/markit/converters/pdf/headers.d.ts +24 -0
- package/dist/types/markit/converters/pdf/index.d.ts +6 -0
- package/dist/types/markit/converters/pdf/render.d.ts +24 -0
- package/dist/types/markit/converters/pdf/types.d.ts +75 -0
- package/dist/types/markit/converters/pptx.d.ts +57 -0
- package/dist/types/markit/converters/xlsx.d.ts +25 -0
- package/dist/types/markit/index.d.ts +2 -0
- package/dist/types/markit/registry.d.ts +16 -0
- package/dist/types/markit/types.d.ts +30 -0
- package/dist/types/session/agent-session.d.ts +7 -8
- package/dist/types/session/auth-storage.d.ts +3 -2
- package/dist/types/session/yield-queue.d.ts +3 -1
- package/dist/types/tools/browser/attach.d.ts +1 -1
- package/dist/types/utils/markit.d.ts +0 -8
- package/dist/types/utils/mupdf-wasm-embed.d.ts +1 -0
- package/dist/types/utils/turndown.d.ts +15 -0
- package/dist/types/utils/zip.d.ts +119 -0
- package/package.json +20 -18
- package/scripts/build-binary.ts +7 -3
- package/scripts/bundle-dist.ts +28 -12
- package/scripts/embed-mupdf-wasm.ts +67 -0
- package/scripts/generate-docs-index.ts +48 -32
- package/scripts/omp +1 -1
- package/src/advisor/__tests__/advisor.test.ts +83 -0
- package/src/advisor/runtime.ts +16 -1
- package/src/cli/auth-broker-cli.ts +1 -3
- package/src/cli/auth-gateway-cli.ts +2 -5
- package/src/cli/update-cli.ts +63 -3
- package/src/config/model-discovery.ts +20 -8
- package/src/config/models-config-schema.ts +8 -1
- package/src/debug/index.ts +44 -0
- package/src/debug/remote-debugger.ts +151 -0
- package/src/debug/report-bundle.ts +2 -1
- package/src/internal-urls/docs-index.generated.txt +2 -0
- package/src/internal-urls/docs-index.ts +102 -0
- package/src/internal-urls/omp-protocol.ts +10 -9
- package/src/markit/NOTICE +32 -0
- package/src/markit/converters/docx.ts +56 -0
- package/src/markit/converters/epub.ts +136 -0
- package/src/markit/converters/mammoth.d.ts +24 -0
- package/src/markit/converters/pdf/columns.ts +103 -0
- package/src/markit/converters/pdf/extract.ts +574 -0
- package/src/markit/converters/pdf/grid.ts +780 -0
- package/src/markit/converters/pdf/headers.ts +106 -0
- package/src/markit/converters/pdf/index.ts +146 -0
- package/src/markit/converters/pdf/render.ts +501 -0
- package/src/markit/converters/pdf/types.ts +84 -0
- package/src/markit/converters/pptx.ts +325 -0
- package/src/markit/converters/xlsx.ts +173 -0
- package/src/markit/index.ts +2 -0
- package/src/markit/registry.ts +59 -0
- package/src/markit/types.ts +35 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +14 -7
- package/src/modes/components/snapcompact-shape-preview.ts +2 -2
- package/src/modes/controllers/input-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +26 -9
- package/src/prompts/advisor/system.md +1 -0
- package/src/sdk.ts +5 -9
- package/src/session/agent-session.ts +75 -40
- package/src/session/auth-storage.ts +2 -11
- package/src/session/yield-queue.ts +7 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/tools/browser/attach.ts +2 -2
- package/src/tools/fetch.ts +25 -60
- package/src/tools/read.ts +1 -1
- package/src/tools/search.ts +1 -6
- package/src/tools/write.ts +25 -65
- package/src/utils/markit.ts +25 -9
- package/src/utils/mupdf-wasm-embed.ts +12 -0
- package/src/utils/tools-manager.ts +2 -11
- package/src/utils/turndown.ts +83 -0
- package/src/{tools/archive-reader.ts → utils/zip.ts} +453 -83
- package/src/web/scrapers/types.ts +3 -46
- package/dist/types/internal-urls/docs-index.generated.d.ts +0 -2
- package/dist/types/tools/archive-reader.d.ts +0 -49
- package/src/internal-urls/docs-index.generated.ts +0 -120
|
@@ -44,6 +44,26 @@ function hasPasteText(value: unknown): value is PasteTarget {
|
|
|
44
44
|
return typeof value === "object" && value !== null && typeof (value as PasteTarget).pasteText === "function";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function pythonCommandPrefixLength(trimmedText: string): 0 | 1 | 2 {
|
|
48
|
+
if (trimmedText.charCodeAt(0) !== 36 /* $ */) return 0;
|
|
49
|
+
if (trimmedText.charCodeAt(1) === 123 /* { */) return 0;
|
|
50
|
+
|
|
51
|
+
const prefixLength = trimmedText.charCodeAt(1) === 36 /* $ */ ? 2 : 1;
|
|
52
|
+
const next = trimmedText.charCodeAt(prefixLength);
|
|
53
|
+
if (Number.isNaN(next)) return prefixLength;
|
|
54
|
+
return next === 32 || next === 9 || next === 10 || next === 13 ? prefixLength : 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parsePythonCommandInput(text: string): { code: string; isExcluded: boolean } | undefined {
|
|
58
|
+
const trimmed = text.trimStart();
|
|
59
|
+
const prefixLength = pythonCommandPrefixLength(trimmed);
|
|
60
|
+
if (prefixLength === 0) return undefined;
|
|
61
|
+
return {
|
|
62
|
+
code: trimmed.slice(prefixLength).trim(),
|
|
63
|
+
isExcluded: prefixLength === 2,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
/** Wrap pasted text in `<attachment>` tags so the model treats it as one quoted block. */
|
|
48
68
|
function wrapPasteInAttachmentBlock(content: string): string {
|
|
49
69
|
return `<attachment>\n${content}\n</attachment>`;
|
|
@@ -381,8 +401,8 @@ export class InputController {
|
|
|
381
401
|
const wasBashMode = this.ctx.isBashMode;
|
|
382
402
|
const wasPythonMode = this.ctx.isPythonMode;
|
|
383
403
|
const trimmed = text.trimStart();
|
|
384
|
-
this.ctx.isBashMode =
|
|
385
|
-
this.ctx.isPythonMode = trimmed
|
|
404
|
+
this.ctx.isBashMode = trimmed.startsWith("!");
|
|
405
|
+
this.ctx.isPythonMode = pythonCommandPrefixLength(trimmed) > 0;
|
|
386
406
|
if (wasBashMode !== this.ctx.isBashMode || wasPythonMode !== this.ctx.isPythonMode) {
|
|
387
407
|
this.ctx.updateEditorBorderColor();
|
|
388
408
|
}
|
|
@@ -550,7 +570,7 @@ export class InputController {
|
|
|
550
570
|
this.ctx.editor.setText("");
|
|
551
571
|
return;
|
|
552
572
|
}
|
|
553
|
-
if (text.startsWith("!") || text
|
|
573
|
+
if (text.startsWith("!") || parsePythonCommandInput(text)) {
|
|
554
574
|
this.ctx.showStatus("Local execution is host-only during a collab session");
|
|
555
575
|
this.ctx.editor.setText("");
|
|
556
576
|
return;
|
|
@@ -598,10 +618,11 @@ export class InputController {
|
|
|
598
618
|
}
|
|
599
619
|
}
|
|
600
620
|
|
|
601
|
-
// Handle python command (
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
621
|
+
// Handle python command (`$ <code>` for normal, `$$ <code>` for excluded from context).
|
|
622
|
+
// Shell-style variables such as `$HOME` are normal prose unless a space follows the sigil.
|
|
623
|
+
const pythonCommand = parsePythonCommandInput(text);
|
|
624
|
+
if (pythonCommand) {
|
|
625
|
+
const { code, isExcluded } = pythonCommand;
|
|
605
626
|
if (code) {
|
|
606
627
|
if (this.ctx.session.isEvalRunning) {
|
|
607
628
|
this.ctx.showWarning("A Python execution is already running. Press Esc to cancel it first.");
|
|
@@ -768,7 +789,7 @@ export class InputController {
|
|
|
768
789
|
}
|
|
769
790
|
return;
|
|
770
791
|
}
|
|
771
|
-
if (text.startsWith("/") || text.startsWith("!") || text
|
|
792
|
+
if (text.startsWith("/") || text.startsWith("!") || parsePythonCommandInput(text)) {
|
|
772
793
|
this.ctx.showStatus("Commands run in the main session — press ←← to return first");
|
|
773
794
|
return; // editor text not cleared: Editor does not auto-clear on submit
|
|
774
795
|
}
|
|
@@ -2309,7 +2309,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2309
2309
|
// Branchless mark+clear when !compactBeforeExecute: mark is gated; clear
|
|
2310
2310
|
// is unconditional and idempotent.
|
|
2311
2311
|
if (options.compactBeforeExecute) {
|
|
2312
|
-
this.session.
|
|
2312
|
+
this.session.markPlanInternalAbortPending();
|
|
2313
2313
|
}
|
|
2314
2314
|
let compactOutcome: CompactionOutcome | undefined;
|
|
2315
2315
|
try {
|
|
@@ -2355,7 +2355,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2355
2355
|
// (i.e., the !compactBeforeExecute branch), and a no-op when the flag
|
|
2356
2356
|
// was already consumed by AgentSession.#handleAgentEvent's aborted
|
|
2357
2357
|
// message_end stamping. Guarantees the flag is dead at every exit.
|
|
2358
|
-
this.session.
|
|
2358
|
+
this.session.clearPlanInternalAbortPending();
|
|
2359
2359
|
}
|
|
2360
2360
|
|
|
2361
2361
|
// Tool restoration runs on every path — the plan mode tools must be
|
|
@@ -2421,10 +2421,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2421
2421
|
// in-flight turn first — abort() bumps the prompt generation and cancels pending
|
|
2422
2422
|
// continuations, so nothing re-streams in the synchronous gap before prompt().
|
|
2423
2423
|
if (this.session.isStreaming) {
|
|
2424
|
-
await this
|
|
2424
|
+
await this.#abortPlanApprovalTurnSilently();
|
|
2425
2425
|
}
|
|
2426
2426
|
await this.session.prompt(planModePrompt, { synthetic: true });
|
|
2427
2427
|
}
|
|
2428
|
+
async #abortPlanApprovalTurnSilently(): Promise<void> {
|
|
2429
|
+
this.session.markPlanInternalAbortPending();
|
|
2430
|
+
try {
|
|
2431
|
+
await this.session.abort();
|
|
2432
|
+
} finally {
|
|
2433
|
+
this.session.clearPlanInternalAbortPending();
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2428
2436
|
|
|
2429
2437
|
async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
|
|
2430
2438
|
if (this.goalModeEnabled || this.goalModePaused) {
|
|
@@ -2807,7 +2815,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2807
2815
|
// plan) while the popup is showing. The event listener fires asynchronously
|
|
2808
2816
|
// (agent's #emit is fire-and-forget), so without this the model sees
|
|
2809
2817
|
// "Plan ready for approval." and immediately re-invokes `resolve` in a loop.
|
|
2810
|
-
|
|
2818
|
+
// This abort is an internal UI transition, not operator cancellation.
|
|
2819
|
+
await this.#abortPlanApprovalTurnSilently();
|
|
2811
2820
|
|
|
2812
2821
|
const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
|
|
2813
2822
|
this.planModePlanFilePath = planFilePath;
|
|
@@ -2912,11 +2921,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2912
2921
|
}
|
|
2913
2922
|
|
|
2914
2923
|
if (choice === "Refine plan") {
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2924
|
+
const refinement = feedback.trim();
|
|
2925
|
+
try {
|
|
2926
|
+
if (refinement) {
|
|
2927
|
+
if (this.onInputCallback) {
|
|
2928
|
+
this.onInputCallback(this.startPendingSubmission({ text: feedback }));
|
|
2929
|
+
} else {
|
|
2930
|
+
await this.session.prompt(feedback);
|
|
2931
|
+
}
|
|
2932
|
+
} else {
|
|
2933
|
+
this.showStatus("Refine plan: enter a follow-up prompt.");
|
|
2934
|
+
}
|
|
2935
|
+
} catch (error) {
|
|
2936
|
+
this.showError(`Failed to refine plan: ${error instanceof Error ? error.message : String(error)}`);
|
|
2920
2937
|
}
|
|
2921
2938
|
return;
|
|
2922
2939
|
}
|
|
@@ -15,6 +15,7 @@ Keep exploration lean — 2–3 calls per advise unless you've spotted a critica
|
|
|
15
15
|
|
|
16
16
|
<communication>
|
|
17
17
|
At most one `advise` per update. Prefer silence when the agent is on track. Address the agent directly. Offer alternatives, not lectures. Never restate what they know; never explain how to use the advisor.
|
|
18
|
+
Do not comment merely to add insight, context, or a second opinion. NEVER restate information the agent already has, including tool or CLI errors returned directly to it. NEVER flag a problem that will surface on its own — type errors, LSP diagnostics, failed builds, failing tests, lint — the agent's own tooling catches those. NEVER repeat advice you already gave.
|
|
18
19
|
</communication>
|
|
19
20
|
|
|
20
21
|
<critical>
|
package/src/sdk.ts
CHANGED
|
@@ -56,6 +56,10 @@ import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate
|
|
|
56
56
|
import { Settings, type SkillsSettings } from "./config/settings";
|
|
57
57
|
import { CursorExecHandlers } from "./cursor";
|
|
58
58
|
import "./discovery";
|
|
59
|
+
import { AuthBrokerClient } from "@oh-my-pi/pi-ai/auth-broker/client";
|
|
60
|
+
import { RemoteAuthCredentialStore } from "@oh-my-pi/pi-ai/auth-broker/remote-store";
|
|
61
|
+
import { readAuthBrokerSnapshotCache, writeAuthBrokerSnapshotCache } from "@oh-my-pi/pi-ai/auth-broker/snapshot-cache";
|
|
62
|
+
import { DEFAULT_SNAPSHOT_CACHE_TTL_MS, type SnapshotResponse } from "@oh-my-pi/pi-ai/auth-broker/types";
|
|
59
63
|
import { resolveConfigValue } from "./config/resolve-config-value";
|
|
60
64
|
import { initializeWithSettings } from "./discovery";
|
|
61
65
|
import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
|
|
@@ -116,15 +120,7 @@ import {
|
|
|
116
120
|
} from "./secrets";
|
|
117
121
|
import { AgentSession } from "./session/agent-session";
|
|
118
122
|
import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
|
|
119
|
-
import {
|
|
120
|
-
AuthBrokerClient,
|
|
121
|
-
AuthStorage,
|
|
122
|
-
DEFAULT_SNAPSHOT_CACHE_TTL_MS,
|
|
123
|
-
RemoteAuthCredentialStore,
|
|
124
|
-
readAuthBrokerSnapshotCache,
|
|
125
|
-
type SnapshotResponse,
|
|
126
|
-
writeAuthBrokerSnapshotCache,
|
|
127
|
-
} from "./session/auth-storage";
|
|
123
|
+
import { AuthStorage } from "./session/auth-storage";
|
|
128
124
|
import {
|
|
129
125
|
type CustomMessage,
|
|
130
126
|
convertToLlm,
|
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
type CompactionSummaryMessage,
|
|
35
35
|
countTokens,
|
|
36
36
|
resolveTelemetry,
|
|
37
|
-
STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL,
|
|
38
37
|
ThinkingLevel,
|
|
39
38
|
} from "@oh-my-pi/pi-agent-core";
|
|
40
39
|
import {
|
|
@@ -1235,13 +1234,12 @@ export class AgentSession {
|
|
|
1235
1234
|
#ttsrResumePromise: Promise<void> | undefined = undefined;
|
|
1236
1235
|
#ttsrResumeResolve: (() => void) | undefined = undefined;
|
|
1237
1236
|
|
|
1238
|
-
/** One-shot flag
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
* later unrelated aborts
|
|
1243
|
-
|
|
1244
|
-
#planCompactAbortPending = false;
|
|
1237
|
+
/** One-shot flag for expected internal plan-mode aborts. Approval actions may
|
|
1238
|
+
* abort the post-`resolve` continuation before compaction, execution, or
|
|
1239
|
+
* manual refinement. Consumed inside `#handleAgentEvent` for the matching
|
|
1240
|
+
* `message_end` + `stopReason: "aborted"`; callers clear it in `finally` so
|
|
1241
|
+
* it cannot leak into later unrelated aborts. */
|
|
1242
|
+
#planInternalAbortPending = false;
|
|
1245
1243
|
|
|
1246
1244
|
#postPromptTasks = new Set<Promise<unknown>>();
|
|
1247
1245
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
@@ -1677,6 +1675,33 @@ export class AgentSession {
|
|
|
1677
1675
|
this.#advisorInterruptImmuneTurnStart = this.#advisorPrimaryTurnsCompleted + 1;
|
|
1678
1676
|
}
|
|
1679
1677
|
|
|
1678
|
+
/**
|
|
1679
|
+
* Re-prime the advisor across a conversation boundary: `/new`, `/branch`,
|
|
1680
|
+
* `/btw`, `/tree`, and session switch/resume. Beyond {@link AdvisorRuntime.reset}
|
|
1681
|
+
* (which only re-primes the advisor's transcript view and is also fired by
|
|
1682
|
+
* within-conversation rewrites like compaction/shake/rewind), this clears the
|
|
1683
|
+
* session-level interrupt latches so the prior conversation's cooldown cannot
|
|
1684
|
+
* leak into the new one: the post-interrupt immune-turn window
|
|
1685
|
+
* (`#advisorPrimaryTurnsCompleted`, `#advisorInterruptImmuneTurnStart`) and the
|
|
1686
|
+
* user-interrupt auto-resume suppression flag. It also drops advisor deliveries
|
|
1687
|
+
* still queued against the prior conversation — pending asides in the yield
|
|
1688
|
+
* queue (advisor entries use `skipIdleFlush`, so they linger until the next
|
|
1689
|
+
* `drainLazy` rather than self-flushing), interrupting cards parked in the
|
|
1690
|
+
* agent steer/follow-up queues, and preserved cards deferred to the next turn —
|
|
1691
|
+
* so none of them inject into the new conversation.
|
|
1692
|
+
*/
|
|
1693
|
+
#resetAdvisorSessionState(): void {
|
|
1694
|
+
this.#advisorRuntime?.reset();
|
|
1695
|
+
this.#advisorPrimaryTurnsCompleted = 0;
|
|
1696
|
+
this.#advisorInterruptImmuneTurnStart = undefined;
|
|
1697
|
+
this.#advisorAutoResumeSuppressed = false;
|
|
1698
|
+
this.yieldQueue.clear("advisor");
|
|
1699
|
+
this.#extractQueuedAdvisorCards();
|
|
1700
|
+
if (this.#pendingNextTurnMessages.some(isAdvisorCard)) {
|
|
1701
|
+
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(m => !isAdvisorCard(m));
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1680
1705
|
#buildAdvisorRuntime(seedToCurrent = false): boolean {
|
|
1681
1706
|
if (this.#isDisposed) return false;
|
|
1682
1707
|
if (this.#advisorRuntime) return true;
|
|
@@ -2115,25 +2140,24 @@ export class AgentSession {
|
|
|
2115
2140
|
return this.#ttsrAbortPending;
|
|
2116
2141
|
}
|
|
2117
2142
|
|
|
2118
|
-
/** Whether
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
return this.#planCompactAbortPending;
|
|
2143
|
+
/** Whether an expected internal plan-mode abort is pending. Consumed by
|
|
2144
|
+
* `#handleAgentEvent` to stamp `SILENT_ABORT_MARKER` on the next aborted
|
|
2145
|
+
* assistant message_end; callers clear it in `finally`. */
|
|
2146
|
+
get isPlanInternalAbortPending(): boolean {
|
|
2147
|
+
return this.#planInternalAbortPending;
|
|
2124
2148
|
}
|
|
2125
2149
|
|
|
2126
2150
|
/** Arm the silent-abort marker for the next aborted assistant message_end.
|
|
2127
|
-
* Caller MUST clear via `
|
|
2151
|
+
* Caller MUST clear via `clearPlanInternalAbortPending()` in a `finally`
|
|
2128
2152
|
* to guarantee no leak. */
|
|
2129
|
-
|
|
2130
|
-
this.#
|
|
2153
|
+
markPlanInternalAbortPending(): void {
|
|
2154
|
+
this.#planInternalAbortPending = true;
|
|
2131
2155
|
}
|
|
2132
2156
|
|
|
2133
2157
|
/** Unconditionally clear the silent-abort flag. Idempotent: safe when the
|
|
2134
2158
|
* flag was never set OR was already consumed by `#handleAgentEvent`. */
|
|
2135
|
-
|
|
2136
|
-
this.#
|
|
2159
|
+
clearPlanInternalAbortPending(): void {
|
|
2160
|
+
this.#planInternalAbortPending = false;
|
|
2137
2161
|
}
|
|
2138
2162
|
|
|
2139
2163
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
@@ -2283,7 +2307,7 @@ export class AgentSession {
|
|
|
2283
2307
|
};
|
|
2284
2308
|
|
|
2285
2309
|
#processAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
2286
|
-
// Plan-mode
|
|
2310
|
+
// Plan-mode internal transition: stamp `SILENT_ABORT_MARKER` on the
|
|
2287
2311
|
// persisted message BEFORE the obfuscator's display-side copy below.
|
|
2288
2312
|
// Invariant (must hold across refactors): this branch precedes the
|
|
2289
2313
|
// `let displayEvent = event; ... displayEvent = { ...event, message: { ...message, content: deobfuscated } }`
|
|
@@ -2291,18 +2315,16 @@ export class AgentSession {
|
|
|
2291
2315
|
// and `event.message` (in-place mutation, used by SessionManager
|
|
2292
2316
|
// persistence) carry the marker, guaranteeing streaming render and
|
|
2293
2317
|
// history replay branch identically. The one-shot flag is consumed
|
|
2294
|
-
// here, scoped strictly to this aborted message_end;
|
|
2295
|
-
// `finally`
|
|
2296
|
-
// every terminal compaction outcome (`ok` / `cancelled` / `failed` /
|
|
2297
|
-
// throw) so a leaked flag cannot silence a later unrelated abort.
|
|
2318
|
+
// here, scoped strictly to this aborted message_end; callers still clear it
|
|
2319
|
+
// in `finally` so a leaked flag cannot silence a later unrelated abort.
|
|
2298
2320
|
if (
|
|
2299
2321
|
event.type === "message_end" &&
|
|
2300
2322
|
event.message.role === "assistant" &&
|
|
2301
2323
|
event.message.stopReason === "aborted" &&
|
|
2302
|
-
this.#
|
|
2324
|
+
this.#planInternalAbortPending
|
|
2303
2325
|
) {
|
|
2304
2326
|
(event.message as AssistantMessage).errorMessage = SILENT_ABORT_MARKER;
|
|
2305
|
-
this.#
|
|
2327
|
+
this.#planInternalAbortPending = false;
|
|
2306
2328
|
}
|
|
2307
2329
|
|
|
2308
2330
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
@@ -2499,6 +2521,13 @@ export class AgentSession {
|
|
|
2499
2521
|
});
|
|
2500
2522
|
this.#retryAttempt = 0;
|
|
2501
2523
|
}
|
|
2524
|
+
if (assistantMsg.provider === "opencode-go") {
|
|
2525
|
+
this.#modelRegistry.authStorage.recordUsageCost(assistantMsg.provider, assistantMsg.usage.cost.total, {
|
|
2526
|
+
sessionId: this.#activeProviderSessionId(),
|
|
2527
|
+
recordedAt: assistantMsg.timestamp,
|
|
2528
|
+
baseUrl: this.#modelRegistry.getProviderBaseUrl?.(assistantMsg.provider),
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2502
2531
|
}
|
|
2503
2532
|
if (event.message.role === "toolResult") {
|
|
2504
2533
|
const { toolName, details, isError, content } = event.message as {
|
|
@@ -6545,7 +6574,7 @@ export class AgentSession {
|
|
|
6545
6574
|
this.#todoReminderAwaitingProgress = false;
|
|
6546
6575
|
this.#planReferenceSent = false;
|
|
6547
6576
|
this.#planReferencePath = "local://PLAN.md";
|
|
6548
|
-
this.#
|
|
6577
|
+
this.#resetAdvisorSessionState();
|
|
6549
6578
|
this.#reconnectToAgent();
|
|
6550
6579
|
|
|
6551
6580
|
// Emit session_switch event with reason "new" to hooks
|
|
@@ -9683,21 +9712,21 @@ export class AgentSession {
|
|
|
9683
9712
|
|
|
9684
9713
|
if (this.#isClassifierRefusal(message)) return true;
|
|
9685
9714
|
if (this.#isProviderErrorFinishReasonBeforeToolUse(message)) return true;
|
|
9686
|
-
if (this.#
|
|
9715
|
+
if (this.#isMalformedFunctionCallError(message)) return true;
|
|
9716
|
+
if (this.#hasReplayUnsafeToolOutput(message)) return false;
|
|
9687
9717
|
if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
|
|
9688
9718
|
|
|
9689
9719
|
const err = message.errorMessage;
|
|
9690
9720
|
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
9691
9721
|
}
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
return false;
|
|
9722
|
+
/**
|
|
9723
|
+
* Retried turns remove the failed assistant message from active context.
|
|
9724
|
+
* Text/thinking-only partials are safe to discard and replay. Retained
|
|
9725
|
+
* tool calls are not: a completed tool call may already have emitted its
|
|
9726
|
+
* tool result after this assistant message, so replaying can duplicate work.
|
|
9727
|
+
*/
|
|
9728
|
+
#hasReplayUnsafeToolOutput(message: AssistantMessage): boolean {
|
|
9729
|
+
return message.content.some(block => block.type === "toolCall");
|
|
9701
9730
|
}
|
|
9702
9731
|
|
|
9703
9732
|
#isStaleOpenAIResponsesReplayError(message: AssistantMessage): boolean {
|
|
@@ -9733,6 +9762,11 @@ export class AgentSession {
|
|
|
9733
9762
|
return /\bProvider (?:returned error finish_reason|finish_reason:\s*error)\b/i.test(message.errorMessage);
|
|
9734
9763
|
}
|
|
9735
9764
|
|
|
9765
|
+
#isMalformedFunctionCallError(message: AssistantMessage): boolean {
|
|
9766
|
+
if (!message.errorMessage) return false;
|
|
9767
|
+
return /\bmalformed.?function.?call\b/i.test(message.errorMessage);
|
|
9768
|
+
}
|
|
9769
|
+
|
|
9736
9770
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
9737
9771
|
return (
|
|
9738
9772
|
this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
|
|
@@ -10984,6 +11018,7 @@ export class AgentSession {
|
|
|
10984
11018
|
}
|
|
10985
11019
|
|
|
10986
11020
|
this.agent.replaceMessages(sessionContext.messages);
|
|
11021
|
+
this.#resetAdvisorSessionState();
|
|
10987
11022
|
this.#syncTodoPhasesFromBranch();
|
|
10988
11023
|
if (switchingToDifferentSession) {
|
|
10989
11024
|
this.#closeAllProviderSessions("session switch");
|
|
@@ -11189,7 +11224,7 @@ export class AgentSession {
|
|
|
11189
11224
|
|
|
11190
11225
|
if (!skipConversationRestore) {
|
|
11191
11226
|
this.agent.replaceMessages(sessionContext.messages);
|
|
11192
|
-
this.#
|
|
11227
|
+
this.#resetAdvisorSessionState();
|
|
11193
11228
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
11194
11229
|
}
|
|
11195
11230
|
|
|
@@ -11278,7 +11313,7 @@ export class AgentSession {
|
|
|
11278
11313
|
}
|
|
11279
11314
|
|
|
11280
11315
|
this.agent.replaceMessages(sessionContext.messages);
|
|
11281
|
-
this.#
|
|
11316
|
+
this.#resetAdvisorSessionState();
|
|
11282
11317
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
11283
11318
|
|
|
11284
11319
|
return { cancelled: false, sessionFile: this.sessionFile };
|
|
@@ -11444,7 +11479,7 @@ export class AgentSession {
|
|
|
11444
11479
|
const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
|
|
11445
11480
|
await this.#restoreMCPSelectionsForSessionContext(displayContext);
|
|
11446
11481
|
this.agent.replaceMessages(displayContext.messages);
|
|
11447
|
-
this.#
|
|
11482
|
+
this.#resetAdvisorSessionState();
|
|
11448
11483
|
this.#syncTodoPhasesFromBranch();
|
|
11449
11484
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
11450
11485
|
|
|
@@ -18,16 +18,7 @@ export type {
|
|
|
18
18
|
ResetCreditRedeemOutcome,
|
|
19
19
|
ResetCreditTarget,
|
|
20
20
|
SerializedAuthStorage,
|
|
21
|
-
SnapshotResponse,
|
|
22
21
|
StoredAuthCredential,
|
|
23
22
|
} from "@oh-my-pi/pi-ai";
|
|
24
|
-
export {
|
|
25
|
-
|
|
26
|
-
AuthStorage,
|
|
27
|
-
DEFAULT_SNAPSHOT_CACHE_TTL_MS,
|
|
28
|
-
REMOTE_REFRESH_SENTINEL,
|
|
29
|
-
RemoteAuthCredentialStore,
|
|
30
|
-
readAuthBrokerSnapshotCache,
|
|
31
|
-
SqliteAuthCredentialStore,
|
|
32
|
-
writeAuthBrokerSnapshotCache,
|
|
33
|
-
} from "@oh-my-pi/pi-ai";
|
|
23
|
+
export { AuthStorage, REMOTE_REFRESH_SENTINEL, SqliteAuthCredentialStore } from "@oh-my-pi/pi-ai";
|
|
24
|
+
export type { SnapshotResponse } from "@oh-my-pi/pi-ai/auth-broker/types";
|
|
@@ -124,7 +124,13 @@ export class YieldQueue {
|
|
|
124
124
|
return thunks;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
/** Drop queued entries. With `kind`, drop only that kind's entries (leaving
|
|
128
|
+
* any pending idle-flush for other kinds intact); otherwise drop everything. */
|
|
129
|
+
clear(kind?: string): void {
|
|
130
|
+
if (kind !== undefined) {
|
|
131
|
+
this.#entries.delete(kind);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
128
134
|
this.#entries.clear();
|
|
129
135
|
this.#idleFlushPending = false;
|
|
130
136
|
}
|
|
@@ -334,7 +334,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
334
334
|
return commandConsumed();
|
|
335
335
|
},
|
|
336
336
|
handleTui: (_command, runtime) => {
|
|
337
|
-
runtime.ctx.showModelSelector(
|
|
337
|
+
runtime.ctx.showModelSelector();
|
|
338
338
|
runtime.ctx.editor.setText("");
|
|
339
339
|
},
|
|
340
340
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as net from "node:net";
|
|
2
2
|
import { Process, ProcessStatus } from "@oh-my-pi/pi-natives";
|
|
3
|
-
import {
|
|
3
|
+
import type { Browser, Page } from "puppeteer-core";
|
|
4
4
|
import { ToolError, throwIfAborted } from "../tool-errors";
|
|
5
5
|
|
|
6
6
|
const ATTACH_TARGET_SKIP_PATTERN =
|
|
@@ -126,7 +126,7 @@ export async function findReusableCdp(
|
|
|
126
126
|
export async function pickElectronTarget(browser: Browser, matcher?: string): Promise<Page> {
|
|
127
127
|
const discoveredPages = await Promise.all(
|
|
128
128
|
browser.targets().map(async target => {
|
|
129
|
-
if (target.type() !==
|
|
129
|
+
if (String(target.type()) !== "page") return null;
|
|
130
130
|
return await target.page().catch(() => null);
|
|
131
131
|
}),
|
|
132
132
|
);
|
package/src/tools/fetch.ts
CHANGED
|
@@ -20,12 +20,11 @@ import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block
|
|
|
20
20
|
import { webpExclusionForModel } from "../utils/image-loading";
|
|
21
21
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
22
22
|
import { ensureTool } from "../utils/tools-manager";
|
|
23
|
+
import { type ArchiveFormat, listArchiveRoot, sniffArchiveFormat } from "../utils/zip";
|
|
23
24
|
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
|
|
24
|
-
import {
|
|
25
|
-
import type { RenderResult } from "../web/scrapers/types";
|
|
25
|
+
import type { RenderResult, SpecialHandler } from "../web/scrapers/types";
|
|
26
26
|
import { finalizeOutput, loadPage, looksLikeHtml, MAX_BYTES, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
|
|
27
27
|
import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
|
|
28
|
-
import { type ArchiveFormat, listArchiveRoot, sniffArchiveFormat } from "./archive-reader";
|
|
29
28
|
import { applyListLimit } from "./list-limit";
|
|
30
29
|
import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
|
|
31
30
|
import { type LineRange, parseLineRanges } from "./path-utils";
|
|
@@ -51,34 +50,9 @@ const CONVERTIBLE_MIMES = new Set([
|
|
|
51
50
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
52
51
|
"application/rtf",
|
|
53
52
|
"application/epub+zip",
|
|
54
|
-
"image/png",
|
|
55
|
-
"image/jpeg",
|
|
56
|
-
"image/gif",
|
|
57
|
-
"image/webp",
|
|
58
|
-
"audio/mpeg",
|
|
59
|
-
"audio/wav",
|
|
60
|
-
"audio/ogg",
|
|
61
53
|
]);
|
|
62
54
|
|
|
63
|
-
const CONVERTIBLE_EXTENSIONS = new Set([
|
|
64
|
-
".pdf",
|
|
65
|
-
".doc",
|
|
66
|
-
".docx",
|
|
67
|
-
".ppt",
|
|
68
|
-
".pptx",
|
|
69
|
-
".xls",
|
|
70
|
-
".xlsx",
|
|
71
|
-
".rtf",
|
|
72
|
-
".epub",
|
|
73
|
-
".png",
|
|
74
|
-
".jpg",
|
|
75
|
-
".jpeg",
|
|
76
|
-
".gif",
|
|
77
|
-
".webp",
|
|
78
|
-
".mp3",
|
|
79
|
-
".wav",
|
|
80
|
-
".ogg",
|
|
81
|
-
]);
|
|
55
|
+
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
|
|
82
56
|
|
|
83
57
|
const NOTEBOOK_MIMES = new Set(["application/x-ipynb+json"]);
|
|
84
58
|
const NOTEBOOK_EXTENSIONS = new Set([".ipynb"]);
|
|
@@ -1044,6 +1018,18 @@ async function tryRenderBinaryPayload(
|
|
|
1044
1018
|
// Unified Special Handler Dispatch
|
|
1045
1019
|
// =============================================================================
|
|
1046
1020
|
|
|
1021
|
+
let specialHandlersPromise: Promise<SpecialHandler[]> | undefined;
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Lazily load the site-specific scraper handlers. The scrapers barrel eagerly
|
|
1025
|
+
* imports ~80 site modules, none of which are needed until the first fetch that
|
|
1026
|
+
* requires a special handler, so we keep them out of the cold-startup graph.
|
|
1027
|
+
*/
|
|
1028
|
+
function loadSpecialHandlers(): Promise<SpecialHandler[]> {
|
|
1029
|
+
specialHandlersPromise ??= import("../web/scrapers").then(m => m.specialHandlers);
|
|
1030
|
+
return specialHandlersPromise;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1047
1033
|
/**
|
|
1048
1034
|
* Try all special handlers
|
|
1049
1035
|
*/
|
|
@@ -1053,6 +1039,7 @@ async function handleSpecialUrls(
|
|
|
1053
1039
|
signal: AbortSignal | undefined,
|
|
1054
1040
|
storage: AgentStorage | null,
|
|
1055
1041
|
): Promise<FetchRenderResult | null> {
|
|
1042
|
+
const specialHandlers = await loadSpecialHandlers();
|
|
1056
1043
|
for (const handler of specialHandlers) {
|
|
1057
1044
|
if (signal?.aborted) {
|
|
1058
1045
|
throw new ToolAbortError();
|
|
@@ -1144,45 +1131,25 @@ async function renderUrl(
|
|
|
1144
1131
|
notes.push(
|
|
1145
1132
|
`Image MIME type ${imageMimeType} is unsupported for inline model serialization; returning text metadata only`,
|
|
1146
1133
|
);
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
notes.push("Attempting binary conversion fallback for unsupported image MIME type");
|
|
1150
|
-
} else {
|
|
1151
|
-
notes.push("Falling back to textual rendering from initial response");
|
|
1152
|
-
}
|
|
1153
|
-
skipConvertibleBinaryRetry = !shouldTryConvertibleFallback;
|
|
1134
|
+
notes.push("Falling back to textual rendering from initial response");
|
|
1135
|
+
skipConvertibleBinaryRetry = true;
|
|
1154
1136
|
} else {
|
|
1155
1137
|
const binary = await fetchBinary(finalUrl, timeout, signal);
|
|
1156
1138
|
if (binary.ok) {
|
|
1157
1139
|
notes.push("Fetched image binary");
|
|
1158
|
-
const conversionExtension = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
|
|
1159
|
-
let convertedText: string | null = null;
|
|
1160
|
-
const converted = await convertWithMarkit(binary.buffer, conversionExtension, timeout, signal);
|
|
1161
|
-
if (converted.ok) {
|
|
1162
|
-
if (converted.content.trim().length > 50) {
|
|
1163
|
-
notes.push("Converted with markit");
|
|
1164
|
-
convertedText = converted.content;
|
|
1165
|
-
} else {
|
|
1166
|
-
notes.push("markit conversion produced no usable output");
|
|
1167
|
-
}
|
|
1168
|
-
} else if (converted.error) {
|
|
1169
|
-
notes.push(`markit conversion failed: ${converted.error}`);
|
|
1170
|
-
} else {
|
|
1171
|
-
notes.push("markit conversion failed");
|
|
1172
|
-
}
|
|
1173
1140
|
|
|
1174
1141
|
if (binary.buffer.byteLength > MAX_INLINE_IMAGE_SOURCE_BYTES) {
|
|
1175
1142
|
notes.push(
|
|
1176
1143
|
`Image exceeds inline source limit (${binary.buffer.byteLength} bytes > ${MAX_INLINE_IMAGE_SOURCE_BYTES} bytes)`,
|
|
1177
1144
|
);
|
|
1178
1145
|
const output = finalizeOutput(
|
|
1179
|
-
|
|
1146
|
+
`Fetched image content (${imageMimeType}), but it is too large to inline render.`,
|
|
1180
1147
|
);
|
|
1181
1148
|
return {
|
|
1182
1149
|
url,
|
|
1183
1150
|
finalUrl,
|
|
1184
1151
|
contentType: imageMimeType,
|
|
1185
|
-
method:
|
|
1152
|
+
method: "image-too-large",
|
|
1186
1153
|
content: output.content,
|
|
1187
1154
|
fetchedAt,
|
|
1188
1155
|
truncated: output.truncated,
|
|
@@ -1199,15 +1166,13 @@ async function renderUrl(
|
|
|
1199
1166
|
if (!isDecodedImage) {
|
|
1200
1167
|
notes.push(`Fetched payload could not be decoded as ${imageMimeType}; returning text metadata only`);
|
|
1201
1168
|
const output = finalizeOutput(
|
|
1202
|
-
|
|
1203
|
-
rawContent ??
|
|
1204
|
-
`Fetched payload was labeled ${imageMimeType}, but bytes were not a valid image.`,
|
|
1169
|
+
rawContent ?? `Fetched payload was labeled ${imageMimeType}, but bytes were not a valid image.`,
|
|
1205
1170
|
);
|
|
1206
1171
|
return {
|
|
1207
1172
|
url,
|
|
1208
1173
|
finalUrl,
|
|
1209
1174
|
contentType: imageMimeType,
|
|
1210
|
-
method:
|
|
1175
|
+
method: "image-invalid",
|
|
1211
1176
|
content: output.content,
|
|
1212
1177
|
fetchedAt,
|
|
1213
1178
|
truncated: output.truncated,
|
|
@@ -1219,13 +1184,13 @@ async function renderUrl(
|
|
|
1219
1184
|
`Image exceeds inline output limit after resize (${resized.buffer.length} bytes > ${MAX_INLINE_IMAGE_OUTPUT_BYTES} bytes)`,
|
|
1220
1185
|
);
|
|
1221
1186
|
const output = finalizeOutput(
|
|
1222
|
-
|
|
1187
|
+
`Fetched image content (${imageMimeType}), but it is too large to inline render.`,
|
|
1223
1188
|
);
|
|
1224
1189
|
return {
|
|
1225
1190
|
url,
|
|
1226
1191
|
finalUrl,
|
|
1227
1192
|
contentType: imageMimeType,
|
|
1228
|
-
method:
|
|
1193
|
+
method: "image-too-large",
|
|
1229
1194
|
content: output.content,
|
|
1230
1195
|
fetchedAt,
|
|
1231
1196
|
truncated: output.truncated,
|
|
@@ -1234,7 +1199,7 @@ async function renderUrl(
|
|
|
1234
1199
|
}
|
|
1235
1200
|
|
|
1236
1201
|
const dimensionNote = formatDimensionNote(resized);
|
|
1237
|
-
let imageSummary =
|
|
1202
|
+
let imageSummary = `Fetched image content (${resized.mimeType}).`;
|
|
1238
1203
|
if (dimensionNote) {
|
|
1239
1204
|
imageSummary += `\n${dimensionNote}`;
|
|
1240
1205
|
}
|
package/src/tools/read.ts
CHANGED
|
@@ -48,8 +48,8 @@ import {
|
|
|
48
48
|
webpExclusionForModel,
|
|
49
49
|
} from "../utils/image-loading";
|
|
50
50
|
import { convertFileWithMarkit } from "../utils/markit";
|
|
51
|
+
import { type ArchiveReader, formatArchiveEntryLines, openArchive, parseArchivePathCandidates } from "../utils/zip";
|
|
51
52
|
import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
|
|
52
|
-
import { type ArchiveReader, formatArchiveEntryLines, openArchive, parseArchivePathCandidates } from "./archive-reader";
|
|
53
53
|
import {
|
|
54
54
|
type ConflictEntry,
|
|
55
55
|
type ConflictScope,
|