@sema-agent/server 7.37.0 → 7.38.0-rc.2

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.
Files changed (83) hide show
  1. package/USAGE.md +2 -1
  2. package/dist/adoption/plan.js +16 -0
  3. package/dist/boot/budget-tracing.d.ts +1 -1
  4. package/dist/boot/budget-tracing.js +12 -5
  5. package/dist/boot/config-center.d.ts +27 -2
  6. package/dist/boot/config-center.js +168 -15
  7. package/dist/boot/limit-sync.d.ts +32 -0
  8. package/dist/boot/limit-sync.js +35 -0
  9. package/dist/boot/reapers.d.ts +68 -0
  10. package/dist/boot/reapers.js +63 -4
  11. package/dist/boot/resolve-spec.js +2 -0
  12. package/dist/boot/runner-deps.d.ts +1 -1
  13. package/dist/boot/runner-deps.js +4 -0
  14. package/dist/boot/stores.js +13 -0
  15. package/dist/config-center/apply-effective.d.ts +41 -3
  16. package/dist/config-center/apply-effective.js +87 -10
  17. package/dist/config-center/apply-ledger.d.ts +82 -0
  18. package/dist/config-center/apply-ledger.js +104 -0
  19. package/dist/config-center/facade.d.ts +1 -1
  20. package/dist/config-center/hot-keys-registry.d.ts +62 -0
  21. package/dist/config-center/hot-keys-registry.js +54 -0
  22. package/dist/config-center/http-client.js +12 -2
  23. package/dist/config-center/restart-signal.js +5 -2
  24. package/dist/config-center/stage-limits.d.ts +59 -0
  25. package/dist/config-center/stage-limits.js +188 -0
  26. package/dist/config-center/types.d.ts +60 -0
  27. package/dist/config-invariants.d.ts +65 -0
  28. package/dist/config-invariants.js +139 -0
  29. package/dist/config-provider.js +11 -0
  30. package/dist/config-types.d.ts +10 -1
  31. package/dist/config.d.ts +11 -0
  32. package/dist/config.js +37 -5
  33. package/dist/http/routes/admin-config-refresh.d.ts +20 -0
  34. package/dist/http/routes/admin-config-refresh.js +43 -0
  35. package/dist/http/routes/diagnostics.js +4 -0
  36. package/dist/http/routes/runs.js +10 -6
  37. package/dist/http/server.d.ts +8 -0
  38. package/dist/http/server.js +26 -0
  39. package/dist/main.js +49 -6
  40. package/dist/memory-sync.js +50 -1
  41. package/dist/observability/cost-quota.d.ts +23 -3
  42. package/dist/observability/cost-quota.js +27 -4
  43. package/dist/observability/fail-open.d.ts +8 -0
  44. package/dist/observability/fail-open.js +8 -0
  45. package/dist/observability/rate-limit.d.ts +11 -1
  46. package/dist/observability/rate-limit.js +8 -2
  47. package/dist/plan-cache-probe.d.ts +5 -7
  48. package/dist/plan-cache-probe.js +27 -5
  49. package/dist/plugins/caching-session-store.d.ts +20 -4
  50. package/dist/plugins/caching-session-store.js +43 -2
  51. package/dist/plugins/fork-routing-session-store.d.ts +26 -4
  52. package/dist/plugins/fork-routing-session-store.js +134 -9
  53. package/dist/plugins/memory-engine-pg.d.ts +1 -0
  54. package/dist/plugins/memory-engine-pg.js +34 -4
  55. package/dist/plugins/memory-engine-tidb.d.ts +1 -0
  56. package/dist/plugins/memory-engine-tidb.js +17 -2
  57. package/dist/plugins/memory-origin-law.d.ts +69 -0
  58. package/dist/plugins/memory-origin-law.js +118 -1
  59. package/dist/plugins/pg-cost-quota.d.ts +7 -2
  60. package/dist/plugins/pg-cost-quota.js +25 -2
  61. package/dist/plugins/pg-rate-limiter.d.ts +7 -2
  62. package/dist/plugins/pg-rate-limiter.js +20 -2
  63. package/dist/plugins/pg-session-storage.d.ts +30 -7
  64. package/dist/plugins/pg-session-storage.js +145 -23
  65. package/dist/plugins/retention-store-sql.js +10 -1
  66. package/dist/plugins/session-placement.d.ts +113 -0
  67. package/dist/plugins/session-placement.js +153 -0
  68. package/dist/plugins/session-store.js +7 -1
  69. package/dist/plugins/tidb-cost-quota.d.ts +7 -2
  70. package/dist/plugins/tidb-cost-quota.js +26 -2
  71. package/dist/plugins/tidb-pool.js +15 -1
  72. package/dist/plugins/tidb-rate-limiter.d.ts +9 -2
  73. package/dist/plugins/tidb-rate-limiter.js +22 -2
  74. package/dist/plugins/tidb-session-storage.js +10 -2
  75. package/dist/plugins/tidb-session-store.d.ts +55 -7
  76. package/dist/plugins/tidb-session-store.js +174 -24
  77. package/dist/plugins/write-behind-counter.d.ts +17 -2
  78. package/dist/plugins/write-behind-counter.js +19 -1
  79. package/dist/runs.d.ts +3 -1
  80. package/dist/runs.js +5 -2
  81. package/dist/trace/engine-notice-wire.d.ts +1 -1
  82. package/dist/trace/engine-notice-wire.js +24 -6
  83. package/package.json +3 -3
