@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
|
@@ -64,7 +64,10 @@ export interface SessionHandoffHost {
|
|
|
64
64
|
clearPendingNextTurnMessages(): void;
|
|
65
65
|
resetTodoCycle(): void;
|
|
66
66
|
buildDisplaySessionContext(): SessionContext;
|
|
67
|
-
|
|
67
|
+
resetAdvisorSessionState(): void;
|
|
68
|
+
drainAndDetachAdvisorRecorders(): Promise<void>;
|
|
69
|
+
reattachAdvisorRecorderFeeds(): void;
|
|
70
|
+
clearAdvisorCost(): void;
|
|
68
71
|
syncTodoPhasesFromBranch(): void;
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -124,6 +127,8 @@ export class SessionHandoff {
|
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
let advisorRecordersDetached = false;
|
|
131
|
+
let sessionTransitioned = false;
|
|
127
132
|
try {
|
|
128
133
|
if (handoffSignal.aborted) {
|
|
129
134
|
throw new Error("Handoff cancelled");
|
|
@@ -224,14 +229,21 @@ export class SessionHandoff {
|
|
|
224
229
|
}
|
|
225
230
|
await this.#host.flushPendingBash();
|
|
226
231
|
await this.#host.sessionManager.flush();
|
|
232
|
+
advisorRecordersDetached = true;
|
|
233
|
+
// Stop and settle in-flight advisors while the old-session feeds can still
|
|
234
|
+
// observe message_end, then mute before opening the replacement session.
|
|
235
|
+
await this.#host.drainAndDetachAdvisorRecorders();
|
|
227
236
|
const bashTransition = this.#host.beginBashSessionTransition();
|
|
228
237
|
this.#host.cancelOwnAsyncJobs();
|
|
229
|
-
let sessionTransitioned = false;
|
|
230
238
|
try {
|
|
231
239
|
await this.#host.sessionManager.newSession(
|
|
232
240
|
previousSessionFile ? { parentSession: previousSessionFile } : undefined,
|
|
233
241
|
);
|
|
234
242
|
this.#host.markBashSessionTransition(bashTransition);
|
|
243
|
+
// The handoff opens a fresh conversation, so the spend of the one it
|
|
244
|
+
// summarizes stays with it. Clearing here, at the commit point, keeps the
|
|
245
|
+
// status line honest even if a later step throws.
|
|
246
|
+
this.#host.clearAdvisorCost();
|
|
235
247
|
sessionTransitioned = true;
|
|
236
248
|
} finally {
|
|
237
249
|
this.#host.finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
@@ -284,7 +296,8 @@ export class SessionHandoff {
|
|
|
284
296
|
// Rebuild agent messages from session
|
|
285
297
|
const sessionContext = this.#host.buildDisplaySessionContext();
|
|
286
298
|
this.#host.agent.replaceMessages(sessionContext.messages);
|
|
287
|
-
this.#host.
|
|
299
|
+
this.#host.resetAdvisorSessionState();
|
|
300
|
+
advisorRecordersDetached = false;
|
|
288
301
|
this.#host.syncTodoPhasesFromBranch();
|
|
289
302
|
if (this.#host.extensionRunner) {
|
|
290
303
|
await this.#host.extensionRunner.emit({
|
|
@@ -301,6 +314,10 @@ export class SessionHandoff {
|
|
|
301
314
|
}
|
|
302
315
|
throw error;
|
|
303
316
|
} finally {
|
|
317
|
+
if (advisorRecordersDetached) {
|
|
318
|
+
if (sessionTransitioned) this.#host.resetAdvisorSessionState();
|
|
319
|
+
else this.#host.reattachAdvisorRecorderFeeds();
|
|
320
|
+
}
|
|
304
321
|
sourceSignal?.removeEventListener("abort", onSourceAbort);
|
|
305
322
|
this.#handoffAbortController = undefined;
|
|
306
323
|
}
|
|
@@ -56,14 +56,14 @@ export function parseSessionContent(content: string): {
|
|
|
56
56
|
return { entries: foldTitleSlot(entries, slot), titleSlot: slot };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
export async function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const entries: FileEntry[] = [];
|
|
59
|
+
/** Parse session JSONL and visit each entry without retaining prior entries. */
|
|
60
|
+
export async function visitEntriesFromFileStream(
|
|
61
|
+
filePath: string,
|
|
62
|
+
visit: (entry: FileEntry) => void,
|
|
63
|
+
): Promise<SessionTitleUpdate | undefined> {
|
|
65
64
|
let titleSlot: SessionTitleUpdate | undefined;
|
|
66
65
|
let sawFirstLine = false;
|
|
66
|
+
let visitorThrew = false;
|
|
67
67
|
// Byte buffer (NOT a decoded string): multibyte UTF-8 sequences that straddle
|
|
68
68
|
// a stream-chunk boundary stay intact, and Bun.JSONL.parseChunk accepts typed
|
|
69
69
|
// arrays directly. Only the unconsumed remainder is held (≤ one record + a
|
|
@@ -75,8 +75,13 @@ export async function loadEntriesFromFileStream(filePath: string): Promise<{
|
|
|
75
75
|
const drain = () => {
|
|
76
76
|
while (buffer.length > 0) {
|
|
77
77
|
const { values, error, read, done } = Bun.JSONL.parseChunk(buffer);
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
for (const value of values) {
|
|
79
|
+
try {
|
|
80
|
+
visit(value as FileEntry);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
visitorThrew = true;
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
80
85
|
}
|
|
81
86
|
if (error) {
|
|
82
87
|
// Malformed record: skip past the next newline and continue.
|
|
@@ -125,10 +130,21 @@ export async function loadEntriesFromFileStream(filePath: string): Promise<{
|
|
|
125
130
|
}
|
|
126
131
|
drain();
|
|
127
132
|
} catch (err) {
|
|
128
|
-
if (
|
|
133
|
+
if (visitorThrew) throw err;
|
|
134
|
+
if (isEnoent(err)) return undefined;
|
|
129
135
|
throw err;
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
return titleSlot;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Exported for testing — the ≥8MiB streaming path (works on any file size). */
|
|
142
|
+
export async function loadEntriesFromFileStream(filePath: string): Promise<{
|
|
143
|
+
entries: FileEntry[];
|
|
144
|
+
titleSlot: SessionTitleUpdate | undefined;
|
|
145
|
+
}> {
|
|
146
|
+
const entries: FileEntry[] = [];
|
|
147
|
+
const titleSlot = await visitEntriesFromFileStream(filePath, entry => entries.push(entry));
|
|
132
148
|
return { entries: foldTitleSlot(entries, titleSlot), titleSlot };
|
|
133
149
|
}
|
|
134
150
|
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
DEFAULT_SHAKE_CONFIG,
|
|
27
27
|
effectiveReserveTokens,
|
|
28
28
|
estimateTokens,
|
|
29
|
+
NativeCompactionError,
|
|
29
30
|
prepareCompaction,
|
|
30
31
|
resolveBudgetReserveTokens,
|
|
31
32
|
resolveThresholdTokens,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
type SummaryOptions,
|
|
35
36
|
shouldCompact,
|
|
36
37
|
shouldUseOpenAiRemoteCompaction,
|
|
38
|
+
shouldUseProviderNativeCompaction,
|
|
37
39
|
} from "@oh-my-pi/pi-agent-core/compaction";
|
|
38
40
|
import {
|
|
39
41
|
DEFAULT_PRUNE_CONFIG,
|
|
@@ -1373,7 +1375,11 @@ export class SessionMaintenance {
|
|
|
1373
1375
|
}
|
|
1374
1376
|
}
|
|
1375
1377
|
|
|
1376
|
-
async resolveContextPromotionTarget(
|
|
1378
|
+
async resolveContextPromotionTarget(
|
|
1379
|
+
currentModel: Model,
|
|
1380
|
+
contextWindow: number,
|
|
1381
|
+
signal?: AbortSignal,
|
|
1382
|
+
): Promise<Model | undefined> {
|
|
1377
1383
|
const availableModels = this.#host.modelRegistry.getAvailable();
|
|
1378
1384
|
if (availableModels.length === 0) return undefined;
|
|
1379
1385
|
|
|
@@ -1381,7 +1387,7 @@ export class SessionMaintenance {
|
|
|
1381
1387
|
if (!candidate) return undefined;
|
|
1382
1388
|
if (modelsAreEqual(candidate, currentModel)) return undefined;
|
|
1383
1389
|
if (candidate.contextWindow == null || candidate.contextWindow <= contextWindow) return undefined;
|
|
1384
|
-
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, this.#host.sessionId());
|
|
1390
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, this.#host.sessionId(), { signal });
|
|
1385
1391
|
if (!apiKey) return undefined;
|
|
1386
1392
|
return candidate;
|
|
1387
1393
|
}
|
|
@@ -1454,10 +1460,18 @@ export class SessionMaintenance {
|
|
|
1454
1460
|
const candidates =
|
|
1455
1461
|
precomputedCandidates ?? this.#getCompactionModelCandidates(this.#host.modelRegistry.getAvailable());
|
|
1456
1462
|
const telemetry = resolveTelemetry(this.#host.agent.telemetry, this.#host.sessionId());
|
|
1463
|
+
let nativeCompactionFailure: { error: NativeCompactionError; provider: string } | undefined;
|
|
1457
1464
|
|
|
1458
1465
|
for (const candidate of candidates) {
|
|
1459
1466
|
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, this.#host.sessionId());
|
|
1460
1467
|
if (!apiKey) continue;
|
|
1468
|
+
if (
|
|
1469
|
+
nativeCompactionFailure &&
|
|
1470
|
+
(candidate.provider !== nativeCompactionFailure.provider ||
|
|
1471
|
+
!shouldUseProviderNativeCompaction(candidate, preparation.settings))
|
|
1472
|
+
) {
|
|
1473
|
+
throw nativeCompactionFailure.error;
|
|
1474
|
+
}
|
|
1461
1475
|
|
|
1462
1476
|
try {
|
|
1463
1477
|
return await compact(
|
|
@@ -1495,12 +1509,17 @@ export class SessionMaintenance {
|
|
|
1495
1509
|
},
|
|
1496
1510
|
);
|
|
1497
1511
|
} catch (error) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1512
|
+
const id = AIError.classify(error instanceof NativeCompactionError ? error.cause : error, candidate.api);
|
|
1513
|
+
if (AIError.is(id, AIError.Flag.AuthFailed)) continue;
|
|
1514
|
+
if (error instanceof NativeCompactionError) {
|
|
1515
|
+
nativeCompactionFailure ??= { error, provider: candidate.provider };
|
|
1516
|
+
continue;
|
|
1500
1517
|
}
|
|
1518
|
+
throw error;
|
|
1501
1519
|
}
|
|
1502
1520
|
}
|
|
1503
1521
|
|
|
1522
|
+
if (nativeCompactionFailure) throw nativeCompactionFailure.error;
|
|
1504
1523
|
throw this.#buildCompactionAuthError();
|
|
1505
1524
|
}
|
|
1506
1525
|
|
|
@@ -2473,6 +2492,7 @@ export class SessionMaintenance {
|
|
|
2473
2492
|
const telemetry = resolveTelemetry(this.#host.agent.telemetry, this.#host.sessionId());
|
|
2474
2493
|
let compactResult: CompactionResult | undefined;
|
|
2475
2494
|
let lastError: unknown;
|
|
2495
|
+
let nativeCompactionFailure: { error: NativeCompactionError; provider: string } | undefined;
|
|
2476
2496
|
codexCompaction = createCodexCompactionContext({
|
|
2477
2497
|
trigger: "auto",
|
|
2478
2498
|
reason: "context_limit",
|
|
@@ -2486,6 +2506,13 @@ export class SessionMaintenance {
|
|
|
2486
2506
|
const hasMoreCandidates = candidateIndex < candidates.length - 1;
|
|
2487
2507
|
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, this.#host.sessionId());
|
|
2488
2508
|
if (!apiKey) continue;
|
|
2509
|
+
if (
|
|
2510
|
+
nativeCompactionFailure &&
|
|
2511
|
+
(candidate.provider !== nativeCompactionFailure.provider ||
|
|
2512
|
+
!shouldUseProviderNativeCompaction(candidate, preparation.settings))
|
|
2513
|
+
) {
|
|
2514
|
+
throw nativeCompactionFailure.error;
|
|
2515
|
+
}
|
|
2489
2516
|
|
|
2490
2517
|
let attempt = 0;
|
|
2491
2518
|
while (true) {
|
|
@@ -2523,22 +2550,33 @@ export class SessionMaintenance {
|
|
|
2523
2550
|
}
|
|
2524
2551
|
|
|
2525
2552
|
const message = error instanceof Error ? error.message : String(error);
|
|
2526
|
-
const id = AIError.classify(
|
|
2553
|
+
const id = AIError.classify(
|
|
2554
|
+
error instanceof NativeCompactionError ? error.cause : error,
|
|
2555
|
+
candidate.api,
|
|
2556
|
+
);
|
|
2527
2557
|
if (AIError.is(id, AIError.Flag.AuthFailed)) {
|
|
2528
|
-
lastError = this.#buildCompactionAuthError();
|
|
2558
|
+
if (!nativeCompactionFailure) lastError = this.#buildCompactionAuthError();
|
|
2529
2559
|
break;
|
|
2530
2560
|
}
|
|
2531
2561
|
if (AIError.is(id, AIError.Flag.Timeout)) {
|
|
2562
|
+
const nativeFailure = error instanceof NativeCompactionError;
|
|
2532
2563
|
logger.warn(
|
|
2533
|
-
|
|
2534
|
-
? "
|
|
2535
|
-
:
|
|
2564
|
+
nativeFailure
|
|
2565
|
+
? "Provider-native auto-compaction timed out, preserving native failure"
|
|
2566
|
+
: hasMoreCandidates
|
|
2567
|
+
? "Auto-compaction summarization timed out, trying next model"
|
|
2568
|
+
: "Auto-compaction summarization timed out, not retrying same model",
|
|
2536
2569
|
{
|
|
2537
2570
|
error: message,
|
|
2538
2571
|
model: `${candidate.provider}/${candidate.id}`,
|
|
2539
2572
|
},
|
|
2540
2573
|
);
|
|
2541
|
-
|
|
2574
|
+
if (nativeFailure) {
|
|
2575
|
+
nativeCompactionFailure ??= { error, provider: candidate.provider };
|
|
2576
|
+
lastError = nativeCompactionFailure.error;
|
|
2577
|
+
} else {
|
|
2578
|
+
lastError = error;
|
|
2579
|
+
}
|
|
2542
2580
|
break;
|
|
2543
2581
|
}
|
|
2544
2582
|
|
|
@@ -2550,7 +2588,12 @@ export class SessionMaintenance {
|
|
|
2550
2588
|
AIError.is(id, AIError.Flag.Transient) ||
|
|
2551
2589
|
AIError.is(id, AIError.Flag.UsageLimit));
|
|
2552
2590
|
if (!shouldRetry) {
|
|
2553
|
-
|
|
2591
|
+
if (error instanceof NativeCompactionError) {
|
|
2592
|
+
nativeCompactionFailure ??= { error, provider: candidate.provider };
|
|
2593
|
+
lastError = nativeCompactionFailure.error;
|
|
2594
|
+
} else {
|
|
2595
|
+
lastError = error;
|
|
2596
|
+
}
|
|
2554
2597
|
break;
|
|
2555
2598
|
}
|
|
2556
2599
|
|
|
@@ -2560,6 +2603,11 @@ export class SessionMaintenance {
|
|
|
2560
2603
|
// If retry delay is too long (>30s), try next candidate instead of waiting
|
|
2561
2604
|
const maxAcceptableDelayMs = 30_000;
|
|
2562
2605
|
if (delayMs > maxAcceptableDelayMs && hasMoreCandidates) {
|
|
2606
|
+
if (error instanceof NativeCompactionError) {
|
|
2607
|
+
nativeCompactionFailure ??= { error, provider: candidate.provider };
|
|
2608
|
+
lastError = nativeCompactionFailure.error;
|
|
2609
|
+
break;
|
|
2610
|
+
}
|
|
2563
2611
|
logger.warn("Auto-compaction retry delay too long, trying next model", {
|
|
2564
2612
|
delayMs,
|
|
2565
2613
|
retryAfterMs,
|
|
@@ -35,6 +35,7 @@ import { type BuildSessionContextOptions, buildSessionContext, type SessionConte
|
|
|
35
35
|
import {
|
|
36
36
|
type BranchSummaryEntry,
|
|
37
37
|
type CompactionEntry,
|
|
38
|
+
type CredentialPinEntry,
|
|
38
39
|
CURRENT_SESSION_VERSION,
|
|
39
40
|
type CustomEntry,
|
|
40
41
|
type CustomMessageEntry,
|
|
@@ -179,6 +180,7 @@ function isDraftOnlyMetadataEntry(entry: SessionEntry): boolean {
|
|
|
179
180
|
case "thinking_level_change":
|
|
180
181
|
case "service_tier_change":
|
|
181
182
|
case "mode_change":
|
|
183
|
+
case "credential_pin":
|
|
182
184
|
return true;
|
|
183
185
|
default:
|
|
184
186
|
return false;
|
|
@@ -1420,6 +1422,30 @@ export class SessionManager {
|
|
|
1420
1422
|
await this.#rewriteAtomically();
|
|
1421
1423
|
}
|
|
1422
1424
|
|
|
1425
|
+
/** Persist this session's transcript as a newly identified OMP session. */
|
|
1426
|
+
async persistCopy(
|
|
1427
|
+
options?: { sessionDir?: string; suppressBreadcrumb?: boolean },
|
|
1428
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
1429
|
+
): Promise<SessionManager> {
|
|
1430
|
+
const sessionDir = options?.sessionDir ?? SessionManager.getDefaultSessionDir(this.#cwd, undefined, storage);
|
|
1431
|
+
const manager = new SessionManager(this.#cwd, sessionDir, true, storage);
|
|
1432
|
+
manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
|
|
1433
|
+
manager.#resetToNewSession();
|
|
1434
|
+
manager.#sessionName = this.#sessionName;
|
|
1435
|
+
manager.#titleSource = this.#titleSource;
|
|
1436
|
+
manager.#titleUpdatedAt = this.#titleUpdatedAt;
|
|
1437
|
+
manager.#header.title = this.#sessionName;
|
|
1438
|
+
manager.#header.titleSource = this.#titleSource;
|
|
1439
|
+
manager.#additionalDirectories = [...this.#additionalDirectories];
|
|
1440
|
+
manager.#header.additionalDirectories =
|
|
1441
|
+
manager.#additionalDirectories.length > 0 ? [...manager.#additionalDirectories] : undefined;
|
|
1442
|
+
manager.#entries = structuredClone(this.#entries);
|
|
1443
|
+
manager.#index.rebuild(manager.#entries);
|
|
1444
|
+
manager.#forceFileCreation = true;
|
|
1445
|
+
await manager.#rewriteAtomically();
|
|
1446
|
+
return manager;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1423
1449
|
/**
|
|
1424
1450
|
* Stage a synchronous group of entry appends and publish the resulting full
|
|
1425
1451
|
* journal with one atomic replace. A failed publish removes only the staged
|
|
@@ -2038,6 +2064,41 @@ export class SessionManager {
|
|
|
2038
2064
|
return [...names];
|
|
2039
2065
|
}
|
|
2040
2066
|
|
|
2067
|
+
/** Append a credential pin recording which OAuth account served `provider`. */
|
|
2068
|
+
appendCredentialPin(provider: string, hash: string): string {
|
|
2069
|
+
const entry: CredentialPinEntry = {
|
|
2070
|
+
type: "credential_pin",
|
|
2071
|
+
...this.#freshEntryFields(),
|
|
2072
|
+
provider,
|
|
2073
|
+
hash,
|
|
2074
|
+
};
|
|
2075
|
+
this.#recordEntry(entry);
|
|
2076
|
+
return entry.id;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* Latest credential pin per provider on the current branch (root → leaf),
|
|
2081
|
+
* with the effective last-use time of the pinned account.
|
|
2082
|
+
*
|
|
2083
|
+
* Pins are appended only when the serving account *changes*, so a long
|
|
2084
|
+
* session on one account carries a single old pin entry. Any assistant turn
|
|
2085
|
+
* for the same provider after that pin was necessarily served by the pinned
|
|
2086
|
+
* account, so its timestamp advances `lastUsedAt` — a resume seconds after
|
|
2087
|
+
* the last turn seeds a warm sticky instead of a stale one.
|
|
2088
|
+
*/
|
|
2089
|
+
getCredentialPins(): Map<string, { hash: string; lastUsedAt: number }> {
|
|
2090
|
+
const pins = new Map<string, { hash: string; lastUsedAt: number }>();
|
|
2091
|
+
for (const entry of this.getBranch()) {
|
|
2092
|
+
if (entry.type === "credential_pin") {
|
|
2093
|
+
pins.set(entry.provider, { hash: entry.hash, lastUsedAt: new Date(entry.timestamp).getTime() });
|
|
2094
|
+
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
|
2095
|
+
const pin = pins.get(entry.message.provider);
|
|
2096
|
+
if (pin) pin.lastUsedAt = Math.max(pin.lastUsedAt, entry.message.timestamp);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return pins;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2041
2102
|
getLeafId(): string | null {
|
|
2042
2103
|
return this.#index.leafId();
|
|
2043
2104
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Agent, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
3
|
-
import { logger, prompt, stringProperty } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { isRecord, logger, prompt, stringProperty } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { reset as resetCapabilities } from "../capability";
|
|
5
5
|
import type { ModelRegistry } from "../config/model-registry";
|
|
6
6
|
import { formatModelString } from "../config/model-resolver";
|
|
@@ -159,6 +159,18 @@ export function projectMountedMCPXdevGuidance(routes: Iterable<MountedMCPToolRou
|
|
|
159
159
|
|
|
160
160
|
const XDEV_MOUNT_NOTICE_MESSAGE_TYPE = "xdev-mount-notice";
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Structured payload persisted on each {@link XDEV_MOUNT_NOTICE_MESSAGE_TYPE}
|
|
164
|
+
* custom message. Lets a resumed session reconstruct which dynamic devices the
|
|
165
|
+
* model has already been told about, so reconnecting hosts do not re-announce
|
|
166
|
+
* (and re-splice a redundant developer message that busts the provider
|
|
167
|
+
* prompt-cache prefix).
|
|
168
|
+
*/
|
|
169
|
+
interface XdevMountNoticeDetails {
|
|
170
|
+
added: string[];
|
|
171
|
+
removed: string[];
|
|
172
|
+
}
|
|
173
|
+
|
|
162
174
|
/** Owns tool registration, presentation, prompt rebuilding, skills, and permissions. */
|
|
163
175
|
export class SessionTools {
|
|
164
176
|
readonly #host: SessionToolsHost;
|
|
@@ -172,6 +184,14 @@ export class SessionTools {
|
|
|
172
184
|
#rpcHostToolNames = new Set<string>();
|
|
173
185
|
#xdev: XdevState | undefined;
|
|
174
186
|
#pendingXdevMountDelta: { added: Set<string>; removed: Set<string> } | undefined;
|
|
187
|
+
/**
|
|
188
|
+
* Dynamic (`xd://`) devices the model has already been told are mounted.
|
|
189
|
+
* Seeded lazily from persisted history on resume (see
|
|
190
|
+
* {@link #ensureAnnouncedMountsSeeded}) and updated as notices are emitted, so
|
|
191
|
+
* a host reconnect that re-mounts the same device does not re-announce it.
|
|
192
|
+
*/
|
|
193
|
+
#announcedMounts = new Set<string>();
|
|
194
|
+
#announcedMountsSeeded = false;
|
|
175
195
|
#presentationPinnedToolNames: ReadonlySet<string> | undefined;
|
|
176
196
|
#runtimeSelectedToolNames: ReadonlySet<string> | undefined;
|
|
177
197
|
#baseSystemPrompt: string[];
|
|
@@ -674,26 +694,108 @@ export class SessionTools {
|
|
|
674
694
|
this.#host.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev");
|
|
675
695
|
}
|
|
676
696
|
|
|
697
|
+
/**
|
|
698
|
+
* Forget the announced-mount baseline for a replaced transcript. Called when
|
|
699
|
+
* session history is swapped wholesale (`/new`, `switchSession`, `branch`): the
|
|
700
|
+
* previous transcript's persisted notices no longer apply, so the next notice
|
|
701
|
+
* re-seeds from the new history and a device reconnecting into it announces
|
|
702
|
+
* again.
|
|
703
|
+
*
|
|
704
|
+
* The pending delta is deliberately preserved: it holds mounts that are still
|
|
705
|
+
* live but not yet delivered to the model, and `branch()` does not rebuild the
|
|
706
|
+
* base system prompt, so dropping it would leave the branched transcript
|
|
707
|
+
* unaware of a still-mounted device that no later refresh would re-queue.
|
|
708
|
+
*/
|
|
709
|
+
resetAnnouncedMounts(): void {
|
|
710
|
+
this.#announcedMounts.clear();
|
|
711
|
+
this.#announcedMountsSeeded = false;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Seed {@link #announcedMounts} from persisted mount notices the first time a
|
|
716
|
+
* notice is consumed. On resume the in-memory mount set is rebuilt from
|
|
717
|
+
* scratch, so without replaying history every already-announced dynamic device
|
|
718
|
+
* would look freshly mounted and re-announce.
|
|
719
|
+
*/
|
|
720
|
+
#ensureAnnouncedMountsSeeded(): void {
|
|
721
|
+
if (this.#announcedMountsSeeded) return;
|
|
722
|
+
this.#announcedMountsSeeded = true;
|
|
723
|
+
for (const message of this.#host.agent.state.messages) {
|
|
724
|
+
if (message.role !== "custom" || message.customType !== XDEV_MOUNT_NOTICE_MESSAGE_TYPE) continue;
|
|
725
|
+
const details = message.details;
|
|
726
|
+
if (
|
|
727
|
+
isRecord(details) &&
|
|
728
|
+
Array.isArray(details.added) &&
|
|
729
|
+
details.added.every(name => typeof name === "string") &&
|
|
730
|
+
Array.isArray(details.removed) &&
|
|
731
|
+
details.removed.every(name => typeof name === "string")
|
|
732
|
+
) {
|
|
733
|
+
for (const name of details.added) this.#announcedMounts.add(name);
|
|
734
|
+
for (const name of details.removed) this.#announcedMounts.delete(name);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Releases before structured notice details persisted only the rendered
|
|
739
|
+
// prompt. Replay its two stable inventory sections so the first resume
|
|
740
|
+
// after upgrading does not re-announce every dynamic device once.
|
|
741
|
+
if (typeof message.content !== "string") continue;
|
|
742
|
+
let section: "added" | "removed" | undefined;
|
|
743
|
+
for (const line of message.content.split("\n")) {
|
|
744
|
+
if (line === "These tools became available:") {
|
|
745
|
+
section = "added";
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (line.startsWith("No longer mounted")) {
|
|
749
|
+
section = "removed";
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (line === "Configured inline device docs:" || line === "</system-notice>") break;
|
|
753
|
+
if (line.startsWith("Read `xd://<tool>`")) {
|
|
754
|
+
section = undefined;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (!section) continue;
|
|
758
|
+
const match = /^- xd:\/\/(\S+?)(?:\s+—|$)/.exec(line);
|
|
759
|
+
const name = match?.[1];
|
|
760
|
+
if (!name) continue;
|
|
761
|
+
if (section === "added") this.#announcedMounts.add(name);
|
|
762
|
+
else this.#announcedMounts.delete(name);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
677
767
|
/** Consumes the hidden notice for unannounced `xd://` mount changes. */
|
|
678
|
-
takePendingXdevMountNotice(): CustomMessage | undefined {
|
|
768
|
+
takePendingXdevMountNotice(): CustomMessage<XdevMountNoticeDetails> | undefined {
|
|
679
769
|
const pending = this.#pendingXdevMountDelta;
|
|
680
770
|
if (!pending) return undefined;
|
|
681
771
|
this.#pendingXdevMountDelta = undefined;
|
|
772
|
+
this.#ensureAnnouncedMountsSeeded();
|
|
773
|
+
// Only announce a net change relative to what the model already knows (from
|
|
774
|
+
// this session and persisted history): a re-mount of an already-announced
|
|
775
|
+
// device — the common resume/reconnect case — and an unmount for a device
|
|
776
|
+
// it was never told about are both suppressed, keeping the provider prompt
|
|
777
|
+
// cache prefix byte-stable across resumes.
|
|
778
|
+
const addedNames = [...pending.added].filter(name => !this.#announcedMounts.has(name));
|
|
779
|
+
const removedNames = [...pending.removed].filter(name => this.#announcedMounts.has(name));
|
|
780
|
+
if (addedNames.length === 0 && removedNames.length === 0) return undefined;
|
|
682
781
|
const summaries = new Map(this.#xdev ? xdevEntries(this.#xdev).map(entry => [entry.name, entry.summary]) : []);
|
|
683
|
-
const added =
|
|
684
|
-
const removed =
|
|
782
|
+
const added = addedNames.map(name => ({ name, summary: summaries.get(name) ?? "" }));
|
|
783
|
+
const removed = removedNames.map(name => ({ name }));
|
|
685
784
|
const docs = this.#xdev
|
|
686
785
|
? xdevDocsFor(
|
|
687
786
|
this.#xdev,
|
|
688
|
-
|
|
787
|
+
new Set(addedNames),
|
|
689
788
|
this.#host.settings.get("tools.xdevDocs"),
|
|
690
789
|
this.#host.settings.get("tools.xdevInlineDevices"),
|
|
691
790
|
)
|
|
692
791
|
: "";
|
|
792
|
+
for (const name of addedNames) this.#announcedMounts.add(name);
|
|
793
|
+
for (const name of removedNames) this.#announcedMounts.delete(name);
|
|
693
794
|
return {
|
|
694
795
|
role: "custom",
|
|
695
796
|
customType: XDEV_MOUNT_NOTICE_MESSAGE_TYPE,
|
|
696
797
|
content: prompt.render(xdevMountNoticePrompt, { added, removed, docs }),
|
|
798
|
+
details: { added: addedNames, removed: removedNames },
|
|
697
799
|
attribution: "agent",
|
|
698
800
|
display: false,
|
|
699
801
|
timestamp: Date.now(),
|
|
@@ -58,6 +58,7 @@ export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn =
|
|
|
58
58
|
textVerbosity: streamOptions?.textVerbosity ?? textVerbosity,
|
|
59
59
|
streamFirstEventTimeoutMs: streamOptions?.streamFirstEventTimeoutMs ?? streamFirstEventTimeoutMs,
|
|
60
60
|
streamIdleTimeoutMs: streamOptions?.streamIdleTimeoutMs ?? streamIdleTimeoutMs,
|
|
61
|
+
maxRetryDelayMs: streamOptions?.maxRetryDelayMs ?? settings.get("retry.maxDelayMs"),
|
|
61
62
|
maxInFlightRequests: validateProviderMaxInFlightRequests(
|
|
62
63
|
streamOptions?.maxInFlightRequests ?? settings.get("providers.maxInFlightRequests"),
|
|
63
64
|
),
|