pi-ui-extend 0.1.36 → 0.1.38
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/app.d.ts +7 -0
- package/dist/app/app.js +40 -5
- package/dist/app/commands/command-controller.js +1 -0
- package/dist/app/commands/command-registry.d.ts +1 -0
- package/dist/app/commands/command-registry.js +8 -0
- package/dist/app/commands/command-session-actions.d.ts +2 -0
- package/dist/app/commands/command-session-actions.js +79 -1
- package/dist/app/extensions/extension-actions-controller.d.ts +4 -1
- package/dist/app/extensions/extension-actions-controller.js +31 -2
- package/dist/app/input/input-controller.d.ts +1 -0
- package/dist/app/input/input-controller.js +23 -2
- package/dist/app/input/terminal-edit-shortcuts.d.ts +1 -0
- package/dist/app/input/terminal-edit-shortcuts.js +7 -0
- package/dist/app/input/voice-controller.js +1 -1
- package/dist/app/popup/popup-action-controller.d.ts +1 -3
- package/dist/app/popup/popup-action-controller.js +1 -5
- package/dist/app/rendering/message-content.js +4 -3
- package/dist/app/rendering/render-controller.js +21 -38
- package/dist/app/rendering/status-line-renderer.d.ts +1 -0
- package/dist/app/rendering/status-line-renderer.js +14 -2
- package/dist/app/runtime.js +12 -2
- package/dist/app/screen/mouse-controller.js +2 -0
- package/dist/app/session/session-event-controller.d.ts +7 -0
- package/dist/app/session/session-event-controller.js +10 -13
- package/dist/app/terminal/terminal-controller.js +1 -0
- package/dist/app/terminal/terminal-output-buffer.d.ts +8 -6
- package/dist/app/terminal/terminal-output-buffer.js +24 -16
- package/dist/bundled-extensions/terminal-bell/index.js +118 -33
- package/dist/markdown-format.d.ts +1 -0
- package/dist/markdown-format.js +30 -16
- package/dist/schemas/pi-tools-suite-schema.d.ts +5 -0
- package/dist/schemas/pi-tools-suite-schema.js +5 -0
- package/dist/tool-renderers/apply-patch.js +6 -1
- package/dist/tool-renderers/patch-normalize.d.ts +24 -0
- package/dist/tool-renderers/patch-normalize.js +163 -0
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +15 -2
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +36 -19
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +5 -2
- package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +8 -3
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +63 -28
- package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +9 -4
- package/external/pi-tools-suite/src/comment-checker/config.ts +98 -0
- package/external/pi-tools-suite/src/comment-checker/detect.ts +215 -0
- package/external/pi-tools-suite/src/comment-checker/index.ts +294 -0
- package/external/pi-tools-suite/src/dcp/commands.ts +29 -15
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +111 -60
- package/external/pi-tools-suite/src/dcp/config.ts +10 -6
- package/external/pi-tools-suite/src/dcp/debug-log.ts +235 -0
- package/external/pi-tools-suite/src/dcp/index.ts +204 -27
- package/external/pi-tools-suite/src/dcp/prompts.ts +25 -28
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +19 -1
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +36 -58
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +18 -0
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
- package/external/pi-tools-suite/src/dcp/pruner.ts +4 -2
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +31 -2
- package/external/pi-tools-suite/src/dcp/state.ts +62 -4
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +18 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +11 -3
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
- package/external/pi-tools-suite/src/todo/index.ts +24 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +3 -3
- package/external/pi-tools-suite/src/usage/index.ts +18 -4
- package/package.json +4 -4
- package/schemas/pi-tools-suite.json +24 -0
|
@@ -13,7 +13,7 @@ You operate in a context-constrained environment. Manage context continuously to
|
|
|
13
13
|
|
|
14
14
|
The ONLY tool you have for context management is \`compress\`. It replaces older conversation content with technical summaries you produce. It supports both range compression (\`ranges\`) and surgical one-message compression (\`messages\`).
|
|
15
15
|
|
|
16
|
-
\`
|
|
16
|
+
\`mNNN\`/\`bN\` DCP boundary IDs and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
|
|
17
17
|
|
|
18
18
|
THE PHILOSOPHY OF COMPRESS
|
|
19
19
|
\`compress\` transforms conversation content into dense, high-fidelity summaries. This is cleanup plus preservation: keep the state needed to continue, discard incidental transcript detail.
|
|
@@ -26,21 +26,21 @@ When multiple independent stale sections exist, prefer several focused compressi
|
|
|
26
26
|
When one older message is huge but the surrounding context is still useful, use message-mode compression for that single message instead of compressing a broad range.
|
|
27
27
|
Summaries should be proportional to future usefulness, not proportional to the amount of text being removed.
|
|
28
28
|
|
|
29
|
-
Use \`compress\` as
|
|
29
|
+
Use \`compress\` as context-pressure housekeeping, not as a reflex after every small step. When context usage is meaningfully high or a DCP reminder provides concrete candidates, compress closed slices before accumulating another large batch of tool output. At low context usage, continue normal work unless a closed slice is clearly large enough that keeping it raw would reduce signal.
|
|
30
30
|
|
|
31
31
|
A closed slice is any finished implementation, verification, config edit, answered exploration, dead-end debugging branch, or test/log inspection. Passing logs are summary-only: preserve command, pass/fail, key failures if any, and whether follow-up is needed; never keep a full passing log in live context. Treat large shell/read/repo/web outputs as disposable evidence once their facts are extracted.
|
|
32
32
|
|
|
33
33
|
Before compressing while work is unfinished, ensure one \`todo in_progress\` captures the active objective and next step.
|
|
34
34
|
|
|
35
|
-
When a \`<dcp-system-reminder>\` appears, treat it as
|
|
35
|
+
When a \`<dcp-system-reminder>\` appears, treat it as a context-pressure signal. Follow critical/high-context reminders promptly. For routine reminders, compress only if a genuinely closed, useful-to-summarize slice exists; otherwise continue the next atomic step and re-check later.
|
|
36
36
|
|
|
37
37
|
CADENCE, SIGNALS, AND LATENCY
|
|
38
38
|
|
|
39
|
-
-
|
|
39
|
+
- Low context usage does not mandate compression
|
|
40
40
|
- Prioritize closedness and independence over raw size
|
|
41
41
|
- Prefer smaller, regular compressions over infrequent massive compressions for better latency and summary quality
|
|
42
42
|
- When multiple independent stale sections are ready, batch compressions in parallel
|
|
43
|
-
- Before
|
|
43
|
+
- Before large new exploration, ask whether an older completed slice can become summary-only; if yes and context pressure is meaningful, compress first
|
|
44
44
|
|
|
45
45
|
COMPRESS WHEN
|
|
46
46
|
|
|
@@ -81,14 +81,14 @@ It is your responsibility to keep a sharp, high-quality context window for optim
|
|
|
81
81
|
*/
|
|
82
82
|
export const COMPRESS_RANGE_DESCRIPTION = `Collapse one or more ranges of the conversation into detailed summaries.
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
CONTEXT-PRESSURE HOUSEKEEPING
|
|
85
|
+
Use compression when it will materially improve the live context window. Low context usage by itself does not require compression, even when a small closed slice exists. When context usage is meaningfully high, or a DCP reminder supplies concrete high-yield candidates, closed implementation, verification, config/doc edit, answered exploration, dead-end debugging, or finished test/log inspection can become summary-only before another large exploratory batch.
|
|
86
86
|
|
|
87
87
|
PASSING LOGS AND LARGE OUTPUTS
|
|
88
88
|
Passing check/test/lint/tsc logs are summary-only after you know the result. Preserve command, pass/fail, key failures if any, and follow-up status; drop full passing output. Treat large shell/read/repo/web outputs as disposable evidence once important facts are extracted.
|
|
89
89
|
|
|
90
90
|
DCP REMINDERS
|
|
91
|
-
If a \`<dcp-system-reminder>\` is present in context, treat it as a
|
|
91
|
+
If a \`<dcp-system-reminder>\` is present in context, treat it as a signal to evaluate compression. Critical or high-context reminders should be handled promptly. Routine reminders should lead to compression only when a safe, closed, useful-to-summarize range exists; otherwise continue the next atomic step and re-check later.
|
|
92
92
|
|
|
93
93
|
THE SUMMARY
|
|
94
94
|
Your summary must be COMPLETE FOR CONTINUATION, not a transcript rewrite. Preserve only information that will plausibly matter later: user intent, accepted constraints, decisions, files/symbols changed or inspected, exact errors that are still actionable, verification status, and next steps.
|
|
@@ -128,7 +128,7 @@ Compressed block sections in context are clearly marked with a header:
|
|
|
128
128
|
|
|
129
129
|
- \`[Compressed conversation section]\`
|
|
130
130
|
|
|
131
|
-
Compressed block IDs always use the \`bN\` form (never \`mNNN\`) and are represented as hidden
|
|
131
|
+
Compressed block IDs always use the \`bN\` form (never \`mNNN\`) and are represented as hidden DCP metadata.
|
|
132
132
|
|
|
133
133
|
Rules:
|
|
134
134
|
|
|
@@ -154,7 +154,7 @@ You specify boundaries by ID using the injected metadata IDs present in the conv
|
|
|
154
154
|
- \`mNNN\` IDs identify raw messages (3 digits, zero-padded, e.g. \`m001\`, \`m042\`)
|
|
155
155
|
- \`bN\` IDs identify previously compressed blocks
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
Current raw message IDs are provided in hidden DCP control metadata at the end of the model context.
|
|
158
158
|
Some message-compression candidate hints include a low/medium/high priority; prefer high-priority stale message IDs for message-mode compression when a full range would be too broad.
|
|
159
159
|
The ID reference line appears at the end of the message it belongs to — it identifies the message above it, not the one below it.
|
|
160
160
|
Treat these reference lines as boundary metadata only, not as tool result content.
|
|
@@ -202,24 +202,23 @@ If the compressed range includes user messages, preserve user intent exactly. Pr
|
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
204
|
* Injected into messages when context usage exceeds maxContextPercent.
|
|
205
|
-
* nudgeForce = "soft" —
|
|
205
|
+
* nudgeForce = "soft" — high context-pressure tone.
|
|
206
206
|
*/
|
|
207
207
|
export const CONTEXT_LIMIT_NUDGE_SOFT = `<dcp-system-reminder>
|
|
208
208
|
ACTION REQUIRED: Context usage is high.
|
|
209
209
|
|
|
210
|
-
Before doing more exploration, look for a closed
|
|
210
|
+
Before doing more exploration, look for a high-yield closed range that no longer needs to stay raw. Compress it now if one is safe and useful.
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
High-priority stale shell/read/repo/web outputs must be compressed once no exact raw text is needed. Passing logs should not remain raw after they are understood.
|
|
212
|
+
This is context-pressure guidance, not a request to compress tiny or still-needed slices. If completed research, implementation, verification, config/doc edit, CI-log inspection, or dead-end debugging is large enough to reduce signal, call the \`compress\` tool before continuing normal work.
|
|
213
|
+
High-priority stale shell/read/repo/web outputs should be compressed once no exact raw text is needed. Passing logs should not remain raw after they are understood.
|
|
215
214
|
|
|
216
215
|
RANGE SELECTION
|
|
217
216
|
Prefer older, resolved history. Avoid the newest active working slice unless it is clearly done.
|
|
218
217
|
Use injected boundary IDs (\`mNNN\` for messages, \`bN\` for compressed blocks) and ensure \`startId\` appears before \`endId\`.
|
|
219
218
|
For a single large stale message, use message-mode compression via the \`messages\` array.
|
|
220
219
|
|
|
221
|
-
If multiple independent ranges are ready, batch them in a single \`compress\` call.
|
|
222
|
-
If nothing is cleanly closed yet, continue
|
|
220
|
+
If multiple independent high-yield ranges are ready, batch them in a single \`compress\` call.
|
|
221
|
+
If nothing is cleanly closed and worth summarizing yet, continue with the next atomic step and re-check later.
|
|
223
222
|
</dcp-system-reminder>`
|
|
224
223
|
|
|
225
224
|
/**
|
|
@@ -227,17 +226,16 @@ If nothing is cleanly closed yet, continue only with the next atomic step and re
|
|
|
227
226
|
* at the configured nudgeFrequency cadence.
|
|
228
227
|
*/
|
|
229
228
|
export const TURN_NUDGE = `<dcp-system-reminder>
|
|
230
|
-
|
|
229
|
+
CONTEXT CHECK: Evaluate whether compression would materially improve the live context.
|
|
231
230
|
|
|
232
|
-
If
|
|
233
|
-
If direction has shifted, compress earlier ranges that are now less relevant.
|
|
231
|
+
If a range is cleanly closed, non-trivial, and unlikely to be needed verbatim again, use the \`compress\` tool. If direction has shifted, consider whether earlier ranges are now less relevant.
|
|
234
232
|
|
|
235
|
-
Do not
|
|
236
|
-
High-priority stale shell/read/repo/web outputs and understood passing logs
|
|
233
|
+
Do not compress just because a small slice closed while context is still low. Prefer compression before another large batch of searches, reads, CI log fetches, or tests when a high-yield stale slice exists.
|
|
234
|
+
High-priority stale shell/read/repo/web outputs and understood passing logs should be compressed once no exact raw text is needed.
|
|
237
235
|
|
|
238
236
|
Prefer small, closed-range compressions over one broad compression.
|
|
239
237
|
Use message-mode compression for isolated large stale messages.
|
|
240
|
-
The goal is to filter noise and distill key information so context accumulation stays under control.
|
|
238
|
+
The goal is to filter meaningful noise and distill key information so context accumulation stays under control.
|
|
241
239
|
Keep active context uncompressed.
|
|
242
240
|
</dcp-system-reminder>`
|
|
243
241
|
|
|
@@ -245,12 +243,11 @@ Keep active context uncompressed.
|
|
|
245
243
|
* Injected after iterationNudgeThreshold tool calls since the last user message.
|
|
246
244
|
*/
|
|
247
245
|
export const ITERATION_NUDGE = `<dcp-system-reminder>
|
|
248
|
-
|
|
246
|
+
CONTEXT CHECK: You've been iterating for a while after the last user message.
|
|
249
247
|
|
|
250
|
-
Pause before the next non-atomic tool
|
|
248
|
+
Pause before the next large non-atomic tool batch. If there is a closed portion that is unlikely to be referenced immediately and is worth summarizing (for example, finished research before implementation, completed config edit, completed CI-log triage, a verified fix, or a dead-end investigation), use the \`compress\` tool on it.
|
|
251
249
|
|
|
252
|
-
|
|
253
|
-
If a completed implementation+verification slice exists, compress it before replying or starting another task.
|
|
250
|
+
Avoid accumulating large tool outputs while a high-yield completed slice remains raw. If only small or still-needed ranges are closed, continue the next atomic step and re-check later.
|
|
254
251
|
|
|
255
252
|
Prefer multiple short, closed ranges over one large range when several independent slices are ready.
|
|
256
253
|
Use message-mode compression for isolated large stale messages.
|
|
@@ -264,7 +261,7 @@ Use message-mode compression for isolated large stale messages.
|
|
|
264
261
|
export const MANUAL_MODE_SYSTEM_PROMPT = `
|
|
265
262
|
You are operating in DCP manual mode for context management.
|
|
266
263
|
|
|
267
|
-
\`
|
|
264
|
+
\`mNNN\`/\`bN\` DCP boundary IDs and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
|
|
268
265
|
|
|
269
266
|
In manual mode you do NOT proactively compress conversation content. Compression is a deliberate, user-directed action.
|
|
270
267
|
|
|
@@ -4,7 +4,6 @@ import type { CompressionCandidate, MessageCompressionCandidate } from "./pruner
|
|
|
4
4
|
import {
|
|
5
5
|
estimateMessageTokens,
|
|
6
6
|
extractBlockId,
|
|
7
|
-
extractMessageId,
|
|
8
7
|
messageText,
|
|
9
8
|
} from "./pruner-metadata.js";
|
|
10
9
|
|
|
@@ -54,16 +53,13 @@ function resolveAddressableBoundaryId(
|
|
|
54
53
|
return null;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (state.messageMetaSnapshot.has(messageId) || state.messageIdSnapshot.has(messageId)) {
|
|
62
|
-
return { id: messageId, text };
|
|
63
|
-
}
|
|
64
|
-
|
|
56
|
+
// Inline [dcp-id] markers are no longer injected into message content; the
|
|
57
|
+
// snapshot rebuilt by injectMessageIds() is the sole addressability source.
|
|
58
|
+
// Resolve the message id by matching its (timestamp, role) in the snapshot.
|
|
65
59
|
const currentId = findCurrentMessageId(msg, state);
|
|
66
|
-
|
|
60
|
+
if (currentId) return { id: currentId, text };
|
|
61
|
+
|
|
62
|
+
return null;
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
export function detectCompressionCandidate(
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { DcpConfig } from "./config.js";
|
|
1
2
|
import type { DcpState } from "./state.js";
|
|
2
3
|
import { PASSTHROUGH_ROLES, estimateMessageTokens } from "./pruner-metadata.js";
|
|
3
4
|
import { stableMessageId } from "./pruner-message-ids.js";
|
|
5
|
+
import { writeDcpDebugLog } from "./debug-log.js";
|
|
4
6
|
|
|
5
7
|
function messageMatchesBoundary(msg: any, messageIndex: number, stableId: string | undefined, timestamp: number): boolean {
|
|
6
8
|
if (stableId && stableMessageId(msg, messageIndex) === stableId) return true;
|
|
@@ -23,7 +25,7 @@ function collectToolCallIds(messages: any[]): Set<string> {
|
|
|
23
25
|
return ids;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
export function syncCompressionBlocks(messages: any[], state: DcpState): void {
|
|
28
|
+
export function syncCompressionBlocks(messages: any[], state: DcpState, config: DcpConfig): void {
|
|
27
29
|
if (state.compressionBlocks.length === 0) return;
|
|
28
30
|
|
|
29
31
|
const toolCallIds = collectToolCallIds(messages);
|
|
@@ -38,6 +40,13 @@ export function syncCompressionBlocks(messages: any[], state: DcpState): void {
|
|
|
38
40
|
) {
|
|
39
41
|
block.active = false;
|
|
40
42
|
block.deactivatedReason = "missing-origin-compress-call";
|
|
43
|
+
writeDcpDebugLog(config, "block.auto_deactivated", {
|
|
44
|
+
blockId: `b${block.id}`,
|
|
45
|
+
reason: "missing-origin-compress-call",
|
|
46
|
+
topic: block.topic,
|
|
47
|
+
createdByToolCallId: block.createdByToolCallId,
|
|
48
|
+
activeBlocksAfter: state.compressionBlocks.filter((b) => b.active).length,
|
|
49
|
+
});
|
|
41
50
|
continue;
|
|
42
51
|
}
|
|
43
52
|
|
|
@@ -49,6 +58,15 @@ export function syncCompressionBlocks(messages: any[], state: DcpState): void {
|
|
|
49
58
|
if (startIdx === -1 || endIdx === -1) {
|
|
50
59
|
block.active = false;
|
|
51
60
|
block.deactivatedReason = "missing-origin-message";
|
|
61
|
+
writeDcpDebugLog(config, "block.auto_deactivated", {
|
|
62
|
+
blockId: `b${block.id}`,
|
|
63
|
+
reason: "missing-origin-message",
|
|
64
|
+
topic: block.topic,
|
|
65
|
+
missingBoundary: startIdx === -1 ? "start" : "end",
|
|
66
|
+
startMessageId: block.startMessageId,
|
|
67
|
+
endMessageId: block.endMessageId,
|
|
68
|
+
activeBlocksAfter: state.compressionBlocks.filter((b) => b.active).length,
|
|
69
|
+
});
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
72
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DcpState } from "./state.js";
|
|
1
|
+
import type { DcpState, MessageIdMeta } from "./state.js";
|
|
2
2
|
import type { DcpConfig } from "./config.js";
|
|
3
3
|
import {
|
|
4
4
|
ID_ELIGIBLE_ROLES,
|
|
@@ -9,11 +9,6 @@ import {
|
|
|
9
9
|
} from "./pruner-metadata.js";
|
|
10
10
|
|
|
11
11
|
export interface InjectMessageIdsOptions {
|
|
12
|
-
/**
|
|
13
|
-
* When false, rebuild internal mNNN snapshots without appending visible
|
|
14
|
-
* DCP marker lines to provider-visible message content.
|
|
15
|
-
*/
|
|
16
|
-
visible?: boolean;
|
|
17
12
|
/** Config enables priority markers for message-mode candidates. */
|
|
18
13
|
config?: DcpConfig;
|
|
19
14
|
}
|
|
@@ -51,10 +46,29 @@ function priorityForMessage(tokenEstimate: number, config: DcpConfig | undefined
|
|
|
51
46
|
return "low";
|
|
52
47
|
}
|
|
53
48
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
function controlLineForMessageId(id: string, state: DcpState): string {
|
|
50
|
+
const meta = state.messageMetaSnapshot.get(id);
|
|
51
|
+
if (!meta) return `- ${id}`;
|
|
52
|
+
|
|
53
|
+
const details: string[] = [meta.role];
|
|
54
|
+
if (meta.blockId !== undefined) details.push(`block=b${meta.blockId}`);
|
|
55
|
+
if (meta.toolName) details.push(`tool=${meta.toolName}`);
|
|
56
|
+
if (meta.priority) details.push(`priority=${meta.priority}`);
|
|
57
|
+
return `- ${id}: ${details.join(", ")}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildMessageIdControlText(state: DcpState): string | undefined {
|
|
61
|
+
const ids = [...state.messageIdSnapshot.keys()];
|
|
62
|
+
if (ids.length === 0) return undefined;
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
"<dcp-message-ids>",
|
|
66
|
+
"DCP metadata for the preceding conversation messages. These IDs are model-visible but UI-hidden control data.",
|
|
67
|
+
"Use only these current IDs with the compress tool; do not quote or output this metadata.",
|
|
68
|
+
`Current raw message IDs: ${ids.join(", ")}`,
|
|
69
|
+
...ids.map((id) => controlLineForMessageId(id, state)),
|
|
70
|
+
"</dcp-message-ids>",
|
|
71
|
+
].join("\n");
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
export function injectMessageIds(
|
|
@@ -62,11 +76,13 @@ export function injectMessageIds(
|
|
|
62
76
|
state: DcpState,
|
|
63
77
|
options: InjectMessageIdsOptions = {},
|
|
64
78
|
): void {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
// Rebuild into local maps and assign once at the end, so readers (compress
|
|
80
|
+
// tool, candidates, provider payload builder) never observe a transiently
|
|
81
|
+
// empty snapshot mid-rebuild. The previous clear()-then-set loop exposed a
|
|
82
|
+
// window where any concurrent reader saw `messageIdSnapshot` as empty,
|
|
83
|
+
// which surfaced to the model as `Current raw message IDs: none`.
|
|
84
|
+
const nextIdSnapshot = new Map<string, number>()
|
|
85
|
+
const nextMetaSnapshot = new Map<string, MessageIdMeta>()
|
|
70
86
|
|
|
71
87
|
let counter = 1;
|
|
72
88
|
|
|
@@ -86,52 +102,11 @@ export function injectMessageIds(
|
|
|
86
102
|
const blockId = extractBlockId(originalText);
|
|
87
103
|
const tokenEstimate = estimateMessageTokens(msg);
|
|
88
104
|
const priority = priorityForMessage(tokenEstimate, options.config);
|
|
89
|
-
const idTag = formatIdTag(id, priority);
|
|
90
105
|
const rawStableId = stableMessageId(msg, messageIndex);
|
|
91
106
|
|
|
92
|
-
if (!visible) {
|
|
93
|
-
// Snapshot-only mode: keep mNNN mappings fresh for DCP internals, but
|
|
94
|
-
// do not expose synthetic IDs to the agent.
|
|
95
|
-
} else if (role === "user") {
|
|
96
|
-
if (typeof msg.content === "string") {
|
|
97
|
-
msg.content = msg.content + `\n${idTag}`;
|
|
98
|
-
} else if (Array.isArray(msg.content)) {
|
|
99
|
-
msg.content = [...msg.content, { type: "text", text: idTag }];
|
|
100
|
-
}
|
|
101
|
-
} else if (role === "toolResult" || role === "bashExecution") {
|
|
102
|
-
if (Array.isArray(msg.content)) {
|
|
103
|
-
msg.content = [...msg.content, { type: "text", text: idTag }];
|
|
104
|
-
} else if (typeof msg.content === "string") {
|
|
105
|
-
msg.content = msg.content + idTag;
|
|
106
|
-
}
|
|
107
|
-
} else if (role === "assistant") {
|
|
108
|
-
if (Array.isArray(msg.content)) {
|
|
109
|
-
// Insert the ID tag before any tool_use (toolCall) blocks.
|
|
110
|
-
// Anthropic requires: thinking → text → tool_use.
|
|
111
|
-
// Appending after tool_use blocks violates that constraint.
|
|
112
|
-
const firstToolCallIdx = msg.content.findIndex(
|
|
113
|
-
(b: any) => b.type === "toolCall",
|
|
114
|
-
);
|
|
115
|
-
const idBlock = { type: "text", text: idTag };
|
|
116
|
-
if (firstToolCallIdx === -1) {
|
|
117
|
-
// No tool_use blocks — append as usual
|
|
118
|
-
msg.content = [...msg.content, idBlock];
|
|
119
|
-
} else {
|
|
120
|
-
// Insert immediately before the first tool_use block
|
|
121
|
-
msg.content = [
|
|
122
|
-
...msg.content.slice(0, firstToolCallIdx),
|
|
123
|
-
idBlock,
|
|
124
|
-
...msg.content.slice(firstToolCallIdx),
|
|
125
|
-
];
|
|
126
|
-
}
|
|
127
|
-
} else if (typeof msg.content === "string") {
|
|
128
|
-
msg.content = msg.content + idTag;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
107
|
if (msg.timestamp !== undefined) {
|
|
133
|
-
|
|
134
|
-
|
|
108
|
+
nextIdSnapshot.set(id, msg.timestamp);
|
|
109
|
+
nextMetaSnapshot.set(id, {
|
|
135
110
|
timestamp: msg.timestamp,
|
|
136
111
|
stableId: rawStableId,
|
|
137
112
|
role,
|
|
@@ -144,4 +119,7 @@ export function injectMessageIds(
|
|
|
144
119
|
});
|
|
145
120
|
}
|
|
146
121
|
}
|
|
122
|
+
|
|
123
|
+
state.messageIdSnapshot = nextIdSnapshot
|
|
124
|
+
state.messageMetaSnapshot = nextMetaSnapshot
|
|
147
125
|
}
|
|
@@ -17,6 +17,8 @@ const DCP_BLOCK_ID_METADATA_LINE_RE = /^\s*<dcp-block-id(?:>|=)b\d+(?:<\/dcp-blo
|
|
|
17
17
|
const DCP_ID_MARKDOWN_REF_LINE_RE = /^\s*\[dcp-id\]:\s*#\s*\(m\d+(?:\s+priority=(?:low|medium|high))?\)(?:\s+priority=(?:low|medium|high))?\s*$/i;
|
|
18
18
|
const DCP_BLOCK_ID_MARKDOWN_REF_LINE_RE = /^\s*\[dcp-block-id\]:\s*#\s*\(b\d+\)\s*$/i;
|
|
19
19
|
const DCP_MARKDOWN_REF_FRAGMENT_LINE_RE = /^\s*\[dcp[^\n]*$/i;
|
|
20
|
+
const DCP_MESSAGE_IDS_START_LINE_RE = /^\s*<dcp-message-ids>/i;
|
|
21
|
+
const DCP_MESSAGE_IDS_END_LINE_RE = /<\/dcp-message-ids>\s*$/i;
|
|
20
22
|
const DCP_SYSTEM_REMINDER_START_LINE_RE = /^\s*<dcp-system-reminder>/i;
|
|
21
23
|
const DCP_SYSTEM_REMINDER_END_LINE_RE = /(?:<\/dcp-system-reminder>|(?<!<)dcp-system-reminder>)\s*$/i;
|
|
22
24
|
const MARKDOWN_FENCE_LINE_RE = /^\s*(```|~~~)/;
|
|
@@ -185,6 +187,7 @@ function stripStaleDcpMetadataLines(text: string): string {
|
|
|
185
187
|
|
|
186
188
|
const lines = text.split("\n");
|
|
187
189
|
const kept: string[] = [];
|
|
190
|
+
let inMessageIds = false;
|
|
188
191
|
let inSystemReminder = false;
|
|
189
192
|
let inMarkdownFence = false;
|
|
190
193
|
|
|
@@ -200,6 +203,16 @@ function stripStaleDcpMetadataLines(text: string): string {
|
|
|
200
203
|
continue;
|
|
201
204
|
}
|
|
202
205
|
|
|
206
|
+
if (inMessageIds) {
|
|
207
|
+
if (DCP_MESSAGE_IDS_END_LINE_RE.test(line)) inMessageIds = false;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (DCP_MESSAGE_IDS_START_LINE_RE.test(line)) {
|
|
212
|
+
if (!DCP_MESSAGE_IDS_END_LINE_RE.test(line)) inMessageIds = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
203
216
|
if (inSystemReminder) {
|
|
204
217
|
if (DCP_SYSTEM_REMINDER_END_LINE_RE.test(line)) inSystemReminder = false;
|
|
205
218
|
continue;
|
|
@@ -253,6 +266,11 @@ function stripStaleDcpMetadataFromAssistantBlock(block: any): any | undefined {
|
|
|
253
266
|
|
|
254
267
|
export function stripStaleDcpMetadataFromAssistantMessage(message: any): any {
|
|
255
268
|
if (!message || typeof message !== "object" || message.role !== "assistant") return message;
|
|
269
|
+
return stripStaleDcpMetadataFromMessage(message);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function stripStaleDcpMetadataFromMessage(message: any): any {
|
|
273
|
+
if (!message || typeof message !== "object") return message;
|
|
256
274
|
if (typeof message.content === "string") {
|
|
257
275
|
return { ...message, content: stripStaleDcpMetadataLines(message.content) };
|
|
258
276
|
}
|
|
@@ -300,13 +300,13 @@ export function getNudgeType(
|
|
|
300
300
|
const cadence = Math.max(1, Math.floor(nudgeFrequency));
|
|
301
301
|
|
|
302
302
|
if (!Number.isFinite(contextPercent)) return null;
|
|
303
|
-
if (contextPercent <= minContextPercent) return null;
|
|
304
|
-
if (state.nudgeCounter + 1 < cadence) return null;
|
|
305
|
-
|
|
306
303
|
if (contextPercent > maxContextPercent) {
|
|
307
304
|
return nudgeForce === "strong" ? "context-strong" : "context-soft";
|
|
308
305
|
}
|
|
309
306
|
|
|
307
|
+
if (contextPercent <= minContextPercent) return null;
|
|
308
|
+
if (state.nudgeCounter + 1 < cadence) return null;
|
|
309
|
+
|
|
310
310
|
if (toolCallsSinceLastUser >= iterationNudgeThreshold) {
|
|
311
311
|
return "iteration";
|
|
312
312
|
}
|
|
@@ -64,7 +64,7 @@ export function applyPruning(
|
|
|
64
64
|
|
|
65
65
|
// 2. Reconcile persisted compression blocks with the current raw context,
|
|
66
66
|
// then apply active compression blocks.
|
|
67
|
-
syncCompressionBlocks(msgs, state);
|
|
67
|
+
syncCompressionBlocks(msgs, state, config);
|
|
68
68
|
applyCompressionBlocks(msgs, state);
|
|
69
69
|
|
|
70
70
|
// 2b. Post-compression safety net: remove any orphaned tool pairs that the
|
|
@@ -83,7 +83,9 @@ export function applyPruning(
|
|
|
83
83
|
// 6. Apply explicit tool output pruning (prunedToolIds)
|
|
84
84
|
applyToolOutputPruning(msgs, state);
|
|
85
85
|
|
|
86
|
-
// 7. Refresh
|
|
86
|
+
// 7. Refresh message ID snapshots used by the compress tool. Markers are
|
|
87
|
+
// never injected into provider-visible content; the before_provider_request
|
|
88
|
+
// hook delivers the id map as a hidden control payload.
|
|
87
89
|
injectMessageIds(msgs, state, { config });
|
|
88
90
|
|
|
89
91
|
// 8. state.messageIdSnapshot/messageMetaSnapshot are already updated by injectMessageIds
|
|
@@ -45,13 +45,13 @@ async function listSessionIds(sessionDir: string): Promise<string[]> {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function resolveDcpStateDir(ctx: ExtensionContext): string | undefined {
|
|
48
|
-
const sessionDir = ctx.sessionManager
|
|
48
|
+
const sessionDir = ctx.sessionManager?.getSessionDir?.()
|
|
49
49
|
if (!sessionDir) return undefined
|
|
50
50
|
return join(sessionDir, DCP_STATE_DIR)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function resolveDcpStatePath(ctx: ExtensionContext): string | undefined {
|
|
54
|
-
const sessionId = ctx.sessionManager
|
|
54
|
+
const sessionId = ctx.sessionManager?.getSessionId?.()
|
|
55
55
|
const stateDir = resolveDcpStateDir(ctx)
|
|
56
56
|
if (!sessionId || !stateDir) return undefined
|
|
57
57
|
return join(stateDir, safeSessionFileName(sessionId))
|
|
@@ -76,6 +76,35 @@ export async function loadDcpState(ctx: ExtensionContext): Promise<SerializedDcp
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Load the DCP sidecar for an arbitrary session file path, e.g. the previous
|
|
81
|
+
* session during fork/resume/new. Resolves the sidecar via the session file's
|
|
82
|
+
* first-line session id rather than the live session manager, so it works
|
|
83
|
+
* independent of the current ctx.sessionManager state.
|
|
84
|
+
*/
|
|
85
|
+
export async function loadDcpStateFromSessionFile(
|
|
86
|
+
sessionFile: string,
|
|
87
|
+
): Promise<SerializedDcpState | undefined> {
|
|
88
|
+
if (!sessionFile) return undefined
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const firstLine = (await readFile(sessionFile, "utf8")).split("\n", 1)[0]?.trim()
|
|
92
|
+
if (!firstLine) return undefined
|
|
93
|
+
const parsed = JSON.parse(firstLine) as { type?: string; id?: unknown }
|
|
94
|
+
if (parsed.type !== "session" || typeof parsed.id !== "string" || !parsed.id) {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
const stateDir = join(dirname(sessionFile), DCP_STATE_DIR)
|
|
98
|
+
const statePath = join(stateDir, safeSessionFileName(parsed.id))
|
|
99
|
+
const text = await readFile(statePath, "utf8")
|
|
100
|
+
return JSON.parse(text) as SerializedDcpState
|
|
101
|
+
} catch {
|
|
102
|
+
// A missing/unreadable sidecar for the previous session (e.g. a fresh
|
|
103
|
+
// fork with no prior compression) means there is simply nothing to inherit.
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
79
108
|
export async function cleanupStaleDcpStateFiles(ctx: ExtensionContext): Promise<number> {
|
|
80
109
|
const stateDir = resolveDcpStateDir(ctx)
|
|
81
110
|
const sessionDir = ctx.sessionManager.getSessionDir()
|
|
@@ -39,13 +39,13 @@ export interface ToolRecord {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface MessageIdMeta {
|
|
42
|
-
/** The actual message.timestamp associated with the visible DCP id. */
|
|
42
|
+
/** The actual message.timestamp associated with the model-visible DCP id. */
|
|
43
43
|
timestamp: number
|
|
44
44
|
/** Stable raw message key when available; falls back to timestamp-derived key. */
|
|
45
45
|
stableId?: string
|
|
46
46
|
/** The message role at the time the id was injected. */
|
|
47
47
|
role: string
|
|
48
|
-
/** Present when this
|
|
48
|
+
/** Present when this addressable message represents an active compression block. */
|
|
49
49
|
blockId?: number
|
|
50
50
|
/** Tool call metadata for tool-result-like messages. */
|
|
51
51
|
toolCallId?: string
|
|
@@ -54,7 +54,7 @@ export interface MessageIdMeta {
|
|
|
54
54
|
text?: string
|
|
55
55
|
/** Rough token estimate for priority/candidate guidance. */
|
|
56
56
|
tokenEstimate?: number
|
|
57
|
-
/** Optional compression priority marker exposed with the visible message ID. */
|
|
57
|
+
/** Optional compression priority marker exposed with the model-visible message ID. */
|
|
58
58
|
priority?: "low" | "medium" | "high"
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -162,7 +162,7 @@ export interface DcpState {
|
|
|
162
162
|
* message positions by timestamp (which is stable across pruning passes).
|
|
163
163
|
*/
|
|
164
164
|
messageIdSnapshot: Map<string, number>
|
|
165
|
-
/** Extra metadata for the visible DCP message IDs in messageIdSnapshot. */
|
|
165
|
+
/** Extra metadata for the model-visible DCP message IDs in messageIdSnapshot. */
|
|
166
166
|
messageMetaSnapshot: Map<string, MessageIdMeta>
|
|
167
167
|
|
|
168
168
|
// ── Turn tracking ──────────────────────────────────────────────────────────
|
|
@@ -278,6 +278,64 @@ export function resetState(state: DcpState): void {
|
|
|
278
278
|
state.lastNudge = undefined
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Merge compression blocks (and their accounting) from a source sidecar into
|
|
283
|
+
* the current state without clobbering other fields. Used at session_start to
|
|
284
|
+
* carry compression state across fork/resume/new into a session whose own
|
|
285
|
+
* sidecar is empty.
|
|
286
|
+
*
|
|
287
|
+
* Returns the number of newly added blocks (blocks whose id already exists are
|
|
288
|
+
* skipped). Accounting is carried over so inherited blocks are neither lost
|
|
289
|
+
* nor double-counted when later folded by a new compression range.
|
|
290
|
+
*/
|
|
291
|
+
export function inheritCompressionBlocks(state: DcpState, data: unknown): number {
|
|
292
|
+
if (!data || typeof data !== "object") return 0
|
|
293
|
+
const saved = data as Partial<SerializedDcpState>
|
|
294
|
+
if (!Array.isArray(saved.compressionBlocks) || saved.compressionBlocks.length === 0) return 0
|
|
295
|
+
|
|
296
|
+
const existingIds = new Set(state.compressionBlocks.map((b) => b.id))
|
|
297
|
+
const validBlocks = saved.compressionBlocks
|
|
298
|
+
.filter(
|
|
299
|
+
(b: any) =>
|
|
300
|
+
b && Number.isFinite(b.startTimestamp) && Number.isFinite(b.endTimestamp),
|
|
301
|
+
)
|
|
302
|
+
.map((b: any) => ({
|
|
303
|
+
...b,
|
|
304
|
+
anchorTimestamp: Number.isFinite(b.anchorTimestamp)
|
|
305
|
+
? b.anchorTimestamp
|
|
306
|
+
: b.endTimestamp + 1,
|
|
307
|
+
})) as CompressionBlock[]
|
|
308
|
+
|
|
309
|
+
const toAdd = validBlocks.filter((b) => !existingIds.has(b.id))
|
|
310
|
+
if (toAdd.length === 0) return 0
|
|
311
|
+
|
|
312
|
+
state.compressionBlocks.push(...toAdd)
|
|
313
|
+
state.nextBlockId =
|
|
314
|
+
Math.max(state.nextBlockId, ...state.compressionBlocks.map((b) => b.id)) + 1
|
|
315
|
+
|
|
316
|
+
if (Array.isArray(saved.accountedCompressionBlockIds)) {
|
|
317
|
+
for (const id of saved.accountedCompressionBlockIds) {
|
|
318
|
+
if (typeof id === "number") state.accountedCompressionBlockIds.add(id)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (Array.isArray(saved.compressionTokenSavings)) {
|
|
322
|
+
for (const [id, val] of saved.compressionTokenSavings) {
|
|
323
|
+
if (
|
|
324
|
+
typeof id === "number" &&
|
|
325
|
+
typeof val === "number" &&
|
|
326
|
+
!state.compressionTokenSavings.has(id)
|
|
327
|
+
) {
|
|
328
|
+
state.compressionTokenSavings.set(id, val)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (typeof saved.tokensSaved === "number" && saved.tokensSaved > state.tokensSaved) {
|
|
333
|
+
state.tokensSaved = saved.tokensSaved
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return toAdd.length
|
|
337
|
+
}
|
|
338
|
+
|
|
281
339
|
/**
|
|
282
340
|
* Compact tool record for persistence — strips outputText, outputDetails,
|
|
283
341
|
* and truncates/summarises inputArgs to keep serialized state bounded.
|
|
@@ -11,6 +11,16 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
|
|
|
11
11
|
// Vision-capable model used by GLM's lookup tool. Remove or set to null to disable lookup.
|
|
12
12
|
"lookupModel": "openai-codex/gpt-5.4-mini",
|
|
13
13
|
"terminalBell": { "sound": true },
|
|
14
|
+
// comment-checker: nudges the agent to remove AI-slop code comments it just
|
|
15
|
+
// added via write/edit/apply_patch. Net-new comments are classified and a
|
|
16
|
+
// short notice is appended to the tool result when they look unnecessary
|
|
17
|
+
// (filler phrasing, restating code, decorative separators, generic
|
|
18
|
+
// paraphrasing). TODO/FIXME, license headers, docstrings, pragmas, linter
|
|
19
|
+
// directives, and shebangs are never flagged. Strictness:
|
|
20
|
+
// "conservative" — only obvious AI-slop (filler/decorative);
|
|
21
|
+
// "balanced" — + restate-code + generic-explanation (default);
|
|
22
|
+
// "aggressive" — any non-valuable net-new comment.
|
|
23
|
+
"commentChecker": { "enabled": true, "strictness": "balanced" },
|
|
14
24
|
// "telegramMirror": {
|
|
15
25
|
// "enabled": true,
|
|
16
26
|
// "botToken": "123456789:ABCdef...",
|
|
@@ -18,6 +28,14 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
|
|
|
18
28
|
// },
|
|
19
29
|
"dcp": {
|
|
20
30
|
"enabled": true,
|
|
31
|
+
// Write a JSONL debug log of DCP context/prune/compress events to
|
|
32
|
+
// ~/.pi/agent/dcp-debug.jsonl. Also overridable via env
|
|
33
|
+
// PI_DCP_DEBUG=1 / PI_DCP_DEBUG_LOG=/path. Off by default.
|
|
34
|
+
// The log is size-limited and rotated: when it reaches debugLog.maxBytes
|
|
35
|
+
// (default 5 MB) it is renamed to .1, pushing older backups down and
|
|
36
|
+
// dropping the oldest past debugLog.maxBackups (default 3).
|
|
37
|
+
"debug": false,
|
|
38
|
+
"debugLog": { "maxBytes": 5242880, "maxBackups": 3 },
|
|
21
39
|
"manualMode": { "enabled": false, "automaticStrategies": true },
|
|
22
40
|
"modelOverrides": {
|
|
23
41
|
"openai-codex/gpt-5.5": {
|
|
@@ -14,6 +14,7 @@ const MODULES: Array<{ name: string; load: () => Promise<ExtensionModule> }> = [
|
|
|
14
14
|
{ name: "ast-grep", load: () => import("./ast-grep/index") },
|
|
15
15
|
{ name: "async-subagents", load: () => import("./async-subagents/index") },
|
|
16
16
|
{ name: "lsp", load: () => import("./lsp/index") },
|
|
17
|
+
{ name: "comment-checker", load: () => import("./comment-checker/index") },
|
|
17
18
|
{ name: "repo-discovery", load: () => import("./repo-discovery/index") },
|
|
18
19
|
{ name: "antigravity-auth", load: () => import("./antigravity-auth/index") },
|
|
19
20
|
{ name: "opencode-import", load: () => import("./opencode-import/index") },
|