pi-ui-extend 1.0.42 → 1.0.45

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 (233) hide show
  1. package/README.md +41 -12
  2. package/dist/app/app.d.ts +10 -0
  3. package/dist/app/app.js +132 -8
  4. package/dist/app/cli/install.js +2 -2
  5. package/dist/app/commands/command-controller.d.ts +4 -0
  6. package/dist/app/commands/command-controller.js +11 -0
  7. package/dist/app/commands/command-git-actions.d.ts +25 -0
  8. package/dist/app/commands/command-git-actions.js +381 -0
  9. package/dist/app/commands/command-host.d.ts +3 -0
  10. package/dist/app/commands/command-host.js +5 -2
  11. package/dist/app/commands/command-model-actions.d.ts +1 -0
  12. package/dist/app/commands/command-model-actions.js +51 -34
  13. package/dist/app/commands/command-navigation-actions.d.ts +1 -0
  14. package/dist/app/commands/command-navigation-actions.js +16 -3
  15. package/dist/app/commands/command-registry.d.ts +2 -0
  16. package/dist/app/commands/command-registry.js +14 -0
  17. package/dist/app/commands/reload-context-inventory.js +12 -1
  18. package/dist/app/input/autocomplete-controller.js +61 -34
  19. package/dist/app/input/input-action-controller.d.ts +2 -0
  20. package/dist/app/input/input-action-controller.js +8 -1
  21. package/dist/app/input/input-controller.d.ts +2 -1
  22. package/dist/app/input/input-controller.js +7 -2
  23. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  24. package/dist/app/input/voice-controller.d.ts +41 -45
  25. package/dist/app/input/voice-controller.js +351 -419
  26. package/dist/app/model/model-usage-controller.js +17 -7
  27. package/dist/app/model/model-usage-status.d.ts +4 -1
  28. package/dist/app/model/model-usage-status.js +270 -46
  29. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  30. package/dist/app/popup/menu-items-controller.js +37 -21
  31. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  32. package/dist/app/popup/popup-action-controller.js +77 -24
  33. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  34. package/dist/app/popup/popup-menu-controller.js +239 -69
  35. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  36. package/dist/app/rendering/dcp-stats.js +40 -425
  37. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  38. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  39. package/dist/app/rendering/render-controller.js +23 -2
  40. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  41. package/dist/app/rendering/status-line-renderer.js +41 -5
  42. package/dist/app/rendering/tab-line-renderer.js +26 -20
  43. package/dist/app/runtime.d.ts +12 -1
  44. package/dist/app/runtime.js +120 -12
  45. package/dist/app/screen/mouse-controller.d.ts +2 -0
  46. package/dist/app/screen/mouse-controller.js +17 -7
  47. package/dist/app/screen/status-controller.d.ts +4 -0
  48. package/dist/app/screen/status-controller.js +5 -0
  49. package/dist/app/session/lazy-session-manager.js +34 -0
  50. package/dist/app/session/session-event-controller.d.ts +1 -0
  51. package/dist/app/session/session-event-controller.js +10 -1
  52. package/dist/app/session/session-history.d.ts +1 -0
  53. package/dist/app/session/session-history.js +12 -1
  54. package/dist/app/session/session-lifecycle-controller.d.ts +6 -1
  55. package/dist/app/session/session-lifecycle-controller.js +10 -1
  56. package/dist/app/session/tabs-controller.d.ts +25 -1
  57. package/dist/app/session/tabs-controller.js +442 -153
  58. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  59. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  60. package/dist/app/types.d.ts +11 -1
  61. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  62. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  63. package/dist/app/workspace/workspace-undo.js +1 -0
  64. package/dist/bundled-extensions/question/index.js +9 -1
  65. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  66. package/dist/bundled-extensions/question/remote.js +33 -0
  67. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  68. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  69. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  70. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  71. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  72. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  73. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  74. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  75. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  76. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  77. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  78. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  79. package/dist/config.d.ts +21 -2
  80. package/dist/config.js +159 -32
  81. package/dist/default-pix-config.js +20 -10
  82. package/dist/schemas/pi-tools-suite-schema.d.ts +8 -4
  83. package/dist/schemas/pi-tools-suite-schema.js +11 -5
  84. package/dist/schemas/pix-schema.d.ts +12 -2
  85. package/dist/schemas/pix-schema.js +37 -9
  86. package/dist/schemas/tasks-schema.js +1 -1
  87. package/docs/concurrency.md +3 -1
  88. package/docs/desktop-mvp.md +10 -5
  89. package/docs/desktop-task-manager.md +81 -28
  90. package/docs/release.md +30 -6
  91. package/external/pi-tools-suite/README.md +165 -39
  92. package/external/pi-tools-suite/docs/session-recovery.md +7 -0
  93. package/external/pi-tools-suite/docs/subagent-model-pools.md +12 -5
  94. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  95. package/external/pi-tools-suite/package.json +6 -1
  96. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  97. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  98. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  99. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  100. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  101. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  102. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  103. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  104. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  105. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  106. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  107. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  108. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  109. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  110. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  111. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  112. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  113. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  114. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  115. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  116. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  117. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  119. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  127. package/external/pi-tools-suite/src/async-subagents/commands.ts +1 -1
  128. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  129. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +0 -2
  130. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  131. package/external/pi-tools-suite/src/async-subagents/core/config.ts +40 -17
  132. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  133. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  134. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +7 -4
  135. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +34 -24
  136. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  137. package/external/pi-tools-suite/src/async-subagents/lib.ts +9 -1
  138. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +0 -1
  139. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +2 -2
  140. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  141. package/external/pi-tools-suite/src/config.ts +14 -0
  142. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  143. package/external/pi-tools-suite/src/context-gateway/config.ts +66 -1
  144. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  145. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +123 -3
  146. package/external/pi-tools-suite/src/context-gateway/index.ts +137 -4
  147. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +5 -5
  148. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +2 -2
  149. package/external/pi-tools-suite/src/context-gateway/types.ts +8 -1
  150. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  151. package/external/pi-tools-suite/src/dcp/auto-compress.ts +41 -14
  152. package/external/pi-tools-suite/src/dcp/commands.ts +14 -156
  153. package/external/pi-tools-suite/src/dcp/compress-tool.ts +8 -2
  154. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  155. package/external/pi-tools-suite/src/dcp/config.ts +58 -4
  156. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  157. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  158. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  159. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  160. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  161. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  162. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +92 -2
  163. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  164. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  165. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  166. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  167. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  168. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  169. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +68 -16
  170. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  171. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  172. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  173. package/external/pi-tools-suite/src/resource-registry/index.ts +282 -21
  174. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  175. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  176. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  177. package/external/pi-tools-suite/src/tool-descriptions.ts +44 -15
  178. package/package.json +7 -8
  179. package/schemas/pi-tools-suite.json +44 -12
  180. package/schemas/pix.json +109 -15
  181. package/schemas/tasks.json +1 -2
  182. package/skills/simplify/SKILL.md +33 -5
  183. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -196
  184. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  185. package/skills/playwright-cli/SKILL.md +0 -420
  186. package/skills/playwright-cli/references/element-attributes.md +0 -23
  187. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  188. package/skills/playwright-cli/references/request-mocking.md +0 -87
  189. package/skills/playwright-cli/references/running-code.md +0 -241
  190. package/skills/playwright-cli/references/session-management.md +0 -273
  191. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  192. package/skills/playwright-cli/references/storage-state.md +0 -290
  193. package/skills/playwright-cli/references/test-generation.md +0 -142
  194. package/skills/playwright-cli/references/tracing.md +0 -154
  195. package/skills/playwright-cli/references/video-recording.md +0 -147
  196. package/skills/spec-wiki/SKILL.md +0 -55
  197. package/skills/spec-wiki/evals/evals.json +0 -132
  198. package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +0 -14
  199. package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +0 -4
  200. package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +0 -9
  201. package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +0 -17
  202. package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +0 -12
  203. package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +0 -11
  204. package/skills/spec-wiki/evals/files/focused/src/session.ts +0 -5
  205. package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +0 -17
  206. package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +0 -2
  207. package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +0 -3
  208. package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +0 -18
  209. package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +0 -5
  210. package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +0 -24
  211. package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +0 -7
  212. package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +0 -7
  213. package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +0 -17
  214. package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +0 -3
  215. package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +0 -3
  216. package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +0 -4
  217. package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +0 -9
  218. package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +0 -15
  219. package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +0 -6
  220. package/skills/spec-wiki/evals/maintenance_regression.py +0 -311
  221. package/skills/spec-wiki/evals/retrieval-eval.md +0 -37
  222. package/skills/spec-wiki/evals/retrieval-evals.json +0 -37
  223. package/skills/spec-wiki/evals/trigger-evals.json +0 -94
  224. package/skills/spec-wiki/references/bootstrap.md +0 -100
  225. package/skills/spec-wiki/references/focused-spec.md +0 -117
  226. package/skills/spec-wiki/references/index-schema.md +0 -330
  227. package/skills/spec-wiki/references/maintenance.md +0 -134
  228. package/skills/spec-wiki/references/query.md +0 -74
  229. package/skills/spec-wiki/scripts/spec_wiki.py +0 -2076
  230. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  231. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  232. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -2,10 +2,11 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
