@compaction/cli 0.1.4 → 0.3.0

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 (170) hide show
  1. package/README.md +51 -12
  2. package/dist/cli/commands/activity.d.ts +8 -0
  3. package/dist/cli/commands/activity.js +35 -0
  4. package/dist/cli/commands/apply-context.js +68 -0
  5. package/dist/cli/commands/billing-delta.js +12 -1
  6. package/dist/cli/commands/capture-claude-code.d.ts +44 -0
  7. package/dist/cli/commands/capture-claude-code.js +206 -0
  8. package/dist/cli/commands/capture.js +283 -1
  9. package/dist/cli/commands/compact.js +24 -11
  10. package/dist/cli/commands/context.d.ts +2 -0
  11. package/dist/cli/commands/context.js +130 -0
  12. package/dist/cli/commands/dev.d.ts +17 -0
  13. package/dist/cli/commands/dev.js +131 -0
  14. package/dist/cli/commands/gateway.d.ts +31 -0
  15. package/dist/cli/commands/gateway.js +312 -0
  16. package/dist/cli/commands/hooks.d.ts +5 -0
  17. package/dist/cli/commands/hooks.js +181 -0
  18. package/dist/cli/commands/import.js +3 -1
  19. package/dist/cli/commands/init.js +422 -17
  20. package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
  21. package/dist/cli/commands/input-compaction-ab.js +125 -0
  22. package/dist/cli/commands/optimize-hosted.d.ts +54 -0
  23. package/dist/cli/commands/optimize-hosted.js +123 -0
  24. package/dist/cli/commands/optimize.js +8 -0
  25. package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
  26. package/dist/cli/commands/output-shaping-ab.js +132 -0
  27. package/dist/cli/commands/output-shaping.d.ts +7 -0
  28. package/dist/cli/commands/output-shaping.js +43 -0
  29. package/dist/cli/commands/policies.d.ts +2 -0
  30. package/dist/cli/commands/policies.js +80 -0
  31. package/dist/cli/commands/precall.d.ts +19 -0
  32. package/dist/cli/commands/precall.js +436 -0
  33. package/dist/cli/commands/recommend.js +1 -1
  34. package/dist/cli/commands/run.js +310 -18
  35. package/dist/cli/commands/upgrade-status.d.ts +42 -0
  36. package/dist/cli/commands/upgrade-status.js +152 -0
  37. package/dist/cli/cursor-export-read.d.ts +13 -0
  38. package/dist/cli/cursor-export-read.js +53 -0
  39. package/dist/cli/cursor-live-preflight.d.ts +19 -0
  40. package/dist/cli/cursor-live-preflight.js +46 -0
  41. package/dist/cli/index.js +39 -2
  42. package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
  43. package/dist/cli/onboarding/GatewayTui.js +113 -0
  44. package/dist/cli/onboarding/model.d.ts +32 -1
  45. package/dist/cli/onboarding/model.js +27 -2
  46. package/dist/cli/terminal-logo.d.ts +27 -0
  47. package/dist/cli/terminal-logo.js +49 -0
  48. package/dist/core/activity-event.d.ts +107 -0
  49. package/dist/core/activity-event.js +182 -0
  50. package/dist/core/activity-store.d.ts +63 -0
  51. package/dist/core/activity-store.js +254 -0
  52. package/dist/core/activity-view.d.ts +76 -0
  53. package/dist/core/activity-view.js +120 -0
  54. package/dist/core/api-client/index.d.ts +3 -1
  55. package/dist/core/api-client/index.js +4 -0
  56. package/dist/core/api-client/payload.d.ts +5 -1
  57. package/dist/core/api-client/payload.js +8 -6
  58. package/dist/core/api-client/persisted-config.d.ts +57 -0
  59. package/dist/core/api-client/persisted-config.js +156 -0
  60. package/dist/core/api-client/tool.d.ts +29 -0
  61. package/dist/core/api-client/tool.js +50 -0
  62. package/dist/core/api-client/types.d.ts +15 -4
  63. package/dist/core/auto-apply-ask.d.ts +58 -0
  64. package/dist/core/auto-apply-ask.js +105 -0
  65. package/dist/core/auto-apply-gates.d.ts +76 -0
  66. package/dist/core/auto-apply-gates.js +113 -0
  67. package/dist/core/before-call-activity.d.ts +35 -0
  68. package/dist/core/before-call-activity.js +103 -0
  69. package/dist/core/before-call-recovery.d.ts +19 -0
  70. package/dist/core/before-call-recovery.js +46 -0
  71. package/dist/core/before-call-stdin.d.ts +59 -0
  72. package/dist/core/before-call-stdin.js +78 -0
  73. package/dist/core/before-call.d.ts +149 -0
  74. package/dist/core/before-call.js +358 -0
  75. package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
  76. package/dist/core/billing-delta/billing-delta-record.js +26 -0
  77. package/dist/core/capture-record.d.ts +40 -0
  78. package/dist/core/capture-record.js +55 -0
  79. package/dist/core/claude-code-before-call.d.ts +37 -0
  80. package/dist/core/claude-code-before-call.js +120 -0
  81. package/dist/core/claude-code-connect.d.ts +31 -0
  82. package/dist/core/claude-code-connect.js +87 -0
  83. package/dist/core/claude-code-hook-record.d.ts +71 -0
  84. package/dist/core/claude-code-hook-record.js +0 -0
  85. package/dist/core/claude-code-hooks.d.ts +77 -0
  86. package/dist/core/claude-code-hooks.js +141 -0
  87. package/dist/core/codex-capture.d.ts +45 -0
  88. package/dist/core/codex-capture.js +204 -0
  89. package/dist/core/command-runner.js +5 -1
  90. package/dist/core/compaction-artifacts.d.ts +51 -0
  91. package/dist/core/compaction-artifacts.js +242 -0
  92. package/dist/core/compactor.d.ts +9 -0
  93. package/dist/core/compactor.js +95 -0
  94. package/dist/core/context-store-eval-cases.d.ts +6 -0
  95. package/dist/core/context-store-eval-cases.js +331 -0
  96. package/dist/core/context-store-eval.d.ts +140 -0
  97. package/dist/core/context-store-eval.js +138 -0
  98. package/dist/core/context-store-fs.d.ts +73 -0
  99. package/dist/core/context-store-fs.js +157 -0
  100. package/dist/core/context-store-sufficiency.d.ts +98 -0
  101. package/dist/core/context-store-sufficiency.js +135 -0
  102. package/dist/core/context-store.d.ts +155 -0
  103. package/dist/core/context-store.js +228 -0
  104. package/dist/core/cross-surface-event.d.ts +306 -0
  105. package/dist/core/cross-surface-event.js +330 -0
  106. package/dist/core/cursor-capture.d.ts +54 -0
  107. package/dist/core/cursor-capture.js +215 -0
  108. package/dist/core/cursor-preflight-probe.d.ts +8 -0
  109. package/dist/core/cursor-preflight-probe.js +88 -0
  110. package/dist/core/cursor-preflight.d.ts +86 -0
  111. package/dist/core/cursor-preflight.js +126 -0
  112. package/dist/core/gateway/apply-activation.d.ts +39 -0
  113. package/dist/core/gateway/apply-activation.js +84 -0
  114. package/dist/core/gateway/apply-policy.d.ts +64 -0
  115. package/dist/core/gateway/apply-policy.js +221 -0
  116. package/dist/core/gateway/apply-receipt.d.ts +36 -0
  117. package/dist/core/gateway/apply-receipt.js +75 -0
  118. package/dist/core/gateway/cache-proof.d.ts +45 -0
  119. package/dist/core/gateway/cache-proof.js +65 -0
  120. package/dist/core/gateway/configure.d.ts +50 -0
  121. package/dist/core/gateway/configure.js +169 -0
  122. package/dist/core/gateway/openai-usage.d.ts +56 -0
  123. package/dist/core/gateway/openai-usage.js +128 -0
  124. package/dist/core/gateway/receipt.d.ts +138 -0
  125. package/dist/core/gateway/receipt.js +120 -0
  126. package/dist/core/gateway/recovery.d.ts +23 -0
  127. package/dist/core/gateway/recovery.js +68 -0
  128. package/dist/core/gateway/server.d.ts +51 -0
  129. package/dist/core/gateway/server.js +276 -0
  130. package/dist/core/gateway/status.d.ts +45 -0
  131. package/dist/core/gateway/status.js +109 -0
  132. package/dist/core/hook-usage-aggregate.d.ts +47 -0
  133. package/dist/core/hook-usage-aggregate.js +161 -0
  134. package/dist/core/input-compaction-ab.d.ts +111 -0
  135. package/dist/core/input-compaction-ab.js +158 -0
  136. package/dist/core/local-run-record.d.ts +109 -0
  137. package/dist/core/local-run-record.js +223 -0
  138. package/dist/core/output-shaping-ab.d.ts +140 -0
  139. package/dist/core/output-shaping-ab.js +146 -0
  140. package/dist/core/output-shaping-attach.d.ts +31 -0
  141. package/dist/core/output-shaping-attach.js +57 -0
  142. package/dist/core/output-shaping.d.ts +56 -0
  143. package/dist/core/output-shaping.js +89 -0
  144. package/dist/core/policy-middleware.d.ts +121 -0
  145. package/dist/core/policy-middleware.js +919 -0
  146. package/dist/core/policy-preferences.d.ts +99 -0
  147. package/dist/core/policy-preferences.js +232 -0
  148. package/dist/core/report-generator.d.ts +19 -1
  149. package/dist/core/report-generator.js +51 -0
  150. package/dist/core/run-aggregator.d.ts +56 -1
  151. package/dist/core/run-aggregator.js +93 -2
  152. package/dist/core/run-flow-report.d.ts +82 -0
  153. package/dist/core/run-flow-report.js +71 -0
  154. package/dist/core/safety-report.js +8 -1
  155. package/dist/core/shim-capture-bridge.d.ts +32 -0
  156. package/dist/core/shim-capture-bridge.js +88 -0
  157. package/dist/core/skill-injection-policy.d.ts +72 -0
  158. package/dist/core/skill-injection-policy.js +183 -0
  159. package/dist/core/spend-attribution.js +1 -1
  160. package/dist/core/token-accounting.d.ts +1 -1
  161. package/dist/core/tool-shim.d.ts +129 -0
  162. package/dist/core/tool-shim.js +447 -0
  163. package/dist/core/trace-adapters.d.ts +8 -0
  164. package/dist/core/trace-adapters.js +4 -0
  165. package/dist/core/trace-parser.d.ts +13 -13
  166. package/dist/core/trace-parser.js +6 -6
  167. package/dist/core/types.d.ts +45 -2
  168. package/dist/core/waste-detector.d.ts +20 -0
  169. package/dist/core/waste-detector.js +160 -6
  170. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  export type TraceRole = "system" | "user" | "assistant" | "tool" | "stdout" | "stderr";
