@sema-agent/server 7.93.7 → 7.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +52 -0
- package/dist/boot/resolve-spec.js +14 -1
- package/dist/boot/stage-09-leader.js +1 -1
- package/dist/boot/stores.js +6 -3
- package/dist/boot/workflow-orchestration.js +5 -1
- package/dist/config-catalog.js +5 -3
- package/dist/config-center/types.d.ts +1 -1
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +17 -0
- package/dist/config-types.d.ts +44 -3
- package/dist/config-types.js +1 -0
- package/dist/config.js +24 -4
- package/dist/hosted-posture.d.ts +176 -0
- package/dist/hosted-posture.js +76 -0
- package/dist/http/resume-ctx-read.d.ts +40 -0
- package/dist/http/resume-ctx-read.js +25 -0
- package/dist/http/resume-legs.js +62 -42
- package/dist/http/routes/capabilities.js +3 -0
- package/dist/http/routes/images.js +12 -0
- package/dist/http/routes/runs.js +18 -4
- package/dist/http/routes/workflows.js +3 -2
- package/dist/http/wire-types.d.ts +13 -4
- package/dist/observability/corrupt-read-seat.d.ts +56 -0
- package/dist/observability/corrupt-read-seat.js +16 -0
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/orchestration/workflow-journal-entry.d.ts +24 -6
- package/dist/orchestration/workflow-journal-entry.js +17 -10
- package/dist/plugins/approval-ask-store-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +16 -3
- package/dist/plugins/checkpoint-store-sql.js +7 -9
- package/dist/plugins/image-bake-store-sql.js +85 -13
- package/dist/plugins/image-index-sql.js +75 -12
- package/dist/plugins/local-checkpoint-store.d.ts +23 -2
- package/dist/plugins/local-checkpoint-store.js +40 -33
- package/dist/plugins/local-session-store.d.ts +14 -0
- package/dist/plugins/local-session-store.js +12 -7
- package/dist/plugins/local-sidecar-quarantine.d.ts +10 -0
- package/dist/plugins/local-sidecar-quarantine.js +15 -0
- package/dist/plugins/local-task-attachment-store.d.ts +1 -1
- package/dist/plugins/local-task-attachment-store.js +5 -2
- package/dist/plugins/mailbox-store-sql.d.ts +6 -3
- package/dist/plugins/mailbox-store-sql.js +45 -43
- package/dist/plugins/memory-embedder-fingerprint.js +8 -12
- package/dist/plugins/memory-engine-pg.js +34 -16
- package/dist/plugins/memory-engine-tidb.js +19 -12
- package/dist/plugins/memory-engine-vector-util.d.ts +26 -0
- package/dist/plugins/memory-engine-vector-util.js +3 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +6 -0
- package/dist/plugins/memory-sync-store-pg.js +14 -10
- package/dist/plugins/memory-sync-store-tidb.js +7 -11
- package/dist/plugins/pg-session-storage.js +26 -28
- package/dist/plugins/session-event-payload.d.ts +37 -0
- package/dist/plugins/session-event-payload.js +20 -0
- package/dist/plugins/sql-json-column.d.ts +124 -22
- package/dist/plugins/sql-json-column.js +22 -1
- package/dist/plugins/sql-row-helpers.d.ts +10 -16
- package/dist/plugins/sql-row-helpers.js +0 -29
- package/dist/plugins/store-backend.js +10 -12
- package/dist/plugins/store-contracts.d.ts +4 -6
- package/dist/plugins/store-contracts.js +0 -60
- package/dist/plugins/task-attachment-store.d.ts +21 -12
- package/dist/plugins/task-attachment-store.js +12 -0
- package/dist/plugins/tidb-session-storage.d.ts +2 -2
- package/dist/plugins/tidb-session-storage.js +12 -11
- package/dist/plugins/tidb-session-store.d.ts +2 -2
- package/dist/plugins/tidb-session-store.js +20 -22
- package/dist/plugins/usage-window-store-sql.d.ts +9 -1
- package/dist/plugins/usage-window-store-sql.js +23 -9
- package/dist/plugins/workflow-journal-store-sql.js +16 -4
- package/dist/run-local.js +4 -10
- package/dist/usage-analytics.d.ts +14 -1
- package/dist/usage-analytics.js +11 -17
- package/package.json +2 -2
|
@@ -20,10 +20,23 @@
|
|
|
20
20
|
* `recordFailOpen`),让每一处容忍都留痕、都可数。本模块只有一个失败方向。
|
|
21
21
|
*
|
|
22
22
|
* ── 机器门 ──────────────────────────────────────────────────────────────────────────────────
|
|
23
|
-
* `test/sql-json-column-single-owner.test.ts`
|
|
23
|
+
* `test/sql-json-column-single-owner.test.ts` 四道:①`JSON.parse(` 在 `src/plugins/**` 只许出现在本文件
|
|
24
24
|
* (未改调的店走显式 `PENDING_STORES` 登记 + 反向门);②JSON 承载列(清单由 `schema-ensure-legs` 的录制
|
|
25
25
|
* 驱动**机器派生**,不手抄)在 SELECT 投影里必须经 {@link jsonColumnText};③{@link decodeJsonColumn} 的
|
|
26
|
-
* `read` 实参必须是具名读者,禁内联 `(b) => b as T
|
|
26
|
+
* `read` 实参必须是具名读者,禁内联 `(b) => b as T`;④(S-577)**读者 × 列可空性**求交,**双向**:每一只
|
|
27
|
+
* 形读者对承载列的可空性要求写在门的那张闭集表里,与 DDL 机器派生的读数不符即红(两个方向各一格)。
|
|
28
|
+
* 那条要求此前是注释里的一句散文,而整仓第二个用它的地方把它违了一整版。
|
|
29
|
+
*
|
|
30
|
+
* ── 「任意 JSON 值」那一族的三只铸点(S-577;区别在**名字**里,不在散文里)─────────────────────
|
|
31
|
+
* 一列的载荷没有声明形时,`null` 字面算不算合法值,**只由这一列有没有「缺席」这个状态**决定 —— 而那是
|
|
32
|
+
* 列可空性的另一种说法。所以这一族按可空性拆成三只,各自的前置条件由门双向咬:
|
|
33
|
+
* | 铸点 | 承载列必须 | JSON 字面 `null` |
|
|
34
|
+
* |---|---|---|
|
|
35
|
+
* | {@link readTriStateJsonValueColumn} | **可空** | 合法值(缺席由列 NULL 单独表达) |
|
|
36
|
+
* | {@link readNonNullJsonValueColumn} | **可空** | 判坏(缺席就落 SQL NULL,写不进 `null` 字面) |
|
|
37
|
+
* | {@link readOpaqueJsonValueColumn} | **NOT NULL** | 合法值(该列**没有**缺席态 ⇒ 不与任何东西同形) |
|
|
38
|
+
* 🔴 三条前置条件都是**机器判**的,没有第四种「看情况」——一列要是三只都不满足,它要的是自己的结构化
|
|
39
|
+
* 读者({@link createZodColumnReader} / {@link createObjectColumnReader}),不是这一族里的第四只。
|
|
27
40
|
*/
|
|
28
41
|
import { z } from "zod";
|
|
29
42
|
import type { SqlDialect } from "./sql-driver.js";
|
|
@@ -42,20 +55,35 @@ export declare function jsonColumnText(dialect: SqlDialect, column: string): str
|
|
|
42
55
|
* 同一只投影的**裸表达式**(不带别名)—— 给 `WHERE` / `UPDATE … WHERE` 这种**没有别名位**的地方用。
|
|
43
56
|
*
|
|
44
57
|
* 🔴 存在的唯一理由是「别名位在那里不合法」,**不是**给读腿留第二条路:读腿一律用 {@link jsonColumnText}
|
|
45
|
-
* (别名是它的产物的一部分,理由见那只函数)
|
|
46
|
-
* M4(把 `device_audit.detail` 里 JSON 文本 `null` 的旧行改成列 NULL)
|
|
58
|
+
* (别名是它的产物的一部分,理由见那只函数)。今天整仓**两处**消费方,各自的「别名位不合法」都是真的:
|
|
59
|
+
* · `one-time-migrations.ts` 的 M4(把 `device_audit.detail` 里 JSON 文本 `null` 的旧行改成列 NULL)——
|
|
60
|
+
* 它要在 `WHERE` 里判这一列的**文本**,而 `WHERE` 没有别名位;
|
|
61
|
+
* · `workflow-journal-store-sql.ts` 的 `loadPage`(S-550 车③a)—— 那一列的投影裹在一条
|
|
62
|
+
* `CASE WHEN LENGTH(result) > ? THEN NULL ELSE <expr> END AS result_json` 里:**别名属于整条 CASE**
|
|
63
|
+
* (`result_json`,因为超字节门的那一支要给 `NULL`),CASE 里面的表达式位上写别名是语法错。
|
|
47
64
|
* 而「怎么把一列 JSON 取成文本」这件事整仓只能有一种写法:两种承载(真 JSON 列 / LONGTEXT 别名)在这条
|
|
48
|
-
*
|
|
65
|
+
* 表达式下合成同一只串,迁移的判等、分页腿的取文本因此与读腿的判形**看同一份字节**。
|
|
66
|
+
*
|
|
67
|
+
* 🚧 **门② 看不见本函数的产物**(如实声明,车① §8.4 登记过的射程边界):它只跳过 `jsonColumnText(…)` 的
|
|
68
|
+
* 子树,本函数的插值对它是一个不透明表达式。所以「误用进 SELECT 投影且没给别名」这一形没有机器门 ——
|
|
69
|
+
* 误用的落点是真库上当场的 `SqlJsonColumnError("not-text")` 响亮抛(行上没有那个键 ⇒ `undefined`),不是静默。
|
|
49
70
|
*/
|
|
50
71
|
export declare function jsonColumnTextExpr(dialect: SqlDialect, column: string): string;
|
|
51
72
|
/**
|
|
52
|
-
* 一次读回失败的**种别**(
|
|
73
|
+
* 一次读回失败的**种别**(店层码,**闭集**;运行期词表在这里,类型从它推导 —— 一张词表只有一个写者):
|
|
53
74
|
* · `not-text` —— 拿到的不是串 = **读腿漏了投影**,那是**码的问题不是数据的问题** ⇒ 响亮的程序错,
|
|
54
75
|
* 绝不冒充「你的库坏了」({@link foldJsonColumn} 因此**不折**这一种);
|
|
55
76
|
* · `not-json` —— 解不出 JSON(撕裂写 / 手改 / 非 JSON 字节);
|
|
56
77
|
* · `malformed` —— 解出来了,但该店的形读者不认(`null` 字面 / 字符串标量 / 双重编码 / 缺键错型)。
|
|
78
|
+
*
|
|
79
|
+
* 🔴 与 core `CORRUPT_BODY_FORMS`(`@contract store.read_back_shape_judged`)的**逐形对拍**写在
|
|
80
|
+
* `test/store-contract-kit.test.ts` 的契约句里,并由那里两格机器钉住(任一侧改词 ⇒ 红,回来重对)。
|
|
81
|
+
* 一句话版:core 那四形数的是**体的形**,本表数的是**承载**这一维 —— 两张表不是同一个坐标系,
|
|
82
|
+
* 所以不合并;本表的 `malformed` 是 core 三只体形词的共同落点,而 core 没有 `not-text` 的对应词
|
|
83
|
+
* (它没有 SELECT 投影这一维)。
|
|
57
84
|
*/
|
|
58
|
-
export
|
|
85
|
+
export declare const SQL_JSON_COLUMN_PROBLEMS: readonly ["not-text", "not-json", "malformed"];
|
|
86
|
+
export type SqlJsonColumnProblem = (typeof SQL_JSON_COLUMN_PROBLEMS)[number];
|
|
59
87
|
/**
|
|
60
88
|
* 店层码,**不上 wire**:各店在自己的错误映射里把它折成该店既有的 corrupt 类码(见 {@link foldJsonColumn})。
|
|
61
89
|
* 终局对外码经 7.92.2 的词法门,永不裸穿。
|
|
@@ -122,6 +150,21 @@ export declare function foldJsonColumn(e: unknown, corrupt: (message: string) =>
|
|
|
122
150
|
* 那会让一次代码漂 / 一次故障伪装成「几行数据坏了」,而两者的处置完全不同。
|
|
123
151
|
*/
|
|
124
152
|
export declare function isJsonColumnBodyProblem(e: unknown): e is SqlJsonColumnError;
|
|
153
|
+
/**
|
|
154
|
+
* **写腿的形闸** —— 一个值**序列化之后**还是不是这一列读得回来的形。判据与读腿**共用同一只形读者**,
|
|
155
|
+
* 所以「写得进」与「读得出」在构造上是同一句话。
|
|
156
|
+
*
|
|
157
|
+
* 🔴 为什么类型面不够(codex 对抗复审 [medium],验真后按类修):TS 的 `object` 说不出「序列化之后是一只
|
|
158
|
+
* JSON 对象」—— 数组 / `Date` / `toJSON()` 返回标量或 `null` 的对象**全部**通得过 `ctx: object`,却各自
|
|
159
|
+
* 写出一行读腿当场判坏的字节(`[1,2]` / `"1970-01-01T…"` / `null` / `7`;实测四形)。后果不是「这次写失败」
|
|
160
|
+
* 而是**这次写成功**:一行原本读得回来的耐久行被换成读不回来的字节,而调用方拿到的是 2xx。
|
|
161
|
+
* ⇒ 闸在**写点**,在落盘**之前**,方向 fail-closed:响亮拒,盘上那一行原样不动。
|
|
162
|
+
*
|
|
163
|
+
* @param text 该值按本列的编码器序列化后的文本(`toJSON` 已生效 —— 这正是类型面看不见的那一层)。
|
|
164
|
+
* @param read 这一列的形读者(**与读腿同一只**;两只不同就又是两份判据了)。
|
|
165
|
+
* @param where 坐标文案,同 {@link decodeJsonColumn}。
|
|
166
|
+
*/
|
|
167
|
+
export declare function assertJsonColumnWritable<T>(text: unknown, read: JsonColumnReader<T>, where: string): void;
|
|
125
168
|
/**
|
|
126
169
|
* 把一只**既有的 zod schema** 当形读者用(本仓多数店的形判据就是它自己那份 schema)。
|
|
127
170
|
* `create*` 而不是 `build*`:产物是一只函数([ref] N2 命名律)。
|
|
@@ -131,10 +174,18 @@ export declare function createZodColumnReader<T>(schema: z.ZodType<T>): JsonColu
|
|
|
131
174
|
* **载荷形属主不在本仓 / 本层**的那一族列的形读者 —— 判「这一格是不是一只 JSON 对象」,然后把它当成
|
|
132
175
|
* 该店的域型交出去。整仓**只有这一处**做这一跳断言。
|
|
133
176
|
*
|
|
134
|
-
* 🔴 什么时候用它(硬条件,别扩大)
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
177
|
+
* 🔴 什么时候用它(硬条件,别扩大):这一列的键表属主**不在本层**,而那一层**没有导出验证器**。
|
|
178
|
+
* 两族,判据同一条(S-577 把这段与本函数第一句对齐 —— 它此前只写了 core 那一族,于是第二族要么被迫
|
|
179
|
+
* 违注、要么被迫另造一只读者,而「加一只特例读者」在本仓是红旗):
|
|
180
|
+
* · 属主在 **core**:`Checkpoint` / `ResumeOutcome` / `GateOutcome` / `WorkflowRun` /
|
|
181
|
+
* `BackgroundAgentRecord` …;
|
|
182
|
+
* · 属主在**本仓的别一层**:`checkpoint_ctx.ctx` 的 `CheckpointCtx`(`http/route-ctx.ts`)—— 键表住在
|
|
183
|
+
* HTTP 层,`src/plugins/**` 按分层不值 import 它,而那一层今天也没有运行期验证器。
|
|
184
|
+
* 在店里手写一份等价 zod 就是设计稿明令禁止的**第二份键集**,它迟早与属主分叉,而分叉那天是「写得进、
|
|
185
|
+
* 读不出」。⚠️ 现成的键闭集门(`request-key-closure.ts` 的 `taskBodyKeyIssue`)**不能**拿来当这里的读判据:
|
|
186
|
+
* 它自己的纪律逐字写着「拒只在 FRESH 提交面,RESUME 重放走宽容层」—— 把它打在读腿上会把存量 parked 行
|
|
187
|
+
* 整族砖死,方向正好相反。
|
|
188
|
+
* 一列只要有**本层自己的** schema 属主,就必须用 {@link createZodColumnReader} —— 用本函数等于把形判据关小。
|
|
138
189
|
*
|
|
139
190
|
* 🔴 为什么是一只工厂而不是让每个店各写一遍 `typeof body !== "object"`:那是同一条判据的 N 份拷贝
|
|
140
191
|
* (S-550 车② 初版真的写了六份),而且每一份都要自己那一跳 `as`。收成一处之后,判据只有一个写者,
|
|
@@ -152,15 +203,43 @@ export declare const readJsonObjectColumn: JsonColumnReader<Record<string, unkno
|
|
|
152
203
|
/** 「一只普通 JSON 对象」的收窄谓词(`null` / 数组 / 标量都不是)。谓词而非 `as`:边界铸形门 [ref] §2。 */
|
|
153
204
|
export declare function isJsonObject(v: unknown): v is Record<string, unknown>;
|
|
154
205
|
/**
|
|
155
|
-
* **任意 JSON
|
|
206
|
+
* **任意 JSON 值**的形读者,**用在可空列上** —— 只把 `not-json` 挡下来,`null` / 标量 / 数组一律是值。
|
|
207
|
+
*
|
|
208
|
+
* 🔴 前置条件(**门④ 机器执法,不是散文**):承载列**可空**,而「缺席」在它上面由**列的 NULL** 单独表达
|
|
209
|
+
* (三态列);于是 JSON 字面 `null` 是一个与缺席**不同形**的合法值。用在 `NOT NULL` 列上当场红 —— 那种列
|
|
210
|
+
* 根本没有缺席态,`null` 字面与「没有这一行」在点读的消费点上逐字同答,而那正是 S-577 的病灶
|
|
211
|
+
* (`checkpoint_ctx.ctx`:`JSON NOT NULL` + 有结构的载荷,一整版都读回一个没人分得出来的 `null`)。
|
|
212
|
+
* NOT NULL 且契约不透明的列用 {@link readOpaqueJsonValueColumn};缺席落 SQL NULL 的用
|
|
213
|
+
* {@link readNonNullJsonValueColumn}(那一族把 `null` 字面判坏)。
|
|
214
|
+
*
|
|
215
|
+
* 之所以叫得出「三态」:范本是 `approval_ask.updated_input`([ref] / [ref])—— 列 NULL = 没带编辑,
|
|
216
|
+
* JSON 文本 `"null"` = **编辑成 null**,对象 = 编辑成那只对象;把 `"null"` 判成坏体会把一条合法的人类
|
|
217
|
+
* 编辑抹掉。这一句是**理由**,不是名单:哪几处在用它由门数,别在这里写计数(S-577 之前这里写着
|
|
218
|
+
* 「今天整仓只有一处符合」—— 那句话当时就是假的,而没有门看着的计数,下一个人加第二处时不会来改它)。
|
|
156
219
|
*
|
|
157
|
-
*
|
|
158
|
-
* 「这一格缺席」在该列上是两件不同的事、且由**列的 NULL** 区分。今天整仓只有一处符合:
|
|
159
|
-
* `approval_ask.updated_input`([ref] / [ref] 的三态读法 —— 列 NULL = 没带编辑,JSON 文本 `"null"` =
|
|
160
|
-
* **编辑成 null**;把 `"null"` 判成坏体会把一条合法的人类编辑抹掉)。
|
|
161
|
-
* 一列只要有结构,就**不许**用它 —— 那是把形判据静默关掉。
|
|
220
|
+
* 一列只要有结构,就**不许**用它 —— 那是把形判据静默关掉(这条是 S-577 的病灶判据,三只铸点同守)。
|
|
162
221
|
*/
|
|
163
|
-
export declare const
|
|
222
|
+
export declare const readTriStateJsonValueColumn: JsonColumnReader<unknown>;
|
|
223
|
+
/**
|
|
224
|
+
* **任意 JSON 值**的形读者,**用在 `NOT NULL` 列上** —— 同样只把 `not-json` 挡下来。
|
|
225
|
+
*
|
|
226
|
+
* 🔴 前置条件(门④ 机器执法,**反向**那一格):承载列 **NOT NULL**,所以这一列**根本没有缺席状态** ——
|
|
227
|
+
* 缺席 = 根本没有这一行 / 这一条。于是 JSON 字面 `null` 不与任何东西同形,它只是一个值。用在**可空**列上
|
|
228
|
+
* 当场红:那种列上「值是 `null`」与「这一格空着」立刻二义,要的是上面那两只之一。
|
|
229
|
+
*
|
|
230
|
+
* 🔴 还有一条前置条件门判不出来,必须亲读契约:这一列的载荷**契约上不透明、没有声明形**(本店不解释它,
|
|
231
|
+
* 只做传输中立的字节存取)。一列只要有声明形 / 有 schema 属主,就必须用 {@link createZodColumnReader}
|
|
232
|
+
* 或 {@link createObjectColumnReader} —— 用本函数等于把形判据关小,而「NOT NULL」这一条它自己是拦不住的。
|
|
233
|
+
* 范本:`agent_memory_engine_push_queue.payload`(`JSON`/`jsonb` NOT NULL,契约逐字是「传输中立包,本店
|
|
234
|
+
* 不解释」;读腿是 `rows.map` 的行字段,缺一条 = 数组里没这一行,`null` 与缺席**不同形**)。
|
|
235
|
+
*
|
|
236
|
+
* 🔴 **它与 {@link readTriStateJsonValueColumn} 的实现逐字相同,而且必须各自独立写出来** —— 不许收成
|
|
237
|
+
* `export const readOpaqueJsonValueColumn = readTriStateJsonValueColumn`(门④c2 会红)。两只的区别整个
|
|
238
|
+
* 落在**前置条件**上(一只要可空列、一只要 NOT NULL 列),而前置条件的载体就是**名字**:合成一个名字就等于
|
|
239
|
+
* 把两条互斥的前置条件重新揉回一起 —— 那正是 S-577 立案时那条散文析取的形。「去重」在这里去掉的不是重复,
|
|
240
|
+
* 是判据。
|
|
241
|
+
*/
|
|
242
|
+
export declare const readOpaqueJsonValueColumn: JsonColumnReader<unknown>;
|
|
164
243
|
/**
|
|
165
244
|
* **任意 JSON 值,但缺席由「列 NULL」表达**的列的形读者 —— `null` 字面判坏,其余一律是值。
|
|
166
245
|
*
|
|
@@ -168,10 +247,33 @@ export declare const readAnyJsonValueColumn: JsonColumnReader<unknown>;
|
|
|
168
247
|
* 写腿接的是 `unknown`(`PendingAction.args` / 账本行载荷 / leader 结果……),于是**数组 / 串 / 数 /
|
|
169
248
|
* 布尔实参都写得进去** —— 拿 {@link readJsonObjectColumn} 判它们就是「写腿收得下、读腿读不出」,而且
|
|
170
249
|
* 后果往往不是一行坏:`rows.map` 里一条这样的合法行会让整只读面抛。
|
|
171
|
-
* 与 {@link
|
|
172
|
-
* 统一落成 **SQL NULL**,所以列里的 `null` 字面按构造写不进来 ——
|
|
173
|
-
*
|
|
174
|
-
* {@link
|
|
250
|
+
* 与 {@link readTriStateJsonValueColumn} 的**唯一**区别是这一形(两只的承载列都必须**可空**):这族列的
|
|
251
|
+
* 写腿把缺席(`undefined` / `null`)统一落成 **SQL NULL**,所以列里的 `null` 字面按构造写不进来 —— 它只
|
|
252
|
+
* 可能来自手改 / 迁移脚本,判坏。用它之前先亲读写腿:缺席若**不是**落 SQL NULL
|
|
253
|
+
* (`approval_ask.updated_input` 的三态),用 {@link readTriStateJsonValueColumn};列若是 `NOT NULL`
|
|
254
|
+
* (根本没有缺席态),用 {@link readOpaqueJsonValueColumn}。
|
|
255
|
+
*
|
|
256
|
+
* 🔴 与 {@link readTriStateJsonValueColumn} 同属「承载列必须**可空**」这一族,所以同受门④ 约束
|
|
257
|
+
* (S-577):这族读者用在 `NOT NULL` 列上一律红 —— 列上没有 NULL 这个状态,「缺席」就无处安放,而写腿把
|
|
258
|
+
* 缺席落成 SQL NULL 这句话在那种列上根本执行不了。
|
|
175
259
|
*/
|
|
176
260
|
export declare const readNonNullJsonValueColumn: JsonColumnReader<unknown>;
|
|
261
|
+
/**
|
|
262
|
+
* **一只 JSON 数组**的承载形读者(内容判据归各列自己的既有属主,这里只判承载形)。
|
|
263
|
+
*
|
|
264
|
+
* 🔴 为什么在属主里而不是各店各写一遍(S-550 车③a 把车② 的店内私有形上收):今天有三个消费方
|
|
265
|
+
* (checkpoint 的 `rule_suggestions` / `pending_steer_queue`、memory-engine 的 `embedding`),判据逐字
|
|
266
|
+
* 相同,而「这一格是不是一只数组」恰好是坏体四形的落点 —— 三份拷贝迟早有一份忘了判 `null` 字面。
|
|
267
|
+
*/
|
|
268
|
+
export declare const readJsonArrayColumn: JsonColumnReader<unknown[]>;
|
|
269
|
+
/**
|
|
270
|
+
* **一只字符串数组**的形读者(`image_bake.argv` / `bands`、`sandbox_image_index.bands`、
|
|
271
|
+
* `mailbox_message.hop_chain`)。
|
|
272
|
+
*
|
|
273
|
+
* 🔴 判到元素形是**必须**的,不是洁癖:mailbox 的 hop 链元素参与 core 的 `hop_loop` **等值比对**,
|
|
274
|
+
* 一条混进数字/对象的链会让准入判决改错;`argv` 是要交给构建器的命令行。四形坏体全落在这条判据外面。
|
|
275
|
+
*/
|
|
276
|
+
export declare const readStringArrayColumn: JsonColumnReader<string[]>;
|
|
277
|
+
/** **串→串的映射**(`sandbox_image_index.toolchain_versions`、`agent_memory_engine_sync_cursor.base_revs`)。 */
|
|
278
|
+
export declare const readStringRecordColumn: JsonColumnReader<Record<string, string>>;
|
|
177
279
|
//# sourceMappingURL=sql-json-column.d.ts.map
|
|
@@ -9,6 +9,7 @@ export function jsonColumnTextExpr(dialect, column) {
|
|
|
9
9
|
}
|
|
10
10
|
return dialect === "pg" ? `${column}::text` : `CAST(${column} AS CHAR)`;
|
|
11
11
|
}
|
|
12
|
+
export const SQL_JSON_COLUMN_PROBLEMS = ["not-text", "not-json", "malformed"];
|
|
12
13
|
export class SqlJsonColumnError extends Error {
|
|
13
14
|
problem;
|
|
14
15
|
where;
|
|
@@ -56,6 +57,9 @@ export function foldJsonColumn(e, corrupt) {
|
|
|
56
57
|
export function isJsonColumnBodyProblem(e) {
|
|
57
58
|
return e instanceof SqlJsonColumnError && e.problem !== "not-text";
|
|
58
59
|
}
|
|
60
|
+
export function assertJsonColumnWritable(text, read, where) {
|
|
61
|
+
decodeJsonColumn(text, read, where);
|
|
62
|
+
}
|
|
59
63
|
export function createZodColumnReader(schema) {
|
|
60
64
|
return (body) => {
|
|
61
65
|
const parsed = schema.safeParse(body);
|
|
@@ -69,6 +73,23 @@ export const readJsonObjectColumn = (body) => isJsonObject(body) ? { value: body
|
|
|
69
73
|
export function isJsonObject(v) {
|
|
70
74
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
71
75
|
}
|
|
72
|
-
export const
|
|
76
|
+
export const readTriStateJsonValueColumn = (body) => ({ value: body });
|
|
77
|
+
export const readOpaqueJsonValueColumn = (body) => ({ value: body });
|
|
73
78
|
export const readNonNullJsonValueColumn = (body) => body === null ? { problem: "the JSON literal `null` — absence is SQL NULL on this column, so a written `null` can only be a hand edit" } : { value: body };
|
|
79
|
+
export const readJsonArrayColumn = (body) => Array.isArray(body) ? { value: body } : { problem: "expected a JSON array" };
|
|
80
|
+
export const readStringArrayColumn = (body) => {
|
|
81
|
+
if (!Array.isArray(body))
|
|
82
|
+
return { problem: "expected a JSON array of strings" };
|
|
83
|
+
const bad = body.findIndex((x) => typeof x !== "string");
|
|
84
|
+
return bad === -1 ? { value: body } : { problem: `element ${bad} of the array is not a string` };
|
|
85
|
+
};
|
|
86
|
+
export const readStringRecordColumn = (body) => {
|
|
87
|
+
if (!isJsonObject(body))
|
|
88
|
+
return { problem: "expected a JSON object of string values" };
|
|
89
|
+
for (const [k, v] of Object.entries(body)) {
|
|
90
|
+
if (typeof v !== "string")
|
|
91
|
+
return { problem: `key ${JSON.stringify(k)} does not carry a string value` };
|
|
92
|
+
}
|
|
93
|
+
return { value: body };
|
|
94
|
+
};
|
|
74
95
|
//# sourceMappingURL=sql-json-column.js.map
|
|
@@ -2,24 +2,18 @@
|
|
|
2
2
|
* Shared pure row-mapping helpers for the SQL store twins ([ref] S8 纯 helper 归位).
|
|
3
3
|
*
|
|
4
4
|
* These were duplicated verbatim across the pg-/tidb- store twin pairs; each store imports the flavor
|
|
5
|
-
* it was written against
|
|
6
|
-
* parseJson`) so call sites and semantics are byte-identical to before.
|
|
5
|
+
* it was written against so call sites and semantics are byte-identical to before.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* 🔴 **JSON 列的解析不在这里,也永远不会回来**(S-550):本文件曾住着三只 `parseJson*`
|
|
8
|
+
* (lenient → `null` / strict → 抛 / fallback → 兜底),三种失败方向由**调用点自选** —— 那正是 S-550
|
|
9
|
+
* 要消灭的现状:同一格读不出的 JSON,在一个店里是「空体」、在另一个店里是「这一行不存在」、在第三个
|
|
10
|
+
* 店里是异常。三只在车③b **删除**(零别名、零转出),整仓唯一属主 = `sql-json-column.ts`
|
|
11
|
+
* (按文本投影 · 恰好解一次码 · 坏体一律拒),容忍写在店的调用点上(显式 `try/catch` + `recordFailOpen`)。
|
|
12
|
+
* 机器执法:`test/sql-json-column-single-owner.test.ts` 门① 对这三个**标识符**全仓零容忍。
|
|
13
|
+
*
|
|
14
|
+
* 剩下的两只 nullable date→ISO helper 仍然是两只:它们对一格非 Date 的渲染**刻意不同**
|
|
15
|
+
* (`String()` 原样透出 vs `new Date()` 重解析),不许合并。
|
|
12
16
|
*/
|
|
13
|
-
/** JSON column → value; a corrupted/legacy JSON column reads back as `null` instead of failing the
|
|
14
|
-
* read. Non-strings pass through (node-pg parses jsonb → object already). */
|
|
15
|
-
export declare function parseJsonLenient<T>(v: unknown): T | null;
|
|
16
|
-
/** JSON column → value; malformed JSON THROWS. For stores whose write arm keeps the column clean
|
|
17
|
-
* (R7 终形:协议面拒绝式,库内恒干净,直读). Non-strings pass through (node-pg parses jsonb →
|
|
18
|
-
* object already). */
|
|
19
|
-
export declare function parseJsonStrict<T>(v: unknown): T | null;
|
|
20
|
-
/** JSON column → value with a caller-supplied fallback for NULL/corrupted cells. Non-strings pass
|
|
21
|
-
* through (node-pg parses jsonb → object already; tolerate a string just in case). */
|
|
22
|
-
export declare function parseJsonOr<T>(v: unknown, fallback: T): T;
|
|
23
17
|
/** NOT NULL datetime column → ISO string (drivers return Date; a string cell passes through via
|
|
24
18
|
* String() — NOT re-parsed). */
|
|
25
19
|
export declare function toIso(v: unknown): string;
|
|
@@ -1,32 +1,3 @@
|
|
|
1
|
-
export function parseJsonLenient(v) {
|
|
2
|
-
if (v == null)
|
|
3
|
-
return null;
|
|
4
|
-
if (typeof v !== "string")
|
|
5
|
-
return v;
|
|
6
|
-
try {
|
|
7
|
-
return JSON.parse(v);
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
export function parseJsonStrict(v) {
|
|
14
|
-
if (v == null)
|
|
15
|
-
return null;
|
|
16
|
-
return (typeof v === "string" ? JSON.parse(v) : v);
|
|
17
|
-
}
|
|
18
|
-
export function parseJsonOr(v, fallback) {
|
|
19
|
-
if (v == null)
|
|
20
|
-
return fallback;
|
|
21
|
-
if (typeof v !== "string")
|
|
22
|
-
return v;
|
|
23
|
-
try {
|
|
24
|
-
return JSON.parse(v);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return fallback;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
1
|
export function toIso(v) {
|
|
31
2
|
return v instanceof Date ? v.toISOString() : String(v);
|
|
32
3
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
+
import { buildDurableAbsenceNote, createCorruptReadSeat } from "../observability/corrupt-read-seat.js";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { AdoptionError, FileStorageBackend, FileSessionRepo } from "@sema-agent/core";
|
|
4
5
|
import { FileWorkflowJournalStore } from "@sema-agent/core";
|
|
@@ -213,23 +214,16 @@ class LocalBackend {
|
|
|
213
214
|
outcomeSinkInst;
|
|
214
215
|
onWarn;
|
|
215
216
|
onError;
|
|
217
|
+
discloseWarn;
|
|
216
218
|
constructor(root, config, onWarn, onError) {
|
|
217
219
|
this.onWarn = onWarn;
|
|
220
|
+
this.discloseWarn = onWarn ?? ((msg, meta) => console.warn(msg, meta));
|
|
218
221
|
this.onError = onError;
|
|
219
222
|
try {
|
|
220
223
|
this.fileBackend = acquireLocalBackendWithWait(() => new FileStorageBackend({
|
|
221
224
|
root,
|
|
222
225
|
...(config.fileHistory !== undefined ? { fileHistory: config.fileHistory } : {}),
|
|
223
|
-
onCorruptRead: (
|
|
224
|
-
path: info.path,
|
|
225
|
-
reason: info.reason,
|
|
226
|
-
...(info.sessionId !== undefined ? { sessionId: info.sessionId } : {}),
|
|
227
|
-
...(info.principal !== undefined ? { principal: info.principal } : {}),
|
|
228
|
-
note: "a durable read was treated as ABSENT because the bytes were unreadable (never a plain ENOENT). " +
|
|
229
|
-
"Consequence depends on which store read it: session-policy = that run lost its SUBTRACTIVE session-rule " +
|
|
230
|
-
"layer (deployment policy / hooks / shell gate still applied); session repo = a listing silently omitted " +
|
|
231
|
-
"rows; file-history = a rewind boundary or scope read as missing. Inspect the named path before deciding.",
|
|
232
|
-
}),
|
|
226
|
+
onCorruptRead: createCorruptReadSeat((msg, fields) => this.discloseWarn(msg, fields), "file_store_corrupt_read", buildDurableAbsenceNote("file-history = a rewind boundary or scope read as missing")),
|
|
233
227
|
}), config.storeLockWaitMs, (fields) => this.onWarn?.("store_dir_lock_wait", { ...fields }));
|
|
234
228
|
}
|
|
235
229
|
catch (e) {
|
|
@@ -239,7 +233,9 @@ class LocalBackend {
|
|
|
239
233
|
throw new Error(`DB_BACKEND=local cannot open data dir ${root}: ${e instanceof Error ? e.message : String(e)}` +
|
|
240
234
|
(tail === undefined ? "" : ` ${tail}`));
|
|
241
235
|
}
|
|
242
|
-
this.sessionStore = new LocalSessionStore(new FileSessionRepo(this.fileBackend.root
|
|
236
|
+
this.sessionStore = new LocalSessionStore(new FileSessionRepo(this.fileBackend.root, {
|
|
237
|
+
onCorruptRead: createCorruptReadSeat((msg, fields) => this.discloseWarn(msg, fields), "session_repo_corrupt_read"),
|
|
238
|
+
}), this.fileBackend.root);
|
|
243
239
|
this.runStore = new FileRunStore(this.fileBackend.root, {
|
|
244
240
|
error: (msg, fields) => void (this.onError ?? this.onWarn)?.(msg, fields),
|
|
245
241
|
strictHydrate: config.runStoreStrictHydrate,
|
|
@@ -249,7 +245,9 @@ class LocalBackend {
|
|
|
249
245
|
this.approvalNonceStore = new FileApprovalNonceStore(this.fileBackend.root);
|
|
250
246
|
this.approvalAskStore = new FileApprovalAskStore(this.fileBackend.root);
|
|
251
247
|
this.sendFileLedgerStore = new FileSendFileLedger(this.fileBackend.root);
|
|
252
|
-
this.workflowJournalStore = new FileWorkflowJournalStore(this.fileBackend.root
|
|
248
|
+
this.workflowJournalStore = new FileWorkflowJournalStore(this.fileBackend.root, undefined, {
|
|
249
|
+
onCorruptRead: createCorruptReadSeat((msg, fields) => this.discloseWarn(msg, fields), "workflow_journal_corrupt_read"),
|
|
250
|
+
});
|
|
253
251
|
this.outcomeSinkInst = new FileOutcomeSink(this.fileBackend.root);
|
|
254
252
|
const ownControlDir = fileMemoryControlDirFor(config);
|
|
255
253
|
this.captureRecordsSource = ownControlDir === undefined
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* Rules for this file:
|
|
10
10
|
* - ZERO dialect dependencies (no mysql2, no pg) — types + pure functions only. The only imports
|
|
11
11
|
* allowed are core contract types and the neutral sql-row-helpers leaf.
|
|
12
|
+
* (S-550 车③a:`mapBakeRow`/`mapImageRow`/`BAKE_SELECT_COLS` 搬回了它们**唯一**的消费者
|
|
13
|
+
* `image-bake-store-sql.ts` / `image-index-sql.ts` —— 「两个 SQL 孪生共用」那条理由在孪生合成
|
|
14
|
+
* 单文件双方言之后就不存在了,而一条 SELECT 的**列清单**必须和它的**投影铸点**、**解码点**长在
|
|
15
|
+
* 同一个文件里:否则 JSON 承载列的投影门(它按「这个文件查了哪张表」定列集)对那份清单是瞎的。)
|
|
12
16
|
* - Adapters import their contracts from HERE; no adapter imports its twin (store-backend.ts, the
|
|
13
17
|
* composition root, is the one place that names every concrete adapter).
|
|
14
18
|
* - The TiDB adapters re-export these names for back-compat (the historical import surface), but new
|
|
@@ -447,10 +451,6 @@ export interface BakeRow {
|
|
|
447
451
|
created_at: Date | string;
|
|
448
452
|
updated_at: Date | string;
|
|
449
453
|
}
|
|
450
|
-
/** SELECT row → {@link BakeRecord} — shared by BOTH SQL twins ([ref] S8 归位, A3 沉底). */
|
|
451
|
-
export declare function mapBakeRow(r: BakeRow): BakeRecord;
|
|
452
|
-
/** Column list mapBakeRow expects — shared by BOTH SQL twins ([ref] S8 归位, A3 沉底). */
|
|
453
|
-
export declare const BAKE_SELECT_COLS: string;
|
|
454
454
|
/** Nested-build security posture (the operator console shows rootless vs privileged-dind). */
|
|
455
455
|
export type ImageNestedBuildMode = "none" | "docker-cli-only" | "rootless-buildkit" | "privileged-dind";
|
|
456
456
|
export interface ImageCapabilities {
|
|
@@ -540,6 +540,4 @@ export interface ImageRow {
|
|
|
540
540
|
created_at: Date | string;
|
|
541
541
|
updated_at: Date | string;
|
|
542
542
|
}
|
|
543
|
-
/** SELECT row → {@link ImageIndexEntry} — shared by BOTH SQL twins ([ref] S8 归位, A3 沉底). */
|
|
544
|
-
export declare function mapImageRow(r: ImageRow): ImageIndexEntry;
|
|
545
543
|
//# sourceMappingURL=store-contracts.d.ts.map
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parseJsonOr, toIso as iso, normalizeIsoOrNull as isoOrNull } from "./sql-row-helpers.js";
|
|
2
1
|
export class StoreBootRefusalError extends Error {
|
|
3
2
|
}
|
|
4
3
|
export const RUN_STATUS_CLASS = {
|
|
@@ -36,63 +35,4 @@ export function sessionTempoOfRunStatus(lastStatus) {
|
|
|
36
35
|
export function runProvenElsewhere(run, thisInstanceId) {
|
|
37
36
|
return typeof run.instanceId === "string" && run.instanceId.length > 0 && thisInstanceId !== undefined && run.instanceId !== thisInstanceId;
|
|
38
37
|
}
|
|
39
|
-
export function mapBakeRow(r) {
|
|
40
|
-
return {
|
|
41
|
-
bakeId: r.bake_id,
|
|
42
|
-
profile: r.profile,
|
|
43
|
-
bands: r.bands == null ? null : parseJsonOr(r.bands, []),
|
|
44
|
-
baseRef: r.base_ref,
|
|
45
|
-
push: !!r.push,
|
|
46
|
-
dryRun: !!r.dry_run,
|
|
47
|
-
logs: !!r.logs,
|
|
48
|
-
argv: parseJsonOr(r.argv, []),
|
|
49
|
-
status: r.status,
|
|
50
|
-
state: r.state ?? null,
|
|
51
|
-
digest: r.digest,
|
|
52
|
-
repo: r.repo,
|
|
53
|
-
ref: r.ref,
|
|
54
|
-
indexId: r.index_id,
|
|
55
|
-
exitCode: r.exit_code === null ? null : Number(r.exit_code),
|
|
56
|
-
error: r.error,
|
|
57
|
-
errorCode: r.error_code ?? null,
|
|
58
|
-
manifestSha: r.manifest_sha,
|
|
59
|
-
tag: r.tag,
|
|
60
|
-
idemKey: r.idem_key,
|
|
61
|
-
ingestSecret: r.ingest_secret,
|
|
62
|
-
runnerId: r.runner_id,
|
|
63
|
-
leaseUntil: isoOrNull(r.lease_until),
|
|
64
|
-
cancelRequested: !!r.cancel_requested,
|
|
65
|
-
requestedBy: r.requested_by,
|
|
66
|
-
createdAt: iso(r.created_at),
|
|
67
|
-
updatedAt: iso(r.updated_at),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
export const BAKE_SELECT_COLS = "bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, state, digest, repo, ref, index_id, " +
|
|
71
|
-
"exit_code, error, error_code, manifest_sha, tag, idem_key, ingest_secret, runner_id, lease_until, " +
|
|
72
|
-
"cancel_requested, requested_by, created_at, updated_at";
|
|
73
|
-
export function mapImageRow(r) {
|
|
74
|
-
return {
|
|
75
|
-
id: r.id,
|
|
76
|
-
profile: r.profile,
|
|
77
|
-
bands: parseJsonOr(r.bands, []),
|
|
78
|
-
repo: r.repo,
|
|
79
|
-
tag: r.tag,
|
|
80
|
-
digest: r.digest,
|
|
81
|
-
toolchainVersions: parseJsonOr(r.toolchain_versions, {}),
|
|
82
|
-
capabilities: parseJsonOr(r.capabilities, {}),
|
|
83
|
-
podContract: parseJsonOr(r.pod_contract, {}),
|
|
84
|
-
sizeBytes: r.size_bytes === null ? null : Number(r.size_bytes),
|
|
85
|
-
status: r.status,
|
|
86
|
-
visibility: r.visibility,
|
|
87
|
-
tenantId: r.tenant_id,
|
|
88
|
-
manifestSha: r.manifest_sha,
|
|
89
|
-
recipeGitSha: r.recipe_git_sha,
|
|
90
|
-
generatorVersion: r.generator_version,
|
|
91
|
-
buildDate: isoOrNull(r.build_date),
|
|
92
|
-
supersedes: r.supersedes,
|
|
93
|
-
signed: !!r.signed,
|
|
94
|
-
createdAt: isoOrNull(r.created_at) ?? new Date(0).toISOString(),
|
|
95
|
-
updatedAt: isoOrNull(r.updated_at) ?? new Date(0).toISOString(),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
38
|
//# sourceMappingURL=store-contracts.js.map
|
|
@@ -24,21 +24,30 @@
|
|
|
24
24
|
* - 绑定:`bind` 把行钉到 sessionId(重复绑定=改绑,同 owner 内 last-wins;同 session 多 task 共享
|
|
25
25
|
* 自然成立——listBySession 是物化输入)。未绑定行 = 上传后从未被任何 task 引用 ⇒ reapUnbound(TTL)。
|
|
26
26
|
*/
|
|
27
|
+
import { z } from "zod";
|
|
27
28
|
import type { Pool } from "mysql2/promise";
|
|
28
29
|
import type { PgQueryFn } from "./pg-query.js";
|
|
29
30
|
import { type IndexSpec } from "./ensure-index.js";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
/**
|
|
32
|
+
* 一条附件 meta 的**形属主**(整仓唯一那份键集)。
|
|
33
|
+
*
|
|
34
|
+
* 🔴 为什么是一只 schema 而不只是一个 interface(S-550 车③b):SQL 车道把这些字段拆成**标量列**,
|
|
35
|
+
* 形由 DDL 管;local 车道把整条记录**序列化成一个 JSON 档**(`attachments/<id>.json`),于是它需要一个
|
|
36
|
+
* 运行期的形判据 —— 而那个判据必须与这里的键集是**同一份**(第二份键集分叉的那天 = 「写得进、读不出」)。
|
|
37
|
+
* 所以类型从 schema 推导({@link TaskAttachmentRecord}),两者按构造不可能漂。
|
|
38
|
+
*/
|
|
39
|
+
export declare const TaskAttachmentRecordSchema: z.ZodObject<{
|
|
40
|
+
id: z.ZodString;
|
|
41
|
+
owner: z.ZodString;
|
|
42
|
+
name: z.ZodString;
|
|
43
|
+
mime: z.ZodString;
|
|
44
|
+
sha256: z.ZodString;
|
|
45
|
+
sizeBytes: z.ZodNumber;
|
|
46
|
+
createdAtMs: z.ZodNumber;
|
|
47
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
48
|
+
boundAtMs: z.ZodOptional<z.ZodNumber>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
export type TaskAttachmentRecord = z.infer<typeof TaskAttachmentRecordSchema>;
|
|
42
51
|
export interface TaskAttachmentStore {
|
|
43
52
|
put(rec: TaskAttachmentRecord & {
|
|
44
53
|
content: Uint8Array;
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { ensureIndexes } from "./ensure-index.js";
|
|
3
|
+
export const TaskAttachmentRecordSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
owner: z.string(),
|
|
6
|
+
name: z.string(),
|
|
7
|
+
mime: z.string(),
|
|
8
|
+
sha256: z.string(),
|
|
9
|
+
sizeBytes: z.number(),
|
|
10
|
+
createdAtMs: z.number(),
|
|
11
|
+
sessionId: z.string().optional(),
|
|
12
|
+
boundAtMs: z.number().optional(),
|
|
13
|
+
});
|
|
2
14
|
export const TASK_ATTACHMENT_TABLE = "task_attachment";
|
|
3
15
|
export function materializedRelPaths(atts) {
|
|
4
16
|
const used = new Set();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BaseSessionStorage, type PromptEpochDescriptor, type SessionMetadata, type SessionTreeEntry, type SessionWriteOptions } from "@sema-agent/core";
|
|
2
2
|
import type { Pool } from "mysql2/promise";
|
|
3
3
|
export declare const SESSION_EVENT_LATEST_BY_TYPE_SQL: {
|
|
4
|
-
readonly floor:
|
|
5
|
-
readonly epochAnchor:
|
|
4
|
+
readonly floor: `SELECT /*+ USE_INDEX(session_event, idx_session_event_type) */ ${string} FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1`;
|
|
5
|
+
readonly epochAnchor: `SELECT /*+ USE_INDEX(session_event, idx_session_event_type) */ seq, ${string} FROM session_event WHERE session_id = ? AND seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100`;
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
8
|
* TiDB-backed {@link BaseSessionStorage} — the durable L1 event log for one session.
|
|
@@ -3,10 +3,12 @@ import { BaseSessionStorage, SessionError, leafIdAfterEntry, normalizePromptEpoc
|
|
|
3
3
|
import { toIso } from "./sql-row-helpers.js";
|
|
4
4
|
import { isMysqlDupKeyError } from "./sql-errors.js";
|
|
5
5
|
import { PLACEMENT_SELECT_COLUMNS, placementFromRow } from "./session-placement.js";
|
|
6
|
+
import { decodeSessionEventPayload } from "./session-event-payload.js";
|
|
7
|
+
import { jsonColumnText } from "./sql-json-column.js";
|
|
6
8
|
const SESSION_EVENT_BY_TYPE_HINT = "/*+ USE_INDEX(session_event, idx_session_event_type) */";
|
|
7
9
|
export const SESSION_EVENT_LATEST_BY_TYPE_SQL = {
|
|
8
|
-
floor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} payload FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1`,
|
|
9
|
-
epochAnchor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} seq, payload FROM session_event WHERE session_id = ? AND seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100`,
|
|
10
|
+
floor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1`,
|
|
11
|
+
epochAnchor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} seq, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100`,
|
|
10
12
|
};
|
|
11
13
|
const isDupKey = isMysqlDupKeyError;
|
|
12
14
|
export class TiDBSessionStorage extends BaseSessionStorage {
|
|
@@ -28,7 +30,7 @@ export class TiDBSessionStorage extends BaseSessionStorage {
|
|
|
28
30
|
if (rows.length === 0)
|
|
29
31
|
return undefined;
|
|
30
32
|
for (const r of rows) {
|
|
31
|
-
const e = (
|
|
33
|
+
const e = decodeSessionEventPayload(r.payload, this.sessionId, r.seq);
|
|
32
34
|
const d = e.type === "prompt_epoch" ? normalizePromptEpoch(e) : normalizePromptEpoch(e.details?.promptEpoch);
|
|
33
35
|
if (d)
|
|
34
36
|
return d;
|
|
@@ -42,8 +44,8 @@ export class TiDBSessionStorage extends BaseSessionStorage {
|
|
|
42
44
|
if (!meta)
|
|
43
45
|
return null;
|
|
44
46
|
const { floorEntryId, floorSeq } = await TiDBSessionStorage.computeFloor(pool, sessionId);
|
|
45
|
-
const [eventRows] = await pool.query(
|
|
46
|
-
const entries = eventRows.map((r) =>
|
|
47
|
+
const [eventRows] = await pool.query(`SELECT seq, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND seq >= ? ORDER BY seq ASC`, [sessionId, floorSeq]);
|
|
48
|
+
const entries = eventRows.map((r) => decodeSessionEventPayload(r.payload, sessionId, r.seq));
|
|
47
49
|
const placement = placementFromRow(meta);
|
|
48
50
|
const metadata = { id: sessionId, createdAt: toIso(meta.created_at), ...(placement !== undefined ? { placement } : {}) };
|
|
49
51
|
return new TiDBSessionStorage(pool, sessionId, metadata, entries, meta.leaf_id ?? null, floorEntryId, floorSeq);
|
|
@@ -52,8 +54,8 @@ export class TiDBSessionStorage extends BaseSessionStorage {
|
|
|
52
54
|
const [compRows] = await pool.query(SESSION_EVENT_LATEST_BY_TYPE_SQL.floor, [sessionId]);
|
|
53
55
|
if (!compRows[0])
|
|
54
56
|
return { floorEntryId: null, floorSeq: 0 };
|
|
55
|
-
const comp = (
|
|
56
|
-
const fk = comp.firstKeptEntryId;
|
|
57
|
+
const comp = decodeSessionEventPayload(compRows[0].payload, sessionId, "latest compaction");
|
|
58
|
+
const fk = comp.type === "compaction" ? comp.firstKeptEntryId : undefined;
|
|
57
59
|
if (!fk)
|
|
58
60
|
return { floorEntryId: null, floorSeq: 0 };
|
|
59
61
|
const [seqRows] = await pool.query("SELECT seq FROM session_event WHERE session_id = ? AND entry_id = ?", [sessionId, fk]);
|
|
@@ -65,11 +67,10 @@ export class TiDBSessionStorage extends BaseSessionStorage {
|
|
|
65
67
|
const inMem = await super.getEntry(id);
|
|
66
68
|
if (inMem)
|
|
67
69
|
return inMem;
|
|
68
|
-
const [rows] = await this.pool.query(
|
|
69
|
-
|
|
70
|
-
if (p == null)
|
|
70
|
+
const [rows] = await this.pool.query(`SELECT ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND entry_id = ?`, [this.sessionId, id]);
|
|
71
|
+
if (rows[0] === undefined)
|
|
71
72
|
return undefined;
|
|
72
|
-
return (
|
|
73
|
+
return decodeSessionEventPayload(rows[0].payload, this.sessionId, id);
|
|
73
74
|
}
|
|
74
75
|
async appendEntry(entry, opts) {
|
|
75
76
|
await this.persist(entry, opts);
|
|
@@ -155,8 +155,8 @@ export declare class TiDBSessionStore implements SessionStore {
|
|
|
155
155
|
*
|
|
156
156
|
* Resolves to `null` (NOT an empty async-iterable) when the session does not exist — checked via `session_meta`
|
|
157
157
|
* EAGERLY here (before returning the iterable), exactly like {@link exportEntries}, so a missing session is distinct
|
|
158
|
-
* from a real-but-empty one (an empty stream). payload is
|
|
159
|
-
*
|
|
158
|
+
* from a real-but-empty one (an empty stream). payload is decoded like {@link exportEntries}
|
|
159
|
+
* (按文本投影 + `decodeSessionEventPayload` 恰好解一次码,坏体当场拒). The check is eager (the method is async) because a generator body is lazy — it can't decide
|
|
160
160
|
* `null` vs. iterable on its own first pull.
|
|
161
161
|
*/
|
|
162
162
|
exportEntriesStream(sessionId: string, opts?: {
|