@shgroup/dsh-serenity-hooks 1.46.0 → 1.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dsh.plugin.json +1 -1
- package/lib/client.js +45 -34
- package/lib/cro-turns.d.ts +3 -3
- package/lib/deepseek-vision-patch.d.ts +1 -1
- package/lib/host/access.d.ts +11 -1
- package/lib/host/contract.d.ts +2 -2
- package/lib/index.d.ts +20 -0
- package/lib/index.js +738 -88
- package/lib/{trajectory-bound-BPWKkzJC.js → message-source-Boc-AbHs.js} +7 -1
- package/lib/message-source.d.ts +28 -0
- package/lib/opencode-provider.d.ts +1 -1
- package/lib/output-guard-seam.d.ts +9 -0
- package/lib/seams/context.d.ts +1 -1
- package/lib/{settings-section-CIuyoSOX.js → settings-section-DTJW4smh.js} +52 -66
- package/lib/settings-section.d.ts +84 -42
- package/lib/unattended-ops.d.ts +247 -0
- package/lib/unattended-seam.d.ts +49 -0
- package/lib/{wake-scheduler-DF-YWcX5.js → wake-scheduler-DdaRNofO.js} +10 -12
- package/package.json +52 -52
package/lib/index.js
CHANGED
|
@@ -2,13 +2,13 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
|
2
2
|
import { a as hostSettings, c as hostWebServer, i as hostSessions, n as hostInjected, o as hostSubagents, r as hostService, s as hostWeb, t as hostAgents } from "./access-fiehjxV6.js";
|
|
3
3
|
import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$1, f as readExclusiveTools, g as resolveInside, i as findGitRoot, l as pathInside, m as readTrajectorySkills, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readHandymanConfig, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-DG4Oc7I1.js";
|
|
4
4
|
import { i as cccRootForExec, n as agentCwdFor, o as listCccs, r as cccRootForCwd, t as NO_CCC_FROM_AGENT_CWD } from "./ccc-roots-FxWXuZPw.js";
|
|
5
|
-
import { C as
|
|
5
|
+
import { C as showSession, D as localFileStamp, E as isoLocal, S as setActiveSessionInfo, T as useSession, _ as parseSessionContextFromEvents, a as pruneMissingBindings, b as sessionEvents$1, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as readActiveSessionMd, w as summarize, x as sessionsRoot, y as resolveSessionByTitle } from "./message-source-Boc-AbHs.js";
|
|
6
6
|
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-CWMRa36W.js";
|
|
7
7
|
import { a as unregisterSkiffSession$1, i as skiffSessionSnapshot$1, n as skiffRoleFor$1, o as ACC_VERSION, r as skiffSessionInfo$1, t as registerSkiffSession$1 } from "./skiff-registry-BjIX_KD1.js";
|
|
8
8
|
import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-KqQA6pkr.js";
|
|
9
|
-
import { a as FACE_PORTS, c as MECHANISM_PORTS, i as ACP_HTTP_PORT, l as SKIFF_DEBUG_PORT, n as registerSettingsSection, o as GATEWAY_PORT, s as MAIN_WEB_PORT, t as readSimpleSettings, u as WEIXIN_SEND_PORT } from "./settings-section-
|
|
9
|
+
import { a as FACE_PORTS, c as MECHANISM_PORTS, i as ACP_HTTP_PORT, l as SKIFF_DEBUG_PORT, n as registerSettingsSection, o as GATEWAY_PORT, s as MAIN_WEB_PORT, t as readSimpleSettings, u as WEIXIN_SEND_PORT } from "./settings-section-DTJW4smh.js";
|
|
10
10
|
import { r as sessionsRootDir, t as hasSessionLogById } from "./session-cleanup-BtkSmry8.js";
|
|
11
|
-
import { a as registerDisposer, c as
|
|
11
|
+
import { a as registerDisposer, c as WAKE_CATCH_UP_MS, i as registerCroTurnTracking, l as addWake, n as sendToTrajectory, o as CRO_FILENAME, r as wakeSchedulerState, s as buildCroSnapshot, t as registerWakeScheduler, u as listWakes } from "./wake-scheduler-DdaRNofO.js";
|
|
12
12
|
import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-CtFYm53S.js";
|
|
13
13
|
import z from "@deepseek-ai/schemastery";
|
|
14
14
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -3444,10 +3444,6 @@ function registerTrajectorySkillSection(agent, root) {
|
|
|
3444
3444
|
}
|
|
3445
3445
|
//#endregion
|
|
3446
3446
|
//#region src/skiff-core.ts
|
|
3447
|
-
const PLUGIN_SOURCE$5 = {
|
|
3448
|
-
kind: "plugin",
|
|
3449
|
-
plugin: "dsh-serenity-hooks"
|
|
3450
|
-
};
|
|
3451
3447
|
const skiffAgents = /* @__PURE__ */ new Map();
|
|
3452
3448
|
/** 查 sessionId 的 Skiff 角色名(无 → null) */
|
|
3453
3449
|
function skiffRoleFor(sessionId) {
|
|
@@ -3857,7 +3853,7 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
|
|
|
3857
3853
|
type: "text",
|
|
3858
3854
|
text: question
|
|
3859
3855
|
}],
|
|
3860
|
-
source: PLUGIN_SOURCE
|
|
3856
|
+
source: PLUGIN_SOURCE
|
|
3861
3857
|
}));
|
|
3862
3858
|
await waitAgentIdle(ctx, agent);
|
|
3863
3859
|
const answer = lastAssistantText$2(agent);
|
|
@@ -4368,10 +4364,7 @@ async function runHandymanJob(ctx, opts) {
|
|
|
4368
4364
|
type: "text",
|
|
4369
4365
|
text: prompt
|
|
4370
4366
|
}],
|
|
4371
|
-
source:
|
|
4372
|
-
kind: "plugin",
|
|
4373
|
-
plugin: "dsh-serenity-hooks"
|
|
4374
|
-
}
|
|
4367
|
+
source: PLUGIN_SOURCE
|
|
4375
4368
|
}));
|
|
4376
4369
|
await waitAgentIdle(ctx, workerAgent);
|
|
4377
4370
|
lastResponse = lastAssistantText$1(workerAgent);
|
|
@@ -5485,6 +5478,445 @@ function createContainerAdminTool(ctx) {
|
|
|
5485
5478
|
});
|
|
5486
5479
|
}
|
|
5487
5480
|
//#endregion
|
|
5481
|
+
//#region src/unattended-ops.ts
|
|
5482
|
+
/**
|
|
5483
|
+
* unattended-ops.ts — 无人值守代理回复(unattended proxy)机制层(S142 **D77**,owner 2026-09-23 提案)
|
|
5484
|
+
*
|
|
5485
|
+
* ## 这东西用来干什么(白话)
|
|
5486
|
+
*
|
|
5487
|
+
* 用户发个任务就走开。现在模型干完一段就停下来等回话,用户不在 ⇒ **会话挂在那儿不动**。
|
|
5488
|
+
* 本机制 = 用户走开后,系统**代替用户说一句"继续"**:让它自己跑完;干完了用一个**验证码**报告;
|
|
5489
|
+
* 卡住了先自己想办法;真不能定的**写下来等用户回来**。
|
|
5490
|
+
*
|
|
5491
|
+
* ## 归属(D23:ACC 管机制与数据,CCC 管措辞与纪律)
|
|
5492
|
+
*
|
|
5493
|
+
* | 面 | 归谁 | 内容 |
|
|
5494
|
+
* |---|---|---|
|
|
5495
|
+
* | **机制** | 🔴 **ACC(本模块)** | 触发判定 / 验证码 / 四选一结构 / 轮上限 / 流水 |
|
|
5496
|
+
* | **不可代批的*具体内容*** | **CCC 纪律** | ACC 只给**通用下限**({@link UNATTENDED_BLOCKED_FLOOR}),CCC 可加严 |
|
|
5497
|
+
*
|
|
5498
|
+
* 🔵 下限之所以落在 ACC(而不是"完全交给 CCC"):**没有这张清单,"自行解决"=把所有把关点自动放行**。
|
|
5499
|
+
* 最终内容仍待 owner 一句(设计 §13-5)——本版先给**保守下限**,CCC 只能在其上加严、不得放宽。
|
|
5500
|
+
*
|
|
5501
|
+
* ## 🔴 核心原则:单一续驱者(single-driver)
|
|
5502
|
+
*
|
|
5503
|
+
* > **一个会话同一时刻只应有一个"续驱者":用户 / CRO / 自排绳 / 无人值守代理。**
|
|
5504
|
+
*
|
|
5505
|
+
* 口径(owner 2026-09-23 19:1x 裁决)= **窄口径:只排除「有 CRO 程序的轨迹」**。
|
|
5506
|
+
* 原「宽口径」(任何自排唤醒都排除)**作废** —— 它会让本机制**永远够不着靠续接绳活着的会话**,
|
|
5507
|
+
* 等于**放弃"代替旧机制"**(绳因此退化为**长周期保险丝**:6–12h 一条)。
|
|
5508
|
+
*
|
|
5509
|
+
* ## 🔴 铁律(与 `cro.ts` / `cro-log.ts` 同族,勿破)
|
|
5510
|
+
*
|
|
5511
|
+
* **本模块的每个导出函数都不抛错**:写盘 / 解析 / 裁剪失败一律返回结构化失败。
|
|
5512
|
+
* ⇒ 代理机制的任何问题**不影响 turn 收尾**、不影响既有链路(唤醒投递 / 输出守卫 / rebuild)。
|
|
5513
|
+
*
|
|
5514
|
+
* ## 🔴 为什么不构成第二真相源(勿删此论证)
|
|
5515
|
+
*
|
|
5516
|
+
* | 载体 | 谁写 | 主语 | 回答 |
|
|
5517
|
+
* |---|---|---|---|
|
|
5518
|
+
* | `<轨迹目录>/unattended-log.json`(**本模块**) | **ACC** | ACC | 「**我注入了什么、判了什么、放过没有**」 |
|
|
5519
|
+
* | `<轨迹目录>/SESSION.md` | **LLM(CCC)** | CCC | 「**这轮发生了什么、为什么这么定**」 |
|
|
5520
|
+
*
|
|
5521
|
+
* **内容不重叠**:本模块只记**机械事实**(轮次 / 验证码 / 判定理由码 / 是否放过),**不写散文**;
|
|
5522
|
+
* LLM 侧的散文与理由归 SESSION.md。完整审计 = 两者合看(本模块 = 注入面|SESSION.md = 判断面)。
|
|
5523
|
+
*
|
|
5524
|
+
* ## 🔴 验证码形态(禁用 4 位字母码)
|
|
5525
|
+
*
|
|
5526
|
+
* 注入文本**自带**"请回复 ABCD" ⇒ 模型**复述指令**即**假完成**(高概率,不是小概率)
|
|
5527
|
+
* ⇒ 用**每轮新生成**的 nonce + **包装 + 独占一行**(`[[UNATTENDED-DONE:<hex>]]`),
|
|
5528
|
+
* 明写「**旧码作废**」,校验**只认当前轮**(防上下文里的旧码复活)。
|
|
5529
|
+
* 先例 = `handyman` background 的 `SERENITY_HANDYMAN_DONE_<16hex>`(同族实现)。
|
|
5530
|
+
*/
|
|
5531
|
+
/** 完成码包装前缀(**独占一行**才有意义;见文件头"禁用 4 位字母码") */
|
|
5532
|
+
const UNATTENDED_DONE_PREFIX = "[[UNATTENDED-DONE:";
|
|
5533
|
+
/** 注入消息的**首行标记**(幂等可辨:模型与事后审计都能一眼看出这是代理消息,不是用户消息) */
|
|
5534
|
+
const UNATTENDED_PROXY_MARK = "[[UNATTENDED-PROXY v1]]";
|
|
5535
|
+
/** 🔴 合成消息**固定身份句**(owner 已裁:「形如用户消息,但首句自报身份与权限边界」) */
|
|
5536
|
+
const UNATTENDED_IDENTITY_LINE = "无人值守代理 · 非本人";
|
|
5537
|
+
/** 生成一枚新 nonce(**每轮一枚**;旧码作废由调用方在文本里声明) */
|
|
5538
|
+
function newNonce() {
|
|
5539
|
+
return randomBytes(8).toString("hex");
|
|
5540
|
+
}
|
|
5541
|
+
/** 完成码全文(注入文本展示用;校验走 {@link hasDoneToken}) */
|
|
5542
|
+
function doneToken(nonce) {
|
|
5543
|
+
return `${UNATTENDED_DONE_PREFIX}${nonce}]]`;
|
|
5544
|
+
}
|
|
5545
|
+
/**
|
|
5546
|
+
* 出现过的完成码 nonce 列表(按出现顺序、去重;**小写归一**)。
|
|
5547
|
+
* 容忍 8–64 位 hex(模型可能手抄多/少几个字符 ⇒ 那就不匹配,属预期:只认本轮那枚)。
|
|
5548
|
+
*/
|
|
5549
|
+
function parseDoneNonces(text) {
|
|
5550
|
+
if (typeof text !== "string" || text === "") return [];
|
|
5551
|
+
const re = /\[\[UNATTENDED-DONE:([0-9a-fA-F]{8,64})\]\]/g;
|
|
5552
|
+
const out = [];
|
|
5553
|
+
let m;
|
|
5554
|
+
while ((m = re.exec(text)) !== null) {
|
|
5555
|
+
const n = (m[1] ?? "").toLowerCase();
|
|
5556
|
+
if (n !== "" && !out.includes(n)) out.push(n);
|
|
5557
|
+
}
|
|
5558
|
+
return out;
|
|
5559
|
+
}
|
|
5560
|
+
/** 触及即停(写待办/交接,**不循环**)——**CCC 的纪律更严则从严** */
|
|
5561
|
+
const UNATTENDED_BLOCKED_FLOOR = [
|
|
5562
|
+
"发布 / 发版",
|
|
5563
|
+
"对外发送(邮件 / 微信 / 对外面输出)",
|
|
5564
|
+
"删除与不可逆操作(rm / 归档 / force push)",
|
|
5565
|
+
"凭据与权限变更",
|
|
5566
|
+
"花钱(外部 API 大额调用 / GPU 长任务)",
|
|
5567
|
+
"代用户拍板(替人做决定)"
|
|
5568
|
+
];
|
|
5569
|
+
/** 四选一正文({@link buildProxyMessage} 与 {@link buildFalseNotifyRebuke} 共用,避免两份措辞漂移) */
|
|
5570
|
+
function proxyBody(input) {
|
|
5571
|
+
return [
|
|
5572
|
+
`A) 已完成、且用户回来会满意 ⇒ 交码 + 附完成凭据(做了什么 / 怎么验的 / 产物路径)`,
|
|
5573
|
+
`B) 已成功向用户发出通知、要停下等他主动回来 ⇒ 交码 + 写明走了哪个通道(**必须真有发送记录**)`,
|
|
5574
|
+
`C) 需要确认但可自决 ⇒ 按「**最保守且可逆**」的选项继续做完(⚠️ **不要交码**,直接做)`,
|
|
5575
|
+
`D) 触及不可代批边界 ⇒ **立即停**,把问题写进待办 / 交接,然后交码`,
|
|
5576
|
+
``,
|
|
5577
|
+
`不可代批的**通用下限**(你所在 CCC 的纪律更严则从严):${(input.blocked ?? UNATTENDED_BLOCKED_FLOOR).join(" / ")}`,
|
|
5578
|
+
``,
|
|
5579
|
+
`交码 = **独占一行、逐字复制下面这一行**(旧码一律作废,只认本轮这个):`,
|
|
5580
|
+
``,
|
|
5581
|
+
` ${doneToken(input.nonce)}`,
|
|
5582
|
+
``,
|
|
5583
|
+
`禁止:把本条当真实用户的授权;禁止把本指令文本复述成回答;禁止什么都没做只回一个码;`,
|
|
5584
|
+
`禁止借机发起**新的长任务**(代理只推进"已在办"的事)。`
|
|
5585
|
+
].join("\n");
|
|
5586
|
+
}
|
|
5587
|
+
/** 组装一次代理注入消息(**形如用户消息,但首句自报身份与权限边界**) */
|
|
5588
|
+
function buildProxyMessage(input) {
|
|
5589
|
+
return [
|
|
5590
|
+
`${UNATTENDED_PROXY_MARK} round=${input.round}/${input.maxRounds} nonce=${input.nonce}`,
|
|
5591
|
+
`${UNATTENDED_IDENTITY_LINE} —— 这条消息是**系统代用户发的**,不是用户本人;我没有任何权限替你拍板。`,
|
|
5592
|
+
``,
|
|
5593
|
+
`用户不在场。你只能以下列**四条之一**收束本轮:`,
|
|
5594
|
+
``,
|
|
5595
|
+
proxyBody(input)
|
|
5596
|
+
].join("\n");
|
|
5597
|
+
}
|
|
5598
|
+
/**
|
|
5599
|
+
* 组装一次**假完成打回**消息(设计 §7 的机械核验分支:声称已通知用户,但本轮无发送记录)。
|
|
5600
|
+
*
|
|
5601
|
+
* ⚠️ 同样带**新 nonce**:它是一次"再来一轮",不是"放过"。
|
|
5602
|
+
*/
|
|
5603
|
+
function buildFalseNotifyRebuke(input) {
|
|
5604
|
+
return [
|
|
5605
|
+
`${UNATTENDED_PROXY_MARK} round=${input.round}/${input.maxRounds} nonce=${input.nonce}`,
|
|
5606
|
+
`${UNATTENDED_IDENTITY_LINE} —— 这条消息是**系统代用户发的**,不是用户本人。`,
|
|
5607
|
+
``,
|
|
5608
|
+
`🔴 你交的码是对的,但**你声称已通知用户,而我这边查不到任何发送记录** ⇒ **不算合法收束**。`,
|
|
5609
|
+
`要么**真的发**(走可用通道;发成之后再交码),要么改走 A / C / D。其余口径同下。`,
|
|
5610
|
+
``,
|
|
5611
|
+
proxyBody(input)
|
|
5612
|
+
].join("\n");
|
|
5613
|
+
}
|
|
5614
|
+
/**
|
|
5615
|
+
* 「收尾文本**声称**已联系用户」的**启发式**判据(设计 §7 的触发器,**不是**判据本身)。
|
|
5616
|
+
*
|
|
5617
|
+
* 🔴 机械判据 = **ACC 侧的发送记录**({@link hasOutboundSendSince});本函数只回答"**要不要去查记录**"。
|
|
5618
|
+
* 误判方向(明标):**误报**(模型换了个说法没被匹配到)⇒ 退回"只认码",不产生假放过;
|
|
5619
|
+
* **误报**(正常措辞被当成声称)⇒ 多打回一轮(有轮上限兜底)。⇒ 偏严,符合设计 §11
|
|
5620
|
+
* 把"谎称已发消息"列为失败模式的取向。
|
|
5621
|
+
*/
|
|
5622
|
+
const NOTIFY_CLAIM_PATTERNS = [
|
|
5623
|
+
/已(经)?(成功)?(向|给|和)?\s*(用户|您|你|他|她|本人|主人)?[^\n。]{0,16}(发送|发出|告知|通知|联系|留言|汇报)/,
|
|
5624
|
+
/\b(?:notified|informed|messaged|contacted)\s+(?:the\s+)?user\b/i,
|
|
5625
|
+
/\b(?:sent|sending)\s+(?:a\s+)?(?:message|notification|notice)\s+to\s+(?:the\s+)?user\b/i,
|
|
5626
|
+
/\bI\s+(?:have\s+)?(?:notified|informed|messaged|contacted)\b/i
|
|
5627
|
+
];
|
|
5628
|
+
/** 收尾文本是否**声称**已联系用户(见 {@link NOTIFY_CLAIM_PATTERNS} 的误判方向说明) */
|
|
5629
|
+
function claimsUserNotification(text) {
|
|
5630
|
+
if (typeof text !== "string" || text === "") return false;
|
|
5631
|
+
return NOTIFY_CLAIM_PATTERNS.some((re) => re.test(text));
|
|
5632
|
+
}
|
|
5633
|
+
/**
|
|
5634
|
+
* **不许开**的会话(设计 §3 的"建议写死",本版**确实写死**):
|
|
5635
|
+
* `skiff-`(F4 问答 / **微信桥会话也是这个前缀** —— 桥那侧的"用户"是**家人**,
|
|
5636
|
+
* 代理替家人说话 = **冒充**)| `acp-`(对外程序化面)| `handyman-`(自主 worker,已有自己的完成码循环)。
|
|
5637
|
+
*
|
|
5638
|
+
* ⚠️ **`rebuild-` 不在排除面**(与 `output-guard-seam.ts` 的取向**刻意不同**):重建会话是
|
|
5639
|
+
* **同一轨迹的载体**(内部维护面),不是对外面;把它排除会让"重建后就没人推了"。
|
|
5640
|
+
*/
|
|
5641
|
+
function isProxyExcludedSessionId(sessionId) {
|
|
5642
|
+
if (typeof sessionId !== "string" || sessionId === "") return true;
|
|
5643
|
+
return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("handyman-");
|
|
5644
|
+
}
|
|
5645
|
+
/**
|
|
5646
|
+
* 一次判定的**唯一入口**(纯函数)。
|
|
5647
|
+
*
|
|
5648
|
+
* 顺序即语义(R↓,勿随意调换):
|
|
5649
|
+
* 1. **闸/空间/会话资格**(关着、不在 CCC、非主会话、排除面)⇒ skip;
|
|
5650
|
+
* 2. **单一续驱者**:用户刚发起 ⇒ skip(人就在场);该轨迹有 CRO ⇒ skip(程序在驱动);
|
|
5651
|
+
* 3. **合法收束优先于轮上限** —— 最后一轮交的码必须算数(否则等于永远收不了尾);
|
|
5652
|
+
* 4. 交码 + 声称已通知 + **无发送记录** ⇒ 假完成,走下一轮(reason=`false-notify`);
|
|
5653
|
+
* 5. 墙钟 / 轮上限 ⇒ cap(**停下不动**,退回改造前的行为,不产生新风险);
|
|
5654
|
+
* 6. 其余 ⇒ inject 下一轮。
|
|
5655
|
+
*/
|
|
5656
|
+
function decideUnattendedAction(f) {
|
|
5657
|
+
if (!f.enabled) return {
|
|
5658
|
+
action: "skip",
|
|
5659
|
+
reason: "mode-off",
|
|
5660
|
+
round: 0
|
|
5661
|
+
};
|
|
5662
|
+
if (!f.root) return {
|
|
5663
|
+
action: "skip",
|
|
5664
|
+
reason: "not-in-ccc",
|
|
5665
|
+
round: 0
|
|
5666
|
+
};
|
|
5667
|
+
if (!f.isMainSession) return {
|
|
5668
|
+
action: "skip",
|
|
5669
|
+
reason: "not-main-session",
|
|
5670
|
+
round: 0
|
|
5671
|
+
};
|
|
5672
|
+
if (f.sessionIdExcluded) return {
|
|
5673
|
+
action: "skip",
|
|
5674
|
+
reason: "excluded-session",
|
|
5675
|
+
round: 0
|
|
5676
|
+
};
|
|
5677
|
+
if (f.userInitiated) return {
|
|
5678
|
+
action: "skip",
|
|
5679
|
+
reason: "user-initiated",
|
|
5680
|
+
round: 0
|
|
5681
|
+
};
|
|
5682
|
+
if (f.hasCroProgram) return {
|
|
5683
|
+
action: "skip",
|
|
5684
|
+
reason: "cro-programmed",
|
|
5685
|
+
round: 0
|
|
5686
|
+
};
|
|
5687
|
+
const run = f.run;
|
|
5688
|
+
if (run !== null && f.doneNonce !== null && f.doneNonce.toLowerCase() === run.nonce.toLowerCase() && run) {
|
|
5689
|
+
if (f.claimsNotify && !f.outboundSeen) {
|
|
5690
|
+
if (run.round >= 15) return {
|
|
5691
|
+
action: "cap",
|
|
5692
|
+
reason: "round-cap",
|
|
5693
|
+
round: run.round
|
|
5694
|
+
};
|
|
5695
|
+
return {
|
|
5696
|
+
action: "inject",
|
|
5697
|
+
reason: "false-notify",
|
|
5698
|
+
round: run.round + 1
|
|
5699
|
+
};
|
|
5700
|
+
}
|
|
5701
|
+
return {
|
|
5702
|
+
action: "accept",
|
|
5703
|
+
reason: "done-code",
|
|
5704
|
+
round: run.round
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5707
|
+
if (run && f.nowMs - run.startedAt > 72e5) return {
|
|
5708
|
+
action: "cap",
|
|
5709
|
+
reason: "wall-clock",
|
|
5710
|
+
round: run.round
|
|
5711
|
+
};
|
|
5712
|
+
if (run && run.round >= 15) return {
|
|
5713
|
+
action: "cap",
|
|
5714
|
+
reason: "round-cap",
|
|
5715
|
+
round: run.round
|
|
5716
|
+
};
|
|
5717
|
+
const round = (run?.round ?? 0) + 1;
|
|
5718
|
+
return {
|
|
5719
|
+
action: "inject",
|
|
5720
|
+
reason: run === null ? "first-stop" : f.doneNonce !== null ? "stale-code" : "no-legal-closure",
|
|
5721
|
+
round
|
|
5722
|
+
};
|
|
5723
|
+
}
|
|
5724
|
+
/** 键 = 载体(dsh 会话 id)——与 `cro-turns.ts` 同款选择(事件本身就以 agent/session 为主语) */
|
|
5725
|
+
const runs = /* @__PURE__ */ new Map();
|
|
5726
|
+
/** 取该会话的代理段(无 ⇒ null) */
|
|
5727
|
+
function getProxyRun(sessionId) {
|
|
5728
|
+
return runs.get(sessionId) ?? null;
|
|
5729
|
+
}
|
|
5730
|
+
/** 开始 / 推进一段代理(`startedAt` 由调用方给出 ⇒ 跨轮保留首次值) */
|
|
5731
|
+
function setProxyRun(sessionId, run) {
|
|
5732
|
+
if (!sessionId) return;
|
|
5733
|
+
runs.set(sessionId, run);
|
|
5734
|
+
}
|
|
5735
|
+
/** 结束该会话的代理段(放过 / 上限 / 用户回来 / 载体销毁) */
|
|
5736
|
+
function clearProxyRun(sessionId) {
|
|
5737
|
+
if (!sessionId) return;
|
|
5738
|
+
runs.delete(sessionId);
|
|
5739
|
+
}
|
|
5740
|
+
/**
|
|
5741
|
+
* 「该载体最近一次由**真实用户**发起的轮次号」(会话 id → turn)。
|
|
5742
|
+
*
|
|
5743
|
+
* ⚠️ 只留**一条**(不是集合):`claimed` 的 `turn` 单调递增,留最新一条即可回答
|
|
5744
|
+
* "本轮是不是用户发起的",且**表的大小 = 会话数**(本容器栽过 5 次"只增不清",此处结构性避免)。
|
|
5745
|
+
*/
|
|
5746
|
+
const userTurns = /* @__PURE__ */ new Map();
|
|
5747
|
+
/** 记下"这个载体的这一轮是真实用户发起的" */
|
|
5748
|
+
function noteUserTurn(sessionId, turn) {
|
|
5749
|
+
if (!sessionId || typeof turn !== "number") return;
|
|
5750
|
+
userTurns.set(sessionId, turn);
|
|
5751
|
+
}
|
|
5752
|
+
/** 该载体的这一轮是否由真实用户发起(判据 = `noteUserTurn` 写入的最新一条) */
|
|
5753
|
+
function isUserTurn(sessionId, turn) {
|
|
5754
|
+
if (!sessionId || typeof turn !== "number") return false;
|
|
5755
|
+
return userTurns.get(sessionId) === turn;
|
|
5756
|
+
}
|
|
5757
|
+
/** 清掉该载体的用户轮次记录(载体销毁时) */
|
|
5758
|
+
function clearUserTurn(sessionId) {
|
|
5759
|
+
if (!sessionId) return;
|
|
5760
|
+
userTurns.delete(sessionId);
|
|
5761
|
+
}
|
|
5762
|
+
/** 清空全部进程内运行态(disposer;**不碰磁盘**) */
|
|
5763
|
+
function clearUnattendedState() {
|
|
5764
|
+
runs.clear();
|
|
5765
|
+
userTurns.clear();
|
|
5766
|
+
outboundSends.clear();
|
|
5767
|
+
}
|
|
5768
|
+
/** 发送记录保留窗(超出即丢:它只服务"这一轮到底发没发"这个瞬时问题) */
|
|
5769
|
+
const UNATTENDED_OUTBOUND_TTL_MS = 216e5;
|
|
5770
|
+
const outboundSends = /* @__PURE__ */ new Map();
|
|
5771
|
+
/**
|
|
5772
|
+
* 记一次**成功的**出站发送(`im-bridge` 工具面在通道返回成功后调用)。
|
|
5773
|
+
*
|
|
5774
|
+
* ⚠️ **覆盖边界(诚实标注)**:只覆盖 **ACC 自己这条通道**(`im-bridge` 的 send / send-file)。
|
|
5775
|
+
* CCC 自有的发送通道(如 `mail-tool` MSM)**ACC 看不见** ⇒ 那类"已通知"落到
|
|
5776
|
+
* {@link hasOutboundSendSince} = false ⇒ 会被判成假完成。⇒ 本版口径:**代理模式下"联系用户"
|
|
5777
|
+
* 只认 ACC 通道**;这与设计 §7 的原话(im-bridge 记日志)一致,边界写在此处以免误读。
|
|
5778
|
+
*/
|
|
5779
|
+
function noteOutboundSend(root, atMs = Date.now()) {
|
|
5780
|
+
if (!root) return;
|
|
5781
|
+
const list = outboundSends.get(root) ?? [];
|
|
5782
|
+
list.push(atMs);
|
|
5783
|
+
const cutoff = atMs - UNATTENDED_OUTBOUND_TTL_MS;
|
|
5784
|
+
const kept = list.filter((t) => t >= cutoff).slice(-20);
|
|
5785
|
+
outboundSends.set(root, kept);
|
|
5786
|
+
}
|
|
5787
|
+
/** 自 `sinceMs` 起该 CCC 是否有过成功出站发送(**只读,不清表**——清理归写入侧的裁剪) */
|
|
5788
|
+
function hasOutboundSendSince(root, sinceMs, nowMs = Date.now()) {
|
|
5789
|
+
if (!root) return false;
|
|
5790
|
+
const list = outboundSends.get(root);
|
|
5791
|
+
if (!list || list.length === 0) return false;
|
|
5792
|
+
const floor = Math.max(sinceMs, nowMs - UNATTENDED_OUTBOUND_TTL_MS);
|
|
5793
|
+
return list.some((t) => t >= floor && t <= nowMs);
|
|
5794
|
+
}
|
|
5795
|
+
/** 固定文件名(与 `cro-wake-log.json` 并排、主语分明:那份记"我叫了什么",这份记"我代说了什么") */
|
|
5796
|
+
const UNATTENDED_LOG_FILENAME = "unattended-log.json";
|
|
5797
|
+
/** 保留窗 = 2 日(与 CRO 流水同值;**常量** ⇒ 无需人工清理动作) */
|
|
5798
|
+
const UNATTENDED_LOG_RETENTION_MS = 1728e5;
|
|
5799
|
+
const UNATTENDED_LOG_VERSION = 1;
|
|
5800
|
+
/** 日志绝对路径 */
|
|
5801
|
+
function unattendedLogPath(root, dirName) {
|
|
5802
|
+
return join(sessionsRoot(root), dirName, UNATTENDED_LOG_FILENAME);
|
|
5803
|
+
}
|
|
5804
|
+
function emptyLog() {
|
|
5805
|
+
return {
|
|
5806
|
+
version: UNATTENDED_LOG_VERSION,
|
|
5807
|
+
entries: []
|
|
5808
|
+
};
|
|
5809
|
+
}
|
|
5810
|
+
/** 条目形状校验(容忍手改:字段缺失/类型不符 ⇒ 丢弃该条,不阻断整档) */
|
|
5811
|
+
function asEntry(value) {
|
|
5812
|
+
const e = value;
|
|
5813
|
+
if (!e || typeof e !== "object") return null;
|
|
5814
|
+
if (typeof e.at !== "string" || e.at === "") return null;
|
|
5815
|
+
if (e.event !== "inject" && e.event !== "accept" && e.event !== "cap" && e.event !== "yield") return null;
|
|
5816
|
+
if (typeof e.reason !== "string") return null;
|
|
5817
|
+
return {
|
|
5818
|
+
at: e.at,
|
|
5819
|
+
event: e.event,
|
|
5820
|
+
round: typeof e.round === "number" ? e.round : 0,
|
|
5821
|
+
nonce: typeof e.nonce === "string" ? e.nonce : null,
|
|
5822
|
+
reason: e.reason,
|
|
5823
|
+
sessionId: typeof e.sessionId === "string" ? e.sessionId : ""
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5826
|
+
/** 读取流水(**永不抛**;读坏 ⇒ 当作空档 + 返回 error 文本) */
|
|
5827
|
+
function loadUnattendedLog(root, dirName) {
|
|
5828
|
+
const path = unattendedLogPath(root, dirName);
|
|
5829
|
+
if (!existsSync(path)) return {
|
|
5830
|
+
log: emptyLog(),
|
|
5831
|
+
error: null
|
|
5832
|
+
};
|
|
5833
|
+
try {
|
|
5834
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
5835
|
+
const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
5836
|
+
const entries = [];
|
|
5837
|
+
for (const item of raw) {
|
|
5838
|
+
const e = asEntry(item);
|
|
5839
|
+
if (e) entries.push(e);
|
|
5840
|
+
}
|
|
5841
|
+
return {
|
|
5842
|
+
log: {
|
|
5843
|
+
version: UNATTENDED_LOG_VERSION,
|
|
5844
|
+
entries
|
|
5845
|
+
},
|
|
5846
|
+
error: null
|
|
5847
|
+
};
|
|
5848
|
+
} catch (err) {
|
|
5849
|
+
return {
|
|
5850
|
+
log: emptyLog(),
|
|
5851
|
+
error: `无人值守流水解析失败(${String(err?.message ?? err)})`
|
|
5852
|
+
};
|
|
5853
|
+
}
|
|
5854
|
+
}
|
|
5855
|
+
/**
|
|
5856
|
+
* 按窗裁剪(**纯函数** ⇒ 可穷举测试)。
|
|
5857
|
+
* 🔴 `at` 不可解析者**一律丢弃**并在返回值里报数(`dropped`)——**不静默吞掉**(同 `cro-log.ts` 纪律)。
|
|
5858
|
+
*/
|
|
5859
|
+
function pruneUnattendedLogEntries(entries, nowMs, windowMs = UNATTENDED_LOG_RETENTION_MS) {
|
|
5860
|
+
const floor = nowMs - windowMs;
|
|
5861
|
+
const kept = [];
|
|
5862
|
+
let pruned = 0;
|
|
5863
|
+
let dropped = 0;
|
|
5864
|
+
for (const e of entries) {
|
|
5865
|
+
const t = Date.parse(e.at);
|
|
5866
|
+
if (Number.isNaN(t)) {
|
|
5867
|
+
dropped += 1;
|
|
5868
|
+
continue;
|
|
5869
|
+
}
|
|
5870
|
+
if (t >= floor) kept.push(e);
|
|
5871
|
+
else pruned += 1;
|
|
5872
|
+
}
|
|
5873
|
+
return {
|
|
5874
|
+
kept,
|
|
5875
|
+
pruned,
|
|
5876
|
+
dropped
|
|
5877
|
+
};
|
|
5878
|
+
}
|
|
5879
|
+
/**
|
|
5880
|
+
* 追加一条流水并按窗裁剪(**本模块的落盘入口;永不抛**)。
|
|
5881
|
+
*
|
|
5882
|
+
* 顺序 = 读 → 追加 → 裁剪 → **原子写**(tmp + rename)。任一步失败 ⇒ `{ok:false, error}`,**不抛**
|
|
5883
|
+
* ——调用方只记一行 console 日志 ⇒ **代理机制的问题绝不影响 turn 收尾**。
|
|
5884
|
+
*/
|
|
5885
|
+
function appendUnattendedLog(root, dirName, entry, nowMs = Date.now()) {
|
|
5886
|
+
if (!root || !dirName) return {
|
|
5887
|
+
ok: false,
|
|
5888
|
+
error: "缺少 CCC 根或轨迹目录名",
|
|
5889
|
+
kept: 0
|
|
5890
|
+
};
|
|
5891
|
+
const path = unattendedLogPath(root, dirName);
|
|
5892
|
+
try {
|
|
5893
|
+
const loaded = loadUnattendedLog(root, dirName);
|
|
5894
|
+
const full = {
|
|
5895
|
+
at: isoLocal(nowMs),
|
|
5896
|
+
...entry
|
|
5897
|
+
};
|
|
5898
|
+
const { kept } = pruneUnattendedLogEntries([...loaded.log.entries, full], nowMs);
|
|
5899
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
5900
|
+
const tmp = `${path}.tmp`;
|
|
5901
|
+
writeFileSync(tmp, `${JSON.stringify({
|
|
5902
|
+
version: UNATTENDED_LOG_VERSION,
|
|
5903
|
+
entries: kept
|
|
5904
|
+
}, null, 2)}\n`, "utf-8");
|
|
5905
|
+
renameSync(tmp, path);
|
|
5906
|
+
return {
|
|
5907
|
+
ok: true,
|
|
5908
|
+
error: null,
|
|
5909
|
+
kept: kept.length
|
|
5910
|
+
};
|
|
5911
|
+
} catch (err) {
|
|
5912
|
+
return {
|
|
5913
|
+
ok: false,
|
|
5914
|
+
error: `无人值守流水写入失败(${String(err?.message ?? err)})`,
|
|
5915
|
+
kept: 0
|
|
5916
|
+
};
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
//#endregion
|
|
5488
5920
|
//#region src/im-bridge.ts
|
|
5489
5921
|
/**
|
|
5490
5922
|
* im-bridge.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
|
|
@@ -5635,17 +6067,19 @@ async function runImBridge(root, req) {
|
|
|
5635
6067
|
const resolved = channel.resolveUser(root, rawUser);
|
|
5636
6068
|
if (!resolved) return fail("ALIAS_UNKNOWN", `unknown user "${rawUser}"`);
|
|
5637
6069
|
try {
|
|
6070
|
+
const sent = await channel.send({
|
|
6071
|
+
root,
|
|
6072
|
+
userId: resolved.id,
|
|
6073
|
+
text,
|
|
6074
|
+
accountId: req.account
|
|
6075
|
+
});
|
|
6076
|
+
noteOutboundSend(root);
|
|
5638
6077
|
return {
|
|
5639
6078
|
ok: true,
|
|
5640
6079
|
channel: channelId,
|
|
5641
6080
|
action,
|
|
5642
6081
|
detail: {
|
|
5643
|
-
...
|
|
5644
|
-
root,
|
|
5645
|
-
userId: resolved.id,
|
|
5646
|
-
text,
|
|
5647
|
-
accountId: req.account
|
|
5648
|
-
}),
|
|
6082
|
+
...sent,
|
|
5649
6083
|
user: resolved.alias ?? resolved.id
|
|
5650
6084
|
}
|
|
5651
6085
|
};
|
|
@@ -5664,19 +6098,21 @@ async function runImBridge(root, req) {
|
|
|
5664
6098
|
const file = readCccFile(root, fileArg);
|
|
5665
6099
|
if (!file.ok) return fail(file.code, file.error);
|
|
5666
6100
|
try {
|
|
6101
|
+
const sent = await channel.sendFile({
|
|
6102
|
+
root,
|
|
6103
|
+
userId: resolved.id,
|
|
6104
|
+
data: file.data,
|
|
6105
|
+
fileName: file.fileName,
|
|
6106
|
+
caption: req.caption,
|
|
6107
|
+
accountId: req.account
|
|
6108
|
+
});
|
|
6109
|
+
noteOutboundSend(root);
|
|
5667
6110
|
return {
|
|
5668
6111
|
ok: true,
|
|
5669
6112
|
channel: channelId,
|
|
5670
6113
|
action,
|
|
5671
6114
|
detail: {
|
|
5672
|
-
...
|
|
5673
|
-
root,
|
|
5674
|
-
userId: resolved.id,
|
|
5675
|
-
data: file.data,
|
|
5676
|
-
fileName: file.fileName,
|
|
5677
|
-
caption: req.caption,
|
|
5678
|
-
accountId: req.account
|
|
5679
|
-
}),
|
|
6115
|
+
...sent,
|
|
5680
6116
|
user: resolved.alias ?? resolved.id
|
|
5681
6117
|
}
|
|
5682
6118
|
};
|
|
@@ -6268,7 +6704,7 @@ function registerBootstrap(ctx) {
|
|
|
6268
6704
|
if (depth === 0) {
|
|
6269
6705
|
if (hasUserMessageHistory(session)) return;
|
|
6270
6706
|
} else if (sid !== void 0 && anchoredSessions.has(sid)) return;
|
|
6271
|
-
if (message?.source?.kind === "plugin") return;
|
|
6707
|
+
if (message?.source?.kind === "plugin:dsh-serenity-hooks") return;
|
|
6272
6708
|
const inbox = agent.inbox;
|
|
6273
6709
|
if (!inbox?.prepend) return;
|
|
6274
6710
|
for (let i = SETTINGS.anchorMessages.length - 1; i >= 0; i--) inbox.prepend("next-turn", {
|
|
@@ -6279,8 +6715,7 @@ function registerBootstrap(ctx) {
|
|
|
6279
6715
|
text: SETTINGS.anchorMessages[i]
|
|
6280
6716
|
}],
|
|
6281
6717
|
source: {
|
|
6282
|
-
kind:
|
|
6283
|
-
plugin: "dsh-serenity-hooks",
|
|
6718
|
+
kind: ACC_MESSAGE_KIND,
|
|
6284
6719
|
form: "notice",
|
|
6285
6720
|
summary: "bootstrap anchor turn"
|
|
6286
6721
|
}
|
|
@@ -6606,10 +7041,6 @@ function trajectoryCompactionReminderText(input) {
|
|
|
6606
7041
|
if (escalated) return `${eventToken("compaction")} ${facts} — you have been reminded repeatedly and have NOT compacted SESSION.md. This is now mandatory: STOP the current task step, load eap (praxis eap), rewrite SESSION.md, then resume. ${principles} ${frontmatterGuard} This reminder persists until the file is under the limit.`;
|
|
6607
7042
|
return `${eventToken("compaction")} ${facts}. Pause the current work and compact this trajectory's log (SESSION.md) before continuing — that log is the persistent body of this trajectory, and its size is operational entropy (H_op) the container pays on every read. ${principles} ${frontmatterGuard} After rewriting, resume the paused work; this reminder stops once the file is under the limit.`;
|
|
6608
7043
|
}
|
|
6609
|
-
const PLUGIN_SOURCE$4 = {
|
|
6610
|
-
kind: "plugin",
|
|
6611
|
-
plugin: "dsh-serenity-hooks"
|
|
6612
|
-
};
|
|
6613
7044
|
/** 每 agent 一个跟踪器(进程内存态,agent token 维度) */
|
|
6614
7045
|
const trackers = /* @__PURE__ */ new Map();
|
|
6615
7046
|
/** 连续超阈值轮数达此值 → 升级为 [TRAJECTORY-ESCALATED] 强制语气(此后持续升级催,直到 rebuild) */
|
|
@@ -6695,7 +7126,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
6695
7126
|
if (blocks.length === 0) return downstream;
|
|
6696
7127
|
const reminderAll = createUserMessage({
|
|
6697
7128
|
content: blocks,
|
|
6698
|
-
source: PLUGIN_SOURCE
|
|
7129
|
+
source: PLUGIN_SOURCE
|
|
6699
7130
|
});
|
|
6700
7131
|
if (downstream.kind === "block") return {
|
|
6701
7132
|
kind: "block",
|
|
@@ -6739,10 +7170,6 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
|
|
|
6739
7170
|
}
|
|
6740
7171
|
return lines.join("\n");
|
|
6741
7172
|
}
|
|
6742
|
-
const PLUGIN_SOURCE$3 = {
|
|
6743
|
-
kind: "plugin",
|
|
6744
|
-
plugin: "dsh-serenity-hooks"
|
|
6745
|
-
};
|
|
6746
7173
|
/**
|
|
6747
7174
|
* ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + Phase 2)。
|
|
6748
7175
|
* ⚠️ 原文案含「+ handyman 模型」,该注入已于 2026-09-16 删除(§0e-G)⇒ 此处同步订正(散文级残留)。
|
|
@@ -6757,7 +7184,7 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
|
6757
7184
|
}];
|
|
6758
7185
|
return createUserMessage({
|
|
6759
7186
|
content,
|
|
6760
|
-
source: PLUGIN_SOURCE
|
|
7187
|
+
source: PLUGIN_SOURCE
|
|
6761
7188
|
});
|
|
6762
7189
|
}
|
|
6763
7190
|
/** 每 agent 是否已注入过(进程内存态) */
|
|
@@ -6871,7 +7298,7 @@ function registerContext(ctx, opts = {}) {
|
|
|
6871
7298
|
syncExclusiveToolsVisibility(agent, root);
|
|
6872
7299
|
} catch {}
|
|
6873
7300
|
};
|
|
6874
|
-
if (opts.seedOnStart ?? true) ctx.on("agent/
|
|
7301
|
+
if (opts.seedOnStart ?? true) ctx.on("agent/created", async (payload) => {
|
|
6875
7302
|
try {
|
|
6876
7303
|
seed(payload.agent);
|
|
6877
7304
|
} catch {}
|
|
@@ -7352,7 +7779,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7352
7779
|
return;
|
|
7353
7780
|
}
|
|
7354
7781
|
const settings = readAdvancedSettings();
|
|
7355
|
-
const { readSimpleSettings } = await import("./settings-section-
|
|
7782
|
+
const { readSimpleSettings } = await import("./settings-section-DTJW4smh.js").then((n) => n.r);
|
|
7356
7783
|
const simple = readSimpleSettings();
|
|
7357
7784
|
const allowed = settings.publicAsk.allowed;
|
|
7358
7785
|
const port = simple.acpHttpPort ?? ACP_HTTP_PORT;
|
|
@@ -7911,21 +8338,14 @@ const HOST_SERVICES = [
|
|
|
7911
8338
|
id: "settings",
|
|
7912
8339
|
name: "settings",
|
|
7913
8340
|
access: "injected",
|
|
7914
|
-
members: [
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
},
|
|
7923
|
-
{
|
|
7924
|
-
name: "update",
|
|
7925
|
-
kind: "function"
|
|
7926
|
-
}
|
|
7927
|
-
],
|
|
7928
|
-
impact: "设置面板不安装 → 所有开关静默 no-op;opencode 路由自动配置失效",
|
|
8341
|
+
members: [{
|
|
8342
|
+
name: "describe",
|
|
8343
|
+
kind: "function"
|
|
8344
|
+
}, {
|
|
8345
|
+
name: "update",
|
|
8346
|
+
kind: "function"
|
|
8347
|
+
}],
|
|
8348
|
+
impact: "设置面板不装配 → 所有开关静默 no-op;opencode 路由自动配置与多模态补丁失效",
|
|
7929
8349
|
required: true
|
|
7930
8350
|
},
|
|
7931
8351
|
{
|
|
@@ -8075,8 +8495,8 @@ const HOST_SERVICES = [
|
|
|
8075
8495
|
}
|
|
8076
8496
|
];
|
|
8077
8497
|
/** dsp 被验证过的宿主版本范围(与 package.json peerDependencies 同源,单一真相源) */
|
|
8078
|
-
const REQUIRED_HOST_RANGE = "^0.1.
|
|
8079
|
-
/** 范围 floor:剥掉 caret/比较前缀(`^0.1.
|
|
8498
|
+
const REQUIRED_HOST_RANGE = "^0.1.7-rc.1";
|
|
8499
|
+
/** 范围 floor:剥掉 caret/比较前缀(`^0.1.7-rc.1` → `0.1.7-rc.1`),供 {@link checkHostVersion} 使用 */
|
|
8080
8500
|
const REQUIRED_HOST_FLOOR = REQUIRED_HOST_RANGE.replace(/^[\^~>=<\s]+/, "");
|
|
8081
8501
|
/** 范围 ceiling:`^0.1.x` 的上界 = `<0.2.0`(显式常量,见 {@link checkHostVersion} 注释) */
|
|
8082
8502
|
const REQUIRED_HOST_CEILING = "0.2.0";
|
|
@@ -9300,7 +9720,7 @@ function registerGateway(ctx) {
|
|
|
9300
9720
|
});
|
|
9301
9721
|
};
|
|
9302
9722
|
sync();
|
|
9303
|
-
ctx.on("agent/
|
|
9723
|
+
ctx.on("agent/created", async (payload) => {
|
|
9304
9724
|
try {
|
|
9305
9725
|
const cwd = payload.agent?.session?.header?.cwd;
|
|
9306
9726
|
if (cwd) {
|
|
@@ -9329,10 +9749,6 @@ var rebuild_exports = /* @__PURE__ */ __exportAll({
|
|
|
9329
9749
|
sanitizeFocusLine: () => sanitizeFocusLine,
|
|
9330
9750
|
stripAckSuffix: () => stripAckSuffix
|
|
9331
9751
|
});
|
|
9332
|
-
const PLUGIN_SOURCE$2 = {
|
|
9333
|
-
kind: "plugin",
|
|
9334
|
-
plugin: "dsh-serenity-hooks"
|
|
9335
|
-
};
|
|
9336
9752
|
/** acknowledge 尾句(first-anchor 的确认要求——rebuild 重建后不重走确认轮,直接继续) */
|
|
9337
9753
|
const ACK_SUFFIX_RE = /Please simply reply "acknowledge" — no action needed\.\s*$/;
|
|
9338
9754
|
/**
|
|
@@ -9648,7 +10064,7 @@ function registerRebuildTurnHook(ctx) {
|
|
|
9648
10064
|
type: "text",
|
|
9649
10065
|
text: `${eventToken("rebuild")} The conversation has been cleared and rebuilt. Follow the anchor instructions above now: read the persistent trajectory (SESSION.md) and continue the work automatically from the last checkpoint.`
|
|
9650
10066
|
}],
|
|
9651
|
-
source: PLUGIN_SOURCE
|
|
10067
|
+
source: PLUGIN_SOURCE
|
|
9652
10068
|
}));
|
|
9653
10069
|
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
9654
10070
|
sessionId: id,
|
|
@@ -11801,7 +12217,7 @@ const MECHANISM_WORDS = [
|
|
|
11801
12217
|
"localstore.json",
|
|
11802
12218
|
".opencode/serenity.json",
|
|
11803
12219
|
"AGENT_SESSIONS",
|
|
11804
|
-
"agent/
|
|
12220
|
+
"agent/created",
|
|
11805
12221
|
"agent/turn-stopping",
|
|
11806
12222
|
"tools/pre-execute",
|
|
11807
12223
|
"tools/post-execute",
|
|
@@ -11911,17 +12327,15 @@ function buildRebuke(hits) {
|
|
|
11911
12327
|
const rebukeStates = /* @__PURE__ */ new Map();
|
|
11912
12328
|
//#endregion
|
|
11913
12329
|
//#region src/output-guard-seam.ts
|
|
11914
|
-
const PLUGIN_SOURCE$1 = {
|
|
11915
|
-
kind: "plugin",
|
|
11916
|
-
plugin: "dsh-serenity-hooks"
|
|
11917
|
-
};
|
|
11918
12330
|
/** 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免):
|
|
11919
12331
|
* skiff-(F4 问答)/ acp-(F4c 程序化)/ rebuild-(v1.22.4 重建会话)前缀 */
|
|
11920
12332
|
function isExternalFaceSession(sessionId) {
|
|
11921
12333
|
if (!sessionId) return false;
|
|
11922
12334
|
return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
|
|
11923
12335
|
}
|
|
11924
|
-
/** 读会话最后一个 assistant/message 文本(skiff-core
|
|
12336
|
+
/** 读会话最后一个 assistant/message 文本(skiff-core 同款)
|
|
12337
|
+
* **2026-09-23(S142 D77)起导出**:`unattended-seam.ts` 复用同一读法(本容器纪律:
|
|
12338
|
+
* 「同一读法只留一份」——两份 `lastAssistantText` 会在宿主改事件形状时**只修一处**)。
|
|
11925
12339
|
* **v1.27.7:剥离 think 块再检测**(用户:"不检查think内容(因为不输出)")——
|
|
11926
12340
|
* ` think…` 是模型思考过程,不进入用户可见输出;敏感词检测只针对**最终呈现文本**,
|
|
11927
12341
|
* 否则 think 内提及内部机制词(思考过程必然推演机制)会误打回。复用 stripThink
|
|
@@ -11972,7 +12386,7 @@ function registerOutputGuardHook(ctx) {
|
|
|
11972
12386
|
type: "text",
|
|
11973
12387
|
text: buildRebuke(hits)
|
|
11974
12388
|
}],
|
|
11975
|
-
source: PLUGIN_SOURCE
|
|
12389
|
+
source: PLUGIN_SOURCE
|
|
11976
12390
|
}));
|
|
11977
12391
|
} catch (error) {
|
|
11978
12392
|
console.warn(`[serenity-hooks] output-guard steer failed: ${String(error?.message ?? error)}`);
|
|
@@ -11980,6 +12394,235 @@ function registerOutputGuardHook(ctx) {
|
|
|
11980
12394
|
});
|
|
11981
12395
|
}
|
|
11982
12396
|
//#endregion
|
|
12397
|
+
//#region src/unattended-seam.ts
|
|
12398
|
+
/**
|
|
12399
|
+
* unattended-seam.ts — 无人值守代理回复的**拦截缝**(S142 **D77**,owner 2026-09-23 令「我需要的是一个机制」)
|
|
12400
|
+
*
|
|
12401
|
+
* ## 停在哪、怎么接(设计 §4 的裁决:**A 的通道 + B 的措辞**)
|
|
12402
|
+
*
|
|
12403
|
+
* 复用的是**已验证过的**通道——`agent/turn-stopping`(serial)里 `agent.steer()`:
|
|
12404
|
+
* 宿主语义 = "**listener 反对就打回**,机器重读 inbox:有新 steer ⇒ 再跑一步,没有 ⇒ 关轮"
|
|
12405
|
+
* (`host/contract.ts` 的 `HOST_EVENTS` 原文)。因此:
|
|
12406
|
+
* · **轮不结束**(语义正是"有人打回")⇒ **零新机械**;
|
|
12407
|
+
* · 文本**写成代理用户的话**(B 的措辞)+ **首句自报身份**(owner 已裁)。
|
|
12408
|
+
*
|
|
12409
|
+
* 模板 = `output-guard-seam.ts`(同一缝位、同一 `createUserMessage + PLUGIN_SOURCE` 形态)。
|
|
12410
|
+
*
|
|
12411
|
+
* ## 🔴「这轮是不是真实用户发起的」怎么判(本机制**唯一**的新机械;2026-09-23 取证)
|
|
12412
|
+
*
|
|
12413
|
+
* 交接块把它列为"动手前必须先查清的一件事",取证结论(读宿主类型面,非猜测):
|
|
12414
|
+
*
|
|
12415
|
+
* | 事实 | 出处 |
|
|
12416
|
+
* |---|---|
|
|
12417
|
+
* | 每条进入 inbox 的消息都发 `agent/inbox/inserted { agent, message }`;**在轮内离开 inbox 时**发 `agent/inbox/claimed { agent, message, turn }` | `@deepseek-ai/dsh-agent` README(Inbox 段)+ `runtime-types.d.ts` |
|
|
12418
|
+
* | `claimed.turn` = **拥有该消息的那一轮**;`turn-stopping.turn` = **即将关闭的那一轮** ⇒ 两者同编号可直接比对 | `runtime-types.d.ts` 同一文件两处 `turn` 声明 |
|
|
12419
|
+
* | 消息来源可辨:`MessageSourceMap` 里**真实用户** = `kind: 'user'`(`user` / `user-rpc` 两种形态**同 kind**);**ACC 自己注入**的一律 `kind: 'plugin:dsh-serenity-hooks'`(v1.47.0 改;旧值 = `'plugin'`) | `@deepseek-ai/dsh-llm` `src/message.ts` 的 `MessageSourceMap`(合并和类型)+ 本仓 `message-source.ts`(**唯一**声明处与常量处) |
|
|
12420
|
+
*
|
|
12421
|
+
* ⇒ **判据 = 「这一轮 claim 到的消息里,有没有 `source.kind === 'user'`」**。
|
|
12422
|
+
* 没有现成信号时本可退化为"记账法"(我们知道自己投过什么),**但不必**:宿主已给出一手信号。
|
|
12423
|
+
*
|
|
12424
|
+
* ## 🔴 为什么"用户发起的轮"要跳过(这是设计 §3-③ 的本意,别当成漏洞)
|
|
12425
|
+
*
|
|
12426
|
+
* 用户**刚说完话**⇒ 人就在跟前,代理插话既是冒充也是浪费。**"用户说完走开"那种情形**
|
|
12427
|
+
* 由**绳(长周期保险丝)/其它非用户发起的唤醒**覆盖:那条轮**不是**用户发起的 ⇒ 代理接手,
|
|
12428
|
+
* 一路推进到**合法收束**。⇒ 两个机制合起来才等于"用户走开后会话还能自己跑完"。
|
|
12429
|
+
*
|
|
12430
|
+
* ## 🔴 铁律(与 `unattended-ops.ts` 同族)
|
|
12431
|
+
*
|
|
12432
|
+
* **本模块的任何失败都不得影响 turn 收尾**:所有 handler 包 try/catch,异常只记一行 console。
|
|
12433
|
+
*/
|
|
12434
|
+
/** 取载体 id(payload 的 `agent` 面;取不到 ⇒ 空串) */
|
|
12435
|
+
function sessionIdOf$2(agent) {
|
|
12436
|
+
const id = agent?.session?.id;
|
|
12437
|
+
return typeof id === "string" ? id : "";
|
|
12438
|
+
}
|
|
12439
|
+
/** 取会话对象(payload 的 `agent.session`) */
|
|
12440
|
+
function sessionOf(agent) {
|
|
12441
|
+
return agent?.session;
|
|
12442
|
+
}
|
|
12443
|
+
/** 取会话 cwd(`header.cwd`;取不到回落进程 cwd——与 `output-guard-seam.ts` 同款读法) */
|
|
12444
|
+
function cwdOf(agent) {
|
|
12445
|
+
return sessionOf(agent)?.header?.cwd ?? process.cwd();
|
|
12446
|
+
}
|
|
12447
|
+
/** 是否 **CCC 内主会话**(`delegationDepth` 缺省 0;子代理 / 无头运行不算) */
|
|
12448
|
+
function isMainSession(agent) {
|
|
12449
|
+
return (sessionOf(agent)?.header?.delegationDepth ?? 0) === 0;
|
|
12450
|
+
}
|
|
12451
|
+
/**
|
|
12452
|
+
* 该会话所属轨迹**是否带 CRO 程序**(窄口径的唯一排除项)。
|
|
12453
|
+
* 判据 = 轨迹目录下存在 `continuous-re-occurrence.ts`(与 `cro.ts` 同源:**文件在 = 启用**)。
|
|
12454
|
+
* 无绑定(未挂在任何轨迹上)⇒ false(没有程序在驱动它)。
|
|
12455
|
+
*/
|
|
12456
|
+
function hasCroProgram(session, root) {
|
|
12457
|
+
const dirName = readLastBound(session)?.dirName;
|
|
12458
|
+
if (!dirName) return false;
|
|
12459
|
+
return existsSync(join(sessionsRoot(root), dirName, CRO_FILENAME));
|
|
12460
|
+
}
|
|
12461
|
+
/** 落一条流水(**永不抛**;无绑定 ⇒ 无落点,只回 false 由调用方记 console) */
|
|
12462
|
+
function logEvent(session, root, entry, nowMs) {
|
|
12463
|
+
const dirName = readLastBound(session)?.dirName;
|
|
12464
|
+
if (!dirName) return false;
|
|
12465
|
+
const res = appendUnattendedLog(root, dirName, entry, nowMs);
|
|
12466
|
+
if (!res.ok) console.log(`[serenity-hooks] unattended: 流水未落盘 — ${res.error ?? "未知原因"}`);
|
|
12467
|
+
return res.ok;
|
|
12468
|
+
}
|
|
12469
|
+
/**
|
|
12470
|
+
* 一轮收尾(`agent/turn-stopping`)的完整处置。
|
|
12471
|
+
*
|
|
12472
|
+
* 顺序:**取事实 → 纯函数判定 → 执行**(判定全在 `unattended-ops.ts`,本函数只做 I/O 与 steer)。
|
|
12473
|
+
* @returns 便于用例观察的判定结果文本(生产不使用返回值)
|
|
12474
|
+
*/
|
|
12475
|
+
function handleTurnStopping(agent, turn) {
|
|
12476
|
+
if (readSimpleSettings().unattendedEnabled !== true) return "skip:mode-off";
|
|
12477
|
+
const sessionId = sessionIdOf$2(agent);
|
|
12478
|
+
if (sessionId === "") return "skip:no-session-id";
|
|
12479
|
+
const root = cccRootForCwd(cwdOf(agent));
|
|
12480
|
+
if (root === null) return "skip:not-in-ccc";
|
|
12481
|
+
const session = sessionOf(agent);
|
|
12482
|
+
const nowMs = Date.now();
|
|
12483
|
+
const run = getProxyRun(sessionId);
|
|
12484
|
+
const text = lastAssistantText(agent);
|
|
12485
|
+
const nonces = parseDoneNonces(text);
|
|
12486
|
+
const sessionIdExcluded = isProxyExcludedSessionId(sessionId);
|
|
12487
|
+
const userInitiated = turn === null ? true : isUserTurn(sessionId, turn);
|
|
12488
|
+
const cro = !sessionIdExcluded && !userInitiated && isMainSession(agent) ? hasCroProgram(session, root) : false;
|
|
12489
|
+
const verdict = decideUnattendedAction({
|
|
12490
|
+
enabled: true,
|
|
12491
|
+
root,
|
|
12492
|
+
isMainSession: isMainSession(agent),
|
|
12493
|
+
sessionIdExcluded,
|
|
12494
|
+
userInitiated,
|
|
12495
|
+
hasCroProgram: cro,
|
|
12496
|
+
run,
|
|
12497
|
+
nowMs,
|
|
12498
|
+
doneNonce: nonces.length > 0 ? nonces[nonces.length - 1] ?? null : null,
|
|
12499
|
+
claimsNotify: claimsUserNotification(text),
|
|
12500
|
+
outboundSeen: run !== null && hasOutboundSendSince(root, run.startedAt, nowMs)
|
|
12501
|
+
});
|
|
12502
|
+
if (verdict.action === "skip") return `skip:${verdict.reason}`;
|
|
12503
|
+
if (verdict.action === "accept" || verdict.action === "cap") {
|
|
12504
|
+
clearProxyRun(sessionId);
|
|
12505
|
+
logEvent(session, root, {
|
|
12506
|
+
event: verdict.action,
|
|
12507
|
+
round: verdict.round,
|
|
12508
|
+
nonce: run?.nonce ?? null,
|
|
12509
|
+
reason: verdict.reason,
|
|
12510
|
+
sessionId
|
|
12511
|
+
}, nowMs);
|
|
12512
|
+
console.log(`[serenity-hooks] unattended: ${verdict.action} (${verdict.reason}, round=${verdict.round}, session=${sessionId})`);
|
|
12513
|
+
return `${verdict.action}:${verdict.reason}`;
|
|
12514
|
+
}
|
|
12515
|
+
const nonce = newNonce();
|
|
12516
|
+
const startedAt = run?.startedAt ?? nowMs;
|
|
12517
|
+
setProxyRun(sessionId, {
|
|
12518
|
+
round: verdict.round,
|
|
12519
|
+
nonce,
|
|
12520
|
+
startedAt
|
|
12521
|
+
});
|
|
12522
|
+
const message = verdict.reason === "false-notify" ? buildFalseNotifyRebuke({
|
|
12523
|
+
round: verdict.round,
|
|
12524
|
+
maxRounds: 15,
|
|
12525
|
+
nonce
|
|
12526
|
+
}) : buildProxyMessage({
|
|
12527
|
+
round: verdict.round,
|
|
12528
|
+
maxRounds: 15,
|
|
12529
|
+
nonce
|
|
12530
|
+
});
|
|
12531
|
+
try {
|
|
12532
|
+
agent.steer(createUserMessage({
|
|
12533
|
+
content: [{
|
|
12534
|
+
type: "text",
|
|
12535
|
+
text: message
|
|
12536
|
+
}],
|
|
12537
|
+
source: PLUGIN_SOURCE
|
|
12538
|
+
}));
|
|
12539
|
+
} catch (error) {
|
|
12540
|
+
clearProxyRun(sessionId);
|
|
12541
|
+
logEvent(session, root, {
|
|
12542
|
+
event: "cap",
|
|
12543
|
+
round: verdict.round,
|
|
12544
|
+
nonce,
|
|
12545
|
+
reason: "steer-failed",
|
|
12546
|
+
sessionId
|
|
12547
|
+
}, nowMs);
|
|
12548
|
+
console.warn(`[serenity-hooks] unattended: steer 失败(本轮放弃) — ${String(error?.message ?? error)}`);
|
|
12549
|
+
return "cap:steer-failed";
|
|
12550
|
+
}
|
|
12551
|
+
logEvent(session, root, {
|
|
12552
|
+
event: "inject",
|
|
12553
|
+
round: verdict.round,
|
|
12554
|
+
nonce,
|
|
12555
|
+
reason: verdict.reason,
|
|
12556
|
+
sessionId
|
|
12557
|
+
}, nowMs);
|
|
12558
|
+
console.log(`[serenity-hooks] unattended: inject round=${verdict.round}/15 (${verdict.reason}, session=${sessionId})`);
|
|
12559
|
+
return `inject:${verdict.reason}`;
|
|
12560
|
+
}
|
|
12561
|
+
/**
|
|
12562
|
+
* 装配无人值守代理缝(`index.ts` apply 调用)。
|
|
12563
|
+
*
|
|
12564
|
+
* 订阅三个事件(**都已在 `host/contract.ts` 白名单里**,新增的 `agent/inbox/claimed` 同批登记):
|
|
12565
|
+
* · `agent/inbox/claimed` —— 记「真实用户发起的轮次」;用户回来时**让位**(清代理段 + 记流水);
|
|
12566
|
+
* · `agent/turn-stopping` —— 判定 + 注入(本机制主体);
|
|
12567
|
+
* · `agent/disposed` / `session/disposed` —— 载体销毁清表(**本容器栽过 5 次"只增不清"**)。
|
|
12568
|
+
*/
|
|
12569
|
+
function registerUnattendedSeam(ctx) {
|
|
12570
|
+
const on = (event, handler) => {
|
|
12571
|
+
try {
|
|
12572
|
+
ctx.on(event, handler);
|
|
12573
|
+
} catch (err) {
|
|
12574
|
+
console.log(`[serenity-hooks] ✗ 无人值守代理未装配(${event} 不可用): ${String(err?.message ?? err)}`);
|
|
12575
|
+
}
|
|
12576
|
+
};
|
|
12577
|
+
on("agent/inbox/claimed", (payload) => {
|
|
12578
|
+
try {
|
|
12579
|
+
const p = payload;
|
|
12580
|
+
const sessionId = sessionIdOf$2(p?.agent);
|
|
12581
|
+
if (sessionId === "") return;
|
|
12582
|
+
if (p.message?.source?.kind !== "user") return;
|
|
12583
|
+
noteUserTurn(sessionId, (typeof p.turn === "number" ? p.turn : null) ?? -1);
|
|
12584
|
+
const run = getProxyRun(sessionId);
|
|
12585
|
+
if (!run) return;
|
|
12586
|
+
clearProxyRun(sessionId);
|
|
12587
|
+
const root = cccRootForCwd(cwdOf(p.agent));
|
|
12588
|
+
if (root) logEvent(sessionOf(p.agent), root, {
|
|
12589
|
+
event: "yield",
|
|
12590
|
+
round: run.round,
|
|
12591
|
+
nonce: run.nonce,
|
|
12592
|
+
reason: "user-returned",
|
|
12593
|
+
sessionId
|
|
12594
|
+
}, Date.now());
|
|
12595
|
+
console.log(`[serenity-hooks] unattended: yield — 用户回来了 (session=${sessionId}, round=${run.round})`);
|
|
12596
|
+
} catch (err) {
|
|
12597
|
+
console.log(`[serenity-hooks] unattended: claimed 观察失败 — ${String(err?.message ?? err)}`);
|
|
12598
|
+
}
|
|
12599
|
+
});
|
|
12600
|
+
on("agent/turn-stopping", (payload) => {
|
|
12601
|
+
try {
|
|
12602
|
+
const p = payload;
|
|
12603
|
+
if (!p?.agent) return;
|
|
12604
|
+
handleTurnStopping(p.agent, typeof p.turn === "number" ? p.turn : null);
|
|
12605
|
+
} catch (err) {
|
|
12606
|
+
console.log(`[serenity-hooks] unattended: turn-stopping 处置失败(已忽略) — ${String(err?.message ?? err)}`);
|
|
12607
|
+
}
|
|
12608
|
+
});
|
|
12609
|
+
on("agent/disposed", (payload) => {
|
|
12610
|
+
const sessionId = sessionIdOf$2(payload?.agent);
|
|
12611
|
+
if (sessionId === "") return;
|
|
12612
|
+
clearProxyRun(sessionId);
|
|
12613
|
+
clearUserTurn(sessionId);
|
|
12614
|
+
});
|
|
12615
|
+
on("session/disposed", (payload) => {
|
|
12616
|
+
const id = payload?.id;
|
|
12617
|
+
if (typeof id !== "string" || id === "") return;
|
|
12618
|
+
clearProxyRun(id);
|
|
12619
|
+
clearUserTurn(id);
|
|
12620
|
+
});
|
|
12621
|
+
registerDisposer(ctx, "unattended proxy", () => {
|
|
12622
|
+
clearUnattendedState();
|
|
12623
|
+
});
|
|
12624
|
+
}
|
|
12625
|
+
//#endregion
|
|
11983
12626
|
//#region src/acp-core.ts
|
|
11984
12627
|
/** JSON-RPC 错误码(对齐标准) */
|
|
11985
12628
|
const RPC_ERROR = {
|
|
@@ -12733,12 +13376,6 @@ qInput.focus()
|
|
|
12733
13376
|
</body>
|
|
12734
13377
|
</html>`;
|
|
12735
13378
|
}
|
|
12736
|
-
//#endregion
|
|
12737
|
-
//#region src/weixin-output-guard.ts
|
|
12738
|
-
const PLUGIN_SOURCE = {
|
|
12739
|
-
kind: "plugin",
|
|
12740
|
-
plugin: "dsh-serenity-hooks"
|
|
12741
|
-
};
|
|
12742
13379
|
const manualSessions = /* @__PURE__ */ new Map();
|
|
12743
13380
|
/**
|
|
12744
13381
|
* 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
|
|
@@ -14231,7 +14868,7 @@ function describeOpencodeAction(action) {
|
|
|
14231
14868
|
*/
|
|
14232
14869
|
async function applyOpencodeAutoConfigOnce(ctx, env) {
|
|
14233
14870
|
const settings = hostSettings(ctx);
|
|
14234
|
-
if (settings?.
|
|
14871
|
+
if (settings?.describe === void 0 || settings.update === void 0) {
|
|
14235
14872
|
console.warn("[serenity-hooks] opencode 路由自动配置跳过:settings 服务不可用(未能读取/写入 llm-pi-ai 命名空间)");
|
|
14236
14873
|
return {
|
|
14237
14874
|
wrote: false,
|
|
@@ -14240,7 +14877,7 @@ async function applyOpencodeAutoConfigOnce(ctx, env) {
|
|
|
14240
14877
|
}
|
|
14241
14878
|
let resolved;
|
|
14242
14879
|
try {
|
|
14243
|
-
resolved = settings.
|
|
14880
|
+
resolved = (settings.describe.call(settings) ?? []).find((f) => f.ns === LLM_PI_AI_NAMESPACE)?.value;
|
|
14244
14881
|
} catch (error) {
|
|
14245
14882
|
console.warn(`[serenity-hooks] opencode 路由自动配置跳过:读取 ${LLM_PI_AI_NAMESPACE} 失败: ${String(error?.message ?? error)}`);
|
|
14246
14883
|
return {
|
|
@@ -14336,7 +14973,7 @@ function registerOpencodeAutoConfig(ctx) {
|
|
|
14336
14973
|
});
|
|
14337
14974
|
}
|
|
14338
14975
|
try {
|
|
14339
|
-
ctx.on?.call(ctx, "settings/updated", (ns) => {
|
|
14976
|
+
ctx.on?.call(ctx, "settings/document-updated", (ns) => {
|
|
14340
14977
|
if (ns === "llm-pi-ai") sync();
|
|
14341
14978
|
});
|
|
14342
14979
|
} catch {}
|
|
@@ -14477,7 +15114,7 @@ function planDeepseekVisionPatch(input) {
|
|
|
14477
15114
|
*/
|
|
14478
15115
|
async function applyDeepseekVisionPatchOnce(ctx, enabled) {
|
|
14479
15116
|
const settings = hostSettings(ctx);
|
|
14480
|
-
if (settings?.
|
|
15117
|
+
if (settings?.describe === void 0 || settings.update === void 0) {
|
|
14481
15118
|
console.warn("[serenity-hooks] DeepSeek 多模态补丁跳过:settings 服务不可用(未能读写 llm-pi-ai 命名空间)");
|
|
14482
15119
|
return {
|
|
14483
15120
|
wrote: false,
|
|
@@ -14486,7 +15123,7 @@ async function applyDeepseekVisionPatchOnce(ctx, enabled) {
|
|
|
14486
15123
|
}
|
|
14487
15124
|
let resolved;
|
|
14488
15125
|
try {
|
|
14489
|
-
resolved = settings.
|
|
15126
|
+
resolved = (settings.describe.call(settings) ?? []).find((f) => f.ns === LLM_PI_AI_NAMESPACE)?.value;
|
|
14490
15127
|
} catch (error) {
|
|
14491
15128
|
console.warn(`[serenity-hooks] DeepSeek 多模态补丁跳过:读取 ${LLM_PI_AI_NAMESPACE} 失败: ${String(error?.message ?? error)}`);
|
|
14492
15129
|
return {
|
|
@@ -14580,7 +15217,7 @@ function registerDeepseekVisionPatch(ctx, enabled) {
|
|
|
14580
15217
|
});
|
|
14581
15218
|
}
|
|
14582
15219
|
try {
|
|
14583
|
-
ctx.on?.call(ctx, "settings/updated", (ns) => {
|
|
15220
|
+
ctx.on?.call(ctx, "settings/document-updated", (ns) => {
|
|
14584
15221
|
if (ns === "llm-pi-ai") sync();
|
|
14585
15222
|
});
|
|
14586
15223
|
} catch {}
|
|
@@ -15125,6 +15762,7 @@ function renderClock(label, s) {
|
|
|
15125
15762
|
const parts = [
|
|
15126
15763
|
`armed=${s.armed}`,
|
|
15127
15764
|
`CRO 闸=${readSimpleSettings().croEnabled !== false}`,
|
|
15765
|
+
`无人值守闸=${readSimpleSettings().unattendedEnabled === true}`,
|
|
15128
15766
|
`tick 次数=${s.ticks}`,
|
|
15129
15767
|
`上次 tick=${ts(s.lastTickAt)}`
|
|
15130
15768
|
];
|
|
@@ -15442,7 +16080,17 @@ const Config = z.object({
|
|
|
15442
16080
|
}),
|
|
15443
16081
|
webFetch: z.object({ enabled: z.boolean().default(true) }),
|
|
15444
16082
|
opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
|
|
15445
|
-
visionPatch: z.object({ enabled: z.boolean().default(true) })
|
|
16083
|
+
visionPatch: z.object({ enabled: z.boolean().default(true) }),
|
|
16084
|
+
gatewayEnabled: z.boolean(),
|
|
16085
|
+
rebuildEnabled: z.boolean(),
|
|
16086
|
+
rebuildThresholdK: z.number().min(50).max(4e3),
|
|
16087
|
+
skiffEnabled: z.boolean(),
|
|
16088
|
+
skiffDebugPort: z.number().min(1024).max(65535),
|
|
16089
|
+
acpEnabled: z.boolean(),
|
|
16090
|
+
acpHttpPort: z.number().min(1024).max(65535),
|
|
16091
|
+
publicAskEnabled: z.boolean(),
|
|
16092
|
+
croEnabled: z.boolean(),
|
|
16093
|
+
unattendedEnabled: z.boolean()
|
|
15446
16094
|
});
|
|
15447
16095
|
function apply(ctx, config) {
|
|
15448
16096
|
try {
|
|
@@ -15494,6 +16142,7 @@ function apply(ctx, config) {
|
|
|
15494
16142
|
registerGateway(ctx);
|
|
15495
16143
|
registerRebuildTurnHook(ctx);
|
|
15496
16144
|
registerOutputGuardHook(ctx);
|
|
16145
|
+
registerUnattendedSeam(ctx);
|
|
15497
16146
|
if (config.env) registerEnv(ctx);
|
|
15498
16147
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
15499
16148
|
registerBootstrap(ctx);
|
|
@@ -15533,9 +16182,10 @@ function apply(ctx, config) {
|
|
|
15533
16182
|
* 任何 live 会话,故 apply 时刻就能解析到,不再需要"等一会儿再试"。
|
|
15534
16183
|
*
|
|
15535
16184
|
* 🔒 **必须保留**(它们不是"重试",删了会引入新缺陷):
|
|
15536
|
-
* - **反应式再触发**(`agent/
|
|
15537
|
-
* "恢复"旧会话不触发 `session/created`,但**开始对话**会触发 `agent/
|
|
15538
|
-
*
|
|
16185
|
+
* - **反应式再触发**(`agent/created` / `session/created` → sync):宿主重启后
|
|
16186
|
+
* "恢复"旧会话不触发 `session/created`,但**开始对话**会触发 `agent/created`
|
|
16187
|
+
* (v0.1.7 前名 `agent/session-start`)——这是唯一能把"进程起来时还解析不到、稍后才有会话"
|
|
16188
|
+
* 这条路径接上的通道。
|
|
15539
16189
|
* - **`starting` 在飞标志**:防同拍双次 start → `EADDRINUSE`。
|
|
15540
16190
|
* (C4 块 A 备注:面宿主亦按面名合并在飞启动,是第二道保险;**本标志仍保留**——
|
|
15541
16191
|
* 它管的是"解析 root 这段装配流程"的在飞语义,且调试服务的启动替身在测试里被 mock,
|
|
@@ -15586,7 +16236,7 @@ function registerSkiff(ctx) {
|
|
|
15586
16236
|
sync();
|
|
15587
16237
|
});
|
|
15588
16238
|
} catch {}
|
|
15589
|
-
for (const eventName of ["agent/
|
|
16239
|
+
for (const eventName of ["agent/created", "session/created"]) try {
|
|
15590
16240
|
ctx.on(eventName, () => {
|
|
15591
16241
|
if (!started) sync();
|
|
15592
16242
|
});
|