pi-ui-extend 0.1.56 → 0.1.59
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/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/src/antigravity-auth/payload.ts +1 -1
- 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 +13 -121
- 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 +193 -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 +14 -0
- package/external/pi-tools-suite/src/index.ts +4 -2
- 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/external/pi-tools-suite/src/todo/index.ts +23 -27
- package/package.json +1 -1
|
@@ -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
|
|
@@ -132,6 +140,17 @@ function appendDcpControlToMessages(messages: unknown, text: string): unknown {
|
|
|
132
140
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
133
141
|
const message = messages[index] as any
|
|
134
142
|
if (!message || typeof message !== "object") continue
|
|
143
|
+
// Responses items such as reasoning/function_call_output do not accept a
|
|
144
|
+
// `content` field. Keep a function output at the tail by appending to its
|
|
145
|
+
// valid `output` string; otherwise scan back to an actual message item.
|
|
146
|
+
if (typeof message.type === "string" && message.type !== "message" && message.role === undefined) {
|
|
147
|
+
if (message.type === "function_call_output" && typeof message.output === "string") {
|
|
148
|
+
return messages.map((candidate: any, candidateIndex) => candidateIndex === index
|
|
149
|
+
? { ...candidate, output: `${candidate.output}\n\n${block}` }
|
|
150
|
+
: candidate)
|
|
151
|
+
}
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
135
154
|
if (message.role === "system" || message.role === "developer") continue
|
|
136
155
|
targetIndex = index
|
|
137
156
|
break
|
|
@@ -213,6 +232,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
213
232
|
|
|
214
233
|
// ── 2. Create state ───────────────────────────────────────────────────────
|
|
215
234
|
const state = createState()
|
|
235
|
+
const pendingProviderToolIds = new Set<string>()
|
|
216
236
|
const appendNudgeTelemetry = (
|
|
217
237
|
event: "emitted" | "upgraded" | "reapplied",
|
|
218
238
|
type: DcpNudgeType,
|
|
@@ -416,6 +436,8 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
416
436
|
let prunedMessages = applyPruning(contextMessages, state, effectiveConfig)
|
|
417
437
|
let candidate = null as ReturnType<typeof detectCompressionCandidate>
|
|
418
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
|
|
419
441
|
|
|
420
442
|
// In manual mode we still apply pruning strategies (if
|
|
421
443
|
// automaticStrategies is on) but skip routine autonomous nudges. Emergency
|
|
@@ -461,15 +483,27 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
461
483
|
thresholds.maxContextPercent += Math.min(summaryBonus, SUMMARY_BUFFER_MAX_CONTEXT_BONUS)
|
|
462
484
|
}
|
|
463
485
|
|
|
464
|
-
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
|
+
)
|
|
465
496
|
const routineNudgesAllowed = contextPercent > thresholds.minContextPercent
|
|
466
|
-
if (!
|
|
497
|
+
if (!emergencyPressureReached && !routineNudgesAllowed) {
|
|
467
498
|
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
468
|
-
|
|
499
|
+
const resetEmergencyPasses = state.consecutiveIgnoredStrongNudges > 0
|
|
500
|
+
state.consecutiveIgnoredStrongNudges = 0
|
|
501
|
+
if (clearedAnchors > 0 || resetEmergencyPasses) await saveDcpState(ctx, state)
|
|
469
502
|
return finishContext("below-threshold", prunedMessages, {
|
|
470
503
|
contextPercent,
|
|
471
504
|
thresholds,
|
|
472
505
|
clearedAnchors,
|
|
506
|
+
resetEmergencyPasses,
|
|
473
507
|
})
|
|
474
508
|
}
|
|
475
509
|
|
|
@@ -494,8 +528,17 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
494
528
|
Number.isFinite(currentContextWindow) &&
|
|
495
529
|
currentContextWindow < previousContextWindow * 0.9 &&
|
|
496
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
|
|
497
538
|
|
|
498
|
-
const nudgeType =
|
|
539
|
+
const nudgeType = hardEmergencyReached && !contextLimitReached
|
|
540
|
+
? "context-strong"
|
|
541
|
+
: windowDowngraded
|
|
499
542
|
? "context-strong"
|
|
500
543
|
: getNudgeType(
|
|
501
544
|
contextPercent,
|
|
@@ -532,11 +575,37 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
532
575
|
}, ctx)
|
|
533
576
|
}
|
|
534
577
|
|
|
535
|
-
const
|
|
578
|
+
const hasNormalCompressionSuggestion =
|
|
536
579
|
candidate !== null || messageCandidates.length > 0
|
|
537
|
-
if (
|
|
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 =
|
|
606
|
+
candidate !== null || messageCandidates.length > 0
|
|
607
|
+
if (!manualEmergencyOnly && !emergencyPressureReached && !hasCompressionSuggestion) {
|
|
538
608
|
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
539
|
-
state.consecutiveIgnoredStrongNudges = 0
|
|
540
609
|
if (clearedAnchors > 0) await saveDcpState(ctx, state)
|
|
541
610
|
if (nudgeType || clearedAnchors > 0) {
|
|
542
611
|
writeDcpDebugLog(effectiveConfig, "context.no_compression_candidate", {
|
|
@@ -549,19 +618,11 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
549
618
|
}
|
|
550
619
|
}
|
|
551
620
|
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
// between. Reset on non-strong nudges and when pressure drops below
|
|
556
|
-
// the emergency threshold (handled by the below-threshold early
|
|
557
|
-
// return above, which clears anchors; counter is also reset on any
|
|
558
|
-
// successful compress in the compress tool).
|
|
559
|
-
if (
|
|
560
|
-
hasCompressionSuggestion &&
|
|
561
|
-
(nudgeType === "context-strong" || nudgeType === "context-soft")
|
|
562
|
-
) {
|
|
621
|
+
// Emergency pressure must advance even when normal candidates are
|
|
622
|
+
// absent; otherwise the current-turn fallback can never reach patience.
|
|
623
|
+
if (emergencyPressureReached) {
|
|
563
624
|
state.consecutiveIgnoredStrongNudges += 1
|
|
564
|
-
} else
|
|
625
|
+
} else {
|
|
565
626
|
state.consecutiveIgnoredStrongNudges = 0
|
|
566
627
|
}
|
|
567
628
|
|
|
@@ -576,6 +637,17 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
576
637
|
thresholds.maxContextPercent,
|
|
577
638
|
candidate,
|
|
578
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
|
+
}
|
|
579
651
|
if (autoDecision.shouldFire && candidate) {
|
|
580
652
|
try {
|
|
581
653
|
const autoResult = await createAutoCompressionBlock({
|
|
@@ -624,7 +696,64 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
624
696
|
}
|
|
625
697
|
}
|
|
626
698
|
|
|
627
|
-
|
|
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)) {
|
|
628
757
|
const nudgeText = appendConcreteNudgeGuidance(
|
|
629
758
|
baseNudgeText(nudgeType),
|
|
630
759
|
candidate,
|
|
@@ -674,6 +803,10 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
674
803
|
} else {
|
|
675
804
|
state.nudgeCounter++
|
|
676
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)
|
|
677
810
|
}
|
|
678
811
|
|
|
679
812
|
if (state.manualMode) {
|
|
@@ -688,25 +821,65 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
688
821
|
return finishContext("complete", prunedMessages, {
|
|
689
822
|
candidate,
|
|
690
823
|
messageCandidates,
|
|
824
|
+
emergencyCurrentTurn: emergencySelection?.stats,
|
|
825
|
+
emergencyPrune: emergencyPruneResult,
|
|
691
826
|
})
|
|
692
827
|
})
|
|
693
828
|
|
|
694
829
|
// ── 10b. before_provider_request: inject DCP IDs outside transcript ────────
|
|
695
830
|
pi.on("before_provider_request", async (event, ctx) => {
|
|
696
831
|
const effectiveConfig = configForContext(ctx)
|
|
832
|
+
pendingProviderToolIds.clear()
|
|
697
833
|
if (!effectiveConfig.enabled) return undefined
|
|
698
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
|
+
|
|
699
846
|
const controlText = buildMessageIdControlText(state)
|
|
700
847
|
if (!controlText) return undefined
|
|
701
848
|
|
|
702
849
|
const payload = appendDcpControlToProviderPayload(event.payload, controlText)
|
|
703
850
|
writeDcpDebugLog(effectiveConfig, "provider_payload.message_ids", {
|
|
704
851
|
injected: payload !== event.payload,
|
|
852
|
+
pendingToolResults: pendingProviderToolIds.size,
|
|
705
853
|
state: summarizeDcpState(state),
|
|
706
854
|
}, ctx)
|
|
707
855
|
return payload === event.payload ? undefined : payload
|
|
708
856
|
})
|
|
709
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
|
+
|
|
710
883
|
// ── 11. agent_end: persist state after each agent run ────────────────────
|
|
711
884
|
pi.on("agent_end", async (_event, ctx) => {
|
|
712
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
|
}
|