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
|
@@ -1,107 +1,55 @@
|
|
|
1
1
|
import type { DcpConfig } from "./config.js";
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
2
|
+
import { hasExactCompressionMembership, type CompressionBlock, type CompressionMember, type DcpState } from "./state.js";
|
|
3
|
+
import { estimateMessageTokens } from "./pruner-metadata.js";
|
|
4
4
|
import { stableMessageKeys } from "./pruner-message-ids.js";
|
|
5
5
|
import { writeDcpDebugLog } from "./debug-log.js";
|
|
6
|
+
import { canonicalMessageHash, rawMutationHashOf } from "./conversation-index.js";
|
|
6
7
|
|
|
7
8
|
function findBoundaryIndex(messages: any[], stableId: string | undefined, timestamp: number): number {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export interface ReconcileInheritedBlocksResult {
|
|
16
|
-
fittingBlockIds: number[];
|
|
17
|
-
activatedBlockIds: number[];
|
|
18
|
-
deactivatedBlockIds: number[];
|
|
9
|
+
if (stableId) {
|
|
10
|
+
const stableKeys = stableMessageKeys(messages);
|
|
11
|
+
return stableKeys.indexOf(stableId);
|
|
12
|
+
}
|
|
13
|
+
const matches = messages.flatMap((message, index) => message.timestamp === timestamp ? [index] : []);
|
|
14
|
+
return matches.length === 1 ? matches[0]! : -1;
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* fork, neither block would otherwise be applied and the whole history would
|
|
29
|
-
* be sent raw. Select the widest fitting block(s) instead, while preserving an
|
|
30
|
-
* explicit user decompression for the block and its covered descendants.
|
|
18
|
+
* Mutation identity of one message occurrence. A projection entry whose body
|
|
19
|
+
* DCP replaced (tool-output placeholder) legitimately stands in for its
|
|
20
|
+
* preserved raw content, so its identity is that recorded raw hash; every
|
|
21
|
+
* other message is identified by its own canonical hash. Exactness is not
|
|
22
|
+
* relaxed: the member hash must still match this identity, in order,
|
|
23
|
+
* contiguously, and any changed raw content still fails closed.
|
|
31
24
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
): ReconcileInheritedBlocksResult {
|
|
36
|
-
const byId = new Map(state.compressionBlocks.map((block) => [block.id, block]));
|
|
37
|
-
const fittingBlockIds = new Set<number>();
|
|
38
|
-
const descendantCache = new Map<number, Set<number>>();
|
|
39
|
-
|
|
40
|
-
for (const block of state.compressionBlocks) {
|
|
41
|
-
if (!Number.isFinite(block.startTimestamp) || !Number.isFinite(block.endTimestamp)) continue;
|
|
42
|
-
const startFound = findBoundaryIndex(messages, block.startMessageId, block.startTimestamp) !== -1;
|
|
43
|
-
const endFound = findBoundaryIndex(messages, block.endMessageId, block.endTimestamp) !== -1;
|
|
44
|
-
if (startFound && endFound) fittingBlockIds.add(block.id);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function descendantsOf(rootId: number): Set<number> {
|
|
48
|
-
const cached = descendantCache.get(rootId);
|
|
49
|
-
if (cached) return cached;
|
|
50
|
-
|
|
51
|
-
const descendants = new Set<number>();
|
|
52
|
-
const pending = [...(byId.get(rootId)?.coveredBlockIds ?? [])];
|
|
53
|
-
while (pending.length > 0) {
|
|
54
|
-
const id = pending.pop()!;
|
|
55
|
-
if (id === rootId || descendants.has(id)) continue;
|
|
56
|
-
descendants.add(id);
|
|
57
|
-
pending.push(...(byId.get(id)?.coveredBlockIds ?? []));
|
|
58
|
-
}
|
|
59
|
-
descendantCache.set(rootId, descendants);
|
|
60
|
-
return descendants;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Decompressing a roll-up intentionally exposes its whole range. Do not
|
|
64
|
-
// unexpectedly reactivate one of its older nested summaries after a fork.
|
|
65
|
-
const suppressedByUser = new Set<number>();
|
|
66
|
-
for (const block of state.compressionBlocks) {
|
|
67
|
-
if (!block.deactivatedByUser) continue;
|
|
68
|
-
suppressedByUser.add(block.id);
|
|
69
|
-
for (const id of descendantsOf(block.id)) suppressedByUser.add(id);
|
|
70
|
-
}
|
|
25
|
+
function mutationIdentity(message: any): string {
|
|
26
|
+
return rawMutationHashOf(message) ?? canonicalMessageHash(message);
|
|
27
|
+
}
|
|
71
28
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
for (
|
|
77
|
-
|
|
29
|
+
function exactMemberSpan(messages: any[], members: CompressionMember[]): { lo: number; hi: number } | undefined {
|
|
30
|
+
const keys = stableMessageKeys(messages);
|
|
31
|
+
const lo = keys.indexOf(members[0]!.stableId);
|
|
32
|
+
if (lo < 0 || lo + members.length > messages.length) return undefined;
|
|
33
|
+
for (let offset = 0; offset < members.length; offset++) {
|
|
34
|
+
const member = members[offset]!;
|
|
35
|
+
if (keys[lo + offset] !== member.stableId || mutationIdentity(messages[lo + offset]) !== member.hash) return undefined;
|
|
78
36
|
}
|
|
37
|
+
return { lo, hi: lo + members.length - 1 };
|
|
38
|
+
}
|
|
79
39
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
for (const block of state.compressionBlocks) {
|
|
87
|
-
const shouldBeActive = activeBlockIds.has(block.id);
|
|
88
|
-
if (block.active !== shouldBeActive) {
|
|
89
|
-
if (shouldBeActive) activatedBlockIds.push(block.id);
|
|
90
|
-
else deactivatedBlockIds.push(block.id);
|
|
91
|
-
}
|
|
92
|
-
block.active = shouldBeActive;
|
|
93
|
-
|
|
94
|
-
if (block.deactivatedByUser) continue;
|
|
95
|
-
block.deactivatedReason = fittingBlockIds.has(block.id)
|
|
96
|
-
? undefined
|
|
97
|
-
: "outside-inherited-branch";
|
|
40
|
+
function exactStableIdSpan(messages: any[], members: CompressionMember[]): { lo: number; hi: number } | undefined {
|
|
41
|
+
const keys = stableMessageKeys(messages);
|
|
42
|
+
const lo = keys.indexOf(members[0]!.stableId);
|
|
43
|
+
if (lo < 0 || lo + members.length > messages.length) return undefined;
|
|
44
|
+
for (let offset = 0; offset < members.length; offset++) {
|
|
45
|
+
if (keys[lo + offset] !== members[offset]!.stableId) return undefined;
|
|
98
46
|
}
|
|
47
|
+
return { lo, hi: lo + members.length - 1 };
|
|
48
|
+
}
|
|
99
49
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
deactivatedBlockIds: deactivatedBlockIds.sort((a, b) => a - b),
|
|
104
|
-
};
|
|
50
|
+
function exactBlockSpan(messages: any[], block: CompressionBlock): { lo: number; hi: number } | undefined {
|
|
51
|
+
if (!hasExactCompressionMembership(block)) return undefined;
|
|
52
|
+
return exactMemberSpan(messages, block.mutationMembers!) ?? exactMemberSpan(messages, block.sourceMembers!);
|
|
105
53
|
}
|
|
106
54
|
|
|
107
55
|
|
|
@@ -123,7 +71,35 @@ export function syncCompressionBlocks(messages: any[], state: DcpState, config:
|
|
|
123
71
|
}
|
|
124
72
|
|
|
125
73
|
for (const block of state.compressionBlocks) {
|
|
126
|
-
if (!block.active
|
|
74
|
+
if (!block.active) continue;
|
|
75
|
+
|
|
76
|
+
if (!hasExactCompressionMembership(block)) {
|
|
77
|
+
block.active = false;
|
|
78
|
+
block.deactivatedReason = "invalid-exact-membership";
|
|
79
|
+
writeDcpDebugLog(config, "block.auto_deactivated", {
|
|
80
|
+
blockId: `b${block.id}`,
|
|
81
|
+
reason: block.deactivatedReason,
|
|
82
|
+
topic: block.topic,
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If the same exact stable sequence still exists but the recorded source
|
|
88
|
+
// and raw hashes no longer match, never widen or guess a replacement.
|
|
89
|
+
if (
|
|
90
|
+
exactStableIdSpan(messages, block.mutationMembers!) &&
|
|
91
|
+
!exactMemberSpan(messages, block.mutationMembers!) &&
|
|
92
|
+
!exactMemberSpan(messages, block.sourceMembers!)
|
|
93
|
+
) {
|
|
94
|
+
block.active = false;
|
|
95
|
+
block.deactivatedReason = "exact-membership-mismatch";
|
|
96
|
+
writeDcpDebugLog(config, "block.auto_deactivated", {
|
|
97
|
+
blockId: `b${block.id}`,
|
|
98
|
+
reason: block.deactivatedReason,
|
|
99
|
+
topic: block.topic,
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
127
103
|
|
|
128
104
|
// ── Skip the missing-origin-compress-call check ────────────────────
|
|
129
105
|
// The compress tool-call that *created* this block is not the block's
|
|
@@ -178,208 +154,124 @@ export function syncCompressionBlocks(messages: any[], state: DcpState, config:
|
|
|
178
154
|
}
|
|
179
155
|
}
|
|
180
156
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Find start and end indices by timestamp
|
|
192
|
-
const startIdx = findBoundaryIndex(messages, block.startMessageId, block.startTimestamp);
|
|
193
|
-
const endIdx = findBoundaryIndex(messages, block.endMessageId, block.endTimestamp);
|
|
194
|
-
|
|
195
|
-
if (startIdx === -1 || endIdx === -1) continue;
|
|
196
|
-
|
|
197
|
-
let lo = Math.min(startIdx, endIdx);
|
|
198
|
-
let hi = Math.max(startIdx, endIdx);
|
|
199
|
-
|
|
200
|
-
// Expand lo backward: if there is an assistant before lo whose tool_use
|
|
201
|
-
// blocks have matching tool_results inside [lo..hi], pull the entire
|
|
202
|
-
// assistant + any intermediate result messages into the range so the
|
|
203
|
-
// group is always removed atomically.
|
|
204
|
-
//
|
|
205
|
-
// Critically we must skip backward past any toolResult / bashExecution
|
|
206
|
-
// messages before lo, because an assistant with multiple tool_calls emits
|
|
207
|
-
// N consecutive result messages — the assistant itself sits further back.
|
|
208
|
-
while (lo > 0) {
|
|
209
|
-
// Walk backward past tool-result messages to find the preceding assistant
|
|
210
|
-
let scanIdx = lo - 1;
|
|
211
|
-
while (scanIdx >= 0) {
|
|
212
|
-
const r = (messages[scanIdx] as any).role as string;
|
|
213
|
-
if (r !== "toolResult" && r !== "bashExecution" && !PASSTHROUGH_ROLES.has(r)) break;
|
|
214
|
-
scanIdx--;
|
|
215
|
-
}
|
|
216
|
-
if (scanIdx < 0 || (messages[scanIdx] as any).role !== "assistant") break;
|
|
217
|
-
|
|
218
|
-
const prev = messages[scanIdx] as any;
|
|
219
|
-
const toolCallIdsInRange = new Set<string>();
|
|
220
|
-
for (let i = lo; i <= hi; i++) {
|
|
221
|
-
const m = messages[i] as any;
|
|
222
|
-
if (
|
|
223
|
-
(m.role === "toolResult" || m.role === "bashExecution") &&
|
|
224
|
-
typeof m.toolCallId === "string"
|
|
225
|
-
) {
|
|
226
|
-
toolCallIdsInRange.add(m.toolCallId);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
const prevContent: any[] = Array.isArray(prev.content) ? prev.content : [];
|
|
230
|
-
const hasMatchingToolCalls = prevContent.some(
|
|
231
|
-
(contentBlock: any) => contentBlock.type === "toolCall" && toolCallIdsInRange.has(contentBlock.id),
|
|
232
|
-
);
|
|
233
|
-
if (!hasMatchingToolCalls) break;
|
|
234
|
-
// Pull assistant + all intermediate result messages into the range
|
|
235
|
-
lo = scanIdx;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Expand hi forward: for every assistant message in [lo..hi] that has
|
|
239
|
-
// tool_use blocks, include any immediately-following tool_result messages
|
|
240
|
-
// that correspond to those blocks. Loop to fixed point because expanding
|
|
241
|
-
// hi could expose more assistants in theory.
|
|
242
|
-
let prevHi: number;
|
|
243
|
-
do {
|
|
244
|
-
prevHi = hi;
|
|
245
|
-
const assistantToolCallIds = new Set<string>();
|
|
246
|
-
for (let i = lo; i <= hi; i++) {
|
|
247
|
-
const m = messages[i] as any;
|
|
248
|
-
if (m.role !== "assistant") continue;
|
|
249
|
-
const content: any[] = Array.isArray(m.content) ? m.content : [];
|
|
250
|
-
for (const contentBlock of content) {
|
|
251
|
-
if (contentBlock.type === "toolCall" && typeof contentBlock.id === "string") {
|
|
252
|
-
assistantToolCallIds.add(contentBlock.id);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
while (hi + 1 < messages.length) {
|
|
257
|
-
const next = messages[hi + 1] as any;
|
|
258
|
-
if (
|
|
259
|
-
(next.role === "toolResult" || next.role === "bashExecution") &&
|
|
260
|
-
assistantToolCallIds.has(next.toolCallId)
|
|
261
|
-
) {
|
|
262
|
-
hi++;
|
|
263
|
-
} else if (PASSTHROUGH_ROLES.has(next.role)) {
|
|
264
|
-
hi++;
|
|
265
|
-
} else {
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
} while (hi !== prevHi);
|
|
270
|
-
|
|
271
|
-
// Estimate tokens removed
|
|
272
|
-
let removedTokens = 0;
|
|
273
|
-
for (let i = lo; i <= hi; i++) {
|
|
274
|
-
removedTokens += estimateMessageTokens(messages[i]);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Remove the range (inclusive)
|
|
278
|
-
messages.splice(lo, hi - lo + 1);
|
|
279
|
-
|
|
280
|
-
// Build synthetic user message for the compressed block
|
|
281
|
-
const syntheticMsg = {
|
|
282
|
-
role: "user",
|
|
283
|
-
content: [
|
|
284
|
-
{
|
|
285
|
-
type: "text",
|
|
286
|
-
text:
|
|
287
|
-
"[Compressed section: " +
|
|
288
|
-
block.topic +
|
|
289
|
-
"]\n\n" +
|
|
290
|
-
block.summary +
|
|
291
|
-
"\n\n[dcp-block-id]: # (b" +
|
|
292
|
-
block.id +
|
|
293
|
-
")",
|
|
294
|
-
},
|
|
295
|
-
],
|
|
296
|
-
// anchorTimestamp is always finite (resolveAnchorTimestamp returns
|
|
297
|
-
// endTimestamp + 1 instead of Infinity), but guard against corrupted
|
|
298
|
-
// state from older sessions where Infinity/null could leak in.
|
|
299
|
-
timestamp: Number.isFinite(block.anchorTimestamp) ? block.anchorTimestamp - 0.5 : block.endTimestamp + 0.5,
|
|
300
|
-
};
|
|
157
|
+
function compressedBlockText(block: CompressionBlock, label: "section" | "message"): string {
|
|
158
|
+
return [
|
|
159
|
+
`[Compressed ${label}: ${block.topic}]`,
|
|
160
|
+
"",
|
|
161
|
+
block.summary,
|
|
162
|
+
"",
|
|
163
|
+
`[dcp-block-id]: # (b${block.id})`,
|
|
164
|
+
].join("\n");
|
|
165
|
+
}
|
|
301
166
|
|
|
302
|
-
|
|
303
|
-
|
|
167
|
+
/** Exact estimator cost of the synthetic v2 range replacement, including the
|
|
168
|
+
* DCP wrapper/carrier text that is not part of block.summaryTokenEstimate. */
|
|
169
|
+
export function estimateCompressionBlockReplacementTokens(block: CompressionBlock): number {
|
|
170
|
+
return estimateMessageTokens({
|
|
171
|
+
role: "user",
|
|
172
|
+
content: [{ type: "text", text: compressedBlockText(block, "section") }],
|
|
173
|
+
timestamp: block.startTimestamp,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
304
176
|
|
|
305
|
-
|
|
306
|
-
|
|
177
|
+
function accountCompressionBlock(
|
|
178
|
+
block: CompressionBlock,
|
|
179
|
+
removedTokens: number,
|
|
180
|
+
addedTokens: number,
|
|
181
|
+
state: DcpState,
|
|
182
|
+
): void {
|
|
183
|
+
if (state.accountedCompressionBlockIds.has(block.id)) return;
|
|
184
|
+
state.accountedCompressionBlockIds.add(block.id);
|
|
185
|
+
state.totalPruneCount++;
|
|
186
|
+
const rawSaved = Math.max(0, removedTokens - addedTokens);
|
|
187
|
+
const coveredSavings = (block.coveredBlockIds ?? []).reduce(
|
|
188
|
+
(sum, id) => sum + (state.compressionTokenSavings.get(id) ?? 0),
|
|
189
|
+
0,
|
|
190
|
+
);
|
|
191
|
+
state.tokensSaved = Math.max(0, state.tokensSaved + rawSaved - coveredSavings);
|
|
192
|
+
state.compressionTokenSavings.set(block.id, rawSaved);
|
|
193
|
+
}
|
|
307
194
|
|
|
308
|
-
|
|
309
|
-
|
|
195
|
+
function preserveProjectedStableId(message: any, stableId: string | undefined): void {
|
|
196
|
+
if (!stableId || !message || typeof message !== "object") return;
|
|
197
|
+
Object.defineProperty(message, "_dcpStableId", {
|
|
198
|
+
value: stableId,
|
|
199
|
+
enumerable: false,
|
|
200
|
+
configurable: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
310
203
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
204
|
+
function markProjectedOrigin(message: any, origin: "block" | "dcp-control", blockId?: number): void {
|
|
205
|
+
if (!message || typeof message !== "object") return;
|
|
206
|
+
Object.defineProperty(message, "_dcpOrigin", {
|
|
207
|
+
value: origin,
|
|
208
|
+
enumerable: false,
|
|
209
|
+
configurable: true,
|
|
210
|
+
});
|
|
211
|
+
if (blockId !== undefined) {
|
|
212
|
+
Object.defineProperty(message, "_dcpBlockId", {
|
|
213
|
+
value: blockId,
|
|
214
|
+
enumerable: false,
|
|
215
|
+
configurable: true,
|
|
216
|
+
});
|
|
324
217
|
}
|
|
325
|
-
|
|
326
|
-
return messages;
|
|
327
218
|
}
|
|
328
219
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
}
|
|
220
|
+
function applyExactMessageBodyBlock(messages: any[], block: CompressionBlock, state: DcpState): boolean {
|
|
221
|
+
const span = exactBlockSpan(messages, block);
|
|
222
|
+
if (!span || span.lo !== span.hi) return false;
|
|
223
|
+
const targetIndex = span.lo;
|
|
224
|
+
const target = messages[targetIndex];
|
|
225
|
+
const removedTokens = estimateMessageTokens(target);
|
|
226
|
+
const text = compressedBlockText(block, "section");
|
|
227
|
+
|
|
228
|
+
if (typeof target?.content === "string") target.content = text;
|
|
229
|
+
else target.content = [{ type: "text", text }];
|
|
230
|
+
preserveProjectedStableId(target, block.startMessageId);
|
|
231
|
+
markProjectedOrigin(target, "block", block.id);
|
|
232
|
+
|
|
233
|
+
const addedTokens = estimateMessageTokens(target);
|
|
234
|
+
accountCompressionBlock(block, removedTokens, addedTokens, state);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
348
237
|
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
238
|
+
function applyExactRangeBlock(messages: any[], block: CompressionBlock, state: DcpState): boolean {
|
|
239
|
+
const span = exactBlockSpan(messages, block);
|
|
240
|
+
if (!span) return false;
|
|
241
|
+
const { lo, hi } = span;
|
|
242
|
+
let removedTokens = 0;
|
|
243
|
+
for (let i = lo; i <= hi; i++) removedTokens += estimateMessageTokens(messages[i]);
|
|
244
|
+
|
|
245
|
+
const syntheticMsg = {
|
|
246
|
+
role: "user",
|
|
247
|
+
content: [{ type: "text", text: compressedBlockText(block, "section") }],
|
|
248
|
+
timestamp: block.startTimestamp,
|
|
249
|
+
};
|
|
250
|
+
markProjectedOrigin(syntheticMsg, "block", block.id);
|
|
251
|
+
messages.splice(lo, hi - lo + 1, syntheticMsg);
|
|
357
252
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (typeof msg.toolCallId === "string" && !assistantToolCallIds.has(msg.toolCallId)) {
|
|
363
|
-
messages.splice(i, 1);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
253
|
+
const addedTokens = estimateMessageTokens(syntheticMsg);
|
|
254
|
+
accountCompressionBlock(block, removedTokens, addedTokens, state);
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
366
257
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (!hasToolCalls) continue;
|
|
258
|
+
export function applyCompressionBlocks(messages: any[], state: DcpState): any[] {
|
|
259
|
+
const activeBlocks = state.compressionBlocks
|
|
260
|
+
.filter((b) => b.active)
|
|
261
|
+
.sort((a, b) => a.startTimestamp - b.startTimestamp);
|
|
262
|
+
if (activeBlocks.length === 0) return messages;
|
|
373
263
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return typeof contentBlock.id === "string" && resultToolCallIds.has(contentBlock.id);
|
|
377
|
-
});
|
|
264
|
+
for (const block of activeBlocks) {
|
|
265
|
+
if (!hasExactCompressionMembership(block)) continue;
|
|
378
266
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
267
|
+
if (block.version === 2 && block.replacementMode === "message-body") {
|
|
268
|
+
applyExactMessageBodyBlock(messages, block, state);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (block.version === 2 && block.replacementMode === "range") {
|
|
272
|
+
applyExactRangeBlock(messages, block, state);
|
|
383
273
|
}
|
|
384
274
|
}
|
|
275
|
+
|
|
276
|
+
return messages;
|
|
385
277
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DcpConfig } from "./config.js";
|
|
2
|
-
import { estimateMessageTokens,
|
|
2
|
+
import { estimateMessageTokens, messageText } from "./pruner-metadata.js";
|
|
3
3
|
import type {
|
|
4
4
|
EmergencyCurrentTurnOutput,
|
|
5
5
|
EmergencyCurrentTurnSelection,
|
|
@@ -48,9 +48,7 @@ export function emergencyPressureState(
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function isRealUserMessage(message: any): boolean {
|
|
51
|
-
|
|
52
|
-
const text = messageText(message);
|
|
53
|
-
return !text.includes("<dcp-system-reminder>") && extractBlockId(text) === undefined;
|
|
51
|
+
return message?.role === "user" && message?._dcpOrigin !== "block" && message?._dcpOrigin !== "dcp-control";
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
function assistantToolCallIds(message: any): string[] {
|
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
ID_ELIGIBLE_ROLES,
|
|
6
6
|
PASSTHROUGH_ROLES,
|
|
7
7
|
estimateMessageTokens,
|
|
8
|
-
extractBlockId,
|
|
9
8
|
messageText,
|
|
10
9
|
stripStaleDcpMetadataLines,
|
|
11
10
|
} from "./pruner-metadata.js";
|
|
11
|
+
import { buildConversationIndex, canonicalMessageHash } from "./conversation-index.js";
|
|
12
|
+
import { recordCompressionProjection } from "./compression-preview.js";
|
|
12
13
|
|
|
13
14
|
export interface InjectMessageIdsOptions {
|
|
14
15
|
/** Config enables priority markers for message-mode candidates. */
|
|
@@ -49,6 +50,9 @@ function fallbackContentFingerprint(msg: any): string {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export function stableMessageId(msg: any, fallbackIndex = 0): string {
|
|
53
|
+
if (typeof msg?._dcpStableId === "string" && msg._dcpStableId.length > 0) {
|
|
54
|
+
return msg._dcpStableId;
|
|
55
|
+
}
|
|
52
56
|
const candidates = [
|
|
53
57
|
msg?.id,
|
|
54
58
|
msg?.entryId,
|
|
@@ -67,8 +71,7 @@ export function stableMessageId(msg: any, fallbackIndex = 0): string {
|
|
|
67
71
|
return `tool:${msg.toolCallId.trim()}`;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
if (blockId !== undefined) return `block:${blockId}`;
|
|
74
|
+
if (Number.isInteger(msg?._dcpBlockId)) return `block:${msg._dcpBlockId}`;
|
|
72
75
|
if (Number.isFinite(msg?.timestamp)) {
|
|
73
76
|
return `ts:${msg.timestamp}:${fallbackContentFingerprint(msg)}`;
|
|
74
77
|
}
|
|
@@ -78,7 +81,7 @@ export function stableMessageId(msg: any, fallbackIndex = 0): string {
|
|
|
78
81
|
/**
|
|
79
82
|
* Return deterministic per-occurrence identities for a message sequence.
|
|
80
83
|
* Session entry IDs and content-fingerprinted fallbacks are normally unique;
|
|
81
|
-
* the suffix keeps byte-identical
|
|
84
|
+
* the suffix keeps byte-identical fallback identities from sharing one mNNN ID.
|
|
82
85
|
*/
|
|
83
86
|
export function stableMessageKeys(messages: any[]): string[] {
|
|
84
87
|
const occurrences = new Map<string, number>();
|
|
@@ -172,17 +175,24 @@ export function injectMessageIds(
|
|
|
172
175
|
const stableKey = stableKeys[messageIndex]!;
|
|
173
176
|
const id = persistentMessageId(stableKey, state);
|
|
174
177
|
const originalText = messageText(msg);
|
|
175
|
-
const blockId =
|
|
178
|
+
const blockId = Number.isInteger(msg?._dcpBlockId) ? msg._dcpBlockId as number : undefined;
|
|
176
179
|
const tokenEstimate = estimateMessageTokens(msg);
|
|
180
|
+
const toolCallIds = role === "assistant" && Array.isArray(msg.content)
|
|
181
|
+
? msg.content
|
|
182
|
+
.filter((part: any) => part?.type === "toolCall" && typeof part.id === "string")
|
|
183
|
+
.map((part: any) => part.id as string)
|
|
184
|
+
: [];
|
|
177
185
|
|
|
178
186
|
assignedIds.set(messageIndex, id);
|
|
179
187
|
assignedMeta.set(messageIndex, {
|
|
180
188
|
timestamp: msg.timestamp,
|
|
181
189
|
stableId: stableKey,
|
|
190
|
+
contentHash: canonicalMessageHash(msg),
|
|
182
191
|
role,
|
|
183
192
|
blockId,
|
|
184
193
|
toolCallId: typeof msg.toolCallId === "string" ? msg.toolCallId : undefined,
|
|
185
194
|
toolName: typeof msg.toolName === "string" ? msg.toolName : undefined,
|
|
195
|
+
toolCallIds: toolCallIds.length > 0 ? toolCallIds : undefined,
|
|
186
196
|
text: originalText,
|
|
187
197
|
tokenEstimate,
|
|
188
198
|
priority: priorityForMessage(tokenEstimate, options.config),
|
|
@@ -224,4 +234,6 @@ export function injectMessageIds(
|
|
|
224
234
|
|
|
225
235
|
state.messageIdSnapshot = nextIdSnapshot;
|
|
226
236
|
state.messageMetaSnapshot = nextMetaSnapshot;
|
|
237
|
+
state.conversationIndexSnapshot = buildConversationIndex(messages, stableKeys, state);
|
|
238
|
+
recordCompressionProjection(state.conversationIndexSnapshot, messages);
|
|
227
239
|
}
|
|
@@ -76,7 +76,17 @@ export function estimateMessageTokens(msg: any): number {
|
|
|
76
76
|
if (part && typeof part === "object") {
|
|
77
77
|
if (typeof part.text === "string") total += estimateTokens(part.text);
|
|
78
78
|
else if (typeof part.thinking === "string") total += estimateTokens(part.thinking);
|
|
79
|
-
else if (part.type === "
|
|
79
|
+
else if (part.type === "toolCall") {
|
|
80
|
+
try {
|
|
81
|
+
// Tool calls are provider-visible context too. Counting the full
|
|
82
|
+
// serialized block keeps range-before and replacement-after on the
|
|
83
|
+
// same estimator and includes IDs, names, arguments/input, and any
|
|
84
|
+
// provider replay metadata without guessing field-by-field.
|
|
85
|
+
total += estimateTokens(JSON.stringify(part) ?? "");
|
|
86
|
+
} catch {
|
|
87
|
+
total += estimateTokens(`${part.name ?? "tool"}:${part.id ?? ""}`);
|
|
88
|
+
}
|
|
89
|
+
} else if (part.type === "image") total += 500; // rough estimate for images
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
92
|
return total;
|