@oh-my-pi/pi-coding-agent 15.9.0 → 15.9.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 +67 -0
- package/dist/types/cli/dry-balance-cli.d.ts +104 -0
- package/dist/types/cli/update-cli.d.ts +15 -1
- package/dist/types/commands/dry-balance.d.ts +31 -0
- package/dist/types/config/append-only-context-mode.d.ts +8 -0
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/models-config-schema.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/config/settings.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/exa/mcp-client.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
- package/dist/types/hindsight/bank.d.ts +17 -9
- package/dist/types/hindsight/mental-models.d.ts +1 -1
- package/dist/types/hindsight/state.d.ts +9 -3
- package/dist/types/mcp/json-rpc.d.ts +6 -1
- package/dist/types/mcp/manager.d.ts +1 -1
- package/dist/types/mcp/tool-bridge.d.ts +4 -0
- package/dist/types/mnemopi/state.d.ts +2 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +40 -8
- package/dist/types/modes/components/session-selector.d.ts +8 -3
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +13 -1
- package/dist/types/session/auth-storage.d.ts +2 -2
- package/dist/types/session/history-storage.d.ts +3 -4
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +17 -4
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tiny/text.d.ts +17 -0
- package/dist/types/tools/index.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +11 -0
- package/dist/types/web/search/providers/base.d.ts +14 -0
- package/dist/types/web/search/providers/exa.d.ts +9 -0
- package/dist/types/web/search/providers/perplexity.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +2 -1
- package/package.json +9 -9
- package/src/cli/dry-balance-cli.ts +823 -0
- package/src/cli/session-picker.ts +1 -0
- package/src/cli/update-cli.ts +54 -2
- package/src/cli-commands.ts +1 -0
- package/src/commands/completions.ts +1 -1
- package/src/commands/dry-balance.ts +43 -0
- package/src/config/append-only-context-mode.ts +37 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/models-config-schema.ts +3 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/config/settings.ts +38 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
- package/src/discovery/github.ts +37 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/exa/mcp-client.ts +11 -5
- package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
- package/src/hindsight/backend.ts +184 -35
- package/src/hindsight/bank.ts +32 -22
- package/src/hindsight/mental-models.ts +1 -1
- package/src/hindsight/state.ts +21 -7
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/omp-protocol.ts +8 -2
- package/src/main.ts +4 -2
- package/src/mcp/json-rpc.ts +8 -0
- package/src/mcp/manager.ts +40 -21
- package/src/mcp/render.ts +3 -0
- package/src/mcp/tool-bridge.ts +10 -2
- package/src/mcp/transports/http.ts +33 -16
- package/src/mnemopi/state.ts +4 -4
- package/src/modes/acp/acp-agent.ts +168 -3
- package/src/modes/components/agent-dashboard.ts +103 -31
- package/src/modes/components/extensions/extension-dashboard.ts +56 -10
- package/src/modes/components/history-search.ts +128 -14
- package/src/modes/components/plugin-settings.ts +270 -36
- package/src/modes/components/session-selector.ts +45 -14
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/tips.txt +5 -1
- package/src/modes/components/transcript-container.ts +35 -6
- package/src/modes/components/tree-selector.ts +29 -2
- package/src/modes/controllers/command-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +18 -7
- package/src/modes/controllers/selector-controller.ts +30 -19
- package/src/modes/interactive-mode.ts +38 -3
- package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
- package/src/modes/utils/keybinding-matchers.ts +10 -0
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/librarian.md +1 -0
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/steering/user-interjection.md +10 -0
- package/src/prompts/system/agent-creation-architect.md +1 -26
- package/src/prompts/system/system-prompt.md +143 -145
- package/src/prompts/system/title-system.md +3 -2
- package/src/prompts/tools/browser.md +29 -29
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/sdk.ts +87 -30
- package/src/session/agent-session.ts +96 -14
- package/src/session/auth-storage.ts +4 -0
- package/src/session/history-storage.ts +11 -18
- package/src/session/messages.ts +80 -0
- package/src/session/session-manager.ts +7 -1
- package/src/slash-commands/types.ts +27 -10
- package/src/task/executor.ts +6 -2
- package/src/task/index.ts +8 -7
- package/src/task/types.ts +2 -0
- package/src/tiny/text.ts +112 -1
- package/src/tools/bash.ts +3 -4
- package/src/tools/index.ts +16 -0
- package/src/tools/job.ts +3 -3
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +3 -3
- package/src/tools/path-utils.ts +21 -0
- package/src/tools/search.ts +18 -1
- package/src/tools/ssh.ts +26 -10
- package/src/tools/write.ts +14 -2
- package/src/tui/status-line.ts +15 -4
- package/src/utils/file-mentions.ts +7 -107
- package/src/utils/title-generator.ts +66 -38
- package/src/web/search/index.ts +3 -1
- package/src/web/search/provider.ts +1 -1
- package/src/web/search/providers/base.ts +17 -0
- package/src/web/search/providers/exa.ts +111 -7
- package/src/web/search/providers/perplexity.ts +8 -4
- package/src/web/search/types.ts +2 -1
package/src/sdk.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
extractRetryHint,
|
|
28
28
|
getAgentDbPath,
|
|
29
29
|
getAgentDir,
|
|
30
|
+
getAuthBrokerSnapshotCachePath,
|
|
30
31
|
getProjectDir,
|
|
31
32
|
logger,
|
|
32
33
|
postmortem,
|
|
@@ -39,6 +40,7 @@ import { createAutoresearchExtension } from "./autoresearch";
|
|
|
39
40
|
import { loadCapability } from "./capability";
|
|
40
41
|
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
|
|
41
42
|
import { bucketRules } from "./capability/rule-buckets";
|
|
43
|
+
import { shouldEnableAppendOnlyContext } from "./config/append-only-context-mode";
|
|
42
44
|
import { ModelRegistry } from "./config/model-registry";
|
|
43
45
|
import {
|
|
44
46
|
formatModelString,
|
|
@@ -100,8 +102,16 @@ import {
|
|
|
100
102
|
} from "./secrets";
|
|
101
103
|
import { AgentSession } from "./session/agent-session";
|
|
102
104
|
import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
|
|
103
|
-
import {
|
|
104
|
-
|
|
105
|
+
import {
|
|
106
|
+
AuthBrokerClient,
|
|
107
|
+
AuthStorage,
|
|
108
|
+
DEFAULT_SNAPSHOT_CACHE_TTL_MS,
|
|
109
|
+
RemoteAuthCredentialStore,
|
|
110
|
+
readAuthBrokerSnapshotCache,
|
|
111
|
+
type SnapshotResponse,
|
|
112
|
+
writeAuthBrokerSnapshotCache,
|
|
113
|
+
} from "./session/auth-storage";
|
|
114
|
+
import { type CustomMessage, convertToLlm, wrapSteeringForModel } from "./session/messages";
|
|
105
115
|
import { getRestorableSessionModels, SessionManager } from "./session/session-manager";
|
|
106
116
|
import { closeAllConnections } from "./ssh/connection-manager";
|
|
107
117
|
import { unmountAll } from "./ssh/sshfs-mount";
|
|
@@ -417,6 +427,17 @@ function getDefaultAgentDir(): string {
|
|
|
417
427
|
return getAgentDir();
|
|
418
428
|
}
|
|
419
429
|
|
|
430
|
+
function resolveSnapshotTtlMs(): number {
|
|
431
|
+
const raw = process.env.OMP_AUTH_BROKER_SNAPSHOT_TTL_MS;
|
|
432
|
+
if (raw === undefined) return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
433
|
+
const value = raw.trim();
|
|
434
|
+
if (value === "") return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
435
|
+
const ttlMs = Number(value);
|
|
436
|
+
if (Number.isFinite(ttlMs) && ttlMs >= 0) return ttlMs;
|
|
437
|
+
logger.warn("Invalid OMP_AUTH_BROKER_SNAPSHOT_TTL_MS; using default", { value: raw });
|
|
438
|
+
return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
439
|
+
}
|
|
440
|
+
|
|
420
441
|
// Discovery Functions
|
|
421
442
|
|
|
422
443
|
/**
|
|
@@ -434,9 +455,42 @@ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir(
|
|
|
434
455
|
const brokerConfig = await resolveAuthBrokerConfig();
|
|
435
456
|
if (brokerConfig) {
|
|
436
457
|
const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
const
|
|
458
|
+
const ttlMs = resolveSnapshotTtlMs();
|
|
459
|
+
const cachePath = getAuthBrokerSnapshotCachePath();
|
|
460
|
+
const persist =
|
|
461
|
+
ttlMs > 0
|
|
462
|
+
? (snapshot: SnapshotResponse): void => {
|
|
463
|
+
void writeAuthBrokerSnapshotCache({
|
|
464
|
+
path: cachePath,
|
|
465
|
+
token: brokerConfig.token,
|
|
466
|
+
url: brokerConfig.url,
|
|
467
|
+
snapshot,
|
|
468
|
+
}).catch(error => {
|
|
469
|
+
logger.debug("auth-broker snapshot cache write failed", { error: String(error) });
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
: undefined;
|
|
473
|
+
|
|
474
|
+
let initialSnapshot: SnapshotResponse | undefined;
|
|
475
|
+
if (ttlMs > 0) {
|
|
476
|
+
initialSnapshot =
|
|
477
|
+
(await readAuthBrokerSnapshotCache({
|
|
478
|
+
path: cachePath,
|
|
479
|
+
token: brokerConfig.token,
|
|
480
|
+
url: brokerConfig.url,
|
|
481
|
+
ttlMs,
|
|
482
|
+
}).catch(error => {
|
|
483
|
+
logger.debug("auth-broker snapshot cache read failed", { error: String(error) });
|
|
484
|
+
return null;
|
|
485
|
+
})) ?? undefined;
|
|
486
|
+
}
|
|
487
|
+
if (!initialSnapshot) {
|
|
488
|
+
const initialResult = await client.fetchSnapshot();
|
|
489
|
+
if (initialResult.status !== 200) throw new Error("Auth broker returned no initial snapshot");
|
|
490
|
+
initialSnapshot = initialResult.snapshot;
|
|
491
|
+
persist?.(initialSnapshot);
|
|
492
|
+
}
|
|
493
|
+
const store = new RemoteAuthCredentialStore({ client, initialSnapshot, onSnapshot: persist });
|
|
440
494
|
// Refresh + usage hooks live on RemoteAuthCredentialStore; AuthStorage
|
|
441
495
|
// discovers them automatically when no explicit option overrides them.
|
|
442
496
|
const storage = new AuthStorage(store, {
|
|
@@ -646,24 +700,6 @@ function registerPythonCleanup(): void {
|
|
|
646
700
|
postmortem.register("python-cleanup", disposeAllKernelSessions);
|
|
647
701
|
}
|
|
648
702
|
|
|
649
|
-
/**
|
|
650
|
-
* Resolve whether to enable append-only context mode based on the setting and provider.
|
|
651
|
-
*
|
|
652
|
-
* - `"on"` → always enable
|
|
653
|
-
* - `"off"` → never enable
|
|
654
|
-
* - `"auto"` → enable for DeepSeek (prefix-caching provider)
|
|
655
|
-
*/
|
|
656
|
-
function resolveAppendOnlyMode(setting: "auto" | "on" | "off" | undefined, provider: string): boolean {
|
|
657
|
-
switch (setting ?? "auto") {
|
|
658
|
-
case "on":
|
|
659
|
-
return true;
|
|
660
|
-
case "off":
|
|
661
|
-
return false;
|
|
662
|
-
default:
|
|
663
|
-
return provider === "deepseek";
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
703
|
function customToolToDefinition(tool: CustomTool): ToolDefinition {
|
|
668
704
|
const definition: ToolDefinition & { [TOOL_DEFINITION_MARKER]: true } = {
|
|
669
705
|
name: tool.name,
|
|
@@ -1185,12 +1221,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1185
1221
|
|
|
1186
1222
|
return preview;
|
|
1187
1223
|
};
|
|
1188
|
-
// Only top-level
|
|
1189
|
-
// parent's manager via `AsyncJobManager.instance()`
|
|
1190
|
-
//
|
|
1191
|
-
//
|
|
1224
|
+
// Only the first top-level session in a process owns an AsyncJobManager.
|
|
1225
|
+
// Subagents inherit the parent's manager via `AsyncJobManager.instance()`
|
|
1226
|
+
// (set below), and any additional top-level session spun up in-process
|
|
1227
|
+
// (e.g. the agent-creation architect in `agent-dashboard.ts`) must share
|
|
1228
|
+
// the live singleton — otherwise its dispose path would clobber the
|
|
1229
|
+
// owning session's manager and break the `task`/`bash` async paths
|
|
1230
|
+
// (issue #1923). The `instance()` guard means later sessions also skip
|
|
1231
|
+
// constructing an orphaned manager that nothing would ever route to.
|
|
1192
1232
|
const asyncJobManager =
|
|
1193
|
-
backgroundJobsEnabled && !options.parentTaskPrefix
|
|
1233
|
+
backgroundJobsEnabled && !options.parentTaskPrefix && !AsyncJobManager.instance()
|
|
1194
1234
|
? new AsyncJobManager({
|
|
1195
1235
|
maxRunningJobs: asyncMaxJobs,
|
|
1196
1236
|
onJobComplete: async (jobId, result, job) => {
|
|
@@ -1209,6 +1249,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1209
1249
|
})
|
|
1210
1250
|
: undefined;
|
|
1211
1251
|
|
|
1252
|
+
const scopedAsyncJobManager = asyncJobManager ?? (options.parentTaskPrefix ? AsyncJobManager.instance() : undefined);
|
|
1253
|
+
|
|
1212
1254
|
const agentRegistry = options.agentRegistry ?? AgentRegistry.global();
|
|
1213
1255
|
const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
|
|
1214
1256
|
const resolvedAgentDisplayName =
|
|
@@ -1310,6 +1352,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1310
1352
|
authStorage,
|
|
1311
1353
|
modelRegistry,
|
|
1312
1354
|
getTelemetry: () => agent?.telemetry,
|
|
1355
|
+
// Subagents inherit the singleton (the parent's manager) so their bash/task
|
|
1356
|
+
// completions still flow into the spawning conversation's yieldQueue.
|
|
1357
|
+
// Secondary in-process top-level sessions (no parentTaskPrefix, no
|
|
1358
|
+
// constructed manager because the singleton was already installed) leave
|
|
1359
|
+
// this undefined so tools and session job snapshots refuse async work
|
|
1360
|
+
// instead of silently routing into the owning session (issue #1923).
|
|
1361
|
+
asyncJobManager: scopedAsyncJobManager,
|
|
1313
1362
|
};
|
|
1314
1363
|
|
|
1315
1364
|
// Wire process-wide internal URL singletons owned by their real classes.
|
|
@@ -1905,7 +1954,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1905
1954
|
return obfuscateMessages(obfuscator, converted);
|
|
1906
1955
|
};
|
|
1907
1956
|
const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal) => {
|
|
1908
|
-
|
|
1957
|
+
const withContext = await extensionRunner.emitContext(messages);
|
|
1958
|
+
return wrapSteeringForModel(withContext);
|
|
1909
1959
|
};
|
|
1910
1960
|
const onPayload = async (payload: unknown, _model?: Model) => {
|
|
1911
1961
|
return await extensionRunner.emitBeforeProviderRequest(payload);
|
|
@@ -2028,7 +2078,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2028
2078
|
getToolChoice: () => session?.nextToolChoice(),
|
|
2029
2079
|
telemetry: options.telemetry,
|
|
2030
2080
|
appendOnlyContext: model
|
|
2031
|
-
?
|
|
2081
|
+
? shouldEnableAppendOnlyContext(settings.get("provider.appendOnlyContext"), model)
|
|
2032
2082
|
? new AppendOnlyContextManager()
|
|
2033
2083
|
: undefined
|
|
2034
2084
|
: undefined,
|
|
@@ -2065,6 +2115,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2065
2115
|
// AsyncJobManager on teardown; subagents inherit the parent's and
|
|
2066
2116
|
// **MUST NOT** tear it down.
|
|
2067
2117
|
ownedAsyncJobManager: asyncJobManager,
|
|
2118
|
+
asyncJobManager: scopedAsyncJobManager,
|
|
2068
2119
|
scopedModels: options.scopedModels,
|
|
2069
2120
|
promptTemplates,
|
|
2070
2121
|
slashCommands,
|
|
@@ -2278,6 +2329,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2278
2329
|
await session.dispose();
|
|
2279
2330
|
} else {
|
|
2280
2331
|
if (hasRegistered) agentRegistry.unregister(resolvedAgentId);
|
|
2332
|
+
if (asyncJobManager) {
|
|
2333
|
+
if (AsyncJobManager.instance() === asyncJobManager) {
|
|
2334
|
+
AsyncJobManager.setInstance(undefined);
|
|
2335
|
+
}
|
|
2336
|
+
await asyncJobManager.dispose({ timeoutMs: 3_000 });
|
|
2337
|
+
}
|
|
2281
2338
|
await disposeKernelSessionsByOwner(evalKernelOwnerId);
|
|
2282
2339
|
}
|
|
2283
2340
|
} catch (cleanupError) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import * as crypto from "node:crypto";
|
|
17
17
|
import * as fs from "node:fs";
|
|
18
|
+
import * as os from "node:os";
|
|
18
19
|
import * as path from "node:path";
|
|
19
20
|
import { scheduler } from "node:timers/promises";
|
|
20
21
|
import { isPromise } from "node:util/types";
|
|
@@ -94,12 +95,14 @@ import {
|
|
|
94
95
|
isUnexpectedSocketCloseMessage,
|
|
95
96
|
logger,
|
|
96
97
|
prompt,
|
|
98
|
+
relativePathWithinRoot,
|
|
97
99
|
Snowflake,
|
|
98
100
|
} from "@oh-my-pi/pi-utils";
|
|
99
101
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
100
102
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
101
103
|
import { reset as resetCapabilities } from "../capability";
|
|
102
104
|
import type { Rule } from "../capability/rule";
|
|
105
|
+
import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mode";
|
|
103
106
|
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
104
107
|
import {
|
|
105
108
|
extractExplicitThinkingSelector,
|
|
@@ -232,7 +235,7 @@ import type {
|
|
|
232
235
|
SessionContext,
|
|
233
236
|
SessionManager,
|
|
234
237
|
} from "./session-manager";
|
|
235
|
-
import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-manager";
|
|
238
|
+
import { EPHEMERAL_MODEL_CHANGE_ROLE, getLatestCompactionEntry, getRestorableSessionModels } from "./session-manager";
|
|
236
239
|
import type { ShakeMode, ShakeResult } from "./shake-types";
|
|
237
240
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
238
241
|
import { YieldQueue } from "./yield-queue";
|
|
@@ -365,6 +368,15 @@ export interface AgentSessionConfig {
|
|
|
365
368
|
* **MUST NOT** dispose it on their own teardown.
|
|
366
369
|
*/
|
|
367
370
|
ownedAsyncJobManager?: AsyncJobManager;
|
|
371
|
+
/**
|
|
372
|
+
* AsyncJobManager reachable by this session for scoped job actions.
|
|
373
|
+
*
|
|
374
|
+
* Top-level owners receive their own manager, subagents receive the inherited
|
|
375
|
+
* parent manager, and secondary in-process top-level sessions receive
|
|
376
|
+
* `undefined` so job snapshots and ACP drains cannot observe the primary's
|
|
377
|
+
* state.
|
|
378
|
+
*/
|
|
379
|
+
asyncJobManager?: AsyncJobManager;
|
|
368
380
|
/** Agent identity (registry id like "Main" or "Alice") used for IRC routing. */
|
|
369
381
|
agentId?: string;
|
|
370
382
|
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
@@ -889,6 +901,14 @@ export class AgentSession {
|
|
|
889
901
|
* this undefined and **MUST NOT** dispose the global instance on teardown.
|
|
890
902
|
*/
|
|
891
903
|
readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
|
|
904
|
+
/**
|
|
905
|
+
* AsyncJobManager scoped to this session for introspection/cancellation.
|
|
906
|
+
*
|
|
907
|
+
* This differs from `#ownedAsyncJobManager`: subagents can inherit a parent
|
|
908
|
+
* manager for their own owner id, while secondary top-level sessions are left
|
|
909
|
+
* undefined to avoid reading the primary's jobs.
|
|
910
|
+
*/
|
|
911
|
+
readonly #asyncJobManager: AsyncJobManager | undefined;
|
|
892
912
|
#pendingPythonMessages: PythonExecutionMessage[] = [];
|
|
893
913
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
894
914
|
#evalExecutionDisposing = false;
|
|
@@ -1079,6 +1099,7 @@ export class AgentSession {
|
|
|
1079
1099
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
1080
1100
|
this.#parentEvalSessionId = config.parentEvalSessionId;
|
|
1081
1101
|
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
1102
|
+
this.#asyncJobManager = config.asyncJobManager ?? config.ownedAsyncJobManager;
|
|
1082
1103
|
this.#scopedModels = config.scopedModels ?? [];
|
|
1083
1104
|
if (config.thinkingLevel === AUTO_THINKING) {
|
|
1084
1105
|
// `auto` is session-level: keep the flag and show a provisional concrete
|
|
@@ -1293,6 +1314,12 @@ export class AgentSession {
|
|
|
1293
1314
|
this.#standingResolveHandler = handler ?? undefined;
|
|
1294
1315
|
}
|
|
1295
1316
|
|
|
1317
|
+
#sessionSwitchReconciler: (() => Promise<void>) | undefined;
|
|
1318
|
+
|
|
1319
|
+
setSessionSwitchReconciler(reconciler: (() => Promise<void>) | null): void {
|
|
1320
|
+
this.#sessionSwitchReconciler = reconciler ?? undefined;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1296
1323
|
/** Provider-scoped mutable state store for transport/session caches. */
|
|
1297
1324
|
get providerSessionState(): Map<string, ProviderSessionState> {
|
|
1298
1325
|
return this.#providerSessionState;
|
|
@@ -1366,7 +1393,7 @@ export class AgentSession {
|
|
|
1366
1393
|
}
|
|
1367
1394
|
|
|
1368
1395
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
1369
|
-
const manager =
|
|
1396
|
+
const manager = this.#asyncJobManager;
|
|
1370
1397
|
if (!manager) return null;
|
|
1371
1398
|
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
1372
1399
|
const running = manager.getRunningJobs(ownerFilter).map(job => ({
|
|
@@ -1391,11 +1418,20 @@ export class AgentSession {
|
|
|
1391
1418
|
* Cancel async jobs registered by *this* agent only. Used by lifecycle
|
|
1392
1419
|
* transitions (newSession, switchSession, handoff, dispose) so a subagent
|
|
1393
1420
|
* cleans up its own background work without touching its parent's jobs.
|
|
1394
|
-
*
|
|
1421
|
+
*
|
|
1422
|
+
* Cancellation runs against this session's scoped manager. Subagents have
|
|
1423
|
+
* unique agent ids and inherit the parent's manager to clean up their own
|
|
1424
|
+
* jobs. A secondary in-process top-level session gets no scoped manager,
|
|
1425
|
+
* because it defaults to `MAIN_AGENT_ID`; reaching through the global
|
|
1426
|
+
* singleton would tear down the owning primary session's bash/task jobs at
|
|
1427
|
+
* dispose time (issue #1923).
|
|
1428
|
+
*
|
|
1429
|
+
* No-op when no manager is reachable or this session has no agent id.
|
|
1395
1430
|
*/
|
|
1396
1431
|
#cancelOwnAsyncJobs(): void {
|
|
1397
1432
|
if (!this.#agentId) return;
|
|
1398
|
-
|
|
1433
|
+
const manager = this.#asyncJobManager;
|
|
1434
|
+
manager?.cancelAll({ ownerId: this.#agentId });
|
|
1399
1435
|
}
|
|
1400
1436
|
|
|
1401
1437
|
// =========================================================================
|
|
@@ -2121,12 +2157,31 @@ export class AgentSession {
|
|
|
2121
2157
|
if (this.#pendingTtsrInjections.length === 0) return undefined;
|
|
2122
2158
|
const rules = this.#pendingTtsrInjections;
|
|
2123
2159
|
const content = rules
|
|
2124
|
-
.map(r =>
|
|
2160
|
+
.map(r =>
|
|
2161
|
+
prompt.render(ttsrInterruptTemplate, {
|
|
2162
|
+
name: r.name,
|
|
2163
|
+
path: this.#displayRulePath(r.path),
|
|
2164
|
+
content: r.content,
|
|
2165
|
+
}),
|
|
2166
|
+
)
|
|
2125
2167
|
.join("\n\n");
|
|
2126
2168
|
this.#pendingTtsrInjections = [];
|
|
2127
2169
|
return { content, rules };
|
|
2128
2170
|
}
|
|
2129
2171
|
|
|
2172
|
+
/**
|
|
2173
|
+
* Render a rule's file path for model-facing TTSR injections without leaking
|
|
2174
|
+
* the absolute home directory: cwd-relative when the rule lives in the
|
|
2175
|
+
* project, `~`-relative when it lives under home, else the raw path.
|
|
2176
|
+
*/
|
|
2177
|
+
#displayRulePath(rulePath: string): string {
|
|
2178
|
+
const cwdRel = relativePathWithinRoot(this.sessionManager.getCwd(), rulePath);
|
|
2179
|
+
if (cwdRel) return cwdRel;
|
|
2180
|
+
const homeRel = relativePathWithinRoot(os.homedir(), rulePath);
|
|
2181
|
+
if (homeRel) return `~/${homeRel}`;
|
|
2182
|
+
return rulePath;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2130
2185
|
#addPendingTtsrInjections(rules: Rule[]): void {
|
|
2131
2186
|
const seen = new Set(this.#pendingTtsrInjections.map(rule => rule.name));
|
|
2132
2187
|
for (const rule of rules) {
|
|
@@ -2179,7 +2234,13 @@ export class AgentSession {
|
|
|
2179
2234
|
if (!rules || rules.length === 0) return undefined;
|
|
2180
2235
|
this.#perToolTtsrInjections.delete(ctx.toolCall.id);
|
|
2181
2236
|
const reminder = rules
|
|
2182
|
-
.map(r =>
|
|
2237
|
+
.map(r =>
|
|
2238
|
+
prompt.render(ttsrToolReminderTemplate, {
|
|
2239
|
+
name: r.name,
|
|
2240
|
+
path: this.#displayRulePath(r.path),
|
|
2241
|
+
content: r.content,
|
|
2242
|
+
}),
|
|
2243
|
+
)
|
|
2183
2244
|
.join("\n\n");
|
|
2184
2245
|
// The TTSR manager was already claimed at bucket time; only persistence remains.
|
|
2185
2246
|
const ruleNames = rules.map(r => r.name.trim()).filter(n => n.length > 0);
|
|
@@ -2983,8 +3044,13 @@ export class AgentSession {
|
|
|
2983
3044
|
this.#releasePowerAssertion();
|
|
2984
3045
|
await this.sessionManager.close();
|
|
2985
3046
|
this.#closeAllProviderSessions("dispose");
|
|
2986
|
-
|
|
3047
|
+
// Flush the retain queue BEFORE clearing the session's pointer so
|
|
3048
|
+
// `HindsightRetainQueue.#doFlush` still sees `session.getHindsightSessionState() === state`.
|
|
3049
|
+
// Reversed, the spliced batch survives just long enough to fail the
|
|
3050
|
+
// identity check and get dropped with a `session vanished` warning.
|
|
3051
|
+
const hindsightState = this.getHindsightSessionState();
|
|
2987
3052
|
await hindsightState?.flushRetainQueue();
|
|
3053
|
+
this.setHindsightSessionState(undefined);
|
|
2988
3054
|
hindsightState?.dispose();
|
|
2989
3055
|
const mnemopiState = setMnemopiSessionState(this, undefined);
|
|
2990
3056
|
mnemopiState?.dispose();
|
|
@@ -3062,7 +3128,7 @@ export class AgentSession {
|
|
|
3062
3128
|
}
|
|
3063
3129
|
|
|
3064
3130
|
async drainAsyncJobDeliveriesForAcp(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
3065
|
-
const manager =
|
|
3131
|
+
const manager = this.#asyncJobManager;
|
|
3066
3132
|
if (!manager) return false;
|
|
3067
3133
|
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
3068
3134
|
const before = manager.getDeliveryState(ownerFilter);
|
|
@@ -4639,6 +4705,7 @@ export class AgentSession {
|
|
|
4639
4705
|
this.agent.steer({
|
|
4640
4706
|
role: "user",
|
|
4641
4707
|
content,
|
|
4708
|
+
steering: true,
|
|
4642
4709
|
attribution: "user",
|
|
4643
4710
|
timestamp: Date.now(),
|
|
4644
4711
|
});
|
|
@@ -5225,7 +5292,11 @@ export class AgentSession {
|
|
|
5225
5292
|
* Validates API key, saves to session log but NOT to settings.
|
|
5226
5293
|
* @throws Error if no API key available for the model
|
|
5227
5294
|
*/
|
|
5228
|
-
async setModelTemporary(
|
|
5295
|
+
async setModelTemporary(
|
|
5296
|
+
model: Model,
|
|
5297
|
+
thinkingLevel?: ThinkingLevel,
|
|
5298
|
+
options?: { ephemeral?: boolean },
|
|
5299
|
+
): Promise<void> {
|
|
5229
5300
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
5230
5301
|
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
5231
5302
|
if (!apiKey) {
|
|
@@ -5234,7 +5305,10 @@ export class AgentSession {
|
|
|
5234
5305
|
|
|
5235
5306
|
this.#clearActiveRetryFallback();
|
|
5236
5307
|
this.#setModelWithProviderSessionReset(model);
|
|
5237
|
-
this.sessionManager.appendModelChange(
|
|
5308
|
+
this.sessionManager.appendModelChange(
|
|
5309
|
+
`${model.provider}/${model.id}`,
|
|
5310
|
+
options?.ephemeral ? EPHEMERAL_MODEL_CHANGE_ROLE : "temporary",
|
|
5311
|
+
);
|
|
5238
5312
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
5239
5313
|
|
|
5240
5314
|
// Apply explicit thinking level if given; otherwise prefer the model's
|
|
@@ -6682,7 +6756,7 @@ export class AgentSession {
|
|
|
6682
6756
|
if (!targetModel) return false;
|
|
6683
6757
|
|
|
6684
6758
|
try {
|
|
6685
|
-
await this.setModelTemporary(targetModel);
|
|
6759
|
+
await this.setModelTemporary(targetModel, undefined, { ephemeral: true });
|
|
6686
6760
|
logger.debug("Context promotion switched model on overflow", {
|
|
6687
6761
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
6688
6762
|
to: `${targetModel.provider}/${targetModel.id}`,
|
|
@@ -6735,8 +6809,8 @@ export class AgentSession {
|
|
|
6735
6809
|
*/
|
|
6736
6810
|
#syncAppendOnlyContext(model: Model | null | undefined): void {
|
|
6737
6811
|
const setting = this.settings.get("provider.appendOnlyContext") ?? "auto";
|
|
6812
|
+
const enable = shouldEnableAppendOnlyContext(setting, model);
|
|
6738
6813
|
const providerId = model?.provider;
|
|
6739
|
-
const enable = setting === "on" || (setting === "auto" && providerId === "deepseek");
|
|
6740
6814
|
const prev = this.#lastAppendOnlyResolution;
|
|
6741
6815
|
if (prev && prev.enable === enable && prev.providerId === providerId) return;
|
|
6742
6816
|
this.#lastAppendOnlyResolution = { enable, providerId };
|
|
@@ -7848,7 +7922,7 @@ export class AgentSession {
|
|
|
7848
7922
|
const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
|
|
7849
7923
|
|
|
7850
7924
|
this.#setModelWithProviderSessionReset(candidate);
|
|
7851
|
-
this.sessionManager.appendModelChange(`${candidate.provider}/${candidate.id}`,
|
|
7925
|
+
this.sessionManager.appendModelChange(`${candidate.provider}/${candidate.id}`, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
7852
7926
|
this.settings.getStorage()?.recordModelUsage(`${candidate.provider}/${candidate.id}`);
|
|
7853
7927
|
this.setThinkingLevel(nextThinkingLevel);
|
|
7854
7928
|
if (!this.#activeRetryFallback) {
|
|
@@ -7921,7 +7995,7 @@ export class AgentSession {
|
|
|
7921
7995
|
const thinkingToApply =
|
|
7922
7996
|
currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
|
|
7923
7997
|
this.#setModelWithProviderSessionReset(primaryModel);
|
|
7924
|
-
this.sessionManager.appendModelChange(`${primaryModel.provider}/${primaryModel.id}`,
|
|
7998
|
+
this.sessionManager.appendModelChange(`${primaryModel.provider}/${primaryModel.id}`, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
7925
7999
|
this.settings.getStorage()?.recordModelUsage(`${primaryModel.provider}/${primaryModel.id}`);
|
|
7926
8000
|
this.setThinkingLevel(thinkingToApply);
|
|
7927
8001
|
this.#clearActiveRetryFallback();
|
|
@@ -8948,6 +9022,14 @@ export class AgentSession {
|
|
|
8948
9022
|
this.#resetMnemopiConversationTrackingIfMnemopi();
|
|
8949
9023
|
}
|
|
8950
9024
|
this.#reconnectToAgent();
|
|
9025
|
+
try {
|
|
9026
|
+
await this.#sessionSwitchReconciler?.();
|
|
9027
|
+
} catch (error) {
|
|
9028
|
+
logger.warn("Failed to reconcile session mode after switch", {
|
|
9029
|
+
targetSessionFile: sessionPath,
|
|
9030
|
+
error: String(error),
|
|
9031
|
+
});
|
|
9032
|
+
}
|
|
8951
9033
|
return true;
|
|
8952
9034
|
} catch (error) {
|
|
8953
9035
|
this.sessionManager.restoreState(previousSessionState);
|
|
@@ -12,12 +12,16 @@ export type {
|
|
|
12
12
|
AuthStorageOptions,
|
|
13
13
|
OAuthCredential,
|
|
14
14
|
SerializedAuthStorage,
|
|
15
|
+
SnapshotResponse,
|
|
15
16
|
StoredAuthCredential,
|
|
16
17
|
} from "@oh-my-pi/pi-ai";
|
|
17
18
|
export {
|
|
18
19
|
AuthBrokerClient,
|
|
19
20
|
AuthStorage,
|
|
21
|
+
DEFAULT_SNAPSHOT_CACHE_TTL_MS,
|
|
20
22
|
REMOTE_REFRESH_SENTINEL,
|
|
21
23
|
RemoteAuthCredentialStore,
|
|
24
|
+
readAuthBrokerSnapshotCache,
|
|
22
25
|
SqliteAuthCredentialStore,
|
|
26
|
+
writeAuthBrokerSnapshotCache,
|
|
23
27
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -209,10 +209,6 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
|
209
209
|
logger.debug("HistoryStorage FTS query failed, using substring only", { error: String(error) });
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
if (ftsRows.length >= safeLimit) {
|
|
213
|
-
return ftsRows.map(row => this.#toEntry(row));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
212
|
// 2. Substring fallback (token-AND LIKE). Catches infix matches FTS5's
|
|
217
213
|
// prefix-only wildcard cannot reach (e.g. "mit" -> "commit"). Bounded
|
|
218
214
|
// by safeLimit, ordered by recency - no full-table load into JS.
|
|
@@ -227,27 +223,24 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
|
227
223
|
return subRows.map(row => this.#toEntry(row));
|
|
228
224
|
}
|
|
229
225
|
|
|
230
|
-
const
|
|
231
|
-
const merged: HistoryEntry[] = [];
|
|
226
|
+
const rowsById = new Map<number, HistoryRow>();
|
|
232
227
|
for (const row of ftsRows) {
|
|
233
|
-
|
|
234
|
-
seen.add(row.id);
|
|
235
|
-
merged.push(this.#toEntry(row));
|
|
228
|
+
rowsById.set(row.id, row);
|
|
236
229
|
}
|
|
237
230
|
for (const row of subRows) {
|
|
238
|
-
if (
|
|
239
|
-
if (seen.has(row.id)) continue;
|
|
240
|
-
seen.add(row.id);
|
|
241
|
-
merged.push(this.#toEntry(row));
|
|
231
|
+
if (!rowsById.has(row.id)) rowsById.set(row.id, row);
|
|
242
232
|
}
|
|
243
|
-
|
|
233
|
+
|
|
234
|
+
return [...rowsById.values()]
|
|
235
|
+
.sort((a, b) => b.created_at - a.created_at || b.id - a.id)
|
|
236
|
+
.slice(0, safeLimit)
|
|
237
|
+
.map(row => this.#toEntry(row));
|
|
244
238
|
}
|
|
245
239
|
|
|
246
240
|
/**
|
|
247
|
-
* IDs of the sessions whose stored prompts match `query`, ordered by
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* resume picker with prompts that the 4KB session-list prefix never sees.
|
|
241
|
+
* IDs of the sessions whose stored prompts match `query`, ordered by prompt
|
|
242
|
+
* recency and de-duplicated. Used to augment session ranking in the resume
|
|
243
|
+
* picker with prompts that the 4KB session-list prefix never sees.
|
|
251
244
|
*/
|
|
252
245
|
matchingSessionIds(query: string, limit = 500): string[] {
|
|
253
246
|
const seen = new Set<string>();
|
package/src/session/messages.ts
CHANGED
|
@@ -18,7 +18,10 @@ import type {
|
|
|
18
18
|
MessageAttribution,
|
|
19
19
|
TextContent,
|
|
20
20
|
ToolResultMessage,
|
|
21
|
+
UserMessage,
|
|
21
22
|
} from "@oh-my-pi/pi-ai";
|
|
23
|
+
import { prompt } from "@oh-my-pi/pi-utils";
|
|
24
|
+
import userInterjectionTemplate from "../prompts/steering/user-interjection.md" with { type: "text" };
|
|
22
25
|
|
|
23
26
|
export {
|
|
24
27
|
type BranchSummaryMessage,
|
|
@@ -105,6 +108,83 @@ export function stripInternalDetailsFields<T>(details: T | undefined): T | undef
|
|
|
105
108
|
return cleaned as T;
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
function isSteeringUserMessage(message: AgentMessage | undefined): message is UserMessage & { steering: true } {
|
|
112
|
+
return message?.role === "user" && message.steering === true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function userMessageWithoutSteering(message: UserMessage): UserMessage {
|
|
116
|
+
const { steering, ...rest } = message;
|
|
117
|
+
void steering;
|
|
118
|
+
return rest;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderSteeringEnvelope(message: string): string {
|
|
122
|
+
return prompt.render(userInterjectionTemplate, { message });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getArrayContentText(content: (TextContent | ImageContent)[]): string {
|
|
126
|
+
let firstText: string | undefined;
|
|
127
|
+
let textParts: string[] | undefined;
|
|
128
|
+
for (const part of content) {
|
|
129
|
+
if (part.type !== "text") continue;
|
|
130
|
+
if (firstText === undefined) {
|
|
131
|
+
firstText = part.text;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (textParts === undefined) {
|
|
135
|
+
textParts = [firstText];
|
|
136
|
+
}
|
|
137
|
+
textParts.push(part.text);
|
|
138
|
+
}
|
|
139
|
+
return textParts === undefined ? (firstText ?? "") : textParts.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getArrayContentImages(content: (TextContent | ImageContent)[]): ImageContent[] {
|
|
143
|
+
let images: ImageContent[] | undefined;
|
|
144
|
+
for (const part of content) {
|
|
145
|
+
if (part.type !== "image") continue;
|
|
146
|
+
if (images === undefined) images = [];
|
|
147
|
+
images.push(part);
|
|
148
|
+
}
|
|
149
|
+
return images ?? [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function wrapSteeringUserMessage(message: UserMessage): UserMessage {
|
|
153
|
+
if (typeof message.content === "string") {
|
|
154
|
+
if (message.content.length === 0) return message;
|
|
155
|
+
return { ...userMessageWithoutSteering(message), content: renderSteeringEnvelope(message.content) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const text = getArrayContentText(message.content);
|
|
159
|
+
if (text.length === 0) return message;
|
|
160
|
+
const content: (TextContent | ImageContent)[] = [{ type: "text", text: renderSteeringEnvelope(text) }];
|
|
161
|
+
content.push(...getArrayContentImages(message.content));
|
|
162
|
+
return { ...userMessageWithoutSteering(message), content };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function wrapSteeringForModel(messages: AgentMessage[]): AgentMessage[] {
|
|
166
|
+
const last = messages[messages.length - 1];
|
|
167
|
+
if (!isSteeringUserMessage(last)) return messages;
|
|
168
|
+
|
|
169
|
+
let firstSteer = messages.length - 1;
|
|
170
|
+
while (firstSteer > 0 && isSteeringUserMessage(messages[firstSteer - 1])) {
|
|
171
|
+
firstSteer--;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let wrappedMessages: AgentMessage[] | undefined;
|
|
175
|
+
for (let i = firstSteer; i < messages.length; i++) {
|
|
176
|
+
const message = messages[i];
|
|
177
|
+
if (!isSteeringUserMessage(message)) continue;
|
|
178
|
+
const wrappedMessage = wrapSteeringUserMessage(message);
|
|
179
|
+
if (wrappedMessage === message) continue;
|
|
180
|
+
if (wrappedMessages === undefined) {
|
|
181
|
+
wrappedMessages = messages.slice();
|
|
182
|
+
}
|
|
183
|
+
wrappedMessages[i] = wrappedMessage;
|
|
184
|
+
}
|
|
185
|
+
return wrappedMessages ?? messages;
|
|
186
|
+
}
|
|
187
|
+
|
|
108
188
|
function getPrunedToolResultContent(message: ToolResultMessage): (TextContent | ImageContent)[] {
|
|
109
189
|
if (message.prunedAt === undefined) {
|
|
110
190
|
return message.content;
|
|
@@ -254,13 +254,19 @@ export interface SessionContext {
|
|
|
254
254
|
modeData?: Record<string, unknown>;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
export const EPHEMERAL_MODEL_CHANGE_ROLE = "fallback";
|
|
258
|
+
|
|
257
259
|
/** Lists session model strings to try when restoring, in fallback order. */
|
|
258
260
|
export function getRestorableSessionModels(
|
|
259
261
|
models: Readonly<Record<string, string>>,
|
|
260
262
|
lastModelChangeRole: string | undefined,
|
|
261
263
|
): string[] {
|
|
262
264
|
const defaultModel = models.default;
|
|
263
|
-
if (
|
|
265
|
+
if (
|
|
266
|
+
!lastModelChangeRole ||
|
|
267
|
+
lastModelChangeRole === "default" ||
|
|
268
|
+
lastModelChangeRole === EPHEMERAL_MODEL_CHANGE_ROLE
|
|
269
|
+
) {
|
|
264
270
|
return defaultModel ? [defaultModel] : [];
|
|
265
271
|
}
|
|
266
272
|
|