@shgroup/dsh-serenity-hooks 1.30.4 → 1.30.8

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/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CX48YNUL.js";
3
- import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-LW4tjF9L.js";
4
- import { C as readProgress, D as writeProgress, E as writeFailedStatus, O as skiffRoleFor, S as newStopToken, T as splitModel, _ as workspaceTrajectoryLine, a as startSkiffDebugServer, b as handymanProgressPaths, c as askSkiff, d as getSkiffAgent, f as skiffMsmGate, g as unregisterSkiffSession, h as skiffTrajectoryEnabled, l as createSkiffAgent, m as skiffSessionSnapshot, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionInfo, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as HANDYMAN_GUIDE, w as requireWhitelistedModel, x as listActiveHandymen, y as buildRoundPrompt } from "./skiff-debug-gYigSgl_.js";
5
- import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, 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-jA0qENOf.js";
6
- import { C as useSession, S as summarize, _ as resolveSessionByTitle, a as archiveSessions, b as setActiveSessionInfo, c as extractSessionMdPathFromText, d as getActiveSessionInfo, f as healthCheck, g as readActiveSessionMd, h as qaCheck, i as SESSION_ACTIONS, l as findLatestActiveSessionMd, m as parseSessionContextFromEvents, n as readLastBound, o as closeSession, p as listSessions, r as DEFAULT_SESSION_SCOPE, s as createSession, t as appendBound, u as findSession, v as sessionEvents, x as showSession, y as sessionsRoot } from "./session-bound-p9oR9dj9.js";
7
- import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-BfVxgCZy.js";
2
+ import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-DAsSHsub.js";
3
+ import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-DlrbHPLD.js";
4
+ import { C as newStopToken, D as writeFailedStatus, E as splitModel, O as writeProgress, S as listActiveHandymen, T as requireWhitelistedModel, _ as workspaceTrajectoryLine, a as startSkiffDebugServer, b as buildRoundPrompt, c as askSkiff, d as getSkiffAgent, f as skiffMsmGate, g as unregisterSkiffSession, h as skiffTrajectoryEnabled, k as skiffRoleFor, l as createSkiffAgent, m as skiffSessionSnapshot, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionInfo, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as waitAgentIdle, w as readProgress, x as handymanProgressPaths, y as HANDYMAN_GUIDE } from "./skiff-debug-8pF60JBO.js";
5
+ import { i as hostSessions, n as hostInjected, o as hostWebServer, r as hostService, t as hostAgents } from "./access-CdL6BAYj.js";
6
+ import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, 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-BJ4g4ZNq.js";
7
+ import { C as summarize, S as showSession, _ as readActiveSessionMd, a as archiveSessions, b as sessionsRoot, c as createSession, d as findSession, f as getActiveSessionInfo, g as qaCheck, h as parseSessionContextFromEvents, i as SESSION_ACTIONS, l as extractSessionMdPathFromText, m as listSessions, n as readLastBound, o as clearActiveSessionInfo, p as healthCheck, r as DEFAULT_SESSION_SCOPE, s as closeSession, t as appendBound, u as findLatestActiveSessionMd, v as resolveSessionByTitle, w as useSession, x as setActiveSessionInfo, y as sessionEvents } from "./session-bound-D2ANqVn-.js";
8
+ import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-BPk0e1du.js";
8
9
  import { a as markdownToPlainText, c as sniffImageExt, i as getUpdates, n as downloadMedia, o as sendTextMessage, r as getConfig, s as sendTyping, t as TypingStatus } from "./weixin-api-OUCuw0Ws.js";
9
10
  import z from "@deepseek-ai/schemastery";
10
11
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -836,6 +837,306 @@ function setSafeMode(root, on) {
836
837
  return { on: isSafeModeOn(root) };
837
838
  }
838
839
  //#endregion
