compressed-context 0.1.0 → 0.2.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 (57) hide show
  1. package/README.md +59 -51
  2. package/package.json +64 -64
  3. package/src/compression/active-summary-cache.ts +50 -50
  4. package/src/compression/adopt.ts +57 -57
  5. package/src/compression/auto.ts +386 -372
  6. package/src/compression/babel.ts +85 -85
  7. package/src/compression/block.ts +25 -25
  8. package/src/compression/cache-history.ts +75 -75
  9. package/src/compression/compaction.ts +189 -189
  10. package/src/compression/decompress-tool.ts +74 -74
  11. package/src/compression/decompress.ts +292 -292
  12. package/src/compression/deleted-sessions.ts +24 -24
  13. package/src/compression/distill.ts +220 -220
  14. package/src/compression/events.ts +275 -275
  15. package/src/compression/markdown.ts +53 -53
  16. package/src/compression/normalize.ts +114 -114
  17. package/src/compression/render.ts +70 -70
  18. package/src/compression/schema.ts +131 -84
  19. package/src/compression/select.ts +123 -123
  20. package/src/compression/session-message.ts +112 -112
  21. package/src/compression/settings-rpc.ts +123 -111
  22. package/src/compression/settings.ts +175 -175
  23. package/src/compression/shape.ts +75 -0
  24. package/src/compression/state.ts +248 -248
  25. package/src/compression/summarize.ts +534 -595
  26. package/src/compression/summary.ts +73 -73
  27. package/src/compression/tool.ts +184 -142
  28. package/src/compression/transcript-store.ts +258 -258
  29. package/src/compression/transcript.ts +63 -63
  30. package/src/compression/validate.ts +81 -81
  31. package/src/i18n/en.ts +93 -93
  32. package/src/i18n/index.ts +28 -28
  33. package/src/i18n/types.ts +89 -89
  34. package/src/i18n/zh.ts +93 -93
  35. package/src/index.ts +116 -108
  36. package/src/shared/blocks.ts +17 -17
  37. package/src/shared/cache.ts +199 -199
  38. package/src/shared/format.ts +23 -23
  39. package/src/shared/media.ts +184 -184
  40. package/src/shared/parse.ts +25 -25
  41. package/src/shared/parts.ts +72 -72
  42. package/src/shared/rpc.ts +145 -145
  43. package/src/shared/scan.ts +32 -32
  44. package/src/shared/settings.ts +466 -471
  45. package/src/shared/stats.ts +142 -142
  46. package/src/shared/tokens.ts +259 -259
  47. package/src/tui/chart/cache-chart.ts +237 -237
  48. package/src/tui/chart/color.ts +58 -58
  49. package/src/tui/index.tsx +109 -111
  50. package/src/tui/panel/cache.tsx +203 -203
  51. package/src/tui/panel/data.ts +105 -105
  52. package/src/tui/panel/index.tsx +176 -176
  53. package/src/tui/panel/summaries.tsx +203 -192
  54. package/src/tui/settings/dialogs.tsx +215 -215
  55. package/src/tui/settings/options.ts +238 -238
  56. package/src/tui/settings.tsx +418 -418
  57. package/src/tui/types.ts +4 -4