@@ -1,3 +1,30 @@
1
+ /**
2
+ * ForkRoutingSessionStore — the subRunner's session backing after core 1.350's fork 终修([1364]①/[1367]①).
3
+ *
4
+ * 问题形:core 的 `hostSessionFork` 在 HOST runner 的店里 fork,fork 子任务带 `requireExistingSession:true`
5
+ * 在 opts.runner(subRunner)上执行——subRunner 拆店时子任务查不到刚 fork 出的会话,响亮失败
6
+ * `resume.session_not_found`(cli 真机复现)。
7
+ *
8
+ * 但把 host durable 店**整个**给 subRunner(第一版修)被 codex 判两 high:普通子任务(council 镜头/
9
+ * team/子代理)会把每次运行都铸成 committed durable 会话——sweepStagingSessions 只扫**无 session_meta**
10
+ * 的 staging 弃置行,committed 子会话没有 reaper=无界增长;且进 owner 过滤的会话枚举(零 run 的内部
11
+ * 会话污染 /v1/sessions)。
12
+ *
13
+ * 此包装店按**会话的真居所**路由,两边语义都保住:
14
+ * - `acquire(id, {requireExisting})`(fork 子 + retained-child resume 的形)→ **host 店优先**,miss 落
15
+ * transient(retained 瞬态子的 resume 仍走私店);两边都 miss=抛 host 侧错误(fail-loud 原语义)。
16
+ * - `acquire()`(无 id=普通子任务铸新会话)→ **transient**(throwaway 姿势保留:council/team 子会话
17
+ * 不落 durable、不进枚举、TTL 自清)。
18
+ * - `acquire(id)`(带 id 不 requireExisting)→ 已有路由按路由走;未知 id 落 transient(claim-create
19
+ * 绝不打到 durable 店——那是 authorizer 的职权)。
20
+ * - `fork(sourceId)` → 源在哪店在哪店 fork(路由未知时 host 先试,null 再 transient);新 id 记路由。
21
+ * - `list`/`size`/`dispose` = transient 侧(host 店归 main runner 所有;不双列、不双清)。
22
+ *
23
+ * 路由表 LRU 上限 {@link ROUTE_MAX}:满额逐最老(refresh-on-use);被逐后下次 `acquire` 按上述规则重derive
24
+ * (requireExisting 的 host-first 重试仍落对店),中途 touch/noteTaskRun 缺路由=落 transient(no-op 级)。
25
+ */
26
+ import { SessionError } from "@sema-agent/core";
27
+ import { recordFailOpen } from "../observability/fail-open.js";
1
28
  const ROUTE_MAX = 8192;
