@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.13
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 +57 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-d8xh7keh.md} +57 -0
- package/dist/cli.js +3069 -3047
- package/dist/types/advisor/delta-split.d.ts +24 -0
- package/dist/types/advisor/runtime.d.ts +2 -2
- package/dist/types/async/job-manager.d.ts +8 -1
- package/dist/types/cli/update-cli.d.ts +53 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-resolver.d.ts +15 -2
- package/dist/types/config/settings-schema.d.ts +4 -0
- package/dist/types/discovery/agents-md.d.ts +10 -1
- package/dist/types/eval/runner-cache.d.ts +12 -0
- package/dist/types/extensibility/extensions/runner.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +15 -4
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
- package/dist/types/lib/xai-http.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +8 -5
- package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +4 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +17 -8
- package/dist/types/modes/types.d.ts +7 -10
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +22 -3
- package/dist/types/session/messages.d.ts +20 -0
- package/dist/types/session/retry-fallback-chains.d.ts +13 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +24 -5
- package/dist/types/session/turn-recovery.d.ts +34 -5
- package/dist/types/slash-commands/types.d.ts +5 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/approval.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +14 -15
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/local-date.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/brave.d.ts +8 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
- package/dist/types/web/search/providers/jina.d.ts +3 -3
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +4 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/delta-split.ts +98 -0
- package/src/advisor/runtime.ts +321 -69
- package/src/async/job-manager.ts +14 -3
- package/src/cli/plugin-cli.ts +30 -2
- package/src/cli/update-cli.ts +259 -24
- package/src/config/keybindings.ts +52 -9
- package/src/config/model-resolver.ts +19 -3
- package/src/config/settings-schema.ts +5 -0
- package/src/cursor.ts +10 -5
- package/src/discovery/agents-md.ts +61 -23
- package/src/eval/jl/kernel.ts +2 -20
- package/src/eval/py/kernel.ts +2 -20
- package/src/eval/rb/kernel.ts +2 -20
- package/src/eval/runner-cache.ts +41 -0
- package/src/exec/non-interactive-env.ts +14 -3
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/runner.ts +184 -66
- package/src/extensibility/extensions/types.ts +26 -2
- package/src/extensibility/extensions/wrapper.ts +13 -7
- package/src/extensibility/plugins/marketplace/manager.ts +6 -2
- package/src/hindsight/client.ts +1 -1
- package/src/lib/xai-http.ts +0 -4
- package/src/lsp/client.ts +2 -0
- package/src/lsp/servers.ts +1 -1
- package/src/mcp/tool-bridge.ts +15 -6
- package/src/modes/components/agent-hub-renderer.ts +9 -3
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/status-line/component.ts +58 -6
- package/src/modes/components/status-line/segments.ts +12 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/user-message.ts +20 -5
- package/src/modes/controllers/event-controller.ts +48 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -7
- package/src/modes/controllers/input-controller.ts +25 -6
- package/src/modes/interactive-mode.ts +315 -129
- package/src/modes/rpc/rpc-frame.ts +13 -5
- package/src/modes/theme/tui-adapters.ts +4 -5
- package/src/modes/types.ts +13 -7
- package/src/modes/utils/hotkeys-markdown.ts +10 -6
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +139 -8
- package/src/session/agent-session-types.ts +2 -0
- package/src/session/agent-session.ts +66 -10
- package/src/session/messages.ts +98 -28
- package/src/session/retry-fallback-chains.ts +14 -0
- package/src/session/session-advisors.ts +32 -15
- package/src/session/session-history-format.ts +15 -1
- package/src/session/session-maintenance.ts +8 -8
- package/src/session/session-manager.ts +6 -2
- package/src/session/session-tools.ts +321 -184
- package/src/session/turn-recovery.ts +225 -47
- package/src/slash-commands/builtin-modes.ts +41 -12
- package/src/slash-commands/types.ts +5 -1
- package/src/task/executor.ts +92 -45
- package/src/task/structured-subagent.ts +5 -5
- package/src/tools/approval.ts +44 -10
- package/src/tools/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- package/src/tools/todo.ts +70 -26
- package/src/tools/tts.ts +3 -2
- package/src/tools/write.ts +7 -3
- package/src/utils/local-date.ts +13 -0
- package/src/utils/tools-manager.ts +2 -2
- package/src/vibe/runtime.ts +22 -14
- package/src/web/kagi.ts +91 -34
- package/src/web/parallel.ts +11 -2
- package/src/web/scrapers/crates-io.ts +2 -2
- package/src/web/scrapers/discogs.ts +2 -2
- package/src/web/scrapers/docs-rs.ts +2 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/pubmed.ts +2 -2
- package/src/web/scrapers/sec-edgar.ts +2 -2
- package/src/web/search/providers/brave.ts +121 -46
- package/src/web/search/providers/codex.ts +88 -12
- package/src/web/search/providers/exa.ts +45 -10
- package/src/web/search/providers/firecrawl.ts +53 -11
- package/src/web/search/providers/gemini.ts +139 -27
- package/src/web/search/providers/jina.ts +48 -25
- package/src/web/search/providers/parallel.ts +23 -9
- package/src/web/search/providers/perplexity.ts +24 -7
- package/src/web/search/providers/searxng.ts +77 -1
- package/src/web/search/providers/tavily.ts +23 -22
- package/src/web/search/providers/tinyfish.ts +44 -10
- package/src/web/search/providers/xai.ts +85 -14
package/src/advisor/runtime.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
formatToolResultErrorPreview,
|
|
14
14
|
PRIMARY_CONTEXT_CUSTOM_TYPES,
|
|
15
15
|
} from "../session/session-history-format";
|
|
16
|
+
import { ADVISOR_RENDER_OPTIONS, renderAdvisorDeltaChunks } from "./delta-split";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Minimal slice of `Agent` the runtime drives — satisfied by pi-agent-core
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
* this field after every prompt to detect a failed turn.
|
|
22
23
|
*/
|
|
23
24
|
export interface AdvisorAgent {
|
|
24
|
-
prompt(input: string): Promise<void>;
|
|
25
|
+
prompt(input: string | AgentMessage[]): Promise<void>;
|
|
25
26
|
abort(reason?: unknown): void;
|
|
26
27
|
reset(): void;
|
|
27
28
|
/**
|
|
@@ -232,13 +233,6 @@ const MAX_COALESCE_ROUNDS = 3;
|
|
|
232
233
|
*/
|
|
233
234
|
const MAX_QUARANTINE_RETRIES = 2;
|
|
234
235
|
|
|
235
|
-
const ADVISOR_RENDER_OPTIONS = {
|
|
236
|
-
includeToolIntent: true,
|
|
237
|
-
watchedRoles: true,
|
|
238
|
-
expandPrimaryContext: true,
|
|
239
|
-
expandEditDiffs: true,
|
|
240
|
-
} as const;
|
|
241
|
-
|
|
242
236
|
interface PendingDelta {
|
|
243
237
|
text: string;
|
|
244
238
|
rawMessages: AgentMessage[];
|
|
@@ -262,9 +256,38 @@ interface DeliveredMessage {
|
|
|
262
256
|
|
|
263
257
|
function fingerprintMessage(message: AgentMessage): bigint | undefined {
|
|
264
258
|
try {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
259
|
+
// Field-selective fingerprint: hash every top-level field the advisor
|
|
260
|
+
// renderer actually reads (mirrors AppendOnlyContextManager.#messageDigest,
|
|
261
|
+
// issue #3406). Unrendered metadata (timestamp, usage, provider internals)
|
|
262
|
+
// churns on provider round-trips and would otherwise trigger a full
|
|
263
|
+
// transcript replay for a no-op change. Rendered fields (from
|
|
264
|
+
// session-history-format.ts): role, content, customType, display, isError,
|
|
265
|
+
// toolResult: cancelled/exitCode/output, custom: details, plus the
|
|
266
|
+
// execution/branch/compaction/file-mention fields the formatter reads:
|
|
267
|
+
// excludeFromContext, command (bashExecution), code (pythonExecution),
|
|
268
|
+
// summary + fromId (branch/compaction), files (fileMention).
|
|
269
|
+
const m = message as unknown as Record<string, unknown>;
|
|
270
|
+
const payload = JSON.stringify({
|
|
271
|
+
r: m.role ?? null,
|
|
272
|
+
c: m.content ?? null,
|
|
273
|
+
toolCallId: m.toolCallId ?? null,
|
|
274
|
+
toolName: m.toolName ?? null,
|
|
275
|
+
err: m.isError ?? null,
|
|
276
|
+
ct: m.customType ?? null,
|
|
277
|
+
disp: m.display ?? null,
|
|
278
|
+
cancel: m.cancelled ?? null,
|
|
279
|
+
exit: m.exitCode ?? null,
|
|
280
|
+
out: m.output ?? null,
|
|
281
|
+
det: m.details ?? null,
|
|
282
|
+
xfc: m.excludeFromContext ?? null,
|
|
283
|
+
cmd: m.command ?? null,
|
|
284
|
+
code: m.code ?? null,
|
|
285
|
+
sum: m.summary ?? null,
|
|
286
|
+
from: m.fromId ?? null,
|
|
287
|
+
files: m.files ?? null,
|
|
288
|
+
});
|
|
289
|
+
if (payload === undefined) return undefined;
|
|
290
|
+
return Bun.hash.wyhash(payload);
|
|
268
291
|
} catch {
|
|
269
292
|
return undefined;
|
|
270
293
|
}
|
|
@@ -281,8 +304,17 @@ export class AdvisorRuntime {
|
|
|
281
304
|
* approved plan). These prompts are re-injected verbatim every primary turn;
|
|
282
305
|
* this lets {@link #renderDelta} collapse an unchanged copy to a one-line
|
|
283
306
|
* marker so the advisor isn't re-fed the full ~1k-token rules each turn.
|
|
284
|
-
|
|
307
|
+
/** Cleared on every re-prime/seed and when a failed batch is dropped. */
|
|
285
308
|
#seenContext = new Map<string, string>();
|
|
309
|
+
/**
|
|
310
|
+
* Snapshot of {@link #seenContext} taken by #prepareBatch before the
|
|
311
|
+
* in-flight batch's first dedup mutation. Restored by
|
|
312
|
+
* {@link #rollbackFailedTurn} when the turn fails and its rawMessages are
|
|
313
|
+
* requeued, so first-time primary-context is re-delivered in full instead
|
|
314
|
+
* of collapsing to "(unchanged — still in effect)" against an advisor
|
|
315
|
+
* history that no longer contains it. Cleared on turn success.
|
|
316
|
+
*/
|
|
317
|
+
#seenContextInFlight: [string, string][] | undefined;
|
|
286
318
|
/** Incremented whenever the advisor loses context so queued raw deltas are re-rendered against fresh dedupe state. */
|
|
287
319
|
#renderRevision = 0;
|
|
288
320
|
/** Regex secret values observed in primary deltas and retained until advisor context resets. */
|
|
@@ -462,6 +494,7 @@ export class AdvisorRuntime {
|
|
|
462
494
|
|
|
463
495
|
#clearSeenContext(): void {
|
|
464
496
|
this.#seenContext.clear();
|
|
497
|
+
this.#seenContextInFlight = undefined;
|
|
465
498
|
this.#advisorRegexSecretValues.clear();
|
|
466
499
|
this.#renderRevision++;
|
|
467
500
|
}
|
|
@@ -477,7 +510,15 @@ export class AdvisorRuntime {
|
|
|
477
510
|
} catch {}
|
|
478
511
|
}
|
|
479
512
|
|
|
480
|
-
#resetAdvisorContext(clearBacklog: boolean, wakeWaiters: boolean): void {
|
|
513
|
+
#resetAdvisorContext(clearBacklog: boolean, wakeWaiters: boolean, reason?: string): void {
|
|
514
|
+
if (reason) {
|
|
515
|
+
logger.debug("advisor context reset", {
|
|
516
|
+
reason,
|
|
517
|
+
lastCount: this.#lastCount,
|
|
518
|
+
pending: this.#pending.length,
|
|
519
|
+
backlog: this.#backlog,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
481
522
|
this.#lastCount = 0;
|
|
482
523
|
this.#deliveredPrefix = [];
|
|
483
524
|
this.#pending = [];
|
|
@@ -541,7 +582,11 @@ export class AdvisorRuntime {
|
|
|
541
582
|
* post-compaction — transcript, giving the advisor fresh context instead of
|
|
542
583
|
* leaving it blind to everything before the rewrite.
|
|
543
584
|
*/
|
|
544
|
-
reset(): void {
|
|
585
|
+
reset(reason = "external"): void {
|
|
586
|
+
// Step-1 observability (issue #7226): every re-prime logs its trigger so
|
|
587
|
+
// live investigations can attribute full-transcript replays (cached_tokens
|
|
588
|
+
// pinned at the instructions/tools boundary) to a concrete path instead of
|
|
589
|
+
// inferring it from payload markers after the fact.
|
|
545
590
|
this.#iterationAbort?.abort("advisor reset");
|
|
546
591
|
this.#epoch++;
|
|
547
592
|
this.#sessionTransitionPaused = false;
|
|
@@ -552,7 +597,7 @@ export class AdvisorRuntime {
|
|
|
552
597
|
this.#consecutiveQuarantines = 0;
|
|
553
598
|
this.#refusalModelsTried.clear();
|
|
554
599
|
this.#failureNotified = false;
|
|
555
|
-
this.#resetAdvisorContext(true, true);
|
|
600
|
+
this.#resetAdvisorContext(true, true, reason);
|
|
556
601
|
}
|
|
557
602
|
|
|
558
603
|
/**
|
|
@@ -584,10 +629,145 @@ export class AdvisorRuntime {
|
|
|
584
629
|
this.#includeThinking = true;
|
|
585
630
|
}
|
|
586
631
|
|
|
587
|
-
|
|
632
|
+
// Candidate 4 (multi-message split): render the Session update as MULTIPLE
|
|
633
|
+
// user messages — one per source message — instead of one ever-growing user
|
|
634
|
+
// message. Provider prompt caches are prefix-based: a single user message
|
|
635
|
+
// whose text keeps growing invalidates the whole message on every turn, so
|
|
636
|
+
// cache_read stays pinned at the instructions/tools boundary (observed
|
|
637
|
+
// 14491 in production, 11066 in tests). Splitting into per-source user
|
|
638
|
+
// messages lets the provider cache each appended message (verified
|
|
639
|
+
// experimentally: cache_read 11066 → 11091 → 11112 vs pinned 11066).
|
|
640
|
+
//
|
|
641
|
+
/**
|
|
642
|
+
* Shared obfuscation side effects for BOTH render paths (single-block
|
|
643
|
+
* {@link #renderPreparedDelta} and multi-message
|
|
644
|
+
* {@link #formatRawDeltaMessageChunks}): collect regex secret values from
|
|
645
|
+
* primary-context custom messages and the rendered markdown, scrub the
|
|
646
|
+
* advisor's own history, and refresh pending placeholder prefixes when new
|
|
647
|
+
* secrets appear. Returns whether new secret values were discovered.
|
|
648
|
+
* Idempotent across the two calls one drain makes for the same prepared
|
|
649
|
+
* list: the second call discovers nothing new and skips the strip.
|
|
650
|
+
*/
|
|
651
|
+
#collectAdvisorSecrets(obfuscator: SecretObfuscator, delta: AgentMessage[], renderedMd: string): boolean {
|
|
652
|
+
let discoveredNewRegexSecretValue = false;
|
|
653
|
+
const addRegexValues = (text: string): void => {
|
|
654
|
+
for (const secretValue of obfuscator.collectRegexSecretValuesForObfuscation(text) ?? []) {
|
|
655
|
+
if (this.#advisorRegexSecretValues.has(secretValue)) continue;
|
|
656
|
+
this.#advisorRegexSecretValues.add(secretValue);
|
|
657
|
+
discoveredNewRegexSecretValue = true;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
for (const message of delta) {
|
|
661
|
+
if (
|
|
662
|
+
message.role === "custom" &&
|
|
663
|
+
PRIMARY_CONTEXT_CUSTOM_TYPES.has(message.customType) &&
|
|
664
|
+
typeof message.content === "string"
|
|
665
|
+
) {
|
|
666
|
+
addRegexValues(message.content);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
addRegexValues(renderedMd);
|
|
670
|
+
scrubAdvisorHistory(obfuscator, this.agent.state.messages, this.#advisorRegexSecretValues);
|
|
671
|
+
if (discoveredNewRegexSecretValue) {
|
|
672
|
+
this.#pending = this.#pending.map(delta => ({
|
|
673
|
+
...delta,
|
|
674
|
+
text: obfuscator.stripUnsafeFriendlyPlaceholderPrefixes(delta.text, this.#advisorRegexSecretValues),
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
677
|
+
return discoveredNewRegexSecretValue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Map primary-context custom messages through the obfuscator. Shared by
|
|
682
|
+
* both render paths so the byte-equivalence contract lives in one place.
|
|
683
|
+
*/
|
|
684
|
+
#obfuscatePrimaryContextMessages(obfuscator: SecretObfuscator, delta: AgentMessage[]): AgentMessage[] {
|
|
685
|
+
return delta.map(message =>
|
|
686
|
+
message.role === "custom" && PRIMARY_CONTEXT_CUSTOM_TYPES.has(message.customType)
|
|
687
|
+
? obfuscateAdvisorMessage(obfuscator, message, this.#advisorRegexSecretValues)
|
|
688
|
+
: message,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Each source message is rendered INDEPENDENTLY via
|
|
693
|
+
// formatSessionHistoryMarkdown in chunked mode (shared toolResultIndex +
|
|
694
|
+
// consumedToolCallIds over the WHOLE delta), so a toolCall finds its
|
|
695
|
+
// toolResult across chunk boundaries and consecutive same-role collapsing
|
|
696
|
+
// is preserved. Concatenating the chunk texts with the same separator the
|
|
697
|
+
// old single-block render used yields byte-identical advisor context.
|
|
698
|
+
// Each chunk is delivered as its own user AgentMessage via a SINGLE
|
|
699
|
+
// Agent.prompt(AgentMessage[]) call, so the advisor model still runs ONCE
|
|
700
|
+
// per update (no per-message assistant turns).
|
|
701
|
+
#formatRawDeltaMessageChunks(preparedMessages: AgentMessage[], wip = false): AgentMessage[] | null {
|
|
702
|
+
// Consumes the ALREADY-prepared view from #prepareBatch: advisor custom
|
|
703
|
+
// messages are filtered and primary-context dedup is applied there, so
|
|
704
|
+
// splitting here never double-folds or leaks hidden messages.
|
|
705
|
+
const delta = preparedMessages;
|
|
706
|
+
if (delta.length === 0) return null;
|
|
707
|
+
|
|
708
|
+
const obfuscator = this.host.obfuscator;
|
|
709
|
+
// Side effects the pure renderer cannot own: collect secrets, scrub the
|
|
710
|
+
// advisor's own history and refresh pending placeholder prefixes (shared
|
|
711
|
+
// helper — see #collectAdvisorSecrets; idempotent for this drain's
|
|
712
|
+
// single-block pass over the same prepared list).
|
|
713
|
+
const probeMd = formatSessionHistoryMarkdown(delta, {
|
|
714
|
+
...ADVISOR_RENDER_OPTIONS,
|
|
715
|
+
includeThinking: this.#includeThinking,
|
|
716
|
+
});
|
|
717
|
+
if (obfuscator?.hasSecrets()) {
|
|
718
|
+
this.#collectAdvisorSecrets(obfuscator, delta, probeMd);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Message-level obfuscation mirrors the old #formatRawDelta path EXACTLY:
|
|
722
|
+
// only primary-context custom messages are mapped (tool args, details.diff,
|
|
723
|
+
// structured fields), because the old path's contract is whole-delta text
|
|
724
|
+
// obfuscation as the final pass. Expanding to every role would mint
|
|
725
|
+
// different placeholders and break byte-equivalence with the old render.
|
|
726
|
+
const renderDelta = obfuscator?.hasSecrets() ? this.#obfuscatePrimaryContextMessages(obfuscator, delta) : delta;
|
|
727
|
+
|
|
728
|
+
const chunks = renderAdvisorDeltaChunks(renderDelta, {
|
|
729
|
+
wip,
|
|
730
|
+
includeThinking: this.#includeThinking,
|
|
731
|
+
obfuscator: obfuscator?.hasSecrets() ? obfuscator : undefined,
|
|
732
|
+
advisorRegexSecretValues: this.#advisorRegexSecretValues,
|
|
733
|
+
});
|
|
734
|
+
return chunks;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
#formatRawDelta(rawMessages: AgentMessage[], wip = false, updateSeenContext = true): string | null {
|
|
588
738
|
const delta = rawMessages
|
|
589
739
|
.filter(message => !(message.role === "custom" && message.customType === "advisor"))
|
|
590
|
-
.map(message =>
|
|
740
|
+
.map(message =>
|
|
741
|
+
updateSeenContext ? this.#dedupContextMessage(message) : this.#dedupContextMessageReadOnly(message),
|
|
742
|
+
);
|
|
743
|
+
return this.#renderPreparedDelta(delta, wip);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Preview variant of #dedupContextMessage: returns the collapse decision
|
|
748
|
+
* WITHOUT advancing the live #seenContext map. Used by #renderDelta so the
|
|
749
|
+
* preview text does not make the batch's first real delivery look like a
|
|
750
|
+
* re-injection.
|
|
751
|
+
*/
|
|
752
|
+
#dedupContextMessageReadOnly(msg: AgentMessage): AgentMessage {
|
|
753
|
+
if (msg.role !== "custom") return msg;
|
|
754
|
+
if (!PRIMARY_CONTEXT_CUSTOM_TYPES.has(msg.customType)) return msg;
|
|
755
|
+
if (typeof msg.content !== "string") return msg;
|
|
756
|
+
if (this.#seenContext.get(msg.customType) === msg.content) {
|
|
757
|
+
return { ...msg, content: "(unchanged — still in effect)" };
|
|
758
|
+
}
|
|
759
|
+
return msg;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Render already-prepared (deduped + advisor-filtered) messages to the
|
|
764
|
+
* single-block Session update text. Does NOT dedup again — callers that
|
|
765
|
+
* prepared the list must pass it here directly, and callers that prepared
|
|
766
|
+
* via #prepareBatch get byte-identical batch text to what the multi-message
|
|
767
|
+
* split consumes.
|
|
768
|
+
*/
|
|
769
|
+
#renderPreparedDelta(preparedMessages: AgentMessage[], wip = false): string | null {
|
|
770
|
+
const delta = preparedMessages;
|
|
591
771
|
if (delta.length === 0) return null;
|
|
592
772
|
const obfuscator = this.host.obfuscator;
|
|
593
773
|
let md = formatSessionHistoryMarkdown(delta, {
|
|
@@ -596,43 +776,22 @@ export class AdvisorRuntime {
|
|
|
596
776
|
});
|
|
597
777
|
if (!md.trim()) return null;
|
|
598
778
|
if (obfuscator?.hasSecrets()) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
discoveredNewRegexSecretValue = true;
|
|
605
|
-
}
|
|
606
|
-
};
|
|
607
|
-
for (const message of delta) {
|
|
608
|
-
if (
|
|
609
|
-
message.role === "custom" &&
|
|
610
|
-
PRIMARY_CONTEXT_CUSTOM_TYPES.has(message.customType) &&
|
|
611
|
-
typeof message.content === "string"
|
|
612
|
-
) {
|
|
613
|
-
addRegexValues(message.content);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
addRegexValues(md);
|
|
617
|
-
scrubAdvisorHistory(obfuscator, this.agent.state.messages, this.#advisorRegexSecretValues);
|
|
618
|
-
if (discoveredNewRegexSecretValue) {
|
|
619
|
-
this.#pending = this.#pending.map(delta => ({
|
|
620
|
-
...delta,
|
|
621
|
-
text: obfuscator.stripUnsafeFriendlyPlaceholderPrefixes(delta.text, this.#advisorRegexSecretValues),
|
|
622
|
-
}));
|
|
623
|
-
}
|
|
624
|
-
md = formatSessionHistoryMarkdown(
|
|
625
|
-
delta.map(message =>
|
|
626
|
-
message.role === "custom" && PRIMARY_CONTEXT_CUSTOM_TYPES.has(message.customType)
|
|
627
|
-
? obfuscateAdvisorMessage(obfuscator, message, this.#advisorRegexSecretValues)
|
|
628
|
-
: message,
|
|
629
|
-
),
|
|
630
|
-
{ ...ADVISOR_RENDER_OPTIONS, includeThinking: this.#includeThinking },
|
|
631
|
-
);
|
|
779
|
+
this.#collectAdvisorSecrets(obfuscator, delta, md);
|
|
780
|
+
md = formatSessionHistoryMarkdown(this.#obfuscatePrimaryContextMessages(obfuscator, delta), {
|
|
781
|
+
...ADVISOR_RENDER_OPTIONS,
|
|
782
|
+
includeThinking: this.#includeThinking,
|
|
783
|
+
});
|
|
632
784
|
md = obfuscator.obfuscate(md, this.#advisorRegexSecretValues);
|
|
633
785
|
}
|
|
634
|
-
|
|
635
|
-
|
|
786
|
+
// Candidate 3: keep the heading byte-identical between wip and final turns
|
|
787
|
+
// and put the WIP marker at the END of the batch, so a wip/final flip
|
|
788
|
+
// never changes the batch prefix. The provider prompt cache is
|
|
789
|
+
// prefix-based; a heading that flips between turns re-prefills the whole
|
|
790
|
+
// user message on every in-progress turn.
|
|
791
|
+
const heading = "### Session update";
|
|
792
|
+
const mdHead = `${heading}\n\n${md}`;
|
|
793
|
+
if (!wip) return mdHead;
|
|
794
|
+
return `${mdHead}\n\n---\n\n[in progress — more steps follow]`;
|
|
636
795
|
}
|
|
637
796
|
|
|
638
797
|
#renderDelta(messages?: AgentMessage[], wip = false): Omit<PendingDelta, "turns" | "overflowRecovery"> | null {
|
|
@@ -653,12 +812,26 @@ export class AdvisorRuntime {
|
|
|
653
812
|
delivered.fingerprint !== fingerprint
|
|
654
813
|
) {
|
|
655
814
|
prefixChanged = true;
|
|
815
|
+
// Full replays are expensive (the whole transcript is re-sent and
|
|
816
|
+
// the provider prompt cache re-prefills from the system prompt), so
|
|
817
|
+
// record exactly which delivered message diverged and which
|
|
818
|
+
// top-level fields changed — without this the trigger is invisible.
|
|
819
|
+
try {
|
|
820
|
+
const oldMsg: Record<string, unknown> = delivered.message as unknown as Record<string, unknown>;
|
|
821
|
+
const newMsg: Record<string, unknown> = current as unknown as Record<string, unknown>;
|
|
822
|
+
const differingFields: string[] = [];
|
|
823
|
+
for (const key of new Set([...Object.keys(oldMsg), ...Object.keys(newMsg)])) {
|
|
824
|
+
if (JSON.stringify(oldMsg[key]) !== JSON.stringify(newMsg[key])) differingFields.push(key);
|
|
825
|
+
}
|
|
826
|
+
logger.debug("advisor delivered prefix changed", { index: i, role: newMsg.role, differingFields });
|
|
827
|
+
} catch {}
|
|
656
828
|
break;
|
|
657
829
|
}
|
|
658
830
|
delivered.message = current;
|
|
659
831
|
}
|
|
660
832
|
if (prefixChanged) {
|
|
661
833
|
this.#epoch++;
|
|
834
|
+
logger.debug("advisor context reset", { reason: "delivered-prefix-changed", lastCount: this.#lastCount });
|
|
662
835
|
this.#resetAdvisorContext(true, true);
|
|
663
836
|
}
|
|
664
837
|
const rawMessages = all.slice(this.#lastCount);
|
|
@@ -668,7 +841,11 @@ export class AdvisorRuntime {
|
|
|
668
841
|
this.#deliveredPrefix.push({ message, fingerprint: fingerprintMessage(message) });
|
|
669
842
|
}
|
|
670
843
|
this.#lastCount = all.length;
|
|
671
|
-
|
|
844
|
+
// Preview render: do NOT advance #seenContext — the batch's real dedup
|
|
845
|
+
// happens once in #prepareBatch. Advancing here would make the first
|
|
846
|
+
// real delivery of a re-injected primary-context message collapse to
|
|
847
|
+
// "(unchanged…)" (double-fold).
|
|
848
|
+
const text = this.#formatRawDelta(rawMessages, wip, false);
|
|
672
849
|
return text ? { text, rawMessages, renderRevision: this.#renderRevision, wip } : null;
|
|
673
850
|
}
|
|
674
851
|
|
|
@@ -715,7 +892,19 @@ export class AdvisorRuntime {
|
|
|
715
892
|
* append-only context); falls back to truncating `state.messages` for tests
|
|
716
893
|
* that hand-roll a minimal facade.
|
|
717
894
|
*/
|
|
895
|
+
#restoreSeenContextInFlight(): void {
|
|
896
|
+
if (!this.#seenContextInFlight) return;
|
|
897
|
+
this.#seenContext.clear();
|
|
898
|
+
for (const [key, value] of this.#seenContextInFlight) this.#seenContext.set(key, value);
|
|
899
|
+
this.#seenContextInFlight = undefined;
|
|
900
|
+
}
|
|
901
|
+
|
|
718
902
|
#rollbackFailedTurn(snapshot: number): void {
|
|
903
|
+
// Restore the primary-context dedup map to its pre-batch state: the
|
|
904
|
+
// failed turn never reached the advisor, so first-time context collapsed
|
|
905
|
+
// to "(unchanged…)" by this batch's #prepareBatch must expand again on
|
|
906
|
+
// the retry/requeue pass.
|
|
907
|
+
this.#restoreSeenContextInFlight();
|
|
719
908
|
const messages = this.agent.state.messages;
|
|
720
909
|
if (messages.length <= snapshot) return;
|
|
721
910
|
try {
|
|
@@ -757,6 +946,7 @@ export class AdvisorRuntime {
|
|
|
757
946
|
): Promise<{
|
|
758
947
|
batch: string | null;
|
|
759
948
|
rawMessages: AgentMessage[];
|
|
949
|
+
preparedMessages: AgentMessage[];
|
|
760
950
|
finalTurns: number;
|
|
761
951
|
wip: boolean;
|
|
762
952
|
resetContext: boolean;
|
|
@@ -803,11 +993,18 @@ export class AdvisorRuntime {
|
|
|
803
993
|
// waiters, latest snapshot, and epoch stay untouched. Re-render only
|
|
804
994
|
// this already-popped raw batch so active plan/reference bodies are
|
|
805
995
|
// restored without replaying any older primary transcript.
|
|
996
|
+
logger.debug("advisor context reset", {
|
|
997
|
+
reason: "context-maintenance",
|
|
998
|
+
lastCount: this.#lastCount,
|
|
999
|
+
pending: this.#pending.length,
|
|
1000
|
+
backlog: this.#backlog,
|
|
1001
|
+
});
|
|
806
1002
|
this.#clearAdvisorContextAtCurrentCursor();
|
|
807
|
-
const rerendered = this.#
|
|
1003
|
+
const { batch: rerendered, preparedMessages } = this.#prepareBatch(rawMessages, wip, batchText);
|
|
808
1004
|
return {
|
|
809
1005
|
batch: rerendered ?? (batchText || null),
|
|
810
1006
|
rawMessages,
|
|
1007
|
+
preparedMessages,
|
|
811
1008
|
finalTurns: turns,
|
|
812
1009
|
wip,
|
|
813
1010
|
resetContext: true,
|
|
@@ -836,11 +1033,47 @@ export class AdvisorRuntime {
|
|
|
836
1033
|
wip = late.at(-1)!.wip;
|
|
837
1034
|
}
|
|
838
1035
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1036
|
+
// Prepare the deduped view AFTER coalescing (rawMessages is complete by
|
|
1037
|
+
// now): filters advisor custom messages and collapses re-injected
|
|
1038
|
+
// primary-context to "(unchanged…)". BOTH the single-block text and the
|
|
1039
|
+
// multi-message split derive from this exact list so they never diverge.
|
|
1040
|
+
const { batch: preparedBatch, preparedMessages } = this.#prepareBatch(rawMessages, wip, batchText);
|
|
1041
|
+
return {
|
|
1042
|
+
batch: preparedBatch ?? (batchText || null),
|
|
1043
|
+
rawMessages,
|
|
1044
|
+
preparedMessages,
|
|
1045
|
+
finalTurns: turns,
|
|
1046
|
+
wip,
|
|
1047
|
+
resetContext: false,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Single dedup+render pass shared by every batch finalization path (normal
|
|
1053
|
+
* and context-reset). Filters advisor custom messages, collapses re-injected
|
|
1054
|
+
* primary-context to "(unchanged…)" via #dedupContextMessage, renders the
|
|
1055
|
+
* single-block batch text from the SAME prepared list the multi-message
|
|
1056
|
+
* split consumes, so the two views can never diverge.
|
|
1057
|
+
*/
|
|
1058
|
+
#prepareBatch(
|
|
1059
|
+
rawMessages: AgentMessage[],
|
|
1060
|
+
wip: boolean,
|
|
1061
|
+
fallback: string | null,
|
|
1062
|
+
): { batch: string | null; preparedMessages: AgentMessage[] } {
|
|
1063
|
+
// Dedup against the LIVE #seenContext (populated by previous turns via
|
|
1064
|
+
// #renderDelta -> #formatRawDelta) so re-injected primary context that
|
|
1065
|
+
// was ALREADY shown collapses to "(unchanged…)", while a FIRST delivery
|
|
1066
|
+
// in this batch stays expanded. This pass advances the live map exactly
|
|
1067
|
+
// once per batch — #renderDelta's text is a preview and must not set it.
|
|
1068
|
+
// Snapshot the dedup map BEFORE this batch's first mutation so a failed
|
|
1069
|
+
// turn can restore it (see #rollbackFailedTurn). `??=` keeps the first
|
|
1070
|
+
// snapshot across coalescing re-prepares within one in-flight batch.
|
|
1071
|
+
this.#seenContextInFlight ??= [...this.#seenContext];
|
|
1072
|
+
const preparedMessages = rawMessages
|
|
1073
|
+
.filter(message => !(message.role === "custom" && message.customType === "advisor"))
|
|
1074
|
+
.map(message => this.#dedupContextMessage(message));
|
|
1075
|
+
const batch = this.#renderPreparedDelta(preparedMessages, wip);
|
|
1076
|
+
return { batch: batch ?? fallback, preparedMessages };
|
|
844
1077
|
}
|
|
845
1078
|
|
|
846
1079
|
#terminalAssistantFailure(snapshot: number): AssistantMessage | undefined {
|
|
@@ -882,8 +1115,10 @@ export class AdvisorRuntime {
|
|
|
882
1115
|
const epoch = this.#epoch;
|
|
883
1116
|
for (const delta of popped) {
|
|
884
1117
|
if (delta.renderRevision === this.#renderRevision) continue;
|
|
885
|
-
|
|
886
|
-
|
|
1118
|
+
// Context maintenance estimates this preview before #prepareBatch makes
|
|
1119
|
+
// its final deduped render. Rebuild stale text against the new context
|
|
1120
|
+
// so the maintenance budget cannot undercount an expanded re-injection.
|
|
1121
|
+
delta.text = this.#formatRawDelta(delta.rawMessages, delta.wip, false) ?? delta.text;
|
|
887
1122
|
delta.renderRevision = this.#renderRevision;
|
|
888
1123
|
}
|
|
889
1124
|
const recoveringOverflow = popped.some(delta => delta.overflowRecovery === true);
|
|
@@ -897,11 +1132,12 @@ export class AdvisorRuntime {
|
|
|
897
1132
|
// Epoch was invalidated during batch collection; restart the loop.
|
|
898
1133
|
if (result === null) continue;
|
|
899
1134
|
if (this.#sessionTransitionPaused) {
|
|
1135
|
+
this.#restoreSeenContextInFlight();
|
|
900
1136
|
this.#pending.unshift(...popped);
|
|
901
1137
|
continue;
|
|
902
1138
|
}
|
|
903
1139
|
|
|
904
|
-
const { batch, rawMessages, finalTurns, wip, resetContext } = result;
|
|
1140
|
+
const { batch, rawMessages, preparedMessages, finalTurns, wip, resetContext } = result;
|
|
905
1141
|
|
|
906
1142
|
if (this.disposed || batch === null) {
|
|
907
1143
|
this.#backlog = Math.max(0, this.#backlog - finalTurns);
|
|
@@ -918,10 +1154,18 @@ export class AdvisorRuntime {
|
|
|
918
1154
|
const messageSnapshot = this.agent.state.messages.length;
|
|
919
1155
|
const contextWasFresh = resetContext || recoveringOverflow || messageSnapshot === 0;
|
|
920
1156
|
try {
|
|
921
|
-
// Reset the host's per-update advisor state (one-advise-per-update
|
|
922
|
-
// gate) and pass through whether this batch reviews partial work.
|
|
923
1157
|
this.host.beginAdvisorUpdate?.(wip);
|
|
924
|
-
|
|
1158
|
+
// Candidate 4 (multi-message split): deliver the Session update as
|
|
1159
|
+
// multiple user messages so the provider prompt cache can
|
|
1160
|
+
// incrementally hit each appended message (cache_read grows with
|
|
1161
|
+
// the session instead of staying pinned at the instructions/tools
|
|
1162
|
+
// boundary). Falls back to the single-block string when the chunk
|
|
1163
|
+
// renderer cannot split (e.g. empty delta). The split is
|
|
1164
|
+
// byte-equivalent to the old single-block render (equivalence
|
|
1165
|
+
// tested), so the advisor sees identical context.
|
|
1166
|
+
const splitMessages = this.#formatRawDeltaMessageChunks(preparedMessages, wip);
|
|
1167
|
+
const promptInput: string | AgentMessage[] = splitMessages ?? batch;
|
|
1168
|
+
const prompt = this.agent.prompt(promptInput);
|
|
925
1169
|
this.#promptInFlight = prompt;
|
|
926
1170
|
try {
|
|
927
1171
|
await prompt;
|
|
@@ -941,6 +1185,7 @@ export class AdvisorRuntime {
|
|
|
941
1185
|
const turnError = getAdvisorTurnError(this.agent.state.messages.slice(messageSnapshot));
|
|
942
1186
|
if (turnError) throw turnError;
|
|
943
1187
|
success = true;
|
|
1188
|
+
this.#seenContextInFlight = undefined;
|
|
944
1189
|
this.#failing = false;
|
|
945
1190
|
this.#consecutiveFailures = 0;
|
|
946
1191
|
this.#failureNotified = false;
|
|
@@ -994,7 +1239,11 @@ export class AdvisorRuntime {
|
|
|
994
1239
|
if (classifierRefusal) {
|
|
995
1240
|
if (this.#includeThinking) {
|
|
996
1241
|
this.#includeThinking = false;
|
|
997
|
-
|
|
1242
|
+
// Do NOT advance #seenContext here: the requeued batch is
|
|
1243
|
+
// re-deduped by #prepareBatch on the next drain, so a mutation
|
|
1244
|
+
// now would double-fold first-time primary context into
|
|
1245
|
+
// "(unchanged — still in effect)" on the retry.
|
|
1246
|
+
const strippedBatch = this.#formatRawDelta(rawMessages, wip, false);
|
|
998
1247
|
if (strippedBatch) {
|
|
999
1248
|
this.#pending.unshift({
|
|
1000
1249
|
text: strippedBatch,
|
|
@@ -1083,13 +1332,13 @@ export class AdvisorRuntime {
|
|
|
1083
1332
|
if (this.#consecutiveQuarantines >= MAX_QUARANTINE_RETRIES) {
|
|
1084
1333
|
this.#notifyFailureOnce(err);
|
|
1085
1334
|
this.#consecutiveQuarantines = 0;
|
|
1086
|
-
this.#resetAdvisorContext(true, true);
|
|
1335
|
+
this.#resetAdvisorContext(true, true, "quarantine-retry-exhausted");
|
|
1087
1336
|
continue;
|
|
1088
1337
|
}
|
|
1089
1338
|
const rePrime = this.#pending.length > 0 ? this.#latestMessages : undefined;
|
|
1090
1339
|
// Wake catchup waiters only when nothing is re-primed; otherwise the
|
|
1091
1340
|
// re-primed turn restores the backlog and waiters resolve on its completion.
|
|
1092
|
-
this.#resetAdvisorContext(true, !rePrime);
|
|
1341
|
+
this.#resetAdvisorContext(true, !rePrime, "quarantine-recovery");
|
|
1093
1342
|
if (rePrime) this.onTurnEnd(rePrime);
|
|
1094
1343
|
continue;
|
|
1095
1344
|
}
|
|
@@ -1155,7 +1404,10 @@ export class AdvisorRuntime {
|
|
|
1155
1404
|
} else {
|
|
1156
1405
|
// Retry once against the fresh advisor context, using only the same
|
|
1157
1406
|
// bounded raw batch. Pending updates remain queued behind it.
|
|
1158
|
-
|
|
1407
|
+
// Same double-fold guard as the refusal branch: #prepareBatch
|
|
1408
|
+
// re-dedups on retry, so this preview render must not mutate
|
|
1409
|
+
// #seenContext.
|
|
1410
|
+
const recoveryBatch = this.#formatRawDelta(rawMessages, wip, false) ?? batch;
|
|
1159
1411
|
this.#pending.unshift({
|
|
1160
1412
|
text: recoveryBatch,
|
|
1161
1413
|
rawMessages,
|
package/src/async/job-manager.ts
CHANGED
|
@@ -5,6 +5,8 @@ const DELIVERY_RETRY_MAX_MS = 30_000;
|
|
|
5
5
|
const DELIVERY_RETRY_JITTER_MS = 200;
|
|
6
6
|
const DEFAULT_RETENTION_MS = 5 * 60 * 1000;
|
|
7
7
|
const DEFAULT_MAX_RUNNING_JOBS = 15;
|
|
8
|
+
/** Abort reason used only when the owning session shuts down the entire manager. */
|
|
9
|
+
export const ASYNC_JOB_MANAGER_SHUTDOWN_REASON = Symbol("AsyncJobManager shutdown");
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Adaptive ("smart") `hub` poll-wait ladder (ms). A tight poll loop climbs
|
|
@@ -414,11 +416,20 @@ export class AsyncJobManager {
|
|
|
414
416
|
* Cancel running jobs. With `filter.ownerId` set, cancels only jobs the
|
|
415
417
|
* matching agent registered; with no filter, cancels every running job
|
|
416
418
|
* (used by `dispose()` to nuke the manager's state).
|
|
419
|
+
*
|
|
420
|
+
* `reason` is forwarded to each job's `AbortController.abort`, so a session
|
|
421
|
+
* teardown can tag its owned jobs with {@link ASYNC_JOB_MANAGER_SHUTDOWN_REASON}
|
|
422
|
+
* before `dispose()` runs — the task executor reads it to park (not
|
|
423
|
+
* tombstone) a subagent interrupted purely by process shutdown.
|
|
417
424
|
*/
|
|
418
|
-
cancelAll(filter?: AsyncJobFilter): void {
|
|
425
|
+
cancelAll(filter?: AsyncJobFilter, reason?: unknown): void {
|
|
426
|
+
this.#cancelJobs(filter, reason);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#cancelJobs(filter?: AsyncJobFilter, reason?: unknown): void {
|
|
419
430
|
for (const job of this.getRunningJobs(filter)) {
|
|
420
431
|
job.status = "cancelled";
|
|
421
|
-
job.abortController.abort();
|
|
432
|
+
job.abortController.abort(reason);
|
|
422
433
|
this.#scheduleEviction(job.id);
|
|
423
434
|
}
|
|
424
435
|
}
|
|
@@ -584,7 +595,7 @@ export class AsyncJobManager {
|
|
|
584
595
|
async dispose(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
585
596
|
this.#disposed = true;
|
|
586
597
|
this.#clearEvictionTimers();
|
|
587
|
-
this
|
|
598
|
+
this.#cancelJobs(undefined, ASYNC_JOB_MANAGER_SHUTDOWN_REASON);
|
|
588
599
|
const timeoutMs = Math.max(options?.timeoutMs ?? 3_000, 0);
|
|
589
600
|
const deadline = Date.now() + timeoutMs;
|
|
590
601
|
const jobsSettled = await this.#waitForAllUntil(deadline);
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -464,7 +464,7 @@ async function handleInstall(
|
|
|
464
464
|
async function handleUninstall(
|
|
465
465
|
manager: PluginManager,
|
|
466
466
|
packages: string[],
|
|
467
|
-
flags: { json?: boolean; scope?: "user" | "project" },
|
|
467
|
+
flags: { json?: boolean; dryRun?: boolean; scope?: "user" | "project" },
|
|
468
468
|
): Promise<void> {
|
|
469
469
|
if (packages.length === 0) {
|
|
470
470
|
console.error(chalk.red(`Usage: ${APP_NAME} plugin uninstall <package> ...`));
|
|
@@ -477,7 +477,35 @@ async function handleUninstall(
|
|
|
477
477
|
const installedPlugins = new Set((await mktMgr.listInstalledPlugins()).map(p => p.id));
|
|
478
478
|
|
|
479
479
|
for (const name of packages) {
|
|
480
|
-
|
|
480
|
+
const viaMarketplace = installedPlugins.has(name);
|
|
481
|
+
|
|
482
|
+
if (flags.dryRun) {
|
|
483
|
+
if (viaMarketplace) {
|
|
484
|
+
try {
|
|
485
|
+
await mktMgr.uninstallPlugin(name, flags.scope, { dryRun: true });
|
|
486
|
+
} catch (err) {
|
|
487
|
+
console.error(chalk.red(`${theme.status.error} Failed to uninstall ${name}: ${err}`));
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Marketplace dry-runs validate the requested scope before reporting.
|
|
493
|
+
if (flags.json) {
|
|
494
|
+
console.log(
|
|
495
|
+
JSON.stringify({
|
|
496
|
+
dryRun: true,
|
|
497
|
+
action: "uninstall",
|
|
498
|
+
plugin: name,
|
|
499
|
+
source: viaMarketplace ? "marketplace" : "npm",
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
} else {
|
|
503
|
+
console.log(chalk.dim(`[dry-run] Would uninstall ${name}`));
|
|
504
|
+
}
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (viaMarketplace) {
|
|
481
509
|
// Exact match against installed marketplace plugin IDs (name@marketplace)
|
|
482
510
|
try {
|
|
483
511
|
await mktMgr.uninstallPlugin(name, flags.scope);
|