pi-ui-extend 0.1.34 → 0.1.36

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 (73) hide show
  1. package/README.md +20 -0
  2. package/dist/app/app.d.ts +1 -0
  3. package/dist/app/app.js +12 -2
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.d.ts +1 -0
  6. package/dist/app/commands/command-model-actions.js +32 -0
  7. package/dist/app/commands/command-navigation-actions.js +3 -0
  8. package/dist/app/commands/command-session-actions.js +2 -0
  9. package/dist/app/constants.d.ts +2 -1
  10. package/dist/app/constants.js +6 -1
  11. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  12. package/dist/app/extensions/extension-actions-controller.js +4 -0
  13. package/dist/app/input/input-controller.d.ts +5 -1
  14. package/dist/app/input/input-controller.js +122 -16
  15. package/dist/app/input/input-paste-handler.js +3 -1
  16. package/dist/app/input/terminal-edit-shortcuts.d.ts +21 -0
  17. package/dist/app/input/terminal-edit-shortcuts.js +92 -16
  18. package/dist/app/popup/popup-action-controller.d.ts +1 -0
  19. package/dist/app/popup/popup-action-controller.js +1 -0
  20. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  21. package/dist/app/rendering/conversation-entry-renderer.js +1 -1
  22. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  23. package/dist/app/rendering/conversation-tool-renderer.js +21 -0
  24. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  25. package/dist/app/rendering/conversation-viewport.js +41 -5
  26. package/dist/app/rendering/editor-layout-renderer.js +3 -2
  27. package/dist/app/rendering/editor-panels.js +27 -10
  28. package/dist/app/runtime.d.ts +1 -0
  29. package/dist/app/runtime.js +33 -14
  30. package/dist/app/session/session-event-controller.d.ts +7 -0
  31. package/dist/app/session/session-event-controller.js +78 -0
  32. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  33. package/dist/app/session/session-lifecycle-controller.js +7 -0
  34. package/dist/app/session/tabs-controller.d.ts +1 -0
  35. package/dist/app/session/tabs-controller.js +4 -1
  36. package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
  37. package/dist/app/subagents/subagents-widget-controller.js +141 -70
  38. package/dist/app/terminal/terminal-controller.d.ts +10 -0
  39. package/dist/app/terminal/terminal-controller.js +91 -2
  40. package/dist/app/todo/todo-model.js +2 -0
  41. package/dist/app/todo/todo-widget-controller.d.ts +2 -0
  42. package/dist/app/todo/todo-widget-controller.js +17 -7
  43. package/dist/app/types.d.ts +4 -0
  44. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
  45. package/dist/app/workspace/workspace-actions-controller.js +1 -0
  46. package/dist/bundled-extensions/question/tui.js +8 -1
  47. package/dist/bundled-extensions/session-title/index.js +65 -14
  48. package/dist/input-editor-files.js +23 -4
  49. package/dist/markdown-format.d.ts +4 -1
  50. package/dist/markdown-format.js +76 -9
  51. package/external/pi-tools-suite/README.md +71 -1
  52. package/external/pi-tools-suite/package.json +5 -5
  53. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
  55. package/external/pi-tools-suite/src/context-usage.ts +6 -1
  56. package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
  57. package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
  58. package/external/pi-tools-suite/src/dcp/config.ts +142 -6
  59. package/external/pi-tools-suite/src/dcp/index.ts +20 -8
  60. package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
  61. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
  62. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
  63. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  64. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
  65. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
  66. package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
  67. package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
  68. package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
  69. package/external/pi-tools-suite/src/todo/index.ts +87 -16
  70. package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
  71. package/external/pi-tools-suite/src/todo/todo.ts +49 -6
  72. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  73. package/package.json +7 -5
@@ -25,7 +25,7 @@ export interface DcpConfig {
25
25
  modelMinContextLimits?: Record<string, number | string> // same formats as modelMinContextPercent; checked before modelMinContextPercent
26
26
  summaryBuffer: boolean
27
27
  nudgeFrequency: number // inject nudge every N context events (default: 2)
28
- iterationNudgeThreshold: number // nudge after N tool calls since last user msg (default: 8)
28
+ iterationNudgeThreshold: number // nudge after N tool calls since last user msg (default: 4)
29
29
  nudgeForce: "strong" | "soft"
30
30
  protectedTools: string[] // these tool outputs always protected from pruning
31
31
  protectTags: boolean
@@ -67,8 +67,17 @@ export interface DcpConfig {
67
67
  }
68
68
  protectedFilePatterns: string[]
69
69
  pruneNotification: "off" | "minimal" | "detailed"
70
+ modelOverrides: Record<string, DcpConfigOverride>
70
71
  }
