@sema-agent/client-core 0.28.0 → 0.29.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/README.md +3 -2
- package/dist/adapter/activeRunSelfHeal.d.ts +191 -0
- package/dist/adapter/activeRunSelfHeal.js +287 -0
- package/dist/adapter/runStream.d.ts +23 -6
- package/dist/adapter/runStream.js +6 -2
- package/dist/hitl/armedGateRegistry.d.ts +19 -0
- package/dist/hitl/armedGateRegistry.js +92 -0
- package/dist/hitl/frameRouter.d.ts +5 -1
- package/dist/hitl/frameRouter.js +1 -1
- package/dist/hitl/gateIdentity.d.ts +42 -0
- package/dist/hitl/gateIdentity.js +56 -0
- 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/planReviewWire.d.ts +51 -0
- package/dist/hitl/planReviewWire.js +172 -17
- package/dist/hitl/toolApprovalWire.d.ts +23 -14
- package/dist/hitl/toolApprovalWire.js +51 -24
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/liveQuestionStore.d.ts +11 -0
- package/dist/liveQuestionStore.js +13 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
23
23
|
|
|
24
24
|
## Scope
|
|
25
25
|
|
|
26
|
-
**Version:** 0.
|
|
26
|
+
**Version:** 0.29.0
|
|
27
27
|
|
|
28
28
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
29
29
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -53,7 +53,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
53
53
|
`SseIdleError`, `probeHealth`, `APIError` and `TaskStopConflictError` are imported as values in
|
|
54
54
|
five modules, and the browser bundle really bundles the SDK through (the portability guard would
|
|
55
55
|
exit 3 rather than quietly mark it external).
|
|
56
|
-
- The declared floor is `>=6.
|
|
56
|
+
- The declared floor is `>=6.16.0`, and it is *witnessed*: the guard checks that an actually
|
|
57
57
|
installed SDK at that line still exports every value-level symbol this package imports and still
|
|
58
58
|
declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
|
|
59
59
|
promise, not a contract.
|
|
@@ -236,6 +236,7 @@ public-surface guard checks that last one).
|
|
|
236
236
|
| `scripts/run-client-core-singleton-test.mjs` | Module-level singletons ⇄ `docs/refactor/p1-scan/singleton-manifest.json`, **both directions**: an unregistered singleton is red (registering it forces someone to answer "what if this got duplicated"), a stale entry is red, and the `dupRisk: high` count only goes down |
|
|
237
237
|
| `scripts/run-catalog-loader-gates-test.mjs` | The model-catalog candidate chain (`loadCatalogWithSources`) and the provider device-code seam: offline ⇒ `bundled` with an honest `online.reason`, a good source ⇒ `online` plus a cache write, a second offline run ⇒ `cacheHit`; the three hostile source shapes (malformed JSON, `schemaVersion: 99`, off-domain `http`) each fall through to the bundled table, and an off-allowlist target is **never dialled** — including a `302` to another host, proven by a real loopback server's hit counter staying at zero; a one-byte edit to `catalog.sha256` drops that source while an unavailable sidecar only warns; and the device-code poller's `pending → ok` / `expired` arms run against a real loopback HTTP server with an injected clock |
|
|
238
238
|
| `scripts/run-abortable-sleep-test.mjs` | The shared `abortableSleep(ms, signal)` leaf (consumed by `workflowClient.ts` and `agentSession/backgroundView.ts`'s poll backoff): normal timeout resolution, immediate wake-up on `abort` mid-wait, `clearTimeout` really firing on that path, and a post-resolve late abort staying a no-op |
|
|
239
|
+
| `scripts/run-selfheal-reopen-test.mjs` | The 409 active-run self-heal decision chain: `governanceForced` narrows on strict `true` only; triage prefers the wire's `pendingGate.kind` and falls back to the status table (an off-table kind is never guessed into a card arm — hands-off plus the honest wording); a first-sight card makes zero closed/reopened claims and a host presentation receipt of `presented: false` demotes the outcome to reopen-failed; park-row ownership is a fail-closed positive proof (own-run ledger or session id — unprovable is not owned); the three gate-identity key literals live in exactly one mint (`hitl/gateIdentity.ts`, AST string-token scan); the armed-gate presentation ledger is per-session; and the `plan_review` reopen arm shares the arm arm's card body, three-state verdict and delivery pipe, consuming the presentation history once a decision is delivered |
|
|
239
240
|
|
|
240
241
|
Each suite carries a floor that only moves up — a refactor that stops executing a group of
|
|
241
242
|
assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapter/activeRunSelfHeal.ts — 「会话被 active-run claim 锁死」的自愈决断层(A-028.1,
|
|
3
|
+
* #244 族A 包半场,2026-08-12;源形 = cli `src/sema/activeRunSelfHeal.ts` 的四路分诊树 +
|
|
4
|
+
* 结局文案层,判别半场 `activeRunBusySignal` 早已在隔壁 `runStream.ts` —— 本文件是它的
|
|
5
|
+
* 消费侧,两半从此同居包内单源)。
|
|
6
|
+
*
|
|
7
|
+
* ── 事故形态 ────────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
* 引擎的 session claim 是会话级锁(createRun EEXIST ⇒ 409)。终态释放它;而 park 态
|
|
9
|
+
* (`suspended` / `needs_review`)保留它,且重启引擎救不回来(boot 期孤儿回收只捞 running)、
|
|
10
|
+
* 时间型 reap 默认整条腿不跑。于是「上一轮 turn 在审批/提问处 park 了,用户走开」之后,这个
|
|
11
|
+
* 会话的每一条新消息恒被 409 拒收。
|
|
12
|
+
*
|
|
13
|
+
* ── 🔴 处置是**分诊**,不是「见 409 就取消」────────────────────────────────────────────────
|
|
14
|
+
* 分诊真源两级(A-028.1 目标形):**`pendingGate.kind` 优先,status 表回退**。
|
|
15
|
+
* · kind 是 wire 指名的 gate 身份(引擎只在 parked 且真有待决 checkpoint 时带它),比 status
|
|
16
|
+
* 粒度准:run status `needs_review` ≠ gate kind `plan_review` —— dry-run 复核门(gate kind
|
|
17
|
+
* `needs_review`)同样落 status needs_review,按 status 猜臂会把它错路成 plan 审批卡。
|
|
18
|
+
* · kind 缺席(旧引擎 / 非 park / store 降级)才回退 status 表。
|
|
19
|
+
* 四路结局:
|
|
20
|
+
* ① plan_review 门 ⇒ **重开审批卡**,绝不 cancel(cancel = 替用户 reject 掉整个 plan)。
|
|
21
|
+
* ② 审批/提问门(human / irreversible_ask / tool_approval)⇒ **重开那张卡**(把决定权还给
|
|
22
|
+
* 用户;cancel+自动重发臂已整退役 —— 实证它是「新消息→cancel→新 run 同 ask 再 park」的
|
|
23
|
+
* 循环病根;重开失败也只如实告知,绝不回退 cancel)。
|
|
24
|
+
* ③ `running`(或任何非 park 态)⇒ **什么都不做**。持有 claim 的可能是用户背景化的 durable
|
|
25
|
+
* run,正在替他干活;见 409 就 cancel = 销毁他明确要求的工作。
|
|
26
|
+
* ④ kind/status 两级都判不出(含今天还不存在的新门/新状态)⇒ 也什么都不做,如实告知。
|
|
27
|
+
* 不确定时不做破坏性动作;新词到货时最坏结果是保守,而不是拿没想过的词做破坏性动作。
|
|
28
|
+
*
|
|
29
|
+
* ── 出路文案纪律 ────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
* 本模块产出的每一句「你可以做什么」都必须是**真的接了线**的动作。默认串承诺的是 cli 的
|
|
31
|
+
* `/clear`(regenerateSessionId ⇒ 全新引擎会话)与引擎自己的 cancel/decide 端点;没有 `/clear`
|
|
32
|
+
* 概念的宿主**必须**经 {@link ActiveRunSelfHealCopy} 换成自己的真出路 —— 绝不出现「按某个键」
|
|
33
|
+
* 而那个键没有注册。
|
|
34
|
+
*
|
|
35
|
+
* 🔴 portability:本文件是零值级 import 的纯函数叶(类型全部 type-only 借 `runStream.ts`),
|
|
36
|
+
* 不进内核/A 层闭包;index 闭包 +1 已登记。
|
|
37
|
+
*/
|
|
38
|
+
import type { ActiveRunBusySignal } from './runStream.js';
|
|
39
|
+
/** wire gate kind → plan_review 重开臂(core CheckpointGate 六 kind 词表之一)。 */
|
|
40
|
+
export declare const PLAN_REVIEW_GATE_KIND = "plan_review";
|
|
41
|
+
/**
|
|
42
|
+
* wire gate kind → ask 重开臂的三个成员:`human` / `irreversible_ask` 是 core CheckpointGate
|
|
43
|
+
* 的审批/提问族(有 approvals.list pending 行可铸卡);`tool_approval` 是 park 判定
|
|
44
|
+
* (`toolApprovalWire.isToolApprovalGate`)已承认的一等 kind —— 两张表必须同集合
|
|
45
|
+
* ([paired-mechanisms-must-share-premise]:park 认得出、分诊路不进去 = 恒 reopen-failed)。
|
|
46
|
+
* 表外 kind(`resource_limit` / `needs_review` / `task_done` / 未来新词)一律走结局④:不动 +
|
|
47
|
+
* 如实说(wire 带的 decidePath 由文案层原样交给用户)。
|
|
48
|
+
*/
|
|
49
|
+
export declare const ASK_PARK_GATE_KINDS: readonly string[];
|
|
50
|
+
/**
|
|
51
|
+
* status 回退表(kind 缺席时的粗粒度代理):plan_review park 的引擎 run status。
|
|
52
|
+
* 🔴 动作**不按状态名硬编码枚举**:两张表之外的一切状态(含今天还不存在的)一律「不动它 +
|
|
53
|
+
* 如实说」,新状态到货时最坏结果是保守。
|
|
54
|
+
*/
|
|
55
|
+
export declare const PLAN_REVIEW_STATES: readonly string[];
|
|
56
|
+
/** status 回退表:审批/提问 park(**只有 `suspended`**)。 */
|
|
57
|
+
export declare const ASK_PARK_STATES: readonly string[];
|
|
58
|
+
/**
|
|
59
|
+
* durable 控制面的**鸭子类型**视图。live 客户端有 get/cancel,mock 车道没有 —— 两个都是可选,
|
|
60
|
+
* `events` 是必填只为让这个接口不是「全可选属性」(全可选会触发 TS 的弱类型检查,把宽客户端形
|
|
61
|
+
* 判成「无公共属性」而拒绝赋值)。三处 `unknown` 是边界职责形(本包不拥有 run 记录的类型,
|
|
62
|
+
* 消费点 `readStatus` 就是窄化动作本身;typeshape 门 RATCHET 逐条登记)。
|
|
63
|
+
*/
|
|
64
|
+
export interface DurableRunVerbs {
|
|
65
|
+
events: unknown;
|
|
66
|
+
get?: (taskId: string, opts?: {
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
}) => Promise<{
|
|
69
|
+
status?: unknown;
|
|
70
|
+
} | null>;
|
|
71
|
+
cancel?: (taskId: string, opts?: {
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
}) => Promise<unknown>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 重开口的判决形(契约,宿主与包内重开腿共同遵守)。
|
|
77
|
+
*
|
|
78
|
+
* · `reopened: true` 的**成文语义** = 「待决材料已确认到手,且卡已交给一个当时在场的渲染面」
|
|
79
|
+
* (pending 行/题面先 await 到手 + overlay/卡口在场检查通过)。它**不是**像素级呈现的证明。
|
|
80
|
+
* · `firstSight` = 这张卡的身份键在本进程**从未登记过呈现**(判据 = `armedGateRegistry`;
|
|
81
|
+
* 读写都在重开腿与宿主呈现面,本分诊纯层只经返回值拿判决,不碰台账)。文案据此分形:
|
|
82
|
+
* 首见卡说「呈上了一张卡」,零 closed/reopened 断言;复见卡才说「closed without being
|
|
83
|
+
* answered, so sema reopened it」—— 对没发生过的历史下断言 = 谎报面。
|
|
84
|
+
* · `presented`(**真呈现回执位**,可选)= 宿主呈现层的回执:`true` = 渲染面确认这张卡真的
|
|
85
|
+
* 呈现了;`false` = 宿主**确知**没呈现(如帧被会话级去重集吃掉);缺席 = 宿主没有回执机制,
|
|
86
|
+
* 按「已交付渲染面、未验证」处理。分诊层的消费规则:`presented === false` 时 reopened 的
|
|
87
|
+
* 断言不成立 —— 按重开失败臂说话(说「已重开」而宿主明知没渲 = 谎报);true/缺席不降级。
|
|
88
|
+
* 回执机制的实现留宿主端,包只成文这个契约位。
|
|
89
|
+
*/
|
|
90
|
+
export type ReopenCardVerdict = {
|
|
91
|
+
reopened: false;
|
|
92
|
+
} | {
|
|
93
|
+
reopened: true;
|
|
94
|
+
firstSight: boolean;
|
|
95
|
+
presented?: boolean;
|
|
96
|
+
};
|
|
97
|
+
/** 分诊树的注入口(重开腿/待决卡探询都是宿主生命周期资产,经 deps 进来,本层保持纯)。 */
|
|
98
|
+
export interface ActiveRunSelfHealDeps {
|
|
99
|
+
/** 屏幕上是否已有一张待决卡(有 ⇒ 结局 = decision-pending,绝不重开第二张)。 */
|
|
100
|
+
hasPendingDecision?: () => boolean;
|
|
101
|
+
/** plan_review park 的重开口(生产 = 包 `hitl/planReviewWire.reopenPlanReviewCard`);
|
|
102
|
+
* 判决形见 {@link ReopenCardVerdict}。 */
|
|
103
|
+
reopenPlanReview?: (taskId: string) => ReopenCardVerdict;
|
|
104
|
+
/** ask park 的重开口(生产 = 宿主 ask 重开编排,纯判据半场在包 `hitl/parkOwnership.ts`)。
|
|
105
|
+
* async:待决行(=卡的内容)必须先到手才有资格说「重开了」。 */
|
|
106
|
+
reopenAskPark?: (taskId: string) => Promise<ReopenCardVerdict>;
|
|
107
|
+
}
|
|
108
|
+
export type SelfHealOutcome =
|
|
109
|
+
/** ask park ⇒ 审批/提问卡已呈上/重开,决定权还给用户;**不 cancel、不重发**。
|
|
110
|
+
* `firstSight` 见 {@link ReopenCardVerdict} —— 只影响话怎么说,不影响动作。 */
|
|
111
|
+
{
|
|
112
|
+
kind: 'ask-reopened';
|
|
113
|
+
taskId: string;
|
|
114
|
+
firstSight: boolean;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 🔴 屏幕上正有一张待决的审批/提问卡 ⇒ **绝不**自动取消。
|
|
118
|
+
* park 态的 cancel 语义 = 替用户否掉那个待决项(needs_review 就是 reject 掉整个 plan)。
|
|
119
|
+
* 只要用户手上还有可以按的那张卡,决定权就该留在他那里,本层只负责把话说清楚。
|
|
120
|
+
*/
|
|
121
|
+
| {
|
|
122
|
+
kind: 'decision-pending';
|
|
123
|
+
taskId: string | null;
|
|
124
|
+
}
|
|
125
|
+
/** plan_review park ⇒ 审批卡已呈上/重开,决定权还给用户;**不 cancel、不重发**。 */
|
|
126
|
+
| {
|
|
127
|
+
kind: 'plan-review-reopened';
|
|
128
|
+
taskId: string;
|
|
129
|
+
firstSight: boolean;
|
|
130
|
+
}
|
|
131
|
+
/** plan_review park 但卡重开不了(渲染面缺席 / 宿主回执确认没呈现等)⇒ 仍然不 cancel,
|
|
132
|
+
* 如实说 + 给真出路。`decidePath` 来自 409 体的 `pendingGate`(wire 给的真路由,不自造)。 */
|
|
133
|
+
| {
|
|
134
|
+
kind: 'plan-review-reopen-failed';
|
|
135
|
+
taskId: string;
|
|
136
|
+
decidePath: string | null;
|
|
137
|
+
}
|
|
138
|
+
/** 持锁的是**活着的** run(通常是背景化的那一轮),或 wire 指名了两张分诊表之外的 gate
|
|
139
|
+
* kind(结局④:不动 + 如实说)⇒ 不动它,如实告知。 */
|
|
140
|
+
| {
|
|
141
|
+
kind: 'not-parked';
|
|
142
|
+
taskId: string;
|
|
143
|
+
status: string;
|
|
144
|
+
}
|
|
145
|
+
/** 读不到状态(缺 verb / 404 / 传输错)⇒ 不做破坏性动作,如实告知。 */
|
|
146
|
+
| {
|
|
147
|
+
kind: 'state-unknown';
|
|
148
|
+
taskId: string | null;
|
|
149
|
+
detail: string;
|
|
150
|
+
}
|
|
151
|
+
/** ask park 但卡重开不了(无渲染面/待决行缺席/问句缺席/归属证不出)⇒ 仍然不 cancel,
|
|
152
|
+
* 如实说 + 给真出路。 */
|
|
153
|
+
| {
|
|
154
|
+
kind: 'ask-reopen-failed';
|
|
155
|
+
taskId: string;
|
|
156
|
+
decidePath: string | null;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* 自愈一次:定门(kind 优先 → status 表回退)→ 按门类重开对应的卡 → 回报结局。**绝不抛**
|
|
160
|
+
* (turn 收尾路径)。kind 在场时不再回查 runs.get —— wire 已指名门身份,省一次往返。
|
|
161
|
+
*/
|
|
162
|
+
export declare function attemptActiveRunSelfHeal(signal: ActiveRunBusySignal, runs: DurableRunVerbs | undefined, deps?: ActiveRunSelfHealDeps): Promise<SelfHealOutcome>;
|
|
163
|
+
/**
|
|
164
|
+
* 端覆盖口:默认串里的两句「出路」是 cli 的真接线动作(`/clear` / 丢 `--resume`),别的宿主
|
|
165
|
+
* 必须换成自己的真出路;整行覆盖给要完全接管措辞/本地化的端。全部可选,不传 = 默认英文串
|
|
166
|
+
* 逐字节不变。
|
|
167
|
+
*/
|
|
168
|
+
export interface ActiveRunSelfHealCopy {
|
|
169
|
+
/** 交互面结局的「真出路」半句(默认 `run /clear to keep working in a fresh session`)。 */
|
|
170
|
+
wayOut?: string;
|
|
171
|
+
/** headless 面「换个会话」的说法(默认 `start a new session (drop --resume/--continue)`)。 */
|
|
172
|
+
freshSession?: string;
|
|
173
|
+
/** 整行覆盖(交互面):按结局返回整行;返回 undefined = 该结局落回默认串。 */
|
|
174
|
+
rowFor?: (outcome: SelfHealOutcome, signal: ActiveRunBusySignal | null | undefined) => string | undefined;
|
|
175
|
+
/** 整行覆盖(headless 面):返回 undefined = 落回默认串。 */
|
|
176
|
+
headlessRowFor?: (signal: ActiveRunBusySignal) => string | undefined;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 每种结局的上屏整行。**每一句出路都真的接了线**;没有真出路的分支就直说「这个会话暂时无法
|
|
180
|
+
* 继续」。重开成功那条也要上屏 —— 卡是替用户重新打开的,他得知道去答哪张、答完做什么。
|
|
181
|
+
* wire 真到得了端的两个 additive 键是 `activeTaskStatus` 与 `pendingGate`,各自有真消费点;
|
|
182
|
+
* 不为「说不定哪天上游会发」的键留悬空分支。
|
|
183
|
+
*/
|
|
184
|
+
export declare function activeRunSelfHealRow(outcome: SelfHealOutcome, signal?: ActiveRunBusySignal | null, copy?: ActiveRunSelfHealCopy): string;
|
|
185
|
+
/**
|
|
186
|
+
* 非交互车道(headless `-p` 等)对同一事实的诚实说法:那里没有 `/clear`,默认串指的是「别再
|
|
187
|
+
* `--resume` 到这个会话」和引擎自己的 cancel/decide 端点 —— 都是真的存在、真的能做的动作。
|
|
188
|
+
* 这条车道没有自愈腿,材料被吞的代价是全额的:一个「答一下就解锁」的会话会被一句「去 cancel」
|
|
189
|
+
* 打发走,而 cancel 恰恰是替用户否掉那个待决项 —— 所以 decide 入口在场时它才是首选出路。
|
|
190
|
+
*/
|
|
191
|
+
export declare function activeRunBusyHeadlessRow(signal: ActiveRunBusySignal, copy?: ActiveRunSelfHealCopy): string;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/** wire gate kind → plan_review 重开臂(core CheckpointGate 六 kind 词表之一)。 */
|
|
2
|
+
export const PLAN_REVIEW_GATE_KIND = 'plan_review';
|
|
3
|
+
/**
|
|
4
|
+
* wire gate kind → ask 重开臂的三个成员:`human` / `irreversible_ask` 是 core CheckpointGate
|
|
5
|
+
* 的审批/提问族(有 approvals.list pending 行可铸卡);`tool_approval` 是 park 判定
|
|
6
|
+
* (`toolApprovalWire.isToolApprovalGate`)已承认的一等 kind —— 两张表必须同集合
|
|
7
|
+
* ([paired-mechanisms-must-share-premise]:park 认得出、分诊路不进去 = 恒 reopen-failed)。
|
|
8
|
+
* 表外 kind(`resource_limit` / `needs_review` / `task_done` / 未来新词)一律走结局④:不动 +
|
|
9
|
+
* 如实说(wire 带的 decidePath 由文案层原样交给用户)。
|
|
10
|
+
*/
|
|
11
|
+
export const ASK_PARK_GATE_KINDS = ['human', 'irreversible_ask', 'tool_approval'];
|
|
12
|
+
/**
|
|
13
|
+
* status 回退表(kind 缺席时的粗粒度代理):plan_review park 的引擎 run status。
|
|
14
|
+
* 🔴 动作**不按状态名硬编码枚举**:两张表之外的一切状态(含今天还不存在的)一律「不动它 +
|
|
15
|
+
* 如实说」,新状态到货时最坏结果是保守。
|
|
16
|
+
*/
|
|
17
|
+
export const PLAN_REVIEW_STATES = ['needs_review'];
|
|
18
|
+
/** status 回退表:审批/提问 park(**只有 `suspended`**)。 */
|
|
19
|
+
export const ASK_PARK_STATES = ['suspended'];
|
|
20
|
+
/** 失败细节:只取 message 且截断 —— 错误对象整体 stringify 可能把请求上下文一起带上屏。 */
|
|
21
|
+
function describeFailure(e) {
|
|
22
|
+
if (typeof e === 'object' && e !== null && 'message' in e) {
|
|
23
|
+
const m = e.message;
|
|
24
|
+
if (typeof m === 'string' && m.length > 0)
|
|
25
|
+
return m.length > 200 ? `${m.slice(0, 200)}…` : m;
|
|
26
|
+
}
|
|
27
|
+
return 'the engine gave no detail';
|
|
28
|
+
}
|
|
29
|
+
/** run 记录里读 status —— 只认非空字符串,别的一律当「读不到」(诚实缺席不编造)。 */
|
|
30
|
+
function readStatus(record) {
|
|
31
|
+
const st = record?.status;
|
|
32
|
+
return typeof st === 'string' && st.length > 0 ? st : null;
|
|
33
|
+
}
|
|
34
|
+
/** 重开判决 → 结局(真呈现回执消费点:`presented === false` 时 reopened 断言不成立)。 */
|
|
35
|
+
function reopenDelivered(verdict) {
|
|
36
|
+
return verdict.reopened === true && verdict.presented !== false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 自愈一次:定门(kind 优先 → status 表回退)→ 按门类重开对应的卡 → 回报结局。**绝不抛**
|
|
40
|
+
* (turn 收尾路径)。kind 在场时不再回查 runs.get —— wire 已指名门身份,省一次往返。
|
|
41
|
+
*/
|
|
42
|
+
export async function attemptActiveRunSelfHeal(signal, runs, deps) {
|
|
43
|
+
// 🔴 最先问的一句:用户手上有没有一张能按的卡?有就把决定权还给他(见 decision-pending 注)。
|
|
44
|
+
let pending = false;
|
|
45
|
+
try {
|
|
46
|
+
pending = deps?.hasPendingDecision?.() === true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
pending = false; // 读不到就当没有 —— 这一项只用来**减少**破坏性动作,不该反过来卡住自愈
|
|
50
|
+
}
|
|
51
|
+
if (pending)
|
|
52
|
+
return { kind: 'decision-pending', taskId: signal.activeTaskId };
|
|
53
|
+
const taskId = signal.activeTaskId;
|
|
54
|
+
if (taskId === null) {
|
|
55
|
+
return {
|
|
56
|
+
kind: 'state-unknown',
|
|
57
|
+
taskId: null,
|
|
58
|
+
detail: 'the engine did not name the run that holds this session',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// ── 一级分诊:pendingGate.kind(wire 指名的门身份;缺席才落 status 表)──────────────────────
|
|
62
|
+
const gateKind = signal.pendingGate?.kind ?? null;
|
|
63
|
+
if (gateKind === PLAN_REVIEW_GATE_KIND)
|
|
64
|
+
return planReviewArm(taskId, signal, deps);
|
|
65
|
+
if (gateKind !== null && ASK_PARK_GATE_KINDS.includes(gateKind))
|
|
66
|
+
return askParkArm(taskId, signal, deps);
|
|
67
|
+
// ── 状态真源两级:wire 直供 > 自己查一趟 runs.get(kind 缺席/表外 kind 的诚实陈述都要它)──
|
|
68
|
+
let status = signal.activeTaskStatus;
|
|
69
|
+
if (status === null) {
|
|
70
|
+
if (typeof runs?.get !== 'function') {
|
|
71
|
+
return { kind: 'state-unknown', taskId, detail: 'this client exposes no durable run verbs' };
|
|
72
|
+
}
|
|
73
|
+
let record;
|
|
74
|
+
try {
|
|
75
|
+
record = await runs.get(taskId);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return { kind: 'state-unknown', taskId, detail: describeFailure(e) };
|
|
79
|
+
}
|
|
80
|
+
status = readStatus(record);
|
|
81
|
+
}
|
|
82
|
+
if (status === null) {
|
|
83
|
+
return { kind: 'state-unknown', taskId, detail: 'the engine reported no status for that run' };
|
|
84
|
+
}
|
|
85
|
+
// 表外 kind(结局④):wire 指名了一个本层不认得的门 —— 按 status 表猜臂正是 kind 优先要禁
|
|
86
|
+
// 的动作(把 dry-run 复核门错路成 plan 卡)。不动它 + 如实说;decidePath 由文案层原样交出。
|
|
87
|
+
if (gateKind !== null)
|
|
88
|
+
return { kind: 'not-parked', taskId, status };
|
|
89
|
+
// ── 二级分诊:status 表回退(kind 缺席 —— 旧引擎 / store 降级)────────────────────────────
|
|
90
|
+
if (PLAN_REVIEW_STATES.includes(status))
|
|
91
|
+
return planReviewArm(taskId, signal, deps);
|
|
92
|
+
if (ASK_PARK_STATES.includes(status))
|
|
93
|
+
return askParkArm(taskId, signal, deps);
|
|
94
|
+
return { kind: 'not-parked', taskId, status };
|
|
95
|
+
}
|
|
96
|
+
/** plan_review 门:重开卡,把决定权还给用户。静默 cancel 在这里 = 替他 reject 掉整个 plan。 */
|
|
97
|
+
function planReviewArm(taskId, signal, deps) {
|
|
98
|
+
let verdict = { reopened: false };
|
|
99
|
+
try {
|
|
100
|
+
verdict = deps?.reopenPlanReview?.(taskId) ?? { reopened: false };
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
verdict = { reopened: false }; // 重开腿的意外 throw 不许炸 turn 收尾;按没能重开如实说
|
|
104
|
+
}
|
|
105
|
+
return reopenDelivered(verdict)
|
|
106
|
+
? { kind: 'plan-review-reopened', taskId, firstSight: verdict.firstSight === true }
|
|
107
|
+
: { kind: 'plan-review-reopen-failed', taskId, decidePath: signal.pendingGate?.decidePath ?? null };
|
|
108
|
+
}
|
|
109
|
+
/** 审批/提问门:重开卡。cancel 在这里的语义 = 替用户否掉待决项,且实证是循环病根 —— 臂已退役。 */
|
|
110
|
+
async function askParkArm(taskId, signal, deps) {
|
|
111
|
+
let verdict = { reopened: false };
|
|
112
|
+
try {
|
|
113
|
+
verdict = (await deps?.reopenAskPark?.(taskId)) ?? { reopened: false };
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
verdict = { reopened: false }; // 同上:意外 throw 按没能重开如实说,绝不回退 cancel
|
|
117
|
+
}
|
|
118
|
+
return reopenDelivered(verdict)
|
|
119
|
+
? { kind: 'ask-reopened', taskId, firstSight: verdict.firstSight === true }
|
|
120
|
+
: { kind: 'ask-reopen-failed', taskId, decidePath: signal.pendingGate?.decidePath ?? null };
|
|
121
|
+
}
|
|
122
|
+
/** cli 的唯一真出路(`/clear` = 新会话 id ⇒ 引擎侧全新 session ⇒ 不受旧 claim 影响)。 */
|
|
123
|
+
const DEFAULT_WAY_OUT = 'run /clear to keep working in a fresh session';
|
|
124
|
+
const DEFAULT_FRESH_SESSION = 'start a new session (drop --resume/--continue)';
|
|
125
|
+
/**
|
|
126
|
+
* 「在等哪一种决定」的人话 —— **只由 wire 给的 `pendingGate.kind` 铸**。引擎对未知 gate 种类
|
|
127
|
+
* 自己返 null,这里照实说「一个决定」,绝不按 kind 猜一张表(猜错 = 又一句「按了没反应」)。
|
|
128
|
+
*/
|
|
129
|
+
function gateKindPhrase(kind) {
|
|
130
|
+
return kind ? `a ${kind} decision` : 'a decision';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 409 体材料 → 「怎么解」的那一句。🔴 只在 wire 真给了 `pendingGate.decidePath` 时才存在;
|
|
134
|
+
* 材料缺席 = 空串,由基句自己承担 —— 绝不自造一条路由。
|
|
135
|
+
*/
|
|
136
|
+
function decidePathClause(signal) {
|
|
137
|
+
const decidePath = signal?.pendingGate?.decidePath;
|
|
138
|
+
if (!decidePath)
|
|
139
|
+
return '';
|
|
140
|
+
return (` The engine says that run is waiting on ${gateKindPhrase(signal?.pendingGate?.kind)} and that it is` +
|
|
141
|
+
` decided at POST ${decidePath} — deciding it releases the session.`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 出身归因(`pendingGate.governanceForced`,server ≥7.13 / SDK 6.15.0 同窗上 409 体)。
|
|
145
|
+
* 治理强制的门不是用户自己配的规则 —— 「去 settings 里把它关掉」在这类门上是假出路;说清出身,
|
|
146
|
+
* 用户才知道该找谁。🔴 只在位真为 true 时存在;缺席一个字都不说(缺席 ≠「这不是治理门」,
|
|
147
|
+
* 渲一句否定 = 把证不出的断言当事实)。
|
|
148
|
+
*/
|
|
149
|
+
function governanceOriginClause(signal) {
|
|
150
|
+
return signal?.pendingGate?.governanceForced === true
|
|
151
|
+
? ' That gate is enforced by this deployment\'s governance policy, not by a permission rule you set.'
|
|
152
|
+
: '';
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 每种结局的上屏整行。**每一句出路都真的接了线**;没有真出路的分支就直说「这个会话暂时无法
|
|
156
|
+
* 继续」。重开成功那条也要上屏 —— 卡是替用户重新打开的,他得知道去答哪张、答完做什么。
|
|
157
|
+
* wire 真到得了端的两个 additive 键是 `activeTaskStatus` 与 `pendingGate`,各自有真消费点;
|
|
158
|
+
* 不为「说不定哪天上游会发」的键留悬空分支。
|
|
159
|
+
*/
|
|
160
|
+
export function activeRunSelfHealRow(outcome, signal, copy) {
|
|
161
|
+
const override = copy?.rowFor?.(outcome, signal);
|
|
162
|
+
if (typeof override === 'string')
|
|
163
|
+
return override;
|
|
164
|
+
const base = activeRunSelfHealBaseRow(outcome, signal, copy?.wayOut ?? DEFAULT_WAY_OUT);
|
|
165
|
+
switch (outcome.kind) {
|
|
166
|
+
// 这两条是「用户此刻卡住了、而且没有别的把手」的结局 —— wire 给的 decide 入口在这里才有用。
|
|
167
|
+
case 'not-parked':
|
|
168
|
+
case 'state-unknown':
|
|
169
|
+
return base + decidePathClause(signal) + governanceOriginClause(signal);
|
|
170
|
+
// 其余结局手上都另有一张更好的把手(重开的卡 / 已开的卡 / 失败臂基句自己渲的 decidePath)
|
|
171
|
+
// —— 再叠一条引擎路由只会稀释那一个动作。
|
|
172
|
+
default:
|
|
173
|
+
return base + governanceOriginClause(signal);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function activeRunSelfHealBaseRow(outcome, signal, wayOut) {
|
|
177
|
+
switch (outcome.kind) {
|
|
178
|
+
case 'decision-pending':
|
|
179
|
+
return (`This session is held by an earlier turn${outcome.taskId ? ` (run ${outcome.taskId})` : ''} that ` +
|
|
180
|
+
`is waiting on a decision you have not made yet, and there is a decision card open for it. ` +
|
|
181
|
+
`sema did NOT cancel it — answering that card is what releases the session, and cancelling ` +
|
|
182
|
+
`would decide it for you. Your message was NOT sent; answer the open card and send it again.`);
|
|
183
|
+
case 'ask-reopened':
|
|
184
|
+
// 🔴 首见/复见分形:首见卡(本进程从未呈现过这张 pending 卡 —— 典型链:上一 turn 的
|
|
185
|
+
// approve/answer 把引擎推进到了**新** gate,本次提交撞 409 时那张新卡还没呈现过)绝不
|
|
186
|
+
// 说 closed/reopened —— 对没发生过的历史下断言 = 谎报面。动作两形一致,只有话不同。
|
|
187
|
+
if (outcome.firstSight) {
|
|
188
|
+
return (`The previous turn advanced to a decision it needs from you (run ${outcome.taskId}), which is ` +
|
|
189
|
+
`why nothing new could start — sema has surfaced that decision card. Answer it and send your ` +
|
|
190
|
+
`message again. (sema did NOT cancel the run: cancelling would have decided it for you.)`);
|
|
191
|
+
}
|
|
192
|
+
return (`The previous turn is parked waiting for your decision (run ${outcome.taskId}), which is why ` +
|
|
193
|
+
`nothing new could start — that card was closed without being answered, so sema reopened it. ` +
|
|
194
|
+
`Answer it and send your message again. (sema did NOT cancel the run: cancelling would have ` +
|
|
195
|
+
`decided it for you.)`);
|
|
196
|
+
case 'plan-review-reopened':
|
|
197
|
+
// 同上分形:首见 plan 卡(如 resume 到一个在别处 park 的会话)不断言「被关过」。
|
|
198
|
+
if (outcome.firstSight) {
|
|
199
|
+
return (`The previous turn produced a plan that is waiting for your review (run ${outcome.taskId}), ` +
|
|
200
|
+
`which is why nothing new could start — sema has surfaced the review card. Answer it and send ` +
|
|
201
|
+
`your message again. (sema did NOT cancel the run: cancelling would have thrown the plan away ` +
|
|
202
|
+
`on your behalf.)`);
|
|
203
|
+
}
|
|
204
|
+
return (`The previous turn is parked waiting for you to review its plan (run ${outcome.taskId}), which is ` +
|
|
205
|
+
`why nothing new could start — that approval card was closed without being answered, so sema ` +
|
|
206
|
+
`reopened it. Answer it and send your message again. (sema did NOT cancel the run: cancelling ` +
|
|
207
|
+
`would have thrown the plan away on your behalf.)`);
|
|
208
|
+
case 'ask-reopen-failed': {
|
|
209
|
+
const viaEngine = outcome.decidePath
|
|
210
|
+
? ` You can also decide it on the engine directly: POST ${outcome.decidePath}.`
|
|
211
|
+
: '';
|
|
212
|
+
return (`The previous turn is parked waiting for your decision (run ${outcome.taskId}) and sema could ` +
|
|
213
|
+
`not reopen that card here. It did NOT cancel the run — that would have decided it for you. ` +
|
|
214
|
+
`Your message was NOT sent; ${wayOut} if you no longer want it.${viaEngine}`);
|
|
215
|
+
}
|
|
216
|
+
case 'plan-review-reopen-failed': {
|
|
217
|
+
// decidePath 来自 409 body(引擎给的真路由);拿不到就不提它 —— 宁可少给一条路,
|
|
218
|
+
// 也不自己按 gate kind 猜一个入口(猜错 = 又一句「按了没反应」)。
|
|
219
|
+
const viaEngine = outcome.decidePath
|
|
220
|
+
? ` You can also decide it on the engine directly: POST ${outcome.decidePath}.`
|
|
221
|
+
: '';
|
|
222
|
+
return (`The previous turn is parked waiting for a plan review (run ${outcome.taskId}) and sema could not ` +
|
|
223
|
+
`reopen that approval card. It did NOT cancel the run — that would have discarded the plan for ` +
|
|
224
|
+
`you. Your message was NOT sent; ${wayOut} if you no longer want that plan.${viaEngine}`);
|
|
225
|
+
}
|
|
226
|
+
case 'not-parked':
|
|
227
|
+
// 🔴 「等它跑完」这句只在**它真的在跑**的时候是真话。分诊表外的门/状态里也可能是 park
|
|
228
|
+
// (表外 gate kind 正是走这条),而 wire 恰恰会在那种情况下带 pendingGate —— 对一个
|
|
229
|
+
// parked run 说「wait for it to finish」是一句永远等不到的假出路。动作仍然一个字不改
|
|
230
|
+
// (不动它),改的只是话:有待决材料时就说清它在等人、怎么答。
|
|
231
|
+
if (signal?.pendingGate) {
|
|
232
|
+
return (`This session is held by an earlier run (run ${outcome.taskId}, status ${outcome.status}) that is ` +
|
|
233
|
+
`waiting on ${gateKindPhrase(signal.pendingGate.kind)} — it will not finish on its own. sema did ` +
|
|
234
|
+
`NOT cancel it, because cancelling would decide that item for you. Your message was NOT sent; ` +
|
|
235
|
+
`${wayOut} if you no longer want it.`);
|
|
236
|
+
}
|
|
237
|
+
return (`This session already has a run in flight (run ${outcome.taskId}, status ${outcome.status}) — ` +
|
|
238
|
+
`most likely a turn you sent to the background. sema did NOT cancel it, because that would ` +
|
|
239
|
+
`throw away work you asked for. Wait for it to finish, or ${wayOut}.`);
|
|
240
|
+
case 'state-unknown':
|
|
241
|
+
return (`This session is locked by an earlier run${outcome.taskId ? ` (run ${outcome.taskId})` : ''} and ` +
|
|
242
|
+
`sema could not read that run's state, so it did not touch it (${outcome.detail}). ` +
|
|
243
|
+
`Your message was NOT sent. This session cannot start a new turn until that run settles — ` +
|
|
244
|
+
`${wayOut}.`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 非交互车道(headless `-p` 等)对同一事实的诚实说法:那里没有 `/clear`,默认串指的是「别再
|
|
249
|
+
* `--resume` 到这个会话」和引擎自己的 cancel/decide 端点 —— 都是真的存在、真的能做的动作。
|
|
250
|
+
* 这条车道没有自愈腿,材料被吞的代价是全额的:一个「答一下就解锁」的会话会被一句「去 cancel」
|
|
251
|
+
* 打发走,而 cancel 恰恰是替用户否掉那个待决项 —— 所以 decide 入口在场时它才是首选出路。
|
|
252
|
+
*/
|
|
253
|
+
export function activeRunBusyHeadlessRow(signal, copy) {
|
|
254
|
+
const override = copy?.headlessRowFor?.(signal);
|
|
255
|
+
if (typeof override === 'string')
|
|
256
|
+
return override;
|
|
257
|
+
const activeTaskId = signal.activeTaskId;
|
|
258
|
+
const handle = activeTaskId ? ` (run ${activeTaskId})` : '';
|
|
259
|
+
const cancelPath = `POST /v1/runs/${activeTaskId ?? '<id>'}/cancel`;
|
|
260
|
+
const status = signal.activeTaskStatus;
|
|
261
|
+
const decidePath = signal.pendingGate?.decidePath ?? null;
|
|
262
|
+
const freshSession = copy?.freshSession ?? DEFAULT_FRESH_SESSION;
|
|
263
|
+
// ① 引擎给了 decide 入口 ⇒ **那**才是首选出路,cancel 降为带价签的备选。
|
|
264
|
+
if (decidePath) {
|
|
265
|
+
return (`This session is held by an earlier run${handle}${status ? ` (status ${status})` : ''} that is parked ` +
|
|
266
|
+
`waiting on ${gateKindPhrase(signal.pendingGate?.kind)}, so this message was NOT sent. Decide it and ` +
|
|
267
|
+
`the session unlocks: POST ${decidePath}. Cancelling instead (${cancelPath}) also unlocks it, but it ` +
|
|
268
|
+
`decides that item against you. Or ${freshSession}.` +
|
|
269
|
+
// 出身归因同款(见 governanceOriginClause):这条车道更需要它 —— 没有卡可看,
|
|
270
|
+
// 用户唯一能读到的就是这一段话。
|
|
271
|
+
governanceOriginClause(signal));
|
|
272
|
+
}
|
|
273
|
+
// ② 状态在场但引擎明说无待决项 ⇒ 至少说清是谁占着、它是什么态。cancel 的代价按状态**不硬编码
|
|
274
|
+
// 一张表**(动作/文案都不许按状态名枚举),两种可能都摆出来。
|
|
275
|
+
if (status) {
|
|
276
|
+
return (`This session is held by an earlier run${handle} with status ${status}, so this message was NOT sent. ` +
|
|
277
|
+
`The engine reported no decision pending on that run: if it is still working, wait for it to finish; ` +
|
|
278
|
+
`if it will never settle, release it with ${cancelPath} (that discards whatever it was doing). ` +
|
|
279
|
+
`Or ${freshSession}.`);
|
|
280
|
+
}
|
|
281
|
+
// ③ 材料全缺(旧引擎 / store 降级)⇒ activeTaskId 与 cancel 是恒在的保底真路(SDK 6.3.0 契约)。
|
|
282
|
+
// freshSession 覆盖在这条也生效(句首形);默认串首字母大写后与源形逐字节相同。
|
|
283
|
+
const startOver = `${freshSession.charAt(0).toUpperCase()}${freshSession.slice(1)}`;
|
|
284
|
+
return (`This session is locked by an earlier run${handle} that was never released, so this message was ` +
|
|
285
|
+
`NOT sent. Nothing here clears on its own. ${startOver}, or ` +
|
|
286
|
+
`release the run on the engine: ${cancelPath}`);
|
|
287
|
+
}
|
|
@@ -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。
|
|
@@ -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 {
|
|
@@ -311,8 +315,8 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
311
315
|
// 像 provider 故障,是误导。识别该终帧 → 一行专属文案(告知在等什么 + 出路);其它错误保持
|
|
312
316
|
// 原 API Error 行(未知错误 fallback 不变)。引擎侧解锁腿已到货([868]:cancel 对
|
|
313
317
|
// suspended/needs_review 就地终态化 ⇒ claim 释放),所以文案指的就是那个端点;
|
|
314
|
-
//
|
|
315
|
-
//
|
|
318
|
+
// 自愈分诊腿(重开卡把决定权还给用户;cancel+重发臂已退役)在 `adapter/activeRunSelfHeal.ts`
|
|
319
|
+
// (A-028.1 上收,宿主经 deps 注入重开口),本层只负责:识别 + 说真话。
|
|
316
320
|
const activeTaskId = failedResult?.activeTaskId ??
|
|
317
321
|
(ev.type === 'failed' ? ev.activeTaskId : undefined);
|
|
318
322
|
// REF-CC-054:判别**结构位两腿**(errorCode → activeTaskId;文案兜底腿 2026-08-03 已退役),
|
|
@@ -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;
|