pi-ui-extend 1.0.39 → 1.0.41

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 (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
@@ -0,0 +1,113 @@
1
+ import type { DcpConfig } from "./config.js"
2
+ import { canonicalMessageHash, copyRawMutationHash } from "./conversation-index.js"
3
+ import { cloneDcpTransactionState } from "./state-transaction.js"
4
+ import type { CompressionBlock, ConversationIndexEntry, DcpState } from "./state.js"
5
+ import { stripStaleDcpMetadataFromMessage, estimateMessageTokens } from "./pruner-metadata.js"
6
+ import { injectMessageIds, stableMessageKeys } from "./pruner-message-ids.js"
7
+ import { applyCompressionBlocks } from "./pruner-compression-blocks.js"
8
+
9
+ /** The latest provider projection is runtime-only and intentionally never persisted. */
10
+ const projectionSnapshots = new WeakMap<ConversationIndexEntry[], any[]>()
11
+
12
+ const DCP_PROVENANCE_KEYS = ["_dcpOrigin", "_dcpStableId", "_dcpBlockId"] as const
13
+
14
+ function copyDcpProvenance(source: any, target: any): any {
15
+ if (!source || !target || typeof source !== "object" || typeof target !== "object") return target
16
+ for (const key of DCP_PROVENANCE_KEYS) {
17
+ const descriptor = Object.getOwnPropertyDescriptor(source, key)
18
+ if (descriptor) Object.defineProperty(target, key, descriptor)
19
+ }
20
+ copyRawMutationHash(source, target)
21
+ return target
22
+ }
23
+
24
+ /** Clone message objects while retaining the non-enumerable DCP provenance used by exact spans. */
25
+ export function cloneProjectionMessages(messages: readonly any[]): any[] {
26
+ return messages.map((message) => {
27
+ const clone = message && typeof message === "object" ? { ...message } : message
28
+ if (!clone || typeof clone !== "object") return clone
29
+ if (message.content && typeof message.content === "object") clone.content = structuredClone(message.content)
30
+ return copyDcpProvenance(message, clone)
31
+ })
32
+ }
33
+
34
+ /** Store a detached copy of the exact provider projection that produced this index. */
35
+ export function recordCompressionProjection(
36
+ entries: ConversationIndexEntry[],
37
+ messages: readonly any[],
38
+ ): void {
39
+ projectionSnapshots.set(entries, cloneProjectionMessages(messages))
40
+ }
41
+
42
+ function verifiedSnapshot(state: DcpState): any[] {
43
+ const snapshot = projectionSnapshots.get(state.conversationIndexSnapshot)
44
+ if (!snapshot || snapshot.length !== state.conversationIndexSnapshot.length) {
45
+ throw new Error(
46
+ "Manual compression requires a current verifiable provider projection; refresh the DCP context and retry.",
47
+ )
48
+ }
49
+ const stableKeys = stableMessageKeys(snapshot)
50
+ for (let index = 0; index < snapshot.length; index++) {
51
+ const entry = state.conversationIndexSnapshot[index]
52
+ if (!entry || entry.stableId !== stableKeys[index] || entry.contentHash !== canonicalMessageHash(snapshot[index])) {
53
+ throw new Error(
54
+ "Manual compression provider projection no longer matches the current DCP snapshot; refresh the context and retry.",
55
+ )
56
+ }
57
+ }
58
+ return snapshot
59
+ }
60
+
61
+ function stripProjectionMetadata(message: any): any {
62
+ const stripped = stripStaleDcpMetadataFromMessage(message)
63
+ return copyDcpProvenance(message, stripped)
64
+ }
65
+
66
+ export interface ManualCompressionProjection {
67
+ projectedBeforeTokens: number
68
+ projectedAfterTokens: number
69
+ netGain: number
70
+ }
71
+
72
+ /**
73
+ * Materialize only an operation's new blocks against the exact current
74
+ * provider projection. Existing blocks are already represented there as
75
+ * synthetic entries, so replaying them would double-count and hide a bad
76
+ * manual summary behind unrelated automatic pruning.
77
+ */
78
+ export function previewManualCompressionProjection(
79
+ state: DcpState,
80
+ config: DcpConfig,
81
+ newBlocks: CompressionBlock[],
82
+ ): ManualCompressionProjection {
83
+ const source = verifiedSnapshot(state)
84
+ const before = cloneProjectionMessages(source)
85
+ const after = cloneProjectionMessages(source).map(stripProjectionMetadata)
86
+ const previewState = cloneDcpTransactionState(state)
87
+ previewState.compressionBlocks = newBlocks.map((block) => structuredClone(block))
88
+ previewState.accountedCompressionBlockIds = new Set()
89
+ previewState.compressionTokenSavings = new Map()
90
+
91
+ applyCompressionBlocks(after, previewState)
92
+ const appliedBlockIds = new Set(
93
+ after.flatMap((message) => Number.isInteger(message?._dcpBlockId) ? [message._dcpBlockId as number] : []),
94
+ )
95
+ const missing = newBlocks.filter((block) => !appliedBlockIds.has(block.id))
96
+ if (missing.length > 0) {
97
+ throw new Error(
98
+ `Manual compression could not materialize ${missing.map((block) => `b${block.id}`).join(", ")} in the current provider projection; no state was published.`,
99
+ )
100
+ }
101
+
102
+ // Recreate carrier overhead on a detached state. This deliberately happens
103
+ // after applying the wrapper so the comparison includes the full delivered
104
+ // provider projection rather than just summary text.
105
+ injectMessageIds(after, previewState, { config })
106
+ const projectedBeforeTokens = before.reduce((sum, message) => sum + estimateMessageTokens(message), 0)
107
+ const projectedAfterTokens = after.reduce((sum, message) => sum + estimateMessageTokens(message), 0)
108
+ return {
109
+ projectedBeforeTokens,
110
+ projectedAfterTokens,
111
+ netGain: projectedBeforeTokens - projectedAfterTokens,
112
+ }
113
+ }
@@ -0,0 +1,70 @@
1
+ import type { DcpState } from "./state.js"
2
+
3
+ /** Routine recovery aims for up to five percentage points, never below min. */
4
+ export function routineRecoveryTokens(tokens: number, window: number, minPercent: number): number {
5
+ return Math.max(1, Math.ceil(Math.min(window * 0.05, Math.max(0, tokens - window * minPercent))))
6
+ }
7
+
8
+ export interface CompressionProgressInput {
9
+ projectedTokens: number
10
+ contextWindow: number
11
+ requiredTokens: number
12
+ kind: "routine" | "emergency"
13
+ observedTokens?: number
14
+ targetHeadroomTokens?: number
15
+ }
16
+
17
+ /** Remaining debt follows new content/usage, not a stale pre-compression sample. */
18
+ export function outstandingCompressionTokens(state: DcpState, input: CompressionProgressInput): number | undefined {
19
+ const previous = state.compressionProgress
20
+ if (!previous || previous.contextWindow !== input.contextWindow) return undefined
21
+ const projectionChange = input.projectedTokens - previous.projectedTokens
22
+ const observedGrowth = input.observedTokens !== undefined && previous.observedTokens !== undefined
23
+ ? Math.max(0, input.observedTokens - previous.observedTokens) : 0
24
+ const growth = observedGrowth > 0 ? Math.max(projectionChange, observedGrowth) : projectionChange
25
+ const stricterTarget = previous.kind === input.kind && previous.targetHeadroomTokens !== undefined && input.targetHeadroomTokens !== undefined
26
+ ? Math.max(0, previous.targetHeadroomTokens - input.targetHeadroomTokens) : 0
27
+ return Math.max(0, previous.remainingTokens + growth + stricterTarget)
28
+ }
29
+
30
+ /** Follow actual projection growth, not context callback/retry count. */
31
+ export function trackCompressionProgress(
32
+ state: DcpState,
33
+ input: CompressionProgressInput,
34
+ ): void {
35
+ const previous = state.compressionProgress
36
+ const remaining = outstandingCompressionTokens(state, input)
37
+ state.compressionProgress = {
38
+ projectedTokens: input.projectedTokens,
39
+ contextWindow: input.contextWindow,
40
+ // The unchanged native sample still describes the input before a manual
41
+ // commit. Do not charge the same already-recovered tokens on every callback.
42
+ remainingTokens: previous && previous.kind === input.kind && remaining !== undefined && remaining > 0
43
+ ? remaining : Math.max(1, input.requiredTokens, remaining ?? 0),
44
+ kind: input.kind,
45
+ observedTokens: input.observedTokens,
46
+ targetHeadroomTokens: previous?.kind === input.kind && previous.targetHeadroomTokens !== undefined && input.targetHeadroomTokens !== undefined
47
+ ? Math.min(previous.targetHeadroomTokens, input.targetHeadroomTokens) : input.targetHeadroomTokens,
48
+ }
49
+ }
50
+
51
+ export function resetCompressionProgress(state: DcpState): void {
52
+ state.consecutiveIgnoredStrongNudges = 0
53
+ state.consecutiveIgnoredNudges = 0
54
+ state.compressionProgress = undefined
55
+ }
56
+
57
+ /** Called on a detached state only after positive full-projection gain is proved. */
58
+ export function settleCompressionProgress(state: DcpState, gain: number, projectedAfterTokens: number): boolean {
59
+ const goal = state.compressionProgress
60
+ if (!goal || gain >= goal.remainingTokens) {
61
+ resetCompressionProgress(state)
62
+ return true
63
+ }
64
+ state.compressionProgress = {
65
+ ...goal,
66
+ remainingTokens: goal.remainingTokens - gain,
67
+ projectedTokens: projectedAfterTokens,
68
+ }
69
+ return false
70
+ }
@@ -16,7 +16,6 @@ export interface DcpConfig {
16
16
  }
17
17
  manualMode: {
18
18
  enabled: boolean
19
- automaticStrategies: boolean // run dedup/purge even in manual mode
20
19
  }
21
20
  compress: {
22
21
  maxContextPercent: number | string // accepts a 0-1 fraction, absolute tokens, or a percent string like "80%"
@@ -50,15 +49,16 @@ export interface DcpConfig {
50
49
  maxSuggestions: number
51
50
  }
52
51
  /**
53
- * Auto-compress fallback: when the model ignores repeated context-strong
54
- * nudges above the emergency threshold, DCP creates a compression block
55
- * itself (without waiting for the model). Lossy and irreversible within a
52
+ * Auto-compress fallback: after completed actionable provider opportunities
53
+ * fail to meet the recovery goal, DCP can create a safe compression block,
54
+ * including after routine-reminder escalation. Lossy within a
56
55
  * session — disabled by default; opt in via config.
57
56
  */
58
57
  autoCompress: {
59
58
  enabled: boolean
60
- /** Number of context-strong nudges emitted (and ignored) before DCP
61
- * auto-compresses. The model gets `patience` genuine strong chances. */
59
+ /** Auto fallback is eligible after more than this many completed,
60
+ * correlated opportunities with a delivered actionable reminder.
61
+ * Failed calls and insufficient commits do not reset patience. */
62
62
  patience: number
63
63
  /** Models to try, in order, when producing a model-generated summary.
64
64
  * Empty array → deterministic programmatic digest (no model call). */
@@ -68,23 +68,6 @@ export interface DcpConfig {
68
68
  }
69
69
  }
70
70
  strategies: {
71
- deduplication: {
72
- enabled: boolean
73
- protectedTools: string[]
74
- }
75
- purgeErrors: {
76
- enabled: boolean
77
- turns: number // prune error inputs after N user turns (default: 4)
78
- protectedTools: string[]
79
- }
80
- autoToolPruning: {
81
- enabled: boolean
82
- maxOutputTokens: number
83
- keepRecentTurns: number
84
- readLikeTools: string[]
85
- readLikeTurns: number
86
- protectedTools: string[]
87
- }
88
71
  emergencyCurrentTurnPruning: {
89
72
  /** Enable same-turn candidates and lossy fallback pruning; emergency reminders remain active. */
90
73
  enabled: boolean
@@ -104,7 +87,6 @@ export interface DcpConfig {
104
87
  }
105
88
  }
106
89
  protectedFilePatterns: string[]
107
- pruneNotification: "off" | "minimal" | "detailed"
108
90
  modelOverrides: Record<string, DcpConfigOverride>
109
91
  }
110
92
 
@@ -125,7 +107,6 @@ const DEFAULT_CONFIG: DcpConfig = {
125
107
  debug: false,
126
108
  manualMode: {
127
109
  enabled: false,
128
- automaticStrategies: true,
129
110
  },
130
111
  compress: {
131
112
  maxContextPercent: 0.65,
@@ -162,39 +143,6 @@ const DEFAULT_CONFIG: DcpConfig = {
162
143
  },
163
144
  },
164
145
  strategies: {
165
- deduplication: {
166
- enabled: true,
167
- protectedTools: [],
168
- },
169
- purgeErrors: {
170
- enabled: true,
171
- turns: 4,
172
- protectedTools: [],
173
- },
174
- autoToolPruning: {
175
- enabled: true,
176
- maxOutputTokens: 1200,
177
- keepRecentTurns: 1,
178
- readLikeTools: [
179
- "read",
180
- "shell",
181
- "bash",
182
- "grep",
183
- "find",
184
- "ls",
185
- "web_search",
186
- "web_fetch",
187
- "repo_architecture",
188
- "repo_structure",
189
- "repo_ast",
190
- "repo_search",
191
- "repo_explain",
192
- "repo_deps",
193
- "ast_grep",
194
- ],
195
- readLikeTurns: 3,
196
- protectedTools: [],
197
- },
198
146
  emergencyCurrentTurnPruning: {
199
147
  enabled: true,
200
148
  hardContextPercent: 0.82,
@@ -207,7 +155,6 @@ const DEFAULT_CONFIG: DcpConfig = {
207
155
  },
208
156
  },
209
157
  protectedFilePatterns: [],
210
- pruneNotification: "detailed",
211
158
  modelOverrides: {},
212
159
  }
213
160
 
@@ -285,7 +232,35 @@ function readDcpFromSuiteConfig(filePath: string): Record<string, unknown> {
285
232
  const raw = readJsoncFile(filePath)
286
233
  const dcp = raw["dcp"]
287
234
  if (dcp === null || typeof dcp !== "object" || Array.isArray(dcp)) return {}
288
- return dcp as Record<string, unknown>
235
+ return stripRemovedDcpKeys(dcp as Record<string, unknown>)
236
+ }
237
+
238
+ function stripRemovedDcpKeys(raw: Record<string, unknown>): Record<string, unknown> {
239
+ const cleaned = structuredClone(raw)
240
+ delete cleaned.pruneNotification
241
+
242
+ const manualMode = cleaned.manualMode
243
+ if (manualMode && typeof manualMode === "object" && !Array.isArray(manualMode)) {
244
+ delete (manualMode as Record<string, unknown>).automaticStrategies
245
+ }
246
+
247
+ const strategies = cleaned.strategies
248
+ if (strategies && typeof strategies === "object" && !Array.isArray(strategies)) {
249
+ const record = strategies as Record<string, unknown>
250
+ delete record.deduplication
251
+ delete record.purgeErrors
252
+ delete record.autoToolPruning
253
+ }
254
+
255
+ const modelOverrides = cleaned.modelOverrides
256
+ if (modelOverrides && typeof modelOverrides === "object" && !Array.isArray(modelOverrides)) {
257
+ for (const [key, override] of Object.entries(modelOverrides as Record<string, unknown>)) {
258
+ if (!override || typeof override !== "object" || Array.isArray(override)) continue
259
+ ;(modelOverrides as Record<string, unknown>)[key] = stripRemovedDcpKeys(override as Record<string, unknown>)
260
+ }
261
+ }
262
+
263
+ return cleaned
289
264
  }
290
265
 
291
266
  function mergeSuiteDcpConfig(config: DcpConfig, filePath: string): DcpConfig {
@@ -431,7 +406,7 @@ export interface LoadConfigOptions {
431
406
  */
432
407
  export function loadConfig(options: LoadConfigOptions = {}): DcpConfig {
433
408
  // Layer 1: defaults (deep clone so we never mutate the constant)
434
- let config: DcpConfig = deepMerge(DEFAULT_CONFIG, {})
409
+ let config: DcpConfig = structuredClone(DEFAULT_CONFIG)
435
410
 
436
411
  const homeDir = options.homeDir ?? os.homedir()
437
412
  config = mergeSuiteDcpConfig(config, path.join(homeDir, ".config", "pi", "pi-tools-suite.jsonc"))