@sema-agent/client-core 0.32.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,6 +16,15 @@
16
16
  > 🔴 **互链**(web [C166]⑦):各版「已知局限」段只记**该版新增**;接入面已知局限的完整台账在
17
17
  > `docs/INTEGRATION-CLIENTS.md` §6e/§7 —— **只读其一会漏**,两处都过。
18
18
 
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
+
19
28
  ## 0.32.0(2026-08-16)
20
29
 
21
30
  **1.0.80 发包前扫码三件(#280 件A / #281 / #284)。公开面 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 报告把它丢了)。 */
@@ -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
- | 公开导出面 | **741** 个运行期符号(+ 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`(**741** 项)。
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
- 实测:741 项 **100% 是运行期导出,零 type-only**。
112
+ 实测:748 项 **100% 是运行期导出,零 type-only**。
113
113
 
114
114
  **推论(端必须知道)**:
115
115
  - barrel 导出的**类型**面比 707 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
@@ -118,16 +118,16 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
118
118
  端依赖这些类型是合法的,但**不要**拿基线 diff 当"类型面没变"的证据。
119
119
  - `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
120
120
 
121
- 741 项的内部构成(帮助端估读表大小):**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 域,逐域计数之和 = 741)
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
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` |
@@ -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`**,
@@ -663,6 +663,7 @@ ledger **自己不开流**(帧体归库、连接归端),无从校验;而本包
663
663
  |---|---|---|---|
664
664
  | `server_fail_closed` | meta `bgNotifyFailClosed` ∧ 本连接 session-bound | **会话级证明**(带上述流前提) | 最强:server 侧注入路已 fail-closed,到达即**这条流的**会话。⚠️ 这一臂**完全不读会话端口**,信的就是「这条流替谁开的」;前提破了它不会报错,而它正是**生产 meta 组合**下命中的那一格 |
665
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 正位解落地后此封顶撤销。单会话宿主(默认槽)结构性不出现此词 |
666
667
  | `own_parent` | `parentTaskId`(spawn 该子代的 leader run)∈ own-run 台账 | **进程级成员证明** | 🔴 只证明「本进程曾亲手驱动过这条 run」,**不区分会话代际** —— own-run 台账是进程级 `Set`、按会话零分区,`/clear` 或换会话后**旧会话**的 run 仍命中(= 在册局限 **P-13** 在通知面的同一张脸)。**不得**当作「属于当前会话」;要按会话归属做事,按 P-13 的出路自注入会话粒度判据,或只认上面两格 |
667
668
  | `absent_parent` | 通知**没带** `parentTaskId` | **非证据** | 🔴 absent-放行姿势(老引擎/老帧形不带该键,fail-closed 会把自家通知整批吞掉)。端要拿归属做有副作用的事(落库/跨会话搬运/翻别人的卡)时,这一格应自裁为「未证明」 |
668
669
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.32.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",