@sema-agent/server 7.36.0-rc.1 → 7.37.0-rc.1
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 +14 -0
- package/dist/boot/resolve-spec.js +6 -1
- package/dist/boot/runner-deps.d.ts +21 -3
- package/dist/boot/runner-deps.js +31 -4
- package/dist/boot/session-faces.d.ts +3 -0
- package/dist/boot/session-faces.js +11 -2
- package/dist/boot/side-query-lane.d.ts +77 -54
- package/dist/boot/side-query-lane.js +102 -66
- package/dist/boot/stores.d.ts +1 -0
- package/dist/boot/stores.js +19 -1
- package/dist/boot/task-list-lane.d.ts +92 -0
- package/dist/boot/task-list-lane.js +63 -0
- package/dist/bounded-session-map.d.ts +3 -0
- package/dist/bounded-session-map.js +5 -0
- package/dist/capabilities/scenarios.d.ts +30 -2
- package/dist/capabilities/scenarios.js +16 -3
- package/dist/config-types.d.ts +21 -1
- package/dist/config.js +8 -1
- package/dist/hooks/branch-transcript.d.ts +3 -2
- package/dist/hooks/branch-transcript.js +7 -1
- package/dist/http/active-run-conflict.d.ts +43 -0
- package/dist/http/active-run-conflict.js +8 -0
- package/dist/http/route-ctx.d.ts +22 -1
- package/dist/http/routes/admin-drain.d.ts +4 -2
- package/dist/http/routes/approvals-assistant.js +13 -1
- package/dist/http/routes/notify-wake.js +6 -0
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/tasks.js +30 -2
- package/dist/http/server.d.ts +3 -1
- package/dist/http/server.js +108 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -0
- package/dist/main.js +11 -3
- package/dist/memory-posture.d.ts +12 -1
- package/dist/memory-posture.js +2 -0
- package/dist/observability/fail-open.d.ts +9 -1
- package/dist/observability/fail-open.js +9 -1
- package/dist/plugins/memory-engine-pg.js +42 -4
- package/dist/plugins/memory-engine-tidb.js +38 -4
- package/dist/plugins/memory-origin-law.d.ts +69 -0
- package/dist/plugins/memory-origin-law.js +98 -0
- package/dist/plugins/retention-store-sql.d.ts +7 -0
- package/dist/plugins/retention-store-sql.js +24 -0
- package/dist/plugins/task-list-store-sql.d.ts +36 -25
- package/dist/plugins/task-list-store-sql.js +102 -0
- package/dist/run-local.js +14 -5
- package/dist/runs.js +17 -0
- package/dist/store-live-probe.d.ts +26 -0
- package/dist/store-live-probe.js +60 -0
- package/dist/trace/engine-notice-wire.d.ts +128 -0
- package/dist/trace/engine-notice-wire.js +256 -0
- package/dist/trace/ledger-events.d.ts +11 -1
- package/dist/trace/ledger-sink.d.ts +17 -0
- package/dist/trace/ledger-sink.js +25 -0
- package/package.json +2 -2
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `engine_notice` wire 腿(#310,设计稿 = 黑板 [4630],core 表态 [4631] / cli 表态 [4634])——
|
|
3
|
+
* core `EngineNotice` 的**第二终点**:按 sessionId 路由到那条会话的事件投影(SSE + durable 账本)。
|
|
4
|
+
*
|
|
5
|
+
* ## 为什么是白名单而不是全族上 wire
|
|
6
|
+
* `RunnerDeps.onNotice` 是 **deps 级单 sink**(一部署一只),`EngineNotice` 上**没有 runId**——
|
|
7
|
+
* 归属只能靠 `detail.sessionId`。于是:
|
|
8
|
+
* · `config.*` 族一个都不带 sessionId ⇒ 结构性无处投(投给谁都是猜);
|
|
9
|
+
* · core 侧**按 sink 去重**的码(`config.read_face_deployment_clamped` / `config.tool_model_gate_*`)
|
|
10
|
+
* 一旦上 per-session 流,语义当场翻面:共享 sink 意味着整进程只发一次,第二条会话的用户看到的是
|
|
11
|
+
* 「随机缺席」而不是「没发生」;
|
|
12
|
+
* · 运维码(`config.env_timeout_discarded` 一族)推给终端用户 = 跨租户串台面。
|
|
13
|
+
* 白名单里的三码,core 侧的去重单位分别是 **session / harvest / prepared-leg**(不是 sink),所以它们
|
|
14
|
+
* 上 per-session 流不产生上面第二条的翻面——这正是白名单避开去重陷阱的判据,不是随手挑的三个码。
|
|
15
|
+
*
|
|
16
|
+
* ## 缺 sessionId 的白名单码:如实不投
|
|
17
|
+
* 三码的 `detail.sessionId` 在 core 类型上是**可选**的(部分铸点确无 session 语境)。缺席时本腿
|
|
18
|
+
* **不投**(fail-closed 方向 = 宁缺席不串台;猜一个"当前活跃会话"就是串台的定义)。日志终点仍逐字
|
|
19
|
+
* 保留 ⇒ 运维观察面零回退,事实一条不丢。
|
|
20
|
+
*
|
|
21
|
+
* ## 白名单的演化机制(设计稿 ④)
|
|
22
|
+
* 本表是**过渡形**。core [4631] 已认领 `NOTICE_AUDIENCE` **码级注册表**(code → "user"|"operator",
|
|
23
|
+
* 闭集映射,随 5.46 / 最迟 5.47)+ `EngineNotice.sessionId` 顶层可选。到货后本表**退役为消费该表**,
|
|
24
|
+
* 不在本仓自造第二份「哪类码该上 wire」的判定语义。
|
|
25
|
+
* 🔴 在此之前的固定动作:**每一次 core 提货批**,LEDGER 对表 core `EngineNotice` 码册的新增码,
|
|
26
|
+
* 三选一登记(入册 / 明拒 + 理由 / 申裁),登记在 `docs/CORE-CONSUMPTION-LEDGER.md` 的该批行里。
|
|
27
|
+
* 漏对表的后果不是报错而是**静默缺席**:新码永远不上 wire,消费端(cli)无从知道该码存在。
|
|
28
|
+
*/
|
|
29
|
+
import { redactSecrets } from "./redact.js";
|
|
30
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
31
|
+
/**
|
|
32
|
+
* 上 wire 的通告码 —— **显式闭集常量表**(设计稿 ①,起步三码)。
|
|
33
|
+
*
|
|
34
|
+
* 三码同族:都是「这条 session 的记忆姿态」的披露,resume 之后仍然相关 ⇒ 也进 durable 账本
|
|
35
|
+
* (cli 断连补看走既有 events 重放腿,[4634](ii))。
|
|
36
|
+
*/
|
|
37
|
+
export const ENGINE_NOTICE_WIRE_CODES = [
|
|
38
|
+
/** design/178 §3:本会话记忆越过一次性 POLLUTED 态。core 去重单位 = **session**。`detail: { reason, sessionId? }` */
|
|
39
|
+
"memory.session_polluted",
|
|
40
|
+
/** design/178 §3:被污染会话的 harvest 执行了容纳。core 去重单位 = **harvest**(检查点收与终局收是两件事)。
|
|
41
|
+
* `detail: { count, moved, escalated, reason?, sessionId? }` —— 🔴 `moved` 与 `escalated` **不可相减**
|
|
42
|
+
* (就地墓碑同时计入两者,core 顶注);消费端并列呈现,不做算术。 */
|
|
43
|
+
"memory.harvest_quarantined",
|
|
44
|
+
/** design/324 #324①:`memoryDelegationEvidence:"attested-only"` 下,一次静态面本会打的污染标记被豁免。
|
|
45
|
+
* core 去重单位 = **prepared leg**。`detail: { reason, subagentType?, sessionId? }` */
|
|
46
|
+
"memory.delegation_static_mark_waived",
|
|
47
|
+
];
|
|
48
|
+
const WIRE_CODES = new Set(ENGINE_NOTICE_WIRE_CODES);
|
|
49
|
+
/** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
|
|
50
|
+
export function isEngineNoticeWireCode(code) {
|
|
51
|
+
return WIRE_CODES.has(code);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 归属键抽取 —— `detail.sessionId` 提升为顶层(设计稿 ②:归属键一等化)。
|
|
55
|
+
*
|
|
56
|
+
* 非串 / 空串一律按**缺席**处理(不铸 `"undefined"` 这种看起来合法的假值——`compactionOutcomeEventData`
|
|
57
|
+
* 的同款缺键守卫纪律)。core [4631] 的顶层 `EngineNotice.sessionId` 到货后,这里先读顶层再回落 detail
|
|
58
|
+
* (两处同值,additive 零 BREAKING),届时本函数是唯一改动点。
|
|
59
|
+
*/
|
|
60
|
+
export function engineNoticeSessionId(notice) {
|
|
61
|
+
const top = notice.sessionId; // core 5.46+ 顶层键(到货前恒 undefined,读它零代价)
|
|
62
|
+
if (typeof top === "string" && top.length > 0)
|
|
63
|
+
return top;
|
|
64
|
+
const fromDetail = notice.detail?.sessionId;
|
|
65
|
+
return typeof fromDetail === "string" && fromDetail.length > 0 ? fromDetail : undefined;
|
|
66
|
+
}
|
|
67
|
+
/** 自由文本(message / detail 里的串)的长度上限。`workspaceChangedEventData` 的 300 是**路径**级 bound;
|
|
68
|
+
* 通告的 `reason` 是一整句话(core 侧已 neutralize + bound),这里给它一个不腰斩正常句子的上限。 */
|
|
69
|
+
const NOTICE_TEXT_MAX = 1000;
|
|
70
|
+
/** detail 递归消毒的结构上限。三码的真 detail 都是 ≤5 键的扁平对象 ⇒ 生产上永不触顶;
|
|
71
|
+
* 上限存在的理由是**未来码**(以及被污染输入)不得把任意大小的对象推上 wire。 */
|
|
72
|
+
const DETAIL_MAX_DEPTH = 4;
|
|
73
|
+
const DETAIL_MAX_KEYS = 32;
|
|
74
|
+
const DETAIL_MAX_ITEMS = 32;
|
|
75
|
+
/**
|
|
76
|
+
* `detail` 消毒(设计稿 ②:`redactSecrets` + 尺寸 bound;`workspaceChangedEventData` 先例)。
|
|
77
|
+
*
|
|
78
|
+
* core 已 neutralize 三码的 `reason`(工具名/子代类型是 host/模型可控输入),**server 按纪律再过一遍**
|
|
79
|
+
* ——§E1 的既有姿态:上 wire 的自由文本一律经本仓的脱敏器,不因为上游说过一次就免检。
|
|
80
|
+
*
|
|
81
|
+
* 契约(逐条,门在 `test/engine-notice-wire.test.ts`):
|
|
82
|
+
* · 串 ⇒ `redactSecrets` + 截到 {@link NOTICE_TEXT_MAX};
|
|
83
|
+
* · 有限 number / boolean / null ⇒ 逐字(harvest 的 count/moved/escalated 是判据数字,不许变形);
|
|
84
|
+
* · 非有限 number(NaN/Infinity)⇒ **丢键**(JSON 里它们会变成 `null`,一个看起来合法的假读数);
|
|
85
|
+
* · 数组 ⇒ 逐项消毒,超 {@link DETAIL_MAX_ITEMS} 截断;对象 ⇒ 逐键消毒,超 {@link DETAIL_MAX_KEYS} 丢尾键;
|
|
86
|
+
* · 深度超 {@link DETAIL_MAX_DEPTH} ⇒ 该子树丢弃;
|
|
87
|
+
* · 其余(function / symbol / bigint / undefined)⇒ 丢键(JSON 不可承载或语义不明)。
|
|
88
|
+
*/
|
|
89
|
+
export function sanitizeNoticeDetail(detail) {
|
|
90
|
+
const out = sanitizeValue(detail ?? {}, 0);
|
|
91
|
+
return (out ?? {});
|
|
92
|
+
}
|
|
93
|
+
function sanitizeValue(v, depth) {
|
|
94
|
+
if (v === null)
|
|
95
|
+
return null;
|
|
96
|
+
if (typeof v === "string")
|
|
97
|
+
return redactSecrets(v).slice(0, NOTICE_TEXT_MAX);
|
|
98
|
+
if (typeof v === "number")
|
|
99
|
+
return Number.isFinite(v) ? v : undefined;
|
|
100
|
+
if (typeof v === "boolean")
|
|
101
|
+
return v;
|
|
102
|
+
if (depth >= DETAIL_MAX_DEPTH)
|
|
103
|
+
return undefined;
|
|
104
|
+
if (Array.isArray(v)) {
|
|
105
|
+
const items = [];
|
|
106
|
+
for (const item of v.slice(0, DETAIL_MAX_ITEMS)) {
|
|
107
|
+
const s = sanitizeValue(item, depth + 1);
|
|
108
|
+
if (s !== undefined)
|
|
109
|
+
items.push(s);
|
|
110
|
+
}
|
|
111
|
+
return items;
|
|
112
|
+
}
|
|
113
|
+
if (typeof v === "object") {
|
|
114
|
+
const rec = {};
|
|
115
|
+
let n = 0;
|
|
116
|
+
for (const [k, val] of Object.entries(v)) {
|
|
117
|
+
if (n >= DETAIL_MAX_KEYS)
|
|
118
|
+
break;
|
|
119
|
+
const s = sanitizeValue(val, depth + 1);
|
|
120
|
+
if (s === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
rec[k] = s;
|
|
123
|
+
n++;
|
|
124
|
+
}
|
|
125
|
+
return rec;
|
|
126
|
+
}
|
|
127
|
+
return undefined; // function / symbol / bigint / undefined
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* `engine_notice` 载荷构造器(三条终点腿同源:live SSE / durable 账本 / 未来的任何第四条腿)。
|
|
131
|
+
*
|
|
132
|
+
* 与 `trace/project.ts` 的 `*EventData` 族同纪律(白名单挑键、禁 `{...notice}`),但**不住在那个文件里**:
|
|
133
|
+
* 那一族的入参全是 core `TaskEvent` 的臂(有 `wire-whitelist-exhaustiveness` 的臂集编译门看着),
|
|
134
|
+
* 而 `EngineNotice` 不是 `TaskEvent` —— 混进去会让那道门的语义变成"两个不同来源的联合",判别力反而降。
|
|
135
|
+
*/
|
|
136
|
+
export function engineNoticeEventData(notice, sessionId, nowMs) {
|
|
137
|
+
return {
|
|
138
|
+
code: notice.code,
|
|
139
|
+
message: redactSecrets(String(notice.message)).slice(0, NOTICE_TEXT_MAX),
|
|
140
|
+
detail: sanitizeNoticeDetail(notice.detail),
|
|
141
|
+
sessionId,
|
|
142
|
+
ts: nowMs,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
let nextLegId = 1;
|
|
146
|
+
/**
|
|
147
|
+
* sessionId → 该会话活跃 run 腿的投递口。**进程内**(`defaultSubagentTailBus` 同款姿势):
|
|
148
|
+
* `onNotice` 是同进程的同步回调,通告与它要投的那条腿必然同副本,跨副本路由这件事结构上不存在。
|
|
149
|
+
*
|
|
150
|
+
* 🔴 跨 session 隔离靠的是 **Map 键的精确串相等**,不是任何「最近活跃」回落 —— 缺席就是缺席(G5)。
|
|
151
|
+
*
|
|
152
|
+
* 🔴 **同 session 的跨代隔离靠 legId「最新代独占」**(#310 codex 对抗复审 R1-[high],验真后修)。
|
|
153
|
+
* 病(真窗,本仓每一处 in-flight 登记都为它写过 identity-guard):run 行在**流体内**就已 setTerminal /
|
|
154
|
+
* setSuspended,而本腿的注销在 `finally` —— 中间那一窗里,同一条 session 上的**下一条腿**(快速续跑 /
|
|
155
|
+
* 紧接着的新提交)已经注册好了自己的口。若按「会话内全量扇出」投,新腿的一条通告会**同时**落进:
|
|
156
|
+
* · 旧腿那条已 end 的 SSE(顶多记一次 fail-open,无害),以及
|
|
157
|
+
* · **旧 taskId 的账本** —— 一条不属于那条 run 的、终态之后的行;resume 复用同 taskId 时还会与新腿
|
|
158
|
+
* 抢同一个 `(task_id, seq)` 域。这不是跨租户泄露,是**同租户跨 run 的账本污染**,同样必须关死。
|
|
159
|
+
* 药:每次 `registerLeg` 铸一个单调 legId,`route` **只投最新代**;旧代的口留在表里(它自己的注销仍
|
|
160
|
+
* 按身份精确删,不会误删新代),但一帧都收不到。方向 = fail-closed(宁可旧腿少一帧,不可新腿的事实
|
|
161
|
+
* 写进旧腿的账)。
|
|
162
|
+
*/
|
|
163
|
+
export class EngineNoticeRouter {
|
|
164
|
+
legs = new Map();
|
|
165
|
+
/**
|
|
166
|
+
* 登记**一条腿**的全部口(live / durable 作为**一代**同进同出)。返回注销函数(幂等,身份精确删)。
|
|
167
|
+
*
|
|
168
|
+
* 每只口各自 try 隔离在 {@link route} 里,单口抛不牵连同腿另一口。
|
|
169
|
+
*/
|
|
170
|
+
registerLeg(sessionId, sinks) {
|
|
171
|
+
const leg = { legId: nextLegId++, sinks: [...sinks] };
|
|
172
|
+
const list = this.legs.get(sessionId) ?? [];
|
|
173
|
+
list.push(leg); // 追加 = 本代成为最新代
|
|
174
|
+
this.legs.set(sessionId, list);
|
|
175
|
+
let done = false;
|
|
176
|
+
return () => {
|
|
177
|
+
if (done)
|
|
178
|
+
return;
|
|
179
|
+
done = true;
|
|
180
|
+
const cur = this.legs.get(sessionId);
|
|
181
|
+
if (!cur)
|
|
182
|
+
return;
|
|
183
|
+
const at = cur.indexOf(leg); // 身份精确删:绝不按位置/长度删(会误删新代)
|
|
184
|
+
if (at >= 0)
|
|
185
|
+
cur.splice(at, 1);
|
|
186
|
+
if (cur.length === 0)
|
|
187
|
+
this.legs.delete(sessionId);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** 单口腿的便捷形(测试与单终点腿用)。 */
|
|
191
|
+
register(sessionId, sink) {
|
|
192
|
+
return this.registerLeg(sessionId, [sink]);
|
|
193
|
+
}
|
|
194
|
+
/** 诊断/门用:某会话此刻**最新代**有几只口在听(旧代不计——它们收不到帧)。 */
|
|
195
|
+
sinkCount(sessionId) {
|
|
196
|
+
const list = this.legs.get(sessionId);
|
|
197
|
+
return list && list.length > 0 ? list[list.length - 1].sinks.length : 0;
|
|
198
|
+
}
|
|
199
|
+
/** 诊断/门用:某会话此刻有几代腿在表里(>1 = 正处在换代窗内)。 */
|
|
200
|
+
legCount(sessionId) {
|
|
201
|
+
return this.legs.get(sessionId)?.length ?? 0;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 分流器的 wire 半场。返回**真投出去的口数**(0 = 没投:非白名单码 / 缺 sessionId / 该会话无活跃腿)。
|
|
205
|
+
*
|
|
206
|
+
* 单只口抛错**不吃掉同腿的其它口**(fleet bus 订阅者隔离同理由),并按 F 类登记留痕。
|
|
207
|
+
*/
|
|
208
|
+
route(notice, nowMs = Date.now()) {
|
|
209
|
+
if (!isEngineNoticeWireCode(notice.code))
|
|
210
|
+
return 0;
|
|
211
|
+
const sessionId = engineNoticeSessionId(notice);
|
|
212
|
+
if (sessionId === undefined)
|
|
213
|
+
return 0; // 如实不投(顶注:宁缺席不串台)
|
|
214
|
+
const list = this.legs.get(sessionId);
|
|
215
|
+
const leg = list && list.length > 0 ? list[list.length - 1] : undefined; // 只投最新代
|
|
216
|
+
if (!leg || leg.sinks.length === 0)
|
|
217
|
+
return 0;
|
|
218
|
+
const row = engineNoticeEventData(notice, sessionId, nowMs);
|
|
219
|
+
let delivered = 0;
|
|
220
|
+
for (const sink of leg.sinks) {
|
|
221
|
+
try {
|
|
222
|
+
sink(row);
|
|
223
|
+
delivered++;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
recordFailOpen("server.engine-notice.wire-sink-threw", `code=${notice.code}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return delivered;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** 进程单例(`defaultSubagentTailBus` 同款):分流器与 run 腿直接 import,不穿长参数列表;测试自 new 独立实例。 */
|
|
233
|
+
export const defaultEngineNoticeRouter = new EngineNoticeRouter();
|
|
234
|
+
/**
|
|
235
|
+
* run 腿的注册助手 —— 三条腿(sync stream / bg / resume-verify)用**同一个**形,免得各写一份漂。
|
|
236
|
+
*
|
|
237
|
+
* `sessionId` 缺席(adhoc 无会话腿)⇒ 返回 no-op 注销函数,一个字节不注册(顶注的 fail-closed 方向)。
|
|
238
|
+
* `live` / `durable` 各自可缺席:bg 腿没有 live SSE(消费方走 events tail),ephemeral sync 腿没有账本。
|
|
239
|
+
*
|
|
240
|
+
* 🔴 两口注册成**两只独立 sink**(不是一只闭包里顺序调两次):route 的隔离粒度是 sink,合成一只会让
|
|
241
|
+
* live 写(断连的 socket、JSON.stringify 抛)吃掉同一拍的 durable 落账 —— 而 durable 那半正是断连之后
|
|
242
|
+
* 唯一还看得见这条通告的地方(G3),两者的失败必须互不牵连。
|
|
243
|
+
*/
|
|
244
|
+
export function registerEngineNoticeLeg(opts) {
|
|
245
|
+
const { sessionId, live, durable } = opts;
|
|
246
|
+
if (sessionId === undefined || sessionId.length === 0)
|
|
247
|
+
return () => undefined;
|
|
248
|
+
const sinks = [...(live ? [live] : []), ...(durable ? [durable] : [])];
|
|
249
|
+
if (sinks.length === 0)
|
|
250
|
+
return () => undefined;
|
|
251
|
+
// 🔴 两口登记成**同一代**(`registerLeg` 一次调用)而不是两次 `register`:代是"腿"的属性,拆成两代
|
|
252
|
+
// 会让换代时旧腿的一只口先被超代、另一只还没 —— 半代状态没有任何正确语义。口的失败隔离是 `route`
|
|
253
|
+
// 里的 per-sink try(与代无关),两者是正交的两件事。
|
|
254
|
+
return (opts.router ?? defaultEngineNoticeRouter).registerLeg(sessionId, sinks);
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=engine-notice-wire.js.map
|
|
@@ -39,5 +39,15 @@ export type LedgerEventType = "reasoning" | "text" | "tool_start" | "tool_end" |
|
|
|
39
39
|
/** design/172 §4.3 新协议的呈卡帧(与上面两只 tool_approval 帧同一条投递面)。 */
|
|
40
40
|
| "approval_request"
|
|
41
41
|
/** 异步 workflow 完成的入箱 drain(`emitPendingWorkflowCompletions` 两个字面型之一)。 */
|
|
42
|
-
| "workflow_complete"
|
|
42
|
+
| "workflow_complete"
|
|
43
|
+
/**
|
|
44
|
+
* #310 `engine_notice` —— core `EngineNotice` 白名单三码按 `detail.sessionId` 路由到该会话的事件投影。
|
|
45
|
+
*
|
|
46
|
+
* 🔴 **是"腿自己的带外行",不是流事件投影**:它不经 `TaskStream`(`RunnerDeps.onNotice` 是 deps 级
|
|
47
|
+
* 同步 sink),所以两条 durable 腿的 `switch (ev.type)` 里**没有也不该有** `case "engine_notice"` ——
|
|
48
|
+
* `test/durable-append-arm-parity.test.ts` 的差集是按那个 switch 抽的,本型结构上在它射程之外
|
|
49
|
+
* (与 `elicitation` / `question` / `approval_request` / `model_usage` 同类)。本型的"三腿是否都挂了口"
|
|
50
|
+
* 由 `test/engine-notice-wire.test.ts` 的腿覆盖门看守,那才是它的对位门。
|
|
51
|
+
*/
|
|
52
|
+
| "engine_notice";
|
|
43
53
|
//# sourceMappingURL=ledger-events.d.ts.map
|
|
@@ -70,6 +70,23 @@ export interface LedgerSink {
|
|
|
70
70
|
status?: string;
|
|
71
71
|
}): Promise<void>;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* **单写串行链**的可复用形(#310 codex 对抗复审 R1-[high],验真后修)—— `seq` 的**分配发生在链步内**,
|
|
75
|
+
* 于是「分配序 == 发起序 == 提交序」。
|
|
76
|
+
*
|
|
77
|
+
* 病(实测形,`LedgerSink` 内部早有这条纪律的内联版、其顶注 [1.211 codex H2] 逐字记着它):裸写口
|
|
78
|
+
* (`(type, data) => rs.appendEvent(taskId, ++seq, type, data)`)在**同步分配 seq、立刻发起 insert**。
|
|
79
|
+
* 主循环 await 自己的写,所以主循环内部有序;但只要有**一个 fire-and-forget 写者**(子代 forward 帧、
|
|
80
|
+
* #310 的通告口),它的 N 号 insert 可能在池化 SQL 后端上晚于后发的 N+1 号提交。events tail 的读法是
|
|
81
|
+
* `getEvents(id, afterSeq)` —— 读到 N+1 就把游标推过 N,**那一行此后永远不会被投递**(重连也不会,
|
|
82
|
+
* 游标只前进)。行在库里,消费端结构性看不见。
|
|
83
|
+
*
|
|
84
|
+
* 药:所有写口共用一条 promise 链,`++seq` 在链步内取。链上一步失败**不断链**(catch → undefined),
|
|
85
|
+
* 调用方仍拿到自己那一步的真实结果(await 者照常收到 reject)——与 `LedgerSink` 内联版逐字同语义。
|
|
86
|
+
*
|
|
87
|
+
* ⚠️ 用它的前提是**这条腿的全部写口都走它**(混用一个绕开链的写口 = 病灶原样保留)。
|
|
88
|
+
*/
|
|
89
|
+
export declare function createSerialLedgerAppend(appendEvent: (seq: number, type: LedgerEventType, data: unknown) => Promise<void>, startSeq: number): (type: LedgerEventType, data: unknown) => Promise<void>;
|
|
73
90
|
export declare function createLedgerSink(opts: {
|
|
74
91
|
/** The seq-stamped durable append — typically `(seq, type, data) => runStore.appendEvent(taskId, seq, type, data)`. */
|
|
75
92
|
appendEvent: (seq: number, type: LedgerEventType, data: unknown) => Promise<void>;
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { redactSecrets } from "./redact.js";
|
|
2
2
|
import { toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, humanInputEventData, turnEndEventData, contextUsageEventData } from "./project.js";
|
|
3
3
|
import { NotifiedKeys, taskNotificationStreamKey } from "../orchestration/workflow-completion-inbox.js";
|
|
4
|
+
/**
|
|
5
|
+
* **单写串行链**的可复用形(#310 codex 对抗复审 R1-[high],验真后修)—— `seq` 的**分配发生在链步内**,
|
|
6
|
+
* 于是「分配序 == 发起序 == 提交序」。
|
|
7
|
+
*
|
|
8
|
+
* 病(实测形,`LedgerSink` 内部早有这条纪律的内联版、其顶注 [1.211 codex H2] 逐字记着它):裸写口
|
|
9
|
+
* (`(type, data) => rs.appendEvent(taskId, ++seq, type, data)`)在**同步分配 seq、立刻发起 insert**。
|
|
10
|
+
* 主循环 await 自己的写,所以主循环内部有序;但只要有**一个 fire-and-forget 写者**(子代 forward 帧、
|
|
11
|
+
* #310 的通告口),它的 N 号 insert 可能在池化 SQL 后端上晚于后发的 N+1 号提交。events tail 的读法是
|
|
12
|
+
* `getEvents(id, afterSeq)` —— 读到 N+1 就把游标推过 N,**那一行此后永远不会被投递**(重连也不会,
|
|
13
|
+
* 游标只前进)。行在库里,消费端结构性看不见。
|
|
14
|
+
*
|
|
15
|
+
* 药:所有写口共用一条 promise 链,`++seq` 在链步内取。链上一步失败**不断链**(catch → undefined),
|
|
16
|
+
* 调用方仍拿到自己那一步的真实结果(await 者照常收到 reject)——与 `LedgerSink` 内联版逐字同语义。
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ 用它的前提是**这条腿的全部写口都走它**(混用一个绕开链的写口 = 病灶原样保留)。
|
|
19
|
+
*/
|
|
20
|
+
export function createSerialLedgerAppend(appendEvent, startSeq) {
|
|
21
|
+
let seq = startSeq;
|
|
22
|
+
let chain = Promise.resolve();
|
|
23
|
+
return (type, data) => {
|
|
24
|
+
const step = chain.then(() => appendEvent(++seq, type, data));
|
|
25
|
+
chain = step.then(() => undefined, () => undefined);
|
|
26
|
+
return step;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
4
29
|
export function createLedgerSink(opts) {
|
|
5
30
|
const keys = opts.notifiedKeys ?? new NotifiedKeys();
|
|
6
31
|
let seq = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.37.0-rc.1",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^5.
|
|
57
|
+
"@sema-agent/core": "^5.46.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.18.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|