pi-ui-extend 1.0.37 → 1.0.39
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 -0
- package/dist/app/extensions/extension-ui-controller.js +3 -0
- package/dist/app/rendering/extension-entry-renderer.js +2 -0
- package/dist/bundled-extensions/question/contract.d.ts +3 -0
- package/dist/bundled-extensions/question/contract.js +24 -0
- package/dist/bundled-extensions/question/desktop.d.ts +11 -0
- package/dist/bundled-extensions/question/desktop.js +143 -0
- package/dist/bundled-extensions/question/index.d.ts +1 -0
- package/dist/bundled-extensions/question/index.js +5 -1
- package/dist/bundled-extensions/question/render.js +6 -0
- package/dist/bundled-extensions/question/result.js +71 -9
- package/dist/bundled-extensions/question/tool-description.js +4 -3
- package/dist/bundled-extensions/question/tui.js +127 -12
- package/dist/bundled-extensions/question/types.d.ts +23 -2
- package/dist/tool-renderers/question.js +20 -1
- package/docs/desktop-markdown-media.md +77 -0
- package/docs/desktop-mvp.md +134 -0
- package/docs/desktop-task-manager.md +124 -0
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +9 -1
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +41 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +13 -1
- package/external/pi-tools-suite/src/async-subagents/index.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/SKILL.md +65 -22
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +23 -8
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/scripts/browser-qa-runner.mjs +180 -41
- package/external/pi-tools-suite/src/async-subagents/subagent-overlay.ts +1 -1
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +1 -1
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -51
- package/external/pi-tools-suite/src/dcp/debug-log.ts +6 -0
- package/external/pi-tools-suite/src/dcp/index.ts +27 -125
- package/external/pi-tools-suite/src/dcp/prompts.ts +2 -2
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +2 -1
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +31 -10
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +6 -7
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +153 -111
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +28 -17
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +61 -27
- package/external/pi-tools-suite/src/dcp/pruner.ts +21 -13
- package/external/pi-tools-suite/src/dcp/state.ts +59 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +4 -0
- package/external/pi-tools-suite/src/lib/rpc-session-state.ts +34 -0
- package/external/pi-tools-suite/src/todo/todo.ts +8 -3
- package/package.json +10 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import type { DcpState, MessageIdMeta } from "./state.js";
|
|
2
3
|
import type { DcpConfig } from "./config.js";
|
|
3
4
|
import {
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
estimateMessageTokens,
|
|
7
8
|
extractBlockId,
|
|
8
9
|
messageText,
|
|
10
|
+
stripStaleDcpMetadataLines,
|
|
9
11
|
} from "./pruner-metadata.js";
|
|
10
12
|
|
|
11
13
|
export interface InjectMessageIdsOptions {
|
|
@@ -13,6 +15,39 @@ export interface InjectMessageIdsOptions {
|
|
|
13
15
|
config?: DcpConfig;
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
const ID_CARRIER_ROLES = new Set(["user", "toolResult", "bashExecution"]);
|
|
19
|
+
|
|
20
|
+
function canonicalContentForStableId(msg: any): unknown {
|
|
21
|
+
if (!Array.isArray(msg?.content)) {
|
|
22
|
+
if (msg?.role === "assistant") return msg?.content ?? "";
|
|
23
|
+
return stripStaleDcpMetadataLines(typeof msg?.content === "string" ? msg.content : "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (msg?.role === "assistant") return msg.content;
|
|
27
|
+
|
|
28
|
+
return msg.content.flatMap((block: any) => {
|
|
29
|
+
if (!block || typeof block !== "object") return [block];
|
|
30
|
+
if (typeof block.text === "string") {
|
|
31
|
+
const text = stripStaleDcpMetadataLines(block.text);
|
|
32
|
+
return text.length > 0 ? [{ ...block, text }] : [];
|
|
33
|
+
}
|
|
34
|
+
if (typeof block.thinking === "string") {
|
|
35
|
+
const thinking = stripStaleDcpMetadataLines(block.thinking);
|
|
36
|
+
return thinking.length > 0 ? [{ ...block, thinking }] : [];
|
|
37
|
+
}
|
|
38
|
+
return [block];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fallbackContentFingerprint(msg: any): string {
|
|
43
|
+
return createHash("sha256")
|
|
44
|
+
.update(String(msg?.role ?? ""))
|
|
45
|
+
.update("\u0000")
|
|
46
|
+
.update(JSON.stringify(canonicalContentForStableId(msg)))
|
|
47
|
+
.digest("hex")
|
|
48
|
+
.slice(0, 20);
|
|
49
|
+
}
|
|
50
|
+
|
|
16
51
|
export function stableMessageId(msg: any, fallbackIndex = 0): string {
|
|
17
52
|
const candidates = [
|
|
18
53
|
msg?.id,
|
|
@@ -32,10 +67,29 @@ export function stableMessageId(msg: any, fallbackIndex = 0): string {
|
|
|
32
67
|
return `tool:${msg.toolCallId.trim()}`;
|
|
33
68
|
}
|
|
34
69
|
|
|
35
|
-
|
|
70
|
+
const blockId = extractBlockId(messageText(msg));
|
|
71
|
+
if (blockId !== undefined) return `block:${blockId}`;
|
|
72
|
+
if (Number.isFinite(msg?.timestamp)) {
|
|
73
|
+
return `ts:${msg.timestamp}:${fallbackContentFingerprint(msg)}`;
|
|
74
|
+
}
|
|
36
75
|
return `idx:${fallbackIndex}`;
|
|
37
76
|
}
|
|
38
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Return deterministic per-occurrence identities for a message sequence.
|
|
80
|
+
* Session entry IDs and content-fingerprinted fallbacks are normally unique;
|
|
81
|
+
* the suffix keeps byte-identical legacy messages from sharing one mNNN ID.
|
|
82
|
+
*/
|
|
83
|
+
export function stableMessageKeys(messages: any[]): string[] {
|
|
84
|
+
const occurrences = new Map<string, number>();
|
|
85
|
+
return messages.map((message, index) => {
|
|
86
|
+
const base = stableMessageId(message, index);
|
|
87
|
+
const occurrence = (occurrences.get(base) ?? 0) + 1;
|
|
88
|
+
occurrences.set(base, occurrence);
|
|
89
|
+
return occurrence === 1 ? base : `${base}#${occurrence}`;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
39
93
|
function priorityForMessage(tokenEstimate: number, config: DcpConfig | undefined): "low" | "medium" | "high" {
|
|
40
94
|
const settings = config?.compress?.messageMode;
|
|
41
95
|
const mediumTokens = Math.max(1, settings?.mediumTokens ?? 500);
|
|
@@ -46,140 +100,128 @@ function priorityForMessage(tokenEstimate: number, config: DcpConfig | undefined
|
|
|
46
100
|
return "low";
|
|
47
101
|
}
|
|
48
102
|
|
|
49
|
-
function
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
const index = Number.parseInt(match[1]!, 10);
|
|
53
|
-
return Number.isFinite(index) ? index : undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function summarizeMessageIds(ids: string[]): string {
|
|
57
|
-
if (ids.length === 0) return "none";
|
|
58
|
-
|
|
59
|
-
const ranges: string[] = [];
|
|
60
|
-
let start = ids[0]!;
|
|
61
|
-
let previous = ids[0]!;
|
|
62
|
-
let previousIndex = messageIdIndex(previous);
|
|
63
|
-
|
|
64
|
-
const flush = () => {
|
|
65
|
-
const startIndex = messageIdIndex(start);
|
|
66
|
-
const endIndex = messageIdIndex(previous);
|
|
67
|
-
if (
|
|
68
|
-
startIndex !== undefined &&
|
|
69
|
-
endIndex !== undefined &&
|
|
70
|
-
endIndex - startIndex >= 2
|
|
71
|
-
) {
|
|
72
|
-
ranges.push(`${start}..${previous}`);
|
|
73
|
-
} else if (start === previous) {
|
|
74
|
-
ranges.push(start);
|
|
75
|
-
} else {
|
|
76
|
-
ranges.push(start, previous);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
for (let i = 1; i < ids.length; i++) {
|
|
81
|
-
const id = ids[i]!;
|
|
82
|
-
const index = messageIdIndex(id);
|
|
83
|
-
if (previousIndex !== undefined && index === previousIndex + 1) {
|
|
84
|
-
previous = id;
|
|
85
|
-
previousIndex = index;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
103
|
+
function persistentMessageId(stableKey: string, state: DcpState): string {
|
|
104
|
+
const existing = state.messageIdsByStableId.get(stableKey);
|
|
105
|
+
if (existing) return existing;
|
|
88
106
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
previousIndex = index;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
flush();
|
|
96
|
-
return ranges.join(", ");
|
|
107
|
+
const id = `m${String(state.nextMessageId++).padStart(3, "0")}`;
|
|
108
|
+
state.messageIdsByStableId.set(stableKey, id);
|
|
109
|
+
return id;
|
|
97
110
|
}
|
|
98
111
|
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for (const id of ids) {
|
|
105
|
-
const meta = state.messageMetaSnapshot.get(id);
|
|
106
|
-
if (!meta) continue;
|
|
107
|
-
if (meta.priority === "high") high.push(id);
|
|
108
|
-
else if (meta.priority === "medium") medium.push(id);
|
|
109
|
-
if (meta.blockId !== undefined) blocks.push(`${id}=b${meta.blockId}`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const parts: string[] = [];
|
|
113
|
-
if (high.length > 0) parts.push(`high=${summarizeMessageIds(high)}`);
|
|
114
|
-
if (medium.length > 0) parts.push(`medium=${summarizeMessageIds(medium)}`);
|
|
115
|
-
if (blocks.length > 0) parts.push(`blocks=${blocks.join(",")}`);
|
|
116
|
-
return parts.length > 0 ? `Hints: ${parts.join("; ")}` : undefined;
|
|
112
|
+
function carrierSelfLabel(role: string): string {
|
|
113
|
+
if (role === "toolResult") return "this tool result";
|
|
114
|
+
if (role === "bashExecution") return "this bash result";
|
|
115
|
+
return "this user message";
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
function buildCarrierControlText(
|
|
119
|
+
precedingAssistantIds: string[],
|
|
120
|
+
ownId: string,
|
|
121
|
+
ownRole: string,
|
|
122
|
+
blockId: number | undefined,
|
|
123
|
+
): string {
|
|
124
|
+
const assignments = [
|
|
125
|
+
...precedingAssistantIds.map((id) => `${id}=preceding assistant message`),
|
|
126
|
+
`${ownId}=${carrierSelfLabel(ownRole)}`,
|
|
127
|
+
];
|
|
125
128
|
return [
|
|
126
129
|
"<dcp-message-ids>",
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
...(hint ? [hint] : []),
|
|
130
|
+
`Stable DCP IDs (use with compress; do not quote/output): ${assignments.join("; ")}`,
|
|
131
|
+
...(blockId === undefined ? [] : [`Active compressed block alias: b${blockId}`]),
|
|
130
132
|
"</dcp-message-ids>",
|
|
131
133
|
].join("\n");
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
function appendControlToCarrier(message: any, controlText: string): void {
|
|
137
|
+
const suffix = `\n\n${controlText}`;
|
|
138
|
+
if (typeof message.content === "string") {
|
|
139
|
+
message.content += suffix;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(message.content)) {
|
|
143
|
+
message.content = [...message.content, { type: "text", text: suffix }];
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
message.content = [{ type: "text", text: controlText }];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build the current addressability snapshot and distribute immutable ID
|
|
151
|
+
* metadata over append-only user/tool-result carriers. Assistant messages are
|
|
152
|
+
* never modified: provider-signed reasoning and function-call items must replay
|
|
153
|
+
* byte-for-byte for Responses continuation.
|
|
154
|
+
*/
|
|
134
155
|
export function injectMessageIds(
|
|
135
156
|
messages: any[],
|
|
136
157
|
state: DcpState,
|
|
137
158
|
options: InjectMessageIdsOptions = {},
|
|
138
159
|
): void {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const nextIdSnapshot = new Map<string, number>()
|
|
145
|
-
const nextMetaSnapshot = new Map<string, MessageIdMeta>()
|
|
146
|
-
|
|
147
|
-
let counter = 1;
|
|
160
|
+
const nextIdSnapshot = new Map<string, number>();
|
|
161
|
+
const nextMetaSnapshot = new Map<string, MessageIdMeta>();
|
|
162
|
+
const stableKeys = stableMessageKeys(messages);
|
|
163
|
+
const assignedIds = new Map<number, string>();
|
|
164
|
+
const assignedMeta = new Map<number, MessageIdMeta>();
|
|
148
165
|
|
|
149
166
|
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
|
150
167
|
const msg = messages[messageIndex];
|
|
151
|
-
const role: string = msg
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (PASSTHROUGH_ROLES.has(role)) continue;
|
|
155
|
-
// Skip non-eligible roles
|
|
156
|
-
if (!ID_ELIGIBLE_ROLES.has(role)) continue;
|
|
157
|
-
|
|
158
|
-
const id = "m" + String(counter).padStart(3, "0");
|
|
159
|
-
counter++;
|
|
168
|
+
const role: string = msg?.role ?? "";
|
|
169
|
+
if (PASSTHROUGH_ROLES.has(role) || !ID_ELIGIBLE_ROLES.has(role)) continue;
|
|
170
|
+
if (!Number.isFinite(msg?.timestamp)) continue;
|
|
160
171
|
|
|
172
|
+
const stableKey = stableKeys[messageIndex]!;
|
|
173
|
+
const id = persistentMessageId(stableKey, state);
|
|
161
174
|
const originalText = messageText(msg);
|
|
162
175
|
const blockId = extractBlockId(originalText);
|
|
163
176
|
const tokenEstimate = estimateMessageTokens(msg);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
|
|
178
|
+
assignedIds.set(messageIndex, id);
|
|
179
|
+
assignedMeta.set(messageIndex, {
|
|
180
|
+
timestamp: msg.timestamp,
|
|
181
|
+
stableId: stableKey,
|
|
182
|
+
role,
|
|
183
|
+
blockId,
|
|
184
|
+
toolCallId: typeof msg.toolCallId === "string" ? msg.toolCallId : undefined,
|
|
185
|
+
toolName: typeof msg.toolName === "string" ? msg.toolName : undefined,
|
|
186
|
+
text: originalText,
|
|
187
|
+
tokenEstimate,
|
|
188
|
+
priority: priorityForMessage(tokenEstimate, options.config),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const publish = (messageIndex: number): void => {
|
|
193
|
+
const id = assignedIds.get(messageIndex);
|
|
194
|
+
const meta = assignedMeta.get(messageIndex);
|
|
195
|
+
if (!id || !meta) return;
|
|
196
|
+
nextIdSnapshot.set(id, meta.timestamp);
|
|
197
|
+
nextMetaSnapshot.set(id, meta);
|
|
198
|
+
};
|
|
199
|
+
const pendingAssistantIndexes: number[] = [];
|
|
200
|
+
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
|
201
|
+
const message = messages[messageIndex];
|
|
202
|
+
const role: string = message?.role ?? "";
|
|
203
|
+
const id = assignedIds.get(messageIndex);
|
|
204
|
+
if (!id) continue;
|
|
205
|
+
|
|
206
|
+
if (role === "assistant") {
|
|
207
|
+
pendingAssistantIndexes.push(messageIndex);
|
|
208
|
+
continue;
|
|
180
209
|
}
|
|
210
|
+
if (!ID_CARRIER_ROLES.has(role)) continue;
|
|
211
|
+
|
|
212
|
+
for (const pendingIndex of pendingAssistantIndexes) publish(pendingIndex);
|
|
213
|
+
publish(messageIndex);
|
|
214
|
+
const pendingAssistantIds = pendingAssistantIndexes
|
|
215
|
+
.map((pendingIndex) => assignedIds.get(pendingIndex))
|
|
216
|
+
.filter((pendingId): pendingId is string => pendingId !== undefined);
|
|
217
|
+
const blockId = assignedMeta.get(messageIndex)?.blockId;
|
|
218
|
+
appendControlToCarrier(
|
|
219
|
+
message,
|
|
220
|
+
buildCarrierControlText(pendingAssistantIds, id, role, blockId),
|
|
221
|
+
);
|
|
222
|
+
pendingAssistantIndexes.length = 0;
|
|
181
223
|
}
|
|
182
224
|
|
|
183
|
-
state.messageIdSnapshot = nextIdSnapshot
|
|
184
|
-
state.messageMetaSnapshot = nextMetaSnapshot
|
|
225
|
+
state.messageIdSnapshot = nextIdSnapshot;
|
|
226
|
+
state.messageMetaSnapshot = nextMetaSnapshot;
|
|
185
227
|
}
|
|
@@ -182,7 +182,7 @@ export function extractMessageId(text: string): string | undefined {
|
|
|
182
182
|
)?.[1];
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
function stripStaleDcpMetadataLines(text: string): string {
|
|
185
|
+
export function stripStaleDcpMetadataLines(text: string): string {
|
|
186
186
|
if (!text.includes("<dcp-") && !text.includes("[dcp-")) return text;
|
|
187
187
|
|
|
188
188
|
const lines = text.split("\n");
|
|
@@ -238,21 +238,29 @@ function stripStaleDcpMetadataLines(text: string): string {
|
|
|
238
238
|
|
|
239
239
|
function stripStaleDcpMetadataFromAssistantBlock(block: any): any | undefined {
|
|
240
240
|
if (!block || typeof block !== "object") return block;
|
|
241
|
-
const
|
|
241
|
+
const textCanChange = typeof block.text === "string" &&
|
|
242
|
+
(block.text.includes("<dcp-") || block.text.includes("[dcp-"));
|
|
243
|
+
const thinkingCanChange = typeof block.thinking === "string" &&
|
|
244
|
+
(block.thinking.includes("<dcp-") || block.thinking.includes("[dcp-"));
|
|
245
|
+
if (!textCanChange && !thinkingCanChange) return block;
|
|
246
|
+
|
|
247
|
+
let next = block;
|
|
242
248
|
let touched = false;
|
|
243
249
|
|
|
244
|
-
if (typeof
|
|
245
|
-
const stripped = stripStaleDcpMetadataLines(
|
|
246
|
-
if (stripped !==
|
|
250
|
+
if (typeof block.text === "string") {
|
|
251
|
+
const stripped = stripStaleDcpMetadataLines(block.text);
|
|
252
|
+
if (stripped !== block.text) {
|
|
253
|
+
next = { ...next };
|
|
247
254
|
touched = true;
|
|
248
255
|
next.text = stripped;
|
|
249
256
|
delete next.textSignature;
|
|
250
257
|
}
|
|
251
258
|
}
|
|
252
259
|
|
|
253
|
-
if (typeof
|
|
254
|
-
const stripped = stripStaleDcpMetadataLines(
|
|
255
|
-
if (stripped !==
|
|
260
|
+
if (typeof block.thinking === "string") {
|
|
261
|
+
const stripped = stripStaleDcpMetadataLines(block.thinking);
|
|
262
|
+
if (stripped !== block.thinking) {
|
|
263
|
+
if (!touched) next = { ...next };
|
|
256
264
|
touched = true;
|
|
257
265
|
next.thinking = stripped;
|
|
258
266
|
delete next.thinkingSignature;
|
|
@@ -265,20 +273,23 @@ function stripStaleDcpMetadataFromAssistantBlock(block: any): any | undefined {
|
|
|
265
273
|
}
|
|
266
274
|
|
|
267
275
|
export function stripStaleDcpMetadataFromAssistantMessage(message: any): any {
|
|
268
|
-
|
|
269
|
-
return stripStaleDcpMetadataFromMessage(message);
|
|
276
|
+
return message;
|
|
270
277
|
}
|
|
271
278
|
|
|
272
279
|
export function stripStaleDcpMetadataFromMessage(message: any): any {
|
|
273
280
|
if (!message || typeof message !== "object") return message;
|
|
281
|
+
// Assistant content may contain provider-signed reasoning, text, or tool-call
|
|
282
|
+
// items. Even DCP-looking text must replay byte-for-byte once persisted.
|
|
283
|
+
if (message.role === "assistant") return message;
|
|
274
284
|
if (typeof message.content === "string") {
|
|
275
|
-
|
|
285
|
+
const stripped = stripStaleDcpMetadataLines(message.content);
|
|
286
|
+
return stripped === message.content ? message : { ...message, content: stripped };
|
|
276
287
|
}
|
|
277
288
|
if (!Array.isArray(message.content)) return message;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
};
|
|
289
|
+
const content = message.content
|
|
290
|
+
.map(stripStaleDcpMetadataFromAssistantBlock)
|
|
291
|
+
.filter((block: any) => block !== undefined);
|
|
292
|
+
const unchanged = content.length === message.content.length &&
|
|
293
|
+
content.every((block: any, index: number) => block === message.content[index]);
|
|
294
|
+
return unchanged ? message : { ...message, content };
|
|
284
295
|
}
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
NudgeThresholds,
|
|
8
8
|
} from "./pruner-types.js";
|
|
9
9
|
import { extractBlockId, messageText } from "./pruner-metadata.js";
|
|
10
|
-
import {
|
|
10
|
+
import { stableMessageKeys } from "./pruner-message-ids.js";
|
|
11
11
|
|
|
12
12
|
function coercePercentThreshold(value: number | string | undefined, fallback: number): number {
|
|
13
13
|
if (typeof value === "number") return Number.isFinite(value) ? value : fallback;
|
|
@@ -56,6 +56,7 @@ function isRealAnchorCandidate(msg: any): boolean {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId: string; timestamp: number; role: string } | null {
|
|
59
|
+
const stableKeys = stableMessageKeys(messages);
|
|
59
60
|
// Prefer the latest real user message: it gives the reminder direct user-like
|
|
60
61
|
// salience without creating a new synthetic message at the end of context.
|
|
61
62
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -65,7 +66,7 @@ function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId
|
|
|
65
66
|
return {
|
|
66
67
|
msg,
|
|
67
68
|
index: i,
|
|
68
|
-
stableId:
|
|
69
|
+
stableId: stableKeys[i]!,
|
|
69
70
|
timestamp: msg.timestamp,
|
|
70
71
|
role: msg.role,
|
|
71
72
|
};
|
|
@@ -79,7 +80,7 @@ function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId
|
|
|
79
80
|
return {
|
|
80
81
|
msg,
|
|
81
82
|
index: i,
|
|
82
|
-
stableId:
|
|
83
|
+
stableId: stableKeys[i]!,
|
|
83
84
|
timestamp: msg.timestamp,
|
|
84
85
|
role: msg.role,
|
|
85
86
|
};
|
|
@@ -88,8 +89,8 @@ function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId
|
|
|
88
89
|
return null;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
function anchorMatchesMessage(anchor: DcpNudgeAnchor, msg: any,
|
|
92
|
-
if (anchor.anchorStableId &&
|
|
92
|
+
function anchorMatchesMessage(anchor: DcpNudgeAnchor, msg: any, stableKey: string): boolean {
|
|
93
|
+
if (anchor.anchorStableId && stableKey === anchor.anchorStableId) return true;
|
|
93
94
|
return msg?.timestamp === anchor.anchorTimestamp;
|
|
94
95
|
}
|
|
95
96
|
|
|
@@ -207,34 +208,33 @@ export function upsertNudgeAnchor(
|
|
|
207
208
|
messages: any[],
|
|
208
209
|
state: DcpState,
|
|
209
210
|
type: DcpNudgeType,
|
|
210
|
-
options: { contextPercent?: number } = {},
|
|
211
|
+
options: { contextPercent?: number; renderedReminder?: string } = {},
|
|
211
212
|
): { anchor: DcpNudgeAnchor | null; created: boolean; updated: boolean } {
|
|
212
|
-
const
|
|
213
|
-
if (!target) {
|
|
214
|
-
// The caller will append one synthetic tail reminder. Drop persisted
|
|
215
|
-
// anchors first so applyAnchoredNudges cannot render a second reminder.
|
|
216
|
-
state.nudgeAnchors = [];
|
|
217
|
-
state.lastNudge = undefined;
|
|
218
|
-
return { anchor: null, created: false, updated: false };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const key = `${target.stableId}|${target.timestamp}`;
|
|
213
|
+
const stableKeys = stableMessageKeys(messages);
|
|
222
214
|
let existing: DcpNudgeAnchor | null = null;
|
|
223
215
|
for (const anchor of state.nudgeAnchors) {
|
|
224
|
-
const
|
|
225
|
-
|
|
216
|
+
const stillPresent = messages.some((message, index) =>
|
|
217
|
+
isRealAnchorCandidate(message) && anchorMatchesMessage(anchor, message, stableKeys[index]!),
|
|
218
|
+
);
|
|
219
|
+
if (!stillPresent) continue;
|
|
226
220
|
if (!existing || isNewerAnchor(anchor, existing)) existing = anchor;
|
|
227
221
|
}
|
|
228
222
|
|
|
229
223
|
const now = Date.now();
|
|
230
224
|
if (existing) {
|
|
231
|
-
//
|
|
232
|
-
//
|
|
225
|
+
// Keep the original carrier and frozen text. Moving a reminder to the
|
|
226
|
+
// newest user message would remove bytes from an old provider-cache prefix.
|
|
233
227
|
state.nudgeAnchors = [existing];
|
|
234
228
|
const shouldUpgrade = typePriority(type) > typePriority(existing.type);
|
|
235
|
-
if (shouldUpgrade)
|
|
236
|
-
|
|
237
|
-
|
|
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
238
|
state.lastNudge = {
|
|
239
239
|
type: existing.type,
|
|
240
240
|
anchorId: existing.id,
|
|
@@ -246,6 +246,15 @@ export function upsertNudgeAnchor(
|
|
|
246
246
|
return { anchor: existing, created: false, updated: shouldUpgrade };
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
const target = findAnchorMessage(messages);
|
|
250
|
+
if (!target) {
|
|
251
|
+
// The caller will append one synthetic tail reminder. Drop persisted
|
|
252
|
+
// anchors first so applyAnchoredNudges cannot render a second reminder.
|
|
253
|
+
state.nudgeAnchors = [];
|
|
254
|
+
state.lastNudge = undefined;
|
|
255
|
+
return { anchor: null, created: false, updated: false };
|
|
256
|
+
}
|
|
257
|
+
|
|
249
258
|
const anchor: DcpNudgeAnchor = {
|
|
250
259
|
id: state.nextNudgeAnchorId++,
|
|
251
260
|
type,
|
|
@@ -254,6 +263,7 @@ export function upsertNudgeAnchor(
|
|
|
254
263
|
anchorRole: target.role,
|
|
255
264
|
turnIndex: state.currentTurn,
|
|
256
265
|
contextPercent: options.contextPercent,
|
|
266
|
+
renderedReminder: options.renderedReminder,
|
|
257
267
|
createdAt: now,
|
|
258
268
|
updatedAt: now,
|
|
259
269
|
};
|
|
@@ -275,12 +285,16 @@ export function applyAnchoredNudges(
|
|
|
275
285
|
messages: any[],
|
|
276
286
|
state: DcpState,
|
|
277
287
|
render: (anchor: DcpNudgeAnchor) => string,
|
|
278
|
-
):
|
|
279
|
-
if (state.nudgeAnchors.length === 0) return;
|
|
288
|
+
): { rendered: boolean; stateChanged: boolean } {
|
|
289
|
+
if (state.nudgeAnchors.length === 0) return { rendered: false, stateChanged: false };
|
|
280
290
|
|
|
291
|
+
const stableKeys = stableMessageKeys(messages);
|
|
292
|
+
const priorAnchors = state.nudgeAnchors;
|
|
281
293
|
let selected: { anchor: DcpNudgeAnchor; index: number } | null = null;
|
|
282
294
|
for (const anchor of state.nudgeAnchors) {
|
|
283
|
-
const index = messages.findIndex((msg, messageIndex) =>
|
|
295
|
+
const index = messages.findIndex((msg, messageIndex) =>
|
|
296
|
+
anchorMatchesMessage(anchor, msg, stableKeys[messageIndex]!),
|
|
297
|
+
);
|
|
284
298
|
if (index === -1) continue;
|
|
285
299
|
if (!selected || isNewerAnchor(anchor, selected.anchor)) {
|
|
286
300
|
selected = { anchor, index };
|
|
@@ -290,7 +304,27 @@ export function applyAnchoredNudges(
|
|
|
290
304
|
// Defensive migration for persisted pre-singleton state: render only the
|
|
291
305
|
// newest valid anchor and discard every stale predecessor.
|
|
292
306
|
state.nudgeAnchors = selected ? [selected.anchor] : [];
|
|
293
|
-
|
|
307
|
+
const anchorSetChanged = state.nudgeAnchors.length !== priorAnchors.length ||
|
|
308
|
+
state.nudgeAnchors.some((anchor, index) => anchor !== priorAnchors[index]);
|
|
309
|
+
if (!selected) return { rendered: false, stateChanged: anchorSetChanged };
|
|
310
|
+
|
|
311
|
+
const materializedReminder = selected.anchor.renderedReminder === undefined;
|
|
312
|
+
const reminder = selected.anchor.renderedReminder ?? render(selected.anchor);
|
|
313
|
+
if (materializedReminder) selected.anchor.renderedReminder = reminder;
|
|
314
|
+
const anchorMessage = messages[selected.index];
|
|
315
|
+
if (anchorMessage?.role === "assistant") {
|
|
316
|
+
messages.splice(selected.index + 1, 0, {
|
|
317
|
+
role: "user",
|
|
318
|
+
content: reminder,
|
|
319
|
+
timestamp: selected.anchor.createdAt,
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
appendTextToMessage(anchorMessage, reminder);
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
rendered: true,
|
|
326
|
+
stateChanged: anchorSetChanged || materializedReminder,
|
|
327
|
+
};
|
|
294
328
|
}
|
|
295
329
|
|
|
296
330
|
export function clearDcpNudgeAnchors(state: DcpState): number {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DcpConfig } from "./config.js";
|
|
2
2
|
import type { DcpState } from "./state.js";
|
|
3
3
|
import { applyCompressionBlocks, repairOrphanedToolPairs, syncCompressionBlocks } from "./pruner-compression-blocks.js";
|
|
4
|
-
import {
|
|
4
|
+
import { stripStaleDcpMetadataFromMessage } from "./pruner-metadata.js";
|
|
5
5
|
import { injectMessageIds } from "./pruner-message-ids.js";
|
|
6
6
|
import {
|
|
7
7
|
applyAutoToolOutputPruning,
|
|
@@ -63,7 +63,7 @@ export function applyPruning(
|
|
|
63
63
|
typeof contentBlock === "object" && contentBlock !== null ? { ...contentBlock } : contentBlock,
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
|
-
return
|
|
66
|
+
return stripStaleDcpMetadataFromMessage(clone);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
// 1. Count user turns → update state.currentTurn. Do this before inserting
|
|
@@ -79,21 +79,29 @@ export function applyPruning(
|
|
|
79
79
|
// expansion logic could not catch (e.g. multi-block interactions, pre-broken state).
|
|
80
80
|
repairOrphanedToolPairs(msgs);
|
|
81
81
|
|
|
82
|
-
// 3.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
// 3-5. Discover new automatic pruning decisions only at stable checkpoints.
|
|
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
|
+
}
|
|
90
99
|
|
|
91
100
|
// 6. Apply explicit tool output pruning (prunedToolIds)
|
|
92
101
|
applyToolOutputPruning(msgs, state);
|
|
93
102
|
|
|
94
|
-
// 7. Refresh message ID snapshots
|
|
95
|
-
//
|
|
96
|
-
// hook delivers the id map as a hidden control payload.
|
|
103
|
+
// 7. Refresh message ID snapshots and append stable distributed metadata to
|
|
104
|
+
// user/tool-result carriers. Assistant items remain byte-stable.
|
|
97
105
|
injectMessageIds(msgs, state, { config });
|
|
98
106
|
|
|
99
107
|
// 8. state.messageIdSnapshot/messageMetaSnapshot are already updated by injectMessageIds
|