pi-ui-extend 1.0.39 → 1.0.41
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/README.md +1 -1
- package/dist/app/commands/command-registry.js +2 -2
- package/dist/app/commands/command-session-actions.d.ts +0 -1
- package/dist/app/commands/command-session-actions.js +22 -13
- package/dist/app/icons.d.ts +14 -0
- package/dist/app/icons.js +33 -0
- package/dist/app/rendering/conversation-tool-renderer.js +2 -2
- package/dist/app/rendering/dcp-stats.d.ts +6 -1
- package/dist/app/rendering/dcp-stats.js +214 -46
- package/dist/app/rendering/editor-panels.js +8 -5
- package/dist/app/session/lazy-session-manager.js +12 -1
- package/dist/app/session/tabs-controller.d.ts +2 -5
- package/dist/app/session/tabs-controller.js +12 -21
- package/dist/app/subagents/subagents-model.d.ts +14 -1
- package/dist/app/subagents/subagents-model.js +34 -15
- package/dist/app/types.d.ts +2 -0
- package/dist/bundled-extensions/session-title/config.js +1 -1
- package/dist/markdown-format.js +27 -9
- package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
- package/dist/schemas/pi-tools-suite-schema.js +46 -31
- package/external/pi-tools-suite/README.md +392 -52
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
- package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
- package/external/pi-tools-suite/docs/evals.md +684 -0
- package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
- package/external/pi-tools-suite/package.json +10 -3
- package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
- package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
- package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
- package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
- package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
- package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
- package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
- package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
- package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
- package/external/pi-tools-suite/src/config.ts +1 -22
- package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
- package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
- package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
- package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
- package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
- package/external/pi-tools-suite/src/dcp/config.ts +36 -61
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
- package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
- package/external/pi-tools-suite/src/dcp/index.ts +617 -203
- package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
- package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
- package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
- package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
- package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
- package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
- package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
- package/external/pi-tools-suite/src/dcp/state.ts +158 -580
- package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
- package/external/pi-tools-suite/src/index.ts +9 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
- package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
- package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
- package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
- package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
- package/package.json +6 -6
- package/schemas/pi-tools-suite.json +159 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
|
@@ -3,13 +3,13 @@ import type { DcpState } from "./state.js";
|
|
|
3
3
|
import type { CompressionCandidate, MessageCompressionCandidate } from "./pruner-types.js";
|
|
4
4
|
import {
|
|
5
5
|
estimateMessageTokens,
|
|
6
|
-
extractBlockId,
|
|
7
|
-
messageText,
|
|
8
6
|
} from "./pruner-metadata.js";
|
|
9
7
|
import { stableMessageKeys } from "./pruner-message-ids.js";
|
|
8
|
+
import { detectToolGroupSpans, findConversationIndexEntry } from "./conversation-index.js";
|
|
10
9
|
|
|
11
10
|
interface CandidateBoundary {
|
|
12
11
|
id: string;
|
|
12
|
+
messageIndex: number;
|
|
13
13
|
role: string;
|
|
14
14
|
timestamp: number;
|
|
15
15
|
tokenEstimate: number;
|
|
@@ -17,41 +17,62 @@ interface CandidateBoundary {
|
|
|
17
17
|
isSystemReminder: boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function
|
|
21
|
-
|
|
20
|
+
function assistantToolCallIds(message: any): string[] {
|
|
21
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) return [];
|
|
22
|
+
return message.content
|
|
23
|
+
.filter((part: any) => part?.type === "toolCall" && typeof part.id === "string")
|
|
24
|
+
.map((part: any) => part.id as string);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isRealUserBoundary(boundary: CandidateBoundary): boolean {
|
|
28
|
+
return boundary.role === "user" && boundary.blockId === undefined && !boundary.isSystemReminder;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildCandidateBoundaries(
|
|
32
|
+
messages: any[],
|
|
33
|
+
state: DcpState,
|
|
34
|
+
options: { allowBlocks: boolean },
|
|
35
|
+
): CandidateBoundary[] {
|
|
36
|
+
const boundaries: CandidateBoundary[] = [];
|
|
37
|
+
const stableKeys = stableMessageKeys(messages);
|
|
38
|
+
for (let index = 0; index < messages.length; index++) {
|
|
39
|
+
const msg = messages[index];
|
|
40
|
+
const boundary = resolveAddressableBoundaryId(msg, stableKeys[index]!, state, options);
|
|
41
|
+
if (!boundary) continue;
|
|
42
|
+
if (!Number.isFinite(msg.timestamp)) continue;
|
|
43
|
+
boundaries.push({
|
|
44
|
+
id: boundary.id,
|
|
45
|
+
messageIndex: index,
|
|
46
|
+
role: msg.role ?? "",
|
|
47
|
+
timestamp: msg.timestamp,
|
|
48
|
+
tokenEstimate: state.messageMetaSnapshot.get(boundary.id)?.tokenEstimate ?? estimateMessageTokens(msg),
|
|
49
|
+
blockId: boundary.blockId,
|
|
50
|
+
isSystemReminder: msg?._dcpOrigin === "dcp-control",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return boundaries;
|
|
22
54
|
}
|
|
23
55
|
|
|
24
56
|
function isActiveBlockId(blockId: number, state: DcpState): boolean {
|
|
25
57
|
return state.compressionBlocks.some((block) => block.id === blockId && block.active);
|
|
26
58
|
}
|
|
27
59
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Compression-block identity is internal projection provenance, never a
|
|
62
|
+
* provider-visible text convention. A raw message may quote a DCP block tag
|
|
63
|
+
* verbatim (including in a fenced example), so parsing its content would let
|
|
64
|
+
* untrusted text impersonate a generated summary.
|
|
65
|
+
*/
|
|
66
|
+
function projectedBlockId(msg: any): number | undefined {
|
|
67
|
+
if (msg?._dcpOrigin !== "block") return undefined;
|
|
68
|
+
const blockId = msg?._dcpBlockId;
|
|
69
|
+
return Number.isInteger(blockId) && blockId > 0 ? blockId : undefined;
|
|
70
|
+
}
|
|
32
71
|
|
|
72
|
+
function findCurrentMessageId(stableKey: string, state: DcpState): string | undefined {
|
|
33
73
|
for (const [id, meta] of state.messageMetaSnapshot) {
|
|
34
74
|
if (meta.stableId === stableKey) return id;
|
|
35
75
|
}
|
|
36
|
-
|
|
37
|
-
// Backward compatibility for snapshots created before stable IDs were
|
|
38
|
-
// recorded. Never mix timestamp fallback with a modern stable-ID snapshot.
|
|
39
|
-
const isLegacySnapshot = [...state.messageMetaSnapshot.values()]
|
|
40
|
-
.every((meta) => meta.stableId === undefined);
|
|
41
|
-
if (!isLegacySnapshot) return undefined;
|
|
42
|
-
|
|
43
|
-
for (const [id, meta] of state.messageMetaSnapshot) {
|
|
44
|
-
if (
|
|
45
|
-
meta.timestamp === timestamp &&
|
|
46
|
-
meta.role === role &&
|
|
47
|
-
meta.blockId === undefined
|
|
48
|
-
) return id;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
for (const [id, ts] of state.messageIdSnapshot) {
|
|
52
|
-
if (ts === timestamp) return id;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
76
|
return undefined;
|
|
56
77
|
}
|
|
57
78
|
|
|
@@ -60,51 +81,102 @@ function resolveAddressableBoundaryId(
|
|
|
60
81
|
stableKey: string,
|
|
61
82
|
state: DcpState,
|
|
62
83
|
options: { allowBlocks: boolean },
|
|
63
|
-
): { id: string; blockId?: number
|
|
64
|
-
const
|
|
65
|
-
const blockId = extractBlockId(text);
|
|
84
|
+
): { id: string; blockId?: number } | null {
|
|
85
|
+
const blockId = projectedBlockId(msg);
|
|
66
86
|
if (blockId !== undefined) {
|
|
67
|
-
if (options.allowBlocks && isActiveBlockId(blockId, state)) return { id: `b${blockId}`, blockId
|
|
68
|
-
if (!hasAddressableSnapshot(state) && options.allowBlocks) return { id: `b${blockId}`, blockId, text };
|
|
87
|
+
if (options.allowBlocks && isActiveBlockId(blockId, state)) return { id: `b${blockId}`, blockId };
|
|
69
88
|
return null;
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
// Inline [dcp-id] markers are no longer injected into message content; the
|
|
73
92
|
// snapshot rebuilt by injectMessageIds() is the sole addressability source.
|
|
74
|
-
// Resolve the message ID by its persistent stable identity.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (currentId) return { id: currentId, text };
|
|
93
|
+
// Resolve the message ID only by its persistent stable identity.
|
|
94
|
+
const currentId = findCurrentMessageId(stableKey, state);
|
|
95
|
+
if (currentId) return { id: currentId };
|
|
78
96
|
|
|
79
97
|
return null;
|
|
80
98
|
}
|
|
81
99
|
|
|
100
|
+
export interface CompressionCandidateSelectionOptions {
|
|
101
|
+
/** Net projected savings required to restore the current budget. */
|
|
102
|
+
requiredSavingsTokens?: number;
|
|
103
|
+
/** Conservative estimator margin added to the required source size. */
|
|
104
|
+
estimatorMarginTokens?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function selectOldestSafePrefix(
|
|
108
|
+
boundaries: CandidateBoundary[],
|
|
109
|
+
state: DcpState,
|
|
110
|
+
settings: { minMessages: number; minTokens: number },
|
|
111
|
+
maxSafeMessageIndex: number,
|
|
112
|
+
options?: CompressionCandidateSelectionOptions,
|
|
113
|
+
): CandidateBoundary[] | null {
|
|
114
|
+
if (boundaries.length === 0) return null;
|
|
115
|
+
if (!options || options.requiredSavingsTokens === undefined) {
|
|
116
|
+
let full = boundaries.slice();
|
|
117
|
+
while (full.length > 0 && full[0]!.isSystemReminder) full = full.slice(1);
|
|
118
|
+
while (full.length > 0 && full[full.length - 1]!.isSystemReminder) full = full.slice(0, -1);
|
|
119
|
+
if (full.length < Math.max(1, settings.minMessages)) return null;
|
|
120
|
+
if (full.reduce((sum, boundary) => sum + boundary.tokenEstimate, 0) < settings.minTokens) return null;
|
|
121
|
+
return full;
|
|
122
|
+
}
|
|
123
|
+
const requiredSavings = Math.max(0, Math.floor(options.requiredSavingsTokens));
|
|
124
|
+
const estimatorMargin = Math.max(0, Math.floor(options?.estimatorMarginTokens ?? 0));
|
|
125
|
+
const targetSourceTokens = Math.max(settings.minTokens, requiredSavings + estimatorMargin);
|
|
126
|
+
const minMessages = Math.max(1, settings.minMessages);
|
|
127
|
+
|
|
128
|
+
let selectedEnd = -1;
|
|
129
|
+
let selectedTokens = 0;
|
|
130
|
+
for (let index = 0; index < boundaries.length; index++) {
|
|
131
|
+
selectedTokens += boundaries[index]!.tokenEstimate;
|
|
132
|
+
if (index + 1 >= minMessages && selectedTokens >= targetSourceTokens) {
|
|
133
|
+
selectedEnd = index;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (selectedEnd < 0) selectedEnd = boundaries.length - 1;
|
|
138
|
+
|
|
139
|
+
let selected = boundaries.slice(0, selectedEnd + 1);
|
|
140
|
+
if (state.conversationIndexSnapshot.length > 0) {
|
|
141
|
+
const groups = detectToolGroupSpans(state.conversationIndexSnapshot);
|
|
142
|
+
const startMessageIndex = selected[0]!.messageIndex;
|
|
143
|
+
let endMessageIndex = selected[selected.length - 1]!.messageIndex;
|
|
144
|
+
let changed = true;
|
|
145
|
+
while (changed) {
|
|
146
|
+
changed = false;
|
|
147
|
+
for (const group of groups) {
|
|
148
|
+
const overlaps = group.startIndex <= endMessageIndex && group.endIndex >= startMessageIndex;
|
|
149
|
+
if (!overlaps) continue;
|
|
150
|
+
if (!group.complete) return null;
|
|
151
|
+
if (group.endIndex > endMessageIndex) {
|
|
152
|
+
if (group.endIndex > maxSafeMessageIndex) return null;
|
|
153
|
+
endMessageIndex = group.endIndex;
|
|
154
|
+
changed = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
selected = boundaries.filter((boundary) => boundary.messageIndex <= endMessageIndex);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
while (selected.length > 0 && selected[0]!.isSystemReminder) selected = selected.slice(1);
|
|
162
|
+
while (selected.length > 0 && selected[selected.length - 1]!.isSystemReminder) selected = selected.slice(0, -1);
|
|
163
|
+
if (selected.length < minMessages) return null;
|
|
164
|
+
if (selected.reduce((sum, boundary) => sum + boundary.tokenEstimate, 0) < settings.minTokens) return null;
|
|
165
|
+
return selected;
|
|
166
|
+
}
|
|
167
|
+
|
|
82
168
|
export function detectCompressionCandidate(
|
|
83
169
|
messages: any[],
|
|
84
170
|
_state: DcpState,
|
|
85
171
|
config: DcpConfig,
|
|
86
172
|
contextPercent: number,
|
|
173
|
+
options?: CompressionCandidateSelectionOptions,
|
|
87
174
|
): CompressionCandidate | null {
|
|
88
175
|
const settings = config.compress.autoCandidates;
|
|
89
176
|
if (!settings.enabled) return null;
|
|
90
177
|
if (contextPercent < settings.minContextPercent) return null;
|
|
91
178
|
|
|
92
|
-
const boundaries:
|
|
93
|
-
const stableKeys = stableMessageKeys(messages);
|
|
94
|
-
for (let index = 0; index < messages.length; index++) {
|
|
95
|
-
const msg = messages[index];
|
|
96
|
-
const boundary = resolveAddressableBoundaryId(msg, stableKeys[index]!, _state, { allowBlocks: true });
|
|
97
|
-
if (!boundary) continue;
|
|
98
|
-
if (!Number.isFinite(msg.timestamp)) continue;
|
|
99
|
-
boundaries.push({
|
|
100
|
-
id: boundary.id,
|
|
101
|
-
role: msg.role ?? "",
|
|
102
|
-
timestamp: msg.timestamp,
|
|
103
|
-
tokenEstimate: _state.messageMetaSnapshot.get(boundary.id)?.tokenEstimate ?? estimateMessageTokens(msg),
|
|
104
|
-
blockId: boundary.blockId,
|
|
105
|
-
isSystemReminder: boundary.text.includes("<dcp-system-reminder>"),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
179
|
+
const boundaries = buildCandidateBoundaries(messages, _state, { allowBlocks: true });
|
|
108
180
|
|
|
109
181
|
if (boundaries.length < settings.minMessages) return null;
|
|
110
182
|
|
|
@@ -114,9 +186,7 @@ export function detectCompressionCandidate(
|
|
|
114
186
|
|
|
115
187
|
for (let i = boundaries.length - 1; i >= 0; i--) {
|
|
116
188
|
const boundary = boundaries[i]!;
|
|
117
|
-
|
|
118
|
-
boundary.role === "user" && boundary.blockId === undefined && !boundary.isSystemReminder;
|
|
119
|
-
if (!isRealUserMessage) continue;
|
|
189
|
+
if (!isRealUserBoundary(boundary)) continue;
|
|
120
190
|
recentUserTurns++;
|
|
121
191
|
if (recentUserTurns >= keepRecentTurns) {
|
|
122
192
|
cutoffIndex = i - 1;
|
|
@@ -126,16 +196,157 @@ export function detectCompressionCandidate(
|
|
|
126
196
|
|
|
127
197
|
if (cutoffIndex < 0) return null;
|
|
128
198
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
199
|
+
const eligible = boundaries.slice(0, cutoffIndex + 1);
|
|
200
|
+
const candidate = selectOldestSafePrefix(
|
|
201
|
+
eligible,
|
|
202
|
+
_state,
|
|
203
|
+
settings,
|
|
204
|
+
eligible[eligible.length - 1]?.messageIndex ?? -1,
|
|
205
|
+
options,
|
|
206
|
+
);
|
|
207
|
+
if (!candidate) return null;
|
|
208
|
+
|
|
209
|
+
const estimatedTokens = candidate.reduce((sum, item) => sum + item.tokenEstimate, 0);
|
|
210
|
+
|
|
211
|
+
const includedBlockIds = Array.from(
|
|
212
|
+
new Set(candidate.map((item) => item.blockId).filter((id): id is number => id !== undefined)),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
startId: candidate[0]!.id,
|
|
217
|
+
endId: candidate[candidate.length - 1]!.id,
|
|
218
|
+
messageCount: candidate.length,
|
|
219
|
+
estimatedTokens,
|
|
220
|
+
includedBlockIds,
|
|
221
|
+
reason: options?.requiredSavingsTokens
|
|
222
|
+
? `minimal oldest protocol-safe prefix restoring ~${Math.max(0, Math.floor(options.requiredSavingsTokens))} budget tokens; older than the most recent ${keepRecentTurns} user turn(s)`
|
|
223
|
+
: `older than the most recent ${keepRecentTurns} user turn(s)`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Emergency-only range candidate for marathon turns.
|
|
229
|
+
*
|
|
230
|
+
* Normal candidates deliberately protect the newest N user turns. That means a
|
|
231
|
+
* single user task can never produce a range candidate no matter how many
|
|
232
|
+
* provider transactions follow it. Above the configured max-context threshold,
|
|
233
|
+
* this detector instead keeps the user request plus a live tail of recent tool
|
|
234
|
+
* transactions and exposes only the older, provider-committed prefix.
|
|
235
|
+
*
|
|
236
|
+
* The newest assistant group is always retained, even when
|
|
237
|
+
* keepRecentToolPairs=0, so the in-flight request/result head is never selected.
|
|
238
|
+
* For older tool groups, a later assistant is only structural ordering evidence:
|
|
239
|
+
* every tool result selected for compression must also have completed provider
|
|
240
|
+
* evidence in `providerSeenToolIds`. Unknown evidence truncates the safe prefix.
|
|
241
|
+
*/
|
|
242
|
+
export function detectEmergencyCompressionCandidate(
|
|
243
|
+
messages: any[],
|
|
244
|
+
state: DcpState,
|
|
245
|
+
config: DcpConfig,
|
|
246
|
+
contextPercent: number,
|
|
247
|
+
maxContextPercent: number,
|
|
248
|
+
options?: CompressionCandidateSelectionOptions,
|
|
249
|
+
): CompressionCandidate | null {
|
|
250
|
+
const settings = config.compress.autoCandidates;
|
|
251
|
+
const emergencySettings = config.strategies.emergencyCurrentTurnPruning;
|
|
252
|
+
// `autoCandidates.enabled` controls routine advisory suggestions only. The
|
|
253
|
+
// emergency planner is a safety path and is gated by its own destructive
|
|
254
|
+
// policy (`emergencyCurrentTurnPruning.enabled` / autoCompress at commit).
|
|
255
|
+
// Reuse autoCandidates sizing knobs without letting its advisory toggle
|
|
256
|
+
// silently disable emergency range planning.
|
|
257
|
+
if (!emergencySettings.enabled) return null;
|
|
258
|
+
if (contextPercent <= maxContextPercent) return null;
|
|
259
|
+
|
|
260
|
+
const boundaries = buildCandidateBoundaries(messages, state, { allowBlocks: true });
|
|
261
|
+
if (boundaries.length < settings.minMessages) return null;
|
|
262
|
+
|
|
263
|
+
let latestUserIndex = -1;
|
|
264
|
+
for (let index = boundaries.length - 1; index >= 0; index--) {
|
|
265
|
+
if (!isRealUserBoundary(boundaries[index]!)) continue;
|
|
266
|
+
latestUserIndex = boundaries[index]!.messageIndex;
|
|
267
|
+
break;
|
|
133
268
|
}
|
|
269
|
+
if (latestUserIndex < 0) return null;
|
|
134
270
|
|
|
135
|
-
|
|
271
|
+
const assistantIndexes: number[] = [];
|
|
272
|
+
for (let index = latestUserIndex + 1; index < messages.length; index++) {
|
|
273
|
+
if (messages[index]?.role === "assistant") assistantIndexes.push(index);
|
|
274
|
+
}
|
|
275
|
+
if (assistantIndexes.length < 2) return null;
|
|
276
|
+
|
|
277
|
+
// Always keep the newest assistant group as the live head. Then extend the
|
|
278
|
+
// preserved head backwards until it contains the requested number of newest
|
|
279
|
+
// complete tool-call/result pairs. Grouping by assistant avoids splitting a
|
|
280
|
+
// parallel tool-call transaction.
|
|
281
|
+
const keepRecentPairs = Math.max(0, Math.floor(emergencySettings.keepRecentToolPairs));
|
|
282
|
+
let preservedPairs = 0;
|
|
283
|
+
let preservedHeadStart = assistantIndexes[assistantIndexes.length - 1]!;
|
|
284
|
+
for (let assistantPos = assistantIndexes.length - 1; assistantPos >= 0; assistantPos--) {
|
|
285
|
+
const assistantIndex = assistantIndexes[assistantPos]!;
|
|
286
|
+
preservedHeadStart = assistantIndex;
|
|
287
|
+
const nextAssistantIndex = assistantIndexes[assistantPos + 1] ?? messages.length;
|
|
288
|
+
const callIds = new Set(assistantToolCallIds(messages[assistantIndex]));
|
|
289
|
+
if (callIds.size > 0) {
|
|
290
|
+
const completedIds = new Set<string>();
|
|
291
|
+
for (let index = assistantIndex + 1; index < nextAssistantIndex; index++) {
|
|
292
|
+
const message = messages[index];
|
|
293
|
+
if (
|
|
294
|
+
message?.role === "toolResult" &&
|
|
295
|
+
typeof message.toolCallId === "string" &&
|
|
296
|
+
callIds.has(message.toolCallId)
|
|
297
|
+
) {
|
|
298
|
+
completedIds.add(message.toolCallId);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
preservedPairs += completedIds.size;
|
|
302
|
+
}
|
|
303
|
+
if (assistantPos === assistantIndexes.length - 1 && keepRecentPairs === 0) break;
|
|
304
|
+
if (preservedPairs >= keepRecentPairs) break;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// F09: provider completion evidence is authoritative for tool-result
|
|
308
|
+
// eligibility in both emergency range and output-pruning paths. A later
|
|
309
|
+
// assistant establishes ordering only; it cannot prove that an earlier tool
|
|
310
|
+
// result survived serialization + stream completion. Stop before the first
|
|
311
|
+
// group whose evidence is incomplete/unknown rather than silently spanning it.
|
|
312
|
+
let evidenceSafeEnd = preservedHeadStart - 1;
|
|
313
|
+
const groups = detectToolGroupSpans(state.conversationIndexSnapshot);
|
|
314
|
+
for (const group of groups) {
|
|
315
|
+
if (group.endIndex <= latestUserIndex) continue;
|
|
316
|
+
if (group.startIndex >= preservedHeadStart) break;
|
|
317
|
+
if (!group.complete || group.toolCallIds.some((id) => !state.providerSeenToolIds.has(id))) {
|
|
318
|
+
evidenceSafeEnd = Math.min(evidenceSafeEnd, group.startIndex - 1);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const eligible = boundaries.filter((boundary) =>
|
|
324
|
+
boundary.messageIndex > latestUserIndex &&
|
|
325
|
+
boundary.messageIndex <= evidenceSafeEnd,
|
|
326
|
+
);
|
|
327
|
+
const candidate = selectOldestSafePrefix(
|
|
328
|
+
eligible,
|
|
329
|
+
state,
|
|
330
|
+
settings,
|
|
331
|
+
preservedHeadStart - 1,
|
|
332
|
+
options,
|
|
333
|
+
);
|
|
334
|
+
if (!candidate) return null;
|
|
335
|
+
|
|
336
|
+
// Independent fail-closed guard: structural ordering is never enough to
|
|
337
|
+
// authorize deletion of an unseen tool result.
|
|
338
|
+
const selectedStart = candidate[0]!.messageIndex;
|
|
339
|
+
const selectedEnd = candidate[candidate.length - 1]!.messageIndex;
|
|
340
|
+
for (let index = selectedStart; index <= selectedEnd; index++) {
|
|
341
|
+
const message = messages[index];
|
|
342
|
+
if (
|
|
343
|
+
(message?.role === "toolResult" || message?.role === "bashExecution") &&
|
|
344
|
+
typeof message.toolCallId === "string" &&
|
|
345
|
+
!state.providerSeenToolIds.has(message.toolCallId)
|
|
346
|
+
) return null;
|
|
347
|
+
}
|
|
136
348
|
|
|
137
349
|
const estimatedTokens = candidate.reduce((sum, item) => sum + item.tokenEstimate, 0);
|
|
138
|
-
if (estimatedTokens < settings.minTokens) return null;
|
|
139
350
|
|
|
140
351
|
const includedBlockIds = Array.from(
|
|
141
352
|
new Set(candidate.map((item) => item.blockId).filter((id): id is number => id !== undefined)),
|
|
@@ -147,7 +358,9 @@ export function detectCompressionCandidate(
|
|
|
147
358
|
messageCount: candidate.length,
|
|
148
359
|
estimatedTokens,
|
|
149
360
|
includedBlockIds,
|
|
150
|
-
reason:
|
|
361
|
+
reason: options?.requiredSavingsTokens
|
|
362
|
+
? `minimal emergency same-turn provider-evidenced prefix restoring ~${Math.max(0, Math.floor(options.requiredSavingsTokens))} budget tokens; preserves newest ${keepRecentPairs} tool pair(s) plus the live assistant head`
|
|
363
|
+
: `emergency same-turn provider-evidenced prefix; preserves newest ${keepRecentPairs} tool pair(s) plus the live assistant head`,
|
|
151
364
|
};
|
|
152
365
|
}
|
|
153
366
|
|
|
@@ -171,21 +384,8 @@ export function detectMessageCompressionCandidates(
|
|
|
171
384
|
if (!settings?.enabled) return [];
|
|
172
385
|
if (contextPercent < settings.minContextPercent) return [];
|
|
173
386
|
|
|
174
|
-
const boundaries:
|
|
175
|
-
|
|
176
|
-
for (let index = 0; index < messages.length; index++) {
|
|
177
|
-
const msg = messages[index];
|
|
178
|
-
const boundary = resolveAddressableBoundaryId(msg, stableKeys[index]!, state, { allowBlocks: false });
|
|
179
|
-
if (!boundary || boundary.blockId !== undefined) continue;
|
|
180
|
-
if (!Number.isFinite(msg.timestamp)) continue;
|
|
181
|
-
boundaries.push({
|
|
182
|
-
id: boundary.id,
|
|
183
|
-
role: msg.role ?? "",
|
|
184
|
-
timestamp: msg.timestamp,
|
|
185
|
-
tokenEstimate: state.messageMetaSnapshot.get(boundary.id)?.tokenEstimate ?? estimateMessageTokens(msg),
|
|
186
|
-
isSystemReminder: boundary.text.includes("<dcp-system-reminder>"),
|
|
187
|
-
});
|
|
188
|
-
}
|
|
387
|
+
const boundaries = buildCandidateBoundaries(messages, state, { allowBlocks: false })
|
|
388
|
+
.filter((boundary) => boundary.blockId === undefined);
|
|
189
389
|
|
|
190
390
|
const keepRecentTurns = Math.max(1, settings.keepRecentTurns ?? 2);
|
|
191
391
|
let recentUserTurns = 0;
|
|
@@ -202,7 +402,10 @@ export function detectMessageCompressionCandidates(
|
|
|
202
402
|
}
|
|
203
403
|
}
|
|
204
404
|
|
|
205
|
-
|
|
405
|
+
// If the transcript does not yet contain enough complete user turns to
|
|
406
|
+
// satisfy the retention policy, every message belongs to the protected
|
|
407
|
+
// recent window. Do not reinterpret the live head as stale history.
|
|
408
|
+
if (recentUserTurns < keepRecentTurns || cutoffIndex < 0) return [];
|
|
206
409
|
|
|
207
410
|
const mediumTokens = Math.max(1, settings.mediumTokens ?? 500);
|
|
208
411
|
const highTokens = Math.max(mediumTokens, settings.highTokens ?? 5000);
|
|
@@ -212,6 +415,23 @@ export function detectMessageCompressionCandidates(
|
|
|
212
415
|
.slice(0, cutoffIndex + 1)
|
|
213
416
|
.filter((candidate) => !candidate.isSystemReminder)
|
|
214
417
|
.filter((candidate) => candidate.role !== "user" || !config.compress.protectUserMessages)
|
|
418
|
+
// V2 message mode replaces only the selected body, so completed tool
|
|
419
|
+
// results are safe surgical candidates. Signed assistants and assistants
|
|
420
|
+
// carrying tool calls remain structurally immutable, and an incomplete
|
|
421
|
+
// tool group is still in-flight even if it appears in an unusual snapshot.
|
|
422
|
+
.filter((candidate) => {
|
|
423
|
+
const message = messages[candidate.messageIndex];
|
|
424
|
+
if (candidate.role === "assistant" && assistantToolCallIds(message).length > 0) return false;
|
|
425
|
+
const indexEntry = findConversationIndexEntry(state.conversationIndexSnapshot, candidate.id);
|
|
426
|
+
if (candidate.role === "assistant" && indexEntry?.signedAssistant) return false;
|
|
427
|
+
if (indexEntry) {
|
|
428
|
+
const incompleteGroup = detectToolGroupSpans(state.conversationIndexSnapshot).some((group) =>
|
|
429
|
+
!group.complete && indexEntry.index >= group.startIndex && indexEntry.index <= group.endIndex,
|
|
430
|
+
);
|
|
431
|
+
if (incompleteGroup) return false;
|
|
432
|
+
}
|
|
433
|
+
return true;
|
|
434
|
+
})
|
|
215
435
|
.filter((candidate) => candidate.tokenEstimate >= mediumTokens)
|
|
216
436
|
.map((candidate): MessageCompressionCandidate => ({
|
|
217
437
|
messageId: candidate.id,
|