pi-ui-extend 0.1.57 → 0.1.60
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/dist/app/app.js +2 -0
- package/dist/app/commands/command-session-actions.js +3 -2
- package/dist/app/extensions/extension-actions-controller.js +1 -3
- package/dist/app/model/model-usage-status.js +26 -10
- package/dist/app/rendering/conversation-entry-renderer.d.ts +3 -0
- package/dist/app/rendering/conversation-entry-renderer.js +2 -0
- package/dist/app/rendering/conversation-viewport.d.ts +3 -0
- package/dist/app/rendering/conversation-viewport.js +4 -0
- package/dist/app/rendering/extension-entry-renderer.d.ts +6 -0
- package/dist/app/rendering/extension-entry-renderer.js +100 -0
- package/dist/app/rendering/tool-block-renderer.d.ts +5 -0
- package/dist/app/rendering/tool-block-renderer.js +1 -1
- package/dist/app/screen/mouse-controller.js +1 -1
- package/dist/app/session/lazy-session-manager.js +18 -1
- package/dist/app/session/pix-system-message.d.ts +1 -0
- package/dist/app/session/pix-system-message.js +4 -0
- package/dist/app/session/session-event-controller.js +7 -1
- package/dist/app/session/session-history.d.ts +3 -0
- package/dist/app/session/session-history.js +19 -2
- package/dist/app/session/session-search.js +2 -0
- package/dist/app/session/session-stats.d.ts +3 -0
- package/dist/app/session/session-stats.js +65 -0
- package/dist/app/types.d.ts +6 -1
- package/dist/bundled-extensions/terminal-bell/index.js +1 -37
- package/external/pi-tools-suite/README.md +17 -1
- package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +102 -0
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +7 -7
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +0 -33
- package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +1 -1
- package/external/pi-tools-suite/src/codex-reasoning-fix/index.ts +7 -114
- package/external/pi-tools-suite/src/dcp/config.ts +27 -0
- package/external/pi-tools-suite/src/dcp/debug-log.ts +2 -1
- package/external/pi-tools-suite/src/dcp/index.ts +182 -20
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +85 -0
- package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +254 -0
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -0
- package/external/pi-tools-suite/src/dcp/pruner-types.ts +30 -0
- package/external/pi-tools-suite/src/dcp/pruner.ts +8 -0
- package/external/pi-tools-suite/src/dcp/state.ts +21 -6
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +22 -8
- package/external/pi-tools-suite/src/telegram-mirror/events.ts +9 -4
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
- package/package.json +4 -4
|
@@ -32,6 +32,10 @@ import {
|
|
|
32
32
|
getNudgeType,
|
|
33
33
|
detectCompressionCandidate,
|
|
34
34
|
detectMessageCompressionCandidates,
|
|
35
|
+
analyzeEmergencyCurrentTurn,
|
|
36
|
+
emergencyPressureState,
|
|
37
|
+
emergencyCurrentTurnMessageCandidates,
|
|
38
|
+
pruneEmergencyCurrentTurn,
|
|
35
39
|
appendConcreteNudgeGuidance,
|
|
36
40
|
applyAnchoredNudges,
|
|
37
41
|
clearDcpNudgeAnchors,
|
|
@@ -55,6 +59,10 @@ import { decideAutoCompress, createAutoCompressionBlock } from "./auto-compress.
|
|
|
55
59
|
import { DCP_STATS_MESSAGE_TYPE, registerCommands } from "./commands.js"
|
|
56
60
|
import { normalizeDcpContextUsage } from "./ui.js"
|
|
57
61
|
import { safeGetContextUsage } from "../context-usage.js"
|
|
62
|
+
import {
|
|
63
|
+
collectProviderToolResultEvidence,
|
|
64
|
+
providerPayloadIncludesToolResult,
|
|
65
|
+
} from "./provider-tool-results.js"
|
|
58
66
|
|
|
59
67
|
// ---------------------------------------------------------------------------
|
|
60
68
|
// Helpers
|
|
@@ -224,6 +232,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
224
232
|
|
|
225
233
|
// ── 2. Create state ───────────────────────────────────────────────────────
|
|
226
234
|
const state = createState()
|
|
235
|
+
const pendingProviderToolIds = new Set<string>()
|
|
227
236
|
const appendNudgeTelemetry = (
|
|
228
237
|
event: "emitted" | "upgraded" | "reapplied",
|
|
229
238
|
type: DcpNudgeType,
|
|
@@ -427,6 +436,8 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
427
436
|
let prunedMessages = applyPruning(contextMessages, state, effectiveConfig)
|
|
428
437
|
let candidate = null as ReturnType<typeof detectCompressionCandidate>
|
|
429
438
|
let messageCandidates = [] as ReturnType<typeof detectMessageCompressionCandidates>
|
|
439
|
+
let emergencySelection = null as ReturnType<typeof analyzeEmergencyCurrentTurn> | null
|
|
440
|
+
let emergencyPruneResult = null as ReturnType<typeof pruneEmergencyCurrentTurn> | null
|
|
430
441
|
|
|
431
442
|
// In manual mode we still apply pruning strategies (if
|
|
432
443
|
// automaticStrategies is on) but skip routine autonomous nudges. Emergency
|
|
@@ -472,15 +483,27 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
472
483
|
thresholds.maxContextPercent += Math.min(summaryBonus, SUMMARY_BUFFER_MAX_CONTEXT_BONUS)
|
|
473
484
|
}
|
|
474
485
|
|
|
475
|
-
const
|
|
486
|
+
const emergencySettings = effectiveConfig.strategies.emergencyCurrentTurnPruning
|
|
487
|
+
const {
|
|
488
|
+
hardEmergencyReached,
|
|
489
|
+
contextLimitReached,
|
|
490
|
+
emergencyPressureReached,
|
|
491
|
+
} = emergencyPressureState(
|
|
492
|
+
contextPercent,
|
|
493
|
+
thresholds.maxContextPercent,
|
|
494
|
+
emergencySettings.hardContextPercent,
|
|
495
|
+
)
|
|
476
496
|
const routineNudgesAllowed = contextPercent > thresholds.minContextPercent
|
|
477
|
-
if (!
|
|
497
|
+
if (!emergencyPressureReached && !routineNudgesAllowed) {
|
|
478
498
|
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
479
|
-
|
|
499
|
+
const resetEmergencyPasses = state.consecutiveIgnoredStrongNudges > 0
|
|
500
|
+
state.consecutiveIgnoredStrongNudges = 0
|
|
501
|
+
if (clearedAnchors > 0 || resetEmergencyPasses) await saveDcpState(ctx, state)
|
|
480
502
|
return finishContext("below-threshold", prunedMessages, {
|
|
481
503
|
contextPercent,
|
|
482
504
|
thresholds,
|
|
483
505
|
clearedAnchors,
|
|
506
|
+
resetEmergencyPasses,
|
|
484
507
|
})
|
|
485
508
|
}
|
|
486
509
|
|
|
@@ -505,8 +528,17 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
505
528
|
Number.isFinite(currentContextWindow) &&
|
|
506
529
|
currentContextWindow < previousContextWindow * 0.9 &&
|
|
507
530
|
contextPercent > thresholds.minContextPercent
|
|
531
|
+
const contextWindowChanged =
|
|
532
|
+
typeof previousContextWindow === "number" &&
|
|
533
|
+
Number.isFinite(previousContextWindow) &&
|
|
534
|
+
typeof currentContextWindow === "number" &&
|
|
535
|
+
Number.isFinite(currentContextWindow) &&
|
|
536
|
+
currentContextWindow !== previousContextWindow
|
|
537
|
+
if (contextWindowChanged) state.consecutiveIgnoredStrongNudges = 0
|
|
508
538
|
|
|
509
|
-
const nudgeType =
|
|
539
|
+
const nudgeType = hardEmergencyReached && !contextLimitReached
|
|
540
|
+
? "context-strong"
|
|
541
|
+
: windowDowngraded
|
|
510
542
|
? "context-strong"
|
|
511
543
|
: getNudgeType(
|
|
512
544
|
contextPercent,
|
|
@@ -543,11 +575,37 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
543
575
|
}, ctx)
|
|
544
576
|
}
|
|
545
577
|
|
|
546
|
-
const
|
|
578
|
+
const hasNormalCompressionSuggestion =
|
|
579
|
+
candidate !== null || messageCandidates.length > 0
|
|
580
|
+
if (
|
|
581
|
+
emergencySettings.enabled &&
|
|
582
|
+
emergencyPressureReached &&
|
|
583
|
+
!manualEmergencyOnly &&
|
|
584
|
+
!hasNormalCompressionSuggestion
|
|
585
|
+
) {
|
|
586
|
+
emergencySelection = analyzeEmergencyCurrentTurn(
|
|
587
|
+
prunedMessages,
|
|
588
|
+
state,
|
|
589
|
+
effectiveConfig,
|
|
590
|
+
)
|
|
591
|
+
messageCandidates = emergencyCurrentTurnMessageCandidates(
|
|
592
|
+
emergencySelection,
|
|
593
|
+
effectiveConfig,
|
|
594
|
+
)
|
|
595
|
+
writeDcpDebugLog(effectiveConfig, "context.strong_nudge_without_candidate", {
|
|
596
|
+
contextPercent,
|
|
597
|
+
thresholds,
|
|
598
|
+
nudgeType,
|
|
599
|
+
emergencyCandidates: messageCandidates.length,
|
|
600
|
+
...emergencySelection.stats,
|
|
601
|
+
state: summarizeDcpState(state),
|
|
602
|
+
}, ctx)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
let hasCompressionSuggestion =
|
|
547
606
|
candidate !== null || messageCandidates.length > 0
|
|
548
|
-
if (!manualEmergencyOnly && !hasCompressionSuggestion) {
|
|
607
|
+
if (!manualEmergencyOnly && !emergencyPressureReached && !hasCompressionSuggestion) {
|
|
549
608
|
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
550
|
-
state.consecutiveIgnoredStrongNudges = 0
|
|
551
609
|
if (clearedAnchors > 0) await saveDcpState(ctx, state)
|
|
552
610
|
if (nudgeType || clearedAnchors > 0) {
|
|
553
611
|
writeDcpDebugLog(effectiveConfig, "context.no_compression_candidate", {
|
|
@@ -560,19 +618,11 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
560
618
|
}
|
|
561
619
|
}
|
|
562
620
|
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
// between. Reset on non-strong nudges and when pressure drops below
|
|
567
|
-
// the emergency threshold (handled by the below-threshold early
|
|
568
|
-
// return above, which clears anchors; counter is also reset on any
|
|
569
|
-
// successful compress in the compress tool).
|
|
570
|
-
if (
|
|
571
|
-
hasCompressionSuggestion &&
|
|
572
|
-
(nudgeType === "context-strong" || nudgeType === "context-soft")
|
|
573
|
-
) {
|
|
621
|
+
// Emergency pressure must advance even when normal candidates are
|
|
622
|
+
// absent; otherwise the current-turn fallback can never reach patience.
|
|
623
|
+
if (emergencyPressureReached) {
|
|
574
624
|
state.consecutiveIgnoredStrongNudges += 1
|
|
575
|
-
} else
|
|
625
|
+
} else {
|
|
576
626
|
state.consecutiveIgnoredStrongNudges = 0
|
|
577
627
|
}
|
|
578
628
|
|
|
@@ -587,6 +637,17 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
587
637
|
thresholds.maxContextPercent,
|
|
588
638
|
candidate,
|
|
589
639
|
)
|
|
640
|
+
if (contextLimitReached && candidate === null) {
|
|
641
|
+
writeDcpDebugLog(effectiveConfig, "compress.auto_blocked_no_candidate", {
|
|
642
|
+
autoCompressEnabled: effectiveConfig.compress.autoCompress.enabled,
|
|
643
|
+
decisionReason: autoDecision.reason,
|
|
644
|
+
contextPercent,
|
|
645
|
+
thresholds,
|
|
646
|
+
consecutiveEmergencyPasses: state.consecutiveIgnoredStrongNudges,
|
|
647
|
+
...(emergencySelection?.stats ?? {}),
|
|
648
|
+
state: summarizeDcpState(state),
|
|
649
|
+
}, ctx)
|
|
650
|
+
}
|
|
590
651
|
if (autoDecision.shouldFire && candidate) {
|
|
591
652
|
try {
|
|
592
653
|
const autoResult = await createAutoCompressionBlock({
|
|
@@ -635,7 +696,64 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
635
696
|
}
|
|
636
697
|
}
|
|
637
698
|
|
|
638
|
-
|
|
699
|
+
// Model-independent safety floor for an unfinished active turn. Only
|
|
700
|
+
// result bodies are replaced; user messages and structural tool pairs
|
|
701
|
+
// stay intact. Fresh results are ineligible until before_provider_request
|
|
702
|
+
// records that the model has had a chance to consume them.
|
|
703
|
+
const emergencyPatienceExceeded =
|
|
704
|
+
state.consecutiveIgnoredStrongNudges > Math.max(0, Math.floor(emergencySettings.patience))
|
|
705
|
+
if (
|
|
706
|
+
emergencySettings.enabled &&
|
|
707
|
+
emergencyPressureReached &&
|
|
708
|
+
candidate === null &&
|
|
709
|
+
emergencySelection &&
|
|
710
|
+
emergencySelection.eligible.length > 0 &&
|
|
711
|
+
(hardEmergencyReached || emergencyPatienceExceeded)
|
|
712
|
+
) {
|
|
713
|
+
const selectionStatsBeforePrune = emergencySelection.stats
|
|
714
|
+
const configuredTarget = Math.max(0, Math.min(1, emergencySettings.targetContextPercent))
|
|
715
|
+
const emergencyMarginTarget = Math.max(0, thresholds.maxContextPercent * 0.9)
|
|
716
|
+
const targetContextPercent = Math.min(configuredTarget, emergencyMarginTarget)
|
|
717
|
+
const targetRecoveryTokens = Math.max(
|
|
718
|
+
1,
|
|
719
|
+
Math.ceil((contextPercent - targetContextPercent) * usage.contextWindow),
|
|
720
|
+
)
|
|
721
|
+
emergencyPruneResult = pruneEmergencyCurrentTurn(
|
|
722
|
+
emergencySelection,
|
|
723
|
+
state,
|
|
724
|
+
targetRecoveryTokens,
|
|
725
|
+
)
|
|
726
|
+
if (emergencyPruneResult.prunedToolCallIds.length > 0) {
|
|
727
|
+
prunedMessages = applyPruning(contextMessages, state, effectiveConfig)
|
|
728
|
+
state.consecutiveIgnoredStrongNudges = 0
|
|
729
|
+
emergencySelection = analyzeEmergencyCurrentTurn(prunedMessages, state, effectiveConfig)
|
|
730
|
+
messageCandidates = emergencyCurrentTurnMessageCandidates(emergencySelection, effectiveConfig)
|
|
731
|
+
hasCompressionSuggestion = candidate !== null || messageCandidates.length > 0
|
|
732
|
+
await saveDcpState(ctx, state)
|
|
733
|
+
writeDcpDebugLog(effectiveConfig, "prune.emergency_current_turn", {
|
|
734
|
+
trigger: hardEmergencyReached ? "hard-context-percent" : "ignored-emergency-reminders",
|
|
735
|
+
contextPercent,
|
|
736
|
+
thresholds,
|
|
737
|
+
targetContextPercent,
|
|
738
|
+
targetRecoveryTokens,
|
|
739
|
+
prunedOutputs: emergencyPruneResult.prunedToolCallIds.length,
|
|
740
|
+
estimatedTokensRecovered: emergencyPruneResult.estimatedTokensRecovered,
|
|
741
|
+
estimatedContextPercentAfter: Math.max(
|
|
742
|
+
0,
|
|
743
|
+
((usage.tokens ?? contextPercent * usage.contextWindow) -
|
|
744
|
+
emergencyPruneResult.estimatedTokensRecovered) /
|
|
745
|
+
usage.contextWindow,
|
|
746
|
+
),
|
|
747
|
+
targetMet: emergencyPruneResult.estimatedTokensRecovered >= targetRecoveryTokens,
|
|
748
|
+
eligibleExhausted:
|
|
749
|
+
emergencyPruneResult.prunedToolCallIds.length >= selectionStatsBeforePrune.eligiblePairs,
|
|
750
|
+
...selectionStatsBeforePrune,
|
|
751
|
+
state: summarizeDcpState(state),
|
|
752
|
+
}, ctx)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (nudgeType && !manualEmergencyOnly && (hasCompressionSuggestion || emergencyPressureReached)) {
|
|
639
757
|
const nudgeText = appendConcreteNudgeGuidance(
|
|
640
758
|
baseNudgeText(nudgeType),
|
|
641
759
|
candidate,
|
|
@@ -685,6 +803,10 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
685
803
|
} else {
|
|
686
804
|
state.nudgeCounter++
|
|
687
805
|
}
|
|
806
|
+
|
|
807
|
+
// Persist patience/window changes even when an existing anchor was only
|
|
808
|
+
// re-applied (that path intentionally emits telemetry without updating it).
|
|
809
|
+
await saveDcpState(ctx, state)
|
|
688
810
|
}
|
|
689
811
|
|
|
690
812
|
if (state.manualMode) {
|
|
@@ -699,25 +821,65 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
699
821
|
return finishContext("complete", prunedMessages, {
|
|
700
822
|
candidate,
|
|
701
823
|
messageCandidates,
|
|
824
|
+
emergencyCurrentTurn: emergencySelection?.stats,
|
|
825
|
+
emergencyPrune: emergencyPruneResult,
|
|
702
826
|
})
|
|
703
827
|
})
|
|
704
828
|
|
|
705
829
|
// ── 10b. before_provider_request: inject DCP IDs outside transcript ────────
|
|
706
830
|
pi.on("before_provider_request", async (event, ctx) => {
|
|
707
831
|
const effectiveConfig = configForContext(ctx)
|
|
832
|
+
pendingProviderToolIds.clear()
|
|
708
833
|
if (!effectiveConfig.enabled) return undefined
|
|
709
834
|
|
|
835
|
+
const providerEvidence = collectProviderToolResultEvidence(event.payload)
|
|
836
|
+
for (const meta of state.messageMetaSnapshot.values()) {
|
|
837
|
+
if (meta.role !== "toolResult") continue
|
|
838
|
+
if (!meta.toolCallId || state.prunedToolIds.has(meta.toolCallId)) continue
|
|
839
|
+
if (state.providerSeenToolIds.has(meta.toolCallId)) continue
|
|
840
|
+
const record = state.toolCalls.get(meta.toolCallId)
|
|
841
|
+
if (record && providerPayloadIncludesToolResult(providerEvidence, record)) {
|
|
842
|
+
pendingProviderToolIds.add(meta.toolCallId)
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
710
846
|
const controlText = buildMessageIdControlText(state)
|
|
711
847
|
if (!controlText) return undefined
|
|
712
848
|
|
|
713
849
|
const payload = appendDcpControlToProviderPayload(event.payload, controlText)
|
|
714
850
|
writeDcpDebugLog(effectiveConfig, "provider_payload.message_ids", {
|
|
715
851
|
injected: payload !== event.payload,
|
|
852
|
+
pendingToolResults: pendingProviderToolIds.size,
|
|
716
853
|
state: summarizeDcpState(state),
|
|
717
854
|
}, ctx)
|
|
718
855
|
return payload === event.payload ? undefined : payload
|
|
719
856
|
})
|
|
720
857
|
|
|
858
|
+
// Promote pending IDs only after the provider accepted the exact payload.
|
|
859
|
+
// Failed/aborted requests leave results fresh and therefore ineligible.
|
|
860
|
+
pi.on("after_provider_response", async (event, ctx) => {
|
|
861
|
+
const effectiveConfig = configForContext(ctx)
|
|
862
|
+
const accepted = event.status >= 200 && event.status < 300
|
|
863
|
+
let newlySeenToolResults = 0
|
|
864
|
+
if (effectiveConfig.enabled && accepted) {
|
|
865
|
+
for (const toolCallId of pendingProviderToolIds) {
|
|
866
|
+
if (!state.providerSeenToolIds.has(toolCallId)) {
|
|
867
|
+
state.providerSeenToolIds.add(toolCallId)
|
|
868
|
+
newlySeenToolResults++
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (newlySeenToolResults > 0) await saveDcpState(ctx, state)
|
|
872
|
+
}
|
|
873
|
+
writeDcpDebugLog(effectiveConfig, "provider_payload.tool_results_seen", {
|
|
874
|
+
status: event.status,
|
|
875
|
+
accepted,
|
|
876
|
+
pendingToolResults: pendingProviderToolIds.size,
|
|
877
|
+
newlySeenToolResults,
|
|
878
|
+
state: summarizeDcpState(state),
|
|
879
|
+
}, ctx)
|
|
880
|
+
pendingProviderToolIds.clear()
|
|
881
|
+
})
|
|
882
|
+
|
|
721
883
|
// ── 11. agent_end: persist state after each agent run ────────────────────
|
|
722
884
|
pi.on("agent_end", async (_event, ctx) => {
|
|
723
885
|
await saveDcpState(ctx, state)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { ToolRecord } from "./state.js";
|
|
3
|
+
|
|
4
|
+
export interface ProviderToolResultEvidence {
|
|
5
|
+
ids: Set<string>;
|
|
6
|
+
anonymousSignatures: Set<string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function responseText(response: unknown): string | undefined {
|
|
10
|
+
if (typeof response === "string") return response;
|
|
11
|
+
if (!response || typeof response !== "object") return undefined;
|
|
12
|
+
const record = response as Record<string, unknown>;
|
|
13
|
+
if (typeof record.output === "string") return record.output;
|
|
14
|
+
if (typeof record.error === "string") return record.error;
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function signature(toolName: string, outputText: string): string {
|
|
19
|
+
return createHash("sha256")
|
|
20
|
+
.update(toolName)
|
|
21
|
+
.update("\u0000")
|
|
22
|
+
.update(outputText)
|
|
23
|
+
.digest("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Collect only tool-result evidence, never assistant tool-call IDs. */
|
|
27
|
+
export function collectProviderToolResultEvidence(payload: unknown): ProviderToolResultEvidence {
|
|
28
|
+
const evidence: ProviderToolResultEvidence = {
|
|
29
|
+
ids: new Set<string>(),
|
|
30
|
+
anonymousSignatures: new Set<string>(),
|
|
31
|
+
};
|
|
32
|
+
const visited = new Set<object>();
|
|
33
|
+
|
|
34
|
+
function visit(value: unknown): void {
|
|
35
|
+
if (!value || typeof value !== "object") return;
|
|
36
|
+
if (visited.has(value)) return;
|
|
37
|
+
visited.add(value);
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
for (const item of value) visit(item);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const record = value as Record<string, unknown>;
|
|
45
|
+
if (record.role === "toolResult" && typeof record.toolCallId === "string") {
|
|
46
|
+
evidence.ids.add(record.toolCallId);
|
|
47
|
+
}
|
|
48
|
+
if (record.role === "tool" && typeof record.tool_call_id === "string") {
|
|
49
|
+
evidence.ids.add(record.tool_call_id);
|
|
50
|
+
}
|
|
51
|
+
if (record.type === "function_call_output" && typeof record.call_id === "string") {
|
|
52
|
+
evidence.ids.add(record.call_id);
|
|
53
|
+
}
|
|
54
|
+
if (record.type === "tool_result" && typeof record.tool_use_id === "string") {
|
|
55
|
+
evidence.ids.add(record.tool_use_id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const functionResponse = record.functionResponse;
|
|
59
|
+
if (functionResponse && typeof functionResponse === "object") {
|
|
60
|
+
const responseRecord = functionResponse as Record<string, unknown>;
|
|
61
|
+
if (typeof responseRecord.id === "string") evidence.ids.add(responseRecord.id);
|
|
62
|
+
const outputText = responseText(responseRecord.response);
|
|
63
|
+
if (typeof responseRecord.name === "string" && outputText !== undefined) {
|
|
64
|
+
evidence.anonymousSignatures.add(signature(responseRecord.name, outputText));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const nested of Object.values(record)) visit(nested);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
visit(payload);
|
|
72
|
+
return evidence;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function providerPayloadIncludesToolResult(
|
|
76
|
+
evidence: ProviderToolResultEvidence,
|
|
77
|
+
record: ToolRecord,
|
|
78
|
+
): boolean {
|
|
79
|
+
if (evidence.ids.has(record.toolCallId)) return true;
|
|
80
|
+
for (const id of evidence.ids) {
|
|
81
|
+
if (record.toolCallId.startsWith(`${id}|`)) return true;
|
|
82
|
+
}
|
|
83
|
+
return typeof record.outputText === "string" &&
|
|
84
|
+
evidence.anonymousSignatures.has(signature(record.toolName, record.outputText));
|
|
85
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { DcpConfig } from "./config.js";
|
|
2
|
+
import { estimateMessageTokens, extractBlockId, messageText } from "./pruner-metadata.js";
|
|
3
|
+
import type {
|
|
4
|
+
EmergencyCurrentTurnOutput,
|
|
5
|
+
EmergencyCurrentTurnSelection,
|
|
6
|
+
MessageCompressionCandidate,
|
|
7
|
+
} from "./pruner-types.js";
|
|
8
|
+
import {
|
|
9
|
+
EMERGENCY_CURRENT_TURN_PLACEHOLDER,
|
|
10
|
+
isToolRecordProtected,
|
|
11
|
+
markToolPruned,
|
|
12
|
+
} from "./pruner-tools.js";
|
|
13
|
+
import type { DcpState } from "./state.js";
|
|
14
|
+
|
|
15
|
+
function emptySelection(): EmergencyCurrentTurnSelection {
|
|
16
|
+
return {
|
|
17
|
+
eligible: [],
|
|
18
|
+
stats: {
|
|
19
|
+
totalPairs: 0,
|
|
20
|
+
totalPairTokens: 0,
|
|
21
|
+
eligiblePairs: 0,
|
|
22
|
+
eligibleTokens: 0,
|
|
23
|
+
eligibleRecoverableTokens: 0,
|
|
24
|
+
preservedPairs: 0,
|
|
25
|
+
preservedTokens: 0,
|
|
26
|
+
preservedRecentPairs: 0,
|
|
27
|
+
preservedRecentTokens: 0,
|
|
28
|
+
preservedUnseenPairs: 0,
|
|
29
|
+
preservedUnseenTokens: 0,
|
|
30
|
+
preservedProtectedPairs: 0,
|
|
31
|
+
preservedProtectedTokens: 0,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function emergencyPressureState(
|
|
37
|
+
contextPercent: number,
|
|
38
|
+
maxContextPercent: number,
|
|
39
|
+
hardContextPercent: number,
|
|
40
|
+
): { hardEmergencyReached: boolean; contextLimitReached: boolean; emergencyPressureReached: boolean } {
|
|
41
|
+
const hardEmergencyReached = contextPercent >= Math.max(0, Math.min(1, hardContextPercent));
|
|
42
|
+
const contextLimitReached = contextPercent > maxContextPercent;
|
|
43
|
+
return {
|
|
44
|
+
hardEmergencyReached,
|
|
45
|
+
contextLimitReached,
|
|
46
|
+
emergencyPressureReached: hardEmergencyReached || contextLimitReached,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isRealUserMessage(message: any): boolean {
|
|
51
|
+
if (message?.role !== "user") return false;
|
|
52
|
+
const text = messageText(message);
|
|
53
|
+
return !text.includes("<dcp-system-reminder>") && extractBlockId(text) === undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assistantToolCallIds(message: any): string[] {
|
|
57
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) return [];
|
|
58
|
+
return message.content
|
|
59
|
+
.filter((part: any) => part?.type === "toolCall" && typeof part.id === "string")
|
|
60
|
+
.map((part: any) => part.id as string);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function messageIdForResult(message: any, state: DcpState): string | undefined {
|
|
64
|
+
for (const [id, meta] of state.messageMetaSnapshot) {
|
|
65
|
+
if (
|
|
66
|
+
meta.blockId === undefined &&
|
|
67
|
+
meta.toolCallId === message.toolCallId &&
|
|
68
|
+
meta.role === message.role &&
|
|
69
|
+
meta.timestamp === message.timestamp
|
|
70
|
+
) return id;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasProtectedTag(message: any, config: DcpConfig): boolean {
|
|
76
|
+
return config.compress.protectTags && /<protect\b[^>]*>[\s\S]*?<\/protect>/i.test(messageText(message));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function analyzeEmergencyCurrentTurn(
|
|
80
|
+
messages: any[],
|
|
81
|
+
state: DcpState,
|
|
82
|
+
config: DcpConfig,
|
|
83
|
+
): EmergencyCurrentTurnSelection {
|
|
84
|
+
const settings = config.strategies.emergencyCurrentTurnPruning;
|
|
85
|
+
if (!settings.enabled) return emptySelection();
|
|
86
|
+
let latestUserIndex = -1;
|
|
87
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
88
|
+
if (isRealUserMessage(messages[index])) {
|
|
89
|
+
latestUserIndex = index;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (latestUserIndex < 0) {
|
|
95
|
+
return emptySelection();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const pairedCallIds = new Set<string>();
|
|
99
|
+
for (let index = latestUserIndex + 1; index < messages.length; index++) {
|
|
100
|
+
for (const toolCallId of assistantToolCallIds(messages[index])) pairedCallIds.add(toolCallId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pairs: Array<EmergencyCurrentTurnOutput & { message: any }> = [];
|
|
104
|
+
for (let index = latestUserIndex + 1; index < messages.length; index++) {
|
|
105
|
+
const message = messages[index];
|
|
106
|
+
if (message?.role !== "toolResult") continue;
|
|
107
|
+
if (typeof message.toolCallId !== "string" || !pairedCallIds.has(message.toolCallId)) continue;
|
|
108
|
+
const record = state.toolCalls.get(message.toolCallId);
|
|
109
|
+
const tokenEstimate = Math.max(record?.tokenEstimate ?? 0, estimateMessageTokens(message));
|
|
110
|
+
const placeholderEstimate = estimateMessageTokens({
|
|
111
|
+
...message,
|
|
112
|
+
content: [{ type: "text", text: EMERGENCY_CURRENT_TURN_PLACEHOLDER }],
|
|
113
|
+
});
|
|
114
|
+
pairs.push({
|
|
115
|
+
toolCallId: message.toolCallId,
|
|
116
|
+
messageId: messageIdForResult(message, state),
|
|
117
|
+
toolName: record?.toolName ?? message.toolName ?? "",
|
|
118
|
+
tokenEstimate,
|
|
119
|
+
recoverableTokens: Math.max(0, tokenEstimate - placeholderEstimate),
|
|
120
|
+
resultIndex: index,
|
|
121
|
+
message,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const keepRecent = Math.max(0, Math.floor(settings.keepRecentToolPairs));
|
|
126
|
+
const recentIds = new Set(pairs.slice(Math.max(0, pairs.length - keepRecent)).map((pair) => pair.toolCallId));
|
|
127
|
+
const eligible: EmergencyCurrentTurnOutput[] = [];
|
|
128
|
+
const preservedIds = new Set<string>();
|
|
129
|
+
let preservedTokens = 0;
|
|
130
|
+
let preservedRecentPairs = 0;
|
|
131
|
+
let preservedRecentTokens = 0;
|
|
132
|
+
let preservedUnseenPairs = 0;
|
|
133
|
+
let preservedUnseenTokens = 0;
|
|
134
|
+
let preservedProtectedPairs = 0;
|
|
135
|
+
let preservedProtectedTokens = 0;
|
|
136
|
+
|
|
137
|
+
const preserve = (
|
|
138
|
+
pair: EmergencyCurrentTurnOutput,
|
|
139
|
+
reason: "recent" | "unseen" | "protected" | "other",
|
|
140
|
+
): void => {
|
|
141
|
+
if (!preservedIds.has(pair.toolCallId)) {
|
|
142
|
+
preservedIds.add(pair.toolCallId);
|
|
143
|
+
preservedTokens += pair.tokenEstimate;
|
|
144
|
+
}
|
|
145
|
+
if (reason === "recent") {
|
|
146
|
+
preservedRecentPairs++;
|
|
147
|
+
preservedRecentTokens += pair.tokenEstimate;
|
|
148
|
+
} else if (reason === "unseen") {
|
|
149
|
+
preservedUnseenPairs++;
|
|
150
|
+
preservedUnseenTokens += pair.tokenEstimate;
|
|
151
|
+
} else if (reason === "protected") {
|
|
152
|
+
preservedProtectedPairs++;
|
|
153
|
+
preservedProtectedTokens += pair.tokenEstimate;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
for (const pair of pairs) {
|
|
158
|
+
if (recentIds.has(pair.toolCallId)) {
|
|
159
|
+
preserve(pair, "recent");
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!state.providerSeenToolIds.has(pair.toolCallId)) {
|
|
163
|
+
preserve(pair, "unseen");
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const record = state.toolCalls.get(pair.toolCallId);
|
|
167
|
+
if (
|
|
168
|
+
!record ||
|
|
169
|
+
isToolRecordProtected(record, config, settings.protectedTools) ||
|
|
170
|
+
hasProtectedTag(pair.message, config)
|
|
171
|
+
) {
|
|
172
|
+
preserve(pair, "protected");
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
state.prunedToolIds.has(pair.toolCallId) ||
|
|
177
|
+
pair.tokenEstimate < Math.max(1, settings.minOutputTokens) ||
|
|
178
|
+
pair.recoverableTokens <= 0
|
|
179
|
+
) {
|
|
180
|
+
preserve(pair, "other");
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
eligible.push({
|
|
184
|
+
toolCallId: pair.toolCallId,
|
|
185
|
+
messageId: pair.messageId,
|
|
186
|
+
toolName: pair.toolName,
|
|
187
|
+
tokenEstimate: pair.tokenEstimate,
|
|
188
|
+
recoverableTokens: pair.recoverableTokens,
|
|
189
|
+
resultIndex: pair.resultIndex,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
eligible.sort((a, b) =>
|
|
194
|
+
a.resultIndex - b.resultIndex ||
|
|
195
|
+
b.tokenEstimate - a.tokenEstimate ||
|
|
196
|
+
a.toolCallId.localeCompare(b.toolCallId),
|
|
197
|
+
);
|
|
198
|
+
return {
|
|
199
|
+
eligible,
|
|
200
|
+
stats: {
|
|
201
|
+
totalPairs: pairs.length,
|
|
202
|
+
totalPairTokens: pairs.reduce((sum, pair) => sum + pair.tokenEstimate, 0),
|
|
203
|
+
eligiblePairs: eligible.length,
|
|
204
|
+
eligibleTokens: eligible.reduce((sum, pair) => sum + pair.tokenEstimate, 0),
|
|
205
|
+
eligibleRecoverableTokens: eligible.reduce((sum, pair) => sum + pair.recoverableTokens, 0),
|
|
206
|
+
preservedPairs: preservedIds.size,
|
|
207
|
+
preservedTokens,
|
|
208
|
+
preservedRecentPairs,
|
|
209
|
+
preservedRecentTokens,
|
|
210
|
+
preservedUnseenPairs,
|
|
211
|
+
preservedUnseenTokens,
|
|
212
|
+
preservedProtectedPairs,
|
|
213
|
+
preservedProtectedTokens,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function emergencyCurrentTurnMessageCandidates(
|
|
219
|
+
selection: EmergencyCurrentTurnSelection,
|
|
220
|
+
config: DcpConfig,
|
|
221
|
+
): MessageCompressionCandidate[] {
|
|
222
|
+
const settings = config.strategies.emergencyCurrentTurnPruning;
|
|
223
|
+
if (!settings.enabled) return [];
|
|
224
|
+
const maxSuggestions = Math.max(1, Math.floor(settings.maxSuggestions));
|
|
225
|
+
const highTokens = Math.max(1, config.compress.messageMode.highTokens);
|
|
226
|
+
return selection.eligible
|
|
227
|
+
.filter((output) => output.messageId !== undefined)
|
|
228
|
+
.slice(0, maxSuggestions)
|
|
229
|
+
.map((output) => ({
|
|
230
|
+
messageId: output.messageId!,
|
|
231
|
+
role: "toolResult",
|
|
232
|
+
estimatedTokens: output.tokenEstimate,
|
|
233
|
+
priority: output.tokenEstimate >= highTokens ? "high" : "medium",
|
|
234
|
+
reason: `old same-turn tool output; newest ${settings.keepRecentToolPairs} complete pair(s) preserved`,
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function pruneEmergencyCurrentTurn(
|
|
239
|
+
selection: EmergencyCurrentTurnSelection,
|
|
240
|
+
state: DcpState,
|
|
241
|
+
targetRecoveryTokens: number,
|
|
242
|
+
): { prunedToolCallIds: string[]; estimatedTokensRecovered: number } {
|
|
243
|
+
const prunedToolCallIds: string[] = [];
|
|
244
|
+
let estimatedTokensRecovered = 0;
|
|
245
|
+
const target = Math.max(1, Math.ceil(targetRecoveryTokens));
|
|
246
|
+
for (const output of selection.eligible) {
|
|
247
|
+
if (estimatedTokensRecovered >= target) break;
|
|
248
|
+
if (markToolPruned(state, output.toolCallId, "emergency-current-turn", output.recoverableTokens)) {
|
|
249
|
+
prunedToolCallIds.push(output.toolCallId);
|
|
250
|
+
estimatedTokensRecovered += output.recoverableTokens;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { prunedToolCallIds, estimatedTokensRecovered };
|
|
254
|
+
}
|
|
@@ -2,6 +2,9 @@ import type { DcpConfig } from "./config.js";
|
|
|
2
2
|
import type { DcpState, ToolRecord } from "./state.js";
|
|
3
3
|
import { estimateMessageTokens } from "./pruner-metadata.js";
|
|
4
4
|
|
|
5
|
+
export const EMERGENCY_CURRENT_TURN_PLACEHOLDER =
|
|
6
|
+
"[Older tool output removed during current-turn context emergency; re-run the tool if exact content is needed]";
|
|
7
|
+
|
|
5
8
|
// Tool outputs that must never be auto-pruned unless a future explicit user
|
|
6
9
|
// action intentionally changes that policy. They mutate state or control DCP.
|
|
7
10
|
const ALWAYS_PROTECTED_TOOLS = new Set(["compress", "write", "edit"]);
|
|
@@ -238,6 +241,9 @@ function placeholderForPrunedTool(msg: any, state: DcpState): string {
|
|
|
238
241
|
if (reason === "manual-sweep") {
|
|
239
242
|
return "[Output removed by /dcp sweep to save context]";
|
|
240
243
|
}
|
|
244
|
+
if (reason === "emergency-current-turn") {
|
|
245
|
+
return EMERGENCY_CURRENT_TURN_PLACEHOLDER;
|
|
246
|
+
}
|
|
241
247
|
if (reason === "old-error" || msg.isError) {
|
|
242
248
|
return "[Error output removed - tool failed more than the configured number of turns ago]";
|
|
243
249
|
}
|