@sema-agent/client-core 0.72.12 → 0.72.14
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 +35 -0
- package/README.md +3 -1
- package/dist/detachWire.d.ts +13 -0
- package/dist/detachWire.js +29 -12
- package/dist/engineAgentPanelStore.d.ts +15 -17
- package/dist/engineAgentPanelStore.js +54 -3
- package/dist/executionLaneCapability.d.ts +69 -0
- package/dist/executionLaneCapability.js +125 -0
- package/dist/fleetAgentPanelProjection.js +5 -4
- package/dist/hitl/approvalsFeed.d.ts +28 -1
- package/dist/hitl/approvalsFeed.js +88 -5
- package/dist/hitl/crashConverged.d.ts +2 -1
- package/dist/hitl/hitlBridge.d.ts +12 -1
- package/dist/hitl/hitlBridge.js +20 -0
- package/dist/hitl/hitlHostSurface.d.ts +4 -0
- package/dist/hitl/hitlHostSurface.js +8 -0
- package/dist/hitl/livePendingAsk.d.ts +107 -0
- package/dist/hitl/livePendingAsk.js +198 -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/hitl/toolApprovalWire.d.ts +16 -0
- package/dist/hitl/toolApprovalWire.js +16 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/notifications.js +28 -17
- package/docs/INTEGRATION-CLIENTS.md +84 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,41 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.72.14(2026-09-18)
|
|
53
|
+
|
|
54
|
+
> 主题:**悬挂的流内 ask 的拉半场**(CC-55;cli L-397 P1)。流内 ask 在铸造那一刻若没有活着的宿主流可投,引擎把它转成悬挂(窗约一小时,不补发),此后只在 `GET /v1/approvals` 回体的 `livePending` 段可发现 —— 后台 workflow 的子代恒落这一形(宿主 turn 先结束),用户面零帧零卡、代理静默卡到窗尽。**patch**:型面纯 additive(+1 常量 +7 函数,公面 1011 → 1019;`ApprovalsFeedSnapshot` / `ApprovalsFeedStats` / `ApprovalsFeedOptions` / `ToolApprovalFrameLaneOpts` / `ApprovalCardRequest` / `ToolApprovalFrameOutcome` 各加一可选或新增位;零 wire 新键;peer 不动)。接入面 §61。
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **`ApprovalsFeedSnapshot.livePending?`**:`startApprovalsFeed` 的快照此前**只含 `pending`**,变化摘要也只算 `pending` ⇒ 悬挂 ask 出现 / 结算都不触发快照,端即使挂了 feed 也看不见。现在 `list()` 回体的 `livePending` 段按结构窄读进快照(sdk barrel 不导出该行型),并进变化摘要。🔴 **键缺席 = 引擎没报这一段**(老引擎),不是「没有」—— 快照上键缺席,绝不铸 `[]`;引擎报了且为空才是 `[]`;键在场却读不懂 ⇒ **整次取件不提交**(保留上一张,不把读不懂渲成没有,计入 `listErrors`)。
|
|
59
|
+
- **`src/hitl/livePendingAsk.ts`**:`readLivePendingRows(body)` 三态(`not_reported` / `malformed` / `present{rows, dropped}`;四必填位坏形丢行计数、同 `approvalId` 留第一条、可选旗标 only-if-true、多余成员不过境);`suspendedSubagentAsks(snapshot, {sessionId?})`(只留子代行;给了会话就只留该会话的,行上没有 `sessionId` = 归不了属 ⇒ 不出卡);`countApprovalsAwaitingDecision(snapshot, filter?)`(`durable` / `live` / `suspendedSubagent`,**不知道 = `null` 不是 0** —— 端的「在等你决定」计数读它,不再只认 durable 停驻一条来源);`createSuspendedAskTracker(filter?)`(快照 → `appeared` / `gone` 的去重账:同 `approvalId` 只出一次;`noteSurfacedByStream` = **双向认领口**,返回 `true` 帧腿可出卡且快照不再出第二张、返回 `false` = 已从快照出过卡 ⇒ 帧腿跳过;`requeue` = 让一只 ask 重新出卡;两本账都**不按条数淘汰活跃条目**(出过卡的随「从列表消失」离账;认领在列表里见过之后消失才离账;只有「认领了但从未在快照里见过」的那一小类受 512 上界管);`noteDecided` = 已决断的在引擎结算前不重新出卡;条目消失恰一次 `gone` = 撤卡;`livePending` 键缺席 ⇒ 已出的卡全部 `gone`)。
|
|
60
|
+
- **决断口 `surfaceSuspendedAskAndRespond(row, respond, signal?, lane?)`**:悬挂行经 `livePendingRowToApprovalFrame` 适配后走**流内帧腿同一条链**(同一个卡口、同一个身份键铸口 `liveFrameCallKey(approvalId)`、同一套回执 / 拒绝读法)。🔴 **数组名即路由判据**:`livePending` 行的决议口是 `POST /v1/tool-approvals/{approvalId}/respond`,不是 durable 行的 decide 口;幂等键 = `approvalId`。
|
|
61
|
+
- **`ToolApprovalFrameLaneOpts.argsUnavailable?: true`**:`livePending` 段刻意不带工具入参 ⇒ 这张卡上 `wireNote` 明说「看不到入参、你是在没看到它们的情况下做决定、这张卡不接受编辑」,卡请求带结构化位 `ApprovalCardRequest.argsUnavailable: true`(端据此不给「编辑后批准」)。🔴 真收到带改写的批准 ⇒ **整次不发**:丢掉改写再发 allow = 批准了人没看到的原始入参,把改写转发 = 拿卡上的空对象替换真实入参,两条都不是人按下的那个决定 ⇒ 结局 `decision:'unresolved'` + `ToolApprovalFrameOutcome.editRefused: true`,屏上一条告知(`EDIT_REFUSED_ON_BLIND_ASK_WARN_TEXT` / `surfaceEditRefusedOnBlindAsk`),ask 仍挂着由端 `requeue` 重出。只由上面那只决断口置位,流内帧腿行为零变化。
|
|
62
|
+
- **对账节拍 `ApprovalsFeedOptions.reconcile?`**(`{ shouldReconcile, intervalMs?, maxIntervalMs? }`)+ `ApprovalsFeedStats.reconciles`:较老的引擎在悬挂 ask 出现 / 结算时不在 `approvals.stream` 上发任何事件 ⇒ push 腿连着也看不见;而「仅活跃 run 时轮询」的 `shouldPoll` 恰好在宿主 turn 结束后关着。本旋钮是**独立**的宿主闸:为真期间(典型:会话有活跃后台代理 ∧ 没有活着的宿主 run)无论哪条腿都定时重取;闸关零额外取件;快照没变则间隔翻倍到上限,变了 / 闸重开回基础间隔;不计 `polls`、不动轮询腿的连续失败计数;谓词抛错 / 非布尔按「查」处理。不传 = 连定时器都不起,行为一字不差。
|
|
63
|
+
- 新门 `scripts/run-live-pending-ask-test.mjs`(52 格;变异反证逐格见红)。
|
|
64
|
+
|
|
65
|
+
### Known limits(本版新增)
|
|
66
|
+
|
|
67
|
+
- 悬挂 ask 的卡**看不到工具入参**(引擎读面今天不带;已请引擎在该段补入参摘要)。卡上如实明说;到货后本包把摘要过到卡上,届时另版。
|
|
68
|
+
- 窗的倒计时不上这张卡:行上只有绝对死线、没有出帧时刻,凑不出可校偏的窗三键;`expiresAtMs` 原样在视图上,端要渲自己按本地钟渲并自担偏差。
|
|
69
|
+
|
|
70
|
+
## 0.72.13(2026-09-18)
|
|
71
|
+
|
|
72
|
+
> 主题:**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。
|
|
73
|
+
|
|
74
|
+
### Changed(0.72.12 勘误级改口,test 0.72.12 验收抓出)
|
|
75
|
+
|
|
76
|
+
- 🔴 **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**。
|
|
77
|
+
- **`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。
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- **`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,不在本包)。
|
|
82
|
+
- **`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 格(红先绿后)。
|
|
83
|
+
- **`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`。
|
|
84
|
+
- **发车前异源复审第一轮三条真修**:① [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(见上)。
|
|
85
|
+
- **发车前异源复审第二轮四条真修**(时序面):① [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 双闸 → 重呈 → 作答 → 请求体)入门。
|
|
86
|
+
|
|
52
87
|
## 0.72.12(2026-09-18)
|
|
53
88
|
|
|
54
89
|
> 主题:**「缺席被折成正向终态」病形按类清剿**(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。
|
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.14
|
|
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
|
|
@@ -312,6 +312,8 @@ public-surface guard checks that last one).
|
|
|
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
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 |
|
|
316
|
+
| `scripts/run-live-pending-ask-test.mjs` | Suspended in-stream asks (the `livePending` section of `GET /v1/approvals`): an absent key reads as not reported (never an empty list or a zero count), an unreadable section leaves the previous snapshot in place, rows are narrow-read (malformed rows dropped and counted, optional flags only-if-true, extra members never cross), the feed digest covers the section so an ask appearing or settling produces a snapshot, the host-gated reconcile cadence re-lists while the push leg is connected and backs off when nothing changes, the tracker surfaces each `approvalId` once and reports it gone once, and the decision runs through the same respond chain as the in-stream frame leg — with the card stating that the tool arguments are not visible and edits on that card never forwarded. |
|
|
315
317
|
| `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 |
|
|
316
318
|
| `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 |
|
|
317
319
|
| `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 |
|
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
|
|
@@ -102,23 +102,6 @@ export type EngineAgentPanelEvent = {
|
|
|
102
102
|
* explicit close arm carries it; the defensive sweeps settle without one). The consumer persists it
|
|
103
103
|
* on the row (task.result) + appends it to a live-viewed transcript (拍1 查看态 seed). */
|
|
104
104
|
report?: string;
|
|
105
|
-
} | {
|
|
106
|
-
/**
|
|
107
|
-
* 0.72.12 CC-50(test [7541] / sema-internal #59 头条站点):这一行**从引擎的行帧投影里消失**
|
|
108
|
-
* 超过 `ABSENT_SETTLE_MS` 仍没见到终态帧。它**不是**终局 —— 本包手上没有任何载体说明这只代理
|
|
109
|
-
* 完没完成(server [7545]:父 run 离场时子行按 `orphaned` 被移除,真终局由后续对账周期补发,
|
|
110
|
-
* 结构性晚于这个窗)。此前这条路径铸 `end{isError:false}`,把「不知道」编成了「成功」,屏上
|
|
111
|
-
* 渲成 ✔ Completed(两条现场腿秒级实测)。
|
|
112
|
-
* 消费端义务:认得 ⇒ 渲「engine no longer reports this agent · outcome unknown」并停表,
|
|
113
|
-
* **不许**把它翻译成 completed / failed / stopped 任何一个终态词;认不得(老壳)⇒ 行停在最后
|
|
114
|
-
* 已知态(可被证伪的「不知道」)。真终态来了仍走 `end`(恰一次);行回来了走 `fleet-row`。
|
|
115
|
-
*/
|
|
116
|
-
kind: 'absent';
|
|
117
|
-
taskId: string;
|
|
118
|
-
/** 最后一次在投影里见到这一行的钟(epoch ms)。 */
|
|
119
|
-
lastSeenAtMs: number;
|
|
120
|
-
/** 发出这条事件时已缺席多久(ms;≥ `ABSENT_SETTLE_MS`)。 */
|
|
121
|
-
absentForMs: number;
|
|
122
105
|
} | {
|
|
123
106
|
/** Exception-path safety net (published by seamQuery after every turn, throw or return):
|
|
124
107
|
* the consumer settles ALL rows it owns that are still running. Idempotent. */
|
|
@@ -137,4 +120,19 @@ type Listener = (ev: EngineAgentPanelEvent) => void;
|
|
|
137
120
|
export declare function publishEngineAgentPanelEvent(ev: EngineAgentPanelEvent): void;
|
|
138
121
|
/** Single consumer (the panel hook). Replays any buffered events, returns an unsubscribe. */
|
|
139
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;
|
|
140
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);
|
|
@@ -86,8 +90,7 @@ export function publishEngineAgentPanelEvent(ev) {
|
|
|
86
90
|
// bounded policy) — a subagent that starts at that point would replay as "still settled", forever.
|
|
87
91
|
if (ev.kind === 'sweep' && buffer.length > 0 && buffer[buffer.length - 1]?.kind === 'sweep')
|
|
88
92
|
return;
|
|
89
|
-
|
|
90
|
-
const progress = ev.kind === 'tick' || ev.kind === 'fleet-row' || ev.kind === 'absent';
|
|
93
|
+
const progress = ev.kind === 'tick' || ev.kind === 'fleet-row';
|
|
91
94
|
if (progress) {
|
|
92
95
|
const kind = ev.kind;
|
|
93
96
|
const taskId = ev.taskId;
|
|
@@ -150,7 +153,7 @@ export function publishEngineAgentPanelEvent(ev) {
|
|
|
150
153
|
return;
|
|
151
154
|
}
|
|
152
155
|
// Full: the oldest PROGRESS event yields (its taskId's next rollup supersedes it anyway).
|
|
153
|
-
const evictAt = buffer.findIndex(b => b.kind === 'tick' || b.kind === 'fleet-row'
|
|
156
|
+
const evictAt = buffer.findIndex(b => b.kind === 'tick' || b.kind === 'fleet-row');
|
|
154
157
|
if (evictAt >= 0) {
|
|
155
158
|
buffer.splice(evictAt, 1);
|
|
156
159
|
buffer.push(ev);
|
|
@@ -192,3 +195,51 @@ export function subscribeEngineAgentPanel(fn) {
|
|
|
192
195
|
listener = null;
|
|
193
196
|
};
|
|
194
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;
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
import { capForDisplay } from './fleetTaskDesc.js';
|
|
27
|
+
import { engineWireTarget } from './engineWireTarget.js';
|
|
28
|
+
import { engineCapsGeneration } from './engineCapsCache.js';
|
|
29
|
+
/**
|
|
30
|
+
* caps 回体 → 本格读数;**畸形一律 `undefined`**(= 这一格不写 ⇒ 读口答 `unobserved`)。
|
|
31
|
+
* 键不在 / `undefined` ⇒ `not_reported`(老 server);`null` / 数组 / 非对象 / 缺任一座 / 座坏形 ⇒ 畸形。
|
|
32
|
+
*/
|
|
33
|
+
export function projectExecutionLaneCapability(caps) {
|
|
34
|
+
if (caps === null || typeof caps !== 'object')
|
|
35
|
+
return undefined;
|
|
36
|
+
if (!('executionLane' in caps))
|
|
37
|
+
return { kind: 'not_reported' };
|
|
38
|
+
const lane = caps.executionLane;
|
|
39
|
+
if (lane === undefined)
|
|
40
|
+
return { kind: 'not_reported' };
|
|
41
|
+
if (lane === null || typeof lane !== 'object' || Array.isArray(lane))
|
|
42
|
+
return undefined;
|
|
43
|
+
const provider = lane.provider;
|
|
44
|
+
const toolsOnThisHost = lane.toolsOnThisHost;
|
|
45
|
+
if (typeof provider !== 'string' || provider === '')
|
|
46
|
+
return undefined;
|
|
47
|
+
if (typeof toolsOnThisHost !== 'boolean')
|
|
48
|
+
return undefined;
|
|
49
|
+
return { kind: 'present', view: { provider, toolsOnThisHost } };
|
|
50
|
+
}
|
|
51
|
+
const readingByBase = new Map();
|
|
52
|
+
/**
|
|
53
|
+
* 宿主 caps probe 的**读面 tee** 落点(与三只兄弟并列;宿主在同一处按序调)。绝不 throw。
|
|
54
|
+
* 投影 `undefined`(畸形)⇒ 这一格**被删**而不是留着上一台引擎的旧值;`opts.generation` 关掉「旧探测覆盖新读数」竞态。
|
|
55
|
+
*/
|
|
56
|
+
export function noteEngineCapsForExecutionLane(baseUrl, caps, opts) {
|
|
57
|
+
try {
|
|
58
|
+
if (typeof baseUrl !== 'string' || baseUrl === '')
|
|
59
|
+
return;
|
|
60
|
+
if (opts?.generation !== undefined && opts.generation !== engineCapsGeneration(baseUrl))
|
|
61
|
+
return;
|
|
62
|
+
const reading = projectExecutionLaneCapability(caps);
|
|
63
|
+
if (reading === undefined) {
|
|
64
|
+
readingByBase.delete(baseUrl);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
readingByBase.set(baseUrl, reading);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* fail-soft:本 tee 任何分支都不许打断 caps 探测链 */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** 本进程观测到的执行车道;这一格空缺 ⇒ `{kind:'unobserved'}`(**绝不**编一条车道)。 */
|
|
74
|
+
export function observedExecutionLane(baseUrl = engineWireTarget()?.baseUrl) {
|
|
75
|
+
if (typeof baseUrl !== 'string' || baseUrl === '')
|
|
76
|
+
return { kind: 'unobserved' };
|
|
77
|
+
const hit = readingByBase.get(baseUrl);
|
|
78
|
+
if (hit !== undefined)
|
|
79
|
+
return hit;
|
|
80
|
+
return { kind: 'unobserved' };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 「工具是否跑在本机」的**判据单源**(此前三端各拼旁证三合取):读到位就用位 —— `true` 是发 `baseDir` 的必要条件、
|
|
84
|
+
* `false` 是「恒不发」的承诺;读不到(`not_reported` / `unobserved`)⇒ **原样**用调用方的旁证,绝不折 false
|
|
85
|
+
* (契约 🔴:老 server 上只能按改前的办法判)。
|
|
86
|
+
*/
|
|
87
|
+
export function toolsRunHereFromExecutionLane(reading, legacyInference) {
|
|
88
|
+
if (reading.kind === 'present')
|
|
89
|
+
return reading.view.toolsOnThisHost;
|
|
90
|
+
return legacyInference;
|
|
91
|
+
}
|
|
92
|
+
/** 车道词的展示上限(UTF-16 单元;与三只兄弟同值同理由)。 */
|
|
93
|
+
const LANE_DETAIL_MAX = 40;
|
|
94
|
+
/**
|
|
95
|
+
* 三态(+未观测)→ doctor 那一行的 detail 串。**唯一措辞真源**(三端共用;端只拼接不另写)。
|
|
96
|
+
* 🔴 `not_reported` 那一句**不许**暗示「不是本机车道」;`present` 的 false 那一句把「技能 baseDir 不发」说出来
|
|
97
|
+
* (用户看到技能相对路径解析不了时,这就是原因)。
|
|
98
|
+
*/
|
|
99
|
+
export function executionLaneDoctorDetail(reading) {
|
|
100
|
+
switch (reading.kind) {
|
|
101
|
+
case 'unobserved':
|
|
102
|
+
return ('execution lane not observed — the engine reports it on /v1/capabilities; ' +
|
|
103
|
+
'this process has not received a capabilities response from the engine yet');
|
|
104
|
+
case 'not_reported':
|
|
105
|
+
return ('execution lane not reported by this engine — only newer engines advertise it; ' +
|
|
106
|
+
'this does not say where tools run, so the client keeps inferring it the way it did before');
|
|
107
|
+
case 'present': {
|
|
108
|
+
const lane = capForDisplay(reading.view.provider, LANE_DETAIL_MAX);
|
|
109
|
+
if (reading.view.toolsOnThisHost) {
|
|
110
|
+
return `execution lane ${lane} — tools run on the engine host (same filesystem), so skills that have a directory of their own can carry a baseDir`;
|
|
111
|
+
}
|
|
112
|
+
return `execution lane ${lane} — tools run on a different machine than the engine, so skill baseDir is never sent on this deployment`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** **换代失效口**(由宿主在引擎温切成功后调):清成未观测,不是留旧值。空串 ⇒ no-op;绝不 throw。 */
|
|
117
|
+
export function forgetExecutionLaneReading(baseUrl) {
|
|
118
|
+
if (typeof baseUrl !== 'string' || baseUrl === '')
|
|
119
|
+
return;
|
|
120
|
+
readingByBase.delete(baseUrl);
|
|
121
|
+
}
|
|
122
|
+
/** 测试钩子。 */
|
|
123
|
+
export function __resetExecutionLaneReadingsForTests() {
|
|
124
|
+
readingByBase.clear();
|
|
125
|
+
}
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
* setAppState 全树重渲)。
|
|
37
37
|
* ② 终态行发一次 `end`(幂等台账),并摘除常驻标 —— 与 bg_notification 合成链同一个 settle 边,
|
|
38
38
|
* 两边先到先得,消费端 settle 本身幂等(running 才动)。
|
|
39
|
-
* ③ 缺席上报(0.72.12 CC-50 改形):行从投影集里消失且**超过宽限窗**仍没见终态 ⇒
|
|
40
|
-
*
|
|
39
|
+
* ③ 缺席上报(0.72.12 CC-50 改形):行从投影集里消失且**超过宽限窗**仍没见终态 ⇒ 经**独立缺席口**(`publishEngineAgentPanelAbsence`,0.72.13;不上主通道)
|
|
40
|
+
* 上报一条(**不是** `end`、**不** settle)。此前这里「按 completed settle」= 把「引擎不再上报这一行」
|
|
41
41
|
* 编成「成功」—— 本包手上没有任何载体说明它完没完成(test [7541] 两条现场腿秒级实测:屏上
|
|
42
42
|
* ✔ Completed 的真实来源就是这一格;server [7545]:子行随父 run 离场被 `orphaned` 移除,真终局由
|
|
43
43
|
* 后续对账周期补发,结构性晚于这个窗)。真终态帧迟到照发那唯一一次 `end`;行回来照发 `fleet-row`。
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* run 发布器双生行的形)——REF-CC-042:与同包 `workflow.rowIdTail` 同一个函数(import,不再
|
|
48
48
|
* 就地维护第二份;该函数出过等价性事故,0.1.1 修,见 `workflow.ts` 头注)。
|
|
49
49
|
*/
|
|
50
|
-
import { publishEngineAgentPanelEvent } from './engineAgentPanelStore.js';
|
|
50
|
+
import { publishEngineAgentPanelAbsence, publishEngineAgentPanelEvent } from './engineAgentPanelStore.js';
|
|
51
51
|
import { rowIdTail } from './workflow.js';
|
|
52
52
|
import { TERMINAL_FLEET_TASK_STATUSES } from './fleet/fleetProjection.js';
|
|
53
53
|
import { DEFAULT_SESSION_KEY } from './sessionSlot.js';
|
|
@@ -234,7 +234,8 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
|
|
|
234
234
|
const absentForMs = nowMs - s.lastSeenAt;
|
|
235
235
|
if (absentForMs <= ABSENT_SETTLE_MS)
|
|
236
236
|
continue;
|
|
237
|
-
|
|
237
|
+
// 🔴 0.72.13:走**独立缺席口**,不上主通道(老消费者的裸 else = sweep 体,会把新 kind 当 sweep;见 store 头注 / DS-07)。
|
|
238
|
+
publishEngineAgentPanelAbsence({ taskId, lastSeenAtMs: s.lastSeenAt, absentForMs });
|
|
238
239
|
seenMap.set(taskId, { ...s, absentReportedAtMs: nowMs });
|
|
239
240
|
}
|
|
240
241
|
// ④ REF-CC-045(fleet2-07):settled 条目回收 —— 给台账一个上界(此前只增不减)。settle 已发过
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
* 端的自检可以断言「跑了 N 秒之后 mode 不是 idle 且 polls+pushEvents > 0」。
|
|
45
45
|
*/
|
|
46
46
|
import type { PendingCheckpoint } from '@sema-agent/sdk';
|
|
47
|
+
import { type LivePendingAskView } from './livePendingAsk.js';
|
|
47
48
|
/** feed 消费的 wire 切片。`stream` 可选 —— 老 SDK / mock 没有它就直接走轮询腿。 */
|
|
48
49
|
export interface ApprovalsFeedClientLike {
|
|
49
50
|
approvals: {
|
|
@@ -51,6 +52,7 @@ export interface ApprovalsFeedClientLike {
|
|
|
51
52
|
signal?: AbortSignal;
|
|
52
53
|
}): Promise<{
|
|
53
54
|
pending: PendingCheckpoint[];
|
|
55
|
+
livePending?: unknown[];
|
|
54
56
|
}>;
|
|
55
57
|
stream?(opts?: {
|
|
56
58
|
signal?: AbortSignal;
|
|
@@ -64,7 +66,12 @@ export type ApprovalsFeedMode = 'push' | 'poll' | 'idle';
|
|
|
64
66
|
export interface ApprovalsFeedSnapshot {
|
|
65
67
|
/** 权威 pending 列表(**恒来自 `list()`**,不是 delta 拼出来的)。 */
|
|
66
68
|
pending: PendingCheckpoint[];
|
|
67
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* 0.72.14:**流内 ask**(含悬挂的子代 ask)的窄读行,同样恒来自 `list()`。
|
|
71
|
+
* 🔴 **键缺席 = 引擎没报这一段**(老引擎),不是「没有」;引擎报了且为空 ⇒ `[]`。读法见 `livePendingAsk.ts`。
|
|
72
|
+
*/
|
|
73
|
+
livePending?: LivePendingAskView[];
|
|
74
|
+
/** 这次快照是被哪条腿触发的(对账节拍触发的取件记 `'poll'`:它就是一次定时 `list()`)。 */
|
|
68
75
|
mode: 'push' | 'poll';
|
|
69
76
|
/** 单调递增的修订号(端可用它判「我看到的是不是最新的」)。 */
|
|
70
77
|
revision: number;
|
|
@@ -82,6 +89,24 @@ export interface ApprovalsFeedStats {
|
|
|
82
89
|
streamFailures: number;
|
|
83
90
|
/** 真正发给订阅者的快照数(内容无变化时不发 ⇒ 这个数会小于 polls+pushEvents)。 */
|
|
84
91
|
snapshots: number;
|
|
92
|
+
/** 0.72.14:对账节拍真取件的次数(与 `polls` 分开记 —— 它不属于回落轮询腿)。 */
|
|
93
|
+
reconciles: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 0.72.14 **对账节拍**:悬挂的流内 ask 出现 / 结算时,较老的引擎不在 `approvals.stream` 上发任何事件 ⇒ push 腿连着也
|
|
97
|
+
* 看不见它;而「仅活跃 run 时轮询」的 `shouldPoll` 恰好在宿主 turn 结束后关着。本旋钮给宿主一个**独立**的闸:
|
|
98
|
+
* `shouldReconcile()` 为真(典型:会话有活跃后台代理 ∧ 没有活着的宿主 run)期间,无论当前是哪条腿都定时重取 `list()`。
|
|
99
|
+
* · 闸关 ⇒ 零额外取件;不传本旋钮 ⇒ 连定时器都不起,行为与此前一字不差。
|
|
100
|
+
* · 取件后快照没变 ⇒ 间隔翻倍直到 `maxIntervalMs`;变了 / 闸由关转开 ⇒ 回到 `intervalMs`。
|
|
101
|
+
* · 谓词抛错 / 返回非布尔 ⇒ 按「查」处理(与 `shouldPoll` 同一个 fail-safe 方向:少查一次 = 一只等人的 ask 迟迟不出现)。
|
|
102
|
+
* · 引擎开始在 stream 上为这一段发事件之后,push 腿自己就会重取,本节拍只剩兜底意义,宿主可以不再传。
|
|
103
|
+
*/
|
|
104
|
+
export interface ApprovalsFeedReconcileOptions {
|
|
105
|
+
shouldReconcile: () => boolean;
|
|
106
|
+
/** 基础间隔;缺省 5000ms。 */
|
|
107
|
+
intervalMs?: number;
|
|
108
|
+
/** 退避上限;缺省 30_000ms。 */
|
|
109
|
+
maxIntervalMs?: number;
|
|
85
110
|
}
|
|
86
111
|
export interface ApprovalsFeedOptions {
|
|
87
112
|
/** 回落轮询间隔;缺省 5000ms。 */
|
|
@@ -107,6 +132,8 @@ export interface ApprovalsFeedOptions {
|
|
|
107
132
|
* · **`refresh()`** 不受管 —— 那是端自己按下的显式动作。
|
|
108
133
|
*/
|
|
109
134
|
shouldPoll?: () => boolean;
|
|
135
|
+
/** 0.72.14:对账节拍(见 {@link ApprovalsFeedReconcileOptions})。缺省不起。 */
|
|
136
|
+
reconcile?: ApprovalsFeedReconcileOptions;
|
|
110
137
|
signal?: AbortSignal;
|
|
111
138
|
}
|
|
112
139
|
export interface ApprovalsFeedHandle {
|