@sema-agent/client-core 0.30.0 → 0.30.2
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 +89 -0
- package/README.md +2 -1
- package/dist/adapter/activeRunSelfHeal.d.ts +21 -0
- package/dist/adapter/activeRunSelfHeal.js +54 -4
- package/dist/hitl/armedGateRegistry.d.ts +62 -4
- package/dist/hitl/armedGateRegistry.js +226 -14
- package/dist/hitl/localAllowRule.d.ts +80 -0
- package/dist/hitl/localAllowRule.js +96 -0
- package/dist/hitl/parkOwnership.d.ts +2 -1
- package/dist/hitl/parkOwnership.js +11 -3
- package/dist/hitl/persistedRulesWire.d.ts +169 -0
- package/dist/hitl/persistedRulesWire.js +236 -0
- package/dist/hitl/planReviewWire.d.ts +31 -1
- package/dist/hitl/planReviewWire.js +69 -30
- package/dist/hitl/toolApprovalWire.d.ts +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/docs/INTEGRATION-CLIENTS.md +9 -9
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hitl/persistedRulesWire.ts — 持久权限规则(design/179 / design/203)车道的**通用判定半场**
|
|
3
|
+
* (A-028.14 上收件,#244 F2,2026-08-14;源形 = cli `src/sema/persistedRulesWire.ts` 的
|
|
4
|
+
* 纯判定/wire 面;CC settings 三层 fs 读、facade 装配(SDK 短命 client)、通知呈现全部留宿主)。
|
|
5
|
+
*
|
|
6
|
+
* ── 病(census top-08 #1 分脑实证)───────────────────────────────────────────────────────────
|
|
7
|
+
* 包 `hitl/toolApprovalWire.ts` 的 `readToolApprovalRespondAck` 已把 `rulePersisted` /
|
|
8
|
+
* `ruleRefusal` 归一进 `ToolApprovalRespondAck`(delivery/decision 闭集门之后),而 cli 对同两键
|
|
9
|
+
* **另持一份裸读**(入参 `unknown`,无上述两道门)—— 同两键两处读法在跑 = 双份台账,server 增键
|
|
10
|
+
* /改形时两处各漂各的。web 消费同一帧族([3536] 记账)时还会长出第三份。
|
|
11
|
+
* 收口:persist-ack 读口({@link readRulePersistOutcome})与 `readToolApprovalRespondAck`
|
|
12
|
+
* **合成一处** —— 三态判决只从**已过包内结构窄化**的 ack 上导出,不再对 wire 原料二次开读。
|
|
13
|
+
*
|
|
14
|
+
* ── 🔴 能力位 gate,不 trial-by-501 ──────────────────────────────────────────────────────────
|
|
15
|
+
* SDK `resources/rules.d.ts` 头注逐字:`capabilities.permissionRules` 与这台 server **所有**规则口
|
|
16
|
+
* 的 501 是同一条谓词(`PERMISSION_RULES_ENABLED ∧ 规则店 wired`),**按它 gate**,别按版本号猜、
|
|
17
|
+
* 也别拿 501 当探针。撤销面两口(GET/DELETE /v1/rules)自 server 7.12.0 才有路由,而
|
|
18
|
+
* `permissionRules` 比撤销面**早出生** ⇒ 一台 7.11.0 worker 会报 `permissionRules:true` 而这两口
|
|
19
|
+
* 404 `not_found.route`。所以撤销面**另按 `capabilities.permissionRulesRevoke`**:
|
|
20
|
+
* · 在场 ⇒ 路由已铸(答不答仍看 `permissionRules`);
|
|
21
|
+
* · 缺席 ⇒ 这台 worker 比撤销面老 ⇒ **藏起治理入口**(不渲死按钮)。
|
|
22
|
+
* 读法用包内同步口 `engineCapTrue`(engineCapsCache;未判/缺键 = false = 藏,version-safe)。
|
|
23
|
+
* baseUrl 由宿主显式传入(cli 的 `SEMA_LIVE_BASEURL` 缺省读法留壳 —— env 是宿主资产)。
|
|
24
|
+
*
|
|
25
|
+
* ── 🔴 404 禁按成因分支 ──────────────────────────────────────────────────────────────────────
|
|
26
|
+
* redeem 的 404 `not_found.rule_ticket` 是同形信封(伪造/别人的/过期/已用过 + 记录/载荷/确认失败
|
|
27
|
+
* 七类共用一格,wire 上分不出来是设计)。唯一可分的是 503 `state.rule_import_retry`:那一支意味着
|
|
28
|
+
* **票还在**,原样重试即可(与「这张票没了」是相反的处置)。
|
|
29
|
+
*
|
|
30
|
+
* ── UNTRUSTED ───────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
* 规则文本与 `skipped[].reason` 都是引擎/用户 settings 侧的内容,只渲染绝不当代码用;`reason` 是
|
|
32
|
+
* **给人看的散文不是机读码**(SDK 头注),分类只许按第一个 `:` 前缀,并且要容得下**没有前缀**的形。
|
|
33
|
+
*/
|
|
34
|
+
import type { CcImportLayer, CcImportPrepareResult, CcImportRedeemResult, PersistedRule, RuleListParams, RuleListResult, RuleRevokeRequest, RuleRevokeResult } from '@sema-agent/sdk';
|
|
35
|
+
/** 规则车道消费的 `client.rules` 切片(注入缝:宿主给真 SDK facade,测试给假件)。 */
|
|
36
|
+
export interface RulesFacade {
|
|
37
|
+
list(params?: RuleListParams, opts?: {
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}): Promise<RuleListResult>;
|
|
40
|
+
revoke(input: RuleRevokeRequest, opts?: {
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}): Promise<RuleRevokeResult>;
|
|
43
|
+
ccImportPrepare(layers: CcImportLayer[], opts?: {
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
}): Promise<CcImportPrepareResult>;
|
|
46
|
+
ccImportRedeem(ticket: string, opts?: {
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}): Promise<CcImportRedeemResult>;
|
|
49
|
+
}
|
|
50
|
+
/** 规则车道在不在(四口 501 同源谓词)。未判/缺键/base 缺席 = false = 整面藏。 */
|
|
51
|
+
export declare function persistedRulesLaneAvailable(baseUrl: string | undefined): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* 治理面(list + revoke)在不在。**两段合取**:
|
|
54
|
+
* · `permissionRulesRevoke` 在场 = 撤销面路由已铸(缺席 = worker 比撤销面老 ⇒ 藏);
|
|
55
|
+
* · `permissionRules` 为真 = 车道真答话(店 + 旋钮)。
|
|
56
|
+
* 只查前者会在「7.11.0 且规则店在」的 worker 上渲出一个恒 404 的治理入口。
|
|
57
|
+
*/
|
|
58
|
+
export declare function persistedRulesGovernanceAvailable(baseUrl: string | undefined): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* 规则口失败的**处置分类**(不是成因分类)。SDK 的 typed 错误族按 `status` + `errorCode` 读,
|
|
61
|
+
* 结构读法(不 instanceof)—— 注入面可能是假件,bundling 下 instanceof 也不该是承重判据。
|
|
62
|
+
*/
|
|
63
|
+
export type RulesFailure =
|
|
64
|
+
/** 501 `capability.rule_store_required` —— 这台部署没接规则店/旋钮关着 ⇒ 整面藏起来。 */
|
|
65
|
+
{
|
|
66
|
+
kind: 'lane-unavailable';
|
|
67
|
+
message: string;
|
|
68
|
+
}
|
|
69
|
+
/** 404 `not_found.route` —— worker 比撤销面老(≤7.11.0)⇒ 治理入口藏起来。 */
|
|
70
|
+
| {
|
|
71
|
+
kind: 'route-missing';
|
|
72
|
+
message: string;
|
|
73
|
+
}
|
|
74
|
+
/** 404 `not_found.rule_ticket` —— 这张票在这里不可用,**禁按成因分支**:丢票,重来一遍。 */
|
|
75
|
+
| {
|
|
76
|
+
kind: 'ticket-dead';
|
|
77
|
+
message: string;
|
|
78
|
+
}
|
|
79
|
+
/** 503 `state.rule_import_retry` —— **票还在**,原样重试(与 ticket-dead 相反的处置)。 */
|
|
80
|
+
| {
|
|
81
|
+
kind: 'retry-same-ticket';
|
|
82
|
+
message: string;
|
|
83
|
+
retryAfterSec?: number;
|
|
84
|
+
}
|
|
85
|
+
/** 503 `state.rule_remove_failed` —— 什么都没写,或结局不定;自己重试后用 list 对账。 */
|
|
86
|
+
| {
|
|
87
|
+
kind: 'retryable';
|
|
88
|
+
message: string;
|
|
89
|
+
}
|
|
90
|
+
/** 400 `request.query_invalid` —— 游标铸在别的 rev/principal/scope 上 ⇒ 丢游标从头列。 */
|
|
91
|
+
| {
|
|
92
|
+
kind: 'cursor-stale';
|
|
93
|
+
message: string;
|
|
94
|
+
}
|
|
95
|
+
/** 413 —— 候选总数超上限(200):缩短后重发,重试同一份没有意义。 */
|
|
96
|
+
| {
|
|
97
|
+
kind: 'too-many-candidates';
|
|
98
|
+
message: string;
|
|
99
|
+
}
|
|
100
|
+
/** 403 `auth.operator_only` —— 越权读/撤销(替别人)被拒。 */
|
|
101
|
+
| {
|
|
102
|
+
kind: 'forbidden';
|
|
103
|
+
message: string;
|
|
104
|
+
} | {
|
|
105
|
+
kind: 'error';
|
|
106
|
+
message: string;
|
|
107
|
+
};
|
|
108
|
+
export declare function classifyRulesFailure(e: unknown): RulesFailure;
|
|
109
|
+
/** 一次「列全」的结果。`rules` 恒是**完整**清单(翻不完 ⇒ 走 failure,绝不交半份清单)。 */
|
|
110
|
+
export type ListAllPersistedRulesOutcome = {
|
|
111
|
+
ok: true;
|
|
112
|
+
rules: PersistedRule[];
|
|
113
|
+
rev: number;
|
|
114
|
+
} | {
|
|
115
|
+
ok: false;
|
|
116
|
+
failure: RulesFailure;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* 列全一位 principal 名下活着的规则。
|
|
120
|
+
*
|
|
121
|
+
* 🔴 **翻页要翻完**:`nextCursor` 缺席才是终点 —— 半途停下拿到的是一份不全的清单,而治理视图
|
|
122
|
+
* 恰恰最不能拿不全的清单当全量。所以这里 drain 到底,任何一页失败都不交部分结果。
|
|
123
|
+
* 🔴 **游标绑 `(rev, principal, scope)`**:两页之间有人加/删了规则 ⇒ 第二页 400
|
|
124
|
+
* `request.query_invalid`。处置 = **丢游标从头列一次**(静默重置成「接着上一页」会得到一份既漏行
|
|
125
|
+
* 又重行的清单);从头再撞一次 ⇒ 如实报 cursor-stale,由调用方(人按 r 刷新)决定。
|
|
126
|
+
* 🔴 **调用方 cursor 不收**(codex F2 对抗复审 [medium]):类型上剔掉 `cursor` 还不够 —— JS
|
|
127
|
+
* 调用方仍能塞进来,而首页的 `...params` 会把它原样送出 ⇒ 「列全」从**中途**开始却报 `ok:true`
|
|
128
|
+
* 完整清单(治理面据此藏掉仍然生效的规则)。运行期显式剥除 + 留痕:drain 恒从第一页起,
|
|
129
|
+
* 「接着别人的 keyset」证明不了完整性,与本函数的契约(rules 恒完整)结构性冲突。
|
|
130
|
+
*/
|
|
131
|
+
export declare function listAllPersistedRules(facade: RulesFacade, params?: Omit<RuleListParams, 'cursor'>, opts?: {
|
|
132
|
+
signal?: AbortSignal;
|
|
133
|
+
}): Promise<ListAllPersistedRulesOutcome>;
|
|
134
|
+
/**
|
|
135
|
+
* `skipped[].reason` 的分类。**只按第一个 `:` 前缀**,且必须容得下**没有前缀**的两种真值形
|
|
136
|
+
* (整层 JSON 解不开 / 条目不是串)——SDK 头注逐字:reason 是给人看的散文,不是机读码,
|
|
137
|
+
* 全串等值匹配与「裸码」两种读法都会漂。
|
|
138
|
+
* 前缀形判据刻意保守(`RuleRejectCode` 形:小写 + 点/下划线),防把散文里的第一个冒号误读成码。
|
|
139
|
+
*/
|
|
140
|
+
export declare function classifySkippedReason(reason: string): {
|
|
141
|
+
code?: string;
|
|
142
|
+
text: string;
|
|
143
|
+
};
|
|
144
|
+
/** 「不再询问到底存上了没」的三态判决(呈现半场按它渲一行,绝不据它翻转裁决)。 */
|
|
145
|
+
export type RulePersistOutcome = {
|
|
146
|
+
state: 'unknown';
|
|
147
|
+
} | {
|
|
148
|
+
state: 'persisted';
|
|
149
|
+
} | {
|
|
150
|
+
state: 'refused';
|
|
151
|
+
reason?: string;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* respond 回执上「不再询问到底存上了没」的**结构化读口**。
|
|
155
|
+
*
|
|
156
|
+
* 🔴 **单一台账**(A-028.14 收口本体):`rulePersisted` / `ruleRefusal` 的结构窄化只有
|
|
157
|
+
* {@link readToolApprovalRespondAck} 这一份 —— 本函数不对 wire 原料二次开读,ack 过不了包内
|
|
158
|
+
* 结构门(缺 `delivery:'applied'` / `approvalId` / 三词闭集 `decision`)⇒ **`unknown`**:
|
|
159
|
+
* 一张连回执身份都不成形的 ack,不配驱动一行「已保存」的用户告知(诚实缺席优先)。
|
|
160
|
+
* 收口前 cli 的裸读会把 `{rulePersisted:true}` 这类半形对象读成 `persisted` —— 那正是双份台账
|
|
161
|
+
* 各漂各的形,常驻套对这一格有反向钉。
|
|
162
|
+
*
|
|
163
|
+
* 🔴 `rulePersisted` 缺席 ≠ `false`(未带 persistRule 的回决 / 旧 server ⇒ 字段省略);
|
|
164
|
+
* 🔴 规则没存上**从不翻转裁决** —— 200 + `rulePersisted:false` + `ruleRefusal` 是诚实形,
|
|
165
|
+
* 宿主只说「这次放行了,但『不再询问』没存上」,绝不渲成整次审批失败。
|
|
166
|
+
* 🔴 `rule_lane_unavailable` **四种成因共用一格、两种是本卡局限** ⇒ 禁据一帧判断整台部署的车道
|
|
167
|
+
* 在不在(那要看能力位)。
|
|
168
|
+
*/
|
|
169
|
+
export declare function readRulePersistOutcome(ack: unknown): RulePersistOutcome;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { engineCapTrue } from '../engineCapsCache.js';
|
|
2
|
+
import { hostLog } from '../host.js';
|
|
3
|
+
import { readToolApprovalRespondAck } from './toolApprovalWire.js';
|
|
4
|
+
// ── 能力位 gate ──────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
/** 规则车道在不在(四口 501 同源谓词)。未判/缺键/base 缺席 = false = 整面藏。 */
|
|
6
|
+
export function persistedRulesLaneAvailable(baseUrl) {
|
|
7
|
+
return engineCapTrue(baseUrl, 'permissionRules');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 治理面(list + revoke)在不在。**两段合取**:
|
|
11
|
+
* · `permissionRulesRevoke` 在场 = 撤销面路由已铸(缺席 = worker 比撤销面老 ⇒ 藏);
|
|
12
|
+
* · `permissionRules` 为真 = 车道真答话(店 + 旋钮)。
|
|
13
|
+
* 只查前者会在「7.11.0 且规则店在」的 worker 上渲出一个恒 404 的治理入口。
|
|
14
|
+
*/
|
|
15
|
+
export function persistedRulesGovernanceAvailable(baseUrl) {
|
|
16
|
+
return engineCapTrue(baseUrl, 'permissionRulesRevoke') && engineCapTrue(baseUrl, 'permissionRules');
|
|
17
|
+
}
|
|
18
|
+
function errShape(e) {
|
|
19
|
+
const o = (e ?? {});
|
|
20
|
+
return {
|
|
21
|
+
...(typeof o.status === 'number' ? { status: o.status } : {}),
|
|
22
|
+
...(typeof o.errorCode === 'string' ? { errorCode: o.errorCode } : {}),
|
|
23
|
+
message: typeof o.message === 'string' && o.message !== '' ? o.message : String(e),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** `Retry-After` 秒数(SDK 把它折进 `retryAfterMs`)。非数/负数 ⇒ 缺席。 */
|
|
27
|
+
function retryAfterSecOf(e) {
|
|
28
|
+
const ms = e.retryAfterMs;
|
|
29
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
return Math.ceil(ms / 1000);
|
|
32
|
+
}
|
|
33
|
+
export function classifyRulesFailure(e) {
|
|
34
|
+
const { status, errorCode, message } = errShape(e);
|
|
35
|
+
if (status === 501 || errorCode === 'capability.rule_store_required') {
|
|
36
|
+
return { kind: 'lane-unavailable', message };
|
|
37
|
+
}
|
|
38
|
+
if (status === 404) {
|
|
39
|
+
// 🔴 两个 404 是**不同处置**(SDK 头注):route 缺席 = 版本太老(藏面);rule_ticket = 票没了
|
|
40
|
+
// (丢票重来)。其余 404 一律按通用错误 —— 绝不按成因猜。
|
|
41
|
+
if (errorCode === 'not_found.route')
|
|
42
|
+
return { kind: 'route-missing', message };
|
|
43
|
+
if (errorCode === 'not_found.rule_ticket')
|
|
44
|
+
return { kind: 'ticket-dead', message };
|
|
45
|
+
return { kind: 'error', message };
|
|
46
|
+
}
|
|
47
|
+
if (errorCode === 'state.rule_import_retry') {
|
|
48
|
+
const sec = retryAfterSecOf(e);
|
|
49
|
+
return { kind: 'retry-same-ticket', message, ...(sec !== undefined ? { retryAfterSec: sec } : {}) };
|
|
50
|
+
}
|
|
51
|
+
if (errorCode === 'state.rule_remove_failed')
|
|
52
|
+
return { kind: 'retryable', message };
|
|
53
|
+
if (status === 400 && errorCode === 'request.query_invalid')
|
|
54
|
+
return { kind: 'cursor-stale', message };
|
|
55
|
+
if (status === 413)
|
|
56
|
+
return { kind: 'too-many-candidates', message };
|
|
57
|
+
if (status === 403 && errorCode === 'auth.operator_only')
|
|
58
|
+
return { kind: 'forbidden', message };
|
|
59
|
+
return { kind: 'error', message };
|
|
60
|
+
}
|
|
61
|
+
/** 一页要多少条。**必须显式给**(codex 对抗复审 [medium] 实撞):server 缺省是 **50**,而页帽
|
|
62
|
+
* 按「200/页」算 ⇒ 真实上界只有 1250 条,一位规则多于 1250 的 principal 会恒拿到「翻不完」的
|
|
63
|
+
* 失败、整个治理面打不开,而注释还写着 5000。夹取语义在 server(非数/越界夹进 1..200),所以给
|
|
64
|
+
* 上限最省往返、也让页帽的算术与现实一致。 */
|
|
65
|
+
const PAGE_LIMIT = 200;
|
|
66
|
+
/** 页数硬帽:server 恒给 nextCursor 的坏形不该让治理面无限翻(PAGE_LIMIT × 25 = 5000 条)。 */
|
|
67
|
+
const MAX_PAGES = 25;
|
|
68
|
+
/**
|
|
69
|
+
* 列全一位 principal 名下活着的规则。
|
|
70
|
+
*
|
|
71
|
+
* 🔴 **翻页要翻完**:`nextCursor` 缺席才是终点 —— 半途停下拿到的是一份不全的清单,而治理视图
|
|
72
|
+
* 恰恰最不能拿不全的清单当全量。所以这里 drain 到底,任何一页失败都不交部分结果。
|
|
73
|
+
* 🔴 **游标绑 `(rev, principal, scope)`**:两页之间有人加/删了规则 ⇒ 第二页 400
|
|
74
|
+
* `request.query_invalid`。处置 = **丢游标从头列一次**(静默重置成「接着上一页」会得到一份既漏行
|
|
75
|
+
* 又重行的清单);从头再撞一次 ⇒ 如实报 cursor-stale,由调用方(人按 r 刷新)决定。
|
|
76
|
+
* 🔴 **调用方 cursor 不收**(codex F2 对抗复审 [medium]):类型上剔掉 `cursor` 还不够 —— JS
|
|
77
|
+
* 调用方仍能塞进来,而首页的 `...params` 会把它原样送出 ⇒ 「列全」从**中途**开始却报 `ok:true`
|
|
78
|
+
* 完整清单(治理面据此藏掉仍然生效的规则)。运行期显式剥除 + 留痕:drain 恒从第一页起,
|
|
79
|
+
* 「接着别人的 keyset」证明不了完整性,与本函数的契约(rules 恒完整)结构性冲突。
|
|
80
|
+
*/
|
|
81
|
+
export async function listAllPersistedRules(facade, params = {}, opts) {
|
|
82
|
+
const { cursor: callerCursor, ...cleanParams } = params;
|
|
83
|
+
if (callerCursor !== undefined) {
|
|
84
|
+
hostLog('debug', 'persistedRulesWire: caller-supplied cursor ignored by listAllPersistedRules — a drain that starts mid-keyset cannot prove completeness, so it always starts from the top');
|
|
85
|
+
}
|
|
86
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
87
|
+
const rules = [];
|
|
88
|
+
let cursor;
|
|
89
|
+
let rev = null;
|
|
90
|
+
let restart = false;
|
|
91
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
92
|
+
let res;
|
|
93
|
+
try {
|
|
94
|
+
// limit 显式给:①页帽的算术要与真实页大小一致(见 PAGE_LIMIT);②keyset 游标绑的是
|
|
95
|
+
// 一份确定的翻页参数,页大小在两页之间变会让「接着上一页」失去意义。调用方可覆盖。
|
|
96
|
+
res = await facade.list({ limit: PAGE_LIMIT, ...cleanParams, ...(cursor !== undefined ? { cursor } : {}) }, opts);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
const failure = classifyRulesFailure(e);
|
|
100
|
+
if (failure.kind === 'cursor-stale' && cursor !== undefined && attempt === 0) {
|
|
101
|
+
hostLog('debug', 'persistedRulesWire: rule list cursor was minted on another rev/principal/scope — dropping it and re-listing from the top (never resuming a stale keyset)');
|
|
102
|
+
restart = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return { ok: false, failure };
|
|
106
|
+
}
|
|
107
|
+
// 🔴 页体 fail-closed 窄化(codex F2 轮二 [high]):SDK 传输层只 JSON.parse,不做运行期
|
|
108
|
+
// schema 校验 —— 一个 2xx 的 `{rev:9}`(无 rules 数组)在旧读法下会被认证成「完整的空清单」
|
|
109
|
+
// (`ok:true, rules:[]`),治理面据此宣称「没有持久规则」而活规则不可见、无法撤销
|
|
110
|
+
// (版本偏斜/后端降级下静默发生)。坏形页 = 判不出,绝不当「读到了空的」:
|
|
111
|
+
// · `rules` 必须是数组、`rev` 必须是有限数(SDK `RuleListResult` 两键皆必填);
|
|
112
|
+
// · `nextCursor` 只有两种合法形:**缺席**(= 终页)或**非空字符串**(= 还有下一页);
|
|
113
|
+
// `''`/null/数字等坏形不许被折成「到头了」—— 猜终点与猜续点同罪。
|
|
114
|
+
if (typeof res !== 'object' ||
|
|
115
|
+
res === null ||
|
|
116
|
+
!Array.isArray(res.rules) ||
|
|
117
|
+
typeof res.rev !== 'number' ||
|
|
118
|
+
!Number.isFinite(res.rev)) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
failure: {
|
|
122
|
+
kind: 'error',
|
|
123
|
+
message: 'rule list page was malformed (missing/ill-typed rules array or rev on a 2xx) — refusing to certify it as a complete governance list',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// 🔴 跨页 rev 钉(codex F2 轮三 [high]):游标契约上绑 rev —— 清单变了,续页**该** 400
|
|
128
|
+
// cursor-stale。一个 2xx 却换了 rev 的续页 = server 违约或降级形,拼起来是**混合快照**;
|
|
129
|
+
// 认证它为完整清单,比 400 那条腿(丢游标重列)更坏 —— 这里不猜不修补,如实 failure。
|
|
130
|
+
if (rev !== null && res.rev !== rev) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
failure: {
|
|
134
|
+
kind: 'error',
|
|
135
|
+
message: `rule list rev changed mid-drain (page rev ${res.rev} ≠ first page rev ${rev}) on a 2xx — refusing to stitch a mixed snapshot into one governance list`,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// 🔴 行级窄化(同轮):撤销承重的两键(`rule` / `scope`,revoke 的按内容身份对)必须是
|
|
140
|
+
// 非空串 —— `rules:[null]` / 缺 scope 的行被展进 `PersistedRule[]`,下游要么渲空白治理项、
|
|
141
|
+
// 要么按 undefined 撤销(什么都对不上)。坏行 = 坏页(丢行会把活规则藏起来,正是本函数
|
|
142
|
+
// 拒绝的病);其余展示键(tool/match/command/adds)不在此过度收紧 —— server additive 演进
|
|
143
|
+
// 不该把整面打红,消费端对展示键自有坏形容忍。
|
|
144
|
+
for (const r of res.rules) {
|
|
145
|
+
const rr = r;
|
|
146
|
+
if (rr === null ||
|
|
147
|
+
typeof rr !== 'object' ||
|
|
148
|
+
typeof rr.rule !== 'string' ||
|
|
149
|
+
rr.rule === '' ||
|
|
150
|
+
typeof rr.scope !== 'string' ||
|
|
151
|
+
rr.scope === '') {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
failure: {
|
|
155
|
+
kind: 'error',
|
|
156
|
+
message: 'rule list page carried a malformed rule row (missing/ill-typed rule or scope) — refusing to present a list that hides or garbles live rules',
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
rev = res.rev;
|
|
162
|
+
rules.push(...res.rules);
|
|
163
|
+
const next = res.nextCursor;
|
|
164
|
+
if (next === undefined)
|
|
165
|
+
return { ok: true, rules, rev };
|
|
166
|
+
if (typeof next !== 'string' || next === '') {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
failure: {
|
|
170
|
+
kind: 'error',
|
|
171
|
+
message: 'rule list page carried a malformed nextCursor (neither absent nor a non-empty string) — refusing to guess where the list ends',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
cursor = next;
|
|
176
|
+
}
|
|
177
|
+
if (!restart) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
failure: {
|
|
181
|
+
kind: 'error',
|
|
182
|
+
message: `rule list did not terminate within ${MAX_PAGES} pages — refusing to present a partial governance list`,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { ok: false, failure: { kind: 'cursor-stale', message: 'rule list kept changing under the cursor — try again' } };
|
|
188
|
+
}
|
|
189
|
+
// ── skipped.reason 分类 ─────────────────────────────────────────────────────────────────────
|
|
190
|
+
/**
|
|
191
|
+
* `skipped[].reason` 的分类。**只按第一个 `:` 前缀**,且必须容得下**没有前缀**的两种真值形
|
|
192
|
+
* (整层 JSON 解不开 / 条目不是串)——SDK 头注逐字:reason 是给人看的散文,不是机读码,
|
|
193
|
+
* 全串等值匹配与「裸码」两种读法都会漂。
|
|
194
|
+
* 前缀形判据刻意保守(`RuleRejectCode` 形:小写 + 点/下划线),防把散文里的第一个冒号误读成码。
|
|
195
|
+
*/
|
|
196
|
+
export function classifySkippedReason(reason) {
|
|
197
|
+
const text = typeof reason === 'string' ? reason : String(reason);
|
|
198
|
+
const idx = text.indexOf(':');
|
|
199
|
+
if (idx <= 0)
|
|
200
|
+
return { text };
|
|
201
|
+
const head = text.slice(0, idx);
|
|
202
|
+
if (!/^[a-z][a-z0-9_]*(\.[a-z0-9_]+)*$/.test(head))
|
|
203
|
+
return { text };
|
|
204
|
+
return { code: head, text };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* respond 回执上「不再询问到底存上了没」的**结构化读口**。
|
|
208
|
+
*
|
|
209
|
+
* 🔴 **单一台账**(A-028.14 收口本体):`rulePersisted` / `ruleRefusal` 的结构窄化只有
|
|
210
|
+
* {@link readToolApprovalRespondAck} 这一份 —— 本函数不对 wire 原料二次开读,ack 过不了包内
|
|
211
|
+
* 结构门(缺 `delivery:'applied'` / `approvalId` / 三词闭集 `decision`)⇒ **`unknown`**:
|
|
212
|
+
* 一张连回执身份都不成形的 ack,不配驱动一行「已保存」的用户告知(诚实缺席优先)。
|
|
213
|
+
* 收口前 cli 的裸读会把 `{rulePersisted:true}` 这类半形对象读成 `persisted` —— 那正是双份台账
|
|
214
|
+
* 各漂各的形,常驻套对这一格有反向钉。
|
|
215
|
+
*
|
|
216
|
+
* 🔴 `rulePersisted` 缺席 ≠ `false`(未带 persistRule 的回决 / 旧 server ⇒ 字段省略);
|
|
217
|
+
* 🔴 规则没存上**从不翻转裁决** —— 200 + `rulePersisted:false` + `ruleRefusal` 是诚实形,
|
|
218
|
+
* 宿主只说「这次放行了,但『不再询问』没存上」,绝不渲成整次审批失败。
|
|
219
|
+
* 🔴 `rule_lane_unavailable` **四种成因共用一格、两种是本卡局限** ⇒ 禁据一帧判断整台部署的车道
|
|
220
|
+
* 在不在(那要看能力位)。
|
|
221
|
+
*/
|
|
222
|
+
export function readRulePersistOutcome(ack) {
|
|
223
|
+
const parsed = readToolApprovalRespondAck(ack);
|
|
224
|
+
if (parsed === undefined)
|
|
225
|
+
return { state: 'unknown' };
|
|
226
|
+
if (parsed.rulePersisted === true)
|
|
227
|
+
return { state: 'persisted' };
|
|
228
|
+
if (parsed.rulePersisted === false) {
|
|
229
|
+
// 空串/非串已在 readToolApprovalRespondAck 降缺席(单一台账),这里只剩「在场即带」。
|
|
230
|
+
return {
|
|
231
|
+
state: 'refused',
|
|
232
|
+
...(typeof parsed.ruleRefusal === 'string' ? { reason: parsed.ruleRefusal } : {}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return { state: 'unknown' };
|
|
236
|
+
}
|
|
@@ -54,6 +54,19 @@ export declare const PLAN_REVIEW_REJECT_LABEL = "No, reject it (keep planning)";
|
|
|
54
54
|
* 负责如实分类,不替调用方做选择。
|
|
55
55
|
*/
|
|
56
56
|
export declare function planReviewDecisionFromAnswer(answer: QuestionAnswer | null | undefined): 'approve' | 'reject' | 'dismissed';
|
|
57
|
+
/**
|
|
58
|
+
* 推代的**门控口**(A-024.4;#244 F1 上收,语义照壳现实现):只有**决断性**作答(approve/
|
|
59
|
+
* reject 标签命中 —— {@link planReviewDecisionFromAnswer} 的官方三值分类)才消费当代 plan 门。
|
|
60
|
+
* Esc/空答对原卡是 dismissal(arm 臂的 dismissed 分支不驱动 decidePlanReview,checkpoint 仍
|
|
61
|
+
* pending)—— 那时推代会把「被关过」翻成「首见」,恰是 A-024.4 在最常见路径(Esc 关卡)上要
|
|
62
|
+
* 保住的区分。消费方 = 宿主 overlay 的 answerAndRelease(respond 成功后调 —— 它是 responder
|
|
63
|
+
* 之外**第二条**决断可见点,原卡与重开卡两条通路对同一张卡各报一次,`notePlanReviewAnswered`
|
|
64
|
+
* 按完整 questionId 去重恰推一代);包内 responder 走 {@link notePlanReviewAnswered} 直报
|
|
65
|
+
* (它们没有 dismissed 臂会到达记账行)。
|
|
66
|
+
*/
|
|
67
|
+
export declare function notePlanReviewAnsweredIfDecisive(questionId: unknown, answer: QuestionAnswer | null | undefined): void;
|
|
68
|
+
/** W1 带 key 变体。 */
|
|
69
|
+
export declare function notePlanReviewAnsweredIfDecisiveFor(sessionKey: string, questionId: unknown, answer: QuestionAnswer | null | undefined): void;
|
|
57
70
|
/** done 帧的 plan_review park 形状(结构性读,别的终态一律 false)。 */
|
|
58
71
|
export declare function isPlanReviewPark(result: unknown): result is {
|
|
59
72
|
taskId: string;
|
|
@@ -108,6 +121,17 @@ export interface ReopenPlanReviewOpts {
|
|
|
108
121
|
* 或候 armPlanReviewApproval 补 sessionKey 位。
|
|
109
122
|
*/
|
|
110
123
|
sessionKey?: string;
|
|
124
|
+
/**
|
|
125
|
+
* **呈现回执模式**(#250 件1 语义;#244 F1 换装位,cli 装配用):在场 ⇒ 本函数转异步 ——
|
|
126
|
+
* publish 之后等宿主呈现面的登记事件(`waitForGateArmed`,键 = 本次发布的**原始帧 id**,
|
|
127
|
+
* 尝试级回执)最多这么多毫秒;回执到手才 `{reopened:true}`,窗尽如实 `{reopened:false}`。
|
|
128
|
+
* 🔴 本模式下重开腿**零登记**:登记时点 = 宿主 overlay 真入队点(`registerArmedGate
|
|
129
|
+
* FromQuestionId`),零回执还登记 = 下一次重开谎称复见(毒化 firstSight)。responder 在
|
|
130
|
+
* 窗尽后**故意保留**:hook 窗超过看门狗才入队的迟到卡必须仍可答(答不了的死卡比迟到更坏)。
|
|
131
|
+
* 缺席 ⇒ 同步路径逐字不变(0.30.0 行为:发布即登记、即回 `{reopened:true}`,像素级回执归
|
|
132
|
+
* 宿主经 `presented` 位补充)。
|
|
133
|
+
*/
|
|
134
|
+
presentationReceiptMs?: number;
|
|
111
135
|
}
|
|
112
136
|
/** 测试钩:清空重开 responder 台账(跨用例状态)。 */
|
|
113
137
|
export declare function _resetActiveReopenRespondersForTest(): void;
|
|
@@ -123,4 +147,10 @@ export declare function _resetActiveReopenRespondersForTest(): void;
|
|
|
123
147
|
* firstSight 判据 = 呈现台账(`armedGateRegistry`,键 = canonical `plan-review:<taskId>`);
|
|
124
148
|
* 先查后记 —— 顺序决定判决正确性。
|
|
125
149
|
*/
|
|
126
|
-
export declare function reopenPlanReviewCard(taskId: string, opts
|
|
150
|
+
export declare function reopenPlanReviewCard(taskId: string, opts: ReopenPlanReviewOpts & {
|
|
151
|
+
presentationReceiptMs: number;
|
|
152
|
+
}): Promise<ReopenCardVerdict>;
|
|
153
|
+
export declare function reopenPlanReviewCard(taskId: string, opts?: ReopenPlanReviewOpts & {
|
|
154
|
+
presentationReceiptMs?: undefined;
|
|
155
|
+
}): ReopenCardVerdict;
|
|
156
|
+
export declare function reopenPlanReviewCard(taskId: string, opts?: ReopenPlanReviewOpts): ReopenCardVerdict | Promise<ReopenCardVerdict>;
|
|
@@ -42,7 +42,7 @@ import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
|
42
42
|
import { engineWireTarget } from '../engineWireTarget.js';
|
|
43
43
|
import { enqueuePlanReviewOutcome } from '../notifications.js';
|
|
44
44
|
import { planReviewQuestionId, REOPEN_ID_TAIL } from './gateIdentity.js';
|
|
45
|
-
import {
|
|
45
|
+
import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, registerArmedGateFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
|
|
46
46
|
import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
47
47
|
/** resume 是 SYNC 驱动到终态的(可能分钟级)——原 raw fetch 无超时;SDK Transport 恒有每请求
|
|
48
48
|
* 超时,取 6h 宽上界(诚实预算:超过它基本是引擎僵死,超时文案与网络失败同走 outcome 通知)。 */
|
|
@@ -72,6 +72,25 @@ export function planReviewDecisionFromAnswer(answer) {
|
|
|
72
72
|
return 'reject';
|
|
73
73
|
return 'dismissed';
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* 推代的**门控口**(A-024.4;#244 F1 上收,语义照壳现实现):只有**决断性**作答(approve/
|
|
77
|
+
* reject 标签命中 —— {@link planReviewDecisionFromAnswer} 的官方三值分类)才消费当代 plan 门。
|
|
78
|
+
* Esc/空答对原卡是 dismissal(arm 臂的 dismissed 分支不驱动 decidePlanReview,checkpoint 仍
|
|
79
|
+
* pending)—— 那时推代会把「被关过」翻成「首见」,恰是 A-024.4 在最常见路径(Esc 关卡)上要
|
|
80
|
+
* 保住的区分。消费方 = 宿主 overlay 的 answerAndRelease(respond 成功后调 —— 它是 responder
|
|
81
|
+
* 之外**第二条**决断可见点,原卡与重开卡两条通路对同一张卡各报一次,`notePlanReviewAnswered`
|
|
82
|
+
* 按完整 questionId 去重恰推一代);包内 responder 走 {@link notePlanReviewAnswered} 直报
|
|
83
|
+
* (它们没有 dismissed 臂会到达记账行)。
|
|
84
|
+
*/
|
|
85
|
+
export function notePlanReviewAnsweredIfDecisive(questionId, answer) {
|
|
86
|
+
notePlanReviewAnsweredIfDecisiveFor(DEFAULT_SESSION_KEY, questionId, answer);
|
|
87
|
+
}
|
|
88
|
+
/** W1 带 key 变体。 */
|
|
89
|
+
export function notePlanReviewAnsweredIfDecisiveFor(sessionKey, questionId, answer) {
|
|
90
|
+
if (planReviewDecisionFromAnswer(answer ?? { answers: [] }) === 'dismissed')
|
|
91
|
+
return;
|
|
92
|
+
notePlanReviewAnsweredFor(sessionKey, questionId);
|
|
93
|
+
}
|
|
75
94
|
/** done 帧的 plan_review park 形状(结构性读,别的终态一律 false)。 */
|
|
76
95
|
export function isPlanReviewPark(result) {
|
|
77
96
|
const r = result;
|
|
@@ -182,12 +201,11 @@ export function armPlanReviewApproval(result) {
|
|
|
182
201
|
// 卡的重开路径把决定权还给用户。呈现台账的键**不清**:同一张卡再被重开就是真「reopened」。
|
|
183
202
|
return { ok: true };
|
|
184
203
|
}
|
|
185
|
-
// A-024.4(
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
clearArmedGate(planReviewQuestionId(taskId));
|
|
204
|
+
// A-024.4(#244 F1 起走分代):决断一经递交,这个门的呈现史就消费掉(当代键清 + 推代);
|
|
205
|
+
// 同 run 的**下一个** plan gate(approve 推进后引擎可再 park 一个新 plan)必须读回首见,
|
|
206
|
+
// 否则文案对一张从未呈现过的新卡说「reopened」。递交后决定未生效的形(RB-471 族)读回
|
|
207
|
+
// 首见只损失「reopened」一词 —— 首见文案零历史断言,诚实方向安全。
|
|
208
|
+
notePlanReviewAnswered(questionId);
|
|
191
209
|
// fire-and-forget:resume 同步驱动到终态可能分钟级,不能挂住 overlay;结果经 queue 通知回来
|
|
192
210
|
void decidePlanReview(taskId, decided);
|
|
193
211
|
return { ok: true };
|
|
@@ -340,31 +358,28 @@ function retireActiveReopen(sessionKey, canonicalId) {
|
|
|
340
358
|
export function _resetActiveReopenRespondersForTest() {
|
|
341
359
|
activeReopenResponders.clear();
|
|
342
360
|
}
|
|
343
|
-
/**
|
|
344
|
-
* 重开某个 parked plan_review 的审批卡(409 自愈分诊树 `attemptActiveRunSelfHeal` 的
|
|
345
|
-
* `reopenPlanReview` 注入口的包内生产实现)。判决形/成文语义见 {@link ReopenCardVerdict}
|
|
346
|
-
* (`adapter/activeRunSelfHeal.ts`);本函数不产 `presented` 位 —— 包看不到像素,回执机制归
|
|
347
|
-
* 宿主端包装。绝不抛(turn 收尾路径)。
|
|
348
|
-
*
|
|
349
|
-
* 与 arm 臂的合成关系(A-028.4):首呈(done 帧)= `armPlanReviewApproval`;重开(409 撞锁)=
|
|
350
|
-
* 本函数。两臂共用同一份题面构造点/标签单源/决断三态判决与同一条 `decidePlanReview` 投递管道;
|
|
351
|
-
* 「铸新 questionId 还是复用同 id」由宿主去重语义作参数(见 {@link ReopenPlanReviewOpts})。
|
|
352
|
-
* firstSight 判据 = 呈现台账(`armedGateRegistry`,键 = canonical `plan-review:<taskId>`);
|
|
353
|
-
* 先查后记 —— 顺序决定判决正确性。
|
|
354
|
-
*/
|
|
355
361
|
export function reopenPlanReviewCard(taskId, opts) {
|
|
362
|
+
const receiptMs = opts?.presentationReceiptMs;
|
|
363
|
+
/** 回执模式下发布之后的收口(订阅必须**先于** publish:总线 handler 同步跑,快 overlay 的
|
|
364
|
+
* 登记可能发生在 publish 返回之前 —— 后订阅会错过已发生的回执)。 */
|
|
365
|
+
const settleOnReceipt = (receipt, firstSight) => receipt.then((presented) => (presented ? { reopened: true, firstSight } : { reopened: false }), () => ({ reopened: false }));
|
|
356
366
|
try {
|
|
357
|
-
if (typeof taskId !== 'string' || taskId.length === 0)
|
|
358
|
-
return { reopened: false };
|
|
367
|
+
if (typeof taskId !== 'string' || taskId.length === 0) {
|
|
368
|
+
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
369
|
+
}
|
|
359
370
|
const sessionKey = opts?.sessionKey ?? DEFAULT_SESSION_KEY;
|
|
360
371
|
// 诚实边界:请求方会话没有 overlay 订阅者(headless / 尚未挂载)⇒ 返回 false,调用方如实说
|
|
361
372
|
// 「没能重开」。overlay 在场答的是「有没有一个 UI 会收到这一帧」;检查按 sessionKey 走 ——
|
|
362
373
|
// 别的会话挂着 overlay 不等于本会话的卡有地方渲(复审 [high]:默认键硬走会把帧投进别人的面)。
|
|
363
|
-
if (!hasQuestionOverlayFor(sessionKey))
|
|
364
|
-
return { reopened: false };
|
|
374
|
+
if (!hasQuestionOverlayFor(sessionKey)) {
|
|
375
|
+
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
376
|
+
}
|
|
365
377
|
const canonicalId = planReviewQuestionId(taskId);
|
|
366
|
-
// 先查(firstSight)后记
|
|
367
|
-
|
|
378
|
+
// 先查(firstSight)后记 —— 顺序决定判决正确性;台账按 sessionKey 分账,键 = **当代** plan 键
|
|
379
|
+
// (A-024.4 分代:0 代与 canonical 字节同形,决断消费后落新代 —— 恒定键会对同 run 的下一只
|
|
380
|
+
// plan 门谎报「被关过」)。
|
|
381
|
+
const armedKey = planReviewArmedKeyFor(sessionKey, taskId);
|
|
382
|
+
const firstSight = !wasGateArmedFor(sessionKey, armedKey);
|
|
368
383
|
const mintFresh = opts?.mintFreshQuestionId !== false;
|
|
369
384
|
if (!mintFresh && hasLocalQuestionResponder(canonicalId)) {
|
|
370
385
|
// 重呈短路(与 arm 臂的重放短路同形):同一个 canonical 身份、同一份题面、同一个 responder。
|
|
@@ -376,12 +391,21 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
376
391
|
}
|
|
377
392
|
// 先退役本会话名下还在场的**重开**卡(如此前走过 mintFresh 臂)—— 同一 gate 不留两张活卡。
|
|
378
393
|
retireActiveReopen(sessionKey, canonicalId);
|
|
394
|
+
if (receiptMs !== undefined) {
|
|
395
|
+
const receipt = waitForGateArmedFor(sessionKey, [canonicalId], receiptMs);
|
|
396
|
+
publishQuestionFrameFor(sessionKey, {
|
|
397
|
+
type: 'question',
|
|
398
|
+
questionId: canonicalId,
|
|
399
|
+
questions: planReviewQuestions(ARM_QUESTION),
|
|
400
|
+
});
|
|
401
|
+
return settleOnReceipt(receipt, firstSight);
|
|
402
|
+
}
|
|
379
403
|
publishQuestionFrameFor(sessionKey, {
|
|
380
404
|
type: 'question',
|
|
381
405
|
questionId: canonicalId,
|
|
382
406
|
questions: planReviewQuestions(ARM_QUESTION),
|
|
383
407
|
});
|
|
384
|
-
registerArmedGateFor(sessionKey,
|
|
408
|
+
registerArmedGateFor(sessionKey, armedKey);
|
|
385
409
|
return { reopened: true, firstSight };
|
|
386
410
|
}
|
|
387
411
|
// 复审 [high] 单活纪律(两轮收紧):铸新卡前退役**全部**旧作答入口 —— 同一个 gate 至多一张
|
|
@@ -417,8 +441,10 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
417
441
|
// 这张卡再被重开就是真「reopened」。
|
|
418
442
|
return { ok: true };
|
|
419
443
|
}
|
|
420
|
-
// A-024.4:决断递交即消费呈现史(与 arm 臂 responder
|
|
421
|
-
|
|
444
|
+
// A-024.4(#244 F1 起走分代):决断递交即消费呈现史(与 arm 臂 responder 同点;overlay 的
|
|
445
|
+
// answerAndRelease 对同一张卡也会经 notePlanReviewAnsweredIfDecisive 报一次,按完整
|
|
446
|
+
// questionId 去重,不双推)。
|
|
447
|
+
notePlanReviewAnsweredFor(sessionKey, questionId);
|
|
422
448
|
try {
|
|
423
449
|
await deliver(taskId, decided);
|
|
424
450
|
}
|
|
@@ -430,7 +456,19 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
430
456
|
return { ok: true };
|
|
431
457
|
});
|
|
432
458
|
activeReopenResponders.set(activeKey, { questionId, unregister });
|
|
433
|
-
|
|
459
|
+
if (receiptMs !== undefined) {
|
|
460
|
+
// 回执模式(#250 件1):零登记(登记时点 = 宿主 overlay 真入队点)、订阅先于 publish、
|
|
461
|
+
// 回执键 = 原始帧 id(尝试级回执 —— 同 gate 并发重开不同键互唤);窗尽如实 false,
|
|
462
|
+
// responder 故意保留(迟到卡必须仍可答,决断腿照走)。
|
|
463
|
+
const receipt = waitForGateArmedFor(sessionKey, [questionId], receiptMs);
|
|
464
|
+
publishQuestionFrameFor(sessionKey, {
|
|
465
|
+
type: 'question',
|
|
466
|
+
questionId,
|
|
467
|
+
questions: planReviewQuestions(REOPEN_QUESTION),
|
|
468
|
+
});
|
|
469
|
+
return settleOnReceipt(receipt, firstSight);
|
|
470
|
+
}
|
|
471
|
+
registerArmedGateFor(sessionKey, armedKey);
|
|
434
472
|
publishQuestionFrameFor(sessionKey, {
|
|
435
473
|
type: 'question',
|
|
436
474
|
questionId,
|
|
@@ -439,6 +477,7 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
439
477
|
return { reopened: true, firstSight };
|
|
440
478
|
}
|
|
441
479
|
catch {
|
|
442
|
-
|
|
480
|
+
// 重开失败绝不炸 turn —— 调用方会如实说「没能重开」(回执模式回同形的 resolved promise)。
|
|
481
|
+
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
443
482
|
}
|
|
444
483
|
}
|
|
@@ -218,8 +218,11 @@ export interface ApprovalCardRequest {
|
|
|
218
218
|
* → 决断带 {@link ApprovalCardAllowDecision.persistRule} 回兑」,兑付口=同副本活卡腿
|
|
219
219
|
* `respond.persistRule`;而 durable 腿的 `/decide` 体**无规则位**(SDK 6.17.2 PendingCheckpoint
|
|
220
220
|
* JSDoc 逐字:display/triage-only)——把行上候选落进那一位,就是一个按下去规则不落地的假
|
|
221
|
-
* affordance
|
|
222
|
-
*
|
|
221
|
+
* affordance。本键的不变量是**决断字节**:决断绝不因它带 `persistRule`(或任何规则位)——
|
|
222
|
+
* 「只读」限定的是 wire 回兑通道,不是屏面。渲成可选中项是**允许**的,前提=兑付走客户端
|
|
223
|
+
* **本地**落规则(cli 1.0.76 起的形:选中 → 客户端写自己的 settings `permissions.allow` 并把
|
|
224
|
+
* 成败如实上屏,决断仍与两态时代逐字节相同;这正是 CC 第三态的原形——本地文件写)。
|
|
225
|
+
* 缺席 = 无候选/老行/坏形(三者同形,不猜);既有卡口不读本键 ⇒ 卡形字节不变。
|
|
223
226
|
*/
|
|
224
227
|
ruleSuggestionsReadOnly?: RuleSuggestion[];
|
|
225
228
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -228,6 +228,8 @@ export * from './hitl/parkRowBirthWait.js';
|
|
|
228
228
|
export * from './hitl/approvalDecisionNoteAudit.js';
|
|
229
229
|
export * from './hitl/askParkRowRouting.js';
|
|
230
230
|
export * from './hitl/resumeRunningCard.js';
|
|
231
|
+
export * from './hitl/persistedRulesWire.js';
|
|
232
|
+
export * from './hitl/localAllowRule.js';
|
|
231
233
|
export * from './hitl/approvalsFeed.js';
|
|
232
234
|
export * from './compensations.js';
|
|
233
235
|
export * from './request/printNotification.js';
|