compressed-context 0.1.0 → 0.1.1
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/package.json +64 -64
- package/src/compression/active-summary-cache.ts +50 -50
- package/src/compression/adopt.ts +57 -57
- package/src/compression/auto.ts +372 -372
- package/src/compression/babel.ts +85 -85
- package/src/compression/block.ts +25 -25
- package/src/compression/cache-history.ts +75 -75
- package/src/compression/compaction.ts +189 -189
- package/src/compression/decompress-tool.ts +74 -74
- package/src/compression/decompress.ts +292 -292
- package/src/compression/deleted-sessions.ts +24 -24
- package/src/compression/distill.ts +220 -220
- package/src/compression/events.ts +275 -275
- package/src/compression/markdown.ts +53 -53
- package/src/compression/normalize.ts +114 -114
- package/src/compression/render.ts +70 -70
- package/src/compression/schema.ts +84 -84
- package/src/compression/select.ts +123 -123
- package/src/compression/session-message.ts +112 -112
- package/src/compression/settings-rpc.ts +111 -111
- package/src/compression/settings.ts +175 -175
- package/src/compression/state.ts +248 -248
- package/src/compression/summarize.ts +595 -595
- package/src/compression/summary.ts +73 -73
- package/src/compression/tool.ts +142 -142
- package/src/compression/transcript-store.ts +258 -258
- package/src/compression/transcript.ts +63 -63
- package/src/compression/validate.ts +81 -81
- package/src/i18n/en.ts +93 -93
- package/src/i18n/index.ts +28 -28
- package/src/i18n/types.ts +89 -89
- package/src/i18n/zh.ts +93 -93
- package/src/index.ts +108 -108
- package/src/shared/blocks.ts +17 -17
- package/src/shared/cache.ts +199 -199
- package/src/shared/format.ts +23 -23
- package/src/shared/media.ts +184 -184
- package/src/shared/parse.ts +25 -25
- package/src/shared/parts.ts +72 -72
- package/src/shared/rpc.ts +145 -145
- package/src/shared/scan.ts +32 -32
- package/src/shared/settings.ts +471 -471
- package/src/shared/stats.ts +142 -142
- package/src/shared/tokens.ts +259 -259
- package/src/tui/chart/cache-chart.ts +237 -237
- package/src/tui/chart/color.ts +58 -58
- package/src/tui/index.tsx +111 -111
- package/src/tui/panel/cache.tsx +203 -203
- package/src/tui/panel/data.ts +105 -105
- package/src/tui/panel/index.tsx +176 -176
- package/src/tui/panel/summaries.tsx +192 -192
- package/src/tui/settings/dialogs.tsx +215 -215
- package/src/tui/settings/options.ts +238 -238
- package/src/tui/settings.tsx +418 -418
- package/src/tui/types.ts +4 -4
package/src/compression/state.ts
CHANGED
|
@@ -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
|
+
}
|