@sema-agent/client-core 0.63.0 → 0.63.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 CHANGED
@@ -49,6 +49,103 @@
49
49
  > 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
50
50
  > commit 漏转在发布前就红,不再靠人记。
51
51
 
52
+ ## 0.63.2(2026-09-09)
53
+
54
+ ### 修复
55
+
56
+ - **终答补差臂在「工具卡后零散文」形下把整段终帧再渲一遍**(cli 1.0.105 收官 daemon-reattach 套实撞:
57
+ mock 车道「散文 → Config 卡 → turn 收口」屏上同一段话两遍;真引擎上 turn 收在工具之后同形)。根因:工具
58
+ 边界把「当前消息已上屏正文」重置成空串,而 provider 一条 assistant 消息 = `[text, tool_use]`,引擎「最后一条
59
+ 消息的文本」正是卡**之前**那段;空串是任何终帧的前缀 ⇒ 补差臂把整段终帧当缺的后缀补出。修:比较对象改为
60
+ `lastCommittedAnswerText`(当前消息零上屏时回退到上一条真有正文的消息;有字时与修前逐字同)。门:
61
+ `run-result-text-backfill` +G5/G5b(卡后零散文 + 终帧 = 卡前那段 ⇒ 零补;终帧多一截 ⇒ 只补后缀);变异
62
+ (比较源改回 `committedAnswerText`)⇒ G5 红。§26g 加订正段。宿主零换装。
63
+
64
+ ## 0.63.1(2026-09-09)
65
+
66
+ test 对 0.63.0 的黑盒回执(G-1…G-20 五路交叉)提了六项;逐项亲核后 **四修二订**。本段只记内容,
67
+ 不含任何 bump。
68
+
69
+ ### 修复
70
+
71
+ - 🔴 **workflow 监视器把「停在审批门上」渲成「在跑」**(G-20)。engine core 7.10.0 起一条 workflow
72
+ **agent 腿**可以耐久挂在审批卡上(`WorkflowItemStatus` +`parked`,`deriveAgentDisplayStatus` 直接产
73
+ `"parked"`),而本包监视器车道的窄读把它折进了开集兜底 —— 屏上那一行说的是「等它跑完」,实际它在
74
+ 有人做决定之前**永远不会**自己往前走一步。修:四处窄读各给 `parked` 一条自己的臂
75
+ (`coerceAgentState` / `agentDisplayStatus` / `coerceRunStatus` / `coercePhaseStatus` + 合成格三元式),
76
+ 开集兜底一个字节不动。🔴 **`agentDisplayStatus` 的这条臂必须排在 `!workflowActive` 之前** —— park
77
+ 之后整只 run 收在 `failed`,排在后面就永远被 `interrupted` 抢答。
78
+ 型面(additive,端的穷尽形要各加一条 case):`AgentState` / `DisplayStatus` /
79
+ `WorkflowRunState['status']` / `WorkflowPhase['status']` 各 +`parked`。详见 §27h3、换装清单 §27i ⑨。
80
+
81
+ - **同一条 wire 的同一个 `status`,三处在场判不同形**(G-20 附带②)。`parseWorkflowPollEnvelope` 判
82
+ `length > 0`(空串=缺席),而 `projectWorkflowTaskOutput` 的 per-agent 与 `projectAgent` 的
83
+ `displayStatus` 只判 `typeof`(空串=在场)。后果:前者把空串渲成 `[]` —— 一对什么也没说的方括号,
84
+ 却长得像一个真答案;后者更坏 —— `'' ?? row.status` 恒取空串(`??` 只挡 null/undefined),那条腿的
85
+ raw `status` 被**静默丢掉**,连回落都走不到(`parked` 正是从这条缝里漏的)。三处统一成
86
+ **空串 = 缺席**;纯空白仍不 trim(两侧同尺)。
87
+
88
+ ### 文档 / 门
89
+ - **异源对抗复审 R2 三条 [medium] 全采纳**(仍全部打在 E 段这条 G-8 承重腿上,三条都真能复现):
90
+ ①`underAnyBranch` 漏了**循环** —— 零次循环里的铸点被认作「无条件」⇒ 补 `for`/`for-of`/`for-in`/
91
+ `while`/`do` 五形;②**铸点与 `/health` 之间的接线没验** —— 把引擎 deployment 对象里的
92
+ `instanceId,` 删掉,两条断言照样绿而实际 `deps.instanceId` 已是 `undefined`、条件 spread 会把这一位
93
+ 整只省掉 ⇒ 新增 E3 接线断言(铸出的值必须真被挂到一只对象上);③**条件 spread 被当成必然输出** ——
94
+ 守卫换成别的旗子、或经包装调用摊进来,都被判成「带上了」⇒ 改成只认「守卫就是那个值本身」的形,
95
+ 包装调用一律判不出(返回形状是运行期的事,不替它编答案)。
96
+ 同批把射程边界写进段头与 §27d:**证的是源码形**(挡上游改锚/改铸法/断接线),**不证**「这段码真被
97
+ 执行」——那要一次真启动的 `/health` 响应,不在本包射程内(本包不起引擎)。
98
+ - **自查同批补两条**(不在复审名单里,同一条病形):①`hasProp` 原是整棵子树遍历 ⇒ `restart: {…}`
99
+ 这类**嵌套值**里的同名键会被算进 `/health` 的键空间,改成「直挂 + 沿 spread 下行」;
100
+ ②①c 的 `statedTableSize` 取第一处命中 ⇒ 同名两处各写一个数时放过另一处,改成**恰一处**才作数;
101
+ 并加「档里每一处带表长的签名行都必须已登记」的漏登即红(新增一张带数字的表时门不再沉默)。
102
+
103
+ - **异源对抗复审 R1 三条 [medium] 全采纳**(都打在本批**新加的门**上,三条都是假绿):
104
+ ①`run-engine-identity-test.mjs` E 段原是两条裸正则 —— 条件里铸、注释残留、挂在别的对象上三类反例
105
+ 都骗得过去 ⇒ 改锚 AST(控制流 + 路由两轴),并把三类反例做成常驻反向正控;
106
+ ②同段见证缺席时仍报「全绿」⇒ 改成未注入打 `SKIPPED-SECTION`(汇总显形 PARTIAL)、注入了却拿不到
107
+ 物料**判红**,结语逐字说清这一段跑没跑;
108
+ ③`run-integration-doc-freshness-test.mjs` ①c 把 AST 节点数当运行期表长 —— 展开元素算一员、任意
109
+ 单参调用都收 ⇒ 改成逐条 fail-closed(只认 `Object.freeze(<全字符串字面量数组>)`),复审给的原样
110
+ 反例(把一员换成展开)现在当场判工具红。
111
+
112
+ - **`/health` 的 `pid` 单锚碰撞**(G-8):裁读 = **设计不改,判据收窄 + 取证入门**。「只剩 `pid` 可比
113
+ 且相等」要求 `instanceId` 在两侧都缺席,而 `instanceId` 在引擎里是**无条件铸**的、`/health` 一路
114
+ 带着它 ⇒ 那种读数在生产上到不了;反过来把它判成 `unknown`,会让一台只报得出 `pid` 的老 worker
115
+ **每一次心跳**都触发重握手 —— 那是拿一个每拍都发生的代价去换一个到不了的风险。
116
+ 落点:§27d 加一段写清「pid 是最弱锚、从不落单」+ 端的兜底句;`run-engine-identity-test.mjs` 加
117
+ **D 段**(设计钉:pid 单锚相等仍 same / pid 相等压不住更强的锚 / 优先序里 pid 是表尾)与
118
+ **E 段**(引擎产物第二见证,**锚 AST 不锚子串**:铸点不在任何 `if`/`switch`/三元/`??` 里,且
119
+ `instanceId` 挂在 `/health` **那一只**响应体上 —— 按 `status:"ok"` + `pid: process.pid` 两键定位。
120
+ 🔴 `SEMA_FENCE_ENGINE_DIR` 未注入 ⇒ 打 `SKIPPED-SECTION`(汇总显形 PARTIAL,不冒充全绿);
121
+ 注入了却拿不到物料 ⇒ **判红**。发布批必须注入一次引擎树把这一段跑绿,才能引用本条的结论)。
122
+
123
+ - **§27h 的 `engine_notice` 码册数字滞后**(G-16):档写「四十八码」,源码那张表(与它的头注)早就是
124
+ **50** 员 —— 上游加了两个码,档没跟。修:数字订正,**并把它变成有牙的** —— `run-integration-doc-
125
+ freshness-test.mjs` 新增 **①c 段**:签名块里的表长数字 ⟷ 源码那张表的真实成员数(TS AST 取
126
+ `Object.freeze([...])` 的元素个数)。同批把同形的五张表全部登记进去(`ENGINE_NOTICE_CODES` /
127
+ `MCP_INJECTION_DROP_REASONS` / `GATE_DENIED_BY_WORDS` / `ASK_ORIGIN_WORDS` /
128
+ `PERMISSION_RULE_ISSUE_CODES`),并把这几处的中文数词改成阿拉伯数字 —— 读不出来的门等于没有门,
129
+ 所以抽取器读不出时判**工具红**而不是静默放过。
130
+
131
+ - 🔴 **表外 lint 码被 60 字符封长,与文档「原样带上」冲突**(G-11)。码是**引擎产的标识符**不是散文 ——
132
+ 运维要拿它整只去问上游,截一半的码谁也查不出来,还长得像一个完整答案。修:码**原样带上不封长**,
133
+ 呈前只走本包同一只不可见字符消毒器(不封长 ≠ 不消毒);`message` 半场的封长照旧(那半场确实是
134
+ 散文)。同族其余三处 `capForDisplay` 的词位(posture / read-face / write-protection 的来源词)
135
+ 刻意不动:那些是**有界词表**里的词、且文档从未承诺「原样」,与本条不同形。详见 §27f、§27i ⑤。
136
+
137
+ - 🔴 **`postureKnobDetail` 的判序反了**(G-3)。旧序先看「读数在不在」再看 `reachable` ⇒ 端拿着上一拍
138
+ 的 view、这一拍 operator 探测打不通、只翻 `reachable:false` 时,屏上照渲**上一拍的值和来源词**,
139
+ 一个字都看不出这一次根本没读到 —— 拿陈读数冒充现势。修:**`!reachable` 先判**,压过任何在手的
140
+ 读数(打不通时旧读数不是现势),那一句仍是既有的「未观测」原文,值与来源词一个都不漏。
141
+ 端因此**不必**再为打不通而先把 view 清空。详见 §27b、换装清单 §27i ①。
142
+
143
+
144
+ - **§27h3 整节重写**:0.63.0 原文两处失真留痕订正 —— ①「core 7.10.0 给 **run** 状态加了 parked」错,
145
+ 加的是 **agent 行**那一级(run 词表今天仍闭在 running|completed|failed);②「本包零改」只对
146
+ TaskOutput 文本车道成立,监视器投影车道当时正在折它。同节把 `readWorkflowRunLine`(本包零命中)
147
+ 订正成真名 `parseWorkflowPollEnvelope`。§27j 的 G-20 判据同批订正口径(park 落在腿上)。
148
+
52
149
  ## 0.63.0(2026-09-09)
