@oh-my-pi/pi-coding-agent 13.15.2 → 13.16.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 +26 -16
- package/package.json +7 -7
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-registry.ts +215 -57
- package/src/config/settings-schema.ts +27 -0
- package/src/extensibility/extensions/types.ts +6 -1
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/custom-editor.ts +6 -4
- package/src/modes/components/hook-editor.ts +57 -8
- package/src/modes/components/model-selector.ts +48 -29
- package/src/modes/components/settings-defs.ts +10 -1
- package/src/modes/components/settings-selector.ts +92 -5
- package/src/modes/controllers/extension-ui-controller.ts +32 -4
- package/src/modes/controllers/input-controller.ts +22 -9
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +78 -30
- package/src/modes/rpc/rpc-types.ts +9 -1
- package/src/modes/theme/theme.ts +70 -0
- package/src/modes/types.ts +6 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/tools/ask.md +1 -0
- package/src/prompts/tools/hashline.md +20 -5
- package/src/sdk.ts +9 -1
- package/src/session/agent-session.ts +338 -80
- package/src/session/messages.ts +23 -0
- package/src/session/session-manager.ts +65 -0
- package/src/system-prompt.ts +63 -2
- package/src/tools/ask.ts +109 -61
- package/src/tools/ast-edit.ts +2 -16
- package/src/tools/ast-grep.ts +2 -17
- package/src/tools/browser.ts +35 -17
- package/src/tools/grep.ts +4 -17
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/render-utils.ts +27 -0
- package/src/tui/tree-list.ts +51 -22
- package/src/utils/image-input.ts +11 -1
- package/src/web/search/providers/codex.ts +10 -3
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
54
54
|
import type { AsyncJob, AsyncJobManager } from "../async";
|
|
55
55
|
import type { Rule } from "../capability/rule";
|
|
56
|
-
import { MODEL_ROLE_IDS, type ModelRegistry
|
|
56
|
+
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
57
57
|
import { extractExplicitThinkingSelector, parseModelString, resolveModelRoleValue } from "../config/model-resolver";
|
|
58
58
|
import { expandPromptTemplate, type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
|
|
59
59
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
@@ -269,7 +269,7 @@ export interface ModelCycleResult {
|
|
|
269
269
|
export interface RoleModelCycleResult {
|
|
270
270
|
model: Model;
|
|
271
271
|
thinkingLevel: ThinkingLevel | undefined;
|
|
272
|
-
role:
|
|
272
|
+
role: string;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
/** Session statistics for /session command */
|
|
@@ -1600,16 +1600,29 @@ export class AgentSession {
|
|
|
1600
1600
|
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
1601
1601
|
}
|
|
1602
1602
|
await this.sessionManager.close();
|
|
1603
|
-
|
|
1604
|
-
state.close();
|
|
1605
|
-
}
|
|
1606
|
-
this.#providerSessionState.clear();
|
|
1603
|
+
this.#closeAllProviderSessions("dispose");
|
|
1607
1604
|
this.#unsubscribePendingActionPush?.();
|
|
1608
1605
|
this.#unsubscribePendingActionPush = undefined;
|
|
1609
1606
|
this.#disconnectFromAgent();
|
|
1610
1607
|
this.#eventListeners = [];
|
|
1611
1608
|
}
|
|
1612
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
|
+
|
|
1613
1626
|
// =========================================================================
|
|
1614
1627
|
// Read-only State Access
|
|
1615
1628
|
// =========================================================================
|
|
@@ -2026,7 +2039,7 @@ export class AgentSession {
|
|
|
2026
2039
|
);
|
|
2027
2040
|
}
|
|
2028
2041
|
|
|
2029
|
-
resolveRoleModel(role:
|
|
2042
|
+
resolveRoleModel(role: string): Model | undefined {
|
|
2030
2043
|
return this.#resolveRoleModel(role, this.#modelRegistry.getAvailable(), this.model);
|
|
2031
2044
|
}
|
|
2032
2045
|
|
|
@@ -2962,6 +2975,7 @@ export class AgentSession {
|
|
|
2962
2975
|
this.#disconnectFromAgent();
|
|
2963
2976
|
await this.abort();
|
|
2964
2977
|
this.#asyncJobManager?.cancelAll();
|
|
2978
|
+
this.#closeAllProviderSessions("new session");
|
|
2965
2979
|
this.agent.reset();
|
|
2966
2980
|
await this.sessionManager.flush();
|
|
2967
2981
|
await this.sessionManager.newSession(options);
|
|
@@ -3082,7 +3096,7 @@ export class AgentSession {
|
|
|
3082
3096
|
* Validates API key, saves to session and settings.
|
|
3083
3097
|
* @throws Error if no API key available for the model
|
|
3084
3098
|
*/
|
|
3085
|
-
async setModel(model: Model, role:
|
|
3099
|
+
async setModel(model: Model, role: string = "default"): Promise<void> {
|
|
3086
3100
|
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
3087
3101
|
if (!apiKey) {
|
|
3088
3102
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -3136,7 +3150,7 @@ export class AgentSession {
|
|
|
3136
3150
|
* @param options - Optional settings: `temporary` to not persist to settings
|
|
3137
3151
|
*/
|
|
3138
3152
|
async cycleRoleModels(
|
|
3139
|
-
roleOrder: readonly
|
|
3153
|
+
roleOrder: readonly string[],
|
|
3140
3154
|
options?: { temporary?: boolean },
|
|
3141
3155
|
): Promise<RoleModelCycleResult | undefined> {
|
|
3142
3156
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
@@ -3146,7 +3160,7 @@ export class AgentSession {
|
|
|
3146
3160
|
if (!currentModel) return undefined;
|
|
3147
3161
|
const matchPreferences = { usageOrder: this.settings.getStorage()?.getModelUsageOrder() };
|
|
3148
3162
|
const roleModels: Array<{
|
|
3149
|
-
role:
|
|
3163
|
+
role: string;
|
|
3150
3164
|
model: Model;
|
|
3151
3165
|
thinkingLevel?: ThinkingLevel;
|
|
3152
3166
|
explicitThinkingLevel: boolean;
|
|
@@ -3176,9 +3190,10 @@ export class AgentSession {
|
|
|
3176
3190
|
if (roleModels.length <= 1) return undefined;
|
|
3177
3191
|
|
|
3178
3192
|
const lastRole = this.sessionManager.getLastModelChangeRole();
|
|
3179
|
-
let currentIndex = lastRole
|
|
3180
|
-
|
|
3181
|
-
|
|
3193
|
+
let currentIndex = lastRole ? roleModels.findIndex(entry => entry.role === lastRole) : -1;
|
|
3194
|
+
if (currentIndex === -1) {
|
|
3195
|
+
currentIndex = roleModels.findIndex(entry => modelsAreEqual(entry.model, currentModel));
|
|
3196
|
+
}
|
|
3182
3197
|
if (currentIndex === -1) currentIndex = 0;
|
|
3183
3198
|
|
|
3184
3199
|
const nextIndex = (currentIndex + 1) % roleModels.length;
|
|
@@ -4085,29 +4100,181 @@ export class AgentSession {
|
|
|
4085
4100
|
}
|
|
4086
4101
|
|
|
4087
4102
|
#closeProviderSessionsForModelSwitch(currentModel: Model, nextModel: Model): void {
|
|
4088
|
-
|
|
4103
|
+
const providerKeys = new Set<string>();
|
|
4104
|
+
if (currentModel.api === "openai-codex-responses" || nextModel.api === "openai-codex-responses") {
|
|
4105
|
+
providerKeys.add("openai-codex-responses");
|
|
4106
|
+
}
|
|
4107
|
+
if (currentModel.api === "openai-responses") {
|
|
4108
|
+
providerKeys.add(`openai-responses:${currentModel.provider}`);
|
|
4109
|
+
}
|
|
4110
|
+
if (nextModel.api === "openai-responses") {
|
|
4111
|
+
providerKeys.add(`openai-responses:${nextModel.provider}`);
|
|
4112
|
+
}
|
|
4089
4113
|
|
|
4090
|
-
const providerKey
|
|
4091
|
-
|
|
4092
|
-
|
|
4114
|
+
for (const providerKey of providerKeys) {
|
|
4115
|
+
const state = this.#providerSessionState.get(providerKey);
|
|
4116
|
+
if (!state) continue;
|
|
4093
4117
|
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4118
|
+
try {
|
|
4119
|
+
state.close();
|
|
4120
|
+
} catch (error) {
|
|
4121
|
+
logger.warn("Failed to close provider session state during model switch", {
|
|
4122
|
+
providerKey,
|
|
4123
|
+
error: String(error),
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
this.#providerSessionState.delete(providerKey);
|
|
4101
4128
|
}
|
|
4129
|
+
}
|
|
4102
4130
|
|
|
4103
|
-
|
|
4131
|
+
#normalizeProviderReplayValue(value: unknown): unknown {
|
|
4132
|
+
if (Array.isArray(value)) {
|
|
4133
|
+
return value.map(item => this.#normalizeProviderReplayValue(item));
|
|
4134
|
+
}
|
|
4135
|
+
if (value && typeof value === "object") {
|
|
4136
|
+
return Object.fromEntries(
|
|
4137
|
+
Object.entries(value).map(([key, entryValue]) => [key, this.#normalizeProviderReplayValue(entryValue)]),
|
|
4138
|
+
);
|
|
4139
|
+
}
|
|
4140
|
+
return value;
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
#normalizeSessionMessageForProviderReplay(message: AgentMessage): unknown {
|
|
4144
|
+
switch (message.role) {
|
|
4145
|
+
case "user":
|
|
4146
|
+
case "developer":
|
|
4147
|
+
return {
|
|
4148
|
+
role: message.role,
|
|
4149
|
+
content: this.#normalizeProviderReplayValue(message.content),
|
|
4150
|
+
providerPayload: message.providerPayload,
|
|
4151
|
+
};
|
|
4152
|
+
case "assistant": {
|
|
4153
|
+
const isResponsesFamilyMessage =
|
|
4154
|
+
message.api === "openai-responses" || message.api === "openai-codex-responses";
|
|
4155
|
+
return {
|
|
4156
|
+
role: message.role,
|
|
4157
|
+
content:
|
|
4158
|
+
isResponsesFamilyMessage && Array.isArray(message.content)
|
|
4159
|
+
? message.content.flatMap(block => {
|
|
4160
|
+
if (block.type === "thinking") {
|
|
4161
|
+
return [];
|
|
4162
|
+
}
|
|
4163
|
+
if (block.type === "toolCall") {
|
|
4164
|
+
return [
|
|
4165
|
+
{
|
|
4166
|
+
type: block.type,
|
|
4167
|
+
id: block.id,
|
|
4168
|
+
name: block.name,
|
|
4169
|
+
arguments: block.arguments,
|
|
4170
|
+
},
|
|
4171
|
+
];
|
|
4172
|
+
}
|
|
4173
|
+
if (block.type === "text") {
|
|
4174
|
+
return [{ type: block.type, text: block.text, textSignature: block.textSignature }];
|
|
4175
|
+
}
|
|
4176
|
+
return [this.#normalizeProviderReplayValue(block)];
|
|
4177
|
+
})
|
|
4178
|
+
: this.#normalizeProviderReplayValue(message.content),
|
|
4179
|
+
api: message.api,
|
|
4180
|
+
provider: message.provider,
|
|
4181
|
+
model: message.model,
|
|
4182
|
+
stopReason: message.stopReason,
|
|
4183
|
+
errorMessage: message.errorMessage,
|
|
4184
|
+
providerPayload: isResponsesFamilyMessage ? undefined : message.providerPayload,
|
|
4185
|
+
};
|
|
4186
|
+
}
|
|
4187
|
+
case "toolResult":
|
|
4188
|
+
return {
|
|
4189
|
+
role: message.role,
|
|
4190
|
+
toolName: message.toolName,
|
|
4191
|
+
toolCallId: message.toolCallId,
|
|
4192
|
+
isError: message.isError,
|
|
4193
|
+
content: this.#normalizeProviderReplayValue(message.content),
|
|
4194
|
+
};
|
|
4195
|
+
case "bashExecution":
|
|
4196
|
+
return {
|
|
4197
|
+
role: message.role,
|
|
4198
|
+
command: message.command,
|
|
4199
|
+
output: message.output,
|
|
4200
|
+
exitCode: message.exitCode,
|
|
4201
|
+
cancelled: message.cancelled,
|
|
4202
|
+
meta: message.meta
|
|
4203
|
+
? {
|
|
4204
|
+
truncation: this.#normalizeProviderReplayValue(message.meta.truncation),
|
|
4205
|
+
limits: this.#normalizeProviderReplayValue(message.meta.limits),
|
|
4206
|
+
diagnostics: message.meta.diagnostics
|
|
4207
|
+
? this.#normalizeProviderReplayValue({
|
|
4208
|
+
summary: message.meta.diagnostics.summary,
|
|
4209
|
+
messages: message.meta.diagnostics.messages,
|
|
4210
|
+
})
|
|
4211
|
+
: undefined,
|
|
4212
|
+
}
|
|
4213
|
+
: undefined,
|
|
4214
|
+
excludeFromContext: message.excludeFromContext,
|
|
4215
|
+
};
|
|
4216
|
+
case "pythonExecution":
|
|
4217
|
+
return {
|
|
4218
|
+
role: message.role,
|
|
4219
|
+
code: message.code,
|
|
4220
|
+
output: message.output,
|
|
4221
|
+
exitCode: message.exitCode,
|
|
4222
|
+
cancelled: message.cancelled,
|
|
4223
|
+
meta: message.meta
|
|
4224
|
+
? {
|
|
4225
|
+
truncation: this.#normalizeProviderReplayValue(message.meta.truncation),
|
|
4226
|
+
limits: this.#normalizeProviderReplayValue(message.meta.limits),
|
|
4227
|
+
diagnostics: message.meta.diagnostics
|
|
4228
|
+
? this.#normalizeProviderReplayValue({
|
|
4229
|
+
summary: message.meta.diagnostics.summary,
|
|
4230
|
+
messages: message.meta.diagnostics.messages,
|
|
4231
|
+
})
|
|
4232
|
+
: undefined,
|
|
4233
|
+
}
|
|
4234
|
+
: undefined,
|
|
4235
|
+
excludeFromContext: message.excludeFromContext,
|
|
4236
|
+
};
|
|
4237
|
+
case "custom":
|
|
4238
|
+
case "hookMessage":
|
|
4239
|
+
return {
|
|
4240
|
+
role: message.role,
|
|
4241
|
+
customType: message.customType,
|
|
4242
|
+
content: this.#normalizeProviderReplayValue(message.content),
|
|
4243
|
+
};
|
|
4244
|
+
case "branchSummary":
|
|
4245
|
+
return { role: message.role, summary: message.summary };
|
|
4246
|
+
case "compactionSummary":
|
|
4247
|
+
return {
|
|
4248
|
+
role: message.role,
|
|
4249
|
+
summary: message.summary,
|
|
4250
|
+
providerPayload: message.providerPayload,
|
|
4251
|
+
};
|
|
4252
|
+
case "fileMention":
|
|
4253
|
+
return {
|
|
4254
|
+
role: message.role,
|
|
4255
|
+
files: message.files.map(file => ({
|
|
4256
|
+
path: file.path,
|
|
4257
|
+
content: file.content,
|
|
4258
|
+
image: file.image,
|
|
4259
|
+
})),
|
|
4260
|
+
};
|
|
4261
|
+
default:
|
|
4262
|
+
return this.#normalizeProviderReplayValue(message);
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4266
|
+
#didSessionMessagesChange(previousMessages: AgentMessage[], nextMessages: AgentMessage[]): boolean {
|
|
4267
|
+
return (
|
|
4268
|
+
JSON.stringify(previousMessages.map(message => this.#normalizeSessionMessageForProviderReplay(message))) !==
|
|
4269
|
+
JSON.stringify(nextMessages.map(message => this.#normalizeSessionMessageForProviderReplay(message)))
|
|
4270
|
+
);
|
|
4104
4271
|
}
|
|
4105
4272
|
|
|
4106
4273
|
#getModelKey(model: Model): string {
|
|
4107
4274
|
return `${model.provider}/${model.id}`;
|
|
4108
4275
|
}
|
|
4109
4276
|
|
|
4110
|
-
#formatRoleModelValue(role:
|
|
4277
|
+
#formatRoleModelValue(role: string, model: Model): string {
|
|
4111
4278
|
const modelKey = `${model.provider}/${model.id}`;
|
|
4112
4279
|
const existingRoleValue = this.settings.getModelRole(role);
|
|
4113
4280
|
if (!existingRoleValue) return modelKey;
|
|
@@ -4129,7 +4296,7 @@ export class AgentSession {
|
|
|
4129
4296
|
return availableModels.find(m => m.provider === currentModel.provider && m.id === configuredTarget);
|
|
4130
4297
|
}
|
|
4131
4298
|
|
|
4132
|
-
#resolveRoleModel(role:
|
|
4299
|
+
#resolveRoleModel(role: string, availableModels: Model[], currentModel: Model | undefined): Model | undefined {
|
|
4133
4300
|
const roleModelStr =
|
|
4134
4301
|
role === "default"
|
|
4135
4302
|
? (this.settings.getModelRole("default") ??
|
|
@@ -5035,7 +5202,9 @@ export class AgentSession {
|
|
|
5035
5202
|
*/
|
|
5036
5203
|
async switchSession(sessionPath: string): Promise<boolean> {
|
|
5037
5204
|
const previousSessionFile = this.sessionManager.getSessionFile();
|
|
5038
|
-
|
|
5205
|
+
const switchingToDifferentSession = previousSessionFile
|
|
5206
|
+
? path.resolve(previousSessionFile) !== path.resolve(sessionPath)
|
|
5207
|
+
: true;
|
|
5039
5208
|
// Emit session_before_switch event (can be cancelled)
|
|
5040
5209
|
if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
|
|
5041
5210
|
const result = (await this.#extensionRunner.emit({
|
|
@@ -5051,68 +5220,149 @@ export class AgentSession {
|
|
|
5051
5220
|
|
|
5052
5221
|
this.#disconnectFromAgent();
|
|
5053
5222
|
await this.abort();
|
|
5223
|
+
|
|
5224
|
+
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
5225
|
+
await this.sessionManager.flush();
|
|
5226
|
+
const previousSessionState = this.sessionManager.captureState();
|
|
5227
|
+
const previousSessionContext = this.sessionManager.buildSessionContext();
|
|
5228
|
+
// switchSession replaces these arrays wholesale during load/rollback, so retaining
|
|
5229
|
+
// the existing message objects is sufficient and avoids structured-clone failures for
|
|
5230
|
+
// extension/custom metadata that is valid to persist but not cloneable.
|
|
5231
|
+
const previousAgentMessages = [...this.agent.state.messages];
|
|
5232
|
+
const previousSteeringMessages = [...this.#steeringMessages];
|
|
5233
|
+
const previousFollowUpMessages = [...this.#followUpMessages];
|
|
5234
|
+
const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
|
|
5235
|
+
const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
|
|
5236
|
+
const previousModel = this.model;
|
|
5237
|
+
const previousThinkingLevel = this.#thinkingLevel;
|
|
5238
|
+
const previousServiceTier = this.agent.serviceTier;
|
|
5239
|
+
const previousSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
|
|
5240
|
+
const previousTools = [...this.agent.state.tools];
|
|
5241
|
+
const previousBaseSystemPrompt = this.#baseSystemPrompt;
|
|
5242
|
+
const previousSystemPrompt = this.agent.state.systemPrompt;
|
|
5243
|
+
const previousFallbackSelectedMCPToolNames = previousSessionFile
|
|
5244
|
+
? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
|
|
5245
|
+
: undefined;
|
|
5246
|
+
|
|
5054
5247
|
this.#steeringMessages = [];
|
|
5055
5248
|
this.#followUpMessages = [];
|
|
5056
5249
|
this.#pendingNextTurnMessages = [];
|
|
5057
5250
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
5058
5251
|
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
// Set new session
|
|
5063
|
-
await this.sessionManager.setSessionFile(sessionPath);
|
|
5064
|
-
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
5065
|
-
|
|
5066
|
-
// Reload messages
|
|
5067
|
-
const sessionContext = this.sessionManager.buildSessionContext();
|
|
5068
|
-
const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
|
|
5069
|
-
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
5252
|
+
try {
|
|
5253
|
+
await this.sessionManager.setSessionFile(sessionPath);
|
|
5254
|
+
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
5070
5255
|
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
});
|
|
5078
|
-
}
|
|
5256
|
+
const sessionContext = this.sessionManager.buildSessionContext();
|
|
5257
|
+
const didReloadConversationChange =
|
|
5258
|
+
!switchingToDifferentSession &&
|
|
5259
|
+
this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
|
|
5260
|
+
const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
|
|
5261
|
+
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
5079
5262
|
|
|
5080
|
-
|
|
5081
|
-
|
|
5263
|
+
// Emit session_switch event to hooks
|
|
5264
|
+
if (this.#extensionRunner) {
|
|
5265
|
+
await this.#extensionRunner.emit({
|
|
5266
|
+
type: "session_switch",
|
|
5267
|
+
reason: "resume",
|
|
5268
|
+
previousSessionFile,
|
|
5269
|
+
});
|
|
5270
|
+
}
|
|
5082
5271
|
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
if (
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5272
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
5273
|
+
this.#syncTodoPhasesFromBranch();
|
|
5274
|
+
if (switchingToDifferentSession) {
|
|
5275
|
+
this.#closeAllProviderSessions("session switch");
|
|
5276
|
+
} else if (didReloadConversationChange) {
|
|
5277
|
+
this.#closeAllProviderSessions("session reload");
|
|
5278
|
+
}
|
|
5279
|
+
|
|
5280
|
+
// Restore model if saved
|
|
5281
|
+
const defaultModelStr = sessionContext.models.default;
|
|
5282
|
+
if (defaultModelStr) {
|
|
5283
|
+
const slashIdx = defaultModelStr.indexOf("/");
|
|
5284
|
+
if (slashIdx > 0) {
|
|
5285
|
+
const provider = defaultModelStr.slice(0, slashIdx);
|
|
5286
|
+
const modelId = defaultModelStr.slice(slashIdx + 1);
|
|
5287
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
5288
|
+
const match = availableModels.find(m => m.provider === provider && m.id === modelId);
|
|
5289
|
+
if (match) {
|
|
5290
|
+
const currentModel = this.model;
|
|
5291
|
+
const shouldResetProviderState =
|
|
5292
|
+
switchingToDifferentSession ||
|
|
5293
|
+
(currentModel !== undefined &&
|
|
5294
|
+
(currentModel.provider !== match.provider ||
|
|
5295
|
+
currentModel.id !== match.id ||
|
|
5296
|
+
currentModel.api !== match.api));
|
|
5297
|
+
if (shouldResetProviderState) {
|
|
5298
|
+
this.#setModelWithProviderSessionReset(match);
|
|
5299
|
+
} else {
|
|
5300
|
+
this.agent.setModel(match);
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5094
5303
|
}
|
|
5095
5304
|
}
|
|
5096
|
-
}
|
|
5097
5305
|
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
this.
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
?
|
|
5112
|
-
: configuredServiceTier
|
|
5306
|
+
const hasThinkingEntry = this.sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
5307
|
+
const hasServiceTierEntry = this.sessionManager
|
|
5308
|
+
.getBranch()
|
|
5309
|
+
.some(entry => entry.type === "service_tier_change");
|
|
5310
|
+
const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
|
|
5311
|
+
const configuredServiceTier = this.settings.get("serviceTier");
|
|
5312
|
+
const nextThinkingLevel = resolveThinkingLevelForModel(
|
|
5313
|
+
this.model,
|
|
5314
|
+
hasThinkingEntry ? (sessionContext.thinkingLevel as ThinkingLevel | undefined) : defaultThinkingLevel,
|
|
5315
|
+
);
|
|
5316
|
+
this.#thinkingLevel = nextThinkingLevel;
|
|
5317
|
+
this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
|
|
5318
|
+
this.agent.serviceTier = hasServiceTierEntry
|
|
5319
|
+
? sessionContext.serviceTier
|
|
5320
|
+
: configuredServiceTier === "none"
|
|
5321
|
+
? undefined
|
|
5322
|
+
: configuredServiceTier;
|
|
5113
5323
|
|
|
5114
|
-
|
|
5115
|
-
|
|
5324
|
+
this.#reconnectToAgent();
|
|
5325
|
+
return true;
|
|
5326
|
+
} catch (error) {
|
|
5327
|
+
this.sessionManager.restoreState(previousSessionState);
|
|
5328
|
+
this.agent.sessionId = previousSessionState.sessionId;
|
|
5329
|
+
let restoreMcpError: unknown;
|
|
5330
|
+
try {
|
|
5331
|
+
await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
|
|
5332
|
+
fallbackSelectedMCPToolNames: previousFallbackSelectedMCPToolNames,
|
|
5333
|
+
});
|
|
5334
|
+
} catch (mcpError) {
|
|
5335
|
+
restoreMcpError = mcpError;
|
|
5336
|
+
logger.warn("Failed to restore MCP selections after switch error", {
|
|
5337
|
+
previousSessionFile,
|
|
5338
|
+
targetSessionFile: sessionPath,
|
|
5339
|
+
error: String(mcpError),
|
|
5340
|
+
});
|
|
5341
|
+
this.#selectedMCPToolNames = new Set(previousSelectedMCPToolNames);
|
|
5342
|
+
this.agent.setTools(previousTools);
|
|
5343
|
+
this.#baseSystemPrompt = previousBaseSystemPrompt;
|
|
5344
|
+
this.agent.setSystemPrompt(previousSystemPrompt);
|
|
5345
|
+
}
|
|
5346
|
+
this.#baseSystemPrompt = previousBaseSystemPrompt;
|
|
5347
|
+
this.agent.setSystemPrompt(previousSystemPrompt);
|
|
5348
|
+
this.agent.replaceMessages(previousAgentMessages);
|
|
5349
|
+
this.#steeringMessages = previousSteeringMessages;
|
|
5350
|
+
this.#followUpMessages = previousFollowUpMessages;
|
|
5351
|
+
this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
|
|
5352
|
+
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
5353
|
+
if (previousModel) {
|
|
5354
|
+
this.agent.setModel(previousModel);
|
|
5355
|
+
}
|
|
5356
|
+
this.#thinkingLevel = previousThinkingLevel;
|
|
5357
|
+
this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
|
|
5358
|
+
this.agent.serviceTier = previousServiceTier;
|
|
5359
|
+
this.#syncTodoPhasesFromBranch();
|
|
5360
|
+
this.#reconnectToAgent();
|
|
5361
|
+
if (restoreMcpError) {
|
|
5362
|
+
throw restoreMcpError;
|
|
5363
|
+
}
|
|
5364
|
+
throw error;
|
|
5365
|
+
}
|
|
5116
5366
|
}
|
|
5117
5367
|
|
|
5118
5368
|
/**
|
|
@@ -5124,7 +5374,10 @@ export class AgentSession {
|
|
|
5124
5374
|
* - selectedText: The text of the selected user message (for editor pre-fill)
|
|
5125
5375
|
* - cancelled: True if a hook cancelled the branch
|
|
5126
5376
|
*/
|
|
5127
|
-
async branch(entryId: string): Promise<{
|
|
5377
|
+
async branch(entryId: string): Promise<{
|
|
5378
|
+
selectedText: string;
|
|
5379
|
+
cancelled: boolean;
|
|
5380
|
+
}> {
|
|
5128
5381
|
const previousSessionFile = this.sessionFile;
|
|
5129
5382
|
const selectedEntry = this.sessionManager.getEntry(entryId);
|
|
5130
5383
|
|
|
@@ -5202,7 +5455,12 @@ export class AgentSession {
|
|
|
5202
5455
|
async navigateTree(
|
|
5203
5456
|
targetId: string,
|
|
5204
5457
|
options: { summarize?: boolean; customInstructions?: string } = {},
|
|
5205
|
-
): Promise<{
|
|
5458
|
+
): Promise<{
|
|
5459
|
+
editorText?: string;
|
|
5460
|
+
cancelled: boolean;
|
|
5461
|
+
aborted?: boolean;
|
|
5462
|
+
summaryEntry?: BranchSummaryEntry;
|
|
5463
|
+
}> {
|
|
5206
5464
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
5207
5465
|
|
|
5208
5466
|
// No-op if already at target
|
package/src/session/messages.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
8
8
|
import type {
|
|
9
|
+
AssistantMessage,
|
|
9
10
|
ImageContent,
|
|
10
11
|
Message,
|
|
11
12
|
MessageAttribution,
|
|
@@ -213,6 +214,28 @@ export function createCompactionSummaryMessage(
|
|
|
213
214
|
};
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
export function sanitizeRehydratedOpenAIResponsesAssistantMessage(message: AssistantMessage): AssistantMessage {
|
|
218
|
+
if (message.providerPayload?.type !== "openaiResponsesHistory") {
|
|
219
|
+
return message;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let didSanitize = false;
|
|
223
|
+
const sanitizedContent = message.content.map(block => {
|
|
224
|
+
if (block.type !== "thinking" || block.thinkingSignature === undefined) {
|
|
225
|
+
return block;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
didSanitize = true;
|
|
229
|
+
return { ...block, thinkingSignature: undefined };
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!didSanitize) {
|
|
233
|
+
return message;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { ...message, content: sanitizedContent };
|
|
237
|
+
}
|
|
238
|
+
|
|
216
239
|
/** Convert CustomMessageEntry to AgentMessage format */
|
|
217
240
|
export function createCustomMessage(
|
|
218
241
|
customType: string,
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
type FileMentionMessage,
|
|
47
47
|
type HookMessage,
|
|
48
48
|
type PythonExecutionMessage,
|
|
49
|
+
sanitizeRehydratedOpenAIResponsesAssistantMessage,
|
|
49
50
|
} from "./messages";
|
|
50
51
|
import type { SessionStorage, SessionStorageWriter } from "./session-storage";
|
|
51
52
|
import { FileSessionStorage, MemorySessionStorage } from "./session-storage";
|
|
@@ -1374,6 +1375,15 @@ export async function resolveResumableSession(
|
|
|
1374
1375
|
|
|
1375
1376
|
return { session: globalMatch, scope: "global" };
|
|
1376
1377
|
}
|
|
1378
|
+
interface SessionManagerStateSnapshot {
|
|
1379
|
+
sessionId: string;
|
|
1380
|
+
sessionName: string | undefined;
|
|
1381
|
+
sessionFile: string | undefined;
|
|
1382
|
+
flushed: boolean;
|
|
1383
|
+
needsFullRewriteOnNextPersist: boolean;
|
|
1384
|
+
fileEntries: FileEntry[];
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1377
1387
|
export class SessionManager {
|
|
1378
1388
|
#sessionId: string = "";
|
|
1379
1389
|
#sessionName: string | undefined;
|
|
@@ -1419,6 +1429,39 @@ export class SessionManager {
|
|
|
1419
1429
|
return this.#blobStore.put(data);
|
|
1420
1430
|
}
|
|
1421
1431
|
|
|
1432
|
+
captureState(): SessionManagerStateSnapshot {
|
|
1433
|
+
return {
|
|
1434
|
+
sessionId: this.#sessionId,
|
|
1435
|
+
sessionName: this.#sessionName,
|
|
1436
|
+
sessionFile: this.#sessionFile,
|
|
1437
|
+
flushed: this.#flushed,
|
|
1438
|
+
needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
|
|
1439
|
+
// Snapshot entry objects by reference: switch/reload replaces the active entry array,
|
|
1440
|
+
// so rollback does not need structured cloning of extension/custom details.
|
|
1441
|
+
fileEntries: [...this.#fileEntries],
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
restoreState(snapshot: SessionManagerStateSnapshot): void {
|
|
1446
|
+
this.#sessionId = snapshot.sessionId;
|
|
1447
|
+
this.#sessionName = snapshot.sessionName;
|
|
1448
|
+
this.#sessionFile = snapshot.sessionFile;
|
|
1449
|
+
this.#flushed = snapshot.flushed;
|
|
1450
|
+
this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
|
|
1451
|
+
this.#fileEntries = [...snapshot.fileEntries];
|
|
1452
|
+
this.#persistWriter = undefined;
|
|
1453
|
+
this.#persistWriterPath = undefined;
|
|
1454
|
+
this.#persistChain = Promise.resolve();
|
|
1455
|
+
this.#persistError = undefined;
|
|
1456
|
+
this.#persistErrorReported = false;
|
|
1457
|
+
this.#artifactManager = null;
|
|
1458
|
+
this.#artifactManagerSessionFile = null;
|
|
1459
|
+
this.#buildIndex();
|
|
1460
|
+
if (this.#sessionFile) {
|
|
1461
|
+
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1422
1465
|
/** Initialize with a specific session file (used by factory methods) */
|
|
1423
1466
|
async #initSessionFile(sessionFile: string): Promise<void> {
|
|
1424
1467
|
await this.setSessionFile(sessionFile);
|
|
@@ -1445,6 +1488,7 @@ export class SessionManager {
|
|
|
1445
1488
|
this.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(this.#fileEntries);
|
|
1446
1489
|
|
|
1447
1490
|
await resolveBlobRefsInEntries(this.#fileEntries, this.#blobStore);
|
|
1491
|
+
this.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
1448
1492
|
|
|
1449
1493
|
this.#buildIndex();
|
|
1450
1494
|
this.#flushed = true;
|
|
@@ -2300,6 +2344,26 @@ export class SessionManager {
|
|
|
2300
2344
|
return buildSessionContext(this.getEntries(), this.#leafId, this.#byId);
|
|
2301
2345
|
}
|
|
2302
2346
|
|
|
2347
|
+
/** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries. */
|
|
2348
|
+
sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean {
|
|
2349
|
+
let didSanitize = false;
|
|
2350
|
+
for (const entry of this.#fileEntries) {
|
|
2351
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") {
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
const sanitizedMessage = sanitizeRehydratedOpenAIResponsesAssistantMessage(entry.message);
|
|
2356
|
+
if (sanitizedMessage === entry.message) {
|
|
2357
|
+
continue;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
entry.message = sanitizedMessage;
|
|
2361
|
+
didSanitize = true;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
return didSanitize;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2303
2367
|
/**
|
|
2304
2368
|
* Get session header.
|
|
2305
2369
|
*/
|
|
@@ -2548,6 +2612,7 @@ export class SessionManager {
|
|
|
2548
2612
|
newHeader.title = sourceHeader?.title;
|
|
2549
2613
|
manager.#fileEntries = [newHeader, ...historyEntries];
|
|
2550
2614
|
manager.#sessionName = newHeader.title;
|
|
2615
|
+
manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
2551
2616
|
manager.#buildIndex();
|
|
2552
2617
|
await manager.#rewriteFile();
|
|
2553
2618
|
return manager;
|