@sema-agent/server 7.5.0 → 7.7.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/LICENSE +1 -1
- package/README.md +18 -3
- package/README.zh-CN.md +14 -3
- package/USAGE.md +100 -2
- package/dist/approval-reconciler.d.ts +1 -1
- package/dist/approval-reconciler.js +6 -5
- package/dist/boot/budget-tracing.js +1 -1
- package/dist/boot/config-center.js +17 -3
- package/dist/boot/execution-env.js +1 -1
- package/dist/boot/parked-revive-gate.d.ts +78 -0
- package/dist/boot/parked-revive-gate.js +114 -0
- package/dist/boot/resolve-spec.d.ts +8 -20
- package/dist/boot/resolve-spec.js +40 -125
- package/dist/budget.d.ts +1 -1
- package/dist/budget.js +1 -1
- package/dist/capabilities/hands-lane.d.ts +91 -0
- package/dist/capabilities/hands-lane.js +104 -0
- package/dist/capabilities/repo-tools.d.ts +1 -1
- package/dist/capabilities/repo-tools.js +8 -2
- package/dist/capabilities/scenarios.d.ts +49 -2
- package/dist/capabilities/scenarios.js +84 -16
- package/dist/config-provider.d.ts +1 -0
- package/dist/config-provider.js +23 -3
- package/dist/config-types.d.ts +5 -3
- package/dist/config.js +5 -4
- package/dist/deployment-governance.d.ts +168 -0
- package/dist/deployment-governance.js +206 -0
- package/dist/fleet/fleet-bus.d.ts +11 -1
- package/dist/fleet/fleet-bus.js +43 -0
- package/dist/fleet/fleet-terminal-window.d.ts +98 -0
- package/dist/fleet/fleet-terminal-window.js +319 -0
- package/dist/hooks/hook-runner.js +11 -0
- package/dist/http/route-ctx.d.ts +7 -1
- package/dist/http/routes/approvals-assistant.js +6 -7
- package/dist/http/routes/fleet.js +164 -14
- package/dist/http/routes/runs.js +7 -2
- package/dist/http/routes/tasks.js +5 -4
- package/dist/http/routes/trace-usage.js +3 -4
- package/dist/http/send.d.ts +23 -0
- package/dist/http/send.js +23 -0
- package/dist/http/server.d.ts +10 -0
- package/dist/http/server.js +28 -12
- package/dist/http/sse-log.js +3 -4
- package/dist/leader/diffout.d.ts +10 -0
- package/dist/leader/diffout.js +14 -2
- package/dist/leader/diffup.js +3 -2
- package/dist/leader/planner.js +7 -0
- package/dist/main.js +79 -32
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/observability/tool-trace.d.ts +13 -0
- package/dist/observability/tool-trace.js +14 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
- package/dist/orchestration/workflow-notify-journal.js +14 -34
- package/dist/plugins/approval-ask-store-sql.d.ts +33 -0
- package/dist/plugins/approval-ask-store-sql.js +66 -33
- package/dist/plugins/background-agent-store-sql.js +16 -16
- package/dist/plugins/breaker-state-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +5 -2
- package/dist/plugins/checkpoint-store-sql.js +5 -2
- package/dist/plugins/image-bake-store-sql.d.ts +1 -1
- package/dist/plugins/image-bake-store-sql.js +27 -27
- package/dist/plugins/image-index-sql.js +15 -15
- package/dist/plugins/mailbox-store-sql.js +3 -3
- package/dist/plugins/memory-engine-pg.js +9 -9
- package/dist/plugins/memory-engine-tidb.js +7 -7
- package/dist/plugins/memory-sync-store-pg.js +13 -13
- package/dist/plugins/memory-sync-store-tidb.js +5 -5
- package/dist/plugins/outcome-ledger-sql.js +7 -7
- package/dist/plugins/pg-cost-quota.js +3 -3
- package/dist/plugins/pg-pool.js +84 -75
- package/dist/plugins/pg-rate-limiter.js +3 -3
- package/dist/plugins/pg-session-storage.d.ts +1 -1
- package/dist/plugins/pg-session-storage.js +12 -13
- package/dist/plugins/remote-env-host.js +3 -1
- package/dist/plugins/remote-env-local-docker.js +6 -3
- package/dist/plugins/remote-env-ssh.d.ts +13 -1
- package/dist/plugins/roster-store-sql.js +8 -8
- package/dist/plugins/store-contracts.d.ts +19 -0
- package/dist/plugins/store-contracts.js +42 -0
- package/dist/plugins/task-attachment-store.js +5 -5
- package/dist/plugins/task-list-store-sql.js +1 -1
- package/dist/plugins/tidb-cost-quota.js +1 -1
- package/dist/plugins/tidb-pool.js +83 -60
- package/dist/plugins/tidb-rate-limiter.js +1 -1
- package/dist/plugins/tidb-session-store.js +2 -5
- package/dist/plugins/tool-result-store-sql.js +2 -2
- package/dist/plugins/usage-window-store-sql.js +13 -13
- package/dist/plugins/write-behind-counter.d.ts +10 -2
- package/dist/plugins/write-behind-counter.js +13 -3
- package/dist/resource-suspend.d.ts +3 -1
- package/dist/resource-suspend.js +3 -1
- package/dist/run-local.d.ts +73 -1
- package/dist/run-local.js +160 -9
- package/dist/runs.d.ts +3 -1
- package/dist/runs.js +3 -1
- package/dist/security.d.ts +12 -0
- package/dist/security.js +12 -0
- package/dist/session-sync-kernel.d.ts +13 -0
- package/dist/session-sync-kernel.js +13 -0
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/project.d.ts +10 -1
- package/dist/trace/project.js +31 -0
- package/package.json +3 -3
|
@@ -74,8 +74,8 @@ export const SCHEMA_STATEMENTS = [
|
|
|
74
74
|
-- setTitleIfNull — the user-facing RENAME layer lives BFF-side as a label overlay, not here).
|
|
75
75
|
title VARCHAR(120) NULL,
|
|
76
76
|
PRIMARY KEY (session_id),
|
|
77
|
-
KEY
|
|
78
|
-
)`,
|
|
77
|
+
KEY idx_session_meta_owner (owner)
|
|
78
|
+
) COLLATE utf8mb4_bin`,
|
|
79
79
|
`CREATE TABLE IF NOT EXISTS session_event (
|
|
80
80
|
session_id VARCHAR(64) NOT NULL,
|
|
81
81
|
seq BIGINT NOT NULL,
|
|
@@ -85,11 +85,25 @@ export const SCHEMA_STATEMENTS = [
|
|
|
85
85
|
payload JSON NOT NULL,
|
|
86
86
|
ts DATETIME(3) NOT NULL,
|
|
87
87
|
PRIMARY KEY (session_id, seq),
|
|
88
|
-
UNIQUE KEY
|
|
89
|
-
)`,
|
|
88
|
+
UNIQUE KEY uq_session_event_entry (session_id, entry_id)
|
|
89
|
+
) COLLATE utf8mb4_bin`,
|
|
90
90
|
// Async run registry (S1) + replayable event log (S2). Any instance can serve a run's status /
|
|
91
91
|
// event replay because both live here, not in a single instance's memory.
|
|
92
92
|
`CREATE TABLE IF NOT EXISTS task_run (
|
|
93
|
+
-- ⚠️ 表级 COLLATE 的**非键列**影响(#192 A11,codex 复审 2026-08-08 round2 点名——先前"只有
|
|
94
|
+
-- outcome_ledger 有聚合语义变化"的说法过窄,如实更正):
|
|
95
|
+
-- · \`status\`:sourceSummary() 的 GROUP BY 键之一,是**闭集词表**(running/failed/…)由服务端铸字面量
|
|
96
|
+
-- 写入 ⇒ 字节分桶恒等于词表分桶,此变更对它无实际影响。
|
|
97
|
+
-- · \`source\`:同为 GROUP BY 键、且是 \`?source=\` 过滤列。🔴 **它不是闭集**(codex 复审 round3 纠正了
|
|
98
|
+
-- 本注的前一版:那版写它限于 oa/cc-mcp/portal,是错的)——真实来源是运维在 \`SERVICE_AUTH_TOKENS\`
|
|
99
|
+
-- 里写的 \`<token>=<system>\`,\`system\` 只过"非空"这一道(config.ts),大小写与重音全由运维自己定。
|
|
100
|
+
-- ⇒ 若某部署此前跑在 *_ci/_ai 库上、且配了只差大小写/重音的两个 system 名,升级后 sourceSummary 的
|
|
101
|
+
-- 分桶会**分裂**、\`?source=\` 也从宽松匹配变成字节精确。零存量用户下无人受影响;要恢复"宽松"语义
|
|
102
|
+
-- 应当去入口把 system 名规范化,而不是把库退回 *_ci(那会连隔离键一起放宽)。
|
|
103
|
+
-- · \`objective_preview\`:\`?q=\` 搜索谓词的被搜列。搜索侧用的是 \`LOWER(col) LIKE LOWER(?)\`
|
|
104
|
+
-- (run-store-sql.ts / pg-session-storage.ts 双方言同形,注里写明"collation-proof ci")⇒ 大小写面
|
|
105
|
+
-- 本就不依赖排序规则,不受影响。**残留**:若某部署此前跑在 *_ai(accent-insensitive)库上,重音
|
|
106
|
+
-- 折叠那一档会随之消失(LOWER() 不去重音)——零存量用户下无人受影响,但它是行为差,记在这里。
|
|
93
107
|
task_id VARCHAR(64) NOT NULL,
|
|
94
108
|
session_id VARCHAR(64) NOT NULL,
|
|
95
109
|
owner VARCHAR(190) NULL,
|
|
@@ -124,19 +138,19 @@ export const SCHEMA_STATEMENTS = [
|
|
|
124
138
|
created_at DATETIME(3) NOT NULL,
|
|
125
139
|
updated_at DATETIME(3) NOT NULL,
|
|
126
140
|
PRIMARY KEY (task_id),
|
|
127
|
-
KEY
|
|
128
|
-
KEY
|
|
141
|
+
KEY idx_task_run_session_status (session_id, status),
|
|
142
|
+
KEY idx_task_run_owner (owner),
|
|
129
143
|
-- idx_job: WHERE job_id = ? (work-view grouping) would full-scan without it.
|
|
130
|
-
KEY
|
|
144
|
+
KEY idx_task_run_job (job_id),
|
|
131
145
|
-- idx_created: listRuns ALWAYS ORDER BY created_at DESC — without this the trace list does a filesort on
|
|
132
146
|
-- every query (deepseek council #2). Covers the unfiltered newest-first list; filtered lists narrow via
|
|
133
147
|
-- idx_owner / idx_job first.
|
|
134
|
-
KEY
|
|
148
|
+
KEY idx_task_run_created (created_at)
|
|
135
149
|
-- ⛔ DELIBERATELY NO KEY idx_error_code (error_code) (dropped 2026-07-26, clay 裁): a whole-repo sweep found
|
|
136
150
|
-- ZERO code paths that filter on error_code — every reference is a SET or a projection — so the index only
|
|
137
151
|
-- ever served an operator's ad-hoc WHERE error_code LIKE 'budget.%', while every task_run write paid its
|
|
138
152
|
-- maintenance cost. If you need that ad-hoc query, build the index temporarily and drop it after.
|
|
139
|
-
)`,
|
|
153
|
+
) COLLATE utf8mb4_bin`,
|
|
140
154
|
// Cross-replica circuit-breaker state (1.38). A row per breaker key ("<provider>:<id>"); each
|
|
141
155
|
// replica write-through-caches its trips here and periodically refreshes so a provider outage is
|
|
142
156
|
// learned fleet-wide instead of every replica re-discovering it. Advisory / eventually consistent
|
|
@@ -148,28 +162,28 @@ export const SCHEMA_STATEMENTS = [
|
|
|
148
162
|
opened_at DATETIME(3) NULL,
|
|
149
163
|
updated_at DATETIME(3) NOT NULL,
|
|
150
164
|
PRIMARY KEY (breaker_key)
|
|
151
|
-
)`,
|
|
165
|
+
) COLLATE utf8mb4_bin`,
|
|
152
166
|
// Cross-replica per-key CUMULATIVE cost quota (design/30 P0-1). One row per (scope_key, aligned window
|
|
153
167
|
// bucket); replicas atomically `micro = micro + delta` (write-behind aggregation), so spend is summed
|
|
154
168
|
// fleet-wide without losing increments. scope_key is opaque (principal today; team/feature later).
|
|
155
169
|
`CREATE TABLE IF NOT EXISTS cost_quota (
|
|
156
170
|
scope_key VARCHAR(190) NOT NULL,
|
|
157
171
|
window_bucket BIGINT NOT NULL,
|
|
158
|
-
|
|
172
|
+
micro_usd BIGINT NOT NULL DEFAULT 0,
|
|
159
173
|
updated_at DATETIME(3) NOT NULL,
|
|
160
174
|
PRIMARY KEY (scope_key, window_bucket),
|
|
161
|
-
KEY
|
|
162
|
-
)`,
|
|
175
|
+
KEY idx_cost_quota_window (window_bucket)
|
|
176
|
+
) COLLATE utf8mb4_bin`,
|
|
163
177
|
// Cross-replica per-key request rate limit (design/30 P0-2) — same write-behind-counter shape as
|
|
164
178
|
// cost_quota (one row per key+aligned-window, atomic n = n + delta). Soft fleet-wide limiting.
|
|
165
179
|
`CREATE TABLE IF NOT EXISTS rate_limit (
|
|
166
180
|
limit_key VARCHAR(190) NOT NULL,
|
|
167
181
|
window_bucket BIGINT NOT NULL,
|
|
168
|
-
|
|
182
|
+
hits INT NOT NULL DEFAULT 0,
|
|
169
183
|
updated_at DATETIME(3) NOT NULL,
|
|
170
184
|
PRIMARY KEY (limit_key, window_bucket),
|
|
171
|
-
KEY
|
|
172
|
-
)`,
|
|
185
|
+
KEY idx_rate_limit_window (window_bucket)
|
|
186
|
+
) COLLATE utf8mb4_bin`,
|
|
173
187
|
// Durable backing for offloaded large tool results (core 1.47/1.49). One row per offload ref
|
|
174
188
|
// (`tr_<sessionId>_<toolCallId>`, globally unique → PK); content is the full tool output moved out of
|
|
175
189
|
// context. Write-once (INSERT IGNORE); TTL-reaped by created_at (idx_created). LONGTEXT holds large
|
|
@@ -179,8 +193,8 @@ export const SCHEMA_STATEMENTS = [
|
|
|
179
193
|
content LONGTEXT NOT NULL,
|
|
180
194
|
created_at DATETIME(3) NOT NULL,
|
|
181
195
|
PRIMARY KEY (ref),
|
|
182
|
-
KEY
|
|
183
|
-
)`,
|
|
196
|
+
KEY idx_tool_result_created (created_at)
|
|
197
|
+
) COLLATE utf8mb4_bin`,
|
|
184
198
|
`CREATE TABLE IF NOT EXISTS task_event (
|
|
185
199
|
task_id VARCHAR(64) NOT NULL,
|
|
186
200
|
seq BIGINT NOT NULL,
|
|
@@ -188,7 +202,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
188
202
|
data JSON NULL,
|
|
189
203
|
ts DATETIME(3) NOT NULL,
|
|
190
204
|
PRIMARY KEY (task_id, seq)
|
|
191
|
-
)`,
|
|
205
|
+
) COLLATE utf8mb4_bin`,
|
|
192
206
|
// Single-active-run-per-session claim. PRIMARY KEY(session_id) makes the claim a unique-key
|
|
193
207
|
// insert: TiDB enforces it even with no prior row (it lacks InnoDB gap locks, so a
|
|
194
208
|
// SELECT…FOR UPDATE on an empty match would NOT serialize two concurrent zero-prior createRuns).
|
|
@@ -197,8 +211,8 @@ export const SCHEMA_STATEMENTS = [
|
|
|
197
211
|
task_id VARCHAR(64) NOT NULL,
|
|
198
212
|
since DATETIME(3) NOT NULL,
|
|
199
213
|
PRIMARY KEY (session_id),
|
|
200
|
-
KEY
|
|
201
|
-
)`,
|
|
214
|
+
KEY idx_task_active_task (task_id)
|
|
215
|
+
) COLLATE utf8mb4_bin`,
|
|
202
216
|
// ([2354]/[2356] A-5,5.0.0:F4 轮询车道的 `approval` 表 DDL 已随车道退役——存量库里的旧表留置不动,
|
|
203
217
|
// 代码零读写;durable 门只用下面的 checkpoint 表。)
|
|
204
218
|
// Durable-checkpoint (core 1.67 / design/45): a policy `ask` persisted as a suspended task so an operator
|
|
@@ -263,8 +277,11 @@ export const SCHEMA_STATEMENTS = [
|
|
|
263
277
|
-- pending_steer_rev:pending_steer_queue 列**自己**的乐观锁计数(与 resolve/reopen 的 rev 是两个轴,
|
|
264
278
|
-- 刻意不复用)。队列化之后 setPendingSteer 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须
|
|
265
279
|
-- 有 CAS 守着,否则两个并发 steer 各读到同一份队列、后写者把前者的条目整个盖掉。
|
|
266
|
-
-- 🔴
|
|
267
|
-
--
|
|
280
|
+
-- 🔴 为什么不拿队列列的旧值当守卫:整数计数是**按构造**唯一的,长 JSON 文本的相等性则取决于列的排序
|
|
281
|
+
-- 规则、字段序、空白与转义形 —— 任何一处判等松一点,CAS 就假命中一次 = 悄悄丢一条 steer 指令。
|
|
282
|
+
-- (#192 A11 更正:本注原先的理由写的是"本表没有显式 COLLATE,库默认多为 *_ci",那半**已不成立** ——
|
|
283
|
+
-- 本表现在钉了 COLLATE utf8mb4_bin,大小写敏感面已由 schema 保证。但结论不变,只是理由回到更根本的
|
|
284
|
+
-- 那条:文本相等性有太多与业务无关的自由度,整数没有。)
|
|
268
285
|
-- **绝不**用它做 resolve 的 OCC:bump 它不影响 core 的 rev 语义。
|
|
269
286
|
pending_steer_rev BIGINT NOT NULL DEFAULT 0,
|
|
270
287
|
-- risk_descriptor (design/80 riskDescriptor inbox): core's INERT CheckpointGate.riskDescriptor ({severity
|
|
@@ -273,9 +290,9 @@ export const SCHEMA_STATEMENTS = [
|
|
|
273
290
|
-- NULL on gates with no descriptor.
|
|
274
291
|
risk_descriptor TEXT NULL,
|
|
275
292
|
PRIMARY KEY (token),
|
|
276
|
-
KEY
|
|
277
|
-
KEY
|
|
278
|
-
)`,
|
|
293
|
+
KEY idx_checkpoint_status_deadline (status, deadline),
|
|
294
|
+
KEY idx_checkpoint_session_status (session_id, status)
|
|
295
|
+
) COLLATE utf8mb4_bin`,
|
|
279
296
|
// Service-side resume rebuild inputs (DURABLE-CHECKPOINT-PLAN §4.4a): sessionId-keyed because core's
|
|
280
297
|
// put(token,cp) can't carry service scenario_ctx. GC'd by the reaper once its checkpoint is gone.
|
|
281
298
|
`CREATE TABLE IF NOT EXISTS checkpoint_ctx (
|
|
@@ -283,7 +300,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
283
300
|
ctx JSON NOT NULL,
|
|
284
301
|
updated_at BIGINT NOT NULL,
|
|
285
302
|
PRIMARY KEY (session_id)
|
|
286
|
-
)`,
|
|
303
|
+
) COLLATE utf8mb4_bin`,
|
|
287
304
|
// E18 resume-at anchor map: the shell holds live E2 eventIds (never persisted by core); TaskSpec.resumeAt takes a
|
|
288
305
|
// persisted SessionTreeEntry.id. This is the service-owned eventId→entryId translation. Keyed (session_id,event_id)
|
|
289
306
|
// so resolve is a PK point-lookup; entry_id is the resolve target. owner = single-DB-fleet defense-in-depth (the
|
|
@@ -296,7 +313,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
296
313
|
created_at DATETIME(3) NOT NULL,
|
|
297
314
|
PRIMARY KEY (session_id, event_id),
|
|
298
315
|
KEY idx_resume_anchor_session (session_id)
|
|
299
|
-
)`,
|
|
316
|
+
) COLLATE utf8mb4_bin`,
|
|
300
317
|
// Per-session per-toolName approval exemption ("本会话不再询问") — an APPROVAL MEMORY the
|
|
301
318
|
// F4 ask-gate consults AFTER deny/neverAuto (both always win). tool_name is CANONICAL space. GC'd with
|
|
302
319
|
// the session (E21 delete). See sema-internal server/docs/design/APPROVAL-SESSION-EXEMPTION.md.
|
|
@@ -306,7 +323,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
306
323
|
granted_by VARCHAR(190) NULL,
|
|
307
324
|
created_at DATETIME(3) NOT NULL,
|
|
308
325
|
PRIMARY KEY (session_id, tool_name)
|
|
309
|
-
)`,
|
|
326
|
+
) COLLATE utf8mb4_bin`,
|
|
310
327
|
// SendUserFile server-side ledger (multi-tenant governance, 1.190.0 [768]②a second half): one row per issued
|
|
311
328
|
// link — the scope↔object mapping the HASHED key segment deliberately hides from URLs (per-tenant list/revoke
|
|
312
329
|
// handle). id = uuidv7 (time-ordered PK = the keyset cursor); scope = VERIFIED principal verbatim ("_" =
|
|
@@ -327,7 +344,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
327
344
|
created_at DATETIME(3) NOT NULL,
|
|
328
345
|
PRIMARY KEY (id),
|
|
329
346
|
KEY idx_sendfile_link_scope (scope, id)
|
|
330
|
-
)`,
|
|
347
|
+
) COLLATE utf8mb4_bin`,
|
|
331
348
|
// E6 durable SessionPolicyStore — operator-tightened per-(session,principal) tool-permission rules (CAS-versioned
|
|
332
349
|
// tighten-only; core reads them at prepare-time, subtract-only). policy_key = sha256(JSON.stringify([sessionId,
|
|
333
350
|
// principal ?? null])) — the SHA of core's exact composite key: a FIXED 64-char PK (an arbitrary-length / JSON-
|
|
@@ -352,7 +369,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
352
369
|
updated_at DATETIME(3) NOT NULL,
|
|
353
370
|
PRIMARY KEY (policy_key),
|
|
354
371
|
KEY idx_session_policy_session (session_id)
|
|
355
|
-
)`,
|
|
372
|
+
) COLLATE utf8mb4_bin`,
|
|
356
373
|
// E19 rewind-files — durable working-tree snapshots keyed by (scope=sessionId, snap_key=SessionTreeEntry.id; same
|
|
357
374
|
// entryId anchor as E18). A per-(scope,key) manifest of relPath→blob_hash + content-addressed blobs (deduped across
|
|
358
375
|
// all snapshots). core's captureManifest/applyManifest do the env walk/restore; this is just the durable persistence.
|
|
@@ -364,7 +381,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
364
381
|
blob_hash CHAR(64) NOT NULL,
|
|
365
382
|
KEY idx_snapshot_manifest_sk (scope, snap_key),
|
|
366
383
|
KEY idx_snapshot_manifest_blob (blob_hash)
|
|
367
|
-
)`,
|
|
384
|
+
) COLLATE utf8mb4_bin`,
|
|
368
385
|
// `bytes` is NULLABLE: a SQL-backend row carries the bytes inline; a MinIO-backend row keeps ONLY the INDEX
|
|
369
386
|
// (blob_hash, byte_len, created_at) here with bytes NULL — the bytes live in MinIO. This INDEX row is what
|
|
370
387
|
// gcOrphanBlobs / reap / deleteBySession (E21 purge) / sweepOrphanBlobs read to compute the orphan/grace SET, so
|
|
@@ -384,7 +401,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
384
401
|
created_at DATETIME(3) NOT NULL,
|
|
385
402
|
PRIMARY KEY (blob_hash),
|
|
386
403
|
KEY idx_snapshot_blob_created (created_at)
|
|
387
|
-
)`,
|
|
404
|
+
) COLLATE utf8mb4_bin`,
|
|
388
405
|
// SVC-2 (core CORE-7) durable WorkflowJournalStore — the cross-replica resume journal for an LLM-authored workflow.
|
|
389
406
|
// One row per (run_id, ordinal): `ordinal` = core's callKeyOrdinal(callKey) (the positional replay index), `result`
|
|
390
407
|
// = JSON.stringify(TaskResult). The (run_id, ordinal) PK makes append IDEMPOTENT (ON DUPLICATE KEY = OVERWRITE /
|
|
@@ -405,7 +422,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
405
422
|
result MEDIUMTEXT NOT NULL,
|
|
406
423
|
created_at BIGINT NOT NULL,
|
|
407
424
|
PRIMARY KEY (run_id, ordinal)
|
|
408
|
-
)`,
|
|
425
|
+
) COLLATE utf8mb4_bin`,
|
|
409
426
|
// RB-242 / WF2 ([1981] core 拍板 a 形):cross-replica workflow resume admission lease. PK (source_run_id, scope)
|
|
410
427
|
// = the bake-store idem_key precedent — the INSERT either wins the claim or collides with the current holder.
|
|
411
428
|
// TTL is the CRASH backstop only (engine releases on terminal via releaseResumeClaim); see workflow-journal-store-sql.ts.
|
|
@@ -415,7 +432,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
415
432
|
new_run_id VARCHAR(191) NOT NULL,
|
|
416
433
|
claimed_at BIGINT NOT NULL,
|
|
417
434
|
PRIMARY KEY (source_run_id, scope)
|
|
418
|
-
)`,
|
|
435
|
+
) COLLATE utf8mb4_bin`,
|
|
419
436
|
// P1 (fleet failover, 2026-07-05): durable cross-replica WorkflowRunStore twin — one row per run, the full
|
|
420
437
|
// WorkflowRun as a JSON blob + extracted index columns; `rev` column = the OCC authority (blob rev overlaid
|
|
421
438
|
// on read). MEDIUMTEXT (16MB) >> the 4MB write guard in the store.
|
|
@@ -430,7 +447,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
430
447
|
PRIMARY KEY (id),
|
|
431
448
|
KEY idx_wfrun_scope_created (scope, created_at),
|
|
432
449
|
KEY idx_wfrun_scope_status (scope, status)
|
|
433
|
-
)`,
|
|
450
|
+
) COLLATE utf8mb4_bin`,
|
|
434
451
|
// P1: cross-replica workflow completion inbox (the push half). seq = insertion order (re-enqueue
|
|
435
452
|
// moves to tail via DELETE+INSERT). summary is bounded upstream (core notifier ≤4000).
|
|
436
453
|
`CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
|
|
@@ -453,7 +470,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
453
470
|
PRIMARY KEY (session_id, run_id),
|
|
454
471
|
UNIQUE KEY uq_wfinbox_seq (seq),
|
|
455
472
|
KEY idx_wfinbox_session_seq (session_id, seq)
|
|
456
|
-
)`,
|
|
473
|
+
) COLLATE utf8mb4_bin`,
|
|
457
474
|
// P1: inbox fences AS ROWS — the actual point of the SQL port: a poll served on replica A must fence the
|
|
458
475
|
// late notify landing on replica B (cross-replica). kind = served (per-runId) | purge (run_id='',
|
|
459
476
|
// session-wide; owner_known=0 → conservative fence-all). Swept opportunistically on every write verb.
|
|
@@ -466,7 +483,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
466
483
|
until_ms BIGINT NOT NULL,
|
|
467
484
|
PRIMARY KEY (session_id, kind, run_id),
|
|
468
485
|
KEY idx_wffence_until (until_ms)
|
|
469
|
-
)`,
|
|
486
|
+
) COLLATE utf8mb4_bin`,
|
|
470
487
|
// 1.108 review fix (lens③ HIGH): the notify JOURNAL twin — under WORKFLOW_RUN_STORE=auto the run store +
|
|
471
488
|
// inbox were SQL but the notify journal stayed a replica-LOCAL File ledger, so a replica that died with an
|
|
472
489
|
// un-acked notify stranded it forever (at-least-once silently degraded to at-most-once across replica death).
|
|
@@ -481,7 +498,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
481
498
|
acked_at BIGINT NULL,
|
|
482
499
|
PRIMARY KEY (run_id),
|
|
483
500
|
KEY idx_wfnotify_acked (acked)
|
|
484
|
-
)`,
|
|
501
|
+
) COLLATE utf8mb4_bin`,
|
|
485
502
|
// Outcome ledger (design/73 §7) — one row per graded task run, keyed by
|
|
486
503
|
// (task_signature, model). v1 is READ-ONLY (dashboard + offline-backtest source); it drives NO
|
|
487
504
|
// automatic policy change — that is flag-gated and must first pass an offline-backtest gate
|
|
@@ -491,6 +508,12 @@ export const SCHEMA_STATEMENTS = [
|
|
|
491
508
|
// `outcome` is DERIVED from mechanical fields only (council blocker 1).
|
|
492
509
|
`CREATE TABLE IF NOT EXISTS outcome_ledger (
|
|
493
510
|
id VARCHAR(64) NOT NULL,
|
|
511
|
+
-- ⚠️ 表级 COLLATE 对本表**不只是钉住隔离键**(#192 A11,codex 复审 2026-08-08 点名):下面两列是
|
|
512
|
+
-- \`summary()\` 的 GROUP BY / ORDER BY 键(outcome-ledger-sql.ts),所以排序规则直接决定**分桶**。
|
|
513
|
+
-- utf8mb4_bin ⇒ 大小写/重音不同的 model id 各成一桶。这是**有意的**:model id 与 task signature 是
|
|
514
|
+
-- 机器铸的标识,不是自然语言;把 "GPT-x" 与 "gpt-x" 合成一桶是在替调用方猜,猜错了就是把两次不同
|
|
515
|
+
-- 实验的分数搅在一起。代价如实记:若某个部署此前跑在 *_ci 库上,升级后历史桶会**分裂**、顺序也会变
|
|
516
|
+
-- (字节序而非语言序)——零存量用户下无人受影响,但它是行为变化,不是纯粹的钉。
|
|
494
517
|
task_signature VARCHAR(190) NOT NULL,
|
|
495
518
|
model VARCHAR(190) NOT NULL,
|
|
496
519
|
task_id VARCHAR(64) NULL,
|
|
@@ -513,10 +536,10 @@ export const SCHEMA_STATEMENTS = [
|
|
|
513
536
|
core_outcome JSON NULL,
|
|
514
537
|
created_at DATETIME(3) NOT NULL,
|
|
515
538
|
PRIMARY KEY (id),
|
|
516
|
-
KEY
|
|
517
|
-
KEY
|
|
518
|
-
KEY
|
|
519
|
-
)`,
|
|
539
|
+
KEY idx_outcome_ledger_sig_model (task_signature, model, run_status),
|
|
540
|
+
KEY idx_outcome_ledger_outcome (outcome),
|
|
541
|
+
KEY idx_outcome_ledger_created (created_at)
|
|
542
|
+
) COLLATE utf8mb4_bin`,
|
|
520
543
|
// sandbox-image-pool control API (IMAGE-API-DESIGN.md §1 / pool design §8). One row per image DIGEST (the
|
|
521
544
|
// immutable runtime ref); a queryable projection of build.sh's replay-sufficient manifest + the publish lifecycle.
|
|
522
545
|
`CREATE TABLE IF NOT EXISTS sandbox_image_index (
|
|
@@ -543,10 +566,10 @@ export const SCHEMA_STATEMENTS = [
|
|
|
543
566
|
updated_at DATETIME(3) NOT NULL,
|
|
544
567
|
PRIMARY KEY (id),
|
|
545
568
|
UNIQUE KEY uq_repo_digest (repo, digest),
|
|
546
|
-
KEY
|
|
547
|
-
KEY
|
|
548
|
-
KEY
|
|
549
|
-
)`,
|
|
569
|
+
KEY idx_sandbox_image_index_profile_status (profile, status, visibility),
|
|
570
|
+
KEY idx_sandbox_image_index_digest (digest),
|
|
571
|
+
KEY idx_sandbox_image_index_tenant (tenant_id, status)
|
|
572
|
+
) COLLATE utf8mb4_bin`,
|
|
550
573
|
// sandbox-image-pool BAKE job (IMAGE-API-DESIGN.md §P2.6). A dedicated row table — NOT a reuse of task_run
|
|
551
574
|
// (FK-coupled to task_active + the run reaper + /v1/runs list; co-mingling bakes would leak non-task rows into
|
|
552
575
|
// reapStale/listRuns). The reader polls the COARSE 4-value `status` (queued|running|done|failed) for SSE
|
|
@@ -558,9 +581,9 @@ export const SCHEMA_STATEMENTS = [
|
|
|
558
581
|
profile VARCHAR(128) NOT NULL,
|
|
559
582
|
bands JSON NULL,
|
|
560
583
|
base_ref VARCHAR(255) NULL,
|
|
561
|
-
push TINYINT NOT NULL DEFAULT 0,
|
|
562
|
-
dry_run TINYINT NOT NULL DEFAULT 0,
|
|
563
|
-
logs TINYINT NOT NULL DEFAULT 0,
|
|
584
|
+
push TINYINT(1) NOT NULL DEFAULT 0,
|
|
585
|
+
dry_run TINYINT(1) NOT NULL DEFAULT 0,
|
|
586
|
+
logs TINYINT(1) NOT NULL DEFAULT 0,
|
|
564
587
|
argv JSON NOT NULL,
|
|
565
588
|
status VARCHAR(16) NOT NULL,
|
|
566
589
|
state VARCHAR(16) NULL,
|
|
@@ -577,19 +600,19 @@ export const SCHEMA_STATEMENTS = [
|
|
|
577
600
|
ingest_secret VARCHAR(64) NULL,
|
|
578
601
|
runner_id VARCHAR(64) NULL,
|
|
579
602
|
lease_until DATETIME(3) NULL,
|
|
580
|
-
cancel_requested TINYINT NOT NULL DEFAULT 0,
|
|
603
|
+
cancel_requested TINYINT(1) NOT NULL DEFAULT 0,
|
|
581
604
|
requested_by VARCHAR(128) NULL,
|
|
582
605
|
created_at DATETIME(3) NOT NULL,
|
|
583
606
|
updated_at DATETIME(3) NOT NULL,
|
|
584
607
|
PRIMARY KEY (bake_id),
|
|
585
|
-
UNIQUE KEY
|
|
586
|
-
KEY
|
|
587
|
-
KEY
|
|
588
|
-
)`,
|
|
608
|
+
UNIQUE KEY uq_image_bake_idem (idem_key),
|
|
609
|
+
KEY idx_image_bake_status (status),
|
|
610
|
+
KEY idx_image_bake_created (created_at)
|
|
611
|
+
) COLLATE utf8mb4_bin`,
|
|
589
612
|
// Per-bake replayable event log (P2.6) — run-trace event-log mechanics ((id,seq) PK, ++seq, getEvents(after),
|
|
590
613
|
// maxSeq, retainedFrom→416, heartbeat-not-persisted) WITHOUT the task_event FK coupling. image-api is the SOLE
|
|
591
614
|
// ++seq writer (seq monotonic from 1). `line_ord` is the runner's per-line ordinal — the at-least-once ingest
|
|
592
|
-
// dedupe key (
|
|
615
|
+
// dedupe key (uq_image_bake_event_line): a re-sent (bakeId,line_ord) is a no-op (P2.10). `level='raw'` rows are the opt-in
|
|
593
616
|
// build.log tail (?logs=1), pruned on a tighter TTL + evicted first (P2.5/P2.9).
|
|
594
617
|
`CREATE TABLE IF NOT EXISTS image_bake_event (
|
|
595
618
|
bake_id VARCHAR(64) NOT NULL,
|
|
@@ -600,8 +623,8 @@ export const SCHEMA_STATEMENTS = [
|
|
|
600
623
|
data JSON NULL,
|
|
601
624
|
ts DATETIME(3) NOT NULL,
|
|
602
625
|
PRIMARY KEY (bake_id, seq),
|
|
603
|
-
UNIQUE KEY
|
|
604
|
-
)`,
|
|
626
|
+
UNIQUE KEY uq_image_bake_event_line (bake_id, line_ord)
|
|
627
|
+
) COLLATE utf8mb4_bin`,
|
|
605
628
|
// Single-flight build lease (P2.7, §9). ONE row per builder pool — image-api owns the authoritative lease so the
|
|
606
629
|
// one docker daemon never has two concurrent builds (a prune-behind-lock could evict layers an in-flight build
|
|
607
630
|
// needs). `expires_at` makes a crashed-runner lease stealable (CAS on expiry).
|
|
@@ -611,7 +634,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
611
634
|
acquired_at DATETIME(3) NOT NULL,
|
|
612
635
|
expires_at DATETIME(3) NOT NULL,
|
|
613
636
|
PRIMARY KEY (pool)
|
|
614
|
-
)`,
|
|
637
|
+
) COLLATE utf8mb4_bin`,
|
|
615
638
|
// Admission sentinel: ONE always-present row per builder pool, `SELECT … FOR UPDATE`-locked to SERIALIZE the
|
|
616
639
|
// createBakeIfIdle busy-check+insert. A bare conditional INSERT…SELECT WHERE NOT EXISTS does NOT serialize two
|
|
617
640
|
// concurrent admissions under TiDB's REPEATABLE READ (nor PG's READ COMMITTED), so without this lock two keyless
|
|
@@ -619,7 +642,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
619
642
|
`CREATE TABLE IF NOT EXISTS image_bake_admit (
|
|
620
643
|
pool VARCHAR(32) NOT NULL,
|
|
621
644
|
PRIMARY KEY (pool)
|
|
622
|
-
)`,
|
|
645
|
+
) COLLATE utf8mb4_bin`,
|
|
623
646
|
// #151(design/172)流内审批协议持久层的两张表。真源 = approval-ask-store-sql.ts 的
|
|
624
647
|
// `TIDB_APPROVAL_ASK_STATEMENTS`(那里与 PG twin 并排,方言差异一眼可对);展开进这个数组是为了让它们
|
|
625
648
|
// 跟着中央 `ensureSchema` 在 **named-lock 的那条 conn** 上建 —— 把 pool 级 ensure 函数塞进执行循环
|
|
@@ -4,7 +4,7 @@ export class TiDBRateLimiter {
|
|
|
4
4
|
counter;
|
|
5
5
|
constructor(pool, limit, windowMs = 60_000, now, onDegraded, queryTimeoutMs) {
|
|
6
6
|
this.limit = limit;
|
|
7
|
-
this.counter = new WriteBehindCounter(pool, { table: "rate_limit", keyCol: "limit_key", valCol: "
|
|
7
|
+
this.counter = new WriteBehindCounter(pool, { table: "rate_limit", keyCol: "limit_key", valCol: "hits" }, windowMs, now, onDegraded, queryTimeoutMs);
|
|
8
8
|
}
|
|
9
9
|
check(key) {
|
|
10
10
|
if (this.limit <= 0)
|
|
@@ -15,11 +15,8 @@ import { StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAf
|
|
|
15
15
|
import { TiDBSessionStorage } from "./tidb-session-storage.js";
|
|
16
16
|
import { escapeLike } from "./sql-escape.js";
|
|
17
17
|
import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
|
|
18
|
-
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER,
|
|
19
|
-
|
|
20
|
-
* committed) AND its OLDEST row is older than this (an in-flight stream keeps appending fresh rows → safe). Mirrors
|
|
21
|
-
* the file-snapshot store's BLOB_GC_GRACE_MS discipline (an in-flight import must never be reaped mid-stream). */
|
|
22
|
-
const STAGING_GC_GRACE_MS = 3_600_000;
|
|
18
|
+
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, // 两孪生店同源(理由见 kernel 处顶注:同值双抄单边改值 ⇒ 两后端 GC 判定分家)
|
|
19
|
+
} from "../session-sync-kernel.js";
|
|
23
20
|
export class TiDBSessionStore {
|
|
24
21
|
pool;
|
|
25
22
|
/** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one. */
|
|
@@ -44,8 +44,8 @@ import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
|
44
44
|
* tables, so a self-contained ensureSchema is safe (central pg-pool aggregation is done separately). */
|
|
45
45
|
export const PG_TOOL_RESULT_SCHEMA = [
|
|
46
46
|
`CREATE TABLE IF NOT EXISTS tool_result (
|
|
47
|
-
ref VARCHAR(190) NOT NULL,
|
|
48
|
-
content TEXT NOT NULL,
|
|
47
|
+
ref VARCHAR(190) COLLATE "C" NOT NULL,
|
|
48
|
+
content TEXT COLLATE "C" NOT NULL,
|
|
49
49
|
created_at TIMESTAMPTZ(3) NOT NULL,
|
|
50
50
|
PRIMARY KEY (ref)
|
|
51
51
|
)`,
|
|
@@ -39,18 +39,18 @@ function parseRecord(raw, key) {
|
|
|
39
39
|
}
|
|
40
40
|
export async function ensureTiDBUsageWindowSchema(pool) {
|
|
41
41
|
await pool.query(`CREATE TABLE IF NOT EXISTS ${USAGE_WINDOW_TABLE} (
|
|
42
|
-
|
|
42
|
+
usage_key VARCHAR(190) NOT NULL,
|
|
43
43
|
record LONGTEXT NOT NULL,
|
|
44
44
|
updated_at_ms BIGINT NOT NULL,
|
|
45
|
-
PRIMARY KEY (
|
|
45
|
+
PRIMARY KEY (usage_key)
|
|
46
46
|
) COLLATE utf8mb4_bin`);
|
|
47
47
|
}
|
|
48
48
|
export async function ensurePgUsageWindowSchema(q) {
|
|
49
49
|
await q(`CREATE TABLE IF NOT EXISTS ${USAGE_WINDOW_TABLE} (
|
|
50
|
-
|
|
51
|
-
record TEXT NOT NULL,
|
|
50
|
+
usage_key VARCHAR(190) COLLATE "C" NOT NULL,
|
|
51
|
+
record TEXT COLLATE "C" NOT NULL,
|
|
52
52
|
updated_at_ms BIGINT NOT NULL,
|
|
53
|
-
PRIMARY KEY (
|
|
53
|
+
PRIMARY KEY (usage_key)
|
|
54
54
|
)`);
|
|
55
55
|
}
|
|
56
56
|
export class TiDBUsageWindowStore {
|
|
@@ -63,19 +63,19 @@ export class TiDBUsageWindowStore {
|
|
|
63
63
|
try {
|
|
64
64
|
await c.beginTransaction();
|
|
65
65
|
// ensure-first:幂等建空行(EMPTY record)⇒ 下一步 FOR UPDATE 行恒在(锁语义干净,无 gap-lock 死锁形)。
|
|
66
|
-
await c.query(`INSERT IGNORE INTO ${USAGE_WINDOW_TABLE} (
|
|
66
|
+
await c.query(`INSERT IGNORE INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES (?, ?, ?)`, [
|
|
67
67
|
key,
|
|
68
68
|
JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
|
|
69
69
|
at,
|
|
70
70
|
]);
|
|
71
|
-
const [rows] = (await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE
|
|
71
|
+
const [rows] = (await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ? FOR UPDATE`, [key]));
|
|
72
72
|
if (rows.length === 0) {
|
|
73
73
|
// 本表无删除臂(不同于 mailbox 的 drop/reap),ensure 后空 = 真障碍 fail-loud。
|
|
74
74
|
throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
|
|
75
75
|
}
|
|
76
76
|
const record = parseRecord(rows[0].record, key);
|
|
77
77
|
const next = chargeUsageRecord(record, tokens, at, windows);
|
|
78
|
-
await c.query(`UPDATE ${USAGE_WINDOW_TABLE} SET record = ?, updated_at_ms = ? WHERE
|
|
78
|
+
await c.query(`UPDATE ${USAGE_WINDOW_TABLE} SET record = ?, updated_at_ms = ? WHERE usage_key = ?`, [JSON.stringify(next), at, key]);
|
|
79
79
|
await c.commit();
|
|
80
80
|
}
|
|
81
81
|
catch (e) {
|
|
@@ -87,7 +87,7 @@ export class TiDBUsageWindowStore {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
async read(key, windows, now) {
|
|
90
|
-
const [rows] = (await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE
|
|
90
|
+
const [rows] = (await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ?`, [key]));
|
|
91
91
|
const record = rows.length === 0 ? EMPTY_USAGE_WINDOW_RECORD : parseRecord(rows[0].record, key);
|
|
92
92
|
return readUsageRecord(record, windows, now);
|
|
93
93
|
}
|
|
@@ -103,18 +103,18 @@ export class PgUsageWindowStore {
|
|
|
103
103
|
await c.query("BEGIN");
|
|
104
104
|
// ensure-first(TiDB 同注):ON CONFLICT DO NOTHING 幂等建空行,再锁——插入不带锁,锁只从
|
|
105
105
|
// FOR UPDATE 来(复审靶1 的 PG 交错:并发首充双双 ensure,后到方 DO NOTHING 后必须真锁行)。
|
|
106
|
-
await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (
|
|
106
|
+
await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES ($1, $2, $3) ON CONFLICT (usage_key) DO NOTHING`, [
|
|
107
107
|
key,
|
|
108
108
|
JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
|
|
109
109
|
at,
|
|
110
110
|
]);
|
|
111
|
-
const { rows } = await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE
|
|
111
|
+
const { rows } = await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = $1 FOR UPDATE`, [key]);
|
|
112
112
|
if (rows.length === 0) {
|
|
113
113
|
throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
|
|
114
114
|
}
|
|
115
115
|
const record = parseRecord(rows[0].record, key);
|
|
116
116
|
const next = chargeUsageRecord(record, tokens, at, windows);
|
|
117
|
-
await c.query(`UPDATE ${USAGE_WINDOW_TABLE} SET record = $1, updated_at_ms = $2 WHERE
|
|
117
|
+
await c.query(`UPDATE ${USAGE_WINDOW_TABLE} SET record = $1, updated_at_ms = $2 WHERE usage_key = $3`, [JSON.stringify(next), at, key]);
|
|
118
118
|
await c.query("COMMIT");
|
|
119
119
|
}
|
|
120
120
|
catch (e) {
|
|
@@ -126,7 +126,7 @@ export class PgUsageWindowStore {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
async read(key, windows, now) {
|
|
129
|
-
const { rows } = await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE
|
|
129
|
+
const { rows } = await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = $1`, [key]);
|
|
130
130
|
const record = rows.length === 0 ? EMPTY_USAGE_WINDOW_RECORD : parseRecord(rows[0].record, key);
|
|
131
131
|
return readUsageRecord(record, windows, now);
|
|
132
132
|
}
|
|
@@ -42,7 +42,7 @@ export interface CounterTable {
|
|
|
42
42
|
* kind (0 = recovered); `prevStreak` lets the consumer detect the recovery edge. Shared by both dialects. */
|
|
43
43
|
export interface CounterDegradeInfo {
|
|
44
44
|
table: string;
|
|
45
|
-
kind: "flush" | "refresh" | "stalled";
|
|
45
|
+
kind: "flush" | "refresh" | "stalled" | "reap";
|
|
46
46
|
streak: number;
|
|
47
47
|
prevStreak: number;
|
|
48
48
|
error?: string;
|
|
@@ -91,7 +91,15 @@ export declare class SqlWriteBehindCounter {
|
|
|
91
91
|
private flushOnce;
|
|
92
92
|
/** Load the current window's fleet totals into the cache (pendingDelta is separate → never clobbered). */
|
|
93
93
|
refresh(): Promise<void>;
|
|
94
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Delete expired window buckets to bound the table.
|
|
96
|
+
*
|
|
97
|
+
* 🔴 失败**必须留痕**(#192 codex 复审 round2 修):这里原先是裸 `catch { return 0 }` —— 一个只影响
|
|
98
|
+
* DELETE 的故障(权限被收回、引擎单独拒 DELETE)会被吞成"删了 0 行",而 flush/refresh 两条腿照常成功
|
|
99
|
+
* ⇒ 健康遥测全绿,同时 rate_limit / cost_quota 的历史桶无界增长。那正是本仓禁止的静默 fail-open。
|
|
100
|
+
* 处置是**留痕不改失败方向**:清理是 best-effort,失败仍返回 0(把一次运维层面的清理失败升级成业务
|
|
101
|
+
* 失败是错的方向),但走与 flush/refresh 同一套 `bump()` ⇒ 有连败计数、有降级钩子、有恢复边沿。
|
|
102
|
+
*/
|
|
95
103
|
reap(): Promise<number>;
|
|
96
104
|
startRefresh(intervalMs?: number): this;
|
|
97
105
|
stop(): void;
|
|
@@ -171,16 +171,26 @@ export class SqlWriteBehindCounter {
|
|
|
171
171
|
this.bump("refresh", true, err); // S9
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Delete expired window buckets to bound the table.
|
|
176
|
+
*
|
|
177
|
+
* 🔴 失败**必须留痕**(#192 codex 复审 round2 修):这里原先是裸 `catch { return 0 }` —— 一个只影响
|
|
178
|
+
* DELETE 的故障(权限被收回、引擎单独拒 DELETE)会被吞成"删了 0 行",而 flush/refresh 两条腿照常成功
|
|
179
|
+
* ⇒ 健康遥测全绿,同时 rate_limit / cost_quota 的历史桶无界增长。那正是本仓禁止的静默 fail-open。
|
|
180
|
+
* 处置是**留痕不改失败方向**:清理是 best-effort,失败仍返回 0(把一次运维层面的清理失败升级成业务
|
|
181
|
+
* 失败是错的方向),但走与 flush/refresh 同一套 `bump()` ⇒ 有连败计数、有降级钩子、有恢复边沿。
|
|
182
|
+
*/
|
|
175
183
|
async reap() {
|
|
176
184
|
try {
|
|
177
185
|
const { affected } = await this.exec(this.dialect === "tidb"
|
|
178
186
|
? `DELETE FROM ${this.t.table} WHERE window_bucket < ?`
|
|
179
187
|
: `DELETE FROM ${this.t.table} WHERE window_bucket < $1`, [this.bucket()]);
|
|
188
|
+
this.bump("reap", false); // 恢复边沿(健康时是 no-op)
|
|
180
189
|
return affected;
|
|
181
190
|
}
|
|
182
|
-
catch {
|
|
183
|
-
|
|
191
|
+
catch (err) {
|
|
192
|
+
this.bump("reap", true, err);
|
|
193
|
+
return 0; // best-effort:留痕之后照旧不抛
|
|
184
194
|
}
|
|
185
195
|
}
|
|
186
196
|
startRefresh(intervalMs = 5_000) {
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* `POST /v1/assistant/tasks/:id/resume`) instead of FAILING; absent ⇒ a limit still FAILS.
|
|
7
7
|
*
|
|
8
8
|
* 🔴 EXCLUDE verify/cascade tasks. Core runs their implementation via `runWithVerification`/`runCascade`, which
|
|
9
|
-
* map an inner durable suspend to a FAILED result (`errorCode: "suspended
|
|
9
|
+
* map an inner durable suspend to a FAILED result (`errorCode: "unexpected.suspended"`,内层 status
|
|
10
|
+
* "needs_review" 时铸 `"unexpected.needs_review"` —— 真源是 core `agents/suspend-guard.js` 的
|
|
11
|
+
* `mapNestedSuspend`;`suspended.*` 三码是 bg 子代/委派报告面的词,不落在这条 TaskResult 腿上). The service would then
|
|
10
12
|
* `setTerminal(failed)` while core had already minted a pending `resource_limit` checkpoint + suspended the
|
|
11
13
|
* remote VM — ORPHANING both (the run row is terminal, so nothing resumes them). So a resource-limit hit on a
|
|
12
14
|
* verify/cascade task must FAIL CLEANLY (no suspend), exactly as before the flag. This exclusion is the SAME
|
package/dist/resource-suspend.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* `POST /v1/assistant/tasks/:id/resume`) instead of FAILING; absent ⇒ a limit still FAILS.
|
|
7
7
|
*
|
|
8
8
|
* 🔴 EXCLUDE verify/cascade tasks. Core runs their implementation via `runWithVerification`/`runCascade`, which
|
|
9
|
-
* map an inner durable suspend to a FAILED result (`errorCode: "suspended
|
|
9
|
+
* map an inner durable suspend to a FAILED result (`errorCode: "unexpected.suspended"`,内层 status
|
|
10
|
+
* "needs_review" 时铸 `"unexpected.needs_review"` —— 真源是 core `agents/suspend-guard.js` 的
|
|
11
|
+
* `mapNestedSuspend`;`suspended.*` 三码是 bg 子代/委派报告面的词,不落在这条 TaskResult 腿上). The service would then
|
|
10
12
|
* `setTerminal(failed)` while core had already minted a pending `resource_limit` checkpoint + suspended the
|
|
11
13
|
* remote VM — ORPHANING both (the run row is terminal, so nothing resumes them). So a resource-limit hit on a
|
|
12
14
|
* verify/cascade task must FAIL CLEANLY (no suspend), exactly as before the flag. This exclusion is the SAME
|