@shgroup/dsh-serenity-hooks 1.45.1 → 1.47.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 +3 -3
- package/dsh.plugin.json +1 -1
- package/lib/client.js +45 -34
- package/lib/cro-turns.d.ts +3 -3
- 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 +1009 -75
- package/lib/{trajectory-bound-BPWKkzJC.js → message-source-Boc-AbHs.js} +7 -1
- package/lib/message-source.d.ts +28 -0
- 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/skills-discovery.d.ts +5 -3
- package/lib/trajectory-skills.d.ts +3 -1
- package/lib/unattended-ops.d.ts +247 -0
- package/lib/unattended-seam.d.ts +49 -0
- package/lib/usage-stats.d.ts +128 -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";
|
|
@@ -1915,14 +1915,16 @@ function waitAgentIdle(ctx, agent) {
|
|
|
1915
1915
|
/**
|
|
1916
1916
|
* skills-discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
|
|
1917
1917
|
*
|
|
1918
|
-
*
|
|
1918
|
+
* 发现全部入口技能(原文全量,不截断)—— **现行三个来源**(原为四个,**来源 3 已于 2026-09-21 删除**):
|
|
1919
1919
|
* 0. **`.serenity` 记号文件内容 = 顶层入口 skill 名**(CCC 记号文件的权威语义,
|
|
1920
1920
|
* tiangong-serenity 的 .serenity 内容为 `tg-serenity`)—— 最高优先
|
|
1921
1921
|
* 1. `.dsh/entry-skill` 指针文件(内容 = skill 名)—— 兼容旧约定
|
|
1922
1922
|
* 2. `.opencode/skills/*-serenity/SKILL.md` —— 自动扫描该 CCC 的顶层入口
|
|
1923
1923
|
* (home-serenity / tg-serenity / pangu-serenity …,命名模式 `*-serenity`)
|
|
1924
|
-
* 3. `.dsh/skills/*-serenity/SKILL.md
|
|
1925
|
-
*
|
|
1924
|
+
* ~~3. `.dsh/skills/*-serenity/SKILL.md`(扫 ACC 装进 CCC 的技能副本)~~ —— 🔴 **已删**:
|
|
1925
|
+
* owner 2026-09-21 令「连模板和安装命令一起删」(B 案)。**判据、实测证据与日期逐字记在
|
|
1926
|
+
* `findEntrySkills()` 体内那一段注释里**——本头部只留结论,**不复述判据**(避免两处会漂的真相源)。
|
|
1927
|
+
* 按名去重;顺序 = 记号文件 → 指针 → opencode 入口。
|
|
1926
1928
|
*
|
|
1927
1929
|
* 任何 CCC 都能自动注入其顶层入口 skill 全文(不硬编码名字)。
|
|
1928
1930
|
*/
|
|
@@ -1991,7 +1993,6 @@ function findEntrySkills(root) {
|
|
|
1991
1993
|
}
|
|
1992
1994
|
}
|
|
1993
1995
|
for (const dir of scanSerenityDirs(resolve(root, ".opencode", "skills"))) add(join(dir, "SKILL.md"), "opencode");
|
|
1994
|
-
for (const dir of scanSerenityDirs(resolve(root, ".dsh", "skills"))) add(join(dir, "SKILL.md"), "dsh");
|
|
1995
1996
|
return out;
|
|
1996
1997
|
}
|
|
1997
1998
|
function truncateContent(content, maxChars) {
|
|
@@ -2153,11 +2154,13 @@ function mergeSkillNames(cccSkills, trajSkills) {
|
|
|
2153
2154
|
* @param mdPath 轨迹的 SESSION.md 路径(**来自绑定的 `mdPath`**,不用 label 拼)
|
|
2154
2155
|
* @param maxChars 总长上限(字符)
|
|
2155
2156
|
* @param cccSkills CCC 级声明(`readTrajectorySkills`;缺省空 = 只有轨迹级,行为与 v1.43 一致)
|
|
2157
|
+
* @param onInjectedNames 观测回调(本次请求实际带上的 skill 名;**纯观测**,供用量统计用——
|
|
2158
|
+
* 回调抛错被吞,**不影响注入正文**)
|
|
2156
2159
|
* @returns 注入正文;两条来源皆无声明 → `''`(空串 = 宿主不产出该 section 块);
|
|
2157
2160
|
* SESSION.md 缺失/读失败 → 响亮提示(若同时有 CCC 级声明,则提示在前、正文随后——
|
|
2158
2161
|
* **提示不因"别处有内容"而被吞掉**)
|
|
2159
2162
|
*/
|
|
2160
|
-
function buildTrajectorySkillsSection(root, mdPath, maxChars = TRAJECTORY_SKILLS_MAX_CHARS, cccSkills = []) {
|
|
2163
|
+
function buildTrajectorySkillsSection(root, mdPath, maxChars = TRAJECTORY_SKILLS_MAX_CHARS, cccSkills = [], onInjectedNames) {
|
|
2161
2164
|
const abs = absoluteMdPath(root, mdPath);
|
|
2162
2165
|
let trajNames = [];
|
|
2163
2166
|
let notice = "";
|
|
@@ -2169,6 +2172,9 @@ function buildTrajectorySkillsSection(root, mdPath, maxChars = TRAJECTORY_SKILLS
|
|
|
2169
2172
|
}
|
|
2170
2173
|
const names = mergeSkillNames(cccSkills, trajNames);
|
|
2171
2174
|
if (names.length === 0) return notice;
|
|
2175
|
+
if (onInjectedNames) try {
|
|
2176
|
+
onInjectedNames(names);
|
|
2177
|
+
} catch {}
|
|
2172
2178
|
const blocks = [];
|
|
2173
2179
|
for (const name of names) {
|
|
2174
2180
|
if (!isSafeSkillName(name)) {
|
|
@@ -2189,6 +2195,274 @@ function buildTrajectorySkillsSection(root, mdPath, maxChars = TRAJECTORY_SKILLS
|
|
|
2189
2195
|
const body = blocks.join("\n\n");
|
|
2190
2196
|
return truncateContent(notice ? `${notice}\n\n${body}` : body, maxChars);
|
|
2191
2197
|
}
|
|
2198
|
+
//#endregion
|
|
2199
|
+
//#region src/usage-stats.ts
|
|
2200
|
+
/**
|
|
2201
|
+
* usage-stats.ts — ACC 用量统计(**skill 加载次数** + **MSM 执行次数**;落该 CCC 的 `_tmp`)
|
|
2202
|
+
*
|
|
2203
|
+
* ## 所有者令(本模块的存在理由)
|
|
2204
|
+
*
|
|
2205
|
+
* 「我需要对 ACC 增加一个统计功能,包括 **skill 加载次数统计,msm 执行次数统计**;
|
|
2206
|
+
* 要求这个统计**自动生成在 CCC 的 `_tmp` 内的指定文件**」
|
|
2207
|
+
* +(对齐时确认)「**skill 加载**两个口径**都记、用字段区分**」「**只记名字、不记参数**」
|
|
2208
|
+
* 「**重启后读旧文件续算**」。
|
|
2209
|
+
*
|
|
2210
|
+
* ## 它解决什么麻烦(白话)
|
|
2211
|
+
*
|
|
2212
|
+
* 在此之前,「**哪些 skill 真的被加载过、哪些 MSM 真的在跑**」在 ACC 侧**一个数都没有** ——
|
|
2213
|
+
* 要判断「这东西还有人用吗」,只能靠翻会话记录**猜**。
|
|
2214
|
+
* ⇒ 本模块让每个 CCC 留下一份**纯计数**账本:**用了哪个、多少次、第一次和最近一次是什么时候**。
|
|
2215
|
+
* 用途 = 清理/精简的**依据**(没有人用的东西与天天在用的东西,不再靠猜)。
|
|
2216
|
+
*
|
|
2217
|
+
* ## 🔴 铁律(与 `cro-log.ts` 同族)
|
|
2218
|
+
*
|
|
2219
|
+
* **本模块每个导出函数都不抛错**:读盘 / 解析 / 写盘失败一律返回结构化失败,
|
|
2220
|
+
* 由调用方只记一行日志(或什么都不做)⇒ **统计的问题绝不影响工具执行与既有链路**。
|
|
2221
|
+
* 调用点在 `tools/post-execute` 的中途 ⇒ **更不能抛**(否则等于用统计打断会话)。
|
|
2222
|
+
*
|
|
2223
|
+
* ## 🔴 只记名字(所有者令;也是本模块唯一的"不入盘"约束)
|
|
2224
|
+
*
|
|
2225
|
+
* **绝不写 `arguments`**:MSM 参数里可能有凭据 / 路径 / 正文。
|
|
2226
|
+
* ⇒ 本模块只保留**计数键**(skill 名 / MSM 名),并对其做**长度与数量上限**(见常量),
|
|
2227
|
+
* 使文件规模**结构上有界**(不需要轮转,也不会被异常名字撑爆)。
|
|
2228
|
+
*
|
|
2229
|
+
* ## 🔴 重启后必须续算(所有者令)
|
|
2230
|
+
*
|
|
2231
|
+
* 宿主会被重启(本容器实测 24h 内 3 次)⇒ 计数**不能只活在内存里**:
|
|
2232
|
+
* 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake-registry.ts` /
|
|
2233
|
+
* `cro-log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
|
|
2234
|
+
*
|
|
2235
|
+
* ## 口径(两个字段,勿合并)
|
|
2236
|
+
*
|
|
2237
|
+
* | 字段 | 含义 | 喂它的地方 |
|
|
2238
|
+
* |---|---|---|
|
|
2239
|
+
* | `skill.loads` | 模型**主动**调宿主 `skill` 工具(把某 skill 的 `SKILL.md` 全文载入) | `tools/post-execute`(工具名 = `skill`) |
|
|
2240
|
+
* | `skill.injections` | ACC **每请求注入**的 skill 段(`trajectory.skills` / 轨迹 frontmatter 声明) | `seams/system-prompt.ts` 的 trajectory-skills section 求值回调 |
|
|
2241
|
+
* | `msm` | `msm` 工具的一次执行,按 **MSM 名**分桶 | `tools/post-execute`(工具名 = `msm`) |
|
|
2242
|
+
*
|
|
2243
|
+
* 🔴 **两者差好几个数量级**:`loads` 是模型的**动作**(稀疏),`injections` 是**请求的属性**(稠密)。
|
|
2244
|
+
* 合并成一个数就**再也读不出**「这个 skill 是被人主动用的,还是只是被配上了」。
|
|
2245
|
+
*/
|
|
2246
|
+
/** 落点目录(所有者令:CCC 的 `_tmp` 内) */
|
|
2247
|
+
const USAGE_STATS_SUBDIR = "_tmp";
|
|
2248
|
+
/** 固定文件名(所有者令「指定文件」;对齐时我取的名字,owner 未反对 ⇒ 定为常量) */
|
|
2249
|
+
const USAGE_STATS_FILENAME = "acc-usage.json";
|
|
2250
|
+
/** 每个桶的键数上限(**结构上界**,见文件头"只记名字"节) */
|
|
2251
|
+
const USAGE_MAX_KEYS_PER_BUCKET = 1e3;
|
|
2252
|
+
const USAGE_STATS_VERSION = 1;
|
|
2253
|
+
/** 统计文件绝对路径(**纯函数**) */
|
|
2254
|
+
function usageStatsPath(root) {
|
|
2255
|
+
return join(root, USAGE_STATS_SUBDIR, USAGE_STATS_FILENAME);
|
|
2256
|
+
}
|
|
2257
|
+
/** 空账本(**纯函数**) */
|
|
2258
|
+
function emptyUsageStats() {
|
|
2259
|
+
return {
|
|
2260
|
+
version: USAGE_STATS_VERSION,
|
|
2261
|
+
updatedAt: isoLocal(0),
|
|
2262
|
+
skill: {
|
|
2263
|
+
loads: {},
|
|
2264
|
+
injections: {}
|
|
2265
|
+
},
|
|
2266
|
+
msm: {}
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
/** 单条计数形状校验(容忍手改:字段缺失/类型不符 ⇒ 丢弃该条,不阻断整档) */
|
|
2270
|
+
function asCounter(value) {
|
|
2271
|
+
const c = value;
|
|
2272
|
+
if (!c || typeof c !== "object") return null;
|
|
2273
|
+
if (typeof c.count !== "number" || !Number.isFinite(c.count) || c.count < 0) return null;
|
|
2274
|
+
const firstAt = typeof c.firstAt === "string" ? c.firstAt : "";
|
|
2275
|
+
const lastAt = typeof c.lastAt === "string" ? c.lastAt : firstAt;
|
|
2276
|
+
return {
|
|
2277
|
+
count: Math.floor(c.count),
|
|
2278
|
+
firstAt,
|
|
2279
|
+
lastAt
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
/** 桶校验(非对象 ⇒ 空桶;逐键过滤非法条) */
|
|
2283
|
+
function asBucket(value) {
|
|
2284
|
+
const out = {};
|
|
2285
|
+
if (!value || typeof value !== "object") return out;
|
|
2286
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2287
|
+
const c = asCounter(v);
|
|
2288
|
+
if (c) out[k] = c;
|
|
2289
|
+
}
|
|
2290
|
+
return out;
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* 读取账本(**永不抛**;读坏 ⇒ 空账本 + 返回 error 文本,**不静默吞掉**)。
|
|
2294
|
+
* 文件不存在 = 正常初态(`error: null` 且空账本)。
|
|
2295
|
+
*/
|
|
2296
|
+
function loadUsageStats(root) {
|
|
2297
|
+
const path = usageStatsPath(root);
|
|
2298
|
+
if (!existsSync(path)) return {
|
|
2299
|
+
stats: emptyUsageStats(),
|
|
2300
|
+
error: null
|
|
2301
|
+
};
|
|
2302
|
+
try {
|
|
2303
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
2304
|
+
const skill = parsed.skill ?? {};
|
|
2305
|
+
return {
|
|
2306
|
+
stats: {
|
|
2307
|
+
version: USAGE_STATS_VERSION,
|
|
2308
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : isoLocal(0),
|
|
2309
|
+
skill: {
|
|
2310
|
+
loads: asBucket(skill.loads),
|
|
2311
|
+
injections: asBucket(skill.injections)
|
|
2312
|
+
},
|
|
2313
|
+
msm: asBucket(parsed.msm)
|
|
2314
|
+
},
|
|
2315
|
+
error: null
|
|
2316
|
+
};
|
|
2317
|
+
} catch (err) {
|
|
2318
|
+
return {
|
|
2319
|
+
stats: emptyUsageStats(),
|
|
2320
|
+
error: `用量统计解析失败(${String(err?.message ?? err)})`
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
/** 键是否可记(非空 + 不超长;**不截断**——截断会造出一个不存在的名字) */
|
|
2325
|
+
function isRecordableUsageName(name) {
|
|
2326
|
+
return typeof name === "string" && name !== "" && [...name].length <= 120;
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* 在桶上自增一格(**纯函数**:不改入参,返回新桶)。
|
|
2330
|
+
* 新键超桶上限 ⇒ **不新增**(但仍自增已有键)⇒ 键数**结构上有界**。
|
|
2331
|
+
*/
|
|
2332
|
+
function bumpBucket(bucket, name, at, maxKeys = USAGE_MAX_KEYS_PER_BUCKET) {
|
|
2333
|
+
const prev = bucket[name];
|
|
2334
|
+
if (!prev && Object.keys(bucket).length >= maxKeys) return bucket;
|
|
2335
|
+
const next = prev ? {
|
|
2336
|
+
count: prev.count + 1,
|
|
2337
|
+
firstAt: prev.firstAt || at,
|
|
2338
|
+
lastAt: at
|
|
2339
|
+
} : {
|
|
2340
|
+
count: 1,
|
|
2341
|
+
firstAt: at,
|
|
2342
|
+
lastAt: at
|
|
2343
|
+
};
|
|
2344
|
+
return {
|
|
2345
|
+
...bucket,
|
|
2346
|
+
[name]: next
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
/** 从工具入参里取名字(首个命中的键胜出;取不到 ⇒ `null`) */
|
|
2350
|
+
function nameFromArgs(args, keys) {
|
|
2351
|
+
if (args === null || typeof args !== "object") return null;
|
|
2352
|
+
const a = args;
|
|
2353
|
+
for (const k of keys) if (isRecordableUsageName(a[k])) return a[k];
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
/** `msm` 工具的名字键(`msm(name, args)` 的第一个参数) */
|
|
2357
|
+
const MSM_NAME_KEYS = ["name"];
|
|
2358
|
+
/** `skill` 工具的名字键(宿主 `skill` 工具用 `name`;另两个是防御性容错) */
|
|
2359
|
+
const SKILL_NAME_KEYS = [
|
|
2360
|
+
"name",
|
|
2361
|
+
"skill",
|
|
2362
|
+
"skillName"
|
|
2363
|
+
];
|
|
2364
|
+
const NOTHING = {
|
|
2365
|
+
ok: true,
|
|
2366
|
+
error: null,
|
|
2367
|
+
recorded: null,
|
|
2368
|
+
name: null,
|
|
2369
|
+
wrote: false
|
|
2370
|
+
};
|
|
2371
|
+
/**
|
|
2372
|
+
* 读 → 自增 → 原子写(**本模块唯一写盘通道;永不抛**)。
|
|
2373
|
+
*
|
|
2374
|
+
* @param root CCC 根
|
|
2375
|
+
* @param mutate 在已载入的账本上做一次自增(返回值 = 本次记录的结论;返回 `NOTHING` 则不写盘)
|
|
2376
|
+
* @param nowMs 当前毫秒(可注入 ⇒ 便于测 firstAt/lastAt)
|
|
2377
|
+
*/
|
|
2378
|
+
function writeThrough(root, mutate, nowMs = Date.now()) {
|
|
2379
|
+
const path = usageStatsPath(root);
|
|
2380
|
+
try {
|
|
2381
|
+
const at = isoLocal(nowMs);
|
|
2382
|
+
const loaded = loadUsageStats(root);
|
|
2383
|
+
const outcome = mutate(loaded.stats, at);
|
|
2384
|
+
if (outcome.recorded === null) return outcome;
|
|
2385
|
+
loaded.stats.updatedAt = at;
|
|
2386
|
+
mkdirSync(join(root, USAGE_STATS_SUBDIR), { recursive: true });
|
|
2387
|
+
const tmp = `${path}.tmp`;
|
|
2388
|
+
writeFileSync(tmp, `${JSON.stringify(loaded.stats, null, 2)}\n`, "utf-8");
|
|
2389
|
+
renameSync(tmp, path);
|
|
2390
|
+
return {
|
|
2391
|
+
...outcome,
|
|
2392
|
+
ok: true,
|
|
2393
|
+
error: null,
|
|
2394
|
+
wrote: true
|
|
2395
|
+
};
|
|
2396
|
+
} catch (err) {
|
|
2397
|
+
return {
|
|
2398
|
+
ok: false,
|
|
2399
|
+
error: `用量统计写入失败(${String(err?.message ?? err)})`,
|
|
2400
|
+
recorded: null,
|
|
2401
|
+
name: null,
|
|
2402
|
+
wrote: false
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* 记录一次工具调用的用量(**`tools/post-execute` 的喂入口**)。
|
|
2408
|
+
*
|
|
2409
|
+
* 只认两类工具名;其余一律 `NOTHING`(**不产生文件、不产生噪声**)。
|
|
2410
|
+
* 🔴 **不看 `arguments` 的其余内容**——只取名字键。
|
|
2411
|
+
*/
|
|
2412
|
+
function recordToolUsage(root, toolName, args, nowMs = Date.now()) {
|
|
2413
|
+
if (toolName === "msm") {
|
|
2414
|
+
const name = nameFromArgs(args, MSM_NAME_KEYS);
|
|
2415
|
+
if (!name) return NOTHING;
|
|
2416
|
+
return writeThrough(root, (stats, at) => {
|
|
2417
|
+
stats.msm = bumpBucket(stats.msm, name, at);
|
|
2418
|
+
return {
|
|
2419
|
+
ok: true,
|
|
2420
|
+
error: null,
|
|
2421
|
+
recorded: "msm",
|
|
2422
|
+
name,
|
|
2423
|
+
wrote: false
|
|
2424
|
+
};
|
|
2425
|
+
}, nowMs);
|
|
2426
|
+
}
|
|
2427
|
+
if (toolName === "skill") {
|
|
2428
|
+
const name = nameFromArgs(args, SKILL_NAME_KEYS);
|
|
2429
|
+
if (!name) return NOTHING;
|
|
2430
|
+
return writeThrough(root, (stats, at) => {
|
|
2431
|
+
stats.skill.loads = bumpBucket(stats.skill.loads, name, at);
|
|
2432
|
+
return {
|
|
2433
|
+
ok: true,
|
|
2434
|
+
error: null,
|
|
2435
|
+
recorded: "skill.load",
|
|
2436
|
+
name,
|
|
2437
|
+
wrote: false
|
|
2438
|
+
};
|
|
2439
|
+
}, nowMs);
|
|
2440
|
+
}
|
|
2441
|
+
return NOTHING;
|
|
2442
|
+
}
|
|
2443
|
+
/**
|
|
2444
|
+
* 记录一次 **skill 注入**(每个请求的 skill 段装配时调用)。
|
|
2445
|
+
*
|
|
2446
|
+
* ⚠️ 与 `recordToolUsage` 不同,本函数的调用频度 = **请求频度**(稠密):
|
|
2447
|
+
* 调用方应**只在真的注入了非空内容时**调用(空 section 由宿主丢弃 ⇒ 不产生噪声)。
|
|
2448
|
+
* 一次调用只写一次盘(`names` 内的多个 skill 合并在**同一次**读改写里)。
|
|
2449
|
+
*/
|
|
2450
|
+
function recordSkillInjections(root, names, nowMs = Date.now()) {
|
|
2451
|
+
const usable = names.filter((n) => isRecordableUsageName(n));
|
|
2452
|
+
if (usable.length === 0) return NOTHING;
|
|
2453
|
+
return writeThrough(root, (stats, at) => {
|
|
2454
|
+
let bucket = stats.skill.injections;
|
|
2455
|
+
for (const n of usable) bucket = bumpBucket(bucket, n, at);
|
|
2456
|
+
stats.skill.injections = bucket;
|
|
2457
|
+
return {
|
|
2458
|
+
ok: true,
|
|
2459
|
+
error: null,
|
|
2460
|
+
recorded: "skill.injection",
|
|
2461
|
+
name: usable.join(","),
|
|
2462
|
+
wrote: false
|
|
2463
|
+
};
|
|
2464
|
+
}, nowMs);
|
|
2465
|
+
}
|
|
2192
2466
|
/** 验证窗口:当前时步 ± 1(容忍时钟漂移/生成延迟) */
|
|
2193
2467
|
const TOTP_WINDOW = 1;
|
|
2194
2468
|
/** 输出位数(标准 6 位) */
|
|
@@ -3159,7 +3433,7 @@ function registerTrajectorySkillSection(agent, root) {
|
|
|
3159
3433
|
text: () => {
|
|
3160
3434
|
const current = readLastBound(session);
|
|
3161
3435
|
if (!current) return "";
|
|
3162
|
-
return buildTrajectorySkillsSection(root, current.mdPath, TRAJECTORY_SKILLS_MAX_CHARS, readTrajectorySkills(root));
|
|
3436
|
+
return buildTrajectorySkillsSection(root, current.mdPath, TRAJECTORY_SKILLS_MAX_CHARS, readTrajectorySkills(root), (names) => recordSkillInjections(root, names));
|
|
3163
3437
|
}
|
|
3164
3438
|
});
|
|
3165
3439
|
trajectorySkillAgents.add(key);
|
|
@@ -3170,10 +3444,6 @@ function registerTrajectorySkillSection(agent, root) {
|
|
|
3170
3444
|
}
|
|
3171
3445
|
//#endregion
|
|
3172
3446
|
//#region src/skiff-core.ts
|
|
3173
|
-
const PLUGIN_SOURCE$5 = {
|
|
3174
|
-
kind: "plugin",
|
|
3175
|
-
plugin: "dsh-serenity-hooks"
|
|
3176
|
-
};
|
|
3177
3447
|
const skiffAgents = /* @__PURE__ */ new Map();
|
|
3178
3448
|
/** 查 sessionId 的 Skiff 角色名(无 → null) */
|
|
3179
3449
|
function skiffRoleFor(sessionId) {
|
|
@@ -3583,7 +3853,7 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
|
|
|
3583
3853
|
type: "text",
|
|
3584
3854
|
text: question
|
|
3585
3855
|
}],
|
|
3586
|
-
source: PLUGIN_SOURCE
|
|
3856
|
+
source: PLUGIN_SOURCE
|
|
3587
3857
|
}));
|
|
3588
3858
|
await waitAgentIdle(ctx, agent);
|
|
3589
3859
|
const answer = lastAssistantText$2(agent);
|
|
@@ -4094,10 +4364,7 @@ async function runHandymanJob(ctx, opts) {
|
|
|
4094
4364
|
type: "text",
|
|
4095
4365
|
text: prompt
|
|
4096
4366
|
}],
|
|
4097
|
-
source:
|
|
4098
|
-
kind: "plugin",
|
|
4099
|
-
plugin: "dsh-serenity-hooks"
|
|
4100
|
-
}
|
|
4367
|
+
source: PLUGIN_SOURCE
|
|
4101
4368
|
}));
|
|
4102
4369
|
await waitAgentIdle(ctx, workerAgent);
|
|
4103
4370
|
lastResponse = lastAssistantText$1(workerAgent);
|
|
@@ -5211,6 +5478,445 @@ function createContainerAdminTool(ctx) {
|
|
|
5211
5478
|
});
|
|
5212
5479
|
}
|
|
5213
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
|
|
5214
5920
|
//#region src/im-bridge.ts
|
|
5215
5921
|
/**
|
|
5216
5922
|
* im-bridge.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
|
|
@@ -5361,17 +6067,19 @@ async function runImBridge(root, req) {
|
|
|
5361
6067
|
const resolved = channel.resolveUser(root, rawUser);
|
|
5362
6068
|
if (!resolved) return fail("ALIAS_UNKNOWN", `unknown user "${rawUser}"`);
|
|
5363
6069
|
try {
|
|
6070
|
+
const sent = await channel.send({
|
|
6071
|
+
root,
|
|
6072
|
+
userId: resolved.id,
|
|
6073
|
+
text,
|
|
6074
|
+
accountId: req.account
|
|
6075
|
+
});
|
|
6076
|
+
noteOutboundSend(root);
|
|
5364
6077
|
return {
|
|
5365
6078
|
ok: true,
|
|
5366
6079
|
channel: channelId,
|
|
5367
6080
|
action,
|
|
5368
6081
|
detail: {
|
|
5369
|
-
...
|
|
5370
|
-
root,
|
|
5371
|
-
userId: resolved.id,
|
|
5372
|
-
text,
|
|
5373
|
-
accountId: req.account
|
|
5374
|
-
}),
|
|
6082
|
+
...sent,
|
|
5375
6083
|
user: resolved.alias ?? resolved.id
|
|
5376
6084
|
}
|
|
5377
6085
|
};
|
|
@@ -5390,19 +6098,21 @@ async function runImBridge(root, req) {
|
|
|
5390
6098
|
const file = readCccFile(root, fileArg);
|
|
5391
6099
|
if (!file.ok) return fail(file.code, file.error);
|
|
5392
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);
|
|
5393
6110
|
return {
|
|
5394
6111
|
ok: true,
|
|
5395
6112
|
channel: channelId,
|
|
5396
6113
|
action,
|
|
5397
6114
|
detail: {
|
|
5398
|
-
...
|
|
5399
|
-
root,
|
|
5400
|
-
userId: resolved.id,
|
|
5401
|
-
data: file.data,
|
|
5402
|
-
fileName: file.fileName,
|
|
5403
|
-
caption: req.caption,
|
|
5404
|
-
accountId: req.account
|
|
5405
|
-
}),
|
|
6115
|
+
...sent,
|
|
5406
6116
|
user: resolved.alias ?? resolved.id
|
|
5407
6117
|
}
|
|
5408
6118
|
};
|
|
@@ -5994,7 +6704,7 @@ function registerBootstrap(ctx) {
|
|
|
5994
6704
|
if (depth === 0) {
|
|
5995
6705
|
if (hasUserMessageHistory(session)) return;
|
|
5996
6706
|
} else if (sid !== void 0 && anchoredSessions.has(sid)) return;
|
|
5997
|
-
if (message?.source?.kind === "plugin") return;
|
|
6707
|
+
if (message?.source?.kind === "plugin:dsh-serenity-hooks") return;
|
|
5998
6708
|
const inbox = agent.inbox;
|
|
5999
6709
|
if (!inbox?.prepend) return;
|
|
6000
6710
|
for (let i = SETTINGS.anchorMessages.length - 1; i >= 0; i--) inbox.prepend("next-turn", {
|
|
@@ -6005,8 +6715,7 @@ function registerBootstrap(ctx) {
|
|
|
6005
6715
|
text: SETTINGS.anchorMessages[i]
|
|
6006
6716
|
}],
|
|
6007
6717
|
source: {
|
|
6008
|
-
kind:
|
|
6009
|
-
plugin: "dsh-serenity-hooks",
|
|
6718
|
+
kind: ACC_MESSAGE_KIND,
|
|
6010
6719
|
form: "notice",
|
|
6011
6720
|
summary: "bootstrap anchor turn"
|
|
6012
6721
|
}
|
|
@@ -6332,10 +7041,6 @@ function trajectoryCompactionReminderText(input) {
|
|
|
6332
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.`;
|
|
6333
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.`;
|
|
6334
7043
|
}
|
|
6335
|
-
const PLUGIN_SOURCE$4 = {
|
|
6336
|
-
kind: "plugin",
|
|
6337
|
-
plugin: "dsh-serenity-hooks"
|
|
6338
|
-
};
|
|
6339
7044
|
/** 每 agent 一个跟踪器(进程内存态,agent token 维度) */
|
|
6340
7045
|
const trackers = /* @__PURE__ */ new Map();
|
|
6341
7046
|
/** 连续超阈值轮数达此值 → 升级为 [TRAJECTORY-ESCALATED] 强制语气(此后持续升级催,直到 rebuild) */
|
|
@@ -6364,6 +7069,9 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
6364
7069
|
const tracker = trackerFor(exec);
|
|
6365
7070
|
const shouldRemind = skiffKeeper ? tracker.step(exec.name) : false;
|
|
6366
7071
|
const downstream = await next();
|
|
7072
|
+
try {
|
|
7073
|
+
recordToolUsage(root, exec.name, exec.arguments);
|
|
7074
|
+
} catch {}
|
|
6367
7075
|
const blocks = [];
|
|
6368
7076
|
if (shouldRemind) {
|
|
6369
7077
|
const code = tracker.ack();
|
|
@@ -6418,7 +7126,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
6418
7126
|
if (blocks.length === 0) return downstream;
|
|
6419
7127
|
const reminderAll = createUserMessage({
|
|
6420
7128
|
content: blocks,
|
|
6421
|
-
source: PLUGIN_SOURCE
|
|
7129
|
+
source: PLUGIN_SOURCE
|
|
6422
7130
|
});
|
|
6423
7131
|
if (downstream.kind === "block") return {
|
|
6424
7132
|
kind: "block",
|
|
@@ -6462,10 +7170,6 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
|
|
|
6462
7170
|
}
|
|
6463
7171
|
return lines.join("\n");
|
|
6464
7172
|
}
|
|
6465
|
-
const PLUGIN_SOURCE$3 = {
|
|
6466
|
-
kind: "plugin",
|
|
6467
|
-
plugin: "dsh-serenity-hooks"
|
|
6468
|
-
};
|
|
6469
7173
|
/**
|
|
6470
7174
|
* ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + Phase 2)。
|
|
6471
7175
|
* ⚠️ 原文案含「+ handyman 模型」,该注入已于 2026-09-16 删除(§0e-G)⇒ 此处同步订正(散文级残留)。
|
|
@@ -6480,7 +7184,7 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
|
6480
7184
|
}];
|
|
6481
7185
|
return createUserMessage({
|
|
6482
7186
|
content,
|
|
6483
|
-
source: PLUGIN_SOURCE
|
|
7187
|
+
source: PLUGIN_SOURCE
|
|
6484
7188
|
});
|
|
6485
7189
|
}
|
|
6486
7190
|
/** 每 agent 是否已注入过(进程内存态) */
|
|
@@ -6594,7 +7298,7 @@ function registerContext(ctx, opts = {}) {
|
|
|
6594
7298
|
syncExclusiveToolsVisibility(agent, root);
|
|
6595
7299
|
} catch {}
|
|
6596
7300
|
};
|
|
6597
|
-
if (opts.seedOnStart ?? true) ctx.on("agent/
|
|
7301
|
+
if (opts.seedOnStart ?? true) ctx.on("agent/created", async (payload) => {
|
|
6598
7302
|
try {
|
|
6599
7303
|
seed(payload.agent);
|
|
6600
7304
|
} catch {}
|
|
@@ -7075,7 +7779,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7075
7779
|
return;
|
|
7076
7780
|
}
|
|
7077
7781
|
const settings = readAdvancedSettings();
|
|
7078
|
-
const { readSimpleSettings } = await import("./settings-section-
|
|
7782
|
+
const { readSimpleSettings } = await import("./settings-section-DTJW4smh.js").then((n) => n.r);
|
|
7079
7783
|
const simple = readSimpleSettings();
|
|
7080
7784
|
const allowed = settings.publicAsk.allowed;
|
|
7081
7785
|
const port = simple.acpHttpPort ?? ACP_HTTP_PORT;
|
|
@@ -7798,8 +8502,8 @@ const HOST_SERVICES = [
|
|
|
7798
8502
|
}
|
|
7799
8503
|
];
|
|
7800
8504
|
/** dsp 被验证过的宿主版本范围(与 package.json peerDependencies 同源,单一真相源) */
|
|
7801
|
-
const REQUIRED_HOST_RANGE = "^0.1.
|
|
7802
|
-
/** 范围 floor:剥掉 caret/比较前缀(`^0.1.
|
|
8505
|
+
const REQUIRED_HOST_RANGE = "^0.1.7-rc.1";
|
|
8506
|
+
/** 范围 floor:剥掉 caret/比较前缀(`^0.1.7-rc.1` → `0.1.7-rc.1`),供 {@link checkHostVersion} 使用 */
|
|
7803
8507
|
const REQUIRED_HOST_FLOOR = REQUIRED_HOST_RANGE.replace(/^[\^~>=<\s]+/, "");
|
|
7804
8508
|
/** 范围 ceiling:`^0.1.x` 的上界 = `<0.2.0`(显式常量,见 {@link checkHostVersion} 注释) */
|
|
7805
8509
|
const REQUIRED_HOST_CEILING = "0.2.0";
|
|
@@ -9023,7 +9727,7 @@ function registerGateway(ctx) {
|
|
|
9023
9727
|
});
|
|
9024
9728
|
};
|
|
9025
9729
|
sync();
|
|
9026
|
-
ctx.on("agent/
|
|
9730
|
+
ctx.on("agent/created", async (payload) => {
|
|
9027
9731
|
try {
|
|
9028
9732
|
const cwd = payload.agent?.session?.header?.cwd;
|
|
9029
9733
|
if (cwd) {
|
|
@@ -9052,10 +9756,6 @@ var rebuild_exports = /* @__PURE__ */ __exportAll({
|
|
|
9052
9756
|
sanitizeFocusLine: () => sanitizeFocusLine,
|
|
9053
9757
|
stripAckSuffix: () => stripAckSuffix
|
|
9054
9758
|
});
|
|
9055
|
-
const PLUGIN_SOURCE$2 = {
|
|
9056
|
-
kind: "plugin",
|
|
9057
|
-
plugin: "dsh-serenity-hooks"
|
|
9058
|
-
};
|
|
9059
9759
|
/** acknowledge 尾句(first-anchor 的确认要求——rebuild 重建后不重走确认轮,直接继续) */
|
|
9060
9760
|
const ACK_SUFFIX_RE = /Please simply reply "acknowledge" — no action needed\.\s*$/;
|
|
9061
9761
|
/**
|
|
@@ -9371,7 +10071,7 @@ function registerRebuildTurnHook(ctx) {
|
|
|
9371
10071
|
type: "text",
|
|
9372
10072
|
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.`
|
|
9373
10073
|
}],
|
|
9374
|
-
source: PLUGIN_SOURCE
|
|
10074
|
+
source: PLUGIN_SOURCE
|
|
9375
10075
|
}));
|
|
9376
10076
|
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
9377
10077
|
sessionId: id,
|
|
@@ -11524,7 +12224,7 @@ const MECHANISM_WORDS = [
|
|
|
11524
12224
|
"localstore.json",
|
|
11525
12225
|
".opencode/serenity.json",
|
|
11526
12226
|
"AGENT_SESSIONS",
|
|
11527
|
-
"agent/
|
|
12227
|
+
"agent/created",
|
|
11528
12228
|
"agent/turn-stopping",
|
|
11529
12229
|
"tools/pre-execute",
|
|
11530
12230
|
"tools/post-execute",
|
|
@@ -11634,17 +12334,15 @@ function buildRebuke(hits) {
|
|
|
11634
12334
|
const rebukeStates = /* @__PURE__ */ new Map();
|
|
11635
12335
|
//#endregion
|
|
11636
12336
|
//#region src/output-guard-seam.ts
|
|
11637
|
-
const PLUGIN_SOURCE$1 = {
|
|
11638
|
-
kind: "plugin",
|
|
11639
|
-
plugin: "dsh-serenity-hooks"
|
|
11640
|
-
};
|
|
11641
12337
|
/** 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免):
|
|
11642
12338
|
* skiff-(F4 问答)/ acp-(F4c 程序化)/ rebuild-(v1.22.4 重建会话)前缀 */
|
|
11643
12339
|
function isExternalFaceSession(sessionId) {
|
|
11644
12340
|
if (!sessionId) return false;
|
|
11645
12341
|
return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
|
|
11646
12342
|
}
|
|
11647
|
-
/** 读会话最后一个 assistant/message 文本(skiff-core
|
|
12343
|
+
/** 读会话最后一个 assistant/message 文本(skiff-core 同款)
|
|
12344
|
+
* **2026-09-23(S142 D77)起导出**:`unattended-seam.ts` 复用同一读法(本容器纪律:
|
|
12345
|
+
* 「同一读法只留一份」——两份 `lastAssistantText` 会在宿主改事件形状时**只修一处**)。
|
|
11648
12346
|
* **v1.27.7:剥离 think 块再检测**(用户:"不检查think内容(因为不输出)")——
|
|
11649
12347
|
* ` think…` 是模型思考过程,不进入用户可见输出;敏感词检测只针对**最终呈现文本**,
|
|
11650
12348
|
* 否则 think 内提及内部机制词(思考过程必然推演机制)会误打回。复用 stripThink
|
|
@@ -11695,7 +12393,7 @@ function registerOutputGuardHook(ctx) {
|
|
|
11695
12393
|
type: "text",
|
|
11696
12394
|
text: buildRebuke(hits)
|
|
11697
12395
|
}],
|
|
11698
|
-
source: PLUGIN_SOURCE
|
|
12396
|
+
source: PLUGIN_SOURCE
|
|
11699
12397
|
}));
|
|
11700
12398
|
} catch (error) {
|
|
11701
12399
|
console.warn(`[serenity-hooks] output-guard steer failed: ${String(error?.message ?? error)}`);
|
|
@@ -11703,6 +12401,235 @@ function registerOutputGuardHook(ctx) {
|
|
|
11703
12401
|
});
|
|
11704
12402
|
}
|
|
11705
12403
|
//#endregion
|
|
12404
|
+
//#region src/unattended-seam.ts
|
|
12405
|
+
/**
|
|
12406
|
+
* unattended-seam.ts — 无人值守代理回复的**拦截缝**(S142 **D77**,owner 2026-09-23 令「我需要的是一个机制」)
|
|
12407
|
+
*
|
|
12408
|
+
* ## 停在哪、怎么接(设计 §4 的裁决:**A 的通道 + B 的措辞**)
|
|
12409
|
+
*
|
|
12410
|
+
* 复用的是**已验证过的**通道——`agent/turn-stopping`(serial)里 `agent.steer()`:
|
|
12411
|
+
* 宿主语义 = "**listener 反对就打回**,机器重读 inbox:有新 steer ⇒ 再跑一步,没有 ⇒ 关轮"
|
|
12412
|
+
* (`host/contract.ts` 的 `HOST_EVENTS` 原文)。因此:
|
|
12413
|
+
* · **轮不结束**(语义正是"有人打回")⇒ **零新机械**;
|
|
12414
|
+
* · 文本**写成代理用户的话**(B 的措辞)+ **首句自报身份**(owner 已裁)。
|
|
12415
|
+
*
|
|
12416
|
+
* 模板 = `output-guard-seam.ts`(同一缝位、同一 `createUserMessage + PLUGIN_SOURCE` 形态)。
|
|
12417
|
+
*
|
|
12418
|
+
* ## 🔴「这轮是不是真实用户发起的」怎么判(本机制**唯一**的新机械;2026-09-23 取证)
|
|
12419
|
+
*
|
|
12420
|
+
* 交接块把它列为"动手前必须先查清的一件事",取证结论(读宿主类型面,非猜测):
|
|
12421
|
+
*
|
|
12422
|
+
* | 事实 | 出处 |
|
|
12423
|
+
* |---|---|
|
|
12424
|
+
* | 每条进入 inbox 的消息都发 `agent/inbox/inserted { agent, message }`;**在轮内离开 inbox 时**发 `agent/inbox/claimed { agent, message, turn }` | `@deepseek-ai/dsh-agent` README(Inbox 段)+ `runtime-types.d.ts` |
|
|
12425
|
+
* | `claimed.turn` = **拥有该消息的那一轮**;`turn-stopping.turn` = **即将关闭的那一轮** ⇒ 两者同编号可直接比对 | `runtime-types.d.ts` 同一文件两处 `turn` 声明 |
|
|
12426
|
+
* | 消息来源可辨:`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`(**唯一**声明处与常量处) |
|
|
12427
|
+
*
|
|
12428
|
+
* ⇒ **判据 = 「这一轮 claim 到的消息里,有没有 `source.kind === 'user'`」**。
|
|
12429
|
+
* 没有现成信号时本可退化为"记账法"(我们知道自己投过什么),**但不必**:宿主已给出一手信号。
|
|
12430
|
+
*
|
|
12431
|
+
* ## 🔴 为什么"用户发起的轮"要跳过(这是设计 §3-③ 的本意,别当成漏洞)
|
|
12432
|
+
*
|
|
12433
|
+
* 用户**刚说完话**⇒ 人就在跟前,代理插话既是冒充也是浪费。**"用户说完走开"那种情形**
|
|
12434
|
+
* 由**绳(长周期保险丝)/其它非用户发起的唤醒**覆盖:那条轮**不是**用户发起的 ⇒ 代理接手,
|
|
12435
|
+
* 一路推进到**合法收束**。⇒ 两个机制合起来才等于"用户走开后会话还能自己跑完"。
|
|
12436
|
+
*
|
|
12437
|
+
* ## 🔴 铁律(与 `unattended-ops.ts` 同族)
|
|
12438
|
+
*
|
|
12439
|
+
* **本模块的任何失败都不得影响 turn 收尾**:所有 handler 包 try/catch,异常只记一行 console。
|
|
12440
|
+
*/
|
|
12441
|
+
/** 取载体 id(payload 的 `agent` 面;取不到 ⇒ 空串) */
|
|
12442
|
+
function sessionIdOf$2(agent) {
|
|
12443
|
+
const id = agent?.session?.id;
|
|
12444
|
+
return typeof id === "string" ? id : "";
|
|
12445
|
+
}
|
|
12446
|
+
/** 取会话对象(payload 的 `agent.session`) */
|
|
12447
|
+
function sessionOf(agent) {
|
|
12448
|
+
return agent?.session;
|
|
12449
|
+
}
|
|
12450
|
+
/** 取会话 cwd(`header.cwd`;取不到回落进程 cwd——与 `output-guard-seam.ts` 同款读法) */
|
|
12451
|
+
function cwdOf(agent) {
|
|
12452
|
+
return sessionOf(agent)?.header?.cwd ?? process.cwd();
|
|
12453
|
+
}
|
|
12454
|
+
/** 是否 **CCC 内主会话**(`delegationDepth` 缺省 0;子代理 / 无头运行不算) */
|
|
12455
|
+
function isMainSession(agent) {
|
|
12456
|
+
return (sessionOf(agent)?.header?.delegationDepth ?? 0) === 0;
|
|
12457
|
+
}
|
|
12458
|
+
/**
|
|
12459
|
+
* 该会话所属轨迹**是否带 CRO 程序**(窄口径的唯一排除项)。
|
|
12460
|
+
* 判据 = 轨迹目录下存在 `continuous-re-occurrence.ts`(与 `cro.ts` 同源:**文件在 = 启用**)。
|
|
12461
|
+
* 无绑定(未挂在任何轨迹上)⇒ false(没有程序在驱动它)。
|
|
12462
|
+
*/
|
|
12463
|
+
function hasCroProgram(session, root) {
|
|
12464
|
+
const dirName = readLastBound(session)?.dirName;
|
|
12465
|
+
if (!dirName) return false;
|
|
12466
|
+
return existsSync(join(sessionsRoot(root), dirName, CRO_FILENAME));
|
|
12467
|
+
}
|
|
12468
|
+
/** 落一条流水(**永不抛**;无绑定 ⇒ 无落点,只回 false 由调用方记 console) */
|
|
12469
|
+
function logEvent(session, root, entry, nowMs) {
|
|
12470
|
+
const dirName = readLastBound(session)?.dirName;
|
|
12471
|
+
if (!dirName) return false;
|
|
12472
|
+
const res = appendUnattendedLog(root, dirName, entry, nowMs);
|
|
12473
|
+
if (!res.ok) console.log(`[serenity-hooks] unattended: 流水未落盘 — ${res.error ?? "未知原因"}`);
|
|
12474
|
+
return res.ok;
|
|
12475
|
+
}
|
|
12476
|
+
/**
|
|
12477
|
+
* 一轮收尾(`agent/turn-stopping`)的完整处置。
|
|
12478
|
+
*
|
|
12479
|
+
* 顺序:**取事实 → 纯函数判定 → 执行**(判定全在 `unattended-ops.ts`,本函数只做 I/O 与 steer)。
|
|
12480
|
+
* @returns 便于用例观察的判定结果文本(生产不使用返回值)
|
|
12481
|
+
*/
|
|
12482
|
+
function handleTurnStopping(agent, turn) {
|
|
12483
|
+
if (readSimpleSettings().unattendedEnabled !== true) return "skip:mode-off";
|
|
12484
|
+
const sessionId = sessionIdOf$2(agent);
|
|
12485
|
+
if (sessionId === "") return "skip:no-session-id";
|
|
12486
|
+
const root = cccRootForCwd(cwdOf(agent));
|
|
12487
|
+
if (root === null) return "skip:not-in-ccc";
|
|
12488
|
+
const session = sessionOf(agent);
|
|
12489
|
+
const nowMs = Date.now();
|
|
12490
|
+
const run = getProxyRun(sessionId);
|
|
12491
|
+
const text = lastAssistantText(agent);
|
|
12492
|
+
const nonces = parseDoneNonces(text);
|
|
12493
|
+
const sessionIdExcluded = isProxyExcludedSessionId(sessionId);
|
|
12494
|
+
const userInitiated = turn === null ? true : isUserTurn(sessionId, turn);
|
|
12495
|
+
const cro = !sessionIdExcluded && !userInitiated && isMainSession(agent) ? hasCroProgram(session, root) : false;
|
|
12496
|
+
const verdict = decideUnattendedAction({
|
|
12497
|
+
enabled: true,
|
|
12498
|
+
root,
|
|
12499
|
+
isMainSession: isMainSession(agent),
|
|
12500
|
+
sessionIdExcluded,
|
|
12501
|
+
userInitiated,
|
|
12502
|
+
hasCroProgram: cro,
|
|
12503
|
+
run,
|
|
12504
|
+
nowMs,
|
|
12505
|
+
doneNonce: nonces.length > 0 ? nonces[nonces.length - 1] ?? null : null,
|
|
12506
|
+
claimsNotify: claimsUserNotification(text),
|
|
12507
|
+
outboundSeen: run !== null && hasOutboundSendSince(root, run.startedAt, nowMs)
|
|
12508
|
+
});
|
|
12509
|
+
if (verdict.action === "skip") return `skip:${verdict.reason}`;
|
|
12510
|
+
if (verdict.action === "accept" || verdict.action === "cap") {
|
|
12511
|
+
clearProxyRun(sessionId);
|
|
12512
|
+
logEvent(session, root, {
|
|
12513
|
+
event: verdict.action,
|
|
12514
|
+
round: verdict.round,
|
|
12515
|
+
nonce: run?.nonce ?? null,
|
|
12516
|
+
reason: verdict.reason,
|
|
12517
|
+
sessionId
|
|
12518
|
+
}, nowMs);
|
|
12519
|
+
console.log(`[serenity-hooks] unattended: ${verdict.action} (${verdict.reason}, round=${verdict.round}, session=${sessionId})`);
|
|
12520
|
+
return `${verdict.action}:${verdict.reason}`;
|
|
12521
|
+
}
|
|
12522
|
+
const nonce = newNonce();
|
|
12523
|
+
const startedAt = run?.startedAt ?? nowMs;
|
|
12524
|
+
setProxyRun(sessionId, {
|
|
12525
|
+
round: verdict.round,
|
|
12526
|
+
nonce,
|
|
12527
|
+
startedAt
|
|
12528
|
+
});
|
|
12529
|
+
const message = verdict.reason === "false-notify" ? buildFalseNotifyRebuke({
|
|
12530
|
+
round: verdict.round,
|
|
12531
|
+
maxRounds: 15,
|
|
12532
|
+
nonce
|
|
12533
|
+
}) : buildProxyMessage({
|
|
12534
|
+
round: verdict.round,
|
|
12535
|
+
maxRounds: 15,
|
|
12536
|
+
nonce
|
|
12537
|
+
});
|
|
12538
|
+
try {
|
|
12539
|
+
agent.steer(createUserMessage({
|
|
12540
|
+
content: [{
|
|
12541
|
+
type: "text",
|
|
12542
|
+
text: message
|
|
12543
|
+
}],
|
|
12544
|
+
source: PLUGIN_SOURCE
|
|
12545
|
+
}));
|
|
12546
|
+
} catch (error) {
|
|
12547
|
+
clearProxyRun(sessionId);
|
|
12548
|
+
logEvent(session, root, {
|
|
12549
|
+
event: "cap",
|
|
12550
|
+
round: verdict.round,
|
|
12551
|
+
nonce,
|
|
12552
|
+
reason: "steer-failed",
|
|
12553
|
+
sessionId
|
|
12554
|
+
}, nowMs);
|
|
12555
|
+
console.warn(`[serenity-hooks] unattended: steer 失败(本轮放弃) — ${String(error?.message ?? error)}`);
|
|
12556
|
+
return "cap:steer-failed";
|
|
12557
|
+
}
|
|
12558
|
+
logEvent(session, root, {
|
|
12559
|
+
event: "inject",
|
|
12560
|
+
round: verdict.round,
|
|
12561
|
+
nonce,
|
|
12562
|
+
reason: verdict.reason,
|
|
12563
|
+
sessionId
|
|
12564
|
+
}, nowMs);
|
|
12565
|
+
console.log(`[serenity-hooks] unattended: inject round=${verdict.round}/15 (${verdict.reason}, session=${sessionId})`);
|
|
12566
|
+
return `inject:${verdict.reason}`;
|
|
12567
|
+
}
|
|
12568
|
+
/**
|
|
12569
|
+
* 装配无人值守代理缝(`index.ts` apply 调用)。
|
|
12570
|
+
*
|
|
12571
|
+
* 订阅三个事件(**都已在 `host/contract.ts` 白名单里**,新增的 `agent/inbox/claimed` 同批登记):
|
|
12572
|
+
* · `agent/inbox/claimed` —— 记「真实用户发起的轮次」;用户回来时**让位**(清代理段 + 记流水);
|
|
12573
|
+
* · `agent/turn-stopping` —— 判定 + 注入(本机制主体);
|
|
12574
|
+
* · `agent/disposed` / `session/disposed` —— 载体销毁清表(**本容器栽过 5 次"只增不清"**)。
|
|
12575
|
+
*/
|
|
12576
|
+
function registerUnattendedSeam(ctx) {
|
|
12577
|
+
const on = (event, handler) => {
|
|
12578
|
+
try {
|
|
12579
|
+
ctx.on(event, handler);
|
|
12580
|
+
} catch (err) {
|
|
12581
|
+
console.log(`[serenity-hooks] ✗ 无人值守代理未装配(${event} 不可用): ${String(err?.message ?? err)}`);
|
|
12582
|
+
}
|
|
12583
|
+
};
|
|
12584
|
+
on("agent/inbox/claimed", (payload) => {
|
|
12585
|
+
try {
|
|
12586
|
+
const p = payload;
|
|
12587
|
+
const sessionId = sessionIdOf$2(p?.agent);
|
|
12588
|
+
if (sessionId === "") return;
|
|
12589
|
+
if (p.message?.source?.kind !== "user") return;
|
|
12590
|
+
noteUserTurn(sessionId, (typeof p.turn === "number" ? p.turn : null) ?? -1);
|
|
12591
|
+
const run = getProxyRun(sessionId);
|
|
12592
|
+
if (!run) return;
|
|
12593
|
+
clearProxyRun(sessionId);
|
|
12594
|
+
const root = cccRootForCwd(cwdOf(p.agent));
|
|
12595
|
+
if (root) logEvent(sessionOf(p.agent), root, {
|
|
12596
|
+
event: "yield",
|
|
12597
|
+
round: run.round,
|
|
12598
|
+
nonce: run.nonce,
|
|
12599
|
+
reason: "user-returned",
|
|
12600
|
+
sessionId
|
|
12601
|
+
}, Date.now());
|
|
12602
|
+
console.log(`[serenity-hooks] unattended: yield — 用户回来了 (session=${sessionId}, round=${run.round})`);
|
|
12603
|
+
} catch (err) {
|
|
12604
|
+
console.log(`[serenity-hooks] unattended: claimed 观察失败 — ${String(err?.message ?? err)}`);
|
|
12605
|
+
}
|
|
12606
|
+
});
|
|
12607
|
+
on("agent/turn-stopping", (payload) => {
|
|
12608
|
+
try {
|
|
12609
|
+
const p = payload;
|
|
12610
|
+
if (!p?.agent) return;
|
|
12611
|
+
handleTurnStopping(p.agent, typeof p.turn === "number" ? p.turn : null);
|
|
12612
|
+
} catch (err) {
|
|
12613
|
+
console.log(`[serenity-hooks] unattended: turn-stopping 处置失败(已忽略) — ${String(err?.message ?? err)}`);
|
|
12614
|
+
}
|
|
12615
|
+
});
|
|
12616
|
+
on("agent/disposed", (payload) => {
|
|
12617
|
+
const sessionId = sessionIdOf$2(payload?.agent);
|
|
12618
|
+
if (sessionId === "") return;
|
|
12619
|
+
clearProxyRun(sessionId);
|
|
12620
|
+
clearUserTurn(sessionId);
|
|
12621
|
+
});
|
|
12622
|
+
on("session/disposed", (payload) => {
|
|
12623
|
+
const id = payload?.id;
|
|
12624
|
+
if (typeof id !== "string" || id === "") return;
|
|
12625
|
+
clearProxyRun(id);
|
|
12626
|
+
clearUserTurn(id);
|
|
12627
|
+
});
|
|
12628
|
+
registerDisposer(ctx, "unattended proxy", () => {
|
|
12629
|
+
clearUnattendedState();
|
|
12630
|
+
});
|
|
12631
|
+
}
|
|
12632
|
+
//#endregion
|
|
11706
12633
|
//#region src/acp-core.ts
|
|
11707
12634
|
/** JSON-RPC 错误码(对齐标准) */
|
|
11708
12635
|
const RPC_ERROR = {
|
|
@@ -12456,12 +13383,6 @@ qInput.focus()
|
|
|
12456
13383
|
</body>
|
|
12457
13384
|
</html>`;
|
|
12458
13385
|
}
|
|
12459
|
-
//#endregion
|
|
12460
|
-
//#region src/weixin-output-guard.ts
|
|
12461
|
-
const PLUGIN_SOURCE = {
|
|
12462
|
-
kind: "plugin",
|
|
12463
|
-
plugin: "dsh-serenity-hooks"
|
|
12464
|
-
};
|
|
12465
13386
|
const manualSessions = /* @__PURE__ */ new Map();
|
|
12466
13387
|
/**
|
|
12467
13388
|
* 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
|
|
@@ -14059,7 +14980,7 @@ function registerOpencodeAutoConfig(ctx) {
|
|
|
14059
14980
|
});
|
|
14060
14981
|
}
|
|
14061
14982
|
try {
|
|
14062
|
-
ctx.on?.call(ctx, "settings/updated", (ns) => {
|
|
14983
|
+
ctx.on?.call(ctx, "settings/document-updated", (ns) => {
|
|
14063
14984
|
if (ns === "llm-pi-ai") sync();
|
|
14064
14985
|
});
|
|
14065
14986
|
} catch {}
|
|
@@ -14303,7 +15224,7 @@ function registerDeepseekVisionPatch(ctx, enabled) {
|
|
|
14303
15224
|
});
|
|
14304
15225
|
}
|
|
14305
15226
|
try {
|
|
14306
|
-
ctx.on?.call(ctx, "settings/updated", (ns) => {
|
|
15227
|
+
ctx.on?.call(ctx, "settings/document-updated", (ns) => {
|
|
14307
15228
|
if (ns === "llm-pi-ai") sync();
|
|
14308
15229
|
});
|
|
14309
15230
|
} catch {}
|
|
@@ -14848,6 +15769,7 @@ function renderClock(label, s) {
|
|
|
14848
15769
|
const parts = [
|
|
14849
15770
|
`armed=${s.armed}`,
|
|
14850
15771
|
`CRO 闸=${readSimpleSettings().croEnabled !== false}`,
|
|
15772
|
+
`无人值守闸=${readSimpleSettings().unattendedEnabled === true}`,
|
|
14851
15773
|
`tick 次数=${s.ticks}`,
|
|
14852
15774
|
`上次 tick=${ts(s.lastTickAt)}`
|
|
14853
15775
|
];
|
|
@@ -15165,7 +16087,17 @@ const Config = z.object({
|
|
|
15165
16087
|
}),
|
|
15166
16088
|
webFetch: z.object({ enabled: z.boolean().default(true) }),
|
|
15167
16089
|
opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
|
|
15168
|
-
visionPatch: z.object({ enabled: z.boolean().default(true) })
|
|
16090
|
+
visionPatch: z.object({ enabled: z.boolean().default(true) }),
|
|
16091
|
+
gatewayEnabled: z.boolean(),
|
|
16092
|
+
rebuildEnabled: z.boolean(),
|
|
16093
|
+
rebuildThresholdK: z.number().min(50).max(4e3),
|
|
16094
|
+
skiffEnabled: z.boolean(),
|
|
16095
|
+
skiffDebugPort: z.number().min(1024).max(65535),
|
|
16096
|
+
acpEnabled: z.boolean(),
|
|
16097
|
+
acpHttpPort: z.number().min(1024).max(65535),
|
|
16098
|
+
publicAskEnabled: z.boolean(),
|
|
16099
|
+
croEnabled: z.boolean(),
|
|
16100
|
+
unattendedEnabled: z.boolean()
|
|
15169
16101
|
});
|
|
15170
16102
|
function apply(ctx, config) {
|
|
15171
16103
|
try {
|
|
@@ -15217,6 +16149,7 @@ function apply(ctx, config) {
|
|
|
15217
16149
|
registerGateway(ctx);
|
|
15218
16150
|
registerRebuildTurnHook(ctx);
|
|
15219
16151
|
registerOutputGuardHook(ctx);
|
|
16152
|
+
registerUnattendedSeam(ctx);
|
|
15220
16153
|
if (config.env) registerEnv(ctx);
|
|
15221
16154
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
15222
16155
|
registerBootstrap(ctx);
|
|
@@ -15256,9 +16189,10 @@ function apply(ctx, config) {
|
|
|
15256
16189
|
* 任何 live 会话,故 apply 时刻就能解析到,不再需要"等一会儿再试"。
|
|
15257
16190
|
*
|
|
15258
16191
|
* 🔒 **必须保留**(它们不是"重试",删了会引入新缺陷):
|
|
15259
|
-
* - **反应式再触发**(`agent/
|
|
15260
|
-
* "恢复"旧会话不触发 `session/created`,但**开始对话**会触发 `agent/
|
|
15261
|
-
*
|
|
16192
|
+
* - **反应式再触发**(`agent/created` / `session/created` → sync):宿主重启后
|
|
16193
|
+
* "恢复"旧会话不触发 `session/created`,但**开始对话**会触发 `agent/created`
|
|
16194
|
+
* (v0.1.7 前名 `agent/session-start`)——这是唯一能把"进程起来时还解析不到、稍后才有会话"
|
|
16195
|
+
* 这条路径接上的通道。
|
|
15262
16196
|
* - **`starting` 在飞标志**:防同拍双次 start → `EADDRINUSE`。
|
|
15263
16197
|
* (C4 块 A 备注:面宿主亦按面名合并在飞启动,是第二道保险;**本标志仍保留**——
|
|
15264
16198
|
* 它管的是"解析 root 这段装配流程"的在飞语义,且调试服务的启动替身在测试里被 mock,
|
|
@@ -15309,7 +16243,7 @@ function registerSkiff(ctx) {
|
|
|
15309
16243
|
sync();
|
|
15310
16244
|
});
|
|
15311
16245
|
} catch {}
|
|
15312
|
-
for (const eventName of ["agent/
|
|
16246
|
+
for (const eventName of ["agent/created", "session/created"]) try {
|
|
15313
16247
|
ctx.on(eventName, () => {
|
|
15314
16248
|
if (!started) sync();
|
|
15315
16249
|
});
|