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,11 +1,20 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CompressionBlock,
|
|
3
|
+
CompressionMember,
|
|
4
|
+
CompressionProtectedFragment,
|
|
5
|
+
DcpState,
|
|
6
|
+
MessageIdMeta,
|
|
7
|
+
} from "./state.js"
|
|
2
8
|
import type { DcpConfig } from "./config.js"
|
|
3
9
|
import { estimateTokens } from "./pruner-metadata.js"
|
|
4
10
|
import { isToolRecordProtected } from "./pruner-tools.js"
|
|
5
|
-
import
|
|
6
|
-
import
|
|
11
|
+
import { compareConversationStableIds, buildExactRangeMembership } from "./conversation-index.js"
|
|
12
|
+
import { createHash } from "node:crypto"
|
|
13
|
+
import { open, realpath } from "node:fs/promises"
|
|
14
|
+
import { isAbsolute, resolve, sep } from "node:path"
|
|
7
15
|
|
|
8
|
-
const
|
|
16
|
+
const MAX_PROTECTED_ARTIFACT_BYTES = 50_000
|
|
17
|
+
const MAX_PROTECTED_ARTIFACT_TOTAL_BYTES = 200_000
|
|
9
18
|
|
|
10
19
|
export interface CompressionBlockCreationResult {
|
|
11
20
|
block: CompressionBlock
|
|
@@ -26,8 +35,16 @@ export interface CreateRangeCompressionBlockOptions {
|
|
|
26
35
|
anchorMessageId?: string
|
|
27
36
|
createdByToolCallId?: string
|
|
28
37
|
mode?: "range" | "message"
|
|
38
|
+
version?: 2
|
|
39
|
+
replacementMode?: "range" | "message-body"
|
|
29
40
|
validatePlaceholders?: boolean
|
|
30
41
|
expandPlaceholders?: boolean
|
|
42
|
+
/** E07 protected fragments prepared with bounded async artifact reads. */
|
|
43
|
+
preparedProtectedFragments?: CompressionProtectedFragment[]
|
|
44
|
+
/** Exact current-projection source membership prepared before summary generation. */
|
|
45
|
+
sourceMembers?: CompressionMember[]
|
|
46
|
+
/** Exact canonical raw mutation membership prepared with sourceMembers. */
|
|
47
|
+
mutationMembers?: CompressionMember[]
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
export interface ResolvedCompressionBoundary {
|
|
@@ -37,7 +54,135 @@ export interface ResolvedCompressionBoundary {
|
|
|
37
54
|
|
|
38
55
|
export interface ResolvedCompressionAnchor extends ResolvedCompressionBoundary {}
|
|
39
56
|
|
|
40
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a model-visible range against the exact latest projection. New
|
|
59
|
+
* callers must persist this plan with their block instead of reconstructing a
|
|
60
|
+
* timestamp interval during a later context pass.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveExactRangeMembership(
|
|
63
|
+
startId: string,
|
|
64
|
+
endId: string,
|
|
65
|
+
state: DcpState,
|
|
66
|
+
messageBody = false,
|
|
67
|
+
): { sourceMembers: CompressionMember[]; mutationMembers: CompressionMember[] } | undefined {
|
|
68
|
+
const plan = buildExactRangeMembership(
|
|
69
|
+
state.conversationIndexSnapshot,
|
|
70
|
+
startId,
|
|
71
|
+
endId,
|
|
72
|
+
state,
|
|
73
|
+
messageBody,
|
|
74
|
+
)
|
|
75
|
+
if (!plan) return undefined
|
|
76
|
+
return {
|
|
77
|
+
sourceMembers: plan.sourceMembers,
|
|
78
|
+
mutationMembers: plan.mutationMembers,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasExactMembers(
|
|
83
|
+
sourceMembers: CompressionMember[] | undefined,
|
|
84
|
+
mutationMembers: CompressionMember[] | undefined,
|
|
85
|
+
): boolean {
|
|
86
|
+
const valid = (members: CompressionMember[] | undefined): members is CompressionMember[] =>
|
|
87
|
+
Array.isArray(members) &&
|
|
88
|
+
members.length > 0 &&
|
|
89
|
+
members.every((member) =>
|
|
90
|
+
typeof member?.stableId === "string" &&
|
|
91
|
+
member.stableId.length > 0 &&
|
|
92
|
+
typeof member.hash === "string" &&
|
|
93
|
+
/^[a-f0-9]{64}$/i.test(member.hash),
|
|
94
|
+
) &&
|
|
95
|
+
new Set(members.map((member) => member.stableId)).size === members.length
|
|
96
|
+
return valid(sourceMembers) && valid(mutationMembers)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function copyMembers(members: CompressionMember[]): CompressionMember[] {
|
|
100
|
+
return members.map((member) => ({ stableId: member.stableId, hash: member.hash.toLowerCase() }))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function messageOrdinal(stableId: string | undefined, state: DcpState): number | undefined {
|
|
104
|
+
if (!stableId) return undefined
|
|
105
|
+
const visibleId = state.messageIdsByStableId.get(stableId) ??
|
|
106
|
+
[...state.messageMetaSnapshot.entries()].find(([, meta]) => meta.stableId === stableId)?.[0]
|
|
107
|
+
const match = visibleId?.match(/^m(\d+)$/i)
|
|
108
|
+
if (!match) return undefined
|
|
109
|
+
const ordinal = Number.parseInt(match[1]!, 10)
|
|
110
|
+
return Number.isFinite(ordinal) ? ordinal : undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compare two conversation boundaries. Current canonical branch order wins
|
|
115
|
+
* whenever both stable identities resolve; timestamps are used only for
|
|
116
|
+
* transient in-memory inputs that have not received stable identities yet.
|
|
117
|
+
*/
|
|
118
|
+
export function compareCompressionBoundaries(
|
|
119
|
+
left: ResolvedCompressionBoundary,
|
|
120
|
+
right: ResolvedCompressionBoundary,
|
|
121
|
+
state: DcpState,
|
|
122
|
+
): number {
|
|
123
|
+
const canonicalOrder = compareConversationStableIds(
|
|
124
|
+
state.conversationIndexSnapshot,
|
|
125
|
+
left.stableId,
|
|
126
|
+
right.stableId,
|
|
127
|
+
state,
|
|
128
|
+
)
|
|
129
|
+
if (canonicalOrder !== undefined) return canonicalOrder
|
|
130
|
+
if (left.timestamp !== right.timestamp) return left.timestamp - right.timestamp
|
|
131
|
+
const leftOrdinal = messageOrdinal(left.stableId, state)
|
|
132
|
+
const rightOrdinal = messageOrdinal(right.stableId, state)
|
|
133
|
+
if (leftOrdinal !== undefined && rightOrdinal !== undefined && leftOrdinal !== rightOrdinal) {
|
|
134
|
+
return leftOrdinal - rightOrdinal
|
|
135
|
+
}
|
|
136
|
+
return 0
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function isCompressionBoundaryWithinRange(
|
|
140
|
+
boundary: ResolvedCompressionBoundary,
|
|
141
|
+
start: ResolvedCompressionBoundary,
|
|
142
|
+
end: ResolvedCompressionBoundary,
|
|
143
|
+
state: DcpState,
|
|
144
|
+
): boolean {
|
|
145
|
+
return compareCompressionBoundaries(boundary, start, state) >= 0 &&
|
|
146
|
+
compareCompressionBoundaries(boundary, end, state) <= 0
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function rangeBoundaries(
|
|
150
|
+
startTimestamp: number,
|
|
151
|
+
endTimestamp: number,
|
|
152
|
+
ids: { startMessageId?: string; endMessageId?: string } = {},
|
|
153
|
+
): { start: ResolvedCompressionBoundary; end: ResolvedCompressionBoundary } {
|
|
154
|
+
return {
|
|
155
|
+
start: { timestamp: startTimestamp, stableId: ids.startMessageId },
|
|
156
|
+
end: { timestamp: endTimestamp, stableId: ids.endMessageId },
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function inferExactMembershipFromBoundaries(
|
|
161
|
+
startMessageId: string | undefined,
|
|
162
|
+
endMessageId: string | undefined,
|
|
163
|
+
state: DcpState,
|
|
164
|
+
messageBody = false,
|
|
165
|
+
): { sourceMembers: CompressionMember[]; mutationMembers: CompressionMember[] } | undefined {
|
|
166
|
+
if (!startMessageId || !endMessageId) return undefined
|
|
167
|
+
const entries = state.conversationIndexSnapshot
|
|
168
|
+
const aliasFor = (stableId: string): string | undefined => {
|
|
169
|
+
const direct = entries.find((entry) => entry.stableId === stableId)
|
|
170
|
+
if (direct?.visibleId) return direct.visibleId
|
|
171
|
+
if (direct?.blockId !== undefined) return `b${direct.blockId}`
|
|
172
|
+
|
|
173
|
+
const block = state.compressionBlocks.find((candidate) =>
|
|
174
|
+
candidate.active && (candidate.startMessageId === stableId || candidate.endMessageId === stableId),
|
|
175
|
+
)
|
|
176
|
+
if (!block) return undefined
|
|
177
|
+
return entries.some((entry) => entry.blockId === block.id) ? `b${block.id}` : undefined
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const startId = aliasFor(startMessageId)
|
|
181
|
+
const endId = aliasFor(endMessageId)
|
|
182
|
+
return startId && endId ? resolveExactRangeMembership(startId, endId, state, messageBody) : undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const BLOCK_PLACEHOLDER_RE = /\(b(\d+)\)/gi
|
|
41
186
|
const MAX_DIAGNOSTIC_IDS = 24
|
|
42
187
|
|
|
43
188
|
function idSortKey(id: string): [number, string] {
|
|
@@ -104,14 +249,16 @@ function formatRestoredBlock(block: CompressionBlock): string {
|
|
|
104
249
|
}
|
|
105
250
|
|
|
106
251
|
/**
|
|
107
|
-
* Replace `(bN)`
|
|
108
|
-
*
|
|
109
|
-
* placeholders are left as-is for backwards compatibility.
|
|
252
|
+
* Replace `(bN)` placeholders in a summary with the stored content of the
|
|
253
|
+
* referenced compression block. Unrecognised placeholders are left as-is.
|
|
110
254
|
*/
|
|
111
255
|
export function expandBlockPlaceholders(summary: string, state: DcpState): string {
|
|
112
|
-
|
|
113
|
-
|
|
256
|
+
const consumed = new Set<number>()
|
|
257
|
+
return summary.replace(BLOCK_PLACEHOLDER_RE, (match, idStr) => {
|
|
258
|
+
const id = parseInt(idStr, 10)
|
|
114
259
|
const block = state.compressionBlocks.find((b) => b.id === id)
|
|
260
|
+
if (block && consumed.has(id)) return ""
|
|
261
|
+
if (block) consumed.add(id)
|
|
115
262
|
return block ? formatRestoredBlock(block) : match
|
|
116
263
|
})
|
|
117
264
|
}
|
|
@@ -124,8 +271,8 @@ function expandBlockPlaceholdersWithRecovery(
|
|
|
124
271
|
const coveredIds = new Set(coveredBlocks.map((block) => block.id))
|
|
125
272
|
const consumed = new Set<number>()
|
|
126
273
|
|
|
127
|
-
const expanded = summary.replace(BLOCK_PLACEHOLDER_RE, (_match, idStr
|
|
128
|
-
const id = parseInt(idStr
|
|
274
|
+
const expanded = summary.replace(BLOCK_PLACEHOLDER_RE, (_match, idStr) => {
|
|
275
|
+
const id = parseInt(idStr, 10)
|
|
129
276
|
if (!coveredIds.has(id) || consumed.has(id)) return ""
|
|
130
277
|
|
|
131
278
|
const block = state.compressionBlocks.find((b) => b.id === id)
|
|
@@ -147,6 +294,20 @@ function expandBlockPlaceholdersWithRecovery(
|
|
|
147
294
|
return expanded + recoveryHeading + recovery
|
|
148
295
|
}
|
|
149
296
|
|
|
297
|
+
function assertVerifiableBlockPlaceholders(summary: string, state: DcpState): void {
|
|
298
|
+
const unknown = new Set<number>()
|
|
299
|
+
for (const match of summary.matchAll(BLOCK_PLACEHOLDER_RE)) {
|
|
300
|
+
const id = Number.parseInt(match[1] ?? "", 10)
|
|
301
|
+
if (!Number.isInteger(id) || !state.compressionBlocks.some((block) => block.id === id)) unknown.add(id)
|
|
302
|
+
}
|
|
303
|
+
if (unknown.size > 0) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Compression summary contains unverifiable block placeholder(s): ${[...unknown].map((id) => `b${id}`).join(", ")}. ` +
|
|
306
|
+
"Expand the referenced content explicitly or retry with current block IDs.",
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
150
311
|
function preparePlaceholderSummary(
|
|
151
312
|
summary: string,
|
|
152
313
|
coveredBlocks: CompressionBlock[],
|
|
@@ -164,22 +325,28 @@ export function findCoveredAndPartialBlocks(
|
|
|
164
325
|
startTimestamp: number,
|
|
165
326
|
endTimestamp: number,
|
|
166
327
|
state: DcpState,
|
|
328
|
+
boundaries: { startMessageId?: string; endMessageId?: string } = {},
|
|
167
329
|
): { coveredBlocks: CompressionBlock[]; partialBlocks: CompressionBlock[] } {
|
|
168
330
|
const coveredBlocks: CompressionBlock[] = []
|
|
169
331
|
const partialBlocks: CompressionBlock[] = []
|
|
332
|
+
const requestedStart = { timestamp: startTimestamp, stableId: boundaries.startMessageId }
|
|
333
|
+
const requestedEnd = { timestamp: endTimestamp, stableId: boundaries.endMessageId }
|
|
170
334
|
|
|
171
335
|
for (const existing of state.compressionBlocks) {
|
|
172
336
|
if (!existing.active) continue
|
|
173
337
|
if (!Number.isFinite(existing.startTimestamp) || !Number.isFinite(existing.endTimestamp)) continue
|
|
174
338
|
|
|
339
|
+
const existingStart = { timestamp: existing.startTimestamp, stableId: existing.startMessageId }
|
|
340
|
+
const existingEnd = { timestamp: existing.endTimestamp, stableId: existing.endMessageId }
|
|
341
|
+
|
|
175
342
|
const overlaps =
|
|
176
|
-
|
|
177
|
-
|
|
343
|
+
compareCompressionBoundaries(requestedStart, existingEnd, state) <= 0 &&
|
|
344
|
+
compareCompressionBoundaries(existingStart, requestedEnd, state) <= 0
|
|
178
345
|
if (!overlaps) continue
|
|
179
346
|
|
|
180
347
|
const fullyCovered =
|
|
181
|
-
|
|
182
|
-
|
|
348
|
+
compareCompressionBoundaries(requestedStart, existingStart, state) <= 0 &&
|
|
349
|
+
compareCompressionBoundaries(existingEnd, requestedEnd, state) <= 0
|
|
183
350
|
|
|
184
351
|
if (fullyCovered) coveredBlocks.push(existing)
|
|
185
352
|
else partialBlocks.push(existing)
|
|
@@ -188,6 +355,22 @@ export function findCoveredAndPartialBlocks(
|
|
|
188
355
|
return { coveredBlocks, partialBlocks }
|
|
189
356
|
}
|
|
190
357
|
|
|
358
|
+
function firstAddressableRawIdAfterBlock(block: CompressionBlock, state: DcpState): string | undefined {
|
|
359
|
+
const endOrdinal = messageOrdinal(block.endMessageId, state)
|
|
360
|
+
if (endOrdinal === undefined) return undefined
|
|
361
|
+
|
|
362
|
+
let best: { id: string; ordinal: number } | undefined
|
|
363
|
+
for (const [id, meta] of state.messageMetaSnapshot) {
|
|
364
|
+
if (meta.blockId !== undefined) continue
|
|
365
|
+
const match = id.match(/^m(\d+)$/i)
|
|
366
|
+
if (!match) continue
|
|
367
|
+
const ordinal = Number.parseInt(match[1]!, 10)
|
|
368
|
+
if (!Number.isFinite(ordinal) || ordinal <= endOrdinal) continue
|
|
369
|
+
if (!best || ordinal < best.ordinal) best = { id, ordinal }
|
|
370
|
+
}
|
|
371
|
+
return best?.id
|
|
372
|
+
}
|
|
373
|
+
|
|
191
374
|
export function getMessageMeta(id: string, state: DcpState): MessageIdMeta | undefined {
|
|
192
375
|
return state.messageMetaSnapshot.get(id.trim())
|
|
193
376
|
}
|
|
@@ -207,17 +390,24 @@ export function appendProtectedPromptInfo(
|
|
|
207
390
|
endTimestamp: number,
|
|
208
391
|
state: DcpState,
|
|
209
392
|
config: DcpConfig,
|
|
393
|
+
ids: { startMessageId?: string; endMessageId?: string } = {},
|
|
210
394
|
): string {
|
|
211
395
|
if (!config.compress.protectTags) return summary
|
|
212
396
|
|
|
213
397
|
const protectedTexts: string[] = []
|
|
214
398
|
const seen = new Set<string>()
|
|
399
|
+
const boundaries = rangeBoundaries(startTimestamp, endTimestamp, ids)
|
|
215
400
|
|
|
216
401
|
for (const meta of state.messageMetaSnapshot.values()) {
|
|
217
402
|
if (meta.role !== "user") continue
|
|
218
403
|
if (meta.blockId !== undefined) continue
|
|
219
404
|
if (!Number.isFinite(meta.timestamp)) continue
|
|
220
|
-
if (
|
|
405
|
+
if (!isCompressionBoundaryWithinRange(
|
|
406
|
+
{ timestamp: meta.timestamp, stableId: meta.stableId },
|
|
407
|
+
boundaries.start,
|
|
408
|
+
boundaries.end,
|
|
409
|
+
state,
|
|
410
|
+
)) continue
|
|
221
411
|
|
|
222
412
|
for (const text of extractProtectTagTexts(meta.text ?? "")) {
|
|
223
413
|
if (seen.has(text)) continue
|
|
@@ -238,17 +428,24 @@ export function appendProtectedUserMessages(
|
|
|
238
428
|
endTimestamp: number,
|
|
239
429
|
state: DcpState,
|
|
240
430
|
enabled: boolean,
|
|
431
|
+
ids: { startMessageId?: string; endMessageId?: string } = {},
|
|
241
432
|
): string {
|
|
242
433
|
if (!enabled) return summary
|
|
243
434
|
|
|
244
435
|
const userTexts: string[] = []
|
|
245
436
|
const seen = new Set<string>()
|
|
437
|
+
const boundaries = rangeBoundaries(startTimestamp, endTimestamp, ids)
|
|
246
438
|
|
|
247
439
|
for (const meta of state.messageMetaSnapshot.values()) {
|
|
248
440
|
if (meta.role !== "user") continue
|
|
249
441
|
if (meta.blockId !== undefined) continue
|
|
250
442
|
if (!Number.isFinite(meta.timestamp)) continue
|
|
251
|
-
if (
|
|
443
|
+
if (!isCompressionBoundaryWithinRange(
|
|
444
|
+
{ timestamp: meta.timestamp, stableId: meta.stableId },
|
|
445
|
+
boundaries.start,
|
|
446
|
+
boundaries.end,
|
|
447
|
+
state,
|
|
448
|
+
)) continue
|
|
252
449
|
|
|
253
450
|
const text = meta.text?.trim()
|
|
254
451
|
if (!text || seen.has(text)) continue
|
|
@@ -262,33 +459,202 @@ export function appendProtectedUserMessages(
|
|
|
262
459
|
return summary + heading + userTexts.map((text) => `\n${text}`).join("")
|
|
263
460
|
}
|
|
264
461
|
|
|
265
|
-
function
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function resolveArtifactPath(filePath: string): string {
|
|
272
|
-
return path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function readProtectedSubagentResult(details: unknown, fallbackText: string): string | undefined {
|
|
276
|
-
if (!details || typeof details !== "object") return undefined
|
|
462
|
+
async function readBoundedProtectedSubagentArtifact(
|
|
463
|
+
details: unknown,
|
|
464
|
+
fallbackText: string,
|
|
465
|
+
cwd: string | undefined,
|
|
466
|
+
): Promise<{ artifactPath: string; text: string } | undefined> {
|
|
467
|
+
if (!cwd || !details || typeof details !== "object") return undefined
|
|
277
468
|
const record = details as any
|
|
278
|
-
const
|
|
279
|
-
if (typeof
|
|
469
|
+
const rawArtifactPath = record.artifacts?.resultMd
|
|
470
|
+
if (typeof rawArtifactPath !== "string" || rawArtifactPath.trim().length === 0) return undefined
|
|
471
|
+
const artifactPath = rawArtifactPath.trim()
|
|
280
472
|
|
|
281
|
-
|
|
282
|
-
let
|
|
473
|
+
let root: string
|
|
474
|
+
let candidate: string
|
|
283
475
|
try {
|
|
284
|
-
|
|
476
|
+
root = await realpath(cwd)
|
|
477
|
+
const requested = isAbsolute(artifactPath) ? artifactPath : resolve(root, artifactPath)
|
|
478
|
+
candidate = await realpath(requested)
|
|
285
479
|
} catch {
|
|
480
|
+
// Missing artifacts are optional recovery material when the tool result text
|
|
481
|
+
// itself is still available. Do not invent a replacement source.
|
|
286
482
|
return undefined
|
|
287
483
|
}
|
|
288
484
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
485
|
+
if (candidate !== root && !candidate.startsWith(root + sep)) {
|
|
486
|
+
throw new Error(`Protected artifact resolves outside the session cwd: ${artifactPath}`)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const handle = await open(candidate, "r")
|
|
490
|
+
try {
|
|
491
|
+
const info = await handle.stat()
|
|
492
|
+
if (!info.isFile()) throw new Error(`Protected artifact is not a regular file: ${artifactPath}`)
|
|
493
|
+
if (info.size > MAX_PROTECTED_ARTIFACT_BYTES) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`Protected artifact exceeds the ${MAX_PROTECTED_ARTIFACT_BYTES}-byte recovery budget: ${artifactPath}`,
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
const buffer = Buffer.alloc(MAX_PROTECTED_ARTIFACT_BYTES + 1)
|
|
499
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0)
|
|
500
|
+
if (bytesRead > MAX_PROTECTED_ARTIFACT_BYTES) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
`Protected artifact grew beyond the ${MAX_PROTECTED_ARTIFACT_BYTES}-byte recovery budget: ${artifactPath}`,
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8").trim()
|
|
506
|
+
if (!text || fallbackText.includes(text)) return undefined
|
|
507
|
+
return { artifactPath, text }
|
|
508
|
+
} finally {
|
|
509
|
+
await handle.close()
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function compressionProtectedFragment(
|
|
514
|
+
kind: CompressionProtectedFragment["kind"],
|
|
515
|
+
origin: string,
|
|
516
|
+
text: string,
|
|
517
|
+
): CompressionProtectedFragment {
|
|
518
|
+
return {
|
|
519
|
+
kind,
|
|
520
|
+
origin,
|
|
521
|
+
hash: createHash("sha256").update(text).digest("hex"),
|
|
522
|
+
text,
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function mergeProtectedFragments(...groups: CompressionProtectedFragment[][]): CompressionProtectedFragment[] {
|
|
527
|
+
const merged: CompressionProtectedFragment[] = []
|
|
528
|
+
const seen = new Set<string>()
|
|
529
|
+
for (const fragment of groups.flat()) {
|
|
530
|
+
const key = `${fragment.origin}:${fragment.hash}`
|
|
531
|
+
if (seen.has(key)) continue
|
|
532
|
+
seen.add(key)
|
|
533
|
+
merged.push(fragment)
|
|
534
|
+
}
|
|
535
|
+
return merged
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function collectCurrentProtectedFragments(
|
|
539
|
+
startTimestamp: number,
|
|
540
|
+
endTimestamp: number,
|
|
541
|
+
state: DcpState,
|
|
542
|
+
config: DcpConfig,
|
|
543
|
+
mode: "range" | "message",
|
|
544
|
+
ids: { startMessageId?: string; endMessageId?: string },
|
|
545
|
+
): CompressionProtectedFragment[] {
|
|
546
|
+
const fragments: CompressionProtectedFragment[] = []
|
|
547
|
+
const boundaries = rangeBoundaries(startTimestamp, endTimestamp, ids)
|
|
548
|
+
|
|
549
|
+
for (const [visibleId, meta] of state.messageMetaSnapshot) {
|
|
550
|
+
if (meta.blockId !== undefined || !Number.isFinite(meta.timestamp)) continue
|
|
551
|
+
if (!isCompressionBoundaryWithinRange(
|
|
552
|
+
{ timestamp: meta.timestamp, stableId: meta.stableId },
|
|
553
|
+
boundaries.start,
|
|
554
|
+
boundaries.end,
|
|
555
|
+
state,
|
|
556
|
+
)) continue
|
|
557
|
+
|
|
558
|
+
const originBase = meta.stableId ?? visibleId ?? String(meta.timestamp)
|
|
559
|
+
if (mode === "range" && config.compress.protectUserMessages && meta.role === "user") {
|
|
560
|
+
const text = meta.text?.trim()
|
|
561
|
+
if (text) fragments.push(compressionProtectedFragment("user", `user:${originBase}`, text))
|
|
562
|
+
}
|
|
563
|
+
if (config.compress.protectTags && meta.role === "user") {
|
|
564
|
+
for (const [index, text] of extractProtectTagTexts(meta.text ?? "").entries()) {
|
|
565
|
+
fragments.push(compressionProtectedFragment("prompt", `prompt:${originBase}:${index}`, text))
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (meta.role !== "toolResult" && meta.role !== "bashExecution") continue
|
|
569
|
+
const toolCallId = meta.toolCallId
|
|
570
|
+
if (!toolCallId) continue
|
|
571
|
+
const record = state.toolCalls.get(toolCallId)
|
|
572
|
+
if (!record || record.toolName === "compress" || !isToolRecordProtected(record, config)) continue
|
|
573
|
+
const output = (record.outputText ?? meta.text ?? "").trim()
|
|
574
|
+
if (!output) continue
|
|
575
|
+
const exact = `### Tool: ${record.toolName}\n${output}`
|
|
576
|
+
fragments.push(compressionProtectedFragment("tool", `tool:${toolCallId}`, exact))
|
|
577
|
+
}
|
|
578
|
+
return mergeProtectedFragments(fragments)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export interface PrepareCompressionProtectedFragmentsOptions {
|
|
582
|
+
startTimestamp: number
|
|
583
|
+
endTimestamp: number
|
|
584
|
+
startMessageId?: string
|
|
585
|
+
endMessageId?: string
|
|
586
|
+
state: DcpState
|
|
587
|
+
config: DcpConfig
|
|
588
|
+
mode: "range" | "message"
|
|
589
|
+
cwd?: string
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Prepare protected continuity fragments before the synchronous block commit.
|
|
594
|
+
* Raw transcript/state remains the primary source. Optional subagent artifacts
|
|
595
|
+
* are read only through bounded async I/O rooted at the session cwd.
|
|
596
|
+
*/
|
|
597
|
+
export async function prepareCompressionProtectedFragments(
|
|
598
|
+
options: PrepareCompressionProtectedFragmentsOptions,
|
|
599
|
+
): Promise<CompressionProtectedFragment[]> {
|
|
600
|
+
const { startTimestamp, endTimestamp, startMessageId, endMessageId, state, config, mode, cwd } = options
|
|
601
|
+
const ids = { startMessageId, endMessageId }
|
|
602
|
+
const { coveredBlocks } = findCoveredAndPartialBlocks(startTimestamp, endTimestamp, state, ids)
|
|
603
|
+
const fragments = mergeProtectedFragments(
|
|
604
|
+
collectInheritedProtectedFragments(coveredBlocks),
|
|
605
|
+
collectCurrentProtectedFragments(startTimestamp, endTimestamp, state, config, mode, ids),
|
|
606
|
+
)
|
|
607
|
+
if (!cwd) return fragments
|
|
608
|
+
|
|
609
|
+
const boundaries = rangeBoundaries(startTimestamp, endTimestamp, ids)
|
|
610
|
+
let totalArtifactBytes = 0
|
|
611
|
+
for (const meta of state.messageMetaSnapshot.values()) {
|
|
612
|
+
if (meta.blockId !== undefined || !Number.isFinite(meta.timestamp)) continue
|
|
613
|
+
if (!isCompressionBoundaryWithinRange(
|
|
614
|
+
{ timestamp: meta.timestamp, stableId: meta.stableId },
|
|
615
|
+
boundaries.start,
|
|
616
|
+
boundaries.end,
|
|
617
|
+
state,
|
|
618
|
+
)) continue
|
|
619
|
+
if (meta.role !== "toolResult" && meta.role !== "bashExecution") continue
|
|
620
|
+
const toolCallId = meta.toolCallId
|
|
621
|
+
if (!toolCallId) continue
|
|
622
|
+
const record = state.toolCalls.get(toolCallId)
|
|
623
|
+
if (!record || !isToolRecordProtected(record, config)) continue
|
|
624
|
+
if (record.toolName !== "subagents" && record.toolName !== "async_subagents_result") continue
|
|
625
|
+
const output = (record.outputText ?? meta.text ?? "").trim()
|
|
626
|
+
if (!output) continue
|
|
627
|
+
|
|
628
|
+
const artifact = await readBoundedProtectedSubagentArtifact(record.outputDetails, output, cwd)
|
|
629
|
+
if (!artifact) continue
|
|
630
|
+
const artifactBytes = Buffer.byteLength(artifact.text, "utf8")
|
|
631
|
+
totalArtifactBytes += artifactBytes
|
|
632
|
+
if (totalArtifactBytes > MAX_PROTECTED_ARTIFACT_TOTAL_BYTES) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`Protected artifacts exceed the ${MAX_PROTECTED_ARTIFACT_TOTAL_BYTES}-byte total recovery budget`,
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
fragments.push(compressionProtectedFragment(
|
|
638
|
+
"tool",
|
|
639
|
+
`artifact:${toolCallId}:${artifact.artifactPath}`,
|
|
640
|
+
`### Expanded subagent result: ${artifact.artifactPath}\n${artifact.text}`,
|
|
641
|
+
))
|
|
642
|
+
}
|
|
643
|
+
return mergeProtectedFragments(fragments)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function collectInheritedProtectedFragments(coveredBlocks: CompressionBlock[]): CompressionProtectedFragment[] {
|
|
647
|
+
return mergeProtectedFragments(
|
|
648
|
+
...coveredBlocks.map((block) => block.protectedFragments ?? []),
|
|
649
|
+
)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function appendProtectedFragmentLedger(summary: string, fragments: CompressionProtectedFragment[]): string {
|
|
653
|
+
const missing = fragments.filter((fragment) => !summary.includes(fragment.text))
|
|
654
|
+
if (missing.length === 0) return summary
|
|
655
|
+
return summary +
|
|
656
|
+
"\n\nThe following protected continuity fragments were preserved verbatim:" +
|
|
657
|
+
missing.map((fragment) => `\n\n${fragment.text}`).join("")
|
|
292
658
|
}
|
|
293
659
|
|
|
294
660
|
function appendProtectedToolOutputs(
|
|
@@ -297,14 +663,21 @@ function appendProtectedToolOutputs(
|
|
|
297
663
|
endTimestamp: number,
|
|
298
664
|
state: DcpState,
|
|
299
665
|
config: DcpConfig,
|
|
666
|
+
ids: { startMessageId?: string; endMessageId?: string } = {},
|
|
300
667
|
): string {
|
|
301
668
|
const protectedOutputs: string[] = []
|
|
302
669
|
const seenToolCallIds = new Set<string>()
|
|
670
|
+
const boundaries = rangeBoundaries(startTimestamp, endTimestamp, ids)
|
|
303
671
|
|
|
304
672
|
for (const meta of state.messageMetaSnapshot.values()) {
|
|
305
673
|
if (meta.blockId !== undefined) continue
|
|
306
674
|
if (!Number.isFinite(meta.timestamp)) continue
|
|
307
|
-
if (
|
|
675
|
+
if (!isCompressionBoundaryWithinRange(
|
|
676
|
+
{ timestamp: meta.timestamp, stableId: meta.stableId },
|
|
677
|
+
boundaries.start,
|
|
678
|
+
boundaries.end,
|
|
679
|
+
state,
|
|
680
|
+
)) continue
|
|
308
681
|
if (meta.role !== "toolResult" && meta.role !== "bashExecution") continue
|
|
309
682
|
|
|
310
683
|
const toolCallId = meta.toolCallId
|
|
@@ -321,11 +694,7 @@ function appendProtectedToolOutputs(
|
|
|
321
694
|
if (!output) continue
|
|
322
695
|
|
|
323
696
|
seenToolCallIds.add(toolCallId)
|
|
324
|
-
|
|
325
|
-
record.toolName === "subagents" || record.toolName === "async_subagents_result"
|
|
326
|
-
? readProtectedSubagentResult(record.outputDetails, output)
|
|
327
|
-
: undefined
|
|
328
|
-
protectedOutputs.push(`\n### Tool: ${record.toolName}\n${output}${expandedSubagentResult ?? ""}`)
|
|
697
|
+
protectedOutputs.push(`\n### Tool: ${record.toolName}\n${output}`)
|
|
329
698
|
}
|
|
330
699
|
|
|
331
700
|
if (protectedOutputs.length === 0) return summary
|
|
@@ -338,12 +707,19 @@ export function estimateVisibleRangeTokens(
|
|
|
338
707
|
endTimestamp: number,
|
|
339
708
|
coveredBlocks: CompressionBlock[],
|
|
340
709
|
state: DcpState,
|
|
710
|
+
ids: { startMessageId?: string; endMessageId?: string } = {},
|
|
341
711
|
): number {
|
|
342
712
|
let total = 0
|
|
713
|
+
const boundaries = rangeBoundaries(startTimestamp, endTimestamp, ids)
|
|
343
714
|
for (const meta of state.messageMetaSnapshot.values()) {
|
|
344
715
|
if (meta.blockId !== undefined) continue
|
|
345
716
|
if (!Number.isFinite(meta.timestamp)) continue
|
|
346
|
-
if (
|
|
717
|
+
if (!isCompressionBoundaryWithinRange(
|
|
718
|
+
{ timestamp: meta.timestamp, stableId: meta.stableId },
|
|
719
|
+
boundaries.start,
|
|
720
|
+
boundaries.end,
|
|
721
|
+
state,
|
|
722
|
+
)) continue
|
|
347
723
|
total += Math.max(0, Math.round(meta.tokenEstimate ?? 0))
|
|
348
724
|
}
|
|
349
725
|
for (const block of coveredBlocks) {
|
|
@@ -423,22 +799,25 @@ export function resolveAnchorTimestamp(endTimestamp: number, state: DcpState): n
|
|
|
423
799
|
return resolveAnchorBoundary(endTimestamp, state).timestamp
|
|
424
800
|
}
|
|
425
801
|
|
|
426
|
-
export function resolveAnchorBoundary(
|
|
427
|
-
|
|
428
|
-
|
|
802
|
+
export function resolveAnchorBoundary(
|
|
803
|
+
endTimestamp: number,
|
|
804
|
+
state: DcpState,
|
|
805
|
+
endMessageId?: string,
|
|
806
|
+
): ResolvedCompressionAnchor {
|
|
807
|
+
const endBoundary = { timestamp: endTimestamp, stableId: endMessageId }
|
|
808
|
+
let anchor: ResolvedCompressionAnchor | null = null
|
|
429
809
|
for (const meta of state.messageMetaSnapshot.values()) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
810
|
+
if (meta.blockId !== undefined || !Number.isFinite(meta.timestamp)) continue
|
|
811
|
+
const candidate = { timestamp: meta.timestamp, stableId: meta.stableId }
|
|
812
|
+
if (compareCompressionBoundaries(candidate, endBoundary, state) <= 0) continue
|
|
813
|
+
if (anchor === null || compareCompressionBoundaries(candidate, anchor, state) < 0) anchor = candidate
|
|
435
814
|
}
|
|
436
815
|
if (anchor === null) {
|
|
437
816
|
for (const ts of state.messageIdSnapshot.values()) {
|
|
438
|
-
if (ts > endTimestamp && (anchor === null || ts < anchor)) anchor = ts
|
|
817
|
+
if (ts > endTimestamp && (anchor === null || ts < anchor.timestamp)) anchor = { timestamp: ts }
|
|
439
818
|
}
|
|
440
819
|
}
|
|
441
|
-
return { timestamp:
|
|
820
|
+
return anchor ?? { timestamp: endTimestamp + 1 }
|
|
442
821
|
}
|
|
443
822
|
|
|
444
823
|
export function createRangeCompressionBlock(
|
|
@@ -453,15 +832,33 @@ export function createRangeCompressionBlock(
|
|
|
453
832
|
endMessageId,
|
|
454
833
|
state,
|
|
455
834
|
config,
|
|
456
|
-
anchorTimestamp
|
|
457
|
-
anchorMessageId,
|
|
835
|
+
anchorTimestamp: requestedAnchorTimestamp,
|
|
836
|
+
anchorMessageId: requestedAnchorMessageId,
|
|
458
837
|
createdByToolCallId,
|
|
459
838
|
mode = "range",
|
|
839
|
+
version = 2,
|
|
840
|
+
replacementMode,
|
|
460
841
|
validatePlaceholders = true,
|
|
461
842
|
expandPlaceholders = true,
|
|
843
|
+
preparedProtectedFragments = [],
|
|
844
|
+
sourceMembers: requestedSourceMembers,
|
|
845
|
+
mutationMembers: requestedMutationMembers,
|
|
462
846
|
} = options
|
|
463
|
-
|
|
464
|
-
|
|
847
|
+
const defaultAnchor = resolveAnchorBoundary(endTimestamp, state, endMessageId)
|
|
848
|
+
const anchorTimestamp = requestedAnchorTimestamp ?? defaultAnchor.timestamp
|
|
849
|
+
const anchorMessageId = requestedAnchorMessageId ?? defaultAnchor.stableId
|
|
850
|
+
const ids = { startMessageId, endMessageId }
|
|
851
|
+
const inferredMembership = !requestedSourceMembers && !requestedMutationMembers
|
|
852
|
+
? inferExactMembershipFromBoundaries(startMessageId, endMessageId, state, replacementMode === "message-body")
|
|
853
|
+
: undefined
|
|
854
|
+
const sourceMembers = requestedSourceMembers ?? inferredMembership?.sourceMembers
|
|
855
|
+
const mutationMembers = requestedMutationMembers ?? inferredMembership?.mutationMembers
|
|
856
|
+
|
|
857
|
+
if (compareCompressionBoundaries(
|
|
858
|
+
{ timestamp: startTimestamp, stableId: startMessageId },
|
|
859
|
+
{ timestamp: endTimestamp, stableId: endMessageId },
|
|
860
|
+
state,
|
|
861
|
+
) > 0) {
|
|
465
862
|
throw new Error("Compression range start must appear before end in the conversation")
|
|
466
863
|
}
|
|
467
864
|
|
|
@@ -476,21 +873,56 @@ export function createRangeCompressionBlock(
|
|
|
476
873
|
startTimestamp,
|
|
477
874
|
endTimestamp,
|
|
478
875
|
state,
|
|
876
|
+
{ startMessageId, endMessageId },
|
|
479
877
|
)
|
|
480
878
|
|
|
481
879
|
if (partialBlocks.length > 0) {
|
|
482
880
|
const blockList = partialBlocks.map((block) => `b${block.id} "${block.topic}"`).join(", ")
|
|
881
|
+
const freeHints = partialBlocks
|
|
882
|
+
.map((block) => {
|
|
883
|
+
const firstFreeId = firstAddressableRawIdAfterBlock(block, state)
|
|
884
|
+
return firstFreeId ? `first raw ID after b${block.id} is ${firstFreeId}` : undefined
|
|
885
|
+
})
|
|
886
|
+
.filter((hint): hint is string => typeof hint === "string")
|
|
887
|
+
const freeHint = freeHints.length > 0 ? ` ${freeHints.join("; ")}.` : ""
|
|
483
888
|
throw new Error(
|
|
484
889
|
`Compression range partially overlaps existing block(s): ${blockList}. ` +
|
|
485
|
-
|
|
890
|
+
`Select the whole block or choose non-overlapping boundaries.${freeHint}`,
|
|
486
891
|
)
|
|
487
892
|
}
|
|
488
893
|
|
|
894
|
+
if (version !== 2 || !hasExactMembers(sourceMembers, mutationMembers)) {
|
|
895
|
+
throw new Error(
|
|
896
|
+
"Compression requires exact canonical source and mutation membership. " +
|
|
897
|
+
"Refresh the current DCP snapshot and retry with visible IDs.",
|
|
898
|
+
)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Every block in the new format carries a protected-fragment ledger. Do not
|
|
902
|
+
// silently fall back to the old recursive-summary behavior: a missing ledger
|
|
903
|
+
// means the in-memory state violates the journal contract and the roll-up
|
|
904
|
+
// must fail closed.
|
|
905
|
+
if (coveredBlocks.some((block) => block.version !== 2 || block.protectedFragments === undefined)) {
|
|
906
|
+
throw new Error("Compression roll-up requires exact v2 blocks with protected-fragment ledgers")
|
|
907
|
+
}
|
|
908
|
+
const ledgerRollup = coveredBlocks.length > 0
|
|
909
|
+
const hasExplicitBlockReferences = [...summary.matchAll(BLOCK_PLACEHOLDER_RE)].length > 0
|
|
910
|
+
if (ledgerRollup) assertVerifiableBlockPlaceholders(summary, state)
|
|
911
|
+
|
|
489
912
|
const placeholderSummary = preparePlaceholderSummary(
|
|
490
913
|
summary,
|
|
491
914
|
coveredBlocks,
|
|
492
915
|
state,
|
|
493
|
-
{
|
|
916
|
+
{
|
|
917
|
+
validatePlaceholders: ledgerRollup ? false : validatePlaceholders,
|
|
918
|
+
// Explicit references are deliberately expanded once, never left to
|
|
919
|
+
// dangle after their source blocks are deactivated. Otherwise modern
|
|
920
|
+
// ledger roll-ups replace old summary prose instead of recursively
|
|
921
|
+
// appending it.
|
|
922
|
+
expandPlaceholders: ledgerRollup
|
|
923
|
+
? hasExplicitBlockReferences
|
|
924
|
+
: expandPlaceholders,
|
|
925
|
+
},
|
|
494
926
|
)
|
|
495
927
|
|
|
496
928
|
const userPreservedSummary = mode === "range"
|
|
@@ -500,6 +932,7 @@ export function createRangeCompressionBlock(
|
|
|
500
932
|
endTimestamp,
|
|
501
933
|
state,
|
|
502
934
|
config.compress.protectUserMessages,
|
|
935
|
+
ids,
|
|
503
936
|
)
|
|
504
937
|
: placeholderSummary
|
|
505
938
|
|
|
@@ -509,6 +942,7 @@ export function createRangeCompressionBlock(
|
|
|
509
942
|
endTimestamp,
|
|
510
943
|
state,
|
|
511
944
|
config,
|
|
945
|
+
ids,
|
|
512
946
|
)
|
|
513
947
|
|
|
514
948
|
const expandedSummary = appendProtectedToolOutputs(
|
|
@@ -517,12 +951,19 @@ export function createRangeCompressionBlock(
|
|
|
517
951
|
endTimestamp,
|
|
518
952
|
state,
|
|
519
953
|
config,
|
|
954
|
+
ids,
|
|
955
|
+
)
|
|
956
|
+
const protectedFragments = mergeProtectedFragments(
|
|
957
|
+
collectInheritedProtectedFragments(coveredBlocks),
|
|
958
|
+
collectCurrentProtectedFragments(startTimestamp, endTimestamp, state, config, mode, ids),
|
|
959
|
+
preparedProtectedFragments,
|
|
520
960
|
)
|
|
961
|
+
const ledgerPreservedSummary = appendProtectedFragmentLedger(expandedSummary, protectedFragments)
|
|
521
962
|
|
|
522
963
|
const block: CompressionBlock = {
|
|
523
964
|
id: state.nextBlockId++,
|
|
524
965
|
topic,
|
|
525
|
-
summary:
|
|
966
|
+
summary: ledgerPreservedSummary,
|
|
526
967
|
startTimestamp,
|
|
527
968
|
endTimestamp,
|
|
528
969
|
startMessageId,
|
|
@@ -531,10 +972,15 @@ export function createRangeCompressionBlock(
|
|
|
531
972
|
anchorMessageId,
|
|
532
973
|
createdByToolCallId,
|
|
533
974
|
active: true,
|
|
534
|
-
summaryTokenEstimate: estimateTokens(
|
|
975
|
+
summaryTokenEstimate: estimateTokens(ledgerPreservedSummary),
|
|
535
976
|
createdAt: Date.now(),
|
|
536
977
|
coveredBlockIds: coveredBlocks.map((covered) => covered.id),
|
|
537
978
|
mode,
|
|
979
|
+
version: 2,
|
|
980
|
+
replacementMode,
|
|
981
|
+
sourceMembers: copyMembers(sourceMembers!),
|
|
982
|
+
mutationMembers: copyMembers(mutationMembers!),
|
|
983
|
+
protectedFragments,
|
|
538
984
|
}
|
|
539
985
|
|
|
540
986
|
state.compressionBlocks.push(block)
|
|
@@ -544,7 +990,7 @@ export function createRangeCompressionBlock(
|
|
|
544
990
|
|
|
545
991
|
return {
|
|
546
992
|
block,
|
|
547
|
-
removedTokenEstimate: estimateVisibleRangeTokens(startTimestamp, endTimestamp, coveredBlocks, state),
|
|
993
|
+
removedTokenEstimate: estimateVisibleRangeTokens(startTimestamp, endTimestamp, coveredBlocks, state, ids),
|
|
548
994
|
summaryTokenEstimate: Math.max(0, Math.round(block.summaryTokenEstimate ?? 0)),
|
|
549
995
|
}
|
|
550
996
|
}
|