@shgroup/dsh-serenity-hooks 1.70.0 → 1.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -19
- package/dsh.plugin.json +2 -2
- package/lib/{ccc-C33QWoWZ.js → ccc-Dkoa2oW3.js} +16 -2
- package/lib/{ccc-roots-RzasspMr.js → ccc-roots-jZYR2GVE.js} +16 -10
- package/lib/client.js +2 -2
- package/lib/{gateway-auth.d.ts → gateway/auth.d.ts} +3 -3
- package/lib/{gateway-dsh-auth.d.ts → gateway/dsh-auth.d.ts} +1 -1
- package/lib/{gateway-proxy.d.ts → gateway/proxy.d.ts} +3 -3
- package/lib/{gateway.d.ts → gateway/server.d.ts} +5 -5
- package/lib/{totp.d.ts → gateway/totp.d.ts} +1 -1
- package/lib/{output-guard.d.ts → guard/filter.d.ts} +2 -2
- package/lib/{output-guard-seam.d.ts → guard/seam.d.ts} +2 -2
- package/lib/{weixin-api-BrRcPiQs.js → ilink-DEpmE3yi.js} +6 -6
- package/lib/im/accounts.d.ts +105 -0
- package/lib/{weixin-bridge.d.ts → im/bridge.d.ts} +16 -9
- package/lib/{im-bridge.d.ts → im/channels.d.ts} +3 -3
- package/lib/{weixin-send-endpoint.d.ts → im/endpoint.d.ts} +2 -2
- package/lib/{weixin-output-guard.d.ts → im/guard.d.ts} +1 -1
- package/lib/{weixin-hook.d.ts → im/hook.d.ts} +1 -1
- package/lib/{weixin-api.d.ts → im/ilink.d.ts} +3 -3
- package/lib/{human-channel.d.ts → im/inbound.d.ts} +4 -4
- package/lib/{weixin-route.d.ts → im/route.d.ts} +52 -11
- package/lib/{weixin-send-api.d.ts → im/send.d.ts} +3 -3
- package/lib/im/weixin-account.d.ts +45 -0
- package/lib/{im-weixin.d.ts → im/weixin-channel.d.ts} +2 -2
- package/lib/index.js +1008 -815
- package/lib/invariant.js +2 -2
- package/lib/{ccc-roots.d.ts → kernel/ccc-roots.d.ts} +10 -10
- package/lib/{ccc.d.ts → kernel/ccc.d.ts} +114 -44
- package/lib/{config-ops.d.ts → kernel/config.d.ts} +2 -2
- package/lib/kernel/constants.d.ts +20 -0
- package/lib/{invariant.d.ts → kernel/invariant.d.ts} +1 -1
- package/lib/{json.d.ts → kernel/json.d.ts} +1 -1
- package/lib/{message-source.d.ts → kernel/message-source.d.ts} +1 -1
- package/lib/{ports.d.ts → kernel/ports.d.ts} +1 -1
- package/lib/{time.d.ts → kernel/time.d.ts} +1 -1
- package/lib/message-source-kVoNK0C5.js +432 -0
- package/lib/{deepseek-vision-patch.d.ts → patches/deepseek-vision.d.ts} +3 -3
- package/lib/{opencode-provider.d.ts → patches/opencode-provider.d.ts} +1 -1
- package/lib/{web-fetch-provider.d.ts → patches/web-fetch.d.ts} +1 -1
- package/lib/{message-source-DhA1dVBi.js → profile-DyMJ_bfr.js} +277 -463
- package/lib/role/skiff/config.d.ts +79 -0
- package/lib/role/skiff/core.d.ts +155 -0
- package/lib/{skiff-debug.d.ts → role/skiff/debug.d.ts} +4 -4
- package/lib/role/skiff/profile.d.ts +115 -0
- package/lib/role/skiff/registry.d.ts +41 -0
- package/lib/{cheap-worker-ops.d.ts → role/worker/core.d.ts} +1 -1
- package/lib/{agent-idle.d.ts → role/worker/idle.d.ts} +2 -2
- package/lib/{cheap-worker-preset-inherit.d.ts → role/worker/preset.d.ts} +2 -2
- package/lib/{weixin-route-D_DvgPqy.js → route-f7JlinIt.js} +132 -47
- package/lib/seams/context.d.ts +3 -3
- package/lib/seams/guards.d.ts +18 -12
- package/lib/seams/keeper.d.ts +1 -1
- package/lib/seams/system-prompt.d.ts +3 -1
- package/lib/settings-lt1i3OZv.js +403 -0
- package/lib/{skills-discovery.d.ts → skills/discovery.d.ts} +1 -1
- package/lib/skills/resource-index.d.ts +2 -2
- package/lib/{face-host.d.ts → surface/faces.d.ts} +4 -4
- package/lib/{api.d.ts → surface/http.d.ts} +2 -2
- package/lib/{container-status.d.ts → surface/observation.d.ts} +16 -16
- package/lib/{settings-section.d.ts → surface/settings.d.ts} +1 -1
- package/lib/{status.d.ts → surface/status.d.ts} +2 -2
- package/lib/{usage-stats.d.ts → surface/usage.d.ts} +4 -4
- package/lib/{voyage-page.d.ts → surface/voyage/page.d.ts} +2 -2
- package/lib/{voyage-vocab.d.ts → surface/voyage/vocab.d.ts} +1 -1
- package/lib/tools/cognitive-trajectory.d.ts +2 -2
- package/lib/tools/{acc-diag.d.ts → diag/index.d.ts} +2 -2
- package/lib/{diag-ops.d.ts → tools/diag/service.d.ts} +7 -7
- package/lib/tools/eap.d.ts +1 -1
- package/lib/tools/fs/index.d.ts +7 -0
- package/lib/{fs-ops.d.ts → tools/fs/service.d.ts} +2 -2
- package/lib/tools/git/index.d.ts +4 -0
- package/lib/{git-ops.d.ts → tools/git/service.d.ts} +2 -2
- package/lib/tools/im-bridge.d.ts +2 -2
- package/lib/tools/{kit.d.ts → kit/index.d.ts} +1 -1
- package/lib/{kit-ops.d.ts → tools/kit/service.d.ts} +6 -6
- package/lib/tools/{localstore.d.ts → localstore/index.d.ts} +1 -1
- package/lib/{localstore-ops.d.ts → tools/localstore/service.d.ts} +2 -2
- package/lib/tools/{msm.d.ts → msm/index.d.ts} +1 -1
- package/lib/{msm-ops.d.ts → tools/msm/service.d.ts} +2 -2
- package/lib/tools/skiff-admin.d.ts +19 -14
- package/lib/{cognitive-trajectory-assistant.d.ts → trajectory/assistant.d.ts} +2 -2
- package/lib/{cognitive-trajectory-bound.d.ts → trajectory/bound.d.ts} +2 -2
- package/lib/{session-cleanup.d.ts → trajectory/cleanup.d.ts} +1 -1
- package/lib/{cognitive-trajectory-ops.d.ts → trajectory/core.d.ts} +55 -25
- package/lib/{live-sessions.d.ts → trajectory/live.d.ts} +1 -1
- package/lib/trajectory/prompt.d.ts +101 -0
- package/lib/{rebuild.d.ts → trajectory/rebuild.d.ts} +1 -1
- package/lib/{cognitive-trajectory-skills.d.ts → trajectory/skills.d.ts} +1 -1
- package/lib/{clock-runtime.d.ts → wake/clock.d.ts} +3 -3
- package/lib/{cro-guide.d.ts → wake/cro-guide.d.ts} +1 -1
- package/lib/{cro-log.d.ts → wake/cro-log.d.ts} +3 -3
- package/lib/{cro-turns.d.ts → wake/cro-turns.d.ts} +5 -5
- package/lib/{cro.d.ts → wake/cro.d.ts} +6 -6
- package/lib/{wake-registry.d.ts → wake/registry.d.ts} +2 -2
- package/lib/{wake-scheduler.d.ts → wake/scheduler.d.ts} +3 -3
- package/package.json +2 -2
- package/lib/constants.d.ts +0 -6
- package/lib/settings-section-0TPYWug9.js +0 -238
- package/lib/skiff-core.d.ts +0 -145
- package/lib/skiff-registry.d.ts +0 -26
- package/lib/skiff-role-cwxAt0gE.js +0 -203
- package/lib/skiff-role.d.ts +0 -89
- package/lib/tools/cc-fs.d.ts +0 -7
- package/lib/tools/git.d.ts +0 -4
package/lib/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.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, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-
|
|
4
|
-
import { A as
|
|
5
|
-
import { a as readWeixinCredential, c as
|
|
6
|
-
import { a as GATEWAY_PORT, c as SKIFF_DEBUG_PORT, d as
|
|
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-jZYR2GVE.js";
|
|
3
|
+
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, g as readUtf8, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-Dkoa2oW3.js";
|
|
4
|
+
import { A as sessionEvents$1, C as listSessions, D as readActiveSessionMd, E as parseTrajectoryCode, F as useSession, I as isoLocal, L as localFileStamp, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as localHuman, S as listActiveTrajectories, T as parseSessionContextFromEvents, _ as createSession, a as readSkiffProfileAt, b as getActiveSessionInfo, c as registerSkiffSession$1, d as unregisterSkiffSession$1, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, l as skiffSessionInfo$1, m as UID_FILENAME, n as isSkiffSession, o as readSkiffProfileByCode, p as DEFAULT_SESSION_SCOPE, r as listSkiffProfiles, s as skiffProfileForSession, t as SKIFF_PROFILE_FILENAME, u as skiffTrajectoryDirNameFor, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as localIdStamp } from "./profile-DyMJ_bfr.js";
|
|
5
|
+
import { a as readWeixinCredential, c as sanitizeFileName$1, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinInboundDir, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, t as extractWeixinMedia, u as weixinSessionIdForTrajectory } from "./route-f7JlinIt.js";
|
|
6
|
+
import { _ as resolveProfileSystemPrompt, a as GATEWAY_PORT, c as SKIFF_DEBUG_PORT, d as buildSkiffBasePrompt, f as createProfilePromptReader, g as profileToolWhitelist, h as profileSystemPromptSource, i as FACE_PORTS, l as WEIXIN_SEND_PORT, m as profileMsmWhitelist, n as readSimpleSettings, o as MAIN_WEB_PORT, p as profileCognitiveTrajectory, r as registerSettingsSection, s as MECHANISM_PORTS, u as SKIFF_PROFILE_HINT, v as ACC_VERSION, y as findPackageRoot } from "./settings-lt1i3OZv.js";
|
|
7
|
+
import { a as pruneMissingBindings, c as setBindingStore, i as listBoundSessionIds, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-kVoNK0C5.js";
|
|
8
8
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
9
|
-
import {
|
|
9
|
+
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-DEpmE3yi.js";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import z from "@deepseek-ai/schemastery";
|
|
12
12
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -25,9 +25,9 @@ import * as zlib from "node:zlib";
|
|
|
25
25
|
import { deriveEventMessage } from "@deepseek-ai/dsh-session";
|
|
26
26
|
import { lookup } from "node:dns/promises";
|
|
27
27
|
import { isIP } from "node:net";
|
|
28
|
-
//#region src/fs
|
|
28
|
+
//#region src/tools/fs/service.ts
|
|
29
29
|
/**
|
|
30
|
-
* fs
|
|
30
|
+
* tools/fs/service.ts — container_fs 纯操作层(cc_fs → container_fs,v1.30;零 DSH 依赖,可独立单测)
|
|
31
31
|
*
|
|
32
32
|
* 行为对齐 osp(opencode-serenity-plugin/src/fs/file-system-tool.ts)——osp 是 ACC 工具 spec:
|
|
33
33
|
* - rm:目录需 recursive 才删除;非空目录无 recursive → [SKIP];保护 .serenity 与 CCC 根;dry-run 预览
|
|
@@ -416,12 +416,12 @@ function runCcFs(root, args) {
|
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
//#endregion
|
|
419
|
-
//#region src/tools/
|
|
419
|
+
//#region src/tools/fs/index.ts
|
|
420
420
|
/**
|
|
421
|
-
*
|
|
421
|
+
* tools/fs/index.ts — container_fs 真实 DSH 工具定义(defineTool)(v1.30:cc_fs → container_fs)
|
|
422
422
|
*
|
|
423
423
|
* 进程内注册(取代 v0.1 的 bash spawn runner):zod/schemastery 参数校验、
|
|
424
|
-
* 规范 JSON 输出、纯 render 投影。逻辑在 fs
|
|
424
|
+
* 规范 JSON 输出、纯 render 投影。逻辑在 tools/fs/service.ts(可单测)。
|
|
425
425
|
*/
|
|
426
426
|
function renderText$11(value) {
|
|
427
427
|
return [{
|
|
@@ -504,7 +504,7 @@ const ccFsTool = defineTool({
|
|
|
504
504
|
}
|
|
505
505
|
});
|
|
506
506
|
//#endregion
|
|
507
|
-
//#region src/kit
|
|
507
|
+
//#region src/tools/kit/service.ts
|
|
508
508
|
const KIT_ACTIONS = [
|
|
509
509
|
"health",
|
|
510
510
|
"time",
|
|
@@ -513,7 +513,7 @@ const KIT_ACTIONS = [
|
|
|
513
513
|
async function runKit(root, args, hostCtx) {
|
|
514
514
|
switch (args.action) {
|
|
515
515
|
case "health": {
|
|
516
|
-
const { containerStatus } = await Promise.resolve().then(() =>
|
|
516
|
+
const { containerStatus } = await Promise.resolve().then(() => observation_exports);
|
|
517
517
|
const s = containerStatus({
|
|
518
518
|
root,
|
|
519
519
|
hostCtx
|
|
@@ -567,9 +567,9 @@ async function runKit(root, args, hostCtx) {
|
|
|
567
567
|
}
|
|
568
568
|
}
|
|
569
569
|
//#endregion
|
|
570
|
-
//#region src/tools/kit.ts
|
|
570
|
+
//#region src/tools/kit/index.ts
|
|
571
571
|
/**
|
|
572
|
-
* kit.ts — dashboard 真实 DSH 工具定义(defineTool)(v1.30:acc_kit → dashboard)
|
|
572
|
+
* tools/kit/index.ts — dashboard 真实 DSH 工具定义(defineTool)(v1.30:acc_kit → dashboard)
|
|
573
573
|
*/
|
|
574
574
|
function renderText$10(value) {
|
|
575
575
|
return [{
|
|
@@ -607,9 +607,9 @@ function createKitTool(ctx) {
|
|
|
607
607
|
});
|
|
608
608
|
}
|
|
609
609
|
//#endregion
|
|
610
|
-
//#region src/git
|
|
610
|
+
//#region src/tools/git/service.ts
|
|
611
611
|
/**
|
|
612
|
-
* git
|
|
612
|
+
* tools/git/service.ts — container_git 纯操作层(cc_git → container_git,v1.30;零 DSH 依赖)
|
|
613
613
|
*
|
|
614
614
|
* 行为对齐 osp(opencode-serenity-plugin/src/git/cc-git-tool.ts)——osp 是 ACC 工具 spec:
|
|
615
615
|
* - status:JSON {clean, files:[{status,file}], summary, ahead, behind}
|
|
@@ -910,9 +910,9 @@ function runGit(root, args) {
|
|
|
910
910
|
}
|
|
911
911
|
}
|
|
912
912
|
//#endregion
|
|
913
|
-
//#region src/tools/git.ts
|
|
913
|
+
//#region src/tools/git/index.ts
|
|
914
914
|
/**
|
|
915
|
-
* git.ts — container_git 真实 DSH 工具定义(defineTool)(v1.30:cc_git → container_git)
|
|
915
|
+
* tools/git/index.ts — container_git 真实 DSH 工具定义(defineTool)(v1.30:cc_git → container_git)
|
|
916
916
|
*/
|
|
917
917
|
function renderText$9(value) {
|
|
918
918
|
return [{
|
|
@@ -971,9 +971,9 @@ const gitTool = defineTool({
|
|
|
971
971
|
}
|
|
972
972
|
});
|
|
973
973
|
//#endregion
|
|
974
|
-
//#region src/
|
|
974
|
+
//#region src/im/endpoint.ts
|
|
975
975
|
/**
|
|
976
|
-
*
|
|
976
|
+
* im/endpoint.ts — 主动发送入口地址的**零依赖叶模块**(v1.30.9,S142)
|
|
977
977
|
*
|
|
978
978
|
* 为什么单独一个模块(R↓):地址的读者是 `msm-ops.buildMsmEnv`(把地址注入 MSM 子进程 env),
|
|
979
979
|
* 写者才是 `weixin-send-api`(监听器启停)。若 msm-ops 直接 import weixin-send-api,
|
|
@@ -981,7 +981,7 @@ const gitTool = defineTool({
|
|
|
981
981
|
* 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
|
|
982
982
|
* (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
|
|
983
983
|
*
|
|
984
|
-
* 同款先例:`host/effect.ts`(拆卸登记)、`
|
|
984
|
+
* 同款先例:`host/effect.ts`(拆卸登记)、`role/worker/idle.ts`(等待结算)。
|
|
985
985
|
*/
|
|
986
986
|
let endpoint = null;
|
|
987
987
|
/** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
|
|
@@ -993,9 +993,9 @@ function weixinSendEndpoint() {
|
|
|
993
993
|
return endpoint;
|
|
994
994
|
}
|
|
995
995
|
//#endregion
|
|
996
|
-
//#region src/msm
|
|
996
|
+
//#region src/tools/msm/service.ts
|
|
997
997
|
/**
|
|
998
|
-
* msm
|
|
998
|
+
* tools/msm/service.ts — msm 纯操作层(acc_msm → msm 执行 + container_admin 管理面,v1.30;零 DSH 依赖)
|
|
999
999
|
*
|
|
1000
1000
|
* MSM(Mech & Semi-Mech)框架:exec 执行 / register|deregister|check 管理。
|
|
1001
1001
|
* 复用 CCC 的 mech-registry.json(v1 或数组格式)。cwd 钉在 CCC 根。
|
|
@@ -1023,7 +1023,7 @@ function bunExecutablePath() {
|
|
|
1023
1023
|
bunExeCache = null;
|
|
1024
1024
|
return null;
|
|
1025
1025
|
}
|
|
1026
|
-
/** CCC 名:从 .serenity 解析(review P2-2 统一收口到 ccc.ts readCccName——跳 # 注释/空行首非空行;此处保留导出名兼容内部调用) */
|
|
1026
|
+
/** CCC 名:从 .serenity 解析(review P2-2 统一收口到 kernel/ccc.ts readCccName——跳 # 注释/空行首非空行;此处保留导出名兼容内部调用) */
|
|
1027
1027
|
function readCccName(root) {
|
|
1028
1028
|
return readCccName$1(root);
|
|
1029
1029
|
}
|
|
@@ -1823,149 +1823,142 @@ async function runMsmAsync(root, args) {
|
|
|
1823
1823
|
}
|
|
1824
1824
|
}
|
|
1825
1825
|
//#endregion
|
|
1826
|
-
//#region src/
|
|
1826
|
+
//#region src/trajectory/prompt.ts
|
|
1827
1827
|
/**
|
|
1828
|
-
*
|
|
1828
|
+
* trajectory/prompt.ts — **轨迹提示词**(Trajectory Prompt):一条轨迹"是谁、按什么章程工作"
|
|
1829
1829
|
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
1830
|
+
* ## 概念(owner 2026-10-19 定向,逐字)
|
|
1831
|
+
*
|
|
1832
|
+
* > 「参考会话的系统提示词,我们建立**轨迹提示词**概念来代替人格。架构上会好很多」
|
|
1833
|
+
*
|
|
1834
|
+
* **轨迹提示词** = 一条认知轨迹的**可编辑章程声明**,由 ACC 按固定顺序装配进系统提示词。
|
|
1835
|
+
* 三个关键词各是一条判据:
|
|
1836
|
+
* - **可编辑**:住在 CCC(盘上),改它**不发版**(D23 归属判据的直接推论)
|
|
1837
|
+
* - **声明**:它声明**身份/章程/方法论**,**不声明能力**(能力面归 `skiff.json` 白名单与工具 schema)
|
|
1838
|
+
* - **固定顺序**:装配顺序由 ACC 定死(见 `seams/system-prompt.ts` 的 ⑧ 位)
|
|
1839
|
+
*
|
|
1840
|
+
* ## 为什么取代「人格」
|
|
1841
|
+
*
|
|
1842
|
+
* 「人格」把身份绑在"像不像一个人"上 ⇒ 因为叫人格,它被设计成**可以整体替换**
|
|
1843
|
+
* (`persona` 置换 EAP 块,即 review F-05/R-14 那个悬置冲突)。而身份与章程**不该被替换,
|
|
1844
|
+
* 只该被特化**。⇒ 本模块把"替换"改成"叠加":容器层与质量层永远保留,轨迹提示词只往上加。
|
|
1845
|
+
*
|
|
1846
|
+
* ## 落点(为什么在轨迹目录里)
|
|
1847
|
+
*
|
|
1848
|
+
* `AGENT_SESSIONS/<轨迹目录>/PROMPT.md`
|
|
1849
|
+
*
|
|
1850
|
+
* - **跟着轨迹走**:`archive` 时整个目录搬走,章程不会掉队(旧形态 `.opencode/skiff/<name>.md`
|
|
1851
|
+
* 与轨迹**分居两处** —— 多条轨迹共用一份文件正是那个后果)。
|
|
1852
|
+
* - **每条轨迹都能有**:不只是 skiff。普通轨迹同样需要"我是谁、按什么章程"。
|
|
1853
|
+
* - 与 `SESSION.md`(身体:延续/日志)、`skiff.json`(约束面:能力)三件并列,
|
|
1854
|
+
* 各管一件事,不合并(合并会让"改能力"与"改身份"互相牵连)。
|
|
1855
|
+
*
|
|
1856
|
+
* ## 兼容(不静默坏)
|
|
1857
|
+
*
|
|
1858
|
+
* `PROMPT.md` 不存在时**不报错**(普通轨迹常态)。skiff 的旧字段 `systemPromptFile`
|
|
1859
|
+
* 在兼容期仍被读取——优先级见 {@link resolveTrajectoryPrompt}。
|
|
1833
1860
|
*/
|
|
1834
|
-
/**
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
model
|
|
1840
|
-
};
|
|
1841
|
-
return {
|
|
1842
|
-
provider: model.slice(0, idx),
|
|
1843
|
-
model: model.slice(idx + 1)
|
|
1844
|
-
};
|
|
1861
|
+
/** 轨迹提示词的固定文件名(**判据就是它存在**——不是目录名前缀,也不是配置里的一行) */
|
|
1862
|
+
const TRAJECTORY_PROMPT_FILENAME = "PROMPT.md";
|
|
1863
|
+
/** 轨迹目录的 PROMPT.md 绝对路径(不论存在与否——调用方自己判存在性) */
|
|
1864
|
+
function trajectoryPromptPath(root, dirName) {
|
|
1865
|
+
return join(sessionsRoot(root), dirName, TRAJECTORY_PROMPT_FILENAME);
|
|
1845
1866
|
}
|
|
1846
|
-
/**
|
|
1847
|
-
|
|
1848
|
-
|
|
1867
|
+
/**
|
|
1868
|
+
* 该轨迹**有没有**轨迹提示词(单一判据)。
|
|
1869
|
+
*
|
|
1870
|
+
* 用途:兼容期的"让位"判定 —— 若已有 `PROMPT.md`,skiff 旧字段 `systemPromptFile`
|
|
1871
|
+
* 必须**让位**,否则同一份章程会在两处各注入一遍(第 ⑧ 段 + `serenity-skiff` 段)。
|
|
1872
|
+
* 🔴 判据只有这一处:别在 skiff 那边自己拼路径再判(两处会漂)。
|
|
1873
|
+
*/
|
|
1874
|
+
function hasTrajectoryPrompt(root, dirName) {
|
|
1875
|
+
try {
|
|
1876
|
+
return existsSync(trajectoryPromptPath(root, dirName));
|
|
1877
|
+
} catch {
|
|
1878
|
+
return false;
|
|
1879
|
+
}
|
|
1849
1880
|
}
|
|
1850
1881
|
/**
|
|
1851
|
-
*
|
|
1852
|
-
* 返回 `null` = 全部可用(调用方照常继续)。
|
|
1882
|
+
* 轨迹提示词的**读取器**(长期热更新;改文件即生效,无需重启)。
|
|
1853
1883
|
*
|
|
1854
|
-
*
|
|
1855
|
-
* **CCC**(`.opencode/serenity.json`,**随 git 走**),而 provider adapter 由**每台机器的宿主**
|
|
1856
|
-
* 注册(profile 配置 / settings 命名空间)⇒ 同一份白名单在一台机器可用、在另一台必失败。
|
|
1857
|
-
* 实测症状:宿主抛 `no adapter registered for provider "<p>"`(`dsh-llm`,code `NO_ADAPTER`),
|
|
1858
|
-
* 而**前台路径的 `diagnostic` 是空的** ⇒ 调用方(LLM)拿到 `stopReason:"error"` 却**不知道为什么**,
|
|
1859
|
-
* 也就无法修正自己的配置 —— 本条把「沉默的失败」变成「可执行的错误」。
|
|
1884
|
+
* ## 为什么返回函数而不是字符串(R↓,实证代价)
|
|
1860
1885
|
*
|
|
1861
|
-
*
|
|
1862
|
-
* `
|
|
1886
|
+
* 旧实现把提示词**快照**进系统提示词段的闭包 ⇒ CCC 改了文件必须重启 dsh web 才生效
|
|
1887
|
+
* (实测:改完 18KB 提示词,live agent 仍用旧文本)。现形态由系统提示词段的 `text()`
|
|
1888
|
+
* **每轮调用**本读取器:按 **mtime + size** 缓存重读(命中缓存零 IO)。
|
|
1863
1889
|
*
|
|
1864
|
-
*
|
|
1890
|
+
* ## 三条失败语义(各治一个失败模式)
|
|
1891
|
+
*
|
|
1892
|
+
* - **文件不存在** ⇒ 返回空串(**不是错误**:普通轨迹没有章程是常态,且删掉即失效
|
|
1893
|
+
* 必须是立即可见的——这就是"删掉 PROMPT.md 即恢复默认"的机制)
|
|
1894
|
+
* - **读取失败** ⇒ 返回**上次成功内容**(⛔ 绝不返回空:空提示词会让轨迹失去章程),
|
|
1895
|
+
* 并按去重标签告警一次
|
|
1896
|
+
* - **首次就读不到** ⇒ 空串 + 告警一次(区分"本来没有"与"有但读不了")
|
|
1897
|
+
*
|
|
1898
|
+
* @param root CCC 根
|
|
1899
|
+
* @param dirName 轨迹目录名(**不是编码**:目录名才是落盘位置)
|
|
1900
|
+
*/
|
|
1901
|
+
function createTrajectoryPromptReader(root, dirName) {
|
|
1902
|
+
const absPath = trajectoryPromptPath(root, dirName);
|
|
1903
|
+
let cachedMtime = -1;
|
|
1904
|
+
let cachedSize = -1;
|
|
1905
|
+
let cachedText = "";
|
|
1906
|
+
let warned = false;
|
|
1907
|
+
return () => {
|
|
1908
|
+
let st;
|
|
1909
|
+
try {
|
|
1910
|
+
st = statSync(absPath);
|
|
1911
|
+
} catch (err) {
|
|
1912
|
+
const code = err?.code;
|
|
1913
|
+
cachedMtime = -1;
|
|
1914
|
+
cachedSize = -1;
|
|
1915
|
+
cachedText = "";
|
|
1916
|
+
if (code !== "ENOENT" && !warned) {
|
|
1917
|
+
warned = true;
|
|
1918
|
+
console.warn(`[serenity-hooks] ✗ 轨迹提示词不可读(${absPath})——按"没有章程"处理: ${String(err?.message ?? err)}`);
|
|
1919
|
+
}
|
|
1920
|
+
return "";
|
|
1921
|
+
}
|
|
1922
|
+
try {
|
|
1923
|
+
if (st.mtimeMs === cachedMtime && st.size === cachedSize) return cachedText;
|
|
1924
|
+
const text = readUtf8(absPath).trim();
|
|
1925
|
+
const isReload = cachedMtime !== -1;
|
|
1926
|
+
cachedMtime = st.mtimeMs;
|
|
1927
|
+
cachedSize = st.size;
|
|
1928
|
+
cachedText = text;
|
|
1929
|
+
warned = false;
|
|
1930
|
+
if (isReload) console.info(`[serenity-hooks] ↻ 轨迹提示词已热重载(${TRAJECTORY_PROMPT_FILENAME},${text.length} 字符)`);
|
|
1931
|
+
return text;
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
if (!warned) {
|
|
1934
|
+
warned = true;
|
|
1935
|
+
console.warn(`[serenity-hooks] ✗ 轨迹提示词读取失败(${absPath})——沿用上次成功内容: ${String(err?.message ?? err)}`);
|
|
1936
|
+
}
|
|
1937
|
+
return cachedText;
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* 装配用的**提示词段**(幂等:无内容 ⇒ 空串,宿主会丢弃空 section/空块)。
|
|
1943
|
+
*
|
|
1944
|
+
* 标记头 `=== Serenity Trajectory Prompt ===`:
|
|
1945
|
+
* ① 让模型知道"下面是**这条轨迹自己的章程**",不是容器规则;
|
|
1946
|
+
* ② 让诊断可以按块名 grep。
|
|
1947
|
+
*
|
|
1948
|
+
* 🔴 这段**追加**在容器层与质量层之后,**永不替换它们**——那正是本概念相对「人格」的核心修正。
|
|
1865
1949
|
*/
|
|
1866
|
-
function
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
raw: m,
|
|
1870
|
-
provider: splitModel(m).provider
|
|
1871
|
-
})).filter((e) => typeof e.provider === "string" && e.provider !== "");
|
|
1872
|
-
const missing = [...new Set(requested.filter((e) => !registered.has(e.provider)).map((e) => e.provider))];
|
|
1873
|
-
if (missing.length === 0) return null;
|
|
1874
|
-
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` : "");
|
|
1875
|
-
const available = facts.registered.map((r) => r.name ? `${r.id} (${r.name})` : r.id).sort();
|
|
1876
|
-
const declared = (facts.declared ?? []).filter((d) => missing.includes(d.provider));
|
|
1877
|
-
const configPath = facts.configPath ?? "<ccc>/.opencode/serenity.json";
|
|
1878
|
-
const missingList = missing.map((p) => `"${p}"`).join(", ");
|
|
1950
|
+
function trajectoryPromptBlock(text) {
|
|
1951
|
+
const body = text.trim();
|
|
1952
|
+
if (body === "") return "";
|
|
1879
1953
|
return [
|
|
1880
|
-
`cheap-worker: provider ${missingList} has no adapter registered on this machine.`,
|
|
1881
|
-
"",
|
|
1882
|
-
" Requested model(s) :",
|
|
1883
|
-
...requested.map((e) => ` ${e.raw}`),
|
|
1884
|
-
` Configured in : ${configPath} → "cheap-worker.models" / "cheap-worker.defaultModel"`,
|
|
1885
|
-
"",
|
|
1886
|
-
" Why this happens : the whitelist lives in the CCC (it travels with the repository), while",
|
|
1887
|
-
" provider adapters are registered per machine by the host. An entry that",
|
|
1888
|
-
" works on one machine can fail on another.",
|
|
1889
|
-
"",
|
|
1890
|
-
" Available on this machine (registered adapters):",
|
|
1891
|
-
list(available),
|
|
1892
|
-
"",
|
|
1893
|
-
" Declared but NOT active here (activate via settings, then retry):",
|
|
1894
|
-
list(declared.map((d) => `${d.provider}${d.displayName ? ` (${d.displayName})` : ""}${d.settingsNs ? ` — settings namespace "${d.settingsNs}"` : ""}`)),
|
|
1895
|
-
"",
|
|
1896
|
-
" Fix (pick one, then retry):",
|
|
1897
|
-
` 1. Point the whitelist at a provider that IS registered here — edit ${configPath}`,
|
|
1898
|
-
" (\"cheap-worker.models\", and \"cheap-worker.defaultModel\" if that is what you relied on).",
|
|
1899
|
-
` 2. Register/activate provider ${missingList} on this machine (host profile config,`,
|
|
1900
|
-
" or the settings namespace listed above).",
|
|
1901
1954
|
"",
|
|
1902
|
-
"
|
|
1955
|
+
"=== Serenity Trajectory Prompt ===",
|
|
1956
|
+
body,
|
|
1957
|
+
""
|
|
1903
1958
|
].join("\n");
|
|
1904
1959
|
}
|
|
1905
|
-
/**
|
|
1906
|
-
* cheap-worker 规模化使用指引(guide 子命令输出)。
|
|
1907
|
-
*
|
|
1908
|
-
* 🆕 2026-10-14:**双模式段已整段退场**(owner 令「删除后台运行能力」)。本指引只描述现行机制:
|
|
1909
|
-
* 前台委派(单任务)/前台并行扇出(jobs)/白名单与 provider 预检。
|
|
1910
|
-
*/
|
|
1911
|
-
const CHEAP_WORKER_GUIDE = `# cheap-worker — Scale-Up Usage Guide (guide)
|
|
1912
|
-
|
|
1913
|
-
## What it is
|
|
1914
|
-
One delegation primitive: **foreground spawn**. The host starts child agent(s) through its delegation
|
|
1915
|
-
service, awaits them, and returns their final text. There is no loop, no completion code, no progress
|
|
1916
|
-
file, and no unattended/background run.
|
|
1917
|
-
|
|
1918
|
-
| | single | parallel (jobs) |
|
|
1919
|
-
|---|---|---|
|
|
1920
|
-
| Call | cheap-worker(task, [label]) | cheap-worker(jobs=[{task, label?}, ...]) |
|
|
1921
|
-
| Behaviour | one child runs the task once | all children start at once, results keep your order |
|
|
1922
|
-
| Cap | — | **10 (fixed, hardcoded)** |
|
|
1923
|
-
| Model | CCC config (single source) | same |
|
|
1924
|
-
|
|
1925
|
-
## ⚠️ Before using: load eap and design the plan
|
|
1926
|
-
Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up cheap-worker plan" based on the EAP framework.
|
|
1927
|
-
|
|
1928
|
-
### 1. Task decomposition (E↑ Explicit)
|
|
1929
|
-
- Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
|
|
1930
|
-
- Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel via \`jobs\`
|
|
1931
|
-
|
|
1932
|
-
### 2. Prompt design (cheap-worker's task parameter)
|
|
1933
|
-
- task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
|
|
1934
|
-
- the child does NOT share your conversation — restate everything it needs in task
|
|
1935
|
-
- Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
|
|
1936
|
-
output a JSON array (fields id/conclusion/evidence), do not modify the original file"
|
|
1937
|
-
- Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
|
|
1938
|
-
the cheap-worker-internal agent is also required to load eap and organize output per the EAP standard
|
|
1939
|
-
|
|
1940
|
-
### 3. Model (CCC-configured, not selectable per call)
|
|
1941
|
-
- The model is read from .opencode/serenity.json "cheap-worker.models" — **the caller cannot pass one**
|
|
1942
|
-
(there is no "model" parameter). "cheap-worker.defaultModel" names which entry is used; it falls back to models[0].
|
|
1943
|
-
- The CCC is expected to point this whitelist at LOW-COST models: cheap-worker is the bulk-execution channel
|
|
1944
|
-
- A child's own recursive subagents inherit the child's model automatically (DSH native)
|
|
1945
|
-
|
|
1946
|
-
### 3b. 🔴 Provider availability (per-machine — the whitelist is NOT enough)
|
|
1947
|
-
- The whitelist lives in the **CCC** ("serenity.json", it **travels with the repository**), but provider
|
|
1948
|
-
**adapters are registered per machine by the host** (profile config / settings namespace). An entry that
|
|
1949
|
-
works on one machine can fail on another with: "no adapter registered for provider <p>".
|
|
1950
|
-
- Before spawning, cheap-worker verifies the provider of the resolved model against the adapters registered
|
|
1951
|
-
**on this machine**; if it is missing it reports the requested model, where it is configured, **the
|
|
1952
|
-
providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the
|
|
1953
|
-
exact fix — delivered in the result's "diagnostic" field (never an opaque failure, never a created child).
|
|
1954
|
-
- ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "cheap-worker.models"
|
|
1955
|
-
(and "cheap-worker.defaultModel") to a provider listed as available, or activate/register the provider on this
|
|
1956
|
-
machine. **Do not retry the same call unchanged** — it fails identically.
|
|
1957
|
-
|
|
1958
|
-
### 4. Parallel strategy
|
|
1959
|
-
- Independent subtasks: cheap-worker(jobs=[{task,label?},...]) ⇒ fan-out, **cap 10**, results come back
|
|
1960
|
-
**in the order you passed them** (label is only a display name).
|
|
1961
|
-
- For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead.
|
|
1962
|
-
|
|
1963
|
-
## Completion criteria
|
|
1964
|
-
- The child's run settles; done=true only when stopReason="completed" — otherwise read "diagnostic"
|
|
1965
|
-
(and "output" for whatever partial text was produced).
|
|
1966
|
-
`;
|
|
1967
1960
|
//#endregion
|
|
1968
|
-
//#region src/
|
|
1961
|
+
//#region src/role/worker/idle.ts
|
|
1969
1962
|
/**
|
|
1970
1963
|
* 等待 agent 空闲或销毁。
|
|
1971
1964
|
* @param ctx 宿主插件上下文(事件通道来源;**结构化访问**,故类型为 `unknown`,见文件头 I2 段)
|
|
@@ -2006,9 +1999,9 @@ function waitAgentIdle(ctx, agent) {
|
|
|
2006
1999
|
});
|
|
2007
2000
|
}
|
|
2008
2001
|
//#endregion
|
|
2009
|
-
//#region src/skills
|
|
2002
|
+
//#region src/skills/discovery.ts
|
|
2010
2003
|
/**
|
|
2011
|
-
* skills
|
|
2004
|
+
* skills/discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
|
|
2012
2005
|
*
|
|
2013
2006
|
* 发现全部入口技能(原文全量,不截断)—— **现行三个来源**(原为四个,**来源 3 已于 2026-09-21 删除**):
|
|
2014
2007
|
* 0. **`.serenity` 记号文件内容 = 顶层入口 skill 名**(CCC 记号文件的权威语义)—— 最高优先
|
|
@@ -2112,7 +2105,7 @@ function truncateContent(content, maxChars) {
|
|
|
2112
2105
|
* 六条硬性规则(逐条对应设计 §4.2,改这里先改那里):
|
|
2113
2106
|
* ① **只枚举,不注入正文** —— 索引里**绝不**出现任何参考文件的正文;
|
|
2114
2107
|
* ② **确定性排序** = 相对路径的 **UTF-8 字节序**(🔴 **不用 mtime**:mtime 随检出漂移,
|
|
2115
|
-
* 会让每请求的注入文本抖动;对照 `src/cro.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
|
|
2108
|
+
* 会让每请求的注入文本抖动;对照 `src/wake/cro.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
|
|
2116
2109
|
* **同一件事两处的取舍相反,那个差异是判据不是巧合**);
|
|
2117
2110
|
* ③ **有界 + 响亮** —— 条数/字符各有上限,超出**必须显式写出还剩多少项**,**绝不静默截断**;
|
|
2118
2111
|
* ④ **摘要可复现** —— 取该文件首个 `#` 标题(剥 markdown 记号);取不到 ⇒ 留空,**不编**;
|
|
@@ -2128,7 +2121,7 @@ function truncateContent(content, maxChars) {
|
|
|
2128
2121
|
*
|
|
2129
2122
|
* 🔴 **两个面,两个入口**(2026-10-12 增;owner 裁决 O2)—— 同源、但成本形态不同:
|
|
2130
2123
|
* · **载入面** `buildResourceIndex()` —— 完整索引(名字 + 首标题 + 字节),带 `.md` 文件头读取;
|
|
2131
|
-
* 在三条路径上各附一次:入口段(`system-prompt.ts` order −50)/轨迹段(`
|
|
2124
|
+
* 在三条路径上各附一次:入口段(`system-prompt.ts` order −50)/轨迹段(`trajectory/skills.ts`
|
|
2132
2125
|
* order −45)/目录 provider 的 `get()`(`opencode-skills.ts`)。
|
|
2133
2126
|
* · **目录面** `buildResourceNameSuffix()` —— **只有名字**,**不读任何正文**;只挂一处:
|
|
2134
2127
|
* 目录 provider 的 `list()`(`available_skills` 的 `SkillSummary.description`)。
|
|
@@ -2389,9 +2382,9 @@ function buildResourceNameSuffix(skillDir, options = {}) {
|
|
|
2389
2382
|
return `(参考件:${names.join(" / ")}${tail})`;
|
|
2390
2383
|
}
|
|
2391
2384
|
//#endregion
|
|
2392
|
-
//#region src/
|
|
2385
|
+
//#region src/trajectory/skills.ts
|
|
2393
2386
|
/**
|
|
2394
|
-
*
|
|
2387
|
+
* trajectory/skills.ts — 轨迹 skill 声明解析 + 注入内容装配(纯逻辑,零 DSH 依赖)
|
|
2395
2388
|
*
|
|
2396
2389
|
* 接口真相源:`docs/cognitive-trajectory-skill-injection.md` v1.0(§2 数据契约 / §3 注入契约)。
|
|
2397
2390
|
* 一句话:**轨迹自己声明它要挂哪些 skill**(`SESSION.md` 顶部 YAML frontmatter 的 `skills:`),
|
|
@@ -2589,9 +2582,9 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
|
|
|
2589
2582
|
return truncateContent(notice ? `${notice}\n\n${body}` : body, maxChars);
|
|
2590
2583
|
}
|
|
2591
2584
|
//#endregion
|
|
2592
|
-
//#region src/usage
|
|
2585
|
+
//#region src/surface/usage.ts
|
|
2593
2586
|
/**
|
|
2594
|
-
* usage
|
|
2587
|
+
* surface/usage.ts — ACC 用量统计(**skill 加载次数** + **MSM 执行次数**;落该 CCC 的 `_tmp`)
|
|
2595
2588
|
*
|
|
2596
2589
|
* ## 所有者令(本模块的存在理由)
|
|
2597
2590
|
*
|
|
@@ -2607,7 +2600,7 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
|
|
|
2607
2600
|
* ⇒ 本模块让每个 CCC 留下一份**纯计数**账本:**用了哪个、多少次、第一次和最近一次是什么时候**。
|
|
2608
2601
|
* 用途 = 清理/精简的**依据**(没有人用的东西与天天在用的东西,不再靠猜)。
|
|
2609
2602
|
*
|
|
2610
|
-
* ## 🔴 铁律(与 `cro-log.ts` 同族)
|
|
2603
|
+
* ## 🔴 铁律(与 `wake/cro-log.ts` 同族)
|
|
2611
2604
|
*
|
|
2612
2605
|
* **本模块每个导出函数都不抛错**:读盘 / 解析 / 写盘失败一律返回结构化失败,
|
|
2613
2606
|
* 由调用方只记一行日志(或什么都不做)⇒ **统计的问题绝不影响工具执行与既有链路**。
|
|
@@ -2622,8 +2615,8 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
|
|
|
2622
2615
|
* ## 🔴 重启后必须续算(所有者令)
|
|
2623
2616
|
*
|
|
2624
2617
|
* 宿主会被重启(本容器实测 24h 内 3 次)⇒ 计数**不能只活在内存里**:
|
|
2625
|
-
* 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake
|
|
2626
|
-
* `cro-log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
|
|
2618
|
+
* 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake/registry.ts` /
|
|
2619
|
+
* `wake/cro-log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
|
|
2627
2620
|
*
|
|
2628
2621
|
* ## 口径(两个字段,勿合并)
|
|
2629
2622
|
*
|
|
@@ -2906,9 +2899,9 @@ function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
|
|
|
2906
2899
|
return null;
|
|
2907
2900
|
}
|
|
2908
2901
|
//#endregion
|
|
2909
|
-
//#region src/config
|
|
2902
|
+
//#region src/kernel/config.ts
|
|
2910
2903
|
/**
|
|
2911
|
-
* config
|
|
2904
|
+
* kernel/config.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
|
|
2912
2905
|
*
|
|
2913
2906
|
* 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
|
|
2914
2907
|
* 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
|
|
@@ -3583,7 +3576,7 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
3583
3576
|
* Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
|
|
3584
3577
|
* 操作边界(安全硬约束)永远保留。未配置 → 与 v1.23.0 逐字节一致(零影响)。
|
|
3585
3578
|
*/
|
|
3586
|
-
function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
3579
|
+
function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE, dirName) {
|
|
3587
3580
|
const persona = readPersonaSettings();
|
|
3588
3581
|
const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
|
|
3589
3582
|
const parts = [
|
|
@@ -3591,12 +3584,18 @@ function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
3591
3584
|
metaphorBlock(),
|
|
3592
3585
|
principlesBlock(root, personaOn),
|
|
3593
3586
|
cceBlock(),
|
|
3594
|
-
|
|
3587
|
+
eapBlock()
|
|
3595
3588
|
];
|
|
3589
|
+
if (personaOn) {
|
|
3590
|
+
const expr = personaBlock(persona.mode, persona.overrideText);
|
|
3591
|
+
if (expr !== "") parts.push(expr);
|
|
3592
|
+
}
|
|
3596
3593
|
const state = [safeModeBlock(root), localstoreBlock(root)].filter((b) => b !== "").join("\n");
|
|
3597
3594
|
if (state) parts.push(state);
|
|
3598
3595
|
const skill = entrySkillSectionText(root);
|
|
3599
3596
|
if (skill) parts.push(skill);
|
|
3597
|
+
const trajPrompt = dirName === void 0 ? "" : trajectoryPromptBlock(createTrajectoryPromptReader(root, dirName)());
|
|
3598
|
+
if (trajPrompt !== "") parts.push(trajPrompt);
|
|
3600
3599
|
parts.push(toolsBlock());
|
|
3601
3600
|
const session = sessionBlock(root, scope);
|
|
3602
3601
|
if (session) parts.push(session);
|
|
@@ -3643,6 +3642,23 @@ function agentScope$2(context) {
|
|
|
3643
3642
|
return (context.agent?.session)?.id ?? "default";
|
|
3644
3643
|
}
|
|
3645
3644
|
/**
|
|
3645
|
+
* 该会话**绑定的轨迹目录名**(有 ⇒ 注入第 ⑧ 段「轨迹提示词」;无 ⇒ `undefined`,不注入)。
|
|
3646
|
+
*
|
|
3647
|
+
* 🔴 为什么用**目录名**而不是编码或 label:`PROMPT.md` 的落盘位置是目录,
|
|
3648
|
+
* 而编码只是目录名里的一段。用 label 拼路径 = 把模糊匹配当精确匹配用(本轨已踩过)。
|
|
3649
|
+
*
|
|
3650
|
+
* ⚠️ 每次装配都重读绑定(**不缓存**):绑定切换后提示词必须立刻跟上,
|
|
3651
|
+
* 这与 `serenity-trajectory-skills` section 的口径一致(动态注入,owner 令 2026-09-20)。
|
|
3652
|
+
*/
|
|
3653
|
+
function boundDirNameFor(session) {
|
|
3654
|
+
try {
|
|
3655
|
+
const name = readLastBound(session)?.dirName;
|
|
3656
|
+
return typeof name === "string" && name !== "" ? name : void 0;
|
|
3657
|
+
} catch {
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
/**
|
|
3646
3662
|
* 全局注册系统提示词 section(osp system.transform 的 DSH 等价)。
|
|
3647
3663
|
* text 回调按 context.agent 的 cwd 上溯 .serenity → 返回该 CCC 的完整注入文本;
|
|
3648
3664
|
* 非 CCC / 无 agent 返回空(不注入)。
|
|
@@ -3657,9 +3673,7 @@ function registerEntrySkillSectionGlobal(ctx) {
|
|
|
3657
3673
|
if (!cwd) return "";
|
|
3658
3674
|
const root = cccRootForCwd(cwd);
|
|
3659
3675
|
if (!root) return "";
|
|
3660
|
-
const
|
|
3661
|
-
if (isSkiffSessionId(scope)) return "";
|
|
3662
|
-
const base = serenitySystemPrompt(root, scope);
|
|
3676
|
+
const base = serenitySystemPrompt(root, agentScope$2(context), boundDirNameFor(context.agent?.session));
|
|
3663
3677
|
const codeLine = codeModeAdaptationLine(ctx, context.scope);
|
|
3664
3678
|
return codeLine ? `${base}\n${codeLine}` : base;
|
|
3665
3679
|
}
|
|
@@ -3691,8 +3705,7 @@ function registerEntrySkillSection(agent, root) {
|
|
|
3691
3705
|
name: "serenity-entry",
|
|
3692
3706
|
order: -50,
|
|
3693
3707
|
text: (context) => {
|
|
3694
|
-
|
|
3695
|
-
const base = serenitySystemPrompt(root, scope);
|
|
3708
|
+
const base = serenitySystemPrompt(root, scope, boundDirNameFor(agent.session));
|
|
3696
3709
|
const codeLine = codeModeAdaptationLine(agent.ctx, context.scope);
|
|
3697
3710
|
return codeLine ? `${base}\n${codeLine}` : base;
|
|
3698
3711
|
}
|
|
@@ -3712,7 +3725,7 @@ function registerEntrySkillSection(agent, root) {
|
|
|
3712
3725
|
* · **轨迹级** = 该会话绑定的轨迹(`readLastBound(session).mdPath` = 它的 `SESSION.md`)
|
|
3713
3726
|
* 顶部 frontmatter 的 `skills:`——这条轨迹额外的。
|
|
3714
3727
|
* 合并规则与顺序见 `cognitive-trajectory-skills.mergeSkillNames`(CCC 级在前,同名去重)。
|
|
3715
|
-
* 正文 = 合并清单里各 skill 的 `SKILL.md` 全文(本模块只注册,装配在 `
|
|
3728
|
+
* 正文 = 合并清单里各 skill 的 `SKILL.md` 全文(本模块只注册,装配在 `trajectory/skills.ts`)。
|
|
3716
3729
|
*
|
|
3717
3730
|
* 为什么是 **per-agent scoped section**(与 `registerEntrySkillSection` 同形):注入必须
|
|
3718
3731
|
* **绑定期间持续存在**(a2)——一次性注入会随对话压缩消失,而 section 每次装配重新求值。
|
|
@@ -3755,13 +3768,152 @@ function registerCognitiveTrajectorySkillSection(agent, root) {
|
|
|
3755
3768
|
}
|
|
3756
3769
|
}
|
|
3757
3770
|
//#endregion
|
|
3758
|
-
//#region src/
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3771
|
+
//#region src/role/worker/core.ts
|
|
3772
|
+
/**
|
|
3773
|
+
* role/worker/core.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
|
|
3774
|
+
*
|
|
3775
|
+
* 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
|
|
3776
|
+
* 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
|
|
3777
|
+
* 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
|
|
3778
|
+
*/
|
|
3779
|
+
/** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
|
|
3780
|
+
function splitModel(model) {
|
|
3781
|
+
const idx = model.indexOf("/");
|
|
3782
|
+
if (idx < 0) return {
|
|
3783
|
+
provider: void 0,
|
|
3784
|
+
model
|
|
3785
|
+
};
|
|
3786
|
+
return {
|
|
3787
|
+
provider: model.slice(0, idx),
|
|
3788
|
+
model: model.slice(idx + 1)
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
/** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
|
|
3792
|
+
function requireWhitelistedModel(model, models) {
|
|
3793
|
+
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(", ")}`);
|
|
3763
3794
|
}
|
|
3764
|
-
/**
|
|
3795
|
+
/**
|
|
3796
|
+
* providerAvailabilityError — 判定「**本机有没有该 provider 的 adapter**」,并产出**可执行**的错误文本。
|
|
3797
|
+
* 返回 `null` = 全部可用(调用方照常继续)。
|
|
3798
|
+
*
|
|
3799
|
+
* 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`cheap-worker` 的模型白名单写在
|
|
3800
|
+
* **CCC**(`.opencode/serenity.json`,**随 git 走**),而 provider adapter 由**每台机器的宿主**
|
|
3801
|
+
* 注册(profile 配置 / settings 命名空间)⇒ 同一份白名单在一台机器可用、在另一台必失败。
|
|
3802
|
+
* 实测症状:宿主抛 `no adapter registered for provider "<p>"`(`dsh-llm`,code `NO_ADAPTER`),
|
|
3803
|
+
* 而**前台路径的 `diagnostic` 是空的** ⇒ 调用方(LLM)拿到 `stopReason:"error"` 却**不知道为什么**,
|
|
3804
|
+
* 也就无法修正自己的配置 —— 本条把「沉默的失败」变成「可执行的错误」。
|
|
3805
|
+
*
|
|
3806
|
+
* 数据来源(宿主 `LlmRuntime`):`listProviders()` = **有 adapter 的路由**;
|
|
3807
|
+
* `listConfigurableProviders()` = **已声明但未必激活**的路由(含激活用的 settings 命名空间)。
|
|
3808
|
+
*
|
|
3809
|
+
* 判据边界:`model` 串里**没有 `/`** 时取不到 provider(沿用 agent 默认路由)⇒ **不判定**(跳过)。
|
|
3810
|
+
*/
|
|
3811
|
+
function providerAvailabilityError(models, facts) {
|
|
3812
|
+
const registered = new Set(facts.registered.map((r) => r.id));
|
|
3813
|
+
const requested = models.map((m) => ({
|
|
3814
|
+
raw: m,
|
|
3815
|
+
provider: splitModel(m).provider
|
|
3816
|
+
})).filter((e) => typeof e.provider === "string" && e.provider !== "");
|
|
3817
|
+
const missing = [...new Set(requested.filter((e) => !registered.has(e.provider)).map((e) => e.provider))];
|
|
3818
|
+
if (missing.length === 0) return null;
|
|
3819
|
+
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` : "");
|
|
3820
|
+
const available = facts.registered.map((r) => r.name ? `${r.id} (${r.name})` : r.id).sort();
|
|
3821
|
+
const declared = (facts.declared ?? []).filter((d) => missing.includes(d.provider));
|
|
3822
|
+
const configPath = facts.configPath ?? "<ccc>/.opencode/serenity.json";
|
|
3823
|
+
const missingList = missing.map((p) => `"${p}"`).join(", ");
|
|
3824
|
+
return [
|
|
3825
|
+
`cheap-worker: provider ${missingList} has no adapter registered on this machine.`,
|
|
3826
|
+
"",
|
|
3827
|
+
" Requested model(s) :",
|
|
3828
|
+
...requested.map((e) => ` ${e.raw}`),
|
|
3829
|
+
` Configured in : ${configPath} → "cheap-worker.models" / "cheap-worker.defaultModel"`,
|
|
3830
|
+
"",
|
|
3831
|
+
" Why this happens : the whitelist lives in the CCC (it travels with the repository), while",
|
|
3832
|
+
" provider adapters are registered per machine by the host. An entry that",
|
|
3833
|
+
" works on one machine can fail on another.",
|
|
3834
|
+
"",
|
|
3835
|
+
" Available on this machine (registered adapters):",
|
|
3836
|
+
list(available),
|
|
3837
|
+
"",
|
|
3838
|
+
" Declared but NOT active here (activate via settings, then retry):",
|
|
3839
|
+
list(declared.map((d) => `${d.provider}${d.displayName ? ` (${d.displayName})` : ""}${d.settingsNs ? ` — settings namespace "${d.settingsNs}"` : ""}`)),
|
|
3840
|
+
"",
|
|
3841
|
+
" Fix (pick one, then retry):",
|
|
3842
|
+
` 1. Point the whitelist at a provider that IS registered here — edit ${configPath}`,
|
|
3843
|
+
" (\"cheap-worker.models\", and \"cheap-worker.defaultModel\" if that is what you relied on).",
|
|
3844
|
+
` 2. Register/activate provider ${missingList} on this machine (host profile config,`,
|
|
3845
|
+
" or the settings namespace listed above).",
|
|
3846
|
+
"",
|
|
3847
|
+
" Do not retry unchanged — the same call fails identically."
|
|
3848
|
+
].join("\n");
|
|
3849
|
+
}
|
|
3850
|
+
/**
|
|
3851
|
+
* cheap-worker 规模化使用指引(guide 子命令输出)。
|
|
3852
|
+
*
|
|
3853
|
+
* 🆕 2026-10-14:**双模式段已整段退场**(owner 令「删除后台运行能力」)。本指引只描述现行机制:
|
|
3854
|
+
* 前台委派(单任务)/前台并行扇出(jobs)/白名单与 provider 预检。
|
|
3855
|
+
*/
|
|
3856
|
+
const CHEAP_WORKER_GUIDE = `# cheap-worker — Scale-Up Usage Guide (guide)
|
|
3857
|
+
|
|
3858
|
+
## What it is
|
|
3859
|
+
One delegation primitive: **foreground spawn**. The host starts child agent(s) through its delegation
|
|
3860
|
+
service, awaits them, and returns their final text. There is no loop, no completion code, no progress
|
|
3861
|
+
file, and no unattended/background run.
|
|
3862
|
+
|
|
3863
|
+
| | single | parallel (jobs) |
|
|
3864
|
+
|---|---|---|
|
|
3865
|
+
| Call | cheap-worker(task, [label]) | cheap-worker(jobs=[{task, label?}, ...]) |
|
|
3866
|
+
| Behaviour | one child runs the task once | all children start at once, results keep your order |
|
|
3867
|
+
| Cap | — | **10 (fixed, hardcoded)** |
|
|
3868
|
+
| Model | CCC config (single source) | same |
|
|
3869
|
+
|
|
3870
|
+
## ⚠️ Before using: load eap and design the plan
|
|
3871
|
+
Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up cheap-worker plan" based on the EAP framework.
|
|
3872
|
+
|
|
3873
|
+
### 1. Task decomposition (E↑ Explicit)
|
|
3874
|
+
- Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
|
|
3875
|
+
- Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel via \`jobs\`
|
|
3876
|
+
|
|
3877
|
+
### 2. Prompt design (cheap-worker's task parameter)
|
|
3878
|
+
- task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
|
|
3879
|
+
- the child does NOT share your conversation — restate everything it needs in task
|
|
3880
|
+
- Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
|
|
3881
|
+
output a JSON array (fields id/conclusion/evidence), do not modify the original file"
|
|
3882
|
+
- Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
|
|
3883
|
+
the cheap-worker-internal agent is also required to load eap and organize output per the EAP standard
|
|
3884
|
+
|
|
3885
|
+
### 3. Model (CCC-configured, not selectable per call)
|
|
3886
|
+
- The model is read from .opencode/serenity.json "cheap-worker.models" — **the caller cannot pass one**
|
|
3887
|
+
(there is no "model" parameter). "cheap-worker.defaultModel" names which entry is used; it falls back to models[0].
|
|
3888
|
+
- The CCC is expected to point this whitelist at LOW-COST models: cheap-worker is the bulk-execution channel
|
|
3889
|
+
- A child's own recursive subagents inherit the child's model automatically (DSH native)
|
|
3890
|
+
|
|
3891
|
+
### 3b. 🔴 Provider availability (per-machine — the whitelist is NOT enough)
|
|
3892
|
+
- The whitelist lives in the **CCC** ("serenity.json", it **travels with the repository**), but provider
|
|
3893
|
+
**adapters are registered per machine by the host** (profile config / settings namespace). An entry that
|
|
3894
|
+
works on one machine can fail on another with: "no adapter registered for provider <p>".
|
|
3895
|
+
- Before spawning, cheap-worker verifies the provider of the resolved model against the adapters registered
|
|
3896
|
+
**on this machine**; if it is missing it reports the requested model, where it is configured, **the
|
|
3897
|
+
providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the
|
|
3898
|
+
exact fix — delivered in the result's "diagnostic" field (never an opaque failure, never a created child).
|
|
3899
|
+
- ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "cheap-worker.models"
|
|
3900
|
+
(and "cheap-worker.defaultModel") to a provider listed as available, or activate/register the provider on this
|
|
3901
|
+
machine. **Do not retry the same call unchanged** — it fails identically.
|
|
3902
|
+
|
|
3903
|
+
### 4. Parallel strategy
|
|
3904
|
+
- Independent subtasks: cheap-worker(jobs=[{task,label?},...]) ⇒ fan-out, **cap 10**, results come back
|
|
3905
|
+
**in the order you passed them** (label is only a display name).
|
|
3906
|
+
- For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead.
|
|
3907
|
+
|
|
3908
|
+
## Completion criteria
|
|
3909
|
+
- The child's run settles; done=true only when stopReason="completed" — otherwise read "diagnostic"
|
|
3910
|
+
(and "output" for whatever partial text was produced).
|
|
3911
|
+
`;
|
|
3912
|
+
//#endregion
|
|
3913
|
+
//#region src/role/skiff/core.ts
|
|
3914
|
+
/** `provider/model` → agentOptions(由 worker/core 提供,保持单一实现) */
|
|
3915
|
+
const skiffAgents = /* @__PURE__ */ new Map();
|
|
3916
|
+
/** 查 sessionId 的会话绑定(轨迹 + ccc;无 → null)——追问校验 */
|
|
3765
3917
|
function skiffSessionInfo(sessionId) {
|
|
3766
3918
|
return skiffSessionInfo$1(sessionId);
|
|
3767
3919
|
}
|
|
@@ -3769,76 +3921,80 @@ function skiffSessionInfo(sessionId) {
|
|
|
3769
3921
|
function getSkiffAgent(sessionId) {
|
|
3770
3922
|
return skiffAgents.get(sessionId);
|
|
3771
3923
|
}
|
|
3772
|
-
|
|
3924
|
+
/** 该会话当前绑定的 skiff 约束面(不是 skiff → null)。**守门与提示词的唯一查询入口。** */
|
|
3925
|
+
function skiffProfileFor(root, sessionId) {
|
|
3926
|
+
if (sessionId === void 0) return null;
|
|
3927
|
+
return skiffProfileForSession(root, sessionId, skiffTrajectoryDirNameFor(sessionId));
|
|
3928
|
+
}
|
|
3929
|
+
function registerSkiffSession(sessionId, trajectoryDirName, ccc, agent) {
|
|
3773
3930
|
skiffAgents.set(sessionId, agent);
|
|
3774
|
-
registerSkiffSession$1(sessionId,
|
|
3931
|
+
registerSkiffSession$1(sessionId, trajectoryDirName, ccc);
|
|
3775
3932
|
}
|
|
3776
3933
|
function unregisterSkiffSession(sessionId) {
|
|
3777
3934
|
skiffAgents.delete(sessionId);
|
|
3778
3935
|
unregisterSkiffSession$1(sessionId);
|
|
3779
3936
|
}
|
|
3780
|
-
/** 自动创建的 bound 标记(readLastBound 恢复判定——只恢复本机制建的;手工 use 的旧 bound 不认) */
|
|
3781
|
-
const AUTO_BOUND_NOTE_PREFIX = "auto-created for skiff role";
|
|
3782
3937
|
/**
|
|
3783
|
-
*
|
|
3784
|
-
*
|
|
3785
|
-
*
|
|
3786
|
-
*
|
|
3938
|
+
* 该 skiff 会话的**工作台路径** = 它自己轨迹的 SESSION.md。
|
|
3939
|
+
*
|
|
3940
|
+
* 🔴 与旧版的差别:旧版会在这里**自动新建**一个 SESSION(`ensureSkiffSession`);
|
|
3941
|
+
* 新版**没有新建这个动作** —— 轨迹就是工作台,`skiff.json` 与 `SESSION.md` 同处一个目录。
|
|
3942
|
+
* ⇒ 语义变成纯粹的"查询:我自己的身体在哪"。
|
|
3943
|
+
*
|
|
3944
|
+
* @returns SESSION.md 绝对路径;不是 skiff / 文件不在 → null
|
|
3945
|
+
*/
|
|
3946
|
+
function skiffWorkspaceMdPath(root, sessionId) {
|
|
3947
|
+
if (sessionId === void 0) return null;
|
|
3948
|
+
const dirName = skiffTrajectoryDirNameFor(sessionId);
|
|
3949
|
+
if (dirName === null) return null;
|
|
3950
|
+
const md = join(sessionsRoot(root), dirName, "SESSION.md");
|
|
3951
|
+
return existsSync(md) ? md : null;
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* 系统提示词中的**工作台纪律块**(S142 用户:只给路径 LLM 不会主动用)。
|
|
3955
|
+
*
|
|
3956
|
+
* SESSION = 工作台 —— skiff 与主舱同一套机制(**零特调**,用户拍板):
|
|
3957
|
+
* SESSION.md 是持久记忆载体、rebuild 自动从本 SESSION 续接。
|
|
3787
3958
|
* 注入 agent 系统提示词(用户对话面不可见)。
|
|
3788
3959
|
*
|
|
3789
|
-
* v1.30.13(S142 诊断 D5
|
|
3790
|
-
*
|
|
3791
|
-
*
|
|
3792
|
-
* 现文案改为「用本角色被授权的写入通道」——ACC 只声明义务(写进工作台),
|
|
3793
|
-
* 具体通道由 CCC 的角色配置与角色提示词决定(ACC/CCC 归属二分;也不把某个 CCC 的
|
|
3794
|
-
* MSM 名硬编码进 ACC 源码)。
|
|
3960
|
+
* v1.30.13(S142 诊断 D5):写入纪律**不点名具体工具** —— 旧文案写死 "with write/edit",
|
|
3961
|
+
* 而某个 skiff 的工具白名单里根本没有 write/edit ⇒ 指令指向它没有的工具
|
|
3962
|
+
* (注入纪律与能力面矛盾,模型只能猜)。现文案改为「用本轨迹被授权的写入通道」。
|
|
3795
3963
|
*/
|
|
3796
3964
|
function workspaceCognitiveTrajectoryLine(mdPath) {
|
|
3797
3965
|
return [
|
|
3798
3966
|
"── Serenity Session Workspace ──",
|
|
3799
|
-
"
|
|
3967
|
+
"You ARE a cognitive trajectory: this workspace SESSION is your own persistent body.",
|
|
3800
3968
|
` SESSION.md: ${mdPath}`,
|
|
3801
3969
|
"",
|
|
3802
3970
|
"Rules of the workspace:",
|
|
3803
3971
|
" 1. This SESSION.md is your persistent memory carrier (like the main cabin SESSION) — ",
|
|
3804
3972
|
" read it first when context feels thin or work spans turns.",
|
|
3805
3973
|
" 2. Record key decisions/progress/unresolved items INTO this SESSION.md using the write channel",
|
|
3806
|
-
" your
|
|
3974
|
+
" your trajectory is actually granted (use whatever write tool or MSM you have — never assume",
|
|
3807
3975
|
" a specific write tool exists; if none is granted, report that instead of pretending to write).",
|
|
3808
3976
|
" Sections: 身份与边界 / 权威锚点 / 工作纪律 / 决策 / 日志 / 当前状态 / 待 owner 裁决.",
|
|
3809
3977
|
" Keep entries at milestone level (one line + evidence pointer); execution detail",
|
|
3810
|
-
" belongs in commit messages. Keep it current — it is",
|
|
3811
|
-
"
|
|
3812
|
-
" 3. When context pressure is high, run cognitive_trajectory rebuild — it auto-resumes from THIS SESSION.md",
|
|
3813
|
-
" (no manual use; do not switch to or read other SESSIONs unless the user explicitly asks).",
|
|
3978
|
+
" belongs in commit messages. Keep it current — it is what a rebuild resumes from.",
|
|
3979
|
+
" 3. When context pressure is high, run cognitive_trajectory rebuild — it auto-resumes from THIS SESSION.md.",
|
|
3814
3980
|
" 4. Never expose this internal path in user-facing replies (chat stays clean).",
|
|
3815
3981
|
"── ──"
|
|
3816
3982
|
].join("\n");
|
|
3817
3983
|
}
|
|
3818
|
-
/**
|
|
3819
|
-
* 已挂工作台提示词段的 agent(幂等:同一 agent 只注册一次,避免 duplicate section)。
|
|
3820
|
-
* WeakSet → agent 被回收即自动清理,无泄漏。
|
|
3821
|
-
*/
|
|
3984
|
+
/** 已挂工作台提示词段的 agent(幂等:同一 agent 只注册一次,避免 duplicate section) */
|
|
3822
3985
|
const workspaceSectionAgents = /* @__PURE__ */ new WeakSet();
|
|
3823
3986
|
/**
|
|
3824
|
-
* 把**工作台纪律块**挂到 agent
|
|
3987
|
+
* 把**工作台纪律块**挂到 agent 的系统提示词段(单一注入点)。
|
|
3825
3988
|
*
|
|
3826
3989
|
* 用户原话(S142,2026-09-08):"微信桥的注入机制,每个用户消息都会注入,skiff 本身也会注入,
|
|
3827
3990
|
* 这样就重复,能否微信桥情况下,注入内容直接取 skiff 的,这样不用配两遍"。
|
|
3828
3991
|
*
|
|
3829
|
-
*
|
|
3830
|
-
*
|
|
3831
|
-
* "live/existing 快路径不重挂提示词"而加)→ 新建 agent 首轮起即重复,且每轮重复付 token。
|
|
3992
|
+
* 现形态:工作台行**只**经本函数挂系统提示词段(name `serenity-skiff-workspace`,
|
|
3993
|
+
* 动态 `text()` 每轮按 scope 读当前活跃 mdPath)。
|
|
3832
3994
|
*
|
|
3833
|
-
*
|
|
3834
|
-
* 动态 `text()` 每轮按 scope 读当前活跃 mdPath → 绑定变化自动跟随,无需重挂):
|
|
3835
|
-
* - `createSkiffAgent` 调它(不再把工作台行拼进基础段);
|
|
3836
|
-
* - 微信桥对 live/existing agent 调它(不再拼 question)。
|
|
3837
|
-
*
|
|
3838
|
-
* @returns true = 段已在位(本次注册成功或此前已注册);false = 该 agent 无法挂系统提示词段
|
|
3839
|
-
* (宿主 systemPrompt 缺失/注册抛错)→ 调用方降级为 question 前缀注入(约束必须在场)
|
|
3995
|
+
* @returns true = 段已在位;false = 该 agent 挂不上(调用方降级为 question 前缀注入)
|
|
3840
3996
|
*/
|
|
3841
|
-
function ensureWorkspacePromptSection(agent, scope) {
|
|
3997
|
+
function ensureWorkspacePromptSection(agent, scope, root) {
|
|
3842
3998
|
if (!agent || typeof agent !== "object") return false;
|
|
3843
3999
|
if (workspaceSectionAgents.has(agent)) return true;
|
|
3844
4000
|
const api = agent.ctx?.systemPrompt;
|
|
@@ -3848,7 +4004,7 @@ function ensureWorkspacePromptSection(agent, scope) {
|
|
|
3848
4004
|
name: "serenity-skiff-workspace",
|
|
3849
4005
|
order: -55,
|
|
3850
4006
|
text: () => {
|
|
3851
|
-
const mdPath =
|
|
4007
|
+
const mdPath = skiffWorkspaceMdPath(root, scope);
|
|
3852
4008
|
return mdPath ? workspaceCognitiveTrajectoryLine(mdPath) : "";
|
|
3853
4009
|
}
|
|
3854
4010
|
});
|
|
@@ -3859,90 +4015,22 @@ function ensureWorkspacePromptSection(agent, scope) {
|
|
|
3859
4015
|
return false;
|
|
3860
4016
|
}
|
|
3861
4017
|
}
|
|
3862
|
-
/**
|
|
3863
|
-
* 确保启用 session 能力的 skiff 会话拥有**专属 SESSION**(工作台):
|
|
3864
|
-
* 懒绑定 + 幂等(per skiff 会话 scope 隔离——微信多用户各自独立,永不共享同一份)。
|
|
3865
|
-
*
|
|
3866
|
-
* 判定链:
|
|
3867
|
-
* ⓪ **临时身份不建工作台**(v1.30.9,S142 用户报"SESSION 爆炸"根治):`persistent=false`
|
|
3868
|
-
* (调试页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
|
|
3869
|
-
* 调用都是新 id、无身份延续,为其建 SESSION = 每次问答留一个永久空目录(无界熵增);
|
|
3870
|
-
* 用户模型「skiff 本质是临时会话机制」——工作台属于**持久身份**,不属于单次调用。
|
|
3871
|
-
* ① 角色未启用 session 能力(cognitiveTrajectory.session !== true)→ null(现状零变化)
|
|
3872
|
-
* ② 已有**本机制自动创建**的 bound(note 前缀 auto-created)→ 恢复(重启/续接记得)
|
|
3873
|
-
* ②b 内存活跃记录(同 scope)→ 恢复 + **自愈补写绑定**(防御纵深:绑定写失败/事件形态未落盘时
|
|
3874
|
-
* 仍复用同一 SESSION,而不是每条消息新建一个——v1.30.5 及更早"每条消息一个 SESSION"的放大效应)
|
|
3875
|
-
* ③ 无 → 自动创建专属 SESSION(createSession desc=`<role> skiff`——编号自动递增,用户不关心是谁)
|
|
3876
|
-
*
|
|
3877
|
-
* @param persistent 该 skiff 会话是否有**持久身份**(固定会话 id)。默认 true(向后兼容调用点);
|
|
3878
|
-
* `createSkiffAgent` 传 `sessionId !== undefined`,微信桥的 existing 快路径传 true。
|
|
3879
|
-
* @returns 绑定后的 SESSION.md 绝对路径(供提示词注入);不适用/失败 → null
|
|
3880
|
-
*/
|
|
3881
|
-
function ensureSkiffSession(root, agent, roleName, role, persistent = true) {
|
|
3882
|
-
if (!cognitiveTrajectorySubset(role).session) return null;
|
|
3883
|
-
if (!persistent) return null;
|
|
3884
|
-
const session = agent.session;
|
|
3885
|
-
const scope = String(agent.session.id ?? agent.id);
|
|
3886
|
-
const lastBound = session ? readLastBound(session) : null;
|
|
3887
|
-
if (lastBound && lastBound.note?.startsWith(AUTO_BOUND_NOTE_PREFIX) && existsSync(lastBound.mdPath)) {
|
|
3888
|
-
setActiveSessionInfo(scope, {
|
|
3889
|
-
sessionId: lastBound.sessionId ?? "",
|
|
3890
|
-
dirName: lastBound.dirName,
|
|
3891
|
-
mdPath: lastBound.mdPath
|
|
3892
|
-
});
|
|
3893
|
-
return lastBound.mdPath;
|
|
3894
|
-
}
|
|
3895
|
-
const inMemory = getActiveSessionInfo(scope);
|
|
3896
|
-
if (inMemory?.mdPath && existsSync(inMemory.mdPath)) {
|
|
3897
|
-
appendBound(session, "reconcile", {
|
|
3898
|
-
dirName: inMemory.dirName,
|
|
3899
|
-
mdPath: inMemory.mdPath,
|
|
3900
|
-
...inMemory.sessionId ? { sessionId: inMemory.sessionId } : {},
|
|
3901
|
-
note: `${AUTO_BOUND_NOTE_PREFIX} ${roleName}`
|
|
3902
|
-
});
|
|
3903
|
-
return inMemory.mdPath;
|
|
3904
|
-
}
|
|
3905
|
-
const desc = `${roleName} skiff`;
|
|
3906
|
-
const created = createSession({
|
|
3907
|
-
root,
|
|
3908
|
-
desc,
|
|
3909
|
-
dryRun: false
|
|
3910
|
-
});
|
|
3911
|
-
const mdPath = join(created.sessionPath, "SESSION.md");
|
|
3912
|
-
setActiveSessionInfo(scope, {
|
|
3913
|
-
sessionId: created.sessionId,
|
|
3914
|
-
dirName: created.dirName,
|
|
3915
|
-
mdPath
|
|
3916
|
-
});
|
|
3917
|
-
appendBound(session, "create", {
|
|
3918
|
-
dirName: created.dirName,
|
|
3919
|
-
mdPath,
|
|
3920
|
-
sessionId: created.sessionId,
|
|
3921
|
-
note: `${AUTO_BOUND_NOTE_PREFIX} ${roleName}`
|
|
3922
|
-
});
|
|
3923
|
-
return mdPath;
|
|
3924
|
-
}
|
|
3925
|
-
/** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
|
|
3926
|
-
* cheap-worker 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
|
|
3927
|
-
* guard 角色白名单再按角色过滤可见/可用面——白名单外工具仍 deny) */
|
|
4018
|
+
/** skiff agent 挂载的 DSH preset(平台工具面由 preset 决定;白名单再按轨迹过滤) */
|
|
3928
4019
|
const SKIFF_PRESET = "standard";
|
|
3929
4020
|
/**
|
|
3930
|
-
*
|
|
3931
|
-
* standard preset(平台工具面)+ scoped 系统提示词(基础提示词 + CCC 定义段,全替换 ACC 默认注入)。
|
|
4021
|
+
* 创建(或复用)一个 skiff 会话。
|
|
3932
4022
|
*
|
|
3933
|
-
* **固定 sessionId 的 resume-or-create
|
|
3934
|
-
*
|
|
3935
|
-
*
|
|
3936
|
-
* 无 log(首次)/持久化未配置 → `ctx.agents.create`(新建)。不指定 sessionId →
|
|
3937
|
-
* 随机 id 恒 create(调试页默认路径)。
|
|
4023
|
+
* **固定 sessionId 的 resume-or-create**(v1.27.2):指定 sessionId 且磁盘已有持久化 log →
|
|
4024
|
+
* `ctx.agents.resume`(DSH 持久化语义:sessionId 即身份,历史 + turn 续号延续);
|
|
4025
|
+
* 无 log(首次)→ `ctx.agents.create`。不指定 → 随机 id 恒 create(调试页路径)。
|
|
3938
4026
|
*
|
|
3939
|
-
* @param
|
|
3940
|
-
*
|
|
4027
|
+
* @param profile 该 skiff 的约束面(来自 `skiff.json`)
|
|
4028
|
+
* @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现"轨迹 ↔ 会话"长期映射)
|
|
3941
4029
|
*/
|
|
3942
|
-
async function createSkiffAgent(ctx, root,
|
|
4030
|
+
async function createSkiffAgent(ctx, root, profile, defaultModel, sessionId) {
|
|
3943
4031
|
if (!ctx.agents) throw new Error("skiff: ctx.agents unavailable");
|
|
3944
|
-
const model =
|
|
3945
|
-
const id = sessionId ?? `skiff-${
|
|
4032
|
+
const model = profile.model?.trim() || defaultModel || "";
|
|
4033
|
+
const id = sessionId ?? `skiff-${profile.code}-${randomUUID()}`;
|
|
3946
4034
|
const setup = async (agentCtx) => {
|
|
3947
4035
|
try {
|
|
3948
4036
|
await agentCtx.get("agentPresets")?.mount?.(agentCtx, SKIFF_PRESET);
|
|
@@ -3950,49 +4038,30 @@ async function createSkiffAgent(ctx, root, roleName, role, defaultModel, session
|
|
|
3950
4038
|
};
|
|
3951
4039
|
const handle = await createOrResumeAgent(ctx, id, root, model, setup, sessionId !== void 0);
|
|
3952
4040
|
const agent = handle.agent;
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
4041
|
+
let readerPath = profile.systemPromptFile?.trim();
|
|
4042
|
+
let reader = createProfilePromptReader(root, profile);
|
|
4043
|
+
const livePrompt = () => {
|
|
4044
|
+
let live = profile;
|
|
3956
4045
|
try {
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
4046
|
+
const fresh = readSkiffProfileAt(join(sessionsRoot(root), profile.dirName), root);
|
|
4047
|
+
if (fresh) live = fresh;
|
|
4048
|
+
} catch {}
|
|
4049
|
+
const wantPath = live.systemPromptFile?.trim();
|
|
4050
|
+
if (wantPath !== readerPath) {
|
|
4051
|
+
reader = createProfilePromptReader(root, live);
|
|
4052
|
+
readerPath = wantPath;
|
|
3960
4053
|
}
|
|
3961
|
-
return ()
|
|
3962
|
-
}
|
|
3963
|
-
|
|
3964
|
-
let workspaceMdPath = null;
|
|
3965
|
-
try {
|
|
3966
|
-
workspaceMdPath = ensureSkiffSession(root, agent, roleName, role, kind === "persistent");
|
|
3967
|
-
} catch (err) {
|
|
3968
|
-
console.warn(`[serenity-hooks] skiff 角色 "${roleName}" 专属 SESSION 处理失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
|
|
3969
|
-
}
|
|
4054
|
+
return reader();
|
|
4055
|
+
};
|
|
4056
|
+
livePrompt();
|
|
3970
4057
|
try {
|
|
3971
|
-
let readerPath = role?.systemPromptFile?.trim();
|
|
3972
|
-
let reader = rolePrompt;
|
|
3973
|
-
const liveRolePrompt = () => {
|
|
3974
|
-
let live = role;
|
|
3975
|
-
try {
|
|
3976
|
-
const fresh = readSkiffRoles(root).get(roleName);
|
|
3977
|
-
if (fresh) live = fresh;
|
|
3978
|
-
} catch {}
|
|
3979
|
-
const wantPath = live?.systemPromptFile?.trim();
|
|
3980
|
-
if (wantPath !== readerPath) {
|
|
3981
|
-
reader = kind === "persistent" ? createRolePromptReader(root, live) : (() => {
|
|
3982
|
-
let snapshot = "";
|
|
3983
|
-
try {
|
|
3984
|
-
snapshot = resolveRoleSystemPrompt(root, live);
|
|
3985
|
-
} catch {}
|
|
3986
|
-
return () => snapshot;
|
|
3987
|
-
})();
|
|
3988
|
-
readerPath = wantPath;
|
|
3989
|
-
}
|
|
3990
|
-
return reader();
|
|
3991
|
-
};
|
|
3992
4058
|
agent.ctx.systemPrompt.section({
|
|
3993
4059
|
name: "serenity-skiff",
|
|
3994
4060
|
order: -60,
|
|
3995
|
-
text: () =>
|
|
4061
|
+
text: () => {
|
|
4062
|
+
const legacy = hasTrajectoryPrompt(root, profile.dirName) ? "" : livePrompt();
|
|
4063
|
+
return [buildSkiffBasePrompt(profile.code), legacy].filter(Boolean).join("\n");
|
|
4064
|
+
}
|
|
3996
4065
|
});
|
|
3997
4066
|
} catch (err) {
|
|
3998
4067
|
console.warn(`[serenity-hooks] skiff 系统提示词注册失败: ${String(err?.message ?? err)}`);
|
|
@@ -4002,8 +4071,8 @@ async function createSkiffAgent(ctx, root, roleName, role, defaultModel, session
|
|
|
4002
4071
|
} catch (err) {
|
|
4003
4072
|
console.warn(`[serenity-hooks] skiff 轨迹 skill 注入注册失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
|
|
4004
4073
|
}
|
|
4005
|
-
if (
|
|
4006
|
-
registerSkiffSession(id,
|
|
4074
|
+
if (skiffWorkspaceMdPath(root, id) !== null) ensureWorkspacePromptSection(agent, id, root);
|
|
4075
|
+
registerSkiffSession(id, profile.dirName, root, agent);
|
|
4007
4076
|
return {
|
|
4008
4077
|
handle,
|
|
4009
4078
|
agent,
|
|
@@ -4029,14 +4098,12 @@ function liveReuseRef(live, setup) {
|
|
|
4029
4098
|
}
|
|
4030
4099
|
/**
|
|
4031
4100
|
* resume-or-create 分派:固定 id → 优先 resume(持久化历史延续),失败降级 create;
|
|
4032
|
-
* 随机 id(无固定 sessionId)→ 恒 create
|
|
4101
|
+
* 随机 id(无固定 sessionId)→ 恒 create。
|
|
4033
4102
|
*
|
|
4034
4103
|
* **v1.27.3 修复(微信桥"重启后不响应"根因):live 会话优先复用**——重启后 DSH 把
|
|
4035
|
-
* 关闭时仍 live 的持久化会话恢复为 live
|
|
4036
|
-
* "cannot prepare session X while it is live"
|
|
4037
|
-
*
|
|
4038
|
-
* 微信桥 catch 吞错 → 用户消息无回复。唯一可行路径 = `ctx.agents.get(id)` 取 live
|
|
4039
|
-
* agent 直接续用(resumed:true;角色提示词重挂与内存注册表登记由 createSkiffAgent 统一做)。
|
|
4104
|
+
* 关闭时仍 live 的持久化会话恢复为 live。此时 resume 报
|
|
4105
|
+
* "cannot prepare session X while it is live"、create 报 "session X already exists"
|
|
4106
|
+
* ⇒ 唯一可行路径 = `ctx.agents.get(id)` 取 live agent 直接续用。
|
|
4040
4107
|
*/
|
|
4041
4108
|
async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
|
|
4042
4109
|
if (!fixedId) return {
|
|
@@ -4096,18 +4163,13 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
|
|
|
4096
4163
|
};
|
|
4097
4164
|
}
|
|
4098
4165
|
}
|
|
4099
|
-
/**
|
|
4100
|
-
* 会话事件读取(v1.28.0 适配 0.1.2-rc.1):rc.1 Session 移除 `.events` 属性 → `snapshotEvents()` 方法。
|
|
4101
|
-
* 兼容双形态(运行时 rc.1 snapshotEvents;测试替身 events)——同一函数两处语义,测试注入零侵入。
|
|
4102
|
-
* 注:与 cognitive-trajectory-ops.ts 导出的共享 `sessionEvents` 同构(v1.28.1 收敛基准)。本模块保留本地副本
|
|
4103
|
-
* 以维持独立 import 面(零 DSH 依赖测试替身形态一致);若未来重构可统一 import 共享版。
|
|
4104
|
-
*/
|
|
4166
|
+
/** 会话事件读取(兼容 `snapshotEvents()` 与测试替身的 `.events`) */
|
|
4105
4167
|
function sessionEvents(session) {
|
|
4106
4168
|
const s = session;
|
|
4107
4169
|
if (typeof s.snapshotEvents === "function") return s.snapshotEvents();
|
|
4108
4170
|
return s.events ?? [];
|
|
4109
4171
|
}
|
|
4110
|
-
/** 读会话最后一个 assistant/message
|
|
4172
|
+
/** 读会话最后一个 assistant/message 文本 */
|
|
4111
4173
|
function lastAssistantText$1(agent) {
|
|
4112
4174
|
const events = sessionEvents(agent.session);
|
|
4113
4175
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
@@ -4119,62 +4181,45 @@ function lastAssistantText$1(agent) {
|
|
|
4119
4181
|
}
|
|
4120
4182
|
return "";
|
|
4121
4183
|
}
|
|
4122
|
-
/**
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
if (text) out.push({
|
|
4130
|
-
role: "user",
|
|
4131
|
-
text
|
|
4132
|
-
});
|
|
4133
|
-
} else if (ev.type === "assistant/message") {
|
|
4134
|
-
const d = ev.data;
|
|
4135
|
-
const text = (d?.message?.content ?? d?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
|
|
4136
|
-
const calls = d?.message?.tool_calls ?? [];
|
|
4137
|
-
if (text) out.push({
|
|
4138
|
-
role: "assistant",
|
|
4139
|
-
text
|
|
4140
|
-
});
|
|
4141
|
-
for (const c of calls ?? []) {
|
|
4142
|
-
const args = typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {});
|
|
4143
|
-
out.push({
|
|
4144
|
-
role: "assistant",
|
|
4145
|
-
text: `→ ${c.name ?? "(tool)"} ${truncate(args, 300)}`,
|
|
4146
|
-
tool: c.name
|
|
4147
|
-
});
|
|
4148
|
-
}
|
|
4149
|
-
} else if (ev.type === "tool/result") {
|
|
4150
|
-
const outText = extractText(ev.data);
|
|
4151
|
-
if (outText) out.push({
|
|
4152
|
-
role: "tool",
|
|
4153
|
-
text: truncate(outText, 500),
|
|
4154
|
-
tool: String(ev.data?.name ?? "")
|
|
4155
|
-
});
|
|
4156
|
-
}
|
|
4157
|
-
} catch {}
|
|
4158
|
-
return out;
|
|
4159
|
-
}
|
|
4160
|
-
function extractText(data) {
|
|
4161
|
-
const content = data?.content;
|
|
4162
|
-
if (!Array.isArray(content)) return "";
|
|
4163
|
-
return content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
|
|
4184
|
+
/** 本轮等待上限(毫秒):超时即判「本轮无产出」——**绝不**把上一轮的旧答案当本轮答案 */
|
|
4185
|
+
const ASK_TURN_TIMEOUT_MS = 18e4;
|
|
4186
|
+
/** 会话里 `assistant/message` 的条数(判「本轮是否真的产出了回答」) */
|
|
4187
|
+
function countAssistantMessages(session) {
|
|
4188
|
+
let n = 0;
|
|
4189
|
+
for (const e of sessionEvents(session)) if (e?.type === "assistant/message") n += 1;
|
|
4190
|
+
return n;
|
|
4164
4191
|
}
|
|
4165
|
-
|
|
4166
|
-
|
|
4192
|
+
/**
|
|
4193
|
+
* 等**本轮**产出一条 assistant 消息。
|
|
4194
|
+
*
|
|
4195
|
+
* ## 为什么不能用 `waitAgentIdle` 单独当判据(2026-10-19 复读缺陷)
|
|
4196
|
+
*
|
|
4197
|
+
* `waitAgentIdle` 的首行是 `if (agent.status === 'idle') finish()` —— 而 `followup()`
|
|
4198
|
+
* 是**异步投递**:刚投出去的这一刻 agent 状态**仍是 idle** ⇒ 等待**立刻结算** ⇒
|
|
4199
|
+
* `lastAssistantText()` 读到的是**上一轮的** `assistant/message` ⇒ 桥把它当"本轮答案"转发。
|
|
4200
|
+
* 若 CCC 又开了 `fallbackOnNoSend`,同一条旧答案会被反复重发(实测三次间隔 300~400ms、逐字相同)。
|
|
4201
|
+
*
|
|
4202
|
+
* ## 判据(`idle` 只是"可以开始看"的信号,不是成功信号)
|
|
4203
|
+
*
|
|
4204
|
+
* ① 期望值 = 调用前 `assistant/message` 的**条数**;
|
|
4205
|
+
* ② 成功 ⟺ 该计数**增长**;
|
|
4206
|
+
* ③ 超时或有界轮询耗尽 ⇒ `false`(调用方据此给空答案,兜底不触发)。
|
|
4207
|
+
*/
|
|
4208
|
+
async function waitForTurnAssistant(ctx, agent, beforeAssistantCount, opts = {}) {
|
|
4209
|
+
const timeoutMs = opts.timeoutMs ?? ASK_TURN_TIMEOUT_MS;
|
|
4210
|
+
const grew = () => countAssistantMessages(agent.session) > beforeAssistantCount;
|
|
4211
|
+
if (await Promise.race([waitAgentIdle(ctx, agent).then(() => true), new Promise((resolve) => {
|
|
4212
|
+
setTimeout(() => resolve(false), timeoutMs);
|
|
4213
|
+
})])) return grew();
|
|
4214
|
+
return grew();
|
|
4167
4215
|
}
|
|
4168
4216
|
/**
|
|
4169
|
-
* 提问一轮:followup →
|
|
4170
|
-
*
|
|
4171
|
-
*
|
|
4172
|
-
* @param options.includeCognitiveTrajectory 是否计算轨迹(v1.26.10 起供**对外面**传 false 跳过计算——
|
|
4173
|
-
* 原消费方 = 3100 公开问答页 / ACP JSON-RPC,**该面已于 2026-10-15 退役** ⇒ 现无生产调用方传 false;
|
|
4174
|
-
* 3099 调试页默认 true 保留)
|
|
4217
|
+
* 提问一轮:followup → 等**本轮**回答 → 读答案。
|
|
4218
|
+
*
|
|
4219
|
+
* @param options.timeoutMs 本轮等待上限(测试注入;生产用缺省)
|
|
4175
4220
|
*/
|
|
4176
|
-
async function askSkiff(ctx, agent, question,
|
|
4177
|
-
const
|
|
4221
|
+
async function askSkiff(ctx, agent, question, options) {
|
|
4222
|
+
const beforeAssistant = countAssistantMessages(agent.session);
|
|
4178
4223
|
agent.followup(createUserMessage({
|
|
4179
4224
|
content: [{
|
|
4180
4225
|
type: "text",
|
|
@@ -4182,49 +4227,46 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
|
|
|
4182
4227
|
}],
|
|
4183
4228
|
source: PLUGIN_SOURCE
|
|
4184
4229
|
}));
|
|
4185
|
-
await waitAgentIdle(ctx, agent);
|
|
4186
|
-
const answer = lastAssistantText$1(agent);
|
|
4187
|
-
const cognitiveTrajectory = options?.includeCognitiveTrajectory === false ? [] : eventsToCognitiveTrajectory(sessionEvents(agent.session).slice(before));
|
|
4188
4230
|
return {
|
|
4189
|
-
answer,
|
|
4190
|
-
sessionId: String(agent.session.id ?? "")
|
|
4191
|
-
cognitiveTrajectory
|
|
4231
|
+
answer: await waitForTurnAssistant(ctx, agent, beforeAssistant, { timeoutMs: options?.timeoutMs }) ? lastAssistantText$1(agent) : "",
|
|
4232
|
+
sessionId: String(agent.session.id ?? "")
|
|
4192
4233
|
};
|
|
4193
4234
|
}
|
|
4194
4235
|
/**
|
|
4195
|
-
*
|
|
4196
|
-
* skiff
|
|
4197
|
-
*
|
|
4236
|
+
* skiff 会话的轨迹纪律参与判定:**不是 skiff 恒 true**(正常参与);
|
|
4237
|
+
* skiff 按 `skiff.json` 的 `cognitiveTrajectory` 子集决定。
|
|
4238
|
+
*
|
|
4239
|
+
* 注册表缺失(进程刚重启、绑定还没读)⇒ 由 `skiffProfileForSession` 回落持久绑定;
|
|
4240
|
+
* 两者都不中 ⇒ 判"不是 skiff" ⇒ 恒 true(正常参与)。**fail-open 是有意的**:
|
|
4241
|
+
* 纪律参与项的失败不该让会话失去 keeper/rebuild(那是能力倒退,比多跑一次更糟)。
|
|
4198
4242
|
*/
|
|
4199
4243
|
function skiffCognitiveTrajectoryEnabled(root, sessionId, key) {
|
|
4200
|
-
if (
|
|
4201
|
-
const
|
|
4202
|
-
if (
|
|
4203
|
-
|
|
4204
|
-
return cognitiveTrajectorySubset(role)[key];
|
|
4244
|
+
if (sessionId === void 0) return true;
|
|
4245
|
+
const profile = skiffProfileFor(root, sessionId);
|
|
4246
|
+
if (profile === null) return true;
|
|
4247
|
+
return profileCognitiveTrajectory(profile)[key];
|
|
4205
4248
|
}
|
|
4206
4249
|
/**
|
|
4207
|
-
* msm 工具的
|
|
4208
|
-
* exec 非白名单 MSM
|
|
4250
|
+
* `msm` 工具的 skiff 门控:非 skiff 恒放行;skiff 会话——
|
|
4251
|
+
* exec 非白名单 MSM 拒绝(**不列名单**)、register/deregister 必拒、
|
|
4209
4252
|
* list 白名单过滤、check/guide/ccc-config 只读放行。
|
|
4210
4253
|
*/
|
|
4211
4254
|
function skiffMsmGate(root, sessionId, action, name) {
|
|
4212
|
-
if (
|
|
4213
|
-
const
|
|
4214
|
-
|
|
4215
|
-
if (!roleName || !role) return { reject: "MSM not allowed in this skiff session" };
|
|
4255
|
+
if (sessionId === void 0) return {};
|
|
4256
|
+
const profile = skiffProfileFor(root, sessionId);
|
|
4257
|
+
if (profile === null) return {};
|
|
4216
4258
|
if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in skiff sessions" };
|
|
4217
4259
|
if (action === "exec") {
|
|
4218
|
-
if (!name || !
|
|
4260
|
+
if (!name || !profile.msms.includes(name)) return { reject: "MSM not allowed" };
|
|
4219
4261
|
return {};
|
|
4220
4262
|
}
|
|
4221
|
-
if (action === "list") return { whitelist:
|
|
4263
|
+
if (action === "list") return { whitelist: profileMsmWhitelist(profile) };
|
|
4222
4264
|
return {};
|
|
4223
4265
|
}
|
|
4224
4266
|
//#endregion
|
|
4225
|
-
//#region src/tools/msm.ts
|
|
4267
|
+
//#region src/tools/msm/index.ts
|
|
4226
4268
|
/**
|
|
4227
|
-
* msm.ts — msm 工具(v1.30 重构:单入口执行,list/exec 合一)
|
|
4269
|
+
* tools/msm/index.ts — msm 工具(v1.30 重构:单入口执行,list/exec 合一)
|
|
4228
4270
|
*
|
|
4229
4271
|
* 设计(S142 用户拍板"msm 单入口同意 + 参数简易高效"):
|
|
4230
4272
|
* acc_msm(8 action enum)→ msm(3 参数:name + args + inspect)——
|
|
@@ -4334,7 +4376,7 @@ const msmTool = defineTool({
|
|
|
4334
4376
|
* ⇒ 正文**不再内嵌**(58 KB 塞进 TS 模板字符串还要转义论文里 4 处 ```mermaid 围栏——
|
|
4335
4377
|
* 本容器为此踩过四次),改为**随包的静态资产 + 运行时读取**:
|
|
4336
4378
|
* 资产 = `assets/eap-paper.md`(**逐字**来自上游论文,未经改写)
|
|
4337
|
-
* 读取 = `loadEapPaper()`(进程内缓存一次;同 `voyage
|
|
4379
|
+
* 读取 = `loadEapPaper()`(进程内缓存一次;同 `surface/voyage/page.ts` 的既有先例)
|
|
4338
4380
|
*
|
|
4339
4381
|
* 🔴 **单一真相源**:正文只在 `assets/eap-paper.md` 一个地方。本文件负责**定位与读取**,
|
|
4340
4382
|
* `praxis.ts` 只负责**路由**。二者都不得内嵌任何论文片段——否则又是"第二份会漂的正文"。
|
|
@@ -8282,7 +8324,7 @@ const DIAGRAM_FONT_STACK = `'Noto Sans', 'DejaVu Sans', '${DIAGRAM_CJK_FONT_FAMI
|
|
|
8282
8324
|
/**
|
|
8283
8325
|
* 包根 = **向上找到含 `package.json` 的那一级**。
|
|
8284
8326
|
*
|
|
8285
|
-
* 🔴 为什么不能照抄 `voyage
|
|
8327
|
+
* 🔴 为什么不能照抄 `surface/voyage/page.ts` 的「`..` 一次」:那个文件在 `src/` 根下,一次 `..` 恰好是包根;
|
|
8286
8328
|
* 本模块在 `src/diagram/` 里 ⇒ 开发态(vitest 直接跑 TS 源码)一次 `..` 只到 `src/`,
|
|
8287
8329
|
* 于是去找 `src/assets/…` —— **实测 ENOENT**(测试全红,且现象是"字体不在",与真因"路径层级算错"
|
|
8288
8330
|
* 长得完全不同)。按 `package.json` 上溯对**开发态(src/diagram)与发布态(lib/diagram)都成立**。
|
|
@@ -9436,9 +9478,9 @@ function createCheapWorkerTool(ctx) {
|
|
|
9436
9478
|
});
|
|
9437
9479
|
}
|
|
9438
9480
|
//#endregion
|
|
9439
|
-
//#region src/
|
|
9481
|
+
//#region src/trajectory/cleanup.ts
|
|
9440
9482
|
/**
|
|
9441
|
-
*
|
|
9483
|
+
* trajectory/cleanup.ts — DSH 平台旧会话自动清理(v1.29,S142 需求③)
|
|
9442
9484
|
*
|
|
9443
9485
|
* 背景(实证):DSH 会话物理存储 = `$DSH_HOME/sessions/`(缺省 ~/.dsh/sessions;
|
|
9444
9486
|
* base bundle cordis.patch.yml root: dshHomePath('sessions'))。磁盘布局:
|
|
@@ -9455,7 +9497,7 @@ function createCheapWorkerTool(ctx) {
|
|
|
9455
9497
|
*
|
|
9456
9498
|
* 本模块纯逻辑(零 ctx 依赖):传入 sessionsRoot + liveIds,可单测。
|
|
9457
9499
|
*/
|
|
9458
|
-
var
|
|
9500
|
+
var cleanup_exports = /* @__PURE__ */ __exportAll({
|
|
9459
9501
|
collectEligibleSessions: () => collectEligibleSessions,
|
|
9460
9502
|
cutoffDaysAgo: () => cutoffDaysAgo,
|
|
9461
9503
|
findSessionLog: () => findSessionLog,
|
|
@@ -9645,9 +9687,9 @@ function cutoffDaysAgo(days, nowMs = Date.now()) {
|
|
|
9645
9687
|
return nowMs - days * DAY_MS;
|
|
9646
9688
|
}
|
|
9647
9689
|
//#endregion
|
|
9648
|
-
//#region src/wake
|
|
9690
|
+
//#region src/wake/registry.ts
|
|
9649
9691
|
/**
|
|
9650
|
-
* wake
|
|
9692
|
+
* wake/registry.ts — 认知轨迹唤醒注册表(D58,S142 2026-09-13)
|
|
9651
9693
|
*
|
|
9652
9694
|
* 需求(用户原话):一个 CCC 内允许多个认知轨迹任意形态并行;某个认知轨迹可以
|
|
9653
9695
|
* **预定未来唤醒自己或别人**,本质上是一次「未来时刻 + 一条 message」的投递;「周期自唤醒」
|
|
@@ -9659,7 +9701,7 @@ function cutoffDaysAgo(days, nowMs = Date.now()) {
|
|
|
9659
9701
|
* - 状态机:`pending` →(投递成功)`delivered`|(超补跑窗口)`missed`|(`wake rm` 物理移除)
|
|
9660
9702
|
* - 投递即终结:一次性,不重投(无回执语义——发起方不 await,见设计文档 §2)
|
|
9661
9703
|
*
|
|
9662
|
-
* 调度与投递在 `wake
|
|
9704
|
+
* 调度与投递在 `wake/scheduler.ts`(需要 ctx),本模块保持纯净。
|
|
9663
9705
|
*/
|
|
9664
9706
|
/** 注册表相对 CCC 根的路径(与 `.bindings.json` 同居 AGENT_SESSIONS/) */
|
|
9665
9707
|
const WAKE_REGISTRY_REL_PATH = "AGENT_SESSIONS/wake-registry.json";
|
|
@@ -10001,9 +10043,9 @@ function listWakes(root, opts = {}) {
|
|
|
10001
10043
|
};
|
|
10002
10044
|
}
|
|
10003
10045
|
//#endregion
|
|
10004
|
-
//#region src/cro.ts
|
|
10046
|
+
//#region src/wake/cro.ts
|
|
10005
10047
|
/**
|
|
10006
|
-
* cro.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
|
|
10048
|
+
* wake/cro.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
|
|
10007
10049
|
*
|
|
10008
10050
|
* ## 这东西用来干什么(白话)
|
|
10009
10051
|
*
|
|
@@ -10044,7 +10086,7 @@ function listWakes(root, opts = {}) {
|
|
|
10044
10086
|
* ## 🔴 铁律:CRO 的任何失败,绝不影响既有机制(设计 §5)
|
|
10045
10087
|
*
|
|
10046
10088
|
* `send-later` / `send-now` / 唤醒表的投递**照常工作**。
|
|
10047
|
-
* 先例 = `
|
|
10089
|
+
* 先例 = `im/hook.ts` 的**旁路容忍**("超时 kill / 非 0 退出 / spawn 失败 → 仅日志返回,绝不抛")。
|
|
10048
10090
|
* ⇒ 本模块的**每一个**导出函数**都不抛错**(返回结构化失败)。
|
|
10049
10091
|
*/
|
|
10050
10092
|
/** 入口文件名(**全写**,所有者令 2026-09-19;不改缩写) */
|
|
@@ -10063,7 +10105,7 @@ const CRO_TIMEOUT_MS = 6e4;
|
|
|
10063
10105
|
const CRO_STDOUT_MAX = 65536;
|
|
10064
10106
|
/** 绑定文件体积上限(它只是个机器名 ⇒ 超过这个数一定是写错了,不读) */
|
|
10065
10107
|
const CRO_HOST_FILE_MAX = 1024;
|
|
10066
|
-
/** SESSION.md 文件名(与 `
|
|
10108
|
+
/** SESSION.md 文件名(与 `trajectory/core.ts` 同值;此处本地常量避免反向依赖) */
|
|
10067
10109
|
const SESSION_MD = "SESSION.md";
|
|
10068
10110
|
/**
|
|
10069
10111
|
* 展示码派生(**只用于展示**,不作识别依据 —— §0I U4:锚永远是 `dirName`)。
|
|
@@ -10188,7 +10230,7 @@ function parseCroOutput(stdout) {
|
|
|
10188
10230
|
* CRO 程序路径:`<CCC 根>/AGENT_SESSIONS/<dirName>/continuous-re-occurrence.ts`。
|
|
10189
10231
|
*
|
|
10190
10232
|
* ⚠️ 路径逃逸校验(`resolveInside`):`dirName` 来自配置/工具入参,
|
|
10191
|
-
* 必须确保解析后仍在 CCC 根内(同 `
|
|
10233
|
+
* 必须确保解析后仍在 CCC 根内(同 `im/hook.ts:160` 手法)。**抛错由调用方吞**。
|
|
10192
10234
|
* @param root CCC 根
|
|
10193
10235
|
* @param dirName 轨迹目录名
|
|
10194
10236
|
* @returns 绝对路径
|
|
@@ -10365,7 +10407,7 @@ function listCroCognitiveTrajectories(root) {
|
|
|
10365
10407
|
/**
|
|
10366
10408
|
* 执行一次 CRO 程序(**永不抛** —— 旁路容忍铁律)。
|
|
10367
10409
|
*
|
|
10368
|
-
* runner 顺序(照 `
|
|
10410
|
+
* runner 顺序(照 `im/hook.ts:171-174` 的 bun 优先 / node 兜底):
|
|
10369
10411
|
* `bun` → `process.execPath`(同运行时)。
|
|
10370
10412
|
* 判据:`ENOENT`(二进制不存在)⇒ 试下一个;**其余失败视为最终结果**
|
|
10371
10413
|
* (程序自己报错就是报错,不该用另一个 runner 掩盖——与 hook 一致)。
|
|
@@ -10627,9 +10669,9 @@ function registerDisposer(ctx, label, cleanup) {
|
|
|
10627
10669
|
}
|
|
10628
10670
|
}
|
|
10629
10671
|
//#endregion
|
|
10630
|
-
//#region src/cro-turns.ts
|
|
10672
|
+
//#region src/wake/cro-turns.ts
|
|
10631
10673
|
/**
|
|
10632
|
-
* cro-turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
|
|
10674
|
+
* wake/cro-turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
|
|
10633
10675
|
*
|
|
10634
10676
|
* ## 为什么需要这个模块(CRO 唯一需要 ACC 新增的状态)
|
|
10635
10677
|
*
|
|
@@ -10643,7 +10685,7 @@ function registerDisposer(ctx, label, cleanup) {
|
|
|
10643
10685
|
* | 事件 | 动作 | 出处 |
|
|
10644
10686
|
* |---|---|---|
|
|
10645
10687
|
* | `agent/created` | 标记「在跑」 | `host/contract.ts`(dsp 已在 `seams/context.ts` 订阅)。⚠️ v0.1.7 前名为 `agent/session-start`;新事件 `@mode = serial` ⇒ 处理函数须快速返回 |
|
|
10646
|
-
* | `agent/turn-stopping` | 清除标记 | `host/contract.ts:277`(dsp 已在 `rebuild.ts:442` 等 4 处订阅) |
|
|
10688
|
+
* | `agent/turn-stopping` | 清除标记 | `host/contract.ts:277`(dsp 已在 `trajectory/rebuild.ts:442` 等 4 处订阅) |
|
|
10647
10689
|
* | `agent/disposed` | **清该载体的项** | `host/contract.ts:280` |
|
|
10648
10690
|
* | `session/disposed` | **清该载体的项** | `host/contract.ts:283` |
|
|
10649
10691
|
*
|
|
@@ -10755,13 +10797,13 @@ function runningCarriersOf(boundSessionIds, nowMs, ttlMs = TURN_TTL_MS) {
|
|
|
10755
10797
|
* 🔴 **本处顺带更正 §2.7 的一处分档**:本文件原被列在"剩余 3 个属**真依赖**档"里,
|
|
10756
10798
|
* 但**逐符号查证后与那个判断不符** ——
|
|
10757
10799
|
* · `Context`:`ctx` 只被**结构化访问**(原码本就写着 `ctx as unknown as { on: … }`)
|
|
10758
|
-
* ⇒ 该注解未提供任何类型保证(与 `
|
|
10800
|
+
* ⇒ 该注解未提供任何类型保证(与 `role/worker/idle.ts` 同档)。
|
|
10759
10801
|
* · `Agent`:**导入后从未使用** —— 事件 payload 一律按 `{ agent?: unknown }` 解构,
|
|
10760
10802
|
* 再交给 `sessionIdOf(...)`(它收 `unknown`)。⇒ **纯粹是死 import**。
|
|
10761
10803
|
* ⇒ **判据(本件再证一次,且比上一件更硬)**:分档**必须逐符号看"它有没有参与类型检查"**,
|
|
10762
10804
|
* **不能按文件整档划分** —— 同一个文件里可以并存"真依赖符号"与"纯名义符号"。
|
|
10763
|
-
* 🔵 **剩余 2 个文件**才是真依赖档:`rebuild.ts` 用 `Session`/`SessionEvent`/
|
|
10764
|
-
* `deriveEventMessage`/`Agent` 等**真调用**的类型;`wake
|
|
10805
|
+
* 🔵 **剩余 2 个文件**才是真依赖档:`trajectory/rebuild.ts` 用 `Session`/`SessionEvent`/
|
|
10806
|
+
* `deriveEventMessage`/`Agent` 等**真调用**的类型;`wake/scheduler.ts` 用 `Agent` 的
|
|
10765
10807
|
* `followup`/`steer`(**真调用**)⇒ 它们走 §2.7-③ **选项 (b)** 或把中立接口下沉到 L0,
|
|
10766
10808
|
* **另立一件**(D85 一次只改一件事)。
|
|
10767
10809
|
* @param ctx 插件上下文(**结构化访问** `on`,故类型为 `unknown`,见下方 I2 说明)
|
|
@@ -10803,9 +10845,9 @@ function registerCroTurnTracking(ctx) {
|
|
|
10803
10845
|
});
|
|
10804
10846
|
}
|
|
10805
10847
|
//#endregion
|
|
10806
|
-
//#region src/cro-log.ts
|
|
10848
|
+
//#region src/wake/cro-log.ts
|
|
10807
10849
|
/**
|
|
10808
|
-
* cro-log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
|
|
10850
|
+
* wake/cro-log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
|
|
10809
10851
|
*
|
|
10810
10852
|
* ## 所有者令(本模块的存在理由)
|
|
10811
10853
|
*
|
|
@@ -10837,11 +10879,11 @@ function registerCroTurnTracking(ctx) {
|
|
|
10837
10879
|
* ## 🔴 轮转(所有者明示「程序化的」)
|
|
10838
10880
|
*
|
|
10839
10881
|
* **写入时按窗裁剪**:每次落盘只保留 `at >= now - CRO_LOG_RETENTION_MS` 的条目,
|
|
10840
|
-
* 随后**原子写**(`tmp + rename`,与 `wake
|
|
10882
|
+
* 随后**原子写**(`tmp + rename`,与 `wake/registry.ts` 同款形态)。
|
|
10841
10883
|
* ⇒ 清理与写入**是同一笔操作** ⇒ **结构上不存在"忘了清"的可能**(比"另设一个每 tick 的清理步"更硬:
|
|
10842
10884
|
* 那多一个"可能忘了跑"的面)。窗宽是**常量**,**无配置键、无人工清理动作**。
|
|
10843
10885
|
*
|
|
10844
|
-
* ## 🔴 铁律(与 `cro.ts` 同族)
|
|
10886
|
+
* ## 🔴 铁律(与 `wake/cro.ts` 同族)
|
|
10845
10887
|
*
|
|
10846
10888
|
* **本模块的每个导出函数都不抛错**:写盘 / 裁剪 / 解析失败一律返回结构化失败,
|
|
10847
10889
|
* 由调用方只记一行 tick 日志 ⇒ **CRO 流水的问题绝不影响投递与既有链路**(设计 §5)。
|
|
@@ -11007,12 +11049,12 @@ function appendCroWakeLog(root, dirName, input, nowMs = Date.now()) {
|
|
|
11007
11049
|
}
|
|
11008
11050
|
}
|
|
11009
11051
|
//#endregion
|
|
11010
|
-
//#region src/clock
|
|
11052
|
+
//#region src/wake/clock.ts
|
|
11011
11053
|
/**
|
|
11012
|
-
* clock
|
|
11054
|
+
* wake/clock.ts — **时钟运行时工厂**(S142 §38 复审候选 C6b,路径 **P2**)
|
|
11013
11055
|
*
|
|
11014
11056
|
* 🔴 2026-09-25(S142 分批清死代码·第 1 批)**更正**:本文件当初是为**两条**独立 5min 时钟抽出的公共引擎:
|
|
11015
|
-
* · 唤醒调度器(`wake
|
|
11057
|
+
* · 唤醒调度器(`wake/scheduler.ts`,一次性唤醒,D58)—— **现存**
|
|
11016
11058
|
* · autopilot 时钟(`autopilot-trajectory.ts`,周期自唤醒,D59)—— 🔴 **已随 v1.35.0 整段退场**
|
|
11017
11059
|
* 二者的**业务判据完全不同**(谁来扫 / 扫什么 / 投递给谁 / 按什么周期),但**引擎骨架逐字同形**
|
|
11018
11060
|
* ——进程态 + 快照 + 复位 + `startTimer` + tick 外壳 + 事件接线 + disposer + `timer.unref()`。
|
|
@@ -11059,7 +11101,7 @@ function appendCroWakeLog(root, dirName, input, nowMs = Date.now()) {
|
|
|
11059
11101
|
* **本工厂是"加法式"重构**(P2 的性质):不新增任何判定逻辑,只把同形骨架收成一份代码;
|
|
11060
11102
|
* 两个模块仍各自导出 `wakeSchedulerState()` / `autopilotClockState()` 与各自的
|
|
11061
11103
|
* `__reset…ForTest()`,**对外可观测面(字段名与语义)零变化**——`acc-diag` ①b 段、
|
|
11062
|
-
* `
|
|
11104
|
+
* `surface/observation.ts:containerClocks()` 与面板都在读它们(见该文件 :225–250 的设计红线)。
|
|
11063
11105
|
*
|
|
11064
11106
|
* 为什么**不**取 P1(真合并成一个 `setInterval` + 两条策略)——2026-09-15 所有者裁决:
|
|
11065
11107
|
* P1 只多省 ~20 行,却要把 `armed` 拆成"宿主 + 每策略"两层、并强行分开串行域,
|
|
@@ -11215,9 +11257,9 @@ function createClock(opts) {
|
|
|
11215
11257
|
return api;
|
|
11216
11258
|
}
|
|
11217
11259
|
//#endregion
|
|
11218
|
-
//#region src/wake
|
|
11260
|
+
//#region src/wake/scheduler.ts
|
|
11219
11261
|
/**
|
|
11220
|
-
* wake
|
|
11262
|
+
* wake/scheduler.ts — 认知轨迹唤醒调度器(D58/D59/D60;S142 2026-09-13)
|
|
11221
11263
|
*
|
|
11222
11264
|
* 一个**中心调度器**(5min tick,复用 autopilot 的节律常量),处理 CCC 唤醒注册表里
|
|
11223
11265
|
* 到期的条目:把「未来时刻 + 一条 message」投递给目标认知轨迹,**fire-and-forget**
|
|
@@ -11964,9 +12006,9 @@ function registerWakeScheduler(ctx) {
|
|
|
11964
12006
|
registerDisposer(ctx, "认知轨迹唤醒调度器", () => clock.dispose());
|
|
11965
12007
|
}
|
|
11966
12008
|
//#endregion
|
|
11967
|
-
//#region src/cro-guide.ts
|
|
12009
|
+
//#region src/wake/cro-guide.ts
|
|
11968
12010
|
/**
|
|
11969
|
-
* cro-guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
|
|
12011
|
+
* wake/cro-guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
|
|
11970
12012
|
*
|
|
11971
12013
|
* ## 为什么指南在 ACC 而不在 CCC(所有者令)
|
|
11972
12014
|
*
|
|
@@ -12212,7 +12254,7 @@ ${renderCroSampleSnapshot()}
|
|
|
12212
12254
|
* 动作面现行 **8 个**:list / show / create / use / rebuild / **`send-now`** / **`send-later`** / **`cro-guide`**。
|
|
12213
12255
|
* 🔴 2026-09-16(所有者裁决):**第 7 个动作 `send-message`(即时投递)**
|
|
12214
12256
|
* ——与 `wake-later` 共用取用通路,差别只在**时刻**(现在 vs 未来)与**回执**(有 vs 无);
|
|
12215
|
-
* 实现落在 `wake
|
|
12257
|
+
* 实现落在 `wake/scheduler.ts` 的 `sendToTrajectory()`("怎么把一句话送到一条轨迹"是调度器的知识)。
|
|
12216
12258
|
* 设计稿 = `docs/cognitive-trajectory-send-message-design.md`(v0.2 定稿)。
|
|
12217
12259
|
* 🔴 2026-09-17(所有者裁决 · **命名 A 案**):**两个投递动作更名** —— `wake-later` → **`send-later`**、
|
|
12218
12260
|
* `send-message` → **`send-now`**。判据(R↓):两者本是**同一条投递通路**(同取用机制 / 同载荷 /
|
|
@@ -12350,6 +12392,8 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12350
12392
|
"cognitive_trajectory — one cognitive trajectory: its persistent body (SESSION.md) + its timeline.",
|
|
12351
12393
|
"A cognitive trajectory is NOT the conversation: SESSION.md is the durable body and never moves;",
|
|
12352
12394
|
"the dsh conversation is only its rebuildable carrier (Ship of Theseus — the carrier is replaced, the cognitive trajectory continues).",
|
|
12395
|
+
"IDENTITY: every trajectory has a CODE — the fixed-position segment of its directory name (YYYY-MM-DD--<code>[--<desc>]).",
|
|
12396
|
+
"The code IS the trajectory's unique identifier, and you can choose it at creation time with create --id.",
|
|
12353
12397
|
"A COGNITIVE TRAJECTORY HAS NO LIFECYCLE — it does not complete, close, or expire, and age never counts against it;",
|
|
12354
12398
|
"it PERMANENTLY EXISTS. The only thing that ever changes is WHERE it is stored: archive moves it out of the active list",
|
|
12355
12399
|
"without touching a single byte of its directory or SESSION.md. There is no reverse action — to restore one, use container_fs mv.",
|
|
@@ -12376,12 +12420,13 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12376
12420
|
"ASYMMETRY (deliberate — the two differ in whether a missing target is normal): send-now creates a target on demand,",
|
|
12377
12421
|
"because sending to a never-used cognitive trajectory is a legitimate intent; send-later does NOT,",
|
|
12378
12422
|
"because scheduling for a cognitive trajectory that has never existed is almost always a mistyped name — auto-creating would hide that error behind an empty cognitive trajectory.",
|
|
12379
|
-
"PARAMETERS: create requires --desc <desc> [--goal] or --issue <ticket> (exactly one) plus --summary (≤20 chars).",
|
|
12423
|
+
"PARAMETERS: create requires --desc <desc> [--id <code>] [--goal] or --issue <ticket> (exactly one) plus --summary (≤20 chars).",
|
|
12380
12424
|
"use requires --summary (≤20 chars) and may pass --force to switch away from the currently-bound cognitive trajectory.",
|
|
12381
12425
|
"rebuild requires --summary (next-phase summary ≤20 chars) + optional --note (task focus for the rebuilt self).",
|
|
12382
|
-
"send-later requires target (
|
|
12426
|
+
"send-later requires target (a code or AGENT_SESSIONS dir name) + at (RFC3339 or +30m/+2h) + message.",
|
|
12383
12427
|
"send-now requires target + message.",
|
|
12384
|
-
"
|
|
12428
|
+
"uid: READS a legacy TRAJECTORY-UID only — setting one is retired (codes replaced it). Use create --id instead.",
|
|
12429
|
+
"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."
|
|
12385
12430
|
].join(" "),
|
|
12386
12431
|
parameters: {
|
|
12387
12432
|
action: {
|
|
@@ -12391,8 +12436,8 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12391
12436
|
description: [
|
|
12392
12437
|
"Subcommand — grouped by effect.",
|
|
12393
12438
|
"LIFECYCLE (changes THIS conversation's binding):",
|
|
12394
|
-
"list (inventory + stats + anomaly marks) / show (
|
|
12395
|
-
"create (new cognitive trajectory;
|
|
12439
|
+
"list (inventory + stats + anomaly marks) / show (a code, dir name, or fuzzy keyword) /",
|
|
12440
|
+
"create (new cognitive trajectory; --id sets its code. Activates itself when you have no binding, otherwise leaves your binding untouched and tells you how to activate it) /",
|
|
12396
12441
|
"use (activate context for THIS conversation; inline integrity check — pass silent, problems reported as hints, not errors) /",
|
|
12397
12442
|
"rebuild (clear-and-rebuild current conversation — requires --summary + optional --note) /",
|
|
12398
12443
|
"archive (move a trajectory to the archived area; one-way, refuses to overwrite, content untouched — --dryRun previews).",
|
|
@@ -12404,11 +12449,11 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12404
12449
|
},
|
|
12405
12450
|
name: {
|
|
12406
12451
|
type: "string",
|
|
12407
|
-
description: "show/use/archive/uid identifier — S### / dir name / UID / fuzzy keyword"
|
|
12452
|
+
description: "show/use/archive/uid identifier — a trajectory code (S### or a custom code) / dir name / legacy UID / fuzzy keyword"
|
|
12408
12453
|
},
|
|
12409
12454
|
uid: {
|
|
12410
12455
|
type: "string",
|
|
12411
|
-
description: `uid:
|
|
12456
|
+
description: `uid: READ-ONLY legacy field. Omit to read a trajectory's legacy ${UID_FILENAME}. Passing any value is REJECTED — setting a UID is retired (2026-10-19); a trajectory's code is its unique identity now, and you choose it at creation with create --id <code>.`
|
|
12412
12457
|
},
|
|
12413
12458
|
note: {
|
|
12414
12459
|
type: "string",
|
|
@@ -12418,6 +12463,10 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12418
12463
|
type: "string",
|
|
12419
12464
|
description: "create: short description (any language, ≤5 words; mutually exclusive with issue)"
|
|
12420
12465
|
},
|
|
12466
|
+
id: {
|
|
12467
|
+
type: "string",
|
|
12468
|
+
description: "create: the trajectory CODE to assign (its stable unique identity — this is what you later pass as name/target). Omit to auto-assign S###. Allowed [A-Za-z0-9._-], starts alphanumeric, ≤64, must NOT contain Chinese or \"--\" (that is the dir-name separator); must not collide with an active trajectory code."
|
|
12469
|
+
},
|
|
12421
12470
|
issue: {
|
|
12422
12471
|
type: "string",
|
|
12423
12472
|
description: "create: ticket number (e.g. apaas-26116; dir named YYYY-MM-DD--<issue>; mutually exclusive with desc)"
|
|
@@ -12471,6 +12520,7 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12471
12520
|
root,
|
|
12472
12521
|
desc: args.desc,
|
|
12473
12522
|
issue: args.issue,
|
|
12523
|
+
id: args.id,
|
|
12474
12524
|
goal: args.goal,
|
|
12475
12525
|
dryRun: isDryRun
|
|
12476
12526
|
});
|
|
@@ -12572,26 +12622,18 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
12572
12622
|
};
|
|
12573
12623
|
}
|
|
12574
12624
|
case "uid": {
|
|
12575
|
-
if (!args.name) throw new Error("uid requires name (S### / dir name / existing UID)");
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
return {
|
|
12581
|
-
ok: true,
|
|
12582
|
-
dirName: entry.dirName,
|
|
12583
|
-
uid: cur,
|
|
12584
|
-
note: cur === null ? `未设定 UID ⇒ 用编码指认(${entry.dirName.match(/--(S\d{3,})--/)?.[1] ?? "无编码"})。` : `当前 UID = ${cur}(文件 ${UID_FILENAME})`
|
|
12585
|
-
};
|
|
12586
|
-
}
|
|
12587
|
-
const next = args.uid === "" ? null : String(args.uid);
|
|
12588
|
-
const r = setTrajectoryUid(join(root, "AGENT_SESSIONS"), args.name, next);
|
|
12625
|
+
if (!args.name) throw new Error("uid requires name (code / S### / dir name / existing UID)");
|
|
12626
|
+
const entry = findSession(join(root, "AGENT_SESSIONS"), args.name);
|
|
12627
|
+
if (!entry) throw new Error(`Trajectory not found: "${args.name}"`);
|
|
12628
|
+
const cur = readTrajectoryUid(entry.path);
|
|
12629
|
+
if (args.uid !== void 0) throw new Error("Setting a trajectory UID has been retired (2026-10-19). A trajectory's code IS its unique identity now, and you can choose it at creation time: cognitive_trajectory create --id <code> --desc <desc> --summary <summary>. Existing TRAJECTORY-UID files are still READ (so old handles keep resolving) — they are just never written.");
|
|
12589
12630
|
return {
|
|
12590
12631
|
ok: true,
|
|
12591
|
-
dirName:
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12632
|
+
dirName: entry.dirName,
|
|
12633
|
+
code: parseTrajectoryCode(entry.dirName),
|
|
12634
|
+
uid: cur,
|
|
12635
|
+
retired: true,
|
|
12636
|
+
note: cur === null ? `未设定遗留 UID ⇒ 用编码指认(${parseTrajectoryCode(entry.dirName) ?? "无编码"})。UID 写入已退役;要一条可指定的标识,请用 create --id 起新轨迹。` : `遗留 UID = ${cur}(文件 ${UID_FILENAME},只读兼容)——仍可用它指认本条轨迹。⚠️ 写入已退役:新轨迹用 create --id 指定编码。`
|
|
12595
12637
|
};
|
|
12596
12638
|
}
|
|
12597
12639
|
case "send-later": {
|
|
@@ -12672,12 +12714,12 @@ function advisoryHint(dirName, mdPath) {
|
|
|
12672
12714
|
return null;
|
|
12673
12715
|
}
|
|
12674
12716
|
//#endregion
|
|
12675
|
-
//#region src/tools/localstore.ts
|
|
12717
|
+
//#region src/tools/localstore/index.ts
|
|
12676
12718
|
/**
|
|
12677
12719
|
* localstore.ts — localstore 真实 DSH 工具定义(defineTool)
|
|
12678
12720
|
*
|
|
12679
12721
|
* ACC 标准本地凭据/配置存储(S133 设计,S134 重设计:CCC 根 localstore.json)。
|
|
12680
|
-
* 进程内注册,零 DSH 依赖逻辑在 localstore
|
|
12722
|
+
* 进程内注册,零 DSH 依赖逻辑在 tools/localstore/service.ts(可单测)。
|
|
12681
12723
|
* doc 子命令输出存储规范,agent 可直接用 fs 工具(read/write)自己读写。
|
|
12682
12724
|
*/
|
|
12683
12725
|
const ACTIONS$1 = [
|
|
@@ -12730,106 +12772,96 @@ const localstoreTool = defineTool({
|
|
|
12730
12772
|
});
|
|
12731
12773
|
//#endregion
|
|
12732
12774
|
//#region src/tools/skiff-admin.ts
|
|
12733
|
-
/** Skiff
|
|
12734
|
-
const SKIFF_GUIDE = `═══ Skiff
|
|
12775
|
+
/** Skiff 定义教程:概念 / `skiff.json` schema / 双白名单 / 轨迹纪律 / 微信路由 */
|
|
12776
|
+
const SKIFF_GUIDE = `═══ Skiff Guide (2026-10-19 redesigned) ═══
|
|
12735
12777
|
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12778
|
+
**A skiff IS a cognitive trajectory — the one whose directory contains \`${SKIFF_PROFILE_FILENAME}\`.**
|
|
12779
|
+
That is the whole rule. No role table, no name registry: put the file there and the trajectory
|
|
12780
|
+
becomes a skiff; delete it and it goes back to being an ordinary trajectory (takes effect at once).
|
|
12739
12781
|
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
"msms": ["msm-a", "msm-b"], // MSM 白名单(独立):msm 工具只能执行这些;register/deregister 必拒
|
|
12745
|
-
"tools": ["read","grep","glob",...], // 非 MSM 工具白名单(独立):白名单外工具一律不可用(guard 强制)
|
|
12746
|
-
"cognitiveTrajectory": { "session": false, "keeper": false, "rebuild": false }, // 轨迹纪律子集(缺省全关 = 完全独立)
|
|
12747
|
-
// 角色人格/认知边界/风格(CCC 完整定义;dsp 只给基础提示词)
|
|
12748
|
-
"systemPromptFile": ".opencode/skiff/<role>.md" // ★ 推荐:引用 md 文件(相对 CCC 根;超长提示词在 JSON 内嵌不可读)
|
|
12749
|
-
// "systemPrompt": "..." // 兼容:内嵌字符串仍可用;两者都设时 systemPromptFile 优先
|
|
12750
|
-
}
|
|
12751
|
-
} } }
|
|
12782
|
+
AGENT_SESSIONS/<YYYY-MM-DD--<code>--<desc>>/
|
|
12783
|
+
├─ SESSION.md ← its persistent body (this IS the workspace — nothing is auto-created)
|
|
12784
|
+
├─ ${SKIFF_PROFILE_FILENAME} ← its constraints (this file is what makes it a skiff)
|
|
12785
|
+
└─ TRAJECTORY-UID ← legacy, read-only (superseded by the code)
|
|
12752
12786
|
|
|
12753
|
-
──
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12787
|
+
── ${SKIFF_PROFILE_FILENAME} schema ──
|
|
12788
|
+
{
|
|
12789
|
+
"model": "provider/model", // optional; falls back to cheap-worker.defaultModel
|
|
12790
|
+
"tools": ["read","grep","msm",...], // non-MSM toolbar whitelist — anything not listed is REMOVED from the schema
|
|
12791
|
+
"msms": ["msm-a","msm-b"], // MSM whitelist — msm exec may only run these; register/deregister always refused
|
|
12792
|
+
"cognitiveTrajectory": { "session": true, "keeper": true, "rebuild": true }, // defaults TRUE (see below)
|
|
12793
|
+
"systemPromptFile": ".opencode/skiff/<role>.md", // recommended: relative to CCC root, hot-reloads on save
|
|
12794
|
+
"systemPrompt": "..." // inline fallback; systemPromptFile wins when both are set
|
|
12795
|
+
}
|
|
12757
12796
|
|
|
12758
|
-
──
|
|
12759
|
-
-
|
|
12760
|
-
-
|
|
12761
|
-
-
|
|
12762
|
-
- 注意:MSM 在脚本层执行(bun),文件访问不受 agent 工具面约束——"只读"语义靠 CCC 自写 MSM 自觉
|
|
12797
|
+
── Two whitelists, both default-deny ──
|
|
12798
|
+
- \`tools: []\` + non-empty \`msms\` = pure-MSM skiff (the usual knowledge-QA shape)
|
|
12799
|
+
- non-empty \`msms\` ⇒ the \`msm\` tool is added automatically (the only entry point for MSMs)
|
|
12800
|
+
- anything outside both lists is invisible in the schema, not merely rejected at call time
|
|
12763
12801
|
|
|
12764
|
-
──
|
|
12765
|
-
|
|
12766
|
-
|
|
12802
|
+
── cognitiveTrajectory: why the defaults are TRUE ──
|
|
12803
|
+
The workspace of a skiff is its own SESSION.md. Setting these to false would cut it off from
|
|
12804
|
+
its own body, so \`session\`/\`keeper\`/\`rebuild\` all default to true. Set false explicitly only
|
|
12805
|
+
when you deliberately want that participation off.
|
|
12767
12806
|
|
|
12768
|
-
──
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
→ 有操作能力(可写修复),参与 keeper 轨迹机制
|
|
12807
|
+
── WeChat routing: user → trajectory code ──
|
|
12808
|
+
\`weixin.routes\`: \`[{ "user": "<wx id or *>", "trajectory": "<code>" }]\`
|
|
12809
|
+
A route pointing at a code with no such trajectory is a hard error (the message is NOT delivered).
|
|
12810
|
+
A trajectory WITHOUT \`${SKIFF_PROFILE_FILENAME}\` is still a legal target — it is just an ordinary trajectory.
|
|
12773
12811
|
|
|
12774
|
-
──
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
生效机制:角色配置**实时读取**(改 .opencode/serenity.json → 刷新调试页即生效);
|
|
12782
|
-
改配置后用 container_admin role apply 做显式校验 + 应用确认(绑定 CCC + 角色清单)
|
|
12783
|
-
会话追问(v1.25.10):调试页持有当前会话,追问自动续接(同会话上下文延续);
|
|
12784
|
-
「新对话」按钮开新会话;进程重启后旧会话不可续(WebUI 仍可见历史)
|
|
12785
|
-
注意:skill 加载对 skiff **恒可用**(不设白名单——读知识面,无写能力)`;
|
|
12786
|
-
/** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ cheap-worker.models / systemPrompt 非空 */
|
|
12812
|
+
── Operating it ──
|
|
12813
|
+
Debug page: Settings → Serenity → enable the Skiff block → http://127.0.0.1:<debugPort>
|
|
12814
|
+
Everything is read live: saving \`${SKIFF_PROFILE_FILENAME}\` takes effect on the next turn, no restart.
|
|
12815
|
+
(The prompt FILE hot-reloads by mtime; the JSON itself is re-read every turn.)
|
|
12816
|
+
After editing, run container_admin role apply for an explicit validation + confirmation pass.
|
|
12817
|
+
Skill loading is always available to skiff sessions (skills are not whitelisted — they are read-only knowledge).`;
|
|
12818
|
+
/** 校验当前 CCC 的 skiff 约束面:msms 均已注册 / model ∈ cheap-worker.models / 提示词非空 */
|
|
12787
12819
|
function validateSkiffConfig(root) {
|
|
12788
|
-
const
|
|
12820
|
+
const profiles = listSkiffProfiles(root);
|
|
12789
12821
|
const issues = [];
|
|
12790
|
-
if (
|
|
12822
|
+
if (profiles.length === 0) return {
|
|
12791
12823
|
ok: true,
|
|
12792
12824
|
issues: [],
|
|
12793
12825
|
roleCount: 0,
|
|
12794
|
-
note:
|
|
12826
|
+
note: `no skiff trajectories in this CCC (no trajectory has ${SKIFF_PROFILE_FILENAME} — zero impact)`
|
|
12795
12827
|
};
|
|
12796
12828
|
const registered = new Set(loadMsmEntries(root).map((e) => e.name));
|
|
12797
12829
|
const hc = readCheapWorkerConfig(root);
|
|
12798
12830
|
const allowedModels = hc ? new Set(hc.models) : null;
|
|
12799
|
-
for (const
|
|
12800
|
-
|
|
12801
|
-
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12831
|
+
for (const p of profiles) {
|
|
12832
|
+
if (!(hasTrajectoryPrompt(root, p.dirName) || (() => {
|
|
12833
|
+
try {
|
|
12834
|
+
return resolveProfileSystemPrompt(root, p).trim() !== "";
|
|
12835
|
+
} catch (err) {
|
|
12836
|
+
issues.push(`trajectory "${p.code}": ${String(err?.message ?? err)}`);
|
|
12837
|
+
return true;
|
|
12838
|
+
}
|
|
12839
|
+
})())) issues.push(`trajectory "${p.code}": no trajectory prompt (add ${TRAJECTORY_PROMPT_FILENAME} in the trajectory dir — recommended — or the legacy systemPromptFile/systemPrompt)`);
|
|
12840
|
+
for (const m of p.msms) if (!registered.has(m)) issues.push(`trajectory "${p.code}": msms "${m}" is not registered in mech-registry.json`);
|
|
12841
|
+
if (p.model && allowedModels && !allowedModels.has(p.model)) issues.push(`trajectory "${p.code}": model "${p.model}" not in cheap-worker.models (${[...allowedModels].join(", ") || "(none)"})`);
|
|
12807
12842
|
}
|
|
12808
12843
|
return {
|
|
12809
12844
|
ok: issues.length === 0,
|
|
12810
12845
|
issues,
|
|
12811
|
-
roleCount:
|
|
12846
|
+
roleCount: profiles.length
|
|
12812
12847
|
};
|
|
12813
12848
|
}
|
|
12814
|
-
/** 列出当前 CCC
|
|
12849
|
+
/** 列出当前 CCC 的 skiff 轨迹(编码 / 模型 / msms / tools / 纪律摘要 / 提示词来源) */
|
|
12815
12850
|
function listSkiffRoles(root) {
|
|
12816
|
-
const
|
|
12817
|
-
if (
|
|
12851
|
+
const profiles = listSkiffProfiles(root);
|
|
12852
|
+
if (profiles.length === 0) return {
|
|
12818
12853
|
roles: [],
|
|
12819
|
-
note:
|
|
12820
|
-
};
|
|
12821
|
-
const items =
|
|
12822
|
-
name,
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
12827
|
-
|
|
12828
|
-
|
|
12829
|
-
|
|
12830
|
-
},
|
|
12831
|
-
promptSource: systemPromptSource(role),
|
|
12832
|
-
...role.systemPromptFile ? { systemPromptFile: role.systemPromptFile } : {}
|
|
12854
|
+
note: `no skiff trajectories in this CCC (no trajectory has ${SKIFF_PROFILE_FILENAME})`
|
|
12855
|
+
};
|
|
12856
|
+
const items = profiles.map((p) => ({
|
|
12857
|
+
name: p.code,
|
|
12858
|
+
dirName: p.dirName,
|
|
12859
|
+
model: p.model ?? "(cheap-worker default)",
|
|
12860
|
+
msms: p.msms,
|
|
12861
|
+
tools: p.tools,
|
|
12862
|
+
cognitiveTrajectory: profileCognitiveTrajectory(p),
|
|
12863
|
+
promptSource: profileSystemPromptSource(p),
|
|
12864
|
+
...p.systemPromptFile ? { systemPromptFile: p.systemPromptFile } : {}
|
|
12833
12865
|
}));
|
|
12834
12866
|
return {
|
|
12835
12867
|
roles: items,
|
|
@@ -12837,29 +12869,29 @@ function listSkiffRoles(root) {
|
|
|
12837
12869
|
};
|
|
12838
12870
|
}
|
|
12839
12871
|
/**
|
|
12840
|
-
* 应用当前 skiff 配置(
|
|
12872
|
+
* 应用当前 skiff 配置(S142 用户:改了 json 应该有个生效机制)。
|
|
12841
12873
|
*
|
|
12842
|
-
*
|
|
12843
|
-
* ①
|
|
12874
|
+
* 语义:约束面是**每轮实时读取**的 ⇒ apply 不是"推送",而是**显式校验 + 应用确认**:
|
|
12875
|
+
* ① 校验合法(复用 validate:msms 注册 / model 白名单 / 提示词非空)
|
|
12844
12876
|
* ② 非法 → 报告问题清单,不应用(提示修复后重跑 apply)
|
|
12845
|
-
* ③ 合法 → 应用确认:绑定 CCC +
|
|
12877
|
+
* ③ 合法 → 应用确认:绑定 CCC + 轨迹清单 + 说明(下一轮即生效)
|
|
12846
12878
|
*/
|
|
12847
12879
|
function applySkiffConfig(root) {
|
|
12848
12880
|
const validation = validateSkiffConfig(root);
|
|
12849
|
-
const
|
|
12881
|
+
const profiles = listSkiffProfiles(root);
|
|
12850
12882
|
if (!validation.ok) return {
|
|
12851
12883
|
applied: false,
|
|
12852
12884
|
cccRoot: root,
|
|
12853
|
-
roleCount:
|
|
12885
|
+
roleCount: profiles.length,
|
|
12854
12886
|
issues: validation.issues,
|
|
12855
12887
|
hint: "fix the issues above, then run container_admin role apply again"
|
|
12856
12888
|
};
|
|
12857
12889
|
return {
|
|
12858
12890
|
applied: true,
|
|
12859
12891
|
cccRoot: root,
|
|
12860
|
-
roleCount:
|
|
12861
|
-
roles:
|
|
12862
|
-
note:
|
|
12892
|
+
roleCount: profiles.length,
|
|
12893
|
+
roles: profiles.map((p) => p.code),
|
|
12894
|
+
note: `constraints are read live from each trajectory's ${SKIFF_PROFILE_FILENAME} — they take effect on the next turn (no restart); ${SKIFF_PROFILE_HINT}`
|
|
12863
12895
|
};
|
|
12864
12896
|
}
|
|
12865
12897
|
//#endregion
|
|
@@ -13002,18 +13034,18 @@ function createContainerAdminTool(ctx) {
|
|
|
13002
13034
|
});
|
|
13003
13035
|
}
|
|
13004
13036
|
//#endregion
|
|
13005
|
-
//#region src/im
|
|
13037
|
+
//#region src/im/channels.ts
|
|
13006
13038
|
/**
|
|
13007
|
-
* im
|
|
13039
|
+
* im/channels.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
|
|
13008
13040
|
*
|
|
13009
13041
|
* 用户原话:「既然微信桥是我们 ACC 提供的,那么 weixin-send 应该是我们 ACC 提供的能力,
|
|
13010
13042
|
* 当用户配置了微信桥则可用,不配置则不可见」+「设定一个复杂的多层工具,叫 im-bridge,
|
|
13011
13043
|
* 里面有微信,然后保留后续可能兼容别的 im 的空间」。
|
|
13012
13044
|
*
|
|
13013
13045
|
* 分层(E↑):
|
|
13014
|
-
* `tools/im
|
|
13046
|
+
* `tools/im/channels.ts`(工具面:参数 → 结果渲染)
|
|
13015
13047
|
* → **本模块**(通道注册表 + 可见性判据 + 动作分发 + 错误码翻译)
|
|
13016
|
-
* → 通道实现(`im-
|
|
13048
|
+
* → 通道实现(`im/weixin-channel.ts`:协议/凭据/账号/记录)
|
|
13017
13049
|
*
|
|
13018
13050
|
* 边界(R↓):
|
|
13019
13051
|
* - **本模块零宿主依赖**(不 import cordis/agent)——可见性判定与分发都是纯逻辑,可独立单测。
|
|
@@ -13286,16 +13318,15 @@ function isWriteTool(toolName, action) {
|
|
|
13286
13318
|
*/
|
|
13287
13319
|
function decideGuard(input) {
|
|
13288
13320
|
const { root, toolName, safeModeOn, blacklist, pathArg, action } = input;
|
|
13289
|
-
if (input.
|
|
13321
|
+
if (input.skiffProfile !== void 0 && input.skiffProfile !== null) {
|
|
13322
|
+
const profile = input.skiffProfile;
|
|
13290
13323
|
if (toolName === "skill") return { kind: "allow" };
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
if ((role?.msms ?? []).includes(toolName)) return {
|
|
13294
|
-
deny: `"${toolName}" is a registered MSM, not a direct tool in this role — call it through the msm tool: msm("${toolName}", ["<args>"]) (pass "--help" or inspect=true for usage)`,
|
|
13324
|
+
if (profile.msms.includes(toolName)) return {
|
|
13325
|
+
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)`,
|
|
13295
13326
|
kind: "deny"
|
|
13296
13327
|
};
|
|
13297
|
-
if (!
|
|
13298
|
-
deny: "tool not allowed in this skiff
|
|
13328
|
+
if (!profileToolWhitelist(profile).has(toolName)) return {
|
|
13329
|
+
deny: "tool not allowed in this skiff trajectory",
|
|
13299
13330
|
kind: "deny"
|
|
13300
13331
|
};
|
|
13301
13332
|
}
|
|
@@ -13450,7 +13481,7 @@ function syncSafeModeRestriction(agent, root) {
|
|
|
13450
13481
|
});
|
|
13451
13482
|
writeRestrictDiag(root);
|
|
13452
13483
|
}
|
|
13453
|
-
/** 条件可见的 IM 工具名(与 tools/im
|
|
13484
|
+
/** 条件可见的 IM 工具名(与 tools/im/channels.ts 的注册名一致) */
|
|
13454
13485
|
const IM_BRIDGE_TOOL_NAME = "im-bridge";
|
|
13455
13486
|
/** agent key → restrict disposer(im-bridge 隐藏状态;key 同安全模式 = 会话 id) */
|
|
13456
13487
|
const imBridgeRestrictions = /* @__PURE__ */ new Map();
|
|
@@ -13582,26 +13613,25 @@ const exclusiveRestrictions = /* @__PURE__ */ new Map();
|
|
|
13582
13613
|
*/
|
|
13583
13614
|
const skiffRoleRestrictions = /* @__PURE__ */ new Map();
|
|
13584
13615
|
/**
|
|
13585
|
-
* 同步 **
|
|
13616
|
+
* 同步 **skiff 会话的工具面**:把白名单之外的工具**从 schema 移除**(不是"看得到但被拒")。
|
|
13586
13617
|
*
|
|
13587
13618
|
* ## 为什么必须有这条(owner 2026-10-19:「工具清单也要热加载」)
|
|
13588
13619
|
*
|
|
13589
|
-
* 在它之前,skiff
|
|
13590
|
-
* 工具全部出现在模型 schema
|
|
13591
|
-
* ⇒ 两个真实后果:
|
|
13620
|
+
* 在它之前,skiff 白名单**只由运行时守卫拦截**(`decideGuard` 的 skiff 分支):
|
|
13621
|
+
* 工具全部出现在模型 schema 里,调用时才被打回。⇒ 两个真实后果:
|
|
13592
13622
|
* ① **模型看得见它不该有的工具** —— 它会去试,试了才撞墙(一轮白烧,且错误文案泛化,
|
|
13593
13623
|
* 看不出"是不在清单里"还是"临时性拒绝");
|
|
13594
|
-
* ② **配置变动在 schema 上永远不生效** —— 改 `skiff.
|
|
13595
|
-
* 不影响"模型看得见什么"。这正是 owner 报的"工具清单不热加载"。
|
|
13624
|
+
* ② **配置变动在 schema 上永远不生效** —— 改 `skiff.json` 的 `tools` 只影响"调用是否放行"。
|
|
13596
13625
|
*
|
|
13597
13626
|
* ## 判据与降级
|
|
13598
13627
|
*
|
|
13599
|
-
* - deny 集合 = **`REGISTERED_TOOLS` −
|
|
13628
|
+
* - deny 集合 = **`REGISTERED_TOOLS` − 白名单**(只对**我们自己注册的**工具算,
|
|
13600
13629
|
* 不去动宿主的工具 —— 宿主的工具由各自的条件同步管)。
|
|
13601
13630
|
* - ⚠️ **未注册的名字会让整次 restrict 失败**(宿主契约逐字 "unknown names … fail"),
|
|
13602
13631
|
* 而 `diagram` 是按开关注册的 ⇒ 与 `hideAllFor` 同款**两档尝试**(先全覆盖,失败退常驻段)。
|
|
13603
|
-
* -
|
|
13604
|
-
*
|
|
13632
|
+
* - 读不到约束面(不是 skiff / 配置删了)⇒ **撤销既有隐藏**(回到"无约束"):
|
|
13633
|
+
* ⛔ 不能只是 `return` —— 那会把上一步的隐藏**永久留在**一个已不再是 skiff 的会话上
|
|
13634
|
+
* (这正是 owner 说的"删掉 `skiff.json` 就该立刻恢复")。
|
|
13605
13635
|
* - 幂等:算出来的 deny 签名不变就跳过(不每步重建 schema)。
|
|
13606
13636
|
*
|
|
13607
13637
|
* @param agent 目标会话
|
|
@@ -13609,20 +13639,20 @@ const skiffRoleRestrictions = /* @__PURE__ */ new Map();
|
|
|
13609
13639
|
*/
|
|
13610
13640
|
function syncSkiffRoleToolVisibility(agent, root) {
|
|
13611
13641
|
const sessionId = agent.session.id ?? "global";
|
|
13612
|
-
|
|
13613
|
-
const
|
|
13614
|
-
if (
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
13620
|
-
|
|
13642
|
+
const profile = skiffProfileForSession(root, sessionId);
|
|
13643
|
+
const existing = skiffRoleRestrictions.get(sessionId);
|
|
13644
|
+
if (profile === null) {
|
|
13645
|
+
if (existing !== void 0) {
|
|
13646
|
+
try {
|
|
13647
|
+
existing.dispose();
|
|
13648
|
+
} catch {}
|
|
13649
|
+
skiffRoleRestrictions.delete(sessionId);
|
|
13650
|
+
}
|
|
13621
13651
|
return;
|
|
13622
13652
|
}
|
|
13653
|
+
const allowed = profileToolWhitelist(profile);
|
|
13623
13654
|
const resident = REGISTERED_TOOLS.slice(0, REGISTERED_TOOLS.length - 3);
|
|
13624
13655
|
const tiers = [REGISTERED_TOOLS, resident];
|
|
13625
|
-
const existing = skiffRoleRestrictions.get(sessionId);
|
|
13626
13656
|
for (const names of tiers) {
|
|
13627
13657
|
const deny = names.filter((n) => !allowed.has(n));
|
|
13628
13658
|
const signature = [...deny].sort().join(",");
|
|
@@ -13857,6 +13887,7 @@ function registerGuards(ctx, opts = {}) {
|
|
|
13857
13887
|
const pathArg = extractPathArg(exec);
|
|
13858
13888
|
const action = extractAction(exec);
|
|
13859
13889
|
const sessionId = exec?.agent?.session?.id;
|
|
13890
|
+
const skiffProfile = skiffProfileForSession(root, sessionId);
|
|
13860
13891
|
return decideGuard({
|
|
13861
13892
|
root,
|
|
13862
13893
|
toolName: exec.name,
|
|
@@ -13864,7 +13895,7 @@ function registerGuards(ctx, opts = {}) {
|
|
|
13864
13895
|
blacklist,
|
|
13865
13896
|
pathArg,
|
|
13866
13897
|
action,
|
|
13867
|
-
|
|
13898
|
+
skiffProfile
|
|
13868
13899
|
});
|
|
13869
13900
|
};
|
|
13870
13901
|
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
@@ -14099,7 +14130,9 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
|
|
|
14099
14130
|
promoted: true
|
|
14100
14131
|
};
|
|
14101
14132
|
const loopSid = session.id;
|
|
14102
|
-
|
|
14133
|
+
const loopCwd = session.header?.cwd ?? process.cwd();
|
|
14134
|
+
const loopRoot = cccRootForCwd(loopCwd);
|
|
14135
|
+
if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || loopRoot !== null && isSkiffSession(loopRoot, loopSid))) return {
|
|
14103
14136
|
boundary: -1,
|
|
14104
14137
|
promoted: true
|
|
14105
14138
|
};
|
|
@@ -14212,12 +14245,13 @@ function registerBootstrap(ctx) {
|
|
|
14212
14245
|
});
|
|
14213
14246
|
ctx.on("agent/inbox/inserted", ({ agent, message }) => {
|
|
14214
14247
|
try {
|
|
14215
|
-
|
|
14248
|
+
const root = agentRoot(agent);
|
|
14249
|
+
if (!root) return;
|
|
14216
14250
|
const session = agent.session;
|
|
14217
14251
|
const depth = session?.header?.delegationDepth ?? 0;
|
|
14218
14252
|
const sid = typeof session?.id === "string" ? session.id : void 0;
|
|
14219
14253
|
if (sid === void 0) return;
|
|
14220
|
-
if (sid.startsWith("cheap-worker-") ||
|
|
14254
|
+
if (sid.startsWith("cheap-worker-") || isSkiffSession(root, sid)) return;
|
|
14221
14255
|
if (depth === 0) {
|
|
14222
14256
|
if (hasUserMessageHistory(session)) return;
|
|
14223
14257
|
} else if (anchoredSessions.has(sid)) return;
|
|
@@ -14347,9 +14381,9 @@ function registerBootstrap(ctx) {
|
|
|
14347
14381
|
}, { prepend: true });
|
|
14348
14382
|
}
|
|
14349
14383
|
//#endregion
|
|
14350
|
-
//#region src/
|
|
14384
|
+
//#region src/trajectory/assistant.ts
|
|
14351
14385
|
/**
|
|
14352
|
-
*
|
|
14386
|
+
* trajectory/assistant.ts — 关卡化注入的统一命名与风格门面(v0.3,S142)
|
|
14353
14387
|
*
|
|
14354
14388
|
* 概念(用户拍板 2026-09-05):dsp 全部"过程中动态提示注入"统一命名为
|
|
14355
14389
|
* cognitive-trajectory-assistant(轨迹助航员)。关卡设计思想塑造**结构与时机**
|
|
@@ -14409,7 +14443,7 @@ const ACK_PREFIX = `${ASSISTANT_PREFIX}-recorded`;
|
|
|
14409
14443
|
* ⇒ 分家:**只用一次的交界材料**写进这份**临时便签**(无格式义务、无留档义务、可随时丢弃)。
|
|
14410
14444
|
*
|
|
14411
14445
|
* ## 🔴 三处引用同一常量(单一真相源)
|
|
14412
|
-
* **写侧** = `seams/keeper.ts` 的 `rebuildReminderText()`|**读侧** = `rebuild.ts` 的重建锚点|
|
|
14446
|
+
* **写侧** = `seams/keeper.ts` 的 `rebuildReminderText()`|**读侧** = `trajectory/rebuild.ts` 的重建锚点|
|
|
14413
14447
|
* **体积提醒** = `seams/keeper.ts` 的 `rebuildTodoReminderText()`。
|
|
14414
14448
|
* 文件名一旦漂移,写侧写的东西读侧就找不到 —— **且没有任何机械信号会报警**。
|
|
14415
14449
|
*
|
|
@@ -14488,7 +14522,7 @@ function reminderText(code) {
|
|
|
14488
14522
|
* 使用者若要查看当前占用/阈值:看面板(`rebuildThresholdK` 滑块 + 状态显示)——那是给人看的。
|
|
14489
14523
|
*
|
|
14490
14524
|
* v1.31.1 交接协议(S142 用户需求"要求 LLM 将当前手头事项写在 SESSION.md 尾部,并要求
|
|
14491
|
-
* rebuild 后去读并处理"):**写侧** = 这里;**读侧** = rebuild 锚点(rebuild.ts)。
|
|
14525
|
+
* rebuild 后去读并处理"):**写侧** = 这里;**读侧** = rebuild 锚点(trajectory/rebuild.ts)。
|
|
14492
14526
|
* 两侧共用 cognitive-trajectory-assistant 的常量(单一真相源)。
|
|
14493
14527
|
*
|
|
14494
14528
|
* 🔴 **v1.49.0(owner 2026-09-26 裁决:"砍掉 in-flight,这正是目的")**:写侧**不再**指向
|
|
@@ -14820,22 +14854,23 @@ function agentScope(agent) {
|
|
|
14820
14854
|
* - cheap-worker 杂工:sessionId 固定 `cheap-worker-` 前缀(tools/cheap-worker.ts 生成)
|
|
14821
14855
|
* 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
|
|
14822
14856
|
*/
|
|
14823
|
-
function shouldAutoRestore(agent) {
|
|
14857
|
+
function shouldAutoRestore(agent, root) {
|
|
14824
14858
|
const session = agent.session;
|
|
14825
14859
|
if (!session) return false;
|
|
14826
14860
|
if (session.header?.origin === "subagent") return false;
|
|
14827
14861
|
if (session.header?.parentSession) return false;
|
|
14828
14862
|
if (session.id?.startsWith("cheap-worker-")) return false;
|
|
14829
|
-
|
|
14863
|
+
const cccRoot = root !== void 0 ? root : cccRootForCwd(session.header?.cwd ?? process.cwd());
|
|
14864
|
+
if (cccRoot && isSkiffSession(cccRoot, session.id)) return false;
|
|
14830
14865
|
return true;
|
|
14831
14866
|
}
|
|
14832
14867
|
/**
|
|
14833
14868
|
* 恢复触发判定(S134 泄漏修复 v1.16.13):根会话 **且已有对话历史**(续跑/恢复的会话)
|
|
14834
|
-
*
|
|
14869
|
+
* 才自动恢复上次激活的宁静号会话——全新会话(新任务)无历史 → 不恢复,
|
|
14835
14870
|
* 避免把过去的 SESSION 上下文注入新任务(跨任务污染)。
|
|
14836
14871
|
*/
|
|
14837
|
-
function shouldRestoreActive(agent) {
|
|
14838
|
-
if (!shouldAutoRestore(agent)) return false;
|
|
14872
|
+
function shouldRestoreActive(agent, root) {
|
|
14873
|
+
if (!shouldAutoRestore(agent, root)) return false;
|
|
14839
14874
|
return sessionEvents$1(agent.session).length > 0;
|
|
14840
14875
|
}
|
|
14841
14876
|
function registerContext(ctx, opts = {}) {
|
|
@@ -14849,7 +14884,7 @@ function registerContext(ctx, opts = {}) {
|
|
|
14849
14884
|
} catch {}
|
|
14850
14885
|
if (!root) return;
|
|
14851
14886
|
const key = agentKey(agent);
|
|
14852
|
-
if (
|
|
14887
|
+
if (isSkiffSession(root, key)) return;
|
|
14853
14888
|
registerEntrySkillSection(agent, root);
|
|
14854
14889
|
registerCognitiveTrajectorySkillSection(agent, root);
|
|
14855
14890
|
const scope = agentScope(agent);
|
|
@@ -14935,7 +14970,7 @@ function registerContext(ctx, opts = {}) {
|
|
|
14935
14970
|
if (root) try {
|
|
14936
14971
|
syncSkiffRoleToolVisibility(agent, root);
|
|
14937
14972
|
} catch {}
|
|
14938
|
-
if (
|
|
14973
|
+
if (root && isSkiffSession(root, key)) return downstream;
|
|
14939
14974
|
try {
|
|
14940
14975
|
syncCccToolVisibility(agent, root);
|
|
14941
14976
|
} catch {}
|
|
@@ -14979,12 +15014,12 @@ function registerCompactRetention(ctx, opts = {}) {
|
|
|
14979
15014
|
ctx.on("session/event", (session, event) => {
|
|
14980
15015
|
if (event.type !== "compaction/end") return;
|
|
14981
15016
|
if (event.data.error) return;
|
|
14982
|
-
if (isSkiffSessionId(String(session.id ?? ""))) return;
|
|
14983
15017
|
const agent = ctx.agents.get(session.id);
|
|
14984
15018
|
if (!agent) return;
|
|
14985
15019
|
const cwd = agent.session?.header?.cwd ?? process.cwd();
|
|
14986
15020
|
const root = cccRootForCwd(cwd);
|
|
14987
15021
|
if (!root) return;
|
|
15022
|
+
if (isSkiffSession(root, String(session.id ?? ""))) return;
|
|
14988
15023
|
try {
|
|
14989
15024
|
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
14990
15025
|
} catch {}
|
|
@@ -15051,9 +15086,9 @@ function loadOpencodeSkill(skillDir) {
|
|
|
15051
15086
|
};
|
|
15052
15087
|
}
|
|
15053
15088
|
//#endregion
|
|
15054
|
-
//#region src/status.ts
|
|
15089
|
+
//#region src/surface/status.ts
|
|
15055
15090
|
/**
|
|
15056
|
-
* status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
|
|
15091
|
+
* surface/status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
|
|
15057
15092
|
*
|
|
15058
15093
|
* WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / cheap-worker 模型。
|
|
15059
15094
|
* setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
|
|
@@ -15188,7 +15223,7 @@ function setSafeMode(root, on) {
|
|
|
15188
15223
|
return { on: isSafeModeOn(root) };
|
|
15189
15224
|
}
|
|
15190
15225
|
//#endregion
|
|
15191
|
-
//#region src/
|
|
15226
|
+
//#region src/surface/http.ts
|
|
15192
15227
|
const ROUTE_PATH = "/serenity/status";
|
|
15193
15228
|
const UPLOAD_PATH = "/serenity/image-upload";
|
|
15194
15229
|
const FILE_UPLOAD_PATH = "/serenity/file-upload";
|
|
@@ -15329,7 +15364,7 @@ function requireWebUi(req, res, message) {
|
|
|
15329
15364
|
* 它就会**静默返回另一个 CCC 的路径**,于是面板把**别人的轨迹/技能/安全模式**当成你的显示出来。
|
|
15330
15365
|
* 这比"显示未激活"危险得多 —— 用户会据此做判断(以为自己的 safe-mode 开着、以为那是自己的轨迹)。
|
|
15331
15366
|
*
|
|
15332
|
-
* 修法与既有先例一致(`rebuild.ts` 对"跨轨迹猜测"的裁决:**响亮失败优于静默接错**):
|
|
15367
|
+
* 修法与既有先例一致(`trajectory/rebuild.ts` 对"跨轨迹猜测"的裁决:**响亮失败优于静默接错**):
|
|
15333
15368
|
* **多个候选 ⇒ 不猜**。只有当 live sessions 里的 CCC 根**唯一**时才采用它
|
|
15334
15369
|
* (那正是 v1.22.2 要救的场景:单 CCC 部署 + 远程访问缺 sessionId);
|
|
15335
15370
|
* 一旦出现**两个及以上**不同 CCC 根,就退回进程 cwd(→ 面板显示"未激活"),绝不挑一个顶替。
|
|
@@ -15539,11 +15574,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15539
15574
|
sendJson$3(res, 400, { error: chk.error });
|
|
15540
15575
|
return;
|
|
15541
15576
|
}
|
|
15542
|
-
const { readWeixinSettings } = await import("./
|
|
15543
|
-
const { weixinBridgeStatus } = await Promise.resolve().then(() =>
|
|
15577
|
+
const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15578
|
+
const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
|
|
15544
15579
|
const settings = readWeixinSettings(chk.root);
|
|
15545
15580
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
15546
|
-
const { readWeixinCredential } = await import("./
|
|
15581
|
+
const { readWeixinCredential } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15547
15582
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
15548
15583
|
accountId: a.accountId,
|
|
15549
15584
|
name: a.name ?? void 0,
|
|
@@ -15572,10 +15607,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15572
15607
|
}
|
|
15573
15608
|
const root = chk.root;
|
|
15574
15609
|
if (body.action === "login-start") {
|
|
15575
|
-
const { fetchQRCode } = await import("./
|
|
15610
|
+
const { fetchQRCode } = await import("./ilink-DEpmE3yi.js").then((n) => n.a);
|
|
15576
15611
|
const { randomUUID } = await import("node:crypto");
|
|
15577
15612
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
15578
|
-
const { readWeixinSettings } = await import("./
|
|
15613
|
+
const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15579
15614
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
15580
15615
|
const loginKey = randomUUID();
|
|
15581
15616
|
weixinLogins.set(loginKey, {
|
|
@@ -15596,24 +15631,36 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15596
15631
|
sendJson$3(res, 400, { error: "missing accountId" });
|
|
15597
15632
|
return;
|
|
15598
15633
|
}
|
|
15599
|
-
const { removeWeixinAccount } = await import("./
|
|
15600
|
-
const { syncCccBridge } = await Promise.resolve().then(() =>
|
|
15634
|
+
const { removeWeixinAccount } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15635
|
+
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15601
15636
|
removeWeixinAccount(root, body.accountId);
|
|
15602
15637
|
syncCccBridge(ctx, root);
|
|
15603
15638
|
sendJson$3(res, 200, { removed: body.accountId });
|
|
15604
15639
|
return;
|
|
15605
15640
|
}
|
|
15606
15641
|
if (body.action === "save-routes") {
|
|
15607
|
-
const
|
|
15608
|
-
if (!Array.isArray(
|
|
15609
|
-
sendJson$3(res, 400, { error: "invalid routes (expected [{user,
|
|
15642
|
+
const raw = body.routes ?? [];
|
|
15643
|
+
if (!Array.isArray(raw)) {
|
|
15644
|
+
sendJson$3(res, 400, { error: "invalid routes (expected [{user, trajectory}, ...])" });
|
|
15610
15645
|
return;
|
|
15611
15646
|
}
|
|
15612
|
-
const
|
|
15613
|
-
const
|
|
15614
|
-
|
|
15615
|
-
|
|
15616
|
-
|
|
15647
|
+
const routes = [];
|
|
15648
|
+
for (const r of raw) {
|
|
15649
|
+
const user = typeof r?.user === "string" ? r.user : "";
|
|
15650
|
+
const code = typeof r?.trajectory === "string" && r.trajectory !== "" ? r.trajectory : typeof r?.role === "string" ? r.role : "";
|
|
15651
|
+
if (user === "" || code === "") {
|
|
15652
|
+
sendJson$3(res, 400, { error: "invalid routes (each row needs non-empty user and trajectory)" });
|
|
15653
|
+
return;
|
|
15654
|
+
}
|
|
15655
|
+
routes.push({
|
|
15656
|
+
user,
|
|
15657
|
+
trajectory: code
|
|
15658
|
+
});
|
|
15659
|
+
}
|
|
15660
|
+
const { saveWeixinRoutes } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15661
|
+
const { findDirNameByCode } = await import("./profile-DyMJ_bfr.js").then((n) => n.i);
|
|
15662
|
+
for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
|
|
15663
|
+
sendJson$3(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
|
|
15617
15664
|
return;
|
|
15618
15665
|
}
|
|
15619
15666
|
saveWeixinRoutes(root, routes);
|
|
@@ -15622,8 +15669,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15622
15669
|
}
|
|
15623
15670
|
if (body.action === "set-enabled") {
|
|
15624
15671
|
const enabled = body.enabled === true;
|
|
15625
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./
|
|
15626
|
-
const { syncCccBridge } = await Promise.resolve().then(() =>
|
|
15672
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15673
|
+
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15627
15674
|
const settings = readWeixinSettings(root);
|
|
15628
15675
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
15629
15676
|
sendJson$3(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
|
|
@@ -15632,7 +15679,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15632
15679
|
setWeixinEnabled(root, enabled);
|
|
15633
15680
|
if (enabled) syncCccBridge(ctx, root);
|
|
15634
15681
|
else {
|
|
15635
|
-
const { stopCccBridge } = await Promise.resolve().then(() =>
|
|
15682
|
+
const { stopCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15636
15683
|
stopCccBridge(root);
|
|
15637
15684
|
}
|
|
15638
15685
|
sendJson$3(res, 200, { enabled });
|
|
@@ -15665,8 +15712,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15665
15712
|
sendJson$3(res, 200, { status: "expired" });
|
|
15666
15713
|
return;
|
|
15667
15714
|
}
|
|
15668
|
-
const { pollQRStatus } = await import("./
|
|
15669
|
-
const { readWeixinSettings } = await import("./
|
|
15715
|
+
const { pollQRStatus } = await import("./ilink-DEpmE3yi.js").then((n) => n.a);
|
|
15716
|
+
const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15670
15717
|
const settings = readWeixinSettings(login.root);
|
|
15671
15718
|
const status = await pollQRStatus({
|
|
15672
15719
|
baseUrl: void 0,
|
|
@@ -15681,8 +15728,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15681
15728
|
});
|
|
15682
15729
|
return;
|
|
15683
15730
|
}
|
|
15684
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./
|
|
15685
|
-
const { syncCccBridge } = await Promise.resolve().then(() =>
|
|
15731
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15732
|
+
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15686
15733
|
const accountId = nextWeixinAccountId(settings);
|
|
15687
15734
|
upsertWeixinAccount(login.root, {
|
|
15688
15735
|
accountId,
|
|
@@ -15730,7 +15777,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15730
15777
|
const id = s.id ?? s.header?.id;
|
|
15731
15778
|
if (id) liveIds.add(id);
|
|
15732
15779
|
}
|
|
15733
|
-
const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await Promise.resolve().then(() =>
|
|
15780
|
+
const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await Promise.resolve().then(() => cleanup_exports);
|
|
15734
15781
|
const root = sessionsRootDir();
|
|
15735
15782
|
if (req.method === "GET") {
|
|
15736
15783
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
@@ -15786,9 +15833,9 @@ function requireCcc(root) {
|
|
|
15786
15833
|
};
|
|
15787
15834
|
}
|
|
15788
15835
|
//#endregion
|
|
15789
|
-
//#region src/voyage
|
|
15836
|
+
//#region src/surface/voyage/vocab.ts
|
|
15790
15837
|
/**
|
|
15791
|
-
* voyage
|
|
15838
|
+
* surface/voyage/vocab.ts — 航行页词表改为**按当前 CCC 运行时注入**(纯逻辑,零 DSH 依赖)
|
|
15792
15839
|
*
|
|
15793
15840
|
* ## 为什么需要它(owner 2026-10-18 报的缺陷)
|
|
15794
15841
|
* 资产 `assets/serenity-voyage.html` 的词表是**构建期扫描容器写死的常量**:
|
|
@@ -15906,7 +15953,7 @@ function injectVoyageVocab(html, live) {
|
|
|
15906
15953
|
return html.replace(VOCAB_LINE_RE, () => `var VOCAB = ${JSON.stringify(merged)};`);
|
|
15907
15954
|
}
|
|
15908
15955
|
//#endregion
|
|
15909
|
-
//#region src/voyage
|
|
15956
|
+
//#region src/surface/voyage/page.ts
|
|
15910
15957
|
const VOYAGE_PATH = "/serenity/voyage";
|
|
15911
15958
|
/**
|
|
15912
15959
|
* 资产相对包根的路径。
|
|
@@ -15915,9 +15962,15 @@ const VOYAGE_PATH = "/serenity/voyage";
|
|
|
15915
15962
|
* `dsh-develop pack-check` 的 `required` 已把它列为发布前硬断言。
|
|
15916
15963
|
*/
|
|
15917
15964
|
const VOYAGE_ASSET = "assets/serenity-voyage.html";
|
|
15918
|
-
/**
|
|
15965
|
+
/**
|
|
15966
|
+
* 包根 = 含本包 `package.json` 的目录。
|
|
15967
|
+
* 🔴 **不许写死层数**:本模块自身位置随目录重构变化(曾 `src/surface/voyage/page.ts`,现
|
|
15968
|
+
* `src/surface/voyage/page.ts`),而发布态是从打包入口 `lib/index.js` 反推 ⇒ 两种布局深度不同,
|
|
15969
|
+
* 写死 `'..'` 必在其中一个布局下静默指错(实测:搬进 `surface/voyage/` 后路由直接读不到资产 → 500)。
|
|
15970
|
+
* ⇒ 交给 {@link findPackageRoot} 按**内容**定位。
|
|
15971
|
+
*/
|
|
15919
15972
|
function packageRootPath() {
|
|
15920
|
-
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15973
|
+
return findPackageRoot() ?? join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15921
15974
|
}
|
|
15922
15975
|
/** 资产绝对路径(导出以便测试与 pack-check 共用同一判据来源) */
|
|
15923
15976
|
function voyageAssetPath() {
|
|
@@ -15933,7 +15986,7 @@ function loadVoyageHtml(assetPath = voyageAssetPath()) {
|
|
|
15933
15986
|
}
|
|
15934
15987
|
return readFileSync(assetPath, "utf-8");
|
|
15935
15988
|
}
|
|
15936
|
-
/** 发 JSON(错误分支用;与
|
|
15989
|
+
/** 发 JSON(错误分支用;与 surface/http.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
|
|
15937
15990
|
function sendJson$2(res, status, body) {
|
|
15938
15991
|
res.statusCode = status;
|
|
15939
15992
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
@@ -15942,7 +15995,7 @@ function sendJson$2(res, status, body) {
|
|
|
15942
15995
|
/**
|
|
15943
15996
|
* 注册 `/serenity/voyage`(GET)。
|
|
15944
15997
|
* @param opts.assetPath 覆盖资产路径(测试用;生产不传)
|
|
15945
|
-
* @param opts.resolveRoot 解析本次请求所属 CCC 根(生产由
|
|
15998
|
+
* @param opts.resolveRoot 解析本次请求所属 CCC 根(生产由 surface/http.ts 的 resolveWorkspace 提供)。
|
|
15946
15999
|
* 🔴 **2026-10-18 新增**:词表按请求所属 CCC **运行时注入**(owner 报「在别的 CCC 点开
|
|
15947
16000
|
* 显示的还是 home-serenity 的信息」)。不传 ⇒ 不注入(保持资产原样 = 旧行为)。
|
|
15948
16001
|
*/
|
|
@@ -16534,9 +16587,9 @@ function summarizeHostContract(report) {
|
|
|
16534
16587
|
return `${required.length > 0 ? `host contract BROKEN (${required.length} required)` : `host contract degraded (${optional.length} optional)`}: ${report.issues.map((i) => i.detail).join("; ")}`;
|
|
16535
16588
|
}
|
|
16536
16589
|
//#endregion
|
|
16537
|
-
//#region src/
|
|
16590
|
+
//#region src/surface/faces.ts
|
|
16538
16591
|
/**
|
|
16539
|
-
*
|
|
16592
|
+
* surface/faces.ts — **面宿主**:插件自起 HTTP listener 的唯一生命周期处(C4 块 A,S142 2026-09-15)
|
|
16540
16593
|
*
|
|
16541
16594
|
* ## 现状 → 本模块要收掉什么(取证:`…/acc-c4c5-current-state.md` §①/§③ 3.1)
|
|
16542
16595
|
*
|
|
@@ -16545,7 +16598,7 @@ function summarizeHostContract(report) {
|
|
|
16545
16598
|
* ⇒ 仍保有面 B/C/D 三个;本段保留"当年是四个"的记述,因为下方"两套拆卸"的病史由它解释。
|
|
16546
16599
|
* 此前四处各写一份同款样板:`createServer` + `server.once('error')` +
|
|
16547
16600
|
* `listen(port, host)` + 模块级 `active` 变量 + `close()`;拆卸也分裂成**两套**——
|
|
16548
|
-
* `seams/lifecycle.ts` 有一个聚合 disposer,而 `gateway.ts` 与 `
|
|
16601
|
+
* `seams/lifecycle.ts` 有一个聚合 disposer,而 `gateway/server.ts` 与 `im/send.ts`
|
|
16549
16602
|
* **各自另注册**一个(D/E 干脆没有自己的 disposer)。
|
|
16550
16603
|
*
|
|
16551
16604
|
* 本模块把"listener 生命周期"收成一处:**active 表 + 统一 listen/close + 统一错误与端口占用
|
|
@@ -16554,7 +16607,7 @@ function summarizeHostContract(report) {
|
|
|
16554
16607
|
* ## 🔴 共享的是「生命周期与样板」,**绝不是**「路由 / 监听器 / 鉴权」
|
|
16555
16608
|
*
|
|
16556
16609
|
* 每个面**保留自己的 listener、自己的路由、自己的鉴权**(`handler` 内部原样不动):
|
|
16557
|
-
* · **A ↔ C 解耦**(`
|
|
16610
|
+
* · **A ↔ C 解耦**(`im/send.ts:4-7`):3080 会被 3081 原样反代(连请求头),
|
|
16558
16611
|
* 把 3082 的端点挂到 3080 上 = 已登录的外部家庭账号即可冒充 bot 发消息
|
|
16559
16612
|
* ⇒ 本模块**不提供**"把面挂到别的面"的能力,也不共享路由表;
|
|
16560
16613
|
* · (原 **E ↔ D** 共用会话核心但各自独立 listener 一条,**随 E 退役删除**);
|
|
@@ -16977,16 +17030,16 @@ ${csrf ? `<input type="hidden" name="csrf" value="${csrf}">` : ""}
|
|
|
16977
17030
|
</div></body></html>`;
|
|
16978
17031
|
}
|
|
16979
17032
|
//#endregion
|
|
16980
|
-
//#region src/gateway
|
|
17033
|
+
//#region src/gateway/proxy.ts
|
|
16981
17034
|
/**
|
|
16982
|
-
* gateway
|
|
17035
|
+
* gateway/proxy.ts — F1 双端口网关:代理辅助纯逻辑(v1.22.1-1.22.3 稳定性修复)
|
|
16983
17036
|
*
|
|
16984
|
-
* 从 gateway.ts 拆分(S142 熵点治理,v1.22.8):
|
|
17037
|
+
* 从 gateway/server.ts 拆分(S142 熵点治理,v1.22.8):
|
|
16985
17038
|
* - crypto.randomUUID polyfill(非安全上下文修复,v1.22.0)
|
|
16986
17039
|
* - 反代请求头构造(信任栅栏修复:Host + Origin 同步改写 loopback,v1.22.1)
|
|
16987
17040
|
* - workspace 白名单过滤(响应 items 前缀过滤 + create 校验 + 403 RPC 响应,v1.22)
|
|
16988
17041
|
* - HTML polyfill 注入(幂等 marker)
|
|
16989
|
-
* 纯函数设计(可单测);gateway.ts 装配层 import 使用。
|
|
17042
|
+
* 纯函数设计(可单测);gateway/server.ts 装配层 import 使用。
|
|
16990
17043
|
*/
|
|
16991
17044
|
/**
|
|
16992
17045
|
* 经第二端口 http://LAN-IP:3081 访问 = 非安全上下文 → DSH client 的
|
|
@@ -17120,7 +17173,7 @@ function buildProxyHeaders(reqHeaders, mainPort, bodyOverride) {
|
|
|
17120
17173
|
return headers;
|
|
17121
17174
|
}
|
|
17122
17175
|
//#endregion
|
|
17123
|
-
//#region src/gateway
|
|
17176
|
+
//#region src/gateway/dsh-auth.ts
|
|
17124
17177
|
/** 从一组 set-cookie 头提取 `dsh-auth-*` cookie(browser-auth sessionCookie 首段) */
|
|
17125
17178
|
function pickDshCookie(setCookieHeader) {
|
|
17126
17179
|
const entries = Array.isArray(setCookieHeader) ? setCookieHeader : setCookieHeader === void 0 ? [] : [setCookieHeader];
|
|
@@ -17183,7 +17236,7 @@ function createDshCookieProvider(connection, authority) {
|
|
|
17183
17236
|
};
|
|
17184
17237
|
}
|
|
17185
17238
|
//#endregion
|
|
17186
|
-
//#region src/gateway.ts
|
|
17239
|
+
//#region src/gateway/server.ts
|
|
17187
17240
|
/** 面名(C4 块 A:listener 生命周期归 `face-host`) */
|
|
17188
17241
|
const FACE_GATEWAY = "gateway";
|
|
17189
17242
|
/** 读取请求体(≤ maxBytes;超限 reject) */
|
|
@@ -17626,7 +17679,7 @@ function registerGateway(ctx) {
|
|
|
17626
17679
|
});
|
|
17627
17680
|
}
|
|
17628
17681
|
//#endregion
|
|
17629
|
-
//#region src/rebuild.ts
|
|
17682
|
+
//#region src/trajectory/rebuild.ts
|
|
17630
17683
|
var rebuild_exports = /* @__PURE__ */ __exportAll({
|
|
17631
17684
|
buildRebuildAnchor: () => buildRebuildAnchor,
|
|
17632
17685
|
performRebuild: () => performRebuild,
|
|
@@ -17704,11 +17757,10 @@ function parseAnchorMdPath(session) {
|
|
|
17704
17757
|
return null;
|
|
17705
17758
|
}
|
|
17706
17759
|
}
|
|
17707
|
-
/** 从 SESSION.md
|
|
17760
|
+
/** 从 SESSION.md 绝对路径派生会话名(编码;issue 目录回退完整目录名) */
|
|
17708
17761
|
function sessionNameFromMdPath(mdPath) {
|
|
17709
17762
|
const dirName = basename(dirname(mdPath));
|
|
17710
|
-
|
|
17711
|
-
return idMatch ? `S${idMatch[1]}` : dirName;
|
|
17763
|
+
return parseTrajectoryCode(dirName) ?? dirName;
|
|
17712
17764
|
}
|
|
17713
17765
|
/**
|
|
17714
17766
|
* 稳固的 SESSION.md 定位(多层候选 + 存在性校验,**绝不输出虚假路径**):
|
|
@@ -17901,9 +17953,8 @@ function renameAfterRebuild(ctx, agent, pending) {
|
|
|
17901
17953
|
try {
|
|
17902
17954
|
const mdPath = pending.mdPath;
|
|
17903
17955
|
const dirName = basename(dirname(mdPath));
|
|
17904
|
-
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
17905
17956
|
const info = {
|
|
17906
|
-
sessionId:
|
|
17957
|
+
sessionId: parseTrajectoryCode(dirName) ?? dirName.replace(/^\d{4}-\d{2}-\d{2}--/, ""),
|
|
17907
17958
|
dirName,
|
|
17908
17959
|
mdPath
|
|
17909
17960
|
};
|
|
@@ -19636,7 +19687,7 @@ f.parseInline;
|
|
|
19636
19687
|
b.parse;
|
|
19637
19688
|
x.lex;
|
|
19638
19689
|
//#endregion
|
|
19639
|
-
//#region src/skiff
|
|
19690
|
+
//#region src/role/skiff/debug.ts
|
|
19640
19691
|
/** 面名(C4 块 A) */
|
|
19641
19692
|
const FACE_SKIFF_DEBUG = "skiff-debug";
|
|
19642
19693
|
/** 监听地址:**只绑 loopback**(本面无认证——仅靠 loopback 挡住外部) */
|
|
@@ -20031,12 +20082,12 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
|
20031
20082
|
return;
|
|
20032
20083
|
}
|
|
20033
20084
|
const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot;
|
|
20034
|
-
const
|
|
20085
|
+
const code = typeof body.role === "string" ? body.role : "";
|
|
20035
20086
|
const question = typeof body.question === "string" ? body.question : "";
|
|
20036
20087
|
const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
|
|
20037
|
-
const
|
|
20038
|
-
if (!
|
|
20039
|
-
sendJson$1(res, 400, { error: `unknown
|
|
20088
|
+
const profile = code !== "" ? readSkiffProfileByCode(ccc, code) : null;
|
|
20089
|
+
if (!profile) {
|
|
20090
|
+
sendJson$1(res, 400, { error: `unknown skiff trajectory: ${code} (no trajectory with code "${code}" has ${SKIFF_PROFILE_FILENAME} in ${ccc})` });
|
|
20040
20091
|
return;
|
|
20041
20092
|
}
|
|
20042
20093
|
if (!question.trim()) {
|
|
@@ -20052,22 +20103,21 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
|
20052
20103
|
sendJson$1(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
|
|
20053
20104
|
return;
|
|
20054
20105
|
}
|
|
20055
|
-
if (info.
|
|
20056
|
-
sendJson$1(res, 400, { error: `session "${sessionId}" belongs to
|
|
20106
|
+
if (info.trajectoryDirName !== profile.dirName || info.ccc !== ccc) {
|
|
20107
|
+
sendJson$1(res, 400, { error: `session "${sessionId}" belongs to trajectory "${info.trajectoryDirName}" in another CCC — start a new conversation` });
|
|
20057
20108
|
return;
|
|
20058
20109
|
}
|
|
20059
20110
|
agent = live;
|
|
20060
20111
|
continued = true;
|
|
20061
20112
|
}
|
|
20062
20113
|
const hc = readCheapWorkerConfig(ccc);
|
|
20063
|
-
if (!agent) agent = (await createSkiffAgent(ctx, ccc,
|
|
20064
|
-
const result = await askSkiff(ctx, agent, question
|
|
20114
|
+
if (!agent) agent = (await createSkiffAgent(ctx, ccc, profile, hc?.defaultModel)).agent;
|
|
20115
|
+
const result = await askSkiff(ctx, agent, question);
|
|
20065
20116
|
sendJson$1(res, 200, {
|
|
20066
20117
|
answer: result.answer,
|
|
20067
20118
|
answer_html: renderSkiffMarkdown(result.answer),
|
|
20068
20119
|
sessionId: result.sessionId,
|
|
20069
|
-
continued
|
|
20070
|
-
cognitiveTrajectory: result.cognitiveTrajectory
|
|
20120
|
+
continued
|
|
20071
20121
|
});
|
|
20072
20122
|
return;
|
|
20073
20123
|
}
|
|
@@ -20077,9 +20127,9 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
|
|
|
20077
20127
|
}
|
|
20078
20128
|
}
|
|
20079
20129
|
//#endregion
|
|
20080
|
-
//#region src/
|
|
20130
|
+
//#region src/guard/filter.ts
|
|
20081
20131
|
/**
|
|
20082
|
-
*
|
|
20132
|
+
* guard/filter.ts — 最终输出敏感词检测 + 打回重生成(v1.26.3,S142 用户需求)
|
|
20083
20133
|
*
|
|
20084
20134
|
* 认知容器对外提供认知结果时,**支撑宁静号自身的凭据与机制**不应透露给用户。
|
|
20085
20135
|
* 用户拍板方案(讨论稿定稿):不做工具结果 redaction(那是模型工作内存),
|
|
@@ -20216,14 +20266,22 @@ function buildRebuke(hits) {
|
|
|
20216
20266
|
/** 打回状态(按 agent 会话跟踪连续命中轮数) */
|
|
20217
20267
|
const rebukeStates = /* @__PURE__ */ new Map();
|
|
20218
20268
|
//#endregion
|
|
20219
|
-
//#region src/
|
|
20220
|
-
/**
|
|
20221
|
-
*
|
|
20222
|
-
*
|
|
20223
|
-
*
|
|
20224
|
-
|
|
20269
|
+
//#region src/guard/seam.ts
|
|
20270
|
+
/**
|
|
20271
|
+
* 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免)。
|
|
20272
|
+
*
|
|
20273
|
+
* 🔴 **2026-10-19 判据更换**:skiff 那一条从「sessionId 前缀 `skiff-`」改为
|
|
20274
|
+
* **「该会话绑定的轨迹有 `skiff.json`」**(owner 重做令)。前缀判据在新模型下是
|
|
20275
|
+
* **错的**——skiff 的身份是轨迹,不再由 id 前缀表达;继续用前缀会漏掉所有
|
|
20276
|
+
* "挂在 skiff 轨迹上、但 id 不带前缀"的会话(典型 = 微信桥按轨迹派生的会话 id)。
|
|
20277
|
+
*
|
|
20278
|
+
* ⚠️ `rebuild-` 前缀保留(那是另一件事:重建会话同属对外面,与 skiff 无关);
|
|
20279
|
+
* `acp-` 分支保留但**已是死判据**(无生产者;面 E 已于 2026-10-15 退役)——
|
|
20280
|
+
* 理由与"为何不擅自删"见文件头注释。
|
|
20281
|
+
*/
|
|
20282
|
+
function isExternalFaceSession(sessionId, root) {
|
|
20225
20283
|
if (!sessionId) return false;
|
|
20226
|
-
return
|
|
20284
|
+
return isSkiffSession(root, sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
|
|
20227
20285
|
}
|
|
20228
20286
|
/** 读会话最后一个 assistant/message 文本(skiff-core 同款)
|
|
20229
20287
|
* **v1.63.3 起唯一的导出理由消失**:原导出是给 `unattended-seam.ts` 复用的(D77),
|
|
@@ -20253,10 +20311,11 @@ function registerOutputGuardHook(ctx) {
|
|
|
20253
20311
|
ctx.on("agent/turn-stopping", (payload) => {
|
|
20254
20312
|
const agent = payload?.agent;
|
|
20255
20313
|
if (!agent) return;
|
|
20256
|
-
|
|
20314
|
+
const sessionId = String(agent.session.id ?? "");
|
|
20257
20315
|
const cwd = agent.session.header?.cwd ?? process.cwd();
|
|
20258
20316
|
const root = cccRootForCwd(cwd);
|
|
20259
20317
|
if (!root) return;
|
|
20318
|
+
if (!isExternalFaceSession(sessionId, root)) return;
|
|
20260
20319
|
const id = agent.id;
|
|
20261
20320
|
const text = lastAssistantText(agent);
|
|
20262
20321
|
if (!text) return;
|
|
@@ -20288,12 +20347,12 @@ function registerOutputGuardHook(ctx) {
|
|
|
20288
20347
|
});
|
|
20289
20348
|
}
|
|
20290
20349
|
//#endregion
|
|
20291
|
-
//#region src/
|
|
20350
|
+
//#region src/im/inbound.ts
|
|
20292
20351
|
/**
|
|
20293
|
-
*
|
|
20352
|
+
* im/inbound.ts — 人机信道的**机制层**(机器级;2026-09-26 owner 定案 ⇒ 方案 §2.6)
|
|
20294
20353
|
*
|
|
20295
20354
|
* ## 为什么单独一个模块(方案 §5-12)
|
|
20296
|
-
* 今天"机制"与"渠道实现"混在 `
|
|
20355
|
+
* 今天"机制"与"渠道实现"混在 `im/bridge.ts` 里。P0-2 要立的这条不变量是**机器级**的 ——
|
|
20297
20356
|
* 它**跨 CCC**、且与"微信"无关(飞书桥/邮件桥同样要守)⇒ 抽到这里;`weixin-bridge` 只做接线。
|
|
20298
20357
|
*
|
|
20299
20358
|
* ## 本模块唯一的不变量
|
|
@@ -20314,7 +20373,7 @@ function registerOutputGuardHook(ctx) {
|
|
|
20314
20373
|
* ② **订阅表查询 + 入站路由决策**(P0-3:`(账号, 用户) → (CCC, 角色)`)—— 见 §订阅表
|
|
20315
20374
|
* ③ **机器级留痕**(P0-3:未投递 / 目标不存在 / 表残缺)—— 见 §留痕。
|
|
20316
20375
|
* 🔴 为什么这些必须机器级:它们**没有目标 CCC** ⇒ 落进任何 CCC 都是"编一个"目标。
|
|
20317
|
-
* **不碰**:轮询循环、凭据解析、会话投递 —— 那些是**渠道实现**,留在 `
|
|
20376
|
+
* **不碰**:轮询循环、凭据解析、会话投递 —— 那些是**渠道实现**,留在 `im/bridge.ts`。
|
|
20318
20377
|
* ⇒ 本模块**零 DSH 依赖**(只有 `node:crypto` / `node:fs`),可独立单测。
|
|
20319
20378
|
*/
|
|
20320
20379
|
/**
|
|
@@ -20664,9 +20723,9 @@ function registerWeixinOutputGuard(ctx) {
|
|
|
20664
20723
|
guardActive = true;
|
|
20665
20724
|
}
|
|
20666
20725
|
//#endregion
|
|
20667
|
-
//#region src/
|
|
20726
|
+
//#region src/im/hook.ts
|
|
20668
20727
|
/**
|
|
20669
|
-
*
|
|
20728
|
+
* im/hook.ts — 微信桥消息记录 hook(F4c-3 扩展,v1.27.13 用户需求)
|
|
20670
20729
|
*
|
|
20671
20730
|
* 需求:微信桥发生的所有消息记录,支持 hook——允许 CCC 通过自行编写 hook
|
|
20672
20731
|
* 脚本进行**持久性保存**(存哪/存成什么/是否入库,全归 CCC 决定——ACC 不绑定存储)。
|
|
@@ -20823,8 +20882,107 @@ function invokeWeixinHook(root, hookRel, event) {
|
|
|
20823
20882
|
return activeRunner(root, hookRel, event);
|
|
20824
20883
|
}
|
|
20825
20884
|
//#endregion
|
|
20826
|
-
//#region src/
|
|
20827
|
-
|
|
20885
|
+
//#region src/im/accounts.ts
|
|
20886
|
+
const impls = /* @__PURE__ */ new Map();
|
|
20887
|
+
/** 注册账号实现(`index.ts` 装配时调用;重复 channel 覆盖——测试与热重载友好) */
|
|
20888
|
+
function registerImAccountImpl(impl) {
|
|
20889
|
+
impls.set(impl.channel, impl);
|
|
20890
|
+
}
|
|
20891
|
+
/** 取实现(未注册 ⇒ `null`,由调用方**响亮出声**,不要静默跳过) */
|
|
20892
|
+
function getImAccountImpl(channel) {
|
|
20893
|
+
return impls.get(channel) ?? null;
|
|
20894
|
+
}
|
|
20895
|
+
/** 已注册的实现名(诊断与错误文案用;顺序 = 注册顺序) */
|
|
20896
|
+
function imAccountChannels() {
|
|
20897
|
+
return [...impls.keys()];
|
|
20898
|
+
}
|
|
20899
|
+
/**
|
|
20900
|
+
* 账号行的渠道归一(**单一处**)。
|
|
20901
|
+
*
|
|
20902
|
+
* 缺省 `weixin`:老配置平移到 `im` 段时不必补这个字段——owner 的"统一配置"
|
|
20903
|
+
* 要能直接吃旧数据(与 `route.ts` 的 `normalizeImSection` 保持同一口径)。
|
|
20904
|
+
*/
|
|
20905
|
+
function normalizeAccountChannel(channel) {
|
|
20906
|
+
const v = (channel ?? "").trim();
|
|
20907
|
+
return v === "" ? "weixin" : v;
|
|
20908
|
+
}
|
|
20909
|
+
//#endregion
|
|
20910
|
+
//#region src/im/weixin-account.ts
|
|
20911
|
+
/** 轮询间隔(getupdates 失败后重试;成功 = 立即续,长轮询本身 hold 35s) */
|
|
20912
|
+
const POLL_RETRY_MS = 3e3;
|
|
20913
|
+
/**
|
|
20914
|
+
* 微信的凭据就绪判据:**有 token** 才算绑定(baseUrl 留空 = 走官方默认)。
|
|
20915
|
+
* 与既有装配处逐字同义(原 `if (token === '') { …跳过… }`)。
|
|
20916
|
+
*/
|
|
20917
|
+
function weixinCredentialReady(cred) {
|
|
20918
|
+
return typeof cred.token === "string" && cred.token.trim() !== "";
|
|
20919
|
+
}
|
|
20920
|
+
let deps = null;
|
|
20921
|
+
/** 装配时注入依赖(`bridge.ts` 在模块初始化末尾调用一次;见 `installWeixinAccountImpl`) */
|
|
20922
|
+
function setWeixinLoopDeps(d) {
|
|
20923
|
+
deps = d;
|
|
20924
|
+
}
|
|
20925
|
+
/** 单账号轮询循环(**函数体逐字照搬自 bridge.ts 的 `runAccountLoop`,仅改类型与依赖来源**) */
|
|
20926
|
+
async function runWeixinAccountLoop(input, loop) {
|
|
20927
|
+
if (!deps) {
|
|
20928
|
+
loop.lastError = "weixin 实现未注入依赖(装配顺序错误)";
|
|
20929
|
+
return;
|
|
20930
|
+
}
|
|
20931
|
+
const d = deps;
|
|
20932
|
+
const ctx = input.ctx;
|
|
20933
|
+
const cred = input.cred;
|
|
20934
|
+
let buf = "";
|
|
20935
|
+
while (!loop.stopped) {
|
|
20936
|
+
loop.lastPollAt = Date.now();
|
|
20937
|
+
try {
|
|
20938
|
+
const resp = await d.getUpdates({
|
|
20939
|
+
baseUrl: cred.baseUrl,
|
|
20940
|
+
token: cred.token,
|
|
20941
|
+
getUpdatesBuf: buf,
|
|
20942
|
+
timeoutMs: 35e3
|
|
20943
|
+
});
|
|
20944
|
+
buf = resp.get_updates_buf ?? buf;
|
|
20945
|
+
for (const msg of resp.msgs ?? []) {
|
|
20946
|
+
if (loop.stopped) break;
|
|
20947
|
+
if (msg.message_type !== 1) continue;
|
|
20948
|
+
if (input.machine) await d.handleMachineIncoming(ctx, input.accountId, cred, msg);
|
|
20949
|
+
else await d.handleIncoming(ctx, input.root, input.accountId, cred, msg);
|
|
20950
|
+
}
|
|
20951
|
+
} catch (err) {
|
|
20952
|
+
loop.lastError = err instanceof Error ? err.message : String(err);
|
|
20953
|
+
await new Promise((r) => setTimeout(r, POLL_RETRY_MS));
|
|
20954
|
+
}
|
|
20955
|
+
}
|
|
20956
|
+
}
|
|
20957
|
+
/** 微信账号实现(登记的 channel 名 = `weixin`;与配置里的取值一致) */
|
|
20958
|
+
const weixinAccountImpl = {
|
|
20959
|
+
channel: "weixin",
|
|
20960
|
+
isReady: weixinCredentialReady,
|
|
20961
|
+
notReadyReason: () => "**无 token**(未绑定)",
|
|
20962
|
+
startLoop(input) {
|
|
20963
|
+
const loop = {
|
|
20964
|
+
stopped: false,
|
|
20965
|
+
lastPollAt: 0
|
|
20966
|
+
};
|
|
20967
|
+
runWeixinAccountLoop(input, loop);
|
|
20968
|
+
return loop;
|
|
20969
|
+
}
|
|
20970
|
+
};
|
|
20971
|
+
/**
|
|
20972
|
+
* 注册微信实现**并注入依赖**(`bridge.ts` 调用;幂等——重复注册覆盖同名 channel)。
|
|
20973
|
+
*
|
|
20974
|
+
* ⚠️ 必须在首次 `syncHumanChannel` / `syncCccBridge` **之前**发生。
|
|
20975
|
+
* `bridge.ts` 在**模块求值时**调用它一次(不是等到 `registerWeixinBridge`)——
|
|
20976
|
+
* 因为测试与面板会**直接**调 `syncCccBridge`,那时装配入口还没跑过;
|
|
20977
|
+
* 若依赖装配入口注册,那些路径会全部走进"未实现"分支(本轮实测:12 例红,根因就是它)。
|
|
20978
|
+
*/
|
|
20979
|
+
function installWeixinAccountImpl(d) {
|
|
20980
|
+
setWeixinLoopDeps(d);
|
|
20981
|
+
registerImAccountImpl(weixinAccountImpl);
|
|
20982
|
+
}
|
|
20983
|
+
//#endregion
|
|
20984
|
+
//#region src/im/bridge.ts
|
|
20985
|
+
var bridge_exports = /* @__PURE__ */ __exportAll({
|
|
20828
20986
|
handleIncoming: () => handleIncoming,
|
|
20829
20987
|
handleMachineIncoming: () => handleMachineIncoming,
|
|
20830
20988
|
manualOutputFallbackNeeded: () => manualOutputFallbackNeeded,
|
|
@@ -20843,14 +21001,12 @@ const bridges = /* @__PURE__ */ new Map();
|
|
|
20843
21001
|
*
|
|
20844
21002
|
* 机器级 poller **不属于任何 CCC**(这正是"上移 ACC 层"的含义:谁轮询不再由 CCC 数量决定)。
|
|
20845
21003
|
* 用途有二:① `runAccountLoop` 的 `root` 形参在机器级模式下不被使用,传它以示"无根";
|
|
20846
|
-
* ② 归属表(`
|
|
21004
|
+
* ② 归属表(`im/inbound.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
|
|
20847
21005
|
* 与任何**真实** CCC 根都不会撞(真根一律是绝对路径)。
|
|
20848
21006
|
*/
|
|
20849
21007
|
const MACHINE_SCOPE = "<machine>";
|
|
20850
21008
|
/** 机器级 poller(账号 id → 循环)。与 `bridges` **互斥**:同一时刻只有一种模式在跑。 */
|
|
20851
21009
|
const machineLoops = /* @__PURE__ */ new Map();
|
|
20852
|
-
/** 轮询间隔(getupdates 失败后重试延迟;成功 = 立即续轮询) */
|
|
20853
|
-
const POLL_RETRY_MS = 3e3;
|
|
20854
21010
|
/** 媒体大小上限(M6:20MB,对齐 vlm-describe;超限降级告知不落盘) */
|
|
20855
21011
|
const MEDIA_MAX_BYTES = 20971520;
|
|
20856
21012
|
/** typing_ticket 缓存(每 (accountId, fromUserId);getconfig 一次后续复用——对齐参考实现 typingTicketCache) */
|
|
@@ -20901,31 +21057,16 @@ async function sendTypingStop(cred, accountId, fromUserId) {
|
|
|
20901
21057
|
console.log(`[serenity-hooks] weixin-bridge typing stop skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
20902
21058
|
}
|
|
20903
21059
|
}
|
|
20904
|
-
/**
|
|
20905
|
-
|
|
20906
|
-
|
|
20907
|
-
|
|
20908
|
-
|
|
20909
|
-
|
|
20910
|
-
|
|
20911
|
-
|
|
20912
|
-
|
|
20913
|
-
|
|
20914
|
-
timeoutMs: 35e3
|
|
20915
|
-
});
|
|
20916
|
-
buf = resp.get_updates_buf ?? buf;
|
|
20917
|
-
for (const msg of resp.msgs ?? []) {
|
|
20918
|
-
if (loop.stopped) break;
|
|
20919
|
-
if (msg.message_type !== 1) continue;
|
|
20920
|
-
if (machine) await handleMachineIncoming(ctx, accountId, cred, msg);
|
|
20921
|
-
else await handleIncoming(ctx, root, accountId, cred, msg);
|
|
20922
|
-
}
|
|
20923
|
-
} catch (err) {
|
|
20924
|
-
loop.lastError = err instanceof Error ? err.message : String(err);
|
|
20925
|
-
await new Promise((r) => setTimeout(r, POLL_RETRY_MS));
|
|
20926
|
-
}
|
|
20927
|
-
}
|
|
20928
|
-
}
|
|
21060
|
+
/**
|
|
21061
|
+
* 🔴 **2026-10-19(P2)`runAccountLoop` 已迁出本文件** —— 逐字搬到
|
|
21062
|
+
* `im/weixin-account.ts`,成为 `ImAccountImpl` 的一个实现(微信)。
|
|
21063
|
+
*
|
|
21064
|
+
* 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
|
|
21065
|
+
* owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
|
|
21066
|
+
* 归**账号自己的实现**,装配处只按 `channel` 查派发表。
|
|
21067
|
+
* ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
|
|
21068
|
+
* `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
|
|
21069
|
+
*/
|
|
20929
21070
|
/**
|
|
20930
21071
|
* 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按 S142 用户"这个词不能让 ACC 定义,
|
|
20931
21072
|
* 要让 CCC 定义"重构)。
|
|
@@ -20970,7 +21111,7 @@ function manualOutputFallbackNeeded(input) {
|
|
|
20970
21111
|
*/
|
|
20971
21112
|
async function resolveCccRootByAlias(ctx, alias) {
|
|
20972
21113
|
try {
|
|
20973
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21114
|
+
const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
|
|
20974
21115
|
const entries = await listCccs(ctx);
|
|
20975
21116
|
const byName = entries.find((e) => e.name === alias);
|
|
20976
21117
|
if (byName) return byName.root;
|
|
@@ -20989,7 +21130,7 @@ async function resolveCccRootByAlias(ctx, alias) {
|
|
|
20989
21130
|
* 三档(方案 §3.1,**刻意分档**):
|
|
20990
21131
|
* - **未命中** ⇒ 不投递 + **留痕**(`console.warn`,静默档:这是"表里没有你",不是错误)
|
|
20991
21132
|
* - **表残缺** ⇒ **响亮出声**(`console.error`;且**独立于是否命中**)
|
|
20992
|
-
* - **目标不存在**(CCC /
|
|
21133
|
+
* - **目标不存在**(CCC / **轨迹**)⇒ **响亮报错**(`console.error`;不静默丢弃)
|
|
20993
21134
|
*
|
|
20994
21135
|
* @param input.cccFallback 过渡期回退(方案 Q5);**机器级模式传 `null`** ⇒ 未命中即不投递
|
|
20995
21136
|
*/
|
|
@@ -21000,7 +21141,7 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
21000
21141
|
user: input.user,
|
|
21001
21142
|
fallback: input.cccFallback ? {
|
|
21002
21143
|
ccc: input.cccFallback.root,
|
|
21003
|
-
role: input.cccFallback.
|
|
21144
|
+
role: input.cccFallback.trajectory
|
|
21004
21145
|
} : null
|
|
21005
21146
|
});
|
|
21006
21147
|
for (const defect of route.defects) {
|
|
@@ -21040,10 +21181,10 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
21040
21181
|
});
|
|
21041
21182
|
return null;
|
|
21042
21183
|
}
|
|
21043
|
-
const
|
|
21044
|
-
if (
|
|
21045
|
-
const detail = `目标 CCC "${target.ccc}"
|
|
21046
|
-
console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})——
|
|
21184
|
+
const { findDirNameByCode } = await import("./profile-DyMJ_bfr.js").then((n) => n.i);
|
|
21185
|
+
if (findDirNameByCode(root, target.role) === null) {
|
|
21186
|
+
const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
|
|
21187
|
+
console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
|
|
21047
21188
|
appendHumanChannelAudit({
|
|
21048
21189
|
kind: "inbound-target-missing",
|
|
21049
21190
|
account: input.account,
|
|
@@ -21054,10 +21195,11 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
21054
21195
|
});
|
|
21055
21196
|
return null;
|
|
21056
21197
|
}
|
|
21198
|
+
const profile = readSkiffProfileByCode(root, target.role);
|
|
21057
21199
|
return {
|
|
21058
21200
|
root,
|
|
21059
|
-
|
|
21060
|
-
|
|
21201
|
+
trajectory: target.role,
|
|
21202
|
+
profile
|
|
21061
21203
|
};
|
|
21062
21204
|
}
|
|
21063
21205
|
/**
|
|
@@ -21093,18 +21235,18 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21093
21235
|
if (preset) resolved = preset.target;
|
|
21094
21236
|
else {
|
|
21095
21237
|
if (!settings.enabled) return;
|
|
21096
|
-
const
|
|
21238
|
+
const cccTarget = matchWeixinRoute(settings.routes ?? [], fromUserId);
|
|
21097
21239
|
resolved = await resolveInboundTarget(ctx, {
|
|
21098
21240
|
account: accountId,
|
|
21099
21241
|
user: fromUserId,
|
|
21100
|
-
cccFallback:
|
|
21242
|
+
cccFallback: cccTarget ? {
|
|
21101
21243
|
root,
|
|
21102
|
-
|
|
21244
|
+
trajectory: cccTarget
|
|
21103
21245
|
} : null
|
|
21104
21246
|
});
|
|
21105
21247
|
}
|
|
21106
21248
|
if (!resolved) return;
|
|
21107
|
-
const {
|
|
21249
|
+
const { trajectory, profile } = resolved;
|
|
21108
21250
|
root = resolved.root;
|
|
21109
21251
|
if (!text && mediaRefs.length === 0) {
|
|
21110
21252
|
if (hasVoiceItem(msg)) await sendTextMessage({
|
|
@@ -21117,19 +21259,25 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21117
21259
|
return;
|
|
21118
21260
|
}
|
|
21119
21261
|
try {
|
|
21120
|
-
const sessionId =
|
|
21262
|
+
const sessionId = weixinSessionIdForTrajectory(trajectory);
|
|
21121
21263
|
const existing = getSkiffAgent(sessionId);
|
|
21122
21264
|
const hc = readCheapWorkerConfig(root);
|
|
21123
|
-
if (existing) try {
|
|
21124
|
-
ensureSkiffSession(root, existing, roleName, role, true);
|
|
21125
|
-
} catch (err) {
|
|
21126
|
-
console.warn(`[serenity-hooks] weixin-bridge ensure 失败(不影响处理): ${String(err?.message ?? err)}`);
|
|
21127
|
-
}
|
|
21128
21265
|
const ref = existing ? {
|
|
21129
21266
|
agent: existing,
|
|
21130
21267
|
sessionId,
|
|
21131
21268
|
resumed: true
|
|
21132
|
-
} : await createSkiffAgent(ctx, root,
|
|
21269
|
+
} : await createSkiffAgent(ctx, root, profile ?? {
|
|
21270
|
+
dirName: trajectory,
|
|
21271
|
+
code: trajectory,
|
|
21272
|
+
path: "",
|
|
21273
|
+
tools: [],
|
|
21274
|
+
msms: [],
|
|
21275
|
+
cognitiveTrajectory: {
|
|
21276
|
+
session: true,
|
|
21277
|
+
keeper: true,
|
|
21278
|
+
rebuild: true
|
|
21279
|
+
}
|
|
21280
|
+
}, hc?.defaultModel, sessionId);
|
|
21133
21281
|
if (!ref.resumed) await sendTextMessage({
|
|
21134
21282
|
baseUrl: cred.baseUrl,
|
|
21135
21283
|
token: cred.token,
|
|
@@ -21186,9 +21334,9 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21186
21334
|
}
|
|
21187
21335
|
}
|
|
21188
21336
|
const parts = [];
|
|
21189
|
-
const
|
|
21190
|
-
if (
|
|
21191
|
-
if (!ensureWorkspacePromptSection(ref.agent, sessionId)) parts.push(workspaceCognitiveTrajectoryLine(
|
|
21337
|
+
const workspaceMd = skiffWorkspaceMdPath(root, sessionId) ?? getActiveSessionInfo(sessionId)?.mdPath ?? null;
|
|
21338
|
+
if (workspaceMd) {
|
|
21339
|
+
if (!ensureWorkspacePromptSection(ref.agent, sessionId, root)) parts.push(workspaceCognitiveTrajectoryLine(workspaceMd));
|
|
21192
21340
|
}
|
|
21193
21341
|
const manualOutput = settings.autoReplyWithLastMessage === false;
|
|
21194
21342
|
if (text) parts.push(text);
|
|
@@ -21199,7 +21347,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21199
21347
|
root,
|
|
21200
21348
|
accountId,
|
|
21201
21349
|
userId: fromUserId,
|
|
21202
|
-
role:
|
|
21350
|
+
role: trajectory
|
|
21203
21351
|
});
|
|
21204
21352
|
parts.push(weixinManualOutputMarker(root, accountId, fromUserId));
|
|
21205
21353
|
} else forgetManualOutputSession(sessionId);
|
|
@@ -21210,13 +21358,13 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21210
21358
|
accountId,
|
|
21211
21359
|
userId: fromUserId,
|
|
21212
21360
|
sessionId,
|
|
21213
|
-
role:
|
|
21361
|
+
role: trajectory,
|
|
21214
21362
|
text,
|
|
21215
21363
|
media: hookMedia
|
|
21216
21364
|
})).catch((err) => {
|
|
21217
21365
|
console.log(`[serenity-hooks] weixin hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
|
|
21218
21366
|
});
|
|
21219
|
-
const answer = (await askSkiff(ctx, ref.agent, question
|
|
21367
|
+
const answer = (await askSkiff(ctx, ref.agent, question)).answer ?? "";
|
|
21220
21368
|
if (manualOutput) {
|
|
21221
21369
|
if (!manualOutputFallbackNeeded({
|
|
21222
21370
|
fallbackOnNoSend: settings.fallbackOnNoSend,
|
|
@@ -21232,13 +21380,13 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21232
21380
|
text: reply,
|
|
21233
21381
|
contextToken: msg.context_token
|
|
21234
21382
|
});
|
|
21235
|
-
console.warn(`[serenity-hooks] ⚠ weixin 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId},
|
|
21383
|
+
console.warn(`[serenity-hooks] ⚠ weixin 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
|
|
21236
21384
|
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
21237
21385
|
cccRoot: root,
|
|
21238
21386
|
accountId,
|
|
21239
21387
|
userId: fromUserId,
|
|
21240
21388
|
sessionId,
|
|
21241
|
-
role:
|
|
21389
|
+
role: trajectory,
|
|
21242
21390
|
reply,
|
|
21243
21391
|
source: "reply-fallback"
|
|
21244
21392
|
})).catch((err) => {
|
|
@@ -21260,7 +21408,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21260
21408
|
accountId,
|
|
21261
21409
|
userId: fromUserId,
|
|
21262
21410
|
sessionId,
|
|
21263
|
-
role:
|
|
21411
|
+
role: trajectory,
|
|
21264
21412
|
reply
|
|
21265
21413
|
})).catch((err) => {
|
|
21266
21414
|
console.log(`[serenity-hooks] weixin hook outgoing error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -21305,7 +21453,7 @@ async function handleMachineIncoming(ctx, accountId, cred, msg) {
|
|
|
21305
21453
|
* 已存在(配置变化热重建)→ 先停旧循环再启动新的。
|
|
21306
21454
|
*
|
|
21307
21455
|
* 🔴 **P0-2(2026-09-27,方案 §2.6):一账号一 poller** —— 起循环**之前**先过机器级归属判定
|
|
21308
|
-
* (`
|
|
21456
|
+
* (`im/inbound.ts`):身份被**别的** CCC 占着的账号**不起循环**,只留一条响亮日志。
|
|
21309
21457
|
* 身份 = **凭据 token 的哈希**(不是 `accountId`:那是每 CCC 的本地标签,跨 CCC 无意义)。
|
|
21310
21458
|
* 先 `releasePollersOf(root)` 再重新 claim —— 热重建语义与"归属粘住不漂"的论证见该函数注释。
|
|
21311
21459
|
*/
|
|
@@ -21320,10 +21468,23 @@ function syncCccBridge(ctx, root) {
|
|
|
21320
21468
|
if (!settings.enabled) return;
|
|
21321
21469
|
const requests = [];
|
|
21322
21470
|
const creds = /* @__PURE__ */ new Map();
|
|
21471
|
+
/** 账号 id → 它的实现(P2:按 `channel` 查派发表) */
|
|
21472
|
+
const implByAccount = /* @__PURE__ */ new Map();
|
|
21323
21473
|
for (const account of settings.accounts ?? []) {
|
|
21324
21474
|
if (account.enabled === false) continue;
|
|
21475
|
+
const channel = normalizeAccountChannel(account.channel);
|
|
21476
|
+
const impl = getImAccountImpl(channel);
|
|
21477
|
+
if (!impl) {
|
|
21478
|
+
if (!warnedChannels.has(`${account.accountId}:${channel}`)) {
|
|
21479
|
+
warnedChannels.add(`${account.accountId}:${channel}`);
|
|
21480
|
+
console.warn(`[serenity-hooks] ⚠ weixin-bridge: 账号 ${account.accountId}(ccc=${root})的渠道 "${channel}" **本机未实现** ⇒ 不起循环(已实现:${imAccountChannels().join(", ") || "(无)"})`);
|
|
21481
|
+
}
|
|
21482
|
+
continue;
|
|
21483
|
+
}
|
|
21325
21484
|
const cred = readWeixinCredential(root, account.accountId);
|
|
21326
21485
|
if (!cred) continue;
|
|
21486
|
+
if (!impl.isReady(cred)) continue;
|
|
21487
|
+
implByAccount.set(account.accountId, impl);
|
|
21327
21488
|
creds.set(account.accountId, cred);
|
|
21328
21489
|
requests.push({
|
|
21329
21490
|
root,
|
|
@@ -21339,13 +21500,16 @@ function syncCccBridge(ctx, root) {
|
|
|
21339
21500
|
};
|
|
21340
21501
|
for (const claim of plan.claims) {
|
|
21341
21502
|
const cred = creds.get(claim.accountId);
|
|
21342
|
-
|
|
21343
|
-
|
|
21344
|
-
|
|
21345
|
-
|
|
21346
|
-
|
|
21503
|
+
const impl = implByAccount.get(claim.accountId);
|
|
21504
|
+
if (!cred || !impl) continue;
|
|
21505
|
+
const loop = impl.startLoop({
|
|
21506
|
+
ctx,
|
|
21507
|
+
root,
|
|
21508
|
+
accountId: claim.accountId,
|
|
21509
|
+
cred,
|
|
21510
|
+
machine: false
|
|
21511
|
+
});
|
|
21347
21512
|
bridge.loops.set(claim.accountId, loop);
|
|
21348
|
-
runAccountLoop(ctx, root, claim.accountId, cred, loop);
|
|
21349
21513
|
}
|
|
21350
21514
|
commitPollerClaims(plan);
|
|
21351
21515
|
if (bridge.loops.size > 0) {
|
|
@@ -21396,6 +21560,8 @@ function syncMachinePollers(ctx, hc) {
|
|
|
21396
21560
|
stopCccBridges();
|
|
21397
21561
|
const requests = [];
|
|
21398
21562
|
const creds = /* @__PURE__ */ new Map();
|
|
21563
|
+
/** 账号 id → 它的实现(P2:**按 `channel` 查派发表**,不再写死 weixin) */
|
|
21564
|
+
const implByAccount = /* @__PURE__ */ new Map();
|
|
21399
21565
|
for (const account of hc.accounts) {
|
|
21400
21566
|
const id = account.id;
|
|
21401
21567
|
if (account.enabled === false) continue;
|
|
@@ -21403,23 +21569,27 @@ function syncMachinePollers(ctx, hc) {
|
|
|
21403
21569
|
console.warn("[serenity-hooks] ⚠ human-channel: 机器级账号表有一行**缺 `id`** ⇒ 跳过(该行无法被订阅表引用)");
|
|
21404
21570
|
continue;
|
|
21405
21571
|
}
|
|
21406
|
-
|
|
21407
|
-
|
|
21408
|
-
|
|
21409
|
-
|
|
21572
|
+
const channel = normalizeAccountChannel(account.channel);
|
|
21573
|
+
const impl = getImAccountImpl(channel);
|
|
21574
|
+
if (!impl) {
|
|
21575
|
+
if (!warnedChannels.has(`${id}:${channel}`)) {
|
|
21576
|
+
warnedChannels.add(`${id}:${channel}`);
|
|
21577
|
+
console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} 的渠道 "${account.channel ?? "(未填)"}" **本机未实现** ⇒ 不起 poller(已实现:${imAccountChannels().join(", ") || "(无)"})`);
|
|
21410
21578
|
}
|
|
21411
21579
|
continue;
|
|
21412
21580
|
}
|
|
21413
21581
|
const token = (account.token ?? "").trim();
|
|
21414
|
-
|
|
21415
|
-
console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} **无 token**(未绑定)⇒ 不起 poller`);
|
|
21416
|
-
continue;
|
|
21417
|
-
}
|
|
21418
|
-
creds.set(id, {
|
|
21582
|
+
const cred = {
|
|
21419
21583
|
token,
|
|
21420
21584
|
baseUrl: "",
|
|
21421
21585
|
userId: account.userId
|
|
21422
|
-
}
|
|
21586
|
+
};
|
|
21587
|
+
if (!impl.isReady(cred)) {
|
|
21588
|
+
console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} ${impl.notReadyReason?.(cred) ?? "**凭据不全**"} ⇒ 不起 poller`);
|
|
21589
|
+
continue;
|
|
21590
|
+
}
|
|
21591
|
+
implByAccount.set(id, impl);
|
|
21592
|
+
creds.set(id, cred);
|
|
21423
21593
|
requests.push({
|
|
21424
21594
|
root: MACHINE_SCOPE,
|
|
21425
21595
|
accountId: id,
|
|
@@ -21431,13 +21601,16 @@ function syncMachinePollers(ctx, hc) {
|
|
|
21431
21601
|
stopMachineLoops();
|
|
21432
21602
|
for (const claim of plan.claims) {
|
|
21433
21603
|
const cred = creds.get(claim.accountId);
|
|
21434
|
-
|
|
21435
|
-
|
|
21436
|
-
|
|
21437
|
-
|
|
21438
|
-
|
|
21604
|
+
const impl = implByAccount.get(claim.accountId);
|
|
21605
|
+
if (!cred || !impl) continue;
|
|
21606
|
+
const loop = impl.startLoop({
|
|
21607
|
+
ctx,
|
|
21608
|
+
root: MACHINE_SCOPE,
|
|
21609
|
+
accountId: claim.accountId,
|
|
21610
|
+
cred,
|
|
21611
|
+
machine: true
|
|
21612
|
+
});
|
|
21439
21613
|
machineLoops.set(claim.accountId, loop);
|
|
21440
|
-
runAccountLoop(ctx, MACHINE_SCOPE, claim.accountId, cred, loop, true);
|
|
21441
21614
|
}
|
|
21442
21615
|
commitPollerClaims(plan);
|
|
21443
21616
|
if (machineLoops.size > 0) console.log(`[serenity-hooks] ✓ human-channel: 机器级 poller accounts=${[...machineLoops.keys()].join(",")}(与 CCC 无关)`);
|
|
@@ -21469,7 +21642,7 @@ function syncHumanChannel(ctx) {
|
|
|
21469
21642
|
(async () => {
|
|
21470
21643
|
try {
|
|
21471
21644
|
stopMachineLoops();
|
|
21472
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21645
|
+
const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
|
|
21473
21646
|
for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
|
|
21474
21647
|
} catch {}
|
|
21475
21648
|
})();
|
|
@@ -21521,8 +21694,8 @@ async function sendProactiveText(input) {
|
|
|
21521
21694
|
const account = resolveWeixinAccount(root, input.accountId);
|
|
21522
21695
|
if (!account.ok) return account;
|
|
21523
21696
|
const { accountId, cred } = account;
|
|
21524
|
-
const
|
|
21525
|
-
const
|
|
21697
|
+
const trajectory = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
|
|
21698
|
+
const sessionId = weixinSessionIdForTrajectory(trajectory !== "" ? trajectory : sanitizeFileName$1(toUserId) || "unknown");
|
|
21526
21699
|
try {
|
|
21527
21700
|
await sendTextMessage({
|
|
21528
21701
|
baseUrl: cred.baseUrl,
|
|
@@ -21542,7 +21715,7 @@ async function sendProactiveText(input) {
|
|
|
21542
21715
|
accountId,
|
|
21543
21716
|
userId: toUserId,
|
|
21544
21717
|
sessionId,
|
|
21545
|
-
role,
|
|
21718
|
+
role: trajectory,
|
|
21546
21719
|
reply: markdownToPlainText(input.text),
|
|
21547
21720
|
source: "proactive"
|
|
21548
21721
|
})).catch((err) => {
|
|
@@ -21553,7 +21726,7 @@ async function sendProactiveText(input) {
|
|
|
21553
21726
|
accountId,
|
|
21554
21727
|
userId: toUserId,
|
|
21555
21728
|
sessionId,
|
|
21556
|
-
role
|
|
21729
|
+
role: trajectory
|
|
21557
21730
|
};
|
|
21558
21731
|
}
|
|
21559
21732
|
/**
|
|
@@ -21589,15 +21762,35 @@ function registerWeixinBridge(ctx) {
|
|
|
21589
21762
|
ctx.on("session/created", () => syncHumanChannel(ctx));
|
|
21590
21763
|
} catch {}
|
|
21591
21764
|
}
|
|
21765
|
+
/**
|
|
21766
|
+
* 🔴 **P2 装配:微信实现登记进"账号实现派发表"**(模块求值时执行,不是等 `apply`)。
|
|
21767
|
+
*
|
|
21768
|
+
* ## 为什么必须在模块求值时做(本轮实测教训)
|
|
21769
|
+
*
|
|
21770
|
+
* 首版把这句放在 `registerWeixinBridge` 里 ⇒ 全量测试 **12 例红**,报的全是
|
|
21771
|
+
* 「`weixinBridgeStatus()` 空数组」。根因:测试与设置面板会**直接**调 `syncCccBridge` /
|
|
21772
|
+
* `syncHumanChannel`,那时装配入口还没跑过 ⇒ 派发表是空的 ⇒ 每条账号都走进
|
|
21773
|
+
* 「渠道未实现」分支(那分支是给**真正**未实现的渠道用的)。
|
|
21774
|
+
*
|
|
21775
|
+
* ⇒ 判据:**"谁能被调"决定"注册要在哪儿发生"**,不能按"装配流程"想当然。
|
|
21776
|
+
*
|
|
21777
|
+
* 注入的是本模块自己的函数(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)——
|
|
21778
|
+
* 不需要 `weixin-account.ts import 回 bridge.ts`,避免两个文件互相 import 成环。
|
|
21779
|
+
*/
|
|
21780
|
+
installWeixinAccountImpl({
|
|
21781
|
+
getUpdates,
|
|
21782
|
+
handleIncoming,
|
|
21783
|
+
handleMachineIncoming
|
|
21784
|
+
});
|
|
21592
21785
|
//#endregion
|
|
21593
|
-
//#region src/
|
|
21786
|
+
//#region src/im/send.ts
|
|
21594
21787
|
/** 请求体上限(文本消息远小于此;防滥用) */
|
|
21595
21788
|
const MAX_BODY_BYTES = 65536;
|
|
21596
21789
|
/** 文本长度上限(微信侧单条消息实践上限;超限拒绝而非静默截断) */
|
|
21597
21790
|
const WEIXIN_SEND_MAX_TEXT = 4e3;
|
|
21598
21791
|
/**
|
|
21599
21792
|
* 面名(C4 块 A:listener 生命周期归 `face-host`,面向宿主提供规格)。
|
|
21600
|
-
* 本面=面 C(端口语义见 `ports.ts` 的 `FACE_PORTS.weixinSend`)。
|
|
21793
|
+
* 本面=面 C(端口语义见 `kernel/ports.ts` 的 `FACE_PORTS.weixinSend`)。
|
|
21601
21794
|
*/
|
|
21602
21795
|
const FACE_WEIXIN_SEND = "weixin-send-api";
|
|
21603
21796
|
/** 监听地址:**只绑 loopback**(本面不变量——不经网关、公网不可达,故不做共享密钥) */
|
|
@@ -21705,7 +21898,7 @@ function matchCcc(input, candidates) {
|
|
|
21705
21898
|
}
|
|
21706
21899
|
/** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
21707
21900
|
async function collectCandidates(ctx) {
|
|
21708
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21901
|
+
const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
|
|
21709
21902
|
const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
|
|
21710
21903
|
const seen = /* @__PURE__ */ new Set();
|
|
21711
21904
|
const out = [];
|
|
@@ -21980,9 +22173,9 @@ function registerLifecycle(ctx) {
|
|
|
21980
22173
|
registerDisposer(ctx, "self-started resources (gateway/weixin/skiff-debug faces + weixin bridges)", disposeAll);
|
|
21981
22174
|
}
|
|
21982
22175
|
//#endregion
|
|
21983
|
-
//#region src/web-fetch
|
|
22176
|
+
//#region src/patches/web-fetch.ts
|
|
21984
22177
|
/**
|
|
21985
|
-
* web-fetch
|
|
22178
|
+
* patches/web-fetch.ts — fake-ip / TUN 网络下的 web_fetch provider(v1.30.12,S142)
|
|
21986
22179
|
*
|
|
21987
22180
|
* 问题(用户报告 + 实证):`web_fetch` 在本机恒失败,报
|
|
21988
22181
|
* `URL hostname "X" resolves to a non-public IP address`(`WEB_BLOCKED_URL`)。
|
|
@@ -22171,7 +22364,7 @@ async function registerWebFetchProvider(ctx) {
|
|
|
22171
22364
|
}
|
|
22172
22365
|
}
|
|
22173
22366
|
//#endregion
|
|
22174
|
-
//#region src/opencode-provider.ts
|
|
22367
|
+
//#region src/patches/opencode-provider.ts
|
|
22175
22368
|
/** `llm-pi-ai` 的 settings 命名空间(宿主 `dsh-llm-pi-ai` 的 `NS`) */
|
|
22176
22369
|
const LLM_PI_AI_NAMESPACE = "llm-pi-ai";
|
|
22177
22370
|
/**
|
|
@@ -22880,9 +23073,9 @@ function registerDeepseekVisionPatch(ctx, enabled) {
|
|
|
22880
23073
|
registerDisposer(ctx, "deepseek vision patch retry timer", clearRetry);
|
|
22881
23074
|
}
|
|
22882
23075
|
//#endregion
|
|
22883
|
-
//#region src/im-
|
|
23076
|
+
//#region src/im/weixin-channel.ts
|
|
22884
23077
|
/**
|
|
22885
|
-
* im-
|
|
23078
|
+
* im/weixin-channel.ts — 微信通道实现(`im-bridge` 的通道之一,v1.31.0)
|
|
22886
23079
|
*
|
|
22887
23080
|
* 能力归 ACC(S142 用户洞察):本文件吸收原 CCC MSM `.opencode/skills/home-serenity/scripts/
|
|
22888
23081
|
* weixin-send.ts` 的 `send` / `send-file` / `users` 语义——协议、凭据、账号选择、别名解析、
|
|
@@ -22988,13 +23181,13 @@ const weixinChannel = {
|
|
|
22988
23181
|
text: input.caption
|
|
22989
23182
|
});
|
|
22990
23183
|
if (settings.hook) {
|
|
22991
|
-
const
|
|
23184
|
+
const trajectory = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
|
|
22992
23185
|
invokeWeixinHook(input.root, settings.hook, buildOutgoingHookEvent({
|
|
22993
23186
|
cccRoot: input.root,
|
|
22994
23187
|
accountId: account.accountId,
|
|
22995
23188
|
userId: input.userId,
|
|
22996
|
-
sessionId:
|
|
22997
|
-
role,
|
|
23189
|
+
sessionId: weixinSessionIdForTrajectory(trajectory !== "" ? trajectory : sanitizeFileName$1(input.userId) || "unknown"),
|
|
23190
|
+
role: trajectory,
|
|
22998
23191
|
reply: input.caption ? `[文件] ${sent.fileName} — ${input.caption}` : `[文件] ${sent.fileName}`,
|
|
22999
23192
|
source: "proactive",
|
|
23000
23193
|
file: {
|
|
@@ -23024,7 +23217,7 @@ const weixinChannel = {
|
|
|
23024
23217
|
})));
|
|
23025
23218
|
const routes = (settings.routes ?? []).map((route) => ({
|
|
23026
23219
|
user: route.user,
|
|
23027
|
-
role: route.
|
|
23220
|
+
role: route.trajectory
|
|
23028
23221
|
}));
|
|
23029
23222
|
return {
|
|
23030
23223
|
enabled: settings.enabled === true,
|
|
@@ -23038,13 +23231,13 @@ const weixinChannel = {
|
|
|
23038
23231
|
//#endregion
|
|
23039
23232
|
//#region src/tools/im-bridge.ts
|
|
23040
23233
|
/**
|
|
23041
|
-
* im
|
|
23234
|
+
* im/channels.ts(工具面)— IM 消息发送工具(v1.31.0)
|
|
23042
23235
|
*
|
|
23043
23236
|
* 用户洞察(S142):微信桥是 ACC 提供的 → **发送能力也应是 ACC 的工具**;
|
|
23044
23237
|
* 「当用户配置了微信桥则可用,不配置则不可见」。
|
|
23045
23238
|
*
|
|
23046
23239
|
* 本文件只做工具面三件事:① 从 agent 会话解析**本会话 CCC**(R4:不接受目标 CCC)
|
|
23047
|
-
* ② 把参数交给 `im
|
|
23240
|
+
* ② 把参数交给 `im/channels.ts`(能力层)分发 ③ 渲染结果/错误。
|
|
23048
23241
|
*
|
|
23049
23242
|
* 可见性不在这里判定:`seams/context.ts` 在会话就绪时按 CCC 配置
|
|
23050
23243
|
* `agent.ctx.tools.restrict({ deny: ['im-bridge'] })` 把本工具从**未配置通道**的
|
|
@@ -23119,9 +23312,9 @@ function createImBridgeTool() {
|
|
|
23119
23312
|
});
|
|
23120
23313
|
}
|
|
23121
23314
|
//#endregion
|
|
23122
|
-
//#region src/live
|
|
23315
|
+
//#region src/trajectory/live.ts
|
|
23123
23316
|
/**
|
|
23124
|
-
* live
|
|
23317
|
+
* trajectory/live.ts — 进程内 live 会话取数 + 精简诊断
|
|
23125
23318
|
*
|
|
23126
23319
|
* ── 为什么单独成模块(R↓,2026-09-15 S142「ACC 侧 autopilot 退场」S1 搬迁)──
|
|
23127
23320
|
* 这两个函数原先住在 `autopilot-trajectory.ts`,但它们的**主语不是 autopilot**:
|
|
@@ -23198,9 +23391,9 @@ function diagLive(ctx) {
|
|
|
23198
23391
|
};
|
|
23199
23392
|
}
|
|
23200
23393
|
//#endregion
|
|
23201
|
-
//#region src/
|
|
23394
|
+
//#region src/surface/observation.ts
|
|
23202
23395
|
/**
|
|
23203
|
-
*
|
|
23396
|
+
* surface/observation.ts — 容器状态模型的**取数层唯一出口**(C5「观察面归一」,S142 2026-09-15)
|
|
23204
23397
|
*
|
|
23205
23398
|
* 为什么存在(R↓):ACC 有多个观察面(`dashboard health` / `acc-diag` / WebUI 面板 /
|
|
23206
23399
|
* 系统提示词注入),它们读的是同一批宿主事实。此前"这些事实怎么取"写在**每个消费方各自的
|
|
@@ -23218,15 +23411,15 @@ function diagLive(ctx) {
|
|
|
23218
23411
|
* 依赖分层(为什么本模块"重"而 `kit-ops` / `status` 保持"轻"):
|
|
23219
23412
|
* {@link containerClocks} 必须读 `wake-scheduler` 的进程内快照,而该模块带 **@deepseek-ai 值依赖**
|
|
23220
23413
|
* (`dsh-llm` 的 `createUserMessage`)。故本模块**只能**被本就重型或异步的消费方使用:
|
|
23221
|
-
* `diag-ops`(静态 import)、`
|
|
23414
|
+
* `diag-ops`(静态 import)、`surface/http.ts` / `kit-ops`(`await import(...)`,与 `surface/http.ts` 既有的
|
|
23222
23415
|
* "保持静态链纯净"约定一致)。**轻链模块不得静态 import 本模块**。
|
|
23223
23416
|
*
|
|
23224
23417
|
* 已有的单一真相源继续指向原处(**不复制**):
|
|
23225
|
-
* L0 根解析 `ccc.ts findSerenityRoot`|L1/L2 根与枚举 `ccc-roots.ts`|宿主契约 `host/contract.ts`
|
|
23226
|
-
* |CCC 配置 `ccc.ts loadSerenityConfig`|注册表 `msm
|
|
23227
|
-
* |唤醒注册表 `wake
|
|
23418
|
+
* L0 根解析 `kernel/ccc.ts findSerenityRoot`|L1/L2 根与枚举 `kernel/ccc-roots.ts`|宿主契约 `host/contract.ts`
|
|
23419
|
+
* |CCC 配置 `kernel/ccc.ts loadSerenityConfig`|注册表 `tools/msm/service.ts`|时钟 `wake/scheduler.ts`
|
|
23420
|
+
* |唤醒注册表 `wake/registry.ts`。
|
|
23228
23421
|
*/
|
|
23229
|
-
var
|
|
23422
|
+
var observation_exports = /* @__PURE__ */ __exportAll({
|
|
23230
23423
|
checkRegistryHealth: () => checkRegistryHealth,
|
|
23231
23424
|
checkRegistryQuality: () => checkRegistryQuality,
|
|
23232
23425
|
containerClocks: () => containerClocks,
|
|
@@ -23247,7 +23440,7 @@ function containerIdentity(root) {
|
|
|
23247
23440
|
}
|
|
23248
23441
|
/**
|
|
23249
23442
|
* 三原则取数。
|
|
23250
|
-
* ⚠️ 与 `ccc.ts` 的 P1/P2 原语是**同一实现**(`existsSync(.serenity)` / `findGitRoot`),
|
|
23443
|
+
* ⚠️ 与 `kernel/ccc.ts` 的 P1/P2 原语是**同一实现**(`existsSync(.serenity)` / `findGitRoot`),
|
|
23251
23444
|
* 本函数只把它们组装成"一次检查"的形状;判定语义未变(P1 要求文件非空、P2 要求 `.git` 存在)。
|
|
23252
23445
|
*/
|
|
23253
23446
|
function containerPrinciples(root) {
|
|
@@ -23271,7 +23464,7 @@ function containerPrinciples(root) {
|
|
|
23271
23464
|
};
|
|
23272
23465
|
}
|
|
23273
23466
|
/**
|
|
23274
|
-
* **判据 A —— 注册表「结构完整性」**(原 `kit
|
|
23467
|
+
* **判据 A —— 注册表「结构完整性」**(原 `tools/kit/service.ts checkRegistryHealth`,C5 迁入本模块)。
|
|
23275
23468
|
*
|
|
23276
23469
|
* 回答的问题:**这张表坏没坏**——文件能不能被安全解析(JSON 合法 / 顶层形状 / entry 字段类型 /
|
|
23277
23470
|
* name 全局唯一 / path 在根内且脚本存在)。
|
|
@@ -23366,7 +23559,7 @@ function checkRegistryHealth(root) {
|
|
|
23366
23559
|
* 时钟的进程内状态("为什么这轮没被唤起"的第一手判据)。
|
|
23367
23560
|
*
|
|
23368
23561
|
* 🔴 **只读快照,不重算**(设计红线):`armed`/`ticks`/`lastTickAt`/`lastSkipReason` 只存在于
|
|
23369
|
-
* 调度器自己的模块级运行时对象里(`wake
|
|
23562
|
+
* 调度器自己的模块级运行时对象里(`wake/scheduler.ts schedulerRuntime`)。任何"从配置或文件
|
|
23370
23563
|
* 重新推导"得到的值都会与真实调度器的状态不一致——那比没有可观测量更糟(会给出假的"一切正常")。
|
|
23371
23564
|
*/
|
|
23372
23565
|
function containerClocks() {
|
|
@@ -23410,11 +23603,11 @@ function containerStatus(opts = {}) {
|
|
|
23410
23603
|
};
|
|
23411
23604
|
}
|
|
23412
23605
|
//#endregion
|
|
23413
|
-
//#region src/diag
|
|
23606
|
+
//#region src/tools/diag/service.ts
|
|
23414
23607
|
/**
|
|
23415
|
-
* diag
|
|
23608
|
+
* tools/diag/service.ts — `acc-diag` 报告装配(v1.33,S142 §32.11;2026-09-15 段数收缩)
|
|
23416
23609
|
*
|
|
23417
|
-
* 为什么单独成模块(R↓):工具面(`tools/
|
|
23610
|
+
* 为什么单独成模块(R↓):工具面(`tools/diag/index.ts`)只做"取 CCC 根 + 渲染",
|
|
23418
23611
|
* 装配逻辑放这里 ⇒ 纯函数可单测、不依赖 DSH 工具注册。
|
|
23419
23612
|
*
|
|
23420
23613
|
* 为什么**无动作参数**:诊断是低频动作(§32.9 收敛取向),一次调用即出全报告,
|
|
@@ -23426,7 +23619,7 @@ function containerStatus(opts = {}) {
|
|
|
23426
23619
|
* ①b 唤醒时钟 —— `containerClocks().wake`(唤醒调度器进程内快照:武装态/计数/上次跳过原因)
|
|
23427
23620
|
* ③ 唤醒注册表 —— `wake-registry.json` 的**在办条目**(state / at / target / lastResult)+ 补跑窗口
|
|
23428
23621
|
* (🔴 2026-09-19 §0Q 甲案:终态条目**结案即清除** ⇒ 本段只列在办项,不再是"历史全量";
|
|
23429
|
-
* 历史由 SESSION.md / 工具输出承担——该承诺原本就写在 `wake
|
|
23622
|
+
* 历史由 SESSION.md / 工具输出承担——该承诺原本就写在 `wake/registry.ts` 注释里,本次做实)
|
|
23430
23623
|
*
|
|
23431
23624
|
* ── 2026-09-15:段数由四降为三(S142「ACC 侧 autopilot 退场」§1 二阶裁定)─────────────
|
|
23432
23625
|
* ACC 侧 autopilot 整段退场后,报告里两段的**主语消失**,故删除(不是"暂时关掉"):
|
|
@@ -23442,7 +23635,7 @@ function containerStatus(opts = {}) {
|
|
|
23442
23635
|
* 条目到期 / 补跑窗口 / 目标可解析 / CCC 闸)。**不得**以"顺手保留 ④"的方式把它偷偷带回。
|
|
23443
23636
|
*
|
|
23444
23637
|
* C5「观察面归一」(2026-09-15):① 走 `diagLive`(那是**live 运行态**的独有取数,没有第二个
|
|
23445
|
-
* 来源);①b 时钟与 ③ 注册表经 `
|
|
23638
|
+
* 来源);①b 时钟与 ③ 注册表经 `surface/observation.ts` 取(唯一取数出口)——**投影仍在本文件**
|
|
23446
23639
|
* (诊断段不含 `message`,面板含;这是渲染差异,不是取数差异)。
|
|
23447
23640
|
*/
|
|
23448
23641
|
/** 人读渲染一行时钟状态("为何没有 tick" 的第一手判据) */
|
|
@@ -23513,9 +23706,9 @@ function renderAccDiag(r) {
|
|
|
23513
23706
|
return lines.join("\n");
|
|
23514
23707
|
}
|
|
23515
23708
|
//#endregion
|
|
23516
|
-
//#region src/tools/
|
|
23709
|
+
//#region src/tools/diag/index.ts
|
|
23517
23710
|
/**
|
|
23518
|
-
*
|
|
23711
|
+
* tools/diag/index.ts(工具面)— ACC 负责人专用的运行态诊断(v1.33,S142 §32.11)
|
|
23519
23712
|
*
|
|
23520
23713
|
* 用户裁决(2026-09-14):「不要 diag-live 了;或者变成一个隐藏 tool **只对 home-serenity 可见**,
|
|
23521
23714
|
* 做成我们专属的运行时诊断工具」。⇒ 合并原 `diag`(脚本侧条件链)+ `diag-live`(进程内运行态),
|
|
@@ -23535,7 +23728,7 @@ function renderAccDiag(r) {
|
|
|
23535
23728
|
* (① live 运行态 / ①b 唤醒时钟 / ③ 唤醒注册表)。
|
|
23536
23729
|
*
|
|
23537
23730
|
* ⚠️ 2026-09-15(ACC 侧 autopilot 退场,S142):原四段降为三段——原 ②「面板解析」与
|
|
23538
|
-
* ④「唤起条件链」的主语随机制消失,已删(见
|
|
23731
|
+
* ④「唤起条件链」的主语随机制消失,已删(见 `tools/diag/service.ts` 文件头;④ 的缺口已登记)。
|
|
23539
23732
|
*/
|
|
23540
23733
|
function createAccDiagTool(ctx) {
|
|
23541
23734
|
return defineTool({
|
|
@@ -24809,7 +25002,7 @@ async function resolveSkiffRoot(ctx) {
|
|
|
24809
25002
|
}
|
|
24810
25003
|
}
|
|
24811
25004
|
} catch {}
|
|
24812
|
-
for (const r of liveRoots) if (
|
|
25005
|
+
for (const r of liveRoots) if (listSkiffProfiles(r).length > 0) return r;
|
|
24813
25006
|
const fromCwd = cccRootForCwd(process.cwd());
|
|
24814
25007
|
if (fromCwd) return fromCwd;
|
|
24815
25008
|
if (liveRoots[0]) return liveRoots[0];
|