@sema-agent/client-core 0.73.2 → 0.73.4
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 +38 -0
- package/README.md +5 -3
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +30 -17
- package/dist/adapter/downstream/terminalToSdkResult.js +60 -8
- package/dist/adapter/runStream.js +40 -2
- package/dist/approvalsStreamLiveCapability.d.ts +42 -0
- package/dist/approvalsStreamLiveCapability.js +84 -0
- package/dist/hitl/approvalsFeed.d.ts +12 -0
- package/dist/hitl/approvalsFeed.js +69 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/notifications.js +4 -1
- package/dist/steering.d.ts +2 -1
- package/dist/steering.js +2 -0
- package/dist/subagent/engineTaskHandleWire.d.ts +3 -1
- package/dist/subagent/engineTaskHandleWire.js +11 -2
- package/dist/toolResult.d.ts +4 -0
- package/dist/toolResult.js +21 -4
- package/docs/INTEGRATION-CLIENTS.md +97 -8
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,44 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.73.4(2026-09-19)
|
|
53
|
+
|
|
54
|
+
> 主题:**终帧 `permission_denials` 第三键的来源**(CC-34 重开)、**审批 feed「欠一次看」重试**(CC-62)与 **workflow run 级错误码**(CC-63)。**patch**:型面纯 additive(+1 常量,公面 1032 → 1033;`TerminalStreamObserved` 具名化并 +1 可选位;`SemaPermissionDenial` +1 可选位;TaskOutput 卡 `task` +1 超集键;`ApprovalsFeedOptions` +1 可选位;`ApprovalsFeedStats` +1 计数);零 wire 新键;peer 不动。
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- **更正 0.73.2 的一句发布说明**:该版写「`permission_denials` 自动升级收口」。实情是引擎 ≥7.23.0 只把**调用 id** 串上了拒绝账本(`_sema_permission_denials[].tool_use_id` 自那时起带值),被拒调用的**入参对象**不在账本的任何一面 ⇒ CC 形三键必填的 `permission_denials` 数组在 0.73.2 / 0.73.3 上**仍然恒空**、`_sema_permission_denials_absent: true` 恒在场。黑盒验收按字面判了 FAIL,判得对。
|
|
59
|
+
- **终帧拒绝清单按 `toolCallId` join 同一条流的 `tool_start` 入参**:`runStream` 在流内记「`toolCallId` → `tool_start.args`」本流快照(只存引用;上限 `TOOL_INPUT_JOIN_MAX_CALLS` = 4096,溢出后单向停记;同一个 id 见到两份不同入参 ⇒ 该 id 永久出表),终局作为 `terminalToSdkResult` 第三参的新可选位 `toolInputByCallId` 交给铸点。join 得上 ∧ 入参是普通对象 ∧ **没被传输层动过** ⇒ 该行三键齐,进 CC 数组,并带来源位 `_sema_tool_input_source: 'tool_start'`;每条拒绝都铸成 CC 形时 `_absent` 不落。其余(行无 id / 本流没见过那只帧:重连后的流、宿主自建管线没交快照 / 入参非对象 / 入参字符串叶里带替换记号 `«redacted…»` 或恰为 `[circular]` / `[depth-limit]` / 超深超大)一律留在 `_sema_permission_denials`,`_absent: true` 照落。账本自己带 `toolInput` 的那天,账本那份恒优先。
|
|
60
|
+
- 读法提醒:审批人**改写过入参**的调用,`tool_start` 帧上是改写后的那份;被拒的调用没有改写,所以拒绝清单上的 `tool_input` 就是模型发出的入参。
|
|
61
|
+
- 被替换记号挡掉的行**少铸不错铸**:入参里本来就含这些字面的极端情形会一并被挡,代价是该行只在 sema 载体上。
|
|
62
|
+
- **审批 feed:push 侧取件没看成不再无人补看**(`startApprovalsFeed`)。push 腿收到事件后那一次 `list()` 若失败(抛出,或回体的 `livePending` 段读不懂),此前只计 `listErrors`;其后的 heartbeat 不触发取件 ⇒ 没有别的事件时,那张待批的卡一直不出现。现在记一笔欠账,按 base、2×base、4×base … 退避重试(新可选位 `pushTakeRetryMs`,缺省 1000;间隔封顶 30 s;`<= 0` = 关),**不按次数放弃**;销账只认**比欠账更晚发起**的取件看成(更早发起、迟到回来的成功看的是失败之前的世界,不销账;反过来,更晚发起的一眼已经看成之后才迟到的旧失败不再记欠账);回体坏形让取件整次抛出的(例:`pending` 不是数组),同样记欠账再原样抛出 —— 既有的逃逸语义(轮询腿连续逃逸计数、`refresh()` 的 rejection)不变。起手那次取件与 `refresh()` 同理。新计数 `stats().pushTakeRetries`。
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- **workflow run 级错误码(CC-63)**:引擎(core ≥7.17.1)在 TaskOutput 结构卡**顶层**铸 run 级 `errorCode`(与 `completionId` 同层;有值才带),本包此前只认得模型面 JSON 里 per-agent 的 `agent_runs[].errorCode`。现在 `structuredToToolUseResult` 的 task-output 臂把它原词透传到 `toolUseResult.task._sema_error_code`;缺席 / 空串 / 非字符串 ⇒ 键不铸(run 无码,不编码);内层 `details` 上的同名键不认。只在**结构面**这条腿上有:模型面正文与列表行都不带这一格。
|
|
67
|
+
- `TOOL_INPUT_JOIN_MAX_CALLS`;具名型 `TerminalStreamObserved`(`doneToSdkResult` / `failedToSdkResult` / `terminalToSdkResult` 第三参此前的内联型,结构不变 +1 可选位)。
|
|
68
|
+
- 门:`run-permission-denial-projection-test.mjs` +F 段(join 两半各有正控、五种被动过的形、扫描预算、账本优先、per-stream、两份不同入参、上限与单向闩、公开入口端到端);`run-live-pending-ask-test.mjs` +S3r 段十九格(含四格确定性乱序、三格坏形回体);`run-client-core-pure-test.mjs` +⑩S4d–S4h(run 级错误码)。变异反证二十四枚逐格见红(另三枚等价变异:账本在场时 join 分支本就不走;空 id 由读侧同一道非空闸挡;销账与摘要的先后在逃逸续记欠账后不改变可观察行为)。
|
|
69
|
+
|
|
70
|
+
## 0.73.3(2026-09-19)
|
|
71
|
+
|
|
72
|
+
> 主题:**CC-61 sdk 9.7.1 提货 + `capabilities.approvalsStreamLive` 四态读口**(server 7.87.1)与 **CC-60 五条模型面文案锚的包侧半场**(下游锚登记表 A-D3 / A-D4 / A-D5 / A-D11 / A-D13,自 1.0.116 起挂在本包名下)。**patch**:型面纯 additive(+6 函数,公面 1026 → 1032;测试钩 48 → 49;`EngineTaskStatusAttachment.status` 联合 +`parked`;`parseModelFacingBash` 返回形 +可选 `note`;`mergeTaskOutput` +可选第三参;devDep sdk ^9.7.1,**peer 地板 >=9.6.0 不动**;零 wire 新键)。接入面 §65。
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- **`src/approvalsStreamLiveCapability.ts`(CC-61;server ≥7.87.1 S-455 / sdk 9.7.1)**:`capabilities.approvalsStreamLive`(= checkpointStore ∧ toolApproval)的四态读面,与 sql / writeProtection / webSearch.backend / executionLane 四只兄弟同构六导出 —— `projectApprovalsStreamLiveCapability` / `noteEngineCapsForApprovalsStreamLive` / `observedApprovalsStreamLive` / `approvalsStreamLiveDoctorDetail` / `forgetApprovalsStreamLiveReading` + 测试钩。🔴 **键缺席 = 判不出**(老 server),绝不折 false;严格布尔,畸形删格。**feed 消费方的判据归包**:`livePendingNeedsReconcile(reading)` —— 只有引擎明说会推(present ∧ true)才 `false`(对账节拍降为兜底);位为 false / 判不出 / 从没观测 ⇒ `true`,悬挂的子代 ask 只能靠 `ApprovalsFeedOptions.reconcile` 拉,**别等** `live_pending`。🔴 它是 doctor / 提示面的判据,**不是**关掉对账节拍的开关:推面可靠时对账退避到 `maxIntervalMs` 当兜底 —— push 腿一次瞬时 `list()` 失败今天不重试(复审查出,记 CC-62),对账节拍是唯一补偿。
|
|
77
|
+
- **五条模型面文案锚(CC-60)**:
|
|
78
|
+
- **A-D3** `mergeTaskOutput(prev, fresh, cursorSemantics?)` 优先读机读位(server ≥7.77.0 `taskOutput` 面的 `cursorSemantics`:`"full"` = 全量重读安全 ⇒ 替换;`"cursor"` = 读了就消费 ⇒ 追加);缺席 / 认不出 ⇒ 现行 spool 标记文案臂兜底(**缺席不当 full**)。`fetchEngineTaskOutput` 真把回体**顶层** `cursorSemantics`(sdk `SubagentOutputResult` 上与 `output` 并列;发车前复审抓的:首版读成 `output.cursorSemantics`,正常回体恒回落文案臂)喂进去(门有源码钉 + 上游锚)。
|
|
79
|
+
- **A-D4** `PROTOCOL_MARKERS.taskOutputResultLine` 认可选括号注记 `--- result (partial) ---`(core 被 stop 打断的子代任务此前正文整段切不出)。
|
|
80
|
+
- **A-D5** `PROTOCOL_MARKERS.bashHead` 认 core 两个发货变体:exit1 注记形 `exit code: 1 (<note>)` 与图片形(首行后无 `--- stdout ---`,紧跟 `[Image data detected …]`);注记反解进 `parseModelFacingBash` 的新可选位 `note`,并在 structured 缺席(core `boundedCard` 超 64 KiB 整只丢)时喂给**同一个**良性退出槽 —— 只放宽正则会把「正文丢失」换成「红色错误卡」。structured 在场仍以它为准。
|
|
81
|
+
- **A-D11** 通知去重的 seed 扫描按**两个生产者的并集**判终局:core task-notification(带 `cancelled`)∪ 本包 `enqueueBgChildNotification`(run 行状态面,带 `blocked`)= {completed, failed, killed, blocked, cancelled};`event` 是唯一非终局。两张冻结元组各自不动(动任一张 = 丢掉另一个生产者的词)。此前只借 run 级表 ⇒ `cancelled` 通知在 crash 窗重放时二次喂模型。
|
|
82
|
+
- **A-D13** `PHRASE_TO_STATUS` 认 core 第四个 vbl 臂 `parked awaiting an out-of-band approval — do NOT re-issue the gated call` ⇒ `parked`(排在 raw-status 兜底组之前;裸 `parked` 不猜)。此前这一形恒不命中 ⇒ compaction 后停在待批检查点的后台任务整行消失。
|
|
83
|
+
- 新门 `scripts/run-approvals-stream-live-capability-test.mjs`(24 格)+ `scripts/run-model-facing-anchors-test.mjs`(20 格);变异反证八枚逐格见红。
|
|
84
|
+
|
|
85
|
+
### Changed(记账)
|
|
86
|
+
|
|
87
|
+
- devDep `@sema-agent/sdk` ^9.6.0 → ^9.7.1(peer 地板不动)。sdk 9.7.0 / 9.7.1 追平的六处型面与包内镜像逐一对表:`Capabilities.executionLane` / `.approvalsStreamLive`(本包结构读,与声明同形)、`LivePendingRow.frame?` / `contentKind?`(本包三道闸结构读,`contentKind` 本版不消费)、`ApprovalStreamEvent.type` +`live_pending` / `live_resolved`(feed 一贯不解析载荷,零动作)、`WiringManifest*` 三具名 schema(包内镜像编译期钉未红,零动作)、`stats.humanReview.gates[].toolCallId`(0.72.11 起已消费)。`queuedCount` 幻影键 sdk 保留到下一个 major ⇒ 本包 wire 键元组暂不动。
|
|
88
|
+
- gate-vocabulary 门 D2c 措辞:`DecisionReason` 的 `read_only` 归属由「候 sdk 镜像」改为「sdk 不铸型面(9.7.0 G31 登记门)」—— 本包零措辞铸点不变。
|
|
89
|
+
|
|
52
90
|
## 0.73.2(2026-09-18)
|
|
53
91
|
|
|
54
92
|
> 主题:**悬挂的子代 ask 从盲卡到全卡**(CC-59;server ≥7.87.0 在 `GET /v1/approvals` 的 `livePending[]` 行上补了 `frame` = 本来会 emit 的那只 `tool_approval` 帧)+ **CC-34 收口**(`permission_denials` 自动升级腿的引擎半场随 server 7.87.0 到货,验证判据交黑盒)。**patch**:型面纯 additive(公面导出名零增减 1026;`LivePendingAskView` +1 可选位 `frame`;零 wire 新键 —— 键由 server 铸;peer 不动)。接入面 §64。
|
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.73.
|
|
38
|
+
**Version:** 0.73.4
|
|
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
|
|
@@ -313,8 +313,10 @@ public-surface guard checks that last one).
|
|
|
313
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). |
|
|
314
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") |
|
|
315
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 |
|
|
316
|
+
| `scripts/run-approvals-stream-live-capability-test.mjs` | The engine's live-approval-push self-description (`capabilities.approvalsStreamLive`, engine ≥7.87.1), read the same four-state way as its four sibling capability readers: an absent key is reported as not reported (never folded into `false`), the value must be a strict boolean, and the one decision the feed consumer needs — whether it must keep pulling suspended asks itself — is answered by `livePendingNeedsReconcile`, which only says no when the engine explicitly says it pushes. |
|
|
317
|
+
| `scripts/run-model-facing-anchors-test.mjs` | Five places where model-facing engine text is parsed back into card slots: the task-output cursor semantics machine-readable key wins over the spool marker text (and its absence is never read as `full`), the `--- result (…) ---` line accepts a parenthesised note, the bash header accepts the engine's exit-note and image variants and carries the note into the same benign-exit slot the structured path uses, the notification seed scan treats both producers' terminal words as terminal, and the parked background-task phrase is recognised. |
|
|
316
318
|
| `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 |
|
|
317
|
-
| `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. |
|
|
319
|
+
| `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. Since 0.73.4 a push-side fetch that did not get a readable body (thrown, or an unreadable `livePending` section) is owed a look: it is retried with doubling back-off until one fetch succeeds (never abandoned by count), cancelled by any successful fetch, cleared on stop, and switchable off. |
|
|
318
320
|
| `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 |
|
|
319
321
|
| `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 |
|
|
320
322
|
| `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 |
|
|
@@ -372,7 +374,7 @@ public-surface guard checks that last one).
|
|
|
372
374
|
| `scripts/run-classifier-status-test.mjs` | What state the auto-mode classifier is in **on this session** — the question a doctor line, a model settings page and a permission card’s status row all ask, and a different question from the one the approval card asks (*why am I being asked right now*), so the sentences are pinned mutually distinct from that face’s as well as from each other. The session-level half of this reading — a breaker record the engine used to keep — was **retired upstream**, and the guard now holds that retirement from **both** sides: the engine's own declarations must really no longer carry it (a fact coming back would mean the removal here was the wrong disposition, and that deserves a conversation rather than silence), and this package must carry no alias, no state word and no leftover narrowing for it — a reading kept alive for something nobody emits any more is a promise the interface cannot keep, and it left the doctor line advertising a state it can never reach. What remains is ordered by the quantity that actually decides whether the classifier is running: the fact from **this round** first, then whether this leg is armed — a decider is minted per run, so a later leg can be armed again. Not armed, and a section that never arrived, both answer **undefined** rather than *available*; that arming question has its own field and answering it twice grows a second ledger. Arming and availability are also **two words, not one**: the engine says a decider was minted *for this leg*, which is an assembly-time fact, while whether that decider answers any given round is a **per-call** one — so an armed leg reads `armed` and only a positive per-call fact (an ask whose origin is the classifier's own denial-bound fallback, which by construction stands *after* the classifier ran) reads `available`. Every other ask origin is refused as evidence and for a stated reason rather than out of caution: several are ones the classifier is structurally forbidden to answer, and for the rest a surviving ask is precisely the case where it did **not** resolve one — so reading availability off them would be a guess. The projection is a **whitelist**, so an older engine still sending the retired member loses it at the boundary while the two live facts beside it ride through untouched. Rendering never throws and never impersonates: a state word this client does not know — including the retired one, which a restored view can still carry — reaches an honest fallback that names it verbatim, carries no invented explanation of a mechanism that no longer exists, and is proven distinct from all three real sentences; prototype keys reach that same fallback rather than a function body, checked against a real out-of-table word so the comparison cannot hold vacuously |
|
|
373
375
|
| `scripts/run-compaction-boundary-projection-test.mjs` | The compaction divider and the one frame that makes its anchor resolvable. The trigger word is passed through as an **open set** instead of being folded to two: the engine deliberately stopped flattening its third value (a compaction that was not optional — a prompt-too-long recovery or trim pressure) and carries what the hook layer saw, so folding it again at the package boundary re-introduces exactly what upstream had just removed, while a consumer branching on *is it manual* keeps its behaviour byte for byte. Only an unreadable word (absent, empty, non-string) falls back — that is *could not read it*, not *read it and did not recognise it*. Two superset keys ride the metadata and neither fabricates: the preserved-segment anchor is minted only when its id really reads out, because half an anchor sends the host looking up an empty string in its map, and the clamp ratio is a **disclosure** whose real zero is a fact rather than an absence. The clamp ratio also carries a registered exit condition — the service really sends it while the SDK arm has no seat for it yet, so the read is defensive and this guard reds the day that seat appears, forcing a re-check instead of leaving a cast to rot. The committed-message frame moves out of *deliberately not projected*: that classification was true about transcript rows and false about **positioning**, since the engine states that consumers build their own id-to-message map from this frame to place the divider — projecting the anchor without it hands the host something it cannot resolve. It becomes a neutral internal arm and an optional chrome ledger event, never a transcript row (the frame carries no body, so minting one would put words in the engine's mouth), with both required ids narrowed and a malformed frame recorded rather than half-minted |
|
|
374
376
|
| `scripts/run-cost-absence-projection-test.mjs` | Telling **declared free** apart from **never priced**, in both directions, because the package was getting each one wrong in the opposite way. The engine separates them on the wire — an absent cost means some spend had no price table, an explicit zero means the model declared itself free — and the result projector used to require a *positive* number, so a genuinely free run could not say so; while the per-model mirror folded absence to zero, so an unpriced run told a billing consumer it cost nothing. The total is now reported as the engine stated it, with absence and non-finite values alone reading as unknown, and a negative passed through rather than corrected, since a refund is a legal figure and the package is not a second accountant. The per-model figure keeps the CC shape intact — that field is a required number and *unknown* is simply not expressible in it — so the value stays zero and a **companion superset bit** carries the distinction, which means the two are read together and a reader that only ever looked at the number is unchanged; the bit is minted only in the absent case and never as `false`, since a key present with a false value reads as a third state. The same mint point serves both the wire's per-model split and the synthesised current-model row, so neither can drift. Alongside it the cache-write figure stops being a hardcoded zero and reads the field the wire has always carried, in both the flat usage and the synthesised row, and all four flat token slots move from a null-coalesce to a finite-number guard — the stats object has an open index signature and the wire is JSON, so a string or an infinity would otherwise land in a slot the types promise is a number, compiling green and surfacing only when something sums it |
|
|
375
|
-
| `scripts/run-permission-denial-projection-test.mjs` | The terminal result's **permission-denial list** being the wire's real one rather than a hardcoded empty array. The session vocabulary carries a list of tool calls that were denied; the projector used to mint `[]` in both the success arm and the error envelope, which folded two different statements into one — *nothing was denied on this run* and *this frame carries no such ledger at all* (an older engine, a rejection envelope, a failure event that arrives without stats) looked identical. Each denied gate on the wire's human-review ledger now becomes one record, in wire order, carrying the keys the wire can actually honour: the tool name when it reported one, and a superset field with the engine's own short, redacted one-line summary of the call's input. **Two lists, deliberately.** The reference shape requires three fields on every element — tool name, call id, and the full input object — and the wire's ledger carries only the first. Filling the other two with an empty string and an empty object would be invention; putting a half-filled element into the reference array would break the element contract, and a strict consumer validating the stream drops the *whole* result message rather than one field. So the reference array admits only fully-formed records — empty today, and filling itself the day the wire grows the two missing fields, with no code change — while every record the wire really has rides a superset carrier beside it. A contract check pins today's absence, so that day turns this guard red on purpose. The companion bit means *this reference list cannot be claimed complete*: no ledger, an unreadable row, an unrecognised decision word (a rejected plan is not a denied tool call, and a row with no decision at all is not a judgement), or a record that could not be fully formed. Only its absence lets a reader say *zero denials*; it is never minted as `false`. Rows that cannot be read drop themselves rather than the whole ledger, and both arms go through one mint point so they cannot drift |
|
|
377
|
+
| `scripts/run-permission-denial-projection-test.mjs` | The terminal result's **permission-denial list** being the wire's real one rather than a hardcoded empty array. The session vocabulary carries a list of tool calls that were denied; the projector used to mint `[]` in both the success arm and the error envelope, which folded two different statements into one — *nothing was denied on this run* and *this frame carries no such ledger at all* (an older engine, a rejection envelope, a failure event that arrives without stats) looked identical. Each denied gate on the wire's human-review ledger now becomes one record, in wire order, carrying the keys the wire can actually honour: the tool name when it reported one, and a superset field with the engine's own short, redacted one-line summary of the call's input. **Two lists, deliberately.** The reference shape requires three fields on every element — tool name, call id, and the full input object — and the wire's ledger carries only the first. Filling the other two with an empty string and an empty object would be invention; putting a half-filled element into the reference array would break the element contract, and a strict consumer validating the stream drops the *whole* result message rather than one field. So the reference array admits only fully-formed records — empty today, and filling itself the day the wire grows the two missing fields, with no code change — while every record the wire really has rides a superset carrier beside it. A contract check pins today's absence, so that day turns this guard red on purpose. The companion bit means *this reference list cannot be claimed complete*: no ledger, an unreadable row, an unrecognised decision word (a rejected plan is not a denied tool call, and a row with no decision at all is not a judgement), or a record that could not be fully formed. Only its absence lets a reader say *zero denials*; it is never minted as `false`. Rows that cannot be read drop themselves rather than the whole ledger, and both arms go through one mint point so they cannot drift. Since 0.73.4 the third CC key is sourced from the same stream's `tool_start` frame, joined by call id: a row joins only when the frame was seen on this stream, its arguments are a plain object, and no string leaf carries a transport replacement token or a cycle / depth placeholder (scan budgeted); both halves have positive controls (a fully joined list drops the discriminator, a partially joined one keeps it), the ledger's own input wins when present, the snapshot is per-stream and capped with a one-way overflow latch, and an id seen with two different argument objects never joins |
|
|
376
378
|
| `scripts/run-cost-reconcile-projection-test.mjs` | The **end-of-run cost reconciliation** reaching consumers at all. The engine splits a run's spend on the wire — the task's own cost, which deliberately excludes delegated sub-agents, the delegated total itself, and the within-task compaction subtotal that sits inside the own figure — and states two reconciliation identities for them. The package used to project none of it, so a cost view could only ever see one number and under-reported both delegated and compaction spend. Both structures are now projected onto the result as superset fields in the wire's integer micro-currency unit, read key by key, with unreadable keys dropped individually, an entirely unreadable structure omitted rather than emitted empty, and unknown categories passed through since the vocabulary belongs upstream. The delegated cost stays **absent when it was never priced**, never a fabricated zero. The same reader also feeds a terminal chrome arm carrying the three parts plus the reconciled total, so the two faces can never compute different answers; the reconciled total is minted only when both sides are known, and otherwise a discriminator bit says which side is unknown. **The reference field for total cost keeps its meaning** — it remains the task's own spend and the delegated total is not folded into it — because that is a shape the wider ecosystem reads; the reconciled figure is offered beside it, not in place of it. A frame that carries no stats emits no arm at all, and the existing rule that in-stream per-turn usage is not published for sub-flows is pinned unchanged, since delegated spend arrives once, at the end. The bit that says those figures are a lower bound is **per stream**, not per context: the emit context belongs to the caller and may be reused across streams, so a gap observed on one run is no evidence at all about the next one — the observation is held for the duration of one stream and handed to both projection faces by value, and the guard drives a reused context both sequentially and concurrently to prove neither direction leaks |
|
|
377
379
|
| `scripts/run-task-progress-terminal-projection-test.mjs` | The one tick that says a delegated child **finished**. The engine fires exactly one final beat carrying a terminal face, and says in the same breath why it exists — so a consumer sees the row finish instead of watching it vanish after the last running beat — but the package's projection whitelist had no seat for that field and its adapter still carried the older premise in a comment, so the terminal beat arrived byte-identical to another running one: the panel row stayed up waiting for a defensive sweep (which only ever settles rows bound to a card still open this turn) or for a separate notification frame. The status now rides through as an **open set** with the vocabulary left upstream, while the question *which words are terminal* is answered by a closed pair on the adapter side — an unrecognised new word takes the running path, because guessing it terminal ends a row that is still working whereas one extra running beat merely renders late. A terminal beat settles the row directly under the lane proof its binding gives it (not the main lane a notification would use, and not by card id, since the engine is naming a child rather than closing a card), freezes the inline group-row twin in the same beat so a later sweep cannot reset the real tool count, clears the session-resident ledger, and fires the stop hook only for a child whose start really fired. It does not mark the row live or emit a second progress beat, and it shares the settled-row ledger with the other two settle legs so a replay or a double-delivery cannot produce a second end. Three things are pinned **unchanged**: a running beat, an absent status (older engines never send the field, and reading absence as terminal would make every child row disappear on its first beat), and the workflow lane gate, which still runs before any of this |
|
|
378
380
|
| `scripts/run-assistant-arm-identity-test.mjs` | The identity keys on an assistant row, and an explicit account of the two that are **deliberately not** there. What the renderer received was a bare role-and-content object, so a dozen consumer sites downstream were each estimating what the message envelope should have told them. The id is taken from the engine's own event id rather than minted locally, because it has to be **the same value** on the live leg and on a durable replay — a freshly minted one would make a replayed message look new to a host's dedup and to rewind — and when the wire carries none the key is simply absent rather than filled with a random stand-in wearing an identity it does not have; it is also kept distinct from the envelope's own local render key, which is a different identity. The model name comes from what the host pinned when it opened the stream (the request was the host's to build) and is never guessed, since a wrong model name is worse than none once a billing or capability face looks it up. Usage and stop reason are **not** minted on this arm, and the reason is frame order rather than effort: content arms arrive before the turn's closing frame, so at the moment the arm is emitted the engine has not yet said what the round cost — anything put there would be an estimate, which is the very thing this work exists to remove — and synthesising a follow-up assistant update when the real figure lands is also refused, because that shape does not exist upstream and would place a message in the transcript the engine never sent. Their real values leave through the turn's own neutral arm as two superset keys, the usage one reusing the **same single mint point** the footer rollup already folds so the two faces cannot diverge, and the stop reason passed through verbatim as an open set — the machine signal for *was this turn cut short*, previously blind on both the stream and the trace. The existing behaviours beside them are pinned too: no arm at all when usage is wholly absent, and the sub-flow cut-out that keeps a child's turn from driving the leader's face |
|
|
@@ -123,6 +123,22 @@ export interface SemaFlatUsage {
|
|
|
123
123
|
*/
|
|
124
124
|
readonly _sema_cache_write_tokens_long?: number;
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* **这一条流**的收口快照(per-stream 的按次入参,绝不挂在调用方可复用的 `EmitContext` 上)。
|
|
128
|
+
* 每一格缺席 ⇒ 这次投影没有对应的流内面,只读终帧 `stats` 那一半。
|
|
129
|
+
*/
|
|
130
|
+
export interface TerminalStreamObserved {
|
|
131
|
+
readonly usageMissingObserved?: boolean;
|
|
132
|
+
readonly nestedUsageByTask?: ReadonlyMap<string, MutableSubagentUsageRow>;
|
|
133
|
+
/**
|
|
134
|
+
* 0.73.4 —— 本流见过的 `tool_start` 帧:`toolCallId → args`(帧上那只对象的**同一个引用**,不复制)。
|
|
135
|
+
* 拒绝账本行给不出入参对象,而被拒那次调用的入参已经在同一条流上;终帧铸拒绝行时按 id join
|
|
136
|
+
* (判据见 {@link joinableToolInput})。同一个 id 出现过两份不同入参的,收集方必须**不放进来**。
|
|
137
|
+
*/
|
|
138
|
+
readonly toolInputByCallId?: ReadonlyMap<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
/** 本流 `toolCallId → args` 快照的条数上限;溢出的调用不记 ⇒ 那一行 join 不上,留在 sema 载体(诚实缺席)。 */
|
|
141
|
+
export declare const TOOL_INPUT_JOIN_MAX_CALLS = 4096;
|
|
126
142
|
/**
|
|
127
143
|
* D-1 / L-192①(0.66.0)—— 一条被拒记录的**全可选**形:CC `SDKPermissionDenial`
|
|
128
144
|
* (agent-types `permissions.d.ts`,真形 `{tool_name: string; tool_use_id: string;
|
|
@@ -133,13 +149,14 @@ export interface SemaFlatUsage {
|
|
|
133
149
|
* 的 gate 行是具名 `HumanReviewGateRow` 六格(`kind` / `waitMs` / `decision?` / `toolName?` /
|
|
134
150
|
* `toolArg?` / `toolCallId?`,`task-result.d.ts` 真字节)。`toolCallId` 是**行身份**(core 逐字:
|
|
135
151
|
* 「absent = not joinable … Never a placeholder」—— 缺席 = 铸点没有调用 id 可给:探针行 / 非
|
|
136
|
-
* tool_approval 的耐久门 / 老引擎;**不是**「没有调用」)
|
|
137
|
-
* (sdk `types.d.ts` 同一格逐字记着「`tool_input`/`toolInput` **不在** gate ledger 上」)。⇒
|
|
138
|
-
*
|
|
152
|
+
* tool_approval 的耐久门 / 老引擎;**不是**「没有调用」)。被拒时的**入参对象**账本上**没有**
|
|
153
|
+
* (sdk `types.d.ts` 同一格逐字记着「`tool_input`/`toolInput` **不在** gate ledger 上」)。⇒ 第三键的
|
|
154
|
+
* 来源是**同一条流的 `tool_start` 帧**(0.73.4;按 `toolCallId` join,判据见 {@link joinableToolInput});
|
|
155
|
+
* join 不上 ⇒ 那一键**缺席**,绝不铸 `{}`:一个空对象在 CC 形上读起来是「这次调用的入参是空的」,那是编的。
|
|
139
156
|
* 🔴 **两键都声明在这里**(不是假 affordance):mint 点对它们是**开集宽读** —— `toolCallId` 在
|
|
140
157
|
* 7.23.0 串上账本那天 `tool_use_id` 就**自动**开始带值(见 {@link permissionDenialParts} 的自动
|
|
141
|
-
* 升级腿,门里钉着 7.23.0 的真字节正控)
|
|
142
|
-
* (老引擎 / 不可 join 的行两键缺席)。
|
|
158
|
+
* 升级腿,门里钉着 7.23.0 的真字节正控);账本上的 `toolInput` 仍等上游(哪天串上,它恒优先于流内
|
|
159
|
+
* join 的那份)。消费端读它们**必须按可选位读**(老引擎 / 不可 join 的行两键缺席)。
|
|
143
160
|
* 🔴 `_sema_tool_arg` = core 已经**脱敏并截短**的一行入参摘要(`primaryActivityArg` 同一道口),
|
|
144
161
|
* 它是 CC「denied: Bash(rm …)」那行显示唯一拿得到的材料。UNTRUSTED-for-display:只渲染,
|
|
145
162
|
* 绝不回喂模型、绝不当鉴权判据。
|
|
@@ -151,6 +168,11 @@ export interface SemaPermissionDenial {
|
|
|
151
168
|
readonly tool_use_id?: string;
|
|
152
169
|
/** 被拒调用的**完整入参**(⇐ `gates[].toolInput`,今天 wire 上没有 ⇒ 恒缺席;非对象一律不铸)。 */
|
|
153
170
|
readonly tool_input?: Record<string, unknown>;
|
|
171
|
+
/**
|
|
172
|
+
* 0.73.4 —— `tool_input` 的来源位,**只在**它来自同一条流的 `tool_start` 帧时在场(账本自己带的那份不铸本位)。
|
|
173
|
+
* 读法:在场 = 这只对象是流内帧上的入参(审批人改写过入参的调用,帧上是改写后的那份)。
|
|
174
|
+
*/
|
|
175
|
+
readonly _sema_tool_input_source?: 'tool_start';
|
|
154
176
|
/** 被拒调用的一行入参摘要(⇐ `gates[].toolArg`,core 侧已脱敏截短);缺席 = 这条腿没串入参。 */
|
|
155
177
|
readonly _sema_tool_arg?: string;
|
|
156
178
|
}
|
|
@@ -371,19 +393,13 @@ export declare function doneToSdkResult(ev: Extract<AgentEvent, {
|
|
|
371
393
|
* 累加表(见 {@link nestedUsageByTaskParts})。两格都是 per-stream 的事实,缺席 ⇒ 这次投影没有流内面
|
|
372
394
|
* (只读 `stats` 那一半、两个分表键都不铸),**绝不**回头去读调用方对象上可能残留的上一条流。
|
|
373
395
|
*/
|
|
374
|
-
observed?:
|
|
375
|
-
readonly usageMissingObserved?: boolean;
|
|
376
|
-
readonly nestedUsageByTask?: ReadonlyMap<string, MutableSubagentUsageRow>;
|
|
377
|
-
}): SDKMessage;
|
|
396
|
+
observed?: TerminalStreamObserved): SDKMessage;
|
|
378
397
|
/** `failed` → SDKResultError (contract 02 §2.11 / 08 CS-11). */
|
|
379
398
|
export declare function failedToSdkResult(ev: Extract<AgentEvent, {
|
|
380
399
|
type: 'failed';
|
|
381
400
|
}>, ctx: EmitContext,
|
|
382
401
|
/** 0.67.2:同 {@link doneToSdkResult} 的第三参。 */
|
|
383
|
-
observed?:
|
|
384
|
-
readonly usageMissingObserved?: boolean;
|
|
385
|
-
readonly nestedUsageByTask?: ReadonlyMap<string, MutableSubagentUsageRow>;
|
|
386
|
-
}): SDKMessage;
|
|
402
|
+
observed?: TerminalStreamObserved): SDKMessage;
|
|
387
403
|
/**
|
|
388
404
|
* Dispatch a terminal AgentEvent to its SDKResult arm.
|
|
389
405
|
*
|
|
@@ -395,7 +411,4 @@ export declare function terminalToSdkResult(ev: Extract<AgentEvent, {
|
|
|
395
411
|
type: 'done';
|
|
396
412
|
} | {
|
|
397
413
|
type: 'failed';
|
|
398
|
-
}>, ctx: EmitContext, observed?:
|
|
399
|
-
readonly usageMissingObserved?: boolean;
|
|
400
|
-
readonly nestedUsageByTask?: ReadonlyMap<string, MutableSubagentUsageRow>;
|
|
401
|
-
}): SDKMessage;
|
|
414
|
+
}>, ctx: EmitContext, observed?: TerminalStreamObserved): SDKMessage;
|
|
@@ -62,6 +62,48 @@ function flattenUsage(stats) {
|
|
|
62
62
|
function nonEmptyStr(v) {
|
|
63
63
|
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
64
64
|
}
|
|
65
|
+
/** 本流 `toolCallId → args` 快照的条数上限;溢出的调用不记 ⇒ 那一行 join 不上,留在 sema 载体(诚实缺席)。 */
|
|
66
|
+
export const TOOL_INPUT_JOIN_MAX_CALLS = 4096;
|
|
67
|
+
/** 传输层的替换记号前缀(闭形 `«redacted»` / `«redacted:<词>»` / `«redacted:<词>:partial»`;按前缀认 = 多认不少认)。 */
|
|
68
|
+
const REDACTION_TOKEN_PREFIX = '\u00abredacted';
|
|
69
|
+
/** 传输层把环 / 超深子树换成的两个占位串(只认「叶值恰等于」)。 */
|
|
70
|
+
function isTransportPlaceholderLeaf(v) {
|
|
71
|
+
return v === '[circular]' || v === '[depth-limit]';
|
|
72
|
+
}
|
|
73
|
+
/** 扫描预算:节点数与深度任一超限 ⇒ 按「判不出干净」处置(不铸)。 */
|
|
74
|
+
const TOOL_INPUT_SCAN_MAX_NODES = 20000;
|
|
75
|
+
const TOOL_INPUT_SCAN_MAX_DEPTH = 80;
|
|
76
|
+
/**
|
|
77
|
+
* 流内 `tool_start.args` 能不能当 CC `tool_input` 用。三道闸,任一不过 ⇒ `undefined`(该行留在 sema 载体):
|
|
78
|
+
* ① 是普通对象(CC 形是 `Record<string, unknown>`;字符串 / 数组 / null 都不是);
|
|
79
|
+
* ② **没被传输层动过**:帧上的入参在到达客户端之前过一道保形脱敏(结构与键名不动,字符串叶里的凭据形
|
|
80
|
+
* 子串换成记号;环与超深子树换成占位串)。被换过的对象**不是**那次调用的入参 —— 铸进 CC 键等于
|
|
81
|
+
* 声称「模型就是拿这只对象调的工具」。方向是少铸不错铸:入参里本来就含这些字面的极端情形会被
|
|
82
|
+
* 一并挡掉,代价是那一行留在 sema 载体;
|
|
83
|
+
* ③ 扫描在预算内走完(超大 / 超深 / 自环的对象判不出干净 ⇒ 不铸)。
|
|
84
|
+
*/
|
|
85
|
+
function joinableToolInput(args) {
|
|
86
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args))
|
|
87
|
+
return undefined;
|
|
88
|
+
let nodes = 0;
|
|
89
|
+
const stack = [{ v: args, depth: 0 }];
|
|
90
|
+
while (stack.length > 0) {
|
|
91
|
+
const { v, depth } = stack.pop();
|
|
92
|
+
if (++nodes > TOOL_INPUT_SCAN_MAX_NODES || depth > TOOL_INPUT_SCAN_MAX_DEPTH)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (typeof v === 'string') {
|
|
95
|
+
if (v.includes(REDACTION_TOKEN_PREFIX) || isTransportPlaceholderLeaf(v))
|
|
96
|
+
return undefined;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (v === null || typeof v !== 'object')
|
|
100
|
+
continue;
|
|
101
|
+
const children = Array.isArray(v) ? v : Object.values(v);
|
|
102
|
+
for (const c of children)
|
|
103
|
+
stack.push({ v: c, depth: depth + 1 });
|
|
104
|
+
}
|
|
105
|
+
return args;
|
|
106
|
+
}
|
|
65
107
|
/**
|
|
66
108
|
* 「这条 gate 是**被拒**的」判词表。
|
|
67
109
|
* 🔴 **识别表,不是合法值表**(同 `engineErrorCodes` 的开集纪律):core 今天在两处铸这一格 ——
|
|
@@ -91,21 +133,24 @@ const GATE_NON_DENIAL_DECISIONS = new Set(['allow', 'approve', 'edit', 'reject']
|
|
|
91
133
|
* (agent-types `permissions.d.ts` 与壳侧 zod `SDKPermissionDenialSchema` 皆然),而 wire 的 gate
|
|
92
134
|
* 账本(7.23.0 起六格)给不出 `tool_input`。两条路都不能走:**补零补空 = 编造**;**塞半条记录 = 破坏元素契约**
|
|
93
135
|
* ——严格消费方 `safeParse` 会把**整条 result 帧**判非法(丢一格 vs 丢整帧,后者严重得多)。
|
|
94
|
-
* ⇒ CC
|
|
95
|
-
* `
|
|
96
|
-
*
|
|
136
|
+
* ⇒ CC 数组只收**三键齐全**的记录,wire 上真有的每一条走 sema 载体 `_sema_permission_denials`,
|
|
137
|
+
* 键按能兑现的铸。`tool_use_id` ⇐ 账本行的 `toolCallId`(引擎 ≥7.23.0);`tool_input` ⇐ 账本行的
|
|
138
|
+
* `toolInput`(今天没有)**或**同一条流 `tool_start` 帧上的入参(0.73.4;调用方把本流快照作为按次入参
|
|
139
|
+
* 交进来,没交 = 没有流内面 = 与 0.73.3 之前逐字同)。🔴 0.73.2 的发布说明曾把「`tool_use_id` 到货」写成
|
|
140
|
+
* 「CC 数组自动填满」—— 那是错的:缺第三键的行进不了 CC 数组,直到本版的流内 join 才真有三键齐的行。
|
|
97
141
|
*
|
|
98
142
|
* 🔴 判别位 `_sema_permission_denials_absent` = **「CC 那条清单不可声称完整」**,四条路径:
|
|
99
143
|
* ① 账本整个读不出(`humanReview` 缺席 / 坏形 / `gates` 不是数组 / 无 stats);
|
|
100
144
|
* ② 有读不出的行(非对象);
|
|
101
145
|
* ③ 有**认不出的判词**(两张判词表之外的词 / 判词缺席)—— 不猜它是不是拒绝;
|
|
102
|
-
* ④ 有被拒记录**没能铸成 CC 形**(
|
|
146
|
+
* ④ 有被拒记录**没能铸成 CC 形**(行无 `toolCallId` / 本流没见过那只 `tool_start`(重连后的流、
|
|
147
|
+
* 宿主自建管线没交快照)/ 入参不是普通对象 / 入参被传输层动过)。
|
|
103
148
|
* 读法因此是两键合读:`[]` + 判别位缺席 = **零拒绝**(正面事实);`[]` + 判别位 `true` = 别渲肯定句。
|
|
104
149
|
* 判别位**只在该说话时铸,绝不铸 `false`**(本包 additive 一贯纪律)。
|
|
105
150
|
*
|
|
106
151
|
* 逐行独立:坏行只丢自己,绝不丢整本账(但会让清单失去「完整」资格,见 ②③)。
|
|
107
152
|
*/
|
|
108
|
-
function permissionDenialParts(stats) {
|
|
153
|
+
function permissionDenialParts(stats, toolInputByCallId) {
|
|
109
154
|
const hr = stats?.humanReview;
|
|
110
155
|
const gates = hr !== null && typeof hr === 'object' ? hr.gates : undefined;
|
|
111
156
|
// 账本整个读不出(缺席 / 坏形 / 无 stats)—— 诚实缺席,不猜。
|
|
@@ -139,15 +184,22 @@ function permissionDenialParts(stats) {
|
|
|
139
184
|
// 上游若用别的名字补上,门里的缺席证据当天红 ⇒ 那一批改读真名。绝不自铸值。
|
|
140
185
|
const toolUseId = nonEmptyStr(row.toolCallId);
|
|
141
186
|
const rawInput = row.toolInput;
|
|
142
|
-
const
|
|
187
|
+
const ledgerInput = rawInput !== null && typeof rawInput === 'object' && !Array.isArray(rawInput)
|
|
143
188
|
? rawInput
|
|
144
189
|
: undefined;
|
|
190
|
+
// 0.73.4:账本没带 ⇒ 按行身份去本流快照里 join(账本那份在场时恒优先;`has` 先判,绝不把
|
|
191
|
+
// 「没见过这只 id」与「见过、入参是 undefined」混成一格)。
|
|
192
|
+
const joinedInput = ledgerInput === undefined && toolUseId !== undefined && toolInputByCallId?.has(toolUseId) === true
|
|
193
|
+
? joinableToolInput(toolInputByCallId.get(toolUseId))
|
|
194
|
+
: undefined;
|
|
195
|
+
const toolInput = ledgerInput ?? joinedInput;
|
|
145
196
|
// 键都读不出时仍然 push 一条空记录:**条数**(「这条 run 被拒了几次」)是这本账最要紧的
|
|
146
197
|
// 事实,丢掉它等于把一次真实发生的拒绝抹成没发生;键按能兑现的铸,不编。
|
|
147
198
|
denials.push({
|
|
148
199
|
...(toolName !== undefined ? { tool_name: toolName } : {}),
|
|
149
200
|
...(toolUseId !== undefined ? { tool_use_id: toolUseId } : {}),
|
|
150
201
|
...(toolInput !== undefined ? { tool_input: toolInput } : {}),
|
|
202
|
+
...(joinedInput !== undefined ? { _sema_tool_input_source: 'tool_start' } : {}),
|
|
151
203
|
...(toolArg !== undefined ? { _sema_tool_arg: toolArg } : {}),
|
|
152
204
|
});
|
|
153
205
|
}
|
|
@@ -725,7 +777,7 @@ function errorResult(ctx, parts) {
|
|
|
725
777
|
usage: flattenUsage(parts.stats),
|
|
726
778
|
modelUsage: modelUsageFor(parts.stats, parts.model),
|
|
727
779
|
// D-1 / L-192①:两句话不再折成一句 —— 清单 + 「有没有这本账」的判别位,见 permissionDenialParts。
|
|
728
|
-
...permissionDenialParts(parts.stats),
|
|
780
|
+
...permissionDenialParts(parts.stats, parts.observed?.toolInputByCallId),
|
|
729
781
|
// D-3 / B-068:失败/到限/park 的 run 一样花过钱,账不因结局不好就不报。
|
|
730
782
|
...costFactParts(parts.stats, parts.observed),
|
|
731
783
|
// CC-06:引擎生效面两键(失败臂同带)。
|
|
@@ -946,7 +998,7 @@ observed) {
|
|
|
946
998
|
// MF-25/P1-5 — the per-model usage split (wire `stats.modelUsage`, or the synthesized current-model row).
|
|
947
999
|
modelUsage: modelUsageFor(stats, r.model),
|
|
948
1000
|
// D-1 / L-192①:同形第二处 —— 与错误信封共用**同一个** mint 点(修前两处各一个字面量 [])。
|
|
949
|
-
...permissionDenialParts(stats),
|
|
1001
|
+
...permissionDenialParts(stats, observed?.toolInputByCallId),
|
|
950
1002
|
// D-3 / B-068:成本明细与子代那本账(micro-USD 原值);`total_cost_usd` 语义一字不动。
|
|
951
1003
|
...costFactParts(stats, observed),
|
|
952
1004
|
// CC-06(0.71.3):`_sema_effective_reasoning` / `_sema_effective_memory_scopes`。
|
|
@@ -2,7 +2,7 @@ import { eventSeq, } from './types.js';
|
|
|
2
2
|
import { eventToSdkMessage, turnEndUsage } from './downstream/eventToSdkMessage.js';
|
|
3
3
|
import { turnUsageToModelUsage } from './downstream/turnUsageToModelUsage.js';
|
|
4
4
|
// D-3(0.66.0):终局对账臂与终帧超集键共用**同一个**成本读器(readRunCostFacts)。
|
|
5
|
-
import { readRunCostFacts, terminalToSdkResult } from './downstream/terminalToSdkResult.js';
|
|
5
|
+
import { readRunCostFacts, terminalToSdkResult, TOOL_INPUT_JOIN_MAX_CALLS } from './downstream/terminalToSdkResult.js';
|
|
6
6
|
import { coerceOutput, publishSubagentContentEvent } from '../subagentContentStore.js';
|
|
7
7
|
/**
|
|
8
8
|
* 409 session-busy 拒绝的 **canonical errorCode**([2377]C-1,server main `049ff2c`,随 5.0.0 发)。
|
|
@@ -414,6 +414,25 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
414
414
|
// 把**本流快照**同时交给两个投影口(终帧与对账臂),两面读同一份、且谁都读不到别人那份。
|
|
415
415
|
// 🔴 **不是**靠「终帧那一拍把 ctx 上那一位清掉」修的:并发的两条流会互相覆盖那次清除。
|
|
416
416
|
let usageMissingObserved = false;
|
|
417
|
+
// ── 0.73.4 —— 本流 `toolCallId → tool_start.args` 快照(终帧拒绝清单第三键的来源)──
|
|
418
|
+
// 拒绝账本行带得出「哪一次调用」(`toolCallId`),带不出那次调用的入参对象;入参在同一条流的
|
|
419
|
+
// `tool_start` 帧上。与上面两格同款:**本函数的局部量**,终局那一拍作为按次入参交给终帧铸点,
|
|
420
|
+
// 绝不挂 `ctx`(调用方可复用给多条流 ⇒ 上一条流的入参会 join 到下一条流的拒绝行上)。
|
|
421
|
+
// 只存帧上那只对象的引用(转录里的 `tool_use.input` 是同一只),不复制;有条数上限。
|
|
422
|
+
// 同一个 id 见到两份**不同**的入参 ⇒ 判不出被拒的是哪一份 ⇒ 该 id 永久出表(不回头)。
|
|
423
|
+
const toolInputByCallId = new Map();
|
|
424
|
+
const toolInputAmbiguous = new Set();
|
|
425
|
+
let toolInputOverflowed = false;
|
|
426
|
+
const sameArgs = (a, b) => {
|
|
427
|
+
if (a === b)
|
|
428
|
+
return true;
|
|
429
|
+
try {
|
|
430
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
417
436
|
for await (const ev of events) {
|
|
418
437
|
// event-id idempotency — drop a re-seen durable seq (contract 02 §1.1).
|
|
419
438
|
const seq = eventSeq(ev);
|
|
@@ -428,6 +447,25 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
428
447
|
}
|
|
429
448
|
seen.add(seq);
|
|
430
449
|
}
|
|
450
|
+
// 0.73.4:记本流的 `tool_start` 入参(主会话与子代内部调用同记 —— 拒绝账本是整条 run 的)。
|
|
451
|
+
if (ev.type === 'tool_start') {
|
|
452
|
+
const callId = ev.toolCallId;
|
|
453
|
+
if (typeof callId === 'string' && callId.length > 0 && !toolInputAmbiguous.has(callId)) {
|
|
454
|
+
const args = ev.args;
|
|
455
|
+
if (!toolInputByCallId.has(callId)) {
|
|
456
|
+
// 溢出是单向闩:一旦有调用没记上,之后的新 id 一律不记(出表腾出的空位不回填 ——
|
|
457
|
+
// 否则一只没记上首份入参的 id 会在重放时被当成首见,两份不同入参的判据就漏了)。
|
|
458
|
+
if (toolInputByCallId.size >= TOOL_INPUT_JOIN_MAX_CALLS)
|
|
459
|
+
toolInputOverflowed = true;
|
|
460
|
+
if (!toolInputOverflowed)
|
|
461
|
+
toolInputByCallId.set(callId, args);
|
|
462
|
+
}
|
|
463
|
+
else if (!sameArgs(toolInputByCallId.get(callId), args)) {
|
|
464
|
+
toolInputByCallId.delete(callId);
|
|
465
|
+
toolInputAmbiguous.add(callId);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
431
469
|
// C1 — SUBAGENT content divert (service 1.89 forwardSubagentEvents): a CONTENT event stamped with
|
|
432
470
|
// parentToolCallId belongs to a SUBAGENT run (EventIdentity contract: present ONLY on sub-agent
|
|
433
471
|
// events) — side-channel it to the 查看态 transcript store and keep it OFF the main pipeline
|
|
@@ -913,7 +951,7 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
913
951
|
// 字节的证据都不是;按那句话办,一条账数得全的 run 会被渲成「≥」并被宿主持久化成不完整状态
|
|
914
952
|
// (失效方向在这里**不是**安全的那一侧,它是在断言一件没发生的事)。
|
|
915
953
|
// ⇒ 观测位改为 per-stream 局部量,终帧按值收(第三参),与上面对账臂读的是同一份。
|
|
916
|
-
const resultFrame = terminalToSdkResult(ev, ctx, { usageMissingObserved, nestedUsageByTask });
|
|
954
|
+
const resultFrame = terminalToSdkResult(ev, ctx, { usageMissingObserved, nestedUsageByTask, toolInputByCallId });
|
|
917
955
|
// 0.72.12 CC-50(test [7541] G2-d「铸了就要有人读」):结果帧上的 `_sema_outcome:'unknown'`(终态记录整个
|
|
918
956
|
// 缺席 / 认不出的终态词;铸点与取舍见 terminalToSdkResult `ErrorResultParts.outcomeUnknown`)在**这里**被消费 ——
|
|
919
957
|
// 交互面合成一行 `Outcome unknown:` 终局行(句子与结果帧 `errors[0]` 单源),壳零接线即可见;它**不是**
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* approvalsStreamLiveCapability — `GET /v1/capabilities.approvalsStreamLive`(server ≥7.87.1;sdk 9.7.1 `Capabilities.approvalsStreamLive?: boolean`)
|
|
3
|
+
* 的三端共用读面(0.73.3;与 sql / writeProtection / webSearch.backend / executionLane 四只兄弟同一套四态词汇)。
|
|
4
|
+
*
|
|
5
|
+
* ── 这一位答的是哪一个问题 ────────────────────────────────────────────────────────────────
|
|
6
|
+
* 「这台引擎会不会在 `approvals.stream` 上推流内 ask 的进出(`live_pending` / `live_resolved`)」= checkpointStore ∧ toolApproval。
|
|
7
|
+
* 它决定 feed 消费方要不要**靠**推面:位为 true ⇒ 推面可靠,对账节拍(`ApprovalsFeedOptions.reconcile`)只是兜底;
|
|
8
|
+
* 位为 false / 键缺席 / 从没观测 ⇒ 推面**不可靠或判不出**,对账节拍是悬挂 ask 的唯一拉面 —— **别等** `live_pending`。
|
|
9
|
+
*
|
|
10
|
+
* ── 🔴 读法 ─────────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
* ① **键缺席 ≠ false**:老 server(<7.87.1)不报这一位 ⇒ `not_reported`(判不出),按「要拉」处理;折成 false 是替引擎断言「不推」。
|
|
12
|
+
* ② 严格布尔;`null` / 串 / 数 / 数组 / 对象 ⇒ 畸形(删格 ⇒ `unobserved`)。
|
|
13
|
+
* ③ 「要不要对账」的判据归包:{@link livePendingNeedsReconcile} —— 只有 present 且 true 才 false;其余一律 true(少查一次 = 一只等人的 ask 迟迟不出现)。
|
|
14
|
+
*/
|
|
15
|
+
/** 四态读数。`unobserved` 由读口在这一格空缺时铸,不由投影铸。 */
|
|
16
|
+
export type ApprovalsStreamLiveReading = {
|
|
17
|
+
kind: 'unobserved';
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'not_reported';
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'present';
|
|
22
|
+
live: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** caps 回体 → 本格读数;畸形一律 `undefined`(= 这一格不写 ⇒ 读口答 `unobserved`)。 */
|
|
25
|
+
export declare function projectApprovalsStreamLiveCapability(caps: unknown): ApprovalsStreamLiveReading | undefined;
|
|
26
|
+
/** 宿主 caps probe 的读面 tee 落点(与四只兄弟并列)。绝不 throw;畸形 ⇒ 删格;`opts.generation` 关掉旧探测覆盖新读数的竞态。 */
|
|
27
|
+
export declare function noteEngineCapsForApprovalsStreamLive(baseUrl: string, caps: unknown, opts?: {
|
|
28
|
+
generation?: number;
|
|
29
|
+
}): void;
|
|
30
|
+
/** 本进程观测到的读数;这一格空缺 ⇒ `{kind:'unobserved'}`。 */
|
|
31
|
+
export declare function observedApprovalsStreamLive(baseUrl?: string | undefined): ApprovalsStreamLiveReading;
|
|
32
|
+
/**
|
|
33
|
+
* feed 消费方「要不要靠对账节拍拉悬挂 ask」的判据单源:只有引擎明说会推(present ∧ true)才 `false`;
|
|
34
|
+
* 位为 false / 老 server 判不出 / 从没观测 ⇒ `true`。fail-safe 方向定在「查」(与 `shouldPoll` 同一条理由)。
|
|
35
|
+
*/
|
|
36
|
+
export declare function livePendingNeedsReconcile(reading: ApprovalsStreamLiveReading): boolean;
|
|
37
|
+
/** doctor 那一行的 detail 串,唯一措辞真源。🔴 `not_reported` 句不许暗示「不推」。 */
|
|
38
|
+
export declare function approvalsStreamLiveDoctorDetail(reading: ApprovalsStreamLiveReading): string;
|
|
39
|
+
/** 换代失效口(引擎温切成功后调):清成未观测。空串 ⇒ no-op;绝不 throw。 */
|
|
40
|
+
export declare function forgetApprovalsStreamLiveReading(baseUrl: string | undefined): void;
|
|
41
|
+
/** 测试钩子。 */
|
|
42
|
+
export declare function __resetApprovalsStreamLiveReadingsForTests(): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* approvalsStreamLiveCapability — `GET /v1/capabilities.approvalsStreamLive`(server ≥7.87.1;sdk 9.7.1 `Capabilities.approvalsStreamLive?: boolean`)
|
|
3
|
+
* 的三端共用读面(0.73.3;与 sql / writeProtection / webSearch.backend / executionLane 四只兄弟同一套四态词汇)。
|
|
4
|
+
*
|
|
5
|
+
* ── 这一位答的是哪一个问题 ────────────────────────────────────────────────────────────────
|
|
6
|
+
* 「这台引擎会不会在 `approvals.stream` 上推流内 ask 的进出(`live_pending` / `live_resolved`)」= checkpointStore ∧ toolApproval。
|
|
7
|
+
* 它决定 feed 消费方要不要**靠**推面:位为 true ⇒ 推面可靠,对账节拍(`ApprovalsFeedOptions.reconcile`)只是兜底;
|
|
8
|
+
* 位为 false / 键缺席 / 从没观测 ⇒ 推面**不可靠或判不出**,对账节拍是悬挂 ask 的唯一拉面 —— **别等** `live_pending`。
|
|
9
|
+
*
|
|
10
|
+
* ── 🔴 读法 ─────────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
* ① **键缺席 ≠ false**:老 server(<7.87.1)不报这一位 ⇒ `not_reported`(判不出),按「要拉」处理;折成 false 是替引擎断言「不推」。
|
|
12
|
+
* ② 严格布尔;`null` / 串 / 数 / 数组 / 对象 ⇒ 畸形(删格 ⇒ `unobserved`)。
|
|
13
|
+
* ③ 「要不要对账」的判据归包:{@link livePendingNeedsReconcile} —— 只有 present 且 true 才 false;其余一律 true(少查一次 = 一只等人的 ask 迟迟不出现)。
|
|
14
|
+
*/
|
|
15
|
+
import { engineWireTarget } from './engineWireTarget.js';
|
|
16
|
+
import { engineCapsGeneration } from './engineCapsCache.js';
|
|
17
|
+
/** caps 回体 → 本格读数;畸形一律 `undefined`(= 这一格不写 ⇒ 读口答 `unobserved`)。 */
|
|
18
|
+
export function projectApprovalsStreamLiveCapability(caps) {
|
|
19
|
+
if (caps === null || typeof caps !== 'object')
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!('approvalsStreamLive' in caps))
|
|
22
|
+
return { kind: 'not_reported' };
|
|
23
|
+
const v = caps.approvalsStreamLive;
|
|
24
|
+
if (v === undefined)
|
|
25
|
+
return { kind: 'not_reported' };
|
|
26
|
+
if (typeof v !== 'boolean')
|
|
27
|
+
return undefined;
|
|
28
|
+
return { kind: 'present', live: v };
|
|
29
|
+
}
|
|
30
|
+
const readingByBase = new Map();
|
|
31
|
+
/** 宿主 caps probe 的读面 tee 落点(与四只兄弟并列)。绝不 throw;畸形 ⇒ 删格;`opts.generation` 关掉旧探测覆盖新读数的竞态。 */
|
|
32
|
+
export function noteEngineCapsForApprovalsStreamLive(baseUrl, caps, opts) {
|
|
33
|
+
try {
|
|
34
|
+
if (typeof baseUrl !== 'string' || baseUrl === '')
|
|
35
|
+
return;
|
|
36
|
+
if (opts?.generation !== undefined && opts.generation !== engineCapsGeneration(baseUrl))
|
|
37
|
+
return;
|
|
38
|
+
const reading = projectApprovalsStreamLiveCapability(caps);
|
|
39
|
+
if (reading === undefined) {
|
|
40
|
+
readingByBase.delete(baseUrl);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
readingByBase.set(baseUrl, reading);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* fail-soft:本 tee 任何分支都不许打断 caps 探测链 */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** 本进程观测到的读数;这一格空缺 ⇒ `{kind:'unobserved'}`。 */
|
|
50
|
+
export function observedApprovalsStreamLive(baseUrl = engineWireTarget()?.baseUrl) {
|
|
51
|
+
if (typeof baseUrl !== 'string' || baseUrl === '')
|
|
52
|
+
return { kind: 'unobserved' };
|
|
53
|
+
return readingByBase.get(baseUrl) ?? { kind: 'unobserved' };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* feed 消费方「要不要靠对账节拍拉悬挂 ask」的判据单源:只有引擎明说会推(present ∧ true)才 `false`;
|
|
57
|
+
* 位为 false / 老 server 判不出 / 从没观测 ⇒ `true`。fail-safe 方向定在「查」(与 `shouldPoll` 同一条理由)。
|
|
58
|
+
*/
|
|
59
|
+
export function livePendingNeedsReconcile(reading) {
|
|
60
|
+
return !(reading.kind === 'present' && reading.live === true);
|
|
61
|
+
}
|
|
62
|
+
/** doctor 那一行的 detail 串,唯一措辞真源。🔴 `not_reported` 句不许暗示「不推」。 */
|
|
63
|
+
export function approvalsStreamLiveDoctorDetail(reading) {
|
|
64
|
+
switch (reading.kind) {
|
|
65
|
+
case 'unobserved':
|
|
66
|
+
return 'live approval push not observed — the engine reports it on /v1/capabilities; this process has not received a capabilities response yet, so suspended asks are pulled by the reconcile cadence';
|
|
67
|
+
case 'not_reported':
|
|
68
|
+
return 'live approval push not reported by this engine — only newer engines advertise it; this does not say whether it pushes, so suspended asks are pulled by the reconcile cadence';
|
|
69
|
+
case 'present':
|
|
70
|
+
return reading.live
|
|
71
|
+
? 'live approval push on — the engine announces suspended asks on the approvals stream; the reconcile cadence is only a fallback'
|
|
72
|
+
: 'live approval push off on this deployment (no checkpoint store or tool approvals disabled) — suspended asks are only visible through the reconcile cadence, so the client polls for them';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** 换代失效口(引擎温切成功后调):清成未观测。空串 ⇒ no-op;绝不 throw。 */
|
|
76
|
+
export function forgetApprovalsStreamLiveReading(baseUrl) {
|
|
77
|
+
if (typeof baseUrl !== 'string' || baseUrl === '')
|
|
78
|
+
return;
|
|
79
|
+
readingByBase.delete(baseUrl);
|
|
80
|
+
}
|
|
81
|
+
/** 测试钩子。 */
|
|
82
|
+
export function __resetApprovalsStreamLiveReadingsForTests() {
|
|
83
|
+
readingByBase.clear();
|
|
84
|
+
}
|
|
@@ -91,6 +91,8 @@ export interface ApprovalsFeedStats {
|
|
|
91
91
|
snapshots: number;
|
|
92
92
|
/** 0.72.14:对账节拍真取件的次数(与 `polls` 分开记 —— 它不属于回落轮询腿)。 */
|
|
93
93
|
reconciles: number;
|
|
94
|
+
/** 0.73.4:「欠一次看」的重试取件次数(见 `ApprovalsFeedOptions.pushTakeRetryMs`)。 */
|
|
95
|
+
pushTakeRetries: number;
|
|
94
96
|
}
|
|
95
97
|
/**
|
|
96
98
|
* 0.72.14 **对账节拍**:悬挂的流内 ask 出现 / 结算时,较老的引擎不在 `approvals.stream` 上发任何事件 ⇒ push 腿连着也
|
|
@@ -132,6 +134,16 @@ export interface ApprovalsFeedOptions {
|
|
|
132
134
|
* · **`refresh()`** 不受管 —— 那是端自己按下的显式动作。
|
|
133
135
|
*/
|
|
134
136
|
shouldPoll?: () => boolean;
|
|
137
|
+
/**
|
|
138
|
+
* 0.73.4:**欠一次看**的重试基础间隔(ms;缺省 1000;`<= 0` = 关,与 0.73.3 之前同)。
|
|
139
|
+
*
|
|
140
|
+
* push 腿收到事件 = 引擎说「变了」;那一次 `list()` 若失败(抛出,或回体读不懂),这个「变了」就没人去看:
|
|
141
|
+
* 其后的 heartbeat 不触发取件,没有别的事件的话,那张待批的卡一直不出现。⇒ 任何一次 push 侧取件
|
|
142
|
+
* (起手那次、事件触发的、`refresh()`)没看成,就记一笔欠账并按 base、2×base、4×base … 退避重试,
|
|
143
|
+
* 间隔封顶 30 s 与 base 的较大者,**不按次数放弃**(有人可能正在等这张卡);**比欠账更晚发起**的任何一次
|
|
144
|
+
* 取件看成(哪条腿都算)即销账、撤定时器、间隔回到 base;更早发起、迟到回来的成功不销账。只在 push 模式记账 —— 轮询模式下轮询腿自己会再来。
|
|
145
|
+
*/
|
|
146
|
+
pushTakeRetryMs?: number;
|
|
135
147
|
/** 0.72.14:对账节拍(见 {@link ApprovalsFeedReconcileOptions})。缺省不起。 */
|
|
136
148
|
reconcile?: ApprovalsFeedReconcileOptions;
|
|
137
149
|
signal?: AbortSignal;
|