71
72
 
73
+ export type DcpConfigOverride = DeepPartial<Omit<DcpConfig, "modelOverrides">>
74
+
75
+ type DeepPartial<T> = T extends Array<infer U>
76
+ ? Array<DeepPartial<U>>
77
+ : T extends object
78
+ ? { [K in keyof T]?: DeepPartial<T[K]> }
79
+ : T
80
+
72
81
  // ---------------------------------------------------------------------------
73
82
  // Defaults
74
83
  // ---------------------------------------------------------------------------
@@ -87,7 +96,7 @@ const DEFAULT_CONFIG: DcpConfig = {
87
96
  modelMinContextPercent: {},
88
97
  summaryBuffer: true,
89
98
  nudgeFrequency: 1,
90
- iterationNudgeThreshold: 6,
99
+ iterationNudgeThreshold: 4,
91
100
  nudgeForce: "soft",
92
101
  protectedTools: ["compress", "write", "edit"],
93
102
  protectTags: false,
@@ -95,14 +104,14 @@ const DEFAULT_CONFIG: DcpConfig = {
95
104
  autoCandidates: {
96
105
  enabled: true,
97
106
  minContextPercent: 0.20,
98
- keepRecentTurns: 2,
107
+ keepRecentTurns: 1,
99
108
  minMessages: 6,
100
109
  minTokens: 1500,
101
110
  },
102
111
  messageMode: {
103
112
  enabled: true,
104
113
  minContextPercent: 0.20,
105
- keepRecentTurns: 2,
114
+ keepRecentTurns: 1,
106
115
  mediumTokens: 500,
107
116
  highTokens: 5000,
108
117
  maxSuggestions: 5,
@@ -120,13 +129,17 @@ const DEFAULT_CONFIG: DcpConfig = {
120
129
  },
121
130
  autoToolPruning: {
122
131
  enabled: true,
123
- maxOutputTokens: 2000,
124
- keepRecentTurns: 2,
132
+ maxOutputTokens: 1200,
133
+ keepRecentTurns: 1,
125
134
  readLikeTools: [
126
135
  "read",
136
+ "shell",
137
+ "bash",
127
138
  "grep",
128
139
  "find",
129
140
  "ls",
141
+ "web_search",
142
+ "web_fetch",
130
143
  "repo_architecture",
131
144
  "repo_structure",
132
145
  "repo_ast",
@@ -141,6 +154,7 @@ const DEFAULT_CONFIG: DcpConfig = {
141
154
  },
142
155
  protectedFilePatterns: [],
143
156
  pruneNotification: "detailed",
157
+ modelOverrides: {},
144
158
  }
145
159
 
146
160
  // ---------------------------------------------------------------------------
@@ -226,6 +240,128 @@ function mergeSuiteDcpConfig(config: DcpConfig, filePath: string): DcpConfig {
226
240
  return deepMerge(config, raw as Partial<DcpConfig>)
227
241
  }
228
242
 
243
+ function normalizeModelKey(key: string | undefined): string | undefined {
244
+ if (typeof key !== "string") return undefined
245
+ const trimmed = key.trim()
246
+ return trimmed.length > 0 ? trimmed : undefined
247
+ }
248
+
249
+ function escapeRegExp(text: string): string {
250
+ return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&")
251
+ }
252
+
253
+ function globToRegExp(pattern: string): RegExp {
254
+ let source = "^"
255
+ for (let i = 0; i < pattern.length; i++) {
256
+ const char = pattern[i]!
257
+ if (char === "*") {
258
+ source += ".*"
259
+ } else if (char === "?") {
260
+ source += "."
261
+ } else {
262
+ source += escapeRegExp(char)
263
+ }
264
+ }
265
+ source += "$"
266
+ return new RegExp(source)
267
+ }
268
+
269
+ function isWildcardPattern(pattern: string): boolean {
270
+ return pattern.includes("*") || pattern.includes("?")
271
+ }
272
+
273
+ function modelPatternMatches(pattern: string, candidate: string): boolean {
274
+ return globToRegExp(pattern).test(candidate)
275
+ }
276
+
277
+ function uniqueModelCandidates(modelKeys: Array<string | undefined>): string[] {
278
+ return modelKeys
279
+ .map((key) => normalizeModelKey(key))
280
+ .filter((key, index, array): key is string => typeof key === "string" && array.indexOf(key) === index)
281
+ }
282
+
283
+ export function matchingModelEntries<T>(
284
+ record: Record<string, T> | undefined,
285
+ modelKeys: Array<string | undefined> = [],
286
+ ): Array<[string, T]> {
287
+ if (!record || Object.keys(record).length === 0) return []
288
+
289
+ const candidates = uniqueModelCandidates(modelKeys)
290
+ if (candidates.length === 0) return []
291
+
292
+ const exactEntries = new Map<string, T>()
293
+ const wildcardEntries: Array<[string, T]> = []
294
+
295
+ for (const [rawKey, value] of Object.entries(record)) {
296
+ const key = normalizeModelKey(rawKey)
297
+ if (!key) continue
298
+ if (isWildcardPattern(key)) wildcardEntries.push([key, value])
299
+ else exactEntries.set(key, value)
300
+ }
301
+
302
+ const fullCandidates = candidates.filter((candidate) => candidate.includes("/"))
303
+ const bareCandidates = candidates.filter((candidate) => !candidate.includes("/"))
304
+ const matches: Array<[string, T]> = []
305
+
306
+ for (const candidate of bareCandidates) {
307
+ for (const entry of wildcardEntries) {
308
+ if (entry[0].includes("/")) continue
309
+ if (modelPatternMatches(entry[0], candidate)) matches.push(entry)
310
+ }
311
+ }
312
+
313
+ for (const candidate of bareCandidates) {
314
+ const value = exactEntries.get(candidate)
315
+ if (value !== undefined) matches.push([candidate, value])
316
+ }
317
+
318
+ for (const candidate of fullCandidates) {
319
+ for (const entry of wildcardEntries) {
320
+ if (!entry[0].includes("/")) continue
321
+ if (modelPatternMatches(entry[0], candidate)) matches.push(entry)
322
+ }
323
+ }
324
+
325
+ for (const candidate of fullCandidates) {
326
+ const value = exactEntries.get(candidate)
327
+ if (value !== undefined) matches.push([candidate, value])
328
+ }
329
+
330
+ return matches
331
+ }
332
+
333
+ export function modelKeysFromContext(ctx: unknown): string[] {
334
+ const ctxModel = (ctx as any)?.model
335
+ const provider = normalizeModelKey(
336
+ ctxModel?.provider ?? ctxModel?.providerId ?? ctxModel?.providerID,
337
+ )
338
+ const model = normalizeModelKey(
339
+ ctxModel?.id ?? ctxModel?.model ?? ctxModel?.modelId ?? ctxModel?.modelID,
340
+ )
341
+
342
+ return [provider && model ? `${provider}/${model}` : undefined, model].filter(
343
+ (key): key is string => typeof key === "string",
344
+ )
345
+ }
346
+
347
+ export function resolveModelConfig(
348
+ config: DcpConfig,
349
+ modelKeys: Array<string | undefined> = [],
350
+ ): DcpConfig {
351
+ const overrides = config.modelOverrides
352
+ if (!overrides || Object.keys(overrides).length === 0) return config
353
+
354
+ const matches = matchingModelEntries(overrides, modelKeys)
355
+ if (matches.length === 0) return config
356
+
357
+ let resolved = deepMerge(config, {})
358
+ for (const [, override] of matches) {
359
+ resolved = deepMerge(resolved, override as Partial<DcpConfig>)
360
+ }
361
+
362
+ return resolved
363
+ }
364
+
229
365
  // ---------------------------------------------------------------------------
230
366
  // Public API
231
367
  // ---------------------------------------------------------------------------
@@ -3,7 +3,7 @@
3
3
  // ---------------------------------------------------------------------------
4
4
 
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
6
- import { loadConfig } from "./config.js"
6
+ import { loadConfig, modelKeysFromContext, resolveModelConfig } from "./config.js"
7
7
  import {
8
8
  createState,
9
9
  resetState,
@@ -102,8 +102,12 @@ function isDcpControlPlaneMessage(message: any): boolean {
102
102
  export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
103
103
  // ── 1. Load config ────────────────────────────────────────────────────────
104
104
  const config = loadConfig()
105
+ const configForContext = (ctx: unknown) => resolveModelConfig(config, modelKeysFromContext(ctx))
106
+ const hasEnabledModelOverride = Object.values(config.modelOverrides).some(
107
+ (override) => override.enabled === true,
108
+ )
105
109
 
106
- if (!config.enabled) return
110
+ if (!config.enabled && !hasEnabledModelOverride) return
107
111
 
108
112
  // ── 2. Create state ───────────────────────────────────────────────────────
109
113
  const state = createState()
@@ -175,6 +179,9 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
175
179
 
176
180
  // ── 7. before_agent_start: inject system prompt ───────────────────────────
177
181
  pi.on("before_agent_start", async (event, _ctx) => {
182
+ const effectiveConfig = configForContext(_ctx)
183
+ if (!effectiveConfig.enabled) return { systemPrompt: event.systemPrompt }
184
+
178
185
  const promptAddition = state.manualMode
179
186
  ? MANUAL_MODE_SYSTEM_PROMPT
180
187
  : SYSTEM_PROMPT
@@ -239,11 +246,15 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
239
246
 
240
247
  // ── 10. context: apply pruning and inject nudges ──────────────────────────
241
248
  pi.on("context", async (event, ctx) => {
249
+ const effectiveConfig = configForContext(ctx)
242
250
  const contextMessages = event.messages.filter((message: any) =>
243
251
  !isUserVisibleOnlyMessage(message) && !isDcpControlPlaneMessage(message)
244
252
  )
253
+ if (!effectiveConfig.enabled) {
254
+ return { messages: contextMessages }
255
+ }
245
256
  annotateMessagesWithBranchEntryIds(contextMessages, ctx)
246
- let prunedMessages = applyPruning(contextMessages, state, config)
257
+ let prunedMessages = applyPruning(contextMessages, state, effectiveConfig)
247
258
  let candidate = null as ReturnType<typeof detectCompressionCandidate>
248
259
  let messageCandidates = [] as ReturnType<typeof detectMessageCompressionCandidates>
249
260
 
@@ -273,11 +284,11 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
273
284
  const ctxModel = (ctx as any).model
274
285
  const provider = ctxModel?.provider ?? ctxModel?.providerId ?? ctxModel?.providerID
275
286
  const model = ctxModel?.id ?? ctxModel?.model ?? ctxModel?.modelId ?? ctxModel?.modelID
276
- const thresholds = resolveContextThresholds(config, [
287
+ const thresholds = resolveContextThresholds(effectiveConfig, [
277
288
  provider && model ? `${provider}/${model}` : undefined,
278
289
  model,
279
290
  ], usage.contextWindow)
280
- if (config.compress.summaryBuffer) {
291
+ if (effectiveConfig.compress.summaryBuffer) {
281
292
  const summaryBonus = getActiveSummaryTokenEstimate(state) / usage.contextWindow
282
293
  thresholds.maxContextPercent += Math.min(summaryBonus, SUMMARY_BUFFER_MAX_CONTEXT_BONUS)
283
294
  }
@@ -292,7 +303,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
292
303
  const nudgeType = getNudgeType(
293
304
  contextPercent,
294
305
  state,
295
- config,
306
+ effectiveConfig,
296
307
  toolCallsSinceLastUser,
297
308
  thresholds,
298
309
  )
@@ -304,12 +315,13 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
304
315
  candidate = detectCompressionCandidate(
305
316
  prunedMessages,
306
317
  state,
307
- config,
318
+ effectiveConfig,
308
319
  contextPercent,
309
320
  )
310
321
  messageCandidates = detectMessageCompressionCandidates(
311
322
  prunedMessages,
312
- config,
323
+ state,
324
+ effectiveConfig,
313
325
  contextPercent,
314
326
  )
315
327
 
@@ -26,7 +26,9 @@ 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. After any completed implementation + verification slice, compress that closed slice before replying or starting a new task unless the next step needs exact raw logs/diffs. Do not carry large stale tool outputs across task boundaries.
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.
30
+
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.
30
32
 
31
33
  Before compressing while work is unfinished, ensure one \`todo in_progress\` captures the active objective and next step.
32
34
 
@@ -38,6 +40,7 @@ CADENCE, SIGNALS, AND LATENCY
38
40
  - Prioritize closedness and independence over raw size
39
41
  - Prefer smaller, regular compressions over infrequent massive compressions for better latency and summary quality
40
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
41
44
 
42
45
  COMPRESS WHEN
43
46
 
@@ -45,6 +48,8 @@ A section is genuinely closed and the raw conversation has served its purpose:
45
48
 
46
49
  - Research concluded and findings are clear
47
50
  - Implementation finished and verified
51
+ - Config/doc edit finished
52
+ - Test, lint, or CI output has been understood, especially passing logs
48
53
  - Exploration exhausted and patterns understood
49
54
  - Dead-end noise can be discarded without waiting for a whole chapter to close
50
55
 
@@ -77,7 +82,10 @@ It is your responsibility to keep a sharp, high-quality context window for optim
77
82
  export const COMPRESS_RANGE_DESCRIPTION = `Collapse one or more ranges of the conversation into detailed summaries.
78
83
 
79
84
  AUTONOMOUS HOUSEKEEPING
80
- Do not wait for context emergencies. After a completed implementation + verification slice, compress that closed slice before replying or starting a new task unless the next step needs exact raw logs/diffs. Do not carry large stale tool outputs across task boundaries.
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.
86
+
87
+ PASSING LOGS AND LARGE OUTPUTS
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.
81
89
 
82
90
  DCP REMINDERS
83
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.
@@ -87,7 +95,7 @@ Your summary must be COMPLETE FOR CONTINUATION, not a transcript rewrite. Preser
87
95
 
88
96
  If active unfinished work exists, start with \`Active objective\` and \`Next step\`.
89
97
 
90
- Default to a compact structured summary (roughly 5-15 bullets for a normal completed work slice). Grow beyond that only when the compressed range contains multiple independent decisions, unresolved blockers, or precise state that is genuinely required to continue.
98
+ Default to a compact structured summary (roughly 4-10 bullets for a normal completed work slice). Grow beyond that only when the compressed range contains multiple independent decisions, unresolved blockers, or precise state that is genuinely required to continue.
91
99
 
92
100
  Do not copy long raw code, JSON, diffs, logs, or tool output into summaries. Prefer semantic descriptions such as “updated foo.json so scene_assets_1.zai-svg has maxConcurrentRuns set to 5.” Include exact snippets only when the literal text is required for safe continuation, and keep them short and single-line.
93
101
 
@@ -98,7 +106,7 @@ USER INTENT FIDELITY
98
106
  When the compressed range includes user messages, preserve the user's intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
99
107
  Directly quote user messages when they are short enough to include safely. Direct quotes are preferred when they best preserve exact meaning.
100
108
 
101
- Be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool outputs, back-and-forth exploration, and incidental line-by-line edit history. What remains should be pure signal with enough detail to resume work confidently.
109
+ Be LEAN. Strip away full logs, repeated search/read output, duplicate summaries, incidental failed attempts, and line-by-line edit history. What remains should be pure signal with enough detail to resume work confidently.
102
110
 
103
111
  TWO COMPRESSION MODES
104
112
  You may use either or both modes in one call:
@@ -174,7 +182,7 @@ You are at or beyond the configured max context threshold. This is an emergency
174
182
  You MUST use the \`compress\` tool now. Do not continue normal exploration until compression is handled.
175
183
 
176
184
  If you are in the middle of a critical atomic operation, finish that atomic step first, then compress immediately.
177
- If a completed implementation+verification slice exists, compress it before replying or starting another task.
185
+ If any closed slice exists (finished implementation, verification, config/doc edit, answered exploration, dead end, or test/log inspection), compress it before replying or starting another task. Passing logs should become command + pass/fail + follow-up status only.
178
186
 
179
187
  RANGE STRATEGY (MANDATORY)
180
188
  Prioritize one large, closed, high-yield compression range first.
@@ -201,9 +209,9 @@ ACTION REQUIRED: Context usage is high.
201
209
 
202
210
  Before doing more exploration, look for a closed, self-contained range that no longer needs to stay raw and compress it now.
203
211
 
204
- Do not treat this as optional housekeeping. If any completed research, implementation, verification, CI-log inspection, or dead-end debugging slice is present, call the \`compress\` tool before continuing normal work.
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.
205
213
  If a completed implementation+verification slice exists, compress it before replying or starting another task.
206
- High-priority stale tool outputs must be compressed once no exact raw text is needed.
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.
207
215
 
208
216
  RANGE SELECTION
209
217
  Prefer older, resolved history. Avoid the newest active working slice unless it is clearly done.
@@ -225,7 +233,7 @@ If any range is cleanly closed and unlikely to be needed again, use the \`compre
225
233
  If direction has shifted, compress earlier ranges that are now less relevant.
226
234
 
227
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.
228
- High-priority stale tool outputs must be compressed once no exact raw text is needed.
236
+ High-priority stale shell/read/repo/web outputs and understood passing logs must be compressed once no exact raw text is needed.
229
237
 
230
238
  Prefer small, closed-range compressions over one broad compression.
231
239
  Use message-mode compression for isolated large stale messages.
@@ -239,7 +247,7 @@ Keep active context uncompressed.
239
247
  export const ITERATION_NUDGE = `<dcp-system-reminder>
240
248
  ACTION REQUIRED: You've been iterating for a while after the last user message.
241
249
 
242
- 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 CI-log triage, a verified fix, or a dead-end investigation), use the \`compress\` tool on it now.
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.
243
251
 
244
252
  Do not keep accumulating tool outputs while a completed slice remains raw. If a range is closed, compression is the next safe action.
245
253
  If a completed implementation+verification slice exists, compress it before replying or starting another task.
@@ -17,6 +17,55 @@ interface CandidateBoundary {
17
17
  isSystemReminder: boolean;
18
18
  }
19
19
 
20
+ function hasAddressableSnapshot(state: DcpState): boolean {
21
+ return state.messageMetaSnapshot.size > 0 || state.messageIdSnapshot.size > 0;
22
+ }
23
+
24
+ function isActiveBlockId(blockId: number, state: DcpState): boolean {
25
+ return state.compressionBlocks.some((block) => block.id === blockId && block.active);
26
+ }
27
+
28
+ function findCurrentMessageId(msg: any, state: DcpState): string | undefined {
29
+ const role = msg?.role ?? "";
30
+ const timestamp = msg?.timestamp;
31
+ if (!Number.isFinite(timestamp)) return undefined;
32
+
33
+ for (const [id, meta] of state.messageMetaSnapshot) {
34
+ if (meta.timestamp === timestamp && meta.role === role && meta.blockId === undefined) return id;
35
+ }
36
+
37
+ for (const [id, ts] of state.messageIdSnapshot) {
38
+ if (ts === timestamp) return id;
39
+ }
40
+
41
+ return undefined;
42
+ }
43
+
44
+ function resolveAddressableBoundaryId(
45
+ msg: any,
46
+ state: DcpState,
47
+ options: { allowBlocks: boolean },
48
+ ): { id: string; blockId?: number; text: string } | null {
49
+ const text = messageText(msg);
50
+ const blockId = extractBlockId(text);
51
+ if (blockId !== undefined) {
52
+ if (options.allowBlocks && isActiveBlockId(blockId, state)) return { id: `b${blockId}`, blockId, text };
53
+ if (!hasAddressableSnapshot(state) && options.allowBlocks) return { id: `b${blockId}`, blockId, text };
54
+ return null;
55
+ }
56
+
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
+
65
+ const currentId = findCurrentMessageId(msg, state);
66
+ return currentId ? { id: currentId, text } : null;
67
+ }
68
+
20
69
  export function detectCompressionCandidate(
21
70
  messages: any[],
22
71
  _state: DcpState,
@@ -29,19 +78,16 @@ export function detectCompressionCandidate(
29
78
 
30
79
  const boundaries: CandidateBoundary[] = [];
31
80
  for (const msg of messages) {
32
- const text = messageText(msg);
33
- const blockId = extractBlockId(text);
34
- const messageId = extractMessageId(text);
35
- const id = blockId !== undefined ? `b${blockId}` : messageId;
36
- if (!id) continue;
81
+ const boundary = resolveAddressableBoundaryId(msg, _state, { allowBlocks: true });
82
+ if (!boundary) continue;
37
83
  if (!Number.isFinite(msg.timestamp)) continue;
38
84
  boundaries.push({
39
- id,
85
+ id: boundary.id,
40
86
  role: msg.role ?? "",
41
87
  timestamp: msg.timestamp,
42
88
  tokenEstimate: estimateMessageTokens(msg),
43
- blockId,
44
- isSystemReminder: text.includes("<dcp-system-reminder>"),
89
+ blockId: boundary.blockId,
90
+ isSystemReminder: boundary.text.includes("<dcp-system-reminder>"),
45
91
  });
46
92
  }
47
93
 
@@ -102,6 +148,7 @@ export function formatCompressionCandidateHint(candidate: CompressionCandidate):
102
148
 
103
149
  export function detectMessageCompressionCandidates(
104
150
  messages: any[],
151
+ state: DcpState,
105
152
  config: DcpConfig,
106
153
  contextPercent: number,
107
154
  ): MessageCompressionCandidate[] {
@@ -111,18 +158,15 @@ export function detectMessageCompressionCandidates(
111
158
 
112
159
  const boundaries: CandidateBoundary[] = [];
113
160
  for (const msg of messages) {
114
- const text = messageText(msg);
115
- const blockId = extractBlockId(text);
116
- const messageId = extractMessageId(text);
117
- if (!messageId || blockId !== undefined) continue;
161
+ const boundary = resolveAddressableBoundaryId(msg, state, { allowBlocks: false });
162
+ if (!boundary || boundary.blockId !== undefined) continue;
118
163
  if (!Number.isFinite(msg.timestamp)) continue;
119
164
  boundaries.push({
120
- id: messageId,
165
+ id: boundary.id,
121
166
  role: msg.role ?? "",
122
167
  timestamp: msg.timestamp,
123
168
  tokenEstimate: estimateMessageTokens(msg),
124
- blockId,
125
- isSystemReminder: text.includes("<dcp-system-reminder>"),
169
+ isSystemReminder: boundary.text.includes("<dcp-system-reminder>"),
126
170
  });
127
171
  }
128
172
 
@@ -1,4 +1,4 @@
1
- import type { DcpConfig } from "./config.js";
1
+ import { matchingModelEntries, type DcpConfig } from "./config.js";
2
2
  import type { DcpState } from "./state.js";
3
3
  import type { NudgeThresholds } from "./pruner-types.js";
4
4
  import { createRequire } from "node:module";
@@ -107,8 +107,6 @@ export function resolveContextThresholds(
107
107
  modelKeys: Array<string | undefined> = [],
108
108
  contextWindow?: number,
109
109
  ): Required<NudgeThresholds> {
110
- const candidates = modelKeys.filter((key): key is string => typeof key === "string" && key.length > 0);
111
-
112
110
  const resolveThresholdValue = (value: number | string | undefined): number | undefined => {
113
111
  if (typeof value === "string") {
114
112
  const trimmed = value.trim();
@@ -130,12 +128,12 @@ export function resolveContextThresholds(
130
128
  };
131
129
 
132
130
  const resolveOverride = (map: Record<string, number | string> | undefined): number | undefined => {
133
- if (!map) return undefined;
134
- for (const key of candidates) {
135
- const value = resolveThresholdValue(map[key]);
136
- if (value !== undefined) return value;
131
+ let resolved: number | undefined;
132
+ for (const [, rawValue] of matchingModelEntries(map, modelKeys)) {
133
+ const value = resolveThresholdValue(rawValue);
134
+ if (value !== undefined) resolved = value;
137
135
  }
138
- return undefined;
136
+ return resolved;
139
137
  };
140
138
 
141
139
  const min =
@@ -157,7 +157,7 @@ function formatCandidateActions(
157
157
 
158
158
  if (candidate) {
159
159
  parts.push(
160
- `Recommended range candidate: ${candidate.startId}..${candidate.endId} (${candidate.messageCount} messages, ~${candidate.estimatedTokens} tokens, ${candidate.reason}). Compress this before the next search/read/test if it is closed.`,
160
+ `Recommended range candidate: ${candidate.startId}..${candidate.endId} (${candidate.messageCount} messages, ~${candidate.estimatedTokens} tokens, ${candidate.reason}). Compress this before the next search/read/test/web lookup if it is closed.`,
161
161
  );
162
162
  if (candidate.includedBlockIds.length > 0) {
163
163
  parts.push(
@@ -172,7 +172,7 @@ function formatCandidateActions(
172
172
  parts.push(
173
173
  `Recommended message candidates: ${listedMessages
174
174
  .map((item) => `${item.messageId} (${item.priority}, ${item.role}, ~${item.estimatedTokens} tokens)`)
175
- .join(", ")}. High-priority stale messages MUST be compressed once their full text is no longer needed. Batch multiple messages in one compress call when possible.`,
175
+ .join(", ")}. High-priority stale messages MUST be compressed once their full text is no longer needed; passing logs should become command + pass/fail + follow-up status only. Batch multiple messages in one compress call when possible.`,
176
176
  );
177
177
  }
178
178
 
@@ -180,7 +180,7 @@ function formatCandidateActions(
180
180
  if (activeBlocks) parts.push(activeBlocks);
181
181
 
182
182
  if (parts.length === 0) {
183
- parts.push("No automatic candidate is certain; scan the older closed context now and compress any completed research, implementation, verification, CI-log inspection, or dead-end debugging slice before accumulating more tool output.");
183
+ parts.push("No automatic candidate is certain; scan the older closed context now and compress any completed research, implementation, config/doc edit, verification, CI-log inspection, or dead-end debugging slice before accumulating more tool output.");
184
184
  }
185
185
 
186
186
  return [`CONCRETE NEXT ACTION`, ...parts].join("\n");
@@ -19,11 +19,61 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
19
19
  "dcp": {
20
20
  "enabled": true,
21
21
  "manualMode": { "enabled": false, "automaticStrategies": true },
22
+ "modelOverrides": {
23
+ "openai-codex/gpt-5.5": {
24
+ "compress": {
25
+ "minContextPercent": "28%",
26
+ "maxContextPercent": "48%"
27
+ }
28
+ },
29
+ "openai-codex/gpt-5.4-mini": {
30
+ "compress": {
31
+ "minContextPercent": "20%",
32
+ "maxContextPercent": "38%"
33
+ }
34
+ },
35
+ "zai/*": {
36
+ "compress": {
37
+ "minContextPercent": "16%",
38
+ "maxContextPercent": "30%"
39
+ }
40
+ },
41
+ "antigravity/*sonnet*": {
42
+ "compress": {
43
+ "minContextPercent": "22%",
44
+ "maxContextPercent": "40%"
45
+ }
46
+ },
47
+ "antigravity/gemini-3.1-pro*": {
48
+ "compress": {
49
+ "minContextPercent": "24%",
50
+ "maxContextPercent": "42%"
51
+ }
52
+ },
53
+ "antigravity/gemini-3-flash*": {
54
+ "compress": {
55
+ "minContextPercent": "18%",
56
+ "maxContextPercent": "34%"
57
+ }
58
+ },
59
+ "antigravity/gemini-2.5-flash*": {
60
+ "compress": {
61
+ "minContextPercent": "18%",
62
+ "maxContextPercent": "32%"
63
+ }
64
+ },
65
+ "antigravity/antigravity-claude-opus-4-6-thinking": {
66
+ "compress": {
67
+ "minContextPercent": "26%",
68
+ "maxContextPercent": "44%"
69
+ }
70
+ }
71
+ },
22
72
  "compress": {
23
73
  "minContextPercent": "20%",
24
74
  "maxContextPercent": "55%",
25
75
  "nudgeFrequency": 1,
26
- "iterationNudgeThreshold": 6,
76
+ "iterationNudgeThreshold": 4,
27
77
  "autoCandidates": { "minContextPercent": 0.2 },
28
78
  "messageMode": { "minContextPercent": 0.2 }
29
79
  }