@shgroup/dsh-serenity-hooks 1.30.5 → 1.30.9

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/lib/kit-ops.d.ts CHANGED
@@ -46,5 +46,5 @@ export interface RegistryHealthReport {
46
46
  issues: string[];
47
47
  }
48
48
  export declare function checkRegistryHealth(root: string): RegistryHealthReport;
49
- export declare function runKit(root: string | null, args: KitArgs): Promise<JsonValue>;
49
+ export declare function runKit(root: string | null, args: KitArgs, hostCtx?: unknown): Promise<JsonValue>;
50
50
  export { loadSerenityConfig };
@@ -54,6 +54,8 @@ export declare function checkLocalstoreGitCompliance(root: string): {
54
54
  ok: boolean;
55
55
  reason?: string;
56
56
  };
57
+ /** 测试辅助:重置"坏 localstore"告警去重(生产零调用) */
58
+ export declare function __resetBrokenStoreWarningsForTest(): void;
57
59
  /** 读取命名空间全部条目:credential → 扁平;config → 分节(剔除 credentials 保留节) */
58
60
  export declare function readStore(root: string, scope: LocalStoreScope): Record<string, string> | Record<string, Record<string, string>>;
59
61
  /** 校验凭据 key 合法(大写蛇形);不合法抛错 */
package/lib/msm-ops.d.ts CHANGED
@@ -19,7 +19,7 @@ export declare const MSM_ACTIONS: readonly MsmAction[];
19
19
  export declare const ACC_CATALOG = "\u2550\u2550\u2550 ACC Usage Catalog \u2550\u2550\u2550\nDirectory of ACC capabilities \u2014 each area lists where to go for details (guides live with their feature; this catalog only points).\n\n \u2460 \u4F1A\u8BDD\u4E0E\u8F68\u8FF9 \u2192 logbook\uFF08list/show/create/use/close/health/qa/archive/summary/hook-develop-guide + rebuild \u8D85\u9650\u91CD\u5EFA\uFF09\n session_rebuild \u5DF2\u5E76\u5165 logbook rebuild\uFF08\u8D85\u9650\u91CD\u5EFA\uFF1B\u9608\u503C K \u89C1 container_admin ccc-config / \u8BBE\u7F6E\u9762\u677F\u91CD\u5EFA\u9608\u503CK\uFF09\n \u2461 \u8BA4\u77E5\u8D28\u91CF\u6846\u67B6 \u2192 praxis\uFF08eap/neat/cce \u4E09\u5408\u4E00\u2014\u2014\u53EF\u5B9E\u8DF5\u7406\u8BBA\u6CE8\u5165\uFF0Csection \u6E10\u8FDB\u62AB\u9732\uFF0C\u65E0\u53C2\u5373\u76EE\u5F55\uFF09\n \u2462 \u5DE5\u5177\u4E0E\u6267\u884C \u2192 container_fs\uFF08\u5BB9\u5668\u6587\u4EF6\u7CFB\u7EDF 15 \u5B50\u547D\u4EE4\uFF09/ container_git\uFF08git\uFF09/ dashboard\uFF08health \u542B\u6CE8\u518C\u8868\u68C0\u67E5/time/wait\uFF09\n acc_msm \u5DF2\u5E76\u5165 msm\uFF08\u6267\u884C+\u53D1\u73B0\uFF09\u4E0E container_admin\uFF08\u7BA1\u7406\uFF09\u2014\u2014msm(\"<name>\", [\"<args>\"])\uFF1Binspect=true \u67E5\u7528\u6CD5\n handyman\uFF08\u6742\u5DE5 agent \u7F16\u6392\u2014\u2014guide \u5B50\u547D\u4EE4\uFF1Ahandyman guide\uFF09\n \u2463 \u89D2\u8272\u4E0E\u5BF9\u5916\u9762 \u2192 container_admin role\uFF08Skiff \u8BA4\u77E5\u5B50\u96C6\u89D2\u8272\uFF1Aguide/validate/apply/list\u2014\u2014skiff_admin \u5DF2\u5E76\u5165\uFF09\n ACP\uFF08\u7A0B\u5E8F\u5316 JSON-RPC 3100\uFF09/ Skiff \u95EE\u7B54\u9875\uFF08\u516C\u7F51 ask\uFF09\u2014\u2014\u9762\u677F\u300C\u5916\u90E8\u80FD\u529B\u300D\u7EC4\n \u2464 \u81EA\u4E3B\u4E0E\u63A5\u5165 \u2192 autopilot-trajectory\uFF08Autopilot \u4E00\u7AD9\u5F0F\uFF1Aall/init/random/diag/doc/check/status/guide\uFF09\n weixin \u5FAE\u4FE1\u6865\uFF08\u914D\u7F6E/\u626B\u7801/\u8DEF\u7531/\u6D88\u606F hook\u2014\u2014CCC \u4FA7 weixin-doctor MSM\uFF1Amsm(\"weixin-doctor\", [\"guide\"])\uFF09\n \u2465 CCC \u914D\u7F6E\u603B\u89C8 \u2192 container_admin msm ccc-config\uFF088 \u6BB5\uFF1Ahandyman/sessionKeeper/localstore/hooks/safeMode/skiff/autopilotTrajectory/weixin\uFF09\n \u2466 \u6CE8\u518C\u8868\u4E0E\u5B89\u5168 \u2192 mech-registry.json \u7531 container_admin msm register/deregister \u7BA1\u7406\uFF08\u5199\u4FDD\u62A4\u2014\u2014\u4E0D\u53EF\u76F4\u63A5\u7F16\u8F91\uFF09\n dashboard health \u8F93\u51FA registry \u5B8C\u6574\u6027\u68C0\u67E5\uFF1B\u635F\u574F\u6062\u590D\u6307\u5F15\u89C1 health registry.issues\n\n\u6BCF\u533A\uFF1A\u4E00\u53E5\u8BDD\u5B9A\u4F4D + \u8BE6\u7EC6\u5165\u53E3\uFF08\u5DE5\u5177\u540D / guide \u5B50\u547D\u4EE4\uFF09\u3002\u8BE6\u60C5\u6C38\u8FDC\u4EE5\u5BF9\u5E94\u5DE5\u5177\u7684 guide/ccc-config \u4E3A\u5355\u4E00\u771F\u76F8\u6E90\u3002\n";
20
20
  export declare const MSM_GUIDE = "MSM Development Manual (Mech & Semi-Mech framework)\n\n## What it is\nMSM = the executable-unit layer. Mech is pure TS with zero LLM reasoning; Semi-Mech is a TS framework + LLM decision points.\nMSM is ACC's deterministic executable-unit layer \u2014 all shell/exec operations go through MSM and cannot be bypassed.\n\n## Registering a new MSM (container_admin msm register)\n1. Write the script under <skill>/scripts/ (runnable via tsx; must include a main() CLI guard with an import.meta.url check)\n2. container_admin msm register --domain msm --action register <name> --skill <s> --path <script path relative to root> --category <mech|semi-mech> --description <desc>\n3. Auto-writes mech-registry.json (preserving the original format) + git commit (commits only the registry file)\n4. Validation: path must be inside root, script must exist, name must be globally unique\n\n## flag schema (v1)\nflags is a new-style object array for parameter validation and path-escape guarding:\n [{\"name\":\"output\",\"type\":\"string\",\"description\":\"output path\"},\n {\"name\":\"target\",\"type\":\"path\",\"description\":\"operation target (type:path enables in-root validation + symlink defense)\"},\n {\"name\":\"force\",\"type\":\"boolean\",\"description\":\"force mode\",\"default\":false}]\n- {name, type} format \u2014 new style, type:\"path\" enables the path-escape guard\n- {flag, description} format \u2014 old style, CLI flag description string\n- On registration, flags are passed via container_admin msm register --flags '<json>'\n\n## Script conventions\n- Top-of-file documentation: purpose / usage / exit codes\n- Exit codes: 0 success / 1 user / 2 system / 3 operator (per ACC protocol classification)\n- main() CLI guard (DC-M2): the script must start with\n if (import.meta.url === `file://${process.argv[1]}`) { main() }\n or an equivalent isMain / require.main === check \u2014 so vitest imports never trigger top-level code\n- A paired .test.ts or .spec.ts (DC-M1, vitest)\n- Business subprocess environment: SERENITY_ROOT / SERENITY_CCC / SERENITY_VERSION injected\n\n## Interaction & confirmation conventions (no blocking confirmation)\n- MSMs run in **user-less subprocesses** (spawn/execFile, 600s timeout) \u2014 **never** use\n readline / prompt / process.stdin etc. to block waiting for user input (it would hang until killed by the timeout)\n- When a second confirmation is needed: **directly return the confirmation request** (list the operation to perform and its impact + how to retry with the confirmation flag),\n returning a non-zero exit code (or an explicit hint)\n- After the agent confirms, **re-invoke the MSM with the confirmation flag** (e.g. --confirm / --yes / --force) to retry\n- Standard mode (two-phase):\n 1. First call: destructive/confirmation-requiring operation detected and no confirmation flag present \u2192 output the confirmation request (list operation/impact/rollback),\n exit non-zero (e.g. 1 user), **perform no changes**\n 2. After the agent evaluates, re-invoke: with the confirmation flag \u2192 execute and output the result\n- Applies to: delete/overwrite/push/batch operations and other irreversible or wide-impact operations\n- Anti-example (forbidden): readline waiting for user input, process.stdin.on('data') blocking waits\n (MSMs have no stdin interaction channel \u2014 they hang until the 600s timeout)\n\n## Quality checks (container_admin msm check, DC-M1~M4)\nDC-M1 has .test.ts/.spec.ts; DC-M2 has a main() guard (function main( / isMain / require.main === / import.meta.url);\nDC-M3 bidirectional: scripts unregistered + registry references missing scripts; DC-M4 path-type flags marked type:\"path\"\n\n## Self-description (protocol flags, first argument only)\nmsm(\"<name>\", [\"--list\"]) \u2014 list all MSMs\nmsm(\"<name>\", [\"--schema\", \"<n>\"]) \u2014 view a specific MSM's parameter schema\nmsm(\"<name>\", [\"--format=json\"]) \u2014 JSON output mode (remaining args passed through losslessly)\n";
