@sema-agent/client-core 0.75.0 → 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 +49 -0
- package/README.md +7 -2
- package/dist/adapt/arms.js +120 -6
- package/dist/adapt/panelTasks.d.ts +6 -2
- package/dist/adapt/panelTasks.js +6 -3
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +45 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +121 -6
- package/dist/agentSession/backgroundView.js +2 -1
- package/dist/agentsWireCaps.d.ts +15 -2
- package/dist/engineAgentPanelStore.d.ts +6 -0
- package/dist/engineAgentPanelStore.js +113 -10
- package/dist/engineErrorCodes.d.ts +4 -0
- package/dist/engineErrorCodes.js +14 -0
- package/dist/fleet/fleetLedger.js +6 -0
- package/dist/fleet/fleetProjection.js +7 -2
- package/dist/fleet/fleetRowAgentType.d.ts +9 -0
- package/dist/fleet/fleetRowAgentType.js +59 -0
- package/dist/fleetAgentPanelProjection.js +24 -3
- 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/panelRunningHistory.d.ts +21 -0
- package/dist/panelRunningHistory.js +25 -0
- package/dist/seam.d.ts +64 -5
- package/dist/seam.js +10 -1
- package/docs/INTEGRATION-CLIENTS.md +112 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,55 @@
|
|
|
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
|
+
|
|
79
|
+
## 0.75.1(2026-09-20)
|
|
80
|
+
|
|
81
|
+
> 主题:**面板反序缓行的残洞**(外部复验读数:短命子代的最终用量在一种到达序下永远交不出)+ **缓行遇同代 running 帧的三向定形** + **`spawnName` 透传与 `agentType` 改读诚实来源**(#969 提货,CC-95)。**patch**:型面 additive(tick 臂 +1 可选位),公面值导出零增;行为面三笔(见下)。接入面 §75。
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- **反序缓行永远放不出(0.75.0 的残洞;外部复验 D1)**:`tick{UUID, cardBound}` 先上屏、终态行随后被缓住、`end{UUID}` 最后到时,漏斗「三把可能的键」**全查不到** —— 因为后两把只在行帧**真发布过**时才登记,而缓行的定义就是「还没发布」。结果:消费端只得到一条空的 ended 行,那一行唯一带最终用量的帧被吞。现在 `end` 归一加**第四条腿**:按缓行**自带的** wire 侧键(`transcriptId` 主 / 委派卡 id 退路)反查缓行,放行仍走「代际三向判 → 同键核 → 经原漏斗归一」,且**整条腿永不 `return`** —— 原 wire 键那一行自己的 `end` 不会被它吞掉。🔴 候选**多于一条**、或候选放行后会与那条 `end` 分流到两把键时,本包**一条都不放**(宁可晚交用量,也不让一只子代分成两行);那几条行的用量仍缺席,记 KNOWN-LIMITS。
|
|
86
|
+
- **缓着终态行时后到的 `running` 帧被无条件当复活(外部复验 B4)**:最终用量会被陈旧快照顶成小数字。现在三向 —— **可证同代** = 陈旧快照(整帧不落地、零事件、缓行留着、台账保持 settled)/ **可证更新** = 复活(作废缓行)/ **判不出** = 维持 0.75.0(KL-26)。可比性只看**帧自带的**两键(沿用记住的值会把「缺席」变成「相等」);`startedAt` 的形核补齐 `Number.isFinite`,与周期判据同源(族扫同批改齐 fleet 投影与后台视图两处)。
|
|
87
|
+
- **`subagent_lifecycle.agentType` 此前发的是委派卡的 `description`**(不是类型):tick 臂的 `name` 是上游由 `description` 派生的**展示名**,喂进 `agentType` 位后宿主的 SubagentStart hook 收到的是任务描述 —— 一个看起来合理的错答案。现在按**诚实来源**三档取:tick 上的结构读类型键(亲核 sdk 9.8.1:`task_progress` 臂**没有** `agentType` / `spawnAgentType`,今天恒不命中)→ fleet 行帧的 `agentType`(在被 `deriveAgentLabel` 折进 `name` **之前**留一份,新包内叶 `src/fleet/fleetRowAgentType.ts`)→ 都没有 ⇒ **键缺席**。🔴 缺席是新的可能形,端的 hook 侧要有 absent 兜底。
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- **`panel_task` tick 臂 `spawnName?`**(#969 提货,additive):结构读 wire `task_progress.spawnName`,非空串才带;**缺席 = 产生者还没有这一位**(旧 core / 旧 server),不是「没有名字」,也绝不用展示名 `name` 顶替。与 `name` 是两位:一个是展示名,一个是派生这只子代时被声明的名字。
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- 0.75.0 发车文案三处更正(已发段是冻结面,一字不动,更正记在本段):① 「`.d.ts` 改五只」实测**四只**(重跑请求要由两版 dist 逐文件 diff 反推,不按族概括);② `panelRunningHistory.d.ts` 的读口改名属**包内叶面**,不出公面 —— 「型面 additive」那句只覆盖公面,叶面这一笔当时没写;③ 「三把可能的键」的真实射程 = 行**没发布过**时只有第一把有效(本版修掉后头注已改成实情,并加了第四条腿)。
|
|
96
|
+
|
|
97
|
+
### Guards
|
|
98
|
+
|
|
99
|
+
- panel-cycle 门 96 → **170 checks**(D1 两序与退路键 / 不张冠李戴 / B4 三向 / 多候选一条都不放 / 缓行真作废 / `spawnName` 与 `agentType` 三档);变异 19 枚(18 见红 + 1 可证等价);异源对抗复审三轮每轮一条 [high] 全采修(多缓行别名覆盖 / 递归派发吞掉 wire 行的 end / 候选与 end 分流到两把键)。diff 门加一只壳侧版本时差归一器(带反空转与退役条件,**刻意不进** `ADAPTER_DIVERGENCES` —— 它不是有意偏离基线)。
|
|
100
|
+
|
|
52
101
|
## 0.75.0(2026-09-20)
|
|
53
102
|
|
|
54
103
|
> 主题:**压缩的非 compacted 结局上 chrome 面**(CC-89;此前三端都看不见压缩失败)+ **peer sdk 地板 `>=9.8.1`**(CC-81)+ **短命子代的最终用量不再丢**(0.74.5 首见即终态零事件的代价面,外部复验读数)。**minor**:① 新内部臂 `compaction_outcome` 进 `INTERNAL_SDK_ARM_TYPES` —— 自建 print / stream-json 出口的端**必须**先给它表态(吞或转),否则它会顺着兜底透传成越界帧;② peer 地板抬升。型面 additive(`ChromeEvent` +1 臂 `CompactionOutcomeChromeEvent`;值导出零增)。接入面 §74。
|
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,9 +317,13 @@ 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. |
|
|
322
|
-
| `scripts/run-
|
|
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. |
|
|
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. |
|
|
325
329
|
| `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. |
|
|
@@ -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';
|
|
@@ -11,6 +11,8 @@ import { isWorkflowAgentTaskId, recordWorkflowAgentTaskId } from '../workflow.js
|
|
|
11
11
|
// L-215+(0.65.0;core [6908]):终局状态词的单铸谓词(三处同形一次改齐)。
|
|
12
12
|
import { isTerminalStatus } from '../runTerminal.js';
|
|
13
13
|
import { resolveEnginePanelTaskId } from '../engineAgentPanelStore.js';
|
|
14
|
+
// 0.75.1 CC-95(b):子代类型的诚实来源台账(fleet 行帧折进 name 之前留的那一份;包内叶)。
|
|
15
|
+
import { fleetRowAgentType } from '../fleet/fleetRowAgentType.js';
|
|
14
16
|
import { chrome, MAIN, transcript, messageIdentityOf } from './ids.js';
|
|
15
17
|
import { CANCEL_MESSAGE, decisionOf, flattenWireOutput, REJECT_MESSAGE, sanitizeToolUseBlock, shortTaskLabel, TASK_TOOL_NAMES, WORKFLOW_TOOL_NAMES, } from './wireShapes.js';
|
|
16
18
|
// ══ M2/M1 —— assistant(tool_use 半场 + durable 文本/思考半场)══════════════════════════════════
|
|
@@ -586,6 +588,103 @@ const compactionOutcomeArm = function* (m) {
|
|
|
586
588
|
...(eventId !== undefined ? { eventId } : {}),
|
|
587
589
|
});
|
|
588
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
|
+
};
|
|
589
688
|
const textSegmentEndArm = function* (m, { text }) {
|
|
590
689
|
// 🔴 断闸按「**键在不在**」判,不按「是不是串」判(异源对抗复审第三轮 [medium] 采纳)。
|
|
591
690
|
// 投影层已对坏 lane 位整帧 fail-closed;这一道是给**非投影口**喂进来的帧(宿主自建管线 /
|
|
@@ -823,6 +922,9 @@ const taskProgressArm = function* (m, { ctx, cards, panel, inst }) {
|
|
|
823
922
|
// ("Bash npm test"),service 已 redact。PROGRESS 语义(喂状态/摘要显示槽),不是诊断。
|
|
824
923
|
// 老引擎缺席 ⇒ 两个消费面维持今天的显示。
|
|
825
924
|
const currentAction = typeof m.currentAction === 'string' && m.currentAction.length > 0 ? m.currentAction : undefined;
|
|
925
|
+
// 0.75.1 CC-95(a)—— `spawnName`:sdk 9.8.1 的 `task_progress` 臂上还没有这一位(新形才铸),所以按
|
|
926
|
+
// **结构读**取(不靠型面),非空串才算数;缺席 = 旧形,不编。
|
|
927
|
+
const spawnName = m.spawnName;
|
|
826
928
|
// inline 群组行孪生:**绑定了**的 tick 同时喂转录里那条行的统计(按委派卡 id 键)。
|
|
827
929
|
if (cardId !== undefined) {
|
|
828
930
|
yield chrome({
|
|
@@ -854,15 +956,22 @@ const taskProgressArm = function* (m, { ctx, cards, panel, inst }) {
|
|
|
854
956
|
: typeof input.task === 'string'
|
|
855
957
|
? input.task
|
|
856
958
|
: undefined;
|
|
959
|
+
// #6 常驻台账的两个写口整臂让位给 M3(module store 不许两个模块各写各的)。
|
|
960
|
+
// 0.74.3 CC-70:常驻标记在**归一后的键**下(fleet 尾段 = 通知车道清标用的那把);此前 UUID 记、17 位清 ⇒ 永不清。
|
|
961
|
+
// 🔴 0.75.1 CC-95(b):归一必须在 `panel.start` **之前**算 —— 子代类型的诚实来源是 fleet 行帧的 `agentType`,
|
|
962
|
+
// 而那本账按**行 id 尾段**记,拿 wire 的 UUID 是查不到的。
|
|
963
|
+
const identity = resolveEnginePanelTaskId(taskId, explicitParent);
|
|
857
964
|
// ⟨帧序耦合 5/7⟩ 首 tick 才 fire Start,而通知臂的 if-started 门读的就是它。
|
|
858
965
|
if (!panel.isLive(taskId)) {
|
|
859
966
|
// 这条引擎子代在壳侧**可观测的起点** = 本 turn 的首 tick。
|
|
860
|
-
|
|
861
|
-
|
|
967
|
+
// 🔴 0.75.1 CC-95(b)子代**类型**的诚实来源(此前取的是 `m.name` = 上游由委派卡 description 派生的**展示名**
|
|
968
|
+
// ⇒ 宿主的子代启动 hook 收到的是任务描述,一个看起来合理的错答案):
|
|
969
|
+
// ① wire `task_progress` 上的结构读类型键 —— 亲核 sdk 9.8.1 `dist/events.d.ts` 的 `task_progress` 臂:
|
|
970
|
+
// **没有** `agentType`、也没有 `spawnAgentType`,这条 lane 今天不报类型 ⇒ **有出处才读**,所以不读;
|
|
971
|
+
// ② fleet 行帧的 `agentType`(sdk `FleetTaskRow.agentType`,§K-7 短类型 label)—— 按归一后的尾段查;
|
|
972
|
+
// ③ 都没有 ⇒ **键缺席**(宿主 hook 侧按 absent 兜底,不编 `subagent`)。
|
|
973
|
+
yield* panel.start(taskId, fleetRowAgentType(identity.taskId));
|
|
862
974
|
}
|
|
863
|
-
// #6 常驻台账的两个写口整臂让位给 M3(module store 不许两个模块各写各的)。
|
|
864
|
-
// 0.74.3 CC-70:常驻标记在**归一后的键**下(fleet 尾段 = 通知车道清标用的那把);此前 UUID 记、17 位清 ⇒ 永不清。
|
|
865
|
-
const identity = resolveEnginePanelTaskId(taskId, explicitParent);
|
|
866
975
|
panel.noteResidency(taskId, identity.taskId);
|
|
867
976
|
panel.markLive(taskId);
|
|
868
977
|
yield chrome({
|
|
@@ -872,6 +981,9 @@ const taskProgressArm = function* (m, { ctx, cards, panel, inst }) {
|
|
|
872
981
|
kind: 'tick',
|
|
873
982
|
taskId,
|
|
874
983
|
...(typeof m.name === 'string' && m.name.length > 0 ? { name: m.name } : {}),
|
|
984
|
+
// 0.75.1 CC-95(a):派生这只子代时被声明的名字(wire `task_progress.spawnName`)。结构读、非空串才带;
|
|
985
|
+
// 缺席 = 旧形的产生者 ⇒ 键不落(不编、不拿展示名 `name` 冒充 —— 两位是两位)。
|
|
986
|
+
...(typeof spawnName === 'string' && spawnName.length > 0 ? { spawnName } : {}),
|
|
875
987
|
...(description !== undefined ? { description } : {}),
|
|
876
988
|
...(prompt !== undefined ? { prompt } : {}),
|
|
877
989
|
...(currentAction !== undefined ? { currentAction } : {}),
|
|
@@ -1300,6 +1412,8 @@ export const ARMS = new Map([
|
|
|
1300
1412
|
['context_usage', contextUsageArm],
|
|
1301
1413
|
// CC-89(0.75.0):压缩的非 compacted 结局(additive;不接 = 压缩失败对用户不可见)。
|
|
1302
1414
|
['compaction_outcome', compactionOutcomeArm],
|
|
1415
|
+
// CC-91(0.76.0):这一次 prepare 的提示词装配清单(additive;不接 = 端只能按本地镜像自己估)。
|
|
1416
|
+
['prompt_assembled', promptAssembledArm],
|
|
1303
1417
|
['wiring_manifest', wiringManifestArm],
|
|
1304
1418
|
// B-078 / L-208(0.65.0):design/172 流内审批两帧 —— 修前它们在投影层就 `dropped`,
|
|
1305
1419
|
// 壳自己另接一份 ⇒ desktop/web 拿不到卡集与撤卡(归层违例)。
|
|
@@ -61,8 +61,12 @@ export interface PanelTaskLedger {
|
|
|
61
61
|
/** #6 常驻台账:live-bound ⇒ 清常驻;inert/unbound ⇒ 记常驻(module store 的**唯一**写口之一)。 */
|
|
62
62
|
/** 0.74.3 CC-70:`residentKey` = 常驻标该记在哪把键下(归一后的 fleet 尾段);缺省 = taskId。绑卡判定仍按 tick 的 taskId。 */
|
|
63
63
|
noteResidency(taskId: string, residentKey?: string): void;
|
|
64
|
-
/**
|
|
65
|
-
|
|
64
|
+
/**
|
|
65
|
+
* SubagentStart(类型恒记,fire 一生一次)。
|
|
66
|
+
* 🔴 0.75.1 CC-95(b):`agentType` **可缺席** —— 调用方查不到诚实来源时就不传,本臂让键缺席,
|
|
67
|
+
* 绝不在这里编一个占位词(此前传的是 tick 的展示名,子代启动 hook 因此收到的是委派卡的 description)。
|
|
68
|
+
*/
|
|
69
|
+
start(taskId: string, agentType?: string): Generator<AdapterOutput>;
|
|
66
70
|
/** 关卡 settle(cardId)或 turn 末/abort 防御 sweep(cardId===null)。 */
|
|
67
71
|
settle(cardId: string | null, isError: boolean, report?: string): Generator<AdapterOutput>;
|
|
68
72
|
/** #6 通知-settle 边(推送半场):真终态在此落行,session 常驻行不再被 turn sweep 假结。 */
|
package/dist/adapt/panelTasks.js
CHANGED
|
@@ -108,13 +108,16 @@ export function createPanelTaskLedger(ctx, cards, inst) {
|
|
|
108
108
|
},
|
|
109
109
|
/** SubagentStart(cli fireSubagentStartHook 的臂化):类型恒记,fire 一生一次。 */
|
|
110
110
|
*start(taskId, agentType) {
|
|
111
|
-
|
|
111
|
+
// 0.75.1 CC-95(b):类型**在场才记**(缺席不写空串进表 —— stop 那一半读的是同一份记忆,写了空串
|
|
112
|
+
// 就等于把「不知道」渲成了一个词)。Stop 的类型查找恒记(fire 与否无关)。
|
|
113
|
+
if (agentType !== undefined)
|
|
114
|
+
subagentTypeById.set(taskId, agentType);
|
|
112
115
|
// 🔴 判重 + 记号都在**唯一那条 yield 之前**(矩阵 §2.1②:零残余窗,别挪)。
|
|
113
116
|
if (inst.hasFiredSubagentStart(taskId))
|
|
114
117
|
return;
|
|
115
118
|
inst.markFiredSubagentStart(taskId);
|
|
116
|
-
ctx.probe?.('hook', `start ${taskId} type=${agentType}`);
|
|
117
|
-
yield chrome({ kind: 'subagent_lifecycle', laneProof: laneOf(taskId), phase: 'start', taskId, agentType });
|
|
119
|
+
ctx.probe?.('hook', `start ${taskId} type=${agentType ?? '-'}`);
|
|
120
|
+
yield chrome({ kind: 'subagent_lifecycle', laneProof: laneOf(taskId), phase: 'start', taskId, ...(agentType !== undefined ? { agentType } : {}) });
|
|
118
121
|
},
|
|
119
122
|
/**
|
|
120
123
|
* 结掉一张关闭中的卡所绑的面板行(`cardId`),或 turn 末/abort 结掉**本 turn 活绑**的全部行
|
|
@@ -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)
|