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/babel.ts
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import type { BabelSummary } from "./schema.ts"
|
|
2
|
-
|
|
3
|
-
/** Babel 首行:`T=标题|G=目标`;允许两侧空格。 */
|
|
4
|
-
const HEADER_RE = /^\s*T\s*=\s*(.*?)\s*\|\s*G\s*=\s*(.*?)\s*$/i
|
|
5
|
-
/** 兼容模型把 T/G 写成两行的情况。 */
|
|
6
|
-
const TITLE_RE = /^\s*T\s*=\s*(.*?)\s*$/i
|
|
7
|
-
const GOAL_RE = /^\s*G\s*=\s*(.*?)\s*$/i
|
|
8
|
-
/** T2/T3 候选行:优先 `c=N`,兼容 `candidate: N`。 */
|
|
9
|
-
const CANDIDATE_RE = /^\s*(?:c|candidate)\s*[:=]\s*(\d+)\s*$/i
|
|
10
|
-
|
|
11
|
-
/** 输入:候选字符串;输出:第一个非空值,全部为空时返回空串。 */
|
|
12
|
-
function firstNonEmpty(...values: readonly (string | undefined)[]): string {
|
|
13
|
-
for (const value of values) {
|
|
14
|
-
const trimmed = value?.trim()
|
|
15
|
-
if (trimmed !== undefined && trimmed.length > 0) return trimmed
|
|
16
|
-
}
|
|
17
|
-
return ""
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** 输入:模型原始文本;输出:去掉代码围栏和首尾空白的文本。 */
|
|
21
|
-
export function cleanBabel(text: string): string {
|
|
22
|
-
return text
|
|
23
|
-
.trim()
|
|
24
|
-
.replace(/^```(?:babel|babeltele|text|md|markdown)?\s*/i, "")
|
|
25
|
-
.replace(/\s*```$/, "")
|
|
26
|
-
.trim()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 输入:文本 + 可选显式 title/goal。
|
|
31
|
-
* 输出:规范 Babel 摘要;缺少标题/目标/正文时返回 undefined。
|
|
32
|
-
* 预期:首行优先解析 `T=...|G=...`,也接受两行 T/G;text 保留规范化首行。
|
|
33
|
-
*/
|
|
34
|
-
export function toBabelSummary(value: unknown, title?: string, goal?: string): BabelSummary | undefined {
|
|
35
|
-
if (typeof value !== "string") return undefined
|
|
36
|
-
const clean = cleanBabel(value)
|
|
37
|
-
if (clean.length === 0) return undefined
|
|
38
|
-
|
|
39
|
-
const lines = clean.split(/\r?\n/)
|
|
40
|
-
const first = lines[0]?.trim() ?? ""
|
|
41
|
-
const header = HEADER_RE.exec(first)
|
|
42
|
-
let parsedTitle = header?.[1]?.trim()
|
|
43
|
-
let parsedGoal = header?.[2]?.trim()
|
|
44
|
-
let bodyLines = lines
|
|
45
|
-
|
|
46
|
-
if (header !== null) {
|
|
47
|
-
bodyLines = lines.slice(1)
|
|
48
|
-
} else {
|
|
49
|
-
const second = lines[1]?.trim() ?? ""
|
|
50
|
-
const titleLine = TITLE_RE.exec(first)
|
|
51
|
-
const goalLine = GOAL_RE.exec(second)
|
|
52
|
-
if (titleLine !== null && goalLine !== null) {
|
|
53
|
-
parsedTitle = titleLine[1]?.trim()
|
|
54
|
-
parsedGoal = goalLine[1]?.trim()
|
|
55
|
-
bodyLines = lines.slice(2)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const resolvedTitle = firstNonEmpty(title, parsedTitle)
|
|
60
|
-
const resolvedGoal = firstNonEmpty(goal, parsedGoal)
|
|
61
|
-
if (resolvedTitle.length === 0 || resolvedGoal.length === 0) return undefined
|
|
62
|
-
|
|
63
|
-
// 有首行时正文从首行之后开始;无首行但显式给了 title/goal 时保留整段正文。
|
|
64
|
-
const body = bodyLines.join("\n").trim()
|
|
65
|
-
if (body.length === 0) return undefined
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
format: "babel",
|
|
69
|
-
title: resolvedTitle,
|
|
70
|
-
goal: resolvedGoal,
|
|
71
|
-
text: `T=${resolvedTitle}|G=${resolvedGoal}\n${body}`,
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** 输入:模型返回的文本;输出:首个非空行是否为 `c=N`,以及去掉该行后的正文。 */
|
|
76
|
-
export function splitBabelCandidate(text: string): { readonly candidate: number; readonly summary: string } | undefined {
|
|
77
|
-
const clean = cleanBabel(text)
|
|
78
|
-
const lines = clean.split(/\r?\n/)
|
|
79
|
-
const first = lines[0]?.trim() ?? ""
|
|
80
|
-
const match = CANDIDATE_RE.exec(first)
|
|
81
|
-
if (match === null) return undefined
|
|
82
|
-
const candidate = Number(match[1])
|
|
83
|
-
if (!Number.isInteger(candidate)) return undefined
|
|
84
|
-
return { candidate, summary: lines.slice(1).join("\n").trim() }
|
|
85
|
-
}
|
|
1
|
+
import type { BabelSummary } from "./schema.ts"
|
|
2
|
+
|
|
3
|
+
/** Babel 首行:`T=标题|G=目标`;允许两侧空格。 */
|
|
4
|
+
const HEADER_RE = /^\s*T\s*=\s*(.*?)\s*\|\s*G\s*=\s*(.*?)\s*$/i
|
|
5
|
+
/** 兼容模型把 T/G 写成两行的情况。 */
|
|
6
|
+
const TITLE_RE = /^\s*T\s*=\s*(.*?)\s*$/i
|
|
7
|
+
const GOAL_RE = /^\s*G\s*=\s*(.*?)\s*$/i
|
|
8
|
+
/** T2/T3 候选行:优先 `c=N`,兼容 `candidate: N`。 */
|
|
9
|
+
const CANDIDATE_RE = /^\s*(?:c|candidate)\s*[:=]\s*(\d+)\s*$/i
|
|
10
|
+
|
|
11
|
+
/** 输入:候选字符串;输出:第一个非空值,全部为空时返回空串。 */
|
|
12
|
+
function firstNonEmpty(...values: readonly (string | undefined)[]): string {
|
|
13
|
+
for (const value of values) {
|
|
14
|
+
const trimmed = value?.trim()
|
|
15
|
+
if (trimmed !== undefined && trimmed.length > 0) return trimmed
|
|
16
|
+
}
|
|
17
|
+
return ""
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 输入:模型原始文本;输出:去掉代码围栏和首尾空白的文本。 */
|
|
21
|
+
export function cleanBabel(text: string): string {
|
|
22
|
+
return text
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/^```(?:babel|babeltele|text|md|markdown)?\s*/i, "")
|
|
25
|
+
.replace(/\s*```$/, "")
|
|
26
|
+
.trim()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 输入:文本 + 可选显式 title/goal。
|
|
31
|
+
* 输出:规范 Babel 摘要;缺少标题/目标/正文时返回 undefined。
|
|
32
|
+
* 预期:首行优先解析 `T=...|G=...`,也接受两行 T/G;text 保留规范化首行。
|
|
33
|
+
*/
|
|
34
|
+
export function toBabelSummary(value: unknown, title?: string, goal?: string): BabelSummary | undefined {
|
|
35
|
+
if (typeof value !== "string") return undefined
|
|
36
|
+
const clean = cleanBabel(value)
|
|
37
|
+
if (clean.length === 0) return undefined
|
|
38
|
+
|
|
39
|
+
const lines = clean.split(/\r?\n/)
|
|
40
|
+
const first = lines[0]?.trim() ?? ""
|
|
41
|
+
const header = HEADER_RE.exec(first)
|
|
42
|
+
let parsedTitle = header?.[1]?.trim()
|
|
43
|
+
let parsedGoal = header?.[2]?.trim()
|
|
44
|
+
let bodyLines = lines
|
|
45
|
+
|
|
46
|
+
if (header !== null) {
|
|
47
|
+
bodyLines = lines.slice(1)
|
|
48
|
+
} else {
|
|
49
|
+
const second = lines[1]?.trim() ?? ""
|
|
50
|
+
const titleLine = TITLE_RE.exec(first)
|
|
51
|
+
const goalLine = GOAL_RE.exec(second)
|
|
52
|
+
if (titleLine !== null && goalLine !== null) {
|
|
53
|
+
parsedTitle = titleLine[1]?.trim()
|
|
54
|
+
parsedGoal = goalLine[1]?.trim()
|
|
55
|
+
bodyLines = lines.slice(2)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resolvedTitle = firstNonEmpty(title, parsedTitle)
|
|
60
|
+
const resolvedGoal = firstNonEmpty(goal, parsedGoal)
|
|
61
|
+
if (resolvedTitle.length === 0 || resolvedGoal.length === 0) return undefined
|
|
62
|
+
|
|
63
|
+
// 有首行时正文从首行之后开始;无首行但显式给了 title/goal 时保留整段正文。
|
|
64
|
+
const body = bodyLines.join("\n").trim()
|
|
65
|
+
if (body.length === 0) return undefined
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
format: "babel",
|
|
69
|
+
title: resolvedTitle,
|
|
70
|
+
goal: resolvedGoal,
|
|
71
|
+
text: `T=${resolvedTitle}|G=${resolvedGoal}\n${body}`,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 输入:模型返回的文本;输出:首个非空行是否为 `c=N`,以及去掉该行后的正文。 */
|
|
76
|
+
export function splitBabelCandidate(text: string): { readonly candidate: number; readonly summary: string } | undefined {
|
|
77
|
+
const clean = cleanBabel(text)
|
|
78
|
+
const lines = clean.split(/\r?\n/)
|
|
79
|
+
const first = lines[0]?.trim() ?? ""
|
|
80
|
+
const match = CANDIDATE_RE.exec(first)
|
|
81
|
+
if (match === null) return undefined
|
|
82
|
+
const candidate = Number(match[1])
|
|
83
|
+
if (!Number.isInteger(candidate)) return undefined
|
|
84
|
+
return { candidate, summary: lines.slice(1).join("\n").trim() }
|
|
85
|
+
}
|
package/src/compression/block.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import type { Summary } from "./schema.ts"
|
|
2
|
-
import { summaryChars } from "./summary.ts"
|
|
3
|
-
import { nextBlockId, nextOrder, type Block } from "./state.ts"
|
|
4
|
-
|
|
5
|
-
/** 输入:已有块 + 摘要 + 覆盖范围 + 原文字符数;输出:新的 T1 块。 */
|
|
6
|
-
export function createT1Block(input: {
|
|
7
|
-
readonly blocks: readonly Block[]
|
|
8
|
-
readonly summary: Summary
|
|
9
|
-
readonly fromId: string
|
|
10
|
-
readonly toId: string
|
|
11
|
-
readonly sourceChars: number
|
|
12
|
-
readonly at?: number
|
|
13
|
-
}): Block {
|
|
14
|
-
return {
|
|
15
|
-
id: nextBlockId(input.blocks),
|
|
16
|
-
tier: 1,
|
|
17
|
-
summary: input.summary,
|
|
18
|
-
fromId: input.fromId,
|
|
19
|
-
toId: input.toId,
|
|
20
|
-
tokensBefore: Math.ceil(input.sourceChars / 4),
|
|
21
|
-
tokensAfter: Math.ceil(summaryChars(input.summary) / 4),
|
|
22
|
-
at: input.at ?? Date.now(),
|
|
23
|
-
order: nextOrder(input.blocks),
|
|
24
|
-
}
|
|
25
|
-
}
|
|
1
|
+
import type { Summary } from "./schema.ts"
|
|
2
|
+
import { summaryChars } from "./summary.ts"
|
|
3
|
+
import { nextBlockId, nextOrder, type Block } from "./state.ts"
|
|
4
|
+
|
|
5
|
+
/** 输入:已有块 + 摘要 + 覆盖范围 + 原文字符数;输出:新的 T1 块。 */
|
|
6
|
+
export function createT1Block(input: {
|
|
7
|
+
readonly blocks: readonly Block[]
|
|
8
|
+
readonly summary: Summary
|
|
9
|
+
readonly fromId: string
|
|
10
|
+
readonly toId: string
|
|
11
|
+
readonly sourceChars: number
|
|
12
|
+
readonly at?: number
|
|
13
|
+
}): Block {
|
|
14
|
+
return {
|
|
15
|
+
id: nextBlockId(input.blocks),
|
|
16
|
+
tier: 1,
|
|
17
|
+
summary: input.summary,
|
|
18
|
+
fromId: input.fromId,
|
|
19
|
+
toId: input.toId,
|
|
20
|
+
tokensBefore: Math.ceil(input.sourceChars / 4),
|
|
21
|
+
tokensAfter: Math.ceil(summaryChars(input.summary) / 4),
|
|
22
|
+
at: input.at ?? Date.now(),
|
|
23
|
+
order: nextOrder(input.blocks),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode/plugin"
|
|
2
|
-
import type { Schema } from "effect"
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
appendCacheSampleFromStep,
|
|
6
|
-
appendCacheSamples,
|
|
7
|
-
cacheHistoryEquals,
|
|
8
|
-
cacheHistoryKey,
|
|
9
|
-
parseCacheHistory,
|
|
10
|
-
type CacheHistory,
|
|
11
|
-
type CacheSample,
|
|
12
|
-
} from "../shared/cache.ts"
|
|
13
|
-
import type { StorageLike } from "./state.ts"
|
|
14
|
-
|
|
15
|
-
/** 输入:storage + sessionID;输出:该会话的历史;缺失或损坏时为空历史。 */
|
|
16
|
-
export async function loadCacheHistory(storage: StorageLike, sessionID: string): Promise<CacheHistory> {
|
|
17
|
-
return parseCacheHistory(await storage.get(cacheHistoryKey(sessionID)))
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** 输入:storage + sessionID + 样本;输出:无;预期:写入 JSON,最多 10 条。 */
|
|
21
|
-
export async function saveCacheHistory(
|
|
22
|
-
storage: StorageLike,
|
|
23
|
-
sessionID: string,
|
|
24
|
-
samples: readonly CacheSample[],
|
|
25
|
-
): Promise<void> {
|
|
26
|
-
await storage.set(cacheHistoryKey(sessionID), { v: 1, samples } as unknown as Schema.Json)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 输入:插件上下文 + sessionID + 单 step 的 assistant usage。
|
|
31
|
-
* 输出:无。
|
|
32
|
-
* 预期:tool loop 的后续 step 不触发 context/usage.updated 的完整读取,
|
|
33
|
-
* 用 `session.step.ended` 事件里的 tokens 直接落一条缓存样本。
|
|
34
|
-
*/
|
|
35
|
-
export async function recordCacheHistoryStep(
|
|
36
|
-
ctx: Plugin.Context,
|
|
37
|
-
sessionID: string,
|
|
38
|
-
input: {
|
|
39
|
-
readonly messageID: string
|
|
40
|
-
readonly tokens: {
|
|
41
|
-
readonly input: number
|
|
42
|
-
readonly output: number
|
|
43
|
-
readonly reasoning: number
|
|
44
|
-
readonly cache: { readonly read: number; readonly write: number }
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
): Promise<void> {
|
|
48
|
-
try {
|
|
49
|
-
const history = await loadCacheHistory(ctx.storage, sessionID)
|
|
50
|
-
const samples = appendCacheSampleFromStep(history.samples, input.messageID, input.tokens)
|
|
51
|
-
if (!cacheHistoryEquals(history.samples, samples)) {
|
|
52
|
-
await saveCacheHistory(ctx.storage, sessionID, samples)
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
// best effort:历史缺失不影响主流程
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 输入:插件上下文 + sessionID。
|
|
61
|
-
* 输出:无;预期:把当前上下文里未记录过的 assistant usage 追加进该会话的历史。
|
|
62
|
-
* 失败只影响历史,不影响压缩与主流程。
|
|
63
|
-
*/
|
|
64
|
-
export async function recordCacheHistory(ctx: Plugin.Context, sessionID: string): Promise<void> {
|
|
65
|
-
try {
|
|
66
|
-
const messages = await ctx.session.context({ sessionID })
|
|
67
|
-
const history = await loadCacheHistory(ctx.storage, sessionID)
|
|
68
|
-
const samples = appendCacheSamples(history.samples, messages)
|
|
69
|
-
if (!cacheHistoryEquals(history.samples, samples)) {
|
|
70
|
-
await saveCacheHistory(ctx.storage, sessionID, samples)
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
// best effort:历史缺失不影响主界面
|
|
74
|
-
}
|
|
75
|
-
}
|
|
1
|
+
import type { Plugin } from "@opencode/plugin"
|
|
2
|
+
import type { Schema } from "effect"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
appendCacheSampleFromStep,
|
|
6
|
+
appendCacheSamples,
|
|
7
|
+
cacheHistoryEquals,
|
|
8
|
+
cacheHistoryKey,
|
|
9
|
+
parseCacheHistory,
|
|
10
|
+
type CacheHistory,
|
|
11
|
+
type CacheSample,
|
|
12
|
+
} from "../shared/cache.ts"
|
|
13
|
+
import type { StorageLike } from "./state.ts"
|
|
14
|
+
|
|
15
|
+
/** 输入:storage + sessionID;输出:该会话的历史;缺失或损坏时为空历史。 */
|
|
16
|
+
export async function loadCacheHistory(storage: StorageLike, sessionID: string): Promise<CacheHistory> {
|
|
17
|
+
return parseCacheHistory(await storage.get(cacheHistoryKey(sessionID)))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 输入:storage + sessionID + 样本;输出:无;预期:写入 JSON,最多 10 条。 */
|
|
21
|
+
export async function saveCacheHistory(
|
|
22
|
+
storage: StorageLike,
|
|
23
|
+
sessionID: string,
|
|
24
|
+
samples: readonly CacheSample[],
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
await storage.set(cacheHistoryKey(sessionID), { v: 1, samples } as unknown as Schema.Json)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 输入:插件上下文 + sessionID + 单 step 的 assistant usage。
|
|
31
|
+
* 输出:无。
|
|
32
|
+
* 预期:tool loop 的后续 step 不触发 context/usage.updated 的完整读取,
|
|
33
|
+
* 用 `session.step.ended` 事件里的 tokens 直接落一条缓存样本。
|
|
34
|
+
*/
|
|
35
|
+
export async function recordCacheHistoryStep(
|
|
36
|
+
ctx: Plugin.Context,
|
|
37
|
+
sessionID: string,
|
|
38
|
+
input: {
|
|
39
|
+
readonly messageID: string
|
|
40
|
+
readonly tokens: {
|
|
41
|
+
readonly input: number
|
|
42
|
+
readonly output: number
|
|
43
|
+
readonly reasoning: number
|
|
44
|
+
readonly cache: { readonly read: number; readonly write: number }
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
const history = await loadCacheHistory(ctx.storage, sessionID)
|
|
50
|
+
const samples = appendCacheSampleFromStep(history.samples, input.messageID, input.tokens)
|
|
51
|
+
if (!cacheHistoryEquals(history.samples, samples)) {
|
|
52
|
+
await saveCacheHistory(ctx.storage, sessionID, samples)
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// best effort:历史缺失不影响主流程
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 输入:插件上下文 + sessionID。
|
|
61
|
+
* 输出:无;预期:把当前上下文里未记录过的 assistant usage 追加进该会话的历史。
|
|
62
|
+
* 失败只影响历史,不影响压缩与主流程。
|
|
63
|
+
*/
|
|
64
|
+
export async function recordCacheHistory(ctx: Plugin.Context, sessionID: string): Promise<void> {
|
|
65
|
+
try {
|
|
66
|
+
const messages = await ctx.session.context({ sessionID })
|
|
67
|
+
const history = await loadCacheHistory(ctx.storage, sessionID)
|
|
68
|
+
const samples = appendCacheSamples(history.samples, messages)
|
|
69
|
+
if (!cacheHistoryEquals(history.samples, samples)) {
|
|
70
|
+
await saveCacheHistory(ctx.storage, sessionID, samples)
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// best effort:历史缺失不影响主界面
|
|
74
|
+
}
|
|
75
|
+
}
|