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.
Files changed (70) hide show
  1. package/dist/app/app.d.ts +7 -0
  2. package/dist/app/app.js +40 -5
  3. package/dist/app/commands/command-controller.js +1 -0
  4. package/dist/app/commands/command-registry.d.ts +1 -0
  5. package/dist/app/commands/command-registry.js +8 -0
  6. package/dist/app/commands/command-session-actions.d.ts +2 -0
  7. package/dist/app/commands/command-session-actions.js +79 -1
  8. package/dist/app/extensions/extension-actions-controller.d.ts +4 -1
  9. package/dist/app/extensions/extension-actions-controller.js +31 -2
  10. package/dist/app/input/input-controller.d.ts +1 -0
  11. package/dist/app/input/input-controller.js +23 -2
  12. package/dist/app/input/terminal-edit-shortcuts.d.ts +1 -0
  13. package/dist/app/input/terminal-edit-shortcuts.js +7 -0
  14. package/dist/app/input/voice-controller.js +1 -1
  15. package/dist/app/popup/popup-action-controller.d.ts +1 -3
  16. package/dist/app/popup/popup-action-controller.js +1 -5
  17. package/dist/app/rendering/message-content.js +4 -3
  18. package/dist/app/rendering/render-controller.js +21 -38
  19. package/dist/app/rendering/status-line-renderer.d.ts +1 -0
  20. package/dist/app/rendering/status-line-renderer.js +14 -2
  21. package/dist/app/runtime.js +12 -2
  22. package/dist/app/screen/mouse-controller.js +2 -0
  23. package/dist/app/session/session-event-controller.d.ts +7 -0
  24. package/dist/app/session/session-event-controller.js +10 -13
  25. package/dist/app/terminal/terminal-controller.js +1 -0
  26. package/dist/app/terminal/terminal-output-buffer.d.ts +8 -6
  27. package/dist/app/terminal/terminal-output-buffer.js +24 -16
  28. package/dist/bundled-extensions/terminal-bell/index.js +118 -33
  29. package/dist/markdown-format.d.ts +1 -0
  30. package/dist/markdown-format.js +30 -16
  31. package/dist/schemas/pi-tools-suite-schema.d.ts +5 -0
  32. package/dist/schemas/pi-tools-suite-schema.js +5 -0
  33. package/dist/tool-renderers/apply-patch.js +6 -1
  34. package/dist/tool-renderers/patch-normalize.d.ts +24 -0
  35. package/dist/tool-renderers/patch-normalize.js +163 -0
  36. package/external/pi-tools-suite/README.md +3 -2
  37. package/external/pi-tools-suite/package.json +3 -3
  38. package/external/pi-tools-suite/src/antigravity-auth/index.ts +15 -2
  39. package/external/pi-tools-suite/src/antigravity-auth/status.ts +36 -19
  40. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +5 -2
  41. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -2
  42. package/external/pi-tools-suite/src/async-subagents/core/config.ts +8 -3
  43. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +63 -28
  44. package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +9 -4
  45. package/external/pi-tools-suite/src/comment-checker/config.ts +98 -0
  46. package/external/pi-tools-suite/src/comment-checker/detect.ts +215 -0
  47. package/external/pi-tools-suite/src/comment-checker/index.ts +294 -0
  48. package/external/pi-tools-suite/src/dcp/commands.ts +29 -15
  49. package/external/pi-tools-suite/src/dcp/compress-tool.ts +111 -60
  50. package/external/pi-tools-suite/src/dcp/config.ts +10 -6
  51. package/external/pi-tools-suite/src/dcp/debug-log.ts +235 -0
  52. package/external/pi-tools-suite/src/dcp/index.ts +204 -27
  53. package/external/pi-tools-suite/src/dcp/prompts.ts +25 -28
  54. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +6 -10
  55. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +19 -1
  56. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +36 -58
  57. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +18 -0
  58. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  59. package/external/pi-tools-suite/src/dcp/pruner.ts +4 -2
  60. package/external/pi-tools-suite/src/dcp/state-persistence.ts +31 -2
  61. package/external/pi-tools-suite/src/dcp/state.ts +62 -4
  62. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +18 -0
  63. package/external/pi-tools-suite/src/index.ts +1 -0
  64. package/external/pi-tools-suite/src/model-tools/index.ts +11 -3
  65. package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
  66. package/external/pi-tools-suite/src/todo/index.ts +24 -0
  67. package/external/pi-tools-suite/src/tool-descriptions.ts +3 -3
  68. package/external/pi-tools-suite/src/usage/index.ts +18 -4
  69. package/package.json +4 -4
  70. 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
