pi-ui-extend 1.0.41 → 1.0.42

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 (124) hide show
  1. package/README.md +10 -2
  2. package/dist/app/app.d.ts +3 -0
  3. package/dist/app/app.js +26 -0
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.js +6 -1
  6. package/dist/app/commands/command-registry.js +1 -1
  7. package/dist/app/commands/command-session-actions.js +9 -3
  8. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  9. package/dist/app/commands/reload-context-inventory.js +59 -0
  10. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  11. package/dist/app/extensions/extension-actions-controller.js +6 -0
  12. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  13. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  14. package/dist/app/rendering/editor-panels.js +10 -4
  15. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  16. package/dist/app/session/session-lifecycle-controller.js +6 -0
  17. package/dist/app/session/tabs-controller.d.ts +1 -0
  18. package/dist/app/session/tabs-controller.js +19 -1
  19. package/dist/app/subagents/subagents-files.js +60 -1
  20. package/dist/app/subagents/subagents-model.d.ts +1 -0
  21. package/dist/app/subagents/subagents-model.js +18 -2
  22. package/dist/app/types.d.ts +5 -0
  23. package/dist/default-pix-config.js +10 -0
  24. package/dist/schemas/index.d.ts +1 -0
  25. package/dist/schemas/index.js +1 -0
  26. package/dist/schemas/pi-tools-suite-schema.d.ts +80 -58
  27. package/dist/schemas/pi-tools-suite-schema.js +43 -78
  28. package/dist/schemas/pix-schema.d.ts +7 -0
  29. package/dist/schemas/pix-schema.js +14 -0
  30. package/dist/schemas/tasks-schema.d.ts +18 -0
  31. package/dist/schemas/tasks-schema.js +41 -0
  32. package/docs/concurrency.md +6 -0
  33. package/docs/desktop-mvp.md +12 -1
  34. package/docs/desktop-task-manager.md +93 -82
  35. package/external/pi-tools-suite/README.md +37 -50
  36. package/external/pi-tools-suite/docs/browser-qa-subagent.md +19 -0
  37. package/external/pi-tools-suite/docs/evals.md +1 -1
  38. package/external/pi-tools-suite/docs/session-recovery.md +40 -14
  39. package/external/pi-tools-suite/docs/subagent-model-pools.md +38 -27
  40. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  42. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +14 -70
  44. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  46. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  47. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -3
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +122 -312
  49. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +15 -2
  50. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +6 -2
  51. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  52. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  53. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  54. package/external/pi-tools-suite/src/async-subagents/lib.ts +7 -8
  55. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -4
  56. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  57. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  58. package/external/pi-tools-suite/src/config.ts +41 -1
  59. package/external/pi-tools-suite/src/context-gateway/config.ts +11 -2
  60. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +102 -0
  61. package/external/pi-tools-suite/src/context-gateway/index.ts +73 -19
  62. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +3 -3
  63. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  64. package/external/pi-tools-suite/src/context-gateway/types.ts +9 -3
  65. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  66. package/external/pi-tools-suite/src/dcp/auto-compress.ts +96 -44
  67. package/external/pi-tools-suite/src/dcp/commands.ts +18 -0
  68. package/external/pi-tools-suite/src/dcp/compress-tool.ts +170 -45
  69. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  70. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  71. package/external/pi-tools-suite/src/dcp/config.ts +56 -13
  72. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  73. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  74. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +1 -1
  75. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  76. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  77. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +13 -9
  78. package/external/pi-tools-suite/src/index.ts +7 -1
  79. package/external/pi-tools-suite/src/resource-registry/index.ts +2290 -0
  80. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  81. package/external/pi-tools-suite/src/tool-descriptions.ts +3 -4
  82. package/package.json +4 -2
  83. package/schemas/pi-tools-suite.json +427 -280
  84. package/schemas/pix.json +24 -0
  85. package/schemas/tasks.json +132 -0
  86. package/skills/spec-wiki/SKILL.md +55 -0
  87. package/skills/spec-wiki/evals/evals.json +132 -0
  88. package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +14 -0
  89. package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +4 -0
  90. package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +9 -0
  91. package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +17 -0
  92. package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +12 -0
  93. package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +11 -0
  94. package/skills/spec-wiki/evals/files/focused/src/session.ts +5 -0
  95. package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +17 -0
  96. package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +2 -0
  97. package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +3 -0
  98. package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +18 -0
  99. package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +5 -0
  100. package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +24 -0
  101. package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +7 -0
  102. package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +7 -0
  103. package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +17 -0
  104. package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +3 -0
  105. package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +3 -0
  106. package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +4 -0
  107. package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +9 -0
  108. package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +15 -0
  109. package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +6 -0
  110. package/skills/spec-wiki/evals/maintenance_regression.py +311 -0
  111. package/skills/spec-wiki/evals/retrieval-eval.md +37 -0
  112. package/skills/spec-wiki/evals/retrieval-evals.json +37 -0
  113. package/skills/spec-wiki/evals/trigger-evals.json +94 -0
  114. package/skills/spec-wiki/references/bootstrap.md +100 -0
  115. package/skills/spec-wiki/references/focused-spec.md +117 -0
  116. package/skills/spec-wiki/references/index-schema.md +330 -0
  117. package/skills/spec-wiki/references/maintenance.md +134 -0
  118. package/skills/spec-wiki/references/query.md +74 -0
  119. package/skills/spec-wiki/scripts/spec_wiki.py +2076 -0
  120. package/docs/desktop-markdown-media.md +0 -77
  121. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  122. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  123. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  124. package/skills/spec-lite/SKILL.md +0 -140
