@sema-agent/client-core 0.75.1 → 0.76.0
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 +27 -0
- package/README.md +6 -1
- package/dist/adapt/arms.js +100 -1
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +45 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +121 -6
- package/dist/agentsWireCaps.d.ts +15 -2
- package/dist/engineAgentPanelStore.js +13 -1
- package/dist/engineErrorCodes.d.ts +4 -0
- package/dist/engineErrorCodes.js +14 -0
- package/dist/fleet/fleetRowAgentType.js +1 -1
- package/dist/hitl/approvalOutcomeNote.d.ts +0 -10
- package/dist/hitl/approvalOutcomeNote.js +35 -8
- package/dist/hitl/approvalResolution.d.ts +148 -0
- package/dist/hitl/approvalResolution.js +199 -0
- package/dist/hitl/approvalsFeed.d.ts +100 -2
- package/dist/hitl/approvalsFeed.js +234 -18
- package/dist/hitl/livePendingAsk.d.ts +26 -6
- package/dist/hitl/livePendingAsk.js +52 -12
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/memorySpecWire.d.ts +175 -0
- package/dist/memorySpecWire.js +320 -0
- package/dist/seam.d.ts +53 -5
- package/dist/seam.js +10 -1
- package/docs/INTEGRATION-CLIENTS.md +64 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,33 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.76.0(2026-09-20)
|
|
53
|
+
|
|
54
|
+
> 主题:**六票一批** —— `prompt_assembled` 投影臂(CC-91)· 审批 feed 的「不知道」三态(CC-98,**型面 BREAKING**)· `ApprovalResolution` 单源判别联合(CC-77 ①)· 记忆 spec 读口与未列键判官(CC-96)· 终态词表补上游钉(CC-78 ①)· `tasks_expand` 退役(CC-92,**型面 BREAKING**)。外加一条**跨全部内部臂**的修复(身份四键一次性快照)。**minor**:两处型面 BREAKING + 两条只对「直接调读器的端」可见的行为面改口;公面值导出 1065 → **1081**(+16)。接入面 §76。
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- 🔴 **身份四键被反复求值,值随读变的帧能让两个入口分叉(跨全部内部臂)**:同一帧经「投影口 → 内部臂 → 适配层」与「宿主自建管线 / 重放腿直喂 `adapt()`」两条路进来时,身份键(`eventId` + 三只子流键)此前在形门、子流判定、取父键三处**各读一次**;遇到 accessor 帧(两次读给不同值)会让两条路把同一帧判成不同 lane —— **子代的组成信息被洗成主会话的**;读身份抛错还会从两个入口逸出。现在两侧都对四键取**一次性快照**:每键恰读一次、全程用同一份、读取抛错整帧拒。修在**共享助手**上,所有内部臂(`compaction_outcome` / `text_end` / `reasoning_end` / `tool_disclosure` / `tool_progress` / `context_usage` …)连带受益。
|
|
59
|
+
- **审批 feed 把「取不到」渲成「没有」(cli L-426 归包)**:`list()` 抛 / `livePending` 读不懂时此前**零发布**,而 `snapshot()` 恒答上一张、计数照旧答具体数 ⇒ 消费端把「这次没取到」读成「一条都没有」,计数冻在旧值;同形第二处是收到空回体直接**撤卡**。现在订阅口发**判别联合** `ApprovalsFeedEmission`(真快照 / `kind:'unknown'{why,at,mode}`),unknown 时计数三格答 `null`、视图空、**零 delta 不撤卡**;恢复后必发真快照;退避与封顶语义逐字未变。
|
|
60
|
+
- **终态词表五张只有字面量快照、没有上游钉(RH-4 第一步)**:「这条 run / 任务结了没」散在 5 个文件的 8 张表里,此前多数只有 `表.join(',') === 'a,b,c'` 那种快照断言 —— 只证明「表今天长这样」,不证明「长这样是对的」,上游加词时原样绿。现在逐表补**真的双向咬**(exact / superset / subset+词数 canary / 边界钉),读的是**源码文本**经 TypeScript 语法树取值,不读编译产物。
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **内部臂 + chrome 臂 `prompt_assembled`(CC-91)**:引擎的 prompt 组装清单(`blocks` / `sections` / `tools` / `totalChars`)此前在投影口 `not_in_slice`、三端零消费。🔴 **本臂只投 `chars`,一个 token 键都不铸** —— 亲核三面真字节:整条 wire 上**不存在 token 真值**,连 `context_usage.sections[].tokens` 都是引擎拿同一份 `chars` 估的(`ceil(chars / 系数)`)。`sections[].id` 是引擎**开集**(`core/<slot>.<name>`),与端的展示分类名对不上,映射归端。
|
|
65
|
+
- **`ApprovalResolution` 单源判别联合(CC-77 ①,additive)**:`decided` / `not_sent{cause}` / `unsettled{cause}` 三臂 + 两张冻结 cause 词表;`'unresolved'` 的三义就此分开(撤卡 / 编辑被拒 / respond 失败各落一处)。既有导出、型、行为一字未动,便签口 `approvalOutcomeNoteOf` 改由本联合派生(逐键对拍等价)。
|
|
66
|
+
- **记忆 spec 读口与未列键判官(CC-96)**:`agents[].memory` 的四键闭白名单三态读口(`scopes` 缺席 ≠ 空集;`writeScope: null` = 本 run 只读 ≠ 缺席);判官 `unknownMemorySpecKeys` 三态(`[]` = 确认零未列键 / 非空 = 看到了这些 / `undefined` = 读不出),读到退役的单数 `scope` 会说出真后果「**整只 agent** 被 400,不是丢这一个键」。🔴 在场判据按**序列化字节**判(按属性上下文取快照),不按进程内对象的 `hasOwn` —— 不可枚举键与 `toJSON` 会让两者分叉,那会让读口一边说「只读、零违规」一边真发退役键。写面 `TaskAgentWireMemory.writeScope` 同批放宽成 `string | null`(读得出却铸不出是半条腿)。
|
|
67
|
+
|
|
68
|
+
### Changed(🔴 BREAKING)
|
|
69
|
+
|
|
70
|
+
- **审批 feed 订阅口换判别联合**(CC-98):端的回调入参从 `ApprovalsFeedSnapshot` 变 `ApprovalsFeedEmission`,**编译期强制表态**(不留兼容层、零别名、零双读)。
|
|
71
|
+
- **chrome 臂 `tasks_expand` 退役**(CC-92,clay 裁定 C-R76 ②):包内零铸点、消费端空桩,双侧皆死。`ChromeEvent` 联合缩窄 + 臂表行删除,靠 `Record<ChromeArmKind, …>` 的编译期耦合咬住;穷举 switch 与字面量比较的消费者都会拿到编译信号(TS2678 / TS2367)。
|
|
72
|
+
- **两条只对「直接调读器的端」可见**(CC-98):`readLivePendingRows` 对「非空数组却一行都读不出」从 `{present, rows:[]}` 改判 `{malformed}`(半坏仍丢行 + present + dropped,空数组仍是真 0);durable `pending` 段开始窄读(`[{}]` / `[7]` 从「发一张 durable:1 的快照」变「发一张不知道」)。
|
|
73
|
+
|
|
74
|
+
### Guards
|
|
75
|
+
|
|
76
|
+
- 新门四只:`run-prompt-assembled-projection-test.mjs`(48 断言)· `run-approvals-feed-unknown-test.mjs`(103 格)· `run-approval-resolution-test.mjs`(45 格)· `run-memory-spec-wire-test.mjs`(68 格)· `run-terminal-table-provenance-test.mjs`(51 格,走语法树不走正则)。变异合计 **70 余枚**逐格见红。异源对抗复审:六辆车合计 **14 轮**,`[high]`×6 + `[medium]`×14 全采修;其中三条由复审**纠正了车自己修反的第一版**。
|
|
77
|
+
- 棘轮:公面 1065 → 1081(+16)· typeshape unknown 出境 370 → 373(负控锚同批)· portability index 闭包 180 → 182 · singleton 清单 405 → 408(high 119 不动)· failloud 豁免 41 → 42(带账:身份快照的 catch 是 fail-closed,读身份抛错 ⇒ 整帧拒)。
|
|
78
|
+
|
|
52
79
|
## 0.75.1(2026-09-20)
|
|
53
80
|
|
|
54
81
|
> 主题:**面板反序缓行的残洞**(外部复验读数:短命子代的最终用量在一种到达序下永远交不出)+ **缓行遇同代 running 帧的三向定形** + **`spawnName` 透传与 `agentType` 改读诚实来源**(#969 提货,CC-95)。**patch**:型面 additive(tick 臂 +1 可选位),公面值导出零增;行为面三笔(见下)。接入面 §75。
|
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.76.0
|
|
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
|
|
@@ -317,8 +317,12 @@ public-surface guard checks that last one).
|
|
|
317
317
|
| `scripts/run-run-cancel-context-test.mjs` | The run record's `cancelContext` side-note (engine ≥7.87.3) read structurally, and the cause of a `turn_aborted{engine_error}` classified from machine-readable evidence only: `cancelled` (code `cancelled`, with the cancel-time context when present) / `engine_error` (any other failure code, passed through verbatim) / `run_still_live` (the record is not terminal — a dropped stream is a client-side fact, not the run's cause) / `unknown` (never guessed). An absent `cancelContext` reads as *not reported*, never as "not cancelled"; `elapsedMs` is never folded to 0. |
|
|
318
318
|
| `scripts/run-suspended-reopen-projection-test.mjs` | The durable `suspended` event's `reopened` key read as three distinct states — `reopened` (with the engine's code, verbatim), `not_reopened` (an explicit `null`), `unstated` (key absent or unreadable) — and carried on the HITL bridge's active gate (`currentGateReopen()`), re-read on every `suspended` and cleared with the gate. |
|
|
319
319
|
| `scripts/run-panel-identity-normalization-test.mjs` | One background subagent has two ids on the wire — the fleet row id tail and the `task_progress` task id (its transcript id). Every panel event goes through one funnel that rewrites the `tick` / `end` task id onto the fleet row's id once a `fleet-row` has registered the key (`transcriptId` first, `parentToolCallId` as the fallback), carrying the original as `wireTaskId` and marking `taskIdOrigin`; an unbound tick whose row has not arrived yet waits one beat (bounded) and is released verbatim on the next tick / `end`, when the buffer is full, or after `MAX_HELD_WIRE_TICK_BEATS` other fleet-row / end / sweep events (a `sweep` itself leaves it alone: there is no row to settle yet); a normalized `end` that carries no cycle identity borrows the registering row's, so a late close of a revived task is recognized as stale; the key table is an LRU (a task that keeps ticking is never evicted by newer registrations); the residency mark migrates with the id and both keys are cleared on settle — except that a stale (previous-cycle) terminal never clears the revived row's mark — so the notification lane can clear it. Once a tick has been delivered verbatim under its UUID, that UUID is the subagent's key: later fleet rows and fleet-side ends are rewritten onto it (the tail kept in `wireTaskId`), so a consumer sees one row in every arrival order; a late tick from a previous cycle is dropped rather than folded into the revived row. |
|
|
320
|
+
| `scripts/run-prompt-assembled-projection-test.mjs` | The `prompt_assembled` frame (one prepare's prompt-assembly manifest) projected to an internal arm and then to the additive `prompt_assembled` chrome event — the per-section / per-block **character** counts, the mounted tool names and `totalChars`, each key present only when the engine really sent it (the frame's `constitution` is deliberately not carried: no consumer asks for it today, and every published key is a contract to keep). The manifest carries **no token counts** anywhere upstream, so this projection mints none: a token figure derived from characters would be an invented number, and the engine's own estimate lives on `context_usage.sections[].tokens` (same id wordlist, joinable). Bad rows are dropped one by one, and a face that loses every row reads as an absent key rather than an empty array — so an absent face means only "this event carries no readable view of it" (an absent upstream key, an empty array and a fully filtered list all land on the same shape) and is never reported as a diagnosis about the engine. `blocks[].id` and `sections[].id` are two different wordlists with a many-to-one relation, and the token join against `context_usage.sections[].tokens` only holds when both sides carry a section view. A frame with no readable composition key at all is malformed, ids and slots are read as an open set, one chrome event per frame with zero transcript rows, several prepares per task are all handed over (de-duplication — "take the last one" — is the host's move), and the lane is told honestly (`parentToolCallId` ⇒ subagent lane; a frame attributable only by `sourceTaskId` / `bgAgentId` is not surfaced on the main lane). Both entry points obey the same rule: the adapt layer rebuilds every row too, so a host pipeline (or a replayed transcript) that feeds the raw frame straight into `adapt()` cannot smuggle extra keys (`tokens`, digests, aliases), a negative `chars` or a `null` row into the chrome payload, an empty array does not count as a composition face, the identity keys are snapshotted once on both paths (read exactly once each, a throwing accessor rejects the whole frame — reading one twice is what lets an accessor frame land on a different lane on each path), and the two paths are compared verbatim so the two readers cannot drift. |
|
|
320
321
|
| `scripts/run-compaction-outcome-projection-test.mjs` | The `compaction_outcome` frame (a compaction that did **not** end as compacted: mooted by the task ending, failed, …) projected to an internal arm and then to the additive `compaction_outcome` chrome event — `outcome` required and verbatim (open set), `trigger` / `reason` present only when the engine sent a non-empty string, malformed frames dropped, zero transcript rows, the lane told honestly (`parentToolCallId` ⇒ subagent lane; a frame attributable only by `sourceTaskId` / `bgAgentId` is not surfaced on the main lane). |
|
|
321
322
|
| `scripts/run-approval-card-retract-test.mjs` | The approval card's **decision-free retraction** and the in-stream frame leg's **outcome hand-back**: a host that must withdraw a card that no longer has a decision channel (session switch, engine switch, a tracker reporting the ask gone) answers `{ kind: 'retracted' }` and the package sends nothing on any of the three legs (in-stream frame, suspended ask, durable park), reporting `decision: 'unresolved'` with a `retracted` flag; `aborted` / `failed` / `deny` keep their meaning (a real deny is still posted), and `onToolApprovalOutcome` hands every in-stream outcome back to the host exactly once, tolerating a throwing or rejecting callback Also the single source for the host-side approval-outcome note (`approvalOutcomeNoteOf`): `settled` is whether the decision was delivered, `retracted` is an independent key present only when the card was retracted, and `detail` is the retraction / edit-refused sentence or the refusal code and message — never a fabricated sentence. |
|
|
323
|
+
| `scripts/run-memory-spec-wire-test.mjs` | The per-agent **memory spec** (`agents[].memory`) read once for every client, plus the judge for the engine's **closed** key list. Two states are kept apart that clients habitually collapse: an absent `scopes` means *no layers were specified*, never "zero layers", and an explicit `writeScope: null` is a positive fact — this run has memory **read-only** (no remember tool, no consolidation write; recall still works) — which is neither "unspecified" nor "memory off". Each of the four keys is read once, on own properties only (an inherited key never reaches the wire, so reading one would report a value the engine cannot see), and a key that is present but unreadable stays in its own slot instead of collapsing into "unspecified"; `enabled` must be a strict boolean and `scopeContract` is an open-set verbatim word. A spec that cannot be read at all answers *undefined*, kept distinct from an agent that simply has no spec. The judge earns its keep on the consequence: the engine checks this spec against a closed list, so one unlisted key — most often the retired singular `scope` — is refused together with the **whole agent definition**, not just that key, and the single sentence minted here says so. What counts as "on the wire" is decided by the bytes, not by the shape of the in-process object: both the reader and the judge work off a `JSON` snapshot of the spec taken **in its property position** (wrapped under the same key, never serialized as a root value — otherwise a `toJSON(key)` that branches on the key hands us one shape and the engine another: one such input made the snapshot say *read-only, no violations* while the real bytes carried the retired key and a writable scope), because `Object.keys` and `hasOwn` disagree with the serializer in ways that change the answer — a non-enumerable `writeScope: null` would otherwise be reported as "memory is read-only for this run" while the engine receives *unspecified* and may still write; a key whose value is `undefined` would be reported as a violation that never leaves the process; a `toJSON` (even inherited) adds keys that `Object.keys` cannot see, including the retired singular one; and a throwing getter would let the judge claim it had looked when the spec cannot be serialized at all. A spec that fails to serialize is reported as unreadable by both ports, and the snapshot is taken once, so every getter runs exactly once. The judge answers in three states, never two: `[]` is an assertion (*looked, nothing unlisted* — including an agent that carries no spec at all), a non-empty list is what it saw, and *undefined* means it could not read the spec (a non-object item, an array, an unreadable `memory`, a throwing getter) — an unreadable spec never poses as a clean one, and an array is not a spec so its index keys are noise rather than findings. It reports only the snapshot's string keys, sorted and bounded, so a prototype, symbol, non-enumerable or `undefined`-valued key is never blamed while a `__proto__` that really does serialize is; the empty-string key is kept rather than dismissed as noise, because it does serialize and dropping it left a non-empty violation list with nothing said about the consequence; every key name in the sentence is quoted and escaped one code point at a time, so no escape is ever cut in half (a half-cut escape used to make the closing quote itself look escaped) and an empty name, a key literally named `""`, a key containing a backslash and a real control character versus a literal `\uXXXX` all read as different violations; a name too long to show is marked `(truncated)` outside the quotes with a pointer to the judge's verbatim list, so a prefix is never presented as the whole key — two long names sharing a prefix do show the same, which is why the mark and the pointer are there; key names are sanitized and bounded on the way into the sentence while the judge itself hands back the verbatim key, because sanitizing belongs in prose and never in a verdict. The announced future key `projectKey` is still unlisted today and is reported as such, with a sentence saying it is not a typo. The two construction-time refusals (`config.memory_project_key_spelling` — a spelling, 400; `config.memory_write_scope_mismatch` — a conflict with the scope already in force, 409) join the existing `config.` recognition table rather than a second word list, and each gets one sentence stating that the refusal landed **before the run started**, so nothing ran; the engine owns the triage and an unrecognised code gets no sentence at all. The write face is widened in the same batch so the package can actually mint what the reader can read: `TaskAgentWireMemory.writeScope` is now an optional `string | null`, since a reader that understands "memory is read-only for this run" while the writer cannot express it is worse than no reader at all — it makes the support look real. Minting `null` survives serialization and reads back as read-only, minting `undefined` drops the key and reads back as unspecified, and the projector still pins `writeScope` explicitly every time. The accepted key list is reconciled against an upstream witness rather than a second local copy: the guard reads the SDK's own declaration comment for this key, requires the two sets to match in both directions, requires that comment to still name the singular `scope` as retired, and requires it to still not mention `projectKey` — so the day upstream admits that key, the guard goes red instead of the package quietly continuing to promise a 400. Each port takes its own snapshot, so a consumer that wants one self-consistent answer about a spec that can still change under it should read `spec.unknownKeys` off the reader — which comes from the same snapshot as the four slots — and send that materialized data rather than the live object. |
|
|
324
|
+
| `scripts/run-approvals-feed-unknown-test.mjs` | The approvals feed tells three states apart: **N items waiting**, **nothing waiting**, and **this fetch did not come back, so we do not know**. Every way a fetch can fail (the call throwing or rejecting, a body that is not an object, a `livePending` section that is not an array — including the `null` seen in the field, a `pending` that is not an array or holds a malformed row) publishes `{kind:'unknown', why, at, mode}` on the subscription — never an empty snapshot and never silence. Real snapshots carry `kind:'snapshot'`; `snapshot()` still answers only with the last real one (a fact about the past) while `reading()` answers whether it is current (`unobserved` / `present` / `unknown`). Recovery always publishes a real snapshot again, even when the contents are byte-identical to before the failure. An unknown reading is never counted as zero: the awaiting-decision counts read `null`, the view is empty, and the tracker reports no removals, so cards on screen are not retracted for a failed fetch. Retry, backoff and circuit-breaking are unchanged. |
|
|
325
|
+
| `scripts/run-approval-resolution-test.mjs` | The single discriminated union for **how an approval decision ended** (`ApprovalResolution`: `decided` / `not_sent` / `unsettled`) and its one mapping entry `approvalResolutionOf`: every outcome of the durable-park leg (12 shapes) and of the in-stream frame / suspended-ask leg (4 shapes) lands on exactly one arm and cause; the three meanings of `decision: 'unresolved'` (retracted card, refused edit, respond that never settled) land on three different arms, with `retracted` winning when both flags are set; an interrupted durable card really posts a deny, so it is `unsettled` (`interrupted`), never `not_sent`; a safety stop never claims the decision left the package, and a refusal is only attributed to the engine when the outcome carries positive evidence (a wire error code, or the pointer key the engine mints on a rejection body) — an aborted or code-less decide failure is reported as a plain decide failure; the decision word is passed through without re-validating the closed set; an unreadable outcome is `unsettled` (`unreadable`), never guessed as `decided`; both cause vocabularies are frozen tuples with every word covered by a case, plus the three predicates; the approval-outcome note (`approvalOutcomeNoteOf`) is now derived from the union and compared key-by-key against a reference copy of its previous logic over the released inputs, with a self-check that the comparison can fail; a source-text pin asserts every `return` carrying `respondRefusal` also carries `'unresolved'`. No behaviour change: the existing outcome types and keys are untouched. |
|
|
322
326
|
| `scripts/run-panel-cycle-identity-test.mjs` | The **cycle identity** on agent-panel events and the fleet ledger's **departure read-out**: a background agent may be revived under the same id, so `fleet-row` and `end` events now carry the wire's own `cycleSeq` / `startedAt` when present (absent means the row has no notion of generations, never "generation one"), `isStaleEngineAgentPanelEnd` is the single rule for ignoring a late `end` from a previous cycle (only when both sides carry a comparable identity; absence never drops a real terminal), a changed `cycleSeq` is a new cycle for usage stickiness and buffer coalescing, the notification lane carries `cycleSeq` only when the wire really sent `seq`, and `task_remove` frames reach the host through `onTaskRemoved` with `removeReason` / `cycleSeq` verbatim, a stale previous-generation removal leaving the newer row in place. A terminal row held back because the consumer has no such row yet is also released by the keys it carries itself (its transcript id, or the delegating call id of a subagent already on screen under its wire id), since the key tables are only written once a row has actually been published — the release still goes through the one funnel, so the subagent stays one row; a running frame arriving after such a held terminal row is a stale snapshot when both sides carry a comparable generation and it matches (no event, the held row keeps its final usage), a revival when the frame is provably newer (the held row is dropped), and is treated as a revival when neither side can be compared. A subagent lifecycle event carries `agentType` only from an honest source — the fleet row's own agent type, recorded before it is folded into the row label — and omits the key when there is none, never substituting the display name |
|
|
323
327
|
| `scripts/run-workflow-size-warning-test.mjs` | The **workflow size warning** verdict shared by every host footer / panel: a three-state result (`warn` / `ok` / `unknown`) read off the optional fleet view keys, where an unknown size is never reported as a normal one (absent `totalCount` / `tokens` without positive over-cap evidence is `unknown`, naming the missing keys), positive evidence on either axis wins regardless of absent keys, the per-agent denominator uses the engine's started count only when it is not below done+failed (a smaller value is a stale reading), otherwise falls back to the done+failed lower bound only when both keys are present — and a lower-bound denominator only yields an upper bound of the projection, which can prove *within cap* (`ok`, flagged) but never *over cap* (`unknown`, with the upper bound exposed) — and the prior is used only when the engine itself reports zero started agents; cap precedence env > explicit guideline > default, prototype keys never act as a guideline, the env reader is pure and does not fall through to the second name on a bad first value; caps, guideline table, env names and the three copy variants are single-sourced |
|
|
324
328
|
| `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. |
|
|
@@ -377,6 +381,7 @@ public-surface guard checks that last one).
|
|
|
377
381
|
| `scripts/run-approval-frame-chrome-arms-test.mjs` | The two in-stream approval frames finally reaching every host through the shared pipeline instead of one shell's private branch — the shape of a layering defect: hosts that only consume the package could not rebuild their pending cards after a reconnect, and did not clear a card the engine had withdrawn. The payload is deliberately carried as the **envelope** the upstream types declare rather than the first-version card: the stream parser applies no predicate, so narrowing here would let a legitimately newer frame pass as the older shape and invite consumers to read keys a newer card never promised. The guard therefore pins that every open key survives untouched, that an unknown version still passes through, and that narrowing is left to the host's own predicates — with the fallback being a generic card and a person, **never** an automatic denial. A frame whose version cannot be read at all is reported as malformed rather than dropped in silence, because both frames carry user-visible decisions and state changes. Both arms are registered as **required** host duties, and their duty text names the load-bearing rules a host would otherwise have to rediscover: which predicate to narrow with, that the reconnect preamble — not a replayed historical frame — is the authority on which cards exist, and that a withdrawal frame can be lost entirely. Unlike the sibling arms, these carry **no** sub-stream cutoff: an approval raised under a delegated call still has to reach a person, and filtering it by ownership is the host's job, not a reason to discard it. Finally the upstream bytes that justify the envelope discipline are checked to still be there, since the whole design rests on them |
|
|
378
382
|
| `scripts/run-terminal-status-vocabulary-test.mjs` | One place that decides whether a run has **ended** and whether it ended badly — written because that judgement had already been hand-copied three times, so the day the engine added a word for *the agent itself reported it cannot continue*, every copy missed it and a panel settled a self-reported failure as a success. The distinction the table exists for is pinned from both sides: that word belongs in it, while the two words meaning *waiting for a person to decide* deliberately do **not** — reading those as endings would bury a run that is actively waiting on the reader. A word this client does not know answers *no*, and the guard states plainly that *no* is not evidence of success: proving success means reading the positive side, so negating this predicate is the very mistake that caused two earlier incidents. The fleet lane gets the same treatment from the other direction: a workflow parked on a durable approval used to fall through to *running*, leaving the person with no hint that a card was waiting, and it now lands on the same rendered word the task lane already used — same fact, same word, checked end to end on a real row. Why the word was added directly rather than carried as a private superset key is checked mechanically against the upstream declaration being open, so the day it closes this reds and the decision gets revisited. The residue sweep is the point: the source tree must contain **no** further inlined copy of the judgement, each of the three former sites is checked to really read the single predicate, and the one reviewed exemption carries its reason **and** a liveness assertion, so an exemption whose justification expires cannot quietly keep standing |
|
|
379
383
|
| `scripts/run-terminal-word-source-test.mjs` | Two tables of ending words, kept apart by **who owns them** — because they used to be one. The engine's own closed set of reasons a run ended, and the server's set of row states a run can finish in, overlap in three words but not in all of them: one word for *something outside stopped it* exists only on the server side, and one for *it paused and can be resumed* exists only on the engine side and means very nearly the opposite of an ending. Merged into a single list, those two sources became indistinguishable, so a new word on either side looked the same as a new word on the other, and the safest-looking move — folding the unknown word into a known one — is the exact mistake that has caused incidents here before. The engine-owned table is checked as a **copy, not an opinion**: it is reconciled word-for-word and in order against the installed engine package, read from both its declaration and its runtime bytes with the two required to agree, so the day upstream adds a fifth reason this reds before anything ships. The two dividing words are each pinned from both sides, including against the upstream declaration directly rather than only against this package's own list. Why the table is copied rather than re-exported is itself an assertion with an expiry: the day upstream publishes the set as a value, this guard reds and the decision gets revisited. The renamed tables leave **no alias** behind, since an alias would let a reader keep consuming the merged list and the split would have bought nothing |
|
|
384
|
+
| `scripts/run-terminal-table-provenance-test.mjs` | Several tables answering *has this ended*, which until now only asserted their own current wording rather than that the wording was right — a snapshot equality passes forever even the day upstream adds a word this package never learns about. Each is reconciled against a named upstream source instead, one comparator shared across all of them rather than one copy per table: a notification's terminal words are the engine's own closed set minus its one live word; a sub-agent tick's terminal words are the engine's own inline status literal minus *running*; a run row's terminal words must **cover every** engine reason a run can end — missing one is the exact failure mode that once let a client retry a connection until its budget ran out while the ending sat unread in the row the whole time — plus one explicitly named legacy word the engine's current declaration no longer carries; a fleet row's terminal words are pinned to **exact equality** with the transport's own status set minus its known non-terminal words (an adversarial pass found the earlier one-directional form let a real terminal word be quietly deleted from this side and still pass), and separately pinned against that set's current member count so the day it changes a person has to look. A sixth table has no clean upstream owner at all — a fact this guard states as a finding, not hides: every status union collected elsewhere in this suite is checked to barely overlap this table's words, with the overlap threshold itself a living assertion that reds the day something upstream finally does match closely enough to replace the guess, and the table is separately checked against every non-terminal word gathered — including one meaning *durably paused*, sourced from the engine's own outcome vocabulary rather than any of the other five unions, after the same adversarial pass found a caller-documented non-terminal word this boundary had missed. A seventh pair, found by the same pass sweeping the whole tree for the same shape of hand-copied table, answers a related but distinct question — whether a session's claim on a run has been released or is still held — and is pinned as two complementary halves of one upstream set: released-minus-one-named-legacy-word and held must partition the transport's status set exactly, so a real state going missing from either side is caught the same way a fabricated one would be. Every extraction in this guard parses real syntax rather than pattern-matching quoted text, so a comment mentioning a word never counts as that word being present, and single- and double-quoted members are read identically |
|
|
380
385
|
| `scripts/run-workflow-park-truth-projection-test.mjs` | The read face for *which approvals a workflow run left parked* — and the credential that must never ride along with it. Upstream strips the redemption token from that response, and this package's reader is built so the token **cannot** come back: each row is assembled field by field from the three identity keys, never copied wholesale, so an extra key appearing upstream is structurally unable to reach anything this package hands a UI. The guard proves that rather than asserting it — a poisoned row carrying a secret is read, and the secret is searched for across the **entire** serialized result, with the same search proven to find it in the input so a blind search cannot pass; renaming the credential key does not help it through, because the rule is *only these three*, not a blocklist; and the reader's own source is checked to contain no object spread, since one such line would quietly void all of it. The other half is an absence distinction with opposite consequences: a record with **no** parks field at all was written by an older engine and proves nothing about whether approvals are waiting, while an empty list is a positive statement that none are — collapsing those two would let a run whose parked approvals cannot be proven be resumed anyway, so they are kept literally distinguishable, and a payload whose rows are all unreadable answers *unknown* rather than *none*. The four refusal codes for this family are checked code by code against the engine's real bytes, never matched by name prefix, and the older umbrella code they were split out of is asserted to still be **alive** — treating the whole code as retired would make a family of real refusals vanish silently **0.68.3 (core 7.18.0):** two more keys ride the same projection duty as `parks` itself: `originUnconfirmed: true` on a row (never `false`; absence is the confirmed state) and `resumeAdmissionIncomplete: true` on the run (presence means "not a resume base"). Dropping either would turn a refused record back into an admissible one, so the guard pins both, including that neither folds into the other **0.69.1 (CC-12):** both keys now also ride the projected `WorkflowRunState`, so a host that only sees the projection can render them |
|
|
381
386
|
| `scripts/run-retired-vocabulary-census-test.mjs` | Whether a retirement really happened. When upstream removes a family, a downstream package can cut it out or keep a courteous alias — and the alias is the worse outcome: three clients keep writing branches for something nobody emits, and a status line advertises a state it can never reach. Choosing the clean cut only means something if a guard holds it, since a comment saying *retired* is not an exit code. Each registered entry is held two ways: the name must be gone from **code positions** in this package (comments stripped first, because the explanation is supposed to stay) and off the published surface, and — the half that keeps this from being self-congratulation — it must really be gone **upstream**, since that is the entire reason it was removed here; if it comes back, the disposition deserves reconsideration rather than silence. The scanner proves it can speak by finding a symbol that is genuinely present before any absence is believed, and distinguishes a mention inside a comment from one in a string literal, which is exactly the form being cleared. A closing check runs the other way: the retirement **story** must remain in the comments, including a promise this package made earlier and has now had to withdraw — deleting the history alongside the code is a bad way to satisfy *zero hits*, and leaves the next reader with code that has no reason |
|
|
382
387
|
| `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 |
|
package/dist/adapt/arms.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isSubFlowSegmentEnd } from '../adapter/types.js';
|
|
1
|
+
import { isSubFlowSegmentEnd, SEGMENT_END_IDENTITY_KEYS } from '../adapter/types.js';
|
|
2
2
|
import { classifyPeerNotification, renderPeerFrameTranscriptText } from '../peerFrames.js';
|
|
3
3
|
import { dropQueuedNotificationsForRun, isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, markEngineWorkflowNotified, noteOwnWorkflowRun, normalizeTaskNotification, noteWorkflowCompletionCardEnqueued, renderTaskNotificationXml, taskNotificationDedupKey } from '../notifications.js';
|
|
4
4
|
import { mapBrainStatusToRetry } from '../retryStatus.js';
|
|
@@ -588,6 +588,103 @@ const compactionOutcomeArm = function* (m) {
|
|
|
588
588
|
...(eventId !== undefined ? { eventId } : {}),
|
|
589
589
|
});
|
|
590
590
|
};
|
|
591
|
+
/**
|
|
592
|
+
* CC-91(0.76.0):逐行**重建**白名单行(不是 `Array.isArray` 就整只透传)——
|
|
593
|
+
* 🔴 本臂与投影口是**两个入口**:宿主自建管线 / 重放存量转录可以绕过投影口直接把 wire 帧喂进 `adapt()`,
|
|
594
|
+
* 那条路上未校验的行会把本臂对外承诺的四条(零 `tokens`、零 digest 过境、`chars` 有限非负、全坏即缺席)
|
|
595
|
+
* 一起破掉,还可能把违反公开型的行(`null` / `chars: -1`)递到消费端手里
|
|
596
|
+
* (异源对抗复审 R1 [medium] 实测可复现:`sections:[{id:'x',chars:-1,tokens:123,contentHash:'…'}]` 原样进 chrome)。
|
|
597
|
+
* ⇒ 与投影口**同律同形**:好行留、坏行逐条丢、全丢光或非数组 ⇒ 键缺席(不铸 `[]`)。
|
|
598
|
+
* 两处实现由门逐字对拍(同一帧走投影口 vs 直喂 adapt,chrome 行必须一模一样),drift 即红。
|
|
599
|
+
*/
|
|
600
|
+
const promptManifestRows = (v, pick) => {
|
|
601
|
+
if (!Array.isArray(v))
|
|
602
|
+
return undefined;
|
|
603
|
+
const rows = [];
|
|
604
|
+
for (const r of v) {
|
|
605
|
+
if (typeof r !== 'object' || r === null || Array.isArray(r))
|
|
606
|
+
continue;
|
|
607
|
+
const row = pick(r);
|
|
608
|
+
if (row !== undefined)
|
|
609
|
+
rows.push(row);
|
|
610
|
+
}
|
|
611
|
+
return rows.length > 0 ? rows : undefined;
|
|
612
|
+
};
|
|
613
|
+
/**
|
|
614
|
+
* CC-91(0.76.0):`prompt_assembled` 内部臂 → chrome `prompt_assembled`。每帧一条;转录面零行。
|
|
615
|
+
* 🔴 **包不去重**:一次任务的每一次 prepare(cascade / verify 的每一 rung、resume 腿)各自一条,server 逐条保序落账 ⇒
|
|
616
|
+
* 每条都交出去,「只要最后一次」是**宿主**的动作(与 `context_usage` 同律;在包内按最后一条折会把先前已计费 rung 的组成面抹掉)。
|
|
617
|
+
* ⚠️ 上游那条流的**重放去重**是另一件事,按 `eventSeq` 在 `adapter/runStream.ts` 做;本臂只说「同一条流上的多条 prepare 都算真事实」。
|
|
618
|
+
* 🔴 lane 如实:`parentToolCallId` 非空串 ⇒ subagent;只有 `sourceTaskId` / `bgAgentId`(或父键空串)的帧没有诚实的 lane 可挂 ⇒ 不发,
|
|
619
|
+
* 绝不落到主会话名下。一条组成面的键都读不出的帧不发(非投影口喂进来的坏帧兜底)。
|
|
620
|
+
* 🔴 **零 tokens 铸点**:本臂只搬 `chars`(上游真字节无 token 位),不在这一层编任何 token 数。
|
|
621
|
+
*/
|
|
622
|
+
const promptAssembledArm = function* (m) {
|
|
623
|
+
const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
|
|
624
|
+
const size = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined);
|
|
625
|
+
// ⚠️ 帧上的 `constitution` 刻意不过境(投影口头注写明理由:今天零消费场景,键即契约)。
|
|
626
|
+
const totalChars = size(m.totalChars);
|
|
627
|
+
const blocks = promptManifestRows(m.blocks, (o) => {
|
|
628
|
+
const id = str(o.id);
|
|
629
|
+
const chars = size(o.chars);
|
|
630
|
+
return id === undefined || chars === undefined ? undefined : { id, chars };
|
|
631
|
+
});
|
|
632
|
+
const sections = promptManifestRows(m.sections, (o) => {
|
|
633
|
+
const id = str(o.id);
|
|
634
|
+
const chars = size(o.chars);
|
|
635
|
+
if (id === undefined || chars === undefined)
|
|
636
|
+
return undefined;
|
|
637
|
+
// 🔴 语义槽在**两个入口上都只认一个键名** `slot`(上游的名字,本层不改名):投影口产物与 wire 原形同名,
|
|
638
|
+
// 所以两条路读的是同一个位。曾经的写法是「`kind` 优先、回退 `slot`」—— 那会让一条**同时**带
|
|
639
|
+
// `kind` 与 `slot`(或只带 `kind`)的帧在两条路上读出不同的槽,而这种帧是合法的(上游行是开集)。
|
|
640
|
+
const slot = str(o.slot);
|
|
641
|
+
return { id, chars, ...(slot !== undefined ? { slot } : {}) };
|
|
642
|
+
});
|
|
643
|
+
const tools = promptManifestRows(m.tools, (o) => {
|
|
644
|
+
const wireName = str(o.wireName);
|
|
645
|
+
return wireName === undefined ? undefined : { wireName };
|
|
646
|
+
});
|
|
647
|
+
if (totalChars === undefined && blocks === undefined && sections === undefined && tools === undefined)
|
|
648
|
+
return;
|
|
649
|
+
// 🔴 身份四键**一次性快照**,与投影口的 `identitySnapshot` / `identityOrMalformed` 同律(每键只读一次、抛错整帧拒):
|
|
650
|
+
// ① 三个子流键**逐个**过形门(在场即必须是串)—— 只守 `parentToolCallId` 的话,一条
|
|
651
|
+
// `{parentToolCallId:'tc-9', sourceTaskId: 7}` 会在投影口被整帧判坏、在直喂这条路上照发一条 subagent chrome;
|
|
652
|
+
// ② **不能就地多读**:进程内帧可以带 accessor,同一个键读两次拿到两个值时,「形门读一次、子流判定再读一次、
|
|
653
|
+
// 取 parent 又读一次」会让同一帧在两条路上归属不同 lane —— 子代的组成信息被洗成主会话的
|
|
654
|
+
// (第三轮复审 [medium] 实测可复现)。所以这里**不**用 `isSubFlowSegmentEnd(m)`(它会再读一遍),
|
|
655
|
+
// 子流判定直接看快照里「三键有没有在场」,语义与它逐字同(键在场 = 子代帧,空串也算在场)。
|
|
656
|
+
const idSnap = {};
|
|
657
|
+
try {
|
|
658
|
+
for (const k of ['eventId', ...SEGMENT_END_IDENTITY_KEYS])
|
|
659
|
+
idSnap[k] = m[k];
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
// §C1 带理由的吞:读身份键本身抛(进程内帧的 accessor 抛)⇒ **整帧拒**,不发 chrome。
|
|
663
|
+
// 为什么这里吞是对的:身份决定的是「这条组成信息算谁的」——读不出归属时,任何一种猜(当主会话 / 当子代)
|
|
664
|
+
// 都会把一只子代的 prompt 组成挂到别人名下;而本臂是**可选渲染面**(required:false),不发的代价只是这一帧不上屏。
|
|
665
|
+
// 为什么不上报:适配层没有诊断出口(chrome 臂本身就是出口),再向上抛会打断整条 adapt 流、连带吞掉同批的真帧。
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (SEGMENT_END_IDENTITY_KEYS.some((k) => idSnap[k] !== undefined && typeof idSnap[k] !== 'string'))
|
|
669
|
+
return;
|
|
670
|
+
let laneProof = MAIN;
|
|
671
|
+
if (SEGMENT_END_IDENTITY_KEYS.some((k) => idSnap[k] !== undefined)) {
|
|
672
|
+
const parent = str(idSnap.parentToolCallId);
|
|
673
|
+
if (parent === undefined)
|
|
674
|
+
return;
|
|
675
|
+
laneProof = { lane: 'subagent', parentToolCallId: parent };
|
|
676
|
+
}
|
|
677
|
+
const eventId = str(idSnap.eventId);
|
|
678
|
+
yield chrome({
|
|
679
|
+
kind: 'prompt_assembled',
|
|
680
|
+
laneProof,
|
|
681
|
+
...(totalChars !== undefined ? { totalChars } : {}),
|
|
682
|
+
...(blocks !== undefined ? { blocks } : {}),
|
|
683
|
+
...(sections !== undefined ? { sections } : {}),
|
|
684
|
+
...(tools !== undefined ? { tools } : {}),
|
|
685
|
+
...(eventId !== undefined ? { eventId } : {}),
|
|
686
|
+
});
|
|
687
|
+
};
|
|
591
688
|
const textSegmentEndArm = function* (m, { text }) {
|
|
592
689
|
// 🔴 断闸按「**键在不在**」判,不按「是不是串」判(异源对抗复审第三轮 [medium] 采纳)。
|
|
593
690
|
// 投影层已对坏 lane 位整帧 fail-closed;这一道是给**非投影口**喂进来的帧(宿主自建管线 /
|
|
@@ -1315,6 +1412,8 @@ export const ARMS = new Map([
|
|
|
1315
1412
|
['context_usage', contextUsageArm],
|
|
1316
1413
|
// CC-89(0.75.0):压缩的非 compacted 结局(additive;不接 = 压缩失败对用户不可见)。
|
|
1317
1414
|
['compaction_outcome', compactionOutcomeArm],
|
|
1415
|
+
// CC-91(0.76.0):这一次 prepare 的提示词装配清单(additive;不接 = 端只能按本地镜像自己估)。
|
|
1416
|
+
['prompt_assembled', promptAssembledArm],
|
|
1318
1417
|
['wiring_manifest', wiringManifestArm],
|
|
1319
1418
|
// B-078 / L-208(0.65.0):design/172 流内审批两帧 —— 修前它们在投影层就 `dropped`,
|
|
1320
1419
|
// 壳自己另接一份 ⇒ desktop/web 拿不到卡集与撤卡(归层违例)。
|
|
@@ -276,6 +276,51 @@ export interface ContextUsageSectionView {
|
|
|
276
276
|
tokens: number;
|
|
277
277
|
kind?: string;
|
|
278
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* CC-91(0.76.0):`prompt_assembled.sections[]` 一行 —— 编译 IR 面的**一段**(core `prompt.assembled` 的 v2 面)。
|
|
281
|
+
*
|
|
282
|
+
* 🔴 **只有 `chars`,没有 `tokens`** —— 这不是本层的取舍,是上游真字节里就没有这一位:core 的臂声明
|
|
283
|
+
* (`core/trace.ts` `kind:"prompt.assembled"` 的 `sections?: Array<{id, slot, carrier, cadence, cacheClass, chars, hash, contentHash?}>`)
|
|
284
|
+
* 与 server 的逐字段白名单(`observability/prompt-manifest.ts` `promptManifestRecordOf`)两面都是 `chars`。
|
|
285
|
+
* ⇒ 本层**不铸** `tokens`:拿 `chars / 系数` 编一个数就是把「不知道」渲成一个看起来精确的假值。
|
|
286
|
+
* 要引擎自己的 token 估算,读 **`context_usage.sections[].tokens`**(core `prompt-assembly/section-usage.ts`
|
|
287
|
+
* `contextUsageSections` 就是拿同一份 `chars` 按引擎系数估的)。🔴 这条 join **只在两面的段视图都在场时成立**:
|
|
288
|
+
* 那一面也是从同一份 manifest 段列投影的,段列缺席时它同样缺席 ⇒ v1-only 腿上没有这条 join,那时**别**拿
|
|
289
|
+
* `blocks` 当段的同名替代({@link PromptManifestBlockView}:两张词表不同,且段→块可多对一)。
|
|
290
|
+
* 🔴 `id` 是 core pack 的 `core/<slot>.<name>` 命名空间**开集**(`core/role.base` / `core/harness.head` /
|
|
291
|
+
* `core/discovery.mcp-instructions` / `core/environment.context` / `core/memory.tail` …),**不是**任何端
|
|
292
|
+
* /context 面板的展示分类名;本层按开集读,认不得的 id 原词照带。
|
|
293
|
+
* `slot` = 语义槽(让视图不必认识每个 id 就能分组),**逐字沿用上游的键名**:同一件事在 `context_usage.sections[]`
|
|
294
|
+
* 那一面叫 `kind`,在本面叫 `slot`,两边各自跟自己的上游,本层**不改名**。改名看着整齐,代价是同一个事实在包内
|
|
295
|
+
* 多一个只有本包知道的别名 —— 而本层有两个入口(投影口 / 直喂 `adapt()`),别名一多两条路就会读出不同的值。
|
|
296
|
+
* 缺席 = 那个条目没声明槽,不是没语义。
|
|
297
|
+
*/
|
|
298
|
+
export interface PromptManifestSectionView {
|
|
299
|
+
id: string;
|
|
300
|
+
/** 这一段**渲染出来的字符数**(prepare 期冻结)。🔴 不是 token 数;别拿它与 `context_usage.usedTokens` 相减。 */
|
|
301
|
+
chars: number;
|
|
302
|
+
slot?: string;
|
|
303
|
+
}
|
|
304
|
+
/** CC-91(0.76.0):`prompt_assembled.blocks[]` 一行 —— v1 面的**带标签块**(`role.base` / `harness.context` /
|
|
305
|
+
* `env.context` / `memory.tail` …)。🔴 与 `sections[].id` 是**两张不同的词表**,而且**多对一**:多个段可折进
|
|
306
|
+
* 同一个块(core `prompt-assembly/types.ts` 的 `blockId`)⇒ 块不是段的另一种写法,别把两面混着分组。
|
|
307
|
+
* v1-only 引擎上段视图整键缺席,这一面是那时唯一的组成视图 —— 🔴 **那时没有与 `context_usage` 的 token join**
|
|
308
|
+
* (那一面的段视图同样是从 manifest 段列投影的,段列缺席时它也缺席)⇒ 别拿块 id 去 join 段面。
|
|
309
|
+
* ⚠️ 两张词表**没有命名空间隔离的保证**:v1 块 id 不带前缀(`role.base`),而 operator 声明的段 id 是自由串
|
|
310
|
+
* (core 的 `EpochDeclaredSections.id`)⇒ 理论上能撞出同名。所以「join 不上」不是唯一的失败形,
|
|
311
|
+
* **按面各自分组**才是安全读法,别靠 id 形状猜自己在哪一面。
|
|
312
|
+
* `chars` 同律:不是 token 数。 */
|
|
313
|
+
export interface PromptManifestBlockView {
|
|
314
|
+
id: string;
|
|
315
|
+
chars: number;
|
|
316
|
+
}
|
|
317
|
+
/** CC-91(0.76.0):`prompt_assembled.tools[]` 一行,**只留 wireName**(这一腿挂载了哪些工具;MCP 类按 server 分组渲染靠它)。
|
|
318
|
+
* 🔴 上游那一行的其余位(`aliases` / `contractId` / `implementationRevision` / `cardId` / `shapeDigest` / `wireSchemaDigest`)
|
|
319
|
+
* 是**内容寻址对账锚**、不是尺寸面,本层刻意不过境(要它们另立票,别顺手整只透传)。⚠️ 这一面**没有任何尺寸位** ——
|
|
320
|
+
* 「工具 schema 占了多少」在这条 wire 上读不出来,缺席不是零。 */
|
|
321
|
+
export interface PromptManifestToolView {
|
|
322
|
+
wireName: string;
|
|
323
|
+
}
|
|
279
324
|
/**
|
|
280
325
|
* CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
|
|
281
326
|
* costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no
|
|
@@ -46,6 +46,8 @@ export const INTERNAL_SDK_ARM_TYPES = new Set([
|
|
|
46
46
|
'context_usage',
|
|
47
47
|
// CC-89(0.75.0):压缩的非 compacted 结局 → 中性内部臂(print 车道同律:吞或转,裸透传 = 越界帧)。
|
|
48
48
|
'compaction_outcome',
|
|
49
|
+
// CC-91(0.76.0):这一次 prepare 的提示词装配清单 → 中性内部臂(print 车道同律:吞或转,裸透传 = 越界帧)。
|
|
50
|
+
'prompt_assembled',
|
|
49
51
|
'workflow_complete',
|
|
50
52
|
'tool_end_result',
|
|
51
53
|
'retry_status',
|
|
@@ -831,13 +833,14 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
831
833
|
// `meta`:流首帧的 taskId/sessionId 一等信号 —— 本切片无可渲染内容(runStream 只按 `eventSeq` 去重)。
|
|
832
834
|
// ⚠️ 活性普查(2026-09-19)更正:传输层**不**替宿主消费它;需要 sessionId / taskId 的端(desktop / web-client)各自直读本帧。
|
|
833
835
|
case 'meta':
|
|
834
|
-
// `file_link` / `
|
|
836
|
+
// `file_link` / `model_usage` / `config_assembled`:
|
|
835
837
|
// 引擎的可观测/审计面帧,本切片(CC transcript 投影)无对位渲染物;
|
|
836
838
|
// 消费它们的是 fleet/审计面而不是会话转录。
|
|
837
839
|
// ⚠️ `message_committed` **已在 0.65.0 搬出本档**(B-072 ④):它不是「没有对位物」——
|
|
838
840
|
// 它是 `compacted.preserved_segment` 的唯一解析腿。新臂见本文件 `case 'message_committed'`。
|
|
841
|
+
// ⚠️ `context_usage` 自 0.71.0 CC-08 ③、`compaction_outcome` 自 0.75.0 CC-89、`prompt_assembled` 自
|
|
842
|
+
// 0.76.0 CC-91 起各自**进切片**为内部臂 → chrome 臂,不再在本组(见各自的 case)。
|
|
839
843
|
case 'file_link':
|
|
840
|
-
case 'prompt_assembled':
|
|
841
844
|
case 'model_usage':
|
|
842
845
|
case 'config_assembled':
|
|
843
846
|
// `needs_review`:plan-review park 的**事件**形;壳消费的是 done{status:'needs_review'} 终帧
|
|
@@ -920,6 +923,11 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
920
923
|
// 没有任何一端看得见。转录里没有对位物(`compact_boundary` 只对成功压缩)⇒ 走内部臂 → chrome 臂,不编消息。
|
|
921
924
|
case 'compaction_outcome':
|
|
922
925
|
return compactionOutcomeProjection(ev, ctx);
|
|
926
|
+
// CC-91(0.76.0):`prompt_assembled` 从 not_in_slice 组里拿出来 —— 这一次 prepare 的**提示词装配清单**
|
|
927
|
+
// (每段多大、挂了哪些工具)是 /context 类面板的唯一引擎侧真值来源;此前三端只能按本地镜像自己估。
|
|
928
|
+
// 转录里没有对位物(它是 ids/digests/counts 的观测面,不是一条消息)⇒ 走内部臂 → chrome 臂,不编消息。
|
|
929
|
+
case 'prompt_assembled':
|
|
930
|
+
return promptAssembledProjection(ev, ctx);
|
|
923
931
|
case 'tool_progress':
|
|
924
932
|
return toolProgressProjection(ev, ctx);
|
|
925
933
|
default:
|
|
@@ -930,18 +938,42 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
930
938
|
return dropped('unknown_arm', String(ev.type ?? 'unknown'));
|
|
931
939
|
}
|
|
932
940
|
}
|
|
933
|
-
/**
|
|
941
|
+
/**
|
|
942
|
+
* 身份四键(`eventId` + 三个子流键)的**一次性快照**:每键**只读一次**,读取抛错 ⇒ 整份不可信。
|
|
943
|
+
*
|
|
944
|
+
* 🔴 为什么是快照而不是就地多读(异源对抗复审第三轮 [medium],实测可复现):wire 是 JSON,但**进程内**的帧
|
|
945
|
+
* (宿主自建管线 / 重放存量转录 / 测试替身)可以带 accessor。同一个键读两次拿到两个值时,「形门读一次、
|
|
946
|
+
* 抄写再读一次、lane 判定又读一次」会让同一帧在不同读点上归属**不同 lane** —— 一条子代帧能被洗成主会话的
|
|
947
|
+
* 组成信息。读一次、全程用同一份,是这条歧义唯一的关门方式;抛错的 getter 同样在这里被关住(此前会从投影口逸出)。
|
|
948
|
+
* ⚠️ 仍按 `ev[k]` 读(**走原型链**:进程内对象的身份键常长在原型上),只是每键只读一次。
|
|
949
|
+
*/
|
|
950
|
+
function identitySnapshot(ev) {
|
|
951
|
+
const snap = {};
|
|
952
|
+
try {
|
|
953
|
+
for (const k of ['eventId', ...SEGMENT_END_IDENTITY_KEYS])
|
|
954
|
+
snap[k] = ev[k];
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
return null; // 读身份都会抛的帧不可信 ⇒ 调用方判坏(绝不带着半份身份继续走)
|
|
958
|
+
}
|
|
959
|
+
return snap;
|
|
960
|
+
}
|
|
961
|
+
/** CC-08:三键身份(sdk `EventIdentity`)—— 在场非串 ⇒ 坏值不买路;在场(含空串)⇒ 原样透传;`eventId` 非空才带。
|
|
962
|
+
* 🔴 判据与抄写都取 {@link identitySnapshot} 的**同一份**读数(每键只读一次)。 */
|
|
934
963
|
function identityOrMalformed(ev, arm) {
|
|
964
|
+
const snap = identitySnapshot(ev);
|
|
965
|
+
if (snap === null)
|
|
966
|
+
return { identity: {}, bad: dropped('malformed', arm) };
|
|
935
967
|
for (const k of SEGMENT_END_IDENTITY_KEYS) {
|
|
936
|
-
if (
|
|
968
|
+
if (snap[k] !== undefined && typeof snap[k] !== 'string')
|
|
937
969
|
return { identity: {}, bad: dropped('malformed', arm) };
|
|
938
970
|
}
|
|
939
971
|
const identity = {};
|
|
940
972
|
for (const k of ['eventId', ...SEGMENT_END_IDENTITY_KEYS]) {
|
|
941
973
|
// 🔴 三键**空串也透**:子流律是「键在场 = 子代帧」(`isSubFlowSegmentEnd` / `text_end` 投影同律),按 length>0 才抄会把
|
|
942
974
|
// `sourceTaskId: ''` 的子代帧洗成主流(异源对抗复审第 1 轮 finding③);`eventId` 是幂等键,仍要非空。
|
|
943
|
-
if (typeof
|
|
944
|
-
identity[k] =
|
|
975
|
+
if (typeof snap[k] === 'string' && (k === 'eventId' ? snap[k].length > 0 : true))
|
|
976
|
+
identity[k] = snap[k];
|
|
945
977
|
}
|
|
946
978
|
return { identity, bad: null };
|
|
947
979
|
}
|
|
@@ -1357,6 +1389,89 @@ function compactionOutcomeProjection(frame, ctx) {
|
|
|
1357
1389
|
...identity,
|
|
1358
1390
|
}))));
|
|
1359
1391
|
}
|
|
1392
|
+
/**
|
|
1393
|
+
* CC-91(0.76.0):`prompt_assembled`(一次 prepare 的提示词装配清单)→ 中性内部臂。
|
|
1394
|
+
*
|
|
1395
|
+
* 逐键诚实:`totalChars` 有限非负才在场(缺席 ≠ 0);`blocks` / `sections` / `tools` 三面**逐行**挑
|
|
1396
|
+
* (`id` / `wireName` 非空串 ∧ `chars` 有限非负),坏行逐条丢、全丢光或非数组 ⇒ 该键缺席。
|
|
1397
|
+
* 🔴 于是键缺席是**三支合流**(上游整键缺席 / 空数组 / 逐行滤完只剩空),本层分不出是哪一支 ⇒ 对外只能说
|
|
1398
|
+
* 「本事件没有可读的这一面」,**不许**把它写成一句关于引擎的诊断(如「这条腿没装配出段 IR」)。不铸 `[]`:
|
|
1399
|
+
* 空数组会被读成「装配出来是空的」,那是另一件事。
|
|
1400
|
+
* 🔴 一条组成面的键都读不出 ⇒ `malformed`:core 把 `blocks` / `totalChars` 声明为必填,两件全丢的帧是坏 producer,
|
|
1401
|
+
* 投一份空清单会让消费端把「读不出」渲成「提示词是空的」。
|
|
1402
|
+
* 🔴 帧上的 `constitution`(谁拥有宪法层)**刻意不过境**:本包与三端今天没有任何消费场景,而每一个上了公面的键都是
|
|
1403
|
+
* 一条要长期兑现的契约 —— additive 键晚加不破坏、早加却删不掉。哪天有端点名要它,再按表态制加(本层零改动成本)。
|
|
1404
|
+
* 🔴 **零 `tokens` 铸点**(见 {@link PromptManifestSectionView} 头注):真字节里没有这一位。
|
|
1405
|
+
*/
|
|
1406
|
+
function promptAssembledProjection(frame, ctx) {
|
|
1407
|
+
// 🔴 不浅拷(与 compaction_outcome 同律):展开会擦掉原型链上的身份键(进程内对象把子代帧洗成主会话)、
|
|
1408
|
+
// 还会执行无关的可枚举 getter;就在同一份记录上逐键各读一次。
|
|
1409
|
+
const ev = frame;
|
|
1410
|
+
const { identity, bad } = identityOrMalformed(ev, 'prompt_assembled');
|
|
1411
|
+
if (bad !== null)
|
|
1412
|
+
return bad;
|
|
1413
|
+
const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
|
|
1414
|
+
const size = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined);
|
|
1415
|
+
const totalChars = size(ev.totalChars);
|
|
1416
|
+
const sections = (() => {
|
|
1417
|
+
if (!Array.isArray(ev.sections))
|
|
1418
|
+
return undefined;
|
|
1419
|
+
const rows = [];
|
|
1420
|
+
for (const r of ev.sections) {
|
|
1421
|
+
if (typeof r !== 'object' || r === null || Array.isArray(r))
|
|
1422
|
+
continue;
|
|
1423
|
+
const o = r;
|
|
1424
|
+
const id = str(o.id);
|
|
1425
|
+
const chars = size(o.chars);
|
|
1426
|
+
if (id === undefined || chars === undefined)
|
|
1427
|
+
continue;
|
|
1428
|
+
const slot = str(o.slot);
|
|
1429
|
+
rows.push({ id, chars, ...(slot !== undefined ? { slot } : {}) });
|
|
1430
|
+
}
|
|
1431
|
+
return rows.length > 0 ? rows : undefined;
|
|
1432
|
+
})();
|
|
1433
|
+
const blocks = (() => {
|
|
1434
|
+
if (!Array.isArray(ev.blocks))
|
|
1435
|
+
return undefined;
|
|
1436
|
+
const rows = [];
|
|
1437
|
+
for (const r of ev.blocks) {
|
|
1438
|
+
if (typeof r !== 'object' || r === null || Array.isArray(r))
|
|
1439
|
+
continue;
|
|
1440
|
+
const o = r;
|
|
1441
|
+
const id = str(o.id);
|
|
1442
|
+
const chars = size(o.chars);
|
|
1443
|
+
if (id === undefined || chars === undefined)
|
|
1444
|
+
continue;
|
|
1445
|
+
rows.push({ id, chars });
|
|
1446
|
+
}
|
|
1447
|
+
return rows.length > 0 ? rows : undefined;
|
|
1448
|
+
})();
|
|
1449
|
+
const tools = (() => {
|
|
1450
|
+
if (!Array.isArray(ev.tools))
|
|
1451
|
+
return undefined;
|
|
1452
|
+
const rows = [];
|
|
1453
|
+
for (const r of ev.tools) {
|
|
1454
|
+
if (typeof r !== 'object' || r === null || Array.isArray(r))
|
|
1455
|
+
continue;
|
|
1456
|
+
const wireName = str(r.wireName);
|
|
1457
|
+
if (wireName === undefined)
|
|
1458
|
+
continue;
|
|
1459
|
+
rows.push({ wireName });
|
|
1460
|
+
}
|
|
1461
|
+
return rows.length > 0 ? rows : undefined;
|
|
1462
|
+
})();
|
|
1463
|
+
if (totalChars === undefined && sections === undefined && blocks === undefined && tools === undefined) {
|
|
1464
|
+
return dropped('malformed', 'prompt_assembled');
|
|
1465
|
+
}
|
|
1466
|
+
return projected(internalArmExit(stamp(ctx, armBody({
|
|
1467
|
+
type: 'prompt_assembled',
|
|
1468
|
+
...(totalChars !== undefined ? { totalChars } : {}),
|
|
1469
|
+
...(blocks !== undefined ? { blocks } : {}),
|
|
1470
|
+
...(sections !== undefined ? { sections } : {}),
|
|
1471
|
+
...(tools !== undefined ? { tools } : {}),
|
|
1472
|
+
...identity,
|
|
1473
|
+
}))));
|
|
1474
|
+
}
|
|
1360
1475
|
function engineNoticeProjection(ev, ctx) {
|
|
1361
1476
|
const code = typeof ev.code === 'string' && ev.code.length > 0 ? ev.code : undefined;
|
|
1362
1477
|
if (code === undefined)
|
package/dist/agentsWireCaps.d.ts
CHANGED
|
@@ -18,10 +18,23 @@ export type TaskAgentWireSkill = {
|
|
|
18
18
|
};
|
|
19
19
|
/** CC 形自定义 agent 的 `memory:` 三词(cli `loadAgentsDir` 的 zod 枚举同源)。 */
|
|
20
20
|
export declare const AGENT_MEMORY_WORDS: ReadonlySet<string>;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* `agents[].memory` 上 wire 的形(上游闭合白名单 `{ scopes?, writeScope?, enabled?, scopeContract? }` 的子集:本包只用前两键)。
|
|
23
|
+
*
|
|
24
|
+
* 🔴 **CC-96 放宽**:`writeScope` 此前写死成 `string`(恒在、且不可为 `null`),而 wire 的取值域是 `string | null`
|
|
25
|
+
* (上游直讯确认该域不变)。写死的后果是**读得出却铸不出** —— {@link import('./memorySpecWire.js').readAgentMemorySpec}
|
|
26
|
+
* 能把显式 `null` 读成「这一次 run 对记忆面只读」(无 remember 工具、不做 consolidation 写,recall 仍可),
|
|
27
|
+
* 而端**没有任何办法经本包声明**这一形:只剩「写到某一层」一条路。半条腿的读面比没有更坏,因为它让
|
|
28
|
+
* 「本端支持只读记忆」看起来是真的。
|
|
29
|
+
* · `writeScope: null` = 显式声明只读;
|
|
30
|
+
* · `writeScope` 键缺席 = 未指定(缺省归引擎);
|
|
31
|
+
* · `writeScope: '<层名>'` = 写到那一层。
|
|
32
|
+
* 🔴 放宽的是**类型**,不是本包投影器的纪律:{@link projectAgentsForWire} 仍**恒显式钉** `writeScope`
|
|
33
|
+
* (见下方投影处的 🔴 注),漏钉会让新条目静默写进全局层 —— 那条约束一个字都没松。
|
|
34
|
+
*/
|
|
22
35
|
export type TaskAgentWireMemory = {
|
|
23
36
|
scopes: string[];
|
|
24
|
-
writeScope
|
|
37
|
+
writeScope?: string | null;
|
|
25
38
|
};
|
|
26
39
|
/** 发往 server 的单个 agent 定义(白名单内字段的壳侧子集——observer 面壳无语义,恒不发;`memory` 自 0.72.3 按定义发)。 */
|
|
27
40
|
export type TaskAgentWireItem = {
|
|
@@ -549,7 +549,19 @@ export function publishEngineAgentPanelEvent(ev) {
|
|
|
549
549
|
// 换成「每行各自补一条 end」则要在这里替主路决定消费端键,会吞掉原 wire 键那一行自己的终态。身份对不上就
|
|
550
550
|
// **一帧都不摘**是本包扣留表的既有律(见 `hitl/gateLedger.ts` 的「判不出 ⇒ 一条都不摘」)。代价 = 这种形下
|
|
551
551
|
// 那几条行的用量仍缺席(与本件修之前同),换来的是绝不凭空多长一行、绝不替谁编一条终态。
|
|
552
|
-
//
|
|
552
|
+
// 🔴 **理由更正(0.76.0;外部验收方 [7764] 逐条回一级源码亲核后证伪了本句旧版)**:旧注释写的是「两个行尾段共享一个
|
|
553
|
+
// **转录锚**不成立(转录锚按代际唯一),所以这一支是防御面」—— 那句话**只论证了转录钥匙那条腿**,而 `heldMatches`
|
|
554
|
+
// 是**转录锚腿 ∪ 委派卡腿**的并集,两条腿异质:
|
|
555
|
+
// · 委派卡那条腿的前提**为假** —— workflow 扇出下一张卡整跑冻结成常量、尾段逐 callKey 不同,**最多 50 个**
|
|
556
|
+
// 尾段共享同一张卡且可同时在飞(上游坐标:core `workflow.ts` 的 `bceParentToolCall` 与 `waIdOf(callKey)`;
|
|
557
|
+
// 默认 `maxAgents` 50)。这正是本包 lane 判据里 `workflow-card-parent` 那一支的上游本体。
|
|
558
|
+
// 🔴 上游当事方 09-20 直讯确认了这一形:**一次扇出的全部子代共享同一张 `parentToolCallId`**(那次 workflow
|
|
559
|
+
// 工具调用自己的卡),逐子代能区分的键**只有 `taskId`**(`wa*` 形)—— 也就是说委派卡这条腿在扇出下**天然多对一**。
|
|
560
|
+
// · 更要紧:**凑够 ≥2 根本不需要两行共享任何一个键** —— 一行只经转录锚命中、另一行只经委派卡命中即可。
|
|
561
|
+
// ⇒ 今天这一支之所以罕见,**靠的不是上面那条律**,而是「workflow 子代的 tick 被 lane 门拦掉 ⇒ 委派卡那条腿的
|
|
562
|
+
// `publishedWireByParent` 没有写入」这条**更脆**的保证 —— 它历史上塌过(上游投影白名单曾把 `workflowRunId` 整个剥掉)。
|
|
563
|
+
// **失效条件**:workflow 子代的 tick 一旦上屏(白名单变宽 / lane 门放行),这一支立刻变成常态路径。
|
|
564
|
+
// 所以本分支**不是**「理论上才会走到的防御面」,它是**有真上游机制**的一支,只是今天被一条脆保证挡着。
|
|
553
565
|
// 🔴 无论放没放,**主路照走**:原 wire 键那一行(它可能早就上过屏)自己的 end 绝不能被这条腿吞掉。
|
|
554
566
|
const releasable = [];
|
|
555
567
|
for (const tail of heldMatches) {
|
|
@@ -66,6 +66,10 @@ export declare const CONFIG_LIMIT_UNKNOWN_KEY = "config.limit_unknown_key";
|
|
|
66
66
|
* 🔴 与本族其余成员同律:**坏旋钮响亮拒,绝不静默折回缺省**。
|
|
67
67
|
*/
|
|
68
68
|
export declare const CONFIG_DELEGATION_ENTRY_CAPS = "config.delegation_entry_caps";
|
|
69
|
+
/** 记忆面 project key 的拼写不合法(**400**;构造期拒,什么都没跑)。 */
|
|
70
|
+
export declare const CONFIG_MEMORY_PROJECT_KEY_SPELLING = "config.memory_project_key_spelling";
|
|
71
|
+
/** 这次请求的记忆写入范围与已生效的那一份不一致(**409**;构造期拒,什么都没跑 —— 冲突,不是拼写错)。 */
|
|
72
|
+
export declare const CONFIG_MEMORY_WRITE_SCOPE_MISMATCH = "config.memory_write_scope_mismatch";
|
|
69
73
|
/**
|
|
70
74
|
* 已知的配置拒绝码(**识别表,非白名单**)。判「这是不是一条配置拒绝」请用
|
|
71
75
|
* {@link isConfigRefusalCode} —— 它按 `config.` 前缀判,未来新成员自动落进来。
|
package/dist/engineErrorCodes.js
CHANGED
|
@@ -76,6 +76,18 @@ export const CONFIG_LIMIT_UNKNOWN_KEY = 'config.limit_unknown_key';
|
|
|
76
76
|
* 🔴 与本族其余成员同律:**坏旋钮响亮拒,绝不静默折回缺省**。
|
|
77
77
|
*/
|
|
78
78
|
export const CONFIG_DELEGATION_ENTRY_CAPS = 'config.delegation_entry_caps';
|
|
79
|
+
// ── 记忆面的两个**构造期**拒码(CC-96;`agents[].memory` / 请求记忆面的同族第二波)────────────
|
|
80
|
+
//
|
|
81
|
+
// 两码与本族其余成员同律(「配置错了」不是「跑失败了」),但**多一条**:它们拒在**受理之前** ——
|
|
82
|
+
// 这一次运行什么都没跑,所以出路一律是「改请求再发」,不是等、也不是去查运行结果。
|
|
83
|
+
// 🔴 **分诊归引擎,本包不预铸第二判官**:拼写合不合法 / 范围算不算不匹配,判据都在引擎侧;本端照码渲一句话
|
|
84
|
+
// (单铸口 = `src/memorySpecWire.ts` 的 `memoryConfigRefusalNoteOf`),绝不自己先算一遍再「帮」它判。
|
|
85
|
+
// ⚠️ 两码的 HTTP 状态**不同**(本族此前清一色 400):拼写 400 / 不匹配 409。状态不是判据(判据是码),
|
|
86
|
+
// 但它是**处置差**的来源 —— 一条是打字错,一条是与已生效的那一份冲突,渲成同一句会把两种出路混掉。
|
|
87
|
+
/** 记忆面 project key 的拼写不合法(**400**;构造期拒,什么都没跑)。 */
|
|
88
|
+
export const CONFIG_MEMORY_PROJECT_KEY_SPELLING = 'config.memory_project_key_spelling';
|
|
89
|
+
/** 这次请求的记忆写入范围与已生效的那一份不一致(**409**;构造期拒,什么都没跑 —— 冲突,不是拼写错)。 */
|
|
90
|
+
export const CONFIG_MEMORY_WRITE_SCOPE_MISMATCH = 'config.memory_write_scope_mismatch';
|
|
79
91
|
/**
|
|
80
92
|
* 已知的配置拒绝码(**识别表,非白名单**)。判「这是不是一条配置拒绝」请用
|
|
81
93
|
* {@link isConfigRefusalCode} —— 它按 `config.` 前缀判,未来新成员自动落进来。
|
|
@@ -86,6 +98,8 @@ export const CONFIG_REFUSAL_CODES = new Set([
|
|
|
86
98
|
CONFIG_STRATEGY_MAX_SIZE_INVALID,
|
|
87
99
|
CONFIG_LIMIT_UNKNOWN_KEY,
|
|
88
100
|
CONFIG_DELEGATION_ENTRY_CAPS,
|
|
101
|
+
CONFIG_MEMORY_PROJECT_KEY_SPELLING,
|
|
102
|
+
CONFIG_MEMORY_WRITE_SCOPE_MISMATCH,
|
|
89
103
|
]);
|
|
90
104
|
// ── 委派席位到限族(core 5.48.0 design/323,[4743] @cli 点名的「两新 coded 拒绝」)──────────────
|
|
91
105
|
//
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 诚实来源亲核(sdk 9.8.1 实装字节):
|
|
10
10
|
* · `task_progress` 臂(`dist/events.d.ts`)上**没有** `agentType`、也没有 `spawnAgentType` —— tick 这条
|
|
11
|
-
* lane 今天根本不报类型,所以本层**不读**无出处的键(读了就是替上游发明一个字段)
|
|
11
|
+
* lane 今天根本不报类型,所以本层**不读**无出处的键(读了就是替上游发明一个字段)。🔴 **措辞更正(外部验收方 [7764])**:因此 tick 这一档**不是一条「今天恒不命中的路径」,它根本没有代码** —— 本文件里 `agentType` 的三处出现全在注释,唯一的代码走的是 fleet 行帧那一档。相应地,「上游铸位当天零改生效」这句**不成立**:上游若铸出 `spawnAgentType`,本包**没有读口**,仍需改码补一档;
|
|
12
12
|
* · `FleetTaskRow.agentType`(`dist/resources/fleet.d.ts`,§K-7 的短类型 label,如 `Explore` /
|
|
13
13
|
* `general-purpose`)**有出处** —— 但 `fleet/fleetProjection.ts` 把它**折进** `FleetTaskView.name`
|
|
14
14
|
* (`deriveAgentLabel` = `agentName ?? agentType`,登记在
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* approvalOutcomeNote — 流内帧腿的审批结局回交(`AskGateWireDeps.onToolApprovalOutcome(frame, outcome)`,0.74.1 CC-68)
|
|
3
|
-
* 到宿主「审批结局便签」的**单源映射**(0.74.5 CC-88;此前三端装配点各手拼一份,语义以 cli 1.0.123 现行为准):
|
|
4
|
-
* · `approvalId` = 帧上的原词;
|
|
5
|
-
* · `settled` = `outcome.decision !== 'unresolved'`(respond 送达并落定;**不看** ack 在不在 —— ack 是 server 的回执形,宿主不读);
|
|
6
|
-
* · `retracted` 只在 `outcome.retracted === true` 时带键(**独立判别位**,不折进 detail:撤卡 ≠ 投递失败 —— 释放认领但不扣重出卡预算);
|
|
7
|
-
* `editRefused` **不算** retracted,只折进 detail;
|
|
8
|
-
* · `detail` 三支优先序:撤卡固定句 → 编辑被拒固定句 → `respondRefusal` 的 errorCode / message 过滤非空后 ` · ` 拼接;
|
|
9
|
-
* 拼出空串 ⇒ 键缺席(绝不编一句;不用决断词、不用 ack 摘要)。
|
|
10
|
-
*/
|
|
11
1
|
import type { ToolApprovalFrame, ToolApprovalFrameOutcome } from './toolApprovalWire.js';
|
|
12
2
|
/** 宿主便签形(三端同一形;`retracted` / `detail` 在场才带)。 */
|
|
13
3
|
export interface StreamApprovalOutcomeNote {
|