2
- export type TraceSource = "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import";
2
+ export type TraceSource = "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | "cursor_import";
3
3
  export interface TraceCommandMetadata {
4
4
  command: string;
5
5
  args: string[];
@@ -43,7 +43,17 @@ export interface CostEstimate {
43
43
  totalCostUsd: number;
44
44
  }
45
45
  export interface WasteFinding {
46
- category: "repeated_tool_output" | "stale_context" | "verbose_observation" | "repeated_skill_injection";
46
+ /**
47
+ * `superseded_same_source_read` (input-compaction category 1, accepted design
48
+ * docs/design/input-compaction-policy-reach.md D2): the SAME source was read multiple times and
49
+ * a LATER read is authoritative; an EARLIER full copy whose content GENUINELY DIFFERS (whitespace-
50
+ * normalized) from the latest can be replaced by a recoverable pointer to the retained original.
51
+ * ADDITIVE to `repeated_tool_output` (which handles identical-up-to-whitespace repeats) and never
52
+ * weakens it: an identical earlier copy is LEFT to `repeated_tool_output`, never category-1-compacted.
53
+ * Convention for this category's `messageIds`: `[0]` = the KEPT latest authoritative read (verbatim),
54
+ * `[1..]` = the earlier superseded reads to compact (each recoverable from the retained original).
55
+ */
56
+ category: "repeated_tool_output" | "stale_context" | "verbose_observation" | "repeated_skill_injection" | "superseded_same_source_read";
47
57
  messageIds: string[];
48
58
  summary: string;
49
59
  estimatedTokens: number;
@@ -247,6 +257,39 @@ export interface CompactionReport {
247
257
  /** Non-sensitive structural count: number of messages hashed. */
248
258
  message_count: number;
249
259
  };
260
+ /**
261
+ * Composed per-run SAVINGS EVIDENCE record (additive, OPTIONAL, backward-compatible) —
262
+ * the V0.2 "evidence labels per result" surface. Older reports omit it. See
263
+ * `SavingsEvidence`. Composes EXISTING report numbers; makes no new/stronger claim.
264
+ */
265
+ savings_evidence?: SavingsEvidence;
266
+ }
267
+ /**
268
+ * Composed, per-run savings-evidence record (V0.2 "evidence labels per result"). Ties the
269
+ * evidence chain into ONE place and states, per run, the cost-source label, the
270
+ * recoverability/semantic status, and exactly which `claims-and-evidence-ladder` rung the
271
+ * figure stands on — so no consumer reads a stronger claim than the evidence supports.
272
+ *
273
+ * A single run is rung 1 (local estimate). The measured rung 1.5
274
+ * (`measured_caveated_estimate_delta`) is produced ONLY by the aggregate increment across
275
+ * N≥3 distinct runs, NEVER here. `billing_confirmed` is ALWAYS false; `semantic_preservation`
276
+ * is ALWAYS `not_evaluated` (never auto-certified).
277
+ */
278
+ export interface SavingsEvidence {
279
+ /** Is the run attributable to a verifiable identity (trace fingerprint present)? */
280
+ trace_identity: "fingerprinted" | "unidentified";
281
+ /** Honest cost-source label for this run's figures (mirrors `CostSource`). */
282
+ cost_source: "provider_reported" | "price_table_estimate" | "local_estimate" | "missing" | "unknown";
283
+ /** Deterministic recoverability status when an eval ran (`compact --eval`); else `not_evaluated`. */
284
+ recoverability: "passed" | "failed" | "not_computed" | "not_evaluated";
285
+ /** Semantic / meaning preservation is NEVER auto-certified — always `not_evaluated`. */
286
+ semantic_preservation: "not_evaluated";
287
+ /** Which claims-and-evidence-ladder rung this single-run figure stands on. */
288
+ evidence_rung: "local_estimate_single_run";
289
+ /** No v0 per-run figure is billing-confirmed. Always `false`. */
290
+ billing_confirmed: false;
291
+ /** One-line honest label (never asserts a stronger claim than the rung). */
292
+ label: string;
250
293
  }
251
294
  export interface CompactionPolicy {
252
295
  policy_name: string;
@@ -5,4 +5,24 @@ export interface WasteDetectionThresholds {
5
5
  minDuplicateCharacters?: number;
6
6
  }
7
7
  export declare function detectWaste(trace: AgentTrace, thresholds?: WasteDetectionThresholds): WasteFinding[];
8
+ /**
9
+ * Input-compaction category 1: detect SUPERSEDED same-source reads.
10
+ *
11
+ * When the SAME source (provable source identity) is read ≥2 times, the LATEST read is authoritative
12
+ * (preserved verbatim). Each EARLIER read whose whitespace-normalized content GENUINELY DIFFERS from
13
+ * the latest's is a superseded candidate that can be replaced by a recoverable pointer to the
14
+ * retained original. Conservative & additive:
15
+ * - Outputs with NO provable source identity are skipped (not eligible).
16
+ * - An earlier output that is identical-up-to-whitespace to the latest is LEFT to the existing
17
+ * `repeated_tool_output` policy (NOT category-1-compacted) — no overlap, no double-compaction.
18
+ * - Each compacted (earlier) output must meet the SAME thresholds detectWaste uses
19
+ * (minDuplicateTokens / minDuplicateCharacters).
20
+ *
21
+ * Finding convention: `messageIds[0]` = KEPT latest authoritative read; `messageIds[1..]` = earlier
22
+ * superseded reads to compact (estimatedTokens = sum of the compacted earlier reads' estimates).
23
+ *
24
+ * NO scoring formula, weight, relevance, or learned logic — only deterministic source-identity
25
+ * equality + whitespace-normalized content difference + the existing min-tokens/chars thresholds.
26
+ */
27
+ export declare function detectSupersededSameSourceReads(trace: AgentTrace, thresholds?: WasteDetectionThresholds): WasteFinding[];
8
28
  export declare function getCompactedMessageIds(findings: WasteFinding[]): Set<string>;
@@ -35,17 +35,171 @@ export function detectWaste(trace, thresholds = {}) {
35
35
  }
36
36
  firstToolOutputBySignature.set(signature, message);
37
37
  }
38
+ // ADDITIVE: input-compaction category 1 (superseded same-source reads). The existing
39
+ // repeated_tool_output logic above is UNCHANGED; category-1 findings are appended so the existing
40
+ // pipeline (getCompactedMessageIds, policy-middleware) can pick them up. Category 1 only emits when
41
+ // an earlier same-source read GENUINELY DIFFERS from the latest, so it never overlaps an
42
+ // identical-up-to-whitespace repeat (which stays a repeated_tool_output finding).
43
+ findings.push(...detectSupersededSameSourceReads(trace, thresholds));
44
+ return findings;
45
+ }
46
+ /**
47
+ * Source identity of a tool OUTPUT for input-compaction category 1 (`superseded_same_source_read`).
48
+ *
49
+ * Identity = `toolName + "::" + normalizedArgs`, where `normalizedArgs` is derived from the
50
+ * originating tool CALL: the nearest PRECEDING assistant message (since the last tool output) with
51
+ * the SAME `toolName` whose `content` parses as the call args. Parsing is deterministic and
52
+ * CONSERVATIVE — two accepted shapes ONLY:
53
+ * 1. a JSON object -> JSON.stringify with sorted keys (so key order does not change identity);
54
+ * 2. a `"Tool call: <cmd>"` string -> the `<cmd>`, whitespace-normalized.
55
+ * Anything else (no preceding same-tool call, unparseable content) yields `null` => the output is
56
+ * NOT eligible for category 1 (conservative: if source identity cannot be PROVEN, do not compact).
57
+ *
58
+ * This is pure deterministic equality of provable source identity. There is NO scoring, threshold
59
+ * (beyond the existing min-tokens/chars in detectWaste), weight, relevance, or learned logic here.
60
+ */
61
+ function normalizeCallArgs(callContent) {
62
+ const trimmed = callContent.trim();
63
+ // Shape 2: a "Tool call: <cmd>" string (the command form, whitespace-normalized).
64
+ const toolCallMatch = /^Tool call:\s*(.+)$/s.exec(trimmed);
65
+ if (toolCallMatch) {
66
+ const command = toolCallMatch[1].trim().replace(/\s+/g, " ");
67
+ return command.length > 0 ? `cmd:${command}` : null;
68
+ }
69
+ // Shape 1: a JSON object -> stable stringify with sorted keys.
70
+ try {
71
+ const parsed = JSON.parse(trimmed);
72
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
73
+ const sortedKeys = Object.keys(parsed).sort();
74
+ const stable = JSON.stringify(parsed, sortedKeys);
75
+ return `json:${stable}`;
76
+ }
77
+ }
78
+ catch {
79
+ // Not JSON — fall through to "not provable".
80
+ }
81
+ return null;
82
+ }
83
+ /**
84
+ * Resolve the PROVABLE source identity of a tool output at `outputIndex`. Walks BACKWARD from the
85
+ * output to the nearest preceding assistant message that has the SAME toolName and whose content
86
+ * parses as call args (the originating CALL). Stops at the previous tool output boundary ("since the
87
+ * last tool output") so a call belonging to an EARLIER output is never mis-attributed. Returns the
88
+ * identity `toolName + "::" + normalizedArgs`, or `null` when no such parseable same-tool call exists
89
+ * (source identity unknown/ambiguous => not eligible for category 1).
90
+ */
91
+ function resolveSourceIdentity(messages, outputIndex) {
92
+ const output = messages[outputIndex];
93
+ const toolName = output.toolName;
94
+ if (!toolName) {
95
+ return null;
96
+ }
97
+ for (let i = outputIndex - 1; i >= 0; i -= 1) {
98
+ const candidate = messages[i];
99
+ // Boundary: do not look past the previous tool output (a call before it originated THAT output).
100
+ if (candidate.role === "tool") {
101
+ return null;
102
+ }
103
+ if (candidate.role !== "assistant" || candidate.toolName !== toolName) {
104
+ continue;
105
+ }
106
+ const normalizedArgs = normalizeCallArgs(candidate.content);
107
+ if (normalizedArgs === null) {
108
+ // A same-tool assistant message that is NOT a parseable call: ambiguous. Conservative -> skip.
109
+ return null;
110
+ }
111
+ return `${toolName}::${normalizedArgs}`;
112
+ }
113
+ return null;
114
+ }
115
+ /**
116
+ * Input-compaction category 1: detect SUPERSEDED same-source reads.
117
+ *
118
+ * When the SAME source (provable source identity) is read ≥2 times, the LATEST read is authoritative
119
+ * (preserved verbatim). Each EARLIER read whose whitespace-normalized content GENUINELY DIFFERS from
120
+ * the latest's is a superseded candidate that can be replaced by a recoverable pointer to the
121
+ * retained original. Conservative & additive:
122
+ * - Outputs with NO provable source identity are skipped (not eligible).
123
+ * - An earlier output that is identical-up-to-whitespace to the latest is LEFT to the existing
124
+ * `repeated_tool_output` policy (NOT category-1-compacted) — no overlap, no double-compaction.
125
+ * - Each compacted (earlier) output must meet the SAME thresholds detectWaste uses
126
+ * (minDuplicateTokens / minDuplicateCharacters).
127
+ *
128
+ * Finding convention: `messageIds[0]` = KEPT latest authoritative read; `messageIds[1..]` = earlier
129
+ * superseded reads to compact (estimatedTokens = sum of the compacted earlier reads' estimates).
130
+ *
131
+ * NO scoring formula, weight, relevance, or learned logic — only deterministic source-identity
132
+ * equality + whitespace-normalized content difference + the existing min-tokens/chars thresholds.
133
+ */
134
+ export function detectSupersededSameSourceReads(trace, thresholds = {}) {
135
+ const minTokens = thresholds.minDuplicateTokens ?? 0;
136
+ const minChars = thresholds.minDuplicateCharacters ?? 0;
137
+ // Group eligible tool outputs by provable source identity, preserving order.
138
+ const groups = new Map();
139
+ for (let index = 0; index < trace.messages.length; index += 1) {
140
+ const message = trace.messages[index];
141
+ if (message.role !== "tool") {
142
+ continue;
143
+ }
144
+ const identity = resolveSourceIdentity(trace.messages, index);
145
+ if (identity === null) {
146
+ continue; // source identity unknown/ambiguous -> not eligible (conservative)
147
+ }
148
+ const existing = groups.get(identity);
149
+ if (existing) {
150
+ existing.push(message);
151
+ }
152
+ else {
153
+ groups.set(identity, [message]);
154
+ }
155
+ }
156
+ const findings = [];
157
+ for (const outputs of groups.values()) {
158
+ if (outputs.length < 2) {
159
+ continue;
160
+ }
161
+ const latest = outputs[outputs.length - 1];
162
+ const latestNormalized = normalizeContent(latest.content);
163
+ const supersededIds = [];
164
+ let estimatedTokens = 0;
165
+ for (let i = 0; i < outputs.length - 1; i += 1) {
166
+ const earlier = outputs[i];
167
+ // GENUINE supersession ONLY: content changed (normalized-different from the latest). An
168
+ // identical-up-to-whitespace earlier copy is the existing repeated_tool_output case -> leave it.
169
+ if (normalizeContent(earlier.content) === latestNormalized) {
170
+ continue;
171
+ }
172
+ // Same thresholds the existing detector uses, applied per compacted (earlier) output.
173
+ const earlierTokens = estimateTextTokens(earlier.content);
174
+ if (earlierTokens < minTokens || earlier.content.length < minChars) {
175
+ continue;
176
+ }
177
+ supersededIds.push(earlier.id);
178
+ estimatedTokens += earlierTokens;
179
+ }
180
+ if (supersededIds.length === 0) {
181
+ continue;
182
+ }
183
+ findings.push({
184
+ category: "superseded_same_source_read",
185
+ messageIds: [latest.id, ...supersededIds],
186
+ summary: `Superseded ${describeTool(latest)} from the same source. ` +
187
+ `Keep latest ${latest.id} (verbatim) and compact superseded ${supersededIds.join(", ")}.`,
188
+ estimatedTokens
189
+ });
190
+ }
38
191
  return findings;
39
192
  }