@@ -775,6 +775,80 @@ export async function generateModelSummary(
775
775
  return { attempts }
776
776
  }
777
777
 
778
+ export interface PreparedCompressionSummary {
779
+ text: string
780
+ representation: "model" | "extractive" | "extractive-fallback"
781
+ sourceManifest: SummarySourceItem[]
782
+ sourceHash: string
783
+ sourceCoverage: SummarySourceCoverage
784
+ summarizerModelRef?: string
785
+ summarizerAttempts?: ModelSummaryAttempt[]
786
+ }
787
+
788
+ /**
789
+ * Shared summary preparation for automatic and explicit compression. The
790
+ * configured model chain is attempted first; if it is absent/unavailable the
791
+ * deterministic extractive continuation record is the safety floor.
792
+ */
793
+ export async function prepareCompressionSummary(options: {
794
+ topic: string
795
+ messages: any[]
796
+ candidate?: CompressionCandidate
797
+ modelRefs: string[]
798
+ timeoutMs: number
799
+ modelRegistry?: ModelSummaryRegistry
800
+ signal?: AbortSignal
801
+ sourceManifest?: SummarySourceItem[]
802
+ }): Promise<PreparedCompressionSummary> {
803
+ options.signal?.throwIfAborted()
804
+ const sourceManifest = options.sourceManifest ?? buildSummarySourceManifest(options.messages)
805
+ let modelResult: ModelSummaryResult | undefined
806
+ if (options.modelRefs.length > 0) {
807
+ modelResult = await generateModelSummary(
808
+ options.modelRefs,
809
+ options.modelRegistry,
810
+ options.signal,
811
+ options.topic,
812
+ options.messages,
813
+ options.timeoutMs,
814
+ sourceManifest,
815
+ )
816
+ options.signal?.throwIfAborted()
817
+ }
818
+ const fallbackCandidate = options.candidate ?? {
819
+ startId: "generated",
820
+ endId: "generated",
821
+ messageCount: sourceManifest.length,
822
+ estimatedTokens: options.messages.reduce((sum, message) => sum + estimateMessageTokens(message), 0),
823
+ includedBlockIds: [],
824
+ reason: "explicit/generated summary source",
825
+ }
826
+ const text = modelResult?.text ?? buildExtractiveSummary(
827
+ options.topic,
828
+ fallbackCandidate,
829
+ sourceManifest,
830
+ )
831
+ if (!modelResult?.text && estimateTokens(text) > 8192) {
832
+ throw new AutoCompressionBlockedError(
833
+ "budget-exhausted",
834
+ "Extractive continuity minimum exceeds its 8192-token budget; no checkpoints were silently dropped",
835
+ )
836
+ }
837
+ return {
838
+ text,
839
+ representation: modelResult?.text
840
+ ? "model"
841
+ : options.modelRefs.length > 0
842
+ ? "extractive-fallback"
843
+ : "extractive",
844
+ sourceManifest,
845
+ sourceHash: hashSummarySourceManifest(sourceManifest),
846
+ sourceCoverage: summarySourceCoverage(sourceManifest),
847
+ summarizerModelRef: modelResult?.usedModelRef,
848
+ summarizerAttempts: modelResult?.attempts.length ? modelResult.attempts : undefined,
849
+ }
850
+ }
851
+
778
852
  function extractAssistantText(result: any): string | undefined {
779
853
  const content = result?.content
780
854
  if (!Array.isArray(content)) return undefined
@@ -913,48 +987,26 @@ export async function createAutoCompressionBlock(
913
987
  }
914
988
  }
