@oh-my-pi/pi-coding-agent 14.5.9 → 14.5.11
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 +50 -0
- package/package.json +7 -15
- package/scripts/build-binary.ts +1 -1
- package/src/cli/update-cli.ts +25 -1
- package/src/config/model-registry.ts +21 -19
- package/src/config/settings-schema.ts +11 -16
- package/src/discovery/claude-plugins.ts +28 -3
- package/src/edit/modes/atom.ts +50 -19
- package/src/edit/modes/hashline.ts +171 -110
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +14 -2
- package/src/extensibility/extensions/runner.ts +34 -1
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/internal-urls/docs-index.generated.ts +54 -54
- package/src/lsp/client.ts +27 -35
- package/src/memories/index.ts +5 -0
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/controllers/todo-command-controller.ts +22 -74
- package/src/modes/interactive-mode.ts +36 -9
- package/src/modes/theme/theme.ts +10 -1
- package/src/modes/types.ts +1 -3
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/prompts/system/auto-continue.md +1 -0
- package/src/prompts/system/eager-todo.md +1 -1
- package/src/prompts/tools/github.md +3 -3
- package/src/prompts/tools/todo-write.md +19 -19
- package/src/sdk.ts +13 -2
- package/src/session/agent-session.ts +196 -96
- package/src/session/session-manager.ts +19 -2
- package/src/tools/bash.ts +9 -4
- package/src/tools/gh.ts +267 -119
- package/src/tools/todo-write.ts +157 -195
- package/src/utils/git.ts +61 -2
- package/src/web/search/providers/searxng.ts +71 -13
- package/examples/custom-tools/todo/index.ts +0 -211
- package/examples/extensions/todo.ts +0 -295
package/src/sdk.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
INTENT_FIELD,
|
|
7
7
|
type ThinkingLevel,
|
|
8
8
|
} from "@oh-my-pi/pi-agent-core";
|
|
9
|
-
import type { Message, Model } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { Message, Model, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import {
|
|
11
11
|
getOpenAICodexTransportDetails,
|
|
12
12
|
prewarmOpenAICodexResponses,
|
|
@@ -793,7 +793,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
793
793
|
thinkingLevel = defaultRoleSpec.thinkingLevel;
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
-
//
|
|
796
|
+
// Prefer the selected model's configured defaultLevel, otherwise fall back
|
|
797
|
+
// to the global settings default.
|
|
798
|
+
if (thinkingLevel === undefined && model?.thinking?.defaultLevel !== undefined) {
|
|
799
|
+
thinkingLevel = model.thinking.defaultLevel;
|
|
800
|
+
}
|
|
797
801
|
if (thinkingLevel === undefined) {
|
|
798
802
|
thinkingLevel = settings.get("defaultThinkingLevel");
|
|
799
803
|
}
|
|
@@ -1498,6 +1502,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1498
1502
|
return await extensionRunner.emitBeforeProviderRequest(payload);
|
|
1499
1503
|
}
|
|
1500
1504
|
: undefined;
|
|
1505
|
+
const onResponse: SimpleStreamOptions["onResponse"] | undefined = extensionRunner
|
|
1506
|
+
? async (response, model) => {
|
|
1507
|
+
await extensionRunner.emitAfterProviderResponse(response, model);
|
|
1508
|
+
}
|
|
1509
|
+
: undefined;
|
|
1501
1510
|
|
|
1502
1511
|
const setToolUIContext = (uiContext: ExtensionUIContext, hasUI: boolean) => {
|
|
1503
1512
|
toolContextStore.setUIContext(uiContext, hasUI);
|
|
@@ -1527,6 +1536,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1527
1536
|
},
|
|
1528
1537
|
convertToLlm: convertToLlmFinal,
|
|
1529
1538
|
onPayload,
|
|
1539
|
+
onResponse,
|
|
1530
1540
|
sessionId: providerSessionId,
|
|
1531
1541
|
transformContext,
|
|
1532
1542
|
steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
|
|
@@ -1599,6 +1609,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1599
1609
|
toolRegistry,
|
|
1600
1610
|
transformContext,
|
|
1601
1611
|
onPayload,
|
|
1612
|
+
onResponse,
|
|
1602
1613
|
convertToLlm: convertToLlmFinal,
|
|
1603
1614
|
rebuildSystemPrompt,
|
|
1604
1615
|
mcpDiscoveryEnabled,
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
calculateRateLimitBackoffMs,
|
|
47
47
|
getSupportedEfforts,
|
|
48
48
|
isContextOverflow,
|
|
49
|
+
isUnexpectedSocketCloseMessage,
|
|
49
50
|
isUsageLimitError,
|
|
50
51
|
modelsAreEqual,
|
|
51
52
|
parseRateLimitReason,
|
|
@@ -104,7 +105,7 @@ import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
|
104
105
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
105
106
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
106
107
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
107
|
-
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
108
|
+
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
108
109
|
import {
|
|
109
110
|
disposeKernelSessionsByOwner,
|
|
110
111
|
executePython as executePythonCommand,
|
|
@@ -120,6 +121,7 @@ import {
|
|
|
120
121
|
} from "../mcp/discoverable-tool-metadata";
|
|
121
122
|
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
122
123
|
import type { PlanModeState } from "../plan-mode/state";
|
|
124
|
+
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
123
125
|
import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
|
|
124
126
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
125
127
|
import handoffDocumentPrompt from "../prompts/system/handoff-document.md" with { type: "text" };
|
|
@@ -244,6 +246,8 @@ export interface AgentSessionConfig {
|
|
|
244
246
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
245
247
|
/** Provider payload hook used by the active session request path */
|
|
246
248
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
249
|
+
/** Provider response hook used by the active session request path */
|
|
250
|
+
onResponse?: SimpleStreamOptions["onResponse"];
|
|
247
251
|
/** Current session message-to-LLM conversion pipeline */
|
|
248
252
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
249
253
|
/** System prompt builder that can consider tool availability */
|
|
@@ -383,6 +387,11 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
|
|
|
383
387
|
return `${selector.provider}/${selector.id}`;
|
|
384
388
|
}
|
|
385
389
|
|
|
390
|
+
/** Composite key for auto-clear timers, keyed by phase name + task content. */
|
|
391
|
+
function todoClearKey(phaseName: string, taskContent: string): string {
|
|
392
|
+
return `${phaseName}\u0000${taskContent}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
386
395
|
const noOpUIContext: ExtensionUIContext = {
|
|
387
396
|
select: async (_title, _options, _dialogOptions) => undefined,
|
|
388
397
|
confirm: async (_title, _message, _dialogOptions) => false,
|
|
@@ -469,7 +478,7 @@ export class AgentSession {
|
|
|
469
478
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
470
479
|
|
|
471
480
|
// Bash execution state
|
|
472
|
-
#
|
|
481
|
+
#bashAbortControllers = new Set<AbortController>();
|
|
473
482
|
#pendingBashMessages: BashExecutionMessage[] = [];
|
|
474
483
|
|
|
475
484
|
// Python execution state
|
|
@@ -507,6 +516,7 @@ export class AgentSession {
|
|
|
507
516
|
#toolRegistry: Map<string, AgentTool>;
|
|
508
517
|
#transformContext: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
509
518
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
519
|
+
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
510
520
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
511
521
|
#rebuildSystemPrompt: ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>) | undefined;
|
|
512
522
|
#baseSystemPrompt: string;
|
|
@@ -526,8 +536,7 @@ export class AgentSession {
|
|
|
526
536
|
#ttsrRetryToken = 0;
|
|
527
537
|
#ttsrResumePromise: Promise<void> | undefined = undefined;
|
|
528
538
|
#ttsrResumeResolve: (() => void) | undefined = undefined;
|
|
529
|
-
#
|
|
530
|
-
#postPromptTaskIds = new Set<number>();
|
|
539
|
+
#postPromptTasks = new Set<Promise<void>>();
|
|
531
540
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
532
541
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
533
542
|
#postPromptTasksAbortController = new AbortController();
|
|
@@ -593,6 +602,7 @@ export class AgentSession {
|
|
|
593
602
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
594
603
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
595
604
|
this.#onPayload = config.onPayload;
|
|
605
|
+
this.#onResponse = config.onResponse;
|
|
596
606
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
597
607
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
598
608
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
@@ -1144,14 +1154,13 @@ export class AgentSession {
|
|
|
1144
1154
|
}
|
|
1145
1155
|
|
|
1146
1156
|
#trackPostPromptTask(task: Promise<void>): void {
|
|
1147
|
-
|
|
1148
|
-
this.#postPromptTaskIds.add(taskId);
|
|
1157
|
+
this.#postPromptTasks.add(task);
|
|
1149
1158
|
this.#ensurePostPromptTasksPromise();
|
|
1150
1159
|
void task
|
|
1151
1160
|
.catch(() => {})
|
|
1152
1161
|
.finally(() => {
|
|
1153
|
-
this.#
|
|
1154
|
-
if (this.#
|
|
1162
|
+
this.#postPromptTasks.delete(task);
|
|
1163
|
+
if (this.#postPromptTasks.size === 0) {
|
|
1155
1164
|
this.#resolvePostPromptTasks();
|
|
1156
1165
|
}
|
|
1157
1166
|
});
|
|
@@ -1217,11 +1226,11 @@ export class AgentSession {
|
|
|
1217
1226
|
await this.#promptWithMessage(
|
|
1218
1227
|
{
|
|
1219
1228
|
role: "developer",
|
|
1220
|
-
content: [{ type: "text", text:
|
|
1229
|
+
content: [{ type: "text", text: autoContinuePrompt }],
|
|
1221
1230
|
attribution: "agent",
|
|
1222
1231
|
timestamp: Date.now(),
|
|
1223
1232
|
},
|
|
1224
|
-
|
|
1233
|
+
autoContinuePrompt,
|
|
1225
1234
|
{ skipPostPromptRecoveryWait: true },
|
|
1226
1235
|
);
|
|
1227
1236
|
};
|
|
@@ -1235,11 +1244,21 @@ export class AgentSession {
|
|
|
1235
1244
|
);
|
|
1236
1245
|
}
|
|
1237
1246
|
|
|
1238
|
-
#cancelPostPromptTasks(): void {
|
|
1247
|
+
async #cancelPostPromptTasks(): Promise<void> {
|
|
1239
1248
|
this.#postPromptTasksAbortController.abort();
|
|
1240
1249
|
this.#postPromptTasksAbortController = new AbortController();
|
|
1241
|
-
this.#
|
|
1242
|
-
|
|
1250
|
+
this.#resolveTtsrResume();
|
|
1251
|
+
|
|
1252
|
+
const pendingTasks = Array.from(this.#postPromptTasks);
|
|
1253
|
+
if (pendingTasks.length === 0) {
|
|
1254
|
+
this.#resolvePostPromptTasks();
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
await Promise.allSettled(pendingTasks);
|
|
1259
|
+
if (this.#postPromptTasks.size === 0) {
|
|
1260
|
+
this.#resolvePostPromptTasks();
|
|
1261
|
+
}
|
|
1243
1262
|
}
|
|
1244
1263
|
/**
|
|
1245
1264
|
* Wait for retry, TTSR resume, and any background continuation to settle.
|
|
@@ -1523,10 +1542,19 @@ export class AgentSession {
|
|
|
1523
1542
|
const path = typeof args.path === "string" ? args.path : undefined;
|
|
1524
1543
|
if (!path) return undefined;
|
|
1525
1544
|
|
|
1545
|
+
// `local://` URLs (e.g. local://PLAN.md for plan-mode) resolve to a real
|
|
1546
|
+
// on-disk artifacts path; pre-caching works as long as we ask the
|
|
1547
|
+
// local-protocol handler. Other internal-scheme URLs (agent://, skill://,
|
|
1548
|
+
// rule://, mcp://, artifact://) have no stable filesystem representation;
|
|
1549
|
+
// skip pre-cache entirely for those — the edit tool itself will reject
|
|
1550
|
+
// them through its normal dispatch path.
|
|
1551
|
+
const resolvedPath = this.#resolveSessionFsPath(path);
|
|
1552
|
+
if (resolvedPath === undefined) return undefined;
|
|
1553
|
+
|
|
1526
1554
|
return {
|
|
1527
1555
|
toolCall,
|
|
1528
1556
|
path,
|
|
1529
|
-
resolvedPath
|
|
1557
|
+
resolvedPath,
|
|
1530
1558
|
diff: typeof args.diff === "string" ? args.diff : undefined,
|
|
1531
1559
|
op: typeof args.op === "string" ? args.op : undefined,
|
|
1532
1560
|
rename: typeof args.rename === "string" ? args.rename : undefined,
|
|
@@ -1600,11 +1628,47 @@ export class AgentSession {
|
|
|
1600
1628
|
}
|
|
1601
1629
|
|
|
1602
1630
|
/** Invalidate cache for a file after an edit completes to prevent stale data */
|
|
1603
|
-
#invalidateFileCacheForPath(
|
|
1604
|
-
const resolvedPath =
|
|
1631
|
+
#invalidateFileCacheForPath(filePath: string): void {
|
|
1632
|
+
const resolvedPath = this.#resolveSessionFsPath(filePath);
|
|
1633
|
+
if (resolvedPath === undefined) return;
|
|
1605
1634
|
this.#streamingEditFileCache.delete(resolvedPath);
|
|
1606
1635
|
}
|
|
1607
1636
|
|
|
1637
|
+
/**
|
|
1638
|
+
* Resolve a path supplied to a tool to a real filesystem path.
|
|
1639
|
+
*
|
|
1640
|
+
* - `local://` URLs route through the local-protocol handler so they map
|
|
1641
|
+
* onto the session's on-disk artifacts directory; pre-caching, ENOENT
|
|
1642
|
+
* handling, and post-edit invalidation all work normally.
|
|
1643
|
+
* - Other internal-scheme URLs (agent://, skill://, rule://, mcp://,
|
|
1644
|
+
* artifact://) have no stable filesystem path; this returns `undefined`
|
|
1645
|
+
* so callers skip filesystem-only operations.
|
|
1646
|
+
* - Cwd-relative and absolute paths resolve via `resolveToCwd`.
|
|
1647
|
+
*/
|
|
1648
|
+
#resolveSessionFsPath(filePath: string): string | undefined {
|
|
1649
|
+
const normalized = normalizeLocalScheme(filePath);
|
|
1650
|
+
if (normalized.startsWith("local:")) {
|
|
1651
|
+
return resolveLocalUrlToPath(normalized, this.#localProtocolOptions());
|
|
1652
|
+
}
|
|
1653
|
+
if (
|
|
1654
|
+
normalized.startsWith("agent://") ||
|
|
1655
|
+
normalized.startsWith("skill://") ||
|
|
1656
|
+
normalized.startsWith("rule://") ||
|
|
1657
|
+
normalized.startsWith("mcp://") ||
|
|
1658
|
+
normalized.startsWith("artifact://")
|
|
1659
|
+
) {
|
|
1660
|
+
return undefined;
|
|
1661
|
+
}
|
|
1662
|
+
return resolveToCwd(normalized, this.sessionManager.getCwd());
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
#localProtocolOptions(): LocalProtocolOptions {
|
|
1666
|
+
return {
|
|
1667
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
1668
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1608
1672
|
#maybeAbortStreamingEdit(event: AgentEvent): void {
|
|
1609
1673
|
if (!this.settings.get("edit.streamingAbort")) return;
|
|
1610
1674
|
if (this.#streamingEditAbortTriggered) return;
|
|
@@ -1892,7 +1956,7 @@ export class AgentSession {
|
|
|
1892
1956
|
} catch (error) {
|
|
1893
1957
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
1894
1958
|
}
|
|
1895
|
-
this.#cancelPostPromptTasks();
|
|
1959
|
+
await this.#cancelPostPromptTasks();
|
|
1896
1960
|
this.#clearTodoClearTimers();
|
|
1897
1961
|
const drained = await this.#asyncJobManager?.dispose({ timeoutMs: 3_000 });
|
|
1898
1962
|
const deliveryState = this.#asyncJobManager?.getDeliveryState();
|
|
@@ -2318,21 +2382,39 @@ export class AgentSession {
|
|
|
2318
2382
|
|
|
2319
2383
|
/** Apply session-level stream hooks to a direct side request. */
|
|
2320
2384
|
prepareSimpleStreamOptions(options: SimpleStreamOptions): SimpleStreamOptions {
|
|
2321
|
-
if (!this.#onPayload) return options;
|
|
2322
|
-
if (!options.onPayload) {
|
|
2323
|
-
return { ...options, onPayload: this.#onPayload };
|
|
2324
|
-
}
|
|
2325
2385
|
const sessionOnPayload = this.#onPayload;
|
|
2326
|
-
const
|
|
2327
|
-
return
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2386
|
+
const sessionOnResponse = this.#onResponse;
|
|
2387
|
+
if (!sessionOnPayload && !sessionOnResponse) return options;
|
|
2388
|
+
|
|
2389
|
+
const preparedOptions: SimpleStreamOptions = { ...options };
|
|
2390
|
+
|
|
2391
|
+
if (sessionOnPayload) {
|
|
2392
|
+
if (!options.onPayload) {
|
|
2393
|
+
preparedOptions.onPayload = sessionOnPayload;
|
|
2394
|
+
} else {
|
|
2395
|
+
const requestOnPayload = options.onPayload;
|
|
2396
|
+
preparedOptions.onPayload = async (payload, model) => {
|
|
2397
|
+
const sessionPayload = await sessionOnPayload(payload, model);
|
|
2398
|
+
const sessionResolvedPayload = sessionPayload ?? payload;
|
|
2399
|
+
const requestPayload = await requestOnPayload(sessionResolvedPayload, model);
|
|
2400
|
+
return requestPayload ?? sessionResolvedPayload;
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
if (sessionOnResponse) {
|
|
2406
|
+
if (!options.onResponse) {
|
|
2407
|
+
preparedOptions.onResponse = sessionOnResponse;
|
|
2408
|
+
} else {
|
|
2409
|
+
const requestOnResponse = options.onResponse;
|
|
2410
|
+
preparedOptions.onResponse = async (response, model) => {
|
|
2411
|
+
await sessionOnResponse(response, model);
|
|
2412
|
+
await requestOnResponse(response, model);
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
return preparedOptions;
|
|
2336
2418
|
}
|
|
2337
2419
|
|
|
2338
2420
|
/** Current steering mode */
|
|
@@ -2466,10 +2548,7 @@ export class AgentSession {
|
|
|
2466
2548
|
if (this.#planReferenceSent) return null;
|
|
2467
2549
|
|
|
2468
2550
|
const planFilePath = this.#planReferencePath;
|
|
2469
|
-
const resolvedPlanPath = resolveLocalUrlToPath(planFilePath,
|
|
2470
|
-
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
2471
|
-
getSessionId: () => this.sessionManager.getSessionId(),
|
|
2472
|
-
});
|
|
2551
|
+
const resolvedPlanPath = resolveLocalUrlToPath(planFilePath, this.#localProtocolOptions());
|
|
2473
2552
|
let planContent: string;
|
|
2474
2553
|
try {
|
|
2475
2554
|
planContent = await Bun.file(resolvedPlanPath).text();
|
|
@@ -2502,15 +2581,9 @@ export class AgentSession {
|
|
|
2502
2581
|
if (!state?.enabled) return null;
|
|
2503
2582
|
const sessionPlanUrl = "local://PLAN.md";
|
|
2504
2583
|
const resolvedPlanPath = state.planFilePath.startsWith("local:")
|
|
2505
|
-
? resolveLocalUrlToPath(normalizeLocalScheme(state.planFilePath),
|
|
2506
|
-
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
2507
|
-
getSessionId: () => this.sessionManager.getSessionId(),
|
|
2508
|
-
})
|
|
2584
|
+
? resolveLocalUrlToPath(normalizeLocalScheme(state.planFilePath), this.#localProtocolOptions())
|
|
2509
2585
|
: resolveToCwd(state.planFilePath, this.sessionManager.getCwd());
|
|
2510
|
-
const resolvedSessionPlan = resolveLocalUrlToPath(sessionPlanUrl,
|
|
2511
|
-
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
2512
|
-
getSessionId: () => this.sessionManager.getSessionId(),
|
|
2513
|
-
});
|
|
2586
|
+
const resolvedSessionPlan = resolveLocalUrlToPath(sessionPlanUrl, this.#localProtocolOptions());
|
|
2514
2587
|
const displayPlanPath =
|
|
2515
2588
|
state.planFilePath.startsWith("local:") || resolvedPlanPath !== resolvedSessionPlan
|
|
2516
2589
|
? state.planFilePath
|
|
@@ -3279,10 +3352,9 @@ export class AgentSession {
|
|
|
3279
3352
|
|
|
3280
3353
|
#cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
|
|
3281
3354
|
return phases.map(phase => ({
|
|
3282
|
-
id: phase.id,
|
|
3283
3355
|
name: phase.name,
|
|
3284
3356
|
tasks: phase.tasks.map(task => {
|
|
3285
|
-
const out: TodoItem = {
|
|
3357
|
+
const out: TodoItem = { content: task.content, status: task.status };
|
|
3286
3358
|
if (task.notes && task.notes.length > 0) out.notes = [...task.notes];
|
|
3287
3359
|
return out;
|
|
3288
3360
|
}),
|
|
@@ -3294,43 +3366,43 @@ export class AgentSession {
|
|
|
3294
3366
|
const delaySec = this.settings.get("tasks.todoClearDelay") ?? 60;
|
|
3295
3367
|
if (delaySec < 0) return; // "Never" — no auto-clear
|
|
3296
3368
|
const delayMs = delaySec * 1000;
|
|
3297
|
-
const
|
|
3369
|
+
const doneKeys = new Set<string>();
|
|
3298
3370
|
for (const phase of phases) {
|
|
3299
3371
|
for (const task of phase.tasks) {
|
|
3300
3372
|
if (task.status === "completed" || task.status === "abandoned") {
|
|
3301
|
-
|
|
3373
|
+
doneKeys.add(todoClearKey(phase.name, task.content));
|
|
3302
3374
|
}
|
|
3303
3375
|
}
|
|
3304
3376
|
}
|
|
3305
3377
|
|
|
3306
3378
|
// Cancel timers for tasks that are no longer done (e.g. status was reverted)
|
|
3307
|
-
for (const [
|
|
3308
|
-
if (!
|
|
3379
|
+
for (const [key, timer] of this.#todoClearTimers) {
|
|
3380
|
+
if (!doneKeys.has(key)) {
|
|
3309
3381
|
clearTimeout(timer);
|
|
3310
|
-
this.#todoClearTimers.delete(
|
|
3382
|
+
this.#todoClearTimers.delete(key);
|
|
3311
3383
|
}
|
|
3312
3384
|
}
|
|
3313
3385
|
|
|
3314
3386
|
// Schedule new timers for newly-done tasks
|
|
3315
|
-
for (const
|
|
3316
|
-
if (this.#todoClearTimers.has(
|
|
3387
|
+
for (const key of doneKeys) {
|
|
3388
|
+
if (this.#todoClearTimers.has(key)) continue;
|
|
3317
3389
|
if (delayMs === 0) {
|
|
3318
3390
|
// Instant — run synchronously on next microtask to batch removals
|
|
3319
|
-
const timer = setTimeout(() => this.#runTodoAutoClear(
|
|
3320
|
-
this.#todoClearTimers.set(
|
|
3391
|
+
const timer = setTimeout(() => this.#runTodoAutoClear(key), 0);
|
|
3392
|
+
this.#todoClearTimers.set(key, timer);
|
|
3321
3393
|
} else {
|
|
3322
|
-
const timer = setTimeout(() => this.#runTodoAutoClear(
|
|
3323
|
-
this.#todoClearTimers.set(
|
|
3394
|
+
const timer = setTimeout(() => this.#runTodoAutoClear(key), delayMs);
|
|
3395
|
+
this.#todoClearTimers.set(key, timer);
|
|
3324
3396
|
}
|
|
3325
3397
|
}
|
|
3326
3398
|
}
|
|
3327
3399
|
|
|
3328
3400
|
/** Remove a single completed task and notify the UI. */
|
|
3329
|
-
#runTodoAutoClear(
|
|
3330
|
-
this.#todoClearTimers.delete(
|
|
3401
|
+
#runTodoAutoClear(key: string): void {
|
|
3402
|
+
this.#todoClearTimers.delete(key);
|
|
3331
3403
|
let removed = false;
|
|
3332
3404
|
for (const phase of this.#todoPhases) {
|
|
3333
|
-
const idx = phase.tasks.findIndex(t => t.
|
|
3405
|
+
const idx = phase.tasks.findIndex(t => todoClearKey(phase.name, t.content) === key);
|
|
3334
3406
|
if (idx !== -1 && (phase.tasks[idx].status === "completed" || phase.tasks[idx].status === "abandoned")) {
|
|
3335
3407
|
phase.tasks.splice(idx, 1);
|
|
3336
3408
|
removed = true;
|
|
@@ -3358,9 +3430,13 @@ export class AgentSession {
|
|
|
3358
3430
|
this.abortRetry();
|
|
3359
3431
|
this.#promptGeneration++;
|
|
3360
3432
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
3361
|
-
this
|
|
3362
|
-
this
|
|
3433
|
+
this.abortCompaction();
|
|
3434
|
+
this.abortHandoff();
|
|
3435
|
+
this.abortBash();
|
|
3436
|
+
this.abortPython();
|
|
3437
|
+
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
3363
3438
|
this.agent.abort();
|
|
3439
|
+
await postPromptDrain;
|
|
3364
3440
|
await this.agent.waitForIdle();
|
|
3365
3441
|
// Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
|
|
3366
3442
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
@@ -3555,8 +3631,9 @@ export class AgentSession {
|
|
|
3555
3631
|
);
|
|
3556
3632
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
3557
3633
|
|
|
3558
|
-
// Re-apply
|
|
3559
|
-
|
|
3634
|
+
// Re-apply thinking for the newly selected model. Prefer the model's
|
|
3635
|
+
// configured defaultLevel; otherwise preserve the current level.
|
|
3636
|
+
this.setThinkingLevel(model.thinking?.defaultLevel ?? this.thinkingLevel);
|
|
3560
3637
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
3561
3638
|
}
|
|
3562
3639
|
|
|
@@ -3577,8 +3654,9 @@ export class AgentSession {
|
|
|
3577
3654
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
|
3578
3655
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
3579
3656
|
|
|
3580
|
-
// Apply explicit thinking level
|
|
3581
|
-
|
|
3657
|
+
// Apply explicit thinking level if given; otherwise prefer the model's
|
|
3658
|
+
// configured defaultLevel; otherwise re-clamp the current level.
|
|
3659
|
+
this.setThinkingLevel(thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel);
|
|
3582
3660
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
3583
3661
|
}
|
|
3584
3662
|
|
|
@@ -3876,9 +3954,13 @@ export class AgentSession {
|
|
|
3876
3954
|
* @param options Optional callbacks for completion/error handling
|
|
3877
3955
|
*/
|
|
3878
3956
|
async compact(customInstructions?: string, options?: CompactOptions): Promise<CompactionResult> {
|
|
3957
|
+
if (this.#compactionAbortController) {
|
|
3958
|
+
throw new Error("Compaction already in progress");
|
|
3959
|
+
}
|
|
3879
3960
|
this.#disconnectFromAgent();
|
|
3880
3961
|
await this.abort();
|
|
3881
|
-
|
|
3962
|
+
const compactionAbortController = new AbortController();
|
|
3963
|
+
this.#compactionAbortController = compactionAbortController;
|
|
3882
3964
|
|
|
3883
3965
|
try {
|
|
3884
3966
|
if (!this.model) {
|
|
@@ -3916,7 +3998,7 @@ export class AgentSession {
|
|
|
3916
3998
|
preparation,
|
|
3917
3999
|
branchEntries: pathEntries,
|
|
3918
4000
|
customInstructions,
|
|
3919
|
-
signal:
|
|
4001
|
+
signal: compactionAbortController.signal,
|
|
3920
4002
|
})) as SessionBeforeCompactResult | undefined;
|
|
3921
4003
|
|
|
3922
4004
|
if (result?.cancel) {
|
|
@@ -3963,7 +4045,7 @@ export class AgentSession {
|
|
|
3963
4045
|
compactionModel,
|
|
3964
4046
|
apiKey,
|
|
3965
4047
|
customInstructions,
|
|
3966
|
-
|
|
4048
|
+
compactionAbortController.signal,
|
|
3967
4049
|
{ promptOverride: hookPrompt, extraContext: hookContext, remoteInstructions: this.#baseSystemPrompt },
|
|
3968
4050
|
);
|
|
3969
4051
|
summary = result.summary;
|
|
@@ -3974,7 +4056,7 @@ export class AgentSession {
|
|
|
3974
4056
|
preserveData = { ...(preserveData ?? {}), ...(result.preserveData ?? {}) };
|
|
3975
4057
|
}
|
|
3976
4058
|
|
|
3977
|
-
if (
|
|
4059
|
+
if (compactionAbortController.signal.aborted) {
|
|
3978
4060
|
throw new Error("Compaction cancelled");
|
|
3979
4061
|
}
|
|
3980
4062
|
|
|
@@ -4021,7 +4103,9 @@ export class AgentSession {
|
|
|
4021
4103
|
options?.onError?.(err);
|
|
4022
4104
|
throw error;
|
|
4023
4105
|
} finally {
|
|
4024
|
-
this.#compactionAbortController
|
|
4106
|
+
if (this.#compactionAbortController === compactionAbortController) {
|
|
4107
|
+
this.#compactionAbortController = undefined;
|
|
4108
|
+
}
|
|
4025
4109
|
this.#reconnectToAgent();
|
|
4026
4110
|
}
|
|
4027
4111
|
}
|
|
@@ -4488,7 +4572,7 @@ export class AgentSession {
|
|
|
4488
4572
|
(task): task is TodoItem & { status: "pending" | "in_progress" } =>
|
|
4489
4573
|
task.status === "pending" || task.status === "in_progress",
|
|
4490
4574
|
)
|
|
4491
|
-
.map(task => ({
|
|
4575
|
+
.map(task => ({ content: task.content, status: task.status })),
|
|
4492
4576
|
}))
|
|
4493
4577
|
.filter(phase => phase.tasks.length > 0);
|
|
4494
4578
|
const incomplete = incompleteByPhase.flatMap(phase => phase.tasks);
|
|
@@ -5263,9 +5347,12 @@ export class AgentSession {
|
|
|
5263
5347
|
|
|
5264
5348
|
#isTransientTransportErrorMessage(errorMessage: string): boolean {
|
|
5265
5349
|
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
|
5266
|
-
// service unavailable, network/connection errors, fetch failed, terminated, retry delay exceeded
|
|
5267
|
-
return
|
|
5268
|
-
errorMessage
|
|
5350
|
+
// service unavailable, network/connection/socket errors, fetch failed, terminated, retry delay exceeded
|
|
5351
|
+
return (
|
|
5352
|
+
isUnexpectedSocketCloseMessage(errorMessage) ||
|
|
5353
|
+
/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(
|
|
5354
|
+
errorMessage,
|
|
5355
|
+
)
|
|
5269
5356
|
);
|
|
5270
5357
|
}
|
|
5271
5358
|
|
|
@@ -5608,15 +5695,16 @@ export class AgentSession {
|
|
|
5608
5695
|
this.agent.replaceMessages(messages.slice(0, -1));
|
|
5609
5696
|
}
|
|
5610
5697
|
|
|
5611
|
-
// Wait with exponential backoff (abortable)
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
}
|
|
5616
|
-
this.#retryAbortController = new AbortController();
|
|
5698
|
+
// Wait with exponential backoff (abortable).
|
|
5699
|
+
const retryAbortController = new AbortController();
|
|
5700
|
+
this.#retryAbortController?.abort();
|
|
5701
|
+
this.#retryAbortController = retryAbortController;
|
|
5617
5702
|
try {
|
|
5618
|
-
await abortableSleep(delayMs,
|
|
5703
|
+
await abortableSleep(delayMs, retryAbortController.signal);
|
|
5619
5704
|
} catch {
|
|
5705
|
+
if (this.#retryAbortController !== retryAbortController) {
|
|
5706
|
+
return false;
|
|
5707
|
+
}
|
|
5620
5708
|
// Aborted during sleep - emit end event so UI can clean up
|
|
5621
5709
|
const attempt = this.#retryAttempt;
|
|
5622
5710
|
this.#retryAttempt = 0;
|
|
@@ -5630,7 +5718,9 @@ export class AgentSession {
|
|
|
5630
5718
|
this.#resolveRetry();
|
|
5631
5719
|
return false;
|
|
5632
5720
|
}
|
|
5633
|
-
this.#retryAbortController
|
|
5721
|
+
if (this.#retryAbortController === retryAbortController) {
|
|
5722
|
+
this.#retryAbortController = undefined;
|
|
5723
|
+
}
|
|
5634
5724
|
|
|
5635
5725
|
// Retry via continue() outside the agent_end event callback chain.
|
|
5636
5726
|
this.#scheduleAgentContinue({ delayMs: 1, generation });
|
|
@@ -5722,12 +5812,13 @@ export class AgentSession {
|
|
|
5722
5812
|
}
|
|
5723
5813
|
}
|
|
5724
5814
|
|
|
5725
|
-
|
|
5815
|
+
const abortController = new AbortController();
|
|
5816
|
+
this.#bashAbortControllers.add(abortController);
|
|
5726
5817
|
|
|
5727
5818
|
try {
|
|
5728
5819
|
const result = await executeBashCommand(command, {
|
|
5729
5820
|
onChunk,
|
|
5730
|
-
signal:
|
|
5821
|
+
signal: abortController.signal,
|
|
5731
5822
|
sessionKey: this.sessionId,
|
|
5732
5823
|
timeout: clampTimeout("bash") * 1000,
|
|
5733
5824
|
onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
|
|
@@ -5736,7 +5827,7 @@ export class AgentSession {
|
|
|
5736
5827
|
this.recordBashResult(command, result, options);
|
|
5737
5828
|
return result;
|
|
5738
5829
|
} finally {
|
|
5739
|
-
this.#
|
|
5830
|
+
this.#bashAbortControllers.delete(abortController);
|
|
5740
5831
|
}
|
|
5741
5832
|
}
|
|
5742
5833
|
|
|
@@ -5775,12 +5866,14 @@ export class AgentSession {
|
|
|
5775
5866
|
* Cancel running bash command.
|
|
5776
5867
|
*/
|
|
5777
5868
|
abortBash(): void {
|
|
5778
|
-
this.#
|
|
5869
|
+
for (const abortController of this.#bashAbortControllers) {
|
|
5870
|
+
abortController.abort();
|
|
5871
|
+
}
|
|
5779
5872
|
}
|
|
5780
5873
|
|
|
5781
5874
|
/** Whether a bash command is currently running */
|
|
5782
5875
|
get isBashRunning(): boolean {
|
|
5783
|
-
return this.#
|
|
5876
|
+
return this.#bashAbortControllers.size > 0;
|
|
5784
5877
|
}
|
|
5785
5878
|
|
|
5786
5879
|
/** Whether there are pending bash messages waiting to be flushed */
|
|
@@ -6518,6 +6611,8 @@ export class AgentSession {
|
|
|
6518
6611
|
cancelled: boolean;
|
|
6519
6612
|
aborted?: boolean;
|
|
6520
6613
|
summaryEntry?: BranchSummaryEntry;
|
|
6614
|
+
/** Raw session context built during navigation — pass to renderInitialMessages to skip a second O(N) walk. */
|
|
6615
|
+
sessionContext?: SessionContext;
|
|
6521
6616
|
}> {
|
|
6522
6617
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
6523
6618
|
|
|
@@ -6647,15 +6742,20 @@ export class AgentSession {
|
|
|
6647
6742
|
this.sessionManager.branch(newLeafId);
|
|
6648
6743
|
}
|
|
6649
6744
|
|
|
6650
|
-
// Update agent state
|
|
6651
|
-
const
|
|
6652
|
-
|
|
6653
|
-
this
|
|
6745
|
+
// Update agent state — build display context to populate agent messages.
|
|
6746
|
+
const stateContext = this.sessionManager.buildSessionContext();
|
|
6747
|
+
const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
|
|
6748
|
+
await this.#restoreMCPSelectionsForSessionContext(displayContext);
|
|
6749
|
+
this.agent.replaceMessages(displayContext.messages);
|
|
6654
6750
|
this.#syncTodoPhasesFromBranch();
|
|
6655
6751
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6656
6752
|
|
|
6657
|
-
|
|
6658
|
-
|
|
6753
|
+
this.#branchSummaryAbortController = undefined;
|
|
6754
|
+
|
|
6755
|
+
// Emit session_tree event; only handlers can mutate session entries, so skip
|
|
6756
|
+
// the emit and the context rebuild when no handlers are registered (mirrors
|
|
6757
|
+
// the session_before_tree guard above).
|
|
6758
|
+
if (this.#extensionRunner?.hasHandlers("session_tree")) {
|
|
6659
6759
|
await this.#extensionRunner.emit({
|
|
6660
6760
|
type: "session_tree",
|
|
6661
6761
|
newLeafId: this.sessionManager.getLeafId(),
|
|
@@ -6663,10 +6763,10 @@ export class AgentSession {
|
|
|
6663
6763
|
summaryEntry,
|
|
6664
6764
|
fromExtension: summaryText ? fromExtension : undefined,
|
|
6665
6765
|
});
|
|
6766
|
+
const rawContext = this.sessionManager.buildSessionContext();
|
|
6767
|
+
return { editorText, cancelled: false, summaryEntry, sessionContext: rawContext };
|
|
6666
6768
|
}
|
|
6667
|
-
|
|
6668
|
-
this.#branchSummaryAbortController = undefined;
|
|
6669
|
-
return { editorText, cancelled: false, summaryEntry };
|
|
6769
|
+
return { editorText, cancelled: false, summaryEntry, sessionContext: stateContext };
|
|
6670
6770
|
}
|
|
6671
6771
|
|
|
6672
6772
|
/**
|