21
21
  /** CCC 配置参考(对齐 osp ccc-config action) */
22
- export declare const CCC_CONFIG_REFERENCE = "\u2550\u2550\u2550 CCC Configuration Reference \u2550\u2550\u2550\n\nCCC-level features are configured in .opencode/serenity.json.\nBelow are all available configuration sections.\n\n\u2500\u2500 1. handyman.models \u2500\u2500\nhandyman\uFF08\u6742\u5DE5\uFF09\u5DE5\u5177\u53EF\u7528\u6A21\u578B\u767D\u540D\u5355\uFF08provider/model \u5217\u8868\uFF09\uFF1B\u672A\u914D\u7F6E\u65F6 handyman \u62A5\u9519\u8981\u6C42\u914D\u7F6E\u3002\n\u7F3A\u7701\u6A21\u578B = models[0]\uFF08\u53EF\u7528 handyman.defaultModel \u6307\u5B9A\uFF0C\u5FC5\u987B \u2208 models\uFF09\u3002\n\n Config:\n { \"handyman\": { \"models\": [\"provider/model-name\"] } }\n\n Example:\n { \"handyman\": { \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\", \"maxRounds\": 100, \"maxParallel\": 10 } }\n\n\u2500\u2500 2. sessionKeeper.threshold \u2500\u2500\ntrajectory-assistant\uFF08TRAJECTORY-ASSISTANT \u00B7 CHECKPOINT\uFF09\u63D0\u9192\u673A\u5236\u7684\u79EF\u5206\u9608\u503C\uFF08\u975E headless \u4E3B agent\uFF09\u3002\n\u6309\u5DE5\u5177\u8C03\u7528\u52A0\u6743 + \u8017\u65F6\u8BA1\u5206\uFF1B\u8FBE\u5230\u9608\u503C\u6CE8\u5165\u63D0\u9192\uFF0C\u8981\u6C42\u6A21\u578B\u56DE\u590D ACK \u7801\u3002\n\n Config:\n { \"sessionKeeper\": { \"threshold\": 150 } }\n\n \u8BA1\u5206\uFF1Awrite/edit = 3\uFF0Ctask = 10\uFF0Cread/grep/glob/msm \u7B49 = 1\uFF0C\u65F6\u95F4 = 1/\u5206\u949F\n \u9ED8\u8BA4\uFF1A150\n\n\u2500\u2500 3. localstore.gitTrack \u2500\u2500\nlocalstore.json \u7684 git \u7B56\u7565\uFF1Aallow \u53EF\u63D0\u4EA4 / deny \u7981\u63D0\u4EA4\uFF08\u9ED8\u8BA4 deny\uFF09\u3002\ndeny \u65F6\u5199\u5165\u81EA\u52A8\u786E\u4FDD .gitignore \u542B\u8BE5\u6587\u4EF6\uFF08\u7269\u7406\u4FDD\u8BC1\uFF09\uFF0Ccontainer_git commit \u4F1A\u68C0\u67E5\u62D2\u7EDD\u3002\n\n Config:\n { \"localstore\": { \"gitTrack\": \"allow\" } }\n\n\u2500\u2500 4. hooks.autoRestoreSession \u2500\u2500\n\u4F1A\u8BDD\u81EA\u52A8\u6062\u590D\uFF08\u9ED8\u8BA4 true\uFF1B\u53D7 events \u95E8\u63A7\u2014\u2014\u4EC5\u6839\u4F1A\u8BDD + \u5DF2\u6709\u5BF9\u8BDD\u5386\u53F2\u624D\u6062\u590D\uFF09\u3002\n\n Config:\n { \"hooks\": { \"autoRestoreSession\": false } }\n\n\u2500\u2500 5. safeMode / blacklist \u2500\u2500\nsafe-mode \u7531 WebUI \u5F00\u5173\u63A7\u5236\uFF08\u5199 .serenity-safe-on \u6807\u8BB0\uFF09\uFF1B\u9ED1\u540D\u5355\u8DEF\u5F84\u53D7 guards seam \u62E6\u622A\u3002\n\n Config:\n { \"safeMode\": { \"blacklist\": [\".secrets/\"] } }\n\n\u2500\u2500 6. skiff.roles\uFF08F4 \u8BA4\u77E5\u5B50\u96C6\u89D2\u8272\uFF09\u2500\u2500\nSkiff \u89D2\u8272 = \u5168\u77E5\u5168\u80FD trajectory \u7684\u4EFB\u610F\u5B50\u96C6\uFF08CCC \u5B9A\u4E49\uFF09\u3002\u6BCF\u89D2\u8272\u72EC\u7ACB\u6A21\u578B + \u53CC\u767D\u540D\u5355\n\uFF08MSM \u767D\u540D\u5355 msms[] \u4E0E\u975E MSM \u5DE5\u5177\u767D\u540D\u5355 tools[]\uFF0C\u767D\u540D\u5355\u5916\u5168\u9690\u85CF\uFF1Bskill \u52A0\u8F7D\u6052\u53EF\u7528\uFF09\u3002\ntrajectory \u7EAA\u5F8B\u5B50\u96C6\uFF08session/keeper/rebuild \u53C2\u4E0E\u9879\uFF09\u9ED8\u8BA4\u5168\u5173 = \u5B8C\u5168\u72EC\u7ACB\u3002\nsystemPrompt \u5185\u8054 \u6216 systemPromptFile\uFF08.md \u5F15\u7528\uFF0C\u63A8\u8350\uFF09\u2014\u2014\u89D2\u8272\u4F1A\u8BDD\u7684\u5B8C\u6574\u4EBA\u683C/\u8FB9\u754C\u63D0\u793A\u8BCD\u3002\nvalidate \u6821\u9A8C / apply \u751F\u6548 / list \u67E5\u770B\uFF1Acontainer_admin role <guide|validate|apply|list>\uFF08skiff_admin \u5DF2\u5E76\u5165\uFF09\u3002\n\n Config:\n { \"skiff\": { \"roles\": {\n \"qa\": {\n \"model\": \"provider/model\",\n \"msms\": [\"web-search\", \"vlm-describe\"],\n \"tools\": [\"read\", \"grep\", \"glob\"],\n \"systemPromptFile\": \".opencode/skiff/qa.md\"\n } } } }\n\n\u2500\u2500 7. autopilotTrajectory\uFF08\u81EA\u52A8\u5DE1\u822A\u8F68\u8FF9\uFF09\u2500\u2500\nCCC \u5B9A\u4E49\u7684\u4E00\u6761\u81EA\u4E3B trajectory\u2014\u2014\u65F6\u949F\u5230\u70B9\u81EA\u52A8\u5524\u8D77\uFF08\u524D\u53F0\u6CE8\u5165\uFF0C\u7528\u6237\u53EF\u89C1\u53EF\u4ECB\u5165\uFF09\u3002\n\u672A\u914D\u7F6E\u6216 enabled=false \u2192 \u5B8C\u5168\u4E0D\u542F\u52A8\uFF08\u96F6\u8D44\u6E90\u5360\u7528\uFF09\u3002\u591A CCC \u72EC\u7ACB\uFF1A\u6BCF CCC \u81EA\u5DF1\u7684\u914D\u7F6E\u3002\n\u5524\u8D77\u6D88\u606F\u56DB\u6BB5\u5F0F\uFF1A\u8F68\u8FF9\u7126\u70B9 topPrompt\uFF08\u6700\u5148\u6CE8\u5165\uFF0C\u7A33\u5B9A\u951A\uFF09\u2192 \u8EAB\u4EFD\u951A\u5B9A \u2192 \u5148\u9A8C\u504F\u89C1\n\uFF08CCC \u6839\u811A\u672C biasProvider \u8F93\u51FA\uFF09\u2192 \u4EFB\u52A1\u3002\u76EE\u6807\u4F1A\u8BDD session \u5FC5\u586B\uFF08\u76EE\u5F55\u987B\u5E26 --auto \u540E\u7F00\uFF09\u3002\n\u8BCA\u65AD/\u72B6\u6001/\u7ACB\u5373\u5524\u8D77\uFF1Aautopilot-trajectory <all|check|status|diag>\uFF08msm(\"autopilot-trajectory\", [\"all\"])\uFF09\u3002\n\n Config:\n { \"autopilotTrajectory\": {\n \"enabled\": true,\n \"intervalHours\": 2,\n \"session\": \"S151\",\n \"biasProvider\": \"autopilot-bias.ts\",\n \"topPrompt\": \"\u672C\u8F68\u8FF9\u6838\u5FC3\u76EE\u6807/\u7EAA\u5F8B/\u8D28\u91CF\u8981\u6C42\uFF08CCC \u81EA\u586B\uFF0C\u9632\u6F02\u79FB\uFF09\",\n \"avoidWakeHours\": { \"start\": 8, \"end\": 18 }\n } }\n\n\u2500\u2500 8. weixin\uFF08\u5FAE\u4FE1\u6865 F4c-3\uFF0C\u542B\u6D88\u606F\u8BB0\u5F55 hook\uFF09\u2500\u2500\nCCC \u7EA7\u5FAE\u4FE1\u4E2A\u4EBA\u53F7\u63A5\u5165\uFF08iLink \u534F\u8BAE\uFF09\uFF1Adsh \u4E00\u8FDB\u7A0B\u591A CCC\uFF0C\u6BCF CCC \u72EC\u7ACB\u5BF9\u63A5\u5FAE\u4FE1\u6865\u3002\n\u8D26\u53F7/\u8DEF\u7531/\u5F00\u5173\u5728\u6B64\u6587\u4EF6\uFF1B**bot_token \u51ED\u636E\u5728 CCC localstore credential scope**\n\uFF08\u626B\u7801\u7ED1\u5B9A\u540E\u81EA\u52A8\u5199\u5165\uFF0C\u6C38\u4E0D\u8FDB git \u660E\u6587\u9762\uFF09\u3002\n\u8DEF\u7531 user \u2192 role\uFF1Aexact \u4F18\u5148\uFF0C* \u901A\u914D\u515C\u5E95\uFF1Brole \u5FC5\u987B \u2208 \u8BE5 CCC skiff.roles\u3002\n\u9762\u677F\uFF08WebUI \u8BBE\u7F6E \u2192 \u5FAE\u4FE1\u6865\uFF09\u53EF\u626B\u7801\u7ED1\u5B9A/\u79FB\u9664\u8D26\u53F7/\u7F16\u8F91\u8DEF\u7531\uFF1Bmsm(\"weixin-doctor\", [\"status\"|\"diag\"|\"verify\"]) \u6392\u67E5\u3002\u51ED\u636E\u4E3B\u52A8\u67E5\u770B\uFF1Alocalstore get WEIXIN_<ACCOUNT>_TOKEN\u3002\n\n Config:\n { \"weixin\": {\n \"enabled\": true,\n \"hook\": \"scripts/weixin-message-hook.ts\",\n \"accounts\": [{ \"accountId\": \"wechat-1\", \"name\": \"\u5BB6\u5EAD\u52A9\u624B\", \"enabled\": true }],\n \"routes\": [{ \"user\": \"*\", \"role\": \"zhaocai\" }]\n } }\n\n \u25B8 weixin.hook\uFF08\u6D88\u606F\u8BB0\u5F55 hook\uFF0Cv1.27.13\uFF09\uFF1A\n \u5FAE\u4FE1\u6865\u6BCF\u6536/\u53D1\u4E00\u6761\u6D88\u606F\u89E6\u53D1\u4E00\u6B21 CCC \u81EA\u5199\u811A\u672C\uFF0C\u7531 CCC \u81EA\u884C\u6301\u4E45\u5316\u4FDD\u5B58\uFF08\u5B58\u54EA/\u5B58\u6210\u4EC0\u4E48\n /\u662F\u5426\u5165\u5E93\u5168\u5F52 CCC\u2014\u2014ACC \u4E0D\u7ED1\u5B9A\u5B58\u50A8\uFF09\u3002\u672A\u914D\u7F6E hook \u2192 \u96F6\u53D8\u5316\uFF08\u4E0D\u89E6\u53D1\uFF09\u3002\n\n \u89E6\u53D1\uFF08\u53CC\u5411\uFF09\uFF1A\n incoming = \u7528\u6237 \u2192 bot\uFF1A\u8DEF\u7531\u547D\u4E2D\u540E\u3001\u5A92\u4F53\u843D\u76D8\u540E\u89E6\u53D1\uFF08\u6587\u672C\u542B\u8BED\u97F3\u8F6C\u5199\uFF1B\u5A92\u4F53\u5E26\u843D\u76D8 relPath\uFF09\n outgoing = bot \u2192 \u7528\u6237\uFF1A\u56DE\u590D\u53D1\u9001\u6210\u529F\u540E\u89E6\u53D1\uFF08reply = \u7528\u6237\u5B9E\u9645\u6536\u5230\u7684\u7EAF\u6587\u672C\uFF0C\u5DF2\u5265\u79BB think\uFF09\n\n \u811A\u672C\u7EA6\u5B9A\uFF08\u4E8B\u4EF6 JSON \u5355\u884C\u7ECF stdin \u4F20\u5165\uFF1Bbun \u4F18\u5148 node \u515C\u5E95\uFF09\uFF1A\n #!/usr/bin/env bun \u6216 node script.js \u2014\u2014 \u8BFB process.stdin \u6574\u6BB5 JSON.parse\n const ev = JSON.parse(await new Response(process.stdin).text())\n ev.event === 'incoming' | 'outgoing'\n\n \u4E8B\u4EF6 schema\uFF08**\u4E0D\u542B\u4EFB\u4F55\u4F1A\u8BDD\u51ED\u636E**\u2014\u2014context_token/bot_token/token/aes_key \u5747\u4E0D\u51FA\u73B0\uFF09\uFF1A\n { \"event\": \"incoming\", \"ts\": 1788359941488, \"cccRoot\": \"/path/ccc\",\n \"accountId\": \"wechat-1\", \"userId\": \"u1@im.wechat\",\n \"sessionId\": \"skiff-weixin-xxx\", \"role\": \"zhaocai\",\n \"message\": { \"text\": \"\u4F60\u597D\", \"media\": [{ \"kind\": \"image\", \"relPath\": \"_tmp/weixin-inbound/<hash>/img_x.jpg\" }] } }\n { \"event\": \"outgoing\", \"ts\": ..., \"cccRoot\": ..., \"accountId\": ...,\n \"userId\": ..., \"sessionId\": ..., \"role\": ...,\n \"reply\": \"\u5DF2\u8BB0\u5F55\uFF08\u7EAF\u6587\u672C\uFF09\" }\n\n \u793A\u4F8B\u811A\u672C\uFF08\u8FFD\u52A0\u5230\u6309\u65E5\u6587\u4EF6\u2014\u2014\u6301\u4E45\u5316\u5F52 CCC \u81EA\u9009\uFF1A\u6587\u4EF6/DB/\u8FDC\u7AEF\u5747\u53EF\uFF09\uFF1A\n const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';\n fs.appendFileSync(p, JSON.stringify(ev) + '\\n');\n\n \u6267\u884C\u8BED\u4E49\uFF08\u65C1\u8DEF\u5BB9\u5FCD\uFF09\uFF1A\u5F02\u6B65 fire-and-forget + 15s \u8D85\u65F6 kill + \u5931\u8D25\u4EC5\u65E5\u5FD7\u2014\u2014\n \u5FAE\u4FE1\u6865\u6D88\u606F\u5904\u7406/\u56DE\u590D\u4E0D\u53D7 hook \u5F71\u54CD\uFF1B\u811A\u672C\u987B\u5728 CCC \u6839\u5185\uFF08\u8DEF\u5F84\u9003\u9038\u62D2\u7EDD\uFF09\u3002\n \u5A92\u4F53 relPath \u6307\u5411 _tmp/weixin-inbound/\uFF08\u4E34\u65F6\u76EE\u5F55\uFF09\u2014\u2014\u9700\u6301\u4E45\u4FDD\u5B58\u5A92\u4F53\u6587\u4EF6\u8BF7\u81EA\u884C copy\u3002\n";
22
+ export declare const CCC_CONFIG_REFERENCE = "\u2550\u2550\u2550 CCC Configuration Reference \u2550\u2550\u2550\n\nCCC-level features are configured in .opencode/serenity.json.\nBelow are all available configuration sections.\n\n\u2500\u2500 1. handyman.models \u2500\u2500\nhandyman\uFF08\u6742\u5DE5\uFF09\u5DE5\u5177\u53EF\u7528\u6A21\u578B\u767D\u540D\u5355\uFF08provider/model \u5217\u8868\uFF09\uFF1B\u672A\u914D\u7F6E\u65F6 handyman \u62A5\u9519\u8981\u6C42\u914D\u7F6E\u3002\n\u7F3A\u7701\u6A21\u578B = models[0]\uFF08\u53EF\u7528 handyman.defaultModel \u6307\u5B9A\uFF0C\u5FC5\u987B \u2208 models\uFF09\u3002\n\n Config:\n { \"handyman\": { \"models\": [\"provider/model-name\"] } }\n\n Example:\n { \"handyman\": { \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\", \"maxRounds\": 100, \"maxParallel\": 10 } }\n\n\u2500\u2500 2. sessionKeeper.threshold \u2500\u2500\ntrajectory-assistant\uFF08TRAJECTORY-ASSISTANT \u00B7 CHECKPOINT\uFF09\u63D0\u9192\u673A\u5236\u7684\u79EF\u5206\u9608\u503C\uFF08\u975E headless \u4E3B agent\uFF09\u3002\n\u6309\u5DE5\u5177\u8C03\u7528\u52A0\u6743 + \u8017\u65F6\u8BA1\u5206\uFF1B\u8FBE\u5230\u9608\u503C\u6CE8\u5165\u63D0\u9192\uFF0C\u8981\u6C42\u6A21\u578B\u56DE\u590D ACK \u7801\u3002\n\n Config:\n { \"sessionKeeper\": { \"threshold\": 150 } }\n\n \u8BA1\u5206\uFF1Awrite/edit = 3\uFF0Ctask = 10\uFF0Cread/grep/glob/msm \u7B49 = 1\uFF0C\u65F6\u95F4 = 1/\u5206\u949F\n \u9ED8\u8BA4\uFF1A150\n\n\u2500\u2500 3. localstore.gitTrack \u2500\u2500\nlocalstore.json \u7684 git \u7B56\u7565\uFF1Aallow \u53EF\u63D0\u4EA4 / deny \u7981\u63D0\u4EA4\uFF08\u9ED8\u8BA4 deny\uFF09\u3002\ndeny \u65F6\u5199\u5165\u81EA\u52A8\u786E\u4FDD .gitignore \u542B\u8BE5\u6587\u4EF6\uFF08\u7269\u7406\u4FDD\u8BC1\uFF09\uFF0Ccontainer_git commit \u4F1A\u68C0\u67E5\u62D2\u7EDD\u3002\n\n Config:\n { \"localstore\": { \"gitTrack\": \"allow\" } }\n\n\u2500\u2500 4. hooks.autoRestoreSession \u2500\u2500\n\u4F1A\u8BDD\u81EA\u52A8\u6062\u590D\uFF08\u9ED8\u8BA4 true\uFF1B\u53D7 events \u95E8\u63A7\u2014\u2014\u4EC5\u6839\u4F1A\u8BDD + \u5DF2\u6709\u5BF9\u8BDD\u5386\u53F2\u624D\u6062\u590D\uFF09\u3002\n\n Config:\n { \"hooks\": { \"autoRestoreSession\": false } }\n\n\u2500\u2500 5. safeMode / blacklist \u2500\u2500\nsafe-mode \u7531 WebUI \u5F00\u5173\u63A7\u5236\uFF08\u5199 .serenity-safe-on \u6807\u8BB0\uFF09\uFF1B\u9ED1\u540D\u5355\u8DEF\u5F84\u53D7 guards seam \u62E6\u622A\u3002\n\n Config:\n { \"safeMode\": { \"blacklist\": [\".secrets/\"] } }\n\n\u2500\u2500 6. skiff.roles\uFF08F4 \u8BA4\u77E5\u5B50\u96C6\u89D2\u8272\uFF09\u2500\u2500\nSkiff \u89D2\u8272 = \u5168\u77E5\u5168\u80FD trajectory \u7684\u4EFB\u610F\u5B50\u96C6\uFF08CCC \u5B9A\u4E49\uFF09\u3002\u6BCF\u89D2\u8272\u72EC\u7ACB\u6A21\u578B + \u53CC\u767D\u540D\u5355\n\uFF08MSM \u767D\u540D\u5355 msms[] \u4E0E\u975E MSM \u5DE5\u5177\u767D\u540D\u5355 tools[]\uFF0C\u767D\u540D\u5355\u5916\u5168\u9690\u85CF\uFF1Bskill \u52A0\u8F7D\u6052\u53EF\u7528\uFF09\u3002\ntrajectory \u7EAA\u5F8B\u5B50\u96C6\uFF08session/keeper/rebuild \u53C2\u4E0E\u9879\uFF09\u9ED8\u8BA4\u5168\u5173 = \u5B8C\u5168\u72EC\u7ACB\u3002\nsystemPrompt \u5185\u8054 \u6216 systemPromptFile\uFF08.md \u5F15\u7528\uFF0C\u63A8\u8350\uFF09\u2014\u2014\u89D2\u8272\u4F1A\u8BDD\u7684\u5B8C\u6574\u4EBA\u683C/\u8FB9\u754C\u63D0\u793A\u8BCD\u3002\nvalidate \u6821\u9A8C / apply \u751F\u6548 / list \u67E5\u770B\uFF1Acontainer_admin role <guide|validate|apply|list>\uFF08skiff_admin \u5DF2\u5E76\u5165\uFF09\u3002\n\n Config:\n { \"skiff\": { \"roles\": {\n \"qa\": {\n \"model\": \"provider/model\",\n \"msms\": [\"web-search\", \"vlm-describe\"],\n \"tools\": [\"read\", \"grep\", \"glob\"],\n \"systemPromptFile\": \".opencode/skiff/qa.md\"\n } } } }\n\n\u2500\u2500 7. autopilotTrajectory\uFF08\u81EA\u52A8\u5DE1\u822A\u8F68\u8FF9\uFF09\u2500\u2500\nCCC \u5B9A\u4E49\u7684\u4E00\u6761\u81EA\u4E3B trajectory\u2014\u2014\u65F6\u949F\u5230\u70B9\u81EA\u52A8\u5524\u8D77\uFF08\u524D\u53F0\u6CE8\u5165\uFF0C\u7528\u6237\u53EF\u89C1\u53EF\u4ECB\u5165\uFF09\u3002\n\u672A\u914D\u7F6E\u6216 enabled=false \u2192 \u5B8C\u5168\u4E0D\u542F\u52A8\uFF08\u96F6\u8D44\u6E90\u5360\u7528\uFF09\u3002\u591A CCC \u72EC\u7ACB\uFF1A\u6BCF CCC \u81EA\u5DF1\u7684\u914D\u7F6E\u3002\n\u5524\u8D77\u6D88\u606F\u56DB\u6BB5\u5F0F\uFF1A\u8F68\u8FF9\u7126\u70B9 topPrompt\uFF08\u6700\u5148\u6CE8\u5165\uFF0C\u7A33\u5B9A\u951A\uFF09\u2192 \u8EAB\u4EFD\u951A\u5B9A \u2192 \u5148\u9A8C\u504F\u89C1\n\uFF08CCC \u6839\u811A\u672C biasProvider \u8F93\u51FA\uFF09\u2192 \u4EFB\u52A1\u3002\u76EE\u6807\u4F1A\u8BDD session \u5FC5\u586B\uFF08\u76EE\u5F55\u987B\u5E26 --auto \u540E\u7F00\uFF09\u3002\n\u8BCA\u65AD/\u72B6\u6001/\u7ACB\u5373\u5524\u8D77\uFF1Aautopilot-trajectory <all|check|status|diag>\uFF08msm(\"autopilot-trajectory\", [\"all\"])\uFF09\u3002\n\n Config:\n { \"autopilotTrajectory\": {\n \"enabled\": true,\n \"intervalHours\": 2,\n \"session\": \"S151\",\n \"biasProvider\": \"autopilot-bias.ts\",\n \"topPrompt\": \"\u672C\u8F68\u8FF9\u6838\u5FC3\u76EE\u6807/\u7EAA\u5F8B/\u8D28\u91CF\u8981\u6C42\uFF08CCC \u81EA\u586B\uFF0C\u9632\u6F02\u79FB\uFF09\",\n \"avoidWakeHours\": { \"start\": 8, \"end\": 18 }\n } }\n\n\u2500\u2500 8. weixin\uFF08\u5FAE\u4FE1\u6865 F4c-3\uFF0C\u542B\u6D88\u606F\u8BB0\u5F55 hook\uFF09\u2500\u2500\nCCC \u7EA7\u5FAE\u4FE1\u4E2A\u4EBA\u53F7\u63A5\u5165\uFF08iLink \u534F\u8BAE\uFF09\uFF1Adsh \u4E00\u8FDB\u7A0B\u591A CCC\uFF0C\u6BCF CCC \u72EC\u7ACB\u5BF9\u63A5\u5FAE\u4FE1\u6865\u3002\n\u8D26\u53F7/\u8DEF\u7531/\u5F00\u5173\u5728\u6B64\u6587\u4EF6\uFF1B**bot_token \u51ED\u636E\u5728 CCC localstore credential scope**\n\uFF08\u626B\u7801\u7ED1\u5B9A\u540E\u81EA\u52A8\u5199\u5165\uFF0C\u6C38\u4E0D\u8FDB git \u660E\u6587\u9762\uFF09\u3002\n\u8DEF\u7531 user \u2192 role\uFF1Aexact \u4F18\u5148\uFF0C* \u901A\u914D\u515C\u5E95\uFF1Brole \u5FC5\u987B \u2208 \u8BE5 CCC skiff.roles\u3002\n\u9762\u677F\uFF08WebUI \u8BBE\u7F6E \u2192 \u5FAE\u4FE1\u6865\uFF09\u53EF\u626B\u7801\u7ED1\u5B9A/\u79FB\u9664\u8D26\u53F7/\u7F16\u8F91\u8DEF\u7531\uFF1Bmsm(\"weixin-doctor\", [\"status\"|\"diag\"|\"verify\"|\"guide\"]) \u6392\u67E5 + \u5927\u800C\u5168\u6307\u5357\u3002\u51ED\u636E\u4E3B\u52A8\u67E5\u770B\uFF1Alocalstore get WEIXIN_<ACCOUNT>_TOKEN\u3002\n \u25B8 \u4E3B\u52A8\u53D1\u9001\uFF08v1.30.9\uFF09\uFF1ACCC \u7528 msm(\"weixin-send\", [\"send\", \"--ccc\", \"<CCC>\", \"--user\", \"<\u522B\u540D|id>\", \"<\u6587\u672C>\"])\n \u7ED9\u7528\u6237\u53D1\u6D88\u606F\u2014\u2014\u7ECF dsp \u4E3B\u52A8\u53D1\u9001\u5165\u53E3\uFF08**\u53EA\u7ED1 127.0.0.1**\uFF0Cplugin \u5168\u5C40\u914D\u7F6E ~/.dsh/serenity-hooks.json\n \u7684 weixinApi.port\uFF0C\u9ED8\u8BA4 3082\uFF1B\u516C\u7F51\u4E0D\u53EF\u8FBE\u6545\u65E0\u5BC6\u94A5\uFF09\u2192 weixin-bridge.sendProactiveText \u2192\n \u6210\u529F\u540E\u89E6\u53D1 outgoing hook\uFF08source=\"proactive\"\uFF09\u3002send-file \u4ECD\u7531 MSM \u76F4\u8FDE iLink \u4E0A\u4F20\u5E76\u81EA\u884C\u8865\u8BB0\u3002\n\n Config:\n { \"weixin\": {\n \"enabled\": true,\n \"hook\": \"scripts/weixin-message-hook.ts\",\n \"accounts\": [{ \"accountId\": \"wechat-1\", \"name\": \"\u5BB6\u5EAD\u52A9\u624B\", \"enabled\": true }],\n \"routes\": [{ \"user\": \"*\", \"role\": \"zhaocai\" }]\n } }\n\n \u25B8 weixin.hook\uFF08\u6D88\u606F\u8BB0\u5F55 hook\uFF0Cv1.27.13\uFF09\uFF1A\n \u5FAE\u4FE1\u6865\u6BCF\u6536/\u53D1\u4E00\u6761\u6D88\u606F\u89E6\u53D1\u4E00\u6B21 CCC \u81EA\u5199\u811A\u672C\uFF0C\u7531 CCC \u81EA\u884C\u6301\u4E45\u5316\u4FDD\u5B58\uFF08\u5B58\u54EA/\u5B58\u6210\u4EC0\u4E48\n /\u662F\u5426\u5165\u5E93\u5168\u5F52 CCC\u2014\u2014ACC \u4E0D\u7ED1\u5B9A\u5B58\u50A8\uFF09\u3002\u672A\u914D\u7F6E hook \u2192 \u96F6\u53D8\u5316\uFF08\u4E0D\u89E6\u53D1\uFF09\u3002\n\n \u89E6\u53D1\uFF08\u53CC\u5411\uFF09\uFF1A\n incoming = \u7528\u6237 \u2192 bot\uFF1A\u8DEF\u7531\u547D\u4E2D\u540E\u3001\u5A92\u4F53\u843D\u76D8\u540E\u89E6\u53D1\uFF08\u6587\u672C\u542B\u8BED\u97F3\u8F6C\u5199\uFF1B\u5A92\u4F53\u5E26\u843D\u76D8 relPath\uFF09\n outgoing = bot \u2192 \u7528\u6237\uFF1A\u53D1\u9001\u6210\u529F\u540E\u89E6\u53D1\uFF08reply = \u7528\u6237\u5B9E\u9645\u6536\u5230\u7684\u7EAF\u6587\u672C\uFF0C\u5DF2\u5265\u79BB think\uFF09\n \u00B7 source=\"reply\"\uFF08\u7F3A\u7701\uFF09= \u56DE\u590D\u7528\u6237\u6D88\u606F\uFF1Bsource=\"proactive\"\uFF08v1.30.9\uFF09= bot \u4E3B\u52A8\u53D1\u8D77\n \uFF08CCC \u7ECF weixin-send MSM \u2192 dsp \u4E3B\u52A8\u53D1\u9001\u5165\u53E3 \u2192 sendProactiveText\uFF09\n \u00B7 file\uFF08\u4EC5 send-file \u8865\u8BB0\uFF09\uFF1A{ \"name\": \"\u62A5\u544A.pdf\", \"size\": 12345, \"caption\": \"\u53EF\u9009\" }\n\n \u811A\u672C\u7EA6\u5B9A\uFF08\u4E8B\u4EF6 JSON \u5355\u884C\u7ECF stdin \u4F20\u5165\uFF1Bbun \u4F18\u5148 node \u515C\u5E95\uFF09\uFF1A\n #!/usr/bin/env bun \u6216 node script.js \u2014\u2014 \u8BFB process.stdin \u6574\u6BB5 JSON.parse\n const ev = JSON.parse(await new Response(process.stdin).text())\n ev.event === 'incoming' | 'outgoing'\n\n \u4E8B\u4EF6 schema\uFF08**\u4E0D\u542B\u4EFB\u4F55\u4F1A\u8BDD\u51ED\u636E**\u2014\u2014context_token/bot_token/token/aes_key \u5747\u4E0D\u51FA\u73B0\uFF09\uFF1A\n { \"event\": \"incoming\", \"ts\": 1788359941488, \"cccRoot\": \"/path/ccc\",\n \"accountId\": \"wechat-1\", \"userId\": \"u1@im.wechat\",\n \"sessionId\": \"skiff-weixin-xxx\", \"role\": \"zhaocai\",\n \"message\": { \"text\": \"\u4F60\u597D\", \"media\": [{ \"kind\": \"image\", \"relPath\": \"_tmp/weixin-inbound/<hash>/img_x.jpg\" }] } }\n { \"event\": \"outgoing\", \"ts\": ..., \"cccRoot\": ..., \"accountId\": ...,\n \"userId\": ..., \"sessionId\": ..., \"role\": ...,\n \"reply\": \"\u5DF2\u8BB0\u5F55\uFF08\u7EAF\u6587\u672C\uFF09\", \"source\": \"reply\" | \"proactive\" }\n\n \u793A\u4F8B\u811A\u672C\uFF08\u8FFD\u52A0\u5230\u6309\u65E5\u6587\u4EF6\u2014\u2014\u6301\u4E45\u5316\u5F52 CCC \u81EA\u9009\uFF1A\u6587\u4EF6/DB/\u8FDC\u7AEF\u5747\u53EF\uFF09\uFF1A\n const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';\n fs.appendFileSync(p, JSON.stringify(ev) + '\\n');\n\n \u6267\u884C\u8BED\u4E49\uFF08\u65C1\u8DEF\u5BB9\u5FCD\uFF09\uFF1A\u5F02\u6B65 fire-and-forget + 15s \u8D85\u65F6 kill + \u5931\u8D25\u4EC5\u65E5\u5FD7\u2014\u2014\n \u5FAE\u4FE1\u6865\u6D88\u606F\u5904\u7406/\u56DE\u590D\u4E0D\u53D7 hook \u5F71\u54CD\uFF1B\u811A\u672C\u987B\u5728 CCC \u6839\u5185\uFF08\u8DEF\u5F84\u9003\u9038\u62D2\u7EDD\uFF09\u3002\n \u5A92\u4F53 relPath \u6307\u5411 _tmp/weixin-inbound/\uFF08\u4E34\u65F6\u76EE\u5F55\uFF09\u2014\u2014\u9700\u6301\u4E45\u4FDD\u5B58\u5A92\u4F53\u6587\u4EF6\u8BF7\u81EA\u884C copy\u3002\n";
23
23
  export interface MsmFlag {
24
24
  name: string;
25
25
  type?: string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * lifecycle.ts — 宿主生命周期订阅 + 资源拆卸(S142 review F-08)
3
+ *
4
+ * 两个此前缺失的收尾面:
5
+ *
6
+ * ① **会话/agent 销毁 → 清理 per-会话内存态**。dsp 有约 10 处 per-会话 Map
7
+ * (活跃 SESSION 作用域、skiff 注册表、keeper 计分、guard 白名单缓存、bootstrap
8
+ * 晋升状态、输出守卫状态…)。此前无人订阅 `agent/disposed` / `session/disposed`
9
+ * (宿主 0.1.2-rc.1 真实存在:`core/agent/src/runtime-types.ts:175`、
10
+ * `core/session/src/index.ts:62`),长跑进程里这些 Map 只增不减——且 `waitIdle`
11
+ * 在 agent 先被销毁时永不结算。
12
+ *
13
+ * ② **插件卸载/HMR/profile 重载 → 停掉自起的监听器与轮询器**。skiff 调试页、ACP
14
+ * HTTP 端点、微信桥轮询都是 dsp 自己起的资源;此前没有 `ctx.effect` 拆卸,
15
+ * 卸载后端口仍被占用(EADDRINUSE)、轮询器重复启动。
16
+ *
17
+ * 设计约束:清理与拆卸**一律吞掉异常**——生命周期回调里抛错会影响宿主的销毁流程
18
+ * (宿主对 `session/disposed` 监听器异常是"contained"语义,但没必要制造噪音)。
19
+ */
20
+ import type { Context } from 'cordis';
21
+ /** 从 Agent / Session / 事件负载中取会话 id(形状宽容:未知结构返回 null) */
22
+ export declare function sessionIdOf(value: unknown): string | null;
23
+ /** 单会话内存态清理(幂等;无对应状态即 no-op) */
24
+ export declare function cleanupSessionState(sessionId: string): void;
25
+ /**
26
+ * 装配生命周期订阅与资源拆卸。
27
+ * @param ctx 宿主插件上下文
28
+ */
29
+ export declare function registerLifecycle(ctx: Context): void;
@@ -1,3 +1,4 @@
1
+ import { a as findSerenityRoot } from "./ccc-DAsSHsub.js";
1
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
2
3
  import { basename, dirname, join } from "node:path";
3
4
  //#region src/session-ops.ts
@@ -671,55 +672,108 @@ function qaCheck(root, key) {
671
672
  if (verified) lines.push(" • Session looks clean — no issues detected");
672
673
  return lines.join("\n");
673
674
  }
674
- //#endregion
675
- //#region src/session-bound.ts
676
- /** 事件 type 常量(声明 + 读取共用) */
677
- const SESSION_BOUND_EVENT = "serenity/bound";
678
- /** 事件形状归一(运行时数据经 session.append 深冻结,此处仅类型收窄) */
679
- function asBoundEvent(e) {
680
- const ev = e;
681
- if (!ev || ev.type !== "serenity/bound") return null;
682
- const d = ev.data;
683
- if (!d || typeof d.dirName !== "string" || typeof d.mdPath !== "string") return null;
684
- return d;
675
+ /** 绑定文件相对 CCC 根的路径 */
676
+ const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
677
+ const BINDINGS_VERSION = 1;
678
+ function sessionHeader(session) {
679
+ const header = session?.header;
680
+ if (!header || typeof header !== "object") return null;
681
+ return header;
685
682
  }
686
- /**
687
- * 读取会话日志中**最后一条** `serenity/bound`(权威绑定,latest-wins)。
688
- * 尾到头扫描(时间序最新在后);无 bound 事件返回 null。
689
- * 纯逻辑零 DSH 依赖(经 sessionEvents helper 读 snapshotEvents/events 兜底)。
690
- */
691
- function readLastBound(session) {
683
+ /** 绑定文件绝对路径(由会话 cwd 上溯 .serenity 定位 CCC 根);无法定位返回 null */
684
+ function bindingsPathFor(session) {
685
+ const header = sessionHeader(session);
686
+ if (typeof header?.cwd !== "string") return null;
687
+ const root = findSerenityRoot(header.cwd);
688
+ if (!root) return null;
689
+ return join(root, BINDINGS_REL_PATH);
690
+ }
691
+ function readBindingsFile(path) {
692
+ if (!existsSync(path)) return {
693
+ version: BINDINGS_VERSION,
694
+ sessions: {}
695
+ };
696
+ try {
697
+ const sessions = JSON.parse(readFileSync(path, "utf-8")).sessions;
698
+ if (!sessions || typeof sessions !== "object") return {
699
+ version: BINDINGS_VERSION,
700
+ sessions: {}
701
+ };
702
+ return {
703
+ version: BINDINGS_VERSION,
704
+ sessions
705
+ };
706
+ } catch {
707
+ return {
708
+ version: BINDINGS_VERSION,
709
+ sessions: {}
710
+ };
711
+ }
712
+ }
713
+ /** 原子写:同目录 tmp + rename(避免半写文件被后续读取) */
714
+ function writeBindingsFile(path, data) {
715
+ mkdirSync(dirname(path), { recursive: true });
716
+ const tmp = `${path}.tmp`;
717
+ writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
718
+ renameSync(tmp, path);
719
+ }
720
+ /** 记录形状校验(容忍手改/旧数据) */
721
+ function asBoundRecord(value) {
722
+ const r = value;
723
+ if (!r || typeof r.dirName !== "string" || typeof r.mdPath !== "string") return null;
724
+ return r;
725
+ }
726
+ /** 旧形态:从会话日志扫最后一条 `serenity/bound`(兼容 v1.30.5 及更早写入的绑定) */
727
+ function readLegacyBoundFromEvents(session) {
692
728
  const events = sessionEvents(session);
693
729
  for (let i = events.length - 1; i >= 0; i--) {
694
- const b = asBoundEvent(events[i]);
695
- if (b) return b;
730
+ const ev = events[i];
731
+ if (!ev || ev.type !== "serenity/bound") continue;
732
+ const rec = asBoundRecord(ev.data);
733
+ if (rec) return rec;
696
734
  }
697
735
  return null;
698
736
  }
699
737
  /**
700
- * append 一条 `serenity/bound` 绑定事件(log-only,无 surfaceOp——元数据不进模型可见面)。
701
- * @param session 目标 dsh 会话(真实 Session;append 泛型经内部断言兼容——类型由
702
- * `serenity/bound` 声明面保证,append 接受已声明事件)
738
+ * 读取会话当前绑定(权威,latest-wins):文件记录优先,无则回落旧事件形态。
739
+ * 无绑定返回 null。
740
+ */
741
+ function readLastBound(session) {
742
+ const path = bindingsPathFor(session);
743
+ const id = sessionHeader(session)?.id;
744
+ if (path && typeof id === "string") {
745
+ const rec = asBoundRecord(readBindingsFile(path).sessions[id]);
746
+ if (rec) return rec;
747
+ }
748
+ return readLegacyBoundFromEvents(session);
749
+ }
750
+ /**
751
+ * 写入绑定(latest-wins 覆盖该会话记录)。
752
+ *
753
+ * 绑定持久化尽力而为:无法定位 CCC 根 / 无会话 id / 写盘失败 → 返回 false,不阻断主流程。
754
+ * @param session 目标 dsh 会话(需 `header.id` + `header.cwd` 可定位 CCC 根)
703
755
  * @param action 绑定动作
704
756
  * @param rec 绑定记录(dirName + mdPath 必填;sessionId 可选展示码)
705
- * @returns 是否成功(append 抛错/会话不可用时 false——绑定失败不阻断主流程)
706
757
  */
707
758
  function appendBound(session, action, rec) {
708
- if (!session || typeof session.append !== "function") return false;
759
+ const path = bindingsPathFor(session);
760
+ const id = sessionHeader(session)?.id;
761
+ if (!path || typeof id !== "string") return false;
709
762
  try {
710
- const append = session.append;
711
- append(SESSION_BOUND_EVENT, {
763
+ const file = readBindingsFile(path);
764
+ file.sessions[id] = {
712
765
  dirName: rec.dirName,
713
766
  mdPath: rec.mdPath,
714
767
  ...rec.sessionId ? { sessionId: rec.sessionId } : {},
715
768
  action,
716
769
  at: Date.now(),
717
770
  ...rec.note ? { note: rec.note } : {}
718
- });
771
+ };
772
+ writeBindingsFile(path, file);
719
773
  return true;
720
774
  } catch {
721
775
  return false;
722
776
  }
723
777
  }
724
778
  //#endregion
725
- export { useSession as C, summarize as S, resolveSessionByTitle as _, archiveSessions as a, setActiveSessionInfo as b, extractSessionMdPathFromText as c, getActiveSessionInfo as d, healthCheck as f, readActiveSessionMd as g, qaCheck as h, SESSION_ACTIONS as i, findLatestActiveSessionMd as l, parseSessionContextFromEvents as m, readLastBound as n, closeSession as o, listSessions as p, DEFAULT_SESSION_SCOPE as r, createSession as s, appendBound as t, findSession as u, sessionEvents as v, showSession as x, sessionsRoot as y };
779
+ export { summarize as C, showSession as S, readActiveSessionMd as _, archiveSessions as a, sessionsRoot as b, createSession as c, findSession as d, getActiveSessionInfo as f, qaCheck as g, parseSessionContextFromEvents as h, SESSION_ACTIONS as i, extractSessionMdPathFromText as l, listSessions as m, readLastBound as n, clearActiveSessionInfo as o, healthCheck as p, DEFAULT_SESSION_SCOPE as r, closeSession as s, appendBound as t, findLatestActiveSessionMd as u, resolveSessionByTitle as v, useSession as w, setActiveSessionInfo as x, sessionEvents as y };
@@ -1,47 +1,29 @@
1
1
  /**
2
- * session-bound.ts — SESSION 绑定持久化(serenity/bound 会话事件)
2
+ * session-bound.ts — SESSION 绑定持久化(CCC 内 `AGENT_SESSIONS/.bindings.json`)
3
3
  *
4
4
  * 目标(S142 用户拍板,方案 v1.0):让「dsh 会话(载体)↔ SESSION(宁静号轨迹)」
5
5
  * 的绑定坚固——LLM 不能因智力因素(幻觉/理解偏差)在过程中或 logbook rebuild 后
6
6
  * 自行更换 SESSION。
7
7
  *
8
- * 机制(DSH 原生,零改 harness):
9
- * `SessionEventMap` 是 merge-extensible(先例:dsh-compaction 扩展 compaction/*;
10
- * dsp 已在 append compaction/prune)——插件可 declare module 自定义事件,
11
- * `Session.append(type, data)` 将其写入会话 append-only 日志,随 session.jsonl
12
- * 持久化落盘,进程重启后 snapshotEvents() 可完整读取。
8
+ * 存储形态(v1.30.6 起,S142 review F-01):绑定**不再写入 dsh 会话日志**。
9
+ * 原因(宿主契约实证):dsp 的自定义事件 `serenity/bound` 既不在宿主的
10
+ * `KNOWN_SESSION_EVENT_TYPES`(生成静态集,明确说明 out-of-repo 插件事件不在其中),
11
+ * 而 `Session.append` 构造事件时只写 `{type,seq,time,data,surfaceOp?,sourceEventSeqs?}`
12
+ * ——**没有写入 envelope `ignorable` 标记的通道**;宿主的 `assertEventsSupported`
13
+ * (session-persistence/coordinator.ts:1248-1252,调用点含恢复/HMR 路径)会拒绝任何
14
+ * "未知且非 ignorable" 的事件 → 含该事件的会话在冷加载时可能抛
15
+ * `SessionFormatUnsupportedError`。v1.29.1~v1.30.5 的设计文档虽写明要带
16
+ * `ignorable: true`(docs/session-binding-hardening-research.md:53),但类型层声明
17
+ * 不等于持久层标记,实际从未落盘。
13
18
  *
14
- * 权威绑定 = 会话日志中**最后一条** `serenity/bound`(append-only latest-wins)。
15
- * 本事件是 log-only 元数据(无 surfaceOp——永不上模型可见面,不进系统提示词)。
19
+ * 现形态:每 CCC 一个 JSON 文件 `AGENT_SESSIONS/.bindings.json`(原子写:tmp + rename),
20
+ * 以 dsh 会话 id 为键,latest-wins 覆盖。**向后兼容**:读取时若文件中无该会话记录,
21
+ * 仍回落到扫描会话日志中的旧 `serenity/bound` 事件(老会话的绑定不丢)。
16
22
  *
17
23
  * 编码无关(U4):绑定锚 = **完整 AGENT_SESSIONS 目录名**(磁盘唯一存在),
18
24
  * sessionId(S###/apaas-xxx/自定义)仅是派生展示字段——绝不假设 S 前缀。
19
25
  */
20
- import type { Session } from '@deepseek-ai/dsh-session';
21
- import type { SessionEventMap } from '@deepseek-ai/dsh-session/types';
22
- declare module '@deepseek-ai/dsh-session/types' {
23
- interface SessionEventMap {
24
- /**
25
- * Serenity SESSION binding — authoritative; appended on every binding change.
26
- * Code-agnostic (keyed by full dir name), log-only (no surfaceOp).
27
- */
28
- 'serenity/bound': {
29
- /** Full AGENT_SESSIONS dir name — the ONLY hard identity. */
30
- dirName: string;
31
- /** SESSION.md absolute path. */
32
- mdPath: string;
33
- /** Display code when parseable (S142 / apaas-26116 / custom); informational. */
34
- sessionId?: string;
35
- /** Why this binding record was written. */
36
- action: 'activate' | 'switch' | 'create' | 'rebuild' | 'reconcile' | 'release';
37
- /** Epoch ms. */
38
- at: number;
39
- /** Optional reason (e.g. 'auto from title'). */
40
- note?: string;
41
- };
42
- }
43
- }
44
- export type SessionBoundAction = SessionEventMap['serenity/bound']['action'];
26
+ export type SessionBoundAction = 'activate' | 'switch' | 'create' | 'rebuild' | 'reconcile' | 'release';
45
27
  export interface SessionBoundRecord {
46
28
  dirName: string;
47
29
  mdPath: string;
@@ -50,29 +32,28 @@ export interface SessionBoundRecord {
50
32
  at: number;
51
33
  note?: string;
52
34
  }
53
- /** 事件 type 常量(声明 + 读取共用) */
35
+ /** 旧形态事件类型(v1.29.1~v1.30.5 写入会话日志;v1.30.6 起只读不写——兼容存量绑定) */
54
36
  export declare const SESSION_BOUND_EVENT: "serenity/bound";
37
+ /** 绑定文件相对 CCC 根的路径 */
38
+ export declare const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
39
+ /** 绑定文件绝对路径(由会话 cwd 上溯 .serenity 定位 CCC 根);无法定位返回 null */
40
+ export declare function bindingsPathFor(session: unknown): string | null;
55
41
  /**
56
- * 读取会话日志中**最后一条** `serenity/bound`(权威绑定,latest-wins)。
57
- * 尾到头扫描(时间序最新在后);无 bound 事件返回 null。
58
- * 纯逻辑零 DSH 依赖(经 sessionEvents helper 读 snapshotEvents/events 兜底)。
42
+ * 读取会话当前绑定(权威,latest-wins):文件记录优先,无则回落旧事件形态。
43
+ * 无绑定返回 null。
59
44
  */
60
45
  export declare function readLastBound(session: unknown): SessionBoundRecord | null;
61
- /**
62
- * 判定会话日志是否已有任何 bound 事件(供 reconcile 判定:无绑定才标题升级)。
63
- */
46
+ /** 判定会话是否已有绑定(供 reconcile 判定:无绑定才标题升级)。 */
64
47
  export declare function hasAnyBound(session: unknown): boolean;
65
48
  /**
66
- * append 一条 `serenity/bound` 绑定事件(log-only,无 surfaceOp——元数据不进模型可见面)。
67
- * @param session 目标 dsh 会话(真实 Session;append 泛型经内部断言兼容——类型由
68
- * `serenity/bound` 声明面保证,append 接受已声明事件)
49
+ * 写入绑定(latest-wins 覆盖该会话记录)。
50
+ *
51
+ * 绑定持久化尽力而为:无法定位 CCC 根 / 无会话 id / 写盘失败 → 返回 false,不阻断主流程。
52
+ * @param session 目标 dsh 会话(需 `header.id` + `header.cwd` 可定位 CCC 根)
69
53
  * @param action 绑定动作
70
54
  * @param rec 绑定记录(dirName + mdPath 必填;sessionId 可选展示码)
71
- * @returns 是否成功(append 抛错/会话不可用时 false——绑定失败不阻断主流程)
72
55
  */
73
- export declare function appendBound(session: Session | {
74
- append: (type: string, data: unknown) => unknown;
75
- } | null | undefined, action: SessionBoundAction, rec: {
56
+ export declare function appendBound(session: unknown, action: SessionBoundAction, rec: {
76
57
  dirName: string;
77
58
  mdPath: string;
78
59
  sessionId?: string;
@@ -1,4 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { a as hostSettings } from "./access-CdL6BAYj.js";
2
3
  import z from "@deepseek-ai/schemastery";
3
4
  //#region src/settings-section.ts
4
5
  var settings_section_exports = /* @__PURE__ */ __exportAll({
@@ -39,6 +40,8 @@ function entryDefaults(config) {
39
40
  }
40
41
  /** 运行时源(installSettingsSection 注入:settings scope 或 entry fallback) */
41
42
  let simpleSource = null;
43
+ /** 缺 settings provider 的告警去重(进程级一次;降级路径可能被多次注册调用) */
44
+ let warnedNoSettingsProvider = false;
42
45
  /** 进程级默认(无 settings 服务时的兜底) */
43
46
  function defaultSimpleSettings() {
44
47
  return {
@@ -79,11 +82,15 @@ function registerSettingsSection(ctx, config) {
79
82
  } catch {}
80
83
  }
81
84
  };
82
- const settingsAny = ctx.settings;
85
+ const settingsAny = hostSettings(ctx);
83
86
  if (settingsAny) {
84
87
  settingsAny.installSection.call(settingsAny, ctx, SERENITY_SETTINGS_NS, simpleSettingsSchema, entryDefaults(config), hooks);
85
88
  return;
86
89
  }
90
+ if (!warnedNoSettingsProvider) {
91
+ warnedNoSettingsProvider = true;
92
+ console.warn("[serenity-hooks] ✗ settings 服务不可用 → 简单配置降级为 entry 默认值(面板开关/阈值改动不生效;检查宿主 settings provider 是否装载)");
93
+ }
87
94
  hooks.setSource(() => ({}));
88
95
  hooks.onChange();
89
96
  }
@@ -92,6 +92,8 @@ export declare const simpleSettingsSchema: z<Schemastery.ObjectS<{
92
92
  }>>;
93
93
  /** 从插件 Config 提取 entry 默认(settings base 层) */
94
94
  export declare function entryDefaults(config: SimpleConfigFragment): SerenitySimpleSettings;
95
+ /** 测试辅助:重置"缺 settings provider"告警去重(生产零调用) */
96
+ export declare function __resetSettingsWarningsForTest(): void;
95
97
  /** 进程级默认(无 settings 服务时的兜底) */
96
98
  export declare function defaultSimpleSettings(): SerenitySimpleSettings;
97
99
  /**
@@ -40,13 +40,21 @@ export declare function workspaceTrajectoryLine(mdPath: string): string;
40
40
  * 懒绑定 + 幂等(per skiff 会话 scope 隔离——微信多用户各自独立,永不共享同一份)。
41
41
  *
42
42
  * 判定链:
43
+ * ⓪ **临时身份不建工作台**(v1.30.9,S142 用户报"SESSION 爆炸"根治):`persistent=false`
44
+ * (ACP/调试页/问答页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
45
+ * 调用都是新 id、无身份延续,为其建 SESSION = 每次问答留一个永久空目录(无界熵增);
46
+ * 用户模型「skiff 本质是临时会话机制」——工作台属于**持久身份**,不属于单次调用。
43
47
  * ① 角色未启用 session 能力(trajectory.session !== true)→ null(现状零变化)
44
- * ② 会话日志已有**本机制自动创建**的 bound(note 前缀 auto-created)→ 恢复(重启/续接记得)
48
+ * ② 已有**本机制自动创建**的 bound(note 前缀 auto-created)→ 恢复(重启/续接记得)
49
+ * ②b 内存活跃记录(同 scope)→ 恢复 + **自愈补写绑定**(防御纵深:绑定写失败/事件形态未落盘时
50
+ * 仍复用同一 SESSION,而不是每条消息新建一个——v1.30.5 及更早"每条消息一个 SESSION"的放大效应)
45
51
  * ③ 无 → 自动创建专属 SESSION(createSession desc=`<role> skiff`——编号自动递增,用户不关心是谁)
46
52
  *
53
+ * @param persistent 该 skiff 会话是否有**持久身份**(固定会话 id)。默认 true(向后兼容调用点);
54
+ * `createSkiffAgent` 传 `sessionId !== undefined`,微信桥的 existing 快路径传 true。
47
55
  * @returns 绑定后的 SESSION.md 绝对路径(供提示词注入);不适用/失败 → null
48
56
  */
49
- export declare function ensureSkiffSession(root: string, agent: Agent, roleName: string, role: SkiffRoleConfig): string | null;
57
+ export declare function ensureSkiffSession(root: string, agent: Agent, roleName: string, role: SkiffRoleConfig, persistent?: boolean): string | null;
50
58
  /** 当前 scope 是否已激活(幂等判定:getActiveSessionInfo 命中同 dirName)——测试用 */
51
59
  export declare function skiffSessionActiveFor(sessionId: string): {
52
60
  sessionId: string;
@@ -1,7 +1,8 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { a as findSerenityRoot, f as readHandymanConfig } from "./ccc-CX48YNUL.js";
3
- import { a as resolveRoleSystemPrompt, i as readSkiffRoles, n as buildSkiffBasePrompt, o as roleMsmWhitelist, r as isSkiffSessionId, u as trajectorySubset } from "./skiff-role-LW4tjF9L.js";
4
- import { b as setActiveSessionInfo, n as readLastBound, s as createSession, t as appendBound } from "./session-bound-p9oR9dj9.js";
2
+ import { a as findSerenityRoot, f as readHandymanConfig } from "./ccc-DAsSHsub.js";
3
+ import { a as resolveRoleSystemPrompt, i as readSkiffRoles, n as buildSkiffBasePrompt, o as roleMsmWhitelist, r as isSkiffSessionId, u as trajectorySubset } from "./skiff-role-DlrbHPLD.js";
4
+ import { i as hostSessions, r as hostService, t as hostAgents } from "./access-CdL6BAYj.js";
5
+ import { c as createSession, f as getActiveSessionInfo, n as readLastBound, t as appendBound, x as setActiveSessionInfo } from "./session-bound-D2ANqVn-.js";
5
6
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
6
7
  import { basename, join } from "node:path";
7
8
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -210,6 +211,47 @@ function listActiveHandymen(root) {
210
211
  return out;
211
212
  }
212
213
  //#endregion
214
+ //#region src/agent-idle.ts
215
+ /**
216
+ * 等待 agent 空闲或销毁。
217
+ * @param ctx 宿主插件上下文(事件通道来源)
218
+ * @param agent 目标 agent
219
+ * @returns 空闲、agent 销毁、或会话销毁时结算(永不 reject)
220
+ */
221
+ function waitAgentIdle(ctx, agent) {
222
+ return new Promise((resolve) => {
223
+ let settled = false;
224
+ const disposers = [];
225
+ const finish = () => {
226
+ if (settled) return;
227
+ settled = true;
228
+ for (const dispose of disposers) try {
229
+ dispose();
230
+ } catch {}
231
+ resolve();
232
+ };
233
+ const subscribe = (name, cb) => {
234
+ try {
235
+ const off = ctx.on?.(name, cb);
236
+ if (typeof off === "function") disposers.push(off);
237
+ } catch {}
238
+ };
239
+ subscribe("agent/status", (payload) => {
240
+ const p = payload;
241
+ if (p?.agent === agent && p?.status === "idle") finish();
242
+ });
243
+ subscribe("agent/disposed", (payload) => {
244
+ if (payload?.agent === agent) finish();
245
+ });
246
+ const sessionId = agent.session?.id;
247
+ subscribe("session/disposed", (session) => {
248
+ const id = session?.id;
249
+ if (typeof id === "string" && id === sessionId) finish();
250
+ });
251
+ if (agent.status === "idle") finish();
252
+ });
253
+ }
254
+ //#endregion
213
255
  //#region src/skiff-core.ts
214
256
  const PLUGIN_SOURCE = {
215
257
  kind: "plugin",
@@ -277,14 +319,23 @@ function workspaceTrajectoryLine(mdPath) {
277
319
  * 懒绑定 + 幂等(per skiff 会话 scope 隔离——微信多用户各自独立,永不共享同一份)。
278
320
  *
279
321
  * 判定链:
322
+ * ⓪ **临时身份不建工作台**(v1.30.9,S142 用户报"SESSION 爆炸"根治):`persistent=false`
323
+ * (ACP/调试页/问答页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
324
+ * 调用都是新 id、无身份延续,为其建 SESSION = 每次问答留一个永久空目录(无界熵增);
325
+ * 用户模型「skiff 本质是临时会话机制」——工作台属于**持久身份**,不属于单次调用。
280
326
  * ① 角色未启用 session 能力(trajectory.session !== true)→ null(现状零变化)
281
- * ② 会话日志已有**本机制自动创建**的 bound(note 前缀 auto-created)→ 恢复(重启/续接记得)
327
+ * ② 已有**本机制自动创建**的 bound(note 前缀 auto-created)→ 恢复(重启/续接记得)
328
+ * ②b 内存活跃记录(同 scope)→ 恢复 + **自愈补写绑定**(防御纵深:绑定写失败/事件形态未落盘时
329
+ * 仍复用同一 SESSION,而不是每条消息新建一个——v1.30.5 及更早"每条消息一个 SESSION"的放大效应)
282
330
  * ③ 无 → 自动创建专属 SESSION(createSession desc=`<role> skiff`——编号自动递增,用户不关心是谁)
283
331
  *
332
+ * @param persistent 该 skiff 会话是否有**持久身份**(固定会话 id)。默认 true(向后兼容调用点);
333
+ * `createSkiffAgent` 传 `sessionId !== undefined`,微信桥的 existing 快路径传 true。
284
334
  * @returns 绑定后的 SESSION.md 绝对路径(供提示词注入);不适用/失败 → null
285
335
  */
286
- function ensureSkiffSession(root, agent, roleName, role) {
336
+ function ensureSkiffSession(root, agent, roleName, role, persistent = true) {
287
337
  if (!trajectorySubset(role).session) return null;
338
+ if (!persistent) return null;
288
339
  const session = agent.session;
289
340
  const scope = String(agent.session.id ?? agent.id);
290
341
  const lastBound = session ? readLastBound(session) : null;
@@ -296,6 +347,16 @@ function ensureSkiffSession(root, agent, roleName, role) {
296
347
  });
297
348
  return lastBound.mdPath;
298
349
  }
350
+ const inMemory = getActiveSessionInfo(scope);
351
+ if (inMemory?.mdPath && existsSync(inMemory.mdPath)) {
352
+ appendBound(session, "reconcile", {
353
+ dirName: inMemory.dirName,
354
+ mdPath: inMemory.mdPath,
355
+ ...inMemory.sessionId ? { sessionId: inMemory.sessionId } : {},
356
+ note: `${AUTO_BOUND_NOTE_PREFIX} ${roleName}`
357
+ });
358
+ return inMemory.mdPath;
359
+ }
299
360
  const desc = `${roleName} skiff`;
300
361
  const created = createSession({
301
362
  root,
@@ -352,7 +413,7 @@ async function createSkiffAgent(ctx, root, roleName, role, defaultModel, session
352
413
  }
353
414
  let workspaceMdPath = null;
354
415
  try {
355
- workspaceMdPath = ensureSkiffSession(root, agent, roleName, role);
416
+ workspaceMdPath = ensureSkiffSession(root, agent, roleName, role, sessionId !== void 0);
356
417
  } catch (err) {
357
418
  console.warn(`[serenity-hooks] skiff 角色 "${roleName}" 专属 SESSION 处理失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
358
419
  }
@@ -380,7 +441,7 @@ async function createSkiffAgent(ctx, root, roleName, role, defaultModel, session
380
441
  /** 取 live agent(DSH 文档:`ctx.agents.get(id)` 返回 bare Agent;会话 live 时 resume/create 均不可用) */
381
442
  function getLiveAgent(ctx, id) {
382
443
  try {
383
- return ctx.agents?.get?.(id);
444
+ return hostAgents(ctx)?.get?.(id);
384
445
  } catch {
385
446
  return;
386
447
  }
@@ -462,22 +523,6 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
462
523
  };
463
524
  }
464
525
  }
465
- /** 等待 agent 空闲(agent/status → idle);无超时(agent 工作多久等多久,handyman 同款) */
466
- function waitIdle(ctx, agent) {
467
- return new Promise((resolve) => {
468
- let settled = false;
469
- let dispose = () => {};
470
- const finish = () => {
471
- if (settled) return;
472
- settled = true;
473
- dispose();
474
- resolve();
475
- };
476
- dispose = ctx.on("agent/status", (payload) => {
477
- if (payload.agent === agent && payload.status === "idle") finish();
478
- });
479
- });
480
- }
481
526
  /**
482
527
  * 会话事件读取(v1.28.0 适配 0.1.2-rc.1):rc.1 Session 移除 `.events` 属性 → `snapshotEvents()` 方法。
483
528
  * 兼容双形态(运行时 rc.1 snapshotEvents;测试替身 events)——同一函数两处语义,测试注入零侵入。
@@ -563,7 +608,7 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
563
608
  }],
564
609
  source: PLUGIN_SOURCE
565
610
  }));
566
- await waitIdle(ctx, agent);
611
+ await waitAgentIdle(ctx, agent);
567
612
  const answer = lastAssistantText(agent);
568
613
  const trajectory = options?.includeTrajectory === false ? [] : eventsToTrajectory(sessionEvents(agent.session).slice(before));
569
614
  return {
@@ -2324,15 +2369,15 @@ async function discoverCccs(ctx, defaultRoot) {
2324
2369
  if (r && !roots.includes(r)) roots.push(r);
2325
2370
  };
2326
2371
  try {
2327
- const registry = ctx.get?.("workspaceRegistry");
2372
+ const registry = hostService(ctx, "workspaceRegistry");
2328
2373
  for (const ws of registry?.list?.() ?? []) pushRoot(ws?.path);
2329
2374
  } catch {}
2330
2375
  if (roots.length === 0) try {
2331
- const sp = ctx.get?.("sessionPersistence");
2376
+ const sp = hostService(ctx, "sessionPersistence");
2332
2377
  for (const h of await sp?.list?.() ?? []) pushRoot(h?.cwd);
2333
2378
  } catch {}
2334
2379
  if (roots.length === 0) try {
2335
- const sessions = ctx.sessions;
2380
+ const sessions = hostSessions(ctx);
2336
2381
  for (const s of sessions?.list?.() ?? []) pushRoot(s?.header?.cwd);
2337
2382
  } catch {}
2338
2383
  if (!roots.includes(defaultRoot)) roots.unshift(defaultRoot);
@@ -2748,4 +2793,4 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
2748
2793
  }
2749
2794
  }
2750
2795
  //#endregion
2751
- export { readProgress as C, writeProgress as D, writeFailedStatus as E, skiffRoleFor$1 as O, newStopToken as S, splitModel as T, workspaceTrajectoryLine as _, startSkiffDebugServer as a, handymanProgressPaths as b, askSkiff as c, getSkiffAgent as d, skiffMsmGate as f, unregisterSkiffSession as g, skiffTrajectoryEnabled as h, skiff_debug_exports as i, createSkiffAgent as l, skiffSessionSnapshot as m, jscSafeJsonText as n, stopSkiffDebugServer as o, skiffSessionInfo as p, renderSkiffMarkdown as r, stripThink as s, discoverCccs as t, ensureSkiffSession as u, HANDYMAN_GUIDE as v, requireWhitelistedModel as w, listActiveHandymen as x, buildRoundPrompt as y };
2796
+ export { newStopToken as C, writeFailedStatus as D, splitModel as E, writeProgress as O, listActiveHandymen as S, requireWhitelistedModel as T, workspaceTrajectoryLine as _, startSkiffDebugServer as a, buildRoundPrompt as b, askSkiff as c, getSkiffAgent as d, skiffMsmGate as f, unregisterSkiffSession as g, skiffTrajectoryEnabled as h, skiff_debug_exports as i, skiffRoleFor$1 as k, createSkiffAgent as l, skiffSessionSnapshot as m, jscSafeJsonText as n, stopSkiffDebugServer as o, skiffSessionInfo as p, renderSkiffMarkdown as r, stripThink as s, discoverCccs as t, ensureSkiffSession as u, waitAgentIdle as v, readProgress as w, handymanProgressPaths as x, HANDYMAN_GUIDE as y };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { m as resolveInside, p as readUtf8, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-CX48YNUL.js";
2
+ import { m as resolveInside, p as readUtf8, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-DAsSHsub.js";
3
3
  import { existsSync } from "node:fs";
4
4
  //#region src/skiff-role.ts
5
5
  /**
@@ -54,7 +54,9 @@ function readSkiffRoles(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
54
54
  systemPromptFile: typeof role.systemPromptFile === "string" ? role.systemPromptFile : void 0
55
55
  });
56
56
  }
57
- } catch {}
57
+ } catch (err) {
58
+ console.warn(`[serenity-hooks] ✗ skiff 角色配置读取失败(按无角色继续): ${String(err?.message ?? err)}`);
59
+ }
58
60
  return out;
59
61
  }
60
62
  function trajectorySubset(role) {