840
+ //#region src/host/contract.ts
841
+ /** dsp 依赖的宿主服务与成员(对照 AI_LAB/dsh-harness-public @ 0.1.2-rc.1 逐一核对) */
842
+ const HOST_SERVICES = [
843
+ {
844
+ id: "tools",
845
+ name: "tools",
846
+ access: "injected",
847
+ members: [{
848
+ name: "register",
849
+ kind: "function"
850
+ }, {
851
+ name: "guard",
852
+ kind: "function"
853
+ }],
854
+ impact: "10 个 ACC 工具无法注册 / 机械守卫失效",
855
+ required: true
856
+ },
857
+ {
858
+ id: "sessions",
859
+ name: "sessions",
860
+ access: "injected",
861
+ members: [
862
+ {
863
+ name: "list",
864
+ kind: "function"
865
+ },
866
+ {
867
+ name: "get",
868
+ kind: "function"
869
+ },
870
+ {
871
+ name: "create",
872
+ kind: "function"
873
+ }
874
+ ],
875
+ impact: "会话定位/rebuild/CCC 解析失效",
876
+ required: true
877
+ },
878
+ {
879
+ id: "agents",
880
+ name: "agents",
881
+ access: "injected",
882
+ members: [{
883
+ name: "create",
884
+ kind: "function"
885
+ }, {
886
+ name: "get",
887
+ kind: "function"
888
+ }],
889
+ impact: "skiff/handyman 无法创建或复用 agent",
890
+ required: true
891
+ },
892
+ {
893
+ id: "webServer",
894
+ name: "webServer",
895
+ access: "injected",
896
+ members: [{
897
+ name: "register",
898
+ kind: "function"
899
+ }, {
900
+ name: "port",
901
+ kind: "value"
902
+ }],
903
+ impact: "状态接口/网关端口发现失效",
904
+ required: true
905
+ },
906
+ {
907
+ id: "settings",
908
+ name: "settings",
909
+ access: "injected",
910
+ members: [{
911
+ name: "installSection",
912
+ kind: "function"
913
+ }],
914
+ impact: "设置面板不安装 → 所有开关静默 no-op",
915
+ required: true
916
+ },
917
+ {
918
+ id: "systemPrompt",
919
+ name: "systemPrompt",
920
+ access: "injected",
921
+ members: [],
922
+ impact: "Induction(入口 skill 段)注入失效",
923
+ required: true
924
+ },
925
+ {
926
+ id: "sessionProjections",
927
+ name: "sessionProjections",
928
+ access: "injected",
929
+ members: [{
930
+ name: "snapshot",
931
+ kind: "function"
932
+ }],
933
+ impact: "上下文压力读数失效 → rebuild 提醒不再触发",
934
+ required: false
935
+ },
936
+ {
937
+ id: "skills",
938
+ name: "skills",
939
+ access: "injected",
940
+ members: [],
941
+ impact: "opencode skill 兼容层(.opencode/skills 注册)失效",
942
+ required: false
943
+ },
944
+ {
945
+ id: "shellEnv",
946
+ name: "shellEnv",
947
+ access: "injected",
948
+ members: [],
949
+ impact: "DSH_SERENITY_* 环境事实注入失效",
950
+ required: false
951
+ },
952
+ {
953
+ id: "agentLoop",
954
+ name: "agentLoop",
955
+ access: "injected",
956
+ members: [],
957
+ impact: "handyman worker agent 无法装配 preset",
958
+ required: false
959
+ },
960
+ {
961
+ id: "sessionTitle",
962
+ name: "sessionTitle",
963
+ access: "lazy",
964
+ members: [{
965
+ name: "rename",
966
+ kind: "function"
967
+ }],
968
+ impact: "会话命名 / rebuild 后重命名跳过(仅告警)",
969
+ required: false
970
+ },
971
+ {
972
+ id: "tokenMeter",
973
+ name: "tokenMeter",
974
+ access: "lazy",
975
+ members: [{
976
+ name: "estimateMessage",
977
+ kind: "function"
978
+ }],
979
+ impact: "rebuild shadow-price 定价失效(计量不准)",
980
+ required: false
981
+ },
982
+ {
983
+ id: "workspaceRegistry",
984
+ name: "workspaceRegistry",
985
+ access: "lazy",
986
+ members: [{
987
+ name: "list",
988
+ kind: "function"
989
+ }],
990
+ impact: "工作区白名单下拉为空(外部访问配置受限)",
991
+ required: false
992
+ },
993
+ {
994
+ id: "sessionPersistence",
995
+ name: "sessionPersistence",
996
+ access: "lazy",
997
+ members: [{
998
+ name: "list",
999
+ kind: "function"
1000
+ }],
1001
+ impact: "CCC 自动发现回落失效(诊断面受限)",
1002
+ required: false
1003
+ },
1004
+ {
1005
+ id: "connection",
1006
+ name: "connection",
1007
+ access: "lazy",
1008
+ members: [{
1009
+ name: "authenticatedUrl",
1010
+ kind: "function"
1011
+ }, {
1012
+ name: "authorizeIndex",
1013
+ kind: "function"
1014
+ }],
1015
+ impact: "双端口网关无法换取 dsh cookie → 外部反代 401",
1016
+ required: false
1017
+ }
1018
+ ];
1019
+ /** dsp 被验证过的宿主版本范围(与 package.json peerDependencies 同源,单一真相源) */
1020
+ const REQUIRED_HOST_RANGE = "^0.1.2-rc.1";
1021
+ /** 最小 semver 比较(无依赖):返回 -1 / 0 / 1;解析失败返回 null */
1022
+ function compareSemver(a, b) {
1023
+ const parse = (v) => {
1024
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-(.*))?$/.exec(v.trim());
1025
+ if (!m) return null;
1026
+ return [
1027
+ Number(m[1]),
1028
+ Number(m[2]),
1029
+ Number(m[3]),
1030
+ m[4] ?? ""
1031
+ ];
1032
+ };
1033
+ const pa = parse(a);
1034
+ const pb = parse(b);
1035
+ if (!pa || !pb) return null;
1036
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
1037
+ const ra = pa[3];
1038
+ const rb = pb[3];
1039
+ if (ra === rb) return 0;
1040
+ if (ra === "") return 1;
1041
+ if (rb === "") return -1;
1042
+ return ra < rb ? -1 : 1;
1043
+ }
1044
+ /**
1045
+ * 宿主版本是否落在 dsp 被验证的范围(`^0.1.2-rc.1`)。
1046
+ * 无版本信息 → 视为未知(ok=false,detail 说明),不猜测。
1047
+ */
1048
+ function checkHostVersion(version) {
1049
+ if (version === null) return {
1050
+ ok: false,
1051
+ detail: `host version unknown (required ${REQUIRED_HOST_RANGE})`
1052
+ };
1053
+ const floor = compareSemver(version, "0.1.2-rc.1");
1054
+ const ceiling = compareSemver(version, "0.2.0");
1055
+ if (floor === null || ceiling === null) return {
1056
+ ok: false,
1057
+ detail: `host version "${version}" not parseable (required ${REQUIRED_HOST_RANGE})`
1058
+ };
1059
+ if (floor < 0) return {
1060
+ ok: false,
1061
+ detail: `host ${version} is below the verified floor (required ${REQUIRED_HOST_RANGE})`
1062
+ };
1063
+ if (ceiling >= 0) return {
1064
+ ok: false,
1065
+ detail: `host ${version} is outside the verified range (required ${REQUIRED_HOST_RANGE})`
1066
+ };
1067
+ return {
1068
+ ok: true,
1069
+ detail: `host ${version} within ${REQUIRED_HOST_RANGE}`
1070
+ };
1071
+ }
1072
+ /** 结构化读取宿主服务(lazy → ctx.get;injected → 直接属性,属性缺失再试 ctx.get) */
1073
+ function readService(ctx, name, access) {
1074
+ return access === "lazy" ? hostService(ctx, name) : hostInjected(ctx, name);
1075
+ }
1076
+ /**
1077
+ * 探针:核对运行中的宿主是否仍满足 dsp 依赖的服务与成员。
1078
+ *
1079
+ * @param ctx 宿主插件上下文(真实 cordis Context;测试可传结构化替身)
1080
+ * @param hostVersion 运行中宿主版本(`readDshVersion()` 结果;null = 未知)
1081
+ * @returns 报告(`ok:false` 时 `issues` 逐条给出缺失项与后果)
1082
+ */
1083
+ function probeHostContract(ctx, hostVersion = null) {
1084
+ const issues = [];
1085
+ let checked = 0;
1086
+ for (const svc of HOST_SERVICES) {
1087
+ checked += 1;
1088
+ let value;
1089
+ try {
1090
+ value = readService(ctx, svc.name, svc.access);
1091
+ } catch {
1092
+ value = void 0;
1093
+ }
1094
+ if (value === void 0 || value === null) {
1095
+ issues.push({
1096
+ id: svc.id,
1097
+ kind: "service",
1098
+ detail: `service "${svc.name}" not available (${svc.access})`,
1099
+ impact: svc.impact,
1100
+ required: svc.required
1101
+ });
1102
+ continue;
1103
+ }
1104
+ for (const member of svc.members) {
1105
+ checked += 1;
1106
+ const actual = value[member.name];
1107
+ if (!(member.kind === "function" ? typeof actual === "function" : actual !== void 0)) issues.push({
1108
+ id: `${svc.id}.${member.name}`,
1109
+ kind: "member",
1110
+ detail: `service "${svc.name}" is missing ${member.kind} "${member.name}"`,
1111
+ impact: svc.impact,
1112
+ required: svc.required
1113
+ });
1114
+ }
1115
+ }
1116
+ const version = checkHostVersion(hostVersion);
1117
+ if (!version.ok) issues.push({
1118
+ id: "host.version",
1119
+ kind: "version",
1120
+ detail: version.detail,
1121
+ impact: "契约未经该宿主版本验证(漂移风险)",
1122
+ required: false
1123
+ });
1124
+ return {
1125
+ ok: issues.every((i) => !i.required),
1126
+ checked,
1127
+ hostVersion,
1128
+ versionOk: version.ok,
1129
+ issues
1130
+ };
1131
+ }
1132
+ /** 单行摘要(启动告警用) */
1133
+ function summarizeHostContract(report) {
1134
+ if (report.ok && report.issues.length === 0) return `host contract ok (${report.checked} checks, host ${report.hostVersion ?? "unknown"})`;
1135
+ const required = report.issues.filter((i) => i.required);
1136
+ const optional = report.issues.filter((i) => !i.required);
1137
+ return `${required.length > 0 ? `host contract BROKEN (${required.length} required)` : `host contract degraded (${optional.length} optional)`}: ${report.issues.map((i) => i.detail).join("; ")}`;
1138
+ }
1139
+ //#endregion
839
1140
  //#region src/kit-ops.ts
