pi-ui-extend 1.0.38 → 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/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 +8 -0
- 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 +12 -0
- package/external/pi-tools-suite/src/async-subagents/index.ts +6 -2
- 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 +8 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { ToolRecord } from "./state.js";
|
|
3
|
+
import { stripStaleDcpMetadataLines } from "./pruner-metadata.js";
|
|
3
4
|
|
|
4
5
|
export interface ProviderToolResultEvidence {
|
|
5
6
|
ids: Set<string>;
|
|
@@ -19,7 +20,7 @@ function signature(toolName: string, outputText: string): string {
|
|
|
19
20
|
return createHash("sha256")
|
|
20
21
|
.update(toolName)
|
|
21
22
|
.update("\u0000")
|
|
22
|
-
.update(outputText)
|
|
23
|
+
.update(stripStaleDcpMetadataLines(outputText))
|
|
23
24
|
.digest("hex");
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
extractBlockId,
|
|
7
7
|
messageText,
|
|
8
8
|
} from "./pruner-metadata.js";
|
|
9
|
+
import { stableMessageKeys } from "./pruner-message-ids.js";
|
|
9
10
|
|
|
10
11
|
interface CandidateBoundary {
|
|
11
12
|
id: string;
|
|
@@ -24,13 +25,27 @@ function isActiveBlockId(blockId: number, state: DcpState): boolean {
|
|
|
24
25
|
return state.compressionBlocks.some((block) => block.id === blockId && block.active);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function findCurrentMessageId(msg: any, state: DcpState): string | undefined {
|
|
28
|
+
function findCurrentMessageId(msg: any, stableKey: string, state: DcpState): string | undefined {
|
|
28
29
|
const role = msg?.role ?? "";
|
|
29
30
|
const timestamp = msg?.timestamp;
|
|
30
31
|
if (!Number.isFinite(timestamp)) return undefined;
|
|
31
32
|
|
|
32
33
|
for (const [id, meta] of state.messageMetaSnapshot) {
|
|
33
|
-
if (meta.
|
|
34
|
+
if (meta.stableId === stableKey) return id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Backward compatibility for snapshots created before stable IDs were
|
|
38
|
+
// recorded. Never mix timestamp fallback with a modern stable-ID snapshot.
|
|
39
|
+
const isLegacySnapshot = [...state.messageMetaSnapshot.values()]
|
|
40
|
+
.every((meta) => meta.stableId === undefined);
|
|
41
|
+
if (!isLegacySnapshot) return undefined;
|
|
42
|
+
|
|
43
|
+
for (const [id, meta] of state.messageMetaSnapshot) {
|
|
44
|
+
if (
|
|
45
|
+
meta.timestamp === timestamp &&
|
|
46
|
+
meta.role === role &&
|
|
47
|
+
meta.blockId === undefined
|
|
48
|
+
) return id;
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
for (const [id, ts] of state.messageIdSnapshot) {
|
|
@@ -42,6 +57,7 @@ function findCurrentMessageId(msg: any, state: DcpState): string | undefined {
|
|
|
42
57
|
|
|
43
58
|
function resolveAddressableBoundaryId(
|
|
44
59
|
msg: any,
|
|
60
|
+
stableKey: string,
|
|
45
61
|
state: DcpState,
|
|
46
62
|
options: { allowBlocks: boolean },
|
|
47
63
|
): { id: string; blockId?: number; text: string } | null {
|
|
@@ -55,8 +71,9 @@ function resolveAddressableBoundaryId(
|
|
|
55
71
|
|
|
56
72
|
// Inline [dcp-id] markers are no longer injected into message content; the
|
|
57
73
|
// snapshot rebuilt by injectMessageIds() is the sole addressability source.
|
|
58
|
-
// Resolve the message
|
|
59
|
-
|
|
74
|
+
// Resolve the message ID by its persistent stable identity. Timestamp/role
|
|
75
|
+
// matching is retained only for legacy snapshots without stable IDs.
|
|
76
|
+
const currentId = findCurrentMessageId(msg, stableKey, state);
|
|
60
77
|
if (currentId) return { id: currentId, text };
|
|
61
78
|
|
|
62
79
|
return null;
|
|
@@ -73,15 +90,17 @@ export function detectCompressionCandidate(
|
|
|
73
90
|
if (contextPercent < settings.minContextPercent) return null;
|
|
74
91
|
|
|
75
92
|
const boundaries: CandidateBoundary[] = [];
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
const stableKeys = stableMessageKeys(messages);
|
|
94
|
+
for (let index = 0; index < messages.length; index++) {
|
|
95
|
+
const msg = messages[index];
|
|
96
|
+
const boundary = resolveAddressableBoundaryId(msg, stableKeys[index]!, _state, { allowBlocks: true });
|
|
78
97
|
if (!boundary) continue;
|
|
79
98
|
if (!Number.isFinite(msg.timestamp)) continue;
|
|
80
99
|
boundaries.push({
|
|
81
100
|
id: boundary.id,
|
|
82
101
|
role: msg.role ?? "",
|
|
83
102
|
timestamp: msg.timestamp,
|
|
84
|
-
tokenEstimate: estimateMessageTokens(msg),
|
|
103
|
+
tokenEstimate: _state.messageMetaSnapshot.get(boundary.id)?.tokenEstimate ?? estimateMessageTokens(msg),
|
|
85
104
|
blockId: boundary.blockId,
|
|
86
105
|
isSystemReminder: boundary.text.includes("<dcp-system-reminder>"),
|
|
87
106
|
});
|
|
@@ -153,15 +172,17 @@ export function detectMessageCompressionCandidates(
|
|
|
153
172
|
if (contextPercent < settings.minContextPercent) return [];
|
|
154
173
|
|
|
155
174
|
const boundaries: CandidateBoundary[] = [];
|
|
156
|
-
|
|
157
|
-
|
|
175
|
+
const stableKeys = stableMessageKeys(messages);
|
|
176
|
+
for (let index = 0; index < messages.length; index++) {
|
|
177
|
+
const msg = messages[index];
|
|
178
|
+
const boundary = resolveAddressableBoundaryId(msg, stableKeys[index]!, state, { allowBlocks: false });
|
|
158
179
|
if (!boundary || boundary.blockId !== undefined) continue;
|
|
159
180
|
if (!Number.isFinite(msg.timestamp)) continue;
|
|
160
181
|
boundaries.push({
|
|
161
182
|
id: boundary.id,
|
|
162
183
|
role: msg.role ?? "",
|
|
163
184
|
timestamp: msg.timestamp,
|
|
164
|
-
tokenEstimate: estimateMessageTokens(msg),
|
|
185
|
+
tokenEstimate: state.messageMetaSnapshot.get(boundary.id)?.tokenEstimate ?? estimateMessageTokens(msg),
|
|
165
186
|
isSystemReminder: boundary.text.includes("<dcp-system-reminder>"),
|
|
166
187
|
});
|
|
167
188
|
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { DcpConfig } from "./config.js";
|
|
2
2
|
import type { DcpState } from "./state.js";
|
|
3
3
|
import { PASSTHROUGH_ROLES, estimateMessageTokens } from "./pruner-metadata.js";
|
|
4
|
-
import {
|
|
4
|
+
import { stableMessageKeys } from "./pruner-message-ids.js";
|
|
5
5
|
import { writeDcpDebugLog } from "./debug-log.js";
|
|
6
6
|
|
|
7
|
-
function messageMatchesBoundary(msg: any, messageIndex: number, stableId: string | undefined, timestamp: number): boolean {
|
|
8
|
-
if (stableId && stableMessageId(msg, messageIndex) === stableId) return true;
|
|
9
|
-
return msg.timestamp === timestamp;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
7
|
function findBoundaryIndex(messages: any[], stableId: string | undefined, timestamp: number): number {
|
|
13
|
-
|
|
8
|
+
const stableKeys = stableId ? stableMessageKeys(messages) : [];
|
|
9
|
+
return messages.findIndex((message, index) => {
|
|
10
|
+
if (stableId && stableKeys[index] === stableId) return true;
|
|
11
|
+
return message.timestamp === timestamp;
|
|
12
|
+
});
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export interface ReconcileInheritedBlocksResult {
|
|
@@ -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
|
}
|