@oh-my-pi/pi-coding-agent 17.3.7 → 17.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/dist/{CHANGELOG-1hmwwt45.md → CHANGELOG-vr9cckb4.md} +60 -0
- package/dist/cli.js +2993 -3001
- package/dist/docs-index.generated.txt +1 -1
- package/dist/{tool-views.generated-jdfmzwmn.js → tool-views.generated-dd2km5r2.js} +19 -19
- package/dist/types/advisor/advise-tool.d.ts +4 -2
- package/dist/types/cli/auth-broker-cli.d.ts +15 -0
- package/dist/types/cli/update-cli.d.ts +8 -0
- package/dist/types/cli-commands.d.ts +10 -2
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/config/settings.d.ts +9 -0
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/launch/presence.d.ts +4 -1
- package/dist/types/mcp/oauth-credentials.d.ts +23 -0
- package/dist/types/mcp/oauth-flow.d.ts +11 -0
- package/dist/types/mnemopi/backend.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +25 -3
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/session/agent-session.d.ts +3 -0
- package/dist/types/session/prewalk.d.ts +4 -0
- package/dist/types/session/session-entries.d.ts +0 -1
- package/dist/types/session/session-manager.d.ts +12 -0
- package/dist/types/session/session-stats.d.ts +13 -1
- package/dist/types/session/skill-title-input.d.ts +13 -0
- package/dist/types/subprocess/worker-client.d.ts +7 -4
- package/dist/types/task/label.d.ts +2 -0
- package/dist/types/task/render.d.ts +2 -0
- package/dist/types/tiny/completion-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +6 -4
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tiny/worker.d.ts +27 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/read-format.d.ts +6 -0
- package/dist/types/tools/read-summary.d.ts +7 -1
- package/dist/types/utils/block-context.d.ts +14 -0
- package/dist/types/utils/fetch-timeout.d.ts +15 -0
- package/dist/types/utils/git.d.ts +25 -1
- package/dist/types/web/search/providers/tinyfish.d.ts +4 -0
- package/package.json +13 -13
- package/src/advisor/advise-tool.ts +5 -3
- package/src/cli/auth-broker-cli.ts +36 -1
- package/src/cli/profile-bootstrap.ts +2 -6
- package/src/cli/update-cli.ts +63 -11
- package/src/cli-commands.ts +61 -7
- package/src/commands/completions.ts +2 -1
- package/src/commit/agentic/index.ts +15 -2
- package/src/commit/git/diff.ts +6 -2
- package/src/config/model-resolver.ts +52 -6
- package/src/config/models-config.ts +2 -2
- package/src/config/settings-schema.ts +33 -0
- package/src/config/settings.ts +159 -30
- package/src/discovery/helpers.ts +45 -2
- package/src/discovery/omp-plugins.ts +2 -1
- package/src/discovery/opencode.ts +56 -3
- package/src/eval/js/process-entry.ts +4 -4
- package/src/export/html/tool-views.generated.js +19 -19
- package/src/extensibility/extensions/runner.ts +3 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +47 -0
- package/src/launch/client.ts +9 -4
- package/src/launch/presence.ts +19 -4
- package/src/lsp/defaults.json +1 -1
- package/src/mcp/manager.ts +41 -20
- package/src/mcp/oauth-credentials.ts +38 -0
- package/src/mcp/oauth-flow.ts +21 -0
- package/src/mcp/tool-bridge.ts +32 -16
- package/src/mnemopi/backend.ts +35 -3
- package/src/modes/components/model-hub.ts +37 -4
- package/src/modes/components/settings-selector.ts +17 -11
- package/src/modes/components/tool-execution.ts +97 -29
- package/src/modes/components/tree-selector.ts +7 -2
- package/src/modes/controllers/event-controller.ts +12 -2
- package/src/modes/controllers/input-controller.ts +64 -27
- package/src/modes/controllers/mcp-command-controller.ts +13 -4
- package/src/modes/interactive-mode.ts +79 -11
- package/src/modes/types.ts +11 -0
- package/src/prompts/system/memory-extraction-system.md +5 -22
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/session/agent-session.ts +52 -6
- package/src/session/messages.ts +6 -0
- package/src/session/prewalk.ts +25 -7
- package/src/session/session-entries.ts +0 -1
- package/src/session/session-maintenance.ts +10 -1
- package/src/session/session-manager.ts +15 -0
- package/src/session/session-stats.ts +24 -3
- package/src/session/settings-stream-fn.ts +7 -0
- package/src/session/skill-title-input.ts +32 -0
- package/src/session/turn-recovery.ts +23 -18
- package/src/subprocess/worker-client.ts +8 -5
- package/src/task/executor.ts +11 -0
- package/src/task/index.ts +2 -0
- package/src/task/label.ts +14 -1
- package/src/task/persisted-revive.ts +13 -0
- package/src/task/render.ts +1 -1
- package/src/task/structured-subagent.ts +5 -2
- package/src/tiny/completion-prompt.ts +16 -0
- package/src/tiny/title-client.ts +15 -6
- package/src/tiny/title-protocol.ts +8 -1
- package/src/tiny/worker.ts +21 -19
- package/src/tools/bash.ts +7 -1
- package/src/tools/read-format.ts +16 -2
- package/src/tools/read-summary.ts +9 -4
- package/src/tools/read.ts +306 -72
- package/src/utils/block-context.ts +15 -1
- package/src/utils/fetch-timeout.ts +33 -0
- package/src/utils/git.ts +54 -11
- package/src/web/search/providers/browser-page.ts +21 -3
- package/src/web/search/providers/tinyfish.ts +26 -0
|
@@ -338,6 +338,7 @@ import { SessionProviderBoundary, type SessionProviderBoundaryHost } from "./ses
|
|
|
338
338
|
import { SessionStatsTracker, type SessionStatsTrackerHost } from "./session-stats";
|
|
339
339
|
import { SessionTools, type SessionToolsHost } from "./session-tools";
|
|
340
340
|
import type { ShakeMode, ShakeResult } from "./shake-types";
|
|
341
|
+
import { skillPromptTitleInput } from "./skill-title-input";
|
|
341
342
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
342
343
|
import { planTurnPersistence, sameMessageContent, sessionMessagePersistenceKey } from "./turn-persistence";
|
|
343
344
|
import { TurnRecovery, type TurnRecoveryHost } from "./turn-recovery";
|
|
@@ -533,6 +534,8 @@ export class AgentSession {
|
|
|
533
534
|
* generation path. Refresh via {@link AgentSession.setTitleSystemPrompt} when
|
|
534
535
|
* the session cwd changes. */
|
|
535
536
|
#titleSystemPrompt: string | undefined;
|
|
537
|
+
#titleGenerationStart: (() => void) | undefined;
|
|
538
|
+
#titleGenerationInFlightFor: string | undefined;
|
|
536
539
|
#titleGenerationAbortController = new AbortController();
|
|
537
540
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
538
541
|
|
|
@@ -1011,8 +1014,13 @@ export class AgentSession {
|
|
|
1011
1014
|
emitNotice: (level, message, source) => this.emitNotice(level, message, source),
|
|
1012
1015
|
setModelTemporary: (model, thinkingLevel, options) => this.setModelTemporary(model, thinkingLevel, options),
|
|
1013
1016
|
setActiveToolsByName: names => this.setActiveToolsByName(names),
|
|
1017
|
+
setActiveToolPresentation: (toolNames, mountedToolNames) =>
|
|
1018
|
+
this.setActiveToolPresentation(toolNames, mountedToolNames),
|
|
1019
|
+
runToolRegistryMutation: mutation => this.runToolRegistryMutation(mutation),
|
|
1014
1020
|
getActiveToolNames: () => this.getActiveToolNames(),
|
|
1015
1021
|
getEnabledToolNames: () => this.getEnabledToolNames(),
|
|
1022
|
+
getSelectedMCPToolNames: () => this.getSelectedMCPToolNames(),
|
|
1023
|
+
getMountedXdevToolNames: () => this.getMountedXdevToolNames(),
|
|
1016
1024
|
hasBuiltInTool: name => this.hasBuiltInTool(name),
|
|
1017
1025
|
getPlanModeState: () => this.getPlanModeState(),
|
|
1018
1026
|
setPlanModeState: state => this.setPlanModeState(state),
|
|
@@ -2361,6 +2369,7 @@ export class AgentSession {
|
|
|
2361
2369
|
assistantMsg.contextSnapshot = {
|
|
2362
2370
|
promptTokens: calculatePromptTokens(assistantMsg.usage),
|
|
2363
2371
|
nonMessageTokens: this.#stats.pendingNonMessageTokens ?? computeNonMessageTokens(this),
|
|
2372
|
+
compactionEpoch: this.#stats.compactionEpoch,
|
|
2364
2373
|
};
|
|
2365
2374
|
}
|
|
2366
2375
|
}
|
|
@@ -5466,11 +5475,20 @@ export class AgentSession {
|
|
|
5466
5475
|
let keywordNotices: CustomMessage[] = [];
|
|
5467
5476
|
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE && message.attribution === "user") {
|
|
5468
5477
|
const details = message.details;
|
|
5478
|
+
let skillName: string | undefined;
|
|
5469
5479
|
let skillArgs = "";
|
|
5470
|
-
if (details && typeof details === "object"
|
|
5471
|
-
|
|
5480
|
+
if (details && typeof details === "object") {
|
|
5481
|
+
if ("name" in details && typeof details.name === "string") skillName = details.name;
|
|
5482
|
+
if ("args" in details && typeof details.args === "string") skillArgs = details.args;
|
|
5472
5483
|
}
|
|
5473
5484
|
keywordNotices = this.#createMagicKeywordNotices(skillArgs);
|
|
5485
|
+
this.maybeStartTitleGeneration(
|
|
5486
|
+
skillPromptTitleInput({
|
|
5487
|
+
name: skillName,
|
|
5488
|
+
args: skillArgs,
|
|
5489
|
+
queueChipText: options?.queueChipText,
|
|
5490
|
+
}),
|
|
5491
|
+
);
|
|
5474
5492
|
}
|
|
5475
5493
|
|
|
5476
5494
|
if (options?.queueOnly) {
|
|
@@ -6538,14 +6556,31 @@ export class AgentSession {
|
|
|
6538
6556
|
this.#extensionRunner?.getCommand(
|
|
6539
6557
|
extensionCommandSpace === -1 ? firstMessage.slice(1) : firstMessage.slice(1, extensionCommandSpace),
|
|
6540
6558
|
) !== undefined;
|
|
6541
|
-
|
|
6559
|
+
const sessionId = this.sessionManager.getSessionId();
|
|
6560
|
+
if (
|
|
6561
|
+
isLocalExtensionCommand ||
|
|
6562
|
+
this.sessionName ||
|
|
6563
|
+
this.#titleGenerationInFlightFor === sessionId ||
|
|
6564
|
+
$env.PI_NO_TITLE ||
|
|
6565
|
+
isLowSignalTitleInput(firstMessage)
|
|
6566
|
+
) {
|
|
6542
6567
|
return;
|
|
6543
6568
|
}
|
|
6544
|
-
|
|
6569
|
+
this.#titleGenerationInFlightFor = sessionId;
|
|
6570
|
+
try {
|
|
6571
|
+
(onStart ?? this.#titleGenerationStart)?.();
|
|
6572
|
+
} catch (error) {
|
|
6573
|
+
if (this.#titleGenerationInFlightFor === sessionId) {
|
|
6574
|
+
this.#titleGenerationInFlightFor = undefined;
|
|
6575
|
+
}
|
|
6576
|
+
throw error;
|
|
6577
|
+
}
|
|
6545
6578
|
this.generateTitle(firstMessage)
|
|
6546
6579
|
.then(async title => {
|
|
6547
|
-
// Re-check after generation so
|
|
6548
|
-
// the first title
|
|
6580
|
+
// Re-check after generation so a later completion cannot replace
|
|
6581
|
+
// the first title, and a request from a replaced session cannot
|
|
6582
|
+
// name the current one.
|
|
6583
|
+
if (this.sessionManager.getSessionId() !== sessionId) return;
|
|
6549
6584
|
if (title && !this.sessionName) {
|
|
6550
6585
|
await this.sessionManager.setSessionName(title, "auto");
|
|
6551
6586
|
}
|
|
@@ -6556,6 +6591,11 @@ export class AgentSession {
|
|
|
6556
6591
|
reason: "uncaught-auto-title-error",
|
|
6557
6592
|
error: err instanceof Error ? err.message : String(err),
|
|
6558
6593
|
});
|
|
6594
|
+
})
|
|
6595
|
+
.finally(() => {
|
|
6596
|
+
if (this.#titleGenerationInFlightFor === sessionId) {
|
|
6597
|
+
this.#titleGenerationInFlightFor = undefined;
|
|
6598
|
+
}
|
|
6559
6599
|
});
|
|
6560
6600
|
}
|
|
6561
6601
|
|
|
@@ -6602,6 +6642,12 @@ export class AgentSession {
|
|
|
6602
6642
|
this.#titleSystemPrompt = prompt;
|
|
6603
6643
|
}
|
|
6604
6644
|
|
|
6645
|
+
/** Install the interactive title-download UI hook. Used when `/skill:` starts
|
|
6646
|
+
* titling from {@link promptCustomMessage} without the input-controller callback. */
|
|
6647
|
+
setTitleGenerationStart(handler: (() => void) | undefined): void {
|
|
6648
|
+
this.#titleGenerationStart = handler;
|
|
6649
|
+
}
|
|
6650
|
+
|
|
6605
6651
|
/**
|
|
6606
6652
|
* Abort current operation and wait for agent to become idle.
|
|
6607
6653
|
*
|
package/src/session/messages.ts
CHANGED
|
@@ -38,6 +38,7 @@ export {
|
|
|
38
38
|
|
|
39
39
|
import type { OutputMeta } from "../tools/output-meta";
|
|
40
40
|
import { formatOutputNotice } from "../tools/output-meta";
|
|
41
|
+
import { titleTextFromSkillPrompt } from "./skill-title-input";
|
|
41
42
|
|
|
42
43
|
export const SKILL_PROMPT_MESSAGE_TYPE = "skill-prompt";
|
|
43
44
|
export const LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE = "lsp-late-diagnostic";
|
|
@@ -163,6 +164,11 @@ function thinkingFromContent(content: unknown): string {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
function titleConversationTurnFromMessage(message: AgentMessage): TitleConversationTurn | undefined {
|
|
167
|
+
if (message.role === "custom") {
|
|
168
|
+
const text = titleTextFromSkillPrompt(message);
|
|
169
|
+
if (!text) return undefined;
|
|
170
|
+
return { role: "user", text };
|
|
171
|
+
}
|
|
166
172
|
if (message.role !== "user" && message.role !== "assistant") return undefined;
|
|
167
173
|
const text = textFromContent(message.content);
|
|
168
174
|
const thinking = message.role === "assistant" ? thinkingFromContent(message.content) : undefined;
|
package/src/session/prewalk.ts
CHANGED
|
@@ -11,6 +11,7 @@ import prewalkChecklistPrompt from "../prompts/system/prewalk-checklist.md" with
|
|
|
11
11
|
import prewalkContinuePrompt from "../prompts/system/prewalk-continue.md" with { type: "text" };
|
|
12
12
|
import prewalkPlanPrompt from "../prompts/system/prewalk-plan.md" with { type: "text" };
|
|
13
13
|
import { type ConfiguredThinkingLevel, prewalkWouldBeNoop } from "../thinking";
|
|
14
|
+
import { isMCPToolName } from "../tools/builtin-names";
|
|
14
15
|
import type { PlanProposalHandler } from "../tools/resolve";
|
|
15
16
|
import { ToolError } from "../tools/tool-errors";
|
|
16
17
|
import type { PlanYolo, Prewalk } from "./agent-session-types";
|
|
@@ -65,8 +66,12 @@ export interface PrewalkCoordinatorHost {
|
|
|
65
66
|
options?: { ephemeral?: boolean },
|
|
66
67
|
): Promise<void>;
|
|
67
68
|
setActiveToolsByName(names: string[]): Promise<void>;
|
|
69
|
+
setActiveToolPresentation(toolNames: string[], mountedToolNames: string[]): Promise<void>;
|
|
70
|
+
runToolRegistryMutation<T>(mutation: () => Promise<T>): Promise<T>;
|
|
68
71
|
getActiveToolNames(): string[];
|
|
69
72
|
getEnabledToolNames(): string[];
|
|
73
|
+
getSelectedMCPToolNames(): string[];
|
|
74
|
+
getMountedXdevToolNames(): string[];
|
|
70
75
|
hasBuiltInTool(name: string): boolean;
|
|
71
76
|
getPlanModeState(): PlanModeState | undefined;
|
|
72
77
|
setPlanModeState(state: PlanModeState | undefined): void;
|
|
@@ -90,7 +95,7 @@ export class PrewalkCoordinator {
|
|
|
90
95
|
#continuePending = false;
|
|
91
96
|
#todoSeen = false;
|
|
92
97
|
#planYolo: PlanYolo | undefined;
|
|
93
|
-
#
|
|
98
|
+
#planYoloPreviousNonMCPPresentation: { enabled: string[]; mounted: string[] } | undefined;
|
|
94
99
|
#planYoloArmed = false;
|
|
95
100
|
|
|
96
101
|
constructor(host: PrewalkCoordinatorHost, options: PrewalkCoordinatorOptions = {}) {
|
|
@@ -247,10 +252,14 @@ export class PrewalkCoordinator {
|
|
|
247
252
|
async armPlanYoloIfNeeded(): Promise<void> {
|
|
248
253
|
if (!this.#planYolo || this.#planYoloArmed) return;
|
|
249
254
|
this.#planYoloArmed = true;
|
|
250
|
-
const
|
|
255
|
+
const previousEnabledTools = this.#host.getEnabledToolNames();
|
|
256
|
+
const previousMountedTools = this.#host.getMountedXdevToolNames();
|
|
251
257
|
const augmentations = this.#host.hasBuiltInTool("write") ? ["write"] : [];
|
|
252
|
-
await this.#host.setActiveToolsByName([...new Set([...
|
|
253
|
-
this.#
|
|
258
|
+
await this.#host.setActiveToolsByName([...new Set([...previousEnabledTools, ...augmentations])]);
|
|
259
|
+
this.#planYoloPreviousNonMCPPresentation = {
|
|
260
|
+
enabled: previousEnabledTools.filter(name => !isMCPToolName(name)),
|
|
261
|
+
mounted: previousMountedTools.filter(name => !isMCPToolName(name)),
|
|
262
|
+
};
|
|
254
263
|
this.#host.setPlanModeState({
|
|
255
264
|
enabled: true,
|
|
256
265
|
planFilePath: this.#host.getPlanReferencePath() || "local://PLAN.md",
|
|
@@ -287,16 +296,25 @@ export class PrewalkCoordinator {
|
|
|
287
296
|
listPlanFiles: () => listPlanFiles({ localProtocolOptions: this.#host.localProtocolOptions() }),
|
|
288
297
|
});
|
|
289
298
|
this.#host.setPlanModeState(undefined);
|
|
290
|
-
const
|
|
299
|
+
const previousPresentation = this.#planYoloPreviousNonMCPPresentation;
|
|
291
300
|
try {
|
|
292
|
-
if (
|
|
301
|
+
if (previousPresentation) {
|
|
302
|
+
await this.#host.runToolRegistryMutation(async () => {
|
|
303
|
+
const liveMCP = this.#host.getSelectedMCPToolNames();
|
|
304
|
+
const liveMountedMCP = this.#host.getMountedXdevToolNames().filter(isMCPToolName);
|
|
305
|
+
await this.#host.setActiveToolPresentation(
|
|
306
|
+
[...new Set([...previousPresentation.enabled, ...liveMCP])],
|
|
307
|
+
[...new Set([...previousPresentation.mounted, ...liveMountedMCP])],
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
293
311
|
} catch (error) {
|
|
294
312
|
this.#host.setPlanModeState(state);
|
|
295
313
|
throw error;
|
|
296
314
|
}
|
|
297
315
|
this.#host.setPlanProposalHandler(null);
|
|
298
316
|
this.#planYolo = undefined;
|
|
299
|
-
this.#
|
|
317
|
+
this.#planYoloPreviousNonMCPPresentation = undefined;
|
|
300
318
|
await this.#host.setModelTemporary(planYolo.target, planYolo.thinkingLevel, { ephemeral: true });
|
|
301
319
|
this.#host.emitNotice(
|
|
302
320
|
"info",
|
|
@@ -477,7 +477,9 @@ export class SessionMaintenance {
|
|
|
477
477
|
const config = this.#withPlanProtection({
|
|
478
478
|
...(opts.config ?? AGGRESSIVE_SHAKE_CONFIG),
|
|
479
479
|
// Skip entries summarized away by the latest compaction — shaking them
|
|
480
|
-
// only churns persisted history with no prompt/cache effect.
|
|
480
|
+
// only churns persisted history with no prompt/cache effect. The cut is
|
|
481
|
+
// unconditional on the wire (see `buildSessionContext`), so a compaction
|
|
482
|
+
// the active model cannot replay still hides its prefix from the prompt.
|
|
481
483
|
keepBoundaryId: latestCompaction?.firstKeptEntryId,
|
|
482
484
|
});
|
|
483
485
|
const regions = collectShakeRegions(branchEntries, config);
|
|
@@ -2729,9 +2731,16 @@ export class SessionMaintenance {
|
|
|
2729
2731
|
}
|
|
2730
2732
|
|
|
2731
2733
|
const retryAfterMs = this.#host.parseRetryAfterMsFromError(message);
|
|
2734
|
+
// An input the summarizer cannot fit is deterministic: the same
|
|
2735
|
+
// prompt fails identically every attempt, so the retry budget is
|
|
2736
|
+
// pure latency and the next candidate (a larger window) is the
|
|
2737
|
+
// only move that can succeed. Overflow therefore vetoes the
|
|
2738
|
+
// transient/usage-limit arms, which a provider blob can trip on
|
|
2739
|
+
// coincidence alone.
|
|
2732
2740
|
const shouldRetry =
|
|
2733
2741
|
retrySettings.enabled &&
|
|
2734
2742
|
attempt < retrySettings.maxRetries &&
|
|
2743
|
+
!AIError.is(id, AIError.Flag.ContextOverflow) &&
|
|
2735
2744
|
(retryAfterMs !== undefined ||
|
|
2736
2745
|
AIError.is(id, AIError.Flag.Transient) ||
|
|
2737
2746
|
AIError.is(id, AIError.Flag.UsageLimit));
|
|
@@ -1950,6 +1950,21 @@ export class SessionManager {
|
|
|
1950
1950
|
return this.#sessionFile;
|
|
1951
1951
|
}
|
|
1952
1952
|
|
|
1953
|
+
/**
|
|
1954
|
+
* Whether the current session has actually been materialized to durable
|
|
1955
|
+
* storage (the JSONL exists on disk / in the active storage backend).
|
|
1956
|
+
*
|
|
1957
|
+
* Session persistence is lazy: the file is only written once the history
|
|
1958
|
+
* contains an assistant message (or an explicit {@link ensureOnDisk}
|
|
1959
|
+
* caller forces it). Until then {@link getSessionFile} returns an allocated
|
|
1960
|
+
* path that leads nowhere, so a `--resume <id>` hint built from it would
|
|
1961
|
+
* always fail. Consumers that advertise a resume command must gate on this
|
|
1962
|
+
* (issue #8860).
|
|
1963
|
+
*/
|
|
1964
|
+
isSessionOnDisk(): boolean {
|
|
1965
|
+
return !!this.#sessionFile && this.#storage.existsSync(this.#sessionFile);
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1953
1968
|
getArtifactsDir(): string | null {
|
|
1954
1969
|
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager.dir;
|
|
1955
1970
|
return artifactsDirectoryFor(this.#sessionFile);
|
|
@@ -22,6 +22,12 @@ interface PendingContextSnapshot {
|
|
|
22
22
|
promptTokens: number;
|
|
23
23
|
nonMessageTokens: number;
|
|
24
24
|
cutoffCount: number;
|
|
25
|
+
/**
|
|
26
|
+
* Compaction epoch at rebase time. Distinguishes a genuinely fresh in-turn
|
|
27
|
+
* anchor (same epoch) from a post-cutoff anchor that predates a mid-run
|
|
28
|
+
* compaction (older epoch) so the latter never out-ranks this snapshot.
|
|
29
|
+
*/
|
|
30
|
+
epoch: number;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/** Capabilities the stats tracker borrows from its owning session. */
|
|
@@ -44,6 +50,7 @@ export class SessionStatsTracker {
|
|
|
44
50
|
readonly #host: SessionStatsTrackerHost;
|
|
45
51
|
#pendingContextSnapshot: PendingContextSnapshot | undefined;
|
|
46
52
|
#contextUsageRevision = 0;
|
|
53
|
+
#compactionEpoch = 0;
|
|
47
54
|
|
|
48
55
|
constructor(host: SessionStatsTrackerHost) {
|
|
49
56
|
this.#host = host;
|
|
@@ -162,8 +169,11 @@ export class SessionStatsTracker {
|
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
171
|
|
|
172
|
+
const anchorEpoch = anchorAssistant?.contextSnapshot?.compactionEpoch ?? 0;
|
|
165
173
|
const useAnchor =
|
|
166
|
-
anchorAssistant !== undefined &&
|
|
174
|
+
anchorAssistant !== undefined &&
|
|
175
|
+
anchorIndex !== -1 &&
|
|
176
|
+
(!pending || (anchorIndex >= pending.cutoffCount && anchorEpoch >= pending.epoch));
|
|
167
177
|
if (useAnchor && anchorAssistant) {
|
|
168
178
|
const promptTokens = correctedPromptTokens(anchorAssistant);
|
|
169
179
|
const nonMessageTokens =
|
|
@@ -255,6 +265,15 @@ export class SessionStatsTracker {
|
|
|
255
265
|
return this.#contextUsageRevision;
|
|
256
266
|
}
|
|
257
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Monotonic compaction epoch, bumped whenever history is compacted. Stamped
|
|
270
|
+
* onto each assistant snapshot at record time so {@link getContextBreakdown}
|
|
271
|
+
* can reject a post-cutoff anchor whose usage predates the last compaction.
|
|
272
|
+
*/
|
|
273
|
+
get compactionEpoch(): number {
|
|
274
|
+
return this.#compactionEpoch;
|
|
275
|
+
}
|
|
276
|
+
|
|
258
277
|
/** Non-message token count captured for the active provider request. */
|
|
259
278
|
get pendingNonMessageTokens(): number | undefined {
|
|
260
279
|
return this.#pendingContextSnapshot?.nonMessageTokens;
|
|
@@ -292,6 +311,7 @@ export class SessionStatsTracker {
|
|
|
292
311
|
assistant.contextSnapshot = {
|
|
293
312
|
promptTokens: calculatePromptTokens(assistant.usage),
|
|
294
313
|
nonMessageTokens: computeNonMessageTokens(this.#host.session),
|
|
314
|
+
compactionEpoch: this.#compactionEpoch,
|
|
295
315
|
};
|
|
296
316
|
}
|
|
297
317
|
const snapshot = assistant.contextSnapshot;
|
|
@@ -302,13 +322,14 @@ export class SessionStatsTracker {
|
|
|
302
322
|
}
|
|
303
323
|
|
|
304
324
|
/** Sets or clears the in-flight context snapshot. */
|
|
305
|
-
setPendingSnapshot(snapshot: PendingContextSnapshot | undefined): void {
|
|
306
|
-
this.#pendingContextSnapshot = snapshot;
|
|
325
|
+
setPendingSnapshot(snapshot: Omit<PendingContextSnapshot, "epoch"> | undefined): void {
|
|
326
|
+
this.#pendingContextSnapshot = snapshot ? { ...snapshot, epoch: this.#compactionEpoch } : undefined;
|
|
307
327
|
this.#contextUsageRevision++;
|
|
308
328
|
}
|
|
309
329
|
|
|
310
330
|
/** Recomputes an in-flight snapshot after history is compacted or rewritten. */
|
|
311
331
|
rebaseAfterCompaction(): void {
|
|
332
|
+
this.#compactionEpoch++;
|
|
312
333
|
if (!this.#pendingContextSnapshot) return;
|
|
313
334
|
const nonMessageTokens = computeNonMessageTokens(this.#host.session);
|
|
314
335
|
const messages = this.#host.agent.state.messages;
|
|
@@ -41,6 +41,12 @@ export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn =
|
|
|
41
41
|
: model.api === "openai-responses"
|
|
42
42
|
? settings.get("textVerbosity")
|
|
43
43
|
: undefined;
|
|
44
|
+
// "auto" leaves the option unset so provider defaults and the
|
|
45
|
+
// PI_CACHE_RETENTION env override keep working; anything else is an
|
|
46
|
+
// explicit per-request retention (long restores 1h Anthropic TTLs and
|
|
47
|
+
// implicitly disables the short-entry keep-alive refresh loop).
|
|
48
|
+
const cacheRetentionSetting = settings.get("providers.cacheRetention");
|
|
49
|
+
const cacheRetention = cacheRetentionSetting === "auto" ? undefined : cacheRetentionSetting;
|
|
44
50
|
const streamFirstEventTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamFirstEventTimeoutSeconds"));
|
|
45
51
|
const streamIdleTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamIdleTimeoutSeconds"));
|
|
46
52
|
// Server-side fallback (opt-in): when the user enables it AND the
|
|
@@ -60,6 +66,7 @@ export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn =
|
|
|
60
66
|
openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant,
|
|
61
67
|
antigravityEndpointMode: streamOptions?.antigravityEndpointMode ?? antigravityEndpointMode,
|
|
62
68
|
textVerbosity: streamOptions?.textVerbosity ?? textVerbosity,
|
|
69
|
+
cacheRetention: streamOptions?.cacheRetention ?? cacheRetention,
|
|
63
70
|
streamFirstEventTimeoutMs: streamOptions?.streamFirstEventTimeoutMs ?? streamFirstEventTimeoutMs,
|
|
64
71
|
streamIdleTimeoutMs: streamOptions?.streamIdleTimeoutMs ?? streamIdleTimeoutMs,
|
|
65
72
|
maxRetryDelayMs: streamOptions?.maxRetryDelayMs ?? settings.get("retry.maxDelayMs"),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Compact title-model input for a user-invoked `/skill:<name>` prompt. */
|
|
2
|
+
export function skillPromptTitleInput(input: { name?: string; args?: string; queueChipText?: string }): string {
|
|
3
|
+
const chip = input.queueChipText?.trim();
|
|
4
|
+
if (chip) return chip;
|
|
5
|
+
const name = input.name?.trim();
|
|
6
|
+
const args = input.args?.trim();
|
|
7
|
+
if (name && args) return `/skill:${name} ${args}`;
|
|
8
|
+
if (name) return `/skill:${name}`;
|
|
9
|
+
return args ?? "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Title text for a persisted skill-prompt custom message. Never the expanded SKILL.md body. */
|
|
13
|
+
export function titleTextFromSkillPrompt(message: {
|
|
14
|
+
role: string;
|
|
15
|
+
customType?: string;
|
|
16
|
+
attribution?: string;
|
|
17
|
+
details?: unknown;
|
|
18
|
+
}): string | undefined {
|
|
19
|
+
if (message.role !== "custom" || message.customType !== "skill-prompt" || message.attribution !== "user") {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
let name: string | undefined;
|
|
23
|
+
let args: string | undefined;
|
|
24
|
+
let queueChipText: string | undefined;
|
|
25
|
+
if (message.details && typeof message.details === "object") {
|
|
26
|
+
const details = message.details as Record<string, unknown>;
|
|
27
|
+
if (typeof details.name === "string") name = details.name;
|
|
28
|
+
if (typeof details.args === "string") args = details.args;
|
|
29
|
+
if (typeof details.__queueChipText === "string") queueChipText = details.__queueChipText;
|
|
30
|
+
}
|
|
31
|
+
return skillPromptTitleInput({ name, args, queueChipText }) || undefined;
|
|
32
|
+
}
|
|
@@ -20,7 +20,6 @@ import type {
|
|
|
20
20
|
} from "@oh-my-pi/pi-ai";
|
|
21
21
|
import { calculateRateLimitBackoffMs, parseRateLimitReason } from "@oh-my-pi/pi-ai";
|
|
22
22
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
23
|
-
import { kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
24
23
|
import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
|
|
25
24
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
26
25
|
import { extractRetryHint, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
@@ -1177,25 +1176,15 @@ export class TurnRecovery {
|
|
|
1177
1176
|
if (!reasonlessAbort && !streamStall && !transportReset) return undefined;
|
|
1178
1177
|
if (reasonlessAbort && genericAbort) message.errorId = AIError.create(AIError.Flag.Abort);
|
|
1179
1178
|
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
1183
|
-
//
|
|
1184
|
-
//
|
|
1185
|
-
// the
|
|
1186
|
-
// irrelevant. An HTTP/2 RST_STREAM / NGHTTP2_* close also ends the Connect
|
|
1187
|
-
// stream, so there is no in-flight server exec to race — unmarked MCP/todo
|
|
1188
|
-
// blocks are safe to continue once every emitted call has a result.
|
|
1179
|
+
// Idle stall and HTTP/2 RST both close the Cursor Connect stream:
|
|
1180
|
+
// the lazy watchdog aborts the request signal, and cursor.ts then
|
|
1181
|
+
// calls `h2Request.close()`. There is no in-flight server exec to
|
|
1182
|
+
// race, so unmarked MCP/todo blocks can continue once every emitted
|
|
1183
|
+
// call has a matching result. A reasonless abort ends the turn and
|
|
1184
|
+
// the agent loop pairs leftover calls with `executed: false`.
|
|
1189
1185
|
const resolvedToolCallIds: string[] = [];
|
|
1190
1186
|
for (const block of message.content) {
|
|
1191
1187
|
if (block.type !== "toolCall") continue;
|
|
1192
|
-
if (
|
|
1193
|
-
streamStall &&
|
|
1194
|
-
message.provider === "cursor" &&
|
|
1195
|
-
(!(kCursorExecResolved in block) || block[kCursorExecResolved] !== true)
|
|
1196
|
-
) {
|
|
1197
|
-
return undefined;
|
|
1198
|
-
}
|
|
1199
1188
|
resolvedToolCallIds.push(block.id);
|
|
1200
1189
|
}
|
|
1201
1190
|
if (resolvedToolCallIds.length === 0) return undefined;
|
|
@@ -1660,6 +1649,9 @@ export class TurnRecovery {
|
|
|
1660
1649
|
if (AIError.isContextOverflow(message, model.contextWindow ?? 0)) return false;
|
|
1661
1650
|
if (AIError.is(id, AIError.Flag.UsageLimit)) return false;
|
|
1662
1651
|
if (AIError.is(id, AIError.Flag.AuthFailed)) return false;
|
|
1652
|
+
// A thinking loop is a same-model resample signal, not a router fault, so a
|
|
1653
|
+
// base-model swap would abandon the loop-guard redirect (issue #8760).
|
|
1654
|
+
if (AIError.is(id, AIError.Flag.ThinkingLoop)) return false;
|
|
1663
1655
|
return this.#host.modelRegistry.find("fireworks", toFireworksBaseModelId(model.id)) !== undefined;
|
|
1664
1656
|
}
|
|
1665
1657
|
|
|
@@ -1969,10 +1961,23 @@ export class TurnRecovery {
|
|
|
1969
1961
|
);
|
|
1970
1962
|
if (switchedCredential) delayMs = 0;
|
|
1971
1963
|
}
|
|
1964
|
+
// A thinking-loop abort is not a provider failure — it is the loop guard
|
|
1965
|
+
// asking for a same-model resample, paired with a hidden
|
|
1966
|
+
// `thinking-loop-redirect` notice that only makes sense on the model that
|
|
1967
|
+
// looped. Walking `fallbackChains` (or parking the selector on a cooldown)
|
|
1968
|
+
// would swap a healthy planning turn to another family based on chain
|
|
1969
|
+
// contents, not model health (issue #8760). Keep it on the same model; the
|
|
1970
|
+
// retry budget still bounds a genuinely stuck stream.
|
|
1971
|
+
const thinkingLoop = AIError.is(id, AIError.Flag.ThinkingLoop);
|
|
1972
1972
|
if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
|
|
1973
1973
|
// A refusal chain stops at the retry budget: the exhausted-attempt
|
|
1974
1974
|
// last resort is for provider failures, not classifier decisions.
|
|
1975
|
-
if (
|
|
1975
|
+
if (
|
|
1976
|
+
allowModelFallback &&
|
|
1977
|
+
retrySettings.modelFallback &&
|
|
1978
|
+
!thinkingLoop &&
|
|
1979
|
+
!(retryBudgetExhausted && classifierRefusal)
|
|
1980
|
+
) {
|
|
1976
1981
|
if (!classifierRefusal) {
|
|
1977
1982
|
this.noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
1978
1983
|
}
|
|
@@ -108,17 +108,20 @@ export const SMOKE_TEST_TIMEOUT_MS = 30_000;
|
|
|
108
108
|
/**
|
|
109
109
|
* Resolve the command used to relaunch the agent CLI into worker mode. In a
|
|
110
110
|
* compiled binary the entry point is the binary itself; otherwise re-enter the
|
|
111
|
-
* declared worker-host entry
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
111
|
+
* declared worker-host entry by absolute path. Workers deliberately spawn
|
|
112
|
+
* without a pinned cwd there: they share the parent's foreground process
|
|
113
|
+
* group, and terminal cwd heuristics (kitty's new_tab_with_cwd) read the
|
|
114
|
+
* newest process in that group, so anchoring them to the install dir leaks
|
|
115
|
+
* into newly opened terminal tabs. With no declared host entry (bun test, SDK
|
|
116
|
+
* embedding) fall back to a cwd-relative `src/cli.ts`, which Bun subprocess
|
|
117
|
+
* IPC handles more reliably under `bun test`.
|
|
115
118
|
*/
|
|
116
119
|
export function resolveWorkerSpawnCmd(workerArg: string): WorkerSpawnCommand {
|
|
117
120
|
const executable = stripWindowsExtendedLengthPathPrefix(process.execPath);
|
|
118
121
|
if (isCompiledBinary()) return { cmd: [executable, workerArg] };
|
|
119
122
|
const hostEntry = workerHostEntry();
|
|
120
123
|
if (hostEntry) {
|
|
121
|
-
return { cmd: [executable,
|
|
124
|
+
return { cmd: [executable, hostEntry, workerArg] };
|
|
122
125
|
}
|
|
123
126
|
const packageRoot = path.resolve(import.meta.dir, "..", "..");
|
|
124
127
|
return { cmd: [executable, "src/cli.ts", workerArg], cwd: packageRoot };
|
package/src/task/executor.ts
CHANGED
|
@@ -35,6 +35,7 @@ import type { HindsightSessionState } from "../hindsight/state";
|
|
|
35
35
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
36
36
|
import type { MCPManager } from "../mcp/manager";
|
|
37
37
|
import type { MnemopiSessionState } from "../mnemopi/state";
|
|
38
|
+
import { initializeExtensions } from "../modes/runtime-init";
|
|
38
39
|
import subagentAsyncPendingTemplate from "../prompts/system/subagent-async-pending.md" with { type: "text" };
|
|
39
40
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
40
41
|
import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
|
|
@@ -3167,6 +3168,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
3167
3168
|
const { session: revived } = await createAgentSession(
|
|
3168
3169
|
buildSubagentSessionOptions(reopened, expectedAgentRef),
|
|
3169
3170
|
);
|
|
3171
|
+
// Re-run the executor's extension wiring on the rebuilt session.
|
|
3172
|
+
// Skipping it leaves the runner pre-init, so a `tool_call` handler
|
|
3173
|
+
// touching a runtime action trips the fail-closed gate and blocks
|
|
3174
|
+
// every tool (including `yield`) in the revived agent (issue #8824).
|
|
3175
|
+
await initializeExtensions(revived, {
|
|
3176
|
+
reportSendError: (action, err) =>
|
|
3177
|
+
logger.error("Extension send failed", { action, error: err.message }),
|
|
3178
|
+
reportRuntimeError: err =>
|
|
3179
|
+
logger.error("Extension error", { path: err.extensionPath, error: err.error }),
|
|
3180
|
+
});
|
|
3170
3181
|
AgentRegistry.global().syncSessionStatus(id, revived);
|
|
3171
3182
|
installIrcWakeTurnMonitor(revived);
|
|
3172
3183
|
return revived;
|
package/src/task/index.ts
CHANGED
|
@@ -1424,6 +1424,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1424
1424
|
...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
|
|
1425
1425
|
...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
|
|
1426
1426
|
...(params.effort !== undefined ? { effort: params.effort } : {}),
|
|
1427
|
+
// `name` is the spawn handle: keep it for id allocation when this
|
|
1428
|
+
// path did not pre-reserve one. Do not treat it as a HUD description.
|
|
1427
1429
|
identity: { id: preAllocatedId, label: params.name },
|
|
1428
1430
|
index: spawnIndex,
|
|
1429
1431
|
parentToolCallId: toolCallId,
|
package/src/task/label.ts
CHANGED
|
@@ -9,6 +9,17 @@ import { generateSessionTitle } from "../utils/title-generator";
|
|
|
9
9
|
|
|
10
10
|
const TASK_LABEL_SYSTEM_PROMPT = prompt.render(taskLabelSystemPrompt);
|
|
11
11
|
|
|
12
|
+
/** True when a generated label is just the spawn handle, including `Name-2`. */
|
|
13
|
+
export function labelEchoesHandle(handle: string | undefined, label: string): boolean {
|
|
14
|
+
if (!handle) return false;
|
|
15
|
+
if (label.localeCompare(handle, undefined, { sensitivity: "accent" }) === 0) return true;
|
|
16
|
+
const separator = handle.lastIndexOf("-");
|
|
17
|
+
if (separator <= 0) return false;
|
|
18
|
+
const prefix = handle.slice(0, separator);
|
|
19
|
+
const suffix = handle.slice(separator + 1);
|
|
20
|
+
return /^\d+$/.test(suffix) && prefix.localeCompare(label, undefined, { sensitivity: "accent" }) === 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
/** Compresses a delegated assignment into a one-sentence UI label via the tiny title model — fired by the executor spawn path because the task wire schema no longer carries a `description`; null on empty input or failure. */
|
|
13
24
|
export async function generateTaskLabel(
|
|
14
25
|
assignment: string,
|
|
@@ -20,7 +31,7 @@ export async function generateTaskLabel(
|
|
|
20
31
|
const text = assignment.trim();
|
|
21
32
|
if (!text) return null;
|
|
22
33
|
try {
|
|
23
|
-
|
|
34
|
+
const label = await generateSessionTitle(
|
|
24
35
|
text,
|
|
25
36
|
registry,
|
|
26
37
|
settings,
|
|
@@ -30,6 +41,8 @@ export async function generateTaskLabel(
|
|
|
30
41
|
TASK_LABEL_SYSTEM_PROMPT,
|
|
31
42
|
signal,
|
|
32
43
|
);
|
|
44
|
+
if (!label || labelEchoesHandle(sessionId, label)) return null;
|
|
45
|
+
return label;
|
|
33
46
|
} catch (err) {
|
|
34
47
|
logger.debug("task-label: generation failed", {
|
|
35
48
|
sessionId,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import type { ModelRegistry } from "../config/model-registry";
|
|
3
4
|
import { formatModelRoleAlias } from "../config/model-roles";
|
|
4
5
|
import type { Settings } from "../config/settings";
|
|
5
6
|
import { MCPManager } from "../mcp/manager";
|
|
7
|
+
import { initializeExtensions } from "../modes/runtime-init";
|
|
6
8
|
import type { PersistedSubagentReviverFactory } from "../registry/agent-lifecycle";
|
|
7
9
|
import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
8
10
|
import { createAgentSession } from "../sdk";
|
|
@@ -153,6 +155,17 @@ export function createPersistedSubagentReviverFactory(
|
|
|
153
155
|
// `alwaysInclude` can re-add non-defaultInactive extension/custom tools
|
|
154
156
|
// the original run didn't carry. Unknown/missing names are ignored.
|
|
155
157
|
await session.setActiveToolsByName([...init.tools, ...session.getMountedXdevToolNames()]);
|
|
158
|
+
// Wire the extension runtime exactly as the live executor does. Without
|
|
159
|
+
// this the runner stays pre-init, every action method throws
|
|
160
|
+
// `ExtensionRuntimeNotInitializedError`, and a `tool_call` handler that
|
|
161
|
+
// touches a runtime action trips the fail-closed gate in `emitToolCall`,
|
|
162
|
+
// blocking every tool — including the hidden `yield` — in the revived
|
|
163
|
+
// agent. `session_start` also re-runs so extensions restore per-session
|
|
164
|
+
// state (issue #8824).
|
|
165
|
+
await initializeExtensions(session, {
|
|
166
|
+
reportSendError: (action, err) => logger.error("Extension send failed", { action, error: err.message }),
|
|
167
|
+
reportRuntimeError: err => logger.error("Extension error", { path: err.extensionPath, error: err.error }),
|
|
168
|
+
});
|
|
156
169
|
// Cold revives must drive registry status themselves — createAgentSession
|
|
157
170
|
// doesn't wire this generically (the live path does it in the executor).
|
|
158
171
|
// The internal run-state signal precedes deferrable public `agent_end`,
|
package/src/task/render.ts
CHANGED
|
@@ -710,7 +710,7 @@ function formatAgentHeaderLabel(args: Partial<TaskParams> | undefined): string |
|
|
|
710
710
|
}
|
|
711
711
|
|
|
712
712
|
/** Dim `⟨agent⟩` badge for a non-default agent type; empty for the generic worker. */
|
|
713
|
-
function agentTypeBadge(agent: string | undefined, theme: Theme): string {
|
|
713
|
+
export function agentTypeBadge(agent: string | undefined, theme: Theme): string {
|
|
714
714
|
const trimmed = agent?.trim();
|
|
715
715
|
if (!trimmed || trimmed === "task") return "";
|
|
716
716
|
return ` ${theme.fg("dim", `${theme.format.bracketLeft}${trimmed}${theme.format.bracketRight}`)}`;
|
|
@@ -245,6 +245,7 @@ function assertDepthAndSpawnAllowed(request: StructuredSubagentRequest, agentNam
|
|
|
245
245
|
export async function resolveEffectiveSubagentPolicy(
|
|
246
246
|
request: StructuredSubagentRequest,
|
|
247
247
|
): Promise<EffectiveSubagentPolicy> {
|
|
248
|
+
await request.session.settings.reloadFromDisk();
|
|
248
249
|
const spawnPolicy = resolveSpawnPolicy(request.session.getSessionSpawns());
|
|
249
250
|
const agentName = request.agent?.trim() || spawnPolicy.defaultAgent;
|
|
250
251
|
const planMode = request.session.getPlanModeState?.()?.enabled === true;
|
|
@@ -393,7 +394,9 @@ function buildExecutorOptions(
|
|
|
393
394
|
assignment: request.assignment.trim(),
|
|
394
395
|
context: request.context?.trim() || undefined,
|
|
395
396
|
planReference: undefined,
|
|
396
|
-
|
|
397
|
+
// Task `name` is the spawn handle (id allocation). Eval `label` is a
|
|
398
|
+
// real UI description. Copy it only for eval so generateTaskLabel can run.
|
|
399
|
+
description: request.invocationKind === "eval" ? trimToUndefined(request.identity?.label) : undefined,
|
|
397
400
|
index: request.index ?? 0,
|
|
398
401
|
parentToolCallId: request.parentToolCallId,
|
|
399
402
|
detached: request.detached,
|
|
@@ -476,7 +479,7 @@ function buildFailureResult(
|
|
|
476
479
|
agentSource: policy.agent.source,
|
|
477
480
|
task: renderSubagentPrompt(request.assignment),
|
|
478
481
|
assignment: request.assignment.trim(),
|
|
479
|
-
description: trimToUndefined(request.identity?.label),
|
|
482
|
+
description: request.invocationKind === "eval" ? trimToUndefined(request.identity?.label) : undefined,
|
|
480
483
|
exitCode: 1,
|
|
481
484
|
output: "",
|
|
482
485
|
stderr: message,
|