@sema-agent/client-core 0.62.1 → 0.63.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.
@@ -0,0 +1,143 @@
1
+ /**
2
+ * src/engineIdentity.ts — 引擎**代际锚**读器(`/health` 的 `pid` / `instanceId` / `startedAt`)
3
+ * 三端共用件(0.63.0;engine ≥7.67.0 / sdk 8.8.0 / S-179)。
4
+ *
5
+ * ── 为什么这一面值得一个模块 ─────────────────────────────────────────────────────────────────
6
+ * `/health` 是一台 worker **唯一免凭证**的门,心跳恒绿。于是「另一个宿主把这台共用引擎重启了」
7
+ * 此前只能靠某个**带凭证**的请求先撞上 401 才被发现 —— 而那条路会把一次重启误读成网络故障
8
+ * (处置完全相反:一个要重新握手,一个要重试)。engine 7.67.0 起 `/health` 无条件带上
9
+ * `startedAt`(这个**进程**自己的起点,epoch ms,模块加载时按 `process.uptime()` 铸一次),
10
+ * 换代因此可以被当成**一等事实**读出来,而不是等一次失败。
11
+ *
12
+ * ── 归层:本模块只读与比,不做状态机 ────────────────────────────────────────────────────────
13
+ * 「读出换代之后要做什么」(丢连接、重握手、提示用户)是**宿主的状态机**,壳有自己的那一只闸。
14
+ * 本包给的是两件纯物:一只窄读器 {@link engineIdentityOf} 与一族纯比较
15
+ * ({@link engineIdentityVerdict} / {@link engineIdentityChanged} / {@link engineIdentityChangedBy})。
16
+ * 三端共用同一套判据,才不会一端把「判不出」当「没换」、另一端把它当「换了」。
17
+ *
18
+ * ── 🔴 三态,不是布尔 ───────────────────────────────────────────────────────────────────────
19
+ * 一个布尔把「没换」与「判不出」压进同一个 `false`,而这两件事的下一步相反(前者继续用,后者
20
+ * 要么再探一次要么按最坏情况握手)。所以真源是三态判词;布尔口保留是因为大多数调用点只关心
21
+ * 「有没有**正面观察到**换代」,它的 `true` 是一句断言、`false` **不是**
22
+ * ([honest-absence-not-fabricated-zero])。
23
+ *
24
+ * ── 🔴 三只锚并列,不是「startedAt 说了算」───────────────────────────────────────────────────
25
+ * `instanceId` 答「是不是同一条命」,`startedAt` 还答「从什么时候起」——上游声明里逐字写着两者
26
+ * **刻意不合并**。所以判据是:**任一**在两侧都在场的锚不同 ⇒ 换代。只看 startedAt 会把
27
+ * 「startedAt 相同而 instanceId 不同」这类真实的坏读数判成「同一条命」。优先序
28
+ * ({@link ENGINE_IDENTITY_ANCHORS})只决定**判词报哪一只**,不决定看不看别的。
29
+ */
30
+ /**
31
+ * 代际锚的**优先序**。序:`startedAt`(最具体:同一条命还答得出从何时起)→ `instanceId`
32
+ * (同一条命吗)→ `pid`(端口上还是那个进程吗)。
33
+ *
34
+ * 🔴 **顺序**只决定 {@link engineIdentityChangedBy} 报哪一只;但**成员集合是承重的** ——
35
+ * 比较器**遍历的正是这张表**,少一只锚就等于那只锚不再参与判定(异源对抗复审 R2 [medium] 实撞:
36
+ * 移掉 `instanceId` 之后,两份 `instanceId` 不同、别的锚相同的合法读数从 `changed` 翻成 `same`,
37
+ * 宿主据此漏掉一次重握手)。
38
+ * 🔴 因此它是 `Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]`:后者一行
39
+ * `.splice()` 就能改,而公面消费者拿到的正是这个实例。这是本仓**已定谳的病形**
40
+ * (`RESUME_RETRY_LATER_CODES` / `RUN_LEVEL_STOP_ERROR_CODES` 两次同款处置)。
41
+ * 运行期反钉在 `run-engine-identity-test.mjs` 的 Bz 段(试改 + 试改后判词没漂,两半都断言)。
42
+ */
43
+ export const ENGINE_IDENTITY_ANCHORS = Object.freeze([
44
+ 'startedAt',
45
+ 'instanceId',
46
+ 'pid',
47
+ ]);
48
+ /** 正的有限数(epoch 时刻 / 进程号都不可能是 0、负数、NaN、Infinity)。 */
49
+ function posNum(v) {
50
+ return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
51
+ }
52
+ /**
53
+ * `/health` 200 体 → 代际锚读数;**畸形一律缺席**,绝不抛出。
54
+ *
55
+ * 🔴 **逐格独立**:一只锚形坏只丢那一格(三只锚是三条独立的证据,一只坏不该把另两只藏起来)。
56
+ * 🔴 **恒返回一只读数对象**(读不出任何一格时是 `{}`),不返回 `undefined` —— 调用方读的是
57
+ * 「哪几只锚这次答得出来」,而不是「这次有没有响应」(后者是它自己的探测腿知道的事)。
58
+ * ⚠️ 只取三只锚;`/health` 上其余的键(`version` / `configHash` / `dataRoot` / 降级位…)各有
59
+ * 自己的读面,不在这里搭便车 —— 一个「顺手多带两个键」的读数会变成第二份 health 台账。
60
+ */
61
+ export function engineIdentityOf(health) {
62
+ if (typeof health !== 'object' || health === null || Array.isArray(health))
63
+ return {};
64
+ const h = health;
65
+ const pid = posNum(h.pid);
66
+ const instanceId = typeof h.instanceId === 'string' && h.instanceId.length > 0 ? h.instanceId : undefined;
67
+ const startedAt = posNum(h.startedAt);
68
+ return {
69
+ ...(pid !== undefined ? { pid } : {}),
70
+ ...(instanceId !== undefined ? { instanceId } : {}),
71
+ ...(startedAt !== undefined ? { startedAt } : {}),
72
+ };
73
+ }
74
+ /**
75
+ * 比较前的**统一归一**:一律过 {@link engineIdentityOf}。
76
+ *
77
+ * 🔴 **不能只做「是不是对象」的强转**(异源对抗复审 [medium]):公开入口吃 `unknown`,强转会把
78
+ * **坏锚**(`null` / `0` / 空串 / 非有限数)当成「可比的证据」—— 两侧同为 `{pid:null}` 于是判
79
+ * `same`,而经窄读器之后两边都是空读数、正确判词是 `unknown`。后果不是渲染错一行,是宿主据此
80
+ * **跳过**本该做的重探或重握手。
81
+ * ⚠️ 对一份**已经窄读过**的视图,本函数是幂等的(合法锚原样通过),所以调用方递原始 `/health` 体
82
+ * 还是递视图,判词一致 —— 门里有一条钉。
83
+ */
84
+ function viewOf(x) {
85
+ return engineIdentityOf(x);
86
+ }
87
+ /**
88
+ * 两份读数说的是不是**同一条命**。
89
+ *
90
+ * 判据(与 {@link engineIdentityChangedBy} 同一套,只是那一只多报一个名字):
91
+ * · **任一**在两侧都在场的锚**不同** ⇒ `changed`;
92
+ * · 至少一只锚可比、且没有一只不同 ⇒ `same`;
93
+ * · **一只可比的锚都没有** ⇒ `unknown`。
94
+ *
95
+ * 🔴 **锚交集为空 ⇒ `unknown` 而不是 `same`**:老 worker 只报 `pid`、新 worker 只报 `startedAt`
96
+ * 时,「没发现不同」是因为**没得比**,不是因为它没换。把这一格读成 `same` 正是本模块要根治的
97
+ * 那类静默病。
98
+ * 🔴 单侧在场的锚**不是反证**:一侧有 `startedAt` 另一侧没有,只说明其中一台答不出这只锚;
99
+ * 只要另有一只锚可比且相同,判词照给 `same`。
100
+ */
101
+ export function engineIdentityVerdict(a, b) {
102
+ const x = viewOf(a);
103
+ const y = viewOf(b);
104
+ let comparable = 0;
105
+ for (const k of ENGINE_IDENTITY_ANCHORS) {
106
+ const av = x[k];
107
+ const bv = y[k];
108
+ if (av === undefined || bv === undefined)
109
+ continue;
110
+ comparable += 1;
111
+ if (av !== bv)
112
+ return 'changed';
113
+ }
114
+ return comparable > 0 ? 'same' : 'unknown';
115
+ }
116
+ /**
117
+ * 换代是**哪一只锚**看出来的(优先序最高的那一只不同的锚)。没换 / 判不出 ⇒ `undefined`。
118
+ * 给的是一句人可读的诊断线索(「端口上换了个进程」vs「同一个进程报了另一条命」),
119
+ * **不是**裁决位 —— 裁决位是 {@link engineIdentityVerdict}。
120
+ */
121
+ export function engineIdentityChangedBy(a, b) {
122
+ const x = viewOf(a);
123
+ const y = viewOf(b);
124
+ for (const k of ENGINE_IDENTITY_ANCHORS) {
125
+ const av = x[k];
126
+ const bv = y[k];
127
+ if (av === undefined || bv === undefined)
128
+ continue;
129
+ if (av !== bv)
130
+ return k;
131
+ }
132
+ return undefined;
133
+ }
134
+ /**
135
+ * **正面观察到换代了吗**。
136
+ * 🔴 `true` 是一句断言(某只可比的锚确实变了);`false` **不是** —— 它同时覆盖「没换」与
137
+ * 「判不出」。要分清这两件事,读 {@link engineIdentityVerdict}(那是本族的真源)。
138
+ * 这个方向是刻意的:据 `true` 做的事(重新握手)在误判时代价可控,据 `false` 做的事
139
+ * (继续用这条连接)在误判时会一直对着一台已经换掉的引擎说话。
140
+ */
141
+ export function engineIdentityChanged(a, b) {
142
+ return engineIdentityVerdict(a, b) === 'changed';
143
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * src/engineNoticeCodes.ts — `engine_notice` 的**码册**与 **audience 表**(0.63.0;L-167;core 7.9.x)。
3
+ *
4
+ * -- 「渲不渲」的判据不在这一端 --------------------------------------------------------------
5
+ * 一条引擎通告该不该占用户的注意力,正确判据**不是**「这一端的渲染表有没有那一格」(那会随各端的
6
+ * 施工进度晃),而是**上游有没有把这个码铸进它的成文码册**:
7
+ * · 码册**内**的码 = 引擎作为治理事实铸出来的,即使这一端还没有专属文案也该让人看见(通用行);
8
+ * · 码册**外**的码 = 转发进同一个水槽的宿主/适配器噪声,呈现面 fail-closed(只落 debug)。
9
+ * audience 回答另一问:这条事实**说给谁听**。一条 operator 行推给终端用户是噪音;一条 user 行只落
10
+ * operator 日志,就是把该告诉他的事瞒下了。两值而已(`user` / `operator`)—— 上游刻意不设第三值:
11
+ * 「这条通告能不能路由到某个会话」不是 audience,而是 `sessionId` 在不在场(**逐次发射**的事实)。
12
+ *
13
+ * -- 为什么是镜像而不是 import ---------------------------------------------------------------
14
+ * `@sema-agent/core` 不是本包消费者的依赖(它既不是 peer 也不是 runtime dep),而本包的 `.d.ts`
15
+ * 一旦引用它,装了本包却没装 core 的下游会当场编译不过。这与本包 `toolResult.ts` / `retryStatus.ts` /
16
+ * `skillsWireCaps.ts` 三处的处置同形:**按 wire 事实镜像,把代价交给门**——
17
+ * `run-engine-notice-catalog-test.mjs` 对**实装 devDep core** 的两份产物逐码逐行双向对账,
18
+ * core 一动这里就先红。上游自己也是这么设计的:它导出码册的理由逐字写着「a downstream table with a
19
+ * row missing … is a mechanically detectable disagreement rather than an argument」。
20
+ *
21
+ * -- 提货记账(码册加员)-----------------------------------------------------------------------
22
+ * · **core 7.10.0 +2**:`delegation.ask_unresolvable`(audience **user** —— 委派链上那只 ask 判不出
23
+ * 归属、没有人可问,这件事的收件人是发起这次委派的**用户**)/ `config.read_face_swapped`
24
+ * (audience **operator** —— READ 容纳面被换过档,是部署事实)。四十八码 ⇒ **五十码**。
25
+ *
26
+ * -- 🔴 单铸律:本模块**不抄**任何一句 core 的措辞 -------------------------------------------
27
+ * `mcp.injection_dropped` 上游立了 `settlement.single_mint` 契约:**宿主只供事实**(哪个会话 /
28
+ * 哪个 server 名 / 四个原因里的哪一个 / 可选哪个字段坏),**core composes** 码字、audience 行与
29
+ * 句子。第二份措辞就是第二个会漂的源。⇒ 本模块只给码册、audience 与**事实窄读器**
30
+ * ({@link readMcpInjectionDrop});转录直接用引擎给的 `message`。门里有一条反向钉:core 那四句
31
+ * 出现在本包源码或产物里 => 当场红。
32
+ */
33
+ /** 一条通告是**说给谁听**的。两值,刻意不设第三值(见模块顶注)。 */
34
+ export type NoticeAudience = 'user' | 'operator';
35
+ /**
36
+ * 引擎**成文码册**的逐字镜像(core `ENGINE_NOTICE_CODES`,**7.10.0 = 五十码**;顺序同源)。
37
+ * 🔴 这是一份**抄件**,不是本包的意见 —— 改它必须同 commit 附 core 坐标,且门会先红。
38
+ * ⚠️ `EngineNotice.code` 的型面在上游**故意留 `string`**(宿主把自己的通告转发进同一个水槽是
39
+ * 被支持的形),所以本表是**判据**不是型 —— 别拿它去窄化那个字段。
40
+
41
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
42
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
43
+ */
44
+ export declare const ENGINE_NOTICE_CODES: readonly string[];
45
+ /**
46
+ * 这个码在不在上游码册里。
47
+ * 🔴 **认原始值**:呈现层的消毒器会把一整类字符换成别的字符,拿清洗值来查表等于让
48
+ * `memory\nsession_polluted` 这种脏码冒充册内码。消毒只进文案,不进判据。
49
+ */
50
+ export declare function engineNoticeInCatalog(code: unknown): boolean;
51
+ /**
52
+ * 逐码的 audience 行(core `NOTICE_AUDIENCE_TABLE` 逐行镜像;与 {@link ENGINE_NOTICE_CODES}
53
+ * **逐键 lockstep** —— 上游用编译期 `satisfies` 钉住这一点,本包用门钉住)。
54
+ */
55
+ export declare const ENGINE_NOTICE_AUDIENCE: Readonly<Record<string, NoticeAudience>>;
56
+ /**
57
+ * 这个码是说给谁听的。**表外码一律保守判 `operator`** —— 把一个读不懂的码推给终端用户是最坏的
58
+ * 猜法(他既看不懂也无从下手),而漏给运维看一条只是让他晚一点知道。
59
+ */
60
+ export declare function noticeAudienceOf(code: unknown): NoticeAudience;
61
+ /**
62
+ * `mcp.injection_dropped` 的四个掉落原因(core `MCP_INJECTION_DROP_REASONS` 逐词镜像)。
63
+ * 消费端要自己的措辞时**键在这个词上**,绝不对 `message` 做等值匹配(那是给人读的散文)。
64
+
65
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
66
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
67
+ */
68
+ export declare const MCP_INJECTION_DROP_REASONS: readonly string[];
69
+ /** `mcp.injection_dropped` 通告 `detail` 上的事实(宿主在掉落点知道的那几件)。 */
70
+ export interface McpInjectionDropFactsView {
71
+ /** 这条通告投给哪个会话。**必填** —— user audience 的通告没有会话就无处投递。 */
72
+ sessionId: string;
73
+ /** 用户自己写的那个 server 名(引擎的 message 里逐字引了它)。 */
74
+ server: string;
75
+ /** 四个原因之一。 */
76
+ reason: string;
77
+ /** 只有 `malformed_entry` 带:宿主发现哪个键坏了,用户据此知道该改哪儿。 */
78
+ field?: string;
79
+ }
80
+ /**
81
+ * `mcp.injection_dropped` 通告 → 事实;不成形 => `undefined`,绝不抛出。
82
+ *
83
+ * 🔴 **只认自己那一个码**:一条别的通告即使 `detail` 长得像,也不给读(读器按码分家,才不会在
84
+ * 上游给另一个码加同名键的那天悄悄改判)。
85
+ * 🔴 `sessionId` / `server` / `reason` 三格缺一 => 整只缺席。core 在**铸点**对同样三格直接抛
86
+ * `TypeError`(「一条没有会话的 user 通告投不到任何地方」);本包在**读点**判缺席而不抛 ——
87
+ * 读点抛只会把一条读不懂的通告升级成一次崩。
88
+ * 🔴 `reason` **按闭集读**:它是消费端分支的键,读不懂的词不许被当成一个能拿去分支的原因。
89
+ * (要渲一句话时,用引擎给的 `message`,见模块顶注的单铸律。)
90
+ */
91
+ export declare function readMcpInjectionDrop(notice: unknown): McpInjectionDropFactsView | undefined;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * src/engineNoticeCodes.ts — `engine_notice` 的**码册**与 **audience 表**(0.63.0;L-167;core 7.9.x)。
3
+ *
4
+ * -- 「渲不渲」的判据不在这一端 --------------------------------------------------------------
5
+ * 一条引擎通告该不该占用户的注意力,正确判据**不是**「这一端的渲染表有没有那一格」(那会随各端的
6
+ * 施工进度晃),而是**上游有没有把这个码铸进它的成文码册**:
7
+ * · 码册**内**的码 = 引擎作为治理事实铸出来的,即使这一端还没有专属文案也该让人看见(通用行);
8
+ * · 码册**外**的码 = 转发进同一个水槽的宿主/适配器噪声,呈现面 fail-closed(只落 debug)。
9
+ * audience 回答另一问:这条事实**说给谁听**。一条 operator 行推给终端用户是噪音;一条 user 行只落
10
+ * operator 日志,就是把该告诉他的事瞒下了。两值而已(`user` / `operator`)—— 上游刻意不设第三值:
11
+ * 「这条通告能不能路由到某个会话」不是 audience,而是 `sessionId` 在不在场(**逐次发射**的事实)。
12
+ *
13
+ * -- 为什么是镜像而不是 import ---------------------------------------------------------------
14
+ * `@sema-agent/core` 不是本包消费者的依赖(它既不是 peer 也不是 runtime dep),而本包的 `.d.ts`
15
+ * 一旦引用它,装了本包却没装 core 的下游会当场编译不过。这与本包 `toolResult.ts` / `retryStatus.ts` /
16
+ * `skillsWireCaps.ts` 三处的处置同形:**按 wire 事实镜像,把代价交给门**——
17
+ * `run-engine-notice-catalog-test.mjs` 对**实装 devDep core** 的两份产物逐码逐行双向对账,
18
+ * core 一动这里就先红。上游自己也是这么设计的:它导出码册的理由逐字写着「a downstream table with a
19
+ * row missing … is a mechanically detectable disagreement rather than an argument」。
20
+ *
21
+ * -- 提货记账(码册加员)-----------------------------------------------------------------------
22
+ * · **core 7.10.0 +2**:`delegation.ask_unresolvable`(audience **user** —— 委派链上那只 ask 判不出
23
+ * 归属、没有人可问,这件事的收件人是发起这次委派的**用户**)/ `config.read_face_swapped`
24
+ * (audience **operator** —— READ 容纳面被换过档,是部署事实)。四十八码 ⇒ **五十码**。
25
+ *
26
+ * -- 🔴 单铸律:本模块**不抄**任何一句 core 的措辞 -------------------------------------------
27
+ * `mcp.injection_dropped` 上游立了 `settlement.single_mint` 契约:**宿主只供事实**(哪个会话 /
28
+ * 哪个 server 名 / 四个原因里的哪一个 / 可选哪个字段坏),**core composes** 码字、audience 行与
29
+ * 句子。第二份措辞就是第二个会漂的源。⇒ 本模块只给码册、audience 与**事实窄读器**
30
+ * ({@link readMcpInjectionDrop});转录直接用引擎给的 `message`。门里有一条反向钉:core 那四句
31
+ * 出现在本包源码或产物里 => 当场红。
32
+ */
33
+ /**
34
+ * 引擎**成文码册**的逐字镜像(core `ENGINE_NOTICE_CODES`,**7.10.0 = 五十码**;顺序同源)。
35
+ * 🔴 这是一份**抄件**,不是本包的意见 —— 改它必须同 commit 附 core 坐标,且门会先红。
36
+ * ⚠️ `EngineNotice.code` 的型面在上游**故意留 `string`**(宿主把自己的通告转发进同一个水槽是
37
+ * 被支持的形),所以本表是**判据**不是型 —— 别拿它去窄化那个字段。
38
+
39
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
40
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
41
+ */
42
+ export const ENGINE_NOTICE_CODES = Object.freeze([
43
+ 'config.autocompact_window_clamped',
44
+ 'config.env_timeout_discarded',
45
+ 'config.materialize_env_discarded',
46
+ 'config.models_swapped',
47
+ 'config.read_face_swapped',
48
+ 'config.read_face_deployment_clamped',
49
+ 'config.tool_model_gate_removed',
50
+ 'config.tool_model_gate_unknown_class',
51
+ 'config.tool_model_gate_env_invalid',
52
+ 'config.tool_card_undeclared',
53
+ 'config.tool_face_undeclared',
54
+ 'config.tool_face_invalid',
55
+ 'config.durable_gate_unavailable',
56
+ 'config.peer_admission_out_of_range',
57
+ 'config.peer_lane_unmounted',
58
+ 'peer.inbound_disposition',
59
+ 'peer.held_settled',
60
+ 'peer.idle_subscription',
61
+ 'classifier.denial_limit',
62
+ 'checkpoint.execution_outcome_unrecorded',
63
+ 'delegation.transcript_integrity',
64
+ 'delegation.ask_unresolvable',
65
+ 'mcp.injection_dropped',
66
+ 'mcp.revocation_probe_failed',
67
+ 'workflow.governance_key_stripped',
68
+ 'workflow.agent_option_ignored',
69
+ 'memory.session_polluted',
70
+ 'memory.harvest_quarantined',
71
+ 'memory.delegation_static_mark_waived',
72
+ 'memory.content_class_declared',
73
+ 'memory.hold_opened',
74
+ 'memory.hold_released',
75
+ 'memory.hold_disposed',
76
+ 'memory.consolidation_recommended',
77
+ 'memory.consolidation_committed',
78
+ 'memory.consolidation_conflict',
79
+ 'memory.consolidation_incomplete',
80
+ 'memory.consolidation_refused',
81
+ 'memory.consolidation_withheld',
82
+ 'route.fallback_to_primary',
83
+ 'route.base_url_changed_key_unchanged',
84
+ 'task.user_steer_undrained',
85
+ 'task.user_followup_undrained',
86
+ 'steering.parked_input_blocked',
87
+ 'task.turn_interrupted',
88
+ 'task.halt_unconsumed',
89
+ 'task.late_approval',
90
+ 'memory.capture_opted_out',
91
+ 'memory.capture_optout_unpersisted',
92
+ 'tool_result.offload_put_failed',
93
+ ]);
94
+ const CATALOG = new Set(ENGINE_NOTICE_CODES);
95
+ /**
96
+ * 这个码在不在上游码册里。
97
+ * 🔴 **认原始值**:呈现层的消毒器会把一整类字符换成别的字符,拿清洗值来查表等于让
98
+ * `memory\nsession_polluted` 这种脏码冒充册内码。消毒只进文案,不进判据。
99
+ */
100
+ export function engineNoticeInCatalog(code) {
101
+ return typeof code === 'string' && CATALOG.has(code);
102
+ }
103
+ /**
104
+ * 逐码的 audience 行(core `NOTICE_AUDIENCE_TABLE` 逐行镜像;与 {@link ENGINE_NOTICE_CODES}
105
+ * **逐键 lockstep** —— 上游用编译期 `satisfies` 钉住这一点,本包用门钉住)。
106
+ */
107
+ export const ENGINE_NOTICE_AUDIENCE = Object.freeze({
108
+ 'config.autocompact_window_clamped': 'operator',
109
+ 'config.env_timeout_discarded': 'operator',
110
+ 'config.materialize_env_discarded': 'operator',
111
+ 'config.models_swapped': 'operator',
112
+ 'config.read_face_swapped': 'operator',
113
+ 'config.read_face_deployment_clamped': 'operator',
114
+ 'config.tool_model_gate_removed': 'operator',
115
+ 'config.tool_model_gate_unknown_class': 'operator',
116
+ 'config.tool_model_gate_env_invalid': 'operator',
117
+ 'config.tool_card_undeclared': 'operator',
118
+ 'config.tool_face_undeclared': 'operator',
119
+ 'config.tool_face_invalid': 'operator',
120
+ 'config.durable_gate_unavailable': 'user',
121
+ 'config.peer_admission_out_of_range': 'operator',
122
+ 'config.peer_lane_unmounted': 'operator',
123
+ 'peer.inbound_disposition': 'user',
124
+ 'peer.held_settled': 'user',
125
+ 'peer.idle_subscription': 'user',
126
+ 'classifier.denial_limit': 'user',
127
+ 'checkpoint.execution_outcome_unrecorded': 'operator',
128
+ 'delegation.transcript_integrity': 'operator',
129
+ 'delegation.ask_unresolvable': 'user',
130
+ 'mcp.injection_dropped': 'user',
131
+ 'mcp.revocation_probe_failed': 'operator',
132
+ 'workflow.governance_key_stripped': 'operator',
133
+ 'workflow.agent_option_ignored': 'operator',
134
+ 'memory.session_polluted': 'user',
135
+ 'memory.harvest_quarantined': 'user',
136
+ 'memory.delegation_static_mark_waived': 'user',
137
+ 'memory.content_class_declared': 'operator',
138
+ 'memory.hold_opened': 'user',
139
+ 'memory.hold_released': 'user',
140
+ 'memory.hold_disposed': 'user',
141
+ 'memory.consolidation_recommended': 'operator',
142
+ 'memory.consolidation_committed': 'operator',
143
+ 'memory.consolidation_conflict': 'operator',
144
+ 'memory.consolidation_incomplete': 'operator',
145
+ 'memory.consolidation_refused': 'operator',
146
+ 'memory.consolidation_withheld': 'user',
147
+ 'route.fallback_to_primary': 'operator',
148
+ 'route.base_url_changed_key_unchanged': 'operator',
149
+ 'task.user_steer_undrained': 'user',
150
+ 'task.user_followup_undrained': 'user',
151
+ 'steering.parked_input_blocked': 'user',
152
+ 'task.turn_interrupted': 'user',
153
+ 'task.halt_unconsumed': 'user',
154
+ 'task.late_approval': 'user',
155
+ 'memory.capture_opted_out': 'user',
156
+ 'memory.capture_optout_unpersisted': 'user',
157
+ 'tool_result.offload_put_failed': 'operator',
158
+ });
159
+ /**
160
+ * 这个码是说给谁听的。**表外码一律保守判 `operator`** —— 把一个读不懂的码推给终端用户是最坏的
161
+ * 猜法(他既看不懂也无从下手),而漏给运维看一条只是让他晚一点知道。
162
+ */
163
+ export function noticeAudienceOf(code) {
164
+ // 🔴 **自有属性判据**(异源对抗复审 [medium]):`Object.freeze` 冻的是自有属性,原型链原样还在 ⇒
165
+ // 一个来自 wire 的 `constructor` / `toString` / `__proto__` 会命中 `Object.prototype` 上的成员,
166
+ // 本口于是交出一个**函数** —— 它既不是 `user` 也不是 `operator`,按两值分发的宿主两条分支都进不去
167
+ // (保守缺省也跟着被绕过)。这是本仓已定谳的病形:wire 键控的表一律按自有属性查。
168
+ const row = typeof code === 'string' && Object.hasOwn(ENGINE_NOTICE_AUDIENCE, code)
169
+ ? ENGINE_NOTICE_AUDIENCE[code]
170
+ : undefined;
171
+ return row ?? 'operator';
172
+ }
173
+ /**
174
+ * `mcp.injection_dropped` 的四个掉落原因(core `MCP_INJECTION_DROP_REASONS` 逐词镜像)。
175
+ * 消费端要自己的措辞时**键在这个词上**,绝不对 `message` 做等值匹配(那是给人读的散文)。
176
+
177
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
178
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
179
+ */
180
+ export const MCP_INJECTION_DROP_REASONS = Object.freeze([
181
+ 'malformed_entry',
182
+ 'name_reserved_by_deployment',
183
+ 'gate_closed',
184
+ 'over_cap',
185
+ ]);
186
+ const DROP_REASONS = new Set(MCP_INJECTION_DROP_REASONS);
187
+ /**
188
+ * `mcp.injection_dropped` 通告 → 事实;不成形 => `undefined`,绝不抛出。
189
+ *
190
+ * 🔴 **只认自己那一个码**:一条别的通告即使 `detail` 长得像,也不给读(读器按码分家,才不会在
191
+ * 上游给另一个码加同名键的那天悄悄改判)。
192
+ * 🔴 `sessionId` / `server` / `reason` 三格缺一 => 整只缺席。core 在**铸点**对同样三格直接抛
193
+ * `TypeError`(「一条没有会话的 user 通告投不到任何地方」);本包在**读点**判缺席而不抛 ——
194
+ * 读点抛只会把一条读不懂的通告升级成一次崩。
195
+ * 🔴 `reason` **按闭集读**:它是消费端分支的键,读不懂的词不许被当成一个能拿去分支的原因。
196
+ * (要渲一句话时,用引擎给的 `message`,见模块顶注的单铸律。)
197
+ */
198
+ export function readMcpInjectionDrop(notice) {
199
+ if (typeof notice !== 'object' || notice === null || Array.isArray(notice))
200
+ return undefined;
201
+ const n = notice;
202
+ if (n.code !== 'mcp.injection_dropped')
203
+ return undefined;
204
+ const d = n.detail;
205
+ if (typeof d !== 'object' || d === null || Array.isArray(d))
206
+ return undefined;
207
+ const o = d;
208
+ const sessionId = typeof o.sessionId === 'string' && o.sessionId.length > 0 ? o.sessionId : undefined;
209
+ const server = typeof o.server === 'string' && o.server.length > 0 ? o.server : undefined;
210
+ const reason = typeof o.reason === 'string' && DROP_REASONS.has(o.reason) ? o.reason : undefined;
211
+ if (sessionId === undefined || server === undefined || reason === undefined)
212
+ return undefined;
213
+ const field = typeof o.field === 'string' && o.field.length > 0 ? o.field : undefined;
214
+ return { sessionId, server, reason, ...(field !== undefined ? { field } : {}) };
215
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * src/gateVocabulary.ts — 门词汇两张表的**唯一措辞铸点**:`DeniedBy`(谁拒的,九词)与
3
+ * `AskOrigin`(谁问的,十一词)(0.63.0;sdk 8.8.0 / core 7.9.0 / L-162 ③)。
4
+ *
5
+ * ── 为什么措辞归包 ─────────────────────────────────────────────────────────────────────────
6
+ * 这两个词到用户面前时都要变成一句人话。让三端各写一份,同一次拒在 TUI / web / desktop 上会
7
+ * 是三句不同的话,而其中至少两句迟早会落后于上游加词。表与句子收在这里,加词只有一处要改,
8
+ * 而门会在上游加词的当天先红。
9
+ *
10
+ * ── 🔴 两张表的**开闭各按其出处**,兜底句因此必须分家 ──────────────────────────────────────
11
+ * 这不是风格问题,是两条不同的事实:
12
+ * · **`DeniedBy` 在这条 wire 上是真闭集**。引擎的 `screenGateOutcome` 把「`deniedBy` 出闭集」
13
+ * 列为记录**缺陷**,而 server 对有缺陷的门记录是**整条不上帧**(report + withhold)⇒ 一个
14
+ * 词表外的 `deniedBy` **结构上到不了消费端**,它到达的形式是 `gate` **整键缺席**。所以本包
15
+ * 真读到一个表外词时,唯一诚实的话是「这条门记录本不该长这样」——那是记录缺陷的信号,
16
+ * 不是「有个新词」。sdk 的型面因此也没有 `(string & {})` 逃生口。
17
+ * · **`AskOrigin` 是真开集**。`tool_approval` 帧上的 `origin` 在 server 侧**只判非空串、不判
18
+ * 成员** ⇒ core 加一个新词的当天,一个**合法**的帧就会带着表外值到达消费端。所以表外词的
19
+ * 那一句必须说「这个词比这一端新」,把它渲成坏记录会让用户去查一个根本不存在的故障。
20
+ * ⇒ 两句兜底逐字不同,门里有一条钉守着它们不被合并成一句。
21
+ *
22
+ * ── 词表是**抄件**不是意见 ────────────────────────────────────────────────────────────────
23
+ * 两张表逐词逐序抄自 sdk 的联合声明,门 `run-gate-vocabulary-test.mjs` 从实装 `.d.ts` 里解出成员
24
+ * 与本表**双向等值**对账(缺一词红、多一词红、顺序不同也红)。改它必须同 commit 附上游坐标。
25
+ */
26
+ import type { DeniedBy } from '@sema-agent/sdk';
27
+ /**
28
+ * **谁拒的** —— 判决是这次 deny 的那一**层**(sdk `DeniedBy` / core `DENIED_BY_VALUES`,
29
+ * 7.6.0 起八词、**7.9.0 起九词**)。顺序逐字同源。
30
+ * 🔴 真闭集(见模块顶注):表外词到不了消费端,所以这张表**没有**逃生口。
31
+
32
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
33
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
34
+ */
35
+ export declare const GATE_DENIED_BY_WORDS: readonly DeniedBy[];
36
+ /**
37
+ * 一个 `deniedBy` 词 → 一句人话。**唯一铸点**(三端共用;端零自拼)。
38
+ *
39
+ * 🔴 表外词的那一句说的是「**这条门记录本不该长这样**」,不是「有个新词」——理由见模块顶注
40
+ * (出集的记录在 server 侧整条不上帧,所以真读到一个表外词意味着记录有缺陷)。原样带上那个
41
+ * 词:运维要拿它去问上游。
42
+ * 🔴 非串 / 空串同样走兜底 —— 一个读不出的层名不是「没有层」(deny 臂在类型上就必须点名一层)。
43
+ * 🔴 **查表用 `Object.hasOwn`,不用 `in` / 裸下标**(异源对抗复审 [medium]):`Object.freeze` 冻的是
44
+ * **自有属性**,原型链原样还在 ⇒ 一个来自 wire 的 `constructor` / `toString` / `__proto__` 会命中
45
+ * `Object.prototype` 上的成员,措辞口于是返回一个**函数**而不是一句话:呈现调用当场坏掉,而且
46
+ * 它绕过了上下两条精心分家的兜底。这是本仓已定谳的病形(wire 键控的表一律用 Map 或自有属性判据)。
47
+ */
48
+ export declare function gateDeniedByDetail(deniedBy: unknown): string;
49
+ /**
50
+ * 一只 ask 的**出身** —— 谁问的(sdk `AskOrigin` / core `ASK_ORIGINS`;7.5.x 及以前八词、
51
+ * 7.6.0 十词、**7.9.0 起十一词**)。顺序逐字同源。
52
+ * 🔴 真开集(见模块顶注):server 只判非空串,新词会带着合法的帧到达。
53
+
54
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
55
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
56
+ */
57
+ export declare const ASK_ORIGIN_WORDS: readonly string[];
58
+ /**
59
+ * 一个 `origin` 词 → 一句人话。**唯一铸点**(三端共用;端零自拼)。
60
+ *
61
+ * 🔴 表外词的那一句说的是「**这个词比这一端新**」,不是「坏记录」——理由见模块顶注(server 只判
62
+ * 非空串,core 加词当天合法的帧就带着它到达)。原样带上那个词,并明说这次仍然是在问人:
63
+ * 读不懂出身**不改变**这只 ask 要人回答这件事。
64
+ * 🔴 非串 / 空串:同走兜底但词位渲 `(none)` —— 「没报出身」与「报了一个读不懂的出身」在这一句里
65
+ * 不必分家(两者对用户的下一步相同:照常回答这只 ask),但都**不许**被折成十一词里的任何一个。
66
+ */
67
+ export declare function askOriginDetail(origin: unknown): string;