@sema-agent/server 3.8.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/budget.js +23 -0
- package/dist/config-types.d.ts +7 -2
- package/dist/http/routes/tasks.js +6 -0
- package/dist/plugins/file-snapshot-store-sql.js +6 -0
- package/dist/plugins/mailbox-store-sql.js +13 -0
- package/dist/plugins/tool-result-store-sql.d.ts +5 -0
- package/dist/plugins/tool-result-store-sql.js +16 -0
- package/dist/plugins/worktree-isolation.js +57 -2
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +4 -0
- package/dist/trace/project.js +6 -0
- package/package.json +2 -2
package/dist/budget.js
CHANGED
|
@@ -178,6 +178,15 @@ export function createTracer(metrics, costQuota, modelUsage, fleetUsage, fleetLe
|
|
|
178
178
|
// 「下界 + costUnknown」——两层语义与本处的关系见 fleet-client.ts 的旁注(3.7.0 前这里曾写
|
|
179
179
|
// 「wire 表达不了缺席、center 只见低估」,已被该车推翻;doc-rot 修 2026-07-30)。
|
|
180
180
|
// └──────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
181
|
+
// [2153]③ 侦察钉(core 2.6.0):`brain.call` 帧新增可选 `turn?: number`(trace.d.ts 确认;此前只有
|
|
182
|
+
// `turn.end`/`repetition.detected`/`task.degraded` 三帧带 turn)。**刻意不接**——`modelUsage.record`
|
|
183
|
+
// 喂的是 E8 durable `model_usage` echo,累加键=`taskId → model`(见 `ModelUsageTracker.byTask`,
|
|
184
|
+
// 上方 class 定义),drain 边界=**per-turn**(run lifecycle 在每个 turn 边界 drain 一次、append 一条
|
|
185
|
+
// durable 事件行)。也就是说 turn 粒度已经由「何时 drain」这个外层动作表达,一次 drain 窗口内本就
|
|
186
|
+
// 只可能有一个 turn 的 delta——帧上逐行再带一份 `turn` 是重复信息,对已经 per-turn 聚合的行没有
|
|
187
|
+
// 增量语义(不是「丢了信息」,是「信息已经在结构里」)。将来若变盘:若聚合粒度改粗(比如攒够 N
|
|
188
|
+
// turn 才 drain 一次,单行 delta 跨多个 turn),或 usage-analytics 想要「同一 drain 窗口内哪个 turn
|
|
189
|
+
// 花了多少」的细分,才值得把 `e.turn` 塞进 `ModelUsageDelta`(那时 byTask 需要再加一层 turn 键)。
|
|
181
190
|
// E8 (shell-host): per-task × per-model usage for the `TaskStats.modelUsage` echo. `record` is a NO-OP unless
|
|
182
191
|
// e.taskId is a REGISTERED top-level run (the leak fence — sub-task/throwaway taskIds are never registered, so
|
|
183
192
|
// they neither leak nor enter the echo; their spend is in TaskStats.nested). Drained at turn boundaries into the
|
|
@@ -201,6 +210,20 @@ export function createTracer(metrics, costQuota, modelUsage, fleetUsage, fleetLe
|
|
|
201
210
|
metrics.observe("brain_call_latency_ms", e.latencyMs);
|
|
202
211
|
}
|
|
203
212
|
else if (e.kind === "tool.call") {
|
|
213
|
+
// [2153]③ 侦察钉(core 2.6.0):`tool.call` 帧新增可选 `toolCallId?: string` / `turn?: number`(trace.d.ts
|
|
214
|
+
// 确认)。**刻意不接进 `tool_calls_total` 的标签**:`toolCallId` 逐调用唯一 → 直接当标签会让这个
|
|
215
|
+
// 计数器的基数=调用总数(等于没聚合),是本文件 `cardinalityGuard` 存在的理由要防的那类爆炸,
|
|
216
|
+
// 连 guard 都救不了(不是「用户输入无界」那类可折叠 __other__ 的情形,是键本身设计上就唯一)。
|
|
217
|
+
// `turn` 本身是低基数(有界于单任务的 turn 数),但作为**跨任务**的 Prometheus 标签会随长任务/
|
|
218
|
+
// 多任务叠加成事实上无界的序列(同 model/tool_name 已用 guard 的理由,turn 目前没有等价上限)——
|
|
219
|
+
// 且当前没有「按 turn 分桶看 tool 调用量」的仪表盘/告警消费者在等这个切面,先不加标签。这两个键
|
|
220
|
+
// 已经在别的通道有名分:`toolCallId` 走 wire 的 `tool_start`/`tool_end` TaskEvent(trace/project.ts
|
|
221
|
+
// toolStartEventData/toolEndEventData,逐调用可关联、不进 metrics 基数);`turn` 走 `turn.end`/
|
|
222
|
+
// `task.end.turns`。本臂(createTracer→/metrics)只做计数,不是这两键的合适去处。将来若要接:
|
|
223
|
+
// (a) 出现「per-turn tool 调用密度」的真实消费者(如异常 turn 侦测),`turn` 分桶价值才成立——
|
|
224
|
+
// 用有界桶(如 `turn <= 20 ? String(turn) : "20+"`)而非原始整数,避免上面同款无界问题;
|
|
225
|
+
// (b) `toolCallId` 除非改造成非标签的关联字段(如结构化日志行,不是 Prometheus label),否则永远
|
|
226
|
+
// 不该进这个计数器。
|
|
204
227
|
metrics.inc("tool_calls_total", { name: toolName(e.name), ok: String(e.ok) });
|
|
205
228
|
}
|
|
206
229
|
else if (e.kind === "task.degraded") {
|
package/dist/config-types.d.ts
CHANGED
|
@@ -670,8 +670,13 @@ export interface ServiceConfigFlat {
|
|
|
670
670
|
* (default/auto/acceptEdits) gate every fs WRITE hand tool (Write/Edit/NotebookEdit) but never touched Bash —
|
|
671
671
|
* a model can `echo -n '…' > file` straight past the write-approval card. `MANUAL_MODE_SHELL_GATE` opts a
|
|
672
672
|
* deployment into ALSO tightening `TaskSpec.shellGate` on those same modes (core `bashReversibilityProbe`
|
|
673
|
-
* under `"classify"` only asks on constructively-irreversible commands — shell redirects etc
|
|
674
|
-
*
|
|
673
|
+
* under `"classify"` only asks on constructively-irreversible commands — shell redirects etc.).
|
|
674
|
+
* 🔴 **语义随 core 版本演进(2026-07-31 核)**:`"classify"` 下「纯读自动放行」这句自 core 2.7.0 起
|
|
675
|
+
* **不再无条件成立** —— RB-412 给 classifier 加了 root 边界扫描,core 内部自铸 boundary
|
|
676
|
+
* (`prepare-task.ts` 的 `shellReadBoundary`:roots=任务 root+additionalRoots,cwd=活 handsCwd),
|
|
677
|
+
* **读到 roots 之外的路径同样会 ask**(server 侧零接线即得:我们只传 shellGate,边界是 core 自算的)。
|
|
678
|
+
* 旧 core(<2.7.0)下仍是名字级判定=越界读放行,两者都能工作、后者更严 ⇒ floor 不因此抬。
|
|
679
|
+
* Absent/unset = `undefined` = **zero behavior change** (this is a tighten-ONLY opt-in, never a
|
|
675
680
|
* default — a deployment must explicitly choose it); `"always"` = CC-strict parity (every Bash asks). Any
|
|
676
681
|
* other value (incl. `"off"`) is treated as unset — this knob has no "off" value to opt BACK OUT with once a
|
|
677
682
|
* session's baseline already tightened elsewhere (tightenTaskSpec would reject that as a loosen anyway). */
|
|
@@ -649,6 +649,12 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
649
649
|
// 提示行。cwd 与已展示的工具 args 同信任级 → 共享白名单构造器(redactSecrets + bound)。
|
|
650
650
|
res.write(`data: ${JSON.stringify({ type: "workspace_changed", ...workspaceChangedEventData(ev) })}\n\n`);
|
|
651
651
|
}
|
|
652
|
+
else if (ev.type === "compaction_outcome") {
|
|
653
|
+
// core 2.5.0([2105]② 披露修):压缩非 compacted 结局上流帧——白名单投影:outcome/trigger
|
|
654
|
+
// 闭枚举 verbatim;reason 是引擎说明文本(可能引用配置值)→ redactSecrets;identity 两键同款。
|
|
655
|
+
const e = ev;
|
|
656
|
+
res.write(`data: ${JSON.stringify({ type: "compaction_outcome", outcome: e.outcome, trigger: e.trigger, ...(e.reason !== undefined ? { reason: redactSecrets(e.reason) } : {}), ...(e.eventId !== undefined ? { eventId: e.eventId } : {}), ...(e.parentToolCallId !== undefined ? { parentToolCallId: e.parentToolCallId } : {}) })}\n\n`);
|
|
657
|
+
}
|
|
652
658
|
else if (ev.type === "text_delta" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
|
|
653
659
|
// 裸 catch-all 白名单化收官(probe-wire-1 表征钉的根治半场):最后四个已知臂离开
|
|
654
660
|
// `JSON.stringify(ev)`。**零字节变化**白名单——已知键(含 E2 四 identity 键:live 现状
|
|
@@ -205,6 +205,12 @@ export class SqlFileSnapshotStore {
|
|
|
205
205
|
// the manifest tx runs → fail-closed: no manifest row ⇒ has() false ⇒ never a half-snapshot a restore would
|
|
206
206
|
// silently truncate. Upsert REFRESHES created_at (incl dedup hits) → blobs touched=now, GC-grace-protected.
|
|
207
207
|
for (const hash of new Set(manifest.values())) {
|
|
208
|
+
// D2(试剂盒揪出,RB-361 同源):路径逃逸型 hash 在任何 fetch **之前**拒,文案逐字同 core
|
|
209
|
+
// File/InMemory 后端(消费方按文案分支不劈叉);SQL 侧 hash 虽只是字符串键,但 srcGetBlob
|
|
210
|
+
// 是跨后端回调(2c 传输面可能远端),可疑值不外递。
|
|
211
|
+
if (hash === "" || hash === "." || hash === ".." || !/^[A-Za-z0-9_.-]+$/.test(hash)) {
|
|
212
|
+
return { ok: false, error: { code: "read_failed", message: `unsafe blob hash ${JSON.stringify(hash)}` } };
|
|
213
|
+
}
|
|
208
214
|
let bytes;
|
|
209
215
|
try {
|
|
210
216
|
bytes = await srcGetBlob(hash);
|
|
@@ -10,6 +10,13 @@ function assertScope(scope) {
|
|
|
10
10
|
/** 键长守卫(复审 F5):TiDB `INSERT IGNORE` 对超 190 字节键**静默截断**(warning 降级)——截断键
|
|
11
11
|
* 与全长参数的字节比对恒 miss(seq 分配读空行 TypeError / 隔离键错位);PG 侧是干净 22001,同点
|
|
12
12
|
* 提前拒 = 双方言同文案 fail-loud。 */
|
|
13
|
+
/** D4(试剂盒揪出,RB-251 语义):身份键含 NUL/控制符——TiDB VARBINARY 能存但与文件头「两后端一致
|
|
14
|
+
* 拒绝」的声明不符(该声明此前是谎);写入面双方言同精神拒绝(PG 臂经 PgUnstorableError 已拒)。 */
|
|
15
|
+
function assertIdentityChars(what, v) {
|
|
16
|
+
if ([...v].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f)) {
|
|
17
|
+
throw new Error(`MailboxStore: ${what} contains control characters (refusing — identity keys must be byte-exact addressable)`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
13
20
|
function assertKeyBytes(what, v) {
|
|
14
21
|
if (Buffer.byteLength(v, "utf8") > 190) {
|
|
15
22
|
throw new Error(`MailboxStore: ${what} exceeds the 190-byte key column (refusing — a truncated key would desync from its byte-exact lookups)`);
|
|
@@ -95,6 +102,8 @@ export class TiDBMailboxStore {
|
|
|
95
102
|
}
|
|
96
103
|
async append(scope, handle, msg) {
|
|
97
104
|
assertScope(scope);
|
|
105
|
+
assertIdentityChars("scope", scope);
|
|
106
|
+
assertIdentityChars("handle", handle);
|
|
98
107
|
assertKeyBytes("scope", scope);
|
|
99
108
|
assertKeyBytes("handle", handle);
|
|
100
109
|
// ensure-first 形:先幂等 INSERT IGNORE 建盒(next_seq=1,首条/drop 后重建同臂)再 FOR UPDATE——
|
|
@@ -290,6 +299,10 @@ export class PgMailboxStore {
|
|
|
290
299
|
WHERE scope_key = $1 AND handle = $2 AND lease_owner = $3`, [scope, handle, owner]);
|
|
291
300
|
}
|
|
292
301
|
async peekCount(scope, handle) {
|
|
302
|
+
// D4(试剂盒揪出):unstorable 身份键在**读面降级**返回 0(该键在写入面就被拒 ⇒ 盒必空),
|
|
303
|
+
// 而不是把驱动层 22021 裸抛给调用方——与 tool-result 读面「degrades instead」同精神。
|
|
304
|
+
if (pgHasUnstorable(scope) || pgHasUnstorable(handle))
|
|
305
|
+
return 0;
|
|
293
306
|
const { rows } = await this.pool.query(`SELECT COUNT(*) AS n FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, handle]);
|
|
294
307
|
return Number(rows[0].n);
|
|
295
308
|
}
|
|
@@ -51,6 +51,11 @@ export declare class SqlToolResultStore implements ToolResultStore {
|
|
|
51
51
|
constructor(db: SqlDriver);
|
|
52
52
|
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
53
53
|
private q;
|
|
54
|
+
/** D1(试剂盒揪出,RB-266 语义):ref 是主键且 deleteBySession 靠 `tr_<sid>_%` 前缀清理——不合规
|
|
55
|
+
* ref 的行既躲过 session 删除也只能等 TTL,永远清不掉。判定逐字镜像 core assertSafeToolResultRef
|
|
56
|
+
* (dist/core/tool-result-store.js:5;core 根不导出,本地镜像,试剂盒契约条目为同源锚)。 */
|
|
57
|
+
private isUnsafeRef;
|
|
58
|
+
private assertSafeRef;
|
|
54
59
|
put(ref: string, content: string): Promise<void>;
|
|
55
60
|
get(ref: string, opts?: {
|
|
56
61
|
offset?: number;
|
|
@@ -28,7 +28,19 @@ export class SqlToolResultStore {
|
|
|
28
28
|
q(tidb, pg) {
|
|
29
29
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
30
30
|
}
|
|
31
|
+
/** D1(试剂盒揪出,RB-266 语义):ref 是主键且 deleteBySession 靠 `tr_<sid>_%` 前缀清理——不合规
|
|
32
|
+
* ref 的行既躲过 session 删除也只能等 TTL,永远清不掉。判定逐字镜像 core assertSafeToolResultRef
|
|
33
|
+
* (dist/core/tool-result-store.js:5;core 根不导出,本地镜像,试剂盒契约条目为同源锚)。 */
|
|
34
|
+
isUnsafeRef(ref) {
|
|
35
|
+
return ref === "" || ref === "." || ref === ".." || ref.includes("/") || ref.includes("\\") ||
|
|
36
|
+
[...ref].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f);
|
|
37
|
+
}
|
|
38
|
+
assertSafeRef(ref) {
|
|
39
|
+
if (this.isUnsafeRef(ref))
|
|
40
|
+
throw new Error(`tool-result store: unsafe ref ${JSON.stringify(ref)}`);
|
|
41
|
+
}
|
|
31
42
|
async put(ref, content) {
|
|
43
|
+
this.assertSafeRef(ref);
|
|
32
44
|
// Sanitize invalid UTF-16 (lone/half surrogates — a JS string CAN hold them, e.g. a binary-ish tool
|
|
33
45
|
// output) to U+FFFD so the column accepts the FULL value via a Buffer utf8 round-trip (valid content is
|
|
34
46
|
// unchanged). Shared by both dialects.
|
|
@@ -45,6 +57,10 @@ export class SqlToolResultStore {
|
|
|
45
57
|
await this.db.query(this.q("INSERT IGNORE INTO tool_result (ref, content, created_at) VALUES (?,?,?)", "INSERT INTO tool_result (ref, content, created_at) VALUES ($1,$2,$3) ON CONFLICT (ref) DO NOTHING"), [ref, safe, new Date()]);
|
|
46
58
|
}
|
|
47
59
|
async get(ref, opts = {}) {
|
|
60
|
+
// D1 读面半条(RB-266「the read face degrades instead」):unsafe ref 在读面**降级**返回 undefined
|
|
61
|
+
// (该 ref 写入面就被拒 ⇒ 必不在库),不把驱动层错误(PG 22021 对 NUL)漏给调用方。
|
|
62
|
+
if (this.isUnsafeRef(ref))
|
|
63
|
+
return undefined;
|
|
48
64
|
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
49
65
|
const hasLimit = opts.limit !== undefined && Number.isFinite(opts.limit);
|
|
50
66
|
const limit = hasLimit ? Math.max(0, Math.floor(opts.limit)) : 0;
|
|
@@ -109,12 +109,46 @@ export function withWorktreeIsolation(baseFactory, opts) {
|
|
|
109
109
|
// down (core's hasDestroy() returns false for a plain base env). Behaviour identical to no wrapper.
|
|
110
110
|
return baseFactory(ctx);
|
|
111
111
|
}
|
|
112
|
+
// S3([2113] core 裁定的 server 半场):keep-if-changed 需要在保留分支跑 inner teardown(LSP evict
|
|
113
|
+
// 链等)而不跑 `git worktree remove` —— core addWorktree 把两者焊成一个复合 destroy 拆不开,所以在
|
|
114
|
+
// rootEnvAt 铸造点捕获 inner env 的**原** destroy 引用(per-mint 变量;core 稍后才把复合 destroy
|
|
115
|
+
// 装回同一对象上)。
|
|
116
|
+
let innerDestroy;
|
|
112
117
|
const { env, worktreeDir } = await serialize(() => addWorktree(opts.baseEnvForGit, {
|
|
113
118
|
repoRoot,
|
|
114
119
|
sessionId: ctx.sessionId, // core sanitizes this into the worktree dir name (human-readable orphan diag)
|
|
115
|
-
rootEnvAt:
|
|
120
|
+
rootEnvAt: async (dir) => {
|
|
121
|
+
const inner = await opts.rootEnvAt(dir);
|
|
122
|
+
const d = inner.destroy;
|
|
123
|
+
if (typeof d === "function")
|
|
124
|
+
innerDestroy = d.bind(inner);
|
|
125
|
+
return inner;
|
|
126
|
+
},
|
|
116
127
|
...(opts.commit ? { commit: opts.commit } : {}),
|
|
117
128
|
}));
|
|
129
|
+
// keep-if-changed 判定基点:mint 后立刻解析 worktree 的 HEAD(=detach 基点)。判定轴与 core Agent
|
|
130
|
+
// 车道逐字同款(subagent.js finish):dirty 文件 >0 ∥ base..HEAD commits >0 ⇒ changed。
|
|
131
|
+
let baseSha;
|
|
132
|
+
{
|
|
133
|
+
const r = await opts.baseEnvForGit.exec("git rev-parse HEAD", { cwd: worktreeDir });
|
|
134
|
+
if (r.ok && r.value.exitCode === 0)
|
|
135
|
+
baseSha = r.value.stdout.trim();
|
|
136
|
+
}
|
|
137
|
+
const isChanged = async () => {
|
|
138
|
+
// 判定失败 ⇒ 保守保留(Agent 车道 changed=true 缺省同款:删错=用户产出蒸发,留错=一个目录)。
|
|
139
|
+
const status = await opts.baseEnvForGit.exec("git status --porcelain", { cwd: worktreeDir });
|
|
140
|
+
if (!status.ok || status.value.exitCode !== 0)
|
|
141
|
+
return true;
|
|
142
|
+
const dirtyFiles = status.value.stdout.split("\n").filter((l) => l.trim() !== "").length;
|
|
143
|
+
if (dirtyFiles > 0)
|
|
144
|
+
return true;
|
|
145
|
+
if (baseSha === undefined)
|
|
146
|
+
return true;
|
|
147
|
+
const ahead = await opts.baseEnvForGit.exec(`git rev-list --count ${baseSha}..HEAD`, { cwd: worktreeDir });
|
|
148
|
+
if (!ahead.ok || ahead.value.exitCode !== 0)
|
|
149
|
+
return true;
|
|
150
|
+
return (parseInt(ahead.value.stdout.trim(), 10) || 0) > 0;
|
|
151
|
+
};
|
|
118
152
|
opts.logger?.info?.("worktree_isolation_minted", {
|
|
119
153
|
sessionId: ctx.sessionId,
|
|
120
154
|
...(ctx.taskId ? { taskId: ctx.taskId } : {}),
|
|
@@ -142,9 +176,30 @@ export function withWorktreeIsolation(baseFactory, opts) {
|
|
|
142
176
|
// already reached; double/concurrent destroy is a legal Runner shape). The first call always goes through
|
|
143
177
|
// coreDestroy (the inner env's own teardown must run).
|
|
144
178
|
let destroyedOnce = false;
|
|
179
|
+
let keptOnce = false;
|
|
145
180
|
env.destroy = () => serialize(async () => {
|
|
181
|
+
if (keptOnce)
|
|
182
|
+
return; // kept 是终态:二次 destroy 不得把保留的产出删掉
|
|
146
183
|
if (destroyedOnce && !(await residue()))
|
|
147
|
-
return; // idempotent repeat — done is done
|
|
184
|
+
return; // idempotent repeat — done is done(原语义:残留时放行重试)
|
|
185
|
+
// S3 keep-if-changed:有改动(或判不出来)⇒ 保留目录+registration 给 userland verify+merge,
|
|
186
|
+
// 只跑 inner teardown(LSP evict/host 收尾;rootEnvAt 铸的是 persistent 形,不 rm 目录),
|
|
187
|
+
// 并披露坐标 —— 不披露的保留=只是换了个姿势的蒸发。残留检查在本分支让位:目录在=目的。
|
|
188
|
+
if (await isChanged()) {
|
|
189
|
+
keptOnce = true;
|
|
190
|
+
try {
|
|
191
|
+
await innerDestroy?.();
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
opts.logger?.info?.("worktree_kept", {
|
|
195
|
+
worktreeDir,
|
|
196
|
+
sessionId: ctx.sessionId,
|
|
197
|
+
...(ctx.taskId ? { taskId: ctx.taskId } : {}),
|
|
198
|
+
note: "changes present — kept for verify/merge; remove with `git worktree remove` when done",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
148
203
|
await coreDestroy();
|
|
149
204
|
destroyedOnce = true;
|
|
150
205
|
if (!(await residue()))
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, TaskEvent } from "@sema-agent/core";
|
|
19
19
|
/** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。 */
|
|
20
20
|
type AssertAllKeysHandled<T extends never> = T;
|
|
21
|
-
type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId";
|
|
21
|
+
type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode";
|
|
22
22
|
type _GuardNotification = AssertAllKeysHandled<Exclude<keyof TaskNotificationPayload, NotificationProjected>>;
|
|
23
23
|
type BgNotifProjected = "taskId" | "sessionId" | "seq" | "status" | "summary" | "stoppedBy" | "resumable" | "recentSteps" | "editedFiles" | "usage" | "transcriptId" | "rootSessionId" | "parentTaskId" | "parentToolCallId" | "completionId";
|
|
24
24
|
type BgNotifExcluded = "kind" | "sessionScoped" | "owner" | "scope" | "description" | "agentType" | "name" | "currentAction" | "currentTool" | "parentSessionId" | "startedAt" | "workflowRunId" | "progressTaskId" | "progressParentTaskId";
|
|
@@ -28,7 +28,7 @@ type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjec
|
|
|
28
28
|
type AskProjected = "toolName" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName";
|
|
29
29
|
type AskExcluded = "toolCallId" | "preview" | "principal" | "requiresRealApproval";
|
|
30
30
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
31
|
-
type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage";
|
|
31
|
+
type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome";
|
|
32
32
|
type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
|
|
33
33
|
export type CoreKeysetGuards = [_GuardNotification, _GuardBgNotif, _GuardRoster, _GuardAsk, _GuardTaskEvent];
|
|
34
34
|
export {};
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -185,6 +185,8 @@ export declare function taskNotificationEventData(ev: {
|
|
|
185
185
|
}>;
|
|
186
186
|
resumable?: boolean;
|
|
187
187
|
completionId?: string;
|
|
188
|
+
error?: string;
|
|
189
|
+
errorCode?: string;
|
|
188
190
|
};
|
|
189
191
|
eventId?: string;
|
|
190
192
|
parentToolCallId?: string;
|
|
@@ -250,6 +252,8 @@ export declare function brainStatusEventData(ev: {
|
|
|
250
252
|
parentToolCallId?: string;
|
|
251
253
|
}): Record<string, unknown>;
|
|
252
254
|
export declare function compactedEventData(ev: {
|
|
255
|
+
eventId?: string;
|
|
256
|
+
parentToolCallId?: string;
|
|
253
257
|
trigger?: string;
|
|
254
258
|
tokensBefore?: number;
|
|
255
259
|
preserved_segment?: {
|
package/dist/trace/project.js
CHANGED
|
@@ -198,6 +198,10 @@ export function taskNotificationEventData(ev) {
|
|
|
198
198
|
...(n.stoppedBy !== undefined ? { stoppedBy: n.stoppedBy } : {}),
|
|
199
199
|
// P1-3(core 1.452):完成幂等锚 verbatim(消费端跨通路去重;[1925] 一轮批)。
|
|
200
200
|
...(n.completionId !== undefined ? { completionId: n.completionId } : {}),
|
|
201
|
+
// RB-386(core 2.4.0 wire note,2.5.0 提货收编):bg agent 失败面归因两键——error 是引擎侧
|
|
202
|
+
// 失败说明(可能含网络地址等)→ redactSecrets;errorCode 是机器码 verbatim。缺席=非失败帧。
|
|
203
|
+
...(n.error !== undefined ? { error: redactSecrets(n.error) } : {}),
|
|
204
|
+
...(n.errorCode !== undefined ? { errorCode: n.errorCode } : {}),
|
|
201
205
|
// [1543]§二 server①:白名单滞后 core payload 四键补齐——
|
|
202
206
|
// source:external 通知来源标(caller 供 = UNTRUSTED)→ redactSecrets;
|
|
203
207
|
// exitCode:数值 verbatim(bash 腿);
|
|
@@ -312,6 +316,8 @@ export function compactedEventData(ev) {
|
|
|
312
316
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
313
317
|
})();
|
|
314
318
|
return {
|
|
319
|
+
...(ev.eventId !== undefined ? { eventId: ev.eventId } : {}),
|
|
320
|
+
...(ev.parentToolCallId !== undefined ? { parentToolCallId: ev.parentToolCallId } : {}),
|
|
315
321
|
// 🔴 2026-07-25:这三处此前**违反本函数注释自己写的那条不变量**(「畸形字段不得上线,与 1.72 同类」)——
|
|
316
322
|
// ① `trigger`/`tokensBefore` 是**裸赋值**:`tokensBefore` 为 `NaN` 时 `JSON.stringify` 把它变成 **`null`**,
|
|
317
323
|
// 于是压缩面板渲染成「压缩前 0 tokens」而不是「未知」;裸赋值还会留下 present-with-undefined 的键
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^2.
|
|
57
|
+
"@sema-agent/core": "^2.5.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.11.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|