@oh-my-pi/pi-coding-agent 17.1.3 → 17.1.4
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 +54 -0
- package/dist/cli.js +3759 -3751
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +1 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/cli/update-cli.d.ts +41 -0
- package/dist/types/cli/usage-cli.d.ts +4 -2
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +19 -0
- package/dist/types/config/settings-schema.d.ts +30 -7
- package/dist/types/cursor.d.ts +47 -1
- package/dist/types/eval/js/process-entry.d.ts +2 -1
- package/dist/types/eval/js/worker-core.d.ts +4 -1
- package/dist/types/extensibility/extensions/runner.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +15 -7
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +9 -0
- package/dist/types/modes/components/custom-editor.d.ts +12 -8
- package/dist/types/plan-mode/approved-plan.d.ts +3 -2
- package/dist/types/secrets/obfuscator.d.ts +7 -31
- package/dist/types/session/agent-session.d.ts +19 -4
- package/dist/types/session/prewalk.d.ts +2 -1
- package/dist/types/session/session-advisors.d.ts +8 -0
- package/dist/types/session/session-metadata.d.ts +29 -0
- package/dist/types/session/turn-recovery.d.ts +5 -5
- package/dist/types/stt/sherpa-runtime.d.ts +38 -0
- package/dist/types/thinking.d.ts +24 -0
- package/dist/types/tools/bash.d.ts +5 -4
- package/dist/types/tools/computer/exposure.d.ts +8 -0
- package/dist/types/tools/computer/supervisor.d.ts +1 -1
- package/dist/types/tools/computer/worker-entry.d.ts +1 -1
- package/dist/types/tools/computer/worker.d.ts +1 -1
- package/dist/types/tools/computer.d.ts +6 -0
- package/dist/types/tools/shell-tokenize.d.ts +14 -0
- package/dist/types/tools/todo.d.ts +7 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +80 -10
- package/src/advisor/runtime.ts +27 -1
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +111 -0
- package/src/cli/auth-gateway-cli.ts +63 -16
- package/src/cli/config-cli.ts +25 -7
- package/src/cli/update-cli.ts +229 -29
- package/src/cli/usage-cli.ts +144 -15
- package/src/cli.ts +21 -15
- package/src/commands/update.ts +6 -0
- package/src/config/__tests__/model-registry.test.ts +42 -7
- package/src/config/model-registry.ts +67 -4
- package/src/config/settings-schema.ts +39 -8
- package/src/config/settings.ts +24 -2
- package/src/cursor.ts +153 -0
- package/src/dap/session.ts +70 -11
- package/src/eval/__tests__/js-context-manager.test.ts +9 -1
- package/src/eval/__tests__/process-entry-import.test.ts +111 -1
- package/src/eval/js/process-entry.ts +9 -5
- package/src/eval/js/worker-core.ts +6 -2
- package/src/exec/bash-executor.ts +4 -2
- package/src/extensibility/extensions/runner.ts +23 -8
- package/src/extensibility/legacy-pi-ai-shim.ts +9 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +14 -2
- package/src/extensibility/legacy-pi-tui-shim.ts +33 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/main.ts +22 -1
- package/src/modes/acp/acp-agent.ts +6 -0
- package/src/modes/components/assistant-message.ts +88 -11
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor.test.ts +170 -0
- package/src/modes/components/custom-editor.ts +79 -29
- package/src/modes/components/settings-defs.ts +5 -1
- package/src/modes/controllers/event-controller.ts +96 -4
- package/src/modes/controllers/selector-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +34 -8
- package/src/modes/utils/context-usage.ts +19 -2
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/plan-mode/approved-plan.ts +18 -10
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/sdk.ts +4 -0
- package/src/secrets/obfuscator.ts +36 -126
- package/src/session/agent-session.ts +69 -60
- package/src/session/prewalk.ts +8 -3
- package/src/session/session-advisors.ts +67 -3
- package/src/session/session-metadata.ts +53 -0
- package/src/session/session-provider-boundary.ts +0 -1
- package/src/session/session-tools.ts +35 -1
- package/src/session/turn-recovery.ts +36 -19
- package/src/slash-commands/builtin-registry.ts +49 -7
- package/src/stt/asr-worker.ts +2 -37
- package/src/stt/sherpa-runtime.ts +71 -0
- package/src/task/executor.ts +6 -5
- package/src/thinking.ts +39 -0
- package/src/tools/bash.ts +43 -15
- package/src/tools/computer/exposure.ts +38 -0
- package/src/tools/computer/supervisor.ts +61 -13
- package/src/tools/computer/worker-entry.ts +28 -19
- package/src/tools/computer/worker.ts +3 -7
- package/src/tools/computer.ts +65 -10
- package/src/tools/gh-cache-invalidation.ts +2 -82
- package/src/tools/shell-tokenize.ts +83 -0
- package/src/tools/todo.ts +44 -17
|
@@ -243,11 +243,6 @@ const HASH_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
|
243
243
|
// base. A collision would let a persisted placeholder deobfuscate to the wrong
|
|
244
244
|
// secret when the configured secret set or its ordering changes across sessions.
|
|
245
245
|
const HASH_LEN = 12;
|
|
246
|
-
// Pre-friendly-name sessions persisted a 4-char, index-derived token; reproduce
|
|
247
|
-
// that exact legacy format so old session text still deobfuscates. The legacy
|
|
248
|
-
// token is keyed on the entry index, not the secret value, so it leaks nothing.
|
|
249
|
-
const LEGACY_HASH_LEN = 4;
|
|
250
|
-
const LEGACY_HASH_SEED = 0x5345_4352;
|
|
251
246
|
const MAX_FRIENDLY_NAME_LEN = 32;
|
|
252
247
|
// Plain/regex obfuscate matches shorter than this are toned down (never placed
|
|
253
248
|
// behind a reversible placeholder) to avoid redacting small words/fragments.
|
|
@@ -458,17 +453,6 @@ function buildKeyedReplacementRun(key: string, length: number): string {
|
|
|
458
453
|
return out;
|
|
459
454
|
}
|
|
460
455
|
|
|
461
|
-
/** Build the pre-friendly-name index-derived placeholder for session resume compatibility. */
|
|
462
|
-
function buildLegacyPlaceholder(index: number): string {
|
|
463
|
-
let v = Bun.hash.xxHash32(String(index), LEGACY_HASH_SEED);
|
|
464
|
-
let tag = "#";
|
|
465
|
-
for (let i = 0; i < LEGACY_HASH_LEN; i++) {
|
|
466
|
-
tag += HASH_CHARS[v % HASH_CHARS.length];
|
|
467
|
-
v = Math.floor(v / HASH_CHARS.length);
|
|
468
|
-
}
|
|
469
|
-
return `${tag}#`;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
456
|
function inferCaseHint(secret: string): PlaceholderCaseHint | undefined {
|
|
473
457
|
let hasCased = false;
|
|
474
458
|
let hasUpper = false;
|
|
@@ -502,21 +486,21 @@ function inferCaseHint(secret: string): PlaceholderCaseHint | undefined {
|
|
|
502
486
|
|
|
503
487
|
function buildPlaceholder(hint: PlaceholderCaseHint | undefined, base: string, friendlyName?: string): string {
|
|
504
488
|
const prefix = friendlyName ? `${friendlyName}_` : "";
|
|
505
|
-
return hint ?
|
|
489
|
+
return hint ? `$$${prefix}${base}:${hint}$$` : `$$${prefix}${base}$$`;
|
|
506
490
|
}
|
|
507
491
|
|
|
508
|
-
/** Regex
|
|
509
|
-
const PLACEHOLDER_RE =
|
|
492
|
+
/** Regex matching `$$HASH$$`, `$$HASH:U$$`, and `$$FRIENDLY_HASH(:hint)$$` placeholders. */
|
|
493
|
+
const PLACEHOLDER_RE = /\$\$(?:[A-Z0-9]+_)?[A-Z0-9]{4,}(?::[ULCM])?\$\$/g;
|
|
510
494
|
|
|
511
495
|
function resumePlaceholderScanAfterRejectedCandidate(match: RegExpExecArray): void {
|
|
512
496
|
// RegExp#exec does not find overlapping matches. Restart at the rejected
|
|
513
|
-
// candidate's closing
|
|
514
|
-
PLACEHOLDER_RE.lastIndex = match.index + match[0].length -
|
|
497
|
+
// candidate's closing delimiter, which can open an immediately adjacent placeholder.
|
|
498
|
+
PLACEHOLDER_RE.lastIndex = match.index + match[0].length - 2;
|
|
515
499
|
}
|
|
516
500
|
|
|
517
501
|
function placeholderWithoutFriendlyName(placeholder: string): string | undefined {
|
|
518
|
-
const match =
|
|
519
|
-
return match ?
|
|
502
|
+
const match = /^\$\$[A-Z0-9]+_([A-Z0-9]{4,}(?::[ULCM])?)\$\$$/.exec(placeholder);
|
|
503
|
+
return match ? `$$${match[1]}$$` : undefined;
|
|
520
504
|
}
|
|
521
505
|
|
|
522
506
|
function lookupFriendlyPlaceholderAlias(
|
|
@@ -529,14 +513,12 @@ function lookupFriendlyPlaceholderAlias(
|
|
|
529
513
|
return unprefixed !== undefined ? deobfuscateMap.get(unprefixed) : undefined;
|
|
530
514
|
}
|
|
531
515
|
|
|
532
|
-
const PENDING_PLACEHOLDER_SUFFIX_RE =
|
|
516
|
+
const PENDING_PLACEHOLDER_SUFFIX_RE = /(?:\$\$(?:[A-Z0-9]+_)?[A-Z0-9]*(?::[ULCM]?)?|\$)$/;
|
|
533
517
|
|
|
534
518
|
// Withhold a trailing run that could be the start of a placeholder from streamed
|
|
535
519
|
// deltas, so a partial token is never emitted before deobfuscation can replace
|
|
536
|
-
// it. A lone trailing
|
|
537
|
-
//
|
|
538
|
-
// corrupt the length-sliced live draft once the token completes. The final
|
|
539
|
-
// non-streamed flush re-emits any buffered tail, so nothing is lost.
|
|
520
|
+
// it. A lone trailing delimiter character is always buffered because it can open
|
|
521
|
+
// a placeholder; the final non-streamed flush re-emits it when no token follows.
|
|
540
522
|
export function stripPendingSecretPlaceholderSuffix(text: string): string {
|
|
541
523
|
const pendingPlaceholderStart = text.match(PENDING_PLACEHOLDER_SUFFIX_RE);
|
|
542
524
|
if (pendingPlaceholderStart?.index === undefined) return text;
|
|
@@ -582,13 +564,6 @@ export class SecretObfuscator {
|
|
|
582
564
|
* one without the key. */
|
|
583
565
|
#deobfuscateMap = new Map<string, { secret: string; recursive: boolean }>();
|
|
584
566
|
|
|
585
|
-
/** Legacy index-derived aliases (unkeyed `#XRRS#`), honored ONLY when replaying
|
|
586
|
-
* stored session content. They are deterministic and trivially guessable, so
|
|
587
|
-
* accepting them on live provider/tool-call paths would let a prompt-injected
|
|
588
|
-
* model synthesize one to exfiltrate a secret; they exist solely so sessions
|
|
589
|
-
* persisted before keyed placeholders still deobfuscate on resume/display. */
|
|
590
|
-
#legacyDeobfuscateMap = new Map<string, { secret: string; recursive: boolean }>();
|
|
591
|
-
|
|
592
567
|
/** Exact placeholder tokens generated by this obfuscator revision (no aliases). */
|
|
593
568
|
#generatedPlaceholders = new Set<string>();
|
|
594
569
|
|
|
@@ -669,7 +644,6 @@ export class SecretObfuscator {
|
|
|
669
644
|
}
|
|
670
645
|
}
|
|
671
646
|
let index = 0;
|
|
672
|
-
let legacyIndex = 0;
|
|
673
647
|
let hasRealSec = this.#regexEntries.length > 0;
|
|
674
648
|
for (const entry of entries) {
|
|
675
649
|
if (entry.type !== "plain") continue;
|
|
@@ -680,11 +654,6 @@ export class SecretObfuscator {
|
|
|
680
654
|
continue;
|
|
681
655
|
}
|
|
682
656
|
const placeholder = this.#createPlaceholder(entry.content, entry.friendlyName);
|
|
683
|
-
this.#legacyDeobfuscateMap.set(buildLegacyPlaceholder(legacyIndex), {
|
|
684
|
-
secret: entry.content,
|
|
685
|
-
recursive: false,
|
|
686
|
-
});
|
|
687
|
-
legacyIndex++;
|
|
688
657
|
this.#plainMappings.set(entry.content, index);
|
|
689
658
|
this.#obfuscateMappings.set(index, { secret: entry.content, placeholder });
|
|
690
659
|
this.#generatedPlaceholders.add(placeholder);
|
|
@@ -706,11 +675,6 @@ export class SecretObfuscator {
|
|
|
706
675
|
return this.#hasAny;
|
|
707
676
|
}
|
|
708
677
|
|
|
709
|
-
/** Whether stored-session restoration can resolve keyed or legacy placeholders. */
|
|
710
|
-
hasStoredSecrets(): boolean {
|
|
711
|
-
return this.#hasAny || this.#legacyDeobfuscateMap.size > 0;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
678
|
/** Obfuscate all secrets in text. Bidirectional placeholders for obfuscate mode, one-way for replace. */
|
|
715
679
|
obfuscate(text: string, sharedRegexSecretValues?: ReadonlySet<string>): string {
|
|
716
680
|
if (!this.#hasAny) return text;
|
|
@@ -904,24 +868,9 @@ export class SecretObfuscator {
|
|
|
904
868
|
return result;
|
|
905
869
|
}
|
|
906
870
|
|
|
907
|
-
/**
|
|
908
|
-
* Deobfuscate keyed placeholders back to original secrets for LIVE paths
|
|
909
|
-
* (provider output, tool-call arguments). Replace-mode is NOT reversed, and
|
|
910
|
-
* legacy index-derived aliases are intentionally ignored so a prompt-injected
|
|
911
|
-
* model cannot synthesize one to recover a secret.
|
|
912
|
-
*/
|
|
871
|
+
/** Deobfuscate keyed placeholders for provider output, tool-call arguments, replay, and display. */
|
|
913
872
|
deobfuscate(text: string): string {
|
|
914
|
-
return this.#deobfuscate(text
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
/**
|
|
918
|
-
* Deobfuscate stored session content for replay/display. Identical to
|
|
919
|
-
* {@link deobfuscate} but additionally honors legacy index-derived aliases so
|
|
920
|
-
* sessions persisted before keyed placeholders still resume correctly. Use
|
|
921
|
-
* only for trusted on-disk session content, never for live model output.
|
|
922
|
-
*/
|
|
923
|
-
deobfuscateStored(text: string): string {
|
|
924
|
-
return this.#deobfuscate(text, true);
|
|
873
|
+
return this.#deobfuscate(text);
|
|
925
874
|
}
|
|
926
875
|
|
|
927
876
|
// Reverse-direction counterpart to `#isGeneratedPlaceholder`'s guard: the
|
|
@@ -929,7 +878,7 @@ export class SecretObfuscator {
|
|
|
929
878
|
// placeholder minted under a renamed friendly name still deobfuscates
|
|
930
879
|
// (see `#prefixIsSecretShaped`'s docstring for why), but unconditionally
|
|
931
880
|
// stripping and ignoring an attacker-authored prefix would let a forged
|
|
932
|
-
// token like
|
|
881
|
+
// token like `$$GITHUBPATABC123_<suffix-copied-from-any-real-placeholder>$$`
|
|
933
882
|
// restore to that OTHER secret's raw value with no check at all — worse
|
|
934
883
|
// than the obfuscate-direction leak, since deobfuscation is what feeds
|
|
935
884
|
// tool-call arguments and provider-output restoration. Refuse the
|
|
@@ -939,14 +888,14 @@ export class SecretObfuscator {
|
|
|
939
888
|
#lookupLiveAlias(placeholder: string): { secret: string; recursive: boolean } | undefined {
|
|
940
889
|
const direct = this.#deobfuscateMap.get(placeholder);
|
|
941
890
|
if (direct !== undefined) return direct;
|
|
942
|
-
const
|
|
891
|
+
const body = placeholder.slice(2, -2);
|
|
892
|
+
const match = /^([A-Z0-9]+)_([A-Z0-9]{4,}(?::[ULCM])?)$/.exec(body);
|
|
943
893
|
if (match === null || this.#prefixIsSecretShaped(match[1]!)) return undefined;
|
|
944
|
-
return this.#deobfuscateMap.get(
|
|
894
|
+
return this.#deobfuscateMap.get(`$$${match[2]}$$`);
|
|
945
895
|
}
|
|
946
896
|
|
|
947
|
-
#deobfuscate(text: string
|
|
948
|
-
if (
|
|
949
|
-
return text;
|
|
897
|
+
#deobfuscate(text: string): string {
|
|
898
|
+
if (!this.#hasAny || !text.includes("$$")) return text;
|
|
950
899
|
let result = text;
|
|
951
900
|
for (;;) {
|
|
952
901
|
let shouldContinue = false;
|
|
@@ -956,16 +905,9 @@ export class SecretObfuscator {
|
|
|
956
905
|
shouldContinue ||= mapped.recursive;
|
|
957
906
|
return mapped.secret;
|
|
958
907
|
}
|
|
959
|
-
if (allowLegacy) {
|
|
960
|
-
const legacy = this.#legacyDeobfuscateMap.get(match);
|
|
961
|
-
if (legacy !== undefined) {
|
|
962
|
-
shouldContinue ||= legacy.recursive;
|
|
963
|
-
return legacy.secret;
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
908
|
return match;
|
|
967
909
|
});
|
|
968
|
-
if (next === result || !shouldContinue || !next.includes("
|
|
910
|
+
if (next === result || !shouldContinue || !next.includes("$$")) return next;
|
|
969
911
|
result = next;
|
|
970
912
|
}
|
|
971
913
|
}
|
|
@@ -976,12 +918,6 @@ export class SecretObfuscator {
|
|
|
976
918
|
return deepWalkStrings(obj, s => this.deobfuscate(s));
|
|
977
919
|
}
|
|
978
920
|
|
|
979
|
-
/** Deep-walk stored session content, deobfuscating string values incl. legacy aliases. */
|
|
980
|
-
deobfuscateStoredObject<T>(obj: T): T {
|
|
981
|
-
if (!this.hasStoredSecrets()) return obj;
|
|
982
|
-
return deepWalkStrings(obj, s => this.deobfuscateStored(s));
|
|
983
|
-
}
|
|
984
|
-
|
|
985
921
|
/** Deep-walk an object, obfuscating all string values. */
|
|
986
922
|
obfuscateObject<T>(obj: T): T {
|
|
987
923
|
if (!this.#hasAny) return obj;
|
|
@@ -1390,7 +1326,7 @@ export class SecretObfuscator {
|
|
|
1390
1326
|
#placeholderForCurrentInput(placeholder: string): string {
|
|
1391
1327
|
const unprefixed = placeholderWithoutFriendlyName(placeholder);
|
|
1392
1328
|
if (unprefixed === undefined) return placeholder;
|
|
1393
|
-
const match =
|
|
1329
|
+
const match = /^([A-Z0-9]+)_/.exec(placeholder.slice(2, -2));
|
|
1394
1330
|
if (match === null || !this.#prefixIsSecretShaped(match[1]!)) return placeholder;
|
|
1395
1331
|
return unprefixed;
|
|
1396
1332
|
}
|
|
@@ -1492,10 +1428,10 @@ export class SecretObfuscator {
|
|
|
1492
1428
|
// deobfuscate-direction check in `#deobfuscate`.
|
|
1493
1429
|
#isGeneratedPlaceholder(placeholder: string): boolean {
|
|
1494
1430
|
if (this.#deobfuscateMap.has(placeholder)) return true;
|
|
1495
|
-
const match =
|
|
1431
|
+
const match = /^([A-Z0-9]+)_([A-Z0-9]{4,}(?::[ULCM])?)$/.exec(placeholder.slice(2, -2));
|
|
1496
1432
|
if (match === null) return false;
|
|
1497
1433
|
if (this.#prefixIsSecretShaped(match[1]!)) return false;
|
|
1498
|
-
return this.#deobfuscateMap.has(
|
|
1434
|
+
return this.#deobfuscateMap.has(`$$${match[2]}$$`);
|
|
1499
1435
|
}
|
|
1500
1436
|
|
|
1501
1437
|
// Replace `search` with `replacement` outside known generated placeholders while
|
|
@@ -1865,40 +1801,25 @@ export class SecretObfuscator {
|
|
|
1865
1801
|
* Restore secret placeholders for local display. Only message kinds the model
|
|
1866
1802
|
* itself authored from obfuscated context carry placeholders — assistant
|
|
1867
1803
|
* content and the LLM-written branch/compaction summaries. User, developer, and
|
|
1868
|
-
* tool-result messages are persisted with their literal text, so
|
|
1869
|
-
*
|
|
1870
|
-
* walked.
|
|
1871
|
-
*
|
|
1872
|
-
* Legacy index-derived aliases (`#XXXX#`) are unkeyed and trivially guessable,
|
|
1873
|
-
* so a prompt-injected model can plant one in any record it influences. Every
|
|
1874
|
-
* agent-feeding path (resume, history rewrite, branch switch) therefore restores
|
|
1875
|
-
* keyed placeholders ONLY (`allowLegacyAliases` false), leaving legacy tokens
|
|
1876
|
-
* inert; display-only transcripts that are never re-obfuscated opt in via
|
|
1877
|
-
* `allowLegacyAliases`.
|
|
1804
|
+
* tool-result messages are persisted with their literal text, so operator-authored
|
|
1805
|
+
* placeholder-shaped text must survive untouched; those roles are never walked.
|
|
1878
1806
|
*/
|
|
1879
1807
|
export function deobfuscateSessionContext(
|
|
1880
1808
|
sessionContext: SessionContext,
|
|
1881
1809
|
obfuscator: SecretObfuscator | undefined,
|
|
1882
|
-
allowLegacyAliases = false,
|
|
1883
1810
|
): SessionContext {
|
|
1884
|
-
if (!obfuscator
|
|
1885
|
-
|
|
1886
|
-
const messages = deobfuscateAgentMessages(obfuscator, sessionContext.messages, allowLegacyAliases);
|
|
1811
|
+
if (!obfuscator?.hasSecrets()) return sessionContext;
|
|
1812
|
+
const messages = deobfuscateAgentMessages(obfuscator, sessionContext.messages);
|
|
1887
1813
|
return messages === sessionContext.messages ? sessionContext : { ...sessionContext, messages };
|
|
1888
1814
|
}
|
|
1889
1815
|
|
|
1890
|
-
export function deobfuscateAgentMessages(
|
|
1891
|
-
|
|
1892
|
-
messages: AgentMessage[],
|
|
1893
|
-
allowLegacyAliases = false,
|
|
1894
|
-
): AgentMessage[] {
|
|
1895
|
-
const deob = (text: string): string =>
|
|
1896
|
-
allowLegacyAliases ? obfuscator.deobfuscateStored(text) : obfuscator.deobfuscate(text);
|
|
1816
|
+
export function deobfuscateAgentMessages(obfuscator: SecretObfuscator, messages: AgentMessage[]): AgentMessage[] {
|
|
1817
|
+
const deob = (text: string): string => obfuscator.deobfuscate(text);
|
|
1897
1818
|
let changed = false;
|
|
1898
1819
|
const result = messages.map((message): AgentMessage => {
|
|
1899
1820
|
switch (message.role) {
|
|
1900
1821
|
case "assistant": {
|
|
1901
|
-
const content = deobfuscateAssistantContent(obfuscator, message.content
|
|
1822
|
+
const content = deobfuscateAssistantContent(obfuscator, message.content);
|
|
1902
1823
|
if (content === message.content) return message;
|
|
1903
1824
|
changed = true;
|
|
1904
1825
|
return { ...message, content };
|
|
@@ -1912,10 +1833,7 @@ export function deobfuscateAgentMessages(
|
|
|
1912
1833
|
case "compactionSummary": {
|
|
1913
1834
|
const summary = deob(message.summary);
|
|
1914
1835
|
const shortSummary = message.shortSummary === undefined ? undefined : deob(message.shortSummary);
|
|
1915
|
-
const blocks =
|
|
1916
|
-
message.blocks === undefined
|
|
1917
|
-
? undefined
|
|
1918
|
-
: deobfuscateTextBlocks(obfuscator, message.blocks, allowLegacyAliases);
|
|
1836
|
+
const blocks = message.blocks === undefined ? undefined : deobfuscateTextBlocks(obfuscator, message.blocks);
|
|
1919
1837
|
if (summary === message.summary && shortSummary === message.shortSummary && blocks === message.blocks) {
|
|
1920
1838
|
return message;
|
|
1921
1839
|
}
|
|
@@ -1937,11 +1855,9 @@ export function deobfuscateAgentMessages(
|
|
|
1937
1855
|
export function deobfuscateAssistantContent(
|
|
1938
1856
|
obfuscator: SecretObfuscator,
|
|
1939
1857
|
content: AssistantMessage["content"],
|
|
1940
|
-
allowLegacyAliases = false,
|
|
1941
1858
|
): AssistantMessage["content"] {
|
|
1942
|
-
if (!
|
|
1943
|
-
const deob = (text: string): string =>
|
|
1944
|
-
allowLegacyAliases ? obfuscator.deobfuscateStored(text) : obfuscator.deobfuscate(text);
|
|
1859
|
+
if (!obfuscator.hasSecrets()) return content;
|
|
1860
|
+
const deob = (text: string): string => obfuscator.deobfuscate(text);
|
|
1945
1861
|
let changed = false;
|
|
1946
1862
|
const result = content.map((block): AssistantMessage["content"][number] => {
|
|
1947
1863
|
if (block.type === "text") {
|
|
@@ -1952,7 +1868,7 @@ export function deobfuscateAssistantContent(
|
|
|
1952
1868
|
}
|
|
1953
1869
|
|
|
1954
1870
|
if (block.type === "toolCall") {
|
|
1955
|
-
const args = deobfuscateToolArguments(obfuscator, block.arguments
|
|
1871
|
+
const args = deobfuscateToolArguments(obfuscator, block.arguments);
|
|
1956
1872
|
const intent = block.intent === undefined ? undefined : deob(block.intent);
|
|
1957
1873
|
const rawBlock = block.rawBlock === undefined ? undefined : deob(block.rawBlock);
|
|
1958
1874
|
if (args === block.arguments && intent === block.intent && rawBlock === block.rawBlock) return block;
|
|
@@ -1972,12 +1888,9 @@ export function deobfuscateAssistantContent(
|
|
|
1972
1888
|
export function deobfuscateToolArguments(
|
|
1973
1889
|
obfuscator: SecretObfuscator,
|
|
1974
1890
|
args: Record<string, unknown>,
|
|
1975
|
-
allowLegacyAliases = false,
|
|
1976
1891
|
): Record<string, unknown> {
|
|
1977
|
-
if (!
|
|
1978
|
-
|
|
1979
|
-
allowLegacyAliases ? obfuscator.deobfuscateStored(text) : obfuscator.deobfuscate(text);
|
|
1980
|
-
return mapJsonStrings(args as JsonValue, deob) as Record<string, unknown>;
|
|
1892
|
+
if (!obfuscator.hasSecrets()) return args;
|
|
1893
|
+
return mapJsonStrings(args as JsonValue, s => obfuscator.deobfuscate(s)) as Record<string, unknown>;
|
|
1981
1894
|
}
|
|
1982
1895
|
|
|
1983
1896
|
/** Redact secrets inside a tool call's arguments (same JSON-walk exception as {@link deobfuscateToolArguments}). */
|
|
@@ -2018,14 +1931,11 @@ function obfuscateTextBlocks(
|
|
|
2018
1931
|
function deobfuscateTextBlocks(
|
|
2019
1932
|
obfuscator: SecretObfuscator,
|
|
2020
1933
|
content: (TextContent | ImageContent)[],
|
|
2021
|
-
allowLegacyAliases = false,
|
|
2022
1934
|
): (TextContent | ImageContent)[] {
|
|
2023
|
-
const deob = (text: string): string =>
|
|
2024
|
-
allowLegacyAliases ? obfuscator.deobfuscateStored(text) : obfuscator.deobfuscate(text);
|
|
2025
1935
|
let changed = false;
|
|
2026
1936
|
const result = content.map((block): TextContent | ImageContent => {
|
|
2027
1937
|
if (block.type !== "text") return block;
|
|
2028
|
-
const text =
|
|
1938
|
+
const text = obfuscator.deobfuscate(block.text);
|
|
2029
1939
|
if (text === block.text) return block;
|
|
2030
1940
|
changed = true;
|
|
2031
1941
|
return { ...block, text };
|
|
@@ -71,7 +71,7 @@ import type {
|
|
|
71
71
|
ToolResultMessage,
|
|
72
72
|
UsageReport,
|
|
73
73
|
} from "@oh-my-pi/pi-ai";
|
|
74
|
-
import {
|
|
74
|
+
import { type Effort, streamSimple } from "@oh-my-pi/pi-ai";
|
|
75
75
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
76
76
|
import { resetOpenAICodexHistoryAfterCompaction } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
|
|
77
77
|
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
@@ -81,7 +81,6 @@ import {
|
|
|
81
81
|
escapeXmlText,
|
|
82
82
|
formatDuration,
|
|
83
83
|
getAgentDbPath,
|
|
84
|
-
getInstallId,
|
|
85
84
|
isBunTestRuntime,
|
|
86
85
|
isEnoent,
|
|
87
86
|
isInteractiveHost,
|
|
@@ -229,7 +228,6 @@ import {
|
|
|
229
228
|
type AsyncResultEntry,
|
|
230
229
|
buildAsyncResultBatchMessage,
|
|
231
230
|
} from "./async-job-delivery";
|
|
232
|
-
import type { AuthStorage } from "./auth-storage";
|
|
233
231
|
import { BashRunner, type BashRunnerHost } from "./bash-runner";
|
|
234
232
|
import {
|
|
235
233
|
checkpointStartedAtFromEntry,
|
|
@@ -304,6 +302,7 @@ import {
|
|
|
304
302
|
} from "./session-maintenance";
|
|
305
303
|
import { cleanupEmptyMoveSession, type SessionManager } from "./session-manager";
|
|
306
304
|
import { SessionMemory, type SessionMemoryHost } from "./session-memory";
|
|
305
|
+
import { buildSessionMetadata } from "./session-metadata";
|
|
307
306
|
import { SessionProviderBoundary, type SessionProviderBoundaryHost } from "./session-provider-boundary";
|
|
308
307
|
import { SessionStatsTracker, type SessionStatsTrackerHost } from "./session-stats";
|
|
309
308
|
import { SessionTools, type SessionToolsHost } from "./session-tools";
|
|
@@ -330,53 +329,6 @@ const PLAN_MODE_REMINDER_MAX = 3;
|
|
|
330
329
|
// Constants
|
|
331
330
|
// ============================================================================
|
|
332
331
|
|
|
333
|
-
/** Standard thinking levels */
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Build the per-request `metadata` payload for the Anthropic provider, shaped
|
|
337
|
-
* like real Claude Code's `getAPIMetadata` output (`{ session_id, account_uuid,
|
|
338
|
-
* device_id }`) so the backend buckets requests under one session and attributes
|
|
339
|
-
* them to the authenticated OAuth account when available. Resolved at request
|
|
340
|
-
* time so token refreshes and login/logout transitions don't strand a stale
|
|
341
|
-
* account UUID in memory. `account_uuid` and `device_id` are omitted for
|
|
342
|
-
* non-Anthropic providers to avoid leaking the user's Claude identity to
|
|
343
|
-
* third-party APIs (including Anthropic-format-compatible proxies such as
|
|
344
|
-
* cloudflare-ai-gateway or gitlab-duo).
|
|
345
|
-
*
|
|
346
|
-
* `provider` is the target provider string (e.g. `"anthropic"`) and gates the
|
|
347
|
-
* `account_uuid` and `device_id` lookups — only `"anthropic"` requests carry them.
|
|
348
|
-
*
|
|
349
|
-
* `sessionId` is forwarded to the auth-storage session-sticky lookup so that
|
|
350
|
-
* multi-credential setups attribute to the same OAuth account used for the
|
|
351
|
-
* actual API request rather than always picking the first credential.
|
|
352
|
-
*
|
|
353
|
-
* `authStorage` is treated as optional so test fixtures that stub `modelRegistry`
|
|
354
|
-
* without a real storage layer still work; the resolver simply skips the lookup
|
|
355
|
-
* and emits `{ session_id }` alone, matching the no-OAuth-credential path.
|
|
356
|
-
*/
|
|
357
|
-
function buildSessionMetadata(
|
|
358
|
-
sessionId: string,
|
|
359
|
-
provider: string,
|
|
360
|
-
authStorage: AuthStorage | undefined,
|
|
361
|
-
): Record<string, unknown> {
|
|
362
|
-
const userId: Record<string, string> = { session_id: sessionId };
|
|
363
|
-
// Only look up account_uuid when the request is going to Anthropic. Injecting
|
|
364
|
-
// a Claude OAuth account_uuid into requests bound for other providers (including
|
|
365
|
-
// Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
|
|
366
|
-
// would leak the user's Anthropic identity to unrelated third-party APIs.
|
|
367
|
-
if (provider === "anthropic") {
|
|
368
|
-
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
|
|
369
|
-
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
370
|
-
userId.account_uuid = accountUuid;
|
|
371
|
-
// Claude Code's `device_id` is a stable 64-hex account-scoped install
|
|
372
|
-
// identifier. Include both omp's persistent install id and the Claude
|
|
373
|
-
// account UUID so two accounts on the same install do not share a device.
|
|
374
|
-
userId.device_id = deriveClaudeDeviceId(getInstallId(), accountUuid);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
return { user_id: JSON.stringify(userId) };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
332
|
const noOpUIContext: ExtensionUIContext = {
|
|
381
333
|
select: async (_title, _options, _dialogOptions) => undefined,
|
|
382
334
|
confirm: async (_title, _message, _dialogOptions) => false,
|
|
@@ -901,6 +853,7 @@ export class AgentSession {
|
|
|
901
853
|
agent: this.agent,
|
|
902
854
|
sessionManager: this.sessionManager,
|
|
903
855
|
model: () => this.model,
|
|
856
|
+
configuredThinkingLevel: () => this.configuredThinkingLevel(),
|
|
904
857
|
emitNotice: (level, message, source) => this.emitNotice(level, message, source),
|
|
905
858
|
setModelTemporary: (model, thinkingLevel, options) => this.setModelTemporary(model, thinkingLevel, options),
|
|
906
859
|
setActiveToolsByName: names => this.setActiveToolsByName(names),
|
|
@@ -2249,7 +2202,7 @@ export class AgentSession {
|
|
|
2249
2202
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
2250
2203
|
// obfuscated placeholders, but listeners (TUI, extensions, exporters) must see real
|
|
2251
2204
|
// values. The original event.message stays obfuscated so the persistence path below
|
|
2252
|
-
// writes
|
|
2205
|
+
// writes `$$HASH$$` tokens to the session file; convertToLlm re-obfuscates outbound
|
|
2253
2206
|
// traffic on the next turn. Walks text, thinking, and toolCall arguments/intent.
|
|
2254
2207
|
let displayEvent: AgentEvent = event;
|
|
2255
2208
|
const obfuscator = this.#obfuscator;
|
|
@@ -2632,8 +2585,14 @@ export class AgentSession {
|
|
|
2632
2585
|
return;
|
|
2633
2586
|
}
|
|
2634
2587
|
|
|
2635
|
-
|
|
2636
|
-
|
|
2588
|
+
const resolvedInterruptedToolTurn = this.#recovery.classifyResolvedInterruptedToolTurn(msg);
|
|
2589
|
+
if (this.#recovery.isRetryableReasonlessAbort(msg) || resolvedInterruptedToolTurn === "reasonless-abort") {
|
|
2590
|
+
const didRetry = await this.#recovery.handleRetryableError(
|
|
2591
|
+
msg,
|
|
2592
|
+
resolvedInterruptedToolTurn === "reasonless-abort"
|
|
2593
|
+
? { allowModelFallback: false, preserveFailedTurn: true }
|
|
2594
|
+
: { allowModelFallback: false },
|
|
2595
|
+
);
|
|
2637
2596
|
if (didRetry) {
|
|
2638
2597
|
await emitAgentEndNotification({ willContinue: true });
|
|
2639
2598
|
return;
|
|
@@ -2659,7 +2618,7 @@ export class AgentSession {
|
|
|
2659
2618
|
return;
|
|
2660
2619
|
}
|
|
2661
2620
|
}
|
|
2662
|
-
const resumeResolvedStreamStall =
|
|
2621
|
+
const resumeResolvedStreamStall = resolvedInterruptedToolTurn === "stream-stall";
|
|
2663
2622
|
if (resumeResolvedStreamStall || this.#recovery.isRetryableError(msg)) {
|
|
2664
2623
|
const didRetry = await this.#recovery.handleRetryableError(
|
|
2665
2624
|
msg,
|
|
@@ -3078,6 +3037,7 @@ export class AgentSession {
|
|
|
3078
3037
|
session_id: this.sessionId,
|
|
3079
3038
|
session_file: this.sessionFile,
|
|
3080
3039
|
stop_hook_active: this.#sessionStopHookActive,
|
|
3040
|
+
signal: this.#postPromptTasksAbortController.signal,
|
|
3081
3041
|
});
|
|
3082
3042
|
if (this.#promptGeneration !== generation || this.#abortInProgress || this.#isDisposed) {
|
|
3083
3043
|
this.#resetSessionStopContinuationState();
|
|
@@ -3336,6 +3296,12 @@ export class AgentSession {
|
|
|
3336
3296
|
this.agent.setMetadataResolver((provider: string) =>
|
|
3337
3297
|
buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
|
|
3338
3298
|
);
|
|
3299
|
+
// Keep every live advisor's provider identity in lockstep with the primary's
|
|
3300
|
+
// across every session-boundary transition — including branch paths that
|
|
3301
|
+
// skip conversation restore — so advisors never emit the previous
|
|
3302
|
+
// conversation's session id/metadata (issue #6625). Guarded because this
|
|
3303
|
+
// runs once during construction before the advisor controller exists.
|
|
3304
|
+
if (this.#advisors) this.#advisors.refreshProviderIdentity();
|
|
3339
3305
|
}
|
|
3340
3306
|
|
|
3341
3307
|
/** Run one abortable auto-learn capture outside the primary agent loop. */
|
|
@@ -4097,10 +4063,7 @@ export class AgentSession {
|
|
|
4097
4063
|
* Transcript for TUI display. Full history is kept for export/resume-style
|
|
4098
4064
|
* callers; live chat can collapse compacted history to keep the hot render
|
|
4099
4065
|
* surface bounded. Display-only — NEVER feed the result to
|
|
4100
|
-
* `agent.replaceMessages` or a provider.
|
|
4101
|
-
* it opts into legacy index-derived alias restoration so pre-keyed sessions
|
|
4102
|
-
* still render their secrets; the agent-feeding paths
|
|
4103
|
-
* (`buildDisplaySessionContext`) keep the keyed-only default.
|
|
4066
|
+
* `agent.replaceMessages` or a provider.
|
|
4104
4067
|
*/
|
|
4105
4068
|
buildTranscriptSessionContext(
|
|
4106
4069
|
options?: Pick<BuildSessionContextOptions, "collapseCompactedHistory" | "keepDanglingToolCalls">,
|
|
@@ -7373,6 +7336,14 @@ export class AgentSession {
|
|
|
7373
7336
|
* (issue #5642).
|
|
7374
7337
|
*/
|
|
7375
7338
|
reopenAsk?: { toolCallId: string; questions: AskToolInput["questions"] };
|
|
7339
|
+
/**
|
|
7340
|
+
* `true` when this call committed a new sibling answer for an `ask`
|
|
7341
|
+
* re-answer (`reanswerAskResult` was applied). The interactive caller
|
|
7342
|
+
* resumes the agent via {@link resumeAfterAskReanswer} *after* rebuilding
|
|
7343
|
+
* its transcript, so the resumed turn never renders against the stale
|
|
7344
|
+
* pre-rebuild UI (issue #6483).
|
|
7345
|
+
*/
|
|
7346
|
+
askReanswerCommitted?: boolean;
|
|
7376
7347
|
}> {
|
|
7377
7348
|
await this.#bash.flushPending();
|
|
7378
7349
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
@@ -7523,6 +7494,10 @@ export class AgentSession {
|
|
|
7523
7494
|
// Determine the new leaf position based on target type
|
|
7524
7495
|
let newLeafId: string | null;
|
|
7525
7496
|
let editorText: string | undefined;
|
|
7497
|
+
// Set when the second-pass `ask` re-answer branch below actually commits a
|
|
7498
|
+
// new sibling answer — the trigger for resuming the agent afterwards so the
|
|
7499
|
+
// model consumes it, mirroring a live `ask` completion (issue #6483).
|
|
7500
|
+
let isAskReanswerCompletion = false;
|
|
7526
7501
|
|
|
7527
7502
|
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
|
|
7528
7503
|
// User message: leaf = parent (null if root), text goes to editor
|
|
@@ -7559,6 +7534,7 @@ export class AgentSession {
|
|
|
7559
7534
|
timestamp: Date.now(),
|
|
7560
7535
|
};
|
|
7561
7536
|
newLeafId = this.sessionManager.appendMessageToBranch(toolResultMessage, targetEntry.parentId);
|
|
7537
|
+
isAskReanswerCompletion = true;
|
|
7562
7538
|
} else {
|
|
7563
7539
|
// Non-user message (or a user-invoked skill-prompt injection): land the
|
|
7564
7540
|
// leaf on the selected node so it stays on the active branch. Skill
|
|
@@ -7604,6 +7580,14 @@ export class AgentSession {
|
|
|
7604
7580
|
|
|
7605
7581
|
this.#branchSummaryAbortController = undefined;
|
|
7606
7582
|
|
|
7583
|
+
// Report a committed `ask` re-answer so the interactive caller can resume
|
|
7584
|
+
// the agent via `resumeAfterAskReanswer()` *after* rebuilding its
|
|
7585
|
+
// transcript. Scheduling the continue here instead would start a fresh
|
|
7586
|
+
// streaming turn whose `agent_start`/`turn_start` events could render
|
|
7587
|
+
// against the stale pre-rebuild UI and then be clobbered by the caller's
|
|
7588
|
+
// `renderInitialMessages(...)` (issue #6483). Plain leaf moves and the
|
|
7589
|
+
// read-only `reopenAsk` probe leave the flag unset.
|
|
7590
|
+
|
|
7607
7591
|
// Emit session_tree event; only handlers can mutate session entries, so skip
|
|
7608
7592
|
// the emit and the context rebuild when no handlers are registered (mirrors
|
|
7609
7593
|
// the session_before_tree guard above).
|
|
@@ -7616,9 +7600,34 @@ export class AgentSession {
|
|
|
7616
7600
|
fromExtension: summaryText ? fromExtension : undefined,
|
|
7617
7601
|
});
|
|
7618
7602
|
const rawContext = this.sessionManager.buildSessionContext();
|
|
7619
|
-
return {
|
|
7603
|
+
return {
|
|
7604
|
+
editorText,
|
|
7605
|
+
cancelled: false,
|
|
7606
|
+
summaryEntry,
|
|
7607
|
+
sessionContext: rawContext,
|
|
7608
|
+
askReanswerCommitted: isAskReanswerCompletion,
|
|
7609
|
+
};
|
|
7620
7610
|
}
|
|
7621
|
-
return {
|
|
7611
|
+
return {
|
|
7612
|
+
editorText,
|
|
7613
|
+
cancelled: false,
|
|
7614
|
+
summaryEntry,
|
|
7615
|
+
sessionContext: stateContext,
|
|
7616
|
+
askReanswerCommitted: isAskReanswerCompletion,
|
|
7617
|
+
};
|
|
7618
|
+
}
|
|
7619
|
+
|
|
7620
|
+
/**
|
|
7621
|
+
* Resume the agent after the interactive `/tree` caller has committed an
|
|
7622
|
+
* `ask` re-answer (`navigateTree` returned `askReanswerCommitted`) and
|
|
7623
|
+
* rebuilt its transcript. Mirrors how a live `ask` completion drives a
|
|
7624
|
+
* follow-up turn, but is deferred to the caller so the resumed turn renders
|
|
7625
|
+
* against the rebuilt UI rather than the stale pre-navigation transcript
|
|
7626
|
+
* (issue #6483). The scheduled continue honors the same disposed/compacting
|
|
7627
|
+
* guards as every other post-prompt continuation.
|
|
7628
|
+
*/
|
|
7629
|
+
resumeAfterAskReanswer(): void {
|
|
7630
|
+
this.#scheduleAgentContinue();
|
|
7622
7631
|
}
|
|
7623
7632
|
|
|
7624
7633
|
/**
|
package/src/session/prewalk.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Agent, AgentMessage, AgentToolResult, AgentTurnEndContext } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { invalidateMessageCache } from "@oh-my-pi/pi-agent-core/compaction";
|
|
3
3
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
5
4
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
6
5
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
7
6
|
import { resolveApprovedPlan } from "../plan-mode/approved-plan";
|
|
@@ -11,7 +10,7 @@ import planYoloHandoffPrompt from "../prompts/system/plan-yolo-handoff.md" with
|
|
|
11
10
|
import prewalkChecklistPrompt from "../prompts/system/prewalk-checklist.md" with { type: "text" };
|
|
12
11
|
import prewalkContinuePrompt from "../prompts/system/prewalk-continue.md" with { type: "text" };
|
|
13
12
|
import prewalkPlanPrompt from "../prompts/system/prewalk-plan.md" with { type: "text" };
|
|
14
|
-
import type
|
|
13
|
+
import { type ConfiguredThinkingLevel, prewalkWouldBeNoop } from "../thinking";
|
|
15
14
|
import type { PlanProposalHandler } from "../tools/resolve";
|
|
16
15
|
import { ToolError } from "../tools/tool-errors";
|
|
17
16
|
import type { PlanYolo, Prewalk } from "./agent-session-types";
|
|
@@ -31,6 +30,7 @@ export interface PrewalkCoordinatorHost {
|
|
|
31
30
|
agent: Agent;
|
|
32
31
|
sessionManager: SessionManager;
|
|
33
32
|
model(): Model | undefined;
|
|
33
|
+
configuredThinkingLevel(): ConfiguredThinkingLevel | undefined;
|
|
34
34
|
emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
|
|
35
35
|
setModelTemporary(
|
|
36
36
|
model: Model,
|
|
@@ -126,8 +126,13 @@ export class PrewalkCoordinator {
|
|
|
126
126
|
this.#scrubPlanNudge(liveMessages);
|
|
127
127
|
const target = prewalk.target;
|
|
128
128
|
const currentModel = this.#host.model();
|
|
129
|
-
if (currentModel
|
|
129
|
+
if (prewalkWouldBeNoop(currentModel, this.#host.configuredThinkingLevel(), target, prewalk.thinkingLevel)) {
|
|
130
130
|
this.#prewalk = undefined;
|
|
131
|
+
this.#host.emitNotice(
|
|
132
|
+
"info",
|
|
133
|
+
`Prewalk: target ${target.provider}/${target.id} already matches the active model and thinking level; nothing to switch.`,
|
|
134
|
+
"prewalk",
|
|
135
|
+
);
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
133
138
|
await this.#host.setModelTemporary(target, prewalk.thinkingLevel, { ephemeral: true });
|