@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.0
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 +127 -0
- package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
- package/dist/cli.js +9554 -12379
- package/dist/template-8vdv6xb4.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +8 -11
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +63 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +8 -5
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/model-controls.d.ts +3 -3
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/system-prompt.d.ts +2 -1
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +138 -24
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/async/job-manager.ts +30 -7
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +11 -1
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/cleanse.ts +45 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-registry.ts +103 -25
- package/src/config/settings-schema.ts +65 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/export/html/index.ts +17 -10
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/launch/broker.ts +14 -4
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +4 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +27 -18
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +67 -64
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/grep.md +7 -6
- package/src/sdk.ts +178 -15
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +330 -179
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +15 -7
- package/src/session/session-advisors.ts +142 -23
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +136 -5
- package/src/system-prompt.ts +3 -2
- package/src/task/executor.ts +17 -4
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/render.ts +20 -4
- package/src/task/worktree.ts +8 -0
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +7 -2
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +38 -5
- package/src/tools/path-utils.ts +88 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/write.ts +44 -3
- package/src/tools/xdev.ts +39 -14
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/render.ts +2 -2
- package/src/web/search/types.ts +1 -1
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-file persistence of the OAuth account that served a session.
|
|
3
|
+
*
|
|
4
|
+
* Provider prompt caches are account-scoped (Anthropic bills a full cache
|
|
5
|
+
* re-write after an account flip), and the auth store's session-sticky routing
|
|
6
|
+
* is process-local when a remote auth broker is configured — the broker
|
|
7
|
+
* store's KV cache is in-memory, so sticky rows die with the CLI process.
|
|
8
|
+
* Resuming a session in a fresh process then re-ranks accounts by usage
|
|
9
|
+
* headroom, which is biased *away* from the account that just served the
|
|
10
|
+
* session (it has the highest recent burn), cold-missing the entire prefix.
|
|
11
|
+
*
|
|
12
|
+
* These helpers close the loop through the session file itself: after each
|
|
13
|
+
* assistant turn the serving account is recorded as a `credential_pin` entry,
|
|
14
|
+
* and on session adoption the pin is matched against the stored accounts and
|
|
15
|
+
* seeded back into the auth store with the session's effective last-use
|
|
16
|
+
* timestamp, so the provider's warm-window semantics still decide whether to
|
|
17
|
+
* stick or re-rank.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { AuthStorage } from "./auth-storage";
|
|
21
|
+
import type { SessionManager } from "./session-manager";
|
|
22
|
+
|
|
23
|
+
/** Account fields shared by `OAuthAccountIdentity` and `OAuthAccountSummary`. */
|
|
24
|
+
interface CredentialPinIdentity {
|
|
25
|
+
accountId?: string;
|
|
26
|
+
email?: string;
|
|
27
|
+
projectId?: string;
|
|
28
|
+
orgId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Stable identifier for a provider account within its billing scope. The
|
|
33
|
+
* digest covers the full scope tuple — the same account in two orgs (Anthropic
|
|
34
|
+
* multi-subscription) or projects (Gemini) is two distinct cache domains and
|
|
35
|
+
* must produce two distinct pins. The digest input is the persisted contract
|
|
36
|
+
* for `CredentialPinEntry.hash` — changing it orphans every recorded pin.
|
|
37
|
+
*
|
|
38
|
+
* Hashing avoids embedding raw emails/uuids in session files, but an unsalted
|
|
39
|
+
* digest of a guessable email is still linkable — treat exported sessions
|
|
40
|
+
* accordingly.
|
|
41
|
+
*
|
|
42
|
+
* Returns `undefined` when the identity carries no account key at all.
|
|
43
|
+
*/
|
|
44
|
+
export function credentialPinHash(provider: string, identity: CredentialPinIdentity): string | undefined {
|
|
45
|
+
if (!identity.accountId && !identity.email) return undefined;
|
|
46
|
+
const key = [
|
|
47
|
+
provider,
|
|
48
|
+
identity.accountId ?? "",
|
|
49
|
+
identity.email ?? "",
|
|
50
|
+
identity.orgId ?? "",
|
|
51
|
+
identity.projectId ?? "",
|
|
52
|
+
].join("\0");
|
|
53
|
+
return new Bun.CryptoHasher("sha256").update(key).digest("hex");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Record the account that served the latest assistant turn for `provider`.
|
|
58
|
+
* Appends a `credential_pin` entry only when the account differs from the
|
|
59
|
+
* branch's latest pin, so steady-state sessions add a single entry; the
|
|
60
|
+
* effective last-use time is derived from later assistant turns on read
|
|
61
|
+
* (see `SessionManager.getCredentialPins`).
|
|
62
|
+
*/
|
|
63
|
+
export function recordCredentialPin(
|
|
64
|
+
authStorage: AuthStorage,
|
|
65
|
+
sessionManager: SessionManager,
|
|
66
|
+
sessionId: string,
|
|
67
|
+
provider: string,
|
|
68
|
+
): void {
|
|
69
|
+
const identity = authStorage.getOAuthAccountIdentity(provider, sessionId);
|
|
70
|
+
if (!identity) return;
|
|
71
|
+
const hash = credentialPinHash(provider, identity);
|
|
72
|
+
if (!hash || sessionManager.getCredentialPins().get(provider)?.hash === hash) return;
|
|
73
|
+
sessionManager.appendCredentialPin(provider, hash);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Re-pin the accounts recorded in the session file onto the auth store's
|
|
78
|
+
* session stickiness. No-op per provider when the account is gone (logged out)
|
|
79
|
+
* or when a live sticky already exists (same-process branch/session switches
|
|
80
|
+
* must not clobber fresher routing). Seeds with the session's effective
|
|
81
|
+
* last-use time so stale resumes still fall through to usage ranking.
|
|
82
|
+
*/
|
|
83
|
+
export function seedCredentialPins(authStorage: AuthStorage, sessionManager: SessionManager, sessionId: string): void {
|
|
84
|
+
for (const [provider, pin] of sessionManager.getCredentialPins()) {
|
|
85
|
+
const accounts = authStorage.listOAuthAccounts(provider, sessionId);
|
|
86
|
+
if (accounts.length === 0 || accounts.some(account => account.active)) continue;
|
|
87
|
+
const match = accounts.find(account => credentialPinHash(provider, account) === pin.hash);
|
|
88
|
+
if (!match) continue;
|
|
89
|
+
authStorage.pinSessionOAuthAccount(provider, sessionId, match.credentialId, {
|
|
90
|
+
lastUsedAtMs: pin.lastUsedAt,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { directoryExists } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { ClaudeSessionStore } from "./claude-session-store";
|
|
3
|
+
import { CodexSessionStore } from "./codex-session-store";
|
|
4
|
+
import type { ForeignSessionInfo, ForeignSessionSource, ForeignSessionStore } from "./foreign-session-store";
|
|
5
|
+
import type { SessionInfo } from "./session-listing";
|
|
6
|
+
import type { SessionManager } from "./session-manager";
|
|
7
|
+
|
|
8
|
+
/** Construct the importer for a supported foreign session source. */
|
|
9
|
+
export function createForeignSessionStore(source: ForeignSessionSource): ForeignSessionStore {
|
|
10
|
+
return source === "claude" ? new ClaudeSessionStore() : new CodexSessionStore();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Display name for a supported foreign session source. */
|
|
14
|
+
export function foreignSessionSourceName(source: ForeignSessionSource): string {
|
|
15
|
+
return source === "claude" ? "Claude" : "Codex";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Convert lightweight foreign metadata for the existing session picker. */
|
|
19
|
+
export function foreignSessionInfoToSessionInfo(info: ForeignSessionInfo): SessionInfo {
|
|
20
|
+
const firstMessage = info.firstMessage ?? "(no messages)";
|
|
21
|
+
return {
|
|
22
|
+
path: info.path,
|
|
23
|
+
id: info.id,
|
|
24
|
+
cwd: info.cwd,
|
|
25
|
+
title: info.title,
|
|
26
|
+
created: info.created,
|
|
27
|
+
modified: info.modified,
|
|
28
|
+
messageCount: info.messageCount ?? 0,
|
|
29
|
+
size: 0,
|
|
30
|
+
firstMessage,
|
|
31
|
+
allMessagesText: firstMessage,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Import and persist one foreign session under a fresh OMP session identity. */
|
|
36
|
+
export async function persistForeignSession(
|
|
37
|
+
store: ForeignSessionStore,
|
|
38
|
+
info: ForeignSessionInfo,
|
|
39
|
+
options?: { fallbackCwd?: string; sessionDir?: string; suppressBreadcrumb?: boolean },
|
|
40
|
+
): Promise<SessionManager> {
|
|
41
|
+
const imported = await store.load(info);
|
|
42
|
+
imported.appendCustomEntry("foreign_session_import", {
|
|
43
|
+
source: info.source,
|
|
44
|
+
sourceId: info.id,
|
|
45
|
+
sourcePath: info.path,
|
|
46
|
+
sourceCwd: info.cwd,
|
|
47
|
+
});
|
|
48
|
+
if (options?.fallbackCwd && !(await directoryExists(imported.getCwd()))) {
|
|
49
|
+
await imported.moveTo(options.fallbackCwd);
|
|
50
|
+
}
|
|
51
|
+
return await imported.persistCopy(options);
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isRecord, readLines } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
3
|
+
/** One readable object record from a foreign JSONL transcript. */
|
|
4
|
+
export interface ForeignJsonRecord {
|
|
5
|
+
readonly value: Record<string, unknown>;
|
|
6
|
+
readonly line: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Stream valid object records while tolerating malformed or truncated lines. */
|
|
10
|
+
export async function* readForeignJsonRecords(filePath: string): AsyncGenerator<ForeignJsonRecord> {
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
let line = 0;
|
|
13
|
+
for await (const bytes of readLines(Bun.file(filePath).stream())) {
|
|
14
|
+
line += 1;
|
|
15
|
+
try {
|
|
16
|
+
const value: unknown = JSON.parse(decoder.decode(bytes));
|
|
17
|
+
if (isRecord(value)) yield { value, line };
|
|
18
|
+
} catch {
|
|
19
|
+
// A partially-written line must not hide the readable transcript prefix.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Read every valid object record from a foreign JSONL transcript. */
|
|
25
|
+
export async function collectForeignJsonRecords(filePath: string): Promise<ForeignJsonRecord[]> {
|
|
26
|
+
const records: ForeignJsonRecord[] = [];
|
|
27
|
+
for await (const record of readForeignJsonRecords(filePath)) records.push(record);
|
|
28
|
+
return records;
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SessionManager } from "./session-manager";
|
|
2
|
+
|
|
3
|
+
/** External coding-agent session source supported by OMP imports. */
|
|
4
|
+
export type ForeignSessionSource = "claude" | "codex";
|
|
5
|
+
|
|
6
|
+
/** Lightweight source metadata used to choose a foreign session before loading its transcript. */
|
|
7
|
+
export interface ForeignSessionInfo {
|
|
8
|
+
readonly source: ForeignSessionSource;
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly cwd: string;
|
|
12
|
+
readonly title?: string;
|
|
13
|
+
readonly created: Date;
|
|
14
|
+
readonly modified: Date;
|
|
15
|
+
readonly messageCount?: number;
|
|
16
|
+
readonly firstMessage?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Lists and converts sessions owned by another coding agent. */
|
|
20
|
+
export interface ForeignSessionStore {
|
|
21
|
+
readonly source: ForeignSessionSource;
|
|
22
|
+
/** Lists source sessions without parsing complete transcripts. */
|
|
23
|
+
list(): Promise<ForeignSessionInfo[]>;
|
|
24
|
+
/** Converts one source session into a non-persistent OMP session. */
|
|
25
|
+
load(session: ForeignSessionInfo): Promise<SessionManager>;
|
|
26
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { Model, ProviderSessionState, ServiceTier, ServiceTierByFamily, Ser
|
|
|
3
3
|
import {
|
|
4
4
|
clearAnthropicFastModeFallback,
|
|
5
5
|
Effort,
|
|
6
|
+
isAnthropicFastModeFallbackDisabled,
|
|
6
7
|
realizesPriorityServiceTier,
|
|
7
8
|
resolveModelServiceTier,
|
|
8
9
|
serviceTierFamily,
|
|
@@ -581,9 +582,9 @@ export class ModelControls {
|
|
|
581
582
|
|
|
582
583
|
/**
|
|
583
584
|
* Classify the current user turn and set the effective thinking level for it.
|
|
584
|
-
* Bounded by a timeout + abort; on
|
|
585
|
-
*
|
|
586
|
-
* throws into the turn, and never clears `#autoThinking
|
|
585
|
+
* Bounded by a timeout + abort; on failure it preserves the last classified
|
|
586
|
+
* level, or uses the provisional concrete level before the first resolution.
|
|
587
|
+
* Never throws into the turn, and never clears `#autoThinking`.
|
|
587
588
|
*/
|
|
588
589
|
async applyAutoThinkingLevel(promptText: string, generation: number): Promise<void> {
|
|
589
590
|
const model = this.#model;
|
|
@@ -596,8 +597,8 @@ export class ModelControls {
|
|
|
596
597
|
let resolved: Effort | undefined;
|
|
597
598
|
if (this.#host.magicKeywordEnabled("ultrathink") && containsUltrathink(promptText)) {
|
|
598
599
|
// The user explicitly asked for maximum thinking; bypass the classifier
|
|
599
|
-
// (and
|
|
600
|
-
// supported level for this model.
|
|
600
|
+
// (and the `providers.autoThinkingMaxEffort` ceiling) and jump straight
|
|
601
|
+
// to the highest supported level for this model.
|
|
601
602
|
resolved = clampAutoThinkingEffort(model, Effort.Max);
|
|
602
603
|
} else {
|
|
603
604
|
const controller = new AbortController();
|
|
@@ -625,7 +626,7 @@ export class ModelControls {
|
|
|
625
626
|
|
|
626
627
|
const effort = clampThinkingLevelToCeiling(
|
|
627
628
|
model,
|
|
628
|
-
resolved ?? resolveProvisionalAutoLevel(model),
|
|
629
|
+
resolved ?? this.#autoResolvedLevel ?? resolveProvisionalAutoLevel(model),
|
|
629
630
|
this.#thinkingLevelCeiling,
|
|
630
631
|
);
|
|
631
632
|
if (effort === undefined) return;
|
|
@@ -666,7 +667,11 @@ export class ModelControls {
|
|
|
666
667
|
*/
|
|
667
668
|
isFastModeActive(): boolean {
|
|
668
669
|
const model = this.#model;
|
|
669
|
-
|
|
670
|
+
if (!model || !realizesPriorityServiceTier(this.effectiveServiceTier(model), model)) return false;
|
|
671
|
+
if (model.provider === "anthropic") {
|
|
672
|
+
return !isAnthropicFastModeFallbackDisabled(this.#host.providerSessionState, model);
|
|
673
|
+
}
|
|
674
|
+
return true;
|
|
670
675
|
}
|
|
671
676
|
|
|
672
677
|
/**
|
|
@@ -731,6 +736,9 @@ export class ModelControls {
|
|
|
731
736
|
if (this.#serviceTierByFamily[family] === "priority") this.setServiceTierFamily(family, undefined);
|
|
732
737
|
return true;
|
|
733
738
|
}
|
|
739
|
+
if (family === "anthropic" && this.#serviceTierByFamily.anthropic === "priority") {
|
|
740
|
+
clearAnthropicFastModeFallback(this.#host.providerSessionState);
|
|
741
|
+
}
|
|
734
742
|
this.setServiceTierFamily(family, "priority");
|
|
735
743
|
return true;
|
|
736
744
|
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
Agent,
|
|
3
3
|
type AgentMessage,
|
|
4
4
|
type AgentTool,
|
|
5
|
+
type AgentToolContext,
|
|
5
6
|
AppendOnlyContextManager,
|
|
6
7
|
type CompactionSummaryMessage,
|
|
7
8
|
countTokens,
|
|
@@ -16,9 +17,11 @@ import {
|
|
|
16
17
|
compactionContextTokens,
|
|
17
18
|
createCompactionSummaryMessage,
|
|
18
19
|
estimateTokens,
|
|
20
|
+
NativeCompactionError,
|
|
19
21
|
prepareCompaction,
|
|
20
22
|
type SessionMessageEntry,
|
|
21
23
|
shouldCompact,
|
|
24
|
+
shouldUseProviderNativeCompaction,
|
|
22
25
|
} from "@oh-my-pi/pi-agent-core/compaction";
|
|
23
26
|
import type {
|
|
24
27
|
AssistantMessage,
|
|
@@ -48,7 +51,6 @@ import {
|
|
|
48
51
|
type AdvisorSeverity,
|
|
49
52
|
AdvisorTranscriptRecorder,
|
|
50
53
|
advisorTranscriptFilename,
|
|
51
|
-
annotateForStaleness,
|
|
52
54
|
buildAdvisorQuarantineSourceText,
|
|
53
55
|
formatAdvisorBatchContent,
|
|
54
56
|
getOrCreateAdvisorProviderSessionId,
|
|
@@ -68,7 +70,8 @@ import {
|
|
|
68
70
|
import { MODEL_ROLES } from "../config/model-roles";
|
|
69
71
|
import { serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
|
|
70
72
|
import type { Settings } from "../config/settings";
|
|
71
|
-
import { CursorExecHandlers } from "../cursor";
|
|
73
|
+
import { CursorExecHandlers, type CursorMcpResourceAdapter } from "../cursor";
|
|
74
|
+
import { bridgeToolMap } from "../cursor-bridge-tools";
|
|
72
75
|
import { estimateToolSchemaTokens } from "../modes/utils/context-usage";
|
|
73
76
|
import type { PlanModeState } from "../plan-mode/state";
|
|
74
77
|
import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
|
|
@@ -178,12 +181,44 @@ interface AdvisorRuntimeDescriptor {
|
|
|
178
181
|
export interface SessionAdvisorsOptions {
|
|
179
182
|
enabled: boolean;
|
|
180
183
|
tools?: AgentTool[];
|
|
184
|
+
/**
|
|
185
|
+
* Build a `grep` honoring a Cursor `pi_grep` frame's own context width and
|
|
186
|
+
* match cap. The advisor's tools are fixed instances carrying session
|
|
187
|
+
* defaults, so without this an advisor running against Cursor silently
|
|
188
|
+
* drops both fields — the same gap the primary bridge closes.
|
|
189
|
+
*/
|
|
190
|
+
createGrepTool?(options: { context?: number; totalMatchLimit?: number }): AgentTool | undefined;
|
|
191
|
+
/**
|
|
192
|
+
* Build the `replace`-mode `edit` a Cursor `pi_edit` frame needs. The
|
|
193
|
+
* advisor's own instance follows the configured `edit.mode` (`hashline` by
|
|
194
|
+
* default), whose schema the frame's `old_text`/`new_text` pairs do not
|
|
195
|
+
* match, so without this every native advisor edit fails validation.
|
|
196
|
+
*/
|
|
197
|
+
createEditTool?(): AgentTool | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* The execute-time context the bridge's tools resolve approval from.
|
|
200
|
+
*
|
|
201
|
+
* `ExtensionToolWrapper` reads the approval mode, per-tool policies and
|
|
202
|
+
* `autoApprove` only from here; with none it falls back to `yolo` and empty
|
|
203
|
+
* policies, so a native frame would run past a configured `ask` or `deny`.
|
|
204
|
+
*/
|
|
205
|
+
getToolContext?: () => AgentToolContext | undefined;
|
|
206
|
+
/**
|
|
207
|
+
* The live MCP connections Cursor's resource frames answer from.
|
|
208
|
+
*
|
|
209
|
+
* Advisors share the session's connections and may hold tools from those
|
|
210
|
+
* same servers, so without this their frames report that every server
|
|
211
|
+
* advertises nothing.
|
|
212
|
+
*/
|
|
213
|
+
mcpResources?: CursorMcpResourceAdapter;
|
|
181
214
|
watchdogPrompt?: string;
|
|
182
215
|
sharedInstructions?: string;
|
|
183
216
|
contextPrompt?: string;
|
|
184
217
|
configs?: AdvisorConfig[];
|
|
185
218
|
streamFn?: StreamFn;
|
|
186
219
|
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
220
|
+
/** Advisor spend already persisted for this session, restored on resume. */
|
|
221
|
+
initialCosts?: ReadonlyMap<string, number>;
|
|
187
222
|
}
|
|
188
223
|
|
|
189
224
|
/** Options accepted when an advisor injects a primary-session message. */
|
|
@@ -222,7 +257,11 @@ export interface SessionAdvisorsHost {
|
|
|
222
257
|
hasPendingNextTurnMessages(): boolean;
|
|
223
258
|
convertToLlmForSideRequest(messages: AgentMessage[]): Message[];
|
|
224
259
|
effectiveServiceTier(model: Model): ServiceTier | undefined;
|
|
225
|
-
resolveContextPromotionTarget(
|
|
260
|
+
resolveContextPromotionTarget(
|
|
261
|
+
currentModel: Model,
|
|
262
|
+
contextWindow: number,
|
|
263
|
+
signal: AbortSignal,
|
|
264
|
+
): Promise<Model | undefined>;
|
|
226
265
|
resolveCompactionModelCandidates(preferredModel: Model | null | undefined, availableModels: Model[]): Model[];
|
|
227
266
|
resolveRetryFallbackRole(currentSelector: string, currentModel?: Model | null): string | undefined;
|
|
228
267
|
findRetryFallbackCandidates(
|
|
@@ -245,6 +284,10 @@ export class SessionAdvisors {
|
|
|
245
284
|
readonly #host: SessionAdvisorsHost;
|
|
246
285
|
#advisorEnabled: boolean;
|
|
247
286
|
#advisorTools: AgentTool[] | undefined;
|
|
287
|
+
#advisorCreateGrepTool: SessionAdvisorsOptions["createGrepTool"];
|
|
288
|
+
#advisorCreateEditTool: SessionAdvisorsOptions["createEditTool"];
|
|
289
|
+
#advisorGetToolContext: SessionAdvisorsOptions["getToolContext"];
|
|
290
|
+
#advisorMcpResources: SessionAdvisorsOptions["mcpResources"];
|
|
248
291
|
#advisorWatchdogPrompt: string | undefined;
|
|
249
292
|
#advisorSharedInstructions: string | undefined;
|
|
250
293
|
#advisorContextPrompt: string | undefined;
|
|
@@ -267,12 +310,17 @@ export class SessionAdvisors {
|
|
|
267
310
|
this.#host = host;
|
|
268
311
|
this.#advisorEnabled = options.enabled;
|
|
269
312
|
this.#advisorTools = options.tools;
|
|
313
|
+
this.#advisorCreateGrepTool = options.createGrepTool;
|
|
314
|
+
this.#advisorCreateEditTool = options.createEditTool;
|
|
315
|
+
this.#advisorGetToolContext = options.getToolContext;
|
|
316
|
+
this.#advisorMcpResources = options.mcpResources;
|
|
270
317
|
this.#advisorWatchdogPrompt = options.watchdogPrompt;
|
|
271
318
|
this.#advisorSharedInstructions = options.sharedInstructions;
|
|
272
319
|
this.#advisorContextPrompt = options.contextPrompt;
|
|
273
320
|
this.#advisorConfigs = options.configs;
|
|
274
321
|
this.#advisorStreamFn = options.streamFn;
|
|
275
322
|
this.#transformProviderContext = options.transformProviderContext;
|
|
323
|
+
if (options.initialCosts) this.#advisorCosts = new Map(options.initialCosts);
|
|
276
324
|
if (this.#advisorEnabled) this.#buildAdvisorRuntime();
|
|
277
325
|
}
|
|
278
326
|
|
|
@@ -314,6 +362,15 @@ export class SessionAdvisors {
|
|
|
314
362
|
this.#stopAdvisorRuntime();
|
|
315
363
|
}
|
|
316
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Pause advisor work while old-session recorder feeds remain attached, then
|
|
367
|
+
* detach only after any active prompt has settled.
|
|
368
|
+
*/
|
|
369
|
+
async drainAndDetachRecorders(): Promise<void> {
|
|
370
|
+
await Promise.all(this.#advisors.map(advisor => advisor.runtime.pauseForSessionTransition()));
|
|
371
|
+
await this.detachAndCloseRecorders();
|
|
372
|
+
}
|
|
373
|
+
|
|
317
374
|
/** Detaches and drains recorder feeds before transcript artifacts are removed. */
|
|
318
375
|
async detachAndCloseRecorders(): Promise<void> {
|
|
319
376
|
const closes: Promise<void>[] = [];
|
|
@@ -326,6 +383,14 @@ export class SessionAdvisors {
|
|
|
326
383
|
await Promise.all(closes);
|
|
327
384
|
}
|
|
328
385
|
|
|
386
|
+
/** Reattach recorder feeds and resume work after a rolled-back or preserving transition. */
|
|
387
|
+
reattachRecorderFeeds(): void {
|
|
388
|
+
for (const advisor of this.#advisors) {
|
|
389
|
+
if (!advisor.agentUnsubscribe) this.#attachAdvisorRecorderFeed(advisor);
|
|
390
|
+
advisor.runtime.resumeAfterSessionTransition();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
329
394
|
/** Re-primes advisor transcript views across a conversation boundary. */
|
|
330
395
|
resetSessionState(options: { preserveCost?: boolean } = {}): void {
|
|
331
396
|
this.#resetAdvisorSessionState(options.preserveCost === true);
|
|
@@ -336,6 +401,11 @@ export class SessionAdvisors {
|
|
|
336
401
|
this.#advisorCosts.clear();
|
|
337
402
|
}
|
|
338
403
|
|
|
404
|
+
/** Replace the ledger with the spend recorded for the session becoming active. */
|
|
405
|
+
restoreCost(costs: ReadonlyMap<string, number>): void {
|
|
406
|
+
this.#advisorCosts = new Map(costs);
|
|
407
|
+
}
|
|
408
|
+
|
|
339
409
|
/**
|
|
340
410
|
* Rebind every live advisor to the active primary conversation's provider
|
|
341
411
|
* identity (session id, prompt-cache key, credential + metadata resolvers,
|
|
@@ -690,11 +760,29 @@ export class SessionAdvisors {
|
|
|
690
760
|
// to delete workspace files it was never granted (issue #5680 review).
|
|
691
761
|
const advisorCanMutateFiles = advisorToolMap.has("write") || advisorToolMap.has("edit");
|
|
692
762
|
if (advisorCanMutateFiles) availableAdvisorToolNames.add("delete");
|
|
763
|
+
// `pi_edit` speaks `replace`'s `old_text`/`new_text` schema, which the
|
|
764
|
+
// advisor's ordinary `EditTool` (built at the session's configured
|
|
765
|
+
// `edit.mode`, `hashline` by default) does not accept. The bridge map
|
|
766
|
+
// swaps in a `replace` instance for the exec channel only — the
|
|
767
|
+
// advisor's own loop keeps the tool it was given — and only when
|
|
768
|
+
// `edit` was actually granted.
|
|
693
769
|
const advisorCursorExecHandlers = new CursorExecHandlers({
|
|
694
770
|
cwd: this.#host.sessionManager.getCwd(),
|
|
695
771
|
getCwd: () => this.#host.sessionManager.getCwd(),
|
|
696
|
-
tools: advisorToolMap,
|
|
697
|
-
|
|
772
|
+
tools: bridgeToolMap(advisorToolMap, this.#advisorCreateEditTool),
|
|
773
|
+
// Approval mode, per-tool policies and `autoApprove` live only on
|
|
774
|
+
// this context; without it every bridge tool resolves as `yolo`.
|
|
775
|
+
getToolContext: this.#advisorGetToolContext,
|
|
776
|
+
allowDirectFileMutation: advisorCanMutateFiles,
|
|
777
|
+
// Gated on the advisor's own grant: the factory builds a fresh
|
|
778
|
+
// tool, so handing it over unconditionally would give a roster
|
|
779
|
+
// without `grep` a search tool it was denied.
|
|
780
|
+
createGrepTool: advisorToolMap.has("grep") ? this.#advisorCreateGrepTool : undefined,
|
|
781
|
+
// Advisors share the session's live MCP connections, so their
|
|
782
|
+
// resource frames answer from the same catalog the primary sees.
|
|
783
|
+
// Not gated on a tool grant: reading what a server advertises is
|
|
784
|
+
// not the same permission as calling one of its tools.
|
|
785
|
+
mcpResources: this.#advisorMcpResources,
|
|
698
786
|
});
|
|
699
787
|
const baseAdvisorStreamFn = this.#advisorStreamFn ?? streamSimple;
|
|
700
788
|
const advisorStreamFn: StreamFn = (requestModel, context, options) =>
|
|
@@ -785,10 +873,13 @@ export class SessionAdvisors {
|
|
|
785
873
|
const runtime = new AdvisorRuntime(advisorAgentFacade, {
|
|
786
874
|
snapshotMessages: () => this.#host.agent.state.messages,
|
|
787
875
|
enqueueAdvice: (note, severity) => this.#routeAdvice(advisorRef, note, severity),
|
|
788
|
-
maintainContext: incomingTokens
|
|
876
|
+
maintainContext: (incomingTokens, signal) =>
|
|
877
|
+
this.#maintainAdvisorContext(advisorRef, incomingTokens, signal),
|
|
789
878
|
obfuscator: this.#host.obfuscator,
|
|
879
|
+
getModelIdentity: () => formatModelString(advisorRef.agent.state.model),
|
|
790
880
|
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
|
|
791
|
-
onTurnError: (error, failedMessages) =>
|
|
881
|
+
onTurnError: (error, failedMessages, signal) =>
|
|
882
|
+
this.#recoverAdvisorTurn(advisorRef, error, failedMessages, signal),
|
|
792
883
|
onTurnSuccess: async () => {
|
|
793
884
|
const fallback = advisorRef.retryFallback;
|
|
794
885
|
if (!advisorRef.retryFallbackPendingSuccess || !fallback) return;
|
|
@@ -890,10 +981,6 @@ export class SessionAdvisors {
|
|
|
890
981
|
logger.debug("advisor advice suppressed by emission guard", { severity, advisor: advisor.name });
|
|
891
982
|
return;
|
|
892
983
|
}
|
|
893
|
-
// When newer primary turns already arrived while the advisor model was
|
|
894
|
-
// processing this batch, the advice was generated without seeing them.
|
|
895
|
-
// Append a lightweight staleness caveat so the primary can weigh recency.
|
|
896
|
-
const deliveredNote = annotateForStaleness(note, advisor.runtime.hasFreshBacklog);
|
|
897
984
|
// The implicit single ("default") advisor stamps no source name, so its
|
|
898
985
|
// agent-facing `<advisory>` bytes stay identical to the pre-multi-advisor path.
|
|
899
986
|
const source = advisor.slug ? advisor.name : undefined;
|
|
@@ -911,10 +998,10 @@ export class SessionAdvisors {
|
|
|
911
998
|
interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
|
|
912
999
|
});
|
|
913
1000
|
if (channel === "aside") {
|
|
914
|
-
this.#host.yieldQueue.enqueue("advisor", { note
|
|
1001
|
+
this.#host.yieldQueue.enqueue("advisor", { note, severity, advisor: source });
|
|
915
1002
|
return;
|
|
916
1003
|
}
|
|
917
|
-
const notes: AdvisorNote[] = [{ note
|
|
1004
|
+
const notes: AdvisorNote[] = [{ note, severity, advisor: source }];
|
|
918
1005
|
const content = formatAdvisorBatchContent(notes);
|
|
919
1006
|
const details = { notes } satisfies AdvisorMessageDetails;
|
|
920
1007
|
if (channel === "preserve") {
|
|
@@ -1020,7 +1107,7 @@ export class SessionAdvisors {
|
|
|
1020
1107
|
}
|
|
1021
1108
|
|
|
1022
1109
|
/** Restore an advisor's configured primary once its fallback cooldown expires. */
|
|
1023
|
-
async #maybeRestoreAdvisorRetryFallbackPrimary(advisor: ActiveAdvisor): Promise<void> {
|
|
1110
|
+
async #maybeRestoreAdvisorRetryFallbackPrimary(advisor: ActiveAdvisor, signal: AbortSignal): Promise<void> {
|
|
1024
1111
|
const fallback = advisor.retryFallback;
|
|
1025
1112
|
if (!fallback || getRetryFallbackRevertPolicy(this.#host.settings) !== "cooldown-expiry") return;
|
|
1026
1113
|
|
|
@@ -1048,8 +1135,9 @@ export class SessionAdvisors {
|
|
|
1048
1135
|
const primaryModel =
|
|
1049
1136
|
resolvedPrimary.model ?? this.#host.modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
1050
1137
|
if (!primaryModel) return;
|
|
1051
|
-
const apiKey = await this.#host.modelRegistry.getApiKey(primaryModel, advisor.providerSessionId);
|
|
1138
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(primaryModel, advisor.providerSessionId, { signal });
|
|
1052
1139
|
if (!apiKey) return;
|
|
1140
|
+
signal.throwIfAborted();
|
|
1053
1141
|
|
|
1054
1142
|
const thinkingToApply =
|
|
1055
1143
|
advisor.thinkingLevel === fallback.lastAppliedThinkingLevel
|
|
@@ -1069,6 +1157,7 @@ export class SessionAdvisors {
|
|
|
1069
1157
|
advisor: ActiveAdvisor,
|
|
1070
1158
|
error: unknown,
|
|
1071
1159
|
failedMessages: readonly AgentMessage[],
|
|
1160
|
+
signal: AbortSignal,
|
|
1072
1161
|
): Promise<boolean> {
|
|
1073
1162
|
if (error instanceof AdvisorOutputQuarantinedError) return false;
|
|
1074
1163
|
|
|
@@ -1091,6 +1180,7 @@ export class SessionAdvisors {
|
|
|
1091
1180
|
retryAfterMs: extractRetryHint(undefined, message),
|
|
1092
1181
|
baseUrl: currentModel.baseUrl,
|
|
1093
1182
|
modelId: currentModel.id,
|
|
1183
|
+
signal,
|
|
1094
1184
|
},
|
|
1095
1185
|
);
|
|
1096
1186
|
return outcome.switched;
|
|
@@ -1122,6 +1212,7 @@ export class SessionAdvisors {
|
|
|
1122
1212
|
retryAfterMs,
|
|
1123
1213
|
baseUrl: currentModel.baseUrl,
|
|
1124
1214
|
modelId: currentModel.id,
|
|
1215
|
+
signal,
|
|
1125
1216
|
},
|
|
1126
1217
|
);
|
|
1127
1218
|
if (outcome.switched) return true;
|
|
@@ -1139,8 +1230,9 @@ export class SessionAdvisors {
|
|
|
1139
1230
|
const resolved = resolveModelOverride([selector.raw], this.#host.modelRegistry, this.#host.settings);
|
|
1140
1231
|
const candidate = resolved.model ?? this.#host.modelRegistry.find(selector.provider, selector.id);
|
|
1141
1232
|
if (!candidate || modelsAreEqual(candidate, currentModel)) continue;
|
|
1142
|
-
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisor.providerSessionId);
|
|
1233
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisor.providerSessionId, { signal });
|
|
1143
1234
|
if (!apiKey) continue;
|
|
1235
|
+
signal.throwIfAborted();
|
|
1144
1236
|
|
|
1145
1237
|
const originalThinkingLevel = advisor.thinkingLevel;
|
|
1146
1238
|
const requestedThinkingLevel = selector.thinkingLevel ?? originalThinkingLevel;
|
|
@@ -1168,13 +1260,18 @@ export class SessionAdvisors {
|
|
|
1168
1260
|
return false;
|
|
1169
1261
|
}
|
|
1170
1262
|
|
|
1171
|
-
async #promoteAdvisorContextModel(
|
|
1263
|
+
async #promoteAdvisorContextModel(
|
|
1264
|
+
advisor: ActiveAdvisor,
|
|
1265
|
+
currentModel: Model,
|
|
1266
|
+
signal: AbortSignal,
|
|
1267
|
+
): Promise<boolean> {
|
|
1172
1268
|
const promotionSettings = this.#host.settings.getGroup("contextPromotion");
|
|
1173
1269
|
if (!promotionSettings.enabled) return false;
|
|
1174
1270
|
const contextWindow = currentModel.contextWindow ?? 0;
|
|
1175
1271
|
if (contextWindow <= 0) return false;
|
|
1176
|
-
const targetModel = await this.#host.resolveContextPromotionTarget(currentModel, contextWindow);
|
|
1272
|
+
const targetModel = await this.#host.resolveContextPromotionTarget(currentModel, contextWindow, signal);
|
|
1177
1273
|
if (!targetModel) return false;
|
|
1274
|
+
signal.throwIfAborted();
|
|
1178
1275
|
|
|
1179
1276
|
// Preserve this advisor's own thinking level (a configured `model:...:high`
|
|
1180
1277
|
// keeps its suffix across a promotion); only the model changes.
|
|
@@ -1198,8 +1295,12 @@ export class SessionAdvisors {
|
|
|
1198
1295
|
}
|
|
1199
1296
|
}
|
|
1200
1297
|
|
|
1201
|
-
async #maintainAdvisorContext(
|
|
1202
|
-
|
|
1298
|
+
async #maintainAdvisorContext(
|
|
1299
|
+
advisor: ActiveAdvisor,
|
|
1300
|
+
incomingTokens: number,
|
|
1301
|
+
signal: AbortSignal,
|
|
1302
|
+
): Promise<boolean> {
|
|
1303
|
+
await this.#maybeRestoreAdvisorRetryFallbackPrimary(advisor, signal);
|
|
1203
1304
|
const agent = advisor.agent;
|
|
1204
1305
|
|
|
1205
1306
|
const compactionSettings = this.#host.settings.getGroup("compaction");
|
|
@@ -1234,7 +1335,7 @@ export class SessionAdvisors {
|
|
|
1234
1335
|
}
|
|
1235
1336
|
|
|
1236
1337
|
// 1. Try promotion first
|
|
1237
|
-
if (await this.#promoteAdvisorContextModel(advisor, advisorModel)) {
|
|
1338
|
+
if (await this.#promoteAdvisorContextModel(advisor, advisorModel, signal)) {
|
|
1238
1339
|
// Promotion succeeded, check if new model has enough space
|
|
1239
1340
|
const newModel = agent.state.model;
|
|
1240
1341
|
const newWindow = newModel.contextWindow ?? 0;
|
|
@@ -1301,6 +1402,7 @@ export class SessionAdvisors {
|
|
|
1301
1402
|
|
|
1302
1403
|
let compactResult: CompactionResult | undefined;
|
|
1303
1404
|
let lastError: unknown;
|
|
1405
|
+
let nativeCompactionFailure: { error: NativeCompactionError; provider: string } | undefined;
|
|
1304
1406
|
// Instrument the advisor's overflow-compaction one-shot like the primary
|
|
1305
1407
|
// compaction path so the advisor model's maintenance call also emits spans.
|
|
1306
1408
|
const telemetry = resolveTelemetry(agent.telemetry, advisorProviderSessionId);
|
|
@@ -1312,8 +1414,16 @@ export class SessionAdvisors {
|
|
|
1312
1414
|
});
|
|
1313
1415
|
|
|
1314
1416
|
for (const candidate of candidates) {
|
|
1315
|
-
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisorProviderSessionId);
|
|
1417
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisorProviderSessionId, { signal });
|
|
1316
1418
|
if (!apiKey) continue;
|
|
1419
|
+
if (
|
|
1420
|
+
nativeCompactionFailure &&
|
|
1421
|
+
(candidate.provider !== nativeCompactionFailure.provider ||
|
|
1422
|
+
!shouldUseProviderNativeCompaction(candidate, compactionSettings))
|
|
1423
|
+
) {
|
|
1424
|
+
throw nativeCompactionFailure.error;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1317
1427
|
// The advisor overflow-compaction one-shot bypasses the advisor `Agent`,
|
|
1318
1428
|
// so its installed metadata resolver never runs. Emit the same
|
|
1319
1429
|
// `metadata.user_id` identity here (resolved per candidate provider,
|
|
@@ -1329,7 +1439,7 @@ export class SessionAdvisors {
|
|
|
1329
1439
|
candidate,
|
|
1330
1440
|
this.#host.modelRegistry.resolver(candidate, advisorProviderSessionId),
|
|
1331
1441
|
undefined,
|
|
1332
|
-
|
|
1442
|
+
signal,
|
|
1333
1443
|
{
|
|
1334
1444
|
thinkingLevel: advisorCompactionThinkingLevel,
|
|
1335
1445
|
convertToLlm: messages => this.#host.convertToLlmForSideRequest(messages),
|
|
@@ -1344,10 +1454,19 @@ export class SessionAdvisors {
|
|
|
1344
1454
|
);
|
|
1345
1455
|
break;
|
|
1346
1456
|
} catch (error) {
|
|
1457
|
+
if (signal.aborted) throw error;
|
|
1458
|
+
const id = AIError.classify(error, candidate.api);
|
|
1459
|
+
if (error instanceof NativeCompactionError && !AIError.is(id, AIError.Flag.AuthFailed)) {
|
|
1460
|
+
nativeCompactionFailure ??= { error, provider: candidate.provider };
|
|
1461
|
+
lastError = nativeCompactionFailure.error;
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1347
1464
|
lastError = error;
|
|
1348
1465
|
}
|
|
1349
1466
|
}
|
|
1350
1467
|
|
|
1468
|
+
if (!compactResult && nativeCompactionFailure) throw nativeCompactionFailure.error;
|
|
1469
|
+
|
|
1351
1470
|
if (!compactResult) {
|
|
1352
1471
|
logger.warn("Advisor compaction failed, falling back to re-prime", { error: String(lastError) });
|
|
1353
1472
|
return true;
|