2
2
  import type { AutocompleteItem } from "@earendil-works/pi-tui"
3
3
  import type { DcpState } from "./state.js"
4
4
  import { modelKeysFromContext, resolveModelConfig, type DcpConfig } from "./config.js"
5
- import type { DcpNudgeType } from "./pruner-types.js"
6
5
  import { isToolRecordProtected, markToolPruned } from "./pruner.js"
7
6
  import { ignoreStaleExtensionContextError, safeGetContextUsage } from "../context-usage.js"
8
7
  import { captureDcpTransactionGuard, cloneDcpTransactionState, runDcpStateTransaction } from "./state-transaction.js"
8
+ import { readDcpJournalBranch } from "./journal.js"
9
+ import { formatDcpStatistics } from "./statistics.js"
9
10
 
10
11
  // ---------------------------------------------------------------------------
11
12
  // Constants
@@ -34,42 +35,6 @@ function fmt(n: number): string {
34
35
  return n.toLocaleString()
35
36
  }
36
37
 
37
- const NUDGE_TYPES: DcpNudgeType[] = ["turn", "iteration", "context-soft", "context-strong"]
38
-
39
- function pct(numerator: number, denominator: number): string {
40
- if (denominator <= 0) return "n/a"
41
- return `${((numerator / denominator) * 100).toFixed(1)}%`
42
- }
43
-
44
- function nudgeLabel(type: DcpNudgeType): string {
45
- switch (type) {
46
- case "context-strong": return "context-strong"
47
- case "context-soft": return "context-soft"
48
- case "iteration": return "iteration"
49
- case "turn": return "turn"
50
- }
51
- }
52
-
53
- function isNudgeType(value: unknown): value is DcpNudgeType {
54
- return typeof value === "string" && (NUDGE_TYPES as string[]).includes(value)
55
- }
56
-
57
- function customEntryData(entry: unknown, customType: string): Record<string, unknown> | undefined {
58
- const record = entry as { type?: unknown; customType?: unknown; data?: unknown }
59
- if (record?.type !== "custom" || record.customType !== customType) return undefined
60
- if (!record.data || typeof record.data !== "object" || Array.isArray(record.data)) return undefined
61
- return record.data as Record<string, unknown>
62
- }
63
-
64
- function branchEntries(ctx: ExtensionCommandContext): any[] {
65
- try {
66
- const branch = ctx.sessionManager?.getBranch?.()
67
- return Array.isArray(branch) ? branch : []
68
- } catch {
69
- return []
70
- }
71
- }
72
-
73
38
  async function staleSafe(action: () => void | Promise<void>): Promise<void> {
74
39
  try {
75
40
  await action()
@@ -78,85 +43,6 @@ async function staleSafe(action: () => void | Promise<void>): Promise<void> {
78
43
  }
79
44
  }
80
45
 
81
- interface DcpNudgeStats {
82
- emitted: number
83
- reapplied: number
84
- upgraded: number
85
- clearedEvents: number
86
- clearedAnchors: number
87
- byType: Record<DcpNudgeType, number>
88
- activeByType: Record<DcpNudgeType, number>
89
- last?: {
90
- type: DcpNudgeType
91
- event: "emitted" | "reapplied" | "upgraded"
92
- createdAt?: number
93
- contextPercent?: number | null
94
- }
95
- }
96
-
97
- function collectNudgeStats(ctx: ExtensionCommandContext, state: DcpState): DcpNudgeStats {
98
- const stats: DcpNudgeStats = {
99
- emitted: 0,
100
- reapplied: 0,
101
- upgraded: 0,
102
- clearedEvents: 0,
103
- clearedAnchors: 0,
104
- byType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
105
- activeByType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
106
- }
107
-
108
- for (const anchor of state.nudgeAnchors) {
109
- if (isNudgeType(anchor.type)) stats.activeByType[anchor.type]++
110
- }
111
-
112
- for (const entry of branchEntries(ctx)) {
113
- const data = customEntryData(entry, "dcp-nudge")
114
- if (!data) continue
115
- const event = data.event
116
- if ((event === "emitted" || event === "reapplied" || event === "upgraded") && isNudgeType(data.type)) {
117
- if (event === "emitted") stats.emitted++
118
- else if (event === "reapplied") stats.reapplied++
119
- else stats.upgraded++
120
- stats.byType[data.type]++
121
- const createdAt = typeof data.createdAt === "number" ? data.createdAt : undefined
122
- const rawContextPercent = data.contextPercent
123
- let contextPercent: number | null | undefined
124
- if (typeof rawContextPercent === "number") contextPercent = rawContextPercent
125
- else if (rawContextPercent === null) contextPercent = null
126
- if (!stats.last || (createdAt ?? 0) >= (stats.last.createdAt ?? 0)) {
127
- stats.last = { type: data.type, event, createdAt, contextPercent }
128
- }
129
- } else if (event === "cleared") {
130
- stats.clearedEvents++
131
- stats.clearedAnchors += typeof data.clearedAnchors === "number" ? Math.max(0, data.clearedAnchors) : 0
132
- }
133
- }
134
-
135
- if (!stats.last && state.lastNudge && isNudgeType(state.lastNudge.type)) {
136
- stats.last = {
137
- type: state.lastNudge.type,
138
- event: "emitted",
139
- createdAt: state.lastNudge.createdAt,
140
- contextPercent: typeof state.lastNudge.contextPercent === "number"
141
- ? state.lastNudge.contextPercent * 100
142
- : undefined,
143
- }
144
- }
145
-
146
- return stats
147
- }
148
-
149
- function formatDate(ts: number | undefined): string {
150
- if (typeof ts !== "number" || !Number.isFinite(ts) || ts <= 0) return "unknown time"
151
- return new Date(ts).toLocaleString()
152
- }
153
-
154
- function formatContextPercent(value: number | null | undefined): string {
155
- if (value === null) return "unknown context"
156
- if (typeof value !== "number" || !Number.isFinite(value)) return "unknown context"
157
- return `${value.toFixed(1)}% context`
158
- }
159
-
160
46
  function sendChatSystemMessage(pi: ExtensionAPI, customType: string, content: string, details?: Record<string, unknown>): void {
161
47
  pi.sendMessage({
162
48
  customType,
@@ -238,44 +124,16 @@ function handleContext(ctx: ExtensionCommandContext, state: DcpState): void {
238
124
  // Stats
239
125
  // ---------------------------------------------------------------------------
240
126
 
241
- function handleStats(pi: ExtensionAPI, ctx: ExtensionCommandContext, state: DcpState): void {
242
- const activeBlocks = state.compressionBlocks.filter((b) => b.active).length
243
- const totalBlocks = state.compressionBlocks.length
244
- const nudgeStats = collectNudgeStats(ctx, state)
245
- const totalNudgeEvents = nudgeStats.emitted + nudgeStats.reapplied + nudgeStats.upgraded
246
- const activeAnchors = state.nudgeAnchors.length
247
- const lines: string[] = []
248
- lines.push("DCP Session Statistics:")
249
- lines.push(` Tokens saved (estimated): ${fmt(state.tokensSaved)}`)
250
- lines.push(` Total pruning operations: ${fmt(state.totalPruneCount)}`)
251
- lines.push(` Compression blocks active: ${activeBlocks} / ${totalBlocks} total`)
252
- lines.push(` Manual mode: ${state.manualMode ? "on" : "off"}`)
253
- lines.push("")
254
- lines.push("Nudge telemetry:")
255
- lines.push(` Sent: ${fmt(nudgeStats.emitted)} emitted, ${fmt(nudgeStats.reapplied)} reapplied, ${fmt(nudgeStats.upgraded)} upgraded`)
256
- lines.push(
257
- ` By type: ${NUDGE_TYPES.map((type) => `${nudgeLabel(type)}=${fmt(nudgeStats.byType[type])}`).join(", ")}`,
258
- )
259
- lines.push(
260
- ` Active anchors: ${fmt(activeAnchors)}${activeAnchors > 0
261
- ? ` (${NUDGE_TYPES.map((type) => `${nudgeLabel(type)}=${fmt(nudgeStats.activeByType[type])}`).join(", ")})`
262
- : ""}`,
263
- )
264
- lines.push(` Cleared after compress: ${fmt(nudgeStats.clearedEvents)} time${nudgeStats.clearedEvents === 1 ? "" : "s"} (${fmt(nudgeStats.clearedAnchors)} anchor${nudgeStats.clearedAnchors === 1 ? "" : "s"})`)
265
- lines.push(` Compliance proxy: ${fmt(nudgeStats.clearedEvents)} compress-after-nudge / ${fmt(totalNudgeEvents)} nudge event${totalNudgeEvents === 1 ? "" : "s"} (${pct(nudgeStats.clearedEvents, totalNudgeEvents)})`)
266
- if (nudgeStats.last) {
267
- lines.push(
268
- ` Last nudge: ${nudgeLabel(nudgeStats.last.type)} ${nudgeStats.last.event} at ${formatDate(nudgeStats.last.createdAt)} (${formatContextPercent(nudgeStats.last.contextPercent)})`,
269
- )
270
- } else {
271
- lines.push(" Last nudge: none recorded")
272
- }
273
-
274
- sendChatSystemMessage(pi, DCP_STATS_MESSAGE_TYPE, lines.join("\n"), {
275
- generatedAt: new Date().toISOString(),
276
- activeAnchors,
277
- nudgeEvents: totalNudgeEvents,
278
- })
127
+ async function handleStats(pi: ExtensionAPI, ctx: ExtensionCommandContext, state: DcpState): Promise<void> {
128
+ const epoch = state.sessionEpoch
129
+ let branch: unknown[] = []
130
+ let historyStatus: "full" | "unavailable" = "full"
131
+ try { branch = await readDcpJournalBranch(ctx) }
132
+ catch { historyStatus = "unavailable" }
133
+ if (state.sessionEpoch !== epoch) return
134
+ sendChatSystemMessage(pi, DCP_STATS_MESSAGE_TYPE,
135
+ formatDcpStatistics({ branch, historyStatus, model: ctx.model, usage: safeGetContextUsage(ctx) }),
136
+ { generatedAt: new Date().toISOString() })
279
137
  }
280
138
 
281
139
  // ---------------------------------------------------------------------------
@@ -290,7 +148,7 @@ async function handleSweep(
290
148
  ): Promise<void> {
291
149
  await ctx.waitForIdle()
292
150
 
293
- const branch = branchEntries(ctx)
151
+ const branch = await readDcpJournalBranch(ctx) as any[]
294
152
 
295
153
  // Build the full set of protected tool names.
296
154
  const protectedTools = new Set<string>([
@@ -480,7 +338,7 @@ export function registerCommands(
480
338
  break
481
339
 
482
340
  case "stats":
483
- handleStats(pi, ctx, state)
341
+ await handleStats(pi, ctx, state)
484
342
  break
485
343
 
486
344
  case "doctor":
@@ -5,7 +5,7 @@
5
5
  import { Type } from "typebox"
6
6
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
7
7
  import type { CompressionMember, DcpState } from "./state.js"
8
- import { modelKeysFromContext, resolveModelConfig, type DcpConfig } from "./config.js"
8
+ import { modelKeysFromContext, resolveModelConfig, summarizerModelRefs, type DcpConfig } from "./config.js"
9
9
  import { captureDcpTransactionGuard, cloneDcpTransactionState, runDcpStateTransaction } from "./state-transaction.js"
10
10
  import { createHash } from "node:crypto"
11
11
  import { clearDcpNudgeAnchors } from "./pruner.js"
@@ -534,7 +534,7 @@ export function registerCompressTool(
534
534
  includedBlockIds: [],
535
535
  reason: "manual delegated summary",
536
536
  },
537
- modelRefs: effectiveConfig.compress.autoCompress.summarizerModel,
537
+ modelRefs: summarizerModelRefs(effectiveConfig.compress.autoCompress),
538
538
  timeoutMs: effectiveConfig.compress.autoCompress.timeoutMs,
539
539
  modelRegistry: (ctx as any).modelRegistry,
540
540
  signal: _signal,
@@ -795,6 +795,12 @@ export function registerCompressTool(
795
795
  for (const block of workingState.compressionBlocks) {
796
796
  if (newBlockIds.includes(block.id)) block.operationRequestHash = requestHash
797
797
  }
798
+ const firstBlock = workingState.compressionBlocks.find((block) => block.id === newBlockIds[0])
799
+ if (firstBlock) firstBlock.commitMetrics = {
800
+ operationId: `manual:${_toolCallId}`, kind: "manual",
801
+ beforeTokens: projection.projectedBeforeTokens,
802
+ afterTokens: projection.projectedAfterTokens, netGainTokens: projection.netGain,
803
+ }
798
804
  assertCurrent()
799
805
  let published = false
800
806
  await persistState(ctx, workingState, {
@@ -27,6 +27,28 @@ export function outstandingCompressionTokens(state: DcpState, input: Compression
27
27
  return Math.max(0, previous.remainingTokens + growth + stricterTarget)
28
28
  }
29
29
 
30
+ /**
31
+ * Bound one automatic recovery plan to what the *current* budget actually
32
+ * requires. `compressionProgress.remainingTokens` is deliberately cumulative:
33
+ * it follows content growth and stricter historical targets across callbacks.
34
+ * That debt is useful for progress accounting, but feeding the whole stale debt
35
+ * back into candidate sizing can make a later, much smaller capacity gap select
36
+ * an uneconomically huge range (and repeatedly fail with non-positive gain).
37
+ */
38
+ export function compressionPlanningTokens(
39
+ state: DcpState,
40
+ input: CompressionProgressInput,
41
+ remainingTokens: number | undefined = outstandingCompressionTokens(state, input),
42
+ ): number {
43
+ const requested = Math.max(1, Math.ceil(input.requiredTokens))
44
+ if (
45
+ remainingTokens === undefined ||
46
+ remainingTokens <= 0 ||
47
+ state.compressionProgress?.kind !== input.kind
48
+ ) return requested
49
+ return Math.min(remainingTokens, requested)
50
+ }
51
+
30
52
  /** Follow actual projection growth, not context callback/retry count. */
31
53
  export function trackCompressionProgress(
32
54
  state: DcpState,
@@ -65,8 +65,12 @@ export interface DcpConfig {
65
65
  * Failed calls and insufficient commits do not reset patience. */
66
66
  patience: number
67
67
  /** Models to try, in order, for auto-compress and explicit generated summaries.
68
+ * The first entry is the primary summarizer. Additional entries remain
69
+ * supported for backward compatibility with the old inline fallback list.
68
70
  * Empty array → deterministic programmatic digest (no model call). */
69
71
  summarizerModel: string[]
72
+ /** Explicit fallback summarizers tried after `summarizerModel`, in order. */
73
+ summarizerFallbackModels: string[]
70
74
  /** Hard ceiling in ms for a summarizer model call on either path. */
71
75
  timeoutMs: number
72
76
  }
@@ -144,6 +148,7 @@ const DEFAULT_CONFIG: DcpConfig = {
144
148
  enabled: false,
145
149
  patience: 2,
146
150
  summarizerModel: [],
151
+ summarizerFallbackModels: [],
147
152
  timeoutMs: 20000,
148
153
  },
149
154
  },
@@ -312,6 +317,55 @@ function normalizeModelKey(key: string | undefined): string | undefined {
312
317
  return trimmed.length > 0 ? trimmed : undefined
313
318
  }
314
319
 
320
+ export function summarizerModelRefs(
321
+ settings: DcpConfig["compress"]["autoCompress"],
322
+ ): string[] {
323
+ const refs = [
324
+ ...(Array.isArray(settings.summarizerModel) ? settings.summarizerModel : []),
325
+ ...(Array.isArray(settings.summarizerFallbackModels) ? settings.summarizerFallbackModels : []),
326
+ ]
327
+ const seen = new Set<string>()
328
+ const result: string[] = []
329
+ for (const ref of refs) {
330
+ if (typeof ref !== "string") continue
331
+ const normalized = ref.trim()
332
+ if (!normalized || seen.has(normalized)) continue
333
+ seen.add(normalized)
334
+ result.push(normalized)
335
+ }
336
+ return result
337
+ }
338
+
339
+ function normalizeModelRefArray(value: unknown): string[] {
340
+ if (!Array.isArray(value)) return []
341
+ const seen = new Set<string>()
342
+ const result: string[] = []
343
+ for (const ref of value) {
344
+ if (typeof ref !== "string") continue
345
+ const normalized = ref.trim()
346
+ if (!normalized || seen.has(normalized)) continue
347
+ seen.add(normalized)
348
+ result.push(normalized)
349
+ }
350
+ return result
351
+ }
352
+
353
+ function normalizeResolvedModelArrays(config: DcpConfig): DcpConfig {
354
+ const autoCompress = config.compress?.autoCompress
355
+ if (!autoCompress || typeof autoCompress !== "object") return config
356
+ return {
357
+ ...config,
358
+ compress: {
359
+ ...config.compress,
360
+ autoCompress: {
361
+ ...autoCompress,
362
+ summarizerModel: normalizeModelRefArray(autoCompress.summarizerModel),
363
+ summarizerFallbackModels: normalizeModelRefArray(autoCompress.summarizerFallbackModels),
364
+ },
365
+ },
366
+ }
367
+ }
368
+
315
369
  function escapeRegExp(text: string): string {
316
370
  return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&")
317
371
  }
@@ -415,17 +469,17 @@ export function resolveModelConfig(
415
469
  modelKeys: Array<string | undefined> = [],
416
470
  ): DcpConfig {
417
471
  const overrides = config.modelOverrides
418
- if (!overrides || Object.keys(overrides).length === 0) return config
472
+ if (!overrides || Object.keys(overrides).length === 0) return normalizeResolvedModelArrays(config)
419
473
 
420
474
  const matches = matchingModelEntries(overrides, modelKeys)
421
- if (matches.length === 0) return config
475
+ if (matches.length === 0) return normalizeResolvedModelArrays(config)
422
476
 
423
477
  let resolved = deepMerge(config, {})
424
478
  for (const [, override] of matches) {
425
479
  resolved = deepMerge(resolved, override as Partial<DcpConfig>)
426
480
  }
427
481
 
428
- return resolved
482
+ return normalizeResolvedModelArrays(resolved)
429
483
  }
430
484
 
431
485
  // ---------------------------------------------------------------------------
@@ -454,5 +508,5 @@ export function loadConfig(options: LoadConfigOptions = {}): DcpConfig {
454
508
  )
455
509
  config.issues = issues
456
510
 
457
- return config
511
+ return normalizeResolvedModelArrays(config)
458
512
  }
@@ -76,19 +76,43 @@ function stripDcpCarrierText(text: string): string {
76
76
  return stripStaleDcpMetadataLines(text)
77
77
  }
78
78
 
79
- function canonicalHashValue(value: unknown, seen = new WeakSet<object>()): unknown {
79
+ const OMIT_JSON_OBJECT_VALUE = Symbol("omit-json-object-value")
80
+
81
+ function canonicalHashValue(
82
+ value: unknown,
83
+ seen = new WeakSet<object>(),
84
+ arrayElement = false,
85
+ ): unknown | typeof OMIT_JSON_OBJECT_VALUE {
80
86
  if (value === null || typeof value === "string" || typeof value === "boolean") return value
81
- if (typeof value === "number") return Number.isFinite(value) ? value : String(value)
87
+ // Exact membership is persisted in JSONL, so hash the value that can actually
88
+ // survive that persistence boundary. JSON.stringify serializes non-finite
89
+ // numbers as null, drops undefined/function/symbol object properties, and
90
+ // converts those values to null when they occupy an array slot. Encoding an
91
+ // in-memory-only sentinel here made otherwise unchanged toolResult.details
92
+ // fail exact replay after restart.
93
+ if (typeof value === "number") return Number.isFinite(value) ? value : null
82
94
  if (typeof value === "bigint") return `${value}n`
83
- if (typeof value === "undefined") return "[undefined]"
84
- if (typeof value === "function" || typeof value === "symbol") return String(value)
85
- if (Array.isArray(value)) return value.map((item) => canonicalHashValue(item, seen))
95
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
96
+ return arrayElement ? null : OMIT_JSON_OBJECT_VALUE
97
+ }
98
+ if (Array.isArray(value)) {
99
+ return value.map((item) => {
100
+ const canonical = canonicalHashValue(item, seen, true)
101
+ return canonical === OMIT_JSON_OBJECT_VALUE ? null : canonical
102
+ })
103
+ }
86
104
  if (typeof value === "object") {
87
105
  if (seen.has(value as object)) return "[cycle]"
106
+ const toJSON = (value as { toJSON?: unknown }).toJSON
107
+ if (typeof toJSON === "function") {
108
+ const serialized = (toJSON as () => unknown).call(value)
109
+ return canonicalHashValue(serialized, seen, arrayElement)
110
+ }
88
111
  seen.add(value as object)
89
112
  const output: Record<string, unknown> = {}
90
113
  for (const key of Object.keys(value as Record<string, unknown>).sort()) {
91
- output[key] = canonicalHashValue((value as Record<string, unknown>)[key], seen)
114
+ const canonical = canonicalHashValue((value as Record<string, unknown>)[key], seen, false)
115
+ if (canonical !== OMIT_JSON_OBJECT_VALUE) output[key] = canonical
92
116
  }
93
117
  seen.delete(value as object)
94
118
  return output
@@ -106,14 +130,17 @@ export function canonicalMessageHash(message: any): string {
106
130
  if (message && typeof message === "object") {
107
131
  for (const key of Object.keys(message).sort()) {
108
132
  if (IDENTITY_ONLY_MESSAGE_KEYS.has(key)) continue
109
- canonical[key] = key === "content"
133
+ const value = key === "content"
110
134
  ? canonicalContentForMessageHash(message)
111
135
  : canonicalHashValue(message[key])
136
+ if (value !== OMIT_JSON_OBJECT_VALUE) canonical[key] = value
112
137
  }
113
138
  } else {
114
- canonical.value = canonicalHashValue(message)
139
+ const value = canonicalHashValue(message)
140
+ if (value !== OMIT_JSON_OBJECT_VALUE) canonical.value = value
115
141
  }
116
- return createHash("sha256").update(JSON.stringify(canonicalHashValue(canonical))).digest("hex")
142
+ const normalized = canonicalHashValue(canonical)
143
+ return createHash("sha256").update(JSON.stringify(normalized === OMIT_JSON_OBJECT_VALUE ? {} : normalized)).digest("hex")
117
144
  }
118
145
 
119
146
  const CANONICAL_HASH_RE = /^[a-f0-9]{64}$/i
@@ -0,0 +1,41 @@
1
+ import type { DcpConfig } from "./config.js";
2
+ import { modelKeysFromContext } from "./config.js";
3
+ import type { DcpState } from "./state.js";
4
+ import { estimateMessageTokens, getActiveSummaryTokenEstimate, resolveContextThresholds } from "./pruner-metadata.js";
5
+ import { planDcpBudget } from "./progress-controller.js";
6
+
7
+ /** Scalar-only diagnostic snapshot; never stores raw messages or credentials.
8
+ * These records are observational, never journal/recovery authority. */
9
+ export function dcpRequestSnapshot(config: DcpConfig, state: DcpState, model: any,
10
+ usage: { tokens?: number | null; contextWindow?: number } | undefined,
11
+ raw: any[], projected: any[], reason: string) {
12
+ const contextWindow = Number.isFinite(model?.contextWindow) && model.contextWindow > 0
13
+ ? model.contextWindow : usage?.contextWindow;
14
+ const rawTokens = raw.reduce((sum, message) => sum + estimateMessageTokens(message), 0);
15
+ const projectedTokens = projected.reduce((sum, message) => sum + estimateMessageTokens(message), 0);
16
+ const thresholds = resolveContextThresholds(config, modelKeysFromContext({ model } as any), contextWindow);
17
+ const budget = contextWindow ? planDcpBudget({
18
+ providerUsageTokens: usage?.tokens, repoProjectedTokens: projectedTokens, contextWindow,
19
+ reservedOutputTokens: model?.maxTokens ?? 0, reservedToolTokens: 0,
20
+ maxContextPercent: thresholds.maxContextPercent,
21
+ hardContextPercent: config.strategies.emergencyCurrentTurnPruning.hardContextPercent,
22
+ targetContextPercent: Math.min(config.strategies.emergencyCurrentTurnPruning.targetContextPercent, thresholds.maxContextPercent * 0.9),
23
+ summaryBufferEnabled: config.compress.summaryBuffer,
24
+ activeSummaryTokens: getActiveSummaryTokenEstimate(state), summaryBufferMaxBonusRatio: 0.05,
25
+ estimatorMarginTokens: Math.max(256, Math.ceil(contextWindow * 0.0025)),
26
+ }) : undefined;
27
+ const routineTokens = contextWindow ? thresholds.minContextPercent * contextWindow : undefined;
28
+ let pressure = "unknown";
29
+ if (budget && routineTokens !== undefined) {
30
+ pressure = budget.capacityExceeded ? "over capacity" : budget.hardPressure ? "hard" : budget.pressured ? "strong"
31
+ : budget.projectedBeforeTokens > routineTokens ? "routine" : "below routine threshold";
32
+ }
33
+ return { model: model?.provider && model?.id ? `${model.provider}/${model.id}` : undefined,
34
+ contextWindow, rawTokens, projectedTokens, inputCapacityTokens: budget?.inputCapacityTokens,
35
+ reservedOutputTokens: budget?.reservedOutputTokens, routineTokens, strongTokens: budget?.softHeadroomTokens,
36
+ hardTokens: budget?.hardHeadroomTokens, enabled: config.enabled, manualMode: state.manualMode,
37
+ autoEnabled: config.compress.autoCompress.enabled, ignored: state.consecutiveIgnoredNudges,
38
+ routineProjectedTokens: budget?.projectedBeforeTokens ?? projectedTokens,
39
+ routineUsageAdjustmentTokens: 0,
40
+ pressure, reason, createdAt: Date.now() };
41
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Proof that a tool result was produced locally in this epoch and has never
3
+ * belonged to an attempted provider prefix. This is NOT provider-seen evidence:
4
+ * failed/ambiguous sends consume freshness too. Never persist or rehydrate it.
5
+ */
6
+ export class FreshToolResultTracker {
7
+ private ready = false;
8
+ private readonly pending = new Map<string, string>();
9
+ private readonly fresh = new Set<string>();
10
+
11
+ reset(): void {
12
+ this.ready = false;
13
+ this.pending.clear();
14
+ this.fresh.clear();
15
+ }
16
+
17
+ /** Call before every send, not after successful completion. */
18
+ beforeProviderRequest(): void {
19
+ this.reset();
20
+ this.ready = true;
21
+ }
22
+
23
+ toolCall(id: string, name: string, alreadyKnown: boolean): void {
24
+ // Resumed/in-flight old calls and reused IDs cannot create a freshness grant.
25
+ if (!this.ready || alreadyKnown) return;
26
+ this.pending.set(id, name);
27
+ }
28
+
29
+ toolResult(id: string, name: string): void {
30
+ const expectedName = this.pending.get(id);
31
+ this.pending.delete(id);
32
+ if (expectedName === name) this.fresh.add(id);
33
+ }
34
+
35
+ get eligibleIds(): ReadonlySet<string> {
36
+ return this.fresh;
37
+ }
38
+ }