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
@@ -0,0 +1,75 @@
1
+ import type { SummaryFormat } from "../shared/settings.ts"
2
+
3
+ /**
4
+ * 本模块只放「摘要长什么样」的文案。
5
+ *
6
+ * 压缩提示词(summarize.ts)和 compress 工具描述(tool.ts)都从这里取,
7
+ * 避免两处各写一份、改一处忘另一处。
8
+ */
9
+
10
+ const JSON_SHAPE = `{
11
+ "title": "optional short label for this block",
12
+ "goal": "required: the objective and any pivots it went through",
13
+ "constraints": ["optional: hard limits and user preferences"],
14
+ "decisions": ["optional: what was decided — why"],
15
+ "progress": {
16
+ "completed": ["optional: work actually finished"],
17
+ "active": ["optional: work in progress, including what the user still expects"],
18
+ "blocked": ["optional: what is blocked — why"]
19
+ },
20
+ "open_items": ["optional: unanswered questions, TODOs, next actions"],
21
+ "files": ["optional: real file paths only, full/path.ext:line — role in the work"],
22
+ "facts": ["optional: exact errors, signatures, versions, config keys, thresholds, measured values"]
23
+ }`
24
+
25
+ const MARKDOWN_SHAPE = `# Short title
26
+ ## Goal
27
+ ...
28
+ ## Constraints
29
+ - ...
30
+ ## Decisions
31
+ - ...
32
+ ## Progress
33
+ ### Completed
34
+ - ...
35
+ ### Active
36
+ - ...
37
+ ### Blocked
38
+ - ...
39
+ ## Open Items
40
+ - ...
41
+ ## Files
42
+ - ...
43
+ ## Facts
44
+ - ...`
45
+
46
+ /** 输入:格式 + 是否要 candidate 行;输出:格式说明行。 */
47
+ export function shapeLines(format: SummaryFormat | undefined, withCandidate: boolean): string[] {
48
+ if (format === "babel") {
49
+ return [
50
+ withCandidate
51
+ ? "Return ONLY Babel text. First line must be `c=N`, where N is the chosen candidate; second line must be `T=<short title>|G=<one-line goal>`."
52
+ : "Return ONLY Babel text. First line must be `T=<short title>|G=<one-line goal>`.",
53
+ "After the header, use adaptive Babel: only T/G is fixed; invent the shortest anchors/delimiters for this source. No fixed key schema, no Markdown headings, no JSON, no code fences, no commentary.",
54
+ ]
55
+ }
56
+ if (format === "markdown") {
57
+ return [
58
+ withCandidate
59
+ ? "Return ONLY markdown. First line must be `candidate: N`, then a self-contained markdown checkpoint."
60
+ : "Return ONLY markdown. Do not return JSON. Do not wrap in code fences.",
61
+ MARKDOWN_SHAPE,
62
+ ]
63
+ }
64
+ return [
65
+ withCandidate
66
+ ? "Return ONLY a JSON object with the normal summary shape PLUS a top-level `candidate` number:"
67
+ : "Return ONLY a JSON object with this exact shape (no markdown fences, no commentary):",
68
+ JSON_SHAPE,
69
+ ]
70
+ }
71
+
72
+ /** 输入:摘要格式;输出:该格式的写法说明,供压缩提示词和工具描述共用。 */
73
+ export function summaryShape(format: SummaryFormat): string {
74
+ return shapeLines(format, false).join("\n")
75
+ }
@@ -1,248 +1,248 @@
1
- import type { Schema } from "effect"
2
-
3
- import { toBabelSummary } from "./babel.ts"
4
- import { isSessionDeleted } from "./deleted-sessions.ts"
5
- import { type Summary, type SummaryFields } from "./schema.ts"
6
-
7
- /** 压缩层级:1 = 原文摘要,2/3 = 摘要再蒸馏。 */
8
- export type Tier = 1 | 2 | 3
9
-
10
- /** 下一次 compaction 的目的;缺省表示 T1。 */
11
- export type CompactionPhase = "t2" | "t3"
12
-
13
- /** 一个已落盘或待落盘的压缩块。 */
14
- export interface Block {
15
- readonly id: string
16
- readonly tier: Tier
17
- readonly summary: Summary
18
- readonly fromId: string
19
- readonly toId: string
20
- readonly tokensBefore: number
21
- readonly tokensAfter: number
22
- readonly at: number
23
- /** active 链中的顺序;合并块占第一个源块的位置。 */
24
- readonly order: number
25
- /** 被本块合并的源块;空表示原始 T1 块。 */
26
- readonly sourceBlockIds?: readonly string[]
27
- }
28
-
29
- /** 每个会话一份的压缩状态。 */
30
- export interface State {
31
- readonly blocks: readonly Block[]
32
- readonly pending?: Block
33
- /** 下一次 compaction 是二级压缩;T1 时缺省。 */
34
- readonly phase?: CompactionPhase
35
- readonly attempts: number
36
- readonly lastError?: string
37
- /** 上一次主动压缩提醒时的上下文 token 数;缺省表示还没建立基线。 */
38
- readonly nudgeBaselineTokens?: number
39
- /** 已发出的主动压缩提醒次数。 */
40
- readonly nudgeCount?: number
41
- /** 全 T3 且无法继续压缩时的终止原因;存在时停止自动压缩。 */
42
- readonly terminal?: string
43
- }
44
-
45
- /** 服务端 storage 的最小接口,便于测试替身。 */
46
- export interface StorageLike {
47
- get(key: string): Promise<Schema.Json | undefined>
48
- set(key: string, value: Schema.Json): Promise<void>
49
- remove(key: string): Promise<void>
50
- }
51
-
52
- export const STATE_PREFIX = "compression:state:"
53
-
54
- /** 连续压缩失败达到这个次数后暂停自动压缩;同一层内还会做摘要重试。 */
55
- export const MAX_COMPRESSION_ATTEMPTS = 3
56
-
57
- export const EMPTY_STATE: State = { blocks: [], attempts: 0 }
58
-
59
- /** 输入:sessionID;输出:storage key。 */
60
- export function stateKey(sessionID: string): string {
61
- return STATE_PREFIX + sessionID
62
- }
63
-
64
- /** 输入:块列表;输出:下一个可用的 order。 */
65
- export function nextOrder(blocks: readonly Block[]): number {
66
- return blocks.reduce((max, block) => Math.max(max, block.order), 0) + 1
67
- }
68
-
69
- /** 输入:已落盘块 + 可选 pending;输出:下一个可用的 bN id。 */
70
- export function nextBlockId(blocks: readonly Block[], pending?: Block): string {
71
- let max = 0
72
- const all = pending === undefined ? blocks : [...blocks, pending]
73
- for (const block of all) {
74
- const numeric = Number(block.id.startsWith("b") ? block.id.slice(1) : "")
75
- if (Number.isInteger(numeric) && numeric > max) max = numeric
76
- }
77
- return `b${(max + 1).toString()}`
78
- }
79
-
80
- function isStringArray(value: unknown): value is readonly string[] {
81
- return Array.isArray(value) && value.every((item) => typeof item === "string")
82
- }
83
-
84
- /** 输入:存储里的摘要;输出:JSON 摘要字段;形状不合法时 undefined。 */
85
- function parseFields(value: unknown): SummaryFields | undefined {
86
- if (typeof value !== "object" || value === null) return undefined
87
- const fields = value as Record<string, unknown>
88
- if (typeof fields["title"] !== "string" || typeof fields["goal"] !== "string") return undefined
89
- if (!isStringArray(fields["constraints"])) return undefined
90
- if (!isStringArray(fields["decisions"])) return undefined
91
- if (!isStringArray(fields["open_items"])) return undefined
92
- if (!isStringArray(fields["files"])) return undefined
93
- if (!isStringArray(fields["facts"])) return undefined
94
- const progress = fields["progress"]
95
- if (typeof progress !== "object" || progress === null) return undefined
96
- const p = progress as Record<string, unknown>
97
- if (!isStringArray(p["completed"]) || !isStringArray(p["active"]) || !isStringArray(p["blocked"])) {
98
- return undefined
99
- }
100
- return {
101
- title: fields["title"],
102
- goal: fields["goal"],
103
- constraints: fields["constraints"],
104
- decisions: fields["decisions"],
105
- progress: { completed: p["completed"], active: p["active"], blocked: p["blocked"] },
106
- open_items: fields["open_items"],
107
- files: fields["files"],
108
- facts: fields["facts"],
109
- }
110
- }
111
-
112
- /** 输入:存储里的摘要;输出:规范摘要;形状不合法时 undefined。 */
113
- function parseSummary(value: unknown): Summary | undefined {
114
- if (typeof value !== "object" || value === null) return undefined
115
- const record = value as Record<string, unknown>
116
- if (record["format"] === "markdown") {
117
- if (typeof record["title"] !== "string" || typeof record["text"] !== "string") return undefined
118
- return { format: "markdown", title: record["title"], text: record["text"] }
119
- }
120
- if (record["format"] === "babel") {
121
- if (typeof record["text"] !== "string") return undefined
122
- return toBabelSummary(
123
- record["text"],
124
- typeof record["title"] === "string" ? record["title"] : undefined,
125
- typeof record["goal"] === "string" ? record["goal"] : undefined,
126
- )
127
- }
128
- if ("format" in record) return undefined
129
- return parseFields(value)
130
- }
131
-
132
- /** 输入:存储里的块;输出:规范块;形状不合法时 undefined。 */
133
- function parseBlock(value: unknown): Block | undefined {
134
- if (typeof value !== "object" || value === null) return undefined
135
- const block = value as Record<string, unknown>
136
- if (typeof block["id"] !== "string") return undefined
137
- const tier = block["tier"]
138
- if (tier !== 1 && tier !== 2 && tier !== 3) return undefined
139
- if (typeof block["fromId"] !== "string" || typeof block["toId"] !== "string") return undefined
140
- if (typeof block["tokensBefore"] !== "number" || typeof block["tokensAfter"] !== "number") return undefined
141
- if (typeof block["at"] !== "number" || typeof block["order"] !== "number") return undefined
142
- const sourceBlockIds = block["sourceBlockIds"]
143
- if (sourceBlockIds !== undefined && !isStringArray(sourceBlockIds)) return undefined
144
- const summary = parseSummary(block["summary"])
145
- if (summary === undefined) return undefined
146
- return {
147
- id: block["id"],
148
- tier,
149
- summary,
150
- fromId: block["fromId"],
151
- toId: block["toId"],
152
- tokensBefore: block["tokensBefore"],
153
- tokensAfter: block["tokensAfter"],
154
- at: block["at"],
155
- order: block["order"],
156
- ...(sourceBlockIds === undefined ? {} : { sourceBlockIds }),
157
- }
158
- }
159
-
160
- /**
161
- * 输入:storage 里的原始值。
162
- * 输出:合法状态;损坏时返回空状态。
163
- * 预期:不读版本号、不做迁移;坏块直接丢弃。
164
- */
165
- export function parseState(value: unknown): State {
166
- if (typeof value !== "object" || value === null) return EMPTY_STATE
167
- const record = value as Record<string, unknown>
168
- const blocks = Array.isArray(record["blocks"])
169
- ? record["blocks"].map(parseBlock).filter((block): block is Block => block !== undefined)
170
- : []
171
- const pending = parseBlock(record["pending"])
172
- const attempts = typeof record["attempts"] === "number" ? record["attempts"] : 0
173
- const lastError = typeof record["lastError"] === "string" ? record["lastError"] : undefined
174
- const nudgeBaselineTokens =
175
- typeof record["nudgeBaselineTokens"] === "number" ? record["nudgeBaselineTokens"] : undefined
176
- const nudgeCount = typeof record["nudgeCount"] === "number" ? record["nudgeCount"] : undefined
177
- const terminal = typeof record["terminal"] === "string" ? record["terminal"] : undefined
178
- const rawPhase = record["phase"]
179
- const phase: CompactionPhase | undefined = rawPhase === "t2" || rawPhase === "t3" ? rawPhase : undefined
180
- return {
181
- blocks,
182
- attempts,
183
- ...(pending === undefined ? {} : { pending }),
184
- ...(lastError === undefined ? {} : { lastError }),
185
- ...(nudgeBaselineTokens === undefined ? {} : { nudgeBaselineTokens }),
186
- ...(nudgeCount === undefined ? {} : { nudgeCount }),
187
- ...(terminal === undefined ? {} : { terminal }),
188
- ...(phase === undefined ? {} : { phase }),
189
- }
190
- }
191
-
192
- /** 输入:storage + sessionID;输出:状态。 */
193
- export async function loadState(storage: StorageLike, sessionID: string): Promise<State> {
194
- return parseState(await storage.get(stateKey(sessionID)))
195
- }
196
-
197
- /** 输入:storage + sessionID + 状态;输出:无;预期:写入 JSON;已删会话直接跳过。 */
198
- export async function saveState(storage: StorageLike, sessionID: string, state: State): Promise<void> {
199
- if (isSessionDeleted(sessionID)) return
200
- await storage.set(stateKey(sessionID), state as unknown as Schema.Json)
201
- }
202
-
203
- /** 输入:状态;输出:pending 转正后的新状态;预期:成功时清 lastError/terminal,保留下一层 phase。 */
204
- export function commitPending(state: State): State {
205
- if (!state.pending) return state
206
- return {
207
- blocks: [...state.blocks, state.pending],
208
- attempts: 0,
209
- ...(state.nudgeCount === undefined ? {} : { nudgeCount: state.nudgeCount }),
210
- ...(state.phase === undefined ? {} : { phase: state.phase }),
211
- }
212
- }
213
-
214
- /** 输入:状态 + 错误;输出:丢弃 pending 后的新状态;预期:失败时清 pending/phase,保留 terminal。 */
215
- export function dropPending(state: State, error: string): State {
216
- return {
217
- blocks: state.blocks,
218
- attempts: state.attempts + 1,
219
- lastError: error,
220
- ...(state.nudgeBaselineTokens === undefined ? {} : { nudgeBaselineTokens: state.nudgeBaselineTokens }),
221
- ...(state.nudgeCount === undefined ? {} : { nudgeCount: state.nudgeCount }),
222
- ...(state.terminal === undefined ? {} : { terminal: state.terminal }),
223
- }
224
- }
225
-
226
- /**
227
- * 输入:失败后的状态 + 错误。
228
- * 输出:清 pending,但保留 phase 以便下一次请求重试。
229
- * 预期:attempts 达到上限后清掉 phase,避免无限重试。
230
- */
231
- export function retryPending(state: State, error: string): State {
232
- const attempts = state.attempts + 1
233
- const keepPhase = state.phase !== undefined && attempts < MAX_COMPRESSION_ATTEMPTS
234
- return {
235
- blocks: state.blocks,
236
- attempts,
237
- lastError: error,
238
- ...(state.nudgeBaselineTokens === undefined ? {} : { nudgeBaselineTokens: state.nudgeBaselineTokens }),
239
- ...(state.nudgeCount === undefined ? {} : { nudgeCount: state.nudgeCount }),
240
- ...(keepPhase ? { phase: state.phase } : {}),
241
- ...(state.terminal === undefined ? {} : { terminal: state.terminal }),
242
- }
243
- }
244
-
245
- /** 输入:状态 + 新块;输出:挂上 pending 的新状态。 */
246
- export function withPending(state: State, block: Block): State {
247
- return { ...state, pending: block }
248
- }
1
+ import type { Schema } from "effect"
2
+
3
+ import { toBabelSummary } from "./babel.ts"
4
+ import { isSessionDeleted } from "./deleted-sessions.ts"
5
+ import { type Summary, type SummaryFields } from "./schema.ts"
6
+
7
+ /** 压缩层级:1 = 原文摘要,2/3 = 摘要再蒸馏。 */
8
+ export type Tier = 1 | 2 | 3
9
+
10
+ /** 下一次 compaction 的目的;缺省表示 T1。 */
11
+ export type CompactionPhase = "t2" | "t3"
12
+
13
+ /** 一个已落盘或待落盘的压缩块。 */
14
+ export interface Block {
15
+ readonly id: string
16
+ readonly tier: Tier
17
+ readonly summary: Summary
18
+ readonly fromId: string
19
+ readonly toId: string
20
+ readonly tokensBefore: number
21
+ readonly tokensAfter: number
22
+ readonly at: number
23
+ /** active 链中的顺序;合并块占第一个源块的位置。 */
24
+ readonly order: number
25
+ /** 被本块合并的源块;空表示原始 T1 块。 */
26
+ readonly sourceBlockIds?: readonly string[]
27
+ }
28
+
29
+ /** 每个会话一份的压缩状态。 */
30
+ export interface State {
31
+ readonly blocks: readonly Block[]
32
+ readonly pending?: Block
33
+ /** 下一次 compaction 是二级压缩;T1 时缺省。 */
34
+ readonly phase?: CompactionPhase
35
+ readonly attempts: number
36
+ readonly lastError?: string
37
+ /** 上一次主动压缩提醒时的上下文 token 数;缺省表示还没建立基线。 */
38
+ readonly nudgeBaselineTokens?: number
39
+ /** 已发出的主动压缩提醒次数。 */
40
+ readonly nudgeCount?: number
41
+ /** 全 T3 且无法继续压缩时的终止原因;存在时停止自动压缩。 */
42
+ readonly terminal?: string
43
+ }
44
+
45
+ /** 服务端 storage 的最小接口,便于测试替身。 */
46
+ export interface StorageLike {
47
+ get(key: string): Promise<Schema.Json | undefined>
48
+ set(key: string, value: Schema.Json): Promise<void>
49
+ remove(key: string): Promise<void>
50
+ }
51
+
52
+ export const STATE_PREFIX = "compression:state:"
53
+
54
+ /** 连续压缩失败达到这个次数后暂停自动压缩;同一层内还会做摘要重试。 */
55
+ export const MAX_COMPRESSION_ATTEMPTS = 3
56
+
57
+ export const EMPTY_STATE: State = { blocks: [], attempts: 0 }
58
+
59
+ /** 输入:sessionID;输出:storage key。 */
60
+ export function stateKey(sessionID: string): string {
61
+ return STATE_PREFIX + sessionID
62
+ }
63
+
64
+ /** 输入:块列表;输出:下一个可用的 order。 */
65
+ export function nextOrder(blocks: readonly Block[]): number {
66
+ return blocks.reduce((max, block) => Math.max(max, block.order), 0) + 1
67
+ }
68
+
69
+ /** 输入:已落盘块 + 可选 pending;输出:下一个可用的 bN id。 */
70
+ export function nextBlockId(blocks: readonly Block[], pending?: Block): string {
71
+ let max = 0
72
+ const all = pending === undefined ? blocks : [...blocks, pending]
73
+ for (const block of all) {
74
+ const numeric = Number(block.id.startsWith("b") ? block.id.slice(1) : "")
75
+ if (Number.isInteger(numeric) && numeric > max) max = numeric
76
+ }
77
+ return `b${(max + 1).toString()}`
78
+ }
79
+
80
+ function isStringArray(value: unknown): value is readonly string[] {
81
+ return Array.isArray(value) && value.every((item) => typeof item === "string")
82
+ }
83
+
84
+ /** 输入:存储里的摘要;输出:JSON 摘要字段;形状不合法时 undefined。 */
85
+ function parseFields(value: unknown): SummaryFields | undefined {
86
+ if (typeof value !== "object" || value === null) return undefined
87
+ const fields = value as Record<string, unknown>
88
+ if (typeof fields["title"] !== "string" || typeof fields["goal"] !== "string") return undefined
89
+ if (!isStringArray(fields["constraints"])) return undefined
90
+ if (!isStringArray(fields["decisions"])) return undefined
91
+ if (!isStringArray(fields["open_items"])) return undefined
92
+ if (!isStringArray(fields["files"])) return undefined
93
+ if (!isStringArray(fields["facts"])) return undefined
94
+ const progress = fields["progress"]
95
+ if (typeof progress !== "object" || progress === null) return undefined
96
+ const p = progress as Record<string, unknown>
97
+ if (!isStringArray(p["completed"]) || !isStringArray(p["active"]) || !isStringArray(p["blocked"])) {
98
+ return undefined
99
+ }
100
+ return {
101
+ title: fields["title"],
102
+ goal: fields["goal"],
103
+ constraints: fields["constraints"],
104
+ decisions: fields["decisions"],
105
+ progress: { completed: p["completed"], active: p["active"], blocked: p["blocked"] },
106
+ open_items: fields["open_items"],
107
+ files: fields["files"],
108
+ facts: fields["facts"],
109
+ }
110
+ }
111
+
112
+ /** 输入:存储里的摘要;输出:规范摘要;形状不合法时 undefined。 */
113
+ function parseSummary(value: unknown): Summary | undefined {
114
+ if (typeof value !== "object" || value === null) return undefined
115
+ const record = value as Record<string, unknown>
116
+ if (record["format"] === "markdown") {
117
+ if (typeof record["title"] !== "string" || typeof record["text"] !== "string") return undefined
118
+ return { format: "markdown", title: record["title"], text: record["text"] }
119
+ }
120
+ if (record["format"] === "babel") {
121
+ if (typeof record["text"] !== "string") return undefined
122
+ return toBabelSummary(
123
+ record["text"],
124
+ typeof record["title"] === "string" ? record["title"] : undefined,
125
+ typeof record["goal"] === "string" ? record["goal"] : undefined,
126
+ )
127
+ }
128
+ if ("format" in record) return undefined
129
+ return parseFields(value)
130
+ }
131
+
132
+ /** 输入:存储里的块;输出:规范块;形状不合法时 undefined。 */
133
+ function parseBlock(value: unknown): Block | undefined {
134
+ if (typeof value !== "object" || value === null) return undefined
135
+ const block = value as Record<string, unknown>
136
+ if (typeof block["id"] !== "string") return undefined
137
+ const tier = block["tier"]
138
+ if (tier !== 1 && tier !== 2 && tier !== 3) return undefined
139
+ if (typeof block["fromId"] !== "string" || typeof block["toId"] !== "string") return undefined
140
+ if (typeof block["tokensBefore"] !== "number" || typeof block["tokensAfter"] !== "number") return undefined
141
+ if (typeof block["at"] !== "number" || typeof block["order"] !== "number") return undefined
142
+ const sourceBlockIds = block["sourceBlockIds"]
143
+ if (sourceBlockIds !== undefined && !isStringArray(sourceBlockIds)) return undefined
144
+ const summary = parseSummary(block["summary"])
145
+ if (summary === undefined) return undefined
146
+ return {
147
+ id: block["id"],
148
+ tier,
149
+ summary,
150
+ fromId: block["fromId"],
151
+ toId: block["toId"],
152
+ tokensBefore: block["tokensBefore"],
153
+ tokensAfter: block["tokensAfter"],
154
+ at: block["at"],
155
+ order: block["order"],
156
+ ...(sourceBlockIds === undefined ? {} : { sourceBlockIds }),
157
+ }
158
+ }
159
+
160
+ /**
161
+ * 输入:storage 里的原始值。
162
+ * 输出:合法状态;损坏时返回空状态。
163
+ * 预期:不读版本号、不做迁移;坏块直接丢弃。
164
+ */
165
+ export function parseState(value: unknown): State {
166
+ if (typeof value !== "object" || value === null) return EMPTY_STATE
167
+ const record = value as Record<string, unknown>
168
+ const blocks = Array.isArray(record["blocks"])
169
+ ? record["blocks"].map(parseBlock).filter((block): block is Block => block !== undefined)
170
+ : []
171
+ const pending = parseBlock(record["pending"])
172
+ const attempts = typeof record["attempts"] === "number" ? record["attempts"] : 0
173
+ const lastError = typeof record["lastError"] === "string" ? record["lastError"] : undefined
174
+ const nudgeBaselineTokens =
175
+ typeof record["nudgeBaselineTokens"] === "number" ? record["nudgeBaselineTokens"] : undefined
176
+ const nudgeCount = typeof record["nudgeCount"] === "number" ? record["nudgeCount"] : undefined
177
+ const terminal = typeof record["terminal"] === "string" ? record["terminal"] : undefined
178
+ const rawPhase = record["phase"]
179
+ const phase: CompactionPhase | undefined = rawPhase === "t2" || rawPhase === "t3" ? rawPhase : undefined
180
+ return {
181
+ blocks,
182
+ attempts,
183
+ ...(pending === undefined ? {} : { pending }),
184
+ ...(lastError === undefined ? {} : { lastError }),
185
+ ...(nudgeBaselineTokens === undefined ? {} : { nudgeBaselineTokens }),
186
+ ...(nudgeCount === undefined ? {} : { nudgeCount }),
187
+ ...(terminal === undefined ? {} : { terminal }),
188
+ ...(phase === undefined ? {} : { phase }),
189
+ }
190
+ }
191
+
192
+ /** 输入:storage + sessionID;输出:状态。 */
193
+ export async function loadState(storage: StorageLike, sessionID: string): Promise<State> {
194
+ return parseState(await storage.get(stateKey(sessionID)))
195
+ }
196
+
197
+ /** 输入:storage + sessionID + 状态;输出:无;预期:写入 JSON;已删会话直接跳过。 */
198
+ export async function saveState(storage: StorageLike, sessionID: string, state: State): Promise<void> {
199
+ if (isSessionDeleted(sessionID)) return
200
+ await storage.set(stateKey(sessionID), state as unknown as Schema.Json)
201
+ }
202
+
203
+ /** 输入:状态;输出:pending 转正后的新状态;预期:成功时清 lastError/terminal,保留下一层 phase。 */
204
+ export function commitPending(state: State): State {
205
+ if (!state.pending) return state
206
+ return {
207
+ blocks: [...state.blocks, state.pending],
208
+ attempts: 0,
209
+ ...(state.nudgeCount === undefined ? {} : { nudgeCount: state.nudgeCount }),
210
+ ...(state.phase === undefined ? {} : { phase: state.phase }),
211
+ }
212
+ }
213
+
214
+ /** 输入:状态 + 错误;输出:丢弃 pending 后的新状态;预期:失败时清 pending/phase,保留 terminal。 */
215
+ export function dropPending(state: State, error: string): State {
216
+ return {
217
+ blocks: state.blocks,
218
+ attempts: state.attempts + 1,
219
+ lastError: error,
220
+ ...(state.nudgeBaselineTokens === undefined ? {} : { nudgeBaselineTokens: state.nudgeBaselineTokens }),
221
+ ...(state.nudgeCount === undefined ? {} : { nudgeCount: state.nudgeCount }),
222
+ ...(state.terminal === undefined ? {} : { terminal: state.terminal }),
223
+ }
224
+ }
225
+
226
+ /**
227
+ * 输入:失败后的状态 + 错误。
228
+ * 输出:清 pending,但保留 phase 以便下一次请求重试。
229
+ * 预期:attempts 达到上限后清掉 phase,避免无限重试。
230
+ */
231
+ export function retryPending(state: State, error: string): State {
232
+ const attempts = state.attempts + 1
233
+ const keepPhase = state.phase !== undefined && attempts < MAX_COMPRESSION_ATTEMPTS
234
+ return {
235
+ blocks: state.blocks,
236
+ attempts,
237
+ lastError: error,
238
+ ...(state.nudgeBaselineTokens === undefined ? {} : { nudgeBaselineTokens: state.nudgeBaselineTokens }),
239
+ ...(state.nudgeCount === undefined ? {} : { nudgeCount: state.nudgeCount }),
240
+ ...(keepPhase ? { phase: state.phase } : {}),
241
+ ...(state.terminal === undefined ? {} : { terminal: state.terminal }),
242
+ }
243
+ }
244
+
245
+ /** 输入:状态 + 新块;输出:挂上 pending 的新状态。 */
246
+ export function withPending(state: State, block: Block): State {
247
+ return { ...state, pending: block }
248
+ }