@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10
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 +94 -1
- package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
- package/dist/cli.js +13216 -15614
- package/dist/types/async/job-manager.d.ts +12 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +13 -0
- package/dist/types/discovery/agents.d.ts +11 -0
- package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -8
- package/dist/types/extensibility/hooks/types.d.ts +5 -5
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
- package/dist/types/hindsight/state.d.ts +0 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/lsp/utils.d.ts +8 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
- package/dist/types/modes/acp/acp-mode.d.ts +1 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
- package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
- package/dist/types/modes/components/agent-hub.d.ts +12 -5
- package/dist/types/modes/components/model-browser.d.ts +9 -1
- package/dist/types/modes/interactive-mode.d.ts +8 -1
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/registry/agent-lifecycle.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +47 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +8 -4
- package/dist/types/session/session-entries.d.ts +10 -0
- package/dist/types/session/session-loader.d.ts +11 -1
- package/dist/types/session/session-manager.d.ts +9 -1
- package/dist/types/session/session-tools.d.ts +10 -1
- package/dist/types/session/turn-recovery.d.ts +6 -2
- package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
- package/dist/types/slash-commands/available-commands.d.ts +1 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +2 -3
- package/dist/types/task/read-only-policy.d.ts +3 -0
- package/dist/types/task/structured-subagent.d.ts +2 -0
- package/dist/types/task/types.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +3 -3
- package/dist/types/tools/browser/launch.d.ts +3 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -2
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/read.d.ts +3 -5
- package/dist/types/tools/run-scope.d.ts +27 -1
- package/dist/types/tools/shell-tokenize.d.ts +21 -3
- package/dist/types/tools/terminal-output.d.ts +1 -1
- package/dist/types/utils/changelog.d.ts +3 -2
- package/dist/types/utils/late-cleanup.d.ts +2 -0
- package/dist/types/utils/turndown.d.ts +1 -1
- package/dist/types/vibe/runtime.d.ts +4 -3
- package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
- package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
- package/examples/custom-tools/README.md +1 -1
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/api-demo.ts +5 -6
- package/examples/extensions/chalk-logger.ts +1 -1
- package/examples/extensions/hello.ts +2 -2
- package/examples/extensions/reload-runtime.ts +1 -1
- package/examples/extensions/tools.ts +2 -2
- package/examples/extensions/with-deps/index.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/package.json +15 -31
- package/scripts/bundle-dist.ts +1 -11
- package/scripts/omp +11 -2
- package/src/advisor/runtime.ts +54 -0
- package/src/async/job-manager.ts +26 -0
- package/src/cli/agents-cli.ts +1 -1
- package/src/cli/args.ts +24 -2
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/claude-trace-cli.ts +1 -1
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/flag-tables.ts +4 -0
- package/src/cli/grep-cli.ts +1 -1
- package/src/cli/grievances-cli.ts +1 -1
- package/src/cli/help-extra.ts +1 -1
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/read-cli.ts +1 -1
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli/shell-cli.ts +1 -1
- package/src/cli/ssh-cli.ts +1 -1
- package/src/cli/stats-cli.ts +4 -4
- package/src/cli/tiny-models-cli.ts +1 -1
- package/src/cli/ttsr-cli.ts +1 -1
- package/src/cli/update-cli.ts +3 -57
- package/src/cli/usage-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli/worktree-cli.ts +1 -1
- package/src/commands/say.ts +1 -1
- package/src/commands/token.ts +1 -1
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/cli.ts +1 -1
- package/src/config/config-file.ts +2 -2
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-discovery.ts +36 -2
- package/src/config/model-registry.ts +4 -1
- package/src/config/model-resolver.ts +54 -7
- package/src/discovery/agents.ts +73 -3
- package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
- package/src/discovery/claude.ts +8 -5
- package/src/discovery/cursor.ts +8 -5
- package/src/discovery/gemini.ts +11 -8
- package/src/discovery/vscode.ts +1 -0
- package/src/discovery/windsurf.ts +4 -2
- package/src/edit/hashline/filesystem.ts +7 -7
- package/src/eval/agent-bridge.ts +1 -3
- package/src/eval/jl/prelude.jl +4 -4
- package/src/eval/js/shared/prelude.txt +2 -2
- package/src/eval/py/prelude.py +0 -3
- package/src/eval/py/runner.py +38 -1
- package/src/eval/rb/prelude.rb +1 -2
- package/src/extensibility/custom-commands/loader.ts +4 -4
- package/src/extensibility/custom-commands/types.ts +5 -5
- package/src/extensibility/custom-tools/loader.ts +4 -4
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +7 -6
- package/src/extensibility/extensions/types.ts +39 -8
- package/src/extensibility/hooks/loader.ts +4 -5
- package/src/extensibility/hooks/types.ts +5 -5
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
- package/src/hindsight/state.ts +2 -22
- package/src/index.ts +2 -2
- package/src/internal-urls/memory-protocol.ts +33 -0
- package/src/launch/terminal-output.ts +1 -1
- package/src/lsp/client.ts +2 -2
- package/src/lsp/utils.ts +29 -0
- package/src/main.ts +85 -21
- package/src/markit/converters/docx.ts +1 -1
- package/src/markit/converters/epub.ts +1 -1
- package/src/markit/converters/pptx.ts +1 -1
- package/src/markit/converters/xlsx.ts +1 -1
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/acp/acp-mode.ts +1 -1
- package/src/modes/components/agent-hub-projection.ts +248 -0
- package/src/modes/components/agent-hub-renderer.ts +194 -0
- package/src/modes/components/agent-hub.ts +670 -192
- package/src/modes/components/agent-transcript-viewer.ts +1 -3
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/bordered-loader.ts +1 -1
- package/src/modes/components/custom-editor.ts +2 -2
- package/src/modes/components/extensions/inspector-panel.ts +11 -6
- package/src/modes/components/footer.ts +1 -3
- package/src/modes/components/hook-input.ts +1 -1
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-browser.ts +11 -3
- package/src/modes/components/model-hub.ts +4 -1
- package/src/modes/components/session-selector.ts +2 -2
- package/src/modes/components/status-line/segments.ts +4 -3
- package/src/modes/controllers/event-controller.ts +188 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/mcp-command-controller.ts +81 -20
- package/src/modes/controllers/selector-controller.ts +40 -49
- package/src/modes/controllers/streaming-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/modes/print-mode.ts +29 -12
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/session-observer-registry.ts +5 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/tools/computer.md +1 -1
- package/src/registry/agent-lifecycle.ts +35 -10
- package/src/registry/agent-registry.ts +65 -2
- package/src/registry/persisted-agents.ts +341 -16
- package/src/sdk.ts +11 -5
- package/src/session/agent-session-types.ts +8 -0
- package/src/session/agent-session.ts +177 -188
- package/src/session/session-entries.ts +10 -0
- package/src/session/session-listing.ts +1 -1
- package/src/session/session-loader.ts +74 -8
- package/src/session/session-manager.ts +22 -2
- package/src/session/session-paths.ts +105 -110
- package/src/session/session-tools.ts +28 -1
- package/src/session/settings-stream-fn.ts +7 -3
- package/src/session/turn-recovery.ts +236 -18
- package/src/session/unexpected-stop-classifier.ts +12 -3
- package/src/slash-commands/acp-builtins.ts +1 -1
- package/src/slash-commands/available-commands.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +173 -47
- package/src/task/index.ts +9 -28
- package/src/task/isolation-runner.ts +43 -12
- package/src/task/persisted-revive.ts +12 -5
- package/src/task/read-only-policy.ts +27 -0
- package/src/task/structured-subagent.ts +31 -6
- package/src/task/types.ts +4 -0
- package/src/tiny/worker.ts +1 -1
- package/src/tools/auto-generated-guard.ts +1 -1
- package/src/tools/bash-interactive.ts +4 -4
- package/src/tools/bash-interceptor.ts +8 -4
- package/src/tools/bash.ts +6 -6
- package/src/tools/browser/cmux/cmux-tab.ts +66 -18
- package/src/tools/browser/launch.ts +76 -19
- package/src/tools/browser/readable.ts +9 -9
- package/src/tools/browser/tab-supervisor.ts +12 -3
- package/src/tools/browser/tab-worker-entry.ts +1 -1
- package/src/tools/browser/tab-worker.ts +101 -12
- package/src/tools/fetch.ts +1 -1
- package/src/tools/glob.ts +4 -2
- package/src/tools/path-utils.ts +22 -5
- package/src/tools/read.ts +22 -5
- package/src/tools/run-scope.ts +290 -4
- package/src/tools/shell-tokenize.ts +38 -9
- package/src/tools/terminal-output.ts +1 -1
- package/src/tools/todo.ts +1 -1
- package/src/utils/changelog.ts +5 -4
- package/src/utils/clipboard.ts +10 -2
- package/src/utils/external-editor.ts +4 -2
- package/src/utils/jj.ts +1 -1
- package/src/utils/late-cleanup.ts +17 -0
- package/src/utils/turndown.ts +1 -2
- package/src/vibe/runtime.ts +202 -58
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/hackage.ts +1 -12
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +2 -1
- package/src/web/scrapers/twitter.ts +2 -2
- package/src/web/scrapers/types.ts +1 -1
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +12 -39
- package/src/web/search/providers/codex.ts +3 -26
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/ecosia.ts +2 -2
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/firecrawl.ts +1 -1
- package/src/web/search/providers/google.ts +1 -1
- package/src/web/search/providers/mojeek.ts +2 -2
- package/src/web/search/providers/perplexity.ts +1 -1
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/startpage.ts +2 -2
- package/src/markit/converters/mammoth.d.ts +0 -24
|
@@ -84,6 +84,8 @@ export interface ModelChangeEntry extends SessionEntryBase {
|
|
|
84
84
|
model: string;
|
|
85
85
|
/** Role: "default", "smol", "slow", etc. Undefined treated as "default" */
|
|
86
86
|
role?: string;
|
|
87
|
+
/** True when this transition selected a retry-fallback model rather than the configured model. */
|
|
88
|
+
resolvedModelIsFallback?: boolean;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
export interface ServiceTierChangeEntry extends SessionEntryBase {
|
|
@@ -207,6 +209,14 @@ export interface SessionInitEntry extends SessionEntryBase {
|
|
|
207
209
|
task: string;
|
|
208
210
|
/** Tools available to the agent */
|
|
209
211
|
tools: string[];
|
|
212
|
+
/** Agent definition name (for example `scout` or `reviewer`). */
|
|
213
|
+
agent?: string;
|
|
214
|
+
/** Semantic model role declared by the agent, retained even after concrete model resolution. */
|
|
215
|
+
modelRole?: string;
|
|
216
|
+
/** Initially resolved provider/model selector for historical display. */
|
|
217
|
+
resolvedModel?: string;
|
|
218
|
+
/** Whether the agent definition is read-only, allowing an exact zero-LoC attribution. */
|
|
219
|
+
readOnly?: boolean;
|
|
210
220
|
/** Output schema if structured output was requested. */
|
|
211
221
|
outputSchema?: unknown;
|
|
212
222
|
/** Enforcement policy recorded with the output schema for faithful revival. */
|
|
@@ -2,7 +2,7 @@ import * as os from "node:os";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { Message } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { getAgentDir as getDefaultAgentDir, logger, parseJsonlLenient, toError } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import { LRUCache } from "
|
|
5
|
+
import { LRUCache } from "@oh-my-pi/pi-utils/lru";
|
|
6
6
|
import { computeDefaultSessionDir } from "./session-paths";
|
|
7
7
|
import { FileSessionStorage, type SessionStorage, type SessionStorageStat } from "./session-storage";
|
|
8
8
|
|
|
@@ -21,6 +21,19 @@ import {
|
|
|
21
21
|
} from "./session-title-slot";
|
|
22
22
|
|
|
23
23
|
const STREAM_LOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;
|
|
24
|
+
const STREAM_YIELD_BYTES = 1 * 1024 * 1024;
|
|
25
|
+
const STREAM_YIELD_ENTRIES = 8_192;
|
|
26
|
+
|
|
27
|
+
export interface VisitEntriesFromFileStreamOptions {
|
|
28
|
+
/** Stop after the visitor returns `false`. */
|
|
29
|
+
shouldContinue?: () => boolean;
|
|
30
|
+
/** Stop after this many valid or malformed JSONL records have been consumed. */
|
|
31
|
+
maxRecords?: number;
|
|
32
|
+
/** Yield to the macrotask queue after this many bytes have been consumed. */
|
|
33
|
+
yieldEveryBytes?: number;
|
|
34
|
+
/** Yield to the macrotask queue after this many entries have been visited. */
|
|
35
|
+
yieldEveryEntries?: number;
|
|
36
|
+
}
|
|
24
37
|
|
|
25
38
|
function splitTitleSlot(content: string): { body: string; slot: SessionTitleUpdate | undefined } {
|
|
26
39
|
const slot = titleUpdateFromSlot(parseTitleSlotFromContent(content));
|
|
@@ -59,11 +72,19 @@ export function parseSessionContent(content: string): {
|
|
|
59
72
|
/** Parse session JSONL and visit each entry without retaining prior entries. */
|
|
60
73
|
export async function visitEntriesFromFileStream(
|
|
61
74
|
filePath: string,
|
|
62
|
-
visit: (entry: FileEntry) => void,
|
|
75
|
+
visit: (entry: FileEntry) => void | boolean,
|
|
76
|
+
options: VisitEntriesFromFileStreamOptions = {},
|
|
63
77
|
): Promise<SessionTitleUpdate | undefined> {
|
|
64
78
|
let titleSlot: SessionTitleUpdate | undefined;
|
|
65
79
|
let sawFirstLine = false;
|
|
80
|
+
let bytesSinceYield = 0;
|
|
81
|
+
let entriesSinceYield = 0;
|
|
82
|
+
let recordsSeen = 0;
|
|
83
|
+
const maxRecords = Math.max(0, options.maxRecords ?? Number.POSITIVE_INFINITY);
|
|
84
|
+
let stopped = false;
|
|
66
85
|
let visitorThrew = false;
|
|
86
|
+
const yieldEveryBytes = Math.max(0, options.yieldEveryBytes ?? STREAM_YIELD_BYTES);
|
|
87
|
+
const yieldEveryEntries = Math.max(0, options.yieldEveryEntries ?? STREAM_YIELD_ENTRIES);
|
|
67
88
|
// Byte buffer (NOT a decoded string): multibyte UTF-8 sequences that straddle
|
|
68
89
|
// a stream-chunk boundary stay intact, and Bun.JSONL.parseChunk accepts typed
|
|
69
90
|
// arrays directly. Only the unconsumed remainder is held (≤ one record + a
|
|
@@ -72,22 +93,62 @@ export async function visitEntriesFromFileStream(
|
|
|
72
93
|
let buffer: Uint8Array = new Uint8Array();
|
|
73
94
|
const decoder = new TextDecoder();
|
|
74
95
|
|
|
75
|
-
const
|
|
76
|
-
|
|
96
|
+
const yieldToMacrotask = async (): Promise<void> => {
|
|
97
|
+
if (yieldEveryBytes === 0 && yieldEveryEntries === 0) return;
|
|
98
|
+
const bytesReady = yieldEveryBytes === 0 || bytesSinceYield < yieldEveryBytes;
|
|
99
|
+
const entriesReady = yieldEveryEntries === 0 || entriesSinceYield < yieldEveryEntries;
|
|
100
|
+
if (bytesReady && entriesReady) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
bytesSinceYield = 0;
|
|
104
|
+
entriesSinceYield = 0;
|
|
105
|
+
await Bun.sleep(0);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const drain = async (): Promise<void> => {
|
|
109
|
+
while (buffer.length > 0 && !stopped) {
|
|
110
|
+
if (recordsSeen >= maxRecords) {
|
|
111
|
+
stopped = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
77
114
|
const { values, error, read, done } = Bun.JSONL.parseChunk(buffer);
|
|
78
115
|
for (const value of values) {
|
|
116
|
+
if (recordsSeen >= maxRecords) {
|
|
117
|
+
stopped = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (options.shouldContinue && !options.shouldContinue()) {
|
|
121
|
+
stopped = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
79
124
|
try {
|
|
80
|
-
visit(value as FileEntry)
|
|
125
|
+
if (visit(value as FileEntry) === false) {
|
|
126
|
+
stopped = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
recordsSeen++;
|
|
130
|
+
entriesSinceYield++;
|
|
131
|
+
if (recordsSeen >= maxRecords) {
|
|
132
|
+
stopped = true;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
81
135
|
} catch (err) {
|
|
82
136
|
visitorThrew = true;
|
|
83
137
|
throw err;
|
|
84
138
|
}
|
|
139
|
+
await yieldToMacrotask();
|
|
85
140
|
}
|
|
141
|
+
if (stopped) break;
|
|
86
142
|
if (error) {
|
|
87
143
|
// Malformed record: skip past the next newline and continue.
|
|
88
144
|
const nextNewline = buffer.indexOf(0x0a, read);
|
|
89
145
|
if (nextNewline === -1) break; // rest of the bad line not yet received
|
|
146
|
+
recordsSeen++;
|
|
90
147
|
buffer = buffer.subarray(nextNewline + 1);
|
|
148
|
+
if (recordsSeen >= maxRecords) {
|
|
149
|
+
stopped = true;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
91
152
|
continue;
|
|
92
153
|
}
|
|
93
154
|
if (read === 0) break; // incomplete record awaiting more data
|
|
@@ -101,6 +162,8 @@ export async function visitEntriesFromFileStream(
|
|
|
101
162
|
|
|
102
163
|
try {
|
|
103
164
|
for await (const chunk of Bun.file(filePath).stream()) {
|
|
165
|
+
if (stopped) break;
|
|
166
|
+
bytesSinceYield += chunk.byteLength;
|
|
104
167
|
buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
|
|
105
168
|
// The optional fixed-width title slot is a physical first line that is
|
|
106
169
|
// NOT JSON; peel it before the parser would (correctly) reject it. The
|
|
@@ -121,14 +184,15 @@ export async function visitEntriesFromFileStream(
|
|
|
121
184
|
}
|
|
122
185
|
}
|
|
123
186
|
}
|
|
124
|
-
drain();
|
|
187
|
+
await drain();
|
|
188
|
+
await yieldToMacrotask();
|
|
125
189
|
}
|
|
126
190
|
// A trailing record without a final newline: terminate it so the parser
|
|
127
191
|
// can complete it (readline yielded it; parseChunk needs the delimiter).
|
|
128
|
-
if (buffer.length > 0 && buffer[buffer.length - 1] !== 0x0a) {
|
|
192
|
+
if (!stopped && buffer.length > 0 && buffer[buffer.length - 1] !== 0x0a) {
|
|
129
193
|
buffer = Buffer.concat([buffer, new Uint8Array([0x0a])]);
|
|
194
|
+
await drain();
|
|
130
195
|
}
|
|
131
|
-
drain();
|
|
132
196
|
} catch (err) {
|
|
133
197
|
if (visitorThrew) throw err;
|
|
134
198
|
if (isEnoent(err)) return undefined;
|
|
@@ -144,7 +208,9 @@ export async function loadEntriesFromFileStream(filePath: string): Promise<{
|
|
|
144
208
|
titleSlot: SessionTitleUpdate | undefined;
|
|
145
209
|
}> {
|
|
146
210
|
const entries: FileEntry[] = [];
|
|
147
|
-
const titleSlot = await visitEntriesFromFileStream(filePath, entry =>
|
|
211
|
+
const titleSlot = await visitEntriesFromFileStream(filePath, entry => {
|
|
212
|
+
entries.push(entry);
|
|
213
|
+
});
|
|
148
214
|
return { entries: foldTitleSlot(entries, titleSlot), titleSlot };
|
|
149
215
|
}
|
|
150
216
|
|
|
@@ -2047,9 +2047,16 @@ export class SessionManager {
|
|
|
2047
2047
|
* Append a model change as a child of the current leaf, then advance the leaf.
|
|
2048
2048
|
* @param model Model in "provider/modelId" format
|
|
2049
2049
|
* @param role Optional role (default: "default")
|
|
2050
|
+
* @param resolvedModelIsFallback Whether this transition selected a retry-fallback model
|
|
2050
2051
|
*/
|
|
2051
|
-
appendModelChange(model: string, role?: string): string {
|
|
2052
|
-
const entry: ModelChangeEntry = {
|
|
2052
|
+
appendModelChange(model: string, role?: string, resolvedModelIsFallback = false): string {
|
|
2053
|
+
const entry: ModelChangeEntry = {
|
|
2054
|
+
type: "model_change",
|
|
2055
|
+
...this.#freshEntryFields(),
|
|
2056
|
+
model,
|
|
2057
|
+
role,
|
|
2058
|
+
resolvedModelIsFallback,
|
|
2059
|
+
};
|
|
2053
2060
|
this.#recordEntry(entry);
|
|
2054
2061
|
return entry.id;
|
|
2055
2062
|
}
|
|
@@ -2058,6 +2065,10 @@ export class SessionManager {
|
|
|
2058
2065
|
systemPrompt: string;
|
|
2059
2066
|
task: string;
|
|
2060
2067
|
tools: string[];
|
|
2068
|
+
agent?: string;
|
|
2069
|
+
modelRole?: string;
|
|
2070
|
+
resolvedModel?: string;
|
|
2071
|
+
readOnly?: boolean;
|
|
2061
2072
|
outputSchema?: unknown;
|
|
2062
2073
|
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
2063
2074
|
restrictToolNames?: boolean;
|
|
@@ -2547,6 +2558,9 @@ export class SessionManager {
|
|
|
2547
2558
|
systemPrompt: string;
|
|
2548
2559
|
task: string;
|
|
2549
2560
|
tools: string[];
|
|
2561
|
+
agent?: string;
|
|
2562
|
+
modelRole?: string;
|
|
2563
|
+
resolvedModel?: string;
|
|
2550
2564
|
outputSchema?: unknown;
|
|
2551
2565
|
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
2552
2566
|
restrictToolNames?: boolean;
|
|
@@ -2567,6 +2581,9 @@ export class SessionManager {
|
|
|
2567
2581
|
systemPrompt: string;
|
|
2568
2582
|
task: string;
|
|
2569
2583
|
tools: string[];
|
|
2584
|
+
agent?: string;
|
|
2585
|
+
modelRole?: string;
|
|
2586
|
+
resolvedModel?: string;
|
|
2570
2587
|
outputSchema?: unknown;
|
|
2571
2588
|
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
2572
2589
|
restrictToolNames?: boolean;
|
|
@@ -2580,6 +2597,9 @@ export class SessionManager {
|
|
|
2580
2597
|
systemPrompt: entry.systemPrompt,
|
|
2581
2598
|
task: entry.task,
|
|
2582
2599
|
tools: entry.tools,
|
|
2600
|
+
agent: entry.agent,
|
|
2601
|
+
modelRole: entry.modelRole,
|
|
2602
|
+
resolvedModel: entry.resolvedModel,
|
|
2583
2603
|
outputSchema: entry.outputSchema,
|
|
2584
2604
|
outputSchemaMode: entry.outputSchemaMode,
|
|
2585
2605
|
restrictToolNames: entry.restrictToolNames,
|
|
@@ -2,17 +2,10 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getTerminalId } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import {
|
|
6
|
-
getSessionsDir,
|
|
7
|
-
getTerminalSessionsDir,
|
|
8
|
-
isEnoent,
|
|
9
|
-
isRecord,
|
|
10
|
-
logger,
|
|
11
|
-
resolveEquivalentPath,
|
|
12
|
-
} from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { getSessionsDir, getTerminalSessionsDir, isEnoent, logger, resolveEquivalentPath } from "@oh-my-pi/pi-utils";
|
|
13
6
|
import type { SessionStorage } from "./session-storage";
|
|
14
7
|
|
|
15
|
-
const
|
|
8
|
+
const migratedSessionRoots = new Set<string>();
|
|
16
9
|
|
|
17
10
|
/**
|
|
18
11
|
* Merge or rename a legacy session directory into its canonical target.
|
|
@@ -24,11 +17,13 @@ function migrateSessionDirPath(oldPath: string, newPath: string): void {
|
|
|
24
17
|
for (const file of fs.readdirSync(oldPath)) {
|
|
25
18
|
const src = path.join(oldPath, file);
|
|
26
19
|
const dst = path.join(newPath, file);
|
|
27
|
-
if (
|
|
28
|
-
|
|
20
|
+
if (fs.existsSync(dst)) {
|
|
21
|
+
logger.warn("Session directory migration collision; preserving legacy entry", { src, dst });
|
|
22
|
+
continue;
|
|
29
23
|
}
|
|
24
|
+
fs.renameSync(src, dst);
|
|
30
25
|
}
|
|
31
|
-
fs.
|
|
26
|
+
fs.rmdirSync(oldPath);
|
|
32
27
|
return;
|
|
33
28
|
}
|
|
34
29
|
if (existing) {
|
|
@@ -42,14 +37,31 @@ function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
|
|
|
42
37
|
return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
function
|
|
40
|
+
function encodeRelativeSessionDirName(prefix: string, relative: string): string {
|
|
46
41
|
const encoded = relative.replace(/[/\\:]/g, "-");
|
|
47
42
|
return encoded ? (prefix.endsWith("-") ? `${prefix}${encoded}` : `${prefix}-${encoded}`) : prefix;
|
|
48
43
|
}
|
|
49
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Reconstruct the short-lived hashed session dir name used by 17.2.5-17.2.8
|
|
47
|
+
* (reverted PR #7397): `<scope>-<readable>-<sha256hex>` keyed by the canonical
|
|
48
|
+
* cwd. Kept only so {@link migrateHashedSessionDir} can recover sessions
|
|
49
|
+
* stranded when 17.2.9 restored the legacy names without a reverse migration.
|
|
50
|
+
*/
|
|
51
|
+
function encodeHashedSessionDirName(canonicalCwd: string, scope: "home" | "tmp" | "abs"): string {
|
|
52
|
+
const normalized = canonicalCwd.replaceAll("\\", "/");
|
|
53
|
+
const readable = path
|
|
54
|
+
.basename(canonicalCwd)
|
|
55
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
56
|
+
.replace(/^-+|-+$/g, "")
|
|
57
|
+
.slice(-80);
|
|
58
|
+
const digest = Bun.SHA256.hash(normalized, "hex");
|
|
59
|
+
return `${scope}-${readable || "project"}-${digest}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
function getDefaultSessionDirName(cwd: string): {
|
|
51
63
|
encodedDirName: string;
|
|
52
|
-
|
|
64
|
+
hashedDirName: string;
|
|
53
65
|
resolvedCwd: string;
|
|
54
66
|
} {
|
|
55
67
|
const resolvedCwd = path.resolve(cwd);
|
|
@@ -60,111 +72,106 @@ function getDefaultSessionDirName(cwd: string): {
|
|
|
60
72
|
const canonicalTempRoot = resolveEquivalentPath(tempRoot);
|
|
61
73
|
const homeRelative = path.relative(canonicalHome, canonicalCwd);
|
|
62
74
|
const tempRelative = path.relative(canonicalTempRoot, canonicalCwd);
|
|
63
|
-
|
|
75
|
+
let encodedDirName: string;
|
|
64
76
|
let scope: "home" | "tmp" | "abs";
|
|
65
|
-
let legacyRelativeDirName: string | undefined;
|
|
66
77
|
if (homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))) {
|
|
78
|
+
encodedDirName = encodeRelativeSessionDirName("-", homeRelative);
|
|
67
79
|
scope = "home";
|
|
68
|
-
legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-", homeRelative);
|
|
69
80
|
} else if (tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))) {
|
|
81
|
+
encodedDirName = encodeRelativeSessionDirName("-tmp", tempRelative);
|
|
70
82
|
scope = "tmp";
|
|
71
|
-
legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-tmp", tempRelative);
|
|
72
83
|
} else {
|
|
84
|
+
encodedDirName = encodeLegacyAbsoluteSessionDirName(canonicalCwd);
|
|
73
85
|
scope = "abs";
|
|
74
86
|
}
|
|
75
|
-
|
|
76
|
-
const normalized = canonicalCwd.replaceAll("\\", "/");
|
|
77
|
-
const readable = path
|
|
78
|
-
.basename(canonicalCwd)
|
|
79
|
-
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
80
|
-
.replace(/^-+|-+$/g, "")
|
|
81
|
-
.slice(-80);
|
|
82
|
-
const digest = Bun.SHA256.hash(normalized, "hex");
|
|
83
|
-
const encodedDirName = `${scope}-${readable || "project"}-${digest}`;
|
|
84
|
-
return { encodedDirName, legacyRelativeDirName, resolvedCwd };
|
|
87
|
+
return { encodedDirName, hashedDirName: encodeHashedSessionDirName(canonicalCwd, scope), resolvedCwd };
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
|
|
92
|
+
* Runs once per sessions root on first access, best-effort.
|
|
93
|
+
*/
|
|
94
|
+
function migrateHomeSessionDirs(sessionsRoot: string): void {
|
|
95
|
+
if (migratedSessionRoots.has(sessionsRoot)) return;
|
|
96
|
+
migratedSessionRoots.add(sessionsRoot);
|
|
97
|
+
|
|
98
|
+
const home = os.homedir();
|
|
99
|
+
const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
|
|
100
|
+
const oldPrefix = `--${homeEncoded}-`;
|
|
101
|
+
const oldExact = `--${homeEncoded}--`;
|
|
102
|
+
|
|
103
|
+
let entries: string[];
|
|
89
104
|
try {
|
|
90
|
-
|
|
91
|
-
const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
92
|
-
const prefix = buffer.toString("utf8", 0, bytesRead);
|
|
93
|
-
for (const line of prefix.split(/\r?\n/, 3)) {
|
|
94
|
-
try {
|
|
95
|
-
const record: unknown = JSON.parse(line);
|
|
96
|
-
if (isRecord(record) && record.type === "session" && typeof record.cwd === "string") {
|
|
97
|
-
return record.cwd;
|
|
98
|
-
}
|
|
99
|
-
} catch {
|
|
100
|
-
// Ignore title slots or truncated/corrupt headers.
|
|
101
|
-
}
|
|
102
|
-
}
|
|
105
|
+
entries = fs.readdirSync(sessionsRoot);
|
|
103
106
|
} catch {
|
|
104
|
-
|
|
105
|
-
} finally {
|
|
106
|
-
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
107
|
+
return;
|
|
107
108
|
}
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
109
|
|
|
111
|
-
function moveSessionBundle(sourceDir: string, targetDir: string, sessionName: string, entries: string[]): void {
|
|
112
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
113
|
-
const artifactsName = path.basename(sessionName, ".jsonl");
|
|
114
110
|
for (const entry of entries) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (!existing) {
|
|
121
|
-
fs.renameSync(source, target);
|
|
122
|
-
} else if (existing.isDirectory() && fs.statSync(source).isDirectory()) {
|
|
123
|
-
migrateSessionDirPath(source, target);
|
|
111
|
+
let remainder: string;
|
|
112
|
+
if (entry === oldExact) {
|
|
113
|
+
remainder = "";
|
|
114
|
+
} else if (entry.startsWith(oldPrefix) && entry.endsWith("--")) {
|
|
115
|
+
remainder = entry.slice(oldPrefix.length, -2);
|
|
124
116
|
} else {
|
|
125
|
-
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const newName = remainder ? `-${remainder}` : "-";
|
|
121
|
+
const oldPath = path.join(sessionsRoot, entry);
|
|
122
|
+
const newPath = path.join(sessionsRoot, newName);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
migrateSessionDirPath(oldPath, newPath);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
logger.warn("Failed to migrate legacy home session directory", {
|
|
128
|
+
oldPath,
|
|
129
|
+
newPath,
|
|
130
|
+
error: String(error),
|
|
131
|
+
});
|
|
126
132
|
}
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
legacyDir
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
136
|
+
function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string, sessionsRoot: string): void {
|
|
137
|
+
const legacyDir = path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(cwd));
|
|
138
|
+
if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
migrateSessionDirPath(legacyDir, sessionDir);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logger.warn("Failed to migrate legacy session directory", {
|
|
144
|
+
oldPath: legacyDir,
|
|
145
|
+
newPath: sessionDir,
|
|
146
|
+
error: String(error),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Migrate a 17.2.5-17.2.8 hashed session dir back into its legacy path-based
|
|
153
|
+
* directory. The 17.2.9 revert restored the legacy names but dropped migration,
|
|
154
|
+
* stranding sessions written under the hashed scheme (issue #7677). Best-effort.
|
|
155
|
+
*/
|
|
156
|
+
function migrateHashedSessionDir(hashedDirName: string, sessionDir: string, sessionsRoot: string): void {
|
|
157
|
+
const hashedDir = path.join(sessionsRoot, hashedDirName);
|
|
158
|
+
if (hashedDir === sessionDir || !fs.existsSync(hashedDir)) return;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
migrateSessionDirPath(hashedDir, sessionDir);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
logger.warn("Failed to migrate hashed session directory", {
|
|
164
|
+
oldPath: hashedDir,
|
|
165
|
+
newPath: sessionDir,
|
|
166
|
+
error: String(error),
|
|
167
|
+
});
|
|
157
168
|
}
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
export function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
|
|
161
172
|
const currentDirName = path.basename(sessionDir);
|
|
162
|
-
const { encodedDirName
|
|
163
|
-
if (
|
|
164
|
-
currentDirName !== encodedDirName &&
|
|
165
|
-
currentDirName !== legacyRelativeDirName &&
|
|
166
|
-
currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)
|
|
167
|
-
) {
|
|
173
|
+
const { encodedDirName } = getDefaultSessionDirName(cwd);
|
|
174
|
+
if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) {
|
|
168
175
|
return undefined;
|
|
169
176
|
}
|
|
170
177
|
return path.dirname(sessionDir);
|
|
@@ -180,23 +187,11 @@ export function computeDefaultSessionDir(
|
|
|
180
187
|
storage: SessionStorage,
|
|
181
188
|
sessionsRoot: string = getSessionsDir(),
|
|
182
189
|
): string {
|
|
183
|
-
const { encodedDirName,
|
|
190
|
+
const { encodedDirName, hashedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
|
|
191
|
+
migrateHomeSessionDirs(sessionsRoot);
|
|
184
192
|
const sessionDir = path.join(sessionsRoot, encodedDirName);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
{ kind: "absolute", name: encodeLegacyAbsoluteSessionDirName(resolvedCwd) },
|
|
188
|
-
];
|
|
189
|
-
for (const legacy of legacyDirs) {
|
|
190
|
-
if (!legacy.name) continue;
|
|
191
|
-
const legacyDir = path.join(sessionsRoot, legacy.name);
|
|
192
|
-
if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) continue;
|
|
193
|
-
try {
|
|
194
|
-
rerouteCollidingSessions(resolvedCwd, legacyDir, sessionsRoot, legacy.kind);
|
|
195
|
-
migrateSessionDirPath(legacyDir, sessionDir);
|
|
196
|
-
} catch {
|
|
197
|
-
// Best effort
|
|
198
|
-
}
|
|
199
|
-
}
|
|
193
|
+
migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot);
|
|
194
|
+
migrateHashedSessionDir(hashedDirName, sessionDir, sessionsRoot);
|
|
200
195
|
storage.ensureDirSync(sessionDir);
|
|
201
196
|
return sessionDir;
|
|
202
197
|
}
|
|
@@ -7,7 +7,7 @@ import { formatModelString } from "../config/model-resolver";
|
|
|
7
7
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
8
8
|
import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
|
|
9
9
|
import { CustomToolAdapter } from "../extensibility/custom-tools/wrapper";
|
|
10
|
-
import type { ExtensionRunner } from "../extensibility/extensions";
|
|
10
|
+
import type { ExtensionRunner, SourceInfo, ToolInfo } from "../extensibility/extensions";
|
|
11
11
|
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
12
12
|
import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "../extensibility/skills";
|
|
13
13
|
import { type LocalProtocolOptions, XD_URL_PREFIX } from "../internal-urls";
|
|
@@ -333,6 +333,33 @@ export class SessionTools {
|
|
|
333
333
|
return Array.from(this.#toolRegistry.keys());
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Full metadata for every registered tool, including source provenance.
|
|
338
|
+
*
|
|
339
|
+
* Backs the `getAllTools()` ExtensionAPI method. Returns {@link ToolInfo}
|
|
340
|
+
* objects (not bare names) so extensions authored against upstream
|
|
341
|
+
* `@earendil-works/pi-coding-agent` — which promises `ToolInfo[]` — can read
|
|
342
|
+
* `sourceInfo.source` unchanged.
|
|
343
|
+
*/
|
|
344
|
+
getAllToolInfos(): ToolInfo[] {
|
|
345
|
+
return Array.from(this.#toolRegistry, ([name, tool]) => {
|
|
346
|
+
const source = this.#builtInToolNames.has(name)
|
|
347
|
+
? "builtin"
|
|
348
|
+
: isMCPToolName(name)
|
|
349
|
+
? "mcp"
|
|
350
|
+
: this.#rpcHostToolNames.has(name)
|
|
351
|
+
? "sdk"
|
|
352
|
+
: "extension";
|
|
353
|
+
const sourceInfo: SourceInfo = {
|
|
354
|
+
path: `<${source}:${name}>`,
|
|
355
|
+
source,
|
|
356
|
+
scope: "temporary",
|
|
357
|
+
origin: "top-level",
|
|
358
|
+
};
|
|
359
|
+
return { name, description: tool.description, parameters: tool.parameters, sourceInfo };
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
336
363
|
#wrapRuntimeTool(tool: AgentTool): AgentTool {
|
|
337
364
|
const wrapped = wrapToolWithMetaNotice(tool);
|
|
338
365
|
const extensionRunner = this.#host.extensionRunner();
|
|
@@ -34,9 +34,13 @@ export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn =
|
|
|
34
34
|
openrouterRoutingPreset && openrouterRoutingPreset !== "default" ? openrouterRoutingPreset : undefined;
|
|
35
35
|
const antigravityEndpointMode = settings.get("providers.antigravityEndpoint");
|
|
36
36
|
const textVerbosity =
|
|
37
|
-
model.api === "openai-codex-responses"
|
|
38
|
-
? settings.
|
|
39
|
-
|
|
37
|
+
model.api === "openai-codex-responses"
|
|
38
|
+
? settings.isConfigured("textVerbosity")
|
|
39
|
+
? settings.get("textVerbosity")
|
|
40
|
+
: undefined
|
|
41
|
+
: model.api === "openai-responses"
|
|
42
|
+
? settings.get("textVerbosity")
|
|
43
|
+
: undefined;
|
|
40
44
|
const streamFirstEventTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamFirstEventTimeoutSeconds"));
|
|
41
45
|
const streamIdleTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamIdleTimeoutSeconds"));
|
|
42
46
|
// Server-side fallback (opt-in): when the user enables it AND the
|