AI agent 的 memory kernel 需要身份、证据与可删除性
halofyai/halofy 源码级解析:服务端身份、统一 ACL、写入治理、ScopedView 检索驱动、ContextAllocator、FaultHandler、append-only 审计链与签名删除证书。
观察结论
它不是又一个 prompt memory 插件,而是把身份、ACL、写入验证、检索驱动、审计链和合规删除放进同一个内核边界。
它把 agent 记忆做成治理内核
源码判断
身份服务端化
namespace、actor、role 从 key 解析,不靠调用方自报。
检索只读化
driver 通过 ScopedView 读取候选,不能写入或绕过 ACL。
删除可证明
合规删除硬删数据,同时留下 tombstone、证书和审计链。
src/auth/index.ts:39 API key 表把 key_hash、role、namespace、expires_at 放在同一权威表里,说明身份不是客户端传参,而是服务端从凭证解析出的上下文。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/auth/index.ts:73 hashKey 只保存 SHA-256 摘要,明文 key 只在签发响应里出现一次,这让泄漏数据库时无法直接重放 agent 凭证。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/auth/index.ts:99 isOversightRole 把 owner、admin、auditor 抽成审计/治理角色集合,后续权限检查可以复用同一组语义。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/auth/index.ts:146 rowAgentCanWrite 把行级写权限压成纯函数,写入面能用同一判断处理 owner/admin/member/agent 的差异。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/auth/index.ts:224 auditDenied 在拒绝路径上也追加审计事件,系统不是只记录成功调用,而是把越权尝试纳入可追溯轨迹。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/auth/index.ts:286 issueKey 生成随机 key 后只插入 hash 与元数据,签发环节完成后系统内没有二次读取明文 key 的接口。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
身份与权限不是 HTTP 参数
源码判断
src/auth/index.ts:331 resolveKey 从 hash 查 principal,并在过期、禁用、不可用 principal 时写 denied audit,身份层承担了第一道治理责任。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/syscalls/index.ts:16 syscall 顶部注释列出 auditor 可读不可写的角色语义,说明 HTTP/MCP 入口共享的是同一套 operation gate。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/syscalls/index.ts:124 SyscallDeps 把 audit、rateLimiter、budget、credential、policy、runtimes、queue 都注入进来,内核调用不是裸函数直达存储。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/syscalls/index.ts:250 L4DispatchOptions 特别声明 source token 不能被日志、审计、持久化或回显,这把短期 OAuth 能力和长记忆数据切开。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/syscalls/index.ts:334 planViaDriver 总是用同一个 ScopedView 构造检索计划,driver 没机会自己绕过 ACL 去碰底层 index。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/syscalls/index.ts:380 MemReadResult 用 denied=false 避免把跨 namespace 的存在性泄露给调用者,这是一处明确的 oracle 防护。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
331 async resolve(plaintext: string): Promise<Identity | null> {
332 const hash = hashKey(plaintext);
333 const res = await this.db.query(
334 `SELECT ${KEY_COLUMNS}, caller_class FROM api_keys WHERE key_hash = $1`,
335 [hash],
336 );
337 const row = res.rows[0] as ApiKeyRow | undefined;
338 // Verification-only principals (principal_purpose = 'channel_verification')
339 // exist solely for in-process deterministic channel probes and must never
340 // authenticate over the public API.
341 if (!row || row.status !== "active" ||
342 (row.principal_purpose !== undefined && row.principal_purpose !== "general")) {
343 await this.auditDenied(
344 "unknown",
345 row?.namespace ?? "unknown",
346 row
347 ? `resolve denied: key ${row.id} is unavailable for public authentication`
348 : "resolve denied: unknown key",
349 );
350 return null;
351 }
352 // D51: an expired key is indistinguishable from a revoked one at the
353 // transport (401). It is NOT flipped to status 'revoked' here — resolve is
354 // on the hot path for every request, and a write on each rejected call is
355 // both a needless round trip and a lie about who revoked it. The audit row
356 // names expiry explicitly so the two causes stay separable in the trail.
357 if (isExpired(row, Date.now())) {
358 await this.auditDenied(
359 row.actor,
360 row.namespace,
361 `resolve denied: key ${row.id} expired at ${String(row.expires_at)}`,
362 );
363 return null;
364 }
365 await this.db.query(`UPDATE api_keys SET last_used = $1 WHERE id = $2`, [
366 new Date().toISOString(),
367 row.id,
368 ]);
369 const entitlements = rowToRecord(row);
370 const sourcePrincipals = entitlements.sourcePrincipals ?? [];
371 const sourceGroups = entitlements.sourceGroups ?? [];
372 const agentAccessLevel = rowAgentLevel(row.role, row.agent_access_level);
373 return {
374 keyId: row.id,
375 namespace: row.namespace,
376 actor: row.actor,
377 role: row.role as Role,
378 ...(agentAccessLevel ? { agentAccessLevel } : {}),
379 ...(row.role === "agent"
380 ? { agentCanWrite: rowAgentCanWrite(row.role, row.agent_can_write) === true }写入路径先治理,再落库
源码判断
src/write/index.ts:1 写入模块的文件头把 extraction、verification、entity resolution、dedupe、insert 排成管线,定位很像数据库事务前的治理编排。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/write/index.ts:61 MemoryIndexLike 暴露 insert、vectorSearch、bumpAccess、closeValidity,说明写入路径可关闭旧事实而不是覆盖原行。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/write/index.ts:133 SUPERSEDE_MIN_COSINE 给语义冲突设置阈值,避免只靠 exact string dedupe 判断一条事实是否应该 supersede。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/write/index.ts:177 governedClassification 把 verbatim、fact、profile 等 hint 映射到允许的分类,写入前先对目的地做语义约束。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/write/index.ts:321 write() 首先分类、再提取、再分批验证,LLM 输出只是候选事实,不能直接落库成权威记忆。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/write/index.ts:354 每个候选事实独立经过验证、实体解析、目的地检查和 embedding,单条失败不会把整个写入批次粗暴吞掉。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/write/index.ts:421 processCandidate 会拒绝低信息事实、剥离实体注解、检查 destination,再进入向量和去重阶段,落库前有多重过滤。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/driver/view.ts:1 ScopedView 的文件头明确它是 driver 世界的唯一构造器,检索插件不能导入存储层另起通道。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
321 async write(
322 req: WriteRequest,
323 policy: WritePolicy,
324 /** Agent identity for audit attribution (D4); defaults to the actor. */
325 agentId: string = req.actor,
326 hooks?: WritePathHooks,
327 ): Promise<WriteResult> {
328 const now = new Date().toISOString();
329 const inputClassification = governedClassification(req.content, req, policy);
330 if (!req.hints?.verbatim) {
331 assertDestinationAllowed(inputClassification, "llm-extraction", {
332 region: policy.dataRegion,
333 });
334 }
335 const result: WriteResult = {
336 stored: [],
337 rejected: [],
338 superseded: [],
339 deduplicated: 0,
340 };
341
342 // a. Extraction.
343 await hooks?.onStage?.("extraction");
344 const candidates = await this.extract(req);
345
346 // b (hoisted). One verifier call covers every extracted candidate —
347 // per-fact calls re-sent the whole transcript per candidate, which made
348 // verification the write path's dominant model spend on multi-fact
349 // documents. Runs BEFORE any candidate commits, so an LLM outage here
350 // propagates with zero progress and the caller's verbatim fallback stays
351 // safe to re-run (nothing committed to duplicate).
352 const verdicts = await this.verifyCandidates(req, policy, candidates, now);
353
354 for (const [index, cand] of candidates.entries()) {
355 // M6 (D14): the caller's hard cap on STORED facts (the L4 write-back
356 // passes its remaining per-fault cache budget). Checked before each
357 // candidate — extraction fan-out can never land more rows than the cap,
358 // and the overflow is reported, not silently dropped.
359 if (req.maxFacts !== undefined && result.stored.length >= req.maxFacts) {
360 result.rejected.push({ candidate: cand.content, reason: "max-facts-cap" });
361 continue;
362 }
363 try {
364 // Entity-registry drift between prepare() and the commit transaction
365 // retries the whole candidate: the transaction rolled back, so a
366 // fresh preparation inside processCandidate re-reads current state
367 // and (normally) resolves the colliding mention as an exact match.
368 // Any other failure - and a conflict that persists past the bound -
369 // propagates to the handler below unchanged.
370 for (let entityAttempt = 1; ; entityAttempt++) {
371 try {
372 await this.processCandidate(req, policy, agentId, now, cand, verdicts[index] ?? false, result, hooks);
373 break;
374 } catch (err) {
375 if (
376 err instanceof EntityResolutionConflictError &&
377 entityAttempt < MAX_ENTITY_RESOLUTION_ATTEMPTS
378 ) {
379 continue;
380 }
381 throw err;
382 }
383 }
384 } catch (err) {
385 // An LLM circuit that opens MID-write — after earlier candidates
386 // were already committed — must never surface as a whole-write
387 // failure: the caller's verbatim fallback would re-run the ENTIRE
388 // write, duplicating the committed facts (plus their supersede/
389 // audit/cost rows) and dropping their ids from the returned result.
390 // Record the outage per remaining candidate and keep the partial
391 // WriteResult accurate instead. With ZERO progress the error still
392 // propagates: the verbatim fallback is safe then (nothing committed
393 // to duplicate).
394 const progress =
395 result.stored.length +
396 result.superseded.length +
397 result.rejected.length +
398 (result.disputed?.length ?? 0) +
399 result.deduplicated >
400 0;
401 if (err instanceof LlmUnavailableError && progress) {
402 result.rejected.push({ candidate: cand.content, reason: "llm-unavailable" });
403 await this.audit(
404 req,
405 agentId,
406 now,
407 "reject",
408 null,
409 "error",
410 "llm-unavailable: circuit opened mid-write; earlier candidates already committed",
411 );
412 continue;
413 }
414 throw err;
415 }
416 }
417
418 return result; 421 /** Steps b–f for one extracted candidate (mutates `result` in place). */
422 private async processCandidate(
423 req: WriteRequest,
424 policy: WritePolicy,
425 agentId: string,
426 now: string,
427 cand: Candidate,
428 /** This candidate's verdict from the hoisted batch verification. */
429 verdictOk: boolean,
430 result: WriteResult,
431 hooks?: WritePathHooks,
432 ): Promise<void> {
433 if (req.provenance !== undefined && !req.hints?.verbatim && isLowInformationFact(cand.content)) {
434 result.rejected.push({ candidate: cand.content, reason: "low-information-fact" });
435 await this.audit(req, agentId, now, "reject", null, "denied", "low-information-fact");
436 return;
437 }
438
439 // b. Verifier verdict (computed once for the whole write in
440 // verifyCandidates) — SKIPPED for verbatim requests: verbatim is the
441 // no-LLM lane (budget brownout / LLM-outage fallback), so it must not
442 // call the LLM again. The candidate is the caller's own content stored
443 // as-is; there is nothing extracted to verify against the transcript.
444 if (policy.verifierEnabled && !req.hints?.verbatim && !verdictOk) {
445 result.rejected.push({
446 candidate: cand.content,
447 reason: "failed-verification",
448 });
449 await this.audit(req, agentId, now, "reject", null, "denied", "failed-verification");
450 return;
451 }
452
453 // c. Entity resolution — canonical names appended as an annotation;
454 // the fact text itself stays verbatim.
455 //
456 // `[entities: …]` is a RESERVED write-path marker: the read side
457 // (DbFragmentationReader) parses canonical entities out of a trailing
458 // annotation. Source-controlled content (a raw doc or verbatim import) that
459 // legitimately ends in that bracket would otherwise inject FORGED canonical
460 // entities the write path never resolved. Strip any trailing occurrence off
461 // the base text first, so the only `[entities: …]` the read side can ever
462 // see is the one the write path itself appends below after real resolution.
463 const base = stripEntitiesAnnotation(cand.content);
464 // A governed localOnly ingestion write (DLP brownout: the document's
465 // classification denied llm-extraction / embedding-provider) must NOT
466 // resolve mentions through the entity registry — EntityRegistry.prepare()
467 // embeds with the external embedder and can call the external LLM
468 // tiebreak, exactly the egress localOnly exists to prevent. But landing
469 // with NO entity metadata makes the fact invisible to entity-grouped
470 // contradiction detection (organization conflict scans report
471 // entity_metadata_unavailable and skip it), so annotate the deterministic
472 // proper-noun mentions directly: zero egress, no registry mutation, same
473 // marker shape the read side parses. Non-ingestion localOnly writes stay
474 // exactly as-is, mirroring the extract() lane's provenance gate.
475 const deterministicMentions =
476 (req.hints?.localOnly || req.hints?.deterministicEntities) &&
477 req.provenance !== undefined
478 ? capitalizedWords(base)
479 : [];
480 // Entity resolution itself can call the embedding provider. Report the
481 // boundary before that work begins, not only before the final fact vector,
482 // so a provider failure is attributed to the real stage.
483 await hooks?.onStage?.("embedding");
484 const preparedEntities: PreparedEntityResolution[] = [];
485 if (this.deps.transact && deterministicMentions.length === 0) {
486 for (const mention of cand.entities) {
487 preparedEntities.push(await this.entities.prepare(req.namespace, mention));
488 }
489 }
490 // Production writes resolve entities only after entering deps.transact so
491 // entity rows, alias changes, erasure fences, the memory row, and its D4
492 // audit evidence share one commit boundary. The non-transactional lane is
493 // retained for small structural unit-test implementations.
494 let content = deterministicMentions.length > 0
495 ? `${base} [entities: ${deterministicMentions.join(", ")}]`
496 : this.deps.transact
497 ? base
498 : await this.annotateEntities(base, req.namespace, cand.entities, this.entities);
499
500 // Embed the fact TEXT (un-annotated base), not the `[entities: …]` marker:
501 // the annotation is derived read-side metadata, so dedupe / supersedence /
502 // search similarity must depend only on the fact itself. This also keeps
503 // the verbatim lane's newly-added entity annotation from perturbing
504 // supersede matching between two near-identical facts.
505 const classification = governedClassification(content, req, policy);
506 if (!req.hints?.localOnly) {
507 assertDestinationAllowed(classification, "embedding-provider", {
508 region: policy.dataRegion,
509 });
510 }
511 assertDestinationAllowed(classification, "canonical-l2", {
512 region: policy.dataRegion,
513 });
514 await hooks?.onStage?.("embedding");
515 const [embedding] = await (
516 req.hints?.localOnly ? this.localEmbedder : this.deps.embedder
517 ).embed([base]);
518 if (!embedding) return; // defensive; embedders return 1:1
519 const sourceAccess = req.provenance?.sourceAccess ?? {
520 principals: [],检索驱动只能站在 ScopedView 后面
源码判断
src/driver/view.ts:140 nameMatchers 用 whole-word 正则避免 Ann 命中 planning 这类误召回,图检索路径对实体边界有显式防护。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/driver/view.ts:181 buildScopedView 的 candidates 调用带 validOnly、access、sourceProjection,候选集从创建时就被 ACL 与 source scope 收窄。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/driver/view.ts:220 vectorSearch 同样传入 validOnly、access、sourceIds,向量检索没有被例外化成更宽的读取面。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/driver/view.ts:242 concepts 在 sourceProjection 存在时 fail closed,这让冷层概念检索不会把指定来源之外的信息混入计划。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/allocator/index.ts:7 ContextAllocator 把 policy-directive 排在 lane 0,并从预留 token slice 装入,治理指令不会被普通 recall 挤出上下文。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
181export function buildScopedView<TDb>(
182 deps: ScopedViewDeps<TDb>,
183 namespace: Namespace,
184 access?: AccessSubject,
185 sourceProjection?: RetrievalSourceProjection,
186): ScopedView {
187 return {
188 namespace,
189
190 async candidates(limit = DEFAULT_CANDIDATE_LIMIT) {
191 const rows = await deps.index.listCandidates(namespace, {
192 validOnly: true,
193 limit,
194 access,
195 sourceIds: sourceProjection?.sourceIds,
196 });
197 return rows.map((r) => ({
198 ref: r.id,
199 content: retrievalContent(r, sourceProjection),
200 }));
201 },
202
203 ...(deps.index.listLexicalCandidates
204 ? {
205 async lexicalCandidates(query: string, limit: number) {
206 const rows = await deps.index.listLexicalCandidates!(namespace, query, {
207 validOnly: true,
208 limit,
209 access,
210 sourceIds: sourceProjection?.sourceIds,
211 });
212 return rows.map((r) => ({
213 ref: r.id,
214 content: retrievalContent(r, sourceProjection),
215 }));
216 },
217 }
218 : {}),
219
220 async vectorSearch(embedding, k) {
221 const hits = await deps.index.vectorSearch(
222 namespace,
223 embedding,
224 k,
225 true,
226 false,
227 false,
228 access,
229 sourceProjection?.sourceIds,
230 );
231 return hits.map((h) => ({
232 ref: h.obj.id,
233 content: retrievalContent(h.obj, sourceProjection),
234 score: h.similarity,
235 }));
236 },
237
238 async embed(texts) {
239 return deps.embedder.embed(texts);
240 },
241
242 async concepts(query, k) {
243 // L3 concepts do not retain per-source provenance. A source-scoped view
244 // therefore fails closed instead of allowing a future driver to widen
245 // the result set through the cold tier.
246 if (sourceProjection?.sourceIds !== undefined) return [];
247 return deps.cold.searchConcepts(deps.db, deps.embedder, namespace, query, k);
248 },
249
250 async entityFacts(mention, hops) {
251 const needle = mention.trim().toLowerCase();
252 if (needle.length === 0) return [];
253
254 const entities = await deps.entities.listEntities(namespace);
255 const seeds = entities.filter((e) =>
256 lowerNames(e).some((n) => n === needle),
257 );
258 if (seeds.length === 0) return [];ContextAllocator 返回 blocks,不拼 prompt
源码判断
src/allocator/index.ts:125 tryAdd 用 ref 去重并按 tokenBudget 跳过超额 block,工作集是可解释的 blocks 而不是拼接 prompt。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/allocator/index.ts:181 legacy pinned concepts 仍是只读兼容路径,策略顺序保留但不会把旧 pin 自动升级成新 governed pin。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/allocator/index.ts:232 predicted-relevant lane 只有 taskHint 存在时才启动,系统不会无条件把相似记忆塞进每一次调用。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/allocator/index.ts:285 assemble audit detail 写入 blocks、tokens、budget、rejectedPins 和 policy digest,召回上下文也有审计证据。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/fault/index.ts:49 L2_SCORE_FLOOR 和 L3_SCORE_FLOOR 把故障补召回变成阈值判定,而不是只要有候选就硬塞回上下文。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
110 async assemble(args: AssembleArgs): Promise<WorkingSet> {
111 const t0 = performance.now();
112 const { namespace, policy, taskHint } = args;
113 // Defensive: some callers (syscall layer) route audit attribution
114 // themselves and may omit actor/agentId at runtime.
115 const actor = args.actor ?? "unknown";
116 const agentId = args.agentId ?? "unknown";
117 const budget = policy.tokenBudget;
118
119 let totalTokens = 0;
120 let rejectedPins = 0;
121 let brownoutDetail = "";
122 const selected: ContextBlock[] = [];
123 const usedRefs = new Set<string>();
124
125 const tryAdd = (block: ContextBlock): void => {
126 if (usedRefs.has(block.ref)) return;
127 if (totalTokens + block.tokenEstimate > budget) return; // never split
128 selected.push(block);
129 usedRefs.add(block.ref);
130 totalTokens += block.tokenEstimate;
131 };
132
133 // -- lane 0: governed policy directives ---------------------------------
134 // Guardrails are packed first out of a reserved slice of the budget, so
135 // no amount of recall can push a policy out of the working set. The
136 // reserve is a CEILING, not an allotment: unused policy tokens stay
137 // available to the ordinary lanes below.
138 let policySet: PolicyDirectiveSet | null = null;
139 if (this.deps.policyDirectives) {
140 try {
141 const loaded = await this.deps.policyDirectives.load(
142 namespace,
143 policyTokenReserve(budget),
144 );
145 if (loaded.documents.length > 0) {
146 policySet = loaded;
147 for (const block of loaded.blocks) tryAdd(block);
148 }
149 } catch {
150 // Brownout (kernel rule 5): an unreadable guardrail lane degrades the
151 // working set, it never fails the agent's call. The audit detail below
152 // records that policy was unavailable for this assembly.
153 brownoutDetail += " brownout=policy_unavailable";
154 }
155 }
156
157 // -- lane 1a: governed stable pins (durable creation order) --------------
158 if (this.deps.pins) {
159 try {
160 const governed = await this.deps.pins.resolve(namespace, args.access);
161 rejectedPins += governed.rejected;
162 for (let i = 0; i < governed.pins.length; i++) {
163 const pin = governed.pins[i];
164 tryAdd({
165 kind: pin.kind === "memory" ? "pinned-memory" : "pinned-concept",
166 ref: pin.ref,
167 content: pin.content,
168 tokenEstimate: estimateTokens(pin.content),
169 orderKey: `0-pin-0-governed-${String(i).padStart(6, "0")}-${pin.pinId}`,
170 });
171 }
172 } catch {
173 // Brownout: pin metadata or its backing store may be unavailable. The
174 // compatibility and ordinary lanes still provide a working set.
175 rejectedPins++;
176 }
177 } 285 const assembledAt = new Date().toISOString();
286 const event = {
287 ts: assembledAt,
288 actor,
289 agentId,
290 op: "assemble",
291 memoryId: null,
292 namespace,
293 purpose: taskHint ?? null,
294 result: "ok",
295 detail: `blocks=${selected.length} tokens=${totalTokens} budget=${budget} rejectedPins=${rejectedPins}${policyDetail}${brownoutDetail}`,
296 } as const;
297 // D36: with the kernel's transact/usage wiring the audit row and its
298 // companion usage row commit in one transaction; module-level tests keep
299 // the standalone append.
300 const writeAudit = async (audit: AuditPort, tx?: unknown): Promise<void> => {
301 const auditSeq = await audit.append(event);
302 if (this.deps.usage && tx !== undefined && typeof auditSeq === "number" && args.usage) {
303 await this.deps.usage(tx, {
304 auditSeq,
305 traceId: args.usage.traceId,
306 ts: assembledAt,
307 namespace,
308 actor,
309 principal: args.usage.principal,
310 callerClass: args.usage.callerClass,
311 op: "assemble",
312 tool: args.usage.tool,
313 result: "ok",
314 latencyMs: performance.now() - t0,
315 tokens: totalTokens,
316 returned: selected.length,
317 driver: null,
318 sourceTier: null,
319 sourceIds: null,
320 topScore: null,
321 });
322 }
323 };
324 if (this.deps.transact) {
325 await this.deps.transact(writeAudit);
326 } else {
327 await writeAudit(this.deps.audit);
328 }
329
330 if (deliveredPolicy && this.deps.policyDirectives?.onDelivered) {
331 try {
332 await this.deps.policyDirectives.onDelivered(deliveredPolicy, args);
333 } catch {
334 // The ledger is evidence, not a gate: a failed compliance write never
335 // costs the agent its working set.
336 }
337 }
338
339 return {
340 namespace,
341 blocks: selected,
342 totalTokens,
343 budget,
344 assembledAt,
345 ...(deliveredPolicy
346 ? {
347 policy: {
348 digest: deliveredPolicy.digest,
349 documents: deliveredPolicy.documents,
350 tokens: deliveredPolicy.totalTokens,
351 required: deliveredPolicy.required,
352 truncated: deliveredPolicy.truncated,
353 },
354 }
355 : {}),
356 };FaultHandler 的价值是可降级而不是更聪明
源码判断
src/fault/index.ts:182 FaultHandler 优先走 driver-planned L2,但随后仍计算绝对分数,driver 的归一化分数不能单独决定是否命中。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/fault/index.ts:217 embedding 出错时 brownout 到 BM25,系统记录 degradedReason,但不把一次向量服务失败变成 agent 调用失败。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/fault/index.ts:265 L2 不够强时才进入 L3 cold concept search,冷层被设计成有门槛的补救路径而不是默认主路。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/fault/index.ts:317 故障路径会 bumpAccess 或 bumpFault 并同事务 append audit,召回压力会回流成可观测信号。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
160 async fault(args: FaultArgs): Promise<FaultResult> {
161 const t0 = performance.now();
162 const { namespace, query } = args;
163 // Defensive: some callers (syscall layer) route audit attribution
164 // themselves and may omit actor/agentId at runtime.
165 const actor = args.actor ?? "unknown";
166 const agentId = args.agentId ?? "unknown";
167 const k = args.k ?? DEFAULT_K;
168 const tokenBudget = args.tokenBudget ?? DEFAULT_TOKEN_BUDGET;
169
170 // -- tier L2 --------------------------------------------------------------
171 // M2: when the kernel wired a driver-routed planner, the L2 candidates
172 // come from the policy-selected retriever cartridge; otherwise the
173 // legacy hybrid search runs. Either way the ACCEPTANCE decision uses the
174 // absolute hybrid score against L2_SCORE_FLOOR (see FaultHandlerDeps).
175 let l2Refs: ScoredRef[];
176 let acceptL2: boolean;
177 let bumpTargetId: string | null;
178 let driverDetail = "";
179 let driverName: string | null = null;
180 let degradedReason: FaultResult["degradedReason"];
181
182 try {
183 if (this.deps.l2Planner) {
184 const outcome = await this.deps.l2Planner(query, k, tokenBudget, args.access);
185 driverDetail =
186 ` driver=${outcome.driver}` +
187 (outcome.fallback ? ` driver_fallback=${outcome.fallback}` : "");
188 driverName = outcome.driver;
189 l2Refs = outcome.refs;
190 const abs = await this.absoluteScores(namespace, query, outcome.refs, args.access);
191 acceptL2 = abs.some((s) => s >= L2_SCORE_FLOOR);
192 bumpTargetId = outcome.refs[0]?.ref ?? null;
193 } else {
194 const l2Hits: HybridHit[] = await hybridSearchL2({
195 index: this.deps.index,
196 embedder: this.deps.embedder,
197 namespace,
198 query,
199 k: Math.max(k * 3, 12),
200 access: args.access,
201 });
202 const bestL2 = l2Hits.length > 0 ? l2Hits[0] : null;
203 acceptL2 = bestL2 !== null && bestL2.score >= L2_SCORE_FLOOR;
204 bumpTargetId = bestL2?.row.id ?? null;
205 l2Refs = l2Hits
206 .filter((h) => h.score > 0)
207 .slice(0, k)
208 .map((h) => ({
209 id: h.row.id,
210 tier: "L2" as const,
211 ref: h.row.id,
212 score: h.score,
213 content: h.row.content,
214 tokenEstimate: estimateTokens(h.row.content),
215 }));
216 }
217 } catch {
218 // Immediate brownout, including failures before the circuit breaker has
219 // opened: retain the governed lexical lane and expose the degradation.
220 const rows = await this.deps.index.listCandidates(namespace, {
221 validOnly: true,
222 limit: 200,
223 access: args.access,
224 });
225 const scores = bm25Scores(
226 query,
227 rows.map((row) => ({ ref: row.id, content: row.content })),
228 );
229 l2Refs = rows
230 .map((row) => ({
231 id: row.id,
232 tier: "L2" as const,
233 ref: row.id,
234 // In a lexical-only brownout the BM25 lane owns the full confidence
235 // scale; retaining the hybrid 0.4 weight would make its maximum too
236 // small to clear the calibrated acceptance floor reliably.
237 score: saturate(scores.get(row.id) ?? 0),
238 content: row.content,
239 tokenEstimate: estimateTokens(row.content),
240 }))
241 .filter((ref) => ref.score > 0)
242 .sort((a, b) => b.score - a.score || a.ref.localeCompare(b.ref))
243 .slice(0, k);
244 acceptL2 = (l2Refs[0]?.score ?? 0) >= L2_SCORE_FLOOR;
245 bumpTargetId = l2Refs[0]?.ref ?? null;
246 driverDetail = " driver=bm25-degraded brownout=embedding_unavailable";
247 driverName = "bm25-degraded";
248 degradedReason = "embedding_unavailable";
249 } 303 const event = {
304 ts: new Date().toISOString(),
305 actor,
306 agentId,
307 op: "fault",
308 memoryId: injected.length > 0 ? injected[0].id : null,
309 namespace,
310 purpose: args.auditPurpose ?? query,
311 result,
312 detail:
313 `source=${source ?? "none"} injected=${injected.length} tokens=${injectedTokens} latencyMs=${latencyMs}${driverDetail}` +
314 (degradedReason ? ` brownout=${degradedReason}` : ""),
315 } as const;
316
317 const mutateAndAudit = async (index: L2IndexPort, audit: AuditPort, tx?: TDb): Promise<void> => {
318 if (accessIds.length > 0) await index.bumpAccess(accessIds);
319 if (faultId !== null) await index.bumpFault(faultId);
320 const auditSeq = await audit.append(event);
321 // D36: the companion usage row rides the audit transaction (the hook
322 // wraps its writes in a SAVEPOINT brownout). The audit detail string
323 // above stays byte-identical.
324 if (this.deps.usage && tx !== undefined && typeof auditSeq === "number" && args.usage) {
325 await this.deps.usage(tx, {
326 auditSeq,
327 traceId: args.usage.traceId,
328 ts: event.ts,
329 namespace,
330 actor,
331 principal: args.usage.principal,
332 callerClass: args.usage.callerClass,
333 op: "fault",
334 tool: args.usage.tool,
335 result,
336 latencyMs,
337 tokens: injectedTokens,
338 returned: injected.length,
339 driver: driverName,
340 sourceTier: source === "L2" ? "warm" : source === "L3" ? "cold" : null,
341 sourceIds: null,
342 topScore: injected[0]?.score ?? null,
343 });
344 }
345 };
346 if (this.deps.transact) {
347 await this.deps.transact(mutateAndAudit);
348 } else {
349 await mutateAndAudit(this.deps.index, this.deps.audit);
350 }
351
352 return {
353 hit: result === "ok",
354 source,
355 injected,
356 latencyMs,
357 ...(degradedReason ? { degradedReason } : {}),
358 };审计链是 append-only 的系统记录
源码判断
src/audit/index.ts:4 AuditLog 的模块注释直接声明每个 syscall 都写行,包括 denied/miss/error,这决定了治理视角下的证据完整性。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/audit/index.ts:92 auditDigest 把 prevHash、ts、actor、op、namespace、result、detail 纳入 canonical JSON,再用 SHA-256 串链。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/audit/index.ts:109 appendAuditEventTx 先 FOR UPDATE 锁 head_hash,再插入事件与新 hash,链头推进和事件提交在一个事务里完成。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/audit/index.ts:180 verifyChain 重算每条 hashed audit row 并检查 audit_chain_state,能发现篡改、删除和链头不一致。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/audit/index.ts:225 search 的注释强调 namespace IN 精确枚举,不用 LIKE 通配,审计导出和控制台筛选都沿用同一 ACL 边界。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
92/** Low-level append primitive used by transactional and recovery paths. */
93function auditDigest(event: AuditEvent, prevHash: string | null): string {
94 const canonical = JSON.stringify({
95 prevHash,
96 ts: event.ts,
97 actor: event.actor,
98 agentId: event.agentId,
99 op: event.op,
100 memoryId: event.memoryId,
101 namespace: event.namespace,
102 purpose: event.purpose,
103 result: event.result,
104 detail: event.detail ?? null,
105 });
106 return createHash("sha256").update(canonical).digest("hex");
107}
108
109async function appendAuditEventTx(db: Db, event: AuditEvent): Promise<number> {
110 const head = await db.query(
111 `SELECT head_hash FROM audit_chain_state WHERE singleton = TRUE FOR UPDATE`,
112 );
113 const prevHash = head.rows[0]?.head_hash == null ? null : String(head.rows[0].head_hash);
114 const eventHash = auditDigest(event, prevHash);
115 const inserted = await db.query(
116 `INSERT INTO audit_log
117 (ts, actor, agent_id, op, memory_id, namespace, purpose, result, detail, prev_hash, event_hash)
118 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
119 RETURNING seq`,
120 [
121 event.ts,
122 event.actor,
123 event.agentId,
124 event.op,
125 event.memoryId,
126 event.namespace,
127 event.purpose,
128 event.result,
129 event.detail ?? null,
130 prevHash,
131 eventHash,
132 ],
133 );
134 await db.query(
135 `UPDATE audit_chain_state SET head_hash = $1, updated_at = $2 WHERE singleton = TRUE`,
136 [eventHash, event.ts],
137 );
138 // seq is BIGINT: pg binds it as text, PGlite as number — coerce either way.
139 return Number((inserted.rows[0] as { seq: unknown }).seq);
140}
141
142/** Standalone append: the chain-head lock and event commit atomically. */
143export async function appendAuditEvent(db: Db, event: AuditEvent): Promise<void> {
144 await db.transaction((tx) => appendAuditEventTx(tx, event)); 180 async verifyChain(): Promise<{ ok: boolean; checked: number; brokenSeq?: number }> {
181 const res = await this.db.query(
182 `SELECT seq, ts, actor, agent_id, op, memory_id, namespace, purpose, result,
183 detail, prev_hash, event_hash
184 FROM audit_log WHERE event_hash IS NOT NULL ORDER BY seq`,
185 );
186 let expectedPrev: string | null = null;
187 let checked = 0;
188 for (const raw of res.rows) {
189 const row = raw as Record<string, unknown>;
190 checked += 1;
191 const event = rowToAuditEvent(row);
192 const storedPrev = row.prev_hash == null ? null : String(row.prev_hash);
193 const storedHash = String(row.event_hash);
194 const expectedHash = auditDigest(event, expectedPrev);
195 if (storedPrev !== expectedPrev || storedHash !== expectedHash) {
196 return { ok: false, checked, brokenSeq: Number(row.seq) };
197 }
198 expectedPrev = storedHash;
199 }
200 const state = await this.db.query(
201 `SELECT head_hash FROM audit_chain_state WHERE singleton = TRUE`,
202 );
203 const storedHead = state.rows[0]?.head_hash == null
204 ? null
205 : String(state.rows[0].head_hash);
206 if (storedHead !== expectedPrev) {
207 return { ok: false, checked };
208 }
209 return { ok: true, checked };合规删除是唯一硬删除例外
源码判断
src/erasure/index.ts:4 forget 是 D7 不删除规则的唯一受治理例外:它硬删 memory_objects,但保留 append-only audit,并返回证书。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/erasure/index.ts:117 isInSubtree 复用 namespaceAncestors 判断 downward containment,擦除只能在 caller 自己的 subtree 内发生。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/erasure/index.ts:201 resolveTargets 对 memory、namespace、actor、entity 都先解析具体行,再按 caller subtree 过滤,避免跨租户删除。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/erasure/index.ts:306 resolveEntityRegistry 只读取 caller subtree 内的 canonical name 和 aliases,避免 sibling alias graph 扩大删除范围。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/erasure/index.ts:374 resolveUsageTargets 会清 usage_events 和 query_traces 的派生记录,并用精确 IN 绑定而非全表事务扫描。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/erasure/certificate.ts:24 stableStringify 对对象 key 递归排序,删除证书签名的 payload 因此能在 signer/verifier 两端得到同样字节。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
src/erasure/certificate.ts:75 verifyCertificate 要求 out-of-band trusted public key 与证书内 publicKey 一致,inline key 不是自签信任锚。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
1/**
2 * Module B (M4 / D3) — mem.forget erasure engine.
3 *
4 * `forget(deps, target, identity)` is the ONE governed exception to D7's "never
5 * delete": a compliance erasure. It resolves the target to a concrete id set
6 * INSIDE the caller's subtree, then in ONE database transaction:
7 * 1. nulls any dangling `supersedes` links from surviving rows into the
8 * to-be-deleted set (the FK `memory_objects.supersedes -> memory_objects.id`
9 * would otherwise reject the DELETE);
10 * 2. HARD-DELETEs the matching rows (their inline embeddings go with them);
11 * 3. inserts one tombstone per erased id;
12 * 4. builds + ed25519-signs a DeletionCertificate (public key inline) and
13 * inserts it.
14 * E6: derived usage rows (usage_events, query_traces for actor/namespace
15 * targets) are scrubbed in the same transaction — no tombstones (they are not
16 * memory objects), but the certificate enumerates their ids.
17 * It NEVER touches `audit_log` — the audit trail is append-only (D4); the
18 * syscall layer appends the `forget` audit row.
19 *
20 * Subtree scoping (isolation = the existing ancestor-prefix ACL, downward):
21 * - the caller may only erase within its OWN subtree — a target whose
22 * namespace is an ANCESTOR or a SIBLING of the caller's namespace resolves
23 * to zero rows;
24 * - resolution runs DOWNWARD: a namespace target matches that namespace and
25 * its descendants (again constrained to the caller's subtree).
26 *
27 * `deps` is a STRUCTURAL slice `{ db, audit }` — no kernel import.
28 */ 24 * Deterministic JSON of an arbitrary JSON value with object keys sorted at
25 * every depth. Arrays keep their order (erasedIds ordering is meaningful and
26 * fixed by the resolver); objects (including the nested ForgetTarget) sort.
27 */
28function stableStringify(value: unknown): string {
29 if (value === null || typeof value !== "object") {
30 return JSON.stringify(value);
31 }
32 if (Array.isArray(value)) {
33 return `[${value.map((v) => stableStringify(v)).join(",")}]`;
34 }
35 const obj = value as Record<string, unknown>;
36 const keys = Object.keys(obj).sort();
37 const parts = keys.map(
38 (k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`,
39 );
40 return `{${parts.join(",")}}`;
41}
42
43/**
44 * Canonical, signature-independent JSON string of a certificate. Given a full
45 * certificate or a payload, the `signature` field is always excluded, so the
46 * signer and the verifier hash exactly the same bytes.
47 */
48export function canonicalPayload(
49 cert: CertificatePayload | DeletionCertificate,
50): string {
51 const { signature: _signature, ...payload } = cert as DeletionCertificate;
52 return stableStringify(payload);
53}
54
55/**
56 * Sign a certificate payload with the kernel's ed25519 key; returns the base64
57 * signature over the canonical payload bytes (UTF-8).
58 */
59export function signCertificate(
60 payload: CertificatePayload,
61 key: SigningKey,
62): string {
63 const bytes = Buffer.from(canonicalPayload(payload), "utf8");
64 return key.sign(bytes).toString("base64");
65}
66
67/**
68 * Verify a certificate against an out-of-band trusted public key. The inline
69 * key is signed descriptive metadata and must identify that same trusted key;
70 * it is never itself a trust anchor. Rebuilds the canonical payload (dropping
71 * `signature`) and checks the ed25519 signature.
72 * Returns false — never throws — for a key mismatch, tampered field, bad
73 * signature, malformed key, or non-ed25519 algorithm.
74 */
75export function verifyCertificate(测试覆盖的是治理不变量
源码判断
test/allocator-fault.test.ts:227 测试直接覆盖 assemble 的 priority order、token budget、去重和 audit detail,证明上下文装配不是 README 声明。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
test/allocator-fault.test.ts:459 embedding 失败的测试期待 brownout=embedding_unavailable,并确认 BM25 仍可命中,这是故障模型的可执行证据。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
test/m7-audit-chain.test.ts:35 审计链测试检查第二行 prev_hash 指向第一行 event_hash,正常追加路径确实形成 hash-linked ledger。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
test/m7-audit-chain.test.ts:48 篡改 detail 后 verifyChain 返回 brokenSeq,说明链不仅存在,而且能捕捉历史事件字段变更。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
test/erasure.test.ts:165 actor erasure 测试要求删除 Alice 三条事实并保留 Bob 的事实,擦除范围按 provenance 和 scope 精确收束。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
test/erasure.test.ts:467 audit survives erasure 测试确认 erased memory 的 write/read 审计仍在,并只追加一个 forget 行。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
test/erasure.test.ts:624 sibling alias graph regression 防止外部租户的 Nimbus-Falcon alias 污染当前租户的 entity erasure。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。
227describe("ContextAllocator.assemble", () => {
228 it("assembles pinned, scope and predicted blocks in priority order under budget", async () => {
229 const { allocator, audit, cold } = makeAllocator(SCOPE_ROWS);
230 const ws = await allocator.assemble({
231 namespace: NS,
232 actor: "user:kay",
233 agentId: "agent-7",
234 policy: policy({
235 pinnedConcepts: [REFUND_CONCEPT.path],
236 tokenBudget: 100,
237 }),
238 taskHint: "database migration checklist",
239 });
240
241 const kinds = ws.blocks.map((b) => b.kind);
242 expect(kinds[0]).toBe("pinned-concept");
243 expect(cold.readNamespaces).toContain(NS);
244 expect(ws.blocks[0].ref).toBe(REFUND_CONCEPT.path);
245
246 const scopeRefs = ws.blocks
247 .filter((b) => b.kind === "scope-memory")
248 .map((b) => b.ref);
249 expect(scopeRefs).toEqual(["m1", "m2"]); // m3 never accessed
250
251 const relRefs = ws.blocks
252 .filter((b) => b.kind === "predicted-relevant")
253 .map((b) => b.ref);
254 expect(relRefs).toContain("m3");
255
256 // no duplicate refs across lanes (dedupe)
257 const refs = ws.blocks.map((b) => b.ref);
258 expect(new Set(refs).size).toBe(refs.length);
259
260 // budget respected, totals consistent
261 expect(ws.totalTokens).toBeLessThanOrEqual(ws.budget);
262 expect(ws.totalTokens).toBe(
263 ws.blocks.reduce((s, b) => s + b.tokenEstimate, 0),
264 );
265
266 // audit row
267 const ev = audit.events.at(-1)!;
268 expect(ev.op).toBe("assemble");
269 expect(ev.result).toBe("ok");
270 expect(ev.namespace).toBe(NS);
271 expect(ev.detail).toContain(`blocks=${ws.blocks.length}`);
272 expect(ev.detail).toContain(`tokens=${ws.totalTokens}`);
273 }); 467 it("pre-existing audit rows for erased memory stay; a forget row is appended", async () => {
468 const ns = "acme/audited";
469 const owner = ownerOf(ns);
470 const f = await plant({
471 id: "01AU1",
472 namespace: ns,
473 content: "A fact that was read before it was erased.",
474 provenance: [{ source: "conversation", actor: "grace", ts: NOW }],
475 });
476
477 // Simulate the original write + a later read being audited (D4).
478 await audit.append({
479 ts: NOW,
480 actor: "grace",
481 agentId: "agent-1",
482 op: "write",
483 memoryId: f.id,
484 namespace: ns,
485 purpose: null,
486 result: "ok",
487 });
488 await audit.append({
489 ts: NOW,
490 actor: "reader",
491 agentId: "agent-2",
492 op: "read",
493 memoryId: f.id,
494 namespace: ns,
495 purpose: "support",
496 result: "ok",
497 });
498
499 const before = await audit.count(ns);
500
501 const result = await forget(
502 { db, audit, signingKey },
503 { kind: "actor", value: "grace" },
504 owner,
505 );
506 expect(result.erasedCount).toBe(1);
507
508 // The row is gone from memory_objects...
509 expect((await index.getById(f.id, ns)).obj).toBeNull();
510
511 // ...but its audit history survives, and a `forget` row was appended.
512 const events = await audit.list(ns, { limit: 100 });
513 const opsForMemory = events
514 .filter((e) => e.memoryId === f.id)
515 .map((e) => e.op);
516 expect(opsForMemory).toContain("write");
517 expect(opsForMemory).toContain("read");
518
519 const after = await audit.count(ns);
520 expect(after).toBe(before + 1); // exactly one new row: the forget
521 expect(events.some((e) => e.op === "forget" && e.result === "ok")).toBe(true);
522 });适合学习的地方与合并前风险
源码判断
适合借鉴
src/syscalls/index.ts:124 把横切能力注入 syscall runtime,适合所有想把 agent 从 demo 推到组织系统的项目;src/driver/view.ts:181 则展示了“插件可扩展但边界不可扩展”的工程写法。src/audit/index.ts:109 与 src/erasure/certificate.ts:75 共同说明,证据链和证书验证最好从第一天进入数据模型。
需要继续确认
src/write/index.ts:354 的候选级处理非常细,但真实部署仍要压测 LLM outage、embedding brownout 和 Postgres 事务边界;src/fault/index.ts:317 会记录 fault pressure,因此运营侧也需要避免把噪声 query 变成错误优化信号。