sage-mem 1.9.1 → 2.0.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 +29 -6
- package/README.zh.md +29 -6
- package/lib/autodream/audit.js +82 -2
- package/lib/autodream/config.js +10 -0
- package/lib/autodream/engine.js +4 -0
- package/lib/autodream/llm-loop.js +77 -10
- package/lib/autodream/prompt.js +3 -3
- package/lib/autodream/report.js +20 -10
- package/lib/autodream/settings.js +6 -0
- package/lib/autodream/tools.js +77 -12
- package/lib/client.js +960 -0
- package/lib/index.js +189 -0
- package/lib/memory/arbitrate.js +236 -0
- package/lib/memory/config.js +16 -0
- package/lib/memory/conflicts.js +225 -22
- package/lib/memory/frontmatter.js +22 -1
- package/lib/memory/scan.js +12 -0
- package/lib/memory/status.js +39 -7
- package/lib/memory/validity.js +156 -0
- package/lib/recall/compose.js +15 -5
- package/lib/recall/injector.js +38 -2
- package/lib/tools/memory-tools.js +47 -4
- package/lib/typert.host.js +166 -0
- package/package.json +3 -2
package/lib/recall/compose.js
CHANGED
|
@@ -56,8 +56,12 @@ export function buildCandidates({ picked, bestChunk, nowMs = Date.now() }) {
|
|
|
56
56
|
*
|
|
57
57
|
* 按**实际进正文的字节**记账,而不是 `delivery.totals` —— 后者含指针的额度,
|
|
58
58
|
* 而指针没有真的进上下文。两本账要对得上,就不能把没花出去的钱记进去。
|
|
59
|
+
*
|
|
60
|
+
* 2.0.0 起多收两个集合(`conflicts` / `expired`):在此之前它们是硬编码空集,
|
|
61
|
+
* 而 `deliver.js` 里「未裁决冲突 / 已失效来源」两条规则早就写好了 —— 这一版只是
|
|
62
|
+
* 把真实数据接上去,**deliver.js 一行不改**(docs/2.0.0-plan.md §6.2)。
|
|
59
63
|
*/
|
|
60
|
-
export function composeDelivery({ candidates, text, intent, contentBytes, nowMs = Date.now() }) {
|
|
64
|
+
export function composeDelivery({ candidates, text, intent, contentBytes, nowMs = Date.now(), conflicts, expired }) {
|
|
61
65
|
const delivery = decideDelivery(candidates, {
|
|
62
66
|
intent,
|
|
63
67
|
// ⚠️ 这里**故意传空集**:按 revision 抑制重复对「同一份文件的不同段落」太粗 ——
|
|
@@ -66,8 +70,8 @@ export function composeDelivery({ candidates, text, intent, contentBytes, nowMs
|
|
|
66
70
|
// 并且单独评测一次。这条记在 docs/decisions.md 的 D15,不是遗漏。
|
|
67
71
|
visibleRevisions: new Set(),
|
|
68
72
|
scopeDenied: new Set(),
|
|
69
|
-
conflicts: new Set(),
|
|
70
|
-
expired: new Set(),
|
|
73
|
+
conflicts: conflicts instanceof Set ? conflicts : new Set(),
|
|
74
|
+
expired: expired instanceof Set ? expired : new Set(),
|
|
71
75
|
needle: text ? text.slice(0, 40) : '',
|
|
72
76
|
}, { bytes: Math.max(0, CONTENT_BUDGET - contentBytes) })
|
|
73
77
|
|
|
@@ -103,7 +107,7 @@ export function composeDelivery({ candidates, text, intent, contentBytes, nowMs
|
|
|
103
107
|
*/
|
|
104
108
|
export function composeStatus({
|
|
105
109
|
files, selected, bodyRanked, bodySearched, text, parts, injectedFiles, skippedForBudget,
|
|
106
|
-
deduped, st, toolsReady, checkpointLine, memoryDir, intentHistoryDependent,
|
|
110
|
+
deduped, st, toolsReady, checkpointLine, memoryDir, intentHistoryDependent, pendingConflicts,
|
|
107
111
|
}) {
|
|
108
112
|
// ── 缺口状态卡 ──
|
|
109
113
|
// 它**替换**原来那行状态文字,不是额外追加:同一件事不说两遍。
|
|
@@ -118,8 +122,13 @@ export function composeStatus({
|
|
|
118
122
|
// 后者是计划里点名的一条:没有历史依赖时不要插「你可能遗漏记忆」,否则提示
|
|
119
123
|
// 每轮都出现,等真有缺口时它已经被当噪音忽略了。
|
|
120
124
|
const gapExists = unread > 0 || skippedForBudget > 0
|
|
125
|
+
// 2.0.0:有矛盾待裁决时**必须**发卡 —— 抑制是「记忆悄悄不进正文」,
|
|
126
|
+
// 状态卡是用户不主动看面板时唯一会自己出现的解释(D27 教训,见 plan §6.4)。
|
|
127
|
+
const pending = Number.isFinite(Number(pendingConflicts)) && Number(pendingConflicts) > 0
|
|
128
|
+
? Math.floor(Number(pendingConflicts))
|
|
129
|
+
: 0
|
|
121
130
|
|
|
122
|
-
const wantCard = gapExists || intentHistoryDependent === true || !!checkpointLine
|
|
131
|
+
const wantCard = gapExists || intentHistoryDependent === true || !!checkpointLine || pending > 0
|
|
123
132
|
// 签名没变时沿用上一步渲染好的那份缺口卡,而不是重渲染一张。
|
|
124
133
|
// 两条理由:其一,那一步与这一步的检索面**逐字相同**,缺口也相同,重渲染只会把
|
|
125
134
|
// 同一件事换个数字再说一遍 —— 而局部变量算出来的数字还是错的(上一步注了 N 条,
|
|
@@ -140,6 +149,7 @@ export function composeStatus({
|
|
|
140
149
|
needle: text ? text.slice(0, 40) : null,
|
|
141
150
|
toolsAvailable: toolsReady,
|
|
142
151
|
memoryDir,
|
|
152
|
+
pendingConflicts: pending,
|
|
143
153
|
})
|
|
144
154
|
// 句柄与卡**共用卡池**:它是同一个通道里的另一句提示,不该另开一份额度。
|
|
145
155
|
const cardText = [cardBody, checkpointLine].filter(Boolean).join('\n')
|
package/lib/recall/injector.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* 1. 只取 `source.kind === 'user'` 的消息 —— DSH 0.1.5 起注入与工具结果也存成 role:'user';
|
|
15
15
|
* 2. 代次处理(`state.begin`)必须在算正文与控制提示**之前**,否则压缩后会拿上一代的账去判重(复审 R03)。
|
|
16
16
|
*/
|
|
17
|
-
import { MEMORY_DIR, RECALL_DISCIPLINE, STATE_ROOT } from '../memory/config.js'
|
|
17
|
+
import { MEMORY_DIR, RECALL_DISCIPLINE, STATE_ROOT, validityEnabled } from '../memory/config.js'
|
|
18
|
+
import { listConflictIndex } from '../memory/conflicts.js'
|
|
19
|
+
import { buildContradictionFileSet, buildExpirySet } from '../memory/validity.js'
|
|
18
20
|
import { extractText } from '../memory/retrieval.js'
|
|
19
21
|
import { scanMemoryFiles } from '../memory/scan.js'
|
|
20
22
|
import { readCheckpoint } from '../memory/checkpoint.js'
|
|
@@ -85,6 +87,31 @@ export function createRecallInjector({ ctx, state, tools, taskIdOf }) {
|
|
|
85
87
|
// 统一的出口在下面 —— 只有**正文与控制提示都为空**才返回。
|
|
86
88
|
|
|
87
89
|
const nowMs = Date.now()
|
|
90
|
+
// ── 2.0.0:有效性过滤与矛盾抑制的真实集合 ──────────────────────────
|
|
91
|
+
// 两件事分开算:失效看**记忆自己的 frontmatter**(事实来源),矛盾看冲突队列
|
|
92
|
+
// (待办 + 审计)。队列读不动时**不抑制**、只留日志 —— 抑制是「少给点内容」,
|
|
93
|
+
// 拿一个读失败去换「记忆消失」不划算(D27)。
|
|
94
|
+
//
|
|
95
|
+
// ⚠️ 只对 `contradiction` 进集合:写失败型记录里盘上那份仍是现行内容(§6.3)。
|
|
96
|
+
let conflictsForDelivery = new Set()
|
|
97
|
+
let pendingConflicts = 0
|
|
98
|
+
const validity = validityEnabled()
|
|
99
|
+
const expired = validity ? buildExpirySet(files, nowMs) : new Set()
|
|
100
|
+
if (validity) {
|
|
101
|
+
try {
|
|
102
|
+
// 用**摘要索引**(不带两份正文、进程内缓存 1 秒):100 条记录时 `listConflicts`
|
|
103
|
+
// 单次实测 p50 78 ms,而它每一步 assemble 都会被调一次(见 conflicts.js 的缓存注释)。
|
|
104
|
+
const listed = await listConflictIndex({ stateRoot: STATE_ROOT })
|
|
105
|
+
if (listed.ok) {
|
|
106
|
+
conflictsForDelivery = buildContradictionFileSet(listed.items)
|
|
107
|
+
pendingConflicts = listed.items.filter((c) => c.status === 'open' && c.kind === 'contradiction').length
|
|
108
|
+
} else {
|
|
109
|
+
warnLog(ctx, `读冲突队列失败,本轮不做矛盾抑制:${listed.error ?? listed.code}`, sessionId)
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
warnLog(ctx, `读冲突队列异常,本轮不做矛盾抑制:${err?.message ?? err}`, sessionId)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
88
115
|
const candidates = buildCandidates({ picked, bestChunk, nowMs })
|
|
89
116
|
|
|
90
117
|
// ── 单会话累计预算:**拆成两本账** ──
|
|
@@ -94,7 +121,15 @@ export function createRecallInjector({ ctx, state, tools, taskIdOf }) {
|
|
|
94
121
|
// 账记在 session id 上;picked 里 baseline 永远排在检索结果之前,所以预算不够时
|
|
95
122
|
// 先被挤掉的是「更晚才想起来的」那些 —— baseline 有优先权。
|
|
96
123
|
// 起点来自 `begin()`:它可能刚因压缩把两本账连同卡签名一起归零过。
|
|
97
|
-
const delivered = composeDelivery({
|
|
124
|
+
const delivered = composeDelivery({
|
|
125
|
+
candidates,
|
|
126
|
+
text,
|
|
127
|
+
intent: intentInfo.intent,
|
|
128
|
+
contentBytes: st.contentBytes,
|
|
129
|
+
nowMs,
|
|
130
|
+
conflicts: conflictsForDelivery,
|
|
131
|
+
expired,
|
|
132
|
+
})
|
|
98
133
|
const { parts, injectedFiles, skippedForBudget, contentBytes } = delivered
|
|
99
134
|
|
|
100
135
|
// ── 任务检查点:压缩后的恢复入口 ──
|
|
@@ -113,6 +148,7 @@ export function createRecallInjector({ ctx, state, tools, taskIdOf }) {
|
|
|
113
148
|
files, selected, bodyRanked, bodySearched, text, parts, injectedFiles, skippedForBudget,
|
|
114
149
|
deduped, st, toolsReady: tools.ready(), checkpointLine, memoryDir: MEMORY_DIR,
|
|
115
150
|
intentHistoryDependent: intentInfo.historyDependent,
|
|
151
|
+
pendingConflicts,
|
|
116
152
|
})
|
|
117
153
|
const { cardBody, cardPart } = status
|
|
118
154
|
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
import { DEFAULT_MAX_BYTES, MAX_MAX_BYTES, readSlice } from '../memory/reader.js'
|
|
20
20
|
import { DEFAULT_LIMIT, MAX_LIMIT, retrieve } from '../memory/search.js'
|
|
21
21
|
import { scanMemoryFiles } from '../memory/scan.js'
|
|
22
|
+
import { parseFrontmatter } from '../memory/frontmatter.js'
|
|
23
|
+
import { describeValidity } from '../memory/validity.js'
|
|
22
24
|
|
|
23
25
|
/** 工具返回内容统一用 text block(宿主把 ContentBlock[] 交给模型)。 */
|
|
24
26
|
const text = (s) => [{ type: 'text', text: String(s ?? '') }]
|
|
@@ -45,8 +47,15 @@ function renderSearchResult(args, value) {
|
|
|
45
47
|
const lines = v.hits.map((h, i) => {
|
|
46
48
|
const where = h.lines ? `${h.file}:${h.lines[0]}-${h.lines[1]}` : h.file
|
|
47
49
|
const why = Array.isArray(h.matchedBy) ? h.matchedBy.join('+') : ''
|
|
50
|
+
// 2.0.0:失效记忆**仍然能被搜到**(问历史时旧口径是证据),但必须标出来 ——
|
|
51
|
+
// 不标的话模型会把旧口径当现状,那正是「标了失效却仍在误导」的更坏版本。
|
|
52
|
+
const validity = h.expired
|
|
53
|
+
? `(已失效${h.invalidAt ? `:invalid_at=${h.invalidAt}` : ''},不是现行口径${h.supersededBy ? `;现行见 ${h.supersededBy}` : ''})`
|
|
54
|
+
: h.invalidAt || h.supersededBy
|
|
55
|
+
? `(有失效标记但未到点${h.invalidAt ? `:invalid_at=${h.invalidAt}` : ''}${h.supersededBy ? `;现行见 ${h.supersededBy}` : ''})`
|
|
56
|
+
: ''
|
|
48
57
|
const excerpt = h.excerpt ? `\n ${renderClip(h.excerpt.replace(/\n/g, ' '), 200)}` : ''
|
|
49
|
-
return `${i + 1}. ${where}${why ? `(${why})` : ''}${excerpt}`
|
|
58
|
+
return `${i + 1}. ${where}${why ? `(${why})` : ''}${validity}${excerpt}`
|
|
50
59
|
})
|
|
51
60
|
const tail = v.nextCursor != null ? `\n\n还有下一页:用 cursor=${v.nextCursor} 继续。` : ''
|
|
52
61
|
return text(`${head}\n${lines.join('\n')}${tail}`)
|
|
@@ -66,6 +75,10 @@ function renderReadResult(args, value) {
|
|
|
66
75
|
const flags = [
|
|
67
76
|
v.lineTooLong ? '这一行本身超过了一屏' : '',
|
|
68
77
|
v.truncated ? '内容被截断' : '',
|
|
78
|
+
// 2.0.0:失效标记必须跟着正文一起给模型 —— 读得到 ≠ 仍然是现行口径。
|
|
79
|
+
v.expired ? '这条记忆已失效(invalid_at 已到点),下面是历史口径,不是现状' : '',
|
|
80
|
+
!v.expired && v.invalidAt ? `这条记忆带失效标记但未到点(invalid_at=${v.invalidAt})` : '',
|
|
81
|
+
v.supersededBy ? `现行口径见 ${v.supersededBy}` : '',
|
|
69
82
|
].filter(Boolean)
|
|
70
83
|
const tail = v.nextStartLine != null
|
|
71
84
|
? `\n\n还有内容:startLine=${v.nextStartLine}${v.nextStartByte ? ` startByte=${v.nextStartByte}` : ''}`
|
|
@@ -86,7 +99,8 @@ export function createMemoryTools(ctx) {
|
|
|
86
99
|
'在本地记忆库(markdown 文件)里检索历史记忆。先用一句话摘要匹配、再按段落查正文,返回候选与命中片段。' +
|
|
87
100
|
'当你需要依据既往决定、用户偏好、或继续未完成的任务,而当前上下文里没有可定位的依据时用它。' +
|
|
88
101
|
'scope 选 summary 只比摘要(最省)、body 只查正文段落、all 两者都查(默认)。' +
|
|
89
|
-
'结果里带 file 与行号,可直接用 read_memory 读那一段。'
|
|
102
|
+
'结果里带 file 与行号,可直接用 read_memory 读那一段。' +
|
|
103
|
+
'已标失效(invalid_at)的记忆**仍然会被搜到**,结果里会带 invalidAt / supersededBy —— 那是历史口径,不是现状。',
|
|
90
104
|
parameters: {
|
|
91
105
|
type: 'object',
|
|
92
106
|
properties: {
|
|
@@ -105,7 +119,24 @@ export function createMemoryTools(ctx) {
|
|
|
105
119
|
if (!query) return { ok: false, error: 'query 不能为空' }
|
|
106
120
|
try {
|
|
107
121
|
const files = await scanMemoryFiles(ctx, exec?.agent?.id ?? '')
|
|
108
|
-
|
|
122
|
+
const result = retrieve({ query, files, scope: args?.scope, limit: args?.limit, cursor: args?.cursor })
|
|
123
|
+
// 2.0.0:把每条命中对应的失效信息带出去(D29 Q3)。检索本身**不过滤**失效记忆 ——
|
|
124
|
+
// 问历史时旧口径是证据;过滤掉才是丢信息。模型靠这里的标记判断「这是不是现状」。
|
|
125
|
+
if (result && result.ok !== false && Array.isArray(result.hits)) {
|
|
126
|
+
const byFile = new Map(files.map((f) => [f.file, f]))
|
|
127
|
+
const nowMs = Date.now()
|
|
128
|
+
result.hits = result.hits.map((h) => {
|
|
129
|
+
const file = byFile.get(h.file)
|
|
130
|
+
if (!file) return h
|
|
131
|
+
const v = describeValidity(file, nowMs)
|
|
132
|
+
const extra = {}
|
|
133
|
+
if (v.invalidAt) extra.invalidAt = v.invalidAt
|
|
134
|
+
if (v.supersededBy) extra.supersededBy = v.supersededBy
|
|
135
|
+
if (v.expired) extra.expired = true
|
|
136
|
+
return Object.keys(extra).length ? { ...h, ...extra } : h
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
return result
|
|
109
140
|
} catch (err) {
|
|
110
141
|
// 工具**绝不抛**:一次检索失败不该打断模型那一轮。
|
|
111
142
|
return { ok: false, error: `检索出错:${err?.message ?? err}` }
|
|
@@ -118,6 +149,7 @@ export function createMemoryTools(ctx) {
|
|
|
118
149
|
description:
|
|
119
150
|
'按文件名读取一份本地记忆的正文,可按行分页。用 search_memory 拿到的 file 与行号来读。' +
|
|
120
151
|
'返回内容带 revision;如果你上次读之后这份记忆被改过,会告诉你 revision 已变,需要重新确认。' +
|
|
152
|
+
'如果这份记忆已标失效(invalid_at 已到点),页头会明确写出来 —— 那是历史口径。' +
|
|
121
153
|
`一次默认读 ${DEFAULT_MAX_BYTES} 字节,返回的 nextStartLine / nextStartByte 用于继续往下读。`,
|
|
122
154
|
parameters: {
|
|
123
155
|
type: 'object',
|
|
@@ -135,13 +167,24 @@ export function createMemoryTools(ctx) {
|
|
|
135
167
|
isConcurrencySafe: () => true,
|
|
136
168
|
async execute(args) {
|
|
137
169
|
try {
|
|
138
|
-
|
|
170
|
+
const result = await readSlice({
|
|
139
171
|
name: args?.name,
|
|
140
172
|
revision: args?.revision,
|
|
141
173
|
startLine: args?.startLine,
|
|
142
174
|
startByte: args?.startByte,
|
|
143
175
|
maxBytes: args?.maxBytes,
|
|
144
176
|
})
|
|
177
|
+
// 只在本页包含 frontmatter 时解析(分页读的后半段没有 frontmatter,解析出来是空 —— 不猜)。
|
|
178
|
+
if (result && result.ok !== false && typeof result.content === 'string') {
|
|
179
|
+
const meta = parseFrontmatter(result.content)
|
|
180
|
+
if (meta.invalidAt || meta.supersededBy) {
|
|
181
|
+
const v = describeValidity({ invalidAt: meta.invalidAt, supersededBy: meta.supersededBy }, Date.now())
|
|
182
|
+
result.invalidAt = v.invalidAt
|
|
183
|
+
result.supersededBy = v.supersededBy
|
|
184
|
+
result.expired = v.expired
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result
|
|
145
188
|
} catch (err) {
|
|
146
189
|
return { ok: false, code: 'io_error', error: `读取出错:${err?.message ?? err}` }
|
|
147
190
|
}
|
package/lib/typert.host.js
CHANGED
|
@@ -178,6 +178,13 @@ const auditReportSchema = z.object({
|
|
|
178
178
|
// 归档/恢复中途崩溃留下的私有名残留(`*.md.archiving-<hex>`)。它可能装着用户唯一
|
|
179
179
|
// 一份内容,所以体检必须报出来 —— strict 不声明它就等于界面上永远看不见。
|
|
180
180
|
'strays': z.array(auditItemSchema).readonly(),
|
|
181
|
+
// 2.0.0:有效性标注的**提示**(不进 problems —— problems 是唯一健康数字)。
|
|
182
|
+
// 不声明的话 strict 会把它剥掉,面板与设置页就永远看不到「某个字段格式坏了」。
|
|
183
|
+
'validityHints': z.array(z.object({
|
|
184
|
+
'file': z.string().readonly(),
|
|
185
|
+
'field': z.string().readonly(),
|
|
186
|
+
'reason': z.string().readonly(),
|
|
187
|
+
}).readonly()).readonly(),
|
|
181
188
|
'problems': z.number().readonly(),
|
|
182
189
|
}).readonly()
|
|
183
190
|
|
|
@@ -307,6 +314,8 @@ const autodreamConfigSchema = z.object({
|
|
|
307
314
|
'provider': z.string().readonly(),
|
|
308
315
|
'model': z.string().readonly(),
|
|
309
316
|
'maxSteps': z.number().readonly(),
|
|
317
|
+
// 1.9.2:单次输出上限(token)。响应与 patch 两侧都必须声明 —— strict codec 会剥掉未声明字段。
|
|
318
|
+
'maxOutputTokens': z.number().readonly(),
|
|
310
319
|
'maxSnapshotKeep': z.number().readonly(),
|
|
311
320
|
'rollbackScope': z.string().readonly(),
|
|
312
321
|
/**
|
|
@@ -414,6 +423,7 @@ const autodreamPatchSchema = z.object({
|
|
|
414
423
|
'provider': z.string().optional(),
|
|
415
424
|
'model': z.string().optional(),
|
|
416
425
|
'maxSteps': z.number().optional(),
|
|
426
|
+
'maxOutputTokens': z.number().optional(),
|
|
417
427
|
'maxSnapshotKeep': z.number().optional(),
|
|
418
428
|
'rollbackScope': z.string().optional(),
|
|
419
429
|
/**
|
|
@@ -509,6 +519,90 @@ const autodreamOptsSchema = z.object({
|
|
|
509
519
|
'scope': z.string().optional(),
|
|
510
520
|
}).readonly()
|
|
511
521
|
|
|
522
|
+
// ── 2.0.0:冲突裁决面(conflict.*)────────────────────────────────
|
|
523
|
+
// 结果 schema 必须**同时容纳成功与失败两种形状**:这一面的约定是失败返回
|
|
524
|
+
// `{ok:false, code, error}` 而不是抛异常。strict 校验会按声明裁剪 —— 只声明成功字段,
|
|
525
|
+
// 失败时的 code / error 就会在传到界面前消失,面板只剩一个没有原因的 ok:false。
|
|
526
|
+
//
|
|
527
|
+
// 另一条边界:列表只回摘要(`invalidate` / `undo` 等动作的结果回完整记录)。
|
|
528
|
+
// 一份记录里两份正文各上限 48 KB,整批列表搬过去会把 remote 面撑爆。
|
|
529
|
+
const conflictIdSchema = z.string().min(1).max(64)
|
|
530
|
+
const conflictOptsSchema = z.object({
|
|
531
|
+
'id': conflictIdSchema,
|
|
532
|
+
'file': fileNameSchema.optional(),
|
|
533
|
+
'note': z.string().max(4096).optional(),
|
|
534
|
+
}).readonly()
|
|
535
|
+
const conflictItemBase = z.object({
|
|
536
|
+
'id': z.string().readonly(),
|
|
537
|
+
'at': z.string().readonly(),
|
|
538
|
+
'kind': z.string().readonly(),
|
|
539
|
+
'status': z.string().readonly(),
|
|
540
|
+
'file': z.string().readonly(),
|
|
541
|
+
'otherFile': z.string().readonly(),
|
|
542
|
+
'note': z.string().readonly(),
|
|
543
|
+
'decision': z.string().readonly(),
|
|
544
|
+
'decidedAt': z.string().readonly(),
|
|
545
|
+
'appliedVersion': z.string().readonly(),
|
|
546
|
+
// 这次被标失效的是哪一方(`file` 或 `otherFile`)——`undo` 唯一可信的落点。
|
|
547
|
+
'invalidatedFile': z.string().readonly(),
|
|
548
|
+
'truncated': z.boolean().readonly(),
|
|
549
|
+
})
|
|
550
|
+
const conflictItemSchema = conflictItemBase.readonly()
|
|
551
|
+
const conflictRecordSchema = conflictItemBase.extend({
|
|
552
|
+
'incoming': z.string().readonly(),
|
|
553
|
+
'onDisk': z.string().readonly(),
|
|
554
|
+
}).readonly()
|
|
555
|
+
const conflictCountsSchema = z.object({
|
|
556
|
+
'open': z.number().readonly(),
|
|
557
|
+
'total': z.number().readonly(),
|
|
558
|
+
}).readonly()
|
|
559
|
+
const conflictWroteSchema = z.object({
|
|
560
|
+
'file': z.string().readonly(),
|
|
561
|
+
'invalidAt': z.string().readonly(),
|
|
562
|
+
'version': z.string().readonly(),
|
|
563
|
+
}).readonly()
|
|
564
|
+
const conflictListSchema = z.object({
|
|
565
|
+
'ok': z.boolean().readonly(),
|
|
566
|
+
'items': z.array(conflictItemSchema).readonly().optional(),
|
|
567
|
+
'broken': z.array(z.string()).readonly().optional(),
|
|
568
|
+
'ignored': z.array(z.string()).readonly().optional(),
|
|
569
|
+
'counts': conflictCountsSchema.optional(),
|
|
570
|
+
'code': z.string().readonly().optional(),
|
|
571
|
+
'error': z.string().readonly().optional(),
|
|
572
|
+
}).readonly()
|
|
573
|
+
const conflictReadResultSchema = z.object({
|
|
574
|
+
'ok': z.boolean().readonly(),
|
|
575
|
+
'record': conflictRecordSchema.optional(),
|
|
576
|
+
'warnings': z.array(z.string()).readonly().optional(),
|
|
577
|
+
'code': z.string().readonly().optional(),
|
|
578
|
+
'error': z.string().readonly().optional(),
|
|
579
|
+
}).readonly()
|
|
580
|
+
const conflictInvalidateResultSchema = z.object({
|
|
581
|
+
'ok': z.boolean().readonly(),
|
|
582
|
+
'record': conflictRecordSchema.optional(),
|
|
583
|
+
'wrote': conflictWroteSchema.optional(),
|
|
584
|
+
'code': z.string().readonly().optional(),
|
|
585
|
+
'error': z.string().readonly().optional(),
|
|
586
|
+
}).readonly()
|
|
587
|
+
const conflictDismissResultSchema = z.object({
|
|
588
|
+
'ok': z.boolean().readonly(),
|
|
589
|
+
'record': conflictRecordSchema.optional(),
|
|
590
|
+
'code': z.string().readonly().optional(),
|
|
591
|
+
'error': z.string().readonly().optional(),
|
|
592
|
+
}).readonly()
|
|
593
|
+
const conflictUndoResultSchema = z.object({
|
|
594
|
+
'ok': z.boolean().readonly(),
|
|
595
|
+
'record': conflictRecordSchema.optional(),
|
|
596
|
+
'cleared': z.object({ 'file': z.string().readonly() }).readonly().optional(),
|
|
597
|
+
'code': z.string().readonly().optional(),
|
|
598
|
+
'error': z.string().readonly().optional(),
|
|
599
|
+
}).readonly()
|
|
600
|
+
const conflictRemoveResultSchema = z.object({
|
|
601
|
+
'ok': z.boolean().readonly(),
|
|
602
|
+
'code': z.string().readonly().optional(),
|
|
603
|
+
'error': z.string().readonly().optional(),
|
|
604
|
+
}).readonly()
|
|
605
|
+
|
|
512
606
|
export const TYPERT = {
|
|
513
607
|
package: 'sage-mem',
|
|
514
608
|
face: 'host',
|
|
@@ -907,6 +1001,78 @@ export const TYPERT = {
|
|
|
907
1001
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamModelList', schema: modelListSchema, create: () => modelListSchema },
|
|
908
1002
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
909
1003
|
},
|
|
1004
|
+
// ── 2.0.0:冲突裁决面(sourceLocation 硬编码为 lib/index.js —— 见 D21)──
|
|
1005
|
+
{
|
|
1006
|
+
id: 'sage-mem#conflict/list',
|
|
1007
|
+
service: 'conflict',
|
|
1008
|
+
namespace: 'conflict',
|
|
1009
|
+
method: 'list',
|
|
1010
|
+
invocation: { kind: 'direct' },
|
|
1011
|
+
parameters: [],
|
|
1012
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictList', schema: conflictListSchema, create: () => conflictListSchema },
|
|
1013
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
id: 'sage-mem#conflict/read',
|
|
1017
|
+
service: 'conflict',
|
|
1018
|
+
namespace: 'conflict',
|
|
1019
|
+
method: 'read',
|
|
1020
|
+
invocation: { kind: 'direct' },
|
|
1021
|
+
parameters: [
|
|
1022
|
+
{ name: 'opts', wire: 'opts', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictOpts', schema: conflictOptsSchema, create: () => conflictOptsSchema } },
|
|
1023
|
+
],
|
|
1024
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictReadResult', schema: conflictReadResultSchema, create: () => conflictReadResultSchema },
|
|
1025
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
id: 'sage-mem#conflict/invalidate',
|
|
1029
|
+
service: 'conflict',
|
|
1030
|
+
namespace: 'conflict',
|
|
1031
|
+
method: 'invalidate',
|
|
1032
|
+
invocation: { kind: 'direct' },
|
|
1033
|
+
parameters: [
|
|
1034
|
+
{ name: 'opts', wire: 'opts', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictOpts', schema: conflictOptsSchema, create: () => conflictOptsSchema } },
|
|
1035
|
+
],
|
|
1036
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictInvalidateResult', schema: conflictInvalidateResultSchema, create: () => conflictInvalidateResultSchema },
|
|
1037
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
id: 'sage-mem#conflict/dismiss',
|
|
1041
|
+
service: 'conflict',
|
|
1042
|
+
namespace: 'conflict',
|
|
1043
|
+
method: 'dismiss',
|
|
1044
|
+
invocation: { kind: 'direct' },
|
|
1045
|
+
parameters: [
|
|
1046
|
+
{ name: 'opts', wire: 'opts', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictOpts', schema: conflictOptsSchema, create: () => conflictOptsSchema } },
|
|
1047
|
+
],
|
|
1048
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictDismissResult', schema: conflictDismissResultSchema, create: () => conflictDismissResultSchema },
|
|
1049
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
id: 'sage-mem#conflict/undo',
|
|
1053
|
+
service: 'conflict',
|
|
1054
|
+
namespace: 'conflict',
|
|
1055
|
+
method: 'undo',
|
|
1056
|
+
invocation: { kind: 'direct' },
|
|
1057
|
+
parameters: [
|
|
1058
|
+
{ name: 'opts', wire: 'opts', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictOpts', schema: conflictOptsSchema, create: () => conflictOptsSchema } },
|
|
1059
|
+
],
|
|
1060
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictUndoResult', schema: conflictUndoResultSchema, create: () => conflictUndoResultSchema },
|
|
1061
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
// ⚠️ 不叫 `remove`:客户端 RemoteNamespaceService 的保留方法名,见 lib/index.js 的注释。
|
|
1065
|
+
id: 'sage-mem#conflict/removeRecord',
|
|
1066
|
+
service: 'conflict',
|
|
1067
|
+
namespace: 'conflict',
|
|
1068
|
+
method: 'removeRecord',
|
|
1069
|
+
invocation: { kind: 'direct' },
|
|
1070
|
+
parameters: [
|
|
1071
|
+
{ name: 'opts', wire: 'opts', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictOpts', schema: conflictOptsSchema, create: () => conflictOptsSchema } },
|
|
1072
|
+
],
|
|
1073
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#ConflictRemoveResult', schema: conflictRemoveResultSchema, create: () => conflictRemoveResultSchema },
|
|
1074
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1075
|
+
},
|
|
910
1076
|
],
|
|
911
1077
|
model: {
|
|
912
1078
|
"services": [],
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sage-mem",
|
|
3
3
|
"description": "Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"icon": "./icon.png",
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"0.1.7-rc.2": "compatible",
|
|
51
51
|
"0.2.0-rc.1": "compatible",
|
|
52
52
|
"0.2.0-rc.2": "compatible",
|
|
53
|
-
"0.2.1-rc.1": "compatible"
|
|
53
|
+
"0.2.1-rc.1": "compatible",
|
|
54
|
+
"0.2.1-rc.3": "compatible"
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
},
|