@sema-agent/client-core 0.75.1 → 0.76.1

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +11 -1
  3. package/dist/adapt/arms.js +137 -35
  4. package/dist/adapt/ids.d.ts +29 -2
  5. package/dist/adapt/ids.js +29 -2
  6. package/dist/adapt/panelTasks.d.ts +4 -3
  7. package/dist/adapt/panelTasks.js +29 -11
  8. package/dist/adapt/textStream.js +5 -5
  9. package/dist/adapt/toolCards.js +2 -2
  10. package/dist/adapt/turnFlags.js +5 -5
  11. package/dist/adapt.d.ts +1 -1
  12. package/dist/adapt.js +8 -8
  13. package/dist/adapter/downstream/eventToSdkMessage.d.ts +57 -0
  14. package/dist/adapter/downstream/eventToSdkMessage.js +125 -17
  15. package/dist/adapter/runStream.js +14 -5
  16. package/dist/adapter/types.d.ts +12 -0
  17. package/dist/adapter/types.js +30 -0
  18. package/dist/agentsWireCaps.d.ts +15 -2
  19. package/dist/approvalsStreamLiveCapability.js +15 -35
  20. package/dist/deviceExecutorManagementCapability.js +15 -35
  21. package/dist/engineAgentPanelStore.js +13 -1
  22. package/dist/engineCapReader.d.ts +77 -0
  23. package/dist/engineCapReader.js +87 -0
  24. package/dist/engineErrorCodes.d.ts +4 -0
  25. package/dist/engineErrorCodes.js +14 -0
  26. package/dist/executionLaneCapability.js +15 -38
  27. package/dist/fleet/fleetRowAgentType.js +1 -1
  28. package/dist/hitl/approvalOutcomeNote.d.ts +0 -10
  29. package/dist/hitl/approvalOutcomeNote.js +35 -8
  30. package/dist/hitl/approvalResolution.d.ts +148 -0
  31. package/dist/hitl/approvalResolution.js +199 -0
  32. package/dist/hitl/approvalsFeed.d.ts +100 -2
  33. package/dist/hitl/approvalsFeed.js +234 -18
  34. package/dist/hitl/livePendingAsk.d.ts +26 -6
  35. package/dist/hitl/livePendingAsk.js +52 -12
  36. package/dist/hitl/persistedRulesWire.d.ts +20 -0
  37. package/dist/hitl/persistedRulesWire.js +23 -0
  38. package/dist/index.d.ts +8 -0
  39. package/dist/index.js +25 -1
  40. package/dist/mcpLiveness.d.ts +179 -0
  41. package/dist/mcpLiveness.js +218 -0
  42. package/dist/mcpPanel.d.ts +17 -0
  43. package/dist/mcpPanel.js +21 -0
  44. package/dist/mcpProbeCapability.d.ts +84 -0
  45. package/dist/mcpProbeCapability.js +124 -0
  46. package/dist/memoryComplianceCapability.d.ts +73 -0
  47. package/dist/memoryComplianceCapability.js +109 -0
  48. package/dist/memoryEntriesWire.d.ts +302 -0
  49. package/dist/memoryEntriesWire.js +595 -0
  50. package/dist/memoryOriginCapability.d.ts +68 -0
  51. package/dist/memoryOriginCapability.js +102 -0
  52. package/dist/memorySpecWire.d.ts +175 -0
  53. package/dist/memorySpecWire.js +320 -0
  54. package/dist/peerLaneCapability.d.ts +62 -0
  55. package/dist/peerLaneCapability.js +100 -0
  56. package/dist/permissionRulesWriteCapability.d.ts +64 -0
  57. package/dist/permissionRulesWriteCapability.js +98 -0
  58. package/dist/seam.d.ts +53 -5
  59. package/dist/seam.js +10 -1
  60. package/dist/selfOrchestrationDenial.js +7 -2
  61. package/dist/sqlEngineCapability.js +15 -35
  62. package/dist/webSearchBackendCapability.js +15 -35
  63. package/dist/writeProtectionCapability.js +15 -35
  64. package/docs/INTEGRATION-CLIENTS.md +148 -15
  65. package/package.json +1 -1
