@oh-my-pi/pi-coding-agent 16.1.2 → 16.1.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 +30 -1
- package/dist/cli.js +3046 -3047
- 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/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/tools/image-gen.d.ts +2 -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.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/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/custom-editor.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- 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 +4 -0
- package/src/session/agent-session.ts +8 -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/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/render-utils.ts +4 -1
- package/src/tts/tts-client.ts +2 -7
- 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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mnemopi local-embeddings worker. Loaded inside the dedicated subprocess
|
|
3
|
+
* spawned by `embed-client.ts` (re-entered through the agent CLI's hidden
|
|
4
|
+
* `__omp_worker_mnemopi_embed` selector). The whole point of this module is
|
|
5
|
+
* that `loadFastembed()` — and therefore `onnxruntime-node`'s NAPI
|
|
6
|
+
* constructor + finalizer — only ever runs in this child address space. The
|
|
7
|
+
* parent `SIGKILL`s us on shutdown so the destructor that crashes Bun on
|
|
8
|
+
* Windows shutdown (issue #3031, mnemopi sibling of #1606/#1607) never runs
|
|
9
|
+
* in either process.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { StandardEmbeddingModel } from "@oh-my-pi/pi-mnemopi/core";
|
|
13
|
+
import { loadFastembed } from "@oh-my-pi/pi-mnemopi/core/fastembed-runtime";
|
|
14
|
+
import type { MnemopiEmbedModelId, MnemopiEmbedTransport, MnemopiEmbedWorkerInbound } from "./embed-protocol";
|
|
15
|
+
|
|
16
|
+
interface LoadedModel {
|
|
17
|
+
model: MnemopiEmbedModelId;
|
|
18
|
+
cacheDir: string | undefined;
|
|
19
|
+
instance: {
|
|
20
|
+
embed(texts: string[], batchSize?: number): AsyncIterable<number[][]> | Iterable<number[][]>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let loaded: Promise<LoadedModel> | null = null;
|
|
25
|
+
let loadedKey = "";
|
|
26
|
+
|
|
27
|
+
async function loadModel(model: MnemopiEmbedModelId, cacheDir: string | undefined): Promise<LoadedModel> {
|
|
28
|
+
const { FlagEmbedding } = await loadFastembed();
|
|
29
|
+
// Cast: `model` arrives as a string from the parent (resolved by
|
|
30
|
+
// mnemopi's `fastembedModelName`). Cast to the non-CUSTOM overload's
|
|
31
|
+
// argument so TypeScript picks the standard-model branch — the parent
|
|
32
|
+
// only ever passes pre-vetted fast-* identifiers.
|
|
33
|
+
const instance = await FlagEmbedding.init({
|
|
34
|
+
model: model as StandardEmbeddingModel,
|
|
35
|
+
cacheDir,
|
|
36
|
+
showDownloadProgress: false,
|
|
37
|
+
});
|
|
38
|
+
return { model, cacheDir, instance };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureLoaded(model: MnemopiEmbedModelId, cacheDir: string | undefined): Promise<LoadedModel> {
|
|
42
|
+
const key = `${model}\u0000${cacheDir ?? ""}`;
|
|
43
|
+
if (loaded !== null && loadedKey === key) return loaded;
|
|
44
|
+
const loading = loadModel(model, cacheDir).catch(error => {
|
|
45
|
+
// Failed loads must not poison the cache — a retry with the same key
|
|
46
|
+
// should re-attempt the load.
|
|
47
|
+
if (loaded === loading) {
|
|
48
|
+
loaded = null;
|
|
49
|
+
loadedKey = "";
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
});
|
|
53
|
+
loaded = loading;
|
|
54
|
+
loadedKey = key;
|
|
55
|
+
return loading;
|
|
56
|
+
}
|
|
57
|
+
async function handleEmbed(
|
|
58
|
+
transport: MnemopiEmbedTransport,
|
|
59
|
+
message: Extract<MnemopiEmbedWorkerInbound, { type: "embed" }>,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
try {
|
|
62
|
+
// Each `embed` carries the model + cacheDir the wrapper was bound to.
|
|
63
|
+
// `ensureLoaded` is idempotent for the same key, so this is a no-op
|
|
64
|
+
// once the model is in memory — and it transparently re-loads after
|
|
65
|
+
// the parent SIGKILLed the previous subprocess but mnemopi still
|
|
66
|
+
// holds the cached `LocalEmbeddingModel` wrapper from before.
|
|
67
|
+
const { instance } = await ensureLoaded(message.model, message.cacheDir);
|
|
68
|
+
const vectors: number[][] = [];
|
|
69
|
+
const batches = instance.embed([...message.texts], message.batchSize);
|
|
70
|
+
for await (const batch of batches) {
|
|
71
|
+
for (const row of batch) vectors.push(row);
|
|
72
|
+
}
|
|
73
|
+
transport.send({ type: "vectors", id: message.id, vectors });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
transport.send({
|
|
76
|
+
type: "error",
|
|
77
|
+
id: message.id,
|
|
78
|
+
error: error instanceof Error ? error.message : String(error),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleInit(
|
|
84
|
+
transport: MnemopiEmbedTransport,
|
|
85
|
+
message: Extract<MnemopiEmbedWorkerInbound, { type: "init" }>,
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
try {
|
|
88
|
+
await ensureLoaded(message.model, message.cacheDir);
|
|
89
|
+
transport.send({ type: "ready", id: message.id });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
transport.send({
|
|
92
|
+
type: "error",
|
|
93
|
+
id: message.id,
|
|
94
|
+
error: error instanceof Error ? error.message : String(error),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function startMnemopiEmbedWorker(transport: MnemopiEmbedTransport): void {
|
|
100
|
+
transport.onMessage(message => {
|
|
101
|
+
switch (message.type) {
|
|
102
|
+
case "ping":
|
|
103
|
+
transport.send({ type: "pong", id: message.id });
|
|
104
|
+
return;
|
|
105
|
+
case "init":
|
|
106
|
+
void handleInit(transport, message);
|
|
107
|
+
return;
|
|
108
|
+
case "embed":
|
|
109
|
+
void handleEmbed(transport, message);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
package/src/mnemopi/state.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
3
3
|
import type * as MnemopiNs from "@oh-my-pi/pi-mnemopi";
|
|
4
4
|
import type { Mnemopi, RecallResult } from "@oh-my-pi/pi-mnemopi";
|
|
5
5
|
import type * as MnemopiCoreNs from "@oh-my-pi/pi-mnemopi/core";
|
|
6
|
+
import type { LocalModelInitializer } from "@oh-my-pi/pi-mnemopi/core";
|
|
6
7
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import {
|
|
8
9
|
composeRecallQuery,
|
|
@@ -13,16 +14,42 @@ import {
|
|
|
13
14
|
import { extractMessages } from "../hindsight/transcript";
|
|
14
15
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
15
16
|
import type { MnemopiBackendConfig, MnemopiScoping } from "./config";
|
|
17
|
+
import { mnemopiEmbedClient } from "./embed-client";
|
|
16
18
|
|
|
17
19
|
// The mnemopi package pulls the embeddings stack; keep it off the CLI startup
|
|
18
20
|
// module graph by loading it lazily at the async boundaries that need it.
|
|
19
21
|
let mnemopiMod: typeof MnemopiNs | undefined;
|
|
20
22
|
let mnemopiCoreMod: typeof MnemopiCoreNs | undefined;
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
// `setLocalModelInitializer` writes a single module-level slot shared by
|
|
25
|
+
// both the root and `/core` re-exports, so install at most once across both
|
|
26
|
+
// loaders. Either entry point is enough to wire up the override.
|
|
27
|
+
let localModelInitializerInstalled = false;
|
|
28
|
+
|
|
29
|
+
function installLocalModelInitializer(setInitializer: (initializer: LocalModelInitializer) => void): void {
|
|
30
|
+
if (localModelInitializerInstalled) return;
|
|
31
|
+
localModelInitializerInstalled = true;
|
|
32
|
+
setInitializer(({ model, cacheDir }) =>
|
|
33
|
+
mnemopiEmbedClient.initialize(model, cacheDir).then(handle => {
|
|
34
|
+
if (handle) return handle;
|
|
35
|
+
throw new Error("mnemopi embed subprocess unavailable");
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lazily load `@oh-my-pi/pi-mnemopi` (memoized) and route fastembed loads
|
|
42
|
+
* through the dedicated embeddings subprocess. The override is installed once
|
|
43
|
+
* — before any consumer gets the chance to call `embed()` — so
|
|
44
|
+
* `onnxruntime-node`'s NAPI constructor + finalizer never run inside the
|
|
45
|
+
* agent's address space (issue #3031). Test seams that swap the initializer
|
|
46
|
+
* with `setLocalModelInitializerForTests` still win because both go through
|
|
47
|
+
* the same module-level slot.
|
|
48
|
+
*/
|
|
23
49
|
export async function loadMnemopi(): Promise<typeof MnemopiNs> {
|
|
24
50
|
if (!mnemopiMod) {
|
|
25
51
|
mnemopiMod = await import("@oh-my-pi/pi-mnemopi");
|
|
52
|
+
installLocalModelInitializer(mnemopiMod.setLocalModelInitializer);
|
|
26
53
|
}
|
|
27
54
|
return mnemopiMod;
|
|
28
55
|
}
|
|
@@ -31,6 +58,7 @@ export async function loadMnemopi(): Promise<typeof MnemopiNs> {
|
|
|
31
58
|
export async function loadMnemopiCore(): Promise<typeof MnemopiCoreNs> {
|
|
32
59
|
if (!mnemopiCoreMod) {
|
|
33
60
|
mnemopiCoreMod = await import("@oh-my-pi/pi-mnemopi/core");
|
|
61
|
+
installLocalModelInitializer(mnemopiCoreMod.setLocalModelInitializer);
|
|
34
62
|
}
|
|
35
63
|
return mnemopiCoreMod;
|
|
36
64
|
}
|
|
@@ -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;
|
|
@@ -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
|
@@ -1051,6 +1051,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1051
1051
|
const modelRegistry =
|
|
1052
1052
|
options.modelRegistry ??
|
|
1053
1053
|
new ModelRegistry(options.authStorage ?? (await logger.time("discoverModels", discoverAuthStorage, agentDir)));
|
|
1054
|
+
// Track whether we internally created the authStorage so we can close it
|
|
1055
|
+
// if construction fails before the session takes ownership.
|
|
1056
|
+
const ownsAuthStorage = !options.authStorage && !options.modelRegistry;
|
|
1054
1057
|
const authStorage = modelRegistry.authStorage;
|
|
1055
1058
|
if (options.authStorage && options.authStorage !== authStorage) {
|
|
1056
1059
|
throw new Error(
|
|
@@ -2854,6 +2857,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2854
2857
|
await asyncJobManager.dispose({ timeoutMs: 3_000 });
|
|
2855
2858
|
}
|
|
2856
2859
|
await disposeKernelSessionsByOwner(evalKernelOwnerId);
|
|
2860
|
+
if (ownsAuthStorage) authStorage.close();
|
|
2857
2861
|
}
|
|
2858
2862
|
} catch (cleanupError) {
|
|
2859
2863
|
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";
|
|
@@ -4208,6 +4210,11 @@ export class AgentSession {
|
|
|
4208
4210
|
hindsightState?.dispose();
|
|
4209
4211
|
const mnemopiState = setMnemopiSessionState(this, undefined);
|
|
4210
4212
|
await mnemopiState?.dispose();
|
|
4213
|
+
// Tear down the embeddings subprocess AFTER mnemopi state.dispose:
|
|
4214
|
+
// consolidate-on-dispose may still call `embed()` to store the final
|
|
4215
|
+
// memories, and that round-trips through the worker we are about to
|
|
4216
|
+
// hard-kill (issue #3031).
|
|
4217
|
+
await shutdownMnemopiEmbedClient();
|
|
4211
4218
|
this.#disconnectFromAgent();
|
|
4212
4219
|
if (this.#unsubscribeAppendOnly) {
|
|
4213
4220
|
this.#unsubscribeAppendOnly();
|
|
@@ -9977,6 +9984,7 @@ export class AgentSession {
|
|
|
9977
9984
|
if (this.#isProviderErrorFinishReasonBeforeToolUse(message)) return true;
|
|
9978
9985
|
if (this.#isMalformedFunctionCallError(message)) return true;
|
|
9979
9986
|
if (this.#hasReplayUnsafeToolOutput(message)) return false;
|
|
9987
|
+
if (message.errorMessage.includes(THINKING_LOOP_ERROR_MARKER)) return true;
|
|
9980
9988
|
if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
|
|
9981
9989
|
|
|
9982
9990
|
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 {
|
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
|
}
|
|
@@ -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/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);
|
|
@@ -13,9 +13,19 @@ export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
|
|
|
13
13
|
* with an opaque HTTP 400. Detect those models so the resize pipeline encodes
|
|
14
14
|
* to PNG/JPEG instead — the automatic equivalent of `OMP_NO_WEBP=1`.
|
|
15
15
|
*/
|
|
16
|
-
export function modelLacksWebpSupport(
|
|
16
|
+
export function modelLacksWebpSupport(
|
|
17
|
+
model: Pick<Model, "provider" | "api" | "imageInputDecoder"> | undefined,
|
|
18
|
+
): boolean {
|
|
17
19
|
if (!model) return false;
|
|
18
|
-
return
|
|
20
|
+
return (
|
|
21
|
+
model.imageInputDecoder === "stb" ||
|
|
22
|
+
model.provider === "ollama" ||
|
|
23
|
+
model.provider === "ollama-cloud" ||
|
|
24
|
+
model.provider === "llama.cpp" ||
|
|
25
|
+
model.provider === "lm-studio" ||
|
|
26
|
+
model.provider === "local-server" ||
|
|
27
|
+
model.api === "ollama-chat"
|
|
28
|
+
);
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
/**
|
package/src/utils/ipc.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Narrow a value to a thenable so a rejection handler can be attached.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the local helper in `mcp/transports/stdio.ts` (kept separate because
|
|
7
|
+
* that copy serves the FileSink stdin-write path and is battle-tested there).
|
|
8
|
+
* This shared copy is the home for the IPC `send()` sites.
|
|
9
|
+
*/
|
|
10
|
+
export function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
11
|
+
return (
|
|
12
|
+
value != null &&
|
|
13
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
14
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Send a message to a Bun subprocess over IPC, neutralizing both the
|
|
20
|
+
* synchronous throw ("cannot be used after the process has exited") and any
|
|
21
|
+
* asynchronous rejection (EPIPE from a pipe that broke between exit being
|
|
22
|
+
* observed and the next `send()`). The dead worker is detected separately via
|
|
23
|
+
* `onExit`/`onError` and respawned or disabled by the owning client; an
|
|
24
|
+
* un-awaited EPIPE rejection must not escape as a fatal unhandled rejection
|
|
25
|
+
* that takes down the whole session. See issue #2997.
|
|
26
|
+
*
|
|
27
|
+
* `label` prefixes the debug log on synchronous failure (e.g. "tts").
|
|
28
|
+
*/
|
|
29
|
+
export function safeSend(proc: { send(message: unknown): unknown }, message: unknown, label: string): void {
|
|
30
|
+
try {
|
|
31
|
+
const result = proc.send(message);
|
|
32
|
+
if (isThenable(result)) result.then(undefined, () => {});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.debug(`${label}: send to subprocess failed`, {
|
|
35
|
+
error: error instanceof Error ? error.message : String(error),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|