840
1141
  /**
841
1142
  * kit-ops.ts — dashboard 纯操作层(acc_kit → dashboard,v1.30;零 DSH 依赖)
@@ -933,7 +1234,7 @@ function checkRegistryHealth(root) {
933
1234
  issues
934
1235
  };
935
1236
  }
936
- async function runKit(root, args) {
1237
+ async function runKit(root, args, hostCtx) {
937
1238
  switch (args.action) {
938
1239
  case "health": {
939
1240
  const cccName = readCccName$1(root);
@@ -973,6 +1274,11 @@ async function runKit(root, args) {
973
1274
  if (root) report.config = loadSerenityConfig(root);
974
1275
  report.accVersion = ACC_VERSION;
975
1276
  report.dshVersion = readDshVersion();
1277
+ if (hostCtx !== void 0) {
1278
+ const contract = probeHostContract(hostCtx, report.dshVersion);
1279
+ report.hostContract = contract;
1280
+ if (!contract.ok) report.status = "degraded";
1281
+ }
976
1282
  return report;
977
1283
  }
978
1284
  case "time": {
@@ -1006,29 +1312,35 @@ function renderText$11(value) {
1006
1312
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
1007
1313
  }];
1008
1314
  }
1009
- const kitTool = defineTool({
1010
- name: "dashboard",
1011
- description: "Dashboard — always-on container instruments (works any time, no side effects): health (CCC three-principle check P1/P2/config + MSM registry integrity report, healthy/degraded) / time (now_iso/now_local/epoch_ms) / wait (wait N seconds, default 1). Routine self-check before entering a CCC.",
1012
- parameters: {
1013
- action: {
1014
- type: "string",
1015
- enum: [...KIT_ACTIONS],
1016
- required: true,
1017
- description: "Subcommand"
1315
+ /**
1316
+ * dashboard 工具(v1.30.7:闭包捕获 ctx → health 可附宿主契约探针 F-05)。
1317
+ * ctx 仅用于**只读探针**(`ctx.get` / 属性读取),无副作用。
1318
+ */
1319
+ function createKitTool(ctx) {
1320
+ return defineTool({
1321
+ name: "dashboard",
1322
+ description: "Dashboard — always-on container instruments (works any time, no side effects): health (CCC three-principle check P1/P2/config + MSM registry integrity + host-contract probe, healthy/degraded) / time (now_iso/now_local/epoch_ms) / wait (wait N seconds, default 1). Routine self-check before entering a CCC.",
1323
+ parameters: {
1324
+ action: {
1325
+ type: "string",
1326
+ enum: [...KIT_ACTIONS],
1327
+ required: true,
1328
+ description: "Subcommand"
1329
+ },
1330
+ seconds: {
1331
+ type: "integer",
1332
+ description: "Seconds to wait (positive integer, default 1)"
1333
+ }
1018
1334
  },
1019
- seconds: {
1020
- type: "integer",
1021
- description: "Seconds to wait (positive integer, default 1)"
1335
+ output: {
1336
+ schema: { type: "json" },
1337
+ render: (args, value) => renderText$11(value)
1338
+ },
1339
+ async execute(args, exec) {
1340
+ return await runKit(findSerenityRoot(agentCwd$8(exec)), args, ctx);
1022
1341
  }
1023
- },
1024
- output: {
1025
- schema: { type: "json" },
1026
- render: (args, value) => renderText$11(value)
1027
- },
1028
- async execute(args, exec) {
1029
- return await runKit(findSerenityRoot(agentCwd$8(exec)), args);
1030
- }
1031
- });
1342
+ });
1343
+ }
1032
1344
  //#endregion
