@sema-agent/client-core 0.31.0 → 0.33.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 CHANGED
@@ -16,7 +16,84 @@
16
16
  > 🔴 **互链**(web [C166]⑦):各版「已知局限」段只记**该版新增**;接入面已知局限的完整台账在
17
17
  > `docs/INTEGRATION-CLIENTS.md` §6e/§7 —— **只读其一会漏**,两处都过。
18
18
 
19
- ## 0.31.0(未发布)
19
+ ## 0.33.0(2026-08-17)
20
+
21
+ **#289 批面②③(dogfood 案1②/案5,[4199] T2/T5 RED 修)——additive,零 BREAKING。**
22
+
23
+ - `adapter/activeRunSelfHeal`:新 `RejectedSubmissionOrigin`('user'|'injected';`activeRunSelfHealRow` additive 第四参,缺省 'user' 时输出逐字节不变)+ `SelfHealSubmissionDisposition` / `selfHealSubmissionDisposition()`(文案与宿主重排队判据共用同一分类函数)+ `injectedSubmissionRow`。系统注入件(plan-review 回执/cron 唤醒/task 通知)撞 409 不再被说成「Your message was NOT sent … send it again」。
24
+ - `adapter/runStream`:治理终态(`limits.*` 开集前缀 / `output.invalid` / `status:'blocked'`)铸中性行——新 `RUN_STOPPED_MESSAGE_PREFIX` / `RUN_BLOCKED_MESSAGE_PREFIX` / `isGovernanceStopRowText()` / `isGovernanceTerminal()`;真 provider 错与认不出的码保留 `API Error:` 身份。
25
+ - `engineErrorCodes`:新 `LIMITS_ERROR_CODE_PREFIX` + `isLimitsExceededCode()`(开集前缀谓词)。
26
+ - 新常驻门 `run-terminal-identity-copy-test`(107 断言);public-export-baseline 741→748。
27
+
28
+ ## 0.32.0(2026-08-16)
29
+
30
+ **1.0.80 发包前扫码三件(#280 件A / #281 / #284)。公开面 additive 两件 + 行为面修一件;
31
+ 无 BREAKING —— 两处签名都是 additive,老调用方逐字零行为变化。**
32
+
33
+ - **#280 件A / steer 取址并入 [4000] Q3=B(行为面,additive 参数)**:
34
+ `steerEngineSubagent(target, text, childTaskId?)` 补第三参。`engineSubagentSteer.ts` 此前直取
35
+ `activeEngineRunId()` —— 它是取址**第四腿**,而 tail / `taskOutput`·`taskStop` / `subagentOutput`
36
+ 三腿已在 0.31.0 翻面,孪生的 resume 腿更是从 #242 批 2 起就按行锚(`resolveOwnerRunId`)。
37
+ 这处不对称正是 `engineSubagentResume` 头注点名的那一条:**同一个 agent 有时能说话有时不能** ——
38
+ 进程级活跃 run 在宿主 turn 收尾即清空(后台子代还在跑,消息却发不出去 = `no-run`),另一条
39
+ turn 正飞时它有值**但可能是别的 run**(打到不相干的 run 上,错值比缺席更坏)。
40
+ 新形三态,与 resume 腿**共用同一个判据函数**(不是第二份手抄):台账有这一行 ⇒ 用行值(哪怕
41
+ 此刻另有 run 在飞);**指名了行**而台账缺席 ⇒ `{ok:false, reason:'no-run'}` + `noteBgOwnerAbsence`
42
+ 留痕(腿名 `subagent-steer`,每 (腿,taskId) 至多一条 warn),绝不回落在飞 run;**没指名行**
43
+ (既有两参调用)⇒ 才回落在飞 run。⚠️ 端侧收益要**端传了 `childTaskId` 才到达用户**:壳/web/桌面
44
+ 凡有行上下文的 steer 调用点(壳 `REPL.tsx` 的 `onAgentSubmit` 已持 `task.id`)应跟一行把它传下来。
45
+ **会话参数同批改二态**(codex 复审 medium):行登记时捕到了会话 ⇒ 用**行的会话**(与行的 runId
46
+ 同源同拍,是唯一自洽的组合;旧形「台账 run + 现势 session」在 session-bound run 上是确定性
47
+ 404,等于修好了 run 定位却仍然 steer 不到);行没有随行会话(`recordBgParentRun` 只写 run,
48
+ 今天 bg 行多数是这一格)⇒ 退现势会话。刻意**不**跟 resume 腿的「没捕到就干脆不带」——本腿遵
49
+ tail/subagentOutput 的 [1498]③ 无条件带 session 纪律,resume 更严是因为它是 AT-MOST-ONCE 的
50
+ 叫醒(不可回收),代价不对称。
51
+ - **#281 / `applyBgNotification` 升级臂的 `task_remove` 前分叉窗(行为面修)**:
52
+ 「终态行帧(completed)在先 + 升级通知(killed)在后 + `task_remove` 丢失」时,旧形只改
53
+ `retained` 不动 live 行,而 `project()` 对留存条目一律 `taskMap.has(id) ⇒ continue` ⇒ 面板读的是
54
+ live 行的**旧终态词**、事实台账已是新词:E4-6 注里点名不许出现的「面板绿而详情页 killed」换了
55
+ 条路复现。常态下同毫秒的 remove 把窗压到不可见,remove 单独丢失且不重连时它**长驻**。
56
+ 修:升级时 `retained` 与 `taskMap` **同改**(仅升级方向;洗绿回声照旧拦截,live 行不动)。
57
+ 在飞读面语义不变(升级后仍是终态词,照旧不进在飞集),`task_remove` 语义不变。
58
+ - **#284 / `onBgNotificationAccepted` 补 `evidence` 第三参(公开面 additive)**:
59
+ 新 type-only 导出 `BgNotificationAcceptEvidence` =
60
+ `'server_fail_closed' | 'own_root' | 'own_parent' | 'absent_parent'`,按**实际命中的放行臂**铸值
61
+ (臂序 = 放行判据求值序,多臂同时成立报第一条)。旧签名把四条**强度不同**的臂压成一个「已放行」
62
+ 布尔事实,端因此无法分级处置。
63
+ 🔴 **三档强度,不是四档递减**(codex 复审 high 采纳;首版注把中间一格写成「硬证据」是过度声称):
64
+ **会话级证明** = `server_fail_closed` / `own_root`;**进程级成员证明** = `own_parent` —— own-run 台账是
65
+ 进程级 `Set`、按会话零分区,`/clear` 或换会话后**旧会话**的 run 仍命中(= 在册局限 **P-13** 在
66
+ 通知面的同一张脸),端**不得**把它读成「属于当前会话」;**非证据** = `absent_parent`(通知没带
67
+ `parentTaskId`;老引擎/老帧形不带该键,fail-closed 会把自家通知整批吞掉)。端拿归属做有副作用的事
68
+ (落库/跨会话搬运/翻别人的卡)时,后两格都应自裁为「未证明当前会话归属」。
69
+ 两参消费方(0.31.0 形)零改动照旧;接入说明见 `docs/INTEGRATION-CLIENTS.md` §6d。
70
+ - **#284 尾件 / `parentTaskId` 脏形 fail-closed(行为面收紧,codex 复审第四轮 high)**:
71
+ own/foreign 隔离门写成 `typeof === 'string' ∧ length>0 ∧ !isOwnEngineRun(…)` —— 对 number /
72
+ object / array / boolean 这类脏值**整条不成立**,门不响、外来通知照收(还会被新 evidence 标成
73
+ `absent_parent`,而那一格的语义是「老引擎**没带**这个键」= 谎报)。fleet 帧从 wire 上来、SDK 只
74
+ `JSON.parse` + rest-spread 不校型,这条路真实可达。现在三态分开:**键缺席 / `null` / 空串**照旧走
75
+ absent-放行臂(与 `wireParentId`「悬空父脏值空串按缺席处理」同规矩,**零新增 drop**);**键在场却
76
+ 不是串** ⇒ 与畸形 `taskId`/`status` 同档 fail-closed:计 `droppedMalformed`、不写状态面、
77
+ 不回调 accepted 钩子、不入通知队列。无正当生产者会发这种形,故不计 BREAKING。
78
+ - **已知局限(本版新增登记,不改行为)**:**P-31**(`docs/INTEGRATION-CLIENTS.md` §7c)——
79
+ fleet 面的会话锚整条走**默认槽**,不跟 ledger 的 `sessionKey`,**两条腿都中招**:
80
+ ①`ownByRoot` 判据用 `engineSessionParam()`(= `hostSessionFor(DEFAULT_SESSION_KEY)`);
81
+ ②更强的一条 —— `serverFailClosed`(meta 两位)**根本不读会话端口**,它信的是「这条流替谁开的」,
82
+ 而本包的开流参数 `fleetStreamOptions()` / `fleetSnapshotOptions()` 是 module 级、只认默认槽会话,
83
+ 连 `sessionKey` 都拿不到;**生产 meta 组合命中的正是这一格**。⇒ **keyed 多会话宿主**上,一条按
84
+ 默认槽开的流接到非默认键 ledger 时,默认槽会话的通知会被放行并报 `evidence` 的前两格。
85
+ 单会话宿主(cli 及今天的三端)不受影响 —— 流与 ledger 恒同会话。
86
+ 🔴 正位解 = fleet 面**整条**换 per-key 会话锚(开流参数 + 归属判据一起动);只改判据那一半会
87
+ 「按默认槽开流、按 keyed 槽判定」自相矛盾、反而丢自己的通知 —— 故本批只登记 + 钉现状,不动行为。
88
+ 同批钉住的另一条:`recordBgParentRun` 今天不写随行会话(resume 腿自己记过的「供给面欠账」),
89
+ 补它要先落 P-31 的会话锚,否则会把**错的**会话写进 owner 记录(比今天不写更坏)。
90
+ - **门**:`scripts/run-client-core-pure-test.mjs` #242 批 3 段 +37 条(B3-Q3c 9 / B3-P3 7 / B3-HKe 11 /
91
+ B3-DIRTY 10),地板 62→99(零松量)。三件各自红先绿后,反钉覆盖 additive 不回归、洗绿方向不放行、
92
+ 台账在场仍照常发起、foreign 丢弃仍零回调、`own_parent` 强度边界不被读成会话证明。
93
+ 变异自证四发(逐发只由目标格抓红,cp 复原后逐字节 cmp 一致):撤 `taskMap.set(id, upgraded)` /
94
+ steer 取址退回 `activeEngineRunId()` / `absent_parent` 铸成 `own_parent` / 撤行会话优先。
95
+
96
+ ## 0.31.0(2026-08-16)
20
97
 
21
98
  **#242 批 3 fleet belt 对账承重批(design-242 §3 批 3;黑板 [4000] Q2=A / Q3=B 裁定执行)。
22
99
  🔴 BREAKING(行为面翻面两处,见下);公开面 additive 三件。**
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.31.0
38
+ **Version:** 0.33.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
@@ -254,6 +254,7 @@ public-surface guard checks that last one).
254
254
  | `scripts/run-rules-side-test.mjs` | The persisted-permission-rules lane's shared decision half. The two capability bits are checked as **two independent gates** — a worker can honestly advertise the rules lane while predating the revoke routes, and that shape must *hide* the governance surface rather than render a dead entry. Failure classification is by **disposition, not cause**: the two 404s (route missing vs. dead ticket) never share a bucket, a 503 `rule_import_retry` means *the ticket is still alive* (the opposite handling of a dead one), and a stale-cursor 400 drops the cursor and re-lists from the top exactly once — never resuming a stale keyset, never surfacing a partial governance list, and never paging past the hard cap. The persist-ack reader is **merged into** `readToolApprovalRespondAck`: the three-state verdict (`persisted` / `refused` / `unknown`) is derived only from an ack that passed the package's structural narrowing, and a half-shaped object such as `{rulePersisted: true}` with no `delivery` reads as `unknown` — the pre-merge shell read would have said `persisted`, which is precisely the double-ledger drift this file closes, so that case is pinned in reverse. The local-allow-rule skeleton pins all five narrowings (whole-tool, tool-name match, literal anchor with the escaped-star counter-example, bare interpreter prefix consulted only for Bash, and the canonical dangerous-pattern overlay) **with their refusal strings byte-for-byte** — the cli's 128-assertion suite anchors the same strings, so a one-character edit here changes observable behaviour on three clients — and asserts the parse is a pure function of its input, because the same call backs both "render the option" and "resolve the selected value" |
