@sema-agent/client-core 0.5.0 → 0.6.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/adapt.d.ts +15 -3
- package/dist/adapt.js +371 -19
- package/dist/clientSlice.d.ts +169 -0
- package/dist/clientSlice.js +62 -0
- package/dist/engineWireSdk.d.ts +25 -1
- package/dist/engineWireSdk.js +30 -4
- package/dist/host.d.ts +90 -0
- package/dist/host.js +84 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +26 -3
- package/dist/request/taskRequest.d.ts +135 -0
- package/dist/request/taskRequest.js +176 -0
- package/dist/scratchpadWireCaps.d.ts +16 -0
- package/dist/scratchpadWireCaps.js +65 -0
- package/dist/seam.d.ts +81 -9
- package/dist/seam.js +30 -0
- package/package.json +1 -1
package/dist/adapt.d.ts
CHANGED
|
@@ -38,6 +38,15 @@ export declare const CANCEL_MESSAGE = "The user doesn't want to take this action
|
|
|
38
38
|
/** CJK 加权 token 估算(cli estimateCjkTokens 逐字:CJK≈0.6,其余≈0.25 tok/char)。 */
|
|
39
39
|
export declare function estimateCjkTokens(s: string): number;
|
|
40
40
|
/** 引擎 subagent 卡的短标签(cli shortTaskLabel 逐字)。 */
|
|
41
|
+
/**
|
|
42
|
+
* T8 —— 压平 §E1 wire `output`(**非均匀**:`string` | `(TextContent|ImageContent)[]`,events.d.ts:78)
|
|
43
|
+
* 成一条纯文本(cli `flattenWireOutput` 逐字)。text 块贡献 `.text`,image 块贡献中性占位 `[image]`
|
|
44
|
+
* (转录卡是文本粒度),裸字符串直通。
|
|
45
|
+
* 🔴 载荷 UNTRUSTED + 仅供观测(events.d.ts:80-85):渲染可以,**绝不回喂模型**;`truncated` 的更不行。
|
|
46
|
+
* B4 把它从 B 层(`wireOutputToBody`)提到独立导出:A 层的 `settlePanelTasks(report)` 也要用它
|
|
47
|
+
* (关卡时把子代最终报告串到面板行 end 事件上),两处必须同一份实现。
|
|
48
|
+
*/
|
|
49
|
+
export declare function flattenWireOutput(output: unknown): string;
|
|
41
50
|
export declare function shortTaskLabel(task: string): string;
|
|
42
51
|
/**
|
|
43
52
|
* tool_use 块的渲染面消毒(cli assistant 臂逐条搬运,**纯函数**,不改入参):
|
|
@@ -53,6 +62,7 @@ export declare function sanitizeToolUseBlock(block: Record<string, unknown>): {
|
|
|
53
62
|
rawInput: unknown;
|
|
54
63
|
eventId?: string;
|
|
55
64
|
parentToolCallId?: string;
|
|
65
|
+
label?: string;
|
|
56
66
|
};
|
|
57
67
|
/** 台账序列化形(#63 回植:重启恢复 = 台账加载 + 纯重放)。 */
|
|
58
68
|
export interface AdapterLedgerState {
|
|
@@ -67,16 +77,18 @@ export interface AdapterLedgerState {
|
|
|
67
77
|
/** 本批覆盖清单——差分守卫按它区分「已覆盖臂必须逐字段等价」与「已声明未覆盖臂」。 */
|
|
68
78
|
export declare const ADAPTER_COVERAGE: {
|
|
69
79
|
/** 已落码的帧臂。 */
|
|
70
|
-
readonly frames: readonly ["assistant", "user", "stream_event", "turn_usage", "result", "system", "task_notification", "workflow_complete", "diagnostics", "steering_injected", "workspace_changed", "retry_status", "tool_end_result(
|
|
80
|
+
readonly frames: readonly ["assistant", "user", "stream_event", "turn_usage", "result", "system", "task_notification", "workflow_complete", "diagnostics", "steering_injected", "workspace_changed", "prompt_suggestions", "retry_status", "task_progress", "tool_end_result(label 补位 + 关卡 settle + response-id 复位 + 开卡台账出栈;不铸 tool_result)"];
|
|
71
81
|
/** 已落码臂产出的 transcript 消息类目(差分守卫的比对域)。 */
|
|
72
82
|
readonly transcriptKinds: readonly ["assistant_text", "assistant_thinking", "assistant_tool_use", "user_tool_result_decision", "user_task_notification", "system_passthrough"];
|
|
73
83
|
/** B3(0.5.0)新落码的臂/半场 —— 从 todo 移过来的,别再在 todo 里留同名条目。 */
|
|
74
84
|
readonly b3: readonly ["T38 三清第三清 + T57 第五处断闸:prompt_suggestions(turn 开场作废 + 子流断闸)", "T40 IDLE-FLUSH 1.5s 竞速(需 ctx.setTimer;缺席=不启用)", "T58 手动 iterator 的 finally cleanup(early break/throw 都要关上游生成器)", "T59 abort 半场(ctx.signal;中断后不产终答)", "T60 无 result 收尾腿(泄尾增量 → 清 liveness → 尾思考 → 尾文本段)"];
|
|
85
|
+
/** B4(0.6.0)新落码的臂/半场 —— A 层收官批,同样别再在 todo 里留同名条目。 */
|
|
86
|
+
readonly b4: readonly ["A 层第 14 臂 task_progress:workflow lane 三级门 + 判据④ belt 回植 + C1 alias + bindTaskCard(显式父/FIFO/含糊即 unbound)+ inline stats + T50 双形读 + SubagentStart + #6 resident 台账 + 面板 tick", "settlePanelTasks 三处 sweep 全接线(tool_end 显式关卡带报告 / result 防御 sweep / turn 末 + abort sweep)", "P0-2 tool label 侧信道(sanitizeToolUseBlock 摘 label + engineToolLabelStore 登记;0.5.0 真缺口,label 会随 tool_use block 落转录 → /compact 直发 provider 400)", "task_notification 的两件宿主义务收回库内自持:dropQueuedNotificationsForRun + clearEnginePanelTaskResident;stop hook 的 if-started 门由库解", "T8 flattenWireOutput 提为独立导出(A 层 settle 报告与 B 层卡体同一份)"];
|
|
75
87
|
/** 如实留白——写不动/依赖未搬运资产的臂,#52b 起分批接。 */
|
|
76
|
-
readonly todo: readonly ["tool_end_result → user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)", "tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured → 面板台账全量同步", "result/turn 末的开卡兜底关闭(同上,依赖合成器)
|
|
88
|
+
readonly todo: readonly ["tool_end_result → user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)= B5", "tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured → 面板台账全量同步 = B5", "result/turn 末的开卡兜底关闭(同上,依赖合成器)= B5", "#117a bg Bash 回执探测(detectEngineBgShellReceipt 文案锚定探测器)= B5(E 层按工具名分臂)", "abort 半场里依赖开卡合成器的那件(兜底关卡)= B5;行 sweep 半场 B4 已接", "macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)"];
|
|
77
89
|
};
|
|
78
90
|
/** 本批**已知的、有意的**行为分歧(差分守卫显式登记,绝不当等价) */
|
|
79
|
-
export declare const ADAPTER_DIVERGENCES: readonly [string, string, string, string];
|
|
91
|
+
export declare const ADAPTER_DIVERGENCES: readonly [string, string, string, string, string, string];
|
|
80
92
|
/** 有台账序列化位的适配器(seam 的 WireToCcAdapter 只定了 exportLedger——见 SEAM-GAP-1)。 */
|
|
81
93
|
export interface WireToCcAdapterWithLedger extends WireToCcAdapter {
|
|
82
94
|
exportLedger(): AdapterLedgerState & Record<string, unknown>;
|
package/dist/adapt.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { deriveTranscriptId } from './seam.js';
|
|
2
|
-
import { normalizeTaskNotification, renderTaskNotificationXml, taskNotificationDedupKey } from './notifications.js';
|
|
2
|
+
import { dropQueuedNotificationsForRun, normalizeTaskNotification, renderTaskNotificationXml, taskNotificationDedupKey, } from './notifications.js';
|
|
3
3
|
import { mapBrainStatusToRetry } from './retryStatus.js';
|
|
4
4
|
import { steeringInjectedToAttachments } from './steering.js';
|
|
5
5
|
import { projectDiagnosticsFrame } from './diagnostics.js';
|
|
6
|
+
// ── B4:A 层搬入后适配器**自持**的四个本包台账(写者与读者同包 = R4 单实例纪律;
|
|
7
|
+
// 这四件都没有 UI 订阅面,做成 chrome 臂只会凭空长出一条「宿主不实现就静默失效」的义务)。
|
|
8
|
+
import { recordEngineToolLabel } from './engineToolLabelStore.js';
|
|
9
|
+
import { registerSubagentAlias } from './subagentContentStore.js';
|
|
10
|
+
import { isWorkflowAgentTaskId, recordWorkflowAgentTaskId } from './workflow.js';
|
|
11
|
+
import { clearEnginePanelTaskResident, markEnginePanelTaskResident } from './engineAgentPanelStore.js';
|
|
6
12
|
/** 主车道证明——本批实现的臂全在 leader lane(子代/workflow lane 的 tick 面留 #52b)。 */
|
|
7
13
|
const MAIN = { lane: 'main' };
|
|
8
14
|
/**
|
|
@@ -62,6 +68,32 @@ export function estimateCjkTokens(s) {
|
|
|
62
68
|
return Math.round(t);
|
|
63
69
|
}
|
|
64
70
|
/** 引擎 subagent 卡的短标签(cli shortTaskLabel 逐字)。 */
|
|
71
|
+
/**
|
|
72
|
+
* T8 —— 压平 §E1 wire `output`(**非均匀**:`string` | `(TextContent|ImageContent)[]`,events.d.ts:78)
|
|
73
|
+
* 成一条纯文本(cli `flattenWireOutput` 逐字)。text 块贡献 `.text`,image 块贡献中性占位 `[image]`
|
|
74
|
+
* (转录卡是文本粒度),裸字符串直通。
|
|
75
|
+
* 🔴 载荷 UNTRUSTED + 仅供观测(events.d.ts:80-85):渲染可以,**绝不回喂模型**;`truncated` 的更不行。
|
|
76
|
+
* B4 把它从 B 层(`wireOutputToBody`)提到独立导出:A 层的 `settlePanelTasks(report)` 也要用它
|
|
77
|
+
* (关卡时把子代最终报告串到面板行 end 事件上),两处必须同一份实现。
|
|
78
|
+
*/
|
|
79
|
+
export function flattenWireOutput(output) {
|
|
80
|
+
if (typeof output === 'string')
|
|
81
|
+
return output;
|
|
82
|
+
if (Array.isArray(output)) {
|
|
83
|
+
const parts = [];
|
|
84
|
+
for (const block of output) {
|
|
85
|
+
if (block && typeof block === 'object') {
|
|
86
|
+
const b = block;
|
|
87
|
+
if (b.type === 'text' && typeof b.text === 'string')
|
|
88
|
+
parts.push(b.text);
|
|
89
|
+
else if (b.type === 'image')
|
|
90
|
+
parts.push('[image]');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return parts.join('');
|
|
94
|
+
}
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
65
97
|
export function shortTaskLabel(task) {
|
|
66
98
|
const firstLine = (task.split('\n').find(l => l.trim()) ?? task).trim();
|
|
67
99
|
const words = firstLine.split(/\s+/).slice(0, 6).join(' ');
|
|
@@ -82,6 +114,13 @@ export function sanitizeToolUseBlock(block) {
|
|
|
82
114
|
const rawInput = out.input;
|
|
83
115
|
const eventId = typeof out.eventId === 'string' ? out.eventId : undefined;
|
|
84
116
|
const parentToolCallId = typeof out.parentToolCallId === 'string' ? out.parentToolCallId : undefined;
|
|
117
|
+
// 🔴 B4 补漏(0.5.0 真缺口,差分守卫此前无 fixture 携 label ⇒ 漏得静默):core 的**展示名**
|
|
118
|
+
// `tool_start.label` 同样是侧信道,必须在渲染前摘掉 —— tool_use block 会进壳 transcript,
|
|
119
|
+
// /compact 时原样直发 provider,多一个键 = 400 invalid_request(cli 头注记的「件1 事故同款」)。
|
|
120
|
+
// 摘下来的值由调用方交给 engineToolLabelStore(纯函数不做副作用),unknown-tool 卡按
|
|
121
|
+
// toolCallId 查。今天唯一的真命中族 = MCP 远端工具(name=mcp__srv__tool / label=srv:tool)。
|
|
122
|
+
const label = typeof out.label === 'string' ? out.label : undefined;
|
|
123
|
+
delete out.label;
|
|
85
124
|
delete out.eventId;
|
|
86
125
|
delete out.parentToolCallId;
|
|
87
126
|
if (out.input && typeof out.input === 'object') {
|
|
@@ -114,7 +153,13 @@ export function sanitizeToolUseBlock(block) {
|
|
|
114
153
|
delete inp.subagent_type;
|
|
115
154
|
}
|
|
116
155
|
}
|
|
117
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
block: out,
|
|
158
|
+
rawInput,
|
|
159
|
+
...(eventId !== undefined ? { eventId } : {}),
|
|
160
|
+
...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
|
|
161
|
+
...(label !== undefined ? { label } : {}),
|
|
162
|
+
};
|
|
118
163
|
}
|
|
119
164
|
/** seam 的 reserved decision 哨兵(cli decisionOf 逐字)。 */
|
|
120
165
|
function decisionOf(block) {
|
|
@@ -137,8 +182,10 @@ export const ADAPTER_COVERAGE = {
|
|
|
137
182
|
'diagnostics',
|
|
138
183
|
'steering_injected',
|
|
139
184
|
'workspace_changed',
|
|
185
|
+
'prompt_suggestions',
|
|
140
186
|
'retry_status',
|
|
141
|
-
'
|
|
187
|
+
'task_progress',
|
|
188
|
+
'tool_end_result(label 补位 + 关卡 settle + response-id 复位 + 开卡台账出栈;不铸 tool_result)',
|
|
142
189
|
],
|
|
143
190
|
/** 已落码臂产出的 transcript 消息类目(差分守卫的比对域)。 */
|
|
144
191
|
transcriptKinds: [
|
|
@@ -157,15 +204,21 @@ export const ADAPTER_COVERAGE = {
|
|
|
157
204
|
'T59 abort 半场(ctx.signal;中断后不产终答)',
|
|
158
205
|
'T60 无 result 收尾腿(泄尾增量 → 清 liveness → 尾思考 → 尾文本段)',
|
|
159
206
|
],
|
|
207
|
+
/** B4(0.6.0)新落码的臂/半场 —— A 层收官批,同样别再在 todo 里留同名条目。 */
|
|
208
|
+
b4: [
|
|
209
|
+
'A 层第 14 臂 task_progress:workflow lane 三级门 + 判据④ belt 回植 + C1 alias + bindTaskCard(显式父/FIFO/含糊即 unbound)+ inline stats + T50 双形读 + SubagentStart + #6 resident 台账 + 面板 tick',
|
|
210
|
+
'settlePanelTasks 三处 sweep 全接线(tool_end 显式关卡带报告 / result 防御 sweep / turn 末 + abort sweep)',
|
|
211
|
+
'P0-2 tool label 侧信道(sanitizeToolUseBlock 摘 label + engineToolLabelStore 登记;0.5.0 真缺口,label 会随 tool_use block 落转录 → /compact 直发 provider 400)',
|
|
212
|
+
'task_notification 的两件宿主义务收回库内自持:dropQueuedNotificationsForRun + clearEnginePanelTaskResident;stop hook 的 if-started 门由库解',
|
|
213
|
+
'T8 flattenWireOutput 提为独立导出(A 层 settle 报告与 B 层卡体同一份)',
|
|
214
|
+
],
|
|
160
215
|
/** 如实留白——写不动/依赖未搬运资产的臂,#52b 起分批接。 */
|
|
161
216
|
todo: [
|
|
162
|
-
'tool_end_result → user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)',
|
|
163
|
-
'tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured → 面板台账全量同步',
|
|
164
|
-
'result/turn 末的开卡兜底关闭(同上,依赖合成器)',
|
|
165
|
-
'
|
|
166
|
-
'
|
|
167
|
-
'#117a bg Bash 回执探测(detectEngineBgShellReceipt 文案锚定探测器)',
|
|
168
|
-
'abort 半场里依赖行台账的两件:settlePanelTasks 兜底 sweep + 开卡合成器兜底关卡(见上面两条)',
|
|
217
|
+
'tool_end_result → user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)= B5',
|
|
218
|
+
'tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured → 面板台账全量同步 = B5',
|
|
219
|
+
'result/turn 末的开卡兜底关闭(同上,依赖合成器)= B5',
|
|
220
|
+
'#117a bg Bash 回执探测(detectEngineBgShellReceipt 文案锚定探测器)= B5(E 层按工具名分臂)',
|
|
221
|
+
'abort 半场里依赖开卡合成器的那件(兜底关卡)= B5;行 sweep 半场 B4 已接',
|
|
169
222
|
'macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)',
|
|
170
223
|
],
|
|
171
224
|
};
|
|
@@ -182,6 +235,13 @@ export const ADAPTER_DIVERGENCES = [
|
|
|
182
235
|
'文件探针写 `${Date.now()} text_flush len=N`,adapt 是 `ctx.probe(\'stream\', …)` 注入口写 ' +
|
|
183
236
|
'`${ctx.now()} text_flush len=N`。行格式与触发点(每次文本 flush 一次)逐字相同,' +
|
|
184
237
|
'差在时钟源与落地方式(宿主决定落文件/上报/丢弃)。',
|
|
238
|
+
'DIVERGENCE-5(B4):A 层行台账的三个探针同 DIVERGENCE-4 处理 —— cli 的 `SEMA_BIND_PROBE` / ' +
|
|
239
|
+
'`SEMA_TICK_PROBE` / `SEMA_HOOK_PROBE` 三个文件探针,在本包是 `ctx.probe(\'bind\'|\'tick\'|\'hook\', line)`。' +
|
|
240
|
+
'行载荷逐字同形(tick 是同键序 JSON),只把 `Date.now()` 换成 `ctx.now()`。',
|
|
241
|
+
'DIVERGENCE-6(B4):`subagent_lifecycle` 的**去重与 if-started 门在库里解**,cli 在 module 级 Set 上解。' +
|
|
242
|
+
'可观测后果相同(同一 taskId 的 Start 恒一次;没 Start 过的 taskId 收不到通知臂的 Stop),' +
|
|
243
|
+
'差在「谁持台账」:cli=进程级、本包=适配器实例级(= 一个 session)。多 session 宿主(web 一页两会话)' +
|
|
244
|
+
'上本包语义**更正确**——cli 的 module 形会让两个会话互吞对方的 Start。',
|
|
185
245
|
];
|
|
186
246
|
const TASK_NOTIFICATION_TAG = 'task-notification';
|
|
187
247
|
const TASK_ID_TAG = 'task-id';
|
|
@@ -196,6 +256,14 @@ class WireToCcAdapterImpl {
|
|
|
196
256
|
cardEnqueuedRuns = new Set();
|
|
197
257
|
/** W1 三级判据素材:本壳见过的 Workflow 工具 tool_use id(module 级台账的实例化)。 */
|
|
198
258
|
seenWorkflowToolCallIds = new Set();
|
|
259
|
+
/**
|
|
260
|
+
* B4 —— SubagentStart 一生只许一次(frame-lane-matrix #4)。cli 是 **module 级** Set:跨 turn 存活的
|
|
261
|
+
* bg 子代在每个新 generator 的首 tick 都会走到 fire 点(`livePanelTasks` 是 per-turn 的),没有它
|
|
262
|
+
* 就每 turn 重 fire 一次。本包放**实例级** = 一个 session 一份,语义等价且不污染进程(多 session
|
|
263
|
+
* 宿主如 web 一个页面开两个会话,cli 的 module 形反而会互相吞)。
|
|
264
|
+
* 同时它也是 `task_notification` 臂发 stop 的门(cli:`firedSubagentStartHookTaskIds.has(taskId)`)。
|
|
265
|
+
*/
|
|
266
|
+
firedSubagentStartHookTaskIds = new Set();
|
|
199
267
|
exportLedger() {
|
|
200
268
|
return {
|
|
201
269
|
version: 1,
|
|
@@ -280,6 +348,10 @@ class WireToCcAdapterImpl {
|
|
|
280
348
|
this.cardEnqueuedRuns.add(runId);
|
|
281
349
|
}
|
|
282
350
|
async *adapt(frames, ctx) {
|
|
351
|
+
// 下面几个 `function*` 表达式(非箭头 —— generator 不能写成箭头)要访问实例台账,
|
|
352
|
+
// 它们的 `this` 不是适配器,所以在这里固定一个引用。
|
|
353
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
354
|
+
const self = this;
|
|
283
355
|
const turnStartAt = ctx.now();
|
|
284
356
|
// ── id 派生 ────────────────────────────────────────────────────────────────────────────────
|
|
285
357
|
// [1653]:稳定键优先序 = 帧 id > seq > toolCallId,全缺才 ctx.uuid()。现役 wire 的稳定键落在
|
|
@@ -318,12 +390,140 @@ class WireToCcAdapterImpl {
|
|
|
318
390
|
let segmentAnchor = null;
|
|
319
391
|
let thinkingAnchor = null;
|
|
320
392
|
const pending = new Map();
|
|
393
|
+
// ── B4 / MF-10:面板行台账(cli generator 局部变量逐个对位)────────────────────────────────
|
|
394
|
+
// 引擎侧子代的 tick 只带 rollup 数字({toolUses,totalTokens}) = CC `AgentProgress` 同形,
|
|
395
|
+
// 所以忠实的 CC-187 面是 footer 的 CoordinatorTaskPanel(CC 自己对进程外 agent 的先例:
|
|
396
|
+
// 面板行 + task-notification,从不合成 inline 消息轨)。下面这组把「tick → 哪张委派卡」的
|
|
397
|
+
// 归属、行的活性(live/inert/unbound)、以及 settle 的三处 sweep 全部管起来。
|
|
398
|
+
/** taskId → 绑定的 open tool_use id。 */
|
|
399
|
+
const taskCardBinding = new Map();
|
|
400
|
+
/** 已被某 taskId 认领的 tool_use id(一张卡只认一个子代)。 */
|
|
401
|
+
const boundToolCalls = new Set();
|
|
402
|
+
/** 本 turn 发布过面板行的全部 taskId(绑定与否都算)。 */
|
|
403
|
+
const livePanelTasks = new Set();
|
|
404
|
+
/** 已落终态的行(settle 幂等)。 */
|
|
405
|
+
const endedPanelTasks = new Set();
|
|
406
|
+
/**
|
|
407
|
+
* #6 —— 绑定时那张卡**正开着**的 taskId(= "live" 绑定)。只有这些行属本 turn 生命周期、
|
|
408
|
+
* 可被 turn 末防御 sweep 结掉。inert(显式父指向已关/没见过的卡 —— 跨 turn bg 子代的典型形)
|
|
409
|
+
* 与 unbound 行是 **session 常驻**:终态只认 task_notification / bg_notification。
|
|
410
|
+
*/
|
|
411
|
+
const liveBoundPanelTasks = new Set();
|
|
412
|
+
/** taskId → 子代类型(Stop hook 查它;cli 同为 turn 局部)。 */
|
|
413
|
+
const subagentTypeById = new Map();
|
|
321
414
|
const chrome = (event) => ({ plane: 'chrome', event });
|
|
322
415
|
const transcript = (message) => ({
|
|
323
416
|
plane: 'transcript',
|
|
324
417
|
message: message,
|
|
325
418
|
});
|
|
326
419
|
const mintResponseId = (frame) => (assistantResponseId ??= `msg_sema_${idOf(frame, 'resp')}`);
|
|
420
|
+
// ── B4 / MF-10:行绑定 + 三处 sweep(cli bindTaskCard / unboundSubagentCandidates /
|
|
421
|
+
// settlePanelTasks 逐条对位)───────────────────────────────────────────────────────────
|
|
422
|
+
/** 行的车道证明:绑到卡的 = subagent lane(带父卡 id),未绑的 = 主 lane(taskId 键的独立行)。 */
|
|
423
|
+
const laneOfTask = (taskId) => {
|
|
424
|
+
const card = taskCardBinding.get(taskId);
|
|
425
|
+
return card !== undefined ? { lane: 'subagent', parentToolCallId: card } : MAIN;
|
|
426
|
+
};
|
|
427
|
+
const bindTaskCard = (taskId, parentToolCallId) => {
|
|
428
|
+
const existing = taskCardBinding.get(taskId);
|
|
429
|
+
if (existing !== undefined)
|
|
430
|
+
return existing;
|
|
431
|
+
let target;
|
|
432
|
+
if (parentToolCallId !== undefined) {
|
|
433
|
+
// P1② 精确绑定 —— wire 点名了委派卡。信但不无条件信([1617]①,frame-lane-matrix #1):
|
|
434
|
+
// 被点名的卡**开着**且**不是** subagent 形工具(Task/Agent/Fork)时拒绝绑定 —— 否则
|
|
435
|
+
// workflow/Bash 卡一关就会把这条行 settle 掉、还会收走它的 inline stats(错归因)。
|
|
436
|
+
// 行保持 unbound(taskId 键的 rollup 仍是真的)。已关/没见过的 id 仍**惰性绑定**(inert):
|
|
437
|
+
// 终态由通知落,严格优于错绑另一张开着的卡。
|
|
438
|
+
const named = pending.get(parentToolCallId);
|
|
439
|
+
if (named !== undefined && !SUBAGENT_TOOL_NAMES.has(named.name))
|
|
440
|
+
return undefined;
|
|
441
|
+
target = parentToolCallId;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
for (const [id, p] of pending) {
|
|
445
|
+
if (!boundToolCalls.has(id) && SUBAGENT_TOOL_NAMES.has(p.name)) {
|
|
446
|
+
target = id;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (target === undefined)
|
|
452
|
+
return undefined;
|
|
453
|
+
taskCardBinding.set(taskId, target);
|
|
454
|
+
boundToolCalls.add(target);
|
|
455
|
+
// #6 live vs inert:只有绑到**此刻开着**的卡才把行系进本 turn 的生命周期
|
|
456
|
+
// (FIFO 目标恒开着;显式目标可能已关 = inert)。
|
|
457
|
+
if (pending.has(target))
|
|
458
|
+
liveBoundPanelTasks.add(taskId);
|
|
459
|
+
// cli 的 `SEMA_BIND_PROBE` 文件探针 → 注入口(载荷逐字同形,见 ADAPTER_DIVERGENCES-5)。
|
|
460
|
+
ctx.probe?.('bind', `bind ${taskId} -> ${target} via=${parentToolCallId !== undefined ? 'explicit' : 'fifo'}`);
|
|
461
|
+
return target;
|
|
462
|
+
};
|
|
463
|
+
/** 对抗复审修(w0zwpa251 #1):≥2 张未绑的 subagent 形卡开着且 wire 没点名 ⇒ 任何猜都是错归因。 */
|
|
464
|
+
const unboundSubagentCandidates = () => {
|
|
465
|
+
let n = 0;
|
|
466
|
+
for (const [id, p] of pending) {
|
|
467
|
+
if (!boundToolCalls.has(id) && SUBAGENT_TOOL_NAMES.has(p.name))
|
|
468
|
+
n++;
|
|
469
|
+
}
|
|
470
|
+
return n;
|
|
471
|
+
};
|
|
472
|
+
/** SubagentStart(cli fireSubagentStartHook 的臂化):类型恒记,fire 一生一次。 */
|
|
473
|
+
const fireSubagentStart = function* (taskId, agentType) {
|
|
474
|
+
subagentTypeById.set(taskId, agentType); // Stop 的类型查找恒记(fire 与否无关)
|
|
475
|
+
if (self.firedSubagentStartHookTaskIds.has(taskId))
|
|
476
|
+
return;
|
|
477
|
+
self.firedSubagentStartHookTaskIds.add(taskId);
|
|
478
|
+
ctx.probe?.('hook', `start ${taskId} type=${agentType}`);
|
|
479
|
+
yield chrome({ kind: 'subagent_lifecycle', laneProof: laneOfTask(taskId), phase: 'start', taskId, agentType });
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* 结掉一张关闭中的卡所绑的面板行(`cardId`),或 turn 末/abort 结掉**本 turn 活绑**的全部行
|
|
483
|
+
* (`cardId===null` 的防御 sweep —— 子代从不发终态 tick)。
|
|
484
|
+
* 拍1 查看态:显式关卡臂携委派卡的 output(= 子代最终报告)到 end 事件上;sweep 无报告。
|
|
485
|
+
*/
|
|
486
|
+
const settlePanelTasks = function* (cardId, isError, report) {
|
|
487
|
+
// inline 群组行孪生(clay dogfood 2026-07-03):冻结该卡的 tick 终值,免得"N agents finished"
|
|
488
|
+
// 行把真实工具数/token 重置回 0。null-cardId 的 sweep 结掉每一条活着的 inline 条目。
|
|
489
|
+
if (cardId !== null) {
|
|
490
|
+
yield chrome({ kind: 'inline_task_stats', laneProof: { lane: 'subagent', parentToolCallId: cardId }, op: 'settle', cardId, isError });
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
yield chrome({ kind: 'inline_task_stats', laneProof: MAIN, op: 'settle_all' });
|
|
494
|
+
}
|
|
495
|
+
for (const taskId of livePanelTasks) {
|
|
496
|
+
if (cardId !== null && taskCardBinding.get(taskId) !== cardId)
|
|
497
|
+
continue;
|
|
498
|
+
// #6 —— 防御 sweep 只结**本 turn 活绑**的行(流被截断的形)。session 常驻行
|
|
499
|
+
// (inert 绑定 / unbound = 跨 turn bg 子代)绝不在这里冻成假 completed:它的终态真相
|
|
500
|
+
// 由 task_notification / bg_notification 落(通知-settle 边)。
|
|
501
|
+
if (cardId === null && !liveBoundPanelTasks.has(taskId))
|
|
502
|
+
continue;
|
|
503
|
+
if (endedPanelTasks.has(taskId))
|
|
504
|
+
continue;
|
|
505
|
+
endedPanelTasks.add(taskId);
|
|
506
|
+
const lane = laneOfTask(taskId);
|
|
507
|
+
const agentType = subagentTypeById.get(taskId);
|
|
508
|
+
yield chrome({
|
|
509
|
+
kind: 'subagent_lifecycle',
|
|
510
|
+
laneProof: lane,
|
|
511
|
+
phase: 'stop',
|
|
512
|
+
taskId,
|
|
513
|
+
...(agentType !== undefined ? { agentType } : {}),
|
|
514
|
+
});
|
|
515
|
+
yield chrome({
|
|
516
|
+
kind: 'panel_task',
|
|
517
|
+
laneProof: lane,
|
|
518
|
+
event: {
|
|
519
|
+
kind: 'end',
|
|
520
|
+
taskId,
|
|
521
|
+
isError,
|
|
522
|
+
...(cardId !== null && report !== undefined ? { report } : {}),
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
};
|
|
327
527
|
/** 提交累积的思考块(cli takeThinking:committed 形 + 关活体块 + 收活动行)。 */
|
|
328
528
|
function* takeThinking() {
|
|
329
529
|
if (thinking.length === 0)
|
|
@@ -481,6 +681,9 @@ class WireToCcAdapterImpl {
|
|
|
481
681
|
const renderBlocks = blocks.map((b, i) => (i === toolUseIdx ? san.block : b));
|
|
482
682
|
const toolUseId = String(san.block.id);
|
|
483
683
|
const toolName = typeof san.block.name === 'string' ? san.block.name : 'unknown';
|
|
684
|
+
// P0-2 侧信道:core 展示名进本包台账(渲染块上已被 sanitize 摘掉)。cli 同位、同顺序
|
|
685
|
+
// (在任何 yield 之前),对 reject/cancel/error 三条早退路径同样生效。
|
|
686
|
+
recordEngineToolLabel(toolUseId, san.label);
|
|
484
687
|
// 引擎模式下壳工具 call() 从不执行 —— ctrl+t 面板台账只能靠这条侧信道(宿主义务)。
|
|
485
688
|
if (TASK_TOOL_NAMES.has(toolName)) {
|
|
486
689
|
yield chrome({
|
|
@@ -588,15 +791,139 @@ class WireToCcAdapterImpl {
|
|
|
588
791
|
break;
|
|
589
792
|
}
|
|
590
793
|
case 'tool_end_result': {
|
|
591
|
-
//
|
|
592
|
-
//
|
|
794
|
+
// B4 起本臂做三件:① P0-2 label 补位(只有 tool_end 带 label 的场景;台账自身幂等不覆盖)
|
|
795
|
+
// ② MF-10 **关卡 settle**(子代从不发终态 tick,父卡的 tool_end 就是完成信号;卡的 output
|
|
796
|
+
// 文本 = 子代最终报告,串到行 end 事件上)③ 卡台账出栈 + response-id 复位(S1)。
|
|
797
|
+
// tool_result 消息本体(B 层结构化卡)仍见 ADAPTER_COVERAGE.todo → B5。
|
|
593
798
|
const callId = typeof m.toolCallId === 'string' ? m.toolCallId : undefined;
|
|
799
|
+
recordEngineToolLabel(callId, m.label);
|
|
800
|
+
if (callId !== undefined) {
|
|
801
|
+
yield* settlePanelTasks(callId, m.isError === true, m.output !== undefined ? flattenWireOutput(m.output) : undefined);
|
|
802
|
+
}
|
|
594
803
|
if (callId !== undefined && pending.has(callId)) {
|
|
595
804
|
pending.delete(callId);
|
|
596
805
|
assistantResponseId = null;
|
|
597
806
|
}
|
|
598
807
|
break;
|
|
599
808
|
}
|
|
809
|
+
case 'task_progress': {
|
|
810
|
+
// ── B4 A 层最后一臂(MF-10)—— 引擎侧子代的累计 usage tick。
|
|
811
|
+
// 只带 rollup 数字 = CC `AgentProgress` 同形 ⇒ 忠实的 CC-187 面是 footer 面板行
|
|
812
|
+
// (CC 自己对进程外 agent 的先例:面板行 + task-notification,从不合成 inline 消息轨)。
|
|
813
|
+
// **不产任何 transcript 消息**:全是 chrome。
|
|
814
|
+
const taskId = typeof m.taskId === 'string' ? m.taskId : undefined;
|
|
815
|
+
if (taskId === undefined)
|
|
816
|
+
break;
|
|
817
|
+
const explicitParent = typeof m.parentToolCallId === 'string' ? m.parentToolCallId : undefined;
|
|
818
|
+
// ── W1 段2([1617]②,frame-lane-matrix #2/#8)workflow lane 门:workflow 子代的 tick 属
|
|
819
|
+
// fleet lane 自描述行(footer 只渲 workflow 聚合行),**绝不**进前台 subagent lane
|
|
820
|
+
// (不登 alias、不 bind、不 livePanelTasks、不发 panel tick —— 修前每个 workflow agent
|
|
821
|
+
// 都在 footer 铸一条幽灵 local_agent 行,还能骑错 run_workflow 卡的 inline stats)。
|
|
822
|
+
// 判据三级:① 帧带 workflowRunId(一手证据)② wa+16hex 形 id(core BCE 合成 id)
|
|
823
|
+
// ③ 显式父指向本会话见过的 Workflow 工具卡(实例台账,覆盖 uuid 形子代 id + 跨 turn 迟到 tick)。
|
|
824
|
+
const workflowRunId = typeof m.workflowRunId === 'string' ? m.workflowRunId : undefined;
|
|
825
|
+
if (workflowRunId !== undefined ||
|
|
826
|
+
isWorkflowAgentTaskId(taskId) ||
|
|
827
|
+
(explicitParent !== undefined && this.seenWorkflowToolCallIds.has(explicitParent))) {
|
|
828
|
+
// 判据④ 回植([1641] workflow 启动闪 subagent 行案):本门是「这条 taskId 属 workflow」的
|
|
829
|
+
// **权威判定**。同一 taskId 在 fleet 面还有一条 run 发布器双生行(`${runId} ${taskId}`)
|
|
830
|
+
// 且那行不带 workflowRunId —— 登进 belt 台账,fleet 行按尾段查它,双生行就再进不了
|
|
831
|
+
// footer task 树。🔴 写者在本包、读者(isKnownWorkflowAgentTaskId)也在本包 = R4 纪律,
|
|
832
|
+
// 做成 chrome 臂反而会让「宿主没接线 ⇒ Set 恒空 ⇒ 判据④ 静默失效」复活。
|
|
833
|
+
recordWorkflowAgentTaskId(taskId);
|
|
834
|
+
ctx.probe?.('tick', JSON.stringify({
|
|
835
|
+
t: ctx.now(),
|
|
836
|
+
taskId,
|
|
837
|
+
lane: 'workflow',
|
|
838
|
+
via: workflowRunId !== undefined
|
|
839
|
+
? 'workflowRunId'
|
|
840
|
+
: isWorkflowAgentTaskId(taskId)
|
|
841
|
+
? 'wa-id'
|
|
842
|
+
: 'workflow-card-parent',
|
|
843
|
+
}));
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
// C1 alias:content 帧只带 parentToolCallId(EventIdentity 上没有 taskId),这条 tick
|
|
847
|
+
// **两者都带** —— 是 store 学到「停放的内容属哪条引擎行」的唯一位置(查看态按引擎 task id 查)。
|
|
848
|
+
if (explicitParent !== undefined)
|
|
849
|
+
registerSubagentAlias(explicitParent, taskId);
|
|
850
|
+
// 复审修 #1:含糊回落(无显式父 + >1 张未绑候选)⇒ 保持 unbound。
|
|
851
|
+
const cardId = explicitParent === undefined && !taskCardBinding.has(taskId) && unboundSubagentCandidates() > 1
|
|
852
|
+
? undefined
|
|
853
|
+
: bindTaskCard(taskId, explicitParent);
|
|
854
|
+
const usage = (m.usage ?? {});
|
|
855
|
+
ctx.probe?.('tick', JSON.stringify({
|
|
856
|
+
t: ctx.now(),
|
|
857
|
+
taskId,
|
|
858
|
+
lane: 'foreground',
|
|
859
|
+
parent: explicitParent ?? null,
|
|
860
|
+
cardId: cardId ?? null,
|
|
861
|
+
currentAction: m.currentAction ?? null,
|
|
862
|
+
toolUses: usage.toolUses ?? null,
|
|
863
|
+
}));
|
|
864
|
+
// core 1.262.0 [558]B / service 1.154.0 [579] —— 子代的活体"我在干什么"一行
|
|
865
|
+
// ("Bash npm test"),service 已 redact。PROGRESS 语义(喂状态/摘要显示槽),不是诊断。
|
|
866
|
+
// 老引擎缺席 ⇒ 两个消费面维持今天的显示。
|
|
867
|
+
const currentAction = typeof m.currentAction === 'string' && m.currentAction.length > 0 ? m.currentAction : undefined;
|
|
868
|
+
// inline 群组行孪生:**绑定了**的 tick 同时喂转录里那条行的统计(按委派卡 id 键)。
|
|
869
|
+
if (cardId !== undefined) {
|
|
870
|
+
yield chrome({
|
|
871
|
+
kind: 'inline_task_stats',
|
|
872
|
+
laneProof: { lane: 'subagent', parentToolCallId: cardId },
|
|
873
|
+
op: 'tick',
|
|
874
|
+
cardId,
|
|
875
|
+
stats: {
|
|
876
|
+
toolUses: typeof usage.toolUses === 'number' ? usage.toolUses : 0,
|
|
877
|
+
totalTokens: typeof usage.totalTokens === 'number' ? usage.totalTokens : 0,
|
|
878
|
+
durationMs: typeof usage.durationMs === 'number' ? usage.durationMs : 0,
|
|
879
|
+
...(currentAction !== undefined ? { currentAction } : {}),
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const card = cardId !== undefined ? pending.get(cardId) : undefined;
|
|
884
|
+
// T50 双形读:`rawInput` 是**改名前**捕获的,真 wire 上引擎子代的 input 是
|
|
885
|
+
// {agent, task, taskName} —— 两形都读(CC 名优先)。
|
|
886
|
+
const input = (card?.rawInput ?? {});
|
|
887
|
+
const description = typeof input.description === 'string'
|
|
888
|
+
? input.description
|
|
889
|
+
: typeof input.taskName === 'string' && input.taskName.trim().length > 0
|
|
890
|
+
? input.taskName
|
|
891
|
+
: typeof input.task === 'string'
|
|
892
|
+
? shortTaskLabel(input.task)
|
|
893
|
+
: undefined;
|
|
894
|
+
const prompt = typeof input.prompt === 'string'
|
|
895
|
+
? input.prompt
|
|
896
|
+
: typeof input.task === 'string'
|
|
897
|
+
? input.task
|
|
898
|
+
: undefined;
|
|
899
|
+
if (!livePanelTasks.has(taskId)) {
|
|
900
|
+
// 这条引擎子代在壳侧**可观测的起点** = 本 turn 的首 tick。
|
|
901
|
+
const agentType = typeof m.name === 'string' && m.name.length > 0 ? m.name : 'subagent';
|
|
902
|
+
yield* fireSubagentStart(taskId, agentType);
|
|
903
|
+
}
|
|
904
|
+
// #6 session 常驻台账:live-bound(卡本 turn 开着)= 本 turn 生命周期,turn 末可 sweep;
|
|
905
|
+
// inert/unbound(跨 turn bg 子代形)= 常驻,两处 sweep 都放行,终态只认通知帧。
|
|
906
|
+
if (liveBoundPanelTasks.has(taskId))
|
|
907
|
+
clearEnginePanelTaskResident(taskId);
|
|
908
|
+
else
|
|
909
|
+
markEnginePanelTaskResident(taskId);
|
|
910
|
+
livePanelTasks.add(taskId);
|
|
911
|
+
yield chrome({
|
|
912
|
+
kind: 'panel_task',
|
|
913
|
+
laneProof: laneOfTask(taskId),
|
|
914
|
+
event: {
|
|
915
|
+
kind: 'tick',
|
|
916
|
+
taskId,
|
|
917
|
+
...(typeof m.name === 'string' && m.name.length > 0 ? { name: m.name } : {}),
|
|
918
|
+
...(description !== undefined ? { description } : {}),
|
|
919
|
+
...(prompt !== undefined ? { prompt } : {}),
|
|
920
|
+
...(currentAction !== undefined ? { currentAction } : {}),
|
|
921
|
+
toolUses: typeof usage.toolUses === 'number' ? usage.toolUses : 0,
|
|
922
|
+
totalTokens: typeof usage.totalTokens === 'number' ? usage.totalTokens : 0,
|
|
923
|
+
},
|
|
924
|
+
});
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
600
927
|
case 'stream_event': {
|
|
601
928
|
const ev = m.event;
|
|
602
929
|
if (ev && ev.type === 'text_delta' && typeof ev.delta === 'string') {
|
|
@@ -638,6 +965,9 @@ class WireToCcAdapterImpl {
|
|
|
638
965
|
}
|
|
639
966
|
case 'result': {
|
|
640
967
|
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
968
|
+
// MF-10 防御 sweep(sweep 点 1/3):结掉那些卡从没关过的行(流被截断)。
|
|
969
|
+
// cli 同位:setRetryStatus(null) 之后、开卡兜底关闭之前。
|
|
970
|
+
yield* settlePanelTasks(null, false);
|
|
641
971
|
yield* takeThinking();
|
|
642
972
|
if (!endEmitted) {
|
|
643
973
|
const u = m.usage;
|
|
@@ -731,19 +1061,36 @@ class WireToCcAdapterImpl {
|
|
|
731
1061
|
status: rawStatus,
|
|
732
1062
|
...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
|
|
733
1063
|
});
|
|
1064
|
+
// B1 —— 这帧 = 引擎 server-side steer-inject 的回声,模型已在引擎侧收到一遍;把还躺在
|
|
1065
|
+
// 队列里的同 run Path B 条目(workflow_complete/probe/bg feeder)在 turn 收口 drain 前
|
|
1066
|
+
// 丢弃,免得 idle auto-submit 再喂模型一遍(跨进程双投的客户端半场)。
|
|
1067
|
+
// 🔴 B4 起由**适配器自持**(B2 已把队列端口搬进本包):此前挂在 `notification_terminal`
|
|
1068
|
+
// 的宿主义务①,是「说得通但没人做」的典型 —— 宿主没接线就静默双投。
|
|
1069
|
+
dropQueuedNotificationsForRun(taskId);
|
|
734
1070
|
if (n.task_type !== 'background_bash') {
|
|
1071
|
+
// #6 通知-settle 边(推送半场):session 常驻的 bg 子代行不再被 turn sweep 假结,
|
|
1072
|
+
// 真终态在此落行(failed/killed ⇒ isError 真归因)。
|
|
1073
|
+
clearEnginePanelTaskResident(taskId);
|
|
735
1074
|
yield chrome({
|
|
736
1075
|
kind: 'panel_task',
|
|
737
1076
|
laneProof: MAIN,
|
|
738
1077
|
event: { kind: 'end', taskId, isError: rawStatus !== 'completed' },
|
|
739
1078
|
});
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1079
|
+
if (!endedPanelTasks.has(taskId)) {
|
|
1080
|
+
endedPanelTasks.add(taskId);
|
|
1081
|
+
// SubagentStop 只对**真 fire 过 Start** 的子代(workflow runId / 外来 id 不配对乱 fire)。
|
|
1082
|
+
// cli 同门 `firedSubagentStartHookTaskIds.has(taskId)` —— B4 前本臂无条件发 stop,
|
|
1083
|
+
// 宿主若照 guard 注释自建台账才不会乱 fire;台账在库里就该库来判。
|
|
1084
|
+
if (this.firedSubagentStartHookTaskIds.has(taskId)) {
|
|
1085
|
+
yield chrome({
|
|
1086
|
+
kind: 'subagent_lifecycle',
|
|
1087
|
+
laneProof: MAIN,
|
|
1088
|
+
phase: 'stop',
|
|
1089
|
+
taskId,
|
|
1090
|
+
guard: 'if-started',
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
747
1094
|
}
|
|
748
1095
|
}
|
|
749
1096
|
// 双通道反向去重:probe 合成的完成卡已入队 ⇒ 后到的推送帧只做上面的记账,不渲第二条。
|
|
@@ -886,6 +1233,9 @@ class WireToCcAdapterImpl {
|
|
|
886
1233
|
if (ctx.signal?.aborted) {
|
|
887
1234
|
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
888
1235
|
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
|
|
1236
|
+
// 复审修 #2a:被打断的 turn 也**必须**结掉自己的面板行,否则 Esc 之后 footer 药丸永远
|
|
1237
|
+
// 显示 "1 local agent"(sweep 点 2/3)。
|
|
1238
|
+
yield* settlePanelTasks(null, false);
|
|
889
1239
|
ctx.log?.('debug', 'turn aborted (Esc) — no terminal answer emitted');
|
|
890
1240
|
return;
|
|
891
1241
|
}
|
|
@@ -893,6 +1243,8 @@ class WireToCcAdapterImpl {
|
|
|
893
1243
|
// 泄尾增量 → 清 liveness → 提交尾思考 → 提交尾文本段。
|
|
894
1244
|
yield* drainLive();
|
|
895
1245
|
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
1246
|
+
// 复审修 #2a 孪生:没有 `result` 臂的收尾路径同样 sweep(sweep 点 3/3)。
|
|
1247
|
+
yield* settlePanelTasks(null, false);
|
|
896
1248
|
yield* takeThinking();
|
|
897
1249
|
yield* takeAnswerSegment();
|
|
898
1250
|
ctx.log?.('debug', `turn done · emittedAssistantText=${emittedAssistantText} answerLen=${answer.length}`);
|