@sema-agent/client-core 0.44.0 → 0.46.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/CHANGELOG.md +515 -1
- package/README.md +2 -1
- package/dist/adapt/arms.js +33 -0
- package/dist/adapt.d.ts +1 -1
- package/dist/adapt.js +2 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +82 -0
- package/dist/engineSessionParam.d.ts +27 -1
- package/dist/engineSessionParam.js +37 -6
- package/dist/fleet/fleetLedger.d.ts +174 -18
- package/dist/fleet/fleetLedger.js +321 -26
- package/dist/hitl/planReviewWire.d.ts +1 -1
- package/dist/hitl/planReviewWire.js +44 -1
- package/dist/printToolResultFrame.js +6 -1
- package/dist/seam.d.ts +41 -1
- package/dist/seam.js +4 -0
- package/dist/subagent/engineCompactWire.d.ts +32 -5
- package/dist/subagent/engineCompactWire.js +129 -48
- package/dist/subagent/engineDelegatedPrompt.js +9 -4
- package/dist/subagent/engineRowStopGate.d.ts +10 -3
- package/dist/subagent/engineRowStopGate.js +15 -6
- package/dist/subagent/engineSubagentOutput.js +12 -6
- package/dist/subagent/engineSubagentResume.d.ts +53 -3
- package/dist/subagent/engineSubagentResume.js +57 -14
- package/dist/subagent/engineSubagentSteer.js +20 -11
- package/dist/subagent/engineSubagentTail.js +18 -6
- package/dist/subagent/engineTaskHandleWire.js +33 -10
- package/dist/subagentContentStore.d.ts +57 -4
- package/dist/subagentContentStore.js +152 -13
- package/dist/toolResult.js +22 -3
- package/dist/wireErrorTriage.d.ts +41 -0
- package/dist/wireErrorTriage.js +88 -1
- package/docs/INTEGRATION-CLIENTS.md +152 -21
- package/package.json +1 -1
|
@@ -1,12 +1,39 @@
|
|
|
1
|
-
/** Test/introspection hook. */
|
|
2
|
-
export declare function isEngineCompactPending(): boolean;
|
|
3
|
-
/**
|
|
1
|
+
/** Test/introspection hook(缺省 = 默认槽,与 0.44.0 逐字同)。 */
|
|
2
|
+
export declare function isEngineCompactPending(sessionKey?: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Test hook: reset the cached capability (fresh engine / respawn)。
|
|
5
|
+
*
|
|
6
|
+
* 🔴 **只清能力面,绝不动 arm 台账**(异源对抗复审第四轮 [medium] 采纳,修回归):本口是**生产口**
|
|
7
|
+
* (引擎重启 / respawn 的能力失效),不是「取消用户的 /compact」。一度顺手加的 `pendingCompactKeys.clear()`
|
|
8
|
+
* 会让「idle 时 `/compact` 回过 armed → 引擎 respawn → 下一条 run 不再压缩」——用户的命令无声蒸发,
|
|
9
|
+
* 而且那是**默认槽**上的行为改变,与本批「单会话宿主逐字节零变化」的声明直接矛盾。
|
|
10
|
+
* 要清 arm 走 {@link __resetEngineCompactArmForTests}(测试钩,生产面没有这个动作)。
|
|
11
|
+
*/
|
|
4
12
|
export declare function resetManualCompactCapability(): void;
|
|
13
|
+
/** 测试钩:清 arm 台账(module 级单例,同进程多组断言必须能清)。生产面**没有**这个动作。 */
|
|
14
|
+
export declare function __resetEngineCompactArmForTests(): void;
|
|
5
15
|
/**
|
|
6
16
|
* The /compact command's live half. Mid-turn: fire now. Idle: arm for the next run bind.
|
|
7
17
|
* Returns what it did (for the command's display line / tests). No-op ('offline') without a live
|
|
8
18
|
* wire; 'unsupported' when the engine's capabilities said manualCompact:false ([488] TOC-local) —
|
|
9
19
|
* the client-side projection compaction still runs, but the ENGINE session context does not shrink.
|
|
20
|
+
*
|
|
21
|
+
* 🔴 design/285 批 3(D7b):本族三处零参 `engineWireTarget()` 全部换成 `engineWireTargetFor(sessionKey)`。
|
|
22
|
+
* /compact 打的是**这个会话此刻在飞的那条 run**,不是某一行子代 —— owner 台账里没有它,所以槽键
|
|
23
|
+
* 只能由调用方给(多会话宿主每个 REPL 面板一个槽)。缺省 `DEFAULT_SESSION_KEY` ⇒ 与 0.44.0 逐字同。
|
|
24
|
+
* 📋 **带反证的驳回半场(异源对抗复审第三轮 [high] 的另一半,如实记)**:复审还要求把快照提前到
|
|
25
|
+
* **request(arm)那一拍**,理由是「arm 与 bind 之间槽可能被重绑,意图属于 A 却在 B 上执行」。
|
|
26
|
+
* **不采**,反证:arm 那一拍**根本没有 taskId** —— `/compact` 在 idle 下的语义是成文的
|
|
27
|
+
* 「**这个槽的下一条 run**」(见本文件头注的 arm-then-fire 段),槽的身份就是 key 本身。
|
|
28
|
+
* 槽真被重绑到另一台引擎时,「下一条 run」就在那台引擎上,压它才是意图;把 request 那一刻的
|
|
29
|
+
* target 冻下来反而会去压一台**已经不再跑这条会话**的引擎。⇒ 冻结的正确起点是**拿到 taskId 的
|
|
30
|
+
* 那一拍**(bind),那之后的每一次延迟与重试都必须用同一份 —— 那正是上面这一条。
|
|
31
|
+
* 现状由常驻钉 `B3-P31/R6i` 钉住(arm 之后换槽装配 ⇒ 发射打的是**换后**那台,且带换后的会话)。
|
|
32
|
+
* 🔴 **一条动词的槽键必须一以贯之**(异源对抗复审 [high] 采纳):目标、`?session=`、**run id**
|
|
33
|
+
* (`activeEngineRunIdFor`)、**能力位**(per-baseUrl 分桶)、**arm 状态**(per-key 分桶)——
|
|
34
|
+
* 五件全按同一个 key。只换其中几件就是**半 keyed 形**:那比换锚之前更坏(之前五件一致地错在同一个
|
|
35
|
+
* 槽上,一眼可见;半 keyed 是「A 的 run id 发到 B 的服务器」「B 的 arm 被 A 的 bind 吃掉」这类只在
|
|
36
|
+
* 多会话下现形的错)。这条正是 plan 决策链本批**豁免**的同一条理由 —— 那边整条链搬不动,这边搬得动。
|
|
10
37
|
*/
|
|
11
|
-
export declare function requestEngineCompact(): 'fired' | 'armed' | 'offline' | 'unsupported';
|
|
12
|
-
export declare function onEngineTaskBound(taskId: string): void;
|
|
38
|
+
export declare function requestEngineCompact(sessionKey?: string): 'fired' | 'armed' | 'offline' | 'unsupported';
|
|
39
|
+
export declare function onEngineTaskBound(taskId: string, sessionKey?: string): void;
|
|
@@ -36,14 +36,22 @@
|
|
|
36
36
|
* 迁移差分记账:①原 raw POST 无超时 → SDK 缺省 60s 预算(compact 是 202 即回的 arm verb,本地
|
|
37
37
|
* 口毫秒级,纯上界改善);②404 单次 2s 延迟重试(早 header bind 跑赢 run-row 持久化)逐字保留。
|
|
38
38
|
*/
|
|
39
|
-
import { hostLog,
|
|
39
|
+
import { hostLog, hostTimersFor } from '../host.js';
|
|
40
40
|
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
import { engineWireTargetFor } from '../engineWireTarget.js';
|
|
42
|
+
import { activeEngineRunIdFor, engineSessionParamFor } from '../engineSessionParam.js';
|
|
43
|
+
import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
44
|
+
/**
|
|
45
|
+
* 🔴 **arm 状态按会话槽分桶**(design/285 批 3,异源对抗复审 [high] 采纳)。
|
|
46
|
+
*
|
|
47
|
+
* 修前是一个**进程级布尔**:`/compact` 在 B 会话上 idle-arm 之后,**先 bind 的那条会话**(可能是 A)
|
|
48
|
+
* 会把它消费掉 —— B 要的压缩发到了 A 的 run 上(动了一条不相干会话的上下文),而 B 自己永远没压缩。
|
|
49
|
+
* 单会话宿主上这两件事是同一件,keyed 化之后才第一次分得开。
|
|
50
|
+
*/
|
|
51
|
+
const pendingCompactKeys = new Set();
|
|
52
|
+
/** Test/introspection hook(缺省 = 默认槽,与 0.44.0 逐字同)。 */
|
|
53
|
+
export function isEngineCompactPending(sessionKey = DEFAULT_SESSION_KEY) {
|
|
54
|
+
return pendingCompactKeys.has(sessionKey);
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
49
57
|
* [488] 定谳(2026-07-07 指纹探针,server 1.130.0):TOC 本地引擎(无 TiDB runStore)的
|
|
@@ -53,73 +61,109 @@ export function isEngineCompactPending() {
|
|
|
53
61
|
* false ⇒ requestEngineCompact 返回 'unsupported'(不再装作 armed/fired)。探测失败 fail-open
|
|
54
62
|
* (null=未知 ⇒ 保持旧行为发 verb,错误面不变)。
|
|
55
63
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
// 🔴 **判定位与在途探测按 baseUrl 分桶**(design/285 批 3,异源对抗复审 [high] 采纳;与
|
|
65
|
+
// `engineCapsCache` / `engineSubagentOutput.capByBase` 同代)。修前是**进程级单值**:keyed 宿主上
|
|
66
|
+
// `requestEngineCompact('B')` 会拿**先探到的那台引擎**(可能是 A)的 `manualCompact` 位去决定要不要
|
|
67
|
+
// 给 B 发 verb —— 一条动词的槽键必须一以贯之,目标/会话/run id/能力位缺一个都是半 keyed 形。
|
|
68
|
+
// ⚠️ 单会话宿主只有一个 baseUrl ⇒ 分桶后与 0.44.0 逐字同(同一个 base 仍是「探一次、缓存住」)。
|
|
69
|
+
const manualCompactCapableByBase = new Map();
|
|
70
|
+
const capabilitiesProbeByBase = new Map();
|
|
71
|
+
function probeManualCompactCapability(sessionKey = DEFAULT_SESSION_KEY) {
|
|
72
|
+
const cfg = engineWireTargetFor(sessionKey);
|
|
62
73
|
if (!cfg)
|
|
63
74
|
return;
|
|
64
|
-
|
|
75
|
+
const base = cfg.baseUrl;
|
|
76
|
+
if (capabilitiesProbeByBase.has(base))
|
|
77
|
+
return;
|
|
78
|
+
capabilitiesProbeByBase.set(base, (async () => {
|
|
65
79
|
try {
|
|
66
80
|
// SDK 0.0.52 client.capabilities()(typed Capabilities.manualCompact)。5s 预算经 client
|
|
67
81
|
// timeoutMs(对抗复审 MED 原语义:hung engine 不 pin probe promise);非 2xx/网络错/超时 SDK
|
|
68
|
-
// 统一抛错 → 与原「non-200 也重试」同归 catch
|
|
82
|
+
// 统一抛错 → 与原「non-200 也重试」同归 catch 删记号允许后续重试。
|
|
69
83
|
const client = makeEngineWireClient({
|
|
70
|
-
baseUrl:
|
|
84
|
+
baseUrl: base,
|
|
71
85
|
...(cfg.token ? { token: cfg.token } : {}),
|
|
72
86
|
principal: cfg.principal,
|
|
73
87
|
timeoutMs: 5000,
|
|
74
88
|
});
|
|
75
89
|
if (!client) {
|
|
76
|
-
|
|
90
|
+
capabilitiesProbeByBase.delete(base);
|
|
77
91
|
return;
|
|
78
92
|
}
|
|
79
93
|
const caps = await client.capabilities();
|
|
80
94
|
if (typeof caps.manualCompact === 'boolean')
|
|
81
|
-
|
|
95
|
+
manualCompactCapableByBase.set(base, caps.manualCompact);
|
|
82
96
|
else
|
|
83
|
-
|
|
97
|
+
capabilitiesProbeByBase.delete(base); // shape we don't understand — retry later
|
|
84
98
|
}
|
|
85
99
|
catch {
|
|
86
|
-
|
|
100
|
+
capabilitiesProbeByBase.delete(base); // non-2xx/network error/timeout: unknown stays fail-open, next call re-probes
|
|
87
101
|
}
|
|
88
|
-
})();
|
|
102
|
+
})());
|
|
89
103
|
}
|
|
90
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Test hook: reset the cached capability (fresh engine / respawn)。
|
|
106
|
+
*
|
|
107
|
+
* 🔴 **只清能力面,绝不动 arm 台账**(异源对抗复审第四轮 [medium] 采纳,修回归):本口是**生产口**
|
|
108
|
+
* (引擎重启 / respawn 的能力失效),不是「取消用户的 /compact」。一度顺手加的 `pendingCompactKeys.clear()`
|
|
109
|
+
* 会让「idle 时 `/compact` 回过 armed → 引擎 respawn → 下一条 run 不再压缩」——用户的命令无声蒸发,
|
|
110
|
+
* 而且那是**默认槽**上的行为改变,与本批「单会话宿主逐字节零变化」的声明直接矛盾。
|
|
111
|
+
* 要清 arm 走 {@link __resetEngineCompactArmForTests}(测试钩,生产面没有这个动作)。
|
|
112
|
+
*/
|
|
91
113
|
export function resetManualCompactCapability() {
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
manualCompactCapableByBase.clear();
|
|
115
|
+
capabilitiesProbeByBase.clear();
|
|
116
|
+
}
|
|
117
|
+
/** 测试钩:清 arm 台账(module 级单例,同进程多组断言必须能清)。生产面**没有**这个动作。 */
|
|
118
|
+
export function __resetEngineCompactArmForTests() {
|
|
119
|
+
pendingCompactKeys.clear();
|
|
94
120
|
}
|
|
95
121
|
/**
|
|
96
122
|
* 延迟发射(`setTimeout` + `unref` 的 port 形)。缺 TimersPort ⇒ **不发射**并留日志 ——
|
|
97
123
|
* 静默 drop 会让 `/compact` 看起来 armed 了却永不落地(见文件头)。
|
|
98
124
|
*/
|
|
99
|
-
function deferFire(ms, fn, why) {
|
|
100
|
-
|
|
125
|
+
function deferFire(ms, fn, why, sessionKey) {
|
|
126
|
+
// 🔴 定时器也按本槽取(异源对抗复审第四轮 [medium] 采纳):零参 `hostTimers()` 读的是**默认槽** ——
|
|
127
|
+
// 按接入契约只走 `installHostFor(key, {timers})` 的 keyed-only 宿主在那里恒空 ⇒ 快照有效、arm 已划掉、
|
|
128
|
+
// 请求永远不发射。一条动词的槽键必须一以贯之,定时器是第六件。
|
|
129
|
+
// (默认槽下 `hostTimersFor(DEFAULT_SESSION_KEY)` ≡ `hostTimers()`,逐字节等价。)
|
|
130
|
+
const timers = hostTimersFor(sessionKey);
|
|
101
131
|
if (!timers) {
|
|
102
132
|
hostLog('debug', `engineCompactWire: no TimersPort — ${why} 不会发射(端漏装 port,/compact 这条腿静默失效)`);
|
|
103
|
-
return;
|
|
133
|
+
return false;
|
|
104
134
|
}
|
|
105
135
|
timers.setTimer(ms, fn);
|
|
136
|
+
return true;
|
|
106
137
|
}
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
/** 按本槽冻一发;`null` = 那一刻没有 wire(与修前的 `!cfg` 早退同语义,只是提前到冻结那一拍)。 */
|
|
139
|
+
function freezeCompactShot(taskId, sessionKey) {
|
|
140
|
+
const cfg = engineWireTargetFor(sessionKey);
|
|
109
141
|
if (!cfg)
|
|
110
|
-
return;
|
|
142
|
+
return null;
|
|
143
|
+
const sid = engineSessionParamFor(sessionKey);
|
|
144
|
+
return {
|
|
145
|
+
taskId,
|
|
146
|
+
sessionKey,
|
|
147
|
+
baseUrl: cfg.baseUrl,
|
|
148
|
+
...(cfg.token !== undefined ? { token: cfg.token } : {}),
|
|
149
|
+
...(cfg.principal !== undefined ? { principal: cfg.principal } : {}),
|
|
150
|
+
...(sid !== undefined ? { session: sid } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function postCompact(shot, retriesLeft = 1) {
|
|
111
154
|
// SDK runs.compact(0.0.52)——POST /v1/runs/:id/compact body {}(无 instructions 时与原 '{}' 逐字
|
|
112
155
|
// 同 wire);header 姿势 = Transport buildHeaders(Bearer 仅有 token 时 + x-agent-principal +
|
|
113
|
-
// content-type)
|
|
156
|
+
// content-type),与原手抄同三头。🔴 三头与 `?session=` 全取**冻结的那一份**,绝不现读。
|
|
114
157
|
const client = makeEngineWireClient({
|
|
115
|
-
baseUrl:
|
|
116
|
-
...(
|
|
117
|
-
principal:
|
|
158
|
+
baseUrl: shot.baseUrl,
|
|
159
|
+
...(shot.token ? { token: shot.token } : {}),
|
|
160
|
+
principal: shot.principal,
|
|
118
161
|
});
|
|
119
162
|
if (!client)
|
|
120
163
|
return;
|
|
164
|
+
const taskId = shot.taskId;
|
|
121
165
|
try {
|
|
122
|
-
const ack = await client.runs.compact(taskId,
|
|
166
|
+
const ack = await client.runs.compact(taskId, shot.session !== undefined ? { session: shot.session } : {});
|
|
123
167
|
hostLog('debug', `engineCompactWire: POST compact ${taskId} → ok${ack?.status ? ` (${ack.status})` : ''}`);
|
|
124
168
|
}
|
|
125
169
|
catch (e) {
|
|
@@ -128,7 +172,8 @@ async function postCompact(taskId, retriesLeft = 1) {
|
|
|
128
172
|
hostLog('debug', `engineCompactWire: POST compact ${taskId} → ${status}`);
|
|
129
173
|
// 404 = the early header bind outran the run-row persist — one deferred retry covers it.
|
|
130
174
|
if (status === 404 && retriesLeft > 0) {
|
|
131
|
-
|
|
175
|
+
// 重试用**同一份**冻结快照:重试窗内换了会话/换了引擎,旧 taskId 也绝不配新凭证。
|
|
176
|
+
deferFire(2000, () => void postCompact(shot, retriesLeft - 1), '404 单次重试', shot.sessionKey);
|
|
132
177
|
}
|
|
133
178
|
return;
|
|
134
179
|
}
|
|
@@ -140,21 +185,42 @@ async function postCompact(taskId, retriesLeft = 1) {
|
|
|
140
185
|
* Returns what it did (for the command's display line / tests). No-op ('offline') without a live
|
|
141
186
|
* wire; 'unsupported' when the engine's capabilities said manualCompact:false ([488] TOC-local) —
|
|
142
187
|
* the client-side projection compaction still runs, but the ENGINE session context does not shrink.
|
|
188
|
+
*
|
|
189
|
+
* 🔴 design/285 批 3(D7b):本族三处零参 `engineWireTarget()` 全部换成 `engineWireTargetFor(sessionKey)`。
|
|
190
|
+
* /compact 打的是**这个会话此刻在飞的那条 run**,不是某一行子代 —— owner 台账里没有它,所以槽键
|
|
191
|
+
* 只能由调用方给(多会话宿主每个 REPL 面板一个槽)。缺省 `DEFAULT_SESSION_KEY` ⇒ 与 0.44.0 逐字同。
|
|
192
|
+
* 📋 **带反证的驳回半场(异源对抗复审第三轮 [high] 的另一半,如实记)**:复审还要求把快照提前到
|
|
193
|
+
* **request(arm)那一拍**,理由是「arm 与 bind 之间槽可能被重绑,意图属于 A 却在 B 上执行」。
|
|
194
|
+
* **不采**,反证:arm 那一拍**根本没有 taskId** —— `/compact` 在 idle 下的语义是成文的
|
|
195
|
+
* 「**这个槽的下一条 run**」(见本文件头注的 arm-then-fire 段),槽的身份就是 key 本身。
|
|
196
|
+
* 槽真被重绑到另一台引擎时,「下一条 run」就在那台引擎上,压它才是意图;把 request 那一刻的
|
|
197
|
+
* target 冻下来反而会去压一台**已经不再跑这条会话**的引擎。⇒ 冻结的正确起点是**拿到 taskId 的
|
|
198
|
+
* 那一拍**(bind),那之后的每一次延迟与重试都必须用同一份 —— 那正是上面这一条。
|
|
199
|
+
* 现状由常驻钉 `B3-P31/R6i` 钉住(arm 之后换槽装配 ⇒ 发射打的是**换后**那台,且带换后的会话)。
|
|
200
|
+
* 🔴 **一条动词的槽键必须一以贯之**(异源对抗复审 [high] 采纳):目标、`?session=`、**run id**
|
|
201
|
+
* (`activeEngineRunIdFor`)、**能力位**(per-baseUrl 分桶)、**arm 状态**(per-key 分桶)——
|
|
202
|
+
* 五件全按同一个 key。只换其中几件就是**半 keyed 形**:那比换锚之前更坏(之前五件一致地错在同一个
|
|
203
|
+
* 槽上,一眼可见;半 keyed 是「A 的 run id 发到 B 的服务器」「B 的 arm 被 A 的 bind 吃掉」这类只在
|
|
204
|
+
* 多会话下现形的错)。这条正是 plan 决策链本批**豁免**的同一条理由 —— 那边整条链搬不动,这边搬得动。
|
|
143
205
|
*/
|
|
144
|
-
export function requestEngineCompact() {
|
|
145
|
-
|
|
206
|
+
export function requestEngineCompact(sessionKey = DEFAULT_SESSION_KEY) {
|
|
207
|
+
const cfg = engineWireTargetFor(sessionKey);
|
|
208
|
+
if (!cfg)
|
|
146
209
|
return 'offline';
|
|
147
|
-
probeManualCompactCapability(); // lazy, cached; first call may still ride the old path (fail-open)
|
|
148
|
-
if (
|
|
210
|
+
probeManualCompactCapability(sessionKey); // lazy, cached per baseUrl; first call may still ride the old path (fail-open)
|
|
211
|
+
if (manualCompactCapableByBase.get(cfg.baseUrl) === false) {
|
|
149
212
|
hostLog('debug', 'engineCompactWire: engine capabilities report manualCompact:false — verb suppressed (TOC-local runStore-less engine, [488])');
|
|
150
213
|
return 'unsupported';
|
|
151
214
|
}
|
|
152
|
-
|
|
215
|
+
// 🔴 run id 也按本槽取(异源对抗复审 [high]):写死默认槽 = 把 A 的 run id 发到 B 的服务器上。
|
|
216
|
+
const taskId = activeEngineRunIdFor(sessionKey);
|
|
153
217
|
if (taskId) {
|
|
154
|
-
|
|
218
|
+
const shot = freezeCompactShot(taskId, sessionKey);
|
|
219
|
+
if (shot !== null)
|
|
220
|
+
void postCompact(shot);
|
|
155
221
|
return 'fired';
|
|
156
222
|
}
|
|
157
|
-
|
|
223
|
+
pendingCompactKeys.add(sessionKey);
|
|
158
224
|
return 'armed';
|
|
159
225
|
}
|
|
160
226
|
/**
|
|
@@ -167,10 +233,25 @@ export function requestEngineCompact() {
|
|
|
167
233
|
* more for slow persists. Both shots are best-effort (postCompact never throws).
|
|
168
234
|
*/
|
|
169
235
|
const BOUND_FIRE_DELAY_MS = 1500;
|
|
170
|
-
export function onEngineTaskBound(taskId) {
|
|
171
|
-
probeManualCompactCapability(); // warm the capability cache off the first turn bind, before any /compact
|
|
172
|
-
|
|
236
|
+
export function onEngineTaskBound(taskId, sessionKey = DEFAULT_SESSION_KEY) {
|
|
237
|
+
probeManualCompactCapability(sessionKey); // warm the capability cache off the first turn bind, before any /compact
|
|
238
|
+
// 🔴 只消费**本槽**的 arm(异源对抗复审 [high]):进程级布尔会让先 bind 的那条会话把别人的 arm 吃掉 ——
|
|
239
|
+
// B 要的压缩发到 A 的 run 上,而 B 自己永远没压缩。
|
|
240
|
+
if (!pendingCompactKeys.has(sessionKey))
|
|
173
241
|
return;
|
|
174
|
-
|
|
175
|
-
|
|
242
|
+
// 🔴 **在 bind 这一拍冻结**(异源对抗复审 [high]):延迟 1.5s 之后再现读 target/session,就会把
|
|
243
|
+
// 这一拍的 taskId 配上那一拍的引擎与会话。
|
|
244
|
+
// 🔴 **冻不出快照就不消费这一记 arm**(异源对抗复审第三轮 [high] 的可采半场):那一刻没有 wire
|
|
245
|
+
// 时,旧形是「先把 arm 划掉、再发现发不出去」= 用户的 `/compact` 无声蒸发,而命令行早就回过
|
|
246
|
+
// 'armed'。改成**冻出来了才划掉**:这一拍发不出就留着,下一次 bind 再兑现(arm-then-fire 的
|
|
247
|
+
// 本意就是「下一条 run」,不是「下一条 run 且那一刻恰好有 wire」)。
|
|
248
|
+
const shot = freezeCompactShot(taskId, sessionKey);
|
|
249
|
+
if (shot === null)
|
|
250
|
+
return;
|
|
251
|
+
// 🔴 **登记上定时器才划掉 arm**(异源对抗复审第四轮 [medium]):端漏装 TimersPort 时 `deferFire`
|
|
252
|
+
// 只留一行日志就返回,先划掉 arm 就等于把用户的 `/compact` 无声吃掉(与上面那条同一个道理,
|
|
253
|
+
// 只是这一次挡的是「冻得出快照但排不上队」)。
|
|
254
|
+
if (deferFire(BOUND_FIRE_DELAY_MS, () => void postCompact(shot), 'bind 后发射', sessionKey)) {
|
|
255
|
+
pendingCompactKeys.delete(sessionKey);
|
|
256
|
+
}
|
|
176
257
|
}
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
import { stripSystemReminderBlocks } from '../systemReminderTag.js';
|
|
48
48
|
import { hostLog } from '../host.js';
|
|
49
49
|
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
50
|
-
import {
|
|
50
|
+
import { engineWireTargetFor } from '../engineWireTarget.js';
|
|
51
|
+
import { resolveOwnerContext } from './engineSubagentResume.js';
|
|
51
52
|
/** 只拉转录**尾部**这么多条(委派 prompt 在尾部方向;见文件头的 fork 形状订正)。 */
|
|
52
53
|
const TAIL_WINDOW = 200;
|
|
53
54
|
/** 单条正文的上限:详情页自己还会截到 300 字符,这里只防拉回一个巨型 blob。 */
|
|
@@ -174,11 +175,14 @@ let transcriptWindowReader = null;
|
|
|
174
175
|
export function __setTranscriptWindowReaderForTests(r) {
|
|
175
176
|
transcriptWindowReader = r;
|
|
176
177
|
}
|
|
177
|
-
async function readTranscriptWindow(transcriptId) {
|
|
178
|
+
async function readTranscriptWindow(transcriptId, sessionKey) {
|
|
178
179
|
if (transcriptWindowReader)
|
|
179
180
|
return transcriptWindowReader(transcriptId);
|
|
180
181
|
// 🔴 B6:六份手抄 `wireConfig()` 收成 `engineWireTarget()`(env 读经 hostEnv,本包零 process)。
|
|
181
|
-
|
|
182
|
+
// 🔴 design/285 批 3(D7b):零参形写死默认槽 —— 转录取件是**按行**的读面(锚 = 这一行登记的
|
|
183
|
+
// transcriptId),所以 baseUrl/token/principal 必须按**这一行所属的槽**取,否则 keyed 宿主上
|
|
184
|
+
// 会拿 A 槽的凭证去 `GET /v1/sessions/:id` 读 B 槽的转录(404,或更坏:读到同 id 的别人)。
|
|
185
|
+
const cfg = engineWireTargetFor(sessionKey);
|
|
182
186
|
if (!cfg)
|
|
183
187
|
return null;
|
|
184
188
|
const client = makeEngineWireClient({
|
|
@@ -213,7 +217,8 @@ export async function fetchDelegatedPrompt(taskId) {
|
|
|
213
217
|
promptHydrateInFlight.add(taskId);
|
|
214
218
|
let msgs = null;
|
|
215
219
|
try {
|
|
216
|
-
|
|
220
|
+
// 槽键取自 owner 台账的这一行(缺席 ⇒ DEFAULT_SESSION_KEY ⇒ 与 0.44.0 逐字同)。
|
|
221
|
+
msgs = await readTranscriptWindow(transcriptId, resolveOwnerContext(taskId).key);
|
|
217
222
|
}
|
|
218
223
|
catch (e) {
|
|
219
224
|
promptHydrateInFlight.delete(taskId);
|
|
@@ -4,8 +4,15 @@ export declare const engineOwnedRowIds: Set<string>;
|
|
|
4
4
|
export declare const fleetViewStubRowIds: Set<string>;
|
|
5
5
|
/** 这一行的真身在引擎里(本地 abortController 管不到它)。 */
|
|
6
6
|
export declare function isEngineViewRow(taskId: string): boolean;
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* 能力门同步读口(liveClient 构造期已 kick /v1/capabilities 进 engineCapsCache)。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 design/285 批 3(D7b):零参 `engineWireTarget()` 换成 `engineWireTargetFor(sessionKey)`。
|
|
11
|
+
* caps 缓存按 **baseUrl** 分桶,而 keyed 多会话宿主每槽一个 baseUrl —— 写死默认槽 = 用 A 引擎的
|
|
12
|
+
* 能力位去判 B 引擎能不能停行。本模块是**零依赖叶**(单实例纪律见头注),不许 import 台账,
|
|
13
|
+
* 所以槽键由调用方给;缺省 `DEFAULT_SESSION_KEY` ⇒ 与 0.44.0 逐字同。
|
|
14
|
+
*/
|
|
15
|
+
export declare function engineTaskHandlesCapable(sessionKey?: string): boolean;
|
|
9
16
|
/**
|
|
10
17
|
* 停这一行**必须先拿到引擎回执**吗?
|
|
11
18
|
*
|
|
@@ -15,4 +22,4 @@ export declare function engineTaskHandlesCapable(): boolean;
|
|
|
15
22
|
* false ⇒ 行跑在本进程(CC 逐字形态),`abortController.abort()` 就是执行本身,端写 `killed`
|
|
16
23
|
* 按构造恒真 ⇒ 同步翻,与 CC 2.1.220 `UPd`(pretty220.js:507299)一字不差。
|
|
17
24
|
*/
|
|
18
|
-
export declare function engineRowNeedsStopConfirm(taskId: string): boolean;
|
|
25
|
+
export declare function engineRowNeedsStopConfirm(taskId: string, sessionKey?: string): boolean;
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* 缺任一(老引擎 / 无 wire / 纯本地 AgentTool 行) → 回落旧行为(本地 abort 即真停,立即翻)。
|
|
28
28
|
*/
|
|
29
29
|
import { engineCapTrue } from '../engineCapsCache.js';
|
|
30
|
-
import {
|
|
30
|
+
import { engineWireTargetFor } from '../engineWireTarget.js';
|
|
31
|
+
import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
31
32
|
/** 引擎 bridge 从 `task_progress` tick / fleet 行帧建的行(module-level:footer 重挂不丢归属)。 */
|
|
32
33
|
export const engineOwnedRowIds = new Set();
|
|
33
34
|
/** fleet 树 enter 时为「面板无行的 fleet 行」stub 出来的纯查看载体行。 */
|
|
@@ -36,9 +37,16 @@ export const fleetViewStubRowIds = new Set();
|
|
|
36
37
|
export function isEngineViewRow(taskId) {
|
|
37
38
|
return engineOwnedRowIds.has(taskId) || fleetViewStubRowIds.has(taskId);
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* 能力门同步读口(liveClient 构造期已 kick /v1/capabilities 进 engineCapsCache)。
|
|
42
|
+
*
|
|
43
|
+
* 🔴 design/285 批 3(D7b):零参 `engineWireTarget()` 换成 `engineWireTargetFor(sessionKey)`。
|
|
44
|
+
* caps 缓存按 **baseUrl** 分桶,而 keyed 多会话宿主每槽一个 baseUrl —— 写死默认槽 = 用 A 引擎的
|
|
45
|
+
* 能力位去判 B 引擎能不能停行。本模块是**零依赖叶**(单实例纪律见头注),不许 import 台账,
|
|
46
|
+
* 所以槽键由调用方给;缺省 `DEFAULT_SESSION_KEY` ⇒ 与 0.44.0 逐字同。
|
|
47
|
+
*/
|
|
48
|
+
export function engineTaskHandlesCapable(sessionKey = DEFAULT_SESSION_KEY) {
|
|
49
|
+
return engineCapTrue(engineWireTargetFor(sessionKey)?.baseUrl, 'taskHandles');
|
|
42
50
|
}
|
|
43
51
|
/**
|
|
44
52
|
* 停这一行**必须先拿到引擎回执**吗?
|
|
@@ -49,6 +57,7 @@ export function engineTaskHandlesCapable() {
|
|
|
49
57
|
* false ⇒ 行跑在本进程(CC 逐字形态),`abortController.abort()` 就是执行本身,端写 `killed`
|
|
50
58
|
* 按构造恒真 ⇒ 同步翻,与 CC 2.1.220 `UPd`(pretty220.js:507299)一字不差。
|
|
51
59
|
*/
|
|
52
|
-
export function engineRowNeedsStopConfirm(taskId) {
|
|
53
|
-
|
|
60
|
+
export function engineRowNeedsStopConfirm(taskId, sessionKey) {
|
|
61
|
+
// `sessionKey` additive(design/285 批 3):keyed 宿主把这一行所属的槽传下来;缺省 = 默认槽。
|
|
62
|
+
return isEngineViewRow(taskId) && engineTaskHandlesCapable(sessionKey ?? DEFAULT_SESSION_KEY);
|
|
54
63
|
}
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
*/
|
|
32
32
|
import { hostLog } from '../host.js';
|
|
33
33
|
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
34
|
+
import { engineWireTargetFor } from '../engineWireTarget.js';
|
|
35
|
+
import { engineSessionParamFor } from '../engineSessionParam.js';
|
|
36
|
+
import { resolveOwnerContext } from './engineSubagentResume.js';
|
|
37
37
|
import { noteBgOwnerAbsence } from './subagentOwnerAbsence.js';
|
|
38
38
|
// ── caps 探测(true 固化 / false TTL / 失败不缓存)────────────────────────────────────────────
|
|
39
39
|
const capByBase = new Map();
|
|
@@ -62,14 +62,17 @@ export function __resetSubagentOutputCapCacheForTests() {
|
|
|
62
62
|
* 调用方回落 bg 事实渲染。绝不 throw。
|
|
63
63
|
*/
|
|
64
64
|
export async function fetchEngineSubagentReport(taskId, opts) {
|
|
65
|
-
|
|
65
|
+
// 🔴 design/285 批 3(D7b):台账记录**先取**(纯读)—— wire target 按**这一行的会话槽**取,
|
|
66
|
+
// 零参 `engineWireTarget()` 写死默认槽。`sessionKey` 缺席 ⇒ 回落默认槽 ⇒ 与 0.44.0 逐字同。
|
|
67
|
+
// ⚠️ 两道闸的**次序一字未动**(无 wire 仍先于「台账缺席」返回)。
|
|
68
|
+
const { record: owner, runId, key } = resolveOwnerContext(taskId);
|
|
69
|
+
const cfg = engineWireTargetFor(key);
|
|
66
70
|
if (!cfg)
|
|
67
71
|
return null;
|
|
68
72
|
// 🔴 #242 批 3([4000] Q3=B 裁定执行,行为翻面):台账缺席即**诚实缺席,绝不回落**
|
|
69
73
|
// `activeEngineRunId()` —— 与 resume 腿 `resolveOwnerRunId` 同口径(错值比缺席更坏:回落
|
|
70
74
|
// 到「此刻在飞的 run」对 session-bound 旧 run 是 404 撞墙,对同名子代是读错人)。缺席不
|
|
71
75
|
// 静默(noteBgOwnerAbsence warn 留痕);调用方按既有 null 回落 bg 事实渲染,fail-soft。
|
|
72
|
-
const runId = getBgParentRun(taskId);
|
|
73
76
|
if (!runId) {
|
|
74
77
|
noteBgOwnerAbsence('subagent-output', taskId);
|
|
75
78
|
return null;
|
|
@@ -86,7 +89,10 @@ export async function fetchEngineSubagentReport(taskId, opts) {
|
|
|
86
89
|
try {
|
|
87
90
|
// [1498]③:无条件带 session(1.244 忽略/1.245 必需,双版本兼容);缺席时(端未装 SessionPort /
|
|
88
91
|
// 端自己还没有值)不带——1.245 对 session-bound run 会 404,按既有 null 回落,下次重试自然带上。
|
|
89
|
-
|
|
92
|
+
// 🔴 design/285 批 3(D6,会话二态;与 steer/tail 同一口径):行登记的会话优先,缺则退**本槽**现势会话。
|
|
93
|
+
// 🔴 BEHAVIOR CHANGE(默认槽也变):tick 腿今天就在默认槽写 `sessionId` ⇒ 会话轮换后读旧行的终报,
|
|
94
|
+
// 此前拿**现势**会话打一条 session-bound 的旧 run = 确定性 404;现在用行登记的那一个。
|
|
95
|
+
const session = owner?.sessionId ?? engineSessionParamFor(key);
|
|
90
96
|
const r = await client.runs.subagentOutput(runId, taskId, {
|
|
91
97
|
...(opts?.signal ? { signal: opts.signal } : {}),
|
|
92
98
|
...(session ? { session } : {}),
|
|
@@ -48,10 +48,18 @@
|
|
|
48
48
|
* 呈前消毒 + 截长,只渲染。
|
|
49
49
|
*/
|
|
50
50
|
import type { SubagentSteerReceipt } from '@sema-agent/sdk';
|
|
51
|
+
import { type SubagentOwnerRecord } from '../subagentContentStore.js';
|
|
51
52
|
/** 能力位键(server `routes/capabilities.ts` 的 `subagentResume`;SDK `Capabilities.subagentResume`)。 */
|
|
52
53
|
export declare const SUBAGENT_RESUME_CAP = "subagentResume";
|
|
53
|
-
/**
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* 复活口在不在。未判/缺键 = false = 详情面不渲「继续」腿。
|
|
56
|
+
*
|
|
57
|
+
* 🔴 design/285 批 3(D7b):`baseUrl` 缺席时的派生从零参 `engineWireTarget()`(写死默认槽)换成
|
|
58
|
+
* `engineWireTargetFor(sessionKey)`。**本腿没有行上下文**(它答的是「这台引擎有没有这个口」,
|
|
59
|
+
* 不是「这一行该打到哪」)⇒ 槽键只能由调用方给,缺省 `DEFAULT_SESSION_KEY` = 与 0.44.0 逐字同。
|
|
60
|
+
* 显式 `baseUrl` 恒赢(既有注入缝不动;空串仍原样保留 —— `??` 只对 `undefined` 让位,与默认参形同)。
|
|
61
|
+
*/
|
|
62
|
+
export declare function subagentResumeAvailable(baseUrl?: string, sessionKey?: string): boolean;
|
|
55
63
|
/**
|
|
56
64
|
* 「继续」腿的 resume 取址(1.0.74 终扫 P2-11)。
|
|
57
65
|
*
|
|
@@ -138,6 +146,34 @@ export declare function classifySubagentResumeFailure(e: unknown): {
|
|
|
138
146
|
* @param activeRunId 注入缝(缺省 = `activeEngineRunId()`)—— 判据要能在没有 SessionPort 的纯层里验。
|
|
139
147
|
*/
|
|
140
148
|
export declare function resolveOwnerRunId(childTaskId?: string, activeRunId?: string | undefined): string | undefined;
|
|
149
|
+
/** {@link resolveOwnerContext} 的返回三件。 */
|
|
150
|
+
export interface SubagentOwnerContext {
|
|
151
|
+
/** 整条台账记录;`undefined` = **诚实不知道**,调用方走各自既有的缺席出路(绝不在这里回落)。 */
|
|
152
|
+
record: SubagentOwnerRecord | undefined;
|
|
153
|
+
/**
|
|
154
|
+
* `= record?.runId`,与既有 `getBgParentRun()` 逐字同。
|
|
155
|
+
* 🔴 **不含**「没指名行就回落到在飞 run」那一态 —— 那一态的判据住在 {@link resolveOwnerRunId},
|
|
156
|
+
* 本函数既不复制它,也**不读 SessionPort**(读了就会给 tail/output/taskHandle 三条
|
|
157
|
+
* 「台账缺席即诚实缺席」的腿凭空添一次 `activeEngineRunId()` 端口读)。
|
|
158
|
+
*/
|
|
159
|
+
runId: string | undefined;
|
|
160
|
+
/** 读侧要用的会话槽键 = `record.sessionKey ?? DEFAULT_SESSION_KEY`。 */
|
|
161
|
+
key: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 「这一行子代属于哪个会话槽」—— design/285 批 3(D6+D7b)读侧的**一次读**。
|
|
165
|
+
*
|
|
166
|
+
* 五个读点(tail / output / taskHandle 两口 / delegatedPrompt)此前各自读一次台账,而 wire target
|
|
167
|
+
* 一律走零参 `engineWireTarget()`(= 默认槽)。keyed 多会话宿主上那意味着「会话对了、打的服务器与
|
|
168
|
+
* 凭证可能还是别人的」(D7b)。本函数把「取哪一行的记录 + 它属于哪个槽」收成一处;
|
|
169
|
+
* `key` 缺席回落默认槽 ⇒ `engineWireTargetFor(DEFAULT_SESSION_KEY)` ≡ `engineWireTarget()`,
|
|
170
|
+
* 单会话宿主逐字同。
|
|
171
|
+
*
|
|
172
|
+
* 🔴 **会话策略刻意留在各腿**(不在这里合并):steer/tail/output 是二态(行的会话优先,缺则退现势),
|
|
173
|
+
* resume 是三态(指名了行却没有会话 ⇒ 干脆不带,AT-MOST-ONCE 不许猜)。收成一份就必然要加一个
|
|
174
|
+
* 「模式」参数,那等于把两条**有意不同**的口径伪装成同一个函数的两种配置。
|
|
175
|
+
*/
|
|
176
|
+
export declare function resolveOwnerContext(childTaskId?: string): SubagentOwnerContext;
|
|
141
177
|
/** 一次 resume 调用需要的三件:facade(null = 无 wire)、宿主 run(undefined = 诚实缺席)、会话参数。 */
|
|
142
178
|
export interface SubagentResumeContext {
|
|
143
179
|
facade: SubagentResumeFacade | null;
|
|
@@ -157,7 +193,15 @@ export interface SubagentResumeContext {
|
|
|
157
193
|
* 猜错会话可能落到**另一个会话的同名子代**上,那是不可回收的伤害;不带参数的失败只是 server
|
|
158
194
|
* fail-closed 的干净 404,已有 `not-found` 出路文案接。与本模块「错值比缺席更坏」同一条口径。
|
|
159
195
|
* · 调用方**完全没指名行**(无行上下文的通用调用)⇒ 才退当前会话(既有口径不变)。
|
|
160
|
-
* ⚠️
|
|
196
|
+
* ⚠️ 供给面欠账(**已由 design/285 批 2 补上**):fleet/通知两腿此前手里没有流会话;ingress 落地后
|
|
197
|
+
* 它们按 `{runId, sessionId, sessionKey}` 三值原子写,第二态自然变成第一态,判据一字未改 ——
|
|
198
|
+
* 这正是本注 0.30.x 时预告过的那一步。
|
|
199
|
+
*
|
|
200
|
+
* 🔴 design/285 批 3(D7b):wire target 从零参 `engineWireTarget()`(默认槽)换成
|
|
201
|
+
* `engineWireTargetFor(owner.sessionKey ?? DEFAULT_SESSION_KEY)`。会话对了而 baseUrl/token/
|
|
202
|
+
* principal 还是别人的,等于把 resume 这条 AT-MOST-ONCE 的动作发到另一台引擎上。
|
|
203
|
+
* 第三态(没指名行)的现势会话回落**同样按 key**(`engineSessionParamFor(key)`)—— 没指名行时
|
|
204
|
+
* key 恒是默认槽,与 0.44.0 逐字同。
|
|
161
205
|
*/
|
|
162
206
|
export declare function resolveSubagentResumeContext(childTaskId?: string): SubagentResumeContext;
|
|
163
207
|
/**
|
|
@@ -200,6 +244,12 @@ export interface RetainWithoutWakeInput {
|
|
|
200
244
|
}) => Promise<void>) | undefined;
|
|
201
245
|
} | null | undefined;
|
|
202
246
|
retainRequested?: boolean | undefined;
|
|
247
|
+
/**
|
|
248
|
+
* 🔴 design/285 批 3(D7b):`baseUrl` 缺席时按哪个会话槽派生。缺省 `DEFAULT_SESSION_KEY`
|
|
249
|
+
* ⇒ 与 0.44.0 逐字同(零参 `engineWireTarget()` 就是默认槽)。本告警是**per-baseUrl once**
|
|
250
|
+
* 的,keyed 宿主不点名槽就会让两个槽共用一条 once 记录(先响的那个把另一个吞掉)。
|
|
251
|
+
*/
|
|
252
|
+
sessionKey?: string | undefined;
|
|
203
253
|
}
|
|
204
254
|
/**
|
|
205
255
|
* 「引擎有复活口 + 本端在付留存成本 + 本端没有 wake 实现」⇒ 响亮一条(每 baseUrl 至多一次)。
|