@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,15 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LocalTaskAttachmentStore —— D-1 附件店的 local 形(seamless-local-cloud 铁律:StoreBackend
|
|
3
|
-
* kind tidb|pg|local 三态,附件面不得 cloud-only)。
|
|
4
|
-
*
|
|
5
|
-
* 布局:`<root>/attachments/<id>.json`(meta)+ `<id>.bin`(bytes)。id=uuidv7(server 铸,不含用户
|
|
6
|
-
* 输入 ⇒ 文件名安全);meta 内含 owner/session 轴,门在读侧比对(单机单进程,无并发租户;SQL 店才是
|
|
7
|
-
* 多租户形)。写=tmp+rename 原子(半写行不可见);索引=首次使用时全目录水合进内存 Map,之后同进程
|
|
8
|
-
* 内存权威(local 模式规模小;跨进程并发不在 local 契约内——与 file-run-store 同口径)。
|
|
9
|
-
*/
|
|
10
1
|
import { promises as fsp } from "node:fs";
|
|
11
2
|
import { join } from "node:path";
|
|
12
|
-
const ID_RE = /^[0-9a-f-]{16,64}$/i;
|
|
3
|
+
const ID_RE = /^[0-9a-f-]{16,64}$/i;
|
|
13
4
|
export class LocalTaskAttachmentStore {
|
|
14
5
|
dir;
|
|
15
6
|
metas;
|
|
@@ -18,8 +9,6 @@ export class LocalTaskAttachmentStore {
|
|
|
18
9
|
this.dir = join(rootDir, "attachments");
|
|
19
10
|
}
|
|
20
11
|
hydrate() {
|
|
21
|
-
// #131-1:记「在飞的 promise」而非「结果」(同仓正确形=local-session-store.loaded)。记结果时,
|
|
22
|
-
// 同进程两条并发首触各自 readdir,晚到的空快照会覆盖已被 put() 写入的索引——盘上有档、内存答 null。
|
|
23
12
|
this.hydration ??= (async () => {
|
|
24
13
|
const m = new Map();
|
|
25
14
|
try {
|
|
@@ -35,12 +24,10 @@ export class LocalTaskAttachmentStore {
|
|
|
35
24
|
m.set(id, meta);
|
|
36
25
|
}
|
|
37
26
|
catch {
|
|
38
|
-
/* 半写/损坏 meta:跳过(bin 无 meta 即孤儿,reap 收) */
|
|
39
27
|
}
|
|
40
28
|
}
|
|
41
29
|
}
|
|
42
30
|
catch {
|
|
43
|
-
/* 目录不存在 = 空店 */
|
|
44
31
|
}
|
|
45
32
|
this.metas = m;
|
|
46
33
|
return m;
|
|
@@ -55,7 +42,6 @@ export class LocalTaskAttachmentStore {
|
|
|
55
42
|
const metaTmp = join(this.dir, `${rec.id}.json.tmp`);
|
|
56
43
|
await fsp.writeFile(binTmp, Buffer.from(content));
|
|
57
44
|
await fsp.rename(binTmp, join(this.dir, `${rec.id}.bin`));
|
|
58
|
-
// meta 后落:meta 在场 ⇒ bytes 必在场(读侧无半态)
|
|
59
45
|
await fsp.writeFile(metaTmp, JSON.stringify(meta));
|
|
60
46
|
await fsp.rename(metaTmp, join(this.dir, `${rec.id}.json`));
|
|
61
47
|
metas.set(rec.id, { ...meta });
|
|
@@ -72,7 +58,7 @@ export class LocalTaskAttachmentStore {
|
|
|
72
58
|
return new Uint8Array(await fsp.readFile(join(this.dir, `${id}.bin`)));
|
|
73
59
|
}
|
|
74
60
|
catch {
|
|
75
|
-
return null;
|
|
61
|
+
return null;
|
|
76
62
|
}
|
|
77
63
|
}
|
|
78
64
|
async persistMeta(m) {
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { pgHasUnstorable, pgSanitizeText, PgUnstorableError } from "./pg-safe-json.js";
|
|
2
2
|
export const MAILBOX_TABLE = "mailbox";
|
|
3
3
|
export const MAILBOX_MSG_TABLE = "mailbox_message";
|
|
4
|
-
/** InMemory append 同文案形(default-deny 谓词精神:无 scope 的盒=路由不明,写入即拒)。 */
|
|
5
4
|
function assertScope(scope) {
|
|
6
5
|
if (scope === undefined || scope === "") {
|
|
7
6
|
throw new Error("MailboxStore.append: refusing a message without a scope");
|
|
8
7
|
}
|
|
9
8
|
}
|
|
10
|
-
/** 键长守卫(复审 F5):TiDB `INSERT IGNORE` 对超 190 字节键**静默截断**(warning 降级)——截断键
|
|
11
|
-
* 与全长参数的字节比对恒 miss(seq 分配读空行 TypeError / 隔离键错位);PG 侧是干净 22001,同点
|
|
12
|
-
* 提前拒 = 双方言同文案 fail-loud。 */
|
|
13
|
-
/** D4(试剂盒揪出,RB-251 语义):身份键含 NUL/控制符——TiDB VARBINARY 能存但与文件头「两后端一致
|
|
14
|
-
* 拒绝」的声明不符(该声明此前是谎);写入面双方言同精神拒绝(PG 臂经 PgUnstorableError 已拒)。 */
|
|
15
9
|
function assertIdentityChars(what, v) {
|
|
16
10
|
if ([...v].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f)) {
|
|
17
11
|
throw new Error(`MailboxStore: ${what} contains control characters (refusing — identity keys must be byte-exact addressable)`);
|
|
@@ -25,25 +19,7 @@ function assertKeyBytes(what, v) {
|
|
|
25
19
|
function bufToStr(v) {
|
|
26
20
|
return Buffer.isBuffer(v) ? v.toString("utf8") : String(v);
|
|
27
21
|
}
|
|
28
|
-
/** `hopChain` 的 JSON 文本列上界(TiDB `TEXT` = 65535 字节)。core 侧链本就有界
|
|
29
|
-
* (`PEER_ADMISSION_DEFAULTS.maxChainLength` = 28,`PEER_HOP_CHAIN_WINDOW` = 32,元素是 peerAxisToken
|
|
30
|
-
* 短串)⇒ 正常形离这个数几个数量级远;守卫存在的理由是**关掉 MySQL 的静默截断类**(非严格模式下
|
|
31
|
-
* 超长 TEXT 写入只发 warning),截断后的 JSON 文本读侧 parse 失败 = 整条链静默变缺席,而缺席在
|
|
32
|
-
* mailbox 上是有语义的(见下 encodeHopChain 注)。同 assertKeyBytes 精神:写入面 fail-loud。 */
|
|
33
22
|
const HOP_CHAIN_MAX_BYTES = 60_000;
|
|
34
|
-
/**
|
|
35
|
-
* `hopChain` 存取(core 5.17.0 design/176)。**JSON 文本列**,不是清洗面也不是身份键面:
|
|
36
|
-
*
|
|
37
|
-
* - 为什么不清洗:链元素参与 core 的 `hop_loop` **等值比对**。内容面的 lossy 清洗(pgSanitizeText 的
|
|
38
|
-
* U+FFFD 标记)会让两个不同 token 塌成同一个(误报环路)或让一个 token 变形(漏判环路)——两个
|
|
39
|
-
* 方向都是把准入判决改错,与 `content` 那种「读者读到近似文本仍可用」的容忍面性质不同。
|
|
40
|
-
* - 为什么也不需要拒绝:`JSON.stringify` 是 **well-formed**(ES2019)——NUL 转义成 `\u0000`、lone
|
|
41
|
-
* surrogate 转义成 `\udXXX`,产物恒是 ASCII 可存字节。于是 PG 的 unstorable 面根本不会被触及,
|
|
42
|
-
* 两方言都拿到逐字 round-trip,不必像 `from`/`scope`/`handle` 那样走拒绝式。
|
|
43
|
-
* - **缺席 ≠ 空数组**:core 明示 mailbox 的 `hopChain` 在场性本身携带语义(「经 sema 准入门录入」vs
|
|
44
|
-
* 「外部/存量写入」,是整份契约里唯一允许从在场性推断的例外)。所以 NULL 读回 = 不铸键,`[]` 读回
|
|
45
|
-
* = 铸一个空数组键,两者绝不互相折叠。
|
|
46
|
-
*/
|
|
47
23
|
function encodeHopChain(hopChain) {
|
|
48
24
|
if (hopChain === undefined)
|
|
49
25
|
return null;
|
|
@@ -53,8 +29,6 @@ function encodeHopChain(hopChain) {
|
|
|
53
29
|
}
|
|
54
30
|
return text;
|
|
55
31
|
}
|
|
56
|
-
/** 读侧:NULL/缺席 ⇒ undefined(不铸键);坏行(非 JSON / 非字符串数组)⇒ 同样 undefined —— 一条读不
|
|
57
|
-
* 懂的链只能诚实降级成「没有链」,绝不半解析出一条会被拿去做环路比对的残链。 */
|
|
58
32
|
function decodeHopChain(raw) {
|
|
59
33
|
if (raw === null || raw === undefined)
|
|
60
34
|
return undefined;
|
|
@@ -80,8 +54,6 @@ function rowToMessage(r) {
|
|
|
80
54
|
};
|
|
81
55
|
}
|
|
82
56
|
export async function ensureTiDBMailboxSchema(pool) {
|
|
83
|
-
// VARBINARY 键列(bg-agent/roster 同案);lease_owner 同为字节等价(claim 的 owner 比对是身份判)。
|
|
84
|
-
// content LONGTEXT:消息正文可超 64K TEXT(转录段落/大 diff 转述)。
|
|
85
57
|
await pool.query(`CREATE TABLE IF NOT EXISTS ${MAILBOX_TABLE} (
|
|
86
58
|
scope_key VARBINARY(190) NOT NULL,
|
|
87
59
|
handle VARBINARY(190) NOT NULL,
|
|
@@ -157,15 +129,11 @@ export class TiDBMailboxStore {
|
|
|
157
129
|
assertIdentityChars("handle", handle);
|
|
158
130
|
assertKeyBytes("scope", scope);
|
|
159
131
|
assertKeyBytes("handle", handle);
|
|
160
|
-
const hopChain = encodeHopChain(msg.hopChain);
|
|
161
|
-
// ensure-first 形:先幂等 INSERT IGNORE 建盒(next_seq=1,首条/drop 后重建同臂)再 FOR UPDATE——
|
|
162
|
-
// 「无行 FOR UPDATE + INSERT」的并发死锁/dup 撞车形整个消掉;seq 分配与 append 同事务(定谳 2)。
|
|
132
|
+
const hopChain = encodeHopChain(msg.hopChain);
|
|
163
133
|
return this.tx(async (c) => {
|
|
164
134
|
await c.query(`INSERT IGNORE INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES (?, ?, ?, 1)`, [scope, handle, scope]);
|
|
165
135
|
let [rows] = (await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
|
|
166
136
|
if (rows.length === 0) {
|
|
167
|
-
// ensure 与锁之间盒被并发 drop/reap 提交(复审 #5 窗)——同事务重建一次(语义 = drop 后重建,
|
|
168
|
-
// seq 从 1;同事务内自插行必可见,再空 = 真障碍 fail-loud)。
|
|
169
137
|
await c.query(`INSERT IGNORE INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES (?, ?, ?, 1)`, [scope, handle, scope]);
|
|
170
138
|
[rows] = (await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
|
|
171
139
|
}
|
|
@@ -181,35 +149,24 @@ export class TiDBMailboxStore {
|
|
|
181
149
|
if (boxes.length === 0)
|
|
182
150
|
return null;
|
|
183
151
|
const box = boxes[0];
|
|
184
|
-
// 定谳 4:严格 expiresAt > now 才挡;定谳 1:同 owner 恒续期(字节等价比对——VARBINARY 读回 Buffer)。
|
|
185
|
-
// 残余记账(F7 TiDB 臂):owner 含 lone surrogate 时 mysql2 编码层塌缩 U+FFFD,读回串 ≠ 原参数
|
|
186
|
-
// → 同主续期被误拒(fail-closed 向,无越权);owner 是内部 mint 的 worker id(ASCII 域),接受。
|
|
187
152
|
if (box.lease_owner !== null &&
|
|
188
153
|
Number(box.lease_expires_at_ms) > now &&
|
|
189
154
|
bufToStr(box.lease_owner) !== owner) {
|
|
190
155
|
return null;
|
|
191
156
|
}
|
|
192
|
-
// FOR UPDATE = 当前读:TiDB 悲观事务的普通 SELECT 按 start_ts 快照(task-list twin 同案)——
|
|
193
|
-
// 锁到 box 行后消息集必须读「现在」,否则 drop+重建窗内老消息混进新盒 lease(maxSeq 错位)。
|
|
194
157
|
const [msgs] = (await c.query(`SELECT seq, from_id, content, sent_at_ms, hop_chain FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY seq FOR UPDATE`, [scope, handle]));
|
|
195
158
|
if (msgs.length === 0)
|
|
196
|
-
return null;
|
|
159
|
+
return null;
|
|
197
160
|
const maxSeq = Number(msgs[msgs.length - 1].seq);
|
|
198
161
|
await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = ?, lease_expires_at_ms = ?, lease_max_seq = ? WHERE scope_key = ? AND handle = ?`, [owner, now + ttlMs, maxSeq, scope, handle]);
|
|
199
162
|
return { messages: msgs.map(rowToMessage), maxSeq };
|
|
200
163
|
});
|
|
201
164
|
}
|
|
202
165
|
async ack(scope, handle, owner, upToSeq) {
|
|
203
|
-
// MB1/core 1.452 BREAKING:ack 带 **owner 围栏**(core InMemory 同语义:lease 缺席或 owner 不符 ⇒
|
|
204
|
-
// 整个 ack no-op,一条消息都不删——原「定谳 3 无守卫,信任 claim 赢者链」被 [1937] MB1 推翻:
|
|
205
|
-
// 任何人可删别人租约内消息)。锁序仍=盒行先(复审 F3 ABBA 死锁形);盒不存在=SELECT 空 ⇒ no-op。
|
|
206
166
|
await this.tx(async (c) => {
|
|
207
167
|
const [rows] = (await c.query(`SELECT lease_owner FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
|
|
208
|
-
// 🔴 真双库抓的坑(2026-07-28,mock 无法抓):lease_owner 是 VARBINARY,mysql2 读回 **Buffer**——
|
|
209
|
-
// `Buffer !== string` 恒真 ⇒ 首版围栏把正主也拒了(所有 ack 恒 no-op)。bufToStr 字节等价比对,
|
|
210
|
-
// claim 的定谳 1 同坑同解(:186)。
|
|
211
168
|
if (rows.length === 0 || rows[0].lease_owner == null || bufToStr(rows[0].lease_owner) !== owner)
|
|
212
|
-
return;
|
|
169
|
+
return;
|
|
213
170
|
await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? AND seq <= ?`, [scope, handle, upToSeq]);
|
|
214
171
|
await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL
|
|
215
172
|
WHERE scope_key = ? AND handle = ? AND lease_max_seq IS NOT NULL AND lease_max_seq <= ?`, [scope, handle, upToSeq]);
|
|
@@ -224,8 +181,6 @@ export class TiDBMailboxStore {
|
|
|
224
181
|
return Number(rows[0].n);
|
|
225
182
|
}
|
|
226
183
|
async drop(scope, handle) {
|
|
227
|
-
// 整盒删含 box 行 = seq 生命周期终结(定谳 2;重建从 1)。**盒行先删**(锁序统一 + F2:反序在
|
|
228
|
-
// 「消息已删、盒未删」窗放并发 append 穿过 → 盒重建撞孤儿消息行 PK,append 从此永败)。
|
|
229
184
|
await this.tx(async (c) => {
|
|
230
185
|
await c.query(`DELETE FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, handle]);
|
|
231
186
|
await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, handle]);
|
|
@@ -235,9 +190,6 @@ export class TiDBMailboxStore {
|
|
|
235
190
|
if (opts?.maxAgeMs === undefined)
|
|
236
191
|
return 0;
|
|
237
192
|
const cutoff = now - opts.maxAgeMs;
|
|
238
|
-
// 全量真扫(定谳 6):事务外粗筛(索引面)+ 事务内逐盒「锁盒行 → 当前读重验 newest」(复审
|
|
239
|
-
// F2/F4:粗筛与删除之间窗内新到的消息必须救活盒,绝不连带删信;GROUP BY 不能 FOR UPDATE,
|
|
240
|
-
// 重验用 ORDER BY … LIMIT 1 当前读)。空盒不删(newest 无行);盒行已被并发 drop = 跳过。
|
|
241
193
|
const [cands] = (await this.pool.query(`SELECT handle FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? GROUP BY handle HAVING MAX(sent_at_ms) < ?`, [scope, cutoff]));
|
|
242
194
|
if (cands.length === 0)
|
|
243
195
|
return 0;
|
|
@@ -280,7 +232,6 @@ export class PgMailboxStore {
|
|
|
280
232
|
c.release();
|
|
281
233
|
}
|
|
282
234
|
}
|
|
283
|
-
/** 身份键拒绝式([1439] 协议纪律):scope/handle/owner/from 清洗形变 = 路由/attribution 错位。 */
|
|
284
235
|
assertIdentity(what, v) {
|
|
285
236
|
if (v !== undefined && pgHasUnstorable(v))
|
|
286
237
|
throw new PgUnstorableError(`mailbox ${what}`);
|
|
@@ -290,11 +241,10 @@ export class PgMailboxStore {
|
|
|
290
241
|
this.assertIdentity("scope", scope);
|
|
291
242
|
this.assertIdentity("handle", handle);
|
|
292
243
|
this.assertIdentity("from", msg.from);
|
|
293
|
-
const content = pgSanitizeText(msg.content);
|
|
244
|
+
const content = pgSanitizeText(msg.content);
|
|
294
245
|
assertKeyBytes("scope", scope);
|
|
295
246
|
assertKeyBytes("handle", handle);
|
|
296
|
-
const hopChain = encodeHopChain(msg.hopChain);
|
|
297
|
-
// ensure-first 形(TiDB 同注):幂等建盒再 FOR UPDATE,死锁/dup 撞车形消。
|
|
247
|
+
const hopChain = encodeHopChain(msg.hopChain);
|
|
298
248
|
return this.tx(async (c) => {
|
|
299
249
|
await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES ($1, $2, $3, 1) ON CONFLICT (scope_key, handle) DO NOTHING`, [
|
|
300
250
|
scope,
|
|
@@ -303,7 +253,6 @@ export class PgMailboxStore {
|
|
|
303
253
|
]);
|
|
304
254
|
let { rows } = await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2 FOR UPDATE`, [scope, handle]);
|
|
305
255
|
if (rows.length === 0) {
|
|
306
|
-
// ensure 与锁之间盒被并发 drop/reap 提交(TiDB 同注)——同事务重建一次。
|
|
307
256
|
await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES ($1, $2, $3, 1) ON CONFLICT (scope_key, handle) DO NOTHING`, [
|
|
308
257
|
scope,
|
|
309
258
|
handle,
|
|
@@ -318,7 +267,7 @@ export class PgMailboxStore {
|
|
|
318
267
|
});
|
|
319
268
|
}
|
|
320
269
|
async claimLease(scope, handle, owner, ttlMs, now = Date.now()) {
|
|
321
|
-
this.assertIdentity("owner", owner);
|
|
270
|
+
this.assertIdentity("owner", owner);
|
|
322
271
|
return this.tx(async (c) => {
|
|
323
272
|
const { rows: boxes } = await c.query(`SELECT lease_owner, lease_expires_at_ms FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2 FOR UPDATE`, [scope, handle]);
|
|
324
273
|
if (boxes.length === 0)
|
|
@@ -335,32 +284,27 @@ export class PgMailboxStore {
|
|
|
335
284
|
});
|
|
336
285
|
}
|
|
337
286
|
async ack(scope, handle, owner, upToSeq) {
|
|
338
|
-
// MB1 owner 围栏(TiDB 孪生同注);锁序=盒行先(F3 ABBA 死锁形消)。
|
|
339
287
|
await this.tx(async (c) => {
|
|
340
288
|
const r = (await c.query(`SELECT lease_owner FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2 FOR UPDATE`, [scope, handle]));
|
|
341
|
-
// PG 列是 VARCHAR COLLATE "C"(读回 string,无 TiDB 臂的 Buffer 坑),bufToStr 幂等包一层求同形。
|
|
342
289
|
if (r.rows.length === 0 || r.rows[0].lease_owner == null || bufToStr(r.rows[0].lease_owner) !== owner)
|
|
343
|
-
return;
|
|
290
|
+
return;
|
|
344
291
|
await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 AND seq <= $3`, [scope, handle, upToSeq]);
|
|
345
292
|
await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL
|
|
346
293
|
WHERE scope_key = $1 AND handle = $2 AND lease_max_seq IS NOT NULL AND lease_max_seq <= $3`, [scope, handle, upToSeq]);
|
|
347
294
|
});
|
|
348
295
|
}
|
|
349
296
|
async releaseLease(scope, handle, owner) {
|
|
350
|
-
this.assertIdentity("owner", owner);
|
|
297
|
+
this.assertIdentity("owner", owner);
|
|
351
298
|
await this.pool.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL
|
|
352
299
|
WHERE scope_key = $1 AND handle = $2 AND lease_owner = $3`, [scope, handle, owner]);
|
|
353
300
|
}
|
|
354
301
|
async peekCount(scope, handle) {
|
|
355
|
-
// D4(试剂盒揪出):unstorable 身份键在**读面降级**返回 0(该键在写入面就被拒 ⇒ 盒必空),
|
|
356
|
-
// 而不是把驱动层 22021 裸抛给调用方——与 tool-result 读面「degrades instead」同精神。
|
|
357
302
|
if (pgHasUnstorable(scope) || pgHasUnstorable(handle))
|
|
358
303
|
return 0;
|
|
359
304
|
const { rows } = await this.pool.query(`SELECT COUNT(*) AS n FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, handle]);
|
|
360
305
|
return Number(rows[0].n);
|
|
361
306
|
}
|
|
362
307
|
async drop(scope, handle) {
|
|
363
|
-
// 盒行先删(TiDB 同注,F2 孤儿消息行窗关死)。
|
|
364
308
|
await this.tx(async (c) => {
|
|
365
309
|
await c.query(`DELETE FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, handle]);
|
|
366
310
|
await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, handle]);
|
|
@@ -370,7 +314,6 @@ export class PgMailboxStore {
|
|
|
370
314
|
if (opts?.maxAgeMs === undefined)
|
|
371
315
|
return 0;
|
|
372
316
|
const cutoff = now - opts.maxAgeMs;
|
|
373
|
-
// 事务外粗筛 + 事务内逐盒锁定重验(TiDB 同注,F2/F4:窗内新信救活盒,不丢信)。
|
|
374
317
|
const { rows: cands } = await this.pool.query(`SELECT handle FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 GROUP BY handle HAVING MAX(sent_at_ms) < $2`, [scope, cutoff]);
|
|
375
318
|
if (cands.length === 0)
|
|
376
319
|
return 0;
|
|
@@ -1,133 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/234 —— embedder **指纹门**(清空重建形,clay 终裁 [3647]⑤ 方向;稿=sema-internal
|
|
3
|
-
* `server/design/design-234-embedder-fingerprint-20260812.md` v1+v2)。
|
|
4
|
-
*
|
|
5
|
-
* ## 病灶
|
|
6
|
-
*
|
|
7
|
-
* 向量行落在 `agent_memory_engine_entry.embedding`,**行上没有任何「产自哪个 embedder」的记账**。既有
|
|
8
|
-
* 防线只有 embedder 的维度门(长度≠`MEMORY_EMBEDDER_DIM` ⇒ 抛),它只拦「换模型且维度恰好不同」;
|
|
9
|
-
* **同维度换模型**(768 家族之间、1024 家族之间迁移=最常见形)完全静默——旧空间与新空间的向量在同一
|
|
10
|
-
* 列里做 cosine,检索排序变噪音,没有任何一层会说出来。
|
|
11
|
-
*
|
|
12
|
-
* ## 形
|
|
13
|
-
*
|
|
14
|
-
* 指纹 = identity 两元组 `{ model, dimensions }`,**明文**存(不 hash:诊断可读性 > 紧凑,日志要能直接
|
|
15
|
-
* 念出「从 X 换到了 Y」)。**endpoint 不进指纹**——换供应商域名/代理/端口不换语义空间,进了会造成大量
|
|
16
|
-
* 误清;残余成文:同名模型在不同供应商若是不同实现(自托管 finetune 撞名)指纹辨不出,operator 明知
|
|
17
|
-
* 异实现时改 `MEMORY_EMBEDDER_MODEL` 名或手动清列(USAGE 有一行 SQL)。
|
|
18
|
-
*
|
|
19
|
-
* 存储位 = **新单行元表**(additive,零删库重建窗)。不给主表加列:主表改形=触发本仓「无 ALTER、删库
|
|
20
|
-
* 重建」BREAKING 窗;行级记账在「清空重建」语义下也无增益(全量清 ⇒ 不存在混合态)。
|
|
21
|
-
*
|
|
22
|
-
* ## 属主边界
|
|
23
|
-
*
|
|
24
|
-
* `memory-engine-pg.ts` 是 core 移入件(minimal-diff move-in 纪律)⇒ 元表 DDL + 比对清空腿全在**本
|
|
25
|
-
* 文件**(server 侧),移入文件零改动;表名常量本文件自有(`PG_MEMORY_ENGINE_TABLES` 不动)。
|
|
26
|
-
* 列名 `meta_key`/`meta_value`:`key` 是 MySQL 保留字,将来若补 TiDB 腿零反引号坑(K2)。
|
|
27
|
-
*
|
|
28
|
-
* ## 六态(v2 总表)
|
|
29
|
-
*
|
|
30
|
-
* | 态 | 判定 | 动作 |
|
|
31
|
-
* |---|---|---|
|
|
32
|
-
* | A | embedder 配置缺席 | **门不装**(连 ensure 都不调),元表不动——装配点的事,不在本文件 |
|
|
33
|
-
* | B | 元表有行、identity 逐键相等 | 照常,**零写** |
|
|
34
|
-
* | C | 元表有行、不等 | 清列 → upsert 新 identity(`reason:"changed"`) |
|
|
35
|
-
* | D | 元表无行、库内**有**非 NULL 向量 | 同 C 清(存量库无法证明同源=保守清,`reason:"unattributed"`) |
|
|
36
|
-
* | E | 元表无行、零向量行 | 只写 identity,不清(`memory_embedder_identity_armed`) |
|
|
37
|
-
* | F | 元表有行但 value 解析失败 | 走清空臂(`reason:"unreadable"`)——**绝不当成匹配**(边界必 schema) |
|
|
38
|
-
*
|
|
39
|
-
* (F 的判据是 `model`/`dimensions` 缺失或类型不对;**多出来的未知键不算坏**,见 schema 处的注。)
|
|
40
|
-
*
|
|
41
|
-
* ## 三条不可动的判据
|
|
42
|
-
*
|
|
43
|
-
* 1. **次序:先清列、后写元表**。反序若中间崩溃 = 新 identity 已记而旧向量还在 = 正是被修的病(此后
|
|
44
|
-
* 每次 boot 恒判 B,残留态永不复检);正序中间崩溃 = 下次 boot 无元表行、向量已 NULL ⇒ E 态补记,
|
|
45
|
-
* 幂等安全。
|
|
46
|
-
* 2. **CAS 循环**(v2 C1-2):元表写后**重读**,读回≠自写 ⇒ 另一副本并发写了**不同** identity(部署
|
|
47
|
-
* 配置漂移)⇒ 响亮 `memory_embedder_identity_conflict`(两值并示)+ 重跑比对,上限
|
|
48
|
-
* {@link CAS_MAX_ROUNDS} 轮,超限抛(拒启)。运行期旧副本混写非 CAS 可拦 ⇒ 归停机纪律(USAGE)。
|
|
49
|
-
* 3. **门自身失败 = 抛(拒启)**。吞掉 = 元表停留旧值 = 下次 boot 再清一次,把一次性代价变成每次 boot
|
|
50
|
-
* 的代价;且「门在场但没跑成」静默 = fail-open。
|
|
51
|
-
*
|
|
52
|
-
* ## 两条已知残余(codex 对抗复审 R1-F1/R1-F3,均判为真;处置=成文,不落码面)
|
|
53
|
-
*
|
|
54
|
-
* - **跨门回滚**(F1):指纹门**之前**的版本(7.14.x/7.15.x:有 embedder、无本门)照写 `embedding` 列却
|
|
55
|
-
* 不认识元表 ⇒「装本版(元表=B)→ 回滚旧版跑一段(写 A 空间向量)→ 升回本版仍配 B」走完,元表与配置
|
|
56
|
-
* 都说 B 而列里是 A 空间 ⇒ 此后恒判 B 态。**与滚动窗是同一族**(第二写者不维护指纹),码面根治只有
|
|
57
|
-
* 行级记账列 —— 稿已列出界(触主表删库重建窗)。纪律面处置与滚动窗同:回滚前后手动清列,或删掉
|
|
58
|
-
* 元表那一行让下次 boot 按 D 态保守清。USAGE 已成文。
|
|
59
|
-
* - **崩溃丢审计行**(F3):「清过就必响亮」以**进程活到 emitOutcome** 为界。清列与写元表是两条各自
|
|
60
|
-
* autocommit 的语句,日志在回读之后才发 ⇒ 清完即崩 ⇒ 下次 boot 元表无行、向量已 NULL ⇒ 走 E 态报
|
|
61
|
-
* 「没有向量可清」;写完元表即崩 ⇒ 下次 boot 判 B 态、**一条日志都不发**。**数据面安全**(次序钉买的
|
|
62
|
-
* 正是这个:绝不残留旧空间),丢的只有那一次的 cleared 计数。刻意**不做**两阶段 pending marker ——
|
|
63
|
-
* 为纯审计价值给 boot 路加一张表 + 两阶段协议,代价与收益不成比例;承诺句在 USAGE 已标出这条边界。
|
|
64
|
-
*
|
|
65
|
-
* 计数取法:`PgQueryResult` 刻意无 `rowCount` ⇒ 用 CTE `WITH c AS (UPDATE … RETURNING 1) SELECT
|
|
66
|
-
* count(*)::int`(禁 `RETURNING id` 全表拉回)。计数**按整次调用累计**、原因锚在第一次真清的那一轮 ——
|
|
67
|
-
* 「清过就必响亮」对终局判 B 的那一支同样成立(codex R2 finding 2)。
|
|
68
|
-
*
|
|
69
|
-
* ## 射程外(稿 §出界)
|
|
70
|
-
*
|
|
71
|
-
* 主动回填件(清空后不回填,行被 patch 时自然重嵌)/ 行级 embedder 记账列(结构性根治滚动窗)/
|
|
72
|
-
* pgvector 接线件的列 dim 校验 / TiDB 向量面(v1 lexical only)。
|
|
73
|
-
*
|
|
74
|
-
* 🔴 **不回填的真代价(#234 定界车 2026-08-14 实测更正,稿 §4/§V3 说轻了一档)**:稿把代价写成「那些行
|
|
75
|
-
* 长期停留 lexical 档」,读起来像**排序退化**;真码是**可达性悬崖**——`memory-engine-pg.ts` 的进程内腿
|
|
76
|
-
* 对无向量行落 `jaccardDistance`,core 的该函数在**零词交集**时返回 `null`,`search` 当场 `continue`,
|
|
77
|
-
* 行被**丢出结果集**;仍有向量的行则走余弦、不被这道门丢(前提是查询侧也嵌得出来——embedder 故障那一
|
|
78
|
-
* 刻查询拿不到向量,整轮退词面档,零交集的行一起丢)。即清空之后、重嵌之前,那些行对改述/同义/跨
|
|
79
|
-
* 语言查询**检索不到**——而那恰恰是向量档存在的理由。真库端到端实证:db-integration 套件「清空后的
|
|
80
|
-
* 代价」一钉(清空前 "zulu" 命中该行,清空后同查询恒空,有字面交集的查询仍命中)。
|
|
81
|
-
* ⚠️ 这条只更正**成文口径**,不改裁定:主动回填仍在射程外(clay 已否);但将来复裁该件时,输入应当是
|
|
82
|
-
* 「可达性悬崖」而不是「排序退化」。
|
|
83
|
-
*/
|
|
84
1
|
import { z } from "zod";
|
|
85
2
|
import { PG_MEMORY_ENGINE_TABLES } from "./memory-engine-pg.js";
|
|
86
|
-
/** 元表名(本文件自有 const —— core 移入文件的 `PG_MEMORY_ENGINE_TABLES` 零改动)。 */
|
|
87
3
|
export const PG_MEMORY_ENGINE_META_TABLE = "agent_memory_engine_meta";
|
|
88
|
-
/** 单行元表里 embedder 指纹那一行的键。 */
|
|
89
4
|
export const EMBEDDER_IDENTITY_META_KEY = "embedder_identity";
|
|
90
|
-
/** CAS 循环上限(v2 C1-2)。超限 = 并发副本带着**不同** identity 在互相覆盖 ⇒ 拒启。 */
|
|
91
5
|
export const CAS_MAX_ROUNDS = 3;
|
|
92
|
-
/**
|
|
93
|
-
* 指纹 schema —— 读回的 `meta_value` 必须过它(手改坏值/回滚残留一律走 F 态,禁 `JSON.parse as` 裸断言)。
|
|
94
|
-
*
|
|
95
|
-
* ⚠️ **未知键刻意放过**(zod 对象默认 strip,不加 `.strict()`):将来某个版本若往这行里多写一个键,
|
|
96
|
-
* 老副本读到它应当照常按 `model`/`dimensions` 比对,而不是判 F 态把整列清掉 —— 「新副本加了个字段」
|
|
97
|
-
* 与「这行坏了」不是一回事,后者才是 F 态要抓的。载荷只有这两个键,多出来的不参与判定。
|
|
98
|
-
*/
|
|
99
6
|
const EmbedderIdentitySchema = z.object({
|
|
100
7
|
model: z.string().min(1),
|
|
101
8
|
dimensions: z.number().int().positive(),
|
|
102
9
|
});
|
|
103
|
-
/**
|
|
104
|
-
* 元表 DDL —— **只发 CREATE**(K1:混入 SELECT/UPDATE 会让 `scripts/dump-schema.ts` 的录制门当场抛)。
|
|
105
|
-
* 幂等,与其余 `ensurePg*Schema` 同姿势;新腿已注册 `PG_LEGS` + `docs/schema/baseline-pg.sql`。
|
|
106
|
-
*/
|
|
107
10
|
export async function ensurePgMemoryEmbedderMetaSchema(query) {
|
|
108
11
|
await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_META_TABLE} (
|
|
109
12
|
meta_key varchar(190) COLLATE "C" PRIMARY KEY,
|
|
110
13
|
meta_value jsonb NOT NULL
|
|
111
14
|
)`);
|
|
112
15
|
}
|
|
113
|
-
/**
|
|
114
|
-
* 元表值 → 三态。入参是 `meta_value::text`(**JSON 文本**),**只解析一次**。
|
|
115
|
-
*
|
|
116
|
-
* 🔴 为什么读 `::text` 而不是读列本身(codex 对抗复审 R2 finding 1,验真为真):node-pg 已经替我们
|
|
117
|
-
* 解析过 jsonb,所以列里躺着一个 JSON **字符串**时,交到手上的就是一个 JS string —— 上一版对 string
|
|
118
|
-
* 再 `JSON.parse` 一次「宽容一格」,于是**双重编码**的坏值(`'"{\"model\":…}"'::jsonb`)会被解回合法
|
|
119
|
-
* 对象、判成「相等」,来源不明的旧向量原地存活 = 本模块要消灭的静默混空间换了个入口。读 `::text` 让
|
|
120
|
-
* 「对象」与「驱动序列化出的字符串」不再有歧义:解析一次之后**必须是普通对象**,真正的 JSON 字符串
|
|
121
|
-
* (以及数组/数字/null)一律落 F 态。顺带也不再依赖任何驱动的 jsonb 解析姿势。
|
|
122
|
-
*/
|
|
123
16
|
function parseStoredText(text) {
|
|
124
17
|
let candidate;
|
|
125
18
|
try {
|
|
126
19
|
candidate = JSON.parse(text);
|
|
127
20
|
}
|
|
128
21
|
catch (err) {
|
|
129
|
-
// 解析错**当数据交出去**(不静默折成一句「读不出来」):这段文字会随 `reason:"unreadable"` 进
|
|
130
|
-
// 那条响亮日志,排障时「元表里躺的到底是什么」是第一个要问的。
|
|
131
22
|
return { kind: "unreadable", raw: `${text.slice(0, 160)} [JSON.parse: ${String(err).slice(0, 80)}]` };
|
|
132
23
|
}
|
|
133
24
|
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
|
|
@@ -144,27 +35,19 @@ async function readStored(query) {
|
|
|
144
35
|
if (row === undefined)
|
|
145
36
|
return { kind: "absent" };
|
|
146
37
|
const text = row.meta_value_text;
|
|
147
|
-
// 列是 NOT NULL 且 `::text` 恒给文本 —— 拿不到文本 = 这条读腿被换过(池包装/驱动改形),响亮落 F,
|
|
148
|
-
// 不猜。
|
|
149
38
|
if (typeof text !== "string")
|
|
150
39
|
return { kind: "unreadable", raw: `[meta_value::text did not return text: ${typeof text}]` };
|
|
151
40
|
return parseStoredText(text);
|
|
152
41
|
}
|
|
153
|
-
/** D/E 的判据:库里还有没有**没主**的向量(元表无行 ⇒ 存量向量无从证明同源)。`LIMIT 1` —— 只问存在性,不拉全表。 */
|
|
154
42
|
async function hasUnattributedVectors(query) {
|
|
155
43
|
const r = await query(`SELECT 1 FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE embedding IS NOT NULL LIMIT 1`);
|
|
156
44
|
return r.rows.length > 0;
|
|
157
45
|
}
|
|
158
|
-
/**
|
|
159
|
-
* 清列 + 计数。`PgQueryResult` 无 `rowCount` ⇒ CTE 把受影响行数在 SQL 侧数完再回一行
|
|
160
|
-
* (`RETURNING id` 会把全表主键拉回进程,量级白烧)。
|
|
161
|
-
*/
|
|
162
46
|
async function clearEmbeddings(query) {
|
|
163
47
|
const r = await query(`WITH c AS (UPDATE ${PG_MEMORY_ENGINE_TABLES.entry} SET embedding = NULL WHERE embedding IS NOT NULL RETURNING 1)
|
|
164
48
|
SELECT count(*)::int AS n FROM c`);
|
|
165
49
|
return Number(r.rows[0]?.n ?? 0);
|
|
166
50
|
}
|
|
167
|
-
/** 三臂统一 upsert(C2:并发首启两副本同时 INSERT 不撞 PK)。 */
|
|
168
51
|
async function writeIdentity(query, identity) {
|
|
169
52
|
await query(`INSERT INTO ${PG_MEMORY_ENGINE_META_TABLE} (meta_key, meta_value) VALUES ($1, $2::jsonb)
|
|
170
53
|
ON CONFLICT (meta_key) DO UPDATE SET meta_value = EXCLUDED.meta_value`, [EMBEDDER_IDENTITY_META_KEY, JSON.stringify(identity)]);
|
|
@@ -172,33 +55,14 @@ async function writeIdentity(query, identity) {
|
|
|
172
55
|
function sameIdentity(a, b) {
|
|
173
56
|
return a.model === b.model && a.dimensions === b.dimensions;
|
|
174
57
|
}
|
|
175
|
-
/** 换模型这件事的运维纪律(日志与 USAGE 同句;滚动窗混写是纪律面残余,门管 boot 面)。 */
|
|
176
58
|
const SCALE_ZERO_NOTE = "change the embedder identity with the fleet stopped (scale to 0, then start): during a ROLLING window the old replicas keep writing vectors in the OLD space after this gate cleared the column";
|
|
177
|
-
/**
|
|
178
|
-
* 指纹比对腿 —— boot 一次(env 族是 boot 快照,运行中不热换)。**门自身失败一律抛**(拒启)。
|
|
179
|
-
*
|
|
180
|
-
* ⚠️ 调用姿势:`ensurePgMemoryEmbedderMetaSchema` 之后、memory backend 构造之前;embedder 配置**在场**
|
|
181
|
-
* 才调(A 态连 ensure 都不调)。
|
|
182
|
-
*/
|
|
183
59
|
export async function reconcileEmbedderFingerprint(query, identity, opts = {}) {
|
|
184
60
|
const { log } = opts;
|
|
185
61
|
const now = opts.now ?? Date.now;
|
|
186
|
-
// 自家入参也过 schema:装配点喂的是 config 解析结果(已校验),但这条腿也被脚本/测试直调,
|
|
187
|
-
// 坏 identity 写进元表 = 下次 boot 恒判 F 态清全库。
|
|
188
62
|
const want = EmbedderIdentitySchema.parse(identity);
|
|
189
63
|
const firstStartedAt = now();
|
|
190
|
-
// 🔴 跨轮累计(codex 对抗复审 R2 finding 2,验真为真):清空计数/原因/旧值**每轮重置**时,
|
|
191
|
-
// 「round1 清了 N 行、回读撞冲突、round2 只能再清到 0」会把一次整列缓存失效报成 `cleared=0`;更坏的
|
|
192
|
-
// 一支是 round2 恰好读回我方 identity ⇒ 从 match 臂直接返回、**一条日志都不发**,破坏性动作在运维
|
|
193
|
-
// 记录里彻底消失(与「任何清空动作必响亮」直接冲突)。所以计数按**整次调用**累计,原因/旧值锚在
|
|
194
|
-
// **第一次真清**的那一轮,并且只要清过就必发 `memory_embedder_identity_changed` —— 终局是 match
|
|
195
|
-
// 也不例外。
|
|
196
64
|
let clearedTotal = 0;
|
|
197
65
|
let firstClear;
|
|
198
|
-
// 🔴 **变更**本身也要跨轮记账,不能只记「清过行」(合并码重扫 codex 复审第二轮,验真为真):零向量库
|
|
199
|
-
// 的 C/D/F 轮 `cleared` 恒 0 ⇒ 它进不了 `firstClear`;若那一轮的回读又撞上并发冲突、下一轮才收敛到
|
|
200
|
-
// match,终局臂只看得见 `clearedTotal===0` 与 `terminalState==="match"`,于是一次**真发生过的** identity
|
|
201
|
-
// 变更(元表已被我方改写)在日志里彻底消失 —— 与「清过必响亮」同族的漏报,只是漏的是零清空那一支。
|
|
202
66
|
let firstChange;
|
|
203
67
|
const emitOutcome = (terminalState, rounds) => {
|
|
204
68
|
const elapsedMs = now() - firstStartedAt;
|
|
@@ -207,30 +71,19 @@ export async function reconcileEmbedderFingerprint(query, identity, opts = {}) {
|
|
|
207
71
|
previous: firstClear?.previous ?? firstChange?.previous ?? null,
|
|
208
72
|
next: want,
|
|
209
73
|
cleared: clearedTotal,
|
|
210
|
-
reason: firstClear?.reason ?? firstChange?.reason ?? terminalState,
|
|
74
|
+
reason: firstClear?.reason ?? firstChange?.reason ?? terminalState,
|
|
211
75
|
rounds,
|
|
212
76
|
elapsedMs,
|
|
213
|
-
// 🔴 代价按真码说(#234 定界车实测,真库端到端钉在 db-integration 的「清空后的代价」):
|
|
214
|
-
// 「退回 lexical」是**说轻了**——词面档对与查询**零词交集**的行返回「无定义」,检索当场把该行
|
|
215
|
-
// 跳过,于是清空之后、重嵌之前,那些行对改述/同义/跨语言查询是**不可达**的(带向量的行则恒有
|
|
216
|
-
// 余弦读数、永不被丢)。operator 看见这条日志的那一刻就该知道丢的是可达性,不是名次。
|
|
217
77
|
note: `${SCALE_ZERO_NOTE}; cleared vectors are NOT backfilled — rows re-embed only as they are next written, and until then each cleared row is reachable ONLY by a query sharing a literal term with it (the lexical floor drops a row with zero term overlap, whereas a row that still has a vector is scored by cosine as long as the query embeds too) — rewrite the corpus to restore vector reach immediately`,
|
|
218
78
|
});
|
|
219
79
|
return;
|
|
220
80
|
}
|
|
221
|
-
// 🔴 **identity 真换了但没有向量可清,同样响亮**(合并码重扫,验真后修)。旧判据只有「清过行」与
|
|
222
|
-
// 「armed」两支,于是 C 态(不等)/ D 态 / F 态(元表行读不出来)碰上 `embedding` 列恰好全 NULL 时,
|
|
223
|
-
// 元表被静默改写、一条日志都不发 —— 可达形:上一次 boot 已清空过、期间无新语料;或回滚残留的坏元表
|
|
224
|
-
// 行 + 空向量列(恰恰是最该留痕的一形)。USAGE.md 把三个 reason 并列写进同一句承诺里,没有对
|
|
225
|
-
// 「清了 0 行」开例外 ⇒ 换 embedder 这件事在启动日志里不许无痕。`match` 仍是零写零响亮(B 态)。
|
|
226
|
-
// 判据是**本次调用里有没有真发生过 identity 变更**(`firstChange`),不是本轮的 `terminalState` ——
|
|
227
|
-
// 后者在「首轮 changed(零清空)→ CAS 冲突 → 次轮 match」这条路上会把变更说成没发生过。
|
|
228
81
|
if (firstChange !== undefined) {
|
|
229
82
|
log?.info("memory_embedder_identity_changed", {
|
|
230
83
|
previous: firstChange.previous,
|
|
231
84
|
next: want,
|
|
232
85
|
cleared: 0,
|
|
233
|
-
reason: firstChange.reason,
|
|
86
|
+
reason: firstChange.reason,
|
|
234
87
|
rounds,
|
|
235
88
|
elapsedMs,
|
|
236
89
|
note: `${SCALE_ZERO_NOTE}; no vectors existed to clear on this boot — the identity itself changed, which is the event`,
|
|
@@ -245,38 +98,26 @@ export async function reconcileEmbedderFingerprint(query, identity, opts = {}) {
|
|
|
245
98
|
elapsedMs,
|
|
246
99
|
});
|
|
247
100
|
}
|
|
248
|
-
// B(且从未清过)= 零写零响亮:换模型才是事件。
|
|
249
101
|
};
|
|
250
102
|
for (let round = 1; round <= CAS_MAX_ROUNDS; round++) {
|
|
251
103
|
const stored = await readStored(query);
|
|
252
|
-
// ── 判定 ────────────────────────────────────────────────────────────────────────────────────
|
|
253
104
|
let state;
|
|
254
105
|
let previous = null;
|
|
255
106
|
if (stored.kind === "ok") {
|
|
256
107
|
previous = stored.value;
|
|
257
108
|
if (sameIdentity(stored.value, want)) {
|
|
258
|
-
// B:零写(连 upsert 都不发 —— 「匹配」这条路径上不许有任何写)。但前序轮若已经清过,
|
|
259
|
-
// 那次清必须照样被记上、被说出来。
|
|
260
109
|
emitOutcome("match", round);
|
|
261
110
|
return { state: "match", cleared: clearedTotal, previous, identity: want, rounds: round };
|
|
262
111
|
}
|
|
263
|
-
state = "changed";
|
|
112
|
+
state = "changed";
|
|
264
113
|
}
|
|
265
114
|
else if (stored.kind === "unreadable") {
|
|
266
|
-
state = "unreadable";
|
|
115
|
+
state = "unreadable";
|
|
267
116
|
}
|
|
268
117
|
else {
|
|
269
|
-
state = (await hasUnattributedVectors(query)) ? "unattributed" : "armed";
|
|
118
|
+
state = (await hasUnattributedVectors(query)) ? "unattributed" : "armed";
|
|
270
119
|
}
|
|
271
|
-
// ── 动作:🔴 先清列、后写元表(次序即正确性,见头注 1)────────────────────────────────────
|
|
272
120
|
if (state !== "armed" && firstChange === undefined) {
|
|
273
|
-
// 变更的锚:第一次判出「不是 match、不是 armed」的那一轮(与清了几行无关)。
|
|
274
|
-
// ⚠️ 如实登记的残余(codex 复审第三轮提出,判为**过报**而非误报,故不改 SQL 语义):两只**同配置**
|
|
275
|
-
// 副本同时首启时,两边都可能先读到旧值 ⇒ 各自 clear+upsert(赢家清到行,输家清 0)⇒ 两边各发一条
|
|
276
|
-
// `memory_embedder_identity_changed`(同一次逻辑变更在两个进程的日志里各留一条)。方向是安全的那
|
|
277
|
-
// 一侧:多一条过报 < 漏一条(本键的全部理由是「换 embedder 不许无痕」),且两条都如实描述了各自
|
|
278
|
-
// 观察到的世界(旧值 → 新值)。根治要把 upsert 换成「只在值真变时才写并回带是否变过」的条件形 ——
|
|
279
|
-
// 那是元表写腿的 SQL 语义变更(双库门),不在本收口批射程,登记为后续件。
|
|
280
121
|
firstChange = { reason: state, previous: previous ?? (stored.kind === "unreadable" ? { unreadable: stored.raw } : null) };
|
|
281
122
|
}
|
|
282
123
|
const cleared = state === "armed" ? 0 : await clearEmbeddings(query);
|
|
@@ -285,13 +126,11 @@ export async function reconcileEmbedderFingerprint(query, identity, opts = {}) {
|
|
|
285
126
|
}
|
|
286
127
|
clearedTotal += cleared;
|
|
287
128
|
await writeIdentity(query, want);
|
|
288
|
-
// ── CAS 回读(见头注 2)────────────────────────────────────────────────────────────────────
|
|
289
129
|
const after = await readStored(query);
|
|
290
130
|
if (after.kind === "ok" && sameIdentity(after.value, want)) {
|
|
291
131
|
emitOutcome(state, round);
|
|
292
132
|
return { state, cleared: clearedTotal, previous, identity: want, rounds: round };
|
|
293
133
|
}
|
|
294
|
-
// 读回≠自写 ⇒ 另一副本并发写了**不同** identity(部署配置漂移)。响亮 + 重跑比对。
|
|
295
134
|
log?.error("memory_embedder_identity_conflict", {
|
|
296
135
|
ours: want,
|
|
297
136
|
theirs: after.kind === "ok" ? after.value : after.kind === "unreadable" ? { unreadable: after.raw } : null,
|