@sema-agent/server 3.12.0 → 3.14.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/dist/brain.d.ts +8 -0
- package/dist/brain.js +47 -2
- package/dist/http/routes/capabilities.js +9 -0
- package/dist/http/routes/sessions.js +11 -8
- package/dist/http/server.js +13 -2
- package/dist/plugins/tool-result-store-sql.d.ts +7 -39
- package/dist/plugins/tool-result-store-sql.js +54 -6
- package/dist/trace/project.d.ts +3 -0
- package/dist/trace/project.js +5 -1
- package/package.json +2 -2
package/dist/brain.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { type Brain, type BreakerState } from "@sema-agent/core";
|
|
2
2
|
import type { ServiceConfig } from "./config-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* 配了快切层(failover ≥2 路 / 断路器)时,**每路**的重试上限。
|
|
5
|
+
*
|
|
6
|
+
* 取 2 的理由:留够吸收一次瞬时抖动(2 次递增退避 ≈ 500ms + 1000ms ≈ 1.5s),同时让外层在秒级而不是
|
|
7
|
+
* 分钟级拿到错误。断路器阈值 5 在这个值下 ≈ 7.5s 开路 —— 与它"快速失败让 failover 立刻切备"的设计意图
|
|
8
|
+
* 同量级。详见 createBrain 里 retryCap 那段的由来。
|
|
9
|
+
*/
|
|
10
|
+
export declare const FAST_FAIL_MAX_RETRIES = 2;
|
|
3
11
|
/**
|
|
4
12
|
* Build the external "Brain" (LLM) from config.
|
|
5
13
|
*
|
package/dist/brain.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { createAnthropicBrain, createCircuitBreakerBrain, createDegradingBrain, createFailoverBrain, createOpenAIBrain, createRoutingBrain, } from "@sema-agent/core";
|
|
2
2
|
import { resolveModelApiKey } from "./key-resolver.js";
|
|
3
|
+
/**
|
|
4
|
+
* 配了快切层(failover ≥2 路 / 断路器)时,**每路**的重试上限。
|
|
5
|
+
*
|
|
6
|
+
* 取 2 的理由:留够吸收一次瞬时抖动(2 次递增退避 ≈ 500ms + 1000ms ≈ 1.5s),同时让外层在秒级而不是
|
|
7
|
+
* 分钟级拿到错误。断路器阈值 5 在这个值下 ≈ 7.5s 开路 —— 与它"快速失败让 failover 立刻切备"的设计意图
|
|
8
|
+
* 同量级。详见 createBrain 里 retryCap 那段的由来。
|
|
9
|
+
*/
|
|
10
|
+
export const FAST_FAIL_MAX_RETRIES = 2;
|
|
3
11
|
const DEFAULT_RESILIENCE = {
|
|
4
12
|
// Watchdog defaults ON — mirrors config.ts env defaults so a config object without a
|
|
5
13
|
// resilience block gets the same posture. connect/firstToken per the [880] fleet audit
|
|
@@ -52,7 +60,32 @@ export function createBrain(config, deps = {}) {
|
|
|
52
60
|
// 压过引擎默认 ⇒ core 把默认抬到 10 对 server 部署零效果。缺席不传之后引擎默认当家、抬默认自动继承。
|
|
53
61
|
// (五报清查 R1① 的 server 半场:限流 provider 走的正是这条 openai 兼容腿,而它此前**零配置出口**——
|
|
54
62
|
// 同文件的 anthropic 腿反倒早有 `ANTHROPIC_MAX_RETRIES`。钉在 test/brain.test.ts 的两条上。)
|
|
55
|
-
|
|
63
|
+
//
|
|
64
|
+
// 🔴 **拓扑感知的缺席值**(2026-08-01,core 2.9.0 提货验收当场实测出来的层间相互作用):
|
|
65
|
+
// core 2.9.0 把每次调用的重试默认抬到 10 且退避改递增形(`min(500·2^(n-1),32000)`),一次失败调用
|
|
66
|
+
// 要耗尽 **~180s** 才把错误交给外层(实测 179.7s)。而 failover / 断路器 / 反应式降级 **三层全在
|
|
67
|
+
// 每路 brain 的重试之外** —— 内层不放弃,外层看不见错误 ⇒ 主网关死了要 ~180s 才切备(旧形 ~0.6s)。
|
|
68
|
+
// 断路器更反讽:它要数失败次数才开路,每笔失败 180s、阈值 5 = 15 分钟,而它的存在理由正是"快速失败
|
|
69
|
+
// 让 failover 立刻切备"。
|
|
70
|
+
//
|
|
71
|
+
// 两边各自都对:core 的 10 次对**单 provider 被限流**是正解;server 的韧性栈对**多网关拓扑**是正解。
|
|
72
|
+
// **正确的默认取决于拓扑,而引擎不知道拓扑** —— 这条信息只有 server 有。
|
|
73
|
+
//
|
|
74
|
+
// ⚠️ 这**不是**又一次"中间层钉死引擎默认"(那正是本文件上一版刚修掉的病),三点区别:
|
|
75
|
+
// ① 显式 `GATEWAY_MAX_RETRIES` **恒赢** —— 运维的话压过我们的推断;
|
|
76
|
+
// ② 只在**外层真有快切层**时才收窄(≥2 路 failover 或断路器开);单路 = 缺席不传,引擎默认当家
|
|
77
|
+
// (那正是限流 provider 的形,10 次是对的);
|
|
78
|
+
// ③ 收窄**在启动日志里说出来**(`gatewayRetryCap` 字段),不是静默替换。
|
|
79
|
+
// 三个快切层,缺一个谓词就漏一族(反应式降级这一格是首版漏掉、被 brain 套件当场咬出来的):
|
|
80
|
+
// ① failover(≥2 路)② 断路器 ③ 反应式降级 —— 后者尤其要算:它的触发条件**就是** `rate_limit`,
|
|
81
|
+
// 而运维开 `MODEL_DEGRADE_REACTIVE` 等于明说"撞限流就换便宜模型,别在原地等"。若这里不收窄,
|
|
82
|
+
// 主模型会先把 10 次递增退避耗完(~180s)才轮到降级 —— 把运维的选择拖成一句空话。
|
|
83
|
+
const fastFailTopology = config.gatewayFallbackUrls.length > 0 || r.circuitBreaker || Boolean(config.degrade?.reactive);
|
|
84
|
+
const retryCap = config.gatewayMaxRetries !== undefined
|
|
85
|
+
? { maxRetries: config.gatewayMaxRetries }
|
|
86
|
+
: fastFailTopology
|
|
87
|
+
? { maxRetries: FAST_FAIL_MAX_RETRIES }
|
|
88
|
+
: {};
|
|
56
89
|
// Local openai-compatible gateway stack: primary + optional same-protocol fallbacks (failover).
|
|
57
90
|
const routes = [config.gatewayBaseUrl, ...config.gatewayFallbackUrls];
|
|
58
91
|
const openaiBrains = routes.map((baseUrl, i) => {
|
|
@@ -144,7 +177,9 @@ export function createBrain(config, deps = {}) {
|
|
|
144
177
|
get apiKey() {
|
|
145
178
|
return fbApiKey();
|
|
146
179
|
},
|
|
147
|
-
|
|
180
|
+
// 🔴 degrade fallback **不吃** fastFail 收窄:它是最外层的兜底,外面**没有**别的切换层了,
|
|
181
|
+
// 所以它该按引擎默认那样耐心重试(限流场景正靠它)。显式 env 仍然恒赢。
|
|
182
|
+
...(config.gatewayMaxRetries !== undefined ? { maxRetries: config.gatewayMaxRetries } : {}),
|
|
148
183
|
fetchImpl,
|
|
149
184
|
...timeouts,
|
|
150
185
|
});
|
|
@@ -170,6 +205,16 @@ export function brainSummary(config) {
|
|
|
170
205
|
idleTimeoutMs: r.idleTimeoutMs || null,
|
|
171
206
|
circuitBreaker: breakerActive,
|
|
172
207
|
circuitBreakerRequested: r.circuitBreaker,
|
|
208
|
+
// 🔴 每路重试上限的**来源**必须可见(2026-08-01):收窄是我们按拓扑推断的,不是引擎默认,
|
|
209
|
+
// 静默替换等于让运维读不出"为什么这台机的失败切换比另一台快"。三态:
|
|
210
|
+
// `env:<n>` = 运维显式配了 GATEWAY_MAX_RETRIES(恒赢)
|
|
211
|
+
// `fast-fail:<n>`= 外层有快切层(failover/断路器),我们收窄到 n(见 createBrain 的由来注)
|
|
212
|
+
// `engine-default` = 缺席不传,引擎默认当家(单路限流 provider 的正解)
|
|
213
|
+
gatewayRetryCap: config.gatewayMaxRetries !== undefined
|
|
214
|
+
? `env:${config.gatewayMaxRetries}`
|
|
215
|
+
: hasFailover || r.circuitBreaker || config.degrade?.reactive
|
|
216
|
+
? `fast-fail:${FAST_FAIL_MAX_RETRIES}`
|
|
217
|
+
: "engine-default",
|
|
173
218
|
circuitBreakerNoop: r.circuitBreaker && !hasFailover ? "needs ≥2 gateway routes" : null,
|
|
174
219
|
// reactive degrade is active only if a valid catalog target resolves
|
|
175
220
|
reactiveDegradeTo: config.degrade?.reactive && config.models?.[config.degrade.to] ? config.degrade.to : null,
|
|
@@ -42,6 +42,15 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
42
42
|
// AskUserQuestion-suspend + the approval center + resumable checkpoints, independent of whether any
|
|
43
43
|
// TOOL is gated (APPROVAL_REQUIRE = the additive F4 layer). Legacy poll-gate approvalStore does NOT
|
|
44
44
|
// count: alone it can't suspend (the P2-preflight lie).
|
|
45
|
+
//
|
|
46
|
+
// 🔴 **辖域比名字宽**(2026-08-01,web [C33]③.3 踩出来的):这个谓词同时是 `/v1/assistant/*`
|
|
47
|
+
// 整条车道的挂载条件 —— `routes/approvals-assistant.ts` 的分支头逐字是
|
|
48
|
+
// `if (deps.checkpointStore && (url.startsWith("/v1/approvals") || url.startsWith("/v1/assistant")))`。
|
|
49
|
+
// 所以 **`GET /v1/assistant/tasks`(计划运行/调度视图)也由这个键判**,而不是由 `scheduler`
|
|
50
|
+
// (那个键说的是 host 腿的自唤醒 cron 工具,与本视图无关)。
|
|
51
|
+
// web 因为按名字猜、用 `scheduler` 判这个面,在 local 部署形上永久渲「不可达」——名字窄、谓词宽
|
|
52
|
+
// 就是这么伤人的。键名不改(改名是 wire 破坏性变更),改成把辖域写在这里 + 附录 A,
|
|
53
|
+
// 让「says yes ⟺ route works」这句话对**读文档的人**也成立,而不只对写代码的人。
|
|
45
54
|
approvals: Boolean(deps.checkpointStore),
|
|
46
55
|
sessions: Boolean(deps.sessionAudit),
|
|
47
56
|
// [1196] session 级 SSE 订阅面:与 /v1/sessions/:id/events 的 501 谓词同源(sessionWatch 只在
|
|
@@ -220,8 +220,11 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
220
220
|
// codex 一轮生命周期状态机:①连接坑与 close 守卫在任何 await 之前同步建立(读存储期间断连不再
|
|
221
221
|
// 漏清/漏还坑);②每个 await 之后查 closed;③所有拒绝/异常路径经同一 release。
|
|
222
222
|
if (sse.sseConnections >= (deps.sessionEventsMaxConnections ?? 256)) {
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
// 🔴 机器码而非裸 writeHead(2026-07-31 缝合审):这条 503 与 drain 的 503 走同一条流,
|
|
224
|
+
// 而 drain 的带 `errorCode:"draining"`(冻结契约)。没有码,消费端只能靠自由文本字面区分
|
|
225
|
+
// 「连接帽 ⇒ 回落 /head 轮询(自愈,保持连接策略)」与「该重连替换实例」—— 正是本仓要根除的姿势。
|
|
226
|
+
res.setHeader("retry-after", "5");
|
|
227
|
+
sendError(res, 503, "state.sse_connection_cap", "session-events connection cap reached — fall back to /head polling");
|
|
225
228
|
return;
|
|
226
229
|
}
|
|
227
230
|
sse.sseConnections++; // 同步占坑(查+占同拍,慢读并发不再超卖);此后一切路径必经 cleanup 还坑
|
|
@@ -247,8 +250,8 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
247
250
|
const stallBound = deps.sessionEventsStallMs ?? 60_000;
|
|
248
251
|
if (sse.sseOwnerLookups >= 64) {
|
|
249
252
|
cleanup();
|
|
250
|
-
res.
|
|
251
|
-
res
|
|
253
|
+
res.setHeader("retry-after", "5");
|
|
254
|
+
sendError(res, 503, "state.sse_owner_lookup_cap", "session-events owner-lookup concurrency cap — retry");
|
|
252
255
|
return;
|
|
253
256
|
}
|
|
254
257
|
sse.sseOwnerLookups++;
|
|
@@ -271,8 +274,8 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
271
274
|
return; // 读期断连:close 守卫已还坑,别再写已死响应
|
|
272
275
|
if (owner === "__stall__") {
|
|
273
276
|
cleanup();
|
|
274
|
-
res.
|
|
275
|
-
res
|
|
277
|
+
res.setHeader("retry-after", "5");
|
|
278
|
+
sendError(res, 503, "state.sse_owner_lookup_stalled", "session-events owner lookup stalled — retry");
|
|
276
279
|
return;
|
|
277
280
|
}
|
|
278
281
|
if (owner === undefined) {
|
|
@@ -364,8 +367,8 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
364
367
|
unsubscribe = watch.subscribe(evSession, owner ?? null, onHead, onEnd); // owner 铸入条目(四轮跨副本围栏);已知条目=同步 replay 首帧;新条目=首拍探针 ≤hotMs
|
|
365
368
|
if (!unsubscribe) {
|
|
366
369
|
cleanup();
|
|
367
|
-
res.
|
|
368
|
-
res
|
|
370
|
+
res.setHeader("retry-after", "5");
|
|
371
|
+
sendError(res, 503, "state.sse_probe_cap", "session-events probe cap reached — fall back to /head polling");
|
|
369
372
|
return;
|
|
370
373
|
}
|
|
371
374
|
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache, no-transform", connection: "keep-alive", "x-accel-buffering": "no" });
|
package/dist/http/server.js
CHANGED
|
@@ -2547,9 +2547,20 @@ function cors(res, req, origins, principalHeader) {
|
|
|
2547
2547
|
if (allow === undefined)
|
|
2548
2548
|
return;
|
|
2549
2549
|
res.setHeader("access-control-allow-origin", allow);
|
|
2550
|
-
|
|
2550
|
+
// 🔴 这三行自 2026-07-14 公开快照起逐字未动,而这期间进来的路由与请求头它一个都不认
|
|
2551
|
+
// (2026-08-01 缝合审)。浏览器前端撞的不是"请求失败"而是**请求根本发不出去** —— OPTIONS 预检就被拒。
|
|
2552
|
+
// 维护纪律:**新增 PUT/DELETE 路由、或新读一个自定义请求头时,这里必须同步**;
|
|
2553
|
+
// `test/cors-v2.test.ts` 的三条钉逐个断言方法与头名,漏一个当场红。
|
|
2554
|
+
res.setHeader("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
2551
2555
|
// Include the principal header so a browser front-end that sends it isn't rejected by the OPTIONS preflight
|
|
2552
2556
|
// (which would otherwise drop identity → an owner=null session under requirePrincipal=false).
|
|
2553
|
-
|
|
2557
|
+
// 后五项都是**真被 handler 读**的自定义头:`idempotency-key`(tasks/runs/images)、
|
|
2558
|
+
// `x-detach-on-disconnect`(tasks 断连不中止)、direct-door 决策证明三件套(approvals /decide 的
|
|
2559
|
+
// crypto 绑定腿 —— 缺了它,direct-door worker 上的 web 审批面在浏览器里根本用不了)。
|
|
2560
|
+
res.setHeader("access-control-allow-headers", `content-type, authorization, last-event-id, ${principalHeader}, idempotency-key, x-detach-on-disconnect, x-approval-principal-token, x-approval-mac, x-approval-mac-kid`);
|
|
2561
|
+
// 🔴 expose-headers 此前**整个缺席** ⇒ fetch 类客户端读不到 `X-Task-Id`(routes/tasks.ts 经 sseHeaders
|
|
2562
|
+
// 下发的 durable rewind handle)。tasks.ts 的 G15 meta 帧注释把这归因为「EventSource 与部分代理读不到
|
|
2563
|
+
// 响应头」—— 对 EventSource 成立,但对 fetch 客户端真因是这里缺 expose,meta 帧只是绕过。
|
|
2564
|
+
res.setHeader("access-control-expose-headers", "x-task-id, retry-after");
|
|
2554
2565
|
}
|
|
2555
2566
|
//# sourceMappingURL=server.js.map
|
|
@@ -1,40 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Durable backing store for offloaded large tool results (core 1.47 offload / 1.49 durable contract) —
|
|
3
|
-
* SINGLE-FILE DUAL-DIALECT (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical
|
|
4
|
-
* `TiDBToolResultStore` / `PgToolResultStore` class names survive as thin ctor subclasses so every consumer
|
|
5
|
-
* (store-backend.ts, the fake-pool unit suites, the real-DB integration suites) is untouched.
|
|
6
|
-
*
|
|
7
|
-
* Mirrors core's reference `TiDbToolResultStore` adapter. Without this, the Runner defaults to a
|
|
8
|
-
* task-scoped in-memory store: an async run that wakes on ANOTHER replica can't `read_tool_result` an
|
|
9
|
-
* offloaded result (the ref misses → the model is told it's unavailable; the preview still stands, so it
|
|
10
|
-
* degrades, never crashes). A durable store survives wake/resume across the stateless fleet.
|
|
11
|
-
*
|
|
12
|
-
* core namespaces the ref as `tr_<sessionId>_<toolCallId>` (1.49) → globally unique → usable directly as
|
|
13
|
-
* the PRIMARY KEY (no composite key needed). `put` is write-once (idempotent on replay/retry: a retry is
|
|
14
|
-
* a NEW toolCallId → new ref, so a given ref never changes content). Retention is anchored to a run's
|
|
15
|
-
* RECOVERABLE window via TTL (`reapOlderThan`): a resumable run may re-fetch an old ref after wake, but a
|
|
16
|
-
* completed run only needs the preview for audit/replay — so a TTL comfortably exceeding the run lifetime
|
|
17
|
-
* bounds storage without losing recoverable full-text.
|
|
18
|
-
*
|
|
19
|
-
* ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
|
|
20
|
-
* - `?` placeholders vs `$n`
|
|
21
|
-
* - write-once insert = `INSERT IGNORE` vs `INSERT ... ON CONFLICT (ref) DO NOTHING`
|
|
22
|
-
* - affectedRows vs rowCount (via SqlDriver)
|
|
23
|
-
* - `SUBSTRING(content, ?, ?)` (1-indexed, comma form) vs the ANSI `SUBSTRING(content FROM $n::int FOR
|
|
24
|
-
* $m::int)` positional form WITH explicit `::int` casts — node-pg binds bare params as text, and
|
|
25
|
-
* `SUBSTRING(s FROM <text>)` is the POSIX-REGEX overload (it would treat $1 as a pattern); the cast
|
|
26
|
-
* forces the positional integer overload. `CHAR_LENGTH` is ANSI and shared.
|
|
27
|
-
* - content column: TiDB LONGTEXT vs PG TEXT (PG has no LONGTEXT; TEXT is unbounded), DATETIME(3) vs
|
|
28
|
-
* TIMESTAMPTZ(3) — schema only (TiDB DDL in tidb-pool.ts; PG DDL self-contained below, exported for the
|
|
29
|
-
* integration test + pg-pool.ts's central apply).
|
|
30
|
-
* - `put`'s UTF-16 sanitize is a GENUINE algorithm divergence, not just SQL text — see the dialect branch
|
|
31
|
-
* inline for why.
|
|
32
|
-
* - ESCAPE-clause literal: TiDB spells `ESCAPE '\\\\'` (declares the backslash escape char explicitly —
|
|
33
|
-
* under `sql_mode=NO_BACKSLASH_ESCAPES` `\` is NOT the LIKE escape char by default, so leaving it
|
|
34
|
-
* implicit would silently stop escaping and let a crafted sessionId's `_`/`%` over-match OTHER
|
|
35
|
-
* sessions' rows); PG spells `ESCAPE '\'` (its default escape char already, kept for lock-step intent
|
|
36
|
-
* with the TiDB twin rather than out of necessity).
|
|
37
|
-
*/
|
|
38
1
|
import type { ToolResultStore, ToolResultSlice } from "@sema-agent/core";
|
|
39
2
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
40
3
|
import type { Pool as PgPool, PoolClient as PgPoolClient } from "pg";
|
|
@@ -52,8 +15,13 @@ export declare class SqlToolResultStore implements ToolResultStore {
|
|
|
52
15
|
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
53
16
|
private q;
|
|
54
17
|
/** D1(试剂盒揪出,RB-266 语义):ref 是主键且 deleteBySession 靠 `tr_<sid>_%` 前缀清理——不合规
|
|
55
|
-
* ref 的行既躲过 session 删除也只能等 TTL
|
|
56
|
-
*
|
|
18
|
+
* ref 的行既躲过 session 删除也只能等 TTL,永远清不掉。
|
|
19
|
+
*
|
|
20
|
+
* 🔴 判定**单源**:直调 core 的 `assertSafeToolResultRef`(core 2.8.0 起入公共面)。
|
|
21
|
+
* 在此之前这里是那 6 个判别条件的**本地镜像** —— 我在 [2159] 主动交出过这条裂缝:镜像是第二真源,
|
|
22
|
+
* core 哪天收紧一格(加 Windows 保留名、长度上限……)我方不会跟着动,而**试剂盒会继续绿**
|
|
23
|
+
* (它测的是「拒不拒」,不是「按同一张表拒」)。core 按请求追加了导出,镜像随之删除,裂缝闭合。
|
|
24
|
+
* 抛错文案与 core 逐字相同,所以 wire 与既有钉都不变。 */
|
|
57
25
|
private isUnsafeRef;
|
|
58
26
|
private assertSafeRef;
|
|
59
27
|
put(ref: string, content: string): Promise<void>;
|
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable backing store for offloaded large tool results (core 1.47 offload / 1.49 durable contract) —
|
|
3
|
+
* SINGLE-FILE DUAL-DIALECT (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical
|
|
4
|
+
* `TiDBToolResultStore` / `PgToolResultStore` class names survive as thin ctor subclasses so every consumer
|
|
5
|
+
* (store-backend.ts, the fake-pool unit suites, the real-DB integration suites) is untouched.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors core's reference `TiDbToolResultStore` adapter. Without this, the Runner defaults to a
|
|
8
|
+
* task-scoped in-memory store: an async run that wakes on ANOTHER replica can't `read_tool_result` an
|
|
9
|
+
* offloaded result (the ref misses → the model is told it's unavailable; the preview still stands, so it
|
|
10
|
+
* degrades, never crashes). A durable store survives wake/resume across the stateless fleet.
|
|
11
|
+
*
|
|
12
|
+
* core namespaces the ref as `tr_<sessionId>_<toolCallId>` (1.49) → globally unique → usable directly as
|
|
13
|
+
* the PRIMARY KEY (no composite key needed). `put` is write-once (idempotent on replay/retry: a retry is
|
|
14
|
+
* a NEW toolCallId → new ref, so a given ref never changes content). Retention is anchored to a run's
|
|
15
|
+
* RECOVERABLE window via TTL (`reapOlderThan`): a resumable run may re-fetch an old ref after wake, but a
|
|
16
|
+
* completed run only needs the preview for audit/replay — so a TTL comfortably exceeding the run lifetime
|
|
17
|
+
* bounds storage without losing recoverable full-text.
|
|
18
|
+
*
|
|
19
|
+
* ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
|
|
20
|
+
* - `?` placeholders vs `$n`
|
|
21
|
+
* - write-once insert = `INSERT IGNORE` vs `INSERT ... ON CONFLICT (ref) DO NOTHING`
|
|
22
|
+
* - affectedRows vs rowCount (via SqlDriver)
|
|
23
|
+
* - `SUBSTRING(content, ?, ?)` (1-indexed, comma form) vs the ANSI `SUBSTRING(content FROM $n::int FOR
|
|
24
|
+
* $m::int)` positional form WITH explicit `::int` casts — node-pg binds bare params as text, and
|
|
25
|
+
* `SUBSTRING(s FROM <text>)` is the POSIX-REGEX overload (it would treat $1 as a pattern); the cast
|
|
26
|
+
* forces the positional integer overload. `CHAR_LENGTH` is ANSI and shared.
|
|
27
|
+
* - content column: TiDB LONGTEXT vs PG TEXT (PG has no LONGTEXT; TEXT is unbounded), DATETIME(3) vs
|
|
28
|
+
* TIMESTAMPTZ(3) — schema only (TiDB DDL in tidb-pool.ts; PG DDL self-contained below, exported for the
|
|
29
|
+
* integration test + pg-pool.ts's central apply).
|
|
30
|
+
* - `put`'s UTF-16 sanitize is a GENUINE algorithm divergence, not just SQL text — see the dialect branch
|
|
31
|
+
* inline for why.
|
|
32
|
+
* - ESCAPE-clause literal: TiDB spells `ESCAPE '\\\\'` (declares the backslash escape char explicitly —
|
|
33
|
+
* under `sql_mode=NO_BACKSLASH_ESCAPES` `\` is NOT the LIKE escape char by default, so leaving it
|
|
34
|
+
* implicit would silently stop escaping and let a crafted sessionId's `_`/`%` over-match OTHER
|
|
35
|
+
* sessions' rows); PG spells `ESCAPE '\'` (its default escape char already, kept for lock-step intent
|
|
36
|
+
* with the TiDB twin rather than out of necessity).
|
|
37
|
+
*/
|
|
38
|
+
import { assertSafeToolResultRef } from "@sema-agent/core";
|
|
1
39
|
import { escapeLike } from "./sql-escape.js";
|
|
2
40
|
import { pgSanitizeText } from "./pg-safe-json.js";
|
|
3
41
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
@@ -29,15 +67,25 @@ export class SqlToolResultStore {
|
|
|
29
67
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
30
68
|
}
|
|
31
69
|
/** D1(试剂盒揪出,RB-266 语义):ref 是主键且 deleteBySession 靠 `tr_<sid>_%` 前缀清理——不合规
|
|
32
|
-
* ref 的行既躲过 session 删除也只能等 TTL
|
|
33
|
-
*
|
|
70
|
+
* ref 的行既躲过 session 删除也只能等 TTL,永远清不掉。
|
|
71
|
+
*
|
|
72
|
+
* 🔴 判定**单源**:直调 core 的 `assertSafeToolResultRef`(core 2.8.0 起入公共面)。
|
|
73
|
+
* 在此之前这里是那 6 个判别条件的**本地镜像** —— 我在 [2159] 主动交出过这条裂缝:镜像是第二真源,
|
|
74
|
+
* core 哪天收紧一格(加 Windows 保留名、长度上限……)我方不会跟着动,而**试剂盒会继续绿**
|
|
75
|
+
* (它测的是「拒不拒」,不是「按同一张表拒」)。core 按请求追加了导出,镜像随之删除,裂缝闭合。
|
|
76
|
+
* 抛错文案与 core 逐字相同,所以 wire 与既有钉都不变。 */
|
|
34
77
|
isUnsafeRef(ref) {
|
|
35
|
-
|
|
36
|
-
|
|
78
|
+
// core 只给 assert 形(它就是判定本体);读面要的是谓词,由同一个 assert 派生 —— 仍是单源。
|
|
79
|
+
try {
|
|
80
|
+
assertSafeToolResultRef(ref);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
37
86
|
}
|
|
38
87
|
assertSafeRef(ref) {
|
|
39
|
-
|
|
40
|
-
throw new Error(`tool-result store: unsafe ref ${JSON.stringify(ref)}`);
|
|
88
|
+
assertSafeToolResultRef(ref);
|
|
41
89
|
}
|
|
42
90
|
async put(ref, content) {
|
|
43
91
|
this.assertSafeRef(ref);
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -248,6 +248,9 @@ export declare function brainStatusEventData(ev: {
|
|
|
248
248
|
phase: string;
|
|
249
249
|
detail?: string;
|
|
250
250
|
retryInSec?: number;
|
|
251
|
+
attempt?: number;
|
|
252
|
+
maxRetries?: number;
|
|
253
|
+
retryInMs?: number;
|
|
251
254
|
eventId?: string;
|
|
252
255
|
parentToolCallId?: string;
|
|
253
256
|
}): Record<string, unknown>;
|
package/dist/trace/project.js
CHANGED
|
@@ -289,10 +289,14 @@ export function workspaceChangedEventData(ev) {
|
|
|
289
289
|
* SERVICE's own observation record (why a turn stalled: rate_limited/retrying/reconnecting/circuit_open),
|
|
290
290
|
* not a core event replay. Whitelist: the closed phase union + neutral bounded detail (redacted) + retryInSec. */
|
|
291
291
|
export function brainStatusEventData(ev) {
|
|
292
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v);
|
|
292
293
|
return {
|
|
293
294
|
phase: String(ev.phase),
|
|
294
295
|
...(typeof ev.detail === "string" && ev.detail.length > 0 ? { detail: redactSecrets(ev.detail).slice(0, 300) } : {}),
|
|
295
|
-
...(
|
|
296
|
+
...(num(ev.retryInSec) ? { retryInSec: ev.retryInSec } : {}),
|
|
297
|
+
...(num(ev.attempt) ? { attempt: ev.attempt } : {}),
|
|
298
|
+
...(num(ev.maxRetries) ? { maxRetries: ev.maxRetries } : {}),
|
|
299
|
+
...(num(ev.retryInMs) ? { retryInMs: ev.retryInMs } : {}),
|
|
296
300
|
...identityFields(ev),
|
|
297
301
|
};
|
|
298
302
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^2.
|
|
57
|
+
"@sema-agent/core": "^2.10.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.11.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|