@sema-agent/client-core 0.3.0 → 0.4.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/README.md +23 -1
- package/dist/agentsWireCaps.d.ts +112 -0
- package/dist/agentsWireCaps.js +319 -0
- package/dist/attachmentsWireCaps.d.ts +47 -0
- package/dist/attachmentsWireCaps.js +46 -0
- package/dist/classifierVerdictWire.d.ts +45 -0
- package/dist/classifierVerdictWire.js +76 -0
- package/dist/clientContextWireCaps.d.ts +46 -0
- package/dist/clientContextWireCaps.js +48 -0
- package/dist/cloudConfigWireCaps.d.ts +110 -0
- package/dist/cloudConfigWireCaps.js +228 -0
- package/dist/controlRouter.d.ts +191 -0
- package/dist/controlRouter.js +244 -0
- package/dist/effortWire.d.ts +34 -0
- package/dist/effortWire.js +38 -0
- package/dist/engineWireSdk.d.ts +49 -0
- package/dist/engineWireSdk.js +61 -0
- package/dist/forkWireCaps.d.ts +25 -0
- package/dist/forkWireCaps.js +42 -0
- package/dist/hostEnv.d.ts +16 -0
- package/dist/hostEnv.js +17 -0
- package/dist/imagesWireCaps.d.ts +28 -0
- package/dist/imagesWireCaps.js +49 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +41 -0
- package/dist/liveModelCatalog.d.ts +62 -0
- package/dist/liveModelCatalog.js +79 -0
- package/dist/mcpWireCaps.d.ts +41 -0
- package/dist/mcpWireCaps.js +51 -0
- package/dist/modelBudgetRule.d.ts +44 -0
- package/dist/modelBudgetRule.js +73 -0
- package/dist/modelWireCaps.d.ts +17 -0
- package/dist/modelWireCaps.js +16 -0
- package/dist/notifications.d.ts +150 -4
- package/dist/notifications.js +472 -0
- package/dist/permissionWireCaps.d.ts +37 -0
- package/dist/permissionWireCaps.js +46 -0
- package/dist/promptProfileWireCaps.d.ts +17 -0
- package/dist/promptProfileWireCaps.js +20 -0
- package/dist/retainBackgroundWireCaps.d.ts +50 -0
- package/dist/retainBackgroundWireCaps.js +58 -0
- package/dist/rewindWireCaps.d.ts +36 -0
- package/dist/rewindWireCaps.js +41 -0
- package/dist/selfOrchestrationWireCaps.d.ts +40 -0
- package/dist/selfOrchestrationWireCaps.js +48 -0
- package/dist/sessionModelLatch.d.ts +35 -0
- package/dist/sessionModelLatch.js +63 -0
- package/dist/skillsWireCaps.d.ts +47 -0
- package/dist/skillsWireCaps.js +41 -0
- package/dist/sseIdleTriage.d.ts +94 -0
- package/dist/sseIdleTriage.js +142 -0
- package/dist/ultracodeWireCaps.d.ts +90 -0
- package/dist/ultracodeWireCaps.js +159 -0
- package/dist/webSearchWireCaps.d.ts +54 -0
- package/dist/webSearchWireCaps.js +76 -0
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/modelWireCaps.ts 逐字搬入(零依赖)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* src/sema/modelWireCaps.ts — PURE per-turn /model projection (CC /model picker parity). seamQuery imports it.
|
|
6
|
+
*
|
|
7
|
+
* The mid-session model lives at `AppState`/`toolUseContext.options.mainLoopModel` (the /model picker writes it,
|
|
8
|
+
* query.ts). sema's engine ALREADY consumes a top-level `body.model` (service resolveTaskModel main.ts:1217 →
|
|
9
|
+
* core `TaskSpec.model`, catalog-gated, body.model wins over @mention wins over default). Without this wire a
|
|
10
|
+
* mid-session /model switch did NOTHING on the live engine (the survey's biggest functional hole). config-class
|
|
11
|
+
* intent → tighten/select only (the service catalog-gates an out-of-roster pick), so it rides the wire directly.
|
|
12
|
+
*/
|
|
13
|
+
/** Project the client's selected model → the wire intent. Non-empty string carried verbatim; else no stamp. */
|
|
14
|
+
export function modelFromAppState(model) {
|
|
15
|
+
return typeof model === 'string' && model.trim().length > 0 ? { model: model.trim() } : {};
|
|
16
|
+
}
|
package/dist/notifications.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 通知投影纯函数(抽自 sema-cli engineTaskNotification/upstreamBridge 通知臂;
|
|
3
|
-
* result 段纪律=cli #49 C1:帧带 result 必入 XML,core renderTaskNotificationXml 同名 tag 同段序)。
|
|
4
|
-
*/
|
|
5
1
|
/**
|
|
6
2
|
* ⚠️ 调用方契约(交叉复审 [MED] 登记):cli 原版在渲染前有两条默认值回填——
|
|
7
3
|
* summary 缺省 ⇒ `Background task "${taskId}" ${status}`;status 非串 ⇒ 'completed'。
|
|
@@ -53,3 +49,153 @@ export declare function taskNotificationDedupKey(n: {
|
|
|
53
49
|
seq?: number | string;
|
|
54
50
|
taskType?: string;
|
|
55
51
|
}): string;
|
|
52
|
+
/** 宿主队列条目的最小结构形(壳 QueuedCommand 的子集;本包只读这三个键)。 */
|
|
53
|
+
export interface QueuedCommandLike {
|
|
54
|
+
mode?: unknown;
|
|
55
|
+
agentId?: unknown;
|
|
56
|
+
value?: unknown;
|
|
57
|
+
}
|
|
58
|
+
export interface NotificationQueuePort {
|
|
59
|
+
/** 壳 = messageQueueManager.enqueuePendingNotification(无显式优先级 ⇒ 队列缺省 'later')。 */
|
|
60
|
+
enqueuePendingNotification(command: {
|
|
61
|
+
value: string;
|
|
62
|
+
mode: 'task-notification';
|
|
63
|
+
}): void;
|
|
64
|
+
/** 壳 = messageQueueManager.removeByFilter;返回被摘掉的条目(数量是判据)。 */
|
|
65
|
+
removeByFilter(predicate: (cmd: QueuedCommandLike) => boolean): QueuedCommandLike[];
|
|
66
|
+
}
|
|
67
|
+
/** 宿主装配:壳侧 shim / web 宿主在**模块加载期**装(装之前的调用会计入 miss)。 */
|
|
68
|
+
export declare function installNotificationQueuePort(port: NotificationQueuePort | null): void;
|
|
69
|
+
/** 🔴 未注入队列口而发生的投递次数 —— 恒应为 0;>0 = 宿主装配漏了,通知已静默丢失。 */
|
|
70
|
+
export declare function notificationQueuePortMisses(): number;
|
|
71
|
+
/** 测试钩:清 port + miss 计数(不动台账,台账各有自己的 reset)。 */
|
|
72
|
+
export declare function _resetNotificationQueuePortForTest(): void;
|
|
73
|
+
/**
|
|
74
|
+
* B1(frame-lane-matrix 三节定谳)— Path A 回声到达时丢弃同 run 的 pending 队列条目。
|
|
75
|
+
* 双投机理:活跃 turn 内 Path B(workflow_complete/bg_notification/probe feeder →
|
|
76
|
+
* enqueuePendingNotification)先入队,Path A(引擎 server-side steer-inject 的
|
|
77
|
+
* task_notification 回声帧)随流到达——引擎侧模型已收到一遍;turn 收口 idle 排水若再
|
|
78
|
+
* auto-submit 队列条目 = 模型二收同一份 XML。壳撤不回引擎侧注入(跨进程),但能在
|
|
79
|
+
* drain 前把已被回声证实送达的同 run 队列条目丢掉。键空间 = 裸 taskId/runId(与
|
|
80
|
+
* notifiedRunIds/cardEnqueuedRunIds 同域);条目匹配按队列 XML 的 <task-id> 标签
|
|
81
|
+
* (enqueue 两族与 bridge 渲染同用 escapeXml,键形字节对齐)。仅主线程 task-notification
|
|
82
|
+
* 条目(agentId undefined);用户输入/其他模式绝不触碰。
|
|
83
|
+
*/
|
|
84
|
+
export declare function dropQueuedNotificationsForRun(taskId: string): number;
|
|
85
|
+
/**
|
|
86
|
+
* Pre-seed the runId dedup WITHOUT enqueueing — the completion already reached the model in-band
|
|
87
|
+
* (clay 2026-07-04 复审:模型用 TaskOutput 轮询拿到 workflow 终态并当轮总结完;引擎 completion inbox
|
|
88
|
+
* 不知道"轮询已送达",下一次开流仍补发 workflow_complete —— 对模型是纯重复,还触发一轮"这是补发通知,
|
|
89
|
+
* 无需重复"的空转)。终态在 turn 内经过壳的投影(TaskOutput workflow 终态卡 / 引擎 server-side 已
|
|
90
|
+
* steer-inject 的 task_notification 帧)即预标记,Channel A 对同 runId 的补发直接丢弃。
|
|
91
|
+
*/
|
|
92
|
+
export declare function markEngineWorkflowNotified(runId: string): void;
|
|
93
|
+
export declare function isWorkflowCompletionCardEnqueued(runId: string): boolean;
|
|
94
|
+
type WorkflowStatusProbe = (runId: string) => Promise<{
|
|
95
|
+
terminal: boolean;
|
|
96
|
+
status: 'completed' | 'failed';
|
|
97
|
+
summary: string;
|
|
98
|
+
} | null>;
|
|
99
|
+
export declare function outstandingWorkflowCount(): number;
|
|
100
|
+
/** headless(-p)退出门用([1340] BGB 分化案):**可送达**的 outstanding 数——probe 未装
|
|
101
|
+
* (mock/离线)恒 0,print 驱动器不为永远等不到的通知悬置;probe 在装则 watcher 5s 节拍
|
|
102
|
+
* 必然送达或 TTL 放弃(2h 上界=run 蒸发极端形,与 CC 等本地 bg 任务的无界形同族)。 */
|
|
103
|
+
export declare function outstandingDeliverableWorkflowCount(): number;
|
|
104
|
+
export declare function subscribeOutstandingWorkflows(listener: () => void): () => void;
|
|
105
|
+
/** liveClient 在构造会话 client 时注册(带正确 baseUrl/token/principal 的 workflows.get)。 */
|
|
106
|
+
export declare function installWorkflowStatusProbe(probe: WorkflowStatusProbe): void;
|
|
107
|
+
type BgTaskStatusProbe = (taskId: string) => Promise<{
|
|
108
|
+
terminal: boolean;
|
|
109
|
+
status: string;
|
|
110
|
+
} | null>;
|
|
111
|
+
export declare function installBgTaskStatusProbe(probe: BgTaskStatusProbe): void;
|
|
112
|
+
/** bridge 在 Agent async_launched 回执(structured type:'agent')经过时登记。幂等;已通知不再登记。 */
|
|
113
|
+
export declare function registerOutstandingBgTask(taskId: string, description: string): void;
|
|
114
|
+
export declare function isOwnWorkflowRun(runId: string): boolean;
|
|
115
|
+
/** 本壳亲手启动过的 workflow run 列表(Set 插入序 = 启动序;/workflows 命令的目标 run 选择用,
|
|
116
|
+
* cmd-workflows.tsx——fleet source 无行可选时的兜底 id 源)。 */
|
|
117
|
+
export declare function listOwnWorkflowRuns(): string[];
|
|
118
|
+
/** bridge 在 run_workflow 回执(structured type:'workflow-run')经过时登记。幂等;已通知的不再登记。 */
|
|
119
|
+
export declare function registerOutstandingWorkflowRun(runId: string): void;
|
|
120
|
+
export interface EngineWorkflowCompletion {
|
|
121
|
+
runId: string;
|
|
122
|
+
status: 'completed' | 'failed';
|
|
123
|
+
summary: string;
|
|
124
|
+
}
|
|
125
|
+
export declare function enqueueBgChildNotification(n: {
|
|
126
|
+
taskId: string;
|
|
127
|
+
status: string;
|
|
128
|
+
/** bg 子代生命周期号(SendMessage 复活即 +1;service seq 字段)。去重键必须含 seq——
|
|
129
|
+
* revive 周期(seq≥2)与首周期同 (taskId,status),不带 seq 会被首周期键吞掉
|
|
130
|
+
* =「续派后完成通知永不上屏」(clay 图2 案根因,core1.355 复验帧实锤 2026-07-20)。 */
|
|
131
|
+
seq?: number;
|
|
132
|
+
summary?: string;
|
|
133
|
+
}): void;
|
|
134
|
+
export declare function enqueueEngineWorkflowNotification(c: EngineWorkflowCompletion): void;
|
|
135
|
+
/** 测试钩(B2 新增,壳侧原文件没有):清空全部通知台账 + probe 槽 + watcher。
|
|
136
|
+
* 🔴 生产绝不调用 —— 台账是 process-lifetime 去重的唯一凭据,清了就会双投。 */
|
|
137
|
+
export declare function _resetEngineTaskNotificationForTest(): void;
|
|
138
|
+
/** 该 objective 是否为 task-notification 自动提交 turn 的注入 XML(整体即通知,非提及)。 */
|
|
139
|
+
export declare function isTaskNotificationObjective(objective: unknown): boolean;
|
|
140
|
+
/**
|
|
141
|
+
* 通知 turn → supplement 文案(非通知 turn → undefined,判别不扩散)。措辞英文、CC 报错语气
|
|
142
|
+
* (事实 + 出路,不惊悚)。status 从 XML `<status>` 读:completed 用定稿句;其余终态如实带
|
|
143
|
+
* status(通知可能报 failed/killed —— 谎称 completed 比不加更糟)。
|
|
144
|
+
*/
|
|
145
|
+
export declare function taskNotificationDeliverySupplement(objective: unknown): string | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* API-error 行文拆分(渲染层消费):尾部的 supplement 行(若在)分离出来渲 dim,main 保留原始
|
|
148
|
+
* error 字面渲红。无 supplement 的行原样返回 —— 既有 API-error 渲染路径零改变。
|
|
149
|
+
*/
|
|
150
|
+
export declare function splitApiErrorSupplement(text: string): {
|
|
151
|
+
main: string;
|
|
152
|
+
supplement?: string;
|
|
153
|
+
};
|
|
154
|
+
/** SSE 平铺 wire 形(server 1.280.0 http/server.js:6347-6355 直证;ownerScope/ownerSessionId 上 wire 前剥掉)。 */
|
|
155
|
+
export interface HookNoticeWire {
|
|
156
|
+
type?: unknown;
|
|
157
|
+
kind?: unknown;
|
|
158
|
+
event?: unknown;
|
|
159
|
+
reason?: unknown;
|
|
160
|
+
detail?: unknown;
|
|
161
|
+
ts?: unknown;
|
|
162
|
+
}
|
|
163
|
+
export type HookNoticeReason = 'no_content' | 'unparsed' | 'skipped';
|
|
164
|
+
/** ActiveGoal.lastNotice 的形(壳 state/AppStateStore.ts 同步声明)。 */
|
|
165
|
+
export interface GoalHookNotice {
|
|
166
|
+
reason: HookNoticeReason;
|
|
167
|
+
detail?: string;
|
|
168
|
+
at: number;
|
|
169
|
+
}
|
|
170
|
+
/** 判定结果:ok=true 才交给宿主写 store;drop 原因供宿主打 SEMA_DEBUG 档(壳侧文案不变)。 */
|
|
171
|
+
export type HookNoticeVerdict = {
|
|
172
|
+
ok: true;
|
|
173
|
+
notice: GoalHookNotice;
|
|
174
|
+
event: 'Stop';
|
|
175
|
+
} | {
|
|
176
|
+
ok: false;
|
|
177
|
+
drop: 'not-hook-notice' | 'unknown-kind' | 'not-session-scoped' | 'non-stop-event';
|
|
178
|
+
kind?: string;
|
|
179
|
+
event?: string;
|
|
180
|
+
reason?: HookNoticeReason;
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* hook_notice 帧的判定本体(壳 ingestHookNoticeFrame 的判定半场逐字等价)。
|
|
184
|
+
* 🔴 归属镜像在**这里**做而不是在 fleetClient 里做 —— #105 变异普查(F3)定谳过:分发条件留在
|
|
185
|
+
* fleetClient 时,离线门只能 grep 源码,而 grep 对「把 'hook_notice' 改成 'hook_noticeZZZ'」
|
|
186
|
+
* 这类变异因注释碰撞+子串命中恒绿。提到这里后离线门直接**行为测本函数**。
|
|
187
|
+
*/
|
|
188
|
+
export declare function classifyHookNoticeFrame(frame: unknown, opts: {
|
|
189
|
+
sessionScoped: boolean;
|
|
190
|
+
now?: number;
|
|
191
|
+
}): HookNoticeVerdict;
|
|
192
|
+
/** 判别口:是不是 hook_notice 帧(宿主 fleet applyFrame 的分发条件;不是则继续走别的臂)。 */
|
|
193
|
+
export declare function isHookNoticeFrame(frame: unknown): boolean;
|
|
194
|
+
/**
|
|
195
|
+
* 「Last check」行的诚实文案([1790] 措辞纪律:「本轮未能评估,已放行」方向;detail 是 server
|
|
196
|
+
* 结构化日志同源的机器话,dim 展示原文比意译更可追责)。
|
|
197
|
+
*/
|
|
198
|
+
export declare function hookNoticeLastCheckText(n: GoalHookNotice): string;
|
|
199
|
+
/** footer 通知条文案前缀(测试锁字面;宿主拼 ` (${reason})`)。 */
|
|
200
|
+
export declare const HOOK_NOTICE_WARN_PREFIX = "Goal check: couldn't evaluate this turn \u2014 proceeded";
|
|
201
|
+
export {};
|
package/dist/notifications.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 通知投影纯函数(抽自 sema-cli engineTaskNotification/upstreamBridge 通知臂;
|
|
3
3
|
* result 段纪律=cli #49 C1:帧带 result 必入 XML,core renderTaskNotificationXml 同名 tag 同段序)。
|
|
4
|
+
* 0.4.0(B2 批)起本模块还含:engineTaskNotification 全文(台账/watcher/入队合成)、
|
|
5
|
+
* taskNotificationErrorSupplement 全文、hookNoticeStore 的判定半场 —— 见文件下半的 B2 分节。
|
|
4
6
|
*/
|
|
7
|
+
import { hostEnv } from './hostEnv.js';
|
|
8
|
+
import { clearEnginePanelTaskResident, publishEngineAgentPanelEvent, } from './engineAgentPanelStore.js';
|
|
5
9
|
function escapeXml(v) {
|
|
6
10
|
return v.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
7
11
|
}
|
|
@@ -95,3 +99,471 @@ export function taskNotificationDedupKey(n) {
|
|
|
95
99
|
const lane = n.taskType === 'external' ? 'external:' : '';
|
|
96
100
|
return `${lane}${n.taskId}:${n.status}${seq ? `:${seq}` : ''}`;
|
|
97
101
|
}
|
|
102
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
103
|
+
// ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2「通知族合并」):cli 三个文件并入本模块
|
|
104
|
+
// ① src/sema/engineTaskNotification.ts(347 行,台账 + watcher + 入队合成)
|
|
105
|
+
// ② src/sema/taskNotificationErrorSupplement.ts(58 行,纯函数)
|
|
106
|
+
// ③ src/sema/hookNoticeStore.ts(177 行)的**判定半场**(store 写入半场留宿主)
|
|
107
|
+
// 🔴 单实例纪律(B1 血泪同款):①带 11 个模块级台账(notifiedRunIds / cardEnqueuedRunIds /
|
|
108
|
+
// outstandingRuns / outstandingBgTasks / ownWorkflowRuns / bgNotifiedKeys / 两个 probe 槽 /
|
|
109
|
+
// listener 集 / watchTimer / tickInFlight)—— 写口(bridge 登记、probe 安装)与读口
|
|
110
|
+
// (footer outstanding 计数、fleet own 判别、去重)必须解析到**同一个包实例**。
|
|
111
|
+
// 🔴 队列口(messageQueueManager)是宿主件:本包不 import 壳的队列,改成**注入 port**
|
|
112
|
+
// (设计稿原文「messageQueueManager 调用 ⇒ chrome 事件」的等价实现:先给最小注入口,
|
|
113
|
+
// chrome 事件化留给 B3 的 AdapterContext 扩容)。**未注入 = 通知投递静默丢失**,所以
|
|
114
|
+
// miss 有计数 + 一次性告警,绝不让它悄悄消失(a0110e88 零渲染案的教训:配对机制必须共享前提)。
|
|
115
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
116
|
+
// cli src/constants/xml.ts 的四个 tag 常量(值逐字对齐;本包不引壳的 constants 桶)。
|
|
117
|
+
const TASK_NOTIFICATION_TAG = 'task-notification';
|
|
118
|
+
const TASK_ID_TAG = 'task-id';
|
|
119
|
+
const STATUS_TAG = 'status';
|
|
120
|
+
const SUMMARY_TAG = 'summary';
|
|
121
|
+
let queuePort = null;
|
|
122
|
+
let queuePortMisses = 0;
|
|
123
|
+
let queuePortWarned = false;
|
|
124
|
+
/** 宿主装配:壳侧 shim / web 宿主在**模块加载期**装(装之前的调用会计入 miss)。 */
|
|
125
|
+
export function installNotificationQueuePort(port) {
|
|
126
|
+
queuePort = port;
|
|
127
|
+
}
|
|
128
|
+
/** 🔴 未注入队列口而发生的投递次数 —— 恒应为 0;>0 = 宿主装配漏了,通知已静默丢失。 */
|
|
129
|
+
export function notificationQueuePortMisses() {
|
|
130
|
+
return queuePortMisses;
|
|
131
|
+
}
|
|
132
|
+
function port() {
|
|
133
|
+
if (queuePort)
|
|
134
|
+
return queuePort;
|
|
135
|
+
queuePortMisses++;
|
|
136
|
+
if (!queuePortWarned) {
|
|
137
|
+
queuePortWarned = true;
|
|
138
|
+
// eslint-disable-next-line no-console
|
|
139
|
+
console.error('[client-core] notification queue port NOT installed — task-notification delivery is being dropped. ' +
|
|
140
|
+
'Host must call installNotificationQueuePort() at startup (see notifications.ts B2 section).');
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/** 测试钩:清 port + miss 计数(不动台账,台账各有自己的 reset)。 */
|
|
145
|
+
export function _resetNotificationQueuePortForTest() {
|
|
146
|
+
queuePort = null;
|
|
147
|
+
queuePortMisses = 0;
|
|
148
|
+
queuePortWarned = false;
|
|
149
|
+
}
|
|
150
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
151
|
+
// ① engineTaskNotification — engine-side background-run completion → CC `<task-notification>` injection.
|
|
152
|
+
//
|
|
153
|
+
// The service pushes a `workflow_complete` named SSE frame (service 1.75, `{runId, status, summary}` —
|
|
154
|
+
// an explicit whitelist) onto the session's NEXT `/v1/tasks/stream` connection when a background
|
|
155
|
+
// workflow it launched earlier finishes out-of-band. The shell turns that frame into CC's OWN
|
|
156
|
+
// remote-task completion mechanism (the RemoteAgentTask `enqueueRemoteNotification` precedent,
|
|
157
|
+
// CC 187 `Nco`): build the `<task-notification>` XML, enqueue it as a `task-notification` queued
|
|
158
|
+
// command, and let CC's native drain / queue processor (idle → auto-submit) deliver it to the model.
|
|
159
|
+
// No explicit priority — CC's RemoteAgentTask enqueues WITHOUT one, so the queue default ('later',
|
|
160
|
+
// messageQueueManager: "so user input is never starved") applies; 'next' is the LocalShellTask/
|
|
161
|
+
// MONITOR_TOOL precedent, not the remote-completion one (adversarial review w0zwpa251 §5).
|
|
162
|
+
//
|
|
163
|
+
// Dedup: CC marks the task registry entry `notified` — an engine workflow has no registry entry, so a
|
|
164
|
+
// PROCESS-LIFETIME Set on runId does the same job (the service inbox is at-least-once; its enqueue is
|
|
165
|
+
// runId-idempotent across restarts, this Set covers redelivery within one shell process).
|
|
166
|
+
//
|
|
167
|
+
// No `<output-file>` line: CC's output-file points at a LOCAL task output file (getTaskOutputPath);
|
|
168
|
+
// an engine run has none — the SDK-exit regex reads tags independently, an absent tag is undefined
|
|
169
|
+
// (the `<tool-use-id>` line is optional in CC's own template the same way).
|
|
170
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
171
|
+
const notifiedRunIds = new Set();
|
|
172
|
+
/**
|
|
173
|
+
* B1(frame-lane-matrix 三节定谳)— Path A 回声到达时丢弃同 run 的 pending 队列条目。
|
|
174
|
+
* 双投机理:活跃 turn 内 Path B(workflow_complete/bg_notification/probe feeder →
|
|
175
|
+
* enqueuePendingNotification)先入队,Path A(引擎 server-side steer-inject 的
|
|
176
|
+
* task_notification 回声帧)随流到达——引擎侧模型已收到一遍;turn 收口 idle 排水若再
|
|
177
|
+
* auto-submit 队列条目 = 模型二收同一份 XML。壳撤不回引擎侧注入(跨进程),但能在
|
|
178
|
+
* drain 前把已被回声证实送达的同 run 队列条目丢掉。键空间 = 裸 taskId/runId(与
|
|
179
|
+
* notifiedRunIds/cardEnqueuedRunIds 同域);条目匹配按队列 XML 的 <task-id> 标签
|
|
180
|
+
* (enqueue 两族与 bridge 渲染同用 escapeXml,键形字节对齐)。仅主线程 task-notification
|
|
181
|
+
* 条目(agentId undefined);用户输入/其他模式绝不触碰。
|
|
182
|
+
*/
|
|
183
|
+
export function dropQueuedNotificationsForRun(taskId) {
|
|
184
|
+
const needle = `<${TASK_ID_TAG}>${escapeXml(taskId)}</${TASK_ID_TAG}>`;
|
|
185
|
+
const p = port();
|
|
186
|
+
if (!p)
|
|
187
|
+
return 0;
|
|
188
|
+
const dropped = p.removeByFilter(cmd => cmd.mode === 'task-notification' &&
|
|
189
|
+
cmd.agentId === undefined &&
|
|
190
|
+
typeof cmd.value === 'string' &&
|
|
191
|
+
cmd.value.includes(needle));
|
|
192
|
+
// 🔴 a0110e88 零渲染案(clay 1.0.55 真机):删掉的条目正是 cardEnqueuedRunIds
|
|
193
|
+
// 记号所指的「会渲卡的队列条目」——条目死了记号还在,推送帧的渲染臂会按记号 break,两条渲染路
|
|
194
|
+
// 互相踢皮球 = UI 永远没有这条通知。删除成功必须同时交还卡所有权(清记号),让帧臂渲染。
|
|
195
|
+
// 模型面不受影响:core 文档化设计(task-notification.ts design/116 §7)保证 turn 边界 steer
|
|
196
|
+
// 注入,引擎侧单投成立;这里只救 UI 半场。
|
|
197
|
+
if (dropped.length > 0) {
|
|
198
|
+
cardEnqueuedRunIds.delete(taskId);
|
|
199
|
+
if (hostEnv().SEMA_DEBUG) {
|
|
200
|
+
// eslint-disable-next-line no-console
|
|
201
|
+
console.error(`[sema][notif] dropped ${dropped.length} queued task-notification(s) for ${taskId} — engine echo already delivered (B1); card ownership returned to the frame lane`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return dropped.length;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Pre-seed the runId dedup WITHOUT enqueueing — the completion already reached the model in-band
|
|
208
|
+
* (clay 2026-07-04 复审:模型用 TaskOutput 轮询拿到 workflow 终态并当轮总结完;引擎 completion inbox
|
|
209
|
+
* 不知道"轮询已送达",下一次开流仍补发 workflow_complete —— 对模型是纯重复,还触发一轮"这是补发通知,
|
|
210
|
+
* 无需重复"的空转)。终态在 turn 内经过壳的投影(TaskOutput workflow 终态卡 / 引擎 server-side 已
|
|
211
|
+
* steer-inject 的 task_notification 帧)即预标记,Channel A 对同 runId 的补发直接丢弃。
|
|
212
|
+
*/
|
|
213
|
+
export function markEngineWorkflowNotified(runId) {
|
|
214
|
+
notifiedRunIds.add(runId);
|
|
215
|
+
if (outstandingRuns.delete(runId))
|
|
216
|
+
notifyOutstanding();
|
|
217
|
+
}
|
|
218
|
+
/** 双通道「完成卡」去重(clay dogfood 2026-07-19 双通知案):与 notifiedRunIds(模型通知去重,
|
|
219
|
+
* external mark 也写它)**分键**——本集只由 probe 合成 enqueue 写入(workflow+bg agent 两族,
|
|
220
|
+
* 对抗复审§1:bg 合成不写此集时 probe 先到→后到推送帧仍渲第二张卡)、只由推送帧渲染前读,
|
|
221
|
+
* 故 external 先到的 mark 不会误吞后到的内部帧(dedup-lane 单测双向钉)。 */
|
|
222
|
+
const cardEnqueuedRunIds = new Set();
|
|
223
|
+
export function isWorkflowCompletionCardEnqueued(runId) {
|
|
224
|
+
return cardEnqueuedRunIds.has(runId);
|
|
225
|
+
}
|
|
226
|
+
const outstandingRuns = new Map(); // runId → registeredAt(ms)
|
|
227
|
+
// ── D4(212 对拍):outstanding 计数订阅面——REPL 空闲态渲「✻ Waiting for N dynamic
|
|
228
|
+
// workflow(s) to finish」(CC 2.1.212 同形,wf-ui-cc2 ground truth)。仅通知计数变化。
|
|
229
|
+
const outstandingListeners = new Set();
|
|
230
|
+
function notifyOutstanding() {
|
|
231
|
+
for (const l of [...outstandingListeners]) {
|
|
232
|
+
try {
|
|
233
|
+
l();
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
/* 订阅方异常不拖累通知链 */
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export function outstandingWorkflowCount() {
|
|
241
|
+
return outstandingRuns.size;
|
|
242
|
+
}
|
|
243
|
+
/** headless(-p)退出门用([1340] BGB 分化案):**可送达**的 outstanding 数——probe 未装
|
|
244
|
+
* (mock/离线)恒 0,print 驱动器不为永远等不到的通知悬置;probe 在装则 watcher 5s 节拍
|
|
245
|
+
* 必然送达或 TTL 放弃(2h 上界=run 蒸发极端形,与 CC 等本地 bg 任务的无界形同族)。 */
|
|
246
|
+
export function outstandingDeliverableWorkflowCount() {
|
|
247
|
+
return statusProbe ? outstandingRuns.size : 0;
|
|
248
|
+
}
|
|
249
|
+
export function subscribeOutstandingWorkflows(listener) {
|
|
250
|
+
outstandingListeners.add(listener);
|
|
251
|
+
return () => {
|
|
252
|
+
outstandingListeners.delete(listener);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
let statusProbe = null;
|
|
256
|
+
let watchTimer = null;
|
|
257
|
+
const WATCH_INTERVAL_MS = 5000;
|
|
258
|
+
const WATCH_TTL_MS = 2 * 60 * 60 * 1000; // 2h 兜底放弃(run 蒸发/owner 不可读时不永轮)
|
|
259
|
+
/** liveClient 在构造会话 client 时注册(带正确 baseUrl/token/principal 的 workflows.get)。 */
|
|
260
|
+
export function installWorkflowStatusProbe(probe) {
|
|
261
|
+
statusProbe = probe;
|
|
262
|
+
}
|
|
263
|
+
const outstandingBgTasks = new Map();
|
|
264
|
+
let bgStatusProbe = null;
|
|
265
|
+
export function installBgTaskStatusProbe(probe) {
|
|
266
|
+
bgStatusProbe = probe;
|
|
267
|
+
}
|
|
268
|
+
/** bridge 在 Agent async_launched 回执(structured type:'agent')经过时登记。幂等;已通知不再登记。 */
|
|
269
|
+
export function registerOutstandingBgTask(taskId, description) {
|
|
270
|
+
if (!taskId || notifiedRunIds.has(taskId) || outstandingBgTasks.has(taskId))
|
|
271
|
+
return;
|
|
272
|
+
outstandingBgTasks.set(taskId, { registeredAt: Date.now(), description });
|
|
273
|
+
ensureWatchTimer();
|
|
274
|
+
}
|
|
275
|
+
/** 本壳亲手启动过的 workflow run(process-lifetime,只增不摘——outstandingRuns 会随完成摘除,
|
|
276
|
+
* 这个集合是 fleet 行渲染的 own 判别台账;clay 2026-07-23 双 session 串扰案:旧代际引擎对
|
|
277
|
+
* fleet 行帧广播,workflow 行必须有壳侧行级判别兜底,与 bg_notification 的 isOwnEngineRun
|
|
278
|
+
* 台账对称)。诚实边界同款:壳重启/resume 后旧 workflow 行在老引擎下会被藏(1.244+ server
|
|
279
|
+
* 已按会话过滤=sessionScoped 让位,不受此限)。 */
|
|
280
|
+
const ownWorkflowRuns = new Set();
|
|
281
|
+
export function isOwnWorkflowRun(runId) {
|
|
282
|
+
return ownWorkflowRuns.has(runId);
|
|
283
|
+
}
|
|
284
|
+
/** 本壳亲手启动过的 workflow run 列表(Set 插入序 = 启动序;/workflows 命令的目标 run 选择用,
|
|
285
|
+
* cmd-workflows.tsx——fleet source 无行可选时的兜底 id 源)。 */
|
|
286
|
+
export function listOwnWorkflowRuns() {
|
|
287
|
+
return [...ownWorkflowRuns];
|
|
288
|
+
}
|
|
289
|
+
/** bridge 在 run_workflow 回执(structured type:'workflow-run')经过时登记。幂等;已通知的不再登记。 */
|
|
290
|
+
export function registerOutstandingWorkflowRun(runId) {
|
|
291
|
+
if (!runId)
|
|
292
|
+
return;
|
|
293
|
+
ownWorkflowRuns.add(runId); // own 台账先记(与 outstanding 摘除解耦)
|
|
294
|
+
if (notifiedRunIds.has(runId) || outstandingRuns.has(runId))
|
|
295
|
+
return;
|
|
296
|
+
outstandingRuns.set(runId, Date.now());
|
|
297
|
+
notifyOutstanding();
|
|
298
|
+
ensureWatchTimer();
|
|
299
|
+
}
|
|
300
|
+
function ensureWatchTimer() {
|
|
301
|
+
if (watchTimer !== null)
|
|
302
|
+
return;
|
|
303
|
+
watchTimer = setInterval(() => {
|
|
304
|
+
void tickWatch();
|
|
305
|
+
}, WATCH_INTERVAL_MS);
|
|
306
|
+
// 不阻止进程退出
|
|
307
|
+
if (typeof watchTimer.unref === 'function') {
|
|
308
|
+
;
|
|
309
|
+
watchTimer.unref?.();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// 重入护栏(对抗复审§5):慢 probe(>5s)时多个 tick 并发走同一 outstanding 快照=同任务
|
|
313
|
+
// 重复探测×N。单飞:在飞即跳过本 tick,下个节拍自然补上。
|
|
314
|
+
let tickInFlight = false;
|
|
315
|
+
async function tickWatch() {
|
|
316
|
+
if (outstandingRuns.size === 0 && outstandingBgTasks.size === 0) {
|
|
317
|
+
if (watchTimer !== null) {
|
|
318
|
+
clearInterval(watchTimer);
|
|
319
|
+
watchTimer = null;
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (tickInFlight)
|
|
324
|
+
return;
|
|
325
|
+
tickInFlight = true;
|
|
326
|
+
try {
|
|
327
|
+
await tickWatchInner();
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
tickInFlight = false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async function tickWatchInner() {
|
|
334
|
+
// bg agent 半场(与 workflow 半场同节拍同 TTL;probe 未装=mock/离线,只等推送补发)
|
|
335
|
+
const bgProbe = bgStatusProbe;
|
|
336
|
+
const bgNow = Date.now();
|
|
337
|
+
for (const [taskId, meta] of [...outstandingBgTasks]) {
|
|
338
|
+
if (notifiedRunIds.has(taskId)) {
|
|
339
|
+
outstandingBgTasks.delete(taskId); // 推送/别的通道已送达 — 收摊
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (bgNow - meta.registeredAt > WATCH_TTL_MS) {
|
|
343
|
+
outstandingBgTasks.delete(taskId);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (!bgProbe)
|
|
347
|
+
continue;
|
|
348
|
+
try {
|
|
349
|
+
const res = await bgProbe(taskId);
|
|
350
|
+
if (res?.terminal) {
|
|
351
|
+
outstandingBgTasks.delete(taskId);
|
|
352
|
+
enqueueBgChildNotification({
|
|
353
|
+
taskId,
|
|
354
|
+
status: res.status,
|
|
355
|
+
summary: `Background agent "${meta.description}" ${res.status}`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// 单次探测失败不放弃(网络抖动);TTL 兜底
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const probe = statusProbe;
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
for (const [runId, registeredAt] of [...outstandingRuns]) {
|
|
366
|
+
if (notifiedRunIds.has(runId)) {
|
|
367
|
+
if (outstandingRuns.delete(runId))
|
|
368
|
+
notifyOutstanding(); // 推送/终态轮询卡已送达 —— 收摊
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (now - registeredAt > WATCH_TTL_MS) {
|
|
372
|
+
if (outstandingRuns.delete(runId))
|
|
373
|
+
notifyOutstanding();
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (!probe)
|
|
377
|
+
continue; // live client 未装(mock/离线)→ 只等推送
|
|
378
|
+
try {
|
|
379
|
+
const res = await probe(runId);
|
|
380
|
+
if (res?.terminal) {
|
|
381
|
+
if (outstandingRuns.delete(runId))
|
|
382
|
+
notifyOutstanding();
|
|
383
|
+
enqueueEngineWorkflowNotification({
|
|
384
|
+
runId,
|
|
385
|
+
status: res.status,
|
|
386
|
+
summary: res.summary,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// 单次探测失败不放弃(网络抖动);TTL 兜底
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* bg_notification 帧桥([446] 帧契约,service 1.119 上线):session 级后台子代的
|
|
397
|
+
* 完成/终态通知走常开 fleet lane → 这里转入 #21 的 pending-notification 注入链
|
|
398
|
+
* (idle 自动注入,彻底关掉「bg 完成通知等下次输入才 drain」的死窗)。
|
|
399
|
+
* (taskId,status) 去重=外圈皮带(durable park 腿重放/at-least-once 双投自免)。
|
|
400
|
+
*/
|
|
401
|
+
const bgNotifiedKeys = new Set();
|
|
402
|
+
export function enqueueBgChildNotification(n) {
|
|
403
|
+
const key = `${n.taskId}:${n.seq ?? 1}:${n.status}`;
|
|
404
|
+
if (bgNotifiedKeys.has(key))
|
|
405
|
+
return;
|
|
406
|
+
// cross-channel 去重(clay 2026-07-06 截图疑似双唤醒定谳):同一完成可能同时走
|
|
407
|
+
// workflow_complete/probe 链(notifiedRunIds 键空间)与 bg_notification 帧——两空间互认,
|
|
408
|
+
// 任一链注入过即跳过,并反向 seed。仅首周期查(revive 周期是新完成,probe 链只报首周期
|
|
409
|
+
// 终态,不构成双投)。
|
|
410
|
+
if ((n.seq ?? 1) <= 1 && notifiedRunIds.has(n.taskId))
|
|
411
|
+
return;
|
|
412
|
+
bgNotifiedKeys.add(key);
|
|
413
|
+
notifiedRunIds.add(n.taskId);
|
|
414
|
+
cardEnqueuedRunIds.add(n.taskId);
|
|
415
|
+
// #6 通知-settle 边(合成半场):bg 子代行不再被 turn sweep 假结(session 常驻台账),真终态
|
|
416
|
+
// 唯二来源 = 推送帧(bridge task_notification 臂)与本合成链(probe/fleet bg_notification 收敛点)。
|
|
417
|
+
// 消费端 settle 幂等(running 才动),推送帧先到时此发布为 no-op。
|
|
418
|
+
try {
|
|
419
|
+
clearEnginePanelTaskResident(n.taskId);
|
|
420
|
+
publishEngineAgentPanelEvent({
|
|
421
|
+
kind: 'end',
|
|
422
|
+
taskId: n.taskId,
|
|
423
|
+
isError: n.status === 'failed' || n.status === 'killed',
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
/* fail-soft — 面板 settle 失败绝不挡模型通知注入 */
|
|
428
|
+
}
|
|
429
|
+
const message = `<${TASK_NOTIFICATION_TAG}>
|
|
430
|
+
<${TASK_ID_TAG}>${escapeXml(n.taskId)}</${TASK_ID_TAG}>
|
|
431
|
+
<${STATUS_TAG}>${escapeXml(n.status)}</${STATUS_TAG}>
|
|
432
|
+
<${SUMMARY_TAG}>${escapeXml(n.summary ?? `Background agent ${n.status}`)}</${SUMMARY_TAG}>
|
|
433
|
+
</${TASK_NOTIFICATION_TAG}>`;
|
|
434
|
+
port()?.enqueuePendingNotification({
|
|
435
|
+
value: message,
|
|
436
|
+
mode: 'task-notification',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
export function enqueueEngineWorkflowNotification(c) {
|
|
440
|
+
if (notifiedRunIds.has(c.runId))
|
|
441
|
+
return;
|
|
442
|
+
notifiedRunIds.add(c.runId);
|
|
443
|
+
cardEnqueuedRunIds.add(c.runId);
|
|
444
|
+
const message = `<${TASK_NOTIFICATION_TAG}>
|
|
445
|
+
<${TASK_ID_TAG}>${escapeXml(c.runId)}</${TASK_ID_TAG}>
|
|
446
|
+
<${STATUS_TAG}>${c.status}</${STATUS_TAG}>
|
|
447
|
+
<${SUMMARY_TAG}>${escapeXml(c.summary)}</${SUMMARY_TAG}>
|
|
448
|
+
</${TASK_NOTIFICATION_TAG}>`;
|
|
449
|
+
port()?.enqueuePendingNotification({
|
|
450
|
+
value: message,
|
|
451
|
+
mode: 'task-notification',
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/** 测试钩(B2 新增,壳侧原文件没有):清空全部通知台账 + probe 槽 + watcher。
|
|
455
|
+
* 🔴 生产绝不调用 —— 台账是 process-lifetime 去重的唯一凭据,清了就会双投。 */
|
|
456
|
+
export function _resetEngineTaskNotificationForTest() {
|
|
457
|
+
notifiedRunIds.clear();
|
|
458
|
+
cardEnqueuedRunIds.clear();
|
|
459
|
+
outstandingRuns.clear();
|
|
460
|
+
outstandingBgTasks.clear();
|
|
461
|
+
ownWorkflowRuns.clear();
|
|
462
|
+
bgNotifiedKeys.clear();
|
|
463
|
+
outstandingListeners.clear();
|
|
464
|
+
statusProbe = null;
|
|
465
|
+
bgStatusProbe = null;
|
|
466
|
+
tickInFlight = false;
|
|
467
|
+
if (watchTimer !== null) {
|
|
468
|
+
clearInterval(watchTimer);
|
|
469
|
+
watchTimer = null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
473
|
+
// ② taskNotificationErrorSupplement — S4-P3(clay 专项;取证 workflow-card-forensics,
|
|
474
|
+
// 2026-07-24「workflow completed 卡下挂 API Error: 503 · draining」案):task-notification
|
|
475
|
+
// 自动提交 turn 的 API-error 行 supplement。
|
|
476
|
+
//
|
|
477
|
+
// 场景:后台 workflow/bg agent 完成 → 完成通知入队自动提交新 turn(enqueueEngineWorkflowNotification/
|
|
478
|
+
// enqueueBgChildNotification → queueProcessor idle 排水)→ 该 turn 的引擎提交失败(典型:撞温切排水的
|
|
479
|
+
// 503 draining,S4-P0 有界重试打光)→ CC 转录形制里 assistant error 行恒以 ⎿ 臂贴在其 user 消息
|
|
480
|
+
// (= 通知卡)正下方,用户误读为「后台任务本身失败」。这里生成一句 supplement 澄清:任务本身已终态,
|
|
481
|
+
// 失败的只是「把结果通知投递给模型」这一轮;/tasks 可查结果。supplement, never mask
|
|
482
|
+
// (detachWire/sandboxDegradeHint 同款姿势):原始 error 行原样保留,只追加一行。
|
|
483
|
+
//
|
|
484
|
+
// 判别:通知 turn 的 objective 就是合成的 `<task-notification>` XML 整体(queueProcessor 自动提交,
|
|
485
|
+
// user 消息 origin kind 'task-notification')——以 trimStart() 后 startsWith 判型,用户手打正文里
|
|
486
|
+
// 【提到】该 tag 不误伤。
|
|
487
|
+
// 渲染:AssistantTextMessage 的 API-error 臂经 splitApiErrorSupplement 把本行拆出来渲 dim。
|
|
488
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
489
|
+
/** 所有 supplement 变体共享的行首 —— 渲染侧拆行锚(splitApiErrorSupplement),两侧必须同步改。 */
|
|
490
|
+
const SUPPLEMENT_LINE_PREFIX = 'The background task itself ';
|
|
491
|
+
/** 该 objective 是否为 task-notification 自动提交 turn 的注入 XML(整体即通知,非提及)。 */
|
|
492
|
+
export function isTaskNotificationObjective(objective) {
|
|
493
|
+
return (typeof objective === 'string' &&
|
|
494
|
+
objective.trimStart().startsWith(`<${TASK_NOTIFICATION_TAG}>`));
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* 通知 turn → supplement 文案(非通知 turn → undefined,判别不扩散)。措辞英文、CC 报错语气
|
|
498
|
+
* (事实 + 出路,不惊悚)。status 从 XML `<status>` 读:completed 用定稿句;其余终态如实带
|
|
499
|
+
* status(通知可能报 failed/killed —— 谎称 completed 比不加更糟)。
|
|
500
|
+
*/
|
|
501
|
+
export function taskNotificationDeliverySupplement(objective) {
|
|
502
|
+
if (!isTaskNotificationObjective(objective))
|
|
503
|
+
return undefined;
|
|
504
|
+
const m = new RegExp(`<${STATUS_TAG}>([^<]*)</${STATUS_TAG}>`).exec(objective);
|
|
505
|
+
const status = m?.[1]?.trim();
|
|
506
|
+
if (status === 'completed') {
|
|
507
|
+
return `${SUPPLEMENT_LINE_PREFIX}completed — only this notification delivery failed. Check /tasks for the result.`;
|
|
508
|
+
}
|
|
509
|
+
const statusNote = status && status.length > 0 ? ` (status: ${status})` : '';
|
|
510
|
+
return `${SUPPLEMENT_LINE_PREFIX}already finished${statusNote} — only this notification delivery failed. Check /tasks for the result.`;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* API-error 行文拆分(渲染层消费):尾部的 supplement 行(若在)分离出来渲 dim,main 保留原始
|
|
514
|
+
* error 字面渲红。无 supplement 的行原样返回 —— 既有 API-error 渲染路径零改变。
|
|
515
|
+
*/
|
|
516
|
+
export function splitApiErrorSupplement(text) {
|
|
517
|
+
const idx = text.lastIndexOf(`\n${SUPPLEMENT_LINE_PREFIX}`);
|
|
518
|
+
if (idx === -1)
|
|
519
|
+
return { main: text };
|
|
520
|
+
return { main: text.slice(0, idx), supplement: text.slice(idx + 1) };
|
|
521
|
+
}
|
|
522
|
+
const KNOWN_REASONS = new Set(['no_content', 'unparsed', 'skipped']);
|
|
523
|
+
/**
|
|
524
|
+
* hook_notice 帧的判定本体(壳 ingestHookNoticeFrame 的判定半场逐字等价)。
|
|
525
|
+
* 🔴 归属镜像在**这里**做而不是在 fleetClient 里做 —— #105 变异普查(F3)定谳过:分发条件留在
|
|
526
|
+
* fleetClient 时,离线门只能 grep 源码,而 grep 对「把 'hook_notice' 改成 'hook_noticeZZZ'」
|
|
527
|
+
* 这类变异因注释碰撞+子串命中恒绿。提到这里后离线门直接**行为测本函数**。
|
|
528
|
+
*/
|
|
529
|
+
export function classifyHookNoticeFrame(frame, opts) {
|
|
530
|
+
const f = frame;
|
|
531
|
+
if (f?.type !== 'hook_notice')
|
|
532
|
+
return { ok: false, drop: 'not-hook-notice' };
|
|
533
|
+
if (f.kind !== 'hook_decision_unavailable') {
|
|
534
|
+
// 未来新 kind:纯 observe 道,未知即忽略(不猜语义)。
|
|
535
|
+
return { ok: false, drop: 'unknown-kind', kind: String(f.kind) };
|
|
536
|
+
}
|
|
537
|
+
// 🔴 归属镜像:server 只对 scoped 连接 fail-closed;未 scoped 连接上这帧可能是任何会话的。
|
|
538
|
+
if (!opts.sessionScoped)
|
|
539
|
+
return { ok: false, drop: 'not-session-scoped' };
|
|
540
|
+
const reason = typeof f.reason === 'string' && KNOWN_REASONS.has(f.reason)
|
|
541
|
+
? f.reason
|
|
542
|
+
: 'skipped';
|
|
543
|
+
const event = typeof f.event === 'string' ? f.event : '';
|
|
544
|
+
// 非 Stop 事件今天没有承载面(PreToolUse 等等有承载面再接)。
|
|
545
|
+
if (event !== 'Stop')
|
|
546
|
+
return { ok: false, drop: 'non-stop-event', event, reason };
|
|
547
|
+
return {
|
|
548
|
+
ok: true,
|
|
549
|
+
event: 'Stop',
|
|
550
|
+
notice: {
|
|
551
|
+
reason,
|
|
552
|
+
...(typeof f.detail === 'string' && f.detail ? { detail: f.detail } : {}),
|
|
553
|
+
at: typeof f.ts === 'number' ? f.ts : (opts.now ?? Date.now()),
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/** 判别口:是不是 hook_notice 帧(宿主 fleet applyFrame 的分发条件;不是则继续走别的臂)。 */
|
|
558
|
+
export function isHookNoticeFrame(frame) {
|
|
559
|
+
return frame?.type === 'hook_notice';
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* 「Last check」行的诚实文案([1790] 措辞纪律:「本轮未能评估,已放行」方向;detail 是 server
|
|
563
|
+
* 结构化日志同源的机器话,dim 展示原文比意译更可追责)。
|
|
564
|
+
*/
|
|
565
|
+
export function hookNoticeLastCheckText(n) {
|
|
566
|
+
return `couldn't evaluate this turn — proceeded (${n.reason}${n.detail ? `: ${n.detail}` : ''})`;
|
|
567
|
+
}
|
|
568
|
+
/** footer 通知条文案前缀(测试锁字面;宿主拼 ` (${reason})`)。 */
|
|
569
|
+
export const HOOK_NOTICE_WARN_PREFIX = 'Goal check: couldn\'t evaluate this turn — proceeded';
|