@oh-my-pi/pi-coding-agent 16.1.2 → 16.1.4
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 +44 -1
- package/dist/cli.js +2990 -2991
- package/dist/types/config/model-resolver.d.ts +3 -3
- package/dist/types/mnemopi/embed-client.d.ts +70 -0
- package/dist/types/mnemopi/embed-protocol.d.ts +52 -0
- package/dist/types/mnemopi/embed-worker.d.ts +12 -0
- package/dist/types/mnemopi/state.d.ts +9 -1
- package/dist/types/modes/components/cache-invalidation-marker.d.ts +23 -10
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/sdk.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/agent-storage.d.ts +2 -0
- package/dist/types/session/auth-broker-config.d.ts +3 -2
- package/dist/types/session/history-storage.d.ts +1 -1
- package/dist/types/session/tool-choice-queue.d.ts +2 -0
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tui/hyperlink.d.ts +3 -2
- package/dist/types/utils/image-loading.d.ts +1 -1
- package/dist/types/utils/ipc.d.ts +22 -0
- package/dist/types/web/search/providers/perplexity-auth.d.ts +37 -0
- package/package.json +12 -12
- package/src/cli/bench-cli.ts +33 -2
- package/src/cli/dry-balance-cli.ts +4 -2
- package/src/cli.ts +8 -0
- package/src/commands/token.ts +52 -33
- package/src/config/append-only-context-mode.ts +45 -0
- package/src/config/model-discovery.ts +3 -0
- package/src/config/model-registry.ts +21 -3
- package/src/config/model-resolver.ts +31 -8
- package/src/discovery/builtin-rules/ts-no-return-type.md +0 -1
- package/src/extensibility/plugins/manager.ts +82 -22
- package/src/lsp/client.ts +24 -0
- package/src/mnemopi/backend.ts +49 -3
- package/src/mnemopi/embed-client.ts +401 -0
- package/src/mnemopi/embed-protocol.ts +35 -0
- package/src/mnemopi/embed-worker.ts +113 -0
- package/src/mnemopi/state.ts +29 -1
- package/src/modes/components/cache-invalidation-marker.ts +31 -15
- package/src/modes/components/custom-editor.test.ts +4 -3
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/status-line/component.ts +64 -18
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/event-controller.ts +8 -0
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/theme/theme.ts +69 -0
- package/src/sdk.ts +37 -0
- package/src/session/agent-session.ts +13 -0
- package/src/session/agent-storage.ts +14 -0
- package/src/session/auth-broker-config.ts +2 -1
- package/src/session/history-storage.ts +13 -1
- package/src/session/tool-choice-queue.ts +6 -0
- package/src/stt/asr-client.ts +2 -7
- package/src/tiny/title-client.ts +2 -7
- package/src/tools/image-gen.ts +4 -8
- package/src/tools/index.ts +2 -0
- package/src/tools/render-utils.ts +4 -1
- package/src/tools/resolve.ts +1 -0
- package/src/tts/tts-client.ts +2 -7
- package/src/tui/hyperlink.ts +6 -3
- package/src/utils/image-loading.ts +12 -2
- package/src/utils/ipc.ts +38 -0
- package/src/web/search/providers/perplexity-auth.ts +133 -0
- package/src/web/search/providers/perplexity.ts +2 -125
|
@@ -39,11 +39,12 @@ function feedGaps(editor: CustomEditor, gaps: number[]): void {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async function decorateInFreshProcess(text: string): Promise<string> {
|
|
42
|
+
async function decorateInFreshProcess(text: string, imageLinks?: readonly string[]): Promise<string> {
|
|
43
43
|
const customEditorUrl = new URL("./custom-editor.ts", import.meta.url).href;
|
|
44
44
|
const script = `
|
|
45
45
|
import { CustomEditor } from ${JSON.stringify(customEditorUrl)};
|
|
46
46
|
const editor = new CustomEditor({});
|
|
47
|
+
editor.imageLinks = ${JSON.stringify(imageLinks)};
|
|
47
48
|
process.stdout.write(editor.decorateText(${JSON.stringify(text)}));
|
|
48
49
|
`;
|
|
49
50
|
const child = await $`bun -e ${script}`.quiet().nothrow();
|
|
@@ -59,8 +60,8 @@ describe("CustomEditor placeholder decoration", () => {
|
|
|
59
60
|
expect(output).toBe("[Paste #1, +30 lines]");
|
|
60
61
|
});
|
|
61
62
|
|
|
62
|
-
it("renders image placeholders before theme initialization", async () => {
|
|
63
|
-
const output = await decorateInFreshProcess("[Image #1]");
|
|
63
|
+
it("renders linked image placeholders before theme and settings initialization", async () => {
|
|
64
|
+
const output = await decorateInFreshProcess("[Image #1]", ["/tmp/example.png"]);
|
|
64
65
|
expect(output).toBe("[Image #1]");
|
|
65
66
|
});
|
|
66
67
|
});
|
|
@@ -177,7 +177,7 @@ export class CustomEditor extends Editor {
|
|
|
177
177
|
/** Per-render scratch flag: did any layout line in this render contain a magic
|
|
178
178
|
* keyword that should shimmer? Reset by {@link #scheduleShimmerIfNeeded} each
|
|
179
179
|
* time a frame is queued. */
|
|
180
|
-
#shimmerTimer:
|
|
180
|
+
#shimmerTimer: Timer | undefined;
|
|
181
181
|
/** Repaint hook the host wires once at construction. Called from the shimmer
|
|
182
182
|
* timer to request the next animation frame. Undefined when nobody is
|
|
183
183
|
* listening (tests, headless callers); the timer chain still self-cleans. */
|
|
@@ -179,9 +179,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
179
179
|
#providers: ProviderTabState[] = STATIC_PROVIDER_TABS;
|
|
180
180
|
#activeTabIndex: number = 0;
|
|
181
181
|
#refreshingProviders: Set<string> = new Set();
|
|
182
|
-
#scheduledProviderRefreshes: Map<string,
|
|
182
|
+
#scheduledProviderRefreshes: Map<string, Timer> = new Map();
|
|
183
183
|
#refreshSpinnerFrame: number = 0;
|
|
184
|
-
#refreshSpinnerInterval?:
|
|
184
|
+
#refreshSpinnerInterval?: Timer;
|
|
185
185
|
|
|
186
186
|
// Context menu state
|
|
187
187
|
#isMenuOpen: boolean = false;
|
|
@@ -154,6 +154,8 @@ interface ContextUsageMemo {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
const EMPTY_MESSAGES: readonly AgentMessage[] = [];
|
|
157
|
+
const STATUS_USAGE_START_DELAY_MS = 0;
|
|
158
|
+
const STATUS_USAGE_REFRESH_TIMEOUT_MS = 2_000;
|
|
157
159
|
|
|
158
160
|
function hasContextSegment(segments: readonly StatusLineSegmentId[]): boolean {
|
|
159
161
|
return segments.includes("context_pct") || segments.includes("context_total");
|
|
@@ -212,6 +214,7 @@ export class StatusLineComponent implements Component {
|
|
|
212
214
|
} | null = null;
|
|
213
215
|
#usageFetchedAt = 0;
|
|
214
216
|
#usageInFlight = false;
|
|
217
|
+
#usageStartTimer: Timer | null = null;
|
|
215
218
|
// Context-usage memo. The status line redraws on every agent event, so the
|
|
216
219
|
// hot path must not recompute context tokens unless an input changed.
|
|
217
220
|
// `getContextUsage()` anchors on the last assistant's real prompt-token
|
|
@@ -344,16 +347,24 @@ export class StatusLineComponent implements Component {
|
|
|
344
347
|
dispose(): void {
|
|
345
348
|
this.#disposed = true;
|
|
346
349
|
this.#onBranchChange = null;
|
|
350
|
+
this.#clearUsageStartTimer();
|
|
347
351
|
if (this.#gitWatcher) {
|
|
348
352
|
this.#gitWatcher.close();
|
|
349
353
|
this.#gitWatcher = null;
|
|
350
354
|
}
|
|
351
355
|
}
|
|
352
356
|
|
|
357
|
+
#clearUsageStartTimer(): void {
|
|
358
|
+
if (!this.#usageStartTimer) return;
|
|
359
|
+
clearTimeout(this.#usageStartTimer);
|
|
360
|
+
this.#usageStartTimer = null;
|
|
361
|
+
}
|
|
362
|
+
|
|
353
363
|
invalidate(): void {
|
|
354
364
|
this.#invalidateGitCaches();
|
|
355
365
|
}
|
|
356
366
|
#invalidateSessionCaches(): void {
|
|
367
|
+
this.#clearUsageStartTimer();
|
|
357
368
|
this.#cachedUsage = null;
|
|
358
369
|
this.#usageFetchedAt = 0;
|
|
359
370
|
this.#usageInFlight = false;
|
|
@@ -521,38 +532,73 @@ export class StatusLineComponent implements Component {
|
|
|
521
532
|
}
|
|
522
533
|
|
|
523
534
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* (non-private) so unit tests can verify the backoff invariant.
|
|
535
|
+
* Startup redraws only arm a short-delayed task; timeout releases the render
|
|
536
|
+
* cadence while a late successful fetch can still refresh the cached segment.
|
|
527
537
|
*/
|
|
528
538
|
refreshUsageInBackground(): void {
|
|
529
539
|
const now = Date.now();
|
|
530
|
-
if (this.#usageInFlight) return;
|
|
540
|
+
if (this.#usageInFlight || this.#usageStartTimer) return;
|
|
531
541
|
if (this.#usageFetchedAt > 0 && now - this.#usageFetchedAt < 5 * 60_000) return;
|
|
532
542
|
const session = this.session;
|
|
533
|
-
const fetcher = (session as { fetchUsageReports?: () => Promise<unknown> }).fetchUsageReports;
|
|
543
|
+
const fetcher = (session as { fetchUsageReports?: (signal?: AbortSignal) => Promise<unknown> }).fetchUsageReports;
|
|
534
544
|
if (typeof fetcher !== "function") return;
|
|
535
545
|
this.#usageInFlight = true;
|
|
536
|
-
|
|
537
|
-
|
|
546
|
+
this.#usageStartTimer = setTimeout(() => {
|
|
547
|
+
this.#usageStartTimer = null;
|
|
548
|
+
void this.#runUsageRefresh(session, fetcher);
|
|
549
|
+
}, STATUS_USAGE_START_DELAY_MS);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async #runUsageRefresh(session: AgentSession, fetcher: (signal?: AbortSignal) => Promise<unknown>): Promise<void> {
|
|
553
|
+
if (this.#disposed || this.session !== session) {
|
|
554
|
+
this.#usageInFlight = false;
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const signal = AbortSignal.timeout(STATUS_USAGE_REFRESH_TIMEOUT_MS);
|
|
558
|
+
let reportsPromise: Promise<unknown> | undefined;
|
|
559
|
+
try {
|
|
560
|
+
reportsPromise = fetcher.call(session, signal);
|
|
561
|
+
this.#applyUsageRefreshReports(session, await this.#raceUsageRefreshWithSignal(reportsPromise, signal));
|
|
562
|
+
} catch {
|
|
563
|
+
if (this.session !== session) return;
|
|
564
|
+
this.#usageFetchedAt = Date.now();
|
|
565
|
+
if (signal.aborted && reportsPromise) {
|
|
566
|
+
this.#observeLateUsageRefresh(session, reportsPromise);
|
|
567
|
+
}
|
|
568
|
+
} finally {
|
|
569
|
+
if (this.session === session) this.#usageInFlight = false;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#applyUsageRefreshReports(session: AgentSession, reports: unknown): void {
|
|
574
|
+
if (this.#disposed || this.session !== session) return;
|
|
575
|
+
this.#cachedUsage = this.#normalizeUsageReports(reports);
|
|
576
|
+
this.#usageFetchedAt = Date.now();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
#observeLateUsageRefresh(session: AgentSession, reportsPromise: Promise<unknown>): void {
|
|
580
|
+
void reportsPromise
|
|
538
581
|
.then(reports => {
|
|
539
|
-
|
|
540
|
-
this.#cachedUsage = this.#normalizeUsageReports(reports);
|
|
541
|
-
this.#usageFetchedAt = Date.now();
|
|
582
|
+
this.#applyUsageRefreshReports(session, reports);
|
|
542
583
|
})
|
|
543
584
|
.catch(() => {
|
|
544
|
-
if (this.session !== session) return;
|
|
545
|
-
// Backoff on error: stamp the fetch time so the 5-min TTL guard
|
|
546
|
-
// also acts as an error budget. Without this, every render
|
|
547
|
-
// kicks off another fetch (gated only by #usageInFlight),
|
|
548
|
-
// which hammers the endpoint during a network outage / 5xx.
|
|
585
|
+
if (this.#disposed || this.session !== session) return;
|
|
549
586
|
this.#usageFetchedAt = Date.now();
|
|
550
|
-
})
|
|
551
|
-
.finally(() => {
|
|
552
|
-
if (this.session === session) this.#usageInFlight = false;
|
|
553
587
|
});
|
|
554
588
|
}
|
|
555
589
|
|
|
590
|
+
async #raceUsageRefreshWithSignal(promise: Promise<unknown>, signal: AbortSignal): Promise<unknown> {
|
|
591
|
+
if (signal.aborted) throw signal.reason;
|
|
592
|
+
const aborted = Promise.withResolvers<never>();
|
|
593
|
+
const onAbort = () => aborted.reject(signal.reason);
|
|
594
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
595
|
+
try {
|
|
596
|
+
return await Promise.race([promise, aborted.promise]);
|
|
597
|
+
} finally {
|
|
598
|
+
signal.removeEventListener("abort", onAbort);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
556
602
|
#normalizeUsageReports(reports: unknown): {
|
|
557
603
|
fiveHour?: { percent: number; resetMinutes?: number };
|
|
558
604
|
sevenDay?: { percent: number; resetHours?: number };
|
|
@@ -142,7 +142,7 @@ export interface LspServerInfo {
|
|
|
142
142
|
*/
|
|
143
143
|
export class WelcomeComponent implements Component {
|
|
144
144
|
#animStart: number | null = null;
|
|
145
|
-
#animTimer:
|
|
145
|
+
#animTimer: Timer | null = null;
|
|
146
146
|
#selectedTip: string | undefined;
|
|
147
147
|
// Render cache: the welcome box is the first transcript-area component, so
|
|
148
148
|
// returning a stable array reference keeps the whole frame prefix stable.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { THINKING_LOOP_ERROR_MARKER } from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
2
3
|
import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
3
4
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
4
5
|
import { extractTextContent } from "../../commit/utils";
|
|
@@ -1014,6 +1015,13 @@ export class EventController {
|
|
|
1014
1015
|
async #handleAutoRetryStart(event: Extract<AgentSessionEvent, { type: "auto_retry_start" }>): Promise<void> {
|
|
1015
1016
|
this.#stopWorkingLoader();
|
|
1016
1017
|
this.ctx.statusContainer.clear();
|
|
1018
|
+
if (event.errorMessage?.includes(THINKING_LOOP_ERROR_MARKER)) {
|
|
1019
|
+
// The retry path drops the failed assistant from runtime context. Do not
|
|
1020
|
+
// restore its inline Error row; just unpin the fixed-region banner so the
|
|
1021
|
+
// retry UI is the visible state.
|
|
1022
|
+
this.#pinnedErrorComponent = undefined;
|
|
1023
|
+
this.ctx.clearPinnedError();
|
|
1024
|
+
}
|
|
1017
1025
|
const delaySeconds = Math.round(event.delayMs / 1000);
|
|
1018
1026
|
this.ctx.retryLoader = new Loader(
|
|
1019
1027
|
this.ctx.ui,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
theme,
|
|
28
28
|
} from "../../modes/theme/theme";
|
|
29
29
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
30
|
-
import type { ResetCreditRedeemOutcome } from "../../session/auth-storage";
|
|
30
|
+
import type { ResetCreditAccountStatus, ResetCreditRedeemOutcome } from "../../session/auth-storage";
|
|
31
31
|
import type { SessionInfo } from "../../session/session-listing";
|
|
32
32
|
import { SessionManager } from "../../session/session-manager";
|
|
33
33
|
import { FileSessionStorage } from "../../session/session-storage";
|
|
@@ -1161,7 +1161,7 @@ export class SelectorController {
|
|
|
1161
1161
|
async showResetUsageSelector(): Promise<void> {
|
|
1162
1162
|
const session = this.ctx.session;
|
|
1163
1163
|
this.ctx.showStatus("Checking saved rate-limit resets…", { dim: true });
|
|
1164
|
-
let statuses:
|
|
1164
|
+
let statuses: ResetCreditAccountStatus[];
|
|
1165
1165
|
try {
|
|
1166
1166
|
statuses = await session.listResetCredits();
|
|
1167
1167
|
} catch (error) {
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -2743,6 +2743,35 @@ export function highlightCode(code: string, lang?: string, highlightTheme: Theme
|
|
|
2743
2743
|
}
|
|
2744
2744
|
|
|
2745
2745
|
export function getSymbolTheme(): SymbolTheme {
|
|
2746
|
+
// Guard against `theme` being undefined (pre-init or cross-module-instance
|
|
2747
|
+
// plugin calls). Fall back to the ASCII preset so the returned symbols are
|
|
2748
|
+
// usable instead of crashing. See #2998.
|
|
2749
|
+
if (typeof theme === "undefined") {
|
|
2750
|
+
const box = {
|
|
2751
|
+
topLeft: "+",
|
|
2752
|
+
topRight: "+",
|
|
2753
|
+
bottomLeft: "+",
|
|
2754
|
+
bottomRight: "+",
|
|
2755
|
+
horizontal: "-",
|
|
2756
|
+
vertical: "|",
|
|
2757
|
+
cross: "+",
|
|
2758
|
+
teeDown: "+",
|
|
2759
|
+
teeUp: "+",
|
|
2760
|
+
teeLeft: "+",
|
|
2761
|
+
teeRight: "+",
|
|
2762
|
+
};
|
|
2763
|
+
return {
|
|
2764
|
+
cursor: ">",
|
|
2765
|
+
inputCursor: "|",
|
|
2766
|
+
boxRound: box,
|
|
2767
|
+
boxSharp: box,
|
|
2768
|
+
table: box,
|
|
2769
|
+
quoteBorder: "|",
|
|
2770
|
+
hrChar: "-",
|
|
2771
|
+
colorSwatch: "[]",
|
|
2772
|
+
spinnerFrames: ["-", "\\", "|", "/"],
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2746
2775
|
const preset = theme.getSymbolPreset();
|
|
2747
2776
|
|
|
2748
2777
|
return {
|
|
@@ -2808,6 +2837,19 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|
|
2808
2837
|
}
|
|
2809
2838
|
|
|
2810
2839
|
export function getSelectListTheme(): SelectListTheme {
|
|
2840
|
+
// Guard against `theme` being undefined (pre-init or cross-module-instance
|
|
2841
|
+
// plugin calls). See #2998.
|
|
2842
|
+
if (typeof theme === "undefined") {
|
|
2843
|
+
return {
|
|
2844
|
+
selectedPrefix: (text: string) => text,
|
|
2845
|
+
selectedText: (text: string) => text,
|
|
2846
|
+
description: (text: string) => text,
|
|
2847
|
+
scrollInfo: (text: string) => text,
|
|
2848
|
+
noMatch: (text: string) => text,
|
|
2849
|
+
symbols: getSymbolTheme(),
|
|
2850
|
+
hovered: (text: string) => text,
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2811
2853
|
return {
|
|
2812
2854
|
selectedPrefix: (text: string) => theme.fg("accent", text),
|
|
2813
2855
|
selectedText: (text: string) => theme.fg("accent", text),
|
|
@@ -2820,6 +2862,16 @@ export function getSelectListTheme(): SelectListTheme {
|
|
|
2820
2862
|
}
|
|
2821
2863
|
|
|
2822
2864
|
export function getEditorTheme(): EditorTheme {
|
|
2865
|
+
// Guard against `theme` being undefined (pre-init or cross-module-instance
|
|
2866
|
+
// plugin calls). See #2998.
|
|
2867
|
+
if (typeof theme === "undefined") {
|
|
2868
|
+
return {
|
|
2869
|
+
borderColor: (text: string) => text,
|
|
2870
|
+
selectList: getSelectListTheme(),
|
|
2871
|
+
symbols: getSymbolTheme(),
|
|
2872
|
+
hintStyle: (text: string) => text,
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2823
2875
|
return {
|
|
2824
2876
|
borderColor: (text: string) => theme.fg("borderMuted", text),
|
|
2825
2877
|
selectList: getSelectListTheme(),
|
|
@@ -2829,6 +2881,23 @@ export function getEditorTheme(): EditorTheme {
|
|
|
2829
2881
|
}
|
|
2830
2882
|
|
|
2831
2883
|
export function getSettingsListTheme(): SettingsListTheme {
|
|
2884
|
+
// Plugins (e.g. pi-rtk-optimizer) may call this before `initTheme()` assigns
|
|
2885
|
+
// the global `theme`, or from a separate module instance under npm-global
|
|
2886
|
+
// installs where the live binding was never initialized. Fall back to plain
|
|
2887
|
+
// text so the call returns a usable (unstyled) theme instead of crashing with
|
|
2888
|
+
// "undefined is not an object (evaluating 'theme.fg')". See #2998.
|
|
2889
|
+
if (typeof theme === "undefined") {
|
|
2890
|
+
return {
|
|
2891
|
+
label: (text: string) => text,
|
|
2892
|
+
value: (text: string) => text,
|
|
2893
|
+
description: (text: string) => text,
|
|
2894
|
+
cursor: "> ",
|
|
2895
|
+
hint: (text: string) => text,
|
|
2896
|
+
heading: (text: string) => text,
|
|
2897
|
+
section: (text: string) => text,
|
|
2898
|
+
hovered: (text: string) => text,
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2832
2901
|
return {
|
|
2833
2902
|
label: (text: string, selected: boolean, changed: boolean) =>
|
|
2834
2903
|
changed ? theme.fg("statusLineGitDirty", text) : selected ? theme.fg("accent", text) : text,
|
package/src/sdk.ts
CHANGED
|
@@ -687,6 +687,37 @@ export async function loadSessionExtensions(
|
|
|
687
687
|
return result;
|
|
688
688
|
}
|
|
689
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Load discovered/configured extensions and register their providers into
|
|
692
|
+
* `modelRegistry`, then discover the dynamic provider catalogs. One-shot CLIs
|
|
693
|
+
* (`omp bench`, dry-balance) build a bare {@link ModelRegistry} that only knows
|
|
694
|
+
* built-in catalog providers; without this, providers contributed by an
|
|
695
|
+
* extension (e.g. a custom OpenAI-compatible provider under
|
|
696
|
+
* `~/.omp/agent/extensions/`) never reach model resolution. Mirrors the
|
|
697
|
+
* session / `omp models` path: drain the queued provider registrations, then
|
|
698
|
+
* `refreshRuntimeProviders` so dynamically-discovered models exist before
|
|
699
|
+
* selectors are resolved.
|
|
700
|
+
*/
|
|
701
|
+
export async function loadCliExtensionProviders(
|
|
702
|
+
modelRegistry: ModelRegistry,
|
|
703
|
+
settings: Settings,
|
|
704
|
+
cwd: string,
|
|
705
|
+
options: Pick<CreateAgentSessionOptions, "disableExtensionDiscovery" | "additionalExtensionPaths"> = {},
|
|
706
|
+
): Promise<void> {
|
|
707
|
+
const eventBus = new EventBus();
|
|
708
|
+
const extensionsResult = await loadSessionExtensions(options, cwd, settings, eventBus);
|
|
709
|
+
const activeSources = extensionsResult.extensions.map(extension => extension.path);
|
|
710
|
+
modelRegistry.syncExtensionSources(activeSources);
|
|
711
|
+
for (const sourceId of new Set(activeSources)) {
|
|
712
|
+
modelRegistry.clearSourceRegistrations(sourceId);
|
|
713
|
+
}
|
|
714
|
+
for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
|
|
715
|
+
modelRegistry.registerProvider(name, config, sourceId);
|
|
716
|
+
}
|
|
717
|
+
extensionsResult.runtime.pendingProviderRegistrations = [];
|
|
718
|
+
await modelRegistry.refreshRuntimeProviders();
|
|
719
|
+
}
|
|
720
|
+
|
|
690
721
|
/**
|
|
691
722
|
* Discover skills from cwd and agentDir.
|
|
692
723
|
*/
|
|
@@ -1051,6 +1082,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1051
1082
|
const modelRegistry =
|
|
1052
1083
|
options.modelRegistry ??
|
|
1053
1084
|
new ModelRegistry(options.authStorage ?? (await logger.time("discoverModels", discoverAuthStorage, agentDir)));
|
|
1085
|
+
// Track whether we internally created the authStorage so we can close it
|
|
1086
|
+
// if construction fails before the session takes ownership.
|
|
1087
|
+
const ownsAuthStorage = !options.authStorage && !options.modelRegistry;
|
|
1054
1088
|
const authStorage = modelRegistry.authStorage;
|
|
1055
1089
|
if (options.authStorage && options.authStorage !== authStorage) {
|
|
1056
1090
|
throw new Error(
|
|
@@ -1515,6 +1549,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1515
1549
|
timestamp: Date.now(),
|
|
1516
1550
|
}),
|
|
1517
1551
|
peekQueueInvoker: () => session.peekQueueInvoker(),
|
|
1552
|
+
peekPendingInvoker: () => session.peekPendingInvoker(),
|
|
1553
|
+
clearPendingInvokers: () => session.clearPendingInvokers(),
|
|
1518
1554
|
peekStandingResolveHandler: () => session.peekStandingResolveHandler(),
|
|
1519
1555
|
setStandingResolveHandler: handler => session.setStandingResolveHandler(handler),
|
|
1520
1556
|
allocateOutputArtifact: async toolType => {
|
|
@@ -2854,6 +2890,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2854
2890
|
await asyncJobManager.dispose({ timeoutMs: 3_000 });
|
|
2855
2891
|
}
|
|
2856
2892
|
await disposeKernelSessionsByOwner(evalKernelOwnerId);
|
|
2893
|
+
if (ownsAuthStorage) authStorage.close();
|
|
2857
2894
|
}
|
|
2858
2895
|
} catch (cleanupError) {
|
|
2859
2896
|
logger.warn("Failed to clean up createAgentSession resources after startup error", {
|
|
@@ -104,6 +104,7 @@ import {
|
|
|
104
104
|
streamSimple,
|
|
105
105
|
} from "@oh-my-pi/pi-ai";
|
|
106
106
|
import { stripToolDescriptions } from "@oh-my-pi/pi-ai/utils/schema";
|
|
107
|
+
import { THINKING_LOOP_ERROR_MARKER } from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
107
108
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
108
109
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
109
110
|
import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
@@ -205,6 +206,7 @@ import type { HindsightSessionState } from "../hindsight/state";
|
|
|
205
206
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
206
207
|
import { IrcBus, type IrcMessage } from "../irc/bus";
|
|
207
208
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
209
|
+
import { shutdownMnemopiEmbedClient } from "../mnemopi/embed-client";
|
|
208
210
|
import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
|
|
209
211
|
import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
210
212
|
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
@@ -2179,6 +2181,11 @@ export class AgentSession {
|
|
|
2179
2181
|
return this.#toolChoiceQueue.peekPendingInvoker();
|
|
2180
2182
|
}
|
|
2181
2183
|
|
|
2184
|
+
/** Clear stale non-forcing pending preview invokers after `resolve` proves none can run. */
|
|
2185
|
+
clearPendingInvokers(): void {
|
|
2186
|
+
this.#toolChoiceQueue.clearPendingInvokers();
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2182
2189
|
/**
|
|
2183
2190
|
* Force the next model call to target a specific active tool, then terminate
|
|
2184
2191
|
* the agent loop. Pushes a two-step sequence [forced, "none"] so the model
|
|
@@ -4208,6 +4215,11 @@ export class AgentSession {
|
|
|
4208
4215
|
hindsightState?.dispose();
|
|
4209
4216
|
const mnemopiState = setMnemopiSessionState(this, undefined);
|
|
4210
4217
|
await mnemopiState?.dispose();
|
|
4218
|
+
// Tear down the embeddings subprocess AFTER mnemopi state.dispose:
|
|
4219
|
+
// consolidate-on-dispose may still call `embed()` to store the final
|
|
4220
|
+
// memories, and that round-trips through the worker we are about to
|
|
4221
|
+
// hard-kill (issue #3031).
|
|
4222
|
+
await shutdownMnemopiEmbedClient();
|
|
4211
4223
|
this.#disconnectFromAgent();
|
|
4212
4224
|
if (this.#unsubscribeAppendOnly) {
|
|
4213
4225
|
this.#unsubscribeAppendOnly();
|
|
@@ -9977,6 +9989,7 @@ export class AgentSession {
|
|
|
9977
9989
|
if (this.#isProviderErrorFinishReasonBeforeToolUse(message)) return true;
|
|
9978
9990
|
if (this.#isMalformedFunctionCallError(message)) return true;
|
|
9979
9991
|
if (this.#hasReplayUnsafeToolOutput(message)) return false;
|
|
9992
|
+
if (message.errorMessage.includes(THINKING_LOOP_ERROR_MARKER)) return true;
|
|
9980
9993
|
if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
|
|
9981
9994
|
|
|
9982
9995
|
const err = message.errorMessage;
|
|
@@ -247,6 +247,20 @@ FROM model_usage_legacy
|
|
|
247
247
|
{ cause: lastError },
|
|
248
248
|
);
|
|
249
249
|
}
|
|
250
|
+
/** @internal Reset all singletons and close their databases — test-only. */
|
|
251
|
+
static resetInstance(): void {
|
|
252
|
+
for (const storage of instances.values()) storage.#close();
|
|
253
|
+
instances.clear();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#close(): void {
|
|
257
|
+
this.#listSettingsStmt.finalize();
|
|
258
|
+
this.#upsertModelUsageStmt.finalize();
|
|
259
|
+
this.#listModelUsageStmt.finalize();
|
|
260
|
+
// SqliteAuthCredentialStore.close() finalizes its own statements and
|
|
261
|
+
// closes the shared #db handle — must run after our statements finalize.
|
|
262
|
+
this.#authStore.close();
|
|
263
|
+
}
|
|
250
264
|
|
|
251
265
|
/**
|
|
252
266
|
* Reads legacy settings persisted in the agent.db `settings` table.
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "@oh-my-pi/pi-ai/auth-broker/discover";
|
|
31
31
|
import { getAgentDir } from "@oh-my-pi/pi-utils";
|
|
32
32
|
import { resolveConfigValue } from "../config/resolve-config-value";
|
|
33
|
+
import type { AuthStorage } from "./auth-storage";
|
|
33
34
|
|
|
34
35
|
export { type AuthBrokerClientConfig, getAuthBrokerTokenFilePath };
|
|
35
36
|
|
|
@@ -82,7 +83,7 @@ export function resolveAuthBrokerConfig(): Promise<AuthBrokerClientConfig | null
|
|
|
82
83
|
export function discoverAuthStorage(
|
|
83
84
|
agentDir: string = getAgentDir(),
|
|
84
85
|
options?: Omit<DiscoverAuthStorageOptions, "agentDir" | "configValueResolver">,
|
|
85
|
-
):
|
|
86
|
+
): Promise<AuthStorage> {
|
|
86
87
|
return discoverAuthStorageShared({
|
|
87
88
|
...options,
|
|
88
89
|
agentDir,
|
|
@@ -145,9 +145,21 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
|
145
145
|
return HistoryStorage.#instance;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/** @internal Reset the singleton — test-only. */
|
|
148
|
+
/** @internal Reset the singleton and close its database — test-only. */
|
|
149
149
|
static resetInstance(): void {
|
|
150
|
+
const instance = HistoryStorage.#instance;
|
|
150
151
|
HistoryStorage.#instance = undefined;
|
|
152
|
+
if (instance) instance.#close();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#close(): void {
|
|
156
|
+
for (const stmt of this.#substringStmts.values()) stmt.finalize();
|
|
157
|
+
this.#substringStmts.clear();
|
|
158
|
+
this.#insertRowStmt.finalize();
|
|
159
|
+
this.#recentStmt.finalize();
|
|
160
|
+
this.#searchStmt.finalize();
|
|
161
|
+
this.#lastPromptStmt.finalize();
|
|
162
|
+
this.#db.close();
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
#insertBatch(rows: Array<Pick<HistoryEntry, "prompt" | "cwd" | "sessionId">>): void {
|
|
@@ -231,6 +231,12 @@ export class ToolChoiceQueue {
|
|
|
231
231
|
this.#pendingInvokers = this.#pendingInvokers.filter(p => p.id !== id);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/** Drop every pending preview invoker without touching hard tool-choice directives. */
|
|
235
|
+
clearPendingInvokers(): void {
|
|
236
|
+
if (this.#pendingInvokers.length === 0) return;
|
|
237
|
+
this.#pendingInvokers = [];
|
|
238
|
+
}
|
|
239
|
+
|
|
234
240
|
/** True when at least one non-forcing pending preview is registered. */
|
|
235
241
|
get hasPendingInvoker(): boolean {
|
|
236
242
|
return this.#pendingInvokers.length > 0;
|
package/src/stt/asr-client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } fro
|
|
|
3
3
|
import type { Subprocess } from "bun";
|
|
4
4
|
import { settings } from "../config/settings";
|
|
5
5
|
import { tinyWorkerEnvOverlay } from "../tiny/title-client";
|
|
6
|
+
import { safeSend } from "../utils/ipc";
|
|
6
7
|
import type { SttProgressEvent, SttWorkerInbound, SttWorkerOutbound } from "./asr-protocol";
|
|
7
8
|
import type { SttModelKey } from "./models";
|
|
8
9
|
|
|
@@ -181,13 +182,7 @@ export function createSttSubprocess(): SpawnedSubprocess {
|
|
|
181
182
|
function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
|
|
182
183
|
return {
|
|
183
184
|
send(message) {
|
|
184
|
-
|
|
185
|
-
proc.send(message);
|
|
186
|
-
} catch (error) {
|
|
187
|
-
logger.debug("stt: send to subprocess failed", {
|
|
188
|
-
error: error instanceof Error ? error.message : String(error),
|
|
189
|
-
});
|
|
190
|
-
}
|
|
185
|
+
safeSend(proc, message, "stt");
|
|
191
186
|
},
|
|
192
187
|
onMessage(handler) {
|
|
193
188
|
inbound.add(handler);
|
package/src/tiny/title-client.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as path from "node:path";
|
|
|
2
2
|
import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { Subprocess } from "bun";
|
|
4
4
|
import { settings } from "../config/settings";
|
|
5
|
+
import { safeSend } from "../utils/ipc";
|
|
5
6
|
import { tinyModelDeviceSettingToEnv } from "./device";
|
|
6
7
|
import { tinyModelDtypeSettingToEnv } from "./dtype";
|
|
7
8
|
import {
|
|
@@ -216,13 +217,7 @@ export function createTinyTitleSubprocess(): SpawnedSubprocess {
|
|
|
216
217
|
function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
|
|
217
218
|
return {
|
|
218
219
|
send(message) {
|
|
219
|
-
|
|
220
|
-
proc.send(message);
|
|
221
|
-
} catch (error) {
|
|
222
|
-
logger.debug("tiny-title: send to subprocess failed", {
|
|
223
|
-
error: error instanceof Error ? error.message : String(error),
|
|
224
|
-
});
|
|
225
|
-
}
|
|
220
|
+
safeSend(proc, message, "tiny-title");
|
|
226
221
|
},
|
|
227
222
|
onMessage(handler) {
|
|
228
223
|
inbound.add(handler);
|
package/src/tools/image-gen.ts
CHANGED
|
@@ -1572,19 +1572,15 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1572
1572
|
};
|
|
1573
1573
|
|
|
1574
1574
|
export async function getImageGenTools(
|
|
1575
|
-
|
|
1576
|
-
|
|
1575
|
+
_modelRegistry?: ModelRegistry,
|
|
1576
|
+
_activeModel?: Model,
|
|
1577
1577
|
): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>> {
|
|
1578
|
-
const apiKey = await findImageApiKey(modelRegistry, activeModel);
|
|
1579
|
-
if (!apiKey) return [];
|
|
1580
1578
|
return [imageGenTool];
|
|
1581
1579
|
}
|
|
1582
1580
|
|
|
1583
1581
|
export async function getImageGenToolsWithRegistry(
|
|
1584
|
-
|
|
1585
|
-
|
|
1582
|
+
_modelRegistry: ModelRegistry,
|
|
1583
|
+
_activeModel?: Model,
|
|
1586
1584
|
): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>> {
|
|
1587
|
-
const apiKey = await findImageApiKey(modelRegistry, activeModel);
|
|
1588
|
-
if (!apiKey) return [];
|
|
1589
1585
|
return [imageGenTool];
|
|
1590
1586
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -316,6 +316,8 @@ export interface ToolSession {
|
|
|
316
316
|
* tool dispatches to it so a staged preview resolves WITHOUT forcing tool_choice — the
|
|
317
317
|
* agent-loop's SoftToolRequirement lifecycle owns reminder injection and escalation. */
|
|
318
318
|
peekPendingInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
|
|
319
|
+
/** Clear stale pending preview markers when `resolve` cannot dispatch them. */
|
|
320
|
+
clearPendingInvokers?(): void;
|
|
319
321
|
/** Peek the long-lived "standing" resolve handler registered by a mode (e.g. plan mode).
|
|
320
322
|
* Consulted by the `resolve` tool as a fallback when no queue invoker is in flight,
|
|
321
323
|
* letting modes accept `resolve` invocations without forcing the tool choice every turn. */
|
|
@@ -657,7 +657,10 @@ export function truncateDiffByHunk(
|
|
|
657
657
|
export function shortenPath(filePath: string, homeDir?: string): string {
|
|
658
658
|
const home = homeDir ?? os.homedir();
|
|
659
659
|
if (home && filePath.startsWith(home)) {
|
|
660
|
-
|
|
660
|
+
const suffix = filePath.slice(home.length);
|
|
661
|
+
if (suffix === "" || suffix.startsWith(path.posix.sep) || suffix.startsWith(path.win32.sep)) {
|
|
662
|
+
return `~${suffix.replaceAll(path.win32.sep, path.posix.sep)}`;
|
|
663
|
+
}
|
|
661
664
|
}
|
|
662
665
|
return filePath;
|
|
663
666
|
}
|
package/src/tools/resolve.ts
CHANGED
|
@@ -212,6 +212,7 @@ export class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolD
|
|
|
212
212
|
this.session.peekPendingInvoker?.() ??
|
|
213
213
|
this.session.peekStandingResolveHandler?.();
|
|
214
214
|
if (!invoker) {
|
|
215
|
+
this.session.clearPendingInvokers?.();
|
|
215
216
|
// `discard` is a request to cancel/abort a staged action. When nothing is
|
|
216
217
|
// pending, the desired end-state (no staged change) already holds, so honor
|
|
217
218
|
// it as a successful cancellation instead of surfacing a hard error to the
|
package/src/tts/tts-client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } fro
|
|
|
3
3
|
import type { Subprocess } from "bun";
|
|
4
4
|
import { settings } from "../config/settings";
|
|
5
5
|
import { tinyWorkerEnvOverlay } from "../tiny/title-client";
|
|
6
|
+
import { safeSend } from "../utils/ipc";
|
|
6
7
|
import { isTtsLocalModelKey, type TtsLocalModelKey } from "./models";
|
|
7
8
|
import type { TtsProgressEvent, TtsWorkerInbound, TtsWorkerOutbound } from "./tts-protocol";
|
|
8
9
|
|
|
@@ -245,13 +246,7 @@ export function createTtsSubprocess(): SpawnedSubprocess {
|
|
|
245
246
|
function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
|
|
246
247
|
return {
|
|
247
248
|
send(message) {
|
|
248
|
-
|
|
249
|
-
proc.send(message);
|
|
250
|
-
} catch (error) {
|
|
251
|
-
logger.debug("tts: send to subprocess failed", {
|
|
252
|
-
error: error instanceof Error ? error.message : String(error),
|
|
253
|
-
});
|
|
254
|
-
}
|
|
249
|
+
safeSend(proc, message, "tts");
|
|
255
250
|
},
|
|
256
251
|
onMessage(handler) {
|
|
257
252
|
inbound.add(handler);
|