sage-mem 1.0.2 → 1.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.
- package/README.md +10 -2
- package/README.zh.md +10 -2
- package/lib/assembly/autodream-mount.js +24 -0
- package/lib/assembly/index.js +13 -0
- package/lib/assembly/observe-mount.js +37 -0
- package/lib/assembly/tools-mount.js +44 -0
- package/lib/assembly/write-gate-mount.js +92 -0
- package/lib/{autodream-audit.js → autodream/audit.js} +2 -2
- package/lib/autodream/engine.js +2 -2
- package/lib/autodream/llm-loop.js +1 -1
- package/lib/autodream/report.js +1 -1
- package/lib/{autodream-tools.js → autodream/tools.js} +8 -8
- package/lib/client.js +93 -16
- package/lib/index.js +43 -720
- package/lib/memory/archive.js +102 -1
- package/lib/memory/write-gate.js +1 -1
- package/lib/recall/compose.js +160 -0
- package/lib/recall/debug.js +23 -0
- package/lib/recall/index.js +15 -0
- package/lib/recall/injector.js +181 -0
- package/lib/recall/select.js +138 -0
- package/lib/recall/state.js +46 -0
- package/lib/recall/text.js +14 -0
- package/lib/settings.js +98 -0
- package/lib/typert.host.js +19 -0
- package/package.json +1 -1
- /package/lib/{autodream-prompt.js → autodream/prompt.js} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话级注入状态的持有者(1.2.0 从 `lib/index.js` 原样搬出)。
|
|
3
|
+
*
|
|
4
|
+
* 一处持有、一处提交:`begin()` 只读并处理代次,`commit()` 一次写回。
|
|
5
|
+
* 为什么提交放最后:中途抛错时状态不该前进 —— 没注进去的东西不能记成「已注过」,
|
|
6
|
+
* 否则一次失败会把这条记忆永久挡在门外。唯一的例外是代次变化:那是**已经发生的事实**
|
|
7
|
+
* (上下文真的被压缩了),必须在算任何东西之前生效,否则这一步会拿着上一代的账做判断。
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ 诚实边界:压缩信号是「消息条数下降」这条启发式,它**覆盖不了条数不降的压缩**
|
|
10
|
+
* (宿主原地改写历史,或截断后又在同一步补足条数)。那种情况下这里看不出代次变化,
|
|
11
|
+
* 记忆仍会被去重挡住。要真正解决得等宿主给出可比的上下文代次标识(目前没有)。
|
|
12
|
+
*/
|
|
13
|
+
export function createSessionInjectState(maxSessions = 64) {
|
|
14
|
+
const sessions = new Map()
|
|
15
|
+
const fresh = () => ({
|
|
16
|
+
signature: null, // 已注入过的检索签名(问题 + 命中文件集)
|
|
17
|
+
contentBytes: 0, // 本代正文累计字节
|
|
18
|
+
cardBytes: 0, // 本代状态卡累计字节(与正文分账,见 config.js)
|
|
19
|
+
cardSig: '', // 本代已发过的那张卡(含检查点句柄的合成文本)
|
|
20
|
+
gapCard: '', // 本代渲染过的缺口卡正文(签名没变时沿用,避免换个数字重发)
|
|
21
|
+
messageCount: -1, // 上一次见效时的消息条数(压缩判据)
|
|
22
|
+
baselineDone: false, // 本代是否已经给过 baseline
|
|
23
|
+
})
|
|
24
|
+
return {
|
|
25
|
+
/**
|
|
26
|
+
* 处理代次变化,取回本会话这一代的起点。**必须在算正文与控制提示之前调用。**
|
|
27
|
+
* 返回值是一份快照 —— 调用方只读,写回统一走 `commit()`。
|
|
28
|
+
*/
|
|
29
|
+
begin(key, messageCount) {
|
|
30
|
+
const prev = sessions.get(key)
|
|
31
|
+
const generationChanged = !!prev && messageCount < prev.messageCount
|
|
32
|
+
// 条数下降 → 四样东西一起归零。少清任何一样都会留下一个「上一代的真相」:
|
|
33
|
+
// 字节账残留 → 新内容注不进来;baseline 残留 → 它再也不出现;
|
|
34
|
+
// 卡签名残留 → 恢复入口永久失效。
|
|
35
|
+
if (generationChanged) sessions.set(key, { ...fresh(), messageCount })
|
|
36
|
+
const cur = sessions.get(key) ?? fresh()
|
|
37
|
+
return { ...cur, generationChanged }
|
|
38
|
+
},
|
|
39
|
+
/** 一次写回。只防长驻进程无限增长:被挤掉的老会话下次醒来至多把同一批记忆再注一次。 */
|
|
40
|
+
commit(key, next) {
|
|
41
|
+
sessions.set(key, { ...next })
|
|
42
|
+
// FIFO 而不是 LRU:Map 重写已存在的键不会挪动位置,与旧行为一致。
|
|
43
|
+
if (sessions.size > maxSessions) sessions.delete(sessions.keys().next().value)
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 注入文本的**组装**(1.2.0 从 `lib/index.js` 原样搬出)。
|
|
3
|
+
*
|
|
4
|
+
* 只有正文时:头部 + 引用纪律 + 正文 [+ 卡]
|
|
5
|
+
* 只有卡时:**就只发卡** —— 头部那句「以下是与你当前问题相关的历史记忆」在没有正文
|
|
6
|
+
* 的情况下是假陈述,而引用纪律管的是「别把记过的东西当当前事实」,没有正文就没有
|
|
7
|
+
* 需要约束的对象。
|
|
8
|
+
*/
|
|
9
|
+
export function composeRecallText({ parts, cardPart, memoryDir, discipline }) {
|
|
10
|
+
if (!parts.length) return cardPart || ''
|
|
11
|
+
return `以下是与你当前问题相关的历史记忆(文件式,来自 ${memoryDir}):\n\n` +
|
|
12
|
+
`${discipline}\n\n${parts.join('\n\n')}` +
|
|
13
|
+
(cardPart ? `\n\n${cardPart}` : '')
|
|
14
|
+
}
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 面板设置与「保留名原样读写」的落点(1.2.0 从 `lib/index.js` 原样搬出)。
|
|
3
|
+
*
|
|
4
|
+
* 这里的每一项都与 `memory/config.js` 的 `SAGE_MEM_*` 一一对应;边界值照抄那边的
|
|
5
|
+
* envInt 调用(lo/hi 不许在这里另立一套)。
|
|
6
|
+
*/
|
|
7
|
+
import { readFile } from 'node:fs/promises'
|
|
8
|
+
import { basename, join } from 'node:path'
|
|
9
|
+
import { STATE_ROOT } from './memory/config.js'
|
|
10
|
+
import { DEFAULT_CONFIG } from './autodream/config.js'
|
|
11
|
+
import { readJson } from './autodream/util.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 面板可改的 5 项注入上限 —— 与 `memory/config.js` 里的 `SAGE_MEM_*` 一一对应。
|
|
15
|
+
* 边界值照抄那边的 envInt 调用(lo/hi 不许在这里另立一套)。
|
|
16
|
+
*/
|
|
17
|
+
export const LIMIT_SPECS = {
|
|
18
|
+
maxResults: { env: 'SAGE_MEM_MAX_RESULTS', dflt: 10, lo: 1, hi: 30 },
|
|
19
|
+
maxChars: { env: 'SAGE_MEM_MAX_CHARS', dflt: 1500, lo: 200, hi: 20000 },
|
|
20
|
+
maxBaseline: { env: 'SAGE_MEM_MAX_BASELINE', dflt: 8, lo: 1, hi: 50 },
|
|
21
|
+
maxSessionBytes: { env: 'SAGE_MEM_MAX_SESSION_BYTES', dflt: 60 * 1024, lo: 4096, hi: 512 * 1024 },
|
|
22
|
+
staleDays: { env: 'SAGE_MEM_STALE_DAYS', dflt: 1, lo: 0, hi: 365 },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 面板设置的落点:状态根下(与 autodream 的配置、台账同级)。 */
|
|
26
|
+
export const SETTINGS_FILE = join(STATE_ROOT, 'settings.json')
|
|
27
|
+
export const RESERVED_FILE = join(STATE_ROOT, 'reserved.json')
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `writeRaw` 的体积上限(4 MB)。
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ **故意不复用 `MAX_FILE_BYTES`(512 KB)**:保留名里就有 `session-log.md` 这种追加型
|
|
33
|
+
* 流水,它天然长得比单条记忆大得多(实测已 > 512 KB)。沿用小上限 = 用户永远存不回去。
|
|
34
|
+
*/
|
|
35
|
+
export const MAX_RAW_BYTES = 4 * 1024 * 1024
|
|
36
|
+
|
|
37
|
+
/** 整数收敛(与 config.js 的 envInt 同一套语义):非有限数回落默认,越界夹到边界。 */
|
|
38
|
+
export const clampInt = (v, spec) => (Number.isFinite(v) ? Math.max(spec.lo, Math.min(spec.hi, Math.round(v))) : spec.dflt)
|
|
39
|
+
|
|
40
|
+
/** 读 `<状态根>/settings.json`;缺失、坏 JSON、不是对象都当「没设置」。 */
|
|
41
|
+
export async function readSettingsFile() {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(await readFile(SETTINGS_FILE, 'utf8'))
|
|
44
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
|
|
45
|
+
} catch {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 待生效的 5 项上限:`settings.json` > 环境变量 > 内置默认。 */
|
|
51
|
+
export function pendingLimits(fileObj) {
|
|
52
|
+
const out = {}
|
|
53
|
+
const saved = fileObj?.limits && typeof fileObj.limits === 'object' ? fileObj.limits : {}
|
|
54
|
+
for (const [key, spec] of Object.entries(LIMIT_SPECS)) {
|
|
55
|
+
const fromFile = Number(saved[key])
|
|
56
|
+
if (Number.isFinite(fromFile)) {
|
|
57
|
+
out[key] = clampInt(fromFile, spec)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
const raw = process.env[spec.env]
|
|
61
|
+
out[key] = clampInt(raw === undefined || raw === '' ? NaN : Number(raw), spec)
|
|
62
|
+
}
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 读 `<状态根>/reserved.json`(用户声明的保护名单)。**保留原始大小写**,好让 get 读回一致。 */
|
|
67
|
+
export async function readReservedFile() {
|
|
68
|
+
try {
|
|
69
|
+
const arr = JSON.parse(await readFile(RESERVED_FILE, 'utf8'))
|
|
70
|
+
if (!Array.isArray(arr)) return []
|
|
71
|
+
return arr
|
|
72
|
+
.map((s) => String(s).trim())
|
|
73
|
+
.filter((s) => s.endsWith('.md') && !s.includes('/') && !s.includes('\\') && !s.startsWith('..'))
|
|
74
|
+
} catch {
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 原样读写用的名字校验:只要求「单段、`.md`」。
|
|
81
|
+
*
|
|
82
|
+
* ⚠️ **故意不走 `safeName`**:那个函数把 `MEMORY.md` / `session-log.md` 当非记忆条目排除,
|
|
83
|
+
* 而它们恰恰是 `readRaw` / `writeRaw` 的目标。这里只防路径成分与扩展名。
|
|
84
|
+
*/
|
|
85
|
+
export function rawName(raw) {
|
|
86
|
+
const s = typeof raw === 'string' ? raw : ''
|
|
87
|
+
if (!s || basename(s) !== s) return null
|
|
88
|
+
// `:` 是 NTFS 交替数据流的分隔符:`MEMORY.md:evil.md` 能过扩展名判据,落盘却是写到
|
|
89
|
+
// 索引的隐藏流上(`readdir` 看不见)。与 `safeName` 同一条理由,这里也要拒。
|
|
90
|
+
if (s.includes(':')) return null
|
|
91
|
+
return /\.md$/i.test(s) ? s : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** autodream 的实际配置(缺文件/坏文件回落默认值)—— 面板要显示真实策略,不能显示想象。 */
|
|
95
|
+
export async function autodreamConfigSnapshot() {
|
|
96
|
+
const parsed = await readJson(join(STATE_ROOT, 'autodream.json'))
|
|
97
|
+
return { ...DEFAULT_CONFIG, ...(parsed?.config ?? {}) }
|
|
98
|
+
}
|
package/lib/typert.host.js
CHANGED
|
@@ -96,6 +96,9 @@ const archivedListSchema = z.object({
|
|
|
96
96
|
// 归档缘由:用户输入的一句话。真正的 YAML 转义在 host 侧 yamlScalar(值里的「: 」「 #」),
|
|
97
97
|
// 这里只兜长度 —— 缘由不该长到把 frontmatter 变成正文。
|
|
98
98
|
const archiveReasonSchema = z.string().max(1000).optional()
|
|
99
|
+
// 归档代次(`archived_at`,`YYYY-MM-DD HH:mm`):面板把它交回「彻底删除」当**身份判据**
|
|
100
|
+
// —— 用户确认过的是「这一版归档件」,名字不是身份。老归档件没有留痕时传空串,所以不设 min。
|
|
101
|
+
const archiveStampSchema = z.string().max(64)
|
|
99
102
|
// starmap.listStars 的可选开关:不看归档星时行为与从前完全一致。
|
|
100
103
|
const includeArchivedSchema = z.boolean().optional()
|
|
101
104
|
|
|
@@ -585,6 +588,22 @@ export const TYPERT = {
|
|
|
585
588
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#ArchivedWriteResult', schema: archivedWriteResultSchema, create: () => archivedWriteResultSchema },
|
|
586
589
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
587
590
|
},
|
|
591
|
+
{
|
|
592
|
+
// **彻底删除**一条归档件(不可恢复)。第二个参数是身份判据,不是装饰:
|
|
593
|
+
// 名字在「用户确认」与「请求到达」之间可能已经被恢复+重新归档成另一版。
|
|
594
|
+
id: 'sage-mem#memory/deleteArchived',
|
|
595
|
+
service: 'memory',
|
|
596
|
+
namespace: 'memory',
|
|
597
|
+
method: 'deleteArchived',
|
|
598
|
+
invocation: { kind: 'direct' },
|
|
599
|
+
parameters: [
|
|
600
|
+
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#ArchivedFileName', schema: fileNameSchema, create: () => fileNameSchema } },
|
|
601
|
+
{ name: 'archivedAt', wire: 'archivedAt', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#ArchiveStamp', schema: archiveStampSchema, create: () => archiveStampSchema } },
|
|
602
|
+
],
|
|
603
|
+
// 成功 `{ ok: true, file }`、失败 `{ ok: false, error }` —— 与归档 / 恢复同形,复用 ArchiveResult。
|
|
604
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#ArchiveResult', schema: archiveResultSchema, create: () => archiveResultSchema },
|
|
605
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
606
|
+
},
|
|
588
607
|
{
|
|
589
608
|
id: 'sage-mem#memory/audit',
|
|
590
609
|
service: 'memory',
|
package/package.json
CHANGED
|
File without changes
|