@sema-agent/client-core 0.72.11 → 0.72.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +3 -1
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +3 -2
- package/dist/adapter/downstream/terminalToSdkResult.js +20 -3
- package/dist/adapter/runStream.d.ts +8 -0
- package/dist/adapter/runStream.js +45 -13
- package/dist/agentSession/backgroundView.js +7 -2
- package/dist/detachWire.d.ts +13 -0
- package/dist/detachWire.js +29 -12
- package/dist/engineAgentPanelStore.d.ts +15 -0
- package/dist/engineAgentPanelStore.js +52 -0
- package/dist/executionLaneCapability.d.ts +69 -0
- package/dist/executionLaneCapability.js +125 -0
- package/dist/fleet/fleetProjection.js +8 -1
- package/dist/fleetAgentPanelProjection.d.ts +1 -1
- package/dist/fleetAgentPanelProjection.js +31 -14
- package/dist/hitl/hitlBridge.d.ts +12 -1
- package/dist/hitl/hitlBridge.js +20 -0
- package/dist/hitl/parkResolver.d.ts +1 -1
- package/dist/hitl/parkResolver.js +2 -0
- package/dist/hitl/planReviewWire.d.ts +50 -3
- package/dist/hitl/planReviewWire.js +167 -23
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/notifications.d.ts +2 -2
- package/dist/notifications.js +46 -23
- package/dist/runTerminal.d.ts +9 -0
- package/dist/runTerminal.js +11 -0
- package/dist/toolResult.js +14 -5
- package/dist/workflow.js +3 -1
- package/dist/workflowClient.js +34 -6
- package/dist/workflowMonitor.d.ts +3 -2
- package/docs/INTEGRATION-CLIENTS.md +90 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,49 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.72.13(2026-09-18)
|
|
53
|
+
|
|
54
|
+
> 主题:**server 7.86.0 提货**(发车帖 [7549] @client-core 两点,表态 [7553] ⓪;CC-46 `plan_review` 决断体 `permissionModeAfter` 发口 —— cli L-385 / S-433;CC-54 `capabilities.executionLane` 四态读面 —— S-426)+ **CC-53** `engineSupportsDetach` 三态化(审计 #60 A-25,「异常吞成健康值」)。**patch**:型面纯 additive(+4 常量 +14 函数,公面 993 → 1011;测试钩 45 → 47;`HitlSafetyError.code` 闭集 +1 `bad_plan_mode`;`PlanReviewOutcome` / `armPlanReviewApproval` / `decidePlanReview` 各加一可选位;零 wire 新键 —— 键由 server 7.86.0 铸;peer 不动)。接入面 §60。
|
|
55
|
+
|
|
56
|
+
### Changed(0.72.12 勘误级改口,test 0.72.12 验收抓出)
|
|
57
|
+
|
|
58
|
+
- 🔴 **fleet 行缺席上报撤出主通道,改走独立订阅口**:0.72.12 把它做成 `publishEngineAgentPanelEvent` 主通道的第五种 `kind:'absent'`,并写「老消费者静默忽略」—— **那句话是错的**:壳面板消费的第四臂是**裸 else**(sweep 体),新 kind 会被当成 sweep,把 owned 且非常驻的无辜 running 行 settle 成 completed(test 在真产物上复现;尚无端提货 0.72.12 的 absent)。0.72.13:`EngineAgentPanelEvent` 联合里的 `absent` 臂**撤回**,主通道 kind 集回到 0.72.11 的四种;缺席走 **`subscribeEngineAgentPanelAbsence(fn)`** / `publishEngineAgentPanelAbsence`(载荷 `EngineAgentPanelAbsence {{ taskId, lastSeenAtMs, absentForMs }}`,**无 `kind` 位**;未订阅时按 taskId 缓冲最新一条、上界 200、订阅即回放)—— 老消费者**结构上**收不到 = 真零影响。去重单位 = **每个缺席周期一条**(行回来后再消失是新周期,会再报);与迟到 `end` 的先后:缺席上报在先(行离场 > 30 s),真终态帧后到时主通道照发那唯一一次 `end`,订阅者先撤缺席标再 settle。🔴 **回收那一格显式回答**:此前缺席 settle 顺手让壳给「没有 runner 的行」打 `notified` / `evictAfter`;撤掉之后**未订阅者**的行既不 settle 也不回收,停在最后已知态直到真终态帧(0.72.12 取舍 (2) 的代价,含不回收 —— 不拿「宣称它完成了」换回收);**订阅者的契约义务** = 行渲 unknown + 停表 **且置可回收标**,状态值域不得写 completed / failed。病形入 `docs/DISEASE-SHAPES.md` **DS-07**。
|
|
59
|
+
- **`enqueueBgChildNotification` 只对通知终态词 settle 面板行**(test 0.72.12 验收 ①;DS-06 同形存量):此前对任何 status 都铸 `end`,而 `isError: isTerminalNotSuccess(status)` 对 `running` / `queued` / `event` / `unknown` / 未来词答 false ⇒ 面板把一条还在跑的行结成 completed(该谓词 JSDoc 明禁拿 false 当成功)。改为只对 `TASK_NOTIFICATION_TERMINAL_STATUSES` 四词铸 `end`,非终态不 settle、常驻标不摘。pure ⑩ S13。
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- **`permissionModeAfter` 发口(CC-46;engine ≥7.86.0 S-433,契约 §4c)**:批准一个 `plan_review` 之后这条任务继续用哪一档 permission mode —— `acceptEdits`(工作目录内的写不再逐次征询;CC `ExitPlanMode` 卡的「Yes, and auto-accept edits」)/ `default`(每次写照常征询;**缺席 = 这一档**)。三处加同一可选位:`HitlBridge.reviewPlan({ decision, permissionModeAfter? })`、`decidePlanReview(taskId, decision, permissionModeAfter?)`、`PlanReviewOutcome.permissionModeAfter?`。🔴 **缺席 = 键不落**(体逐字节同旧 `{decision}`,不铸 `default`);**只配 `approve`、闭集两词**(`PLAN_REVIEW_MODE_AFTER_WORDS` / `isPlanReviewModeAfter`)—— 坏形**本地拒**一个字节不出门(`HitlSafetyError` 新码 **`bad_plan_mode`**,`GATE_FAILURE_CODES` 白名单同批 5 → 6;`decidePlanReview` 走既有 outcome 管道说「could NOT be sent」,拒文不回显值)。🔴 **适用面读不到 wire**(契约逐字:两种任务产出同一张卡,只有提交方知道它是不是 `permissionMode:"plan"` 只读起步):**三选卡只在双闸都过时才出** —— `armPlanReviewApproval(result, sessionKey?, { submittedInPlanMode: true })`(宿主声明)∧ 本进程 caps 缓存里的引擎版本 ≥7.86.0(`versionSupportsPlanReviewModeAfter` 三态,读不出 = `unknown` **不折 supported**;更老的 server 静默丢这个键 = 卡上承诺了却不兑现);否则老两选卡逐字同旧、决断体不带键。卡选项单源 `planReviewCardOptions(offer)`,标签 `PLAN_REVIEW_APPROVE_AUTO_LABEL` / `PLAN_REVIEW_APPROVE_MANUAL_LABEL`,判读 `planReviewChoiceFromAnswer`(旧 `planReviewDecisionFromAnswer` 不变,两个 Yes 都读 approve);重放重呈读同一份 per-task 记号保证同一张卡前后选项一致;重开腿恒老两选。🔴 引擎答 400 `request.field_conflict`(这条任务不是只读起步):`default` ≡ 缺席 ⇒ **去键重发恰一次**(零语义损失;契约:被这道门拒掉的请求一个字节的状态都不动);`acceptEdits` **不**重发(不静默降成逐次征询 = 不替用户改决定),outcome 说批准没生效、计划仍在等、改选逐次征询可过。⚠️ 直连门(worker 签名)部署:送这一位必须把它签进 HMAC 信封尾位(契约 §6.2;签名器在端 / BFF,不在本包)。
|
|
64
|
+
- **`src/executionLaneCapability.ts`(CC-54;engine ≥7.86.0 S-426,契约附录 B.1)**:`capabilities.executionLane { provider, toolsOnThisHost }` 的四态读面,与 sql / writeProtection / webSearch.backend 三只兄弟同构 —— `projectExecutionLaneCapability` / `noteEngineCapsForExecutionLane`(tee,永不抛,陈旧代际整只丢)/ `observedExecutionLane` / `forgetExecutionLaneReading` / `executionLaneDoctorDetail`(措辞单源)。🔴 **键缺席 = `not_reported`(老 server 判不了),绝不读作「不是本机车道」**(那会让老 server 上的 host 部署一律欠报技能 `baseDir`);蕴含单向(`false` ⇒ `baseDir` 恒不发;`true` 只是必要条件);`provider` 非空串**开集**(不手抄 `RemoteExecProvider` 闭集当判据),上屏前消毒封长;`toolsOnThisHost` 严格布尔;`null` / 数组 / 缺座 ⇒ 畸形删格;多余成员(内情)不过境。**「工具是否跑在本机」判据归包**:`toolsRunHereFromExecutionLane(reading, legacyInference)` —— 读到位用位,读不到**原样**用调用方旁证(此前三端各拼「本壳 spawn ∧ 无远端 URL ∧ REMOTE_EXEC ∈ {空, host}」三合取,`-p` / mock 车道发不出 `baseDir`)。新门 `run-execution-lane-capability-test.mjs` 27 格(红先绿后)。
|
|
65
|
+
- **`probeEngineDetachSupport` 三态 + `detachSupportDisclosure`(CC-53;#60 A-25)**:`engineSupportsDetach` 此前对「/health 非 2xx / 坏 JSON / 不可达」与「确认支持」同返 `true`。新读口三态 `supported` / `unsupported` / `unknown`;布尔口改成它的薄封装(只有确证陈旧才 false —— **fail-soft 行为零变化**,`unknown` 照发 detach 头),变的是调用方现在分得清并能披露(`detachSupportDisclosure('unknown')` 单源句;另两态 `undefined`)。普查基线销账 5 → 4 条。🔴 回体必须是对象:`null` / 数组 / 标量 / `version` 非串 / 版本串解析不了 ⇒ `unknown`(不拿读不懂的值当「陈旧」证据);只有「对象且无 version 键」与「可解析且低于地板」才 `unsupported`。
|
|
66
|
+
- **发车前异源复审第一轮三条真修**:① [high] 三选卡判读此前只取 `selected[0]` —— 矛盾多选 `[auto-accept, reject]` 会被执行为 approve + acceptEdits(放宽写征询)⇒ **恰一题、恰一项**才算作答,且标签必须属于**本次实际展示**的那张卡(两选卡收到 auto 标签 = 答的不是这张卡),其余一律 dismissed;② [medium] 版本闸前缀匹配把 `7.86.0-rc.1` / `07.86.0` / `7.86.0garbage` 当支持证据 ⇒ **整串**匹配正式版 x.y.z,其余 unknown;③ [medium] detach 畸形 health 回体折 unsupported(见上)。
|
|
67
|
+
- **发车前异源复审第二轮四条真修**(时序面):① [high] 非终态通知此前先占了完成去重账,同一任务随后的真终态被跨通道去重吞掉 ⇒ 零 `end`、常驻标永不摘 —— 完成三本账与面板 settle 一律只认通知终态词,非终态只过逐条去重(`running → completed` / `unknown → failed` 序列入门);② [high] `planReviewChoiceFromAnswer` / `planReviewDecisionFromAnswer` **不给展示标签 = 只认老两选**(此前缺省放行三选卡的两个 Yes ⇒ 恒两选的重开卡会把一个没展示过的标签投递成 approve);三选卡的调用方显式传 `planReviewCardOptions(true).map(o => o.label)`;③ [medium] 缺席缓冲随同 taskId 的主通道事实(tick / fleet-row / end)失效 —— 迟订阅不再回放一条已被「行回来 / 真终态」作废的缺席;④ [medium] 沿用原 responder 的重呈(`mintFreshQuestionId:false`)题面沿用原卡选项(此前三选 arm 后重绘成两选,屏上的 Yes 不在 responder 读域里,点了不投递)。三选卡端到端序列(arm 双闸 → 重呈 → 作答 → 请求体)入门。
|
|
68
|
+
|
|
69
|
+
## 0.72.12(2026-09-18)
|
|
70
|
+
|
|
71
|
+
> 主题:**「缺席被折成正向终态」病形按类清剿**(test 证据帖 [7541] + sema-internal #59 M1 / #64 M6,owner:client-core;表态 [7543];CC-50 P1 + CC-51)。头条 = 本包 `fleetAgentPanelProjection` 缺席兜底把「引擎不再上报这一行」铸成 `end{isError:false}` ⇒ cli 屏上 `✔ Completed`(两条现场腿秒级实测)。**patch**:型面 additive(`EngineAgentPanelEvent` 新臂 `absent`、`WorkflowRunState.status` / `WorkflowPhase.status` 各加 `'unknown'`、结果帧新超集键 `_sema_outcome`、+2 常量 +2 函数,公面 989 → 993;peer 不动);**行为面有三处刻意的真变化**,逐条在「取舍」段。接入面 §59。
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- **头条(CC-50 M1):fleet 行缺席不再 settle、不再铸 `end{isError:false}`** —— `projectFleetAgentRows` 的缺席路径改发第三态事件 **`{ kind:'absent', taskId, lastSeenAtMs, absentForMs }`**(A 形,[7543] ②;`EngineAgentPanelEvent` additive 新臂),行保持未结局;真终态帧迟到照发那**唯一一次** `end`(缺席不再占用去重记号 —— server [7545]:子行随父 run 离场按 `orphaned` 移除,真终局由后续对账周期补发,结构性晚于本包 30 s 窗);行回来照发 `fleet-row`(即使值未变,消费端据此撤 absent 标);已上报 absent 且不再在场的条目按上报时刻 + 回收期回收,回收后迟到终态仍恰一次 `end`。`ABSENT_SETTLE_MS` 名字沿用(公面导出),语义改为「缺席上报窗」。消费端义务:认得 `absent` ⇒ 渲「engine no longer reports this agent · outcome unknown」并停表,**不许**翻译成 completed / failed / stopped;认不得(老壳,三臂无 else)⇒ 行停在最后已知态 —— 可被证伪的「不知道」,不是 Completed。🔴 边界:`agent.status` / `isError` 的值域**一字不改**(壳详情页头的 catch-all else 会把任何第三个词渲成 Stopped,test 亲读)。panel store 把 `absent` 按进度类缓冲(同行合并、满了先让位)。pure 门 ③ 段改钉 + 9 格、honesty F13 反转(迟到终态**必须**发那一次 end)。
|
|
76
|
+
- **六站一次改齐,规则只有一条:「缺席 / 解不出 / 非串 / 认不出的词 ⇒ `unknown` 位,绝不落 completed / success / running」**(#59 A-05 / A-06 / A-07 / A-08 / A-12 / A-17;#64 A-28):① `TaskOutput` structured 腿 `retrieval_status:'success'` 但无 status 且模型面解不出 ⇒ `task.status:'unknown'`(此前 success ⇒ completed 两级连折;`not_ready` / `timeout` 仍 running —— 那是引擎的正向证据);② legacy 腿模型面解不出 status ⇒ `unknown`;③ `parseWorkflowPollEnvelope` 缺 status ⇒ `unknown`(此前 completed,且 `completed ∈ WF_TERMINAL` 顺手 `markEngineWorkflowNotified` 把补发通道缴械 —— 现在零调用);④ `normalizeTaskNotification` / `taskNotificationDedupKeyFromWire` 缺 status ⇒ `unknown`,summary 改说 `Background task "<id>" reported without a status (outcome unknown)`,去重键 `:unknown` 不与真 completed 同桶;⑤ `createBackgroundView` 200 回体缺 `tasks` 键 ⇒ 源健康位 `unavailable`(此前 ok + 空表);⑥ `projectWorkflowRun` run / phase 状态 `default` 臂 ⇒ `'unknown'`(此前 `'running'`;`WorkflowRunState.status` / `WorkflowPhase.status` 联合各 +1 additive),`coerceWorkflowStatus` 未知词 ⇒ `idle`(FleetTaskStatus 是 wire 闭集无 unknown 位,与同文件 `coerceTaskStatus` 同律);pure 门 ⑩ 段 13 格 + 三处旧判据改钉(它们此前**钉住的正是病形**)。
|
|
77
|
+
- **结果边界(#59 A-12,「取舍」段第 1 条)**:`terminalToSdkResult` 对 **终态记录整个缺席**(`readRunTerminal` 返 `null`)不再落 `success` 臂 —— 改 `error_during_execution` + `is_error:true` + additive **`_sema_outcome:'unknown'`** + `errors[0]` 逐字「run ended with no terminal record — outcome unknown, not a failure verdict (…treat the result as unverified)」;认不出的终态词那臂同带该旗(句子仍报出那个词)。🔴 **稀疏**:真 completed / 真 failed 帧不带此键(G2-e)。**包内消费者同 commit 落下**(test [7541] G2-d,「铸了就要有人读」):`runStream` 终局臂读结果帧上的旗,合成一行 **`Outcome unknown: <同一句>`**(第三条中性终局行身份;`OUTCOME_UNKNOWN_ROW_PREFIX` / `isOutcomeUnknownRowText` 导出,只认行首;行类旗 `isApiErrorMessage` 置位同 `Model output error:` 行),壳零接线即用户可见面有差异;合成终态行收成单铸点 `syntheticTerminalRow`(铸链数不增)。type-superset +1 = 55;identity-copy 门 G1d 七格。
|
|
78
|
+
- **反钉 = 以点扫面的门,不是六个站点门**:新门 **`run-absence-fold-census-test.mjs`**。病形锚在**兜底位置的值**(函数尾无条件 return / 所有已知臂不匹配的 default / 没有词可传时铸的字面 / 异常路径的返回值):**兜底位置只许 `unknown` 或诚实缺席,不许任何正向词**;语法形只是检出手段。检出走 **AST**(不看注释与字符串,多行写法不漏)五形:M1a `??` / `||` 右臂正向终态词 · M1b 三元 **else 臂**正向终态词(任意条件)· M6 显式 `default:` 首 return 正向词 · M2 `catch` 块首 return `true / [] / {}` 与 `.catch(() => 健康值)` 箭头形 · M0 函数尾无条件 return 正向词且函数内另有 return。存量登记 `scripts/absence-fold-baseline.json` 今日 **5 条**各带 why(`detachWire` fail-soft 版本闸候 CC-53 三态 / `coerceAgentState` default ⇒ progress:`AgentState` = CC atlas WPe 输入词表 EXACTLY —— **声明的限制** / `backgroundView` `… : 'ok'` 有「刚成功拉到列表」的正向证据 / `scratchpadWireCaps` catch ⇒ `{}` = 不发可选键的诚实缺席 / `toolResult` `retrieval_status` else 臂 `success`:CC 闭集无第三格且有「调用已返回」的正向证据),未登记命中红且点名 file:line,登记数 = 实际数(虚高也红);判别力自证走**围栏**(任一形负控不过 ⇒ 门拒绝出数):五形逐形注入内存副本各多报恰一处 + 两个正确写法回归钉(`{ kind:'completed' }` / `case 'x': return 'done'`)+ 注释与字符串不算;头条反钉(AST:零 `isError: false` 属性赋值、`kind:'absent'` 恰一处)。门 AST 化后**自扫出两处同类**一并修:`projectWorkflowRun` 合成阶段 `synthPhaseStatus` 尾三元与 `bucketStatus` 空桶仍折 `running`(无活跃证据 ⇒ `unknown`;腿上 park 仍压过 run 级 failed)、TaskOutput 腿的 `retrieval_status` 是 **CC 同名键**(闭集 success / timeout / not_ready;老壳卡片 success 才展示正文、not_ready 渲「still running」):闭集里没有「不知道」,绝不塞第四个词 —— 认不出 / 缺席 ⇒ `success`(正向证据 = 这份 structured detail 是一次已返回的取回调用的产物),「结局不知道」由 `task.status:'unknown'` 承载(登记在普查基线带 why);`task.status` 只在引擎**亲口**说 not_ready / timeout 时回落 running。负控套 +1(19/19)。`docs/DISEASE-SHAPES.md` **DS-06** 入册。
|
|
79
|
+
|
|
80
|
+
### 取舍(三处真行为变化,不是「纠正疏忽」;test [7541] 直讯三轮互收执)
|
|
81
|
+
|
|
82
|
+
1. **SDK 结果边界 fail-closed**:CC `subtype` 闭集没有第三格,边界上必须给一个终局。误报错的代价 = 用户多跑一次;误报成功的代价 = 用户信一份坏产物 ⇒ 取前者,并用 `_sema_outcome:'unknown'` + 逐字句子把「这不是失败判决」说出来。此前「零行为变化」维持 success 的那一路(mock 车道 / 不带终态词的旧合成帧)**从 success 翻成 error**;要成功请在帧上带真终态词(`status:'completed'` 或 `terminal:{kind:'completed'}`)。
|
|
83
|
+
2. **缺席不再 settle**:老壳上离场的行不会自己消失(回收由本包台账侧照旧,只是不发 `end`),行停在最后已知态直到真终态帧或壳认得 `absent`。
|
|
84
|
+
3. **通知 / 去重键 / TaskOutput 状态位的第三个词 `unknown`**:渲染面按开集读的端零变化;按闭集 switch 的端会走 default —— 请勿把 default 渲成正向词(那正是 #64)。
|
|
85
|
+
|
|
86
|
+
- **异源对抗复审四轮,下游兼容六条全部真修**:① `retrieval_status`(见上)两个中间版本都被打回 —— 原词透传 / 铸 `unknown` 会让老壳把已完成的 final answer 渲成 Task not ready;按 structured 两键推 `not_ready` 漏了模型面文本回落取得的终态与正文,且 `not_ready` 在老壳上 = 假「仍在运行」(pure ⑩ S9 六格含该复现形);② `taskNotificationDeliverySupplement` 尾模板对任何词都说 `already finished`,与同批新铸的 `unknown` 合成出假终局 ⇒ 只对**通知自己的终态词**(新单源 `TASK_NOTIFICATION_TERMINAL_STATUSES` / `isTaskNotificationTerminalStatus`,住在词表属主 `runTerminal.ts`:core `TaskNotificationStatus` 四终态 completed / failed / killed / cancelled;`event` 非终态;不借 run 级词表 —— 那张没有 cancelled)说 finished,其余说「state this client cannot confirm … outcome is unknown here」(S10 三格);③ run 状态词认不出 / 缺席但腿上有活跃证据 ⇒ run 级按证据说 `running`(老壳用 `status === 'running'` 算 workflowActive),零证据才 `unknown`,不拿「腿全做完」反推 run 完成(S11);④ 合成阶段保留腿上的 error 证据:同一组腿有无 phase 标签,阶段状态一致(error + progress ⇒ 阶段 failed、run 仍 running;S12)。
|
|
87
|
+
|
|
88
|
+
### Known limits(本版新增)
|
|
89
|
+
- run 状态词认不出且腿上**零活跃证据**时 run 级为 `unknown`:尚未认 `unknown` 的壳按 `status === 'running'` 算 workflowActive ⇒ 该 run 下 inactive 的腿会显示为 Stopped(此前显示 running,两者都不是事实);壳半场按 §59 S-4 接 `unknown` 后消失。
|
|
90
|
+
- 普查门 AST 五形**不覆盖**:`if / else if` 链最后一个 `else` 里的 return、带插值的模板字面量(本版 `taskNotificationDeliverySupplement` 即此形,已人工修)、解构 / 参数默认值;加检出形不改规则(DS-06)。
|
|
91
|
+
|
|
92
|
+
- `coerceAgentState`(workflow agent 行)default 仍 ⇒ `progress`:`AgentState` 是 CC atlas WPe **输入词表 EXACTLY**,没有 unknown 位可落;run / phase 两级已落 `unknown`。登记在 `absence-fold-baseline.json` 带 why,候 CC 词表或另铸 additive 位。
|
|
93
|
+
- `fleetAgentPanelProjection` 的 `row.status ?? 'running'` 是**内部**终态判用的占位(fleet-row 事件不带 status),不上屏;`row.tokens ?? 0` 属 M4(#62 A-18)归 CC-52(0.72.13)。
|
|
94
|
+
|
|
52
95
|
## 0.72.11(2026-09-18)
|
|
53
96
|
|
|
54
97
|
> 主题:**core 7.23.0 提货**(发车帖 [7537] / 提货单 [7538] @client-core 段:镜像一件 + 加法三件 + 零动作两件;CC-49)。**patch**:型面零变化(通告闭集 +1,零新导出;公面 989 不变;peer 不动;devDep core `~7.23.0`)。接入面 §58。
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
35
35
|
|
|
36
36
|
## Scope
|
|
37
37
|
|
|
38
|
-
**Version:** 0.72.
|
|
38
|
+
**Version:** 0.72.13
|
|
39
39
|
|
|
40
40
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
41
41
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -311,6 +311,8 @@ public-surface guard checks that last one).
|
|
|
311
311
|
| `scripts/run-export-liveness-test.mjs` | Every runtime export in the public baseline must be **alive**: referenced by some gate, or explicitly registered in `scripts/export-liveness.json` as `contract` (consumed by a client with no gate yet), `internal` (an internal helper amplified onto the public surface by `export *`), `candidate` (with ticket + retire-by) or `retire` (dead; retire-by version). Registration is accounting, not exemption: a row for a name a gate already references is stale and must go, a row for a name no longer exported is red, `retire`/`candidate` rows go red the moment `package.json` reaches their retire-by version, and the row count only ratchets down. When the sibling client trees are on disk the consumption evidence is checked by name — a `contract` row's claimed consumers must equal the real set, and a `retire` name must not be imported by any client. Names that have already left the surface are kept in a per-version `removed` ledger: they must never reappear in the baseline or the registry, and the ledger's versions must not run ahead of the changelog. |
|
|
312
312
|
| `scripts/run-tool-disclosure-progress-projection-test.mjs` | The two wire arms sdk 9.6.0 adds — `tool_disclosure` (name-only tool census: open-set `policy`, `thresholdPercent` absent ≠ default, `deferred`/`activated` full snapshots) and `tool_progress` (one frame, two beats: Bash ticks carry an output tail with `totalLines`/`totalBytes` that come and go together; other tools carry only `elapsedSeconds`) — project to neutral internal arms plus chrome arms. Required keys missing ⇒ `malformed`; bad optional keys drop only themselves; the sub-flow three-key gate keeps child frames off the leader lane; both arms are `required: false` in the arm table with duties stated (the output tail is untrusted raw and must never be fed back to the model). |
|
|
313
313
|
| `scripts/run-mcp-panel-projection-test.mjs` | The `GET /v1/sessions/:id/mcp` panel reader (`projectMcpPanel`; server >=7.77.0 adds the optional `lastLegMcp` key) and the single wording mint for its "last leg" line. Absence of `lastLegMcp` is one literal sentence that never blames the engine version (a new session, a leg outside the retention window, a leg without a manifest and an older engine all look the same on the wire); a key that is present but unreadable is a different sentence plus a `lastLegMcpUnreadable: true` mark, never folded into absence. The `mcp[]` roster goes through the same reader as the live `wiring_manifest` third section, so a replayed roster and a live one have one shape. The two faces of the panel (`servers[]` and the last-leg roster) may legitimately differ, so the view carries no agreement flag and none of the five sentences mentions `servers`. Required keys are pinned to the SDK `openapi.yaml` component bytes **0.69.0:** `fetchMcpPanel` fetches the panel through the SDK client's own `sessions.mcp` call (same transport and auth as every other read) and projects it; transport failure, an unreadable body and an empty session id all come back as `undefined`, never as a fabricated empty panel 0.71.0 adds section K: `mcpEngineLegPresence(view)` — the engine-side MCP presence tri-state read only off the panel view (`unknown` when the view could not be read, never rendered as "no MCP configured") |
|
|
314
|
+
| `scripts/run-absence-fold-census-test.mjs` | A package-wide census of the "absence folded into a positive outcome" defect shape, so that fixing the six sites this release does not merely move the shape somewhere else. The defect is defined by position, not syntax: a fallback position (the unconditional tail return, the `default:` arm, the literal minted when there is nothing to pass on, the value returned from an error path) may only say `unknown` or stay absent, never a positive word. Detection walks the syntax tree of every source file, so comments, strings and multi-line spellings cannot hide or fake a hit, and covers five forms: the right arm of `??` / `||`, the else arm of a ternary, the first return of an explicit `default:`, a `catch` block or `.catch(() => …)` arrow returning a healthy value, and a function whose last statement returns a positive word after other returns. Every remaining hit must be registered with a written reason, an unregistered hit fails the gate naming the file and line, the registered count must equal the real count so a cleared site cannot leave a spare allowance behind, and the gate proves its own teeth behind a fence (a failed self-proof refuses to report any count): each form injected into an in-memory copy must add exactly one hit, two correct spellings are pinned as non-hits, and samples inside comments or strings do not count. It also pins the headline site: the fleet panel projection no longer mints an `end` with `isError: false` on absence |
|
|
315
|
+
| `scripts/run-execution-lane-capability-test.mjs` | The deployment execution-lane self-description (`capabilities.executionLane`, engine ≥7.86.0), read the same four-state way as its three sibling capability readers. An absent key is reported as "not reported" and never as "tools do not run on this host": on an older engine the client keeps inferring the lane the way it did before, because reading absence as `false` would silently stop every host deployment from sending skill `baseDir`. The implication is one-way (`toolsOnThisHost: false` means `baseDir` is never sent; `true` is only a necessary condition), `provider` is read as an open non-empty string rather than a hand-copied closed set and is sanitised and bounded before display, `toolsOnThisHost` must be a strict boolean, extra members never cross, the tee never throws, stale generations are dropped whole, and the single "do tools run here" predicate uses the bit when it is present and the caller's own inference, unchanged, when it is not |
|
|
314
316
|
| `scripts/run-leader-conflict-test.mjs` | The leader-run terminal `needs_human` + `result.conflict` (engine ≥7.83.0), read once for all three shells. A `result` without a `conflict` key is reported as `none` and worded as "no conflict details" rather than "no conflict": on the wire it is indistinguishable from an older engine that never reports one, and the reader does not pick a side. A `conflict` that is present but unreadable is a third word, never folded into `none`, because a tree that carries conflict markers must not be rendered as clean. `filesTruncated` is honoured only as `true` (absent means the list is complete; any other value makes the section unreadable), `workers[].applied` is passed through as the boolean fact it is, `salvaged[].patch` is handed to the save path byte-for-byte (an artifact, not screen text; a malformed row is dropped alone, and a salvage list that is present but cannot be fully read is flagged rather than rendered as "no patches"), and `rejHead` — the one on-screen diagnostic — is escaped and bounded before display. The record never throws on hostile input, extra members do not cross, arrays are fresh copies, and the single sentence minted here names the base, the files, which branches landed and which did not, and how many patches can be saved, without ever suggesting a retry: `needs_human` is a run waiting for a person, and the no-details sentence says plainly that it is not proof of a merge |
|
|
315
317
|
| `scripts/run-mcp-reconnect-test.mjs` | The in-session MCP re-dial verb (`POST /v1/sessions/:id/mcp/reconnect`, engine ≥7.85.0), consumed. The single discriminant is `outcome` and all three answers are HTTP 200, so the reader branches on the word and never on the status; the `unsupported` answer carries exactly five keys and the reader refuses to invent a zero or an empty list for the four fields the engine did not produce, while `accepted` / `refused` treat those four as required and go malformed when one is missing. The tool roster follows the **presence** of `toolNames` (absent = untouched, empty = withdrawn), the connection record passes `errorCode` through as an open set, and every remote-authored string is sanitised and bounded before display. Failures are classified by `errorCode` alone, a missing code is reported as unknown rather than guessed, the verb never throws, and the request-side guard (non-empty name, ≤190 chars) stops a call that the contract would reject anyway. The capability bit reads absent as "cannot tell" rather than "unavailable", and the one sentence the contract insists every UI carries — that re-dialing is a transaction, not a refresh — is minted here once |
|
|
316
318
|
| `scripts/run-core-value-ports-test.mjs` | The port-injection seam for ten **engine value-level** facilities (autonomous-loop prompt assembly, permission-rule loosening, tool-policy composition, protocol/retired-name/grammar lookups, rule compilation, the discussion workflow name). This package cannot re-export them (the engine barrel drags Node built-ins into the browser bundle), so it declares the ports and honest-absence readers; a Node host installs the engine's own functions verbatim. The guard pins: every reader returns `undefined` when nothing is installed (never a fabricated empty array or default policy), arguments and results pass through by reference, engine errors propagate unchanged, partial installs read partially, restore functions unwind to the previous bag, and the module source has zero engine imports |
|
|
@@ -59,8 +59,9 @@
|
|
|
59
59
|
* unknown — → 'error_during_execution' true(防御:
|
|
60
60
|
* 非 completed 的终局一律≠空成功;errors[] 里如实报出那个词的字面。**因由座在场
|
|
61
61
|
* 却整个读不出**(坏形/混合载体)也落这一臂,措辞改说「读不出」而不是编一个词)
|
|
62
|
-
* 〈整个缺席〉 — → '
|
|
63
|
-
* (mock 车道 / 不带终态词的旧合成帧;读器返 null
|
|
62
|
+
* 〈整个缺席〉 — → 'error_during_execution' true(0.72.12 CC-50:
|
|
63
|
+
* (mock 车道 / 不带终态词的旧合成帧;读器返 null) fail-closed 取舍 + `_sema_outcome:'unknown'`;
|
|
64
|
+
* 此前落 'success' = 把「没有终态记录」编成成功;unknown 臂同带该标记)
|
|
64
65
|
*
|
|
65
66
|
* errorCode 透传:引擎的码骑 additive seam 字段 `errorCode`(CC schema 无此字段——与 success 臂的
|
|
66
67
|
* `model` 同款超集姿势),集成面可精确分流(limits.max_tokens_exceeded vs cancelled …)。
|
|
@@ -739,6 +739,8 @@ function errorResult(ctx, parts) {
|
|
|
739
739
|
// ADAPTER-F4:具名 additive 位(见 `ErrorResultParts.salvagedResult`)。展开的是**一个已知键**,
|
|
740
740
|
// 覆写不到上面任何一个不变量;要加第二个位必须动这里,而动这里在 diff 里显形。
|
|
741
741
|
...(parts.salvagedResult !== undefined ? { result: parts.salvagedResult } : {}),
|
|
742
|
+
// CC-50:结局不知道的稀疏标记(见 `ErrorResultParts.outcomeUnknown`);真终局帧永不带。
|
|
743
|
+
...(parts.outcomeUnknown === true ? { _sema_outcome: 'unknown' } : {}),
|
|
742
744
|
});
|
|
743
745
|
}
|
|
744
746
|
/** `done` → SDKResultSuccess (contract 02 §2.10 / 08 CS-10). */
|
|
@@ -783,8 +785,8 @@ observed) {
|
|
|
783
785
|
// 🔴 0.60.0(engine ≥7.64.0 / sdk 8.4.0):终局分流的判据从**八个平面键**换成**一条带标因由**
|
|
784
786
|
// (`result.terminal`)。读法单源在 `runTerminal.readRunTerminal` —— 本文件一个 `status` 字面
|
|
785
787
|
// 都不再比较(平面形的两个来源、以及「不认识的终态词绝不落成功」那条不变量,全在读器里)。
|
|
786
|
-
// `null` = 这份载体上连终态词都没有(mock 车道 / 不带 status 的旧合成帧)⇒
|
|
787
|
-
//
|
|
788
|
+
// `null` = 这份载体上连终态词都没有(mock 车道 / 不带 status 的旧合成帧)⇒ 0.72.12 起 fail-closed +
|
|
789
|
+
// `_sema_outcome:'unknown'`(此前维持成功臂;取舍见 `ErrorResultParts.outcomeUnknown`)。
|
|
788
790
|
const terminal = readRunTerminal(rRaw);
|
|
789
791
|
const durationMs = elapsedMs(ctx);
|
|
790
792
|
// [909]B1 — errorCode 透传(additive seam 字段;CC schema 无此键,与 success 臂 `model` 同款超集
|
|
@@ -892,11 +894,26 @@ observed) {
|
|
|
892
894
|
// [909]B1 — 防御臂:四臂之外的未知终态词(core 未来加因由臂 / 未知 wire 变形)绝不洗成空成功
|
|
893
895
|
// —— error_during_execution + errors[] 如实报出那个词的字面。读器把两代字节的这一格都归到
|
|
894
896
|
// `unknown` 臂(因由形取 `terminal.kind`,扁平形取 `status`),所以本臂对两代同样有效。
|
|
895
|
-
// 🔴 **终态词整个缺席**(
|
|
897
|
+
// 🔴 **终态词整个缺席**(读器返 `null`):0.72.12 CC-50 起**不再**落 success 臂 —— 此前这里维持成功
|
|
898
|
+
// (mock 车道 / 不带终态词的旧合成帧「零行为变化」),等于把「这份载体上连终态词都没有」编成一份成功
|
|
899
|
+
// 结果。改为 fail-closed + `_sema_outcome:'unknown'`(取舍与理由见 `ErrorResultParts.outcomeUnknown`);
|
|
900
|
+
// 旧合成帧那一路从 success 翻成 error 是**真行为变化**,随 0.72.12 发车帖「取舍」段披露,mock 车道
|
|
901
|
+
// 要成功请在帧上带真终态词(`status:'completed'` / `terminal:{kind:'completed'}`)。
|
|
902
|
+
if (terminal === null || terminal === undefined) {
|
|
903
|
+
return errorResult(ctx, {
|
|
904
|
+
...errorBase,
|
|
905
|
+
subtype: 'error_during_execution',
|
|
906
|
+
outcomeUnknown: true,
|
|
907
|
+
errors: [
|
|
908
|
+
'run ended with no terminal record — outcome unknown, not a failure verdict (this client cannot tell whether the run succeeded; treat the result as unverified)',
|
|
909
|
+
],
|
|
910
|
+
});
|
|
911
|
+
}
|
|
896
912
|
if (terminal?.kind === 'unknown') {
|
|
897
913
|
return errorResult(ctx, {
|
|
898
914
|
...errorBase,
|
|
899
915
|
subtype: 'error_during_execution',
|
|
916
|
+
outcomeUnknown: true,
|
|
900
917
|
errors: [
|
|
901
918
|
terminal.message ??
|
|
902
919
|
(terminal.word !== undefined
|
|
@@ -175,6 +175,14 @@ export declare const MODEL_OUTPUT_ERROR_PREFIX = "Model output error";
|
|
|
175
175
|
export declare function isModelOutputErrorText(text: string): boolean;
|
|
176
176
|
/** 这一行是不是「模型输出问题」行(渲染面 / 归约面的谓词;与铸点同源,端别自抄前缀字面)。与 {@link isGovernanceStopRowText} 互斥。 */
|
|
177
177
|
export declare function isModelOutputErrorRowText(text: string): boolean;
|
|
178
|
+
/**
|
|
179
|
+
* 0.72.12 CC-50:第三条中性终局行身份 —— **结局不知道**(done 帧无终态记录 / 认不出的终态词)。
|
|
180
|
+
* 三端渲染面按行首认它(与 `Model output error:` / `Run stopped:` 同律);它不是 API 错误、不是失败判决,
|
|
181
|
+
* 是「别把这次结果当已验证」。铸点单源在 runStream 终局臂(读结果帧 `_sema_outcome`)。
|
|
182
|
+
*/
|
|
183
|
+
export declare const OUTCOME_UNKNOWN_ROW_PREFIX = "Outcome unknown";
|
|
184
|
+
/** 这一行是不是「结局不知道」终局行(只认行首)。 */
|
|
185
|
+
export declare function isOutcomeUnknownRowText(text: string): boolean;
|
|
178
186
|
/**
|
|
179
187
|
* 终态是不是治理/契约面的(⇒ 中性行),而不是 provider/HTTP 面的(⇒ 保留 `API Error:`)。
|
|
180
188
|
*
|
|
@@ -197,6 +197,35 @@ export function isModelOutputErrorText(text) {
|
|
|
197
197
|
export function isModelOutputErrorRowText(text) {
|
|
198
198
|
return text.trimStart().startsWith(`${MODEL_OUTPUT_ERROR_PREFIX}:`);
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* 0.72.12 CC-50:第三条中性终局行身份 —— **结局不知道**(done 帧无终态记录 / 认不出的终态词)。
|
|
202
|
+
* 三端渲染面按行首认它(与 `Model output error:` / `Run stopped:` 同律);它不是 API 错误、不是失败判决,
|
|
203
|
+
* 是「别把这次结果当已验证」。铸点单源在 runStream 终局臂(读结果帧 `_sema_outcome`)。
|
|
204
|
+
*/
|
|
205
|
+
export const OUTCOME_UNKNOWN_ROW_PREFIX = 'Outcome unknown';
|
|
206
|
+
/**
|
|
207
|
+
* 合成终态行的**唯一铸点**(0.72.12 起两处共用:`API Error:` / `Run stopped:` / `Model output error:` 那一行,与
|
|
208
|
+
* `Outcome unknown:` 那一行)。
|
|
209
|
+
* [2084]③:message 内部要有判别哨兵——外层 isApiErrorMessage/err- uuid 只护住读信封的消费者,而转录回喂/摘要器/
|
|
210
|
+
* 评分器读的是 message 本体,没有 in-message 标记就会把这句引擎文案当成模型自己说的话。CC 2.1.220 同款哨兵 =
|
|
211
|
+
* model:'<synthetic>'(下游投影层对 string 型 model 原样保留,不再回填真模型名)。
|
|
212
|
+
* 🔴 `isApiErrorMessage: true` 是 CC 的**行类**旗(「这是合成的终态错误行,不是模型说的话」),不是「provider API
|
|
213
|
+
* 错误」的断言:可见性过滤 / -p 契约投影 / tick 熔断都读它。
|
|
214
|
+
*/
|
|
215
|
+
function syntheticTerminalRow(ctx, text) {
|
|
216
|
+
return {
|
|
217
|
+
session_id: ctx.sessionId ?? '',
|
|
218
|
+
uuid: `err-${Date.now().toString(36)}`,
|
|
219
|
+
type: 'assistant',
|
|
220
|
+
message: { role: 'assistant', model: '<synthetic>', content: [{ type: 'text', text }] },
|
|
221
|
+
parent_tool_use_id: null,
|
|
222
|
+
isApiErrorMessage: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/** 这一行是不是「结局不知道」终局行(只认行首)。 */
|
|
226
|
+
export function isOutcomeUnknownRowText(text) {
|
|
227
|
+
return text.trimStart().startsWith(`${OUTCOME_UNKNOWN_ROW_PREFIX}:`);
|
|
228
|
+
}
|
|
200
229
|
/**
|
|
201
230
|
* 终态是不是治理/契约面的(⇒ 中性行),而不是 provider/HTTP 面的(⇒ 保留 `API Error:`)。
|
|
202
231
|
*
|
|
@@ -833,18 +862,7 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
833
862
|
? // 0.72.8 CC-43:第二条中性臂 —— 模型输出坏了(core 原句锚),不是 API 错误;busy / 治理两臂优先(结构位赢过文本锚)。
|
|
834
863
|
`${MODEL_OUTPUT_ERROR_PREFIX}: ${errText}${maxTokHint}`
|
|
835
864
|
: `API Error: ${errText}${maxTokHint}`;
|
|
836
|
-
yield
|
|
837
|
-
session_id: ctx.sessionId ?? '',
|
|
838
|
-
uuid: `err-${Date.now().toString(36)}`,
|
|
839
|
-
type: 'assistant',
|
|
840
|
-
// [2084]③:message 内部要有判别哨兵——外层 isApiErrorMessage/err- uuid 只护住读信封的
|
|
841
|
-
// 消费者,而转录回喂/摘要器/评分器读的是 message 本体,没有 in-message 标记就会把这句
|
|
842
|
-
// 引擎文案当成模型自己说的话。CC 2.1.220 同款哨兵 = model:'<synthetic>'(下游投影层对
|
|
843
|
-
// string 型 model 原样保留,不再回填真模型名)。
|
|
844
|
-
message: { role: 'assistant', model: '<synthetic>', content: [{ type: 'text', text: rowText }] },
|
|
845
|
-
parent_tool_use_id: null,
|
|
846
|
-
isApiErrorMessage: true,
|
|
847
|
-
};
|
|
865
|
+
yield syntheticTerminalRow(ctx, rowText);
|
|
848
866
|
}
|
|
849
867
|
// plan_review park(core [508]③ 拍板形状):done `status:"needs_review"` + gate plan_review 不是
|
|
850
868
|
// 普通完成——壳弹审批卡(planReviewWire 合成 question 借 AskUserQuestion overlay;决断走
|
|
@@ -895,7 +913,21 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
895
913
|
// 字节的证据都不是;按那句话办,一条账数得全的 run 会被渲成「≥」并被宿主持久化成不完整状态
|
|
896
914
|
// (失效方向在这里**不是**安全的那一侧,它是在断言一件没发生的事)。
|
|
897
915
|
// ⇒ 观测位改为 per-stream 局部量,终帧按值收(第三参),与上面对账臂读的是同一份。
|
|
898
|
-
|
|
916
|
+
const resultFrame = terminalToSdkResult(ev, ctx, { usageMissingObserved, nestedUsageByTask });
|
|
917
|
+
// 0.72.12 CC-50(test [7541] G2-d「铸了就要有人读」):结果帧上的 `_sema_outcome:'unknown'`(终态记录整个
|
|
918
|
+
// 缺席 / 认不出的终态词;铸点与取舍见 terminalToSdkResult `ErrorResultParts.outcomeUnknown`)在**这里**被消费 ——
|
|
919
|
+
// 交互面合成一行 `Outcome unknown:` 终局行(句子与结果帧 `errors[0]` 单源),壳零接线即可见;它**不是**
|
|
920
|
+
// `API Error:`(那是 provider 故障身份),也不是「失败」:结果边界 fail-closed 是取舍,行文案把这一点说出来。
|
|
921
|
+
// 🔴 稀疏:真 completed / 真 failed 帧不带旗,这里零行(G2-e 负控)。
|
|
922
|
+
const outcome = resultFrame.
|
|
923
|
+
_sema_outcome;
|
|
924
|
+
if (outcome === 'unknown') {
|
|
925
|
+
const errs = resultFrame.errors;
|
|
926
|
+
const sentence = Array.isArray(errs) && typeof errs[0] === 'string' ? errs[0] : 'the run ended without a terminal record this client can read';
|
|
927
|
+
// 行类旗 isApiErrorMessage 置位(合成终态行),不是「provider 错误」断言 —— 同 Model output error 行。
|
|
928
|
+
yield syntheticTerminalRow(ctx, `${OUTCOME_UNKNOWN_ROW_PREFIX}: ${sentence}`);
|
|
929
|
+
}
|
|
930
|
+
yield resultFrame;
|
|
899
931
|
return;
|
|
900
932
|
}
|
|
901
933
|
// Every other arm → typed three-state projection(REF-CC-058)。
|
|
@@ -184,7 +184,10 @@ export function createBackgroundView(client, opts) {
|
|
|
184
184
|
async function pullAssistant() {
|
|
185
185
|
try {
|
|
186
186
|
const { tasks } = await client.assistant.tasks({ signal: abort.signal });
|
|
187
|
-
|
|
187
|
+
// 0.72.12 CC-50(#59 A-17):200 回体缺 tasks 键 = 读不出,不是「零任务」⇒ unavailable(空表 + ok 是编的)。
|
|
188
|
+
if (!Array.isArray(tasks))
|
|
189
|
+
return { health: 'unavailable', tasks: [] };
|
|
190
|
+
return { health: capsApprovals === false ? 'not-configured' : 'ok', tasks };
|
|
188
191
|
}
|
|
189
192
|
catch (err) {
|
|
190
193
|
debugLog(`assistant.tasks degrade: ${String(err)}`);
|
|
@@ -199,7 +202,9 @@ export function createBackgroundView(client, opts) {
|
|
|
199
202
|
try {
|
|
200
203
|
// G19 一次性快照 verb:SDK 内部开 SSE 流取首 snapshot 帧即断;abort.signal 直通 ⇒ dispose 能掐在飞连接。
|
|
201
204
|
const { tasks } = await client.fleet.snapshot({ signal: abort.signal });
|
|
202
|
-
|
|
205
|
+
if (!Array.isArray(tasks))
|
|
206
|
+
return { health: 'unavailable', tasks: [] }; // CC-50:同 assistant 腿
|
|
207
|
+
return { health: 'ok', tasks };
|
|
203
208
|
}
|
|
204
209
|
catch (err) {
|
|
205
210
|
debugLog(`fleet.snapshot degrade: ${String(err)}`);
|
package/dist/detachWire.d.ts
CHANGED
|
@@ -91,6 +91,19 @@ export declare function headlessDetachDisabledByEnv(env?: EnvLike): boolean;
|
|
|
91
91
|
* staleness — the header is harmless on the wire, and the stream POST fails loudly on a dead engine).
|
|
92
92
|
* Same probe shape as limitsWire.engineLimitsSupportWarning (1.5s budget, never throws).
|
|
93
93
|
*/
|
|
94
|
+
/** 0.72.13 CC-53:版本闸探测的**三态**读数。`unknown` = /health 答得奇怪(非 2xx / 坏 JSON)或整个不可达 —— 不能证明陈旧,也没证明支持。 */
|
|
95
|
+
export type EngineDetachSupport = 'supported' | 'unsupported' | 'unknown';
|
|
96
|
+
/**
|
|
97
|
+
* 三态读口(0.72.13 CC-53;test 证据帖「异常被吞成健康值」一类):此前只有布尔口,「不可达」与「确认支持」同返 `true`。
|
|
98
|
+
* fail-soft 的**行为**不变(`unknown` 照发 detach 头:头在 wire 上无害,死引擎由随后的流 POST 响亮失败),变的是
|
|
99
|
+
* 调用方现在**分得清**并能把这件事说出来({@link detachSupportDisclosure})。1.5s 预算,永不抛。
|
|
100
|
+
*/
|
|
101
|
+
export declare function probeEngineDetachSupport(baseUrl: string, opts?: {
|
|
102
|
+
fetchImpl?: typeof fetch;
|
|
103
|
+
timeoutMs?: number;
|
|
104
|
+
}): Promise<EngineDetachSupport>;
|
|
105
|
+
/** `unknown` 时给用户 / 日志的一句披露(单源);另两态 ⇒ `undefined`(没什么要披露的)。 */
|
|
106
|
+
export declare function detachSupportDisclosure(support: EngineDetachSupport): string | undefined;
|
|
94
107
|
export declare function engineSupportsDetach(baseUrl: string, opts?: {
|
|
95
108
|
fetchImpl?: typeof fetch;
|
|
96
109
|
timeoutMs?: number;
|
package/dist/detachWire.js
CHANGED
|
@@ -89,13 +89,11 @@ export function headlessDetachDisabledByEnv(env = hostEnv()) {
|
|
|
89
89
|
return envFlagOff(env[HEADLESS_DETACH_ENV]);
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* staleness — the header is harmless on the wire, and the stream POST fails loudly on a dead engine).
|
|
96
|
-
* Same probe shape as limitsWire.engineLimitsSupportWarning (1.5s budget, never throws).
|
|
92
|
+
* 三态读口(0.72.13 CC-53;test 证据帖「异常被吞成健康值」一类):此前只有布尔口,「不可达」与「确认支持」同返 `true`。
|
|
93
|
+
* fail-soft 的**行为**不变(`unknown` 照发 detach 头:头在 wire 上无害,死引擎由随后的流 POST 响亮失败),变的是
|
|
94
|
+
* 调用方现在**分得清**并能把这件事说出来({@link detachSupportDisclosure})。1.5s 预算,永不抛。
|
|
97
95
|
*/
|
|
98
|
-
export async function
|
|
96
|
+
export async function probeEngineDetachSupport(baseUrl, opts) {
|
|
99
97
|
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
100
98
|
const controller = new AbortController();
|
|
101
99
|
const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? 1500);
|
|
@@ -105,19 +103,38 @@ export async function engineSupportsDetach(baseUrl, opts) {
|
|
|
105
103
|
signal: controller.signal,
|
|
106
104
|
});
|
|
107
105
|
if (!res.ok)
|
|
108
|
-
return
|
|
109
|
-
const body =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
return 'unknown';
|
|
107
|
+
const body = await res.json();
|
|
108
|
+
// 回体必须是对象:null / 数组 / 标量 = 答得奇怪 ⇒ unknown(异源对抗复审:此前折成 unsupported,消费者据此关掉 detach)。
|
|
109
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body))
|
|
110
|
+
return 'unknown';
|
|
111
|
+
if (!('version' in body))
|
|
112
|
+
return 'unsupported'; // 健康回体、无 version 键 = pre-1.96 引擎(确证陈旧)
|
|
113
|
+
const version = body.version;
|
|
114
|
+
// version 在场但不是可解析的 x.y.z 串 ⇒ unknown(不拿读不懂的值当「陈旧」证据)
|
|
115
|
+
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+/.test(version))
|
|
116
|
+
return 'unknown';
|
|
117
|
+
if (versionSupportsDetach(version))
|
|
118
|
+
return 'supported';
|
|
119
|
+
return 'unsupported';
|
|
113
120
|
}
|
|
114
121
|
catch {
|
|
115
|
-
return
|
|
122
|
+
return 'unknown';
|
|
116
123
|
}
|
|
117
124
|
finally {
|
|
118
125
|
clearTimeout(timer);
|
|
119
126
|
}
|
|
120
127
|
}
|
|
128
|
+
/** `unknown` 时给用户 / 日志的一句披露(单源);另两态 ⇒ `undefined`(没什么要披露的)。 */
|
|
129
|
+
export function detachSupportDisclosure(support) {
|
|
130
|
+
if (support !== 'unknown')
|
|
131
|
+
return undefined;
|
|
132
|
+
return 'could not confirm whether this engine supports detach (its /health did not answer usably); sending the detach header anyway — it is harmless on the wire, and a dead engine fails the request loudly';
|
|
133
|
+
}
|
|
134
|
+
export async function engineSupportsDetach(baseUrl, opts) {
|
|
135
|
+
// 布尔口 = 三态口的薄封装(保留给既有调用方):只有**确证陈旧**才不发;`unknown` 仍 fail-soft 发。
|
|
136
|
+
return (await probeEngineDetachSupport(baseUrl, opts)) !== 'unsupported';
|
|
137
|
+
}
|
|
121
138
|
/**
|
|
122
139
|
* Resolve the detach header for a headless `-p` submit. DEFAULT ON; `SEMA_HEADLESS_DETACH`=false|0
|
|
123
140
|
* ⇒ off (env-off); SEMA_ENGINE_URL external engine confirmed <1.196.0 ⇒ off (old-engine — the caller
|
|
@@ -120,4 +120,19 @@ type Listener = (ev: EngineAgentPanelEvent) => void;
|
|
|
120
120
|
export declare function publishEngineAgentPanelEvent(ev: EngineAgentPanelEvent): void;
|
|
121
121
|
/** Single consumer (the panel hook). Replays any buffered events, returns an unsubscribe. */
|
|
122
122
|
export declare function subscribeEngineAgentPanel(fn: Listener): () => void;
|
|
123
|
+
/** 缺席上报的载荷(独立订阅口;**没有** `kind` 位 —— 它不属于主通道联合)。 */
|
|
124
|
+
export interface EngineAgentPanelAbsence {
|
|
125
|
+
taskId: string;
|
|
126
|
+
/** 最后一次在投影里见到这一行的钟(epoch ms)。 */
|
|
127
|
+
lastSeenAtMs: number;
|
|
128
|
+
/** 发出这条上报时已缺席多久(ms;> `ABSENT_SETTLE_MS`)。 */
|
|
129
|
+
absentForMs: number;
|
|
130
|
+
}
|
|
131
|
+
type AbsenceListener = (ev: EngineAgentPanelAbsence) => void;
|
|
132
|
+
/** 发布一条缺席上报(由 `projectFleetAgentRows` 的缺席路径调;消费者抛错不反噬投影)。 */
|
|
133
|
+
export declare function publishEngineAgentPanelAbsence(ev: EngineAgentPanelAbsence): void;
|
|
134
|
+
/** 单消费者(面板 hook)。订阅即回放缓冲,返回取消订阅。不订阅 = 行为与 0.72.11 的壳完全相同(行停在最后已知态)。 */
|
|
135
|
+
export declare function subscribeEngineAgentPanelAbsence(fn: AbsenceListener): () => void;
|
|
136
|
+
/** 测试钩:清缺席口的订阅与缓冲。 */
|
|
137
|
+
export declare function __resetEngineAgentPanelAbsenceForTests(): void;
|
|
123
138
|
export {};
|
|
@@ -53,6 +53,10 @@ const MAX_BUFFER = 200;
|
|
|
53
53
|
let listener = null;
|
|
54
54
|
const buffer = [];
|
|
55
55
|
export function publishEngineAgentPanelEvent(ev) {
|
|
56
|
+
// 同 taskId 的任何主通道事实(tick / fleet-row = 行在场;end = 真终态)都让**缓冲里**那条缺席过期 —— 迟订阅的消费者
|
|
57
|
+
// 不许先恢复了行、再被一条旧缺席标成 unknown(已递达订阅者的缺席由订阅者按契约在这些事件上撤标)。
|
|
58
|
+
if (ev.kind !== 'sweep')
|
|
59
|
+
absenceBuffer.delete(ev.taskId);
|
|
56
60
|
if (listener) {
|
|
57
61
|
try {
|
|
58
62
|
listener(ev);
|
|
@@ -191,3 +195,51 @@ export function subscribeEngineAgentPanel(fn) {
|
|
|
191
195
|
listener = null;
|
|
192
196
|
};
|
|
193
197
|
}
|
|
198
|
+
const MAX_ABSENCE_BUFFER = 200;
|
|
199
|
+
let absenceListener = null;
|
|
200
|
+
/** 未订阅时的缓冲:按 taskId 留最新一条(同一行只有最新一次缺席有信息),有上界(最旧的先让位)。 */
|
|
201
|
+
const absenceBuffer = new Map();
|
|
202
|
+
/** 发布一条缺席上报(由 `projectFleetAgentRows` 的缺席路径调;消费者抛错不反噬投影)。 */
|
|
203
|
+
export function publishEngineAgentPanelAbsence(ev) {
|
|
204
|
+
if (absenceListener) {
|
|
205
|
+
try {
|
|
206
|
+
absenceListener(ev);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// A consumer render error must never tear down the projection.
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
absenceBuffer.delete(ev.taskId);
|
|
214
|
+
absenceBuffer.set(ev.taskId, ev);
|
|
215
|
+
if (absenceBuffer.size > MAX_ABSENCE_BUFFER) {
|
|
216
|
+
const oldest = absenceBuffer.keys().next().value;
|
|
217
|
+
if (oldest !== undefined)
|
|
218
|
+
absenceBuffer.delete(oldest);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** 单消费者(面板 hook)。订阅即回放缓冲,返回取消订阅。不订阅 = 行为与 0.72.11 的壳完全相同(行停在最后已知态)。 */
|
|
222
|
+
export function subscribeEngineAgentPanelAbsence(fn) {
|
|
223
|
+
absenceListener = fn;
|
|
224
|
+
if (absenceBuffer.size > 0) {
|
|
225
|
+
const pending = [...absenceBuffer.values()];
|
|
226
|
+
absenceBuffer.clear();
|
|
227
|
+
for (const ev of pending) {
|
|
228
|
+
try {
|
|
229
|
+
fn(ev);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// ignore — same guard as publish
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return () => {
|
|
237
|
+
if (absenceListener === fn)
|
|
238
|
+
absenceListener = null;
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** 测试钩:清缺席口的订阅与缓冲。 */
|
|
242
|
+
export function __resetEngineAgentPanelAbsenceForTests() {
|
|
243
|
+
absenceListener = null;
|
|
244
|
+
absenceBuffer.clear();
|
|
245
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* executionLaneCapability — **本部署的执行车道自述**的三端共用读面(0.72.13;server ≥7.86.0 / S-426 的
|
|
3
|
+
* `GET /v1/capabilities.executionLane`,契约附录 B.1;sdk 9.6.0 的 `Capabilities` 尚未声明该键)。
|
|
4
|
+
*
|
|
5
|
+
* ── 这一格答的是哪一个问题 ────────────────────────────────────────────────────────────────
|
|
6
|
+
* 「工具跑的那台机器就是这台 server 吗」(文件系统同一性)。它决定技能的 `baseDir` 能不能发:此前消费端只能拿
|
|
7
|
+
* 旁证三合取(本壳 spawn ∧ 无远端 URL ∧ `REMOTE_EXEC` ∈ {空, host})去猜,`-p` / mock 两条车道因此发不出 `baseDir`。
|
|
8
|
+
* 7.86.0 起 server 自己说:`{ provider, toolsOnThisHost }`。
|
|
9
|
+
*
|
|
10
|
+
* ── 🔴 三条契约读法(附录 B.1 逐字)────────────────────────────────────────────────────────
|
|
11
|
+
* ① **键缺席 ≠「不是本机车道」**:老 server(<7.86.0)不报这一位 ⇒ `not_reported`,只能按改前的旁证判。读成 false
|
|
12
|
+
* 会让老 server 上的 host 部署一律欠报 `baseDir`,还渲错车道。
|
|
13
|
+
* ② **蕴含单向,别读成等价**:`toolsOnThisHost:false` ⇒ `baseDir` 恒不发(本位的承诺);`true` 只是**必要条件** ——
|
|
14
|
+
* 扁平形技能与插件来源技能在任何车道上都不带,`baseDir` 一律按 present-iff 读。
|
|
15
|
+
* ③ **只出词,不出内情**:apiUrl / host / serial / 镜像 / 凭据 / `mountPath` 契约上一个字都不上 wire;真出现了也不是
|
|
16
|
+
* 本读面该带出去的东西 ⇒ 多余成员不过境。它也**不是**权限位(`cwdHonored` 才是),不许拿它顶替。
|
|
17
|
+
*
|
|
18
|
+
* ── 四态读数(与 sql / writeProtection / webSearch.backend 三只兄弟同一套词汇)──────────────
|
|
19
|
+
* `unobserved`(本进程一次 caps 都没观测到)/ `not_reported`(观测到了、回体无此键 = 老 server)/ `present`。
|
|
20
|
+
* 本位没有 `none` 档:铸点恒铸对象(`REMOTE_EXEC` 未设折 `"host"`),`null` / 缺座 / 坏形 ⇒ 畸形(删格 ⇒ `unobserved`)。
|
|
21
|
+
*
|
|
22
|
+
* ── 🔴 窄读域只许等于或宽于铸点域 ────────────────────────────────────────────────────────────
|
|
23
|
+
* `provider` 的闭集取型自 server 的 `RemoteExecProvider`(加一条车道那张表自动跟着长)。本包**不**手抄那几个词当判据
|
|
24
|
+
* (抄一份 = 给自己立第二个判官,上游加车道那天把新词判成畸形):按**非空串**读,上屏前按 UNTRUSTED-for-display 消毒封长。
|
|
25
|
+
*/
|
|
26
|
+
/** `present` 时的读数:车道词(开集)+ 文件系统同一性位(严格布尔)。 */
|
|
27
|
+
export interface ExecutionLaneCapabilityView {
|
|
28
|
+
provider: string;
|
|
29
|
+
toolsOnThisHost: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** 四态读数(见文件头)。`unobserved` 由**读口**在这一格空缺时铸,不由投影铸。 */
|
|
32
|
+
export type ExecutionLaneReading = {
|
|
33
|
+
kind: 'unobserved';
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'not_reported';
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'present';
|
|
38
|
+
view: ExecutionLaneCapabilityView;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* caps 回体 → 本格读数;**畸形一律 `undefined`**(= 这一格不写 ⇒ 读口答 `unobserved`)。
|
|
42
|
+
* 键不在 / `undefined` ⇒ `not_reported`(老 server);`null` / 数组 / 非对象 / 缺任一座 / 座坏形 ⇒ 畸形。
|
|
43
|
+
*/
|
|
44
|
+
export declare function projectExecutionLaneCapability(caps: unknown): ExecutionLaneReading | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* 宿主 caps probe 的**读面 tee** 落点(与三只兄弟并列;宿主在同一处按序调)。绝不 throw。
|
|
47
|
+
* 投影 `undefined`(畸形)⇒ 这一格**被删**而不是留着上一台引擎的旧值;`opts.generation` 关掉「旧探测覆盖新读数」竞态。
|
|
48
|
+
*/
|
|
49
|
+
export declare function noteEngineCapsForExecutionLane(baseUrl: string, caps: unknown, opts?: {
|
|
50
|
+
generation?: number;
|
|
51
|
+
}): void;
|
|
52
|
+
/** 本进程观测到的执行车道;这一格空缺 ⇒ `{kind:'unobserved'}`(**绝不**编一条车道)。 */
|
|
53
|
+
export declare function observedExecutionLane(baseUrl?: string | undefined): ExecutionLaneReading;
|
|
54
|
+
/**
|
|
55
|
+
* 「工具是否跑在本机」的**判据单源**(此前三端各拼旁证三合取):读到位就用位 —— `true` 是发 `baseDir` 的必要条件、
|
|
56
|
+
* `false` 是「恒不发」的承诺;读不到(`not_reported` / `unobserved`)⇒ **原样**用调用方的旁证,绝不折 false
|
|
57
|
+
* (契约 🔴:老 server 上只能按改前的办法判)。
|
|
58
|
+
*/
|
|
59
|
+
export declare function toolsRunHereFromExecutionLane(reading: ExecutionLaneReading, legacyInference: boolean): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* 三态(+未观测)→ doctor 那一行的 detail 串。**唯一措辞真源**(三端共用;端只拼接不另写)。
|
|
62
|
+
* 🔴 `not_reported` 那一句**不许**暗示「不是本机车道」;`present` 的 false 那一句把「技能 baseDir 不发」说出来
|
|
63
|
+
* (用户看到技能相对路径解析不了时,这就是原因)。
|
|
64
|
+
*/
|
|
65
|
+
export declare function executionLaneDoctorDetail(reading: ExecutionLaneReading): string;
|
|
66
|
+
/** **换代失效口**(由宿主在引擎温切成功后调):清成未观测,不是留旧值。空串 ⇒ no-op;绝不 throw。 */
|
|
67
|
+
export declare function forgetExecutionLaneReading(baseUrl: string | undefined): void;
|
|
68
|
+
/** 测试钩子。 */
|
|
69
|
+
export declare function __resetExecutionLaneReadingsForTests(): void;
|