255
255
  | `scripts/run-park-decision-layer-test.mjs` | The decision layer behind the "stuck behind a card" family, shared by every client. A pending row that is **not in the queue** is three states, not one: a bounded, interruptible re-probe loop distinguishes *a decidable row*, *not born yet* (no positive evidence that anything settled — an empty queue proves nothing) and *settled elsewhere*, always probes at least once so a zero budget keeps the pre-fix semantics verbatim, cuts a hung read face off at the window rather than only noticing afterwards, and reports the honest failure when the window is spent instead of inventing a decision. The decision-note reader is likewise three-state: an explicit `noteRecorded: false` outranks an echoed note body, absence renders **no line at all**, and untrusted note text is flattened and bounded before it ever reaches a renderer. Row routing anchors on the deciding quantity — a row carrying `gateKind: "human"` with `toolName: "Write"` is a tool gate, because `human` is the engine's *generic* "someone must decide", not a synonym for a question — and the queue scan refuses to surface a row it cannot positively prove belongs to this session. A chain that fails after the row vanished is split by whether a card was ever presented: decided-elsewhere, or not-its-turn-yet. A row-level single-flight makes "at most one card per pending item" structural rather than incidental. The resume three-way card pins the option **order** (the zero-effect choice sits at index 0, because the frame carries no default-focus field and a stray Enter must not attach or cancel), renders only options the wired verbs can honour, collapses every ambiguous answer to zero action, omits the liveness line entirely when the engine gave no evidence, and — when there is no card lane at all — prints three real routes and exits on a dedicated code rather than reporting success |
256
256
  | `scripts/run-selfheal-reopen-test.mjs` | The 409 active-run self-heal decision chain: `governanceForced` narrows on strict `true` only; triage prefers the wire's `pendingGate.kind` and falls back to the status table (an off-table kind is never guessed into a card arm — hands-off plus the honest wording); a first-sight card makes zero closed/reopened claims and a host presentation receipt of `presented: false` demotes the outcome to reopen-failed; park-row ownership is a fail-closed positive proof (own-run ledger or session id — unprovable is not owned); the three gate-identity key literals live in exactly one mint (`hitl/gateIdentity.ts`, AST string-token scan); the armed-gate presentation ledger is per-session; and the `plan_review` reopen arm shares the arm arm's card body, three-state verdict and delivery pipe, consuming the presentation history once a decision is delivered. The same chain also carries the `running` three-way card: both plan-family gate kinds route to the plan arm and all four ask-family kinds to the ask arm (an off-table kind still never gets guessed into either); the card is offered only for verbs that can actually be honoured and a missing presenter means zero action rather than a silent cancel; a steer is sent **exactly once** with its three delivery outcomes worded apart (a `queued` receipt is the wire correcting the triage input, so the named park word decides which card gets reopened, and an unrecognised park word drives neither arm), and a steer failure is split into *provably not delivered* (4xx) and *delivery unknown*, because telling a user to resend a non-idempotent instruction that may already have landed is how duplicates get made. After a user-chosen cancel, "the session is free" is asserted only from a whitelist of terminal states — park states hold the claim, an unrecognised state word is not a release, a failed read is *unknown* rather than a release, and only a 404 counts as one — and the honest timeout line quotes how long it really waited |
257
+ | `scripts/run-terminal-identity-copy-test.mjs` | Terminal-state **identity**, in both lanes where a stop gets a name. A run stopped by this deployment's own governance knobs — the open-set `limits.*` family, `output.invalid`, and the `blocked` contract terminal a ReportBlocked agent produces — is not a provider failure, and labelling it `API Error:` sends the reader to check the network, the key and the quota when the handle is the `--max-turns` they passed themselves. Those terminals now render a neutral row; the reverse direction is guarded just as hard, because asserting "this is *not* an API error" on a code the package does not recognise is the same misfiling pointed the other way — a real `gateway HTTP 502`, a `conflict.session_active_run` and any unknown code all keep the `API Error:` prefix, and the row keeps its `isApiErrorMessage` class flag so brief-mode visibility filtering does not silently drop it. The second half is who the rejected submission belonged to: the self-heal copy told every caller "Your message was NOT sent … send it again", which is three separate untruths for a system injection (a plan-review outcome, a cron wake-up, a task notification) — not the user's message, and not re-sendable, since a host queue marks those non-editable and non-recallable. The injected form says so instead, and the one sentence that promises re-delivery is pinned to the single disposition that earns it: `selfHealSubmissionDisposition` is the same function the host consults before putting the item back on its queue, so the promise and the behaviour cannot drift apart, and the arms where no card could be surfaced state plainly that nothing was delivered and nothing will retry |
257
258
 
258
259
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
259
260
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
@@ -321,6 +321,35 @@ export type SelfHealOutcome =
321
321
  detail: string;
322
322
  delivery: 'rejected' | 'unknown';
323
323
  };
324
+ /**
325
+ * 撞 409 的那条提交是谁写的。
326
+ *
327
+ * `'user'`(缺省)= 用户在输入框里敲的那条 —— 现有全部文案的隐含前提,不传即逐字节不变。
328
+ * `'injected'` = 宿主代模型/排程注入的**系统件**:`<plan-review-outcome>` 回执、cron /
329
+ * ScheduleWakeup 唤醒、task 通知。对这一类说「Your message was NOT sent … send it again」是
330
+ * 两重谎:①那不是用户的消息;②它在队列里不可编辑不可 ↑ 召回(壳侧 `isQueuedCommandEditable`
331
+ * 对 isMeta 恒 false),用户**在结构上就没有**「再发一次」这个动作。
332
+ */
333
+ export type RejectedSubmissionOrigin = 'user' | 'injected';
334
+ /**
335
+ * 结局 → 这条被拒提交**到底怎么了**。文案(本文件)与宿主的重排队判据(壳
336
+ * `queueProcessor`/`handlePromptSubmit`)读的是**同一个**分类函数 —— 两边各写一份必漂,
337
+ * 而漂的后果是「话说会重试、实际没重排队」(或反之),正是本文件头在骂的假承诺形。
338
+ *
339
+ * · `held-for-decision` —— 没送出去,**而且用户手上此刻真有一张答了就能放行的卡**。
340
+ * ⇒ 宿主可以安全地把系统注入件按原优先级放回队列,答完卡自然重投。
341
+ * · `resending` —— 调用方马上就会重发同一条(cancel 已确认释放)。
342
+ * · `handed-off` —— 正文已经交给引擎了(steer),不是「没送出去」。
343
+ * · `not-delivered` —— 没送出去,且**没有**一张此刻能答的卡 ⇒ 重排队只会空转,如实告知。
344
+ */
345
+ export type SelfHealSubmissionDisposition = 'held-for-decision' | 'resending' | 'handed-off' | 'not-delivered';
346
+ /**
347
+ * 🔴 判据锚在「**有没有一张现在就能答、答了就放行的卡**」这个决定量上,不是锚在「是不是 park
348
+ * 态」这个前置条件上([anchor-on-the-deciding-quantity])。所以三张 reopen-failed / not-parked /
349
+ * state-unknown 全部落 `not-delivered`:它们同样是 park,但卡没能呈到用户面前,把注入件放回队列
350
+ * 只会在下一拍再撞一次同样的 409。
351
+ */
352
+ export declare function selfHealSubmissionDisposition(outcome: SelfHealOutcome): SelfHealSubmissionDisposition;
324
353
  /**
325
354
  * 一次**至多一次**(non-idempotent)POST 失败之后:到底是「服务端明确拒了」还是「不知道有没有
326
355
  * 落地」。steer 与 cancel 两条腿共用这一把尺 —— 它们同属「这一枪不能盲发第二次」的族,而两类
@@ -418,7 +447,7 @@ export declare const INTERACTIVE_WAY_OUT = "run /clear to keep working in a fres
418
447
  * wire 真到得了端的两个 additive 键是 `activeTaskStatus` 与 `pendingGate`,各自有真消费点;
419
448
  * 不为「说不定哪天上游会发」的键留悬空分支。
420
449
  */
421
- export declare function activeRunSelfHealRow(outcome: SelfHealOutcome, signal?: ActiveRunBusySignal | null, copy?: ActiveRunSelfHealCopy): string;
450
+ export declare function activeRunSelfHealRow(outcome: SelfHealOutcome, signal?: ActiveRunBusySignal | null, copy?: ActiveRunSelfHealCopy, origin?: RejectedSubmissionOrigin): string;
422
451
  /**
423
452
  * 非交互车道(headless `-p` 等)对同一事实的诚实说法:那里没有 `/clear`,默认串指的是「别再
424
453
  * `--resume` 到这个会话」和引擎自己的 cancel/decide 端点 —— 都是真的存在、真的能做的动作。
@@ -121,6 +121,29 @@ export const CLAIM_RELEASED_STATES = ['completed', 'failed', 'blocked', 'timeout
121
121
  * 收口成「确认不了」。
122
122
  */
123
123
  export const CLAIM_HELD_STATES = ['running', 'suspended', 'needs_review'];
124
+ /**
125
+ * 🔴 判据锚在「**有没有一张现在就能答、答了就放行的卡**」这个决定量上,不是锚在「是不是 park
126
+ * 态」这个前置条件上([anchor-on-the-deciding-quantity])。所以三张 reopen-failed / not-parked /
127
+ * state-unknown 全部落 `not-delivered`:它们同样是 park,但卡没能呈到用户面前,把注入件放回队列
128
+ * 只会在下一拍再撞一次同样的 409。
129
+ */
130
+ export function selfHealSubmissionDisposition(outcome) {
131
+ switch (outcome.kind) {
132
+ case 'decision-pending':
133
+ return 'held-for-decision';
134
+ case 'ask-reopened':
135
+ case 'plan-review-reopened':
136
+ // 重开成功 = 卡真的呈上去了(`ReopenCardVerdict` 带宿主呈现回执,呈不出来会降级成
137
+ // reopen-failed 那一臂)⇒ 用户手上确有把手。
138
+ return 'held-for-decision';
139
+ case 'running-cancelled':
140
+ return 'resending';
141
+ case 'running-steered':
142
+ return 'handed-off';
143
+ default:
144
+ return 'not-delivered';
145
+ }
146
+ }
124
147
  /** 会话作用域位的透传口(缺席即不置键 —— `exactOptionalPropertyTypes` 下 `{session: undefined}`
125
148
  * 与「没有这个键」不是一回事,而 SDK 那一侧读的正是「在不在场」)。 */
