@oh-my-pi/pi-coding-agent 13.14.2 → 13.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/package.json +10 -8
- package/src/autoresearch/command-initialize.md +34 -0
- package/src/autoresearch/command-resume.md +17 -0
- package/src/autoresearch/contract.ts +332 -0
- package/src/autoresearch/dashboard.ts +447 -0
- package/src/autoresearch/git.ts +243 -0
- package/src/autoresearch/helpers.ts +458 -0
- package/src/autoresearch/index.ts +693 -0
- package/src/autoresearch/prompt.md +227 -0
- package/src/autoresearch/resume-message.md +16 -0
- package/src/autoresearch/state.ts +386 -0
- package/src/autoresearch/tools/init-experiment.ts +310 -0
- package/src/autoresearch/tools/log-experiment.ts +833 -0
- package/src/autoresearch/tools/run-experiment.ts +640 -0
- package/src/autoresearch/types.ts +218 -0
- package/src/cli/args.ts +8 -2
- package/src/cli/initial-message.ts +58 -0
- package/src/config/keybindings.ts +423 -212
- package/src/config/model-registry.ts +1 -0
- package/src/config/model-resolver.ts +57 -9
- package/src/config/settings-schema.ts +38 -10
- package/src/config/settings.ts +1 -4
- package/src/export/html/template.css +43 -13
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.html +1 -0
- package/src/export/html/template.js +107 -0
- package/src/extensibility/extensions/types.ts +31 -8
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/main.ts +44 -44
- package/src/mcp/oauth-discovery.ts +1 -1
- package/src/modes/acp/acp-agent.ts +957 -0
- package/src/modes/acp/acp-event-mapper.ts +531 -0
- package/src/modes/acp/acp-mode.ts +13 -0
- package/src/modes/acp/index.ts +2 -0
- package/src/modes/components/agent-dashboard.ts +5 -4
- package/src/modes/components/custom-editor.ts +53 -51
- package/src/modes/components/extensions/extension-dashboard.ts +2 -1
- package/src/modes/components/history-search.ts +2 -1
- package/src/modes/components/hook-editor.ts +2 -1
- package/src/modes/components/hook-input.ts +8 -7
- package/src/modes/components/hook-selector.ts +15 -10
- package/src/modes/components/keybinding-hints.ts +9 -9
- package/src/modes/components/login-dialog.ts +3 -3
- package/src/modes/components/mcp-add-wizard.ts +2 -1
- package/src/modes/components/model-selector.ts +14 -3
- package/src/modes/components/oauth-selector.ts +2 -1
- package/src/modes/components/session-selector.ts +2 -1
- package/src/modes/components/settings-selector.ts +2 -1
- package/src/modes/components/status-line-segment-editor.ts +2 -1
- package/src/modes/components/tree-selector.ts +3 -2
- package/src/modes/components/user-message-selector.ts +3 -8
- package/src/modes/components/user-message.ts +16 -0
- package/src/modes/controllers/extension-ui-controller.ts +89 -4
- package/src/modes/controllers/input-controller.ts +48 -29
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +17 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/prompt-action-autocomplete.ts +7 -7
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/theme/theme.ts +53 -44
- package/src/modes/types.ts +9 -2
- package/src/modes/utils/hotkeys-markdown.ts +20 -20
- package/src/modes/utils/keybinding-matchers.ts +21 -0
- package/src/modes/utils/ui-helpers.ts +1 -1
- package/src/patch/hashline.ts +139 -127
- package/src/patch/index.ts +77 -59
- package/src/patch/shared.ts +19 -11
- package/src/prompts/tools/hashline.md +43 -116
- package/src/sdk.ts +34 -17
- package/src/session/agent-session.ts +436 -86
- package/src/session/messages.ts +23 -0
- package/src/session/session-manager.ts +97 -31
- package/src/tools/ask.ts +56 -30
- package/src/tools/bash-interceptor.ts +1 -39
- package/src/tools/bash-skill-urls.ts +1 -1
- package/src/tools/browser.ts +1 -1
- package/src/tools/gemini-image.ts +1 -1
- package/src/tools/resolve.ts +1 -1
- package/src/utils/child-process.ts +88 -0
- package/src/utils/image-input.ts +11 -1
- package/src/web/search/providers/codex.ts +10 -3
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
calculateRateLimitBackoffMs,
|
|
47
47
|
getSupportedEfforts,
|
|
48
48
|
isContextOverflow,
|
|
49
|
+
isUsageLimitError,
|
|
49
50
|
modelsAreEqual,
|
|
50
51
|
parseRateLimitReason,
|
|
51
52
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -224,6 +225,8 @@ export interface AgentSessionConfig {
|
|
|
224
225
|
mcpDiscoveryEnabled?: boolean;
|
|
225
226
|
/** MCP tool names to activate for the current session when discovery mode is enabled. */
|
|
226
227
|
initialSelectedMCPToolNames?: string[];
|
|
228
|
+
/** Whether constructor-provided MCP defaults should be persisted immediately. */
|
|
229
|
+
persistInitialMCPToolSelection?: boolean;
|
|
227
230
|
/** MCP server names whose tools should seed discovery-mode sessions whenever those servers are connected. */
|
|
228
231
|
defaultSelectedMCPServerNames?: string[];
|
|
229
232
|
/** MCP tool names that should seed brand-new sessions created from this AgentSession. */
|
|
@@ -363,6 +366,7 @@ export class AgentSession {
|
|
|
363
366
|
#followUpMessages: string[] = [];
|
|
364
367
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
365
368
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
369
|
+
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
366
370
|
#planModeState: PlanModeState | undefined;
|
|
367
371
|
#planReferenceSent = false;
|
|
368
372
|
#planReferencePath = "local://PLAN.md";
|
|
@@ -483,8 +487,11 @@ export class AgentSession {
|
|
|
483
487
|
this.#pruneSelectedMCPToolNames();
|
|
484
488
|
const persistedSelectedMCPToolNames = this.sessionManager.buildSessionContext().selectedMCPToolNames;
|
|
485
489
|
const currentSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
490
|
+
const persistInitialMCPToolSelection =
|
|
491
|
+
config.persistInitialMCPToolSelection ?? this.sessionManager.getBranch().length === 0;
|
|
486
492
|
if (
|
|
487
493
|
this.#mcpDiscoveryEnabled &&
|
|
494
|
+
persistInitialMCPToolSelection &&
|
|
488
495
|
!this.#selectedMCPToolNamesMatch(persistedSelectedMCPToolNames, currentSelectedMCPToolNames)
|
|
489
496
|
) {
|
|
490
497
|
this.sessionManager.appendMCPToolSelection(currentSelectedMCPToolNames);
|
|
@@ -781,7 +788,6 @@ export class AgentSession {
|
|
|
781
788
|
attempt: this.#retryAttempt,
|
|
782
789
|
});
|
|
783
790
|
this.#retryAttempt = 0;
|
|
784
|
-
this.#resolveRetry();
|
|
785
791
|
}
|
|
786
792
|
}
|
|
787
793
|
|
|
@@ -857,6 +863,7 @@ export class AgentSession {
|
|
|
857
863
|
const didRetry = await this.#handleRetryableError(msg);
|
|
858
864
|
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
|
859
865
|
}
|
|
866
|
+
this.#resolveRetry();
|
|
860
867
|
|
|
861
868
|
if (msg.stopReason === "aborted" && this.#checkpointState) {
|
|
862
869
|
this.#checkpointState = undefined;
|
|
@@ -1593,16 +1600,29 @@ export class AgentSession {
|
|
|
1593
1600
|
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
1594
1601
|
}
|
|
1595
1602
|
await this.sessionManager.close();
|
|
1596
|
-
|
|
1597
|
-
state.close();
|
|
1598
|
-
}
|
|
1599
|
-
this.#providerSessionState.clear();
|
|
1603
|
+
this.#closeAllProviderSessions("dispose");
|
|
1600
1604
|
this.#unsubscribePendingActionPush?.();
|
|
1601
1605
|
this.#unsubscribePendingActionPush = undefined;
|
|
1602
1606
|
this.#disconnectFromAgent();
|
|
1603
1607
|
this.#eventListeners = [];
|
|
1604
1608
|
}
|
|
1605
1609
|
|
|
1610
|
+
#closeAllProviderSessions(reason: string): void {
|
|
1611
|
+
for (const [providerKey, state] of this.#providerSessionState) {
|
|
1612
|
+
try {
|
|
1613
|
+
state.close();
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
logger.warn("Failed to close provider session state", {
|
|
1616
|
+
providerKey,
|
|
1617
|
+
reason,
|
|
1618
|
+
error: String(error),
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
this.#providerSessionState.clear();
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1606
1626
|
// =========================================================================
|
|
1607
1627
|
// Read-only State Access
|
|
1608
1628
|
// =========================================================================
|
|
@@ -2566,6 +2586,74 @@ export class AgentSession {
|
|
|
2566
2586
|
});
|
|
2567
2587
|
}
|
|
2568
2588
|
|
|
2589
|
+
#queueHiddenNextTurnMessage(message: CustomMessage, triggerTurn: boolean): void {
|
|
2590
|
+
this.#pendingNextTurnMessages.push(message);
|
|
2591
|
+
if (!triggerTurn) return;
|
|
2592
|
+
const generation = this.#promptGeneration;
|
|
2593
|
+
if (this.#scheduledHiddenNextTurnGeneration === generation) {
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
this.#scheduledHiddenNextTurnGeneration = generation;
|
|
2597
|
+
this.#schedulePostPromptTask(
|
|
2598
|
+
async () => {
|
|
2599
|
+
if (this.#scheduledHiddenNextTurnGeneration === generation) {
|
|
2600
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
2601
|
+
}
|
|
2602
|
+
if (this.#pendingNextTurnMessages.length === 0) {
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
try {
|
|
2606
|
+
await this.#promptQueuedHiddenNextTurnMessages();
|
|
2607
|
+
} catch {
|
|
2608
|
+
// Leave the hidden next-turn messages queued for the next explicit prompt.
|
|
2609
|
+
}
|
|
2610
|
+
},
|
|
2611
|
+
{
|
|
2612
|
+
generation,
|
|
2613
|
+
onSkip: () => {
|
|
2614
|
+
if (this.#scheduledHiddenNextTurnGeneration === generation) {
|
|
2615
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
2616
|
+
}
|
|
2617
|
+
},
|
|
2618
|
+
},
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
async #promptQueuedHiddenNextTurnMessages(): Promise<void> {
|
|
2623
|
+
if (this.#pendingNextTurnMessages.length === 0) {
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
const queuedMessages = [...this.#pendingNextTurnMessages];
|
|
2628
|
+
this.#pendingNextTurnMessages = [];
|
|
2629
|
+
const message = queuedMessages[queuedMessages.length - 1];
|
|
2630
|
+
if (!message) {
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
const prependMessages = queuedMessages.slice(0, -1);
|
|
2635
|
+
const textContent = this.#getCustomMessageTextContent(message);
|
|
2636
|
+
try {
|
|
2637
|
+
await this.#promptWithMessage(message, textContent, {
|
|
2638
|
+
prependMessages,
|
|
2639
|
+
skipPostPromptRecoveryWait: true,
|
|
2640
|
+
});
|
|
2641
|
+
} catch (error) {
|
|
2642
|
+
this.#pendingNextTurnMessages = [...queuedMessages, ...this.#pendingNextTurnMessages];
|
|
2643
|
+
throw error;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
#getCustomMessageTextContent(message: Pick<CustomMessage, "content">): string {
|
|
2648
|
+
if (typeof message.content === "string") {
|
|
2649
|
+
return message.content;
|
|
2650
|
+
}
|
|
2651
|
+
return message.content
|
|
2652
|
+
.filter((content): content is TextContent => content.type === "text")
|
|
2653
|
+
.map(content => content.text)
|
|
2654
|
+
.join("");
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2569
2657
|
/**
|
|
2570
2658
|
* Throw an error if the text is an extension command.
|
|
2571
2659
|
*/
|
|
@@ -2606,7 +2694,7 @@ export class AgentSession {
|
|
|
2606
2694
|
};
|
|
2607
2695
|
if (this.isStreaming) {
|
|
2608
2696
|
if (options?.deliverAs === "nextTurn") {
|
|
2609
|
-
this.#
|
|
2697
|
+
this.#queueHiddenNextTurnMessage(appMessage, options?.triggerTurn ?? false);
|
|
2610
2698
|
return;
|
|
2611
2699
|
}
|
|
2612
2700
|
|
|
@@ -2618,6 +2706,22 @@ export class AgentSession {
|
|
|
2618
2706
|
return;
|
|
2619
2707
|
}
|
|
2620
2708
|
|
|
2709
|
+
if (options?.deliverAs === "nextTurn") {
|
|
2710
|
+
if (options?.triggerTurn) {
|
|
2711
|
+
await this.agent.prompt(appMessage);
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
this.agent.appendMessage(appMessage);
|
|
2715
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
2716
|
+
message.customType,
|
|
2717
|
+
message.content,
|
|
2718
|
+
message.display,
|
|
2719
|
+
message.details,
|
|
2720
|
+
message.attribution ?? "agent",
|
|
2721
|
+
);
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2621
2725
|
if (options?.triggerTurn) {
|
|
2622
2726
|
await this.agent.prompt(appMessage);
|
|
2623
2727
|
return;
|
|
@@ -2685,9 +2789,9 @@ export class AgentSession {
|
|
|
2685
2789
|
return { steering, followUp };
|
|
2686
2790
|
}
|
|
2687
2791
|
|
|
2688
|
-
/** Number of pending messages (includes
|
|
2792
|
+
/** Number of pending messages (includes steering, follow-up, and next-turn messages) */
|
|
2689
2793
|
get queuedMessageCount(): number {
|
|
2690
|
-
return this.#steeringMessages.length + this.#followUpMessages.length;
|
|
2794
|
+
return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
|
|
2691
2795
|
}
|
|
2692
2796
|
|
|
2693
2797
|
/** Get pending messages (read-only) */
|
|
@@ -2829,6 +2933,7 @@ export class AgentSession {
|
|
|
2829
2933
|
async abort(): Promise<void> {
|
|
2830
2934
|
this.abortRetry();
|
|
2831
2935
|
this.#promptGeneration++;
|
|
2936
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
2832
2937
|
this.#resolveTtsrResume();
|
|
2833
2938
|
this.#cancelPostPromptTasks();
|
|
2834
2939
|
this.agent.abort();
|
|
@@ -2870,6 +2975,7 @@ export class AgentSession {
|
|
|
2870
2975
|
this.#disconnectFromAgent();
|
|
2871
2976
|
await this.abort();
|
|
2872
2977
|
this.#asyncJobManager?.cancelAll();
|
|
2978
|
+
this.#closeAllProviderSessions("new session");
|
|
2873
2979
|
this.agent.reset();
|
|
2874
2980
|
await this.sessionManager.flush();
|
|
2875
2981
|
await this.sessionManager.newSession(options);
|
|
@@ -2878,6 +2984,7 @@ export class AgentSession {
|
|
|
2878
2984
|
this.#steeringMessages = [];
|
|
2879
2985
|
this.#followUpMessages = [];
|
|
2880
2986
|
this.#pendingNextTurnMessages = [];
|
|
2987
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
2881
2988
|
|
|
2882
2989
|
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
2883
2990
|
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
@@ -3611,6 +3718,7 @@ export class AgentSession {
|
|
|
3611
3718
|
this.#steeringMessages = [];
|
|
3612
3719
|
this.#followUpMessages = [];
|
|
3613
3720
|
this.#pendingNextTurnMessages = [];
|
|
3721
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
3614
3722
|
this.#todoReminderCount = 0;
|
|
3615
3723
|
|
|
3616
3724
|
// Inject the handoff document as a custom message
|
|
@@ -3991,22 +4099,174 @@ export class AgentSession {
|
|
|
3991
4099
|
}
|
|
3992
4100
|
|
|
3993
4101
|
#closeProviderSessionsForModelSwitch(currentModel: Model, nextModel: Model): void {
|
|
3994
|
-
|
|
4102
|
+
const providerKeys = new Set<string>();
|
|
4103
|
+
if (currentModel.api === "openai-codex-responses" || nextModel.api === "openai-codex-responses") {
|
|
4104
|
+
providerKeys.add("openai-codex-responses");
|
|
4105
|
+
}
|
|
4106
|
+
if (currentModel.api === "openai-responses") {
|
|
4107
|
+
providerKeys.add(`openai-responses:${currentModel.provider}`);
|
|
4108
|
+
}
|
|
4109
|
+
if (nextModel.api === "openai-responses") {
|
|
4110
|
+
providerKeys.add(`openai-responses:${nextModel.provider}`);
|
|
4111
|
+
}
|
|
3995
4112
|
|
|
3996
|
-
const providerKey
|
|
3997
|
-
|
|
3998
|
-
|
|
4113
|
+
for (const providerKey of providerKeys) {
|
|
4114
|
+
const state = this.#providerSessionState.get(providerKey);
|
|
4115
|
+
if (!state) continue;
|
|
3999
4116
|
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4117
|
+
try {
|
|
4118
|
+
state.close();
|
|
4119
|
+
} catch (error) {
|
|
4120
|
+
logger.warn("Failed to close provider session state during model switch", {
|
|
4121
|
+
providerKey,
|
|
4122
|
+
error: String(error),
|
|
4123
|
+
});
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
this.#providerSessionState.delete(providerKey);
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
#normalizeProviderReplayValue(value: unknown): unknown {
|
|
4131
|
+
if (Array.isArray(value)) {
|
|
4132
|
+
return value.map(item => this.#normalizeProviderReplayValue(item));
|
|
4133
|
+
}
|
|
4134
|
+
if (value && typeof value === "object") {
|
|
4135
|
+
return Object.fromEntries(
|
|
4136
|
+
Object.entries(value).map(([key, entryValue]) => [key, this.#normalizeProviderReplayValue(entryValue)]),
|
|
4137
|
+
);
|
|
4007
4138
|
}
|
|
4139
|
+
return value;
|
|
4140
|
+
}
|
|
4008
4141
|
|
|
4009
|
-
|
|
4142
|
+
#normalizeSessionMessageForProviderReplay(message: AgentMessage): unknown {
|
|
4143
|
+
switch (message.role) {
|
|
4144
|
+
case "user":
|
|
4145
|
+
case "developer":
|
|
4146
|
+
return {
|
|
4147
|
+
role: message.role,
|
|
4148
|
+
content: this.#normalizeProviderReplayValue(message.content),
|
|
4149
|
+
providerPayload: message.providerPayload,
|
|
4150
|
+
};
|
|
4151
|
+
case "assistant": {
|
|
4152
|
+
const isResponsesFamilyMessage =
|
|
4153
|
+
message.api === "openai-responses" || message.api === "openai-codex-responses";
|
|
4154
|
+
return {
|
|
4155
|
+
role: message.role,
|
|
4156
|
+
content:
|
|
4157
|
+
isResponsesFamilyMessage && Array.isArray(message.content)
|
|
4158
|
+
? message.content.flatMap(block => {
|
|
4159
|
+
if (block.type === "thinking") {
|
|
4160
|
+
return [];
|
|
4161
|
+
}
|
|
4162
|
+
if (block.type === "toolCall") {
|
|
4163
|
+
return [
|
|
4164
|
+
{
|
|
4165
|
+
type: block.type,
|
|
4166
|
+
id: block.id,
|
|
4167
|
+
name: block.name,
|
|
4168
|
+
arguments: block.arguments,
|
|
4169
|
+
},
|
|
4170
|
+
];
|
|
4171
|
+
}
|
|
4172
|
+
if (block.type === "text") {
|
|
4173
|
+
return [{ type: block.type, text: block.text, textSignature: block.textSignature }];
|
|
4174
|
+
}
|
|
4175
|
+
return [this.#normalizeProviderReplayValue(block)];
|
|
4176
|
+
})
|
|
4177
|
+
: this.#normalizeProviderReplayValue(message.content),
|
|
4178
|
+
api: message.api,
|
|
4179
|
+
provider: message.provider,
|
|
4180
|
+
model: message.model,
|
|
4181
|
+
stopReason: message.stopReason,
|
|
4182
|
+
errorMessage: message.errorMessage,
|
|
4183
|
+
providerPayload: isResponsesFamilyMessage ? undefined : message.providerPayload,
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
case "toolResult":
|
|
4187
|
+
return {
|
|
4188
|
+
role: message.role,
|
|
4189
|
+
toolName: message.toolName,
|
|
4190
|
+
toolCallId: message.toolCallId,
|
|
4191
|
+
isError: message.isError,
|
|
4192
|
+
content: this.#normalizeProviderReplayValue(message.content),
|
|
4193
|
+
};
|
|
4194
|
+
case "bashExecution":
|
|
4195
|
+
return {
|
|
4196
|
+
role: message.role,
|
|
4197
|
+
command: message.command,
|
|
4198
|
+
output: message.output,
|
|
4199
|
+
exitCode: message.exitCode,
|
|
4200
|
+
cancelled: message.cancelled,
|
|
4201
|
+
meta: message.meta
|
|
4202
|
+
? {
|
|
4203
|
+
truncation: this.#normalizeProviderReplayValue(message.meta.truncation),
|
|
4204
|
+
limits: this.#normalizeProviderReplayValue(message.meta.limits),
|
|
4205
|
+
diagnostics: message.meta.diagnostics
|
|
4206
|
+
? this.#normalizeProviderReplayValue({
|
|
4207
|
+
summary: message.meta.diagnostics.summary,
|
|
4208
|
+
messages: message.meta.diagnostics.messages,
|
|
4209
|
+
})
|
|
4210
|
+
: undefined,
|
|
4211
|
+
}
|
|
4212
|
+
: undefined,
|
|
4213
|
+
excludeFromContext: message.excludeFromContext,
|
|
4214
|
+
};
|
|
4215
|
+
case "pythonExecution":
|
|
4216
|
+
return {
|
|
4217
|
+
role: message.role,
|
|
4218
|
+
code: message.code,
|
|
4219
|
+
output: message.output,
|
|
4220
|
+
exitCode: message.exitCode,
|
|
4221
|
+
cancelled: message.cancelled,
|
|
4222
|
+
meta: message.meta
|
|
4223
|
+
? {
|
|
4224
|
+
truncation: this.#normalizeProviderReplayValue(message.meta.truncation),
|
|
4225
|
+
limits: this.#normalizeProviderReplayValue(message.meta.limits),
|
|
4226
|
+
diagnostics: message.meta.diagnostics
|
|
4227
|
+
? this.#normalizeProviderReplayValue({
|
|
4228
|
+
summary: message.meta.diagnostics.summary,
|
|
4229
|
+
messages: message.meta.diagnostics.messages,
|
|
4230
|
+
})
|
|
4231
|
+
: undefined,
|
|
4232
|
+
}
|
|
4233
|
+
: undefined,
|
|
4234
|
+
excludeFromContext: message.excludeFromContext,
|
|
4235
|
+
};
|
|
4236
|
+
case "custom":
|
|
4237
|
+
case "hookMessage":
|
|
4238
|
+
return {
|
|
4239
|
+
role: message.role,
|
|
4240
|
+
customType: message.customType,
|
|
4241
|
+
content: this.#normalizeProviderReplayValue(message.content),
|
|
4242
|
+
};
|
|
4243
|
+
case "branchSummary":
|
|
4244
|
+
return { role: message.role, summary: message.summary };
|
|
4245
|
+
case "compactionSummary":
|
|
4246
|
+
return {
|
|
4247
|
+
role: message.role,
|
|
4248
|
+
summary: message.summary,
|
|
4249
|
+
providerPayload: message.providerPayload,
|
|
4250
|
+
};
|
|
4251
|
+
case "fileMention":
|
|
4252
|
+
return {
|
|
4253
|
+
role: message.role,
|
|
4254
|
+
files: message.files.map(file => ({
|
|
4255
|
+
path: file.path,
|
|
4256
|
+
content: file.content,
|
|
4257
|
+
image: file.image,
|
|
4258
|
+
})),
|
|
4259
|
+
};
|
|
4260
|
+
default:
|
|
4261
|
+
return this.#normalizeProviderReplayValue(message);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
#didSessionMessagesChange(previousMessages: AgentMessage[], nextMessages: AgentMessage[]): boolean {
|
|
4266
|
+
return (
|
|
4267
|
+
JSON.stringify(previousMessages.map(message => this.#normalizeSessionMessageForProviderReplay(message))) !==
|
|
4268
|
+
JSON.stringify(nextMessages.map(message => this.#normalizeSessionMessageForProviderReplay(message)))
|
|
4269
|
+
);
|
|
4010
4270
|
}
|
|
4011
4271
|
|
|
4012
4272
|
#getModelKey(model: Model): string {
|
|
@@ -4278,7 +4538,9 @@ export class AgentSession {
|
|
|
4278
4538
|
const shouldRetry =
|
|
4279
4539
|
retrySettings.enabled &&
|
|
4280
4540
|
attempt < retrySettings.maxRetries &&
|
|
4281
|
-
(retryAfterMs !== undefined ||
|
|
4541
|
+
(retryAfterMs !== undefined ||
|
|
4542
|
+
this.#isTransientErrorMessage(message) ||
|
|
4543
|
+
isUsageLimitError(message));
|
|
4282
4544
|
if (!shouldRetry) {
|
|
4283
4545
|
lastError = error;
|
|
4284
4546
|
break;
|
|
@@ -4476,8 +4738,9 @@ export class AgentSession {
|
|
|
4476
4738
|
// =========================================================================
|
|
4477
4739
|
|
|
4478
4740
|
/**
|
|
4479
|
-
* Check if an error is retryable (
|
|
4741
|
+
* Check if an error is retryable (transient errors or usage limits).
|
|
4480
4742
|
* Context overflow errors are NOT retryable (handled by compaction instead).
|
|
4743
|
+
* Usage-limit errors are retryable because the retry handler performs credential switching.
|
|
4481
4744
|
*/
|
|
4482
4745
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
4483
4746
|
if (message.stopReason !== "error" || !message.errorMessage) return false;
|
|
@@ -4487,20 +4750,17 @@ export class AgentSession {
|
|
|
4487
4750
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
4488
4751
|
|
|
4489
4752
|
const err = message.errorMessage;
|
|
4490
|
-
return this.#
|
|
4753
|
+
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
4491
4754
|
}
|
|
4492
4755
|
|
|
4493
|
-
#
|
|
4494
|
-
// Match: overloaded_error,
|
|
4495
|
-
|
|
4756
|
+
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
4757
|
+
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
|
4758
|
+
// service unavailable, network/connection errors, fetch failed, terminated, retry delay exceeded
|
|
4759
|
+
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall/i.test(
|
|
4496
4760
|
errorMessage,
|
|
4497
4761
|
);
|
|
4498
4762
|
}
|
|
4499
4763
|
|
|
4500
|
-
#isUsageLimitErrorMessage(errorMessage: string): boolean {
|
|
4501
|
-
return /usage.?limit|usage_limit_reached|limit_reached|quota.?exceeded|resource.?exhausted/i.test(errorMessage);
|
|
4502
|
-
}
|
|
4503
|
-
|
|
4504
4764
|
#parseRetryAfterMsFromError(errorMessage: string): number | undefined {
|
|
4505
4765
|
const now = Date.now();
|
|
4506
4766
|
const retryAfterMsMatch = /retry-after-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
|
|
@@ -4582,7 +4842,7 @@ export class AgentSession {
|
|
|
4582
4842
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
4583
4843
|
let delayMs = retrySettings.baseDelayMs * 2 ** (this.#retryAttempt - 1);
|
|
4584
4844
|
|
|
4585
|
-
if (this.model &&
|
|
4845
|
+
if (this.model && isUsageLimitError(errorMessage)) {
|
|
4586
4846
|
const retryAfterMs =
|
|
4587
4847
|
this.#parseRetryAfterMsFromError(errorMessage) ??
|
|
4588
4848
|
calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
@@ -4941,7 +5201,9 @@ export class AgentSession {
|
|
|
4941
5201
|
*/
|
|
4942
5202
|
async switchSession(sessionPath: string): Promise<boolean> {
|
|
4943
5203
|
const previousSessionFile = this.sessionManager.getSessionFile();
|
|
4944
|
-
|
|
5204
|
+
const switchingToDifferentSession = previousSessionFile
|
|
5205
|
+
? path.resolve(previousSessionFile) !== path.resolve(sessionPath)
|
|
5206
|
+
: true;
|
|
4945
5207
|
// Emit session_before_switch event (can be cancelled)
|
|
4946
5208
|
if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
|
|
4947
5209
|
const result = (await this.#extensionRunner.emit({
|
|
@@ -4957,70 +5219,149 @@ export class AgentSession {
|
|
|
4957
5219
|
|
|
4958
5220
|
this.#disconnectFromAgent();
|
|
4959
5221
|
await this.abort();
|
|
4960
|
-
this.#steeringMessages = [];
|
|
4961
|
-
this.#followUpMessages = [];
|
|
4962
|
-
this.#pendingNextTurnMessages = [];
|
|
4963
5222
|
|
|
4964
|
-
// Flush pending writes before switching
|
|
5223
|
+
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
4965
5224
|
await this.sessionManager.flush();
|
|
5225
|
+
const previousSessionState = this.sessionManager.captureState();
|
|
5226
|
+
const previousSessionContext = this.sessionManager.buildSessionContext();
|
|
5227
|
+
// switchSession replaces these arrays wholesale during load/rollback, so retaining
|
|
5228
|
+
// the existing message objects is sufficient and avoids structured-clone failures for
|
|
5229
|
+
// extension/custom metadata that is valid to persist but not cloneable.
|
|
5230
|
+
const previousAgentMessages = [...this.agent.state.messages];
|
|
5231
|
+
const previousSteeringMessages = [...this.#steeringMessages];
|
|
5232
|
+
const previousFollowUpMessages = [...this.#followUpMessages];
|
|
5233
|
+
const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
|
|
5234
|
+
const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
|
|
5235
|
+
const previousModel = this.model;
|
|
5236
|
+
const previousThinkingLevel = this.#thinkingLevel;
|
|
5237
|
+
const previousServiceTier = this.agent.serviceTier;
|
|
5238
|
+
const previousSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
|
|
5239
|
+
const previousTools = [...this.agent.state.tools];
|
|
5240
|
+
const previousBaseSystemPrompt = this.#baseSystemPrompt;
|
|
5241
|
+
const previousSystemPrompt = this.agent.state.systemPrompt;
|
|
5242
|
+
const previousFallbackSelectedMCPToolNames = previousSessionFile
|
|
5243
|
+
? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
|
|
5244
|
+
: undefined;
|
|
4966
5245
|
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
this
|
|
5246
|
+
this.#steeringMessages = [];
|
|
5247
|
+
this.#followUpMessages = [];
|
|
5248
|
+
this.#pendingNextTurnMessages = [];
|
|
5249
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
4970
5250
|
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
5251
|
+
try {
|
|
5252
|
+
await this.sessionManager.setSessionFile(sessionPath);
|
|
5253
|
+
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
4975
5254
|
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
});
|
|
4983
|
-
}
|
|
5255
|
+
const sessionContext = this.sessionManager.buildSessionContext();
|
|
5256
|
+
const didReloadConversationChange =
|
|
5257
|
+
!switchingToDifferentSession &&
|
|
5258
|
+
this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
|
|
5259
|
+
const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
|
|
5260
|
+
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
4984
5261
|
|
|
4985
|
-
|
|
4986
|
-
|
|
5262
|
+
// Emit session_switch event to hooks
|
|
5263
|
+
if (this.#extensionRunner) {
|
|
5264
|
+
await this.#extensionRunner.emit({
|
|
5265
|
+
type: "session_switch",
|
|
5266
|
+
reason: "resume",
|
|
5267
|
+
previousSessionFile,
|
|
5268
|
+
});
|
|
5269
|
+
}
|
|
4987
5270
|
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
if (
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
5271
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
5272
|
+
this.#syncTodoPhasesFromBranch();
|
|
5273
|
+
if (switchingToDifferentSession) {
|
|
5274
|
+
this.#closeAllProviderSessions("session switch");
|
|
5275
|
+
} else if (didReloadConversationChange) {
|
|
5276
|
+
this.#closeAllProviderSessions("session reload");
|
|
5277
|
+
}
|
|
5278
|
+
|
|
5279
|
+
// Restore model if saved
|
|
5280
|
+
const defaultModelStr = sessionContext.models.default;
|
|
5281
|
+
if (defaultModelStr) {
|
|
5282
|
+
const slashIdx = defaultModelStr.indexOf("/");
|
|
5283
|
+
if (slashIdx > 0) {
|
|
5284
|
+
const provider = defaultModelStr.slice(0, slashIdx);
|
|
5285
|
+
const modelId = defaultModelStr.slice(slashIdx + 1);
|
|
5286
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
5287
|
+
const match = availableModels.find(m => m.provider === provider && m.id === modelId);
|
|
5288
|
+
if (match) {
|
|
5289
|
+
const currentModel = this.model;
|
|
5290
|
+
const shouldResetProviderState =
|
|
5291
|
+
switchingToDifferentSession ||
|
|
5292
|
+
(currentModel !== undefined &&
|
|
5293
|
+
(currentModel.provider !== match.provider ||
|
|
5294
|
+
currentModel.id !== match.id ||
|
|
5295
|
+
currentModel.api !== match.api));
|
|
5296
|
+
if (shouldResetProviderState) {
|
|
5297
|
+
this.#setModelWithProviderSessionReset(match);
|
|
5298
|
+
} else {
|
|
5299
|
+
this.agent.setModel(match);
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
4999
5302
|
}
|
|
5000
5303
|
}
|
|
5001
|
-
}
|
|
5002
|
-
|
|
5003
|
-
const hasThinkingEntry = this.sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
5004
|
-
const hasServiceTierEntry = this.sessionManager.getBranch().some(entry => entry.type === "service_tier_change");
|
|
5005
|
-
const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
|
|
5006
5304
|
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
this.
|
|
5013
|
-
|
|
5014
|
-
|
|
5305
|
+
const hasThinkingEntry = this.sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
5306
|
+
const hasServiceTierEntry = this.sessionManager
|
|
5307
|
+
.getBranch()
|
|
5308
|
+
.some(entry => entry.type === "service_tier_change");
|
|
5309
|
+
const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
|
|
5310
|
+
const configuredServiceTier = this.settings.get("serviceTier");
|
|
5311
|
+
const nextThinkingLevel = resolveThinkingLevelForModel(
|
|
5312
|
+
this.model,
|
|
5313
|
+
hasThinkingEntry ? (sessionContext.thinkingLevel as ThinkingLevel | undefined) : defaultThinkingLevel,
|
|
5314
|
+
);
|
|
5315
|
+
this.#thinkingLevel = nextThinkingLevel;
|
|
5316
|
+
this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
|
|
5317
|
+
this.agent.serviceTier = hasServiceTierEntry
|
|
5318
|
+
? sessionContext.serviceTier
|
|
5319
|
+
: configuredServiceTier === "none"
|
|
5320
|
+
? undefined
|
|
5321
|
+
: configuredServiceTier;
|
|
5015
5322
|
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
}
|
|
5019
|
-
this.sessionManager.
|
|
5323
|
+
this.#reconnectToAgent();
|
|
5324
|
+
return true;
|
|
5325
|
+
} catch (error) {
|
|
5326
|
+
this.sessionManager.restoreState(previousSessionState);
|
|
5327
|
+
this.agent.sessionId = previousSessionState.sessionId;
|
|
5328
|
+
let restoreMcpError: unknown;
|
|
5329
|
+
try {
|
|
5330
|
+
await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
|
|
5331
|
+
fallbackSelectedMCPToolNames: previousFallbackSelectedMCPToolNames,
|
|
5332
|
+
});
|
|
5333
|
+
} catch (mcpError) {
|
|
5334
|
+
restoreMcpError = mcpError;
|
|
5335
|
+
logger.warn("Failed to restore MCP selections after switch error", {
|
|
5336
|
+
previousSessionFile,
|
|
5337
|
+
targetSessionFile: sessionPath,
|
|
5338
|
+
error: String(mcpError),
|
|
5339
|
+
});
|
|
5340
|
+
this.#selectedMCPToolNames = new Set(previousSelectedMCPToolNames);
|
|
5341
|
+
this.agent.setTools(previousTools);
|
|
5342
|
+
this.#baseSystemPrompt = previousBaseSystemPrompt;
|
|
5343
|
+
this.agent.setSystemPrompt(previousSystemPrompt);
|
|
5344
|
+
}
|
|
5345
|
+
this.#baseSystemPrompt = previousBaseSystemPrompt;
|
|
5346
|
+
this.agent.setSystemPrompt(previousSystemPrompt);
|
|
5347
|
+
this.agent.replaceMessages(previousAgentMessages);
|
|
5348
|
+
this.#steeringMessages = previousSteeringMessages;
|
|
5349
|
+
this.#followUpMessages = previousFollowUpMessages;
|
|
5350
|
+
this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
|
|
5351
|
+
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
5352
|
+
if (previousModel) {
|
|
5353
|
+
this.agent.setModel(previousModel);
|
|
5354
|
+
}
|
|
5355
|
+
this.#thinkingLevel = previousThinkingLevel;
|
|
5356
|
+
this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
|
|
5357
|
+
this.agent.serviceTier = previousServiceTier;
|
|
5358
|
+
this.#syncTodoPhasesFromBranch();
|
|
5359
|
+
this.#reconnectToAgent();
|
|
5360
|
+
if (restoreMcpError) {
|
|
5361
|
+
throw restoreMcpError;
|
|
5362
|
+
}
|
|
5363
|
+
throw error;
|
|
5020
5364
|
}
|
|
5021
|
-
|
|
5022
|
-
this.#reconnectToAgent();
|
|
5023
|
-
return true;
|
|
5024
5365
|
}
|
|
5025
5366
|
|
|
5026
5367
|
/**
|
|
@@ -5032,7 +5373,10 @@ export class AgentSession {
|
|
|
5032
5373
|
* - selectedText: The text of the selected user message (for editor pre-fill)
|
|
5033
5374
|
* - cancelled: True if a hook cancelled the branch
|
|
5034
5375
|
*/
|
|
5035
|
-
async branch(entryId: string): Promise<{
|
|
5376
|
+
async branch(entryId: string): Promise<{
|
|
5377
|
+
selectedText: string;
|
|
5378
|
+
cancelled: boolean;
|
|
5379
|
+
}> {
|
|
5036
5380
|
const previousSessionFile = this.sessionFile;
|
|
5037
5381
|
const selectedEntry = this.sessionManager.getEntry(entryId);
|
|
5038
5382
|
|
|
@@ -5059,6 +5403,7 @@ export class AgentSession {
|
|
|
5059
5403
|
|
|
5060
5404
|
// Clear pending messages (bound to old session state)
|
|
5061
5405
|
this.#pendingNextTurnMessages = [];
|
|
5406
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
5062
5407
|
|
|
5063
5408
|
// Flush pending writes before branching
|
|
5064
5409
|
await this.sessionManager.flush();
|
|
@@ -5109,7 +5454,12 @@ export class AgentSession {
|
|
|
5109
5454
|
async navigateTree(
|
|
5110
5455
|
targetId: string,
|
|
5111
5456
|
options: { summarize?: boolean; customInstructions?: string } = {},
|
|
5112
|
-
): Promise<{
|
|
5457
|
+
): Promise<{
|
|
5458
|
+
editorText?: string;
|
|
5459
|
+
cancelled: boolean;
|
|
5460
|
+
aborted?: boolean;
|
|
5461
|
+
summaryEntry?: BranchSummaryEntry;
|
|
5462
|
+
}> {
|
|
5113
5463
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
5114
5464
|
|
|
5115
5465
|
// No-op if already at target
|