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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { createHash } from "node:crypto"
|
|
6
6
|
import type { DcpNudgeType } from "./pruner-types.js"
|
|
7
|
+
import type { DcpBlockedReason } from "./progress-controller.js"
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A record of a single tool call, keyed by toolCallId in DcpState.toolCalls.
|
|
@@ -15,11 +16,7 @@ export interface ToolRecord {
|
|
|
15
16
|
toolName: string
|
|
16
17
|
/** The arguments passed to the tool (from the corresponding ToolCall) */
|
|
17
18
|
inputArgs: Record<string, unknown>
|
|
18
|
-
/**
|
|
19
|
-
* Deduplication fingerprint: `toolName::sha256:<hash>` where the hash is
|
|
20
|
-
* computed over recursively key-sorted args. Two calls with the same name +
|
|
21
|
-
* identical args share the same fingerprint without persisting full args.
|
|
22
|
-
*/
|
|
19
|
+
/** Stable input fingerprint: `toolName::sha256:<hash>` over key-sorted args. */
|
|
23
20
|
inputFingerprint: string
|
|
24
21
|
/** Whether the tool result was an error */
|
|
25
22
|
isError: boolean
|
|
@@ -43,6 +40,8 @@ export interface MessageIdMeta {
|
|
|
43
40
|
timestamp: number
|
|
44
41
|
/** Stable raw message key when available; falls back to timestamp-derived key. */
|
|
45
42
|
stableId?: string
|
|
43
|
+
/** SHA-256 of canonical provider-visible content at snapshot publication. */
|
|
44
|
+
contentHash?: string
|
|
46
45
|
/** The message role at the time the id was injected. */
|
|
47
46
|
role: string
|
|
48
47
|
/** Present when this addressable message represents an active compression block. */
|
|
@@ -50,6 +49,8 @@ export interface MessageIdMeta {
|
|
|
50
49
|
/** Tool call metadata for tool-result-like messages. */
|
|
51
50
|
toolCallId?: string
|
|
52
51
|
toolName?: string
|
|
52
|
+
/** Tool calls emitted by an assistant message; these messages are structurally coupled to their results. */
|
|
53
|
+
toolCallIds?: string[]
|
|
53
54
|
/** Plain text extracted from the message when the id was injected. */
|
|
54
55
|
text?: string
|
|
55
56
|
/** Rough token estimate for priority/candidate guidance. */
|
|
@@ -58,6 +59,64 @@ export interface MessageIdMeta {
|
|
|
58
59
|
priority?: "low" | "medium" | "high"
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
export interface ConversationIndexEntry {
|
|
63
|
+
/** Position in the current projected branch before DCP metadata carriers. */
|
|
64
|
+
index: number
|
|
65
|
+
/** Stable identity of this projected message. */
|
|
66
|
+
stableId: string
|
|
67
|
+
/** SHA-256 of the canonical provider-visible message content. */
|
|
68
|
+
contentHash: string
|
|
69
|
+
/**
|
|
70
|
+
* Canonical hash of the raw session message before DCP rewrote this
|
|
71
|
+
* projection entry's body (e.g. a tool-output placeholder). Equals
|
|
72
|
+
* `contentHash` for untouched entries. Runtime-only; exact mutation
|
|
73
|
+
* membership binds to this so a later raw pass still materializes the block.
|
|
74
|
+
* Omitted when the raw and projected hashes are identical.
|
|
75
|
+
*/
|
|
76
|
+
rawContentHash?: string
|
|
77
|
+
/** Persistent mNNN identifier when the message is addressable as raw content. */
|
|
78
|
+
visibleId?: string
|
|
79
|
+
/** Message role in the current projection. */
|
|
80
|
+
role: string
|
|
81
|
+
/** Message timestamp when finite. */
|
|
82
|
+
timestamp?: number
|
|
83
|
+
/** Active block represented by this projected placeholder, when any. */
|
|
84
|
+
blockId?: number
|
|
85
|
+
/** Tool result identity for result-like messages. */
|
|
86
|
+
toolCallId?: string
|
|
87
|
+
/** Tool calls emitted by an assistant message. */
|
|
88
|
+
toolCallIds?: string[]
|
|
89
|
+
/** PI-internal entries are not directly addressable but remain part of range closure. */
|
|
90
|
+
passthrough: boolean
|
|
91
|
+
/** Provenance of this projected entry; never inferred from model-visible text. */
|
|
92
|
+
origin: "raw" | "block" | "dcp-control"
|
|
93
|
+
/** Signed assistant content must never be edited in place. */
|
|
94
|
+
signedAssistant: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CompressionProtectedFragment {
|
|
98
|
+
kind: "user" | "prompt" | "tool"
|
|
99
|
+
/** Stable provenance key; not rendered into provider-visible context. */
|
|
100
|
+
origin: string
|
|
101
|
+
/** SHA-256 of the exact protected text. */
|
|
102
|
+
hash: string
|
|
103
|
+
/** Exact text that must survive rollups. */
|
|
104
|
+
text: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* An immutable source/mutation member recorded by modern compression blocks.
|
|
109
|
+
*
|
|
110
|
+
* `stableId` identifies one occurrence in the canonical branch sequence and
|
|
111
|
+
* `hash` binds that identity to its original provider-visible content. The
|
|
112
|
+
* timestamp is deliberately not part of this record: host imports/reloads can
|
|
113
|
+
* normalize timestamps without changing the actual message.
|
|
114
|
+
*/
|
|
115
|
+
export interface CompressionMember {
|
|
116
|
+
stableId: string
|
|
117
|
+
hash: string
|
|
118
|
+
}
|
|
119
|
+
|
|
61
120
|
/**
|
|
62
121
|
* A compression block created by the `compress` tool.
|
|
63
122
|
* Tracks the range of messages that were summarised and where to inject the
|
|
@@ -88,6 +147,8 @@ export interface CompressionBlock {
|
|
|
88
147
|
anchorMessageId?: string
|
|
89
148
|
/** Tool call ID of the compress invocation that created this block. */
|
|
90
149
|
createdByToolCallId?: string
|
|
150
|
+
/** Binds idempotent manual replay to the exact requested parameters. */
|
|
151
|
+
operationRequestHash?: string
|
|
91
152
|
/** Whether this block is still being applied (false = soft-deleted) */
|
|
92
153
|
active: boolean
|
|
93
154
|
/** Token estimate for the summary text itself */
|
|
@@ -98,8 +159,38 @@ export interface CompressionBlock {
|
|
|
98
159
|
coveredBlockIds?: number[]
|
|
99
160
|
/** Whether this block was created from a range or a single raw message. */
|
|
100
161
|
mode?: "range" | "message"
|
|
101
|
-
/**
|
|
102
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Exact projection semantics. New journal sessions only accept version 2
|
|
164
|
+
* blocks, planned before commit without widening their mutation set.
|
|
165
|
+
*/
|
|
166
|
+
version?: 2
|
|
167
|
+
/** Exact v2 materialization behavior. */
|
|
168
|
+
replacementMode?: "range" | "message-body"
|
|
169
|
+
/**
|
|
170
|
+
* Exact provider-projection entries that supplied this summary. For a
|
|
171
|
+
* roll-up this can contain a synthetic block placeholder while
|
|
172
|
+
* `mutationMembers` contains that placeholder's canonical raw provenance.
|
|
173
|
+
*/
|
|
174
|
+
sourceMembers?: CompressionMember[]
|
|
175
|
+
/**
|
|
176
|
+
* Exact canonical raw entries that a v2 application is allowed to replace.
|
|
177
|
+
* The apply path requires every member, in this order and with this hash, to
|
|
178
|
+
* be present contiguously before it mutates the projection.
|
|
179
|
+
*/
|
|
180
|
+
mutationMembers?: CompressionMember[]
|
|
181
|
+
/** E06 provenance for automatically prepared summaries. */
|
|
182
|
+
autoSummaryRepresentation?: "model" | "extractive" | "extractive-fallback"
|
|
183
|
+
/** SHA-256 of the bounded source manifest used by the auto summary. */
|
|
184
|
+
sourceHash?: string
|
|
185
|
+
/** Bounded source-manifest coverage recorded at auto-summary commit. */
|
|
186
|
+
sourceCoverage?: {
|
|
187
|
+
itemCount: number
|
|
188
|
+
truncatedItems: number
|
|
189
|
+
toolCallCount: number
|
|
190
|
+
toolResultCount: number
|
|
191
|
+
}
|
|
192
|
+
/** Exact protected continuity fragments carried across rollups. */
|
|
193
|
+
protectedFragments?: CompressionProtectedFragment[]
|
|
103
194
|
/** Internal reason for automatic soft-deactivation. */
|
|
104
195
|
deactivatedReason?: string
|
|
105
196
|
}
|
|
@@ -139,7 +230,29 @@ export interface DcpLastNudge {
|
|
|
139
230
|
/**
|
|
140
231
|
* Full runtime state for the DCP extension.
|
|
141
232
|
*/
|
|
233
|
+
export interface DcpProgressRecovery {
|
|
234
|
+
blockedReason: DcpBlockedReason
|
|
235
|
+
projectedBeforeTokens: number
|
|
236
|
+
inputCapacityTokens: number
|
|
237
|
+
requiredSavingsTokens: number
|
|
238
|
+
contextWindow: number
|
|
239
|
+
createdAt: number
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Outstanding net savings goal; a small successful block is not necessarily relief. */
|
|
243
|
+
export interface DcpCompressionProgress {
|
|
244
|
+
remainingTokens: number
|
|
245
|
+
projectedTokens: number
|
|
246
|
+
contextWindow: number
|
|
247
|
+
kind: "routine" | "emergency"
|
|
248
|
+
/** Last budget observation; unchanged native usage must not undo partial savings. */
|
|
249
|
+
observedTokens?: number
|
|
250
|
+
targetHeadroomTokens?: number
|
|
251
|
+
}
|
|
252
|
+
|
|
142
253
|
export interface DcpState {
|
|
254
|
+
/** Runtime-only owner epoch. Incremented whenever the active session state is replaced. */
|
|
255
|
+
sessionEpoch: number
|
|
143
256
|
// ── Tool tracking ──────────────────────────────────────────────────────────
|
|
144
257
|
/** toolCallId → ToolRecord, populated when a tool_result event fires */
|
|
145
258
|
toolCalls: Map<string, ToolRecord>
|
|
@@ -147,7 +260,7 @@ export interface DcpState {
|
|
|
147
260
|
prunedToolIds: Set<string>
|
|
148
261
|
/** toolCallId → reason used for human-readable pruning placeholders/stats. */
|
|
149
262
|
prunedToolReasons: Map<string, string>
|
|
150
|
-
/** Tool results
|
|
263
|
+
/** Tool results consumed by at least one successfully completed provider response stream. */
|
|
151
264
|
providerSeenToolIds: Set<string>
|
|
152
265
|
|
|
153
266
|
// ── Compression ────────────────────────────────────────────────────────────
|
|
@@ -168,6 +281,8 @@ export interface DcpState {
|
|
|
168
281
|
messageIdSnapshot: Map<string, number>
|
|
169
282
|
/** Extra metadata for the model-visible DCP message IDs in messageIdSnapshot. */
|
|
170
283
|
messageMetaSnapshot: Map<string, MessageIdMeta>
|
|
284
|
+
/** Ephemeral canonical projection index from the latest context pass. */
|
|
285
|
+
conversationIndexSnapshot: ConversationIndexEntry[]
|
|
171
286
|
/** Stable message identity → persistent model-visible mNNN assignment. */
|
|
172
287
|
messageIdsByStableId: Map<string, string>
|
|
173
288
|
/** Monotonic counter for persistent message IDs. */
|
|
@@ -180,11 +295,6 @@ export interface DcpState {
|
|
|
180
295
|
* context array in the `context` event handler.
|
|
181
296
|
*/
|
|
182
297
|
currentTurn: number
|
|
183
|
-
/** Last user-turn count at which automatic prune discovery ran. */
|
|
184
|
-
lastAutomaticPruneTurn: number
|
|
185
|
-
/** Highest created block ID at the last automatic-prune checkpoint. */
|
|
186
|
-
lastAutomaticPruneBlockId: number
|
|
187
|
-
|
|
188
298
|
// ── Statistics ─────────────────────────────────────────────────────────────
|
|
189
299
|
/** Running total of tokens estimated to have been saved by pruning/compression */
|
|
190
300
|
tokensSaved: number
|
|
@@ -204,9 +314,9 @@ export interface DcpState {
|
|
|
204
314
|
accountedPrunedToolIds: Set<string>
|
|
205
315
|
// ── Mode ───────────────────────────────────────────────────────────────────
|
|
206
316
|
/**
|
|
207
|
-
* When true,
|
|
208
|
-
*
|
|
209
|
-
*
|
|
317
|
+
* When true, DCP will not autonomously create summaries or routine nudges.
|
|
318
|
+
* The explicit compress tool and bounded emergency safety behavior retain
|
|
319
|
+
* their separate contracts.
|
|
210
320
|
*/
|
|
211
321
|
manualMode: boolean
|
|
212
322
|
|
|
@@ -240,12 +350,16 @@ export interface DcpState {
|
|
|
240
350
|
*/
|
|
241
351
|
lastContextWindow?: number
|
|
242
352
|
/**
|
|
243
|
-
* How many
|
|
244
|
-
* without
|
|
245
|
-
*
|
|
246
|
-
* patience cannot be reset by the exact condition it is designed to handle.
|
|
353
|
+
* How many completed, correlated main-provider opportunities contained an
|
|
354
|
+
* emergency DCP reminder without sufficient committed compression/pruning gain.
|
|
355
|
+
* Repeated `context` transforms and retries do not advance this counter.
|
|
247
356
|
*/
|
|
248
357
|
consecutiveIgnoredStrongNudges: number
|
|
358
|
+
/** Completed provider opportunities with any actionable reminder, including failed compress attempts. */
|
|
359
|
+
consecutiveIgnoredNudges: number
|
|
360
|
+
compressionProgress?: DcpCompressionProgress
|
|
361
|
+
/** Last terminal E05 handoff, persisted so restart does not hide why progress stopped. */
|
|
362
|
+
progressRecovery?: DcpProgressRecovery
|
|
249
363
|
}
|
|
250
364
|
|
|
251
365
|
// ---------------------------------------------------------------------------
|
|
@@ -255,6 +369,7 @@ export interface DcpState {
|
|
|
255
369
|
/** Create a fresh, zeroed DcpState instance. */
|
|
256
370
|
export function createState(): DcpState {
|
|
257
371
|
return {
|
|
372
|
+
sessionEpoch: 1,
|
|
258
373
|
toolCalls: new Map(),
|
|
259
374
|
prunedToolIds: new Set(),
|
|
260
375
|
prunedToolReasons: new Map(),
|
|
@@ -263,11 +378,10 @@ export function createState(): DcpState {
|
|
|
263
378
|
nextBlockId: 1,
|
|
264
379
|
messageIdSnapshot: new Map(),
|
|
265
380
|
messageMetaSnapshot: new Map(),
|
|
381
|
+
conversationIndexSnapshot: [],
|
|
266
382
|
messageIdsByStableId: new Map(),
|
|
267
383
|
nextMessageId: 1,
|
|
268
384
|
currentTurn: 0,
|
|
269
|
-
lastAutomaticPruneTurn: -1,
|
|
270
|
-
lastAutomaticPruneBlockId: 0,
|
|
271
385
|
tokensSaved: 0,
|
|
272
386
|
totalPruneCount: 0,
|
|
273
387
|
totalToolCallCount: 0,
|
|
@@ -282,6 +396,9 @@ export function createState(): DcpState {
|
|
|
282
396
|
lastNudge: undefined,
|
|
283
397
|
lastContextWindow: undefined,
|
|
284
398
|
consecutiveIgnoredStrongNudges: 0,
|
|
399
|
+
consecutiveIgnoredNudges: 0,
|
|
400
|
+
compressionProgress: undefined,
|
|
401
|
+
progressRecovery: undefined,
|
|
285
402
|
}
|
|
286
403
|
}
|
|
287
404
|
|
|
@@ -291,6 +408,7 @@ export function createState(): DcpState {
|
|
|
291
408
|
* reset immediately.
|
|
292
409
|
*/
|
|
293
410
|
export function resetState(state: DcpState): void {
|
|
411
|
+
state.sessionEpoch = Math.max(1, Math.floor(state.sessionEpoch || 0) + 1)
|
|
294
412
|
state.toolCalls.clear()
|
|
295
413
|
state.prunedToolIds.clear()
|
|
296
414
|
state.prunedToolReasons.clear()
|
|
@@ -299,11 +417,10 @@ export function resetState(state: DcpState): void {
|
|
|
299
417
|
state.nextBlockId = 1
|
|
300
418
|
state.messageIdSnapshot.clear()
|
|
301
419
|
state.messageMetaSnapshot.clear()
|
|
420
|
+
state.conversationIndexSnapshot = []
|
|
302
421
|
state.messageIdsByStableId.clear()
|
|
303
422
|
state.nextMessageId = 1
|
|
304
423
|
state.currentTurn = 0
|
|
305
|
-
state.lastAutomaticPruneTurn = -1
|
|
306
|
-
state.lastAutomaticPruneBlockId = 0
|
|
307
424
|
state.tokensSaved = 0
|
|
308
425
|
state.totalPruneCount = 0
|
|
309
426
|
state.totalToolCallCount = 0
|
|
@@ -318,546 +435,32 @@ export function resetState(state: DcpState): void {
|
|
|
318
435
|
state.lastNudge = undefined
|
|
319
436
|
state.lastContextWindow = undefined
|
|
320
437
|
state.consecutiveIgnoredStrongNudges = 0
|
|
438
|
+
state.consecutiveIgnoredNudges = 0
|
|
439
|
+
state.compressionProgress = undefined
|
|
440
|
+
state.progressRecovery = undefined
|
|
321
441
|
}
|
|
322
442
|
|
|
323
|
-
|
|
324
|
-
* Merge compression blocks (and their accounting) from a source sidecar into
|
|
325
|
-
* the current state without clobbering other fields. Used at session_start to
|
|
326
|
-
* carry compression state across fork/resume/new into a session whose own
|
|
327
|
-
* sidecar is empty.
|
|
328
|
-
*
|
|
329
|
-
* Returns the number of newly added blocks (blocks whose id already exists are
|
|
330
|
-
* skipped). Accounting is carried over so inherited blocks are neither lost
|
|
331
|
-
* nor double-counted when later folded by a new compression range.
|
|
332
|
-
*/
|
|
333
|
-
export function inheritCompressionBlocks(state: DcpState, data: unknown): number {
|
|
334
|
-
if (!data || typeof data !== "object") return 0
|
|
335
|
-
const saved = data as Partial<SerializedDcpState>
|
|
336
|
-
if (!Array.isArray(saved.compressionBlocks) || saved.compressionBlocks.length === 0) return 0
|
|
337
|
-
|
|
338
|
-
const existingIds = new Set(state.compressionBlocks.map((b) => b.id))
|
|
339
|
-
const validBlocks = saved.compressionBlocks
|
|
340
|
-
.filter(
|
|
341
|
-
(b: any) =>
|
|
342
|
-
b && Number.isFinite(b.startTimestamp) && Number.isFinite(b.endTimestamp),
|
|
343
|
-
)
|
|
344
|
-
.map((b: any) => ({
|
|
345
|
-
...b,
|
|
346
|
-
anchorTimestamp: Number.isFinite(b.anchorTimestamp)
|
|
347
|
-
? b.anchorTimestamp
|
|
348
|
-
: b.endTimestamp + 1,
|
|
349
|
-
})) as CompressionBlock[]
|
|
350
|
-
|
|
351
|
-
const toAdd = validBlocks.filter((b) => !existingIds.has(b.id))
|
|
352
|
-
if (toAdd.length === 0) return 0
|
|
353
|
-
|
|
354
|
-
state.compressionBlocks.push(...toAdd)
|
|
355
|
-
state.nextBlockId =
|
|
356
|
-
Math.max(state.nextBlockId, ...state.compressionBlocks.map((b) => b.id)) + 1
|
|
357
|
-
|
|
358
|
-
if (Array.isArray(saved.accountedCompressionBlockIds)) {
|
|
359
|
-
for (const id of saved.accountedCompressionBlockIds) {
|
|
360
|
-
if (typeof id === "number") state.accountedCompressionBlockIds.add(id)
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
if (Array.isArray(saved.compressionTokenSavings)) {
|
|
364
|
-
for (const [id, val] of saved.compressionTokenSavings) {
|
|
365
|
-
if (
|
|
366
|
-
typeof id === "number" &&
|
|
367
|
-
typeof val === "number" &&
|
|
368
|
-
!state.compressionTokenSavings.has(id)
|
|
369
|
-
) {
|
|
370
|
-
state.compressionTokenSavings.set(id, val)
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
if (typeof saved.tokensSaved === "number" && saved.tokensSaved > state.tokensSaved) {
|
|
375
|
-
state.tokensSaved = saved.tokensSaved
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return toAdd.length
|
|
379
|
-
}
|
|
443
|
+
const COMPRESSION_MEMBER_HASH_RE = /^[a-f0-9]{64}$/i
|
|
380
444
|
|
|
381
|
-
|
|
382
|
-
* Compact tool record for persistence — strips outputText, outputDetails,
|
|
383
|
-
* and truncates/summarises inputArgs to keep serialized state bounded.
|
|
384
|
-
*/
|
|
385
|
-
export interface CompactToolRecord {
|
|
386
|
-
toolCallId: string
|
|
387
|
-
toolName: string
|
|
388
|
-
inputFingerprint: string
|
|
389
|
-
isError: boolean
|
|
390
|
-
turnIndex: number
|
|
391
|
-
timestamp: number
|
|
392
|
-
tokenEstimate: number
|
|
393
|
-
/**
|
|
394
|
-
* Extracted string values from inputArgs that could match file-protection
|
|
395
|
-
* patterns, persisted so `isProtectedByFilePattern` still works after
|
|
396
|
-
* session restore. Capped to avoid bloating state with huge arg values.
|
|
397
|
-
*/
|
|
398
|
-
inputStringValues?: string[]
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Maximum number of recent tool records retained in persisted state.
|
|
403
|
-
* Older records are still kept when referenced by active compression blocks,
|
|
404
|
-
* pruned tool IDs, or accounted prune IDs.
|
|
405
|
-
*/
|
|
406
|
-
export const PERSISTED_TOOL_CALLS_MAX_RECENT = 200
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Maximum length of individual string values extracted from inputArgs
|
|
410
|
-
* for file-pattern matching. Longer values are truncated.
|
|
411
|
-
*/
|
|
412
|
-
const INPUT_STRING_VALUE_MAX_LENGTH = 512
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Maximum number of inputStringValues to keep per tool record.
|
|
416
|
-
*/
|
|
417
|
-
const INPUT_STRING_VALUES_MAX_COUNT = 20
|
|
418
|
-
|
|
419
|
-
export interface SerializedDcpState {
|
|
420
|
-
compressionBlocks: CompressionBlock[]
|
|
421
|
-
nextBlockId: number
|
|
422
|
-
prunedToolIds: string[]
|
|
423
|
-
prunedToolReasons: Array<[string, string]>
|
|
424
|
-
/** Tool result IDs known to have appeared in a provider request. */
|
|
425
|
-
providerSeenToolIds?: string[]
|
|
426
|
-
/** Full tool records — present in legacy snapshots. */
|
|
427
|
-
toolCalls?: ToolRecord[]
|
|
428
|
-
/** Compact tool records — present in new compact snapshots. */
|
|
429
|
-
compactToolCalls?: CompactToolRecord[]
|
|
430
|
-
/**
|
|
431
|
-
* Total number of tool calls observed during the session, including those
|
|
432
|
-
* trimmed from the persisted snapshot. Allows `/dcp stats` to report
|
|
433
|
-
* approximate totals.
|
|
434
|
-
*/
|
|
435
|
-
totalToolCallCount?: number
|
|
436
|
-
tokensSaved: number
|
|
437
|
-
totalPruneCount: number
|
|
438
|
-
accountedCompressionBlockIds: number[]
|
|
439
|
-
compressionTokenSavings: Array<[number, number]>
|
|
440
|
-
accountedPrunedToolIds: string[]
|
|
441
|
-
manualMode: boolean
|
|
442
|
-
nudgeAnchors?: DcpNudgeAnchor[]
|
|
443
|
-
nextNudgeAnchorId?: number
|
|
444
|
-
lastNudge?: DcpLastNudge
|
|
445
|
-
/** Persistent stable-identity → mNNN assignments. */
|
|
446
|
-
messageIdsByStableId?: Array<[string, string]>
|
|
447
|
-
nextMessageId?: number
|
|
448
|
-
/**
|
|
449
|
-
* Persisted since v??. `context` events re-seed currentTurn from raw
|
|
450
|
-
* messages, but keeping it across session restarts gives diagnostics and
|
|
451
|
-
* telemetry a contiguous turn counter instead of resetting to 0.
|
|
452
|
-
*/
|
|
453
|
-
currentTurn?: number
|
|
454
|
-
/** Automatic pruning runs only when one of these checkpoints changes. */
|
|
455
|
-
lastAutomaticPruneTurn?: number
|
|
456
|
-
lastAutomaticPruneBlockId?: number
|
|
457
|
-
/**
|
|
458
|
-
* Persisted since v?.?. Without persistence a pi process restart silently
|
|
459
|
-
* reset the nudge cadence counter to 0, which could suppress the next
|
|
460
|
-
* reminder on a session that was already near a context threshold.
|
|
461
|
-
*/
|
|
462
|
-
nudgeCounter?: number
|
|
463
|
-
/** Persisted since v?.?. Diagnostic turn of the last emitted nudge. */
|
|
464
|
-
lastNudgeTurn?: number
|
|
465
|
-
/**
|
|
466
|
-
* Persisted so a mid-session window downgrade (model switch to a smaller
|
|
467
|
-
* context window) is still detectable after a pi process restart / resume.
|
|
468
|
-
*/
|
|
469
|
-
lastContextWindow?: number
|
|
470
|
-
/**
|
|
471
|
-
* Persisted so the auto-compress fallback's patience counter survives a pi
|
|
472
|
-
* process restart / resume, preventing a stuck "model ignores strong nudges"
|
|
473
|
-
* state from being silently cleared by a reload.
|
|
474
|
-
*/
|
|
475
|
-
consecutiveIgnoredStrongNudges?: number
|
|
476
|
-
/** Hash of the last persisted serialized state, used for dedup. */
|
|
477
|
-
_stateHash?: string
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function isToolRecord(value: unknown): value is ToolRecord {
|
|
445
|
+
function isCompressionMember(value: unknown): value is CompressionMember {
|
|
481
446
|
if (!value || typeof value !== "object") return false
|
|
482
|
-
const
|
|
447
|
+
const member = value as Partial<CompressionMember>
|
|
483
448
|
return (
|
|
484
|
-
typeof
|
|
485
|
-
|
|
486
|
-
typeof
|
|
449
|
+
typeof member.stableId === "string" &&
|
|
450
|
+
member.stableId.length > 0 &&
|
|
451
|
+
typeof member.hash === "string" &&
|
|
452
|
+
COMPRESSION_MEMBER_HASH_RE.test(member.hash)
|
|
487
453
|
)
|
|
488
454
|
}
|
|
489
455
|
|
|
490
|
-
function
|
|
491
|
-
if (!value ||
|
|
492
|
-
|
|
493
|
-
return (
|
|
494
|
-
typeof record.toolCallId === "string" &&
|
|
495
|
-
typeof record.toolName === "string" &&
|
|
496
|
-
typeof record.inputFingerprint === "string"
|
|
497
|
-
)
|
|
456
|
+
function isCompressionMemberList(value: unknown): value is CompressionMember[] {
|
|
457
|
+
if (!Array.isArray(value) || value.length === 0 || !value.every(isCompressionMember)) return false
|
|
458
|
+
return new Set(value.map((member) => member.stableId)).size === value.length
|
|
498
459
|
}
|
|
499
460
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return (
|
|
504
|
-
typeof anchor.id === "number" &&
|
|
505
|
-
typeof anchor.type === "string" &&
|
|
506
|
-
Number.isFinite(anchor.anchorTimestamp) &&
|
|
507
|
-
typeof anchor.anchorRole === "string" &&
|
|
508
|
-
typeof anchor.turnIndex === "number" &&
|
|
509
|
-
typeof anchor.createdAt === "number" &&
|
|
510
|
-
typeof anchor.updatedAt === "number"
|
|
511
|
-
)
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function isLastNudge(value: unknown): value is DcpLastNudge {
|
|
515
|
-
if (!value || typeof value !== "object") return false
|
|
516
|
-
const nudge = value as Partial<DcpLastNudge>
|
|
517
|
-
return (
|
|
518
|
-
typeof nudge.type === "string" &&
|
|
519
|
-
typeof nudge.anchorId === "number" &&
|
|
520
|
-
Number.isFinite(nudge.anchorTimestamp) &&
|
|
521
|
-
typeof nudge.createdAt === "number"
|
|
522
|
-
)
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// ---------------------------------------------------------------------------
|
|
526
|
-
// Compact tool-record helpers
|
|
527
|
-
// ---------------------------------------------------------------------------
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Recursively extract string values from a nested object, matching the
|
|
531
|
-
* logic in `pruner-tools.ts::collectStringValues`. Depth-limited to 6.
|
|
532
|
-
*/
|
|
533
|
-
function extractStringValues(value: unknown, out: string[] = [], depth = 0): string[] {
|
|
534
|
-
if (depth > 6) return out
|
|
535
|
-
if (typeof value === "string") {
|
|
536
|
-
out.push(value)
|
|
537
|
-
return out
|
|
538
|
-
}
|
|
539
|
-
if (Array.isArray(value)) {
|
|
540
|
-
for (const item of value) extractStringValues(item, out, depth + 1)
|
|
541
|
-
return out
|
|
542
|
-
}
|
|
543
|
-
if (value !== null && typeof value === "object") {
|
|
544
|
-
for (const item of Object.values(value as Record<string, unknown>)) {
|
|
545
|
-
extractStringValues(item, out, depth + 1)
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return out
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Produce a compact tool record for persistence: strip outputText,
|
|
553
|
-
* outputDetails, and reduce inputArgs to just extracted string values
|
|
554
|
-
* for file-pattern protection checking.
|
|
555
|
-
*/
|
|
556
|
-
export function compactifyToolRecord(record: ToolRecord): CompactToolRecord {
|
|
557
|
-
const stringValues = extractStringValues(record.inputArgs)
|
|
558
|
-
// Truncate individual values and limit total count
|
|
559
|
-
const cappedValues = stringValues
|
|
560
|
-
.slice(0, INPUT_STRING_VALUES_MAX_COUNT)
|
|
561
|
-
.map((v) => (v.length > INPUT_STRING_VALUE_MAX_LENGTH ? v.slice(0, INPUT_STRING_VALUE_MAX_LENGTH) : v))
|
|
562
|
-
|
|
563
|
-
const compact: CompactToolRecord = {
|
|
564
|
-
toolCallId: record.toolCallId,
|
|
565
|
-
toolName: record.toolName,
|
|
566
|
-
inputFingerprint: record.inputFingerprint,
|
|
567
|
-
isError: record.isError,
|
|
568
|
-
turnIndex: record.turnIndex,
|
|
569
|
-
timestamp: record.timestamp,
|
|
570
|
-
tokenEstimate: record.tokenEstimate,
|
|
571
|
-
}
|
|
572
|
-
if (cappedValues.length > 0) {
|
|
573
|
-
compact.inputStringValues = cappedValues
|
|
574
|
-
}
|
|
575
|
-
return compact
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Determine which tool-call IDs are referenced by active compression blocks
|
|
580
|
-
* (via createdByToolCallId) or by pruned/accounted sets, and therefore must
|
|
581
|
-
* be retained even when trimming old records.
|
|
582
|
-
*/
|
|
583
|
-
function referencedToolCallIds(state: DcpState): Set<string> {
|
|
584
|
-
const refs = new Set<string>()
|
|
585
|
-
// Tool IDs referenced by active compression blocks
|
|
586
|
-
for (const block of state.compressionBlocks) {
|
|
587
|
-
if (block.active && block.createdByToolCallId) {
|
|
588
|
-
refs.add(block.createdByToolCallId)
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
// Pruned tool IDs
|
|
592
|
-
for (const id of state.prunedToolIds) refs.add(id)
|
|
593
|
-
// Accounted pruned tool IDs (superset of prunedToolIds in some cases)
|
|
594
|
-
for (const id of state.accountedPrunedToolIds) refs.add(id)
|
|
595
|
-
return refs
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/** Serialize runtime state into a JSON-safe object for pi.appendEntry(). */
|
|
599
|
-
export function serializeState(state: DcpState): SerializedDcpState {
|
|
600
|
-
// Build compact tool records, keeping referenced + recent ones.
|
|
601
|
-
const allRecords = Array.from(state.toolCalls.values())
|
|
602
|
-
const refs = referencedToolCallIds(state)
|
|
603
|
-
|
|
604
|
-
// Sort by timestamp descending so we can pick the most recent ones
|
|
605
|
-
const sorted = allRecords
|
|
606
|
-
.slice()
|
|
607
|
-
.sort((a, b) => b.timestamp - a.timestamp)
|
|
608
|
-
|
|
609
|
-
const compactToolCalls: CompactToolRecord[] = []
|
|
610
|
-
const seen = new Set<string>()
|
|
611
|
-
|
|
612
|
-
// First pass: always include referenced records
|
|
613
|
-
for (const record of sorted) {
|
|
614
|
-
if (refs.has(record.toolCallId)) {
|
|
615
|
-
compactToolCalls.push(compactifyToolRecord(record))
|
|
616
|
-
seen.add(record.toolCallId)
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Second pass: add recent records up to the limit
|
|
621
|
-
for (const record of sorted) {
|
|
622
|
-
if (seen.has(record.toolCallId)) continue
|
|
623
|
-
if (compactToolCalls.length >= PERSISTED_TOOL_CALLS_MAX_RECENT) break
|
|
624
|
-
compactToolCalls.push(compactifyToolRecord(record))
|
|
625
|
-
seen.add(record.toolCallId)
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const persistedToolCallIds = new Set(compactToolCalls.map((record) => record.toolCallId))
|
|
629
|
-
const providerSeenToolIds = Array.from(state.providerSeenToolIds)
|
|
630
|
-
.filter((toolCallId) => persistedToolCallIds.has(toolCallId))
|
|
631
|
-
|
|
632
|
-
return {
|
|
633
|
-
compressionBlocks: state.compressionBlocks,
|
|
634
|
-
nextBlockId: state.nextBlockId,
|
|
635
|
-
prunedToolIds: Array.from(state.prunedToolIds),
|
|
636
|
-
prunedToolReasons: Array.from(state.prunedToolReasons.entries()),
|
|
637
|
-
providerSeenToolIds,
|
|
638
|
-
compactToolCalls,
|
|
639
|
-
totalToolCallCount: allRecords.length,
|
|
640
|
-
tokensSaved: state.tokensSaved,
|
|
641
|
-
totalPruneCount: state.totalPruneCount,
|
|
642
|
-
accountedCompressionBlockIds: Array.from(state.accountedCompressionBlockIds),
|
|
643
|
-
compressionTokenSavings: Array.from(state.compressionTokenSavings.entries()),
|
|
644
|
-
accountedPrunedToolIds: Array.from(state.accountedPrunedToolIds),
|
|
645
|
-
manualMode: state.manualMode,
|
|
646
|
-
nudgeAnchors: state.nudgeAnchors,
|
|
647
|
-
nextNudgeAnchorId: state.nextNudgeAnchorId,
|
|
648
|
-
lastNudge: state.lastNudge,
|
|
649
|
-
messageIdsByStableId: Array.from(state.messageIdsByStableId.entries()),
|
|
650
|
-
nextMessageId: state.nextMessageId,
|
|
651
|
-
currentTurn: state.currentTurn,
|
|
652
|
-
lastAutomaticPruneTurn: state.lastAutomaticPruneTurn,
|
|
653
|
-
lastAutomaticPruneBlockId: state.lastAutomaticPruneBlockId,
|
|
654
|
-
nudgeCounter: state.nudgeCounter,
|
|
655
|
-
lastNudgeTurn: state.lastNudgeTurn,
|
|
656
|
-
lastContextWindow: state.lastContextWindow,
|
|
657
|
-
consecutiveIgnoredStrongNudges: state.consecutiveIgnoredStrongNudges,
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Restore persisted DCP state into an already-reset state object.
|
|
663
|
-
* Handles older persisted payloads that did not include the newer accounting
|
|
664
|
-
* and tool-call fingerprint fields.
|
|
665
|
-
*/
|
|
666
|
-
export function restoreState(state: DcpState, data: unknown): void {
|
|
667
|
-
if (!data || typeof data !== "object") return
|
|
668
|
-
const saved = data as Partial<SerializedDcpState>
|
|
669
|
-
|
|
670
|
-
if (Array.isArray(saved.compressionBlocks)) {
|
|
671
|
-
const validBlocks = saved.compressionBlocks
|
|
672
|
-
.filter((b: any) => Number.isFinite(b?.startTimestamp) && Number.isFinite(b?.endTimestamp))
|
|
673
|
-
.map((b: any) => ({
|
|
674
|
-
...b,
|
|
675
|
-
anchorTimestamp: Number.isFinite(b.anchorTimestamp)
|
|
676
|
-
? b.anchorTimestamp
|
|
677
|
-
: b.endTimestamp + 1,
|
|
678
|
-
})) as CompressionBlock[]
|
|
679
|
-
state.compressionBlocks = validBlocks
|
|
680
|
-
state.nextBlockId =
|
|
681
|
-
typeof saved.nextBlockId === "number"
|
|
682
|
-
? saved.nextBlockId
|
|
683
|
-
: validBlocks.length > 0
|
|
684
|
-
? Math.max(0, ...validBlocks.map((b) => b.id)) + 1
|
|
685
|
-
: 1
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (Array.isArray(saved.prunedToolIds)) {
|
|
689
|
-
state.prunedToolIds = new Set(saved.prunedToolIds.filter((id): id is string => typeof id === "string"))
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (Array.isArray(saved.prunedToolReasons)) {
|
|
693
|
-
state.prunedToolReasons = new Map(
|
|
694
|
-
saved.prunedToolReasons.filter(
|
|
695
|
-
(entry): entry is [string, string] =>
|
|
696
|
-
Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "string",
|
|
697
|
-
),
|
|
698
|
-
)
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (Array.isArray(saved.providerSeenToolIds)) {
|
|
702
|
-
state.providerSeenToolIds = new Set(
|
|
703
|
-
saved.providerSeenToolIds.filter((id): id is string => typeof id === "string"),
|
|
704
|
-
)
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (Array.isArray(saved.compactToolCalls)) {
|
|
708
|
-
// New compact format: restore CompactToolRecords as ToolRecords with
|
|
709
|
-
// synthetic inputArgs derived from inputStringValues.
|
|
710
|
-
state.toolCalls = new Map(
|
|
711
|
-
saved.compactToolCalls
|
|
712
|
-
.filter(isCompactToolRecord)
|
|
713
|
-
.map((compact) => {
|
|
714
|
-
const record: ToolRecord = {
|
|
715
|
-
toolCallId: compact.toolCallId,
|
|
716
|
-
toolName: compact.toolName,
|
|
717
|
-
// Reconstruct minimal inputArgs from persisted string values
|
|
718
|
-
// so isProtectedByFilePattern still works.
|
|
719
|
-
inputArgs: compact.inputStringValues
|
|
720
|
-
? { _restoredValues: compact.inputStringValues }
|
|
721
|
-
: {},
|
|
722
|
-
inputFingerprint: compact.inputFingerprint,
|
|
723
|
-
isError: compact.isError,
|
|
724
|
-
turnIndex: compact.turnIndex,
|
|
725
|
-
timestamp: compact.timestamp,
|
|
726
|
-
tokenEstimate: compact.tokenEstimate,
|
|
727
|
-
// outputText and outputDetails intentionally not restored —
|
|
728
|
-
// they are only used during live compression block creation.
|
|
729
|
-
}
|
|
730
|
-
return [record.toolCallId, record] as const
|
|
731
|
-
}),
|
|
732
|
-
)
|
|
733
|
-
} else if (Array.isArray(saved.toolCalls)) {
|
|
734
|
-
// Legacy full format: restore as-is for backward compatibility.
|
|
735
|
-
state.toolCalls = new Map(
|
|
736
|
-
saved.toolCalls
|
|
737
|
-
.filter(isToolRecord)
|
|
738
|
-
.map((record) => [record.toolCallId, record] as const),
|
|
739
|
-
)
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
if (typeof saved.tokensSaved === "number") state.tokensSaved = saved.tokensSaved
|
|
743
|
-
if (typeof saved.totalPruneCount === "number") state.totalPruneCount = saved.totalPruneCount
|
|
744
|
-
|
|
745
|
-
// Restore totalToolCallCount from the persisted snapshot, or fall back to
|
|
746
|
-
// the number of restored tool records (which may be a trimmed subset).
|
|
747
|
-
if (typeof saved.totalToolCallCount === "number" && saved.totalToolCallCount >= 0) {
|
|
748
|
-
state.totalToolCallCount = saved.totalToolCallCount
|
|
749
|
-
} else {
|
|
750
|
-
state.totalToolCallCount = state.toolCalls.size
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
if (Array.isArray(saved.accountedCompressionBlockIds)) {
|
|
754
|
-
state.accountedCompressionBlockIds = new Set(
|
|
755
|
-
saved.accountedCompressionBlockIds.filter((id): id is number => typeof id === "number"),
|
|
756
|
-
)
|
|
757
|
-
} else {
|
|
758
|
-
// Old payloads already persisted aggregate stats but not per-block accounting.
|
|
759
|
-
// Treat existing active blocks as accounted to avoid inflated stats after reload.
|
|
760
|
-
state.accountedCompressionBlockIds = new Set(state.compressionBlocks.map((b) => b.id))
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if (Array.isArray(saved.compressionTokenSavings)) {
|
|
764
|
-
state.compressionTokenSavings = new Map(
|
|
765
|
-
saved.compressionTokenSavings.filter(
|
|
766
|
-
(entry): entry is [number, number] =>
|
|
767
|
-
Array.isArray(entry) && typeof entry[0] === "number" && typeof entry[1] === "number",
|
|
768
|
-
),
|
|
769
|
-
)
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (Array.isArray(saved.accountedPrunedToolIds)) {
|
|
773
|
-
state.accountedPrunedToolIds = new Set(
|
|
774
|
-
saved.accountedPrunedToolIds.filter((id): id is string => typeof id === "string"),
|
|
775
|
-
)
|
|
776
|
-
} else {
|
|
777
|
-
// Same migration safety for pre-accounting payloads.
|
|
778
|
-
state.accountedPrunedToolIds = new Set(state.prunedToolIds)
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (typeof saved.manualMode === "boolean") {
|
|
782
|
-
state.manualMode = saved.manualMode
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (Array.isArray(saved.nudgeAnchors)) {
|
|
786
|
-
const seen = new Set<string>()
|
|
787
|
-
state.nudgeAnchors = saved.nudgeAnchors
|
|
788
|
-
.filter(isNudgeAnchor)
|
|
789
|
-
.filter((anchor) => {
|
|
790
|
-
const key = `${anchor.anchorStableId ?? ""}|${anchor.anchorTimestamp}`
|
|
791
|
-
if (seen.has(key)) return false
|
|
792
|
-
seen.add(key)
|
|
793
|
-
return true
|
|
794
|
-
})
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
if (typeof saved.nextNudgeAnchorId === "number" && saved.nextNudgeAnchorId > 0) {
|
|
798
|
-
state.nextNudgeAnchorId = Math.floor(saved.nextNudgeAnchorId)
|
|
799
|
-
} else if (state.nudgeAnchors.length > 0) {
|
|
800
|
-
state.nextNudgeAnchorId = Math.max(...state.nudgeAnchors.map((anchor) => anchor.id)) + 1
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
if (isLastNudge(saved.lastNudge)) {
|
|
804
|
-
state.lastNudge = saved.lastNudge
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
if (Array.isArray(saved.messageIdsByStableId)) {
|
|
808
|
-
const usedIds = new Set<string>()
|
|
809
|
-
const usedStableKeys = new Set<string>()
|
|
810
|
-
state.messageIdsByStableId = new Map(
|
|
811
|
-
saved.messageIdsByStableId.filter(
|
|
812
|
-
(entry): entry is [string, string] => {
|
|
813
|
-
if (!Array.isArray(entry) || typeof entry[0] !== "string" || typeof entry[1] !== "string") return false
|
|
814
|
-
if (!entry[0] || !/^m\d+$/.test(entry[1]) || usedIds.has(entry[1]) || usedStableKeys.has(entry[0])) return false
|
|
815
|
-
usedIds.add(entry[1])
|
|
816
|
-
usedStableKeys.add(entry[0])
|
|
817
|
-
return true
|
|
818
|
-
},
|
|
819
|
-
),
|
|
820
|
-
)
|
|
821
|
-
}
|
|
822
|
-
const highestRestoredMessageId = Math.max(
|
|
823
|
-
0,
|
|
824
|
-
...Array.from(state.messageIdsByStableId.values()).map((id) => Number.parseInt(id.slice(1), 10) || 0),
|
|
825
|
-
)
|
|
826
|
-
if (typeof saved.nextMessageId === "number" && Number.isFinite(saved.nextMessageId) && saved.nextMessageId > 0) {
|
|
827
|
-
state.nextMessageId = Math.max(Math.floor(saved.nextMessageId), highestRestoredMessageId + 1)
|
|
828
|
-
} else {
|
|
829
|
-
state.nextMessageId = highestRestoredMessageId + 1
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// nudgeCounter: clamp to a non-negative integer so a corrupted payload
|
|
833
|
-
// cannot stall reminders by going negative. Default 0 keeps older sessions
|
|
834
|
-
// behaving like a fresh cadence on first post-restart context event.
|
|
835
|
-
if (typeof saved.nudgeCounter === "number" && Number.isFinite(saved.nudgeCounter) && saved.nudgeCounter >= 0) {
|
|
836
|
-
state.nudgeCounter = Math.floor(saved.nudgeCounter)
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// lastNudgeTurn and currentTurn default to createState() values when absent
|
|
840
|
-
// (lastNudgeTurn = -1, currentTurn = 0). currentTurn is re-derived from
|
|
841
|
-
// raw messages on every `context` event, so persistence here is best-effort
|
|
842
|
-
// for telemetry continuity rather than authoritative.
|
|
843
|
-
if (typeof saved.lastNudgeTurn === "number" && Number.isFinite(saved.lastNudgeTurn)) {
|
|
844
|
-
state.lastNudgeTurn = Math.floor(saved.lastNudgeTurn)
|
|
845
|
-
}
|
|
846
|
-
if (typeof saved.currentTurn === "number" && Number.isFinite(saved.currentTurn) && saved.currentTurn >= 0) {
|
|
847
|
-
state.currentTurn = Math.floor(saved.currentTurn)
|
|
848
|
-
}
|
|
849
|
-
if (typeof saved.lastAutomaticPruneTurn === "number" && Number.isFinite(saved.lastAutomaticPruneTurn)) {
|
|
850
|
-
state.lastAutomaticPruneTurn = Math.floor(saved.lastAutomaticPruneTurn)
|
|
851
|
-
}
|
|
852
|
-
if (typeof saved.lastAutomaticPruneBlockId === "number" && Number.isFinite(saved.lastAutomaticPruneBlockId) && saved.lastAutomaticPruneBlockId >= 0) {
|
|
853
|
-
state.lastAutomaticPruneBlockId = Math.floor(saved.lastAutomaticPruneBlockId)
|
|
854
|
-
}
|
|
855
|
-
if (typeof saved.lastContextWindow === "number" && Number.isFinite(saved.lastContextWindow) && saved.lastContextWindow > 0) {
|
|
856
|
-
state.lastContextWindow = saved.lastContextWindow
|
|
857
|
-
}
|
|
858
|
-
if (typeof saved.consecutiveIgnoredStrongNudges === "number" && Number.isFinite(saved.consecutiveIgnoredStrongNudges) && saved.consecutiveIgnoredStrongNudges >= 0) {
|
|
859
|
-
state.consecutiveIgnoredStrongNudges = Math.floor(saved.consecutiveIgnoredStrongNudges)
|
|
860
|
-
}
|
|
461
|
+
/** True only for a complete, unambiguous modern v2 membership record. */
|
|
462
|
+
export function hasExactCompressionMembership(block: Pick<CompressionBlock, "sourceMembers" | "mutationMembers">): boolean {
|
|
463
|
+
return isCompressionMemberList(block.sourceMembers) && isCompressionMemberList(block.mutationMembers)
|
|
861
464
|
}
|
|
862
465
|
|
|
863
466
|
// ---------------------------------------------------------------------------
|
|
@@ -883,14 +486,7 @@ function sortObjectKeys(value: unknown): unknown {
|
|
|
883
486
|
return value
|
|
884
487
|
}
|
|
885
488
|
|
|
886
|
-
/**
|
|
887
|
-
* Create a stable deduplication fingerprint for a tool call.
|
|
888
|
-
*
|
|
889
|
-
* Two calls with the same `toolName` and semantically identical `args`
|
|
890
|
-
* (regardless of key ordering) will produce the same fingerprint.
|
|
891
|
-
*
|
|
892
|
-
* Format: `<toolName>::sha256:<hash of recursively key-sorted args>`
|
|
893
|
-
*/
|
|
489
|
+
/** Create a stable fingerprint for a tool call and canonicalized input args. */
|
|
894
490
|
export function createInputFingerprint(
|
|
895
491
|
toolName: string,
|
|
896
492
|
args: Record<string, unknown>,
|
|
@@ -901,21 +497,3 @@ export function createInputFingerprint(
|
|
|
901
497
|
.digest("hex")
|
|
902
498
|
return `${toolName}::sha256:${hash}`
|
|
903
499
|
}
|
|
904
|
-
|
|
905
|
-
// ---------------------------------------------------------------------------
|
|
906
|
-
// State hashing for save deduplication
|
|
907
|
-
// ---------------------------------------------------------------------------
|
|
908
|
-
|
|
909
|
-
/**
|
|
910
|
-
* Compute a fast hash of serialized state for deduplication.
|
|
911
|
-
* Uses a simple DJB2-like hash over the JSON string. This is not
|
|
912
|
-
* cryptographic — it's only used to avoid writing identical snapshots.
|
|
913
|
-
*/
|
|
914
|
-
export function hashSerializedState(serialized: SerializedDcpState): string {
|
|
915
|
-
const json = JSON.stringify(serialized)
|
|
916
|
-
let hash = 5381
|
|
917
|
-
for (let i = 0; i < json.length; i++) {
|
|
918
|
-
hash = ((hash << 5) + hash + json.charCodeAt(i)) | 0
|
|
919
|
-
}
|
|
920
|
-
return (hash >>> 0).toString(36)
|
|
921
|
-
}
|