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
@@ -128,6 +128,16 @@ function validateExactBlock(value: unknown): asserts value is CompressionBlock {
128
128
  throw new DcpJournalError(`DCP journal block b${value.id} has invalid ${key}`);
129
129
  }
130
130
  }
131
+ if (value.commitMetrics !== undefined) {
132
+ const m = value.commitMetrics;
133
+ if (!isRecord(m) || typeof m.operationId !== "string" || !m.operationId ||
134
+ !["manual", "auto", "consolidation"].includes(m.kind as string) ||
135
+ !finiteNonNegativeInteger(m.beforeTokens) || !finiteNonNegativeInteger(m.afterTokens) ||
136
+ !finiteNonNegativeInteger(m.netGainTokens) || m.netGainTokens <= 0 ||
137
+ m.beforeTokens - m.afterTokens !== m.netGainTokens) {
138
+ throw new DcpJournalError(`DCP journal block b${value.id} has invalid commit metrics`);
139
+ }
140
+ }
131
141
  for (const key of ["sourceMembers", "mutationMembers"] as const) {
132
142
  const members = value[key];
133
143
  if (!Array.isArray(members) || members.length === 0) {
@@ -438,13 +448,125 @@ function journalBranch(ctx: ExtensionContext): unknown[] {
438
448
  }
439
449
  }
440
450
 
451
+ function indexBranchParents(entries: readonly unknown[], requireFullChain: boolean): Map<string, string | null> | undefined {
452
+ const parents = new Map<string, string | null>();
453
+ let previousId: string | null = null;
454
+ for (const entry of entries) {
455
+ if (!isRecord(entry) || typeof entry.id !== "string" || !entry.id
456
+ || parents.has(entry.id)) return undefined;
457
+ let parentId: string | null = null;
458
+ if (entry.parentId !== null) {
459
+ if (typeof entry.parentId !== "string" || !entry.parentId) return undefined;
460
+ parentId = entry.parentId;
461
+ }
462
+ if (requireFullChain && parentId !== previousId) return undefined;
463
+ parents.set(entry.id, parentId);
464
+ previousId = entry.id;
465
+ }
466
+ return parents;
467
+ }
468
+
469
+ /**
470
+ * getBranch() on Pix's lazy manager is a JSONL presentation tail: it may omit
471
+ * older ancestors AND interleave abandoned branches. Do not "simplify" this
472
+ * into a positional prefix/suffix comparison or an ID-membership test.
473
+ *
474
+ * Walk parentId from the captured current leaf. Only the full read may supply
475
+ * ancestors missing from the tail; overlapping IDs must have identical parents
476
+ * (SDK entry identities/parent links are immutable). Both the old leaf and the
477
+ * read tip must lie on that walk. A side-branch tip merely present in the tail
478
+ * is not evidence. Missing links/cycles/conflicts deny the retry.
479
+ *
480
+ * This proves permission to re-read, NOT a publishable merged snapshot. Never
481
+ * return the tail/union as history or hydrate synchronously to validate it.
482
+ * Contract + counterexample: specs/dcp-statistics.md#lazy-tail-retry-invariant.
483
+ */
484
+ function readSnapshotFollowsCurrentBranch(
485
+ entries: readonly unknown[],
486
+ currentEntries: readonly unknown[],
487
+ previousLeafId: string,
488
+ currentLeafId: string,
489
+ ): boolean {
490
+ const readParents = indexBranchParents(entries, true);
491
+ const currentParents = indexBranchParents(currentEntries, false);
492
+ if (!readParents?.has(previousLeafId) || !currentParents?.has(currentLeafId)) return false;
493
+
494
+ const readTipId = (entries.at(-1) as { id: string }).id;
495
+ const visited = new Set<string>();
496
+ let cursor: string | null = currentLeafId;
497
+ let reachedPrevious = false;
498
+ let reachedReadTip = false;
499
+ while (cursor !== null) {
500
+ if (visited.has(cursor)) return false;
501
+ visited.add(cursor);
502
+ const currentParent = currentParents.get(cursor);
503
+ const readParent = readParents.get(cursor);
504
+ if (currentParent !== undefined && readParent !== undefined && currentParent !== readParent) return false;
505
+ // An absent old ancestor may be supplied by the full read, but a missing
506
+ // new descendant cannot be guessed. Null is a root, not a missing value.
507
+ const parent = currentParent !== undefined ? currentParent : readParent;
508
+ if (parent === undefined) return false;
509
+ reachedPrevious ||= cursor === previousLeafId;
510
+ reachedReadTip ||= cursor === readTipId;
511
+ cursor = parent;
512
+ }
513
+ return reachedPrevious && reachedReadTip;
514
+ }
515
+
441
516
  export async function readDcpJournalBranch(ctx: ExtensionContext): Promise<unknown[]> {
442
517
  const manager = ctx.sessionManager as any;
443
- if (typeof manager.readFullBranchEntries === "function") {
444
- const entries = await manager.readFullBranchEntries();
445
- return Array.isArray(entries) ? entries : [];
518
+ const sessionId = manager.getSessionId?.();
519
+ const hasLeafIdentity = typeof manager.getLeafId === "function";
520
+ const readFullBranch = typeof manager.readFullBranchEntries === "function"
521
+ ? () => manager.readFullBranchEntries()
522
+ : undefined;
523
+
524
+ // Lazy full-history reads yield to the event loop. A normal append on the
525
+ // same active branch (for example, tool results arriving while a reloaded
526
+ // runtime is replaying its journal) advances the leaf without changing the
527
+ // branch owner. Retry that transient race so startup does not permanently
528
+ // block DCP. A real branch move/fork, owner replacement, or a branch that
529
+ // keeps moving still fails closed.
530
+ const maxAttempts = readFullBranch ? 3 : 1;
531
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
532
+ const leafId = manager.getLeafId?.();
533
+ const entries = readFullBranch ? await readFullBranch() : manager.getBranch();
534
+ if (ctx.sessionManager !== manager || manager.getSessionId?.() !== sessionId) {
535
+ throw new DcpJournalError("DCP branch changed during history read; rebuild context");
536
+ }
537
+ if (!Array.isArray(entries)) throw new DcpJournalError("DCP full branch is unavailable");
538
+ // A presentation cursor is not a complete SDK branch, even if it contains
539
+ // recent users or journal deltas. Never silently initialize from that tail.
540
+ if (entries[0]?.parentId != null) throw new DcpJournalError("DCP received an incomplete branch (presentation tail)");
541
+
542
+ const currentLeafId = manager.getLeafId?.();
543
+ if (currentLeafId === leafId) {
544
+ if (hasLeafIdentity) {
545
+ const readLeafId = entries.at(-1)?.id;
546
+ if ((leafId == null && entries.length > 0) || (leafId != null && readLeafId !== leafId)) {
547
+ throw new DcpJournalError("DCP branch changed during history read; rebuild context");
548
+ }
549
+ }
550
+ return entries;
551
+ }
552
+
553
+ let sameBranchAdvance = false;
554
+ if (leafId && currentLeafId) {
555
+ try {
556
+ const currentEntries = manager.getBranch();
557
+ sameBranchAdvance = Array.isArray(currentEntries)
558
+ && manager.getLeafId?.() === currentLeafId
559
+ && readSnapshotFollowsCurrentBranch(entries, currentEntries, leafId, currentLeafId);
560
+ } catch {
561
+ sameBranchAdvance = false;
562
+ }
563
+ }
564
+ if (!sameBranchAdvance || attempt === maxAttempts - 1) {
565
+ throw new DcpJournalError("DCP branch changed during history read; rebuild context");
566
+ }
446
567
  }
447
- return journalBranch(ctx);
568
+
569
+ throw new DcpJournalError("DCP branch changed during history read; rebuild context");
448
570
  }
449
571
 
450
572
  function restoreLeafAfterAppendFailure(ctx: ExtensionContext, previousLeafId: string | null | undefined): void {
@@ -15,7 +15,8 @@ export type DcpBlockedReason =
15
15
  | "summarizer-unavailable"
16
16
  | "non-positive-gain"
17
17
  | "missing-source"
18
- | "budget-exhausted";
18
+ | "budget-exhausted"
19
+ | "auto-compress-failed";
19
20
 
20
21
  export interface DcpBlockedReasonInput {
21
22
  pressured: boolean;
@@ -102,8 +102,17 @@ export interface CompressionCandidateSelectionOptions {
102
102
  requiredSavingsTokens?: number;
103
103
  /** Conservative estimator margin added to the required source size. */
104
104
  estimatorMarginTokens?: number;
105
+ /**
106
+ * Whether an automatic candidate may fold already-compressed block
107
+ * placeholders into a new block. Capacity-recovery plans disable this so an
108
+ * economical existing summary is not expanded again while rescuing raw tail
109
+ * growth. Manual/non-pressure consolidation keeps the historical default.
110
+ */
111
+ allowCompressionBlocks?: boolean;
105
112
  }
106
113
 
114
+ const MIN_AUTOMATIC_BLOCK_CONSOLIDATION = 8;
115
+
107
116
  function selectOldestSafePrefix(
108
117
  boundaries: CandidateBoundary[],
109
118
  state: DcpState,
@@ -176,7 +185,9 @@ export function detectCompressionCandidate(
176
185
  if (!settings.enabled) return null;
177
186
  if (contextPercent < settings.minContextPercent) return null;
178
187
 
179
- const boundaries = buildCandidateBoundaries(messages, _state, { allowBlocks: true });
188
+ const boundaries = buildCandidateBoundaries(messages, _state, {
189
+ allowBlocks: options?.allowCompressionBlocks ?? true,
190
+ });
180
191
 
181
192
  if (boundaries.length < settings.minMessages) return null;
182
193
 
@@ -224,6 +235,83 @@ export function detectCompressionCandidate(
224
235
  };
225
236
  }
226
237
 
238
+ /**
239
+ * Periodically fold a contiguous prefix of already-compressed summaries into
240
+ * one summary. Raw-tail recovery deliberately excludes bN blocks so it cannot
241
+ * recreate the incident's b1 -> b2 -> ... ladder; without a separate bounded
242
+ * consolidation step, however, a marathon turn can accumulate dozens of small
243
+ * active summaries until summary overhead itself fills the provider window.
244
+ *
245
+ * This detector is intentionally block-only: every selected boundary must be a
246
+ * synthetic block message and physically adjacent in the provider projection.
247
+ * Requiring a batch of blocks amortizes the intentional prefix rewrite and
248
+ * prevents one-old-summary-plus-a-tiny-tail recompression on every turn.
249
+ */
250
+ export function detectCompressionBlockConsolidationCandidate(
251
+ messages: any[],
252
+ state: DcpState,
253
+ config: DcpConfig,
254
+ options?: CompressionCandidateSelectionOptions,
255
+ ): CompressionCandidate | null {
256
+ const settings = config.compress.autoCandidates;
257
+ const boundaries = buildCandidateBoundaries(messages, state, { allowBlocks: true });
258
+ const minimumBlocks = MIN_AUTOMATIC_BLOCK_CONSOLIDATION;
259
+ const requiredSavings = Math.max(0, Math.floor(options?.requiredSavingsTokens ?? 0));
260
+ const estimatorMargin = Math.max(0, Math.floor(options?.estimatorMarginTokens ?? 0));
261
+ const targetSourceTokens = Math.max(settings.minTokens, requiredSavings + estimatorMargin);
262
+
263
+ let run: CandidateBoundary[] = [];
264
+ const chooseRun = (candidateRun: CandidateBoundary[]): CandidateBoundary[] | null => {
265
+ if (candidateRun.length < minimumBlocks) return null;
266
+ let selectedTokens = 0;
267
+ let selectedEnd = minimumBlocks - 1;
268
+ for (let index = 0; index < candidateRun.length; index++) {
269
+ selectedTokens += candidateRun[index]!.tokenEstimate;
270
+ if (index + 1 >= minimumBlocks && selectedTokens >= targetSourceTokens) {
271
+ selectedEnd = index;
272
+ break;
273
+ }
274
+ selectedEnd = index;
275
+ }
276
+ return candidateRun.slice(0, Math.max(minimumBlocks, selectedEnd + 1));
277
+ };
278
+
279
+ for (const boundary of boundaries) {
280
+ const previous = run.at(-1);
281
+ const continuesBlockRun = boundary.blockId !== undefined &&
282
+ (!previous || boundary.messageIndex === previous.messageIndex + 1);
283
+ if (continuesBlockRun) {
284
+ run.push(boundary);
285
+ continue;
286
+ }
287
+ const selected = chooseRun(run);
288
+ if (selected) {
289
+ const estimatedTokens = selected.reduce((sum, item) => sum + item.tokenEstimate, 0);
290
+ return {
291
+ startId: selected[0]!.id,
292
+ endId: selected[selected.length - 1]!.id,
293
+ messageCount: selected.length,
294
+ estimatedTokens,
295
+ includedBlockIds: selected.map((item) => item.blockId!),
296
+ reason: `automatic block-only consolidation of ${selected.length} adjacent summaries`,
297
+ };
298
+ }
299
+ run = boundary.blockId !== undefined ? [boundary] : [];
300
+ }
301
+
302
+ const selected = chooseRun(run);
303
+ if (!selected) return null;
304
+ const estimatedTokens = selected.reduce((sum, item) => sum + item.tokenEstimate, 0);
305
+ return {
306
+ startId: selected[0]!.id,
307
+ endId: selected[selected.length - 1]!.id,
308
+ messageCount: selected.length,
309
+ estimatedTokens,
310
+ includedBlockIds: selected.map((item) => item.blockId!),
311
+ reason: `automatic block-only consolidation of ${selected.length} adjacent summaries`,
312
+ };
313
+ }
314
+
227
315
  /**
228
316
  * Emergency-only range candidate for marathon turns.
229
317
  *
@@ -257,7 +345,9 @@ export function detectEmergencyCompressionCandidate(
257
345
  if (!emergencySettings.enabled) return null;
258
346
  if (contextPercent <= maxContextPercent) return null;
259
347
 
260
- const boundaries = buildCandidateBoundaries(messages, state, { allowBlocks: true });
348
+ const boundaries = buildCandidateBoundaries(messages, state, {
349
+ allowBlocks: options?.allowCompressionBlocks ?? true,
350
+ });
261
351
  if (boundaries.length < settings.minMessages) return null;
262
352
 
263
353
  let latestUserIndex = -1;
@@ -32,31 +32,51 @@ function nudgeTypeLabel(type: DcpNudgeType): string {
32
32
 
33
33
  function isRealAnchorCandidate(msg: any): boolean {
34
34
  const role = msg?.role ?? "";
35
- if (role !== "user" && role !== "assistant") return false;
35
+ if (role !== "user" && role !== "toolResult") return false;
36
36
  return msg?._dcpOrigin !== "block" && msg?._dcpOrigin !== "dcp-control";
37
37
  }
38
38
 
39
- function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId: string; timestamp: number; role: string } | null {
39
+ function findAnchorMessage(messages: any[], freshToolResultIds?: ReadonlySet<string>): { msg: any; index: number; stableId: string; timestamp: number; role: string } | null {
40
40
  const stableKeys = stableMessageKeys(messages);
41
- // A reminder may only be introduced on a fresh user tail. If assistant/tool
42
- // traffic already follows that user message then it has already belonged to
43
- // an earlier provider prefix; editing it now would invalidate that prefix.
41
+ // Only append to a fresh tail; never edit an earlier user/assistant prefix.
42
+ // Tool-result freshness requires a positive local lifecycle grant, not the
43
+ // absence of successful provider evidence (which is unknown after restart).
44
44
  for (let i = messages.length - 1; i >= 0; i--) {
45
45
  const msg = messages[i];
46
46
  if (msg?._dcpOrigin === "dcp-control") continue;
47
- if (msg?.role !== "user" || !isRealAnchorCandidate(msg) || !Number.isFinite(msg.timestamp)) return null;
47
+ if (!isRealAnchorCandidate(msg) || !Number.isFinite(msg.timestamp)) return null;
48
+ if (msg.role === "toolResult") {
49
+ if (!freshToolResultIds?.has(msg.toolCallId)) return null;
50
+ // A duplicate/reused call ID is not an unambiguous fresh carrier.
51
+ if (messages.filter((item) => item?.role === "toolResult" && item.toolCallId === msg.toolCallId).length !== 1) return null;
52
+ }
48
53
  return { msg, index: i, stableId: stableKeys[i]!, timestamp: msg.timestamp, role: msg.role };
49
54
  }
50
55
 
51
56
  return null;
52
57
  }
53
58
 
54
- export function hasCacheSafeNudgeCarrier(messages: any[]): boolean {
55
- return findAnchorMessage(messages) !== null;
59
+ export function hasCacheSafeNudgeCarrier(
60
+ messages: any[],
61
+ freshToolResultIds?: ReadonlySet<string>,
62
+ anchors: readonly DcpNudgeAnchor[] = [],
63
+ ): boolean {
64
+ if (findAnchorMessage(messages, freshToolResultIds) !== null) return true;
65
+ // A previously published reminder remains deliverable without editing its
66
+ // carrier. Losing the grant to create a NEW reminder on a provider attempt
67
+ // must not make retries bypass the completed-opportunity patience policy.
68
+ const stableKeys = stableMessageKeys(messages);
69
+ return anchors.some((anchor) => typeof anchor.renderedReminder === "string" &&
70
+ anchor.renderedReminder.length > 0 &&
71
+ messages.some((message, index) => anchorMatchesMessage(anchor, message, stableKeys[index]!)));
56
72
  }
57
73
 
58
74
  function anchorMatchesMessage(anchor: DcpNudgeAnchor, msg: any, stableKey: string): boolean {
59
- if (anchor.anchorStableId && stableKey === anchor.anchorStableId) return true;
75
+ if (!isRealAnchorCandidate(msg) || msg.role !== anchor.anchorRole) return false;
76
+ if (anchor.anchorStableId) return stableKey === anchor.anchorStableId;
77
+ // Legacy user anchors may be timestamp-only. Tool anchors must never rebind
78
+ // to another parallel result that happens to share their timestamp.
79
+ if (msg.role === "toolResult") return false;
60
80
  return msg?.timestamp === anchor.anchorTimestamp;
61
81
  }
62
82
 
@@ -78,24 +98,8 @@ function appendTextToMessage(msg: any, text: string): void {
78
98
  return;
79
99
  }
80
100
 
81
- const textBlock = { type: "text", text: suffix };
82
- if (msg.role !== "assistant") {
83
- msg.content = [...msg.content, textBlock];
84
- return;
85
- }
86
-
87
- // Keep provider ordering constraints: assistant text must appear before
88
- // toolCall blocks for models that enforce text/thinking/tool_use ordering.
89
- const firstToolCallIdx = msg.content.findIndex((block: any) => block?.type === "toolCall");
90
- if (firstToolCallIdx === -1) {
91
- msg.content = [...msg.content, textBlock];
92
- } else {
93
- msg.content = [
94
- ...msg.content.slice(0, firstToolCallIdx),
95
- textBlock,
96
- ...msg.content.slice(firstToolCallIdx),
97
- ];
98
- }
101
+ // Only user/tool-result carriers reach here. Assistant items are immutable.
102
+ msg.content = [...msg.content, { type: "text", text: suffix }];
99
103
  }
100
104
 
101
105
  function insertBeforeReminderClose(reminder: string, detail: string): string {
@@ -174,7 +178,7 @@ export function upsertNudgeAnchor(
174
178
  messages: any[],
175
179
  state: DcpState,
176
180
  type: DcpNudgeType,
177
- options: { contextPercent?: number; renderedReminder?: string } = {},
181
+ options: { contextPercent?: number; renderedReminder?: string; freshToolResultIds?: ReadonlySet<string> } = {},
178
182
  ): { anchor: DcpNudgeAnchor | null; created: boolean; updated: boolean } {
179
183
  const stableKeys = stableMessageKeys(messages);
180
184
  let existing: DcpNudgeAnchor | null = null;
@@ -198,11 +202,10 @@ export function upsertNudgeAnchor(
198
202
  state.lastNudge = undefined;
199
203
  }
200
204
 
201
- const target = findAnchorMessage(messages);
205
+ const target = findAnchorMessage(messages, options.freshToolResultIds);
202
206
  if (!target) {
203
- // Mid-turn reminder creation is deliberately deferred rather than
204
- // synthesizing an ephemeral tail message that would disappear on the next
205
- // provider continuation and break prefix equality.
207
+ // No positively fresh carrier: do not synthesize a disappearing tail or
208
+ // reinterpret unknown historical exposure as permission to edit history.
206
209
  return { anchor: null, created: false, updated: false };
207
210
  }
208
211
 
@@ -263,7 +266,7 @@ export function applyAnchoredNudges(
263
266
  const reminder = selected.anchor.renderedReminder ?? render(selected.anchor);
264
267
  if (materializedReminder) selected.anchor.renderedReminder = reminder;
265
268
  const anchorMessage = messages[selected.index];
266
- if (anchorMessage?.role !== "user") {
269
+ if (!isRealAnchorCandidate(anchorMessage)) {
267
270
  state.nudgeAnchors = [];
268
271
  state.lastNudge = undefined;
269
272
  return { rendered: false, stateChanged: true };
@@ -30,6 +30,7 @@ export {
30
30
  markToolPruned,
31
31
  } from "./pruner-tools.js";
32
32
  export {
33
+ detectCompressionBlockConsolidationCandidate,
33
34
  detectCompressionCandidate,
34
35
  detectEmergencyCompressionCandidate,
35
36
  detectMessageCompressionCandidates,
@@ -0,0 +1,86 @@
1
+ import { canonicalMessageHash } from "./conversation-index.js";
2
+ import type { CompressionBlock } from "./state.js";
3
+
4
+ interface ProjectionFrame {
5
+ owner: string;
6
+ avoidedTokens: number;
7
+ blocks: string;
8
+ }
9
+
10
+ export interface RoutinePressureEstimate {
11
+ projectedTokens: number;
12
+ adjustmentTokens: number;
13
+ }
14
+
15
+ function usableUsage(message: any): boolean {
16
+ if (message?.role !== "assistant" || ["error", "aborted"].includes(message.stopReason)) return false;
17
+ const usage = message.usage;
18
+ if (!usage) return false;
19
+ const tokens = usage.totalTokens || [usage.input, usage.output, usage.cacheRead, usage.cacheWrite]
20
+ .reduce((sum, value) => sum + (typeof value === "number" && Number.isFinite(value) ? value : 0), 0);
21
+ return typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0;
22
+ }
23
+
24
+ function blockVersion(blocks: readonly CompressionBlock[]): string {
25
+ return blocks.filter((block) => block.active).map((block) => block.id).join(",");
26
+ }
27
+
28
+ /**
29
+ * Runtime-only calibration for ROUTINE nudges, never a provider-send budget.
30
+ * The SDK's last assistant usage still includes history removed by a later DCP
31
+ * rewrite. Subtract only the additional locally measured raw/projected delta
32
+ * since that assistant's correlated request. This keeps provider overhead and
33
+ * tail growth, and avoids counting older compression twice. Uncorrelated usage
34
+ * and lifecycle changes fall back to the existing conservative estimate.
35
+ */
36
+ export class RoutinePressureTracker {
37
+ private prepared: ProjectionFrame | undefined;
38
+ private pending: ProjectionFrame | undefined;
39
+ private sample: (ProjectionFrame & { hash: string; timestamp: unknown }) | undefined;
40
+
41
+ reset(): void {
42
+ this.prepared = undefined;
43
+ this.pending = undefined;
44
+ this.sample = undefined;
45
+ }
46
+
47
+ prepare(owner: string, rawTokens: number, projectedTokens: number, blocks: readonly CompressionBlock[]): void {
48
+ this.prepared = { owner, avoidedTokens: rawTokens - projectedTokens, blocks: blockVersion(blocks) };
49
+ }
50
+
51
+ beforeRequest(): void {
52
+ this.pending = this.prepared;
53
+ }
54
+
55
+ complete(message: any, correlated: boolean): void {
56
+ const pending = this.pending;
57
+ this.pending = undefined;
58
+ if (!usableUsage(message)) return;
59
+ this.sample = correlated && pending && typeof message.timestamp === "number" && Number.isFinite(message.timestamp)
60
+ ? { ...pending, hash: canonicalMessageHash(message), timestamp: message.timestamp }
61
+ : undefined;
62
+ }
63
+
64
+ estimate(owner: string, nativeTokens: number | null | undefined, raw: readonly any[],
65
+ rawTokens: number, projectedTokens: number, blocks: readonly CompressionBlock[]): RoutinePressureEstimate {
66
+ const fallback = Math.max(projectedTokens, nativeTokens ?? 0);
67
+ const unchanged = { projectedTokens: fallback, adjustmentTokens: 0 };
68
+ const sample = this.sample;
69
+ if (!sample || sample.owner !== owner || nativeTokens === null || nativeTokens === undefined) return unchanged;
70
+ const version = blockVersion(blocks);
71
+ if (!version || version === sample.blocks) return unchanged;
72
+ // Confirm the SDK is still based on this exact successful assistant, not
73
+ // an unrelated/new response, failed stream or a sample from another owner.
74
+ let latest: any;
75
+ for (let index = raw.length - 1; index >= 0; index--) {
76
+ if (usableUsage(raw[index])) {
77
+ latest = raw[index];
78
+ break;
79
+ }
80
+ }
81
+ if (!latest || latest.timestamp !== sample.timestamp || canonicalMessageHash(latest) !== sample.hash) return unchanged;
82
+ const newSavings = Math.max(0, rawTokens - projectedTokens - sample.avoidedTokens);
83
+ const adjusted = Math.max(projectedTokens, nativeTokens - newSavings);
84
+ return { projectedTokens: adjusted, adjustmentTokens: fallback - adjusted };
85
+ }
86
+ }
@@ -153,6 +153,15 @@ export interface CompressionBlock {
153
153
  active: boolean
154
154
  /** Token estimate for the summary text itself */
155
155
  summaryTokenEstimate: number
156
+ /** One record per successful transaction (on its first block), never on retries.
157
+ * Optional for older journal blocks; absence means unmeasured, not zero gain. */
158
+ commitMetrics?: {
159
+ operationId: string
160
+ kind: "manual" | "auto" | "consolidation"
161
+ beforeTokens: number
162
+ afterTokens: number
163
+ netGainTokens: number
164
+ }
156
165
  /** Wall-clock time the block was created (Date.now()) */
157
166
  createdAt: number
158
167
  /** Older compression blocks subsumed by this block during roll-up. */
@@ -0,0 +1,8 @@
1
+ export interface DcpStatisticsInput {
2
+ branch?: readonly any[];
3
+ model?: any;
4
+ usage?: any;
5
+ historyStatus?: "full" | "unavailable";
6
+ }
7
+ export function collectDcpStatistics(input: DcpStatisticsInput): any;
8
+ export function formatDcpStatistics(input: DcpStatisticsInput): string;