@@ -1,199 +1,199 @@
1
- /**
2
- * 缓存命中历史的跨进程载体。
3
- *
4
- * 服务端插件在 session.idle 时把最近 10 条 assistant 消息的 usage 追加到
5
- * storage(`cache:history:<sessionID>`);TUI 通过 RPC 读取。
6
- * 本模块只放纯函数:采样、追加、校验、排序,方便单测。
7
- */
8
-
9
- /** 每个会话一份,key 里带 sessionID,父/子会话互不影响。 */
10
- export const CACHE_HISTORY_PREFIX = "cache:history:"
11
-
12
- /** 只保留最近 10 次请求。 */
13
- export const CACHE_HISTORY_LIMIT = 10
14
-
15
- /** 一条缓存命中样本;hitRate 是 0..100,向下截断到两位小数。 */
16
- export interface CacheSample {
17
- readonly messageID: string
18
- readonly at: number
19
- readonly input: number
20
- readonly output: number
21
- readonly cached: number
22
- readonly hitRate: number
23
- }
24
-
25
- /** storage 里的持久化结构。 */
26
- export interface CacheHistory {
27
- readonly v: 1
28
- readonly samples: readonly CacheSample[]
29
- }
30
-
31
- export const EMPTY_CACHE_HISTORY: CacheHistory = { v: 1, samples: [] }
32
-
33
- /**
34
- * 输入:总输入 token + 命中 token。
35
- * 输出:0..100 的百分比,向下截断到两位小数。
36
- * 预期:不四舍五入;99.999% 显示 99.99,不会变成 100.00。
37
- */
38
- export function hitRatePercent(input: number, cached: number): number {
39
- if (input <= 0) return 0
40
- return Math.floor(Math.min(1, cached / input) * 10_000) / 100
41
- }
42
-
43
- /** 输入:命中率;输出:固定两位小数的百分比文本(不含 `%`)。 */
44
- export function formatHitRate(hitRate: number): string {
45
- return hitRate.toFixed(2)
46
- }
47
-
48
- /** 输入:sessionID;输出:该会话的 storage key。 */
49
- export function cacheHistoryKey(sessionID: string): string {
50
- return CACHE_HISTORY_PREFIX + sessionID
51
- }
52
-
53
- /**
54
- * 输入:消息(结构最小化,便于测试替身)。
55
- * 输出:assistant 且带 tokens 时返回样本,否则 undefined。
56
- */
57
- export interface CacheMessage {
58
- readonly id: string
59
- readonly type: string
60
- readonly time?: { readonly created?: number }
61
- readonly tokens?: {
62
- readonly input: number
63
- readonly output: number
64
- readonly reasoning: number
65
- readonly cache: { readonly read: number; readonly write: number }
66
- }
67
- }
68
-
69
- /** 输入:消息 + 缺时间时的回退值;输出:样本或 undefined。 */
70
- export function sampleFromMessage(message: CacheMessage, fallbackAt: number): CacheSample | undefined {
71
- if (message.type !== "assistant") return undefined
72
- const tokens = message.tokens
73
- if (tokens === undefined) return undefined
74
- const input = tokens.input + tokens.cache.read + tokens.cache.write
75
- const output = tokens.output + tokens.reasoning
76
- const cached = tokens.cache.read
77
- return {
78
- messageID: message.id,
79
- at: message.time?.created ?? fallbackAt,
80
- input,
81
- output,
82
- cached,
83
- hitRate: hitRatePercent(input, cached),
84
- }
85
- }
86
-
87
- /** 输入:样本列表;输出:按时间升序、去重、只留最新 10 条。 */
88
- function normalizeSamples(samples: readonly CacheSample[]): readonly CacheSample[] {
89
- const unique = new Map<string, CacheSample>()
90
- for (const sample of samples) {
91
- // 同一条消息重复出现时保留第一次记录,避免重试把同一条刷成多条。
92
- if (!unique.has(sample.messageID)) unique.set(sample.messageID, sample)
93
- }
94
- return [...unique.values()]
95
- .sort((left, right) => left.at - right.at || left.messageID.localeCompare(right.messageID))
96
- .slice(-CACHE_HISTORY_LIMIT)
97
- }
98
-
99
- /**
100
- * 输入:已有历史 + 当前可见消息。
101
- * 输出:追加未记录过的 assistant 样本后的新历史。
102
- * 预期:按 messageID 去重;按时间排序;只保留最新 10 条。
103
- */
104
- export function appendCacheSamples(
105
- existing: readonly CacheSample[],
106
- messages: readonly CacheMessage[],
107
- now: number = Date.now(),
108
- ): readonly CacheSample[] {
109
- const known = new Set(existing.map((sample) => sample.messageID))
110
- const added: CacheSample[] = []
111
- for (const message of messages) {
112
- if (known.has(message.id)) continue
113
- const sample = sampleFromMessage(message, now)
114
- if (sample === undefined) continue
115
- known.add(sample.messageID)
116
- added.push(sample)
117
- }
118
- return normalizeSamples([...existing, ...added])
119
- }
120
-
121
- /**
122
- * 输入:已有历史 + 单个 step 的 assistant usage。
123
- * 输出:追加该 step 样本后的新历史。
124
- * 预期:tool loop 里 context/usage.updated 拿不到最新 step usage,
125
- * `session.step.ended` 事件直接携带 tokens,这里按 messageID 去重落一条。
126
- */
127
- export function appendCacheSampleFromStep(
128
- existing: readonly CacheSample[],
129
- messageID: string,
130
- tokens: {
131
- readonly input: number
132
- readonly output: number
133
- readonly reasoning: number
134
- readonly cache: { readonly read: number; readonly write: number }
135
- },
136
- fallbackAt: number = Date.now(),
137
- ): readonly CacheSample[] {
138
- return appendCacheSamples(existing, [{ id: messageID, type: "assistant", tokens }], fallbackAt)
139
- }
140
-
141
- /** 输入:任意 storage 值;输出:合法历史;损坏时返回空历史,绝不抛错。hitRate 按 input/cached 重算。 */
142
- export function parseCacheHistory(value: unknown): CacheHistory {
143
- if (typeof value !== "object" || value === null) return EMPTY_CACHE_HISTORY
144
- const record = value as Record<string, unknown>
145
- if (record["v"] !== 1 || !Array.isArray(record["samples"])) return EMPTY_CACHE_HISTORY
146
- const samples: CacheSample[] = []
147
- for (const item of record["samples"]) {
148
- if (typeof item !== "object" || item === null) continue
149
- const sample = item as Record<string, unknown>
150
- const messageID = sample["messageID"]
151
- const at = sample["at"]
152
- const input = sample["input"]
153
- const output = sample["output"]
154
- const cached = sample["cached"]
155
- if (typeof messageID !== "string" || messageID.length === 0) continue
156
- if (
157
- typeof at !== "number" ||
158
- !Number.isFinite(at) ||
159
- at < 0 ||
160
- typeof input !== "number" ||
161
- !Number.isFinite(input) ||
162
- input < 0 ||
163
- typeof output !== "number" ||
164
- !Number.isFinite(output) ||
165
- output < 0 ||
166
- typeof cached !== "number" ||
167
- !Number.isFinite(cached) ||
168
- cached < 0
169
- ) {
170
- continue
171
- }
172
- samples.push({
173
- messageID,
174
- at,
175
- input,
176
- output,
177
- cached,
178
- hitRate: hitRatePercent(input, cached),
179
- })
180
- }
181
- return { v: 1, samples: normalizeSamples(samples) }
182
- }
183
-
184
- /** 输入:两条历史;输出:样本是否完全一致;用于避免无变化的重复写盘。 */
185
- export function cacheHistoryEquals(left: readonly CacheSample[], right: readonly CacheSample[]): boolean {
186
- if (left.length !== right.length) return false
187
- return left.every((sample, index) => {
188
- const other = right[index]
189
- if (other === undefined) return false
190
- return (
191
- sample.messageID === other.messageID &&
192
- sample.at === other.at &&
193
- sample.input === other.input &&
194
- sample.output === other.output &&
195
- sample.cached === other.cached &&
196
- sample.hitRate === other.hitRate
197
- )
198
- })
199
- }
1
+ /**
2
+ * 缓存命中历史的跨进程载体。
3
+ *
4
+ * 服务端插件在 session.idle 时把最近 10 条 assistant 消息的 usage 追加到
5
+ * storage(`cache:history:<sessionID>`);TUI 通过 RPC 读取。
6
+ * 本模块只放纯函数:采样、追加、校验、排序,方便单测。
7
+ */
8
+
9
+ /** 每个会话一份,key 里带 sessionID,父/子会话互不影响。 */
10
+ export const CACHE_HISTORY_PREFIX = "cache:history:"
11
+
12
+ /** 只保留最近 10 次请求。 */
13
+ export const CACHE_HISTORY_LIMIT = 10
14
+
15
+ /** 一条缓存命中样本;hitRate 是 0..100,向下截断到两位小数。 */
16
+ export interface CacheSample {
17
+ readonly messageID: string
18
+ readonly at: number
19
+ readonly input: number
20
+ readonly output: number
21
+ readonly cached: number
22
+ readonly hitRate: number
23
+ }
24
+
25
+ /** storage 里的持久化结构。 */
26
+ export interface CacheHistory {
27
+ readonly v: 1
28
+ readonly samples: readonly CacheSample[]
29
+ }
30
+
31
+ export const EMPTY_CACHE_HISTORY: CacheHistory = { v: 1, samples: [] }
32
+
33
+ /**
34
+ * 输入:总输入 token + 命中 token。
35
+ * 输出:0..100 的百分比,向下截断到两位小数。
36
+ * 预期:不四舍五入;99.999% 显示 99.99,不会变成 100.00。
37
+ */
38
+ export function hitRatePercent(input: number, cached: number): number {
39
+ if (input <= 0) return 0
40
+ return Math.floor(Math.min(1, cached / input) * 10_000) / 100
41
+ }
42
+
43
+ /** 输入:命中率;输出:固定两位小数的百分比文本(不含 `%`)。 */
44
+ export function formatHitRate(hitRate: number): string {
45
+ return hitRate.toFixed(2)
46
+ }
47
+
48
+ /** 输入:sessionID;输出:该会话的 storage key。 */
49
+ export function cacheHistoryKey(sessionID: string): string {
50
+ return CACHE_HISTORY_PREFIX + sessionID
51
+ }
52
+
53
+ /**
54
+ * 输入:消息(结构最小化,便于测试替身)。
55
+ * 输出:assistant 且带 tokens 时返回样本,否则 undefined。
56
+ */
57
+ export interface CacheMessage {
58
+ readonly id: string
59
+ readonly type: string
60
+ readonly time?: { readonly created?: number }
61
+ readonly tokens?: {
62
+ readonly input: number
63
+ readonly output: number
64
+ readonly reasoning: number
65
+ readonly cache: { readonly read: number; readonly write: number }
66
+ }
67
+ }
68
+
69
+ /** 输入:消息 + 缺时间时的回退值;输出:样本或 undefined。 */
70
+ export function sampleFromMessage(message: CacheMessage, fallbackAt: number): CacheSample | undefined {
71
+ if (message.type !== "assistant") return undefined
72
+ const tokens = message.tokens
73
+ if (tokens === undefined) return undefined
74
+ const input = tokens.input + tokens.cache.read + tokens.cache.write
75
+ const output = tokens.output + tokens.reasoning
76
+ const cached = tokens.cache.read
77
+ return {
78
+ messageID: message.id,
79
+ at: message.time?.created ?? fallbackAt,
80
+ input,
81
+ output,
82
+ cached,
83
+ hitRate: hitRatePercent(input, cached),
84
+ }
85
+ }
86
+
87
+ /** 输入:样本列表;输出:按时间升序、去重、只留最新 10 条。 */
88
+ function normalizeSamples(samples: readonly CacheSample[]): readonly CacheSample[] {
89
+ const unique = new Map<string, CacheSample>()
90
+ for (const sample of samples) {
91
+ // 同一条消息重复出现时保留第一次记录,避免重试把同一条刷成多条。
92
+ if (!unique.has(sample.messageID)) unique.set(sample.messageID, sample)
93
+ }
94
+ return [...unique.values()]
95
+ .sort((left, right) => left.at - right.at || left.messageID.localeCompare(right.messageID))
96
+ .slice(-CACHE_HISTORY_LIMIT)
97
+ }
98
+
99
+ /**
100
+ * 输入:已有历史 + 当前可见消息。
101
+ * 输出:追加未记录过的 assistant 样本后的新历史。
102
+ * 预期:按 messageID 去重;按时间排序;只保留最新 10 条。
103
+ */
104
+ export function appendCacheSamples(
105
+ existing: readonly CacheSample[],
106
+ messages: readonly CacheMessage[],
107
+ now: number = Date.now(),
108
+ ): readonly CacheSample[] {
109
+ const known = new Set(existing.map((sample) => sample.messageID))
110
+ const added: CacheSample[] = []
111
+ for (const message of messages) {
112
+ if (known.has(message.id)) continue
113
+ const sample = sampleFromMessage(message, now)
114
+ if (sample === undefined) continue
115
+ known.add(sample.messageID)
116
+ added.push(sample)
117
+ }
118
+ return normalizeSamples([...existing, ...added])
119
+ }
120
+
121
+ /**
122
+ * 输入:已有历史 + 单个 step 的 assistant usage。
123
+ * 输出:追加该 step 样本后的新历史。
124
+ * 预期:tool loop 里 context/usage.updated 拿不到最新 step usage,
125
+ * `session.step.ended` 事件直接携带 tokens,这里按 messageID 去重落一条。
126
+ */
127
+ export function appendCacheSampleFromStep(
128
+ existing: readonly CacheSample[],
129
+ messageID: string,
130
+ tokens: {
131
+ readonly input: number
132
+ readonly output: number
133
+ readonly reasoning: number
134
+ readonly cache: { readonly read: number; readonly write: number }
135
+ },
136
+ fallbackAt: number = Date.now(),
137
+ ): readonly CacheSample[] {
138
+ return appendCacheSamples(existing, [{ id: messageID, type: "assistant", tokens }], fallbackAt)
139
+ }
140
+
141
+ /** 输入:任意 storage 值;输出:合法历史;损坏时返回空历史,绝不抛错。hitRate 按 input/cached 重算。 */
142
+ export function parseCacheHistory(value: unknown): CacheHistory {
143
+ if (typeof value !== "object" || value === null) return EMPTY_CACHE_HISTORY
144
+ const record = value as Record<string, unknown>
145
+ if (record["v"] !== 1 || !Array.isArray(record["samples"])) return EMPTY_CACHE_HISTORY
146
+ const samples: CacheSample[] = []
147
+ for (const item of record["samples"]) {
148
+ if (typeof item !== "object" || item === null) continue
149
+ const sample = item as Record<string, unknown>
150
+ const messageID = sample["messageID"]
151
+ const at = sample["at"]
152
+ const input = sample["input"]
153
+ const output = sample["output"]
154
+ const cached = sample["cached"]
155
+ if (typeof messageID !== "string" || messageID.length === 0) continue
156
+ if (
157
+ typeof at !== "number" ||
158
+ !Number.isFinite(at) ||
159
+ at < 0 ||
160
+ typeof input !== "number" ||
161
+ !Number.isFinite(input) ||
162
+ input < 0 ||
163
+ typeof output !== "number" ||
164
+ !Number.isFinite(output) ||
165
+ output < 0 ||
166
+ typeof cached !== "number" ||
167
+ !Number.isFinite(cached) ||
168
+ cached < 0
169
+ ) {
170
+ continue
171
+ }
172
+ samples.push({
173
+ messageID,
174
+ at,
175
+ input,
176
+ output,
177
+ cached,
178
+ hitRate: hitRatePercent(input, cached),
179
+ })
180
+ }
181
+ return { v: 1, samples: normalizeSamples(samples) }
182
+ }
183
+
184
+ /** 输入:两条历史;输出:样本是否完全一致;用于避免无变化的重复写盘。 */
185
+ export function cacheHistoryEquals(left: readonly CacheSample[], right: readonly CacheSample[]): boolean {
186
+ if (left.length !== right.length) return false
187
+ return left.every((sample, index) => {
188
+ const other = right[index]
189
+ if (other === undefined) return false
190
+ return (
191
+ sample.messageID === other.messageID &&
192
+ sample.at === other.at &&
193
+ sample.input === other.input &&
194
+ sample.output === other.output &&
195
+ sample.cached === other.cached &&
196
+ sample.hitRate === other.hitRate
197
+ )
198
+ })
199
+ }
@@ -1,23 +1,23 @@
1
- /**
2
- * 面板数字格式化。
3
- *
4
- * 输入:token 数量(非负整数)。
5
- * 输出:紧凑字符串:<1000 原样、<10000 一位小数 k、<1e6 取整 k、
6
- * <1e7 一位小数 M、更大取整 M。
7
- * 预期:侧栏一行内放得下。
8
- */
9
- export function formatTokens(count: number): string {
10
- if (!Number.isFinite(count) || count <= 0) return "0"
11
- if (count < 1_000) return count.toString()
12
- if (count < 10_000) return `${(count / 1_000).toFixed(1)}k`
13
- if (count < 1_000_000) return `${Math.round(count / 1_000).toString()}k`
14
- if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`
15
- return `${Math.round(count / 1_000_000).toString()}M`
16
- }
17
-
18
- /** 输入:毫秒时间戳;输出:本地时间 `YYYY/MM/DD HH:mm`;预期:固定格式,不依赖 locale。 */
19
- export function formatTimestamp(at: number): string {
20
- const date = new Date(at)
21
- const pad = (value: number): string => value.toString().padStart(2, "0")
22
- return `${date.getFullYear().toString()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
23
- }
1
+ /**
2
+ * 面板数字格式化。
3
+ *
4
+ * 输入:token 数量(非负整数)。
5
+ * 输出:紧凑字符串:<1000 原样、<10000 一位小数 k、<1e6 取整 k、
6
+ * <1e7 一位小数 M、更大取整 M。
7
+ * 预期:侧栏一行内放得下。
8
+ */
9
+ export function formatTokens(count: number): string {
10
+ if (!Number.isFinite(count) || count <= 0) return "0"
11
+ if (count < 1_000) return count.toString()
12
+ if (count < 10_000) return `${(count / 1_000).toFixed(1)}k`
13
+ if (count < 1_000_000) return `${Math.round(count / 1_000).toString()}k`
14
+ if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`
15
+ return `${Math.round(count / 1_000_000).toString()}M`
16
+ }
17
+
18
+ /** 输入:毫秒时间戳;输出:本地时间 `YYYY/MM/DD HH:mm`;预期:固定格式,不依赖 locale。 */
19
+ export function formatTimestamp(at: number): string {
20
+ const date = new Date(at)
21
+ const pad = (value: number): string => value.toString().padStart(2, "0")
22
+ return `${date.getFullYear().toString()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
23
+ }