@oh-my-pi/pi-coding-agent 16.1.13 → 16.1.15
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 +48 -0
- package/dist/cli.js +5615 -2833
- package/dist/types/advisor/runtime.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +64 -14
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
- package/dist/types/eval/backend-helpers.d.ts +23 -0
- package/dist/types/eval/executor-base.d.ts +118 -0
- package/dist/types/eval/index.d.ts +2 -0
- package/dist/types/eval/jl/executor.d.ts +44 -0
- package/dist/types/eval/jl/index.d.ts +11 -0
- package/dist/types/eval/jl/kernel.d.ts +28 -0
- package/dist/types/eval/jl/prelude.d.ts +1 -0
- package/dist/types/eval/jl/runtime.d.ts +22 -0
- package/dist/types/eval/kernel-base.d.ts +105 -0
- package/dist/types/eval/py/kernel.d.ts +3 -61
- package/dist/types/eval/rb/executor.d.ts +77 -0
- package/dist/types/eval/rb/index.d.ts +11 -0
- package/dist/types/eval/rb/kernel.d.ts +31 -0
- package/dist/types/eval/rb/prelude.d.ts +1 -0
- package/dist/types/eval/rb/runtime.d.ts +23 -0
- package/dist/types/eval/runtime-env.d.ts +24 -0
- package/dist/types/eval/types.d.ts +3 -3
- package/dist/types/export/share.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +2 -15
- package/dist/types/extensibility/hooks/loader.d.ts +1 -25
- package/dist/types/extensibility/session-handler-types.d.ts +23 -0
- package/dist/types/jsonrpc/message-framing.d.ts +35 -0
- package/dist/types/mcp/transports/stdio.d.ts +12 -1
- package/dist/types/mnemopi/embed-client.d.ts +7 -24
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +11 -0
- package/dist/types/modes/components/selector-helpers.d.ts +53 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +8 -1
- package/dist/types/modes/types.d.ts +0 -2
- package/dist/types/modes/utils/copy-targets.d.ts +1 -1
- package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +2 -2
- package/dist/types/stt/asr-client.d.ts +3 -29
- package/dist/types/subprocess/worker-client.d.ts +149 -0
- package/dist/types/subprocess/worker-runtime.d.ts +107 -0
- package/dist/types/tiny/title-client.d.ts +14 -34
- package/dist/types/tools/eval-backends.d.ts +6 -3
- package/dist/types/tools/eval-render.d.ts +6 -5
- package/dist/types/tools/eval.d.ts +13 -15
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tts/tts-client.d.ts +3 -28
- package/dist/types/tui/code-cell.d.ts +7 -0
- package/dist/types/utils/file-display-mode.d.ts +1 -1
- package/dist/types/utils/shell-snapshot.d.ts +10 -0
- package/dist/types/web/parallel.d.ts +6 -0
- package/dist/types/web/search/providers/perplexity.d.ts +17 -3
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +114 -0
- package/src/advisor/runtime.ts +129 -1
- package/src/config/model-registry.ts +12 -4
- package/src/config/settings-schema.ts +74 -18
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +13 -107
- package/src/eval/__tests__/julia-prelude.test.ts +77 -0
- package/src/eval/backend-helpers.ts +48 -0
- package/src/eval/executor-base.ts +425 -0
- package/src/eval/index.ts +2 -0
- package/src/eval/jl/executor.ts +540 -0
- package/src/eval/jl/index.ts +54 -0
- package/src/eval/jl/kernel.ts +235 -0
- package/src/eval/jl/prelude.jl +930 -0
- package/src/eval/jl/prelude.ts +3 -0
- package/src/eval/jl/runner.jl +634 -0
- package/src/eval/jl/runtime.ts +118 -0
- package/src/eval/js/index.ts +3 -14
- package/src/eval/kernel-base.ts +569 -0
- package/src/eval/py/executor.ts +43 -252
- package/src/eval/py/index.ts +9 -20
- package/src/eval/py/kernel.ts +29 -544
- package/src/eval/rb/executor.ts +504 -0
- package/src/eval/rb/index.ts +54 -0
- package/src/eval/rb/kernel.ts +230 -0
- package/src/eval/rb/prelude.rb +721 -0
- package/src/eval/rb/prelude.ts +3 -0
- package/src/eval/rb/runner.rb +474 -0
- package/src/eval/rb/runtime.ts +132 -0
- package/src/eval/runtime-env.ts +104 -0
- package/src/eval/types.ts +3 -3
- package/src/exec/bash-executor.ts +44 -0
- package/src/export/share.ts +51 -28
- package/src/extensibility/extensions/runner.ts +4 -11
- package/src/extensibility/hooks/loader.ts +3 -21
- package/src/extensibility/session-handler-types.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/jsonrpc/message-framing.ts +142 -0
- package/src/lsp/client.ts +13 -109
- package/src/mcp/transports/stdio.ts +20 -4
- package/src/mnemopi/embed-client.ts +43 -198
- package/src/modes/components/agent-dashboard.ts +17 -40
- package/src/modes/components/chat-transcript-builder.ts +18 -102
- package/src/modes/components/custom-editor.test.ts +22 -0
- package/src/modes/components/custom-editor.ts +29 -1
- package/src/modes/components/extensions/extension-dashboard.ts +4 -13
- package/src/modes/components/extensions/extension-list.ts +16 -44
- package/src/modes/components/footer.ts +4 -3
- package/src/modes/components/history-search.ts +4 -16
- package/src/modes/components/selector-helpers.ts +129 -0
- package/src/modes/components/settings-selector.ts +7 -5
- package/src/modes/components/status-line/component.ts +5 -1
- package/src/modes/components/status-line/context-thresholds.ts +11 -3
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/tree-selector.ts +13 -18
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -9
- package/src/modes/controllers/input-controller.ts +32 -54
- package/src/modes/interactive-mode.ts +5 -7
- package/src/modes/theme/theme.ts +35 -0
- package/src/modes/types.ts +0 -2
- package/src/modes/utils/copy-targets.ts +3 -2
- package/src/modes/utils/interactive-context-helpers.ts +27 -0
- package/src/modes/utils/transcript-render-helpers.ts +157 -0
- package/src/modes/utils/ui-helpers.ts +21 -126
- package/src/prompts/system/system-prompt.md +5 -5
- package/src/prompts/tools/bash.md +2 -3
- package/src/prompts/tools/eval.md +6 -4
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/read.md +1 -2
- package/src/prompts/tools/replace.md +1 -1
- package/src/sdk.ts +13 -7
- package/src/secrets/obfuscator.ts +3 -9
- package/src/session/agent-session.ts +42 -9
- package/src/slash-commands/builtin-registry.ts +2 -1
- package/src/stt/asr-client.ts +35 -215
- package/src/stt/asr-worker.ts +29 -181
- package/src/subprocess/worker-client.ts +297 -0
- package/src/subprocess/worker-runtime.ts +277 -0
- package/src/task/executor.ts +4 -4
- package/src/tiny/title-client.ts +53 -219
- package/src/tiny/worker.ts +29 -180
- package/src/tools/eval-backends.ts +10 -3
- package/src/tools/eval-render.ts +17 -8
- package/src/tools/eval.ts +187 -22
- package/src/tools/index.ts +51 -28
- package/src/tts/tts-client.ts +38 -206
- package/src/tts/tts-worker.ts +23 -97
- package/src/tui/code-cell.ts +12 -1
- package/src/utils/file-display-mode.ts +2 -3
- package/src/utils/shell-snapshot.ts +63 -1
- package/src/web/parallel.ts +43 -42
- package/src/web/search/providers/parallel.ts +10 -99
- package/src/web/search/providers/perplexity.ts +18 -6
|
@@ -164,12 +164,14 @@ import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
|
164
164
|
import { loadCapability } from "../discovery";
|
|
165
165
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
166
166
|
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
167
|
+
import { disposeJuliaKernelSessionsByOwner } from "../eval/jl/executor";
|
|
167
168
|
import { namespaceSessionId as namespacePythonSessionId } from "../eval/py";
|
|
168
169
|
import {
|
|
169
170
|
disposeKernelSessionsByOwner,
|
|
170
171
|
executePython as executePythonCommand,
|
|
171
172
|
type PythonResult,
|
|
172
173
|
} from "../eval/py/executor";
|
|
174
|
+
import { disposeRubyKernelSessionsByOwner } from "../eval/rb/executor";
|
|
173
175
|
import { defaultEvalSessionId } from "../eval/session-id";
|
|
174
176
|
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
175
177
|
import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
|
|
@@ -520,7 +522,7 @@ export interface AgentSessionConfig {
|
|
|
520
522
|
obfuscator?: SecretObfuscator;
|
|
521
523
|
/** Inherited eval executor session id from a parent agent. */
|
|
522
524
|
parentEvalSessionId?: string;
|
|
523
|
-
/** Logical owner for retained
|
|
525
|
+
/** Logical owner for retained eval kernels created by this session. */
|
|
524
526
|
evalKernelOwnerId?: string;
|
|
525
527
|
/**
|
|
526
528
|
* AsyncJobManager that this session installed as the process-global instance.
|
|
@@ -1934,6 +1936,7 @@ export class AgentSession {
|
|
|
1934
1936
|
snapshotMessages: () => this.agent.state.messages,
|
|
1935
1937
|
enqueueAdvice,
|
|
1936
1938
|
maintainContext: incomingTokens => this.#maintainAdvisorContext(incomingTokens),
|
|
1939
|
+
obfuscator: this.#obfuscator,
|
|
1937
1940
|
});
|
|
1938
1941
|
if (seedToCurrent) {
|
|
1939
1942
|
this.#advisorRuntime.seedTo(this.agent.state.messages.length);
|
|
@@ -4173,7 +4176,7 @@ export class AgentSession {
|
|
|
4173
4176
|
|
|
4174
4177
|
/**
|
|
4175
4178
|
* Synchronously mark the session as disposing so new work is rejected
|
|
4176
|
-
* immediately:
|
|
4179
|
+
* immediately: eval starts throw, queued asides are dropped, and the
|
|
4177
4180
|
* aside provider is detached. Idempotent; `dispose()` runs it first.
|
|
4178
4181
|
*
|
|
4179
4182
|
* Wrappers that await other teardown before delegating to `dispose()` MUST
|
|
@@ -4235,13 +4238,13 @@ export class AgentSession {
|
|
|
4235
4238
|
AsyncJobManager.setInstance(undefined);
|
|
4236
4239
|
}
|
|
4237
4240
|
}
|
|
4238
|
-
const
|
|
4239
|
-
if (!
|
|
4240
|
-
logger.warn(
|
|
4241
|
-
"Detaching retained Python kernel ownership during dispose while Python execution is still active",
|
|
4242
|
-
);
|
|
4241
|
+
const evalExecutionsSettled = await this.#prepareEvalExecutionsForDispose();
|
|
4242
|
+
if (!evalExecutionsSettled) {
|
|
4243
|
+
logger.warn("Detaching retained eval-kernel ownership during dispose while eval execution is still active");
|
|
4243
4244
|
}
|
|
4244
4245
|
await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
4246
|
+
await disposeRubyKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
4247
|
+
await disposeJuliaKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
4245
4248
|
await shutdownTinyTitleClient();
|
|
4246
4249
|
this.#releasePowerAssertion();
|
|
4247
4250
|
await this.sessionManager.close();
|
|
@@ -9024,6 +9027,21 @@ export class AgentSession {
|
|
|
9024
9027
|
providerKeys.add(`openai-responses:${nextModel.provider}`);
|
|
9025
9028
|
}
|
|
9026
9029
|
|
|
9030
|
+
// `openai-completions` sessions are keyed `openai-completions:<provider>:<resolvedBaseUrl>:<modelId>`
|
|
9031
|
+
// and cache backend-specific decisions (strict-tools disable scopes, reasoning-effort
|
|
9032
|
+
// fallbacks). The resolved request base URL can differ from the catalog `model.baseUrl`
|
|
9033
|
+
// (Moonshot env override, Alibaba Coding Plan enterprise URL, Azure deployment URL),
|
|
9034
|
+
// so evict by provider prefix when the user moves away from that completions backend.
|
|
9035
|
+
let completionsPrefixToEvict: string | undefined;
|
|
9036
|
+
if (currentModel.api === "openai-completions") {
|
|
9037
|
+
const currentScope = `${currentModel.provider}:${currentModel.baseUrl ?? ""}`;
|
|
9038
|
+
const nextScope =
|
|
9039
|
+
nextModel.api === "openai-completions" ? `${nextModel.provider}:${nextModel.baseUrl ?? ""}` : undefined;
|
|
9040
|
+
if (currentScope !== nextScope) {
|
|
9041
|
+
completionsPrefixToEvict = `openai-completions:${currentModel.provider}:`;
|
|
9042
|
+
}
|
|
9043
|
+
}
|
|
9044
|
+
|
|
9027
9045
|
for (const providerKey of providerKeys) {
|
|
9028
9046
|
const state = this.#providerSessionState.get(providerKey);
|
|
9029
9047
|
if (!state) continue;
|
|
@@ -9039,6 +9057,21 @@ export class AgentSession {
|
|
|
9039
9057
|
|
|
9040
9058
|
this.#providerSessionState.delete(providerKey);
|
|
9041
9059
|
}
|
|
9060
|
+
|
|
9061
|
+
if (completionsPrefixToEvict !== undefined) {
|
|
9062
|
+
for (const [key, state] of this.#providerSessionState) {
|
|
9063
|
+
if (!key.startsWith(completionsPrefixToEvict)) continue;
|
|
9064
|
+
try {
|
|
9065
|
+
state.close();
|
|
9066
|
+
} catch (error) {
|
|
9067
|
+
logger.warn("Failed to close provider session state during model switch", {
|
|
9068
|
+
providerKey: key,
|
|
9069
|
+
error: String(error),
|
|
9070
|
+
});
|
|
9071
|
+
}
|
|
9072
|
+
this.#providerSessionState.delete(key);
|
|
9073
|
+
}
|
|
9074
|
+
}
|
|
9042
9075
|
}
|
|
9043
9076
|
|
|
9044
9077
|
#normalizeProviderReplayValue(value: unknown): unknown {
|
|
@@ -12166,8 +12199,8 @@ export class AgentSession {
|
|
|
12166
12199
|
pendingMessages?: AgentMessage[];
|
|
12167
12200
|
}): ContextUsageBreakdown | undefined {
|
|
12168
12201
|
const model = this.model;
|
|
12169
|
-
const
|
|
12170
|
-
|
|
12202
|
+
const rawContextWindow = options?.contextWindow ?? model?.contextWindow ?? 0;
|
|
12203
|
+
const contextWindow = Number.isFinite(rawContextWindow) && rawContextWindow > 0 ? rawContextWindow : 0;
|
|
12171
12204
|
|
|
12172
12205
|
const { skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens } = computeNonMessageBreakdown(this);
|
|
12173
12206
|
const categoryNonMessageTokens = skillsTokens + toolsTokens + systemContextTokens + systemPromptTokens;
|
|
@@ -602,11 +602,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
602
602
|
},
|
|
603
603
|
{
|
|
604
604
|
name: "share",
|
|
605
|
-
description: "Share session via an encrypted link (
|
|
605
|
+
description: "Share session via an encrypted link (share server or secret gist)",
|
|
606
606
|
handle: async (_command, runtime) => {
|
|
607
607
|
try {
|
|
608
608
|
const result = await shareSession(runtime.sessionManager, {
|
|
609
609
|
serverUrl: runtime.settings.get("share.serverUrl"),
|
|
610
|
+
store: runtime.settings.get("share.store"),
|
|
610
611
|
state: runtime.session.state,
|
|
611
612
|
obfuscator: runtime.settings.get("share.redactSecrets") ? runtime.session.obfuscator : undefined,
|
|
612
613
|
});
|
package/src/stt/asr-client.ts
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import {
|
|
3
|
+
createUnavailableWorker,
|
|
4
|
+
createWorkerHandle,
|
|
5
|
+
createWorkerSubprocess,
|
|
6
|
+
logWorkerMessage,
|
|
7
|
+
resolveWorkerSpawnCmd,
|
|
8
|
+
SMOKE_TEST_TIMEOUT_MS,
|
|
9
|
+
type SpawnedSubprocess,
|
|
10
|
+
smokeTestWorker,
|
|
11
|
+
spawnWorkerOrUnavailable,
|
|
12
|
+
type WorkerHandle,
|
|
13
|
+
} from "../subprocess/worker-client";
|
|
14
|
+
import { tinyWorkerEnv } from "../tiny/title-client";
|
|
6
15
|
import { safeSend } from "../utils/ipc";
|
|
7
16
|
import type { SttProgressEvent, SttWorkerInbound, SttWorkerOutbound } from "./asr-protocol";
|
|
8
17
|
import type { SttModelKey } from "./models";
|
|
9
18
|
|
|
10
|
-
/**
|
|
11
|
-
* Abstraction over the speech-recognition subprocess. Modelled as a worker
|
|
12
|
-
* interface so the parent composes lifecycle, ping/pong, and request/response
|
|
13
|
-
* correlation uniformly; the runtime implementation is a Bun child process so
|
|
14
|
-
* `onnxruntime-node`'s NAPI finalizer never runs inside the main agent address
|
|
15
|
-
* space — that destructor segfaults Bun on shutdown (issue #1606).
|
|
16
|
-
*/
|
|
17
|
-
interface WorkerHandle {
|
|
18
|
-
send(message: SttWorkerInbound): void;
|
|
19
|
-
onMessage(handler: (message: SttWorkerOutbound) => void): () => void;
|
|
20
|
-
onError(handler: (error: Error) => void): () => void;
|
|
21
|
-
terminate(): Promise<void>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
19
|
type PendingRequest =
|
|
25
20
|
| { kind: "transcribe"; modelKey: SttModelKey; resolve: (text: string) => void; reject: (error: Error) => void }
|
|
26
21
|
| { kind: "download"; modelKey: SttModelKey; resolve: (ok: boolean) => void };
|
|
@@ -64,205 +59,50 @@ interface StreamState {
|
|
|
64
59
|
finish: (apply: () => void) => void;
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
// Cold-starting the worker subprocess from a compiled binary (decompress +
|
|
68
|
-
// module graph load) is slow on contended CI runners; the probe only needs to
|
|
69
|
-
// prove the worker spawns and ponges, so a generous bound removes the flake.
|
|
70
|
-
const SMOKE_TEST_TIMEOUT_MS = 30_000;
|
|
71
|
-
|
|
72
62
|
/**
|
|
73
63
|
* Hidden subcommand on the main CLI that boots the speech-recognition worker in
|
|
74
64
|
* the spawned subprocess. Kept in sync with the dispatch in `cli.ts`.
|
|
75
65
|
*/
|
|
76
66
|
export const STT_WORKER_ARG = "__omp_worker_stt";
|
|
77
67
|
|
|
78
|
-
function readTinyModelSetting(key: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
|
|
79
|
-
try {
|
|
80
|
-
const value = settings.get(key);
|
|
81
|
-
return typeof value === "string" ? value : undefined;
|
|
82
|
-
} catch {
|
|
83
|
-
// Settings may be uninitialized (e.g. `omp --smoke-test`); fall back to env/default.
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Env handed to the speech subprocess. The `PI_TINY_DEVICE` / `PI_TINY_DTYPE`
|
|
90
|
-
* env vars win; otherwise the persisted `providers.tinyModelDevice` /
|
|
91
|
-
* `providers.tinyModelDtype` settings are mapped onto those vars so the
|
|
92
|
-
* subprocess's env-based resolution picks them up (shared with tiny models).
|
|
93
|
-
*/
|
|
94
|
-
function sttWorkerEnv(): Record<string, string> {
|
|
95
|
-
const overlay = tinyWorkerEnvOverlay(
|
|
96
|
-
$env,
|
|
97
|
-
readTinyModelSetting("providers.tinyModelDevice"),
|
|
98
|
-
readTinyModelSetting("providers.tinyModelDtype"),
|
|
99
|
-
);
|
|
100
|
-
const base = $env as Record<string, string | undefined>;
|
|
101
|
-
const merged: Record<string, string> = {};
|
|
102
|
-
for (const key in base) {
|
|
103
|
-
const value = base[key];
|
|
104
|
-
if (typeof value === "string") merged[key] = value;
|
|
105
|
-
}
|
|
106
|
-
for (const key in overlay) merged[key] = overlay[key];
|
|
107
|
-
return merged;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
interface SttWorkerSpawnCommand {
|
|
111
|
-
cmd: string[];
|
|
112
|
-
cwd?: string;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Resolve the command used to relaunch the agent CLI into stt-worker mode. In a
|
|
117
|
-
* compiled binary the entry point is the binary itself; otherwise re-enter the
|
|
118
|
-
* declared worker-host entry with a cwd-relative script path (Bun's subprocess
|
|
119
|
-
* IPC is more reliable that way under `bun test`), falling back to this
|
|
120
|
-
* package's own `src/cli.ts` when no host entry is declared.
|
|
121
|
-
*/
|
|
122
|
-
function sttWorkerSpawnCmd(): SttWorkerSpawnCommand {
|
|
123
|
-
if (isCompiledBinary()) return { cmd: [process.execPath, STT_WORKER_ARG] };
|
|
124
|
-
const hostEntry = workerHostEntry();
|
|
125
|
-
if (hostEntry) {
|
|
126
|
-
return { cmd: [process.execPath, path.basename(hostEntry), STT_WORKER_ARG], cwd: path.dirname(hostEntry) };
|
|
127
|
-
}
|
|
128
|
-
const packageRoot = path.resolve(import.meta.dir, "..", "..");
|
|
129
|
-
return { cmd: [process.execPath, "src/cli.ts", STT_WORKER_ARG], cwd: packageRoot };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
interface SpawnedSubprocess {
|
|
133
|
-
proc: Subprocess<"ignore", "ignore", "ignore">;
|
|
134
|
-
inbound: Set<(message: SttWorkerOutbound) => void>;
|
|
135
|
-
errors: Set<(error: Error) => void>;
|
|
136
|
-
/**
|
|
137
|
-
* Flipped to `true` right before the parent SIGKILLs the child so `onExit`
|
|
138
|
-
* can distinguish the expected hard-kill from a crash/OOM/external signal.
|
|
139
|
-
*/
|
|
140
|
-
intentionalExit: { value: boolean };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
68
|
/**
|
|
144
69
|
* Spawn the speech worker as a subprocess. Exported for tests and the smoke
|
|
145
70
|
* probe; production callers go through {@link spawnSttWorker}.
|
|
146
71
|
*/
|
|
147
|
-
export function createSttSubprocess(): SpawnedSubprocess {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const proc = Bun.spawn({
|
|
153
|
-
cmd: spawnCommand.cmd,
|
|
154
|
-
cwd: spawnCommand.cwd,
|
|
155
|
-
env: sttWorkerEnv(),
|
|
156
|
-
stdin: "ignore",
|
|
157
|
-
stdout: "ignore",
|
|
158
|
-
stderr: "ignore",
|
|
159
|
-
serialization: "advanced",
|
|
160
|
-
windowsHide: true,
|
|
161
|
-
ipc(message) {
|
|
162
|
-
for (const handler of inbound) handler(message as SttWorkerOutbound);
|
|
163
|
-
},
|
|
164
|
-
onExit(_proc, exitCode, signalCode) {
|
|
165
|
-
if (exitCode === 0) return;
|
|
166
|
-
// Swallow only the expected SIGKILL from `terminate()`; every other
|
|
167
|
-
// signal exit is a real worker death that must fault in-flight
|
|
168
|
-
// requests so callers don't await forever.
|
|
169
|
-
if (exitCode === null && intentionalExit.value) return;
|
|
170
|
-
const reason = exitCode !== null ? `code ${exitCode}` : `signal ${signalCode ?? "unknown"}`;
|
|
171
|
-
const err = new Error(`stt subprocess exited with ${reason}`);
|
|
172
|
-
for (const handler of errors) handler(err);
|
|
173
|
-
},
|
|
72
|
+
export function createSttSubprocess(): SpawnedSubprocess<SttWorkerOutbound> {
|
|
73
|
+
return createWorkerSubprocess<SttWorkerOutbound>({
|
|
74
|
+
spawnCommand: resolveWorkerSpawnCmd(STT_WORKER_ARG),
|
|
75
|
+
env: tinyWorkerEnv(),
|
|
76
|
+
exitLabel: "stt subprocess",
|
|
174
77
|
});
|
|
175
|
-
// Don't keep the parent event loop alive on an idle worker; dispose calls
|
|
176
|
-
// `terminate()` explicitly. Bun's test runner can starve IPC delivery for
|
|
177
|
-
// unref'd subprocesses, so keep it referenced under tests.
|
|
178
|
-
if (!isBunTestRuntime()) proc.unref();
|
|
179
|
-
return { proc, inbound, errors, intentionalExit };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
|
|
183
|
-
return {
|
|
184
|
-
send(message) {
|
|
185
|
-
safeSend(proc, message, "stt");
|
|
186
|
-
},
|
|
187
|
-
onMessage(handler) {
|
|
188
|
-
inbound.add(handler);
|
|
189
|
-
return () => inbound.delete(handler);
|
|
190
|
-
},
|
|
191
|
-
onError(handler) {
|
|
192
|
-
errors.add(handler);
|
|
193
|
-
return () => errors.delete(handler);
|
|
194
|
-
},
|
|
195
|
-
async terminate() {
|
|
196
|
-
// SIGKILL: the whole point of subprocess isolation is that the parent
|
|
197
|
-
// never runs `onnxruntime-node`'s NAPI finalizer. Hard-kill instead —
|
|
198
|
-
// the model lives in process memory and the OS reclaims everything.
|
|
199
|
-
intentionalExit.value = true;
|
|
200
|
-
try {
|
|
201
|
-
proc.kill("SIGKILL");
|
|
202
|
-
} catch {
|
|
203
|
-
// Already gone.
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function spawnInlineUnavailableWorker(error: unknown): WorkerHandle {
|
|
210
|
-
const listeners = new Set<(message: SttWorkerOutbound) => void>();
|
|
211
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
212
|
-
const emit = (message: SttWorkerOutbound): void => {
|
|
213
|
-
for (const listener of listeners) listener(message);
|
|
214
|
-
};
|
|
215
|
-
return {
|
|
216
|
-
send(message) {
|
|
217
|
-
queueMicrotask(() => {
|
|
218
|
-
if (message.type === "ping") {
|
|
219
|
-
emit({ type: "pong", id: message.id });
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
emit({ type: "error", id: message.id, error: errorMessage });
|
|
223
|
-
});
|
|
224
|
-
},
|
|
225
|
-
onMessage(handler) {
|
|
226
|
-
listeners.add(handler);
|
|
227
|
-
return () => listeners.delete(handler);
|
|
228
|
-
},
|
|
229
|
-
onError() {
|
|
230
|
-
return () => {};
|
|
231
|
-
},
|
|
232
|
-
async terminate() {
|
|
233
|
-
listeners.clear();
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
78
|
}
|
|
237
79
|
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
error: error instanceof Error ? error.message : String(error),
|
|
244
|
-
});
|
|
245
|
-
return spawnInlineUnavailableWorker(error);
|
|
246
|
-
}
|
|
80
|
+
function wrapSubprocess(
|
|
81
|
+
spawned: SpawnedSubprocess<SttWorkerOutbound>,
|
|
82
|
+
): WorkerHandle<SttWorkerInbound, SttWorkerOutbound> {
|
|
83
|
+
const { proc } = spawned;
|
|
84
|
+
return createWorkerHandle<SttWorkerInbound, SttWorkerOutbound>(spawned, message => safeSend(proc, message, "stt"));
|
|
247
85
|
}
|
|
248
86
|
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
87
|
+
function spawnSttWorker(): WorkerHandle<SttWorkerInbound, SttWorkerOutbound> {
|
|
88
|
+
return spawnWorkerOrUnavailable(
|
|
89
|
+
() => wrapSubprocess(createSttSubprocess()),
|
|
90
|
+
createUnavailableWorker<SttWorkerInbound, SttWorkerOutbound>,
|
|
91
|
+
"stt worker spawn failed; speech-to-text disabled",
|
|
92
|
+
);
|
|
253
93
|
}
|
|
254
94
|
|
|
255
95
|
export class SttClient {
|
|
256
|
-
#worker: WorkerHandle | null = null;
|
|
96
|
+
#worker: WorkerHandle<SttWorkerInbound, SttWorkerOutbound> | null = null;
|
|
257
97
|
#unsubscribeMessage: (() => void) | null = null;
|
|
258
98
|
#unsubscribeError: (() => void) | null = null;
|
|
259
99
|
#pending = new Map<string, PendingRequest>();
|
|
260
100
|
#streams = new Map<string, StreamState>();
|
|
261
101
|
#progressListeners = new Set<(event: SttProgressEvent) => void>();
|
|
262
102
|
#nextRequestId = 0;
|
|
263
|
-
#spawnWorker: () => WorkerHandle
|
|
103
|
+
#spawnWorker: () => WorkerHandle<SttWorkerInbound, SttWorkerOutbound>;
|
|
264
104
|
|
|
265
|
-
constructor(spawnWorker: () => WorkerHandle = spawnSttWorker) {
|
|
105
|
+
constructor(spawnWorker: () => WorkerHandle<SttWorkerInbound, SttWorkerOutbound> = spawnSttWorker) {
|
|
266
106
|
this.#spawnWorker = spawnWorker;
|
|
267
107
|
}
|
|
268
108
|
|
|
@@ -407,7 +247,7 @@ export class SttClient {
|
|
|
407
247
|
}
|
|
408
248
|
}
|
|
409
249
|
|
|
410
|
-
#ensureWorker(): WorkerHandle {
|
|
250
|
+
#ensureWorker(): WorkerHandle<SttWorkerInbound, SttWorkerOutbound> {
|
|
411
251
|
if (this.#worker) return this.#worker;
|
|
412
252
|
const worker = this.#spawnWorker();
|
|
413
253
|
this.#worker = worker;
|
|
@@ -497,25 +337,5 @@ export async function smokeTestSttWorker({
|
|
|
497
337
|
}: {
|
|
498
338
|
timeoutMs?: number;
|
|
499
339
|
} = {}): Promise<void> {
|
|
500
|
-
|
|
501
|
-
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
502
|
-
const timer = setTimeout(() => reject(new Error(`stt worker did not pong within ${timeoutMs}ms`)), timeoutMs);
|
|
503
|
-
const unsubscribeMessage = handle.onMessage(message => {
|
|
504
|
-
if (message.type === "pong") {
|
|
505
|
-
resolve();
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
if (message.type === "log") return;
|
|
509
|
-
reject(new Error(`stt worker: expected pong, got ${JSON.stringify(message)}`));
|
|
510
|
-
});
|
|
511
|
-
const unsubscribeError = handle.onError(reject);
|
|
512
|
-
try {
|
|
513
|
-
handle.send({ type: "ping", id: "smoke" } satisfies SttWorkerInbound);
|
|
514
|
-
await promise;
|
|
515
|
-
} finally {
|
|
516
|
-
clearTimeout(timer);
|
|
517
|
-
unsubscribeMessage();
|
|
518
|
-
unsubscribeError();
|
|
519
|
-
await handle.terminate();
|
|
520
|
-
}
|
|
340
|
+
await smokeTestWorker(wrapSubprocess(createSttSubprocess()), "stt worker", timeoutMs);
|
|
521
341
|
}
|