53
150
 
54
151
  对表 **sdk 8.8.0 / engine ≥7.67.0 / core 7.10.0**(devDep core `~7.3.0` → `~7.10.0`)。sdk 地板 `>=8.5.0` → `>=8.8.0`
package/README.md CHANGED
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
35
35
 
36
36
  ## Scope
37
37
 
38
- **Version:** 0.63.0
38
+ **Version:** 0.63.2
39
39
 
40
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
41
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -652,7 +652,9 @@ const resultArm = function* (m, { ctx, idOf, text, cards, panel, flags }) {
652
652
  yield* text.takeAnswerSegment();
653
653
  // 具名成 terminalText:M1 收编后 `text` 是流合并器的名字,原来的同名局部会遮蔽它。
654
654
  const terminalText = typeof m.result === 'string' ? m.result : '';
655
- const committed = text.committedAnswerText;
655
+ // 🔴 比较对象 = 引擎「最后一条 assistant 消息」在屏上那段(0.63.2):卡后零散文时回退到卡前那条
656
+ // (provider 一条消息 = [text, tool_use];空串是任何终帧的前缀 ⇒ 拿空串比会把整段终帧再渲一遍)。
657
+ const committed = text.lastCommittedAnswerText;
656
658
  const emitTerminal = function* (body, tag) {
657
659
  yield transcript({
658
660
  type: 'assistant',
@@ -87,6 +87,16 @@ export interface TextStream {
87
87
  * 这一个记的是「屏上这条消息已经有了哪些字」,是**补差判据的输入**。
88
88
  */
89
89
  readonly committedAnswerText: string;
90
+ /**
91
+ * 引擎「最后一条 assistant 消息」在屏上对应的那段正文 —— 终答补差的**正确比较对象**(0.63.2)。
92
+ *
93
+ * 🔴 与 {@link TextStream.committedAnswerText} 的差别只在一格:工具卡是这一轮最后一件事、卡后
94
+ * **零散文**时,`committedAnswerText` 已被边界重置成空串,而 provider 的一条 assistant 消息 =
95
+ * [text, tool_use] 两块 —— 引擎取的「最后一条消息的文本」正是卡**之前**那段。空串是任何终帧的
96
+ * 前缀,拿它去比会把整段终帧当「缺的后缀」再渲一遍(同一段话上屏两遍)。⇒ 当前消息一个字都没
97
+ * 上屏时,回退到**上一条真有正文的消息**那段;当前消息有字时与 `committedAnswerText` 逐字同。
98
+ */
99
+ readonly lastCommittedAnswerText: string;
90
100
  /**
91
101
  * **一条新的 assistant 消息从这里开始**(重置 {@link TextStream.committedAnswerText})。
92
102
  *
@@ -49,6 +49,8 @@ export function createTextStream(ctx, idOf) {
49
49
  let emittedAssistantText = false;
50
50
  /** **当前这条 assistant 消息**已提交上屏的正文(补差判据的输入;与 `answer` 刻意分家,见接口注)。 */
51
51
  let committedText = '';
52
+ /** 上一条**真有正文**的 assistant 消息在屏上的那段(见 lastCommittedAnswerText 头注)。 */
53
+ let previousCommittedText = '';
52
54
  let textPending = '';
53
55
  let thinkingPending = '';
54
56
  let textBlockOpen = false;
@@ -203,11 +205,16 @@ export function createTextStream(ctx, idOf) {
203
205
  committedText += committed;
204
206
  },
205
207
  beginAssistantMessage: () => {
208
+ if (committedText.length > 0)
209
+ previousCommittedText = committedText;
206
210
  committedText = '';
207
211
  },
208
212
  get committedAnswerText() {
209
213
  return committedText;
210
214
  },
215
+ get lastCommittedAnswerText() {
216
+ return committedText.length > 0 ? committedText : previousCommittedText;
217
+ },
211
218
  get emittedAssistantText() {
212
219
  return emittedAssistantText;
213
220
  },
@@ -34,7 +34,7 @@
34
34
  * `message` 是引擎/用户 settings 侧的内容(里面会带用户自己写的规则文本):只渲染,呈前**先转义、
35
35
  * 后按转义结果封长**(与本包其余 detail 铸点同一条纪律),绝不当码用、绝不做等值匹配。
36
36
  */
37
- import { capForDisplay } from './fleetTaskDesc.js';
37
+ import { capForDisplay, escapeDisplayControlChars } from './fleetTaskDesc.js';
38
38
  /**
39
39
  * 引擎**真会铸**的规则 lint 拒因码(core `PermissionRuleIssue.code`,7.9.x = 十五码)。
40
40
  * 顺序与 core 声明同源。
@@ -114,8 +114,13 @@ export function permissionRuleIssueDetail(code, message) {
114
114
  const text = typeof message === 'string' && message.length > 0
115
115
  ? capForDisplay(message, ISSUE_MESSAGE_MAX)
116
116
  : '';
117
+ // 🔴 **码原样带上,不封长**(0.63.1,G-11):`code` 是**引擎产的标识符**,不是散文 —— 运维要拿它
118
+ // 整只去问上游,截一半的码谁也查不出来,而且长得像一个完整答案。呈前只做**不可见字符消毒**
119
+ // (走本包同一只 {@link escapeDisplayControlChars};不封长 ≠ 不消毒)。`message` 半场的封长
120
+ // 照旧:那半场确实是散文,屏上的列宽预算是给它的。两半场判据分家是刻意的。
121
+ const codeWord = typeof code === 'string' && code.length > 0 ? escapeDisplayControlChars(code) : '(no code)';
117
122
  const head = known !== undefined
118
123
  ? known
119
- : `this rule was refused with ${typeof code === 'string' && code.length > 0 ? capForDisplay(code, 60) : '(no code)'}, a code newer than this client`;
124
+ : `this rule was refused with ${codeWord}, a code newer than this client`;
120
125
  return text.length > 0 ? `${head} — ${text}` : head;
121
126
  }
@@ -75,7 +75,9 @@ export declare function projectServerGateKnobs(wiring: unknown): ServerGateKnobs
75
75
  * ① 读数在场 —— `<旋钮> <值> (source: <来源> — <指路句>)`;
76
76
  * ② 读数在场但来自老 worker 的裸值(`legacy`)—— 报得出**值**,但逐字点明「这台 worker 说不出
77
77
  * 是谁定的」;**句中不出现任何来源词**(冒称来源比不说更坏:它会让人以为查过了);
78
- * ③ `opts.reachable === false` —— 「未观测」:这一次进程没读到 operator 响应;
78
+ * ③ `opts.reachable === false` —— 「未观测」:这一次进程没读到 operator 响应。
79
+ * 🔴 **这一格先判**(0.63.1):它压过任何在手的读数 —— 宿主拿着上一拍的 view、这一拍探测
80
+ * 打不通时,它能翻的只有这一位;先看读数在不在会照渲上一拍的值,等于拿陈读数冒充现势;
79
81
  * ④ `reachable:true` 但读数仍缺席 —— 「不报」:响应读到了,只是这一位读不出来。
80
82
  * **不武断咎为版本**:整键缺席(老引擎)与在场却形坏折成同一个缺席,句子不能替一种情形撒谎。
81
83
  * 🔴 ③④ 两句**一个值都不报** —— 它们要是顺口说了 `off`,消费端就会把「读不出」当成「关着」。
@@ -131,7 +131,9 @@ function knobValueWord(value) {
131
131
  * ① 读数在场 —— `<旋钮> <值> (source: <来源> — <指路句>)`;
132
132
  * ② 读数在场但来自老 worker 的裸值(`legacy`)—— 报得出**值**,但逐字点明「这台 worker 说不出
133
133
  * 是谁定的」;**句中不出现任何来源词**(冒称来源比不说更坏:它会让人以为查过了);
134
- * ③ `opts.reachable === false` —— 「未观测」:这一次进程没读到 operator 响应;
134
+ * ③ `opts.reachable === false` —— 「未观测」:这一次进程没读到 operator 响应。
135
+ * 🔴 **这一格先判**(0.63.1):它压过任何在手的读数 —— 宿主拿着上一拍的 view、这一拍探测
136
+ * 打不通时,它能翻的只有这一位;先看读数在不在会照渲上一拍的值,等于拿陈读数冒充现势;
135
137
  * ④ `reachable:true` 但读数仍缺席 —— 「不报」:响应读到了,只是这一位读不出来。
136
138
  * **不武断咎为版本**:整键缺席(老引擎)与在场却形坏折成同一个缺席,句子不能替一种情形撒谎。
137
139
  * 🔴 ③④ 两句**一个值都不报** —— 它们要是顺口说了 `off`,消费端就会把「读不出」当成「关着」。
@@ -145,6 +147,14 @@ export function postureKnobDetail(knob, opts) {
145
147
  // 塞进 `toString`,裸下标会命中 `Object.prototype` 上的函数并被拼进句子。读不出名字时退到
146
148
  // 一个中性词:**名字读不出不该让「这根旋钮的值是什么」也一起说不出来**。
147
149
  const label = Object.hasOwn(KNOB_LABELS, opts.knob) ? KNOB_LABELS[opts.knob] : 'posture knob';
150
+ // 🔴 **判序承重**(0.63.1,G-3):`!reachable` **先判**,压过任何在手的读数。调用惯例上宿主手里
151
+ // 就只有上一拍那一只 view —— 探测这一拍打不通时它能翻的只有 `reachable`,别的没得换。先看
152
+ // `knob !== undefined` 的旧序会照渲**上一拍的读数**,屏上一个字都看不出这一次根本没读到,
153
+ // 等于把陈读数冒充现势。打不通时旧读数不是现势:值与来源词一个都不许漏出去。
154
+ // ⇒ 求值序是 ③ → ①/② → ④(与上方四句的编号次序无关,编号只是四种情形的清单)。
155
+ if (!opts.reachable) {
156
+ return `${label} not observed (this end could not read the engine's diagnostics)`;
157
+ }
148
158
  if (knob !== undefined) {
149
159
  const word = knobValueWord(knob.value);
150
160
  if (knob.legacy === true) {
@@ -156,9 +166,6 @@ export function postureKnobDetail(knob, opts) {
156
166
  ? `${label} ${word} (source: ${source} — ${note})`
157
167
  : `${label} ${word} (source: ${source})`;
158
168
  }
159
- if (!opts.reachable) {
160
- return `${label} not observed (this end could not read the engine's diagnostics)`;
161
- }
162
169
  return `${label} not reported by this engine (an engine below 7.67.0, or a response this end could not parse)`;
163
170
  }
164
171
  /**
package/dist/workflow.js CHANGED
@@ -96,7 +96,10 @@ export function projectWorkflowTaskOutput(jsonSrc) {
96
96
  if (!r || typeof r !== 'object')
97
97
  continue;
98
98
  const label = typeof r.label === 'string' && r.label.length > 0 ? r.label : '(agent)';
99
- const st = typeof r.status === 'string' ? r.status : '?';
99
+ // 🔴 `parseWorkflowPollEnvelope` **同一判**(0.63.1,G-20 附带②):同一条 wire 上的
100
+ // 同一个字段,两条链路的「在场」判据必须同形。空串走 `typeof` 单门会被当成一个读数
101
+ // 渲成 `[]` —— 一对什么也没说的方括号,还长得像一个真答案。空串 = 缺席。
102
+ const st = typeof r.status === 'string' && r.status.length > 0 ? r.status : '?';
100
103
  const errLine = typeof r.error === 'string' && r.error.length > 0
101
104
  ? ` — ${r.error.split('\n')[0].slice(0, 120)}`
102
105
  : '';
@@ -67,9 +67,23 @@ import { hostLog } from './host.js';
67
67
  // (workflowMonitor.ts) is the 187-faithful consumed set. We read the known fields, coerce
68
68
  // the vocabularies, and fill the shell-local render defaults the wire does not carry — same discipline as
69
69
  // projectTasks/projectWorkflows in fleet/fleetProjection.ts.
70
+ /**
71
+ * ── `parked` 在这一族投影里的纪律(0.63.1;G-20)────────────────────────────────────────────
72
+ * 「停在审批门上」是一个**说得出口的状态词**,不是一个未知的未来值:那条腿在有人做决定之前
73
+ * **永远不会**自己往前走一步,而 `running` 承诺的恰恰是「等着就好」。⇒ 下面四处窄读一律给它
74
+ * 一条**自己的臂**,开集兜底(未知未来值 → 活跃)一个字节都不动。
75
+ *
76
+ * 三级词表今天各自的真实供给(engine core 7.10.0,亲核):
77
+ * · **agent 行** —— `WorkflowItemStatus` 含 `parked`(#642),`deriveAgentDisplayStatus` 直接产
78
+ * `"parked"`(`AgentDisplayStatus` 六词)⇒ 这是**今天就会到**的读数。
79
+ * · **run 级** —— 仍闭在 running|completed|failed;一条腿 park ⇒ 整只 run 按
80
+ * `WorkflowAgentParkedError` 收在 `failed` ⇒ `parked` 今天到不了这一位,那条臂是**前向预挂**。
81
+ * · **phase 级** —— 上游明说「a phase or group never parks」,同为前向预挂;本包另从桶里的腿
82
+ * **推**出这一格(见 `projectPhases`)。
83
+ */
70
84
  /** run status → the monitor's run-level vocabulary. SDK: queued|running|completed|failed|(open). The monitor
71
- * run status is running|done|failed|stopped|paused — there is no run-level 'queued', so a queued run reads as
72
- * 'running' (the header shows the raw status text anyway; the box treats non-done/failed as active). */
85
+ * run status is running|done|failed|stopped|paused|parked — there is no run-level 'queued', so a queued run
86
+ * reads as 'running' (the header shows the raw status text anyway; the box treats non-done/failed as active). */
73
87
  function coerceRunStatus(s) {
74
88
  switch (s) {
75
89
  case 'completed':
@@ -78,6 +92,8 @@ function coerceRunStatus(s) {
78
92
  return 'failed';
79
93
  case 'running':
80
94
  return 'running';
95
+ case 'parked':
96
+ return 'parked'; // 前向预挂(见上方族头注):词真到了就原样透出,绝不折成「在跑」
81
97
  default:
82
98
  return 'running'; // queued / unknown-future → render as an active run (open-set fallback)
83
99
  }
@@ -94,6 +110,8 @@ function coercePhaseStatus(s) {
94
110
  case 'queued':
95
111
  case 'pending':
96
112
  return 'not-started';
113
+ case 'parked':
114
+ return 'parked'; // 前向预挂,同 coerceRunStatus
97
115
  default:
98
116
  return 'running';
99
117
  }
@@ -123,6 +141,11 @@ function coerceAgentState(s) {
123
141
  // !workflowActive — the run IS terminal whenever core emits this).
124
142
  case 'interrupted':
125
143
  return 'inactive';
144
+ // 🔴 core 7.10.0 #642:这条腿耐久挂在一张审批卡上(行上记着 checkpoint token)。**今天就会到**
145
+ // 的读数(raw `status` 与 `displayStatus` 两条腿都会带),原词透出——折进 `progress` 就是把
146
+ // 「要人去批」渲成「等它跑完」。
147
+ case 'parked':
148
+ return 'parked';
126
149
  default:
127
150
  return 'progress'; // unknown-future live status → render as an active agent (open-set fallback)
128
151
  }
@@ -174,7 +197,10 @@ function projectAgent(row, index) {
174
197
  // queued|running|done|failed|interrupted) over the RAW record `status` (running|completed|failed): the raw
175
198
  // vocabulary carries no queued/interrupted arm — a queued agent's record status is literally 'running', so
176
199
  // coercing `row.status` alone rendered every queued agent as "Running" (the [840] parity gap).
177
- const displayStatus = typeof rec.displayStatus === 'string' ? rec.displayStatus : undefined;
200
+ // 🔴 空串 = 缺席(0.63.1,G-20 同形存量⑤):`??` 只挡 null/undefined,一个空串的 `displayStatus`
201
+ // 会把 raw `status` 整个静默吃掉(`'' ?? row.status` === `''`)—— 那条腿真正的状态词连回落
202
+ // 这条路都走不到。与 `workflow.ts` 两条链路同尺:空串不是读数。
203
+ const displayStatus = typeof rec.displayStatus === 'string' && rec.displayStatus.length > 0 ? rec.displayStatus : undefined;
178
204
  const state = coerceAgentState(displayStatus ?? row.status);
179
205
  const activity = projectActivity(row.activity);
180
206
  const last = activity.length ? activity[activity.length - 1] : undefined;
@@ -276,10 +302,9 @@ function projectPhases(run, agents) {
276
302
  if (distinct.length > 0) {
277
303
  return distinct.map(title => {
278
304
  const bucket = agents.filter(a => a._phase === title);
279
- const allDone = bucket.every(a => a.state === 'done');
280
305
  return {
281
306
  title,
282
- status: bucket.some(a => a.state === 'error') ? 'failed' : allDone ? 'done' : 'running',
307
+ status: bucketStatus(bucket),
283
308
  doneCount: bucket.filter(a => a.state === 'done').length,
284
309
  totalCount: bucket.length,
285
310
  agents: bucket.map(stripPhase),
@@ -289,13 +314,45 @@ function projectPhases(run, agents) {
289
314
  return [
290
315
  {
291
316
  title: 'Workflow',
292
- status: run.status === 'completed' ? 'done' : run.status === 'failed' ? 'failed' : 'running',
317
+ // 同形第二处(G-20):这条三元式与 `coerceRunStatus` 读的是同一位 run 状态,兜底也同形 ——
318
+ // 只修一处等于把病搬个家。`parked` 走 `coerceRunStatus` 的同一条臂,不在这里另写一遍判据。
319
+ status: synthPhaseStatus(run.status, agents),
293
320
  doneCount: agents.filter(a => a.state === 'done').length,
294
321
  totalCount: agents.length,
295
322
  agents: agents.map(stripPhase),
296
323
  },
297
324
  ];
298
325
  }
326
+ /**
327
+ * 一桶腿 → 这一格的状态词。
328
+ *
329
+ * 🔴 `parked` 的判据是「**这一格里没有一条腿能自己往前走**」:全员不是完成就是停在门上,且至少有
330
+ * 一条真停着。混着还在跑的腿时**仍报 running** —— 那一格确实还在跑,渲成 parked 是另一个方向的谎。
331
+ * 🔴 `failed` 仍先判:一格里既有失败又有停着的腿时,先要人看的是那条失败的。
332
+ */
333
+ function bucketStatus(bucket) {
334
+ if (bucket.some(a => a.state === 'error'))
335
+ return 'failed';
336
+ if (bucket.length > 0 && bucket.every(a => a.state === 'done'))
337
+ return 'done';
338
+ if (bucket.some(a => a.state === 'parked') && bucket.every(a => a.state === 'parked' || a.state === 'done')) {
339
+ return 'parked';
340
+ }
341
+ return 'running';
342
+ }
343
+ /** 合成 `Workflow` 单格(路径三:wire 既无结构化 phases 也无 agent 上的 phase 标)的状态词。
344
+ * 跟 run 走(同 {@link coerceRunStatus} 的臂),run 说不出 `parked` 时再从腿上推一次 —— 今天真正
345
+ * 会带 `parked` 的正是腿那一级,而路径三的这一格代表的就是整只 run 的全部腿。 */
346
+ function synthPhaseStatus(runStatus, agents) {
347
+ if (runStatus === 'completed')
348
+ return 'done';
349
+ if (runStatus === 'parked')
350
+ return 'parked';
351
+ const derived = bucketStatus(agents);
352
+ if (derived === 'parked')
353
+ return 'parked';
354
+ return runStatus === 'failed' ? 'failed' : 'running';
355
+ }
299
356
  /** THE projector: SDK `WorkflowRun` (client.workflows.get) → the monitor's `WorkflowRunState`. Pure/total —
300
357
  * tolerates absent/permissive fields and never throws (the graceful-degrade contract lives above, in the
301
358
  * source loop; this just maps a well-formed run). Exported for the mock-parity unit cross-check. */
@@ -14,8 +14,13 @@
14
14
  * 留在 TUI 的:`computeVisibleWindow`(滚动窗口数学 = 视口概念,端专属)、glyph/color 映射、
15
15
  * 以及整棵 JSX。壳侧 shim = 那个 tsx 改成从本包 re-export 这两样,自己不再定义。
16
16
  */
17
- export type AgentState = 'start' | 'progress' | 'queued' | 'done' | 'error' | 'inactive';
18
- export type DisplayStatus = 'queued' | 'running' | 'done' | 'failed' | 'skipped' | 'interrupted';
17
+ export type AgentState = 'start' | 'progress' | 'queued' | 'done' | 'error' | 'inactive'
18
+ /** sema 超集:这条腿耐久挂在一张审批卡上(engine core 7.10.0 #642)。见上方头注。 */
19
+ | 'parked';
20
+ export type DisplayStatus = 'queued' | 'running' | 'done' | 'failed' | 'skipped' | 'interrupted'
21
+ /** sema 超集:同上。🔴 端的 glyph/颜色映射是**穷尽形**,加这一词就必须同批加一条 case
22
+ * (措辞按「waiting for approval」一类,别复用 running 的字与色)。 */
23
+ | 'parked';
19
24
  export interface WorkflowToolCall {
20
25
  name: string;
21
26
  summary?: string;
@@ -61,7 +66,10 @@ export interface WorkflowAgent {
61
66
  }
62
67
  export interface WorkflowPhase {
63
68
  title: string;
64
- status: 'not-started' | 'running' | 'done' | 'failed';
69
+ /** 🔴 `parked` = 这一格里没有一条腿能自己往前走(全员不是完成就是停在审批门上)。见 `AgentState`
70
+ * 的同名头注;上游今天不给 phase 铸这个词(core:「a phase or group never parks」),本包这一位有两个
71
+ * 来路:①上游哪天真铸了 ⇒ 原词透出而不是折成 running;②本包从桶里的腿**推**出来。 */
72
+ status: 'not-started' | 'running' | 'done' | 'failed' | 'parked';
65
73
  doneCount: number;
66
74
  totalCount: number;
67
75
  agents: WorkflowAgent[];
@@ -73,7 +81,11 @@ export interface WorkflowRunState {
73
81
  summary?: string;
74
82
  script: string;
75
83
  scriptPath?: string;
76
- status: 'running' | 'done' | 'failed' | 'stopped' | 'paused';
84
+ /** 🔴 `parked`(0.63.1,additive):**前向预挂** —— engine core 7.10.0 run 级词表仍闭在
85
+ * running|completed|failed(一条腿 park ⇒ 整只 run 按 `WorkflowAgentParkedError` 收在 `failed`),
86
+ * 所以今天没有任何受支持的引擎在这一位上发 `parked`。留这条臂是为了「词真到了就原样透出」,
87
+ * 绝不是说它现在会到 —— 判「这条 run 停着等人批」今天要看**腿**那一级(见 `AgentState.parked`)。 */
88
+ status: 'running' | 'done' | 'failed' | 'stopped' | 'paused' | 'parked';
77
89
  cloud?: boolean;
78
90
  agentCount: number;
79
91
  totalTokens: number;
@@ -97,5 +109,9 @@ export interface WorkflowRunState {
97
109
  * fields on the active path. The AgentState type carries exactly this input set, so the type contract now
98
110
  * matches the atlas vocabulary 1:1 — the branches below consume only atlas input states and the output
99
111
  * DisplayStatus set + glyph/color mapping ($jn) is identical.
112
+ *
113
+ * 🔴 **`parked` 是这条规则上唯一的 sema 超集臂**(0.63.1):atlas 没有它,因为 CC 的编排里没有耐久
114
+ * 审批这一层。它**不派生**——上游直接铸这个词(engine core 7.10.0 #642 的 agent 行 `status` 与
115
+ * `deriveAgentDisplayStatus` 产物),这里只是不许把它折掉。臂的**位置**是承重的,理由写在分支处。
100
116
  */
101
117
  export declare function agentDisplayStatus(a: WorkflowAgent, workflowActive: boolean): DisplayStatus;
@@ -29,6 +29,10 @@
29
29
  * fields on the active path. The AgentState type carries exactly this input set, so the type contract now
30
30
  * matches the atlas vocabulary 1:1 — the branches below consume only atlas input states and the output
31
31
  * DisplayStatus set + glyph/color mapping ($jn) is identical.
32
+ *
33
+ * 🔴 **`parked` 是这条规则上唯一的 sema 超集臂**(0.63.1):atlas 没有它,因为 CC 的编排里没有耐久
34
+ * 审批这一层。它**不派生**——上游直接铸这个词(engine core 7.10.0 #642 的 agent 行 `status` 与
35
+ * `deriveAgentDisplayStatus` 产物),这里只是不许把它折掉。臂的**位置**是承重的,理由写在分支处。
32
36
  */
33
37
  export function agentDisplayStatus(a, workflowActive) {
34
38
  if (a.state === 'done')
@@ -36,6 +40,12 @@ export function agentDisplayStatus(a, workflowActive) {
36
40
  // atlas: `state==="error"` → skipped?"skipped":"failed".
37
41
  if (a.state === 'error')
38
42
  return a.skipped ? 'skipped' : 'failed';
43
+ // 🔴 sema 超集臂,**位置承重**:必须在 `!workflowActive` 之前判。一条腿 park 之后 run 自己收在
44
+ // `failed`(core `WorkflowAgentParkedError`)⇒ `workflowActive` 恒假 ⇒ 放到后面就永远被
45
+ // `interrupted` 吃掉,而 interrupted 说的是「run 结束时它还没跑完」——那是一件**已经过去**的事,
46
+ // parked 说的是「它还挂在那儿,等一个人做决定」,是一件**还能动手**的事。两句下一步不同。
47
+ if (a.state === 'parked')
48
+ return 'parked';
39
49
  // atlas `!t` (workflow not active) → interrupted (also the `inactive`-state landing).
40
50
  if (!workflowActive)
41
51
  return 'interrupted';
@@ -4175,6 +4175,13 @@ readonly detachArmed?: boolean | undefined
4175
4175
  「本轮全部正文」去比,一个「散文 → 工具 → 散文」的**正常多步回答**会被判成分岔,只差首尾空白的回答
4176
4176
  同理。⇒ 左边必须是**当前这条 assistant 消息**已上屏的正文(工具边界重置),相等判定先过一次 `trim()`。
4177
4177
 
4178
+ **0.63.2 订正(卡后零散文)**:provider 的一条 assistant 消息 = `[text, tool_use]` 两块。工具卡是这一轮
4179
+ **最后**一件事、卡后一个字都没有时,引擎「最后一条消息的文本」正是卡**之前**那段;而工具边界已把
4180
+ 「当前消息已上屏正文」重置成空串,空串是任何终帧的前缀 ⇒ 修前补差臂把整段终帧当「缺的后缀」再渲一遍
4181
+ (同一段话上屏两遍;cli 1.0.105 收官 mock 车道「散文 → Config 卡 → 收口」实撞)。⇒ 左边现在是
4182
+ 「当前消息已上屏正文,**当前消息一个字都没上屏时回退到上一条真有正文的消息**」(`lastCommittedAnswerText`);
4183
+ 当前消息有字时与修前逐字同。宿主零换装。
4184
+
4178
4185
  **新 chrome 臂**:
4179
4186
 
4180
4187
  ```ts
@@ -4359,6 +4366,12 @@ POSTURE_SOURCE_WORDS: readonly string[] // ['env','center','posture','engine-d
4359
4366
  版本**)。🔴 后两句**一个值都不报** —— 顺口说了 `off`,消费端就会把「读不出」当成「关着」。
4360
4367
  数值一律按毫秒渲人话(`24h` / `5m`;除不尽就报毫秒,`1.5h` 比 `90m` 容易读错)。
4361
4368
 
4369
+ 🔴 **判序:`reachable:false` 先判,压过任何在手的读数**(0.63.1 订正 —— 0.63.0 是先看读数在不在)。
4370
+ 这是一条**调用惯例**上的事实:端手里就只有上一拍那一只 view,探测这一拍打不通时它能翻的只有
4371
+ `reachable`,别的没得换。旧序下「旧 knob + `reachable:false`」照渲**上一拍的值和来源词**,屏上一个字
4372
+ 都看不出这一次根本没读到 —— 那是拿陈读数冒充现势。⇒ 端**不必**为了这个自己把 view 清空,
4373
+ 传旧 view + `reachable:false` 就是正解,四句里出的一定是「未观测」那一句。
4374
+
4362
4375
  ---
4363
4376
 
4364
4377
  ### 27c. ② 三态规则身份:`behavior` 必填且不给默认(BREAKING 迁移②)
@@ -4437,6 +4450,33 @@ ENGINE_IDENTITY_ANCHORS: readonly ['startedAt', 'instanceId', 'pid']
4437
4450
  一只锚可比且相同,判词照给 `same`。
4438
4451
  - **归层**:「换代之后要做什么」(丢连接、重握手、提示用户)是**宿主的状态机**,不在本包。
4439
4452
 
4453
+ 🔴 **`pid` 是最弱锚,而「只剩 pid 可比」不是任何受支持引擎的读数**(0.63.1 补记,收窄 G-8 的判据口径)
4454
+
4455
+ 有人会问:两次 `/health` 的 `pid` 恰好相同(端口复用、容器里 pid 恒 `1`)时判 `same`,会不会把一次
4456
+ 真换代读成「同一条命」?**会 —— 但那要求 `instanceId` 在两侧都缺席**,而那种读数在生产上到不了:
4457
+
4458
+ - `instanceId` 在引擎里是**无条件铸**的(不是可选特性,不由任何开关关掉),`/health` 体一路带着它;
4459
+ 常驻门 `run-engine-identity-test.mjs` 的 **E 段**用引擎产物**锚 AST**盯着这条外部事实,**三节全验**:
4460
+ ①**铸** —— `const instanceId = uuidv7()` 不在任何 `if`/`switch`/`try`/三元/`&&`/`??`/循环里;
4461
+ ②**接线** —— 铸出的那个值真被挂到一只对象上(否则 `deps.instanceId` 就是 `undefined`);
4462
+ ③**发射** —— `instanceId` 真在 `/health` **那一只**响应体的键空间上(按 `status:"ok"` +
4463
+ `pid: process.pid` 两键定位;挂在别的对象上、长在嵌套值里、经包装调用摊进来,一律判**判不出**)。
4464
+ 条件 spread 只认「守卫就是那个值本身」(`...(deps.instanceId ? { instanceId: deps.instanceId } : {})`)
4465
+ —— 那种形说的是「只在它自己缺席时才省」,配上 ①② 就是恒在场;换成别的旗子只是「可能带上」。
4466
+ 🔴 **射程边界**:这三节证的是**源码形**,挡的是「上游把这条锚改成可选 / 改了铸法 / 断了接线」这一类
4467
+ 漂移;它**证不了**「这段代码真的被执行」——那要一次真启动的 `/health` 响应,不在本包射程内。
4468
+ 🔴 **这条见证是 G-8 裁读的承重腿**:`SEMA_FENCE_ENGINE_DIR` 未注入时那一段打 `SKIPPED-SECTION`
4469
+ (汇总里显形成 PARTIAL),**发布批必须注入一次引擎树把它跑绿**才能引用本段的结论。
4470
+ - 只要 `instanceId` 在两侧都在场,它就压过 `pid` —— `pid` 相同而 `instanceId` 不同**判 `changed`**,
4471
+ 优先序里 `pid` 排表尾(D 段两条钉)。
4472
+
4473
+ ⇒ 本包**刻意不**把「只剩 `pid` 可比且相等」判成 `unknown`。那样做的代价是**真实且每拍发生的**:
4474
+ 一台报得出 `pid`、报不出另两只锚的老 worker,每一次心跳都会被判「判不出」⇒ 宿主按最坏情况重握手。
4475
+ 拿一个每拍都发生的代价,去换一个上面两条证明到不了的风险,不划算。
4476
+
4477
+ 🔴 端要记住的一句:`pid` **从不落单** —— 它旁边总有 `instanceId`。哪天真读到一份**只有 `pid`** 的
4478
+ `/health`,那不是「老引擎」,那是一个本文档没有覆盖的形,按最坏情况处理(重握手)而不是按 `same`。
4479
+
4440
4480
  ---
4441
4481
 
4442
4482
  ### 27e. ④ 门词汇两张表 +2 与唯一措辞铸点
@@ -4449,9 +4489,9 @@ core 7.9.0 各加一词:`DeniedBy` **+`persisted_rule`**(这个人自己名下
4449
4489
  **签名**
4450
4490
 
4451
4491
  ```ts
4452
- GATE_DENIED_BY_WORDS: readonly DeniedBy[] // 九词,顺序同源
4492
+ GATE_DENIED_BY_WORDS: readonly DeniedBy[] // 9 词,顺序同源
4453
4493
  gateDeniedByDetail(deniedBy: unknown): string
4454
- ASK_ORIGIN_WORDS: readonly string[] // 十一词,顺序同源
4494
+ ASK_ORIGIN_WORDS: readonly string[] // 11 词,顺序同源
4455
4495
  askOriginDetail(origin: unknown): string
4456
4496
  ```
4457
4497
 
@@ -4482,7 +4522,7 @@ askOriginDetail(origin: unknown): string
4482
4522
  **签名**
4483
4523
 
4484
4524
  ```ts
4485
- PERMISSION_RULE_ISSUE_CODES: readonly string[] // 十五码
4525
+ PERMISSION_RULE_ISSUE_CODES: readonly string[] // 15 码
4486
4526
  RETIRED_PERMISSION_RULE_ISSUE_CODES: readonly string[] // 记账座,查码别查它
4487
4527
  permissionRuleIssueDetail(code: unknown, message: unknown): string
4488
4528
  ```
@@ -4493,9 +4533,12 @@ permissionRuleIssueDetail(code: unknown, message: unknown): string
4493
4533
  `unsupported.*` = 规则本身合法但**这条车道不接**(换写法、或落到规则店而不是策略折叠)。上游把它们
4494
4534
  分成两个前缀,正是因为「以为挡住了」比没挡更危险。
4495
4535
 
4496
- **缺席语义**:表外码 ⇒ 那一句说「这个码比这一端新」,原样带上那个码,**并照样带上 `message`**
4497
- (码读不懂不代表引擎那句话读不懂,而那句话往往才是能动手改的那条信息)。`message` 是**散文不是码**:
4498
- 只渲染、呈前**先转义后封长**,绝不做等值匹配、绝不当分类位。坏码 / `message` 一律**不抛**
4536
+ **缺席语义**:表外码 ⇒ 那一句说「这个码比这一端新」,**原样**带上那个码,**并照样带上 `message`**
4537
+ (码读不懂不代表引擎那句话读不懂,而那句话往往才是能动手改的那条信息)。🔴 **「原样」是逐字的**
4538
+ (0.63.1 订正:0.63.0 实际把码封在 60 字符内,与本句冲突)—— 码是**引擎产的标识符**,运维要拿它
4539
+ 整只去问上游,截一半的码谁也查不出来、还长得像一个完整答案。呈前只做**不可见字符消毒**,**不封长**
4540
+ (不封长 ≠ 不消毒)。`message` 是**散文不是码**:只渲染、呈前**先转义后封长**(封长只给这半场),
4541
+ 绝不做等值匹配、绝不当分类位。坏码 / 坏 `message` 一律**不抛**
4499
4542
  (这条路径跑在「用户规则写错了」那一刻,读器再抛一次只会把一个说得清的错误换成一个说不清的崩)。
4500
4543
 
4501
4544
  ---
@@ -4554,11 +4597,11 @@ an argument」。audience 答另一问:这条事实**说给谁听**。
4554
4597
  **签名**
4555
4598
 
4556
4599
  ```ts
4557
- ENGINE_NOTICE_CODES: readonly string[] // 四十八码,顺序同源
4600
+ ENGINE_NOTICE_CODES: readonly string[] // 50 码,顺序同源
4558
4601
  ENGINE_NOTICE_AUDIENCE: Readonly<Record<string, 'user' | 'operator'>>
4559
4602
  noticeAudienceOf(code: unknown): 'user' | 'operator'
4560
4603
  engineNoticeInCatalog(code: unknown): boolean
4561
- MCP_INJECTION_DROP_REASONS: readonly string[] // 四个原因词
4604
+ MCP_INJECTION_DROP_REASONS: readonly string[] // 4 个原因词
4562
4605
  readMcpInjectionDrop(notice: unknown): McpInjectionDropFactsView | undefined
4563
4606
  ```
4564
4607
 
@@ -4624,13 +4667,49 @@ core 双向对账)。**候上游导出即整只退役改成从 sdk 取**。
4624
4667
 
4625
4668
  ---
4626
4669
 
4627
- ### 27h3. `WorkflowRunStatus` `parked`:**零改**,原词透出
4670
+ ### 27h3. `parked`:一条腿停在审批门上(0.63.1 订正 —— 0.63.0 这一节写错了两处)
4671
+
4672
+ **0.63.0 原文的两处失真,先说清楚**(留痕不删,免得下一个人再按原文接线):
4628
4673
 
4629
- core 7.10.0 给 workflow run 的状态开放联合加了 `parked`。本包**一个字节都不用改**:
4630
- `src/workflow.ts` 的两处读法本来就把 `status` 当**开集字符串**原样透出(`typeof === 'string' &&
4631
- length > 0` 即收),从不窄读成枚举 —— 所以 `parked` 自动逐字到达端上,端按自己的行渲染即可。
4632
- 📋 **如实记一笔已知形**(不是本批引入,也不在本批射程):`readWorkflowRunLine` `status` 读不出时
4633
- 回落 `'completed'` —— 那是把「说不出」渲成「完成了」。它先于本批存在,登记在此供后续独立处置。
4674
+ 1. 写的是「core 7.10.0 给 **workflow run 的状态**开放联合加了 `parked`」——**不对**。上游加的是
4675
+ **agent 行**那一级:`WorkflowItemStatus` +`parked`(#642),且 `deriveAgentDisplayStatus` 直接产
4676
+ `"parked"`(`AgentDisplayStatus` 六词)。**run 级**词表今天仍闭在 running|completed|failed ——
4677
+ 一条腿 park 之后整只 run 按 `WorkflowAgentParkedError` 收在 `failed`。
4678
+ 2. 写的是「本包**零改**,`src/workflow.ts` 两处读法原样透出」——那两处(`parseWorkflowPollEnvelope`
4679
+ / `projectWorkflowTaskOutput`,**不是**原文写的 `readWorkflowRunLine`,那个名字在本包里零命中)
4680
+ 确实原样透出,但它们是 **TaskOutput 文本车道**;**监视器投影车道**(`src/workflowClient.ts`)是
4681
+ 另一条,而它当时把 `parked` 折进了「在跑」。
4682
+
4683
+ **0.63.1 改了什么**:监视器车道的四处窄读各给 `parked` 一条**自己的臂**,开集兜底一个字节不动。
4684
+
4685
+ | 车道 / 位 | 0.63.0 | 0.63.1 | 今天真会到吗 |
4686
+ |---|---|---|---|
4687
+ | agent 行 `status` / `displayStatus`(`coerceAgentState`) | 折 `progress` | `parked` | 🔴 **会** —— 上游今天就铸 |
4688
+ | `agentDisplayStatus()`(WPe) | 落 `interrupted` | `parked` | 🔴 **会**(park 之后 run 恒非活跃,所以这条臂**必须排在** `!workflowActive` 之前) |
4689
+ | run 级 `status`(`coerceRunStatus`) | 折 `running` | `parked` | ❌ 前向预挂(run 词表未开) |
4690
+ | phase 级 `status`(`coercePhaseStatus` + 合成格三元式) | 折 `running` | `parked` | ❌ 前向预挂;另由本包**从桶里的腿推**出来 |
4691
+
4692
+ **型面**(三处 additive,端的穷尽形要各加一条 case):`AgentState` +`parked` · `DisplayStatus`
4693
+ +`parked` · `WorkflowRunState['status']` / `WorkflowPhase['status']` +`parked`。
4694
+
4695
+ **桶推导的判据**(本包自己加的那一格,`projectPhases`):一格报 `parked` ⇔ **这一格里没有一条腿能
4696
+ 自己往前走** —— 全员不是完成就是停在门上,且至少一条真停着。混着还在跑的腿时**仍报 `running`**
4697
+ (那一格确实还在跑,渲成 parked 是另一个方向的谎);`failed` 仍先判。
4698
+
4699
+ - 🔴 **为什么不能折**:`parked` 的下一步是**去批它**,`running` 的下一步是**等着**。这条腿在有人
4700
+ 做决定之前**永远不会**自己往前走一步 —— 折过去等于把一件要人动手的事渲成一件不用管的事。
4701
+ - 🔴 **开集兜底没动**:未知未来值照旧落活跃臂(run/phase → `running`,agent → `progress`)。
4702
+ `parked` 是**加了一条案臂**,不是把兜底改宽。
4703
+ **同批把「空串算不算读数」在这一族里拉齐**(G-20 附带②):同一条 wire 上的同一个 `status` 字段,
4704
+ 三处在场判此前不同形 —— `parseWorkflowPollEnvelope` 判 `length > 0`(空串=缺席),
4705
+ `projectWorkflowTaskOutput` 的 per-agent 与 `projectAgent` 的 `displayStatus` 只判 `typeof`
4706
+ (空串=在场)。后两处的后果各不相同但都是谎:前者把空串渲成 `[]`(一对什么也没说的方括号,
4707
+ 却长得像一个真答案),后者更坏 —— `'' ?? row.status` 恒取空串(`??` 只挡 null/undefined),
4708
+ 那条腿的 raw `status` 被**静默丢掉**,连回落都走不到。三处现在同一判:**空串 = 缺席**;
4709
+ 纯空白**不** trim(两侧同尺,仍算在场)。端侧无需改动,只是不会再收到 `[]` 与被吞掉的状态词。
4710
+
4711
+ - 📋 **如实记一笔已知形**(不是本批引入,也不在本批射程):`parseWorkflowPollEnvelope` 在 `status`
4712
+ 读不出时回落 `'completed'` —— 那是把「说不出」渲成「完成了」。它先于本批存在,登记在此供后续独立处置。
4634
4713
 
4635
4714
  ---
4636
4715
 
@@ -4638,15 +4717,16 @@ length > 0` 即收),从不窄读成枚举 —— 所以 `parked` 自动逐字到
4638
4717
 
4639
4718
  | 件 | 壳(cli 1.0.106) | web 端 |
4640
4719
  |---|---|---|
4641
- | ① | `doctor` 的 Permissions 段加**三行**:`postureKnobDetail(view.durableApproval, {reachable, knob:'durableApproval'})` / 同形的 `streamAskWindowMs` / `sessionAutoTitle`;`reachable` 由壳自己那次 operator 探测的结果给。🔴 分类读一律走 `postureKnobSourceOf()`,**别直读** `.source` | 诊断面同左;无 operator 权限的部署上这一面整段不渲(端点本身 operator-gated) |
4720
+ | ① | `doctor` 的 Permissions 段加**三行**:`postureKnobDetail(view.durableApproval, {reachable, knob:'durableApproval'})` / 同形的 `streamAskWindowMs` / `sessionAutoTitle`;`reachable` 由壳自己那次 operator 探测的结果给 —— 🆕 0.63.1 起**不必**为打不通而先把 view 清空,旧 view + `reachable:false` 直接渲「未观测」(`!reachable` 先判)。🔴 分类读一律走 `postureKnobSourceOf()`,**别直读** `.source` | 诊断面同左;无 operator 权限的部署上这一面整段不渲(端点本身 operator-gated) |
4642
4721
  | ② | `/permissions` 治理表加「态」一列 = `persistedRuleBehaviorLabel(row)`;撤销按钮改经 `revokeTargetFromPersistedRule(row)` —— **铸不出体就把那一行的撤销入口藏掉**(别退化成两元组撤销) | 同左 |
4643
4722
  | ③ | 三条 uplift 锚 `*-startedat-*`:①引擎 spawn 后的首探记下 `engineIdentityOf(health)`;②重连 / 定期心跳时 `engineIdentityVerdict(prev, now)`,`changed` ⇒ 走既有的 `engineReplacedGate`;③`unknown` **不许**走「没换」那条臂(它要么再探一次、要么按最坏情况重握手) | 同左;web 端没有 spawn 半场,只有 ②③ |
4644
4723
  | ④ | 门记录那一行:`gateDeniedByDetail(gateDeniedBy(g))`(九词);审批卡的出身行:`askOriginDetail(frame.origin)`(十一词)。壳侧同族自拼句子一律退役 | 同左 |
4645
- | ⑤ | `/permissions` 的规则报错行:`permissionRuleIssueDetail(issue.code, issue.message)` | 同左 |
4724
+ | ⑤ | `/permissions` 的规则报错行:`permissionRuleIssueDetail(issue.code, issue.message)`。🆕 0.63.1:表外码**整只**进那一句(不再封 60 字符),端的行渲染要给它留得下换行/截断的余地 —— 🔴 别在端上再截一刀,那等于把这次修的病搬到端里 | 同左 |
4646
4725
  | ⑥ | `wiring_manifest` 帧到达时 `projectToolRoster(manifest)`;**在场**则自报面与工具卡 shim 走 `toolRosterNames()` / `toolShimFromRoster()`,**缺席**才回落 `liveInitToolFace` 三表。`tool_roster_delta` 帧走 `applyToolRosterDelta()`,`skew` 时**记一次偏斜并按快照重同步**(别拿 summary 算位置) | 同左 |
4647
4726
  | ⑦ | `engine_notice` 呈现闸改按 `engineNoticeInCatalog(code)`;投递面按 `noticeAudienceOf(code)` 分流(`user` 上屏 / `operator` 落运维面)。壳自己的 `CORE_ENGINE_NOTICE_CODES` / `PENDING_ENGINE_NOTICE_CODES` 两表**可以退役成一张「明拒表」**(本包这张是上游码册的机械镜像,壳只需保留它**刻意不渲**的那几个码)。🔴 `mcp.injection_dropped` 的转录**直接用 `message`**,别自拼 | 同左 |
4648
4727
 
4649
4728
  | ⑧ | 审批卡上「为什么问我」那一行:`classifierUnavailableDetail(classifierUnavailableOf(ask)?.cause)`;durable park 行的卡走**同一只**读器(键路同形)。🔴 这一格是**显示元数据**,别据它翻转任何裁决;缺席**不许**渲成「分类器跑成了」 | 同左 |
4729
+ | ⑨ | `workflowMonitor` 的 agent 行:`DisplayStatus` 的 **`parked`** 是一条新臂,glyph/颜色映射与状态词是**穷尽形** —— 必须同批加一条 case,措辞按「waiting for approval」一类,🔴 **别复用 `running`**的字与色(那正是 0.63.0 的病)。`AgentState` 侧同理。合成格/phase 行拿到 `parked` 时同样别渲成在跑 | 同左 |
4650
4730
 
4651
4731
  ---
4652
4732
 
@@ -4694,8 +4774,10 @@ length > 0` 即收),从不窄读成枚举 —— 所以 `parked` 自动逐字到
4694
4774
  - **G-19**:让分类器**答了但答在契约之外**(parse_error)—— 卡上**不出现**「分类器跑不了」那一族的
4695
4775
  任何一句(引擎在这一形上根本不盖那一格);而**不武装** auto 模式跑同样的调用 —— 卡上同样没有那一行
4696
4776
  (缺席 ≠「分类器跑成了」)。
4697
- - **G-20**:让一条 workflow run 进入 `parked` —— 端上那一行显示的状态词**逐字是 `parked`**
4698
- (不是被折成 `running` / `completed` / 或一个 `?`)。
4777
+ - **G-20**(0.63.1 订正判据口径:park 落在**腿**上不落在 run 上)——让一条 workflow **agent 腿**停在
4778
+ 审批门上:监视器里那一行的状态词**逐字是 `parked`**(不是 `running`,也不是 `interrupted` —— 那条腿
4779
+ park 之后整只 run 就收在 `failed`,所以「run 不活跃 ⇒ interrupted」那条老臂会抢答);批掉那张卡之后
4780
+ 那一行**不再**是 `parked`。反向:随便造一个这一端没见过的状态词,那一行仍渲成在跑(开集兜底没被改宽)。
4699
4781
 
4700
4782
  ---
4701
4783
 
@@ -4704,7 +4786,7 @@ length > 0` 即收),从不窄读成枚举 —— 所以 `parked` 自动逐字到
4704
4786
  | 门 | 守什么 |
4705
4787
  |---|---|
4706
4788
  | 🆕 `scripts/run-posture-knob-projection-test.mjs` | ① 三旋钮窄读 / 裸值折壳但来源不冒称 / 主锚坏整段缺席·旁枝坏只丢一根 / 四句逐字互异 + 毫秒人话 / 转义后封长 / sdk spec + d.ts 真字节(含「本面数值旋钮恰一根」的措辞前提钉) |
4707
- | 🆕 `scripts/run-engine-identity-test.mjs` | ③ 三锚逐格窄读 / 三态判词 + 优先序 / **锚交集为空 ⇒ unknown 而非 same** / 布尔口只对 changed 说 true / sdk `health.d.ts` 真字节含缺席语义注 |
4789
+ | 🆕 `scripts/run-engine-identity-test.mjs` | ③ 三锚逐格窄读 / 三态判词 + 优先序 / **锚交集为空 ⇒ unknown 而非 same** / 布尔口只对 changed 说 true / sdk `health.d.ts` 真字节含缺席语义注;🆕 **D 段**(0.63.1)G-8 的设计钉 —— pid 单锚相等仍判 same、pid 相等压不住更强的锚、优先序里 pid 是表尾;🆕 **E 段** 引擎产物第二见证(锚 AST 不锚子串,**铸/接线/发射三节全验**)—— 铸点不在任何条件构造或循环里 · 铸出的值真被挂到一只对象上 · `instanceId` 真在 `/health` 那只体的键空间上(`status:"ok"` + `pid: process.pid` 两键定位;别的对象/嵌套值/包装调用/别的旗子做守卫,一律判不出)。🔴 `SEMA_FENCE_ENGINE_DIR` **未注入 ⇒ 打 `SKIPPED-SECTION`**(汇总里显形成 PARTIAL,不冒充全绿);**注入了却拿不到物料 ⇒ 判红**(显式配置却无见证 = 配置坏了)。射程边界:证的是源码形,不证「这段码真被执行」 |
4708
4790
  | 🆕 `scripts/run-gate-vocabulary-test.mjs` | ④ 两张表对 sdk 联合成员**双向等值**(缺一词红、多一词红、乱序红)/ 措辞逐词覆盖 + 句句互异 / **两张表的开闭兜底逐字分家** |
4709
4791
  | 🆕 `scripts/run-permission-rule-issue-codes-test.mjs` | ⑤ 表 ⟷ core 真铸码双向等值 / 表 ∪ 退役表 ⟷ core 声明联合(显形改名遗留)/ `invalid`·`unsupported` 两族措辞逐字互斥 / 散文消毒 + 转义后封长 / 可选 `SEMA_FENCE_ENGINE_DIR` 第二见证 |
4710
4792
  | 🆕 `scripts/run-tool-roster-projection-test.mjs` | ⑥ **绝不半张名册** / digest 恰 16 位小写 hex / `pathTarget` 三新键可选且缺席不铸默认 / shim 只从名册取(轴缺席不填 false·never)/ **skew 不是拒绝** / 坏帧什么都不发生 / 纯函数不改入参 |
@@ -4713,6 +4795,6 @@ length > 0` 即收),从不窄读成枚举 —— 所以 `parked` 自动逐字到
4713
4795
  | `scripts/run-sdk-floor-test.mjs` | `FLOOR` 8.5.0 → **8.8.0**,抬版理由逐条记账(编译期对账钉 / 撤销体铸点 / `startedAt` / `pathTarget` 三键 / 两张闭集加员) |
4714
4796
  | `scripts/run-client-core-portability-test.mjs` | index 闭包上限 148 → **154**(六件新读器叶,逐件登记;`EXPECTED_PACKAGES_INDEX` 一字未动) |
4715
4797
  | `scripts/run-public-surface-test.mjs` | 公面导出基线 **869 → 901** |
4716
- | `scripts/run-integration-doc-freshness-test.mjs` | 本节新增坐标在盘 + 入库;§0a/§2/§2b 计数与基线等值 |
4798
+ | `scripts/run-integration-doc-freshness-test.mjs` | 本节新增坐标在盘 + 入库;§0a/§2/§2b 计数与基线等值;🆕 **①c**(0.63.1):签名块里的**表长数字** ⟷ 源码那张表的真实成员数(TS AST 取 `Object.freeze([...])` 的元素个数)。档里一律写阿拉伯数字 —— 中文数词读不出来的门等于没有门,抽取器读不出即判工具红 |
4717
4799
 
4718
4800
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.63.0",
3
+ "version": "0.63.2",
4
4
  "description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
5
5
  "license": "MIT",
6
6
  "type": "module",