@sema-agent/client-core 0.12.2 → 0.14.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.
Files changed (131) hide show
  1. package/README.md +15 -2
  2. package/dist/abortableSleep.d.ts +29 -0
  3. package/dist/abortableSleep.js +43 -0
  4. package/dist/adapt/arms.d.ts +67 -0
  5. package/dist/adapt/arms.js +623 -0
  6. package/dist/adapt/ids.d.ts +22 -0
  7. package/dist/adapt/ids.js +34 -0
  8. package/dist/adapt/instanceLedger.d.ts +36 -0
  9. package/dist/adapt/instanceLedger.js +50 -0
  10. package/dist/adapt/panelTasks.d.ts +59 -0
  11. package/dist/adapt/panelTasks.js +193 -0
  12. package/dist/adapt/textStream.d.ts +63 -0
  13. package/dist/adapt/textStream.js +141 -0
  14. package/dist/adapt/toolCards.d.ts +65 -0
  15. package/dist/adapt/toolCards.js +100 -0
  16. package/dist/adapt/turnFlags.d.ts +37 -0
  17. package/dist/adapt/turnFlags.js +55 -0
  18. package/dist/adapt/wireShapes.d.ts +96 -0
  19. package/dist/adapt/wireShapes.js +167 -0
  20. package/dist/adapt.d.ts +33 -60
  21. package/dist/adapt.js +84 -1207
  22. package/dist/adapter/downstream/eventToSdkMessage.d.ts +57 -13
  23. package/dist/adapter/downstream/eventToSdkMessage.js +174 -106
  24. package/dist/adapter/downstream/terminalToSdkResult.js +151 -160
  25. package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +36 -0
  26. package/dist/adapter/downstream/turnUsageToModelUsage.js +34 -6
  27. package/dist/adapter/runStream.d.ts +36 -4
  28. package/dist/adapter/runStream.js +195 -13
  29. package/dist/adapter/types.d.ts +28 -2
  30. package/dist/agentSession/backgroundView.js +4 -15
  31. package/dist/agentsWireCaps.d.ts +10 -6
  32. package/dist/agentsWireCaps.js +21 -7
  33. package/dist/argvFlagValue.d.ts +41 -0
  34. package/dist/argvFlagValue.js +69 -0
  35. package/dist/attachmentsWireCaps.d.ts +3 -2
  36. package/dist/attachmentsWireCaps.js +5 -3
  37. package/dist/classifierVerdictWire.d.ts +8 -0
  38. package/dist/classifierVerdictWire.js +8 -0
  39. package/dist/cloudConfigWireCaps.d.ts +28 -1
  40. package/dist/cloudConfigWireCaps.js +51 -11
  41. package/dist/controlRouter.d.ts +14 -8
  42. package/dist/controlRouter.js +15 -21
  43. package/dist/detachWire.d.ts +15 -5
  44. package/dist/detachWire.js +17 -7
  45. package/dist/effortWire.d.ts +0 -21
  46. package/dist/effortWire.js +6 -20
  47. package/dist/engineInlineTaskStats.d.ts +12 -6
  48. package/dist/engineWireSdk.d.ts +12 -0
  49. package/dist/env/localeGeo.js +2 -1
  50. package/dist/envFlag.d.ts +39 -0
  51. package/dist/envFlag.js +51 -0
  52. package/dist/finalVerifyWire.d.ts +7 -5
  53. package/dist/finalVerifyWire.js +6 -5
  54. package/dist/fleet/fleetLedger.d.ts +32 -9
  55. package/dist/fleet/fleetLedger.js +119 -34
  56. package/dist/fleet/fleetProjection.d.ts +44 -6
  57. package/dist/fleet/fleetProjection.js +52 -10
  58. package/dist/fleetAgentPanelProjection.d.ts +18 -1
  59. package/dist/fleetAgentPanelProjection.js +61 -14
  60. package/dist/forkWireCaps.d.ts +2 -1
  61. package/dist/forkWireCaps.js +5 -11
  62. package/dist/goalStopHook.d.ts +142 -0
  63. package/dist/goalStopHook.js +258 -0
  64. package/dist/headlessPermissionModeWire.d.ts +10 -0
  65. package/dist/headlessPermissionModeWire.js +24 -21
  66. package/dist/headlessReconnectWire.d.ts +7 -1
  67. package/dist/headlessReconnectWire.js +20 -2
  68. package/dist/hitl/approvalsFeed.d.ts +22 -1
  69. package/dist/hitl/approvalsFeed.js +103 -9
  70. package/dist/hitl/askGateWire.d.ts +27 -96
  71. package/dist/hitl/askGateWire.js +69 -618
  72. package/dist/hitl/frameRouter.d.ts +86 -0
  73. package/dist/hitl/frameRouter.js +383 -0
  74. package/dist/hitl/gateLedger.d.ts +119 -0
  75. package/dist/hitl/gateLedger.js +113 -0
  76. package/dist/hitl/hitlBridge.d.ts +75 -15
  77. package/dist/hitl/hitlBridge.js +94 -22
  78. package/dist/hitl/hitlHostSurface.d.ts +49 -0
  79. package/dist/hitl/hitlHostSurface.js +155 -0
  80. package/dist/hitl/parkResolver.d.ts +74 -0
  81. package/dist/hitl/parkResolver.js +250 -0
  82. package/dist/hitl/planReviewWire.d.ts +60 -2
  83. package/dist/hitl/planReviewWire.js +197 -91
  84. package/dist/hitl/toolApprovalWire.d.ts +67 -4
  85. package/dist/hitl/toolApprovalWire.js +129 -31
  86. package/dist/hooksWireCaps.d.ts +1 -82
  87. package/dist/hooksWireCaps.js +44 -237
  88. package/dist/host.d.ts +16 -5
  89. package/dist/index.d.ts +2 -0
  90. package/dist/index.js +15 -1
  91. package/dist/interactiveToolsWire.d.ts +15 -4
  92. package/dist/interactiveToolsWire.js +24 -26
  93. package/dist/limitsWire.js +7 -40
  94. package/dist/liveInitToolFace.d.ts +51 -6
  95. package/dist/liveQuestionStore.d.ts +18 -6
  96. package/dist/liveQuestionStore.js +15 -0
  97. package/dist/model/providerPresets.js +11 -1
  98. package/dist/notifications.d.ts +62 -2
  99. package/dist/notifications.js +306 -49
  100. package/dist/printToolResultFrame.d.ts +12 -4
  101. package/dist/printToolResultFrame.js +11 -21
  102. package/dist/retainBackgroundWireCaps.d.ts +3 -2
  103. package/dist/retainBackgroundWireCaps.js +5 -9
  104. package/dist/sandboxWire.d.ts +9 -31
  105. package/dist/sandboxWire.js +51 -50
  106. package/dist/scenarioWire.d.ts +1 -1
  107. package/dist/scenarioWire.js +25 -36
  108. package/dist/seam.d.ts +23 -6
  109. package/dist/seam.js +40 -30
  110. package/dist/seatContract.d.ts +369 -83
  111. package/dist/seatContract.js +585 -198
  112. package/dist/selfOrchestrationWireCaps.d.ts +6 -5
  113. package/dist/selfOrchestrationWireCaps.js +8 -12
  114. package/dist/sessionSlot.d.ts +8 -0
  115. package/dist/sessionSlot.js +1 -0
  116. package/dist/steering.js +2 -2
  117. package/dist/subagent/engineTaskHandleWire.d.ts +3 -0
  118. package/dist/subagent/engineTaskHandleWire.js +17 -2
  119. package/dist/subagentContentStore.d.ts +5 -5
  120. package/dist/toolResult.d.ts +89 -8
  121. package/dist/toolResult.js +99 -30
  122. package/dist/typePins.d.ts +17 -0
  123. package/dist/typePins.js +1 -0
  124. package/dist/ultracodeWireCaps.js +5 -6
  125. package/dist/unrefTimer.d.ts +30 -0
  126. package/dist/unrefTimer.js +5 -0
  127. package/dist/workflow.d.ts +3 -2
  128. package/dist/workflow.js +3 -2
  129. package/dist/workflowClient.d.ts +7 -0
  130. package/dist/workflowClient.js +47 -12
  131. package/package.json +3 -3
@@ -36,7 +36,7 @@
36
36
  * 挂账(诚实边界):approve 后壳本地的 plan-mode footer 徽章不自动退(CC 会退)——退 mode 的
37
37
  * app-state seam 不在本模块可达面,记 rc.47 接 REPL 层;engine 侧 handsReadOnly 已由 resume 处理。
38
38
  */
39
- import { publishQuestionFrame, registerLocalQuestionResponder, } from '../liveQuestionStore.js';
39
+ import { publishQuestionFrame, registerLocalQuestionResponder, hasLocalQuestionResponder, } from '../liveQuestionStore.js';
40
40
  import { hostLog } from '../host.js';
41
41
  import { makeEngineWireClient } from '../engineWireSdk.js';
42
42
  import { engineWireTarget } from '../engineWireTarget.js';
@@ -44,8 +44,31 @@ import { enqueuePlanReviewOutcome } from '../notifications.js';
44
44
  /** resume 是 SYNC 驱动到终态的(可能分钟级)——原 raw fetch 无超时;SDK Transport 恒有每请求
45
45
  * 超时,取 6h 宽上界(诚实预算:超过它基本是引擎僵死,超时文案与网络失败同走 outcome 通知)。 */
46
46
  const PLAN_REVIEW_RESUME_TIMEOUT_MS = 6 * 60 * 60_000;
47
- const APPROVE_LABEL = 'Yes, approve and run the plan';
48
- const REJECT_LABEL = 'No, reject it (keep planning)';
47
+ /**
48
+ * REF-CC-026:此前是 module-private 常量,唯一另一个消费者(cli `planReviewReopen.ts`)只能靠
49
+ * 人眼手抄同步(头注写着「与 client-core planReviewWire 的原卡逐字一致」)——漂一个字节,判决就
50
+ * 落到危险的默认臂(见下方 `planReviewDecisionFromAnswer` 的头注)。现在导出成唯一真源;cli 侧改
51
+ * import 是后续跟车工单(壳仓文件域,不在本卡范围)。
52
+ */
53
+ export const PLAN_REVIEW_APPROVE_LABEL = 'Yes, approve and run the plan';
54
+ export const PLAN_REVIEW_REJECT_LABEL = 'No, reject it (keep planning)';
55
+ const APPROVE_LABEL = PLAN_REVIEW_APPROVE_LABEL;
56
+ const REJECT_LABEL = PLAN_REVIEW_REJECT_LABEL;
57
+ /**
58
+ * REF-CC-026:三值判决(此前 `selected === APPROVE_LABEL ? 'approve' : 'reject'` 把「没读懂/没
59
+ * 选」的第三种输入静默塌缩进危险的 reject 臂——overlay 的 dismiss/拒答语义是「送空 answers」,
60
+ * 与用户真的点了 REJECT_LABEL 长得一模一样)。两个标签都做**显式**等值比对,任何第三种输入
61
+ * (含 undefined)落 `'dismissed'`,由调用方决定是保持 park 还是显式当 reject 处理——本函数只
62
+ * 负责如实分类,不替调用方做选择。
63
+ */
64
+ export function planReviewDecisionFromAnswer(answer) {
65
+ const selected = answer?.answers?.[0]?.selected?.[0];
66
+ if (selected === APPROVE_LABEL)
67
+ return 'approve';
68
+ if (selected === REJECT_LABEL)
69
+ return 'reject';
70
+ return 'dismissed';
71
+ }
49
72
  /** done 帧的 plan_review park 形状(结构性读,别的终态一律 false)。 */
50
73
  export function isPlanReviewPark(result) {
51
74
  const r = result;
@@ -55,45 +78,109 @@ export function isPlanReviewPark(result) {
55
78
  typeof r.taskId === 'string' &&
56
79
  r.taskId.length > 0);
57
80
  }
81
+ /**
82
+ * REF-CC-027:已武装未消解的 taskId 去重集(与 `HitlBridge`/`liveQuestionStore` 同款「进程内单例
83
+ * 承重不变量」——见 `docs/refactor/p1-scan/singleton-manifest.json` 登记)。durable re-attach 会把
84
+ * 同一个 `done{needs_review}` 帧再送一遍(与 askGateWire 的 `decidedGates` 同一族问题);此前
85
+ * `armPlanReviewApproval` 对同一 taskId 的重复调用会**再注册一次** responder(旧的注销函数被整个
86
+ * 丢掉),第二次注册会覆盖 `localResponders` 里的条目——若第一张卡此刻还没答,它的 unregister
87
+ * 闭包会变成一个指向"已被覆盖"槽位的悬空引用。这个集合让重复 arm 直接幂等短路,不再产生第二次
88
+ * `registerLocalQuestionResponder` 调用。
89
+ */
90
+ const armedPlanReviewTaskIds = new Set();
91
+ /** 测试钩:清掉武装态(不动 liveQuestionStore 本身,那边有自己的 reset)。 */
92
+ export function _resetArmedPlanReviewsForTest() {
93
+ armedPlanReviewTaskIds.clear();
94
+ }
95
+ /** 合成 questionId 的唯一铸口(短路臂与首次 arm 共用,别各铸各的)。 */
96
+ function planReviewQuestionId(taskId) {
97
+ return `plan-review:${taskId}`;
98
+ }
99
+ /** 审批卡的题面 —— 首次 arm 与「重放 arm 重新弹卡」共用同一份,两处不许各写一份。 */
100
+ function publishPlanReviewCard(taskId) {
101
+ publishQuestionFrame({
102
+ type: 'question',
103
+ questionId: planReviewQuestionId(taskId),
104
+ questions: [
105
+ {
106
+ header: 'Plan review',
107
+ question: 'The plan is ready for review. Approve it and start the implementation?',
108
+ options: [
109
+ { label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)' },
110
+ { label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it' },
111
+ ],
112
+ multiSelect: false,
113
+ },
114
+ ],
115
+ });
116
+ }
58
117
  /**
59
118
  * Arm the approval card for a parked plan_review (call with the RAW done.result). Returns true when
60
- * armed (live path + shape matched); false = caller renders the terminal as usual. Fail-soft: any
61
- * error inside must never break the stream drain.
119
+ * armed (live path + shape matched, INCLUDING the idempotent "already armed" short-circuit — see
120
+ * REF-CC-027 above); false = caller renders the terminal as usual. Fail-soft: any error inside must
121
+ * never break the stream drain.
62
122
  */
63
123
  export function armPlanReviewApproval(result) {
124
+ let armedTaskId;
64
125
  try {
65
126
  if (!isPlanReviewPark(result))
66
127
  return false;
67
128
  if (!engineWireTarget())
68
129
  return false;
69
130
  const { taskId } = result;
70
- const questionId = `plan-review:${taskId}`;
71
- registerLocalQuestionResponder(questionId, async (_id, answer) => {
72
- const selected = answer?.answers?.[0]?.selected?.[0];
73
- const decision = selected === APPROVE_LABEL ? 'approve' : 'reject';
131
+ if (armedPlanReviewTaskIds.has(taskId)) {
132
+ // REF-CC-027:同一张卡还没消解就被重放的 arm 撞上——幂等,**不二次注册** responder。
133
+ //
134
+ // [2393] hitl-F3(2026-08-02):但「不二次注册」不等于「什么都不做」。旧码在这里只打一行
135
+ // debug 就 `return true`,于是这条短路成了一堵挡死重开路径的墙:
136
+ // · `liveQuestionStore.ts` 的契约明写「dialog dismissed without answering ⇒ caller cleans
137
+ // up」,而本模块**没有** dismiss/abort 腿 —— 用户 Esc 关掉卡(不作答)之后,武装态
138
+ // 一直是 true,responder 也一直挂着;
139
+ // · 此后每一次 arm(durable re-attach 重放 / 409 撞上再开)都走这条短路:**一张卡都不弹**,
140
+ // 调用方拿到 true 于是也不渲终帧 —— 用户面静默挂住,plan 卡在 park 里没有任何出口。
141
+ // 修法按「还在不在」分两支,两支都不产生第二次 `registerLocalQuestionResponder`:
142
+ if (hasLocalQuestionResponder(planReviewQuestionId(taskId))) {
143
+ // responder 还绑着 ⇒ 这张卡仍然可作答,只是可能已经不在屏上了。**重新弹一次**
144
+ // (同一个 questionId、同一份题面、同一个 responder):卡还开着的宿主只是收到一帧同形
145
+ // 的重绘,卡已经被 dismiss 的宿主则拿回了它的重开路径。
146
+ hostLog('debug', `planReviewWire: arm replayed — re-presenting the still-armed card for task ${taskId}`);
147
+ publishPlanReviewCard(taskId);
148
+ return true;
149
+ }
150
+ // responder 不在了(被别的注册顶掉后注销 / 被单方面清理)⇒ 武装态是**陈旧**的。
151
+ // 复用它 = 弹一张没有任何人能收答的卡(比不弹更坏:它看起来能点)。丢掉陈旧记号,
152
+ // 往下走整条重新 arm —— 这正是 REF-CC-027 头注里那个「悬空引用」形的收尾。
153
+ hostLog('debug', `planReviewWire: stale armed state for task ${taskId} (no local responder) — re-arming from scratch`);
154
+ armedPlanReviewTaskIds.delete(taskId);
155
+ }
156
+ armedPlanReviewTaskIds.add(taskId);
157
+ armedTaskId = taskId;
158
+ const questionId = planReviewQuestionId(taskId);
159
+ const unregister = registerLocalQuestionResponder(questionId, async (_id, answer) => {
160
+ // REF-CC-027:一次性 —— 首句就注销 + 清武装态(照 askGateWire 的 cleanup() 形)。
161
+ // liveQuestionStore.respondToQuestion 自身也是一次性取件(delete-before-invoke),这里是
162
+ // 第二道钉:武装态一清,同 taskId 才能重新正常 arm(证明 responder 真的落定了)。
163
+ unregister();
164
+ armedPlanReviewTaskIds.delete(taskId);
165
+ const decided = planReviewDecisionFromAnswer(answer);
166
+ if (decided === 'dismissed') {
167
+ // dismiss/无法判读:诚实缺席优先于编造默认值(REF-CC-026)——不静默驱动 decidePlanReview,
168
+ // 卡的重开路径把决定权还给用户。
169
+ return { ok: true };
170
+ }
74
171
  // fire-and-forget:resume 同步驱动到终态可能分钟级,不能挂住 overlay;结果经 queue 通知回来
75
- void decidePlanReview(taskId, decision);
172
+ void decidePlanReview(taskId, decided);
76
173
  return { ok: true };
77
174
  });
78
- publishQuestionFrame({
79
- type: 'question',
80
- questionId,
81
- questions: [
82
- {
83
- header: 'Plan review',
84
- question: 'The plan is ready for review. Approve it and start the implementation?',
85
- options: [
86
- { label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)' },
87
- { label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it' },
88
- ],
89
- multiSelect: false,
90
- },
91
- ],
92
- });
175
+ publishPlanReviewCard(taskId);
93
176
  hostLog('debug', `planReviewWire: approval card armed for parked plan (task ${taskId})`);
94
177
  return true;
95
178
  }
96
179
  catch (e) {
180
+ // REF-CC-027:arm 本身失败(register/publish 抛)绝不能把 taskId 永久锁在武装态里
181
+ // ——那样这张卡再也无法被正常 arm 一次。
182
+ if (armedTaskId !== undefined)
183
+ armedPlanReviewTaskIds.delete(armedTaskId);
97
184
  hostLog('debug', `planReviewWire: arm failed (fail-soft): ${String(e)}`);
98
185
  return false;
99
186
  }
@@ -104,80 +191,99 @@ export function armPlanReviewApproval(result) {
104
191
  * server body.error 原文)→ 同款「HTTP <status> <error>」outcome 文案;网络失败走原「could not
105
192
  * reach the engine」臂。 */
106
193
  export async function decidePlanReview(taskId, decision) {
194
+ // REF-CC-025:此前 `cfg`/`client` 缺席各自 `return` 静默——卡已经对用户回了「收到」
195
+ // (armPlanReviewApproval 的 responder 早就 `return {ok:true}` 过了),但决断本身连一次网络
196
+ // 请求都没发出去,而模型/用户没有任何回程信号。两条早退现在都汇进同一条 outcome 管道(下方
197
+ // 唯一的 enqueue 调用点),不再是两条独立的静默 return。
198
+ let outcome;
107
199
  const cfg = engineWireTarget();
108
- if (!cfg)
109
- return;
110
- const client = makeEngineWireClient({
111
- baseUrl: cfg.baseUrl,
112
- ...(cfg.token ? { token: cfg.token } : {}),
113
- principal: cfg.principal,
114
- timeoutMs: PLAN_REVIEW_RESUME_TIMEOUT_MS,
115
- });
116
- if (!client)
117
- return;
118
- let outcome = '';
119
- try {
120
- const body = await client.assistant.planReview(taskId, { decision });
121
- hostLog('debug', `planReviewWire: ${decision} → ok ${JSON.stringify(body).slice(0, 200)}`);
122
- // [2315]/[2316](#109,2026-08-02):decide 的 **2xx 不当终态** —— test 黑盒实测 reject 9/9
123
- // 返回 200 而会话仍锁在同一 gate(core RB-471:重开兜底对 reject 腿恒真误触发)。这里回拉
124
- // 一次任务状态,按真形分三路措辞;根因归 core/server,本腿是「对外动作回读验证」在产品面的
125
- // 同款,上游修后仍有价值(approve 合法推进新 gate 的形也靠它说真话)。
126
- // 回拉用**短超时**独立 client(15s):verify 腿挂住不该占用 decide 的 6h 宽预算。
127
- let postStatus;
128
- try {
129
- const probe = makeEngineWireClient({
130
- baseUrl: cfg.baseUrl,
131
- ...(cfg.token ? { token: cfg.token } : {}),
132
- principal: cfg.principal,
133
- timeoutMs: 15_000,
134
- });
135
- const post = probe ? (await probe.runs.get(taskId)) : null;
136
- postStatus = typeof post?.status === 'string' ? post.status : undefined;
137
- }
138
- catch (e) {
139
- hostLog('debug', `planReviewWire: post-decide status re-pull failed (fail-soft): ${String(e)}`);
140
- }
141
- // 回拉失败时回落 decide 200 体自带的 status(次级来源;两者都缺=unverified,措辞如实降级)。
142
- const effective = postStatus ?? (typeof body?.status === 'string' ? body.status : undefined);
143
- if (effective === 'needs_review') {
144
- // 决定没生效,仍锁原 gate。不渲「已处理」;真出路只有 approve 或 cancel(下一次提交撞 409
145
- // 时 activeRunSelfHeal 会把审批卡重开——这里不自动重弹卡,避免「刚拒绝又弹卡」的突袭感,
146
- // 决定权经卡的重开路径还给用户)。
147
- outcome =
148
- `The plan_review ${decision} returned HTTP 200 but the session is STILL locked on the same review gate ` +
149
- `(post-decide status: needs_review). The decision did NOT take effect (known engine issue, RB-471 family). ` +
150
- `Tell the user plainly that the ${decision} did not go through; the reliable exits today are approving the plan or cancelling the task.`;
151
- }
152
- else if (effective === 'suspended') {
153
- // 合法推进到新 gate(test [2315] ②形):不是「完成」,如实说下一张审批卡会跟上。
154
- outcome =
155
- `The plan was ${decision === 'approve' ? 'approved' : 'rejected'} and the task advanced to a NEW approval gate ` +
156
- `(post-decide status: suspended) — the next approval card will surface it; this is not a completion yet.`;
157
- }
158
- else if (effective !== undefined) {
159
- outcome = `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — final status: ${effective} (post-decide re-checked: task left the review gate).`;
160
- }
161
- else {
162
- outcome = `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — final status: unknown (post-decide verification unavailable; treat as unconfirmed).`;
163
- }
200
+ if (!cfg) {
201
+ hostLog('error', `planReviewWire: ${decision} NOT sent — engineWireTarget() unavailable`);
202
+ outcome = `The plan_review ${decision} could NOT be sent: no engine connection is configured on this host. Tell the user plainly that the decision did not go through.`;
164
203
  }
165
- catch (e) {
166
- // APIError 判型走 status duck-check(sseIdleTriage 同款纪律:双包时 instanceof 会分叉)。
167
- const status = e?.status;
168
- if (typeof status === 'number') {
169
- const msg = e instanceof Error ? e.message : String(e);
170
- hostLog('debug', `planReviewWire: ${decision} → ${status} ${msg.slice(0, 200)}`);
171
- outcome = `The plan_review decision failed: HTTP ${status} ${msg}`.trim();
204
+ else {
205
+ const client = makeEngineWireClient({
206
+ baseUrl: cfg.baseUrl,
207
+ ...(cfg.token ? { token: cfg.token } : {}),
208
+ principal: cfg.principal,
209
+ timeoutMs: PLAN_REVIEW_RESUME_TIMEOUT_MS,
210
+ });
211
+ if (!client) {
212
+ hostLog('error', `planReviewWire: ${decision} NOT sent — makeEngineWireClient() returned null`);
213
+ outcome = `The plan_review ${decision} could NOT be sent: the engine client could not be constructed. Tell the user plainly that the decision did not go through.`;
172
214
  }
173
215
  else {
174
- outcome = `The plan_review decision could not reach the engine: ${String(e)}`;
216
+ try {
217
+ const body = await client.assistant.planReview(taskId, { decision });
218
+ hostLog('debug', `planReviewWire: ${decision} → ok ${JSON.stringify(body).slice(0, 200)}`);
219
+ // [2315]/[2316](#109,2026-08-02):decide 的 **2xx 不当终态** —— test 黑盒实测 reject 9/9
220
+ // 返回 200 而会话仍锁在同一 gate(core RB-471:重开兜底对 reject 腿恒真误触发)。这里回拉
221
+ // 一次任务状态,按真形分三路措辞;根因归 core/server,本腿是「对外动作回读验证」在产品面的
222
+ // 同款,上游修后仍有价值(approve 合法推进新 gate 的形也靠它说真话)。
223
+ // 回拉用**短超时**独立 client(15s):verify 腿挂住不该占用 decide 的 6h 宽预算。
224
+ let postStatus;
225
+ try {
226
+ const probe = makeEngineWireClient({
227
+ baseUrl: cfg.baseUrl,
228
+ ...(cfg.token ? { token: cfg.token } : {}),
229
+ principal: cfg.principal,
230
+ timeoutMs: 15_000,
231
+ });
232
+ const post = probe ? (await probe.runs.get(taskId)) : null;
233
+ postStatus = typeof post?.status === 'string' ? post.status : undefined;
234
+ }
235
+ catch (e) {
236
+ hostLog('debug', `planReviewWire: post-decide status re-pull failed (fail-soft): ${String(e)}`);
237
+ }
238
+ // 回拉失败时回落 decide 200 体自带的 status(次级来源;两者都缺=unverified,措辞如实降级)。
239
+ const effective = postStatus ?? (typeof body?.status === 'string' ? body.status : undefined);
240
+ if (effective === 'needs_review') {
241
+ // 决定没生效,仍锁原 gate。不渲「已处理」;真出路只有 approve 或 cancel(下一次提交撞 409
242
+ // 时 activeRunSelfHeal 会把审批卡重开——这里不自动重弹卡,避免「刚拒绝又弹卡」的突袭感,
243
+ // 决定权经卡的重开路径还给用户)。
244
+ outcome =
245
+ `The plan_review ${decision} returned HTTP 200 but the session is STILL locked on the same review gate ` +
246
+ `(post-decide status: needs_review). The decision did NOT take effect (known engine issue, RB-471 family). ` +
247
+ `Tell the user plainly that the ${decision} did not go through; the reliable exits today are approving the plan or cancelling the task.`;
248
+ }
249
+ else if (effective === 'suspended') {
250
+ // 合法推进到新 gate(test [2315] ②形):不是「完成」,如实说下一张审批卡会跟上。
251
+ outcome =
252
+ `The plan was ${decision === 'approve' ? 'approved' : 'rejected'} and the task advanced to a NEW approval gate ` +
253
+ `(post-decide status: suspended) — the next approval card will surface it; this is not a completion yet.`;
254
+ }
255
+ else if (effective !== undefined) {
256
+ outcome = `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — final status: ${effective} (post-decide re-checked: task left the review gate).`;
257
+ }
258
+ else {
259
+ outcome = `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — final status: unknown (post-decide verification unavailable; treat as unconfirmed).`;
260
+ }
261
+ }
262
+ catch (e) {
263
+ // APIError 判型走 status duck-check(sseIdleTriage 同款纪律:双包时 instanceof 会分叉)。
264
+ const status = e?.status;
265
+ if (typeof status === 'number') {
266
+ const msg = e instanceof Error ? e.message : String(e);
267
+ hostLog('debug', `planReviewWire: ${decision} → ${status} ${msg.slice(0, 200)}`);
268
+ outcome = `The plan_review decision failed: HTTP ${status} ${msg}`.trim();
269
+ }
270
+ else {
271
+ outcome = `The plan_review decision could not reach the engine: ${String(e)}`;
272
+ }
273
+ }
175
274
  }
176
275
  }
276
+ // REF-CC-025:唯一的 enqueue 调用点(三条上游路径——cfg 缺席 / client 缺席 / 正常决断—— 都汇到
277
+ // 这里)。返回值此前被丢弃:`enqueuePlanReviewOutcome` 返回 `false` 意味着队列口没装或没实现
278
+ // `enqueueMetaPrompt`(notifications.ts 头注:「模型永远不知道 plan 被批准/驳回后引擎跑出了
279
+ // 什么结果」),这是与 HTTP 失败同等重量的静默丢失,必须留痕。
177
280
  try {
178
- enqueuePlanReviewOutcome(`<plan-review-outcome>\n${outcome}${decision === 'approve' ? `\nUse TaskOutput("${taskId}") if you need the resumed task's full output, then give the user a brief report of what was done.` : '\nBriefly acknowledge to the user that the plan was rejected and continue planning.'}\n</plan-review-outcome>`);
281
+ const delivered = enqueuePlanReviewOutcome(`<plan-review-outcome>\n${outcome}${decision === 'approve' ? `\nUse TaskOutput("${taskId}") if you need the resumed task's full output, then give the user a brief report of what was done.` : '\nBriefly acknowledge to the user that the plan was rejected and continue planning.'}\n</plan-review-outcome>`);
282
+ if (!delivered) {
283
+ hostLog('error', `planReviewWire: outcome enqueue MISSED (queue port not installed or lacks enqueueMetaPrompt) for task ${taskId} — model will not learn the ${decision} result`);
284
+ }
179
285
  }
180
286
  catch (e) {
181
- hostLog('debug', `planReviewWire: outcome enqueue failed: ${String(e)}`);
287
+ hostLog('debug', `planReviewWire: outcome enqueue threw: ${String(e)}`);
182
288
  }
183
289
  }
@@ -1,3 +1,66 @@
1
+ /**
2
+ * ⇄ B7 批搬迁(2026-07-27,设计稿 §3 B7):cli `src/sema/liveToolApprovalWire.ts`(508 行)——
3
+ * **拆**(设计稿原文:「liveToolApprovalWire.ts | 509 | …… | **拆分**,B7;工具 schema 属
4
+ * agent-types 域」)。
5
+ *
6
+ * ── 拆缝在哪 ────────────────────────────────────────────────────────────────────────────────
7
+ * **进包(本文件)**:gate 判定(`isFsApprovalGate` / `toolNameIsFsWrite`)· 结构等值
8
+ * (`structurallyEqual`,ctrl+g amend 的「真编辑过」判据)· 同步帧契约与判别式
9
+ * (`ToolApprovalFrame` / `isToolApprovalFrame` / `isFromSubagent`)· 徽章合成
10
+ * (`subagentBadgeFor`)· gate 文案兜 path(`pathFromGateMessage`)· **两条决断腿的编排**
11
+ * (`surfaceFsApprovalAndDecide` durable-park 腿 / `surfaceToolApprovalFrameAndRespond` 同步帧腿)。
12
+ * **留宿主**:**卡本身**(`surfaceApprovalCard`)—— 它要 leader `ToolUseConfirm` 队列、壳的
13
+ * vendored `Tool` 注册表(`FileWriteTool`/`FileEditTool`/`NotebookEditTool`/`EngineWorkflowTool`)、
14
+ * `applyPermissionUpdates` 与 `AppState.toolPermissionContext`。那是**端的渲染与权限态**,
15
+ * web/桌面各有各的形(浏览器根本没有 Ink 三选卡)。落点 = 下面的 `ApprovalCardPort`。
16
+ *
17
+ * 🔴 **拆缝的失效形**([paired-mechanisms-must-share-premise]):卡口没装 ⇒ 两条腿都拿不到人的
18
+ * 决断。**这不是「静默不启用」可以接受的面** —— 同步帧腿上引擎正同步阻塞着,不 respond 就干等
19
+ * TTL;durable 腿上 run 卡在 suspended。所以本口:
20
+ * · **计 miss**(`approvalCardPortMisses()`,宿主自检恒应为 0);
21
+ * · 缺席时返回 `{kind:'failed', reason:'no approval card port installed …'}` —— 与壳原文
22
+ * 「no leader ToolUseConfirm queue registered(print/non-REPL mode)」**同一条 fail-soft 语义**
23
+ * (print/headless 本来就没有卡面),调用方照旧走 fail-closed deny / 诚实红。
24
+ *
25
+ * 🔴 搬迁差分(逐条,零行为变化):
26
+ * 1. `toolForName`(4 个 `require('../tools/…')`)整条留宿主 —— 工具注册表是壳资产。
27
+ * 2. `getLeaderToolUseConfirmQueue` / `getLeaderSetToolPermissionContext` / `applyPermissionUpdates` /
28
+ * `getAppStateStoreRef` / `syntheticAssistantMessage` / `stubToolUseContext` 全部随卡留宿主。
29
+ * 3. `logForDebugging` → `hostLog('debug'|'error', …)`(本包零 `utils/debug`,portability 门盯着)。
30
+ * 4. `require('./fleetClient.js').readEngineActiveBgTasks()` → 静态 import 包内
31
+ * `fleet/fleetLedger.js` 的同名读口(B6 已搬入;延迟 require 的理由「纯 wire 模块不背 fleet 图
32
+ * 冷启动」在包内不成立 —— 同一个包)。⚠️ try/catch 保留:台账不可用时兜 taskId 短形那一支
33
+ * 仍要成立。
34
+ *
35
+ * ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
36
+ *
37
+ * liveToolApprovalWire — 权限三选卡的壳侧半场([816] 壳侧承诺① / [819]② / [820] 四模式表,2026-07-14)。
38
+ *
39
+ * WIRE(server 1.191 已到货,[830]①):引擎对 fs 写工具(Write/Edit/NotebookEdit)出 ask ——
40
+ * 真腿=同步 `tool_approval` 帧(named SSE,redactDeep+16KiB 帽)→ 三选卡 → POST
41
+ * /v1/tool-approvals/:id/respond {allow|allow_session|deny}(本文件下半)。durable park 腿
42
+ * (checkpointGate → approvals.decide)🔴 **现势(REF-CC-023/031,2026-08-02)**:不是「旧引擎兜底」——
43
+ * [2150] S1 半场(83-87 行)证实产品默认 durable 投递下,引擎对 Bash 出的 durable park 就是**活路径**
44
+ * (r1 E5 fail-stuck 实例);synthApprovalProbe 合成单测仍在,但这条腿今天会被真用户真流量走到。
45
+ * 本模块把 gate 面翻成壳的 VENDORED CC 权限对话框(PermissionRequest → FileWritePermissionRequest,
46
+ * 三选文案逐字 CC 207:`Yes` / `Yes, allow all edits during this session (shift+tab)`[cwd 外变体带
47
+ * dirname] / `No`,题干 `Do you want to create <basename>?` verb=create/overwrite/write to)——零新 UI,
48
+ * 键行为(1/2/3、shift+tab 聚焦第 2 项、esc=reject)全部继承 vendor 组件。
49
+ *
50
+ * 决断腿:
51
+ * - Yes(accept-once)→ HitlBridge.decideTool({decision:'approve'})(D-1 绑定 verbatim,合 [819]②)。
52
+ * - Yes-allow-all(accept-session)→ 同 approve,且 permissionUpdates 里 vendor 产出的
53
+ * `{type:'setMode', mode:'acceptEdits', destination:'session'}` 经 leader setter 落壳 AppState →
54
+ * seamQuery 的 permissionModeField 下一 turn 起 stamp `permissionMode:"acceptEdits"`([816] 壳侧
55
+ * 承诺②:accept-session=升 intent,wire 已通)。cwd 外变体还带 addDirectories(session)——照 CC 收。
56
+ * - No(reject)→ decideTool({decision:'deny'});调用方(liveHitlAskWire)对该 call 的重放 tool_end
57
+ * stamp REJECT_MESSAGE,vendor 的 renderToolUseRejectedMessage 渲 `User rejected <op> to <path>`
58
+ * (subtle 色,CC 逐字)。
59
+ *
60
+ * 🔴 fail-soft 铁律(与 ask 桥同款):leader 队列未注册(print/非 REPL)、pending 蒸发、decide 409/404
61
+ * 都返回 failed —— 调用方回退「flush + 原样终帧」的诚实红,绝不更糟。
62
+ * 🔴 UNTRUSTED:gate args 为模型作文(service 已 redact),只渲染绝不回喂;决断只带 decision 枚举。
63
+ */
1
64
  import { type HitlClientLike } from './hitlBridge.js';
2
65
  /** fs 写权限 gate 判定:未来的一等 kind(tool_approval)或按 toolName(server 桥首批=fs 写三件,
3
66
  * [820] 表)。AskUserQuestion 永不进这里(ask 桥先判)。 */
@@ -19,14 +82,14 @@ export declare function isToolApprovalGate(gate: {
19
82
  } | undefined | null): boolean;
20
83
  export type FsApprovalOutcome = {
21
84
  kind: 'decided';
22
- gatedCallId?: string;
23
- denied?: boolean;
85
+ gatedCallId?: string | undefined;
86
+ denied?: boolean | undefined;
24
87
  } | {
25
88
  kind: 'aborted';
26
- gatedCallId?: string;
89
+ gatedCallId?: string | undefined;
27
90
  } | {
28
91
  kind: 'failed';
29
- gatedCallId?: string;
92
+ gatedCallId?: string | undefined;
30
93
  reason: string;
31
94
  };
32
95
  /** 本桥消费的 wire 面(liveHitlAskWire 的 AskGateWireDeps 同形切片,mock 可注入)。 */