1033
1345
  //#region src/git-ops.ts
1034
1346
  /**
@@ -2332,7 +2644,7 @@ function handymanPresetInheritance(parentCtx) {
2332
2644
  *
2333
2645
  * 机制:ctx.agents.create()(带 setup 钩子)创建专用 agent(进程内),
2334
2646
  * 每轮 followup → agent/status idle → 读 session.events 响应 → 写进度 → stop token 检查 →
2335
- * 未完成继续下一轮;followup/waitIdle 抛错(非正常停止)→ dispose 并重新 create agent
2647
+ * 未完成继续下一轮;followup/等待空闲抛错(非正常停止)→ dispose 并重新 create agent
2336
2648
  * (重启计数,≤100),同一轮重试。工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
2337
2649
  *
2338
2650
  * preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
@@ -2347,21 +2659,6 @@ function renderText$4(value) {
2347
2659
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
2348
2660
  }];
2349
2661
  }
2350
- /** 等待 agent 空闲(agent/status → idle);无超时(handyman 可永续,agent 工作多久等多久) */
2351
- function waitIdle(ctx, agent) {
2352
- return new Promise((resolve) => {
2353
- let settled = false;
2354
- const finish = () => {
2355
- if (settled) return;
2356
- settled = true;
2357
- dispose();
2358
- resolve();
2359
- };
2360
- const dispose = ctx.on("agent/status", (payload) => {
2361
- if (payload.agent === agent && payload.status === "idle") finish();
2362
- });
2363
- });
2364
- }
2365
2662
  /** 读取会话最后一个 assistant/message 文本 */
2366
2663
  function lastAssistantText$1(agent) {
2367
2664
  const s = agent.session;
@@ -2461,7 +2758,7 @@ async function runHandymanJob(ctx, opts) {
2461
2758
  plugin: "dsh-serenity-hooks"
2462
2759
  }
2463
2760
  }));
2464
- await waitIdle(ctx, workerAgent);
2761
+ await waitAgentIdle(ctx, workerAgent);
2465
2762
  lastResponse = lastAssistantText$1(workerAgent);
