@shgroup/dsh-serenity-hooks 1.77.0 → 1.78.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/dsh.plugin.json +1 -1
- package/lib/{ccc-roots-rwgzt_0F.js → ccc-roots-D3ty1C8P.js} +1 -1
- package/lib/index.js +588 -2607
- package/lib/{message-source-CYa8LbbM.js → message-source-BOG0piL8.js} +240 -4
- package/lib/{profile-DplLGGqo.js → profile-C7BuyZK7.js} +63 -5
- package/lib/{route-COOFLTpU.js → route-Dr0SFD8A.js} +2 -279
- package/lib/service-DDmHSglV.js +282 -0
- package/lib/session-BxDVrxPH.js +2496 -0
- package/lib/settings-Bb4OnEdI.js +145 -0
- package/lib/tools/trajectory-constraint-admin.d.ts +8 -1
- package/lib/tools/trajectory-self-docs.d.ts +35 -0
- package/lib/trajectory/core.d.ts +4 -1
- package/lib/trajectory/model-runtime.d.ts +97 -0
- package/lib/trajectory/policy/profile.d.ts +50 -5
- package/lib/trajectory/policy/rules.d.ts +17 -4
- package/lib/trajectory/resident.d.ts +57 -0
- package/package.json +1 -1
- package/lib/settings-f9WZXcgi.js +0 -368
package/lib/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { i as __toESM, n as __exportAll } from "./rolldown-runtime-B4iAMlE-.js";
|
|
2
|
-
import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-
|
|
3
|
-
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig,
|
|
4
|
-
import { A as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { a as
|
|
2
|
+
import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-D3ty1C8P.js";
|
|
3
|
+
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CyYJL4F2.js";
|
|
4
|
+
import { A as sessionEvents, B as isoLocal, C as listSessions, E as parseTrajectoryCode, F as trajectoryDescOf, H as localHuman, I as useSession, L as TRAJECTORY_FILENAME, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as TRAJECTORY_LEGACY_FILENAMES, S as listActiveTrajectories, T as parseSessionContextFromEvents, U as localIdStamp, V as localFileStamp, _ as createSession, a as readTrajectoryConstraintAt, b as getActiveSessionInfo, c as trajectoryConstraintForSession, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, m as UID_FILENAME, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, r as listTrajectoryConstraints, s as replacesDefaultIdentity, t as TRAJECTORY_CONSTRAINT_FILENAME, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as readTrajectoryFile } from "./profile-C7BuyZK7.js";
|
|
5
|
+
import { n as checkLocalstoreGitCompliance, o as readStore, s as runLocalStore, t as LOCALSTORE_SCOPES } from "./service-DDmHSglV.js";
|
|
6
|
+
import { _ as ACC_VERSION, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as resolveConstraintSystemPrompt, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as findPackageRoot } from "./message-source-BOG0piL8.js";
|
|
7
|
+
import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-BxDVrxPH.js";
|
|
8
8
|
import { n as hasSessionLogById, r as sessionsRootDir } from "./cleanup-C-92qnDs.js";
|
|
9
|
+
import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-Bb4OnEdI.js";
|
|
9
10
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
11
|
+
import { a as readFeishuCredential, d as weixinSessionIdForTrajectory, i as matchWeixinRoute, l as sanitizeFileName$1, n as extractWeixinText, o as readWeixinCredential, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-Dr0SFD8A.js";
|
|
10
12
|
import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-BMk2w2KY.js";
|
|
11
13
|
import { a as registerImAccountImpl, i as normalizeAccountChannel, n as getImAccountImpl, r as imAccountChannels } from "./accounts-BHQbakUh.js";
|
|
12
14
|
import { createRequire } from "node:module";
|
|
@@ -20,7 +22,7 @@ import { promisify } from "node:util";
|
|
|
20
22
|
import { platform as platform$1 } from "node:process";
|
|
21
23
|
import { fileURLToPath } from "node:url";
|
|
22
24
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
23
|
-
import { createHash,
|
|
25
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
24
26
|
import { Resvg, initWasm } from "@resvg/resvg-wasm";
|
|
25
27
|
import { createServer, request } from "node:http";
|
|
26
28
|
import * as zlib from "node:zlib";
|
|
@@ -1897,2491 +1899,6 @@ async function runMsmAsync(root, args) {
|
|
|
1897
1899
|
}
|
|
1898
1900
|
}
|
|
1899
1901
|
//#endregion
|
|
1900
|
-
//#region src/trajectory/prompt.ts
|
|
1901
|
-
/**
|
|
1902
|
-
* trajectory/prompt.ts — **轨迹提示词**(Trajectory Prompt):一条轨迹"是谁、按什么章程工作"
|
|
1903
|
-
*
|
|
1904
|
-
* ## 概念(owner 2026-10-19 定向,逐字)
|
|
1905
|
-
*
|
|
1906
|
-
* > 「参考会话的系统提示词,我们建立**轨迹提示词**概念来代替人格。架构上会好很多」
|
|
1907
|
-
*
|
|
1908
|
-
* **轨迹提示词** = 一条认知轨迹的**可编辑章程声明**,由 ACC 按固定顺序装配进系统提示词。
|
|
1909
|
-
* 三个关键词各是一条判据:
|
|
1910
|
-
* - **可编辑**:住在 CCC(盘上),改它**不发版**(D23 归属判据的直接推论)
|
|
1911
|
-
* - **声明**:它声明**身份/章程/方法论**,**不声明能力**(能力面归 `trajectory-constraint.json` 白名单与工具 schema)
|
|
1912
|
-
* - **固定顺序**:装配顺序由 ACC 定死(见 `seams/system-prompt.ts` 的 ⑧ 位)
|
|
1913
|
-
*
|
|
1914
|
-
* ## 为什么取代「人格」
|
|
1915
|
-
*
|
|
1916
|
-
* 「人格」把身份绑在"像不像一个人"上 ⇒ 因为叫人格,它被设计成**可以整体替换**
|
|
1917
|
-
* (`persona` 置换 EAP 块,即 review F-05/R-14 那个悬置冲突)。而身份与章程**不该被替换,
|
|
1918
|
-
* 只该被特化**。⇒ 本模块把"替换"改成"叠加":容器层与质量层永远保留,轨迹提示词只往上加。
|
|
1919
|
-
*
|
|
1920
|
-
* ## 落点(为什么在轨迹目录里)
|
|
1921
|
-
*
|
|
1922
|
-
* `AGENT_SESSIONS/<轨迹目录>/PROMPT.md`
|
|
1923
|
-
*
|
|
1924
|
-
* - **跟着轨迹走**:`archive` 时整个目录搬走,章程不会掉队(旧形态 `.opencode/skiff/<name>.md`
|
|
1925
|
-
* 与轨迹**分居两处** —— 多条轨迹共用一份文件正是那个后果)。
|
|
1926
|
-
* - **每条轨迹都能有**:不只是 轨迹约束。普通轨迹同样需要"我是谁、按什么章程"。
|
|
1927
|
-
* - 与 `SESSION.md`(身体:延续/日志)、`trajectory-constraint.json`(约束面:能力)三件并列,
|
|
1928
|
-
* 各管一件事,不合并(合并会让"改能力"与"改身份"互相牵连)。
|
|
1929
|
-
*
|
|
1930
|
-
* ## 兼容(不静默坏)
|
|
1931
|
-
*
|
|
1932
|
-
* `PROMPT.md` 不存在时**不报错**(普通轨迹常态)。轨迹约束的旧字段 `systemPromptFile`
|
|
1933
|
-
* 在兼容期仍被读取——优先级见 {@link resolveTrajectoryPrompt}。
|
|
1934
|
-
*/
|
|
1935
|
-
/** 轨迹提示词的固定文件名(**判据就是它存在**——不是目录名前缀,也不是配置里的一行) */
|
|
1936
|
-
const TRAJECTORY_PROMPT_FILENAME = "PROMPT.md";
|
|
1937
|
-
/** 轨迹目录的 PROMPT.md 绝对路径(不论存在与否——调用方自己判存在性) */
|
|
1938
|
-
function trajectoryPromptPath(root, dirName) {
|
|
1939
|
-
return join(sessionsRoot(root), dirName, TRAJECTORY_PROMPT_FILENAME);
|
|
1940
|
-
}
|
|
1941
|
-
/**
|
|
1942
|
-
* 该轨迹**有没有**轨迹提示词(单一判据)。
|
|
1943
|
-
*
|
|
1944
|
-
* 用途:兼容期的"让位"判定 —— 若已有 `PROMPT.md`,轨迹约束 旧字段 `systemPromptFile`
|
|
1945
|
-
* 必须**让位**,否则同一份章程会在两处各注入一遍(第 ⑧ 段 + `serenity-轨迹约束` 段)。
|
|
1946
|
-
* 🔴 判据只有这一处:别在 轨迹约束那边自己拼路径再判(两处会漂)。
|
|
1947
|
-
*/
|
|
1948
|
-
function hasTrajectoryPrompt(root, dirName) {
|
|
1949
|
-
try {
|
|
1950
|
-
return existsSync(trajectoryPromptPath(root, dirName));
|
|
1951
|
-
} catch {
|
|
1952
|
-
return false;
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
/**
|
|
1956
|
-
* 轨迹提示词的**读取器**(长期热更新;改文件即生效,无需重启)。
|
|
1957
|
-
*
|
|
1958
|
-
* ## 为什么返回函数而不是字符串(R↓,实证代价)
|
|
1959
|
-
*
|
|
1960
|
-
* 旧实现把提示词**快照**进系统提示词段的闭包 ⇒ CCC 改了文件必须重启 dsh web 才生效
|
|
1961
|
-
* (实测:改完 18KB 提示词,live agent 仍用旧文本)。现形态由系统提示词段的 `text()`
|
|
1962
|
-
* **每轮调用**本读取器:按 **mtime + size** 缓存重读(命中缓存零 IO)。
|
|
1963
|
-
*
|
|
1964
|
-
* ## 三条失败语义(各治一个失败模式)
|
|
1965
|
-
*
|
|
1966
|
-
* - **文件不存在** ⇒ 返回空串(**不是错误**:普通轨迹没有章程是常态,且删掉即失效
|
|
1967
|
-
* 必须是立即可见的——这就是"删掉 PROMPT.md 即恢复默认"的机制)
|
|
1968
|
-
* - **读取失败** ⇒ 返回**上次成功内容**(⛔ 绝不返回空:空提示词会让轨迹失去章程),
|
|
1969
|
-
* 并按去重标签告警一次
|
|
1970
|
-
* - **首次就读不到** ⇒ 空串 + 告警一次(区分"本来没有"与"有但读不了")
|
|
1971
|
-
*
|
|
1972
|
-
* @param root CCC 根
|
|
1973
|
-
* @param dirName 轨迹目录名(**不是编码**:目录名才是落盘位置)
|
|
1974
|
-
*/
|
|
1975
|
-
function createTrajectoryPromptReader(root, dirName) {
|
|
1976
|
-
const absPath = trajectoryPromptPath(root, dirName);
|
|
1977
|
-
let cachedMtime = -1;
|
|
1978
|
-
let cachedSize = -1;
|
|
1979
|
-
let cachedText = "";
|
|
1980
|
-
let warned = false;
|
|
1981
|
-
return () => {
|
|
1982
|
-
let st;
|
|
1983
|
-
try {
|
|
1984
|
-
st = statSync(absPath);
|
|
1985
|
-
} catch (err) {
|
|
1986
|
-
const code = err?.code;
|
|
1987
|
-
cachedMtime = -1;
|
|
1988
|
-
cachedSize = -1;
|
|
1989
|
-
cachedText = "";
|
|
1990
|
-
if (code !== "ENOENT" && !warned) {
|
|
1991
|
-
warned = true;
|
|
1992
|
-
console.warn(`[serenity-hooks] ✗ 轨迹提示词不可读(${absPath})——按"没有章程"处理: ${String(err?.message ?? err)}`);
|
|
1993
|
-
}
|
|
1994
|
-
return "";
|
|
1995
|
-
}
|
|
1996
|
-
try {
|
|
1997
|
-
if (st.mtimeMs === cachedMtime && st.size === cachedSize) return cachedText;
|
|
1998
|
-
const text = readUtf8(absPath).trim();
|
|
1999
|
-
const isReload = cachedMtime !== -1;
|
|
2000
|
-
cachedMtime = st.mtimeMs;
|
|
2001
|
-
cachedSize = st.size;
|
|
2002
|
-
cachedText = text;
|
|
2003
|
-
warned = false;
|
|
2004
|
-
if (isReload) console.info(`[serenity-hooks] ↻ 轨迹提示词已热重载(${TRAJECTORY_PROMPT_FILENAME},${text.length} 字符)`);
|
|
2005
|
-
return text;
|
|
2006
|
-
} catch (err) {
|
|
2007
|
-
if (!warned) {
|
|
2008
|
-
warned = true;
|
|
2009
|
-
console.warn(`[serenity-hooks] ✗ 轨迹提示词读取失败(${absPath})——沿用上次成功内容: ${String(err?.message ?? err)}`);
|
|
2010
|
-
}
|
|
2011
|
-
return cachedText;
|
|
2012
|
-
}
|
|
2013
|
-
};
|
|
2014
|
-
}
|
|
2015
|
-
/**
|
|
2016
|
-
* 装配用的**提示词段**(幂等:无内容 ⇒ 空串,宿主会丢弃空 section/空块)。
|
|
2017
|
-
*
|
|
2018
|
-
* 标记头 `=== Serenity Trajectory Prompt ===`:
|
|
2019
|
-
* ① 让模型知道"下面是**这条轨迹自己的章程**",不是容器规则;
|
|
2020
|
-
* ② 让诊断可以按块名 grep。
|
|
2021
|
-
*
|
|
2022
|
-
* 🔴 这段**追加**在容器层与质量层之后,**永不替换它们**——那正是本概念相对「人格」的核心修正。
|
|
2023
|
-
*/
|
|
2024
|
-
function trajectoryPromptBlock(text) {
|
|
2025
|
-
const body = text.trim();
|
|
2026
|
-
if (body === "") return "";
|
|
2027
|
-
return [
|
|
2028
|
-
"",
|
|
2029
|
-
"=== Serenity Trajectory Prompt ===",
|
|
2030
|
-
body,
|
|
2031
|
-
""
|
|
2032
|
-
].join("\n");
|
|
2033
|
-
}
|
|
2034
|
-
//#endregion
|
|
2035
|
-
//#region src/trajectory/carrier.ts
|
|
2036
|
-
/**
|
|
2037
|
-
* 等待 agent 空闲或销毁。
|
|
2038
|
-
* @param ctx 宿主插件上下文(事件通道来源;**结构化访问**,故类型为 `unknown`,见文件头 I2 段)
|
|
2039
|
-
* @param agent 目标 agent
|
|
2040
|
-
* @returns 空闲、agent 销毁、或会话销毁时结算(永不 reject)
|
|
2041
|
-
*/
|
|
2042
|
-
function waitAgentIdle(ctx, agent) {
|
|
2043
|
-
return new Promise((resolve) => {
|
|
2044
|
-
let settled = false;
|
|
2045
|
-
const disposers = [];
|
|
2046
|
-
const finish = () => {
|
|
2047
|
-
if (settled) return;
|
|
2048
|
-
settled = true;
|
|
2049
|
-
for (const dispose of disposers) try {
|
|
2050
|
-
dispose();
|
|
2051
|
-
} catch {}
|
|
2052
|
-
resolve();
|
|
2053
|
-
};
|
|
2054
|
-
const subscribe = (name, cb) => {
|
|
2055
|
-
try {
|
|
2056
|
-
const off = ctx.on?.(name, cb);
|
|
2057
|
-
if (typeof off === "function") disposers.push(off);
|
|
2058
|
-
} catch {}
|
|
2059
|
-
};
|
|
2060
|
-
subscribe("agent/status", (payload) => {
|
|
2061
|
-
const p = payload;
|
|
2062
|
-
if (p?.agent === agent && p?.status === "idle") finish();
|
|
2063
|
-
});
|
|
2064
|
-
subscribe("agent/disposed", (payload) => {
|
|
2065
|
-
if (payload?.agent === agent) finish();
|
|
2066
|
-
});
|
|
2067
|
-
const sessionId = agent.session?.id;
|
|
2068
|
-
subscribe("session/disposed", (session) => {
|
|
2069
|
-
const id = session?.id;
|
|
2070
|
-
if (typeof id === "string" && id === sessionId) finish();
|
|
2071
|
-
});
|
|
2072
|
-
if (agent.status === "idle") finish();
|
|
2073
|
-
});
|
|
2074
|
-
}
|
|
2075
|
-
//#endregion
|
|
2076
|
-
//#region src/skills/discovery.ts
|
|
2077
|
-
/**
|
|
2078
|
-
* skills/discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
|
|
2079
|
-
*
|
|
2080
|
-
* 发现全部入口技能(原文全量,不截断)—— **现行三个来源**(原为四个,**来源 3 已于 2026-09-21 删除**):
|
|
2081
|
-
* 0. **`.serenity` 记号文件内容 = 顶层入口 skill 名**(CCC 记号文件的权威语义)—— 最高优先
|
|
2082
|
-
* 1. `.dsh/entry-skill` 指针文件(内容 = skill 名)—— 兼容旧约定
|
|
2083
|
-
* 2. `.opencode/skills/*-serenity/SKILL.md` —— 自动扫描该 CCC 的顶层入口
|
|
2084
|
-
* (命名模式 `*-serenity`)
|
|
2085
|
-
* ~~3. `.dsh/skills/*-serenity/SKILL.md`(扫 ACC 装进 CCC 的技能副本)~~ —— 🔴 **已删**:
|
|
2086
|
-
* owner 2026-09-21 令「连模板和安装命令一起删」(B 案)。**判据、实测证据与日期逐字记在
|
|
2087
|
-
* `findEntrySkills()` 体内那一段注释里**——本头部只留结论,**不复述判据**(避免两处会漂的真相源)。
|
|
2088
|
-
* 按名去重;顺序 = 记号文件 → 指针 → opencode 入口。
|
|
2089
|
-
*
|
|
2090
|
-
* 任何 CCC 都能自动注入其顶层入口 skill 全文(不硬编码名字)。
|
|
2091
|
-
*/
|
|
2092
|
-
/** 顶层入口命名模式:目录名以 -serenity 结尾(home-serenity / tg-serenity / acc-serenity …) */
|
|
2093
|
-
const SERENITY_SUFFIX = "-serenity";
|
|
2094
|
-
function isSerenityEntry(name) {
|
|
2095
|
-
return name.endsWith(SERENITY_SUFFIX);
|
|
2096
|
-
}
|
|
2097
|
-
/** 扫描某 skills 根下所有 `*-serenity` 目录(含 SKILL.md 的) */
|
|
2098
|
-
function scanSerenityDirs(skillsRoot) {
|
|
2099
|
-
if (!existsSync(skillsRoot)) return [];
|
|
2100
|
-
return readdirSync(skillsRoot).filter((name) => isSerenityEntry(name)).map((name) => join(skillsRoot, name)).filter((dir) => statSync(dir).isDirectory() && existsSync(join(dir, "SKILL.md"))).sort();
|
|
2101
|
-
}
|
|
2102
|
-
/**
|
|
2103
|
-
* 防穿越:skill 名必须是简单目录名(拒绝分隔符与 `..`/`.` 路径段)。
|
|
2104
|
-
* v1.34.1:导出——轨迹 skill 声明(CCC 数据)的名字也要**先过这一关**才能用于拼路径
|
|
2105
|
-
* (规格 `docs/cognitive-trajectory-skill-injection.md` §3「名字安全(必做)」)。
|
|
2106
|
-
*/
|
|
2107
|
-
function isSafeSkillName(name) {
|
|
2108
|
-
if (name.length === 0) return false;
|
|
2109
|
-
if (name.includes("/") || name.includes("\\")) return false;
|
|
2110
|
-
return !name.split(/[/\\]/).some((seg) => seg === ".." || seg === ".");
|
|
2111
|
-
}
|
|
2112
|
-
/**
|
|
2113
|
-
* 按名在两个 skills 根下定位 SKILL.md(`.dsh/skills` 优先,其次 `.opencode/skills`)。
|
|
2114
|
-
* v1.34.1:导出——轨迹 skill 注入复用本函数(不新造发现逻辑);不安全名在此**先行拒绝**
|
|
2115
|
-
* (返回 null,且不发生任何以该名拼出的 fs 访问)。
|
|
2116
|
-
*/
|
|
2117
|
-
function findSkillMd(root, name) {
|
|
2118
|
-
if (!isSafeSkillName(name)) return null;
|
|
2119
|
-
for (const base of [".dsh", ".opencode"]) {
|
|
2120
|
-
const p = resolve(root, base, "skills", name, "SKILL.md");
|
|
2121
|
-
if (existsSync(p)) return p;
|
|
2122
|
-
}
|
|
2123
|
-
return null;
|
|
2124
|
-
}
|
|
2125
|
-
/** 发现全部入口技能(原文全量,不截断) */
|
|
2126
|
-
function findEntrySkills(root) {
|
|
2127
|
-
const out = [];
|
|
2128
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2129
|
-
const add = (path, source, forcedName) => {
|
|
2130
|
-
if (!existsSync(path)) return;
|
|
2131
|
-
const name = forcedName ?? basename(dirname(path));
|
|
2132
|
-
if (seen.has(name)) return;
|
|
2133
|
-
seen.add(name);
|
|
2134
|
-
out.push({
|
|
2135
|
-
name,
|
|
2136
|
-
content: readFileSync(path, "utf-8"),
|
|
2137
|
-
dir: dirname(path),
|
|
2138
|
-
source
|
|
2139
|
-
});
|
|
2140
|
-
};
|
|
2141
|
-
const marker = resolve(root, ".serenity");
|
|
2142
|
-
if (existsSync(marker)) {
|
|
2143
|
-
const name = readFileSync(marker, "utf-8").trim();
|
|
2144
|
-
if (name) {
|
|
2145
|
-
const md = findSkillMd(root, name);
|
|
2146
|
-
if (md) add(md, "serenity-marker", name);
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
const pointer = resolve(root, ".dsh", "entry-skill");
|
|
2150
|
-
if (existsSync(pointer)) {
|
|
2151
|
-
const name = readFileSync(pointer, "utf-8").trim();
|
|
2152
|
-
if (name) {
|
|
2153
|
-
const md = findSkillMd(root, name);
|
|
2154
|
-
if (md) add(md, "pointer", name);
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
for (const dir of scanSerenityDirs(resolve(root, ".opencode", "skills"))) add(join(dir, "SKILL.md"), "opencode");
|
|
2158
|
-
return out;
|
|
2159
|
-
}
|
|
2160
|
-
function truncateContent(content, maxChars) {
|
|
2161
|
-
if (content.length <= maxChars) return content;
|
|
2162
|
-
return content.slice(0, maxChars) + `\n... (truncated, original length ${content.length} chars)`;
|
|
2163
|
-
}
|
|
2164
|
-
//#endregion
|
|
2165
|
-
//#region src/skills/resource-index.ts
|
|
2166
|
-
/**
|
|
2167
|
-
* resource-index.ts — **两层披露树的第二层**:skill 参考件索引(纯逻辑,零 DSH 依赖)
|
|
2168
|
-
*
|
|
2169
|
-
* 设计真相源:`docs/serenity-skill-design.md` §4.2(六条硬性规则)+ §5.1(目录布局)。
|
|
2170
|
-
* 一句话:**把 skill 的参考件"有什么"告诉模型,但不把"是什么"塞进去** ——
|
|
2171
|
-
* 正文留给第三层(模型按需 `read`)。
|
|
2172
|
-
*
|
|
2173
|
-
* 为什么由我们生成(R↓,读宿主取证):宿主 `@deepseek-ai/dsh-skill` 的 `renderSkillContent()`
|
|
2174
|
-
* (`lib/index.js` L57-81)**已经**产出 `<skill_resources>` 块,并逐字写着
|
|
2175
|
-
* "Load referenced resources only as needed." ⇒ **两层模型是宿主自己给的**;而
|
|
2176
|
-
* `SkillSummary` / `SkillDefinition` **没有任何列举子文件的位**(只有 `resourceBase` 一个基址)
|
|
2177
|
-
* ⇒ 宿主刻意留给 provider 的正是**这一半:枚举**。
|
|
2178
|
-
*
|
|
2179
|
-
* 六条硬性规则(逐条对应设计 §4.2,改这里先改那里):
|
|
2180
|
-
* ① **只枚举,不注入正文** —— 索引里**绝不**出现任何参考文件的正文;
|
|
2181
|
-
* ② **确定性排序** = 相对路径的 **UTF-8 字节序**(🔴 **不用 mtime**:mtime 随检出漂移,
|
|
2182
|
-
* 会让每请求的注入文本抖动;对照 `src/cro/core.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
|
|
2183
|
-
* **同一件事两处的取舍相反,那个差异是判据不是巧合**);
|
|
2184
|
-
* ③ **有界 + 响亮** —— 条数/字符各有上限,超出**必须显式写出还剩多少项**,**绝不静默截断**;
|
|
2185
|
-
* ④ **摘要可复现** —— 取该文件首个 `#` 标题(剥 markdown 记号);取不到 ⇒ 留空,**不编**;
|
|
2186
|
-
* ⑤ **本体文件特殊标注** —— `skill.ttl` 存在 ⇒ 该行标 🧬 + 顶部加一句"已建模";
|
|
2187
|
-
* ⑥ **范围 = `references/` 一级条目 + `skill.ttl`** —— 子目录**给名字 + 计数、不展开、不读正文**
|
|
2188
|
-
* (🆕 2026-10-11 由**真实输入**逼出的修订:原写"不进子目录",实测 `references/` 下确实有内容目录,
|
|
2189
|
-
* 只报"有 N 个子目录"不给名字 = **不可行动**;折中仍在裁决 Q2「只披露名字、不注入正文」的意图内)。
|
|
2190
|
-
*
|
|
2191
|
-
* 🔴 本文件**只认 `<skillDir>/skill.ttl`**(owner 裁决 Q6:静默、只认固定名)。
|
|
2192
|
-
* `references/*-domain.ttl`(旧名)一律当**普通参考件**:不特殊对待、不告警 —— 那 14 份由 CCC 自行改名。
|
|
2193
|
-
* 🔴 「未建模」不是异常(裁决 Q6 / 判据 J9):无 `skill.ttl` 的**普通 skill** 照常产出索引,
|
|
2194
|
-
* **零告警、零误差** —— 增强必须对普通 skill 完全向后兼容。
|
|
2195
|
-
*
|
|
2196
|
-
* 🔴 **两个面,两个入口**(2026-10-12 增;owner 裁决 O2)—— 同源、但成本形态不同:
|
|
2197
|
-
* · **载入面** `buildResourceIndex()` —— 完整索引(名字 + 首标题 + 字节),带 `.md` 文件头读取;
|
|
2198
|
-
* 在三条路径上各附一次:入口段(`system-prompt.ts` order −50)/轨迹段(`trajectory/skills.ts`
|
|
2199
|
-
* order −45)/目录 provider 的 `get()`(`opencode-skills.ts`)。
|
|
2200
|
-
* · **目录面** `buildResourceNameSuffix()` —— **只有名字**,**不读任何正文**;只挂一处:
|
|
2201
|
-
* 目录 provider 的 `list()`(`available_skills` 的 `SkillSummary.description`)。
|
|
2202
|
-
* 🔴 为什么目录面在这里:`SkillSummary` 契约里**没有列资源的位**(只有 name/description/whenToUse)
|
|
2203
|
-
* ⇒ `description` 是唯一位置,且必须**自动生成**。
|
|
2204
|
-
* ⇒ 两个面**共用** `collectEntries()`(范围 = 规则⑥只写一遍)。
|
|
2205
|
-
*/
|
|
2206
|
-
/** 第二层占位口:本 skill 的本体约束文件(固定名,设计 §5.1) */
|
|
2207
|
-
const SKILL_ONTOLOGY_FILE = "skill.ttl";
|
|
2208
|
-
/** 参考件根(一级;不进子目录) */
|
|
2209
|
-
const REFERENCE_DIR = "references";
|
|
2210
|
-
/** 摘要长度上限(防单行超长标题把表格撑坏) */
|
|
2211
|
-
const EXCERPT_MAX_CHARS = 60;
|
|
2212
|
-
/** 读文件头部字节数上限:摘标题只值读这么些(不为一行标题读整个大文件) */
|
|
2213
|
-
const HEAD_BYTES = 4096;
|
|
2214
|
-
/** 目录计数上限:防病态大树把一次注入拖慢(超出 ⇒ 报 `≥N`,仍然响亮) */
|
|
2215
|
-
const DIR_SCAN_MAX_FILES = 1e3;
|
|
2216
|
-
/**
|
|
2217
|
-
* 递归统计目录下的文件数与总字节(**只统计,不读内容**)。
|
|
2218
|
-
* 为什么递归:设计原写"不进子目录",而**真实输入**显示 `references/` 下确有内容目录
|
|
2219
|
-
* (`home-rhetoric/references/corpus-subject-own`/`family-profiles/references/baby-handlebook`/
|
|
2220
|
-
* `home-serenity/references/diagram-lab`)⇒ 只报"有 N 个子目录"而**不给名字**是不可行动的。
|
|
2221
|
-
* 折中:**给名字 + 计数,仍不展开、仍不读正文**(= 裁决 Q2「只披露名字、不注入正文」的意图内)。
|
|
2222
|
-
*/
|
|
2223
|
-
function scanDirStats(dir) {
|
|
2224
|
-
let files = 0;
|
|
2225
|
-
let bytes = 0;
|
|
2226
|
-
let capped = false;
|
|
2227
|
-
const walk = (d) => {
|
|
2228
|
-
if (capped || files >= DIR_SCAN_MAX_FILES) {
|
|
2229
|
-
capped = true;
|
|
2230
|
-
return;
|
|
2231
|
-
}
|
|
2232
|
-
let names = [];
|
|
2233
|
-
try {
|
|
2234
|
-
names = readdirSync(d);
|
|
2235
|
-
} catch {
|
|
2236
|
-
return;
|
|
2237
|
-
}
|
|
2238
|
-
for (const name of names) {
|
|
2239
|
-
if (files >= DIR_SCAN_MAX_FILES) {
|
|
2240
|
-
capped = true;
|
|
2241
|
-
return;
|
|
2242
|
-
}
|
|
2243
|
-
const abs = join(d, name);
|
|
2244
|
-
let isDir = false;
|
|
2245
|
-
let size = 0;
|
|
2246
|
-
try {
|
|
2247
|
-
const st = statSync(abs);
|
|
2248
|
-
isDir = st.isDirectory();
|
|
2249
|
-
size = st.size;
|
|
2250
|
-
} catch {
|
|
2251
|
-
continue;
|
|
2252
|
-
}
|
|
2253
|
-
if (isDir) walk(abs);
|
|
2254
|
-
else {
|
|
2255
|
-
files++;
|
|
2256
|
-
bytes += size;
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
};
|
|
2260
|
-
walk(dir);
|
|
2261
|
-
return {
|
|
2262
|
-
files,
|
|
2263
|
-
bytes,
|
|
2264
|
-
capped
|
|
2265
|
-
};
|
|
2266
|
-
}
|
|
2267
|
-
/** 按 UTF-8 **字节序**比较两个相对路径(规则②;不是 locale 比较、不是 mtime)。
|
|
2268
|
-
* 参数放宽到 `{ key }`:**未加水**的条目与**已加水**的条目共用同一个比较器 ⇒ 排序规则只写一遍。 */
|
|
2269
|
-
function byByteOrder(a, b) {
|
|
2270
|
-
return Buffer.compare(a.key, b.key);
|
|
2271
|
-
}
|
|
2272
|
-
/**
|
|
2273
|
-
* 千分位分组(规则②的延伸:**不依赖 locale**)。
|
|
2274
|
-
* 为什么不用 `toLocaleString()`:它随宿主 locale 变 ⇒ 同一目录在不同机器上产出**不同的注入文本**
|
|
2275
|
-
* (本仓已有一条「locale 稳健性」的挂账,不再添一处)。
|
|
2276
|
-
*/
|
|
2277
|
-
function groupThousands(n) {
|
|
2278
|
-
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
2279
|
-
}
|
|
2280
|
-
/**
|
|
2281
|
-
* 取 markdown 首个 `#` 标题作摘要(规则④)。
|
|
2282
|
-
*
|
|
2283
|
-
* 细节(都由"可复现"推导):
|
|
2284
|
-
* · **跳过 YAML frontmatter**(`---` 开头到下一 `---`)—— 否则题名行会被当成正文标题;
|
|
2285
|
-
* · 只读文件**头部** `HEAD_BYTES`(一行标题不值得读整个大文件);
|
|
2286
|
-
* · 剥掉行内记号(`*` `` ` `` `_` `#`)与首尾空白,压平内部连续空白;
|
|
2287
|
-
* · 取不到 ⇒ `''`(**留空,不编** —— 编一个摘要等于让索引说假话);
|
|
2288
|
-
* · 超长截断时**留省略号**(与规则③同一精神:截断要看得见)。
|
|
2289
|
-
*/
|
|
2290
|
-
function firstHeading(file) {
|
|
2291
|
-
let head;
|
|
2292
|
-
try {
|
|
2293
|
-
head = readFileSync(file, "utf-8").slice(0, HEAD_BYTES);
|
|
2294
|
-
} catch {
|
|
2295
|
-
return "";
|
|
2296
|
-
}
|
|
2297
|
-
const lines = head.split(/\r?\n/);
|
|
2298
|
-
let start = 0;
|
|
2299
|
-
if (lines[0]?.trim() === "---") {
|
|
2300
|
-
for (let i = 1; i < lines.length; i++) if (lines[i]?.trim() === "---") {
|
|
2301
|
-
start = i + 1;
|
|
2302
|
-
break;
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
for (let i = start; i < lines.length; i++) {
|
|
2306
|
-
const m = /^#{1,6}\s+(.*)$/.exec(lines[i] ?? "");
|
|
2307
|
-
if (!m) continue;
|
|
2308
|
-
const clean = (m[1] ?? "").replace(/[*`_]/g, "").replace(/\s+/g, " ").trim();
|
|
2309
|
-
if (clean === "") continue;
|
|
2310
|
-
return clean.length > EXCERPT_MAX_CHARS ? `${clean.slice(0, 59)}…` : clean;
|
|
2311
|
-
}
|
|
2312
|
-
return "";
|
|
2313
|
-
}
|
|
2314
|
-
/**
|
|
2315
|
-
* 收集第二层的**条目**(范围 = 规则⑥:`references/` 一级条目 + 固定名本体)。
|
|
2316
|
-
*
|
|
2317
|
-
* 🔴 **只做 `readdir` / `stat`,不读任何文件正文** —— 两个消费者(第二层索引 / 目录面的名字清单)
|
|
2318
|
-
* **共用这一份范围定义**:规则⑥只该有一个家,各写一遍必然漂移。
|
|
2319
|
-
* 🔴 返回已按**字节序**排好(规则②:确定性,不用 mtime)。
|
|
2320
|
-
*/
|
|
2321
|
-
function collectEntries(skillDir) {
|
|
2322
|
-
const entries = [];
|
|
2323
|
-
const refDir = join(skillDir, REFERENCE_DIR);
|
|
2324
|
-
if (existsSync(refDir)) {
|
|
2325
|
-
let names = [];
|
|
2326
|
-
try {
|
|
2327
|
-
names = readdirSync(refDir).sort();
|
|
2328
|
-
} catch {
|
|
2329
|
-
names = [];
|
|
2330
|
-
}
|
|
2331
|
-
for (const name of names) {
|
|
2332
|
-
const abs = join(refDir, name);
|
|
2333
|
-
let isDir = false;
|
|
2334
|
-
try {
|
|
2335
|
-
isDir = statSync(abs).isDirectory();
|
|
2336
|
-
} catch {
|
|
2337
|
-
continue;
|
|
2338
|
-
}
|
|
2339
|
-
const rel = `${REFERENCE_DIR}/${name}`;
|
|
2340
|
-
entries.push({
|
|
2341
|
-
rel,
|
|
2342
|
-
kind: isDir ? "dir" : "file",
|
|
2343
|
-
abs,
|
|
2344
|
-
ontology: false,
|
|
2345
|
-
key: Buffer.from(rel, "utf-8")
|
|
2346
|
-
});
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
const ontoAbs = join(skillDir, SKILL_ONTOLOGY_FILE);
|
|
2350
|
-
if (existsSync(ontoAbs)) entries.push({
|
|
2351
|
-
rel: SKILL_ONTOLOGY_FILE,
|
|
2352
|
-
kind: "file",
|
|
2353
|
-
abs: ontoAbs,
|
|
2354
|
-
ontology: true,
|
|
2355
|
-
key: Buffer.from(SKILL_ONTOLOGY_FILE, "utf-8")
|
|
2356
|
-
});
|
|
2357
|
-
entries.sort(byByteOrder);
|
|
2358
|
-
return entries;
|
|
2359
|
-
}
|
|
2360
|
-
/**
|
|
2361
|
-
* 给条目**加水**(字节 + 摘要)—— 这两项都要读盘,因此与"有哪些条目"分开。
|
|
2362
|
-
* 读不到 ⇒ `null`(宁可少列,也不列出读不到的项)。
|
|
2363
|
-
*/
|
|
2364
|
-
function hydrate(entry) {
|
|
2365
|
-
if (entry.kind === "dir") {
|
|
2366
|
-
const { files, bytes, capped } = scanDirStats(entry.abs);
|
|
2367
|
-
return {
|
|
2368
|
-
rel: entry.rel,
|
|
2369
|
-
kind: "dir",
|
|
2370
|
-
bytes,
|
|
2371
|
-
excerpt: `子目录 · ${capped ? `≥${groupThousands(files)}` : groupThousands(files)} 个文件`,
|
|
2372
|
-
ontology: false,
|
|
2373
|
-
key: entry.key
|
|
2374
|
-
};
|
|
2375
|
-
}
|
|
2376
|
-
let bytes;
|
|
2377
|
-
try {
|
|
2378
|
-
bytes = statSync(entry.abs).size;
|
|
2379
|
-
} catch {
|
|
2380
|
-
return null;
|
|
2381
|
-
}
|
|
2382
|
-
return {
|
|
2383
|
-
rel: entry.rel,
|
|
2384
|
-
kind: "file",
|
|
2385
|
-
bytes,
|
|
2386
|
-
excerpt: entry.rel.endsWith(".md") ? firstHeading(entry.abs) : "",
|
|
2387
|
-
ontology: entry.ontology,
|
|
2388
|
-
key: entry.key
|
|
2389
|
-
};
|
|
2390
|
-
}
|
|
2391
|
-
/** 收集候选(`references/` 一级条目 + 固定名本体),加好水并按字节序排好 */
|
|
2392
|
-
function collectItems(skillDir) {
|
|
2393
|
-
const items = [];
|
|
2394
|
-
for (const entry of collectEntries(skillDir)) {
|
|
2395
|
-
const item = hydrate(entry);
|
|
2396
|
-
if (item !== null) items.push(item);
|
|
2397
|
-
}
|
|
2398
|
-
return items;
|
|
2399
|
-
}
|
|
2400
|
-
/**
|
|
2401
|
-
* 生成第二层索引正文。
|
|
2402
|
-
*
|
|
2403
|
-
* @param skillDir skill 目录绝对路径
|
|
2404
|
-
* @param options 上限覆盖(缺省用本模块常量)
|
|
2405
|
-
* @returns 索引正文;**无可列项 ⇒ `''`**(规则⑥的一般化:`references/` 与 `skill.ttl` 都没有
|
|
2406
|
-
* ⇒ 整块不产出 —— 空标题是噪声)。调用方按"空串不产出该块"处理。
|
|
2407
|
-
*/
|
|
2408
|
-
function buildResourceIndex(skillDir, options = {}) {
|
|
2409
|
-
const maxItems = options.maxItems ?? 40;
|
|
2410
|
-
const maxChars = options.maxChars ?? 4096;
|
|
2411
|
-
const items = collectItems(skillDir);
|
|
2412
|
-
if (items.length === 0) return "";
|
|
2413
|
-
const modeled = items.some((i) => i.ontology);
|
|
2414
|
-
const shown = items.slice(0, maxItems);
|
|
2415
|
-
const hidden = items.length - shown.length;
|
|
2416
|
-
const lines = ["### 本 skill 的参考件(第二层 · 按需读取)", "> 这里只有**清单**,没有正文:需要时用 `read` 打开下列路径(相对本 skill 目录)。"];
|
|
2417
|
-
if (modeled) lines.push("> 🧬 **本 skill 已建模**:文字内容以 `skill.ttl` 为总约束。");
|
|
2418
|
-
lines.push("", "| 文件 | 摘要 | 字节 |", "|---|---|---|");
|
|
2419
|
-
for (const item of shown) {
|
|
2420
|
-
const label = item.kind === "dir" ? `${item.rel}/` : item.rel;
|
|
2421
|
-
const excerpt = item.ontology ? "🧬 本 skill 的本体约束(已建模)" : item.excerpt;
|
|
2422
|
-
lines.push(`| ${label} | ${excerpt} | ${groupThousands(item.bytes)} |`);
|
|
2423
|
-
}
|
|
2424
|
-
if (hidden > 0) lines.push("", `…**还有 ${hidden} 项未列出**(本层单次上限 ${maxItems} 项)。`);
|
|
2425
|
-
const body = lines.join("\n");
|
|
2426
|
-
if (body.length <= maxChars) return body;
|
|
2427
|
-
return `${body.slice(0, maxChars)}\n…(第二层索引已按 ${groupThousands(maxChars)} 字符上限截断,原文 ${groupThousands(body.length)} 字符)`;
|
|
2428
|
-
}
|
|
2429
|
-
/**
|
|
2430
|
-
* 收集**只有名字**的第二层清单(目录面专用)。
|
|
2431
|
-
*
|
|
2432
|
-
* 为什么单独一个入口(而不是复用 `buildResourceIndex`):**成本形态不同** ——
|
|
2433
|
-
* 索引要给每个 `.md` 读文件头取摘要(规则④),而目录面是**每请求 × 每个 skill** 都要算的
|
|
2434
|
-
* ⇒ 这条路径**一个字节正文都不读**(只 `readdir`/`stat`)。
|
|
2435
|
-
* 范围仍与索引**同源**(共用 `collectEntries` ⇒ 规则⑥只有一个家)。
|
|
2436
|
-
*/
|
|
2437
|
-
function buildResourceNames(skillDir, options = {}) {
|
|
2438
|
-
const maxItems = options.maxItems ?? 40;
|
|
2439
|
-
const entries = collectEntries(skillDir);
|
|
2440
|
-
const shown = entries.slice(0, maxItems);
|
|
2441
|
-
return {
|
|
2442
|
-
names: shown.map((e) => e.kind === "dir" ? `${basename(e.rel)}/` : basename(e.rel)),
|
|
2443
|
-
hidden: entries.length - shown.length
|
|
2444
|
-
};
|
|
2445
|
-
}
|
|
2446
|
-
/**
|
|
2447
|
-
* 目录面后缀:把名字清单拼成**可直接接在 `description` 后面**的一段文本。
|
|
2448
|
-
*
|
|
2449
|
-
* 🔴 **自动生成,绝不手写**(手写必与文件树漂);无可列项 ⇒ `''`(**普通 skill 零噪声** —— 判据 J9)。
|
|
2450
|
-
* 🔴 **有界 + 响亮**(规则③):超上限时写出"另有 N 项",**绝不静默截断**。
|
|
2451
|
-
*/
|
|
2452
|
-
function buildResourceNameSuffix(skillDir, options = {}) {
|
|
2453
|
-
const { names, hidden } = buildResourceNames(skillDir, options);
|
|
2454
|
-
if (names.length === 0) return "";
|
|
2455
|
-
const tail = hidden > 0 ? ` …另有 ${hidden} 项` : "";
|
|
2456
|
-
return `(参考件:${names.join(" / ")}${tail})`;
|
|
2457
|
-
}
|
|
2458
|
-
//#endregion
|
|
2459
|
-
//#region src/trajectory/skills.ts
|
|
2460
|
-
/**
|
|
2461
|
-
* trajectory/skills.ts — 轨迹 skill 声明解析 + 注入内容装配(纯逻辑,零 DSH 依赖)
|
|
2462
|
-
*
|
|
2463
|
-
* 接口真相源:`docs/cognitive-trajectory-skill-injection.md` v1.0(§2 数据契约 / §3 注入契约)。
|
|
2464
|
-
* 一句话:**轨迹自己声明它要挂哪些 skill**(`SESSION.md` 顶部 YAML frontmatter 的 `skills:`),
|
|
2465
|
-
* ACC 在**绑定期间**把这些 skill 的 `SKILL.md` 全文注入系统提示词。
|
|
2466
|
-
*
|
|
2467
|
-
* 分工(R↓:为什么解析与注册分开):
|
|
2468
|
-
* · 本模块 = **纯文本层**(解析 / 装配 / 安全判定)——可脱离 DSH 单测;
|
|
2469
|
-
* · `seams/system-prompt.ts` 的 `registerCognitiveTrajectorySkillSection` = **宿主接入层**
|
|
2470
|
-
* (per-agent `ctx.systemPrompt.section`),只做门与注册,不再解析文本。
|
|
2471
|
-
*
|
|
2472
|
-
* 边界(规格 §2.2):**不解析** `skills` 以外任何键的语义(CCC 可自由使用,ACC 视而不见);
|
|
2473
|
-
* **不改写** frontmatter(ACC 只读)。
|
|
2474
|
-
*/
|
|
2475
|
-
/** 全文注入总长上限(字符数;规格 §3「缺省 32 KB」——防上下文爆炸) */
|
|
2476
|
-
const COGNITIVE_TRAJECTORY_SKILLS_MAX_CHARS = 32768;
|
|
2477
|
-
/** 缺失标记(规格 §3/§6-3 的验收锚点:任何"拿不到全文"的情况都必须带它,绝不静默)
|
|
2478
|
-
* 🔴 2026-09-25(S142 分批清死代码·第 1 批):**降为模块内常量**——全仓唯一消费者在本文件,
|
|
2479
|
-
* 导出只服务测试(测试用的是字面量 `'[缺失]'`)⇒ 导出面无意义。 */
|
|
2480
|
-
const SKILL_MISSING_MARK = "[缺失]";
|
|
2481
|
-
/** 统一缺失提示(规格 §3 逐字约定:`[缺失] <name>(未找到该 skill)`) */
|
|
2482
|
-
function missingNotice(name) {
|
|
2483
|
-
return `${SKILL_MISSING_MARK} ${name}(未找到该 skill)`;
|
|
2484
|
-
}
|
|
2485
|
-
/**
|
|
2486
|
-
* 抽取文件顶部 YAML frontmatter 区(规格 §2.1)。
|
|
2487
|
-
*
|
|
2488
|
-
* 规则:**第一行必须是 `---`**;到下一个 `---` 行为止是 frontmatter 区。
|
|
2489
|
-
* 首行不是 `---`,或没有闭合 `---` ⇒ 视为**无 frontmatter**(返回 null)。
|
|
2490
|
-
* 边界判断(R↓):未闭合块按"无 frontmatter"处理而非吞掉整个文件——半个声明块是格式错误,
|
|
2491
|
-
* 猜它的意图等于把坏数据当好消息(宁可零注入,也不注入来路不明的半截列表)。
|
|
2492
|
-
*
|
|
2493
|
-
* @param content SESSION.md 全文
|
|
2494
|
-
* @returns frontmatter 区各行(不含两条 `---` 定界行);无 → null
|
|
2495
|
-
*/
|
|
2496
|
-
function extractFrontmatter(content) {
|
|
2497
|
-
const lines = content.split(/\r?\n/);
|
|
2498
|
-
if (lines[0] !== "---") return null;
|
|
2499
|
-
for (let i = 1; i < lines.length; i++) if (lines[i] === "---") return lines.slice(1, i);
|
|
2500
|
-
return null;
|
|
2501
|
-
}
|
|
2502
|
-
/** 行内数组值 `[a, b, c]`(仅当首尾为方括号时视为行内数组;否则返回 null) */
|
|
2503
|
-
function parseInlineArray(value) {
|
|
2504
|
-
const trimmed = value.trim();
|
|
2505
|
-
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
2506
|
-
return trimmed.slice(1, -1).split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
2507
|
-
}
|
|
2508
|
-
/**
|
|
2509
|
-
* 解析声明(规格 §2.1,**最小实现,不引 YAML 依赖**)。
|
|
2510
|
-
*
|
|
2511
|
-
* 只认 `skills:` 键;未知键一律忽略(向前兼容,不报错)。两种值形态都支持:
|
|
2512
|
-
* ① 行内数组 `skills: [home-rhetoric, acc-eap]`
|
|
2513
|
-
* ② 缩进列表(`skills:` 后各起一行 ` - a`)
|
|
2514
|
-
* 名字:去首尾空白、空项丢弃、**保持书写顺序**(注入顺序 = 书写顺序)、同名去重(保留首次位置)。
|
|
2515
|
-
*
|
|
2516
|
-
* 未定义形态(不在规格的两种值形态内,如 `skills: a` 裸标量)⇒ **视为无声明**:
|
|
2517
|
-
* 本函数只认契约里写明的两种形态,猜第三种等于自造契约(规格 §2.1 只列了 ①②)。
|
|
2518
|
-
*
|
|
2519
|
-
* @param sessionMd SESSION.md 全文
|
|
2520
|
-
* @returns 声明的 skill 名(按书写顺序去重);无 frontmatter / 无 `skills` 键 / 值为空 → `[]`
|
|
2521
|
-
*/
|
|
2522
|
-
function parseDeclaredSkills(sessionMd) {
|
|
2523
|
-
const fm = extractFrontmatter(sessionMd);
|
|
2524
|
-
if (!fm) return [];
|
|
2525
|
-
const out = [];
|
|
2526
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2527
|
-
const push = (name) => {
|
|
2528
|
-
const clean = name.trim();
|
|
2529
|
-
if (clean === "" || seen.has(clean)) return;
|
|
2530
|
-
seen.add(clean);
|
|
2531
|
-
out.push(clean);
|
|
2532
|
-
};
|
|
2533
|
-
for (let i = 0; i < fm.length; i++) {
|
|
2534
|
-
const line = fm[i] ?? "";
|
|
2535
|
-
const m = /^skills\s*:(.*)$/.exec(line);
|
|
2536
|
-
if (!m) continue;
|
|
2537
|
-
const rest = (m[1] ?? "").trim();
|
|
2538
|
-
const inline = parseInlineArray(rest);
|
|
2539
|
-
if (inline) {
|
|
2540
|
-
for (const name of inline) push(name);
|
|
2541
|
-
continue;
|
|
2542
|
-
}
|
|
2543
|
-
if (rest !== "") continue;
|
|
2544
|
-
for (let j = i + 1; j < fm.length; j++) {
|
|
2545
|
-
const item = /^\s*-\s*(.*)$/.exec(fm[j] ?? "");
|
|
2546
|
-
if (!item) break;
|
|
2547
|
-
push(item[1] ?? "");
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
return out;
|
|
2551
|
-
}
|
|
2552
|
-
/** SESSION.md 绝对路径(绑定记录存的是绝对路径;非绝对态按 CCC 根解析——防御旧数据) */
|
|
2553
|
-
function absoluteMdPath(root, mdPath) {
|
|
2554
|
-
return mdPath.startsWith(root) ? mdPath : resolve(root, mdPath);
|
|
2555
|
-
}
|
|
2556
|
-
/**
|
|
2557
|
-
* 注册门(规格 §3「无绑定 / 无声明 ⇒ 完全不注册该 section」)。
|
|
2558
|
-
*
|
|
2559
|
-
* 返回 true 的两种情况:
|
|
2560
|
-
* · 文件在且**有非空声明** → 注册(有内容可注入)
|
|
2561
|
-
* · 文件**不在 / 读不到** → 同样注册——此时 section 文本是响亮的 `[缺失]` 提示
|
|
2562
|
-
* (规格 §6-6 要求"不静默";不注册就等于静默)
|
|
2563
|
-
* 其余(文件在但无 frontmatter / 无 `skills` 键 / 值为空)⇒ false,不注册、不产生噪声。
|
|
2564
|
-
*/
|
|
2565
|
-
function hasCognitiveTrajectorySkillsDeclaration(root, mdPath) {
|
|
2566
|
-
const abs = absoluteMdPath(root, mdPath);
|
|
2567
|
-
if (!existsSync(abs)) return true;
|
|
2568
|
-
try {
|
|
2569
|
-
return parseDeclaredSkills(readFileSync(abs, "utf-8")).length > 0;
|
|
2570
|
-
} catch {
|
|
2571
|
-
return true;
|
|
2572
|
-
}
|
|
2573
|
-
}
|
|
2574
|
-
/** 去空白 + 丢空项 + 同名去重(保留首次出现位置)——两条声明来源共用同一规范化 */
|
|
2575
|
-
function normalizeNames(names) {
|
|
2576
|
-
const out = [];
|
|
2577
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2578
|
-
for (const raw of names) {
|
|
2579
|
-
const clean = raw.trim();
|
|
2580
|
-
if (clean === "" || seen.has(clean)) continue;
|
|
2581
|
-
seen.add(clean);
|
|
2582
|
-
out.push(clean);
|
|
2583
|
-
}
|
|
2584
|
-
return out;
|
|
2585
|
-
}
|
|
2586
|
-
/**
|
|
2587
|
-
* 合并两条声明来源(v1.44.0):**CCC 级在前,轨迹级追加**,同名去重取首次位置。
|
|
2588
|
-
*
|
|
2589
|
-
* 两条来源(owner 令 2026-09-20):
|
|
2590
|
-
* · **CCC 级** = `.opencode/serenity.json` 的 `cognitiveTrajectory.skills`(本 CCC 所有轨迹共有的底座);
|
|
2591
|
-
* · **轨迹级** = 该轨迹 `SESSION.md` frontmatter 的 `skills:`(这条轨迹额外的)。
|
|
2592
|
-
*
|
|
2593
|
-
* 为什么是**并集**而不是二选一(R↓):C1 设计当初否决"纯 CCC 全局"的理由是"会**丢掉
|
|
2594
|
-
* per-trajectory**";并集把两侧都保住——容器给底座,单条轨迹仍可加自己的额外项。
|
|
2595
|
-
* 为什么 CCC 级**在前**:底座先立、增量随后,与"入口身份块 order=-50 → 本 section order=-45"
|
|
2596
|
-
* 的既有先后观感一致。
|
|
2597
|
-
*/
|
|
2598
|
-
function mergeSkillNames(cccSkills, trajSkills) {
|
|
2599
|
-
return normalizeNames([...cccSkills, ...trajSkills]);
|
|
2600
|
-
}
|
|
2601
|
-
/**
|
|
2602
|
-
* 装配注入正文(规格 §3):按**合并后的**顺序拼接各 skill 的 `SKILL.md` 全文,每段带来源抬头
|
|
2603
|
-
* `=== skill: <name> ===`;拿不到全文的一律以 `[缺失]` 响亮提示占位(**不静默**)。
|
|
2604
|
-
*
|
|
2605
|
-
* 安全(规格 §3 🔒):名字来自 CCC 数据(frontmatter / CCC 配置)⇒ **先过 `isSafeSkillName`**;
|
|
2606
|
-
* 不安全 ⇒ 按缺失处理,且不调用 `findSkillMd` ⇒ **不发生任何以该名拼出的 fs 访问**。
|
|
2607
|
-
*
|
|
2608
|
-
* 长度守卫:复用已导出的 `truncateContent`,**在合并之后**统一截断(两条来源共享同一上限,
|
|
2609
|
-
* 不是各给 32 KB——否则上限会随来源数翻倍)。
|
|
2610
|
-
*
|
|
2611
|
-
* @param root CCC 根
|
|
2612
|
-
* @param mdPath 轨迹的 SESSION.md 路径(**来自绑定的 `mdPath`**,不用 label 拼)
|
|
2613
|
-
* @param maxChars 总长上限(字符)
|
|
2614
|
-
* @param cccSkills CCC 级声明(`readCognitiveTrajectorySkills`;缺省空 = 只有轨迹级,行为与 v1.43 一致)
|
|
2615
|
-
* @param onInjectedNames 观测回调(本次请求实际带上的 skill 名;**纯观测**,供用量统计用——
|
|
2616
|
-
* 回调抛错被吞,**不影响注入正文**)
|
|
2617
|
-
* @returns 注入正文;两条来源皆无声明 → `''`(空串 = 宿主不产出该 section 块);
|
|
2618
|
-
* SESSION.md 缺失/读失败 → 响亮提示(若同时有 CCC 级声明,则提示在前、正文随后——
|
|
2619
|
-
* **提示不因"别处有内容"而被吞掉**)
|
|
2620
|
-
*/
|
|
2621
|
-
function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIVE_TRAJECTORY_SKILLS_MAX_CHARS, cccSkills = [], onInjectedNames) {
|
|
2622
|
-
const abs = absoluteMdPath(root, mdPath);
|
|
2623
|
-
let trajNames = [];
|
|
2624
|
-
let notice = "";
|
|
2625
|
-
if (!existsSync(abs)) notice = `${SKILL_MISSING_MARK} ${abs}(轨迹的 SESSION.md 不存在——skill 声明无从读取;若刚重建/移动过轨迹,请确认绑定记录)`;
|
|
2626
|
-
else try {
|
|
2627
|
-
trajNames = parseDeclaredSkills(readFileSync(abs, "utf-8"));
|
|
2628
|
-
} catch (err) {
|
|
2629
|
-
notice = `${SKILL_MISSING_MARK} ${abs}(SESSION.md 读取失败:${err instanceof Error ? err.message : String(err)})`;
|
|
2630
|
-
}
|
|
2631
|
-
const names = mergeSkillNames(cccSkills, trajNames);
|
|
2632
|
-
if (names.length === 0) return notice;
|
|
2633
|
-
if (onInjectedNames) try {
|
|
2634
|
-
onInjectedNames(names);
|
|
2635
|
-
} catch {}
|
|
2636
|
-
const blocks = [];
|
|
2637
|
-
for (const name of names) {
|
|
2638
|
-
if (!isSafeSkillName(name)) {
|
|
2639
|
-
blocks.push(`${SKILL_MISSING_MARK} ${name}(名字不安全,已拒绝解析路径)`);
|
|
2640
|
-
continue;
|
|
2641
|
-
}
|
|
2642
|
-
const file = findSkillMd(root, name);
|
|
2643
|
-
if (!file) {
|
|
2644
|
-
blocks.push(missingNotice(name));
|
|
2645
|
-
continue;
|
|
2646
|
-
}
|
|
2647
|
-
try {
|
|
2648
|
-
const index = buildResourceIndex(dirname(file));
|
|
2649
|
-
const body = readFileSync(file, "utf-8");
|
|
2650
|
-
blocks.push(`=== skill: ${name} ===\n${index ? `${body}\n\n${index}` : body}`);
|
|
2651
|
-
} catch {
|
|
2652
|
-
blocks.push(missingNotice(name));
|
|
2653
|
-
}
|
|
2654
|
-
}
|
|
2655
|
-
const body = blocks.join("\n\n");
|
|
2656
|
-
return truncateContent(notice ? `${notice}\n\n${body}` : body, maxChars);
|
|
2657
|
-
}
|
|
2658
|
-
//#endregion
|
|
2659
|
-
//#region src/surface/usage.ts
|
|
2660
|
-
/**
|
|
2661
|
-
* surface/usage.ts — ACC 用量统计(**skill 加载次数** + **MSM 执行次数**;落该 CCC 的 `_tmp`)
|
|
2662
|
-
*
|
|
2663
|
-
* ## 所有者令(本模块的存在理由)
|
|
2664
|
-
*
|
|
2665
|
-
* 「我需要对 ACC 增加一个统计功能,包括 **skill 加载次数统计,msm 执行次数统计**;
|
|
2666
|
-
* 要求这个统计**自动生成在 CCC 的 `_tmp` 内的指定文件**」
|
|
2667
|
-
* +(对齐时确认)「**skill 加载**两个口径**都记、用字段区分**」「**只记名字、不记参数**」
|
|
2668
|
-
* 「**重启后读旧文件续算**」。
|
|
2669
|
-
*
|
|
2670
|
-
* ## 它解决什么麻烦(白话)
|
|
2671
|
-
*
|
|
2672
|
-
* 在此之前,「**哪些 skill 真的被加载过、哪些 MSM 真的在跑**」在 ACC 侧**一个数都没有** ——
|
|
2673
|
-
* 要判断「这东西还有人用吗」,只能靠翻会话记录**猜**。
|
|
2674
|
-
* ⇒ 本模块让每个 CCC 留下一份**纯计数**账本:**用了哪个、多少次、第一次和最近一次是什么时候**。
|
|
2675
|
-
* 用途 = 清理/精简的**依据**(没有人用的东西与天天在用的东西,不再靠猜)。
|
|
2676
|
-
*
|
|
2677
|
-
* ## 🔴 铁律(与 `cro/log.ts` 同族)
|
|
2678
|
-
*
|
|
2679
|
-
* **本模块每个导出函数都不抛错**:读盘 / 解析 / 写盘失败一律返回结构化失败,
|
|
2680
|
-
* 由调用方只记一行日志(或什么都不做)⇒ **统计的问题绝不影响工具执行与既有链路**。
|
|
2681
|
-
* 调用点在 `tools/post-execute` 的中途 ⇒ **更不能抛**(否则等于用统计打断会话)。
|
|
2682
|
-
*
|
|
2683
|
-
* ## 🔴 只记名字(所有者令;也是本模块唯一的"不入盘"约束)
|
|
2684
|
-
*
|
|
2685
|
-
* **绝不写 `arguments`**:MSM 参数里可能有凭据 / 路径 / 正文。
|
|
2686
|
-
* ⇒ 本模块只保留**计数键**(skill 名 / MSM 名),并对其做**长度与数量上限**(见常量),
|
|
2687
|
-
* 使文件规模**结构上有界**(不需要轮转,也不会被异常名字撑爆)。
|
|
2688
|
-
*
|
|
2689
|
-
* ## 🔴 重启后必须续算(所有者令)
|
|
2690
|
-
*
|
|
2691
|
-
* 宿主会被重启(本容器实测 24h 内 3 次)⇒ 计数**不能只活在内存里**:
|
|
2692
|
-
* 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake/registry.ts` /
|
|
2693
|
-
* `cro/log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
|
|
2694
|
-
*
|
|
2695
|
-
* ## 口径(两个字段,勿合并)
|
|
2696
|
-
*
|
|
2697
|
-
* | 字段 | 含义 | 喂它的地方 |
|
|
2698
|
-
* |---|---|---|
|
|
2699
|
-
* | `skill.loads` | 模型**主动**调宿主 `skill` 工具(把某 skill 的 `SKILL.md` 全文载入) | `tools/post-execute`(工具名 = `skill`) |
|
|
2700
|
-
* | `skill.injections` | ACC **每请求注入**的 skill 段(`cognitiveTrajectory.skills` / 轨迹 frontmatter 声明) | `seams/system-prompt.ts` 的 cognitive-trajectory-skills section 求值回调 |
|
|
2701
|
-
* | `msm` | `msm` 工具的一次执行,按 **MSM 名**分桶 | `tools/post-execute`(工具名 = `msm`) |
|
|
2702
|
-
*
|
|
2703
|
-
* 🔴 **两者差好几个数量级**:`loads` 是模型的**动作**(稀疏),`injections` 是**请求的属性**(稠密)。
|
|
2704
|
-
* 合并成一个数就**再也读不出**「这个 skill 是被人主动用的,还是只是被配上了」。
|
|
2705
|
-
*/
|
|
2706
|
-
/** 落点目录(所有者令:CCC 的 `_tmp` 内) */
|
|
2707
|
-
const USAGE_STATS_SUBDIR = "_tmp";
|
|
2708
|
-
/** 固定文件名(所有者令「指定文件」;对齐时我取的名字,owner 未反对 ⇒ 定为常量) */
|
|
2709
|
-
const USAGE_STATS_FILENAME = "acc-usage.json";
|
|
2710
|
-
/** 每个桶的键数上限(**结构上界**,见文件头"只记名字"节) */
|
|
2711
|
-
const USAGE_MAX_KEYS_PER_BUCKET = 1e3;
|
|
2712
|
-
const USAGE_STATS_VERSION = 1;
|
|
2713
|
-
/** 统计文件绝对路径(**纯函数**) */
|
|
2714
|
-
function usageStatsPath(root) {
|
|
2715
|
-
return join(root, USAGE_STATS_SUBDIR, USAGE_STATS_FILENAME);
|
|
2716
|
-
}
|
|
2717
|
-
/** 空账本(**纯函数**) */
|
|
2718
|
-
function emptyUsageStats() {
|
|
2719
|
-
return {
|
|
2720
|
-
version: USAGE_STATS_VERSION,
|
|
2721
|
-
updatedAt: isoLocal(0),
|
|
2722
|
-
skill: {
|
|
2723
|
-
loads: {},
|
|
2724
|
-
injections: {}
|
|
2725
|
-
},
|
|
2726
|
-
msm: {}
|
|
2727
|
-
};
|
|
2728
|
-
}
|
|
2729
|
-
/** 单条计数形状校验(容忍手改:字段缺失/类型不符 ⇒ 丢弃该条,不阻断整档) */
|
|
2730
|
-
function asCounter(value) {
|
|
2731
|
-
const c = value;
|
|
2732
|
-
if (!c || typeof c !== "object") return null;
|
|
2733
|
-
if (typeof c.count !== "number" || !Number.isFinite(c.count) || c.count < 0) return null;
|
|
2734
|
-
const firstAt = typeof c.firstAt === "string" ? c.firstAt : "";
|
|
2735
|
-
const lastAt = typeof c.lastAt === "string" ? c.lastAt : firstAt;
|
|
2736
|
-
return {
|
|
2737
|
-
count: Math.floor(c.count),
|
|
2738
|
-
firstAt,
|
|
2739
|
-
lastAt
|
|
2740
|
-
};
|
|
2741
|
-
}
|
|
2742
|
-
/** 桶校验(非对象 ⇒ 空桶;逐键过滤非法条) */
|
|
2743
|
-
function asBucket(value) {
|
|
2744
|
-
const out = {};
|
|
2745
|
-
if (!value || typeof value !== "object") return out;
|
|
2746
|
-
for (const [k, v] of Object.entries(value)) {
|
|
2747
|
-
const c = asCounter(v);
|
|
2748
|
-
if (c) out[k] = c;
|
|
2749
|
-
}
|
|
2750
|
-
return out;
|
|
2751
|
-
}
|
|
2752
|
-
/**
|
|
2753
|
-
* 读取账本(**永不抛**;读坏 ⇒ 空账本 + 返回 error 文本,**不静默吞掉**)。
|
|
2754
|
-
* 文件不存在 = 正常初态(`error: null` 且空账本)。
|
|
2755
|
-
*/
|
|
2756
|
-
function loadUsageStats(root) {
|
|
2757
|
-
const path = usageStatsPath(root);
|
|
2758
|
-
if (!existsSync(path)) return {
|
|
2759
|
-
stats: emptyUsageStats(),
|
|
2760
|
-
error: null
|
|
2761
|
-
};
|
|
2762
|
-
try {
|
|
2763
|
-
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
2764
|
-
const skill = parsed.skill ?? {};
|
|
2765
|
-
return {
|
|
2766
|
-
stats: {
|
|
2767
|
-
version: USAGE_STATS_VERSION,
|
|
2768
|
-
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : isoLocal(0),
|
|
2769
|
-
skill: {
|
|
2770
|
-
loads: asBucket(skill.loads),
|
|
2771
|
-
injections: asBucket(skill.injections)
|
|
2772
|
-
},
|
|
2773
|
-
msm: asBucket(parsed.msm)
|
|
2774
|
-
},
|
|
2775
|
-
error: null
|
|
2776
|
-
};
|
|
2777
|
-
} catch (err) {
|
|
2778
|
-
return {
|
|
2779
|
-
stats: emptyUsageStats(),
|
|
2780
|
-
error: `用量统计解析失败(${String(err?.message ?? err)})`
|
|
2781
|
-
};
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
/** 键是否可记(非空 + 不超长;**不截断**——截断会造出一个不存在的名字) */
|
|
2785
|
-
function isRecordableUsageName(name) {
|
|
2786
|
-
return typeof name === "string" && name !== "" && [...name].length <= 120;
|
|
2787
|
-
}
|
|
2788
|
-
/**
|
|
2789
|
-
* 在桶上自增一格(**纯函数**:不改入参,返回新桶)。
|
|
2790
|
-
* 新键超桶上限 ⇒ **不新增**(但仍自增已有键)⇒ 键数**结构上有界**。
|
|
2791
|
-
*/
|
|
2792
|
-
function bumpBucket(bucket, name, at, maxKeys = USAGE_MAX_KEYS_PER_BUCKET) {
|
|
2793
|
-
const prev = bucket[name];
|
|
2794
|
-
if (!prev && Object.keys(bucket).length >= maxKeys) return bucket;
|
|
2795
|
-
const next = prev ? {
|
|
2796
|
-
count: prev.count + 1,
|
|
2797
|
-
firstAt: prev.firstAt || at,
|
|
2798
|
-
lastAt: at
|
|
2799
|
-
} : {
|
|
2800
|
-
count: 1,
|
|
2801
|
-
firstAt: at,
|
|
2802
|
-
lastAt: at
|
|
2803
|
-
};
|
|
2804
|
-
return {
|
|
2805
|
-
...bucket,
|
|
2806
|
-
[name]: next
|
|
2807
|
-
};
|
|
2808
|
-
}
|
|
2809
|
-
/** 从工具入参里取名字(首个命中的键胜出;取不到 ⇒ `null`) */
|
|
2810
|
-
function nameFromArgs(args, keys) {
|
|
2811
|
-
if (args === null || typeof args !== "object") return null;
|
|
2812
|
-
const a = args;
|
|
2813
|
-
for (const k of keys) if (isRecordableUsageName(a[k])) return a[k];
|
|
2814
|
-
return null;
|
|
2815
|
-
}
|
|
2816
|
-
/** `msm` 工具的名字键(`msm(name, args)` 的第一个参数) */
|
|
2817
|
-
const MSM_NAME_KEYS = ["name"];
|
|
2818
|
-
/** `skill` 工具的名字键(宿主 `skill` 工具用 `name`;另两个是防御性容错) */
|
|
2819
|
-
const SKILL_NAME_KEYS = [
|
|
2820
|
-
"name",
|
|
2821
|
-
"skill",
|
|
2822
|
-
"skillName"
|
|
2823
|
-
];
|
|
2824
|
-
const NOTHING = {
|
|
2825
|
-
ok: true,
|
|
2826
|
-
error: null,
|
|
2827
|
-
recorded: null,
|
|
2828
|
-
name: null,
|
|
2829
|
-
wrote: false
|
|
2830
|
-
};
|
|
2831
|
-
/**
|
|
2832
|
-
* 读 → 自增 → 原子写(**本模块唯一写盘通道;永不抛**)。
|
|
2833
|
-
*
|
|
2834
|
-
* @param root CCC 根
|
|
2835
|
-
* @param mutate 在已载入的账本上做一次自增(返回值 = 本次记录的结论;返回 `NOTHING` 则不写盘)
|
|
2836
|
-
* @param nowMs 当前毫秒(可注入 ⇒ 便于测 firstAt/lastAt)
|
|
2837
|
-
*/
|
|
2838
|
-
function writeThrough(root, mutate, nowMs = Date.now()) {
|
|
2839
|
-
const path = usageStatsPath(root);
|
|
2840
|
-
try {
|
|
2841
|
-
const at = isoLocal(nowMs);
|
|
2842
|
-
const loaded = loadUsageStats(root);
|
|
2843
|
-
const outcome = mutate(loaded.stats, at);
|
|
2844
|
-
if (outcome.recorded === null) return outcome;
|
|
2845
|
-
loaded.stats.updatedAt = at;
|
|
2846
|
-
mkdirSync(join(root, USAGE_STATS_SUBDIR), { recursive: true });
|
|
2847
|
-
const tmp = `${path}.tmp`;
|
|
2848
|
-
writeFileSync(tmp, `${JSON.stringify(loaded.stats, null, 2)}\n`, "utf-8");
|
|
2849
|
-
renameSync(tmp, path);
|
|
2850
|
-
return {
|
|
2851
|
-
...outcome,
|
|
2852
|
-
ok: true,
|
|
2853
|
-
error: null,
|
|
2854
|
-
wrote: true
|
|
2855
|
-
};
|
|
2856
|
-
} catch (err) {
|
|
2857
|
-
return {
|
|
2858
|
-
ok: false,
|
|
2859
|
-
error: `用量统计写入失败(${String(err?.message ?? err)})`,
|
|
2860
|
-
recorded: null,
|
|
2861
|
-
name: null,
|
|
2862
|
-
wrote: false
|
|
2863
|
-
};
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
/**
|
|
2867
|
-
* 记录一次工具调用的用量(**`tools/post-execute` 的喂入口**)。
|
|
2868
|
-
*
|
|
2869
|
-
* 只认两类工具名;其余一律 `NOTHING`(**不产生文件、不产生噪声**)。
|
|
2870
|
-
* 🔴 **不看 `arguments` 的其余内容**——只取名字键。
|
|
2871
|
-
*/
|
|
2872
|
-
function recordToolUsage(root, toolName, args, nowMs = Date.now()) {
|
|
2873
|
-
if (toolName === "msm") {
|
|
2874
|
-
const name = nameFromArgs(args, MSM_NAME_KEYS);
|
|
2875
|
-
if (!name) return NOTHING;
|
|
2876
|
-
return writeThrough(root, (stats, at) => {
|
|
2877
|
-
stats.msm = bumpBucket(stats.msm, name, at);
|
|
2878
|
-
return {
|
|
2879
|
-
ok: true,
|
|
2880
|
-
error: null,
|
|
2881
|
-
recorded: "msm",
|
|
2882
|
-
name,
|
|
2883
|
-
wrote: false
|
|
2884
|
-
};
|
|
2885
|
-
}, nowMs);
|
|
2886
|
-
}
|
|
2887
|
-
if (toolName === "skill") {
|
|
2888
|
-
const name = nameFromArgs(args, SKILL_NAME_KEYS);
|
|
2889
|
-
if (!name) return NOTHING;
|
|
2890
|
-
return writeThrough(root, (stats, at) => {
|
|
2891
|
-
stats.skill.loads = bumpBucket(stats.skill.loads, name, at);
|
|
2892
|
-
return {
|
|
2893
|
-
ok: true,
|
|
2894
|
-
error: null,
|
|
2895
|
-
recorded: "skill.load",
|
|
2896
|
-
name,
|
|
2897
|
-
wrote: false
|
|
2898
|
-
};
|
|
2899
|
-
}, nowMs);
|
|
2900
|
-
}
|
|
2901
|
-
return NOTHING;
|
|
2902
|
-
}
|
|
2903
|
-
/**
|
|
2904
|
-
* 记录一次 **skill 注入**(每个请求的 skill 段装配时调用)。
|
|
2905
|
-
*
|
|
2906
|
-
* ⚠️ 与 `recordToolUsage` 不同,本函数的调用频度 = **请求频度**(稠密):
|
|
2907
|
-
* 调用方应**只在真的注入了非空内容时**调用(空 section 由宿主丢弃 ⇒ 不产生噪声)。
|
|
2908
|
-
* 一次调用只写一次盘(`names` 内的多个 skill 合并在**同一次**读改写里)。
|
|
2909
|
-
*/
|
|
2910
|
-
function recordSkillInjections(root, names, nowMs = Date.now()) {
|
|
2911
|
-
const usable = names.filter((n) => isRecordableUsageName(n));
|
|
2912
|
-
if (usable.length === 0) return NOTHING;
|
|
2913
|
-
return writeThrough(root, (stats, at) => {
|
|
2914
|
-
let bucket = stats.skill.injections;
|
|
2915
|
-
for (const n of usable) bucket = bumpBucket(bucket, n, at);
|
|
2916
|
-
stats.skill.injections = bucket;
|
|
2917
|
-
return {
|
|
2918
|
-
ok: true,
|
|
2919
|
-
error: null,
|
|
2920
|
-
recorded: "skill.injection",
|
|
2921
|
-
name: usable.join(","),
|
|
2922
|
-
wrote: false
|
|
2923
|
-
};
|
|
2924
|
-
}, nowMs);
|
|
2925
|
-
}
|
|
2926
|
-
/** 验证窗口:当前时步 ± 1(容忍时钟漂移/生成延迟) */
|
|
2927
|
-
const TOTP_WINDOW = 1;
|
|
2928
|
-
/** 输出位数(标准 6 位) */
|
|
2929
|
-
const TOTP_DIGITS = 6;
|
|
2930
|
-
const B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
2931
|
-
/** base32(无填充,大小写不敏感)→ 字节;非法字符抛错。 */
|
|
2932
|
-
function base32Decode(input) {
|
|
2933
|
-
const clean = input.toUpperCase().replace(/[\s=]/g, "");
|
|
2934
|
-
if (clean.length === 0) throw new Error("empty base32");
|
|
2935
|
-
const out = [];
|
|
2936
|
-
let bits = 0;
|
|
2937
|
-
let value = 0;
|
|
2938
|
-
for (const ch of clean) {
|
|
2939
|
-
const idx = B32_ALPHABET.indexOf(ch);
|
|
2940
|
-
if (idx === -1) throw new Error(`invalid base32 char: ${ch}`);
|
|
2941
|
-
value = value << 5 | idx;
|
|
2942
|
-
bits += 5;
|
|
2943
|
-
if (bits >= 8) {
|
|
2944
|
-
out.push(value >>> bits - 8 & 255);
|
|
2945
|
-
bits -= 8;
|
|
2946
|
-
}
|
|
2947
|
-
}
|
|
2948
|
-
return Uint8Array.from(out);
|
|
2949
|
-
}
|
|
2950
|
-
/** 一个时步的 TOTP code(6 位,前导零保留)。counter = floor(epochSeconds / step) */
|
|
2951
|
-
function totpCode(secretBase32, counter) {
|
|
2952
|
-
const key = base32Decode(secretBase32);
|
|
2953
|
-
const msg = Buffer.alloc(8);
|
|
2954
|
-
msg.writeBigUInt64BE(BigInt(counter));
|
|
2955
|
-
const digest = createHmac("sha1", Buffer.from(key)).update(msg).digest();
|
|
2956
|
-
const offset = digest[digest.length - 1] & 15;
|
|
2957
|
-
const bin = (digest[offset] & 127) << 24 | digest[offset + 1] << 16 | digest[offset + 2] << 8 | digest[offset + 3];
|
|
2958
|
-
return String(bin % 10 ** TOTP_DIGITS).padStart(TOTP_DIGITS, "0");
|
|
2959
|
-
}
|
|
2960
|
-
/** 当前 epoch 秒 */
|
|
2961
|
-
function nowEpochSeconds() {
|
|
2962
|
-
return Math.floor(Date.now() / 1e3);
|
|
2963
|
-
}
|
|
2964
|
-
/**
|
|
2965
|
-
* 校验用户输入的 code(允许 ±TOTP_WINDOW 时步漂移,防重放窗口内同 code 复用由
|
|
2966
|
-
* 调用方按账号记录最近成功 counter 实现——本函数只做纯算法校验)。
|
|
2967
|
-
* @returns 命中的 counter(用于防重放);不匹配返回 null。
|
|
2968
|
-
*/
|
|
2969
|
-
function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
|
|
2970
|
-
if (!/^\d{6}$/.test(code)) return null;
|
|
2971
|
-
const current = Math.floor(nowSeconds / 30);
|
|
2972
|
-
for (let offset = -1; offset <= TOTP_WINDOW; offset++) if (totpCode(secretBase32, current + offset) === code) return current + offset;
|
|
2973
|
-
return null;
|
|
2974
|
-
}
|
|
2975
|
-
//#endregion
|
|
2976
|
-
//#region src/kernel/ports.ts
|
|
2977
|
-
/**
|
|
2978
|
-
* 集中端口表。
|
|
2979
|
-
* A=宿主主面(插件不监听)/ B=网关(可配,默认关)/ C=微信发送(默认开,仅 loopback)
|
|
2980
|
-
* / D=轨迹约束调试(默认关)。
|
|
2981
|
-
*/
|
|
2982
|
-
const FACE_PORTS = {
|
|
2983
|
-
main: {
|
|
2984
|
-
port: 3080,
|
|
2985
|
-
host: null,
|
|
2986
|
-
defaultEnabled: true,
|
|
2987
|
-
meaning: "宿主 DSH 主 WebUI + 插件 /serenity/* 路由面(**插件不起此 listener**,端口自宿主读)"
|
|
2988
|
-
},
|
|
2989
|
-
gateway: {
|
|
2990
|
-
port: 3081,
|
|
2991
|
-
host: "0.0.0.0",
|
|
2992
|
-
defaultEnabled: false,
|
|
2993
|
-
meaning: "双端口网关(外部登录 + 全量反代宿主主面;host/port 走 plugin 全局配置)"
|
|
2994
|
-
},
|
|
2995
|
-
weixinSend: {
|
|
2996
|
-
port: 3082,
|
|
2997
|
-
host: "127.0.0.1",
|
|
2998
|
-
defaultEnabled: true,
|
|
2999
|
-
meaning: "微信主动发送面(容器外进程 → POST /send;与 3080 显式解耦,不经网关)"
|
|
3000
|
-
}
|
|
3001
|
-
};
|
|
3002
|
-
FACE_PORTS.main.port;
|
|
3003
|
-
/** 网关端口默认值(面 B) */
|
|
3004
|
-
const GATEWAY_PORT = FACE_PORTS.gateway.port;
|
|
3005
|
-
/** 微信主动发送端口默认值(面 C) */
|
|
3006
|
-
const WEIXIN_SEND_PORT = FACE_PORTS.weixinSend.port;
|
|
3007
|
-
/**
|
|
3008
|
-
* **机制端口**词表(output-guard 用:agent 在**用户可见输出**里提及内部端口即打回)。
|
|
3009
|
-
*
|
|
3010
|
-
* 由 {@link FACE_PORTS} 机械派生 ⇒ 与面清单**永不漂移**。
|
|
3011
|
-
* ⚠️ 修复记录(C4 块 B,2026-09-15):旧常量硬写 `['3080','3081','3099','3100']`
|
|
3012
|
-
* ——**漏 3082**(微信发送面,默认开)⇒ 守卫拦不住"3082"这类提及。现由表派生,四面齐全。
|
|
3013
|
-
* ⚠️ 面变更记录(2026-10-15):**面 E(3100 ACP/问答)已整体退役**(owner 裁「砍掉外部问答」)
|
|
3014
|
-
* ⇒ 本词表随之少一项,**无需另改**(这正是"派生而非手抄"的收益)。
|
|
3015
|
-
*/
|
|
3016
|
-
const MECHANISM_PORTS = Object.keys(FACE_PORTS).map((id) => String(FACE_PORTS[id].port));
|
|
3017
|
-
//#endregion
|
|
3018
|
-
//#region src/kernel/config.ts
|
|
3019
|
-
/**
|
|
3020
|
-
* kernel/config.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
|
|
3021
|
-
*
|
|
3022
|
-
* 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
|
|
3023
|
-
* 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
|
|
3024
|
-
* CCC 的 localstore.json 只管 CCC 自己的凭据/配置。v1.21.x 曾把
|
|
3025
|
-
* `serenityAdvanced` 存进 CCC localstore(归属错误 + 与 DSH settings 开关割裂),
|
|
3026
|
-
* 本版本迁移到 plugin 全局文件(migrateLegacyLocalstore 一次性迁移)。
|
|
3027
|
-
*
|
|
3028
|
-
* 文件:$DSH_HOME/serenity-hooks.json(缺省 ~/.dsh/serenity-hooks.json;
|
|
3029
|
-
* env SERENITY_HOOKS_CONFIG 可覆盖——测试/部署注入)。
|
|
3030
|
-
* 权限:0600(含账号密码 hash,敏感)。
|
|
3031
|
-
*
|
|
3032
|
-
* 安全:密码仅存 scrypt hash(node:crypto 内置,零依赖);wire 层永不返回 hash
|
|
3033
|
-
* (GET 只回 user/id,设置面板"密码"字段提交空串 = 不修改)。
|
|
3034
|
-
*/
|
|
3035
|
-
/** 高级设定节名(localstore.json 顶层) */
|
|
3036
|
-
const ADVANCED_SECTION = "serenityAdvanced";
|
|
3037
|
-
/** 默认值(工厂——每次返回新对象,防止调用方意外共享引用) */
|
|
3038
|
-
function defaultAdvancedSettings() {
|
|
3039
|
-
return {
|
|
3040
|
-
gateway: {
|
|
3041
|
-
enabled: false,
|
|
3042
|
-
host: FACE_PORTS.gateway.host ?? "0.0.0.0",
|
|
3043
|
-
port: GATEWAY_PORT,
|
|
3044
|
-
accounts: [],
|
|
3045
|
-
workspaces: [],
|
|
3046
|
-
cookieSecure: false,
|
|
3047
|
-
allowWorkspaceCreate: true,
|
|
3048
|
-
totpEnabled: false
|
|
3049
|
-
},
|
|
3050
|
-
persona: {
|
|
3051
|
-
mode: "",
|
|
3052
|
-
overrideText: ""
|
|
3053
|
-
},
|
|
3054
|
-
weixinApi: {
|
|
3055
|
-
enabled: true,
|
|
3056
|
-
port: WEIXIN_SEND_PORT
|
|
3057
|
-
}
|
|
3058
|
-
};
|
|
3059
|
-
}
|
|
3060
|
-
const SCRYPT_KEYLEN = 32;
|
|
3061
|
-
/** 生成 scrypt hash(格式 `salt:hex`);salt 16 字节随机 */
|
|
3062
|
-
function hashPassword(password) {
|
|
3063
|
-
const salt = randomBytes(16);
|
|
3064
|
-
const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
|
|
3065
|
-
return `${salt.toString("hex")}:${derived.toString("hex")}`;
|
|
3066
|
-
}
|
|
3067
|
-
/** 校验密码与存储 hash(timing-safe) */
|
|
3068
|
-
function verifyPassword(password, stored) {
|
|
3069
|
-
const idx = stored.indexOf(":");
|
|
3070
|
-
if (idx <= 0) return false;
|
|
3071
|
-
const salt = Buffer.from(stored.slice(0, idx), "hex");
|
|
3072
|
-
const expected = Buffer.from(stored.slice(idx + 1), "hex");
|
|
3073
|
-
const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
|
|
3074
|
-
return expected.length === derived.length && timingSafeEqual(expected, derived);
|
|
3075
|
-
}
|
|
3076
|
-
/** 全局配置文件路径:env SERENITY_HOOKS_CONFIG 覆盖(测试注入)→ $DSH_HOME → ~/.dsh */
|
|
3077
|
-
function globalConfigPath() {
|
|
3078
|
-
const override = process.env.SERENITY_HOOKS_CONFIG;
|
|
3079
|
-
if (override && override !== "") return override;
|
|
3080
|
-
const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
|
|
3081
|
-
return join(dshHome, "serenity-hooks.json");
|
|
3082
|
-
}
|
|
3083
|
-
function readFileSafe(p) {
|
|
3084
|
-
if (!existsSync(p)) return {};
|
|
3085
|
-
try {
|
|
3086
|
-
const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
|
|
3087
|
-
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
3088
|
-
return {};
|
|
3089
|
-
} catch {
|
|
3090
|
-
return {};
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
3093
|
-
function writeFileSafe(p, data) {
|
|
3094
|
-
writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
3095
|
-
try {
|
|
3096
|
-
chmodSync(p, 384);
|
|
3097
|
-
} catch {}
|
|
3098
|
-
}
|
|
3099
|
-
/** 深合并默认值(缺省字段补齐;accounts 数组整体替换) */
|
|
3100
|
-
function mergeWithDefaults(raw) {
|
|
3101
|
-
const def = defaultAdvancedSettings();
|
|
3102
|
-
if (raw === null || typeof raw !== "object") return def;
|
|
3103
|
-
const o = raw;
|
|
3104
|
-
const gateway = o.gateway ?? {};
|
|
3105
|
-
const persona = o.persona ?? {};
|
|
3106
|
-
const weixinApi = o.weixinApi ?? {};
|
|
3107
|
-
return {
|
|
3108
|
-
gateway: {
|
|
3109
|
-
enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
|
|
3110
|
-
host: typeof gateway.host === "string" && gateway.host !== "" ? gateway.host : def.gateway.host,
|
|
3111
|
-
port: typeof gateway.port === "number" && Number.isInteger(gateway.port) ? gateway.port : def.gateway.port,
|
|
3112
|
-
accounts: Array.isArray(gateway.accounts) ? gateway.accounts.filter((a) => typeof a === "object" && a !== null && typeof a.id === "string" && typeof a.user === "string" && typeof a.passHash === "string") : def.gateway.accounts,
|
|
3113
|
-
workspaces: Array.isArray(gateway.workspaces) ? gateway.workspaces.filter((w) => typeof w === "string" && w !== "") : def.gateway.workspaces,
|
|
3114
|
-
cookieSecure: typeof gateway.cookieSecure === "boolean" ? gateway.cookieSecure : def.gateway.cookieSecure,
|
|
3115
|
-
allowWorkspaceCreate: typeof gateway.allowWorkspaceCreate === "boolean" ? gateway.allowWorkspaceCreate : def.gateway.allowWorkspaceCreate,
|
|
3116
|
-
totpEnabled: typeof gateway.totpEnabled === "boolean" ? gateway.totpEnabled : def.gateway.totpEnabled
|
|
3117
|
-
},
|
|
3118
|
-
persona: {
|
|
3119
|
-
mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
|
|
3120
|
-
overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
|
|
3121
|
-
},
|
|
3122
|
-
weixinApi: {
|
|
3123
|
-
enabled: typeof weixinApi.enabled === "boolean" ? weixinApi.enabled : def.weixinApi.enabled,
|
|
3124
|
-
port: typeof weixinApi.port === "number" && Number.isInteger(weixinApi.port) && weixinApi.port >= 0 ? weixinApi.port : def.weixinApi.port
|
|
3125
|
-
}
|
|
3126
|
-
};
|
|
3127
|
-
}
|
|
3128
|
-
/** 读取全局配置(文件缺失/坏 JSON → 默认值) */
|
|
3129
|
-
function readAdvancedSettings() {
|
|
3130
|
-
return mergeWithDefaults(readFileSafe(globalConfigPath()));
|
|
3131
|
-
}
|
|
3132
|
-
/** 写入全局配置(整体替换) */
|
|
3133
|
-
function writeAdvancedSettings(settings) {
|
|
3134
|
-
writeFileSafe(globalConfigPath(), settings);
|
|
3135
|
-
}
|
|
3136
|
-
/**
|
|
3137
|
-
* 部分更新:传入 Partial,深合并到现有值。
|
|
3138
|
-
* accounts 传入数组 → 整体替换;accounts 未传 → 保留现有。
|
|
3139
|
-
*/
|
|
3140
|
-
function updateAdvancedSettings(patch) {
|
|
3141
|
-
const current = readAdvancedSettings();
|
|
3142
|
-
const gw = patch.gateway;
|
|
3143
|
-
const ps = patch.persona;
|
|
3144
|
-
const next = {
|
|
3145
|
-
gateway: gw !== void 0 ? {
|
|
3146
|
-
enabled: typeof gw.enabled === "boolean" ? gw.enabled : current.gateway.enabled,
|
|
3147
|
-
host: typeof gw.host === "string" && gw.host !== "" ? gw.host : current.gateway.host,
|
|
3148
|
-
port: typeof gw.port === "number" && Number.isInteger(gw.port) ? gw.port : current.gateway.port,
|
|
3149
|
-
accounts: Array.isArray(gw.accounts) ? gw.accounts : current.gateway.accounts,
|
|
3150
|
-
workspaces: Array.isArray(gw.workspaces) ? gw.workspaces.filter((w) => typeof w === "string" && w !== "") : current.gateway.workspaces,
|
|
3151
|
-
cookieSecure: typeof gw.cookieSecure === "boolean" ? gw.cookieSecure : current.gateway.cookieSecure,
|
|
3152
|
-
allowWorkspaceCreate: typeof gw.allowWorkspaceCreate === "boolean" ? gw.allowWorkspaceCreate : current.gateway.allowWorkspaceCreate,
|
|
3153
|
-
totpEnabled: typeof gw.totpEnabled === "boolean" ? gw.totpEnabled : current.gateway.totpEnabled
|
|
3154
|
-
} : current.gateway,
|
|
3155
|
-
persona: ps !== void 0 ? {
|
|
3156
|
-
mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
|
|
3157
|
-
overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
|
|
3158
|
-
} : current.persona,
|
|
3159
|
-
weixinApi: patch.weixinApi !== void 0 ? {
|
|
3160
|
-
enabled: typeof patch.weixinApi.enabled === "boolean" ? patch.weixinApi.enabled : current.weixinApi.enabled,
|
|
3161
|
-
port: typeof patch.weixinApi.port === "number" && Number.isInteger(patch.weixinApi.port) && patch.weixinApi.port >= 0 ? patch.weixinApi.port : current.weixinApi.port
|
|
3162
|
-
} : current.weixinApi
|
|
3163
|
-
};
|
|
3164
|
-
writeAdvancedSettings(next);
|
|
3165
|
-
return next;
|
|
3166
|
-
}
|
|
3167
|
-
/**
|
|
3168
|
-
* 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
|
|
3169
|
-
* 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
|
|
3170
|
-
* @param root - CCC 根(localstore.json 所在目录)
|
|
3171
|
-
* @returns true = 已迁移(旧节保留在 localstore 供回滚,读取方以全局文件为准)
|
|
3172
|
-
*/
|
|
3173
|
-
function migrateLegacyLocalstore(root) {
|
|
3174
|
-
if (!root) return false;
|
|
3175
|
-
const gpath = globalConfigPath();
|
|
3176
|
-
if (existsSync(gpath)) return false;
|
|
3177
|
-
const legacy = readFileSafe(join(root, "localstore.json"))[ADVANCED_SECTION];
|
|
3178
|
-
if (legacy === void 0 || legacy === null || typeof legacy !== "object") return false;
|
|
3179
|
-
writeFileSafe(gpath, legacy);
|
|
3180
|
-
return true;
|
|
3181
|
-
}
|
|
3182
|
-
/** 持久化 → wire(剥离 passHash/totpSecret;只留布尔) */
|
|
3183
|
-
function toWire(settings) {
|
|
3184
|
-
return {
|
|
3185
|
-
gateway: {
|
|
3186
|
-
enabled: settings.gateway.enabled,
|
|
3187
|
-
host: settings.gateway.host,
|
|
3188
|
-
port: settings.gateway.port,
|
|
3189
|
-
accounts: settings.gateway.accounts.map((a) => ({
|
|
3190
|
-
id: a.id,
|
|
3191
|
-
user: a.user,
|
|
3192
|
-
hasPassword: a.passHash !== "",
|
|
3193
|
-
hasTotp: typeof a.totpSecret === "string" && a.totpSecret !== ""
|
|
3194
|
-
})),
|
|
3195
|
-
workspaces: [...settings.gateway.workspaces],
|
|
3196
|
-
cookieSecure: settings.gateway.cookieSecure,
|
|
3197
|
-
allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
|
|
3198
|
-
totpEnabled: settings.gateway.totpEnabled
|
|
3199
|
-
},
|
|
3200
|
-
persona: settings.persona
|
|
3201
|
-
};
|
|
3202
|
-
}
|
|
3203
|
-
/**
|
|
3204
|
-
* 已知工作区投影(v1.28.0 适配 0.1.2-rc.1 A2 方案 A′):
|
|
3205
|
-
* rc.1 workspace.list unary 删除 → AccountsEditor 白名单下拉的数据源改走 gateway 自有
|
|
3206
|
-
* /serenity/config 的 knownWorkspaces(host workspaceRegistry.list() 投影,白名单过滤)。
|
|
3207
|
-
* 纯函数可单测。allowPrefixes 空 = 全部放行(向后兼容默认)。
|
|
3208
|
-
*/
|
|
3209
|
-
function projectKnownWorkspaces(workspaces, allowPrefixes) {
|
|
3210
|
-
return workspaces.filter((w) => typeof w.path === "string" && w.path !== "").filter((w) => allowPrefixes.length === 0 || allowPrefixes.some((p) => w.path.startsWith(p))).map((w) => ({
|
|
3211
|
-
path: w.path,
|
|
3212
|
-
title: typeof w.title === "string" && w.title !== "" ? w.title : w.path
|
|
3213
|
-
}));
|
|
3214
|
-
}
|
|
3215
|
-
/**
|
|
3216
|
-
* wire → 持久化(面板 PUT 用):
|
|
3217
|
-
* accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
|
|
3218
|
-
* 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
|
|
3219
|
-
*/
|
|
3220
|
-
function applyWirePatch(wire) {
|
|
3221
|
-
const current = readAdvancedSettings();
|
|
3222
|
-
const patch = {};
|
|
3223
|
-
if (wire.gateway !== void 0) {
|
|
3224
|
-
const gwPatch = { ...current.gateway };
|
|
3225
|
-
if (typeof wire.gateway.enabled === "boolean") gwPatch.enabled = wire.gateway.enabled;
|
|
3226
|
-
if (typeof wire.gateway.host === "string" && wire.gateway.host !== "") gwPatch.host = wire.gateway.host;
|
|
3227
|
-
if (typeof wire.gateway.port === "number" && Number.isInteger(wire.gateway.port)) gwPatch.port = wire.gateway.port;
|
|
3228
|
-
if (Array.isArray(wire.gateway.workspaces)) gwPatch.workspaces = wire.gateway.workspaces.filter((w) => typeof w === "string" && w !== "");
|
|
3229
|
-
if (typeof wire.gateway.cookieSecure === "boolean") gwPatch.cookieSecure = wire.gateway.cookieSecure;
|
|
3230
|
-
if (typeof wire.gateway.allowWorkspaceCreate === "boolean") gwPatch.allowWorkspaceCreate = wire.gateway.allowWorkspaceCreate;
|
|
3231
|
-
if (typeof wire.gateway.totpEnabled === "boolean") gwPatch.totpEnabled = wire.gateway.totpEnabled;
|
|
3232
|
-
if (Array.isArray(wire.gateway.accounts)) {
|
|
3233
|
-
const byId = new Map(current.gateway.accounts.map((a) => [a.id, a]));
|
|
3234
|
-
gwPatch.accounts = wire.gateway.accounts.map((a) => {
|
|
3235
|
-
const existing = byId.get(a.id);
|
|
3236
|
-
const pass = a.pass;
|
|
3237
|
-
const totp = a.totpSecret;
|
|
3238
|
-
const totpReset = a.totpReset === true;
|
|
3239
|
-
if (typeof totp === "string" && totp !== "") try {
|
|
3240
|
-
base32Decode(totp);
|
|
3241
|
-
} catch {
|
|
3242
|
-
throw new Error(`Account "${a.user}" TOTP secret is not valid base32`);
|
|
3243
|
-
}
|
|
3244
|
-
const nextTotp = totpReset ? void 0 : typeof totp === "string" && totp !== "" ? totp : existing?.totpSecret;
|
|
3245
|
-
const withTotp = {
|
|
3246
|
-
id: a.id,
|
|
3247
|
-
user: a.user,
|
|
3248
|
-
...nextTotp === void 0 ? {} : { totpSecret: nextTotp }
|
|
3249
|
-
};
|
|
3250
|
-
if (typeof pass === "string" && pass !== "") return {
|
|
3251
|
-
...withTotp,
|
|
3252
|
-
passHash: hashPassword(pass)
|
|
3253
|
-
};
|
|
3254
|
-
if (existing) return {
|
|
3255
|
-
...withTotp,
|
|
3256
|
-
passHash: existing.passHash
|
|
3257
|
-
};
|
|
3258
|
-
throw new Error(`Account "${a.user}" (id=${a.id}) has no password and no existing hash — new accounts must set a password`);
|
|
3259
|
-
});
|
|
3260
|
-
}
|
|
3261
|
-
patch.gateway = gwPatch;
|
|
3262
|
-
}
|
|
3263
|
-
if (wire.persona !== void 0) {
|
|
3264
|
-
const psPatch = { ...current.persona };
|
|
3265
|
-
if (typeof wire.persona.mode === "string") psPatch.mode = wire.persona.mode;
|
|
3266
|
-
if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
|
|
3267
|
-
patch.persona = psPatch;
|
|
3268
|
-
}
|
|
3269
|
-
return updateAdvancedSettings(patch);
|
|
3270
|
-
}
|
|
3271
|
-
//#endregion
|
|
3272
|
-
//#region src/seams/system-prompt.ts
|
|
3273
|
-
/** 读取 persona 彩蛋配置(plugin 全局;未配置 → mode='' 彩蛋关闭) */
|
|
3274
|
-
function readPersonaSettings() {
|
|
3275
|
-
try {
|
|
3276
|
-
const s = readAdvancedSettings();
|
|
3277
|
-
return {
|
|
3278
|
-
mode: s.persona.mode ?? "",
|
|
3279
|
-
overrideText: s.persona.overrideText ?? ""
|
|
3280
|
-
};
|
|
3281
|
-
} catch {
|
|
3282
|
-
return {
|
|
3283
|
-
mode: "",
|
|
3284
|
-
overrideText: ""
|
|
3285
|
-
};
|
|
3286
|
-
}
|
|
3287
|
-
}
|
|
3288
|
-
/** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及)。无 g flag:`.test()` 须逐行独立无状态,避免 lastIndex 游移漏滤(问题A) */
|
|
3289
|
-
const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/;
|
|
3290
|
-
function sanitizeSkillContent(content) {
|
|
3291
|
-
return content.split("\n").filter((line) => !HIDDEN_LINES.test(line)).join("\n");
|
|
3292
|
-
}
|
|
3293
|
-
/** 1) 身份块:ACC 身份 + CCC 名 + 平台工具说明(需求③ S142:工具清单移出为独立 toolsBlock 放装配末尾——身份先行、工具参考殿后) */
|
|
3294
|
-
function identityBlock(root) {
|
|
3295
|
-
const cccName = basename(root);
|
|
3296
|
-
return [
|
|
3297
|
-
"",
|
|
3298
|
-
"=== Serenity ACC ===",
|
|
3299
|
-
`ACC: dsh-serenity-hooks v${ACC_VERSION}`,
|
|
3300
|
-
`CCC: ${cccName}`,
|
|
3301
|
-
"",
|
|
3302
|
-
"You are running inside a Concrete Cognitive Container (CCC) —",
|
|
3303
|
-
"the runtime instance of an Abstract Cognitive Container (ACC).",
|
|
3304
|
-
"The ACC (this plugin) is a cognitive container harness: it provides",
|
|
3305
|
-
"deterministic tools, mechanical constraints, and session continuity.",
|
|
3306
|
-
"The complete built-in tool list is at the end of this prompt under the",
|
|
3307
|
-
"\"Serenity Tools\" heading — read it before using any ACC tool.",
|
|
3308
|
-
"",
|
|
3309
|
-
" ℹ️ Use relative paths from the CCC root for CCC-internal file operations (read/write/edit/glob/grep etc.), e.g. AGENT_SESSIONS/2026-08-14--S134--x/SESSION.md; Root / absolute SESSION.md paths are identifiers only, not tool arguments",
|
|
3310
|
-
"",
|
|
3311
|
-
"The DSH platform tools remain available too (read/write/edit/glob/grep/web_search/ask_user_question/subagent/workflow/goal and more) — the ACC tools are the serenity-native layer, not the only tools.",
|
|
3312
|
-
"",
|
|
3313
|
-
"Additional MSMs registered by this CCC are available — call msm() with no arguments to list them, or msm(\"<name>\") to execute one.",
|
|
3314
|
-
""
|
|
3315
|
-
].join("\n");
|
|
3316
|
-
}
|
|
3317
|
-
/**
|
|
3318
|
-
* 工具清单块(需求③ S142 用户拍板:工具列表移装配末尾 + MSM 调用示例):
|
|
3319
|
-
* 原 accBlock 内嵌 13 行工具清单 → 独立成块放装配末尾(SKILL 后、Session 前)。
|
|
3320
|
-
* 身份先行(认知轨迹开头不被 13 行清单干扰)、工具参考殿后(需要时再看)。
|
|
3321
|
-
* 附带 MSM 调用示例(用户拍板:顶层提示词加调用方式说明,避免偶发调用错误——
|
|
3322
|
-
* 模型对 msm 参数面/协议 flag 理解不稳)。
|
|
3323
|
-
*/
|
|
3324
|
-
function toolsBlock() {
|
|
3325
|
-
return [
|
|
3326
|
-
"",
|
|
3327
|
-
"=== Serenity Tools ===",
|
|
3328
|
-
"The ACC (this plugin) provides the following built-in tools:",
|
|
3329
|
-
"",
|
|
3330
|
-
" container_fs — container filesystem operations (15 subcommands: root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find)",
|
|
3331
|
-
" cognitive_trajectory — one cognitive trajectory: its persistent body (SESSION.md) + its timeline. Lifecycle: list (inventory + stats) / show / create / use (activate; inline integrity check — silent when fine) / rebuild (clear-and-rebuild this conversation in place, Ship of Theseus); delivery: send-now (deliver one message RIGHT NOW; live target injected immediately, non-live cold-resumed like a direct wake — returns a synchronous receipt) / send-later (one future instant + one message to any cognitive trajectory; fire-and-forget — no receipt, no recall) / cro-guide (the CRO programming guide: how to give THIS cognitive trajectory its own program that decides when it should be woken)",
|
|
3332
|
-
" dashboard — always-on container instruments: health (CCC three-principle check + registry integrity) / time (now) / wait (N seconds)",
|
|
3333
|
-
" container_git— git operations (status/commit/push/log/pull/diff)",
|
|
3334
|
-
" msm — execute a registered CCC MSM: msm(name, args); partial name returns candidates; inspect=true shows usage",
|
|
3335
|
-
" praxis — actionable theory injection: praxis (index) / praxis eap / praxis neat / praxis cce / praxis ontology",
|
|
3336
|
-
" ontology — ontology engineering ENGINE (executable): init (write template) / check (M1-M10 structural; level=strict adds L1-L5 hints) / explain (summary) / verify (V1-V4 runtime compliance) / review (semantic-review prompt) / compliance (axiom-vs-skill checklist). Sibling of `praxis ontology` — that one is the METHOD TEXT, this one RUNS it. path must stay inside the container; the receipt carries the engine exit code (0/1/2/4) and the engine text verbatim, so read it rather than assuming success",
|
|
3337
|
-
" cheap-worker — delegate work to a worker agent on the CCC-configured model; one serial child per task, returns its final text. `jobs=[...]` fans out in parallel (cap 10, results keep your order). No loop, no completion code, no progress file. The model is NOT selectable per call (read from the CCC config); a worker's tool set excludes cheap-worker itself",
|
|
3338
|
-
" serenity-skill-creator — creator for serenity-skills (this container's enhanced skill standard): guide (authoring/optimization method — read first) / create <name> (scaffold SKILL.md + skill.ttl + references/, base-chain import path computed) / check <name> (A1 structural + A2 base-chain; three states: modeled-ok / plain-skill / modeled-but-check-could-not-run) / review <name> (C tier: axiom-vs-text compliance question list, half-mechanical) / index <name> (preview the second-layer resource list the model will see) / status (which skills are modeled). A skill is \"modeled\" iff it has a file named exactly skill.ttl next to SKILL.md; a plain skill is NOT an anomaly. Only `create` writes, and it refuses an existing directory",
|
|
3339
|
-
" localstore — ACC local credential/config storage (CCC-root localstore.json; git policy localstore.gitTrack default deny)",
|
|
3340
|
-
" container_admin — container administration (the maintenance bay): role (轨迹约束 roles: guide/validate/apply/list) / msm (register/deregister/check/guide/catalog/ccc-config) / config (view)",
|
|
3341
|
-
"",
|
|
3342
|
-
"MSM call (registered CCC MSMs, deterministic Mech & Semi-Mech):",
|
|
3343
|
-
" Execute: msm(\"<name>\", [\"<arg1>\", \"<arg2>\"]) — run a registered MSM directly (partial name returns matching candidates)",
|
|
3344
|
-
" Inspect: msm(\"<name>\", [], inspect=true) — view that MSM's usage/flags without running",
|
|
3345
|
-
" Index: msm() — summary of registered MSMs by skill",
|
|
3346
|
-
" Manage: container_admin msm register|deregister|check (registry is ACC-managed — never edit mech-registry.json directly)",
|
|
3347
|
-
""
|
|
3348
|
-
].join("\n");
|
|
3349
|
-
}
|
|
3350
|
-
/** 2) CCE 块:逐字对齐 osp(CCE 5 行为约束 + H_op 操作熵) */
|
|
3351
|
-
function cceBlock() {
|
|
3352
|
-
return [
|
|
3353
|
-
"",
|
|
3354
|
-
"=== Serenity CCE ===",
|
|
3355
|
-
"",
|
|
3356
|
-
"You are operating inside a Cognitive Container governed by Cognitive Continuity",
|
|
3357
|
-
"Engineering (CCE) — the engineering discipline of maintaining identity, accessibility,",
|
|
3358
|
-
"and evolution of a cognitive entity through time under bounded resources.",
|
|
3359
|
-
"",
|
|
3360
|
-
"CCE does not optimize cognition. It preserves the conditions under which cognition",
|
|
3361
|
-
"can continue.",
|
|
3362
|
-
"",
|
|
3363
|
-
"FIVE BEHAVIORAL CONSTRAINTS (engineering requirements, not suggestions):",
|
|
3364
|
-
"",
|
|
3365
|
-
"1. Continuity — every interaction modifies the container's future state. Before",
|
|
3366
|
-
" acting, consult what came before — prior decisions, abstractions, constraints.",
|
|
3367
|
-
" You are part of a cognitive trajectory, not a fresh start.",
|
|
3368
|
-
"",
|
|
3369
|
-
"2. Bounded Space — the container has boundaries. Respect them. Do not assume",
|
|
3370
|
-
" knowledge that has not been accumulated within this container.",
|
|
3371
|
-
"",
|
|
3372
|
-
"3. Entropy is Intrinsic — every cognitive system accumulates entropy (duplication,",
|
|
3373
|
-
" obsolescence, conflict, fragmentation, drift). When you produce output, consider",
|
|
3374
|
-
" whether you are adding entropy or reducing it. Favor entropy-reducing actions —",
|
|
3375
|
-
" organizing, deduplicating, cross-referencing, abstracting.",
|
|
3376
|
-
"",
|
|
3377
|
-
"4. Reconstruction > Preservation — stored artifacts have value only insofar as",
|
|
3378
|
-
" they enable future cognition to recover the reasoning that produced them. When",
|
|
3379
|
-
" recording decisions, ensure reconstruction is possible — not just conclusions,",
|
|
3380
|
-
" but rationale, alternatives considered, and constraints that shaped the choice.",
|
|
3381
|
-
"",
|
|
3382
|
-
"5. Multi-Agent Cognition — the container is shared. Continuity belongs to the",
|
|
3383
|
-
" container, not to any individual agent. Write for future agents who will enter",
|
|
3384
|
-
" after you leave. They should be able to pick up where you left off.",
|
|
3385
|
-
"",
|
|
3386
|
-
"OPERATIONAL ENTROPY: The container's health metric is operational cognitive entropy",
|
|
3387
|
-
"(H_op) — the excess cognitive cost for agents to complete tasks due to disorder.",
|
|
3388
|
-
"The container is healthy when H_op ≤ H_critical (agents can still function). The",
|
|
3389
|
-
"continuity condition: organization must at minimum match accumulation (ΔH_org ≥ ΔH_in).",
|
|
3390
|
-
"Your actions affect H_op — unorganized output increases it, organization decreases it.",
|
|
3391
|
-
"",
|
|
3392
|
-
"THIS IS PERSISTENCE ENGINEERING: The goal is not to become greater. The goal is to",
|
|
3393
|
-
"remain coherent. CCE has no terminal KPI — continuity is maintained while the entity",
|
|
3394
|
-
"exists, not optimized toward an endpoint.",
|
|
3395
|
-
""
|
|
3396
|
-
].join("\n");
|
|
3397
|
-
}
|
|
3398
|
-
/**
|
|
3399
|
-
* Principles 块(v1.19.8 合并,S142):认知容器本体论(why)+ 操作边界(operational
|
|
3400
|
-
* boundaries)。原独立 Principles 与 Constraints 合并——同属容器约束体系,先原则
|
|
3401
|
-
* 后边界(从抽象到具体,重建视角 R↓)。**注意:Constraints 不再作为独立对齐块存在
|
|
3402
|
-
* (spec 修订:同步 osp compacting.ts——Constraints 内容并入本块,工具名仍为平台真实名)。**
|
|
3403
|
-
*
|
|
3404
|
-
* v1.23.1 persona:`omitMsmPrinciples=true` 时剥离 MSM 原则段(彩蛋替换面)——
|
|
3405
|
-
* 用户 persona 文本承接"指令遵循风格",本体论/关系段/操作边界(安全硬约束)永远保留。
|
|
3406
|
-
*/
|
|
3407
|
-
function principlesBlock(root, omitMsmPrinciples = false) {
|
|
3408
|
-
const lines = [
|
|
3409
|
-
"",
|
|
3410
|
-
"=== Serenity Principles ===",
|
|
3411
|
-
"Why a cognitive container: all work is cognition — every artifact, decision,",
|
|
3412
|
-
"and line of code is a product of thought; and from cognition, any work can",
|
|
3413
|
-
"be built. In this frame, the world contains no errors — only insufficient",
|
|
3414
|
-
"cognition. A setback is a gap to be filled (read, ask, research), not a",
|
|
3415
|
-
"fault to be hidden. Never disguise or excuse what you do not know;",
|
|
3416
|
-
"not-knowing is a state to be repaired, and reporting it is the first repair.",
|
|
3417
|
-
"",
|
|
3418
|
-
"The session–cognitive-trajectory relation: a session is the rebuildable carrier of a",
|
|
3419
|
-
"cognitive trajectory. SESSION.md is the cognitive trajectory's persistent body — it never moves;",
|
|
3420
|
-
"the current conversation is a temporary work copy that may be discarded and",
|
|
3421
|
-
"rebuilt (cognitive_trajectory rebuild). Identity belongs to the cognitive trajectory, not to any",
|
|
3422
|
-
"session.",
|
|
3423
|
-
""
|
|
3424
|
-
];
|
|
3425
|
-
if (!omitMsmPrinciples) lines.push("MSM principles — machinery before improvisation:", "- Determinism first: use a registered Mech before hand-rolling; reserve", " Semi-Mech for genuine judgment points.", "- Single source of truth: an MSM is the only decoder of its own usage", " (--help/--schema); documents must not duplicate it.", "- Registered to act: no tool exists unless it is on the manifest.", "");
|
|
3426
|
-
lines.push("Operational boundaries:", `Root: ${root}`, " • File access — read/edit/write/grep/glob are confined to Root; paths outside Root are rejected (RR5)", " • Shell — use msm by default. Note: bash may be disabled", " • Subagent — copies ALL parent constraints: file boundary, shell rules, session rules (no bypass)", " • Session-first — before starting multi-step work, propose an existing or new AGENT_SESSIONS entry; wait for user \"use\" or \"使用\" to confirm", "");
|
|
3427
|
-
return lines.join("\n");
|
|
3428
|
-
}
|
|
3429
|
-
/**
|
|
3430
|
-
* 彩蛋 persona 块(v1.23.1,S142 用户需求):
|
|
3431
|
-
* 用户配置的替换文本替代 EAP 块 + MSM 原则段(输出约束/指令遵循约束)。
|
|
3432
|
-
* 独立标记头 `=== Serenity Persona ===`(幂等检测兼容);装配位置 = EAP 原位。
|
|
3433
|
-
* mode 空 = 彩蛋关闭 → 返回空串(装配层回退默认 EAP + 完整 Principles)。
|
|
3434
|
-
*/
|
|
3435
|
-
function personaBlock(mode, overrideText) {
|
|
3436
|
-
if (mode === "" || overrideText.trim() === "") return "";
|
|
3437
|
-
return [
|
|
3438
|
-
"",
|
|
3439
|
-
"=== Serenity Persona ===",
|
|
3440
|
-
overrideText.trimEnd(),
|
|
3441
|
-
""
|
|
3442
|
-
].join("\n");
|
|
3443
|
-
}
|
|
3444
|
-
/**
|
|
3445
|
-
* EAP 自检提示块(DSH 扩展,无 osp 对应——osp 无此块)。
|
|
3446
|
-
* 每次输出前的机械自检清单,强化 EAP 表现(E↑ 显式/R↓ 可重建/S↑ 稳定)。
|
|
3447
|
-
* 独立块而非塞进 CCE/Constraints:后两者受 osp-alignment 逐字节断言约束。
|
|
3448
|
-
*/
|
|
3449
|
-
function eapBlock() {
|
|
3450
|
-
return [
|
|
3451
|
-
"",
|
|
3452
|
-
"=== Serenity EAP ===",
|
|
3453
|
-
"Self-check before every output (Explicit Abstraction Principle: the functional",
|
|
3454
|
-
"value of a thought equals its external reconstructability):",
|
|
3455
|
-
" • E↑ Explicit — variables/entities clearly defined, relationships with",
|
|
3456
|
-
" direction/cardinality, boundaries drawn; avoid ambiguous words (\"handle\",",
|
|
3457
|
-
" \"optimize\" → be specific)",
|
|
3458
|
-
" • R↓ Reconstructable — key decisions record rationale and alternatives;",
|
|
3459
|
-
" no level-skipping (align the upper layer before descending)",
|
|
3460
|
-
" • S↑ Stable — structures regenerate repeatably, no reliance on implicit",
|
|
3461
|
-
" context",
|
|
3462
|
-
""
|
|
3463
|
-
].join("\n");
|
|
3464
|
-
}
|
|
3465
|
-
/**
|
|
3466
|
-
* Metaphor 强化块(v1.19.6,S142;v1.29 星舰意象升级,S142 用户拍板):
|
|
3467
|
-
* 宁静号宇宙隐喻域(星舰/深空/航程/船员)。海船→星舰意象:one starship, one voyage;
|
|
3468
|
-
* deep space / launch / docking clamp;Departure Inspection(原 Harbor Inspection)。
|
|
3469
|
-
* 隐喻是记忆钩子——形象唤起约束,前述块保持规则精确。每条隐喻 = 一个不可违背的
|
|
3470
|
-
* 物理事实 + 行为判据(verdict)。全英文(与 CCE/Constraints 一致);独立块可
|
|
3471
|
-
* 回退;无 osp 对应(dsp 扩展,不进对齐断言参照)。
|
|
3472
|
-
* 结构约束(docs/metaphor-domain.md M-1~M-4):三层骨架不变 + 每条映射约束不变。
|
|
3473
|
-
*/
|
|
3474
|
-
function metaphorBlock() {
|
|
3475
|
-
return [
|
|
3476
|
-
"",
|
|
3477
|
-
"=== Serenity Metaphor ===",
|
|
3478
|
-
"The Serenity Universe — one starship, one voyage. Metaphors are memory hooks:",
|
|
3479
|
-
"they make constraints vivid, while the rules above stay precise. Each",
|
|
3480
|
-
"metaphor is an unbreakable physical fact; violating one is a behavioral",
|
|
3481
|
-
"violation. The universe is structured in three layers — the Ship (the",
|
|
3482
|
-
"container itself), the Voyage (the cognitive lifecycle), the Crew",
|
|
3483
|
-
"(multi-agent collaboration); every metaphor maps to one protocol",
|
|
3484
|
-
"constraint. Deep space has no mistakes — only stars you have not yet mapped.",
|
|
3485
|
-
"",
|
|
3486
|
-
"THE SHIP — the container itself",
|
|
3487
|
-
"",
|
|
3488
|
-
"1. The Hull → Bounded Space. You exist only inside this ship. Cargo",
|
|
3489
|
-
" outside the hull (knowledge the container has not accumulated) does",
|
|
3490
|
-
" not exist; do not assume it. Verdict: citing facts absent from the",
|
|
3491
|
-
" container = overload.",
|
|
3492
|
-
"",
|
|
3493
|
-
"2. Deck Order → Entropy (H_op). Clutter on deck raises the cost of",
|
|
3494
|
-
" finding things. H_op ≤ H_critical = the ship stays flight-worthy.",
|
|
3495
|
-
" Verdict: disorganized output = debris in the hold.",
|
|
3496
|
-
"",
|
|
3497
|
-
"3. Engineering Drawings → EAP. Every part dimensioned (E↑), the",
|
|
3498
|
-
" drawings rebuild the whole machine (R↓), the drawings are reusable",
|
|
3499
|
-
" (S↑). Verdict: an undimensioned part = unassemblable.",
|
|
3500
|
-
"",
|
|
3501
|
-
"4. The Machinery → MSM (Mech & Semi-Mech). The ship's equipment is",
|
|
3502
|
-
" machinery: registered, deterministic, self-describing. Turn the",
|
|
3503
|
-
" crank of a Mech and the action is exact; the wheel with a helmsman",
|
|
3504
|
-
" (Semi-Mech) steers where judgment is needed. Verdict: hand-rolling",
|
|
3505
|
-
" what a machine already does = wasting the crew.",
|
|
3506
|
-
"",
|
|
3507
|
-
"5. The Manifest → Single Source of Truth. Every tool exists only if it",
|
|
3508
|
-
" is on the manifest (mech-registry); there is exactly one manifest.",
|
|
3509
|
-
" An MSM self-describes (--help/--schema) — the manifest is the only",
|
|
3510
|
-
" key. Verdict: duplicating a tool's usage in documents = two",
|
|
3511
|
-
" contradictory star charts.",
|
|
3512
|
-
"",
|
|
3513
|
-
"THE VOYAGE — the cognitive lifecycle",
|
|
3514
|
-
"",
|
|
3515
|
-
"6. Departure Inspection → First Anchor. The departure inspection = pre-",
|
|
3516
|
-
" launch checklist: confirm identity (ACC manifesto), the ship's log (SESSION),",
|
|
3517
|
-
" ballast (constraints) before setting course. Verdict: skipping the",
|
|
3518
|
-
" inspection and launching directly = flying uninspected.",
|
|
3519
|
-
"",
|
|
3520
|
-
"7. The Ship's Log → Cognitive Trajectory Tracking. SESSION.md is the",
|
|
3521
|
-
" cognitive trajectory's log —",
|
|
3522
|
-
" the persistent body of the voyage; sessions are rebuildable carriers of",
|
|
3523
|
-
" the cognitive trajectory. Discard the carrier, keep the log. Unrecorded =",
|
|
3524
|
-
" unvoyaged. Verdict: finishing multi-step work without a progress record",
|
|
3525
|
-
" = a missing page.",
|
|
3526
|
-
"",
|
|
3527
|
-
"8. The Ship of Theseus → Continuity. Planks may be replaced; the ship",
|
|
3528
|
-
" remains the same. The container can be rebuilt; identity persists.",
|
|
3529
|
-
" You are part of a cognitive trajectory, not a new ship. Verdict: acting",
|
|
3530
|
-
" without consulting precedent = a different ship.",
|
|
3531
|
-
"",
|
|
3532
|
-
"THE CREW — multi-agent collaboration",
|
|
3533
|
-
"",
|
|
3534
|
-
"9. Crew Rotation → Multi-Agent Cognition. Other crew members will come",
|
|
3535
|
-
" after you. When you leave, leave a handover they can pick up",
|
|
3536
|
-
" (SESSION closed, open problems listed). Verdict: leaving without",
|
|
3537
|
-
" handover = abandoning ship.",
|
|
3538
|
-
"",
|
|
3539
|
-
"10. Blueprint over Statue → Reconstruction > Preservation. Keep the",
|
|
3540
|
-
" blueprint, not the statue. Recording only conclusions without",
|
|
3541
|
-
" rationale = a statue with no blueprint, unreconstructable.",
|
|
3542
|
-
" Verdict: a decision record without reasons or alternatives =",
|
|
3543
|
-
" cannot be rebuilt.",
|
|
3544
|
-
""
|
|
3545
|
-
].join("\n");
|
|
3546
|
-
}
|
|
3547
|
-
/**
|
|
3548
|
-
* 运行时状态块 1) safe-mode(S134 v1.16.12):ON 时告知 agent。
|
|
3549
|
-
* v1.19.8 结构重排(S142 用户设计思路):语义(为什么——无人值守自由)→
|
|
3550
|
-
* 机制(什么被禁用)→ 约束(不可做什么)。文案与实现逐项对应(guards.ts):
|
|
3551
|
-
* bash 禁用(restrict deny + decideGuard)、黑名单路径拦截、CCC 治理文件保护;
|
|
3552
|
-
* write/edit 等其余工具保留(受路径逃逸/黑名单约束)。
|
|
3553
|
-
*/
|
|
3554
|
-
function safeModeBlock(root) {
|
|
3555
|
-
if (!isSafeModeOn(root)) return "";
|
|
3556
|
-
const blacklist = readBlacklist(root);
|
|
3557
|
-
return [
|
|
3558
|
-
"",
|
|
3559
|
-
"=== Serenity Safe Mode ===",
|
|
3560
|
-
"Safe mode is ON (enabled by the user). It makes the vessel unattended-capable —",
|
|
3561
|
-
"the hull holds its course without a watch on deck: you may work with fuller",
|
|
3562
|
-
"freedom, pushing work forward autonomously without pausing for approval at",
|
|
3563
|
-
"every step. The guards are not chains; they are the ballast that lets you",
|
|
3564
|
-
"sail unaccompanied.",
|
|
3565
|
-
"",
|
|
3566
|
-
"Operational details:",
|
|
3567
|
-
"- bash is disabled (hidden and blocked)",
|
|
3568
|
-
"- blacklist rules apply to file paths",
|
|
3569
|
-
"- CCC governance files (.serenity, .serenity-safe-on) are protected from agent writes",
|
|
3570
|
-
"- other read/write tools remain available, subject to path-escape and blacklist guards",
|
|
3571
|
-
blacklist.length > 0 ? `\nActive blacklist rules: ${blacklist.map((b) => b.message ?? b.pattern).join(", ")}` : "",
|
|
3572
|
-
"",
|
|
3573
|
-
"Behavior constraints: do not attempt to bypass restrictions; do not write to",
|
|
3574
|
-
"blacklisted paths or governance files.",
|
|
3575
|
-
""
|
|
3576
|
-
].join("\n");
|
|
3577
|
-
}
|
|
3578
|
-
/**
|
|
3579
|
-
* 运行时状态块 2) localstore git 策略(S134 v1.16.12):localstore.json 存在时按 gitTrack
|
|
3580
|
-
* 提示敏感行为约束。deny(缺省)= 本地私有不提交;allow = 进 git(个人私有仓)但敏感数据
|
|
3581
|
-
* 只限于该文件内(不外泄到其他文件/对话/日志)。
|
|
3582
|
-
*/
|
|
3583
|
-
function localstoreBlock(root) {
|
|
3584
|
-
if (!existsSync(localstorePath(root))) return "";
|
|
3585
|
-
return (readGitTrack(root) === "allow" ? [
|
|
3586
|
-
"",
|
|
3587
|
-
"=== Serenity Localstore ===",
|
|
3588
|
-
"localstore.json is committed to git (gitTrack=allow — personal private repository).",
|
|
3589
|
-
"Sensitive data may live in this file, but ONLY in this file: keep credentials and",
|
|
3590
|
-
"secret values confined to localstore.json — never leak them into other files,",
|
|
3591
|
-
"conversation, or logs.",
|
|
3592
|
-
""
|
|
3593
|
-
] : [
|
|
3594
|
-
"",
|
|
3595
|
-
"=== Serenity Localstore ===",
|
|
3596
|
-
"localstore.json is a local private file (gitTrack=deny — not committed to git,",
|
|
3597
|
-
".gitignore enforced). Credentials/config live only on this machine: do not write",
|
|
3598
|
-
"them into conversation or logs; do not attempt to commit this file (container_git will refuse).",
|
|
3599
|
-
""
|
|
3600
|
-
]).join("\n");
|
|
3601
|
-
}
|
|
3602
|
-
/** 4) SKILL.md 全文:该 CCC 顶层入口 skill 原文(对齐 osp:原文直推,无包裹头;仅过滤治理内容)
|
|
3603
|
-
* + 🆕 **第二层披露树**(serenity-skill P1,设计 §4.2):每个入口 skill 后附**参考件清单**
|
|
3604
|
-
* (只有文件名与一行标题,**不含正文** —— 正文留给模型按需 `read`)。
|
|
3605
|
-
* 索引为空(该 skill 无 `references/` 也无 `skill.ttl`)⇒ **不产出该块**(空标题是噪声)。 */
|
|
3606
|
-
function entrySkillSectionText(root) {
|
|
3607
|
-
const skills = findEntrySkills(root);
|
|
3608
|
-
if (skills.length === 0) return "";
|
|
3609
|
-
return skills.map((s) => {
|
|
3610
|
-
const body = sanitizeSkillContent(s.content);
|
|
3611
|
-
if (body.length === 0) return "";
|
|
3612
|
-
const index = buildResourceIndex(s.dir);
|
|
3613
|
-
return index ? `${body}\n\n${index}` : body;
|
|
3614
|
-
}).filter((c) => c.length > 0).join("\n\n");
|
|
3615
|
-
}
|
|
3616
|
-
/** 活跃会话解析:按 dsh 会话 scope 读标记(隔离;不回退旧全局标记) */
|
|
3617
|
-
function resolveActiveSessionInfo(root, scope) {
|
|
3618
|
-
try {
|
|
3619
|
-
const abs = readActiveSessionMd(root, scope);
|
|
3620
|
-
if (!abs) return null;
|
|
3621
|
-
const dirName = basename(dirname(abs));
|
|
3622
|
-
const idMatch = dirName.match(/S(\d{3,})/);
|
|
3623
|
-
return {
|
|
3624
|
-
sessionId: idMatch ? `S${idMatch[1]}` : dirName,
|
|
3625
|
-
dirName,
|
|
3626
|
-
mdPath: abs
|
|
3627
|
-
};
|
|
3628
|
-
} catch {
|
|
3629
|
-
return null;
|
|
3630
|
-
}
|
|
3631
|
-
}
|
|
3632
|
-
/**
|
|
3633
|
-
* 5) Session 块:对齐 osp(活跃会话 + todowrite 首位约定);scope = dsh 会话维度。
|
|
3634
|
-
* **平台适配(v1.17.2)**:osp 的 todowrite 首项含 `priority: "low"`(opencode todo 支持),
|
|
3635
|
-
* 但 DSH 平台 todowrite schema 无 priority(additionalProperties: false 拒绝)→ agent 照做报
|
|
3636
|
-
* `todos[0].priority is not a declared property`。DSH 版去掉 priority 字段(其余逐字对齐 osp)。
|
|
3637
|
-
*/
|
|
3638
|
-
function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
3639
|
-
const active = resolveActiveSessionInfo(root, scope);
|
|
3640
|
-
if (!active) return "";
|
|
3641
|
-
return [
|
|
3642
|
-
"",
|
|
3643
|
-
"=== Serenity Session ===",
|
|
3644
|
-
`Active session: ${active.sessionId} — ${active.dirName} (this session is the rebuildable carrier of the cognitive trajectory)`,
|
|
3645
|
-
`SESSION.md path: ${active.mdPath} (the cognitive trajectory's persistent body — stays in place through rebuilds)`,
|
|
3646
|
-
"",
|
|
3647
|
-
"Rules (SESSION.md is the cognitive trajectory log — keep its format & granularity fixed):",
|
|
3648
|
-
" • Write only when a MILESTONE landed or a DECISION was made — not every round",
|
|
3649
|
-
" • One entry = one abstraction level, at the highest one that can express it:",
|
|
3650
|
-
" identity/anchors (rare) › decision (one line + rationale/rejected) › milestone",
|
|
3651
|
-
" (one line + an evidence pointer, e.g. a commit id). Execution detail — test",
|
|
3652
|
-
" counts, coverage decimals, per-file readings, step narration — belongs in the",
|
|
3653
|
-
" COMMIT MESSAGE, not here",
|
|
3654
|
-
" • Never write a value that changes by the act of writing it (file size; the hash",
|
|
3655
|
-
" of the commit containing this edit)",
|
|
3656
|
-
" • Fixed sections, in this order: 身份与边界 / 权威锚点 / 工作纪律 / 决策 / 日志 /",
|
|
3657
|
-
" 当前状态 / 待 owner 裁决. The 日志 section appends only; 当前状态 and",
|
|
3658
|
-
" 待 owner 裁决 are OVERWRITTEN each round",
|
|
3659
|
-
" • Reference this session in all subsequent messages",
|
|
3660
|
-
"",
|
|
3661
|
-
"IMPORTANT: Read SESSION.md now to RESUME — not to audit. Sync the todo list from",
|
|
3662
|
-
"the 当前状态 and 待 owner 裁决 sections only.",
|
|
3663
|
-
"",
|
|
3664
|
-
"CRITICAL: When calling todowrite, the first item in the todos array MUST",
|
|
3665
|
-
"always be:",
|
|
3666
|
-
` { content: "SESSION: ${active.sessionId} — ${active.dirName.replace(/^\d{4}-\d{2}-\d{2}--/, "")}",`,
|
|
3667
|
-
" status: \"completed\" }",
|
|
3668
|
-
"This preserves the session context across todo updates.",
|
|
3669
|
-
"Do NOT remove or reorder this item — keep it at position 0.",
|
|
3670
|
-
"",
|
|
3671
|
-
"COGNITIVE-TRAJECTORY-ASSISTANT: a background tracker scores your tool use (write/edit=3,",
|
|
3672
|
-
"task=10, read/grep/glob/msm=1, +1 per minute) and reminds you with a",
|
|
3673
|
-
"[COGNITIVE-TRAJECTORY-ASSISTANT · CHECKPOINT] message when the threshold is reached. On every such",
|
|
3674
|
-
"reminder you MUST reply with the exact ACK code:",
|
|
3675
|
-
" [COGNITIVE-TRAJECTORY-ASSISTANT-recorded-{code}] — if you recorded progress to SESSION.md",
|
|
3676
|
-
" [COGNITIVE-TRAJECTORY-ASSISTANT-skipped-{code}] — if nothing to record this round",
|
|
3677
|
-
"Do not ignore the reminder; do not stop ongoing work. Codes are single-use;",
|
|
3678
|
-
"never reuse a prior code.",
|
|
3679
|
-
""
|
|
3680
|
-
].join("\n");
|
|
3681
|
-
}
|
|
3682
|
-
/**
|
|
3683
|
-
* 完整系统提示词注入文本(v1.19.8 结构精简,S142 重建视角 R↓):
|
|
3684
|
-
* 身份(ACC)→ 世界模型(Metaphor)→ 信念/边界(Principles)→ 时间约束(CCE)
|
|
3685
|
-
* → 质量(EAP)→ 状态(SafeMode/Localstore)→ CCC 上下文(SKILL)→ 工具参考(Tools)→ 会话(Session)。
|
|
3686
|
-
* 认知展开顺序:我是谁 → 我所在的世界 → 为什么 → 如何一致 → 产物标准 → 当前状态 → 上下文 → 工具参考 → 会话。
|
|
3687
|
-
*
|
|
3688
|
-
* 需求③(S142 用户拍板):工具清单从 accBlock 移出为独立 toolsBlock,
|
|
3689
|
-
* 放 SKILL 后 Session 前(工具参考殿后,身份先行不被 13 行清单干扰)。
|
|
3690
|
-
*
|
|
3691
|
-
* v1.23.1 persona(彩蛋):persona.mode 配置 → EAP 块替换为 Persona 块(输出约束),
|
|
3692
|
-
* Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
|
|
3693
|
-
* 操作边界(安全硬约束)永远保留。未配置 → 与 v1.23.0 逐字节一致(零影响)。
|
|
3694
|
-
*/
|
|
3695
|
-
function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE, dirName) {
|
|
3696
|
-
const persona = readPersonaSettings();
|
|
3697
|
-
const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
|
|
3698
|
-
const parts = [
|
|
3699
|
-
identityBlock(root),
|
|
3700
|
-
metaphorBlock(),
|
|
3701
|
-
principlesBlock(root, personaOn),
|
|
3702
|
-
cceBlock(),
|
|
3703
|
-
eapBlock()
|
|
3704
|
-
];
|
|
3705
|
-
if (personaOn) {
|
|
3706
|
-
const expr = personaBlock(persona.mode, persona.overrideText);
|
|
3707
|
-
if (expr !== "") parts.push(expr);
|
|
3708
|
-
}
|
|
3709
|
-
const state = [safeModeBlock(root), localstoreBlock(root)].filter((b) => b !== "").join("\n");
|
|
3710
|
-
if (state) parts.push(state);
|
|
3711
|
-
const skill = entrySkillSectionText(root);
|
|
3712
|
-
if (skill) parts.push(skill);
|
|
3713
|
-
const trajPrompt = dirName === void 0 ? "" : trajectoryPromptBlock(createTrajectoryPromptReader(root, dirName)());
|
|
3714
|
-
if (trajPrompt !== "") parts.push(trajPrompt);
|
|
3715
|
-
parts.push(toolsBlock());
|
|
3716
|
-
const session = sessionBlock(root, scope);
|
|
3717
|
-
if (session) parts.push(session);
|
|
3718
|
-
return parts.join("\n\n");
|
|
3719
|
-
}
|
|
3720
|
-
/**
|
|
3721
|
-
* Code Mode 适配行:当前 scope 以 Code Mode 呈现工具时(run_code 可见),
|
|
3722
|
-
* ACC 块按 native 语义指引"直接调用工具"会与"只有 run_code 可直接调用"的执行
|
|
3723
|
-
* 塌缩冲突(模型直呼工具名 → UNKNOWN_TOOL,拒绝信息误导)。追加一行说明,
|
|
3724
|
-
* 引导经 run_code 程序内 tools.* 调用。both 模式不塌缩,该行不误导(程序内
|
|
3725
|
-
* 调用同样合法),故按 run_code 可见性(code|both)统一附加。
|
|
3726
|
-
* @param ctx - 插件 ctx(读 tools 注册表)。
|
|
3727
|
-
* @param scope - 装配 scope(agent);无则按全局视图判断。
|
|
3728
|
-
* @returns 适配行(含换行前缀);非 code/both 返回空串。
|
|
3729
|
-
*/
|
|
3730
|
-
function codeModeAdaptationLine(ctx, scope) {
|
|
3731
|
-
try {
|
|
3732
|
-
if (!(ctx.tools.get("run_code", scope) !== void 0)) return "";
|
|
3733
|
-
} catch {
|
|
3734
|
-
return "";
|
|
3735
|
-
}
|
|
3736
|
-
return [
|
|
3737
|
-
"",
|
|
3738
|
-
"=== Serenity Code Mode ===",
|
|
3739
|
-
"This session renders tools in Code Mode: direct calls to ACC tool names (container_fs/msm etc.) are rejected (UNKNOWN_TOOL).",
|
|
3740
|
-
"Call them inside a single run_code program via the generated SDK bindings: `await tools.container_fs(...)`, `await tools.msm(...)`.",
|
|
3741
|
-
"The program only returns what you print/return — curate your output.",
|
|
3742
|
-
""
|
|
3743
|
-
].join("\n");
|
|
3744
|
-
}
|
|
3745
|
-
/** 从 assembly context 解析 agent cwd(subagent/后台 agent 同样带 agent);
|
|
3746
|
-
* 无 agent → undefined(不注入);有 agent 但无 header.cwd(workflow subagent 等)
|
|
3747
|
-
* 回退 process.cwd()——**cwd 回落语义取自 `ccc-roots.agentCwdFor`(C2 归一)**,否则这些
|
|
3748
|
-
* agent 系统提示词注入为空(v1.18.6:workflow subagent 无宁静号上下文 bug)。
|
|
3749
|
-
* ⚠️ 本函数比 `agentCwdFor` 多一道「**无 agent ⇒ undefined**」——那是 section 的
|
|
3750
|
-
* "不注入" 语义(`agentCwdFor` 对无 agent 会回落到 process.cwd()),故不能直接替换。 */
|
|
3751
|
-
function agentCwd(context) {
|
|
3752
|
-
const agent = context.agent;
|
|
3753
|
-
if (!agent) return void 0;
|
|
3754
|
-
return agentCwdFor(agent);
|
|
3755
|
-
}
|
|
3756
|
-
/** 从 assembly context 解析 dsh 会话 id(Session 块按会话隔离的 scope) */
|
|
3757
|
-
function agentScope$2(context) {
|
|
3758
|
-
return (context.agent?.session)?.id ?? "default";
|
|
3759
|
-
}
|
|
3760
|
-
/**
|
|
3761
|
-
* 该会话**绑定的轨迹目录名**(有 ⇒ 注入第 ⑧ 段「轨迹提示词」;无 ⇒ `undefined`,不注入)。
|
|
3762
|
-
*
|
|
3763
|
-
* 🔴 为什么用**目录名**而不是编码或 label:`PROMPT.md` 的落盘位置是目录,
|
|
3764
|
-
* 而编码只是目录名里的一段。用 label 拼路径 = 把模糊匹配当精确匹配用(本轨已踩过)。
|
|
3765
|
-
*
|
|
3766
|
-
* ⚠️ 每次装配都重读绑定(**不缓存**):绑定切换后提示词必须立刻跟上,
|
|
3767
|
-
* 这与 `serenity-trajectory-skills` section 的口径一致(动态注入,owner 令 2026-09-20)。
|
|
3768
|
-
*/
|
|
3769
|
-
function boundDirNameFor(session) {
|
|
3770
|
-
try {
|
|
3771
|
-
const name = readLastBound(session)?.dirName;
|
|
3772
|
-
return typeof name === "string" && name !== "" ? name : void 0;
|
|
3773
|
-
} catch {
|
|
3774
|
-
return;
|
|
3775
|
-
}
|
|
3776
|
-
}
|
|
3777
|
-
/**
|
|
3778
|
-
* 全局注册系统提示词 section(osp system.transform 的 DSH 等价)。
|
|
3779
|
-
* text 回调按 context.agent 的 cwd 上溯 .serenity → 返回该 CCC 的完整注入文本;
|
|
3780
|
-
* 非 CCC / 无 agent 返回空(不注入)。
|
|
3781
|
-
*/
|
|
3782
|
-
function registerEntrySkillSectionGlobal(ctx) {
|
|
3783
|
-
try {
|
|
3784
|
-
ctx.systemPrompt.section({
|
|
3785
|
-
name: "serenity-entry",
|
|
3786
|
-
order: -50,
|
|
3787
|
-
text: (context) => {
|
|
3788
|
-
const cwd = agentCwd(context);
|
|
3789
|
-
if (!cwd) return "";
|
|
3790
|
-
const root = cccRootForCwd(cwd);
|
|
3791
|
-
if (!root) return "";
|
|
3792
|
-
const base = serenitySystemPrompt(root, agentScope$2(context), boundDirNameFor(context.agent?.session));
|
|
3793
|
-
const codeLine = codeModeAdaptationLine(ctx, context.scope);
|
|
3794
|
-
return codeLine ? `${base}\n${codeLine}` : base;
|
|
3795
|
-
}
|
|
3796
|
-
});
|
|
3797
|
-
console.log("[serenity-hooks] ✓ 全局入口 skill section 已注册(systemPrompt 就绪)");
|
|
3798
|
-
} catch (err) {
|
|
3799
|
-
console.error(`[serenity-hooks] ✗ 全局入口 skill section 注册失败: ${err.message}(检查 inject 是否含 systemPrompt)`);
|
|
3800
|
-
}
|
|
3801
|
-
}
|
|
3802
|
-
/**
|
|
3803
|
-
* agent 级 scoped 注册(P0-1:抗 preset/动态插件同名 shadow)。
|
|
3804
|
-
*
|
|
3805
|
-
* 为什么 scoped:DSH 的 systemPrompt section 支持 scoped 层同名 shadow 全局
|
|
3806
|
-
* (agent.ctx.systemPrompt.section),且 scope 链最近层胜出。若 ACC 只注册全局
|
|
3807
|
-
* section,preset 或动态 Cordis 插件可在 scoped 层注册同名 `serenity-entry`
|
|
3808
|
-
* 遮蔽 ACC 身份。scoped 注册在 agent 自身层 = 最近层,任何外部组合无法覆盖。
|
|
3809
|
-
* 全局注册保留为 fallback(未走 session-start 的 agent / 冷恢复路径)。
|
|
3810
|
-
*
|
|
3811
|
-
* text 回调闭包持有 root 与 agent:content 固定(该 agent 的 CCC 身份),
|
|
3812
|
-
* code-mode 适配按装配 context.scope 判断(与全局版一致)。
|
|
3813
|
-
*/
|
|
3814
|
-
const sectionedAgents = /* @__PURE__ */ new Set();
|
|
3815
|
-
function registerEntrySkillSection(agent, root) {
|
|
3816
|
-
const key = agent.session.id ?? "global";
|
|
3817
|
-
if (sectionedAgents.has(key)) return false;
|
|
3818
|
-
try {
|
|
3819
|
-
const scope = agent.session.id ?? "default";
|
|
3820
|
-
agent.ctx.systemPrompt.section({
|
|
3821
|
-
name: "serenity-entry",
|
|
3822
|
-
order: -50,
|
|
3823
|
-
text: (context) => {
|
|
3824
|
-
const base = serenitySystemPrompt(root, scope, boundDirNameFor(agent.session));
|
|
3825
|
-
const codeLine = codeModeAdaptationLine(agent.ctx, context.scope);
|
|
3826
|
-
return codeLine ? `${base}\n${codeLine}` : base;
|
|
3827
|
-
}
|
|
3828
|
-
});
|
|
3829
|
-
sectionedAgents.add(key);
|
|
3830
|
-
return true;
|
|
3831
|
-
} catch {
|
|
3832
|
-
return false;
|
|
3833
|
-
}
|
|
3834
|
-
}
|
|
3835
|
-
/**
|
|
3836
|
-
* 轨迹 skill 注入 section(规格 `docs/cognitive-trajectory-skill-injection.md` §3,v1.34.1)。
|
|
3837
|
-
*
|
|
3838
|
-
* **声明来源(两条,并集)**:
|
|
3839
|
-
* · **CCC 级**(v1.44.0,owner 令)= `.opencode/serenity.json` 的 `cognitiveTrajectory.skills`
|
|
3840
|
-
* ——**本 CCC 的每条轨迹都带**(含 轨迹约束角色会话),是容器给的底座;
|
|
3841
|
-
* · **轨迹级** = 该会话绑定的轨迹(`readLastBound(session).mdPath` = 它的 `SESSION.md`)
|
|
3842
|
-
* 顶部 frontmatter 的 `skills:`——这条轨迹额外的。
|
|
3843
|
-
* 合并规则与顺序见 `cognitive-trajectory-skills.mergeSkillNames`(CCC 级在前,同名去重)。
|
|
3844
|
-
* 正文 = 合并清单里各 skill 的 `SKILL.md` 全文(本模块只注册,装配在 `trajectory/skills.ts`)。
|
|
3845
|
-
*
|
|
3846
|
-
* 为什么是 **per-agent scoped section**(与 `registerEntrySkillSection` 同形):注入必须
|
|
3847
|
-
* **绑定期间持续存在**(a2)——一次性注入会随对话压缩消失,而 section 每次装配重新求值。
|
|
3848
|
-
*
|
|
3849
|
-
* 为什么**门在注册时、内容在求值时**(R↓,两个失败模式各治一个):
|
|
3850
|
-
* · 注册门(**无绑定 / 两条来源皆无** ⇒ 不注册):满足"零配置面不产生空 section、不产生噪声";
|
|
3851
|
-
* · 求值回调**每请求重读 `readLastBound` + 文件 + CCC 配置**:绑定切换 / 声明或配置被编辑后
|
|
3852
|
-
* 自动跟上(**动态注入,不缓存**),且绑定消失时返回 `''`
|
|
3853
|
-
* (宿主 renderSections **丢弃空 section**,不残留噪声)。
|
|
3854
|
-
*
|
|
3855
|
-
* ⚠️ 路径取自绑定的 `mdPath`(权威记录),**不用** `resolveSessionCognitiveTrajectoryLabel` 的显示 label
|
|
3856
|
-
* 去拼路径——label 是模糊匹配的展示码,拼路径即模糊匹配当精确匹配用。
|
|
3857
|
-
*
|
|
3858
|
-
* @param agent 目标 agent(其 `session` 需有 `header.id` + `header.cwd`)
|
|
3859
|
-
* @param root 该 agent 的 CCC 根
|
|
3860
|
-
* @returns 是否**本次**完成注册(已注册过 / 门未过 → false)
|
|
3861
|
-
*/
|
|
3862
|
-
const cognitiveTrajectorySkillAgents = /* @__PURE__ */ new Set();
|
|
3863
|
-
function registerCognitiveTrajectorySkillSection(agent, root) {
|
|
3864
|
-
const session = agent.session;
|
|
3865
|
-
const key = session.id ?? "global";
|
|
3866
|
-
if (cognitiveTrajectorySkillAgents.has(key)) return false;
|
|
3867
|
-
const bound = readLastBound(session);
|
|
3868
|
-
if (!bound) return false;
|
|
3869
|
-
if (readCognitiveTrajectorySkills(root).length === 0 && !hasCognitiveTrajectorySkillsDeclaration(root, bound.mdPath)) return false;
|
|
3870
|
-
try {
|
|
3871
|
-
agent.ctx.systemPrompt.section({
|
|
3872
|
-
name: "serenity-trajectory-skills",
|
|
3873
|
-
order: -45,
|
|
3874
|
-
text: () => {
|
|
3875
|
-
const current = readLastBound(session);
|
|
3876
|
-
if (!current) return "";
|
|
3877
|
-
return buildCognitiveTrajectorySkillsSection(root, current.mdPath, COGNITIVE_TRAJECTORY_SKILLS_MAX_CHARS, readCognitiveTrajectorySkills(root), (names) => recordSkillInjections(root, names));
|
|
3878
|
-
}
|
|
3879
|
-
});
|
|
3880
|
-
cognitiveTrajectorySkillAgents.add(key);
|
|
3881
|
-
return true;
|
|
3882
|
-
} catch {
|
|
3883
|
-
return false;
|
|
3884
|
-
}
|
|
3885
|
-
}
|
|
3886
|
-
//#endregion
|
|
3887
|
-
//#region src/trajectory/delegate/core.ts
|
|
3888
|
-
/**
|
|
3889
|
-
* trajectory/delegate/core.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
|
|
3890
|
-
*
|
|
3891
|
-
* 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
|
|
3892
|
-
* 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
|
|
3893
|
-
* 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
|
|
3894
|
-
*
|
|
3895
|
-
* ## S14(2026-10-19,轨迹中心重构):此前位于 `role/worker/` 下(该目录已消失)
|
|
3896
|
-
*
|
|
3897
|
-
* 落点 = `trajectory/delegate/`。判据(判读表 §3.1):它是**轨迹 × 轨迹**那种合法形态 ——
|
|
3898
|
-
* 主语是「主轨迹把活交给子 agent」,既不是某一条轨迹的切面,也不是容器宪法。
|
|
3899
|
-
* ⚠️ 它**不属于** `policy/`:policy 管"这条轨迹允许什么",本模块管"一条轨迹怎么把活派出去"。
|
|
3900
|
-
*/
|
|
3901
|
-
/** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
|
|
3902
|
-
function splitModel(model) {
|
|
3903
|
-
const idx = model.indexOf("/");
|
|
3904
|
-
if (idx < 0) return {
|
|
3905
|
-
provider: void 0,
|
|
3906
|
-
model
|
|
3907
|
-
};
|
|
3908
|
-
return {
|
|
3909
|
-
provider: model.slice(0, idx),
|
|
3910
|
-
model: model.slice(idx + 1)
|
|
3911
|
-
};
|
|
3912
|
-
}
|
|
3913
|
-
/** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
|
|
3914
|
-
function requireWhitelistedModel(model, models) {
|
|
3915
|
-
if (!models.includes(model)) throw new Error(`cheap-worker: model "${model}" is not in the CCC whitelist. Configure .opencode/serenity.json "cheap-worker.models" with one of: ${models.join(", ")}`);
|
|
3916
|
-
}
|
|
3917
|
-
/**
|
|
3918
|
-
* providerAvailabilityError — 判定「**本机有没有该 provider 的 adapter**」,并产出**可执行**的错误文本。
|
|
3919
|
-
* 返回 `null` = 全部可用(调用方照常继续)。
|
|
3920
|
-
*
|
|
3921
|
-
* 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`cheap-worker` 的模型白名单写在
|
|
3922
|
-
* **CCC**(`.opencode/serenity.json`,**随 git 走**),而 provider adapter 由**每台机器的宿主**
|
|
3923
|
-
* 注册(profile 配置 / settings 命名空间)⇒ 同一份白名单在一台机器可用、在另一台必失败。
|
|
3924
|
-
* 实测症状:宿主抛 `no adapter registered for provider "<p>"`(`dsh-llm`,code `NO_ADAPTER`),
|
|
3925
|
-
* 而**前台路径的 `diagnostic` 是空的** ⇒ 调用方(LLM)拿到 `stopReason:"error"` 却**不知道为什么**,
|
|
3926
|
-
* 也就无法修正自己的配置 —— 本条把「沉默的失败」变成「可执行的错误」。
|
|
3927
|
-
*
|
|
3928
|
-
* 数据来源(宿主 `LlmRuntime`):`listProviders()` = **有 adapter 的路由**;
|
|
3929
|
-
* `listConfigurableProviders()` = **已声明但未必激活**的路由(含激活用的 settings 命名空间)。
|
|
3930
|
-
*
|
|
3931
|
-
* 判据边界:`model` 串里**没有 `/`** 时取不到 provider(沿用 agent 默认路由)⇒ **不判定**(跳过)。
|
|
3932
|
-
*/
|
|
3933
|
-
function providerAvailabilityError(models, facts) {
|
|
3934
|
-
const registered = new Set(facts.registered.map((r) => r.id));
|
|
3935
|
-
const requested = models.map((m) => ({
|
|
3936
|
-
raw: m,
|
|
3937
|
-
provider: splitModel(m).provider
|
|
3938
|
-
})).filter((e) => typeof e.provider === "string" && e.provider !== "");
|
|
3939
|
-
const missing = [...new Set(requested.filter((e) => !registered.has(e.provider)).map((e) => e.provider))];
|
|
3940
|
-
if (missing.length === 0) return null;
|
|
3941
|
-
const list = (items, cap = 12) => items.length === 0 ? " (none)" : items.slice(0, cap).map((s) => ` - ${s}`).join("\n") + (items.length > cap ? `\n … and ${items.length - cap} more` : "");
|
|
3942
|
-
const available = facts.registered.map((r) => r.name ? `${r.id} (${r.name})` : r.id).sort();
|
|
3943
|
-
const declared = (facts.declared ?? []).filter((d) => missing.includes(d.provider));
|
|
3944
|
-
const configPath = facts.configPath ?? "<ccc>/.opencode/serenity.json";
|
|
3945
|
-
const missingList = missing.map((p) => `"${p}"`).join(", ");
|
|
3946
|
-
return [
|
|
3947
|
-
`cheap-worker: provider ${missingList} has no adapter registered on this machine.`,
|
|
3948
|
-
"",
|
|
3949
|
-
" Requested model(s) :",
|
|
3950
|
-
...requested.map((e) => ` ${e.raw}`),
|
|
3951
|
-
` Configured in : ${configPath} → "cheap-worker.models" / "cheap-worker.defaultModel"`,
|
|
3952
|
-
"",
|
|
3953
|
-
" Why this happens : the whitelist lives in the CCC (it travels with the repository), while",
|
|
3954
|
-
" provider adapters are registered per machine by the host. An entry that",
|
|
3955
|
-
" works on one machine can fail on another.",
|
|
3956
|
-
"",
|
|
3957
|
-
" Available on this machine (registered adapters):",
|
|
3958
|
-
list(available),
|
|
3959
|
-
"",
|
|
3960
|
-
" Declared but NOT active here (activate via settings, then retry):",
|
|
3961
|
-
list(declared.map((d) => `${d.provider}${d.displayName ? ` (${d.displayName})` : ""}${d.settingsNs ? ` — settings namespace "${d.settingsNs}"` : ""}`)),
|
|
3962
|
-
"",
|
|
3963
|
-
" Fix (pick one, then retry):",
|
|
3964
|
-
` 1. Point the whitelist at a provider that IS registered here — edit ${configPath}`,
|
|
3965
|
-
" (\"cheap-worker.models\", and \"cheap-worker.defaultModel\" if that is what you relied on).",
|
|
3966
|
-
` 2. Register/activate provider ${missingList} on this machine (host profile config,`,
|
|
3967
|
-
" or the settings namespace listed above).",
|
|
3968
|
-
"",
|
|
3969
|
-
" Do not retry unchanged — the same call fails identically."
|
|
3970
|
-
].join("\n");
|
|
3971
|
-
}
|
|
3972
|
-
/**
|
|
3973
|
-
* cheap-worker 规模化使用指引(guide 子命令输出)。
|
|
3974
|
-
*
|
|
3975
|
-
* 🆕 2026-10-14:**双模式段已整段退场**(owner 令「删除后台运行能力」)。本指引只描述现行机制:
|
|
3976
|
-
* 前台委派(单任务)/前台并行扇出(jobs)/白名单与 provider 预检。
|
|
3977
|
-
*/
|
|
3978
|
-
const CHEAP_WORKER_GUIDE = `# cheap-worker — Scale-Up Usage Guide (guide)
|
|
3979
|
-
|
|
3980
|
-
## What it is
|
|
3981
|
-
One delegation primitive: **foreground spawn**. The host starts child agent(s) through its delegation
|
|
3982
|
-
service, awaits them, and returns their final text. There is no loop, no completion code, no progress
|
|
3983
|
-
file, and no unattended/background run.
|
|
3984
|
-
|
|
3985
|
-
| | single | parallel (jobs) |
|
|
3986
|
-
|---|---|---|
|
|
3987
|
-
| Call | cheap-worker(task, [label]) | cheap-worker(jobs=[{task, label?}, ...]) |
|
|
3988
|
-
| Behaviour | one child runs the task once | all children start at once, results keep your order |
|
|
3989
|
-
| Cap | — | **10 (fixed, hardcoded)** |
|
|
3990
|
-
| Model | CCC config (single source) | same |
|
|
3991
|
-
|
|
3992
|
-
## ⚠️ Before using: load eap and design the plan
|
|
3993
|
-
Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up cheap-worker plan" based on the EAP framework.
|
|
3994
|
-
|
|
3995
|
-
### 1. Task decomposition (E↑ Explicit)
|
|
3996
|
-
- Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
|
|
3997
|
-
- Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel via \`jobs\`
|
|
3998
|
-
|
|
3999
|
-
### 2. Prompt design (cheap-worker's task parameter)
|
|
4000
|
-
- task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
|
|
4001
|
-
- the child does NOT share your conversation — restate everything it needs in task
|
|
4002
|
-
- Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
|
|
4003
|
-
output a JSON array (fields id/conclusion/evidence), do not modify the original file"
|
|
4004
|
-
- Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
|
|
4005
|
-
the cheap-worker-internal agent is also required to load eap and organize output per the EAP standard
|
|
4006
|
-
|
|
4007
|
-
### 3. Model (CCC-configured, not selectable per call)
|
|
4008
|
-
- The model is read from .opencode/serenity.json "cheap-worker.models" — **the caller cannot pass one**
|
|
4009
|
-
(there is no "model" parameter). "cheap-worker.defaultModel" names which entry is used; it falls back to models[0].
|
|
4010
|
-
- The CCC is expected to point this whitelist at LOW-COST models: cheap-worker is the bulk-execution channel
|
|
4011
|
-
- A child's own recursive subagents inherit the child's model automatically (DSH native)
|
|
4012
|
-
|
|
4013
|
-
### 3b. 🔴 Provider availability (per-machine — the whitelist is NOT enough)
|
|
4014
|
-
- The whitelist lives in the **CCC** ("serenity.json", it **travels with the repository**), but provider
|
|
4015
|
-
**adapters are registered per machine by the host** (profile config / settings namespace). An entry that
|
|
4016
|
-
works on one machine can fail on another with: "no adapter registered for provider <p>".
|
|
4017
|
-
- Before spawning, cheap-worker verifies the provider of the resolved model against the adapters registered
|
|
4018
|
-
**on this machine**; if it is missing it reports the requested model, where it is configured, **the
|
|
4019
|
-
providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the
|
|
4020
|
-
exact fix — delivered in the result's "diagnostic" field (never an opaque failure, never a created child).
|
|
4021
|
-
- ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "cheap-worker.models"
|
|
4022
|
-
(and "cheap-worker.defaultModel") to a provider listed as available, or activate/register the provider on this
|
|
4023
|
-
machine. **Do not retry the same call unchanged** — it fails identically.
|
|
4024
|
-
|
|
4025
|
-
### 4. Parallel strategy
|
|
4026
|
-
- Independent subtasks: cheap-worker(jobs=[{task,label?},...]) ⇒ fan-out, **cap 10**, results come back
|
|
4027
|
-
**in the order you passed them** (label is only a display name).
|
|
4028
|
-
- For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead.
|
|
4029
|
-
|
|
4030
|
-
## Completion criteria
|
|
4031
|
-
- The child's run settles; done=true only when stopReason="completed" — otherwise read "diagnostic"
|
|
4032
|
-
(and "output" for whatever partial text was produced).
|
|
4033
|
-
`;
|
|
4034
|
-
//#endregion
|
|
4035
|
-
//#region src/trajectory/session.ts
|
|
4036
|
-
/** `provider/model` → agentOptions(由 delegate/core 提供,保持单一实现) */
|
|
4037
|
-
const trajectoryAgents = /* @__PURE__ */ new Map();
|
|
4038
|
-
/** 查 sessionId 的活体 agent(进程内会话延续用;未注册/已清理 → undefined) */
|
|
4039
|
-
function getTrajectoryAgent(sessionId) {
|
|
4040
|
-
return trajectoryAgents.get(sessionId);
|
|
4041
|
-
}
|
|
4042
|
-
/** 该会话当前绑定的 轨迹约束面(不是 轨迹约束 → null)。**守门与提示词的唯一查询入口。** */
|
|
4043
|
-
function trajectoryConstraintFor(root, sessionId) {
|
|
4044
|
-
if (sessionId === void 0) return null;
|
|
4045
|
-
return trajectoryConstraintForSession(root, sessionId, trajectoryDirNameForSession(sessionId));
|
|
4046
|
-
}
|
|
4047
|
-
function registerTrajectorySession(sessionId, trajectoryDirName, ccc, agent) {
|
|
4048
|
-
trajectoryAgents.set(sessionId, agent);
|
|
4049
|
-
registerTrajectorySession$1(sessionId, trajectoryDirName, ccc);
|
|
4050
|
-
}
|
|
4051
|
-
function unregisterTrajectorySession(sessionId) {
|
|
4052
|
-
trajectoryAgents.delete(sessionId);
|
|
4053
|
-
unregisterTrajectorySession$1(sessionId);
|
|
4054
|
-
}
|
|
4055
|
-
/**
|
|
4056
|
-
* 该 轨迹约束会话的**工作台路径** = 它自己轨迹的 SESSION.md。
|
|
4057
|
-
*
|
|
4058
|
-
* 🔴 与旧版的差别:旧版会在这里**自动新建**一个 SESSION(`ensureTrajectorySession`);
|
|
4059
|
-
* 新版**没有新建这个动作** —— 轨迹就是工作台,`trajectory-constraint.json` 与 `SESSION.md` 同处一个目录。
|
|
4060
|
-
* ⇒ 语义变成纯粹的"查询:我自己的身体在哪"。
|
|
4061
|
-
*
|
|
4062
|
-
* @returns SESSION.md 绝对路径;不是 轨迹约束 / 文件不在 → null
|
|
4063
|
-
*/
|
|
4064
|
-
function trajectoryWorkspaceMdPath(root, sessionId) {
|
|
4065
|
-
if (sessionId === void 0) return null;
|
|
4066
|
-
const dirName = trajectoryDirNameForSession(sessionId);
|
|
4067
|
-
if (dirName === null) return null;
|
|
4068
|
-
const md = join(sessionsRoot(root), dirName, "SESSION.md");
|
|
4069
|
-
return existsSync(md) ? md : null;
|
|
4070
|
-
}
|
|
4071
|
-
/**
|
|
4072
|
-
* 系统提示词中的**工作台纪律块**(S142 用户:只给路径 LLM 不会主动用)。
|
|
4073
|
-
*
|
|
4074
|
-
* SESSION = 工作台 —— 轨迹约束与主舱同一套机制(**零特调**,用户拍板):
|
|
4075
|
-
* SESSION.md 是持久记忆载体、rebuild 自动从本 SESSION 续接。
|
|
4076
|
-
* 注入 agent 系统提示词(用户对话面不可见)。
|
|
4077
|
-
*
|
|
4078
|
-
* v1.30.13(S142 诊断 D5):写入纪律**不点名具体工具** —— 旧文案写死 "with write/edit",
|
|
4079
|
-
* 而某个 轨迹约束的工具白名单里根本没有 write/edit ⇒ 指令指向它没有的工具
|
|
4080
|
-
* (注入纪律与能力面矛盾,模型只能猜)。现文案改为「用本轨迹被授权的写入通道」。
|
|
4081
|
-
*/
|
|
4082
|
-
function workspaceCognitiveTrajectoryLine(mdPath) {
|
|
4083
|
-
return [
|
|
4084
|
-
"── Serenity Session Workspace ──",
|
|
4085
|
-
"You ARE a cognitive trajectory: this workspace SESSION is your own persistent body.",
|
|
4086
|
-
` SESSION.md: ${mdPath}`,
|
|
4087
|
-
"",
|
|
4088
|
-
"Rules of the workspace:",
|
|
4089
|
-
" 1. This SESSION.md is your persistent memory carrier (like the main cabin SESSION) — ",
|
|
4090
|
-
" read it first when context feels thin or work spans turns.",
|
|
4091
|
-
" 2. Record key decisions/progress/unresolved items INTO this SESSION.md using the write channel",
|
|
4092
|
-
" your trajectory is actually granted (use whatever write tool or MSM you have — never assume",
|
|
4093
|
-
" a specific write tool exists; if none is granted, report that instead of pretending to write).",
|
|
4094
|
-
" Sections: 身份与边界 / 权威锚点 / 工作纪律 / 决策 / 日志 / 当前状态 / 待 owner 裁决.",
|
|
4095
|
-
" Keep entries at milestone level (one line + evidence pointer); execution detail",
|
|
4096
|
-
" belongs in commit messages. Keep it current — it is what a rebuild resumes from.",
|
|
4097
|
-
" 3. When context pressure is high, run cognitive_trajectory rebuild — it auto-resumes from THIS SESSION.md.",
|
|
4098
|
-
" 4. Never expose this internal path in user-facing replies (chat stays clean).",
|
|
4099
|
-
"── ──"
|
|
4100
|
-
].join("\n");
|
|
4101
|
-
}
|
|
4102
|
-
/** 已挂工作台提示词段的 agent(幂等:同一 agent 只注册一次,避免 duplicate section) */
|
|
4103
|
-
const workspaceSectionAgents = /* @__PURE__ */ new WeakSet();
|
|
4104
|
-
/**
|
|
4105
|
-
* 把**工作台纪律块**挂到 agent 的系统提示词段(单一注入点)。
|
|
4106
|
-
*
|
|
4107
|
-
* 用户原话(S142,2026-09-08):"微信桥的注入机制,每个用户消息都会注入,轨迹约束本身也会注入,
|
|
4108
|
-
* 这样就重复,能否微信桥情况下,注入内容直接取 轨迹约束的,这样不用配两遍"。
|
|
4109
|
-
*
|
|
4110
|
-
* 现形态:工作台行**只**经本函数挂系统提示词段(name `serenity-轨迹约束-workspace`,
|
|
4111
|
-
* 动态 `text()` 每轮按 scope 读当前活跃 mdPath)。
|
|
4112
|
-
*
|
|
4113
|
-
* @returns true = 段已在位;false = 该 agent 挂不上(调用方降级为 question 前缀注入)
|
|
4114
|
-
*/
|
|
4115
|
-
function ensureWorkspacePromptSection(agent, scope, root) {
|
|
4116
|
-
if (!agent || typeof agent !== "object") return false;
|
|
4117
|
-
if (workspaceSectionAgents.has(agent)) return true;
|
|
4118
|
-
const api = agent.ctx?.systemPrompt;
|
|
4119
|
-
if (typeof api?.section !== "function") return false;
|
|
4120
|
-
try {
|
|
4121
|
-
api.section({
|
|
4122
|
-
name: "serenity-轨迹约束-workspace",
|
|
4123
|
-
order: -55,
|
|
4124
|
-
text: () => {
|
|
4125
|
-
const mdPath = trajectoryWorkspaceMdPath(root, scope);
|
|
4126
|
-
return mdPath ? workspaceCognitiveTrajectoryLine(mdPath) : "";
|
|
4127
|
-
}
|
|
4128
|
-
});
|
|
4129
|
-
workspaceSectionAgents.add(agent);
|
|
4130
|
-
return true;
|
|
4131
|
-
} catch (err) {
|
|
4132
|
-
console.warn(`[serenity-hooks] 轨迹约束 工作台提示词段注册失败(回退 question 注入): ${String(err?.message ?? err)}`);
|
|
4133
|
-
return false;
|
|
4134
|
-
}
|
|
4135
|
-
}
|
|
4136
|
-
/** 轨迹约束 agent 挂载的 DSH preset(平台工具面由 preset 决定;白名单再按轨迹过滤) */
|
|
4137
|
-
const TRAJECTORY_PRESET = "standard";
|
|
4138
|
-
/**
|
|
4139
|
-
* 创建(或复用)一个 轨迹约束会话。
|
|
4140
|
-
*
|
|
4141
|
-
* **固定 sessionId 的 resume-or-create**(v1.27.2):指定 sessionId 且磁盘已有持久化 log →
|
|
4142
|
-
* `ctx.agents.resume`(DSH 持久化语义:sessionId 即身份,历史 + turn 续号延续);
|
|
4143
|
-
* 无 log(首次)→ `ctx.agents.create`。不指定 → 随机 id 恒 create(调试页路径)。
|
|
4144
|
-
*
|
|
4145
|
-
* @param profile 该 轨迹约束的约束面(来自 `trajectory-constraint.json`)
|
|
4146
|
-
* @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现"轨迹 ↔ 会话"长期映射)
|
|
4147
|
-
*/
|
|
4148
|
-
async function createTrajectoryAgent(ctx, root, profile, defaultModel, sessionId) {
|
|
4149
|
-
if (!ctx.agents) throw new Error("trajectory constraint: ctx.agents unavailable");
|
|
4150
|
-
const model = profile.model?.trim() || defaultModel || "";
|
|
4151
|
-
const id = sessionId ?? `轨迹约束-${profile.code}-${randomUUID()}`;
|
|
4152
|
-
const setup = async (agentCtx) => {
|
|
4153
|
-
try {
|
|
4154
|
-
await agentCtx.get("agentPresets")?.mount?.(agentCtx, TRAJECTORY_PRESET);
|
|
4155
|
-
} catch {}
|
|
4156
|
-
};
|
|
4157
|
-
const handle = await createOrResumeAgent(ctx, id, root, model, setup, sessionId !== void 0);
|
|
4158
|
-
const agent = handle.agent;
|
|
4159
|
-
let readerPath = profile.systemPromptFile?.trim();
|
|
4160
|
-
let reader = createProfilePromptReader(root, profile);
|
|
4161
|
-
const livePrompt = () => {
|
|
4162
|
-
let live = profile;
|
|
4163
|
-
try {
|
|
4164
|
-
const fresh = readTrajectoryConstraintAt(join(sessionsRoot(root), profile.dirName), root);
|
|
4165
|
-
if (fresh) live = fresh;
|
|
4166
|
-
} catch {}
|
|
4167
|
-
const wantPath = live.systemPromptFile?.trim();
|
|
4168
|
-
if (wantPath !== readerPath) {
|
|
4169
|
-
reader = createProfilePromptReader(root, live);
|
|
4170
|
-
readerPath = wantPath;
|
|
4171
|
-
}
|
|
4172
|
-
return reader();
|
|
4173
|
-
};
|
|
4174
|
-
livePrompt();
|
|
4175
|
-
try {
|
|
4176
|
-
agent.ctx.systemPrompt.section({
|
|
4177
|
-
name: "serenity-轨迹约束",
|
|
4178
|
-
order: -60,
|
|
4179
|
-
text: () => {
|
|
4180
|
-
const legacy = hasTrajectoryPrompt(root, profile.dirName) ? "" : livePrompt();
|
|
4181
|
-
return [buildTrajectoryBasePrompt(profile.code), legacy].filter(Boolean).join("\n");
|
|
4182
|
-
}
|
|
4183
|
-
});
|
|
4184
|
-
} catch (err) {
|
|
4185
|
-
console.warn(`[serenity-hooks] 轨迹约束 系统提示词注册失败: ${String(err?.message ?? err)}`);
|
|
4186
|
-
}
|
|
4187
|
-
if (agent?.session) try {
|
|
4188
|
-
registerCognitiveTrajectorySkillSection(agent, root);
|
|
4189
|
-
} catch (err) {
|
|
4190
|
-
console.warn(`[serenity-hooks] 轨迹约束轨迹 skill 注入注册失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
|
|
4191
|
-
}
|
|
4192
|
-
if (trajectoryWorkspaceMdPath(root, id) !== null) ensureWorkspacePromptSection(agent, id, root);
|
|
4193
|
-
registerTrajectorySession(id, profile.dirName, root, agent);
|
|
4194
|
-
return {
|
|
4195
|
-
handle,
|
|
4196
|
-
agent,
|
|
4197
|
-
sessionId: id,
|
|
4198
|
-
resumed: handle.resumed
|
|
4199
|
-
};
|
|
4200
|
-
}
|
|
4201
|
-
/** 取 live agent(DSH 文档:`ctx.agents.get(id)` 返回 bare Agent;会话 live 时 resume/create 均不可用) */
|
|
4202
|
-
function getLiveAgent(ctx, id) {
|
|
4203
|
-
try {
|
|
4204
|
-
return hostAgents(ctx)?.get?.(id);
|
|
4205
|
-
} catch {
|
|
4206
|
-
return;
|
|
4207
|
-
}
|
|
4208
|
-
}
|
|
4209
|
-
/** live 复用返回(resumed:true 历史延续;dispose 空操作——live agent 不归本模块所有,不能拆) */
|
|
4210
|
-
function liveReuseRef(live, setup) {
|
|
4211
|
-
return setup(live.ctx).catch(() => {}).then(() => ({
|
|
4212
|
-
agent: live,
|
|
4213
|
-
dispose: async () => {},
|
|
4214
|
-
resumed: true
|
|
4215
|
-
}));
|
|
4216
|
-
}
|
|
4217
|
-
/**
|
|
4218
|
-
* resume-or-create 分派:固定 id → 优先 resume(持久化历史延续),失败降级 create;
|
|
4219
|
-
* 随机 id(无固定 sessionId)→ 恒 create。
|
|
4220
|
-
*
|
|
4221
|
-
* **v1.27.3 修复(微信桥"重启后不响应"根因):live 会话优先复用**——重启后 DSH 把
|
|
4222
|
-
* 关闭时仍 live 的持久化会话恢复为 live。此时 resume 报
|
|
4223
|
-
* "cannot prepare session X while it is live"、create 报 "session X already exists"
|
|
4224
|
-
* ⇒ 唯一可行路径 = `ctx.agents.get(id)` 取 live agent 直接续用。
|
|
4225
|
-
*/
|
|
4226
|
-
async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
|
|
4227
|
-
if (!fixedId) return {
|
|
4228
|
-
...await ctx.agents.create({
|
|
4229
|
-
sessionId: id,
|
|
4230
|
-
meta: {
|
|
4231
|
-
cwd: root,
|
|
4232
|
-
agentPreset: TRAJECTORY_PRESET
|
|
4233
|
-
},
|
|
4234
|
-
setup,
|
|
4235
|
-
...model ? { agentOptions: splitModel(model) } : {}
|
|
4236
|
-
}),
|
|
4237
|
-
resumed: false
|
|
4238
|
-
};
|
|
4239
|
-
const liveAgent = getLiveAgent(ctx, id);
|
|
4240
|
-
if (liveAgent) return liveReuseRef(liveAgent, setup);
|
|
4241
|
-
const agentsWithResume = ctx.agents;
|
|
4242
|
-
if (typeof agentsWithResume.resume !== "function") return {
|
|
4243
|
-
...await ctx.agents.create({
|
|
4244
|
-
sessionId: id,
|
|
4245
|
-
meta: {
|
|
4246
|
-
cwd: root,
|
|
4247
|
-
agentPreset: TRAJECTORY_PRESET
|
|
4248
|
-
},
|
|
4249
|
-
setup,
|
|
4250
|
-
...model ? { agentOptions: splitModel(model) } : {}
|
|
4251
|
-
}),
|
|
4252
|
-
resumed: false
|
|
4253
|
-
};
|
|
4254
|
-
try {
|
|
4255
|
-
return {
|
|
4256
|
-
...await agentsWithResume.resume({
|
|
4257
|
-
resumeSessionId: id,
|
|
4258
|
-
setup,
|
|
4259
|
-
...model ? { agentOptions: splitModel(model) } : {}
|
|
4260
|
-
}),
|
|
4261
|
-
resumed: true
|
|
4262
|
-
};
|
|
4263
|
-
} catch (err) {
|
|
4264
|
-
const liveNow = getLiveAgent(ctx, id);
|
|
4265
|
-
if (liveNow) return liveReuseRef(liveNow, setup);
|
|
4266
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4267
|
-
const stack = err instanceof Error ? err.stack ?? "" : "";
|
|
4268
|
-
console.log(`[serenity-hooks] 轨迹约束 resume 失败降级 create (id=${id}): ${msg}`);
|
|
4269
|
-
if (stack) console.log(`[serenity-hooks] 轨迹约束 resume stack:\n${stack.slice(0, 1200)}`);
|
|
4270
|
-
return {
|
|
4271
|
-
...await ctx.agents.create({
|
|
4272
|
-
sessionId: id,
|
|
4273
|
-
meta: {
|
|
4274
|
-
cwd: root,
|
|
4275
|
-
agentPreset: TRAJECTORY_PRESET
|
|
4276
|
-
},
|
|
4277
|
-
setup,
|
|
4278
|
-
...model ? { agentOptions: splitModel(model) } : {}
|
|
4279
|
-
}),
|
|
4280
|
-
resumed: false
|
|
4281
|
-
};
|
|
4282
|
-
}
|
|
4283
|
-
}
|
|
4284
|
-
/** 会话事件读取(兼容 `snapshotEvents()` 与测试替身的 `.events`) */
|
|
4285
|
-
function sessionEvents(session) {
|
|
4286
|
-
const s = session;
|
|
4287
|
-
if (typeof s.snapshotEvents === "function") return s.snapshotEvents();
|
|
4288
|
-
return s.events ?? [];
|
|
4289
|
-
}
|
|
4290
|
-
/** 读会话最后一个 assistant/message 文本 */
|
|
4291
|
-
function lastAssistantText$1(agent) {
|
|
4292
|
-
const events = sessionEvents(agent.session);
|
|
4293
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
4294
|
-
const e = events[i];
|
|
4295
|
-
if (e && e.type === "assistant/message") {
|
|
4296
|
-
const text = (e.data?.message?.content ?? e.data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
|
|
4297
|
-
if (text) return text;
|
|
4298
|
-
}
|
|
4299
|
-
}
|
|
4300
|
-
return "";
|
|
4301
|
-
}
|
|
4302
|
-
/** 本轮等待上限(毫秒):超时即判「本轮无产出」——**绝不**把上一轮的旧答案当本轮答案 */
|
|
4303
|
-
const ASK_TURN_TIMEOUT_MS = 18e4;
|
|
4304
|
-
/** 会话里 `assistant/message` 的条数(判「本轮是否真的产出了回答」) */
|
|
4305
|
-
function countAssistantMessages(session) {
|
|
4306
|
-
let n = 0;
|
|
4307
|
-
for (const e of sessionEvents(session)) if (e?.type === "assistant/message") n += 1;
|
|
4308
|
-
return n;
|
|
4309
|
-
}
|
|
4310
|
-
/**
|
|
4311
|
-
* 等**本轮**产出一条 assistant 消息。
|
|
4312
|
-
*
|
|
4313
|
-
* ## 为什么不能用 `waitAgentIdle` 单独当判据(2026-10-19 复读缺陷)
|
|
4314
|
-
*
|
|
4315
|
-
* `waitAgentIdle` 的首行是 `if (agent.status === 'idle') finish()` —— 而 `followup()`
|
|
4316
|
-
* 是**异步投递**:刚投出去的这一刻 agent 状态**仍是 idle** ⇒ 等待**立刻结算** ⇒
|
|
4317
|
-
* `lastAssistantText()` 读到的是**上一轮的** `assistant/message` ⇒ 桥把它当"本轮答案"转发。
|
|
4318
|
-
* 若 CCC 又开了 `fallbackOnNoSend`,同一条旧答案会被反复重发(实测三次间隔 300~400ms、逐字相同)。
|
|
4319
|
-
*
|
|
4320
|
-
* ## 判据(`idle` 只是"可以开始看"的信号,不是成功信号)
|
|
4321
|
-
*
|
|
4322
|
-
* ① 期望值 = 调用前 `assistant/message` 的**条数**;
|
|
4323
|
-
* ② 成功 ⟺ 该计数**增长**;
|
|
4324
|
-
* ③ 超时或有界轮询耗尽 ⇒ `false`(调用方据此给空答案,兜底不触发)。
|
|
4325
|
-
*/
|
|
4326
|
-
async function waitForTurnAssistant(ctx, agent, beforeAssistantCount, opts = {}) {
|
|
4327
|
-
const timeoutMs = opts.timeoutMs ?? ASK_TURN_TIMEOUT_MS;
|
|
4328
|
-
const grew = () => countAssistantMessages(agent.session) > beforeAssistantCount;
|
|
4329
|
-
if (await Promise.race([waitAgentIdle(ctx, agent).then(() => true), new Promise((resolve) => {
|
|
4330
|
-
setTimeout(() => resolve(false), timeoutMs);
|
|
4331
|
-
})])) return grew();
|
|
4332
|
-
return grew();
|
|
4333
|
-
}
|
|
4334
|
-
/**
|
|
4335
|
-
* 提问一轮:followup → 等**本轮**回答 → 读答案。
|
|
4336
|
-
*
|
|
4337
|
-
* @param options.timeoutMs 本轮等待上限(测试注入;生产用缺省)
|
|
4338
|
-
*/
|
|
4339
|
-
async function askTrajectory(ctx, agent, question, options) {
|
|
4340
|
-
const beforeAssistant = countAssistantMessages(agent.session);
|
|
4341
|
-
agent.followup(createUserMessage({
|
|
4342
|
-
content: [{
|
|
4343
|
-
type: "text",
|
|
4344
|
-
text: question
|
|
4345
|
-
}],
|
|
4346
|
-
source: PLUGIN_SOURCE
|
|
4347
|
-
}));
|
|
4348
|
-
return {
|
|
4349
|
-
answer: await waitForTurnAssistant(ctx, agent, beforeAssistant, { timeoutMs: options?.timeoutMs }) ? lastAssistantText$1(agent) : "",
|
|
4350
|
-
sessionId: String(agent.session.id ?? "")
|
|
4351
|
-
};
|
|
4352
|
-
}
|
|
4353
|
-
/**
|
|
4354
|
-
* 轨迹约束会话的轨迹纪律参与判定:**不是 轨迹约束恒 true**(正常参与);
|
|
4355
|
-
* 轨迹约束 按 `trajectory-constraint.json` 的 `cognitiveTrajectory` 子集决定。
|
|
4356
|
-
*
|
|
4357
|
-
* 注册表缺失(进程刚重启、绑定还没读)⇒ 由 `trajectoryConstraintForSession` 回落持久绑定;
|
|
4358
|
-
* 两者都不中 ⇒ 判"不是 轨迹约束" ⇒ 恒 true(正常参与)。**fail-open 是有意的**:
|
|
4359
|
-
* 纪律参与项的失败不该让会话失去 keeper/rebuild(那是能力倒退,比多跑一次更糟)。
|
|
4360
|
-
*/
|
|
4361
|
-
function trajectoryConstraintParticipates(root, sessionId, key) {
|
|
4362
|
-
if (sessionId === void 0) return true;
|
|
4363
|
-
const profile = trajectoryConstraintFor(root, sessionId);
|
|
4364
|
-
if (profile === null) return true;
|
|
4365
|
-
return constraintCognitiveTrajectory(profile)[key];
|
|
4366
|
-
}
|
|
4367
|
-
/**
|
|
4368
|
-
* `msm` 工具的 轨迹约束门控:非 轨迹约束恒放行;轨迹约束会话——
|
|
4369
|
-
* exec 非白名单 MSM 拒绝(**不列名单**)、register/deregister 必拒、
|
|
4370
|
-
* list 白名单过滤、check/guide/ccc-config 只读放行。
|
|
4371
|
-
*/
|
|
4372
|
-
function trajectoryConstraintMsmGate(root, sessionId, action, name) {
|
|
4373
|
-
if (sessionId === void 0) return {};
|
|
4374
|
-
const profile = trajectoryConstraintFor(root, sessionId);
|
|
4375
|
-
if (profile === null) return {};
|
|
4376
|
-
if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in trajectory-constraint sessions" };
|
|
4377
|
-
if (action === "exec") {
|
|
4378
|
-
if (!name || !profile.msms.includes(name)) return { reject: "MSM not allowed" };
|
|
4379
|
-
return {};
|
|
4380
|
-
}
|
|
4381
|
-
if (action === "list") return { whitelist: profileMsmWhitelist(profile) };
|
|
4382
|
-
return {};
|
|
4383
|
-
}
|
|
4384
|
-
//#endregion
|
|
4385
1902
|
//#region src/tools/msm/index.ts
|
|
4386
1903
|
/**
|
|
4387
1904
|
* tools/msm/index.ts — msm 工具(v1.30 重构:单入口执行,list/exec 合一)
|
|
@@ -12443,6 +9960,107 @@ ${renderCroSampleSnapshot()}
|
|
|
12443
9960
|
**吃一份快照,吐一行决策。先自测,绿了再改名。**
|
|
12444
9961
|
`;
|
|
12445
9962
|
//#endregion
|
|
9963
|
+
//#region src/trajectory/resident.ts
|
|
9964
|
+
/**
|
|
9965
|
+
* trajectory/resident.ts — **常驻名单**(D173 的兑现口,`cognitive_trajectory resident` 的实现)
|
|
9966
|
+
*
|
|
9967
|
+
* ## 这条能力的判据(owner 2026-10-20 原话,逐字)
|
|
9968
|
+
*
|
|
9969
|
+
* > 「常驻的意思就是**自己声明为自己欢迎别人来麻烦自己**,和永不休眠没啥关系,
|
|
9970
|
+
* > **trajectory never ends 悖论的一种实现**」
|
|
9971
|
+
*
|
|
9972
|
+
* ⇒ **常驻不是资源状态**。它不是"永不休眠"、不是"常驻内存"、不是"不许销毁我的会话"。
|
|
9973
|
+
* 一条轨迹**永远存在**(没有生命周期),常驻与否都不改变这一点。
|
|
9974
|
+
*
|
|
9975
|
+
* ⇒ **它管的是「被找到」,不是「不被销毁」。**
|
|
9976
|
+
* 两件事混为一谈,就会去动调度器的载体生命周期 —— 那正是本轨反复踩的"名字撒谎"。
|
|
9977
|
+
*
|
|
9978
|
+
* ## 因此本模块的三条硬边界
|
|
9979
|
+
*
|
|
9980
|
+
* 1. **纯读**:不写任何文件、不碰注册表、不碰 `wake/scheduler.ts`。
|
|
9981
|
+
* 2. **不进系统提示词**(owner 裁决 Q2:说明类信息按需取,不占每轮 token)。
|
|
9982
|
+
* 3. **只报写了 `serves` 的轨迹** —— 只写半个(有 `resident` 没 `serves`)等于
|
|
9983
|
+
* "把自己挂上去却不说能干什么",按手册的判据那比不写更坏,所以**不列出来**,
|
|
9984
|
+
* 但要**单独数出**来,让写的人知道自己的写法没生效。
|
|
9985
|
+
*
|
|
9986
|
+
* ## 归属(D23)
|
|
9987
|
+
* 机制在 ACC(怎么找、按什么顺序、列什么字段);内容在 CCC(`serves` 那句话写什么)。
|
|
9988
|
+
*/
|
|
9989
|
+
/**
|
|
9990
|
+
* 列出全部**可被找到**的轨迹(按编码排序,确定性)。
|
|
9991
|
+
*
|
|
9992
|
+
* ⚠️ 读盘失败 / 坏 JSON 一律**跳过该条**(不抛):一份坏的声明不该让整张名单拿不到。
|
|
9993
|
+
*
|
|
9994
|
+
* @param root CCC 根
|
|
9995
|
+
*/
|
|
9996
|
+
function collectResident(root) {
|
|
9997
|
+
const entries = [];
|
|
9998
|
+
const incomplete = [];
|
|
9999
|
+
let names = [];
|
|
10000
|
+
try {
|
|
10001
|
+
names = readdirSync(sessionsRoot(root), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name);
|
|
10002
|
+
} catch {
|
|
10003
|
+
return {
|
|
10004
|
+
entries,
|
|
10005
|
+
incomplete
|
|
10006
|
+
};
|
|
10007
|
+
}
|
|
10008
|
+
for (const dirName of names) {
|
|
10009
|
+
let profile;
|
|
10010
|
+
try {
|
|
10011
|
+
profile = readTrajectoryConstraintAt(join(sessionsRoot(root), dirName), root);
|
|
10012
|
+
} catch {
|
|
10013
|
+
continue;
|
|
10014
|
+
}
|
|
10015
|
+
if (!profile || profile.resident === void 0) continue;
|
|
10016
|
+
const serves = profile.resident.serves;
|
|
10017
|
+
if (serves === void 0 || serves === "") {
|
|
10018
|
+
incomplete.push(profile.code);
|
|
10019
|
+
continue;
|
|
10020
|
+
}
|
|
10021
|
+
entries.push({
|
|
10022
|
+
code: profile.code,
|
|
10023
|
+
dirName: profile.dirName,
|
|
10024
|
+
serves,
|
|
10025
|
+
...profile.model !== void 0 ? { model: profile.model } : {}
|
|
10026
|
+
});
|
|
10027
|
+
}
|
|
10028
|
+
entries.sort((a, b) => a.code < b.code ? -1 : a.code > b.code ? 1 : 0);
|
|
10029
|
+
incomplete.sort();
|
|
10030
|
+
return {
|
|
10031
|
+
entries,
|
|
10032
|
+
incomplete
|
|
10033
|
+
};
|
|
10034
|
+
}
|
|
10035
|
+
/**
|
|
10036
|
+
* 渲染成给模型读的文本(`cognitive_trajectory resident` 的返回值)。
|
|
10037
|
+
*
|
|
10038
|
+
* 🔴 **空名单也是有效结果**,且必须说清"这意味着什么" —— 否则读者会以为工具坏了。
|
|
10039
|
+
*/
|
|
10040
|
+
function listResident(root) {
|
|
10041
|
+
const { entries, incomplete } = collectResident(root);
|
|
10042
|
+
const lines = ["═══ 常驻轨迹(自己声明「欢迎别人来麻烦我」)═══", ""];
|
|
10043
|
+
if (entries.length === 0) {
|
|
10044
|
+
lines.push(" (本 CCC 暂无轨迹声明常驻)");
|
|
10045
|
+
lines.push(" ⇒ 这不是错误:常驻是**自愿的自我声明**,没写就是\"不参与被找\"。");
|
|
10046
|
+
} else for (const e of entries) {
|
|
10047
|
+
const model = e.model !== void 0 ? ` [model: ${e.model}]` : "";
|
|
10048
|
+
lines.push(` · ${e.code}${model}`);
|
|
10049
|
+
lines.push(` ${e.serves}`);
|
|
10050
|
+
}
|
|
10051
|
+
if (incomplete.length > 0) {
|
|
10052
|
+
lines.push("");
|
|
10053
|
+
lines.push(` ⚠️ 写了 resident 但**没写 serves**(不会被列出):${incomplete.join("、")}`);
|
|
10054
|
+
lines.push(" 只写半个 = 把自己挂上去却不说能干什么 —— 别人会来问,然后发现问错了人。");
|
|
10055
|
+
}
|
|
10056
|
+
lines.push("");
|
|
10057
|
+
lines.push("── 怎么让自己出现在这里 ──");
|
|
10058
|
+
lines.push(" 在你这条轨迹目录的 trajectory.json 里写:");
|
|
10059
|
+
lines.push(" { \"resident\": { \"serves\": \"一句话说清我擅长什么、能帮上什么忙\" } }");
|
|
10060
|
+
lines.push(" ⚠️ 只写这一个字段**不影响任何默认行为**(D183);其余 key 全部可选。");
|
|
10061
|
+
return lines.join("\n");
|
|
10062
|
+
}
|
|
10063
|
+
//#endregion
|
|
12446
10064
|
//#region src/tools/cognitive-trajectory.ts
|
|
12447
10065
|
/**
|
|
12448
10066
|
* trajectory.ts — `cognitive_trajectory` 真实 DSH 工具定义(defineTool)
|
|
@@ -12588,50 +10206,41 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12588
10206
|
return defineTool({
|
|
12589
10207
|
name: "cognitive_trajectory",
|
|
12590
10208
|
description: [
|
|
12591
|
-
"
|
|
12592
|
-
"
|
|
12593
|
-
"
|
|
12594
|
-
"
|
|
12595
|
-
"
|
|
12596
|
-
"
|
|
12597
|
-
"
|
|
12598
|
-
"
|
|
12599
|
-
"
|
|
10209
|
+
"A COGNITIVE TRAJECTORY is a durable body (SESSION.md) that outlives every conversation carrying it.",
|
|
10210
|
+
"AXIOM 1 — THE TRAJECTORY IS THE SUBJECT; THE CONVERSATION IS ITS CARRIER. Rebuilding the carrier replaces the conversation and leaves SESSION.md byte-identical.",
|
|
10211
|
+
"AXIOM 2 — A TRAJECTORY HAS NO LIFECYCLE. It never completes, closes, or expires, and age never counts against it. The only thing that changes is WHERE it is stored: archive moves it out of the active list without touching its directory.",
|
|
10212
|
+
"AXIOM 3 — IDENTITY IS THE CODE: the fixed-position segment of the directory name `YYYY-MM-DD--<code>[--<desc>]`. The code (not the summary, not the title) is the unique identifier, and create --id lets you choose it.",
|
|
10213
|
+
"DOES NOT: this tool never deletes a trajectory, never edits SESSION.md content, and never schedules recurring work (that is cro-guide).",
|
|
10214
|
+
"ENTITIES. TRAJECTORY = a directory under AGENT_SESSIONS/ holding SESSION.md. CARRIER = a dsh session bound to it. BINDING DECLARATION = the file `<trajectory>/current.dsh_session_bind` — ONE FILE PER TRAJECTORY, a single slot, never a table.",
|
|
10215
|
+
"IMMUTABLE 1 — ONE SESSION BELONGS TO AT MOST ONE TRAJECTORY, FOR LIFE. use refuses to move a session that already carries a different trajectory. There is NO --force override (retired 2026-10-20; passing it returns a refusal explaining why). To give another trajectory a carrier, open a new session.",
|
|
10216
|
+
"IMMUTABLE 2 — ONE TRAJECTORY HAS AT MOST ONE CURRENT CARRIER, and THE BINDING DECLARATION IS THE ONLY TRUTH. If several dsh sessions happen to be open, only the one named in the declaration carries the trajectory; the others are simply unbound. Never infer a binding from \"a session looks active\".",
|
|
10217
|
+
"IMMUTABLE 3 — A DECLARATION WHOSE SESSION NO LONGER EXISTS COUNTS AS UNBOUND. No cleanup is ever required, and a stale declaration is deliberately left in place: it records who carried this trajectory last.",
|
|
10218
|
+
"ACTIONS, GROUPED BY EFFECT — read the grouping before choosing, because it answers \"does this change what I am bound to?\":",
|
|
12600
10219
|
"(A) LIFECYCLE — changes which cognitive trajectory THIS conversation is bound to:",
|
|
12601
|
-
"list (inventory + stats
|
|
12602
|
-
"
|
|
12603
|
-
"
|
|
12604
|
-
"archive (move a trajectory into the archived area so it leaves the active list; one-way, never overwrites, and its content is left byte-identical).",
|
|
12605
|
-
"(B) DELIVERY — sends ONE message to ANOTHER cognitive trajectory; your own binding is unchanged:",
|
|
12606
|
-
"send-now (deliver RIGHT NOW) / send-later (deliver at ONE future instant).",
|
|
12607
|
-
"(C) REFERENCE — cro-guide (how to give THIS cognitive trajectory its own program that decides when it should be woken).",
|
|
10220
|
+
"list (inventory + stats) / show (read one SESSION.md) / create (make a NEW trajectory) / use (bind THIS conversation) / rebuild (clear-and-rebuild THIS conversation in place) / archive (move a trajectory out of the active list; one-way, never overwrites).",
|
|
10221
|
+
"(B) DELIVERY — sends ONE message to ANOTHER trajectory; your own binding is unchanged: send-now (right now) / send-later (one future instant).",
|
|
10222
|
+
"(C) REFERENCE — cro-guide: how to give a trajectory its own program that decides when it should be woken.",
|
|
12608
10223
|
"HOW TO CHOOSE (a rule, not a guess):",
|
|
12609
|
-
"not yet bound + starting new work ⇒ create
|
|
12610
|
-
"already bound + recording a
|
|
12611
|
-
"
|
|
12612
|
-
"want
|
|
12613
|
-
"
|
|
12614
|
-
"
|
|
12615
|
-
"RECEIPTS (read no more than is written): send-now returns a synchronous receipt meaning exactly \"injected / turn started\" —",
|
|
12616
|
-
"NEVER \"the target ran or replied\"; to prove the target actually moved, read its SESSION.md.",
|
|
12617
|
-
"It never queues behind the current turn: a busy target is steered mid-turn (read in the CURRENT turn), an idle target starts a turn at once.",
|
|
10224
|
+
"not yet bound + starting new work ⇒ create (with no prior binding it activates immediately, so the new trajectory has a carrier at once).",
|
|
10225
|
+
"already bound + recording a SEPARATE future line ⇒ create (it does NOT steal your binding; activate the new one later by send-now).",
|
|
10226
|
+
"want THIS conversation to move ⇒ use. It refuses if this session already carries another trajectory (IMMUTABLE 1) — that refusal is the design, not an error to work around.",
|
|
10227
|
+
"want to make another trajectory act ⇒ send-now. The target needs no prior setup: if it has no live session, one is created and bound automatically, and your message is injected.",
|
|
10228
|
+
"RECEIPTS (read no more than is written): send-now returns a synchronous receipt meaning exactly \"injected / turn started\" — NEVER \"the target ran or replied\". To prove the target actually moved, read its SESSION.md.",
|
|
10229
|
+
"send-now never queues behind the current turn: a busy target is steered mid-turn (read in the CURRENT turn), an idle target starts a turn at once.",
|
|
12618
10230
|
"send-later is fire-and-forget by design: no receipt, no recall, no panel.",
|
|
12619
|
-
"ASYMMETRY (deliberate
|
|
12620
|
-
"
|
|
12621
|
-
"
|
|
12622
|
-
"
|
|
12623
|
-
"
|
|
12624
|
-
"
|
|
12625
|
-
"
|
|
12626
|
-
"send-now
|
|
12627
|
-
"
|
|
12628
|
-
"
|
|
12629
|
-
"
|
|
12630
|
-
"
|
|
12631
|
-
"
|
|
12632
|
-
"⚠️ This is a behavioral rule, not a mechanical gate: nothing stops a careless call. Treat it as etiquette with a cost.",
|
|
12633
|
-
"uid: READS a legacy TRAJECTORY-UID only — setting one is retired (codes replaced it). Use create --id instead.",
|
|
12634
|
-
"The summary is appended to the dsh session title (S###-YYYY-MM-DD-<summary>); the code and date stay server-derived unless you pass --id."
|
|
10231
|
+
"ASYMMETRY (deliberate): send-now may create a missing carrier; send-later NEVER does. Scheduling for a trajectory that never existed is almost always a mistyped name, and auto-creating would hide that error.",
|
|
10232
|
+
"CARRIER GUARD: send-now auto-creates a carrier ONLY for a trajectory that has never had one. If a declaration exists but its session is gone, it refuses and says so — one trajectory keeps one carrier. Two independent gates can refuse: (a) the declared session cannot be resolved at all; (b) it existed and is now gone.",
|
|
10233
|
+
"PARAMETERS: create needs --desc <desc> [--id <code>] [--goal], OR --issue <ticket> (exactly one), PLUS --summary (≤20 chars).",
|
|
10234
|
+
"use needs --name + --summary (≤20 chars). --force is ACCEPTED BUT REFUSED (retired): it exists only to explain why.",
|
|
10235
|
+
"rebuild needs --summary (next-phase, ≤20 chars) + optional --note (≤200 chars, task focus for the rebuilt self).",
|
|
10236
|
+
"send-now needs target + message. send-later needs target + at (RFC3339 with timezone, or +30m / +2h) + message.",
|
|
10237
|
+
"target/name accept a code (S### or custom), a full AGENT_SESSIONS directory name, or a fuzzy keyword.",
|
|
10238
|
+
"AUTHORIZATION (read before creating anything): create and send-now both cost real work and must serve a human intent. Call create only when the user asked for a new line of work (or confirmed it) — never to \"tidy up\" or pre-provision what nobody requested. When in doubt, ask instead of creating. This is a behavioral rule, not a mechanical gate: treat it as etiquette with a cost.",
|
|
10239
|
+
"uid: READS a legacy TRAJECTORY-UID only. Setting one is retired — a trajectory's code is its identity now; use create --id <code>.",
|
|
10240
|
+
"summary is appended to the dsh session title as <code>-<date>-<summary>; the code and date stay server-derived unless you pass --id.",
|
|
10241
|
+
"SELF-DECLARATION MANUAL — how to write your own charter (PROMPT.md), declare yourself resident",
|
|
10242
|
+
"(trajectory.json `resident.serves`), or restrict yourself (tool/model whitelists):",
|
|
10243
|
+
"container_admin config docs trajectory-self. Read it before changing any file in your own directory."
|
|
12635
10244
|
].join(" "),
|
|
12636
10245
|
parameters: {
|
|
12637
10246
|
action: {
|
|
@@ -12639,17 +10248,18 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12639
10248
|
enum: [...COGNITIVE_TRAJECTORY_ACTIONS],
|
|
12640
10249
|
required: true,
|
|
12641
10250
|
description: [
|
|
12642
|
-
"Subcommand — grouped by effect.",
|
|
10251
|
+
"Subcommand — grouped by effect (see Tier 3 of the description).",
|
|
12643
10252
|
"LIFECYCLE (changes THIS conversation's binding):",
|
|
12644
|
-
"list (inventory + stats
|
|
12645
|
-
"create (new
|
|
12646
|
-
"use (activate context for THIS conversation;
|
|
10253
|
+
"list (inventory + stats) / show (a code, dir name, or fuzzy keyword) /",
|
|
10254
|
+
"create (new trajectory; --id sets its code. Activates itself when you have no binding, otherwise leaves your binding untouched and tells you how to activate it) /",
|
|
10255
|
+
"use (activate context for THIS conversation; refuses if this session already carries another trajectory — no override) /",
|
|
12647
10256
|
"rebuild (clear-and-rebuild current conversation — requires --summary + optional --note) /",
|
|
12648
10257
|
"archive (move a trajectory to the archived area; one-way, refuses to overwrite, content untouched — --dryRun previews).",
|
|
12649
10258
|
"DELIVERY (sends one message to ANOTHER cognitive trajectory; your binding is unchanged):",
|
|
12650
10259
|
"send-now (deliver RIGHT NOW; the target needs no prior setup — if it has no live session, one is created and bound automatically) /",
|
|
12651
10260
|
"send-later (one future instant + one message; fire-and-forget, and it does NOT auto-create a target).",
|
|
12652
|
-
"REFERENCE: cro-guide (no parameters: prints the CRO programming guide, including a ready-to-use sample snapshot)"
|
|
10261
|
+
"REFERENCE: cro-guide (no parameters: prints the CRO programming guide, including a ready-to-use sample snapshot) /",
|
|
10262
|
+
"resident (READ-ONLY: the roster of trajectories that declared themselves RESIDENT — \"I am here, this is what I am good at, come bother me\". It lists each code with its own `serves` sentence, plus any code that wrote `resident` without `serves` (half a declaration is worse than none: people ask, then find they asked the wrong one). ⚠️ Residency is NOT a resource state — it does not mean \"never sleeps\" or \"never destroyed\"; a trajectory permanently exists either way. It governs BEING FOUND, not being kept alive, and it never touches the scheduler)."
|
|
12653
10263
|
].join(" ")
|
|
12654
10264
|
},
|
|
12655
10265
|
name: {
|
|
@@ -12686,7 +10296,7 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12686
10296
|
},
|
|
12687
10297
|
force: {
|
|
12688
10298
|
type: "boolean",
|
|
12689
|
-
description: "use:
|
|
10299
|
+
description: "use: RETIRED (2026-10-20) — one session belongs to one cognitive trajectory for life, so switching is refused unconditionally. The flag is still ACCEPTED so that an old call gets an explanation instead of an \"unknown argument\" error; it never overrides the guard."
|
|
12690
10300
|
},
|
|
12691
10301
|
dryRun: {
|
|
12692
10302
|
type: "boolean",
|
|
@@ -12702,7 +10312,7 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12702
10312
|
},
|
|
12703
10313
|
message: {
|
|
12704
10314
|
type: "string",
|
|
12705
|
-
description: "send-later/send-now: the single message delivered to the target cognitive trajectory. send-now may create and bind a carrier when the target has
|
|
10315
|
+
description: "send-later/send-now: the single message delivered to the target cognitive trajectory. send-now may create and bind a carrier ONLY when the target has never had one — acceptable only for a delivery the user actually wants made (user-intent-driven), never for speculatively starting work."
|
|
12706
10316
|
}
|
|
12707
10317
|
},
|
|
12708
10318
|
output: {
|
|
@@ -12869,6 +10479,21 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12869
10479
|
};
|
|
12870
10480
|
}
|
|
12871
10481
|
case "cro-guide": return CRO_GUIDE;
|
|
10482
|
+
case "resident":
|
|
10483
|
+
/**
|
|
10484
|
+
* 常驻名单(D173 兑现,owner 2026-10-20)。
|
|
10485
|
+
*
|
|
10486
|
+
* 🔴 **这个动作的存在本身就是一条判据**:v1.77.1 的手册曾承诺"通过本工具的
|
|
10487
|
+
* 常驻名单动作按需查询",而**那个动作当时并不存在**(D179(a),S196 踩到)。
|
|
10488
|
+
* ⇒ 现在它真的存在了。
|
|
10489
|
+
*
|
|
10490
|
+
* 🔴 **纯读**(不碰 fs 写、不碰注册表、不碰调度器):D173 的判据是
|
|
10491
|
+
* 「常驻」管的是**被找到**,不是**不被销毁** —— 两件事混为一谈就会去动
|
|
10492
|
+
* `wake/scheduler.ts` 的载体生命周期,那正是本轨反复踩的"名字撒谎"。
|
|
10493
|
+
*
|
|
10494
|
+
* 🔴 **不进系统提示词**(owner 裁决 Q2:说明类信息按需取,不占每轮 token)。
|
|
10495
|
+
*/
|
|
10496
|
+
return listResident(root);
|
|
12872
10497
|
default: throw new Error(`Unknown action: ${args.action}`);
|
|
12873
10498
|
}
|
|
12874
10499
|
}
|
|
@@ -12967,46 +10592,69 @@ const localstoreTool = defineTool({
|
|
|
12967
10592
|
});
|
|
12968
10593
|
//#endregion
|
|
12969
10594
|
//#region src/tools/trajectory-constraint-admin.ts
|
|
12970
|
-
/**
|
|
12971
|
-
|
|
10595
|
+
/**
|
|
10596
|
+
* 轨迹约束 定义教程:**只讲「限制面」**(约束面 / 双白名单 / 纪律参与项 / 路由)。
|
|
10597
|
+
*
|
|
10598
|
+
* 🔴 2026-10-20 拆分(owner 裁 Q2):本章原先同时承载「描述自己」与「限制自己」。
|
|
10599
|
+
* 两者读者与前提不同 —— **描述自己是所有轨迹的常态,限制自己才是特例** ——
|
|
10600
|
+
* 故「章程 / 常驻 / 三个载体」已移入 `config docs trajectory-self`。
|
|
10601
|
+
* ⛔ **本文件不再重复那份内容**(一个内容一个正文入口,v1.75.0 指针纪律)。
|
|
10602
|
+
*/
|
|
10603
|
+
const TRAJECTORY_CONSTRAINT_GUIDE = `═══ 轨迹约束 Guide(限制面;2026-10-20 split)═══
|
|
12972
10604
|
|
|
12973
10605
|
**A 轨迹约束 IS a cognitive trajectory — the one whose directory contains \`${TRAJECTORY_CONSTRAINT_FILENAME}\`.**
|
|
12974
10606
|
That is the whole rule. No role table, no name registry: put the file there and the trajectory
|
|
12975
10607
|
becomes a 轨迹约束; delete it and it goes back to being an ordinary trajectory (takes effect at once).
|
|
12976
10608
|
|
|
10609
|
+
⚠️ **本章只讲"限制自己"。** 想写章程(PROMPT.md)或声明常驻(resident)?
|
|
10610
|
+
→ \`container_admin config docs trajectory-self\`(那是**所有轨迹**都用得上的自述手册)。
|
|
10611
|
+
|
|
12977
10612
|
AGENT_SESSIONS/<YYYY-MM-DD--<code>--<desc>>/
|
|
12978
10613
|
├─ SESSION.md ← its persistent body (this IS the workspace — nothing is auto-created)
|
|
12979
|
-
├─ PROMPT.md ← its charter (trajectory-
|
|
10614
|
+
├─ PROMPT.md ← its charter (see trajectory-self; NOT copied here)
|
|
12980
10615
|
├─ ${TRAJECTORY_CONSTRAINT_FILENAME} ← its declaration (this file is what makes it a 轨迹约束)
|
|
12981
10616
|
└─ continuous-re-occurrence.ts ← optional CRO program (spawned, never imported)
|
|
12982
10617
|
|
|
12983
10618
|
The older names ${TRAJECTORY_LEGACY_FILENAMES.map((n) => `\`${n}\``).join(" / ")} are still READ
|
|
12984
10619
|
(the family is checked in that order) — writing goes to \`${TRAJECTORY_FILENAME}\` only.
|
|
12985
10620
|
|
|
12986
|
-
── ${TRAJECTORY_CONSTRAINT_FILENAME} schema
|
|
10621
|
+
── ${TRAJECTORY_CONSTRAINT_FILENAME} schema(限制面部分)──
|
|
12987
10622
|
{
|
|
12988
|
-
"model": "provider/model", // optional;
|
|
12989
|
-
"tools": ["read","grep","msm",...], //
|
|
12990
|
-
"msms": ["msm-a","msm-b"], //
|
|
12991
|
-
"cognitiveTrajectory": { "session": true, "keeper": true, "rebuild": true }, //
|
|
12992
|
-
"
|
|
12993
|
-
"systemPrompt": "...", // inline fallback; systemPromptFile wins when both are set
|
|
12994
|
-
"uid": "my-handle", // optional legacy alias (read-only; the code is the identity now)
|
|
10623
|
+
"model": "provider/model", // optional; **不写 = 用会话当前模型**(不回退 cheap-worker;每轮生效)
|
|
10624
|
+
"tools": ["read","grep","msm",...], // optional; **不写 = 不收窄**。显式 [] = 全隐藏(除 skill)
|
|
10625
|
+
"msms": ["msm-a","msm-b"], // optional; **不写 = 不收窄**。register/deregister 恒拒
|
|
10626
|
+
"cognitiveTrajectory": { "session": true, "keeper": true, "rebuild": true }, // 缺省 TRUE(见下)
|
|
10627
|
+
"resident": { "serves": "..." }, // optional; 对外用途自述(见 trajectory-self 章)
|
|
12995
10628
|
"cro": { "host": "<machine name>" } // optional: which machine may run this trajectory's CRO program
|
|
12996
10629
|
}
|
|
12997
10630
|
|
|
12998
|
-
|
|
12999
|
-
|
|
10631
|
+
🔴 **所有 key 都是可选的:不写 = 不设定 = 不影响默认行为**(owner 2026-10-20 裁决)。
|
|
10632
|
+
⛔ **"没写" ≠ "写了空数组"**:\`"tools": []\` 是"我一个都不给"(除 \`skill\` 外全隐藏);
|
|
10633
|
+
**不写 \`tools\`** 是"不收窄"。这两者在旧版被混成同一个意思,害人把自己锁死过。
|
|
10634
|
+
|
|
10635
|
+
⚠️ 提示词**不在这里**(新形态):写 \`PROMPT.md\` 即可,**所有轨迹**都适用。
|
|
10636
|
+
旧字段 \`systemPromptFile\` / \`systemPrompt\` 属**兼容期**形态,仍在读、但不再推荐;
|
|
10637
|
+
两者同时存在时 \`PROMPT.md\` 让位规则见 \`trajectory/prompt.ts\`(此处不重复)。
|
|
10638
|
+
|
|
10639
|
+
⚠️ **只想声明常驻、不想限制自己**:只写 \`"resident"\` 字段,**tools/msms 一个都不要写**。
|
|
10640
|
+
⇒ 不收窄任何工具面(见下方"两个默认")。⛔ **不要**为了"空着"而写 \`"tools": []\` —— 那是全隐藏。
|
|
10641
|
+
|
|
10642
|
+
── Two whitelists: UNSET means unrestricted, EMPTY means nothing ──
|
|
10643
|
+
- **字段缺省(没写)** = **不收窄**(这条轨迹只放别的设定,比如只写 resident)
|
|
10644
|
+
- \`"tools": []\`(显式空数组)= **全隐藏**,除恒可用的 \`skill\`
|
|
10645
|
+
- \`tools\` + non-empty \`msms\` = pure-MSM trajectory (the usual knowledge-QA shape)
|
|
13000
10646
|
- non-empty \`msms\` ⇒ the \`msm\` tool is added automatically (the only entry point for MSMs)
|
|
13001
|
-
- anything outside
|
|
10647
|
+
- anything outside a **declared** whitelist is invisible in the schema, not merely rejected at call time
|
|
10648
|
+
🔴 判据是**字段在不在**,不是**数组空不空**(D181)。旧版把两者混成"空 = 全拒",
|
|
10649
|
+
于是「只想声明常驻」的人被静默锁死 —— 那是本仓实测踩过的坑(D179(b))。
|
|
13002
10650
|
|
|
13003
10651
|
── cognitiveTrajectory: why the defaults are TRUE ──
|
|
13004
10652
|
The workspace of a 轨迹约束 is its own SESSION.md. Setting these to false would cut it off from
|
|
13005
10653
|
its own body, so \`session\`/\`keeper\`/\`rebuild\` all default to true. Set false explicitly only
|
|
13006
10654
|
when you deliberately want that participation off.
|
|
13007
10655
|
|
|
13008
|
-
──
|
|
13009
|
-
\`
|
|
10656
|
+
── IM routing: user → trajectory code(微信 / 飞书同一张表)──
|
|
10657
|
+
\`im.routes\`: \`[{ "user": "<channel user id or *>", "trajectory": "<code>" }]\`
|
|
13010
10658
|
A route pointing at a code with no such trajectory is a hard error (the message is NOT delivered).
|
|
13011
10659
|
A trajectory WITHOUT \`${TRAJECTORY_CONSTRAINT_FILENAME}\` is still a legal target — it is just an ordinary trajectory.
|
|
13012
10660
|
|
|
@@ -13014,7 +10662,9 @@ becomes a 轨迹约束; delete it and it goes back to being an ordinary trajecto
|
|
|
13014
10662
|
Everything is read live: saving \`${TRAJECTORY_CONSTRAINT_FILENAME}\` takes effect on the next turn, no restart.
|
|
13015
10663
|
(The prompt FILE hot-reloads by mtime; the JSON itself is re-read every turn.)
|
|
13016
10664
|
After editing, run container_admin config apply for an explicit validation + confirmation pass.
|
|
13017
|
-
Skill loading is always available to 轨迹约束 sessions (skills are not whitelisted — they are read-only knowledge)
|
|
10665
|
+
Skill loading is always available to 轨迹约束 sessions (skills are not whitelisted — they are read-only knowledge).
|
|
10666
|
+
|
|
10667
|
+
写完之后怎么验 → container_admin config docs trajectory-self 的「写完之后怎么验」段(同一套判据,不复制)。`;
|
|
13018
10668
|
/** 校验当前 CCC 的 轨迹约束面:msms 均已注册 / model ∈ cheap-worker.models / 提示词非空 */
|
|
13019
10669
|
function validateTrajectoryConstraint(root) {
|
|
13020
10670
|
const profiles = listTrajectoryConstraints(root);
|
|
@@ -13037,7 +10687,7 @@ function validateTrajectoryConstraint(root) {
|
|
|
13037
10687
|
return true;
|
|
13038
10688
|
}
|
|
13039
10689
|
})())) issues.push(`trajectory "${p.code}": no trajectory prompt (add ${TRAJECTORY_PROMPT_FILENAME} in the trajectory dir — recommended — or the legacy systemPromptFile/systemPrompt)`);
|
|
13040
|
-
for (const m of p.msms) if (!registered.has(m)) issues.push(`trajectory "${p.code}": msms "${m}" is not registered in mech-registry.json`);
|
|
10690
|
+
for (const m of p.msms ?? []) if (!registered.has(m)) issues.push(`trajectory "${p.code}": msms "${m}" is not registered in mech-registry.json`);
|
|
13041
10691
|
if (p.model && allowedModels && !allowedModels.has(p.model)) issues.push(`trajectory "${p.code}": model "${p.model}" not in cheap-worker.models (${[...allowedModels].join(", ") || "(none)"})`);
|
|
13042
10692
|
}
|
|
13043
10693
|
return {
|
|
@@ -13053,16 +10703,29 @@ function listTrajectoryConstraintSummary(root) {
|
|
|
13053
10703
|
roles: [],
|
|
13054
10704
|
note: `no 轨迹约束 trajectories in this CCC (no trajectory has ${TRAJECTORY_CONSTRAINT_FILENAME})`
|
|
13055
10705
|
};
|
|
13056
|
-
const items = profiles.map((p) =>
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
10706
|
+
const items = profiles.map((p) => {
|
|
10707
|
+
/**
|
|
10708
|
+
* 🔴 三个字段的读数必须**如实表达三态**(D179(c)+D181,S196 就是被这里的旧读数误导的)。
|
|
10709
|
+
*
|
|
10710
|
+
* | 文件里 | 打印 | 为什么不能印别的 |
|
|
10711
|
+
* |---|---|---|
|
|
10712
|
+
* | 没写 `model` | `'(unchanged)'` | 旧文案印 `'(cheap-worker default)'` ⇒ **暗示会回退到杂工模型**,而实际是**不动**。那是一次**空头承诺**:用户以为要被换模型,其实是误解 |
|
|
10713
|
+
* | 没写 `tools`/`msms` | `'(unrestricted)'` | 缺省 = **不收窄**(D181)。印 `[]` 会让人以为"我被全隐藏了" |
|
|
10714
|
+
* | 写了 `[]` | `'[]'` | 显式空数组 = **全隐藏**,与上一行**恰好相反**,必须能一眼分辨 |
|
|
10715
|
+
*/
|
|
10716
|
+
const wl = (v, whenUnset) => v === void 0 ? whenUnset : v;
|
|
10717
|
+
return {
|
|
10718
|
+
name: p.code,
|
|
10719
|
+
dirName: p.dirName,
|
|
10720
|
+
model: p.model ?? "(unchanged)",
|
|
10721
|
+
msms: wl(p.msms, "(unrestricted)"),
|
|
10722
|
+
tools: wl(p.tools, "(unrestricted)"),
|
|
10723
|
+
...p.resident !== void 0 ? { resident: p.resident.serves !== void 0 ? p.resident.serves : "(no serves)" } : {},
|
|
10724
|
+
cognitiveTrajectory: constraintCognitiveTrajectory(p),
|
|
10725
|
+
promptSource: constraintSystemPromptSource(p, hasTrajectoryPrompt(root, p.dirName)),
|
|
10726
|
+
...p.systemPromptFile ? { systemPromptFile: p.systemPromptFile } : {}
|
|
10727
|
+
};
|
|
10728
|
+
});
|
|
13066
10729
|
return {
|
|
13067
10730
|
roles: items,
|
|
13068
10731
|
count: items.length
|
|
@@ -13094,6 +10757,99 @@ function applyTrajectoryConstraint(root) {
|
|
|
13094
10757
|
note: `constraints are read live from each trajectory's ${TRAJECTORY_CONSTRAINT_FILENAME} — they take effect on the next turn (no restart); ${TRAJECTORY_CONSTRAINT_HINT}`
|
|
13095
10758
|
};
|
|
13096
10759
|
}
|
|
10760
|
+
/**
|
|
10761
|
+
* 一章的全文。
|
|
10762
|
+
*
|
|
10763
|
+
* ⚠️ 本函数是**纯常量拼接**(无 IO、无 ctx):手册正文不随容器状态变化,
|
|
10764
|
+
* 唯一随状态变的东西("我现在是不是轨迹约束")由 `config list` 回答,不写进正文。
|
|
10765
|
+
*/
|
|
10766
|
+
const TRAJECTORY_SELF_GUIDE = [
|
|
10767
|
+
"═══ 一条认知轨迹如何描述自己(self-declaration guide)═══",
|
|
10768
|
+
"",
|
|
10769
|
+
"这份手册回答一件事:**我(某条认知轨迹)想给自己做设定时,该写进哪个文件、怎么写、怎么验。**",
|
|
10770
|
+
"",
|
|
10771
|
+
`── 三个载体:想改哪件事,就写进哪个文件 ──
|
|
10772
|
+
|
|
10773
|
+
<trajectory>/SESSION.md ← 我的**身体**(认知轨迹本体;持久、永不移动)
|
|
10774
|
+
<trajectory>/PROMPT.md ← 我的**章程**(我是谁、按什么工作;**所有轨迹都可以有**)
|
|
10775
|
+
<trajectory>/trajectory.json ← 我的**声明**(结构化字段;有它才成为"轨迹约束",见下)
|
|
10776
|
+
|
|
10777
|
+
⚠️ 三者主语相同(都是这条轨迹)、生命周期相同(随 git 走)、写入频率相同(人偶尔改)
|
|
10778
|
+
⇒ 这正是它们**该分开而不该合并**的理由:每一项都对应一种**不同的意图**。`,
|
|
10779
|
+
"",
|
|
10780
|
+
`── PROMPT.md:我的章程(推荐所有轨迹都写)──
|
|
10781
|
+
|
|
10782
|
+
写什么:我这名认知轨迹的**身份与工作方式** —— 我负责什么、我的判断依据是什么、
|
|
10783
|
+
我的边界在哪、我怎么跟其他轨迹协作。
|
|
10784
|
+
谁读它:**只有绑定在我身上的会话**读到它(作为系统提示词的一个独立段)。
|
|
10785
|
+
何时生效:**保存即生效**(按 mtime 热重载,无需重启、无需 rebuild)。
|
|
10786
|
+
没有它会怎样:不报错。我拿到的是 ACC 的通用身份 —— 普通轨迹的常态,不是缺陷。
|
|
10787
|
+
|
|
10788
|
+
⚠️ 与 SESSION.md 的分工(别写重了):
|
|
10789
|
+
SESSION.md = **发生了什么**(日志、决策、当前状态)
|
|
10790
|
+
PROMPT.md = **我是谁、怎么工作**(章程;很少改,改了就改变行为)`,
|
|
10791
|
+
"",
|
|
10792
|
+
`── 常驻:自己声明"欢迎别人来麻烦我"(trajectory.json 的 resident 字段)──
|
|
10793
|
+
|
|
10794
|
+
🔴 先排除一个误解:**常驻不是资源状态**。
|
|
10795
|
+
它不是"永不休眠"、不是"常驻内存"、不是"不许销毁我的会话"。
|
|
10796
|
+
一条轨迹**永远存在**(没有生命周期),常驻与否都不改变这一点。
|
|
10797
|
+
|
|
10798
|
+
那它是什么:**我对自己用途的公开声明** —— "我在这儿、我擅长什么、欢迎来麻烦我"。
|
|
10799
|
+
别的轨迹要找人干活时,先查这份名单,而不是靠猜谁可能懂。
|
|
10800
|
+
|
|
10801
|
+
写法:
|
|
10802
|
+
{ "resident": { "serves": "一句话说清我擅长什么、能帮上什么忙" } }
|
|
10803
|
+
|
|
10804
|
+
⚠️ 两个门槛,缺一不可:
|
|
10805
|
+
① 写了 \`resident\` ⇒ 我进得了别人的候选名单(**可被发现**)
|
|
10806
|
+
② \`serves\` 非空 ⇒ 别人读了这句话能判断"这事是不是该找你"(**可被判断**)
|
|
10807
|
+
只写半个(有字段、没 serves)等于把自己挂上去却不说能干什么 —— 那比不写更坏,
|
|
10808
|
+
因为别人会来问,然后发现问错了人。
|
|
10809
|
+
|
|
10810
|
+
谁能看见:\`cognitive_trajectory\` 的 **\`resident\` 动作**(只读发现)。
|
|
10811
|
+
**它不进系统提示词**(不占每个请求的 token)—— 只在有人真的要找人时才读。
|
|
10812
|
+
这是 owner 2026-10-20 的裁决:说明类信息按需取,不常驻在上下文里。
|
|
10813
|
+
|
|
10814
|
+
🔴 它**不碰任何默认行为**(D183):只写 resident 的轨迹,ACC 身份注入、首轮锚定、
|
|
10815
|
+
自动恢复、compact 重注入 **全部照常**。常驻管的是**被找到**,不是**不被销毁**。`,
|
|
10816
|
+
"",
|
|
10817
|
+
`── 🔴 所有 key 都是可选的:不写 = 不设定 = 不影响默认行为 ──
|
|
10818
|
+
|
|
10819
|
+
这条是总规则(owner 2026-10-20 裁决)。下面逐字段说清"不写会怎样":
|
|
10820
|
+
|
|
10821
|
+
| 字段 | 不写它 | 生效时机 |
|
|
10822
|
+
|---|---|---|
|
|
10823
|
+
| \`model\` | 用会话当前的模型(**不会**回退到 cheap-worker) | **每轮**(改文件下一轮即生效) |
|
|
10824
|
+
| \`tools\` | **不收窄**工具面 | 每轮 |
|
|
10825
|
+
| \`msms\` | **不收窄** MSM 面 | 每轮 |
|
|
10826
|
+
| \`resident\` | 不出现在常驻名单里(不参与"被找") | 每轮 |
|
|
10827
|
+
| \`cognitiveTrajectory\` | 三项纪律全参与(true) | 每轮 |
|
|
10828
|
+
| \`systemPromptFile\` / \`systemPrompt\` | 用 ACC 的通用身份(**不替换**) | 保存即生效 |
|
|
10829
|
+
|
|
10830
|
+
🔴 **"没写" 与 "写了空数组" 是两种意思**:
|
|
10831
|
+
\`"tools": []\`(显式空数组)= **我一个都不给** ⇒ 除 \`skill\` 外全部工具被隐藏。
|
|
10832
|
+
**不写 \`tools\`** = **不收窄** ⇒ 工具面完全不变。
|
|
10833
|
+
⇒ 想只声明常驻?那就**只写 resident**,别的键一个都不要写。
|
|
10834
|
+
|
|
10835
|
+
🔴 **写 \`trajectory.json\` 本身不改任何默认行为** —— 它只是"我把设定放这儿了"。
|
|
10836
|
+
只有当某个 key **真的取代了默认**(目前唯一一个是自带章程 systemPrompt*)时,
|
|
10837
|
+
这条轨迹才不再注入 ACC 身份。其余字段都不影响。`,
|
|
10838
|
+
"",
|
|
10839
|
+
`── 想限制自己(白名单 / 模型 / 纪律参与项)──
|
|
10840
|
+
|
|
10841
|
+
schema、双白名单、纪律参与项的完整说明 → container_admin config docs trajectory-constraint`,
|
|
10842
|
+
"",
|
|
10843
|
+
`── 写完之后怎么验(别只看文档,要能机械判定)──
|
|
10844
|
+
|
|
10845
|
+
章程生效了吗 改 PROMPT.md 后**在同一会话里**看下一轮的系统提示词段是否变化(保存即生效)
|
|
10846
|
+
我声明了什么 container_admin config list —— 逐轨迹列出 model/tools/msms 的**生效值**;
|
|
10847
|
+
没写的字段印 \`(unchanged)\` / \`(unrestricted)\`,不是伪装成某个默认值
|
|
10848
|
+
配置合法吗 container_admin config validate —— msms 均已注册 / model 在可用表里
|
|
10849
|
+
常驻被看见了吗 cognitive_trajectory action=resident —— 我出现在名单里,且 serves 可读
|
|
10850
|
+
|
|
10851
|
+
⚠️ 以上四项都是**读取当前真实状态**,不是"保存成功"的回执。保存成功 ≠ 生效(本仓反复踩过)。`
|
|
10852
|
+
].join("\n");
|
|
13097
10853
|
//#endregion
|
|
13098
10854
|
//#region src/tools/container-admin.ts
|
|
13099
10855
|
/**
|
|
@@ -13236,9 +10992,14 @@ const DOCS_CHAPTERS = [
|
|
|
13236
10992
|
summary: "CCC 配置全表——9 段各是什么、怎么填、填错会怎样(.opencode/serenity.json)",
|
|
13237
10993
|
render: () => CCC_CONFIG_REFERENCE
|
|
13238
10994
|
},
|
|
10995
|
+
{
|
|
10996
|
+
id: "trajectory-self",
|
|
10997
|
+
summary: "一条轨迹怎么描述自己——章程 PROMPT.md / 常驻声明(resident.serves)/ 三个载体的分工 + 写完怎么验",
|
|
10998
|
+
render: () => TRAJECTORY_SELF_GUIDE
|
|
10999
|
+
},
|
|
13239
11000
|
{
|
|
13240
11001
|
id: "trajectory-constraint",
|
|
13241
|
-
summary: "
|
|
11002
|
+
summary: "轨迹约束(只讲限制面):trajectory.json 的双白名单 / 模型 / 纪律参与项 / IM 路由",
|
|
13242
11003
|
render: () => TRAJECTORY_CONSTRAINT_GUIDE
|
|
13243
11004
|
}
|
|
13244
11005
|
];
|
|
@@ -13250,7 +11011,7 @@ const DOCS_CHAPTERS = [
|
|
|
13250
11011
|
function createContainerAdminTool(ctx) {
|
|
13251
11012
|
return defineTool({
|
|
13252
11013
|
name: "container_admin",
|
|
13253
|
-
description: "Container administration (the ship's maintenance bay — one entry for managing the CCC): config — CCC configuration: docs (usage-manual index) / docs <chapter> (one chapter in full) / view (current values) + 轨迹约束 (guide/validate/apply/list); msm — MSM registry management (register/deregister/check quality DC-M1~M4) + manuals (guide dev manual / catalog ACC capability directory / ccc-config CCC configuration reference). Execution of registered MSMs belongs to the msm tool; this tool manages the container.",
|
|
11014
|
+
description: "Container administration (the ship's maintenance bay — one entry for managing the CCC): config — CCC configuration: docs (usage-manual index) / docs <chapter> (one chapter in full) / view (current values) + 轨迹约束 (guide/validate/apply/list); msm — MSM registry management (register/deregister/check quality DC-M1~M4) + manuals (guide dev manual / catalog ACC capability directory / ccc-config CCC configuration reference). TRAJECTORY SELF-DOCS: a cognitive trajectory describing ITSELF — charter (PROMPT.md) / resident declaration / self-restriction (whitelists) ⇒ config docs trajectory-self; the restriction side only ⇒ config docs trajectory-constraint. Execution of registered MSMs belongs to the msm tool; this tool manages the container.",
|
|
13254
11015
|
parameters: {
|
|
13255
11016
|
domain: {
|
|
13256
11017
|
type: "string",
|
|
@@ -13650,11 +11411,12 @@ function decideGuard(input) {
|
|
|
13650
11411
|
if (input.trajectoryConstraint !== void 0 && input.trajectoryConstraint !== null) {
|
|
13651
11412
|
const profile = input.trajectoryConstraint;
|
|
13652
11413
|
if (toolName === "skill") return { kind: "allow" };
|
|
13653
|
-
if (profile.msms.includes(toolName)) return {
|
|
11414
|
+
if ((profile.msms ?? []).includes(toolName)) return {
|
|
13654
11415
|
deny: `"${toolName}" is a registered MSM, not a direct tool in this trajectory — call it through the msm tool: msm("${toolName}", ["<args>"]) (pass "--help" or inspect=true for usage)`,
|
|
13655
11416
|
kind: "deny"
|
|
13656
11417
|
};
|
|
13657
|
-
|
|
11418
|
+
const whitelist = profileToolWhitelist(profile);
|
|
11419
|
+
if (whitelist !== null && !whitelist.has(toolName)) return {
|
|
13658
11420
|
deny: "tool not allowed in this 轨迹约束 trajectory",
|
|
13659
11421
|
kind: "deny"
|
|
13660
11422
|
};
|
|
@@ -13980,6 +11742,15 @@ function syncTrajectoryConstraintToolVisibility(agent, root) {
|
|
|
13980
11742
|
return;
|
|
13981
11743
|
}
|
|
13982
11744
|
const allowed = profileToolWhitelist(profile);
|
|
11745
|
+
if (allowed === null) {
|
|
11746
|
+
if (existing !== void 0) {
|
|
11747
|
+
try {
|
|
11748
|
+
existing.dispose();
|
|
11749
|
+
} catch {}
|
|
11750
|
+
trajectoryConstraintRestrictions.delete(sessionId);
|
|
11751
|
+
}
|
|
11752
|
+
return;
|
|
11753
|
+
}
|
|
13983
11754
|
const resident = REGISTERED_TOOLS.slice(0, REGISTERED_TOOLS.length - 3);
|
|
13984
11755
|
const tiers = [REGISTERED_TOOLS, resident];
|
|
13985
11756
|
for (const names of tiers) {
|
|
@@ -14277,7 +12048,7 @@ const DEFAULT_FAKE_INITIAL_MESSAGES = ["You are the operator agent of Serenity
|
|
|
14277
12048
|
* 统一经 sessionEvents() 读取(snapshotEvents() 优先,.events 兜底)。
|
|
14278
12049
|
*/
|
|
14279
12050
|
function hasUserMessageHistory(session) {
|
|
14280
|
-
return sessionEvents
|
|
12051
|
+
return sessionEvents(session).some((event) => {
|
|
14281
12052
|
const e = event;
|
|
14282
12053
|
if (e.type !== "user/message") return false;
|
|
14283
12054
|
return e.data?.source?.kind === "user";
|
|
@@ -14422,7 +12193,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
|
|
|
14422
12193
|
let boundary = -1;
|
|
14423
12194
|
let signalCount = 0;
|
|
14424
12195
|
let rounds = 0;
|
|
14425
|
-
const events = sessionEvents
|
|
12196
|
+
const events = sessionEvents(session);
|
|
14426
12197
|
if (events.length > 0) for (const event of events) {
|
|
14427
12198
|
const e = event;
|
|
14428
12199
|
const seq = typeof e.seq === "number" ? e.seq : 0;
|
|
@@ -14461,7 +12232,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
|
|
|
14461
12232
|
const loopSid = session.id;
|
|
14462
12233
|
const loopCwd = session.header?.cwd ?? process.cwd();
|
|
14463
12234
|
const loopRoot = cccRootForCwd(loopCwd);
|
|
14464
|
-
if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || loopRoot !== null &&
|
|
12235
|
+
if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || loopRoot !== null && replacesDefaultIdentity(trajectoryConstraintFor(loopRoot, loopSid)))) return {
|
|
14465
12236
|
boundary: -1,
|
|
14466
12237
|
promoted: true
|
|
14467
12238
|
};
|
|
@@ -14580,7 +12351,7 @@ function registerBootstrap(ctx) {
|
|
|
14580
12351
|
const depth = session?.header?.delegationDepth ?? 0;
|
|
14581
12352
|
const sid = typeof session?.id === "string" ? session.id : void 0;
|
|
14582
12353
|
if (sid === void 0) return;
|
|
14583
|
-
if (sid.startsWith("cheap-worker-") ||
|
|
12354
|
+
if (sid.startsWith("cheap-worker-") || replacesDefaultIdentity(trajectoryConstraintFor(root, sid))) return;
|
|
14584
12355
|
if (depth === 0) {
|
|
14585
12356
|
if (hasUserMessageHistory(session)) return;
|
|
14586
12357
|
} else if (anchoredSessions.has(sid)) return;
|
|
@@ -15120,12 +12891,209 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
15120
12891
|
});
|
|
15121
12892
|
}
|
|
15122
12893
|
//#endregion
|
|
12894
|
+
//#region src/trajectory/model-runtime.ts
|
|
12895
|
+
/**
|
|
12896
|
+
* trajectory/model-runtime.ts — **`trajectory.json` 的 `model` 字段:每轮求值 + 运行时热切换**
|
|
12897
|
+
*
|
|
12898
|
+
* ## 这个模块存在的唯一理由(owner 2026-10-20 令,逐字)
|
|
12899
|
+
*
|
|
12900
|
+
* > 「我认为认知轨迹作为统一概念 **设定不应根据入口不同而有变化 所以应当直接生效 运行时热加载**」
|
|
12901
|
+
*
|
|
12902
|
+
* ## 旧实现错在哪(D182 的事实底数)
|
|
12903
|
+
*
|
|
12904
|
+
* `model` 的**唯一消费点**曾是 `createTrajectoryAgent` 建 agent 时,而它的**唯一调用方**是
|
|
12905
|
+
* IM 入站路由 ⇒ 对**已经存在的会话**(web 会话、`send-now` 冷启的载体……)**完全不生效**。
|
|
12906
|
+
* 而 `container_admin config list` 把它印成 `'(cheap-worker default)'`,看上去像个统一设定。
|
|
12907
|
+
* ⇒ **同一个字段在 A 入口生效、在 B 入口不生效** —— 正是 owner 要禁的形态。
|
|
12908
|
+
*
|
|
12909
|
+
* ## 修法:接宿主**自带**的原语,不复刻语义
|
|
12910
|
+
*
|
|
12911
|
+
* `@deepseek-ai/dsh-agent` 导出 `installModelSelection(agentCtx, selection)`,它挂三条缝:
|
|
12912
|
+
*
|
|
12913
|
+
* | 缝 | 作用 |
|
|
12914
|
+
* |---|---|
|
|
12915
|
+
* | `system-prompt/assemble` | 把选中路由写进提示词变量快照(保证"这一步用哪个模型"一致) |
|
|
12916
|
+
* | **`agent/request`** | **把冻结的调用配置替换成 `selection.current`** ⇒ 这就是"下一轮生效" |
|
|
12917
|
+
* | `agent/pre-step` | 路由变化时追加 `[model changed: …]` 持久提示 |
|
|
12918
|
+
*
|
|
12919
|
+
* 🔴 **为什么必须直接调它而不是自己挂 `agent/request`**(本仓既有纪律):
|
|
12920
|
+
* 自己复刻就要重做第三条的三态判断(reject/空步/aborted)与 `requestHeader` 比对,
|
|
12921
|
+
* **复刻错了会得到与事实相反的结论**。宿主已实现 ⇒ 直接调用。
|
|
12922
|
+
*
|
|
12923
|
+
* 🔴 **跨宿主档位**:`_tmp/host-*` 全部 **10 个**快照(`0.1.5-rc.1` … `0.2.1-rc.2`)
|
|
12924
|
+
* 逐个核对,该导出**全部存在且签名一致**(唯一差异在宿主内部的提示 `source` 形状,
|
|
12925
|
+
* **不由我们构造**)⇒ 无需宿主升级、无需分档分支。
|
|
12926
|
+
*
|
|
12927
|
+
* ## 三条硬边界(少一条就会重演 S196 的坑)
|
|
12928
|
+
*
|
|
12929
|
+
* 1. **未写 `model` ⇒ 不装这条缝**(`undefined` 原样返回)。⛔ **绝不回退 `cheap-worker.defaultModel`**
|
|
12930
|
+
* —— 那个回退把"我想声明自述"变成"我被降级到杂工模型",而用户完全不会预期到。
|
|
12931
|
+
* 2. **切换会留下宿主的 `[model changed: …]` 提示**。这是**特性不是噪声**:
|
|
12932
|
+
* 否则模型不知道自己换了,它会照旧解释上方那些由**另一个模型**生成的回合。
|
|
12933
|
+
* 3. **模型名非法 ⇒ 不切 + 响亮告警**,绝不静默停在旧模型上("改了没生效"是最难查的一类)。
|
|
12934
|
+
*
|
|
12935
|
+
* ## 归属(D23)
|
|
12936
|
+
* **机制在 ACC**(怎么读、怎么装、何时刷新、非法怎么办);**内容在 CCC**(`trajectory.json` 写哪个模型)。
|
|
12937
|
+
*/
|
|
12938
|
+
/**
|
|
12939
|
+
* 从轨迹声明里取模型选择(**每轮调用**,故不缓存)。
|
|
12940
|
+
*
|
|
12941
|
+
* @param root CCC 根
|
|
12942
|
+
* @param dirName 轨迹目录名
|
|
12943
|
+
* @returns `{ provider?, model }`;**未写 `model` ⇒ `undefined`(= 不动当前模型)**
|
|
12944
|
+
*
|
|
12945
|
+
* ⚠️ 读盘失败/坏 JSON ⇒ `undefined` 并**不抛**:一次读失败不该让整轮失败
|
|
12946
|
+
* (`readTrajectoryConstraintAt` 内部已对坏 JSON 告警)。
|
|
12947
|
+
*/
|
|
12948
|
+
function readModelSelection(root, dirName) {
|
|
12949
|
+
let profile;
|
|
12950
|
+
try {
|
|
12951
|
+
profile = readTrajectoryConstraintAt(join(sessionsRoot(root), dirName), root);
|
|
12952
|
+
} catch {
|
|
12953
|
+
return;
|
|
12954
|
+
}
|
|
12955
|
+
const raw = profile?.model?.trim();
|
|
12956
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
12957
|
+
const { provider, model } = splitModel(raw);
|
|
12958
|
+
if (model.trim() === "") return void 0;
|
|
12959
|
+
return provider === void 0 ? { model } : {
|
|
12960
|
+
provider,
|
|
12961
|
+
model
|
|
12962
|
+
};
|
|
12963
|
+
}
|
|
12964
|
+
/** 已装的模型选择(进程级;`sessionId → selection`,与 tool-restrictions 同构) */
|
|
12965
|
+
const installed = /* @__PURE__ */ new Map();
|
|
12966
|
+
/** 会话销毁时清理 */
|
|
12967
|
+
function forgetModelRuntime(sessionId) {
|
|
12968
|
+
const cur = installed.get(sessionId);
|
|
12969
|
+
if (cur === void 0) return;
|
|
12970
|
+
try {
|
|
12971
|
+
cur.dispose();
|
|
12972
|
+
} catch {}
|
|
12973
|
+
installed.delete(sessionId);
|
|
12974
|
+
}
|
|
12975
|
+
/**
|
|
12976
|
+
* 给这个 agent 装**模型运行时热切换**(每轮重读 `trajectory.json`)。
|
|
12977
|
+
*
|
|
12978
|
+
* 🔴 **只在轨迹声明了 `model` 时装**(D182 边界 ①):
|
|
12979
|
+
* 装了缝而 `current` 为 `undefined` 会**覆盖**宿主已有的模型选择 ⇒ 等于把会话的模型清空。
|
|
12980
|
+
* 故"没写" **必须** 表现为"这条缝不存在",而不是"缝在、值为空"。
|
|
12981
|
+
*
|
|
12982
|
+
* 🔴 **每轮刷新**:`agent/pre-step` 每步都调它,求值器**重读磁盘**
|
|
12983
|
+
* (`readTrajectoryConstraintAt` 无缓存)⇒ 改 `trajectory.json` 下一轮即生效,
|
|
12984
|
+
* **与"谁创建了这个会话"无关**(这就是 owner 的令)。
|
|
12985
|
+
*
|
|
12986
|
+
* @param agent 目标 agent
|
|
12987
|
+
* @param root CCC 根
|
|
12988
|
+
* @param dirName 该 agent 绑定的轨迹目录名
|
|
12989
|
+
* @returns 是否装了缝(`false` = 该轨迹没声明 model,未改动任何东西)
|
|
12990
|
+
*/
|
|
12991
|
+
function syncTrajectoryModel(agent, root, dirName) {
|
|
12992
|
+
const sessionId = agent.session?.id ?? "global";
|
|
12993
|
+
const wanted = readModelSelection(root, dirName);
|
|
12994
|
+
if (wanted === void 0) {
|
|
12995
|
+
forgetModelRuntime(sessionId);
|
|
12996
|
+
return false;
|
|
12997
|
+
}
|
|
12998
|
+
const existing = installed.get(sessionId);
|
|
12999
|
+
if (existing !== void 0) {
|
|
13000
|
+
existing.current = wanted;
|
|
13001
|
+
return true;
|
|
13002
|
+
}
|
|
13003
|
+
const install = resolveInstallModelSelection();
|
|
13004
|
+
if (install === null) {
|
|
13005
|
+
console.warn(`[serenity-hooks] ✗ 轨迹 "${dirName}" 声明了 model,但**宿主未导出 installModelSelection** ⇒ 模型热切换无法生效(当前会话模型保持不变)。这是宿主契约变化,请核对 @deepseek-ai/dsh-agent 的导出。`);
|
|
13006
|
+
return false;
|
|
13007
|
+
}
|
|
13008
|
+
const selection = {
|
|
13009
|
+
current: wanted,
|
|
13010
|
+
assembled: void 0
|
|
13011
|
+
};
|
|
13012
|
+
try {
|
|
13013
|
+
const dispose = install(agent.ctx, selection);
|
|
13014
|
+
installed.set(sessionId, {
|
|
13015
|
+
...selection,
|
|
13016
|
+
dispose
|
|
13017
|
+
});
|
|
13018
|
+
return true;
|
|
13019
|
+
} catch (err) {
|
|
13020
|
+
console.warn(`[serenity-hooks] ✗ 轨迹 "${dirName}" 的模型热切换装配失败(会话模型保持不变): ${String(err?.message ?? err)}`);
|
|
13021
|
+
return false;
|
|
13022
|
+
}
|
|
13023
|
+
}
|
|
13024
|
+
/**
|
|
13025
|
+
* 取宿主的 `installModelSelection`。
|
|
13026
|
+
*
|
|
13027
|
+
* 🔴 **为什么是 `createRequire` 动态取,而不是顶层 `import`**(R↓,两个理由):
|
|
13028
|
+
* ① 本仓对 `@deepseek-ai/dsh-agent` 的**全部**引用此前都是 `import type`(零运行时依赖);
|
|
13029
|
+
* 改成顶层运行时 import 会把它变成**硬依赖**——而它是 `peerDependencies`,
|
|
13030
|
+
* 宿主没装好时**整个插件起不来**,那比"模型不切换"严重得多。
|
|
13031
|
+
* ② 这是一个**可选能力**:拿不到就告警并保持会话原模型(`null` 分支),插件其余部分照常工作。
|
|
13032
|
+
*
|
|
13033
|
+
* ⚠️ 本仓是 ESM(`package.json` `"type": "module"`)⇒ **裸 `require` 不可用**,
|
|
13034
|
+
* 必须走 `createRequire(import.meta.url)`(同 `diagram/rasterize.ts` 的既有做法)。
|
|
13035
|
+
*
|
|
13036
|
+
* ⚠️ 解析结果**缓存**(成功或确实没有都缓存):避免每轮重复 require 与重复告警刷屏。
|
|
13037
|
+
*/
|
|
13038
|
+
let resolveCache;
|
|
13039
|
+
function resolveInstallModelSelection() {
|
|
13040
|
+
if (resolveCache !== void 0) return resolveCache;
|
|
13041
|
+
try {
|
|
13042
|
+
const mod = createRequire(import.meta.url)("@deepseek-ai/dsh-agent");
|
|
13043
|
+
resolveCache = typeof mod.installModelSelection === "function" ? mod.installModelSelection : null;
|
|
13044
|
+
} catch {
|
|
13045
|
+
resolveCache = null;
|
|
13046
|
+
}
|
|
13047
|
+
return resolveCache;
|
|
13048
|
+
}
|
|
13049
|
+
//#endregion
|
|
15123
13050
|
//#region src/seams/context.ts
|
|
13051
|
+
/**
|
|
13052
|
+
* 该会话的轨迹**是否自带章程**(= 该不该旁路 ACC 身份类默认行为)。D183。
|
|
13053
|
+
*
|
|
13054
|
+
* 🔴 判据唯一入口 = `replacesDefaultIdentity`(见 `policy/profile.ts` 的判据表)。
|
|
13055
|
+
* ⛔ 不要在这里写 `isTrajectoryConstrained` —— 那个只回答"有没有声明",
|
|
13056
|
+
* 用它做旁路就是 owner 2026-10-20 指出的那个 bug(只写 resident 也丢身份)。
|
|
13057
|
+
*/
|
|
13058
|
+
function hasTrajectoryOwnCharter(root, sessionId) {
|
|
13059
|
+
if (typeof sessionId !== "string" || sessionId === "") return false;
|
|
13060
|
+
try {
|
|
13061
|
+
return replacesDefaultIdentity(trajectoryConstraintFor(root, sessionId));
|
|
13062
|
+
} catch {
|
|
13063
|
+
return false;
|
|
13064
|
+
}
|
|
13065
|
+
}
|
|
13066
|
+
/**
|
|
13067
|
+
* 该 agent 绑定的轨迹目录名(没有 ⇒ `null`)。
|
|
13068
|
+
*
|
|
13069
|
+
* 🔴 判据唯一入口 = `trajectoryConstraintFor`(它内部并取"持久绑定 + 内存注册表"两来源,
|
|
13070
|
+
* 见 `policy/profile.ts` 的 R↓)。⛔ 不要在这里另写一套解析 —— 两份判据会漂。
|
|
13071
|
+
*/
|
|
13072
|
+
function trajectoryDirNameOf(agent, root) {
|
|
13073
|
+
const sessionId = agent.session?.id;
|
|
13074
|
+
if (typeof sessionId !== "string" || sessionId === "") return null;
|
|
13075
|
+
try {
|
|
13076
|
+
return trajectoryConstraintFor(root, sessionId)?.dirName ?? null;
|
|
13077
|
+
} catch {
|
|
13078
|
+
return null;
|
|
13079
|
+
}
|
|
13080
|
+
}
|
|
13081
|
+
/**
|
|
13082
|
+
* 同步该 agent 的**模型热切换**(D182)。
|
|
13083
|
+
*
|
|
13084
|
+
* 🔴 把它做成"按 agent 解析目录名 ⇒ 交给 `trajectory/model-runtime`"的薄适配,
|
|
13085
|
+
* 是为了让 `model-runtime` 保持**零 seams 依赖**(可独立单测)。
|
|
13086
|
+
*/
|
|
13087
|
+
function syncTrajectoryModelForSession(agent, root) {
|
|
13088
|
+
const dirName = trajectoryDirNameOf(agent, root);
|
|
13089
|
+
if (dirName === null) return;
|
|
13090
|
+
syncTrajectoryModel(agent, root, dirName);
|
|
13091
|
+
}
|
|
15124
13092
|
const DEFAULT_ENTRY_SKILL_MAX_CHARS = 3e4;
|
|
15125
13093
|
/** 从 dsh 会话日志读标题(latest-wins `session/title` 事件;无返回 null)——供标题 reconcile(U3) */
|
|
15126
13094
|
function readDshSessionTitle(session) {
|
|
15127
13095
|
try {
|
|
15128
|
-
const events = sessionEvents
|
|
13096
|
+
const events = sessionEvents(session);
|
|
15129
13097
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
15130
13098
|
const e = events[i];
|
|
15131
13099
|
if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
|
|
@@ -15190,7 +13158,7 @@ function shouldAutoRestore(agent, root) {
|
|
|
15190
13158
|
if (session.header?.parentSession) return false;
|
|
15191
13159
|
if (session.id?.startsWith("cheap-worker-")) return false;
|
|
15192
13160
|
const cccRoot = root !== void 0 ? root : cccRootForCwd(session.header?.cwd ?? process.cwd());
|
|
15193
|
-
if (cccRoot &&
|
|
13161
|
+
if (cccRoot && hasTrajectoryOwnCharter(cccRoot, session.id)) return false;
|
|
15194
13162
|
return true;
|
|
15195
13163
|
}
|
|
15196
13164
|
/**
|
|
@@ -15200,7 +13168,7 @@ function shouldAutoRestore(agent, root) {
|
|
|
15200
13168
|
*/
|
|
15201
13169
|
function shouldRestoreActive(agent, root) {
|
|
15202
13170
|
if (!shouldAutoRestore(agent, root)) return false;
|
|
15203
|
-
return sessionEvents
|
|
13171
|
+
return sessionEvents(agent.session).length > 0;
|
|
15204
13172
|
}
|
|
15205
13173
|
function registerContext(ctx, opts = {}) {
|
|
15206
13174
|
const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
|
|
@@ -15213,7 +13181,7 @@ function registerContext(ctx, opts = {}) {
|
|
|
15213
13181
|
} catch {}
|
|
15214
13182
|
if (!root) return;
|
|
15215
13183
|
const key = agentKey(agent);
|
|
15216
|
-
if (
|
|
13184
|
+
if (hasTrajectoryOwnCharter(root, key)) return;
|
|
15217
13185
|
registerEntrySkillSection(agent, root);
|
|
15218
13186
|
registerCognitiveTrajectorySkillSection(agent, root);
|
|
15219
13187
|
const scope = agentScope(agent);
|
|
@@ -15231,7 +13199,7 @@ function registerContext(ctx, opts = {}) {
|
|
|
15231
13199
|
};
|
|
15232
13200
|
}
|
|
15233
13201
|
if (!restored) {
|
|
15234
|
-
const info = parseSessionContextFromEvents(sessionEvents
|
|
13202
|
+
const info = parseSessionContextFromEvents(sessionEvents(dshSession));
|
|
15235
13203
|
if (info) {
|
|
15236
13204
|
const abs = info.mdPath.startsWith(root) ? info.mdPath : resolve(root, info.mdPath);
|
|
15237
13205
|
if (existsSync(abs)) restored = {
|
|
@@ -15284,6 +13252,9 @@ function registerContext(ctx, opts = {}) {
|
|
|
15284
13252
|
try {
|
|
15285
13253
|
syncHostScheduleToolHiding(agent, root);
|
|
15286
13254
|
} catch {}
|
|
13255
|
+
try {
|
|
13256
|
+
syncTrajectoryModelForSession(agent, root);
|
|
13257
|
+
} catch {}
|
|
15287
13258
|
};
|
|
15288
13259
|
if (opts.seedOnStart ?? true) ctx.on("agent/created", async (payload) => {
|
|
15289
13260
|
try {
|
|
@@ -15296,10 +13267,15 @@ function registerContext(ctx, opts = {}) {
|
|
|
15296
13267
|
const root = cccRootForCwd(cwd);
|
|
15297
13268
|
const key = agentKey(agent);
|
|
15298
13269
|
const downstream = await next();
|
|
15299
|
-
if (root)
|
|
15300
|
-
|
|
15301
|
-
|
|
15302
|
-
|
|
13270
|
+
if (root) {
|
|
13271
|
+
try {
|
|
13272
|
+
syncTrajectoryConstraintToolVisibility(agent, root);
|
|
13273
|
+
} catch {}
|
|
13274
|
+
try {
|
|
13275
|
+
syncTrajectoryModelForSession(agent, root);
|
|
13276
|
+
} catch {}
|
|
13277
|
+
}
|
|
13278
|
+
if (root && hasTrajectoryOwnCharter(root, key)) return downstream;
|
|
15303
13279
|
try {
|
|
15304
13280
|
syncCccToolVisibility(agent, root);
|
|
15305
13281
|
} catch {}
|
|
@@ -15348,7 +13324,10 @@ function registerCompactRetention(ctx, opts = {}) {
|
|
|
15348
13324
|
const cwd = agent.session?.header?.cwd ?? process.cwd();
|
|
15349
13325
|
const root = cccRootForCwd(cwd);
|
|
15350
13326
|
if (!root) return;
|
|
15351
|
-
|
|
13327
|
+
const sid = String(session.id ?? "");
|
|
13328
|
+
try {
|
|
13329
|
+
if (replacesDefaultIdentity(trajectoryConstraintFor(root, sid))) return;
|
|
13330
|
+
} catch {}
|
|
15352
13331
|
try {
|
|
15353
13332
|
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
15354
13333
|
} catch {}
|
|
@@ -15843,7 +13822,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15843
13822
|
const settings = readAdvancedSettings();
|
|
15844
13823
|
let known = [];
|
|
15845
13824
|
try {
|
|
15846
|
-
|
|
13825
|
+
const registry = hostService(ctx, "workspaceRegistry");
|
|
13826
|
+
known = projectKnownWorkspaces(registry?.list?.() ?? [], settings.gateway.workspaces);
|
|
15847
13827
|
} catch {}
|
|
15848
13828
|
sendJson$2(res, 200, {
|
|
15849
13829
|
config: toWire(settings),
|
|
@@ -15853,7 +13833,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15853
13833
|
}
|
|
15854
13834
|
if (req.method === "PUT") {
|
|
15855
13835
|
const raw = await readBody$2(req, 131072);
|
|
15856
|
-
const
|
|
13836
|
+
const body = JSON.parse(raw);
|
|
13837
|
+
const saved = applyWirePatch(body.config ?? {});
|
|
15857
13838
|
try {
|
|
15858
13839
|
ctx.emit?.("serenity/config-updated");
|
|
15859
13840
|
} catch {}
|
|
@@ -15903,11 +13884,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15903
13884
|
sendJson$2(res, 400, { error: chk.error });
|
|
15904
13885
|
return;
|
|
15905
13886
|
}
|
|
15906
|
-
const { readWeixinSettings } = await import("./route-
|
|
13887
|
+
const { readWeixinSettings } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
|
|
15907
13888
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
|
|
15908
13889
|
const settings = readWeixinSettings(chk.root);
|
|
15909
13890
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
15910
|
-
const { imCredentialBound } = await import("./route-
|
|
13891
|
+
const { imCredentialBound } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
|
|
15911
13892
|
const { normalizeAccountChannel } = await import("./accounts-BHQbakUh.js").then((n) => n.t);
|
|
15912
13893
|
const accounts = (settings.accounts ?? []).map((a) => {
|
|
15913
13894
|
const channel = normalizeAccountChannel(a.channel);
|
|
@@ -15944,7 +13925,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15944
13925
|
const { fetchQRCode } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
|
|
15945
13926
|
const { randomUUID } = await import("node:crypto");
|
|
15946
13927
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
15947
|
-
const { readWeixinSettings } = await import("./route-
|
|
13928
|
+
const { readWeixinSettings } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
|
|
15948
13929
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
15949
13930
|
const loginKey = randomUUID();
|
|
15950
13931
|
weixinLogins.set(loginKey, {
|
|
@@ -15965,7 +13946,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15965
13946
|
sendJson$2(res, 400, { error: "missing accountId" });
|
|
15966
13947
|
return;
|
|
15967
13948
|
}
|
|
15968
|
-
const { removeWeixinAccount } = await import("./route-
|
|
13949
|
+
const { removeWeixinAccount } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
|
|
15969
13950
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15970
13951
|
removeWeixinAccount(root, body.accountId);
|
|
15971
13952
|
syncCccBridge(ctx, root);
|
|
@@ -15991,8 +13972,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15991
13972
|
trajectory: code
|
|
15992
13973
|
});
|
|
15993
13974
|
}
|
|
15994
|
-
const { saveWeixinRoutes } = await import("./route-
|
|
15995
|
-
const { findDirNameByCode } = await import("./profile-
|
|
13975
|
+
const { saveWeixinRoutes } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
|
|
13976
|
+
const { findDirNameByCode } = await import("./profile-C7BuyZK7.js").then((n) => n.i);
|
|
15996
13977
|
for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
|
|
15997
13978
|
sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
|
|
15998
13979
|
return;
|
|
@@ -16003,7 +13984,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
16003
13984
|
}
|
|
16004
13985
|
if (body.action === "set-enabled") {
|
|
16005
13986
|
const enabled = body.enabled === true;
|
|
16006
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./route-
|
|
13987
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
|
|
16007
13988
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
16008
13989
|
const settings = readWeixinSettings(root);
|
|
16009
13990
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -16026,7 +14007,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
16026
14007
|
sendJson$2(res, 400, { error: "appId 与 appSecret 都必须填(只填一个等于没绑)" });
|
|
16027
14008
|
return;
|
|
16028
14009
|
}
|
|
16029
|
-
const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-
|
|
14010
|
+
const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
|
|
16030
14011
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
16031
14012
|
const before = readWeixinSettings(root);
|
|
16032
14013
|
const accountId = (body.accountId ?? "").trim() !== "" ? body.accountId.trim() : nextFeishuAccountId(before);
|
|
@@ -16076,7 +14057,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
16076
14057
|
return;
|
|
16077
14058
|
}
|
|
16078
14059
|
const { pollQRStatus } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
|
|
16079
|
-
const { readWeixinSettings } = await import("./route-
|
|
14060
|
+
const { readWeixinSettings } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
|
|
16080
14061
|
const settings = readWeixinSettings(login.root);
|
|
16081
14062
|
const status = await pollQRStatus({
|
|
16082
14063
|
baseUrl: void 0,
|
|
@@ -16091,7 +14072,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
16091
14072
|
});
|
|
16092
14073
|
return;
|
|
16093
14074
|
}
|
|
16094
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-
|
|
14075
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
|
|
16095
14076
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
16096
14077
|
const accountId = nextWeixinAccountId(settings);
|
|
16097
14078
|
upsertWeixinAccount(login.root, {
|
|
@@ -18104,7 +16085,7 @@ function parseAnchorMdPath(session) {
|
|
|
18104
16085
|
try {
|
|
18105
16086
|
const nodes = [...session.surface.nodes];
|
|
18106
16087
|
if (nodes.length === 0) return null;
|
|
18107
|
-
const events = sessionEvents
|
|
16088
|
+
const events = sessionEvents(session);
|
|
18108
16089
|
if (events.length === 0) return null;
|
|
18109
16090
|
const event = events[nodes[0]];
|
|
18110
16091
|
if (!event) return null;
|
|
@@ -18140,7 +16121,7 @@ function sessionNameFromMdPath(mdPath) {
|
|
|
18140
16121
|
function resolveSessionMdPath(root, scope, session) {
|
|
18141
16122
|
const candidates = [];
|
|
18142
16123
|
candidates.push(getActiveSessionInfo(scope)?.mdPath ?? null);
|
|
18143
|
-
const events = sessionEvents
|
|
16124
|
+
const events = sessionEvents(session);
|
|
18144
16125
|
if (events.length > 0) candidates.push(parseSessionContextFromEvents(events)?.mdPath ?? null);
|
|
18145
16126
|
candidates.push(readLastBound(session)?.mdPath ?? null);
|
|
18146
16127
|
candidates.push(parseAnchorMdPath(session));
|
|
@@ -18253,7 +16234,7 @@ function performRebuild(session, pending, meter) {
|
|
|
18253
16234
|
if (targets.length === 0) return false;
|
|
18254
16235
|
if (meter) {
|
|
18255
16236
|
let shadowedTokenCount = 0;
|
|
18256
|
-
const events = sessionEvents
|
|
16237
|
+
const events = sessionEvents(session);
|
|
18257
16238
|
for (const seq of targets) {
|
|
18258
16239
|
const event = events[seq];
|
|
18259
16240
|
if (!event) continue;
|
|
@@ -20299,7 +18280,7 @@ function isExternalFaceSession(sessionId, root) {
|
|
|
20299
18280
|
* 否则 think 内提及内部机制词(思考过程必然推演机制)会误打回。复用 stripThink
|
|
20300
18281
|
* (v1.26.8 状态机,弃正则——同 v1.27.1 微信桥回复链路)。 */
|
|
20301
18282
|
function lastAssistantText(agent) {
|
|
20302
|
-
const events = sessionEvents
|
|
18283
|
+
const events = sessionEvents(agent.session);
|
|
20303
18284
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
20304
18285
|
const e = events[i];
|
|
20305
18286
|
if (e && e.type === "assistant/message") {
|
|
@@ -20974,7 +18955,7 @@ const MEDIA_MAX_BYTES = 20971520;
|
|
|
20974
18955
|
*/
|
|
20975
18956
|
async function resolveCccRootByAlias(ctx, alias) {
|
|
20976
18957
|
try {
|
|
20977
|
-
const { listCccs } = await import("./ccc-roots-
|
|
18958
|
+
const { listCccs } = await import("./ccc-roots-D3ty1C8P.js").then((n) => n.a);
|
|
20978
18959
|
const entries = await listCccs(ctx);
|
|
20979
18960
|
const byName = entries.find((e) => e.name === alias);
|
|
20980
18961
|
if (byName) return byName.root;
|
|
@@ -21042,7 +19023,7 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
21042
19023
|
});
|
|
21043
19024
|
return null;
|
|
21044
19025
|
}
|
|
21045
|
-
const { findDirNameByCode } = await import("./profile-
|
|
19026
|
+
const { findDirNameByCode } = await import("./profile-C7BuyZK7.js").then((n) => n.i);
|
|
21046
19027
|
if (findDirNameByCode(root, target.role) === null) {
|
|
21047
19028
|
const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
|
|
21048
19029
|
console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
|
|
@@ -21854,7 +19835,7 @@ function syncHumanChannel(ctx) {
|
|
|
21854
19835
|
(async () => {
|
|
21855
19836
|
try {
|
|
21856
19837
|
stopMachineLoops();
|
|
21857
|
-
const { listCccs } = await import("./ccc-roots-
|
|
19838
|
+
const { listCccs } = await import("./ccc-roots-D3ty1C8P.js").then((n) => n.a);
|
|
21858
19839
|
for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
|
|
21859
19840
|
} catch {}
|
|
21860
19841
|
})();
|
|
@@ -22118,7 +20099,7 @@ function matchCcc(input, candidates) {
|
|
|
22118
20099
|
}
|
|
22119
20100
|
/** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
22120
20101
|
async function collectCandidates(ctx) {
|
|
22121
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20102
|
+
const { listCccs } = await import("./ccc-roots-D3ty1C8P.js").then((n) => n.a);
|
|
22122
20103
|
const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
|
|
22123
20104
|
const seen = /* @__PURE__ */ new Set();
|
|
22124
20105
|
const out = [];
|
|
@@ -23569,7 +21550,7 @@ function createImBridgeTool() {
|
|
|
23569
21550
|
*/
|
|
23570
21551
|
function readSessionTitle(session) {
|
|
23571
21552
|
try {
|
|
23572
|
-
const events = sessionEvents
|
|
21553
|
+
const events = sessionEvents(session);
|
|
23573
21554
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
23574
21555
|
const e = events[i];
|
|
23575
21556
|
if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
|