2
29
  export class ForkRoutingSessionStore {
3
30
  host;
@@ -10,9 +37,26 @@ export class ForkRoutingSessionStore {
10
37
  * 亲读),逐出后 transient-first 的 requireExisting 会把陈旧快照端回给 revive。本集不逐出(上界=
11
38
  * parked 子代数,天然小);pick/acquire 首查。 */
12
39
  promoted = new Set();
40
+ /** #292 placement 半场:placed id 名册(与 promoted 同姿势——不随路由 LRU 逐出)。placed 会话的
41
+ * 真居所恒=host(分区在 durable 店),任何形的 acquire/touch/release 都不得落 transient——
42
+ * 否则 transient 的 claim-create 会铸出同 id 影子会话,requireExisting 的 transient-first 顺序
43
+ * 会让影子遮蔽真转录(F-7 顺序对 placed 反向成害)。 */
44
+ placed = new Set();
45
+ /** #292:host 店的 placements 声明透传(声明制——包装层不 duck-type,host 声明了才武装 placed
46
+ * 路由;host 不声明时本店全行为与 5.47 前逐字节同)。 */
47
+ placements;
48
+ /** #292:分区枚举透传(host 有才有——恒在的空实现会把「不能枚举」谎报成「没有 placed 会话」)。 */
49
+ listPlaced;
13
50
  constructor(host, transient) {
14
51
  this.host = host;
15
52
  this.transient = transient;
53
+ // #292 声明/枚举面:host 声明什么透传什么(transient 永不声明 placements——分区住 durable 店)。
54
+ if (host.placements !== undefined)
55
+ this.placements = host.placements;
56
+ const hostListPlaced = host.listPlaced;
57
+ if (typeof hostListPlaced === "function") {
58
+ this.listPlaced = (kind, o) => hostListPlaced.call(host, kind, o);
59
+ }
16
60
  // backlog(hasSessionFork 恒真):两 backing 都无 fork 面时,包装店的 fork 方法**摘除**——core 的
17
61
  // fork 门(hasSessionFork(opts.runner.sessions))才能诚实报 fork.unavailable,而不是放行后折叠成
18
62
  // 误导性的「source not found」。现三 backing 均有 fork,此臂为接入无 fork durable backend 备。
@@ -30,29 +74,71 @@ export class ForkRoutingSessionStore {
30
74
  }
31
75
  }
32
76
  pick(id) {
33
- if (this.promoted.has(id))
34
- return this.host; // [1593] 晋升名册优先(路由可被 LRU 逐出,名册不逐)
77
+ if (this.promoted.has(id) || this.placed.has(id))
78
+ return this.host; // 名册优先(路由可被 LRU 逐出,名册不逐;placed 同姿势 #292)
35
79
  const r = this.route.get(id);
36
80
  if (r !== undefined)
37
81
  this.remember(id, r); // refresh recency
38
82
  return r === "host" ? this.host : this.transient;
39
83
  }
84
+ /** #292:host 声明分区时的冷缓存判位探针。判据=host `requireExisting` acquire 的 `SessionMetadata.placement`
85
+ * (5.48 additive,File/SQL/Ttl 三形都带——比 `placementOf` 面更普适:Ttl 缓存层不透传 placementOf)。
86
+ * placed 命中把 acquired 会话一并交回(调用方正需要它时零二次 acquire);非 placed/absent 的探针
87
+ * acquire 一律 **forget** 收尾([4743] 义务①:验存在的 acquire 禁 release——release(placed)=真删)。 */
88
+ async hostPlacedProbe(id) {
89
+ if (this.placed.has(id)) {
90
+ const acquired = await this.host.acquire(id, { requireExisting: true });
91
+ return { state: "placed", acquired };
92
+ }
93
+ if (this.placements?.subagent === undefined)
94
+ return { state: "absent" };
95
+ let acquired;
96
+ try {
97
+ acquired = await this.host.acquire(id, { requireExisting: true });
98
+ }
99
+ catch (err) {
100
+ // not_found=常态(绝大多数 id 非 placed),静默折 absent;**真故障**(host 不可达/读错)同样折
101
+ // absent 但计数留痕(#191 门②:F 类兜底必走 recordFailOpen)——放行的是 transient 腿在 host
102
+ // 故障窗内保持独立可用,代价与恢复路径见 tag 注册表。
103
+ if (!(err instanceof SessionError && err.code === "not_found")) {
104
+ recordFailOpen("server.fork-routing.placed-probe-host-error", err instanceof Error ? err.message : String(err));
105
+ }
106
+ return { state: "absent" };
107
+ }
108
+ const meta = await acquired.session.getMetadata();
109
+ if (meta.placement !== undefined) {
110
+ this.placed.add(id);
111
+ return { state: "placed", acquired };
112
+ }
113
+ await this.host.forget?.(id); // 只为验居所:丢缓存引用,内容不动(禁 release)
114
+ return { state: "unplaced" };
115
+ }
40
116
  async acquire(sessionId, opts) {
41
117
  if (sessionId === undefined) {
42
- const a = await this.transient.acquire();
118
+ const a = await this.transient.acquire(undefined, opts);
43
119
  this.remember(a.sessionId, "transient");
44
120
  return a;
45
121
  }
46
- if (this.promoted.has(sessionId)) {
122
+ // #292 建会话入分区形(受信 spawner:带 placement、不 requireExisting)——分区住 host durable 店;
123
+ // host 不声明 placements 时按旧行为落 transient(core 铸造门本就不会给这种部署铸 placement,
124
+ // 这臂只是防御性一致)。
125
+ if (opts?.placement !== undefined && opts.requireExisting !== true && this.placements?.subagent !== undefined) {
126
+ const a = await this.host.acquire(sessionId, opts);
127
+ this.placed.add(sessionId);
128
+ this.remember(sessionId, "host");
129
+ return a;
130
+ }
131
+ if (this.promoted.has(sessionId) || this.placed.has(sessionId)) {
47
132
  // [1593] 晋升名册:恒走 host——绝不能让 transient-first 把 forget 后仍活在私店 repo 里的陈旧
48
- // 快照端给 revive(探针 Finding A)。
133
+ // 快照端给 revive(探针 Finding A)。placed 名册同规则(#292:影子遮蔽真转录的反向 F-7)。
49
134
  const a = await this.host.acquire(sessionId, opts);
50
135
  this.remember(sessionId, "host");
51
136
  return a;
52
137
  }
53
138
  const routed = this.route.get(sessionId);
54
- if (routed !== undefined) {
55
- const a = await this.pick(sessionId).acquire(sessionId, opts);
139
+ if (routed === "host") {
140
+ const a = await this.host.acquire(sessionId, opts);
141
+ this.remember(sessionId, "host");
56
142
  return a;
57
143
  }
58
144
  if (opts?.requireExisting) {
@@ -60,9 +146,19 @@ export class ForkRoutingSessionStore {
60
146
  // transient(miss)→ fallback host 命中,语义不变。顺序反转封住 F-7 组合链:同 id 在两店并存时
61
147
  // (child uuid 被同租户误提交到主 API 而 host 店 claim-create 出空壳),真子会话(transient)赢,
62
148
  // 空壳不遮蔽。双 miss 抛 host 侧错误(响亮,fail-closed 到「真不存在」)。
149
+ // #292 例外:transient 命中且 host 声明分区时,还要过一次 placed 判位——同 id 若在 host 是 placed
150
+ // 会话,transient 那份是影子(claim-create 铸的),真转录必须赢(F-7 顺序对 placed 反向成害)。
63
151
  let transientErr;
64
152
  try {
65
153
  const a = await this.transient.acquire(sessionId, opts);
154
+ if (this.placements?.subagent !== undefined) {
155
+ const probe = await this.hostPlacedProbe(sessionId);
156
+ if (probe.state === "placed" && probe.acquired !== undefined) {
157
+ await this.transient.forget?.(sessionId); // 影子只丢引用(私店 repo 行本就 sweep 不到,名册是真防线)
158
+ this.remember(sessionId, "host");
159
+ return probe.acquired;
160
+ }
161
+ }
66
162
  this.remember(sessionId, "transient");
67
163
  return a;
68
164
  }
@@ -71,6 +167,20 @@ export class ForkRoutingSessionStore {
71
167
  }
72
168
  try {
73
169
  const a = await this.host.acquire(sessionId, opts);
170
+ // 顺手:host 端回的会话若带 placement 元组,入名册(后续零探针直达)。只在声明分区的部署上读
171
+ // (非声明形零行为差),且 meta 读失败不阻断 acquire(名册只是省探针的缓存,不是判定)。
172
+ if (this.placements?.subagent !== undefined) {
173
+ try {
174
+ const meta = await a.session.getMetadata();
175
+ if (meta.placement !== undefined)
176
+ this.placed.add(sessionId);
177
+ }
178
+ catch (err) {
179
+ // 名册只是省探针的缓存,不是判定——缺一条=该 id 下次多付一次 hostPlacedProbe,正确性零影响;
180
+ // 计数留痕(#191 门②)让「meta 一直读失败」的环境病在遥测显形。
181
+ recordFailOpen("server.fork-routing.placed-roster-meta-unreadable", err instanceof Error ? err.message : String(err));
182
+ }
183
+ }
74
184
  this.remember(sessionId, "host");
75
185
  return a;
76
186
  }
@@ -80,6 +190,17 @@ export class ForkRoutingSessionStore {
80
190
  }
81
191
  }
82
192
  // 带 id 但不要求已存在:claim-create 只允许落 transient(durable 店的 claim-create 是 authorizer 的职权)。
193
+ // #292 claim closure(义务⑥过墙形):id 若已是 host 分区的 placed 会话,claim-create 落 transient 会
194
+ // 铸同 id 影子——委托给 host claim 形,让其 placement_refused 响亮拒(绝不静默铸影子)。
195
+ if (this.placements?.subagent !== undefined) {
196
+ const probe = await this.hostPlacedProbe(sessionId);
197
+ if (probe.state === "placed") {
198
+ await this.host.forget?.(sessionId); // 探针引用丢弃(refusal 路径不把会话交出)
199
+ const a = await this.host.acquire(sessionId, opts); // claim 形打声明店 ⇒ placement_refused(义务⑥)
200
+ this.remember(sessionId, "host");
201
+ return a; // 不可达(上一行必抛);保留形以防 host 语义演化时静默吞
202
+ }
203
+ }
83
204
  const a = await this.transient.acquire(sessionId, opts);
84
205
  this.remember(sessionId, "transient");
85
206
  return a;
@@ -151,7 +272,9 @@ export class ForkRoutingSessionStore {
151
272
  catch {
152
273
  // 私店无此会话:可能已在 host(直查确认=幂等),否则如实抛(host 侧错误响亮上浮)
153
274
  const probe = await this.host.acquire(sessionId, { requireExisting: true });
154
- await this.host.release(probe.sessionId);
275
+ // #292 preflight 纪律([4743] 义务①):**只为验存在**的 acquire 收尾一律 forget —— host 店的
276
+ // release 对 placed 会话是真删除(core 5.48 义务 4),用它给一次探活收尾就是拿删除当探针。
277
+ await this.host.forget?.(probe.sessionId);
155
278
  this.promoted.add(sessionId);
156
279
  this.remember(sessionId, "host");
157
280
  return;
@@ -179,7 +302,9 @@ export class ForkRoutingSessionStore {
179
302
  throw new Error(`session "${sessionId}" promotion verification failed (leaf mismatch after import)`);
180
303
  }
181
304
  finally {
182
- await this.host.release(sessionId);
305
+ // #292:晋升写完只需**丢引用**(内容必须留下,那正是晋升的目的)⇒ forget,不是 release
306
+ // (同上:release 对 placed 会话已是真删除,而这条 finally 连失败路径都会走)。
307
+ await this.host.forget?.(sessionId);
183
308
  }
184
309
  try {
185
310
  await this.transient.forget?.(sessionId);
@@ -64,6 +64,7 @@ export declare class PgMemoryEngineBackend implements MemoryBackend {
64
64
  getByIds(ids: readonly string[]): Promise<MemoryEntry[]>;
65
65
  search(query: string, scopes: readonly string[], opts?: {
66
66
  limit?: number;
67
+ exposureBands?: boolean;
67
68
  }): Promise<ScoredMemoryEntry[]>;
68
69
  applyPatches(patches: readonly NotePatch[]): Promise<PatchReport>;
69
70
  private applyOne;
@@ -25,7 +25,7 @@ import { computeEntryRev, serializeEntryFile } from "@sema-agent/core";
25
25
  import { pgSafeJsonStringify, pgHasUnstorable } from "./pg-safe-json.js";
26
26
  import { assertMemoryScopeWidth, memoryEntryKeyWidthRefusal } from "./memory-key-guards.js"; // R5 批γ + #271 件1:scope/slug 写前宽守卫
27
27
  // #307 件6(core 5.46.0 design/336 §2.3-2):external-origin 不可变律 —— 两支 SQL 孪生共用**一份**判据。
28
- import { createBatchOriginBaseline, originWhitewashRefusal, rememberBatchOrigin, unknownPatchOpRefusal } from "./memory-origin-law.js";
28
+ import { ambiguousOriginRefusal, createBatchOriginBaseline, exposureBandOf, exposureCarriage, originMarkedSqlPredicate, originWhitewashRefusal, rememberBatchOrigin, unknownPatchOpRefusal, } from "./memory-origin-law.js";
29
29
  /** Table names (single source). Deliberately DISJOINT from the legacy `agent_memory*` tables —
30
30
  * the retired MemoryStore plane and this entry plane must never cross-write. */
31
31
  export const PG_MEMORY_ENGINE_TABLES = {
@@ -133,6 +133,9 @@ export class PgMemoryEngineBackend {
133
133
  mtimeMs: Number(r.mtime_ms),
134
134
  rev: r.rev,
135
135
  sizeBytes: Number(r.size_bytes),
136
+ // #319(design/336 §5.2):committed external-origin 事实随**每一个**头面(listHeaders + search)。
137
+ // 归一化只有一份(memory-origin-law.exposureCarriage → core 的 committedOriginOf),两支方言同源。
138
+ ...exposureCarriage(fm),
136
139
  };
137
140
  }
138
141
  entryFromRow(r) {
@@ -163,6 +166,11 @@ export class PgMemoryEngineBackend {
163
166
  return res.rows.map((r) => this.entryFromRow(r)); // unknown ids: silently absent
164
167
  }
165
168
  async search(query, scopes, opts) {
169
+ // #319(design/336 §5.2):两带序 —— 未标记先、已标记后,**带序在截断之前**(理由见
170
+ // memory-origin-law.exposureBandOf 的顶注)。缺席/false ⇒ 单带序,与本方法 pre-336 形逐字节同。
171
+ const bands = opts?.exposureBands === true;
172
+ /** 契约序:(带号 ‖ 缺席时恒 0) → score 升 → id。三档共用一只比较器,谁都别自己写一份。 */
173
+ 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);
166
174
  // Normalize the limit ONCE for all three rungs — `Math.floor`
167
175
  // makes a fractional limit (0.5) behave like File's `slice` (→ 0 → empty), `!(lim > 0)` also
168
176
  // rejects NaN (NaN > 0 is false — it previously reached the native rung's interpolated LIMIT as
@@ -182,12 +190,23 @@ export class PgMemoryEngineBackend {
182
190
  const qv = await this.embedder.embed(query);
183
191
  if (Array.isArray(qv) && qv.length === this.embedder.dimensions) {
184
192
  const vecParam = scopes.length + 1;
193
+ // #319:本档的排序**和截断都在 SQL 里**,所以两带序也必须进 ORDER BY —— 在进程里对一页
194
+ // 已截断的结果重排是排不出来的(那正是契约要拒的「干净条目被饿死」)。带号谓词=不可变律
195
+ // 模块里那条 `committedOriginOf` 的下推形(逐元素跑 core 的行首正则,与 JS 判据机器对表;
196
+ // 为什么不能图省事写成 LIKE,见 originMarkedSqlPredicate 的顶注)。
197
+ const bandExpr = bands ? `(CASE WHEN ${originMarkedSqlPredicate("frontmatter")} THEN 1 ELSE 0 END) ASC, ` : "";
185
198
  const res = await this.query(`SELECT id, scope, slug, frontmatter, rev, mtime_ms, size_bytes, embedding <=> $${vecParam}::vector AS dist
186
199
  FROM ${PG_MEMORY_ENGINE_TABLES.entry}
187
200
  WHERE scope IN (${inPlaceholders(1, scopes.length)}) AND embedding IS NOT NULL
188
- ORDER BY embedding <=> $${vecParam}::vector ASC
201
+ ORDER BY ${bandExpr}embedding <=> $${vecParam}::vector ASC, id ASC
189
202
  LIMIT ${Math.min(limit, 100_000)}`, [...scopes, vectorLiteral(qv)]);
190
- return res.rows.map((r) => ({ ...this.headerFromRow(r), score: Number(r.dist) }));
203
+ // ⚠️ `id ASC` 必须写在 SQL 的 ORDER BY 里、也就是 LIMIT **之前**(codex 对抗复审 R1-[medium]):
204
+ // 同分(同一条向量距离)超过 limit 条时,决定「谁被截掉」的是这一句;缺了它,留下哪几条由执行
205
+ // 计划随机决定,页内怎么重排都换不回被截掉的那条。列是 `text COLLATE "C"` ⇒ 字节序,与下面
206
+ // 进程内比较器的定序同向。
207
+ const hits = res.rows.map((r) => ({ ...this.headerFromRow(r), score: Number(r.dist) }));
208
+ // 页内再排一次:契约序的最终裁决在进程侧(单带时是恒等重排)。
209
+ return hits.sort(contractOrder);
191
210
  }
192
211
  // Wrong-dimension query vector (transient embedder fault) — fall open to the in-process path.
193
212
  }
@@ -214,7 +233,8 @@ export class PgMemoryEngineBackend {
214
233
  }
215
234
  // opus 审 C4: deterministic id tie-break — tied scores must not order by row-arrival accident
216
235
  // (the cross-backend equivalence assertion compares ordered projections; File mirrors this).
217
- scored.sort((a, b) => a.score - b.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
236
+ // #319:整档在进程内排完**再**切片 两带序天然落在截断之前(SQL 侧无 LIMIT 可绕过)
237
+ scored.sort(contractOrder);
218
238
  return scored.slice(0, limit);
219
239
  }
220
240
  async applyPatches(patches) {
@@ -293,6 +313,16 @@ export class PgMemoryEngineBackend {
293
313
  return;
294
314
  }
295
315
  }
316
+ // #319 增量([4768] 勘误 + core `1a5ebaa1`):**歧义 origin 表示**零写拒。判位与 File 的 planOne
317
+ // 逐字同 —— 表示合法性先于状态比较:排在键形守卫之后、add/update 两支的 guard/跨 scope/白洗/CAS
318
+ // 之前,且在任何 I/O 之前。判据用 core 导出的谓词(禁手抄),文案照抄 File(跨后端同一输入同一答复)。
319
+ if (patch.op !== "delete" && patch.entry !== undefined) {
320
+ const refusal = ambiguousOriginRefusal(patch.entry.frontmatter);
321
+ if (refusal !== undefined) {
322
+ report.conflicts.push({ op: patch.op, id: patch.id, reason: refusal });
323
+ return;
324
+ }
325
+ }
296
326
  if (patch.op === "add") {
297
327
  const rawEntry = patch.entry;
298
328
  if (!rawEntry) {
@@ -51,6 +51,7 @@ export declare class TiDBMemoryEngineBackend implements MemoryBackend {
51
51
  getByIds(ids: readonly string[]): Promise<MemoryEntry[]>;
52
52
  search(query: string, scopes: readonly string[], opts?: {
53
53
  limit?: number;
54
+ exposureBands?: boolean;
54
55
  }): Promise<ScoredMemoryEntry[]>;
55
56
  applyPatches(patches: readonly NotePatch[]): Promise<PatchReport>;
56
57
  private applyOne;
@@ -26,7 +26,7 @@ import { computeEntryRev, serializeEntryFile } from "@sema-agent/core";
26
26
  import { pgHasUnstorable } from "./pg-safe-json.js";
27
27
  import { assertMemoryScopeWidth, memoryEntryKeyWidthRefusal } from "./memory-key-guards.js"; // R5 批γ + #271 件1:scope/slug 写前宽守卫
28
28
  // #307 件6(core 5.46.0 design/336 §2.3-2):external-origin 不可变律 —— 与 Pg 孪生共用**一份**判据。
29
- import { createBatchOriginBaseline, originWhitewashRefusal, rememberBatchOrigin, unknownPatchOpRefusal } from "./memory-origin-law.js";
29
+ import { ambiguousOriginRefusal, createBatchOriginBaseline, exposureBandOf, exposureCarriage, originWhitewashRefusal, rememberBatchOrigin, unknownPatchOpRefusal, } from "./memory-origin-law.js";
30
30
  import { isMysqlDupKeyError } from "./sql-errors.js";
31
31
  /** Table names (single source) — SAME names as PG_MEMORY_ENGINE_TABLES (the two dialects never share
32
32
  * one database), deliberately DISJOINT from the legacy `agent_memory*` MemoryStore plane. */
@@ -117,6 +117,8 @@ export class TiDBMemoryEngineBackend {
117
117
  mtimeMs: Number(r.mtime_ms),
118
118
  rev: r.rev,
119
119
  sizeBytes: Number(r.size_bytes),
120
+ // #319(design/336 §5.2,Pg 孪生同注):committed external-origin 事实随每一个头面;归一化同源。
121
+ ...exposureCarriage(fm),
120
122
  };
121
123
  }
122
124
  entryFromRow(r) {
@@ -146,6 +148,9 @@ export class TiDBMemoryEngineBackend {
146
148
  return rows.map((r) => this.entryFromRow(r)); // unknown ids: silently absent
147
149
  }
148
150
  async search(query, scopes, opts) {
151
+ // #319(design/336 §5.2,Pg 孪生同注):两带序 —— 未标记先,**带序在截断之前**。本方言只有词面档,
152
+ // 整档在进程内排完再切片,所以「带序先于截断」天然成立(无 SQL LIMIT 可绕过)。
153
+ const bands = opts?.exposureBands === true;
149
154
  // Limit normalization mirrors the Pg twin verbatim (floor semantics; NaN rejected; Infinity = no cap).
150
155
  const rawLimit = opts?.limit ?? 20;
151
156
  const limit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : rawLimit;
@@ -168,7 +173,7 @@ export class TiDBMemoryEngineBackend {
168
173
  }
169
174
  // Deterministic id tie-break (opus 审 C4,Pg 版同注): tied scores must not order by row-arrival
170
175
  // accident — the cross-backend equivalence assertion compares ordered projections.
171
- scored.sort((a, b) => a.score - b.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
176
+ scored.sort((a, b) => (bands ? exposureBandOf(a) - exposureBandOf(b) : 0) || a.score - b.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
172
177
  return scored.slice(0, limit);
173
178
  }
174
179
  async applyPatches(patches) {
@@ -242,6 +247,16 @@ export class TiDBMemoryEngineBackend {
242
247
  return;
243
248
  }
244
249
  }
250
+ // #319 增量([4768] 勘误 + core `1a5ebaa1`,Pg 孪生同注):**歧义 origin 表示**零写拒。判位与
251
+ // File 的 planOne 逐字同(表示合法性先于状态比较,且在任何 I/O 之前);判据用 core 导出的谓词
252
+ // (禁手抄——解 extra 里的 origin 形块那只解析器一旦解得比 core 宽松,就是给洗白开后门)。
253
+ if (patch.op !== "delete" && patch.entry !== undefined) {
254
+ const refusal = ambiguousOriginRefusal(patch.entry.frontmatter);
255
+ if (refusal !== undefined) {
256
+ report.conflicts.push({ op: patch.op, id: patch.id, reason: refusal });
257
+ return;
258
+ }
259
+ }
245
260
  if (patch.op === "add") {
246
261
  const entry = patch.entry;
247
262
  if (!entry) {
@@ -37,6 +37,75 @@
37
37
  * (core 契约逐字要求的那条)才成立。
38
38
  */
39
39
  import { type MemoryEntryFrontmatter, type MemoryEntryOrigin } from "@sema-agent/core";
40
+ /**
41
+ * #319 增量(sema-comms [4768] 勘误 + core 仓 `1a5ebaa1`,conformance 30→31)—— **歧义 origin 表示**
42
+ * 的拒绝门,与不可变律同批、同一个写路径上。
43
+ *
44
+ * ## 病
45
+ *
46
+ * 同一条目**同时**携带 typed `frontmatter.origin` 与一个**冲突的** origin 形 `extra` 块(或多个彼此
47
+ * 冲突的 extra 块)时:不可变律的比较器走 `committedOriginOf`,而它在 typed 在场时优先读 typed ——
48
+ * 于是那枚 typed 满足「标记原样带下去」,冲突的块却照样进了存储。任何一次**重解析**(File 的投影
49
+ * 往返 / sync 对拍 / bundle 校验)都可能答出**另一枚**标记 ⇒ 成员改写的载具。不可变律本身没漏,
50
+ * 漏的是「根本不该收下这种表示」这一步。
51
+ *
52
+ * ## 钥匙在 DISAGREEMENT,不在载体数
53
+ *
54
+ * 载体彼此**一致**的多载形(typed 旁边一个逐成员相等的 extra 形)是合法的 legacy 承载,必须照收 ——
55
+ * 那正是序列化闭合(同一条记录在 File 投影里往返一圈就长这样)。「见到 extra 就拒」会把正常的
56
+ * legacy 条目全部打死。
57
+ *
58
+ * ## 判据禁手抄
59
+ *
60
+ * 谓词一律用 core 导出的 {@link ambiguousOriginRepresentation}:它要解 `extra` 里的 origin 形块
61
+ * (跨块字段组合、解析期块重排都算歧义),自己写一份近似的等于给洗白开后门 —— 这条在 #307 就写死过。
62
+ * core 5.47.0 起该谓词进公开导出面([4698] 请托兑现),本仓的「撤除条件钉」也因此在 5.47 提货当天
63
+ * 准时翻红,本批就是那条钉指定的兑现动作。
64
+ *
65
+ * ## 文案与 File 逐字同
66
+ *
67
+ * 拒绝文案照抄 core `file-backend.ts` 的 `planOne`(亲读 `1a5ebaa1` 前后两版确认未变),因为
68
+ * 契约按 `/representation conflict|malformed patch refused/` 匹配,而**跨后端同一输入必须同一答复**
69
+ * —— 消费方按 reason 分类处置,两支方言各写各的文案就等于让同一件事在不同部署上分叉。
70
+ *
71
+ * ## 判位
72
+ *
73
+ * 与 File 同precedence:**表示合法性先于状态比较** —— 排在 id/键形守卫之后、add/update 分支
74
+ * (guard 算术、跨 scope 拒、白洗判据、rev CAS)之前,且在**任何 I/O 之前**(零写拒)。
75
+ */
76
+ export declare function ambiguousOriginRefusal(fm: MemoryEntryFrontmatter): string | undefined;
77
+ /**
78
+ * #319(core 5.47/5.48 契约冻结面,design/336 §5.2)—— **头面 exposure 事实**的同一份归一化。
79
+ *
80
+ * 契约逐字:`listHeaders` 与 `search` 两个头面都要携带 `exposure`,且**同一条归一化**
81
+ * (`committedOriginOf`:typed `frontmatter.origin` 与 legacy 的 `extra` origin 形字节块两种承载,
82
+ * 一次判、两面同答)。这是**数据事实**不是模式:后端恒报库里存着什么,读侧怎么处置(不透明句柄/
83
+ * 横幅/两带序)归引擎层的 provenance 模式。缺席 ⇔ 没标记(不等于「证明干净」)。
84
+ *
85
+ * 为什么与不可变律同住一个模块:两者读的是**同一个** committed origin。判据(白洗拒)与事实
86
+ * (exposure 携带)分家写,迟早在某一支方言上一边认得 extra 形、另一边只认 typed 形 —— 那时
87
+ * 「洗不掉的标记」在检索面**看不见**,而两支后端谁都不会报错。
88
+ */
89
+ export declare function exposureCarriage(fm: MemoryEntryFrontmatter): {
90
+ exposure?: "external";
91
+ };
92
+ /**
93
+ * 两带序的带号:未标记 = 0(**先**),已标记 = 1。
94
+ *
95
+ * 契约的载荷理由(core r1-9,逐字转述):两带序必须落在**截断之前** —— 一页 limit 全是带标条目时,
96
+ * 排在 limit+1 位的干净条目会被饿死,而工具层**无法**从一页已截断的结果里把它捞回来。
97
+ * `exposureBands` 缺席/false ⇒ 单带序,与 pre-336 逐字节同(`provenance:"off"` 的读面)。
98
+ */
99
+ export declare function exposureBandOf(header: {
100
+ exposure?: "external";
101
+ }): 0 | 1;
102
+ /** core `/^\s*origin\s*:/`(`extraOriginCarriers` 的开块判据)的 SQL 孪生。 */
103
+ export declare const ORIGIN_CARRIER_LINE_SQL_REGEX = "^[\\u0009\\u000a\\u000b\\u000c\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*origin[\\u0009\\u000a\\u000b\\u000c\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*:";
104
+ /**
105
+ * 「这一行带 committed origin 标记」的 SQL 布尔式(两种承载形同判,与 {@link exposureCarriage} 同源)。
106
+ * `column` = 该表 frontmatter 列的表达式(调用方自己保证它是列名/限定名,**不接受外来串**)。
107
+ */
108
+ export declare function originMarkedSqlPredicate(column: string): string;
40
109
  /** 本批次的「已提交 origin」基线:id → 本批**第一次**触碰它时库里那一份(无行 ⇒ `undefined`)。 */
41
110
  export type BatchOriginBaseline = Map<string, MemoryEntryOrigin | undefined>;
42
111
  /** 新建一只批次基线(每次 `applyPatches` 一只;跨批次**不得**复用 —— 已提交的墓碑正是靠「换批清零」放行)。 */
@@ -36,7 +36,124 @@
36
36
  * 之后同批的任何 op 都按那一份判。⇒「同批 delete+re-add 拒 / 已提交墓碑后 add 放」这条分界
37
37
  * (core 契约逐字要求的那条)才成立。
38
38
  */
39
- import { committedOriginOf, originEquals } from "@sema-agent/core";
39
+ import { ambiguousOriginRepresentation, committedOriginOf, originEquals } from "@sema-agent/core";
40
+ /**
41
+ * #319 增量(sema-comms [4768] 勘误 + core 仓 `1a5ebaa1`,conformance 30→31)—— **歧义 origin 表示**
42
+ * 的拒绝门,与不可变律同批、同一个写路径上。
43
+ *
44
+ * ## 病
45
+ *
46
+ * 同一条目**同时**携带 typed `frontmatter.origin` 与一个**冲突的** origin 形 `extra` 块(或多个彼此
47
+ * 冲突的 extra 块)时:不可变律的比较器走 `committedOriginOf`,而它在 typed 在场时优先读 typed ——
48
+ * 于是那枚 typed 满足「标记原样带下去」,冲突的块却照样进了存储。任何一次**重解析**(File 的投影
49
+ * 往返 / sync 对拍 / bundle 校验)都可能答出**另一枚**标记 ⇒ 成员改写的载具。不可变律本身没漏,
50
+ * 漏的是「根本不该收下这种表示」这一步。
51
+ *
52
+ * ## 钥匙在 DISAGREEMENT,不在载体数
53
+ *
54
+ * 载体彼此**一致**的多载形(typed 旁边一个逐成员相等的 extra 形)是合法的 legacy 承载,必须照收 ——
55
+ * 那正是序列化闭合(同一条记录在 File 投影里往返一圈就长这样)。「见到 extra 就拒」会把正常的
56
+ * legacy 条目全部打死。
57
+ *
58
+ * ## 判据禁手抄
59
+ *
60
+ * 谓词一律用 core 导出的 {@link ambiguousOriginRepresentation}:它要解 `extra` 里的 origin 形块
61
+ * (跨块字段组合、解析期块重排都算歧义),自己写一份近似的等于给洗白开后门 —— 这条在 #307 就写死过。
62
+ * core 5.47.0 起该谓词进公开导出面([4698] 请托兑现),本仓的「撤除条件钉」也因此在 5.47 提货当天
63
+ * 准时翻红,本批就是那条钉指定的兑现动作。
64
+ *
65
+ * ## 文案与 File 逐字同
66
+ *
67
+ * 拒绝文案照抄 core `file-backend.ts` 的 `planOne`(亲读 `1a5ebaa1` 前后两版确认未变),因为
68
+ * 契约按 `/representation conflict|malformed patch refused/` 匹配,而**跨后端同一输入必须同一答复**
69
+ * —— 消费方按 reason 分类处置,两支方言各写各的文案就等于让同一件事在不同部署上分叉。
70
+ *
71
+ * ## 判位
72
+ *
73
+ * 与 File 同precedence:**表示合法性先于状态比较** —— 排在 id/键形守卫之后、add/update 分支
74
+ * (guard 算术、跨 scope 拒、白洗判据、rev CAS)之前,且在**任何 I/O 之前**(零写拒)。
75
+ */
76
+ export function ambiguousOriginRefusal(fm) {
77
+ if (!ambiguousOriginRepresentation(fm))
78
+ return undefined;
79
+ return "external-origin marker representation conflict: the entry carries conflicting or duplicated origin representations (malformed patch refused)";
80
+ }
81
+ /**
82
+ * #319(core 5.47/5.48 契约冻结面,design/336 §5.2)—— **头面 exposure 事实**的同一份归一化。
83
+ *
84
+ * 契约逐字:`listHeaders` 与 `search` 两个头面都要携带 `exposure`,且**同一条归一化**
85
+ * (`committedOriginOf`:typed `frontmatter.origin` 与 legacy 的 `extra` origin 形字节块两种承载,
86
+ * 一次判、两面同答)。这是**数据事实**不是模式:后端恒报库里存着什么,读侧怎么处置(不透明句柄/
87
+ * 横幅/两带序)归引擎层的 provenance 模式。缺席 ⇔ 没标记(不等于「证明干净」)。
88
+ *
89
+ * 为什么与不可变律同住一个模块:两者读的是**同一个** committed origin。判据(白洗拒)与事实
90
+ * (exposure 携带)分家写,迟早在某一支方言上一边认得 extra 形、另一边只认 typed 形 —— 那时
91
+ * 「洗不掉的标记」在检索面**看不见**,而两支后端谁都不会报错。
92
+ */
93
+ export function exposureCarriage(fm) {
94
+ return committedOriginOf(fm) !== undefined ? { exposure: "external" } : {};
95
+ }
96
+ /**
97
+ * 两带序的带号:未标记 = 0(**先**),已标记 = 1。
98
+ *
99
+ * 契约的载荷理由(core r1-9,逐字转述):两带序必须落在**截断之前** —— 一页 limit 全是带标条目时,
100
+ * 排在 limit+1 位的干净条目会被饿死,而工具层**无法**从一页已截断的结果里把它捞回来。
101
+ * `exposureBands` 缺席/false ⇒ 单带序,与 pre-336 逐字节同(`provenance:"off"` 的读面)。
102
+ */
103
+ export function exposureBandOf(header) {
104
+ return header.exposure === "external" ? 1 : 0;
105
+ }
106
+ /**
107
+ * `committedOriginOf` 的**下推形**:同一条判据的 SQL 写法,给「排序与截断都发生在 SQL 里」的那一档
108
+ * (Pg 的原生 pgvector 档)用。
109
+ *
110
+ * ## 为什么非要下推(不能只在进程里排)
111
+ *
112
+ * 原生档 `ORDER BY … LIMIT n` 是**库里**排完就截断的:进程拿到的已经是一页 n 行,被截掉的那条干净
113
+ * 条目在页里根本不存在,重排救不回来 —— 而「带序必须先于截断」正是这条契约的载荷所在。所以带号
114
+ * 必须进 ORDER BY,也就必须有一份 SQL 侧的判据。
115
+ *
116
+ * ## 为什么写成逐元素的行首正则(codex 对抗复审 R1-[high] 的直接产物)
117
+ *
118
+ * 初版图省事写的是 `frontmatter->>'extra' LIKE '%origin:%'`,自称「只多认不漏认」。**错了**,两个方向
119
+ * 都不成立:`['origin :']`(冒号前有空格)是 core 认的合法承载形,而 LIKE 漏掉它 —— 一条**带标记**的
120
+ * 条目被排进干净带,占掉的正是干净条目的位置;反过来 `['prefix origin: suffix']` 被 LIKE 误认,一条
121
+ * **干净**条目沉进后带、可能被截断饿死。两个方向的结局是同一个:干净条目被饿死,而这正是本条款要拒的。
122
+ *
123
+ * 现形逐元素跑 core 那条行首正则(`/^\s*origin\s*:/`)。真库实测 10 种拼写(裸 / 冒号前空格 / 前导
124
+ * 空格 / 制表 / 全角空格 U+3000 / 行中出现 / originx: / 非 origin 行 …)与 JS 判据**逐条同答**。
125
+ *
126
+ * ## 这份「第二实现」的同步义务
127
+ *
128
+ * 两份实现同表一个语义 = 本仓明令的危险形,所以它带**机器对表**:
129
+ * `test/memory-engine-db-integration.test.ts` 的钉6 拿同一组拼写在**真 PG** 上让本谓词与
130
+ * `committedOriginOf` 逐条对答,不一致当场红(离线格做不了这件事:被测的正是 PG 自己的正则引擎)。
131
+ * core 改那条正则时,红的是对表格 —— 不是某台部署上悄悄跑偏的排序。
132
+ *
133
+ * ## 字符类为什么是**穷举码点**而不是 `[[:space:]]`(codex 第二轮 R2-[medium],采纳)
134
+ *
135
+ * PG 的 POSIX 字符类**跟着数据库 LC_CTYPE 走**:同一条 SQL 在 C locale 与某个 UTF-8 locale 下认的
136
+ * 空白集合可以不一样(例如 U+0085 NEL —— JS 的 `\s` **不**含它)。而本谓词的正确性判据是「与 JS 的
137
+ * `\s` 逐字同」,一条依赖部署 locale 的判据等于把同一条记忆在两台机器上排进不同的带 —— 而带号错了
138
+ * 就会在 LIMIT 那一刀上不可恢复地截掉本该入页的干净条目。所以这里**不用**任何 locale 敏感构造,把
139
+ * ECMAScript `\s` 的码点逐个写死(PG 的 ARE 支持 `\uXXXX` 转义,已在真库上验过);对表格里同时钉了
140
+ * U+0085 这条**反例**(JS 不认 ⇒ SQL 也必须不认),locale 一旦重新混进来它当场红。
141
+ */
142
+ /** ⚠️ 本常量**只准写 ASCII**:那些 unicode 空白必须以 `\\uXXXX` 的**字面反斜杠形**交给 PG 的 ARE
143
+ * 自己解析(已在真库上验过)。把真字符敲进源码会同时踩两条线 —— 源码卫生门(不可见字节)与
144
+ * U+2028/U+2029(它们在 JS 源码里就是换行,会把这一行劈开)。 */
145
+ const SQL_JS_WHITESPACE_CLASS = "[\\u0009\\u000a\\u000b\\u000c\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]";
146
+ /** core `/^\s*origin\s*:/`(`extraOriginCarriers` 的开块判据)的 SQL 孪生。 */
147
+ export const ORIGIN_CARRIER_LINE_SQL_REGEX = `^${SQL_JS_WHITESPACE_CLASS}*origin${SQL_JS_WHITESPACE_CLASS}*:`;
148
+ /**
149
+ * 「这一行带 committed origin 标记」的 SQL 布尔式(两种承载形同判,与 {@link exposureCarriage} 同源)。
150
+ * `column` = 该表 frontmatter 列的表达式(调用方自己保证它是列名/限定名,**不接受外来串**)。
151
+ */
152
+ export function originMarkedSqlPredicate(column) {
153
+ return (`(${column}->'origin' IS NOT NULL OR (jsonb_typeof(${column}->'extra') = 'array' AND EXISTS (` +
154
+ `SELECT 1 FROM jsonb_array_elements_text(${column}->'extra') AS origin_line(line) ` +
155
+ `WHERE origin_line.line ~ '${ORIGIN_CARRIER_LINE_SQL_REGEX}')))`);
156
+ }
40
157
  /** 新建一只批次基线(每次 `applyPatches` 一只;跨批次**不得**复用 —— 已提交的墓碑正是靠「换批清零」放行)。 */
41
158
  export function createBatchOriginBaseline() {
42
159
  return new Map();
@@ -12,12 +12,17 @@
12
12
  * and branched THERE. Only the table binding lives here.
13
13
  */
14
14
  import type { Pool, PoolClient } from "pg";
15
- import type { QuotaDecision, QuotaTracker } from "../observability/cost-quota.js";
15
+ import type { QuotaDecision, QuotaLimits, QuotaTracker } from "../observability/cost-quota.js";
16
16
  import { type CounterDegradeHook } from "./write-behind-counter.js";
17
17
  export declare class PgCostQuota implements QuotaTracker {
18
- private readonly limitMicroUsd;
19
18
  private readonly counter;
19
+ /** #322:与 TiDB 孪生逐字同语义(限额=比较参数;关断哨兵零 SQL;窗长换代=换桶)。 */
20
+ private limitMicroUsd;
21
+ private refreshRequested;
22
+ private refreshIntervalMs;
20
23
  constructor(pool: Pool, limitMicroUsd: number, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook, queryTimeoutMs?: number);
24
+ setLimits(next: QuotaLimits): void;
25
+ private syncRefresh;
21
26
  check(scopeKey: string): QuotaDecision;
22
27
  add(scopeKey: string, micro: number): void;
23
28
  flush(): Promise<void>;