@sema-agent/server 7.2.0 → 7.3.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/README.md +2 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +6 -1
- package/dist/approval-ask-machine.d.ts +39 -0
- package/dist/approval-ask-machine.js +101 -0
- package/dist/approval-card.d.ts +244 -0
- package/dist/approval-card.js +237 -0
- package/dist/approval-deny-reasons.d.ts +56 -0
- package/dist/approval-deny-reasons.js +54 -0
- package/dist/approval-reconciler.d.ts +167 -0
- package/dist/approval-reconciler.js +307 -0
- package/dist/boot/coordinators.d.ts +1 -0
- package/dist/boot/coordinators.js +36 -3
- package/dist/boot/lexical-path-env.d.ts +14 -0
- package/dist/boot/lexical-path-env.js +116 -0
- package/dist/boot/reapers.d.ts +34 -0
- package/dist/boot/reapers.js +198 -23
- package/dist/boot/resolve-spec.js +82 -30
- package/dist/config-types.d.ts +61 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +130 -2
- package/dist/elicitation.d.ts +4 -0
- package/dist/elicitation.js +2 -2
- package/dist/http/routes/capabilities.js +14 -0
- package/dist/http/routes/runs.d.ts +1 -0
- package/dist/http/routes/runs.js +548 -16
- package/dist/http/routes/tasks.js +151 -9
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +114 -4
- package/dist/http/sse-log.d.ts +51 -0
- package/dist/http/sse-log.js +64 -0
- package/dist/http/wire-types.d.ts +20 -5
- package/dist/main.js +1 -1
- package/dist/plugins/approval-ask-store-memory.d.ts +38 -0
- package/dist/plugins/approval-ask-store-memory.js +299 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +341 -0
- package/dist/plugins/approval-ask-store-sql.js +705 -0
- package/dist/plugins/background-agent-store-sql.js +20 -1
- package/dist/plugins/checkpoint-store-sql.d.ts +84 -9
- package/dist/plugins/checkpoint-store-sql.js +297 -16
- package/dist/plugins/local-checkpoint-store.d.ts +6 -5
- package/dist/plugins/local-checkpoint-store.js +4 -0
- package/dist/plugins/pg-pool.js +11 -0
- package/dist/plugins/store-backend.d.ts +18 -0
- package/dist/plugins/store-backend.js +10 -0
- package/dist/plugins/tidb-pool.js +27 -4
- package/dist/question.d.ts +3 -0
- package/dist/question.js +2 -2
- package/dist/runs.d.ts +16 -1
- package/dist/runs.js +61 -3
- package/dist/runtime-caps-resolver.d.ts +7 -1
- package/dist/runtime-caps-resolver.js +65 -3
- package/dist/spec-fields.d.ts +4 -0
- package/dist/spec-fields.js +6 -0
- package/dist/task-settings.d.ts +35 -15
- package/dist/task-settings.js +19 -5
- package/dist/tool-approval.d.ts +296 -3
- package/dist/tool-approval.js +1066 -50
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.js +14 -1
- package/dist/trace/project.d.ts +55 -0
- package/dist/trace/project.js +135 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -137,12 +137,13 @@ The server is configured entirely through environment variables. The most import
|
|
|
137
137
|
| `SERVICE_AUTH_TOKEN` | — | Callers must send `Authorization: Bearer <token>` |
|
|
138
138
|
| `DB_BACKEND` | `local`* | `mysql` (any MySQL-protocol DB: MySQL/TiDB/MariaDB; `tidb` alias) / `pg` (PostgreSQL) / `local` (file-backed, no DB) / `memory` (explicit in-memory: nothing survives a restart, durable-runs faces 501). *Bare boot (no DB env at all) defaults to `local` so a single-user machine keeps its runs across restarts; any SQL signal (`SESSION_BACKEND` or `TIDB_/MYSQL_/PG_HOST`) keeps the `mysql` engine default, and `REQUIRE_PRINCIPAL=true` bare boots stay `memory` (the local file store has no tenant isolation — a warning says so). A DEFAULT-derived `local` that cannot create its data root degrades to memory with a warning + the `store_backend_degraded` gauge; an EXPLICIT `DB_BACKEND=local` fails loud instead. Setting `mysql`/`pg` explicitly also switches sessions to durable |
|
|
139
139
|
| `SESSION_BACKEND` | `memory`* | `memory` / `mysql` (durable session center; `tidb` alias) / `auto`. *Defaults to durable when `DB_BACKEND` is explicitly `mysql`/`pg` |
|
|
140
|
-
| `REMOTE_EXEC` | unset | Sandbox execution lane: `host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`; unset = in-process stub (with `CONFIG_PROVIDER=local` the default becomes `host`) |
|
|
140
|
+
| `REMOTE_EXEC` | unset | Sandbox execution lane: `host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`; unset = in-process stub (with `CONFIG_PROVIDER=local` the default becomes `host`). Naming a lane without its required env (e.g. `e2b` without `E2B_API_KEY`) or an unrecognized value **refuses to start** — no silent downgrade to the host/in-process lane (#157 fail-closed) |
|
|
141
141
|
| `CONFIG_PROVIDER` | unset | Config source: `local` (file-backed `config.d/`, single machine) / `remote` (registry control plane) |
|
|
142
142
|
| `DEFAULT_SCENARIO` | `code` | Default scenario when the request body names none |
|
|
143
143
|
| `SANDBOX_PKG_SOURCE` | `global` | Package sources inside sandboxes: `global` (official upstreams) / `cn` (China mirrors) / `custom` / `none` |
|
|
144
144
|
| `SENSITIVE_WRITE_PATTERNS` | core's recommended set | Sensitive-path write deny list; comma-separated value replaces the set, `off` disables |
|
|
145
145
|
| `MANUAL_MODE_SHELL_GATE` | unset (off) | `always`\|`classify` — tighten `Bash` into the approval chain, applied unconditionally at the governance layer (≥7.1.0: independent of client permission mode, lane, or settings presence) |
|
|
146
|
+
| `SCRATCHPAD_SWEEP_TTL_MS` | 7 days | Idle-reap window for per-session scratchpad dirs (by dir mtime; `0` disables). The scratchpad is **ephemeral by contract**: replica-local disk, NOT part of the durable-suspend persistence set — a resume on a different replica, or after a sweep, starts with an empty dir (same two-track posture as the Agent SDK hosting doc: conversation persists, working-directory artifacts don't). Raise/disable only on single-replica deployments that park approvals for longer than the window |
|
|
146
147
|
| `MODEL_CONNECT_TIMEOUT_MS` | `30000` | Gateway connect timeout |
|
|
147
148
|
| `MODEL_FIRST_TOKEN_TIMEOUT_MS` | `120000` | First-token timeout |
|
|
148
149
|
| `MODEL_IDLE_TIMEOUT_MS` | `300000` | Mid-stream idle timeout (`0` = off) |
|
package/README.zh-CN.md
CHANGED
|
@@ -130,7 +130,7 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
|
|
|
130
130
|
| `SERVICE_AUTH_TOKEN` | — | 调用方需带 `Authorization: Bearer <token>` |
|
|
131
131
|
| `DB_BACKEND` | `mysql` | SQL 引擎:`mysql`(任何 MySQL 协议库:MySQL/TiDB/MariaDB;`tidb` 为兼容别名)/ `pg`(PostgreSQL)/ `local`(免 DB 文件持久化)。显式设置 `mysql`/`pg` 时 session 自动转 durable |
|
|
132
132
|
| `SESSION_BACKEND` | `memory`* | `memory` / `mysql`(durable 会话中心;`tidb` 别名)/ `auto`。*显式 `DB_BACKEND=mysql/pg` 时默认转 durable |
|
|
133
|
-
| `REMOTE_EXEC` | 未设 | 沙箱执行通道:`host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`;未设 = 进程内 stub(`CONFIG_PROVIDER=local` 时缺省转 `host`) |
|
|
133
|
+
| `REMOTE_EXEC` | 未设 | 沙箱执行通道:`host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`;未设 = 进程内 stub(`CONFIG_PROVIDER=local` 时缺省转 `host`)。点名了通道但必需 env 不全(如 `e2b` 缺 `E2B_API_KEY`)或值不在闭集内 ⇒ **启动即拒**——不再静默降级到 host/进程内通道(#157 fail-closed) |
|
|
134
134
|
| `CONFIG_PROVIDER` | 未设 | 配置来源:`local`(单机文件 `config.d/`)/ `remote`(registry 控制面) |
|
|
135
135
|
| `DEFAULT_SCENARIO` | `code` | 请求体未指定场景时的缺省场景 |
|
|
136
136
|
| `SANDBOX_PKG_SOURCE` | `global` | 沙箱内装包源:`global`(官方源)/ `cn`(国内镜像)/ `custom` / `none` |
|
package/USAGE.md
CHANGED
|
@@ -316,6 +316,11 @@ FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启 flee
|
|
|
316
316
|
| `errorCode` | 程序化分支:如 `"conflict"`(乐观锁丢失,可重试) |
|
|
317
317
|
| `blockedReason` | `status=blocked` 时:为什么做不了(缺信息/权限) |
|
|
318
318
|
|
|
319
|
+
> ⚠️ **任务成败以 `status`/`errorCode` 为准,勿以 HTTP 状态码判**:同步腿 200=提交受理成功(任务可能
|
|
320
|
+
> `failed`+`errorCode`);`POST /v1/runs` 202=已受理(执行期结局落在终态 run 记录/`done` 帧)。执行期
|
|
321
|
+
> 错误结构上无法用 HTTP 状态码承载——流的响应头在 run 开跑前就发完了([2812]②b 口径,详见
|
|
322
|
+
> `docs/ASSISTANT-WIRE-CONTRACT.md` 附录 A)。
|
|
323
|
+
|
|
319
324
|
---
|
|
320
325
|
|
|
321
326
|
## 2. Hello world——同步发一个任务,拿到答案(最简单)
|
|
@@ -386,7 +391,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
|
386
391
|
| `GET /health` | 健康(无需鉴权) |
|
|
387
392
|
| `GET /metrics` | Prometheus 指标(有 token 时需带) |
|
|
388
393
|
|
|
389
|
-
> **operator 鉴权(审批队列)**:`OPERATOR_PRINCIPALS=ops:alice,ops:bob`(CSV)= 谁能当 operator——列任意 owner 待办 + 决议(批/否)
|
|
394
|
+
> **operator 鉴权(审批队列)**:`OPERATOR_PRINCIPALS=ops:alice,ops:bob`(CSV)= 谁能当 operator——列任意 owner 待办 + 决议(批/否)。**单租户部署不设=旧行为**(握 service token 即 operator,向后兼容);设了之后,非名单 principal 列待办只看自己的、且**不能决议**(403,防"请求方批自己的高危操作"绕过 F4 闸)。⚠️ **多租户形拒启**(#157-①):`DURABLE_APPROVAL=true` + `REQUIRE_PRINCIPAL=true` 而 `OPERATOR_PRINCIPALS` 空 ⇒ 进程启动失败并点名修法——否则空名单会让任一已验证租户读到其他租户的待批队列(读面 true-for-all)。设名单,或确属单租户则不设 `REQUIRE_PRINCIPAL`。
|
|
390
395
|
>
|
|
391
396
|
> **parked 后台子代的待办分两个 scope 桶**(durable 审批面,core 1.389 起):父任务显式转发审批范围的常规 ask 落在该范围的 scope 下(可预算);无转发时无人值守拦下的敏感操作 ask 落在按 principal 派生的隔离 scope 下(带缺省 deadline、永不自动放行)。operator 全量列表天然两桶全见;**按 `?owner` 过滤时注意两桶可能不同名**,展示面要两个都查。
|
|
392
397
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type AskState = "STREAM_PENDING" | "DECIDED" | "PARKING" | "PARKED" | "DENIED" | "VOID";
|
|
2
|
+
export type BatchState = "OPEN" | "ROUTING_UNBOUND" | "ROUTING_BOUND" | "ABORTED";
|
|
3
|
+
/** ask 侧终态集合(无出边的 4 态)——store 层 CAS 谓词与 reap/对账扫描复用这份闭集,不许各自重列。 */
|
|
4
|
+
export declare const ASK_TERMINAL: ReadonlySet<AskState>;
|
|
5
|
+
/** 批侧终态集合(无出边的 2 态)。 */
|
|
6
|
+
export declare const BATCH_TERMINAL: ReadonlySet<BatchState>;
|
|
7
|
+
/** ask 转移合法性(表驱动,含拒绝同值自环——STREAM_PENDING→STREAM_PENDING 不在表里,故 false)。 */
|
|
8
|
+
export declare function canAskTransition(from: AskState, to: AskState): boolean;
|
|
9
|
+
/** 批转移合法性,同形。 */
|
|
10
|
+
export declare function canBatchTransition(from: BatchState, to: BatchState): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* 🔴 身份元组的两条轴(车3 设计稿 §3.2′,2026-08-06 换轴,原轴 `(sourceTaskId, toolCallId, leg:number)`
|
|
13
|
+
* 被推翻)—— 这段注释是「为什么是这五个部件」的唯一书面记录:
|
|
14
|
+
*
|
|
15
|
+
* ── `runId`(新增轴,封死跨 run 复用)────────────────────────────────────────────────────────────────
|
|
16
|
+
* core 给 `AskRequest.sourceTaskId` 填的是 **sessionId**(`prepare-task.js:2595` 的 `askSourceIdentity`),
|
|
17
|
+
* 不是 wire run id。而原轴给每条新 run 的首腿都重置 `leg=0` ⇒ 同一会话的**下一次全新提交**若撞上同一
|
|
18
|
+
* `toolCallId`(design/172 定稿 §3.1 记录了部分网关按**请求编号**而非全局编号铸它)会整组重现,
|
|
19
|
+
* `ensureAsk` 的幂等 upsert 于是命中上一 run 已 `DECIDED(approve)` 的旧行并把它回放成 `true` ——
|
|
20
|
+
* 静默复用一次早已过去的人类同意。这不是低概率碰撞,是可确定复现的路径(§10 钉 C-1)。
|
|
21
|
+
* `runId` = 本 run 的 wire id(durable 腿 = 真 run uuid;adhoc 腿 = 该腿一次性 uuid):每次全新提交必是
|
|
22
|
+
* 新值,同一腿内恒定(重试/failover/闭包再入不变)⇒ 幂等仍成立。
|
|
23
|
+
*
|
|
24
|
+
* ── `legKey`(取代数值 `leg`,一腿一凭据)─────────────────────────────────────────────────────────────
|
|
25
|
+
* = `sha256(resume checkpoint token)` 的 hex,**首腿 = 空串**。一次 park→resume 的 token 就是这条腿的
|
|
26
|
+
* 天然身份:同 token 重投 = 同一腿(幂等,正确);新 park ⇒ 新 token ⇒ 新腿 ⇒ 新 askId。
|
|
27
|
+
* 取摘要而非原始 token 是因为 token 是能力凭据(本仓有 `stripCheckpointToken` 专门把它从可重放账本里
|
|
28
|
+
* 剥掉),而 `askId` 是 wire 可见值 ⇒ **只存/只喂摘要**。用它当轴顺带消掉了原裁需要的 `task_run` 加列
|
|
29
|
+
* 与 `markResuming` 返回加宽(后者有第二个调用点 `http/routes/runs.ts:463`,加宽会误杀已成功 resume 的
|
|
30
|
+
* running run)。残留边界:同一 run 内**不经 checkpoint token** 的重入形若未来出现会得到同一 legKey ——
|
|
31
|
+
* 今天不存在这种腿(run 内每次重入都必须经 checkpoint resolve),故不设兜底。
|
|
32
|
+
*/
|
|
33
|
+
/** ask_id = sha256('ask' ⊕ sourceTaskId ⊕ runId ⊕ toolCallId ⊕ legKey ⊕ parentToolCallId).slice(0,64)。
|
|
34
|
+
* 确定性:同参数元组(含重试/failover/闭包再入)永远派生同一个 id——这是 ensureAsk 幂等 upsert 成立的前提。 */
|
|
35
|
+
export declare function deriveAskId(sourceTaskId: string, runId: string, toolCallId: string, legKey: string, parentToolCallId?: string): string;
|
|
36
|
+
/** batch_id = sha256('batch' ⊕ sourceTaskId ⊕ runId ⊕ legKey).slice(0,64)。同一 (sourceTaskId, runId,
|
|
37
|
+
* legKey) 恒派生同一批——批 = 同一决策点的兄弟 ask 集合,故不掺 toolCallId。 */
|
|
38
|
+
export declare function deriveBatchId(sourceTaskId: string, runId: string, legKey: string): string;
|
|
39
|
+
//# sourceMappingURL=approval-ask-machine.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 流内审批协议(design/172 v3.1 §3.0)持久层状态机 —— #151 车1。纯函数,零 IO:态词表 + 合法转移表 +
|
|
3
|
+
* 判定函数 + 确定性 id 派生。所有落库/网络/时钟一律不在本文件——store 层(approval-ask-store-*.ts)消费
|
|
4
|
+
* 这里的 `canAskTransition`/`canBatchTransition` 做 SQL 前置合法性判定,`deriveAskId`/`deriveBatchId`
|
|
5
|
+
* 做身份键计算。
|
|
6
|
+
*
|
|
7
|
+
* ── 态词表(闭集)────────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
* ask 级 6 态:STREAM_PENDING(流内待决)| DECIDED(已回决)| PARKING(窗到期中选、正在转投递面)|
|
|
9
|
+
* PARKED(已绑定投递坐标,等待走 gate)| DENIED(已拒)| VOID(非中选兄弟收敛/撤卡终态)。
|
|
10
|
+
* 批级 4 态:OPEN(接受回决)| ROUTING_UNBOUND(已转投递面,尚未绑定具体 ask)|
|
|
11
|
+
* ROUTING_BOUND(bind-once 后已绑定)| ABORTED(整批撤卡)。
|
|
12
|
+
*
|
|
13
|
+
* ── 合法转移表(design/172 §3.0 图,逐字照抄,全枚举——本文件是唯一真源)────────────────────────
|
|
14
|
+
* ask: STREAM_PENDING → DECIDED | PARKING | VOID
|
|
15
|
+
* PARKING → PARKED | DENIED | VOID(VOID = 非中选兄弟收敛)
|
|
16
|
+
* 其余(4 个终态) → ∅ —— 唯一例外是 §3.0/§4 的 provisional 版本化补偿:那条路径专走
|
|
17
|
+
* store 的 `resolveProvisional`(WHERE …AND provisional=1…),故意**不**经过这张表——
|
|
18
|
+
* 补偿允许终态→终态,而这张表把终态定义为「无出边」,两者不矛盾(不同的门,不同的谓词)。
|
|
19
|
+
* 批: OPEN → ROUTING_UNBOUND | ABORTED
|
|
20
|
+
* ROUTING_UNBOUND → ROUTING_BOUND | ABORTED
|
|
21
|
+
* 其余(2 个终态) → ∅
|
|
22
|
+
*
|
|
23
|
+
* 🔴 施工纪律:表驱动,禁 if 链——`canAskTransition`/`canBatchTransition` 只做一次数组查找,新增/
|
|
24
|
+
* 删除一条转移只需要改上面两张 `Record<State, readonly State[]>` 字面量,不需要碰判定函数本体。
|
|
25
|
+
*/
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
/** ask 侧终态集合(无出边的 4 态)——store 层 CAS 谓词与 reap/对账扫描复用这份闭集,不许各自重列。 */
|
|
28
|
+
export const ASK_TERMINAL = new Set(["DECIDED", "PARKED", "DENIED", "VOID"]);
|
|
29
|
+
/** 批侧终态集合(无出边的 2 态)。 */
|
|
30
|
+
export const BATCH_TERMINAL = new Set(["ROUTING_BOUND", "ABORTED"]);
|
|
31
|
+
const ASK_TRANSITIONS = {
|
|
32
|
+
STREAM_PENDING: ["DECIDED", "PARKING", "VOID"],
|
|
33
|
+
PARKING: ["PARKED", "DENIED", "VOID"],
|
|
34
|
+
DECIDED: [],
|
|
35
|
+
PARKED: [],
|
|
36
|
+
DENIED: [],
|
|
37
|
+
VOID: [],
|
|
38
|
+
};
|
|
39
|
+
const BATCH_TRANSITIONS = {
|
|
40
|
+
OPEN: ["ROUTING_UNBOUND", "ABORTED"],
|
|
41
|
+
ROUTING_UNBOUND: ["ROUTING_BOUND", "ABORTED"],
|
|
42
|
+
ROUTING_BOUND: [],
|
|
43
|
+
ABORTED: [],
|
|
44
|
+
};
|
|
45
|
+
/** ask 转移合法性(表驱动,含拒绝同值自环——STREAM_PENDING→STREAM_PENDING 不在表里,故 false)。 */
|
|
46
|
+
export function canAskTransition(from, to) {
|
|
47
|
+
return ASK_TRANSITIONS[from].includes(to);
|
|
48
|
+
}
|
|
49
|
+
/** 批转移合法性,同形。 */
|
|
50
|
+
export function canBatchTransition(from, to) {
|
|
51
|
+
return BATCH_TRANSITIONS[from].includes(to);
|
|
52
|
+
}
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// 确定性 id 派生(design/172 §3.1 / 本车设计定稿 §5)
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* 分隔符用 NUL(U+0000)而非任何可打印字符:各 id 部件(sourceTaskId/toolCallId/…)是外部系统给的
|
|
58
|
+
* 不透明字符串,不能假设它们不含冒号/空格/其他候选分隔符——黑板有 sessionId 用单字符分隔拼接被
|
|
59
|
+
* 注入撞键的既往 finding(同族修法),NUL 是唯一一个这些部件字段实际上不会自然出现的字节,
|
|
60
|
+
* 所以拼接后不同的 (parts) 元组不可能被撞成同一条 pre-hash 字符串。
|
|
61
|
+
*/
|
|
62
|
+
const ID_SEPARATOR = "\u0000";
|
|
63
|
+
/** parts 用 NUL 拼接 + sha256 定长——哈希输出恒 64 hex 字符,`.slice(0, 64)` 是防御性写法而非实际截断
|
|
64
|
+
* (VARCHAR(255) 列宽远超过 64,这里的「截 64」纯粹是把长度收拢到一个恒定值,不依赖 sha256 实现细节)。 */
|
|
65
|
+
function deterministicId(prefix, parts) {
|
|
66
|
+
const joined = [prefix, ...parts.map((p) => p ?? "")].join(ID_SEPARATOR);
|
|
67
|
+
return createHash("sha256").update(joined, "utf8").digest("hex").slice(0, 64);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 🔴 身份元组的两条轴(车3 设计稿 §3.2′,2026-08-06 换轴,原轴 `(sourceTaskId, toolCallId, leg:number)`
|
|
71
|
+
* 被推翻)—— 这段注释是「为什么是这五个部件」的唯一书面记录:
|
|
72
|
+
*
|
|
73
|
+
* ── `runId`(新增轴,封死跨 run 复用)────────────────────────────────────────────────────────────────
|
|
74
|
+
* core 给 `AskRequest.sourceTaskId` 填的是 **sessionId**(`prepare-task.js:2595` 的 `askSourceIdentity`),
|
|
75
|
+
* 不是 wire run id。而原轴给每条新 run 的首腿都重置 `leg=0` ⇒ 同一会话的**下一次全新提交**若撞上同一
|
|
76
|
+
* `toolCallId`(design/172 定稿 §3.1 记录了部分网关按**请求编号**而非全局编号铸它)会整组重现,
|
|
77
|
+
* `ensureAsk` 的幂等 upsert 于是命中上一 run 已 `DECIDED(approve)` 的旧行并把它回放成 `true` ——
|
|
78
|
+
* 静默复用一次早已过去的人类同意。这不是低概率碰撞,是可确定复现的路径(§10 钉 C-1)。
|
|
79
|
+
* `runId` = 本 run 的 wire id(durable 腿 = 真 run uuid;adhoc 腿 = 该腿一次性 uuid):每次全新提交必是
|
|
80
|
+
* 新值,同一腿内恒定(重试/failover/闭包再入不变)⇒ 幂等仍成立。
|
|
81
|
+
*
|
|
82
|
+
* ── `legKey`(取代数值 `leg`,一腿一凭据)─────────────────────────────────────────────────────────────
|
|
83
|
+
* = `sha256(resume checkpoint token)` 的 hex,**首腿 = 空串**。一次 park→resume 的 token 就是这条腿的
|
|
84
|
+
* 天然身份:同 token 重投 = 同一腿(幂等,正确);新 park ⇒ 新 token ⇒ 新腿 ⇒ 新 askId。
|
|
85
|
+
* 取摘要而非原始 token 是因为 token 是能力凭据(本仓有 `stripCheckpointToken` 专门把它从可重放账本里
|
|
86
|
+
* 剥掉),而 `askId` 是 wire 可见值 ⇒ **只存/只喂摘要**。用它当轴顺带消掉了原裁需要的 `task_run` 加列
|
|
87
|
+
* 与 `markResuming` 返回加宽(后者有第二个调用点 `http/routes/runs.ts:463`,加宽会误杀已成功 resume 的
|
|
88
|
+
* running run)。残留边界:同一 run 内**不经 checkpoint token** 的重入形若未来出现会得到同一 legKey ——
|
|
89
|
+
* 今天不存在这种腿(run 内每次重入都必须经 checkpoint resolve),故不设兜底。
|
|
90
|
+
*/
|
|
91
|
+
/** ask_id = sha256('ask' ⊕ sourceTaskId ⊕ runId ⊕ toolCallId ⊕ legKey ⊕ parentToolCallId).slice(0,64)。
|
|
92
|
+
* 确定性:同参数元组(含重试/failover/闭包再入)永远派生同一个 id——这是 ensureAsk 幂等 upsert 成立的前提。 */
|
|
93
|
+
export function deriveAskId(sourceTaskId, runId, toolCallId, legKey, parentToolCallId) {
|
|
94
|
+
return deterministicId("ask", [sourceTaskId, runId, toolCallId, legKey, parentToolCallId]);
|
|
95
|
+
}
|
|
96
|
+
/** batch_id = sha256('batch' ⊕ sourceTaskId ⊕ runId ⊕ legKey).slice(0,64)。同一 (sourceTaskId, runId,
|
|
97
|
+
* legKey) 恒派生同一批——批 = 同一决策点的兄弟 ask 集合,故不掺 toolCallId。 */
|
|
98
|
+
export function deriveBatchId(sourceTaskId, runId, legKey) {
|
|
99
|
+
return deterministicId("batch", [sourceTaskId, runId, legKey]);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=approval-ask-machine.js.map
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #151 车3(design/172 流内审批协议)—— **审批卡的 schema 属主 + 重放腿的纯函数**。
|
|
3
|
+
*
|
|
4
|
+
* 本模块只出两样东西,**都不带 IO、不带定时器、不发帧**:
|
|
5
|
+
* 1. `ApprovalCardSchema` / `ApprovalCardEnvelopeSchema` —— 落库 `approval_asks.card_json` 与一切读面
|
|
6
|
+
* (重放腿=刀 3c、回决端点=车4、对账扫描=车5)共用的**同一份** schema。写侧(刀 3b 的 `ensureAsk`)
|
|
7
|
+
* 与读侧走同一个符号,于是「存的形」与「读的形」不可能各自漂。
|
|
8
|
+
* 2. `buildReplayFrame` / `buildApprovalPreamble` —— 行 → 帧的**纯投影**(设计稿 §5.2)。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 为什么读面必须 `safeParse` 而不是裸 as-cast:`card_json` 是 JSON 列,驱动回读的是 `unknown`。
|
|
11
|
+
* 宪法 [2704](边界必 schema / 禁裸 as-cast / schema 单一属主)在这里是硬约束——一条形状漂了的历史行
|
|
12
|
+
* 若被 as-cast 成 `ApprovalCard`,它会带着 `undefined` 字段一路走到 wire 上,消费端拿到的是「结构上
|
|
13
|
+
* 合法、语义上空」的卡。`safeParse` 让这种行**当场落地为「跳过 + 一次 warn」**(§5.3)。
|
|
14
|
+
*
|
|
15
|
+
* schema 属主裁定(设计稿 §12-2,属主 §14 裁 (a) 变体):v1 的 schema 属主 = server 本仓,server 加
|
|
16
|
+
* **zod 直依赖**(此前 zod 只经 `@sema-agent/registry-core` 传递到场)。抽进 `@sema-agent/registry-core`
|
|
17
|
+
* 的时机 = 出现第二个**运行期**消费者(cli 呈卡校验排期时)——届时是「属主迁移」而不是「复制形状」,
|
|
18
|
+
* 不违单一属主宪法;此刻抽包 = 为不存在的消费者发一轮 registry-core 版本 + floor bump,零收益。
|
|
19
|
+
*
|
|
20
|
+
* 命名(CLAUDE.md 工厂命名律):本文件全部是 `build*` —— 返回值是纯数据(帧对象 / 帧数组 / 计数),
|
|
21
|
+
* 没有方法、没有捕获的行为。
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import type { AskRow } from "./plugins/approval-ask-store-sql.js";
|
|
25
|
+
/** 模型自由文本(`sourceAgentName` / `delegation.agentName`)的限长(设计稿 §6.2)。设计 §3.1 把
|
|
26
|
+
* 「限长 + 脱敏」写成 **server 新增责任**(引擎无此层):spawning model 挑的名字是自由文本,
|
|
27
|
+
* 不能指望壳去截——一条 100KB 的 agentName 在 server 侧就该被拒,而不是变成一张撑爆呈卡面的卡。 */
|
|
28
|
+
export declare const MAX_AGENT_NAME = 200;
|
|
29
|
+
/**
|
|
30
|
+
* design/172 §3.1 的**中性投影**——呈卡面看到的全部内容,与 wire 面的既有 `ToolApprovalFrame` 解耦。
|
|
31
|
+
*
|
|
32
|
+
* `risk` 的三态形(设计稿 §14.1,core [2794] 回帖后定):`AskRequest.riskAxes?.{irreversible,egress}`
|
|
33
|
+
* 是 **additive optional**,**缺席 = 引擎未判,不是「安全」**。所以两轴在这里是 `optional()`:
|
|
34
|
+
* `true` / `false` / **缺席(未标注)** 三态各自可分,投影层**禁把缺席折算成 false** —— 那等于替引擎
|
|
35
|
+
* 打包票。`requiresRealApproval` 是 core 今天唯一在场的粗粒度安全类标记(必填,车2 已有真值来源)。
|
|
36
|
+
*/
|
|
37
|
+
export declare const ApprovalCardSchema: z.ZodObject<{
|
|
38
|
+
toolName: z.ZodString;
|
|
39
|
+
message: z.ZodString;
|
|
40
|
+
args: z.ZodOptional<z.ZodUnknown>;
|
|
41
|
+
argsOmitted: z.ZodOptional<z.ZodLiteral<true>>;
|
|
42
|
+
toolCallId: z.ZodOptional<z.ZodString>;
|
|
43
|
+
risk: z.ZodObject<{
|
|
44
|
+
irreversible: z.ZodOptional<z.ZodBoolean>;
|
|
45
|
+
egress: z.ZodOptional<z.ZodBoolean>;
|
|
46
|
+
requiresRealApproval: z.ZodBoolean;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
|
|
49
|
+
sourceTaskId: z.ZodOptional<z.ZodString>;
|
|
50
|
+
sourceAgentName: z.ZodOptional<z.ZodString>;
|
|
51
|
+
delegation: z.ZodOptional<z.ZodObject<{
|
|
52
|
+
parentToolCallId: z.ZodString;
|
|
53
|
+
depth: z.ZodNumber;
|
|
54
|
+
agentName: z.ZodOptional<z.ZodString>;
|
|
55
|
+
}, z.core.$strip>>;
|
|
56
|
+
}, z.core.$strict>;
|
|
57
|
+
export type ApprovalCard = z.infer<typeof ApprovalCardSchema>;
|
|
58
|
+
/**
|
|
59
|
+
* `approval_asks.card_json` 里真正存的东西(设计稿 §6.2)。存**信封**而不是裸卡的理由:重放帧要回填
|
|
60
|
+
* 车2 铸的 `approvalId`(wire 面的回决通道桥 + 消费端与并行 `tool_approval` 帧的去重键),而它不属于
|
|
61
|
+
* 「卡的内容」——放进卡里会让 schema 的语义边界糊掉。`schemaVersion` 是形的版本闩(与行上的
|
|
62
|
+
* `schema_version` 列同值,列供 SQL 侧过滤,信封里这份供读面在解出内容**之前**判形)。
|
|
63
|
+
*/
|
|
64
|
+
export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
|
|
65
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
66
|
+
approvalId: z.ZodString;
|
|
67
|
+
card: z.ZodObject<{
|
|
68
|
+
toolName: z.ZodString;
|
|
69
|
+
message: z.ZodString;
|
|
70
|
+
args: z.ZodOptional<z.ZodUnknown>;
|
|
71
|
+
argsOmitted: z.ZodOptional<z.ZodLiteral<true>>;
|
|
72
|
+
toolCallId: z.ZodOptional<z.ZodString>;
|
|
73
|
+
risk: z.ZodObject<{
|
|
74
|
+
irreversible: z.ZodOptional<z.ZodBoolean>;
|
|
75
|
+
egress: z.ZodOptional<z.ZodBoolean>;
|
|
76
|
+
requiresRealApproval: z.ZodBoolean;
|
|
77
|
+
}, z.core.$strict>;
|
|
78
|
+
fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
|
|
79
|
+
sourceTaskId: z.ZodOptional<z.ZodString>;
|
|
80
|
+
sourceAgentName: z.ZodOptional<z.ZodString>;
|
|
81
|
+
delegation: z.ZodOptional<z.ZodObject<{
|
|
82
|
+
parentToolCallId: z.ZodString;
|
|
83
|
+
depth: z.ZodNumber;
|
|
84
|
+
agentName: z.ZodOptional<z.ZodString>;
|
|
85
|
+
}, z.core.$strip>>;
|
|
86
|
+
}, z.core.$strict>;
|
|
87
|
+
}, z.core.$strict>;
|
|
88
|
+
export type ApprovalCardEnvelope = z.infer<typeof ApprovalCardEnvelopeSchema>;
|
|
89
|
+
/** 本车铸的信封形版本(写侧=刀 3b,读侧=本文件)。 */
|
|
90
|
+
export declare const APPROVAL_CARD_SCHEMA_VERSION = 1;
|
|
91
|
+
/**
|
|
92
|
+
* `buildApprovalCard` 的入参 —— 已经过车2 洗涤(`redactDeep`/`redactSecrets`)与**字节帽**
|
|
93
|
+
* (`Buffer.byteLength`,设计稿 §6.5)的那一份投影素材。
|
|
94
|
+
*
|
|
95
|
+
* 🔴 为什么结构声明而不是 `import type { ToolApprovalFrame }`:那会让本模块反向依赖 `tool-approval.ts`
|
|
96
|
+
* (它已经 import 本模块的 `buildRevokeFrame`),形成一个纯为取一个类型而存在的环。结构形同时让「洗涤
|
|
97
|
+
* 与帽在**上游**已经做完」成为签名上的事实——本函数不 redact、不数字节,它只投影(纯数据 `build*`)。
|
|
98
|
+
* 既有的 `ToolApprovalFrame` 值结构上满足本接口(多出的 `type`/`approvalId` 两键在变量传参下无碍),
|
|
99
|
+
* 于是「wire 帧与 card_json 用同一份已洗素材」是**结构上**成立的,不是靠两处各算一遍的巧合(§6.5)。
|
|
100
|
+
*/
|
|
101
|
+
export interface ApprovalCardSource {
|
|
102
|
+
/** 缺席折空串——`ToolApprovalFrame.toolName` 在 wire 类型上是 optional(闭合帧不带),而卡上必填。 */
|
|
103
|
+
toolName?: string;
|
|
104
|
+
/** 已 redact。 */
|
|
105
|
+
message?: string;
|
|
106
|
+
/** 已 redactDeep + 字节帽;省略时 `argsOmitted` 为真。 */
|
|
107
|
+
args?: unknown;
|
|
108
|
+
argsOmitted?: boolean;
|
|
109
|
+
toolCallId?: string;
|
|
110
|
+
fromSubagent?: true;
|
|
111
|
+
sourceTaskId?: string;
|
|
112
|
+
/** 已 redactSecrets。 */
|
|
113
|
+
sourceAgentName?: string;
|
|
114
|
+
delegation?: {
|
|
115
|
+
parentToolCallId: string;
|
|
116
|
+
depth: number;
|
|
117
|
+
agentName?: string;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* design/172 §3.1 的**中性投影**(设计稿 §6.2)—— 写侧的唯一铸造点。
|
|
122
|
+
*
|
|
123
|
+
* `risk` 三态(§14.1):两轴 `optional`,`true`/`false`/**缺席(未标注)** 各自可分。`req` 是 core 交来的
|
|
124
|
+
* `AskRequest`(可能带、也可能不带 `riskAxes`),按 `unknown` 窄读;`requiresRealApproval` 是 core 今天
|
|
125
|
+
* 唯一在场的粗粒度安全类标记,缺席 = 这不是一次安全类 ask(core 的铸造点语义,不是我们的折算)。
|
|
126
|
+
*/
|
|
127
|
+
export declare function buildApprovalCard(source: ApprovalCardSource, req: unknown, requiresRealApproval: boolean): ApprovalCard;
|
|
128
|
+
/** 落库信封的纯构造(写侧;读侧 = `ApprovalCardEnvelopeSchema.safeParse`,**同一个 schema**)。 */
|
|
129
|
+
export declare function buildApprovalCardEnvelope(approvalId: string, card: ApprovalCard): ApprovalCardEnvelope;
|
|
130
|
+
/**
|
|
131
|
+
* design/172 §3.1 的呈卡帧(设计稿 §4.1)—— **live 铸造**形(重放形见 {@link buildReplayFrame},
|
|
132
|
+
* 两者投的是同一种帧,区别只在 `card` 的来源:这里是刚投影出来的,那里是从行上回读的)。
|
|
133
|
+
*
|
|
134
|
+
* `expiresAtMs` **入参**而不是在这里现算:同一只 ask 的窗**只铸一次**(§3.1「永不赋新 deadline」),
|
|
135
|
+
* 调用点已经算好了它(既是定时器时长的来源,也是落库 `expires_at_ms` 的值)——在这里重算会铸出第二个
|
|
136
|
+
* 真源,而两个真源必然漂。
|
|
137
|
+
*/
|
|
138
|
+
export declare function buildApprovalRequestFrame(input: {
|
|
139
|
+
askId: string;
|
|
140
|
+
taskId: string;
|
|
141
|
+
approvalId: string;
|
|
142
|
+
card: ApprovalCard;
|
|
143
|
+
expiresAtMs: number;
|
|
144
|
+
nowMs: number;
|
|
145
|
+
}): ApprovalRequestFrame;
|
|
146
|
+
/**
|
|
147
|
+
* design/172 §3.1 的呈卡帧(设计稿 §4.1 逐字)。
|
|
148
|
+
*
|
|
149
|
+
* 与既有 `tool_approval` 帧是**并行加帧、不替换**(§4.2):存量壳只认旧帧,新帧是闭集加员;两帧同带
|
|
150
|
+
* `approvalId`,消费端据它去重。
|
|
151
|
+
*
|
|
152
|
+
* 🔴 铸造/发射**不在刀 3c**(那是 3b 的域)。本文件只出「持久行 → 帧」的重放投影 `buildReplayFrame`。
|
|
153
|
+
*/
|
|
154
|
+
export interface ApprovalRequestFrame {
|
|
155
|
+
/** = SSE 具名事件名(与 `tool_approval` 同约定)。 */
|
|
156
|
+
type: "approval_request";
|
|
157
|
+
schemaVersion: 1;
|
|
158
|
+
/** `deriveAskId` 派生(持久层主键;五元组见 approval-ask-machine.ts)。 */
|
|
159
|
+
askId: string;
|
|
160
|
+
/** wire run id(= 协调器 ctx 的 taskId)。 */
|
|
161
|
+
taskId: string;
|
|
162
|
+
/** v1 闭集单员("content" 留位不实现)。 */
|
|
163
|
+
kind: "permission";
|
|
164
|
+
card: ApprovalCard;
|
|
165
|
+
/** `max(0, expiresAtMs - serverNowMs)` —— 单调减,**永不续窗**。 */
|
|
166
|
+
expiresInMs: number;
|
|
167
|
+
/** 一次铸定、逐字回读,**永不重算**(§3.1「永不赋新 deadline、永不重启窗」)。 */
|
|
168
|
+
expiresAtMs: number;
|
|
169
|
+
serverNowMs: number;
|
|
170
|
+
/**
|
|
171
|
+
* server 扩(不在设计字面):车4 落地前的**回决通道桥**——现行
|
|
172
|
+
* `POST /v1/tool-approvals/:id/respond` 的 id。也是消费端把本帧与并行的 `tool_approval` 帧去重的键。
|
|
173
|
+
* 车4 上线后仍保留(旧壳过渡窗)。
|
|
174
|
+
*/
|
|
175
|
+
approvalId: string;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* design/172 §3.3 的**批级撤卡帧**(车6;设计稿 §3 逐字)。
|
|
179
|
+
*
|
|
180
|
+
* 语义:本批里 `askIds` 列出的那些卡**已经作废**,壳按白名单清掉它们(未知 `reason` 按通用撤卡处理)。
|
|
181
|
+
* 已 `DECIDED` 的兄弟**永远不在** `askIds` 里(§3.0「已 DECIDED 不受撤卡影响」)——名单恒等于持久层
|
|
182
|
+
* 那一次原子事务真正翻成 `VOID` 的行集(`expireAsk`/`bindBatch` 的 `voidedSiblings` 返回形),不是
|
|
183
|
+
* 「本批全体」的推断。
|
|
184
|
+
*
|
|
185
|
+
* 🔴 **发射面 = live emit only,不进 durable tail**(设计稿 §8 C-3/C-5):撤卡帧由 reaper 腿/收敛器
|
|
186
|
+
* 这类**没有账本上下文**的调用点产出,那里没有 seq 可分配,硬塞进账本会铸出一条序号伪造的行。丢帧的
|
|
187
|
+
* 结构补偿 = 172 §3.1 的「preamble 是壳侧卡集**全量对账基准**,不在基准内的一律清」——壳重连时的
|
|
188
|
+
* preamble 本身就会让一张没收到撤卡帧的僵尸卡消失。
|
|
189
|
+
*
|
|
190
|
+
* v1 **不带 per-ask 终态**(§7-4 裁定):中选者另有 `PARKED` 的下行面(410 体 + 重放消失),壳按
|
|
191
|
+
* `askIds` 清卡即可。
|
|
192
|
+
*/
|
|
193
|
+
export interface ApprovalRevokeFrame {
|
|
194
|
+
/** = SSE 具名事件名(同 `approval_request` 约定)。 */
|
|
195
|
+
type: "approval_revoke";
|
|
196
|
+
schemaVersion: 1;
|
|
197
|
+
batchId: string;
|
|
198
|
+
/** 本次被撤(翻成 `VOID`)的 ask 集;空集**不发帧**(调用方判,见各发射点)。 */
|
|
199
|
+
askIds: string[];
|
|
200
|
+
/** `superseded_by_park` = 降级连坐(窗到期/收敛器绑定中选者,兄弟被撤);`aborted` = run 取消。 */
|
|
201
|
+
reason: "superseded_by_park" | "aborted";
|
|
202
|
+
serverNowMs: number;
|
|
203
|
+
}
|
|
204
|
+
/** 撤卡帧的纯构造(`build*`:返回纯数据,无方法无捕获行为)。`askIds` 拷贝一份 —— 调用方传进来的多是
|
|
205
|
+
* store 返回的数组,帧不该与它共享可变引用。 */
|
|
206
|
+
export declare function buildRevokeFrame(batchId: string, askIds: readonly string[], reason: ApprovalRevokeFrame["reason"], serverNowMs: number): ApprovalRevokeFrame;
|
|
207
|
+
/**
|
|
208
|
+
* 持久行 → 重放帧(设计稿 §5.2 逐字)。
|
|
209
|
+
*
|
|
210
|
+
* 三条不变量都在这几行里:
|
|
211
|
+
* - **坏行不炸开流**:`safeParse` 失败 ⇒ 返回 `undefined`(调用方跳过 + 记一次 warn,§5.3)。
|
|
212
|
+
* - **不续窗**:`expiresAtMs` 逐字回读,**永不重铸**;`expiresInMs` 由它与 `nowMs` 现算 ⇒ 同一张卡
|
|
213
|
+
* 连续两次重放必然严格单调减(机器判据 = §10 钉 B-2)。
|
|
214
|
+
* - **零写**:本函数不碰 store —— 重放腿只读,不 `transitionAsk`、不改 `expires_at_ms`、不重挂定时器。
|
|
215
|
+
* 过窗未收敛的行(`expiresInMs === 0`)**仍然重放**,不在这里代打 `expireAsk` CAS(那会制造第五个
|
|
216
|
+
* 竞争者;收敛是车2 的窗到期竞争者与车5 恢复扫描的职责)。
|
|
217
|
+
*/
|
|
218
|
+
export declare function buildReplayFrame(row: AskRow, nowMs: number): ApprovalRequestFrame | undefined;
|
|
219
|
+
/** {@link buildApprovalPreamble} 的返回形——帧 + 两个**计数**(调用方按自己的 logger 记 warn;本模块
|
|
220
|
+
* 保持纯函数,不吃 logger:那会让一个 `build*` 捕获行为)。 */
|
|
221
|
+
export interface ApprovalPreamble {
|
|
222
|
+
/** 按 `createdAtMs` 升序(= 两个读口的 SQL `ORDER BY` 与 InMemory twin 的排序),截帽后仍是升序。 */
|
|
223
|
+
frames: ApprovalRequestFrame[];
|
|
224
|
+
/** `card_json` 形不合(safeParse 失败)被跳过的行数。>0 ⇒ 调用方记一次 warn。 */
|
|
225
|
+
skipped: number;
|
|
226
|
+
/** 超 `replayMax` 被丢弃的**最旧**卡数。>0 ⇒ 调用方记一次 warn(§5.3 读面帽)。 */
|
|
227
|
+
dropped: number;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 开流 preamble 的行集 → 帧集(设计稿 §5.3 的读面帽 + 坏行跳过)。
|
|
231
|
+
*
|
|
232
|
+
* 帽的方向:超限时**只投最新的 N 张**——未决卡的价值随时间倒序递减(最老的那些多半已经在别处过窗/
|
|
233
|
+
* 被收敛),而壳的呈卡面容量有限。丢弃是**读面**行为,不写库、不改状态;写侧的准入门(设计稿 §0 X-2)
|
|
234
|
+
* 是另一件事,在刀 3b。
|
|
235
|
+
*
|
|
236
|
+
* `replayMax <= 0` 视作「不投」(运维显式关掉重放),不当成「无帽」。
|
|
237
|
+
*/
|
|
238
|
+
export declare function buildApprovalPreamble(rows: readonly AskRow[], nowMs: number, replayMax: number): ApprovalPreamble;
|
|
239
|
+
/** preamble 帧的 SSE 投递形(具名事件,**不带 `id:`** —— 它不是账本行,不参与 Last-Event-ID 游标)。 */
|
|
240
|
+
export declare function buildApprovalPreambleSseFrames(frames: readonly ApprovalRequestFrame[]): Array<{
|
|
241
|
+
event: string;
|
|
242
|
+
data: unknown;
|
|
243
|
+
}>;
|
|
244
|
+
//# sourceMappingURL=approval-card.d.ts.map
|