2466
2763
  } catch {
2467
2764
  restarts++;
@@ -2727,7 +3024,7 @@ function renameDshSessionOnUse(deps, session, titles, active, summary) {
2727
3024
  */
2728
3025
  function renameDshSessionForActive(ctx, exec, info, summary) {
2729
3026
  try {
2730
- const titles = ctx.get?.("sessionTitle");
3027
+ const titles = hostService(ctx, "sessionTitle");
2731
3028
  const dshSession = exec.agent?.session;
2732
3029
  if (!dshSession) {
2733
3030
  console.warn(`[serenity-hooks] dsh 会话重命名未执行: 缺少 agent session(info: ${info.sessionId})`);
@@ -3328,6 +3625,40 @@ const containerAdminTool = defineTool({
3328
3625
  }
3329
3626
  });
3330
3627
  //#endregion
3628
+ //#region src/host/effect.ts
3629
+ /** 已告警的标签(进程级去重:重复装配/多测试替身不刷屏,但绝不静默) */
3630
+ const warnedLabels = /* @__PURE__ */ new Set();
3631
+ /**
3632
+ * 登记一个卸载时执行的清理。
3633
+ * @param ctx 宿主插件上下文
3634
+ * @param label 日志标签(定位是哪个资源未装配)
3635
+ * @param cleanup 卸载时执行;抛错被吞掉(不阻断其它清理)
3636
+ * @returns 是否成功装配(false = 宿主无 `ctx.effect`,已告警)
3637
+ */
3638
+ function registerDisposer(ctx, label, cleanup) {
3639
+ const effect = ctx?.effect;
3640
+ if (typeof effect !== "function") {
3641
+ if (!warnedLabels.has(label)) {
3642
+ warnedLabels.add(label);
3643
+ console.warn(`[serenity-hooks] ✗ 资源拆卸未装配(${label}):ctx.effect 不可用(宿主契约缺失)`);
3644
+ }
3645
+ return false;
3646
+ }
3647
+ try {
3648
+ effect.call(ctx, () => () => {
3649
+ try {
3650
+ cleanup();
3651
+ } catch (err) {
3652
+ console.warn(`[serenity-hooks] ✗ 资源清理失败(${label}): ${String(err?.message ?? err)}`);
3653
+ }
3654
+ }, `dsh-serenity-hooks: ${label}`);
3655
+ return true;
3656
+ } catch (err) {
3657
+ console.warn(`[serenity-hooks] ✗ 资源拆卸注册失败(${label}): ${String(err?.message ?? err)}`);
3658
+ return false;
3659
+ }
3660
+ }
3661
+ //#endregion
3331
3662
  //#region src/autopilot-trajectory.ts
3332
3663
  var autopilot_trajectory_exports = /* @__PURE__ */ __exportAll({
3333
3664
  AUDIT_HISTORY_MAX: () => 50,
@@ -3678,6 +4009,12 @@ function registerAutopilot(ctx) {
3678
4009
  try {
3679
4010
  ctx.on("serenity/settings-changed", () => startTimer());
3680
4011
  } catch {}
4012
+ registerDisposer(ctx, "autopilot 时钟", () => {
4013
+ if (timer) {
4014
+ clearInterval(timer);
4015
+ timer = null;
4016
+ }
4017
+ });
3681
4018
  }
3682
4019
  /**
3683
4020
  * 目标 agent:从 SESSION.md 反向定位 dsh 会话(cwd 归属校验 + **bound 精确匹配优先**,
@@ -3697,8 +4034,8 @@ function resolveTargetAgent(ctx, mdPath) {
3697
4034
  const idMatch = dirName.match(/--S(\d{3,})--/);
3698
4035
  const sid = idMatch ? `S${idMatch[1]}` : null;
3699
4036
  const targetRoot = findSerenityRoot(mdPath);
3700
- const sessions = ctx.sessions;
3701
- const agents = ctx.agents;
4037
+ const sessions = hostSessions(ctx);
4038
+ const agents = hostAgents(ctx);
3702
4039
  const candidates = [];
3703
4040
  try {
3704
4041
  for (const s of sessions?.list?.() ?? []) {
@@ -3764,7 +4101,7 @@ function diagnoseTargetUnavailable(ctx, mdPath) {
3764
4101
  const sameCccTitles = [];
3765
4102
  const agentMissing = { matched: false };
3766
4103
  try {
3767
- const sessions = ctx.sessions;
4104
+ const sessions = hostSessions(ctx);
3768
4105
  for (const s of sessions?.list?.() ?? []) {
3769
4106
  const sess = s;
3770
4107
  const cwd = sess?.header?.cwd ?? "";
@@ -3777,7 +4114,7 @@ function diagnoseTargetUnavailable(ctx, mdPath) {
3777
4114
  const boundMatched = bound !== null && bound.dirName === dirName;
3778
4115
  const titleMatched = sid !== null && title !== null && (title === sid || title.startsWith(`${sid}-`));
3779
4116
  if (boundMatched || titleMatched) {
3780
- if (!ctx.agents?.get?.(sess.id ?? "")) agentMissing.matched = true;
4117
+ if (!hostAgents(ctx)?.get?.(sess.id ?? "")) agentMissing.matched = true;
3781
4118
  }
3782
4119
  }
3783
4120
  } catch {}
@@ -3790,7 +4127,7 @@ function resolveAutopilotRoot(ctx) {
3790
4127
  const fromCwd = findSerenityRoot(process.cwd());
3791
4128
  if (fromCwd) return fromCwd;
3792
4129
  try {
3793
- const sessions = ctx.sessions;
4130
+ const sessions = hostSessions(ctx);
3794
4131
  for (const s of sessions?.list?.() ?? []) {
3795
4132
  const r = findSerenityRoot(s?.header?.cwd ?? "");
3796
4133
  if (r) return r;
@@ -3843,7 +4180,7 @@ function getAutopilotStatus(root) {
3843
4180
  function listLiveSessions(ctx) {
3844
4181
  const out = [];
3845
4182
  try {
3846
- const sessions = ctx.sessions;
4183
+ const sessions = hostSessions(ctx);
3847
4184
  for (const s of sessions?.list?.() ?? []) {
3848
4185
  const cwd = s?.header?.cwd ?? null;
3849
4186
  out.push({
@@ -4457,7 +4794,7 @@ function rebuildReminderText(tokensK, thresholdK, escalated = false) {
4457
4794
  /** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
4458
4795
  function readContextPressure(ctx, session) {
4459
4796
  try {
4460
- const projections = ctx.get?.("sessionProjections");
4797
+ const projections = hostService(ctx, "sessionProjections");
4461
4798
  if (!projections) return null;
4462
4799
  const pressure = (projections.snapshot?.(session))?.values?.contextPressure;
4463
4800
  if (!pressure || typeof pressure.projectedTokens !== "number") return null;
@@ -5874,10 +6211,10 @@ function resolveWorkspaceCore(sessionCwd, workspaceParam, listCwds, fallback) {
5874
6211
  }
5875
6212
  function resolveWorkspace(ctx, params) {
5876
6213
  let sessionCwd;
5877
- if (params.sessionId) sessionCwd = (ctx.sessions?.get?.(params.sessionId))?.header?.cwd;
6214
+ if (params.sessionId) sessionCwd = (hostSessions(ctx)?.get?.(params.sessionId))?.header?.cwd;
5878
6215
  let listCwds = () => [];
5879
6216
  try {
5880
- const sessions = ctx.sessions;
6217
+ const sessions = hostSessions(ctx);
5881
6218
  if (typeof sessions?.list === "function") listCwds = () => sessions.list().map((s) => s.header?.cwd ?? "").filter(Boolean);
5882
6219
  } catch {}
5883
6220
  return resolveWorkspaceCore(sessionCwd, params.workspace, listCwds, process.cwd());
@@ -6032,8 +6369,7 @@ function registerStatusApi(ctx, opts = {}) {
6032
6369
  const settings = readAdvancedSettings();
6033
6370
  let known = [];
6034
6371
  try {
6035
- const registry = ctx.get?.("workspaceRegistry");
6036
- known = projectKnownWorkspaces(registry?.list?.() ?? [], settings.gateway.workspaces);
6372
+ known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
6037
6373
  } catch {}
6038
6374
  sendJson$1(res, 200, {
6039
6375
  config: toWire(settings),
@@ -6071,7 +6407,7 @@ function registerStatusApi(ctx, opts = {}) {
6071
6407
  workspace: url.searchParams.get("workspace") ?? void 0
6072
6408
  });
6073
6409
  const root = findSerenityRoot(workspace) ?? "";
6074
- const { discoverCccs } = await import("./skiff-debug-gYigSgl_.js").then((n) => n.i);
6410
+ const { discoverCccs } = await import("./skiff-debug-8pF60JBO.js").then((n) => n.i);
6075
6411
  sendJson$1(res, 200, { cccs: await discoverCccs(ctx, root) });
6076
6412
  } catch (err) {
6077
6413
  sendJson$1(res, 400, { error: err.message ?? String(err) });
@@ -6101,7 +6437,7 @@ function registerStatusApi(ctx, opts = {}) {
6101
6437
  return;
6102
6438
  }
6103
6439
  const settings = readAdvancedSettings();
6104
- const { readSimpleSettings } = await import("./settings-section-BfVxgCZy.js").then((n) => n.r);
6440
+ const { readSimpleSettings } = await import("./settings-section-BPk0e1du.js").then((n) => n.r);
6105
6441
  const simple = readSimpleSettings();
6106
6442
  const allowed = settings.publicAsk.allowed;
6107
6443
  const port = simple.acpHttpPort ?? 3100;
@@ -6197,11 +6533,11 @@ function registerStatusApi(ctx, opts = {}) {
6197
6533
  sendJson$1(res, 400, { error: chk.error });
6198
6534
  return;
6199
6535
  }
6200
- const { readWeixinSettings } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6536
+ const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6201
6537
  const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
6202
6538
  const settings = readWeixinSettings(chk.root);
6203
6539
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
6204
- const { readWeixinCredential } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6540
+ const { readWeixinCredential } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6205
6541
  const accounts = (settings.accounts ?? []).map((a) => ({
6206
6542
  accountId: a.accountId,
6207
6543
  name: a.name ?? void 0,
@@ -6233,7 +6569,7 @@ function registerStatusApi(ctx, opts = {}) {
6233
6569
  const { fetchQRCode } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
6234
6570
  const { randomUUID } = await import("node:crypto");
6235
6571
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
6236
- const { readWeixinSettings } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6572
+ const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6237
6573
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
6238
6574
  const loginKey = randomUUID();
6239
6575
  weixinLogins.set(loginKey, {
@@ -6254,7 +6590,7 @@ function registerStatusApi(ctx, opts = {}) {
6254
6590
  sendJson$1(res, 400, { error: "missing accountId" });
6255
6591
  return;
6256
6592
  }
6257
- const { removeWeixinAccount } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6593
+ const { removeWeixinAccount } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6258
6594
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6259
6595
  removeWeixinAccount(root, body.accountId);
6260
6596
  syncCccBridge(ctx, root);
@@ -6267,8 +6603,8 @@ function registerStatusApi(ctx, opts = {}) {
6267
6603
  sendJson$1(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
6268
6604
  return;
6269
6605
  }
6270
- const { saveWeixinRoutes } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6271
- const { readSkiffRoles } = await import("./skiff-role-LW4tjF9L.js").then((n) => n.c);
6606
+ const { saveWeixinRoutes } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6607
+ const { readSkiffRoles } = await import("./skiff-role-DlrbHPLD.js").then((n) => n.c);
6272
6608
  const roles = readSkiffRoles(root);
6273
6609
  for (const r of routes) if (!roles.has(r.role)) {
6274
6610
  sendJson$1(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
@@ -6280,7 +6616,7 @@ function registerStatusApi(ctx, opts = {}) {
6280
6616
  }
6281
6617
  if (body.action === "set-enabled") {
6282
6618
  const enabled = body.enabled === true;
6283
- const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6619
+ const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6284
6620
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6285
6621
  const settings = readWeixinSettings(root);
6286
6622
  if (enabled && (settings.accounts ?? []).length === 0) {
@@ -6327,7 +6663,7 @@ function registerStatusApi(ctx, opts = {}) {
6327
6663
  return;
6328
6664
  }
6329
6665
  const { pollQRStatus } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
6330
- const { readWeixinSettings } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6666
+ const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6331
6667
  const settings = readWeixinSettings(login.root);
6332
6668
  const status = await pollQRStatus({
6333
6669
  baseUrl: void 0,
@@ -6342,7 +6678,7 @@ function registerStatusApi(ctx, opts = {}) {
6342
6678
  });
6343
6679
  return;
6344
6680
  }
6345
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6681
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6346
6682
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6347
6683
  const accountId = nextWeixinAccountId(settings);
6348
6684
  upsertWeixinAccount(login.root, {
@@ -6859,6 +7195,38 @@ function workspaceDenyResponse(rpcId) {
6859
7195
  });
6860
7196
  }
6861
7197
  /**
7198
+ * 判定 workspace 创建请求的端点(v1.30.6,review F-03)。
7199
+ *
7200
+ * 宿主 rc.1 的 Remote 端点由 `typertEndpoint({namespace,method})` 派生为
7201
+ * `workspace/create`,因此浏览器实际请求 `POST /api/workspace/create`
7202
+ * (packages/typert/registry/src/service.ts:63-69;client/connection/src/client/rpc.ts:44)。
7203
+ * 旧实现只匹配 `/api/workspace.create` → 该分支从未命中,外部白名单与禁建校验失效。
7204
+ * 保留旧端点以兼容旧宿主。
7205
+ */
7206
+ function isWorkspaceCreatePath(pathname) {
7207
+ return pathname === "/api/workspace/create" || pathname === "/api/workspace.create";
7208
+ }
7209
+ /**
7210
+ * 从 workspace 创建请求体解析 `{ rpcId, path }`(v1.30.6,review F-03)。
7211
+ *
7212
+ * rc.1 wire 信封:`{ type:'client-request', rpcId, method:'workspace/create',
7213
+ * payload:{ args:{ path } } }`(packages/api/gateway/src/index.ts:955 `args: payload.args`);
7214
+ * 旧宿主为 `payload.path`。解析失败返回 rpcId='unknown' + path=undefined。
7215
+ */
7216
+ function parseWorkspaceCreateBody(body) {
7217
+ try {
7218
+ const parsed = JSON.parse(body);
7219
+ const rpcId = typeof parsed.rpcId === "string" ? parsed.rpcId : "unknown";
7220
+ const raw = parsed.payload?.args?.path ?? parsed.payload?.path;
7221
+ return typeof raw === "string" ? {
7222
+ rpcId,
7223
+ path: raw
7224
+ } : { rpcId };
7225
+ } catch {
7226
+ return { rpcId: "unknown" };
7227
+ }
7228
+ }
7229
+ /**
6862
7230
  * 在 HTML 的 </head> 前注入 polyfill(幂等:含 marker 则跳过)
6863
7231
  */
6864
7232
  function injectPolyfillHtml(html) {
@@ -7102,14 +7470,10 @@ function startGateway(config, getAccounts, getDshCookie) {
7102
7470
  }
7103
7471
  const session = authed(req);
7104
7472
  if (session !== void 0) {
7105
- if (req.method === "POST" && url.pathname === "/api/workspace.create") {
7106
- if (!allowWorkspaceCreate) {
7107
- readBody$1(req, 131072).then((body) => {
7108
- let rpcId = "unknown";
7109
- try {
7110
- const parsed = JSON.parse(body);
7111
- if (typeof parsed.rpcId === "string") rpcId = parsed.rpcId;
7112
- } catch {}
7473
+ if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
7474
+ readBody$1(req, 131072).then((body) => {
7475
+ const { rpcId, path } = parseWorkspaceCreateBody(body);
7476
+ if (!allowWorkspaceCreate) {
7113
7477
  res.writeHead(403, { "content-type": "application/json" });
7114
7478
  res.end(JSON.stringify({
7115
7479
  type: "server-response",
@@ -7123,31 +7487,8 @@ function startGateway(config, getAccounts, getDshCookie) {
7123
7487
  }
7124
7488
  }
7125
7489
  }));
7126
- }).catch(() => {
7127
- res.writeHead(400, { "content-type": "application/json" });
7128
- res.end(JSON.stringify({
7129
- type: "server-response",
7130
- rpcId: "unknown",
7131
- result: {
7132
- ok: false,
7133
- error: {
7134
- code: "bad-request",
7135
- message: "body too large",
7136
- details: {}
7137
- }
7138
- }
7139
- }));
7140
- });
7141
- return;
7142
- }
7143
- readBody$1(req, 131072).then((body) => {
7144
- let rpcId = "unknown";
7145
- let path;
7146
- try {
7147
- const parsed = JSON.parse(body);
7148
- if (typeof parsed.rpcId === "string") rpcId = parsed.rpcId;
7149
- if (typeof parsed.payload?.path === "string") path = parsed.payload.path;
7150
- } catch {}
7490
+ return;
7491
+ }
7151
7492
  if (!workspaceAllowed(allowWorkspaces, path)) {
7152
7493
  res.writeHead(403, { "content-type": "application/json" });
7153
7494
  res.end(workspaceDenyResponse(rpcId));
@@ -7372,7 +7713,7 @@ function buildDshCookieProvider(ctx, mainPort) {
7372
7713
  let diagnosed = false;
7373
7714
  return () => {
7374
7715
  try {
7375
- const connection = ctx.get?.("connection");
7716
+ const connection = hostService(ctx, "connection");
7376
7717
  if (!connection || typeof connection.authenticatedUrl !== "function" || typeof connection.authorizeIndex !== "function") {
7377
7718
  if (!diagnosed) {
7378
7719
  diagnosed = true;
@@ -7404,7 +7745,7 @@ function registerGateway(ctx) {
7404
7745
  let lastSig = null;
7405
7746
  const sync = () => {
7406
7747
  try {
7407
- const webServer = ctx.get?.("webServer");
7748
+ const webServer = hostWebServer(ctx);
7408
7749
  if (!webServer?.port) return;
7409
7750
  const enabled = readSimpleSettings().gatewayEnabled;
7410
7751
  const settings = readAdvancedSettings();
@@ -7455,6 +7796,11 @@ function registerGateway(ctx) {
7455
7796
  ctx.on("serenity/settings-changed", () => {
7456
7797
  sync();
7457
7798
  });
7799
+ registerDisposer(ctx, "gateway 第二监听器", () => {
7800
+ current?.dispose();
7801
+ current = null;
7802
+ lastSig = null;
7803
+ });
7458
7804
  }
7459
7805
  //#endregion
7460
7806
  //#region src/rebuild.ts
@@ -7702,7 +8048,7 @@ function renameAfterRebuild(ctx, agent, pending) {
7702
8048
  dirName,
7703
8049
  mdPath
7704
8050
  }, pending.summary);
7705
- const titles = ctx.get?.("sessionTitle");
8051
+ const titles = hostService(ctx, "sessionTitle");
7706
8052
  if (!titles || typeof titles.rename !== "function") {
7707
8053
  console.warn("[serenity-hooks] rebuild 后重命名跳过: sessionTitle 服务不可用");
7708
8054
  return;
@@ -7738,7 +8084,7 @@ function registerRebuildTurnHook(ctx) {
7738
8084
  }
7739
8085
  pendingRebuilds.delete(id);
7740
8086
  try {
7741
- const tokenMeter = ctx.get?.("tokenMeter");
8087
+ const tokenMeter = hostService(ctx, "tokenMeter");
7742
8088
  const meter = tokenMeter && typeof tokenMeter.estimateMessage === "function" ? { estimateMessage: (m) => tokenMeter.estimateMessage(m) } : void 0;
7743
8089
  if (performRebuild(agent.session, pending, meter)) {
7744
8090
  renameAfterRebuild(ctx, agent, pending);
@@ -8096,14 +8442,14 @@ var AcpServer = class {
8096
8442
  sessionId: result.sessionId
8097
8443
  };
8098
8444
  }
8099
- /** session/cancel:{ sessionId } → 中断当前 prompt(DSH interrupt) */
8445
+ /** session/cancel:{ sessionId } → 取消该会话的当前活动(Agent.cancel,宿主 rc.1 契约) */
8100
8446
  cancel(params) {
8101
8447
  const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
8102
8448
  if (!sessionId) return { cancelled: false };
8103
8449
  const agent = getSkiffAgent(sessionId);
8104
8450
  if (!agent) return { cancelled: false };
8105
8451
  try {
8106
- agent.interrupt?.();
8452
+ agent.cancel({ kind: "user" });
8107
8453
  } catch {}
8108
8454
  return { cancelled: true };
8109
8455
  }
@@ -8909,6 +9255,7 @@ function invokeWeixinHook(root, hookRel, event) {
8909
9255
  var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
8910
9256
  handleIncoming: () => handleIncoming,
8911
9257
  registerWeixinBridge: () => registerWeixinBridge,
9258
+ stopAllBridges: () => stopAllBridges,
8912
9259
  stopCccBridge: () => stopCccBridge,
8913
9260
  syncCccBridge: () => syncCccBridge,
8914
9261
  weixinBridgeStatus: () => weixinBridgeStatus
@@ -9188,6 +9535,11 @@ function stopCccBridge(root) {
9188
9535
  for (const loop of bridge.loops.values()) loop.stopped = true;
9189
9536
  bridges.delete(root);
9190
9537
  }
9538
+ /** 停止全部桥(插件 dispose) */
9539
+ function stopAllBridges() {
9540
+ for (const bridge of bridges.values()) for (const loop of bridge.loops.values()) loop.stopped = true;
9541
+ bridges.clear();
9542
+ }
9191
9543
  /** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
9192
9544
  function weixinBridgeStatus() {
9193
9545
  const out = [];
@@ -9208,7 +9560,7 @@ function weixinBridgeStatus() {
9208
9560
  function registerWeixinBridge(ctx) {
9209
9561
  const syncFromLive = () => {
9210
9562
  try {
9211
- const cwds = (ctx.sessions?.list?.() ?? []).map((s) => s.header?.cwd ?? "").filter(Boolean);
9563
+ const cwds = (hostSessions(ctx)?.list?.() ?? []).map((s) => s.header?.cwd ?? "").filter(Boolean);
9212
9564
  const roots = [...new Set(cwds.map((c) => findSerenityRoot(c))).values()].filter((r) => r !== null);
9213
9565
  for (const root of roots) syncCccBridge(ctx, root);
9214
9566
  } catch {}
@@ -9219,6 +9571,53 @@ function registerWeixinBridge(ctx) {
9219
9571
  } catch {}
9220
9572
  }
9221
9573
  //#endregion
9574
+ //#region src/seams/lifecycle.ts
9575
+ /** 从 Agent / Session / 事件负载中取会话 id(形状宽容:未知结构返回 null) */
9576
+ function sessionIdOf(value) {
9577
+ const v = value;
9578
+ const id = v?.session?.id ?? v?.id ?? v?.header?.id;
9579
+ return typeof id === "string" && id !== "" ? id : null;
9580
+ }
9581
+ /** 单会话内存态清理(幂等;无对应状态即 no-op) */
9582
+ function cleanupSessionState(sessionId) {
9583
+ try {
9584
+ unregisterSkiffSession(sessionId);
9585
+ } catch {}
9586
+ try {
9587
+ clearActiveSessionInfo(sessionId);
9588
+ } catch {}
9589
+ }
9590
+ /**
9591
+ * 装配生命周期订阅与资源拆卸。
9592
+ * @param ctx 宿主插件上下文
9593
+ */
9594
+ function registerLifecycle(ctx) {
9595
+ try {
9596
+ ctx.on("agent/disposed", (payload) => {
9597
+ const id = sessionIdOf(payload?.agent);
9598
+ if (id) cleanupSessionState(id);
9599
+ });
9600
+ } catch {}
9601
+ try {
9602
+ ctx.on("session/disposed", (session) => {
9603
+ const id = sessionIdOf(session);
9604
+ if (id) cleanupSessionState(id);
9605
+ });
9606
+ } catch {}
9607
+ const disposeAll = () => {
9608
+ try {
9609
+ stopSkiffDebugServer();
9610
+ } catch {}
9611
+ try {
9612
+ stopAcpHttpServer();
9613
+ } catch {}
9614
+ try {
9615
+ stopAllBridges();
9616
+ } catch {}
9617
+ };
9618
+ registerDisposer(ctx, "self-started resources (skiff-debug/acp/weixin)", disposeAll);
9619
+ }
9620
+ //#endregion
9222
9621
  //#region src/index.ts
9223
9622
  const name = "dsh-serenity-hooks";
9224
9623
  /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
@@ -9262,10 +9661,14 @@ const Config = z.object({
9262
9661
  })
9263
9662
  });
9264
9663
  function apply(ctx, config) {
9664
+ try {
9665
+ const report = probeHostContract(ctx, readDshVersion());
9666
+ if (report.issues.length > 0) console.warn(`[serenity-hooks] ${summarizeHostContract(report)}`);
9667
+ } catch {}
9265
9668
  if (config.tools) {
9266
9669
  ctx.tools.register(ccFsTool);
9267
9670
  ctx.tools.register(createSessionTool(ctx));
9268
- ctx.tools.register(kitTool);
9671
+ ctx.tools.register(createKitTool(ctx));
9269
9672
  ctx.tools.register(gitTool);
9270
9673
  ctx.tools.register(msmTool);
9271
9674
  ctx.tools.register(praxisTool);
@@ -9300,6 +9703,7 @@ function apply(ctx, config) {
9300
9703
  registerAcp(ctx);
9301
9704
  registerAutopilot(ctx);
9302
9705
  registerWeixinBridge(ctx);
9706
+ registerLifecycle(ctx);
9303
9707
  }
9304
9708
  /**
9305
9709
  * F4 Skiff 调试服务装配:启停 = 人工(设置面板 Skiff 区块开关,settings 持久化)。
@@ -9341,7 +9745,7 @@ function registerSkiff(ctx) {
9341
9745
  function resolveSkiffRoot(ctx) {
9342
9746
  const liveRoots = [];
9343
9747
  try {
9344
- const sessions = ctx.sessions;
9748
+ const sessions = hostSessions(ctx);
9345
9749
  for (const s of sessions?.list?.() ?? []) {
9346
9750
  const cwd = s?.header?.cwd;
9347
9751
  if (typeof cwd === "string") {
@@ -9358,7 +9762,7 @@ function resolveSkiffRoot(ctx) {
9358
9762
  /** 主 WebUI 端口(WebUI 链接;webServer 未装配回退 3080) */
9359
9763
  function readWebPort(ctx) {
9360
9764
  try {
9361
- const ws = ctx.webServer;
9765
+ const ws = hostWebServer(ctx);
9362
9766
  return typeof ws?.port === "number" ? ws.port : 3080;
9363
9767
  } catch {
9364
9768
  return 3080;