add-coder 0.3.34 → 0.3.35
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/dist/index.js +21 -3
- package/package.json +3 -3
- package/templates/.add-coder-src-hash.json +144 -90
- package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/claude/hooks/notification.mjs +0 -4
- package/templates/adapters/claude/hooks/permission-denied.mjs +0 -4
- package/templates/adapters/claude/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/claude/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/claude/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/claude/hooks/session-end.mjs +0 -4
- package/templates/adapters/claude/hooks/session-start.mjs +65 -5
- package/templates/adapters/claude/hooks/stop-check.mjs +6 -87
- package/templates/adapters/claude/hooks/stop-failure.mjs +0 -4
- package/templates/adapters/claude/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/claude/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/codex/hooks/notification.mjs +0 -4
- package/templates/adapters/codex/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/codex/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/codex/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/codex/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/codex/hooks/session-end.mjs +0 -4
- package/templates/adapters/codex/hooks/session-start.mjs +65 -5
- package/templates/adapters/codex/hooks/stop-check.mjs +6 -87
- package/templates/adapters/codex/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/codex/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/qoder/hooks/notification.mjs +0 -4
- package/templates/adapters/qoder/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/qoder/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/qoder/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/qoder/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/qoder/hooks/review-checklist.mjs +0 -4
- package/templates/adapters/qoder/hooks/session-end.mjs +0 -4
- package/templates/adapters/qoder/hooks/session-start.mjs +67 -5
- package/templates/adapters/qoder/hooks/stop-check.mjs +6 -87
- package/templates/adapters/qoder/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/qoder/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/trae/hooks/notification.mjs +0 -4
- package/templates/adapters/trae/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/trae/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/trae/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/trae/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/trae/hooks/session-end.mjs +0 -4
- package/templates/adapters/trae/hooks/session-start.mjs +65 -5
- package/templates/adapters/trae/hooks/stop-check.mjs +6 -87
- package/templates/adapters/trae/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/trae/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/vscode/hooks/notification.mjs +0 -4
- package/templates/adapters/vscode/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/vscode/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/vscode/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/vscode/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/vscode/hooks/session-end.mjs +0 -4
- package/templates/adapters/vscode/hooks/session-start.mjs +65 -5
- package/templates/adapters/vscode/hooks/stop-check.mjs +6 -87
- package/templates/adapters/vscode/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/vscode/hooks/subagent-stop.mjs +0 -4
- package/templates/core/governance/doc-format-guard.ts +29 -112
- package/templates/core/governance/post-tool-router.ts +33 -1
- package/templates/core/governance/prompt-router.ts +47 -0
- package/templates/core/governance/rules.ts +0 -4
- package/templates/core/governance/session-start-guard.ts +48 -1
- package/templates/core/governance/stop-router.ts +7 -101
- package/templates/core/prisma/add.prisma +203 -0
- package/templates/core/scripts/db-ensure.sh +61 -2
- package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
- package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
- package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
- package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
- package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
- package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
- package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
- package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
- package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
- package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
- package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
- package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
- package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
- package/templates/core/scripts/mcp-server.ts +36 -0
- package/templates/core/templates/checklist-template.md +13 -0
- package/templates/core/templates/review-implementation-template.md +24 -0
- package/templates/core/templates/review-template.md +16 -0
- package/templates/core/validation/index.ts +136 -0
- package/templates/core/validation/policy.ts +91 -0
- package/templates/core/validation/registry.ts +61 -0
- package/templates/core/validation/schema-validator.ts +277 -0
- package/templates/core/validation/validators/add-route.ts +32 -0
- package/templates/core/validation/validators/checklist.ts +48 -0
- package/templates/core/validation/validators/handoff.ts +46 -0
- package/templates/core/validation/validators/hitl.ts +22 -0
- package/templates/core/validation/validators/index.ts +52 -0
- package/templates/core/validation/validators/plan.ts +20 -0
- package/templates/core/validation/validators/report.ts +16 -0
- package/templates/core/validation/validators/review.ts +30 -0
- package/templates/core/validation/validators/spec.ts +25 -0
- package/templates/core/validation/validators/tasks.ts +40 -0
- package/templates/core/validation/validators/types.ts +32 -0
- package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
- package/templates/core/scripts/mcp-server/shared/dps-spec-ref.ts +0 -17
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Memory 生命周期状态机(Plan §5)
|
|
3
|
+
*
|
|
4
|
+
* CANDIDATE → PENDING → ACTIVE → STALE → SUPERSEDED → ARCHIVED
|
|
5
|
+
* │ │ │ └──────────────→ ARCHIVED
|
|
6
|
+
* └──────────┴─────────┴──────────────────────→ REJECTED
|
|
7
|
+
*
|
|
8
|
+
* 首版默认只允许人工或显式治理调用激活(approve 的强制校验在此表达)。
|
|
9
|
+
*/
|
|
10
|
+
import { MemoryError } from "./errors.js"
|
|
11
|
+
|
|
12
|
+
export type MemoryStatus =
|
|
13
|
+
| "CANDIDATE" | "PENDING" | "ACTIVE" | "STALE"
|
|
14
|
+
| "SUPERSEDED" | "REJECTED" | "ARCHIVED"
|
|
15
|
+
|
|
16
|
+
export type MemoryAction =
|
|
17
|
+
| "propose" | "submit_review" | "approve" | "reject"
|
|
18
|
+
| "mark_stale" | "supersede" | "archive" | "restore"
|
|
19
|
+
|
|
20
|
+
interface Transition {
|
|
21
|
+
from: MemoryStatus[]
|
|
22
|
+
to: MemoryStatus
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 迁移表:propose 是创建语义(无 from),其余均为状态到状态 */
|
|
26
|
+
export const TRANSITIONS: Record<MemoryAction, Transition> = {
|
|
27
|
+
propose: { from: [], to: "CANDIDATE" },
|
|
28
|
+
submit_review: { from: ["CANDIDATE"], to: "PENDING" },
|
|
29
|
+
approve: { from: ["PENDING", "CANDIDATE"], to: "ACTIVE" },
|
|
30
|
+
reject: { from: ["CANDIDATE", "PENDING"], to: "REJECTED" },
|
|
31
|
+
mark_stale: { from: ["ACTIVE"], to: "STALE" },
|
|
32
|
+
supersede: { from: ["ACTIVE", "STALE"], to: "SUPERSEDED" },
|
|
33
|
+
archive: { from: ["ACTIVE", "STALE", "SUPERSEDED"], to: "ARCHIVED" },
|
|
34
|
+
restore: { from: ["ARCHIVED", "REJECTED"], to: "CANDIDATE" },
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** approve / supersede 的不变量校验输入(DB 查询结果由调用方传入,领域层保持纯函数) */
|
|
38
|
+
export interface TransitionContext {
|
|
39
|
+
evidenceCount?: number
|
|
40
|
+
approvedBy?: string | null
|
|
41
|
+
approvedAt?: Date | null
|
|
42
|
+
/** supersede 专用:新记忆 id 与 repository/scope 兼容性判定结果 */
|
|
43
|
+
supersededById?: string | null
|
|
44
|
+
supersessionCompatible?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TransitionResult {
|
|
48
|
+
from: MemoryStatus
|
|
49
|
+
to: MemoryStatus
|
|
50
|
+
action: MemoryAction
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 判定迁移是否合法;非法时抛 MemoryError(ERR_ILLEGAL_TRANSITION / ERR_EVIDENCE_REQUIRED / ...) */
|
|
54
|
+
export function assertTransition(
|
|
55
|
+
current: MemoryStatus,
|
|
56
|
+
action: MemoryAction,
|
|
57
|
+
ctx: TransitionContext = {},
|
|
58
|
+
): TransitionResult {
|
|
59
|
+
const t = TRANSITIONS[action]
|
|
60
|
+
if (action === "propose") {
|
|
61
|
+
// 创建语义:不与现有状态冲突(由调用方决定新建或合并)
|
|
62
|
+
return { from: current, to: t.to, action }
|
|
63
|
+
}
|
|
64
|
+
if (!t.from.includes(current)) {
|
|
65
|
+
throw new MemoryError("ERR_ILLEGAL_TRANSITION", `${current} --${action}--> ${t.to} 不允许(允许起点: ${t.from.join("/")})`)
|
|
66
|
+
}
|
|
67
|
+
if (action === "approve") {
|
|
68
|
+
if (!ctx.evidenceCount || ctx.evidenceCount < 1) {
|
|
69
|
+
throw new MemoryError("ERR_EVIDENCE_REQUIRED")
|
|
70
|
+
}
|
|
71
|
+
if (!ctx.approvedBy) {
|
|
72
|
+
throw new MemoryError("ERR_APPROVAL_REQUIRED", "缺少 approvedBy")
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (action === "supersede") {
|
|
76
|
+
if (!ctx.supersededById) {
|
|
77
|
+
throw new MemoryError("ERR_SUPERSESSION_INVALID", "缺少 supersededById")
|
|
78
|
+
}
|
|
79
|
+
if (ctx.supersessionCompatible === false) {
|
|
80
|
+
throw new MemoryError("ERR_SUPERSESSION_INVALID", "新旧 Memory 的 repository/scope 不兼容")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { from: current, to: t.to, action }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 默认召回允许的状态集合(Plan §7.1:默认排除 CANDIDATE/PENDING/REJECTED/ARCHIVED/SUPERSEDED/过期项) */
|
|
87
|
+
export const DEFAULT_RECALL_STATUSES: readonly MemoryStatus[] = ["ACTIVE"]
|
|
88
|
+
|
|
89
|
+
/** 显式诊断模式额外放行 STALE(带警告) */
|
|
90
|
+
export const DIAGNOSTIC_RECALL_STATUSES: readonly MemoryStatus[] = ["ACTIVE", "STALE"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Embedding 抽象与降级(Plan §8.1/§8.4,Spec §9)
|
|
3
|
+
*
|
|
4
|
+
* 提供者:none(默认,显式不可用)/ local-onnx / openai-compatible(Phase 5 轮 3)。
|
|
5
|
+
* Vector adapter:pgvector / sqlite-vec(retrieval/vector/),能力检测失败即降级。
|
|
6
|
+
* 契约:记忆系统任何环节不得因 embedding 缺失而阻塞 Gate(FTS-only 合法降级)。
|
|
7
|
+
*/
|
|
8
|
+
import { MemoryError } from "../domain/errors.js"
|
|
9
|
+
import type { ComponentHealth, RankedId, RecallFilter } from "../retrieval/types.js"
|
|
10
|
+
|
|
11
|
+
export interface EmbeddingProvider {
|
|
12
|
+
readonly id: string // "none" | "local-onnx" | "openai-compatible" | "custom"
|
|
13
|
+
readonly dimension: number
|
|
14
|
+
embed(texts: string[]): Promise<number[][]>
|
|
15
|
+
health(): Promise<ComponentHealth>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface VectorSearchAdapter {
|
|
19
|
+
search(vector: number[], filter: RecallFilter, limit: number): Promise<RankedId[]>
|
|
20
|
+
upsert(memoryId: string, vector: number[], model: string): Promise<void>
|
|
21
|
+
health(): Promise<ComponentHealth>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 首版唯一 provider:显式不可用。调用方据此走 FTS-only 并标注 degradedMode */
|
|
25
|
+
export function createNoneEmbeddingProvider(): EmbeddingProvider {
|
|
26
|
+
return {
|
|
27
|
+
id: "none",
|
|
28
|
+
dimension: 0,
|
|
29
|
+
embed: (): Promise<number[][]> => Promise.reject(new MemoryError("ERR_EMBEDDING_DISABLED")),
|
|
30
|
+
health: (): Promise<ComponentHealth> =>
|
|
31
|
+
Promise.resolve({ component: "embedding", status: "disabled", detail: "EmbeddingProvider=none(首版定案,FTS-only)" }),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Vector adapter capability 存根:health 报告 unavailable,search/upsert 拒绝 */
|
|
36
|
+
export function createUnavailableVectorAdapter(): VectorSearchAdapter {
|
|
37
|
+
return {
|
|
38
|
+
search: (): Promise<RankedId[]> =>
|
|
39
|
+
Promise.reject(new MemoryError("ERR_EMBEDDING_DISABLED", "VectorSearchAdapter 未配置(Phase 5 接入)")),
|
|
40
|
+
upsert: (): Promise<void> =>
|
|
41
|
+
Promise.reject(new MemoryError("ERR_EMBEDDING_DISABLED", "VectorSearchAdapter 未配置(Phase 5 接入)")),
|
|
42
|
+
health: (): Promise<ComponentHealth> =>
|
|
43
|
+
Promise.resolve({ component: "vector-search", status: "unavailable", detail: "向量索引未启用(Phase 5 接入 pgvector/sqlite-vec)" }),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 维度真源校验(Review R5 回流):provider 上报维度必须与 DB 侧声明维度一致,否则拒写。
|
|
49
|
+
* declaredDim 来自 AddMemory.embeddingDim(写入路径的真源)。
|
|
50
|
+
*/
|
|
51
|
+
export function assertEmbeddingDimension(providerDim: number, declaredDim: number): void {
|
|
52
|
+
if (declaredDim <= 0) return // 未声明维度:首写,由 provider 值落库
|
|
53
|
+
if (providerDim !== declaredDim) {
|
|
54
|
+
throw new MemoryError(
|
|
55
|
+
"ERR_DIMENSION_MISMATCH",
|
|
56
|
+
`嵌入维度不一致:provider=${providerDim} vs 声明=${declaredDim}(拒绝写入,避免脏向量)`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type EmbeddingMode = "none" | "local-onnx" | "openai-compatible"
|
|
62
|
+
|
|
63
|
+
export interface EmbeddingEnvConfig {
|
|
64
|
+
mode?: string
|
|
65
|
+
model?: string
|
|
66
|
+
dimension?: number
|
|
67
|
+
baseUrl?: string
|
|
68
|
+
apiKey?: string
|
|
69
|
+
cacheDir?: string
|
|
70
|
+
remoteHost?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 从环境变量解析提供者配置(纯函数,便于测试与文档化) */
|
|
74
|
+
export function parseEmbeddingEnv(env: NodeJS.ProcessEnv = process.env): EmbeddingEnvConfig {
|
|
75
|
+
const mode = (env.ADD_MEMORY_EMBEDDING ?? "none").toLowerCase()
|
|
76
|
+
const dim = Number(env.ADD_MEMORY_EMBEDDING_DIM ?? "")
|
|
77
|
+
return {
|
|
78
|
+
mode,
|
|
79
|
+
model: env.ADD_MEMORY_EMBEDDING_MODEL,
|
|
80
|
+
dimension: Number.isFinite(dim) && dim > 0 ? Math.floor(dim) : undefined,
|
|
81
|
+
baseUrl: env.ADD_MEMORY_EMBEDDING_BASE_URL,
|
|
82
|
+
apiKey: env.ADD_MEMORY_EMBEDDING_API_KEY,
|
|
83
|
+
cacheDir: env.ADD_MEMORY_EMBEDDING_CACHE_DIR,
|
|
84
|
+
remoteHost: env.ADD_MEMORY_EMBEDDING_REMOTE_HOST,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 按配置装配提供者(默认 none → 行为与首版一致)。
|
|
90
|
+
* local-onnx / openai-compatible 走动态 import,未配置的路径不加载任何重依赖。
|
|
91
|
+
*/
|
|
92
|
+
export async function createEmbeddingProviderFromConfig(
|
|
93
|
+
config: EmbeddingEnvConfig = parseEmbeddingEnv(),
|
|
94
|
+
): Promise<EmbeddingProvider> {
|
|
95
|
+
const mode = (config.mode ?? "none") as EmbeddingMode
|
|
96
|
+
if (mode === "local-onnx") {
|
|
97
|
+
const { createLocalOnnxEmbeddingProvider } = await import("./local-onnx.js")
|
|
98
|
+
return createLocalOnnxEmbeddingProvider({
|
|
99
|
+
model: config.model,
|
|
100
|
+
cacheDir: config.cacheDir,
|
|
101
|
+
remoteHost: config.remoteHost,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
if (mode === "openai-compatible") {
|
|
105
|
+
if (!config.baseUrl) {
|
|
106
|
+
throw new MemoryError("ERR_EMBEDDING_DISABLED", "openai-compatible 需要 ADD_MEMORY_EMBEDDING_BASE_URL")
|
|
107
|
+
}
|
|
108
|
+
const { createOpenAiCompatibleEmbeddingProvider } = await import("./openai-compatible.js")
|
|
109
|
+
return createOpenAiCompatibleEmbeddingProvider({
|
|
110
|
+
baseUrl: config.baseUrl,
|
|
111
|
+
model: config.model ?? "text-embedding-3-small",
|
|
112
|
+
apiKey: config.apiKey,
|
|
113
|
+
dimension: config.dimension,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
return createNoneEmbeddingProvider()
|
|
117
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 本地 ONNX 嵌入提供者(Plan §3.4 轮 3 Task 3.1 / Spec §6 §VectorCapability)
|
|
3
|
+
*
|
|
4
|
+
* 关键约束:
|
|
5
|
+
* 1. **可选依赖**:@huggingface/transformers 与模型权重都可能在目标机器缺失——加载失败
|
|
6
|
+
* 必须收敛为 ERR_EMBEDDING_DISABLED(调用方据此走 fts-only),绝不抛裸异常阻塞 Gate;
|
|
7
|
+
* 2. **维度以模型元数据为真源**:dimension 由首次推理的实际输出长度决定(不硬编码),
|
|
8
|
+
* 与 DB 侧 AddMemory.embeddingDim 不一致时由调用方拒绝写入(Review R5 回流);
|
|
9
|
+
* 3. 惰性加载:不在模块顶层 import/下载模型,首次 embed 时才初始化。
|
|
10
|
+
*/
|
|
11
|
+
import { MemoryError } from "../domain/errors.js"
|
|
12
|
+
import type { ComponentHealth } from "../retrieval/types.js"
|
|
13
|
+
import type { EmbeddingProvider } from "./index.js"
|
|
14
|
+
|
|
15
|
+
/** 默认模型:中文小模型(真源维度 512,与 Plan R5「维度真源」一致) */
|
|
16
|
+
export const DEFAULT_LOCAL_ONNX_MODEL = "Xenova/bge-small-zh-v1.5"
|
|
17
|
+
|
|
18
|
+
export interface LocalOnnxOptions {
|
|
19
|
+
model?: string
|
|
20
|
+
/** 显式缓存目录;缺省复用 HF_HUB_CACHE → HF_HOME/hub → ~/.cache/huggingface/hub */
|
|
21
|
+
cacheDir?: string
|
|
22
|
+
/** 镜像站(内网/受限网络) */
|
|
23
|
+
remoteHost?: string
|
|
24
|
+
/** 测试注入:直接给定 pipeline 工厂 */
|
|
25
|
+
loadPipeline?: (model: string) => Promise<(texts: string[]) => Promise<number[][]>>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LocalOnnxProvider extends EmbeddingProvider {
|
|
29
|
+
/** 首次推理后可用;未初始化时为 0(调用方以 assertEmbeddingDimension 校验) */
|
|
30
|
+
readonly resolvedDimension: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createLocalOnnxEmbeddingProvider(opts: LocalOnnxOptions = {}): LocalOnnxProvider {
|
|
34
|
+
const model = opts.model ?? DEFAULT_LOCAL_ONNX_MODEL
|
|
35
|
+
let embedFn: ((texts: string[]) => Promise<number[][]>) | null = null
|
|
36
|
+
let dimension = 0
|
|
37
|
+
|
|
38
|
+
async function ensureLoaded(): Promise<(texts: string[]) => Promise<number[][]>> {
|
|
39
|
+
if (embedFn) return embedFn
|
|
40
|
+
try {
|
|
41
|
+
if (opts.loadPipeline) {
|
|
42
|
+
embedFn = await opts.loadPipeline(model)
|
|
43
|
+
} else {
|
|
44
|
+
const { pipeline, env } = await import("@huggingface/transformers")
|
|
45
|
+
if (opts.cacheDir) env.cacheDir = opts.cacheDir
|
|
46
|
+
if (opts.remoteHost) env.remoteHost = opts.remoteHost
|
|
47
|
+
const extractor = await pipeline("feature-extraction", model)
|
|
48
|
+
embedFn = async (texts: string[]) => {
|
|
49
|
+
const result = await extractor(texts, { pooling: "mean", normalize: true })
|
|
50
|
+
const list = result.tolist() as number[][]
|
|
51
|
+
return list.length === texts.length ? list : [list as unknown as number[]]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return embedFn
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new MemoryError(
|
|
57
|
+
"ERR_EMBEDDING_DISABLED",
|
|
58
|
+
`本地 ONNX 模型不可用(model=${model}):${error instanceof Error ? error.message : String(error)}`,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
id: "local-onnx",
|
|
65
|
+
get dimension(): number {
|
|
66
|
+
return dimension
|
|
67
|
+
},
|
|
68
|
+
get resolvedDimension(): number {
|
|
69
|
+
return dimension
|
|
70
|
+
},
|
|
71
|
+
async embed(texts: string[]): Promise<number[][]> {
|
|
72
|
+
if (texts.length === 0) return []
|
|
73
|
+
const fn = await ensureLoaded()
|
|
74
|
+
let vectors: number[][]
|
|
75
|
+
try {
|
|
76
|
+
vectors = await fn(texts)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new MemoryError(
|
|
79
|
+
"ERR_EMBEDDING_DISABLED",
|
|
80
|
+
`本地 ONNX 推理失败(model=${model}):${error instanceof Error ? error.message : String(error)}`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
const dim = vectors[0]?.length ?? 0
|
|
84
|
+
if (dim === 0) {
|
|
85
|
+
throw new MemoryError("ERR_EMBEDDING_DISABLED", `本地 ONNX 返回空向量(model=${model})`)
|
|
86
|
+
}
|
|
87
|
+
// 维度真源:以模型实际输出为准;同一 provider 内必须自洽
|
|
88
|
+
if (dimension !== 0 && dimension !== dim) {
|
|
89
|
+
throw new MemoryError("ERR_DIMENSION_MISMATCH", `provider 维度漂移:${dimension} → ${dim}`)
|
|
90
|
+
}
|
|
91
|
+
dimension = dim
|
|
92
|
+
return vectors
|
|
93
|
+
},
|
|
94
|
+
health(): Promise<ComponentHealth> {
|
|
95
|
+
if (embedFn && dimension > 0) {
|
|
96
|
+
return Promise.resolve({ component: "embedding", status: "ok", detail: `local-onnx model=${model} dim=${dimension}` })
|
|
97
|
+
}
|
|
98
|
+
return Promise.resolve({
|
|
99
|
+
component: "embedding",
|
|
100
|
+
status: "degraded",
|
|
101
|
+
detail: `local-onnx 未初始化(model=${model});首次 embed 时加载,失败则降级 fts-only`,
|
|
102
|
+
})
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* OpenAI 兼容嵌入提供者(Plan §3.4 轮 3 Task 3.1)
|
|
3
|
+
*
|
|
4
|
+
* 适用于任意 /v1/embeddings 兼容端点(本地 vLLM / 自建网关 / 官方 API)。
|
|
5
|
+
* 契约同 local-onnx:维度以首次响应为准(配置项 dimension 仅作期望值校验),
|
|
6
|
+
* 任何失败收敛为 ERR_EMBEDDING_DISABLED,不阻塞 Gate。
|
|
7
|
+
*/
|
|
8
|
+
import { MemoryError } from "../domain/errors.js"
|
|
9
|
+
import type { ComponentHealth } from "../retrieval/types.js"
|
|
10
|
+
import type { EmbeddingProvider } from "./index.js"
|
|
11
|
+
|
|
12
|
+
export interface OpenAiCompatibleOptions {
|
|
13
|
+
baseUrl: string
|
|
14
|
+
model: string
|
|
15
|
+
apiKey?: string
|
|
16
|
+
/** 期望维度(可选):与首次响应不一致时报 ERR_DIMENSION_MISMATCH */
|
|
17
|
+
dimension?: number
|
|
18
|
+
timeoutMs?: number
|
|
19
|
+
/** 测试注入 */
|
|
20
|
+
fetchImpl?: typeof fetch
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createOpenAiCompatibleEmbeddingProvider(
|
|
24
|
+
opts: OpenAiCompatibleOptions,
|
|
25
|
+
): EmbeddingProvider {
|
|
26
|
+
const doFetch = opts.fetchImpl ?? fetch
|
|
27
|
+
const endpoint = `${opts.baseUrl.replace(/\/+$/, "")}/embeddings`
|
|
28
|
+
let dimension = opts.dimension ?? 0
|
|
29
|
+
let lastError: string | null = null
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
id: "openai-compatible",
|
|
33
|
+
get dimension(): number {
|
|
34
|
+
return dimension
|
|
35
|
+
},
|
|
36
|
+
async embed(texts: string[]): Promise<number[][]> {
|
|
37
|
+
if (texts.length === 0) return []
|
|
38
|
+
const controller = new AbortController()
|
|
39
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 15_000)
|
|
40
|
+
let payload: { data?: { embedding?: number[] }[] }
|
|
41
|
+
try {
|
|
42
|
+
const res = await doFetch(endpoint, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
"content-type": "application/json",
|
|
46
|
+
...(opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {}),
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({ model: opts.model, input: texts }),
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
})
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new Error(`HTTP ${res.status}`)
|
|
53
|
+
}
|
|
54
|
+
payload = (await res.json()) as { data?: { embedding?: number[] }[] }
|
|
55
|
+
} catch (error) {
|
|
56
|
+
lastError = error instanceof Error ? error.message : String(error)
|
|
57
|
+
throw new MemoryError("ERR_EMBEDDING_DISABLED", `兼容端点调用失败(${endpoint}):${lastError}`)
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timer)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const vectors = (payload.data ?? []).map((d) => d.embedding ?? []).filter((v) => v.length > 0)
|
|
63
|
+
if (vectors.length === 0) {
|
|
64
|
+
throw new MemoryError("ERR_EMBEDDING_DISABLED", `兼容端点未返回向量(model=${opts.model})`)
|
|
65
|
+
}
|
|
66
|
+
const dim = vectors[0].length
|
|
67
|
+
if (dimension !== 0 && dimension !== dim) {
|
|
68
|
+
throw new MemoryError("ERR_DIMENSION_MISMATCH", `期望维度 ${dimension},实收 ${dim}`)
|
|
69
|
+
}
|
|
70
|
+
dimension = dim
|
|
71
|
+
lastError = null
|
|
72
|
+
return vectors
|
|
73
|
+
},
|
|
74
|
+
health(): Promise<ComponentHealth> {
|
|
75
|
+
if (dimension > 0) {
|
|
76
|
+
return Promise.resolve({ component: "embedding", status: "ok", detail: `openai-compatible model=${opts.model} dim=${dimension}` })
|
|
77
|
+
}
|
|
78
|
+
return Promise.resolve({
|
|
79
|
+
component: "embedding",
|
|
80
|
+
status: lastError ? "unavailable" : "degraded",
|
|
81
|
+
detail: lastError
|
|
82
|
+
? `兼容端点不可用:${lastError}`
|
|
83
|
+
: `兼容端点未调用(model=${opts.model},endpoint=${endpoint})`,
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Consolidation 异步任务(Spec §10,Plan §9.2 Post-Handoff 行)
|
|
3
|
+
*
|
|
4
|
+
* 职责(全部 fail-open 隔离:单步失败不影响他步,错误入报告):
|
|
5
|
+
* 1. drainEvidenceQueue —— 采证队列落库
|
|
6
|
+
* 2. 重复检测 —— 同 repositoryRef+contentHash 多行 → 报告(不自动合并,人审)
|
|
7
|
+
* 3. 冲突队列 —— CANDIDATE/PENDING 与 ACTIVE 高相似 → 报告(不自动激活,Plan §3)
|
|
8
|
+
* 4. 指标候选 —— AddMetricSnapshot 连续异常 streak 命中 → 创建 CANDIDATE(幂等)
|
|
9
|
+
* 5. Handoff Digest —— HANDOFF Evidence → HANDOFF_DIGEST Candidate(幂等,绝不直接 ACTIVE)
|
|
10
|
+
* 6. 刷新 L1 快照 —— 供 session-start Hook 注入
|
|
11
|
+
*/
|
|
12
|
+
import type {
|
|
13
|
+
AddMemoryEvidenceRow,
|
|
14
|
+
AddMemoryEvidenceLinkRow,
|
|
15
|
+
AddMemoryRow,
|
|
16
|
+
AddMetricSnapshotRow,
|
|
17
|
+
TableDelegate,
|
|
18
|
+
} from "../../db-types.js"
|
|
19
|
+
import { detectConflicts, type Conflict } from "../domain/conflicts.js"
|
|
20
|
+
import { contentHash } from "../domain/dedup.js"
|
|
21
|
+
import { detectAnomalyStreak, type MetricPoint } from "../domain/metric-candidate.js"
|
|
22
|
+
import { buildHandoffDigest } from "../domain/handoff-digest.js"
|
|
23
|
+
import { drainEvidenceQueue, type DrainResult } from "./evidence-collector.js"
|
|
24
|
+
import { refreshL1Snapshot, type SnapshotDeps, type SnapshotResult } from "./snapshot.js"
|
|
25
|
+
|
|
26
|
+
export interface ConsolidationDeps extends SnapshotDeps {
|
|
27
|
+
memoryDb: TableDelegate<AddMemoryRow>
|
|
28
|
+
evidenceDb: Pick<TableDelegate<AddMemoryEvidenceRow>, "upsert" | "findMany">
|
|
29
|
+
linkDb: Pick<TableDelegate<AddMemoryEvidenceLinkRow>, "findFirst" | "create">
|
|
30
|
+
metricDb: TableDelegate<AddMetricSnapshotRow>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ConsolidationReport {
|
|
34
|
+
drain: DrainResult
|
|
35
|
+
duplicates: { contentHash: string; ids: string[] }[]
|
|
36
|
+
conflictQueue: { candidateId: string; topic: string; conflicts: Conflict[] }[]
|
|
37
|
+
metricCandidatesCreated: string[]
|
|
38
|
+
handoffDigestsCreated: string[]
|
|
39
|
+
snapshot: SnapshotResult | null
|
|
40
|
+
errors: string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runConsolidation(deps: ConsolidationDeps): Promise<ConsolidationReport> {
|
|
44
|
+
const report: ConsolidationReport = {
|
|
45
|
+
drain: { processed: 0, skipped: 0, errors: [], newOffset: 0 },
|
|
46
|
+
duplicates: [],
|
|
47
|
+
conflictQueue: [],
|
|
48
|
+
metricCandidatesCreated: [],
|
|
49
|
+
handoffDigestsCreated: [],
|
|
50
|
+
snapshot: null,
|
|
51
|
+
errors: [],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 1. 采证队列落库
|
|
55
|
+
try {
|
|
56
|
+
report.drain = await drainEvidenceQueue(deps)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
report.errors.push(`drain: ${e instanceof Error ? e.message : String(e)}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2/3. 重复检测 + 冲突队列(共享一次全量读取)
|
|
62
|
+
try {
|
|
63
|
+
const rows = await deps.memoryDb.findMany({
|
|
64
|
+
where: { repositoryRef: deps.repositoryRef, status: { notIn: ["REJECTED", "ARCHIVED"] } },
|
|
65
|
+
take: 500,
|
|
66
|
+
})
|
|
67
|
+
const byHash = new Map<string, string[]>()
|
|
68
|
+
for (const r of rows) {
|
|
69
|
+
const arr = byHash.get(r.contentHash) ?? []
|
|
70
|
+
arr.push(r.id)
|
|
71
|
+
byHash.set(r.contentHash, arr)
|
|
72
|
+
}
|
|
73
|
+
for (const [hash, ids] of byHash) {
|
|
74
|
+
if (ids.length > 1) report.duplicates.push({ contentHash: hash, ids })
|
|
75
|
+
}
|
|
76
|
+
const actives = rows.filter((r) => r.status === "ACTIVE")
|
|
77
|
+
for (const c of rows.filter((r) => r.status === "CANDIDATE" || r.status === "PENDING")) {
|
|
78
|
+
const conflicts = detectConflicts(c, actives)
|
|
79
|
+
if (conflicts.length > 0) report.conflictQueue.push({ candidateId: c.id, topic: c.topic, conflicts })
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
report.errors.push(`dedup/conflicts: ${e instanceof Error ? e.message : String(e)}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 4. 指标候选(Plan §10:连续异常 streak 才生成,内容必须是可验证结论)
|
|
86
|
+
try {
|
|
87
|
+
const metrics = await deps.metricDb.findMany({
|
|
88
|
+
where: { repositoryRef: deps.repositoryRef },
|
|
89
|
+
orderBy: { measuredAt: "asc" },
|
|
90
|
+
take: 500,
|
|
91
|
+
})
|
|
92
|
+
const byType = new Map<string, MetricPoint[]>()
|
|
93
|
+
for (const m of metrics) {
|
|
94
|
+
const arr = byType.get(m.metricType) ?? []
|
|
95
|
+
arr.push({
|
|
96
|
+
repositoryRef: m.repositoryRef,
|
|
97
|
+
metricType: m.metricType,
|
|
98
|
+
value: m.value,
|
|
99
|
+
baseline: m.baseline,
|
|
100
|
+
planKeyword: m.planKeyword,
|
|
101
|
+
measuredAt: m.measuredAt,
|
|
102
|
+
sourceRef: m.sourceRef,
|
|
103
|
+
})
|
|
104
|
+
byType.set(m.metricType, arr)
|
|
105
|
+
}
|
|
106
|
+
for (const points of byType.values()) {
|
|
107
|
+
const proposal = detectAnomalyStreak(points)
|
|
108
|
+
if (!proposal) continue
|
|
109
|
+
const hash = contentHash(proposal.content)
|
|
110
|
+
const existing = await deps.memoryDb.findFirst({
|
|
111
|
+
where: {
|
|
112
|
+
repositoryRef: deps.repositoryRef,
|
|
113
|
+
contentHash: hash,
|
|
114
|
+
scopeType: "REPOSITORY",
|
|
115
|
+
scopeValue: deps.repositoryRef,
|
|
116
|
+
status: { notIn: ["REJECTED", "ARCHIVED"] },
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
if (existing) continue // 幂等:已提过同结论
|
|
120
|
+
const created = await deps.memoryDb.create({
|
|
121
|
+
data: {
|
|
122
|
+
kind: "PATTERN",
|
|
123
|
+
topic: proposal.topic,
|
|
124
|
+
content: proposal.content,
|
|
125
|
+
scopeType: "REPOSITORY",
|
|
126
|
+
scopeValue: deps.repositoryRef,
|
|
127
|
+
repositoryRef: deps.repositoryRef,
|
|
128
|
+
contentHash: hash,
|
|
129
|
+
createdBy: "memory-jobs:consolidation",
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
for (const ref of proposal.evidenceSourceRefs) {
|
|
133
|
+
const ev = await deps.evidenceDb.upsert({
|
|
134
|
+
where: {
|
|
135
|
+
repositoryRef_sourceType_sourceRef_contentHash: {
|
|
136
|
+
repositoryRef: deps.repositoryRef, sourceType: "DPS_GATE", sourceRef: ref, contentHash: contentHash(ref),
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
create: {
|
|
140
|
+
repositoryRef: deps.repositoryRef, sourceType: "DPS_GATE", sourceRef: ref,
|
|
141
|
+
excerpt: ref.slice(0, 500), contentHash: contentHash(ref),
|
|
142
|
+
},
|
|
143
|
+
update: {},
|
|
144
|
+
})
|
|
145
|
+
const linked = await deps.linkDb.findFirst({ where: { memoryId: created.id, evidenceId: ev.id } })
|
|
146
|
+
if (!linked) await deps.linkDb.create({ data: { memoryId: created.id, evidenceId: ev.id } })
|
|
147
|
+
}
|
|
148
|
+
report.metricCandidatesCreated.push(created.id)
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
report.errors.push(`metric-candidates: ${e instanceof Error ? e.message : String(e)}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 5. Handoff Digest(Plan §3.1 轮 2):HANDOFF Evidence → HANDOFF_DIGEST Candidate
|
|
155
|
+
// 幂等去重靠 dedupKey 派生的 contentHash;绝不直接置 ACTIVE(只产 CANDIDATE)
|
|
156
|
+
try {
|
|
157
|
+
const handoffEvidence = await deps.evidenceDb.findMany({
|
|
158
|
+
where: { repositoryRef: deps.repositoryRef, sourceType: "HANDOFF" },
|
|
159
|
+
orderBy: { occurredAt: "asc" },
|
|
160
|
+
take: 200,
|
|
161
|
+
})
|
|
162
|
+
for (const ev of handoffEvidence) {
|
|
163
|
+
const proposal = buildHandoffDigest(
|
|
164
|
+
{ handoffRef: ev.sourceRef, excerpt: ev.excerpt, planKeyword: ev.planKeyword },
|
|
165
|
+
{ repositoryRef: deps.repositoryRef },
|
|
166
|
+
)
|
|
167
|
+
const existing = await deps.memoryDb.findFirst({
|
|
168
|
+
where: {
|
|
169
|
+
repositoryRef: deps.repositoryRef,
|
|
170
|
+
contentHash: proposal.contentHash,
|
|
171
|
+
scopeType: proposal.scopeType,
|
|
172
|
+
scopeValue: proposal.scopeValue,
|
|
173
|
+
status: { notIn: ["REJECTED", "ARCHIVED"] },
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
if (existing) {
|
|
177
|
+
// 幂等:同一交接 evidence 的摘要已入队过 → 仅补齐证据关联
|
|
178
|
+
const linkedAlready = await deps.linkDb.findFirst({
|
|
179
|
+
where: { memoryId: existing.id, evidenceId: ev.id },
|
|
180
|
+
})
|
|
181
|
+
if (!linkedAlready) {
|
|
182
|
+
await deps.linkDb.create({
|
|
183
|
+
data: { memoryId: existing.id, evidenceId: ev.id },
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
const created = await deps.memoryDb.create({
|
|
189
|
+
data: {
|
|
190
|
+
kind: proposal.kind,
|
|
191
|
+
status: "CANDIDATE", // 绝不直接 ACTIVE
|
|
192
|
+
topic: proposal.topic,
|
|
193
|
+
content: proposal.content,
|
|
194
|
+
scopeType: proposal.scopeType,
|
|
195
|
+
scopeValue: proposal.scopeValue,
|
|
196
|
+
repositoryRef: deps.repositoryRef,
|
|
197
|
+
contentHash: proposal.contentHash,
|
|
198
|
+
importance: 0.6,
|
|
199
|
+
confidence: 0.5,
|
|
200
|
+
createdBy: "memory-jobs:consolidation",
|
|
201
|
+
metadata: { ...proposal.metadata, dedupKey: proposal.dedupKey, evidenceId: ev.id },
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
const linked = await deps.linkDb.findFirst({
|
|
205
|
+
where: { memoryId: created.id, evidenceId: ev.id },
|
|
206
|
+
})
|
|
207
|
+
if (!linked) {
|
|
208
|
+
await deps.linkDb.create({
|
|
209
|
+
data: { memoryId: created.id, evidenceId: ev.id },
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
report.handoffDigestsCreated.push(created.id)
|
|
213
|
+
}
|
|
214
|
+
} catch (e) {
|
|
215
|
+
report.errors.push(`handoff-digest: ${e instanceof Error ? e.message : String(e)}`)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 6. 刷新 L1 快照
|
|
219
|
+
try {
|
|
220
|
+
report.snapshot = await refreshL1Snapshot(deps)
|
|
221
|
+
} catch (e) {
|
|
222
|
+
report.errors.push(`snapshot: ${e instanceof Error ? e.message : String(e)}`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return report
|
|
226
|
+
}
|