@a9i5k4/dsh-auto-memory 0.1.29 → 0.1.30
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 +352 -266
- package/README.zh-CN.md +370 -264
- package/cordis.patch.yml +9 -9
- package/lib/activation-host.js +455 -0
- package/lib/activation-inbox-state.js +261 -0
- package/lib/activation-inbox.js +426 -0
- package/lib/client.js +1379 -50
- package/lib/context-bridge.js +619 -0
- package/lib/context-host.js +712 -0
- package/lib/context-sink-python.js +90 -0
- package/lib/episodic-store.js +316 -0
- package/lib/evidence-store.js +272 -0
- package/lib/fact-store.js +418 -0
- package/lib/index-sync.js +160 -0
- package/lib/index.js +2357 -102
- package/lib/intent-clean.js +74 -0
- package/lib/m4-corpus.js +169 -0
- package/lib/m7-index-sync-host.js +194 -0
- package/lib/m7-wire.js +268 -0
- package/lib/memory-anchor.js +451 -0
- package/lib/memory-hub.js +259 -0
- package/lib/memory-index.js +145 -0
- package/lib/memory-writer.js +391 -0
- package/lib/policies/activation_policy_v2.json +88 -0
- package/lib/policies/recall_intent_lr_v1.json +1 -0
- package/lib/procedure-store.js +406 -0
- package/lib/python-sidecar-client.js +326 -0
- package/lib/semantic-decide.js +265 -0
- package/lib/semantic-js.js +381 -0
- package/lib/shadow-host.js +361 -0
- package/lib/shadow-retrieval.js +673 -0
- package/lib/storage-manage.js +203 -0
- package/package.json +2 -2
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M6-2 Per-runtime Activation Inbox(docs/M6-CONTRACT.md §8-§9,§13 M6-2)。
|
|
3
|
+
* 纯内存状态机,零 IO、零依赖(activation-inbox 纯核心);不接 Host、不碰 prompt。
|
|
4
|
+
* - 每个 SessionRuntime 一个 inbox 实例(Host 接线时经严格身份键注册,M6-3);
|
|
5
|
+
* 本模块禁止任何全局 pendingPacket 或「最后活跃代理」式 fallback——状态只存在于实例内。
|
|
6
|
+
* - offer(request):JS 硬校验(schema/身份/重复/contextVersion/memoryIndexVersion/抑制名单)
|
|
7
|
+
* → 构建 ReferenceTailPacketPre → pending(新 cv 替换旧 pending=latest-wins)。
|
|
8
|
+
* - claim({nowStep,cursor}):TTL/cursor/index/cooldown 四重门 → claimed 并返回 packet。
|
|
9
|
+
* - markDelivered(packetId):claimed→delivered,启动 cooldown;只有此时才允许上游建 seen。
|
|
10
|
+
* - dispose():清空全部状态。
|
|
11
|
+
* 全部同输入确定;UTF-8 无 BOM。
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
validateActivationRequestPre, buildReferenceTailPacketPre, isExpired,
|
|
15
|
+
REFERENCE_TAIL_BUDGET_V1, ACTIVATION_POLICY_VERSION, DELIVERY_STATES_V1,
|
|
16
|
+
} from './activation-inbox.js'
|
|
17
|
+
|
|
18
|
+
/** cooldown 步数策略(冻结;投递后冷却)。 */
|
|
19
|
+
export const REFERENCE_TAIL_COOLDOWN_STEPS_V1 = 2
|
|
20
|
+
|
|
21
|
+
const OFFER_REASONS = Object.freeze([
|
|
22
|
+
'pending', 'replaced', 'duplicate-packet', 'duplicate-activation', 'duplicate-observation',
|
|
23
|
+
'stale-context', 'stale-index', 'identity-mismatch', 'suppressed-candidate', 'invalid-request',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
/** 单 runtime 收件箱状态机。 */
|
|
27
|
+
export function createActivationInboxPre(opts = {}) {
|
|
28
|
+
const identity = {
|
|
29
|
+
sessionId: String(opts.sessionId || ''),
|
|
30
|
+
agentId: String(opts.agentId || ''),
|
|
31
|
+
workspaceKey: String(opts.workspaceKey || ''),
|
|
32
|
+
}
|
|
33
|
+
let disposed = false
|
|
34
|
+
let pending = null // ReferenceTailPacketPre(deliveryState=pending)
|
|
35
|
+
let claimedPacketId = null
|
|
36
|
+
const deliveredIds = new Set()
|
|
37
|
+
const deliveredOrder = []
|
|
38
|
+
const seenActivationIds = new Set()
|
|
39
|
+
const seenObservationIds = new Set()
|
|
40
|
+
const suppressedMemoryIds = new Set() // 风险门:correction/revoked 记忆禁止进入 packet(M5 聚合喂入)
|
|
41
|
+
let cooldownUntilStep = 0
|
|
42
|
+
let lastActivationAt = 0
|
|
43
|
+
let lastDeliveredAtStep = 0
|
|
44
|
+
let currentCursor = { contextVersion: Number.isFinite(opts.contextVersion) ? opts.contextVersion : 0, memoryIndexVersion: String(opts.memoryIndexVersion || '') }
|
|
45
|
+
let lastWorkerEpoch = ''
|
|
46
|
+
const stats = { offered: 0, acceptedPending: 0, replaced: 0, duplicates: 0, claimed: 0, delivered: 0, expiredDrops: 0, staleDrops: 0, cooldownRejects: 0, suppressedOffers: 0 }
|
|
47
|
+
|
|
48
|
+
function dropPending(reason) {
|
|
49
|
+
if (!pending) return
|
|
50
|
+
pending.deliveryState = reason === 'expired' ? 'expired' : 'dropped'
|
|
51
|
+
pending = null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function offerActivation(request, ctx = {}) {
|
|
55
|
+
if (disposed) return { ok: false, reason: 'disposed' }
|
|
56
|
+
stats.offered++
|
|
57
|
+
const rv = validateActivationRequestPre(request)
|
|
58
|
+
if (!rv.ok) return { ok: false, reason: 'invalid-request:' + rv.reason }
|
|
59
|
+
const req = rv.request
|
|
60
|
+
// 身份门(§4):sessionId/agentId/workspaceKey 必须与本 runtime 完全一致(cross-workspace 拒绝)
|
|
61
|
+
if (req.sessionId !== identity.sessionId || req.agentId !== identity.agentId || req.workspaceKey !== identity.workspaceKey) {
|
|
62
|
+
return { ok: false, reason: 'identity-mismatch' }
|
|
63
|
+
}
|
|
64
|
+
// 重复激活门(§4):activationId/observationId 任一重复即拒绝
|
|
65
|
+
if (seenActivationIds.has(req.activationId)) { stats.duplicates++; return { ok: false, reason: 'duplicate-activation' } }
|
|
66
|
+
if (seenObservationIds.has(req.observationId)) { stats.duplicates++; return { ok: false, reason: 'duplicate-observation' } }
|
|
67
|
+
// 抑制名单(correction/revoked 等):命中即整单拒绝(precision-first)
|
|
68
|
+
const blocked = req.candidates.filter((c) => suppressedMemoryIds.has(c.memoryId))
|
|
69
|
+
if (blocked.length) { stats.suppressedOffers++; return { ok: false, reason: 'suppressed-candidate', blocked: blocked.map((b) => b.memoryId) } }
|
|
70
|
+
// cursor 门:请求的 contextVersion 落后当前 runtime 游标 → stale(时序门在身份/重复门之后)
|
|
71
|
+
if (Number.isInteger(currentCursor.contextVersion) && req.contextVersion < currentCursor.contextVersion) {
|
|
72
|
+
return { ok: false, reason: 'stale-context' }
|
|
73
|
+
}
|
|
74
|
+
if (ctx.currentMemoryIndexVersion !== undefined && ctx.currentMemoryIndexVersion !== req.memoryIndexVersion) {
|
|
75
|
+
return { ok: false, reason: 'stale-index' }
|
|
76
|
+
}
|
|
77
|
+
const nowStep = Number(ctx.nowStep) || 0
|
|
78
|
+
const built = buildReferenceTailPacketPre({ request: req, nowStep })
|
|
79
|
+
if (!built.ok) return { ok: false, reason: built.reason }
|
|
80
|
+
// latest-wins 替换语义(§8):同 contextVersion 同 packetId=幂等接受;否则替换旧 pending
|
|
81
|
+
if (pending && pending.packetId === built.packet.packetId) {
|
|
82
|
+
stats.duplicates++
|
|
83
|
+
return { ok: true, outcome: 'duplicate-packet', packetId: pending.packetId }
|
|
84
|
+
}
|
|
85
|
+
const replacedCtx = pending ? pending.contextVersion : null
|
|
86
|
+
dropPending('dropped')
|
|
87
|
+
seenActivationIds.add(req.activationId)
|
|
88
|
+
seenObservationIds.add(req.observationId)
|
|
89
|
+
lastWorkerEpoch = String(req.workerEpoch || '')
|
|
90
|
+
lastActivationAt = Date.now()
|
|
91
|
+
pending = built.packet
|
|
92
|
+
if (replacedCtx !== null) stats.replaced++; else stats.acceptedPending++
|
|
93
|
+
return { ok: true, outcome: replacedCtx === null ? 'pending' : 'replaced', packetId: pending.packetId, replacedContextVersion: replacedCtx }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setCursor(cursor) {
|
|
97
|
+
if (disposed) return
|
|
98
|
+
if (cursor && Number.isInteger(cursor.contextVersion)) currentCursor.contextVersion = cursor.contextVersion
|
|
99
|
+
if (cursor && cursor.memoryIndexVersion !== undefined) currentCursor.memoryIndexVersion = String(cursor.memoryIndexVersion)
|
|
100
|
+
// cursor 前进使旧 pending 失效(latest-wins)
|
|
101
|
+
if (pending && pending.contextVersion < currentCursor.contextVersion) dropPending('dropped')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function suppressMemories(ids) {
|
|
105
|
+
for (const id of Array.isArray(ids) ? ids : [ids]) suppressedMemoryIds.add(String(id))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* M10 存储管理级联清理(2026-08-30 P3):一条记忆被删除后,必须让它在途的激活包立即失效——
|
|
110
|
+
* pending 包文本是烘焙好的,不清理就会在下一轮照样渲染出已删除的记忆(HANDOFF §2 P3 缺口②)。
|
|
111
|
+
* 两步:①进抑制名单(后续 offer 整单拒绝,precision-first 语义不变)
|
|
112
|
+
* ②pending 包若含该 memoryId 整包丢弃(claimed 包由 Host 侧 runtimeState 清理)。
|
|
113
|
+
* 不触碰 validator/Reference Tail 固定边界/seen 语义——只影响「还没投递出去的包」。
|
|
114
|
+
*/
|
|
115
|
+
function purgeMemoryId(memoryId) {
|
|
116
|
+
const id = String(memoryId || '')
|
|
117
|
+
if (!id) return { ok: false, reason: 'no-memory-id', droppedPending: false }
|
|
118
|
+
suppressMemories([id])
|
|
119
|
+
let droppedPending = false
|
|
120
|
+
if (pending && Array.isArray(pending.references) && pending.references.some((r) => r && r.memoryId === id)) {
|
|
121
|
+
dropPending('dropped')
|
|
122
|
+
droppedPending = true
|
|
123
|
+
}
|
|
124
|
+
return { ok: true, droppedPending, suppressed: true }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function claim(ctx = {}) {
|
|
128
|
+
if (disposed) return { ok: false, reason: 'disposed' }
|
|
129
|
+
const nowStep = Number(ctx.nowStep) || 0
|
|
130
|
+
if (!pending) return { ok: false, reason: 'none-pending' }
|
|
131
|
+
if (nowStep <= cooldownUntilStep && cooldownUntilStep > 0) { stats.cooldownRejects++; return { ok: false, reason: 'cooldown', cooldownRemainingSteps: cooldownUntilStep - nowStep } }
|
|
132
|
+
if (isExpired(pending, nowStep)) {
|
|
133
|
+
stats.expiredDrops++
|
|
134
|
+
dropPending('expired')
|
|
135
|
+
return { ok: false, reason: 'expired' }
|
|
136
|
+
}
|
|
137
|
+
if (ctx.currentContextVersion !== undefined && pending.contextVersion !== ctx.currentContextVersion) {
|
|
138
|
+
stats.staleDrops++
|
|
139
|
+
dropPending('dropped')
|
|
140
|
+
return { ok: false, reason: 'stale-context' }
|
|
141
|
+
}
|
|
142
|
+
if (ctx.currentMemoryIndexVersion !== undefined && pending.memoryIndexVersion !== ctx.currentMemoryIndexVersion) {
|
|
143
|
+
stats.staleDrops++
|
|
144
|
+
dropPending('dropped')
|
|
145
|
+
return { ok: false, reason: 'stale-index' }
|
|
146
|
+
}
|
|
147
|
+
pending.deliveryState = 'claimed'
|
|
148
|
+
claimedPacketId = pending.packetId
|
|
149
|
+
stats.claimed++
|
|
150
|
+
return { ok: true, packet: pending }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** §8:只有实际进入下一请求 messages 才调用;此时启动 cooldown。返回是否成功。 */
|
|
154
|
+
function markDelivered(packetId, ctx = {}) {
|
|
155
|
+
if (disposed || !claimedPacketId || claimedPacketId !== packetId) return { ok: false, reason: 'not-claimed' }
|
|
156
|
+
const nowStep = Number(ctx.nowStep) || 0
|
|
157
|
+
pending = null
|
|
158
|
+
claimedPacketId = null
|
|
159
|
+
deliveredIds.add(packetId)
|
|
160
|
+
deliveredOrder.push(packetId)
|
|
161
|
+
while (deliveredOrder.length > REFERENCE_TAIL_BUDGET_V1.deliveredIdsCapacity) {
|
|
162
|
+
const oldest = deliveredOrder.shift()
|
|
163
|
+
deliveredIds.delete(oldest)
|
|
164
|
+
}
|
|
165
|
+
cooldownUntilStep = nowStep + REFERENCE_TAIL_COOLDOWN_STEPS_V1
|
|
166
|
+
lastDeliveredAtStep = nowStep
|
|
167
|
+
stats.delivered++
|
|
168
|
+
return { ok: true, packetId, cooldownUntilStep }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** claimed→pending 回滚(pump 目标 runtime 缺失等场景);不影响 cooldown。 */
|
|
172
|
+
function rollbackClaim() {
|
|
173
|
+
if (!claimedPacketId || !pending) return { ok: false, reason: 'not-claimed' }
|
|
174
|
+
pending.deliveryState = 'pending'
|
|
175
|
+
claimedPacketId = null
|
|
176
|
+
stats.claimed = Math.max(0, stats.claimed - 1)
|
|
177
|
+
return { ok: true }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function debugView() {
|
|
181
|
+
return {
|
|
182
|
+
identityKeysPresent: Boolean(identity.sessionId && identity.workspaceKey),
|
|
183
|
+
pending: pending ? { packetId: pending.packetId, contextVersion: pending.contextVersion, references: pending.references.length, expiresAtStep: pending.expiresAtStep } : null,
|
|
184
|
+
claimedPacketId: claimedPacketId,
|
|
185
|
+
deliveredCount: deliveredIds.size,
|
|
186
|
+
cooldownUntilStep,
|
|
187
|
+
lastActivationAt,
|
|
188
|
+
lastDeliveredAtStep,
|
|
189
|
+
lastWorkerEpoch: lastWorkerEpoch.slice(0, 16),
|
|
190
|
+
stats: { ...stats },
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function dispose(reason) {
|
|
195
|
+
void reason
|
|
196
|
+
disposed = true
|
|
197
|
+
dropPending('dropped')
|
|
198
|
+
claimedPacketId = null
|
|
199
|
+
deliveredIds.clear()
|
|
200
|
+
deliveredOrder.length = 0
|
|
201
|
+
seenActivationIds.clear()
|
|
202
|
+
seenObservationIds.clear()
|
|
203
|
+
suppressedMemoryIds.clear()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
identity,
|
|
208
|
+
offerActivation,
|
|
209
|
+
setCursor,
|
|
210
|
+
suppressMemories,
|
|
211
|
+
purgeMemoryId,
|
|
212
|
+
claim,
|
|
213
|
+
rollbackClaim,
|
|
214
|
+
markDelivered,
|
|
215
|
+
debugView,
|
|
216
|
+
dispose,
|
|
217
|
+
get pendingPacket() { return pending },
|
|
218
|
+
_statsForTest: stats,
|
|
219
|
+
_suppressedForTest: suppressedMemoryIds,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 严格身份键注册表(M6-3 Host 接线用):无可靠身份不得创建 inbox(无 default 兜底桶)。
|
|
225
|
+
* 键='session:'+sessionId;同一 sessionId 复用同一 inbox。
|
|
226
|
+
*/
|
|
227
|
+
export class ActivationInboxRegistry {
|
|
228
|
+
constructor() { this._byKey = new Map() }
|
|
229
|
+
static keyOf(sessionId, workspaceKey) {
|
|
230
|
+
if (!sessionId || !workspaceKey) return null
|
|
231
|
+
return 'session:' + String(sessionId) + '|ws:' + String(workspaceKey)
|
|
232
|
+
}
|
|
233
|
+
forRuntime(sessionId, workspaceKey, factoryOpts = {}) {
|
|
234
|
+
const key = ActivationInboxRegistry.keyOf(sessionId, workspaceKey)
|
|
235
|
+
if (!key) return null
|
|
236
|
+
let box = this._byKey.get(key)
|
|
237
|
+
if (!box) {
|
|
238
|
+
box = createActivationInboxPre({ ...factoryOpts, sessionId, workspaceKey })
|
|
239
|
+
this._byKey.set(key, box)
|
|
240
|
+
} else if (factoryOpts.agentId && !box.identity.agentId) {
|
|
241
|
+
box.identity.agentId = String(factoryOpts.agentId)
|
|
242
|
+
}
|
|
243
|
+
return box
|
|
244
|
+
}
|
|
245
|
+
get(sessionId, workspaceKey) {
|
|
246
|
+
const key = ActivationInboxRegistry.keyOf(sessionId, workspaceKey)
|
|
247
|
+
return key ? this._byKey.get(key) || null : null
|
|
248
|
+
}
|
|
249
|
+
/** M10 级联清理用:遍历全部 inbox(只读用途,返回副本数组,不泄漏内部 Map)。 */
|
|
250
|
+
boxes() { return [...this._byKey.values()] }
|
|
251
|
+
disposeSession(sessionId, workspaceKey) {
|
|
252
|
+
const key = ActivationInboxRegistry.keyOf(sessionId, workspaceKey)
|
|
253
|
+
if (key && this._byKey.has(key)) { this._byKey.get(key).dispose('session-disposed'); this._byKey.delete(key); return true }
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
disposeAll(reason) {
|
|
257
|
+
for (const box of this._byKey.values()) box.dispose(reason)
|
|
258
|
+
this._byKey.clear()
|
|
259
|
+
}
|
|
260
|
+
get size() { return this._byKey.size }
|
|
261
|
+
}
|