@a9i5k4/dsh-auto-memory 2.5.3 → 3.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 +171 -1
- package/README.zh-CN.md +171 -1
- package/docs/CONTRIBUTORS.html +471 -0
- package/docs/HANDOFF-CRITERIA.md +92 -0
- package/docs/INTEGRATION-ANALYSIS.md +350 -348
- package/docs/USER-GUIDE.en.md +56 -1
- package/docs/USER-GUIDE.zh-CN.md +57 -2
- package/docs/internal/ACCEPT-35-LIVE.md +143 -0
- package/docs/internal/ACCEPTANCE-20260914.md +90 -0
- package/docs/internal/ARCH-REVIEW-BRIEF.md +411 -0
- package/docs/internal/ARCH-REVIEW-REQUEST.md +201 -0
- package/docs/internal/ARCH-REVIEW-ROUND2.md +169 -0
- package/docs/internal/ARCH-REVIEW-ROUND3.md +206 -0
- package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +314 -0
- package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +192 -0
- package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +72 -0
- package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +131 -0
- package/docs/internal/DECISIONS-20260914-SESSION.md +269 -0
- package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +219 -0
- package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +132 -0
- package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +13 -0
- package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +74 -0
- package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +352 -0
- package/docs/internal/GPT-REVIEW-PROMPT.md +216 -0
- package/docs/internal/GROUP-WEBHOOK-SETUP.md +33 -0
- package/docs/internal/KICKOFF-P0.md +254 -0
- package/docs/internal/MASTER-PLAN-3.0.md +411 -0
- package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +85 -0
- package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +222 -0
- package/docs/internal/PENDING-FIXES-20260916.md +289 -0
- package/docs/internal/RAG-KARPATHY-PROGRAM.md +229 -0
- package/docs/internal/REPORT-P0-NIGHTLY.md +212 -0
- package/docs/internal/REPORT-P5-ACCEPTANCE.md +31 -0
- package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +153 -0
- package/docs/internal/REVIEW-WB-GRAPH-SELF.md +81 -0
- package/docs/internal/ROADMAP-20260917-WEEK.md +305 -0
- package/docs/internal/ROADMAP.md +106 -0
- package/docs/internal/RUN-P0-NIGHTLY.md +227 -0
- package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +175 -0
- package/docs/internal/S10-GAPS-PLAIN-20260917.md +125 -0
- package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +360 -0
- package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +90 -0
- package/docs/internal/THREE-LAYER-CONTRACT.md +210 -0
- package/docs/internal/TODO-BACKLOG.md +263 -142
- package/docs/internal/TODO-GRAPH.html +715 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +493 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +710 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +710 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +703 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +710 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +715 -0
- package/docs/internal/WB-FORMAT-CONVENTION.md +112 -0
- package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +71 -0
- package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +56 -0
- package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +787 -0
- package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +112 -0
- package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +230 -0
- package/docs/prompts/M8-3-enable-verify.md +49 -49
- package/lib/acceptance.js +71 -0
- package/lib/activation-host.js +90 -9
- package/lib/activation-inbox.js +25 -7
- package/lib/board-mode.js +30 -0
- package/lib/client.js +878 -75
- package/lib/context-bridge.js +2 -2
- package/lib/context-host.js +70 -6
- package/lib/engine-identity.js +149 -0
- package/lib/engine-switch.js +247 -0
- package/lib/episodic-store.js +11 -10
- package/lib/evidence-store.js +2 -2
- package/lib/fact-store.js +1 -1
- package/lib/fs-retry.js +46 -0
- package/lib/index.js +1987 -153
- package/lib/intent-clean-safe.js +40 -0
- package/lib/intent-clean.js +12 -16
- package/lib/l0-extract.js +263 -149
- package/lib/l0-index-sync.js +195 -0
- package/lib/l0-index.js +349 -239
- package/lib/ledger-criteria.js +142 -0
- package/lib/m7-index-sync-host.js +65 -4
- package/lib/m7-wire.js +3 -3
- package/lib/memory-anchor.js +56 -1
- package/lib/memory-envelope.js +252 -0
- package/lib/memory-hub.js +14 -4
- package/lib/memory-mutation.js +246 -0
- package/lib/memory-writer.js +204 -24
- package/lib/procedure-observation.js +48 -0
- package/lib/procedure-store.js +34 -17
- package/lib/python-setup.js +1 -1
- package/lib/rerank-host.js +160 -0
- package/lib/rules-layer.js +261 -0
- package/lib/semantic-js.js +15 -0
- package/lib/shadow-retrieval.js +3 -3
- package/lib/state-commit.js +245 -0
- package/lib/subagent-gc.js +4 -8
- package/lib/tier-layer-inject.js +650 -0
- package/lib/tier0-catalog.js +693 -0
- package/lib/water-window.js +263 -186
- package/lib/wb-contract.js +495 -0
- package/lib/wb-sidecar.js +839 -0
- package/lib/ws-overview-rank.js +2 -2
- package/package.json +1 -1
- package/python/m7_embedding_v1.py +5 -5
- package/python/worker_semantic_v1.py +17 -6
- package/python/worker_v1.py +38 -4
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一状态提交契约(state_commit_v1)—— P1 主体(2026-09-15)。
|
|
3
|
+
*
|
|
4
|
+
* 依据:`docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md`(已获用户批准)§2.1 / §2.2 / §2.3,
|
|
5
|
+
* `MASTER-PLAN-3.0.md` Phase 1、`TODO-GRAPH.html` 卡 V2-P1。
|
|
6
|
+
*
|
|
7
|
+
* **一句话目标**:把「谁在写、写的什么版本、写完算不算数」收敛成**一个提交边界**。
|
|
8
|
+
*
|
|
9
|
+
* **三条硬规则(本模块是它们的唯一实现点)**:
|
|
10
|
+
* 1. **miv 是内容身份 + 状态清单摘要**(哈希,**不递增、不比较**)。
|
|
11
|
+
* 任何代码不得写 `if (miv > lastMiv)` —— 它不是版本序。
|
|
12
|
+
* 2. **`boardId` 在工作区内稳定、不含 sessionId**:由 `sha256(workspaceKey + '|' + scope)` 派生。
|
|
13
|
+
* 接续后新窗口换 sessionId,若 boardId 跟着变,图就会被当成两块,共享语义直接崩。
|
|
14
|
+
* 3. **必须废弃的口径**:白板 `index.json` 的 `rebuilt_at` **不得当版本序** ——
|
|
15
|
+
* 它是时间戳,重建时间变而内容没变时它会变 ⇒ 用它当版本序会造成**假失效 + 真混版**。
|
|
16
|
+
* 断言 T1-5c 专门钉死这一条。
|
|
17
|
+
*
|
|
18
|
+
* **边界(不在本模块做)**:
|
|
19
|
+
* - 不加长期编辑锁、不按会话分片(卡内明确否决)——并发靠「队列串行 + 边界内比较」。
|
|
20
|
+
* - 不解析白板格式(KICKOFF §3.4:白板线拥有 `parseWhiteboardPre`)。
|
|
21
|
+
* - `atomicReplace` 不是 CAS,本模块不给它加语义;提交校验必须发生在**队列内部**。
|
|
22
|
+
*
|
|
23
|
+
* S9 合规:零 IO、零外部依赖(只用 node:crypto)、纯函数、无网络/无 LLM/无子进程/无 await。
|
|
24
|
+
* UTF-8 无 BOM。
|
|
25
|
+
*/
|
|
26
|
+
import { createHash } from 'node:crypto'
|
|
27
|
+
|
|
28
|
+
export const STATE_COMMIT_VERSION_PRE = 'state_commit_v1'
|
|
29
|
+
|
|
30
|
+
/** miv 前缀(与 `shadow-retrieval.js:144` 既有口径一致:`idx_` + first32hex)。 */
|
|
31
|
+
export const MIV_PREFIX_PRE = 'idx_'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 三种状态的**命名隔离**(卡内要求"三种状态不能混")。
|
|
35
|
+
* 三者**不得互转**:记忆有效状态是"条目还算不算数",任务进度是"活干完没有",
|
|
36
|
+
* 归档位置是"东西放哪儿"。`archived` 一词两义的问题以独立常量解决,**映射由白板线适配器负责**。
|
|
37
|
+
*/
|
|
38
|
+
export const MEMORY_STATUS_PRE = Object.freeze(['current', 'superseded', 'retracted'])
|
|
39
|
+
export const TASK_STATE_PRE = Object.freeze(['open', 'done', 'passed'])
|
|
40
|
+
export const ARCHIVE_STATE_PRE = Object.freeze(['active', 'archived'])
|
|
41
|
+
|
|
42
|
+
/** 提交单据必填字段(缺任一 ⇒ fail-closed 拒绝,不猜测)。 */
|
|
43
|
+
export const COMMIT_REQUIRED_V1 = Object.freeze(['workspaceKey', 'boardId', 'txId', 'actor'])
|
|
44
|
+
|
|
45
|
+
/** 冲突/拒绝原因码 → 可读中文。 */
|
|
46
|
+
export const COMMIT_REASONS_V1 = Object.freeze({
|
|
47
|
+
'not-object': '传入的不是对象',
|
|
48
|
+
'missing-field': '提交单据缺必填字段',
|
|
49
|
+
'invalid-writes': 'writes 形状非法(必须是数组)',
|
|
50
|
+
'digest-mismatch': '提交前摘要不匹配(外部编辑或并发写)',
|
|
51
|
+
'state-version-mismatch': '状态版本不匹配(外部变更或并发提交)',
|
|
52
|
+
'actor-invalid': 'actor 形状非法(需 { sessionId, kind })',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/** 原因码 → 可读中文(未知码原样返回)。 */
|
|
56
|
+
export function describeCommitReasonPre(code) {
|
|
57
|
+
const k = String(code == null ? '' : code)
|
|
58
|
+
return COMMIT_REASONS_V1[k] || k || '未知原因'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function asNonEmptyString(v) {
|
|
62
|
+
if (typeof v !== 'string') return null
|
|
63
|
+
const t = v.trim()
|
|
64
|
+
return t ? t : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function canonicalJson(v) {
|
|
68
|
+
// 稳定键序序列化(避免插入顺序导致同内容不同哈希)
|
|
69
|
+
if (v === null || typeof v !== 'object') return JSON.stringify(v)
|
|
70
|
+
if (Array.isArray(v)) return '[' + v.map(canonicalJson).join(',') + ']'
|
|
71
|
+
const keys = Object.keys(v).sort()
|
|
72
|
+
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(v[k])).join(',') + '}'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sha256HexPre(text) {
|
|
76
|
+
return createHash('sha256').update(String(text), 'utf8').digest('hex')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* **miv 单源**(P1 步 2):`memoryIndexVersionPre(projection)`。
|
|
81
|
+
*
|
|
82
|
+
* ⚠️ **与既有实现的边界(2026-09-15 施工期核实,必须分清)**:
|
|
83
|
+
* 本函数算的是 **"状态清单摘要"口径**的 miv —— 输入是**规范化投影**
|
|
84
|
+
* (`{records:[{id,status,l0?}], boardCards?}`),用于「同一份快照」的**身份判定**,
|
|
85
|
+
* 以及 pinned 状态进入摘要(状态变了 miv 必变)。
|
|
86
|
+
* 它**不替换**契约 §8 的 `shadow-retrieval.js:145 memoryIndexVersion(sources)` ——
|
|
87
|
+
* 那个吃的是 **source tuples**(scope/sourceRef/epoch/version/fileDigest),是**建索引**侧的真源。
|
|
88
|
+
* 两者**输入不同、用途不同**,不可互相替换;本函数是"提交边界侧"的口径,
|
|
89
|
+
* 且**刻意保持与 §8 相同的前缀与长度**(`idx_` + 32 hex),使二者在外部看来同形。
|
|
90
|
+
*
|
|
91
|
+
* ⚠️ 本函数**不负责**收敛 `index.js:4188 tierCurrentMivPre()`(那处的缓存/指纹语义属 T0-2 已交付内容),
|
|
92
|
+
* P1 不动它 —— 见设计稿 §2.2 边界说明。任何"用本函数替换 tier 自造版"的改动都**不在 P1 范围**。
|
|
93
|
+
*
|
|
94
|
+
* 输入:`{ records: [{id, status, l0?}], boardCards?: [{id, status}], scope? }`
|
|
95
|
+
* 输出:`'idx_' + first32hex(sha256(canonical))`
|
|
96
|
+
*
|
|
97
|
+
* canonical 构成(按 id 升序,换行拼接,无尾随空白):
|
|
98
|
+
* 每条 → `<id>\t<status>\t<contentDigest>`;`contentDigest` = 内容摘要(无内容时取空串)。
|
|
99
|
+
* `boardCards` 若给出,追加在 records 之后(同样排序)—— 白板卡片状态变化也进摘要。
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ **不递增、不比较**。这是哈希身份。`rebuilt_at`(时间戳)**不得**参与本函数,也不得
|
|
102
|
+
* 被任何调用方当作版本序使用 —— 见文件头第 3 条硬规则。
|
|
103
|
+
*/
|
|
104
|
+
export function memoryIndexVersionPre(projection) {
|
|
105
|
+
const o = projection && typeof projection === 'object' ? projection : {}
|
|
106
|
+
const records = Array.isArray(o.records) ? o.records : []
|
|
107
|
+
const boardCards = Array.isArray(o.boardCards) ? o.boardCards : []
|
|
108
|
+
const tuples = []
|
|
109
|
+
for (const r of records) {
|
|
110
|
+
if (!r || typeof r !== 'object') continue
|
|
111
|
+
const id = asNonEmptyString(r.id)
|
|
112
|
+
if (!id) continue
|
|
113
|
+
const status = asNonEmptyString(r.status) || ''
|
|
114
|
+
const contentDigest = r.l0 == null ? '' : sha256HexPre(canonicalJson(r.l0)).slice(0, 16)
|
|
115
|
+
tuples.push([id, status, contentDigest])
|
|
116
|
+
}
|
|
117
|
+
for (const c of boardCards) {
|
|
118
|
+
if (!c || typeof c !== 'object') continue
|
|
119
|
+
const id = asNonEmptyString(c.id)
|
|
120
|
+
if (!id) continue
|
|
121
|
+
tuples.push(['board:' + id, asNonEmptyString(c.status) || '', ''])
|
|
122
|
+
}
|
|
123
|
+
tuples.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
124
|
+
const canonical = tuples.map((t) => t.join('\t')).join('\n')
|
|
125
|
+
return MIV_PREFIX_PRE + sha256HexPre(canonical).slice(0, 32)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* **boardId 派生**(卡内硬约束):工作区内稳定、**不含 sessionId**。
|
|
130
|
+
*
|
|
131
|
+
* ⚠️ 入参**只接受 workspaceKey 与 scope**。刻意不接收 sessionId/agent 对象 ——
|
|
132
|
+
* 从签名上就杜绝"顺手把会话号掺进去"这个错误。
|
|
133
|
+
*/
|
|
134
|
+
export function boardIdPre(workspaceKey, scope) {
|
|
135
|
+
const ws = asNonEmptyString(workspaceKey)
|
|
136
|
+
if (!ws) return null
|
|
137
|
+
const sc = asNonEmptyString(scope) || 'Workspace'
|
|
138
|
+
return 'board_' + sha256HexPre(ws + '|' + sc).slice(0, 24)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 构造并校验提交单据(P1 步 1)。
|
|
143
|
+
*
|
|
144
|
+
* 形状:`{ workspaceKey, boardId, txId, expectedDigest?, expectedStateVersion?,
|
|
145
|
+
* actor: { sessionId, contSeq?, kind }, writes: [{ path, content, expectedDigest? }],
|
|
146
|
+
* stateChanges?: [{ target, from, to }] }`
|
|
147
|
+
*
|
|
148
|
+
* **fail-closed**:缺必填字段 / actor 形状非法 / writes 非数组 ⇒ `{ok:false}`,不猜测、不补默认值。
|
|
149
|
+
*
|
|
150
|
+
* **向后兼容(T1-8)**:`expectedStateVersion` 与 `expectedDigest` 均为**可选**;
|
|
151
|
+
* 不传时下游行为必须与本契约引入前**逐字节一致**(该校验由调用方在队列内执行)。
|
|
152
|
+
*/
|
|
153
|
+
export function buildStateCommitPre(input) {
|
|
154
|
+
const o = input && typeof input === 'object' ? input : null
|
|
155
|
+
if (!o) return { ok: false, reason: 'not-object', detail: describeCommitReasonPre('not-object') }
|
|
156
|
+
|
|
157
|
+
const missing = []
|
|
158
|
+
const workspaceKey = asNonEmptyString(o.workspaceKey)
|
|
159
|
+
if (!workspaceKey) missing.push('workspaceKey')
|
|
160
|
+
const boardId = asNonEmptyString(o.boardId)
|
|
161
|
+
if (!boardId) missing.push('boardId')
|
|
162
|
+
const txId = asNonEmptyString(o.txId)
|
|
163
|
+
if (!txId) missing.push('txId')
|
|
164
|
+
|
|
165
|
+
const actor = o.actor && typeof o.actor === 'object' ? o.actor : null
|
|
166
|
+
const actorSession = actor ? asNonEmptyString(actor.sessionId) : null
|
|
167
|
+
const actorKind = actor ? asNonEmptyString(actor.kind) : null
|
|
168
|
+
if (!actor || !actorSession || !actorKind) {
|
|
169
|
+
return { ok: false, reason: 'actor-invalid', detail: describeCommitReasonPre('actor-invalid'), missing: actor ? ['actor.sessionId', 'actor.kind'] : ['actor'] }
|
|
170
|
+
}
|
|
171
|
+
if (missing.length) {
|
|
172
|
+
return { ok: false, reason: 'missing-field', detail: describeCommitReasonPre('missing-field'), missing }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (o.writes !== undefined && !Array.isArray(o.writes)) {
|
|
176
|
+
return { ok: false, reason: 'invalid-writes', detail: describeCommitReasonPre('invalid-writes') }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const commit = {
|
|
180
|
+
schemaVersion: STATE_COMMIT_VERSION_PRE,
|
|
181
|
+
workspaceKey,
|
|
182
|
+
boardId,
|
|
183
|
+
txId,
|
|
184
|
+
actor: {
|
|
185
|
+
sessionId: actorSession,
|
|
186
|
+
contSeq: Number.isFinite(Number(actor.contSeq)) ? Number(actor.contSeq) : undefined,
|
|
187
|
+
kind: actorKind,
|
|
188
|
+
},
|
|
189
|
+
writes: Array.isArray(o.writes) ? o.writes : [],
|
|
190
|
+
stateChanges: Array.isArray(o.stateChanges) ? o.stateChanges : [],
|
|
191
|
+
}
|
|
192
|
+
// 可选字段:仅在显式给出时带上(保证"不传 = 行为不变")
|
|
193
|
+
if (o.expectedDigest != null) commit.expectedDigest = String(o.expectedDigest)
|
|
194
|
+
if (o.expectedStateVersion != null) commit.expectedStateVersion = String(o.expectedStateVersion)
|
|
195
|
+
return { ok: true, commit }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 冲突描述(卡内 T1-7C 要求:拒绝信息必须带**当前版本 + 冲突目标**,不静默覆盖)。
|
|
200
|
+
* 纯函数:只组装信息,不做 IO。
|
|
201
|
+
*/
|
|
202
|
+
export function commitConflictPre(commit, observed) {
|
|
203
|
+
const c = commit && typeof commit === 'object' ? commit : {}
|
|
204
|
+
const ob = observed && typeof observed === 'object' ? observed : {}
|
|
205
|
+
const expected = c.expectedStateVersion != null ? String(c.expectedStateVersion) : (c.expectedDigest != null ? String(c.expectedDigest) : '(none)')
|
|
206
|
+
const actual = ob.stateVersion != null ? String(ob.stateVersion) : (ob.fileDigest != null ? String(ob.fileDigest) : '(unknown)')
|
|
207
|
+
const target = ob.target != null ? String(ob.target) : (Array.isArray(c.writes) && c.writes[0] && c.writes[0].path ? String(c.writes[0].path) : '(unknown)')
|
|
208
|
+
const who = c.actor && c.actor.sessionId ? String(c.actor.sessionId) : '(unknown)'
|
|
209
|
+
const contSeq = c.actor && c.actor.contSeq != null ? '@' + String(c.actor.contSeq) : ''
|
|
210
|
+
const which = ob.kind === 'state-version' ? 'state-version-mismatch' : 'digest-mismatch'
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
reason: which,
|
|
214
|
+
detail: describeCommitReasonPre(which),
|
|
215
|
+
// 可见冲突三要素:期望值 / 实测值 / 冲突目标(+ 谁)
|
|
216
|
+
expected,
|
|
217
|
+
observed: actual,
|
|
218
|
+
target,
|
|
219
|
+
txId: c.txId != null ? String(c.txId) : '',
|
|
220
|
+
boardId: c.boardId != null ? String(c.boardId) : '',
|
|
221
|
+
actor: who + contSeq,
|
|
222
|
+
text: '[提交被拒] tx=' + (c.txId != null ? String(c.txId) : '(none)')
|
|
223
|
+
+ ' 目标=' + target
|
|
224
|
+
+ ' 期望=' + expected + ' 实测=' + actual
|
|
225
|
+
+ ' 冲突方=' + who + contSeq,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** 成功回执:提交边界返回的可观测凭据(含 miv —— 快照侧据此判"同一份")。 */
|
|
230
|
+
export function commitReceiptPre(commit, result) {
|
|
231
|
+
const c = commit && typeof commit === 'object' ? commit : {}
|
|
232
|
+
const r = result && typeof result === 'object' ? result : {}
|
|
233
|
+
return {
|
|
234
|
+
schemaVersion: STATE_COMMIT_VERSION_PRE,
|
|
235
|
+
ok: true,
|
|
236
|
+
txId: c.txId != null ? String(c.txId) : '',
|
|
237
|
+
boardId: c.boardId != null ? String(c.boardId) : '',
|
|
238
|
+
workspaceKey: c.workspaceKey != null ? String(c.workspaceKey) : '',
|
|
239
|
+
digest: r.digest != null ? String(r.digest) : '',
|
|
240
|
+
stateVersion: r.stateVersion != null ? String(r.stateVersion) : '',
|
|
241
|
+
miv: r.miv != null ? String(r.miv) : '',
|
|
242
|
+
at: Number.isFinite(Number(r.at)) ? Number(r.at) : Date.now(),
|
|
243
|
+
actor: c.actor && c.actor.sessionId ? String(c.actor.sessionId) : '',
|
|
244
|
+
}
|
|
245
|
+
}
|
package/lib/subagent-gc.js
CHANGED
|
@@ -23,6 +23,7 @@ import { readdir, stat, rename, mkdir, access } from 'node:fs/promises'
|
|
|
23
23
|
import { readFile } from 'node:fs/promises'
|
|
24
24
|
import { zstdDecompressSync } from 'node:zlib'
|
|
25
25
|
import path from 'node:path'
|
|
26
|
+
import { retryRename } from './fs-retry.js'
|
|
26
27
|
|
|
27
28
|
/** 本插件 spawn 的子代理 label 前缀(runSubagent 的 label 参数)。 */
|
|
28
29
|
export const PLUGIN_LABEL_PREFIX = 'auto-memory-'
|
|
@@ -258,14 +259,9 @@ export async function recycleSessions(opts = {}) {
|
|
|
258
259
|
if (await exists(destDir)) { out.skipped.push({ sid: c.sid, reason: 'backup-exists' }); continue }
|
|
259
260
|
if (!apply) { out.moved.push({ sid: c.sid, label: c.label, sizeBytes: c.sizeBytes, to: destDir, dryRun: true }); out.bytes += c.sizeBytes; continue }
|
|
260
261
|
await mkdir(path.dirname(destDir), { recursive: true })
|
|
261
|
-
// Windows 下 rename 对被占用目录抛 EPERM(宿主/杀软句柄未释放)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
for (const delay of [0, 250, 800, 2000]) {
|
|
265
|
-
if (delay) await new Promise((r) => setTimeout(r, delay))
|
|
266
|
-
try { await rename(c.dir, destDir); renamed = true; break } catch (e) { lastErr = e }
|
|
267
|
-
}
|
|
268
|
-
if (!renamed) throw lastErr
|
|
262
|
+
// Windows 下 rename 对被占用目录抛 EPERM(宿主/杀软句柄未释放)⇒ 复用统一的有界退避重试
|
|
263
|
+
// (issue #48:退避语义此前在本文件与 index.js 各写一份,口径漂移风险高)。
|
|
264
|
+
await retryRename(c.dir, destDir, { fs: { rename }, delays: [0, 250, 800, 2000] })
|
|
269
265
|
out.moved.push({ sid: c.sid, label: c.label, sizeBytes: c.sizeBytes, to: destDir })
|
|
270
266
|
out.bytes += c.sizeBytes
|
|
271
267
|
if (projcacheRoot) {
|