@oh-my-pi/pi-coding-agent 17.1.8 → 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 +92 -0
- package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-be1f2t8h.md} +92 -0
- package/dist/cli.js +9986 -9776
- package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
- package/dist/types/advisor/runtime.d.ts +8 -2
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- 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/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/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -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 +4 -2
- 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/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/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/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/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/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 +12 -12
- package/src/advisor/runtime.ts +138 -12
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/auto-thinking/classifier.ts +48 -10
- 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.ts +9 -0
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -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/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/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-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/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 +15 -9
- 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/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 +31 -17
- 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/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/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- 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 +302 -175
- 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 +11 -3
- package/src/session/session-advisors.ts +140 -16
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- 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 +131 -3
- package/src/system-prompt.ts +3 -2
- package/src/task/executor.ts +17 -4
- package/src/task/render.ts +20 -4
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/browser.ts +5 -0
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +28 -4
- package/src/tools/path-utils.ts +88 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/write.ts +9 -3
- package/src/tools/xdev.ts +39 -14
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/types.ts +1 -1
|
@@ -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,
|
|
@@ -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();
|
|
@@ -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,
|
|
@@ -67,7 +70,8 @@ import {
|
|
|
67
70
|
import { MODEL_ROLES } from "../config/model-roles";
|
|
68
71
|
import { serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
|
|
69
72
|
import type { Settings } from "../config/settings";
|
|
70
|
-
import { CursorExecHandlers } from "../cursor";
|
|
73
|
+
import { CursorExecHandlers, type CursorMcpResourceAdapter } from "../cursor";
|
|
74
|
+
import { bridgeToolMap } from "../cursor-bridge-tools";
|
|
71
75
|
import { estimateToolSchemaTokens } from "../modes/utils/context-usage";
|
|
72
76
|
import type { PlanModeState } from "../plan-mode/state";
|
|
73
77
|
import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
|
|
@@ -177,12 +181,44 @@ interface AdvisorRuntimeDescriptor {
|
|
|
177
181
|
export interface SessionAdvisorsOptions {
|
|
178
182
|
enabled: boolean;
|
|
179
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;
|
|
180
214
|
watchdogPrompt?: string;
|
|
181
215
|
sharedInstructions?: string;
|
|
182
216
|
contextPrompt?: string;
|
|
183
217
|
configs?: AdvisorConfig[];
|
|
184
218
|
streamFn?: StreamFn;
|
|
185
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>;
|
|
186
222
|
}
|
|
187
223
|
|
|
188
224
|
/** Options accepted when an advisor injects a primary-session message. */
|
|
@@ -221,7 +257,11 @@ export interface SessionAdvisorsHost {
|
|
|
221
257
|
hasPendingNextTurnMessages(): boolean;
|
|
222
258
|
convertToLlmForSideRequest(messages: AgentMessage[]): Message[];
|
|
223
259
|
effectiveServiceTier(model: Model): ServiceTier | undefined;
|
|
224
|
-
resolveContextPromotionTarget(
|
|
260
|
+
resolveContextPromotionTarget(
|
|
261
|
+
currentModel: Model,
|
|
262
|
+
contextWindow: number,
|
|
263
|
+
signal: AbortSignal,
|
|
264
|
+
): Promise<Model | undefined>;
|
|
225
265
|
resolveCompactionModelCandidates(preferredModel: Model | null | undefined, availableModels: Model[]): Model[];
|
|
226
266
|
resolveRetryFallbackRole(currentSelector: string, currentModel?: Model | null): string | undefined;
|
|
227
267
|
findRetryFallbackCandidates(
|
|
@@ -244,6 +284,10 @@ export class SessionAdvisors {
|
|
|
244
284
|
readonly #host: SessionAdvisorsHost;
|
|
245
285
|
#advisorEnabled: boolean;
|
|
246
286
|
#advisorTools: AgentTool[] | undefined;
|
|
287
|
+
#advisorCreateGrepTool: SessionAdvisorsOptions["createGrepTool"];
|
|
288
|
+
#advisorCreateEditTool: SessionAdvisorsOptions["createEditTool"];
|
|
289
|
+
#advisorGetToolContext: SessionAdvisorsOptions["getToolContext"];
|
|
290
|
+
#advisorMcpResources: SessionAdvisorsOptions["mcpResources"];
|
|
247
291
|
#advisorWatchdogPrompt: string | undefined;
|
|
248
292
|
#advisorSharedInstructions: string | undefined;
|
|
249
293
|
#advisorContextPrompt: string | undefined;
|
|
@@ -266,12 +310,17 @@ export class SessionAdvisors {
|
|
|
266
310
|
this.#host = host;
|
|
267
311
|
this.#advisorEnabled = options.enabled;
|
|
268
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;
|
|
269
317
|
this.#advisorWatchdogPrompt = options.watchdogPrompt;
|
|
270
318
|
this.#advisorSharedInstructions = options.sharedInstructions;
|
|
271
319
|
this.#advisorContextPrompt = options.contextPrompt;
|
|
272
320
|
this.#advisorConfigs = options.configs;
|
|
273
321
|
this.#advisorStreamFn = options.streamFn;
|
|
274
322
|
this.#transformProviderContext = options.transformProviderContext;
|
|
323
|
+
if (options.initialCosts) this.#advisorCosts = new Map(options.initialCosts);
|
|
275
324
|
if (this.#advisorEnabled) this.#buildAdvisorRuntime();
|
|
276
325
|
}
|
|
277
326
|
|
|
@@ -313,6 +362,15 @@ export class SessionAdvisors {
|
|
|
313
362
|
this.#stopAdvisorRuntime();
|
|
314
363
|
}
|
|
315
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
|
+
|
|
316
374
|
/** Detaches and drains recorder feeds before transcript artifacts are removed. */
|
|
317
375
|
async detachAndCloseRecorders(): Promise<void> {
|
|
318
376
|
const closes: Promise<void>[] = [];
|
|
@@ -325,6 +383,14 @@ export class SessionAdvisors {
|
|
|
325
383
|
await Promise.all(closes);
|
|
326
384
|
}
|
|
327
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
|
+
|
|
328
394
|
/** Re-primes advisor transcript views across a conversation boundary. */
|
|
329
395
|
resetSessionState(options: { preserveCost?: boolean } = {}): void {
|
|
330
396
|
this.#resetAdvisorSessionState(options.preserveCost === true);
|
|
@@ -335,6 +401,11 @@ export class SessionAdvisors {
|
|
|
335
401
|
this.#advisorCosts.clear();
|
|
336
402
|
}
|
|
337
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
|
+
|
|
338
409
|
/**
|
|
339
410
|
* Rebind every live advisor to the active primary conversation's provider
|
|
340
411
|
* identity (session id, prompt-cache key, credential + metadata resolvers,
|
|
@@ -689,11 +760,29 @@ export class SessionAdvisors {
|
|
|
689
760
|
// to delete workspace files it was never granted (issue #5680 review).
|
|
690
761
|
const advisorCanMutateFiles = advisorToolMap.has("write") || advisorToolMap.has("edit");
|
|
691
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.
|
|
692
769
|
const advisorCursorExecHandlers = new CursorExecHandlers({
|
|
693
770
|
cwd: this.#host.sessionManager.getCwd(),
|
|
694
771
|
getCwd: () => this.#host.sessionManager.getCwd(),
|
|
695
|
-
tools: advisorToolMap,
|
|
696
|
-
|
|
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,
|
|
697
786
|
});
|
|
698
787
|
const baseAdvisorStreamFn = this.#advisorStreamFn ?? streamSimple;
|
|
699
788
|
const advisorStreamFn: StreamFn = (requestModel, context, options) =>
|
|
@@ -784,10 +873,13 @@ export class SessionAdvisors {
|
|
|
784
873
|
const runtime = new AdvisorRuntime(advisorAgentFacade, {
|
|
785
874
|
snapshotMessages: () => this.#host.agent.state.messages,
|
|
786
875
|
enqueueAdvice: (note, severity) => this.#routeAdvice(advisorRef, note, severity),
|
|
787
|
-
maintainContext: incomingTokens
|
|
876
|
+
maintainContext: (incomingTokens, signal) =>
|
|
877
|
+
this.#maintainAdvisorContext(advisorRef, incomingTokens, signal),
|
|
788
878
|
obfuscator: this.#host.obfuscator,
|
|
879
|
+
getModelIdentity: () => formatModelString(advisorRef.agent.state.model),
|
|
789
880
|
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
|
|
790
|
-
onTurnError: (error, failedMessages) =>
|
|
881
|
+
onTurnError: (error, failedMessages, signal) =>
|
|
882
|
+
this.#recoverAdvisorTurn(advisorRef, error, failedMessages, signal),
|
|
791
883
|
onTurnSuccess: async () => {
|
|
792
884
|
const fallback = advisorRef.retryFallback;
|
|
793
885
|
if (!advisorRef.retryFallbackPendingSuccess || !fallback) return;
|
|
@@ -1015,7 +1107,7 @@ export class SessionAdvisors {
|
|
|
1015
1107
|
}
|
|
1016
1108
|
|
|
1017
1109
|
/** Restore an advisor's configured primary once its fallback cooldown expires. */
|
|
1018
|
-
async #maybeRestoreAdvisorRetryFallbackPrimary(advisor: ActiveAdvisor): Promise<void> {
|
|
1110
|
+
async #maybeRestoreAdvisorRetryFallbackPrimary(advisor: ActiveAdvisor, signal: AbortSignal): Promise<void> {
|
|
1019
1111
|
const fallback = advisor.retryFallback;
|
|
1020
1112
|
if (!fallback || getRetryFallbackRevertPolicy(this.#host.settings) !== "cooldown-expiry") return;
|
|
1021
1113
|
|
|
@@ -1043,8 +1135,9 @@ export class SessionAdvisors {
|
|
|
1043
1135
|
const primaryModel =
|
|
1044
1136
|
resolvedPrimary.model ?? this.#host.modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
1045
1137
|
if (!primaryModel) return;
|
|
1046
|
-
const apiKey = await this.#host.modelRegistry.getApiKey(primaryModel, advisor.providerSessionId);
|
|
1138
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(primaryModel, advisor.providerSessionId, { signal });
|
|
1047
1139
|
if (!apiKey) return;
|
|
1140
|
+
signal.throwIfAborted();
|
|
1048
1141
|
|
|
1049
1142
|
const thinkingToApply =
|
|
1050
1143
|
advisor.thinkingLevel === fallback.lastAppliedThinkingLevel
|
|
@@ -1064,6 +1157,7 @@ export class SessionAdvisors {
|
|
|
1064
1157
|
advisor: ActiveAdvisor,
|
|
1065
1158
|
error: unknown,
|
|
1066
1159
|
failedMessages: readonly AgentMessage[],
|
|
1160
|
+
signal: AbortSignal,
|
|
1067
1161
|
): Promise<boolean> {
|
|
1068
1162
|
if (error instanceof AdvisorOutputQuarantinedError) return false;
|
|
1069
1163
|
|
|
@@ -1086,6 +1180,7 @@ export class SessionAdvisors {
|
|
|
1086
1180
|
retryAfterMs: extractRetryHint(undefined, message),
|
|
1087
1181
|
baseUrl: currentModel.baseUrl,
|
|
1088
1182
|
modelId: currentModel.id,
|
|
1183
|
+
signal,
|
|
1089
1184
|
},
|
|
1090
1185
|
);
|
|
1091
1186
|
return outcome.switched;
|
|
@@ -1117,6 +1212,7 @@ export class SessionAdvisors {
|
|
|
1117
1212
|
retryAfterMs,
|
|
1118
1213
|
baseUrl: currentModel.baseUrl,
|
|
1119
1214
|
modelId: currentModel.id,
|
|
1215
|
+
signal,
|
|
1120
1216
|
},
|
|
1121
1217
|
);
|
|
1122
1218
|
if (outcome.switched) return true;
|
|
@@ -1134,8 +1230,9 @@ export class SessionAdvisors {
|
|
|
1134
1230
|
const resolved = resolveModelOverride([selector.raw], this.#host.modelRegistry, this.#host.settings);
|
|
1135
1231
|
const candidate = resolved.model ?? this.#host.modelRegistry.find(selector.provider, selector.id);
|
|
1136
1232
|
if (!candidate || modelsAreEqual(candidate, currentModel)) continue;
|
|
1137
|
-
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisor.providerSessionId);
|
|
1233
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisor.providerSessionId, { signal });
|
|
1138
1234
|
if (!apiKey) continue;
|
|
1235
|
+
signal.throwIfAborted();
|
|
1139
1236
|
|
|
1140
1237
|
const originalThinkingLevel = advisor.thinkingLevel;
|
|
1141
1238
|
const requestedThinkingLevel = selector.thinkingLevel ?? originalThinkingLevel;
|
|
@@ -1163,13 +1260,18 @@ export class SessionAdvisors {
|
|
|
1163
1260
|
return false;
|
|
1164
1261
|
}
|
|
1165
1262
|
|
|
1166
|
-
async #promoteAdvisorContextModel(
|
|
1263
|
+
async #promoteAdvisorContextModel(
|
|
1264
|
+
advisor: ActiveAdvisor,
|
|
1265
|
+
currentModel: Model,
|
|
1266
|
+
signal: AbortSignal,
|
|
1267
|
+
): Promise<boolean> {
|
|
1167
1268
|
const promotionSettings = this.#host.settings.getGroup("contextPromotion");
|
|
1168
1269
|
if (!promotionSettings.enabled) return false;
|
|
1169
1270
|
const contextWindow = currentModel.contextWindow ?? 0;
|
|
1170
1271
|
if (contextWindow <= 0) return false;
|
|
1171
|
-
const targetModel = await this.#host.resolveContextPromotionTarget(currentModel, contextWindow);
|
|
1272
|
+
const targetModel = await this.#host.resolveContextPromotionTarget(currentModel, contextWindow, signal);
|
|
1172
1273
|
if (!targetModel) return false;
|
|
1274
|
+
signal.throwIfAborted();
|
|
1173
1275
|
|
|
1174
1276
|
// Preserve this advisor's own thinking level (a configured `model:...:high`
|
|
1175
1277
|
// keeps its suffix across a promotion); only the model changes.
|
|
@@ -1193,8 +1295,12 @@ export class SessionAdvisors {
|
|
|
1193
1295
|
}
|
|
1194
1296
|
}
|
|
1195
1297
|
|
|
1196
|
-
async #maintainAdvisorContext(
|
|
1197
|
-
|
|
1298
|
+
async #maintainAdvisorContext(
|
|
1299
|
+
advisor: ActiveAdvisor,
|
|
1300
|
+
incomingTokens: number,
|
|
1301
|
+
signal: AbortSignal,
|
|
1302
|
+
): Promise<boolean> {
|
|
1303
|
+
await this.#maybeRestoreAdvisorRetryFallbackPrimary(advisor, signal);
|
|
1198
1304
|
const agent = advisor.agent;
|
|
1199
1305
|
|
|
1200
1306
|
const compactionSettings = this.#host.settings.getGroup("compaction");
|
|
@@ -1229,7 +1335,7 @@ export class SessionAdvisors {
|
|
|
1229
1335
|
}
|
|
1230
1336
|
|
|
1231
1337
|
// 1. Try promotion first
|
|
1232
|
-
if (await this.#promoteAdvisorContextModel(advisor, advisorModel)) {
|
|
1338
|
+
if (await this.#promoteAdvisorContextModel(advisor, advisorModel, signal)) {
|
|
1233
1339
|
// Promotion succeeded, check if new model has enough space
|
|
1234
1340
|
const newModel = agent.state.model;
|
|
1235
1341
|
const newWindow = newModel.contextWindow ?? 0;
|
|
@@ -1296,6 +1402,7 @@ export class SessionAdvisors {
|
|
|
1296
1402
|
|
|
1297
1403
|
let compactResult: CompactionResult | undefined;
|
|
1298
1404
|
let lastError: unknown;
|
|
1405
|
+
let nativeCompactionFailure: { error: NativeCompactionError; provider: string } | undefined;
|
|
1299
1406
|
// Instrument the advisor's overflow-compaction one-shot like the primary
|
|
1300
1407
|
// compaction path so the advisor model's maintenance call also emits spans.
|
|
1301
1408
|
const telemetry = resolveTelemetry(agent.telemetry, advisorProviderSessionId);
|
|
@@ -1307,8 +1414,16 @@ export class SessionAdvisors {
|
|
|
1307
1414
|
});
|
|
1308
1415
|
|
|
1309
1416
|
for (const candidate of candidates) {
|
|
1310
|
-
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisorProviderSessionId);
|
|
1417
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisorProviderSessionId, { signal });
|
|
1311
1418
|
if (!apiKey) continue;
|
|
1419
|
+
if (
|
|
1420
|
+
nativeCompactionFailure &&
|
|
1421
|
+
(candidate.provider !== nativeCompactionFailure.provider ||
|
|
1422
|
+
!shouldUseProviderNativeCompaction(candidate, compactionSettings))
|
|
1423
|
+
) {
|
|
1424
|
+
throw nativeCompactionFailure.error;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1312
1427
|
// The advisor overflow-compaction one-shot bypasses the advisor `Agent`,
|
|
1313
1428
|
// so its installed metadata resolver never runs. Emit the same
|
|
1314
1429
|
// `metadata.user_id` identity here (resolved per candidate provider,
|
|
@@ -1324,7 +1439,7 @@ export class SessionAdvisors {
|
|
|
1324
1439
|
candidate,
|
|
1325
1440
|
this.#host.modelRegistry.resolver(candidate, advisorProviderSessionId),
|
|
1326
1441
|
undefined,
|
|
1327
|
-
|
|
1442
|
+
signal,
|
|
1328
1443
|
{
|
|
1329
1444
|
thinkingLevel: advisorCompactionThinkingLevel,
|
|
1330
1445
|
convertToLlm: messages => this.#host.convertToLlmForSideRequest(messages),
|
|
@@ -1339,10 +1454,19 @@ export class SessionAdvisors {
|
|
|
1339
1454
|
);
|
|
1340
1455
|
break;
|
|
1341
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
|
+
}
|
|
1342
1464
|
lastError = error;
|
|
1343
1465
|
}
|
|
1344
1466
|
}
|
|
1345
1467
|
|
|
1468
|
+
if (!compactResult && nativeCompactionFailure) throw nativeCompactionFailure.error;
|
|
1469
|
+
|
|
1346
1470
|
if (!compactResult) {
|
|
1347
1471
|
logger.warn("Advisor compaction failed, falling back to re-prime", { error: String(lastError) });
|
|
1348
1472
|
return true;
|
|
@@ -154,6 +154,7 @@ export interface TitleChangeEntry extends SessionEntryBase {
|
|
|
154
154
|
declare module "@oh-my-pi/pi-agent-core/compaction/entries" {
|
|
155
155
|
interface CustomCompactionSessionEntries {
|
|
156
156
|
titleChange: TitleChangeEntry;
|
|
157
|
+
credentialPin: CredentialPinEntry;
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
|
|
@@ -164,6 +165,24 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
|
|
|
164
165
|
injectedRules: string[];
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Records which OAuth account served this session's requests for a provider.
|
|
170
|
+
*
|
|
171
|
+
* Provider prompt caches (Anthropic in particular) are account-scoped, and the
|
|
172
|
+
* auth store's session-sticky routing is process-local under a remote auth
|
|
173
|
+
* broker, so resume must re-pin the same account to reuse the warm cache
|
|
174
|
+
* prefix. Stores a sha-256 of the account + billing-scope tuple instead of
|
|
175
|
+
* the raw email/uuid/org; note an unsalted digest of a guessable email is
|
|
176
|
+
* still linkable, so exported sessions are pseudonymous, not anonymous.
|
|
177
|
+
*/
|
|
178
|
+
export interface CredentialPinEntry extends SessionEntryBase {
|
|
179
|
+
type: "credential_pin";
|
|
180
|
+
/** Provider id the pin applies to (e.g. "anthropic"). */
|
|
181
|
+
provider: string;
|
|
182
|
+
/** `credentialPinHash()` of the serving account's identity + scope tuple. */
|
|
183
|
+
hash: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
167
186
|
/** Session init entry - captures initial context for subagent sessions (debugging/replay). */
|
|
168
187
|
export interface SessionInitEntry extends SessionEntryBase {
|
|
169
188
|
type: "session_init";
|
|
@@ -230,7 +249,8 @@ export type SessionEntry =
|
|
|
230
249
|
| TitleChangeEntry
|
|
231
250
|
| TtsrInjectionEntry
|
|
232
251
|
| SessionInitEntry
|
|
233
|
-
| ModeChangeEntry
|
|
252
|
+
| ModeChangeEntry
|
|
253
|
+
| CredentialPinEntry;
|
|
234
254
|
|
|
235
255
|
/** Raw logical file entry after loaders strip any fixed-width title slot. */
|
|
236
256
|
export type FileEntry = SessionHeader | SessionEntry;
|