@sema-agent/server 7.4.0 → 7.6.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 +80 -1
- package/dist/approval-card.d.ts +15 -3
- package/dist/approval-card.js +41 -7
- package/dist/approval-reconciler.d.ts +109 -12
- package/dist/approval-reconciler.js +152 -24
- package/dist/boot/config-center.js +15 -2
- package/dist/boot/coordinators.js +10 -2
- package/dist/boot/execution-env.js +1 -1
- package/dist/boot/org-memory.d.ts +6 -0
- package/dist/boot/org-memory.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/reapers.d.ts +2 -0
- package/dist/boot/reapers.js +11 -4
- package/dist/boot/resolve-spec.d.ts +3 -19
- package/dist/boot/resolve-spec.js +73 -67
- package/dist/boot/runner-deps.d.ts +23 -1
- package/dist/boot/runner-deps.js +8 -11
- package/dist/boot/workflow-orchestration.d.ts +8 -3
- package/dist/boot/workflow-orchestration.js +23 -1
- package/dist/budget.d.ts +1 -1
- package/dist/budget.js +1 -1
- package/dist/capabilities/repo-tools.d.ts +1 -1
- package/dist/capabilities/repo-tools.js +8 -2
- package/dist/config-center/apply-effective.js +33 -10
- package/dist/config-provider.d.ts +1 -0
- package/dist/config-provider.js +23 -3
- package/dist/config-types.d.ts +27 -9
- package/dist/config.d.ts +6 -1
- package/dist/config.js +61 -15
- package/dist/deployment-governance.d.ts +168 -0
- package/dist/deployment-governance.js +206 -0
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +3 -1
- package/dist/fleet/fleet-bus.d.ts +17 -2
- package/dist/fleet/fleet-bus.js +68 -3
- package/dist/fleet/fleet-terminal-window.d.ts +98 -0
- package/dist/fleet/fleet-terminal-window.js +316 -0
- package/dist/governance-ask-marks.d.ts +31 -0
- package/dist/governance-ask-marks.js +122 -0
- package/dist/hooks/hook-runner.d.ts +28 -0
- package/dist/hooks/hook-runner.js +149 -25
- package/dist/http/routes/approvals-assistant.js +6 -7
- package/dist/http/routes/diagnostics.js +10 -5
- package/dist/http/routes/fleet.js +160 -14
- package/dist/http/routes/memory-policy.d.ts +2 -1
- package/dist/http/routes/memory-policy.js +77 -13
- package/dist/http/routes/runs.js +6 -2
- package/dist/http/routes/tasks.js +59 -22
- 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 +9 -0
- package/dist/http/server.js +28 -14
- package/dist/http/sse-log.js +3 -4
- package/dist/http/wire-types.d.ts +7 -2
- 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 +39 -31
- package/dist/observability/fail-open.d.ts +17 -2
- package/dist/observability/fail-open.js +19 -4
- package/dist/observability/prompt-manifest.d.ts +5 -1
- package/dist/orchestration/workflow-notify-journal.d.ts +58 -2
- package/dist/orchestration/workflow-notify-journal.js +130 -45
- package/dist/parked-decide.js +9 -4
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
- package/dist/plugins/approval-ask-store-memory.js +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +60 -5
- package/dist/plugins/approval-ask-store-sql.js +75 -35
- package/dist/plugins/background-agent-store-sql.js +16 -16
- package/dist/plugins/background-shell-support.d.ts +1 -1
- package/dist/plugins/background-shell-support.js +2 -2
- package/dist/plugins/breaker-state-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -8
- package/dist/plugins/checkpoint-store-sql.js +76 -13
- 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/local-checkpoint-store.d.ts +20 -1
- package/dist/plugins/local-checkpoint-store.js +19 -0
- package/dist/plugins/mailbox-store-sql.d.ts +4 -10
- package/dist/plugins/mailbox-store-sql.js +59 -6
- 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 +146 -5
- package/dist/runs.d.ts +11 -1
- package/dist/runs.js +18 -3
- package/dist/runtime-governance.d.ts +18 -0
- package/dist/runtime-governance.js +90 -3
- 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/task-settings.d.ts +3 -9
- package/dist/task-settings.js +16 -13
- package/dist/tool-approval.d.ts +33 -6
- package/dist/tool-approval.js +80 -23
- package/dist/trace/core-keyset-guard.d.ts +18 -4
- package/dist/trace/project.d.ts +10 -1
- package/dist/trace/project.js +31 -0
- package/package.json +3 -3
- package/dist/boot/lexical-path-env.d.ts +0 -10
- package/dist/boot/lexical-path-env.js +0 -88
- package/dist/capabilities/oa-tools.d.ts +0 -15
- package/dist/capabilities/oa-tools.js +0 -54
- package/dist/finance/cost-taxonomy.d.ts +0 -34
- package/dist/finance/cost-taxonomy.js +0 -26
- package/dist/plugins/approval-store-sql.d.ts +0 -116
- package/dist/plugins/approval-store-sql.js +0 -151
- package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
- package/dist/plugins/file-workflow-journal-store.js +0 -12
- package/dist/plugins/pg-approval-store.d.ts +0 -9
- package/dist/plugins/pg-approval-store.js +0 -9
- package/dist/plugins/pg-breaker-state.d.ts +0 -8
- package/dist/plugins/pg-breaker-state.js +0 -8
- package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
- package/dist/plugins/pg-checkpoint-store.js +0 -10
- package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/pg-file-snapshot-store.js +0 -8
- package/dist/plugins/pg-image-bake.d.ts +0 -12
- package/dist/plugins/pg-image-bake.js +0 -11
- package/dist/plugins/pg-image-index.d.ts +0 -12
- package/dist/plugins/pg-image-index.js +0 -11
- package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
- package/dist/plugins/pg-outcome-ledger.js +0 -11
- package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/pg-resume-anchor-store.js +0 -7
- package/dist/plugins/pg-run-store.d.ts +0 -9
- package/dist/plugins/pg-run-store.js +0 -9
- package/dist/plugins/pg-session-policy-store.d.ts +0 -7
- package/dist/plugins/pg-session-policy-store.js +0 -7
- package/dist/plugins/pg-session-store.d.ts +0 -12
- package/dist/plugins/pg-session-store.js +0 -12
- package/dist/plugins/pg-tool-result-store.d.ts +0 -9
- package/dist/plugins/pg-tool-result-store.js +0 -9
- package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-journal-store.js +0 -9
- package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-run-store.js +0 -9
- package/dist/plugins/tidb-approval-store.d.ts +0 -8
- package/dist/plugins/tidb-approval-store.js +0 -8
- package/dist/plugins/tidb-breaker-state.d.ts +0 -7
- package/dist/plugins/tidb-breaker-state.js +0 -7
- package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
- package/dist/plugins/tidb-checkpoint-store.js +0 -9
- package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/tidb-file-snapshot-store.js +0 -8
- package/dist/plugins/tidb-image-bake.d.ts +0 -12
- package/dist/plugins/tidb-image-bake.js +0 -11
- package/dist/plugins/tidb-image-index.d.ts +0 -12
- package/dist/plugins/tidb-image-index.js +0 -11
- package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
- package/dist/plugins/tidb-outcome-ledger.js +0 -12
- package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/tidb-resume-anchor-store.js +0 -7
- package/dist/plugins/tidb-run-store.d.ts +0 -10
- package/dist/plugins/tidb-run-store.js +0 -9
- package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
- package/dist/plugins/tidb-session-policy-store.js +0 -7
- package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
- package/dist/plugins/tidb-tool-result-store.js +0 -10
- package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/tidb-workflow-journal-store.js +0 -9
- package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
- package/dist/plugins/tidb-workflow-run-store.js +0 -10
- package/dist/plugins/workflow-journal-limits.d.ts +0 -12
- package/dist/plugins/workflow-journal-limits.js +0 -12
- package/dist/sema-registry.d.ts +0 -41
- package/dist/sema-registry.js +0 -40
|
@@ -71,13 +71,46 @@ export const APPROVAL_BATCHES_TABLE = "approval_batches";
|
|
|
71
71
|
*
|
|
72
72
|
* SCHEMA POLICY 同 `tidb-pool.ts` 头注:纯 CREATE,禁 ALTER 增量 seam,改列直接改这里 + 重建库
|
|
73
73
|
* (`docs/schema/baseline-mysql.sql` 是由这些文本机器录制出的**产物**,永不手改)。
|
|
74
|
+
*
|
|
75
|
+
* ── 列宽的**测量依据**(#192 A10;U2 逐列取证在档)──────────────────────────────────────────────────
|
|
76
|
+
* 本表家族原先一律 VARCHAR(255),是"先建表后想"的产物。按"每列的值到底由谁铸、有没有入口上限"重新裁:
|
|
77
|
+
*
|
|
78
|
+
* 收窄(有硬上限背书,证据即上限本身):
|
|
79
|
+
* · `ask_id` / `batch_id` / `bound_ask_id` → 64:`deriveAskId`/`deriveBatchId` 恒是 sha256 hex 取前
|
|
80
|
+
* 64 字符(`approval-ask-machine.ts` deterministicId),按构造不可能更长。
|
|
81
|
+
* · `task_id` → 64:入口 `UUIDV7_RE`(`security.ts:134`)只放行 36 字符的规范 uuidv7;与兄弟列
|
|
82
|
+
* `task_run.task_id VARCHAR(64)` 同宽(此前 255 是本家族独有的偏离,无依据)。
|
|
83
|
+
* · `session_id` → 64:提交入口硬拒 >64(`http/server.ts` "sessionId must be at most 64 characters"),
|
|
84
|
+
* 与 `session_meta`/`task_run` 同宽。
|
|
85
|
+
* · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts:350`)硬拒更长者;
|
|
86
|
+
* 全仓每一根 owner/scope 轴都是 190,此前 255 同样是无依据偏离。
|
|
87
|
+
* · `gate_token` → 120:它抄的是 checkpoint 的 token(core `mintCheckpointToken` = 16 字节 hex = 32 字符),
|
|
88
|
+
* 取与**被抄那一列** `checkpoint.token VARCHAR(120)` 同宽 —— 同一个值在两张表上宽度必须一致,
|
|
89
|
+
* 否则抄的那一步就是一道静默截断。512 是原先的 16 倍冗余。
|
|
90
|
+
*
|
|
91
|
+
* 🔑 上面四条收窄之所以**不引入任何新暴露面**,靠的不是入口断言而是一条更硬的事实:同一个 session_id /
|
|
92
|
+
* task_id / owner **早就**同时躺在 `session_meta.session_id VARCHAR(64)` / `task_run.task_id VARCHAR(64)` /
|
|
93
|
+
* `task_run.owner VARCHAR(190)` 里。任何长到能撑爆本表新宽度的值,在写到那几张**更中心**的表时就已经
|
|
94
|
+
* 先炸了 —— 本表此前的 255 从来不是一道额外的安全余量,只是一处与全仓不一致的偏离。
|
|
95
|
+
* ⇒ 收窄的风险上界 = 0;真正要担心的是反过来:留着 255 会让人误以为这里可以存更长的 id。
|
|
96
|
+
*
|
|
97
|
+
* 🔴 **刻意不收窄**(测量结果不支持,记在这里免得下一个人以为是漏了):
|
|
98
|
+
* · `source_task_id` / `tool_call_id` / `parent_tool_call_id` / `gate_bound_call_id` —— 这四列的值是
|
|
99
|
+
* **模型/引擎铸的原始 id**,服务端在写行这一步**没有任何长度断言**(`approval-card.ts` 的
|
|
100
|
+
* `MAX_IDENT` 只裁了给人看的那份 card 投影,不是本列)。没有入口上限就收窄 = 把"存不下"这件事
|
|
101
|
+
* 推迟到 INSERT 才炸(MySQL 更糟:静默截断),换来的只是几十字节。要收窄,先补入口断言
|
|
102
|
+
* (照 `assertPrincipalShape`/`assertIdempotencyKeyShape` 的姿势),那是另一件事。
|
|
103
|
+
* · `bound_input_hash` / `gate_bound_input_hash` —— 按约定是 sha256 hex(64),但 `readBoundInputHash`
|
|
104
|
+
* 只校验"非空字符串",没校长度/字形 ⇒ 同上,先有断言再谈收窄。
|
|
105
|
+
* · `idempotency_key` —— 255 **正是**入口断言的上限本身(zod `.max(255)` + 店内 `assertIdempotencyKeyShape`
|
|
106
|
+
* 双执法),宽度与上限同源,恰好正确,动它反而制造截断面。
|
|
74
107
|
*/
|
|
75
108
|
export const TIDB_APPROVAL_ASK_STATEMENTS = [
|
|
76
109
|
`CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCHES_TABLE} (
|
|
77
|
-
batch_id VARCHAR(
|
|
78
|
-
task_id VARCHAR(
|
|
110
|
+
batch_id VARCHAR(64) NOT NULL,
|
|
111
|
+
task_id VARCHAR(64) NOT NULL,
|
|
79
112
|
state VARCHAR(32) NOT NULL,
|
|
80
|
-
bound_ask_id VARCHAR(
|
|
113
|
+
bound_ask_id VARCHAR(64) NULL,
|
|
81
114
|
version INT NOT NULL DEFAULT 0,
|
|
82
115
|
created_at_ms BIGINT NOT NULL,
|
|
83
116
|
updated_at_ms BIGINT NOT NULL,
|
|
@@ -85,12 +118,12 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
|
|
|
85
118
|
KEY idx_batch_task (task_id)
|
|
86
119
|
) COLLATE utf8mb4_bin`,
|
|
87
120
|
`CREATE TABLE IF NOT EXISTS ${APPROVAL_ASKS_TABLE} (
|
|
88
|
-
ask_id VARCHAR(
|
|
89
|
-
task_id VARCHAR(
|
|
121
|
+
ask_id VARCHAR(64) NOT NULL,
|
|
122
|
+
task_id VARCHAR(64) NOT NULL,
|
|
90
123
|
source_task_id VARCHAR(255) NOT NULL,
|
|
91
|
-
session_id VARCHAR(
|
|
92
|
-
owner VARCHAR(
|
|
93
|
-
batch_id VARCHAR(
|
|
124
|
+
session_id VARCHAR(64) NOT NULL,
|
|
125
|
+
owner VARCHAR(190) NULL,
|
|
126
|
+
batch_id VARCHAR(64) NOT NULL,
|
|
94
127
|
tool_call_id VARCHAR(255) NOT NULL,
|
|
95
128
|
-- leg_key: 车3 §3.2′ 身份轴——sha256(resume checkpoint token) 的 hex(64 字符),首腿存空串。
|
|
96
129
|
-- 取代原来的数值 leg 列:一腿一凭据,零 DDL 依赖 task_run、零 markResuming 改动。NOT NULL(空串
|
|
@@ -110,7 +143,7 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
|
|
|
110
143
|
decision_note TEXT NULL,
|
|
111
144
|
decided_at_ms BIGINT NULL,
|
|
112
145
|
denied_reason VARCHAR(64) NULL,
|
|
113
|
-
gate_token VARCHAR(
|
|
146
|
+
gate_token VARCHAR(120) NULL,
|
|
114
147
|
gate_bound_call_id VARCHAR(255) NULL,
|
|
115
148
|
gate_bound_input_hash VARCHAR(255) NULL,
|
|
116
149
|
idempotency_key VARCHAR(255) NULL,
|
|
@@ -141,10 +174,10 @@ export async function ensureTiDBApprovalAskSchema(pool) {
|
|
|
141
174
|
}
|
|
142
175
|
export async function ensurePgApprovalAskSchema(q) {
|
|
143
176
|
await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCHES_TABLE} (
|
|
144
|
-
batch_id VARCHAR(
|
|
145
|
-
task_id VARCHAR(
|
|
146
|
-
state VARCHAR(32) NOT NULL,
|
|
147
|
-
bound_ask_id VARCHAR(
|
|
177
|
+
batch_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
178
|
+
task_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
179
|
+
state VARCHAR(32) COLLATE "C" NOT NULL,
|
|
180
|
+
bound_ask_id VARCHAR(64) COLLATE "C",
|
|
148
181
|
version INT NOT NULL DEFAULT 0,
|
|
149
182
|
created_at_ms BIGINT NOT NULL,
|
|
150
183
|
updated_at_ms BIGINT NOT NULL,
|
|
@@ -152,33 +185,33 @@ export async function ensurePgApprovalAskSchema(q) {
|
|
|
152
185
|
)`);
|
|
153
186
|
await q(`CREATE INDEX IF NOT EXISTS idx_batch_task ON ${APPROVAL_BATCHES_TABLE} (task_id)`);
|
|
154
187
|
await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_ASKS_TABLE} (
|
|
155
|
-
ask_id VARCHAR(
|
|
156
|
-
task_id VARCHAR(
|
|
157
|
-
source_task_id VARCHAR(255) NOT NULL,
|
|
158
|
-
session_id VARCHAR(
|
|
159
|
-
owner VARCHAR(
|
|
160
|
-
batch_id VARCHAR(
|
|
161
|
-
tool_call_id VARCHAR(255) NOT NULL,
|
|
188
|
+
ask_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
189
|
+
task_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
190
|
+
source_task_id VARCHAR(255) COLLATE "C" NOT NULL,
|
|
191
|
+
session_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
192
|
+
owner VARCHAR(190) COLLATE "C",
|
|
193
|
+
batch_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
194
|
+
tool_call_id VARCHAR(255) COLLATE "C" NOT NULL,
|
|
162
195
|
-- leg_key: 车3 §3.2′ 身份轴——sha256(resume checkpoint token) 的 hex(64 字符),首腿存空串
|
|
163
196
|
-- (空串是首腿的真值,不是缺席,故 NOT NULL)。MySQL 侧 twin 逐字同形。
|
|
164
|
-
leg_key VARCHAR(64) NOT NULL,
|
|
165
|
-
parent_tool_call_id VARCHAR(255),
|
|
197
|
+
leg_key VARCHAR(64) COLLATE "C" NOT NULL,
|
|
198
|
+
parent_tool_call_id VARCHAR(255) COLLATE "C",
|
|
166
199
|
-- bound_input_hash(车5 §9 C2)——MySQL twin 逐字同形(宽度/语义见那侧注)。
|
|
167
|
-
bound_input_hash VARCHAR(255),
|
|
168
|
-
state VARCHAR(32) NOT NULL,
|
|
200
|
+
bound_input_hash VARCHAR(255) COLLATE "C",
|
|
201
|
+
state VARCHAR(32) COLLATE "C" NOT NULL,
|
|
169
202
|
-- clay 批 TINYINT(1) 归一先例:PG 侧统一 SMALLINT 0/1(此前本家族用过 BOOLEAN,已裁弃).
|
|
170
203
|
provisional SMALLINT NOT NULL DEFAULT 0,
|
|
171
204
|
version INT NOT NULL DEFAULT 0,
|
|
172
|
-
decision VARCHAR(16),
|
|
173
|
-
decision_actor TEXT,
|
|
174
|
-
decision_note TEXT,
|
|
205
|
+
decision VARCHAR(16) COLLATE "C",
|
|
206
|
+
decision_actor TEXT COLLATE "C",
|
|
207
|
+
decision_note TEXT COLLATE "C",
|
|
175
208
|
decided_at_ms BIGINT,
|
|
176
|
-
denied_reason VARCHAR(64),
|
|
177
|
-
gate_token VARCHAR(
|
|
178
|
-
gate_bound_call_id VARCHAR(255),
|
|
179
|
-
gate_bound_input_hash VARCHAR(255),
|
|
180
|
-
idempotency_key VARCHAR(255),
|
|
181
|
-
card_json TEXT NOT NULL,
|
|
209
|
+
denied_reason VARCHAR(64) COLLATE "C",
|
|
210
|
+
gate_token VARCHAR(120) COLLATE "C",
|
|
211
|
+
gate_bound_call_id VARCHAR(255) COLLATE "C",
|
|
212
|
+
gate_bound_input_hash VARCHAR(255) COLLATE "C",
|
|
213
|
+
idempotency_key VARCHAR(255) COLLATE "C",
|
|
214
|
+
card_json TEXT COLLATE "C" NOT NULL,
|
|
182
215
|
schema_version INT NOT NULL,
|
|
183
216
|
expires_at_ms BIGINT NOT NULL,
|
|
184
217
|
created_at_ms BIGINT NOT NULL,
|
|
@@ -330,13 +363,17 @@ export class SqlApprovalAskStore {
|
|
|
330
363
|
}
|
|
331
364
|
async ensureAsk(row) {
|
|
332
365
|
const now = row.createdAtMs;
|
|
366
|
+
/** 本次 ask 插入的 affected 行数(1 = 真插入 / 0 = 幂等命中)——判别位的**唯一**来源,见
|
|
367
|
+
* {@link EnsureAskResult}。两方言各自的语句形不同(`INSERT IGNORE` vs `ON CONFLICT DO NOTHING`),
|
|
368
|
+
* 但 affected 的语义是同一个,`SqlDriver` 已把 mysql2 的 `affectedRows` 与 pg 的 `rowCount` 归一。 */
|
|
369
|
+
let insertedRows = 0;
|
|
333
370
|
const conn = await this.db.connect();
|
|
334
371
|
try {
|
|
335
372
|
await conn.begin();
|
|
336
373
|
await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_BATCHES_TABLE} (batch_id, task_id, state, version, created_at_ms, updated_at_ms) VALUES (?, ?, 'OPEN', 0, ?, ?)`, `INSERT INTO ${APPROVAL_BATCHES_TABLE} (batch_id, task_id, state, version, created_at_ms, updated_at_ms) VALUES ($1, $2, 'OPEN', 0, $3, $4) ON CONFLICT (batch_id) DO NOTHING`), [row.batchId, row.taskId, now, now]);
|
|
337
374
|
// 🔴 `idempotency_key` **不在 INSERT 列清单里**(车4 §12-E 双写者收口):该列已归回决专用,只有
|
|
338
375
|
// `decideAsk` 赢 CAS 时写它,铸行恒 NULL。留在这里的 NULL 字面量就是这条纪律的落笔处。
|
|
339
|
-
await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASKS_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, version, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
376
|
+
const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASKS_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, version, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
340
377
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASKS_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, version, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
341
378
|
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $11, $12, $13, $14, $15) ON CONFLICT (ask_id) DO NOTHING"), [
|
|
342
379
|
row.askId,
|
|
@@ -355,6 +392,7 @@ export class SqlApprovalAskStore {
|
|
|
355
392
|
now,
|
|
356
393
|
now,
|
|
357
394
|
]);
|
|
395
|
+
insertedRows = askInsert.affected;
|
|
358
396
|
await conn.commit();
|
|
359
397
|
}
|
|
360
398
|
catch (err) {
|
|
@@ -372,7 +410,9 @@ export class SqlApprovalAskStore {
|
|
|
372
410
|
const out = await this.getAsk(row.askId);
|
|
373
411
|
if (!out)
|
|
374
412
|
throw new Error(`ensureAsk: row vanished after insert (askId=${row.askId})`);
|
|
375
|
-
|
|
413
|
+
// 判别位取**提交那一刻**引擎报的 affected,不是事后回读推断的:回读发生在提交之后,那时并发的
|
|
414
|
+
// 第二个调用可能已经改过行,任何「从行上反推谁插的」都是又一次猜。
|
|
415
|
+
return { row: out, inserted: insertedRows > 0 };
|
|
376
416
|
}
|
|
377
417
|
async transitionAsk(askId, from, to, patch) {
|
|
378
418
|
// machine 层先判(非法转移=编程错误,throw 而非返回 false——设计定稿 §4 原话)。
|
|
@@ -163,7 +163,7 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
|
|
|
163
163
|
handle VARBINARY(190) NOT NULL,
|
|
164
164
|
scope VARCHAR(190) NOT NULL,
|
|
165
165
|
owner VARCHAR(190) NOT NULL,
|
|
166
|
-
session_scoped TINYINT NOT NULL,
|
|
166
|
+
session_scoped TINYINT(1) NOT NULL,
|
|
167
167
|
session_id VARCHAR(190) NULL,
|
|
168
168
|
parent_session_id VARCHAR(190) NULL,
|
|
169
169
|
-- core 1.367 δ(additive):root 锚(listBySession 第二臂;NULL = 该记录无 root 锚,谓词天然拒)
|
|
@@ -198,29 +198,29 @@ export async function ensurePgBackgroundAgentSchema(q) {
|
|
|
198
198
|
await q(`CREATE TABLE IF NOT EXISTS ${BACKGROUND_AGENT_TABLE} (
|
|
199
199
|
scope_key VARCHAR(190) COLLATE "C" NOT NULL,
|
|
200
200
|
handle VARCHAR(190) COLLATE "C" NOT NULL,
|
|
201
|
-
scope VARCHAR(190) NOT NULL,
|
|
202
|
-
owner VARCHAR(190) NOT NULL,
|
|
201
|
+
scope VARCHAR(190) COLLATE "C" NOT NULL,
|
|
202
|
+
owner VARCHAR(190) COLLATE "C" NOT NULL,
|
|
203
203
|
session_scoped SMALLINT NOT NULL,
|
|
204
|
-
session_id VARCHAR(190),
|
|
205
|
-
parent_session_id VARCHAR(190),
|
|
204
|
+
session_id VARCHAR(190) COLLATE "C",
|
|
205
|
+
parent_session_id VARCHAR(190) COLLATE "C",
|
|
206
206
|
-- core 1.367 δ(additive,TiDB 同案):root 锚(listBySession 第二臂)
|
|
207
|
-
root_session_id VARCHAR(190),
|
|
208
|
-
session_anchor VARCHAR(190),
|
|
209
|
-
name VARCHAR(255),
|
|
210
|
-
agent_type VARCHAR(190),
|
|
211
|
-
status VARCHAR(16) NOT NULL,
|
|
207
|
+
root_session_id VARCHAR(190) COLLATE "C",
|
|
208
|
+
session_anchor VARCHAR(190) COLLATE "C",
|
|
209
|
+
name VARCHAR(255) COLLATE "C",
|
|
210
|
+
agent_type VARCHAR(190) COLLATE "C",
|
|
211
|
+
status VARCHAR(16) COLLATE "C" NOT NULL,
|
|
212
212
|
spawned_at_ms BIGINT NOT NULL,
|
|
213
213
|
updated_at_ms BIGINT NOT NULL,
|
|
214
214
|
settled_at_ms BIGINT,
|
|
215
|
-
usage_json TEXT,
|
|
215
|
+
usage_json TEXT COLLATE "C",
|
|
216
216
|
rev BIGINT NOT NULL,
|
|
217
217
|
-- core 1.383([1565] / design/153 件3a)ε(TiDB 同案注):updateIf 的 CAS 守卫专用列
|
|
218
|
-
parked_checkpoint_token VARCHAR(190),
|
|
219
|
-
park_claim_id VARCHAR(190),
|
|
218
|
+
parked_checkpoint_token VARCHAR(190) COLLATE "C",
|
|
219
|
+
park_claim_id VARCHAR(190) COLLATE "C",
|
|
220
220
|
-- core 5.14.0 #22(TiDB 同案注:三态与「写侧投影」口径见 MySQL 孪生的行内注)。
|
|
221
|
-
admitted_org_scopes TEXT,
|
|
222
|
-
admitted_org_write_scope VARCHAR(190),
|
|
223
|
-
record_json TEXT NOT NULL,
|
|
221
|
+
admitted_org_scopes TEXT COLLATE "C",
|
|
222
|
+
admitted_org_write_scope VARCHAR(190) COLLATE "C",
|
|
223
|
+
record_json TEXT COLLATE "C" NOT NULL,
|
|
224
224
|
PRIMARY KEY (scope_key, handle)
|
|
225
225
|
)`);
|
|
226
226
|
await q(`CREATE INDEX IF NOT EXISTS idx_bga_anchor ON ${BACKGROUND_AGENT_TABLE} (scope_key, session_anchor, spawned_at_ms)`);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - **owned-id isolation** — `poll`/`kill` look the shellId up in THIS manager's registry; an id from another env
|
|
12
12
|
* (or a forged one) is `not_found`, never resolved against the provider (design/103 §3.8 / interface 越权契约).
|
|
13
13
|
* - **maxConcurrent** — counts only `running` shells (terminal entries are kept for residual polling but free their
|
|
14
|
-
* slot, so "
|
|
14
|
+
* slot, so "TaskStop one first" is actionable — matches the TOC `maxConcurrent 终态占槽` review fix).
|
|
15
15
|
* - **timeout hard wall** — a control-plane timer kills the shell and flips it to `killed` at the bounded BG
|
|
16
16
|
* timeout (design/103 §3.6); the driver may set an additional provider-level backstop ≥ this wall.
|
|
17
17
|
* - **dispose** — kills + cleans up EVERY shell, best-effort, MUST NOT throw, idempotent (design/103 §3.7).
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - **owned-id isolation** — `poll`/`kill` look the shellId up in THIS manager's registry; an id from another env
|
|
12
12
|
* (or a forged one) is `not_found`, never resolved against the provider (design/103 §3.8 / interface 越权契约).
|
|
13
13
|
* - **maxConcurrent** — counts only `running` shells (terminal entries are kept for residual polling but free their
|
|
14
|
-
* slot, so "
|
|
14
|
+
* slot, so "TaskStop one first" is actionable — matches the TOC `maxConcurrent 终态占槽` review fix).
|
|
15
15
|
* - **timeout hard wall** — a control-plane timer kills the shell and flips it to `killed` at the bounded BG
|
|
16
16
|
* timeout (design/103 §3.6); the driver may set an additional provider-level backstop ≥ this wall.
|
|
17
17
|
* - **dispose** — kills + cleans up EVERY shell, best-effort, MUST NOT throw, idempotent (design/103 §3.7).
|
|
@@ -207,7 +207,7 @@ export class BackgroundShellManager {
|
|
|
207
207
|
/** The shared spawn/adopt body: limit check, opaque id, terminal-latch buffering, BG-timeout hard wall. */
|
|
208
208
|
async register(launch, timeoutSec) {
|
|
209
209
|
if (this.liveCount() >= this.caps.maxConcurrent) {
|
|
210
|
-
return fail(new BackgroundShellError("limit_exceeded", `Too many running background shells (max ${this.caps.maxConcurrent});
|
|
210
|
+
return fail(new BackgroundShellError("limit_exceeded", `Too many running background shells (max ${this.caps.maxConcurrent}); TaskStop one first.`));
|
|
211
211
|
}
|
|
212
212
|
const bgTimeoutSec = this.boundedBgTimeoutSec(timeoutSec); // #131-T0:与 adoptSync 同座(NaN 当缺席)
|
|
213
213
|
// Opaque brand — NOT derived from the provider job/pid (design/103 §3.8 越权红线).
|
|
@@ -4,8 +4,8 @@ import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
|
4
4
|
* centrally in tidb-pool.ts (no PG-side twin needed there — production wires ONE ensureTidbSchema). */
|
|
5
5
|
export const PG_BREAKER_STATE_SCHEMA = [
|
|
6
6
|
`CREATE TABLE IF NOT EXISTS circuit_breaker (
|
|
7
|
-
breaker_key VARCHAR(190) NOT NULL,
|
|
8
|
-
phase VARCHAR(16) NOT NULL,
|
|
7
|
+
breaker_key VARCHAR(190) COLLATE "C" NOT NULL,
|
|
8
|
+
phase VARCHAR(16) COLLATE "C" NOT NULL,
|
|
9
9
|
failures INTEGER NOT NULL DEFAULT 0,
|
|
10
10
|
opened_at TIMESTAMPTZ(3),
|
|
11
11
|
updated_at TIMESTAMPTZ(3) NOT NULL,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
|
-
import { type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor } from "@sema-agent/core";
|
|
3
|
+
import { type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
|
|
4
4
|
import { type SqlDriver } from "./sql-driver.js";
|
|
5
5
|
/**
|
|
6
6
|
* design/80 D-1 (§3 invariant #3 — crash-safe reaper backstop): an ABSOLUTE upper bound on a pending
|
|
@@ -77,6 +77,16 @@ export interface CheckpointAskCandidate {
|
|
|
77
77
|
/** `checkpoint.status` 列(权威;`pending` | `resolved` | `expired`)。不做过滤,由收敛器判活性。 */
|
|
78
78
|
status: string;
|
|
79
79
|
createdAtMs: number;
|
|
80
|
+
/**
|
|
81
|
+
* 🔴 和解三元组的第一维(#168 件1,黑板 [2897]③①)—— checkpoint 自己带的 `sourceTaskId`(core 在
|
|
82
|
+
* suspendAsk 铸行时恒填该腿的 `sessionId`)。**没有对应的列**,只能从 blob 窄读;读不出/缺席 = null。
|
|
83
|
+
*
|
|
84
|
+
* 为什么必须进候选集:读口按 `(scope, session_id, tool_call_id)` 查,而委派子代那条腿的 ask 落行时
|
|
85
|
+
* `session_id` 记的是**投递上下文**(根会话),park 却发生在**子代自己**的 sessionId 上 —— 只靠
|
|
86
|
+
* (toolCallId, hash) 两维,一条根腿的 park 会被一只子代的 ask 认领(摘要相同的两次同 args 调用完全
|
|
87
|
+
* 正常)。三元组全同才算同一件事。
|
|
88
|
+
*/
|
|
89
|
+
sourceTaskId: string | null;
|
|
80
90
|
/** 该 park 绑定的 tool call(= `pendingAction.toolCallId`)。`unparseable` 行为 null。 */
|
|
81
91
|
boundCallId: string | null;
|
|
82
92
|
/** 服务端铸的入参摘要——判据 1 的第二道硬等式(与 `approval_asks.bound_input_hash` 相等才算命中)。 */
|
|
@@ -109,13 +119,54 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
109
119
|
protected readonly logger?: {
|
|
110
120
|
info?(msg: string, meta?: unknown): void;
|
|
111
121
|
} | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* `CheckpointStore.durability` 声明(#167 欠账,#168 件5)—— 行落在 MySQL-protocol / PostgreSQL 的
|
|
124
|
+
* `checkpoint` 表里,进程重启、副本轮换、整机重建都不丢 ⇒ `"durable"`,如实。
|
|
125
|
+
*
|
|
126
|
+
* 🔴 为什么这一格空着是有代价的:core 的 `resolveDeclaredDurability` 把**缺席**折成 `"process-local"`
|
|
127
|
+
* (它不能替一个没表态的店猜),于是静态装配面对每一个真持久部署都读出 `process_local`,
|
|
128
|
+
* `GET /v1/diagnostics/wiring` 的 park 车道读数与启动自检的那条警告都因此不可信 —— 而 park 正是流内
|
|
129
|
+
* 审批协议的降级目的地,「重启后还赎不赎得回」是运维必须能一眼看见的事。声明是店自己的责任,不是
|
|
130
|
+
* 消费侧靠 backend.kind 猜出来的。
|
|
131
|
+
*/
|
|
132
|
+
readonly durability: StoreDurability;
|
|
133
|
+
/**
|
|
134
|
+
* `CheckpointStore.fidelity` 声明(core 5.17.0 [3052] 提货批 #172)——**如实按介质判**:本店把整个
|
|
135
|
+
* checkpoint 经 {@link SqlCheckpointStore.json} 编码进一个 JSON 列(TiDB 逐字文本 / PG lossless
|
|
136
|
+
* `::jsonb` 协议信封),读侧 `parseJson` 还原 ⇒ 能扛过 round-trip 的只有 JSON 值域,`"json"`。
|
|
137
|
+
*
|
|
138
|
+
* 🔴 为什么必须显式写、哪怕缺席也折向 json:core 的 `resolveDeclaredFidelity` 对缺席是 fail-closed
|
|
139
|
+
* (读 json),所以沉默不会立刻错——但沉默**表达不出**「我核对过我的介质就是这个宽度」。park 铸行
|
|
140
|
+
* 的 args / preview / 风险描述 / `boundInputHash` 全部从这一格算出的投影铸;哪天这四行编码里任何
|
|
141
|
+
* 一处改了介质(换存储格式、换列类型),声明在场才有东西可以红,沉默那格只会安静地按错宽度铸出
|
|
142
|
+
* 一份「审批人看到的 ≠ 盘上躺着的 ≠ resume 执行的」。同 `durability` 的 #168 件5 教训:表态是店
|
|
143
|
+
* 自己的责任,不是消费侧靠 backend.kind 猜。
|
|
144
|
+
*
|
|
145
|
+
* ⚠️ **已知残余(不是遮掩,是这两个词表达不了的那一格)**:PG 臂比 `"json"` 字面**略窄** ——
|
|
146
|
+
* {@link pgProtocolJsonStringify} 对含 NUL / lone surrogate 的串 fail-loud 拒绝(R4-H1 有意裁定:
|
|
147
|
+
* 复核面必须与真执行的 args 在 NUL 那一位上一致,「悄悄清洗再存」是不可接受的那一支),而 core 的
|
|
148
|
+
* json 宽度收下这些码位。`StoreFidelity` 的闭集只有 `"structured-clone" | "json"`,没有第三个词能说
|
|
149
|
+
* 「json 减去本介质存不下的码位」——声明 `"structured-clone"` 是大得多的谎,所以 `"json"` 仍是两者
|
|
150
|
+
* 里唯一诚实的选择。后果有界且 fail-closed:core 5.17.0 起 park 铸行失败会把 cause 带到 gate、追加
|
|
151
|
+
* 到 fallback 的 deny 上,这条 args 退回**同步门**由人判(不静默漏批、不挂死)。边界钉在
|
|
152
|
+
* `wiring-governance-operator.test.ts` 的 #172 组;已上报上游求一个能表达该宽度的词。
|
|
153
|
+
*/
|
|
154
|
+
readonly fidelity: StoreFidelity;
|
|
112
155
|
constructor(db: SqlDriver, logger?: {
|
|
113
156
|
info?(msg: string, meta?: unknown): void;
|
|
114
157
|
} | undefined);
|
|
115
158
|
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
116
159
|
private q;
|
|
117
|
-
/** JSON column binding
|
|
118
|
-
*
|
|
160
|
+
/** JSON column binding. TiDB: plain `JSON.stringify`, stored verbatim. PG: `pgProtocolJsonStringify` —
|
|
161
|
+
* ordinary `JSON.stringify` that **refuses** (throws `PgUnstorableError`) when the value carries a code
|
|
162
|
+
* point PG cannot hold (NUL / lone surrogate); the `::jsonb` at the call site is just the bind cast, not
|
|
163
|
+
* an escaping layer.
|
|
164
|
+
*
|
|
165
|
+
* "LOSSLESS" in R4-H1 means exactly **"never silently lossy"**, not "encodes everything": the contrast is
|
|
166
|
+
* with `pgSanitizeText`, the lossy U+FFFD-scarring path used for CONTENT faces. An approval row is not a
|
|
167
|
+
* content face — the operator's review surface has to agree with the executed args AT the NUL position, so
|
|
168
|
+
* scrubbing the byte and storing the scrubbed row is the unacceptable arm; refusing loudly is the chosen one.
|
|
169
|
+
* That refusal is why {@link SqlCheckpointStore.fidelity} carries a documented residual (see it). */
|
|
119
170
|
private json;
|
|
120
171
|
private isDupKey;
|
|
121
172
|
/** Create-once. core mints the token (`mintCheckpointToken`) and calls this during suspend. */
|
|
@@ -175,8 +226,11 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
175
226
|
*
|
|
176
227
|
* 🔴 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须自己有 CAS:两个并发 steer 读到同一份
|
|
177
228
|
* 队列、后写者整个盖掉前者 —— 就是 #147 要消灭的静默丢失换了个位置复发。守卫是 `pending_steer_rev`
|
|
178
|
-
* **专用整数列**(
|
|
179
|
-
* CAS
|
|
229
|
+
* **专用整数列**(不是拿旧列值比:整数按构造唯一,而长 JSON 文本的相等性取决于排序规则/字段序/空白与
|
|
230
|
+
* 转义形,任一处判等松一点就是一次 CAS 假命中 = 悄悄丢一条 steer。#192 A11 更正:本注原先的理由写的是
|
|
231
|
+
* "本表无显式 COLLATE,*_ci 会把只差大小写的两份信封判等",那半已不成立——`checkpoint` 现在钉了
|
|
232
|
+
* `COLLATE utf8mb4_bin`/PG 侧 `COLLATE "C"`;结论不变,理由回到上面那条更根本的;
|
|
233
|
+
* 也不是复用 resolve 的 `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
|
|
180
234
|
* CAS 输了就重读重算(队列只有 3 个位子,争用者天然极少);给足冗余仍全败 ⇒ **抛**,不返回 false ——
|
|
181
235
|
* `false` 在调用方的语义是「没有 pending 行可转向」(HTTP 409「已不再挂起」),把争用报成那个是撒谎。
|
|
182
236
|
*
|
|
@@ -268,9 +322,14 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
268
322
|
* 扫描(§8 C-6)。什么算读不出:blob 的 `version` 超出本 build 支持(`get()` 那条前向兼容门在这里
|
|
269
323
|
* 不能 throw,否则一条超前行会让整个 session 的对账永久卡死)、或 blob JSON 坏。
|
|
270
324
|
*
|
|
271
|
-
* 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId`
|
|
272
|
-
*
|
|
273
|
-
*
|
|
325
|
+
* 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId` 盖下来的权威投影,SQL 谓词
|
|
326
|
+
* 先按它(或 NULL)收窄;列为 NULL 的行(无工具动作的 park,或列存在之前的旧行)靠解 blob 补判——解得出
|
|
327
|
+
* 且相等才算候选,解不出就标 `unparseable`。
|
|
328
|
+
* 🔴 **没有「列命中即零解析」的快路径**(原注写过,已作废,别照它优化):函数体对**每一行**无条件解
|
|
329
|
+
* blob,原因有二 ——(a) 前向兼容门与 blob 可读性门必须门在**所有**命中路径之前(codex F3 + 确认轮:
|
|
330
|
+
* 列长得对不代表 blob 读得出,放行一条读不出的行去 `bindBatch` 会把 ask 钉成 PARKED + 一张本进程读不出
|
|
331
|
+
* 的 resume 坐标,而 PARKED 不可回滚);(b) `#168` 之后 blob 顶层的 `sourceTaskId` 是和解三元组的第一
|
|
332
|
+
* 维,列命中行结构上也必须解 blob 才拿得到它。
|
|
274
333
|
*/
|
|
275
334
|
findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
|
|
276
335
|
/**
|
|
@@ -99,8 +99,15 @@ function pendingActionToolCallId(blob) {
|
|
|
99
99
|
}
|
|
100
100
|
if (typeof parsed !== "object" || parsed === null)
|
|
101
101
|
return { readable: false };
|
|
102
|
+
// 🔴 `sourceTaskId` 是 checkpoint 的**顶层**字段(不在 pendingAction 里),没有对应的列 ⇒ 只能从
|
|
103
|
+
// blob 读。它是和解三元组的第一维(#168 件1);形不合(有这个键但不是字符串)= 结构漂移,按本函数
|
|
104
|
+
// 一贯的收窄纪律判 `readable: false`,不静默折成缺席。
|
|
105
|
+
const rawSource = "sourceTaskId" in parsed ? parsed.sourceTaskId : null;
|
|
106
|
+
if (rawSource !== null && rawSource !== undefined && typeof rawSource !== "string")
|
|
107
|
+
return { readable: false };
|
|
108
|
+
const sourceTaskId = typeof rawSource === "string" ? rawSource : null;
|
|
102
109
|
if (!("pendingAction" in parsed))
|
|
103
|
-
return { readable: true, toolCallId: null, boundInputHash: null, kind: null }; // 无 pendingAction 的 park:确定不匹配
|
|
110
|
+
return { readable: true, toolCallId: null, boundInputHash: null, kind: null, sourceTaskId }; // 无 pendingAction 的 park:确定不匹配
|
|
104
111
|
const pa = parsed.pendingAction;
|
|
105
112
|
if (typeof pa !== "object" || pa === null)
|
|
106
113
|
return { readable: false };
|
|
@@ -108,7 +115,7 @@ function pendingActionToolCallId(blob) {
|
|
|
108
115
|
if (kind !== null && typeof kind !== "string")
|
|
109
116
|
return { readable: false };
|
|
110
117
|
if (!("toolCallId" in pa))
|
|
111
|
-
return { readable: true, toolCallId: null, boundInputHash: null, kind }; // plan_review / task_done / resource_limit 腿
|
|
118
|
+
return { readable: true, toolCallId: null, boundInputHash: null, kind, sourceTaskId }; // plan_review / task_done / resource_limit 腿
|
|
112
119
|
const id = pa.toolCallId;
|
|
113
120
|
if (typeof id !== "string")
|
|
114
121
|
return { readable: false };
|
|
@@ -122,7 +129,7 @@ function pendingActionToolCallId(blob) {
|
|
|
122
129
|
const bih = "boundInputHash" in pa ? pa.boundInputHash : null;
|
|
123
130
|
if (bih !== null && typeof bih !== "string")
|
|
124
131
|
return { readable: false };
|
|
125
|
-
return { readable: true, toolCallId: id, boundInputHash: bih, kind };
|
|
132
|
+
return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceTaskId };
|
|
126
133
|
}
|
|
127
134
|
const DUP_ENTRY = 1062; // MySQL/TiDB ER_DUP_ENTRY
|
|
128
135
|
const PG_UNIQUE_VIOLATION = "23505";
|
|
@@ -228,6 +235,39 @@ const STEER_CAS_ATTEMPTS = 8;
|
|
|
228
235
|
export class SqlCheckpointStore {
|
|
229
236
|
db;
|
|
230
237
|
logger;
|
|
238
|
+
/**
|
|
239
|
+
* `CheckpointStore.durability` 声明(#167 欠账,#168 件5)—— 行落在 MySQL-protocol / PostgreSQL 的
|
|
240
|
+
* `checkpoint` 表里,进程重启、副本轮换、整机重建都不丢 ⇒ `"durable"`,如实。
|
|
241
|
+
*
|
|
242
|
+
* 🔴 为什么这一格空着是有代价的:core 的 `resolveDeclaredDurability` 把**缺席**折成 `"process-local"`
|
|
243
|
+
* (它不能替一个没表态的店猜),于是静态装配面对每一个真持久部署都读出 `process_local`,
|
|
244
|
+
* `GET /v1/diagnostics/wiring` 的 park 车道读数与启动自检的那条警告都因此不可信 —— 而 park 正是流内
|
|
245
|
+
* 审批协议的降级目的地,「重启后还赎不赎得回」是运维必须能一眼看见的事。声明是店自己的责任,不是
|
|
246
|
+
* 消费侧靠 backend.kind 猜出来的。
|
|
247
|
+
*/
|
|
248
|
+
durability = "durable";
|
|
249
|
+
/**
|
|
250
|
+
* `CheckpointStore.fidelity` 声明(core 5.17.0 [3052] 提货批 #172)——**如实按介质判**:本店把整个
|
|
251
|
+
* checkpoint 经 {@link SqlCheckpointStore.json} 编码进一个 JSON 列(TiDB 逐字文本 / PG lossless
|
|
252
|
+
* `::jsonb` 协议信封),读侧 `parseJson` 还原 ⇒ 能扛过 round-trip 的只有 JSON 值域,`"json"`。
|
|
253
|
+
*
|
|
254
|
+
* 🔴 为什么必须显式写、哪怕缺席也折向 json:core 的 `resolveDeclaredFidelity` 对缺席是 fail-closed
|
|
255
|
+
* (读 json),所以沉默不会立刻错——但沉默**表达不出**「我核对过我的介质就是这个宽度」。park 铸行
|
|
256
|
+
* 的 args / preview / 风险描述 / `boundInputHash` 全部从这一格算出的投影铸;哪天这四行编码里任何
|
|
257
|
+
* 一处改了介质(换存储格式、换列类型),声明在场才有东西可以红,沉默那格只会安静地按错宽度铸出
|
|
258
|
+
* 一份「审批人看到的 ≠ 盘上躺着的 ≠ resume 执行的」。同 `durability` 的 #168 件5 教训:表态是店
|
|
259
|
+
* 自己的责任,不是消费侧靠 backend.kind 猜。
|
|
260
|
+
*
|
|
261
|
+
* ⚠️ **已知残余(不是遮掩,是这两个词表达不了的那一格)**:PG 臂比 `"json"` 字面**略窄** ——
|
|
262
|
+
* {@link pgProtocolJsonStringify} 对含 NUL / lone surrogate 的串 fail-loud 拒绝(R4-H1 有意裁定:
|
|
263
|
+
* 复核面必须与真执行的 args 在 NUL 那一位上一致,「悄悄清洗再存」是不可接受的那一支),而 core 的
|
|
264
|
+
* json 宽度收下这些码位。`StoreFidelity` 的闭集只有 `"structured-clone" | "json"`,没有第三个词能说
|
|
265
|
+
* 「json 减去本介质存不下的码位」——声明 `"structured-clone"` 是大得多的谎,所以 `"json"` 仍是两者
|
|
266
|
+
* 里唯一诚实的选择。后果有界且 fail-closed:core 5.17.0 起 park 铸行失败会把 cause 带到 gate、追加
|
|
267
|
+
* 到 fallback 的 deny 上,这条 args 退回**同步门**由人判(不静默漏批、不挂死)。边界钉在
|
|
268
|
+
* `wiring-governance-operator.test.ts` 的 #172 组;已上报上游求一个能表达该宽度的词。
|
|
269
|
+
*/
|
|
270
|
+
fidelity = "json";
|
|
231
271
|
constructor(db, logger) {
|
|
232
272
|
this.db = db;
|
|
233
273
|
this.logger = logger;
|
|
@@ -236,8 +276,16 @@ export class SqlCheckpointStore {
|
|
|
236
276
|
q(tidb, pg) {
|
|
237
277
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
238
278
|
}
|
|
239
|
-
/** JSON column binding
|
|
240
|
-
*
|
|
279
|
+
/** JSON column binding. TiDB: plain `JSON.stringify`, stored verbatim. PG: `pgProtocolJsonStringify` —
|
|
280
|
+
* ordinary `JSON.stringify` that **refuses** (throws `PgUnstorableError`) when the value carries a code
|
|
281
|
+
* point PG cannot hold (NUL / lone surrogate); the `::jsonb` at the call site is just the bind cast, not
|
|
282
|
+
* an escaping layer.
|
|
283
|
+
*
|
|
284
|
+
* "LOSSLESS" in R4-H1 means exactly **"never silently lossy"**, not "encodes everything": the contrast is
|
|
285
|
+
* with `pgSanitizeText`, the lossy U+FFFD-scarring path used for CONTENT faces. An approval row is not a
|
|
286
|
+
* content face — the operator's review surface has to agree with the executed args AT the NUL position, so
|
|
287
|
+
* scrubbing the byte and storing the scrubbed row is the unacceptable arm; refusing loudly is the chosen one.
|
|
288
|
+
* That refusal is why {@link SqlCheckpointStore.fidelity} carries a documented residual (see it). */
|
|
241
289
|
json(value, label) {
|
|
242
290
|
return dialectProtocolJsonEncoder(this.db.dialect)(value, label);
|
|
243
291
|
}
|
|
@@ -300,8 +348,12 @@ export class SqlCheckpointStore {
|
|
|
300
348
|
// design/80 D-1: surface the reopen-by-reason + OCC fields off the AUTHORITATIVE columns (the blob is the
|
|
301
349
|
// suspend-time snapshot; resolve/reopen mutate only the columns). Core's resume reads these to validate an
|
|
302
350
|
// env_failed re-resume against the persisted winner, and to require the `rev` it observed is still live (OCC).
|
|
303
|
-
// resolvedOutcome is DERIVED from the stored full outcome
|
|
304
|
-
//
|
|
351
|
+
// resolvedOutcome is DERIVED from the stored full outcome by core's `winnerFromOutcome`, so it is preserved
|
|
352
|
+
// across a reopen for free (reopen leaves `outcome` untouched). Do NOT read a key list into this line: the
|
|
353
|
+
// derived shape is core's and it GROWS (`answer` since 5.7.0, `reason` since 5.16.0 — the operator's deny
|
|
354
|
+
// note IS persisted; an older copy of this comment enumerated only {boundCallId,decision,updatedInput?} and
|
|
355
|
+
// read as if it weren't). We store the whole outcome blob and round-trip it, so additive keys ride for free
|
|
356
|
+
// — `ResolvedOutcome` in core's `checkpoint-store.d.ts` is the single owner of that list.
|
|
305
357
|
const outcomeRaw = parseJson(r.outcome);
|
|
306
358
|
if (outcomeRaw)
|
|
307
359
|
cp.resolvedOutcome = winnerFromOutcome(outcomeRaw);
|
|
@@ -402,8 +454,11 @@ export class SqlCheckpointStore {
|
|
|
402
454
|
*
|
|
403
455
|
* 🔴 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须自己有 CAS:两个并发 steer 读到同一份
|
|
404
456
|
* 队列、后写者整个盖掉前者 —— 就是 #147 要消灭的静默丢失换了个位置复发。守卫是 `pending_steer_rev`
|
|
405
|
-
* **专用整数列**(
|
|
406
|
-
* CAS
|
|
457
|
+
* **专用整数列**(不是拿旧列值比:整数按构造唯一,而长 JSON 文本的相等性取决于排序规则/字段序/空白与
|
|
458
|
+
* 转义形,任一处判等松一点就是一次 CAS 假命中 = 悄悄丢一条 steer。#192 A11 更正:本注原先的理由写的是
|
|
459
|
+
* "本表无显式 COLLATE,*_ci 会把只差大小写的两份信封判等",那半已不成立——`checkpoint` 现在钉了
|
|
460
|
+
* `COLLATE utf8mb4_bin`/PG 侧 `COLLATE "C"`;结论不变,理由回到上面那条更根本的;
|
|
461
|
+
* 也不是复用 resolve 的 `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
|
|
407
462
|
* CAS 输了就重读重算(队列只有 3 个位子,争用者天然极少);给足冗余仍全败 ⇒ **抛**,不返回 false ——
|
|
408
463
|
* `false` 在调用方的语义是「没有 pending 行可转向」(HTTP 409「已不再挂起」),把争用报成那个是撒谎。
|
|
409
464
|
*
|
|
@@ -595,9 +650,14 @@ export class SqlCheckpointStore {
|
|
|
595
650
|
* 扫描(§8 C-6)。什么算读不出:blob 的 `version` 超出本 build 支持(`get()` 那条前向兼容门在这里
|
|
596
651
|
* 不能 throw,否则一条超前行会让整个 session 的对账永久卡死)、或 blob JSON 坏。
|
|
597
652
|
*
|
|
598
|
-
* 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId`
|
|
599
|
-
*
|
|
600
|
-
*
|
|
653
|
+
* 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId` 盖下来的权威投影,SQL 谓词
|
|
654
|
+
* 先按它(或 NULL)收窄;列为 NULL 的行(无工具动作的 park,或列存在之前的旧行)靠解 blob 补判——解得出
|
|
655
|
+
* 且相等才算候选,解不出就标 `unparseable`。
|
|
656
|
+
* 🔴 **没有「列命中即零解析」的快路径**(原注写过,已作废,别照它优化):函数体对**每一行**无条件解
|
|
657
|
+
* blob,原因有二 ——(a) 前向兼容门与 blob 可读性门必须门在**所有**命中路径之前(codex F3 + 确认轮:
|
|
658
|
+
* 列长得对不代表 blob 读得出,放行一条读不出的行去 `bindBatch` 会把 ask 钉成 PARKED + 一张本进程读不出
|
|
659
|
+
* 的 resume 坐标,而 PARKED 不可回滚);(b) `#168` 之后 blob 顶层的 `sourceTaskId` 是和解三元组的第一
|
|
660
|
+
* 维,列命中行结构上也必须解 blob 才拿得到它。
|
|
601
661
|
*/
|
|
602
662
|
async findCheckpointCandidatesForAsk(scope, sessionId, toolCallId, sinceMs) {
|
|
603
663
|
const { rows } = await this.db.query(this.q("SELECT token, status, created_at, tool_call_id, bound_input_hash, version, checkpoint FROM checkpoint " +
|
|
@@ -605,11 +665,14 @@ export class SqlCheckpointStore {
|
|
|
605
665
|
"WHERE scope=$1 AND session_id=$2 AND created_at>=$3 AND (tool_call_id=$4 OR tool_call_id IS NULL) ORDER BY created_at ASC"), [scope, sessionId, sinceMs, toolCallId]);
|
|
606
666
|
const out = [];
|
|
607
667
|
for (const r of rows) {
|
|
668
|
+
// `sourceTaskId: null` 是**所有 unparseable 臂的共同底**(读不出的行不许带出一个可用于身份比对的
|
|
669
|
+
// 值);健康臂在下面用 blob 解出来的值覆盖它。
|
|
608
670
|
const base = {
|
|
609
671
|
token: String(r.token),
|
|
610
672
|
status: String(r.status),
|
|
611
673
|
createdAtMs: Number(r.created_at),
|
|
612
674
|
boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
|
|
675
|
+
sourceTaskId: null,
|
|
613
676
|
};
|
|
614
677
|
// 🔴 前向兼容门必须在**任何**命中路径之前(codex 复审 F3,2026-08-06 真缺陷):版本超前的行是
|
|
615
678
|
// `get()` 明确拒读的行,而 `tool_call_id` 列命中与否跟能不能读懂 blob 毫无关系。若让列的快路径
|
|
@@ -652,7 +715,7 @@ export class SqlCheckpointStore {
|
|
|
652
715
|
// 列在场时以列为准(权威投影);列为 NULL 的旧行回落用 blob 解出来的值。
|
|
653
716
|
const effective = col ?? derived.toolCallId;
|
|
654
717
|
if (effective === toolCallId)
|
|
655
|
-
out.push({ ...base, boundInputHash: effectiveHash, boundCallId: effective });
|
|
718
|
+
out.push({ ...base, boundInputHash: effectiveHash, sourceTaskId: derived.sourceTaskId, boundCallId: effective });
|
|
656
719
|
// 解得出、但不是这只 ask 的 callId(或这条 park 本来就没有工具动作)⇒ **确定**不是候选:
|
|
657
720
|
// 既不返回也不标坏行(标坏行会让收敛器把一条明确的「不匹配」当成「不确定」)。
|
|
658
721
|
}
|
|
@@ -7,7 +7,7 @@ import { type BakeStatus, type BakeState, type BakeRecord, type BakeEvent, type
|
|
|
7
7
|
* TINYINT(1) 归一(clay 批 2026-07-26):布尔列的 PG 映射统一 SMALLINT 0/1(此前本家族用 BOOLEAN)——
|
|
8
8
|
* 两方言驱动读回同为 number ⇒ 不再存在「忘了配的适配器」这个病族;写入参与 SQL 字面量一律 0/1,
|
|
9
9
|
* 读侧 `!!`/`Boolean()` 保持,store 出口 JS 类型逐字节不变(真双库套件的严格值断言为证)。
|
|
10
|
-
* The
|
|
10
|
+
* The line UNIQUE is given the explicit name uq_image_bake_event_line so a 23505 can be attributed to it via err.constraint. */
|
|
11
11
|
export declare const PG_IMAGE_BAKE_SCHEMA: string[];
|
|
12
12
|
/** Idempotent self-contained PG schema apply (for the integration test; central aggregation is done separately). */
|
|
13
13
|
export declare function ensurePgImageBakeSchema(pool: PgPool | PoolClient, poolName?: string): Promise<void>;
|