@sema-agent/client-core 0.72.12 → 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 +17 -0
- package/README.md +2 -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/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.js +28 -17
- package/docs/INTEGRATION-CLIENTS.md +48 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,23 @@
|
|
|
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
|
+
|
|
52
69
|
## 0.72.12(2026-09-18)
|
|
53
70
|
|
|
54
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。
|
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
|
|
@@ -312,6 +312,7 @@ 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 |
|
|
315
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 |
|
|
316
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 |
|
|
317
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 |
|
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 已发过
|
|
@@ -138,7 +138,18 @@ export interface PlanReviewOutcome {
|
|
|
138
138
|
decision: 'approve' | 'edit' | 'reject';
|
|
139
139
|
editedPlan?: string;
|
|
140
140
|
reason?: string;
|
|
141
|
+
/**
|
|
142
|
+
* 0.72.13 CC-46(engine ≥7.86.0;契约 §4c):批准之后这条任务继续用的 permission mode。**只配 `approve`**;
|
|
143
|
+
* 缺席 = 键不落(老壳姿势,引擎按 `default`)。🔴 适用面读不到 wire:只有**提交这条任务的那一方**知道它是不是
|
|
144
|
+
* `permissionMode:"plan"` 只读起步的 —— 不是只读起步的任务送这一位会吃 400 `request.field_conflict`,那次批准整个送不出去。
|
|
145
|
+
*/
|
|
146
|
+
permissionModeAfter?: PlanReviewModeAfter;
|
|
141
147
|
}
|
|
148
|
+
/** 契约 §4c `permissionModeAfter` 的闭集两词(`acceptEdits` = 工作目录内的写不再逐次征询;`default` = 每次写照常征询)。 */
|
|
149
|
+
export declare const PLAN_REVIEW_MODE_AFTER_WORDS: readonly ["default", "acceptEdits"];
|
|
150
|
+
export type PlanReviewModeAfter = (typeof PLAN_REVIEW_MODE_AFTER_WORDS)[number];
|
|
151
|
+
/** 这个值是不是闭集里的词(词表外 —— 含 `plan` / `bypassPermissions` / `auto` —— 引擎 400:一次批准不许顺带关掉征询面)。 */
|
|
152
|
+
export declare function isPlanReviewModeAfter(v: string | undefined): v is PlanReviewModeAfter;
|
|
142
153
|
/**
|
|
143
154
|
* {@link HitlSafetyError.code} 的闭集(0.60.0 抽名,**成员一个没动**)。
|
|
144
155
|
*
|
|
@@ -147,7 +158,7 @@ export interface PlanReviewOutcome {
|
|
|
147
158
|
* ——抄一份就是给自己立第二个判官(B-025 的病形)。加员仍必须**同批**改齐
|
|
148
159
|
* `parkResolver.GATE_FAILURE_CODES` 白名单(见下方原注)。
|
|
149
160
|
*/
|
|
150
|
-
export type HitlSafetyCode = 'binding_mismatch' | 'no_pending' | 'wrong_gate' | 'bad_plan_edit' | 'empty_answer';
|
|
161
|
+
export type HitlSafetyCode = 'binding_mismatch' | 'no_pending' | 'wrong_gate' | 'bad_plan_edit' | 'bad_plan_mode' | 'empty_answer';
|
|
151
162
|
/**
|
|
152
163
|
* 一次决断链失败**停在哪一段**(0.60.0;异源对抗复审 [medium] 采纳,B-037 候包件 ② 的完整兑现)。
|
|
153
164
|
*
|
package/dist/hitl/hitlBridge.js
CHANGED
|
@@ -35,6 +35,12 @@ export function denyReasonForWire(reason, tag) {
|
|
|
35
35
|
`(server caps approval reason at ${MAX_DENY_REASON_CHARS}; over-cap is a 413 reason_too_large that would drop the DECISION, not just the reason)`);
|
|
36
36
|
return cut;
|
|
37
37
|
}
|
|
38
|
+
/** 契约 §4c `permissionModeAfter` 的闭集两词(`acceptEdits` = 工作目录内的写不再逐次征询;`default` = 每次写照常征询)。 */
|
|
39
|
+
export const PLAN_REVIEW_MODE_AFTER_WORDS = Object.freeze(['default', 'acceptEdits']);
|
|
40
|
+
/** 这个值是不是闭集里的词(词表外 —— 含 `plan` / `bypassPermissions` / `auto` —— 引擎 400:一次批准不许顺带关掉征询面)。 */
|
|
41
|
+
export function isPlanReviewModeAfter(v) {
|
|
42
|
+
return v !== undefined && PLAN_REVIEW_MODE_AFTER_WORDS.includes(v);
|
|
43
|
+
}
|
|
38
44
|
/** A safety stop: a contract law was about to be violated (e.g. a binding mismatch, a wrong-gate route).
|
|
39
45
|
* The caller MUST re-present to the human or surface the error — NEVER silently retry or auto-decide
|
|
40
46
|
* (contract/04 §9.1 fail-closed law). */
|
|
@@ -603,7 +609,21 @@ export class HitlBridge {
|
|
|
603
609
|
else if (outcome.editedPlan !== undefined) {
|
|
604
610
|
throw new HitlSafetyError(`plan-review "${outcome.decision}" must NOT carry editedPlan`, 'bad_plan_edit');
|
|
605
611
|
}
|
|
612
|
+
// 0.72.13 CC-46:permissionModeAfter invariant(契约 §4c)—— 只配 approve、闭集两词;坏形本地拦,一个字节都不出门
|
|
613
|
+
// (server 会 400 field_conflict / field_invalid,而那次决断就整个丢了)。拒文不回显调用方给的值。
|
|
614
|
+
const modeAfter = outcome.permissionModeAfter;
|
|
615
|
+
if (modeAfter !== undefined) {
|
|
616
|
+
if (outcome.decision !== 'approve') {
|
|
617
|
+
throw new HitlSafetyError(`plan-review "${outcome.decision}" must NOT carry permissionModeAfter (it only applies to an approval)`, 'bad_plan_mode');
|
|
618
|
+
}
|
|
619
|
+
if (!isPlanReviewModeAfter(modeAfter)) {
|
|
620
|
+
throw new HitlSafetyError('plan-review permissionModeAfter must be "default" or "acceptEdits"', 'bad_plan_mode');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// sdk 9.6.0 的 PlanReviewRequest 尚未声明 permissionModeAfter ⇒ 交集型加一可选位(不断言、不 any);缺席 = 键不落。
|
|
606
624
|
const req = { decision: outcome.decision };
|
|
625
|
+
if (modeAfter !== undefined && isPlanReviewModeAfter(modeAfter))
|
|
626
|
+
req.permissionModeAfter = modeAfter;
|
|
607
627
|
// eopt:上面的 invariant 检查已经保证 decision==='edit' ⇒ editedPlan 非空,但那道保证隔着一个
|
|
608
628
|
// 已经关闭的 if 块,TS 不跨块记账——这里重新直接判一次 undefined,narrowing 与赋值同一表达式内完成
|
|
609
629
|
// (PlanReviewRequest 是 SDK 外部 wire 型,不能改;诚实缺席 = 键不落,不传 `editedPlan: undefined`)。
|
|
@@ -45,7 +45,7 @@ export declare const MAX_TOTAL_PARKS = 64;
|
|
|
45
45
|
* 双向钉住,任何一边加员而另一边不跟 ⇒ 编译红。此前两边靠人肉同步,而不同步的可观测后果是
|
|
46
46
|
* 「新码被 `isGateFailureCode` 降成 undefined 后静默走文案兜底臂」—— 门里长得跟正常一模一样。
|
|
47
47
|
*/
|
|
48
|
-
export declare const GATE_FAILURE_CODES: readonly ["no_pending", "binding_mismatch", "wrong_gate", "bad_plan_edit", "empty_answer"];
|
|
48
|
+
export declare const GATE_FAILURE_CODES: readonly ["no_pending", "binding_mismatch", "wrong_gate", "bad_plan_edit", "bad_plan_mode", "empty_answer"];
|
|
49
49
|
export type GateFailureCode = (typeof GATE_FAILURE_CODES)[number];
|
|
50
50
|
export type GateOutcome =
|
|
51
51
|
/** `receipt`(B-070 / L-200,0.65.0):这次 decide 的 **200 回执读数** —— 与 fs 腿的
|
|
@@ -98,6 +98,8 @@ export const GATE_FAILURE_CODES = [
|
|
|
98
98
|
'binding_mismatch',
|
|
99
99
|
'wrong_gate',
|
|
100
100
|
'bad_plan_edit',
|
|
101
|
+
/** 0.72.13 CC-46:plan-review 的 permissionModeAfter 坏形(非 approve 带这一位 / 词表外)被本地拦下。 */
|
|
102
|
+
'bad_plan_mode',
|
|
101
103
|
/** FIX②:空作答被本地拦下(空 answers[] / 空 selected[] / 空 header)。 */
|
|
102
104
|
'empty_answer',
|
|
103
105
|
];
|