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.
Files changed (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /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 { CompressionBlock, DcpState, MessageIdMeta } from "./state.js"
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 * as fs from "node:fs"
6
- import * as path from "node:path"
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 MAX_PROTECTED_SUBAGENT_RESULT_CHARS = 50_000
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
- const BLOCK_PLACEHOLDER_RE = /\(b(\d+)\)|\{block_(\d+)\}/gi
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)` / legacy `{block_N}` placeholders in a summary with the
108
- * stored content of the referenced compression block. Unrecognised
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
- return summary.replace(BLOCK_PLACEHOLDER_RE, (match, idStr, legacyIdStr) => {
113
- const id = parseInt(idStr ?? legacyIdStr, 10)
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, legacyIdStr) => {
128
- const id = parseInt(idStr ?? legacyIdStr, 10)
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
- startTimestamp <= existing.endTimestamp &&
177
- existing.startTimestamp <= endTimestamp
343
+ compareCompressionBoundaries(requestedStart, existingEnd, state) <= 0 &&
344
+ compareCompressionBoundaries(existingStart, requestedEnd, state) <= 0
178
345
  if (!overlaps) continue
179
346
 
180
347
  const fullyCovered =
181
- startTimestamp <= existing.startTimestamp &&
182
- existing.endTimestamp <= endTimestamp
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 (meta.timestamp < startTimestamp || meta.timestamp > endTimestamp) continue
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 (meta.timestamp < startTimestamp || meta.timestamp > endTimestamp) continue
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 truncateProtectedOutput(text: string, maxChars: number): string {
266
- if (text.length <= maxChars) return text
267
- const omitted = text.length - maxChars
268
- return text.slice(0, maxChars) + `\n[Protected output truncated; ${omitted} characters omitted]`
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 artifactPath = record.artifacts?.resultMd
279
- if (typeof artifactPath !== "string" || artifactPath.trim().length === 0) return undefined
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
- const absolutePath = resolveArtifactPath(artifactPath.trim())
282
- let text: string
473
+ let root: string
474
+ let candidate: string
283
475
  try {
284
- text = fs.readFileSync(absolutePath, "utf8")
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
- const trimmed = text.trim()
290
- if (!trimmed || fallbackText.includes(trimmed)) return undefined
291
- return `\n### Expanded subagent result: ${artifactPath}\n${truncateProtectedOutput(trimmed, MAX_PROTECTED_SUBAGENT_RESULT_CHARS)}`
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 (meta.timestamp < startTimestamp || meta.timestamp > endTimestamp) continue
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
- const expandedSubagentResult =
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 (meta.timestamp < startTimestamp || meta.timestamp > endTimestamp) continue
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(endTimestamp: number, state: DcpState): ResolvedCompressionAnchor {
427
- let anchor: number | null = null
428
- let stableId: string | undefined
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
- const ts = meta.timestamp
431
- if (ts > endTimestamp && (anchor === null || ts < anchor)) {
432
- anchor = ts
433
- stableId = meta.stableId
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: anchor ?? endTimestamp + 1, stableId }
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 = resolveAnchorBoundary(endTimestamp, state).timestamp,
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
- if (startTimestamp > endTimestamp) {
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
- "Select the whole block or choose non-overlapping boundaries.",
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
- { validatePlaceholders, expandPlaceholders },
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: expandedSummary,
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(expandedSummary),
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
  }