915
989
 
916
- // Summary source selection. `summaryMode` distinguishes three cases so the
917
- // DCP debug log can tell a real model summary from a programmatic fallback
918
- // caused by summarizer failure:
919
- // - "model": a configured model produced the summary.
920
- // - "programmatic": no summarizer models configured (floor by design).
921
- // - "programmatic_fallback": models were configured but all failed/empty.
922
- const sourceManifest = buildSummarySourceManifest(messagesInRange)
923
- let summary = ""
924
- let summaryMode: "programmatic" | "model" | "programmatic_fallback" = "programmatic"
925
- let summarizerModelRef: string | undefined
926
- let summarizerAttempts: ModelSummaryAttempt[] | undefined
927
-
928
- const modelRefs = settings.summarizerModel
929
- if (modelRefs.length > 0) {
930
- const modelResult = await generateModelSummary(
931
- modelRefs,
932
- modelRegistry,
933
- signal,
934
- topic,
935
- messagesInRange,
936
- settings.timeoutMs,
937
- sourceManifest,
938
- )
939
- assertCurrent()
940
- summarizerAttempts = modelResult.attempts.length > 0 ? modelResult.attempts : undefined
941
- if (modelResult.text) {
942
- summary = modelResult.text
943
- summaryMode = "model"
944
- summarizerModelRef = modelResult.usedModelRef
945
- } else {
946
- // All configured models failed or returned empty — fall back to the
947
- // programmatic digest, but mark the mode distinctly so the fallback
948
- // is visible in DCP debug logs.
949
- summaryMode = "programmatic_fallback"
950
- }
951
- }
952
- if (!summary) {
953
- summary = buildExtractiveSummary(topic, effectiveCandidate, sourceManifest)
954
- if (estimateTokens(summary) > 8192) {
955
- throw new AutoCompressionBlockedError("budget-exhausted", "Extractive continuity minimum exceeds its 8192-token budget; no checkpoints were silently dropped")
956
- }
957
- }
990
+ const preparedSummary = await prepareCompressionSummary({
991
+ topic,
992
+ messages: messagesInRange,
993
+ candidate: effectiveCandidate,
994
+ modelRefs: settings.summarizerModel,
995
+ timeoutMs: settings.timeoutMs,
996
+ modelRegistry,
997
+ signal,
998
+ })
999
+ assertCurrent()
1000
+ const summary = preparedSummary.text
1001
+ const sourceManifest = preparedSummary.sourceManifest
1002
+ const summaryMode: "programmatic" | "model" | "programmatic_fallback" =
1003
+ preparedSummary.representation === "model"
1004
+ ? "model"
1005
+ : preparedSummary.representation === "extractive-fallback"
1006
+ ? "programmatic_fallback"
1007
+ : "programmatic"
1008
+ const summarizerModelRef = preparedSummary.summarizerModelRef
1009
+ const summarizerAttempts = preparedSummary.summarizerAttempts
958
1010
 
959
1011
  const workingState = createAutoCompressionWorkingState(state)
960
1012
  const anchor = resolveAnchorBoundary(endTimestamp, workingState, endBoundary.stableId)
