@sema-agent/client-core 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +400 -0
- package/README.md +19 -2
- package/dist/adapt/arms.js +24 -1
- package/dist/adapt/wireShapes.d.ts +7 -0
- package/dist/adapt/wireShapes.js +7 -0
- package/dist/adapter/activeRunSelfHeal.d.ts +407 -0
- package/dist/adapter/activeRunSelfHeal.js +771 -0
- package/dist/adapter/runStream.d.ts +23 -6
- package/dist/adapter/runStream.js +19 -5
- package/dist/engineWireSdk.d.ts +10 -2
- package/dist/engineWireSdk.js +7 -3
- package/dist/hitl/approvalDecisionNoteAudit.d.ts +58 -0
- package/dist/hitl/approvalDecisionNoteAudit.js +91 -0
- package/dist/hitl/armedGateRegistry.d.ts +19 -0
- package/dist/hitl/armedGateRegistry.js +92 -0
- package/dist/hitl/askParkRowRouting.d.ts +150 -0
- package/dist/hitl/askParkRowRouting.js +183 -0
- package/dist/hitl/frameRouter.d.ts +5 -1
- package/dist/hitl/frameRouter.js +1 -1
- package/dist/hitl/gateIdentity.d.ts +50 -0
- package/dist/hitl/gateIdentity.js +64 -0
- package/dist/hitl/hitlBridge.d.ts +7 -0
- package/dist/hitl/hitlBridge.js +11 -2
- package/dist/hitl/parkOwnership.d.ts +65 -0
- package/dist/hitl/parkOwnership.js +49 -0
- package/dist/hitl/parkResolver.js +3 -1
- package/dist/hitl/parkRowBirthWait.d.ts +63 -0
- package/dist/hitl/parkRowBirthWait.js +192 -0
- package/dist/hitl/planReviewWire.d.ts +51 -0
- package/dist/hitl/planReviewWire.js +172 -17
- package/dist/hitl/resumeRunningCard.d.ts +134 -0
- package/dist/hitl/resumeRunningCard.js +177 -0
- package/dist/hitl/toolApprovalWire.d.ts +69 -23
- package/dist/hitl/toolApprovalWire.js +60 -24
- package/dist/index.d.ts +9 -0
- package/dist/index.js +34 -0
- package/dist/liveQuestionStore.d.ts +11 -0
- package/dist/liveQuestionStore.js +13 -0
- package/dist/seatContract.d.ts +27 -0
- package/dist/seatContract.js +42 -0
- package/dist/subagent/engineSubagentTail.d.ts +0 -2
- package/dist/subagent/engineSubagentTail.js +7 -15
- package/dist/subagentContentStore.d.ts +58 -2
- package/dist/subagentContentStore.js +95 -6
- package/dist/toolResult.d.ts +26 -0
- package/dist/toolResult.js +38 -6
- package/dist/workflowClient.d.ts +6 -1
- package/docs/INTEGRATION-CLIENTS.md +844 -0
- package/docs/REFACTOR-LEDGER.md +392 -0
- package/package.json +7 -4
|
@@ -33,22 +33,39 @@
|
|
|
33
33
|
import type { AgentEvent } from '@sema-agent/sdk';
|
|
34
34
|
import { type SDKMessage, type EmitContext, type ModelUsage } from './types.js';
|
|
35
35
|
import type { EngineTurnUsage } from './downstream/turnUsageToModelUsage.js';
|
|
36
|
+
/**
|
|
37
|
+
* 409 body 的 pendingGate 材料(A-028.1,2026-08-12 具名化并补 `governanceForced` 位 ——
|
|
38
|
+
* 此前包侧只有 {kind, decidePath} 两位,壳侧抄件已多出该位 = 同一 wire 位两份解析器形不同,
|
|
39
|
+
* 以超集形归一进包)。
|
|
40
|
+
*/
|
|
41
|
+
export interface ActiveRunPendingGate {
|
|
42
|
+
/** 在等哪一种门;引擎对未知 kind 自己返 null。 */
|
|
43
|
+
readonly kind: string | null;
|
|
44
|
+
/** 兑现那个决定的**真路由**(wire 给的,消费方绝不按 kind 硬编码一张表)。 */
|
|
45
|
+
readonly decidePath: string | null;
|
|
46
|
+
/**
|
|
47
|
+
* 这道门的**出身**是运维治理层(AUTONOMY / commandPolicy / 守卫集)强制的
|
|
48
|
+
* (server ≥7.12 / SDK 6.15.0 `PendingGateMaterial`,ADDITIVE)。
|
|
49
|
+
* 🔴 **只在为真时在场,server 恒不写 false**;缺席 ≠「这不是治理门」,只是「没有治理来源的
|
|
50
|
+
* 证据」。消费方只做**在场才说**的加法:纯归因一句,不参与任何分诊/动作。
|
|
51
|
+
*/
|
|
52
|
+
readonly governanceForced?: true;
|
|
53
|
+
}
|
|
36
54
|
/** #114/[C65] 单源化:三端共用的 409 active-run 拒收终帧判别(富信号形,包级唯一权威)。
|
|
37
|
-
* 与壳侧抄件(cli `src/sema/activeRunSelfHeal.ts`)同义——0.13.0 到货后壳换包导入删本地抄件。
|
|
38
55
|
* 🔧 2026-08-02 事实纠正([C77]③):原文写的是「与壳/desktop **各自**照抄件同义……各端换包
|
|
39
56
|
* 导入删本地抄件」,而 desktop 全树扫描(`activeRunBusySignal` / `session_active_run` /
|
|
40
57
|
* `activeTaskId` 的 409 判别族)**零命中** —— 那边根本没有抄件,「各端」是句不实的话。
|
|
41
|
-
* 删掉不实的那半:今天的抄件只有壳一份。
|
|
58
|
+
* 删掉不实的那半:今天的抄件只有壳一份。
|
|
59
|
+
* 🔧 A-028.1(2026-08-12):抄件漂移收口 —— 壳侧那份的 pendingGate 已多出 `governanceForced`
|
|
60
|
+
* 位,本形按超集归一(见 {@link ActiveRunPendingGate});分诊树/结局文案层同批落
|
|
61
|
+
* `adapter/activeRunSelfHeal.ts`,壳侧换包导入在提货批。 */
|
|
42
62
|
export interface ActiveRunBusySignal {
|
|
43
63
|
/** 占锁 run 的 id;wire 没带 ⇒ null(诚实缺席,绝不铸造)。 */
|
|
44
64
|
readonly activeTaskId: string | null;
|
|
45
65
|
/** server 3.21+ 真发的占锁 run 状态;缺席 ⇒ null(由调用方走 runs.get 一级)。 */
|
|
46
66
|
readonly activeTaskStatus: string | null;
|
|
47
67
|
/** 409 body 的 pendingGate(wire 给的真路由,不自造);缺席 ⇒ null。 */
|
|
48
|
-
readonly pendingGate:
|
|
49
|
-
readonly kind: string | null;
|
|
50
|
-
readonly decidePath: string | null;
|
|
51
|
-
} | null;
|
|
68
|
+
readonly pendingGate: ActiveRunPendingGate | null;
|
|
52
69
|
}
|
|
53
70
|
/**
|
|
54
71
|
* 判别一个**原始 AgentEvent** 是不是 409 active-run 拒收终帧;非 busy ⇒ null。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { eventSeq, } from './types.js';
|
|
2
2
|
import { eventToSdkMessage, turnEndUsage } from './downstream/eventToSdkMessage.js';
|
|
3
3
|
import { terminalToSdkResult } from './downstream/terminalToSdkResult.js';
|
|
4
|
-
import { publishSubagentContentEvent } from '../subagentContentStore.js';
|
|
4
|
+
import { coerceOutput, publishSubagentContentEvent } from '../subagentContentStore.js';
|
|
5
5
|
/**
|
|
6
6
|
* 409 session-busy 拒绝的 **canonical errorCode**([2377]C-1,server main `049ff2c`,随 5.0.0 发)。
|
|
7
7
|
* 引擎把它 stamp 在 `done{status:'failed'}` / `failed` 终帧上,壳据此**结构判读**,不再读人话。
|
|
@@ -48,9 +48,13 @@ export function activeRunBusySignal(ev) {
|
|
|
48
48
|
if (typeof rawGate === 'object' && rawGate !== null) {
|
|
49
49
|
const k = rawGate.kind;
|
|
50
50
|
const d = rawGate.decidePath;
|
|
51
|
+
// 出身位(A-028.1):**只认严格 true**。任何别的形(false / 'true' / 1 / 缺席)一律不置键 ——
|
|
52
|
+
// 键在场即渲徽标类文案,把一个含糊值读成「治理强制」= 对用户下一个证不出的断言。
|
|
53
|
+
const g = rawGate.governanceForced;
|
|
51
54
|
pendingGate = {
|
|
52
55
|
kind: typeof k === 'string' && k.length > 0 ? k : null,
|
|
53
56
|
decidePath: typeof d === 'string' && d.length > 0 ? d : null,
|
|
57
|
+
...(g === true ? { governanceForced: true } : {}),
|
|
54
58
|
};
|
|
55
59
|
}
|
|
56
60
|
return {
|
|
@@ -222,14 +226,24 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
222
226
|
const sub = ev;
|
|
223
227
|
publishSubagentContentEvent({
|
|
224
228
|
type: sub.type,
|
|
225
|
-
//
|
|
229
|
+
// 🔴 EventIdentity 上**没有** `taskId`(sdk events.d.ts 的 `interface EventIdentity` 只有
|
|
230
|
+
// eventId / parentToolCallId,加 LIVE 白名单四臂的 sourceTaskId / bgAgentId;`taskId` 只长在
|
|
231
|
+
// `meta` 首帧)⇒ 生产上这里恒走右臂,归账键由 store 的 `parentToTask` 补(canonicalKey)。
|
|
232
|
+
// `??` 保留是**容将来**:哪天上游真在内容帧上发 taskId,这条直接认,不必改形。
|
|
233
|
+
// (0.30.0 发包扫描订正:此前这行注释写「taskId is on every subagent event」,与 store 侧
|
|
234
|
+
// `canonicalKey` 的注释互相矛盾,且被 .d.ts 直接证伪。)
|
|
226
235
|
taskId: sub.taskId ?? sub.parentToolCallId,
|
|
227
236
|
parentToolCallId: sub.parentToolCallId,
|
|
228
237
|
delta: sub.delta,
|
|
229
238
|
toolCallId: sub.toolCallId,
|
|
230
239
|
toolName: sub.toolName,
|
|
231
240
|
args: sub.args,
|
|
232
|
-
|
|
241
|
+
// #158 移交①([3674](d) 姊妹病,2026-08-12):此前是 `typeof sub.output === 'string' ?
|
|
242
|
+
// sub.output : undefined` —— 而 wire 的 `tool_end.output` 是非均匀的(块数组形合法),
|
|
243
|
+
// 于是子代 lane 的块数组 output 经本臂进内容账本**恒空**(查看态卡有工具、结果栏永远空白)。
|
|
244
|
+
// 换用 store 自己的那个唯一字符串化口(tail 腿 engineSubagentTail 用的同一份):两条腿喂
|
|
245
|
+
// 同一个账本,字符串化口就不能有第二份。
|
|
246
|
+
output: coerceOutput(sub.output),
|
|
233
247
|
isError: sub.isError,
|
|
234
248
|
});
|
|
235
249
|
continue;
|
|
@@ -311,8 +325,8 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
311
325
|
// 像 provider 故障,是误导。识别该终帧 → 一行专属文案(告知在等什么 + 出路);其它错误保持
|
|
312
326
|
// 原 API Error 行(未知错误 fallback 不变)。引擎侧解锁腿已到货([868]:cancel 对
|
|
313
327
|
// suspended/needs_review 就地终态化 ⇒ claim 释放),所以文案指的就是那个端点;
|
|
314
|
-
//
|
|
315
|
-
//
|
|
328
|
+
// 自愈分诊腿(重开卡把决定权还给用户;cancel+重发臂已退役)在 `adapter/activeRunSelfHeal.ts`
|
|
329
|
+
// (A-028.1 上收,宿主经 deps 注入重开口),本层只负责:识别 + 说真话。
|
|
316
330
|
const activeTaskId = failedResult?.activeTaskId ??
|
|
317
331
|
(ev.type === 'failed' ? ev.activeTaskId : undefined);
|
|
318
332
|
// REF-CC-054:判别**结构位两腿**(errorCode → activeTaskId;文案兜底腿 2026-08-03 已退役),
|
package/dist/engineWireSdk.d.ts
CHANGED
|
@@ -67,8 +67,16 @@ export type EngineProbeOpts = {
|
|
|
67
67
|
};
|
|
68
68
|
export type EngineWireClientConfig = {
|
|
69
69
|
baseUrl: string;
|
|
70
|
-
/** 真 token 串;缺省经 wireAuthTokenFor 三态解析(loopback-unauthed / 'anon')。
|
|
71
|
-
|
|
70
|
+
/** 真 token 串;缺省经 wireAuthTokenFor 三态解析(loopback-unauthed / 'anon')。
|
|
71
|
+
*
|
|
72
|
+
* `{ mode: 'same-origin-relay' }`([C175],0.29.0)= **浏览器同源宿主的显式声明形**:凭证由
|
|
73
|
+
* 同源反代(cookie/session)承载,出站零 Authorization 头;它是 SDK 浏览器守卫的**唯一**豁免形
|
|
74
|
+
* (AgentClient 在浏览器宿主拒绝 token/loopback-unauthed/'anon' 三态构造),且该形下 `baseUrl`
|
|
75
|
+
* 允许相对/同源路径(`''`、`'/api'`)。🔴 这个形**只能显式传入**(作者声明「我部署在同源反代后」),
|
|
76
|
+
* 绝不由 `resolveWireAuth` 三态解析推导出来 —— 解析口的三态语义一字不动,relay 形直传 SDK。 */
|
|
77
|
+
token?: string | {
|
|
78
|
+
mode: 'same-origin-relay';
|
|
79
|
+
};
|
|
72
80
|
/** 缺席/undefined=不发 x-agent-principal 头(owner-null;F-011 停发,replEntry live 车道同闸口
|
|
73
81
|
* 语义;显式 `| undefined` 让 EngineWireTarget.principal 直传合法——exactOptionalPropertyTypes)。 */
|
|
74
82
|
principal?: string | undefined;
|
package/dist/engineWireSdk.js
CHANGED
|
@@ -72,15 +72,19 @@ export function wireAuthTokenFor(baseUrl, token) {
|
|
|
72
72
|
*/
|
|
73
73
|
export function makeEngineWireClient(cfg) {
|
|
74
74
|
try {
|
|
75
|
-
|
|
75
|
+
const base = {
|
|
76
76
|
baseUrl: cfg.baseUrl,
|
|
77
|
-
authToken: resolveWireAuth(cfg.baseUrl, cfg.token),
|
|
78
77
|
// F-011 停发:缺席/空串=不给键(SDK 6.11 缺席=不发 x-agent-principal 头,owner-null)。
|
|
79
78
|
...(cfg.principal !== undefined && cfg.principal !== '' ? { principal: cfg.principal } : {}),
|
|
80
79
|
...(cfg.timeoutMs !== undefined ? { timeoutMs: cfg.timeoutMs } : {}),
|
|
81
80
|
maxRetries: cfg.maxRetries ?? 0,
|
|
82
81
|
...(cfg.fetchImpl ? { fetch: cfg.fetchImpl } : {}),
|
|
83
|
-
}
|
|
82
|
+
};
|
|
83
|
+
// relay 形直传(显式声明,不过三态解析);串/缺席走 resolveWireAuth 三态,语义与 0.28.x 字节
|
|
84
|
+
// 不变。分支构造而非三元合流:SDK AgentClientConfig 按 authToken 判别联合,联合值不可直赋。
|
|
85
|
+
return typeof cfg.token === 'object'
|
|
86
|
+
? new AgentClient({ ...base, authToken: cfg.token })
|
|
87
|
+
: new AgentClient({ ...base, authToken: resolveWireAuth(cfg.baseUrl, cfg.token) });
|
|
84
88
|
}
|
|
85
89
|
catch {
|
|
86
90
|
return null;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hitl/approvalDecisionNoteAudit.ts — 审批**回决备注**(`decisionNote` / `noteRecorded`)的读面
|
|
3
|
+
* 判定 + 人话行(#265 上收件,2026-08-14;源形 = cli `src/sema/approvalDecisionNoteAudit.ts`,
|
|
4
|
+
* 供给半场 = server 7.15.0 的 `AskDecisionAck.decisionNote` / `ToolApprovalRespondAck.noteRecorded`)。
|
|
5
|
+
*
|
|
6
|
+
* ── 病 ────────────────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
* 送出去的那条 deny 理由到底有没有落进审计档,客户端这边**一个字都不知道**:决断腿此前是
|
|
8
|
+
* `await decide…` 裸调,回体整个被丢掉。于是「我写了拒绝理由」与「那条理由真的进了审计行」之间
|
|
9
|
+
* 没有任何证据 —— deny 归因链在用户这一端是断的。
|
|
10
|
+
*
|
|
11
|
+
* ── 三态,不是两态 ────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
* server 对这一位的契约(SDK `ToolApprovalRespondAck.noteRecorded` / `AskDecisionAck.decisionNote`
|
|
13
|
+
* 头注逐字)是**三态**,把它压成布尔就会造谎:
|
|
14
|
+
* · `recorded` —— `noteRecorded === true`,**或** durable ack 回显了 `decisionNote` 正文
|
|
15
|
+
* (契约:「行上有才发」⇒ 回显本身就是落行的证据);
|
|
16
|
+
* · `not-recorded` —— `noteRecorded === false`。这是店的**真实结果**(并发歧义臂的输家如实 false),
|
|
17
|
+
* 它说的是「理由没落档」,**不是**「审批失败」—— 决断照旧成立,渲染面必须
|
|
18
|
+
* 在同一行里把这句说出来,否则用户会以为自己按的那个拒绝没生效;
|
|
19
|
+
* · `unknown` —— 两个位都不在场(旧 server / 这台部署没有 durable ask 账本 / 本次压根没送 note)。
|
|
20
|
+
* ⇒ **整行不渲**。缺席≠false —— 渲一行「未记录」等于替引擎回答一个它没回答
|
|
21
|
+
* 的问题。
|
|
22
|
+
*
|
|
23
|
+
* 🔴 `noteRecorded === false` **压过**正文回显:显式的 per-call 真相优先。两者同时在场的形
|
|
24
|
+
* (并发歧义臂:行上留的是赢家那条备注,我这条没落)只有这样才说得准 —— 正文仍带出来当归因
|
|
25
|
+
* 材料(用户看得见「行上现在是哪条」),但状态词按 false 走。
|
|
26
|
+
*
|
|
27
|
+
* ── 🔴 归层 ───────────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
* 本文件 = **纯判定 + 文案**,`import` 列表为空(常驻门 ④ 段逐次对账)。呈现口不在这里 ——
|
|
29
|
+
* 装配层把行交给宿主的通知/转录面。三端(cli / desktop / web)撞的是同一件事,读的是同一份 ack。
|
|
30
|
+
*
|
|
31
|
+
* ── UNTRUSTED ─────────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
* `decisionNote` 的正文是**卡口自由文本**原样回声(用户自己打的拒绝理由,也可能是别的客户端打的)。
|
|
33
|
+
* 读面就地有界截断 + 清洗控制字符 —— 只渲染,永不回喂模型,也绝不整段进日志。
|
|
34
|
+
*/
|
|
35
|
+
/** 审计面通知的独占 key —— 与审批流告警面分格(两件事不抢同一格)。 */
|
|
36
|
+
export declare const DECISION_NOTE_NOTICE_KEY = "approval-decision-note";
|
|
37
|
+
/** 备注面的三态判决。`note` = 引擎回显的正文(缺席 = 引擎没回显,**不是**「没有备注」)。 */
|
|
38
|
+
export interface DecisionNoteAudit {
|
|
39
|
+
state: 'recorded' | 'not-recorded' | 'unknown';
|
|
40
|
+
note?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 读一份回体(durable `AskDecisionAck` / legacy `ToolApprovalRespondAck` / 409
|
|
44
|
+
* `conflict.ask_decided` 体)里的备注面判决。
|
|
45
|
+
*
|
|
46
|
+
* 🔴 **在场即读、不在场即 unknown**:两个位在 SDK 类型上都是可选的,而「类型说 optional」与
|
|
47
|
+
* 「这台引擎真的发了」是两件事 —— 一律结构窄读,读不出就是缺席,绝不合成。
|
|
48
|
+
*/
|
|
49
|
+
export declare function readDecisionNoteAudit(ack: unknown): DecisionNoteAudit;
|
|
50
|
+
/**
|
|
51
|
+
* 人话行。**`unknown` ⇒ `null`(整行不渲)** —— 这条纪律不可削。
|
|
52
|
+
*
|
|
53
|
+
* @param opts.settledElsewhere 这份材料来自 409 `conflict.ask_decided`(别的客户端/legacy 腿先结算)
|
|
54
|
+
* ⇒ 行里要点明归因来源,否则用户会把首决那条备注当成自己刚写的那条。
|
|
55
|
+
*/
|
|
56
|
+
export declare function decisionNoteAuditLine(audit: DecisionNoteAudit, opts?: {
|
|
57
|
+
settledElsewhere?: boolean;
|
|
58
|
+
}): string | null;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hitl/approvalDecisionNoteAudit.ts — 审批**回决备注**(`decisionNote` / `noteRecorded`)的读面
|
|
3
|
+
* 判定 + 人话行(#265 上收件,2026-08-14;源形 = cli `src/sema/approvalDecisionNoteAudit.ts`,
|
|
4
|
+
* 供给半场 = server 7.15.0 的 `AskDecisionAck.decisionNote` / `ToolApprovalRespondAck.noteRecorded`)。
|
|
5
|
+
*
|
|
6
|
+
* ── 病 ────────────────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
* 送出去的那条 deny 理由到底有没有落进审计档,客户端这边**一个字都不知道**:决断腿此前是
|
|
8
|
+
* `await decide…` 裸调,回体整个被丢掉。于是「我写了拒绝理由」与「那条理由真的进了审计行」之间
|
|
9
|
+
* 没有任何证据 —— deny 归因链在用户这一端是断的。
|
|
10
|
+
*
|
|
11
|
+
* ── 三态,不是两态 ────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
* server 对这一位的契约(SDK `ToolApprovalRespondAck.noteRecorded` / `AskDecisionAck.decisionNote`
|
|
13
|
+
* 头注逐字)是**三态**,把它压成布尔就会造谎:
|
|
14
|
+
* · `recorded` —— `noteRecorded === true`,**或** durable ack 回显了 `decisionNote` 正文
|
|
15
|
+
* (契约:「行上有才发」⇒ 回显本身就是落行的证据);
|
|
16
|
+
* · `not-recorded` —— `noteRecorded === false`。这是店的**真实结果**(并发歧义臂的输家如实 false),
|
|
17
|
+
* 它说的是「理由没落档」,**不是**「审批失败」—— 决断照旧成立,渲染面必须
|
|
18
|
+
* 在同一行里把这句说出来,否则用户会以为自己按的那个拒绝没生效;
|
|
19
|
+
* · `unknown` —— 两个位都不在场(旧 server / 这台部署没有 durable ask 账本 / 本次压根没送 note)。
|
|
20
|
+
* ⇒ **整行不渲**。缺席≠false —— 渲一行「未记录」等于替引擎回答一个它没回答
|
|
21
|
+
* 的问题。
|
|
22
|
+
*
|
|
23
|
+
* 🔴 `noteRecorded === false` **压过**正文回显:显式的 per-call 真相优先。两者同时在场的形
|
|
24
|
+
* (并发歧义臂:行上留的是赢家那条备注,我这条没落)只有这样才说得准 —— 正文仍带出来当归因
|
|
25
|
+
* 材料(用户看得见「行上现在是哪条」),但状态词按 false 走。
|
|
26
|
+
*
|
|
27
|
+
* ── 🔴 归层 ───────────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
* 本文件 = **纯判定 + 文案**,`import` 列表为空(常驻门 ④ 段逐次对账)。呈现口不在这里 ——
|
|
29
|
+
* 装配层把行交给宿主的通知/转录面。三端(cli / desktop / web)撞的是同一件事,读的是同一份 ack。
|
|
30
|
+
*
|
|
31
|
+
* ── UNTRUSTED ─────────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
* `decisionNote` 的正文是**卡口自由文本**原样回声(用户自己打的拒绝理由,也可能是别的客户端打的)。
|
|
33
|
+
* 读面就地有界截断 + 清洗控制字符 —— 只渲染,永不回喂模型,也绝不整段进日志。
|
|
34
|
+
*/
|
|
35
|
+
/** 备注面在渲染时的正文上界(一行 footer 通知,不是转录面正文)。 */
|
|
36
|
+
const NOTE_DISPLAY_MAX = 200;
|
|
37
|
+
/** 审计面通知的独占 key —— 与审批流告警面分格(两件事不抢同一格)。 */
|
|
38
|
+
export const DECISION_NOTE_NOTICE_KEY = 'approval-decision-note';
|
|
39
|
+
/**
|
|
40
|
+
* 控制字符/换行清洗 + 有界截断(UNTRUSTED 正文只走这一个口)。
|
|
41
|
+
* 🔴 换行必须折平:footer 通知是**单行**面,一条带换行的备注会把整段排版撕开。
|
|
42
|
+
*/
|
|
43
|
+
function cleanNote(raw) {
|
|
44
|
+
const flat = raw
|
|
45
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]+/g, ' ')
|
|
46
|
+
.replace(/ {2,}/g, ' ')
|
|
47
|
+
.trim();
|
|
48
|
+
return flat.length <= NOTE_DISPLAY_MAX ? flat : `${flat.slice(0, NOTE_DISPLAY_MAX - 1)}…`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 读一份回体(durable `AskDecisionAck` / legacy `ToolApprovalRespondAck` / 409
|
|
52
|
+
* `conflict.ask_decided` 体)里的备注面判决。
|
|
53
|
+
*
|
|
54
|
+
* 🔴 **在场即读、不在场即 unknown**:两个位在 SDK 类型上都是可选的,而「类型说 optional」与
|
|
55
|
+
* 「这台引擎真的发了」是两件事 —— 一律结构窄读,读不出就是缺席,绝不合成。
|
|
56
|
+
*/
|
|
57
|
+
export function readDecisionNoteAudit(ack) {
|
|
58
|
+
if (ack === null || typeof ack !== 'object')
|
|
59
|
+
return { state: 'unknown' };
|
|
60
|
+
const o = ack;
|
|
61
|
+
const note = typeof o.decisionNote === 'string' && o.decisionNote.trim() !== '' ? cleanNote(o.decisionNote) : undefined;
|
|
62
|
+
// 显式 per-call 真相优先(见头注)。
|
|
63
|
+
if (o.noteRecorded === false)
|
|
64
|
+
return { state: 'not-recorded', ...(note !== undefined ? { note } : {}) };
|
|
65
|
+
if (o.noteRecorded === true)
|
|
66
|
+
return { state: 'recorded', ...(note !== undefined ? { note } : {}) };
|
|
67
|
+
// `noteRecorded` 是非布尔垃圾值(旧 server / 代理改写)⇒ 当它不在场,只看正文回显。
|
|
68
|
+
if (note !== undefined)
|
|
69
|
+
return { state: 'recorded', note };
|
|
70
|
+
return { state: 'unknown' };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 人话行。**`unknown` ⇒ `null`(整行不渲)** —— 这条纪律不可削。
|
|
74
|
+
*
|
|
75
|
+
* @param opts.settledElsewhere 这份材料来自 409 `conflict.ask_decided`(别的客户端/legacy 腿先结算)
|
|
76
|
+
* ⇒ 行里要点明归因来源,否则用户会把首决那条备注当成自己刚写的那条。
|
|
77
|
+
*/
|
|
78
|
+
export function decisionNoteAuditLine(audit, opts) {
|
|
79
|
+
if (audit.state === 'unknown')
|
|
80
|
+
return null;
|
|
81
|
+
const quoted = audit.note !== undefined ? `: "${audit.note}"` : '';
|
|
82
|
+
if (opts?.settledElsewhere === true) {
|
|
83
|
+
// 首决备注只在 recorded 侧有意义(false = 我这条没落,但行上仍是别人那条)。
|
|
84
|
+
return audit.state === 'recorded'
|
|
85
|
+
? `this approval was already decided elsewhere — the reason recorded on the audit trail${quoted}`
|
|
86
|
+
: `this approval was already decided elsewhere; your reason was not saved to the audit trail — that decision stands${quoted}`;
|
|
87
|
+
}
|
|
88
|
+
return audit.state === 'recorded'
|
|
89
|
+
? `decision reason recorded on the audit trail${quoted}`
|
|
90
|
+
: `your decision stands — the engine did not save its reason to the audit trail${quoted}`;
|
|
91
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** 登记一个已呈现的决断卡身份键(空/非法输入静默忽略 —— 登记面绝不炸渲染链)。 */
|
|
2
|
+
export declare function registerArmedGate(key: string | null | undefined): void;
|
|
3
|
+
/** W1 带 key 变体(多会话宿主每会话一键,互不串账)。 */
|
|
4
|
+
export declare function registerArmedGateFor(sessionKey: string, key: string | null | undefined): void;
|
|
5
|
+
/** 该身份键的卡在本进程本会话呈现过吗?(false = 首见;文案分形的唯一判据) */
|
|
6
|
+
export declare function wasGateArmed(key: string | null | undefined): boolean;
|
|
7
|
+
/** W1 带 key 变体。 */
|
|
8
|
+
export declare function wasGateArmedFor(sessionKey: string, key: string | null | undefined): boolean;
|
|
9
|
+
/** 消费一个身份键(A-024.4:plan_review 决断递交后清键 —— 同 taskId 的下一个 plan gate 读回首见)。 */
|
|
10
|
+
export declare function clearArmedGate(key: string | null | undefined): void;
|
|
11
|
+
/** W1 带 key 变体。 */
|
|
12
|
+
export declare function clearArmedGateFor(sessionKey: string, key: string | null | undefined): void;
|
|
13
|
+
/** 宿主呈现面登记口:收到可渲染 question 帧即记(键归一见 gateIdentity;坏形静默忽略 ——
|
|
14
|
+
* id 是 wire/合成位,入参按边界收 unknown,本函数就是窄化动作本身)。 */
|
|
15
|
+
export declare function registerArmedGateFromQuestionId(questionId: unknown): void;
|
|
16
|
+
/** W1 带 key 变体。 */
|
|
17
|
+
export declare function registerArmedGateFromQuestionIdFor(sessionKey: string, questionId: unknown): void;
|
|
18
|
+
/** 测试用:清全部会话的台账。 */
|
|
19
|
+
export declare function _resetArmedGateRegistryForTest(): void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* armedGateRegistry.ts — per-session 呈现台账:哪些决断卡在**这个宿主进程的哪个会话**里真的
|
|
3
|
+
* 呈现过(A-028.3,#244 族A 包半场,2026-08-12;源形 = cli `src/sema/armedGateRegistry.ts`,
|
|
4
|
+
* 上收时由模块级单 Set 改为 sessionSlot per-session 键,支持多会话宿主)。
|
|
5
|
+
*
|
|
6
|
+
* ── 为什么需要它 ────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
* 409 自愈重开臂(`adapter/activeRunSelfHeal.ts` 的分诊消费方)此前对每一张 pending 卡都说
|
|
8
|
+
* 「that card was closed without being answered, so sema reopened it」。但已定谳的机理链
|
|
9
|
+
* (plan approve → outcome enqueue 成新提交 → 引擎已推进到**新** gate → 该提交撞 409 →
|
|
10
|
+
* 重开腿铸出全新 pending 卡)里,那张卡是**首见**的 —— 从未被 arm 过,更谈不上 closed/
|
|
11
|
+
* reopened。文案对首见卡编造历史 = 对用户下一个证不出的断言。重开机制本身是设计内的
|
|
12
|
+
* surfacing 路径(承重,不动);本台账只供「说了什么」分形:firstSight 判据 = 这张卡的身份键
|
|
13
|
+
* 有没有在本进程本会话呈现过。
|
|
14
|
+
*
|
|
15
|
+
* ── 键的词汇(单源 = `gateIdentity.ts`,别自造字面)────────────────────────────────────────
|
|
16
|
+
* · tool 审批卡:durable 腿 = `approvalCallKey(gatedCallId, taskId)`;live 帧腿 =
|
|
17
|
+
* `liveFrameCallKey(approvalId)`(帧上无 gatedCallId,与 pending 行的 callId 天然对不上 ——
|
|
18
|
+
* 漏配的代价只是把「重开」说成「呈上」,首见文案零历史断言,诚实方向安全)。
|
|
19
|
+
* · AskUserQuestion overlay:`askGateQuestionId(callKey)` 铸帧 id,本台账经
|
|
20
|
+
* {@link registerArmedGateFromQuestionId} 剥前缀取键(= callKey,与卡键同域)。
|
|
21
|
+
* · plan_review:`planReviewQuestionId(taskId)` 整串作键(保留前缀,与 taskId 直接作 ask 键
|
|
22
|
+
* 永不撞域);重开腿铸 `…#reopen-*`,归一化剥尾 —— 首呈与重开落同一键。
|
|
23
|
+
*
|
|
24
|
+
* ── 登记点(「没呈现就不算 arm」)───────────────────────────────────────────────────────────
|
|
25
|
+
* ① 宿主呈现面:overlay 收到可渲染 question 帧时 / 卡口真把卡 enqueue 进渲染队列时,宿主调
|
|
26
|
+
* {@link registerArmedGateFromQuestionId} / {@link registerArmedGate}(auto-allow/deny/
|
|
27
|
+
* 失败臂不登记)。包的 publish 点**不**代登记 —— publish 无 overlay 时是 no-op,代登记 =
|
|
28
|
+
* 把「发过帧」谎报成「呈现过」。
|
|
29
|
+
* ② 重开腿:先读(firstSight 判决)后写(铸卡即 arm)—— 同一张卡第二次撞 409 才说 reopened。
|
|
30
|
+
* 包内 `planReviewWire.reopenPlanReviewCard` 与壳侧 ask 重开腿都按此序。
|
|
31
|
+
* ③ 消费点:plan_review 决断**递交后**清键({@link clearArmedGate};A-024.4 —— 同 run 的下一
|
|
32
|
+
* 个 plan gate 必须读回首见,键粒度是 taskId,不清就把新门谎成复见)。ask 族键粒度是
|
|
33
|
+
* callId(每 gate 唯一),无此问题,不清。
|
|
34
|
+
*
|
|
35
|
+
* per-session 语义:sessionSlot 注册表(desktop session-host 多引擎会话互不串账);零参 API =
|
|
36
|
+
* DEFAULT_SESSION_KEY 兼容层,单会话宿主(cli)装配一行不动。callId 全局唯一,跨 /clear 不撞键。
|
|
37
|
+
*/
|
|
38
|
+
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
39
|
+
import { armedKeyFromQuestionId } from './gateIdentity.js';
|
|
40
|
+
// 🔴 模块级单例(singleton-manifest 登记):per-session 键 → 已呈现身份键集。
|
|
41
|
+
const armedGatesByKey = createSessionSlot();
|
|
42
|
+
function setFor(sessionKey) {
|
|
43
|
+
let s = armedGatesByKey.get(sessionKey);
|
|
44
|
+
if (!s) {
|
|
45
|
+
s = new Set();
|
|
46
|
+
armedGatesByKey.set(sessionKey, s);
|
|
47
|
+
}
|
|
48
|
+
return s;
|
|
49
|
+
}
|
|
50
|
+
/** 登记一个已呈现的决断卡身份键(空/非法输入静默忽略 —— 登记面绝不炸渲染链)。 */
|
|
51
|
+
export function registerArmedGate(key) {
|
|
52
|
+
registerArmedGateFor(DEFAULT_SESSION_KEY, key);
|
|
53
|
+
}
|
|
54
|
+
/** W1 带 key 变体(多会话宿主每会话一键,互不串账)。 */
|
|
55
|
+
export function registerArmedGateFor(sessionKey, key) {
|
|
56
|
+
if (typeof key === 'string' && key.length > 0)
|
|
57
|
+
setFor(sessionKey).add(key);
|
|
58
|
+
}
|
|
59
|
+
/** 该身份键的卡在本进程本会话呈现过吗?(false = 首见;文案分形的唯一判据) */
|
|
60
|
+
export function wasGateArmed(key) {
|
|
61
|
+
return wasGateArmedFor(DEFAULT_SESSION_KEY, key);
|
|
62
|
+
}
|
|
63
|
+
/** W1 带 key 变体。 */
|
|
64
|
+
export function wasGateArmedFor(sessionKey, key) {
|
|
65
|
+
if (typeof key !== 'string' || key.length === 0)
|
|
66
|
+
return false;
|
|
67
|
+
return armedGatesByKey.get(sessionKey)?.has(key) === true;
|
|
68
|
+
}
|
|
69
|
+
/** 消费一个身份键(A-024.4:plan_review 决断递交后清键 —— 同 taskId 的下一个 plan gate 读回首见)。 */
|
|
70
|
+
export function clearArmedGate(key) {
|
|
71
|
+
clearArmedGateFor(DEFAULT_SESSION_KEY, key);
|
|
72
|
+
}
|
|
73
|
+
/** W1 带 key 变体。 */
|
|
74
|
+
export function clearArmedGateFor(sessionKey, key) {
|
|
75
|
+
if (typeof key === 'string' && key.length > 0)
|
|
76
|
+
armedGatesByKey.get(sessionKey)?.delete(key);
|
|
77
|
+
}
|
|
78
|
+
/** 宿主呈现面登记口:收到可渲染 question 帧即记(键归一见 gateIdentity;坏形静默忽略 ——
|
|
79
|
+
* id 是 wire/合成位,入参按边界收 unknown,本函数就是窄化动作本身)。 */
|
|
80
|
+
export function registerArmedGateFromQuestionId(questionId) {
|
|
81
|
+
registerArmedGateFromQuestionIdFor(DEFAULT_SESSION_KEY, questionId);
|
|
82
|
+
}
|
|
83
|
+
/** W1 带 key 变体。 */
|
|
84
|
+
export function registerArmedGateFromQuestionIdFor(sessionKey, questionId) {
|
|
85
|
+
if (typeof questionId !== 'string' || questionId.length === 0)
|
|
86
|
+
return;
|
|
87
|
+
registerArmedGateFor(sessionKey, armedKeyFromQuestionId(questionId));
|
|
88
|
+
}
|
|
89
|
+
/** 测试用:清全部会话的台账。 */
|
|
90
|
+
export function _resetArmedGateRegistryForTest() {
|
|
91
|
+
armedGatesByKey.clear();
|
|
92
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hitl/askParkRowRouting.ts — ask park(`suspended`)重开链的**纯判定层**(#265 上收件,
|
|
3
|
+
* 2026-08-14;源形 = cli `src/sema/askParkReopen.ts` 的判据半场,该文件的呈现/决断编排留在端上)。
|
|
4
|
+
*
|
|
5
|
+
* 三端(cli / desktop / web)重开一张 ask park 的卡时,要按同一套判据回答四个问题;本文件是这
|
|
6
|
+
* 四问的唯一真源,**一个渲染面都不碰**(帧/卡口/HitlBridge 全在调用方):
|
|
7
|
+
* ① 「此刻队列里有没有一行**该由本臂决**的行?」 → {@link classifyAskParkRows}(三态)
|
|
8
|
+
* ② 「这一行的身份键是什么?」 → {@link askParkRowIdentity}
|
|
9
|
+
* ③ 「这一行该走问答臂还是工具门臂?」 → {@link askParkRowArm}
|
|
10
|
+
* ④ 「链跑砸了、而行已不在表 —— 这是已决还是没轮到?」 → {@link classifyAskParkChainFailure}
|
|
11
|
+
* 外加一件构造保证:同一行的并发重开**合流**成一次({@link createRowArmSingleFlight})。
|
|
12
|
+
*
|
|
13
|
+
* ── ① 三态与证据纪律(cli #269 的整个要点)────────────────────────────────────────────────────
|
|
14
|
+
* `/v1/approvals` 是 **scope 级全量单队列**:child 委派的 park 行 `taskId` = child 自己的 run id,
|
|
15
|
+
* 所以「按 busy 信号点名的 taskId 单键过滤」会结构性漏看。发现面 = 全量取回 → 先找点名行 →
|
|
16
|
+
* 没有时**在队列无歧义(恰一行)且该行归属可正向证明**的前提下兜底。
|
|
17
|
+
* 🔴 分态只有一条纪律:**只有正面证据才配 `settled`**。
|
|
18
|
+
* · 空表 / 多行无点名行 / 唯一行归属证明不了 ⇒ 一律 `unborn`(缺席证明不了任何事;这三种形都
|
|
19
|
+
* 可能在下一拍变成「我的那一行出生了」——引擎正在跑那次决定 Write 参数的模型调用);
|
|
20
|
+
* · 行在、但门种不属 ask 门族 ⇒ 也只给 `unborn`。「看得见的这一行不属本臂」与「这里已经没有本臂
|
|
21
|
+
* 该决的事」是两件事,而门换代窗恰恰会短暂两者同形(plan_review 行在 decide 落地后还能被读到
|
|
22
|
+
* 一小会儿,接替它的 human/tool_approval 行尚未出生)。把这一拍读成终判 = 把失败形换个姿势
|
|
23
|
+
* 原样复现。代价(如实记):真·走错臂时要等满有界窗才说那句诚实话。
|
|
24
|
+
* ⇒ 本函数**今天一条 `settled` 都不产**,这是成文结论不是遗漏:队列读面给得出的一切都只是缺席类
|
|
25
|
+
* 证据。`settled` 留在 {@link ParkRowProbe} 契约里给**读面更强**的宿主(注入 gate 实例态/wire
|
|
26
|
+
* 直供门身份)用 —— 那时「已了结」才是拿得出正面证据的判决。
|
|
27
|
+
*
|
|
28
|
+
* ── ③ 行路由:锚在**决定量**上,不锚门种的名字 ───────────────────────────────────────────────
|
|
29
|
+
* 🔴 `gateKind === 'human'` **不是**「这是 AskUserQuestion」的证据(engine 7.18.0 真 wire 直证):
|
|
30
|
+
* plan 批准之后铸出的 **Write** 审批行带的就是 `gateKind:"human" · toolName:"Write" ·
|
|
31
|
+
* input:{file_path,content}`。`human` 在引擎侧是**泛化的「要人来决定」**(本包 `hitlBridge.observe`
|
|
32
|
+
* 对 `gate:null` 的补形同样是 `{kind:'human'}`),它与 `tool_approval` 是「粗粒度 / 细粒度」的关系,
|
|
33
|
+
* 不是「问答 / 工具」的二分。把 `human` 单义化成问答臂,那一行就会被送进没有问句 payload 的臂 ⇒
|
|
34
|
+
* 静默失败 ⇒ write 卡永不呈现。问答臂要的东西是**问句**,那就问「这行到底有没有问句」。
|
|
35
|
+
*
|
|
36
|
+
* ── 归层 ────────────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
* 本文件值级 import 全在包内(gateIdentity / parkOwnership / frameRouter / activeRunSelfHeal 词表),
|
|
38
|
+
* SDK 只 type-only;零宿主端口、零模块级状态(单活闸是**工厂**,寿命归调用方 —— 包不替宿主决定
|
|
39
|
+
* 那张表活多久)。
|
|
40
|
+
*/
|
|
41
|
+
import type { PendingCheckpoint } from '@sema-agent/sdk';
|
|
42
|
+
import type { ParkRowProbe } from './parkRowBirthWait.js';
|
|
43
|
+
import { type ParkOwnershipDeps } from './parkOwnership.js';
|
|
44
|
+
/**
|
|
45
|
+
* 待决行有界等待窗的**建议上限**(端可覆盖;`waitForParkRowBirth` 自己不带缺省)。
|
|
46
|
+
* 实测锚:plan 批准之后引擎要跑**一次真实模型调用**才算得出 Write 的参数、才把审批行落库 ——
|
|
47
|
+
* t+2500ms 读到的仍是空表,t+3000ms 行才出生。15s = 实测 3s 的 5 倍余量(慢模型/负载下的头部
|
|
48
|
+
* 延迟);超限如实收口 —— 到那一刻降级话是真话,这正是有界的意义。
|
|
49
|
+
* 🔴 代价如实记:真孤儿 park(座位占着、确实没有任何待决行)要等满这个窗才说那句诚实的失败话。
|
|
50
|
+
* 方向取舍 = 让**常见且可修**的那一态(行未出生)真的被接住,代价是**罕见且本就已坏**的那一态
|
|
51
|
+
* 多等一个有界窗;等待全程可被中断(signal 直通重查环)。
|
|
52
|
+
*/
|
|
53
|
+
export declare const ASK_PARK_ROW_WAIT_MS = 15000;
|
|
54
|
+
/** 重查节奏建议值。500ms 级:比行出生的量级(秒)细一个数量级,又不至于把本地引擎打成轮询风暴。 */
|
|
55
|
+
export declare const ASK_PARK_ROW_POLL_MS = 500;
|
|
56
|
+
/** {@link classifyAskParkRows} 的注入口(缺省 = 包内归属判据)。 */
|
|
57
|
+
export interface AskParkRowScanDeps {
|
|
58
|
+
/** 归属判据(缺省 = `pendingRowIsOwnedByThisSession`)。fail-CLOSED:证不出 = 不采信。 */
|
|
59
|
+
isOwned?: (row: PendingCheckpoint) => boolean;
|
|
60
|
+
/** 缺省归属判据的注入位(多会话宿主传 sessionKey / 自己的 currentSessionId)。 */
|
|
61
|
+
ownership?: ParkOwnershipDeps;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* ① 「本臂此刻有没有一行可决断的行」——三态判决(见文件头)。**纯函数**:行由调用方读面取回,
|
|
65
|
+
* 本函数只判;绝不抛。
|
|
66
|
+
*
|
|
67
|
+
* @param rows `/v1/approvals` 全量 pending 行(scope 级单队列原样)
|
|
68
|
+
* @param taskId busy 信号点名的 run id(优先匹配它;匹配不到才谈兜底)
|
|
69
|
+
*/
|
|
70
|
+
export declare function classifyAskParkRows(rows: readonly PendingCheckpoint[], taskId: string, deps?: AskParkRowScanDeps): ParkRowProbe<PendingCheckpoint>;
|
|
71
|
+
/** ② 行身份三件套(重开链的一切键都从这里出;两处各算一遍必漂)。 */
|
|
72
|
+
export interface AskParkRowIdentity {
|
|
73
|
+
/** 🔴 行内身份绑定:decide 腿一律按**该行自己的** taskId 走,绝不绑请求者的 —— 兜底腿把别的行
|
|
74
|
+
* 解到请求者名下时,按请求者的 id 去决断就是决了另一件事。 */
|
|
75
|
+
rowTaskId: string;
|
|
76
|
+
gatedCallId: string | undefined;
|
|
77
|
+
/** 首见/复见 + 呈现回执的台账键(`gateIdentity.approvalCallKey` 单源,端零手抄字面)。 */
|
|
78
|
+
armedKey: string;
|
|
79
|
+
}
|
|
80
|
+
/** ② 算一行的身份三件套。`taskId` = 请求者点名的 id(行自己没带 taskId 时的回退)。 */
|
|
81
|
+
export declare function askParkRowIdentity(taskId: string, row: PendingCheckpoint): AskParkRowIdentity;
|
|
82
|
+
/**
|
|
83
|
+
* ③ 这一行该走哪条臂。判据锚在**决定量**(这行到底有没有问句)上,不锚门种的名字 —— 理由见文件头。
|
|
84
|
+
* · kind 缺席(pre-3.0.0 旧行)⇒ 维持 toolName 判据(旧行没有别的信号);
|
|
85
|
+
* · kind 是 ask 门族里 `human` 之外的词(tool_approval / irreversible_ask / policy_ask)⇒ 工具臂;
|
|
86
|
+
* · kind = `human` ⇒ 三选一命中即问答臂:toolName 明说是 ask 工具 / 行真带问句 payload /
|
|
87
|
+
* 行**连 toolName 都没有**(没有工具可渲,问答臂是唯一可能的形,且它自己会对空 payload 诚实收口)。
|
|
88
|
+
* 🔴 `toolName` 是 UNTRUSTED wire 位且 `/v1/approvals` 行不经工具名归一 —— 用包的规范判据
|
|
89
|
+
* {@link isAskTool}(空白/下划线/大小写不敏感),严格串比会把小写形误路由进工具臂。
|
|
90
|
+
*/
|
|
91
|
+
export declare function askParkRowArm(row: PendingCheckpoint): 'question' | 'tool-gate';
|
|
92
|
+
/**
|
|
93
|
+
* ④ 「这一行还在不在待决表里」——回读判据(键取法与 {@link askParkRowIdentity} 同源)。
|
|
94
|
+
* 🔴 调用方读面失败时**必须**当「还在」(保守:照走重试/上屏;重复 decide 由 server 侧 checkpoint
|
|
95
|
+
* 一次性消费挡住,二发只会得到 stale/no_pending 拒绝,不会 double-act)。
|
|
96
|
+
*/
|
|
97
|
+
export declare function askParkRowStillPending(rows: readonly PendingCheckpoint[], identity: AskParkRowIdentity): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* ④ 链失败之后「行已不在 pending 表」的**两义分臂**判决。
|
|
100
|
+
* · `retry` —— 行还在表上 ⇒ 这就是一次真失败:照走重试 / 如实上屏;
|
|
101
|
+
* · `row-gone-after-card` —— 行没了 **且卡呈现过** ⇒ **静默收口**:零重试、零失败话、零成功断言。
|
|
102
|
+
* 🔴 名字刻意只说**可观察事实**(二次评审两轮处置):它不叫「已在别处决了」,因为呈现只证明卡
|
|
103
|
+
* 渲出来过,证明不了有人做过决定 —— 一个带决断语义的判决值会诱导宿主替引擎宣布成功。它成立的
|
|
104
|
+
* 判据是两件可观察事实的合取:队列回读说这一行已不在待决表(一次真读数,不是缺席推断),且本链
|
|
105
|
+
* 确实把卡交到过渲染面。处置的理由也只由这两件事推出:重试 = 对一个不在待决表的行再决一次
|
|
106
|
+
* (server 侧 checkpoint 一次性消费会拒),上屏「the session is still held」= 对一个已不在队列的
|
|
107
|
+
* 行说它还占着。🔴 宿主**不许**据此对用户断言「你的决定已生效」。残余(如实记):行在呈现窗里
|
|
108
|
+
* 被换代/过期而恰好没人决过时,这一格会把它按已收口处理 —— 代价是这一轮不再重开(用户下一条
|
|
109
|
+
* 消息会再撞 409,自愈腿重跑);要真的分出「已决」只能靠决断回执 / typed stale·conflict 应答那类
|
|
110
|
+
* **正面证据**,而那属于决断腿的供给面,不在本读面。
|
|
111
|
+
* 🔴 为什么不干脆退回 `row-unborn` 继续重查(二次评审 R3 [medium] 提议,**未采纳**,理由对称):
|
|
112
|
+
* 那会把**最常见的良性时序**(决断真落地 → 座位释放 → 队列从此空表)变成「白等满一个窗,然后
|
|
113
|
+
* 对用户说『没能重开那张卡』」—— 一句同样为假的话,而且它出现在事情**本来就成了**的那一格。
|
|
114
|
+
* 两个方向各有一次谎报风险,取舍按代价:本格的代价是这一轮不重开(下一条消息自愈腿重跑,可收敛),
|
|
115
|
+
* 对面的代价是每一次成功决断都附赠一句假失败 + 一个满窗停顿(每次都发生,不可收敛)。
|
|
116
|
+
* · `row-unborn` —— 行没了 **且一张卡都没呈上** ⇒ 没有任何人决过任何事,行消失只可能是
|
|
117
|
+
* 「刚才那一行换代了 / 下一只门的行还没铸出来」。此时按成功收口 = 对一件从没发生过的决断下
|
|
118
|
+
* 断言,而呈现回执永远等不到 ⇒ 用户拿到降级话,三秒后真的出生的那张卡再没人去接。这一格
|
|
119
|
+
* **不是失败**(不上失败话、不重试),交回有界重查环。
|
|
120
|
+
*
|
|
121
|
+
* 🔴 `cardPresented` 必须是**无窗的事实**(整条链寿命里的呈现事件订阅),不能拿带看门狗的呈现回执
|
|
122
|
+
* 近似:迟到卡会让带窗的那只回 false,于是一次真呈现被误判成「行从未出生」,重查环再铸一张卡 ⇒
|
|
123
|
+
* 同一个待决项两张可按的卡。
|
|
124
|
+
*/
|
|
125
|
+
export type AskParkChainFailureDisposition = 'retry' | 'row-gone-after-card' | 'row-unborn';
|
|
126
|
+
export declare function classifyAskParkChainFailure(i: {
|
|
127
|
+
rowStillPending: boolean;
|
|
128
|
+
cardPresented: boolean;
|
|
129
|
+
}): AskParkChainFailureDisposition;
|
|
130
|
+
/**
|
|
131
|
+
* **行级重开单活闸**:同一行(键 = {@link AskParkRowIdentity.armedKey})的第二个调用方**合流**到
|
|
132
|
+
* 在飞的那一次(拿逐字相同的判决),不另起一条链 —— 「一个待决项至多一张卡」由此是构造保证。
|
|
133
|
+
*
|
|
134
|
+
* 🔴 为什么按**行键**而不是请求者的 taskId:发现面的兜底腿会把**不同** taskId 的两个调用方解到
|
|
135
|
+
* **同一行**,那时两条链会各铸一张身份不同、都能按的卡 —— 同一个待决项两张卡,先按的决断、后按的
|
|
136
|
+
* 变成 stale 误导面。而重开臂的寿命一旦从毫秒级拉到一个秒级的有界重查窗,「结构上不容易撞上」就
|
|
137
|
+
* 不再是防线([paired-mechanisms]:两半各持「对方会兜底」的假设 = 零结果),必须在源头收掉。
|
|
138
|
+
*
|
|
139
|
+
* 🔴 **工厂,不是模块级台账**:这张表的寿命 = 宿主一个会话的重开面,由装配点持有;包不替宿主决定
|
|
140
|
+
* 它活多久,多会话宿主天然一会话一只(单例化反而会让两个会话互相合流)。
|
|
141
|
+
*/
|
|
142
|
+
export interface RowArmSingleFlight<T> {
|
|
143
|
+
/** 有在飞的同键链 ⇒ 返回它(第二个调用方零副作用);否则起一条并登记(结束后自动清账)。 */
|
|
144
|
+
join: (key: string, start: () => Promise<T>) => Promise<T>;
|
|
145
|
+
/** 当前在飞条数(诊断/测试用)。 */
|
|
146
|
+
size: () => number;
|
|
147
|
+
/** 清空(套件之间不许互相串态)。 */
|
|
148
|
+
clear: () => void;
|
|
149
|
+
}
|
|
150
|
+
export declare function createRowArmSingleFlight<T>(): RowArmSingleFlight<T>;
|