@sema-agent/server 7.43.0 → 7.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -1,71 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #228 —— OpenAI 兼容的 embedder(黑板 [3590] 裁②:core 只留了 `PgEmbedder` 这个**纯接口**
|
|
3
|
-
* (`{ embed(text) → number[]; dimensions }`),没有任何现成实现件;部署半场归本仓)。
|
|
4
|
-
*
|
|
5
|
-
* ## 形
|
|
6
|
-
*
|
|
7
|
-
* `POST {endpoint}/embeddings`,body `{ model, input }`,读 `data[0].embedding` —— OpenAI /
|
|
8
|
-
* DashScope / TEI / vllm / ollama 的 `/v1` 面同形。auth 可选(自托管端点通常没有 key)。
|
|
9
|
-
*
|
|
10
|
-
* ## 为什么校验做得这么硬
|
|
11
|
-
*
|
|
12
|
-
* 向量列写错维度是**静默毒库**形([3606] 裁③):错长度向量既不会让 DB 报错(本仓 pg 记忆表的
|
|
13
|
-
* embedding 列是 `jsonb`,无维度约束),也不会让检索报错(维度不匹配的行只是悄悄退回词面档)——
|
|
14
|
-
* 于是「向量面开着」这句话变成一句谎,而且没有任何一层会说出来。所以:
|
|
15
|
-
* · 响应形用 zod 校验(`data[0].embedding` 必须是 `number[]`),不符 = 抛带上下文的错;
|
|
16
|
-
* · 长度与声明维度不等 = 抛,**绝不截断 / 补零**(那正是把毒喂进库的两种手法);
|
|
17
|
-
* · 非 2xx / 非 JSON = 抛(status + body 前 200 字节,memory-sync transport 同款诊断姿势)。
|
|
18
|
-
*
|
|
19
|
-
* 抛出去之后由调用方处置:`PgMemoryEngineBackend.embeddingParam` 的长度门会让该行退回词面档,
|
|
20
|
-
* `search` 的词面腿 `.catch(() => null)` 会让检索继续跑 —— 即「embedder 坏了 = 退回 lexical」,
|
|
21
|
-
* 而不是「embedder 坏了 = 写坏向量」。
|
|
22
|
-
*
|
|
23
|
-
* ## 超时
|
|
24
|
-
*
|
|
25
|
-
* `AbortSignal.timeout(timeoutMs)`(缺省 30s,`MEMORY_EMBEDDER_TIMEOUT_MS` 覆盖,越界/坏值拒启)。
|
|
26
|
-
* 记忆写入路径是同步等 embed 的,挂死的端点会把整条 harvest 吊住(undici 默认 headers 超时 300s 太钝)。
|
|
27
|
-
*/
|
|
28
1
|
import { z } from "zod";
|
|
29
|
-
/** OpenAI `/v1/embeddings` 响应的**最小**契约:只读第一条向量,其余键(usage/object/model)不关心。 */
|
|
30
2
|
const EmbeddingsResponse = z.object({
|
|
31
3
|
data: z.array(z.object({ embedding: z.array(z.number()) })).min(1),
|
|
32
4
|
});
|
|
33
|
-
/**
|
|
34
|
-
* 端点归一:`MEMORY_EMBEDDER_ENDPOINT` 收两种写法 —— OpenAI 兼容**基址**(`https://x/v1`)或**完整**
|
|
35
|
-
* 端点(`https://x/v1/embeddings`)。两种都是 operator 手上真实存在的抄法,而一律硬拼 `/embeddings`
|
|
36
|
-
* 会产出 `/v1/embeddings/embeddings`(这条键最常见的手滑形)。归一不是降级:两条路都指向同一个真 URL,
|
|
37
|
-
* 没有任何一种输入被静默改成**别的**语义。
|
|
38
|
-
*
|
|
39
|
-
* ⚠️ 只动 `pathname`(codex 复审 F3,已核真):第一版在**整串**上做 `endsWith`/拼接,于是带 query 的
|
|
40
|
-
* 端点全错 —— Azure OpenAI 形 `https://h/v1/embeddings?api-version=1` 会被拼成 query 值里带
|
|
41
|
-
* `1/embeddings`,`https://h/v1?api-version=1` 则永远拿不到 `/embeddings` 路径。两种都过得了启动期的
|
|
42
|
-
* URL 合法性门,只在运行期炸(而运行期的炸法就是上面那条「写入失败/检索退档」的静默病)。
|
|
43
|
-
*/
|
|
44
5
|
export function embeddingsUrl(endpoint) {
|
|
45
|
-
const u = new URL(endpoint);
|
|
46
|
-
u.hash = "";
|
|
6
|
+
const u = new URL(endpoint);
|
|
7
|
+
u.hash = "";
|
|
47
8
|
const path = u.pathname.replace(/\/+$/, "");
|
|
48
|
-
// 「已经带尾巴了吗」按**解码后**的末段判(codex 二轮 F3,已核真):`/v1/%65mbeddings` 与
|
|
49
|
-
// `/v1/embeddings` 是同一条路径,只比对序列化文本会给前者再叠一层。只解 unreserved 转义
|
|
50
|
-
// (RFC 3986 的 `A-Za-z0-9-._~`)—— 其余转义原样保留,decodeURIComponent 的坏 `%` 抛错面也一并避开。
|
|
51
9
|
u.pathname = decodeUnreservedEscapes(path).endsWith("/embeddings") ? path : `${path}/embeddings`;
|
|
52
10
|
return u.toString();
|
|
53
11
|
}
|
|
54
|
-
/** 只把 unreserved 字符的百分号转义解回来(不抛、不改变其它转义)。 */
|
|
55
12
|
function decodeUnreservedEscapes(s) {
|
|
56
13
|
return s.replace(/%([0-9A-Fa-f]{2})/g, (whole, hex) => {
|
|
57
14
|
const c = String.fromCharCode(Number.parseInt(hex, 16));
|
|
58
15
|
return /[A-Za-z0-9\-._~]/.test(c) ? c : whole;
|
|
59
16
|
});
|
|
60
17
|
}
|
|
61
|
-
/**
|
|
62
|
-
* 诊断面用的**脱敏** URL(codex 复审 F4,已核真):端点里可能带 userinfo(`https://user:pass@h/v1`)
|
|
63
|
-
* 或把凭据放 query(Azure 的 `api-key=`),而错误文本会进日志、进 PatchReport 的 conflict reason。
|
|
64
|
-
* 主机与路径保留(诊断价值全在这里),userinfo 抹掉、query 值一律换成 `***`(键名保留,便于认形)。
|
|
65
|
-
*/
|
|
66
18
|
export function redactEndpointForLog(raw) {
|
|
67
19
|
if (!URL.canParse(raw))
|
|
68
|
-
return "<invalid-url>";
|
|
20
|
+
return "<invalid-url>";
|
|
69
21
|
const u = new URL(raw);
|
|
70
22
|
u.username = "";
|
|
71
23
|
u.password = "";
|
|
@@ -73,28 +25,20 @@ export function redactEndpointForLog(raw) {
|
|
|
73
25
|
u.searchParams.set(k, "***");
|
|
74
26
|
return u.toString();
|
|
75
27
|
}
|
|
76
|
-
/** 端点 query 上的**值**(Azure 形把 key 放 query;这些字面量要从任何诊断文本里洗掉)。 */
|
|
77
28
|
function credentialsInQuery(endpoint) {
|
|
78
29
|
if (!URL.canParse(endpoint))
|
|
79
30
|
return [];
|
|
80
31
|
return [...new URL(endpoint).searchParams.values()].filter((v) => v.length > 0);
|
|
81
32
|
}
|
|
82
|
-
/** 造一个 OpenAI 兼容的 `PgEmbedder`(工厂命名律:返回带行为的对象 ⇒ `create*`)。 */
|
|
83
33
|
export function createOpenAiCompatEmbedder(opts) {
|
|
84
34
|
const url = embeddingsUrl(opts.endpoint);
|
|
85
35
|
const shownUrl = redactEndpointForLog(url);
|
|
86
36
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
87
37
|
const { model, dimensions, timeoutMs, apiKey, onFailure } = opts;
|
|
88
|
-
// 洗白名单 = 本部署已知的**全部**凭据字面量:apiKey + 端点 query 上的值(Azure 形把 key 放 query;
|
|
89
|
-
// codex 二轮 F1)。userinfo 在 config 层就被拒了,这里不再单列。
|
|
90
38
|
const secrets = [...(apiKey !== undefined ? [apiKey] : []), ...credentialsInQuery(opts.endpoint)];
|
|
91
|
-
/** 供应商 body / 传输层错误进诊断文本前先洗一遍:回显自家凭据的代理/网关是真实存在的形。 */
|
|
92
39
|
const scrub = (raw) => secrets.reduce((acc, s) => acc.replaceAll(s, "***"), raw);
|
|
93
40
|
const safeSlice = (raw) => scrub(raw).slice(0, 200);
|
|
94
41
|
const call = async (text) => {
|
|
95
|
-
// 传输层失败(DNS/连不上/超时)也要走脱敏形(codex 二轮 F1):undici 的错误链里可能带上完整 URL,
|
|
96
|
-
// 而这条错会进 warn 日志与 PatchReport 的 conflict reason。**刻意不挂 cause**:挂上等于把没洗过的
|
|
97
|
-
// 原始文本从后门放回诊断面(打印错误链的人一样看得见),这里只带洗过的文本。
|
|
98
42
|
let res;
|
|
99
43
|
try {
|
|
100
44
|
res = await fetchImpl(url, {
|
|
@@ -113,8 +57,6 @@ export function createOpenAiCompatEmbedder(opts) {
|
|
|
113
57
|
const raw = await res.text().catch((readErr) => {
|
|
114
58
|
throw new Error(`memory embedder POST ${shownUrl}: response body unreadable — ${safeSlice(String(readErr))}`);
|
|
115
59
|
});
|
|
116
|
-
// 诊断面纪律:回显 status + body 前缀(端点的 typed error body 就是最好的线索),但 URL 走脱敏形、
|
|
117
|
-
// body 先洗掉全部已知凭据字面量 —— 错误文本会进日志与 PatchReport 的 conflict reason。
|
|
118
60
|
if (!res.ok)
|
|
119
61
|
throw new Error(`memory embedder POST ${shownUrl}: HTTP ${res.status} ${safeSlice(raw)}`);
|
|
120
62
|
let json;
|
|
@@ -141,25 +83,17 @@ export function createOpenAiCompatEmbedder(opts) {
|
|
|
141
83
|
return await call(text);
|
|
142
84
|
}
|
|
143
85
|
catch (err) {
|
|
144
|
-
// 先让部署面看见,再**原样**抛(观测腿绝不改变结果,连真错的属性都不碰)
|
|
145
86
|
try {
|
|
146
87
|
onFailure?.(err);
|
|
147
88
|
}
|
|
148
89
|
catch (hookErr) {
|
|
149
|
-
// 观测腿自己坏了:①不许顶替真故障(拿监控的病盖住业务的病是最坏的一种交换);②不许改动真错
|
|
150
|
-
// ——第一版往 err.cause 上挂,冻结/不可写的 err 会让**赋值本身**抛,于是 TypeError 顶替了
|
|
151
|
-
// 真错(codex 二轮 F2,已核真);③也不许无声 —— 走 process.emitWarning,不碰真错分毫。
|
|
152
90
|
process.emitWarning(`memory embedder onFailure hook threw: ${String(hookErr)}`, "MemoryEmbedderObservabilityWarning");
|
|
153
91
|
}
|
|
154
|
-
throw err;
|
|
92
|
+
throw err;
|
|
155
93
|
}
|
|
156
94
|
},
|
|
157
95
|
};
|
|
158
96
|
}
|
|
159
|
-
/**
|
|
160
|
-
* 装配腿:配置在场 ⇒ 造 embedder,缺席 ⇒ `undefined`(= core 的三档推断落回 lexical)。
|
|
161
|
-
* 这一层单独存在是为了让「注入真的接上了」可测 —— `openStores` 本身要真 DB 才跑得起来。
|
|
162
|
-
*/
|
|
163
97
|
export function memoryEmbedderFor(config, deps = {}) {
|
|
164
98
|
const cfg = config.memoryEmbedder;
|
|
165
99
|
if (cfg === undefined)
|
|
@@ -1,43 +1,15 @@
|
|
|
1
|
-
// ⚠️ ORIGIN: @sema-agent/core 1.267.0 `src/core/memory-engine/pg-backend.ts`(core commit 3071dd3)——
|
|
2
|
-
// 边界重切整段移交:core 从此只养 File backend+MemoryBackend seam+memoryBackendContract
|
|
3
|
-
// 契约套件;Pg/TiDB 方言实现归 service(数据面方言=deployment 半场,TiDBRunStore/PgRunStore 先例)。
|
|
4
|
-
// 移交纪律:保留此头注双向可追;core 下版撤 PgMemoryEngineBackend 公开导出。
|
|
5
|
-
// 改动 vs origin:import 面重指(types/frontmatter 从 @sema-agent/core 公开面;memory-vector 三函数+
|
|
6
|
-
// isUniqueViolation 内联于 memory-engine-vector-util.ts(core 1.424 示例适配器退包面后回归内联,勿再切 re-export)。
|
|
7
|
-
// design/138 S3-core — PgMemoryEngineBackend: the SECOND MemoryBackend implementation (TOB/multi-tenant
|
|
8
|
-
// authority disk), proving the S1 entry-transactional seam holds off the file plane. Same contract as
|
|
9
|
-
// FileMemoryEngineBackend; radically simpler internals, because Postgres retires the file backend's
|
|
10
|
-
// control-plane machinery instead of porting it (S3 拍板, design/138 §S3-core):
|
|
11
|
-
// - committed-rev ledger → the row's `rev` column IS the committed rev (no zero-copy problem: the
|
|
12
|
-
// model edits file PROJECTIONS, never DB rows, so "read current rev" cannot self-conflict);
|
|
13
|
-
// - redo journal → per-statement conditional writes (`UPDATE … WHERE rev = $expected
|
|
14
|
-
// RETURNING`, the pg.ts checkpoint OCC template) — the DB's atomicity replaces staging/journaling;
|
|
15
|
-
// - shadow/ quarantine/ → dropped: a DB row cannot go "missing" outside an explicit delete patch,
|
|
16
|
-
// so the "缺失≠删除" recovery axis has no Pg counterpart; inbound writes are engine-only (a
|
|
17
|
-
// deployment sharing the tables with other writers owns validation — same posture as stores/pg.ts);
|
|
18
|
-
// - scope registry → dropped: `scope` is a plain column; tenant keying is the deployment's.
|
|
19
|
-
// Vector rungs (design/81, re-mounted here per §4): lexical (jaccard, byte-equal to FileBackend) →
|
|
20
|
-
// portable (jsonb vectors, in-process cosine) → native (pgvector `<=>` SQL). All three share the
|
|
21
|
-
// cosine-distance contract score ∈ [0,2] — upgrading recall is a config change, not a recalibration.
|
|
22
1
|
import { cosineDistance, jaccardDistance, termSet } from "@sema-agent/core";
|
|
23
2
|
import { isUniqueViolation } from "./memory-engine-vector-util.js";
|
|
24
3
|
import { computeEntryRev, serializeEntryFile } from "@sema-agent/core";
|
|
25
4
|
import { pgSafeJsonStringify, pgHasUnstorable } from "./pg-safe-json.js";
|
|
26
|
-
import { assertMemoryScopeWidth, memoryEntryKeyWidthRefusal } from "./memory-key-guards.js";
|
|
27
|
-
// #307 件6(core 5.46.0 design/336 §2.3-2):external-origin 不可变律 —— 两支 SQL 孪生共用**一份**判据。
|
|
5
|
+
import { assertMemoryScopeWidth, memoryEntryKeyWidthRefusal } from "./memory-key-guards.js";
|
|
28
6
|
import { ambiguousOriginRefusal, createBatchCommittedBaseline, exposureBandOf, exposureCarriage, originMarkedSqlPredicate, originWhitewashRefusal, rememberBatchCommitted, distilledWhitewashRefusal, distilledCarriage, unknownPatchOpRefusal, } from "./memory-origin-law.js";
|
|
29
|
-
/** Table names (single source). Deliberately DISJOINT from the legacy `agent_memory*` tables —
|
|
30
|
-
* the retired MemoryStore plane and this entry plane must never cross-write. */
|
|
31
7
|
export const PG_MEMORY_ENGINE_TABLES = {
|
|
32
8
|
entry: "agent_memory_engine_entry",
|
|
33
9
|
cursor: "agent_memory_engine_cursor",
|
|
34
10
|
};
|
|
35
|
-
/** Idempotent DDL for the entry plane. Call once at startup (or run through your migration tool). */
|
|
36
11
|
export async function ensurePgMemoryEngineSchema(query, opts = {}) {
|
|
37
12
|
const vec = opts.memoryVector;
|
|
38
|
-
// dimensions must be a finite positive integer — NaN/Infinity would interpolate
|
|
39
|
-
// invalid DDL (`vector(NaN)`), and a silent fractional truncation could disagree with the embedder's
|
|
40
|
-
// declared dimensions. Boot-time config error ⇒ throw loudly, never emit broken DDL.
|
|
41
13
|
if (vec !== undefined && (!Number.isInteger(vec.dimensions) || vec.dimensions < 1)) {
|
|
42
14
|
throw new Error(`ensurePgMemoryEngineSchema: memoryVector.dimensions must be a positive integer (got ${String(vec.dimensions)})`);
|
|
43
15
|
}
|
|
@@ -59,27 +31,16 @@ export async function ensurePgMemoryEngineSchema(query, opts = {}) {
|
|
|
59
31
|
await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_ENGINE_TABLES.entry}_scope ON ${PG_MEMORY_ENGINE_TABLES.entry} (scope)`);
|
|
60
32
|
}
|
|
61
33
|
catch {
|
|
62
|
-
/* pg-mem: partial CREATE INDEX support — the index is a perf add-on, never correctness */
|
|
63
34
|
}
|
|
64
|
-
// The (scope, slug) UNIQUE constraint is CORRECTNESS, not perf — two
|
|
65
|
-
// instances racing an add of the same scope+slug (different ids) could both probe "free" and both
|
|
66
|
-
// insert, leaving two entries whose file projections overwrite each other at materialize. The DB
|
|
67
|
-
// constraint is the only true arbiter; the add path catches the violation and retries the next
|
|
68
|
-
// suffix. NOT wrapped in try/catch — if this constraint cannot be created, the backend is unsafe.
|
|
69
35
|
await query(`CREATE UNIQUE INDEX IF NOT EXISTS uq_${PG_MEMORY_ENGINE_TABLES.entry}_scope_slug ON ${PG_MEMORY_ENGINE_TABLES.entry} (scope, slug)`);
|
|
70
36
|
await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.cursor} (
|
|
71
37
|
scope varchar(190) COLLATE "C" PRIMARY KEY,
|
|
72
38
|
cursor text COLLATE "C" NOT NULL
|
|
73
39
|
)`);
|
|
74
40
|
}
|
|
75
|
-
/** pgvector input literal — the same `[…]` string JSON.parse reads back from jsonb (one format, two columns). */
|
|
76
41
|
function vectorLiteral(v) {
|
|
77
42
|
return `[${v.join(",")}]`;
|
|
78
43
|
}
|
|
79
|
-
/** `$k, $k+1, …` for a dynamic IN list. Used instead of `= ANY($1)` — pg-mem (the offline test rig)
|
|
80
|
-
* silently returns ZERO rows for `ANY(param)` on an INDEXED column (index-path bug, verified
|
|
81
|
-
* 2026-07-11: unindexed ANY works, indexed ANY is empty, indexed `=` works). IN-lists are plain SQL
|
|
82
|
-
* on both engines, so the production path never bends toward the emulator. */
|
|
83
44
|
function inPlaceholders(first, count) {
|
|
84
45
|
return Array.from({ length: count }, (_, i) => `$${first + i}`).join(", ");
|
|
85
46
|
}
|
|
@@ -92,9 +53,7 @@ export class PgMemoryEngineBackend {
|
|
|
92
53
|
pgvectorSql;
|
|
93
54
|
clock;
|
|
94
55
|
history;
|
|
95
|
-
/** Cumulative lost-row count — audit-line breakage must itself be auditable. */
|
|
96
56
|
historyRowsLost = 0;
|
|
97
|
-
/** design/81 rung indicator — same semantics as PgMemoryStore.vectorMode. */
|
|
98
57
|
get vectorMode() {
|
|
99
58
|
return this.pgvectorSql ? "native" : this.embedder ? "portable" : "lexical";
|
|
100
59
|
}
|
|
@@ -107,12 +66,9 @@ export class PgMemoryEngineBackend {
|
|
|
107
66
|
if (this.pgvectorSql && !this.embedder)
|
|
108
67
|
throw new Error("PgMemoryEngineBackend: pgvectorSql requires an embedder");
|
|
109
68
|
}
|
|
110
|
-
/** The embed/search haystack — BYTE-EQUAL to FileBackend's lexical haystack (file-backend search). */
|
|
111
69
|
haystackOf(slug, fm, body) {
|
|
112
70
|
return `${fm.name ?? slug} ${fm.description ?? ""} ${body}`;
|
|
113
71
|
}
|
|
114
|
-
/** Dimension-guarded embedding parameter (mirrors PgMemoryStore.embeddingParam — wrong-length ⇒ null,
|
|
115
|
-
* the row falls open to the lexical stand-in instead of poisoning reads). */
|
|
116
72
|
async embeddingParam(text) {
|
|
117
73
|
if (!this.embedder)
|
|
118
74
|
return null;
|
|
@@ -133,10 +89,7 @@ export class PgMemoryEngineBackend {
|
|
|
133
89
|
mtimeMs: Number(r.mtime_ms),
|
|
134
90
|
rev: r.rev,
|
|
135
91
|
sizeBytes: Number(r.size_bytes),
|
|
136
|
-
// #319(design/336 §5.2):committed external-origin 事实随**每一个**头面(listHeaders + search)。
|
|
137
|
-
// 归一化只有一份(memory-origin-law.exposureCarriage → core 的 committedOriginOf),两支方言同源。
|
|
138
92
|
...exposureCarriage(fm),
|
|
139
|
-
// #337(conformance c38,TiDB 孪生同注):distilled 事实(carrierRev + supersedes)随每一个头面。
|
|
140
93
|
...distilledCarriage(fm),
|
|
141
94
|
};
|
|
142
95
|
}
|
|
@@ -151,9 +104,6 @@ export class PgMemoryEngineBackend {
|
|
|
151
104
|
};
|
|
152
105
|
}
|
|
153
106
|
async listHeaders(scopes) {
|
|
154
|
-
// Dedupe (all three read paths): a repeated key must not repeat rows — real Postgres IN-lists
|
|
155
|
-
// don't, but pg-mem duplicates matches per repeated placeholder value; dedupe is also the honest
|
|
156
|
-
// semantic (a caller repeating an id asks about ONE entry).
|
|
157
107
|
const uniq = [...new Set(scopes)];
|
|
158
108
|
if (uniq.length === 0)
|
|
159
109
|
return [];
|
|
@@ -165,52 +115,31 @@ export class PgMemoryEngineBackend {
|
|
|
165
115
|
if (uniq.length === 0)
|
|
166
116
|
return [];
|
|
167
117
|
const res = await this.query(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id IN (${inPlaceholders(1, uniq.length)}) ORDER BY id`, uniq);
|
|
168
|
-
return res.rows.map((r) => this.entryFromRow(r));
|
|
118
|
+
return res.rows.map((r) => this.entryFromRow(r));
|
|
169
119
|
}
|
|
170
120
|
async search(query, scopes, opts) {
|
|
171
|
-
// #319(design/336 §5.2):两带序 —— 未标记先、已标记后,**带序在截断之前**(理由见
|
|
172
|
-
// memory-origin-law.exposureBandOf 的顶注)。缺席/false ⇒ 单带序,与本方法 pre-336 形逐字节同。
|
|
173
121
|
const bands = opts?.exposureBands === true;
|
|
174
|
-
/** 契约序:(带号 ‖ 缺席时恒 0) → score 升 → id。三档共用一只比较器,谁都别自己写一份。 */
|
|
175
122
|
const contractOrder = (a, b) => (bands ? exposureBandOf(a) - exposureBandOf(b) : 0) || a.score - b.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
176
|
-
// Normalize the limit ONCE for all three rungs — `Math.floor`
|
|
177
|
-
// makes a fractional limit (0.5) behave like File's `slice` (→ 0 → empty), `!(lim > 0)` also
|
|
178
|
-
// rejects NaN (NaN > 0 is false — it previously reached the native rung's interpolated LIMIT as
|
|
179
|
-
// a SQL syntax error). Infinity stays legal ("no cap") and is clamped before interpolation.
|
|
180
123
|
const rawLimit = opts?.limit ?? 20;
|
|
181
124
|
const limit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : rawLimit;
|
|
182
125
|
if (scopes.length === 0 || !(limit > 0))
|
|
183
126
|
return [];
|
|
184
|
-
// The EMPTY-QUERY contract is backend-uniform — File returns [] when the
|
|
185
|
-
// query has no terms, so the vector rungs must not answer it either (an embedder may happily
|
|
186
|
-
// embed "" and score every row). One guard ahead of ALL rungs.
|
|
187
127
|
const q = termSet(query);
|
|
188
128
|
if (q.size === 0)
|
|
189
129
|
return [];
|
|
190
|
-
// Native rung: rank in SQL. Excludes embedding-less rows (documented migration note).
|
|
191
130
|
if (this.pgvectorSql && this.embedder) {
|
|
192
131
|
const qv = await this.embedder.embed(query);
|
|
193
132
|
if (Array.isArray(qv) && qv.length === this.embedder.dimensions) {
|
|
194
133
|
const vecParam = scopes.length + 1;
|
|
195
|
-
// #319:本档的排序**和截断都在 SQL 里**,所以两带序也必须进 ORDER BY —— 在进程里对一页
|
|
196
|
-
// 已截断的结果重排是排不出来的(那正是契约要拒的「干净条目被饿死」)。带号谓词=不可变律
|
|
197
|
-
// 模块里那条 `committedOriginOf` 的下推形(逐元素跑 core 的行首正则,与 JS 判据机器对表;
|
|
198
|
-
// 为什么不能图省事写成 LIKE,见 originMarkedSqlPredicate 的顶注)。
|
|
199
134
|
const bandExpr = bands ? `(CASE WHEN ${originMarkedSqlPredicate("frontmatter")} THEN 1 ELSE 0 END) ASC, ` : "";
|
|
200
135
|
const res = await this.query(`SELECT id, scope, slug, frontmatter, rev, mtime_ms, size_bytes, embedding <=> $${vecParam}::vector AS dist
|
|
201
136
|
FROM ${PG_MEMORY_ENGINE_TABLES.entry}
|
|
202
137
|
WHERE scope IN (${inPlaceholders(1, scopes.length)}) AND embedding IS NOT NULL
|
|
203
138
|
ORDER BY ${bandExpr}embedding <=> $${vecParam}::vector ASC, id ASC
|
|
204
139
|
LIMIT ${Math.min(limit, 100_000)}`, [...scopes, vectorLiteral(qv)]);
|
|
205
|
-
// ⚠️ `id ASC` 必须写在 SQL 的 ORDER BY 里、也就是 LIMIT **之前**(codex 对抗复审 R1-[medium]):
|
|
206
|
-
// 同分(同一条向量距离)超过 limit 条时,决定「谁被截掉」的是这一句;缺了它,留下哪几条由执行
|
|
207
|
-
// 计划随机决定,页内怎么重排都换不回被截掉的那条。列是 `text COLLATE "C"` ⇒ 字节序,与下面
|
|
208
|
-
// 进程内比较器的定序同向。
|
|
209
140
|
const hits = res.rows.map((r) => ({ ...this.headerFromRow(r), score: Number(r.dist) }));
|
|
210
|
-
// 页内再排一次:契约序的最终裁决在进程侧(单带时是恒等重排)。
|
|
211
141
|
return hits.sort(contractOrder);
|
|
212
142
|
}
|
|
213
|
-
// Wrong-dimension query vector (transient embedder fault) — fall open to the in-process path.
|
|
214
143
|
}
|
|
215
144
|
const res = await this.query(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes, embedding FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE scope IN (${inPlaceholders(1, scopes.length)})`, [...scopes]);
|
|
216
145
|
const queryVec = this.embedder ? await this.embedder.embed(query).catch(() => null) : null;
|
|
@@ -219,8 +148,6 @@ export class PgMemoryEngineBackend {
|
|
|
219
148
|
for (const raw of res.rows) {
|
|
220
149
|
const r = raw;
|
|
221
150
|
const fm = fromJson(r.frontmatter);
|
|
222
|
-
// Portable rung: cosine over the stored jsonb vector when both sides are dimension-valid;
|
|
223
|
-
// any missing/invalid side falls open to the lexical floor for THIS row (never dropped).
|
|
224
151
|
if (queryVec && Array.isArray(queryVec) && queryVec.length === dims && r.embedding != null) {
|
|
225
152
|
const stored = fromJson(r.embedding);
|
|
226
153
|
if (Array.isArray(stored) && stored.length === dims) {
|
|
@@ -233,24 +160,14 @@ export class PgMemoryEngineBackend {
|
|
|
233
160
|
continue;
|
|
234
161
|
scored.push({ ...this.headerFromRow(r), score: d });
|
|
235
162
|
}
|
|
236
|
-
// opus 审 C4: deterministic id tie-break — tied scores must not order by row-arrival accident
|
|
237
|
-
// (the cross-backend equivalence assertion compares ordered projections; File mirrors this).
|
|
238
|
-
// #319:整档在进程内排完**再**切片 ⇒ 两带序天然落在截断之前(SQL 侧无 LIMIT 可绕过)。
|
|
239
163
|
scored.sort(contractOrder);
|
|
240
164
|
return scored.slice(0, limit);
|
|
241
165
|
}
|
|
242
166
|
async applyPatches(patches) {
|
|
243
167
|
const report = { applied: [], conflicts: [] };
|
|
244
|
-
// At most ONE non-delete op per id per batch (file-backend parity — a COPY keeping its frontmatter
|
|
245
|
-
// id would leave the projection↔applied binding ambiguous; refuse the later one).
|
|
246
168
|
const nonDeleteIds = new Set();
|
|
247
|
-
// In-batch (scope, slug) claims so two adds of one slug in one batch suffix deterministically.
|
|
248
169
|
const plannedSlugs = new Set();
|
|
249
|
-
// 142-S5.1: one history row per APPLIED patch, flushed as one batch below (append-only audit face).
|
|
250
170
|
const historyRows = [];
|
|
251
|
-
// #307 件6(design/336 §2.3-2):external-origin 不可变律的**批次基线**。本后端逐条 patch 直打真库,
|
|
252
|
-
// 所以同批 delete 之后再 add 时库里已无该行 —— 只看当下会把它误判成「合法新生命」。按本批第一次
|
|
253
|
-
// 触碰记下已提交 origin,整批按那一份判(理由与两半判据见 memory-origin-law.ts 顶注)。
|
|
254
171
|
const originBaseline = createBatchCommittedBaseline();
|
|
255
172
|
for (const patch of patches) {
|
|
256
173
|
try {
|
|
@@ -267,17 +184,11 @@ export class PgMemoryEngineBackend {
|
|
|
267
184
|
report.conflicts.push({ op: patch.op, id: patch.id, reason: `io error: ${err instanceof Error ? err.message : String(err)}` });
|
|
268
185
|
}
|
|
269
186
|
}
|
|
270
|
-
// BEST-EFFORT history append (see PgMemoryEngineBackendOptions.history for the honest trade-off:
|
|
271
|
-
// the entry writes above are already committed; a failed audit append must not fail the report —
|
|
272
|
-
// swallow + warn, never throw. appendHistory is (id,rev)-idempotent, so a caller retry is safe.
|
|
273
187
|
if (this.history && historyRows.length > 0) {
|
|
274
188
|
try {
|
|
275
189
|
await this.history.appendHistory(historyRows);
|
|
276
190
|
}
|
|
277
191
|
catch (err) {
|
|
278
|
-
// Every warn carries the CUMULATIVE lost count — a persistent silent failure
|
|
279
|
-
// (history table dropped, grant flipped) shows as a growing number, not a one-off line.
|
|
280
|
-
// The metrics counter leg lives in the main.ts sink wrapper (memory_history_append_failed_total).
|
|
281
192
|
this.historyRowsLost += historyRows.length;
|
|
282
193
|
console.warn(`PgMemoryEngineBackend: best-effort history append failed (${historyRows.length} row(s) lost, ${this.historyRowsLost} cumulative — audit face only, entries committed): ${err instanceof Error ? err.message : String(err)}`);
|
|
283
194
|
}
|
|
@@ -286,9 +197,6 @@ export class PgMemoryEngineBackend {
|
|
|
286
197
|
}
|
|
287
198
|
async applyOne(patch, report, plannedSlugs, historyRows, originBaseline) {
|
|
288
199
|
const T = PG_MEMORY_ENGINE_TABLES.entry;
|
|
289
|
-
// #307 件6(codex R1-[high]):op 闭集**前置**判 —— 词表外的 op 会跳过两道白洗守卫(它们都写成
|
|
290
|
-
// `if (patch.op === "update")`)、落进 update 的 SQL 并报成一次 applied。零 I/O 拒绝(理由与
|
|
291
|
-
// 今日可达性见 memory-origin-law.ts 的 unknownPatchOpRefusal 顶注;tidb 孪生同处同判)。
|
|
292
200
|
{
|
|
293
201
|
const refusal = unknownPatchOpRefusal(patch.op);
|
|
294
202
|
if (refusal !== undefined) {
|
|
@@ -296,18 +204,10 @@ export class PgMemoryEngineBackend {
|
|
|
296
204
|
return;
|
|
297
205
|
}
|
|
298
206
|
}
|
|
299
|
-
// A patch whose entry carries a DIFFERENT id than the patch addresses is
|
|
300
|
-
// malformed — the two backends would otherwise diverge (File writes the entry's id, Pg would
|
|
301
|
-
// key the SELECT on patch.id but the write on entry.id). Refused up front, both backends alike.
|
|
302
207
|
if (patch.op !== "delete" && patch.entry !== undefined && patch.entry.id !== patch.id) {
|
|
303
208
|
report.conflicts.push({ op: patch.op, id: patch.id, reason: `patch id ${JSON.stringify(patch.id)} != entry id ${JSON.stringify(patch.entry.id)} (malformed patch refused)` });
|
|
304
209
|
return;
|
|
305
210
|
}
|
|
306
|
-
// #271 件1(R-1):键宽在**任何 I/O 之前**判,add/update 两条腿一处收(tidb 版同注)。R5 那批只
|
|
307
|
-
// 护住了 add 腿的 slug ⇒ 一次改名(update `SET scope=…, slug=…`)就能把超宽键送进 DB;而超宽键
|
|
308
|
-
// 的两个结局都坏——PG 落不可分类的裸 22001,非严格 MySQL **静默截断**(截断后的键指向**另一个**
|
|
309
|
-
// scope/slug:两租户的盘折叠 / 两条目互串)。拒绝式,与 unstorable_bytes 同族(后端能力差异诚实
|
|
310
|
-
// 暴露,重试恒同答=幂等成立);写语句一条都不发出去。
|
|
311
211
|
if (patch.op !== "delete" && patch.entry !== undefined) {
|
|
312
212
|
const refusal = memoryEntryKeyWidthRefusal(patch.entry.scope, patch.entry.slug);
|
|
313
213
|
if (refusal !== undefined) {
|
|
@@ -315,9 +215,6 @@ export class PgMemoryEngineBackend {
|
|
|
315
215
|
return;
|
|
316
216
|
}
|
|
317
217
|
}
|
|
318
|
-
// #319 增量([4768] 勘误 + core `1a5ebaa1`):**歧义 origin 表示**零写拒。判位与 File 的 planOne
|
|
319
|
-
// 逐字同 —— 表示合法性先于状态比较:排在键形守卫之后、add/update 两支的 guard/跨 scope/白洗/CAS
|
|
320
|
-
// 之前,且在任何 I/O 之前。判据用 core 导出的谓词(禁手抄),文案照抄 File(跨后端同一输入同一答复)。
|
|
321
218
|
if (patch.op !== "delete" && patch.entry !== undefined) {
|
|
322
219
|
const refusal = ambiguousOriginRefusal(patch.entry.frontmatter);
|
|
323
220
|
if (refusal !== undefined) {
|
|
@@ -331,28 +228,13 @@ export class PgMemoryEngineBackend {
|
|
|
331
228
|
report.conflicts.push({ op: "add", id: patch.id, reason: "add patch without an entry" });
|
|
332
229
|
return;
|
|
333
230
|
}
|
|
334
|
-
// codex R2-H3→R3(终形):**拒绝式**——sync 协议的 rev/CAS/幂等全部对拍原文,存储层任何改写
|
|
335
|
-
// (有损或 canonical)都让 ack rev≠库内 rev(响应丢失重试=假冲突,R3 实锚)。含不可存字节的
|
|
336
|
-
// entry 在 PG 后端 fail-loud 报 conflict(reason 指路客户端清源);File 后端能存=后端能力差,
|
|
337
|
-
// 诚实暴露优于静默改写。重试恒同答=幂等成立。
|
|
338
231
|
if (pgHasUnstorable(rawEntry.frontmatter) || pgHasUnstorable(rawEntry.body) || pgHasUnstorable(rawEntry.slug)) {
|
|
339
232
|
report.conflicts.push({ op: "add", id: rawEntry.id, reason: "unstorable_bytes (PG cannot store NUL/lone surrogates; strip them at the source — the store never rewrites content)" });
|
|
340
233
|
return;
|
|
341
234
|
}
|
|
342
|
-
// (R5 的 add-only slug 守卫已上提到 applyOne 顶部的键宽块 —— add/update 同判,见那里的注。)
|
|
343
235
|
const entry = rawEntry;
|
|
344
|
-
// opus 审 C3: an add whose id already lives in a DIFFERENT scope is refused explicitly — the bare
|
|
345
|
-
// `ON CONFLICT (id) DO UPDATE SET scope=…` would silently MOVE the row across scopes (and diverge
|
|
346
|
-
// from the File backend, which never scans other scopes on add). The engine's call surface never
|
|
347
|
-
// produces this input (harvest/materialize are scope-local); a conflict report is the honest
|
|
348
|
-
// fail-closed answer. Same-scope re-add stays the idempotent overwrite (File parity).
|
|
349
|
-
// One probe serves BOTH the E-02 guard below and the cross-scope refusal (File-backend parity).
|
|
350
236
|
const prior = await this.query(`SELECT scope, slug, rev, frontmatter FROM ${T} WHERE id = $1`, [entry.id]);
|
|
351
237
|
const priorRow = prior.rows.length > 0 ? prior.rows[0] : undefined;
|
|
352
|
-
// #307 件6(design/336 §2.3-2)—— external-origin 不可变律,**判在 guard 之前**:core File backend
|
|
353
|
-
// 的 planOne 也是先判 origin 再判 `guard:"absent"`,而契约试剂盒对「带 guard 的洗白 add」要的是
|
|
354
|
-
// `/malformed patch refused/`;顺序反了会先报 `add_guard_absent_conflict`,同一输入两后端两个答复。
|
|
355
|
-
// 基线取「本批第一次触碰」那一份(同批 delete 之后的 re-add 照样被这条拒;已提交的墓碑才是合法出口)。
|
|
356
238
|
{
|
|
357
239
|
const committedBaseFm = rememberBatchCommitted(originBaseline, entry.id, priorRow !== undefined ? fromJson(priorRow.frontmatter) : undefined);
|
|
358
240
|
const refusal = originWhitewashRefusal("add", committedBaseFm, entry.frontmatter);
|
|
@@ -360,19 +242,12 @@ export class PgMemoryEngineBackend {
|
|
|
360
242
|
report.conflicts.push({ op: "add", id: entry.id, reason: refusal });
|
|
361
243
|
return;
|
|
362
244
|
}
|
|
363
|
-
// #337(conformance c36,design/339,TiDB 孪生同注):distilled 不可变律的 add 腿 —— 与 origin
|
|
364
|
-
// 律同基线同判位(guard 之前;同批 delete+re-add 靠基线拒,已提交墓碑后的 add 放行)。
|
|
365
245
|
const dRefusal = distilledWhitewashRefusal("add", committedBaseFm, entry.frontmatter);
|
|
366
246
|
if (dRefusal !== undefined) {
|
|
367
247
|
report.conflicts.push({ op: "add", id: entry.id, reason: dRefusal });
|
|
368
248
|
return;
|
|
369
249
|
}
|
|
370
250
|
}
|
|
371
|
-
// E-02 add-if-absent guard (core 1.275.1/1.276.0 contract clause, design/142 S2.5): a
|
|
372
|
-
// `guard:"absent"` add must NEVER blind-overwrite an existing id — same rev AND same
|
|
373
|
-
// (scope, slug) projection = idempotent fall-through; different rev OR different projection =
|
|
374
|
-
// REPORTED `add_guard_absent_conflict` carrying currentRev (File backend semantics verbatim;
|
|
375
|
-
// caught live by the contract suite on real TiDB 2026-07-13 — the twins had no guard leg).
|
|
376
251
|
if (patch.guard === "absent" && priorRow) {
|
|
377
252
|
const currentRev = String(priorRow.rev);
|
|
378
253
|
if (currentRev !== computeEntryRev(entry)) {
|
|
@@ -383,7 +258,6 @@ export class PgMemoryEngineBackend {
|
|
|
383
258
|
report.conflicts.push({ op: "add", id: entry.id, reason: `add_guard_absent_conflict (id already exists at the same rev but a different projection ${JSON.stringify(`${String(priorRow.scope)}/${String(priorRow.slug)}`)}; add-if-absent refuses to duplicate)`, currentRev });
|
|
384
259
|
return;
|
|
385
260
|
}
|
|
386
|
-
// same rev AND same projection ⇒ idempotent; fall through to the normal add (no-op re-write).
|
|
387
261
|
}
|
|
388
262
|
if (priorRow) {
|
|
389
263
|
const held = String(priorRow.scope);
|
|
@@ -392,18 +266,10 @@ export class PgMemoryEngineBackend {
|
|
|
392
266
|
return;
|
|
393
267
|
}
|
|
394
268
|
}
|
|
395
|
-
|
|
396
|
-
// (file-backend parity). In-batch planned slugs count as taken too.
|
|
397
|
-
// The SELECT probe alone is a race window — two instances can both observe "free" and both
|
|
398
|
-
// insert one slug. The INSERT itself sits INSIDE the loop: a (scope,slug) unique-violation from
|
|
399
|
-
// the concurrent loser retries the next suffix, so the DB constraint (ensureSchema) is the one
|
|
400
|
-
// true arbiter and the probe is only an optimization.
|
|
401
|
-
const cEntry = entry; // R3 终形:拒绝式守卫后恒干净
|
|
269
|
+
const cEntry = entry;
|
|
402
270
|
const embedding = await this.embeddingParam(this.haystackOf(cEntry.slug, cEntry.frontmatter, cEntry.body));
|
|
403
271
|
let slug = cEntry.slug;
|
|
404
272
|
for (let n = 2;; n++) {
|
|
405
|
-
// #271 件1(codex 对抗复审 H1;tidb 版同注):**派生**候选也要吃守卫 —— 顶部那道只判原始 slug,
|
|
406
|
-
// 而撞键派生的 `${slug}-${n}` 可以越过列宽(511 + "-2" = 513),此前直奔 INSERT 落裸 22001。
|
|
407
273
|
const derivedRefusal = memoryEntryKeyWidthRefusal(entry.scope, slug);
|
|
408
274
|
if (derivedRefusal !== undefined) {
|
|
409
275
|
report.conflicts.push({ op: "add", id: entry.id, reason: `${derivedRefusal} (slug collision suffix "-${n - 1}" no longer fits the column — shorten the entry name)` });
|
|
@@ -424,14 +290,12 @@ export class PgMemoryEngineBackend {
|
|
|
424
290
|
catch (err) {
|
|
425
291
|
if (isUniqueViolation(err)) {
|
|
426
292
|
slug = `${entry.slug}-${n}`;
|
|
427
|
-
continue;
|
|
293
|
+
continue;
|
|
428
294
|
}
|
|
429
295
|
throw err;
|
|
430
296
|
}
|
|
431
297
|
plannedSlugs.add(key);
|
|
432
298
|
report.applied.push({ op: "add", id: stored.id, slug: stored.slug });
|
|
433
|
-
// History: chain head — prevRev absent by contract (a same-scope idempotent re-add of
|
|
434
|
-
// identical content dedupes on the (id, rev) PK; changed content starts a fresh head).
|
|
435
299
|
if (this.history)
|
|
436
300
|
historyRows.push({ id: stored.id, rev, op: "add", scope: stored.scope, snapshot: { ...stored, rev }, writtenAtMs: this.clock() });
|
|
437
301
|
return;
|
|
@@ -440,8 +304,6 @@ export class PgMemoryEngineBackend {
|
|
|
440
304
|
slug = `${entry.slug}-${n}`;
|
|
441
305
|
}
|
|
442
306
|
}
|
|
443
|
-
// update/delete address an EXISTING entry by immutable id. (scope in the projection = the
|
|
444
|
-
// history row's scope for delete — a tombstone can't be joined back to the deleted row.)
|
|
445
307
|
const cur = await this.query(`SELECT slug, rev, scope, frontmatter FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id = $1`, [patch.id]);
|
|
446
308
|
if (cur.rows.length === 0) {
|
|
447
309
|
report.conflicts.push({ op: patch.op, id: patch.id, reason: "unknown id (entry not found in backend)" });
|
|
@@ -450,13 +312,7 @@ export class PgMemoryEngineBackend {
|
|
|
450
312
|
const currentRev = String(cur.rows[0].rev);
|
|
451
313
|
const currentSlug = String(cur.rows[0].slug);
|
|
452
314
|
const currentScope = String(cur.rows[0].scope);
|
|
453
|
-
// Read-side whitewash refusal (core 1.302 contract I-2, File-backend parity incl. ordering: judged
|
|
454
|
-
// BEFORE the rev CAS): an update may not strip provenance/trust off a repo-ingested entry. Judged on
|
|
455
|
-
// the committed frontmatter of THIS SELECT — a write racing past it flips rev and the conditional
|
|
456
|
-
// write below re-reports as a CAS conflict, so the window stays closed.
|
|
457
315
|
const committedFm = fromJson(cur.rows[0].frontmatter);
|
|
458
|
-
// #307 件6:**delete 腿也要记基线** —— 同批「delete 掉一条带标记的行、再 add 一条不带标记的」正是
|
|
459
|
-
// 洗白的最直接写法,而 delete 一旦落库,后面那条 add 的 SELECT 就什么都看不到了。
|
|
460
316
|
const committedBaseFm = rememberBatchCommitted(originBaseline, patch.id, committedFm);
|
|
461
317
|
if (patch.op === "update" && patch.entry !== undefined) {
|
|
462
318
|
if (committedFm.provenance?.kind === "repo_file") {
|
|
@@ -472,15 +328,11 @@ export class PgMemoryEngineBackend {
|
|
|
472
328
|
return;
|
|
473
329
|
}
|
|
474
330
|
}
|
|
475
|
-
// #307 件6:origin 不可变律的 update 腿。顺序与 core File backend 逐字同 —— repo_file 白洗判据
|
|
476
|
-
// 在前、origin 在后、两者都在 rev CAS 之前(judged on the committed frontmatter of THIS SELECT)。
|
|
477
331
|
const refusal = originWhitewashRefusal("update", committedBaseFm, patch.entry.frontmatter);
|
|
478
332
|
if (refusal !== undefined) {
|
|
479
333
|
report.conflicts.push({ op: "update", id: patch.id, reason: refusal });
|
|
480
334
|
return;
|
|
481
335
|
}
|
|
482
|
-
// #337(conformance c36/c37,TiDB 孪生同注):distilled 不可变律的 update 腿 —— 同样先于 rev CAS
|
|
483
|
-
// (c37:骑着 stale baseRev 的 strip 答 malformed refusal,不是 rev mismatch)。
|
|
484
336
|
const dRefusal = distilledWhitewashRefusal("update", committedBaseFm, patch.entry.frontmatter);
|
|
485
337
|
if (dRefusal !== undefined) {
|
|
486
338
|
report.conflicts.push({ op: "update", id: patch.id, reason: dRefusal });
|
|
@@ -488,28 +340,16 @@ export class PgMemoryEngineBackend {
|
|
|
488
340
|
}
|
|
489
341
|
}
|
|
490
342
|
if (patch.baseRev !== undefined && patch.baseRev !== currentRev) {
|
|
491
|
-
// Per-id CAS (§2.4 并发): NEVER blind-write over a concurrent change. No shadow-restore leg here —
|
|
492
|
-
// the DB row IS the committed state; nothing on a disk projection can shadow it.
|
|
493
|
-
// ABA note (opus 审 C1): rev is a CONTENT hash, so an A→B→A round-trip by another writer makes a
|
|
494
|
-
// stale baseRev "current" again and this CAS passes. Benign by construction — same hash ⇒ same
|
|
495
|
-
// canonical content, the overwrite is idempotent; only the intervening-write SIGNAL is lost.
|
|
496
|
-
// Shared with the File backend (same computeEntryRev), inherent to content-hash CAS.
|
|
497
343
|
report.conflicts.push({ op: patch.op, id: patch.id, reason: "rev mismatch (concurrent change)", baseRev: patch.baseRev, currentRev });
|
|
498
344
|
return;
|
|
499
345
|
}
|
|
500
346
|
if (patch.op === "delete") {
|
|
501
|
-
// Conditional on the rev we just judged — a write that lands between the SELECT and this DELETE
|
|
502
|
-
// re-reports as the same CAS conflict instead of silently deleting the newer committed state.
|
|
503
347
|
const res = await this.query(`DELETE FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id = $1 AND rev = $2 RETURNING id`, [patch.id, currentRev]);
|
|
504
348
|
if (res.rows.length === 0) {
|
|
505
349
|
report.conflicts.push({ op: "delete", id: patch.id, reason: "rev mismatch (concurrent change)", baseRev: patch.baseRev, currentRev: await this.freshRev(patch.id, currentRev) });
|
|
506
350
|
return;
|
|
507
351
|
}
|
|
508
352
|
report.applied.push({ op: "delete", id: patch.id, slug: currentSlug });
|
|
509
|
-
// History tombstone. rev must be DISTINCT from the deleted version's rev — the (id, rev) PK
|
|
510
|
-
// already holds a row for currentRev (the add/update that committed it), and ON CONFLICT DO
|
|
511
|
-
// NOTHING would silently drop a colliding tombstone. Deterministic derived key: revs are
|
|
512
|
-
// 16-hex content hashes (computeEntryRev), so the prefixed form stays well under varchar(128).
|
|
513
353
|
if (this.history)
|
|
514
354
|
historyRows.push({ id: patch.id, rev: `tombstone:${currentRev}`, prevRev: currentRev, op: "delete", scope: currentScope, writtenAtMs: this.clock() });
|
|
515
355
|
return;
|
|
@@ -523,7 +363,7 @@ export class PgMemoryEngineBackend {
|
|
|
523
363
|
report.conflicts.push({ op: "update", id: patch.id, reason: "unstorable_bytes (PG cannot store NUL/lone surrogates; strip them at the source — the store never rewrites content)" });
|
|
524
364
|
return;
|
|
525
365
|
}
|
|
526
|
-
const cEntry = entry;
|
|
366
|
+
const cEntry = entry;
|
|
527
367
|
const rev = computeEntryRev(cEntry);
|
|
528
368
|
const embedding = await this.embeddingParam(this.haystackOf(cEntry.slug, cEntry.frontmatter, cEntry.body));
|
|
529
369
|
const res = await this.query(`UPDATE ${PG_MEMORY_ENGINE_TABLES.entry}
|
|
@@ -535,13 +375,9 @@ export class PgMemoryEngineBackend {
|
|
|
535
375
|
return;
|
|
536
376
|
}
|
|
537
377
|
report.applied.push({ op: "update", id: entry.id, slug: entry.slug });
|
|
538
|
-
// History: rev-chain link — snapshot carries the committed entry (rev = the NEW content hash).
|
|
539
378
|
if (this.history)
|
|
540
379
|
historyRows.push({ id: entry.id, rev, prevRev: currentRev, op: "update", scope: entry.scope, snapshot: { ...entry, rev }, writtenAtMs: this.clock() });
|
|
541
380
|
}
|
|
542
|
-
/** When the conditional write returns zero rows (a write landed inside the
|
|
543
|
-
* SELECT→write window), the report's `currentRev` must be the rev NOW — re-reporting the pre-race
|
|
544
|
-
* SELECT value could claim `baseRev === currentRev` alongside a "rev mismatch" reason. */
|
|
545
381
|
async freshRev(id, fallback) {
|
|
546
382
|
const res = await this.query(`SELECT rev FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id = $1`, [id]);
|
|
547
383
|
return res.rows.length > 0 ? String(res.rows[0].rev) : fallback;
|
|
@@ -551,7 +387,7 @@ export class PgMemoryEngineBackend {
|
|
|
551
387
|
return res.rows.length > 0 ? String(res.rows[0].cursor) : undefined;
|
|
552
388
|
}
|
|
553
389
|
async setConsolidationCursor(scope, cursor) {
|
|
554
|
-
assertMemoryScopeWidth(scope);
|
|
390
|
+
assertMemoryScopeWidth(scope);
|
|
555
391
|
await this.query(`INSERT INTO ${PG_MEMORY_ENGINE_TABLES.cursor} (scope, cursor) VALUES ($1, $2)
|
|
556
392
|
ON CONFLICT (scope) DO UPDATE SET cursor = $2`, [scope, cursor]);
|
|
557
393
|
}
|