@shgroup/dsh-serenity-hooks 1.63.2 → 1.64.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/README.md +1 -1
- package/dsh.plugin.json +1 -1
- package/lib/client.js +0 -23
- package/lib/host/contract.d.ts +1 -1
- package/lib/index.d.ts +0 -4
- package/lib/index.js +32 -707
- package/lib/output-guard-seam.d.ts +4 -2
- package/lib/{settings-section-DGUQZo5j.js → settings-section-DN7g8K7Q.js} +0 -2
- package/lib/settings-section.d.ts +0 -10
- package/package.json +1 -1
- package/lib/unattended-ops.d.ts +0 -249
- package/lib/unattended-seam.d.ts +0 -49
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebSe
|
|
|
3
3
|
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readTrajectorySkills, i as findGitRoot, l as pathInside, m as readSafeModeHiddenTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readExclusiveTools, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-B0ZGL9lx.js";
|
|
4
4
|
import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-B4P3Uw6D.js";
|
|
5
5
|
import { a as readGitTrack, c as unsetEntry, i as localstorePath, l as writeEntry, n as checkLocalstoreGitCompliance, o as readStore, r as getEntry, s as runLocalStore, t as LOCALSTORE_SCOPES } from "./localstore-ops-DMs_ovHo.js";
|
|
6
|
-
import { a as FACE_PORTS, c as MECHANISM_PORTS, d as registerSkiffSession$1, f as skiffRoleFor$1, h as ACC_VERSION, i as ACP_PORT, l as SKIFF_DEBUG_PORT, m as unregisterSkiffSession$1, n as readSimpleSettings, o as GATEWAY_PORT, p as skiffSessionInfo$1, r as registerSettingsSection, s as MAIN_WEB_PORT, u as WEIXIN_SEND_PORT } from "./settings-section-
|
|
6
|
+
import { a as FACE_PORTS, c as MECHANISM_PORTS, d as registerSkiffSession$1, f as skiffRoleFor$1, h as ACC_VERSION, i as ACP_PORT, l as SKIFF_DEBUG_PORT, m as unregisterSkiffSession$1, n as readSimpleSettings, o as GATEWAY_PORT, p as skiffSessionInfo$1, r as registerSettingsSection, s as MAIN_WEB_PORT, u as WEIXIN_SEND_PORT } from "./settings-section-DN7g8K7Q.js";
|
|
7
7
|
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-B615L9MV.js";
|
|
8
8
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
9
9
|
import { a as readWeixinCredential, c as weixinInboundDir, i as matchWeixinRoute, l as weixinSessionIdFor, n as extractWeixinText, o as readWeixinSettings, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-Aheyq3fN.js";
|
|
@@ -9661,7 +9661,7 @@ function emptyRegistry() {
|
|
|
9661
9661
|
};
|
|
9662
9662
|
}
|
|
9663
9663
|
/** 条目形状校验(容忍手改:字段缺失/类型不符 → 丢弃该条,不阻断整表) */
|
|
9664
|
-
function asEntry$
|
|
9664
|
+
function asEntry$1(value) {
|
|
9665
9665
|
const e = value;
|
|
9666
9666
|
if (!e || typeof e.id !== "string" || typeof e.target !== "string") return null;
|
|
9667
9667
|
if (typeof e.at !== "string" || typeof e.message !== "string") return null;
|
|
@@ -9695,7 +9695,7 @@ function loadWakeRegistry(root) {
|
|
|
9695
9695
|
const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
9696
9696
|
const entries = [];
|
|
9697
9697
|
for (const item of raw) {
|
|
9698
|
-
const e = asEntry$
|
|
9698
|
+
const e = asEntry$1(item);
|
|
9699
9699
|
if (e) entries.push(e);
|
|
9700
9700
|
}
|
|
9701
9701
|
return {
|
|
@@ -10668,7 +10668,7 @@ const TURN_TTL_MS = 18e5;
|
|
|
10668
10668
|
*/
|
|
10669
10669
|
const runningBySession = /* @__PURE__ */ new Map();
|
|
10670
10670
|
/** 取载体 id(事件 payload 的 `agent` 面;取不到 ⇒ null) */
|
|
10671
|
-
function sessionIdOf$
|
|
10671
|
+
function sessionIdOf$2(agent) {
|
|
10672
10672
|
const id = agent?.session?.id;
|
|
10673
10673
|
return typeof id === "string" && id !== "" ? id : null;
|
|
10674
10674
|
}
|
|
@@ -10754,23 +10754,23 @@ function registerCroTurnTracking(ctx) {
|
|
|
10754
10754
|
}
|
|
10755
10755
|
};
|
|
10756
10756
|
on("agent/created", (payload) => {
|
|
10757
|
-
const id = sessionIdOf$
|
|
10757
|
+
const id = sessionIdOf$2(payload?.agent);
|
|
10758
10758
|
if (id) markTurnRunning(id, Date.now());
|
|
10759
10759
|
});
|
|
10760
10760
|
on("agent/status", (payload) => {
|
|
10761
10761
|
const p = payload;
|
|
10762
|
-
const id = sessionIdOf$
|
|
10762
|
+
const id = sessionIdOf$2(p?.agent);
|
|
10763
10763
|
if (!id) return;
|
|
10764
10764
|
const turn = typeof p.turn === "number" ? p.turn : null;
|
|
10765
10765
|
if (p.status === "running") markTurnRunning(id, Date.now(), turn);
|
|
10766
10766
|
else if (p.status === "idle") clearTurnRunning(id);
|
|
10767
10767
|
});
|
|
10768
10768
|
on("agent/turn-stopping", (payload) => {
|
|
10769
|
-
const id = sessionIdOf$
|
|
10769
|
+
const id = sessionIdOf$2(payload?.agent);
|
|
10770
10770
|
if (id) clearTurnRunning(id);
|
|
10771
10771
|
});
|
|
10772
10772
|
on("agent/disposed", (payload) => {
|
|
10773
|
-
const id = sessionIdOf$
|
|
10773
|
+
const id = sessionIdOf$2(payload?.agent);
|
|
10774
10774
|
if (id) clearTurnRunning(id);
|
|
10775
10775
|
});
|
|
10776
10776
|
on("session/disposed", (payload) => {
|
|
@@ -10839,14 +10839,14 @@ const CRO_LOG_VERSION = 1;
|
|
|
10839
10839
|
function croWakeLogPath(root, dirName) {
|
|
10840
10840
|
return join(sessionsRoot(root), dirName, CRO_LOG_FILENAME);
|
|
10841
10841
|
}
|
|
10842
|
-
function emptyLog
|
|
10842
|
+
function emptyLog() {
|
|
10843
10843
|
return {
|
|
10844
10844
|
version: CRO_LOG_VERSION,
|
|
10845
10845
|
entries: []
|
|
10846
10846
|
};
|
|
10847
10847
|
}
|
|
10848
10848
|
/** 条目形状校验(容忍手改:字段缺失/类型不符 → 丢弃该条,不阻断整档) */
|
|
10849
|
-
function asEntry
|
|
10849
|
+
function asEntry(value) {
|
|
10850
10850
|
const e = value;
|
|
10851
10851
|
if (!e || typeof e !== "object") return null;
|
|
10852
10852
|
if (typeof e.at !== "string" || e.at === "") return null;
|
|
@@ -10881,7 +10881,7 @@ function summarizeCroPrompt(prompt) {
|
|
|
10881
10881
|
function loadCroWakeLog(root, dirName) {
|
|
10882
10882
|
const path = croWakeLogPath(root, dirName);
|
|
10883
10883
|
if (!existsSync(path)) return {
|
|
10884
|
-
log: emptyLog
|
|
10884
|
+
log: emptyLog(),
|
|
10885
10885
|
error: null
|
|
10886
10886
|
};
|
|
10887
10887
|
try {
|
|
@@ -10889,7 +10889,7 @@ function loadCroWakeLog(root, dirName) {
|
|
|
10889
10889
|
const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
10890
10890
|
const entries = [];
|
|
10891
10891
|
for (const item of raw) {
|
|
10892
|
-
const e = asEntry
|
|
10892
|
+
const e = asEntry(item);
|
|
10893
10893
|
if (e) entries.push(e);
|
|
10894
10894
|
}
|
|
10895
10895
|
return {
|
|
@@ -10901,7 +10901,7 @@ function loadCroWakeLog(root, dirName) {
|
|
|
10901
10901
|
};
|
|
10902
10902
|
} catch (err) {
|
|
10903
10903
|
return {
|
|
10904
|
-
log: emptyLog
|
|
10904
|
+
log: emptyLog(),
|
|
10905
10905
|
error: `CRO 流水解析失败(${String(err?.message ?? err)})`
|
|
10906
10906
|
};
|
|
10907
10907
|
}
|
|
@@ -12941,447 +12941,6 @@ function createContainerAdminTool(ctx) {
|
|
|
12941
12941
|
});
|
|
12942
12942
|
}
|
|
12943
12943
|
//#endregion
|
|
12944
|
-
//#region src/unattended-ops.ts
|
|
12945
|
-
/**
|
|
12946
|
-
* unattended-ops.ts — 无人值守代理回复(unattended proxy)机制层(S142 **D77**,owner 2026-09-23 提案)
|
|
12947
|
-
*
|
|
12948
|
-
* ## 这东西用来干什么(白话)
|
|
12949
|
-
*
|
|
12950
|
-
* 用户发个任务就走开。现在模型干完一段就停下来等回话,用户不在 ⇒ **会话挂在那儿不动**。
|
|
12951
|
-
* 本机制 = 用户走开后,系统**代替用户说一句"继续"**:让它自己跑完;干完了用一个**验证码**报告;
|
|
12952
|
-
* 卡住了先自己想办法;真不能定的**写下来等用户回来**。
|
|
12953
|
-
*
|
|
12954
|
-
* ## 归属(D23:ACC 管机制与数据,CCC 管措辞与纪律)
|
|
12955
|
-
*
|
|
12956
|
-
* | 面 | 归谁 | 内容 |
|
|
12957
|
-
* |---|---|---|
|
|
12958
|
-
* | **机制** | 🔴 **ACC(本模块)** | 触发判定 / 验证码 / 四选一结构 / 轮上限 / 流水 |
|
|
12959
|
-
* | **不可代批的*具体内容*** | **CCC 纪律** | ACC 只给**通用下限**({@link UNATTENDED_BLOCKED_FLOOR}),CCC 可加严 |
|
|
12960
|
-
*
|
|
12961
|
-
* 🔵 下限之所以落在 ACC(而不是"完全交给 CCC"):**没有这张清单,"自行解决"=把所有把关点自动放行**。
|
|
12962
|
-
* 最终内容仍待 owner 一句(设计 §13-5)——本版先给**保守下限**,CCC 只能在其上加严、不得放宽。
|
|
12963
|
-
*
|
|
12964
|
-
* ## 🔴 核心原则:单一续驱者(single-driver)
|
|
12965
|
-
*
|
|
12966
|
-
* > **一个会话同一时刻只应有一个"续驱者":用户 / CRO / 自排绳 / 无人值守代理。**
|
|
12967
|
-
*
|
|
12968
|
-
* 口径(owner 2026-09-23 19:1x 裁决)= **窄口径:只排除「有 CRO 程序的轨迹」**。
|
|
12969
|
-
* 原「宽口径」(任何自排唤醒都排除)**作废** —— 它会让本机制**永远够不着靠续接绳活着的会话**,
|
|
12970
|
-
* 等于**放弃"代替旧机制"**(绳因此退化为**长周期保险丝**:6–12h 一条)。
|
|
12971
|
-
*
|
|
12972
|
-
* ## 🔴 铁律(与 `cro.ts` / `cro-log.ts` 同族,勿破)
|
|
12973
|
-
*
|
|
12974
|
-
* **本模块的每个导出函数都不抛错**:写盘 / 解析 / 裁剪失败一律返回结构化失败。
|
|
12975
|
-
* ⇒ 代理机制的任何问题**不影响 turn 收尾**、不影响既有链路(唤醒投递 / 输出守卫 / rebuild)。
|
|
12976
|
-
*
|
|
12977
|
-
* ## 🔴 为什么不构成第二真相源(勿删此论证)
|
|
12978
|
-
*
|
|
12979
|
-
* | 载体 | 谁写 | 主语 | 回答 |
|
|
12980
|
-
* |---|---|---|---|
|
|
12981
|
-
* | `<轨迹目录>/unattended-log.json`(**本模块**) | **ACC** | ACC | 「**我注入了什么、判了什么、放过没有**」 |
|
|
12982
|
-
* | `<轨迹目录>/SESSION.md` | **LLM(CCC)** | CCC | 「**这轮发生了什么、为什么这么定**」 |
|
|
12983
|
-
*
|
|
12984
|
-
* **内容不重叠**:本模块只记**机械事实**(轮次 / 验证码 / 判定理由码 / 是否放过),**不写散文**;
|
|
12985
|
-
* LLM 侧的散文与理由归 SESSION.md。完整审计 = 两者合看(本模块 = 注入面|SESSION.md = 判断面)。
|
|
12986
|
-
*
|
|
12987
|
-
* ## 🔴 验证码形态(禁用 4 位字母码)
|
|
12988
|
-
*
|
|
12989
|
-
* 注入文本**自带**"请回复 ABCD" ⇒ 模型**复述指令**即**假完成**(高概率,不是小概率)
|
|
12990
|
-
* ⇒ 用**每轮新生成**的 nonce + **包装 + 独占一行**(`[[UNATTENDED-DONE:<hex>]]`),
|
|
12991
|
-
* 明写「**旧码作废**」,校验**只认当前轮**(防上下文里的旧码复活)。
|
|
12992
|
-
* 先例 = `cheap-worker` background 的 `SERENITY_CHEAP_WORKER_DONE_<16hex>`(同族实现)。
|
|
12993
|
-
*/
|
|
12994
|
-
/** 完成码包装前缀(**独占一行**才有意义;见文件头"禁用 4 位字母码") */
|
|
12995
|
-
const UNATTENDED_DONE_PREFIX = "[[UNATTENDED-DONE:";
|
|
12996
|
-
/** 注入消息的**首行标记**(幂等可辨:模型与事后审计都能一眼看出这是代理消息,不是用户消息) */
|
|
12997
|
-
const UNATTENDED_PROXY_MARK = "[[UNATTENDED-PROXY v1]]";
|
|
12998
|
-
/** 🔴 合成消息**固定身份句**(owner 已裁:「形如用户消息,但首句自报身份与权限边界」) */
|
|
12999
|
-
const UNATTENDED_IDENTITY_LINE = "无人值守代理 · 非本人";
|
|
13000
|
-
/** 生成一枚新 nonce(**每轮一枚**;旧码作废由调用方在文本里声明) */
|
|
13001
|
-
function newNonce() {
|
|
13002
|
-
return randomBytes(8).toString("hex");
|
|
13003
|
-
}
|
|
13004
|
-
/** 完成码全文(注入文本展示用;校验走 {@link hasDoneToken}) */
|
|
13005
|
-
function doneToken(nonce) {
|
|
13006
|
-
return `${UNATTENDED_DONE_PREFIX}${nonce}]]`;
|
|
13007
|
-
}
|
|
13008
|
-
/**
|
|
13009
|
-
* 出现过的完成码 nonce 列表(按出现顺序、去重;**小写归一**)。
|
|
13010
|
-
* 容忍 8–64 位 hex(模型可能手抄多/少几个字符 ⇒ 那就不匹配,属预期:只认本轮那枚)。
|
|
13011
|
-
*/
|
|
13012
|
-
function parseDoneNonces(text) {
|
|
13013
|
-
if (typeof text !== "string" || text === "") return [];
|
|
13014
|
-
const re = /\[\[UNATTENDED-DONE:([0-9a-fA-F]{8,64})\]\]/g;
|
|
13015
|
-
const out = [];
|
|
13016
|
-
let m;
|
|
13017
|
-
while ((m = re.exec(text)) !== null) {
|
|
13018
|
-
const n = (m[1] ?? "").toLowerCase();
|
|
13019
|
-
if (n !== "" && !out.includes(n)) out.push(n);
|
|
13020
|
-
}
|
|
13021
|
-
return out;
|
|
13022
|
-
}
|
|
13023
|
-
/** 触及即停(写待办/交接,**不循环**)——**CCC 的纪律更严则从严** */
|
|
13024
|
-
const UNATTENDED_BLOCKED_FLOOR = [
|
|
13025
|
-
"发布 / 发版",
|
|
13026
|
-
"对外发送(邮件 / 微信 / 对外面输出)",
|
|
13027
|
-
"删除与不可逆操作(rm / 归档 / force push)",
|
|
13028
|
-
"凭据与权限变更",
|
|
13029
|
-
"花钱(外部 API 大额调用 / GPU 长任务)",
|
|
13030
|
-
"代用户拍板(替人做决定)"
|
|
13031
|
-
];
|
|
13032
|
-
/** 四选一正文({@link buildProxyMessage} 与 {@link buildFalseNotifyRebuke} 共用,避免两份措辞漂移) */
|
|
13033
|
-
function proxyBody(input) {
|
|
13034
|
-
return [
|
|
13035
|
-
`A) 已完成、且用户回来会满意 ⇒ 交码 + 附完成凭据(做了什么 / 怎么验的 / 产物路径)`,
|
|
13036
|
-
`B) 已成功向用户发出通知、要停下等他主动回来 ⇒ 交码 + 写明走了哪个通道(**必须真有发送记录**)`,
|
|
13037
|
-
`C) 需要确认但可自决 ⇒ 按「**最保守且可逆**」的选项继续做完(⚠️ **不要交码**,直接做)`,
|
|
13038
|
-
`D) 触及不可代批边界 ⇒ **立即停**,把问题写进待办 / 交接,然后交码`,
|
|
13039
|
-
``,
|
|
13040
|
-
`不可代批的**通用下限**(你所在 CCC 的纪律更严则从严):${(input.blocked ?? UNATTENDED_BLOCKED_FLOOR).join(" / ")}`,
|
|
13041
|
-
``,
|
|
13042
|
-
`交码 = **独占一行、逐字复制下面这一行**(旧码一律作废,只认本轮这个):`,
|
|
13043
|
-
``,
|
|
13044
|
-
` ${doneToken(input.nonce)}`,
|
|
13045
|
-
``,
|
|
13046
|
-
`禁止:把本条当真实用户的授权;禁止把本指令文本复述成回答;禁止什么都没做只回一个码;`,
|
|
13047
|
-
`禁止借机发起**新的长任务**(代理只推进"已在办"的事)。`
|
|
13048
|
-
].join("\n");
|
|
13049
|
-
}
|
|
13050
|
-
/** 组装一次代理注入消息(**形如用户消息,但首句自报身份与权限边界**) */
|
|
13051
|
-
function buildProxyMessage(input) {
|
|
13052
|
-
return [
|
|
13053
|
-
`${UNATTENDED_PROXY_MARK} round=${input.round}/${input.maxRounds} nonce=${input.nonce}`,
|
|
13054
|
-
`${UNATTENDED_IDENTITY_LINE} —— 这条消息是**系统代用户发的**,不是用户本人;我没有任何权限替你拍板。`,
|
|
13055
|
-
``,
|
|
13056
|
-
`用户不在场。你只能以下列**四条之一**收束本轮:`,
|
|
13057
|
-
``,
|
|
13058
|
-
proxyBody(input)
|
|
13059
|
-
].join("\n");
|
|
13060
|
-
}
|
|
13061
|
-
/**
|
|
13062
|
-
* 组装一次**假完成打回**消息(设计 §7 的机械核验分支:声称已通知用户,但本轮无发送记录)。
|
|
13063
|
-
*
|
|
13064
|
-
* ⚠️ 同样带**新 nonce**:它是一次"再来一轮",不是"放过"。
|
|
13065
|
-
*/
|
|
13066
|
-
function buildFalseNotifyRebuke(input) {
|
|
13067
|
-
return [
|
|
13068
|
-
`${UNATTENDED_PROXY_MARK} round=${input.round}/${input.maxRounds} nonce=${input.nonce}`,
|
|
13069
|
-
`${UNATTENDED_IDENTITY_LINE} —— 这条消息是**系统代用户发的**,不是用户本人。`,
|
|
13070
|
-
``,
|
|
13071
|
-
`🔴 你交的码是对的,但**你声称已通知用户,而我这边查不到任何发送记录** ⇒ **不算合法收束**。`,
|
|
13072
|
-
`要么**真的发**(走可用通道;发成之后再交码),要么改走 A / C / D。其余口径同下。`,
|
|
13073
|
-
``,
|
|
13074
|
-
proxyBody(input)
|
|
13075
|
-
].join("\n");
|
|
13076
|
-
}
|
|
13077
|
-
/**
|
|
13078
|
-
* 「收尾文本**声称**已联系用户」的**启发式**判据(设计 §7 的触发器,**不是**判据本身)。
|
|
13079
|
-
*
|
|
13080
|
-
* 🔴 机械判据 = **ACC 侧的发送记录**({@link hasOutboundSendSince});本函数只回答"**要不要去查记录**"。
|
|
13081
|
-
* 误判方向(明标):**误报**(模型换了个说法没被匹配到)⇒ 退回"只认码",不产生假放过;
|
|
13082
|
-
* **误报**(正常措辞被当成声称)⇒ 多打回一轮(有轮上限兜底)。⇒ 偏严,符合设计 §11
|
|
13083
|
-
* 把"谎称已发消息"列为失败模式的取向。
|
|
13084
|
-
*/
|
|
13085
|
-
const NOTIFY_CLAIM_PATTERNS = [
|
|
13086
|
-
/已(经)?(成功)?(向|给|和)?\s*(用户|您|你|他|她|本人|主人)?[^\n。]{0,16}(发送|发出|告知|通知|联系|留言|汇报)/,
|
|
13087
|
-
/\b(?:notified|informed|messaged|contacted)\s+(?:the\s+)?user\b/i,
|
|
13088
|
-
/\b(?:sent|sending)\s+(?:a\s+)?(?:message|notification|notice)\s+to\s+(?:the\s+)?user\b/i,
|
|
13089
|
-
/\bI\s+(?:have\s+)?(?:notified|informed|messaged|contacted)\b/i
|
|
13090
|
-
];
|
|
13091
|
-
/** 收尾文本是否**声称**已联系用户(见 {@link NOTIFY_CLAIM_PATTERNS} 的误判方向说明) */
|
|
13092
|
-
function claimsUserNotification(text) {
|
|
13093
|
-
if (typeof text !== "string" || text === "") return false;
|
|
13094
|
-
return NOTIFY_CLAIM_PATTERNS.some((re) => re.test(text));
|
|
13095
|
-
}
|
|
13096
|
-
/**
|
|
13097
|
-
* **不许开**的会话(设计 §3 的"建议写死",本版**确实写死**):
|
|
13098
|
-
* `skiff-`(F4 问答 / **微信桥会话也是这个前缀** —— 桥那侧的"用户"是**家人**,
|
|
13099
|
-
* 代理替家人说话 = **冒充**)| `acp-`(对外程序化面 —— ⚠️ **2026-10-15 起已成死判据**:
|
|
13100
|
-
* 该前缀从无生产者且面 E 已退役;理由与"为何保留"见 `output-guard-seam.ts` 文件头)|
|
|
13101
|
-
* `cheap-worker-`(自主 worker,已有自己的完成码循环)。
|
|
13102
|
-
*
|
|
13103
|
-
* ⚠️ **`rebuild-` 不在排除面**(与 `output-guard-seam.ts` 的取向**刻意不同**):重建会话是
|
|
13104
|
-
* **同一轨迹的载体**(内部维护面),不是对外面;把它排除会让"重建后就没人推了"。
|
|
13105
|
-
*/
|
|
13106
|
-
function isProxyExcludedSessionId(sessionId) {
|
|
13107
|
-
if (typeof sessionId !== "string" || sessionId === "") return true;
|
|
13108
|
-
return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("cheap-worker-");
|
|
13109
|
-
}
|
|
13110
|
-
/**
|
|
13111
|
-
* 一次判定的**唯一入口**(纯函数)。
|
|
13112
|
-
*
|
|
13113
|
-
* 顺序即语义(R↓,勿随意调换):
|
|
13114
|
-
* 1. **闸/空间/会话资格**(关着、不在 CCC、非主会话、排除面)⇒ skip;
|
|
13115
|
-
* 2. **单一续驱者**:用户刚发起 ⇒ skip(人就在场);该轨迹有 CRO ⇒ skip(程序在驱动);
|
|
13116
|
-
* 3. **合法收束优先于轮上限** —— 最后一轮交的码必须算数(否则等于永远收不了尾);
|
|
13117
|
-
* 4. 交码 + 声称已通知 + **无发送记录** ⇒ 假完成,走下一轮(reason=`false-notify`);
|
|
13118
|
-
* 5. 墙钟 / 轮上限 ⇒ cap(**停下不动**,退回改造前的行为,不产生新风险);
|
|
13119
|
-
* 6. 其余 ⇒ inject 下一轮。
|
|
13120
|
-
*/
|
|
13121
|
-
function decideUnattendedAction(f) {
|
|
13122
|
-
if (!f.enabled) return {
|
|
13123
|
-
action: "skip",
|
|
13124
|
-
reason: "mode-off",
|
|
13125
|
-
round: 0
|
|
13126
|
-
};
|
|
13127
|
-
if (!f.root) return {
|
|
13128
|
-
action: "skip",
|
|
13129
|
-
reason: "not-in-ccc",
|
|
13130
|
-
round: 0
|
|
13131
|
-
};
|
|
13132
|
-
if (!f.isMainSession) return {
|
|
13133
|
-
action: "skip",
|
|
13134
|
-
reason: "not-main-session",
|
|
13135
|
-
round: 0
|
|
13136
|
-
};
|
|
13137
|
-
if (f.sessionIdExcluded) return {
|
|
13138
|
-
action: "skip",
|
|
13139
|
-
reason: "excluded-session",
|
|
13140
|
-
round: 0
|
|
13141
|
-
};
|
|
13142
|
-
if (f.userInitiated) return {
|
|
13143
|
-
action: "skip",
|
|
13144
|
-
reason: "user-initiated",
|
|
13145
|
-
round: 0
|
|
13146
|
-
};
|
|
13147
|
-
if (f.hasCroProgram) return {
|
|
13148
|
-
action: "skip",
|
|
13149
|
-
reason: "cro-programmed",
|
|
13150
|
-
round: 0
|
|
13151
|
-
};
|
|
13152
|
-
const run = f.run;
|
|
13153
|
-
if (run !== null && f.doneNonce !== null && f.doneNonce.toLowerCase() === run.nonce.toLowerCase() && run) {
|
|
13154
|
-
if (f.claimsNotify && !f.outboundSeen) {
|
|
13155
|
-
if (run.round >= 15) return {
|
|
13156
|
-
action: "cap",
|
|
13157
|
-
reason: "round-cap",
|
|
13158
|
-
round: run.round
|
|
13159
|
-
};
|
|
13160
|
-
return {
|
|
13161
|
-
action: "inject",
|
|
13162
|
-
reason: "false-notify",
|
|
13163
|
-
round: run.round + 1
|
|
13164
|
-
};
|
|
13165
|
-
}
|
|
13166
|
-
return {
|
|
13167
|
-
action: "accept",
|
|
13168
|
-
reason: "done-code",
|
|
13169
|
-
round: run.round
|
|
13170
|
-
};
|
|
13171
|
-
}
|
|
13172
|
-
if (run && f.nowMs - run.startedAt > 72e5) return {
|
|
13173
|
-
action: "cap",
|
|
13174
|
-
reason: "wall-clock",
|
|
13175
|
-
round: run.round
|
|
13176
|
-
};
|
|
13177
|
-
if (run && run.round >= 15) return {
|
|
13178
|
-
action: "cap",
|
|
13179
|
-
reason: "round-cap",
|
|
13180
|
-
round: run.round
|
|
13181
|
-
};
|
|
13182
|
-
const round = (run?.round ?? 0) + 1;
|
|
13183
|
-
return {
|
|
13184
|
-
action: "inject",
|
|
13185
|
-
reason: run === null ? "first-stop" : f.doneNonce !== null ? "stale-code" : "no-legal-closure",
|
|
13186
|
-
round
|
|
13187
|
-
};
|
|
13188
|
-
}
|
|
13189
|
-
/** 键 = 载体(dsh 会话 id)——与 `cro-turns.ts` 同款选择(事件本身就以 agent/session 为主语) */
|
|
13190
|
-
const runs = /* @__PURE__ */ new Map();
|
|
13191
|
-
/** 取该会话的代理段(无 ⇒ null) */
|
|
13192
|
-
function getProxyRun(sessionId) {
|
|
13193
|
-
return runs.get(sessionId) ?? null;
|
|
13194
|
-
}
|
|
13195
|
-
/** 开始 / 推进一段代理(`startedAt` 由调用方给出 ⇒ 跨轮保留首次值) */
|
|
13196
|
-
function setProxyRun(sessionId, run) {
|
|
13197
|
-
if (!sessionId) return;
|
|
13198
|
-
runs.set(sessionId, run);
|
|
13199
|
-
}
|
|
13200
|
-
/** 结束该会话的代理段(放过 / 上限 / 用户回来 / 载体销毁) */
|
|
13201
|
-
function clearProxyRun(sessionId) {
|
|
13202
|
-
if (!sessionId) return;
|
|
13203
|
-
runs.delete(sessionId);
|
|
13204
|
-
}
|
|
13205
|
-
/**
|
|
13206
|
-
* 「该载体最近一次由**真实用户**发起的轮次号」(会话 id → turn)。
|
|
13207
|
-
*
|
|
13208
|
-
* ⚠️ 只留**一条**(不是集合):`claimed` 的 `turn` 单调递增,留最新一条即可回答
|
|
13209
|
-
* "本轮是不是用户发起的",且**表的大小 = 会话数**(本容器栽过 5 次"只增不清",此处结构性避免)。
|
|
13210
|
-
*/
|
|
13211
|
-
const userTurns = /* @__PURE__ */ new Map();
|
|
13212
|
-
/** 记下"这个载体的这一轮是真实用户发起的" */
|
|
13213
|
-
function noteUserTurn(sessionId, turn) {
|
|
13214
|
-
if (!sessionId || typeof turn !== "number") return;
|
|
13215
|
-
userTurns.set(sessionId, turn);
|
|
13216
|
-
}
|
|
13217
|
-
/** 该载体的这一轮是否由真实用户发起(判据 = `noteUserTurn` 写入的最新一条) */
|
|
13218
|
-
function isUserTurn(sessionId, turn) {
|
|
13219
|
-
if (!sessionId || typeof turn !== "number") return false;
|
|
13220
|
-
return userTurns.get(sessionId) === turn;
|
|
13221
|
-
}
|
|
13222
|
-
/** 清掉该载体的用户轮次记录(载体销毁时) */
|
|
13223
|
-
function clearUserTurn(sessionId) {
|
|
13224
|
-
if (!sessionId) return;
|
|
13225
|
-
userTurns.delete(sessionId);
|
|
13226
|
-
}
|
|
13227
|
-
/** 清空全部进程内运行态(disposer;**不碰磁盘**) */
|
|
13228
|
-
function clearUnattendedState() {
|
|
13229
|
-
runs.clear();
|
|
13230
|
-
userTurns.clear();
|
|
13231
|
-
outboundSends.clear();
|
|
13232
|
-
}
|
|
13233
|
-
/** 发送记录保留窗(超出即丢:它只服务"这一轮到底发没发"这个瞬时问题) */
|
|
13234
|
-
const UNATTENDED_OUTBOUND_TTL_MS = 216e5;
|
|
13235
|
-
const outboundSends = /* @__PURE__ */ new Map();
|
|
13236
|
-
/**
|
|
13237
|
-
* 记一次**成功的**出站发送(`im-bridge` 工具面在通道返回成功后调用)。
|
|
13238
|
-
*
|
|
13239
|
-
* ⚠️ **覆盖边界(诚实标注)**:只覆盖 **ACC 自己这条通道**(`im-bridge` 的 send / send-file)。
|
|
13240
|
-
* CCC 自有的发送通道(如 `mail-tool` MSM)**ACC 看不见** ⇒ 那类"已通知"落到
|
|
13241
|
-
* {@link hasOutboundSendSince} = false ⇒ 会被判成假完成。⇒ 本版口径:**代理模式下"联系用户"
|
|
13242
|
-
* 只认 ACC 通道**;这与设计 §7 的原话(im-bridge 记日志)一致,边界写在此处以免误读。
|
|
13243
|
-
*/
|
|
13244
|
-
function noteOutboundSend(root, atMs = Date.now()) {
|
|
13245
|
-
if (!root) return;
|
|
13246
|
-
const list = outboundSends.get(root) ?? [];
|
|
13247
|
-
list.push(atMs);
|
|
13248
|
-
const cutoff = atMs - UNATTENDED_OUTBOUND_TTL_MS;
|
|
13249
|
-
const kept = list.filter((t) => t >= cutoff).slice(-20);
|
|
13250
|
-
outboundSends.set(root, kept);
|
|
13251
|
-
}
|
|
13252
|
-
/** 自 `sinceMs` 起该 CCC 是否有过成功出站发送(**只读,不清表**——清理归写入侧的裁剪) */
|
|
13253
|
-
function hasOutboundSendSince(root, sinceMs, nowMs = Date.now()) {
|
|
13254
|
-
if (!root) return false;
|
|
13255
|
-
const list = outboundSends.get(root);
|
|
13256
|
-
if (!list || list.length === 0) return false;
|
|
13257
|
-
const floor = Math.max(sinceMs, nowMs - UNATTENDED_OUTBOUND_TTL_MS);
|
|
13258
|
-
return list.some((t) => t >= floor && t <= nowMs);
|
|
13259
|
-
}
|
|
13260
|
-
/** 固定文件名(与 `cro-wake-log.json` 并排、主语分明:那份记"我叫了什么",这份记"我代说了什么") */
|
|
13261
|
-
const UNATTENDED_LOG_FILENAME = "unattended-log.json";
|
|
13262
|
-
/** 保留窗 = 2 日(与 CRO 流水同值;**常量** ⇒ 无需人工清理动作) */
|
|
13263
|
-
const UNATTENDED_LOG_RETENTION_MS = 1728e5;
|
|
13264
|
-
const UNATTENDED_LOG_VERSION = 1;
|
|
13265
|
-
/** 日志绝对路径 */
|
|
13266
|
-
function unattendedLogPath(root, dirName) {
|
|
13267
|
-
return join(sessionsRoot(root), dirName, UNATTENDED_LOG_FILENAME);
|
|
13268
|
-
}
|
|
13269
|
-
function emptyLog() {
|
|
13270
|
-
return {
|
|
13271
|
-
version: UNATTENDED_LOG_VERSION,
|
|
13272
|
-
entries: []
|
|
13273
|
-
};
|
|
13274
|
-
}
|
|
13275
|
-
/** 条目形状校验(容忍手改:字段缺失/类型不符 ⇒ 丢弃该条,不阻断整档) */
|
|
13276
|
-
function asEntry(value) {
|
|
13277
|
-
const e = value;
|
|
13278
|
-
if (!e || typeof e !== "object") return null;
|
|
13279
|
-
if (typeof e.at !== "string" || e.at === "") return null;
|
|
13280
|
-
if (e.event !== "inject" && e.event !== "accept" && e.event !== "cap" && e.event !== "yield") return null;
|
|
13281
|
-
if (typeof e.reason !== "string") return null;
|
|
13282
|
-
return {
|
|
13283
|
-
at: e.at,
|
|
13284
|
-
event: e.event,
|
|
13285
|
-
round: typeof e.round === "number" ? e.round : 0,
|
|
13286
|
-
nonce: typeof e.nonce === "string" ? e.nonce : null,
|
|
13287
|
-
reason: e.reason,
|
|
13288
|
-
sessionId: typeof e.sessionId === "string" ? e.sessionId : ""
|
|
13289
|
-
};
|
|
13290
|
-
}
|
|
13291
|
-
/** 读取流水(**永不抛**;读坏 ⇒ 当作空档 + 返回 error 文本) */
|
|
13292
|
-
function loadUnattendedLog(root, dirName) {
|
|
13293
|
-
const path = unattendedLogPath(root, dirName);
|
|
13294
|
-
if (!existsSync(path)) return {
|
|
13295
|
-
log: emptyLog(),
|
|
13296
|
-
error: null
|
|
13297
|
-
};
|
|
13298
|
-
try {
|
|
13299
|
-
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
13300
|
-
const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
13301
|
-
const entries = [];
|
|
13302
|
-
for (const item of raw) {
|
|
13303
|
-
const e = asEntry(item);
|
|
13304
|
-
if (e) entries.push(e);
|
|
13305
|
-
}
|
|
13306
|
-
return {
|
|
13307
|
-
log: {
|
|
13308
|
-
version: UNATTENDED_LOG_VERSION,
|
|
13309
|
-
entries
|
|
13310
|
-
},
|
|
13311
|
-
error: null
|
|
13312
|
-
};
|
|
13313
|
-
} catch (err) {
|
|
13314
|
-
return {
|
|
13315
|
-
log: emptyLog(),
|
|
13316
|
-
error: `无人值守流水解析失败(${String(err?.message ?? err)})`
|
|
13317
|
-
};
|
|
13318
|
-
}
|
|
13319
|
-
}
|
|
13320
|
-
/**
|
|
13321
|
-
* 按窗裁剪(**纯函数** ⇒ 可穷举测试)。
|
|
13322
|
-
* 🔴 `at` 不可解析者**一律丢弃**并在返回值里报数(`dropped`)——**不静默吞掉**(同 `cro-log.ts` 纪律)。
|
|
13323
|
-
*/
|
|
13324
|
-
function pruneUnattendedLogEntries(entries, nowMs, windowMs = UNATTENDED_LOG_RETENTION_MS) {
|
|
13325
|
-
const floor = nowMs - windowMs;
|
|
13326
|
-
const kept = [];
|
|
13327
|
-
let pruned = 0;
|
|
13328
|
-
let dropped = 0;
|
|
13329
|
-
for (const e of entries) {
|
|
13330
|
-
const t = Date.parse(e.at);
|
|
13331
|
-
if (Number.isNaN(t)) {
|
|
13332
|
-
dropped += 1;
|
|
13333
|
-
continue;
|
|
13334
|
-
}
|
|
13335
|
-
if (t >= floor) kept.push(e);
|
|
13336
|
-
else pruned += 1;
|
|
13337
|
-
}
|
|
13338
|
-
return {
|
|
13339
|
-
kept,
|
|
13340
|
-
pruned,
|
|
13341
|
-
dropped
|
|
13342
|
-
};
|
|
13343
|
-
}
|
|
13344
|
-
/**
|
|
13345
|
-
* 追加一条流水并按窗裁剪(**本模块的落盘入口;永不抛**)。
|
|
13346
|
-
*
|
|
13347
|
-
* 顺序 = 读 → 追加 → 裁剪 → **原子写**(tmp + rename)。任一步失败 ⇒ `{ok:false, error}`,**不抛**
|
|
13348
|
-
* ——调用方只记一行 console 日志 ⇒ **代理机制的问题绝不影响 turn 收尾**。
|
|
13349
|
-
*/
|
|
13350
|
-
function appendUnattendedLog(root, dirName, entry, nowMs = Date.now()) {
|
|
13351
|
-
if (!root || !dirName) return {
|
|
13352
|
-
ok: false,
|
|
13353
|
-
error: "缺少 CCC 根或轨迹目录名",
|
|
13354
|
-
kept: 0
|
|
13355
|
-
};
|
|
13356
|
-
const path = unattendedLogPath(root, dirName);
|
|
13357
|
-
try {
|
|
13358
|
-
const loaded = loadUnattendedLog(root, dirName);
|
|
13359
|
-
const full = {
|
|
13360
|
-
at: isoLocal(nowMs),
|
|
13361
|
-
...entry
|
|
13362
|
-
};
|
|
13363
|
-
const { kept } = pruneUnattendedLogEntries([...loaded.log.entries, full], nowMs);
|
|
13364
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
13365
|
-
const tmp = `${path}.tmp`;
|
|
13366
|
-
writeFileSync(tmp, `${JSON.stringify({
|
|
13367
|
-
version: UNATTENDED_LOG_VERSION,
|
|
13368
|
-
entries: kept
|
|
13369
|
-
}, null, 2)}\n`, "utf-8");
|
|
13370
|
-
renameSync(tmp, path);
|
|
13371
|
-
return {
|
|
13372
|
-
ok: true,
|
|
13373
|
-
error: null,
|
|
13374
|
-
kept: kept.length
|
|
13375
|
-
};
|
|
13376
|
-
} catch (err) {
|
|
13377
|
-
return {
|
|
13378
|
-
ok: false,
|
|
13379
|
-
error: `无人值守流水写入失败(${String(err?.message ?? err)})`,
|
|
13380
|
-
kept: 0
|
|
13381
|
-
};
|
|
13382
|
-
}
|
|
13383
|
-
}
|
|
13384
|
-
//#endregion
|
|
13385
12944
|
//#region src/im-bridge.ts
|
|
13386
12945
|
/**
|
|
13387
12946
|
* im-bridge.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
|
|
@@ -13532,19 +13091,17 @@ async function runImBridge(root, req) {
|
|
|
13532
13091
|
const resolved = channel.resolveUser(root, rawUser);
|
|
13533
13092
|
if (!resolved) return fail("ALIAS_UNKNOWN", `unknown user "${rawUser}"`);
|
|
13534
13093
|
try {
|
|
13535
|
-
const sent = await channel.send({
|
|
13536
|
-
root,
|
|
13537
|
-
userId: resolved.id,
|
|
13538
|
-
text,
|
|
13539
|
-
accountId: req.account
|
|
13540
|
-
});
|
|
13541
|
-
noteOutboundSend(root);
|
|
13542
13094
|
return {
|
|
13543
13095
|
ok: true,
|
|
13544
13096
|
channel: channelId,
|
|
13545
13097
|
action,
|
|
13546
13098
|
detail: {
|
|
13547
|
-
...
|
|
13099
|
+
...await channel.send({
|
|
13100
|
+
root,
|
|
13101
|
+
userId: resolved.id,
|
|
13102
|
+
text,
|
|
13103
|
+
accountId: req.account
|
|
13104
|
+
}),
|
|
13548
13105
|
user: resolved.alias ?? resolved.id
|
|
13549
13106
|
}
|
|
13550
13107
|
};
|
|
@@ -13563,21 +13120,19 @@ async function runImBridge(root, req) {
|
|
|
13563
13120
|
const file = readCccFile(root, fileArg);
|
|
13564
13121
|
if (!file.ok) return fail(file.code, file.error);
|
|
13565
13122
|
try {
|
|
13566
|
-
const sent = await channel.sendFile({
|
|
13567
|
-
root,
|
|
13568
|
-
userId: resolved.id,
|
|
13569
|
-
data: file.data,
|
|
13570
|
-
fileName: file.fileName,
|
|
13571
|
-
caption: req.caption,
|
|
13572
|
-
accountId: req.account
|
|
13573
|
-
});
|
|
13574
|
-
noteOutboundSend(root);
|
|
13575
13123
|
return {
|
|
13576
13124
|
ok: true,
|
|
13577
13125
|
channel: channelId,
|
|
13578
13126
|
action,
|
|
13579
13127
|
detail: {
|
|
13580
|
-
...
|
|
13128
|
+
...await channel.sendFile({
|
|
13129
|
+
root,
|
|
13130
|
+
userId: resolved.id,
|
|
13131
|
+
data: file.data,
|
|
13132
|
+
fileName: file.fileName,
|
|
13133
|
+
caption: req.caption,
|
|
13134
|
+
accountId: req.account
|
|
13135
|
+
}),
|
|
13581
13136
|
user: resolved.alias ?? resolved.id
|
|
13582
13137
|
}
|
|
13583
13138
|
};
|
|
@@ -14011,7 +13566,7 @@ function forgetExclusiveToolsVisibility(sessionId) {
|
|
|
14011
13566
|
*
|
|
14012
13567
|
* 🔴 **为什么需要它(缺陷根因)**:工具是 `apply()` 里 `ctx.tools.register(...)` 注册到**宿主级**的
|
|
14013
13568
|
* ⇒ 它们出现在**每一个**会话的工具清单里,**与会话 cwd 无关**。其余各缝(身份注入 / guards /
|
|
14014
|
-
* keeper / output-guard /
|
|
13569
|
+
* keeper / output-guard / bootstrap)都已有 `if (!root) return` 这道门,
|
|
14015
13570
|
* **唯独"工具可见性"这一层没有** ⇒ 在非 CCC 目录开会话时,模型仍看得见这 14 个工具,
|
|
14016
13571
|
* 于是会去调 —— 调了才抛 `No CCC found`。**那是运行时报错,不是事先不可见**。
|
|
14017
13572
|
*
|
|
@@ -20338,8 +19893,10 @@ function isExternalFaceSession(sessionId) {
|
|
|
20338
19893
|
return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
|
|
20339
19894
|
}
|
|
20340
19895
|
/** 读会话最后一个 assistant/message 文本(skiff-core 同款)
|
|
20341
|
-
* **
|
|
20342
|
-
*
|
|
19896
|
+
* **v1.63.3 起唯一的导出理由消失**:原导出是给 `unattended-seam.ts` 复用的(D77),
|
|
19897
|
+
* 该文件已随无人值守功能整条退役 ⇒ 现只有本模块自己用(`isLeak` 的调用点)。
|
|
19898
|
+
* ⚠️ 保持 `export` 无害(它有测试覆盖,且将来"同一读法只留一份"的纪律仍成立),
|
|
19899
|
+
* 但**不要**再写成"为了 X 复用才导出"——那个 X 不存在了。
|
|
20343
19900
|
* **v1.27.7:剥离 think 块再检测**(用户:"不检查think内容(因为不输出)")——
|
|
20344
19901
|
* ` think…` 是模型思考过程,不进入用户可见输出;敏感词检测只针对**最终呈现文本**,
|
|
20345
19902
|
* 否则 think 内提及内部机制词(思考过程必然推演机制)会误打回。复用 stripThink
|
|
@@ -20398,235 +19955,6 @@ function registerOutputGuardHook(ctx) {
|
|
|
20398
19955
|
});
|
|
20399
19956
|
}
|
|
20400
19957
|
//#endregion
|
|
20401
|
-
//#region src/unattended-seam.ts
|
|
20402
|
-
/**
|
|
20403
|
-
* unattended-seam.ts — 无人值守代理回复的**拦截缝**(S142 **D77**,owner 2026-09-23 令「我需要的是一个机制」)
|
|
20404
|
-
*
|
|
20405
|
-
* ## 停在哪、怎么接(设计 §4 的裁决:**A 的通道 + B 的措辞**)
|
|
20406
|
-
*
|
|
20407
|
-
* 复用的是**已验证过的**通道——`agent/turn-stopping`(serial)里 `agent.steer()`:
|
|
20408
|
-
* 宿主语义 = "**listener 反对就打回**,机器重读 inbox:有新 steer ⇒ 再跑一步,没有 ⇒ 关轮"
|
|
20409
|
-
* (`host/contract.ts` 的 `HOST_EVENTS` 原文)。因此:
|
|
20410
|
-
* · **轮不结束**(语义正是"有人打回")⇒ **零新机械**;
|
|
20411
|
-
* · 文本**写成代理用户的话**(B 的措辞)+ **首句自报身份**(owner 已裁)。
|
|
20412
|
-
*
|
|
20413
|
-
* 模板 = `output-guard-seam.ts`(同一缝位、同一 `createUserMessage + PLUGIN_SOURCE` 形态)。
|
|
20414
|
-
*
|
|
20415
|
-
* ## 🔴「这轮是不是真实用户发起的」怎么判(本机制**唯一**的新机械;2026-09-23 取证)
|
|
20416
|
-
*
|
|
20417
|
-
* 交接块把它列为"动手前必须先查清的一件事",取证结论(读宿主类型面,非猜测):
|
|
20418
|
-
*
|
|
20419
|
-
* | 事实 | 出处 |
|
|
20420
|
-
* |---|---|
|
|
20421
|
-
* | 每条进入 inbox 的消息都发 `agent/inbox/inserted { agent, message }`;**在轮内离开 inbox 时**发 `agent/inbox/claimed { agent, message, turn }` | `@deepseek-ai/dsh-agent` README(Inbox 段)+ `runtime-types.d.ts` |
|
|
20422
|
-
* | `claimed.turn` = **拥有该消息的那一轮**;`turn-stopping.turn` = **即将关闭的那一轮** ⇒ 两者同编号可直接比对 | `runtime-types.d.ts` 同一文件两处 `turn` 声明 |
|
|
20423
|
-
* | 消息来源可辨:`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`(**唯一**声明处与常量处) |
|
|
20424
|
-
*
|
|
20425
|
-
* ⇒ **判据 = 「这一轮 claim 到的消息里,有没有 `source.kind === 'user'`」**。
|
|
20426
|
-
* 没有现成信号时本可退化为"记账法"(我们知道自己投过什么),**但不必**:宿主已给出一手信号。
|
|
20427
|
-
*
|
|
20428
|
-
* ## 🔴 为什么"用户发起的轮"要跳过(这是设计 §3-③ 的本意,别当成漏洞)
|
|
20429
|
-
*
|
|
20430
|
-
* 用户**刚说完话**⇒ 人就在跟前,代理插话既是冒充也是浪费。**"用户说完走开"那种情形**
|
|
20431
|
-
* 由**绳(长周期保险丝)/其它非用户发起的唤醒**覆盖:那条轮**不是**用户发起的 ⇒ 代理接手,
|
|
20432
|
-
* 一路推进到**合法收束**。⇒ 两个机制合起来才等于"用户走开后会话还能自己跑完"。
|
|
20433
|
-
*
|
|
20434
|
-
* ## 🔴 铁律(与 `unattended-ops.ts` 同族)
|
|
20435
|
-
*
|
|
20436
|
-
* **本模块的任何失败都不得影响 turn 收尾**:所有 handler 包 try/catch,异常只记一行 console。
|
|
20437
|
-
*/
|
|
20438
|
-
/** 取载体 id(payload 的 `agent` 面;取不到 ⇒ 空串) */
|
|
20439
|
-
function sessionIdOf$2(agent) {
|
|
20440
|
-
const id = agent?.session?.id;
|
|
20441
|
-
return typeof id === "string" ? id : "";
|
|
20442
|
-
}
|
|
20443
|
-
/** 取会话对象(payload 的 `agent.session`) */
|
|
20444
|
-
function sessionOf(agent) {
|
|
20445
|
-
return agent?.session;
|
|
20446
|
-
}
|
|
20447
|
-
/** 取会话 cwd(`header.cwd`;取不到回落进程 cwd——与 `output-guard-seam.ts` 同款读法) */
|
|
20448
|
-
function cwdOf(agent) {
|
|
20449
|
-
return sessionOf(agent)?.header?.cwd ?? process.cwd();
|
|
20450
|
-
}
|
|
20451
|
-
/** 是否 **CCC 内主会话**(`delegationDepth` 缺省 0;子代理 / 无头运行不算) */
|
|
20452
|
-
function isMainSession(agent) {
|
|
20453
|
-
return (sessionOf(agent)?.header?.delegationDepth ?? 0) === 0;
|
|
20454
|
-
}
|
|
20455
|
-
/**
|
|
20456
|
-
* 该会话所属轨迹**是否带 CRO 程序**(窄口径的唯一排除项)。
|
|
20457
|
-
* 判据 = 轨迹目录下存在 `continuous-re-occurrence.ts`(与 `cro.ts` 同源:**文件在 = 启用**)。
|
|
20458
|
-
* 无绑定(未挂在任何轨迹上)⇒ false(没有程序在驱动它)。
|
|
20459
|
-
*/
|
|
20460
|
-
function hasCroProgram(session, root) {
|
|
20461
|
-
const dirName = readLastBound(session)?.dirName;
|
|
20462
|
-
if (!dirName) return false;
|
|
20463
|
-
return existsSync(join(sessionsRoot(root), dirName, CRO_FILENAME));
|
|
20464
|
-
}
|
|
20465
|
-
/** 落一条流水(**永不抛**;无绑定 ⇒ 无落点,只回 false 由调用方记 console) */
|
|
20466
|
-
function logEvent(session, root, entry, nowMs) {
|
|
20467
|
-
const dirName = readLastBound(session)?.dirName;
|
|
20468
|
-
if (!dirName) return false;
|
|
20469
|
-
const res = appendUnattendedLog(root, dirName, entry, nowMs);
|
|
20470
|
-
if (!res.ok) console.log(`[serenity-hooks] unattended: 流水未落盘 — ${res.error ?? "未知原因"}`);
|
|
20471
|
-
return res.ok;
|
|
20472
|
-
}
|
|
20473
|
-
/**
|
|
20474
|
-
* 一轮收尾(`agent/turn-stopping`)的完整处置。
|
|
20475
|
-
*
|
|
20476
|
-
* 顺序:**取事实 → 纯函数判定 → 执行**(判定全在 `unattended-ops.ts`,本函数只做 I/O 与 steer)。
|
|
20477
|
-
* @returns 便于用例观察的判定结果文本(生产不使用返回值)
|
|
20478
|
-
*/
|
|
20479
|
-
function handleTurnStopping(agent, turn) {
|
|
20480
|
-
if (readSimpleSettings().unattendedEnabled !== true) return "skip:mode-off";
|
|
20481
|
-
const sessionId = sessionIdOf$2(agent);
|
|
20482
|
-
if (sessionId === "") return "skip:no-session-id";
|
|
20483
|
-
const root = cccRootForCwd(cwdOf(agent));
|
|
20484
|
-
if (root === null) return "skip:not-in-ccc";
|
|
20485
|
-
const session = sessionOf(agent);
|
|
20486
|
-
const nowMs = Date.now();
|
|
20487
|
-
const run = getProxyRun(sessionId);
|
|
20488
|
-
const text = lastAssistantText(agent);
|
|
20489
|
-
const nonces = parseDoneNonces(text);
|
|
20490
|
-
const sessionIdExcluded = isProxyExcludedSessionId(sessionId);
|
|
20491
|
-
const userInitiated = turn === null ? true : isUserTurn(sessionId, turn);
|
|
20492
|
-
const cro = !sessionIdExcluded && !userInitiated && isMainSession(agent) ? hasCroProgram(session, root) : false;
|
|
20493
|
-
const verdict = decideUnattendedAction({
|
|
20494
|
-
enabled: true,
|
|
20495
|
-
root,
|
|
20496
|
-
isMainSession: isMainSession(agent),
|
|
20497
|
-
sessionIdExcluded,
|
|
20498
|
-
userInitiated,
|
|
20499
|
-
hasCroProgram: cro,
|
|
20500
|
-
run,
|
|
20501
|
-
nowMs,
|
|
20502
|
-
doneNonce: nonces.length > 0 ? nonces[nonces.length - 1] ?? null : null,
|
|
20503
|
-
claimsNotify: claimsUserNotification(text),
|
|
20504
|
-
outboundSeen: run !== null && hasOutboundSendSince(root, run.startedAt, nowMs)
|
|
20505
|
-
});
|
|
20506
|
-
if (verdict.action === "skip") return `skip:${verdict.reason}`;
|
|
20507
|
-
if (verdict.action === "accept" || verdict.action === "cap") {
|
|
20508
|
-
clearProxyRun(sessionId);
|
|
20509
|
-
logEvent(session, root, {
|
|
20510
|
-
event: verdict.action,
|
|
20511
|
-
round: verdict.round,
|
|
20512
|
-
nonce: run?.nonce ?? null,
|
|
20513
|
-
reason: verdict.reason,
|
|
20514
|
-
sessionId
|
|
20515
|
-
}, nowMs);
|
|
20516
|
-
console.log(`[serenity-hooks] unattended: ${verdict.action} (${verdict.reason}, round=${verdict.round}, session=${sessionId})`);
|
|
20517
|
-
return `${verdict.action}:${verdict.reason}`;
|
|
20518
|
-
}
|
|
20519
|
-
const nonce = newNonce();
|
|
20520
|
-
const startedAt = run?.startedAt ?? nowMs;
|
|
20521
|
-
setProxyRun(sessionId, {
|
|
20522
|
-
round: verdict.round,
|
|
20523
|
-
nonce,
|
|
20524
|
-
startedAt
|
|
20525
|
-
});
|
|
20526
|
-
const message = verdict.reason === "false-notify" ? buildFalseNotifyRebuke({
|
|
20527
|
-
round: verdict.round,
|
|
20528
|
-
maxRounds: 15,
|
|
20529
|
-
nonce
|
|
20530
|
-
}) : buildProxyMessage({
|
|
20531
|
-
round: verdict.round,
|
|
20532
|
-
maxRounds: 15,
|
|
20533
|
-
nonce
|
|
20534
|
-
});
|
|
20535
|
-
try {
|
|
20536
|
-
agent.steer(createUserMessage({
|
|
20537
|
-
content: [{
|
|
20538
|
-
type: "text",
|
|
20539
|
-
text: message
|
|
20540
|
-
}],
|
|
20541
|
-
source: PLUGIN_SOURCE
|
|
20542
|
-
}));
|
|
20543
|
-
} catch (error) {
|
|
20544
|
-
clearProxyRun(sessionId);
|
|
20545
|
-
logEvent(session, root, {
|
|
20546
|
-
event: "cap",
|
|
20547
|
-
round: verdict.round,
|
|
20548
|
-
nonce,
|
|
20549
|
-
reason: "steer-failed",
|
|
20550
|
-
sessionId
|
|
20551
|
-
}, nowMs);
|
|
20552
|
-
console.warn(`[serenity-hooks] unattended: steer 失败(本轮放弃) — ${String(error?.message ?? error)}`);
|
|
20553
|
-
return "cap:steer-failed";
|
|
20554
|
-
}
|
|
20555
|
-
logEvent(session, root, {
|
|
20556
|
-
event: "inject",
|
|
20557
|
-
round: verdict.round,
|
|
20558
|
-
nonce,
|
|
20559
|
-
reason: verdict.reason,
|
|
20560
|
-
sessionId
|
|
20561
|
-
}, nowMs);
|
|
20562
|
-
console.log(`[serenity-hooks] unattended: inject round=${verdict.round}/15 (${verdict.reason}, session=${sessionId})`);
|
|
20563
|
-
return `inject:${verdict.reason}`;
|
|
20564
|
-
}
|
|
20565
|
-
/**
|
|
20566
|
-
* 装配无人值守代理缝(`index.ts` apply 调用)。
|
|
20567
|
-
*
|
|
20568
|
-
* 订阅三个事件(**都已在 `host/contract.ts` 白名单里**,新增的 `agent/inbox/claimed` 同批登记):
|
|
20569
|
-
* · `agent/inbox/claimed` —— 记「真实用户发起的轮次」;用户回来时**让位**(清代理段 + 记流水);
|
|
20570
|
-
* · `agent/turn-stopping` —— 判定 + 注入(本机制主体);
|
|
20571
|
-
* · `agent/disposed` / `session/disposed` —— 载体销毁清表(**本容器栽过 5 次"只增不清"**)。
|
|
20572
|
-
*/
|
|
20573
|
-
function registerUnattendedSeam(ctx) {
|
|
20574
|
-
const on = (event, handler) => {
|
|
20575
|
-
try {
|
|
20576
|
-
ctx.on(event, handler);
|
|
20577
|
-
} catch (err) {
|
|
20578
|
-
console.log(`[serenity-hooks] ✗ 无人值守代理未装配(${event} 不可用): ${String(err?.message ?? err)}`);
|
|
20579
|
-
}
|
|
20580
|
-
};
|
|
20581
|
-
on("agent/inbox/claimed", (payload) => {
|
|
20582
|
-
try {
|
|
20583
|
-
const p = payload;
|
|
20584
|
-
const sessionId = sessionIdOf$2(p?.agent);
|
|
20585
|
-
if (sessionId === "") return;
|
|
20586
|
-
if (p.message?.source?.kind !== "user") return;
|
|
20587
|
-
noteUserTurn(sessionId, (typeof p.turn === "number" ? p.turn : null) ?? -1);
|
|
20588
|
-
const run = getProxyRun(sessionId);
|
|
20589
|
-
if (!run) return;
|
|
20590
|
-
clearProxyRun(sessionId);
|
|
20591
|
-
const root = cccRootForCwd(cwdOf(p.agent));
|
|
20592
|
-
if (root) logEvent(sessionOf(p.agent), root, {
|
|
20593
|
-
event: "yield",
|
|
20594
|
-
round: run.round,
|
|
20595
|
-
nonce: run.nonce,
|
|
20596
|
-
reason: "user-returned",
|
|
20597
|
-
sessionId
|
|
20598
|
-
}, Date.now());
|
|
20599
|
-
console.log(`[serenity-hooks] unattended: yield — 用户回来了 (session=${sessionId}, round=${run.round})`);
|
|
20600
|
-
} catch (err) {
|
|
20601
|
-
console.log(`[serenity-hooks] unattended: claimed 观察失败 — ${String(err?.message ?? err)}`);
|
|
20602
|
-
}
|
|
20603
|
-
});
|
|
20604
|
-
on("agent/turn-stopping", (payload) => {
|
|
20605
|
-
try {
|
|
20606
|
-
const p = payload;
|
|
20607
|
-
if (!p?.agent) return;
|
|
20608
|
-
handleTurnStopping(p.agent, typeof p.turn === "number" ? p.turn : null);
|
|
20609
|
-
} catch (err) {
|
|
20610
|
-
console.log(`[serenity-hooks] unattended: turn-stopping 处置失败(已忽略) — ${String(err?.message ?? err)}`);
|
|
20611
|
-
}
|
|
20612
|
-
});
|
|
20613
|
-
on("agent/disposed", (payload) => {
|
|
20614
|
-
const sessionId = sessionIdOf$2(payload?.agent);
|
|
20615
|
-
if (sessionId === "") return;
|
|
20616
|
-
clearProxyRun(sessionId);
|
|
20617
|
-
clearUserTurn(sessionId);
|
|
20618
|
-
});
|
|
20619
|
-
on("session/disposed", (payload) => {
|
|
20620
|
-
const id = payload?.id;
|
|
20621
|
-
if (typeof id !== "string" || id === "") return;
|
|
20622
|
-
clearProxyRun(id);
|
|
20623
|
-
clearUserTurn(id);
|
|
20624
|
-
});
|
|
20625
|
-
registerDisposer(ctx, "unattended proxy", () => {
|
|
20626
|
-
clearUnattendedState();
|
|
20627
|
-
});
|
|
20628
|
-
}
|
|
20629
|
-
//#endregion
|
|
20630
19958
|
//#region src/human-channel.ts
|
|
20631
19959
|
/**
|
|
20632
19960
|
* human-channel.ts — 人机信道的**机制层**(机器级;2026-09-26 owner 定案 ⇒ 方案 §2.6)
|
|
@@ -24901,7 +24229,6 @@ function renderClock(label, s) {
|
|
|
24901
24229
|
const parts = [
|
|
24902
24230
|
`armed=${s.armed}`,
|
|
24903
24231
|
`CRO 闸=${readSimpleSettings().croEnabled !== false}`,
|
|
24904
|
-
`无人值守闸=${readSimpleSettings().unattendedEnabled === true}`,
|
|
24905
24232
|
`tick 次数=${s.ticks}`,
|
|
24906
24233
|
`上次 tick=${ts(s.lastTickAt)}`
|
|
24907
24234
|
];
|
|
@@ -26022,7 +25349,6 @@ const Config = z.object({
|
|
|
26022
25349
|
skiffEnabled: z.boolean().volatile(),
|
|
26023
25350
|
skiffDebugPort: z.number().min(1024).max(65535).volatile(),
|
|
26024
25351
|
croEnabled: z.boolean().volatile(),
|
|
26025
|
-
unattendedEnabled: z.boolean().volatile(),
|
|
26026
25352
|
diagramEnabled: z.boolean().volatile(),
|
|
26027
25353
|
acpFaceEnabled: z.boolean().volatile(),
|
|
26028
25354
|
acpFacePort: z.number().min(1024).max(65535).volatile()
|
|
@@ -26080,7 +25406,6 @@ function apply(ctx, config) {
|
|
|
26080
25406
|
registerGateway(ctx);
|
|
26081
25407
|
registerRebuildTurnHook(ctx);
|
|
26082
25408
|
registerOutputGuardHook(ctx);
|
|
26083
|
-
registerUnattendedSeam(ctx);
|
|
26084
25409
|
if (config.env) registerEnv(ctx);
|
|
26085
25410
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
26086
25411
|
registerBootstrap(ctx);
|