@shgroup/dsh-serenity-hooks 1.56.9 → 1.57.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 +2 -2
- package/assets/README.md +4 -4
- package/assets/serenity-voyage.html +3 -4
- package/dsh.plugin.json +3 -3
- package/lib/agent-idle.d.ts +2 -2
- package/lib/api.d.ts +0 -1
- package/lib/{ccc-DXky9UZu.js → ccc-B0ZGL9lx.js} +30 -13
- package/lib/{ccc-roots-Bo0HKLlM.js → ccc-roots-BC8mn8iM.js} +3 -3
- package/lib/ccc-roots.d.ts +1 -1
- package/lib/ccc.d.ts +34 -12
- package/lib/cheap-worker-ops.d.ts +49 -0
- package/lib/cheap-worker-preset-inherit.d.ts +35 -0
- package/lib/client.js +11 -99
- package/lib/cro-guide.d.ts +4 -2
- package/lib/git-ops.d.ts +1 -1
- package/lib/host/access.d.ts +2 -2
- package/lib/im-weixin.d.ts +10 -3
- package/lib/index.js +211 -592
- package/lib/invariant.d.ts +1 -1
- package/lib/invariant.js +1 -1
- package/lib/localstore-ops.d.ts +1 -1
- package/lib/{message-source-BExngUPX.js → message-source-BcCRsEOh.js} +3 -3
- package/lib/rebuild.d.ts +1 -1
- package/lib/seams/context.d.ts +2 -2
- package/lib/skiff-core.d.ts +1 -1
- package/lib/{skiff-role-BSg6Qu9k.js → skiff-role-B615L9MV.js} +3 -3
- package/lib/skiff-role.d.ts +2 -2
- package/lib/skills-discovery.d.ts +2 -3
- package/lib/status.d.ts +2 -2
- package/lib/tools/cheap-worker.d.ts +21 -0
- package/lib/tools/skiff-admin.d.ts +2 -2
- package/lib/unattended-ops.d.ts +4 -4
- package/lib/voyage-page.d.ts +3 -4
- package/lib/{weixin-route-BNyH5ghC.js → weixin-route-B9gU1YXz.js} +22 -6
- package/lib/weixin-send-api.d.ts +1 -1
- package/package.json +2 -2
- package/lib/handyman-ops.d.ts +0 -97
- package/lib/handyman-preset-inherit.d.ts +0 -35
- package/lib/tools/handyman.d.ts +0 -24
package/lib/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
2
|
import { a as hostSessions, c as hostWeb, i as hostService, l as hostWebServer, n as hostInjected, o as hostSettings, r as hostLlm, s as hostSubagents, t as hostAgents } from "./access-CNccNtgf.js";
|
|
3
|
-
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as
|
|
4
|
-
import { i as cccRootForExec, n as agentCwdFor, o as listCccs, r as cccRootForCwd, t as NO_CCC_FROM_AGENT_CWD } from "./ccc-roots-
|
|
5
|
-
import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-
|
|
6
|
-
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-
|
|
3
|
+
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readTrajectorySkills, i as findGitRoot, l as pathInside, m as readSafeModeHiddenTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readExclusiveTools, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-B0ZGL9lx.js";
|
|
4
|
+
import { i as cccRootForExec, n as agentCwdFor, o as listCccs, r as cccRootForCwd, t as NO_CCC_FROM_AGENT_CWD } from "./ccc-roots-BC8mn8iM.js";
|
|
5
|
+
import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-BcCRsEOh.js";
|
|
6
|
+
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-B9gU1YXz.js";
|
|
7
7
|
import { a as unregisterSkiffSession$1, i as skiffSessionSnapshot$1, n as skiffRoleFor$1, o as ACC_VERSION, r as skiffSessionInfo$1, t as registerSkiffSession$1 } from "./skiff-registry-BjIX_KD1.js";
|
|
8
|
-
import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-
|
|
8
|
+
import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-B615L9MV.js";
|
|
9
9
|
import { a as ACP_HTTP_PORT, c as MAIN_WEB_PORT, d as WEIXIN_SEND_PORT, l as MECHANISM_PORTS, n as readSimpleSettings, o as FACE_PORTS, r as registerSettingsSection, s as GATEWAY_PORT, u as SKIFF_DEBUG_PORT } from "./settings-section-xHABl8nk.js";
|
|
10
10
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
11
11
|
import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-BrRcPiQs.js";
|
|
@@ -623,7 +623,7 @@ function createKitTool(ctx) {
|
|
|
623
623
|
* - diff:git diff [--cached] [<ref>] [-- <path>]
|
|
624
624
|
* 保留 dsp 增强:localstore git 合规联动(S134:deny 且 .gitignore 未覆盖 → status 提示 / commit 拒绝)。
|
|
625
625
|
*
|
|
626
|
-
* 🔴 2026-09-26
|
|
626
|
+
* 🔴 2026-09-26 修(由另一个 CCC 实例报障):**push 曾把"超时被杀"报成成功**。
|
|
627
627
|
* 机制(**实测**,非推断):`spawnSync` 超时**不设 `err.killed`**(实测 `code=ETIMEDOUT` /
|
|
628
628
|
* `signal=SIGTERM` / `status=null` / `stderr` 长度 0),而 `execFileSync` **成功时丢弃 stderr**;
|
|
629
629
|
* 于是"超时被杀"与"真成功"都交出 `{stdout:'', stderr:''}` —— 旧判据 `if (!stderr) → 成功`
|
|
@@ -642,7 +642,7 @@ const GIT_ACTIONS = [
|
|
|
642
642
|
];
|
|
643
643
|
/** 本地动作超时(ms) */
|
|
644
644
|
const DEFAULT_LOCAL_TIMEOUT_MS = 3e4;
|
|
645
|
-
/** 网络动作(fetch/push/pull)超时(ms)——大体量推送可能远超 30s
|
|
645
|
+
/** 网络动作(fetch/push/pull)超时(ms)——大体量推送可能远超 30s(实测某实例 >60s) */
|
|
646
646
|
const DEFAULT_NET_TIMEOUT_MS = 18e4;
|
|
647
647
|
/**
|
|
648
648
|
* 超时可覆盖(`DSH_localTimeout()` / `DSH_netTimeout()`)。
|
|
@@ -1083,14 +1083,14 @@ Directory of ACC capabilities — each area lists where to go for details (guide
|
|
|
1083
1083
|
与 praxis ontology 的分工 = 那个是**方法文本**、这个是**能跑的那个**)
|
|
1084
1084
|
③ 工具与执行 → container_fs(容器文件系统 15 子命令)/ container_git(git)/ dashboard(health 含注册表检查/time/wait)
|
|
1085
1085
|
acc_msm 已并入 msm(执行+发现)与 container_admin(管理)——msm("<name>", ["<args>"]);inspect=true 查用法
|
|
1086
|
-
|
|
1086
|
+
cheap-worker(杂工 agent 编排——guide 子命令:cheap-worker guide)
|
|
1087
1087
|
专属工具(**默认对所有 CCC 隐藏**,只有在自己 exclusiveTools 里点名的 CCC 可见)→ acc-diag(运行态诊断)
|
|
1088
1088
|
④ 角色与对外面 → container_admin role(Skiff 认知子集角色:guide/validate/apply/list——skiff_admin 已并入)
|
|
1089
1089
|
ACP(程序化 JSON-RPC 3100)/ Skiff 问答页(公网 ask)——面板「外部能力」组
|
|
1090
1090
|
⑤ 自主与接入 → container_trajectory send-now(跨轨迹的**即时**递送,**不排队**:live 目标在跑就当场注入当前轮、空闲则立即起轮;非 live 冷载入——等效于直接 wake;**有同步回执**)
|
|
1091
1091
|
container_trajectory send-later(跨轨迹的"未来时刻 + 一条 message";fire-and-forget,无回执无回收)
|
|
1092
1092
|
weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:msm("weixin-doctor", ["guide"]))
|
|
1093
|
-
⑥ CCC 配置总览 → container_admin msm ccc-config(9 段:
|
|
1093
|
+
⑥ CCC 配置总览 → container_admin msm ccc-config(9 段:cheap-worker/sessionKeeper/localstore/hooks/safeMode/skiff/trajectory/weixin/**exclusiveTools**)
|
|
1094
1094
|
⑦ 注册表与安全 → mech-registry.json 由 container_admin msm register/deregister 管理(写保护——不可直接编辑)
|
|
1095
1095
|
dashboard health 输出 registry 完整性检查;损坏恢复指引见 health registry.issues
|
|
1096
1096
|
CCC 级开关:exclusiveTools(声明专属工具)/ safeMode.blacklist(写路径黑名单)
|
|
@@ -1158,24 +1158,27 @@ const CCC_CONFIG_REFERENCE = `═══ CCC Configuration Reference ═══
|
|
|
1158
1158
|
CCC-level features are configured in .opencode/serenity.json.
|
|
1159
1159
|
Below are all available configuration sections.
|
|
1160
1160
|
|
|
1161
|
-
── 1.
|
|
1162
|
-
|
|
1163
|
-
缺省模型 = models[0](可用
|
|
1164
|
-
**CCC 应把该白名单指向低成本模型**——
|
|
1161
|
+
── 1. cheap-worker.models ──
|
|
1162
|
+
cheap-worker(杂工)工具可用模型白名单(provider/model 列表);未配置时 cheap-worker 报错要求配置。
|
|
1163
|
+
缺省模型 = models[0](可用 cheap-worker.defaultModel 指定,必须 ∈ models)。
|
|
1164
|
+
**CCC 应把该白名单指向低成本模型**——cheap-worker 是批量执行通道。
|
|
1165
|
+
🔴 模型**不可由调用方指定**(工具无 model 参数)⇒ 本白名单是模型选择的唯一来源。
|
|
1165
1166
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1167
|
+
Behaviour(2026-10-14:background 模式已删除,只剩前台委派):
|
|
1168
|
+
cheap-worker(task, [label]):一次前台委派(宿主委派服务 ctx.subagents.start("spawn") +
|
|
1168
1169
|
agentOptions 注入模型)——不循环、不校验完成码、不写进度文件;返回子 agent 最终文本。
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1170
|
+
cheap-worker(jobs=[{task,label?},...]):并行扇出,上限 10(硬编码),结果保持传入顺序。
|
|
1171
|
+
⇒ **无 background 模式、无进度文件、无续跑**(owner 2026-10-14 令「删除后台运行能力」)。
|
|
1172
|
+
|
|
1173
|
+
Old key(兼容):
|
|
1174
|
+
读不到 "cheap-worker" 时**回落到旧键 "handyman"**(同形状:models / defaultModel)。
|
|
1175
|
+
旧键里的 "maxRounds"/"maxParallel" 已随 background 退场,留着无害但不再生效。
|
|
1173
1176
|
|
|
1174
1177
|
Config:
|
|
1175
|
-
{ "
|
|
1178
|
+
{ "cheap-worker": { "models": ["provider/model-name"] } }
|
|
1176
1179
|
|
|
1177
1180
|
Example:
|
|
1178
|
-
{ "
|
|
1181
|
+
{ "cheap-worker": { "models": ["minimax-cn-coding-plan/MiniMax-M3"], "defaultModel": "minimax-cn-coding-plan/MiniMax-M3" } }
|
|
1179
1182
|
|
|
1180
1183
|
── 2. sessionKeeper.threshold / sessionKeeper.sessionMdMaxKB ──
|
|
1181
1184
|
trajectory-assistant(TRAJECTORY-ASSISTANT · CHECKPOINT)提醒机制的积分阈值(非 headless 主 agent)。
|
|
@@ -1324,7 +1327,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1324
1327
|
"autoReplyWithLastMessage": true,
|
|
1325
1328
|
"fallbackOnNoSend": false,
|
|
1326
1329
|
"accounts": [{ "accountId": "wechat-1", "name": "家庭助手", "enabled": true }],
|
|
1327
|
-
"routes": [{ "user": "*", "role": "
|
|
1330
|
+
"routes": [{ "user": "*", "role": "my-role" }]
|
|
1328
1331
|
} }
|
|
1329
1332
|
|
|
1330
1333
|
▸ weixin.hook(消息记录 hook,v1.27.13):
|
|
@@ -1347,7 +1350,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1347
1350
|
事件 schema(**不含任何会话凭据**——context_token/bot_token/token/aes_key 均不出现):
|
|
1348
1351
|
{ "event": "incoming", "ts": 1788359941488, "cccRoot": "/path/ccc",
|
|
1349
1352
|
"accountId": "wechat-1", "userId": "u1@im.wechat",
|
|
1350
|
-
"sessionId": "skiff-weixin-xxx", "role": "
|
|
1353
|
+
"sessionId": "skiff-weixin-xxx", "role": "my-role",
|
|
1351
1354
|
"message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
|
|
1352
1355
|
{ "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
|
|
1353
1356
|
"userId": ..., "sessionId": ..., "role": ...,
|
|
@@ -1821,77 +1824,14 @@ async function runMsmAsync(root, args) {
|
|
|
1821
1824
|
}
|
|
1822
1825
|
}
|
|
1823
1826
|
//#endregion
|
|
1824
|
-
//#region src/
|
|
1827
|
+
//#region src/cheap-worker-ops.ts
|
|
1825
1828
|
/**
|
|
1826
|
-
*
|
|
1829
|
+
* cheap-worker-ops.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
|
|
1827
1830
|
*
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
+
* 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
|
|
1832
|
+
* 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
|
|
1833
|
+
* 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
|
|
1831
1834
|
*/
|
|
1832
|
-
/** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长(按码点截断,修复代理对切散 U+FFFD) */
|
|
1833
|
-
function sanitizeLabel(label) {
|
|
1834
|
-
return [...label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "")].slice(0, 50).join("");
|
|
1835
|
-
}
|
|
1836
|
-
function handymanProgressPaths(root, label) {
|
|
1837
|
-
const dir = join(root, "AGENT_SESSIONS");
|
|
1838
|
-
const safe = sanitizeLabel(label);
|
|
1839
|
-
return {
|
|
1840
|
-
md: join(dir, `handyman-${safe}.md`),
|
|
1841
|
-
json: join(dir, `handyman-${safe}.json`)
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
|
-
/** 读取进度(续跑);无文件返回 round 0 */
|
|
1845
|
-
function readProgress(root, label) {
|
|
1846
|
-
const { json } = handymanProgressPaths(root, label);
|
|
1847
|
-
if (!existsSync(json)) return null;
|
|
1848
|
-
try {
|
|
1849
|
-
return JSON.parse(readFileSync(json, "utf-8"));
|
|
1850
|
-
} catch {
|
|
1851
|
-
return null;
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
function writeProgress(root, label, p) {
|
|
1855
|
-
const { md, json } = handymanProgressPaths(root, label);
|
|
1856
|
-
mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
|
|
1857
|
-
writeFileSync(json, JSON.stringify({
|
|
1858
|
-
...p,
|
|
1859
|
-
status: p.status ?? "running",
|
|
1860
|
-
updated: isoLocal()
|
|
1861
|
-
}, null, 2) + "\n", "utf-8");
|
|
1862
|
-
const lines = [
|
|
1863
|
-
`# handyman: ${label}`,
|
|
1864
|
-
`- Model: ${p.model}`,
|
|
1865
|
-
`- Round: ${p.round}`,
|
|
1866
|
-
`- Done: ${p.done}`,
|
|
1867
|
-
"",
|
|
1868
|
-
`## Latest response`,
|
|
1869
|
-
"",
|
|
1870
|
-
p.lastResponse,
|
|
1871
|
-
""
|
|
1872
|
-
];
|
|
1873
|
-
writeFileSync(md, lines.join("\n"), "utf-8");
|
|
1874
|
-
}
|
|
1875
|
-
/** 失败状态落盘(对齐 osp writeFailedStatus:done=true / status=failed / errorCode) */
|
|
1876
|
-
function writeFailedStatus(root, label, info) {
|
|
1877
|
-
const { json } = handymanProgressPaths(root, label);
|
|
1878
|
-
mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
|
|
1879
|
-
const prev = readProgress(root, label);
|
|
1880
|
-
writeFileSync(json, JSON.stringify({
|
|
1881
|
-
round: prev?.round ?? 0,
|
|
1882
|
-
done: true,
|
|
1883
|
-
label,
|
|
1884
|
-
model: prev?.model ?? "",
|
|
1885
|
-
status: "failed",
|
|
1886
|
-
errorCode: info.errorCode,
|
|
1887
|
-
errorMessage: info.errorMessage,
|
|
1888
|
-
updated: isoLocal(),
|
|
1889
|
-
lastResponse: prev?.lastResponse ?? ""
|
|
1890
|
-
}, null, 2) + "\n", "utf-8");
|
|
1891
|
-
}
|
|
1892
|
-
function newStopToken() {
|
|
1893
|
-
return `SERENITY_HANDYMAN_DONE_${randomBytes(8).toString("hex")}`;
|
|
1894
|
-
}
|
|
1895
1835
|
/** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
|
|
1896
1836
|
function splitModel(model) {
|
|
1897
1837
|
const idx = model.indexOf("/");
|
|
@@ -1906,13 +1846,13 @@ function splitModel(model) {
|
|
|
1906
1846
|
}
|
|
1907
1847
|
/** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
|
|
1908
1848
|
function requireWhitelistedModel(model, models) {
|
|
1909
|
-
if (!models.includes(model)) throw new Error(`
|
|
1849
|
+
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(", ")}`);
|
|
1910
1850
|
}
|
|
1911
1851
|
/**
|
|
1912
1852
|
* providerAvailabilityError — 判定「**本机有没有该 provider 的 adapter**」,并产出**可执行**的错误文本。
|
|
1913
1853
|
* 返回 `null` = 全部可用(调用方照常继续)。
|
|
1914
1854
|
*
|
|
1915
|
-
* 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`
|
|
1855
|
+
* 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`cheap-worker` 的模型白名单写在
|
|
1916
1856
|
* **CCC**(`.opencode/serenity.json`,**随 git 走**),而 provider adapter 由**每台机器的宿主**
|
|
1917
1857
|
* 注册(profile 配置 / settings 命名空间)⇒ 同一份白名单在一台机器可用、在另一台必失败。
|
|
1918
1858
|
* 实测症状:宿主抛 `no adapter registered for provider "<p>"`(`dsh-llm`,code `NO_ADAPTER`),
|
|
@@ -1938,11 +1878,11 @@ function providerAvailabilityError(models, facts) {
|
|
|
1938
1878
|
const configPath = facts.configPath ?? "<ccc>/.opencode/serenity.json";
|
|
1939
1879
|
const missingList = missing.map((p) => `"${p}"`).join(", ");
|
|
1940
1880
|
return [
|
|
1941
|
-
`
|
|
1881
|
+
`cheap-worker: provider ${missingList} has no adapter registered on this machine.`,
|
|
1942
1882
|
"",
|
|
1943
1883
|
" Requested model(s) :",
|
|
1944
1884
|
...requested.map((e) => ` ${e.raw}`),
|
|
1945
|
-
` Configured in : ${configPath} → "
|
|
1885
|
+
` Configured in : ${configPath} → "cheap-worker.models" / "cheap-worker.defaultModel"`,
|
|
1946
1886
|
"",
|
|
1947
1887
|
" Why this happens : the whitelist lives in the CCC (it travels with the repository), while",
|
|
1948
1888
|
" provider adapters are registered per machine by the host. An entry that",
|
|
@@ -1956,135 +1896,75 @@ function providerAvailabilityError(models, facts) {
|
|
|
1956
1896
|
"",
|
|
1957
1897
|
" Fix (pick one, then retry):",
|
|
1958
1898
|
` 1. Point the whitelist at a provider that IS registered here — edit ${configPath}`,
|
|
1959
|
-
" (\"
|
|
1899
|
+
" (\"cheap-worker.models\", and \"cheap-worker.defaultModel\" if that is what you relied on).",
|
|
1960
1900
|
` 2. Register/activate provider ${missingList} on this machine (host profile config,`,
|
|
1961
1901
|
" or the settings namespace listed above).",
|
|
1962
1902
|
"",
|
|
1963
1903
|
" Do not retry unchanged — the same call fails identically."
|
|
1964
1904
|
].join("\n");
|
|
1965
1905
|
}
|
|
1966
|
-
/** 轮次 prompt(对齐老 loop 结构:回顾进度 → 自由工作 → 汇报;S134 EAP 化:固定详尽) */
|
|
1967
|
-
function buildRoundPrompt(opts) {
|
|
1968
|
-
const { root, session, label, round, stopToken, progress, task } = opts;
|
|
1969
|
-
const resumeNote = progress && progress.round > 0 ? `Previous round (round ${progress.round}) completed: ${progress.lastResponse.slice(0, 300)}\nAlways continue from where you left off; never redo completed work.` : "This is the first round.";
|
|
1970
|
-
return `# ${label} — handyman round ${round}
|
|
1971
|
-
|
|
1972
|
-
CCC root: ${root}
|
|
1973
|
-
${session ? `Work session: ${session} (progress recorded in AGENT_SESSIONS/${session}/SESSION.md)` : ""}
|
|
1974
|
-
${task ? `Task: ${task}` : `Task: follow the work corresponding to label "${label}" (if a work session exists, read SESSION.md first to clarify the goal)`}
|
|
1975
|
-
${resumeNote}
|
|
1976
|
-
|
|
1977
|
-
## Work rules (fixed every round, must follow)
|
|
1978
|
-
1. Work freely within this round: read files, modify code, execute commands — use every means to advance the task.
|
|
1979
|
-
2. If this task is **reading/curating or text-writing work** (extracting from files, summarizing, writing docs, generating text, etc.),
|
|
1980
|
-
first load eap (acc-eap skill) and organize output per the EAP standard:
|
|
1981
|
-
- E↑ Explicit: entities/variables clearly defined, relationships with direction and cardinality, boundaries drawn, no ambiguous words
|
|
1982
|
-
- R↓ Reconstructable: key conclusions record sources and reasoning, rebuildable by later agents
|
|
1983
|
-
- S↑ Stable: output structure regenerates repeatably, no reliance on implicit context
|
|
1984
|
-
3. Reports must be concrete and verifiable — no filler.
|
|
1985
|
-
|
|
1986
|
-
## Per-round report (fixed format, answer each item)
|
|
1987
|
-
1. What was done this round (concrete)
|
|
1988
|
-
2. Next-step plan
|
|
1989
|
-
3. Whether the task is complete (if complete, output only ${stopToken})
|
|
1990
|
-
|
|
1991
|
-
If the task is complete, output only ${stopToken}.`;
|
|
1992
|
-
}
|
|
1993
1906
|
/**
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
1996
|
-
*
|
|
1907
|
+
* cheap-worker 规模化使用指引(guide 子命令输出)。
|
|
1908
|
+
*
|
|
1909
|
+
* 🆕 2026-10-14:**双模式段已整段退场**(owner 令「删除后台运行能力」)。本指引只描述现行机制:
|
|
1910
|
+
* 前台委派(单任务)/前台并行扇出(jobs)/白名单与 provider 预检。
|
|
1997
1911
|
*/
|
|
1998
|
-
const
|
|
1912
|
+
const CHEAP_WORKER_GUIDE = `# cheap-worker — Scale-Up Usage Guide (guide)
|
|
1913
|
+
|
|
1914
|
+
## What it is
|
|
1915
|
+
One delegation primitive: **foreground spawn**. The host starts child agent(s) through its delegation
|
|
1916
|
+
service, awaits them, and returns their final text. There is no loop, no completion code, no progress
|
|
1917
|
+
file, and no unattended/background run.
|
|
1999
1918
|
|
|
2000
|
-
|
|
2001
|
-
| | foreground (default) | background |
|
|
1919
|
+
| | single | parallel (jobs) |
|
|
2002
1920
|
|---|---|---|
|
|
2003
|
-
|
|
|
2004
|
-
|
|
|
2005
|
-
|
|
|
2006
|
-
|
|
|
2007
|
-
| Parallel jobs | **yes** — jobs=[{task,label?,model?},...], **cap 10 (fixed)** | yes (jobs=[...], handyman.maxParallel default 10) |
|
|
2008
|
-
| Mechanism | host delegation service ctx.subagents.start("spawn") + agentOptions | ctx.agents.create + internal while-loop |
|
|
2009
|
-
| Model source | CCC handyman.models whitelist (same for both) | same |
|
|
1921
|
+
| Call | cheap-worker(task, [label]) | cheap-worker(jobs=[{task, label?}, ...]) |
|
|
1922
|
+
| Behaviour | one child runs the task once | all children start at once, results keep your order |
|
|
1923
|
+
| Cap | — | **10 (fixed, hardcoded)** |
|
|
1924
|
+
| Model | CCC config (single source) | same |
|
|
2010
1925
|
|
|
2011
1926
|
## ⚠️ Before using: load eap and design the plan
|
|
2012
|
-
Before calling
|
|
1927
|
+
Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up cheap-worker plan" based on the EAP framework.
|
|
2013
1928
|
|
|
2014
1929
|
### 1. Task decomposition (E↑ Explicit)
|
|
2015
1930
|
- Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
|
|
2016
|
-
- Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel
|
|
1931
|
+
- Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel via \`jobs\`
|
|
2017
1932
|
|
|
2018
|
-
### 2. Prompt design (
|
|
1933
|
+
### 2. Prompt design (cheap-worker's task parameter)
|
|
2019
1934
|
- task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
|
|
2020
|
-
-
|
|
1935
|
+
- the child does NOT share your conversation — restate everything it needs in task
|
|
2021
1936
|
- Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
|
|
2022
1937
|
output a JSON array (fields id/conclusion/evidence), do not modify the original file"
|
|
2023
1938
|
- Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
|
|
2024
|
-
the
|
|
1939
|
+
the cheap-worker-internal agent is also required to load eap and organize output per the EAP standard
|
|
2025
1940
|
|
|
2026
|
-
### 3. Model
|
|
2027
|
-
-
|
|
2028
|
-
|
|
2029
|
-
-
|
|
2030
|
-
-
|
|
1941
|
+
### 3. Model (CCC-configured, not selectable per call)
|
|
1942
|
+
- The model is read from .opencode/serenity.json "cheap-worker.models" — **the caller cannot pass one**
|
|
1943
|
+
(there is no "model" parameter). "cheap-worker.defaultModel" names which entry is used; it falls back to models[0].
|
|
1944
|
+
- The CCC is expected to point this whitelist at LOW-COST models: cheap-worker is the bulk-execution channel
|
|
1945
|
+
- A child's own recursive subagents inherit the child's model automatically (DSH native)
|
|
2031
1946
|
|
|
2032
1947
|
### 3b. 🔴 Provider availability (per-machine — the whitelist is NOT enough)
|
|
2033
1948
|
- The whitelist lives in the **CCC** ("serenity.json", it **travels with the repository**), but provider
|
|
2034
1949
|
**adapters are registered per machine by the host** (profile config / settings namespace). An entry that
|
|
2035
1950
|
works on one machine can fail on another with: "no adapter registered for provider <p>".
|
|
2036
|
-
- Before spawning,
|
|
1951
|
+
- Before spawning, cheap-worker verifies the provider of the resolved model against the adapters registered
|
|
2037
1952
|
**on this machine**; if it is missing it reports the requested model, where it is configured, **the
|
|
2038
1953
|
providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the
|
|
2039
|
-
exact fix —
|
|
2040
|
-
- ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "
|
|
2041
|
-
(and "
|
|
1954
|
+
exact fix — delivered in the result's "diagnostic" field (never an opaque failure, never a created child).
|
|
1955
|
+
- ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "cheap-worker.models"
|
|
1956
|
+
(and "cheap-worker.defaultModel") to a provider listed as available, or activate/register the provider on this
|
|
2042
1957
|
machine. **Do not retry the same call unchanged** — it fails identically.
|
|
2043
1958
|
|
|
2044
|
-
### 4. Parallel strategy
|
|
2045
|
-
- Independent subtasks
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
· **background** — handyman(mode="background", jobs=[...]): each job gets its own label + task + stop token + progress file
|
|
2049
|
-
- Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label
|
|
2050
|
-
(AGENT_SESSIONS/handyman-<label>.json) — same label resumes, different labels never interfere
|
|
2051
|
-
- Parallel cap: **foreground 10 (fixed)** / background handyman.maxParallel (default 10 — cheap models are cheap)
|
|
2052
|
-
- Aggregation: foreground returns all outputs in one result; background: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)
|
|
2053
|
-
- For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead
|
|
1959
|
+
### 4. Parallel strategy
|
|
1960
|
+
- Independent subtasks: cheap-worker(jobs=[{task,label?},...]) ⇒ fan-out, **cap 10**, results come back
|
|
1961
|
+
**in the order you passed them** (label is only a display name).
|
|
1962
|
+
- For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead.
|
|
2054
1963
|
|
|
2055
1964
|
## Completion criteria
|
|
2056
|
-
-
|
|
2057
|
-
|
|
2058
|
-
code (stop token); dialogue round cap (default 100, osp fail-safe, forced stop beyond the cap, resumable);
|
|
2059
|
-
automatic restart on abnormal agent stop (≤100 restarts, anti-infinite-loop)
|
|
2060
|
-
|
|
2061
|
-
## Waiting UI
|
|
2062
|
-
- The WebUI session-header Serenity detail card shows running background handymen's progress
|
|
2063
|
-
(label / round / last response), one line per parallel job, ~3s refresh (foreground mode writes no progress)
|
|
1965
|
+
- The child's run settles; done=true only when stopReason="completed" — otherwise read "diagnostic"
|
|
1966
|
+
(and "output" for whatever partial text was produced).
|
|
2064
1967
|
`;
|
|
2065
|
-
/** 列出 AGENT_SESSIONS/handyman-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
|
|
2066
|
-
function listActiveHandymen(root) {
|
|
2067
|
-
const dir = join(root, "AGENT_SESSIONS");
|
|
2068
|
-
if (!existsSync(dir)) return [];
|
|
2069
|
-
const out = [];
|
|
2070
|
-
for (const entry of readdirSync(dir)) {
|
|
2071
|
-
if (!entry.startsWith("handyman-") || !entry.endsWith(".json")) continue;
|
|
2072
|
-
try {
|
|
2073
|
-
const data = JSON.parse(readFileSync(join(dir, entry), "utf-8"));
|
|
2074
|
-
if (typeof data.label !== "string" || typeof data.round !== "number") continue;
|
|
2075
|
-
out.push({
|
|
2076
|
-
label: data.label,
|
|
2077
|
-
round: data.round,
|
|
2078
|
-
done: data.done === true,
|
|
2079
|
-
model: typeof data.model === "string" ? data.model : "",
|
|
2080
|
-
updated: typeof data.updated === "string" ? data.updated : "",
|
|
2081
|
-
lastResponse: typeof data.lastResponse === "string" ? data.lastResponse : ""
|
|
2082
|
-
});
|
|
2083
|
-
} catch {}
|
|
2084
|
-
}
|
|
2085
|
-
out.sort((a, b) => Date.parse(b.updated) - Date.parse(a.updated));
|
|
2086
|
-
return out;
|
|
2087
|
-
}
|
|
2088
1968
|
//#endregion
|
|
2089
1969
|
//#region src/agent-idle.ts
|
|
2090
1970
|
/**
|
|
@@ -2132,11 +2012,10 @@ function waitAgentIdle(ctx, agent) {
|
|
|
2132
2012
|
* skills-discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
|
|
2133
2013
|
*
|
|
2134
2014
|
* 发现全部入口技能(原文全量,不截断)—— **现行三个来源**(原为四个,**来源 3 已于 2026-09-21 删除**):
|
|
2135
|
-
* 0. **`.serenity` 记号文件内容 = 顶层入口 skill 名**(CCC
|
|
2136
|
-
* tiangong-serenity 的 .serenity 内容为 `tg-serenity`)—— 最高优先
|
|
2015
|
+
* 0. **`.serenity` 记号文件内容 = 顶层入口 skill 名**(CCC 记号文件的权威语义)—— 最高优先
|
|
2137
2016
|
* 1. `.dsh/entry-skill` 指针文件(内容 = skill 名)—— 兼容旧约定
|
|
2138
2017
|
* 2. `.opencode/skills/*-serenity/SKILL.md` —— 自动扫描该 CCC 的顶层入口
|
|
2139
|
-
*
|
|
2018
|
+
* (命名模式 `*-serenity`)
|
|
2140
2019
|
* ~~3. `.dsh/skills/*-serenity/SKILL.md`(扫 ACC 装进 CCC 的技能副本)~~ —— 🔴 **已删**:
|
|
2141
2020
|
* owner 2026-09-21 令「连模板和安装命令一起删」(B 案)。**判据、实测证据与日期逐字记在
|
|
2142
2021
|
* `findEntrySkills()` 体内那一段注释里**——本头部只留结论,**不复述判据**(避免两处会漂的真相源)。
|
|
@@ -3447,7 +3326,7 @@ function toolsBlock() {
|
|
|
3447
3326
|
" msm — execute a registered CCC MSM: msm(name, args); partial name returns candidates; inspect=true shows usage",
|
|
3448
3327
|
" praxis — actionable theory injection: praxis (index) / praxis eap / praxis neat / praxis cce / praxis ontology",
|
|
3449
3328
|
" ontology — ontology engineering ENGINE (executable): init (write template) / check (M1-M10 structural; level=strict adds L1-L5 hints) / explain (summary) / verify (V1-V4 runtime compliance) / review (semantic-review prompt) / compliance (axiom-vs-skill checklist). Sibling of `praxis ontology` — that one is the METHOD TEXT, this one RUNS it. path must stay inside the container; the receipt carries the engine exit code (0/1/2/4) and the engine text verbatim, so read it rather than assuming success",
|
|
3450
|
-
"
|
|
3329
|
+
" cheap-worker — delegate work to a worker agent on the CCC-configured model; one serial child per task, returns its final text. `jobs=[...]` fans out in parallel (cap 10, results keep your order). No loop, no completion code, no progress file. The model is NOT selectable per call (read from the CCC config); a worker's tool set excludes cheap-worker itself",
|
|
3451
3330
|
" serenity-skill-creator — creator for serenity-skills (this container's enhanced skill standard): guide (authoring/optimization method — read first) / create <name> (scaffold SKILL.md + skill.ttl + references/, base-chain import path computed) / check <name> (A1 structural + A2 base-chain; three states: modeled-ok / plain-skill / modeled-but-check-could-not-run) / review <name> (C tier: axiom-vs-text compliance question list, half-mechanical) / index <name> (preview the second-layer resource list the model will see) / status (which skills are modeled). A skill is \"modeled\" iff it has a file named exactly skill.ttl next to SKILL.md; a plain skill is NOT an anomaly. Only `create` writes, and it refuses an existing directory",
|
|
3452
3331
|
" localstore — ACC local credential/config storage (CCC-root localstore.json; git policy localstore.gitTrack default deny)",
|
|
3453
3332
|
" container_admin — container administration (the maintenance bay): role (Skiff roles: guide/validate/apply/list) / msm (register/deregister/check/guide/catalog/ccc-config) / config (view)",
|
|
@@ -4017,7 +3896,7 @@ const AUTO_BOUND_NOTE_PREFIX = "auto-created for skiff role";
|
|
|
4017
3896
|
* 注入 agent 系统提示词(用户对话面不可见)。
|
|
4018
3897
|
*
|
|
4019
3898
|
* v1.30.13(S142 诊断 D5):写入纪律**不点名具体工具**。旧文案写死 "with write/edit",
|
|
4020
|
-
*
|
|
3899
|
+
* 而某角色的工具白名单已移除 write/edit(写能力收归 CCC 的 session-write MSM)
|
|
4021
3900
|
* → 指令指向它根本没有的工具(注入纪律与能力面矛盾,模型只能猜)。
|
|
4022
3901
|
* 现文案改为「用本角色被授权的写入通道」——ACC 只声明义务(写进工作台),
|
|
4023
3902
|
* 具体通道由 CCC 的角色配置与角色提示词决定(ACC/CCC 归属二分;也不把某个 CCC 的
|
|
@@ -4153,7 +4032,7 @@ function ensureSkiffSession(root, agent, roleName, role, persistent = true) {
|
|
|
4153
4032
|
return mdPath;
|
|
4154
4033
|
}
|
|
4155
4034
|
/** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
|
|
4156
|
-
*
|
|
4035
|
+
* cheap-worker 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
|
|
4157
4036
|
* guard 角色白名单再按角色过滤可见/可用面——白名单外工具仍 deny) */
|
|
4158
4037
|
const SKIFF_PRESET = "standard";
|
|
4159
4038
|
/**
|
|
@@ -4316,8 +4195,8 @@ function sessionEvents(session) {
|
|
|
4316
4195
|
if (typeof s.snapshotEvents === "function") return s.snapshotEvents();
|
|
4317
4196
|
return s.events ?? [];
|
|
4318
4197
|
}
|
|
4319
|
-
/** 读会话最后一个 assistant/message 文本(
|
|
4320
|
-
function lastAssistantText$
|
|
4198
|
+
/** 读会话最后一个 assistant/message 文本(cheap-worker 同款) */
|
|
4199
|
+
function lastAssistantText$1(agent) {
|
|
4321
4200
|
const events = sessionEvents(agent.session);
|
|
4322
4201
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
4323
4202
|
const e = events[i];
|
|
@@ -4391,7 +4270,7 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
|
|
|
4391
4270
|
source: PLUGIN_SOURCE
|
|
4392
4271
|
}));
|
|
4393
4272
|
await waitAgentIdle(ctx, agent);
|
|
4394
|
-
const answer = lastAssistantText$
|
|
4273
|
+
const answer = lastAssistantText$1(agent);
|
|
4395
4274
|
const trajectory = options?.includeTrajectory === false ? [] : eventsToTrajectory(sessionEvents(agent.session).slice(before));
|
|
4396
4275
|
return {
|
|
4397
4276
|
answer,
|
|
@@ -7391,7 +7270,7 @@ function cmdCompliance(args) {
|
|
|
7391
7270
|
*
|
|
7392
7271
|
* 2026-09-14 S188 修(缺陷④):旧实现把 `skillDependsOn(A, B)` 的两端都收进 targets,
|
|
7393
7272
|
* 于是按生成指引逐 target 审会得到一半**方向错误的伪 Fail**(如"home-memory 未声明
|
|
7394
|
-
* 依赖
|
|
7273
|
+
* 依赖 `roleX-memory`"——B 端根本不欠任何声明)。同时修缺陷⑤:合取式
|
|
7395
7274
|
* (`dependsOn(X,A) and dependsOn(X,B)`)里 X 出现两次,旧实现不去重。
|
|
7396
7275
|
*/
|
|
7397
7276
|
function findDependsOnDeclarers(node) {
|
|
@@ -9307,49 +9186,23 @@ function createSkillCreatorTool() {
|
|
|
9307
9186
|
});
|
|
9308
9187
|
}
|
|
9309
9188
|
//#endregion
|
|
9310
|
-
//#region src/
|
|
9189
|
+
//#region src/tools/cheap-worker.ts
|
|
9311
9190
|
/**
|
|
9312
|
-
*
|
|
9313
|
-
* @param parentCtx - 发起 handyman 的 agent 的 scope ctx;无(headless/非 agent 上下文)时为 undefined。
|
|
9314
|
-
* @returns 继承结果:meta 用的 agentPreset 与创建 setup 钩子。
|
|
9315
|
-
*/
|
|
9316
|
-
function handymanPresetInheritance(parentCtx) {
|
|
9317
|
-
if (parentCtx === void 0) return { setup: (childCtx) => {
|
|
9318
|
-
childCtx.tools.restrict({ deny: ["handyman"] });
|
|
9319
|
-
} };
|
|
9320
|
-
const agentPreset = parentCtx.get("agentPresets")?.composedPreset(parentCtx);
|
|
9321
|
-
if (agentPreset === void 0) return { setup: (childCtx) => {
|
|
9322
|
-
childCtx.tools.restrict({ deny: ["handyman"] });
|
|
9323
|
-
} };
|
|
9324
|
-
return {
|
|
9325
|
-
agentPreset,
|
|
9326
|
-
setup: (childCtx) => {
|
|
9327
|
-
childCtx.get("agentPresets")?.composeFrom(childCtx, parentCtx);
|
|
9328
|
-
childCtx.tools.restrict({ deny: ["handyman"] });
|
|
9329
|
-
}
|
|
9330
|
-
};
|
|
9331
|
-
}
|
|
9332
|
-
//#endregion
|
|
9333
|
-
//#region src/tools/handyman.ts
|
|
9334
|
-
/**
|
|
9335
|
-
* handyman.ts — handyman(杂工)真实 DSH 工具(v1.24.0:loop 牛马 → handyman 杂工)
|
|
9191
|
+
* cheap-worker.ts — cheap-worker(杂工)真实 DSH 工具
|
|
9336
9192
|
*
|
|
9337
|
-
*
|
|
9338
|
-
*
|
|
9339
|
-
*
|
|
9340
|
-
*
|
|
9341
|
-
* - **内部递归同样低能 subagent**(worker 工具面含 subagent,DSH 原生模型继承)
|
|
9342
|
-
* - **workflow 能力**(jobs 编排:主 agent 一次并行多个杂工,maxParallel 默认 10)
|
|
9343
|
-
* - **worker 不含 handyman 本身**(递归编排归主 agent,防无限嵌套)
|
|
9344
|
-
* - **不兼容旧 loop 进度文件**(仅 handyman- 前缀)
|
|
9193
|
+
* v1.24.0:loop(牛马)→ handyman(杂工);2026-10-14:handyman → cheap-worker。
|
|
9194
|
+
* 🆕 2026-10-14(owner 令「删除后台运行能力」):**background 模式整条退场** ——
|
|
9195
|
+
* 轮次循环 / stop-token 完成码 / 进度文件 / 重启计数 / maxRounds/maxParallel 全部删除。
|
|
9196
|
+
* 现存只有**前台委派**一种形态:宿主起子 agent、等它跑完、取回最终文本。
|
|
9345
9197
|
*
|
|
9346
|
-
*
|
|
9347
|
-
*
|
|
9348
|
-
*
|
|
9349
|
-
* (重启计数,≤100),同一轮重试。工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
|
|
9198
|
+
* 机制:走宿主**委派正门** `ctx.subagents.start('spawn', …)`(深度上限、子 agent 所有权、
|
|
9199
|
+
* `subagent/start|end` 事件、`dispose()` 收尾全部沿用宿主实现),模型经 `agentOptions` 注入。
|
|
9200
|
+
* 前置:`exec.agent` 必须存在(前台委派要有发起方)。
|
|
9350
9201
|
*
|
|
9351
|
-
*
|
|
9352
|
-
*
|
|
9202
|
+
* 🔴 **模型不可由调用方指定**(owner 令):**没有 `model` 参数**,一律读 CCC 配置
|
|
9203
|
+
* `cheap-worker.models` / `cheap-worker.defaultModel`(单一真相源)。
|
|
9204
|
+
*
|
|
9205
|
+
* 递归防护:子 agent 的工具面 deny `cheap-worker` 本身(编排归主 agent,防无限嵌套)。
|
|
9353
9206
|
*/
|
|
9354
9207
|
function renderText$4(value) {
|
|
9355
9208
|
return [{
|
|
@@ -9357,52 +9210,18 @@ function renderText$4(value) {
|
|
|
9357
9210
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
9358
9211
|
}];
|
|
9359
9212
|
}
|
|
9360
|
-
/** 读取会话最后一个 assistant/message 文本 */
|
|
9361
|
-
function lastAssistantText$1(agent) {
|
|
9362
|
-
const s = agent.session;
|
|
9363
|
-
const events = typeof s.snapshotEvents === "function" ? s.snapshotEvents() : s.events ?? [];
|
|
9364
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
9365
|
-
const e = events[i];
|
|
9366
|
-
if (e && e.type === "assistant/message") {
|
|
9367
|
-
const data = e.data;
|
|
9368
|
-
const text = (data?.message?.content ?? data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
|
|
9369
|
-
if (text) return text;
|
|
9370
|
-
}
|
|
9371
|
-
}
|
|
9372
|
-
return "";
|
|
9373
|
-
}
|
|
9374
|
-
/** 非正常停止时重启 agent 的次数上限(防死循环保险阀):对话轮次有上限,重启有上限 */
|
|
9375
|
-
const HANDYMAN_MAX_RESTARTS = 100;
|
|
9376
|
-
/** 运行时解析 jobs 参数(DSH schema 不支持 object items 的 required/校验 → 手工校验类型) */
|
|
9377
|
-
function parseJobs(raw) {
|
|
9378
|
-
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
9379
|
-
const jobs = [];
|
|
9380
|
-
for (const item of raw) {
|
|
9381
|
-
if (typeof item !== "object" || item === null) return null;
|
|
9382
|
-
const rec = item;
|
|
9383
|
-
if (typeof rec.task !== "string" || typeof rec.label !== "string") return null;
|
|
9384
|
-
jobs.push({
|
|
9385
|
-
task: rec.task,
|
|
9386
|
-
label: rec.label,
|
|
9387
|
-
model: typeof rec.model === "string" ? rec.model : void 0
|
|
9388
|
-
});
|
|
9389
|
-
}
|
|
9390
|
-
return jobs;
|
|
9391
|
-
}
|
|
9392
9213
|
/**
|
|
9393
|
-
* 🔴
|
|
9214
|
+
* 🔴 **前台并行上限 = 10**(owner 2026-09-29 具名令:「这个版本顺手帮我把 handyman 并行度从 5 调整成 10」;
|
|
9394
9215
|
* 原值 5 出自 owner 2026-09-27 的具名令「最大 5;就当是支持自定模型的 subagent 用了,急需」)。
|
|
9395
9216
|
*
|
|
9396
9217
|
* 为什么是**硬编码**而不是又一个配置键:owner 两次都是**直接给数**(同 `REBUILD-TODO` 那次的裁决口径
|
|
9397
9218
|
* —— 设定项能不加就不加,见 S142 的「可砍设定项」)。若将来还要调,仍按具名令改这个常量。
|
|
9398
9219
|
*
|
|
9399
|
-
*
|
|
9400
|
-
*
|
|
9401
|
-
* ⚠️ 现在两档**数值相等但语义不同**(前台 = 占住本轮 / 后台 = 不占)⇒ **刻意不合并成一个常量**:
|
|
9402
|
-
* 合并会让"哪天要给前台单独降档"变成一次语义手术。
|
|
9220
|
+
* 为什么是 10 而不是更高:**前台委派会占住主 agent 的这一轮**(调用方在等结果),
|
|
9221
|
+
* 并发越高越容易把宿主的并发面与用量推上去。
|
|
9403
9222
|
*/
|
|
9404
9223
|
const FOREGROUND_MAX_PARALLEL = 10;
|
|
9405
|
-
/** foreground 的 jobs
|
|
9224
|
+
/** foreground 的 jobs 解析(label 可选 —— 前台 label 只是显示名,不命名任何文件) */
|
|
9406
9225
|
function parseForegroundJobs(raw) {
|
|
9407
9226
|
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
9408
9227
|
const jobs = [];
|
|
@@ -9412,8 +9231,7 @@ function parseForegroundJobs(raw) {
|
|
|
9412
9231
|
if (typeof rec.task !== "string" || rec.task.trim() === "") return null;
|
|
9413
9232
|
jobs.push({
|
|
9414
9233
|
task: rec.task,
|
|
9415
|
-
...typeof rec.label === "string" && rec.label !== "" ? { label: rec.label } : {}
|
|
9416
|
-
...typeof rec.model === "string" && rec.model !== "" ? { model: rec.model } : {}
|
|
9234
|
+
...typeof rec.label === "string" && rec.label !== "" ? { label: rec.label } : {}
|
|
9417
9235
|
});
|
|
9418
9236
|
}
|
|
9419
9237
|
return jobs;
|
|
@@ -9430,8 +9248,8 @@ function subagentOutputText(output) {
|
|
|
9430
9248
|
* ⇒ 一律**不阻断**,保持旧行为 —— 增强不得变成新的失败源)。
|
|
9431
9249
|
*
|
|
9432
9250
|
* 为什么必须放在"创建之前":宿主对未知 provider 抛 `no adapter registered for provider "…"`
|
|
9433
|
-
* (`dsh-llm`,code `NO_ADAPTER
|
|
9434
|
-
* `diagnostic
|
|
9251
|
+
* (`dsh-llm`,code `NO_ADAPTER`)。若照常创建:只拿回 `stopReason:"error"` + **空
|
|
9252
|
+
* `diagnostic`**(实测)⇒ 调用方(LLM)无法自我修正配置。
|
|
9435
9253
|
*/
|
|
9436
9254
|
function providerPreflight(ctx, model) {
|
|
9437
9255
|
const llm = hostLlm(ctx);
|
|
@@ -9456,14 +9274,13 @@ function providerPreflight(ctx, model) {
|
|
|
9456
9274
|
});
|
|
9457
9275
|
}
|
|
9458
9276
|
/**
|
|
9459
|
-
*
|
|
9277
|
+
* 单次前台串行委派。
|
|
9460
9278
|
*
|
|
9461
|
-
* 与 background 的差别(R↓):
|
|
9462
9279
|
* - 走宿主**委派正门** `ctx.subagents.start('spawn', …)`——深度上限、子 agent 所有权、
|
|
9463
9280
|
* `subagent/start|end` 生命周期事件、`dispose()` 收尾全部沿用宿主实现(不重写 agent 创建);
|
|
9464
9281
|
* - 模型经 `agentOptions` 注入(spawn 后端能力位 `agentOptions: true`),因此**不依赖**
|
|
9465
9282
|
* 宿主未放开的 `subagent-model-selection` 设置;
|
|
9466
|
-
* -
|
|
9283
|
+
* - 不循环、不写文件、不做完成码校验——"一次调用一次结果"。
|
|
9467
9284
|
*
|
|
9468
9285
|
* 前置条件:`opts.parent` 必须是发起 agent(由调用方 `execute` 在解析 CCC 之前校验;
|
|
9469
9286
|
* 本函数不再重复检查——单一真相源)。
|
|
@@ -9472,7 +9289,7 @@ async function runForegroundJob(ctx, opts) {
|
|
|
9472
9289
|
const { task, label, model, models, parent, signal } = opts;
|
|
9473
9290
|
requireWhitelistedModel(model, models);
|
|
9474
9291
|
const subagents = hostSubagents(ctx);
|
|
9475
|
-
if (!subagents?.start) throw new Error("
|
|
9292
|
+
if (!subagents?.start) throw new Error("cheap-worker: host subagents service unavailable — neither ctx.subagents nor ctx.get(\"subagents\") yielded a start() function. Check `dashboard health` (host-contract section, service \"subagents\").");
|
|
9476
9293
|
const preflight = providerPreflight(ctx, model);
|
|
9477
9294
|
if (preflight !== null) return {
|
|
9478
9295
|
mode: "foreground",
|
|
@@ -9495,7 +9312,7 @@ async function runForegroundJob(ctx, opts) {
|
|
|
9495
9312
|
...provider === void 0 ? {} : { provider },
|
|
9496
9313
|
model: modelName
|
|
9497
9314
|
},
|
|
9498
|
-
toolFilter: { deny: ["
|
|
9315
|
+
toolFilter: { deny: ["cheap-worker"] }
|
|
9499
9316
|
});
|
|
9500
9317
|
try {
|
|
9501
9318
|
const result = run.result === void 0 ? {} : await run.result;
|
|
@@ -9513,165 +9330,28 @@ async function runForegroundJob(ctx, opts) {
|
|
|
9513
9330
|
if (run.dispose) await run.dispose().catch(() => {});
|
|
9514
9331
|
}
|
|
9515
9332
|
}
|
|
9516
|
-
/**
|
|
9517
|
-
|
|
9518
|
-
* 同步阻塞直到完成/保险阀。jobs 编排时多个 worker 并行(Promise.all)。
|
|
9519
|
-
*/
|
|
9520
|
-
async function runHandymanJob(ctx, opts) {
|
|
9521
|
-
const { root, job, defaultModel, models, maxRounds, parentSession, parentCtx, session } = opts;
|
|
9522
|
-
const label = job.label;
|
|
9523
|
-
const model = job.model ?? defaultModel;
|
|
9524
|
-
requireWhitelistedModel(model, models);
|
|
9525
|
-
const preflightError = providerPreflight(ctx, model);
|
|
9526
|
-
if (preflightError !== null) throw new Error(preflightError);
|
|
9527
|
-
const { provider, model: modelName } = splitModel(model);
|
|
9528
|
-
const stopToken = newStopToken();
|
|
9529
|
-
let progress = readProgress(root, label);
|
|
9530
|
-
const startRound = progress ? progress.round + 1 : 1;
|
|
9531
|
-
const inherited = handymanPresetInheritance(parentCtx);
|
|
9532
|
-
if (!ctx.agents) throw new Error("handyman: ctx.agents unavailable");
|
|
9533
|
-
let handle;
|
|
9534
|
-
let workerAgent;
|
|
9535
|
-
const spawnAgent = async () => {
|
|
9536
|
-
const sessionId = `handyman-${label}-${randomUUID()}`;
|
|
9537
|
-
handle = await ctx.agents.create({
|
|
9538
|
-
sessionId,
|
|
9539
|
-
meta: {
|
|
9540
|
-
cwd: root,
|
|
9541
|
-
origin: "subagent",
|
|
9542
|
-
...parentSession === void 0 ? {} : { parentSession },
|
|
9543
|
-
...inherited.agentPreset === void 0 ? {} : { agentPreset: inherited.agentPreset }
|
|
9544
|
-
},
|
|
9545
|
-
agentOptions: {
|
|
9546
|
-
provider,
|
|
9547
|
-
model: modelName
|
|
9548
|
-
},
|
|
9549
|
-
...inherited.setup === void 0 ? {} : { setup: inherited.setup }
|
|
9550
|
-
});
|
|
9551
|
-
workerAgent = handle.agent;
|
|
9552
|
-
};
|
|
9553
|
-
await spawnAgent();
|
|
9554
|
-
let done = false;
|
|
9555
|
-
let lastResponse = progress?.lastResponse ?? "";
|
|
9556
|
-
let finalRound = startRound - 1;
|
|
9557
|
-
let restarts = 0;
|
|
9558
|
-
let finishReason = "done";
|
|
9559
|
-
try {
|
|
9560
|
-
let round = startRound;
|
|
9561
|
-
while (true) {
|
|
9562
|
-
if (round > maxRounds) {
|
|
9563
|
-
finishReason = "max_rounds";
|
|
9564
|
-
break;
|
|
9565
|
-
}
|
|
9566
|
-
finalRound = round;
|
|
9567
|
-
const prompt = buildRoundPrompt({
|
|
9568
|
-
root,
|
|
9569
|
-
session,
|
|
9570
|
-
label,
|
|
9571
|
-
round,
|
|
9572
|
-
stopToken,
|
|
9573
|
-
progress,
|
|
9574
|
-
task: job.task
|
|
9575
|
-
});
|
|
9576
|
-
try {
|
|
9577
|
-
workerAgent.followup(createUserMessage({
|
|
9578
|
-
content: [{
|
|
9579
|
-
type: "text",
|
|
9580
|
-
text: prompt
|
|
9581
|
-
}],
|
|
9582
|
-
source: PLUGIN_SOURCE
|
|
9583
|
-
}));
|
|
9584
|
-
await waitAgentIdle(ctx, workerAgent);
|
|
9585
|
-
lastResponse = lastAssistantText$1(workerAgent);
|
|
9586
|
-
} catch {
|
|
9587
|
-
restarts++;
|
|
9588
|
-
if (restarts > HANDYMAN_MAX_RESTARTS) {
|
|
9589
|
-
finishReason = "restart_exceeded";
|
|
9590
|
-
break;
|
|
9591
|
-
}
|
|
9592
|
-
await handle.dispose().catch(() => {});
|
|
9593
|
-
await spawnAgent();
|
|
9594
|
-
continue;
|
|
9595
|
-
}
|
|
9596
|
-
progress = {
|
|
9597
|
-
round,
|
|
9598
|
-
done: false,
|
|
9599
|
-
label,
|
|
9600
|
-
model,
|
|
9601
|
-
updated: isoLocal(),
|
|
9602
|
-
lastResponse
|
|
9603
|
-
};
|
|
9604
|
-
writeProgress(root, label, progress);
|
|
9605
|
-
if (lastResponse.includes(stopToken)) {
|
|
9606
|
-
done = true;
|
|
9607
|
-
finishReason = "done";
|
|
9608
|
-
break;
|
|
9609
|
-
}
|
|
9610
|
-
round++;
|
|
9611
|
-
}
|
|
9612
|
-
writeProgress(root, label, {
|
|
9613
|
-
round: finalRound,
|
|
9614
|
-
done,
|
|
9615
|
-
label,
|
|
9616
|
-
model,
|
|
9617
|
-
updated: isoLocal(),
|
|
9618
|
-
lastResponse,
|
|
9619
|
-
status: done ? "done" : "running"
|
|
9620
|
-
});
|
|
9621
|
-
if (finishReason !== "done") writeFailedStatus(root, label, {
|
|
9622
|
-
errorCode: finishReason,
|
|
9623
|
-
errorMessage: finishReason === "max_rounds" ? `Reached ${maxRounds} rounds without completion — resume with same label to continue.` : `Agent restarted ${HANDYMAN_MAX_RESTARTS} times without progress — resume with same label to continue.`
|
|
9624
|
-
});
|
|
9625
|
-
} finally {
|
|
9626
|
-
await handle.dispose().catch(() => {});
|
|
9627
|
-
}
|
|
9628
|
-
const { json } = handymanProgressPaths(root, label);
|
|
9629
|
-
return {
|
|
9630
|
-
label,
|
|
9631
|
-
done,
|
|
9632
|
-
rounds: finalRound,
|
|
9633
|
-
finishReason,
|
|
9634
|
-
restarts,
|
|
9635
|
-
model,
|
|
9636
|
-
lastResponse: lastResponse.slice(0, 2e3),
|
|
9637
|
-
progressFile: json
|
|
9638
|
-
};
|
|
9639
|
-
}
|
|
9640
|
-
/** 创建 handyman 工具(闭包捕获插件 ctx → 可访问 ctx.agents) */
|
|
9641
|
-
function createHandymanTool(ctx) {
|
|
9333
|
+
/** 创建 cheap-worker 工具(闭包捕获插件 ctx → 可访问 ctx.subagents) */
|
|
9334
|
+
function createCheapWorkerTool(ctx) {
|
|
9642
9335
|
return defineTool({
|
|
9643
|
-
name: "
|
|
9644
|
-
description: "The
|
|
9336
|
+
name: "cheap-worker",
|
|
9337
|
+
description: "The cheap-worker (杂工): delegate work to a worker agent on the CCC-configured model, and return its final text. One serial child per task; `jobs` fans out in parallel.\nUsage: cheap-worker(task, [label]) for a single delegation; cheap-worker(jobs=[{task,label?},...]) for parallel fan-out (cap 10, results keep your order).\nThe child does NOT share this conversation — the task must be self-contained.\nNo loop, no completion-code validation, no progress file, no unattended/background run.\nModel: NOT selectable per call — it is read from .opencode/serenity.json \"cheap-worker.models\" (missing config = error); \"cheap-worker.defaultModel\" names the one used, falling back to models[0].\nProvider check: the whitelist lives in the CCC (it travels with the repo) while provider adapters are registered per machine by the host — so before spawning, the provider of the resolved model is verified against the adapters present on THIS machine. If it is missing you get the available providers plus the exact fix in `diagnostic`, and no child is created.\nRecursion: a worker's tool set excludes cheap-worker itself (orchestration belongs to the main agent).\nGuide: cheap-worker(guide=true) prints the scale-up usage guide.",
|
|
9645
9338
|
parameters: {
|
|
9646
|
-
mode: {
|
|
9647
|
-
type: "string",
|
|
9648
|
-
description: "Delegation mode: \"foreground\" (default — child agent(s) run once and return their final text; accepts `jobs` for parallel fan-out, cap 10) or \"background\" (loop-validated worker with completion-code check, round cap, auto-restart, progress file, parallel jobs)"
|
|
9649
|
-
},
|
|
9650
9339
|
task: {
|
|
9651
9340
|
type: "string",
|
|
9652
|
-
description: "The task goal to complete (
|
|
9341
|
+
description: "The task goal to complete (must be self-contained — the child does not share this conversation)"
|
|
9653
9342
|
},
|
|
9654
9343
|
label: {
|
|
9655
9344
|
type: "string",
|
|
9656
|
-
description: "
|
|
9657
|
-
},
|
|
9658
|
-
session: {
|
|
9659
|
-
type: "string",
|
|
9660
|
-
description: "Work session S### (context hint, progress reference)"
|
|
9661
|
-
},
|
|
9662
|
-
model: {
|
|
9663
|
-
type: "string",
|
|
9664
|
-
description: "provider/model — must be in the CCC whitelist handyman.models; default reads handyman.defaultModel"
|
|
9345
|
+
description: "Optional child display name"
|
|
9665
9346
|
},
|
|
9666
9347
|
jobs: {
|
|
9667
9348
|
type: "array",
|
|
9668
|
-
description: "Multi-job orchestration (workflow capability): [{task, label
|
|
9349
|
+
description: "Multi-job orchestration (workflow capability): [{task, label?}, ...] — runs jobs in parallel, cap 10 (fixed). Results keep the order you passed them in.",
|
|
9669
9350
|
items: {
|
|
9670
9351
|
type: "object",
|
|
9671
9352
|
properties: {
|
|
9672
9353
|
task: { type: "string" },
|
|
9673
|
-
label: { type: "string" }
|
|
9674
|
-
model: { type: "string" }
|
|
9354
|
+
label: { type: "string" }
|
|
9675
9355
|
},
|
|
9676
9356
|
additionalProperties: false
|
|
9677
9357
|
}
|
|
@@ -9686,93 +9366,51 @@ function createHandymanTool(ctx) {
|
|
|
9686
9366
|
render: (_args, value) => renderText$4(value)
|
|
9687
9367
|
},
|
|
9688
9368
|
async execute(args, exec) {
|
|
9689
|
-
if (args.guide) return { guide:
|
|
9690
|
-
if (
|
|
9369
|
+
if (args.guide) return { guide: CHEAP_WORKER_GUIDE };
|
|
9370
|
+
if (!exec.agent) throw new Error("cheap-worker: requires a calling agent (exec.agent was undefined)");
|
|
9691
9371
|
const root = cccRootForExec(exec);
|
|
9692
9372
|
if (!root) throw new Error(NO_CCC_FROM_AGENT_CWD);
|
|
9693
|
-
const hc =
|
|
9694
|
-
if (hc === null) throw new Error("
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
if (
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
task: job.task,
|
|
9704
|
-
...job.label === void 0 ? {} : { label: job.label },
|
|
9705
|
-
model: job.model ?? (typeof args.model === "string" && args.model !== "" ? args.model : hc.defaultModel),
|
|
9706
|
-
models: hc.models,
|
|
9707
|
-
parent: exec.agent,
|
|
9708
|
-
signal: exec.signal
|
|
9709
|
-
})));
|
|
9710
|
-
const done = results.every((r) => r.done);
|
|
9711
|
-
return {
|
|
9712
|
-
mode: "foreground",
|
|
9713
|
-
done,
|
|
9714
|
-
jobs: results,
|
|
9715
|
-
usage: {
|
|
9716
|
-
how: `foreground parallel mode starts ${results.length} child agents at once through the host delegation service (ctx.subagents.start "spawn"), awaits all of them, and returns each child's final text. No loop, no completion-code validation, no progress file. Cap = ${FOREGROUND_MAX_PARALLEL}.`,
|
|
9717
|
-
model: "restricted to the CCC whitelist handyman.models; per-job model overrides the call-level model; default reads handyman.defaultModel",
|
|
9718
|
-
next: done ? "All children completed; use each job's output directly (results are in the same order as the jobs you passed)" : "Some children did not complete — inspect each job's diagnostic/output (results keep the order you passed); for anti-early-finish guarantees use mode=\"background\""
|
|
9719
|
-
}
|
|
9720
|
-
};
|
|
9721
|
-
}
|
|
9722
|
-
if (typeof args.task !== "string" || args.task.trim() === "") throw new Error("handyman foreground: task is required (and must be self-contained — the child does not share this conversation)");
|
|
9723
|
-
const model = typeof args.model === "string" && args.model !== "" ? args.model : hc.defaultModel;
|
|
9724
|
-
const res = await runForegroundJob(ctx, {
|
|
9725
|
-
task: args.task,
|
|
9726
|
-
...typeof args.label === "string" && args.label !== "" ? { label: args.label } : {},
|
|
9727
|
-
model,
|
|
9373
|
+
const hc = readCheapWorkerConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
|
|
9374
|
+
if (hc === null) throw new Error("cheap-worker requires a model whitelist: configure .opencode/serenity.json \"cheap-worker.models\" (e.g. {\"cheap-worker\": {\"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"}})");
|
|
9375
|
+
if (args.jobs !== void 0) {
|
|
9376
|
+
const jobs = parseForegroundJobs(args.jobs);
|
|
9377
|
+
if (jobs === null) throw new Error("cheap-worker: jobs must be [{task, label?}, ...] (every job requires a non-empty task)");
|
|
9378
|
+
if (jobs.length > FOREGROUND_MAX_PARALLEL) throw new Error(`cheap-worker: ${jobs.length} jobs exceed the parallel cap ${FOREGROUND_MAX_PARALLEL} (split into batches)`);
|
|
9379
|
+
const results = await Promise.all(jobs.map((job) => runForegroundJob(ctx, {
|
|
9380
|
+
task: job.task,
|
|
9381
|
+
...job.label === void 0 ? {} : { label: job.label },
|
|
9382
|
+
model: hc.defaultModel,
|
|
9728
9383
|
models: hc.models,
|
|
9729
9384
|
parent: exec.agent,
|
|
9730
9385
|
signal: exec.signal
|
|
9731
|
-
});
|
|
9386
|
+
})));
|
|
9387
|
+
const done = results.every((r) => r.done);
|
|
9732
9388
|
return {
|
|
9733
|
-
|
|
9389
|
+
mode: "foreground",
|
|
9390
|
+
done,
|
|
9391
|
+
jobs: results,
|
|
9734
9392
|
usage: {
|
|
9735
|
-
how:
|
|
9736
|
-
model: "
|
|
9737
|
-
next:
|
|
9393
|
+
how: `foreground parallel mode starts ${results.length} child agents at once through the host delegation service (ctx.subagents.start "spawn"), awaits all of them, and returns each child's final text. No loop, no completion-code validation, no progress file. Cap = ${FOREGROUND_MAX_PARALLEL}.`,
|
|
9394
|
+
model: "NOT selectable per call — resolved from the CCC config cheap-worker.models (cheap-worker.defaultModel, else models[0])",
|
|
9395
|
+
next: done ? "All children completed; use each job's output directly (results are in the same order as the jobs you passed)" : "Some children did not complete — inspect each job's diagnostic/output (results keep the order you passed)"
|
|
9738
9396
|
}
|
|
9739
9397
|
};
|
|
9740
9398
|
}
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
if (typeof args.task !== "string" || typeof args.label !== "string") throw new Error("handyman: task and label are required (single-job mode)");
|
|
9747
|
-
return [{
|
|
9748
|
-
task: args.task,
|
|
9749
|
-
label: args.label,
|
|
9750
|
-
...typeof args.model === "string" && args.model !== "" ? { model: args.model } : {}
|
|
9751
|
-
}];
|
|
9752
|
-
})();
|
|
9753
|
-
if (jobs.length === 0) throw new Error("handyman: no jobs provided");
|
|
9754
|
-
if (jobs.length > hc.maxParallel) throw new Error(`handyman: ${jobs.length} jobs exceed maxParallel ${hc.maxParallel} (configure handyman.maxParallel to raise)`);
|
|
9755
|
-
if (jobs.some((j) => !j.task || !j.label)) throw new Error("handyman: every job requires task and label");
|
|
9756
|
-
const parentCtx = exec.agent?.ctx;
|
|
9757
|
-
const parentSession = (exec.agent?.session)?.id;
|
|
9758
|
-
const results = await Promise.all(jobs.map((job) => runHandymanJob(ctx, {
|
|
9759
|
-
root,
|
|
9760
|
-
job,
|
|
9761
|
-
defaultModel: hc.defaultModel,
|
|
9399
|
+
if (typeof args.task !== "string" || args.task.trim() === "") throw new Error("cheap-worker: task is required (and must be self-contained — the child does not share this conversation)");
|
|
9400
|
+
const res = await runForegroundJob(ctx, {
|
|
9401
|
+
task: args.task,
|
|
9402
|
+
...typeof args.label === "string" && args.label !== "" ? { label: args.label } : {},
|
|
9403
|
+
model: hc.defaultModel,
|
|
9762
9404
|
models: hc.models,
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
session: typeof args.session === "string" ? args.session : void 0
|
|
9767
|
-
})));
|
|
9405
|
+
parent: exec.agent,
|
|
9406
|
+
signal: exec.signal
|
|
9407
|
+
});
|
|
9768
9408
|
return {
|
|
9769
|
-
|
|
9770
|
-
jobs: results.map((r) => ({ ...r })),
|
|
9409
|
+
...res,
|
|
9771
9410
|
usage: {
|
|
9772
|
-
how: "
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
next: results.every((r) => r.done) ? "All jobs completed; check the progress files to wrap up" : "Some jobs incomplete (max_rounds / restart_exceeded fail-safe); resume with the same labels"
|
|
9411
|
+
how: "foreground mode starts ONE serial child agent through the host delegation service (ctx.subagents.start \"spawn\") with the CCC-configured model; it awaits the run and returns the child's final text. No loop, no completion-code validation, no progress file.",
|
|
9412
|
+
model: "NOT selectable per call — resolved from the CCC config cheap-worker.models (cheap-worker.defaultModel, else models[0])",
|
|
9413
|
+
next: res.done ? "Child completed; use its output directly" : `Child did not complete (stopReason=${res.stopReason}); inspect diagnostic/output and decide whether to retry`
|
|
9776
9414
|
}
|
|
9777
9415
|
};
|
|
9778
9416
|
}
|
|
@@ -11985,13 +11623,15 @@ function registerWakeScheduler(ctx) {
|
|
|
11985
11623
|
/**
|
|
11986
11624
|
* 样例快照输入 —— **代表性数据**(不是真实某条轨迹的现状)。
|
|
11987
11625
|
*
|
|
11988
|
-
* 取值刻意让它能演示所有者举的
|
|
11626
|
+
* 取值刻意让它能演示所有者举的"天亮/有人/非高峰 + 已在干活 + 日志过大"场景。
|
|
11627
|
+
* 🔒 **路径与轨迹名一律用中性占位**(`/home/user/workspace`、`<S###>` 式示例),
|
|
11628
|
+
* 不写任何真实机器路径或私有会话名 —— 本文件随 npm 包发布(2026-10-14 隐私核查)。
|
|
11989
11629
|
*/
|
|
11990
11630
|
const CRO_SAMPLE_SNAPSHOT_INPUT = {
|
|
11991
|
-
dirName: "2026-
|
|
11992
|
-
cccRoot: "/home/
|
|
11631
|
+
dirName: "2026-01-01--S001--example-trajectory",
|
|
11632
|
+
cccRoot: "/home/user/workspace",
|
|
11993
11633
|
nowMs: 17898e8,
|
|
11994
|
-
sessionMdPath: "/home/
|
|
11634
|
+
sessionMdPath: "/home/user/workspace/AGENT_SESSIONS/2026-01-01--S001--example-trajectory/SESSION.md",
|
|
11995
11635
|
sessionMdBytes: 182e3,
|
|
11996
11636
|
sessionMdMtimeMs: 1789799e6,
|
|
11997
11637
|
references: [{
|
|
@@ -12007,10 +11647,10 @@ const CRO_SAMPLE_SNAPSHOT_INPUT = {
|
|
|
12007
11647
|
liveSessionIds: ["session-adccc461-9f32-4d9a-a688-9da81fa1ca35"],
|
|
12008
11648
|
runningSessionIds: [],
|
|
12009
11649
|
pendingWakes: [{
|
|
12010
|
-
id: "w-
|
|
12011
|
-
at: "2026-
|
|
12012
|
-
createdAt: "2026-
|
|
12013
|
-
createdBy: "
|
|
11650
|
+
id: "w-20260101-0900-aaaa",
|
|
11651
|
+
at: "2026-01-01T09:30:00+08:00",
|
|
11652
|
+
createdAt: "2026-01-01T07:00:00+08:00",
|
|
11653
|
+
createdBy: "S001"
|
|
12014
11654
|
}],
|
|
12015
11655
|
scheduler: {
|
|
12016
11656
|
armed: true,
|
|
@@ -12031,7 +11671,7 @@ function renderCroSampleSnapshot() {
|
|
|
12031
11671
|
/**
|
|
12032
11672
|
* CRO 编写指南正文(ACC 内置手册;由工具的 guide 动作输出)。
|
|
12033
11673
|
*
|
|
12034
|
-
* 结构照既有 guide 惯例(`MSM_GUIDE` / `
|
|
11674
|
+
* 结构照既有 guide 惯例(`MSM_GUIDE` / `cheap-worker(guide=true)`):
|
|
12035
11675
|
* 用途 → 位置 → 我给什么 → 你给我什么 → 骨架 → 常见模式 → 坑 → **自测**。
|
|
12036
11676
|
*/
|
|
12037
11677
|
const CRO_GUIDE = `CRO(Continuous Re-Occurrence · 持续再发生)编写指南
|
|
@@ -12558,7 +12198,7 @@ function renderText$2(value) {
|
|
|
12558
12198
|
}
|
|
12559
12199
|
const localstoreTool = defineTool({
|
|
12560
12200
|
name: "localstore",
|
|
12561
|
-
description: "ACC standard local credential/config storage: one tool manages two namespaces — credential (credentials) and config (local preferences). Stored in localstore.json at the CCC root (JSON format, directly readable by MSMs). Git policy: .opencode/serenity.json localstore.gitTrack (allow commits / deny commits, default deny; .dsh fallback) — when deny, writes ensure .gitignore contains this file (physical guarantee), container_git commit checks and refuses. Subcommands: list (list keys, credentials never return values) / get <name> (read value) / set <name> <value> (write) / unset <name> (delete) / show <name> (metadata, credentials never print values) / doc (output the storage spec — path/format/key conventions/git policy; agents may operate the file directly with read/write per the spec). Default scope=credential; config requires --scope config (path is section.key, e.g.
|
|
12201
|
+
description: "ACC standard local credential/config storage: one tool manages two namespaces — credential (credentials) and config (local preferences). Stored in localstore.json at the CCC root (JSON format, directly readable by MSMs). Git policy: .opencode/serenity.json localstore.gitTrack (allow commits / deny commits, default deny; .dsh fallback) — when deny, writes ensure .gitignore contains this file (physical guarantee), container_git commit checks and refuses. Subcommands: list (list keys, credentials never return values) / get <name> (read value) / set <name> <value> (write) / unset <name> (delete) / show <name> (metadata, credentials never print values) / doc (output the storage spec — path/format/key conventions/git policy; agents may operate the file directly with read/write per the spec). Default scope=credential; config requires --scope config (path is section.key, e.g. cheap-worker.models).",
|
|
12562
12202
|
parameters: {
|
|
12563
12203
|
action: {
|
|
12564
12204
|
type: "string",
|
|
@@ -12602,7 +12242,7 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
|
|
|
12602
12242
|
── 角色配置 schema ──
|
|
12603
12243
|
{ "skiff": { "roles": {
|
|
12604
12244
|
"<role-name>": {
|
|
12605
|
-
"model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退
|
|
12245
|
+
"model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退 cheap-worker.defaultModel)
|
|
12606
12246
|
"msms": ["msm-a", "msm-b"], // MSM 白名单(独立):msm 工具只能执行这些;register/deregister 必拒
|
|
12607
12247
|
"tools": ["read","grep","glob",...], // 非 MSM 工具白名单(独立):白名单外工具一律不可用(guard 强制)
|
|
12608
12248
|
"trajectory": { "session": false, "keeper": false, "rebuild": false }, // 轨迹纪律子集(缺省全关 = 完全独立)
|
|
@@ -12646,7 +12286,7 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
|
|
|
12646
12286
|
会话追问(v1.25.10):调试页持有当前会话,追问自动续接(同会话上下文延续);
|
|
12647
12287
|
「新对话」按钮开新会话;进程重启后旧会话不可续(WebUI 仍可见历史)
|
|
12648
12288
|
注意:skill 加载对 skiff **恒可用**(不设白名单——读知识面,无写能力)`;
|
|
12649
|
-
/** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈
|
|
12289
|
+
/** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ cheap-worker.models / systemPrompt 非空 */
|
|
12650
12290
|
function validateSkiffConfig(root) {
|
|
12651
12291
|
const roles = readSkiffRoles(root);
|
|
12652
12292
|
const issues = [];
|
|
@@ -12657,7 +12297,7 @@ function validateSkiffConfig(root) {
|
|
|
12657
12297
|
note: "no skiff roles defined (skiff disabled — zero impact)"
|
|
12658
12298
|
};
|
|
12659
12299
|
const registered = new Set(loadMsmEntries(root).map((e) => e.name));
|
|
12660
|
-
const hc =
|
|
12300
|
+
const hc = readCheapWorkerConfig(root);
|
|
12661
12301
|
const allowedModels = hc ? new Set(hc.models) : null;
|
|
12662
12302
|
for (const [name, role] of roles) {
|
|
12663
12303
|
try {
|
|
@@ -12666,7 +12306,7 @@ function validateSkiffConfig(root) {
|
|
|
12666
12306
|
issues.push(`role "${name}": ${String(err?.message ?? err)}`);
|
|
12667
12307
|
}
|
|
12668
12308
|
for (const m of role.msms ?? []) if (!registered.has(m)) issues.push(`role "${name}": msms "${m}" is not registered in mech-registry.json`);
|
|
12669
|
-
if (role.model && allowedModels && !allowedModels.has(role.model)) issues.push(`role "${name}": model "${role.model}" not in
|
|
12309
|
+
if (role.model && allowedModels && !allowedModels.has(role.model)) issues.push(`role "${name}": model "${role.model}" not in cheap-worker.models (${[...allowedModels].join(", ") || "(none)"})`);
|
|
12670
12310
|
}
|
|
12671
12311
|
return {
|
|
12672
12312
|
ok: issues.length === 0,
|
|
@@ -12683,7 +12323,7 @@ function listSkiffRoles(root) {
|
|
|
12683
12323
|
};
|
|
12684
12324
|
const items = [...roles.entries()].map(([name, role]) => ({
|
|
12685
12325
|
name,
|
|
12686
|
-
model: role.model ?? "(
|
|
12326
|
+
model: role.model ?? "(cheap-worker default)",
|
|
12687
12327
|
msms: role.msms ?? [],
|
|
12688
12328
|
tools: role.tools ?? [],
|
|
12689
12329
|
trajectory: {
|
|
@@ -12913,7 +12553,7 @@ function createContainerAdminTool(ctx) {
|
|
|
12913
12553
|
* 注入文本**自带**"请回复 ABCD" ⇒ 模型**复述指令**即**假完成**(高概率,不是小概率)
|
|
12914
12554
|
* ⇒ 用**每轮新生成**的 nonce + **包装 + 独占一行**(`[[UNATTENDED-DONE:<hex>]]`),
|
|
12915
12555
|
* 明写「**旧码作废**」,校验**只认当前轮**(防上下文里的旧码复活)。
|
|
12916
|
-
* 先例 = `
|
|
12556
|
+
* 先例 = `cheap-worker` background 的 `SERENITY_CHEAP_WORKER_DONE_<16hex>`(同族实现)。
|
|
12917
12557
|
*/
|
|
12918
12558
|
/** 完成码包装前缀(**独占一行**才有意义;见文件头"禁用 4 位字母码") */
|
|
12919
12559
|
const UNATTENDED_DONE_PREFIX = "[[UNATTENDED-DONE:";
|
|
@@ -13020,14 +12660,14 @@ function claimsUserNotification(text) {
|
|
|
13020
12660
|
/**
|
|
13021
12661
|
* **不许开**的会话(设计 §3 的"建议写死",本版**确实写死**):
|
|
13022
12662
|
* `skiff-`(F4 问答 / **微信桥会话也是这个前缀** —— 桥那侧的"用户"是**家人**,
|
|
13023
|
-
* 代理替家人说话 = **冒充**)| `acp-`(对外程序化面)| `
|
|
12663
|
+
* 代理替家人说话 = **冒充**)| `acp-`(对外程序化面)| `cheap-worker-`(自主 worker,已有自己的完成码循环)。
|
|
13024
12664
|
*
|
|
13025
12665
|
* ⚠️ **`rebuild-` 不在排除面**(与 `output-guard-seam.ts` 的取向**刻意不同**):重建会话是
|
|
13026
12666
|
* **同一轨迹的载体**(内部维护面),不是对外面;把它排除会让"重建后就没人推了"。
|
|
13027
12667
|
*/
|
|
13028
12668
|
function isProxyExcludedSessionId(sessionId) {
|
|
13029
12669
|
if (typeof sessionId !== "string" || sessionId === "") return true;
|
|
13030
|
-
return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("
|
|
12670
|
+
return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("cheap-worker-");
|
|
13031
12671
|
}
|
|
13032
12672
|
/**
|
|
13033
12673
|
* 一次判定的**唯一入口**(纯函数)。
|
|
@@ -14137,7 +13777,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
|
|
|
14137
13777
|
promoted: true
|
|
14138
13778
|
};
|
|
14139
13779
|
const loopSid = session.id;
|
|
14140
|
-
if (typeof loopSid === "string" && (loopSid.startsWith("
|
|
13780
|
+
if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || isSkiffSessionId(loopSid))) return {
|
|
14141
13781
|
boundary: -1,
|
|
14142
13782
|
promoted: true
|
|
14143
13783
|
};
|
|
@@ -14241,7 +13881,7 @@ function registerBootstrap(ctx) {
|
|
|
14241
13881
|
const session = agent.session;
|
|
14242
13882
|
const depth = session?.header?.delegationDepth ?? 0;
|
|
14243
13883
|
const sid = typeof session?.id === "string" ? session.id : void 0;
|
|
14244
|
-
if (sid !== void 0 && (sid.startsWith("
|
|
13884
|
+
if (sid !== void 0 && (sid.startsWith("cheap-worker-") || isSkiffSessionId(sid))) return;
|
|
14245
13885
|
if (depth === 0) {
|
|
14246
13886
|
if (hasUserMessageHistory(session)) return;
|
|
14247
13887
|
} else if (sid !== void 0 && anchoredSessions.has(sid)) return;
|
|
@@ -14782,7 +14422,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
|
|
|
14782
14422
|
}
|
|
14783
14423
|
/**
|
|
14784
14424
|
* ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + Phase 2)。
|
|
14785
|
-
* ⚠️ 原文案含「+
|
|
14425
|
+
* ⚠️ 原文案含「+ cheap-worker 模型」,该注入已于 2026-09-16 删除(§0e-G)⇒ 此处同步订正(散文级残留)。
|
|
14786
14426
|
* 完整身份(ACC 5 块 + CCE + Constraints + EAP + SKILL 全文 + Session 块)由**系统提示词层**
|
|
14787
14427
|
* (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
|
|
14788
14428
|
* 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
|
|
@@ -14810,7 +14450,7 @@ function agentScope(agent) {
|
|
|
14810
14450
|
* 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
|
|
14811
14451
|
* - subagent:session header `origin === 'subagent'`(DSH 路由语义,agent-lookup.ts)
|
|
14812
14452
|
* - 派生会话:`parentSession` 存在(任何子会话)
|
|
14813
|
-
* -
|
|
14453
|
+
* - cheap-worker 杂工:sessionId 固定 `cheap-worker-` 前缀(tools/cheap-worker.ts 生成)
|
|
14814
14454
|
* 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
|
|
14815
14455
|
*/
|
|
14816
14456
|
function shouldAutoRestore(agent) {
|
|
@@ -14818,7 +14458,7 @@ function shouldAutoRestore(agent) {
|
|
|
14818
14458
|
if (!session) return false;
|
|
14819
14459
|
if (session.header?.origin === "subagent") return false;
|
|
14820
14460
|
if (session.header?.parentSession) return false;
|
|
14821
|
-
if (session.id?.startsWith("
|
|
14461
|
+
if (session.id?.startsWith("cheap-worker-")) return false;
|
|
14822
14462
|
if (isSkiffSessionId(session.id)) return false;
|
|
14823
14463
|
return true;
|
|
14824
14464
|
}
|
|
@@ -14979,7 +14619,7 @@ function registerCompactRetention(ctx, opts = {}) {
|
|
|
14979
14619
|
/**
|
|
14980
14620
|
* status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
|
|
14981
14621
|
*
|
|
14982
|
-
* WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 /
|
|
14622
|
+
* WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / cheap-worker 模型。
|
|
14983
14623
|
* setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
|
|
14984
14624
|
*/
|
|
14985
14625
|
/** 宿主包名 —— 从运行入口反推包根时的**身份核对**用(见 {@link dshVersionFromEntry}) */
|
|
@@ -15064,7 +14704,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
|
15064
14704
|
safeModeOn: false,
|
|
15065
14705
|
blacklist: [],
|
|
15066
14706
|
threshold: null,
|
|
15067
|
-
|
|
14707
|
+
cheapWorkerModel: null,
|
|
15068
14708
|
restrict
|
|
15069
14709
|
};
|
|
15070
14710
|
const cfg = loadSerenityConfig(root, configPaths);
|
|
@@ -15074,7 +14714,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
|
15074
14714
|
safeModeOn: isSafeModeOn(root),
|
|
15075
14715
|
blacklist: readBlacklist(root, configPaths),
|
|
15076
14716
|
threshold: cfg.sessionKeeper?.threshold ?? null,
|
|
15077
|
-
|
|
14717
|
+
cheapWorkerModel: readCheapWorkerConfig(root, configPaths)?.defaultModel ?? null,
|
|
15078
14718
|
restrict
|
|
15079
14719
|
};
|
|
15080
14720
|
}
|
|
@@ -15089,7 +14729,6 @@ function setSafeMode(root, on) {
|
|
|
15089
14729
|
//#endregion
|
|
15090
14730
|
//#region src/api.ts
|
|
15091
14731
|
const ROUTE_PATH = "/serenity/status";
|
|
15092
|
-
const HANDYMEN_PATH = "/serenity/handymen";
|
|
15093
14732
|
const UPLOAD_PATH = "/serenity/image-upload";
|
|
15094
14733
|
const FILE_UPLOAD_PATH = "/serenity/file-upload";
|
|
15095
14734
|
const CONFIG_PATH = "/serenity/config";
|
|
@@ -15244,31 +14883,6 @@ function resolveWorkspace(ctx, params) {
|
|
|
15244
14883
|
}
|
|
15245
14884
|
function registerStatusApi(ctx, opts = {}) {
|
|
15246
14885
|
const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
|
|
15247
|
-
ctx.webServer.register({
|
|
15248
|
-
kind: "exact",
|
|
15249
|
-
path: HANDYMEN_PATH,
|
|
15250
|
-
handler: async (req, res) => {
|
|
15251
|
-
try {
|
|
15252
|
-
if (req.method !== "GET") {
|
|
15253
|
-
sendJson$4(res, 405, { error: "method not allowed" });
|
|
15254
|
-
return;
|
|
15255
|
-
}
|
|
15256
|
-
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
15257
|
-
const workspace = resolveWorkspace(ctx, {
|
|
15258
|
-
sessionId: url.searchParams.get("sessionId") ?? void 0,
|
|
15259
|
-
workspace: url.searchParams.get("workspace") ?? void 0
|
|
15260
|
-
});
|
|
15261
|
-
const root = cccRootForCwd(workspace);
|
|
15262
|
-
if (!root) {
|
|
15263
|
-
sendJson$4(res, 200, { handymen: [] });
|
|
15264
|
-
return;
|
|
15265
|
-
}
|
|
15266
|
-
sendJson$4(res, 200, { handymen: listActiveHandymen(root) });
|
|
15267
|
-
} catch (err) {
|
|
15268
|
-
sendJson$4(res, 400, { error: err.message ?? String(err) });
|
|
15269
|
-
}
|
|
15270
|
-
}
|
|
15271
|
-
});
|
|
15272
14886
|
ctx.webServer.register({
|
|
15273
14887
|
kind: "exact",
|
|
15274
14888
|
path: UPLOAD_PATH,
|
|
@@ -15482,11 +15096,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15482
15096
|
sendJson$4(res, 400, { error: chk.error });
|
|
15483
15097
|
return;
|
|
15484
15098
|
}
|
|
15485
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
15099
|
+
const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15486
15100
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15487
15101
|
const settings = readWeixinSettings(chk.root);
|
|
15488
15102
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
15489
|
-
const { readWeixinCredential } = await import("./weixin-route-
|
|
15103
|
+
const { readWeixinCredential } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15490
15104
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
15491
15105
|
accountId: a.accountId,
|
|
15492
15106
|
name: a.name ?? void 0,
|
|
@@ -15518,7 +15132,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15518
15132
|
const { fetchQRCode } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
|
|
15519
15133
|
const { randomUUID } = await import("node:crypto");
|
|
15520
15134
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
15521
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
15135
|
+
const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15522
15136
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
15523
15137
|
const loginKey = randomUUID();
|
|
15524
15138
|
weixinLogins.set(loginKey, {
|
|
@@ -15539,7 +15153,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15539
15153
|
sendJson$4(res, 400, { error: "missing accountId" });
|
|
15540
15154
|
return;
|
|
15541
15155
|
}
|
|
15542
|
-
const { removeWeixinAccount } = await import("./weixin-route-
|
|
15156
|
+
const { removeWeixinAccount } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15543
15157
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15544
15158
|
removeWeixinAccount(root, body.accountId);
|
|
15545
15159
|
syncCccBridge(ctx, root);
|
|
@@ -15552,8 +15166,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15552
15166
|
sendJson$4(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
15553
15167
|
return;
|
|
15554
15168
|
}
|
|
15555
|
-
const { saveWeixinRoutes } = await import("./weixin-route-
|
|
15556
|
-
const { readSkiffRoles } = await import("./skiff-role-
|
|
15169
|
+
const { saveWeixinRoutes } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15170
|
+
const { readSkiffRoles } = await import("./skiff-role-B615L9MV.js").then((n) => n.u);
|
|
15557
15171
|
const roles = readSkiffRoles(root);
|
|
15558
15172
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
15559
15173
|
sendJson$4(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
|
|
@@ -15565,7 +15179,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15565
15179
|
}
|
|
15566
15180
|
if (body.action === "set-enabled") {
|
|
15567
15181
|
const enabled = body.enabled === true;
|
|
15568
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-
|
|
15182
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15569
15183
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15570
15184
|
const settings = readWeixinSettings(root);
|
|
15571
15185
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -15609,7 +15223,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15609
15223
|
return;
|
|
15610
15224
|
}
|
|
15611
15225
|
const { pollQRStatus } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
|
|
15612
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
15226
|
+
const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15613
15227
|
const settings = readWeixinSettings(login.root);
|
|
15614
15228
|
const status = await pollQRStatus({
|
|
15615
15229
|
baseUrl: void 0,
|
|
@@ -15624,7 +15238,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15624
15238
|
});
|
|
15625
15239
|
return;
|
|
15626
15240
|
}
|
|
15627
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-
|
|
15241
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
|
|
15628
15242
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15629
15243
|
const accountId = nextWeixinAccountId(settings);
|
|
15630
15244
|
upsertWeixinAccount(login.root, {
|
|
@@ -15988,7 +15602,7 @@ const HOST_SERVICES = [
|
|
|
15988
15602
|
name: "get",
|
|
15989
15603
|
kind: "function"
|
|
15990
15604
|
}],
|
|
15991
|
-
impact: "skiff/
|
|
15605
|
+
impact: "skiff/cheap-worker 无法创建或复用 agent",
|
|
15992
15606
|
required: true
|
|
15993
15607
|
},
|
|
15994
15608
|
{
|
|
@@ -16070,7 +15684,7 @@ const HOST_SERVICES = [
|
|
|
16070
15684
|
name: "agentLoop",
|
|
16071
15685
|
access: "injected",
|
|
16072
15686
|
members: [],
|
|
16073
|
-
impact: "
|
|
15687
|
+
impact: "cheap-worker worker agent 无法装配 preset",
|
|
16074
15688
|
required: false
|
|
16075
15689
|
},
|
|
16076
15690
|
{
|
|
@@ -16081,7 +15695,7 @@ const HOST_SERVICES = [
|
|
|
16081
15695
|
name: "start",
|
|
16082
15696
|
kind: "function"
|
|
16083
15697
|
}],
|
|
16084
|
-
impact: "
|
|
15698
|
+
impact: "cheap-worker 委派不可用(前台委派的正门,响亮报错)",
|
|
16085
15699
|
required: false
|
|
16086
15700
|
},
|
|
16087
15701
|
{
|
|
@@ -17551,7 +17165,7 @@ function sessionNameFromMdPath(mdPath) {
|
|
|
17551
17165
|
* v1.30.13(S142 用户"微信桥 + skiff 会话锚定要准确",诊断 D4):**移除**原 ④「全局最新未完成
|
|
17552
17166
|
* 会话」(`findLatestActiveSessionMd`)。理由(R↓):④ 是**跨轨迹**猜测——候选①②③全空时,
|
|
17553
17167
|
* 它会把 rebuild 静默接到 AGENT_SESSIONS 里最新的另一个会话(临时 skiff 会话 persistent=false、
|
|
17554
|
-
*
|
|
17168
|
+
* 重启后首条消息前、新会话首条消息都命中该形态),而"接错轨迹"比重建失败危险得多
|
|
17555
17169
|
* (错误轨迹被继续写入 + 正确轨迹静默丢失)。现在全部候选失败 → 返回 null → 调用方
|
|
17556
17170
|
* 报错引导用户显式 `container_trajectory use`(响亮失败优于静默接错)。
|
|
17557
17171
|
* 备选:④ 仅在"同 scope 确无绑定"时兜底——同样会接错轨迹,故不采纳。
|
|
@@ -19891,7 +19505,7 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
|
|
|
19891
19505
|
agent = live;
|
|
19892
19506
|
continued = true;
|
|
19893
19507
|
}
|
|
19894
|
-
const hc =
|
|
19508
|
+
const hc = readCheapWorkerConfig(ccc);
|
|
19895
19509
|
if (!agent) agent = (await createSkiffAgent(ctx, ccc, roleName, role, hc?.defaultModel)).agent;
|
|
19896
19510
|
const result = await askSkiff(ctx, agent, question, 0);
|
|
19897
19511
|
sendJson$2(res, 200, {
|
|
@@ -20416,7 +20030,7 @@ var AcpServer = class {
|
|
|
20416
20030
|
continued: true
|
|
20417
20031
|
};
|
|
20418
20032
|
}
|
|
20419
|
-
const hc =
|
|
20033
|
+
const hc = readCheapWorkerConfig(ccc);
|
|
20420
20034
|
return {
|
|
20421
20035
|
sessionId: (await createSkiffAgent(this.ctx, ccc, roleName, role, hc?.defaultModel)).sessionId,
|
|
20422
20036
|
role: roleName,
|
|
@@ -20761,7 +20375,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
|
|
|
20761
20375
|
continued = true;
|
|
20762
20376
|
}
|
|
20763
20377
|
}
|
|
20764
|
-
if (!agent) agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg,
|
|
20378
|
+
if (!agent) agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, readCheapWorkerConfig(root)?.defaultModel)).agent;
|
|
20765
20379
|
const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
|
|
20766
20380
|
sendJson$1(res, 200, {
|
|
20767
20381
|
answer: result.answer,
|
|
@@ -21742,7 +21356,7 @@ async function runAccountLoop(ctx, root, accountId, cred, loop, machine = false)
|
|
|
21742
21356
|
*
|
|
21743
21357
|
* 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值
|
|
21744
21358
|
* (CCC 根 / 账号 / 用户 id)。**纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在
|
|
21745
|
-
* CCC 的角色提示词文件里(如 `.opencode/skiff
|
|
21359
|
+
* CCC 的角色提示词文件里(如 `.opencode/skiff/<role>.md` 的「输出通道」节),CCC 可随时改,
|
|
21746
21360
|
* 且 v1.30.15 起角色提示词热重载——改完立即生效,无需 ACC 发版。
|
|
21747
21361
|
*
|
|
21748
21362
|
* 为什么不用 ACC 写措辞(历史教训):v1.30.10~v1.30.15 由 ACC 内嵌一段纪律文案,用户实测
|
|
@@ -21780,7 +21394,7 @@ function manualOutputFallbackNeeded(input) {
|
|
|
21780
21394
|
*/
|
|
21781
21395
|
async function resolveCccRootByAlias(ctx, alias) {
|
|
21782
21396
|
try {
|
|
21783
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21397
|
+
const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
|
|
21784
21398
|
const entries = await listCccs(ctx);
|
|
21785
21399
|
const byName = entries.find((e) => e.name === alias);
|
|
21786
21400
|
if (byName) return byName.root;
|
|
@@ -21929,7 +21543,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
|
|
|
21929
21543
|
try {
|
|
21930
21544
|
const sessionId = weixinSessionIdFor(fromUserId);
|
|
21931
21545
|
const existing = getSkiffAgent(sessionId);
|
|
21932
|
-
const hc =
|
|
21546
|
+
const hc = readCheapWorkerConfig(root);
|
|
21933
21547
|
if (existing) try {
|
|
21934
21548
|
ensureSkiffSession(root, existing, roleName, role, true);
|
|
21935
21549
|
} catch (err) {
|
|
@@ -22279,7 +21893,7 @@ function syncHumanChannel(ctx) {
|
|
|
22279
21893
|
(async () => {
|
|
22280
21894
|
try {
|
|
22281
21895
|
stopMachineLoops();
|
|
22282
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21896
|
+
const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
|
|
22283
21897
|
for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
|
|
22284
21898
|
} catch {}
|
|
22285
21899
|
})();
|
|
@@ -22447,7 +22061,7 @@ function parseSendRequest(body) {
|
|
|
22447
22061
|
if (!user.includes("@")) return {
|
|
22448
22062
|
ok: false,
|
|
22449
22063
|
error: `invalid "user": ${user}`,
|
|
22450
|
-
remediation: "需为 iLink from_user_id(含
|
|
22064
|
+
remediation: "需为 iLink from_user_id(含 @);别名请在调用方解析"
|
|
22451
22065
|
};
|
|
22452
22066
|
const text = typeof o.text === "string" ? o.text.trim() : "";
|
|
22453
22067
|
if (text === "") return {
|
|
@@ -22515,7 +22129,7 @@ function matchCcc(input, candidates) {
|
|
|
22515
22129
|
}
|
|
22516
22130
|
/** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
22517
22131
|
async function collectCandidates(ctx) {
|
|
22518
|
-
const { listCccs } = await import("./ccc-roots-
|
|
22132
|
+
const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
|
|
22519
22133
|
const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
|
|
22520
22134
|
const seen = /* @__PURE__ */ new Set();
|
|
22521
22135
|
const out = [];
|
|
@@ -23698,14 +23312,19 @@ function registerDeepseekVisionPatch(ctx, enabled) {
|
|
|
23698
23312
|
* - `sendFile` 走 iLink 上传(`weixin-api.sendFileMessage`)+ 同一 outgoing hook(带 `file` 元数据)
|
|
23699
23313
|
*/
|
|
23700
23314
|
/**
|
|
23701
|
-
*
|
|
23702
|
-
*
|
|
23315
|
+
* 解析该 CCC 的别名表(v1.56.10)。
|
|
23316
|
+
*
|
|
23317
|
+
* 🔴 **为什么不再硬编码**(2026-10-14 隐私核查):别名是**使用者自己的称呼**,属 CCC 的数据
|
|
23318
|
+
* (D23:加一个别名不该要发版)。此前硬编码在 ACC 源码 ⇒ **这些称呼随 npm 包公开发布**。
|
|
23319
|
+
* 现在别名**只**来自该 CCC 的 `.opencode/serenity.json` → `weixin.aliases`;
|
|
23320
|
+
* ACC 不认识任何具体名字。未配置 ⇒ `{}`(此时只能用裸 iLink 用户 id)。
|
|
23321
|
+
*
|
|
23322
|
+
* ⚠️ **迁移**:升级前用过别名的部署,需把原表补进自己的 CCC 配置,否则别名解析不到
|
|
23323
|
+
* (见 CHANGELOG v1.56.10 的迁移段;裸 `xxx@im.wechat` 形式始终可用)。
|
|
23703
23324
|
*/
|
|
23704
|
-
|
|
23705
|
-
|
|
23706
|
-
|
|
23707
|
-
xiaowang: "WEIXIN_USER_DANICA"
|
|
23708
|
-
};
|
|
23325
|
+
function weixinAliasMap(root) {
|
|
23326
|
+
return readWeixinSettings(root).aliases ?? {};
|
|
23327
|
+
}
|
|
23709
23328
|
/** 读该 CCC 的一个凭据值(空/缺失 → null) */
|
|
23710
23329
|
function credentialValue(root, key) {
|
|
23711
23330
|
try {
|
|
@@ -23728,7 +23347,7 @@ const weixinChannel = {
|
|
|
23728
23347
|
const raw = input.trim();
|
|
23729
23348
|
if (raw === "") return null;
|
|
23730
23349
|
const alias = raw.toLowerCase();
|
|
23731
|
-
const key =
|
|
23350
|
+
const key = weixinAliasMap(root)[alias];
|
|
23732
23351
|
if (key) {
|
|
23733
23352
|
const id = credentialValue(root, key);
|
|
23734
23353
|
return id ? {
|
|
@@ -23744,7 +23363,7 @@ const weixinChannel = {
|
|
|
23744
23363
|
},
|
|
23745
23364
|
listUsers(root) {
|
|
23746
23365
|
const out = [];
|
|
23747
|
-
for (const [alias, key] of Object.entries(
|
|
23366
|
+
for (const [alias, key] of Object.entries(weixinAliasMap(root))) {
|
|
23748
23367
|
const id = credentialValue(root, key);
|
|
23749
23368
|
if (id) out.push({
|
|
23750
23369
|
alias,
|
|
@@ -23868,7 +23487,7 @@ function renderText(value) {
|
|
|
23868
23487
|
function createImBridgeTool() {
|
|
23869
23488
|
return defineTool({
|
|
23870
23489
|
name: "im-bridge",
|
|
23871
|
-
description: `Send messages to family members through an IM channel configured for THIS container (currently: weixin / 微信). Every successful send is recorded in the container log automatically; this tool only acts on the current container (no target container parameter). It is hidden entirely when this container has no IM channel configured. Channels: ${imChannelIds().join(", ") || "(none registered)"}. Actions: send (text) / send-file (a file inside this container) / users (list configured recipients) / status (channel health). user accepts a configured alias (e.g.
|
|
23490
|
+
description: `Send messages to family members through an IM channel configured for THIS container (currently: weixin / 微信). Every successful send is recorded in the container log automatically; this tool only acts on the current container (no target container parameter). It is hidden entirely when this container has no IM channel configured. Channels: ${imChannelIds().join(", ") || "(none registered)"}. Actions: send (text) / send-file (a file inside this container) / users (list configured recipients) / status (channel health). user accepts a configured alias (e.g. user-a, user-b) or a raw channel user id. Failures return a stable code plus a remediation hint.`,
|
|
23872
23491
|
parameters: {
|
|
23873
23492
|
channel: {
|
|
23874
23493
|
type: "string",
|
|
@@ -23883,7 +23502,7 @@ function createImBridgeTool() {
|
|
|
23883
23502
|
},
|
|
23884
23503
|
user: {
|
|
23885
23504
|
type: "string",
|
|
23886
|
-
description: "Recipient: configured alias (
|
|
23505
|
+
description: "Recipient: configured alias (user-a, user-b) or raw channel user id (send / send-file)"
|
|
23887
23506
|
},
|
|
23888
23507
|
text: {
|
|
23889
23508
|
type: "string",
|
|
@@ -25401,7 +25020,7 @@ function apply(ctx, config) {
|
|
|
25401
25020
|
ctx.tools.register(praxisTool);
|
|
25402
25021
|
ctx.tools.register(createOntologyTool());
|
|
25403
25022
|
ctx.tools.register(createSkillCreatorTool());
|
|
25404
|
-
ctx.tools.register(
|
|
25023
|
+
ctx.tools.register(createCheapWorkerTool(ctx));
|
|
25405
25024
|
ctx.tools.register(localstoreTool);
|
|
25406
25025
|
ctx.tools.register(createContainerAdminTool(ctx));
|
|
25407
25026
|
ctx.tools.register(createImBridgeTool());
|