@@ -1004,8 +1056,8 @@ export async function createAutoCompressionBlock(
1004
1056
  : summaryMode === "programmatic_fallback"
1005
1057
  ? "extractive-fallback"
1006
1058
  : "extractive"
1007
- const sourceHash = hashSummarySourceManifest(sourceManifest)
1008
- const sourceCoverage = summarySourceCoverage(sourceManifest)
1059
+ const sourceHash = preparedSummary.sourceHash
1060
+ const sourceCoverage = preparedSummary.sourceCoverage
1009
1061
  created.block.autoSummaryRepresentation = summaryRepresentation
1010
1062
  created.block.sourceHash = sourceHash
1011
1063
  created.block.sourceCoverage = sourceCoverage
@@ -179,6 +179,7 @@ const HELP_TEXT = `DCP — Dynamic Context Pruning
179
179
  Commands:
180
180
  /dcp context — Show context window usage breakdown
181
181
  /dcp stats — Show pruning statistics for this session
182
+ /dcp doctor — Show config compatibility diagnostics
182
183
  /dcp sweep [N] — Prune last N tool outputs (default: all since last user msg)
183
184
  /dcp manual — Show manual mode status
184
185
  /dcp manual on — Enable manual mode (disable autonomous compression nudges)
@@ -189,6 +190,18 @@ function handleHelp(ctx: ExtensionCommandContext): void {
189
190
  ctx.ui.notify(HELP_TEXT, "info")
190
191
  }
191
192
 
193
+ function handleDoctor(ctx: ExtensionCommandContext, config: DcpConfig): void {
194
+ const issues = config.issues ?? []
195
+ const lines = ["DCP Doctor", ""]
196
+ if (issues.length === 0) {
197
+ lines.push("Config: no removed or ignored DCP settings detected.")
198
+ } else {
199
+ lines.push(`Config: ${issues.length} removed/ignored setting${issues.length === 1 ? "" : "s"}:`)
200
+ for (const issue of issues) lines.push(` - ${issue}`)
201
+ }
202
+ ctx.ui.notify(lines.join("\n"), issues.length > 0 ? "warning" : "info")
203
+ }
204
+
192
205
  // ---------------------------------------------------------------------------
193
206
  // Context usage
194
207
  // ---------------------------------------------------------------------------
@@ -434,6 +447,7 @@ export function registerCommands(
434
447
  const subcommands: AutocompleteItem[] = [
435
448
  { value: "context", label: "context", description: "Show context window usage breakdown" },
436
449
  { value: "stats", label: "stats", description: "Show pruning statistics" },
450
+ { value: "doctor", label: "doctor", description: "Show config compatibility diagnostics" },
437
451
  { value: "sweep", label: "sweep", description: "Prune tool outputs" },
438
452
  { value: "manual", label: "manual", description: "Toggle manual mode" },
439
453
  { value: "compress", label: "compress", description: "Trigger LLM compression" },
@@ -469,6 +483,10 @@ export function registerCommands(
469
483
  handleStats(pi, ctx, state)
470
484
  break
471
485
 
486
+ case "doctor":
487
+ handleDoctor(ctx, config)
488
+ break
489
+
472
490
  case "sweep": {
473
491
  const rawN = parts[1] !== undefined ? parseInt(parts[1], 10) : 0
474
492
  const n = isNaN(rawN) || rawN < 0 ? 0 : rawN
@@ -30,8 +30,55 @@ import {
30
30
  detectToolGroupSpans,
31
31
  findConversationIndexEntry,
32
32
  } from "./conversation-index.js"
33
- import { previewManualCompressionProjection } from "./compression-preview.js"
33
+ import {
34
+ previewManualCompressionProjection,
35
+ verifiedManualCompressionMessages,
36
+ } from "./compression-preview.js"
34
37
  import { settleCompressionProgress } from "./compression-progress.js"
38
+ import { estimateMessageTokens } from "./pruner-metadata.js"
39
+ import {
40
+ buildSummarySourceManifest,
41
+ hashSummarySourceManifest,
42
+ prepareCompressionSummary,
43
+ type ModelSummaryAttempt,
44
+ } from "./auto-compress.js"
45
+
46
+ export const COMPRESS_TOOL_PARAMETERS = Type.Object({
47
+ topic: Type.String({
48
+ description:
49
+ "Short label (3-5 words) for display - e.g., 'Auth System Exploration'",
50
+ }),
51
+ ranges: Type.Optional(Type.Array(
52
+ Type.Object({
53
+ startId: Type.String({
54
+ description:
55
+ "First ID (mNNN/bN); never start inside a tool group—include its calling assistant.",
56
+ }),
57
+ endId: Type.String({
58
+ description:
59
+ "Last ID (mNNN/bN); include all results of the final tool group, including parallel calls.",
60
+ }),
61
+ summary: Type.Optional(Type.String({
62
+ description: "Optional; omit to use the configured DCP summarizer.",
63
+ })),
64
+ }),
65
+ { description: "One or more ranges to compress" },
66
+ )),
67
+ messages: Type.Optional(Type.Array(
68
+ Type.Object({
69
+ messageId: Type.String({
70
+ description: "Raw message ID to compress individually (e.g. m001)",
71
+ }),
72
+ topic: Type.Optional(Type.String({
73
+ description: "Short label for this one-message summary; defaults to top-level topic",
74
+ })),
75
+ summary: Type.Optional(Type.String({
76
+ description: "Optional; omit to use the configured DCP summarizer.",
77
+ })),
78
+ }),
79
+ { description: "Individual raw messages to compress surgically" },
80
+ )),
81
+ })
35
82
 
36
83
  type MessageSkipKind =
37
84
  | "duplicate"
@@ -51,7 +98,7 @@ interface MessageSkipIssue {
51
98
  interface ResolvedRangePlan {
52
99
  startId: string
53
100
  endId: string
54
- summary: string
101
+ summary?: string
55
102
  startTimestamp: number
56
103
  endTimestamp: number
57
104
  startMessageId?: string
@@ -63,7 +110,7 @@ interface ResolvedRangePlan {
63
110
  interface ResolvedMessagePlan {
64
111
  messageId: string
65
112
  topic: string
66
- summary: string
113
+ summary?: string
67
114
  timestamp: number
68
115
  stableId?: string
69
116
  sourceMembers: CompressionMember[]
@@ -72,6 +119,33 @@ interface ResolvedMessagePlan {
72
119
 
73
120
  type ResolvedRangeBoundaries = Omit<ResolvedRangePlan, "sourceMembers" | "mutationMembers">
74
121
 
122
+ type ManualSummaryMode = "explicit" | "model" | "extractive" | "extractive-fallback"
123
+
124
+ function verifiedSelectionMessages(
125
+ state: DcpState,
126
+ sourceMembers: CompressionMember[],
127
+ ): any[] {
128
+ const projection = verifiedManualCompressionMessages(state)
129
+ const byStableId = new Map(
130
+ state.conversationIndexSnapshot.map((entry) => [entry.stableId, entry] as const),
131
+ )
132
+ return sourceMembers.map((member) => {
133
+ const entry = byStableId.get(member.stableId)
134
+ if (!entry || entry.contentHash !== member.hash || !Number.isSafeInteger(entry.index)) {
135
+ throw new Error(
136
+ `Manual compression summary source ${member.stableId} no longer matches the verified provider projection; refresh context and retry.`,
137
+ )
138
+ }
139
+ const message = projection[entry.index]
140
+ if (!message) {
141
+ throw new Error(
142
+ `Manual compression summary source ${member.stableId} is missing from the verified provider projection; refresh context and retry.`,
143
+ )
144
+ }
145
+ return message
146
+ })
147
+ }
148
+
75
149
  function validateNonOverlappingRanges(plans: ResolvedRangeBoundaries[], state: DcpState): void {
76
150
  const sorted = [...plans].sort((a, b) =>
77
151
  compareCompressionBoundaries(
@@ -246,43 +320,7 @@ export function registerCompressTool(
246
320
  label: COMPRESS_TOOL_DESCRIPTION.label,
247
321
  description: COMPRESS_TOOL_DESCRIPTION.description,
248
322
  promptSnippet: COMPRESS_TOOL_DESCRIPTION.promptSnippet ?? "Compress ranges of conversation into summaries to manage context",
249
- parameters: Type.Object({
250
- topic: Type.String({
251
- description:
252
- "Short label (3-5 words) for display - e.g., 'Auth System Exploration'",
253
- }),
254
- ranges: Type.Optional(Type.Array(
255
- Type.Object({
256
- startId: Type.String({
257
- description:
258
- "First ID (mNNN/bN); never start inside a tool group—include its calling assistant.",
259
- }),
260
- endId: Type.String({
261
- description:
262
- "Last ID (mNNN/bN); include all results of the final tool group, including parallel calls.",
263
- }),
264
- summary: Type.String({
265
- description:
266
- "Continuation-focused technical summary; avoid raw JSON/code/diffs unless a short literal is required",
267
- }),
268
- }),
269
- { description: "One or more ranges to compress" },
270
- )),
271
- messages: Type.Optional(Type.Array(
272
- Type.Object({
273
- messageId: Type.String({
274
- description: "Raw message ID to compress individually (e.g. m001)",
275
- }),
276
- topic: Type.Optional(Type.String({
277
- description: "Short label for this one-message summary; defaults to top-level topic",
278
- })),
279
- summary: Type.String({
280
- description: "Continuation-focused technical summary replacing this raw message; avoid raw JSON/code/diffs unless required",
281
- }),
282
- }),
283
- { description: "Individual raw messages to compress surgically" },
284
- )),
285
- }),
323
+ parameters: COMPRESS_TOOL_PARAMETERS,
286
324
 
287
325
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
288
326
  _signal?.throwIfAborted()
@@ -462,6 +500,93 @@ export function registerCompressTool(
462
500
  }
463
501
  assertDisjointSelections(rangePlans, preflightMessages)
464
502
 
503
+ const summaryPreparations: Array<{
504
+ selection: string
505
+ mode: ManualSummaryMode
506
+ summarizerModelRef?: string
507
+ attempts?: Array<{ ref: string; outcome: ModelSummaryAttempt["outcome"] }>
508
+ }> = []
509
+ const resolveSummary = async (input: {
510
+ startId: string
511
+ endId: string
512
+ topic: string
513
+ sourceMembers: CompressionMember[]
514
+ explicitSummary?: string
515
+ }): Promise<string> => {
516
+ const selection = input.startId === input.endId
517
+ ? input.startId
518
+ : `${input.startId}..${input.endId}`
519
+ if (typeof input.explicitSummary === "string") {
520
+ summaryPreparations.push({ selection, mode: "explicit" })
521
+ return input.explicitSummary
522
+ }
523
+ const sourceMessages = verifiedSelectionMessages(state, input.sourceMembers)
524
+ const sourceManifest = buildSummarySourceManifest(sourceMessages)
525
+ const sourceHash = hashSummarySourceManifest(sourceManifest)
526
+ const prepared = await prepareCompressionSummary({
527
+ topic: input.topic,
528
+ messages: sourceMessages,
529
+ candidate: {
530
+ startId: input.startId,
531
+ endId: input.endId,
532
+ messageCount: sourceMessages.length,
533
+ estimatedTokens: sourceMessages.reduce((sum, message) => sum + estimateMessageTokens(message), 0),
534
+ includedBlockIds: [],
535
+ reason: "manual delegated summary",
536
+ },
537
+ modelRefs: effectiveConfig.compress.autoCompress.summarizerModel,
538
+ timeoutMs: effectiveConfig.compress.autoCompress.timeoutMs,
539
+ modelRegistry: (ctx as any).modelRegistry,
540
+ signal: _signal,
541
+ sourceManifest,
542
+ })
543
+ assertCurrent()
544
+ const refreshedMessages = verifiedSelectionMessages(state, input.sourceMembers)
545
+ const refreshedHash = hashSummarySourceManifest(buildSummarySourceManifest(refreshedMessages))
546
+ if (refreshedHash !== sourceHash || prepared.sourceHash !== sourceHash) {
547
+ throw new Error(
548
+ `stale_plan: manual compression summary source changed while preparing ${selection}`,
549
+ )
550
+ }
551
+ const attempts = prepared.summarizerAttempts?.map(({ ref, outcome }) => ({ ref, outcome }))
552
+ summaryPreparations.push({
553
+ selection,
554
+ mode: prepared.representation,
555
+ summarizerModelRef: prepared.summarizerModelRef,
556
+ attempts,
557
+ })
558
+ log("compress.summary_prepared", {
559
+ toolCallId: _toolCallId,
560
+ selection,
561
+ mode: prepared.representation,
562
+ summarizerModelRef: prepared.summarizerModelRef,
563
+ summarizerAttempts: prepared.summarizerAttempts,
564
+ sourceHash: prepared.sourceHash,
565
+ sourceCoverage: prepared.sourceCoverage,
566
+ })
567
+ return prepared.text
568
+ }
569
+
570
+ for (const range of rangePlans) {
571
+ range.summary = await resolveSummary({
572
+ startId: range.startId,
573
+ endId: range.endId,
574
+ topic: params.topic,
575
+ sourceMembers: range.sourceMembers,
576
+ explicitSummary: range.summary,
577
+ })
578
+ }
579
+ for (const plan of preflightMessages) {
580
+ plan.summary = await resolveSummary({
581
+ startId: plan.messageId,
582
+ endId: plan.messageId,
583
+ topic: plan.topic,
584
+ sourceMembers: plan.sourceMembers,
585
+ explicitSummary: plan.summary,
586
+ })
587
+ }
588
+ const preflightMessageById = new Map(preflightMessages.map((plan) => [plan.messageId, plan] as const))
589
+
465
590
  for (const range of rangePlans) {
466
591
  try {
467
592
  const anchor = resolveAnchorBoundary(range.endTimestamp, workingState, range.endMessageId)
@@ -478,7 +603,7 @@ export function registerCompressTool(
478
603
 
479
604
  const created = createRangeCompressionBlock({
480
605
  topic: params.topic,
481
- summary: range.summary,
606
+ summary: range.summary!,
482
607
  startTimestamp: range.startTimestamp,
483
608
  endTimestamp: range.endTimestamp,
484
609
  startMessageId: range.startMessageId,
@@ -521,7 +646,6 @@ export function registerCompressTool(
521
646
  const seenMessageIds = new Set<string>()
522
647
 
523
648
  for (const entry of messages) {
524
- const { summary } = entry
525
649
  const messageId = typeof entry.messageId === "string" ? entry.messageId.trim() : ""
526
650
  if (seenMessageIds.has(messageId)) {
527
651
  skippedMessageIssues.push({ kind: "duplicate", messageId })
@@ -604,7 +728,7 @@ export function registerCompressTool(
604
728
 
605
729
  const created = createRangeCompressionBlock({
606
730
  topic: entry.topic ?? params.topic,
607
- summary,
731
+ summary: preflightMessageById.get(messageId)?.summary!,
608
732
  startTimestamp: meta.timestamp,
609
733
  endTimestamp: meta.timestamp,
610
734
  startMessageId: meta.stableId,
@@ -620,8 +744,8 @@ export function registerCompressTool(
620
744
  validatePlaceholders: false,
621
745
  expandPlaceholders: false,
622
746
  preparedProtectedFragments,
623
- sourceMembers: preflightMessages.find((plan) => plan.messageId === messageId)?.sourceMembers,
624
- mutationMembers: preflightMessages.find((plan) => plan.messageId === messageId)?.mutationMembers,
747
+ sourceMembers: preflightMessageById.get(messageId)?.sourceMembers,
748
+ mutationMembers: preflightMessageById.get(messageId)?.mutationMembers,
625
749
  })
626
750
  const block = created.block
627
751
  newBlockIds.push(block.id)
@@ -735,6 +859,7 @@ export function registerCompressTool(
735
859
  }
736
860
  const resultDetails = {
737
861
  ...visualDetails,
862
+ summaryPreparations,
738
863
  projectedBeforeTokens: projection?.projectedBeforeTokens,
739
864
  projectedAfterTokens: projection?.projectedAfterTokens,
740
865
  netGain: projection?.netGain,
@@ -8,6 +8,7 @@ import type {
8
8
  import type { DcpConfig } from "./config.js"
9
9
  import { estimateTokens } from "./pruner-metadata.js"
10
10
  import { isToolRecordProtected } from "./pruner-tools.js"
11
+ import { toolRecordContinuity } from "./protected-continuity.js"
11
12
  import { compareConversationStableIds, buildExactRangeMembership } from "./conversation-index.js"
12
13
  import { createHash } from "node:crypto"
13
14
  import { open, realpath } from "node:fs/promises"
@@ -570,10 +571,10 @@ function collectCurrentProtectedFragments(
570
571
  if (!toolCallId) continue
571
572
  const record = state.toolCalls.get(toolCallId)
572
573
  if (!record || record.toolName === "compress" || !isToolRecordProtected(record, config)) continue
573
- const output = (record.outputText ?? meta.text ?? "").trim()
574
- if (!output) continue
575
- const exact = `### Tool: ${record.toolName}\n${output}`
576
- fragments.push(compressionProtectedFragment("tool", `tool:${toolCallId}`, exact))
574
+ const continuity = toolRecordContinuity(record, config)
575
+ const text = continuity.text?.trim()
576
+ if (!text) continue
577
+ fragments.push(compressionProtectedFragment("tool", `tool:${toolCallId}`, text))
577
578
  }
