@sema-agent/server 3.5.3 → 3.6.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/dist/budget.d.ts +5 -1
- package/dist/budget.js +39 -23
- package/dist/fleet/fleet-bus.d.ts +19 -0
- package/dist/fleet/fleet-bus.js +94 -2
- package/dist/fleet-client.d.ts +10 -1
- package/dist/fleet-client.js +11 -2
- package/dist/fleet-lease.d.ts +7 -1
- package/dist/fleet-lease.js +7 -1
- package/dist/hooks/branch-transcript.d.ts +20 -0
- package/dist/hooks/branch-transcript.js +78 -7
- package/dist/plugins/worktree-isolation.js +24 -14
- package/dist/trace/project.d.ts +13 -2
- package/dist/trace/project.js +22 -3
- package/package.json +3 -3
package/dist/budget.d.ts
CHANGED
|
@@ -29,7 +29,11 @@ export interface ModelUsageDelta {
|
|
|
29
29
|
outputTokens: number;
|
|
30
30
|
cacheReadTokens: number;
|
|
31
31
|
cacheWriteTokens: number;
|
|
32
|
-
|
|
32
|
+
/** 🔴 RB-368(core 2.3.0 起)**可选 = 缺席表示「未知」,不是 0**。unpriced 部署(目录里该模型没配
|
|
33
|
+
* `cost`、也没注 `pricing`)下 core 在 `brain.call` 帧上**整键缺席**,我们如实透传;显式全零 `cost`
|
|
34
|
+
* = 运营者声明免费 ⇒ 键在场且为 0。两者可区分,这正是 #59 折零案的源头修复。
|
|
35
|
+
* 与 SDK `ModelUsageDelta`(五键全可选)/ `usage-analytics.UsageModelEntry` 同轨。 */
|
|
36
|
+
costMicroUsd?: number;
|
|
33
37
|
}
|
|
34
38
|
/**
|
|
35
39
|
* E8 (shell-host contract): per-task × per-model usage accumulator, fed (synchronously) by the `brain.call`
|
package/dist/budget.js
CHANGED
|
@@ -76,10 +76,18 @@ export class ModelUsageTracker {
|
|
|
76
76
|
cur.outputTokens += d.outputTokens;
|
|
77
77
|
cur.cacheReadTokens += d.cacheReadTokens;
|
|
78
78
|
cur.cacheWriteTokens += d.cacheWriteTokens;
|
|
79
|
-
|
|
79
|
+
// 🔴 RB-368 未知传染:已知 + 未知 = **未知**(和里含一个不知道的项,这个和就不知道)⇒ 删键,不折 0。
|
|
80
|
+
// 顺序无关(先未知后已知同样保持未知),因为 `cur.costMicroUsd` 已是 undefined 时 `+` 也不成立。
|
|
81
|
+
if (d.costMicroUsd === undefined || cur.costMicroUsd === undefined)
|
|
82
|
+
delete cur.costMicroUsd;
|
|
83
|
+
else
|
|
84
|
+
cur.costMicroUsd += d.costMicroUsd;
|
|
80
85
|
}
|
|
81
86
|
else {
|
|
82
|
-
|
|
87
|
+
// 显式重建而非 `{...d}`:调用方可能传 `{costMicroUsd: undefined}`(显式 undefined 键),
|
|
88
|
+
// 那样 `Object.hasOwn` 为真、JSON.stringify 又把它抹掉 ⇒ 「缺席」在内存与落库两处形状不一致。
|
|
89
|
+
const { costMicroUsd, ...tokens } = d;
|
|
90
|
+
perModel.set(model, { ...tokens, ...(costMicroUsd !== undefined ? { costMicroUsd } : {}) });
|
|
83
91
|
}
|
|
84
92
|
}
|
|
85
93
|
/** Return the usage accumulated for this task SINCE the last drain (per neutral model id), and RESET it.
|
|
@@ -131,37 +139,45 @@ export function createTracer(metrics, costQuota, modelUsage, fleetUsage, fleetLe
|
|
|
131
139
|
if (principal)
|
|
132
140
|
costQuota?.add(principal, e.costMicroUsd);
|
|
133
141
|
}
|
|
134
|
-
// ┌─ #59 裁定([2052]① cli 报案 →
|
|
135
|
-
//
|
|
142
|
+
// ┌─ #59 裁定 / RB-368 结案([2052]① cli 报案 → [2073] 立案 → [2076] 工单挂 core → [2083] 到货) ─┐
|
|
143
|
+
// 病灶报案:下面三处曾是 `e.costMicroUsd ?? 0`(账本 + fleetUsage + fleetLease),把「成本未知」记成
|
|
136
144
|
// 「免费」,与 `pricingConfigured=false` 的部署组合 ⇒ 全部账目假 0。
|
|
137
145
|
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
// 把这三处改成透传缺席**治不了根**(缺席永不到达),只会在 wire/SDK 面开一个今天零 producer 的可缺键。
|
|
144
|
-
// `?? 0` 因此定性为**类型面守卫**(core 契约把该键声明为 optional),不是语义折零点。
|
|
146
|
+
// **第一轮裁定(core ≤2.2.0,已被上游修复取代,留档见证判断链)**:亲读上游后纠偏——core 当时在
|
|
147
|
+
// brain.call 帧上**恒发数字、从不缺席**(`costMicroUsd: turnCostMicroUsd` 是无条件键;无价目表时
|
|
148
|
+
// `modelCostToPricing(undefined)` 产出全零单价 ⇒ 如实算出 0)。歧义**产生在 core**,server 收到的
|
|
149
|
+
// 信息里根本不含这个区分 ⇒ 当时改透传治不了根(缺席永不到达),`?? 0` 定性为**类型面守卫**。
|
|
150
|
+
// 结论是把根因工单挂回 core,而不是在本层做样子。
|
|
145
151
|
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
152
|
+
// 🔴 **第二轮 = 现行(core ≥2.3.0,RB-368 到货)**:core 已在源头消歧——unpriced 部署下
|
|
153
|
+
// brain.call(`runtask.js:831`)/ task.end(`:2851`)/ `stats.costMicroUsd`+`costBreakdown`
|
|
154
|
+
// (`assemble-result.js:39,144`)**整键缺席**;显式全零 `cost` = 声明免费 ⇒ 照发 0。于是三处
|
|
155
|
+
// `?? 0` 全部改为**条件展开透传**:键在 ⇒ 原样传(含真 0);键不在 ⇒ 三路都不带该键。
|
|
156
|
+
// 事实钉在 test/budget.test.ts(真 Runner 跑一趟取证),floor 已钉 ^2.3.0——装回旧 core 这三条
|
|
157
|
+
// 透传臂会变成永不到达的死枝而全量照绿,那正是 floor 门存在的理由。
|
|
158
|
+
//
|
|
159
|
+
// 三问(第二轮):
|
|
160
|
+
// · 谁需要 —— 消费端要区分真零/未知(cli 在 CC 对照里正是靠「诚实缺席不编造」赢的那一点;
|
|
161
|
+
// cli #68 的两信号分渲 = 部署级 pricingConfigured + 行级缺席,行级这一半由本改动供给)。
|
|
148
162
|
// · 谁受伤 —— (a) 聚合面遇缺席:usage-analytics 与 aggregateModelUsage 此前把 DB JSON 当全必填做裸
|
|
149
163
|
// 算术,一行缺键就 NaN/字符串毒掉整窗(#59 同批已修,红先复现在
|
|
150
164
|
// test/usage-analytics.test.ts + test/model-usage.test.ts);(b) 历史行:已按 0 记账的行无法回溯
|
|
151
|
-
//
|
|
152
|
-
// · 补偿 —— 第一级 = `capabilities.pricingConfigured`(main.ts
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
165
|
+
// 区分,只能靠部署位读回——**本改动不回溯**,分界线 = 升到 core 2.3.0 那一刻。
|
|
166
|
+
// · 补偿 —— 第一级 = `capabilities.pricingConfigured`(main.ts,已在):false ⇒ 消费端把任何 0
|
|
167
|
+
// 渲染成「未知」而非「免费」;第二级 = 行级缺席(本改动),沿账本→durable `model_usage`→
|
|
168
|
+
// `aggregateModelUsage`→`TaskStats.modelUsage` 回声→usage-analytics 的 `estimated` 标记,一路
|
|
169
|
+
// 保持缺席(聚合面的**未知传染**语义见 trace/project.ts)。
|
|
170
|
+
// · **一处没接住,如实记账**:fleet 批报腿(下面 fleetUsage)——registry-core 的
|
|
171
|
+
// `UsageEntry.costMicroUsd` 是 `z.number()` **必填**,wire 上表达不了缺席。故批报入参虽然
|
|
172
|
+
// 如实不带键,accumulator 侧仍只能把未知折成「不计入已知和」(见 fleet-client.ts 的旁注),
|
|
173
|
+
// center 看到的仍是低估而非「未知」。要真治需 registry-core 契约改 optional + center 消费面同改。
|
|
158
174
|
// └──────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
159
175
|
// E8 (shell-host): per-task × per-model usage for the `TaskStats.modelUsage` echo. `record` is a NO-OP unless
|
|
160
176
|
// e.taskId is a REGISTERED top-level run (the leak fence — sub-task/throwaway taskIds are never registered, so
|
|
161
177
|
// they neither leak nor enter the echo; their spend is in TaskStats.nested). Drained at turn boundaries into the
|
|
162
178
|
// durable log (resume-safe); the registered entry is dropped at run end (clear). NOT cardinality-guarded by
|
|
163
179
|
// model: bounded by a run's model set, and by the active-run set.
|
|
164
|
-
modelUsage?.record(e.taskId, e.model, { inputTokens: e.promptTokens, outputTokens: e.completionTokens, cacheReadTokens: e.cacheRead, cacheWriteTokens: e.cacheWrite, costMicroUsd: e.costMicroUsd
|
|
180
|
+
modelUsage?.record(e.taskId, e.model, { inputTokens: e.promptTokens, outputTokens: e.completionTokens, cacheReadTokens: e.cacheRead, cacheWriteTokens: e.cacheWrite, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}) }); // RB-368 透传缺席
|
|
165
181
|
// weight-at-burn:加权 tokens = (prompt+completion)×模型配额倍率,在记账点固化(此后改倍率
|
|
166
182
|
// 不回算历史,registry ModelEntry.quotaWeight 同句契约)。weight 源=config.modelQuotaWeights(registry
|
|
167
183
|
// hot-apply;env lane 空表 ⇒ 1 兜底);非法回调值防御性归 1(tracer 契约 never-throws,不让坏表毒记账)。
|
|
@@ -170,10 +186,10 @@ export function createTracer(metrics, costQuota, modelUsage, fleetUsage, fleetLe
|
|
|
170
186
|
const weightedTokens = Math.round((e.promptTokens + e.completionTokens) * qw);
|
|
171
187
|
// P2 用量批报:principal×model 窗口累计(同 ⑤b 的 ALS 归因点 — 子任务花费也归到发起 principal),
|
|
172
188
|
// fleet-client 每窗口 drain→POST /api/fleet/usage。未配 fleet(accumulator 缺席)= 零行为。
|
|
173
|
-
fleetUsage?.record(currentPrincipal(), e.model, e.taskId, { inputTokens: e.promptTokens, outputTokens: e.completionTokens, costMicroUsd: e.costMicroUsd
|
|
189
|
+
fleetUsage?.record(currentPrincipal(), e.model, e.taskId, { inputTokens: e.promptTokens, outputTokens: e.completionTokens, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}), weightedTokens, quotaWeightAtUse: qw }); // RB-368 透传缺席
|
|
174
190
|
// lease 消费(D4 AP):同一 ALS 归因点做 lease 本地扣减 —— 只对当前持 lease 的 principal
|
|
175
191
|
// 生效(FleetLeaseManager 内部判),sync + never throws。双轴=weightedTokens(配额轴)+costMicroUsd($ 轴)。
|
|
176
|
-
fleetLease?.recordSpend(currentPrincipal(), { weightedTokens, costMicroUsd: e.costMicroUsd
|
|
192
|
+
fleetLease?.recordSpend(currentPrincipal(), { weightedTokens, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}) }); // RB-368 透传缺席
|
|
177
193
|
if (e.firstTokenMs !== undefined)
|
|
178
194
|
metrics.observe("brain_first_token_ms", e.firstTokenMs);
|
|
179
195
|
metrics.observe("brain_call_latency_ms", e.latencyMs);
|
|
@@ -109,6 +109,16 @@ export interface FleetTaskRow {
|
|
|
109
109
|
path: string;
|
|
110
110
|
edits: number;
|
|
111
111
|
}>;
|
|
112
|
+
/** [2070]①/[2080] 裁定(server 半场,3.5.4):**产出这一行的车道**。
|
|
113
|
+
* - 缺席 = BCE 车道(`fleetBackgroundChildPublisher`)的 a\* 与 wa\* 真行,或顶层 run 行 —— 单源,常态。
|
|
114
|
+
* - `"run-leg"` = 由每腿的 `fleetRunPublisher` 从 `task_progress` tick 铸的**复合 id 行**
|
|
115
|
+
* (`${runId} ${childTaskId}`)。此形只在**没有 BCE 真行**的前台 delegation 上保留(同步腿委派的
|
|
116
|
+
* 子代不过 background-child registry ⇒ 不让位就没有行);同 (scope, progressTaskId) 一旦有 BCE 真行,
|
|
117
|
+
* 本车道**让位不铸**(双生行退役)。
|
|
118
|
+
* 消费端读法:带 `sourceLane:"run-leg"` 的行是过渡形,与 a\* 行**不会**同时描述同一个 agent;不需要再
|
|
119
|
+
* 自己发明「复合 id 首段==parentId 且撞 (parentId,name)」那类集合级判据(cli 侧的防御税)。
|
|
120
|
+
* ADDITIVE / tolerate-absent。 */
|
|
121
|
+
sourceLane?: "run-leg";
|
|
112
122
|
}
|
|
113
123
|
/** One workflow row (wire subset of contract `FleetWorkflow`). */
|
|
114
124
|
export interface FleetWorkflowRow {
|
|
@@ -272,6 +282,15 @@ export declare class FleetEventBus {
|
|
|
272
282
|
publishHookNotice(notice: HookNotice): void;
|
|
273
283
|
/** Drop a task row (terminal + swept) + fan out the removal. Idempotent (a no-op if already gone). */
|
|
274
284
|
removeTask(id: string): void;
|
|
285
|
+
private readonly bceClaims;
|
|
286
|
+
private static readonly BCE_CLAIM_MAX;
|
|
287
|
+
private static bceKey;
|
|
288
|
+
/** BCE 车道:本车道已为该子代 uuid 建了真行 —— run-leg 车道自此让位。幂等。 */
|
|
289
|
+
claimBackgroundChildLane(scope: string | undefined, childUuid: string, rowId: string): void;
|
|
290
|
+
/** BCE 车道:该子代已终态离场 —— 让位解除(后续同 uuid 的迟到 tick 回到旧行为)。幂等。 */
|
|
291
|
+
releaseBackgroundChildLane(scope: string | undefined, childUuid: string): void;
|
|
292
|
+
/** run-leg 车道:该子代是否已有 BCE 真行(有 ⇒ 让位,不铸复合 id 行)。 */
|
|
293
|
+
backgroundChildLaneRow(scope: string | undefined, childUuid: string): string | undefined;
|
|
275
294
|
/** Upsert a workflow row (MERGE by `id`) + fan out. */
|
|
276
295
|
publishWorkflow(delta: Partial<FleetWorkflowRow> & {
|
|
277
296
|
id: string;
|
package/dist/fleet/fleet-bus.js
CHANGED
|
@@ -56,6 +56,37 @@ export class FleetEventBus {
|
|
|
56
56
|
if (this.tasks.delete(id))
|
|
57
57
|
this.emit({ type: "task_remove", id, ts: this.now() });
|
|
58
58
|
}
|
|
59
|
+
// ┌─ [2070]①/[2080] 让位登记簿(BCE 车道 → run-leg 车道的单向让位信号)────────────────────────────┐
|
|
60
|
+
// 为什么登记簿而不是"查快照有没有 a* 行":两条车道用**不同 id 域**给同一个 agent 建行(BCE=`a*`/`wa*`
|
|
61
|
+
// 稳定 handle;run-leg=`${runId} ${childUuid}` 复合键),快照里没有可直接比对的键。join 键 = core 在
|
|
62
|
+
// BCE 帧上给的 `progressTaskId`(= run-leg tick 的 `taskId`,即子代 uuid),[2080] 明确它已在 tick/terminal
|
|
63
|
+
// 帧上。BCE 车道学到 uuid 即 claim,terminal 即 release;run-leg 车道 claim 在场就让位不铸。
|
|
64
|
+
// 单向 + 只增可见性:claim 丢了(容量逐出/旧 core 不发 progressTaskId)= 退回双生行的旧行为,不会错杀。
|
|
65
|
+
bceClaims = new Map(); // `${scope}\u0000${childUuid}` → a* 行 id
|
|
66
|
+
static BCE_CLAIM_MAX = 4096;
|
|
67
|
+
static bceKey(scope, childUuid) { return `${scope ?? "default"}\u0000${childUuid}`; }
|
|
68
|
+
/** BCE 车道:本车道已为该子代 uuid 建了真行 —— run-leg 车道自此让位。幂等。 */
|
|
69
|
+
claimBackgroundChildLane(scope, childUuid, rowId) {
|
|
70
|
+
const k = FleetEventBus.bceKey(scope, childUuid);
|
|
71
|
+
this.bceClaims.delete(k);
|
|
72
|
+
this.bceClaims.set(k, rowId);
|
|
73
|
+
// 容量兜底:逐最老。丢一条 claim 只是让那个 agent 退回"双生行"旧行为(可见性回退,不是错行),
|
|
74
|
+
// 所以这里可以简单逐出——与 Meta 表"活溯源绝不逐出"的取舍不同,因为丢 claim 无归因损失。
|
|
75
|
+
if (this.bceClaims.size > FleetEventBus.BCE_CLAIM_MAX) {
|
|
76
|
+
const oldest = this.bceClaims.keys().next();
|
|
77
|
+
if (!oldest.done)
|
|
78
|
+
this.bceClaims.delete(oldest.value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** BCE 车道:该子代已终态离场 —— 让位解除(后续同 uuid 的迟到 tick 回到旧行为)。幂等。 */
|
|
82
|
+
releaseBackgroundChildLane(scope, childUuid) {
|
|
83
|
+
this.bceClaims.delete(FleetEventBus.bceKey(scope, childUuid));
|
|
84
|
+
}
|
|
85
|
+
/** run-leg 车道:该子代是否已有 BCE 真行(有 ⇒ 让位,不铸复合 id 行)。 */
|
|
86
|
+
backgroundChildLaneRow(scope, childUuid) {
|
|
87
|
+
return this.bceClaims.get(FleetEventBus.bceKey(scope, childUuid));
|
|
88
|
+
}
|
|
89
|
+
// └────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
59
90
|
/** Upsert a workflow row (MERGE by `id`) + fan out. */
|
|
60
91
|
publishWorkflow(delta) {
|
|
61
92
|
const merged = { ...(this.workflows.get(delta.id) ?? { id: delta.id, name: delta.id, status: "running" }), ...delta };
|
|
@@ -125,6 +156,36 @@ export function fleetRunPublisher(bus, run) {
|
|
|
125
156
|
const t = usage?.toolUses;
|
|
126
157
|
return typeof t === "number" ? t : undefined;
|
|
127
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* 🔴 [2070]①/[2080] 让位判据(run-leg 侧半场,server 3.5.4)。
|
|
161
|
+
*
|
|
162
|
+
* 病灶(cli [2070]① 实测):本车道给**每一条**子代 tick 铸复合 id 行 `${runId} ${childTaskId}`,而
|
|
163
|
+
* BCE 车道(`fleetBackgroundChildPublisher`)对同一个 agent 已有 `a*`/`wa*` 稳定 handle 真行 ⇒ 同屏两行
|
|
164
|
+
* (424 次采样 180 次 max=2,连续可见窗约 15s)。BCE 侧此前只有**反应式**清扫(首 tick 一次性后缀扫),
|
|
165
|
+
* 清完本车道下一拍又铸回来,所以 6 双生帧 : 7 真帧。
|
|
166
|
+
*
|
|
167
|
+
* [2080] 裁定=**双生臂让位、不铸新 id 域**(join 键 progressTaskId 已在 BCE 帧上)。本函数即让位闸:
|
|
168
|
+
* 同 (scope, childTaskId) 已有 BCE claim ⇒ 返回 true,调用点**不发行帧**;并把本车道**先前**可能已铸的
|
|
169
|
+
* 复合行清场(claim 可能晚于我们第一拍到达),此后该 agent 单源。
|
|
170
|
+
*
|
|
171
|
+
* 为什么不是"直接删掉复合行铸造":前台 delegation(同步腿委派的子代)**不过** background-child
|
|
172
|
+
* registry ⇒ 永远没有 BCE 真行,让位就等于让这些 agent 从面板消失。那一形保留复合行,并在帧上带
|
|
173
|
+
* `sourceLane:"run-leg"` 明示这是过渡形——消费端不必再自己发明集合级去重判据。
|
|
174
|
+
*/
|
|
175
|
+
const yieldToBackgroundChildLane = (childTaskId, cid) => {
|
|
176
|
+
if (bus.backgroundChildLaneRow(run.scope, childTaskId) === undefined)
|
|
177
|
+
return false;
|
|
178
|
+
if (children.has(cid)) {
|
|
179
|
+
// 我们早于 claim 铸过一行 —— 清场(BCE 侧的后缀扫是同一意图的反应式兜底,两者幂等叠加无害)
|
|
180
|
+
bus.removeTask(cid);
|
|
181
|
+
children.delete(cid);
|
|
182
|
+
childStartedAt.delete(cid);
|
|
183
|
+
for (const [k, v] of childByToolCall)
|
|
184
|
+
if (v === cid)
|
|
185
|
+
childByToolCall.delete(k);
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
};
|
|
128
189
|
const sumTokens = (usage) => {
|
|
129
190
|
const u = usage;
|
|
130
191
|
if (!u)
|
|
@@ -158,6 +219,8 @@ export function fleetRunPublisher(bus, run) {
|
|
|
158
219
|
else if (ev.type === "task_progress" && ev.taskId) {
|
|
159
220
|
// core 1.147 subagent usage tick (per-turn, cumulative) → a CHILD fleet row nested under the run.
|
|
160
221
|
const cid = childId(ev.taskId);
|
|
222
|
+
if (yieldToBackgroundChildLane(ev.taskId, cid))
|
|
223
|
+
return; // [2070]① BCE 真行在场 ⇒ 让位不铸
|
|
161
224
|
children.add(cid);
|
|
162
225
|
// BC-2 (core 1.151): use the subagent's sanitized display `name` (taskName/agent-type) for the child row.
|
|
163
226
|
// [2070]③:name 缺席时**不铸**(此前回落 taskId=子代 uuid,人眼乱码)——行 IDENTITY 一直是 `cid`
|
|
@@ -176,7 +239,7 @@ export function fleetRunPublisher(bus, run) {
|
|
|
176
239
|
// [1415]③ 点亮半场:core 1.354 起 settle 时补终态 tick(status 值域 additive 扩 completed|failed;
|
|
177
240
|
// 此前恒 "running"=行为逐位不变)——workflow 子的 uuid 行终于有完成态可见(running→removed 的
|
|
178
241
|
// [1414]#3 形就此闭)。
|
|
179
|
-
bus.publishTask({ id: cid, ...(childName !== undefined ? { name: childName, agentType: childName } : {}), parentId: run.runId, scope: run.scope, ...sessionField, status: tickStatus(ev.status), tokens: sumTokens(ev.usage), ...(() => { const t = toolUsesOf(ev.usage); return t !== undefined ? { toolUses: t } : {}; })(), elapsedMs: Date.now() - childStartedAt.get(cid) });
|
|
242
|
+
bus.publishTask({ id: cid, sourceLane: "run-leg", ...(childName !== undefined ? { name: childName, agentType: childName } : {}), parentId: run.runId, scope: run.scope, ...sessionField, status: tickStatus(ev.status), tokens: sumTokens(ev.usage), ...(() => { const t = toolUsesOf(ev.usage); return t !== undefined ? { toolUses: t } : {}; })(), elapsedMs: Date.now() - childStartedAt.get(cid) });
|
|
180
243
|
}
|
|
181
244
|
else if (ev.type === "tool_start" && ev.toolName) {
|
|
182
245
|
// BC-1 (clay 2026-06-27): the top-level run row's `description` = LIVE ACTIVITY (the tool now running), NOT
|
|
@@ -210,6 +273,8 @@ export function fleetRunPublisher(bus, run) {
|
|
|
210
273
|
if (ev.type !== "task_progress" || !ev.taskId)
|
|
211
274
|
return;
|
|
212
275
|
const cid = childId(ev.taskId);
|
|
276
|
+
if (yieldToBackgroundChildLane(ev.taskId, cid))
|
|
277
|
+
return; // [2070]① BCE 真行在场 ⇒ 让位不铸
|
|
213
278
|
children.add(cid);
|
|
214
279
|
const parentId = ev.parentTaskId && ev.parentTaskId !== run.rootTaskId ? childId(ev.parentTaskId) : run.runId;
|
|
215
280
|
// [WF2-A] redact the forwarded subagent name for parity with the top-level run name (see onEvent above).
|
|
@@ -220,7 +285,7 @@ export function fleetRunPublisher(bus, run) {
|
|
|
220
285
|
childStartedAt.set(cid, Date.now());
|
|
221
286
|
if (ev.parentToolCallId)
|
|
222
287
|
childByToolCall.set(ev.parentToolCallId, cid);
|
|
223
|
-
bus.publishTask({ id: cid, ...(fwdName !== undefined ? { name: fwdName, agentType: fwdName } : {}), parentId, scope: run.scope, ...sessionField, status: tickStatus(ev.status), tokens: sumTokens(ev.usage), ...(() => { const t = toolUsesOf(ev.usage); return t !== undefined ? { toolUses: t } : {}; })(), elapsedMs: Date.now() - childStartedAt.get(cid) }); // [1415]③ 同上;[1748] toolUses 同 bg lane 口径
|
|
288
|
+
bus.publishTask({ id: cid, sourceLane: "run-leg", ...(fwdName !== undefined ? { name: fwdName, agentType: fwdName } : {}), parentId, scope: run.scope, ...sessionField, status: tickStatus(ev.status), tokens: sumTokens(ev.usage), ...(() => { const t = toolUsesOf(ev.usage); return t !== undefined ? { toolUses: t } : {}; })(), elapsedMs: Date.now() - childStartedAt.get(cid) }); // [1415]③ 同上;[1748] toolUses 同 bg lane 口径
|
|
224
289
|
},
|
|
225
290
|
onChildTerminal(taskId, status, altId, toolUseId) {
|
|
226
291
|
// A background subagent/bash CHILD settles — task_progress never emits a terminal tick,
|
|
@@ -375,6 +440,24 @@ export function fleetBackgroundChildPublisher(bus, log) {
|
|
|
375
440
|
}
|
|
376
441
|
}
|
|
377
442
|
};
|
|
443
|
+
/**
|
|
444
|
+
* [2070]①/[2080] 让位登记(BCE 侧半场)。把「本车道已为这个子代 uuid 建了真行」登记到 bus 上,
|
|
445
|
+
* run-leg 车道的 task_progress 臂据此让位(不铸 `${runId} ${uuid}` 复合行)。
|
|
446
|
+
*
|
|
447
|
+
* uuid 从哪来(两源,先到先得):①`e.sessionId` —— fork 形 spawn 就带(= forkedId);
|
|
448
|
+
* ②`e.progressTaskId` —— core 在 tick/terminal 帧上给的 join 键([2080] 逐字确认已在场),普通 bg
|
|
449
|
+
* producer 要等首 tick 才现。两者都是**同一个** run-leg tick 的 `taskId`,这就是 join 的依据。
|
|
450
|
+
*
|
|
451
|
+
* `progressParentTaskId` 判别照 suppressedTwin 同款:只有**本 child 自己**的 tick 才算数——嵌套孙代的
|
|
452
|
+
* 帧带的是父的 uuid,拿它 claim 会把父的 run-leg 行误让位掉(父在前台 delegation 形下没有 BCE 真行)。
|
|
453
|
+
* 旧 core 不发这两键 ⇒ 无 claim ⇒ 退回双生行旧行为(可见性回退,不错杀)。
|
|
454
|
+
*/
|
|
455
|
+
const claimIfKnown = (e, m) => {
|
|
456
|
+
const own = !e.progressParentTaskId || e.progressParentTaskId === (m.parentTaskId ?? m.parentSessionLink);
|
|
457
|
+
const uuid = e.sessionId ?? (own ? e.progressTaskId : undefined);
|
|
458
|
+
if (uuid)
|
|
459
|
+
bus.claimBackgroundChildLane(m.tenantScope, uuid, e.taskId);
|
|
460
|
+
};
|
|
378
461
|
// [1338]③ 换算臂:core spawn emit 现把 hostSessionId 装进 parentTaskId(会话 id,非行 id)→ 子行
|
|
379
462
|
// parentId 指向不存在的行=孤儿伪影(值域修在 core)。server 兜:parentTaskId 不是现存行 id、却恰是
|
|
380
463
|
// 某 TOP-LEVEL run 行的 sessionId 时,换算成该 run 行 id。guard 式——core 修后 parentTaskId 直接命中
|
|
@@ -544,6 +627,9 @@ export function fleetBackgroundChildPublisher(bus, log) {
|
|
|
544
627
|
: {}),
|
|
545
628
|
};
|
|
546
629
|
remember(e.taskId, m);
|
|
630
|
+
// [2070]① 让位登记(spawn 臂):fork 形 spawn 就带 sessionId=子代 uuid;普通 bg producer 的 spawn
|
|
631
|
+
// 不带,要等首 tick 的 progressTaskId(下方 handleTick 补 claim)。
|
|
632
|
+
claimIfKnown(e, m);
|
|
547
633
|
// [1364]③:spawn 帧 1.349+ 也带 agentType(类型名)/name——行 name 位保 description(display 标签,
|
|
548
634
|
// 历史语义),TYPE 列自 spawn 即亮(此前要等首 tick 自愈)。
|
|
549
635
|
// [1827]①(cli):spawn 行也透传 transcriptId(委派 prompt 的映射,详情页运行中即可取件)。
|
|
@@ -554,6 +640,9 @@ export function fleetBackgroundChildPublisher(bus, log) {
|
|
|
554
640
|
dlog("bg_child_event", { kind: "spawn", taskId: e.taskId, sessionScoped: e.sessionScoped, scope: m.tenantScope, hostSessionId: m.hostSessionId ?? null, parentTaskId: m.parentTaskId ?? null });
|
|
555
641
|
};
|
|
556
642
|
const handleTick = (e, m) => {
|
|
643
|
+
// [2070]① 让位登记(tick 臂):普通 bg 形的 uuid 首次可见就在这里。**在**下面任何发布之前 claim,
|
|
644
|
+
// 这样即便 run-leg 的 forward 与本 tick 同拍到达,run-leg 也已经能看到让位信号。
|
|
645
|
+
claimIfKnown(e, m);
|
|
557
646
|
// dedup the per-leg uuid-keyed twin ONCE: the leg's onForwardEvent keys the same child's rows by
|
|
558
647
|
// `${runId} ${uuid}` and on the /v1/runs path runId === the host canonical taskId (spec.taskId is the
|
|
559
648
|
// durable run id). The tick that carries the child's OWN frame has progressParentTaskId === the host
|
|
@@ -632,6 +721,9 @@ export function fleetBackgroundChildPublisher(bus, log) {
|
|
|
632
721
|
: {}),
|
|
633
722
|
});
|
|
634
723
|
bus.removeTask(e.taskId);
|
|
724
|
+
// [2070]① 让位解除:本子代已终态离场,同 uuid 的迟到帧回到旧行为(claim 只在活期有效)。
|
|
725
|
+
if (m.aliasUuid)
|
|
726
|
+
bus.releaseBackgroundChildLane(m.tenantScope, m.aliasUuid);
|
|
635
727
|
// codex 1.240-R1(M2):1.356 的回调序=bceTick 先于 host forward——tick 处的双生抑制扫在 uuid 双生
|
|
636
728
|
// 行发布**之前**,one-shot 空扫后置旗,双生行永存。terminal 补扫一次(aliasUuid=tick 学到的 child
|
|
637
729
|
// uuid),把 run-leg 的复合键行清场。
|
package/dist/fleet-client.d.ts
CHANGED
|
@@ -24,10 +24,19 @@ export declare const FLEET_USAGE_KEY_CAP = 1024;
|
|
|
24
24
|
export declare class FleetUsageAccumulator {
|
|
25
25
|
private cells;
|
|
26
26
|
private runsSeen;
|
|
27
|
+
/**
|
|
28
|
+
* 🔴 RB-368 边界(core 2.3.0):`d.costMicroUsd` **可缺席 = 未知**(unpriced 部署,budget.ts 如实不带键)。
|
|
29
|
+
* 这里只能折成「不计入已知和」(`?? 0`)——**不是**因为未知等于 0,而是因为 wire 上表达不了:
|
|
30
|
+
* registry-core 的 `UsageEntry.costMicroUsd` 是 `z.number()` **必填**,batch 报文没有缺席位。
|
|
31
|
+
* 后果如实记账:center 侧看到的是**低估**($ 轴少算),不是「未知」;配额轴(weightedTokens)不受影响,
|
|
32
|
+
* 仍逐笔精确 —— 也就是说 lease/配额治理在 unpriced 部署下靠 token 轴仍然咬得住,$ 轴形同未启用。
|
|
33
|
+
* 要真治:registry-core 把该键改 optional(或加 `costUnknownCalls`)+ center 聚合面同改,再回本处透传。
|
|
34
|
+
* 在那之前,请**不要**把这个 `?? 0` 读成「已裁定未知=0」——裁定见 budget.ts 的 #59/RB-368 块。
|
|
35
|
+
*/
|
|
27
36
|
record(principal: string | undefined, model: string, taskId: string, d: {
|
|
28
37
|
inputTokens: number;
|
|
29
38
|
outputTokens: number;
|
|
30
|
-
costMicroUsd
|
|
39
|
+
costMicroUsd?: number;
|
|
31
40
|
weightedTokens?: number;
|
|
32
41
|
quotaWeightAtUse?: number;
|
|
33
42
|
}): void;
|
package/dist/fleet-client.js
CHANGED
|
@@ -24,6 +24,15 @@ export const FLEET_USAGE_KEY_CAP = 1024;
|
|
|
24
24
|
export class FleetUsageAccumulator {
|
|
25
25
|
cells = new Map(); // key = principal <NUL> model
|
|
26
26
|
runsSeen = new Set(); // taskId per window → runs count (first brain.call of a run in this window)
|
|
27
|
+
/**
|
|
28
|
+
* 🔴 RB-368 边界(core 2.3.0):`d.costMicroUsd` **可缺席 = 未知**(unpriced 部署,budget.ts 如实不带键)。
|
|
29
|
+
* 这里只能折成「不计入已知和」(`?? 0`)——**不是**因为未知等于 0,而是因为 wire 上表达不了:
|
|
30
|
+
* registry-core 的 `UsageEntry.costMicroUsd` 是 `z.number()` **必填**,batch 报文没有缺席位。
|
|
31
|
+
* 后果如实记账:center 侧看到的是**低估**($ 轴少算),不是「未知」;配额轴(weightedTokens)不受影响,
|
|
32
|
+
* 仍逐笔精确 —— 也就是说 lease/配额治理在 unpriced 部署下靠 token 轴仍然咬得住,$ 轴形同未启用。
|
|
33
|
+
* 要真治:registry-core 把该键改 optional(或加 `costUnknownCalls`)+ center 聚合面同改,再回本处透传。
|
|
34
|
+
* 在那之前,请**不要**把这个 `?? 0` 读成「已裁定未知=0」——裁定见 budget.ts 的 #59/RB-368 块。
|
|
35
|
+
*/
|
|
27
36
|
record(principal, model, taskId, d) {
|
|
28
37
|
const p = principal ?? "__anonymous__"; // single-user turnkey: no principal — still meter, under a stable bucket
|
|
29
38
|
let key = `${p}\u0000${model}`;
|
|
@@ -36,7 +45,7 @@ export class FleetUsageAccumulator {
|
|
|
36
45
|
if (cur) {
|
|
37
46
|
cur.inputTokens += d.inputTokens;
|
|
38
47
|
cur.outputTokens += d.outputTokens;
|
|
39
|
-
cur.costMicroUsd += d.costMicroUsd;
|
|
48
|
+
cur.costMicroUsd += d.costMicroUsd ?? 0; // 缺席=未知 ⇒ 不计入已知和(wire 必填,见上方旁注)
|
|
40
49
|
cur.weightedTokens += d.weightedTokens ?? 0;
|
|
41
50
|
if (d.quotaWeightAtUse !== undefined)
|
|
42
51
|
cur.quotaWeightAtUse = d.quotaWeightAtUse; // 审计尾迹=最新倍率(窗口内基本恒定)
|
|
@@ -44,7 +53,7 @@ export class FleetUsageAccumulator {
|
|
|
44
53
|
cur.runs += 1;
|
|
45
54
|
}
|
|
46
55
|
else {
|
|
47
|
-
this.cells.set(key, { inputTokens: d.inputTokens, outputTokens: d.outputTokens, costMicroUsd: d.costMicroUsd, weightedTokens: d.weightedTokens ?? 0, quotaWeightAtUse: d.quotaWeightAtUse ?? 1, runs: firstOfRun ? 1 : 0 });
|
|
56
|
+
this.cells.set(key, { inputTokens: d.inputTokens, outputTokens: d.outputTokens, costMicroUsd: d.costMicroUsd ?? 0, weightedTokens: d.weightedTokens ?? 0, quotaWeightAtUse: d.quotaWeightAtUse ?? 1, runs: firstOfRun ? 1 : 0 });
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
/** Drain the window into UsageReport entries and RESET. Empty window → []. */
|
package/dist/fleet-lease.d.ts
CHANGED
|
@@ -95,7 +95,13 @@ export declare class FleetLeaseManager {
|
|
|
95
95
|
/** Local deduction — wired at the SAME tracer `brain.call` point as the usage accumulator (ALS principal),
|
|
96
96
|
* sync + never throws (TracerHook contract). No-op unless the principal currently holds a lease.
|
|
97
97
|
* registry-core 0.6.1 双轴:weightedTokens(加权 tokens 配额轴,weight-at-burn 已在调用方固化)
|
|
98
|
-
* + costMicroUsd($ 轴,轴名沿用 0.5 既定)。任一轴耗尽=leaseExhausted 拒(deny-wins,registry 类型层定死)。
|
|
98
|
+
* + costMicroUsd($ 轴,轴名沿用 0.5 既定)。任一轴耗尽=leaseExhausted 拒(deny-wins,registry 类型层定死)。
|
|
99
|
+
*
|
|
100
|
+
* 🔴 RB-368(core 2.3.0)已核 undefined 安全:两轴都是 `?? 0`,unpriced 部署下 budget.ts 不带
|
|
101
|
+
* costMicroUsd 键调进来 ⇒ $ 轴不扣、不产 NaN。**这是有意的 fail-open**:租约是**执行面**,把"未知"
|
|
102
|
+
* 当"扣满"会让没配价目表的部署一上来就被 leaseExhausted 拒掉所有请求(deny-wins),那是把可观测性
|
|
103
|
+
* 缺口升级成可用性事故。补偿=配额轴(weightedTokens)不依赖价目表、逐笔精确,治理在 unpriced 部署下
|
|
104
|
+
* 仍然咬得住;$ 轴对这类部署等于未启用,这一点在部署位 `capabilities.pricingConfigured` 上可读回。 */
|
|
99
105
|
recordSpend(principal: string | undefined, d: {
|
|
100
106
|
weightedTokens?: number;
|
|
101
107
|
costMicroUsd?: number;
|
package/dist/fleet-lease.js
CHANGED
|
@@ -74,7 +74,13 @@ export class FleetLeaseManager {
|
|
|
74
74
|
/** Local deduction — wired at the SAME tracer `brain.call` point as the usage accumulator (ALS principal),
|
|
75
75
|
* sync + never throws (TracerHook contract). No-op unless the principal currently holds a lease.
|
|
76
76
|
* registry-core 0.6.1 双轴:weightedTokens(加权 tokens 配额轴,weight-at-burn 已在调用方固化)
|
|
77
|
-
* + costMicroUsd($ 轴,轴名沿用 0.5 既定)。任一轴耗尽=leaseExhausted 拒(deny-wins,registry 类型层定死)。
|
|
77
|
+
* + costMicroUsd($ 轴,轴名沿用 0.5 既定)。任一轴耗尽=leaseExhausted 拒(deny-wins,registry 类型层定死)。
|
|
78
|
+
*
|
|
79
|
+
* 🔴 RB-368(core 2.3.0)已核 undefined 安全:两轴都是 `?? 0`,unpriced 部署下 budget.ts 不带
|
|
80
|
+
* costMicroUsd 键调进来 ⇒ $ 轴不扣、不产 NaN。**这是有意的 fail-open**:租约是**执行面**,把"未知"
|
|
81
|
+
* 当"扣满"会让没配价目表的部署一上来就被 leaseExhausted 拒掉所有请求(deny-wins),那是把可观测性
|
|
82
|
+
* 缺口升级成可用性事故。补偿=配额轴(weightedTokens)不依赖价目表、逐笔精确,治理在 unpriced 部署下
|
|
83
|
+
* 仍然咬得住;$ 轴对这类部署等于未启用,这一点在部署位 `capabilities.pricingConfigured` 上可读回。 */
|
|
78
84
|
recordSpend(principal, d) {
|
|
79
85
|
if (!principal)
|
|
80
86
|
return;
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import type { SessionTreeEntry } from "@sema-agent/core";
|
|
2
|
+
/**
|
|
3
|
+
* ── [2092] 渲染臂的判据:**证据面与模型所见同进同出** ─────────────────────────────────────────────
|
|
4
|
+
* core 的 `SessionTreeEntry` 是 12 型联合,曾经这里只渲 message/compaction 两支 ⇒ 10 型静默丢弃。
|
|
5
|
+
* 其中 `custom_message` 正是非标准入口用户内容(steering)的落点:core `convertToLlm` 把它按
|
|
6
|
+
* `role:"user"` 送模型,而证据面渲没了 ⇒ 评估者如实判「证据不足」⇒ 拦停死循环直到 CAP(cli 带
|
|
7
|
+
* 最小复现的真缺陷)。所以分类规则不是「渲会话内容」,而是:**模型看得见的必须在,看不见的必须不在**。
|
|
8
|
+
*
|
|
9
|
+
* 两张表 = 对 core 型全集的**显式全分类**;test/branch-transcript.test.ts 里的闭合钉从 core d.ts
|
|
10
|
+
* 逐字取联合成员核对「型全集 = 渲染臂 ∪ 跳过表」—— core 加第 13 型 ⇒ 红,逼一次显式分类,
|
|
11
|
+
* 默认可见(下面循环的 default 臂做通用文本抽取)而不是默认失踪。
|
|
12
|
+
*/
|
|
13
|
+
export declare const RENDERED_ENTRY_TYPES: readonly ["message", "compaction", "custom_message"];
|
|
14
|
+
/**
|
|
15
|
+
* 跳过的理由逐型给(「纯控制型」不是一句话,是逐个核过模型看不见):
|
|
16
|
+
* thinking_level_change / model_change / label / session_info / leaf / prompt_epoch /
|
|
17
|
+
* announced_listing / workspace_state —— 控制面元数据,core 折 messages 时不产出内容;
|
|
18
|
+
* custom —— data-only(session.js 折 messages 只认 message/custom_message 两型),模型看不见它的
|
|
19
|
+
* data;把它渲出来反而是把模型没见过的东西冒充成证据,与 [2092] 是同一判据的反方向。
|
|
20
|
+
*/
|
|
21
|
+
export declare const SKIPPED_ENTRY_TYPES: readonly ["thinking_level_change", "model_change", "label", "session_info", "leaf", "prompt_epoch", "announced_listing", "workspace_state", "custom"];
|
|
2
22
|
/** 渲染结果。**判别式**:证据不足时调用方拿不到 `text`,想用也用不了(见顶注)。 */
|
|
3
23
|
export type BranchTranscript = {
|
|
4
24
|
sufficient: true;
|
|
@@ -22,14 +22,40 @@
|
|
|
22
22
|
* ⇒ 截断时必须在文本里**明说截了**,让模型知道自己看到的是一个后缀,而不是全部。
|
|
23
23
|
*/
|
|
24
24
|
import { redactSecrets } from "../trace/redact.js";
|
|
25
|
+
/**
|
|
26
|
+
* ── [2092] 渲染臂的判据:**证据面与模型所见同进同出** ─────────────────────────────────────────────
|
|
27
|
+
* core 的 `SessionTreeEntry` 是 12 型联合,曾经这里只渲 message/compaction 两支 ⇒ 10 型静默丢弃。
|
|
28
|
+
* 其中 `custom_message` 正是非标准入口用户内容(steering)的落点:core `convertToLlm` 把它按
|
|
29
|
+
* `role:"user"` 送模型,而证据面渲没了 ⇒ 评估者如实判「证据不足」⇒ 拦停死循环直到 CAP(cli 带
|
|
30
|
+
* 最小复现的真缺陷)。所以分类规则不是「渲会话内容」,而是:**模型看得见的必须在,看不见的必须不在**。
|
|
31
|
+
*
|
|
32
|
+
* 两张表 = 对 core 型全集的**显式全分类**;test/branch-transcript.test.ts 里的闭合钉从 core d.ts
|
|
33
|
+
* 逐字取联合成员核对「型全集 = 渲染臂 ∪ 跳过表」—— core 加第 13 型 ⇒ 红,逼一次显式分类,
|
|
34
|
+
* 默认可见(下面循环的 default 臂做通用文本抽取)而不是默认失踪。
|
|
35
|
+
*/
|
|
36
|
+
export const RENDERED_ENTRY_TYPES = ["message", "compaction", "custom_message"];
|
|
37
|
+
/**
|
|
38
|
+
* 跳过的理由逐型给(「纯控制型」不是一句话,是逐个核过模型看不见):
|
|
39
|
+
* thinking_level_change / model_change / label / session_info / leaf / prompt_epoch /
|
|
40
|
+
* announced_listing / workspace_state —— 控制面元数据,core 折 messages 时不产出内容;
|
|
41
|
+
* custom —— data-only(session.js 折 messages 只认 message/custom_message 两型),模型看不见它的
|
|
42
|
+
* data;把它渲出来反而是把模型没见过的东西冒充成证据,与 [2092] 是同一判据的反方向。
|
|
43
|
+
*/
|
|
44
|
+
export const SKIPPED_ENTRY_TYPES = [
|
|
45
|
+
"thinking_level_change",
|
|
46
|
+
"model_change",
|
|
47
|
+
"label",
|
|
48
|
+
"session_info",
|
|
49
|
+
"leaf",
|
|
50
|
+
"prompt_epoch",
|
|
51
|
+
"announced_listing",
|
|
52
|
+
"workspace_state",
|
|
53
|
+
"custom",
|
|
54
|
+
];
|
|
25
55
|
/** 单次评估送进模型的 transcript 字符上限。超出保留**尾部**(最近的对话对"条件是否已达成"更相关)。 */
|
|
26
56
|
export const BRANCH_TRANSCRIPT_MAX_CHARS = 60_000;
|
|
27
|
-
/**
|
|
28
|
-
function
|
|
29
|
-
const m = entry.message;
|
|
30
|
-
if (!m || typeof m.role !== "string")
|
|
31
|
-
return undefined;
|
|
32
|
-
const c = m.content;
|
|
57
|
+
/** content(string | 片段数组)→ 可读文本。只取 text 片段;image/工具结果等非文本片段不进 transcript。 */
|
|
58
|
+
function contentText(c) {
|
|
33
59
|
const parts = [];
|
|
34
60
|
if (typeof c === "string")
|
|
35
61
|
parts.push(c);
|
|
@@ -40,7 +66,36 @@ function messageText(entry) {
|
|
|
40
66
|
parts.push(t);
|
|
41
67
|
}
|
|
42
68
|
}
|
|
43
|
-
|
|
69
|
+
return parts.join("\n").trim();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `role:"bashExecution"` 消息(`!` 命令)→ 模型所见文本。镜像 core `bashExecutionToText`
|
|
73
|
+
* (dist/engine/harness/messages.js —— core 根不导出,本地镜像,锚在闭合钉+本注上):
|
|
74
|
+
* convertToLlm 把它按 `role:"user"` 送模型,除非 `excludeFromContext`(那时模型看不见 ⇒ 证据面也不该有)。
|
|
75
|
+
*/
|
|
76
|
+
function bashExecutionText(m) {
|
|
77
|
+
if (m["excludeFromContext"])
|
|
78
|
+
return undefined;
|
|
79
|
+
let text = `Ran \`${String(m["command"] ?? "")}\`\n`;
|
|
80
|
+
const out = m["output"];
|
|
81
|
+
text += typeof out === "string" && out ? `\`\`\`\n${out}\n\`\`\`` : "(no output)";
|
|
82
|
+
if (m["cancelled"])
|
|
83
|
+
text += "\n\n(command cancelled)";
|
|
84
|
+
else if (m["exitCode"] !== null && m["exitCode"] !== undefined && m["exitCode"] !== 0)
|
|
85
|
+
text += `\n\nCommand exited with code ${String(m["exitCode"])}`;
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
/** 从一条 message entry 里抽人类可读文本。 */
|
|
89
|
+
function messageText(entry) {
|
|
90
|
+
const m = entry.message;
|
|
91
|
+
if (!m || typeof m.role !== "string")
|
|
92
|
+
return undefined;
|
|
93
|
+
if (m.role === "bashExecution") {
|
|
94
|
+
const text = bashExecutionText(m)?.trim();
|
|
95
|
+
// convertToLlm 对 bashExecution 的送模型 role 是 "user" —— 证据面同款,别让评估者猜生僻 role。
|
|
96
|
+
return text ? { role: "user", text } : undefined;
|
|
97
|
+
}
|
|
98
|
+
const text = contentText(m.content);
|
|
44
99
|
return text ? { role: m.role, text } : undefined;
|
|
45
100
|
}
|
|
46
101
|
/**
|
|
@@ -64,7 +119,23 @@ export function renderBranchTranscript(entries, opts) {
|
|
|
64
119
|
const sum = e.summary;
|
|
65
120
|
if (typeof sum === "string" && sum.trim())
|
|
66
121
|
lines.push(`[earlier conversation, summarized] ${sum.trim()}`);
|
|
122
|
+
continue;
|
|
67
123
|
}
|
|
124
|
+
if (e.type === "custom_message") {
|
|
125
|
+
// [2092] 真缺陷的落点:steering 等非标准入口的用户内容。core convertToLlm 把它按 role:"user"
|
|
126
|
+
// 送模型(display 只管 UI,不改模型可见性)—— 证据面同进同出,渲成 user 行。
|
|
127
|
+
const text = contentText(e.content);
|
|
128
|
+
if (text)
|
|
129
|
+
lines.push(`user: ${text}`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (SKIPPED_ENTRY_TYPES.includes(e.type))
|
|
133
|
+
continue;
|
|
134
|
+
// default 臂:比我们的分类表更新的型(core 加了第 13 型、部署窗里还没重新分类)——
|
|
135
|
+
// **默认可见而不是默认失踪**:通用文本抽取,能抽出来就带型标渲出;闭合钉会在 CI 红,逼显式分类。
|
|
136
|
+
const generic = contentText(e.content ?? e.text);
|
|
137
|
+
if (generic)
|
|
138
|
+
lines.push(`[${e.type}] ${generic}`);
|
|
68
139
|
}
|
|
69
140
|
if (lines.length === 0)
|
|
70
141
|
return { sufficient: false, reason: "nothing-renderable" };
|
|
@@ -65,6 +65,26 @@ function isWithin(root, child) {
|
|
|
65
65
|
// (or absolute, on Windows drive change) path for an outside/sibling target.
|
|
66
66
|
return rel.length > 0 && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* SERIALIZE git worktree ADMIN ops per repo (MODULE-level, keyed by normalized repoRoot). `git worktree
|
|
70
|
+
* add`/`remove`/`prune` all mutate the same repo's `.git/worktrees` registry; running any two CONCURRENTLY
|
|
71
|
+
* against one repo races. Two callers share this registry:
|
|
72
|
+
* - the isolation wrapper's mint (`add`) and teardown (`remove`) legs;
|
|
73
|
+
* - the service reaper's `prune` leg (#62 svc3 race: prune used to bypass the wrapper's closure-local
|
|
74
|
+
* lock entirely — the adverse arm is prune observing a mid-mint "registered, dir not yet populated"
|
|
75
|
+
* window and DEREGISTERING the half-minted worktree, handing the task a dir git no longer tracks and
|
|
76
|
+
* making its eventual `git worktree remove` fail loud on a healthy run).
|
|
77
|
+
* A promise-chain mutex per repoRoot: each op runs after the prior one SETTLES (success OR failure — never
|
|
78
|
+
* deadlock). Git ops are ~ms and serial-safe; agents still run fully parallel. The map is bounded by the
|
|
79
|
+
* number of distinct configured repoRoots (operator config, typically 1) — no eviction needed.
|
|
80
|
+
*/
|
|
81
|
+
const gitAdminLocks = new Map();
|
|
82
|
+
function serializeGitAdmin(repoRoot, fn) {
|
|
83
|
+
const key = norm(repoRoot);
|
|
84
|
+
const run = (gitAdminLocks.get(key) ?? Promise.resolve()).then(fn, fn);
|
|
85
|
+
gitAdminLocks.set(key, run.then(() => undefined, () => undefined));
|
|
86
|
+
return run;
|
|
87
|
+
}
|
|
68
88
|
/**
|
|
69
89
|
* Wrap a deployment's `executionEnvFactory` so an agent with `ctx.isolation === "worktree"` runs in its own
|
|
70
90
|
* detached git worktree. See the file header for the full contract + trust gate + caveats.
|
|
@@ -80,19 +100,7 @@ export function withWorktreeIsolation(baseFactory, opts) {
|
|
|
80
100
|
if (!allowedRoots.some((root) => isWithin(root, repoRoot))) {
|
|
81
101
|
throw new Error(`withWorktreeIsolation: repoRoot ${repoRoot} is not within any allowedRoots [${allowedRoots.join(", ")}] — refusing to git-worktree an untrusted path`);
|
|
82
102
|
}
|
|
83
|
-
|
|
84
|
-
// `.git/worktrees` registry under a repo-level lock; running them CONCURRENTLY against one repo races (a
|
|
85
|
-
// parallel fan-out finishing together → the Runner calls several `destroy()` at once → some
|
|
86
|
-
// `git worktree remove` lose the lock and leave the worktree dir+registration behind — an orphan the
|
|
87
|
-
// dir-only `pruneWorktrees` can't reap). A per-wrapper (= per-repoRoot) promise-chain mutex chains every
|
|
88
|
-
// add/remove so they apply one at a time (git ops are fast + serial-safe; the agents themselves still run
|
|
89
|
-
// fully in parallel — only the ~ms git bookkeeping serializes).
|
|
90
|
-
let gitLock = Promise.resolve();
|
|
91
|
-
const serialize = (fn) => {
|
|
92
|
-
const run = gitLock.then(fn, fn); // run after the prior op SETTLES (success OR failure — never deadlock)
|
|
93
|
-
gitLock = run.then(() => undefined, () => undefined);
|
|
94
|
-
return run;
|
|
95
|
-
};
|
|
103
|
+
const serialize = (fn) => serializeGitAdmin(repoRoot, fn);
|
|
96
104
|
return async (ctx) => {
|
|
97
105
|
// TRUST GATE: the ONLY trigger is core's RunInternals-sourced ctx.isolation. We read nothing else from ctx
|
|
98
106
|
// (and never anything from a TaskSpec) to decide isolation — see file header.
|
|
@@ -162,7 +170,9 @@ export function withWorktreeIsolation(baseFactory, opts) {
|
|
|
162
170
|
*/
|
|
163
171
|
export async function reapOrphanWorktrees(baseEnvForGit, repoRoot, logger) {
|
|
164
172
|
try {
|
|
165
|
-
|
|
173
|
+
// #62:prune 动的是与 add/remove 同一张 `.git/worktrees` 注册表 ⇒ 必须过同一把 per-repoRoot 锁
|
|
174
|
+
// (曾绕锁直跑;恶劣臂见 serializeGitAdmin 注)。
|
|
175
|
+
await serializeGitAdmin(repoRoot, () => pruneWorktrees(baseEnvForGit, norm(repoRoot)));
|
|
166
176
|
}
|
|
167
177
|
catch (e) {
|
|
168
178
|
logger?.warn?.("worktree_prune_failed", { repoRoot: norm(repoRoot), error: e instanceof Error ? e.message : String(e) });
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -46,8 +46,19 @@ export declare function attachModelUsage<T extends TaskResult>(result: T, ctx: {
|
|
|
46
46
|
* #59([2052]①):事件体是 **DB 里的 JSON**(旧版本 / 别的 producer 都可能写),此前的 `?? 0` 只接住
|
|
47
47
|
* null/undefined——NaN 与字符串会原样进 `+`,一个坏键就把该模型的整 run 总量变成 NaN 或字符串拼接,
|
|
48
48
|
* 再顺着 `TaskStats.modelUsage` 回声流进 usage-analytics 的整窗聚合。守卫改「非有限数 = 缺席、按 0 计」,
|
|
49
|
-
* 与 usage-analytics
|
|
50
|
-
*
|
|
49
|
+
* 与 usage-analytics 读侧同口径。
|
|
50
|
+
*
|
|
51
|
+
* 🔴 RB-368(core 2.3.0,[2083] 提货单①)——**缺席必须活着走完整条链**。此前本折叠对「costMicroUsd
|
|
52
|
+
* 缺席」与「=0」输出相同(都是 0),那时无所谓:core 恒发数字,缺席没有 producer。现在 unpriced 部署
|
|
53
|
+
* 下 core 行级缺席、budget.ts 如实透传落库,若在这里折 0,诚实就只活到落库为止——回声上又变回
|
|
54
|
+
* 「花了 0」,行级消歧白做。故改**未知传染**:该模型任一贡献行缺 costMicroUsd 键 ⇒ 总量的该键**整个不在**
|
|
55
|
+
* (和里含一个不知道的项,这个和就不知道)。传染按**模型隔离**,不毒别的模型,更不毒整窗。
|
|
56
|
+
*
|
|
57
|
+
* 判别轴(两种"没数"不同罪):
|
|
58
|
+
* · **键缺席** = 契约允许的「未知」(SDK ModelUsageDelta 五键全可选)⇒ 传染,输出也缺席。
|
|
59
|
+
* · **键在场但值坏**(NaN / 字符串 / null)= 数据损坏,写了键的 producer 是在声称自己知道 ⇒ 维持既有
|
|
60
|
+
* 折叠(按 0 计,不传染,已知量不受损),否则一条陈年坏行就能把整个模型的成本抹成"未知"。
|
|
61
|
+
* 下游补偿件早已就位:usage-analytics 的 numOf + `estimated` 标记(缺数值键 ⇒ 总量偏低并诚实标注)。 */
|
|
51
62
|
export declare function aggregateModelUsage(events: RunEvent[]): Record<string, ModelUsageDelta> | undefined;
|
|
52
63
|
export type TraceBlock = {
|
|
53
64
|
type: "thinking";
|
package/dist/trace/project.js
CHANGED
|
@@ -44,11 +44,23 @@ export async function attachModelUsage(result, ctx) {
|
|
|
44
44
|
* #59([2052]①):事件体是 **DB 里的 JSON**(旧版本 / 别的 producer 都可能写),此前的 `?? 0` 只接住
|
|
45
45
|
* null/undefined——NaN 与字符串会原样进 `+`,一个坏键就把该模型的整 run 总量变成 NaN 或字符串拼接,
|
|
46
46
|
* 再顺着 `TaskStats.modelUsage` 回声流进 usage-analytics 的整窗聚合。守卫改「非有限数 = 缺席、按 0 计」,
|
|
47
|
-
* 与 usage-analytics
|
|
48
|
-
*
|
|
47
|
+
* 与 usage-analytics 读侧同口径。
|
|
48
|
+
*
|
|
49
|
+
* 🔴 RB-368(core 2.3.0,[2083] 提货单①)——**缺席必须活着走完整条链**。此前本折叠对「costMicroUsd
|
|
50
|
+
* 缺席」与「=0」输出相同(都是 0),那时无所谓:core 恒发数字,缺席没有 producer。现在 unpriced 部署
|
|
51
|
+
* 下 core 行级缺席、budget.ts 如实透传落库,若在这里折 0,诚实就只活到落库为止——回声上又变回
|
|
52
|
+
* 「花了 0」,行级消歧白做。故改**未知传染**:该模型任一贡献行缺 costMicroUsd 键 ⇒ 总量的该键**整个不在**
|
|
53
|
+
* (和里含一个不知道的项,这个和就不知道)。传染按**模型隔离**,不毒别的模型,更不毒整窗。
|
|
54
|
+
*
|
|
55
|
+
* 判别轴(两种"没数"不同罪):
|
|
56
|
+
* · **键缺席** = 契约允许的「未知」(SDK ModelUsageDelta 五键全可选)⇒ 传染,输出也缺席。
|
|
57
|
+
* · **键在场但值坏**(NaN / 字符串 / null)= 数据损坏,写了键的 producer 是在声称自己知道 ⇒ 维持既有
|
|
58
|
+
* 折叠(按 0 计,不传染,已知量不受损),否则一条陈年坏行就能把整个模型的成本抹成"未知"。
|
|
59
|
+
* 下游补偿件早已就位:usage-analytics 的 numOf + `estimated` 标记(缺数值键 ⇒ 总量偏低并诚实标注)。 */
|
|
49
60
|
export function aggregateModelUsage(events) {
|
|
50
61
|
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
51
62
|
const total = {};
|
|
63
|
+
const costUnknown = new Set(); // RB-368:该模型至少有一行没带 costMicroUsd 键 ⇒ 总量不可知
|
|
52
64
|
let any = false;
|
|
53
65
|
for (const ev of events) {
|
|
54
66
|
if (ev.type !== "model_usage")
|
|
@@ -63,10 +75,17 @@ export function aggregateModelUsage(events) {
|
|
|
63
75
|
cur.outputTokens += num(d.outputTokens);
|
|
64
76
|
cur.cacheReadTokens += num(d.cacheReadTokens);
|
|
65
77
|
cur.cacheWriteTokens += num(d.cacheWriteTokens);
|
|
66
|
-
|
|
78
|
+
// `d` 是 DB 里的 JSON:用 hasOwn 判「键在不在」(值为 undefined 的显式键与缺键同罪——落库后
|
|
79
|
+
// 两者本就无法区分,JSON.stringify 会把显式 undefined 抹掉)。
|
|
80
|
+
if (d !== null && typeof d === "object" && Object.hasOwn(d, "costMicroUsd") && d.costMicroUsd !== undefined)
|
|
81
|
+
cur.costMicroUsd = num(cur.costMicroUsd) + num(d.costMicroUsd);
|
|
82
|
+
else
|
|
83
|
+
costUnknown.add(model);
|
|
67
84
|
total[model] = cur;
|
|
68
85
|
}
|
|
69
86
|
}
|
|
87
|
+
for (const model of costUnknown)
|
|
88
|
+
delete total[model]?.costMicroUsd;
|
|
70
89
|
return any ? total : undefined;
|
|
71
90
|
}
|
|
72
91
|
/** Identity fields core stamps on each content event (1.121.0 `TaskEventIdentity`, shell-host contract E2):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@sema-agent/core": "^2.
|
|
56
|
+
"@sema-agent/core": "^2.3.0",
|
|
57
57
|
"@sema-agent/registry-core": "^0.10.24",
|
|
58
58
|
"e2b": "^2.28.0",
|
|
59
59
|
"libsodium-wrappers": "^0.8.4",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"sharp": "^0.35.3"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@sema-agent/sdk": "^2.1.
|
|
70
|
+
"@sema-agent/sdk": "^2.1.3",
|
|
71
71
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
72
72
|
"@types/node": "22.10.2",
|
|
73
73
|
"@types/pg": "^8.20.0",
|