- \`[dcp-id]: # (mNNN)\` markdown reference lines and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
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 steady housekeeping while you work. Closed slice => compress now. Do not start a new search, file-read batch, test, verification run, or web lookup while older completed work remains raw.
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 an active instruction for the next safe action, not as background advice. Before making another exploratory tool call or starting a new subtask, check whether any earlier slice is closed; if yes, call \`compress\` first. Do not merely acknowledge the reminder or postpone it across multiple tool calls.
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
- - No fixed threshold mandates compression
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 more exploration, ask whether an older completed slice can become summary-only; if yes, compress first
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
- AUTONOMOUS HOUSEKEEPING
85
- Do not wait for context emergencies. Closed slice => compress now. A closed slice includes completed implementation, verification, config/doc edit, answered exploration, dead-end debugging, or finished test/log inspection. Compress before starting the next search/read/test/web lookup unless exact raw text is still needed.
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 direct instruction to evaluate compression immediately. If a safe closed range exists, call this tool before further exploration. Skipping compression is acceptable only when the newest raw context is still needed for the next concrete edit, test, or answer.
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 markdown reference metadata lines.
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
- Each message has an ID in a hidden single-line markdown reference metadata line like \`[dcp-id]: # (m001)\`.
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" — steady housekeeping tone.
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, self-contained range that no longer needs to stay raw and compress it now.
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
- Do not treat this as optional housekeeping. If any completed research, implementation, verification, config/doc edit, CI-log inspection, or dead-end debugging slice is present, call the \`compress\` tool before continuing normal work.
213
- If a completed implementation+verification slice exists, compress it before replying or starting another task.
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 only with the next atomic step and re-check immediately afterward.
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
- ACTION REQUIRED: Evaluate the conversation for compressible ranges before continuing.
229
+ CONTEXT CHECK: Evaluate whether compression would materially improve the live context.
231
230
 
232
- If any range is cleanly closed and unlikely to be needed again, use the \`compress\` tool now.
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 defer this across another batch of searches, reads, CI log fetches, or tests. The next safe action should be compression whenever a closed slice exists.
236
- High-priority stale shell/read/repo/web outputs and understood passing logs must be compressed once no exact raw text is needed.
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
- ACTION REQUIRED: You've been iterating for a while after the last user message.
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 call. If there is a closed portion that is unlikely to be referenced immediately (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 now.
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
- Do not keep accumulating tool outputs while a completed slice remains raw. If a range is closed, compression is the next safe action.
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
- \`[dcp-id]: # (mNNN)\` markdown reference lines and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
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
- const messageId = extractMessageId(text);
58
- if (!messageId) return null;
59
- if (!hasAddressableSnapshot(state)) return { id: messageId, text };
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
- return currentId ? { id: currentId, text } : null;
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 formatIdTag(id: string, _priority: "low" | "medium" | "high"): string {
55
- // Keep the provider-visible marker itself short and single-line. Priority is
56
- // retained in state and emitted in message-compression candidate hints.
57
- return `\n[dcp-id]: # (${id})`;
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
- const visible = options.visible ?? true;
66
-
67
- // Clear the snapshots and rebuild
68
- state.messageIdSnapshot.clear();
69
- state.messageMetaSnapshot.clear();
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
- state.messageIdSnapshot.set(id, msg.timestamp);
134
- state.messageMetaSnapshot.set(id, {
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 visible message ID snapshots used by the compress tool.
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.getSessionDir()
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.getSessionId()
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 visible message represents an active compression block. */
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") },