578
579
  return mergeProtectedFragments(fragments)
579
580
  }
@@ -657,51 +658,6 @@ function appendProtectedFragmentLedger(summary: string, fragments: CompressionPr
657
658
  missing.map((fragment) => `\n\n${fragment.text}`).join("")
658
659
  }
659
660
 
660
- function appendProtectedToolOutputs(
661
- summary: string,
662
- startTimestamp: number,
663
- endTimestamp: number,
664
- state: DcpState,
665
- config: DcpConfig,
666
- ids: { startMessageId?: string; endMessageId?: string } = {},
667
- ): string {
668
- const protectedOutputs: string[] = []
669
- const seenToolCallIds = new Set<string>()
670
- const boundaries = rangeBoundaries(startTimestamp, endTimestamp, ids)
671
-
672
- for (const meta of state.messageMetaSnapshot.values()) {
673
- if (meta.blockId !== undefined) continue
674
- if (!Number.isFinite(meta.timestamp)) continue
675
- if (!isCompressionBoundaryWithinRange(
676
- { timestamp: meta.timestamp, stableId: meta.stableId },
677
- boundaries.start,
678
- boundaries.end,
679
- state,
680
- )) continue
681
- if (meta.role !== "toolResult" && meta.role !== "bashExecution") continue
682
-
683
- const toolCallId = meta.toolCallId
684
- if (!toolCallId || seenToolCallIds.has(toolCallId)) continue
685
- const record = state.toolCalls.get(toolCallId)
686
- if (!record) continue
687
-
688
- // The compress tool's own JSON accounting output is control-plane data and
689
- // rarely useful in future summaries; avoid recursively copying it.
690
- if (record.toolName === "compress") continue
691
- if (!isToolRecordProtected(record, config)) continue
692
-
693
- const output = (record.outputText ?? meta.text ?? "").trim()
694
- if (!output) continue
695
-
696
- seenToolCallIds.add(toolCallId)
697
- protectedOutputs.push(`\n### Tool: ${record.toolName}\n${output}`)
698
- }
699
-
700
- if (protectedOutputs.length === 0) return summary
701
- const heading = "\n\nThe following protected tool outputs were included in this conversation section:"
702
- return summary + heading + protectedOutputs.join("")
703
- }
704
-
705
661
  export function estimateVisibleRangeTokens(
706
662
  startTimestamp: number,
707
663
  endTimestamp: number,
@@ -945,20 +901,12 @@ export function createRangeCompressionBlock(
945
901
  ids,
946
902
  )
947
903
 
948
- const expandedSummary = appendProtectedToolOutputs(
949
- promptPreservedSummary,
950
- startTimestamp,
951
- endTimestamp,
952
- state,
953
- config,
954
- ids,
955
- )
956
904
  const protectedFragments = mergeProtectedFragments(
957
905
  collectInheritedProtectedFragments(coveredBlocks),
958
906
  collectCurrentProtectedFragments(startTimestamp, endTimestamp, state, config, mode, ids),
959
907
  preparedProtectedFragments,
960
908
  )
961
- const ledgerPreservedSummary = appendProtectedFragmentLedger(expandedSummary, protectedFragments)
909
+ const ledgerPreservedSummary = appendProtectedFragmentLedger(promptPreservedSummary, protectedFragments)
962
910
 
963
911
  const block: CompressionBlock = {
964
912
  id: state.nextBlockId++,
@@ -58,6 +58,15 @@ function verifiedSnapshot(state: DcpState): any[] {
58
58
  return snapshot
59
59
  }
60
60
 
61
+ /**
62
+ * Return a detached, verified copy of the exact provider projection used by
63
+ * manual compression. Generated summaries must use this source rather than
64
+ * reconstructing history from timestamps or persisted session entries.
65
+ */
66
+ export function verifiedManualCompressionMessages(state: DcpState): any[] {
67
+ return cloneProjectionMessages(verifiedSnapshot(state))
68
+ }
69
+
61
70
  function stripProjectionMetadata(message: any): any {
62
71
  const stripped = stripStaleDcpMetadataFromMessage(message)
63
72
  return copyDcpProvenance(message, stripped)