@@ -0,0 +1,62 @@
1
+ /**
2
+ * peerLaneCapability — `GET /v1/capabilities.peerLane`(server ≥7.91.1;sdk 9.8.1 尚未声明该键 ⇒ 结构读)
3
+ * 的三端共用读面(0.76.1 CC-102;与 sql / writeProtection / webSearch.backend / executionLane /
4
+ * approvalsStreamLive / deviceExecutor.management 各只兄弟同一套四态词汇)。
5
+ *
6
+ * ── 这一位答的是哪一个问题 ────────────────────────────────────────────────────────────────
7
+ * 「本部署的会话之间能不能互相发现并投递」——引擎的跨会话目录席是否已挂(会话枚举面 ∧ 耐久
8
+ * mailbox ∧ 该 mailbox 的收件人生命周期面,三缺一即缺席)。位为真时模型侧多出 `ListAgents`
9
+ * (列同一 principal 的其它会话)、`SendMessage` 的 `session.<id>` 地址臂,以及每 turn 边界对本
10
+ * 会话那只会话盒的排水。
11
+ *
12
+ * ── 🔴 本位不是 HTTP 面 ────────────────────────────────────────────────────────────────────
13
+ * 上游没有为这一族能力单开任何新端点 —— 会话清单仍走既有的会话列举口。所以本位的消费方式是
14
+ * **渲染 / 预期**(要不要露出跨会话的入口),不是拿它去 gate 某一个动词的调用。
15
+ * ⚠️ 辖域只到「席」:上游另有一道装配期挂载判据(mailbox 必须显式声明跨进程安全),不齐时会在
16
+ * 装配期响亮拒挂并通告运维;本读面刻意不复判那一道 —— 复判 = 给同一件事立第二个判官。
17
+ *
18
+ * ── 🔴 四态,不是两态([honest-absence-not-fabricated-zero])──────────────────────────────────
19
+ * ① `unobserved` —— 本进程一次 caps 响应都没观测到。
20
+ * ② `not_reported` —— caps 观测到了,而回体上**没有** `peerLane` 这个键 ⇒ 老 worker(比 7.91.1 老)。
21
+ * 这台部署有没有跨会话车道**判不出**,不是「没有」。
22
+ * ③ `absent` —— 键在、值**明确是** `false`:这台部署没有挂跨会话目录席。**正面事实**,
23
+ * 与 `not_reported`(判不出)是两件不同的事,处置也相反。
24
+ * ④ `present` —— 键在、值为 `true`:席已挂。
25
+ * 值既不是 `true` 也不是 `false`(串 / 数 / null / 数组 / 对象)⇒ 畸形 ⇒ 删格(⇒ `unobserved`)。
26
+ * 🔴 三种「没有」(`unobserved` / `not_reported` / `absent`)**不许互折** —— 折起来会把「判不出」
27
+ * 说成「引擎明说没有」,或把「一次都没问过」说成「问过、答案是没有」。
28
+ */
29
+ /** 四态读数。`unobserved` 由读口在这一格空缺时铸,不由投影铸。 */
30
+ export type PeerLaneReading = {
31
+ kind: 'unobserved';
32
+ } | {
33
+ kind: 'not_reported';
34
+ } | {
35
+ kind: 'absent';
36
+ } | {
37
+ kind: 'present';
38
+ };
39
+ /** caps 回体 → 本格读数;畸形一律 `undefined`(= 这一格不写 ⇒ 读口答 `unobserved`)。 */
40
+ export declare function projectPeerLaneCapability(caps: unknown): PeerLaneReading | undefined;
41
+ /** 宿主 caps probe 的读面 tee 落点。绝不 throw;畸形 ⇒ 删格;`opts.generation` 关掉旧探测覆盖新读数的竞态。 */
42
+ export declare function noteEngineCapsForPeerLane(baseUrl: string, caps: unknown, opts?: {
43
+ generation?: number;
44
+ }): void;
45
+ /** 本进程观测到的读数;这一格空缺 ⇒ `{kind:'unobserved'}`。 */
46
+ export declare function observedPeerLane(baseUrl?: string | undefined): PeerLaneReading;
47
+ /**
48
+ * 「要不要在壳上露出跨会话入口(`ListAgents` / `SendMessage` 的 `session.<id>` 地址臂)」的判据
49
+ * 单源(三态):只有引擎明说挂了(`present`)才 `'yes'`;明说没挂(`absent`)⇒ `'no'`;
50
+ * **键缺席(`not_reported`)也是 `'no'`** —— 这一位的在场性本身就是版本信号,上游对缺席的成文语义是
51
+ * 「老 worker:本部署没有跨会话车道,按会话 id 投递按查无此会话拒」,所以缺席**不是**判不出,是一句
52
+ * 有内容的否定(与写键那一位「缺席 ⇒ 藏入口」同一条读法);只有本进程**从没观测过**(`unobserved`)
53
+ * 才是真的判不出 ⇒ `'unknown'`(别预判成有也别预判成没有)。
54
+ */
55
+ export declare function peerLaneAvailable(reading: PeerLaneReading): 'yes' | 'no' | 'unknown';
56
+ /** doctor 那一行的 detail 串,唯一措辞真源。🔴 `unobserved` 那句不许暗示「有」或「没有」;`not_reported` 那句按上游成文语义说
57
+ * 「老 worker,没有跨会话车道」,并把「为什么缺席就是没有」(在场性即版本信号)说清,不让端把它读成第二种「判不出」。 */
58
+ export declare function peerLaneDoctorDetail(reading: PeerLaneReading): string;
59
+ /** 换代失效口(引擎温切成功后调):清成未观测。空串 ⇒ no-op;绝不 throw。 */
60
+ export declare function forgetPeerLaneReading(baseUrl: string | undefined): void;
61
+ /** 测试钩子。 */
62
+ export declare function __resetPeerLaneReadingsForTests(): void;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * peerLaneCapability — `GET /v1/capabilities.peerLane`(server ≥7.91.1;sdk 9.8.1 尚未声明该键 ⇒ 结构读)
3
+ * 的三端共用读面(0.76.1 CC-102;与 sql / writeProtection / webSearch.backend / executionLane /
4
+ * approvalsStreamLive / deviceExecutor.management 各只兄弟同一套四态词汇)。
5
+ *
6
+ * ── 这一位答的是哪一个问题 ────────────────────────────────────────────────────────────────
7
+ * 「本部署的会话之间能不能互相发现并投递」——引擎的跨会话目录席是否已挂(会话枚举面 ∧ 耐久
8
+ * mailbox ∧ 该 mailbox 的收件人生命周期面,三缺一即缺席)。位为真时模型侧多出 `ListAgents`
9
+ * (列同一 principal 的其它会话)、`SendMessage` 的 `session.<id>` 地址臂,以及每 turn 边界对本
10
+ * 会话那只会话盒的排水。
11
+ *
12
+ * ── 🔴 本位不是 HTTP 面 ────────────────────────────────────────────────────────────────────
13
+ * 上游没有为这一族能力单开任何新端点 —— 会话清单仍走既有的会话列举口。所以本位的消费方式是
14
+ * **渲染 / 预期**(要不要露出跨会话的入口),不是拿它去 gate 某一个动词的调用。
15
+ * ⚠️ 辖域只到「席」:上游另有一道装配期挂载判据(mailbox 必须显式声明跨进程安全),不齐时会在
16
+ * 装配期响亮拒挂并通告运维;本读面刻意不复判那一道 —— 复判 = 给同一件事立第二个判官。
17
+ *
18
+ * ── 🔴 四态,不是两态([honest-absence-not-fabricated-zero])──────────────────────────────────
19
+ * ① `unobserved` —— 本进程一次 caps 响应都没观测到。
20
+ * ② `not_reported` —— caps 观测到了,而回体上**没有** `peerLane` 这个键 ⇒ 老 worker(比 7.91.1 老)。
21
+ * 这台部署有没有跨会话车道**判不出**,不是「没有」。
22
+ * ③ `absent` —— 键在、值**明确是** `false`:这台部署没有挂跨会话目录席。**正面事实**,
23
+ * 与 `not_reported`(判不出)是两件不同的事,处置也相反。
24
+ * ④ `present` —— 键在、值为 `true`:席已挂。
25
+ * 值既不是 `true` 也不是 `false`(串 / 数 / null / 数组 / 对象)⇒ 畸形 ⇒ 删格(⇒ `unobserved`)。
26
+ * 🔴 三种「没有」(`unobserved` / `not_reported` / `absent`)**不许互折** —— 折起来会把「判不出」
27
+ * 说成「引擎明说没有」,或把「一次都没问过」说成「问过、答案是没有」。
28
+ */
29
+ import { engineWireTarget } from './engineWireTarget.js';
30
+ import { createEngineCapReader } from './engineCapReader.js';
31
+ /** caps 回体 → 本格读数;畸形一律 `undefined`(= 这一格不写 ⇒ 读口答 `unobserved`)。 */
32
+ export function projectPeerLaneCapability(caps) {
33
+ if (caps === null || typeof caps !== 'object')
34
+ return undefined;
35
+ if (!('peerLane' in caps))
36
+ return { kind: 'not_reported' };
37
+ // 🔴 只读一次:在场判据与取值同一次读(变化的 getter 不会让两处判断看到两个不同的值)。
38
+ const v = caps.peerLane;
39
+ if (v === undefined)
40
+ return { kind: 'not_reported' };
41
+ if (typeof v !== 'boolean')
42
+ return undefined;
43
+ return v ? { kind: 'present' } : { kind: 'absent' };
44
+ }
45
+ /**
46
+ * 本格的 per-baseUrl 读账 + tee / 读口 / 失效口 / 测试钩四口 —— 能力位读器**共用同一份实现**
47
+ * ({@link createEngineCapReader});本文件只留这一格真正不同的部分:投影函数
48
+ * {@link projectPeerLaneCapability}、doctor 措辞表,以及本位自己的判据助手。
49
+ * 🔴 Map 在工厂闭包里,**一只读器一张**:收到读不懂的回体只删自己这一格,绝不连坐别的能力面。
50
+ */
51
+ const reader = createEngineCapReader({
52
+ name: 'peerLane',
53
+ project: projectPeerLaneCapability,
54
+ makeUnobserved: () => ({ kind: 'unobserved' }),
55
+ });
56
+ /** 宿主 caps probe 的读面 tee 落点。绝不 throw;畸形 ⇒ 删格;`opts.generation` 关掉旧探测覆盖新读数的竞态。 */
57
+ export function noteEngineCapsForPeerLane(baseUrl, caps, opts) {
58
+ reader.note(baseUrl, caps, opts);
59
+ }
60
+ /** 本进程观测到的读数;这一格空缺 ⇒ `{kind:'unobserved'}`。 */
61
+ export function observedPeerLane(baseUrl = engineWireTarget()?.baseUrl) {
62
+ return reader.observed(baseUrl);
63
+ }
64
+ /**
65
+ * 「要不要在壳上露出跨会话入口(`ListAgents` / `SendMessage` 的 `session.<id>` 地址臂)」的判据
66
+ * 单源(三态):只有引擎明说挂了(`present`)才 `'yes'`;明说没挂(`absent`)⇒ `'no'`;
67
+ * **键缺席(`not_reported`)也是 `'no'`** —— 这一位的在场性本身就是版本信号,上游对缺席的成文语义是
68
+ * 「老 worker:本部署没有跨会话车道,按会话 id 投递按查无此会话拒」,所以缺席**不是**判不出,是一句
69
+ * 有内容的否定(与写键那一位「缺席 ⇒ 藏入口」同一条读法);只有本进程**从没观测过**(`unobserved`)
70
+ * 才是真的判不出 ⇒ `'unknown'`(别预判成有也别预判成没有)。
71
+ */
72
+ export function peerLaneAvailable(reading) {
73
+ if (reading.kind === 'present')
74
+ return 'yes';
75
+ if (reading.kind === 'absent' || reading.kind === 'not_reported')
76
+ return 'no';
77
+ return 'unknown';
78
+ }
79
+ /** doctor 那一行的 detail 串,唯一措辞真源。🔴 `unobserved` 那句不许暗示「有」或「没有」;`not_reported` 那句按上游成文语义说
80
+ * 「老 worker,没有跨会话车道」,并把「为什么缺席就是没有」(在场性即版本信号)说清,不让端把它读成第二种「判不出」。 */
81
+ export function peerLaneDoctorDetail(reading) {
82
+ switch (reading.kind) {
83
+ case 'unobserved':
84
+ return 'cross-session lane not observed — the engine reports it on /v1/capabilities (peerLane); this process has no usable capabilities reading cached for it (none received, or the last one was unreadable)';
85
+ case 'not_reported':
86
+ return 'cross-session lane not reported by this engine — the engine predates the lane, so this deployment has no cross-session discovery or delivery (the key itself is the version signal; addressing another session by id is refused as unknown)';
87
+ case 'absent':
88
+ return 'no cross-session lane on this deployment — cross-session discovery and delivery (listing other sessions, addressing one by id) are unavailable';
89
+ case 'present':
90
+ return 'cross-session lane on — this engine can discover and deliver across sessions for the same caller';
91
+ }
92
+ }
93
+ /** 换代失效口(引擎温切成功后调):清成未观测。空串 ⇒ no-op;绝不 throw。 */
94
+ export function forgetPeerLaneReading(baseUrl) {
95
+ reader.forget(baseUrl);
96
+ }
97
+ /** 测试钩子。 */
98
+ export function __resetPeerLaneReadingsForTests() {
99
+ reader.__resetForTests();
100
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * permissionRulesWriteCapability — `GET /v1/capabilities.permissionRulesWrite`(server ≥7.91.1;sdk 9.8.1
3
+ * 尚未声明该键 ⇒ 结构读)的三端共用读面(0.76.1 CC-102;与 sql / writeProtection / webSearch.backend /
4
+ * executionLane / approvalsStreamLive / deviceExecutor.management / peerLane 各只兄弟同一套四态词汇)。
5
+ *
6
+ * ── 这一位答的是哪一个问题 ────────────────────────────────────────────────────────────────
7
+ * 「收紧方向的单步规则写入口(把某条工具规则改成 deny/ask 的那一步)对**这一次调用方**够不够得着」。
8
+ * 这一位与「规则车道整体在不在」是上游同一个内部表达式的两次读——车道在场 ∧ 这次调用带得到可验明
9
+ * 的身份——所以两者永远同真同假;但它们**不是同一个位**,原因见下。
10
+ *
11
+ * ── 🔴 读法(两处上游源码逐字一致,别按撤销面那一位类推)─────────────────────────────────────
12
+ * ① **键缺席 ⇒ 这份二进制比单步写口本身还老**:更早的引擎上,规则车道的其它几口已经在正常
13
+ * 应答(甚至已经报真),但这一条写口压根没有路由——键缺席就是这件事的信号,消费端据此**藏起**
14
+ * 这个写入口(真打了也能认:那类拒绝是裸的「方法不许」形,但不必打了才知道)。
15
+ * ② **键在场 ⇒ 按这一位自己的值判「这次够不够得着」**:值为 `true`/`false` 直接就是答案,不需要
16
+ * 再去核对车道整体那一位。
17
+ * ③ ⚠️ **与撤销面那一只存在性信号刻意不同源**:那一位是**部署级**的、与调用方身份无关的布尔(它的
18
+ * 值本身要当版本探针用);本位不是——本位的值同时编码了「车道在不在」与「这次调用带没带得到
19
+ * 身份」两件事,上游明确说这两位的读法不能互相套用。
20
+ * ④ 位为真**不**保证任意一次写都成功:方向 / 规则文本 / scope 的合法性由每次调用自己的应答逐次
21
+ * 回答,这一位只回答「入口在不在、这次摸不摸得到」。
22
+ *
23
+ * ── 🔴 四态,不是两态([honest-absence-not-fabricated-zero])──────────────────────────────────
24
+ * ① `unobserved` —— 本进程一次 caps 响应都没观测到。
25
+ * ② `not_reported` —— caps 观测到了,而回体上**没有**这个键 ⇒ 引擎比这条写口老(见①),藏入口。
26
+ * ③ `absent` —— 键在、值**明确是** `false`:这次调用够不着这条写口(车道没开,或这次调用
27
+ * 带不到可验明的身份)。**正面事实**,与 `not_reported`(判不出)是两件不同的事。
28
+ * ④ `present` —— 键在、值为 `true`:这次调用摸得到这条写口。
29
+ * 值既不是 `true` 也不是 `false` ⇒ 畸形 ⇒ 删格(⇒ `unobserved`)。
30
+ * 🔴 三种「没有」(`unobserved` / `not_reported` / `absent`)**不许互折**。
31
+ */
32
+ /** 四态读数。`unobserved` 由读口在这一格空缺时铸,不由投影铸。 */
33
+ export type PermissionRulesWriteReading = {
34
+ kind: 'unobserved';
35
+ } | {
36
+ kind: 'not_reported';
37
+ } | {
38
+ kind: 'absent';
39
+ } | {
40
+ kind: 'present';
41
+ };
42
+ /** caps 回体 → 本格读数;畸形一律 `undefined`(= 这一格不写 ⇒ 读口答 `unobserved`)。 */
43
+ export declare function projectPermissionRulesWriteCapability(caps: unknown): PermissionRulesWriteReading | undefined;
44
+ /** 宿主 caps probe 的读面 tee 落点。绝不 throw;畸形 ⇒ 删格;`opts.generation` 关掉旧探测覆盖新读数的竞态。 */
45
+ export declare function noteEngineCapsForPermissionRulesWrite(baseUrl: string, caps: unknown, opts?: {
46
+ generation?: number;
47
+ }): void;
48
+ /** 本进程观测到的读数;这一格空缺 ⇒ `{kind:'unobserved'}`。 */
49
+ export declare function observedPermissionRulesWrite(baseUrl?: string | undefined): PermissionRulesWriteReading;
50
+ /**
51
+ * 「这次调用摸不摸得到单步写入口」的判据单源(布尔):只有 `present` 才 `true`;其余三态
52
+ * (`absent` / `not_reported` / `unobserved`)一律 `false`——藏入口的默认方向是**保守**的,
53
+ * 判不出与明确没有对「要不要露出这个写按钮」是同一个答案。
54
+ *
55
+ * 🔴 本函数是本位**唯一**的派生点:{@link "./hitl/persistedRulesWire.js".persistedRulesWriteAvailable}
56
+ * 只调用它,不再合取别的位、也不再另读一遍 caps——同一件事只许有一个判官。
57
+ */
58
+ export declare function permissionRulesWriteAvailable(reading: PermissionRulesWriteReading): boolean;
59
+ /** doctor 那一行的 detail 串,唯一措辞真源。🔴 两种「读不出」都不许暗示「够得着」或「够不着」。 */
60
+ export declare function permissionRulesWriteDoctorDetail(reading: PermissionRulesWriteReading): string;
61
+ /** 换代失效口(引擎温切成功后调):清成未观测。空串 ⇒ no-op;绝不 throw。 */
62
+ export declare function forgetPermissionRulesWriteReading(baseUrl: string | undefined): void;
63
+ /** 测试钩子。 */
64
+ export declare function __resetPermissionRulesWriteReadingsForTests(): void;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * permissionRulesWriteCapability — `GET /v1/capabilities.permissionRulesWrite`(server ≥7.91.1;sdk 9.8.1
3
+ * 尚未声明该键 ⇒ 结构读)的三端共用读面(0.76.1 CC-102;与 sql / writeProtection / webSearch.backend /
4
+ * executionLane / approvalsStreamLive / deviceExecutor.management / peerLane 各只兄弟同一套四态词汇)。
5
+ *
6
+ * ── 这一位答的是哪一个问题 ────────────────────────────────────────────────────────────────
7
+ * 「收紧方向的单步规则写入口(把某条工具规则改成 deny/ask 的那一步)对**这一次调用方**够不够得着」。
8
+ * 这一位与「规则车道整体在不在」是上游同一个内部表达式的两次读——车道在场 ∧ 这次调用带得到可验明
9
+ * 的身份——所以两者永远同真同假;但它们**不是同一个位**,原因见下。
10
+ *
11
+ * ── 🔴 读法(两处上游源码逐字一致,别按撤销面那一位类推)─────────────────────────────────────
12
+ * ① **键缺席 ⇒ 这份二进制比单步写口本身还老**:更早的引擎上,规则车道的其它几口已经在正常
13
+ * 应答(甚至已经报真),但这一条写口压根没有路由——键缺席就是这件事的信号,消费端据此**藏起**
14
+ * 这个写入口(真打了也能认:那类拒绝是裸的「方法不许」形,但不必打了才知道)。
15
+ * ② **键在场 ⇒ 按这一位自己的值判「这次够不够得着」**:值为 `true`/`false` 直接就是答案,不需要
16
+ * 再去核对车道整体那一位。
17
+ * ③ ⚠️ **与撤销面那一只存在性信号刻意不同源**:那一位是**部署级**的、与调用方身份无关的布尔(它的
18
+ * 值本身要当版本探针用);本位不是——本位的值同时编码了「车道在不在」与「这次调用带没带得到
19
+ * 身份」两件事,上游明确说这两位的读法不能互相套用。
20
+ * ④ 位为真**不**保证任意一次写都成功:方向 / 规则文本 / scope 的合法性由每次调用自己的应答逐次
21
+ * 回答,这一位只回答「入口在不在、这次摸不摸得到」。
22
+ *
23
+ * ── 🔴 四态,不是两态([honest-absence-not-fabricated-zero])──────────────────────────────────
24
+ * ① `unobserved` —— 本进程一次 caps 响应都没观测到。
25
+ * ② `not_reported` —— caps 观测到了,而回体上**没有**这个键 ⇒ 引擎比这条写口老(见①),藏入口。
26
+ * ③ `absent` —— 键在、值**明确是** `false`:这次调用够不着这条写口(车道没开,或这次调用
27
+ * 带不到可验明的身份)。**正面事实**,与 `not_reported`(判不出)是两件不同的事。
28
+ * ④ `present` —— 键在、值为 `true`:这次调用摸得到这条写口。
29
+ * 值既不是 `true` 也不是 `false` ⇒ 畸形 ⇒ 删格(⇒ `unobserved`)。
30
+ * 🔴 三种「没有」(`unobserved` / `not_reported` / `absent`)**不许互折**。
31
+ */
32
+ import { engineWireTarget } from './engineWireTarget.js';
33
+ import { createEngineCapReader } from './engineCapReader.js';
34
+ /** caps 回体 → 本格读数;畸形一律 `undefined`(= 这一格不写 ⇒ 读口答 `unobserved`)。 */
35
+ export function projectPermissionRulesWriteCapability(caps) {
36
+ if (caps === null || typeof caps !== 'object')
37
+ return undefined;
38
+ if (!('permissionRulesWrite' in caps))
39
+ return { kind: 'not_reported' };
40
+ // 🔴 只读一次:在场判据与取值同一次读。
41
+ const v = caps.permissionRulesWrite;
42
+ if (v === undefined)
43
+ return { kind: 'not_reported' };
44
+ if (typeof v !== 'boolean')
45
+ return undefined;
46
+ return v ? { kind: 'present' } : { kind: 'absent' };
47
+ }
48
+ /**
49
+ * 本格的 per-baseUrl 读账 + tee / 读口 / 失效口 / 测试钩四口 —— 能力位读器**共用同一份实现**
50
+ * ({@link createEngineCapReader});本文件只留这一格真正不同的部分:投影函数
51
+ * {@link projectPermissionRulesWriteCapability}、doctor 措辞表,以及本位自己的判据助手。
52
+ * 🔴 Map 在工厂闭包里,**一只读器一张**:收到读不懂的回体只删自己这一格,绝不连坐别的能力面。
53
+ */
54
+ const reader = createEngineCapReader({
55
+ name: 'permissionRulesWrite',
56
+ project: projectPermissionRulesWriteCapability,
57
+ makeUnobserved: () => ({ kind: 'unobserved' }),
58
+ });
59
+ /** 宿主 caps probe 的读面 tee 落点。绝不 throw;畸形 ⇒ 删格;`opts.generation` 关掉旧探测覆盖新读数的竞态。 */
60
+ export function noteEngineCapsForPermissionRulesWrite(baseUrl, caps, opts) {
61
+ reader.note(baseUrl, caps, opts);
62
+ }
63
+ /** 本进程观测到的读数;这一格空缺 ⇒ `{kind:'unobserved'}`。 */
64
+ export function observedPermissionRulesWrite(baseUrl = engineWireTarget()?.baseUrl) {
65
+ return reader.observed(baseUrl);
66
+ }
67
+ /**
68
+ * 「这次调用摸不摸得到单步写入口」的判据单源(布尔):只有 `present` 才 `true`;其余三态
69
+ * (`absent` / `not_reported` / `unobserved`)一律 `false`——藏入口的默认方向是**保守**的,
70
+ * 判不出与明确没有对「要不要露出这个写按钮」是同一个答案。
71
+ *
72
+ * 🔴 本函数是本位**唯一**的派生点:{@link "./hitl/persistedRulesWire.js".persistedRulesWriteAvailable}
73
+ * 只调用它,不再合取别的位、也不再另读一遍 caps——同一件事只许有一个判官。
74
+ */
75
+ export function permissionRulesWriteAvailable(reading) {
76
+ return reading.kind === 'present';
77
+ }
78
+ /** doctor 那一行的 detail 串,唯一措辞真源。🔴 两种「读不出」都不许暗示「够得着」或「够不着」。 */
79
+ export function permissionRulesWriteDoctorDetail(reading) {
80
+ switch (reading.kind) {
81
+ case 'unobserved':
82
+ return 'permission-rule write entry not observed — the engine reports it on /v1/capabilities (permissionRulesWrite); this process has no usable capabilities reading cached for it (none received, or the last one was unreadable)';
83
+ case 'not_reported':
84
+ return 'permission-rule write entry not reported by this engine — this binary predates the entry itself; hide it rather than calling it';
85
+ case 'absent':
86
+ return 'permission-rule write entry unreachable for this call — either the rules lane is not open on this deployment, or this call cannot bring a verifiable identity';
87
+ case 'present':
88
+ return 'permission-rule write entry reachable for this call — a per-call attempt still answers for itself whether the direction, text, and scope are accepted';
89
+ }
90
+ }
91
+ /** 换代失效口(引擎温切成功后调):清成未观测。空串 ⇒ no-op;绝不 throw。 */
92
+ export function forgetPermissionRulesWriteReading(baseUrl) {
93
+ reader.forget(baseUrl);
94
+ }
95
+ /** 测试钩子。 */
96
+ export function __resetPermissionRulesWriteReadingsForTests() {
97
+ reader.__resetForTests();
98
+ }
package/dist/seam.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import type { ModelUsage, SDKMessage } from '@sema-agent/agent-types';
10
10
  import type { EngineTurnUsage } from './adapter/downstream/turnUsageToModelUsage.js';
11
11
  import type { RunCostReconcile } from './adapter/downstream/terminalToSdkResult.js';
12
- import type { WiringManifestAutoMode, WiringManifestModelGate, WiringManifestMcpEntryView, WiringManifestHookEntryView, WiringManifestLspSeamView, WiringManifestWriteProtectionView, WiringManifestAutoConsolidationView, WiringManifestReadDenyView, ContextUsageSectionView } from './adapter/downstream/eventToSdkMessage.js';
12
+ import type { WiringManifestAutoMode, WiringManifestModelGate, WiringManifestMcpEntryView, WiringManifestHookEntryView, WiringManifestLspSeamView, WiringManifestWriteProtectionView, WiringManifestAutoConsolidationView, WiringManifestReadDenyView, ContextUsageSectionView, PromptManifestSectionView, PromptManifestBlockView, PromptManifestToolView } from './adapter/downstream/eventToSdkMessage.js';
13
13
  import type { ToolRosterView, ToolRosterDeltaView } from './toolRoster.js';
14
14
  /**
15
15
  * `AbortSignal` 的结构型(B3 扩容,SEAM-GAP-4)。
@@ -153,9 +153,6 @@ export type ChromeEvent = {
153
153
  laneProof: LaneProof;
154
154
  taskId: string;
155
155
  registration: unknown;
156
- } | {
157
- kind: 'tasks_expand';
158
- laneProof: LaneProof;
159
156
  } | {
160
157
  kind: 'thinking_activity';
161
158
  laneProof: LaneProof;
@@ -545,7 +542,7 @@ export type ChromeEvent = {
545
542
  * ⚠️ **只有 live 腿有本帧**(server 7.69.0 亲读:durable/bg 腿把它喂给 rewind 锚而不入账本)
546
543
  * ⇒ replay/resume 上映射表是空的;那时正解是**不画分割线**,不是回退成「画在最后一条」。
547
544
  */
548
- | MessageCommittedChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent | ThinkingSegmentEndChromeEvent | ToolDisclosureChromeEvent | ToolProgressChromeEvent | ToolRosterDeltaChromeEvent | ContextUsageChromeEvent | CompactionOutcomeChromeEvent | WiringManifestChromeEvent | ResultTextDivergedChromeEvent
545
+ | MessageCommittedChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent | ThinkingSegmentEndChromeEvent | ToolDisclosureChromeEvent | ToolProgressChromeEvent | ToolRosterDeltaChromeEvent | ContextUsageChromeEvent | CompactionOutcomeChromeEvent | PromptAssembledChromeEvent | WiringManifestChromeEvent | ResultTextDivergedChromeEvent
549
546
  /**
550
547
  * B-078 / L-208(0.65.0):design/172 流内审批协议的**两条帧**。修前它们在标准管线上落
551
548
  * `dropped('unsupported_arm')`,壳自己另接一份 ⇒ 只走包管线的宿主(desktop/web)拿不到
@@ -820,6 +817,57 @@ export interface CompactionOutcomeChromeEvent {
820
817
  reason?: string;
821
818
  eventId?: string;
822
819
  }
820
+ /**
821
+ * CC-91(0.76.0;sdk `prompt_assembled` = `{type} & PromptManifest`):**这一次 prepare 的提示词装配清单** ——
822
+ * 系统提示词由哪些段 / 块组成、每段多大、这一腿挂了哪些工具。durable 腿的台账帧。
823
+ *
824
+ * 🔴 **没有 `tokens`,只有 `chars`** —— 上游真字节里就没有 token 位(core `prompt.assembled` 的 sections 是
825
+ * `chars`,server 的白名单投影逐字段同形)。本包**刻意不铸** `tokens`:`chars / 系数` 是编的,不是真值。
826
+ * 要引擎自己的 token 估算去读 {@link ContextUsageChromeEvent} 的 `sections[].tokens`(引擎就是拿同一份
827
+ * `chars` 估的)。🔴 **这条 join 只在两面的 `sections` 都在场时成立**(同一张 v2 段 id 词表):`context_usage`
828
+ * 那一面的段视图也是从同一份 manifest 段列投影的,manifest 没有段列时它同样缺席 ⇒ **v1-only 腿上没有这条 join**,
829
+ * 那时别拿 `blocks` 当 `sections` 的同名替代(见下,两张词表不同)。
830
+ * 🔴 **`chars` 与 `context_usage.usedTokens` 禁相减**(两个量纲不是一条账,且 chars ≠ tokens 更差一层);
831
+ * `Σ chars` 也不构成「提示词占了窗口多少」——那句话只能由 `context_usage` 那一帧回答。
832
+ * 🔴 **一次任务可能多条**(cascade / verify 的每一 rung、resume 腿各自 re-prepare,server 逐条保序落账)⇒
833
+ * 本包每一条都交出去、**不去重**;要「当前组成」的宿主按**最后一条**取(与 `context_usage` 同律)。
834
+ * 🔴 `sections[].id` 与 `blocks[].id` 是**两张不同的词表**,不要混着分组:
835
+ * · `sections[].id` = core pack 的**段** id(`core/<slot>.<name>` 命名空间,例 `core/role.base` / `core/harness.head`);
836
+ * · `blocks[].id` = v1 的**带标签块** id(`role.base` / `harness.context` / `env.context` / `memory.tail` …),
837
+ * 而且是**多对一**:多个段可以折进同一个块 ⇒ 块不是段的另一种写法,两面的行数与粒度都可以不同。
838
+ * ⚠️ 两张词表**没有命名空间隔离的保证**(块 id 不带前缀、operator 声明的段 id 是自由串,理论上能撞同名)⇒
839
+ * **按面各自分组**,别靠 id 形状判断自己在哪一面,更别拿块 id 去 join `context_usage` 的段面。
840
+ * 两张词表都是引擎**开集**原词:按成员渲染必须带 default 臂,认不得的原词照显示;**别**假定任何固定的分类闭集,
841
+ * 也别把某一类的缺席读成「那一类是零」(见下)。
842
+ * ⚠️ **这一面没有工具尺寸**:`tools[]` 只有名字,「工具 schema 占了多少」在这条 wire 上读不出来。
843
+ * 🔴 **缺席只说「本事件没有可读的这一面」,不是一句关于引擎的诊断**:上游整键缺席、发了空数组、逐行滤完只剩空
844
+ * 三种情形在本臂上**同形**(都折成键缺席)⇒ 看到 `sections` / `tools` / `blocks` 不在场,**不许**据此渲
845
+ * 「这个 provider 交的是不透明提示词」「这一腿没挂工具」之类的结论 —— 那需要别的独立证据。
846
+ * ⚠️ `sections[].slot` 是**语义槽**(`identity` / `security` / `harness` / `discovery` / `memory` …,开集),逐字沿用上游键名 ——
847
+ * 同一件事在 {@link ContextUsageChromeEvent} 的 `sections[].kind` 上叫 `kind`,两面各跟自己的上游,别指望同名。
848
+ * ⚠️ **skills 清单不在这一面**:core 已把它移出稳定前缀(观测口是 `steering_injected{source:"skills_listing"}`)⇒
849
+ * 清单里找不到 skills **不是**「skills 是 0」。
850
+ * ⚠️ `hash` / `contentHash` 等对账锚、以及帧上的 `constitution`(谁拥有宪法层)本包**不过境** —— 前者进程加盐跨重启不可比,后者今天零消费场景
851
+ * (每个上公面的键都是一条要长期兑现的契约:additive 键晚加不破坏、早加删不掉)。要它们按表态制另提需求。
852
+ * 🔴 本臂**不是终态**、也不是一条转录消息(转录面零行);`laneProof.lane === 'subagent'` = 某只子代这一腿的清单
853
+ * (server 今天的登记闸只登记 top-level run,所以这一支是防御性的,不是常态),别渲成主会话的。
854
+ */
855
+ export interface PromptAssembledChromeEvent {
856
+ kind: 'prompt_assembled';
857
+ laneProof: LaneProof;
858
+ /** 整条提示词的渲染字符数。缺席 = 没报,**不是 0**。 */
859
+ totalChars?: number;
860
+ /** v1 面:带标签块(`role.base` / `harness.context` / …;与 `sections[].id` **不是同一张词表**,且段→块可多对一)。
861
+ * 缺席 = 本事件没有可读的块视图(上游缺席 / 空数组 / 逐行滤光三者同形),**不是**「没有块」。 */
862
+ blocks?: readonly PromptManifestBlockView[];
863
+ /** v2 面:编译 IR 的逐段视图。🔴 整键缺席 = **本事件没有可读的段视图**(上游整键缺席 / 空数组 / 逐行滤光同形),
864
+ * **不是**各段为零、也**不能**反推「引擎没装配出段 IR」(那是上游缺席那一支的成因之一,本面分不出是哪一支)。 */
865
+ sections?: readonly PromptManifestSectionView[];
866
+ /** v2 面:这一腿挂载的工具名(MCP 类按名字前缀分组渲染靠它)。缺席 = 本事件没有可读的工具视图(三支同形),
867
+ * **不是**「没挂工具」、更不是「工具占 0」(这一面本来就没有尺寸位)。 */
868
+ tools?: readonly PromptManifestToolView[];
869
+ eventId?: string;
870
+ }
823
871
  export interface ToolDisclosureChromeEvent {
824
872
  kind: 'tool_disclosure';
825
873
  laneProof: LaneProof;
package/dist/seam.js CHANGED
@@ -11,7 +11,6 @@ const CHROME_ARM_TABLE = {
11
11
  panel_task: { required: true, duty: 'publishEngineAgentPanelEvent(event) 原样转发' },
12
12
  bgshell_settle: { required: true, duty: '后台 shell 面板行落终态(含 outputPath)' },
13
13
  bgshell_register: { required: true, duty: '后台 shell 面板行建行 + 登记「句柄→宿主 run」' },
14
- tasks_expand: { required: false, duty: '展开 ctrl+t 任务面板' },
15
14
  thinking_activity: { required: false, duty: '开/收活体 "∴ Thinking…" 行' },
16
15
  request_start: { required: false, duty: 'spinner 置 requesting 态' },
17
16
  stream_delta: { required: false, duty: '渲活体预览 + 推进 responseLength' },
@@ -93,6 +92,16 @@ const CHROME_ARM_TABLE = {
93
92
  duty: 'CC-89(0.75.0):一次压缩的非 compacted 结局(作废 / 失败等),给用户一条可见提示。`outcome` / `trigger` 是引擎开集原词,按成员渲染必带 default 臂、认不得照原词显示;' +
94
93
  '🔴 不是终态 —— 任务还在跑,别据它收 spinner;`laneProof.lane` 为 subagent = 子代的压缩结局,别渲成主会话的;成功压缩不走本臂(权威是转录的 compact_boundary)。不接 = 压缩失败对用户不可见。',
95
94
  },
95
+ prompt_assembled: {
96
+ required: false,
97
+ duty: 'CC-91(0.76.0):这一次 prepare 的提示词装配清单(每段 / 块多大、挂了哪些工具),/context 类面板的引擎侧真值来源。🔴 一次任务可能多条(cascade / verify / resume 各自 re-prepare)' +
98
+ ' ⇒ 包不去重,要「当前组成」的宿主按**最后一条**取(与 context_usage 同律);🔴 本面只有 chars **没有 tokens**(上游真字节无此位)—— 要 token 估算去读 context_usage.sections[].tokens,' +
99
+ '但那条 join **只在两面的 sections 都在场时成立**(同一张 v2 段 id 词表;v1-only 腿两面都没有段视图 ⇒ 没有这条 join);chars 与 usedTokens **禁相减**(量纲不是一条账,chars ≠ tokens 更差一层)、禁拿 chars/系数 自己编 token 数;' +
100
+ '🔴 sections[].id(core pack 段 id,core/<slot>.<name>)与 blocks[].id(v1 带标签块)是**两张不同词表**且段→块可多对一,别混着分组;两张都是引擎开集原词,按成员渲染必带 default 臂,别假定任何固定分类闭集;' +
101
+ '🔴 sections / blocks / tools 整键缺席只说「本事件没有可读的这一面」(上游缺席 / 空数组 / 逐行滤光三支同形)—— 不是各段为零、不许据此渲「provider 交的是不透明提示词」或「没挂工具」这类诊断;tools 只有名字没有尺寸;' +
102
+ 'skills 清单不在本面(观测口是 steering_injected{source:"skills_listing"})⇒ 找不到 skills 不等于 skills 是 0;' +
103
+ '转录面零行,本臂不是终态;laneProof.lane 为 subagent = 子代那一腿的清单,别渲成主会话的。不接 = 同旧(端只能按本地镜像自己估)。',
104
+ },
96
105
  tool_disclosure: {
97
106
  required: false,
98
107
  duty: 'CC-08(0.71.0):这条腿的工具名字占位普查。`deferred` / `activated` 都是**全量快照,整只替换**(不做增量合并);' +
@@ -160,8 +160,13 @@ export function stripSelfOrchestrationIntent(req) {
160
160
  }
161
161
  return out;
162
162
  }
163
- const ABSENT = { kind: 'absent' };
164
- const ACCESSOR = { kind: 'accessor' };
163
+ // 🔴 0.76.1 族扫收编(与主车道证明那半场同族、**处置刻意不同**):这两只也是「模块级对象当缺省值
164
+ // 往外交」的形,但它们的暴露面完全不同 —— `ownRead` 是本文件私有、结果只被读 `kind` / `value`、
165
+ // 一只都不出模块,所以「两个消费者按身份把两件事认成一件」这一半在这里不成立;剩下的唯一风险是
166
+ // 本模块将来有人往上写,而那正是冻结接得住的。⇒ 这里冻结代价为零、判别力刚好;主车道那半场是
167
+ // 交给宿主的公开输出面,只能靠**逐次新建**解别名(见 `adapt/ids.ts` 的 `mainLane`)。
168
+ const ABSENT = Object.freeze({ kind: 'absent' });
169
+ const ACCESSOR = Object.freeze({ kind: 'accessor' });
165
170
  /**
166
171
  * 从一个**不可信**对象上取一位:只认**自有数据描述符**的 `value`。
167
172
  *
@@ -43,7 +43,7 @@
43
43
  */
44
44
  import { capForDisplay } from './fleetTaskDesc.js';
45
45
  import { engineWireTarget } from './engineWireTarget.js';
46
- import { snapshotCapsGeneration, capsGenerationStillCurrent } from './engineCapsGenerationGuard.js';
46
+ import { createEngineCapReader } from './engineCapReader.js';
47
47
  /**
48
48
  * caps 回体 → 本格读数;**畸形一律 `undefined`**(= 这一格不写 ⇒ 读口答 `unobserved`)。
49
49
  *
@@ -80,7 +80,16 @@ export function projectSqlEngineCapability(caps) {
80
80
  return undefined;
81
81
  return { kind: 'present', view: { engine, isolation, txnMode } };
82
82
  }
83
- const readingByBase = new Map();
83
+ /**
84
+ * 本格的 per-baseUrl 读账 + tee / 读口 / 失效口 / 测试钩四口 —— 各只能力位读器**共用同一份实现**
85
+ * ({@link createEngineCapReader});本文件只留这一格真正不同的部分:投影函数 {@link projectSqlEngineCapability}、doctor 措辞表,以及本位自己的判据助手(如有)。
86
+ * 🔴 Map 在工厂闭包里,**一只读器一张**:收到读不懂的回体只删自己这一格,绝不连坐别的能力面。
87
+ */
88
+ const reader = createEngineCapReader({
89
+ name: 'sql',
90
+ project: projectSqlEngineCapability,
91
+ makeUnobserved: () => ({ kind: 'unobserved' }),
92
+ });
84
93
  /**
85
94
  * 宿主 caps probe 的**读面 tee** 落点(壳侧与 mcpGate / sessionBackground / projectContext /
86
95
  * crashConverged / workflowsGate 五条并列)。绝不 throw —— 读面腿不许反噬 caps 探测链。
@@ -103,32 +112,7 @@ const readingByBase = new Map();
103
112
  * 有任何东西变坏。🔴 **tee 自己抓号是假判据**(陈旧那一发在这里读到的是新代号,永远像当代的)。
104
113
  */
105
114
  export function noteEngineCapsForSqlEngine(baseUrl, caps, opts) {
106
- if (typeof baseUrl !== 'string' || baseUrl === '')
107
- return;
108
- // 🔴 代际只在入口**快照一次**(异源对抗复审 R3):opts 是调用方的对象,回体上的 getter 能在投影中途改它;首末两次核对都用这份快照,
109
- // 读 opts / 读代际抛出 ⇒ 不动格(与陈旧代际同一处置)。
110
- const gen = snapshotCapsGeneration(opts);
111
- if (gen === null)
112
- return; // 调用方 opts 的 getter 抛出 = 供给坏了 ⇒ 不动格
113
- if (!capsGenerationStillCurrent(baseUrl, gen))
114
- return;
115
- let reading;
116
- try {
117
- reading = projectSqlEngineCapability(caps);
118
- }
119
- catch {
120
- // 🔴 0.74.2 同形存量(CC-69 复审 R1 族扫):抛 getter / Proxy 的回体 = 畸形,与「投影答 undefined」同一处置 —— 删格,不许留着上一台的读数。
121
- reading = undefined;
122
- }
123
- // 🔴 投影之后**再核一次代际**(异源对抗复审 R2):投影会执行回体上不可信的 getter,它可以在中途让这一格换代
124
- // (invalidate + 新代际的 tee 已写入);此时无论投影返回还是抛出,都不许动新代际的格。
125
- if (!capsGenerationStillCurrent(baseUrl, gen))
126
- return;
127
- if (reading === undefined) {
128
- readingByBase.delete(baseUrl);
129
- return;
130
- }
131
- readingByBase.set(baseUrl, reading);
115
+ reader.note(baseUrl, caps, opts);
132
116
  }
133
117
  /**
134
118
  * 本进程观测到的 SQL 姿态;这一格空缺 ⇒ `{kind:'unobserved'}`(**绝不**折成 `none`)。
@@ -138,9 +122,7 @@ export function noteEngineCapsForSqlEngine(baseUrl, caps, opts) {
138
122
  * ⇒ Node 壳上行为一字节不变,浏览器/桌面宿主则由 `installEngineWireTarget()` 显式装)。
139
123
  */
140
124
  export function observedSqlEngine(baseUrl = engineWireTarget()?.baseUrl) {
141
- if (typeof baseUrl !== 'string' || baseUrl === '')
142
- return { kind: 'unobserved' };
143
- return readingByBase.get(baseUrl) ?? { kind: 'unobserved' };
125
+ return reader.observed(baseUrl);
144
126
  }
145
127
  /** 单行 UNTRUSTED 座的展示上限(UTF-16 单元;与壳侧那份同值)。 */
146
128
  const SQL_DETAIL_MAX = 40;
@@ -210,11 +192,9 @@ export function sqlEngineDoctorDetail(reading) {
210
192
  * 空串 ⇒ no-op;绝不 throw(它跑在引擎温切路径上)。
211
193
  */
212
194
  export function forgetSqlEngineReading(baseUrl) {
213
- if (typeof baseUrl !== 'string' || baseUrl === '')
214
- return;
215
- readingByBase.delete(baseUrl);
195
+ reader.forget(baseUrl);
216
196
  }
217
197
  /** 测试钩子。 */
218
198
  export function __resetSqlEngineReadingsForTests() {
219
- readingByBase.clear();
199
+ reader.__resetForTests();
220
200
  }
@@ -38,7 +38,7 @@
38
38
  */
39
39
  import { capForDisplay } from './fleetTaskDesc.js';
40
40
  import { engineWireTarget } from './engineWireTarget.js';
41
- import { snapshotCapsGeneration, capsGenerationStillCurrent } from './engineCapsGenerationGuard.js';
41
+ import { createEngineCapReader } from './engineCapReader.js';
42
42
  /** `backend: "none"` —— 唯一带独立语义的词(见文件头);其余词按开集读。 */
43
43
  export const WEB_SEARCH_BACKEND_NONE = 'none';
44
44
  /**
@@ -66,7 +66,16 @@ export function projectWebSearchBackendCapability(caps) {
66
66
  return { kind: 'none' };
67
67
  return { kind: 'present', view: { backend } };
68
68
  }
69
- const readingByBase = new Map();
69
+ /**
70
+ * 本格的 per-baseUrl 读账 + tee / 读口 / 失效口 / 测试钩四口 —— 各只能力位读器**共用同一份实现**
71
+ * ({@link createEngineCapReader});本文件只留这一格真正不同的部分:投影函数 {@link projectWebSearchBackendCapability}、doctor 措辞表,以及本位自己的判据助手(如有)。
72
+ * 🔴 Map 在工厂闭包里,**一只读器一张**:收到读不懂的回体只删自己这一格,绝不连坐别的能力面。
73
+ */
74
+ const reader = createEngineCapReader({
75
+ name: 'webSearch.backend',
76
+ project: projectWebSearchBackendCapability,
77
+ makeUnobserved: () => ({ kind: 'unobserved' }),
78
+ });
70
79
  /**
71
80
  * 宿主 caps probe 的**读面 tee** 落点(与 `noteEngineCapsForSqlEngine` / `noteEngineCapsForWriteProtection`
72
81
  * 并列;宿主在同一处按序调三只)。绝不 throw —— 读面腿不许反噬 caps 探测链。
@@ -77,38 +86,11 @@ const readingByBase = new Map();
77
86
  * 那一刻 `engineCapsGeneration(baseUrl)` 抓号、在这里原样交回,陈旧那一发整只丢掉;不传 ⇒ 照写。
78
87
  */
79
88
  export function noteEngineCapsForWebSearchBackend(baseUrl, caps, opts) {
80
- if (typeof baseUrl !== 'string' || baseUrl === '')
81
- return;
82
- // 🔴 代际只在入口**快照一次**(异源对抗复审 R3):opts 是调用方的对象,回体上的 getter 能在投影中途改它;首末两次核对都用这份快照,
83
- // 读 opts / 读代际抛出 ⇒ 不动格(与陈旧代际同一处置)。
84
- const gen = snapshotCapsGeneration(opts);
85
- if (gen === null)
86
- return; // 调用方 opts 的 getter 抛出 = 供给坏了 ⇒ 不动格
87
- if (!capsGenerationStillCurrent(baseUrl, gen))
88
- return;
89
- let reading;
90
- try {
91
- reading = projectWebSearchBackendCapability(caps);
92
- }
93
- catch {
94
- // 🔴 0.74.2 同形存量(CC-69 复审 R1 族扫):抛 getter / Proxy 的回体 = 畸形,与「投影答 undefined」同一处置 —— 删格,不许留着上一台的读数。
95
- reading = undefined;
96
- }
97
- // 🔴 投影之后**再核一次代际**(异源对抗复审 R2):投影会执行回体上不可信的 getter,它可以在中途让这一格换代
98
- // (invalidate + 新代际的 tee 已写入);此时无论投影返回还是抛出,都不许动新代际的格。
99
- if (!capsGenerationStillCurrent(baseUrl, gen))
100
- return;
101
- if (reading === undefined) {
102
- readingByBase.delete(baseUrl);
103
- return;
104
- }
105
- readingByBase.set(baseUrl, reading);
89
+ reader.note(baseUrl, caps, opts);
106
90
  }
107
91
  /** 本进程观测到的部署默认后端;这一格空缺 ⇒ `{kind:'unobserved'}`(**绝不**折成 `none`)。 */
108
92
  export function observedWebSearchBackend(baseUrl = engineWireTarget()?.baseUrl) {
109
- if (typeof baseUrl !== 'string' || baseUrl === '')
110
- return { kind: 'unobserved' };
111
- return readingByBase.get(baseUrl) ?? { kind: 'unobserved' };
93
+ return reader.observed(baseUrl);
112
94
  }
113
95
  /** provider 词的展示上限(UTF-16 单元;与两只兄弟同值同理由 —— 开集词按 UNTRUSTED-for-display 处置)。 */
114
96
  const WS_DETAIL_MAX = 40;
@@ -144,11 +126,9 @@ export function webSearchBackendDoctorDetail(reading) {
144
126
  * 处置是**清成未观测**,不是留旧值也不是铸一个 `none`。空串 ⇒ no-op;绝不 throw。
145
127
  */
146
128
  export function forgetWebSearchBackendReading(baseUrl) {
147
- if (typeof baseUrl !== 'string' || baseUrl === '')
148
- return;
149
- readingByBase.delete(baseUrl);
129
+ reader.forget(baseUrl);
150
130
  }
151
131
  /** 测试钩子。 */
152
132
  export function __resetWebSearchBackendReadingsForTests() {
153
- readingByBase.clear();
133
+ reader.__resetForTests();
154
134
  }