126
149
  function sessionOpts(deps) {
@@ -604,10 +627,14 @@ function governanceOriginClause(signal) {
604
627
  * wire 真到得了端的两个 additive 键是 `activeTaskStatus` 与 `pendingGate`,各自有真消费点;
605
628
  * 不为「说不定哪天上游会发」的键留悬空分支。
606
629
  */
607
- export function activeRunSelfHealRow(outcome, signal, copy) {
630
+ export function activeRunSelfHealRow(outcome, signal, copy, origin) {
608
631
  const override = copy?.rowFor?.(outcome, signal);
609
632
  if (typeof override === 'string')
610
633
  return override;
634
+ // #289 批面② —— 系统注入件的低噪形。整块**前置**于默认串:注入形与用户形是两句完全不同的话,
635
+ // 不是在同一句上加定语(见 {@link injectedSubmissionRow} 头注)。缺省 'user' ⇒ 下面逐字节原样。
636
+ if (origin === 'injected')
637
+ return injectedSubmissionRow(outcome);
611
638
  const base = activeRunSelfHealBaseRow(outcome, signal, copy?.wayOut ?? DEFAULT_WAY_OUT);
612
639
  switch (outcome.kind) {
613
640
  // 这两条是「用户此刻卡住了、而且没有别的把手」的结局 —— wire 给的 decide 入口在这里才有用。
@@ -620,6 +647,41 @@ export function activeRunSelfHealRow(outcome, signal, copy) {
620
647
  return base + governanceOriginClause(signal);
621
648
  }
622
649
  }
650
+ /**
651
+ * 系统注入件被拒时的**低噪**整行(#289 批面②;dogfood inject C1 文案半场)。
652
+ *
653
+ * ── 为什么不是「在原句上换个主语」──────────────────────────────────────────────────────────
654
+ * 原来那 8 条臂全部以「Your message was NOT sent … send it again」收尾。对注入件这句里的每一个
655
+ * 词都不成立:不是 your、不是 message、而且 send it again 这个动作**在结构上不存在**(注入件在
656
+ * 队列里 isMeta ⇒ 不可编辑、不可 ↑ 召回、队列预览里都不渲)。所以这一形从头写,不复用出路半句。
657
+ *
658
+ * ── 与行为的对账(不许再出一句假承诺)────────────────────────────────────────────────────────
659
+ * 「it will be delivered after you decide」这句**只在** {@link selfHealSubmissionDisposition} 判
660
+ * `held-for-decision` 时才说 —— 那正是宿主真会把它按原优先级放回队列的那一档。判据同源,不是
661
+ * 两边各写一份(记忆 `paired-mechanisms-must-share-premise`)。其余档一律如实说「这一条没有送达」,
662
+ * 绝不承诺一次不会发生的重投。
663
+ *
664
+ * ── 噪音预算 ────────────────────────────────────────────────────────────────────────────────
665
+ * 用户没写过这条消息,所以这一行的读者其实是**将来的自己**(为什么模型没收到 plan 的结果)。
666
+ * 一句话说清三件事:是系统件、当时什么挡住了它、接下来会不会自己再来。不给出路半句 —— 用户此刻
667
+ * 该做的事是答那张卡,而那张卡就在屏幕上。
668
+ */
669
+ function injectedSubmissionRow(outcome) {
670
+ const handle = 'taskId' in outcome && outcome.taskId ? ` (run ${outcome.taskId})` : '';
671
+ switch (selfHealSubmissionDisposition(outcome)) {
672
+ case 'held-for-decision':
673
+ return (`A system notification could not be delivered while an earlier turn${handle} is waiting on a ` +
674
+ `decision — sema kept it queued and will deliver it after you answer the open card.`);
675
+ case 'resending':
676
+ return `A system notification was held while the earlier run${handle} was cancelled; sema is delivering it now.`;
677
+ case 'handed-off':
678
+ return (`A system notification was handed to the run that is already working${handle} instead of starting ` +
679
+ `a new turn — watch that run for what it does with it.`);
680
+ case 'not-delivered':
681
+ return (`A system notification was NOT delivered: this session is still held by an earlier run${handle} and ` +
682
+ `sema could not put a decision for it on screen, so it did not retry. The model was not told about it.`);
683
+ }
684
+ }
623
685
  function activeRunSelfHealBaseRow(outcome, signal, wayOut) {
624
686
  switch (outcome.kind) {
625
687
  case 'decision-pending':
@@ -75,6 +75,41 @@ export interface ActiveRunBusySignal {
75
75
  * ② `failed{errorCode|activeTaskId,…}`(任何把它投影成 failed 臂的上游形)。
76
76
  */
77
77
  export declare function activeRunBusySignal(ev: unknown): ActiveRunBusySignal | null;
78
+ /**
79
+ * 中性终态行的前缀。**刻意不是 `API Error`** —— 这一行说的是「这条 run 被本部署的治理旋钮停了」
80
+ * (轮数/预算/墙钟/token 到限、结构化输出重试耗尽),不是 provider 或 HTTP 层出了故障。
81
+ *
82
+ * 事故形(台账 api C1/C2 逐坐标坐实):`sema -p --max-turns N` 的 STDERR 逐字是
83
+ * `API Error: max turns exceeded`,交互面同一构造点也渲成红色「API 错误」行。用户读到 API Error
84
+ * 会去查网络/密钥/额度,而真正的把手是他自己传的 `--max-turns`。CC 对同一件事写的是
85
+ * `Error: Reached max turns (N)` —— 一个字的 `API` 都没有,本前缀与那条身份声明同向。
86
+ */
87
+ export declare const RUN_STOPPED_MESSAGE_PREFIX = "Run stopped";
88
+ /** blocked 终态自带的前缀(`Run blocked: <reason>`,见 errText 构造)。 */
89
+ export declare const RUN_BLOCKED_MESSAGE_PREFIX = "Run blocked";
90
+ /**
91
+ * 这条终态行是不是「治理/契约终态」而非 provider 故障 —— 渲染层据此给中性行同样的错误行 chrome
92
+ * (它仍然是一条终态错误行,只是**身份**不是 API 错误),而不必按前缀猜。
93
+ *
94
+ * 🔴 与产出侧同一个真源:产出在 {@link runStream} 的 governance 臂,判别在这里,端只 import。
95
+ * 各写一份正则必漂(记忆 `machine-readable-signal-not-visual-anchor`)。
96
+ */
97
+ export declare function isGovernanceStopRowText(text: string): boolean;
98
+ /**
99
+ * 终态是不是治理/契约面的(⇒ 中性行),而不是 provider/HTTP 面的(⇒ 保留 `API Error:`)。
100
+ *
101
+ * 🔴 **开集纪律**:只有**认得出**的治理码才走中性臂;认不出的码一律落回 `API Error:` 原臂
102
+ * (原样透传引擎的话)。反过来做 = 替引擎断言「这不是 API 错误」,而那正是本修在骂的那类错档。
103
+ *
104
+ * 成员:
105
+ * · `status:'blocked'`(ReportBlocked 契约终态 —— agent 自己说它无法继续,零 provider 参与);
106
+ * · `limits.*` 全族(开集前缀,{@link isLimitsExceededCode});
107
+ * · `output.invalid`(结构化输出重试耗尽 —— CC 自己也有专门 subtype,同样不叫 API 错误)。
108
+ */
109
+ export declare function isGovernanceTerminal(input: {
110
+ status: string | undefined;
111
+ errorCode: string | undefined;
112
+ }): boolean;
78
113
  /** 测试钩:清空「已上报过的臂」去重表(去重是**跨调用**状态,不清就只有第一条用例看得见)。 */
79
114
  export declare function _resetDroppedFrameReportForTest(): void;
80
115
  /** 测试钩:去重表当前条数 —— ADAPTER-F5 的上限断言要读的**决定结果的量**(行数只证「吼了几次」,
@@ -7,7 +7,7 @@ import { coerceOutput, publishSubagentContentEvent } from '../subagentContentSto
7
7
  * 引擎把它 stamp 在 `done{status:'failed'}` / `failed` 终帧上,壳据此**结构判读**,不再读人话。
8
8
  * G1 去字面化(2026-08-04):字面收编进 `engineErrorCodes.ts` 单一真源,本文件只 import。
9
9
  */
10
- import { ACTIVE_RUN_BUSY_ERROR_CODE } from '../engineErrorCodes.js';
10
+ import { ACTIVE_RUN_BUSY_ERROR_CODE, OUTPUT_INVALID, isLimitsExceededCode } from '../engineErrorCodes.js';
11
11
  /** 本文件发的 chrome 事件全在 leader lane(子代内容在上面就被 divert 走了)。 */
12
12
  const MAIN = { lane: 'main' };
13
13
  /**
@@ -81,6 +81,48 @@ function classifyActiveRunBusy(input) {
81
81
  return { busy: true, handle };
82
82
  return { busy: false, handle };
83
83
  }
84
+ // ── 治理终态 vs provider/HTTP 真错(#289 批面③ / dogfood api C1+C2+案5)────────────────────────
85
+ /**
86
+ * 中性终态行的前缀。**刻意不是 `API Error`** —— 这一行说的是「这条 run 被本部署的治理旋钮停了」
87
+ * (轮数/预算/墙钟/token 到限、结构化输出重试耗尽),不是 provider 或 HTTP 层出了故障。
88
+ *
89
+ * 事故形(台账 api C1/C2 逐坐标坐实):`sema -p --max-turns N` 的 STDERR 逐字是
90
+ * `API Error: max turns exceeded`,交互面同一构造点也渲成红色「API 错误」行。用户读到 API Error
91
+ * 会去查网络/密钥/额度,而真正的把手是他自己传的 `--max-turns`。CC 对同一件事写的是
92
+ * `Error: Reached max turns (N)` —— 一个字的 `API` 都没有,本前缀与那条身份声明同向。
93
+ */
94
+ export const RUN_STOPPED_MESSAGE_PREFIX = 'Run stopped';
95
+ /** blocked 终态自带的前缀(`Run blocked: <reason>`,见 errText 构造)。 */
96
+ export const RUN_BLOCKED_MESSAGE_PREFIX = 'Run blocked';
97
+ /**
98
+ * 这条终态行是不是「治理/契约终态」而非 provider 故障 —— 渲染层据此给中性行同样的错误行 chrome
99
+ * (它仍然是一条终态错误行,只是**身份**不是 API 错误),而不必按前缀猜。
100
+ *
101
+ * 🔴 与产出侧同一个真源:产出在 {@link runStream} 的 governance 臂,判别在这里,端只 import。
102
+ * 各写一份正则必漂(记忆 `machine-readable-signal-not-visual-anchor`)。
103
+ */
104
+ export function isGovernanceStopRowText(text) {
105
+ const t = text.trimStart();
106
+ return t.startsWith(`${RUN_STOPPED_MESSAGE_PREFIX}:`) || t.startsWith(`${RUN_BLOCKED_MESSAGE_PREFIX}`);
107
+ }
108
+ /**
109
+ * 终态是不是治理/契约面的(⇒ 中性行),而不是 provider/HTTP 面的(⇒ 保留 `API Error:`)。
110
+ *
111
+ * 🔴 **开集纪律**:只有**认得出**的治理码才走中性臂;认不出的码一律落回 `API Error:` 原臂
112
+ * (原样透传引擎的话)。反过来做 = 替引擎断言「这不是 API 错误」,而那正是本修在骂的那类错档。
113
+ *
114
+ * 成员:
115
+ * · `status:'blocked'`(ReportBlocked 契约终态 —— agent 自己说它无法继续,零 provider 参与);
116
+ * · `limits.*` 全族(开集前缀,{@link isLimitsExceededCode});
117
+ * · `output.invalid`(结构化输出重试耗尽 —— CC 自己也有专门 subtype,同样不叫 API 错误)。
118
+ */
119
+ export function isGovernanceTerminal(input) {
120
+ if (input.status === 'blocked')
121
+ return true;
122
+ if (isLimitsExceededCode(input.errorCode))
123
+ return true;
124
+ return input.errorCode === OUTPUT_INVALID;
125
+ }
84
126
  /**
85
127
  * 已上报过的未知帧类型(REF-CC-057:每种臂只吼一次,免得一条流刷屏)。
86
128
  * 🔴 单例登记见 `docs/refactor/p1-scan/singleton-manifest.json`:复制两份的后果 = 同一种臂多打
@@ -353,12 +395,29 @@ async function* runStreamInner(events, ctx, handle = {}) {
353
395
  // 重启引擎捞不回来(boot 期孤儿回收只捞 running)、时间型 reap 挂在
354
396
  // APPROVAL_TIMEOUT_SEC(默认 0 ⇒ 整条腿不跑),唯一兜底窗是 30 天。
355
397
  // 换成真话 + 真的存在的两条动作(引擎的 cancel 端点 / 换一个会话)。
398
+ // #289 批面③ —— **错档前缀**(dogfood api C1/C2 + 案5)。轮数/预算/墙钟/token 到限、
399
+ // 结构化输出重试耗尽、ReportBlocked 的 blocked:这几种是**引擎侧治理/契约终态**,一个
400
+ // provider 请求都没失败过。此前它们与真 401/网关 502 共用 `API Error: ` 身份声明,把
401
+ // 「你自己设的 --max-turns 到了」说成 API 故障(判据与出处见 `isGovernanceTerminal`)。
402
+ // 认得出的治理终态 ⇒ 中性终态行;认不出的码一律走原 `API Error:` 臂(开集纪律)。
403
+ // 🔴 `isApiErrorMessage: true` 仍然置位:它是 CC 的**行类**旗(「这是合成的终态错误行,
404
+ // 不是模型说的话」),不是「这是 provider API 错误」的断言 —— 可见性过滤
405
+ // (`Messages.filterForBriefTool`)、-p 契约投影、tick 熔断都读它,撤掉 = 这一行在
406
+ // brief 模式里静默消失。改的是**身份文案**,不是行类。
407
+ const governance = isGovernanceTerminal({
408
+ status: ev.type === 'failed' ? undefined : failedResult?.status,
409
+ errorCode: ev.type === 'failed' ? ev.errorCode : failedResult?.errorCode,
410
+ });
356
411
  const busyHandle = busy.handle;
357
412
  const rowText = busy.busy
358
413
  ? `This session is locked by an earlier run${busyHandle ? ` (run ${busyHandle})` : ''} that ` +
359
414
  `has not been released, so this message was NOT sent. Nothing releases it on its own — ` +
360
415
  `cancel that run (POST /v1/runs/${busyHandle ?? '<id>'}/cancel), or start a new session.`
361
- : `API Error: ${errText}${maxTokHint}`;
416
+ : governance
417
+ ? // blocked 的 errText 自己就以 `Run blocked` 开头(见上方 errText 构造),再套一层
418
+ // `Run stopped:` 会变成两个前缀叠字;其余治理码用中性前缀 + 引擎原话。
419
+ `${isGovernanceStopRowText(errText) ? errText : `${RUN_STOPPED_MESSAGE_PREFIX}: ${errText}`}${maxTokHint}`
420
+ : `API Error: ${errText}${maxTokHint}`;
362
421
  yield {
363
422
  session_id: ctx.sessionId ?? '',
364
423
  uuid: `err-${Date.now().toString(36)}`,
@@ -33,6 +33,17 @@ export declare const LIMITS_MAX_COST_EXCEEDED = "limits.max_cost_exceeded";
33
33
  export declare const LIMITS_MAX_TURNS_EXCEEDED = "limits.max_turns_exceeded";
34
34
  /** 墙钟预算到限(5.8.0 起是**响亮终局** `status:'failed'`;`status:'timeout'` 该终态词整体退役)。 */
35
35
  export declare const LIMITS_MAX_WALLTIME_EXCEEDED = "limits.max_walltime_exceeded";
36
+ /** 限额族的**族前缀**(单源;上面四个常量都以它开头)。 */
37
+ export declare const LIMITS_ERROR_CODE_PREFIX = "limits.";
38
+ /**
39
+ * 该码是否属「引擎侧治理限额到限」族。**开集前缀判**(与 {@link isConfigRefusalCode} 同款):
40
+ * core 每加一根新的限额轴(`limits.max_*`)判别自动跟上,按成员判的消费点则要跟车。
41
+ *
42
+ * 🔴 用途边界:这一族回答的是「谁停的这条 run」= **本部署的治理旋钮**,不是 provider/HTTP 故障。
43
+ * 呈现层据此避开 `API Error:` 那个身份声明(见 `adapter/runStream.ts` 的 governance 臂)——
44
+ * 把一次「轮数用光了」说成 API 错误,用户会去查网络/密钥/额度,而真正的把手是 `--max-turns`。
45
+ */
46
+ export declare function isLimitsExceededCode(code: string | undefined): boolean;
36
47
  /** 部署级 token 治理窗耗尽。**唯一携带可执行等待量的停因**,与 `retryAfterMs` 配对到货。
37
48
  * 5.10.0 起:治理窗与环境寿命同一轮边界到期时,**窗赢**(此前 env-first 报告把它丢了)。 */
38
49
  export declare const USAGE_WINDOW_EXHAUSTED = "usage.window_exhausted";
@@ -34,6 +34,19 @@ export const LIMITS_MAX_COST_EXCEEDED = 'limits.max_cost_exceeded';
34
34
  export const LIMITS_MAX_TURNS_EXCEEDED = 'limits.max_turns_exceeded';
35
35
  /** 墙钟预算到限(5.8.0 起是**响亮终局** `status:'failed'`;`status:'timeout'` 该终态词整体退役)。 */
36
36
  export const LIMITS_MAX_WALLTIME_EXCEEDED = 'limits.max_walltime_exceeded';
37
+ /** 限额族的**族前缀**(单源;上面四个常量都以它开头)。 */
38
+ export const LIMITS_ERROR_CODE_PREFIX = 'limits.';
39
+ /**
40
+ * 该码是否属「引擎侧治理限额到限」族。**开集前缀判**(与 {@link isConfigRefusalCode} 同款):
41
+ * core 每加一根新的限额轴(`limits.max_*`)判别自动跟上,按成员判的消费点则要跟车。
42
+ *
43
+ * 🔴 用途边界:这一族回答的是「谁停的这条 run」= **本部署的治理旋钮**,不是 provider/HTTP 故障。
44
+ * 呈现层据此避开 `API Error:` 那个身份声明(见 `adapter/runStream.ts` 的 governance 臂)——
45
+ * 把一次「轮数用光了」说成 API 错误,用户会去查网络/密钥/额度,而真正的把手是 `--max-turns`。
46
+ */
47
+ export function isLimitsExceededCode(code) {
48
+ return typeof code === 'string' && code.startsWith(LIMITS_ERROR_CODE_PREFIX);
49
+ }
37
50
  // ── 停钟族(core 5.10.0「三时钟序」:usage_window > env_lifetime > stall)─────────────────────
38
51
  /** 部署级 token 治理窗耗尽。**唯一携带可执行等待量的停因**,与 `retryAfterMs` 配对到货。
39
52
  * 5.10.0 起:治理窗与环境寿命同一轮边界到期时,**窗赢**(此前 env-first 报告把它丢了)。 */
@@ -114,6 +114,49 @@ export declare function fleetSnapshotOptions(opts?: {
114
114
  export type HookNoticeFrame = Extract<FleetFrame, {
115
115
  type: 'hook_notice';
116
116
  }>;
117
+ /**
118
+ * 🔴 #284:`onBgNotificationAccepted` 的**证据等级** —— 这条通知是**凭哪一条**放行的。
119
+ *
120
+ * 为什么钩子要说这个:四条放行臂的证据强度**不一样**,而旧签名把它们压成一个「已放行」布尔事实,
121
+ * 端因此无法对不同强度的证据分级处置(强证据可直接落库/翻卡,弱证据宜先渲染、别拿它当归属结论)。
122
+ * 判据锚在**实际命中的那条臂**上,不是锚在「有没有 parentTaskId」这类前置条件上。
123
+ *
124
+ * 🔴 **每个词命名的是「放行臂」,不是「归属结论」**(codex 复审两轮收紧:首版把 `own_parent` 写成
125
+ * 「硬证据」、二版只给 `own_root` 挂了会话锚警告,都是过度声称)。强度分档如下 —— 前两格的
126
+ * 会话级读法**带前提**,第三格永远只是进程级,第四格根本不是证据。
127
+ *
128
+ * 🔴 **前两格的共同前提:喂给本 ledger 的那条 fleet 流,是按本 ledger 的会话开的。**
129
+ * 本 ledger **自己不开流**(帧体归库、连接归端),所以它无从校验这一点;而本包给出的开流参数
130
+ * `fleetStreamOptions()` / `fleetSnapshotOptions()` 是 **module 级函数、只认默认槽会话**
131
+ * (`engineSessionParam()` = `hostSessionFor(DEFAULT_SESSION_KEY)`),连 `opts.sessionKey` 都拿不到。
132
+ * ⇒ **单会话宿主**(cli 及今天的三端)前提恒成立,前两格就是会话级证明;
133
+ * ⇒ **keyed 多会话宿主**上,一条按默认槽开的流被接到非默认键 ledger 时,前两格会替**别的会话**
134
+ * 作证(`server_fail_closed` 尤其是**生产 meta 组合下的那一格**)。在册局限 **P-31**
135
+ * (`docs/INTEGRATION-CLIENTS.md` §7c);正位解 = 开流参数与本判据**一起**换 per-key 会话锚,
136
+ * 只改一半会让「按默认槽开流、按 keyed 槽判定」自相矛盾、反而丢自己的通知。
137
+ *
138
+ * · **会话级证明(带上述前提)**:
139
+ * · `server_fail_closed` — server 侧注入路已 fail-closed(meta `bgNotifyFailClosed` ∧ 本连接
140
+ * session-bound):最强,到达即**这条流的**会话,端侧台账判别整体让位([1510])。
141
+ * ⚠️ 这一臂**完全不读会话端口**,它信的就是「这条流是替谁开的」——前提破了它也不会报错;
142
+ * · `own_root` — 通知带 `rootSessionId`(委托树 root 宿主会话,固定点语义)=== `engineSessionParam()`
143
+ * (**默认槽**的 SessionPort,不是本 ledger 的 `sessionKey` 槽)。
144
+ * · **进程级成员证明**(只证明「本进程曾亲手驱动过这条 run」,🔴 **不区分会话代际**):
145
+ * · `own_parent` — `parentTaskId`(spawn 该子代的 leader run)∈ 本端 own-run 台账。台账真源
146
+ * `subagentContentStore.ownEngineRuns` 是**进程级 `Set`、按会话零分区**(它自己的头注:
147
+ * 「进程内存态,壳重启即空」),所以 `/clear` 或换会话之后,**旧会话**登记的 run 仍然命中
148
+ * —— 这正是在册局限 **P-13**(`docs/INTEGRATION-CLIENTS.md` §7c)在通知面的同一张脸。
149
+ * ⇒ 端不得把它当作「属于当前会话」的证明;要按会话归属做事,自注入会话粒度的 own-run
150
+ * 判据(P-13 给的出路),或只认上面那两格。
151
+ * · **非证据**:
152
+ * · `absent_parent` — 通知**没带** `parentTaskId`:🔴 **这不是证据,是 absent-放行姿势**
153
+ * (老引擎/老帧形不带该键,fail-closed 会把自家通知整批吞掉,故按放行处理)。端要拿归属做
154
+ * 有副作用的事(落库、跨会话搬运、翻别人的卡)时,这一格应当自裁为「未证明」。
155
+ *
156
+ * 臂序 = `applyBgNotification` 放行判据的求值序(多臂同时成立时报**第一条**);开集只在本包加臂时
157
+ * 扩,端按未知词兜底(`default` 当 `absent_parent` 一档处理最安全)。
158
+ */
159
+ export type BgNotificationAcceptEvidence = 'server_fail_closed' | 'own_root' | 'session_anchor_untrusted' | 'own_parent' | 'absent_parent';
117
160
  export interface FleetLedgerHooks {
118
161
  /**
119
162
  * hook_notice 帧的**写 store** 半场(判定在 `classifyHookNoticeFrame`,本台账只做分发)。
@@ -128,10 +171,13 @@ export interface FleetLedgerHooks {
128
171
  * · 终态与非终态通知都回调(状态筛选归消费方);
129
172
  * · `rowIds` = 到达时刻 live 集里按键域(整行 id + 尾段)命中的行 id(可能为空 = 行帧车道
130
173
  * 没有这行;可能多条 = server 双生行)。快照取在退休动作**之前**;
174
+ * · `evidence`(#284,additive 第三参)= **凭哪一条臂放行的**,见
175
+ * {@link BgNotificationAcceptEvidence} —— 四条臂证据强度不同,`absent_parent` 尤其**不是
176
+ * 归属证明**。两参消费方(0.31.0 形)零改动照旧;
131
177
  * · 钩子抛错不拆台账(try/catch + debug 留痕),与帧消费循环隔离。
132
178
  * 端从此**零归属逻辑、零复刻**(壳侧 meta 两位门 [cross-repo-fix-at-source] 同批整条删)。
133
179
  */
134
- onBgNotificationAccepted?(n: FleetBgNotificationWire, rowIds: readonly string[]): void;
180
+ onBgNotificationAccepted?(n: FleetBgNotificationWire, rowIds: readonly string[], evidence: BgNotificationAcceptEvidence): void;
135
181
  }
136
182
  /** REF-CC-044(fleet2-06)/REF-CC-039(fleet2-01):`createFleetLedger` 的可选装配位。 */
137
183
  export interface CreateFleetLedgerOptions {
@@ -350,6 +350,26 @@ export function createFleetLedger(hooks = {}, opts = {}) {
350
350
  debug('[fleet-frame] MALFORMED bg_notification dropped(taskId 或 status 缺席/非串)');
351
351
  return;
352
352
  }
353
+ // 🔴 #284 尾件(codex 复审第四轮 high,红先绿后=B3-DIRTY):`parentTaskId` 是**隔离判据的锚**,
354
+ // 它的脏形必须先于判据处置。下面那道 own/foreign 门写成
355
+ // `typeof === 'string' && length > 0 && !isOwnEngineRun(…)` —— 对 number/object/boolean
356
+ // 这类脏值**整条不成立** ⇒ 门不响、通知照收,而新的 evidence 还会把它标成 `absent_parent`
357
+ // (「老引擎没带这个键」的兼容臂):键**带了**,只是坏的,标成缺席是谎报。帧从 wire 上来、
358
+ // SDK 只 JSON.parse + rest-spread 不校型(同 `recentSteps` 处的威胁模型),这条路真实可达。
359
+ // 三态分开,只收紧真正的脏形:
360
+ // · 键缺席 / `undefined` / `null` / 空串 ⇒ absent-放行臂**照旧**(与 `wireParentId`
361
+ // 「悬空父脏值空串按缺席处理」同一规矩;零新增 drop —— 老引擎与「本来就没有父」不受伤);
362
+ // · 键在场却**不是串**(含 `null`,0.32.0 扫码收紧)⇒ 与 taskId/status 同档 fail-closed:
363
+ // 计 `droppedMalformed`、不写状态面、不回调 accepted 钩子、不入通知队列(隔离优先,
364
+ // 坏值不许买路)。`null` 不入兼容臂的理由:wire schema 只允许**缺席或 string**——
365
+ // 老引擎是不带这个键,不是带 `null`;把 null 归入缺席等于给「外来 parent 锚被置 null」
366
+ // 这一腐败/漂移形留一条绕过 foreign 检查的路(空串是 string 的退化形,真老形,保留)。
367
+ if (n.parentTaskId !== undefined && typeof n.parentTaskId !== 'string') {
368
+ droppedMalformed++;
369
+ debug(`[fleet-frame] MALFORMED bg_notification dropped(parentTaskId 键在场但非串:${typeof n.parentTaskId}` +
370
+ ` —— 隔离判据的锚不许被坏值旁路;taskId=${n.taskId})`);
371
+ return;
372
+ }
353
373
  // 🔴 own/foreign 判别(双开泄漏案帧级防线;[1498]④ 换锚重写):payload `sessionId` 是**子代
354
374
  // 自身** session uuid,宿主会话在 ownerSessionId 上且出 wire 前被剥 —— 所以判别锚
355
375
  // `parentTaskId`(spawn 该子代的 leader run id)对 own-run 台账:
@@ -375,7 +395,29 @@ export function createFleetLedger(hooks = {}, opts = {}) {
375
395
  debug(`[fleet-frame] bg_notification DROPPED foreign/unowned parentRun(taskId=${n.taskId} parentRun=${n.parentTaskId})`);
376
396
  return;
377
397
  }
378
- debug(`[fleet-frame] bg_notification taskId=${n.taskId} status=${n.status} seq=${n.seq ?? '-'}`);
398
+ // 🔴 #284:放行**凭据**定级(additive 第三参喂 accepted 钩子)。臂序 = 上面那道放行判据的
399
+ // 求值序,逐臂对位:`serverFailClosed` 先短路 → `ownByRoot` 次之 → 剩下两格由
400
+ // `parentTaskId` 在不在分开(走到这里且带非空 parentTaskId ⇒ 它必然 ∈ own-run 台账,
401
+ // 否则上面已丢弃)。多臂同时成立报第一条 —— 报的是「这一帧**实际**靠哪条过的门」,
402
+ // 不是「最强的那条理论上也成立」。
403
+ // 🔴 非默认 sessionKey 封顶(0.32.0 发包前扫码采 codex 折中,P-31 的诚实半步):前两格的
404
+ // 会话级读法前提=「这条流是按本 ledger 的会话开的」,而开流参数只认默认槽 ⇒ keyed ledger
405
+ // 上该前提**无法成立也无法校验**。封顶词=`session_anchor_untrusted`(不降到 own_parent:
406
+ // 经 serverFailClosed/ownByRoot 放行的帧,其 parentTaskId 可能是 foreign——标进程成员是
407
+ // 比错标会话更糟的谎报;也不标 absent_parent:键可能在场)。语义=「这帧被会话级臂放行,
408
+ // 但本 ledger 无法信任那个会话锚」——消费方按非证据档自裁。单会话宿主(默认 key,今日
409
+ // 三端)逐字不变;P-31 正位解(per-key 会话锚全套)落地时撤此封顶。
410
+ const sessionEvidenceTrusted = sessionKey === DEFAULT_SESSION_KEY;
411
+ const evidence = serverFailClosed || ownByRoot
412
+ ? sessionEvidenceTrusted
413
+ ? serverFailClosed
414
+ ? 'server_fail_closed'
415
+ : 'own_root'
416
+ : 'session_anchor_untrusted'
417
+ : typeof n.parentTaskId === 'string' && n.parentTaskId.length > 0
418
+ ? 'own_parent'
419
+ : 'absent_parent';
420
+ debug(`[fleet-frame] bg_notification taskId=${n.taskId} status=${n.status} seq=${n.seq ?? '-'} accept=${evidence}`);
379
421
  // 键域(#242 批 3):`n.taskId` = registry handle = 行 id 的**尾段**。live 集按「整行 id 或
380
422
  // 尾段命中」收齐**全部**别名(server 双生行:裸 id 与 `runId childId` 复合形同时在场时两条
381
423
  // 一起处理)—— 快照取在任何退休动作之前,原样交给 accepted 钩子。首段(runId)绝不作键
@@ -435,11 +477,20 @@ export function createFleetLedger(hooks = {}, opts = {}) {
435
477
  }
436
478
  if (rowStatus !== n.status) {
437
479
  // 升级(REF-CC-040 腿三):通知是 settle 权威,两读面同步改写,不分叉。
438
- retained.set(id, { row: { ...known, status: coerced }, expireAt: nowMs + TERMINAL_RETAIN_MS });
480
+ // 🔴 #281(0.31.0 扫码 P3 的真窗,红先绿后=B3-P3):`retained` **和** live 行一起改。
481
+ // 只写 retained 的旧形有一个 remove 之前的分叉窗 —— `project()` 对留存条目一律
482
+ // `taskMap.has(id) ⇒ continue`,于是面板读的是 live 行的**旧终态词**(completed)
483
+ // 而事实台账已是 killed:正是 E4-6 注里点名不许出现的「面板绿而详情页 killed」。
484
+ // 常态下同毫秒的 `task_remove` 会把窗压到不可见,但 remove 单独丢失且不重连时它
485
+ // **长驻**(自愈只能等重连 REPLACE 或整窗过期)。live 行改写后两读面当拍同说;
486
+ // 在飞读面不受影响(升级后仍是终态词,照旧不进在飞集),`task_remove` 语义不变。
487
+ const upgraded = { ...known, status: coerced };
488
+ retained.set(id, { row: upgraded, expireAt: nowMs + TERMINAL_RETAIN_MS });
489
+ taskMap.set(id, upgraded);
439
490
  }
440
491
  writtenIds.add(id);
441
492
  applied = true;
442
- // live 终态行不动(remove 帧随后清;终态行不进在飞读面,留着无害且行车道语义完整)
493
+ // live 终态行不退休(remove 帧随后清;终态行不进在飞读面)——只在升级时同步它的状态词。
443
494
  }
444
495
  else {
445
496
  // 退休:live 行离开在飞集,进留存池,带 E4-7 解耦标(见 bgView.read)。
@@ -537,7 +588,7 @@ export function createFleetLedger(hooks = {}, opts = {}) {
537
588
  // 钩子类型虽 void,宿主传 async 函数合法(P-F3):返回 thenable 时必须承接拒绝,
538
589
  // 否则 async 消费方一炸就是 unhandledRejection = Node 缺省整进程崩(TUI 最坏形,
539
590
  // engineSubagentTail meta sink 同族先例)。同步抛与异步拒同一条留痕出口。
540
- const out = hooks.onBgNotificationAccepted?.(n, rowIds);
591
+ const out = hooks.onBgNotificationAccepted?.(n, rowIds, evidence);
541
592
  if (out && typeof out.then === 'function') {
542
593
  void Promise.resolve(out).catch((e) => {
543
594
  hostLog('debug', `[fleet-frame] onBgNotificationAccepted hook rejected: ${String(e).slice(0, 160)}`);
@@ -6,5 +6,30 @@ export type SubagentSteerOutcome = {
6
6
  reason: 'no-wire' | 'no-run' | 'not-running' | 'error';
7
7
  detail?: string;
8
8
  };
9
- /** Steer a running engine subagent (target = its parentToolCallId, or agentName). */
10
- export declare function steerEngineSubagent(target: string, text: string): Promise<SubagentSteerOutcome>;
9
+ /**
10
+ * Steer a running engine subagent (target = its parentToolCallId, or agentName).
11
+ *
12
+ * 🔴 #280 件A(取址第四腿并入 [4000] Q3=B 口径):`childTaskId` 在场时**台账优先、缺席即诚实缺席**,
13
+ * 与 tail / taskOutput·taskStop / subagentOutput 三腿同姿势,也与孪生的 resume 腿共用**同一个**
14
+ * 取址判据(`resolveOwnerRunId`,不是第二份手抄)。此前本腿直取 `activeEngineRunId()` —— 正是
15
+ * `engineSubagentResume` 头注点名的那处不对称(「同一个 agent 有时能说话有时不能」):
16
+ * · 进程级活跃 run 被并发 main/fork 流互相覆写、又在每个 query 的 finally 清空 ⇒ 后台子代在
17
+ * 宿主 turn 收尾之后仍在跑时,这里恒空(no-run,消息发不出去);另一条 turn 正飞时它有值
18
+ * 但**是别的 run**(打到不相干的 run 上 —— 错值比缺席更坏,与 Q3 同一条理由)。
19
+ * 三态(`resolveOwnerRunId` 的单一判据):台账有这一行 ⇒ 用行值(哪怕此刻另有 run 在飞);
20
+ * 指名了行而台账缺席 ⇒ `no-run` + `noteBgOwnerAbsence` 留痕(绝不回落在飞 run);
21
+ * **没指名行**(既有两参调用)⇒ 才回落在飞 run —— 老调用方逐字零行为变化,本参数是 additive。
22
+ *
23
+ * 🔴 会话参数**二态**(codex 复审 medium 采纳:「取了台账的 run 却配现势 session」是确定性 404):
24
+ * · 行登记时**捕到了会话** ⇒ 用**行的会话**(与行的 runId 同源同拍,是唯一自洽的组合);
25
+ * · 行**没有**随行会话(`recordBgParentRun` 只写 run —— 今天 bg 行多数是这一格)⇒ 退**现势会话**。
26
+ * 刻意**不**跟 resume 腿的「没捕到就干脆不带」:本腿与 tail/subagentOutput 同族,遵 [1498]③
27
+ * 无条件带 session 的双版本兼容纪律;runId 既已由台账钉死,配错的 `{runId, session}` 组合在
28
+ * session-bound run 上是 server 干净的 fail-closed 404(落既有 `not-running` 出路),**不会**把消息
29
+ * 投到别的会话 —— 而「没捕到就不带」会让今天最常见的那一格整批 404。resume 那边更严是因为它是
30
+ * AT-MOST-ONCE 的叫醒(真跑一轮,不可回收),两者的代价不对称。
31
+ * 诚实边界:行无会话且端已切到别的会话时,对旧行 steer 会得到 `not-running`(而非静默投错人)。
32
+ *
33
+ * @param childTaskId 这一行子代的 taskId(fleet 行 id 尾段;端有行上下文时**应当**传)。
34
+ */
35
+ export declare function steerEngineSubagent(target: string, text: string, childTaskId?: string): Promise<SubagentSteerOutcome>;
@@ -27,15 +27,51 @@
27
27
  import { hostLog } from '../host.js';
28
28
  import { makeEngineWireClient } from '../engineWireSdk.js';
29
29
  import { engineWireTarget } from '../engineWireTarget.js';
30
- import { activeEngineRunId, engineSessionParamSpread } from '../engineSessionParam.js';
31
- /** Steer a running engine subagent (target = its parentToolCallId, or agentName). */
32
- export async function steerEngineSubagent(target, text) {
30
+ import { engineSessionParamSpread } from '../engineSessionParam.js';
31
+ import { getBgParentRunOwner } from '../subagentContentStore.js';
32
+ import { resolveOwnerRunId } from './engineSubagentResume.js';
33
+ import { noteBgOwnerAbsence } from './subagentOwnerAbsence.js';
34
+ /**
35
+ * Steer a running engine subagent (target = its parentToolCallId, or agentName).
36
+ *
37
+ * 🔴 #280 件A(取址第四腿并入 [4000] Q3=B 口径):`childTaskId` 在场时**台账优先、缺席即诚实缺席**,
38
+ * 与 tail / taskOutput·taskStop / subagentOutput 三腿同姿势,也与孪生的 resume 腿共用**同一个**
39
+ * 取址判据(`resolveOwnerRunId`,不是第二份手抄)。此前本腿直取 `activeEngineRunId()` —— 正是
40
+ * `engineSubagentResume` 头注点名的那处不对称(「同一个 agent 有时能说话有时不能」):
41
+ * · 进程级活跃 run 被并发 main/fork 流互相覆写、又在每个 query 的 finally 清空 ⇒ 后台子代在
42
+ * 宿主 turn 收尾之后仍在跑时,这里恒空(no-run,消息发不出去);另一条 turn 正飞时它有值
43
+ * 但**是别的 run**(打到不相干的 run 上 —— 错值比缺席更坏,与 Q3 同一条理由)。
44
+ * 三态(`resolveOwnerRunId` 的单一判据):台账有这一行 ⇒ 用行值(哪怕此刻另有 run 在飞);
45
+ * 指名了行而台账缺席 ⇒ `no-run` + `noteBgOwnerAbsence` 留痕(绝不回落在飞 run);
46
+ * **没指名行**(既有两参调用)⇒ 才回落在飞 run —— 老调用方逐字零行为变化,本参数是 additive。
47
+ *
48
+ * 🔴 会话参数**二态**(codex 复审 medium 采纳:「取了台账的 run 却配现势 session」是确定性 404):
49
+ * · 行登记时**捕到了会话** ⇒ 用**行的会话**(与行的 runId 同源同拍,是唯一自洽的组合);
50
+ * · 行**没有**随行会话(`recordBgParentRun` 只写 run —— 今天 bg 行多数是这一格)⇒ 退**现势会话**。
51
+ * 刻意**不**跟 resume 腿的「没捕到就干脆不带」:本腿与 tail/subagentOutput 同族,遵 [1498]③
52
+ * 无条件带 session 的双版本兼容纪律;runId 既已由台账钉死,配错的 `{runId, session}` 组合在
53
+ * session-bound run 上是 server 干净的 fail-closed 404(落既有 `not-running` 出路),**不会**把消息
54
+ * 投到别的会话 —— 而「没捕到就不带」会让今天最常见的那一格整批 404。resume 那边更严是因为它是
55
+ * AT-MOST-ONCE 的叫醒(真跑一轮,不可回收),两者的代价不对称。
56
+ * 诚实边界:行无会话且端已切到别的会话时,对旧行 steer 会得到 `not-running`(而非静默投错人)。
57
+ *
58
+ * @param childTaskId 这一行子代的 taskId(fleet 行 id 尾段;端有行上下文时**应当**传)。
59
+ */
60
+ export async function steerEngineSubagent(target, text, childTaskId) {
33
61
  const cfg = engineWireTarget();
34
62
  if (!cfg)
35
63
  return { ok: false, reason: 'no-wire' };
36
- const runId = activeEngineRunId();
37
- if (!runId)
64
+ // 空串/非串按「没指名行」处理(与 resolveOwnerRunId 的 named 判据同口径);窄成 string|undefined
65
+ // 而不是 boolean —— 布尔位不给 TS 窄化,留痕那一行会被迫 cast。
66
+ const named = typeof childTaskId === 'string' && childTaskId !== '' ? childTaskId : undefined;
67
+ const runId = resolveOwnerRunId(named);
68
+ if (!runId) {
69
+ // 指名了行才算「台账缺席」——不指名的通用调用只是没有在飞 run,不是宿主映射缺席(两者出路
70
+ // 同为 no-run,但只有前者是 Q3 要响亮的那一类)。
71
+ if (named !== undefined)
72
+ noteBgOwnerAbsence('subagent-steer', named);
38
73
  return { ok: false, reason: 'no-run' };
74
+ }
39
75
  const client = makeEngineWireClient({
40
76
  baseUrl: cfg.baseUrl,
41
77
  ...(cfg.token ? { token: cfg.token } : {}),
@@ -43,9 +79,13 @@ export async function steerEngineSubagent(target, text) {
43
79
  });
44
80
  if (!client)
45
81
  return { ok: false, reason: 'no-wire' };
82
+ // 会话二态(见头注):行登记的会话优先,缺则退现势会话。这里是同一张表的第二次同步读
83
+ // (与上面 `resolveOwnerRunId` 之间无 await ⇒ 同一拍),取址判据本身仍只有那一处,不另铸。
84
+ const rowSession = named !== undefined ? getBgParentRunOwner(named)?.sessionId : undefined;
85
+ const session = rowSession !== undefined ? { session: rowSession } : engineSessionParamSpread();
46
86
  try {
47
87
  // service 1.89 receipt shape: { note: "Message queued for delivery to <name> …", delivery, … }
48
- const body = await client.runs.steerSubagent(runId, target, { content: text }, engineSessionParamSpread());
88
+ const body = await client.runs.steerSubagent(runId, target, { content: text }, session);
49
89
  let receipt = 'Message queued for delivery at the next tool round.';
50
90
  const copy = body?.note ?? body?.message;
51
91
  if (typeof copy === 'string' && copy.length > 0)
@@ -23,7 +23,7 @@
23
23
  | peer:wire 契约 | `@sema-agent/sdk` **>=6.17.2**(value-level,非 type-only) | `package.json` `peerDependencies` |
24
24
  | peer:会话词汇表 | `@sema-agent/agent-types` **>=0.2.0**(type-only,零运行时) | 同上 |
25
25
  | runtime dep | `diff` ^9.0.0(**唯一**一条;portability 门按**等值**钉死) | `package.json` `dependencies` |
26
- | 公开导出面 | **740** 个运行期符号(+ 37 个测试钩) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
26
+ | 公开导出面 | **748** 个运行期符号(+ 37 个测试钩) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
27
27
  | 常驻门 | 以 `scripts/gates-manifest.json` 的 `suites` 长度为准(**本档不抄这个数**) | `scripts/gates-manifest.json`;`npm test` 的名单等值门与它逐名对账 |
28
28
  | 沿革档 | 0.29.0 起建 `CHANGELOG.md`;更早批次记账在 `src/index.ts` 文件头 + `docs/REFACTOR-LEDGER.md` | — |
29
29
 
@@ -99,7 +99,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
99
99
 
100
100
  ## §2 公共导出面地图(按域)
101
101
 
102
- > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**740** 项)。
102
+ > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**748** 项)。
103
103
  > 本节**不逐名抄**,只给「域 → 承重导出 → 用途 → 实现锚」。承重导出 = 一个端为了让这个域干活
104
104
  > **必须**直接调到的那几个符号;其余是它们的类型、变体与辅助位。
105
105
  > 单一入口:`import { … } from '@sema-agent/client-core'`(`exports` 只有 `.` 一个;
@@ -109,7 +109,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
109
109
 
110
110
  `public-export-baseline.json` 由 **`dist/index.js` 的运行期导出**生成(生成口径自述见
111
111
  `scripts/run-client-core-typeshape-test.mjs`,双向精确集合门在 `scripts/run-public-surface-test.mjs`)。
112
- 实测:740 项 **100% 是运行期导出,零 type-only**。
112
+ 实测:748 项 **100% 是运行期导出,零 type-only**。
113
113
 
114
114
  **推论(端必须知道)**:
115
115
  - barrel 导出的**类型**面比 707 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
@@ -118,19 +118,19 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
118
118
  端依赖这些类型是合法的,但**不要**拿基线 diff 当"类型面没变"的证据。
119
119
  - `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
120
120
 
121
- 740 项的内部构成(帮助端估读表大小):**218** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
121
+ 748 项的内部构成(帮助端估读表大小):**221** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
122
122
  (矩阵、键集、env 名、锚串)而非可调用物;**4** 项是 PascalCase 运行期值
123
123
  (`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError`);
124
124
  **39** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
125
125
 
126
- ### 2b. 域图(16 域,逐域计数之和 = 740)
126
+ ### 2b. 域图(16 域,逐域计数之和 = 748)
127
127
 
128
128
  | # | 域 | 名数 | 承重导出 | 用途 | 实现锚 |
129
129
  |---|---|---|---|---|---|
130
- | 1 | **适配内核(下行主链)** | 29 | `adapt` · `createWireToCcAdapter` · `runStream` · `eventToSdkMessage` · `terminalToSdkResult` · `turnUsageToModelUsage` · `isRunStreamActive` · `ADAPTER_DIVERGENCES` | 引擎 SSE `AgentEvent` → 端要渲的**双面输出**:transcript(`SDKMessage`)+ chrome(瞬态 `ChromeEvent`)。**本包存在的理由** | `src/adapt.ts`、`src/adapt/{arms,wireShapes,panelTasks}.ts`(经 `adapt.ts` 再导出)、`src/adapter/runStream.ts`、`src/adapter/downstream/*`、`src/adapter/types.ts` |
130
+ | 1 | **适配内核(下行主链)** | 33 | `adapt` · `createWireToCcAdapter` · `runStream` · `eventToSdkMessage` · `terminalToSdkResult` · `turnUsageToModelUsage` · `isRunStreamActive` · `ADAPTER_DIVERGENCES` | 引擎 SSE `AgentEvent` → 端要渲的**双面输出**:transcript(`SDKMessage`)+ chrome(瞬态 `ChromeEvent`)。**本包存在的理由** | `src/adapt.ts`、`src/adapt/{arms,wireShapes,panelTasks}.ts`(经 `adapt.ts` 再导出)、`src/adapter/runStream.ts`、`src/adapter/downstream/*`、`src/adapter/types.ts` |
131
131
  | 2 | **seam 公共契约** | 2(其余为 type-only) | `CHROME_ARMS` · `deriveTranscriptId` | 公共词汇 + **id 确定性不变量**(同一条流重放 ⇒ 同一串 id)。`CHROME_ARMS` = 端「我要消费哪些 chrome 臂」的对照清单 | `src/seam.ts` |
132
132
  | 3 | **HITL 决断卡链**(§4/§5 主战场) | 119 | `makeHitlCanUseTool` · `HitlBridge` · `findPendingForTask` · `HitlSafetyError` · `bridgeAskUserQuestionGates` · `surfaceToolApprovalFrameAndRespond` / `surfaceFsApprovalAndDecide` · `readToolApprovalRespondAck` · `installApprovalCardPort(For)` · `installHitlHostSurface(For)` · `armPlanReviewApproval` · `reopenPlanReviewCard` · `decidePlanReview` · `startApprovalsFeed` · `pendingRowIsOwnedByThisSession` · `approvalCallKey`/`liveFrameCallKey`/`planReviewQuestionId` · `registerArmedGateFor`/`wasGateArmedFor`/`clearArmedGateFor` · `waitForGateArmed(For)`/`onGateArmed(For)`/`gateArmedWaitMs`(#244 F1 呈现回执事件源) · `planReviewArmedKey(For)`/`notePlanReviewAnswered(For)`/`notePlanReviewAnsweredIfDecisive(For)`(A-024.4 plan 呈现分代) · `toolEndOutputText` · `isAskTool` · `waitForParkRowBirth` · `classifyAskParkRows` / `askParkRowArm` / `classifyAskParkChainFailure` · `readDecisionNoteAudit` / `decisionNoteAuditLine` · `resumeRunningOptions` / `resumeChoiceFromLabels` · `persistedRulesLaneAvailable`/`persistedRulesGovernanceAvailable` · `classifyRulesFailure` · `listAllPersistedRules` · `classifySkippedReason` · `readRulePersistOutcome`(#244 F2:persist-ack 读口与 `readToolApprovalRespondAck` 合成一处) · `parseLocalAllowRule`(durable 腿本地落规则窄化骨架,谓词经 `LocalAllowRuleDeps` 注入) | suspended→decide→resume 环。🔴 **D-1 两元组 verbatim 回显**是字节级断言的安全不变量,端**不许重实现它的任何一段**。🔴 键空间边界(web [C1] d3 拦截):`gateIdentity` 四常量两函数只覆盖 HITL questionId/callKey 空间;seat 的 `TOOL_PERMISSION_REQUEST_ID_DOMAINS`(`plan:` 等)是另一键空间,**两者绝不合并**(合并=座位校验器静默拒全部 plan-review 卡) | `src/hitl/hitlBridge.ts`、`toolApprovalWire.ts`、`askGateWire.ts`、`planReviewWire.ts`、`hitlHostSurface.ts`、`gateIdentity.ts`、`armedGateRegistry.ts`、`parkOwnership.ts`、`parkResolver.ts`、`approvalsFeed.ts`、`frameRouter.ts`(**只挑名导出** `toolEndOutputText`/`ENGINE_ABORT_TOOL_RESULT`/`isAskTool`/`HITL_REJECT_MESSAGE`)、`parkRowBirthWait.ts`、`approvalDecisionNoteAudit.ts`、`askParkRowRouting.ts`、`resumeRunningCard.ts`(#265 上收的判定层)、`persistedRulesWire.ts`、`localAllowRule.ts`(#244 F2 规则侧) |
133
- | 4 | **子代 wire + 面板侧信道台账** | 81 | `tailEngineSubagent` · `installSubagentActivitySink` · `installSubagentTailMetaSink`(#280 件2:tail meta 帧发布口,`contentFrames` 判别位载体)· `stopEngineTask` + `classifyTaskStopConflict` · `fetchEngineSubagentReport` · `steerEngineSubagent` · `resumeSettledSubagent` + `resolveSubagentResumeContext` + `resolveOwnerRunId` + `classifySubagentResumeFailure` + `subagentResumeAvailable`(#242 批 2 A-028.7:resume 判定半场上收,与 steer 孪生同居;取址三态 = 台账有行用行值 / 指名了行但台账缺席则**诚实缺席绝不回落在飞 run** / 没指名行才回落。出路文案归端)· `recordSubagentOwnerFromProgress` + `getBgParentRunOwner`(A-028.6:「子代 → 宿主 run」**单表**,宿主 run 必须由持 stream-local 值的调用方显式传入,包内绝不从 `activeEngineRunId()` 推断)· `noteBgOwnerAbsence`(#242 批 3 [4000] Q3=B:tail/taskOutput·taskStop/subagentOutput 三腿台账缺席即诚实缺席**绝不回落在飞 run**,缺席 warn 留痕每 (腿,taskId) 一条)· `auditRetainWithoutWake`([4000] Q5:引擎宣示 `subagentResume` + 本端在付 `retainSubagentSessions` + 端未实现 `wakeSubagent` ⇒ 响亮一条;`CLIENT_VERBS.wakeSubagent` 维持 fail-soft)· `subscribeSubagentContent` · `subscribeEngineAgentPanel` · `publishQuestionFrame` / `respondToQuestion` | 驱动与观测委派子代;经 module 级台账喂活体 agent/task 面板。全部**能力位 gate**(§5b) | `src/subagent/*.ts`、`src/subagentContentStore.ts`、`src/engineAgentPanelStore.ts`、`src/engineInlineTaskStats.ts`、`src/engineToolLabelStore.ts`、`src/liveQuestionStore.ts` |
133
+ | 4 | **子代 wire + 面板侧信道台账** | 82 | `tailEngineSubagent` · `installSubagentActivitySink` · `installSubagentTailMetaSink`(#280 件2:tail meta 帧发布口,`contentFrames` 判别位载体)· `stopEngineTask` + `classifyTaskStopConflict` · `fetchEngineSubagentReport` · `steerEngineSubagent`(0.32.0 未发布 #280 件A:additive 第三参 `childTaskId` —— 端有行上下文时**应当**传,传了就走「台账优先 / 缺席即诚实缺席 + `noteBgOwnerAbsence` 留痕」的 Q3 口径,与 tail·taskOutput·subagentOutput 三腿同姿势、与孪生 resume 腿共用同一个 `resolveOwnerRunId` 判据;**不传**则逐字维持旧行为=回落在飞 run)· `resumeSettledSubagent` + `resolveSubagentResumeContext` + `resolveOwnerRunId` + `classifySubagentResumeFailure` + `subagentResumeAvailable`(#242 批 2 A-028.7:resume 判定半场上收,与 steer 孪生同居;取址三态 = 台账有行用行值 / 指名了行但台账缺席则**诚实缺席绝不回落在飞 run** / 没指名行才回落。出路文案归端)· `recordSubagentOwnerFromProgress` + `getBgParentRunOwner`(A-028.6:「子代 → 宿主 run」**单表**,宿主 run 必须由持 stream-local 值的调用方显式传入,包内绝不从 `activeEngineRunId()` 推断)· `noteBgOwnerAbsence`(#242 批 3 [4000] Q3=B:tail/taskOutput·taskStop/subagentOutput 三腿台账缺席即诚实缺席**绝不回落在飞 run**,缺席 warn 留痕每 (腿,taskId) 一条)· `clearBgTerminalFacts`(#242 批 3 扫码修:复活=新周期,旧周期终态事实作废——fleetLedger 复活两腿按尾段清账,factsAccepted 方向核不再拿上周期终态当先例)· `auditRetainWithoutWake`([4000] Q5:引擎宣示 `subagentResume` + 本端在付 `retainSubagentSessions` + 端未实现 `wakeSubagent` ⇒ 响亮一条;`CLIENT_VERBS.wakeSubagent` 维持 fail-soft)· `subscribeSubagentContent` · `subscribeEngineAgentPanel` · `publishQuestionFrame` / `respondToQuestion` | 驱动与观测委派子代;经 module 级台账喂活体 agent/task 面板。全部**能力位 gate**(§5b) | `src/subagent/*.ts`、`src/subagentContentStore.ts`、`src/engineAgentPanelStore.ts`、`src/engineInlineTaskStats.ts`、`src/engineToolLabelStore.ts`、`src/liveQuestionStore.ts` |
134
134
  | 5 | **fleet 投影** | 43 | `createFleetLedger` · `projectTasks` · `projectWorkflows` · `projectFleetAgentRows` · `readEngineActiveBgTasks` · `FLEET_TASK_VIEW_KEYS` | 老 `fleetClient` 那一刀的成品:**帧体归库、连接归端** —— 端持 SSE 连接,库做行投影 + 保留台账 | `src/fleet/fleetProjection.ts`、`src/fleet/fleetLedger.ts`、`src/fleetAgentPanelProjection.ts`、`src/fleetTaskDesc.ts` |
135
135
  | 6 | **请求装配(上行唯一构造口)** | 8 | `buildTaskRequest` · `REQUEST_FIELD_MATRIX` · `unregisteredRequestKeys` · `applyLiveRequestDefaults` · `taskNotificationToPrintFrame` | 两条车道(`interactive`/`print`)出站请求的**唯一**构造器;`unregisteredRequestKeys` 是可执行门 —— 端偷带一个未登记键上 wire 就红 | `src/request/taskRequest.ts`、`src/request/printNotification.ts` |
136
136
  | 7 | **通知与 outstanding 台账** | 37 | `installNotificationQueuePort` · `normalizeTaskNotification` · `taskNotificationDedupKeyFromWire` · `registerOutstandingBgTask` / `registerOutstandingWorkflowRun` · `notificationQueuePortMisses` · `subscribeOutstandingWorkflows` · `outstandingDeliverableWorkflowCount` | `task_notification` 归一 + 去重 + 投递进宿主命令队列的**一把闸**;`outstandingDeliverableWorkflowCount()` 是 headless `-p` 的**退出门** | `src/notifications.ts`(11 个 module 台账) |
@@ -141,8 +141,8 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
141
141
  | 12 | **workflow 与后台工作视图** | 19 | `projectWorkflowRun` · `createLiveWorkflowSource` · `ensureWorkflowActivityLedger` · `readWorkflowActivityLedger` · `stopWorkflowActivityLedger` · `resetWorkflowActivityLedgers` · `createBackgroundView` · `projectBackgroundView` · `recordWorkflowAgentTaskId` · `agentDisplayStatus` | 活过一个 turn 的长任务读面:workflow run + 跨 session 后台任务归一表(`assistant.tasks` 与 fleet SSE **两源独立降级**) | `src/workflow.ts`、`src/workflowClient.ts`、`src/workflowMonitor.ts`、`src/agentSession/backgroundView.ts`(+ 纯类型 `src/agentSession/contract.ts`) |
142
142
  | 13 | **座位 IPC 契约** | 33 | `LOCAL_SESSIONS_SPEC` · `SEAT_METHOD_NAMES` · `SEAT_EVENT_TYPES` · `isLocalSessionEvent` · `isToolPermissionRequest` · `toolPermissionRequestId` · `SEAT_VALIDATOR_KEY_COVERAGE` | desktop↔web 座位 IPC 契约的**单一真源**(此前两边各一份、名字零重合 ⇒ 编译器永远不会告诉你它们漂了)。🔴 加 verb 忘了加 `LOCAL_SESSIONS_SPEC` **不报错**:preload 不注册 channel、渲染端读到 `undefined` | `src/seatContract.ts`(**零 import**,纯类型 + 常量 + 纯谓词) |
143
143
  | 14 | **宿主端口与会话槽** | 26 | `installHost` · `installHostFor` · `hostPortMisses(For)` · `DEFAULT_SESSION_KEY` · `hostEnv` · `unrefTimer` · `parseLocaleTag` / `pickUiLanguage`(#244 F4 族D A-028.20:locale tag 手术单源 + UI 语言判定;与 `resolveRegionHint` 双出口成文 —— 语言偏好域 en/zh ≠ 地址可达域 cn/intl/unknown,`zh-Hant` 前者 zh 后者 intl 是设计)· `engineNamespaceKeyFor` / `mergeSessionMapRecord` / `mergeEngineEntry`(A-028.12:会话 id 映射单一键形 + merge 判定;存储经 `SessionMapStorePort` 归端 —— cli 文件锁/原子写,web localStorage)| 进程/端级装配层(settings/fs/queue/timers/session/log/probe),与 per-turn 的 `AdapterContext` **分层**。头注的判定规则:**这个能力每 turn 都会变吗?** 会 ⇒ `ctx`;不会 ⇒ `installHost` | `src/host.ts`、`src/hostEnv.ts`、`src/sessionSlot.ts`、`src/unrefTimer.ts`、`src/env/{localeGeo,localeTag,uiLanguage}.ts`、`src/sessionMap.ts` |
144
- | 15 | **控制面与传输** | 68 | `ControlRouter`(+ `ControlSafetyError`)· `makeEngineWireClient` + `resolveWireAuth` · `installEngineWireTarget` · `diagnoseSseIdleTear` / `isSseIdleError` · `attemptActiveRunSelfHeal` + `activeRunBusySignal` + `activeRunSelfHealRow` / `activeRunBusyHeadlessRow` · `kickEngineCapsProbe` / `engineCapTrue` · `mapBrainStatusToRetry` · `waitForClaimRelease` + `CLAIM_RELEASED_STATES` / `CLAIM_HELD_STATES` · `atMostOnceFailureClass` / `readSteerDelivery` · `INTERACTIVE_WAY_OUT`(默认出路串单源)· `normalizeWirePrincipal`(A-028.10:principal 在场性 trim 原语 —— 全空白=缺席不发头,engineWireTarget 两臂/makeEngineWireClient/壳 livePrincipal 同尺)· `classifyTurnWireError` / `isWireTransportError` / `isPreStreamDrainingReject` / `isResumeAtRejection` / `drainingRetryDelayMs` / `scenarioDenyFromError` + `WIRE_NETWORK_ERROR_PATTERN`(A-028.11/.13:turn 错误分型判定半场,人话文案与渲染归端) | 上行通道的**监管**半场(submit / steer / kill / 队列命令定序)+ 传输构造、caps 探测、SSE 断流分诊、**409 active-run 自愈** | `src/controlRouter.ts`、`steering.ts`、`sseIdleTriage.ts`、`retryStatus.ts`、`diagnostics.ts`、`engineWireSdk.ts`、`engineWireTarget.ts`、`src/principalWire.ts`、`src/wireErrorTriage.ts`、`engineSessionParam.ts`、`engineCapsCache.ts`、`liveInitToolFace.ts`、`adapter/activeRunSelfHeal.ts` |
145
- | 16 | **引擎词汇表与包自检** | 39 | `CONFIG_REFUSAL_CODES` / `isConfigRefusalCode` · `STOP_CONFLICT_CODES` · `isInterruptedToolEndCode` · `isRewindFamilyCode` · `CLIENT_VERBS` · `compensationSplitViolations` | 三端分臂共用的**去字面化** `errorCode` 词表(病根正是三端各抄一份字面);编译期 verb 闭合门;搬迁补偿登记表 | `src/engineErrorCodes.ts`(32 项;A-028.11/.13 补 `DRAINING_ERROR_CODE`/`SCENARIO_NOT_ALLOWED_ERROR_CODE`/`RESUME_AT_ERROR_CODE_PREFIX`)、`src/classifierVerdictWire.ts`、`src/compensations.ts`、`src/clientSlice.ts` |
144
+ | 15 | **控制面与传输** | 69 | `ControlRouter`(+ `ControlSafetyError`)· `makeEngineWireClient` + `resolveWireAuth` · `installEngineWireTarget` · `diagnoseSseIdleTear` / `isSseIdleError` · `attemptActiveRunSelfHeal` + `activeRunBusySignal` + `activeRunSelfHealRow` / `activeRunBusyHeadlessRow` · `kickEngineCapsProbe` / `engineCapTrue` · `mapBrainStatusToRetry` · `waitForClaimRelease` + `CLAIM_RELEASED_STATES` / `CLAIM_HELD_STATES` · `atMostOnceFailureClass` / `readSteerDelivery` · `INTERACTIVE_WAY_OUT`(默认出路串单源)· `normalizeWirePrincipal`(A-028.10:principal 在场性 trim 原语 —— 全空白=缺席不发头,engineWireTarget 两臂/makeEngineWireClient/壳 livePrincipal 同尺)· `classifyTurnWireError` / `isWireTransportError` / `isPreStreamDrainingReject` / `isResumeAtRejection` / `drainingRetryDelayMs` / `scenarioDenyFromError` + `WIRE_NETWORK_ERROR_PATTERN`(A-028.11/.13:turn 错误分型判定半场,人话文案与渲染归端) | 上行通道的**监管**半场(submit / steer / kill / 队列命令定序)+ 传输构造、caps 探测、SSE 断流分诊、**409 active-run 自愈** | `src/controlRouter.ts`、`steering.ts`、`sseIdleTriage.ts`、`retryStatus.ts`、`diagnostics.ts`、`engineWireSdk.ts`、`engineWireTarget.ts`、`src/principalWire.ts`、`src/wireErrorTriage.ts`、`engineSessionParam.ts`、`engineCapsCache.ts`、`liveInitToolFace.ts`、`adapter/activeRunSelfHeal.ts` |
145
+ | 16 | **引擎词汇表与包自检** | 41 | `CONFIG_REFUSAL_CODES` / `isConfigRefusalCode` · `STOP_CONFLICT_CODES` · `isInterruptedToolEndCode` · `isRewindFamilyCode` · `CLIENT_VERBS` · `compensationSplitViolations` | 三端分臂共用的**去字面化** `errorCode` 词表(病根正是三端各抄一份字面);编译期 verb 闭合门;搬迁补偿登记表 | `src/engineErrorCodes.ts`(34 项;A-028.11/.13 补 `DRAINING_ERROR_CODE`/`SCENARIO_NOT_ALLOWED_ERROR_CODE`/`RESUME_AT_ERROR_CODE_PREFIX`)、`src/classifierVerdictWire.ts`、`src/compensations.ts`、`src/clientSlice.ts` |
146
146
 
147
147
  🔴 **`engineErrorCodes` 的开集纪律**(该文件头注逐字):这些 `ReadonlySet` / 前缀谓词一律是**识别表**,
148
148
  回答的是「我认不认得这个码」,**绝不是**「合法码只有这些」。消费点 `switch` **必须留 `default`**,
@@ -644,9 +644,36 @@ HITL 面有 `hitlHostSurfaceFor` —— 见 §5a 的 (a) 表;队列口没有,登
644
644
  (fail-closed,跨会话不认领)。desktop 的 `sessionForEngineId` 单腿判定应收敛到本判据,
645
645
  **传 `sessionKey` + 自注入 `isOwnRun`**。
646
646
 
647
+ #### `onBgNotificationAccepted` 的 `evidence` 参数(0.32.0 未发布,additive 第三参)
648
+
649
+ `createFleetLedger` 的 `FleetLedgerHooks.onBgNotificationAccepted(n, rowIds, evidence)` 在 `bg_notification`
650
+ 帧**通过归属判别之后**回调(被隔离丢弃/畸形早退的不回调)。0.32.0 起补第三参 `evidence`
651
+ (`BgNotificationAcceptEvidence`,type-only 导出):这一帧**凭哪一条臂**放行的。
652
+ **两参消费方(0.31.0 形)零改动照旧** —— 多出来的实参 JS 侧自然忽略。
653
+
654
+ 🔴 **每个词命名的是「放行臂」,不是「归属结论」**;三档强度,中间两格**不同档**。
655
+
656
+ 🔴 **前两格的会话级读法带一个前提:喂给这个 ledger 的 fleet 流,是按这个 ledger 的会话开的。**
657
+ ledger **自己不开流**(帧体归库、连接归端),无从校验;而本包的开流参数 `fleetStreamOptions()` /
658
+ `fleetSnapshotOptions()` 是 **module 级、只认默认槽会话**,连 `sessionKey` 都拿不到。
659
+ ⇒ **单会话宿主**(cli 及今天的三端)前提恒成立;⇒ **keyed 多会话宿主**必须自己保证流与 ledger 同会话,
660
+ 否则前两格会替**别的会话**作证 —— 在册局限 **P-31**(§7c)。
661
+
662
+ | `evidence` | 放行臂 | 强度 | 端该怎么读 |
663
+ |---|---|---|---|
664
+ | `server_fail_closed` | meta `bgNotifyFailClosed` ∧ 本连接 session-bound | **会话级证明**(带上述流前提) | 最强:server 侧注入路已 fail-closed,到达即**这条流的**会话。⚠️ 这一臂**完全不读会话端口**,信的就是「这条流替谁开的」;前提破了它不会报错,而它正是**生产 meta 组合**下命中的那一格 |
665
+ | `own_root` | 通知 `rootSessionId` === `engineSessionParam()`(固定点语义) | **会话级证明**(带上述流前提) | 判据读的是 **`DEFAULT_SESSION_KEY` 槽**的 `SessionPort`,**不是**本 ledger 的 `sessionKey` 槽 —— 单会话宿主两者恒同 |
666
+ | `session_anchor_untrusted` | keyed ledger(非默认 `sessionKey`)上由**会话级臂**放行 | **非证据**(封顶词) | 🔴 会话级臂放行了,但本 ledger 无法信任那个会话锚(开流参数只认默认槽,前提无法成立也无法校验)—— 不冒充 `server_fail_closed`/`own_root`,也不谎标进程成员(帧的 `parentTaskId` 可能是 foreign)。端按非证据档自裁;P-31 正位解落地后此封顶撤销。单会话宿主(默认槽)结构性不出现此词 |
667
+ | `own_parent` | `parentTaskId`(spawn 该子代的 leader run)∈ own-run 台账 | **进程级成员证明** | 🔴 只证明「本进程曾亲手驱动过这条 run」,**不区分会话代际** —— own-run 台账是进程级 `Set`、按会话零分区,`/clear` 或换会话后**旧会话**的 run 仍命中(= 在册局限 **P-13** 在通知面的同一张脸)。**不得**当作「属于当前会话」;要按会话归属做事,按 P-13 的出路自注入会话粒度判据,或只认上面两格 |
668
+ | `absent_parent` | 通知**没带** `parentTaskId` | **非证据** | 🔴 absent-放行姿势(老引擎/老帧形不带该键,fail-closed 会把自家通知整批吞掉)。端要拿归属做有副作用的事(落库/跨会话搬运/翻别人的卡)时,这一格应自裁为「未证明」 |
669
+
670
+ 臂序 = 包内放行判据的**求值序**,多臂同时成立报**实际过门的第一条**(不报「最强的那条理论上也成立」)。
671
+ 词表开集:端对未知词按 `absent_parent` 一档兜底最安全。真源 = `src/fleet/fleetLedger.ts` 的
672
+ `BgNotificationAcceptEvidence` 顶注。
673
+
647
674
  ### 6e. 🔴 已知局限(多会话端**接之前必读**)
648
675
 
649
- 见 §7c 的 **P-10 ~ P-14** 五条 —— 全部是 sessionKey 面的在册局限,
676
+ 见 §7c 的 **P-10 ~ P-14 + P-31** 六条 —— 全部是 sessionKey 面的在册局限,
650
677
  CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读完它们之前接卡口与 plan review。**
651
678
 
652
679
  其中两条会直接把多会话端坑到「装了但不生效」:
@@ -691,6 +718,7 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
691
718
  | **P-12** | low | canonical 重呈短路臂**沿用 arm responder** ⇒ `ReopenPlanReviewOpts.deliverDecision` 注入口**不生效**(成文例外 + debug 留痕) | `src/hitl/planReviewWire.ts`(`mintFreshQuestionId:false` 分支) | 任何**包装决断投递**的端(重试/退避/上屏定序,cli 的 `decideRetry` 是参照)必须走默认 `mintFreshQuestionId` 臂 —— 否则你的包装被静默旁路,跑的是裸 `decidePlanReview` 的 fire-and-forget |
692
719
  | **P-13** | 成文局限(不改行为) | 默认键下的 own-run 归属缺省腿是**进程级**证据,**不区分同一宿主进程内的会话代际** —— `/clear` 前登记的 run 在新会话语境下**仍判 owned**。最坏后果逐字:`用户看到自己旧会话的审批卡` | `src/hitl/parkOwnership.ts`(`ParkOwnershipDeps.isOwnRun` JSDoc);`docs/REFACTOR-LEDGER.md` 记为**驳为成文局限** | 多会话端必须**自注入**会话粒度的 `isOwnRun`;或传非默认 `sessionKey` 并接受缺省腿被整条跳过(代价 = 多一次诚实的 reopen-failed) |
693
720
  | **P-14** | high(打包面) | `activeReopenResponders` 的**单活纪律是 module 单例**:两份实例 ⇒ 各退各的,跨份的旧卡退役不掉 —— **退化回修复前的重复活卡形** | `src/hitl/planReviewWire.ts`(`activeReopenResponders`,singleton-manifest 在册) | 见 §8-G:必须保证 bundle 里只有**一份** `@sema-agent/client-core` |
721
+ | **P-31** | med(0.32.0 未发布登记;codex 复审第二/三轮抓出) | **fleet 面的会话锚整条走默认槽,不跟 ledger 的 `sessionKey`** —— 两条腿都中招:①`bg_notification` 的 `ownByRoot` 判据用 `engineSessionParam()` = `hostSessionFor(DEFAULT_SESSION_KEY)`;②更强的一条 —— `serverFailClosed`(meta 两位)**根本不读会话端口**,它信的是「这条流是替谁开的」,而本包给出的开流参数 `fleetStreamOptions()` / `fleetSnapshotOptions()` 是 **module 级函数、只认默认槽会话**,连 `sessionKey` 都拿不到。⇒ keyed 多会话宿主上,一条按默认槽开的流接到**非默认键** ledger 时,属于默认槽会话的通知会被放行并以 `evidence:'server_fail_closed'`(🔴 **生产 meta 组合下命中的正是这一格**)或 `'own_root'` 交给 hook | `src/fleet/fleetLedger.ts`(`serverFailClosed` / `ownByRoot` / `fleetStreamOptions`);`src/host.ts`(`hostSession` = 默认键) | 单会话宿主(cli 及今天的三端)**不受影响**——默认槽就是它的会话,流与 ledger 恒同会话。**keyed 多会话宿主**:①必须自己保证「流按哪个会话开、就接给哪个 ledger」;②在①落实之前,不要把 `evidence` 的前两格当作「属于本 ledger 会话」的证明去做有副作用的事(落库/翻卡/跨会话搬运)。🔴 正位解 = fleet 面**整条**换 per-key 会话锚(开流参数 + 归属判据一起动);**只改判据这一半会自相矛盾**(按默认槽开流、按 keyed 槽判定 ⇒ 反而丢自己的通知),故本批**不动行为**,登记待裁 |
694
722
 
695
723
  ### 7d. 请求面与其它在册件
696
724
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
5
5
  "license": "MIT",
6
6
  "type": "module",