pi-midcompact 0.3.0 → 0.5.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.
package/src/state.ts CHANGED
@@ -1,4 +1,16 @@
1
- import type { CompressionState, DraftPlan, SessionEntryLike, TransactionState } from "./types.js";
1
+ // Persistence boundary. Owns restoration of CompressionState, TransactionState,
2
+ // and DraftPlan from the session branch. Provides backward-compatible defaults
3
+ // so old v1 state (without startMode) restores without migration. The runtime
4
+ // planning lock is not persisted here; it lives only in memory.
5
+
6
+ import type {
7
+ CompressionState,
8
+ DraftPlan,
9
+ DraftRange,
10
+ SessionEntryLike,
11
+ StartMode,
12
+ TransactionState,
13
+ } from "./types.js";
2
14
 
3
15
  export const STATE_ENTRY = "midcompact-state";
4
16
  export const TXN_ENTRY = "midcompact-transaction";
@@ -12,6 +24,11 @@ export function emptyCompressionState(): CompressionState {
12
24
  return { version: 1, createdAt: new Date().toISOString(), blocks: [] };
13
25
  }
14
26
 
27
+ /** Default routing for transactions persisted before startMode existed. */
28
+ export function defaultStartMode(mode: StartMode | undefined): StartMode {
29
+ return mode ?? "agent";
30
+ }
31
+
15
32
  export function restoreCompressionState(entries: readonly unknown[]): CompressionState | undefined {
16
33
  let latest: CompressionState | undefined;
17
34
  for (const raw of entries) {
@@ -22,7 +39,10 @@ export function restoreCompressionState(entries: readonly unknown[]): Compressio
22
39
  return latest;
23
40
  }
24
41
 
25
- export function restoreTransaction(entries: readonly unknown[]): { transaction?: TransactionState; draft?: DraftPlan } {
42
+ export function restoreTransaction(entries: readonly unknown[]): {
43
+ transaction?: TransactionState;
44
+ draft?: DraftPlan;
45
+ } {
26
46
  let transaction: TransactionState | undefined;
27
47
  let draft: DraftPlan | undefined;
28
48
  for (const raw of entries) {
@@ -62,3 +82,20 @@ function isDraft(value: unknown): value is DraftPlan {
62
82
  const plan = value as Record<string, unknown>;
63
83
  return plan.version === 1 && typeof plan.transactionId === "string" && typeof plan.revision === "number" && Array.isArray(plan.ranges);
64
84
  }
85
+
86
+ /**
87
+ * An old DraftRange may lack the factual char/image fields. Coerce to the
88
+ * current shape so callers always see the new fields; missing values default to
89
+ * 0 and are recomputed by the next mutation.
90
+ */
91
+ export function coerceDraftRange(range: DraftRange): DraftRange {
92
+ return {
93
+ ...range,
94
+ originalContentChars: typeof range.originalContentChars === "number" ? range.originalContentChars : 0,
95
+ originalImageCount: typeof range.originalImageCount === "number" ? range.originalImageCount : 0,
96
+ originalImagePayloadBytes: typeof range.originalImagePayloadBytes === "number" ? range.originalImagePayloadBytes : 0,
97
+ replacementContentChars: typeof range.replacementContentChars === "number" ? range.replacementContentChars : 0,
98
+ originalApproxTokens: typeof range.originalApproxTokens === "number" ? range.originalApproxTokens : 0,
99
+ compressedApproxTokens: typeof range.compressedApproxTokens === "number" ? range.compressedApproxTokens : 0,
100
+ };
101
+ }
package/src/telemetry.ts CHANGED
@@ -1,12 +1,16 @@
1
+ // Telemetry presentation. Owns Pi-reported usage formatting and factual
2
+ // char/image aggregation. Agent-facing output no longer presents local
3
+ // character-count-derived token estimates or projected token percentages as
4
+ // authoritative figures. Legacy approximate-token fields remain computable for
5
+ // backward-compatible readers but are not shown in new Agent-facing output.
6
+
1
7
  import type { ContextUsageSnapshot, DraftPlan, DraftTelemetry, TransactionState } from "./types.js";
2
8
 
3
- interface ContextUsageLike {
9
+ export function snapshotContextUsage(usage: {
4
10
  tokens: number | null;
5
11
  contextWindow: number;
6
12
  percent: number | null;
7
- }
8
-
9
- export function snapshotContextUsage(usage: ContextUsageLike | undefined): ContextUsageSnapshot | undefined {
13
+ } | undefined): ContextUsageSnapshot | undefined {
10
14
  if (!usage || !Number.isFinite(usage.contextWindow) || usage.contextWindow <= 0) return undefined;
11
15
  return {
12
16
  tokens: typeof usage.tokens === "number" && Number.isFinite(usage.tokens) ? Math.max(0, Math.round(usage.tokens)) : null,
@@ -16,8 +20,28 @@ export function snapshotContextUsage(usage: ContextUsageLike | undefined): Conte
16
20
  };
17
21
  }
18
22
 
19
- export function draftTelemetry(transaction: TransactionState | undefined, draft: DraftPlan | undefined): DraftTelemetry {
23
+ /**
24
+ * Build draft telemetry. The factual char/image fields are authoritative for
25
+ * new output; legacy approximate-token fields are kept only for old readers.
26
+ */
27
+ export function draftTelemetry(
28
+ transaction: TransactionState | undefined,
29
+ draft: DraftPlan | undefined,
30
+ ): DraftTelemetry {
20
31
  const ranges = draft?.ranges ?? [];
32
+ let selectedOriginalContentChars = 0;
33
+ let selectedReplacementContentChars = 0;
34
+ let selectedImageCount = 0;
35
+ let selectedImagePayloadBytes = 0;
36
+ let pendingSummaryCount = 0;
37
+ for (const range of ranges) {
38
+ selectedOriginalContentChars += range.originalContentChars;
39
+ selectedReplacementContentChars += range.replacementContentChars;
40
+ selectedImageCount += range.originalImageCount;
41
+ selectedImagePayloadBytes += range.originalImagePayloadBytes;
42
+ if (range.summary.trim().length === 0) pendingSummaryCount += 1;
43
+ }
44
+ // Legacy approximate-token sums, kept for backward-compatible readers only.
21
45
  const selectedOriginalApproxTokens = ranges.reduce((sum, range) => sum + range.originalApproxTokens, 0);
22
46
  const selectedCompressedApproxTokens = ranges.reduce((sum, range) => sum + range.compressedApproxTokens, 0);
23
47
  const estimatedSavedTokens = Math.max(0, selectedOriginalApproxTokens - selectedCompressedApproxTokens);
@@ -29,9 +53,16 @@ export function draftTelemetry(transaction: TransactionState | undefined, draft:
29
53
  ? null
30
54
  : (projectedTokens / contextWindow) * 100;
31
55
  return {
32
- anchorTokens,
56
+ anchorUsage: transaction?.anchorUsage,
33
57
  contextWindow,
58
+ anchorTokens,
34
59
  anchorPercent,
60
+ selectedOriginalContentChars,
61
+ selectedReplacementContentChars,
62
+ selectedImageCount,
63
+ selectedImagePayloadBytes,
64
+ rangeCount: ranges.length,
65
+ pendingSummaryCount,
35
66
  selectedOriginalApproxTokens,
36
67
  selectedCompressedApproxTokens,
37
68
  estimatedSavedTokens,
@@ -53,25 +84,24 @@ export function formatPercent(percent: number | null, approximate = false): stri
53
84
  return `${approximate ? "~" : ""}${trim(percent)}%`;
54
85
  }
55
86
 
87
+ /**
88
+ * Agent-facing awareness summary. Presents Pi-reported usage (labelled as such)
89
+ * and factual char/image stats only. No local token-savings or projected-token-
90
+ * percentage claims.
91
+ */
56
92
  export function formatTelemetry(telemetry: DraftTelemetry): string {
57
- const lines = ["Context awareness (informational, not a target):"];
58
- if (telemetry.contextWindow !== null) {
93
+ const lines = ["Context awareness (Pi reported; informational, not a target):"];
94
+ const usage = telemetry.anchorUsage;
95
+ if (usage?.contextWindow) {
59
96
  lines.push(
60
- `anchor: ${formatTokenCount(telemetry.anchorTokens)} / ${formatTokenCount(telemetry.contextWindow)} (${formatPercent(telemetry.anchorPercent)})`,
97
+ `anchor: ${formatTokenCount(usage.tokens)} / ${formatTokenCount(usage.contextWindow)} (${formatPercent(usage.percent)}) [Pi reported]`,
61
98
  );
62
99
  } else {
63
- lines.push("anchor: Pi context usage unavailable");
100
+ lines.push("anchor: Pi context usage unavailable [Pi reported; not derived from local char counts].");
64
101
  }
65
102
  lines.push(
66
- `draft selection: ~${formatTokenCount(telemetry.selectedOriginalApproxTokens)} → ~${formatTokenCount(telemetry.selectedCompressedApproxTokens)} (save ~${formatTokenCount(telemetry.estimatedSavedTokens)})`,
103
+ `draft: ${telemetry.rangeCount} range(s) · ${telemetry.selectedOriginalContentChars} → ${telemetry.selectedReplacementContentChars} content chars · ${telemetry.selectedImageCount} images (${telemetry.selectedImagePayloadBytes} payload bytes) · ${telemetry.pendingSummaryCount} pending summary`,
67
104
  );
68
- if (telemetry.contextWindow !== null && telemetry.projectedTokens !== null) {
69
- lines.push(
70
- `projected if committed now: ~${formatTokenCount(telemetry.projectedTokens)} / ${formatTokenCount(telemetry.contextWindow)} (${formatPercent(telemetry.projectedPercent, true)})`,
71
- );
72
- } else {
73
- lines.push("projected total: unavailable until Pi reports anchor usage");
74
- }
75
105
  return lines.join("\n");
76
106
  }
77
107
 
package/src/types.ts CHANGED
@@ -1,3 +1,6 @@
1
+ // Shared data contracts. This module defines structures only; it owns no
2
+ // metric, grouping, selection, or persistence rules.
3
+
1
4
  export interface MessageLike {
2
5
  role: string;
3
6
  content?: unknown;
@@ -9,6 +12,7 @@ export interface MessageLike {
9
12
  command?: string;
10
13
  output?: string;
11
14
  summary?: string;
15
+ display?: boolean;
12
16
  }
13
17
 
14
18
  export interface SessionEntryLike {
@@ -36,6 +40,26 @@ export type AtomKind =
36
40
  | "orphan_tool_result"
37
41
  | "other";
38
42
 
43
+ /** Factual measurement of a single image content part. */
44
+ export interface ImageFact {
45
+ /** Sequence within the owning message. */
46
+ index: number;
47
+ mimeType: string;
48
+ /** Decoded base64 payload length in bytes. */
49
+ payloadBytes: number;
50
+ /** Pixel dimensions when reliably readable; absent otherwise. */
51
+ width?: number;
52
+ height?: number;
53
+ }
54
+
55
+ /** Factual content statistics for a message, atom, group, or range. */
56
+ export interface ContentMetrics {
57
+ /** Unicode code point count of visible content text fields. */
58
+ contentChars: number;
59
+ imageCount: number;
60
+ images: ImageFact[];
61
+ }
62
+
39
63
  export interface Atom {
40
64
  ref: string;
41
65
  index: number;
@@ -45,6 +69,9 @@ export interface Atom {
45
69
  messageKeys: string[];
46
70
  preview: string;
47
71
  fullText: string;
72
+ /** Factual content metrics aggregated across this atom's messages. */
73
+ metrics: ContentMetrics;
74
+ /** @deprecated legacy approximate token estimate; kept for old readers, not authoritative. */
48
75
  approxTokens: number;
49
76
  compressible: boolean;
50
77
  protocolClosed: boolean;
@@ -67,7 +94,17 @@ export interface CompressionBlock {
67
94
  entryIds: string[];
68
95
  messageKeys: string[];
69
96
  createdAt: string;
97
+ /** Factual original content chars replaced by this block. */
98
+ originalContentChars: number;
99
+ /** Factual image count in the original range. */
100
+ originalImageCount: number;
101
+ /** Factual decoded image payload bytes in the original range. */
102
+ originalImagePayloadBytes: number;
103
+ /** Factual replacement message content chars (midcompact wrapper + summary). */
104
+ replacementContentChars: number;
105
+ /** @deprecated legacy approximate token estimate; kept for old readers, not authoritative. */
70
106
  originalApproxTokens: number;
107
+ /** @deprecated legacy approximate token estimate; kept for old readers, not authoritative. */
71
108
  compressedApproxTokens: number;
72
109
  }
73
110
 
@@ -76,10 +113,16 @@ export interface CommitStats {
76
113
  committedAt: string;
77
114
  addedBlockIds: string[];
78
115
  addedRangeCount: number;
116
+ /** Factual totals for the committed ranges. */
117
+ selectedOriginalContentChars: number;
118
+ selectedReplacementContentChars: number;
119
+ selectedImageCount: number;
120
+ selectedImagePayloadBytes: number;
121
+ anchorUsage?: ContextUsageSnapshot;
122
+ /** @deprecated legacy approximate token fields; kept for old readers, not authoritative. */
79
123
  selectedOriginalApproxTokens: number;
80
124
  selectedCompressedApproxTokens: number;
81
125
  estimatedSavedTokens: number;
82
- anchorUsage?: ContextUsageSnapshot;
83
126
  projectedTokens: number | null;
84
127
  projectedPercent: number | null;
85
128
  }
@@ -99,10 +142,19 @@ export interface DraftRange {
99
142
  startIndex: number;
100
143
  endIndex: number;
101
144
  topic?: string;
145
+ /** Empty until a summary is written; a range with empty summary is "pending". */
102
146
  summary: string;
103
147
  entryIds: string[];
104
148
  messageKeys: string[];
149
+ /** Factual original content chars in this range. */
150
+ originalContentChars: number;
151
+ originalImageCount: number;
152
+ originalImagePayloadBytes: number;
153
+ /** Factual replacement message content chars (wrapper + summary). */
154
+ replacementContentChars: number;
155
+ /** @deprecated legacy approximate token estimate; kept for old readers, not authoritative. */
105
156
  originalApproxTokens: number;
157
+ /** @deprecated legacy approximate token estimate; kept for old readers, not authoritative. */
106
158
  compressedApproxTokens: number;
107
159
  startPreview: string;
108
160
  endPreview: string;
@@ -115,19 +167,56 @@ export interface DraftPlan {
115
167
  ranges: DraftRange[];
116
168
  }
117
169
 
170
+ /** How a transaction's initial DraftPlan editing is routed. Informational only; */
171
+ /** both routes operate on the same DraftPlan and do not freeze boundaries. */
172
+ export type StartMode = "agent" | "user";
173
+
118
174
  export interface TransactionState {
119
175
  version: 1;
120
176
  id: string;
121
177
  anchorEntryId: string;
122
178
  startedAt: string;
179
+ /** Initial routing chosen at start: Agent-first prompt or User-first UI. */
180
+ startMode?: StartMode;
123
181
  /** Frozen awareness captured when /midcompact starts; informational, never a target. */
124
182
  anchorUsage?: ContextUsageSnapshot;
125
183
  }
126
184
 
185
+ /** Who currently holds the runtime mutex over DraftPlan edits. Not persisted. */
186
+ export type PlanningLockOwner = "agent" | "ui";
187
+
188
+ /** A requested atom span that may cross KEEP/protected atoms; fed into selection normalization. */
189
+ export interface SelectionSpan {
190
+ startRef: string;
191
+ endRef: string;
192
+ }
193
+
194
+ /** A finalized ordinary span after KEEP/protected subtraction. */
195
+ export interface OrdinarySpan {
196
+ startRef: string;
197
+ endRef: string;
198
+ startIndex: number;
199
+ endIndex: number;
200
+ }
201
+
127
202
  export interface DraftTelemetry {
128
- anchorTokens: number | null;
203
+ /** Pi-reported usage at the frozen anchor; informational. */
204
+ anchorUsage?: ContextUsageSnapshot;
205
+ /** @deprecated Pi-reported anchor context window; use anchorUsage. Kept for UI readers. */
129
206
  contextWindow: number | null;
207
+ /** @deprecated Pi-reported anchor tokens; use anchorUsage. Kept for UI readers. */
208
+ anchorTokens: number | null;
209
+ /** @deprecated Pi-reported anchor percent; use anchorUsage. Kept for UI readers. */
130
210
  anchorPercent: number | null;
211
+ /** Factual selected metrics. */
212
+ selectedOriginalContentChars: number;
213
+ selectedReplacementContentChars: number;
214
+ selectedImageCount: number;
215
+ selectedImagePayloadBytes: number;
216
+ rangeCount: number;
217
+ /** Ranges still awaiting a non-empty summary. */
218
+ pendingSummaryCount: number;
219
+ /** @deprecated legacy approximate token fields; kept for old readers, not authoritative. */
131
220
  selectedOriginalApproxTokens: number;
132
221
  selectedCompressedApproxTokens: number;
133
222
  estimatedSavedTokens: number;
@@ -145,6 +234,56 @@ export interface LocateQuery {
145
234
  detail?: "brief" | "full";
146
235
  }
147
236
 
237
+ export interface InventoryQuery {
238
+ pageSize?: number;
239
+ cursor?: string;
240
+ }
241
+
242
+ export interface InventoryGroup {
243
+ ref: string;
244
+ label: string;
245
+ isPrefix: boolean;
246
+ startAtomRef: string;
247
+ endAtomRef: string;
248
+ atomCount: number;
249
+ messageCount: number;
250
+ contentChars: number;
251
+ imageCount: number;
252
+ imagePayloadBytes: number;
253
+ imageMimeTypes: string[];
254
+ protectedAtomCount: number;
255
+ compressibleAtomCount: number;
256
+ }
257
+
258
+ export interface InventoryTotals {
259
+ atomCount: number;
260
+ messageCount: number;
261
+ contentChars: number;
262
+ imageCount: number;
263
+ imagePayloadBytes: number;
264
+ groupCount: number;
265
+ protectedAtomCount: number;
266
+ compressibleAtomCount: number;
267
+ }
268
+
269
+ export interface InventoryPiUsage {
270
+ available: boolean;
271
+ contextWindow: number | null;
272
+ tokens: number | null;
273
+ percent: number | null;
274
+ /** Provenance label, e.g. "Pi reported at anchor start". */
275
+ provenance: string;
276
+ }
277
+
278
+ export interface InventoryPage {
279
+ anchor: { transactionId: string; anchorEntryId: string };
280
+ piUsage: InventoryPiUsage;
281
+ totals: InventoryTotals;
282
+ groups: InventoryGroup[];
283
+ nextCursor: string | null;
284
+ pageSize: number;
285
+ }
286
+
148
287
  export type ReviewAction =
149
288
  | { action: "close" }
150
289
  | { action: "edit-summary"; draftId: string }