@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.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 +40 -0
- package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-d9xenpn9.md} +40 -0
- package/dist/cli.js +3025 -3016
- package/dist/types/capability/mcp.d.ts +11 -0
- package/dist/types/capability/skill.d.ts +6 -0
- package/dist/types/cli/command-help.d.ts +3 -0
- package/dist/types/commands/share.d.ts +25 -0
- package/dist/types/commit/agentic/index.d.ts +3 -1
- package/dist/types/commit/execute.d.ts +32 -0
- package/dist/types/commit/index.d.ts +2 -1
- package/dist/types/commit/pipeline.d.ts +7 -1
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +1 -34
- package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
- package/dist/types/discovery/agent-plugins.d.ts +1 -0
- package/dist/types/discovery/contained-path.d.ts +34 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -2
- package/dist/types/extensibility/skills.d.ts +5 -0
- package/dist/types/mcp/transports/header-policy.d.ts +45 -0
- package/dist/types/mcp/types.d.ts +15 -0
- package/dist/types/modes/components/agent-hub.d.ts +2 -0
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -2
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
- package/dist/types/secrets/index.d.ts +23 -1
- package/dist/types/session/agent-session-events.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/prewalk.d.ts +3 -1
- package/dist/types/session/session-tools.d.ts +8 -0
- package/dist/types/session/turn-recovery.d.ts +7 -2
- package/dist/types/task/index.d.ts +2 -0
- package/dist/types/tools/shell-tokenize.d.ts +16 -0
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +4 -1
- package/src/capability/mcp.ts +11 -0
- package/src/capability/skill.ts +6 -0
- package/src/cli/command-help.ts +4 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli-commands.ts +5 -0
- package/src/commands/commit.ts +16 -3
- package/src/commands/share.ts +71 -0
- package/src/commit/agentic/index.ts +39 -21
- package/src/commit/execute.ts +56 -0
- package/src/commit/index.ts +2 -1
- package/src/commit/pipeline.ts +20 -7
- package/src/config/model-discovery.ts +1 -1
- package/src/config/model-registry.ts +21 -1
- package/src/config/settings-schema.ts +2 -33
- package/src/config/settings.ts +46 -0
- package/src/discovery/agent-plugin-format.ts +551 -0
- package/src/discovery/agent-plugins.ts +341 -0
- package/src/discovery/claude-plugins.ts +21 -5
- package/src/discovery/contained-path.ts +78 -0
- package/src/discovery/helpers.ts +23 -9
- package/src/discovery/index.ts +1 -0
- package/src/discovery/omp-plugins.ts +20 -7
- package/src/exec/non-interactive-env.ts +1 -0
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +78 -14
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +12 -0
- package/src/extensibility/plugins/marketplace/manager.ts +2 -1
- package/src/extensibility/shared-events.ts +3 -2
- package/src/extensibility/skills.ts +9 -0
- package/src/internal-urls/skill-protocol.ts +14 -0
- package/src/internal-urls/vault-protocol.ts +2 -2
- package/src/launch/client.ts +11 -1
- package/src/launch/protocol.ts +7 -2
- package/src/main.ts +3 -2
- package/src/mcp/config.ts +3 -0
- package/src/mcp/manager.ts +12 -9
- package/src/mcp/transports/header-policy.ts +95 -0
- package/src/mcp/transports/http.ts +35 -52
- package/src/mcp/transports/sse.ts +20 -27
- package/src/mcp/types.ts +15 -0
- package/src/modes/acp/acp-agent.ts +15 -4
- package/src/modes/components/agent-dashboard.ts +2 -0
- package/src/modes/components/agent-hub.ts +5 -0
- package/src/modes/components/agent-transcript-viewer.ts +3 -0
- package/src/modes/components/chat-transcript-builder.ts +3 -0
- package/src/modes/components/custom-editor.ts +0 -9
- package/src/modes/components/status-line/component.ts +1 -0
- package/src/modes/components/status-line/segments.ts +6 -4
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tool-execution.ts +13 -16
- package/src/modes/components/tree-selector.ts +62 -3
- package/src/modes/controllers/command-controller.ts +4 -1
- package/src/modes/controllers/event-controller.ts +15 -15
- package/src/modes/controllers/input-controller.ts +20 -2
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/utils/transcript-render-helpers.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/sdk.ts +6 -49
- package/src/secrets/index.ts +52 -1
- package/src/session/agent-session-events.ts +2 -2
- package/src/session/agent-session.ts +33 -14
- package/src/session/messages.ts +1 -0
- package/src/session/prewalk.ts +57 -17
- package/src/session/session-context.ts +3 -5
- package/src/session/session-handoff.ts +5 -1
- package/src/session/session-tools.ts +37 -3
- package/src/session/todo-tracker.ts +11 -1
- package/src/session/turn-recovery.ts +94 -72
- package/src/slash-commands/builtin-registry.ts +12 -8
- package/src/task/index.ts +21 -6
- package/src/tools/bash-skill-urls.ts +15 -0
- package/src/tools/bash.ts +10 -11
- package/src/tools/debug.ts +1 -1
- package/src/tools/jtd-to-json-schema.ts +123 -21
- package/src/tools/read.ts +5 -4
- package/src/tools/shell-tokenize.ts +98 -0
- package/src/web/search/providers/exa.ts +1 -1
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
9
9
|
isCustomMessageContent,
|
|
10
10
|
normalizeCustomMessagePayload,
|
|
11
|
+
PREWALK_PLAN_MESSAGE_TYPE,
|
|
11
12
|
} from "./messages";
|
|
12
13
|
import { type CompactionEntry, EPHEMERAL_MODEL_CHANGE_ROLE, type SessionEntry } from "./session-entries";
|
|
13
14
|
|
|
@@ -332,15 +333,12 @@ export function buildSessionContext(
|
|
|
332
333
|
const appendMessage = (entry: SessionEntry) => {
|
|
333
334
|
handleEntryResetTracking(entry);
|
|
334
335
|
if (entry.type === "message") {
|
|
335
|
-
if (
|
|
336
|
-
!options?.transcript &&
|
|
337
|
-
entry.message.role === "assistant" &&
|
|
338
|
-
entry.message.retryRecovery?.status === "recovered"
|
|
339
|
-
) {
|
|
336
|
+
if (!options?.transcript && entry.message.role === "assistant" && entry.message.retryRecovery) {
|
|
340
337
|
return;
|
|
341
338
|
}
|
|
342
339
|
pushMessage(entry.message);
|
|
343
340
|
} else if (entry.type === "custom_message") {
|
|
341
|
+
if (!options?.transcript && entry.customType === PREWALK_PLAN_MESSAGE_TYPE) return;
|
|
344
342
|
if (!isCustomMessageContent(entry.content)) return;
|
|
345
343
|
const normalized = normalizeCustomMessagePayload(entry);
|
|
346
344
|
const attribution = entry.attribution === undefined ? undefined : normalized.attribution;
|
|
@@ -309,7 +309,11 @@ export class SessionHandoff {
|
|
|
309
309
|
|
|
310
310
|
return { document: handoffText, savedPath };
|
|
311
311
|
} catch (error) {
|
|
312
|
-
|
|
312
|
+
// Only a genuine abort (user Esc or the source turn cancelling) is a
|
|
313
|
+
// cancellation. A provider that throws a name==="AbortError" error without the
|
|
314
|
+
// handoff signal being aborted (stall/idle timeout, nested resolution failure)
|
|
315
|
+
// is a real failure and must surface verbatim, not be masked as "cancelled".
|
|
316
|
+
if (handoffSignal.aborted) {
|
|
313
317
|
throw new Error("Handoff cancelled");
|
|
314
318
|
}
|
|
315
319
|
throw error;
|
|
@@ -198,6 +198,15 @@ export class SessionTools {
|
|
|
198
198
|
#presentationPinnedToolNames: ReadonlySet<string> | undefined;
|
|
199
199
|
#runtimeSelectedToolNames: ReadonlySet<string> | undefined;
|
|
200
200
|
#baseSystemPrompt: string[];
|
|
201
|
+
/**
|
|
202
|
+
* Per-turn system prompt returned by a `before_agent_start` extension hook
|
|
203
|
+
* ("replace the system prompt for this turn"). While set, base-prompt
|
|
204
|
+
* rebuilds keep this override on the agent instead of the rebuilt base, so a
|
|
205
|
+
* rebuild landing in the prompt window (compaction/promotion, memory
|
|
206
|
+
* promotion, MCP/RPC tool refresh, hindsight MM-TTL refresh) cannot silently
|
|
207
|
+
* drop it before the request. Cleared when the turn ends.
|
|
208
|
+
*/
|
|
209
|
+
#turnSystemPromptOverride: string[] | undefined;
|
|
201
210
|
#lastAppliedToolSignature: string | undefined;
|
|
202
211
|
/**
|
|
203
212
|
* `xd://` device names the current base system prompt renders in its catalog
|
|
@@ -262,6 +271,31 @@ export class SessionTools {
|
|
|
262
271
|
this.#baseSystemPrompt = prompt;
|
|
263
272
|
}
|
|
264
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Pushes `base` to the agent as the effective system prompt, unless an active
|
|
276
|
+
* per-turn {@link #turnSystemPromptOverride} takes precedence. Every base
|
|
277
|
+
* rebuild applies its result through here so a mid-turn rebuild preserves the
|
|
278
|
+
* override.
|
|
279
|
+
*/
|
|
280
|
+
#applyAgentSystemPrompt(base: string[]): void {
|
|
281
|
+
this.#host.agent.setSystemPrompt(this.#turnSystemPromptOverride ?? base);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Registers the per-turn `before_agent_start` system-prompt override and
|
|
286
|
+
* applies it to the agent. Base rebuilds during the turn preserve it until
|
|
287
|
+
* {@link clearTurnSystemPromptOverride}.
|
|
288
|
+
*/
|
|
289
|
+
setTurnSystemPromptOverride(prompt: string[]): void {
|
|
290
|
+
this.#turnSystemPromptOverride = prompt;
|
|
291
|
+
this.#host.agent.setSystemPrompt(prompt);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Drops the active per-turn override; later rebuilds fall back to the base prompt. */
|
|
295
|
+
clearTurnSystemPromptOverride(): void {
|
|
296
|
+
this.#turnSystemPromptOverride = undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
265
299
|
/** Skills currently rendered into the system prompt. */
|
|
266
300
|
get skills(): Skill[] {
|
|
267
301
|
return this.#skills;
|
|
@@ -690,7 +724,7 @@ export class SessionTools {
|
|
|
690
724
|
if (this.#lastAppliedToolSignature !== undefined) this.#host.clearInheritedProviderPromptCacheKey();
|
|
691
725
|
this.#baseSystemPrompt = rebuiltSystemPrompt;
|
|
692
726
|
this.#host.clearMemoryPromotionSnapshot();
|
|
693
|
-
this.#
|
|
727
|
+
this.#applyAgentSystemPrompt(this.#baseSystemPrompt);
|
|
694
728
|
this.#lastAppliedToolSignature = rebuiltSignature;
|
|
695
729
|
this.#promptModelKey = this.#currentPromptModelKey();
|
|
696
730
|
this.#basePromptXdevNames = new Set(rebuiltXdevCatalogNames);
|
|
@@ -1130,7 +1164,7 @@ export class SessionTools {
|
|
|
1130
1164
|
) {
|
|
1131
1165
|
this.#host.clearInheritedProviderPromptCacheKey();
|
|
1132
1166
|
}
|
|
1133
|
-
this.#
|
|
1167
|
+
this.#applyAgentSystemPrompt(this.#baseSystemPrompt);
|
|
1134
1168
|
this.#promptModelKey = this.#currentPromptModelKey();
|
|
1135
1169
|
// Refresh the cached signature so a subsequent `applyActiveToolsByName` with
|
|
1136
1170
|
// the same tool set does not re-rebuild on top of the explicit refresh we
|
|
@@ -1170,7 +1204,7 @@ export class SessionTools {
|
|
|
1170
1204
|
this.#host.captureMemoryPromotionSnapshot(previousBaseSystemPrompt);
|
|
1171
1205
|
const stablePrompt = [...previousBaseSystemPrompt, injected];
|
|
1172
1206
|
this.#baseSystemPrompt = stablePrompt;
|
|
1173
|
-
this.#
|
|
1207
|
+
this.#applyAgentSystemPrompt(stablePrompt);
|
|
1174
1208
|
return stablePrompt;
|
|
1175
1209
|
} catch (err) {
|
|
1176
1210
|
logger.debug("Memory backend beforeAgentStartPrompt failed", {
|
|
@@ -27,6 +27,15 @@ const QUESTION_PROMPT_RE =
|
|
|
27
27
|
const USER_DIRECTED_PROMPT_RE = /\b(?:you|your|we|our)\b/i;
|
|
28
28
|
const USER_RESPONSE_CUE_RE =
|
|
29
29
|
/^(?:please\s+)?(?:confirm|reply|choose|pick|decide|advise)\b|^(?:please\s+)?answer\b|^(?:please\s+)?(?:let\s+me\s+know|tell\s+me)\b/i;
|
|
30
|
+
/**
|
|
31
|
+
* A trailing question mark is the universal signal that a line is a question, but
|
|
32
|
+
* the English word/pronoun gates above exist to filter incidental "?" out of prose
|
|
33
|
+
* (e.g. a TypeScript `foo?: string` tail). Non-English text has no cheap word list,
|
|
34
|
+
* yet any non-ASCII character in a "?"/"?"-terminated line reliably marks it as
|
|
35
|
+
* genuine prose — CJK/Japanese/Korean, Spanish `¿…?`, accented Latin — so treat it
|
|
36
|
+
* as a real user-directed question. Fixes non-Latin prompts going undetected (#7803).
|
|
37
|
+
*/
|
|
38
|
+
const NON_ASCII_TEXT_RE = /[^\x00-\x7F]/;
|
|
30
39
|
|
|
31
40
|
interface PromptLine {
|
|
32
41
|
text: string;
|
|
@@ -361,7 +370,8 @@ function isQuestionPromptLine(line: string): boolean {
|
|
|
361
370
|
return (
|
|
362
371
|
candidate.hadPromptLabel ||
|
|
363
372
|
QUESTION_PROMPT_RE.test(candidate.text) ||
|
|
364
|
-
USER_DIRECTED_PROMPT_RE.test(candidate.text)
|
|
373
|
+
USER_DIRECTED_PROMPT_RE.test(candidate.text) ||
|
|
374
|
+
NON_ASCII_TEXT_RE.test(candidate.text)
|
|
365
375
|
);
|
|
366
376
|
}
|
|
367
377
|
|
|
@@ -28,7 +28,7 @@ import type { ModelRegistry } from "../config/model-registry";
|
|
|
28
28
|
import { formatModelStringWithRouting, resolveModelOverride } from "../config/model-resolver";
|
|
29
29
|
|
|
30
30
|
import type { Settings } from "../config/settings";
|
|
31
|
-
import type {
|
|
31
|
+
import type { RetryErrorUpdate } from "../extensibility/shared-events";
|
|
32
32
|
import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
|
|
33
33
|
import thinkingLoopRedirectTemplate from "../prompts/system/thinking-loop-redirect.md" with { type: "text" };
|
|
34
34
|
import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
|
|
@@ -161,7 +161,7 @@ export interface TurnRecoveryOptions {
|
|
|
161
161
|
initialRetryFallback?: InitialRetryFallbackState;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
type
|
|
164
|
+
type PendingRetryError = {
|
|
165
165
|
entryId: string;
|
|
166
166
|
persistenceKey: string;
|
|
167
167
|
recovery: AssistantRetryRecoveryKind;
|
|
@@ -184,7 +184,7 @@ export class TurnRecovery {
|
|
|
184
184
|
#retryResolve: (() => void) | undefined;
|
|
185
185
|
#activeRetryFallback: ActiveRetryFallbackState | undefined;
|
|
186
186
|
#usageReserveApprovedSelector: string | undefined;
|
|
187
|
-
#
|
|
187
|
+
#pendingRetryErrors: PendingRetryError[] = [];
|
|
188
188
|
#usageLimitOutcomes = new WeakMap<AssistantMessage, Promise<UsageLimitOutcome>>();
|
|
189
189
|
#emptyStopRetryCount = 0;
|
|
190
190
|
#unexpectedStopRetryCount = 0;
|
|
@@ -250,14 +250,17 @@ export class TurnRecovery {
|
|
|
250
250
|
role: this.#activeRetryFallback.role,
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
|
-
const
|
|
253
|
+
const retryErrors = await this.#markPendingRetryErrors({
|
|
254
|
+
status: "recovered",
|
|
255
|
+
supersedingMessage: message,
|
|
256
|
+
});
|
|
254
257
|
await this.#host.emitSessionEvent({
|
|
255
258
|
type: "auto_retry_end",
|
|
256
259
|
success: true,
|
|
257
260
|
attempt: this.#retryAttempt,
|
|
258
|
-
|
|
261
|
+
retryErrors,
|
|
259
262
|
});
|
|
260
|
-
this.#
|
|
263
|
+
this.#clearPendingRetryErrors();
|
|
261
264
|
this.#retryAttempt = 0;
|
|
262
265
|
}
|
|
263
266
|
|
|
@@ -272,7 +275,7 @@ export class TurnRecovery {
|
|
|
272
275
|
attempt,
|
|
273
276
|
finalError: message.errorMessage,
|
|
274
277
|
});
|
|
275
|
-
this.#
|
|
278
|
+
this.#clearPendingRetryErrors();
|
|
276
279
|
}
|
|
277
280
|
|
|
278
281
|
/** Persists an otherwise skipped terminal empty error turn. */
|
|
@@ -305,8 +308,13 @@ export class TurnRecovery {
|
|
|
305
308
|
return this.#runRecoveryCompactionWithRollback(reason, message, allowDefer, options);
|
|
306
309
|
}
|
|
307
310
|
|
|
308
|
-
/**
|
|
309
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Restores the configured primary after fallback cooldown expiry.
|
|
313
|
+
* @returns true when the active model was actually switched back to the
|
|
314
|
+
* primary, so callers can re-run the pre-send context-fit check against the
|
|
315
|
+
* reverted (possibly smaller) window before issuing the next request.
|
|
316
|
+
*/
|
|
317
|
+
maybeRestoreRetryFallbackPrimary(): Promise<boolean> {
|
|
310
318
|
return this.#maybeRestoreRetryFallbackPrimary();
|
|
311
319
|
}
|
|
312
320
|
|
|
@@ -381,8 +389,8 @@ export class TurnRecovery {
|
|
|
381
389
|
}
|
|
382
390
|
}
|
|
383
391
|
|
|
384
|
-
#
|
|
385
|
-
this.#
|
|
392
|
+
#clearPendingRetryErrors(): void {
|
|
393
|
+
this.#pendingRetryErrors = [];
|
|
386
394
|
}
|
|
387
395
|
|
|
388
396
|
/**
|
|
@@ -432,7 +440,7 @@ export class TurnRecovery {
|
|
|
432
440
|
return parts.join("; ");
|
|
433
441
|
}
|
|
434
442
|
|
|
435
|
-
async #
|
|
443
|
+
async #recordPendingRetryError(
|
|
436
444
|
message: AssistantMessage,
|
|
437
445
|
id: number,
|
|
438
446
|
options: { switchedCredential: boolean; switchedModel: boolean; delayMs: number },
|
|
@@ -451,11 +459,11 @@ export class TurnRecovery {
|
|
|
451
459
|
break;
|
|
452
460
|
}
|
|
453
461
|
if (!branchEntry) return;
|
|
454
|
-
if (this.#
|
|
462
|
+
if (this.#pendingRetryErrors.some(error => error.entryId === branchEntry.id)) return;
|
|
455
463
|
const rateLimited = AIError.is(id, AIError.Flag.UsageLimit);
|
|
456
464
|
const recovery = this.#retryRecoveryKind(id, options.switchedCredential, options.switchedModel, options.delayMs);
|
|
457
465
|
const note = this.#retryRecoveryNote(recovery, rateLimited);
|
|
458
|
-
this.#
|
|
466
|
+
this.#pendingRetryErrors.push({
|
|
459
467
|
entryId: branchEntry.id,
|
|
460
468
|
persistenceKey,
|
|
461
469
|
recovery,
|
|
@@ -464,24 +472,17 @@ export class TurnRecovery {
|
|
|
464
472
|
});
|
|
465
473
|
}
|
|
466
474
|
|
|
467
|
-
async #
|
|
468
|
-
|
|
475
|
+
async #markPendingRetryErrors(
|
|
476
|
+
completion: { status: "recovered"; supersedingMessage: AssistantMessage } | { status: "superseded" },
|
|
477
|
+
): Promise<RetryErrorUpdate[]> {
|
|
478
|
+
if (this.#pendingRetryErrors.length === 0) return [];
|
|
469
479
|
const branch = this.#host.sessionManager.getBranch();
|
|
470
480
|
const branchById = new Map<string, SessionEntry>();
|
|
471
481
|
for (const entry of branch) {
|
|
472
482
|
branchById.set(entry.id, entry);
|
|
473
483
|
}
|
|
474
|
-
const
|
|
475
|
-
const
|
|
476
|
-
timestamp: supersedingMessage.timestamp,
|
|
477
|
-
provider: supersedingMessage.provider,
|
|
478
|
-
model: supersedingMessage.model,
|
|
479
|
-
};
|
|
480
|
-
if (supersedingMessage.responseId) {
|
|
481
|
-
supersededBy.responseId = supersedingMessage.responseId;
|
|
482
|
-
}
|
|
483
|
-
const recoveredErrors: RecoveredRetryError[] = [];
|
|
484
|
-
for (const pending of this.#pendingRecoveredRetryErrors) {
|
|
484
|
+
const retryErrors: RetryErrorUpdate[] = [];
|
|
485
|
+
for (const pending of this.#pendingRetryErrors) {
|
|
485
486
|
let entry = branchById.get(pending.entryId);
|
|
486
487
|
if (entry?.type !== "message" || entry.message.role !== "assistant") {
|
|
487
488
|
entry = branch
|
|
@@ -495,27 +496,45 @@ export class TurnRecovery {
|
|
|
495
496
|
);
|
|
496
497
|
}
|
|
497
498
|
if (entry?.type !== "message" || entry.message.role !== "assistant") continue;
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
499
|
+
let retryRecovery: AssistantRetryRecovery;
|
|
500
|
+
if (completion.status === "recovered") {
|
|
501
|
+
retryRecovery = {
|
|
502
|
+
kind: "auto-retry",
|
|
503
|
+
status: "recovered",
|
|
504
|
+
attempt: pending.attempt,
|
|
505
|
+
recoveredAt: new Date().toISOString(),
|
|
506
|
+
recovery: pending.recovery,
|
|
507
|
+
note: pending.note,
|
|
508
|
+
supersededBy: {
|
|
509
|
+
timestamp: completion.supersedingMessage.timestamp,
|
|
510
|
+
...(completion.supersedingMessage.responseId === undefined
|
|
511
|
+
? {}
|
|
512
|
+
: { responseId: completion.supersedingMessage.responseId }),
|
|
513
|
+
provider: completion.supersedingMessage.provider,
|
|
514
|
+
model: completion.supersedingMessage.model,
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
} else {
|
|
518
|
+
retryRecovery = {
|
|
519
|
+
kind: "auto-retry",
|
|
520
|
+
status: "superseded",
|
|
521
|
+
attempt: pending.attempt,
|
|
522
|
+
recovery: pending.recovery,
|
|
523
|
+
note: pending.note,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
507
526
|
entry.message.retryRecovery = retryRecovery;
|
|
508
|
-
|
|
527
|
+
retryErrors.push({
|
|
509
528
|
entryId: entry.id,
|
|
510
529
|
persistenceKey: pending.persistenceKey,
|
|
511
530
|
note: retryRecovery.note,
|
|
512
531
|
retryRecovery,
|
|
513
532
|
});
|
|
514
533
|
}
|
|
515
|
-
if (
|
|
534
|
+
if (retryErrors.length > 0) {
|
|
516
535
|
await this.#host.sessionManager.rewriteEntries();
|
|
517
536
|
}
|
|
518
|
-
return
|
|
537
|
+
return retryErrors;
|
|
519
538
|
}
|
|
520
539
|
|
|
521
540
|
async #handleEmptyAssistantStop(assistantMessage: AssistantMessage): Promise<boolean> {
|
|
@@ -547,7 +566,7 @@ export class TurnRecovery {
|
|
|
547
566
|
attempt: this.#retryAttempt > 0 ? this.#retryAttempt : attempts,
|
|
548
567
|
finalError,
|
|
549
568
|
});
|
|
550
|
-
this.#
|
|
569
|
+
this.#clearPendingRetryErrors();
|
|
551
570
|
this.#retryAttempt = 0;
|
|
552
571
|
this.resolveRetry();
|
|
553
572
|
// A zero-content turn carries no transcript value, while its provider usage
|
|
@@ -1433,10 +1452,10 @@ export class TurnRecovery {
|
|
|
1433
1452
|
return true;
|
|
1434
1453
|
}
|
|
1435
1454
|
|
|
1436
|
-
async #maybeRestoreRetryFallbackPrimary(): Promise<
|
|
1437
|
-
if (!this.#activeRetryFallback) return;
|
|
1438
|
-
if (this.#activeRetryFallback.pinned) return;
|
|
1439
|
-
if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
|
|
1455
|
+
async #maybeRestoreRetryFallbackPrimary(): Promise<boolean> {
|
|
1456
|
+
if (!this.#activeRetryFallback) return false;
|
|
1457
|
+
if (this.#activeRetryFallback.pinned) return false;
|
|
1458
|
+
if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return false;
|
|
1440
1459
|
|
|
1441
1460
|
const {
|
|
1442
1461
|
originalSelector: originalSelectorRaw,
|
|
@@ -1446,19 +1465,19 @@ export class TurnRecovery {
|
|
|
1446
1465
|
const originalSelector = parseRetryFallbackSelector(originalSelectorRaw, this.#host.modelRegistry);
|
|
1447
1466
|
if (!originalSelector) {
|
|
1448
1467
|
this.clearActiveRetryFallback();
|
|
1449
|
-
return;
|
|
1468
|
+
return false;
|
|
1450
1469
|
}
|
|
1451
1470
|
|
|
1452
1471
|
const currentModel = this.#host.model();
|
|
1453
|
-
if (!currentModel) return;
|
|
1472
|
+
if (!currentModel) return false;
|
|
1454
1473
|
const currentSelector = formatRetryFallbackSelector(currentModel, this.#host.thinkingLevel());
|
|
1455
1474
|
if (currentSelector === originalSelector.raw) {
|
|
1456
1475
|
if (!this.isRetryFallbackSelectorSuppressed(originalSelector)) {
|
|
1457
1476
|
this.clearActiveRetryFallback();
|
|
1458
1477
|
}
|
|
1459
|
-
return;
|
|
1478
|
+
return false;
|
|
1460
1479
|
}
|
|
1461
|
-
if (this.isRetryFallbackSelectorSuppressed(originalSelector)) return;
|
|
1480
|
+
if (this.isRetryFallbackSelectorSuppressed(originalSelector)) return false;
|
|
1462
1481
|
|
|
1463
1482
|
const resolvedPrimary = resolveModelOverride(
|
|
1464
1483
|
[originalSelector.raw],
|
|
@@ -1467,9 +1486,9 @@ export class TurnRecovery {
|
|
|
1467
1486
|
);
|
|
1468
1487
|
const primaryModel =
|
|
1469
1488
|
resolvedPrimary.model ?? this.#host.modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
1470
|
-
if (!primaryModel) return;
|
|
1489
|
+
if (!primaryModel) return false;
|
|
1471
1490
|
const apiKey = await this.#host.modelRegistry.getApiKey(primaryModel, this.#host.sessionId());
|
|
1472
|
-
if (!apiKey) return;
|
|
1491
|
+
if (!apiKey) return false;
|
|
1473
1492
|
|
|
1474
1493
|
const currentThinkingLevel = this.#host.configuredThinkingLevel();
|
|
1475
1494
|
const thinkingToApply =
|
|
@@ -1480,6 +1499,7 @@ export class TurnRecovery {
|
|
|
1480
1499
|
this.#host.settings.getStorage()?.recordModelUsage(primarySelector);
|
|
1481
1500
|
this.#host.setThinkingLevel(thinkingToApply);
|
|
1482
1501
|
this.clearActiveRetryFallback();
|
|
1502
|
+
return true;
|
|
1483
1503
|
}
|
|
1484
1504
|
|
|
1485
1505
|
#parseRetryAfterMsFromError(errorMessage: string): number | undefined {
|
|
@@ -1581,27 +1601,27 @@ export class TurnRecovery {
|
|
|
1581
1601
|
|
|
1582
1602
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
1583
1603
|
const id = this.#classifyRetryMessage(message);
|
|
1604
|
+
const rateLimitReason = parseRateLimitReason(errorMessage);
|
|
1584
1605
|
const staleOpenAIResponsesReplayError = AIError.is(id, AIError.Flag.StaleResponsesItem);
|
|
1585
1606
|
const recordedUsageLimitOutcome = await this.#usageLimitOutcomes.get(message);
|
|
1586
1607
|
const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
|
|
1587
1608
|
let delayMs = staleOpenAIResponsesReplayError
|
|
1588
1609
|
? 0
|
|
1589
1610
|
: calculateRetryBackoffDelayMs(retrySettings.baseDelayMs, this.#retryAttempt);
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1596
|
-
// body merely mentions concurrency is still a usage limit (handled below),
|
|
1597
|
-
// so gate on the flag matching the rotation decision.
|
|
1611
|
+
// Transient rate/concurrency caps stay on the same credential, but must
|
|
1612
|
+
// honor their reason-specific windows. The default exponential base
|
|
1613
|
+
// (≈500ms, capped at 8s) otherwise re-hits the cap and burns the retry
|
|
1614
|
+
// budget before either window can clear. An explicit provider
|
|
1615
|
+
// retry-after is authoritative in both directions, so the heuristic
|
|
1616
|
+
// window only applies when the error carries no parsed timing.
|
|
1598
1617
|
if (
|
|
1599
1618
|
!staleOpenAIResponsesReplayError &&
|
|
1600
1619
|
!AIError.is(id, AIError.Flag.UsageLimit) &&
|
|
1601
|
-
|
|
1620
|
+
parsedRetryAfterMs === undefined &&
|
|
1621
|
+
(rateLimitReason === "CONCURRENT_LIMIT" || rateLimitReason === "RATE_LIMIT_EXCEEDED")
|
|
1602
1622
|
) {
|
|
1603
|
-
const
|
|
1604
|
-
if (
|
|
1623
|
+
const reasonBackoffMs = calculateRateLimitBackoffMs(rateLimitReason);
|
|
1624
|
+
if (reasonBackoffMs > delayMs) delayMs = reasonBackoffMs;
|
|
1605
1625
|
}
|
|
1606
1626
|
let switchedCredential = false;
|
|
1607
1627
|
let switchedModel = false;
|
|
@@ -1677,16 +1697,18 @@ export class TurnRecovery {
|
|
|
1677
1697
|
|
|
1678
1698
|
if (retryBudgetExhausted) {
|
|
1679
1699
|
if (!switchedModel) {
|
|
1700
|
+
const attempt = this.#retryAttempt - 1;
|
|
1701
|
+
message.errorMessage = `Retry budget exhausted after ${attempt} ${attempt === 1 ? "retry" : "retries"}: ${errorMessage}`;
|
|
1680
1702
|
await this.persistTerminalEmptyErrorTurn(message);
|
|
1681
|
-
|
|
1682
|
-
// final failure and reset.
|
|
1703
|
+
const retryErrors = await this.#markPendingRetryErrors({ status: "superseded" });
|
|
1683
1704
|
await this.#host.emitSessionEvent({
|
|
1684
1705
|
type: "auto_retry_end",
|
|
1685
1706
|
success: false,
|
|
1686
|
-
attempt
|
|
1687
|
-
finalError:
|
|
1707
|
+
attempt,
|
|
1708
|
+
finalError: errorMessage,
|
|
1709
|
+
retryErrors,
|
|
1688
1710
|
});
|
|
1689
|
-
this.#
|
|
1711
|
+
this.#clearPendingRetryErrors();
|
|
1690
1712
|
this.#retryAttempt = 0;
|
|
1691
1713
|
this.resolveRetry(); // Resolve so waitForRetry() completes
|
|
1692
1714
|
return false;
|
|
@@ -1711,7 +1733,7 @@ export class TurnRecovery {
|
|
|
1711
1733
|
attempt: this.#retryAttempt - 1,
|
|
1712
1734
|
finalError: errorMessage,
|
|
1713
1735
|
});
|
|
1714
|
-
this.#
|
|
1736
|
+
this.#clearPendingRetryErrors();
|
|
1715
1737
|
}
|
|
1716
1738
|
this.#retryAttempt = 0;
|
|
1717
1739
|
this.resolveRetry();
|
|
@@ -1736,7 +1758,7 @@ export class TurnRecovery {
|
|
|
1736
1758
|
attempt: this.#retryAttempt - 1,
|
|
1737
1759
|
finalError: errorMessage,
|
|
1738
1760
|
});
|
|
1739
|
-
this.#
|
|
1761
|
+
this.#clearPendingRetryErrors();
|
|
1740
1762
|
}
|
|
1741
1763
|
this.#retryAttempt = 0;
|
|
1742
1764
|
this.resolveRetry();
|
|
@@ -1761,12 +1783,12 @@ export class TurnRecovery {
|
|
|
1761
1783
|
attempt,
|
|
1762
1784
|
finalError: `Provider requested ${delayMs}ms wait, exceeds retry.maxDelayMs (${maxDelayMs}ms). Original error: ${errorMessage}`,
|
|
1763
1785
|
});
|
|
1764
|
-
this.#
|
|
1786
|
+
this.#clearPendingRetryErrors();
|
|
1765
1787
|
this.resolveRetry();
|
|
1766
1788
|
return false;
|
|
1767
1789
|
}
|
|
1768
1790
|
|
|
1769
|
-
await this.#
|
|
1791
|
+
await this.#recordPendingRetryError(message, id, { switchedCredential, switchedModel, delayMs });
|
|
1770
1792
|
|
|
1771
1793
|
await this.#host.emitSessionEvent({
|
|
1772
1794
|
type: "auto_retry_start",
|
|
@@ -1808,7 +1830,7 @@ export class TurnRecovery {
|
|
|
1808
1830
|
attempt,
|
|
1809
1831
|
finalError: "Retry cancelled",
|
|
1810
1832
|
});
|
|
1811
|
-
this.#
|
|
1833
|
+
this.#clearPendingRetryErrors();
|
|
1812
1834
|
this.resolveRetry();
|
|
1813
1835
|
return false;
|
|
1814
1836
|
}
|
|
@@ -1881,7 +1903,7 @@ export class TurnRecovery {
|
|
|
1881
1903
|
attempt,
|
|
1882
1904
|
finalError: `Retry continuation failed locally: ${localError}. Original error: ${message.errorMessage ?? "Unknown error"}`,
|
|
1883
1905
|
});
|
|
1884
|
-
this.#
|
|
1906
|
+
this.#clearPendingRetryErrors();
|
|
1885
1907
|
this.resolveRetry();
|
|
1886
1908
|
}
|
|
1887
1909
|
|
|
@@ -44,6 +44,7 @@ import type { SessionOAuthAccountList } from "../session/agent-session-types";
|
|
|
44
44
|
import { COMPACT_MODES, parseCompactArgs } from "../session/compact-modes";
|
|
45
45
|
import { resolveResumableSession } from "../session/session-listing";
|
|
46
46
|
import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
|
|
47
|
+
import { refreshAgentDiscovery } from "../task";
|
|
47
48
|
import type { ComputerTool } from "../tools/computer";
|
|
48
49
|
import { computerExposureMode } from "../tools/computer/exposure";
|
|
49
50
|
import { expandTilde, resolveToCwd } from "../tools/path-utils";
|
|
@@ -762,10 +763,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
762
763
|
if (!runtime.session.modelRegistry.hasConfiguredAuth(resolved.model)) {
|
|
763
764
|
return usage(`No API key for ${resolved.model.provider}/${resolved.model.id}`, runtime);
|
|
764
765
|
}
|
|
765
|
-
runtime.session.armPrewalk(resolved.model, resolved.thinkingLevel);
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
766
|
+
const armed = runtime.session.armPrewalk(resolved.model, resolved.thinkingLevel);
|
|
767
|
+
if (armed) {
|
|
768
|
+
await runtime.output(
|
|
769
|
+
`Prewalk on: switching to ${resolved.model.provider}/${resolved.model.id} at the next edit/write (todo-gated).`,
|
|
770
|
+
);
|
|
771
|
+
}
|
|
769
772
|
return commandConsumed();
|
|
770
773
|
},
|
|
771
774
|
},
|
|
@@ -3067,14 +3070,15 @@ export const BUILTIN_SLASH_COMMANDS_INTERNAL: ReadonlyArray<SlashCommandSpec> =
|
|
|
3067
3070
|
|
|
3068
3071
|
/**
|
|
3069
3072
|
* Reload the interactive session's plugin runtime: invalidate fs/plugin-root
|
|
3070
|
-
* caches, rediscover skills
|
|
3071
|
-
* cache, and reconnect MCP servers (rebinding the session's MCP
|
|
3072
|
-
* by `/reload-plugins`'s TUI handler and the `handle`-adapter's
|
|
3073
|
-
* hook so both honor the command's documented
|
|
3073
|
+
* caches, rediscover skills, file slash commands, and task agents, reset the
|
|
3074
|
+
* capability cache, and reconnect MCP servers (rebinding the session's MCP
|
|
3075
|
+
* tools). Shared by `/reload-plugins`'s TUI handler and the `handle`-adapter's
|
|
3076
|
+
* `reloadPlugins` hook so both honor the command's documented reload scope.
|
|
3074
3077
|
*/
|
|
3075
3078
|
async function reloadTuiPluginState(ctx: InteractiveModeContext): Promise<void> {
|
|
3076
3079
|
const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd());
|
|
3077
3080
|
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
3081
|
+
await refreshAgentDiscovery(ctx.sessionManager.getCwd());
|
|
3078
3082
|
await ctx.refreshSkillState();
|
|
3079
3083
|
await ctx.refreshSlashCommandState();
|
|
3080
3084
|
resetCapabilities();
|
package/src/task/index.ts
CHANGED
|
@@ -441,16 +441,19 @@ export function composeSpawnAdvisory(args: {
|
|
|
441
441
|
class TaskJobError extends Error {}
|
|
442
442
|
|
|
443
443
|
/**
|
|
444
|
-
* Process-level
|
|
444
|
+
* Process-level create-time discovery memo and published reload snapshots,
|
|
445
|
+
* keyed by resolved cwd.
|
|
445
446
|
*
|
|
446
447
|
* `TaskTool.create` runs for every (sub)agent session in this process and the
|
|
447
448
|
* walk-up + plugin-registry scan in `discoverAgents` is identical for a given
|
|
448
|
-
* cwd, so repeat creations reuse the first scan.
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
449
|
+
* cwd, so repeat creations reuse the first scan. Explicit plugin reloads
|
|
450
|
+
* replace the matching snapshot so already-created tools advertise the latest
|
|
451
|
+
* definitions. Execution-time discovery (`#runSpawn`) intentionally stays
|
|
452
|
+
* fresh. The memo also tracks the live `discoverAgents` binding: test spies
|
|
453
|
+
* swap that binding, which invalidates both caches automatically.
|
|
452
454
|
*/
|
|
453
455
|
const discoveryMemo = new Map<string, Promise<DiscoveryResult>>();
|
|
456
|
+
const discoverySnapshots = new Map<string, AgentDefinition[]>();
|
|
454
457
|
let discoveryMemoFn: typeof discoverAgents | undefined;
|
|
455
458
|
|
|
456
459
|
function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
|
|
@@ -458,6 +461,7 @@ function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
|
|
|
458
461
|
if (discoveryMemoFn !== fn) {
|
|
459
462
|
discoveryMemoFn = fn;
|
|
460
463
|
discoveryMemo.clear();
|
|
464
|
+
discoverySnapshots.clear();
|
|
461
465
|
}
|
|
462
466
|
const key = path.resolve(cwd);
|
|
463
467
|
let pending = discoveryMemo.get(key);
|
|
@@ -471,6 +475,17 @@ function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
|
|
|
471
475
|
return pending;
|
|
472
476
|
}
|
|
473
477
|
|
|
478
|
+
/** Rescan one cwd and publish its definitions to existing and future task tools. */
|
|
479
|
+
export async function refreshAgentDiscovery(cwd: string): Promise<void> {
|
|
480
|
+
const key = path.resolve(cwd);
|
|
481
|
+
discoveryMemo.delete(key);
|
|
482
|
+
const pending = discoverAgentsForCreate(cwd);
|
|
483
|
+
const { agents } = await pending;
|
|
484
|
+
if (discoveryMemo.get(key) === pending) {
|
|
485
|
+
discoverySnapshots.set(key, agents);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
474
489
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
475
490
|
// Tool Class
|
|
476
491
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -587,7 +602,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
587
602
|
const planMode = this.session.getPlanModeState?.()?.enabled === true;
|
|
588
603
|
const isolationMode = this.session.settings.get("task.isolation.mode");
|
|
589
604
|
return renderDescription({
|
|
590
|
-
agents: this.#discoveredAgents,
|
|
605
|
+
agents: discoverySnapshots.get(path.resolve(this.session.cwd)) ?? this.#discoveredAgents,
|
|
591
606
|
isolationEnabled: !planMode && isolationMode !== "none",
|
|
592
607
|
applyIsolatedChanges: this.session.settings.get("task.isolation.apply"),
|
|
593
608
|
disabledAgents,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { resolveContainedPathSync } from "../discovery/contained-path";
|
|
3
4
|
import type { Skill } from "../extensibility/skills";
|
|
4
5
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
5
6
|
import { validateRelativePath } from "../internal-urls/skill-protocol";
|
|
@@ -89,6 +90,20 @@ export function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): st
|
|
|
89
90
|
if (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {
|
|
90
91
|
throw new ToolError("Path traversal is not allowed in skill:// URLs");
|
|
91
92
|
}
|
|
93
|
+
// Agent Plugin skills (§4.1): the resource must canonically resolve within
|
|
94
|
+
// the plugin root. Fail closed: a dangling or unresolvable path is rejected
|
|
95
|
+
// rather than handed to bash, where writing through it could create the
|
|
96
|
+
// outside target. Symlinks may target other files inside the same package.
|
|
97
|
+
if (skill.containRoot) {
|
|
98
|
+
const contained = resolveContainedPathSync(skill.containRoot, resolvedPath);
|
|
99
|
+
if (contained.status === "outside") {
|
|
100
|
+
throw new ToolError(`skill:// path resolves outside the plugin root: ${url}`);
|
|
101
|
+
}
|
|
102
|
+
if (contained.status === "missing") {
|
|
103
|
+
throw new ToolError(`skill:// path does not exist: ${url}`);
|
|
104
|
+
}
|
|
105
|
+
return contained.realPath;
|
|
106
|
+
}
|
|
92
107
|
|
|
93
108
|
return resolvedPath;
|
|
94
109
|
}
|