@sema-agent/client-core 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +238 -0
- package/README.md +1 -1
- package/dist/adapter/downstream/eventToSdkMessage.js +24 -0
- package/dist/attachmentsWireCaps.d.ts +43 -8
- package/dist/attachmentsWireCaps.js +64 -14
- package/dist/hitl/askGateWire.d.ts +4 -2
- package/dist/hitl/editedRuleTextPrecheck.d.ts +102 -0
- package/dist/hitl/editedRuleTextPrecheck.js +91 -0
- package/dist/hitl/hitlBridge.d.ts +17 -3
- package/dist/hitl/hitlBridge.js +20 -4
- package/dist/hitl/hitlHostSurface.d.ts +33 -3
- package/dist/hitl/hitlHostSurface.js +33 -3
- package/dist/hitl/parkResolver.js +15 -3
- package/dist/hitl/toolApprovalWire.d.ts +118 -2
- package/dist/hitl/toolApprovalWire.js +123 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/model/catalogLoader.js +151 -45
- package/dist/printToolResultFrame.d.ts +19 -0
- package/dist/seatContract.d.ts +25 -1
- package/dist/seatContract.js +29 -2
- package/docs/INTEGRATION-CLIENTS.md +123 -7
- package/package.json +2 -2
|
@@ -297,7 +297,9 @@ parkGatedCallId) {
|
|
|
297
297
|
case 'failed':
|
|
298
298
|
return { kind: 'failed', gatedCallId, reason: card.reason };
|
|
299
299
|
case 'aborted':
|
|
300
|
-
// turn 被中断(Esc/Ctrl+C)
|
|
300
|
+
// turn 被中断(Esc/Ctrl+C):把挂着的 ask 用一次 **TOOL 级 deny** 结算掉(server
|
|
301
|
+
// `ASSISTANT-WIRE-CONTRACT.md` §4a;0.42.0 撤稿 —— 原文写的是「cancel-by-deny(contract/04
|
|
302
|
+
// §2.4)」,两条前提均已作废,逐条见 `parkResolver.ts` 同臂的撤稿段)。REF-CC-023(2026-08-02):
|
|
301
303
|
// 有界观察(observeCancelByDeny)取代裸 `.catch(()=>{})` 全吞 —— 与 askGateWire.ts 同款
|
|
302
304
|
// reason 文案,同一次事故(件3,中断事故修复批 G,2026-07-15)的两条腿现在共用同一个观察器。
|
|
303
305
|
observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId, undefined, pending), taskId);
|
|
@@ -515,6 +517,88 @@ export function readToolApprovalRespondAck(v) {
|
|
|
515
517
|
...(typeof o.noteRecorded === 'boolean' ? { noteRecorded: o.noteRecorded } : {}),
|
|
516
518
|
};
|
|
517
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* 从 respond 抛出来的东西上读 {@link ToolApprovalRespondRefusal}。**永不抛、永不造**。
|
|
522
|
+
*
|
|
523
|
+
* 键位口径与 {@link import('../wireErrorTriage.js').classifyTurnWireError} 同源([2055] 死键
|
|
524
|
+
* 纪律:只认活键 `errorCode`,退役的 `code` 槽不做兼容)。
|
|
525
|
+
*
|
|
526
|
+
* @returns 三位**全缺席**时返回 `undefined` —— 一个三位皆空的 refusal 对象是「有拒句」的假象。
|
|
527
|
+
*
|
|
528
|
+
* 🔴 **文本取值序**(异源复审 [medium] 修):`err.message` 非空串 > 抛出物本身是非空串 >
|
|
529
|
+
* 受保护的 `String(err)`,且**只接受**真有内容的结果 —— `[object Object]` / `null` /
|
|
530
|
+
* `undefined` 三种占位串一律当作「读不出」。
|
|
531
|
+
* 🔴 **`String(err)` 用 try 包住**:抛出物可以自带 `Symbol.toPrimitive` / `toString` 钩子并在里面
|
|
532
|
+
* 抛错。本函数的唯一调用点在 `surfaceToolApprovalFrameAndRespond` 的 catch 里,那里的契约是
|
|
533
|
+
* 「respond 失败 ⇒ 返回 unresolved」;让一个不可信的转换钩子把这条收敛路径变成 reject,
|
|
534
|
+
* 等于给注入面开了一个「让整次审批消费抛出去」的口。
|
|
535
|
+
*/
|
|
536
|
+
export function readToolApprovalRespondRefusal(err) {
|
|
537
|
+
// 🔴 **取属性本身就可能抛**(异源复审 [medium] 的形比它自己说的还宽一档,施工时实撞):
|
|
538
|
+
// 抛出物可以是一个带 `get message() { throw }` 的对象 —— 连 `typeof e.message` 这一步都会炸,
|
|
539
|
+
// 根本走不到下面的转换。所以逐位读**各自**包 try,读不动就当那一位缺席。
|
|
540
|
+
const pick = (key) => {
|
|
541
|
+
try {
|
|
542
|
+
return err?.[key];
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const rawStatus = pick('status');
|
|
549
|
+
const rawCode = pick('errorCode');
|
|
550
|
+
const status = typeof rawStatus === 'number' && Number.isFinite(rawStatus) ? rawStatus : undefined;
|
|
551
|
+
const errorCode = typeof rawCode === 'string' && rawCode.length > 0 ? rawCode : undefined;
|
|
552
|
+
const message = refusalTextOf(err, pick('message'));
|
|
553
|
+
if (status === undefined && errorCode === undefined && message === undefined)
|
|
554
|
+
return undefined;
|
|
555
|
+
const rest = {
|
|
556
|
+
...(status !== undefined ? { status } : {}),
|
|
557
|
+
...(errorCode !== undefined ? { errorCode } : {}),
|
|
558
|
+
...(message !== undefined ? { message } : {}),
|
|
559
|
+
};
|
|
560
|
+
// 三选一联合的**构造侧证明**:哪一位在场就从哪一位收窄(与上面的 early-return 合起来,
|
|
561
|
+
// 「至少有一位」在编译期与运行期是同一条不变量,不是两份各自维护的承诺)。
|
|
562
|
+
if (status !== undefined)
|
|
563
|
+
return { ...rest, status };
|
|
564
|
+
if (errorCode !== undefined)
|
|
565
|
+
return { ...rest, errorCode };
|
|
566
|
+
return { ...rest, message: message };
|
|
567
|
+
}
|
|
568
|
+
/** 无信息量的占位串:`String()` 对这几类抛出物的产物,它们不是「拒句」。 */
|
|
569
|
+
const USELESS_REFUSAL_TEXTS = new Set(['[object Object]', 'null', 'undefined', '']);
|
|
570
|
+
/** 见 {@link readToolApprovalRespondRefusal} 的取值序与 try 保护。读不出 ⇒ `undefined`。 */
|
|
571
|
+
function refusalTextOf(err, message) {
|
|
572
|
+
if (typeof message === 'string' && message.length > 0)
|
|
573
|
+
return message;
|
|
574
|
+
if (typeof err === 'string' && err.length > 0)
|
|
575
|
+
return err;
|
|
576
|
+
let coerced;
|
|
577
|
+
try {
|
|
578
|
+
coerced = String(err);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// 不可信的 toString/Symbol.toPrimitive 钩子抛了 —— 诚实缺席,绝不让它逃出 catch。
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
return USELESS_REFUSAL_TEXTS.has(coerced) ? undefined : coerced;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* 机读码的**日志安全形**(异源复审 [medium] 采纳,0.42.0):`errorCode` 来自 UNTRUSTED 应答体且
|
|
588
|
+
* 无长度/控制字符/换行约束,把它原样插进宿主日志 = 可伪造日志行、可注入终端控制序列、还能把一段
|
|
589
|
+
* 服务端正文当「码」漏进 sink。
|
|
590
|
+
*
|
|
591
|
+
* 🔴 **只有形合的才照写**(真机读码的字符集):`[A-Za-z0-9._:-]{1,64}`。形不合 ⇒ 只报**长度**,
|
|
592
|
+
* 正文一个字节都不进日志(与本文件 note 那条「留痕只写元数据」逐字同纪律)。
|
|
593
|
+
* 🔴 这**不影响**交还调用方的那一份:{@link ToolApprovalRespondRefusal.errorCode} 仍是**原样**
|
|
594
|
+
* ——净化的是日志这条外溢面,不是契约。
|
|
595
|
+
*/
|
|
596
|
+
const MACHINE_CODE_SHAPE = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
597
|
+
function logSafeErrorCode(code) {
|
|
598
|
+
if (code === undefined)
|
|
599
|
+
return 'none';
|
|
600
|
+
return MACHINE_CODE_SHAPE.test(code) ? code : `<unprintable len=${code.length}>`;
|
|
601
|
+
}
|
|
518
602
|
/** 结构性识别流上的 tool_approval 帧(named SSE frame,payload.type === 帧名)。
|
|
519
603
|
* ⚠️ 口径更正(2026-08-08):此处原写「非 AgentEvent arm」—— SDK #185a 起这两个帧**是**
|
|
520
604
|
* `AgentEvent` 的臂了(durable 腿也回放),所以结构识别与 union 收窄两条路都成立;本函数仍按
|
|
@@ -750,9 +834,33 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
750
834
|
// 按表拒(rule_not_offered),这道核对把坏卡口实现挡在包边界并留痕(丢键不丢决断:人的
|
|
751
835
|
// allow/deny 已经落定,一条对不上的规则文本不该反过来污染整次回决)。deny 恒不带(server
|
|
752
836
|
// 宽收后忽略,发了只留「像是记住了」的错觉)。
|
|
837
|
+
//
|
|
838
|
+
// ── #225 编辑臂(0.42.0,[5071] wire 形):自由文本时**表核让位**,但能力位缺席就别发 ────────
|
|
839
|
+
// 🔴 让位的判据锚在**出身声明** `card.persistRuleEdited === true`,不锚「文本恰好不在表里」——
|
|
840
|
+
// 后者会让一个坏卡口实现(拼错了候选原文)自动升级成「自由文本臂」,把这道核对彻底架空。
|
|
841
|
+
// 🔴 让位的**理由**:表核的全部意义是「绝不把没被 offer 过的文本送进规则店」,而编辑臂的定义
|
|
842
|
+
// 就是人自己打的文本 —— 对它做表核,等于把这条 wire 上刚开出来的能力在包边界原样关掉
|
|
843
|
+
// ([5111] 探针实测的闭集三处之②:自由文本恒丢键)。真判官在引擎侧同一函数体
|
|
844
|
+
// (core `confirmRuleApproval`),不在这里。
|
|
845
|
+
// 🔴 能力位闸(`lane.respondFreeFormRulesCapable`)与 note 那条**同一条纪律**:位缺席就别发。
|
|
846
|
+
// 编辑臂在老 server 上会诚实降级 `rule_not_offered`(不是静默 200),但那仍然是一次
|
|
847
|
+
// 「人以为存上了、其实拿回一个拒绝」的往返 —— 能提前知道这台不供,就别把人的编辑送出去。
|
|
753
848
|
let persistRule;
|
|
849
|
+
let persistRuleEdited;
|
|
754
850
|
if (card.kind === 'allow' && typeof card.persistRule === 'string' && card.persistRule !== '' && decision !== 'deny') {
|
|
755
|
-
|
|
851
|
+
const edited = card.persistRuleEdited === true;
|
|
852
|
+
if (edited) {
|
|
853
|
+
if (lane?.respondFreeFormRulesCapable === true) {
|
|
854
|
+
persistRule = card.persistRule;
|
|
855
|
+
persistRuleEdited = true;
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
hostLog('debug', `liveToolApprovalWire: DROPPING edited persistRule for ${frame.approvalId} (len=${card.persistRule.length}) — the ` +
|
|
859
|
+
'respondFreeFormRules capability is not confirmed for this engine, so the free-form arm is not sent at all ' +
|
|
860
|
+
'(the decision itself still goes through unchanged)');
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
else if (ruleSuggestions?.some(sug => sug.rule === card.persistRule) === true) {
|
|
756
864
|
persistRule = card.persistRule;
|
|
757
865
|
}
|
|
758
866
|
else {
|
|
@@ -763,6 +871,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
763
871
|
...(signal && !signal.aborted ? { signal } : {}),
|
|
764
872
|
...(card.kind === 'allow' && card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}),
|
|
765
873
|
...(persistRule !== undefined ? { persistRule } : {}),
|
|
874
|
+
...(persistRuleEdited !== undefined ? { persistRuleEdited } : {}),
|
|
766
875
|
...(note !== undefined ? { note } : {}),
|
|
767
876
|
});
|
|
768
877
|
// 🔴 相关性门(对抗复审二轮):ack 必须是**这一次**审批的回执 —— id 与决断词都要对上。
|
|
@@ -801,7 +910,17 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
801
910
|
return ack !== undefined ? { decision, ack } : { decision };
|
|
802
911
|
}
|
|
803
912
|
catch (e) {
|
|
804
|
-
|
|
805
|
-
|
|
913
|
+
// #225 件5(0.42.0):**原文交还调用方**,不再只留一行 debug 就把它吞掉。
|
|
914
|
+
// 🔴 `decision:'unresolved'` 逐字保留(respond 没落定这件事没变);新增的只是 `respondRefusal`
|
|
915
|
+
// 这一格诊断面 —— 宿主拿它去渲「你的规则被拒了,引擎说:…」,而不是把一次响亮 400 呈成
|
|
916
|
+
// 「什么也没发生」([5111] 探针实测的闭集三处之③)。
|
|
917
|
+
// 🔴 留痕仍只写**元数据**(状态码 + 码 + 长度),正文不进日志:它是 UNTRUSTED 的应答体文本,
|
|
918
|
+
// 回喂宿主 sink 零诊断增量、还是外溢面(与 note 那条同一条纪律)。
|
|
919
|
+
const respondRefusal = readToolApprovalRespondRefusal(e);
|
|
920
|
+
hostLog('debug', `liveToolApprovalWire: respond(${decision}) failed for ${frame.approvalId} ` +
|
|
921
|
+
`(status=${respondRefusal?.status ?? 'none'} errorCode=${logSafeErrorCode(respondRefusal?.errorCode)} ` +
|
|
922
|
+
`messageLen=${respondRefusal?.message?.length ?? 0}) — engine self-settles (TTL/abort); ` +
|
|
923
|
+
'the refusal text is handed back on outcome.respondRefusal for the host to surface');
|
|
924
|
+
return respondRefusal !== undefined ? { decision: 'unresolved', respondRefusal } : { decision: 'unresolved' };
|
|
806
925
|
}
|
|
807
926
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -224,6 +224,7 @@ export { toolEndOutputText, ENGINE_ABORT_TOOL_RESULT } from './hitl/frameRouter.
|
|
|
224
224
|
export { isAskTool } from './hitl/frameRouter.js';
|
|
225
225
|
export * from './hitl/hitlHostSurface.js';
|
|
226
226
|
export * from './hitl/toolApprovalWire.js';
|
|
227
|
+
export * from './hitl/editedRuleTextPrecheck.js';
|
|
227
228
|
export * from './hitl/askGateWire.js';
|
|
228
229
|
export * from './hitl/planReviewWire.js';
|
|
229
230
|
export * from './hitl/gateIdentity.js';
|
package/dist/index.js
CHANGED
|
@@ -326,6 +326,13 @@ export { toolEndOutputText, ENGINE_ABORT_TOOL_RESULT } from './hitl/frameRouter.
|
|
|
326
326
|
export { isAskTool } from './hitl/frameRouter.js';
|
|
327
327
|
export * from './hitl/hitlHostSurface.js';
|
|
328
328
|
export * from './hitl/toolApprovalWire.js';
|
|
329
|
+
// ── #225 编辑臂预检的转出口([5076] 自领件,0.42.0)————————————————————————————————————
|
|
330
|
+
// core 5.57.0 导出了 `precheckEditedRuleText`(与 `confirmRuleApproval` **同一函数体**的 text×command
|
|
331
|
+
// 三步门)。本包**不能**做 value 级 re-export —— `@sema-agent/core` 的 barrel 值级拉 `node:crypto`/
|
|
332
|
+
// `node:fs`/`node:path`,而 portability 门的 `EXPECTED_PACKAGES_INDEX` 是等值门 + ③ 段拿 esbuild
|
|
333
|
+
// `--platform=browser` 真打包,一条这样的边会把整台引擎焊进 web/desktop 的产物。
|
|
334
|
+
// ⇒ 转出口取「类型面 + 注入口 + 诚实缺席读口」三件(逐条理由见模块头注)。
|
|
335
|
+
export * from './hitl/editedRuleTextPrecheck.js';
|
|
329
336
|
export * from './hitl/askGateWire.js';
|
|
330
337
|
export * from './hitl/planReviewWire.js';
|
|
331
338
|
// ── A-028.2/.3(#244 族A,2026-08-12):决断卡链的键词汇/呈现台账/归属判据三件 ————————————
|
|
@@ -234,10 +234,41 @@ export async function loadCatalogWithSources(opts) {
|
|
|
234
234
|
const timeoutMs = opts?.timeoutMs ?? DEFAULT_CATALOG_TIMEOUT_MS;
|
|
235
235
|
const attempts = [];
|
|
236
236
|
const warnings = [];
|
|
237
|
-
let hit = null;
|
|
238
237
|
let lastDialed;
|
|
238
|
+
// ── 委托:载荷判决 / 三层合并 / 来源标注全在 resolveModelCatalog ──────────────────────────
|
|
239
|
+
// 传给它的 `fetchJson` 只是「把已经拿到的这一份交出去」的闭包 —— 候选链与传输门是本文件的活,
|
|
240
|
+
// 校验与合并是它的活,两边不重叠也不互相重写。
|
|
241
|
+
const resolveWith = async (p) => {
|
|
242
|
+
const onlineUrl = p?.url ?? lastDialed ?? sources[0];
|
|
243
|
+
const resolveOpts = {
|
|
244
|
+
...(onlineUrl !== undefined ? { onlineUrl } : {}),
|
|
245
|
+
fetchJson: async () => {
|
|
246
|
+
if (p === null)
|
|
247
|
+
throw new Error(`all ${sources.length} catalog source(s) failed; see attempts[]`);
|
|
248
|
+
return p.doc;
|
|
249
|
+
},
|
|
250
|
+
...(opts?.overrides !== undefined ? { overrides: opts.overrides } : {}),
|
|
251
|
+
...(opts?.nowMs !== undefined ? { nowMs: opts.nowMs } : {}),
|
|
252
|
+
};
|
|
253
|
+
return resolveModelCatalog(resolveOpts);
|
|
254
|
+
};
|
|
255
|
+
// ══ 🔴 [4982] 候选② 的**同形存量**(0.42.0 异源复审 [high] 采纳)══════════════════════════
|
|
256
|
+
//
|
|
257
|
+
// 病根与「缓存腿从没被问过」是**同一条**:候选链的停止判据锚在**传输层**(`hit !== null`)而不是
|
|
258
|
+
// **内容判决**。后果比缓存那一格更重 —— 首源发得下来但内容不合格时,`break` 让**后续健康源
|
|
259
|
+
// 永远不会被请求**:一次坏发布、或一个 CDN 的半更新窗,就能把整条后备链压掉。
|
|
260
|
+
// ⇒ 停止判据换成 `accepted !== null`(内容判决),逐源「传输 → 旁签 → **内容校验**」,
|
|
261
|
+
// 只有真被接受的那一份才停链。
|
|
262
|
+
//
|
|
263
|
+
// 🔴 `rejected` 是**诚实兜底**不是备选源:全链都被拒时,拿**第一份被拒的**去出结果,
|
|
264
|
+
// 这样 `online.reason` 说的是真话(`schema-version-unsupported`),而不是退成
|
|
265
|
+
// 「一个源都没连上」(`fetch-failed`)—— 那会把「内容坏了」谎报成「网络坏了」。
|
|
266
|
+
// 它**绝不**参与「用哪份目录」的竞争:被拒就是被拒,`online.ok === false` 时合并结果本就
|
|
267
|
+
// 回落内置表。
|
|
268
|
+
let accepted = null;
|
|
269
|
+
let rejected = null;
|
|
239
270
|
for (const src of sources) {
|
|
240
|
-
if (
|
|
271
|
+
if (accepted !== null)
|
|
241
272
|
break;
|
|
242
273
|
const got = await guardedGet(src, allowedHosts, fetchImpl, timeoutMs);
|
|
243
274
|
if (got.outcome !== 'insecure-url' && got.outcome !== 'host-not-allowed')
|
|
@@ -289,7 +320,17 @@ export async function loadCatalogWithSources(opts) {
|
|
|
289
320
|
warnings.push(`catalog sha256 sidecar unavailable for ${finalUrl} (${shaGot.outcome}) — payload accepted without the checksum`);
|
|
290
321
|
}
|
|
291
322
|
attempts.push({ url: src, outcome: 'ok', ...(got.status !== undefined ? { status: got.status } : {}), shaChecked });
|
|
292
|
-
|
|
323
|
+
const candidate = { url: finalUrl, doc, raw: got.text, shaChecked };
|
|
324
|
+
// 内容判决就在这里做 —— 传输成功**不等于**这份目录能用。
|
|
325
|
+
const verdict = await resolveWith(candidate);
|
|
326
|
+
if (verdict.online.ok) {
|
|
327
|
+
accepted = { hit: candidate, base: verdict };
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (rejected === null)
|
|
331
|
+
rejected = { hit: candidate, base: verdict };
|
|
332
|
+
warnings.push(`catalog source ${finalUrl} was fetched but REJECTED by content validation ` +
|
|
333
|
+
`(${verdict.online.reason ?? 'unknown'}) — trying the next source`);
|
|
293
334
|
}
|
|
294
335
|
// ── 缓存腿(口缺席 ⇒ 整条不启用,cacheHit 键缺席)────────────────────────────────────────
|
|
295
336
|
const cache = opts?.cache;
|
|
@@ -297,7 +338,15 @@ export async function loadCatalogWithSources(opts) {
|
|
|
297
338
|
let cacheHit = cache !== undefined ? false : undefined;
|
|
298
339
|
let cacheStale;
|
|
299
340
|
let cachedSourceUrl;
|
|
300
|
-
|
|
341
|
+
/**
|
|
342
|
+
* 读一次缓存信封 → `SourceHit`(+ 陈旧判)。**永不抛**;坏形/缺件记 warn 返 null。
|
|
343
|
+
* 🔴 [4982] 候选②(0.42.0)提出来的:此前这段是内联的,只有「传输全败」那一条路走得到它。
|
|
344
|
+
* 现在有**两个**调用点(传输全败 / 传输 ok 但内容被拒),提成一处才不会有两份读法。
|
|
345
|
+
* `alreadyWarned` = 第二次调用时不重复堆同一批 warn(同一份坏缓存说两遍是噪音)。
|
|
346
|
+
*/
|
|
347
|
+
const readCachedHit = async (alreadyWarned) => {
|
|
348
|
+
if (cache === undefined || cachePath === undefined)
|
|
349
|
+
return null;
|
|
301
350
|
let text = null;
|
|
302
351
|
let readError;
|
|
303
352
|
try {
|
|
@@ -306,52 +355,109 @@ export async function loadCatalogWithSources(opts) {
|
|
|
306
355
|
catch (e) {
|
|
307
356
|
readError = shortError(e);
|
|
308
357
|
}
|
|
309
|
-
if (readError !== undefined)
|
|
358
|
+
if (readError !== undefined && !alreadyWarned)
|
|
310
359
|
warnings.push(`catalog cache read failed: ${readError}`);
|
|
311
|
-
if (typeof text
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
360
|
+
if (typeof text !== 'string' || text.length === 0)
|
|
361
|
+
return null;
|
|
362
|
+
let env;
|
|
363
|
+
let envError;
|
|
364
|
+
try {
|
|
365
|
+
env = JSON.parse(text);
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
envError = shortError(e);
|
|
369
|
+
}
|
|
370
|
+
if (envError !== undefined) {
|
|
371
|
+
if (!alreadyWarned)
|
|
321
372
|
warnings.push(`catalog cache is not parsable JSON (ignored): ${envError}`);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
cacheHit = true;
|
|
329
|
-
cachedSourceUrl = url;
|
|
330
|
-
if (opts?.nowMs !== undefined && typeof env?.fetchedAt === 'number') {
|
|
331
|
-
cacheStale = opts.nowMs - env.fetchedAt > CATALOG_CACHE_STALE_MS;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
else if (envError === undefined) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
const doc = env?.catalog;
|
|
376
|
+
const url = env?.sourceUrl;
|
|
377
|
+
if (doc === undefined || typeof url !== 'string' || url.length === 0) {
|
|
378
|
+
if (!alreadyWarned)
|
|
335
379
|
warnings.push('catalog cache envelope missing sourceUrl/catalog (ignored)');
|
|
336
|
-
|
|
380
|
+
return null;
|
|
337
381
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (payload === null)
|
|
348
|
-
throw new Error(`all ${sources.length} catalog source(s) failed; see attempts[]`);
|
|
349
|
-
return payload.doc;
|
|
350
|
-
},
|
|
351
|
-
...(opts?.overrides !== undefined ? { overrides: opts.overrides } : {}),
|
|
352
|
-
...(opts?.nowMs !== undefined ? { nowMs: opts.nowMs } : {}),
|
|
382
|
+
// 缓存里那份**就是**当初的线上载荷 —— 照样过 validateOnlineCatalog(下面同一条路),
|
|
383
|
+
// 不给它开后门:一份当年合法、今天已超区间的文档必须照样被拒。
|
|
384
|
+
const stale = opts?.nowMs !== undefined && typeof env?.fetchedAt === 'number'
|
|
385
|
+
? opts.nowMs - env.fetchedAt > CATALOG_CACHE_STALE_MS
|
|
386
|
+
: undefined;
|
|
387
|
+
return {
|
|
388
|
+
hit: { url, doc, raw: text, shaChecked: false },
|
|
389
|
+
...(stale !== undefined ? { stale } : {}),
|
|
390
|
+
};
|
|
353
391
|
};
|
|
354
|
-
|
|
392
|
+
// ══ 缓存腿:**内容判决**缺席时才问它(传输全败 与 内容被拒 两条路共用这一格)═══════════════
|
|
393
|
+
//
|
|
394
|
+
// 🔴 [4982] 候选②(P0-KPI,0.42.0):此前这条腿的门槛是 `hit === null`,而 `hit` 只记录**传输层**
|
|
395
|
+
// 结果(200 + 旁签过就算 hit)。于是「线上目录发得下来、但内容被 validateOnlineCatalog 拒」
|
|
396
|
+
// 这条缝里,loader 认为「有 hit」⇒ **永不查缓存**(即便缓存里躺着一份上次真正被接受过的
|
|
397
|
+
// 合法目录),用户只拿到内置精简表;而 `cacheHit:false` 这个**诚实位反过来说谎** —— 它读起来
|
|
398
|
+
// 是「没有缓存」,真相是「有缓存,但从头到尾没人问过它」。
|
|
399
|
+
// 判据锚在**真正决定结果的量**上:决定「用不用得上目录」的是**内容判决**(`accepted`),
|
|
400
|
+
// 不是传输判决(`hit !== null`)。异源复审 [high] 之后,候选链的停止判据也换成了同一个量
|
|
401
|
+
// (见上方 `accepted` 头注)—— 两处同根同治,不留同形存量。
|
|
402
|
+
// 🔴 缓存那份**不开后门**:它照样过同一个 `resolveModelCatalog`。也被拒时**不拿它顶替**
|
|
403
|
+
// (一份同样不合格的文档顶替不了什么),`cacheHit` 诚实留在 `false` —— 本位的语义是
|
|
404
|
+
// 「这次的目录**是从缓存来的**」,不是「问过缓存」;抬成 true 会让「缓存救场了」与
|
|
405
|
+
// 「缓存也坏了」在读数上不可分。「问过但没用上」那件事由 warning 说。
|
|
406
|
+
/**
|
|
407
|
+
* 线上腿的**归因汇总**(异源复审 [medium] 采纳,0.42.0)。
|
|
408
|
+
*
|
|
409
|
+
* 🔴 修的是一句会指错方向的话:此前只要**任一**源产生 `rejected`,回落 warning 就写
|
|
410
|
+
* 「every online catalog source was fetched but REJECTED」。而其余源完全可能是网络失败、
|
|
411
|
+
* 畸形 JSON、sha 不匹配、或被域白名单挡下 —— 把一次 CDN/网络事故说成「目录发布损坏」,
|
|
412
|
+
* 运维会照着错误方向查,恢复时间平白变长。
|
|
413
|
+
* ⇒ 按 `attempts` 的**真实计数**分两类措辞:全部进过内容校验且全被拒 ⇒ 说 "every … REJECTED";
|
|
414
|
+
* 混合故障 ⇒ 逐类报数,并保留内容拒绝的 reason。
|
|
415
|
+
*/
|
|
416
|
+
const onlineLegSummary = () => {
|
|
417
|
+
const contentRejected = attempts.filter((a) => a.outcome === 'ok').length;
|
|
418
|
+
const otherFailures = attempts.length - contentRejected;
|
|
419
|
+
const reason = rejected?.base.online.reason ?? 'unknown';
|
|
420
|
+
if (otherFailures === 0 && contentRejected > 0) {
|
|
421
|
+
return `every online catalog source (${contentRejected}) was fetched but REJECTED by content validation (${reason})`;
|
|
422
|
+
}
|
|
423
|
+
return (`the online catalog leg failed in more than one way: ${contentRejected} source(s) were fetched but ` +
|
|
424
|
+
`REJECTED by content validation (${reason}), ${otherFailures} source(s) failed before content ` +
|
|
425
|
+
'validation (transport / integrity / policy — see attempts[])');
|
|
426
|
+
};
|
|
427
|
+
let cached = null;
|
|
428
|
+
if (accepted === null)
|
|
429
|
+
cached = await readCachedHit(false);
|
|
430
|
+
let payload;
|
|
431
|
+
let base;
|
|
432
|
+
if (accepted !== null) {
|
|
433
|
+
payload = accepted.hit;
|
|
434
|
+
base = accepted.base;
|
|
435
|
+
}
|
|
436
|
+
else if (cached !== null) {
|
|
437
|
+
const fromCache = await resolveWith(cached.hit);
|
|
438
|
+
if (fromCache.online.ok) {
|
|
439
|
+
payload = cached.hit;
|
|
440
|
+
base = fromCache;
|
|
441
|
+
cacheHit = true;
|
|
442
|
+
cachedSourceUrl = cached.hit.url;
|
|
443
|
+
if (cached.stale !== undefined)
|
|
444
|
+
cacheStale = cached.stale;
|
|
445
|
+
if (rejected !== null)
|
|
446
|
+
warnings.push(`${onlineLegSummary()} — fell back to the cached catalog (${cached.hit.url})`);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
// 缓存那份也不合格 ⇒ 诚实回落:`online` 报的仍是**线上腿自己**的判词(有被拒的那一份就用
|
|
450
|
+
// 它的,一个都没连上就是 fetch-failed),绝不拿缓存的判词冒充线上的。
|
|
451
|
+
payload = rejected?.hit ?? null;
|
|
452
|
+
base = rejected?.base ?? (await resolveWith(null));
|
|
453
|
+
warnings.push(`${onlineLegSummary()}, and the cached catalog was rejected by content validation too — ` +
|
|
454
|
+
'falling back to the built-in table');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
payload = rejected?.hit ?? null;
|
|
459
|
+
base = rejected?.base ?? (await resolveWith(null));
|
|
460
|
+
}
|
|
355
461
|
// 线上腿真的被接受了才写缓存(被 validateOnlineCatalog 拒掉的载荷绝不进缓存 ——
|
|
356
462
|
// 否则下一次断网时我们会把一份已知不合格的文档当兜底)。
|
|
357
463
|
if (base.online.ok && payload !== null && cacheHit !== true && cache !== undefined && cachePath !== undefined) {
|
|
@@ -86,6 +86,25 @@ export interface ToolEndResultArmLike {
|
|
|
86
86
|
settledBy?: unknown;
|
|
87
87
|
/** 见 {@link settledBy} —— 两键同批同源(#263 半场,0.30.8),合读、缺席不反推、转录不是认证。 */
|
|
88
88
|
approver?: unknown;
|
|
89
|
+
/**
|
|
90
|
+
* 这次 deny **是哪一条拒绝臂**(core ≥5.35 `AskDenyResolution`;A-D2 半场,0.42.0)——
|
|
91
|
+
* `human_refused` / `window_expired` / `no_approver` / `blanket_allow_refused` /
|
|
92
|
+
* `approver_unavailable` / `task_aborted` / `presentation_failed` / `approver_error` /
|
|
93
|
+
* `approver_contract`。
|
|
94
|
+
*
|
|
95
|
+
* 🔴 与 {@link settledBy} **合读而不互替**:settledBy 只说「这次等待是哪一**种**收场」,本位
|
|
96
|
+
* 分类的是**拒绝臂本身**。单锚 settledBy 会把「审批方违约」(approver 回了契约外的东西)
|
|
97
|
+
* 那一臂一起误收进「窗口走完了」。
|
|
98
|
+
* 🔴 **判据用正面匹配,不用「不等于」**(异源复审 [medium] 采纳,0.42.0):要判「审批窗口自己
|
|
99
|
+
* 走完了」,写 `settledBy === 'timeout' && resolution === 'window_expired'`。
|
|
100
|
+
* **绝不**写 `resolution !== 'approver_contract'` —— 那个不等式对**缺席**(旧引擎不发这一位)和
|
|
101
|
+
* 对**任何未来新码**都为真,于是「不知道是怎么拒的」会被折成「窗口自然结束」,正是本位存在要防的
|
|
102
|
+
* 那件事;两条正交轴在那种写法里被一个否定比较替代掉了。缺席与未知值一律进 `default` 分支。
|
|
103
|
+
* 🔴 **缺席不带语义,不许反推**:每一次真执行了的调用、每一次 policy/hook 直拒、以及
|
|
104
|
+
* durable/decide 腿的结算都不带这一位;缺席 ≠「不是拒绝」也 ≠「人拒的」。
|
|
105
|
+
* 🔴 本形同样是**防御性读形**(`unknown`):九词是引擎的闭集,消费方分支已知值 + 永远带 default。
|
|
106
|
+
*/
|
|
107
|
+
resolution?: unknown;
|
|
89
108
|
parentToolCallId?: unknown;
|
|
90
109
|
uuid?: unknown;
|
|
91
110
|
session_id?: unknown;
|
package/dist/seatContract.d.ts
CHANGED
|
@@ -64,8 +64,32 @@
|
|
|
64
64
|
* method exists (`start`/`stop`/`interrupt`/`sendMessage`/`onEvent`/`onToolPermissionRequest`/
|
|
65
65
|
* `respondToToolPermission` are all real CC LocalSessions methods — see the 854-channel census).
|
|
66
66
|
*
|
|
67
|
+
* ══ 🔴 证据等级标注:本文件的 CC 断言取自**桌面 unpack**,本地语料库不可复验 ════════════════
|
|
68
|
+
*
|
|
69
|
+
* (test [5087] 提出的「语料**种类**缺口」,cli [5088] 认领三件之①,随 0.42.0 落;判据形见下)
|
|
70
|
+
*
|
|
71
|
+
* 本文件里所有以「CC 如何如何」为形的断言 —— 上面那句「212 methods / 854-channel census /
|
|
72
|
+
* 方法名逐字保留」、下面 `checkLocalSessionEvent` 的「CC 的 `fQe` 逐字段对照」、以及一切引
|
|
73
|
+
* `.vite/build/index.chunk-*.js` 坐标的句子 —— **证据等级 = 桌面 unpack(CC Electron app 的
|
|
74
|
+
* `.vite/build` 产物),本地语料库不可复验**。
|
|
75
|
+
*
|
|
76
|
+
* 为什么这一条必须写在这里(而不是只写进台账):
|
|
77
|
+
* · 本仓手边可复验的参照语料**只覆盖终端 CLI 形态**的静态产物,**没有**桌面壳的 unpack 产物。
|
|
78
|
+
* 这不是「语料版本旧」,是**产物形态不对** —— 拿 CLI 形态的语料去 grep 桌面壳里的符号只会得到
|
|
79
|
+
* 零命中,而零命中在这里**既不能证真也不能证伪**;
|
|
80
|
+
* · 「CC 取证纪律」要求 CC 断言的合法证据只有两种:本机 claude 实测,或语料二进制直证。本文件
|
|
81
|
+
* 的这一批两种都够不着 ⇒ 断言**保留**(它当初是照着真产物写的),但**必须带着这个等级一起读**;
|
|
82
|
+
* · 🔴 **这段注释随 `dist/*.d.ts` 出包**,三端读到的就是它。只把等级写进不进 tarball 的台账,
|
|
83
|
+
* 消费端读到的仍是一句看起来可复验、实际不可复验的断言(与本文件 `workerBadge` 出身订正
|
|
84
|
+
* 同一条理由)。
|
|
85
|
+
*
|
|
86
|
+
* **对下游的判据**:凡要对这些断言做「CC 到底是不是这样」的复核,**别拿终端 CLI 形态的语料当反证**
|
|
87
|
+
* ——零命中是语料形态缺口的必然结果,不是断言错。真正的复核路只有一条:取一份 CC 桌面版
|
|
88
|
+
* unpack 产物再对照(已在册为独立小件,做成后参照语料库增桌面形态子目录)。
|
|
89
|
+
*
|
|
67
90
|
* Event payload contract: ported in FORM from CC's `onEvent` runtime validator `fQe`
|
|
68
|
-
* (unpack `.vite/build/index.chunk-CnWKsyE_.js:369773
|
|
91
|
+
* (unpack `.vite/build/index.chunk-CnWKsyE_.js:369773`;**证据等级:桌面 unpack,本地语料库
|
|
92
|
+
* 不可复验** —— 见上方标注段). The load-bearing property we keep
|
|
69
93
|
* byte-faithful ([1832] axiom 2, independently proven by CC's own code): on an otherwise
|
|
70
94
|
* fully-schema'd IPC surface, the `message`/`messages` fields (= CC session-vocabulary SDKMessage
|
|
71
95
|
* bodies, our transcript plane) are UNVALIDATED passthrough positions, while every other field is a
|
package/dist/seatContract.js
CHANGED
|
@@ -64,8 +64,32 @@
|
|
|
64
64
|
* method exists (`start`/`stop`/`interrupt`/`sendMessage`/`onEvent`/`onToolPermissionRequest`/
|
|
65
65
|
* `respondToToolPermission` are all real CC LocalSessions methods — see the 854-channel census).
|
|
66
66
|
*
|
|
67
|
+
* ══ 🔴 证据等级标注:本文件的 CC 断言取自**桌面 unpack**,本地语料库不可复验 ════════════════
|
|
68
|
+
*
|
|
69
|
+
* (test [5087] 提出的「语料**种类**缺口」,cli [5088] 认领三件之①,随 0.42.0 落;判据形见下)
|
|
70
|
+
*
|
|
71
|
+
* 本文件里所有以「CC 如何如何」为形的断言 —— 上面那句「212 methods / 854-channel census /
|
|
72
|
+
* 方法名逐字保留」、下面 `checkLocalSessionEvent` 的「CC 的 `fQe` 逐字段对照」、以及一切引
|
|
73
|
+
* `.vite/build/index.chunk-*.js` 坐标的句子 —— **证据等级 = 桌面 unpack(CC Electron app 的
|
|
74
|
+
* `.vite/build` 产物),本地语料库不可复验**。
|
|
75
|
+
*
|
|
76
|
+
* 为什么这一条必须写在这里(而不是只写进台账):
|
|
77
|
+
* · 本仓手边可复验的参照语料**只覆盖终端 CLI 形态**的静态产物,**没有**桌面壳的 unpack 产物。
|
|
78
|
+
* 这不是「语料版本旧」,是**产物形态不对** —— 拿 CLI 形态的语料去 grep 桌面壳里的符号只会得到
|
|
79
|
+
* 零命中,而零命中在这里**既不能证真也不能证伪**;
|
|
80
|
+
* · 「CC 取证纪律」要求 CC 断言的合法证据只有两种:本机 claude 实测,或语料二进制直证。本文件
|
|
81
|
+
* 的这一批两种都够不着 ⇒ 断言**保留**(它当初是照着真产物写的),但**必须带着这个等级一起读**;
|
|
82
|
+
* · 🔴 **这段注释随 `dist/*.d.ts` 出包**,三端读到的就是它。只把等级写进不进 tarball 的台账,
|
|
83
|
+
* 消费端读到的仍是一句看起来可复验、实际不可复验的断言(与本文件 `workerBadge` 出身订正
|
|
84
|
+
* 同一条理由)。
|
|
85
|
+
*
|
|
86
|
+
* **对下游的判据**:凡要对这些断言做「CC 到底是不是这样」的复核,**别拿终端 CLI 形态的语料当反证**
|
|
87
|
+
* ——零命中是语料形态缺口的必然结果,不是断言错。真正的复核路只有一条:取一份 CC 桌面版
|
|
88
|
+
* unpack 产物再对照(已在册为独立小件,做成后参照语料库增桌面形态子目录)。
|
|
89
|
+
*
|
|
67
90
|
* Event payload contract: ported in FORM from CC's `onEvent` runtime validator `fQe`
|
|
68
|
-
* (unpack `.vite/build/index.chunk-CnWKsyE_.js:369773
|
|
91
|
+
* (unpack `.vite/build/index.chunk-CnWKsyE_.js:369773`;**证据等级:桌面 unpack,本地语料库
|
|
92
|
+
* 不可复验** —— 见上方标注段). The load-bearing property we keep
|
|
69
93
|
* byte-faithful ([1832] axiom 2, independently proven by CC's own code): on an otherwise
|
|
70
94
|
* fully-schema'd IPC surface, the `message`/`messages` fields (= CC session-vocabulary SDKMessage
|
|
71
95
|
* bodies, our transcript plane) are UNVALIDATED passthrough positions, while every other field is a
|
|
@@ -370,7 +394,10 @@ const checkSeatChromeEnvelope = (v, field) => {
|
|
|
370
394
|
};
|
|
371
395
|
/**
|
|
372
396
|
* onEvent payload validator — line-for-line FORM port of CC's `fQe`
|
|
373
|
-
* (`.vite/build/index.chunk-CnWKsyE_.js:369773`)
|
|
397
|
+
* (`.vite/build/index.chunk-CnWKsyE_.js:369773`)。
|
|
398
|
+
* 🔴 **证据等级:桌面 unpack,本地语料库不可复验**(0.42.0 标注件,判据与复核路见文件头注的
|
|
399
|
+
* 「证据等级标注」段;下面每一条 `CC …` 对照句都受该等级约束)。
|
|
400
|
+
* Field-by-field mapping:
|
|
374
401
|
* - `type`/`sessionId`: required strings (same);`type` 另加闭集成员校验(REF-CC-065,词由本宿主铸);
|
|
375
402
|
* - `message`: CC's minified source reads `typeof e.message<"u"` inside a comma expression whose
|
|
376
403
|
* value is discarded — i.e. NO validation, a deliberate transcript-plane passthrough. Same here.
|