@sema-agent/client-core 0.3.0 → 0.5.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 +57 -4
- package/dist/adapt.d.ts +30 -3
- package/dist/adapt.js +490 -354
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +81 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +330 -0
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +71 -0
- package/dist/adapter/downstream/terminalToSdkResult.js +370 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +26 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.js +12 -0
- package/dist/adapter/runStream.d.ts +38 -0
- package/dist/adapter/runStream.js +177 -0
- package/dist/adapter/types.d.ts +73 -0
- package/dist/adapter/types.js +23 -0
- 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 +52 -0
- package/dist/index.js +62 -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/seam.d.ts +97 -2
- 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 +2 -2
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';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/permissionWireCaps.ts 逐字搬入(零依赖)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* src/sema/permissionWireCaps.ts — PURE CC permission-mode INTENT projection (R4, revised per the two-axis
|
|
6
|
+
* seamless principle). seamQuery imports it.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 Design (memory seamless-two-axis-wire-principle): the frontend carries the RAW permission-mode INTENT; the
|
|
9
|
+
* SERVICE interprets it (axis-aware, tighten-only). We do NOT pre-interpret `plan → enablePlanMode+handsReadOnly`
|
|
10
|
+
* on the client — that bakes policy into one frontend and would drift between the TUI shell and the web portal.
|
|
11
|
+
* Instead we stamp `permissionMode` and let the service own the interpretation (plan ⇒ present_plan + read-only;
|
|
12
|
+
* acceptEdits/bypassPermissions ⇒ loosening, coerced to default engine-side, enforced client-side). This keeps the
|
|
13
|
+
* shell a thin per-turn intent projector that's identical across TOC-local / TOB-cloud / centralized-session axes.
|
|
14
|
+
*
|
|
15
|
+
* `auto`(分类器批3,黑板 [907] server 五模式表):server ≥1.209 coercePermissionMode 认 "auto"
|
|
16
|
+
* (解释 = default 同款 fs 写 ask 门;存活 ask 由引擎侧 auto-mode 分类器先行裁决——武装 AND 门
|
|
17
|
+
* RuntimeCaps.autoMode ∧ RunnerDeps.autoMode,缺一静默回 ask,fail-closed)。壳只透传意图,分类器
|
|
18
|
+
* 规则/权益都在装配面,非 TaskSpec 字段。版本安全:老 server coerce 未知非空词 → "default"
|
|
19
|
+
* (manual ask 门)= 只紧不松。
|
|
20
|
+
*/
|
|
21
|
+
export type PermissionMode = 'default' | 'plan' | 'acceptEdits' | 'bypassPermissions' | 'auto';
|
|
22
|
+
export interface PermissionModeField {
|
|
23
|
+
permissionMode?: PermissionMode;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Project the client's permission mode → the wire intent. Carries any KNOWN non-`default` mode verbatim (the
|
|
27
|
+
* service interprets); `default`/unknown/undefined ⇒ no stamp (the deployment baseline governs). No policy baked
|
|
28
|
+
* here — relevance + tighten-only governance is the service's job.
|
|
29
|
+
*
|
|
30
|
+
* `stampDefault`([830]① server 1.191 五模式解释层):server 侧 absent-mode 不挂门(存量 headless
|
|
31
|
+
* 零回归),manual(=CC default)要挂 fs 写 ask 门必须显式 stamp `permissionMode:"default"`。只有
|
|
32
|
+
* 三选卡真能弹出来的调用方(交互 REPL,leader ToolUseConfirm 队列已注册)才传 true —— print/headless
|
|
33
|
+
* 不 stamp;老 server 对 "default" 走 basePolicy(与 absent 等价),version-safe。
|
|
34
|
+
*/
|
|
35
|
+
export declare function permissionModeField(mode: string | undefined, opts?: {
|
|
36
|
+
stampDefault?: boolean;
|
|
37
|
+
}): PermissionModeField;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/permissionWireCaps.ts 逐字搬入(零依赖)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* src/sema/permissionWireCaps.ts — PURE CC permission-mode INTENT projection (R4, revised per the two-axis
|
|
6
|
+
* seamless principle). seamQuery imports it.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 Design (memory seamless-two-axis-wire-principle): the frontend carries the RAW permission-mode INTENT; the
|
|
9
|
+
* SERVICE interprets it (axis-aware, tighten-only). We do NOT pre-interpret `plan → enablePlanMode+handsReadOnly`
|
|
10
|
+
* on the client — that bakes policy into one frontend and would drift between the TUI shell and the web portal.
|
|
11
|
+
* Instead we stamp `permissionMode` and let the service own the interpretation (plan ⇒ present_plan + read-only;
|
|
12
|
+
* acceptEdits/bypassPermissions ⇒ loosening, coerced to default engine-side, enforced client-side). This keeps the
|
|
13
|
+
* shell a thin per-turn intent projector that's identical across TOC-local / TOB-cloud / centralized-session axes.
|
|
14
|
+
*
|
|
15
|
+
* `auto`(分类器批3,黑板 [907] server 五模式表):server ≥1.209 coercePermissionMode 认 "auto"
|
|
16
|
+
* (解释 = default 同款 fs 写 ask 门;存活 ask 由引擎侧 auto-mode 分类器先行裁决——武装 AND 门
|
|
17
|
+
* RuntimeCaps.autoMode ∧ RunnerDeps.autoMode,缺一静默回 ask,fail-closed)。壳只透传意图,分类器
|
|
18
|
+
* 规则/权益都在装配面,非 TaskSpec 字段。版本安全:老 server coerce 未知非空词 → "default"
|
|
19
|
+
* (manual ask 门)= 只紧不松。
|
|
20
|
+
*/
|
|
21
|
+
const KNOWN = new Set([
|
|
22
|
+
'default',
|
|
23
|
+
'plan',
|
|
24
|
+
'acceptEdits',
|
|
25
|
+
'bypassPermissions',
|
|
26
|
+
'auto',
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* Project the client's permission mode → the wire intent. Carries any KNOWN non-`default` mode verbatim (the
|
|
30
|
+
* service interprets); `default`/unknown/undefined ⇒ no stamp (the deployment baseline governs). No policy baked
|
|
31
|
+
* here — relevance + tighten-only governance is the service's job.
|
|
32
|
+
*
|
|
33
|
+
* `stampDefault`([830]① server 1.191 五模式解释层):server 侧 absent-mode 不挂门(存量 headless
|
|
34
|
+
* 零回归),manual(=CC default)要挂 fs 写 ask 门必须显式 stamp `permissionMode:"default"`。只有
|
|
35
|
+
* 三选卡真能弹出来的调用方(交互 REPL,leader ToolUseConfirm 队列已注册)才传 true —— print/headless
|
|
36
|
+
* 不 stamp;老 server 对 "default" 走 basePolicy(与 absent 等价),version-safe。
|
|
37
|
+
*/
|
|
38
|
+
export function permissionModeField(mode, opts) {
|
|
39
|
+
if (typeof mode === 'string' && mode !== 'default' && KNOWN.has(mode)) {
|
|
40
|
+
return { permissionMode: mode };
|
|
41
|
+
}
|
|
42
|
+
if (opts?.stampDefault === true && (mode === undefined || mode === 'default')) {
|
|
43
|
+
return { permissionMode: 'default' };
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/promptProfileWireCaps.ts 逐字搬入(零依赖)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* src/sema/promptProfileWireCaps.ts — PURE projection: the shell's prompt-profile pick → the
|
|
6
|
+
* `TaskRequest.promptProfile` axis (core 1.328 [1144]: CC LT(model_id)-同构的呈现轴,
|
|
7
|
+
* "simple"=CC 对 BYOM id 发的短形面(引擎缺省)/"classic"=长形面,按任务/模型可切)。
|
|
8
|
+
*
|
|
9
|
+
* Config source: env `SEMA_PROMPT_PROFILE`(A/B 与手选口;center 下发面走引擎侧 TaskSpec 直通,
|
|
10
|
+
* 不经此位)。STRICT allowlist:仅 "simple"/"classic"(大小写不敏感,trim)stamp;其它/缺省 ⇒
|
|
11
|
+
* `undefined`(不 stamp=引擎缺省 simple,请求形状不变——additive 纪律与 retainBackgroundWireCaps
|
|
12
|
+
* 同款)。CALLER 前置 live 门(mock 形不变)。
|
|
13
|
+
*/
|
|
14
|
+
import { type EnvLike } from './hostEnv.js';
|
|
15
|
+
export declare const PROMPT_PROFILE_ENV: "SEMA_PROMPT_PROFILE";
|
|
16
|
+
export type PromptProfile = 'simple' | 'classic';
|
|
17
|
+
export declare function promptProfileFromEnv(env?: EnvLike): PromptProfile | undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/promptProfileWireCaps.ts 逐字搬入(零依赖)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* src/sema/promptProfileWireCaps.ts — PURE projection: the shell's prompt-profile pick → the
|
|
6
|
+
* `TaskRequest.promptProfile` axis (core 1.328 [1144]: CC LT(model_id)-同构的呈现轴,
|
|
7
|
+
* "simple"=CC 对 BYOM id 发的短形面(引擎缺省)/"classic"=长形面,按任务/模型可切)。
|
|
8
|
+
*
|
|
9
|
+
* Config source: env `SEMA_PROMPT_PROFILE`(A/B 与手选口;center 下发面走引擎侧 TaskSpec 直通,
|
|
10
|
+
* 不经此位)。STRICT allowlist:仅 "simple"/"classic"(大小写不敏感,trim)stamp;其它/缺省 ⇒
|
|
11
|
+
* `undefined`(不 stamp=引擎缺省 simple,请求形状不变——additive 纪律与 retainBackgroundWireCaps
|
|
12
|
+
* 同款)。CALLER 前置 live 门(mock 形不变)。
|
|
13
|
+
*/
|
|
14
|
+
import { hostEnv } from './hostEnv.js';
|
|
15
|
+
export const PROMPT_PROFILE_ENV = 'SEMA_PROMPT_PROFILE';
|
|
16
|
+
export function promptProfileFromEnv(env = hostEnv()) {
|
|
17
|
+
const raw = env[PROMPT_PROFILE_ENV];
|
|
18
|
+
const s = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
|
|
19
|
+
return s === 'simple' || s === 'classic' ? s : undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/retainBackgroundWireCaps.ts 逐字搬入(零依赖)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* src/sema/retainBackgroundWireCaps.ts — PURE projection: the shell's "retain background processes past
|
|
6
|
+
* task teardown" opt-in → the TOP-LEVEL `TaskRequest.retainBackgroundProcesses` boolean the SERVICE consumes
|
|
7
|
+
* (spec-fields.js `retainBackgroundProcessesFromBody`: `raw !== true` ⇒ dropped;requirePrincipal 多租户形
|
|
8
|
+
* 服务端另有门)。Mirrors selfOrchestrationWireCaps(the verified top-of-body wire pattern)。
|
|
9
|
+
*
|
|
10
|
+
* 背景([1046] 核谳,TB [991]② reap 族):core 直跑腿(sema-tb)出厂显式
|
|
11
|
+
* `retainBackgroundProcesses:true`(design/128 T1-1——「交付存活服务」任务的 verifier 在进程退出后
|
|
12
|
+
* 才连接),而组合腿产品曾默认关 ⇒ 两腿在 reap 族不是同一场考试。
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ 缺省已翻转为 ON(clay 亲报「后台 shell 命令跑到一半被杀」案,2026-07-26;[1046]④ 的裁量件本次
|
|
15
|
+
* 由真实场景需求兑现)。缺省不发时,引擎 turn finally 的**两道门**都关着:
|
|
16
|
+
* ① `settleKilledForOwner({retainProcesses})` 的 retain 豁免;
|
|
17
|
+
* ② `retainBgBySpec = spec.retainBackgroundProcesses === true && !ownedEnvDying` —— 为真才整块跳过
|
|
18
|
+
* `bgEnv.disposeBackgroundShells(...)`(该 blanket sweep 不经 task-registry,故被它收掉的任务
|
|
19
|
+
* `stopSource` 为空,终态归因落到 `stoppedBy = "system"` 缺省 = clay 观察到的 `stopped-by: system`)。
|
|
20
|
+
*
|
|
21
|
+
* 三问(宪法 NO-SHIP 检查):
|
|
22
|
+
* 谁需要:真人用户 —— `run_in_background` 起的构建/测试/服务在当轮结束后必须还活着,否则
|
|
23
|
+
* BashOutput/TaskOutput 续读到的是尸体(clay 案:输出文件 0 字节 + status killed)。
|
|
24
|
+
* 谁受害:隔离执行环境(e2b/k8s/ssh 等 `isIsolated(env)`)—— 那里 `ownedEnvDying` 为真,retain 被
|
|
25
|
+
* **无条件覆盖**(core 侧 `!opts.envDying` / `&& !ownedEnvDying`),进程随宿主一起走。这是
|
|
26
|
+
* 正确的:进程宿主没了,留 handle 无意义。故本翻转对隔离腿零语义变化。
|
|
27
|
+
* 次级受害面 = 本机资源泄漏(后台进程活过当轮)—— 但这正是 CC 的既定契约,不是回归。
|
|
28
|
+
* 补偿机制:`SEMA_RETAIN_BACKGROUND=0|false|no|off` 逃生口 ⇒ 不 stamp ⇒ 回引擎缺省 reap;
|
|
29
|
+
* 进程始终可由 KillShell/TaskStop 显式收,`/bashes` 面板可见可停;引擎 hard-wall
|
|
30
|
+
* (defaultBgTimeoutSec 30min / maxBgTimeoutSec 2h)仍是兜底上限。
|
|
31
|
+
*
|
|
32
|
+
* CC 逐字锚(对标纲领 = 100% 达 CC 效果):CC 的 Bash `run_in_background` 契约 = 进程活过当轮,
|
|
33
|
+
* 由 BashOutput 续读、KillShell 显式收 —— **不在 turn 边界回收**。缺省 reap 是对 CC 的偏离,本次钉回。
|
|
34
|
+
*
|
|
35
|
+
* 语义:缺省 ON —— unset / 未识别值 ⇒ `true`(stamp);显式 truthy(1/true/yes/on)⇒ `true`;
|
|
36
|
+
* 只有显式 falsy(0/false/no/off)⇒ `undefined`(不 stamp ⇒ 引擎缺省 reap)。
|
|
37
|
+
* 服务端 `retainBackgroundProcessesFromBody` 是 `raw !== true ⇒ 丢弃` 的严格刻度,所以 opt-out 一律
|
|
38
|
+
* 表达为**不 stamp**,绝不发 `false`(形状诚实)。
|
|
39
|
+
* PURE — callers pass `env`;caller gates to LIVE mode(mock request shape 不变)。
|
|
40
|
+
*/
|
|
41
|
+
import { type EnvLike } from './hostEnv.js';
|
|
42
|
+
/** The env key the shell reads(SEMA_ 命名空间,与引擎自己的 spec∨env 通道正交——这里是壳→
|
|
43
|
+
* TaskRequest 的 per-request 意图面,非引擎部署面)。 */
|
|
44
|
+
export declare const RETAIN_BACKGROUND_ENV: "SEMA_RETAIN_BACKGROUND";
|
|
45
|
+
/**
|
|
46
|
+
* ENV source → `true`(缺省 ON = CC `run_in_background` 契约:后台进程活过当轮),
|
|
47
|
+
* 只有显式 falsy 逃生口(`SEMA_RETAIN_BACKGROUND=0|false|no|off`)⇒ `undefined`(不 stamp ⇒ 引擎缺省 reap)。
|
|
48
|
+
* 返回型刻意是 `true | undefined` —— 服务端 `raw !== true ⇒ 丢弃`,opt-out 只能表达为「不 stamp」。
|
|
49
|
+
*/
|
|
50
|
+
export declare function retainBackgroundFromEnv(env?: EnvLike): true | undefined;
|