@sema-agent/client-core 0.72.11 → 0.72.13
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 +43 -0
- package/README.md +3 -1
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +3 -2
- package/dist/adapter/downstream/terminalToSdkResult.js +20 -3
- package/dist/adapter/runStream.d.ts +8 -0
- package/dist/adapter/runStream.js +45 -13
- package/dist/agentSession/backgroundView.js +7 -2
- package/dist/detachWire.d.ts +13 -0
- package/dist/detachWire.js +29 -12
- package/dist/engineAgentPanelStore.d.ts +15 -0
- package/dist/engineAgentPanelStore.js +52 -0
- package/dist/executionLaneCapability.d.ts +69 -0
- package/dist/executionLaneCapability.js +125 -0
- package/dist/fleet/fleetProjection.js +8 -1
- package/dist/fleetAgentPanelProjection.d.ts +1 -1
- package/dist/fleetAgentPanelProjection.js +31 -14
- package/dist/hitl/hitlBridge.d.ts +12 -1
- package/dist/hitl/hitlBridge.js +20 -0
- package/dist/hitl/parkResolver.d.ts +1 -1
- package/dist/hitl/parkResolver.js +2 -0
- package/dist/hitl/planReviewWire.d.ts +50 -3
- package/dist/hitl/planReviewWire.js +167 -23
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/notifications.d.ts +2 -2
- package/dist/notifications.js +46 -23
- package/dist/runTerminal.d.ts +9 -0
- package/dist/runTerminal.js +11 -0
- package/dist/toolResult.js +14 -5
- package/dist/workflow.js +3 -1
- package/dist/workflowClient.js +34 -6
- package/dist/workflowMonitor.d.ts +3 -2
- package/docs/INTEGRATION-CLIENTS.md +90 -12
- package/package.json +1 -1
|
@@ -41,6 +41,8 @@ import { hostLog } from '../host.js';
|
|
|
41
41
|
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
42
42
|
import { engineWireTarget } from '../engineWireTarget.js';
|
|
43
43
|
import { enqueuePlanReviewOutcome } from '../notifications.js';
|
|
44
|
+
import { isPlanReviewModeAfter } from './hitlBridge.js';
|
|
45
|
+
import { engineCapString } from '../engineCapsCache.js';
|
|
44
46
|
import { planReviewQuestionId, REOPEN_ID_TAIL } from './gateIdentity.js';
|
|
45
47
|
import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, registerArmedGateFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
|
|
46
48
|
import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
@@ -58,8 +60,42 @@ const PLAN_REVIEW_RESUME_TIMEOUT_MS = 6 * 60 * 60_000;
|
|
|
58
60
|
*/
|
|
59
61
|
export const PLAN_REVIEW_APPROVE_LABEL = 'Yes, approve and run the plan';
|
|
60
62
|
export const PLAN_REVIEW_REJECT_LABEL = 'No, reject it (keep planning)';
|
|
63
|
+
/**
|
|
64
|
+
* 0.72.13 CC-46(engine ≥7.86.0 S-433;契约 §4c):三选卡的两个 Yes —— 对齐 CC `ExitPlanMode` 卡
|
|
65
|
+
* 「Yes, and auto-accept edits」/「Yes, and manually approve edits」。只在**双闸**都过时才出三选卡(见 {@link planReviewCardOptions}
|
|
66
|
+
* 的调用点):宿主声明这条任务是它自己用 `permissionMode:"plan"` 提交的(适用面读不到 wire)∧ 引擎版本证据 ≥7.86.0。
|
|
67
|
+
*/
|
|
68
|
+
export const PLAN_REVIEW_APPROVE_AUTO_LABEL = 'Yes, and auto-accept edits';
|
|
69
|
+
export const PLAN_REVIEW_APPROVE_MANUAL_LABEL = 'Yes, and manually approve edits';
|
|
70
|
+
/** `permissionModeAfter` 的引擎地板(server 7.86.0;更老的 server 静默丢这个键 = 承诺了却不兑现)。 */
|
|
71
|
+
export const PLAN_REVIEW_MODE_AFTER_MIN_ENGINE = Object.freeze([7, 86, 0]);
|
|
72
|
+
/** 引擎版本串够不够新:三态 —— 读不出(缺席 / 非 x.y.z)⇒ `unknown`,**不折 supported**(没证据不许出三选卡)。 */
|
|
73
|
+
export function versionSupportsPlanReviewModeAfter(v) {
|
|
74
|
+
// 🔴 **整串**匹配正式版 x.y.z(无前导零):预发布(7.86.0-rc.1 可能还没实现这一位)/ 尾随垃圾 / v 前缀 / 空白 一律 unknown ——
|
|
75
|
+
// 它们不是支持证据(异源对抗复审:前缀匹配曾把这些都放行,宿主声明为 true 时三选卡就出了,而老引擎静默丢键)。
|
|
76
|
+
const mm = v ? /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(v) : null;
|
|
77
|
+
if (!mm)
|
|
78
|
+
return 'unknown';
|
|
79
|
+
const got = [Number(mm[1]), Number(mm[2]), Number(mm[3])];
|
|
80
|
+
for (let i = 0; i < 3; i++) {
|
|
81
|
+
const g = got[i];
|
|
82
|
+
const need = PLAN_REVIEW_MODE_AFTER_MIN_ENGINE[i];
|
|
83
|
+
if (g !== need)
|
|
84
|
+
return g > need ? 'supported' : 'unsupported';
|
|
85
|
+
}
|
|
86
|
+
return 'supported';
|
|
87
|
+
}
|
|
61
88
|
const APPROVE_LABEL = PLAN_REVIEW_APPROVE_LABEL;
|
|
62
89
|
const REJECT_LABEL = PLAN_REVIEW_REJECT_LABEL;
|
|
90
|
+
/** 老两选卡的标签集 = 判读口不给展示标签时的缺省读域。 */
|
|
91
|
+
const LEGACY_TWO_CHOICE_LABELS = Object.freeze([PLAN_REVIEW_APPROVE_LABEL, PLAN_REVIEW_REJECT_LABEL]);
|
|
92
|
+
/** 四个已知标签(只供**记账**口判「是不是一次决断性作答」,不驱动任何决断)。 */
|
|
93
|
+
const ALL_KNOWN_LABELS = Object.freeze([
|
|
94
|
+
PLAN_REVIEW_APPROVE_AUTO_LABEL,
|
|
95
|
+
PLAN_REVIEW_APPROVE_MANUAL_LABEL,
|
|
96
|
+
PLAN_REVIEW_APPROVE_LABEL,
|
|
97
|
+
PLAN_REVIEW_REJECT_LABEL,
|
|
98
|
+
]);
|
|
63
99
|
/**
|
|
64
100
|
* REF-CC-026:三值判决(此前 `selected === APPROVE_LABEL ? 'approve' : 'reject'` 把「没读懂/没
|
|
65
101
|
* 选」的第三种输入静默塌缩进危险的 reject 臂——overlay 的 dismiss/拒答语义是「送空 answers」,
|
|
@@ -67,14 +103,55 @@ const REJECT_LABEL = PLAN_REVIEW_REJECT_LABEL;
|
|
|
67
103
|
* (含 undefined)落 `'dismissed'`,由调用方决定是保持 park 还是显式当 reject 处理——本函数只
|
|
68
104
|
* 负责如实分类,不替调用方做选择。
|
|
69
105
|
*/
|
|
70
|
-
export function planReviewDecisionFromAnswer(answer
|
|
71
|
-
|
|
106
|
+
export function planReviewDecisionFromAnswer(answer,
|
|
107
|
+
/** 本次实际展示的那张卡的标签;不给 = 老两选(见 {@link planReviewChoiceFromAnswer})。 */
|
|
108
|
+
shownLabels) {
|
|
109
|
+
const choice = planReviewChoiceFromAnswer(answer, shownLabels);
|
|
110
|
+
if (choice === 'dismissed')
|
|
111
|
+
return 'dismissed';
|
|
112
|
+
return choice.decision;
|
|
113
|
+
}
|
|
114
|
+
export function planReviewChoiceFromAnswer(answer,
|
|
115
|
+
/**
|
|
116
|
+
* 本次**实际展示**的那张卡的标签(`planReviewCardOptions(offer).map(o => o.label)`);只认其中的标签。
|
|
117
|
+
* 🔴 **不给 = 老两选**:三选卡的两个 Yes 只有调用方显式说「我展示的是三选卡」才读得成批准 —— 缺省不许替老调用方
|
|
118
|
+
* 悄悄扩大批准的输入域(异源对抗复审:重开腿恒两选,缺省放行时一个没展示过的 auto 标签会被投递成 approve)。
|
|
119
|
+
*/
|
|
120
|
+
shownLabels = LEGACY_TWO_CHOICE_LABELS) {
|
|
121
|
+
// 🔴 这是一张**单选**卡,而其中一项在安全轴上(批准后放宽写征询):恰一题、恰一项才算作答。矛盾多选(`[auto, reject]`)/
|
|
122
|
+
// 多题 / 空 selected 一律 dismissed —— 此前只取 `selected[0]`,拒绝意图会被丢弃、放宽被执行(异源对抗复审 [high])。
|
|
123
|
+
const answers = answer?.answers;
|
|
124
|
+
if (!Array.isArray(answers) || answers.length !== 1)
|
|
125
|
+
return 'dismissed';
|
|
126
|
+
const picked = answers[0]?.selected;
|
|
127
|
+
if (!Array.isArray(picked) || picked.length !== 1)
|
|
128
|
+
return 'dismissed';
|
|
129
|
+
const selected = picked[0];
|
|
130
|
+
// 标签必须属于这次展示的卡:两选卡上不存在「auto-accept edits」,收到它 = 答的不是这张卡。
|
|
131
|
+
if (typeof selected !== 'string' || !shownLabels.includes(selected))
|
|
132
|
+
return 'dismissed';
|
|
133
|
+
if (selected === PLAN_REVIEW_APPROVE_AUTO_LABEL)
|
|
134
|
+
return { decision: 'approve', permissionModeAfter: 'acceptEdits' };
|
|
135
|
+
if (selected === PLAN_REVIEW_APPROVE_MANUAL_LABEL)
|
|
136
|
+
return { decision: 'approve', permissionModeAfter: 'default' };
|
|
72
137
|
if (selected === APPROVE_LABEL)
|
|
73
|
-
return 'approve';
|
|
138
|
+
return { decision: 'approve' };
|
|
74
139
|
if (selected === REJECT_LABEL)
|
|
75
|
-
return 'reject';
|
|
140
|
+
return { decision: 'reject' };
|
|
76
141
|
return 'dismissed';
|
|
77
142
|
}
|
|
143
|
+
/** 卡选项的**单源**:`offerModeAfter=false` ⇒ 老两选(逐字同旧);`true` ⇒ 三选(两个 Yes + No)。 */
|
|
144
|
+
export function planReviewCardOptions(offerModeAfter) {
|
|
145
|
+
const reject = { label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it' };
|
|
146
|
+
if (!offerModeAfter) {
|
|
147
|
+
return [{ label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)' }, reject];
|
|
148
|
+
}
|
|
149
|
+
return [
|
|
150
|
+
{ label: PLAN_REVIEW_APPROVE_AUTO_LABEL, description: 'Resume and execute the plan; edits inside the working directory are not asked about one by one' },
|
|
151
|
+
{ label: PLAN_REVIEW_APPROVE_MANUAL_LABEL, description: 'Resume and execute the plan; every edit still asks for approval' },
|
|
152
|
+
reject,
|
|
153
|
+
];
|
|
154
|
+
}
|
|
78
155
|
/**
|
|
79
156
|
* 推代的**门控口**(A-024.4;#244 F1 上收,语义照壳现实现):只有**决断性**作答(approve/
|
|
80
157
|
* reject 标签命中 —— {@link planReviewDecisionFromAnswer} 的官方三值分类)才消费当代 plan 门。
|
|
@@ -90,7 +167,8 @@ export function notePlanReviewAnsweredIfDecisive(questionId, answer) {
|
|
|
90
167
|
}
|
|
91
168
|
/** W1 带 key 变体。 */
|
|
92
169
|
function notePlanReviewAnsweredIfDecisiveFor(sessionKey, questionId, answer) {
|
|
93
|
-
|
|
170
|
+
// 记账口:两种卡的任一已知标签都算决断性作答(这里不投递决断;投递侧各自按**自己展示的卡**校验标签)。
|
|
171
|
+
if (planReviewDecisionFromAnswer(answer ?? { answers: [] }, ALL_KNOWN_LABELS) === 'dismissed')
|
|
94
172
|
return;
|
|
95
173
|
notePlanReviewAnsweredFor(sessionKey, questionId);
|
|
96
174
|
}
|
|
@@ -121,9 +199,15 @@ export function isPlanReviewPark(result) {
|
|
|
121
199
|
* `registerLocalQuestionResponder` 调用。
|
|
122
200
|
*/
|
|
123
201
|
const armedPlanReviewTaskIds = new Set();
|
|
202
|
+
/**
|
|
203
|
+
* 0.72.13 CC-46:这张已武装的卡**出的是不是三选卡**(两个 Yes 各带一档批准后模式)。与 {@link armedPlanReviewTaskIds}
|
|
204
|
+
* 同生命周期(arm 写、决断性作答 / arm 失败清);重放重呈读它,保证同一张卡前后两次呈现选项一致。缺席 = 老两选。
|
|
205
|
+
*/
|
|
206
|
+
const offerModeAfterByTask = new Map();
|
|
124
207
|
/** 测试钩:清掉武装态(不动 liveQuestionStore 本身,那边有自己的 reset)。 */
|
|
125
208
|
export function _resetArmedPlanReviewsForTest() {
|
|
126
209
|
armedPlanReviewTaskIds.clear();
|
|
210
|
+
offerModeAfterByTask.clear();
|
|
127
211
|
}
|
|
128
212
|
/* 合成 questionId 的唯一铸口:A-028.3 起收编进 `gateIdentity.planReviewQuestionId`(公面导出,
|
|
129
213
|
* 壳侧重开腿从此 import 同一铸口,不再手抄字面)。本文件只 import,不再自铸。 */
|
|
@@ -132,15 +216,13 @@ const ARM_QUESTION = 'The plan is ready for review. Approve it and start the imp
|
|
|
132
216
|
/** 重开腿的题面正句(点明「这张卡就是占住会话的那件事」;标签/选项与首呈同源)。 */
|
|
133
217
|
const REOPEN_QUESTION = 'This plan is still waiting for your review — it is what is holding this session. Approve it and start the implementation?';
|
|
134
218
|
/** 题面唯一构造点 —— 首次 arm、重放重呈、重开三处共用,标签/选项描述不许各写一份。 */
|
|
135
|
-
function planReviewQuestions(question) {
|
|
219
|
+
function planReviewQuestions(question, offerModeAfter = false) {
|
|
136
220
|
return [
|
|
137
221
|
{
|
|
138
222
|
header: 'Plan review',
|
|
139
223
|
question,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
{ label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it' },
|
|
143
|
-
],
|
|
224
|
+
// 选项单源 `planReviewCardOptions`:老两选逐字同旧;三选只在 arm 的双闸都过时出(重开腿恒老两选 = 批准不带键 ⇒ default)。
|
|
225
|
+
options: planReviewCardOptions(offerModeAfter),
|
|
144
226
|
multiSelect: false,
|
|
145
227
|
},
|
|
146
228
|
];
|
|
@@ -150,7 +232,7 @@ function publishPlanReviewCard(taskId) {
|
|
|
150
232
|
publishQuestionFrame({
|
|
151
233
|
type: 'question',
|
|
152
234
|
questionId: planReviewQuestionId(taskId),
|
|
153
|
-
questions: planReviewQuestions(ARM_QUESTION),
|
|
235
|
+
questions: planReviewQuestions(ARM_QUESTION, offerModeAfterByTask.get(taskId) === true),
|
|
154
236
|
});
|
|
155
237
|
}
|
|
156
238
|
/**
|
|
@@ -159,7 +241,13 @@ function publishPlanReviewCard(taskId) {
|
|
|
159
241
|
* REF-CC-027 above); false = caller renders the terminal as usual. Fail-soft: any error inside must
|
|
160
242
|
* never break the stream drain.
|
|
161
243
|
*/
|
|
162
|
-
export function armPlanReviewApproval(result, sessionKey
|
|
244
|
+
export function armPlanReviewApproval(result, sessionKey,
|
|
245
|
+
/**
|
|
246
|
+
* 0.72.13 CC-46:`submittedInPlanMode: true` = 宿主声明「这条任务是我用 `permissionMode:"plan"` 提交的」(只读起步)。
|
|
247
|
+
* 🔴 这件事**读不到 wire 上**(契约 §4c:两种任务产出同一张卡),所以只能由提交方声明;不声明 / false ⇒ 老两选卡、决断体不带键。
|
|
248
|
+
* 声明了还要过第二道闸:本进程 caps 缓存里的引擎版本 ≥7.86.0(更老的 server 静默丢这个键 = 卡上承诺了却不兑现)。
|
|
249
|
+
*/
|
|
250
|
+
opts) {
|
|
163
251
|
let armedTaskId;
|
|
164
252
|
try {
|
|
165
253
|
if (!isPlanReviewPark(result))
|
|
@@ -227,6 +315,13 @@ export function armPlanReviewApproval(result, sessionKey) {
|
|
|
227
315
|
}
|
|
228
316
|
armedPlanReviewTaskIds.add(taskId);
|
|
229
317
|
armedTaskId = taskId;
|
|
318
|
+
// CC-46 双闸:宿主声明 ∧ 引擎版本证据(读不出 ⇒ unknown ⇒ 不出三选卡)。
|
|
319
|
+
const offerModeAfter = opts?.submittedInPlanMode === true &&
|
|
320
|
+
versionSupportsPlanReviewModeAfter(engineCapString(engineWireTarget()?.baseUrl, 'version')) === 'supported';
|
|
321
|
+
if (offerModeAfter)
|
|
322
|
+
offerModeAfterByTask.set(taskId, true);
|
|
323
|
+
else
|
|
324
|
+
offerModeAfterByTask.delete(taskId);
|
|
230
325
|
const questionId = planReviewQuestionId(taskId);
|
|
231
326
|
const unregister = registerLocalQuestionResponder(questionId, async (_id, answer) => {
|
|
232
327
|
// REF-CC-027:一次性 —— 首句就注销 + 清武装态(照 askGateWire 的 cleanup() 形)。
|
|
@@ -234,19 +329,24 @@ export function armPlanReviewApproval(result, sessionKey) {
|
|
|
234
329
|
// 第二道钉:武装态一清,同 taskId 才能重新正常 arm(证明 responder 真的落定了)。
|
|
235
330
|
unregister();
|
|
236
331
|
armedPlanReviewTaskIds.delete(taskId);
|
|
237
|
-
const
|
|
238
|
-
|
|
332
|
+
const shown = planReviewCardOptions(offerModeAfterByTask.get(taskId) === true).map(o => o.label);
|
|
333
|
+
const choice = planReviewChoiceFromAnswer(answer, shown);
|
|
334
|
+
if (choice === 'dismissed') {
|
|
239
335
|
// dismiss/无法判读:诚实缺席优先于编造默认值(REF-CC-026)——不静默驱动 decidePlanReview,
|
|
240
336
|
// 卡的重开路径把决定权还给用户。呈现台账的键**不清**:同一张卡再被重开就是真「reopened」。
|
|
241
337
|
return { ok: true };
|
|
242
338
|
}
|
|
339
|
+
const decided = choice.decision;
|
|
340
|
+
// CC-46:只有三选卡的两个 Yes 带批准后模式;老两选的 Yes / No 不带 ⇒ 决断体不落键。
|
|
341
|
+
const modeAfter = choice.decision === 'approve' ? choice.permissionModeAfter : undefined;
|
|
342
|
+
offerModeAfterByTask.delete(taskId);
|
|
243
343
|
// A-024.4(#244 F1 起走分代):决断一经递交,这个门的呈现史就消费掉(当代键清 + 推代);
|
|
244
344
|
// 同 run 的**下一个** plan gate(approve 推进后引擎可再 park 一个新 plan)必须读回首见,
|
|
245
345
|
// 否则文案对一张从未呈现过的新卡说「reopened」。递交后决定未生效的形(RB-471 族)读回
|
|
246
346
|
// 首见只损失「reopened」一词 —— 首见文案零历史断言,诚实方向安全。
|
|
247
347
|
notePlanReviewAnswered(questionId);
|
|
248
348
|
// fire-and-forget:resume 同步驱动到终态可能分钟级,不能挂住 overlay;结果经 queue 通知回来
|
|
249
|
-
void decidePlanReview(taskId, decided);
|
|
349
|
+
void decidePlanReview(taskId, decided, modeAfter);
|
|
250
350
|
return { ok: true };
|
|
251
351
|
});
|
|
252
352
|
publishPlanReviewCard(taskId);
|
|
@@ -256,8 +356,10 @@ export function armPlanReviewApproval(result, sessionKey) {
|
|
|
256
356
|
catch (e) {
|
|
257
357
|
// REF-CC-027:arm 本身失败(register/publish 抛)绝不能把 taskId 永久锁在武装态里
|
|
258
358
|
// ——那样这张卡再也无法被正常 arm 一次。
|
|
259
|
-
if (armedTaskId !== undefined)
|
|
359
|
+
if (armedTaskId !== undefined) {
|
|
260
360
|
armedPlanReviewTaskIds.delete(armedTaskId);
|
|
361
|
+
offerModeAfterByTask.delete(armedTaskId);
|
|
362
|
+
}
|
|
261
363
|
hostLog('debug', `planReviewWire: arm failed (fail-soft): ${String(e)}`);
|
|
262
364
|
return false;
|
|
263
365
|
}
|
|
@@ -267,7 +369,13 @@ export function armPlanReviewApproval(result, sessionKey) {
|
|
|
267
369
|
* plan_review` 同 body {decision})——raw fetch 退役。错误模型:非 2xx 抛 APIError(message =
|
|
268
370
|
* server body.error 原文)→ 同款「HTTP <status> <error>」outcome 文案;网络失败走原「could not
|
|
269
371
|
* reach the engine」臂。 */
|
|
270
|
-
export async function decidePlanReview(taskId, decision
|
|
372
|
+
export async function decidePlanReview(taskId, decision,
|
|
373
|
+
/**
|
|
374
|
+
* 0.72.13 CC-46(engine ≥7.86.0;契约 §4c):批准之后用哪一档。缺席 ⇒ 体逐字节同旧 `{decision}`。只配 approve、闭集两词 ——
|
|
375
|
+
* 坏形本地拒(零 POST,outcome 如实说没送出去,不回显值)。引擎答 400 `request.field_conflict`(这条任务不是只读起步):
|
|
376
|
+
* `default` 与缺席同义 ⇒ 去键重发恰一次;`acceptEdits` **不**重发(不静默降成逐次征询),outcome 说批准没生效。
|
|
377
|
+
*/
|
|
378
|
+
permissionModeAfter) {
|
|
271
379
|
// REF-CC-025:此前 `cfg`/`client` 缺席各自 `return` 静默——卡已经对用户回了「收到」
|
|
272
380
|
// (armPlanReviewApproval 的 responder 早就 `return {ok:true}` 过了),但决断本身连一次网络
|
|
273
381
|
// 请求都没发出去,而模型/用户没有任何回程信号。两条早退现在都汇进同一条 outcome 管道(下方
|
|
@@ -276,7 +384,12 @@ export async function decidePlanReview(taskId, decision) {
|
|
|
276
384
|
// 🔴 design/285 批 3 显式裁定:本处与 `armPlanReviewApproval` 同一笔豁免(理由逐字见那边)——
|
|
277
385
|
// 决断是从 arm 立的那张卡的 responder 回调进来的,槽键必须与立卡时同源,单换这一跳即两头不一致。
|
|
278
386
|
const cfg = engineWireTarget();
|
|
279
|
-
|
|
387
|
+
const badMode = permissionModeAfter !== undefined && (decision !== 'approve' || !isPlanReviewModeAfter(permissionModeAfter));
|
|
388
|
+
if (badMode) {
|
|
389
|
+
hostLog('error', `planReviewWire: ${decision} NOT sent — permissionModeAfter is only valid with approve and must be "default" or "acceptEdits"`);
|
|
390
|
+
outcome = `The plan_review ${decision} could NOT be sent: the post-approval permission mode given with it is not valid for this decision (it only applies to an approval, and must be "default" or "acceptEdits"). Tell the user plainly that the decision did not go through; the plan is still waiting.`;
|
|
391
|
+
}
|
|
392
|
+
else if (!cfg) {
|
|
280
393
|
hostLog('error', `planReviewWire: ${decision} NOT sent — engineWireTarget() unavailable`);
|
|
281
394
|
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.`;
|
|
282
395
|
}
|
|
@@ -293,7 +406,27 @@ export async function decidePlanReview(taskId, decision) {
|
|
|
293
406
|
}
|
|
294
407
|
else {
|
|
295
408
|
try {
|
|
296
|
-
|
|
409
|
+
// sdk 9.6.0 的 PlanReviewRequest 未声明 permissionModeAfter ⇒ 交集型加一可选位;缺席 = 键不落(体逐字节同旧)。
|
|
410
|
+
const mode = isPlanReviewModeAfter(permissionModeAfter) ? permissionModeAfter : undefined;
|
|
411
|
+
const reqBody = { decision };
|
|
412
|
+
if (mode !== undefined)
|
|
413
|
+
reqBody.permissionModeAfter = mode;
|
|
414
|
+
let body;
|
|
415
|
+
try {
|
|
416
|
+
body = await client.assistant.planReview(taskId, reqBody);
|
|
417
|
+
}
|
|
418
|
+
catch (e1) {
|
|
419
|
+
// 400 request.field_conflict + 带了 default:default ≡ 缺席(契约:缺席 = 这一档)⇒ 去键重发恰一次,零语义损失。
|
|
420
|
+
// 被这道门拒掉的请求一个字节的状态都不动(契约),同一次批准可原样重试。acceptEdits 不走这里(见下方 catch)。
|
|
421
|
+
const conflict = e1;
|
|
422
|
+
if (mode === 'default' && conflict?.status === 400 && conflict?.errorCode === 'request.field_conflict') {
|
|
423
|
+
hostLog('debug', 'planReviewWire: permissionModeAfter "default" refused as not applicable — re-sending the approval without the key (same meaning)');
|
|
424
|
+
body = await client.assistant.planReview(taskId, { decision });
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
throw e1;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
297
430
|
hostLog('debug', `planReviewWire: ${decision} → ok ${JSON.stringify(body).slice(0, 200)}`);
|
|
298
431
|
// [2315]/[2316](#109,2026-08-02):decide 的 **2xx 不当终态** —— test 黑盒实测 reject 9/9
|
|
299
432
|
// 返回 200 而会话仍锁在同一 gate(core RB-471:重开兜底对 reject 腿恒真误触发)。这里回拉
|
|
@@ -341,7 +474,16 @@ export async function decidePlanReview(taskId, decision) {
|
|
|
341
474
|
catch (e) {
|
|
342
475
|
// APIError 判型走 status duck-check(sseIdleTriage 同款纪律:双包时 instanceof 会分叉)。
|
|
343
476
|
const status = e?.status;
|
|
344
|
-
|
|
477
|
+
const refusedCode = e?.errorCode;
|
|
478
|
+
if (status === 400 && refusedCode === 'request.field_conflict' && permissionModeAfter === 'acceptEdits') {
|
|
479
|
+
// CC-46:这条任务不是只读起步的(模型自选 plan 模式那种形)⇒ 引擎拒收「批准后自动接受编辑」。**不**静默降档重发:
|
|
480
|
+
// 用户选的是「不再逐次征询」,悄悄换成「逐次征询」= 替他改了决定。引擎一个字节的状态都没动,计划仍在等。
|
|
481
|
+
hostLog('debug', 'planReviewWire: approve + acceptEdits refused (request.field_conflict) — NOT re-sent with a different mode');
|
|
482
|
+
outcome =
|
|
483
|
+
'The plan approval was NOT applied: the engine refused "auto-accept edits" for this task (it was not started read-only in plan mode, so that option does not apply). ' +
|
|
484
|
+
'Nothing changed engine-side — the plan is still waiting for review. Tell the user plainly, and that approving with "manually approve edits" will go through.';
|
|
485
|
+
}
|
|
486
|
+
else if (typeof status === 'number') {
|
|
345
487
|
const msg = e instanceof Error ? e.message : String(e);
|
|
346
488
|
hostLog('debug', `planReviewWire: ${decision} → ${status} ${msg.slice(0, 200)}`);
|
|
347
489
|
outcome = `The plan_review decision failed: HTTP ${status} ${msg}`.trim();
|
|
@@ -450,14 +592,15 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
450
592
|
publishQuestionFrameFor(sessionKey, {
|
|
451
593
|
type: 'question',
|
|
452
594
|
questionId: canonicalId,
|
|
453
|
-
questions: planReviewQuestions(ARM_QUESTION),
|
|
595
|
+
questions: planReviewQuestions(ARM_QUESTION, offerModeAfterByTask.get(taskId) === true),
|
|
454
596
|
});
|
|
455
597
|
return settleOnReceipt(receipt, firstSight);
|
|
456
598
|
}
|
|
457
599
|
publishQuestionFrameFor(sessionKey, {
|
|
458
600
|
type: 'question',
|
|
459
601
|
questionId: canonicalId,
|
|
460
|
-
|
|
602
|
+
// 🔴 沿用 arm responder ⇒ 题面必须是**那张卡**的选项(三选 arm 后重绘成两选 = 屏上的 Yes 不在 responder 的读域里,点了不投递)。
|
|
603
|
+
questions: planReviewQuestions(ARM_QUESTION, offerModeAfterByTask.get(taskId) === true),
|
|
461
604
|
});
|
|
462
605
|
registerArmedGateFor(sessionKey, armedKey);
|
|
463
606
|
return { reopened: true, firstSight };
|
|
@@ -489,7 +632,8 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
489
632
|
if (activeReopenResponders.get(activeKey)?.questionId === questionId) {
|
|
490
633
|
activeReopenResponders.delete(activeKey);
|
|
491
634
|
}
|
|
492
|
-
|
|
635
|
+
// 本腿展示的恒是老两选 ⇒ 显式按两选标签校验(未展示的 auto / manual 标签 = 答的不是这张卡 ⇒ dismissed)。
|
|
636
|
+
const decided = planReviewDecisionFromAnswer(answer, LEGACY_TWO_CHOICE_LABELS);
|
|
493
637
|
if (decided === 'dismissed') {
|
|
494
638
|
// 三态(REF-CC-026):dismiss/读不出不驱动 decidePlanReview;呈现台账不清 ——
|
|
495
639
|
// 这张卡再被重开就是真「reopened」。
|
package/dist/index.d.ts
CHANGED
|
@@ -155,6 +155,7 @@ export * from './engineCapsCache.js';
|
|
|
155
155
|
export * from './sqlEngineCapability.js';
|
|
156
156
|
export * from './writeProtectionCapability.js';
|
|
157
157
|
export * from './webSearchBackendCapability.js';
|
|
158
|
+
export * from './executionLaneCapability.js';
|
|
158
159
|
export * from './mcpReconnect.js';
|
|
159
160
|
export * from './leaderConflict.js';
|
|
160
161
|
export * from './runTerminal.js';
|
package/dist/index.js
CHANGED
|
@@ -187,6 +187,9 @@ export * from './writeProtectionCapability.js';
|
|
|
187
187
|
// S-382(0.72.5;server ≥7.82.1,sdk 尚未声明):`Capabilities.webSearch.backend` 的四态窄读器 ——
|
|
188
188
|
// 部署默认 WebSearch 后端(`none` = 正面事实 / 键缺席 = 老引擎判不了,两者处置相反)。与上两只同构同纪律。
|
|
189
189
|
export * from './webSearchBackendCapability.js';
|
|
190
|
+
// 0.72.13 CC-54:`capabilities.executionLane`(server ≥7.86.0 S-426)四态读面 —— 键缺席 = 老 server 判不了(≠ 不是本机车道);
|
|
191
|
+
// 「工具是否跑在本机」判据归包(读到位用位,读不到原样用调用方旁证)。
|
|
192
|
+
export * from './executionLaneCapability.js';
|
|
190
193
|
// S-381 / core 7.22.0 #857(0.72.9 CC-30;server ≥7.85.0,sdk 尚未声明):会话内 MCP re-dial 的消费口 —— 200 三态按 outcome 分形读、
|
|
191
194
|
// unsupported 恒五键、名册按 toolNames 在场性、失败按 errorCode 分诊、「重连 = 一次交易」措辞单源;能力位 mcpReconnect 三态读。
|
|
192
195
|
export * from './mcpReconnect.js';
|
package/dist/notifications.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ⚠️ 调用方契约(交叉复审 [MED] 登记):cli 原版在渲染前有两条默认值回填——
|
|
3
|
-
* summary 缺省 ⇒ `Background task "${taskId}" ${status}`;status 非串 ⇒ 'completed'。
|
|
3
|
+
* summary 缺省 ⇒ `Background task "${taskId}" ${status}`;status 非串 ⇒ 'unknown'(0.72.12 起;此前 'completed')。
|
|
4
4
|
* 本纯函数不做回填(职责在 adapt() 输入侧),直接透传引擎原始 JSON 会渲出空 <summary>。
|
|
5
5
|
* 落 adapt() 时必须先补齐这两个字段再调本函数。
|
|
6
6
|
* 另注:<status> 段转义为**有意统一变更**(cli 原版曾不转义;2026-07-25 双侧统一为转义,
|
|
@@ -35,7 +35,7 @@ export declare function renderTaskNotificationXml(n: TaskNotificationFields): st
|
|
|
35
35
|
export declare function deriveNotificationResidualLines(n: Record<string, unknown>): string[];
|
|
36
36
|
/**
|
|
37
37
|
* 引擎 task_notification 原始 payload → renderTaskNotificationXml 的入参(**含上面注释里那两条
|
|
38
|
-
* 回填**:summary 缺省 ⇒ `Background task "<id>" <status>`;status 非串 ⇒ '
|
|
38
|
+
* 回填**:summary 缺省 ⇒ `Background task "<id>" <status>`;status 非串 ⇒ 'unknown',summary 改说 outcome unknown)。
|
|
39
39
|
* task_id 空串/缺失 ⇒ null(RB-75 同族:空 id 会塌成 ':status' 共享桶互吞,无 id 的通知本就无意义)。
|
|
40
40
|
* 字段名映射按 wire 真形:task_id/output_file/task_type/source 是 snake/wire 名,其余同名。
|
|
41
41
|
*/
|
package/dist/notifications.js
CHANGED
|
@@ -9,7 +9,7 @@ import { unrefTimer } from './unrefTimer.js';
|
|
|
9
9
|
import { clearEnginePanelTaskResident, publishEngineAgentPanelEvent, } from './engineAgentPanelStore.js';
|
|
10
10
|
// L-215②(0.65.0;core [6908]):「非成功终局」的**单铸谓词** —— 面板 settle 的 `isError` 位与
|
|
11
11
|
// 三端自己的后台白名单必须读同一个判据;内联一个 `=== 'failed' || === 'killed'` 就是第二份会漂的台账。
|
|
12
|
-
import { isTerminalNotSuccess, isTerminalStatus } from './runTerminal.js';
|
|
12
|
+
import { isTaskNotificationTerminalStatus, isTerminalNotSuccess, isTerminalStatus } from './runTerminal.js';
|
|
13
13
|
function escapeXml(v) {
|
|
14
14
|
return v.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
15
15
|
}
|
|
@@ -58,7 +58,7 @@ export function deriveNotificationResidualLines(n) {
|
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* 引擎 task_notification 原始 payload → renderTaskNotificationXml 的入参(**含上面注释里那两条
|
|
61
|
-
* 回填**:summary 缺省 ⇒ `Background task "<id>" <status>`;status 非串 ⇒ '
|
|
61
|
+
* 回填**:summary 缺省 ⇒ `Background task "<id>" <status>`;status 非串 ⇒ 'unknown',summary 改说 outcome unknown)。
|
|
62
62
|
* task_id 空串/缺失 ⇒ null(RB-75 同族:空 id 会塌成 ':status' 共享桶互吞,无 id 的通知本就无意义)。
|
|
63
63
|
* 字段名映射按 wire 真形:task_id/output_file/task_type/source 是 snake/wire 名,其余同名。
|
|
64
64
|
*/
|
|
@@ -66,7 +66,8 @@ export function normalizeTaskNotification(n) {
|
|
|
66
66
|
const taskId = typeof n.task_id === 'string' && n.task_id.length > 0 ? n.task_id : undefined;
|
|
67
67
|
if (taskId === undefined)
|
|
68
68
|
return null;
|
|
69
|
-
|
|
69
|
+
// 0.72.12 CC-50(#59 A-08):status 非串 ⇒ 'unknown'(此前 'completed',并用它编 summary 与去重键)。
|
|
70
|
+
const status = typeof n.status === 'string' ? n.status : 'unknown';
|
|
70
71
|
const lines = Array.isArray(n.lines)
|
|
71
72
|
? n.lines.filter((l) => typeof l === 'string')
|
|
72
73
|
: [];
|
|
@@ -76,7 +77,9 @@ export function normalizeTaskNotification(n) {
|
|
|
76
77
|
status,
|
|
77
78
|
summary: typeof n.summary === 'string' && n.summary.length > 0
|
|
78
79
|
? n.summary
|
|
79
|
-
:
|
|
80
|
+
: status === 'unknown'
|
|
81
|
+
? `Background task "${taskId}" reported without a status (outcome unknown)`
|
|
82
|
+
: `Background task "${taskId}" ${status}`,
|
|
80
83
|
...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
|
|
81
84
|
...(typeof n.toolUseId === 'string' && n.toolUseId.length > 0 ? { toolUseId: n.toolUseId } : {}),
|
|
82
85
|
...(typeof n.output_file === 'string' && n.output_file.length > 0
|
|
@@ -121,7 +124,8 @@ export function taskNotificationDedupKey(n) {
|
|
|
121
124
|
export function taskNotificationDedupKeyFromWire(n) {
|
|
122
125
|
return taskNotificationDedupKey({
|
|
123
126
|
taskId: typeof n.task_id === 'string' ? n.task_id : '',
|
|
124
|
-
|
|
127
|
+
// CC-50:与 normalizeTaskNotification 同判,缺席不与真 completed 同桶。
|
|
128
|
+
status: typeof n.status === 'string' ? n.status : 'unknown',
|
|
125
129
|
...(typeof n.seq === 'number' || typeof n.seq === 'string' ? { seq: n.seq } : {}),
|
|
126
130
|
...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
|
|
127
131
|
});
|
|
@@ -898,26 +902,37 @@ export function enqueueBgChildNotification(n) {
|
|
|
898
902
|
return;
|
|
899
903
|
}
|
|
900
904
|
bgNotifiedKeys.add(key);
|
|
901
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
905
|
+
// 🔴 完成三本账(完成去重 / 周期 / 完成卡)与面板 settle **只认通知自己的终态词**(completed / failed / killed / cancelled)。
|
|
906
|
+
// 非终态(running / queued / event / unknown / 未来词)只过上面那本**逐条**去重账:它若占了完成账,同一任务随后的
|
|
907
|
+
// 真终态会被跨通道去重吞掉 ⇒ 零 end、常驻标永不摘(异源对抗复审)。
|
|
908
|
+
const terminal = isTaskNotificationTerminalStatus(n.status);
|
|
909
|
+
if (terminal) {
|
|
910
|
+
// [2393] F-1:周期维必须跟着前进 —— 收摊臂删条目的唯一证据就是这一格。
|
|
911
|
+
markRunNotified(n.taskId, cycle);
|
|
912
|
+
cardEnqueuedRunIds.add(n.taskId);
|
|
913
|
+
}
|
|
904
914
|
// #6 通知-settle 边(合成半场):bg 子代行不再被 turn sweep 假结(session 常驻台账),真终态
|
|
905
915
|
// 唯二来源 = 推送帧(bridge task_notification 臂)与本合成链(probe/fleet bg_notification 收敛点)。
|
|
906
916
|
// 消费端 settle 幂等(running 才动),推送帧先到时此发布为 no-op。
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
917
|
+
if (terminal) {
|
|
918
|
+
try {
|
|
919
|
+
// 0.72.13(test 0.72.12 验收 ①;DS-06 同形存量):只对**通知自己的终态词**(completed / failed / killed / cancelled)settle 面板行。
|
|
920
|
+
// 此前对任何 status 都铸 `end`,而 `isError: isTerminalNotSuccess(status)` 对 running / queued / event / 未来词答 false ⇒
|
|
921
|
+
// 面板把一条还在跑 / 认不出状态的行结成 **completed** —— 该谓词的 JSDoc 明禁拿它的 false 当成功。非终态 ⇒ 不 settle、常驻标不摘。
|
|
922
|
+
clearEnginePanelTaskResident(n.taskId);
|
|
923
|
+
publishEngineAgentPanelEvent({
|
|
924
|
+
kind: 'end',
|
|
925
|
+
taskId: n.taskId,
|
|
926
|
+
// L-215②(0.65.0):读**单铸谓词**而不是内联两词 —— 修前这里只认 `failed`/`killed`,
|
|
927
|
+
// 而 core [6908] 的 `blocked` 是 **agent 自报的终态**(不是等人)⇒ 一条自报走不下去的
|
|
928
|
+
// 后台 run 在面板上被 settle 成**成功**。🔴 `suspended`/`needs_review` 仍不在表里
|
|
929
|
+
// (那两词是「等一次人的决定」,判成终局会把一条正等着你的 run 在面板上判死)。
|
|
930
|
+
isError: isTerminalNotSuccess(n.status),
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
/* fail-soft — 面板 settle 失败绝不挡模型通知注入 */
|
|
935
|
+
}
|
|
921
936
|
}
|
|
922
937
|
const message = `<${TASK_NOTIFICATION_TAG}>
|
|
923
938
|
<${TASK_ID_TAG}>${escapeXml(n.taskId)}</${TASK_ID_TAG}>
|
|
@@ -1050,7 +1065,15 @@ export function taskNotificationDeliverySupplement(objective) {
|
|
|
1050
1065
|
return `${SUPPLEMENT_LINE_PREFIX}completed — only this notification delivery failed. Check /tasks for the result.`;
|
|
1051
1066
|
}
|
|
1052
1067
|
const statusNote = status && status.length > 0 ? ` (status: ${status})` : '';
|
|
1053
|
-
|
|
1068
|
+
// 0.72.12 CC-50(异源对抗复审 R2 [medium]):只对**明确终态**说 already finished。此前这个尾模板对任何词都断言「已结束」,
|
|
1069
|
+
// 而同批 normalizeTaskNotification 开始对缺 status 的通知铸 `unknown` ⇒ 补充行重新造出终局确定性(DS-06:兜底位置落正向词,
|
|
1070
|
+
// 且模板表达式逃过 AST 五形)。缺席 / unknown / 未来词 / 活跃词 ⇒ 只陈述投递失败,不推断任务结局。
|
|
1071
|
+
// 终态词表对账**通知**自己的词(单源 `runTerminal.TASK_NOTIFICATION_TERMINAL_STATUSES`),不借 run 级 `isTerminalStatus`
|
|
1072
|
+
// (那张表没有 cancelled、多一个 blocked,语义不同)。
|
|
1073
|
+
if (isTaskNotificationTerminalStatus(status)) {
|
|
1074
|
+
return `${SUPPLEMENT_LINE_PREFIX}already finished${statusNote} — only this notification delivery failed. Check /tasks for the result.`;
|
|
1075
|
+
}
|
|
1076
|
+
return `${SUPPLEMENT_LINE_PREFIX}is in a state this client cannot confirm${statusNote} — this notification delivery failed and the task's outcome is unknown here. Check /tasks for its current state.`;
|
|
1054
1077
|
}
|
|
1055
1078
|
/**
|
|
1056
1079
|
* API-error 行文拆分(渲染层消费):尾部的 supplement 行(若在)分离出来渲 dim,main 保留原始
|
package/dist/runTerminal.d.ts
CHANGED
|
@@ -313,3 +313,12 @@ export type RunTerminalStatus = (typeof RUN_TERMINAL_STATUSES)[number];
|
|
|
313
313
|
* 🔴 表外词 ⇒ `false` = 「本端认不出这是个终局」,**不是**「它还在跑」;要判「在跑」请读正向证据。
|
|
314
314
|
*/
|
|
315
315
|
export declare function isTerminalStatus(status: unknown): status is RunTerminalStatus;
|
|
316
|
+
/**
|
|
317
|
+
* 0.72.12 CC-50:**通知**(`task_notification.status`)自己的终态词表 —— 引擎 `TaskNotificationStatus` 五词里的四个终态
|
|
318
|
+
* (`completed` / `failed` / `killed` / `cancelled`;第五词 `event` 是唯一非终态 = 仍在跑的 Monitor 批)逐字镜像。
|
|
319
|
+
* 🔴 它与 run 级 {@link RUN_TERMINAL_STATUSES} 是**两张表**(那张有 `blocked`、没有 `cancelled`):通知说「已取消」是明确终态,
|
|
320
|
+
* 借 run 级表会把它读成「认不出」(异源对抗复审抓出)。住在词表属主文件里,src 别处零内联(终态词汇门 D3)。
|
|
321
|
+
*/
|
|
322
|
+
export declare const TASK_NOTIFICATION_TERMINAL_STATUSES: readonly string[];
|
|
323
|
+
/** 这个通知 status 是不是**明确终态**;表外词(`event` / `unknown` / 未来词 / 缺席)⇒ false = 「本端不能断言它结束了」。 */
|
|
324
|
+
export declare function isTaskNotificationTerminalStatus(status: string | undefined): boolean;
|
package/dist/runTerminal.js
CHANGED
|
@@ -320,3 +320,14 @@ export const RUN_TERMINAL_STATUSES = Object.freeze([
|
|
|
320
320
|
export function isTerminalStatus(status) {
|
|
321
321
|
return typeof status === 'string' && RUN_TERMINAL_STATUSES.includes(status);
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* 0.72.12 CC-50:**通知**(`task_notification.status`)自己的终态词表 —— 引擎 `TaskNotificationStatus` 五词里的四个终态
|
|
325
|
+
* (`completed` / `failed` / `killed` / `cancelled`;第五词 `event` 是唯一非终态 = 仍在跑的 Monitor 批)逐字镜像。
|
|
326
|
+
* 🔴 它与 run 级 {@link RUN_TERMINAL_STATUSES} 是**两张表**(那张有 `blocked`、没有 `cancelled`):通知说「已取消」是明确终态,
|
|
327
|
+
* 借 run 级表会把它读成「认不出」(异源对抗复审抓出)。住在词表属主文件里,src 别处零内联(终态词汇门 D3)。
|
|
328
|
+
*/
|
|
329
|
+
export const TASK_NOTIFICATION_TERMINAL_STATUSES = Object.freeze(['completed', 'failed', 'killed', 'cancelled']);
|
|
330
|
+
/** 这个通知 status 是不是**明确终态**;表外词(`event` / `unknown` / 未来词 / 缺席)⇒ false = 「本端不能断言它结束了」。 */
|
|
331
|
+
export function isTaskNotificationTerminalStatus(status) {
|
|
332
|
+
return status !== undefined && TASK_NOTIFICATION_TERMINAL_STATUSES.includes(status);
|
|
333
|
+
}
|
package/dist/toolResult.js
CHANGED
|
@@ -471,7 +471,8 @@ export function wireOutputToBody(toolName, output, isError, truncated, flatten,
|
|
|
471
471
|
return wrap({
|
|
472
472
|
task_id: parsed.taskId ?? '',
|
|
473
473
|
task_type: 'local_agent',
|
|
474
|
-
|
|
474
|
+
// 0.72.12 CC-50(#59 A-06):解不出 ⇒ unknown,不编 completed。
|
|
475
|
+
status: parsed.status ?? 'unknown',
|
|
475
476
|
description: parsed.taskId ?? '',
|
|
476
477
|
output: parsed.output ?? '',
|
|
477
478
|
result: parsed.output ?? '',
|
|
@@ -824,19 +825,27 @@ modelText) {
|
|
|
824
825
|
const taskId = typeof s.task_id === 'string' ? s.task_id : undefined;
|
|
825
826
|
if (taskId === undefined)
|
|
826
827
|
return null;
|
|
827
|
-
const retrieval = s.retrieval_status === 'timeout' || s.retrieval_status === 'not_ready'
|
|
828
|
-
? s.retrieval_status
|
|
829
|
-
: 'success';
|
|
830
828
|
const taskType = typeof s.taskType === 'string' ? s.taskType : '';
|
|
831
829
|
const inner = s.details && typeof s.details === 'object' ? s.details : undefined;
|
|
832
830
|
const hasStatus = typeof s.status === 'string' && s.status.length > 0;
|
|
833
831
|
const hasContent = typeof s.content === 'string';
|
|
832
|
+
// 0.72.12 CC-50(#59 A-05 前半)。🔴 `retrieval_status` 是 **CC 同名键**(闭集 success / timeout / not_ready):老壳卡片
|
|
833
|
+
// success 才展示正文、not_ready 渲「still running」、timeout 渲超时 —— 闭集里没有「不知道」,绝不往里塞第四个词。
|
|
834
|
+
// 认不出 / 缺席 ⇒ `success`:正向证据 = 这份 structured detail 是一次**已返回**的取回调用的产物(引擎没说 timeout /
|
|
835
|
+
// not_ready 就是取回来了);「结局不知道」由下面的 `status:'unknown'` 承载,不由 retrieval 位承载。
|
|
836
|
+
// (异源对抗复审两轮:原词透传 ⇒ 已完成正文被藏;按 structured 两键推 not_ready ⇒ 漏了文本回落且渲成假「仍在运行」。
|
|
837
|
+
// 本行登记在 scripts/absence-fold-baseline.json 带 why。)
|
|
838
|
+
const rawRetrieval = typeof s.retrieval_status === 'string' ? s.retrieval_status : undefined;
|
|
839
|
+
const engineSaidNotReady = rawRetrieval === 'timeout' || rawRetrieval === 'not_ready';
|
|
840
|
+
const retrieval = engineSaidNotReady ? rawRetrieval : 'success';
|
|
834
841
|
// 🔴 惰性回落:structured 把 status 和正文都带齐了就**根本不解**模型面文本(T13 正则退位);
|
|
835
842
|
// 少哪一半就只为那一半解一次。判据锚在「这两个量还缺不缺」,不锚在「structured 在不在」。
|
|
836
843
|
const parsed = hasStatus && hasContent ? null : parseModelFacingTaskOutput(modelText ?? '');
|
|
844
|
+
// 0.72.12 CC-50(#59 A-05):retrieval success 只说「取到了」,不说「完成了」⇒ 无 status 落 unknown;
|
|
845
|
+
// 引擎**亲口**说 not_ready / timeout 才是「还没好」的正向证据 ⇒ running(本包按无证据推出来的 not_ready 不算)。
|
|
837
846
|
const status = hasStatus
|
|
838
847
|
? s.status
|
|
839
|
-
: (parsed?.status ?? (
|
|
848
|
+
: (parsed?.status ?? (engineSaidNotReady ? 'running' : 'unknown'));
|
|
840
849
|
// 'unknown' 分支类型上没有 output 字段(REF-CC-TYPESHAPE-14:不是「值恰好是 undefined」)——
|
|
841
850
|
// 显式按 kind 取,别对整个 union 裸访问不共有的属性。
|
|
842
851
|
const bodyText = hasContent ? s.content : parsed && parsed.kind !== 'unknown' ? parsed.output : undefined;
|
package/dist/workflow.js
CHANGED
|
@@ -69,7 +69,9 @@ export function parseWorkflowPollEnvelope(body) {
|
|
|
69
69
|
return null;
|
|
70
70
|
return {
|
|
71
71
|
taskId: j.task_id,
|
|
72
|
-
|
|
72
|
+
// 0.72.12 CC-50(#59 A-07):缺 status ⇒ 'unknown'。此前折成 'completed',而 'completed' ∈ WF_TERMINAL ⇒
|
|
73
|
+
// 消费点顺手 markEngineWorkflowNotified,把真完成通知的补发通道一起缴械。缺席不是终局。
|
|
74
|
+
status: typeof j.status === 'string' && j.status.length > 0 ? j.status : 'unknown',
|
|
73
75
|
...(typeof j.name === 'string' && j.name.length > 0 ? { name: j.name } : {}),
|
|
74
76
|
};
|
|
75
77
|
}
|