40
193
  export function getCompactedMessageIds(findings) {
41
194
  const compactedIds = new Set();
42
195
  for (const finding of findings) {
43
- // ONLY "repeated_tool_output" findings are ever compacted. This is the report-only
44
- // safety boundary for "repeated_skill_injection": such findings are detected and
45
- // reported but, by this guard, never enter the compacted message-id set and never
46
- // alter the compacted trace. policy-middleware also independently filters to
47
- // "repeated_tool_output" (findRepeatCandidates), so skill injections are never applied.
48
- if (finding.category !== "repeated_tool_output") {
196
+ // Categories whose later message(s) are compacted (messageIds[0] is the KEPT message):
197
+ // - "repeated_tool_output": keep the first copy, compact the later identical duplicate(s).
198
+ // - "superseded_same_source_read" (category 1): keep the LATEST authoritative read, compact the
199
+ // earlier superseded copies (messageIds[1..]).
200
+ // "repeated_skill_injection" is report-only and is NEVER compacted by this guard (its findings are
201
+ // detected/reported but never enter the compacted set; policy-middleware also independently filters).
202
+ if (finding.category !== "repeated_tool_output" && finding.category !== "superseded_same_source_read") {
49
203
  continue;
50
204
  }
51
205
  const [, ...duplicateIds] = finding.messageIds;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compaction/cli",
3
- "version": "0.1.4",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },