dsh-prime-memory 0.11.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 (151) hide show
  1. package/CHANGELOG.en.md +28 -0
  2. package/CHANGELOG.ja.md +30 -0
  3. package/CHANGELOG.ko.md +30 -0
  4. package/CHANGELOG.md +1220 -0
  5. package/ENGINEERING-NOTES.md +452 -0
  6. package/INSTALL.en.md +92 -0
  7. package/INSTALL.ja.md +92 -0
  8. package/INSTALL.ko.md +92 -0
  9. package/INSTALL.md +92 -0
  10. package/LICENSE +21 -0
  11. package/README.en.md +458 -0
  12. package/README.ja.md +306 -0
  13. package/README.ko.md +306 -0
  14. package/README.md +425 -0
  15. package/assets/changelog/0.8.10/01-write-only-pill.png +0 -0
  16. package/assets/changelog/0.8.9/01-panel.png +0 -0
  17. package/assets/changelog/0.8.9/02-halo.png +0 -0
  18. package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
  19. package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
  20. package/assets/img/EmbeddingSource.png +0 -0
  21. package/assets/img/Hero.png +0 -0
  22. package/assets/img/Layers.png +0 -0
  23. package/assets/img/MemoryTools.png +0 -0
  24. package/assets/img/Modes.png +0 -0
  25. package/assets/img/ToolTrajectory.png +0 -0
  26. package/assets/img/ui-dark.jpg +0 -0
  27. package/assets/img/ui-light.jpg +0 -0
  28. package/assets/readme/bench-dialog.svg +70 -0
  29. package/assets/readme/bench-workflow.svg +79 -0
  30. package/assets/readme/flow.svg +189 -0
  31. package/assets/readme/storage.svg +115 -0
  32. package/cordis.patch.yml +16 -0
  33. package/dist/bench-control.d.ts +34 -0
  34. package/dist/bench-control.js +16 -0
  35. package/dist/client.js +4293 -0
  36. package/dist/config.d.ts +683 -0
  37. package/dist/config.js +129 -0
  38. package/dist/contract.d.ts +820 -0
  39. package/dist/contract.js +1 -0
  40. package/dist/embedding-worker.cjs +176 -0
  41. package/dist/graph/apply.d.ts +37 -0
  42. package/dist/graph/apply.js +270 -0
  43. package/dist/graph/constraints.d.ts +47 -0
  44. package/dist/graph/constraints.js +38 -0
  45. package/dist/graph/search.d.ts +16 -0
  46. package/dist/graph/search.js +115 -0
  47. package/dist/graph/types.d.ts +142 -0
  48. package/dist/graph/types.js +14 -0
  49. package/dist/hooks/capture.d.ts +32 -0
  50. package/dist/hooks/capture.js +194 -0
  51. package/dist/hooks/recall.d.ts +63 -0
  52. package/dist/hooks/recall.js +429 -0
  53. package/dist/index.d.ts +534 -0
  54. package/dist/index.js +344 -0
  55. package/dist/llm-usage.d.ts +26 -0
  56. package/dist/llm-usage.js +37 -0
  57. package/dist/llm.d.ts +153 -0
  58. package/dist/llm.js +530 -0
  59. package/dist/pipeline/graph.d.ts +35 -0
  60. package/dist/pipeline/graph.js +104 -0
  61. package/dist/pipeline/l1.d.ts +19 -0
  62. package/dist/pipeline/l1.js +271 -0
  63. package/dist/pipeline/l2.d.ts +13 -0
  64. package/dist/pipeline/l2.js +83 -0
  65. package/dist/pipeline/l3.d.ts +15 -0
  66. package/dist/pipeline/l3.js +78 -0
  67. package/dist/pipeline/rebuild.d.ts +61 -0
  68. package/dist/pipeline/rebuild.js +307 -0
  69. package/dist/pipeline/ruminate.d.ts +89 -0
  70. package/dist/pipeline/ruminate.js +298 -0
  71. package/dist/pipeline/runner.d.ts +167 -0
  72. package/dist/pipeline/runner.js +638 -0
  73. package/dist/pipeline/trigger.d.ts +40 -0
  74. package/dist/pipeline/trigger.js +75 -0
  75. package/dist/prompts/graph-projection.d.ts +70 -0
  76. package/dist/prompts/graph-projection.js +167 -0
  77. package/dist/prompts/l1-dedup.d.ts +22 -0
  78. package/dist/prompts/l1-dedup.js +251 -0
  79. package/dist/prompts/l1-extraction.d.ts +22 -0
  80. package/dist/prompts/l1-extraction.js +457 -0
  81. package/dist/prompts/persona.d.ts +23 -0
  82. package/dist/prompts/persona.js +240 -0
  83. package/dist/prompts/scene.d.ts +32 -0
  84. package/dist/prompts/scene.js +414 -0
  85. package/dist/runtime-package-lock.json +982 -0
  86. package/dist/settings.d.ts +50 -0
  87. package/dist/settings.js +355 -0
  88. package/dist/stats.d.ts +109 -0
  89. package/dist/stats.js +929 -0
  90. package/dist/store/bm25.d.ts +19 -0
  91. package/dist/store/bm25.js +63 -0
  92. package/dist/store/cost-ledger.d.ts +75 -0
  93. package/dist/store/cost-ledger.js +171 -0
  94. package/dist/store/download-queue.d.ts +79 -0
  95. package/dist/store/download-queue.js +424 -0
  96. package/dist/store/embedding-source.d.ts +118 -0
  97. package/dist/store/embedding-source.js +443 -0
  98. package/dist/store/embedding.d.ts +90 -0
  99. package/dist/store/embedding.js +206 -0
  100. package/dist/store/graph-store.d.ts +94 -0
  101. package/dist/store/graph-store.js +641 -0
  102. package/dist/store/l0.d.ts +40 -0
  103. package/dist/store/l0.js +197 -0
  104. package/dist/store/l1.d.ts +93 -0
  105. package/dist/store/l1.js +297 -0
  106. package/dist/store/local-embedding.d.ts +89 -0
  107. package/dist/store/local-embedding.js +227 -0
  108. package/dist/store/model-catalog.d.ts +48 -0
  109. package/dist/store/model-catalog.js +81 -0
  110. package/dist/store/occupancy.d.ts +30 -0
  111. package/dist/store/occupancy.js +134 -0
  112. package/dist/store/pending.d.ts +36 -0
  113. package/dist/store/pending.js +103 -0
  114. package/dist/store/persona.d.ts +15 -0
  115. package/dist/store/persona.js +60 -0
  116. package/dist/store/recall-dedupe.d.ts +26 -0
  117. package/dist/store/recall-dedupe.js +138 -0
  118. package/dist/store/runtime-installer.d.ts +59 -0
  119. package/dist/store/runtime-installer.js +243 -0
  120. package/dist/store/scenes.d.ts +24 -0
  121. package/dist/store/scenes.js +160 -0
  122. package/dist/store/search-utils.d.ts +38 -0
  123. package/dist/store/search-utils.js +100 -0
  124. package/dist/store/session-modes.d.ts +35 -0
  125. package/dist/store/session-modes.js +144 -0
  126. package/dist/store/sqlite.d.ts +246 -0
  127. package/dist/store/sqlite.js +1491 -0
  128. package/dist/store/state.d.ts +41 -0
  129. package/dist/store/state.js +72 -0
  130. package/dist/token-cost.d.ts +23 -0
  131. package/dist/token-cost.js +185 -0
  132. package/dist/tools/index.d.ts +34 -0
  133. package/dist/tools/index.js +758 -0
  134. package/dist/types.d.ts +139 -0
  135. package/dist/types.js +38 -0
  136. package/dist/util/context-occupancy.d.ts +68 -0
  137. package/dist/util/context-occupancy.js +92 -0
  138. package/dist/util/filelog.d.ts +6 -0
  139. package/dist/util/filelog.js +108 -0
  140. package/dist/util/io.d.ts +18 -0
  141. package/dist/util/io.js +97 -0
  142. package/dist/util/recall-budget.d.ts +31 -0
  143. package/dist/util/recall-budget.js +84 -0
  144. package/dist/util/sanitize.d.ts +11 -0
  145. package/dist/util/sanitize.js +67 -0
  146. package/dist/util/text.d.ts +16 -0
  147. package/dist/util/text.js +61 -0
  148. package/dist/util/tokenizer.d.ts +9 -0
  149. package/dist/util/tokenizer.js +50 -0
  150. package/dsh.plugin.json +22 -0
  151. package/package.json +118 -0
@@ -0,0 +1,139 @@
1
+ /**
2
+ * 领域类型与词汇表(净室重写)。
3
+ *
4
+ * 覆盖:记忆族/档位、Hall 目录、L0/L1 记录形状、抽取产出与族判定三级兜底、
5
+ * L2 场景摘要与 L1 检索命中。字段名与取值是磁盘/管线两侧的既定契约,不可更名。
6
+ */
7
+ /** 蒸馏 Prompt 家族:chat = 个人记忆(persona/episodic/instruction + 用户画像),work = 工作记忆(work_fact/work_task/work_method/work_artifact + Team Operating Doctrine)。 */
8
+ export type MemoryFamily = 'chat' | 'work';
9
+ /** 会话记忆档位:auto = 双族自动判定 | chat/work = 单族 | off = 本会话对记忆系统隐身。 */
10
+ export type MemoryMode = 'auto' | 'chat' | 'work' | 'off';
11
+ /** 蒸馏可用的档位(off 在捕获侧被拦截,永远到不了管线)。 */
12
+ export type ExtractMode = 'auto' | 'chat' | 'work';
13
+ /**
14
+ * Hall(粗分类属性通道,与 family/type 正交):给 L1 记忆加一个跨族的可检索标签。
15
+ * 主线 3 个默认启用;finance/journey 为实验性(默认不进 hall.enabled,显式加入才参与
16
+ * 自动打标)。细粒度归属由 prompt 语义判断,拿不准时省略 hall(不进 General 兜底,避免噪声)。
17
+ */
18
+ export interface HallDef {
19
+ id: string;
20
+ label: string;
21
+ /** 实验性(默认不进 hall.enabled,需用户显式开启才参与自动打标/过滤)。 */
22
+ experimental?: boolean;
23
+ }
24
+ export declare const HALL_CATALOG: HallDef[];
25
+ /** 默认启用的 Hall id(主线 3;实验性条目要用户写进 config hall.enabled 才生效)。 */
26
+ export declare const HALL_DEFAULT_ENABLED: string[];
27
+ export type HallId = (typeof HALL_CATALOG)[number]['id'];
28
+ export declare function hallLabel(id: string): string;
29
+ /** 记录族标签推断:work_* 前缀 → work,其余(含 auto 档兜底)→ chat。 */
30
+ export declare function familyForType(type: string): MemoryFamily;
31
+ /**
32
+ * 记忆的持续性(时间轴第三维,与 createdAt/updatedAt 正交):
33
+ * - 'p' point 时点事件(某事在某刻发生)——历史事实,永不被"取代"
34
+ * - 's' span 已结束的持续区间——validTo 是关键信息,不可被后来者改写
35
+ * - 'o' open 仍在持续——被矛盾事实取代时应闭合 validTo 并标 rw
36
+ * - 't' timeless 无时间性(规则/偏好/恒真事实)——不随时间失效
37
+ *
38
+ * 缺省(undefined)= 未判定:不参与取代/闭合判定,只当普通事实。
39
+ */
40
+ export type Persistence = 'p' | 's' | 'o' | 't';
41
+ export declare const PERSISTENCE_VALUES: readonly Persistence[];
42
+ /** 持续性取值归一:只认 p/s/o/t,其余(缺省/非法)返回 undefined。 */
43
+ export declare function normPersistence(raw: unknown): Persistence | undefined;
44
+ /** 日志接口(适配 ctx.logger)。 */
45
+ export interface MemoryLogger {
46
+ debug?(msg: string): void;
47
+ info(msg: string): void;
48
+ warn(msg: string): void;
49
+ error(msg: string): void;
50
+ }
51
+ /** L0 会话消息(管线内的运行时形态)。 */
52
+ export interface ConversationMessage {
53
+ /** 唯一消息 ID(L1 prompt 的 source_message_ids 追踪用)。 */
54
+ id: string;
55
+ role: 'user' | 'assistant';
56
+ content: string;
57
+ /** epoch ms */
58
+ timestamp: number;
59
+ }
60
+ /** L0 JSONL 记录(一条消息一行,磁盘事实源形状)。 */
61
+ export interface L0MessageRecord {
62
+ sessionId: string;
63
+ recordedAt: string;
64
+ id: string;
65
+ role: 'user' | 'assistant';
66
+ content: string;
67
+ timestamp: number;
68
+ }
69
+ /** L1 抽取产出(LLM 返回的记忆条目,尚未分配 record id)。 */
70
+ export interface ExtractedMemory {
71
+ content: string;
72
+ type: string;
73
+ priority: number;
74
+ source_message_ids: string[];
75
+ metadata: Record<string, unknown>;
76
+ /** 所属情境名(L1 抽取的情境切分结果)。 */
77
+ scene_name: string;
78
+ /** auto 档抽取输出的显式族判定(chat|work;纯档 Prompt 无此字段)。
79
+ * 语境归族、形状不归族——避免"个人计划性事实被 work_* 形状吸走"的族错标。 */
80
+ family?: string;
81
+ }
82
+ /** 抽取输出 family 字段归一:只认 chat|work,其余(缺省/非法值)交由调用方回落。 */
83
+ export declare function normExtractedFamily(raw: unknown): MemoryFamily | undefined;
84
+ /** 记录族三级兜底链:会话档位强制(纯档)→ 抽取显式判定(auto)→ type 前缀推导(旧输出兜底)。 */
85
+ export declare function resolveRecordFamily(forced: MemoryFamily | undefined, extracted: unknown, type: string): MemoryFamily;
86
+ /** L1 持久化记录(磁盘与 DB 的权威形状;version/source_message_ids/metadata 由写入侧补默认)。 */
87
+ export interface MemoryRecord {
88
+ id: string;
89
+ content: string;
90
+ type: string;
91
+ priority: number;
92
+ scene_name: string;
93
+ /** 合并/更新时保留的时间戳并集。 */
94
+ timestamps: number[];
95
+ createdAt: number;
96
+ updatedAt: number;
97
+ /** 每条 update/merge 合并 +1。 */
98
+ version?: number;
99
+ /** 来源消息 id(JSONL 事实源保留;检索库不存该列)。 */
100
+ source_message_ids?: string[];
101
+ /** 类型附加信息(episodic 的活动起止时间等)。 */
102
+ metadata?: Record<string, unknown>;
103
+ /** 来源会话(缺省 default;跨会话记忆共享)。 */
104
+ sessionId?: string;
105
+ /** 所属族(写入缺省由 familyForType(type) 回填;召回/浏览/去重候选按族过滤的唯一依据)。 */
106
+ family?: MemoryFamily;
107
+ /**
108
+ * 有效期起(epoch ms):该事实在**真实世界**开始成立的时间。
109
+ * 与 createdAt(入库时间)是两条不同的轴——"2026-03 在 A 项目"这条事实,
110
+ * createdAt 是它进库的时刻,validFrom 才是 2026-03。缺省 = 未知。
111
+ */
112
+ validFrom?: number;
113
+ /**
114
+ * 有效期止(epoch ms):该事实停止成立的时间。
115
+ * 为空(undefined)表示**仍在持续**(persistence='o')或**无时间性**(persistence='t'),
116
+ * 二者由 persistence 区分——这正是不能用"空值"当"已结束"的原因。
117
+ */
118
+ validTo?: number;
119
+ /** 持续性(见 Persistence);缺省 = 未判定。 */
120
+ persistence?: Persistence;
121
+ }
122
+ /** L2 场景块摘要(META 解析结果)。 */
123
+ export interface SceneSummary {
124
+ path: string;
125
+ created: string;
126
+ updated: string;
127
+ summary: string;
128
+ heat: number;
129
+ }
130
+ /** L1 检索命中。 */
131
+ export interface L1Hit {
132
+ id: string;
133
+ content: string;
134
+ type: string;
135
+ scene_name: string;
136
+ score: number;
137
+ priority?: number;
138
+ family?: MemoryFamily;
139
+ }
package/dist/types.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 领域类型与词汇表(净室重写)。
3
+ *
4
+ * 覆盖:记忆族/档位、Hall 目录、L0/L1 记录形状、抽取产出与族判定三级兜底、
5
+ * L2 场景摘要与 L1 检索命中。字段名与取值是磁盘/管线两侧的既定契约,不可更名。
6
+ */
7
+ export const HALL_CATALOG = [
8
+ { id: 'work', label: '工作' },
9
+ { id: 'relationships', label: '人际关系' },
10
+ { id: 'general', label: '通用' },
11
+ { id: 'finance', label: '财务', experimental: true },
12
+ { id: 'journey', label: '旅程', experimental: true },
13
+ ];
14
+ /** 默认启用的 Hall id(主线 3;实验性条目要用户写进 config hall.enabled 才生效)。 */
15
+ export const HALL_DEFAULT_ENABLED = ['work', 'relationships', 'general'];
16
+ export function hallLabel(id) {
17
+ const h = HALL_CATALOG.find((x) => x.id === id);
18
+ return h ? h.label : id;
19
+ }
20
+ /** 记录族标签推断:work_* 前缀 → work,其余(含 auto 档兜底)→ chat。 */
21
+ export function familyForType(type) {
22
+ return type.startsWith('work') ? 'work' : 'chat';
23
+ }
24
+ export const PERSISTENCE_VALUES = ['p', 's', 'o', 't'];
25
+ /** 持续性取值归一:只认 p/s/o/t,其余(缺省/非法)返回 undefined。 */
26
+ export function normPersistence(raw) {
27
+ return typeof raw === 'string' && PERSISTENCE_VALUES.includes(raw)
28
+ ? raw
29
+ : undefined;
30
+ }
31
+ /** 抽取输出 family 字段归一:只认 chat|work,其余(缺省/非法值)交由调用方回落。 */
32
+ export function normExtractedFamily(raw) {
33
+ return raw === 'chat' || raw === 'work' ? raw : undefined;
34
+ }
35
+ /** 记录族三级兜底链:会话档位强制(纯档)→ 抽取显式判定(auto)→ type 前缀推导(旧输出兜底)。 */
36
+ export function resolveRecordFamily(forced, extracted, type) {
37
+ return forced ?? normExtractedFamily(extracted) ?? familyForType(type);
38
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 上下文占用账本:插件注入内容在当前会话上下文窗口中的启发式 token 存量。
3
+ *
4
+ * 本模块是全部占用数字的唯一算术来源(host 记账与 client 渲染共用,esbuild
5
+ * 将其内联进 client bundle),任何一侧不得另写算法。口径与官方
6
+ * @deepseek-ai/dsh-token-meter/estimate 的固定密度启发式同式:
7
+ *
8
+ * - text 块 = ceil(chars / 4) + 4(块开销);
9
+ * - 整条 user 消息(召回注入体的形态)再叠加 role overhead 4;
10
+ * - 系统提示里的稳定区子片计入时不加任何 overhead——那 +8 是官方对整段
11
+ * 消息/整体系统提示的结构成本,切片重复计会与官方总数对不上;
12
+ * - 字符数一律 JS `.length`(UTF-16 单元),与官方产出严格可比。
13
+ */
14
+ /** 固定文本密度:每 4 字符记 1 token(官方 CHARS_PER_TOKEN 同值)。 */
15
+ export declare const CHARS_PER_TOKEN = 4;
16
+ /** 召回注入体(单 text 块 user message)的启发式 token 数。 */
17
+ export declare function estimateInjectedMessageTokens(chars: number): number;
18
+ /** 稳定区子片的启发式 token 数(不加结构开销,见模块头注释)。 */
19
+ export declare function estimateStableSectionTokens(chars: number): number;
20
+ /**
21
+ * 单会话占用账本(权威账本只在 host 侧;client 仅消费展示)。
22
+ * stockTokens = recallTokens + profileTokens 恒等式由三个迁移函数共同维护。
23
+ */
24
+ export interface OccupancyLedger {
25
+ /** 当前估算仍在上下文窗口内的记忆存量(token)。 */
26
+ stockTokens: number;
27
+ /** 累计召回注入存留份额。 */
28
+ recallTokens: number;
29
+ /** 系统提示稳定区当前份额(OFF 使 text() 返回空时即时清零)。 */
30
+ profileTokens: number;
31
+ /** 最近一轮召回注入的增量(悬浮卡/面板"最近一次"语义)。 */
32
+ lastInjectTokens: number;
33
+ updatedAt: number;
34
+ }
35
+ export declare function emptyOccupancyLedger(now?: number): OccupancyLedger;
36
+ /** 召回注入成功后的入账(失败跳过的轮次不调用——账目零扰动)。 */
37
+ export declare function recordRecallInjection(l: OccupancyLedger, chars: number, now?: number): void;
38
+ /**
39
+ * 稳定区份额随每次实际组进的文本同步(含切回非 OFF 的重新计入):
40
+ * 增量记账保证重复调用不双算,清零后重设即可净额回补。
41
+ */
42
+ export declare function recordProfileShare(l: OccupancyLedger, chars: number, now?: number): void;
43
+ /** OFF 边界:稳定区随下一次请求组装物理离场,账目同边界清零(宁低勿高)。 */
44
+ export declare function clearProfileShare(l: OccupancyLedger, now?: number): void;
45
+ /**
46
+ * compaction 复位:采用"清零重新累积"的低估近似——被压缩掉的注入以整数轮粒度
47
+ * 退出账本,幸存的摘要行不重复认领(已知限制,方向宁低勿高)。
48
+ */
49
+ export declare function resetForCompaction(l: OccupancyLedger, now?: number): void;
50
+ /**
51
+ * 官方上下文环周长:viewBox 14 内 r=5.5 圆(2π×5.5 ≈ 34.55751918948772),
52
+ * 与真机反推的官方 fill dasharray 分母逐字一致。
53
+ */
54
+ export declare const CONTEXT_METER_CIRCUMFERENCE = 34.55751918948772;
55
+ /**
56
+ * 光晕弧 stroke-dasharray:形状沿用官方 fill(len + 全周长 gap)。
57
+ * @param occupancyRatio 占窗口比 0..1(越界钳制)。
58
+ * @param minLen 最小可见弧长(14px 渲染下 <1 单位即亚像素不可见,真实占比常低至
59
+ * 0.6%≈0.2 单位——指示灯语义需要"存在即可见",精确数字归面板)。
60
+ */
61
+ export declare function haloDashArray(occupancyRatio: number, circumference?: number, minLen?: number): string;
62
+ /** 官方环触发按钮的结构签名(locale 无关定位依据)。 */
63
+ export interface ContextMeterAnchorSignature {
64
+ ariaHasPopup?: string | null;
65
+ viewBox?: string | null;
66
+ circleRadii?: ReadonlyArray<number>;
67
+ }
68
+ export declare function isContextMeterAnchor(sig: ContextMeterAnchorSignature): boolean;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 上下文占用账本:插件注入内容在当前会话上下文窗口中的启发式 token 存量。
3
+ *
4
+ * 本模块是全部占用数字的唯一算术来源(host 记账与 client 渲染共用,esbuild
5
+ * 将其内联进 client bundle),任何一侧不得另写算法。口径与官方
6
+ * @deepseek-ai/dsh-token-meter/estimate 的固定密度启发式同式:
7
+ *
8
+ * - text 块 = ceil(chars / 4) + 4(块开销);
9
+ * - 整条 user 消息(召回注入体的形态)再叠加 role overhead 4;
10
+ * - 系统提示里的稳定区子片计入时不加任何 overhead——那 +8 是官方对整段
11
+ * 消息/整体系统提示的结构成本,切片重复计会与官方总数对不上;
12
+ * - 字符数一律 JS `.length`(UTF-16 单元),与官方产出严格可比。
13
+ */
14
+ /** 固定文本密度:每 4 字符记 1 token(官方 CHARS_PER_TOKEN 同值)。 */
15
+ export const CHARS_PER_TOKEN = 4;
16
+ const BLOCK_OVERHEAD = 4;
17
+ const ROLE_OVERHEAD = 4;
18
+ /** 召回注入体(单 text 块 user message)的启发式 token 数。 */
19
+ export function estimateInjectedMessageTokens(chars) {
20
+ return Math.ceil(chars / CHARS_PER_TOKEN) + BLOCK_OVERHEAD + ROLE_OVERHEAD;
21
+ }
22
+ /** 稳定区子片的启发式 token 数(不加结构开销,见模块头注释)。 */
23
+ export function estimateStableSectionTokens(chars) {
24
+ return Math.ceil(chars / CHARS_PER_TOKEN);
25
+ }
26
+ export function emptyOccupancyLedger(now = Date.now()) {
27
+ return { stockTokens: 0, recallTokens: 0, profileTokens: 0, lastInjectTokens: 0, updatedAt: now };
28
+ }
29
+ /** 召回注入成功后的入账(失败跳过的轮次不调用——账目零扰动)。 */
30
+ export function recordRecallInjection(l, chars, now = Date.now()) {
31
+ const tokens = estimateInjectedMessageTokens(chars);
32
+ l.recallTokens += tokens;
33
+ l.stockTokens += tokens;
34
+ l.lastInjectTokens = tokens;
35
+ l.updatedAt = now;
36
+ }
37
+ /**
38
+ * 稳定区份额随每次实际组进的文本同步(含切回非 OFF 的重新计入):
39
+ * 增量记账保证重复调用不双算,清零后重设即可净额回补。
40
+ */
41
+ export function recordProfileShare(l, chars, now = Date.now()) {
42
+ const tokens = estimateStableSectionTokens(chars);
43
+ l.stockTokens += tokens - l.profileTokens;
44
+ l.profileTokens = tokens;
45
+ l.updatedAt = now;
46
+ }
47
+ /** OFF 边界:稳定区随下一次请求组装物理离场,账目同边界清零(宁低勿高)。 */
48
+ export function clearProfileShare(l, now = Date.now()) {
49
+ l.stockTokens -= l.profileTokens;
50
+ l.profileTokens = 0;
51
+ l.updatedAt = now;
52
+ }
53
+ /**
54
+ * compaction 复位:采用"清零重新累积"的低估近似——被压缩掉的注入以整数轮粒度
55
+ * 退出账本,幸存的摘要行不重复认领(已知限制,方向宁低勿高)。
56
+ */
57
+ export function resetForCompaction(l, now = Date.now()) {
58
+ l.stockTokens = 0;
59
+ l.recallTokens = 0;
60
+ l.profileTokens = 0;
61
+ l.lastInjectTokens = 0;
62
+ l.updatedAt = now;
63
+ }
64
+ /* ── 渲染侧纯函数(client 寄生组件用;数字唯一来源在此) ── */
65
+ /**
66
+ * 官方上下文环周长:viewBox 14 内 r=5.5 圆(2π×5.5 ≈ 34.55751918948772),
67
+ * 与真机反推的官方 fill dasharray 分母逐字一致。
68
+ */
69
+ export const CONTEXT_METER_CIRCUMFERENCE = 34.55751918948772;
70
+ /**
71
+ * 光晕弧 stroke-dasharray:形状沿用官方 fill(len + 全周长 gap)。
72
+ * @param occupancyRatio 占窗口比 0..1(越界钳制)。
73
+ * @param minLen 最小可见弧长(14px 渲染下 <1 单位即亚像素不可见,真实占比常低至
74
+ * 0.6%≈0.2 单位——指示灯语义需要"存在即可见",精确数字归面板)。
75
+ */
76
+ export function haloDashArray(occupancyRatio, circumference = CONTEXT_METER_CIRCUMFERENCE, minLen = 0) {
77
+ const clamped = Number.isFinite(occupancyRatio) ? Math.min(1, Math.max(0, occupancyRatio)) : 0;
78
+ const len = Math.max(clamped * circumference, clamped > 0 ? minLen : 0);
79
+ return `${len} ${circumference}`;
80
+ }
81
+ /** 半径比对容差:吞掉渲染库对属性值的无害浮点噪声。 */
82
+ const RADIUS_EPSILON = 1e-6;
83
+ export function isContextMeterAnchor(sig) {
84
+ if (sig.ariaHasPopup !== 'dialog')
85
+ return false;
86
+ if (sig.viewBox !== '0 0 14 14')
87
+ return false;
88
+ const radii = sig.circleRadii;
89
+ if (!Array.isArray(radii) || radii.length !== 2)
90
+ return false;
91
+ return radii.every((r) => Math.abs(r - 5.5) < RADIUS_EPSILON);
92
+ }
@@ -0,0 +1,6 @@
1
+ import type { MemoryLogger } from '../types.js';
2
+ /** 缓冲行数达到该值立即刷盘(未到定时器也不积压);同时是体积检查的抽样粒度。 */
3
+ export declare const SIZE_CHECK_INTERVAL = 32;
4
+ /** 错误对象转带堆栈的单行描述(诊断日志用,非 Error 直接字符串化)。 */
5
+ export declare function errDetail(err: unknown): string;
6
+ export declare function withFileLog(dataDir: string, logger: MemoryLogger): MemoryLogger;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * 文件日志:dsh 宿主只把插件日志打到控制台(无持久化),这里镜像 warn/error/info
3
+ * 到数据目录 memory.log,供事后诊断蒸馏管线。
4
+ *
5
+ * 重写版把落盘改为异步缓冲:捕获/召回是每轮对话的热路径,逐条同步 append 在
6
+ * Windows+杀软环境可达毫秒级。行先进内存缓冲,定时/定量批量 appendFile(队列
7
+ * 串行保序);轮转与 Windows EBUSY 截断兜底语义保留。写入失败静默忽略——
8
+ * 诊断日志绝不能反过来拖垮管线。
9
+ */
10
+ import { appendFileSync, existsSync, renameSync, statSync, writeFileSync } from 'node:fs';
11
+ import { appendFile } from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+ const MAX_LOG_BYTES = 2 * 1024 * 1024;
14
+ /** 轮转 rename 连续失败多少次后放弃归档、直接截断重开(Windows 文件占用兜底)。 */
15
+ const ROTATE_FAIL_LIMIT = 5;
16
+ /** 缓冲行数达到该值立即刷盘(未到定时器也不积压);同时是体积检查的抽样粒度。 */
17
+ export const SIZE_CHECK_INTERVAL = 32;
18
+ /** 定时刷盘间隔(ms);unref,不阻止进程退出。 */
19
+ const FLUSH_INTERVAL_MS = 500;
20
+ /** 错误对象转带堆栈的单行描述(诊断日志用,非 Error 直接字符串化)。 */
21
+ export function errDetail(err) {
22
+ if (err instanceof Error)
23
+ return `${err.message} @ ${err.stack?.split('\n')[1]?.trim() ?? err.name}`;
24
+ return String(err);
25
+ }
26
+ export function withFileLog(dataDir, logger) {
27
+ const logPath = join(dataDir, 'memory.log');
28
+ let buffer = [];
29
+ let rotateFailures = 0;
30
+ let writesSinceCheck = 0;
31
+ let flushing = Promise.resolve();
32
+ const line = (level, msg) => `${new Date().toISOString()} [${level}] ${msg}\n`;
33
+ const rotateIfNeeded = () => {
34
+ if (writesSinceCheck++ % SIZE_CHECK_INTERVAL !== 0 || !existsSync(logPath) || statSync(logPath).size <= MAX_LOG_BYTES) {
35
+ return;
36
+ }
37
+ try {
38
+ renameSync(logPath, `${logPath}.1`);
39
+ rotateFailures = 0;
40
+ }
41
+ catch {
42
+ // rename 可能因文件被占用失败(Windows EBUSY):连续失败达上限后截断重开,
43
+ // 保住"日志体积有上界"的底线(放弃 .1 归档,保当前日志可用)
44
+ if (++rotateFailures >= ROTATE_FAIL_LIMIT) {
45
+ writeFileSync(logPath, '');
46
+ rotateFailures = 0;
47
+ }
48
+ }
49
+ };
50
+ const drain = (lines) => {
51
+ flushing = flushing
52
+ .then(async () => {
53
+ if (lines.length === 0)
54
+ return;
55
+ rotateIfNeeded();
56
+ await appendFile(logPath, lines.join(''), 'utf-8');
57
+ })
58
+ .catch(() => {
59
+ /* ignore */
60
+ });
61
+ };
62
+ const flushNow = () => {
63
+ if (buffer.length === 0)
64
+ return;
65
+ const batch = buffer;
66
+ buffer = [];
67
+ drain(batch);
68
+ };
69
+ const timer = setInterval(flushNow, FLUSH_INTERVAL_MS);
70
+ timer.unref?.();
71
+ process.once('beforeExit', () => {
72
+ if (buffer.length > 0) {
73
+ const batch = buffer;
74
+ buffer = [];
75
+ try {
76
+ appendFileSync(logPath, batch.join(''), 'utf-8');
77
+ }
78
+ catch {
79
+ /* ignore */
80
+ }
81
+ }
82
+ });
83
+ const write = (level, msg) => {
84
+ try {
85
+ buffer.push(line(level, msg));
86
+ if (buffer.length >= SIZE_CHECK_INTERVAL)
87
+ flushNow();
88
+ }
89
+ catch {
90
+ /* ignore */
91
+ }
92
+ };
93
+ return {
94
+ debug: (m) => logger.debug?.(m),
95
+ info: (m) => {
96
+ logger.info(m);
97
+ write('info', m);
98
+ },
99
+ warn: (m) => {
100
+ logger.warn(m);
101
+ write('warn', m);
102
+ },
103
+ error: (m) => {
104
+ logger.error(m);
105
+ write('error', m);
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,18 @@
1
+ export declare function ensureDir(dir: string): Promise<void>;
2
+ /**
3
+ * 原子写文本文件。tmp 写满后先 fsync 数据块再 rename——否则断电时文件系统可能
4
+ * 先持久化 rename 元数据、后持久化数据块(ext4 delayed allocation / NTFS 均可能),
5
+ * 目标文件变成空文件或半截。tmp 名带随机段防同毫秒碰撞;失败路径清理孤儿 tmp。
6
+ */
7
+ export declare function atomicWriteText(file: string, content: string): Promise<void>;
8
+ /** 原子写 JSON(两空格缩进,人工可查)。 */
9
+ export declare function atomicWriteJson(file: string, value: unknown): Promise<void>;
10
+ export declare function readJsonIfExists<T>(file: string): Promise<T | undefined>;
11
+ export declare function readTextIfExists(file: string): Promise<string | undefined>;
12
+ /** 追加 JSONL 行(存在则追加,否则创建);空数组零副作用。 */
13
+ export declare function appendJsonl(file: string, lines: unknown[]): Promise<void>;
14
+ /** 读取 JSONL 全部行(坏行跳过,不抛出)。 */
15
+ export declare function readJsonl<T>(file: string): Promise<T[]>;
16
+ export declare function nowIso(): string;
17
+ /** 本地时区的 YYYY-MM-DD 键(L0/L1 按天分文件的文件名来源)。 */
18
+ export declare function dayKey(ts: number): string;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 持久化工具:原子写(tmp + fsync + rename)与 JSON/JSONL 读写。
3
+ *
4
+ * JSONL 追加是热路径(每轮对话一次),走 OS 写回不加 fsync——逐条 fsync 的延迟
5
+ * 代价大于崩溃窗口丢尾部几行的损失;状态文件则必须原子写,防半截状态。
6
+ */
7
+ import { randomUUID } from 'node:crypto';
8
+ import { promises as fs } from 'node:fs';
9
+ import * as path from 'node:path';
10
+ export async function ensureDir(dir) {
11
+ await fs.mkdir(dir, { recursive: true });
12
+ }
13
+ /**
14
+ * 原子写文本文件。tmp 写满后先 fsync 数据块再 rename——否则断电时文件系统可能
15
+ * 先持久化 rename 元数据、后持久化数据块(ext4 delayed allocation / NTFS 均可能),
16
+ * 目标文件变成空文件或半截。tmp 名带随机段防同毫秒碰撞;失败路径清理孤儿 tmp。
17
+ */
18
+ export async function atomicWriteText(file, content) {
19
+ await ensureDir(path.dirname(file));
20
+ const tmp = `${file}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
21
+ try {
22
+ await fs.writeFile(tmp, content, 'utf-8');
23
+ const fh = await fs.open(tmp, 'r+');
24
+ try {
25
+ await fh.sync();
26
+ }
27
+ finally {
28
+ await fh.close();
29
+ }
30
+ await fs.rename(tmp, file);
31
+ }
32
+ catch (err) {
33
+ await fs.unlink(tmp).catch(() => { });
34
+ throw err;
35
+ }
36
+ }
37
+ /** 原子写 JSON(两空格缩进,人工可查)。 */
38
+ export async function atomicWriteJson(file, value) {
39
+ await atomicWriteText(file, JSON.stringify(value, null, 2));
40
+ }
41
+ export async function readJsonIfExists(file) {
42
+ try {
43
+ const raw = await fs.readFile(file, 'utf-8');
44
+ return JSON.parse(raw);
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ export async function readTextIfExists(file) {
51
+ try {
52
+ return await fs.readFile(file, 'utf-8');
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ /** 追加 JSONL 行(存在则追加,否则创建);空数组零副作用。 */
59
+ export async function appendJsonl(file, lines) {
60
+ if (lines.length === 0)
61
+ return;
62
+ await ensureDir(path.dirname(file));
63
+ const payload = lines.map((l) => JSON.stringify(l)).join('\n') + '\n';
64
+ await fs.appendFile(file, payload, 'utf-8');
65
+ }
66
+ /** 读取 JSONL 全部行(坏行跳过,不抛出)。 */
67
+ export async function readJsonl(file) {
68
+ try {
69
+ const raw = await fs.readFile(file, 'utf-8');
70
+ const out = [];
71
+ for (const line of raw.split('\n')) {
72
+ const t = line.trim();
73
+ if (!t)
74
+ continue;
75
+ try {
76
+ out.push(JSON.parse(t));
77
+ }
78
+ catch {
79
+ // 跳过坏行
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ }
88
+ export function nowIso() {
89
+ return new Date().toISOString();
90
+ }
91
+ /** 本地时区的 YYYY-MM-DD 键(L0/L1 按天分文件的文件名来源)。 */
92
+ export function dayKey(ts) {
93
+ const d = new Date(ts);
94
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
95
+ const dd = String(d.getDate()).padStart(2, '0');
96
+ return `${d.getFullYear()}-${mm}-${dd}`;
97
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 召回预算与超时(ADR-0001 / 规格 A 节)。
3
+ *
4
+ * 预算:单条记忆截断上限 + 整轮总量上限——超限截断并以后缀引导模型用记忆工具
5
+ * 查全文(截断是引流而不是损失:工具路径返回完整记录);总量超限按融合排名丢尾部。
6
+ * 超时:召回是增强能力,超时跳过本轮注入、绝不阻塞对话(CONTEXT.md「召回超时」语义
7
+ * 在本模块以 raceRecallTimeout 落地)。
8
+ */
9
+ /** 截断后缀:显式告诉模型全文在工具侧(引导主动深挖)。注入字符串,逐字为契约。 */
10
+ export declare const RECALL_TRUNCATION_SUFFIX = "\u2026(\u5DF2\u622A\u65AD;\u53EF\u7528 memory_search \u6216 conversation_search \u67E5\u770B\u8BE6\u60C5)";
11
+ export interface RecallBudgetLimits {
12
+ /** 单条记忆注入长度上限(字符);0 = 不限。 */
13
+ maxCharsPerMemory: number;
14
+ /** 整轮注入总量上限(字符);0 = 不限。超限时低分(排名靠后)尾部先丢。 */
15
+ maxTotalRecallChars: number;
16
+ }
17
+ /** 按 code point 计数截断(不劈开代理对),带引导后缀。 */
18
+ export declare function truncateRecallLine(line: string, maxChars: number): string;
19
+ /**
20
+ * 对召回行施加预算:先逐条截断,再按总量预算装填——装不下的尾部整条丢弃。
21
+ * 输入行应按相关性降序(低分先丢)。行间换行符计入总量预算。
22
+ */
23
+ export declare function applyRecallBudget(lines: string[], limits: RecallBudgetLimits): string[];
24
+ /**
25
+ * 召回总预算:超时返回 undefined(调用方跳过本轮注入),正常 resolve 返回原值。
26
+ * resolve 为空结果(空数组)与超时(undefined)语义不同,调用方据此区分日志。
27
+ */
28
+ export declare function raceRecallTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | undefined>;
29
+ /** 召回路径嵌入调用的内层钳制(固定值):给 FTS 降级留出总预算内的时间。
30
+ * 远程作用于 HTTP fetch;本地作用于 worker 代理的等待(race 放弃、迟到回复丢弃)。 */
31
+ export declare const RECALL_EMBED_CAP_MS = 3000;