pi-ui-extend 1.0.39 → 1.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/app/commands/command-registry.js +2 -2
- package/dist/app/commands/command-session-actions.d.ts +0 -1
- package/dist/app/commands/command-session-actions.js +22 -13
- package/dist/app/icons.d.ts +14 -0
- package/dist/app/icons.js +33 -0
- package/dist/app/rendering/conversation-tool-renderer.js +2 -2
- package/dist/app/rendering/dcp-stats.d.ts +6 -1
- package/dist/app/rendering/dcp-stats.js +214 -46
- package/dist/app/rendering/editor-panels.js +8 -5
- package/dist/app/session/lazy-session-manager.js +12 -1
- package/dist/app/session/tabs-controller.d.ts +2 -5
- package/dist/app/session/tabs-controller.js +12 -21
- package/dist/app/subagents/subagents-model.d.ts +14 -1
- package/dist/app/subagents/subagents-model.js +34 -15
- package/dist/app/types.d.ts +2 -0
- package/dist/bundled-extensions/session-title/config.js +1 -1
- package/dist/markdown-format.js +27 -9
- package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
- package/dist/schemas/pi-tools-suite-schema.js +46 -31
- package/external/pi-tools-suite/README.md +392 -52
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
- package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
- package/external/pi-tools-suite/docs/evals.md +684 -0
- package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
- package/external/pi-tools-suite/package.json +10 -3
- package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
- package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
- package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
- package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
- package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
- package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
- package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
- package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
- package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
- package/external/pi-tools-suite/src/config.ts +1 -22
- package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
- package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
- package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
- package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
- package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
- package/external/pi-tools-suite/src/dcp/config.ts +36 -61
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
- package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
- package/external/pi-tools-suite/src/dcp/index.ts +617 -203
- package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
- package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
- package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
- package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
- package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
- package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
- package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
- package/external/pi-tools-suite/src/dcp/state.ts +158 -580
- package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
- package/external/pi-tools-suite/src/index.ts +9 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
- package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
- package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
- package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
- package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
- package/package.json +6 -6
- package/schemas/pi-tools-suite.json +159 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
import { Type } from "typebox"
|
|
6
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
7
|
-
import type { DcpState } from "./state.js"
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
7
|
+
import type { CompressionMember, DcpState } from "./state.js"
|
|
8
8
|
import { modelKeysFromContext, resolveModelConfig, type DcpConfig } from "./config.js"
|
|
9
|
-
import {
|
|
9
|
+
import { captureDcpTransactionGuard, cloneDcpTransactionState, runDcpStateTransaction } from "./state-transaction.js"
|
|
10
|
+
import { createHash } from "node:crypto"
|
|
10
11
|
import { clearDcpNudgeAnchors } from "./pruner.js"
|
|
11
12
|
import type { DcpCompressionVisualDetails } from "./ui.js"
|
|
12
13
|
import { normalizeDcpContextUsage } from "./ui.js"
|
|
@@ -14,13 +15,23 @@ import { COMPRESS_TOOL_DESCRIPTION } from "../tool-descriptions.js"
|
|
|
14
15
|
import { safeGetContextUsage } from "../context-usage.js"
|
|
15
16
|
import { summarizeDcpState, writeDcpDebugLog } from "./debug-log.js"
|
|
16
17
|
import {
|
|
18
|
+
compareCompressionBoundaries,
|
|
17
19
|
createRangeCompressionBlock,
|
|
18
20
|
findCoveredAndPartialBlocks,
|
|
19
21
|
formatCompressionIdDiagnostics,
|
|
20
22
|
getMessageMeta,
|
|
23
|
+
prepareCompressionProtectedFragments,
|
|
21
24
|
resolveAnchorBoundary,
|
|
25
|
+
resolveExactRangeMembership,
|
|
22
26
|
resolveIdToBoundary,
|
|
23
27
|
} from "./compression-blocks.js"
|
|
28
|
+
import {
|
|
29
|
+
closeConversationRange,
|
|
30
|
+
detectToolGroupSpans,
|
|
31
|
+
findConversationIndexEntry,
|
|
32
|
+
} from "./conversation-index.js"
|
|
33
|
+
import { previewManualCompressionProjection } from "./compression-preview.js"
|
|
34
|
+
import { settleCompressionProgress } from "./compression-progress.js"
|
|
24
35
|
|
|
25
36
|
type MessageSkipKind =
|
|
26
37
|
| "duplicate"
|
|
@@ -29,6 +40,7 @@ type MessageSkipKind =
|
|
|
29
40
|
| "non-finite"
|
|
30
41
|
| "protected-user"
|
|
31
42
|
| "already-compressed"
|
|
43
|
+
| "tool-group"
|
|
32
44
|
|
|
33
45
|
interface MessageSkipIssue {
|
|
34
46
|
kind: MessageSkipKind
|
|
@@ -44,19 +56,45 @@ interface ResolvedRangePlan {
|
|
|
44
56
|
endTimestamp: number
|
|
45
57
|
startMessageId?: string
|
|
46
58
|
endMessageId?: string
|
|
59
|
+
sourceMembers: CompressionMember[]
|
|
60
|
+
mutationMembers: CompressionMember[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ResolvedMessagePlan {
|
|
64
|
+
messageId: string
|
|
65
|
+
topic: string
|
|
66
|
+
summary: string
|
|
67
|
+
timestamp: number
|
|
68
|
+
stableId?: string
|
|
69
|
+
sourceMembers: CompressionMember[]
|
|
70
|
+
mutationMembers: CompressionMember[]
|
|
47
71
|
}
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
type ResolvedRangeBoundaries = Omit<ResolvedRangePlan, "sourceMembers" | "mutationMembers">
|
|
74
|
+
|
|
75
|
+
function validateNonOverlappingRanges(plans: ResolvedRangeBoundaries[], state: DcpState): void {
|
|
50
76
|
const sorted = [...plans].sort((a, b) =>
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
compareCompressionBoundaries(
|
|
78
|
+
{ timestamp: a.startTimestamp, stableId: a.startMessageId },
|
|
79
|
+
{ timestamp: b.startTimestamp, stableId: b.startMessageId },
|
|
80
|
+
state,
|
|
81
|
+
) ||
|
|
82
|
+
compareCompressionBoundaries(
|
|
83
|
+
{ timestamp: a.endTimestamp, stableId: a.endMessageId },
|
|
84
|
+
{ timestamp: b.endTimestamp, stableId: b.endMessageId },
|
|
85
|
+
state,
|
|
86
|
+
),
|
|
53
87
|
)
|
|
54
88
|
const issues: string[] = []
|
|
55
89
|
|
|
56
90
|
for (let i = 1; i < sorted.length; i++) {
|
|
57
91
|
const previous = sorted[i - 1]!
|
|
58
92
|
const current = sorted[i]!
|
|
59
|
-
if (
|
|
93
|
+
if (compareCompressionBoundaries(
|
|
94
|
+
{ timestamp: current.startTimestamp, stableId: current.startMessageId },
|
|
95
|
+
{ timestamp: previous.endTimestamp, stableId: previous.endMessageId },
|
|
96
|
+
state,
|
|
97
|
+
) > 0) continue
|
|
60
98
|
issues.push(
|
|
61
99
|
`${previous.startId}..${previous.endId} overlaps ${current.startId}..${current.endId}`,
|
|
62
100
|
)
|
|
@@ -69,6 +107,67 @@ function validateNonOverlappingRanges(plans: ResolvedRangePlan[]): void {
|
|
|
69
107
|
}
|
|
70
108
|
}
|
|
71
109
|
|
|
110
|
+
function validateProtocolClosedRanges(plans: ResolvedRangeBoundaries[], state: DcpState): void {
|
|
111
|
+
if (state.conversationIndexSnapshot.length === 0) return
|
|
112
|
+
|
|
113
|
+
for (const plan of plans) {
|
|
114
|
+
const closure = closeConversationRange(state.conversationIndexSnapshot, plan.startId, plan.endId)
|
|
115
|
+
if (!closure) continue
|
|
116
|
+
if (closure.incompleteToolGroup) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Compression range ${plan.startId}..${plan.endId} intersects an incomplete tool group. ` +
|
|
119
|
+
"Wait until the complete assistant/tool-result group is present or choose another closed range.",
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
if (closure.expanded) {
|
|
123
|
+
const safeStart = closure.startId ?? plan.startId
|
|
124
|
+
const safeEnd = closure.endId ?? plan.endId
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Compression range ${plan.startId}..${plan.endId} cuts through a tool group. ` +
|
|
127
|
+
`The protocol-safe closed range is ${safeStart}..${safeEnd}. ` +
|
|
128
|
+
"Retry with the complete range so the supplied summary covers every message that will be removed.",
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function selectionMemberKeys(membership: Pick<ResolvedRangePlan | ResolvedMessagePlan, "sourceMembers" | "mutationMembers">): Set<string> {
|
|
135
|
+
return new Set(
|
|
136
|
+
[...membership.sourceMembers, ...membership.mutationMembers]
|
|
137
|
+
.map((member) => `${member.stableId}:${member.hash}`),
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function assertDisjointSelections(
|
|
142
|
+
ranges: ResolvedRangePlan[],
|
|
143
|
+
messages: ResolvedMessagePlan[],
|
|
144
|
+
): void {
|
|
145
|
+
const selections = [
|
|
146
|
+
...ranges.map((range) => ({ label: `${range.startId}..${range.endId}`, membership: range })),
|
|
147
|
+
...messages.map((message) => ({ label: message.messageId, membership: message })),
|
|
148
|
+
]
|
|
149
|
+
for (let leftIndex = 0; leftIndex < selections.length; leftIndex++) {
|
|
150
|
+
const left = selections[leftIndex]!
|
|
151
|
+
const leftKeys = selectionMemberKeys(left.membership)
|
|
152
|
+
for (let rightIndex = leftIndex + 1; rightIndex < selections.length; rightIndex++) {
|
|
153
|
+
const right = selections[rightIndex]!
|
|
154
|
+
if ([...selectionMemberKeys(right.membership)].some((key) => leftKeys.has(key))) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Overlapping compression selections cannot be committed in the same call: ${left.label} overlaps ${right.label}.`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function messageTouchesIncompleteToolGroup(messageId: string, state: DcpState): boolean {
|
|
164
|
+
const entry = findConversationIndexEntry(state.conversationIndexSnapshot, messageId)
|
|
165
|
+
if (!entry) return false
|
|
166
|
+
return detectToolGroupSpans(state.conversationIndexSnapshot).some((group) =>
|
|
167
|
+
!group.complete && entry.index >= group.startIndex && entry.index <= group.endIndex,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
72
171
|
function formatSkippedMessages(issues: MessageSkipIssue[]): string[] {
|
|
73
172
|
const grouped = new Map<MessageSkipKind, string[]>()
|
|
74
173
|
for (const issue of issues) {
|
|
@@ -82,6 +181,7 @@ function formatSkippedMessages(issues: MessageSkipIssue[]): string[] {
|
|
|
82
181
|
"non-finite": "resolved to a corrupted non-finite timestamp",
|
|
83
182
|
"protected-user": "is a raw user message protected by compress.protectUserMessages",
|
|
84
183
|
"already-compressed": "already belongs to an active compression block",
|
|
184
|
+
"tool-group": "belongs to a structurally coupled tool group and cannot be replaced as one isolated message",
|
|
85
185
|
}
|
|
86
186
|
|
|
87
187
|
return Array.from(grouped.entries()).map(([kind, ids]) => {
|
|
@@ -98,15 +198,49 @@ function formatSkippedMessages(issues: MessageSkipIssue[]): string[] {
|
|
|
98
198
|
})
|
|
99
199
|
}
|
|
100
200
|
|
|
201
|
+
function createCompressionWorkingState(state: DcpState): DcpState {
|
|
202
|
+
return cloneDcpTransactionState(state)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function commitCompressionWorkingState(state: DcpState, workingState: DcpState): void {
|
|
206
|
+
state.compressionBlocks = workingState.compressionBlocks
|
|
207
|
+
state.nextBlockId = workingState.nextBlockId
|
|
208
|
+
state.nudgeAnchors = workingState.nudgeAnchors
|
|
209
|
+
state.nudgeCounter = workingState.nudgeCounter
|
|
210
|
+
state.lastNudge = workingState.lastNudge
|
|
211
|
+
state.consecutiveIgnoredStrongNudges = workingState.consecutiveIgnoredStrongNudges
|
|
212
|
+
state.consecutiveIgnoredNudges = workingState.consecutiveIgnoredNudges
|
|
213
|
+
state.compressionProgress = workingState.compressionProgress
|
|
214
|
+
}
|
|
215
|
+
|
|
101
216
|
// ---------------------------------------------------------------------------
|
|
102
217
|
// Tool registration
|
|
103
218
|
// ---------------------------------------------------------------------------
|
|
104
219
|
|
|
220
|
+
export interface CompressToolDependencies {
|
|
221
|
+
persistState?: (
|
|
222
|
+
ctx: ExtensionContext,
|
|
223
|
+
state: DcpState,
|
|
224
|
+
publication?: { beforePublish?: () => void; onPublished?: () => void },
|
|
225
|
+
) => Promise<void> | void
|
|
226
|
+
isSessionSupported?: () => boolean
|
|
227
|
+
}
|
|
228
|
+
|
|
105
229
|
export function registerCompressTool(
|
|
106
230
|
pi: ExtensionAPI,
|
|
107
231
|
state: DcpState,
|
|
108
232
|
config: DcpConfig,
|
|
233
|
+
dependencies: CompressToolDependencies = {},
|
|
109
234
|
): void {
|
|
235
|
+
const persistState = dependencies.persistState ?? (async (
|
|
236
|
+
_ctx: ExtensionContext,
|
|
237
|
+
_state: DcpState,
|
|
238
|
+
publication?: { beforePublish?: () => void; onPublished?: () => void },
|
|
239
|
+
) => {
|
|
240
|
+
publication?.beforePublish?.()
|
|
241
|
+
publication?.onPublished?.()
|
|
242
|
+
})
|
|
243
|
+
const isSessionSupported = dependencies.isSessionSupported ?? (() => true)
|
|
110
244
|
pi.registerTool({
|
|
111
245
|
name: "compress",
|
|
112
246
|
label: COMPRESS_TOOL_DESCRIPTION.label,
|
|
@@ -121,11 +255,11 @@ export function registerCompressTool(
|
|
|
121
255
|
Type.Object({
|
|
122
256
|
startId: Type.String({
|
|
123
257
|
description:
|
|
124
|
-
"
|
|
258
|
+
"First ID (mNNN/bN); never start inside a tool group—include its calling assistant.",
|
|
125
259
|
}),
|
|
126
260
|
endId: Type.String({
|
|
127
261
|
description:
|
|
128
|
-
"
|
|
262
|
+
"Last ID (mNNN/bN); include all results of the final tool group, including parallel calls.",
|
|
129
263
|
}),
|
|
130
264
|
summary: Type.String({
|
|
131
265
|
description:
|
|
@@ -151,6 +285,15 @@ export function registerCompressTool(
|
|
|
151
285
|
}),
|
|
152
286
|
|
|
153
287
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
288
|
+
_signal?.throwIfAborted()
|
|
289
|
+
if (!isSessionSupported()) {
|
|
290
|
+
throw new Error("DCP is unavailable in this session. Start a new session to use the current DCP journal format.")
|
|
291
|
+
}
|
|
292
|
+
const assertCurrent = captureDcpTransactionGuard(state, config, _signal, ctx)
|
|
293
|
+
const requestHash = createHash("sha256").update(JSON.stringify(params)).digest("hex")
|
|
294
|
+
return runDcpStateTransaction(state, async () => {
|
|
295
|
+
assertCurrent()
|
|
296
|
+
const operationEpoch = state.sessionEpoch
|
|
154
297
|
const effectiveConfig = resolveModelConfig(config, modelKeysFromContext(ctx))
|
|
155
298
|
if (!effectiveConfig.enabled) {
|
|
156
299
|
throw new Error("DCP is disabled for the active model")
|
|
@@ -159,6 +302,8 @@ export function registerCompressTool(
|
|
|
159
302
|
const newBlockIds: number[] = []
|
|
160
303
|
const ranges = Array.isArray(params.ranges) ? params.ranges : []
|
|
161
304
|
const messages = Array.isArray(params.messages) ? params.messages : []
|
|
305
|
+
const workingState = createCompressionWorkingState(state)
|
|
306
|
+
const committedRangeLogs: Array<Record<string, unknown>> = []
|
|
162
307
|
let operationRemovedTokens = 0
|
|
163
308
|
let operationSummaryTokens = 0
|
|
164
309
|
|
|
@@ -170,16 +315,53 @@ export function registerCompressTool(
|
|
|
170
315
|
topic: params.topic,
|
|
171
316
|
ranges: ranges.map((range) => ({ startId: range.startId, endId: range.endId })),
|
|
172
317
|
messages: messages.map((entry) => ({ messageId: entry.messageId, topic: entry.topic })),
|
|
173
|
-
state: summarizeDcpState(state),
|
|
318
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
174
319
|
})
|
|
175
320
|
|
|
321
|
+
const replayBlocks = state.compressionBlocks.filter((block) => block.createdByToolCallId === _toolCallId)
|
|
322
|
+
if (replayBlocks.length > 0) {
|
|
323
|
+
if (replayBlocks.some((block) => block.operationRequestHash !== requestHash)) {
|
|
324
|
+
throw new Error("Compression operation identity conflict: tool call was reused with different or unverifiable parameters")
|
|
325
|
+
}
|
|
326
|
+
const replayBlockIds = replayBlocks.map((block) => block.id)
|
|
327
|
+
const usage = normalizeDcpContextUsage(safeGetContextUsage(ctx))
|
|
328
|
+
const visualDetails: DcpCompressionVisualDetails = {
|
|
329
|
+
blockIds: replayBlockIds,
|
|
330
|
+
topic: params.topic,
|
|
331
|
+
ranges: ranges.length,
|
|
332
|
+
messages: messages.length,
|
|
333
|
+
itemCount: ranges.length + messages.length,
|
|
334
|
+
totalSummaryTokens: replayBlocks.reduce((sum, block) => sum + (block.summaryTokenEstimate ?? 0), 0),
|
|
335
|
+
activeBlocks: state.compressionBlocks.filter((block) => block.active).length,
|
|
336
|
+
totalBlocks: state.compressionBlocks.length,
|
|
337
|
+
prunedTools: state.prunedToolIds.size,
|
|
338
|
+
tokensSaved: replayBlockIds.reduce((sum, id) => sum + (state.compressionTokenSavings.get(id) ?? 0), 0),
|
|
339
|
+
contextTokens: usage?.tokens,
|
|
340
|
+
contextWindow: usage?.contextWindow,
|
|
341
|
+
contextPercent: usage?.percent,
|
|
342
|
+
skippedMessages: 0,
|
|
343
|
+
skippedMessageIssues: [],
|
|
344
|
+
idempotentReplay: true,
|
|
345
|
+
}
|
|
346
|
+
const resultDetails = { ...visualDetails, outputFormat: "json" as const }
|
|
347
|
+
log("compress.idempotent_replay", {
|
|
348
|
+
toolCallId: _toolCallId,
|
|
349
|
+
blockIds: replayBlockIds.map((id) => `b${id}`),
|
|
350
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
351
|
+
})
|
|
352
|
+
return {
|
|
353
|
+
content: [{ type: "text", text: JSON.stringify(resultDetails, null, 2) }],
|
|
354
|
+
details: resultDetails,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
176
358
|
if (ranges.length === 0 && messages.length === 0) {
|
|
177
359
|
throw new Error("compress requires at least one ranges[] or messages[] entry")
|
|
178
360
|
}
|
|
179
361
|
|
|
180
362
|
let rangePlans: ResolvedRangePlan[]
|
|
181
363
|
try {
|
|
182
|
-
|
|
364
|
+
const boundaryPlans: ResolvedRangeBoundaries[] = ranges.map((range) => {
|
|
183
365
|
const { startId, endId, summary } = range
|
|
184
366
|
|
|
185
367
|
// ── Resolve boundary timestamps ──────────────────────────────────
|
|
@@ -188,7 +370,7 @@ export function registerCompressTool(
|
|
|
188
370
|
const startTimestamp = startBoundary.timestamp
|
|
189
371
|
const endTimestamp = endBoundary.timestamp
|
|
190
372
|
|
|
191
|
-
if (
|
|
373
|
+
if (compareCompressionBoundaries(startBoundary, endBoundary, state) > 0) {
|
|
192
374
|
throw new Error(
|
|
193
375
|
`Range start "${startId}" must appear before end "${endId}" in the conversation`,
|
|
194
376
|
)
|
|
@@ -219,19 +401,80 @@ export function registerCompressTool(
|
|
|
219
401
|
}
|
|
220
402
|
})
|
|
221
403
|
|
|
222
|
-
validateNonOverlappingRanges(
|
|
404
|
+
validateNonOverlappingRanges(boundaryPlans, state)
|
|
405
|
+
validateProtocolClosedRanges(boundaryPlans, state)
|
|
406
|
+
rangePlans = boundaryPlans.map((range) => {
|
|
407
|
+
const { partialBlocks } = findCoveredAndPartialBlocks(range.startTimestamp, range.endTimestamp, state, range)
|
|
408
|
+
if (partialBlocks.length > 0) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`Compression range partially overlaps existing block(s): ${partialBlocks.map((block) => `b${block.id}`).join(", ")}. ` +
|
|
411
|
+
`Select the whole block or non-overlapping boundaries.\n${formatCompressionIdDiagnostics(state)}`,
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
const membership = resolveExactRangeMembership(range.startId, range.endId, state)
|
|
415
|
+
if (!membership) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`Manual compression requires exact source membership for ${range.startId}..${range.endId}; refresh the current DCP context and retry.`,
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
return { ...range, ...membership }
|
|
421
|
+
})
|
|
223
422
|
} catch (error) {
|
|
224
423
|
log("compress.resolve_failed", {
|
|
225
424
|
toolCallId: _toolCallId,
|
|
226
425
|
error: error instanceof Error ? error.message : String(error),
|
|
227
|
-
state: summarizeDcpState(state),
|
|
426
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
228
427
|
})
|
|
229
428
|
throw error
|
|
230
429
|
}
|
|
231
430
|
|
|
431
|
+
// Reject a batch that would claim the same provider-visible source more
|
|
432
|
+
// than once before any detached block is created. Invalid message-mode
|
|
433
|
+
// entries retain their established soft-skip behavior below.
|
|
434
|
+
const preflightMessages: ResolvedMessagePlan[] = []
|
|
435
|
+
const preflightSeenMessageIds = new Set<string>()
|
|
436
|
+
for (const entry of messages) {
|
|
437
|
+
const messageId = typeof entry.messageId === "string" ? entry.messageId.trim() : ""
|
|
438
|
+
if (preflightSeenMessageIds.has(messageId)) continue
|
|
439
|
+
preflightSeenMessageIds.add(messageId)
|
|
440
|
+
if (/^b\d+$/i.test(messageId)) continue
|
|
441
|
+
const meta = getMessageMeta(messageId, state)
|
|
442
|
+
const indexEntry = findConversationIndexEntry(state.conversationIndexSnapshot, messageId)
|
|
443
|
+
if (!meta || meta.blockId !== undefined || !Number.isFinite(meta.timestamp) ||
|
|
444
|
+
(effectiveConfig.compress.protectUserMessages && meta.role === "user") ||
|
|
445
|
+
(meta.role === "assistant" && ((meta.toolCallIds?.length ?? 0) > 0 || indexEntry?.signedAssistant)) ||
|
|
446
|
+
messageTouchesIncompleteToolGroup(messageId, state)) continue
|
|
447
|
+
const membership = resolveExactRangeMembership(messageId, messageId, state, true)
|
|
448
|
+
if (!membership) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Manual message compression requires exact source membership for ${messageId}; refresh the current DCP context and retry.`,
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
preflightMessages.push({
|
|
454
|
+
messageId,
|
|
455
|
+
topic: entry.topic ?? params.topic,
|
|
456
|
+
summary: entry.summary,
|
|
457
|
+
timestamp: meta.timestamp,
|
|
458
|
+
stableId: meta.stableId,
|
|
459
|
+
sourceMembers: membership.sourceMembers,
|
|
460
|
+
mutationMembers: membership.mutationMembers,
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
assertDisjointSelections(rangePlans, preflightMessages)
|
|
464
|
+
|
|
232
465
|
for (const range of rangePlans) {
|
|
233
466
|
try {
|
|
234
|
-
const anchor = resolveAnchorBoundary(range.endTimestamp,
|
|
467
|
+
const anchor = resolveAnchorBoundary(range.endTimestamp, workingState, range.endMessageId)
|
|
468
|
+
const preparedProtectedFragments = await prepareCompressionProtectedFragments({
|
|
469
|
+
startTimestamp: range.startTimestamp,
|
|
470
|
+
endTimestamp: range.endTimestamp,
|
|
471
|
+
startMessageId: range.startMessageId,
|
|
472
|
+
endMessageId: range.endMessageId,
|
|
473
|
+
state: workingState,
|
|
474
|
+
config: effectiveConfig,
|
|
475
|
+
mode: "range",
|
|
476
|
+
cwd: (ctx as any).cwd,
|
|
477
|
+
})
|
|
235
478
|
|
|
236
479
|
const created = createRangeCompressionBlock({
|
|
237
480
|
topic: params.topic,
|
|
@@ -243,15 +486,20 @@ export function registerCompressTool(
|
|
|
243
486
|
anchorTimestamp: anchor.timestamp,
|
|
244
487
|
anchorMessageId: anchor.stableId,
|
|
245
488
|
createdByToolCallId: _toolCallId,
|
|
246
|
-
state,
|
|
489
|
+
state: workingState,
|
|
247
490
|
config: effectiveConfig,
|
|
248
491
|
mode: "range",
|
|
492
|
+
version: 2,
|
|
493
|
+
replacementMode: "range",
|
|
494
|
+
preparedProtectedFragments,
|
|
495
|
+
sourceMembers: range.sourceMembers,
|
|
496
|
+
mutationMembers: range.mutationMembers,
|
|
249
497
|
})
|
|
250
498
|
const block = created.block
|
|
251
499
|
newBlockIds.push(block.id)
|
|
252
500
|
operationRemovedTokens += created.removedTokenEstimate
|
|
253
501
|
operationSummaryTokens += created.summaryTokenEstimate
|
|
254
|
-
|
|
502
|
+
committedRangeLogs.push({
|
|
255
503
|
toolCallId: _toolCallId,
|
|
256
504
|
range: { startId: range.startId, endId: range.endId },
|
|
257
505
|
blockId: `b${block.id}`,
|
|
@@ -263,7 +511,7 @@ export function registerCompressTool(
|
|
|
263
511
|
toolCallId: _toolCallId,
|
|
264
512
|
range: { startId: range.startId, endId: range.endId },
|
|
265
513
|
error: error instanceof Error ? error.message : String(error),
|
|
266
|
-
state: summarizeDcpState(state),
|
|
514
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
267
515
|
})
|
|
268
516
|
throw error
|
|
269
517
|
}
|
|
@@ -286,14 +534,14 @@ export function registerCompressTool(
|
|
|
286
534
|
continue
|
|
287
535
|
}
|
|
288
536
|
|
|
289
|
-
const meta = getMessageMeta(messageId,
|
|
537
|
+
const meta = getMessageMeta(messageId, workingState)
|
|
290
538
|
if (!meta) {
|
|
291
539
|
skippedMessageIssues.push({
|
|
292
540
|
kind: "unknown",
|
|
293
541
|
messageId,
|
|
294
542
|
detail:
|
|
295
543
|
"The ID is not present in the current DCP snapshot; it may be stale after compression, pruning, reload, or session switching.\n" +
|
|
296
|
-
formatCompressionIdDiagnostics(
|
|
544
|
+
formatCompressionIdDiagnostics(workingState),
|
|
297
545
|
})
|
|
298
546
|
continue
|
|
299
547
|
}
|
|
@@ -309,11 +557,30 @@ export function registerCompressTool(
|
|
|
309
557
|
skippedMessageIssues.push({ kind: "protected-user", messageId })
|
|
310
558
|
continue
|
|
311
559
|
}
|
|
560
|
+
const indexEntry = findConversationIndexEntry(workingState.conversationIndexSnapshot, messageId)
|
|
561
|
+
if (meta.role === "assistant" && ((meta.toolCallIds?.length ?? 0) > 0 || indexEntry?.signedAssistant)) {
|
|
562
|
+
skippedMessageIssues.push({
|
|
563
|
+
kind: "tool-group",
|
|
564
|
+
messageId,
|
|
565
|
+
detail:
|
|
566
|
+
"Signed assistant content and assistants containing tool calls cannot be edited in place; use ranges[] with a complete protocol-safe group.",
|
|
567
|
+
})
|
|
568
|
+
continue
|
|
569
|
+
}
|
|
570
|
+
if (messageTouchesIncompleteToolGroup(messageId, workingState)) {
|
|
571
|
+
skippedMessageIssues.push({
|
|
572
|
+
kind: "tool-group",
|
|
573
|
+
messageId,
|
|
574
|
+
detail: "The selected message belongs to an incomplete in-flight tool group and is not safe to replace yet.",
|
|
575
|
+
})
|
|
576
|
+
continue
|
|
577
|
+
}
|
|
312
578
|
|
|
313
579
|
const { coveredBlocks, partialBlocks } = findCoveredAndPartialBlocks(
|
|
314
580
|
meta.timestamp,
|
|
315
581
|
meta.timestamp,
|
|
316
|
-
|
|
582
|
+
workingState,
|
|
583
|
+
{ startMessageId: meta.stableId, endMessageId: meta.stableId },
|
|
317
584
|
)
|
|
318
585
|
if (coveredBlocks.length > 0 || partialBlocks.length > 0) {
|
|
319
586
|
const blockList = [...coveredBlocks, ...partialBlocks]
|
|
@@ -323,7 +590,17 @@ export function registerCompressTool(
|
|
|
323
590
|
continue
|
|
324
591
|
}
|
|
325
592
|
|
|
326
|
-
const anchor = resolveAnchorBoundary(meta.timestamp,
|
|
593
|
+
const anchor = resolveAnchorBoundary(meta.timestamp, workingState, meta.stableId)
|
|
594
|
+
const preparedProtectedFragments = await prepareCompressionProtectedFragments({
|
|
595
|
+
startTimestamp: meta.timestamp,
|
|
596
|
+
endTimestamp: meta.timestamp,
|
|
597
|
+
startMessageId: meta.stableId,
|
|
598
|
+
endMessageId: meta.stableId,
|
|
599
|
+
state: workingState,
|
|
600
|
+
config: effectiveConfig,
|
|
601
|
+
mode: "message",
|
|
602
|
+
cwd: (ctx as any).cwd,
|
|
603
|
+
})
|
|
327
604
|
|
|
328
605
|
const created = createRangeCompressionBlock({
|
|
329
606
|
topic: entry.topic ?? params.topic,
|
|
@@ -335,11 +612,16 @@ export function registerCompressTool(
|
|
|
335
612
|
anchorTimestamp: anchor.timestamp,
|
|
336
613
|
anchorMessageId: anchor.stableId,
|
|
337
614
|
createdByToolCallId: _toolCallId,
|
|
338
|
-
state,
|
|
615
|
+
state: workingState,
|
|
339
616
|
config: effectiveConfig,
|
|
340
617
|
mode: "message",
|
|
618
|
+
version: 2,
|
|
619
|
+
replacementMode: "message-body",
|
|
341
620
|
validatePlaceholders: false,
|
|
342
621
|
expandPlaceholders: false,
|
|
622
|
+
preparedProtectedFragments,
|
|
623
|
+
sourceMembers: preflightMessages.find((plan) => plan.messageId === messageId)?.sourceMembers,
|
|
624
|
+
mutationMembers: preflightMessages.find((plan) => plan.messageId === messageId)?.mutationMembers,
|
|
343
625
|
})
|
|
344
626
|
const block = created.block
|
|
345
627
|
newBlockIds.push(block.id)
|
|
@@ -354,15 +636,58 @@ export function registerCompressTool(
|
|
|
354
636
|
)
|
|
355
637
|
}
|
|
356
638
|
|
|
357
|
-
|
|
639
|
+
let projection: ReturnType<typeof previewManualCompressionProjection> | undefined
|
|
640
|
+
let pressureRelieved = false
|
|
641
|
+
let clearedNudgeAnchors = 0
|
|
358
642
|
if (newBlockIds.length > 0) {
|
|
359
|
-
|
|
643
|
+
const newBlocks = workingState.compressionBlocks.filter((block) => newBlockIds.includes(block.id))
|
|
644
|
+
projection = previewManualCompressionProjection(state, effectiveConfig, newBlocks)
|
|
645
|
+
if (projection.netGain <= 0) {
|
|
646
|
+
log("compress.projection_rejected", {
|
|
647
|
+
toolCallId: _toolCallId,
|
|
648
|
+
projectedBeforeTokens: projection.projectedBeforeTokens,
|
|
649
|
+
projectedAfterTokens: projection.projectedAfterTokens,
|
|
650
|
+
netGain: projection.netGain,
|
|
651
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
652
|
+
})
|
|
653
|
+
throw new Error(
|
|
654
|
+
`Manual compression rejected non-positive full-projection gain: ` +
|
|
655
|
+
`${projection.projectedBeforeTokens} before, ${projection.projectedAfterTokens} after, ${projection.netGain} net. ` +
|
|
656
|
+
"No DCP state was published.",
|
|
657
|
+
)
|
|
658
|
+
}
|
|
659
|
+
// This is deliberately after the full projection proof. Partial
|
|
660
|
+
// progress retains its outstanding recovery debt and patience.
|
|
661
|
+
pressureRelieved = settleCompressionProgress(
|
|
662
|
+
workingState,
|
|
663
|
+
projection.netGain,
|
|
664
|
+
projection.projectedAfterTokens,
|
|
665
|
+
)
|
|
666
|
+
// The selected IDs may have been replaced, so one old reminder may be
|
|
667
|
+
// invalidated at this rewrite boundary. The next context pass will
|
|
668
|
+
// render a fresh anchor without erasing the patience counters above.
|
|
669
|
+
clearedNudgeAnchors = clearDcpNudgeAnchors(workingState)
|
|
670
|
+
if (!pressureRelieved) workingState.nudgeCounter = Math.max(1, effectiveConfig.compress.nudgeFrequency)
|
|
671
|
+
for (const block of workingState.compressionBlocks) {
|
|
672
|
+
if (newBlockIds.includes(block.id)) block.operationRequestHash = requestHash
|
|
673
|
+
}
|
|
674
|
+
assertCurrent()
|
|
675
|
+
let published = false
|
|
676
|
+
await persistState(ctx, workingState, {
|
|
677
|
+
beforePublish: assertCurrent,
|
|
678
|
+
onPublished: () => { published = true },
|
|
679
|
+
})
|
|
680
|
+
if (!published) assertCurrent()
|
|
681
|
+
if (state.sessionEpoch === operationEpoch) commitCompressionWorkingState(state, workingState)
|
|
682
|
+
for (const details of committedRangeLogs) log("compress.range_created", details)
|
|
360
683
|
}
|
|
361
|
-
if (clearedNudgeAnchors > 0) {
|
|
684
|
+
if (clearedNudgeAnchors > 0 && state.sessionEpoch === operationEpoch) {
|
|
362
685
|
try {
|
|
363
686
|
pi.appendEntry("dcp-nudge", {
|
|
364
687
|
event: "cleared",
|
|
365
|
-
reason: "compress",
|
|
688
|
+
reason: pressureRelieved ? "compress" : "compress-partial",
|
|
689
|
+
pressureRelieved,
|
|
690
|
+
remainingRecoveryTokens: workingState.compressionProgress?.remainingTokens ?? 0,
|
|
366
691
|
clearedAnchors: clearedNudgeAnchors,
|
|
367
692
|
blockIds: newBlockIds,
|
|
368
693
|
createdAt: Date.now(),
|
|
@@ -372,22 +697,23 @@ export function registerCompressTool(
|
|
|
372
697
|
}
|
|
373
698
|
}
|
|
374
699
|
|
|
375
|
-
if (newBlockIds.length > 0) {
|
|
376
|
-
await saveDcpState(ctx, state)
|
|
377
|
-
}
|
|
378
|
-
|
|
379
700
|
log("compress.success", {
|
|
380
701
|
toolCallId: _toolCallId,
|
|
381
702
|
newBlockIds: newBlockIds.map((id) => `b${id}`),
|
|
382
703
|
skippedMessages: skippedMessageIssues.length,
|
|
383
|
-
|
|
704
|
+
projectedBeforeTokens: projection?.projectedBeforeTokens,
|
|
705
|
+
projectedAfterTokens: projection?.projectedAfterTokens,
|
|
706
|
+
netGain: projection?.netGain,
|
|
707
|
+
pressureRelieved,
|
|
708
|
+
remainingRecoveryTokens: workingState.compressionProgress?.remainingTokens ?? 0,
|
|
709
|
+
state: summarizeDcpState(state, effectiveConfig),
|
|
384
710
|
})
|
|
385
711
|
|
|
386
|
-
const usage = normalizeDcpContextUsage(safeGetContextUsage(ctx))
|
|
387
|
-
const operationTokensSaved = Math.max(0, operationRemovedTokens - operationSummaryTokens)
|
|
712
|
+
const usage = state.sessionEpoch === operationEpoch ? normalizeDcpContextUsage(safeGetContextUsage(ctx)) : undefined
|
|
713
|
+
const operationTokensSaved = Math.max(0, projection?.netGain ?? operationRemovedTokens - operationSummaryTokens)
|
|
388
714
|
const itemCount = ranges.length + messages.length
|
|
389
715
|
const totalSummaryTokens = newBlockIds.reduce((sum, id) => {
|
|
390
|
-
const b =
|
|
716
|
+
const b = workingState.compressionBlocks.find((block) => block.id === id)
|
|
391
717
|
return sum + (b?.summaryTokenEstimate ?? 0)
|
|
392
718
|
}, 0)
|
|
393
719
|
const visualDetails: DcpCompressionVisualDetails = {
|
|
@@ -409,6 +735,13 @@ export function registerCompressTool(
|
|
|
409
735
|
}
|
|
410
736
|
const resultDetails = {
|
|
411
737
|
...visualDetails,
|
|
738
|
+
projectedBeforeTokens: projection?.projectedBeforeTokens,
|
|
739
|
+
projectedAfterTokens: projection?.projectedAfterTokens,
|
|
740
|
+
netGain: projection?.netGain,
|
|
741
|
+
pressureRelieved,
|
|
742
|
+
remainingRecoveryTokens: workingState.compressionProgress?.remainingTokens ?? 0,
|
|
743
|
+
committed: true,
|
|
744
|
+
ownerChangedAfterPublication: state.sessionEpoch !== operationEpoch,
|
|
412
745
|
outputFormat: "json" as const,
|
|
413
746
|
}
|
|
414
747
|
|
|
@@ -422,6 +755,7 @@ export function registerCompressTool(
|
|
|
422
755
|
],
|
|
423
756
|
details: resultDetails,
|
|
424
757
|
}
|
|
758
|
+
})
|
|
425
759
|
},
|
|
426
760
|
})
|
|
427
761
|
}
|