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
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import {
|
|
3
|
+
hasExactCompressionMembership,
|
|
4
|
+
type CompressionMember,
|
|
5
|
+
type ConversationIndexEntry,
|
|
6
|
+
type DcpState,
|
|
7
|
+
} from "./state.js"
|
|
8
|
+
import { PASSTHROUGH_ROLES, stripStaleDcpMetadataLines } from "./pruner-metadata.js"
|
|
9
|
+
|
|
10
|
+
export interface ToolGroupSpan {
|
|
11
|
+
startIndex: number
|
|
12
|
+
endIndex: number
|
|
13
|
+
toolCallIds: string[]
|
|
14
|
+
complete: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ClosedRangeSelection {
|
|
18
|
+
requestedStartIndex: number
|
|
19
|
+
requestedEndIndex: number
|
|
20
|
+
startIndex: number
|
|
21
|
+
endIndex: number
|
|
22
|
+
startId?: string
|
|
23
|
+
endId?: string
|
|
24
|
+
expanded: boolean
|
|
25
|
+
incompleteToolGroup: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ExactRangeMembership {
|
|
29
|
+
/** Provider-projection entries that were actually given to the summarizer. */
|
|
30
|
+
sourceMembers: CompressionMember[]
|
|
31
|
+
/** Canonical raw entries that a future materialization may replace. */
|
|
32
|
+
mutationMembers: CompressionMember[]
|
|
33
|
+
/** Indices of selected non-control source entries in the supplied projection. */
|
|
34
|
+
sourceIndexes: number[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const IDENTITY_ONLY_MESSAGE_KEYS = new Set([
|
|
38
|
+
"id",
|
|
39
|
+
"entryId",
|
|
40
|
+
"messageId",
|
|
41
|
+
"_dcpEntryId",
|
|
42
|
+
"_dcpStableId",
|
|
43
|
+
"_dcpOrigin",
|
|
44
|
+
"_dcpBlockId",
|
|
45
|
+
// Timestamp is transport/order metadata. A stable branch identity + content
|
|
46
|
+
// hash must survive hosts that normalize timestamps during reload/import.
|
|
47
|
+
"timestamp",
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
function canonicalContentForMessageHash(message: any): unknown {
|
|
51
|
+
if (!Array.isArray(message?.content)) {
|
|
52
|
+
if (message?.role === "assistant") return message?.content ?? ""
|
|
53
|
+
return stripDcpCarrierText(typeof message?.content === "string" ? message.content : "")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (message?.role === "assistant") return message.content
|
|
57
|
+
|
|
58
|
+
return message.content.flatMap((part: any) => {
|
|
59
|
+
if (!part || typeof part !== "object") return [part]
|
|
60
|
+
const next = { ...part }
|
|
61
|
+
if (typeof next.text === "string") {
|
|
62
|
+
const text = stripDcpCarrierText(next.text)
|
|
63
|
+
if (!text && next.type === "text") return []
|
|
64
|
+
next.text = text
|
|
65
|
+
}
|
|
66
|
+
if (typeof next.thinking === "string") {
|
|
67
|
+
const thinking = stripDcpCarrierText(next.thinking)
|
|
68
|
+
if (!thinking && next.type === "thinking") return []
|
|
69
|
+
next.thinking = thinking
|
|
70
|
+
}
|
|
71
|
+
return [next]
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stripDcpCarrierText(text: string): string {
|
|
76
|
+
return stripStaleDcpMetadataLines(text)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function canonicalHashValue(value: unknown, seen = new WeakSet<object>()): unknown {
|
|
80
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value
|
|
81
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value)
|
|
82
|
+
if (typeof value === "bigint") return `${value}n`
|
|
83
|
+
if (typeof value === "undefined") return "[undefined]"
|
|
84
|
+
if (typeof value === "function" || typeof value === "symbol") return String(value)
|
|
85
|
+
if (Array.isArray(value)) return value.map((item) => canonicalHashValue(item, seen))
|
|
86
|
+
if (typeof value === "object") {
|
|
87
|
+
if (seen.has(value as object)) return "[cycle]"
|
|
88
|
+
seen.add(value as object)
|
|
89
|
+
const output: Record<string, unknown> = {}
|
|
90
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
91
|
+
output[key] = canonicalHashValue((value as Record<string, unknown>)[key], seen)
|
|
92
|
+
}
|
|
93
|
+
seen.delete(value as object)
|
|
94
|
+
return output
|
|
95
|
+
}
|
|
96
|
+
return String(value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Hash a message without mutating it. Assistant content is intentionally
|
|
101
|
+
* copied byte-for-byte into the canonical value: signatures/thinking blocks
|
|
102
|
+
* are never stripped or normalized by DCP membership accounting.
|
|
103
|
+
*/
|
|
104
|
+
export function canonicalMessageHash(message: any): string {
|
|
105
|
+
const canonical: Record<string, unknown> = {}
|
|
106
|
+
if (message && typeof message === "object") {
|
|
107
|
+
for (const key of Object.keys(message).sort()) {
|
|
108
|
+
if (IDENTITY_ONLY_MESSAGE_KEYS.has(key)) continue
|
|
109
|
+
canonical[key] = key === "content"
|
|
110
|
+
? canonicalContentForMessageHash(message)
|
|
111
|
+
: canonicalHashValue(message[key])
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
canonical.value = canonicalHashValue(message)
|
|
115
|
+
}
|
|
116
|
+
return createHash("sha256").update(JSON.stringify(canonicalHashValue(canonical))).digest("hex")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const CANONICAL_HASH_RE = /^[a-f0-9]{64}$/i
|
|
120
|
+
const rawMutationHashes = new WeakMap<object, string>()
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Record the canonical hash of a raw message immediately before DCP rewrites
|
|
124
|
+
* its projection body (tool-output placeholder). The module-owned WeakMap is
|
|
125
|
+
* runtime-only and cannot leak into provider serialization or be spoofed by a
|
|
126
|
+
* message field. Provenance travels on the exact message occurrence, never
|
|
127
|
+
* keyed by a stable identity: fallback identities derived from projected
|
|
128
|
+
* content would not be stable across the rewrite.
|
|
129
|
+
*
|
|
130
|
+
* Without this, exact mutation membership binds to the pruned projection hash,
|
|
131
|
+
* the next raw context pass can never match the span, and the block silently
|
|
132
|
+
* fails to materialize while candidate selection keeps re-selecting the range.
|
|
133
|
+
*/
|
|
134
|
+
export function preserveRawMutationHash(message: any): void {
|
|
135
|
+
if (!message || typeof message !== "object") return
|
|
136
|
+
if (!rawMutationHashes.has(message)) rawMutationHashes.set(message, canonicalMessageHash(message))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Canonical raw (pre-prune) hash preserved on a projected message, if any. */
|
|
140
|
+
export function rawMutationHashOf(message: any): string | undefined {
|
|
141
|
+
if (!message || typeof message !== "object") return undefined
|
|
142
|
+
const preserved = rawMutationHashes.get(message)
|
|
143
|
+
return typeof preserved === "string" && CANONICAL_HASH_RE.test(preserved) ? preserved : undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Transfer module-owned raw provenance when DCP clones a projection message. */
|
|
147
|
+
export function copyRawMutationHash(source: any, target: any): void {
|
|
148
|
+
if (!source || typeof source !== "object" || !target || typeof target !== "object") return
|
|
149
|
+
const preserved = rawMutationHashOf(source)
|
|
150
|
+
if (preserved) rawMutationHashes.set(target, preserved)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasAssistantSignature(message: any): boolean {
|
|
154
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) return false
|
|
155
|
+
return message.content.some((part: any) =>
|
|
156
|
+
part &&
|
|
157
|
+
typeof part === "object" &&
|
|
158
|
+
// These are the SDK's opaque replay fields. Presence is enough to freeze
|
|
159
|
+
// the whole assistant message: a malformed/empty signature is not a
|
|
160
|
+
// reason to rewrite the nested provider-owned content around it.
|
|
161
|
+
(
|
|
162
|
+
Object.prototype.hasOwnProperty.call(part, "textSignature") ||
|
|
163
|
+
Object.prototype.hasOwnProperty.call(part, "thinkingSignature")
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function buildConversationIndex(
|
|
169
|
+
messages: any[],
|
|
170
|
+
stableKeys: string[],
|
|
171
|
+
state: DcpState,
|
|
172
|
+
): ConversationIndexEntry[] {
|
|
173
|
+
const visibleByStableId = new Map<string, string>()
|
|
174
|
+
for (const [visibleId, meta] of state.messageMetaSnapshot) {
|
|
175
|
+
if (meta.stableId) visibleByStableId.set(meta.stableId, visibleId)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return messages.map((message, index) => {
|
|
179
|
+
const stableId = stableKeys[index]!
|
|
180
|
+
const visibleId = visibleByStableId.get(stableId)
|
|
181
|
+
const meta = visibleId ? state.messageMetaSnapshot.get(visibleId) : undefined
|
|
182
|
+
const contentHash = canonicalMessageHash(message)
|
|
183
|
+
const rawContentHash = rawMutationHashOf(message)
|
|
184
|
+
return {
|
|
185
|
+
index,
|
|
186
|
+
stableId,
|
|
187
|
+
contentHash,
|
|
188
|
+
...(rawContentHash && rawContentHash !== contentHash ? { rawContentHash } : {}),
|
|
189
|
+
visibleId,
|
|
190
|
+
role: message?.role ?? "",
|
|
191
|
+
timestamp: Number.isFinite(message?.timestamp) ? message.timestamp : undefined,
|
|
192
|
+
blockId: meta?.blockId,
|
|
193
|
+
toolCallId: typeof message?.toolCallId === "string" ? message.toolCallId : meta?.toolCallId,
|
|
194
|
+
toolCallIds: message?.role === "assistant" && Array.isArray(message.content)
|
|
195
|
+
? message.content.filter((part: any) => part?.type === "toolCall" && typeof part.id === "string").map((part: any) => part.id)
|
|
196
|
+
: undefined,
|
|
197
|
+
passthrough: PASSTHROUGH_ROLES.has(message?.role ?? ""),
|
|
198
|
+
origin: message?._dcpOrigin === "block"
|
|
199
|
+
? "block"
|
|
200
|
+
: message?._dcpOrigin === "dcp-control"
|
|
201
|
+
? "dcp-control"
|
|
202
|
+
: "raw",
|
|
203
|
+
signedAssistant: hasAssistantSignature(message),
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function addressableAliases(entry: ConversationIndexEntry): string[] {
|
|
209
|
+
return [
|
|
210
|
+
...(entry.visibleId ? [entry.visibleId] : []),
|
|
211
|
+
...(entry.blockId !== undefined ? [`b${entry.blockId}`] : []),
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function findConversationIndexEntry(
|
|
216
|
+
entries: ConversationIndexEntry[],
|
|
217
|
+
id: string,
|
|
218
|
+
): ConversationIndexEntry | undefined {
|
|
219
|
+
const normalized = id.trim().toLowerCase()
|
|
220
|
+
return entries.find((entry) =>
|
|
221
|
+
addressableAliases(entry).some((alias) => alias.toLowerCase() === normalized),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Return canonical branch order for two stable identities when both are
|
|
227
|
+
* represented by the current projection. An active block's raw boundaries are
|
|
228
|
+
* deliberately resolved to its single projected placeholder, so roll-ups use
|
|
229
|
+
* projection order rather than timestamp or mNNN allocation order.
|
|
230
|
+
*/
|
|
231
|
+
export function compareConversationStableIds(
|
|
232
|
+
entries: ConversationIndexEntry[],
|
|
233
|
+
leftStableId: string | undefined,
|
|
234
|
+
rightStableId: string | undefined,
|
|
235
|
+
state: DcpState,
|
|
236
|
+
): number | undefined {
|
|
237
|
+
if (!leftStableId || !rightStableId) return undefined
|
|
238
|
+
if (leftStableId === rightStableId) return 0
|
|
239
|
+
|
|
240
|
+
const projectedIndexFor = (stableId: string): number | undefined => {
|
|
241
|
+
const direct = entries.find((entry) => entry.stableId === stableId)
|
|
242
|
+
if (direct) return direct.index
|
|
243
|
+
|
|
244
|
+
const block = state.compressionBlocks.find((candidate) =>
|
|
245
|
+
candidate.active && candidate.version === 2 && hasExactCompressionMembership(candidate) &&
|
|
246
|
+
(candidate.startMessageId === stableId || candidate.endMessageId === stableId),
|
|
247
|
+
)
|
|
248
|
+
if (!block) return undefined
|
|
249
|
+
return entries.find((entry) => entry.blockId === block.id)?.index
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const left = projectedIndexFor(leftStableId)
|
|
253
|
+
const right = projectedIndexFor(rightStableId)
|
|
254
|
+
if (left === undefined || right === undefined) return undefined
|
|
255
|
+
return left - right
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function appendUniqueMembers(
|
|
259
|
+
target: CompressionMember[],
|
|
260
|
+
members: CompressionMember[],
|
|
261
|
+
): boolean {
|
|
262
|
+
const existing = new Map(target.map((member) => [member.stableId, member.hash]))
|
|
263
|
+
for (const member of members) {
|
|
264
|
+
const priorHash = existing.get(member.stableId)
|
|
265
|
+
if (priorHash !== undefined) {
|
|
266
|
+
if (priorHash !== member.hash) return false
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
target.push({ stableId: member.stableId, hash: member.hash })
|
|
270
|
+
existing.set(member.stableId, member.hash)
|
|
271
|
+
}
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Convert a closed projection selection into both source and raw mutation
|
|
277
|
+
* membership. A synthetic block contributes its own visible source entry and
|
|
278
|
+
* expands only through its already-persisted raw mutation membership. This is
|
|
279
|
+
* what prevents a later materialization from rediscovering a timestamp span.
|
|
280
|
+
*/
|
|
281
|
+
export function buildExactRangeMembership(
|
|
282
|
+
entries: ConversationIndexEntry[],
|
|
283
|
+
startId: string,
|
|
284
|
+
endId: string,
|
|
285
|
+
state: DcpState,
|
|
286
|
+
messageBody = false,
|
|
287
|
+
): ExactRangeMembership | undefined {
|
|
288
|
+
const entry = messageBody ? findConversationIndexEntry(entries, startId) : undefined
|
|
289
|
+
const closure = messageBody
|
|
290
|
+
? entry && { startIndex: entry.index, endIndex: entry.index, incompleteToolGroup: false }
|
|
291
|
+
: closeConversationRange(entries, startId, endId)
|
|
292
|
+
if (!closure || closure.incompleteToolGroup) return undefined
|
|
293
|
+
|
|
294
|
+
const sourceMembers: CompressionMember[] = []
|
|
295
|
+
const mutationMembers: CompressionMember[] = []
|
|
296
|
+
const sourceIndexes: number[] = []
|
|
297
|
+
for (let index = closure.startIndex; index <= closure.endIndex; index++) {
|
|
298
|
+
const entry = entries[index]!
|
|
299
|
+
// DCP control-plane entries are never source material and must not become
|
|
300
|
+
// accidental raw mutation members merely because they sit between IDs.
|
|
301
|
+
if (entry.origin === "dcp-control") continue
|
|
302
|
+
if (!entry.stableId || !entry.contentHash) return undefined
|
|
303
|
+
sourceMembers.push({ stableId: entry.stableId, hash: entry.contentHash })
|
|
304
|
+
sourceIndexes.push(entry.index)
|
|
305
|
+
|
|
306
|
+
if (entry.origin === "raw") {
|
|
307
|
+
// Mutation membership binds to the canonical RAW content (pre-prune):
|
|
308
|
+
// materialization always runs against the raw session history before
|
|
309
|
+
// DCP rewrites tool-result bodies. sourceMembers keeps the projected
|
|
310
|
+
// hash because that is exactly what the summarizer inspected.
|
|
311
|
+
const rawHash = entry.rawContentHash ?? entry.contentHash
|
|
312
|
+
if (!appendUniqueMembers(mutationMembers, [{ stableId: entry.stableId, hash: rawHash }])) {
|
|
313
|
+
return undefined
|
|
314
|
+
}
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (entry.origin !== "block" || entry.blockId === undefined) return undefined
|
|
319
|
+
const block = state.compressionBlocks.find((candidate) => candidate.id === entry.blockId && candidate.active)
|
|
320
|
+
if (!block || !hasExactCompressionMembership(block)) return undefined
|
|
321
|
+
if (!appendUniqueMembers(mutationMembers, block.mutationMembers!)) return undefined
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (sourceMembers.length === 0 || mutationMembers.length === 0) return undefined
|
|
325
|
+
return { sourceMembers, mutationMembers, sourceIndexes }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function detectToolGroupSpans(entries: ConversationIndexEntry[]): ToolGroupSpan[] {
|
|
329
|
+
const groups: ToolGroupSpan[] = []
|
|
330
|
+
|
|
331
|
+
for (let i = 0; i < entries.length; i++) {
|
|
332
|
+
const assistant = entries[i]!
|
|
333
|
+
if (assistant.role !== "assistant" || !assistant.toolCallIds?.length) continue
|
|
334
|
+
|
|
335
|
+
const pending = new Set(assistant.toolCallIds)
|
|
336
|
+
let endIndex = i
|
|
337
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
338
|
+
const entry = entries[j]!
|
|
339
|
+
if (entry.passthrough) {
|
|
340
|
+
endIndex = j
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
if (
|
|
344
|
+
(entry.role === "toolResult" || entry.role === "bashExecution") &&
|
|
345
|
+
entry.toolCallId &&
|
|
346
|
+
pending.has(entry.toolCallId)
|
|
347
|
+
) {
|
|
348
|
+
pending.delete(entry.toolCallId)
|
|
349
|
+
endIndex = j
|
|
350
|
+
if (pending.size === 0) break
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
break
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
groups.push({
|
|
357
|
+
startIndex: i,
|
|
358
|
+
endIndex,
|
|
359
|
+
toolCallIds: [...assistant.toolCallIds],
|
|
360
|
+
complete: pending.size === 0,
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return groups
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function nearestAddressableId(
|
|
368
|
+
entries: ConversationIndexEntry[],
|
|
369
|
+
index: number,
|
|
370
|
+
direction: 1 | -1,
|
|
371
|
+
): string | undefined {
|
|
372
|
+
for (let i = index; i >= 0 && i < entries.length; i += direction) {
|
|
373
|
+
const entry = entries[i]!
|
|
374
|
+
if (entry.blockId !== undefined) return `b${entry.blockId}`
|
|
375
|
+
if (entry.visibleId) return entry.visibleId
|
|
376
|
+
}
|
|
377
|
+
return undefined
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function closeConversationRange(
|
|
381
|
+
entries: ConversationIndexEntry[],
|
|
382
|
+
startId: string,
|
|
383
|
+
endId: string,
|
|
384
|
+
): ClosedRangeSelection | undefined {
|
|
385
|
+
const startEntry = findConversationIndexEntry(entries, startId)
|
|
386
|
+
const endEntry = findConversationIndexEntry(entries, endId)
|
|
387
|
+
if (!startEntry || !endEntry) return undefined
|
|
388
|
+
|
|
389
|
+
const requestedStartIndex = Math.min(startEntry.index, endEntry.index)
|
|
390
|
+
const requestedEndIndex = Math.max(startEntry.index, endEntry.index)
|
|
391
|
+
let startIndex = requestedStartIndex
|
|
392
|
+
let endIndex = requestedEndIndex
|
|
393
|
+
let incompleteToolGroup = false
|
|
394
|
+
const groups = detectToolGroupSpans(entries)
|
|
395
|
+
|
|
396
|
+
let changed: boolean
|
|
397
|
+
do {
|
|
398
|
+
changed = false
|
|
399
|
+
for (const group of groups) {
|
|
400
|
+
const overlaps = group.startIndex <= endIndex && group.endIndex >= startIndex
|
|
401
|
+
if (!overlaps) continue
|
|
402
|
+
if (!group.complete) incompleteToolGroup = true
|
|
403
|
+
const nextStart = Math.min(startIndex, group.startIndex)
|
|
404
|
+
const nextEnd = Math.max(endIndex, group.endIndex)
|
|
405
|
+
if (nextStart !== startIndex || nextEnd !== endIndex) changed = true
|
|
406
|
+
startIndex = nextStart
|
|
407
|
+
endIndex = nextEnd
|
|
408
|
+
}
|
|
409
|
+
} while (changed)
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
requestedStartIndex,
|
|
413
|
+
requestedEndIndex,
|
|
414
|
+
startIndex,
|
|
415
|
+
endIndex,
|
|
416
|
+
startId: nearestAddressableId(entries, startIndex, 1),
|
|
417
|
+
endId: nearestAddressableId(entries, endIndex, -1),
|
|
418
|
+
expanded: startIndex !== requestedStartIndex || endIndex !== requestedEndIndex,
|
|
419
|
+
incompleteToolGroup,
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -88,7 +88,11 @@ function sessionInfo(ctx: ExtensionContext | undefined): Record<string, unknown>
|
|
|
88
88
|
return info
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export function summarizeDcpState(state: DcpState): Record<string, unknown> {
|
|
91
|
+
export function summarizeDcpState(state: DcpState, config?: DcpConfig): Record<string, unknown> {
|
|
92
|
+
// Call sites often construct debug detail objects before writeDcpDebugLog can
|
|
93
|
+
// early-return. Passing config lets this potentially O(n) snapshot stay an
|
|
94
|
+
// O(1) no-op when debug logging is disabled.
|
|
95
|
+
if (config && !dcpDebugEnabled(config)) return {}
|
|
92
96
|
const rawIds = [...new Set([
|
|
93
97
|
...state.messageIdSnapshot.keys(),
|
|
94
98
|
...state.messageMetaSnapshot.keys(),
|
|
@@ -130,12 +134,10 @@ export function summarizeDcpState(state: DcpState): Record<string, unknown> {
|
|
|
130
134
|
persistentMessageIds: state.messageIdsByStableId.size,
|
|
131
135
|
nextMessageId: state.nextMessageId,
|
|
132
136
|
prunedTools: state.prunedToolIds.size,
|
|
133
|
-
automaticPruneCheckpoint: {
|
|
134
|
-
turn: state.lastAutomaticPruneTurn,
|
|
135
|
-
blockId: state.lastAutomaticPruneBlockId,
|
|
136
|
-
},
|
|
137
137
|
providerSeenTools: state.providerSeenToolIds.size,
|
|
138
138
|
consecutiveEmergencyPasses: state.consecutiveIgnoredStrongNudges,
|
|
139
|
+
consecutiveActionableOpportunities: state.consecutiveIgnoredNudges,
|
|
140
|
+
compressionProgress: state.compressionProgress,
|
|
139
141
|
nudgeAnchors: state.nudgeAnchors.map((anchor) => ({
|
|
140
142
|
id: anchor.id,
|
|
141
143
|
type: anchor.type,
|