@oh-my-pi/pi-coding-agent 17.3.5 → 17.3.8
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 +80 -0
- package/dist/{CHANGELOG-tt9k4jpr.md → CHANGELOG-vr9cckb4.md} +80 -0
- package/dist/cli.js +2993 -3001
- package/dist/docs-index.generated.txt +1 -1
- package/dist/{tool-views.generated-jdfmzwmn.js → tool-views.generated-dd2km5r2.js} +19 -19
- package/dist/types/advisor/advise-tool.d.ts +4 -2
- package/dist/types/cli/auth-broker-cli.d.ts +15 -0
- package/dist/types/cli/stats-cli.d.ts +1 -6
- package/dist/types/cli/update-cli.d.ts +8 -0
- package/dist/types/cli-commands.d.ts +10 -2
- package/dist/types/commands/stats.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/config/settings.d.ts +9 -0
- package/dist/types/extensibility/extensions/runner.d.ts +23 -4
- package/dist/types/extensibility/extensions/types.d.ts +58 -0
- package/dist/types/launch/presence.d.ts +4 -1
- package/dist/types/mcp/oauth-credentials.d.ts +23 -0
- package/dist/types/mcp/oauth-flow.d.ts +11 -0
- package/dist/types/mnemopi/backend.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +25 -3
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/session/agent-session.d.ts +3 -0
- package/dist/types/session/prewalk.d.ts +4 -0
- package/dist/types/session/session-entries.d.ts +0 -1
- package/dist/types/session/session-manager.d.ts +12 -0
- package/dist/types/session/session-stats.d.ts +13 -1
- package/dist/types/session/skill-title-input.d.ts +13 -0
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +1 -0
- package/dist/types/subprocess/worker-client.d.ts +7 -4
- package/dist/types/task/label.d.ts +2 -0
- package/dist/types/task/render.d.ts +2 -0
- package/dist/types/tiny/completion-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +6 -4
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tiny/worker.d.ts +27 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/file-write-fallback.d.ts +124 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +23 -0
- package/dist/types/tools/read-format.d.ts +6 -0
- package/dist/types/tools/read-summary.d.ts +7 -1
- package/dist/types/utils/block-context.d.ts +14 -0
- package/dist/types/utils/fetch-timeout.d.ts +15 -0
- package/dist/types/utils/git.d.ts +25 -1
- package/dist/types/web/search/providers/tinyfish.d.ts +4 -0
- package/package.json +13 -13
- package/src/advisor/advise-tool.ts +5 -3
- package/src/cli/auth-broker-cli.ts +36 -1
- package/src/cli/profile-bootstrap.ts +2 -6
- package/src/cli/stats-cli.ts +6 -72
- package/src/cli/update-cli.ts +63 -11
- package/src/cli-commands.ts +61 -7
- package/src/commands/completions.ts +2 -1
- package/src/commands/stats.ts +7 -4
- package/src/commit/agentic/index.ts +15 -2
- package/src/commit/git/diff.ts +6 -2
- package/src/config/model-resolver.ts +52 -6
- package/src/config/models-config.ts +2 -2
- package/src/config/settings-schema.ts +33 -0
- package/src/config/settings.ts +159 -30
- package/src/discovery/helpers.ts +45 -2
- package/src/discovery/omp-plugins.ts +2 -1
- package/src/discovery/opencode.ts +56 -3
- package/src/edit/hashline/filesystem.ts +9 -3
- package/src/edit/modes/patch.ts +31 -5
- package/src/eval/js/process-entry.ts +4 -4
- package/src/export/html/tool-views.generated.js +19 -19
- package/src/extensibility/extensions/loader.ts +11 -0
- package/src/extensibility/extensions/runner.ts +118 -5
- package/src/extensibility/extensions/types.ts +60 -0
- package/src/extensibility/extensions/wrapper.ts +10 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +47 -0
- package/src/launch/client.ts +9 -4
- package/src/launch/presence.ts +19 -4
- package/src/lsp/defaults.json +1 -1
- package/src/lsp/writethrough.ts +20 -10
- package/src/mcp/manager.ts +41 -20
- package/src/mcp/oauth-credentials.ts +38 -0
- package/src/mcp/oauth-flow.ts +21 -0
- package/src/mcp/tool-bridge.ts +32 -16
- package/src/mnemopi/backend.ts +35 -3
- package/src/modes/components/model-hub.ts +37 -4
- package/src/modes/components/settings-selector.ts +17 -11
- package/src/modes/components/tool-execution.ts +97 -29
- package/src/modes/components/tree-selector.ts +7 -2
- package/src/modes/controllers/event-controller.ts +12 -2
- package/src/modes/controllers/input-controller.ts +64 -27
- package/src/modes/controllers/mcp-command-controller.ts +13 -4
- package/src/modes/interactive-mode.ts +79 -11
- package/src/modes/types.ts +11 -0
- package/src/prompts/system/memory-extraction-system.md +5 -22
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/session/agent-session.ts +52 -6
- package/src/session/messages.ts +6 -0
- package/src/session/prewalk.ts +25 -7
- package/src/session/session-entries.ts +0 -1
- package/src/session/session-maintenance.ts +10 -1
- package/src/session/session-manager.ts +15 -0
- package/src/session/session-stats.ts +24 -3
- package/src/session/settings-stream-fn.ts +7 -0
- package/src/session/skill-title-input.ts +32 -0
- package/src/session/turn-recovery.ts +23 -18
- package/src/slash-commands/builtin-session.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +23 -9
- package/src/subprocess/worker-client.ts +8 -5
- package/src/task/executor.ts +11 -0
- package/src/task/index.ts +2 -0
- package/src/task/label.ts +14 -1
- package/src/task/persisted-revive.ts +13 -0
- package/src/task/render.ts +1 -1
- package/src/task/structured-subagent.ts +5 -2
- package/src/tiny/completion-prompt.ts +16 -0
- package/src/tiny/title-client.ts +15 -6
- package/src/tiny/title-protocol.ts +8 -1
- package/src/tiny/worker.ts +21 -19
- package/src/tools/bash.ts +7 -1
- package/src/tools/file-write-fallback.ts +467 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/path-utils.ts +79 -0
- package/src/tools/read-format.ts +16 -2
- package/src/tools/read-summary.ts +9 -4
- package/src/tools/read.ts +306 -72
- package/src/utils/block-context.ts +15 -1
- package/src/utils/fetch-timeout.ts +33 -0
- package/src/utils/git.ts +54 -11
- package/src/web/search/providers/browser-page.ts +21 -3
- package/src/web/search/providers/tinyfish.ts +26 -0
|
@@ -17,7 +17,7 @@ import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
|
17
17
|
import { materializeImageReferenceLinks, shiftImageMarkers } from "../../modes/image-references";
|
|
18
18
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
19
19
|
import { parseQueueShorthand, splitQueuedMessages } from "../../modes/queue-input";
|
|
20
|
-
import {
|
|
20
|
+
import { buildSkillCommandPrompt, isKnownSkillCommand } from "../../modes/skill-command";
|
|
21
21
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
22
22
|
import manualContinuePrompt from "../../prompts/system/manual-continue.md" with { type: "text" };
|
|
23
23
|
import { USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
@@ -173,6 +173,11 @@ export class InputController {
|
|
|
173
173
|
},
|
|
174
174
|
) {}
|
|
175
175
|
|
|
176
|
+
/** Session-level title starts (user `/skill:` via promptCustomMessage) reuse this UI. */
|
|
177
|
+
notifyTitleGenerationStart(): void {
|
|
178
|
+
this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
|
|
179
|
+
}
|
|
180
|
+
|
|
176
181
|
#enhancedPaste?: EnhancedPasteController;
|
|
177
182
|
#focusedLeftTapListenerInstalled = false;
|
|
178
183
|
#focusedPasteListenerInstalled = false;
|
|
@@ -1168,21 +1173,42 @@ export class InputController {
|
|
|
1168
1173
|
};
|
|
1169
1174
|
|
|
1170
1175
|
this.ctx.editor.clearDraft(text);
|
|
1176
|
+
let optimistic = false;
|
|
1171
1177
|
try {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
if (!handled) {
|
|
1178
|
+
// Build the user-attributed skill message once so the optimistic
|
|
1179
|
+
// transcript row and the dispatched message share content.
|
|
1180
|
+
const built = await buildSkillCommandPrompt(this.ctx, text, streamingBehavior, draftImages);
|
|
1181
|
+
if (!built) {
|
|
1177
1182
|
restoreDraft();
|
|
1178
1183
|
return false;
|
|
1179
1184
|
}
|
|
1185
|
+
// Paint the row before the awaited dispatch so a slow preflight (memory
|
|
1186
|
+
// recall, before_agent_start hooks, auto-thinking, pre-prompt compaction)
|
|
1187
|
+
// does not leave the submission invisible (issue #8895). A streaming
|
|
1188
|
+
// submission queues instead and surfaces its chip, so only paint when the
|
|
1189
|
+
// turn will run fresh.
|
|
1190
|
+
optimistic = !this.ctx.session.isStreaming;
|
|
1191
|
+
if (optimistic) {
|
|
1192
|
+
// Mirror the message promptCustomMessage will build for the turn so the
|
|
1193
|
+
// canonical message_start reconciles this row rather than duplicating it.
|
|
1194
|
+
this.ctx.renderOptimisticSkillMessage(
|
|
1195
|
+
{ role: "custom", ...built.message, timestamp: Date.now() },
|
|
1196
|
+
{ imageLinks: draftImageLinks },
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
await this.ctx.session.promptCustomMessage(built.message, built.options);
|
|
1180
1200
|
return true;
|
|
1181
1201
|
} catch (error) {
|
|
1202
|
+
if (optimistic) this.ctx.clearOptimisticSkillMessage();
|
|
1182
1203
|
restoreDraft();
|
|
1183
1204
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1184
1205
|
return true;
|
|
1185
1206
|
} finally {
|
|
1207
|
+
if (optimistic && this.ctx.optimisticSkillMessagePending) {
|
|
1208
|
+
// Dispatch resolved without a canonical skill message_start (aborted
|
|
1209
|
+
// preflight, or a streaming-race requeue): drop the pending row.
|
|
1210
|
+
this.ctx.clearOptimisticSkillMessage();
|
|
1211
|
+
}
|
|
1186
1212
|
if (this.ctx.session.isStreaming) {
|
|
1187
1213
|
this.ctx.updatePendingMessagesDisplay();
|
|
1188
1214
|
this.ctx.ui.requestRender();
|
|
@@ -1637,6 +1663,38 @@ export class InputController {
|
|
|
1637
1663
|
const focusedNow = this.ctx.ui.getFocused();
|
|
1638
1664
|
const promptTarget =
|
|
1639
1665
|
focusedNow && focusedNow !== this.ctx.editor && hasPasteText(focusedNow) ? focusedNow : null;
|
|
1666
|
+
// #8769: On macOS, Finder `Cmd+C` on an image file puts BOTH a
|
|
1667
|
+
// `public.file-url` representation and a generated 1024x1024
|
|
1668
|
+
// file-icon bitmap on the pasteboard. `arboard::get_image()`
|
|
1669
|
+
// succeeds with the icon, so probing the image representation first
|
|
1670
|
+
// would attach the generic Finder icon instead of the copied
|
|
1671
|
+
// screenshot — a vision model then sees a white `PNG` document
|
|
1672
|
+
// icon. Probe the file URLs before the bitmap and let any that
|
|
1673
|
+
// resolve to a supported image file win over the icon: the
|
|
1674
|
+
// authoritative file bytes are what the user copied.
|
|
1675
|
+
//
|
|
1676
|
+
// #3506: this branch also recovers file-url-only pasteboards
|
|
1677
|
+
// (Finder selections, certain screenshot tools) where
|
|
1678
|
+
// `arboard::get_image()` returns `ContentNotAvailable` and
|
|
1679
|
+
// `pbpaste` is empty. Every image-shaped path routes through
|
|
1680
|
+
// {@link handleImagePathPaste}, matching the bracketed-paste
|
|
1681
|
+
// handler in `CustomEditor.handleInput`; multi-image Finder
|
|
1682
|
+
// selections must not silently drop after the first attach.
|
|
1683
|
+
// `readMacFileUrls` returns an empty list off Darwin, so on every
|
|
1684
|
+
// other platform this is a no-op and the bitmap read below still
|
|
1685
|
+
// runs first.
|
|
1686
|
+
const fileUrls = promptTarget ? [] : ((await this.clipboard.readMacFileUrls?.()) ?? []);
|
|
1687
|
+
let attachedFromFileUrls = false;
|
|
1688
|
+
for (const url of fileUrls) {
|
|
1689
|
+
const candidate = extractImagePathFromText(url);
|
|
1690
|
+
if (!candidate) continue;
|
|
1691
|
+
await this.handleImagePathPaste(candidate);
|
|
1692
|
+
attachedFromFileUrls = true;
|
|
1693
|
+
}
|
|
1694
|
+
if (attachedFromFileUrls) return true;
|
|
1695
|
+
// No usable image-file URL (pure bitmap pasteboard: screenshots,
|
|
1696
|
+
// browser copies, or a non-image Finder selection). Fall to the
|
|
1697
|
+
// image representation.
|
|
1640
1698
|
const image = await this.clipboard.readImage();
|
|
1641
1699
|
if (image) {
|
|
1642
1700
|
if (promptTarget) {
|
|
@@ -1652,27 +1710,6 @@ export class InputController {
|
|
|
1652
1710
|
`Unsupported clipboard image format: ${image.mimeType}`,
|
|
1653
1711
|
);
|
|
1654
1712
|
}
|
|
1655
|
-
// #3506: macOS Finder `Cmd+C` puts only a `public.file-url`
|
|
1656
|
-
// representation on the pasteboard. `pbpaste` (the backing call
|
|
1657
|
-
// for `readText` on Darwin) only surfaces plain text / RTF / EPS,
|
|
1658
|
-
// so it returns empty for file-url-only pasteboards — the smart
|
|
1659
|
-
// text fallback below would dead-end with "Clipboard is empty".
|
|
1660
|
-
// Reach the file URL directly via AppleScript and route every
|
|
1661
|
-
// image-shaped path through {@link handleImagePathPaste}, matching
|
|
1662
|
-
// the bracketed-paste handler in `CustomEditor.handleInput` which
|
|
1663
|
-
// iterates every extracted image path. Multi-image Finder
|
|
1664
|
-
// selections must not silently drop after the first attach.
|
|
1665
|
-
// `readMacFileUrls` returns an empty list off Darwin, so the
|
|
1666
|
-
// check is free on every other platform.
|
|
1667
|
-
const fileUrls = promptTarget ? [] : ((await this.clipboard.readMacFileUrls?.()) ?? []);
|
|
1668
|
-
let attachedFromFileUrls = false;
|
|
1669
|
-
for (const url of fileUrls) {
|
|
1670
|
-
const candidate = extractImagePathFromText(url);
|
|
1671
|
-
if (!candidate) continue;
|
|
1672
|
-
await this.handleImagePathPaste(candidate);
|
|
1673
|
-
attachedFromFileUrls = true;
|
|
1674
|
-
}
|
|
1675
|
-
if (attachedFromFileUrls) return true;
|
|
1676
1713
|
// Smart paste (#1628): no image on the clipboard — fall back to
|
|
1677
1714
|
// pasting its text so the same chord covers both payload kinds.
|
|
1678
1715
|
// Hosts that pre-empt the terminal's own paste (VS Code's
|
|
@@ -1198,11 +1198,20 @@ export class MCPCommandController {
|
|
|
1198
1198
|
connectionError = error as Error;
|
|
1199
1199
|
}
|
|
1200
1200
|
|
|
1201
|
-
// Server connected fine without auth
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1201
|
+
// Server connected fine without auth. A tool-level challenge overrides
|
|
1202
|
+
// this: servers may allow the anonymous handshake yet protect individual
|
|
1203
|
+
// tool calls with `_meta["mcp/www_authenticate"]`. Even without such a
|
|
1204
|
+
// challenge, a clean `initialize` is only weak evidence — per the MCP
|
|
1205
|
+
// spec a server MAY permit unauthenticated `initialize` while requiring a
|
|
1206
|
+
// bearer token for `tools/call`. The user explicitly asked to reauth, so
|
|
1207
|
+
// honor it when the server advertises OAuth discovery metadata; only
|
|
1208
|
+
// refuse when there is genuinely no OAuth endpoint to acquire.
|
|
1204
1209
|
if (connectionSucceeded && !authChallenge) {
|
|
1205
|
-
|
|
1210
|
+
const discovered = "url" in config && config.url ? await discoverOAuthEndpoints(config.url) : null;
|
|
1211
|
+
if (!discovered) {
|
|
1212
|
+
throw new Error("Server connection succeeded without OAuth; reauthorization is not required.");
|
|
1213
|
+
}
|
|
1214
|
+
return discovered;
|
|
1206
1215
|
}
|
|
1207
1216
|
|
|
1208
1217
|
// Tool calls can carry richer RFC 6750/RFC 9728 hints than the original
|
|
@@ -117,7 +117,8 @@ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, buildTuiBuiltinSlashCommands } fr
|
|
|
117
117
|
import { formatDuration } from "../slash-commands/helpers/format";
|
|
118
118
|
import { STTController, type SttState } from "../stt";
|
|
119
119
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
|
|
120
|
-
import {
|
|
120
|
+
import { labelEchoesHandle } from "../task/label";
|
|
121
|
+
import { agentTypeBadge, formatTaskId } from "../task/render";
|
|
121
122
|
import type { ConfiguredThinkingLevel } from "../thinking";
|
|
122
123
|
import { tinyTitleClient } from "../tiny/title-client";
|
|
123
124
|
import type { LspStartupServerInfo } from "../tools";
|
|
@@ -165,7 +166,7 @@ import type { HookInputComponent } from "./components/hook-input";
|
|
|
165
166
|
import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
|
|
166
167
|
import { type PlanReviewAnnotationState, PlanReviewOverlay } from "./components/plan-review-overlay";
|
|
167
168
|
import { StatusLineComponent } from "./components/status-line";
|
|
168
|
-
import type
|
|
169
|
+
import { stopSharedSpinnerTicker, type ToolExecutionHandle } from "./components/tool-execution";
|
|
169
170
|
import { TranscriptContainer } from "./components/transcript-container";
|
|
170
171
|
import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
|
|
171
172
|
import { BtwController } from "./controllers/btw-controller";
|
|
@@ -452,8 +453,8 @@ const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100;
|
|
|
452
453
|
|
|
453
454
|
/**
|
|
454
455
|
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
|
455
|
-
* a bounded set of running-agent rows in the same `Id
|
|
456
|
-
* inline task rows use (muted task preview when no description was given).
|
|
456
|
+
* a bounded set of running-agent rows in the same `Id ⟨role⟩: description` shape
|
|
457
|
+
* the inline task rows use (muted task preview when no description was given).
|
|
457
458
|
* Layout mirrors the Todos HUD exactly: unindented header, then
|
|
458
459
|
* `renderTreeList` rows (dim connectors) shifted right by one space.
|
|
459
460
|
* Only detached background spawns are listed: a sync task call blocks the
|
|
@@ -476,16 +477,23 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n
|
|
|
476
477
|
expanded: true,
|
|
477
478
|
renderItem: session => {
|
|
478
479
|
const displayId = formatTaskId(session.id);
|
|
479
|
-
|
|
480
|
+
const role = session.agent ?? session.progress?.agent;
|
|
481
|
+
const badge = agentTypeBadge(role, theme);
|
|
482
|
+
let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}${badge}`;
|
|
480
483
|
const description = session.description?.trim() || session.progress?.description?.trim();
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
+
const distinctDescription =
|
|
485
|
+
description && !labelEchoesHandle(session.id, description) ? description : undefined;
|
|
486
|
+
if (distinctDescription) {
|
|
487
|
+
const budget = Math.max(
|
|
488
|
+
TRUNCATE_LENGTHS.SHORT,
|
|
489
|
+
columns - visibleWidth(displayId) - visibleWidth(Bun.stripANSI(badge)) - 10,
|
|
490
|
+
);
|
|
491
|
+
line += `${theme.fg("accent", ":")} ${theme.fg("accent", truncateToWidth(replaceTabs(distinctDescription), budget))}`;
|
|
484
492
|
} else {
|
|
485
493
|
// No spawn description: fall back to a muted task preview, same as
|
|
486
494
|
// the inline task rows when a row has no label.
|
|
487
495
|
const taskPreview = session.progress?.task?.trim();
|
|
488
|
-
if (taskPreview) {
|
|
496
|
+
if (taskPreview && !labelEchoesHandle(session.id, taskPreview)) {
|
|
489
497
|
line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`;
|
|
490
498
|
}
|
|
491
499
|
}
|
|
@@ -604,6 +612,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
604
612
|
#pendingSubmissionDispose: (() => void) | undefined;
|
|
605
613
|
#pendingSubmissionPreservesDraft = false;
|
|
606
614
|
#optimisticUserMessageComponents: Component[] = [];
|
|
615
|
+
#optimisticSkillMessageComponents: Component[] = [];
|
|
616
|
+
/** True while an optimistically-rendered `/skill:` row awaits its canonical
|
|
617
|
+
* `message_start`. Read by the event controller to reconcile the row. */
|
|
618
|
+
optimisticSkillMessagePending = false;
|
|
607
619
|
lastSigintTime = 0;
|
|
608
620
|
lastEscapeTime = 0;
|
|
609
621
|
lastLeftTapTime = 0;
|
|
@@ -895,6 +907,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
895
907
|
this.#selectorController = new SelectorController(this);
|
|
896
908
|
this.#focusController = new SessionFocusController(this);
|
|
897
909
|
this.#inputController = new InputController(this);
|
|
910
|
+
this.session.setTitleGenerationStart?.(() => {
|
|
911
|
+
this.#inputController.notifyTitleGenerationStart();
|
|
912
|
+
});
|
|
898
913
|
this.#observerRegistry = new SessionObserverRegistry();
|
|
899
914
|
}
|
|
900
915
|
|
|
@@ -1678,6 +1693,51 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1678
1693
|
this.addMessageToChat(message, options);
|
|
1679
1694
|
}
|
|
1680
1695
|
|
|
1696
|
+
/**
|
|
1697
|
+
* Optimistically render a user-invoked `/skill:` row before its awaited
|
|
1698
|
+
* dispatch so a slow preflight (memory recall, `before_agent_start` hooks,
|
|
1699
|
+
* auto-thinking classification, pre-prompt compaction) does not leave the
|
|
1700
|
+
* submission invisible — normal prompts paint their row via
|
|
1701
|
+
* {@link startPendingSubmission} the same way (issue #8895). The canonical
|
|
1702
|
+
* skill `message_start` swaps this row in place via
|
|
1703
|
+
* {@link reconcileOptimisticSkillMessage}; a failed or bailed dispatch drops
|
|
1704
|
+
* it via {@link clearOptimisticSkillMessage}.
|
|
1705
|
+
*/
|
|
1706
|
+
renderOptimisticSkillMessage(
|
|
1707
|
+
message: AgentMessage,
|
|
1708
|
+
options?: { imageLinks?: readonly (string | undefined)[] },
|
|
1709
|
+
): void {
|
|
1710
|
+
this.clearOptimisticSkillMessage();
|
|
1711
|
+
this.optimisticSkillMessagePending = true;
|
|
1712
|
+
this.#optimisticSkillMessageComponents = this.#captureAddedChatComponents(() => {
|
|
1713
|
+
this.addMessageToChat(message, options);
|
|
1714
|
+
});
|
|
1715
|
+
this.ensureLoadingAnimation();
|
|
1716
|
+
this.ui.requestRender();
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/** Replace the optimistic `/skill:` row with the canonical message emitted by
|
|
1720
|
+
* the session, mirroring {@link replaceOptimisticUserMessage} for skills. */
|
|
1721
|
+
reconcileOptimisticSkillMessage(message: AgentMessage): void {
|
|
1722
|
+
this.optimisticSkillMessagePending = false;
|
|
1723
|
+
for (const component of this.#optimisticSkillMessageComponents) {
|
|
1724
|
+
this.chatContainer.removeChild(component);
|
|
1725
|
+
}
|
|
1726
|
+
this.#optimisticSkillMessageComponents = [];
|
|
1727
|
+
this.addMessageToChat(message);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/** Drop the optimistic `/skill:` row when dispatch fails or bails before the
|
|
1731
|
+
* message reaches the agent (aborted preflight, streaming-race requeue). */
|
|
1732
|
+
clearOptimisticSkillMessage(): void {
|
|
1733
|
+
this.optimisticSkillMessagePending = false;
|
|
1734
|
+
if (this.#optimisticSkillMessageComponents.length === 0) return;
|
|
1735
|
+
for (const component of this.#optimisticSkillMessageComponents) {
|
|
1736
|
+
this.chatContainer.removeChild(component);
|
|
1737
|
+
}
|
|
1738
|
+
this.#optimisticSkillMessageComponents = [];
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1681
1741
|
startPendingSubmission(
|
|
1682
1742
|
input: {
|
|
1683
1743
|
text: string;
|
|
@@ -4149,6 +4209,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4149
4209
|
this.#stopLoadingAnimation(false);
|
|
4150
4210
|
}
|
|
4151
4211
|
this.#cleanupMicAnimation();
|
|
4212
|
+
// Stop the shared tool-spinner ticker: a live block missed by per-component
|
|
4213
|
+
// stopAnimation would otherwise keep an 80ms interval pinning the process.
|
|
4214
|
+
stopSharedSpinnerTicker();
|
|
4152
4215
|
this.#liveCommandController.dispose();
|
|
4153
4216
|
this.#cancelTodoAutoClearTimer();
|
|
4154
4217
|
this.#cancelObserverUiSyncTimer();
|
|
@@ -4239,10 +4302,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4239
4302
|
popTerminalTitle();
|
|
4240
4303
|
this.stop();
|
|
4241
4304
|
|
|
4242
|
-
// Print resumption hint if
|
|
4305
|
+
// Print resumption hint only if the session was actually materialized to
|
|
4306
|
+
// durable storage. Persistence is lazy — a session that exits before its
|
|
4307
|
+
// first assistant message (or dies early to an auth error, a mid-flight
|
|
4308
|
+
// Ctrl+C, or a launch-then-quit) never wrote its JSONL, so the path is
|
|
4309
|
+
// allocated but the file does not exist and `--resume <id>` would fail
|
|
4310
|
+
// (issue #8860).
|
|
4243
4311
|
const sessionId = this.sessionManager.getSessionId();
|
|
4244
4312
|
const sessionFile = this.sessionManager.getSessionFile();
|
|
4245
|
-
if (sessionId && sessionFile) {
|
|
4313
|
+
if (sessionId && sessionFile && this.sessionManager.isSessionOnDisk()) {
|
|
4246
4314
|
process.stderr.write(`\n${chalk.dim(`Resume this session with ${APP_NAME} --resume ${sessionId}`)}\n`);
|
|
4247
4315
|
}
|
|
4248
4316
|
|
package/src/modes/types.ts
CHANGED
|
@@ -313,6 +313,17 @@ export interface InteractiveModeContext {
|
|
|
313
313
|
message: AgentMessage,
|
|
314
314
|
options?: { imageLinks?: readonly (string | undefined)[] },
|
|
315
315
|
): void;
|
|
316
|
+
/** True while an optimistically-rendered `/skill:` row awaits its canonical `message_start`. */
|
|
317
|
+
optimisticSkillMessagePending: boolean;
|
|
318
|
+
/** Optimistically renders a user-invoked `/skill:` row before its awaited dispatch (issue #8895). */
|
|
319
|
+
renderOptimisticSkillMessage(
|
|
320
|
+
message: AgentMessage,
|
|
321
|
+
options?: { imageLinks?: readonly (string | undefined)[] },
|
|
322
|
+
): void;
|
|
323
|
+
/** Swaps the optimistic `/skill:` row for the canonical message emitted by the session. */
|
|
324
|
+
reconcileOptimisticSkillMessage(message: AgentMessage): void;
|
|
325
|
+
/** Drops the optimistic `/skill:` row when dispatch fails or bails before reaching the agent. */
|
|
326
|
+
clearOptimisticSkillMessage(): void;
|
|
316
327
|
isKnownSlashCommand(text: string): boolean;
|
|
317
328
|
addMessageToChat(
|
|
318
329
|
message: AgentMessage,
|
|
@@ -1,26 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
You are a precise long-term memory extractor.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Capture only persistent, reusable information:
|
|
5
|
-
- facts (name, role, employer, config, ports, versions, numbers)
|
|
6
|
-
- explicit instructions to the assistant
|
|
7
|
-
- stable preferences
|
|
8
|
-
- dated events or deadlines
|
|
3
|
+
Extract only persistent information explicitly stated in the user message: stable facts, explicit instructions to the assistant, stable preferences, dates, deadlines, paths, ports, and versions.
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
If nothing qualifies, output exactly: NO_FACTS
|
|
5
|
+
Never infer, explain, invent, or copy information from another message. Ignore greetings, acknowledgements, weather, and one-off plans. When a value is corrected, output only the latest value.
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
Message: My name is Sam, I work at Globex, and I always use 2-space indents.
|
|
15
|
-
Items:
|
|
16
|
-
name is Sam
|
|
17
|
-
works at Globex
|
|
18
|
-
prefers 2-space indents
|
|
7
|
+
Preserve names, numbers, paths, versions, dates, and the original language exactly. Output one short plain-text fact per line with no bullets, numbering, labels, JSON, or commentary.
|
|
19
8
|
|
|
20
|
-
|
|
21
|
-
Message: lol nice weather today, might grab a coffee later
|
|
22
|
-
Items:
|
|
23
|
-
NO_FACTS
|
|
24
|
-
|
|
25
|
-
Message: {text}
|
|
26
|
-
Items:
|
|
9
|
+
If nothing qualifies, output exactly NO_FACTS.
|
|
@@ -96,7 +96,7 @@ Write JSON args as `content` to `xd://<tool>` via `{{toolRefs.write}}`. Invalid
|
|
|
96
96
|
|
|
97
97
|
{{#has tools "think"}}
|
|
98
98
|
§ Scratchpad
|
|
99
|
-
`{{toolRefs.think}}`: private scratchpad; not shown to user.
|
|
99
|
+
`{{toolRefs.think}}`: private scratchpad; not shown to user. MUST use for planning; other tools become callable when it completes.
|
|
100
100
|
{{/has}}
|
|
101
101
|
|
|
102
102
|
§ Tool Policy
|
|
@@ -338,6 +338,7 @@ import { SessionProviderBoundary, type SessionProviderBoundaryHost } from "./ses
|
|
|
338
338
|
import { SessionStatsTracker, type SessionStatsTrackerHost } from "./session-stats";
|
|
339
339
|
import { SessionTools, type SessionToolsHost } from "./session-tools";
|
|
340
340
|
import type { ShakeMode, ShakeResult } from "./shake-types";
|
|
341
|
+
import { skillPromptTitleInput } from "./skill-title-input";
|
|
341
342
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
342
343
|
import { planTurnPersistence, sameMessageContent, sessionMessagePersistenceKey } from "./turn-persistence";
|
|
343
344
|
import { TurnRecovery, type TurnRecoveryHost } from "./turn-recovery";
|
|
@@ -533,6 +534,8 @@ export class AgentSession {
|
|
|
533
534
|
* generation path. Refresh via {@link AgentSession.setTitleSystemPrompt} when
|
|
534
535
|
* the session cwd changes. */
|
|
535
536
|
#titleSystemPrompt: string | undefined;
|
|
537
|
+
#titleGenerationStart: (() => void) | undefined;
|
|
538
|
+
#titleGenerationInFlightFor: string | undefined;
|
|
536
539
|
#titleGenerationAbortController = new AbortController();
|
|
537
540
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
538
541
|
|
|
@@ -1011,8 +1014,13 @@ export class AgentSession {
|
|
|
1011
1014
|
emitNotice: (level, message, source) => this.emitNotice(level, message, source),
|
|
1012
1015
|
setModelTemporary: (model, thinkingLevel, options) => this.setModelTemporary(model, thinkingLevel, options),
|
|
1013
1016
|
setActiveToolsByName: names => this.setActiveToolsByName(names),
|
|
1017
|
+
setActiveToolPresentation: (toolNames, mountedToolNames) =>
|
|
1018
|
+
this.setActiveToolPresentation(toolNames, mountedToolNames),
|
|
1019
|
+
runToolRegistryMutation: mutation => this.runToolRegistryMutation(mutation),
|
|
1014
1020
|
getActiveToolNames: () => this.getActiveToolNames(),
|
|
1015
1021
|
getEnabledToolNames: () => this.getEnabledToolNames(),
|
|
1022
|
+
getSelectedMCPToolNames: () => this.getSelectedMCPToolNames(),
|
|
1023
|
+
getMountedXdevToolNames: () => this.getMountedXdevToolNames(),
|
|
1016
1024
|
hasBuiltInTool: name => this.hasBuiltInTool(name),
|
|
1017
1025
|
getPlanModeState: () => this.getPlanModeState(),
|
|
1018
1026
|
setPlanModeState: state => this.setPlanModeState(state),
|
|
@@ -2361,6 +2369,7 @@ export class AgentSession {
|
|
|
2361
2369
|
assistantMsg.contextSnapshot = {
|
|
2362
2370
|
promptTokens: calculatePromptTokens(assistantMsg.usage),
|
|
2363
2371
|
nonMessageTokens: this.#stats.pendingNonMessageTokens ?? computeNonMessageTokens(this),
|
|
2372
|
+
compactionEpoch: this.#stats.compactionEpoch,
|
|
2364
2373
|
};
|
|
2365
2374
|
}
|
|
2366
2375
|
}
|
|
@@ -5466,11 +5475,20 @@ export class AgentSession {
|
|
|
5466
5475
|
let keywordNotices: CustomMessage[] = [];
|
|
5467
5476
|
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE && message.attribution === "user") {
|
|
5468
5477
|
const details = message.details;
|
|
5478
|
+
let skillName: string | undefined;
|
|
5469
5479
|
let skillArgs = "";
|
|
5470
|
-
if (details && typeof details === "object"
|
|
5471
|
-
|
|
5480
|
+
if (details && typeof details === "object") {
|
|
5481
|
+
if ("name" in details && typeof details.name === "string") skillName = details.name;
|
|
5482
|
+
if ("args" in details && typeof details.args === "string") skillArgs = details.args;
|
|
5472
5483
|
}
|
|
5473
5484
|
keywordNotices = this.#createMagicKeywordNotices(skillArgs);
|
|
5485
|
+
this.maybeStartTitleGeneration(
|
|
5486
|
+
skillPromptTitleInput({
|
|
5487
|
+
name: skillName,
|
|
5488
|
+
args: skillArgs,
|
|
5489
|
+
queueChipText: options?.queueChipText,
|
|
5490
|
+
}),
|
|
5491
|
+
);
|
|
5474
5492
|
}
|
|
5475
5493
|
|
|
5476
5494
|
if (options?.queueOnly) {
|
|
@@ -6538,14 +6556,31 @@ export class AgentSession {
|
|
|
6538
6556
|
this.#extensionRunner?.getCommand(
|
|
6539
6557
|
extensionCommandSpace === -1 ? firstMessage.slice(1) : firstMessage.slice(1, extensionCommandSpace),
|
|
6540
6558
|
) !== undefined;
|
|
6541
|
-
|
|
6559
|
+
const sessionId = this.sessionManager.getSessionId();
|
|
6560
|
+
if (
|
|
6561
|
+
isLocalExtensionCommand ||
|
|
6562
|
+
this.sessionName ||
|
|
6563
|
+
this.#titleGenerationInFlightFor === sessionId ||
|
|
6564
|
+
$env.PI_NO_TITLE ||
|
|
6565
|
+
isLowSignalTitleInput(firstMessage)
|
|
6566
|
+
) {
|
|
6542
6567
|
return;
|
|
6543
6568
|
}
|
|
6544
|
-
|
|
6569
|
+
this.#titleGenerationInFlightFor = sessionId;
|
|
6570
|
+
try {
|
|
6571
|
+
(onStart ?? this.#titleGenerationStart)?.();
|
|
6572
|
+
} catch (error) {
|
|
6573
|
+
if (this.#titleGenerationInFlightFor === sessionId) {
|
|
6574
|
+
this.#titleGenerationInFlightFor = undefined;
|
|
6575
|
+
}
|
|
6576
|
+
throw error;
|
|
6577
|
+
}
|
|
6545
6578
|
this.generateTitle(firstMessage)
|
|
6546
6579
|
.then(async title => {
|
|
6547
|
-
// Re-check after generation so
|
|
6548
|
-
// the first title
|
|
6580
|
+
// Re-check after generation so a later completion cannot replace
|
|
6581
|
+
// the first title, and a request from a replaced session cannot
|
|
6582
|
+
// name the current one.
|
|
6583
|
+
if (this.sessionManager.getSessionId() !== sessionId) return;
|
|
6549
6584
|
if (title && !this.sessionName) {
|
|
6550
6585
|
await this.sessionManager.setSessionName(title, "auto");
|
|
6551
6586
|
}
|
|
@@ -6556,6 +6591,11 @@ export class AgentSession {
|
|
|
6556
6591
|
reason: "uncaught-auto-title-error",
|
|
6557
6592
|
error: err instanceof Error ? err.message : String(err),
|
|
6558
6593
|
});
|
|
6594
|
+
})
|
|
6595
|
+
.finally(() => {
|
|
6596
|
+
if (this.#titleGenerationInFlightFor === sessionId) {
|
|
6597
|
+
this.#titleGenerationInFlightFor = undefined;
|
|
6598
|
+
}
|
|
6559
6599
|
});
|
|
6560
6600
|
}
|
|
6561
6601
|
|
|
@@ -6602,6 +6642,12 @@ export class AgentSession {
|
|
|
6602
6642
|
this.#titleSystemPrompt = prompt;
|
|
6603
6643
|
}
|
|
6604
6644
|
|
|
6645
|
+
/** Install the interactive title-download UI hook. Used when `/skill:` starts
|
|
6646
|
+
* titling from {@link promptCustomMessage} without the input-controller callback. */
|
|
6647
|
+
setTitleGenerationStart(handler: (() => void) | undefined): void {
|
|
6648
|
+
this.#titleGenerationStart = handler;
|
|
6649
|
+
}
|
|
6650
|
+
|
|
6605
6651
|
/**
|
|
6606
6652
|
* Abort current operation and wait for agent to become idle.
|
|
6607
6653
|
*
|
package/src/session/messages.ts
CHANGED
|
@@ -38,6 +38,7 @@ export {
|
|
|
38
38
|
|
|
39
39
|
import type { OutputMeta } from "../tools/output-meta";
|
|
40
40
|
import { formatOutputNotice } from "../tools/output-meta";
|
|
41
|
+
import { titleTextFromSkillPrompt } from "./skill-title-input";
|
|
41
42
|
|
|
42
43
|
export const SKILL_PROMPT_MESSAGE_TYPE = "skill-prompt";
|
|
43
44
|
export const LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE = "lsp-late-diagnostic";
|
|
@@ -163,6 +164,11 @@ function thinkingFromContent(content: unknown): string {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
function titleConversationTurnFromMessage(message: AgentMessage): TitleConversationTurn | undefined {
|
|
167
|
+
if (message.role === "custom") {
|
|
168
|
+
const text = titleTextFromSkillPrompt(message);
|
|
169
|
+
if (!text) return undefined;
|
|
170
|
+
return { role: "user", text };
|
|
171
|
+
}
|
|
166
172
|
if (message.role !== "user" && message.role !== "assistant") return undefined;
|
|
167
173
|
const text = textFromContent(message.content);
|
|
168
174
|
const thinking = message.role === "assistant" ? thinkingFromContent(message.content) : undefined;
|
package/src/session/prewalk.ts
CHANGED
|
@@ -11,6 +11,7 @@ import prewalkChecklistPrompt from "../prompts/system/prewalk-checklist.md" with
|
|
|
11
11
|
import prewalkContinuePrompt from "../prompts/system/prewalk-continue.md" with { type: "text" };
|
|
12
12
|
import prewalkPlanPrompt from "../prompts/system/prewalk-plan.md" with { type: "text" };
|
|
13
13
|
import { type ConfiguredThinkingLevel, prewalkWouldBeNoop } from "../thinking";
|
|
14
|
+
import { isMCPToolName } from "../tools/builtin-names";
|
|
14
15
|
import type { PlanProposalHandler } from "../tools/resolve";
|
|
15
16
|
import { ToolError } from "../tools/tool-errors";
|
|
16
17
|
import type { PlanYolo, Prewalk } from "./agent-session-types";
|
|
@@ -65,8 +66,12 @@ export interface PrewalkCoordinatorHost {
|
|
|
65
66
|
options?: { ephemeral?: boolean },
|
|
66
67
|
): Promise<void>;
|
|
67
68
|
setActiveToolsByName(names: string[]): Promise<void>;
|
|
69
|
+
setActiveToolPresentation(toolNames: string[], mountedToolNames: string[]): Promise<void>;
|
|
70
|
+
runToolRegistryMutation<T>(mutation: () => Promise<T>): Promise<T>;
|
|
68
71
|
getActiveToolNames(): string[];
|
|
69
72
|
getEnabledToolNames(): string[];
|
|
73
|
+
getSelectedMCPToolNames(): string[];
|
|
74
|
+
getMountedXdevToolNames(): string[];
|
|
70
75
|
hasBuiltInTool(name: string): boolean;
|
|
71
76
|
getPlanModeState(): PlanModeState | undefined;
|
|
72
77
|
setPlanModeState(state: PlanModeState | undefined): void;
|
|
@@ -90,7 +95,7 @@ export class PrewalkCoordinator {
|
|
|
90
95
|
#continuePending = false;
|
|
91
96
|
#todoSeen = false;
|
|
92
97
|
#planYolo: PlanYolo | undefined;
|
|
93
|
-
#
|
|
98
|
+
#planYoloPreviousNonMCPPresentation: { enabled: string[]; mounted: string[] } | undefined;
|
|
94
99
|
#planYoloArmed = false;
|
|
95
100
|
|
|
96
101
|
constructor(host: PrewalkCoordinatorHost, options: PrewalkCoordinatorOptions = {}) {
|
|
@@ -247,10 +252,14 @@ export class PrewalkCoordinator {
|
|
|
247
252
|
async armPlanYoloIfNeeded(): Promise<void> {
|
|
248
253
|
if (!this.#planYolo || this.#planYoloArmed) return;
|
|
249
254
|
this.#planYoloArmed = true;
|
|
250
|
-
const
|
|
255
|
+
const previousEnabledTools = this.#host.getEnabledToolNames();
|
|
256
|
+
const previousMountedTools = this.#host.getMountedXdevToolNames();
|
|
251
257
|
const augmentations = this.#host.hasBuiltInTool("write") ? ["write"] : [];
|
|
252
|
-
await this.#host.setActiveToolsByName([...new Set([...
|
|
253
|
-
this.#
|
|
258
|
+
await this.#host.setActiveToolsByName([...new Set([...previousEnabledTools, ...augmentations])]);
|
|
259
|
+
this.#planYoloPreviousNonMCPPresentation = {
|
|
260
|
+
enabled: previousEnabledTools.filter(name => !isMCPToolName(name)),
|
|
261
|
+
mounted: previousMountedTools.filter(name => !isMCPToolName(name)),
|
|
262
|
+
};
|
|
254
263
|
this.#host.setPlanModeState({
|
|
255
264
|
enabled: true,
|
|
256
265
|
planFilePath: this.#host.getPlanReferencePath() || "local://PLAN.md",
|
|
@@ -287,16 +296,25 @@ export class PrewalkCoordinator {
|
|
|
287
296
|
listPlanFiles: () => listPlanFiles({ localProtocolOptions: this.#host.localProtocolOptions() }),
|
|
288
297
|
});
|
|
289
298
|
this.#host.setPlanModeState(undefined);
|
|
290
|
-
const
|
|
299
|
+
const previousPresentation = this.#planYoloPreviousNonMCPPresentation;
|
|
291
300
|
try {
|
|
292
|
-
if (
|
|
301
|
+
if (previousPresentation) {
|
|
302
|
+
await this.#host.runToolRegistryMutation(async () => {
|
|
303
|
+
const liveMCP = this.#host.getSelectedMCPToolNames();
|
|
304
|
+
const liveMountedMCP = this.#host.getMountedXdevToolNames().filter(isMCPToolName);
|
|
305
|
+
await this.#host.setActiveToolPresentation(
|
|
306
|
+
[...new Set([...previousPresentation.enabled, ...liveMCP])],
|
|
307
|
+
[...new Set([...previousPresentation.mounted, ...liveMountedMCP])],
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
293
311
|
} catch (error) {
|
|
294
312
|
this.#host.setPlanModeState(state);
|
|
295
313
|
throw error;
|
|
296
314
|
}
|
|
297
315
|
this.#host.setPlanProposalHandler(null);
|
|
298
316
|
this.#planYolo = undefined;
|
|
299
|
-
this.#
|
|
317
|
+
this.#planYoloPreviousNonMCPPresentation = undefined;
|
|
300
318
|
await this.#host.setModelTemporary(planYolo.target, planYolo.thinkingLevel, { ephemeral: true });
|
|
301
319
|
this.#host.emitNotice(
|
|
302
320
|
"info",
|
|
@@ -477,7 +477,9 @@ export class SessionMaintenance {
|
|
|
477
477
|
const config = this.#withPlanProtection({
|
|
478
478
|
...(opts.config ?? AGGRESSIVE_SHAKE_CONFIG),
|
|
479
479
|
// Skip entries summarized away by the latest compaction — shaking them
|
|
480
|
-
// only churns persisted history with no prompt/cache effect.
|
|
480
|
+
// only churns persisted history with no prompt/cache effect. The cut is
|
|
481
|
+
// unconditional on the wire (see `buildSessionContext`), so a compaction
|
|
482
|
+
// the active model cannot replay still hides its prefix from the prompt.
|
|
481
483
|
keepBoundaryId: latestCompaction?.firstKeptEntryId,
|
|
482
484
|
});
|
|
483
485
|
const regions = collectShakeRegions(branchEntries, config);
|
|
@@ -2729,9 +2731,16 @@ export class SessionMaintenance {
|
|
|
2729
2731
|
}
|
|
2730
2732
|
|
|
2731
2733
|
const retryAfterMs = this.#host.parseRetryAfterMsFromError(message);
|
|
2734
|
+
// An input the summarizer cannot fit is deterministic: the same
|
|
2735
|
+
// prompt fails identically every attempt, so the retry budget is
|
|
2736
|
+
// pure latency and the next candidate (a larger window) is the
|
|
2737
|
+
// only move that can succeed. Overflow therefore vetoes the
|
|
2738
|
+
// transient/usage-limit arms, which a provider blob can trip on
|
|
2739
|
+
// coincidence alone.
|
|
2732
2740
|
const shouldRetry =
|
|
2733
2741
|
retrySettings.enabled &&
|
|
2734
2742
|
attempt < retrySettings.maxRetries &&
|
|
2743
|
+
!AIError.is(id, AIError.Flag.ContextOverflow) &&
|
|
2735
2744
|
(retryAfterMs !== undefined ||
|
|
2736
2745
|
AIError.is(id, AIError.Flag.Transient) ||
|
|
2737
2746
|
AIError.is(id, AIError.Flag.UsageLimit));
|