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
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
MessageCompressionCandidate,
|
|
7
7
|
NudgeThresholds,
|
|
8
8
|
} from "./pruner-types.js";
|
|
9
|
-
import {
|
|
9
|
+
import { messageText } from "./pruner-metadata.js";
|
|
10
10
|
import { stableMessageKeys } from "./pruner-message-ids.js";
|
|
11
11
|
|
|
12
12
|
function coercePercentThreshold(value: number | string | undefined, fallback: number): number {
|
|
@@ -21,23 +21,6 @@ function coercePercentThreshold(value: number | string | undefined, fallback: nu
|
|
|
21
21
|
return parsed <= 1 ? parsed : fallback;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function injectNudge(messages: any[], nudgeText: string): void {
|
|
25
|
-
messages.push({
|
|
26
|
-
role: "user",
|
|
27
|
-
content: nudgeText,
|
|
28
|
-
timestamp: Date.now(),
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function typePriority(type: DcpNudgeType): number {
|
|
33
|
-
switch (type) {
|
|
34
|
-
case "context-strong": return 4;
|
|
35
|
-
case "context-soft": return 3;
|
|
36
|
-
case "iteration": return 2;
|
|
37
|
-
case "turn": return 1;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
24
|
function nudgeTypeLabel(type: DcpNudgeType): string {
|
|
42
25
|
switch (type) {
|
|
43
26
|
case "context-strong": return "context limit (strong)";
|
|
@@ -50,45 +33,28 @@ function nudgeTypeLabel(type: DcpNudgeType): string {
|
|
|
50
33
|
function isRealAnchorCandidate(msg: any): boolean {
|
|
51
34
|
const role = msg?.role ?? "";
|
|
52
35
|
if (role !== "user" && role !== "assistant") return false;
|
|
53
|
-
|
|
54
|
-
if (extractBlockId(text) !== undefined) return false;
|
|
55
|
-
return true;
|
|
36
|
+
return msg?._dcpOrigin !== "block" && msg?._dcpOrigin !== "dcp-control";
|
|
56
37
|
}
|
|
57
38
|
|
|
58
39
|
function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId: string; timestamp: number; role: string } | null {
|
|
59
40
|
const stableKeys = stableMessageKeys(messages);
|
|
60
|
-
//
|
|
61
|
-
//
|
|
41
|
+
// A reminder may only be introduced on a fresh user tail. If assistant/tool
|
|
42
|
+
// traffic already follows that user message then it has already belonged to
|
|
43
|
+
// an earlier provider prefix; editing it now would invalidate that prefix.
|
|
62
44
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
63
45
|
const msg = messages[i];
|
|
64
|
-
if (msg?.
|
|
65
|
-
if (!Number.isFinite(msg.timestamp))
|
|
66
|
-
return {
|
|
67
|
-
msg,
|
|
68
|
-
index: i,
|
|
69
|
-
stableId: stableKeys[i]!,
|
|
70
|
-
timestamp: msg.timestamp,
|
|
71
|
-
role: msg.role,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Fallback to assistant messages if no raw user message is present.
|
|
76
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
77
|
-
const msg = messages[i];
|
|
78
|
-
if (msg?.role !== "assistant" || !isRealAnchorCandidate(msg)) continue;
|
|
79
|
-
if (!Number.isFinite(msg.timestamp)) continue;
|
|
80
|
-
return {
|
|
81
|
-
msg,
|
|
82
|
-
index: i,
|
|
83
|
-
stableId: stableKeys[i]!,
|
|
84
|
-
timestamp: msg.timestamp,
|
|
85
|
-
role: msg.role,
|
|
86
|
-
};
|
|
46
|
+
if (msg?._dcpOrigin === "dcp-control") continue;
|
|
47
|
+
if (msg?.role !== "user" || !isRealAnchorCandidate(msg) || !Number.isFinite(msg.timestamp)) return null;
|
|
48
|
+
return { msg, index: i, stableId: stableKeys[i]!, timestamp: msg.timestamp, role: msg.role };
|
|
87
49
|
}
|
|
88
50
|
|
|
89
51
|
return null;
|
|
90
52
|
}
|
|
91
53
|
|
|
54
|
+
export function hasCacheSafeNudgeCarrier(messages: any[]): boolean {
|
|
55
|
+
return findAnchorMessage(messages) !== null;
|
|
56
|
+
}
|
|
57
|
+
|
|
92
58
|
function anchorMatchesMessage(anchor: DcpNudgeAnchor, msg: any, stableKey: string): boolean {
|
|
93
59
|
if (anchor.anchorStableId && stableKey === anchor.anchorStableId) return true;
|
|
94
60
|
return msg?.timestamp === anchor.anchorTimestamp;
|
|
@@ -220,41 +186,27 @@ export function upsertNudgeAnchor(
|
|
|
220
186
|
if (!existing || isNewerAnchor(anchor, existing)) existing = anchor;
|
|
221
187
|
}
|
|
222
188
|
|
|
223
|
-
const now = Date.now();
|
|
224
189
|
if (existing) {
|
|
225
|
-
//
|
|
226
|
-
//
|
|
190
|
+
// Once published, both carrier and bytes are immutable. A later urgency
|
|
191
|
+
// increase must not rewrite an old provider prefix.
|
|
227
192
|
state.nudgeAnchors = [existing];
|
|
228
|
-
|
|
229
|
-
if (shouldUpgrade) {
|
|
230
|
-
existing.type = type;
|
|
231
|
-
existing.contextPercent = options.contextPercent ?? existing.contextPercent;
|
|
232
|
-
existing.renderedReminder = options.renderedReminder ?? existing.renderedReminder;
|
|
233
|
-
existing.updatedAt = now;
|
|
234
|
-
} else if (!existing.renderedReminder && options.renderedReminder) {
|
|
235
|
-
// One-time migration for anchors persisted before frozen rendering.
|
|
236
|
-
existing.renderedReminder = options.renderedReminder;
|
|
237
|
-
}
|
|
238
|
-
state.lastNudge = {
|
|
239
|
-
type: existing.type,
|
|
240
|
-
anchorId: existing.id,
|
|
241
|
-
anchorTimestamp: existing.anchorTimestamp,
|
|
242
|
-
anchorStableId: existing.anchorStableId,
|
|
243
|
-
contextPercent: existing.contextPercent,
|
|
244
|
-
createdAt: now,
|
|
245
|
-
};
|
|
246
|
-
return { anchor: existing, created: false, updated: shouldUpgrade };
|
|
193
|
+
return { anchor: existing, created: false, updated: false };
|
|
247
194
|
}
|
|
248
195
|
|
|
249
|
-
|
|
250
|
-
if (!target) {
|
|
251
|
-
// The caller will append one synthetic tail reminder. Drop persisted
|
|
252
|
-
// anchors first so applyAnchoredNudges cannot render a second reminder.
|
|
196
|
+
if (state.nudgeAnchors.length > 0) {
|
|
253
197
|
state.nudgeAnchors = [];
|
|
254
198
|
state.lastNudge = undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const target = findAnchorMessage(messages);
|
|
202
|
+
if (!target) {
|
|
203
|
+
// Mid-turn reminder creation is deliberately deferred rather than
|
|
204
|
+
// synthesizing an ephemeral tail message that would disappear on the next
|
|
205
|
+
// provider continuation and break prefix equality.
|
|
255
206
|
return { anchor: null, created: false, updated: false };
|
|
256
207
|
}
|
|
257
208
|
|
|
209
|
+
const now = Date.now();
|
|
258
210
|
const anchor: DcpNudgeAnchor = {
|
|
259
211
|
id: state.nextNudgeAnchorId++,
|
|
260
212
|
type,
|
|
@@ -301,8 +253,7 @@ export function applyAnchoredNudges(
|
|
|
301
253
|
}
|
|
302
254
|
}
|
|
303
255
|
|
|
304
|
-
//
|
|
305
|
-
// newest valid anchor and discard every stale predecessor.
|
|
256
|
+
// Render only the newest valid anchor and discard stale predecessors.
|
|
306
257
|
state.nudgeAnchors = selected ? [selected.anchor] : [];
|
|
307
258
|
const anchorSetChanged = state.nudgeAnchors.length !== priorAnchors.length ||
|
|
308
259
|
state.nudgeAnchors.some((anchor, index) => anchor !== priorAnchors[index]);
|
|
@@ -312,15 +263,12 @@ export function applyAnchoredNudges(
|
|
|
312
263
|
const reminder = selected.anchor.renderedReminder ?? render(selected.anchor);
|
|
313
264
|
if (materializedReminder) selected.anchor.renderedReminder = reminder;
|
|
314
265
|
const anchorMessage = messages[selected.index];
|
|
315
|
-
if (anchorMessage?.role
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
timestamp: selected.anchor.createdAt,
|
|
320
|
-
});
|
|
321
|
-
} else {
|
|
322
|
-
appendTextToMessage(anchorMessage, reminder);
|
|
266
|
+
if (anchorMessage?.role !== "user") {
|
|
267
|
+
state.nudgeAnchors = [];
|
|
268
|
+
state.lastNudge = undefined;
|
|
269
|
+
return { rendered: false, stateChanged: true };
|
|
323
270
|
}
|
|
271
|
+
appendTextToMessage(anchorMessage, reminder);
|
|
324
272
|
return {
|
|
325
273
|
rendered: true,
|
|
326
274
|
stateChanged: anchorSetChanged || materializedReminder,
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import type { DcpConfig } from "./config.js";
|
|
2
2
|
import type { DcpState, ToolRecord } from "./state.js";
|
|
3
|
-
import {
|
|
3
|
+
import { preserveRawMutationHash } from "./conversation-index.js";
|
|
4
4
|
|
|
5
5
|
export const EMERGENCY_CURRENT_TURN_PLACEHOLDER =
|
|
6
6
|
"[Older tool output removed during current-turn context emergency; re-run the tool if exact content is needed]";
|
|
7
7
|
|
|
8
|
-
// Tool outputs
|
|
9
|
-
//
|
|
10
|
-
|
|
8
|
+
// Tool outputs whose effects may mutate the workspace/process or control DCP.
|
|
9
|
+
// Normalize names so provider/extension casing and aliases cannot bypass the
|
|
10
|
+
// safety policy. Unknown tools are handled conservatively by autonomous
|
|
11
|
+
// supersession below rather than assumed read-only.
|
|
12
|
+
const ALWAYS_PROTECTED_TOOLS = new Set([
|
|
13
|
+
"compress", "write", "edit", "apply_patch", "patch",
|
|
14
|
+
"bash", "shell", "powershell", "exec", "execute",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function normalizeToolName(name: string | undefined): string {
|
|
18
|
+
return (name ?? "").trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
19
|
+
}
|
|
11
20
|
|
|
12
21
|
function buildProtectedTools(config: DcpConfig, extra: string[] = []): Set<string> {
|
|
13
22
|
return new Set([
|
|
14
23
|
...ALWAYS_PROTECTED_TOOLS,
|
|
15
|
-
...(config.compress.protectedTools ?? []),
|
|
16
|
-
...extra,
|
|
24
|
+
...(config.compress.protectedTools ?? []).map(normalizeToolName),
|
|
25
|
+
...extra.map(normalizeToolName),
|
|
17
26
|
]);
|
|
18
27
|
}
|
|
19
28
|
|
|
@@ -82,38 +91,13 @@ function isProtectedByFilePattern(record: ToolRecord | undefined, config: DcpCon
|
|
|
82
91
|
);
|
|
83
92
|
}
|
|
84
93
|
|
|
85
|
-
function recordForToolResult(msg: any, state: DcpState): ToolRecord | undefined {
|
|
86
|
-
if (typeof msg.toolCallId !== "string") return undefined;
|
|
87
|
-
return state.toolCalls.get(msg.toolCallId);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function toolNameForResult(msg: any, record: ToolRecord | undefined): string {
|
|
91
|
-
return record?.toolName ?? msg.toolName ?? "";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function estimateToolResultTokens(msg: any, record: ToolRecord | undefined): number {
|
|
95
|
-
return Math.max(record?.tokenEstimate ?? 0, estimateMessageTokens(msg));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function toolResultIsProtected(
|
|
99
|
-
msg: any,
|
|
100
|
-
record: ToolRecord | undefined,
|
|
101
|
-
config: DcpConfig,
|
|
102
|
-
extraProtectedTools: string[] = [],
|
|
103
|
-
): boolean {
|
|
104
|
-
const protectedTools = buildProtectedTools(config, extraProtectedTools);
|
|
105
|
-
const toolName = toolNameForResult(msg, record);
|
|
106
|
-
if (protectedTools.has(toolName)) return true;
|
|
107
|
-
return isProtectedByFilePattern(record, config);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
94
|
export function isToolRecordProtected(
|
|
111
95
|
record: ToolRecord,
|
|
112
96
|
config: DcpConfig,
|
|
113
97
|
extraProtectedTools: string[] = [],
|
|
114
98
|
): boolean {
|
|
115
99
|
const protectedTools = buildProtectedTools(config, extraProtectedTools);
|
|
116
|
-
if (protectedTools.has(record.toolName)) return true;
|
|
100
|
+
if (protectedTools.has(normalizeToolName(record.toolName))) return true;
|
|
117
101
|
return isProtectedByFilePattern(record, config);
|
|
118
102
|
}
|
|
119
103
|
|
|
@@ -138,127 +122,32 @@ export function markToolPruned(
|
|
|
138
122
|
return !wasAlreadyPruned;
|
|
139
123
|
}
|
|
140
124
|
|
|
141
|
-
export function applyDeduplication(messages: any[], state: DcpState, config: DcpConfig): void {
|
|
142
|
-
if (!config.strategies.deduplication.enabled) return;
|
|
143
|
-
if (state.manualMode && !config.manualMode.automaticStrategies) return;
|
|
144
|
-
|
|
145
|
-
// fingerprint → array of toolCallIds in timestamp order
|
|
146
|
-
const fingerprintMap = new Map<string, Array<{ id: string; tokens: number }>>();
|
|
147
|
-
|
|
148
|
-
for (const msg of messages) {
|
|
149
|
-
if (msg.role !== "toolResult") continue;
|
|
150
|
-
const record = recordForToolResult(msg, state);
|
|
151
|
-
if (!record) continue;
|
|
152
|
-
if (toolResultIsProtected(msg, record, config, config.strategies.deduplication.protectedTools ?? [])) continue;
|
|
153
|
-
|
|
154
|
-
const fp = record.inputFingerprint;
|
|
155
|
-
if (!fingerprintMap.has(fp)) {
|
|
156
|
-
fingerprintMap.set(fp, []);
|
|
157
|
-
}
|
|
158
|
-
fingerprintMap.get(fp)!.push({ id: msg.toolCallId, tokens: estimateToolResultTokens(msg, record) });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// For each fingerprint with duplicates, prune all but the last
|
|
162
|
-
for (const [, ids] of fingerprintMap) {
|
|
163
|
-
if (ids.length <= 1) continue;
|
|
164
|
-
// Keep the last one; prune the rest
|
|
165
|
-
for (let i = 0; i < ids.length - 1; i++) {
|
|
166
|
-
markToolPruned(state, ids[i]!.id, "duplicate", ids[i]!.tokens);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Apply error purging: mark old error tool outputs for pruning.
|
|
173
|
-
* Mutates state.prunedToolIds.
|
|
174
|
-
*/
|
|
175
|
-
export function applyErrorPurging(messages: any[], state: DcpState, config: DcpConfig): void {
|
|
176
|
-
if (!config.strategies.purgeErrors.enabled) return;
|
|
177
|
-
if (state.manualMode && !config.manualMode.automaticStrategies) return;
|
|
178
|
-
|
|
179
|
-
const turnsThreshold = config.strategies.purgeErrors.turns ?? 3;
|
|
180
|
-
|
|
181
|
-
for (const msg of messages) {
|
|
182
|
-
if (msg.role !== "toolResult") continue;
|
|
183
|
-
if (!msg.isError) continue;
|
|
184
|
-
|
|
185
|
-
const record = recordForToolResult(msg, state);
|
|
186
|
-
if (!record) continue;
|
|
187
|
-
if (toolResultIsProtected(msg, record, config, config.strategies.purgeErrors.protectedTools ?? [])) continue;
|
|
188
|
-
|
|
189
|
-
if (state.currentTurn - record.turnIndex >= turnsThreshold) {
|
|
190
|
-
markToolPruned(state, msg.toolCallId, "old-error", estimateToolResultTokens(msg, record));
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Policy-based autonomous tool-output pruning. This is the non-LLM half of
|
|
197
|
-
* DCP: large, old, repeated, or stale discovery outputs can be replaced with
|
|
198
|
-
* placeholders without waiting for the model to call `compress`.
|
|
199
|
-
*/
|
|
200
|
-
export function applyAutoToolOutputPruning(messages: any[], state: DcpState, config: DcpConfig): void {
|
|
201
|
-
const strategy = config.strategies.autoToolPruning;
|
|
202
|
-
if (!strategy.enabled) return;
|
|
203
|
-
if (state.manualMode && !config.manualMode.automaticStrategies) return;
|
|
204
|
-
|
|
205
|
-
const maxOutputTokens = Math.max(1, strategy.maxOutputTokens ?? 2000);
|
|
206
|
-
const keepRecentTurns = Math.max(0, strategy.keepRecentTurns ?? 2);
|
|
207
|
-
const readLikeTurns = Math.max(0, strategy.readLikeTurns ?? 3);
|
|
208
|
-
const readLikeTools = new Set(strategy.readLikeTools ?? []);
|
|
209
|
-
|
|
210
|
-
for (const msg of messages) {
|
|
211
|
-
if (msg.role !== "toolResult") continue;
|
|
212
|
-
if (typeof msg.toolCallId !== "string") continue;
|
|
213
|
-
|
|
214
|
-
const record = recordForToolResult(msg, state);
|
|
215
|
-
if (!record) continue;
|
|
216
|
-
if (toolResultIsProtected(msg, record, config, strategy.protectedTools ?? [])) continue;
|
|
217
|
-
|
|
218
|
-
const ageTurns = Math.max(0, state.currentTurn - record.turnIndex);
|
|
219
|
-
const tokenEstimate = estimateToolResultTokens(msg, record);
|
|
220
|
-
const toolName = toolNameForResult(msg, record);
|
|
221
|
-
|
|
222
|
-
if (tokenEstimate > maxOutputTokens && ageTurns >= keepRecentTurns) {
|
|
223
|
-
markToolPruned(state, msg.toolCallId, "large-output", tokenEstimate);
|
|
224
|
-
} else if (readLikeTools.has(toolName) && ageTurns >= readLikeTurns) {
|
|
225
|
-
markToolPruned(state, msg.toolCallId, "stale-read", tokenEstimate);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
125
|
function placeholderForPrunedTool(msg: any, state: DcpState): string {
|
|
231
126
|
const reason = state.prunedToolReasons.get(msg.toolCallId);
|
|
232
|
-
if (reason === "duplicate") {
|
|
233
|
-
return "[Output removed to save context - duplicate tool call; latest matching result kept]";
|
|
234
|
-
}
|
|
235
|
-
if (reason === "large-output") {
|
|
236
|
-
return "[Large tool output removed to save context after it aged out of the active working set]";
|
|
237
|
-
}
|
|
238
|
-
if (reason === "stale-read") {
|
|
239
|
-
return "[Stale read/search output removed to save context; re-read if exact content is needed again]";
|
|
240
|
-
}
|
|
241
127
|
if (reason === "manual-sweep") {
|
|
242
128
|
return "[Output removed by /dcp sweep to save context]";
|
|
243
129
|
}
|
|
244
130
|
if (reason === "emergency-current-turn") {
|
|
245
131
|
return EMERGENCY_CURRENT_TURN_PLACEHOLDER;
|
|
246
132
|
}
|
|
247
|
-
if (reason === "old-error" || msg.isError) {
|
|
248
|
-
return "[Error output removed - tool failed more than the configured number of turns ago]";
|
|
249
|
-
}
|
|
250
133
|
return "[Output removed to save context - information superseded or no longer needed]";
|
|
251
134
|
}
|
|
252
135
|
|
|
253
136
|
/**
|
|
254
137
|
* Apply explicit tool output pruning from state.prunedToolIds.
|
|
255
138
|
* Replaces content of matching toolResult messages in place.
|
|
139
|
+
*
|
|
140
|
+
* The canonical hash of the un-pruned raw message is preserved on the
|
|
141
|
+
* projection entry (runtime-only) before the body is replaced: exact mutation
|
|
142
|
+
* membership for compression blocks must bind to the raw content a later
|
|
143
|
+
* materialization pass will actually see, not to this placeholder.
|
|
256
144
|
*/
|
|
257
145
|
export function applyToolOutputPruning(messages: any[], state: DcpState): void {
|
|
258
146
|
for (const msg of messages) {
|
|
259
147
|
if (msg.role !== "toolResult") continue;
|
|
260
148
|
if (!state.prunedToolIds.has(msg.toolCallId)) continue;
|
|
261
149
|
|
|
150
|
+
preserveRawMutationHash(msg);
|
|
262
151
|
msg.content = [
|
|
263
152
|
{
|
|
264
153
|
type: "text",
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import type { DcpConfig } from "./config.js";
|
|
2
2
|
import type { DcpState } from "./state.js";
|
|
3
|
-
import { applyCompressionBlocks,
|
|
3
|
+
import { applyCompressionBlocks, syncCompressionBlocks } from "./pruner-compression-blocks.js";
|
|
4
4
|
import { stripStaleDcpMetadataFromMessage } from "./pruner-metadata.js";
|
|
5
5
|
import { injectMessageIds } from "./pruner-message-ids.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
applyDeduplication,
|
|
9
|
-
applyErrorPurging,
|
|
10
|
-
applyToolOutputPruning,
|
|
11
|
-
} from "./pruner-tools.js";
|
|
6
|
+
import { copyRawMutationHash } from "./conversation-index.js";
|
|
7
|
+
import { applyToolOutputPruning } from "./pruner-tools.js";
|
|
12
8
|
|
|
13
9
|
export type {
|
|
14
10
|
CompressionCandidate,
|
|
@@ -35,6 +31,7 @@ export {
|
|
|
35
31
|
} from "./pruner-tools.js";
|
|
36
32
|
export {
|
|
37
33
|
detectCompressionCandidate,
|
|
34
|
+
detectEmergencyCompressionCandidate,
|
|
38
35
|
detectMessageCompressionCandidates,
|
|
39
36
|
formatCompressionCandidateHint,
|
|
40
37
|
formatMessageCompressionCandidateHint,
|
|
@@ -44,7 +41,7 @@ export {
|
|
|
44
41
|
applyAnchoredNudges,
|
|
45
42
|
clearDcpNudgeAnchors,
|
|
46
43
|
getNudgeType,
|
|
47
|
-
|
|
44
|
+
hasCacheSafeNudgeCarrier,
|
|
48
45
|
nudgeTypeLabel,
|
|
49
46
|
upsertNudgeAnchor,
|
|
50
47
|
} from "./pruner-nudge.js";
|
|
@@ -63,7 +60,13 @@ export function applyPruning(
|
|
|
63
60
|
typeof contentBlock === "object" && contentBlock !== null ? { ...contentBlock } : contentBlock,
|
|
64
61
|
);
|
|
65
62
|
}
|
|
66
|
-
|
|
63
|
+
const stripped = stripStaleDcpMetadataFromMessage(clone);
|
|
64
|
+
for (const key of ["_dcpOrigin", "_dcpBlockId", "_dcpStableId"] as const) {
|
|
65
|
+
const descriptor = Object.getOwnPropertyDescriptor(m, key);
|
|
66
|
+
if (descriptor) Object.defineProperty(stripped, key, descriptor);
|
|
67
|
+
}
|
|
68
|
+
copyRawMutationHash(m, stripped);
|
|
69
|
+
return stripped;
|
|
67
70
|
});
|
|
68
71
|
|
|
69
72
|
// 1. Count user turns → update state.currentTurn. Do this before inserting
|
|
@@ -75,32 +78,14 @@ export function applyPruning(
|
|
|
75
78
|
syncCompressionBlocks(msgs, state, config);
|
|
76
79
|
applyCompressionBlocks(msgs, state);
|
|
77
80
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
// Rewriting an old result after every same-turn duplicate breaks provider
|
|
84
|
-
// continuation repeatedly. A new user turn or compression block already
|
|
85
|
-
// establishes a natural history boundary where one prefix rebuild is
|
|
86
|
-
// acceptable. Branches that move the turn count backwards also reopen the
|
|
87
|
-
// checkpoint instead of suppressing pruning indefinitely.
|
|
88
|
-
const newestBlockId = Math.max(0, state.nextBlockId - 1);
|
|
89
|
-
const automaticPruneCheckpoint =
|
|
90
|
-
state.currentTurn !== state.lastAutomaticPruneTurn ||
|
|
91
|
-
newestBlockId !== state.lastAutomaticPruneBlockId;
|
|
92
|
-
if (automaticPruneCheckpoint) {
|
|
93
|
-
applyDeduplication(msgs, state, config);
|
|
94
|
-
applyErrorPurging(msgs, state, config);
|
|
95
|
-
applyAutoToolOutputPruning(msgs, state, config);
|
|
96
|
-
state.lastAutomaticPruneTurn = state.currentTurn;
|
|
97
|
-
state.lastAutomaticPruneBlockId = newestBlockId;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 6. Apply explicit tool output pruning (prunedToolIds)
|
|
81
|
+
// Existing explicit/emergency pruning decisions are replayed, but routine
|
|
82
|
+
// context construction never discovers new retroactive deletions. A new user
|
|
83
|
+
// turn must remain append-only for provider prefix caching; destructive
|
|
84
|
+
// rewrites happen only through an explicit compress/sweep boundary or the
|
|
85
|
+
// bounded emergency path.
|
|
101
86
|
applyToolOutputPruning(msgs, state);
|
|
102
87
|
|
|
103
|
-
//
|
|
88
|
+
// Refresh message ID snapshots and append stable distributed metadata to
|
|
104
89
|
// user/tool-result carriers. Assistant items remain byte-stable.
|
|
105
90
|
injectMessageIds(msgs, state, { config });
|
|
106
91
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createInputFingerprint, type DcpState, type ToolRecord } from "./state.js"
|
|
2
|
+
import { estimateMessageTokens, messageText } from "./pruner-metadata.js"
|
|
3
|
+
|
|
4
|
+
export interface DcpToolRecordRehydration {
|
|
5
|
+
recordsUpdated: number
|
|
6
|
+
exactArgsRestored: number
|
|
7
|
+
exactOutputsRestored: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface RawToolCall {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
input: Record<string, unknown>
|
|
14
|
+
turnIndex: number
|
|
15
|
+
timestamp: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assistantToolCalls(message: any, turnIndex: number): RawToolCall[] {
|
|
19
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) return []
|
|
20
|
+
const calls: RawToolCall[] = []
|
|
21
|
+
for (const part of message.content) {
|
|
22
|
+
if (!part || part.type !== "toolCall" || typeof part.id !== "string") continue
|
|
23
|
+
const name = typeof part.name === "string"
|
|
24
|
+
? part.name
|
|
25
|
+
: typeof part.function?.name === "string"
|
|
26
|
+
? part.function.name
|
|
27
|
+
: ""
|
|
28
|
+
const rawInput = part.input ?? part.arguments ?? part.function?.arguments
|
|
29
|
+
let input: Record<string, unknown> = {}
|
|
30
|
+
if (rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)) {
|
|
31
|
+
input = rawInput as Record<string, unknown>
|
|
32
|
+
} else if (typeof rawInput === "string") {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(rawInput)
|
|
35
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) input = parsed
|
|
36
|
+
} catch {
|
|
37
|
+
// Keep opaque/non-JSON provider arguments out of structured recovery.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
calls.push({
|
|
41
|
+
id: part.id,
|
|
42
|
+
name,
|
|
43
|
+
input,
|
|
44
|
+
turnIndex,
|
|
45
|
+
timestamp: Number.isFinite(message.timestamp) ? message.timestamp : 0,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
return calls
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasRestoredOnlyArgs(record: ToolRecord): boolean {
|
|
52
|
+
const keys = Object.keys(record.inputArgs ?? {})
|
|
53
|
+
return keys.length === 0 || (keys.length === 1 && keys[0] === "_restoredValues")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Rebuild exact, ephemeral tool metadata from the raw session branch.
|
|
58
|
+
*
|
|
59
|
+
* The journal intentionally persists only projection decisions, not tool
|
|
60
|
+
* outputs or delivery evidence. After restart this function reconstructs the
|
|
61
|
+
* tool records needed for protection/fingerprinting from raw history while
|
|
62
|
+
* leaving `providerSeenToolIds` untouched. Absence of provider evidence after
|
|
63
|
+
* restart therefore remains "unknown" and cannot authorize emergency pruning.
|
|
64
|
+
*/
|
|
65
|
+
export function rehydrateToolRecordsFromMessages(
|
|
66
|
+
messages: any[],
|
|
67
|
+
state: DcpState,
|
|
68
|
+
): DcpToolRecordRehydration {
|
|
69
|
+
const calls = new Map<string, RawToolCall>()
|
|
70
|
+
let turnIndex = 0
|
|
71
|
+
for (const message of messages) {
|
|
72
|
+
if (message?.role === "user") turnIndex++
|
|
73
|
+
for (const call of assistantToolCalls(message, turnIndex)) calls.set(call.id, call)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let recordsUpdated = 0
|
|
77
|
+
let exactArgsRestored = 0
|
|
78
|
+
let exactOutputsRestored = 0
|
|
79
|
+
const touched = new Set<string>()
|
|
80
|
+
|
|
81
|
+
for (const message of messages) {
|
|
82
|
+
if (message?.role !== "toolResult" || typeof message.toolCallId !== "string") continue
|
|
83
|
+
const call = calls.get(message.toolCallId)
|
|
84
|
+
const text = messageText(message).trim()
|
|
85
|
+
const toolName = call?.name || (typeof message.toolName === "string" ? message.toolName : "")
|
|
86
|
+
let record = state.toolCalls.get(message.toolCallId)
|
|
87
|
+
if (!record) {
|
|
88
|
+
const inputArgs = call?.input ?? {}
|
|
89
|
+
record = {
|
|
90
|
+
toolCallId: message.toolCallId,
|
|
91
|
+
toolName,
|
|
92
|
+
inputArgs,
|
|
93
|
+
inputFingerprint: createInputFingerprint(toolName, inputArgs),
|
|
94
|
+
isError: message.isError === true,
|
|
95
|
+
turnIndex: call?.turnIndex ?? turnIndex,
|
|
96
|
+
timestamp: Number.isFinite(message.timestamp) ? message.timestamp : call?.timestamp ?? 0,
|
|
97
|
+
tokenEstimate: estimateMessageTokens(message),
|
|
98
|
+
...(text ? { outputText: text } : {}),
|
|
99
|
+
...(message.details !== undefined ? { outputDetails: message.details } : {}),
|
|
100
|
+
}
|
|
101
|
+
state.toolCalls.set(message.toolCallId, record)
|
|
102
|
+
if (Object.keys(inputArgs).length > 0) exactArgsRestored++
|
|
103
|
+
if (text) exactOutputsRestored++
|
|
104
|
+
touched.add(message.toolCallId)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
if (call && hasRestoredOnlyArgs(record) && Object.keys(call.input).length > 0) {
|
|
108
|
+
record.inputArgs = call.input
|
|
109
|
+
if (call.name) record.toolName = call.name
|
|
110
|
+
record.inputFingerprint = createInputFingerprint(record.toolName, call.input)
|
|
111
|
+
record.turnIndex = call.turnIndex
|
|
112
|
+
exactArgsRestored++
|
|
113
|
+
touched.add(message.toolCallId)
|
|
114
|
+
}
|
|
115
|
+
if (!record.outputText && text) {
|
|
116
|
+
record.outputText = text
|
|
117
|
+
exactOutputsRestored++
|
|
118
|
+
touched.add(message.toolCallId)
|
|
119
|
+
}
|
|
120
|
+
if (typeof message.toolName === "string" && message.toolName) record.toolName = message.toolName
|
|
121
|
+
if (typeof message.isError === "boolean") record.isError = message.isError
|
|
122
|
+
if (Number.isFinite(message.timestamp)) record.timestamp = message.timestamp
|
|
123
|
+
record.tokenEstimate = Math.max(record.tokenEstimate ?? 0, estimateMessageTokens(message))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
recordsUpdated = touched.size
|
|
127
|
+
state.totalToolCallCount = Math.max(state.totalToolCallCount, state.toolCalls.size)
|
|
128
|
+
return { recordsUpdated, exactArgsRestored, exactOutputsRestored }
|
|
129
|
+
}
|