@sema-agent/client-core 0.72.14 → 0.73.1
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 +45 -0
- package/README.md +2 -1
- package/dist/controlRouter.d.ts +4 -1
- package/dist/controlRouter.js +5 -0
- package/dist/engineAgentPanelStore.js +10 -2
- package/dist/fleet/fleetLedger.d.ts +0 -4
- package/dist/fleet/fleetLedger.js +12 -4
- package/dist/fleet/fleetProjection.d.ts +20 -8
- package/dist/fleet/fleetProjection.js +20 -8
- package/dist/fleetAgentPanelProjection.js +26 -11
- package/dist/hitl/planReviewWire.d.ts +17 -5
- package/dist/hitl/planReviewWire.js +83 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/wireRefusalCopy.d.ts +44 -0
- package/dist/wireRefusalCopy.js +110 -0
- package/dist/workflowClient.js +23 -6
- package/dist/workflowMonitor.d.ts +2 -1
- package/docs/INTEGRATION-CLIENTS.md +90 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,51 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.73.1(2026-09-18)
|
|
53
|
+
|
|
54
|
+
> 主题:两张清账票 —— **CC-57** `decidePlanReview` 的 outcome 尾句按「决断有没有生效」选(黑盒验收抓出的产品关切;pre-existing)+ **CC-56** 两条 wire 拒绝的人话面(cancel 的 409 按机器码分家;提交面 429 用量窗)。**patch**:型面纯 additive(+2 常量 +5 函数,公面 1019 → 1026;测试钩 47 → 48;`ControlSafetyCode` 闭集 +1 `cancel_run_not_running`;零 wire 新键;peer 不动)。接入面 §63。
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- **`decidePlanReview` 的 outcome 尾句不再按 `decision` 无条件追加**(CC-57):此前 approve 恒追加「用 `TaskOutput` 取续跑结果并汇报做了什么」、reject 恒追加「告知用户计划已拒绝并继续规划」,于是本地拒发(零 POST)、无引擎连接、引擎 4xx 拒收、送到了但任务仍锁在同一张门上 —— 这些正文写着 could NOT be sent / did NOT take effect 的路径,后面跟着一句让模型把没生效的决断当成已生效去汇报的指令(同一段 meta prompt 自相矛盾)。现在尾句由新导出 `planReviewOutcomeTail(taskId, decision, effect)` 单点铸,`effect` 六态:`took_effect`(回拉确认离开审批门;尾句逐字同旧)/ `advanced`(推进到另一张门:说还有一张审批在等、不说做完了)/ `still_parked`(送到了但回拉确认仍锁在同一张门上,或引擎按契约整拒且状态不动:计划仍在等)/ `not_sent`(一个字节没送出去:计划仍在等)/ `not_applied`(引擎 4xx 拒收:只知道没生效,**不断言**审批门还在 —— 404 / 并发结算都可能 —— 指路先读任务当前状态)/ `unconfirmed`(回拉不可用且回体无状态 / 5xx / 网络失败:说已递交但效果未能确认,指路 `TaskOutput`;**正文也不再断言成功**)。**缺省是 `unconfirmed`**:任何一条忘了归类的路径都落在最保守的那一句上。
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- **同 taskId 的重复决断投递闩**(下游在真机上定谳:三选卡选「auto-accept edits」后 approve → ok,约 0.9 s 后又出一次 approve → 404;三份现场一致,壳侧投递口已加闩仍未排除):`decidePlanReview` 入口一把 per-taskId 在飞集 —— 第一次 POST **落地前**的第二次 decide 直接拒(错误级留痕,不发第二条 meta prompt);窗到**最终一次** POST 落地为止(`default` 撞 400 后的去键重发仍属原决断,重发期间闩不放),**不盖**其后最长 15 s 的状态回拉与 outcome 投递(引擎收下 approve 后可以立刻续跑又停一张新门,那张门的作答是合法的第二次决断);闩带所有权 token,只有持闩的那次调用能放闩(复审抓的:旧调用回拉完成后的 finally 曾无条件删掉新调用刚取的闩);三条投递路径各一行带 `path=` 的 debug(时间线显示第二次 approve 紧跟在 outcome 注入件起新一轮之后 1 ms,与「新一轮撞 409 → 自愈重开卡 → 卡被立刻作答」同形;铸新题号的重开在 canonical responder 已注销时此前**不留日志**,所以「reopen 零命中」排除不了它)。**一条在复审中撤回的设计**:决断落地后 15 s 的重开冷却 —— 它会把「续跑后马上再 park」与「回拉仍 needs_review」两形的自愈重开变成不可恢复的失败(自愈链对 `reopened:false` 不重投),本版不对重开加任何闸。测试钩 `__resetPlanReviewDecisionLatchForTests`。
|
|
63
|
+
- **`src/wireRefusalCopy.ts`(CC-56)**:
|
|
64
|
+
- **cancel 的 409 按机器码分家**:`cancelConflictFromError(err)` ⇒ `{ kind: 'approval_settled' | 'run_not_running' | 'unrecognized', code? }`(不是 409 ⇒ `null`;码缺席 / 认不出 ⇒ `unrecognized`,原码带出、不猜);`cancelConflictContent(detail)` 三句互异 —— `approval_settled`(park 臂:那扇审批门被并发的决断 / 回收抢先结算了 ⇒ **去看那个决定对任务做了什么**,不提重试)/ `run_not_running`(running 臂:取消旗反复挂不上 ⇒ **再发一次取消**,引擎一个字节没改)/ `unrecognized`(两种可能都摆,不替引擎断言)。常量 `CONFLICT_APPROVAL_SETTLED` / `CONFLICT_RUN_NOT_RUNNING`。
|
|
65
|
+
- **`ControlRouter.cancel` 把两种 409 抛成两个安全码**:`conflict.run_not_running` ⇒ 新码 `cancel_run_not_running`(处置 = 直接重试);`conflict.approval_settled` 与码缺席的 409 仍是 `cancel_lost_race`(行为逐字同旧)。此前只看 409 状态码,两种相反的处置合成一句,其中一半人被指错路。
|
|
66
|
+
- **提交面 429 `usage.window_exhausted` 的读口与人话**:`usageWindowExhaustedFromError(err)` ⇒ `{ code, retryAfterSec? }`(**只认 HTTP 429 载体**:状态 429 或 sdk 的 `UsageWindowExhaustedError` / `RateLimitedError` 名 —— 同码也会落在终态 run 记录上(跑到 turn 边界才被用量窗停下),那一形工作可能已经发生、处置相反,不在本读面;等待量 `retryAfterSec` 优先、`retryAfterMs` 上取整换算、坏形不编数;续跑面的兄弟码 `resume.usage_window_exhausted` ⇒ `null`);`usageWindowExhaustedContent(detail)` 有数带数、无数不编数,说清这是部署的用量窗、原样再发就行,不是请求写错了。此前该码只有常量、无人话铸点、无消费点。
|
|
67
|
+
- 新门 `scripts/run-wire-refusal-copy-test.mjs`(25 格);decide-verify 门 +17 格(④c–④k 尾句六态 + ⑩ 闩六格:在飞拒 / 落地后放行 / 回拉期间放行 / 所有权 token / 去键重发期间不放 / 重开无闸);变异反证十七枚逐格见红。
|
|
68
|
+
|
|
69
|
+
### Declined(记账)
|
|
70
|
+
|
|
71
|
+
- `decide.workflow_park_identity_lost`(cli L-230 第三格):server 7.71.0 引入、7.75.0 退役,sdk 从未建模;端收到未知 `decide.*` 码走开集通用行。本包不为已退役码留位。
|
|
72
|
+
- cancel 409 回体顶层 `status`(行的真词):sdk 的错误类不承载回体附加键,本包读不到 ⇒ 本版不出这一位。
|
|
73
|
+
|
|
74
|
+
## 0.73.0(2026-09-18)
|
|
75
|
+
|
|
76
|
+
> 主题:**fleet 视图的数值键不再把「不知道」编成 0**(CC-52;审计 #62 A-18,「缺席被折成正向值」同族的数值形)。**minor,型面 BREAKING**:`FleetTaskView` 的 `elapsedMs` / `tokens` / `queuedCount` 与 `FleetWorkflowView` 的 `doneCount` / `totalCount` / `elapsedMs` / `tokens` / `failedCount`、以及 `WorkflowRunState.totalTokens` 由必填改**可选** —— 消费方 `tsc` 会红一次(`number | undefined`),那正是通知。公面导出名零增减(1019);零 wire 新键;peer 不动。接入面 §62。
|
|
77
|
+
|
|
78
|
+
### Changed(BREAKING)
|
|
79
|
+
|
|
80
|
+
- **八个数值键:有键 ⇒ 真值(含真 0);缺键 ⇒ 不知道**。此前 `projectTasks` / `projectWorkflows` 对 wire 缺席一律 `?? 0`,于是屏上的「0s」「0 tokens」「0/0 agents done」里有一部分是本包替引擎编的。现在 wire 缺席 ⇒ 视图键缺席;坏形(负数 / NaN / Infinity / 非数;计数位另加非整数)同样缺席,不折 0 也不原样过境。sdk 的行型上这些键本来就是可选,本版只是不再在投影层把可选抹掉。
|
|
81
|
+
- **逐键缺席语义**(端按语义分渲,不是一律「—」):
|
|
82
|
+
- `FleetTaskView.elapsedMs` 缺席 = **这一行没有时长基数**:既没有 launch 锚 `startedAt`、也没有引擎快照 `elapsedMs`(很老的引擎 / 极早期首帧)。渲「—」。有 `startedAt`(且不晚于本端时钟)的行台账自算 `now - startedAt`;`startedAt` 晚于本端时钟的行退到引擎快照 + 空窗,连快照也没有就缺席。
|
|
83
|
+
- `FleetTaskView.tokens` 缺席 = **引擎还没为这一行报过用量**(排队中 / 刚起 / 该车道不报)。渲「—」或不渲这一段;**不是** 0 tokens。
|
|
84
|
+
- `FleetTaskView.queuedCount` 缺席 = **引擎没报这一位**(本包不保证任何引擎版本上报它;sdk 行型上它是可选整数)。缺席 ⇒ 不渲排队徽标、不渲 `0 queued`;有键即真值。
|
|
85
|
+
- `FleetWorkflowView.doneCount` / `totalCount` / `failedCount` / `tokens` / `elapsedMs` 缺席 = **本拍没报**:现役引擎的 workflow 行铸点五键恒发,缺席只出现在更老的引擎或乱序的部分增量上。渲「—」;🔴 **不互相推导**(`failedCount` 缺席不读作 0;`totalCount` 缺席不拿 `doneCount` 顶;`startedCount` 缺席时 `doneCount + failedCount` 这个下界只在两键都在场时成立)。
|
|
86
|
+
- **台账的帧间时钟补偿只给有基数的行推进**(`fleetLedger` 两处):基数 = 引擎快照 `elapsedMs`,且**先验再算**(非负有限数;负基数加上空窗会被洗成一个看着合法的正数)。基数缺席时此前 `(elapsedMs ?? 0) + 本端空窗` 把「本端收到这一行多久了」渲成「它跑了多久」—— 那是本端的钟,不是这一行的时长 ⇒ 现在保持缺席。有 `startedAt` 的行(现役引擎恒带)走 `now - startedAt`,不受影响。
|
|
87
|
+
|
|
88
|
+
- **同轴两条读面一并改**(发车前异源复审查漏抓出;它们不经过上面两只投影函数):
|
|
89
|
+
- **agent 面板投影** `projectFleetAgentRows`:行上没有 `tokens` 时此前发布 `fleet-row{ totalTokens: 0 }`,已知用量之后来一帧缺席又发布 0 把它清零。现在缺席 / 坏形 ⇒ `totalTokens` 键不落;已知值在**同一周期内**粘住不被缺席帧覆盖(行已 settle 后同 id 复活、或 launch 锚换了 = 新周期,不继承上一周期的数);终态帧只有用量变了也会在 `end` 之前先发一条带最终用量的 `fleet-row`(消费端通常不再更新非 running 行);同周期内每一条 `fleet-row` 都带上本周期已知的 `startedAt`(不只是它变化的那一帧),未订阅期间的事件缓冲据此把 launch 锚变化当作合并屏障(跨周期不逐键合并,回放与在线投递同果);🔴 `fleet-row` 是部分帧(省略的键 = 沿用行上旧值),`startedAt` 变了时清掉行上旧用量是消费端的义务;引擎真报的 0 逐字发布。(事件型上该键本来就是可选,类型面零变化。)
|
|
90
|
+
- **workflow monitor** `projectWorkflowRun`:`WorkflowRunState.totalTokens` 由必填改**可选**(BREAKING)。有键 ⇒ 一个**完整**的总数。引擎聚合 `stats.tokens` 合法 ⇒ 用它(有 nested 载体时其 `tokens` 也得合法才相加);聚合残缺(own 缺席而 nested 在场、或 nested 载体在场但读不出数)⇒ 键缺席,不按 0 补齐;两者都没有时只在**每条腿都报过合法用量**时才求和;有腿未报 / 非法数(负 / NaN / 小数)/ 零条腿 ⇒ 键缺席。此前未报的腿按 0 加进去,monitor 显示一个确定的「0」或偏小的总数。
|
|
91
|
+
|
|
92
|
+
### 取舍
|
|
93
|
+
|
|
94
|
+
- 这是一次**故意的编译期打断**:additive 的做法(另加一组 `*Known` 布尔)会让老代码继续把编出来的 0 渲上屏,而这一类病的全部问题就是「没人知道那个 0 是编的」。三端在预告期内均未要求延后。
|
|
95
|
+
- 🔴 **不改消费代码只换包不是安全的**:类型检查会红,但绕过类型(纯 JS 调用 / 只转译不查型的构建)时,运行期的后果不止是屏上出现 `undefined` / `NaN` 字样 —— **凡是拿这些键做算术或阈值比较的地方会静默改变行为**。典型形:规模告警一类的判定里 `totalCount` / `tokens` 缺席 ⇒ 比较恒 false、预测量为 NaN ⇒ **告警不再触发**(此前编出来的 0 反而会让它触发)。迁移要求见 §62y:未知的规模不能被当作规模正常。
|
|
96
|
+
|
|
52
97
|
## 0.72.14(2026-09-18)
|
|
53
98
|
|
|
54
99
|
> 主题:**悬挂的流内 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。
|
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.
|
|
38
|
+
**Version:** 0.73.1
|
|
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
|
|
@@ -309,6 +309,7 @@ public-surface guard checks that last one).
|
|
|
309
309
|
| `scripts/run-posture-knob-projection-test.mjs` | The three deployment knobs on the operator face (`serverGates.durableApproval` / `streamAskWindowMs` / `sessionAutoTitle`, engine >=7.67.0), each read as a value **plus who set it plus one operator-facing pointer** rather than a bare value — a bare boolean cannot answer why this particular machine is on this setting or how to pin it back, and a default that flips with the deployment shape is invisible without that. A worker too old to report readings still sends a bare boolean; the reader folds it into the same shell so consumers keep one branch, but raises a `legacy` bit, answers `undefined` from the machine-readable source accessor, and mints a sentence that contains no source word at all — claiming a source nobody reported is worse than admitting the worker cannot say. The other two knobs are honestly absent on such a worker rather than defaulted, a malformed side knob drops only itself while the anchor knob drops the whole reading, and the four sentences are pinned literally distinct so an operator can tell "not observed" from "not reported" from a real value. The last leg reads the installed SDK's `openapi.yaml` and `types.d.ts` directly, including a pin that exactly one knob on this face is numeric — the premise the millisecond-to-prose rendering rests on |
|
|
310
310
|
| `scripts/run-terminal-facts-projection-test.mjs` | The four unconsumed terminal-receipt facts: `TaskResult.effectiveReasoning` / `effectiveMemoryScopes` are narrowed into `_sema_effective_reasoning` / `_sema_effective_memory_scopes` on the CC-shaped `result` (success and error envelopes alike; a malformed value mints nothing, never a default tier), the resume **reopen** family (`resume.env_failed` / `tool_unavailable` / `tool_contract_mismatch`) is a frozen closed set with a reader and three-sentence copy that is disjoint from the refusal and retry-later sets, and `routePairingVerdict` reads `ModelInfo.routePairing` as ok / broken / unknown without policing the open set. |
|
|
311
311
|
| `scripts/run-export-liveness-test.mjs` | Every runtime export in the public baseline must be **alive**: referenced by some gate, or explicitly registered in `scripts/export-liveness.json` as `contract` (consumed by a client with no gate yet), `internal` (an internal helper amplified onto the public surface by `export *`), `candidate` (with ticket + retire-by) or `retire` (dead; retire-by version). Registration is accounting, not exemption: a row for a name a gate already references is stale and must go, a row for a name no longer exported is red, `retire`/`candidate` rows go red the moment `package.json` reaches their retire-by version, and the row count only ratchets down. When the sibling client trees are on disk the consumption evidence is checked by name — a `contract` row's claimed consumers must equal the real set, and a `retire` name must not be imported by any client. Names that have already left the surface are kept in a per-version `removed` ledger: they must never reappear in the baseline or the registry, and the ledger's versions must not run ahead of the changelog. |
|
|
312
|
+
| `scripts/run-wire-refusal-copy-test.mjs` | Two wire refusals read the same way on every client: a cancel's 409 carries one of two codes with opposite dispositions (`conflict.approval_settled` — someone else already decided, go read the result; `conflict.run_not_running` — nothing changed, send the cancel again), an unrecognised or codeless 409 is reported as such rather than guessed, and the submit-side 429 `usage.window_exhausted` is read as a waitable refusal whose wait is stated only when the engine supplied one. `ControlRouter.cancel` raises a distinct safety code for the retry-directly case. |
|
|
312
313
|
| `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
314
|
| `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
315
|
| `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 |
|
package/dist/controlRouter.d.ts
CHANGED
|
@@ -120,8 +120,11 @@ export type ControlSafetyCode =
|
|
|
120
120
|
/** 开集兜底:`steering.` 前缀但本表不认得的**未来**码(SDK 前缀分派同款姿势)。
|
|
121
121
|
* 处置=按「这条 steer 没落地」呈现,并把 `cause.errorCode` 原样打进日志,别按成员猜语义。 */
|
|
122
122
|
| 'steering_other'
|
|
123
|
-
/** cancel 的 409:挂起的 gate 被并发决定/过期(重读状态后重试 cancel)。 */
|
|
123
|
+
/** cancel 的 409:挂起的 gate 被并发决定/过期(重读状态后重试 cancel)。码缺席 / 认不出的 409 也落这一位(行为同旧)。 */
|
|
124
124
|
| 'cancel_lost_race'
|
|
125
|
+
/** 0.73.1:cancel 的 409 `conflict.run_not_running` —— 取消旗反复挂不上(行在 running 与 park 之间来回跳);引擎一个字节没改,处置 = **直接重试**。
|
|
126
|
+
* 与 `cancel_lost_race` 处置相反(那一位是「别人已经替这条 run 做了决定,先去看结果」),所以各占一个码。 */
|
|
127
|
+
| 'cancel_run_not_running'
|
|
125
128
|
/** 404 — non-owner / unknown run(无存在性谕示器)。 */
|
|
126
129
|
| 'not_found';
|
|
127
130
|
export declare class ControlSafetyError extends Error {
|
package/dist/controlRouter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelConflictContent, cancelConflictFromError } from './wireRefusalCopy.js';
|
|
1
2
|
import { uuid } from './adapter/types.js';
|
|
2
3
|
export class ControlSafetyError extends Error {
|
|
3
4
|
code;
|
|
@@ -147,6 +148,10 @@ export class ControlRouter {
|
|
|
147
148
|
return await this.client.runs.cancel(this.taskId, opts?.signal !== undefined ? { signal: opts.signal } : undefined);
|
|
148
149
|
}
|
|
149
150
|
catch (e) {
|
|
151
|
+
// 0.73.1:409 先按机器码分家(见 wireRefusalCopy.ts 头注);码缺席 / 认不出 ⇒ 落到下面那条旧臂,行为逐字同旧。
|
|
152
|
+
if (cancelConflictFromError(e)?.kind === 'run_not_running') {
|
|
153
|
+
throw new ControlSafetyError(`runs.cancel 409 conflict.run_not_running — ${cancelConflictContent({ kind: 'run_not_running' })}`, 'cancel_run_not_running', e);
|
|
154
|
+
}
|
|
150
155
|
if (isCancelSuspendedConflict(e)) {
|
|
151
156
|
throw new ControlSafetyError('runs.cancel 409 — the pending gate was decided or expired concurrently (lost CAS race); ' +
|
|
152
157
|
're-read the run state and retry cancel if it is still active', 'cancel_lost_race', e);
|
|
@@ -52,6 +52,12 @@ export function isEnginePanelTaskResident(taskId) {
|
|
|
52
52
|
const MAX_BUFFER = 200;
|
|
53
53
|
let listener = null;
|
|
54
54
|
const buffer = [];
|
|
55
|
+
/** 两条 fleet-row 都带 launch 锚且不相等 ⇒ 新周期(任一条没带锚 = 这一帧没说,不算变化)。 */
|
|
56
|
+
function launchAnchorChanged(prev, next) {
|
|
57
|
+
if (prev.kind !== 'fleet-row' || next.kind !== 'fleet-row')
|
|
58
|
+
return false;
|
|
59
|
+
return prev.startedAt !== undefined && next.startedAt !== undefined && prev.startedAt !== next.startedAt;
|
|
60
|
+
}
|
|
55
61
|
export function publishEngineAgentPanelEvent(ev) {
|
|
56
62
|
// 同 taskId 的任何主通道事实(tick / fleet-row = 行在场;end = 真终态)都让**缓冲里**那条缺席过期 —— 迟订阅的消费者
|
|
57
63
|
// 不许先恢复了行、再被一条旧缺席标成 unknown(已递达订阅者的缺席由订阅者按契约在这些事件上撤标)。
|
|
@@ -123,9 +129,11 @@ export function publishEngineAgentPanelEvent(ev) {
|
|
|
123
129
|
}
|
|
124
130
|
if (b.taskId !== taskId)
|
|
125
131
|
continue; // another task's progress — irrelevant to this row
|
|
126
|
-
|
|
132
|
+
// 0.73.0:launch 锚变了 = 同一个 taskId 的**新周期**,也是一道屏障。逐键合并的「省略的键沿用旧值」只在同一周期内
|
|
133
|
+
// 成立;跨周期合并会把上一周期的 totalTokens / toolUses 继承进新周期(投影层刻意省略的键被缓冲层补了回来)。
|
|
134
|
+
if (b.kind === kind && !launchAnchorChanged(b, ev))
|
|
127
135
|
i = at;
|
|
128
|
-
break; // same lane ⇒ merge target; other lane ⇒ barrier
|
|
136
|
+
break; // same lane & same cycle ⇒ merge target; other lane / new cycle ⇒ barrier
|
|
129
137
|
}
|
|
130
138
|
if (i >= 0) {
|
|
131
139
|
// 🔴 Coalescing must not DROP sticky keys either (0.41.0 adversarial review #4). Both progress
|
|
@@ -385,10 +385,6 @@ export interface FleetLedger {
|
|
|
385
385
|
/** 测试直取:当前 task 行原样(顺序 = 插入序)。 */
|
|
386
386
|
__rowsForTest(): FleetTaskRow[];
|
|
387
387
|
}
|
|
388
|
-
/**
|
|
389
|
-
* 建一个 fleet 帧台账。**不开流、不构造 client** —— 宿主的 consume 循环把解码后的帧喂进来。
|
|
390
|
-
* (这条切缝就是设计稿 §2.5 给 fleetClient 划的那一刀:帧体归库、连接归端。)
|
|
391
|
-
*/
|
|
392
388
|
export declare function createFleetLedger(hooks?: FleetLedgerHooks, opts?: CreateFleetLedgerOptions): FleetLedger;
|
|
393
389
|
/**
|
|
394
390
|
* `bg_notification` 帧的通知体 wire 形(SDK `FleetFrame` 联合的具名臂投影)。
|
|
@@ -109,6 +109,10 @@ export function fleetSnapshotOptions(opts) {
|
|
|
109
109
|
* 建一个 fleet 帧台账。**不开流、不构造 client** —— 宿主的 consume 循环把解码后的帧喂进来。
|
|
110
110
|
* (这条切缝就是设计稿 §2.5 给 fleetClient 划的那一刀:帧体归库、连接归端。)
|
|
111
111
|
*/
|
|
112
|
+
/** 补偿链的基数判据:非负有限数才算(先验再算 —— 负基数加上空窗会被洗成一个看着合法的正数;异源对抗复审)。 */
|
|
113
|
+
function isElapsedBase(v) {
|
|
114
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
115
|
+
}
|
|
112
116
|
export function createFleetLedger(hooks = {}, opts = {}) {
|
|
113
117
|
const sessionKey = opts.sessionKey ?? DEFAULT_SESSION_KEY;
|
|
114
118
|
const nowFn = opts.nowFn ?? Date.now;
|
|
@@ -289,7 +293,7 @@ export function createFleetLedger(hooks = {}, opts = {}) {
|
|
|
289
293
|
rowMeta.set(row.id, { receivedAtMs });
|
|
290
294
|
debug(`[fleet-row] id=${row.id} parent=${row.parentId ?? '-'} agentType=${row.agentType ?? '-'}` +
|
|
291
295
|
` agentName=${row.agentName ?? '-'} name=${row.name ?? '-'} status=${row.status ?? '-'}` +
|
|
292
|
-
` tokens=${row.tokens ??
|
|
296
|
+
` tokens=${row.tokens ?? '-'} parentToolCallId=${row.parentToolCallId ?? '-'}`);
|
|
293
297
|
// [1491] subagentOutput 寻址:子代行出生帧带 parentId(spawn run)——记映射供查看态终报读面。
|
|
294
298
|
// 归属判别与通知臂同锚(R1/F1,2026-07-22):
|
|
295
299
|
// · parent ∈ own-run 台账 → own:登映射 + 把本行 taskId 也登进台账(F1 闭包 —— 嵌套孙代的
|
|
@@ -586,8 +590,8 @@ export function createFleetLedger(hooks = {}, opts = {}) {
|
|
|
586
590
|
const meta = rowMeta.get(id);
|
|
587
591
|
const frozenElapsed = typeof started === 'number' && started > 0 && started <= nowMs
|
|
588
592
|
? nowMs - started
|
|
589
|
-
: meta !== undefined
|
|
590
|
-
?
|
|
593
|
+
: meta !== undefined && isElapsedBase(known.elapsedMs)
|
|
594
|
+
? known.elapsedMs + Math.max(0, nowMs - meta.receivedAtMs) // 0.73.0:无基数不铸(同 project() 的补偿链)
|
|
591
595
|
: known.elapsedMs;
|
|
592
596
|
taskMap.delete(id);
|
|
593
597
|
rowMeta.delete(id);
|
|
@@ -707,7 +711,11 @@ export function createFleetLedger(hooks = {}, opts = {}) {
|
|
|
707
711
|
if (!meta)
|
|
708
712
|
return r;
|
|
709
713
|
const idle = Math.max(0, nowMs - meta.receivedAtMs);
|
|
710
|
-
|
|
714
|
+
// 0.73.0:补偿链只给**真有基数**的行推进(基数 = 引擎快照 elapsedMs)。基数缺席时 `?? 0` 会把「本端收到这一行多久了」
|
|
715
|
+
// 渲成「它跑了多久」—— 那是本端的钟,不是这一行的时长 ⇒ 保持缺席。
|
|
716
|
+
if (!isElapsedBase(r.elapsedMs))
|
|
717
|
+
return r;
|
|
718
|
+
return idle > 0 ? { ...r, elapsedMs: r.elapsedMs + idle } : r;
|
|
711
719
|
});
|
|
712
720
|
// ② 终态留存池:live 集没有的行在宽限窗内继续投影;被查看中的行不过期。
|
|
713
721
|
const retainedRows = [];
|
|
@@ -46,10 +46,17 @@ export interface FleetTaskView {
|
|
|
46
46
|
depth: number;
|
|
47
47
|
parentId?: string;
|
|
48
48
|
status: FleetTaskStatus;
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* 🔴 0.73.0(BREAKING):三个数值键由必填改**可选** —— 有键 ⇒ 真值(含真 0);缺键 ⇒ 「不知道」。此前 `?? 0` 把 wire
|
|
51
|
+
* 缺席编成 0(「0 秒」「0 tokens」「0 个排队」都是本包替引擎编的话)。坏形(负数 / NaN / 非数)同样缺席。
|
|
52
|
+
* · `elapsedMs` 缺席 = 这一行既没有 launch 锚也没有引擎快照(极早期的首帧 / 很老的引擎);渲「—」,不渲 0s。
|
|
53
|
+
* · `tokens` 缺席 = 引擎还没为这一行报过用量(排队中 / 刚起 / 该车道不报);渲「—」,不渲 0。
|
|
54
|
+
* · `queuedCount` 缺席 = 引擎没报这一位(本包不保证任何引擎版本上报它);不渲排队徽标,不渲「0 queued」。
|
|
55
|
+
*/
|
|
56
|
+
elapsedMs?: number;
|
|
57
|
+
tokens?: number;
|
|
51
58
|
tokenDir: 'down' | 'up';
|
|
52
|
-
queuedCount
|
|
59
|
+
queuedCount?: number;
|
|
53
60
|
/** REF-CC-050(fleet2-12):投影器无条件赋值(`?? false`),门 `ALWAYS_EMITTED_TASK_VIEW_KEYS`
|
|
54
61
|
* 已把它钉成「恒发」——类型必填而不是留一条测不到的缺席分支。 */
|
|
55
62
|
awaitingPlanApproval: boolean;
|
|
@@ -121,11 +128,16 @@ export interface FleetWorkflowView {
|
|
|
121
128
|
name: string;
|
|
122
129
|
description: string;
|
|
123
130
|
status: FleetTaskStatus;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
/**
|
|
132
|
+
* 🔴 0.73.0(BREAKING):五个数值键由必填改**可选**(读法同 {@link FleetTaskView.elapsedMs})。现役引擎的 workflow 行
|
|
133
|
+
* 铸点五键**恒发**,缺席只出现在更老的引擎 / 乱序的部分增量上 —— 那时渲「—」,**不互相推导**
|
|
134
|
+
* (`failedCount` 缺席不读作 0;`totalCount` 缺席不拿 `doneCount` 顶)。
|
|
135
|
+
*/
|
|
136
|
+
doneCount?: number;
|
|
137
|
+
totalCount?: number;
|
|
138
|
+
elapsedMs?: number;
|
|
139
|
+
tokens?: number;
|
|
140
|
+
failedCount?: number;
|
|
129
141
|
/** 🔴 缺席 ≠ 0:0 = 引擎说「一个都没起」,undefined = 老引擎「不知道」(退 done+failed 下界)。 */
|
|
130
142
|
startedCount?: number;
|
|
131
143
|
}
|
|
@@ -327,11 +327,11 @@ export function projectTasks(allRows, opts) {
|
|
|
327
327
|
depth: depthOf(r),
|
|
328
328
|
...(parentId !== undefined ? { parentId } : {}),
|
|
329
329
|
status: coerceTaskStatus(r.status),
|
|
330
|
-
elapsedMs
|
|
331
|
-
tokens
|
|
330
|
+
...wireNumberKey('elapsedMs', wireDuration(r.elapsedMs)),
|
|
331
|
+
...wireNumberKey('tokens', wireCount(r.tokens)),
|
|
332
332
|
// 🔴 wire 从无 tokenDir(#50 门禁盘出的真类型错:读点恒 undefined 曾靠 ?? 掩住)——恒壳侧缺省。
|
|
333
333
|
tokenDir: 'down',
|
|
334
|
-
queuedCount
|
|
334
|
+
...wireNumberKey('queuedCount', wireCount(r.queuedCount)),
|
|
335
335
|
awaitingPlanApproval: r.awaitingPlanApproval ?? false,
|
|
336
336
|
descendantCount: childCount.get(r.id) ?? 0,
|
|
337
337
|
// 198 isViewed:这一行就是被查看的那个 agent(行 id 尾段 = 引擎 taskId)。
|
|
@@ -357,6 +357,18 @@ export function projectTasks(allRows, opts) {
|
|
|
357
357
|
return task;
|
|
358
358
|
});
|
|
359
359
|
}
|
|
360
|
+
/** 计数位的窄读:非负**整数**才算数(0 是真 0);其余(缺席 / 负数 / 小数 / NaN / Infinity / 非数)⇒ `undefined`。 */
|
|
361
|
+
function wireCount(v) {
|
|
362
|
+
return typeof v === 'number' && Number.isInteger(v) && v >= 0 ? v : undefined;
|
|
363
|
+
}
|
|
364
|
+
/** 时长位的窄读:非负有限数才算数;其余 ⇒ `undefined`。 */
|
|
365
|
+
function wireDuration(v) {
|
|
366
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined;
|
|
367
|
+
}
|
|
368
|
+
/** 有值才落键(缺席 ⇒ 空对象 ⇒ 视图上键缺席,不铸 0)。 */
|
|
369
|
+
function wireNumberKey(key, v) {
|
|
370
|
+
return (v !== undefined ? { [key]: v } : {});
|
|
371
|
+
}
|
|
360
372
|
/** wire workflow 行 → 渲染行。 */
|
|
361
373
|
export function projectWorkflows(rows) {
|
|
362
374
|
return rows.map(r => {
|
|
@@ -367,11 +379,11 @@ export function projectWorkflows(rows) {
|
|
|
367
379
|
name: collapseLabel(r.name ?? '') || 'Dynamic workflow',
|
|
368
380
|
description: projectDescription(r.description),
|
|
369
381
|
status: coerceWorkflowStatus(r.status),
|
|
370
|
-
doneCount
|
|
371
|
-
totalCount
|
|
372
|
-
elapsedMs
|
|
373
|
-
tokens
|
|
374
|
-
failedCount
|
|
382
|
+
...wireNumberKey('doneCount', wireCount(r.doneCount)),
|
|
383
|
+
...wireNumberKey('totalCount', wireCount(r.totalCount)),
|
|
384
|
+
...wireNumberKey('elapsedMs', wireDuration(r.elapsedMs)),
|
|
385
|
+
...wireNumberKey('tokens', wireCount(r.tokens)),
|
|
386
|
+
...wireNumberKey('failedCount', wireCount(r.failedCount)),
|
|
375
387
|
...(startedCount !== undefined ? { startedCount } : {}),
|
|
376
388
|
};
|
|
377
389
|
});
|
|
@@ -134,7 +134,10 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
|
|
|
134
134
|
continue;
|
|
135
135
|
present.add(taskId);
|
|
136
136
|
const status = row.status ?? 'running';
|
|
137
|
-
|
|
137
|
+
// 0.73.0:三态 —— 帧上真给了非负整数才算数;缺席 / 坏形 ⇒ 沿用已知值(粘性),从没报过就是 undefined。
|
|
138
|
+
// 此前 `?? 0`:首帧发布 `totalTokens:0`(面板渲「0 tokens」),已知之后来一帧缺席又发布 0 把用量清零。
|
|
139
|
+
const wireTokens = typeof row.tokens === 'number' && Number.isInteger(row.tokens) && row.tokens >= 0 ? row.tokens : undefined;
|
|
140
|
+
const tokens = wireTokens; // 粘性在下面(拿到 prev 与周期锚之后)补
|
|
138
141
|
// 🔴 三态直取:`row.toolUses` 缺席就是 undefined,这里**不许**兜 0(见 FleetAgentRowLike 注)。
|
|
139
142
|
const toolUses = typeof row.toolUses === 'number' ? row.toolUses : undefined;
|
|
140
143
|
const transcriptId = row.transcriptId || undefined;
|
|
@@ -142,12 +145,24 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
|
|
|
142
145
|
const currentTool = currentToolKeyOf(row.currentTool) !== undefined ? row.currentTool : undefined;
|
|
143
146
|
const currentToolKey = currentToolKeyOf(row.currentTool);
|
|
144
147
|
const prev = seenMap.get(taskId);
|
|
148
|
+
// 🔴 粘性只在**同一周期**内成立:上一条已 settle(同 id 复活 = 新周期)或 launch 锚换了 ⇒ 不继承上一周期的用量
|
|
149
|
+
// (否则新周期首帧没报 tokens 时会把上一周期的数当成当前读数发布;异源对抗复审)。
|
|
150
|
+
const sameCycle = prev !== undefined &&
|
|
151
|
+
!prev.settled &&
|
|
152
|
+
(startedAt === undefined || prev.startedAt === undefined || prev.startedAt === startedAt);
|
|
153
|
+
const knownTokens = tokens !== undefined ? tokens : sameCycle ? prev?.tokens : undefined;
|
|
154
|
+
// 本周期已知的 launch 锚:帧上给了用帧上的,没给则同周期沿用。🔴 它随**每一条** fleet-row 发布(不只是变化那一帧)——
|
|
155
|
+
// 事件缓冲靠它判「这两条部分帧是不是同一周期」;缺锚的部分帧会让跨周期合并绕过屏障(异源对抗复审 [high])。
|
|
156
|
+
const knownStartedAt = startedAt !== undefined ? startedAt : sameCycle ? prev?.startedAt : undefined;
|
|
145
157
|
if (TERMINAL_FLEET_TASK_STATUSES.has(status)) {
|
|
146
158
|
// ② 终态一次性 settle(幂等台账;消费端 settle 也幂等)。
|
|
147
159
|
if (!prev?.settled) {
|
|
148
160
|
// 终态行也把最后一拍的累计工具数/转录锚带上:详情页在行翻终态后仍要显示真计数与
|
|
149
161
|
// 委派 prompt,而终态帧常常是这一行**唯一**带全 usage 的一帧(bg lane 只在活动边界发行帧)。
|
|
150
|
-
if (
|
|
162
|
+
if (
|
|
163
|
+
// 最终用量也算(终态帧常是唯一带全 usage 的一帧;消费端不更新非 running 行 ⇒ 必须赶在 end 之前发)
|
|
164
|
+
(tokens !== undefined && prev?.tokens !== tokens) ||
|
|
165
|
+
(toolUses !== undefined && prev?.toolUses !== toolUses) ||
|
|
151
166
|
(transcriptId !== undefined && prev?.transcriptId !== transcriptId) ||
|
|
152
167
|
(startedAt !== undefined && prev?.startedAt !== startedAt)) {
|
|
153
168
|
publishEngineAgentPanelEvent({
|
|
@@ -155,10 +170,10 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
|
|
|
155
170
|
taskId,
|
|
156
171
|
...(row.name ? { name: row.name } : {}),
|
|
157
172
|
...(row.description ? { description: row.description } : {}),
|
|
158
|
-
totalTokens:
|
|
173
|
+
...(knownTokens !== undefined ? { totalTokens: knownTokens } : {}),
|
|
159
174
|
...(toolUses !== undefined ? { toolUses } : {}),
|
|
160
175
|
...(transcriptId !== undefined ? { transcriptId } : {}),
|
|
161
|
-
...(
|
|
176
|
+
...(knownStartedAt !== undefined ? { startedAt: knownStartedAt } : {}),
|
|
162
177
|
});
|
|
163
178
|
}
|
|
164
179
|
publishEngineAgentPanelEvent({
|
|
@@ -172,10 +187,10 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
|
|
|
172
187
|
}
|
|
173
188
|
seenMap.set(taskId, {
|
|
174
189
|
status,
|
|
175
|
-
tokens,
|
|
190
|
+
tokens: knownTokens,
|
|
176
191
|
toolUses: toolUses !== undefined ? toolUses : prev?.toolUses,
|
|
177
192
|
transcriptId: transcriptId !== undefined ? transcriptId : prev?.transcriptId,
|
|
178
|
-
startedAt:
|
|
193
|
+
startedAt: knownStartedAt,
|
|
179
194
|
currentToolKey: currentToolKey !== undefined ? currentToolKey : prev?.currentToolKey,
|
|
180
195
|
lastSeenAt: nowMs,
|
|
181
196
|
settled: true,
|
|
@@ -193,7 +208,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
|
|
|
193
208
|
prev.settled ||
|
|
194
209
|
prev.absentReportedAtMs !== undefined || // 行回来了:即使值未变也发一条,消费端据此撤掉 absent 标
|
|
195
210
|
prev.status !== status ||
|
|
196
|
-
prev.tokens !==
|
|
211
|
+
prev.tokens !== knownTokens ||
|
|
197
212
|
(toolUses !== undefined && prev.toolUses !== toolUses) ||
|
|
198
213
|
(transcriptId !== undefined && prev.transcriptId !== transcriptId) ||
|
|
199
214
|
(startedAt !== undefined && prev.startedAt !== startedAt) ||
|
|
@@ -203,21 +218,21 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
|
|
|
203
218
|
taskId,
|
|
204
219
|
...(row.name ? { name: row.name } : {}),
|
|
205
220
|
...(row.description ? { description: row.description } : {}),
|
|
206
|
-
totalTokens:
|
|
221
|
+
...(knownTokens !== undefined ? { totalTokens: knownTokens } : {}),
|
|
207
222
|
// 🔴 缺席即不落键(消费端据「键在不在」判三态)。
|
|
208
223
|
...(toolUses !== undefined ? { toolUses } : {}),
|
|
209
224
|
...(transcriptId !== undefined ? { transcriptId } : {}),
|
|
210
|
-
...(
|
|
225
|
+
...(knownStartedAt !== undefined ? { startedAt: knownStartedAt } : {}),
|
|
211
226
|
...(currentTool !== undefined ? { currentTool } : {}),
|
|
212
227
|
});
|
|
213
228
|
}
|
|
214
229
|
// 台账对这几位是**粘性**的:缺席帧不擦掉已知值(同上的单向语义)。
|
|
215
230
|
seenMap.set(taskId, {
|
|
216
231
|
status,
|
|
217
|
-
tokens,
|
|
232
|
+
tokens: knownTokens,
|
|
218
233
|
toolUses: toolUses !== undefined ? toolUses : prev?.toolUses,
|
|
219
234
|
transcriptId: transcriptId !== undefined ? transcriptId : prev?.transcriptId,
|
|
220
|
-
startedAt:
|
|
235
|
+
startedAt: knownStartedAt,
|
|
221
236
|
currentToolKey: currentToolKey !== undefined ? currentToolKey : prev?.currentToolKey,
|
|
222
237
|
lastSeenAt: nowMs,
|
|
223
238
|
settled: false,
|
|
@@ -127,11 +127,23 @@ export declare function armPlanReviewApproval(result: unknown, sessionKey?: stri
|
|
|
127
127
|
opts?: {
|
|
128
128
|
submittedInPlanMode?: boolean;
|
|
129
129
|
}): boolean;
|
|
130
|
-
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
130
|
+
/**
|
|
131
|
+
* 一次 plan-review 决断**有没有生效**(0.73.1;{@link decidePlanReview} 的 outcome 尾句按它选):
|
|
132
|
+
* · `took_effect` —— 回拉确认任务已离开审批门;
|
|
133
|
+
* · `advanced` —— 决断生效、任务推进到**另一张**审批门(不是做完了);
|
|
134
|
+
* · `still_parked` —— 送到了(200)但回拉确认任务仍锁在同一张门上(计划仍在等,这一形能确定);
|
|
135
|
+
* · `not_sent` —— 一个字节都没送出去(本地拒 / 无连接 / client 造不出)⇒ 计划仍在等,这一形也能确定;
|
|
136
|
+
* · `not_applied` —— 引擎明确拒收(4xx)⇒ 只知道这次没生效,**不知道**审批门还在不在(404 / 并发结算都可能)⇒ 先读任务状态;
|
|
137
|
+
* · `unconfirmed` —— 不知道(回拉不可用且回体无状态 / 5xx / 网络失败)。
|
|
138
|
+
*/
|
|
139
|
+
export type PlanReviewDecisionEffect = 'took_effect' | 'advanced' | 'still_parked' | 'not_sent' | 'not_applied' | 'unconfirmed';
|
|
140
|
+
/**
|
|
141
|
+
* outcome 尾句的**唯一铸点**(给模型的下一步指令)。🔴 只有 `took_effect` 才许说「已批准并汇报做了什么 / 已拒绝」;
|
|
142
|
+
* 其余三态一律不许出现把决断当成已生效的措辞 —— 那会让模型向用户汇报一件没发生的事。
|
|
143
|
+
*/
|
|
144
|
+
export declare function planReviewOutcomeTail(taskId: string, decision: 'approve' | 'reject', effect: PlanReviewDecisionEffect): string;
|
|
145
|
+
/** 测试钩:清投递口闩。 */
|
|
146
|
+
export declare function __resetPlanReviewDecisionLatchForTests(): void;
|
|
135
147
|
export declare function decidePlanReview(taskId: string, decision: 'approve' | 'reject',
|
|
136
148
|
/**
|
|
137
149
|
* 0.72.13 CC-46(engine ≥7.86.0;契约 §4c):批准之后用哪一档。缺席 ⇒ 体逐字节同旧 `{decision}`。只配 approve、闭集两词 ——
|