@shgroup/dsh-serenity-hooks 1.29.1 → 1.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.29.1",
3
+ "version": "1.29.2",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/handyman/session_rebuild/skiff_admin + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)+ Skiff 认知子集角色(实验性)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/index.js CHANGED
@@ -4015,15 +4015,19 @@ function stripAckSuffix(text) {
4015
4015
  /**
4016
4016
  * 构建重建锚点消息(v1.22.4 定稿语义 + v1.22.5 保留 first-anchor 正文):
4017
4017
  * 「[TRAJECTORY-REBUILD] + first-anchor 协议正文(去 acknowledge 尾句)+ 继续 {SESSION 名} 的工作」。
4018
+ * v1.29.2(R1):可选 task focus 段——简短任务焦点文字传入重建后会话(不含历史,
4019
+ * 历史完整在 SESSION.md;让重建后的自己第一时间理解本轮焦点,降低冷启动认知成本)。
4018
4020
  * @param root - CCC 根
4019
4021
  * @param sessionName - 当前 use 的宁静号 SESSION 名(如 S142);无激活则通用指令
4020
4022
  * @param activeMdPath - 持久轨迹 SESSION.md 绝对路径(保持原位)
4021
4023
  * @param anchorMessages - first-anchor 协议消息序列(缺省 DEFAULT_ANCHOR_MESSAGES;可注入测试)
4024
+ * @param focus - 可选任务焦点(单行化 + ≤200 字消毒;无则不输出 focus 段——向后兼容)
4022
4025
  */
4023
- function buildRebuildAnchor(root, sessionName, activeMdPath, anchorMessages = DEFAULT_ANCHOR_MESSAGES) {
4026
+ function buildRebuildAnchor(root, sessionName, activeMdPath, anchorMessages = DEFAULT_ANCHOR_MESSAGES, focus) {
4024
4027
  const rel = activeMdPath.startsWith(root) ? activeMdPath.slice(root.length + 1) : activeMdPath;
4025
4028
  const sessionDir = basename(dirname(activeMdPath));
4026
4029
  const sessionLine = sessionDir !== "AGENT_SESSIONS" ? `- Serenity session: ${sessionName !== "" ? `${sessionName} (${sessionDir})` : sessionDir}` : null;
4030
+ const focusLine = sanitizeFocusLine(focus);
4027
4031
  return [
4028
4032
  `${eventToken("rebuild")} The conversation has been cleared and rebuilt (Ship of Theseus: the carrier is replaced, the trajectory continues).`,
4029
4033
  "",
@@ -4032,9 +4036,22 @@ function buildRebuildAnchor(root, sessionName, activeMdPath, anchorMessages = DE
4032
4036
  ...sessionLine ? [sessionLine] : [],
4033
4037
  `- Persistent trajectory — SESSION.md path: ${rel}`,
4034
4038
  ` (the trajectory's persistent body — stays in place through rebuilds)`,
4035
- `- Read that SESSION.md first (goal/decisions/progress/unresolved), then continue from the last checkpoint.`
4039
+ `- Read that SESSION.md first (goal/decisions/progress/unresolved), then continue from the last checkpoint.`,
4040
+ ...focusLine ? [`- Task focus: ${focusLine}`] : []
4036
4041
  ].join("\n");
4037
4042
  }
4043
+ /**
4044
+ * 任务焦点行消毒(纯函数可单测):单行化(换行/回车 → 空格)+ 控制字符清除 +
4045
+ * ≤200 字截断。空/纯空白 → null(不输出 focus 段)。防 LLM 传多行注入伪造锚点结构。
4046
+ */
4047
+ function sanitizeFocusLine(focus) {
4048
+ if (!focus) return null;
4049
+ const single = focus.replace(/[\r\n\t]+/g, " ").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").trim();
4050
+ if (single === "") return null;
4051
+ return single.length <= FOCUS_MAX_CHARS ? single : single.slice(0, FOCUS_MAX_CHARS);
4052
+ }
4053
+ /** 任务焦点最大长度(字/码点;200 足够一句话任务焦点——非历史) */
4054
+ const FOCUS_MAX_CHARS = 200;
4038
4055
  /** 从会话 surface 首条 user 消息(重建锚点)解析 SESSION.md 路径(events 异常/缺失时的兜底) */
4039
4056
  function parseAnchorMdPath(session) {
4040
4057
  try {
@@ -4121,7 +4138,8 @@ async function queueRebuild(ctx, opts) {
4121
4138
  const mdPath = resolveSessionMdPath(root, dshSessionId, session);
4122
4139
  if (!mdPath) throw new Error("Unable to determine the active SESSION.md — no session context found in this conversation. Run \"session use <S###> --summary <内容概括 ≤20 字>\" first to activate the trajectory to resume, then retry session_rebuild.");
4123
4140
  const sessionName = getActiveSessionInfo(dshSessionId)?.sessionId ?? sessionNameFromMdPath(mdPath);
4124
- const anchor = buildRebuildAnchor(root, sessionName, mdPath);
4141
+ const focus = note && note.trim() !== "" ? note : void 0;
4142
+ const anchor = buildRebuildAnchor(root, sessionName, mdPath, DEFAULT_ANCHOR_MESSAGES, focus);
4125
4143
  appendBound(session, "rebuild", {
4126
4144
  dirName: basename(dirname(mdPath)),
4127
4145
  mdPath,
@@ -4132,6 +4150,7 @@ async function queueRebuild(ctx, opts) {
4132
4150
  anchor,
4133
4151
  summary,
4134
4152
  mdPath,
4153
+ focus,
4135
4154
  queuedAt: Date.now()
4136
4155
  });
4137
4156
  writeRebuildDiag(root, {
@@ -4323,7 +4342,7 @@ function createRebuildTool(ctx) {
4323
4342
  parameters: {
4324
4343
  note: {
4325
4344
  type: "string",
4326
- description: "Optional: one-sentence rebuild background note (for the rebuilt self)"
4345
+ description: "Optional: task focus ≤200 chars for the rebuilt self — what to work on next (short, no history; SESSION.md holds the full history). Injected as \"- Task focus: …\" into the rebuild anchor."
4327
4346
  },
4328
4347
  summary: {
4329
4348
  type: "string",
@@ -4951,25 +4970,60 @@ function registerAutopilot(ctx) {
4951
4970
  ctx.on("serenity/settings-changed", () => startTimer());
4952
4971
  } catch {}
4953
4972
  }
4954
- /** 目标 agent:从 SESSION.md 反向定位 dsh 会话(标题 F3 命名 S###-日期 匹配 + cwd 归属校验);不可得 → null */
4973
+ /**
4974
+ * 目标 agent:从 SESSION.md 反向定位 dsh 会话(cwd 归属校验 + **bound 精确匹配优先**,
4975
+ * 标题 F3 命名回退);不可得 → null。
4976
+ *
4977
+ * v1.29.2(R2,用户"autopilot 会话绑定应当更稳固——唤起的时候能唤起最新的、
4978
+ * 绑定 autopilot SESSION 的会话"):v1.29.1 `serenity/bound` 事件是权威绑定
4979
+ * (session use 激活时 append,dirName = 完整 AGENT_SESSIONS 目录名,编码无关 U4)。
4980
+ * 旧实现只按标题猜(=== sid / startsWith(sid-))——若绑定该 SESSION 的 dsh 会话
4981
+ * 标题不是 S###- 前缀(LLM 改过/重建后 rename 异常),绑定明明在却唤起失败。
4982
+ * 加强:**bound.dirName 精确匹配优先**(权威证据),标题匹配降级为回退(存量
4983
+ * 无 bound 的 live 会话兼容);多候选(罕见:同 SESSION 绑定多个 live 会话)取
4984
+ * **绑定最新**(bound.at 最大——最近 use 过 = 最可能当前在用)。
4985
+ */
4955
4986
  function resolveTargetAgent(ctx, mdPath) {
4956
- const idMatch = basename(dirname(mdPath)).match(/--S(\d{3,})--/);
4987
+ const dirName = basename(dirname(mdPath));
4988
+ const idMatch = dirName.match(/--S(\d{3,})--/);
4957
4989
  const sid = idMatch ? `S${idMatch[1]}` : null;
4958
4990
  const targetRoot = findSerenityRoot(mdPath);
4991
+ const sessions = ctx.sessions;
4992
+ const agents = ctx.agents;
4993
+ const candidates = [];
4959
4994
  try {
4960
- const sessions = ctx.sessions;
4961
4995
  for (const s of sessions?.list?.() ?? []) {
4962
- const cwd = s?.header?.cwd ?? "";
4996
+ const sess = s;
4997
+ const cwd = sess?.header?.cwd ?? "";
4963
4998
  if (targetRoot) {
4964
4999
  if (findSerenityRoot(cwd) !== targetRoot) continue;
4965
5000
  } else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
4966
- const title = readSessionTitle(s);
4967
- if (sid && title && (title === sid || title.startsWith(`${sid}-`))) {
4968
- const agent = ctx.agents?.get?.(s.id ?? "");
4969
- if (agent) return agent;
4970
- }
5001
+ const bound = readLastBound(sess);
5002
+ const title = readSessionTitle(sess);
5003
+ const boundMatched = bound !== null && bound.dirName === dirName;
5004
+ const titleMatched = sid !== null && title !== null && (title === sid || title.startsWith(`${sid}-`));
5005
+ if (boundMatched) candidates.push({
5006
+ session: sess,
5007
+ boundAt: bound?.at ?? null,
5008
+ titleMatched
5009
+ });
5010
+ else if (titleMatched) candidates.push({
5011
+ session: sess,
5012
+ boundAt: null,
5013
+ titleMatched
5014
+ });
4971
5015
  }
4972
5016
  } catch {}
5017
+ if (candidates.length === 0) return null;
5018
+ candidates.sort((a, b) => {
5019
+ const aBound = a.boundAt !== null;
5020
+ if (aBound !== (b.boundAt !== null)) return aBound ? -1 : 1;
5021
+ return (b.boundAt ?? 0) - (a.boundAt ?? 0);
5022
+ });
5023
+ for (const c of candidates) {
5024
+ const agent = agents?.get?.(c.session.id ?? "");
5025
+ if (agent) return agent;
5026
+ }
4973
5027
  return null;
4974
5028
  }
4975
5029
  /**
@@ -4989,31 +5043,38 @@ function readSessionTitle(session) {
4989
5043
  }
4990
5044
  /**
4991
5045
  * 诊断:目标会话 agent 为何不可得(供 performAutopilotWake 失败信息——
4992
- * 区分"live 会话里无匹配" vs "匹配但 agent 未加载")。返回诊断文本(无则 null)。
5046
+ * 区分"live 会话" / "标题不匹配" / "bound 或标题命中但 agent 未加载")。
5047
+ * v1.29.2(R2):bound 命中但 agent 未加载也归入「已匹配未加载」(原只查标题)。
5048
+ * 返回诊断文本(无则 null)。
4993
5049
  */
4994
5050
  function diagnoseTargetUnavailable(ctx, mdPath) {
4995
- const idMatch = basename(dirname(mdPath)).match(/--S(\d{3,})--/);
5051
+ const dirName = basename(dirname(mdPath));
5052
+ const idMatch = dirName.match(/--S(\d{3,})--/);
4996
5053
  const sid = idMatch ? `S${idMatch[1]}` : null;
4997
5054
  const targetRoot = findSerenityRoot(mdPath);
4998
5055
  const sameCccTitles = [];
4999
- const agentMissing = { sid: false };
5056
+ const agentMissing = { matched: false };
5000
5057
  try {
5001
5058
  const sessions = ctx.sessions;
5002
5059
  for (const s of sessions?.list?.() ?? []) {
5003
- const cwd = s?.header?.cwd ?? "";
5060
+ const sess = s;
5061
+ const cwd = sess?.header?.cwd ?? "";
5004
5062
  if (targetRoot) {
5005
5063
  if (findSerenityRoot(cwd) !== targetRoot) continue;
5006
5064
  } else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
5007
- const title = readSessionTitle(s);
5065
+ const bound = readLastBound(sess);
5066
+ const title = readSessionTitle(sess);
5008
5067
  if (title) sameCccTitles.push(title);
5009
- if (sid && title && (title === sid || title.startsWith(`${sid}-`))) {
5010
- if (!ctx.agents?.get?.(s.id ?? "")) agentMissing.sid = true;
5068
+ const boundMatched = bound !== null && bound.dirName === dirName;
5069
+ const titleMatched = sid !== null && title !== null && (title === sid || title.startsWith(`${sid}-`));
5070
+ if (boundMatched || titleMatched) {
5071
+ if (!ctx.agents?.get?.(sess.id ?? "")) agentMissing.matched = true;
5011
5072
  }
5012
5073
  }
5013
5074
  } catch {}
5014
- if (agentMissing.sid) return `会话 ${sid} 已打开但 agent 未加载(会话可能刚创建/正在恢复——稍后重试)`;
5015
- if (sameCccTitles.length > 0) return `目标 CCC 内 live 会话标题: [${sameCccTitles.join(", ")}]——均不匹配 ${sid}(目标会话未在 WebUI 打开,或命名未生效)`;
5016
- return `目标 CCC 内无 live 会话(先在 WebUI 打开 ${sid} 会话后重试)`;
5075
+ if (agentMissing.matched) return `会话已绑定/匹配 ${sid ?? dirName} agent 未加载(会话可能刚创建/正在恢复——稍后重试)`;
5076
+ if (sameCccTitles.length > 0) return `目标 CCC 内 live 会话标题: [${sameCccTitles.join(", ")}]——均不匹配 ${sid ?? dirName}(目标会话未在 WebUI 打开,或绑定/命名未生效)`;
5077
+ return `目标 CCC 内无 live 会话(先在 WebUI 打开 ${sid ?? dirName} 会话后重试)`;
5017
5078
  }
5018
5079
  /** Autopilot 绑定的回退 CCC 根:进程 cwd 上溯 .serenity 优先,回退任一 live 会话 root */
5019
5080
  function resolveAutopilotRoot(ctx) {
package/lib/rebuild.d.ts CHANGED
@@ -45,12 +45,20 @@ export declare function stripAckSuffix(text: string): string;
45
45
  /**
46
46
  * 构建重建锚点消息(v1.22.4 定稿语义 + v1.22.5 保留 first-anchor 正文):
47
47
  * 「[TRAJECTORY-REBUILD] + first-anchor 协议正文(去 acknowledge 尾句)+ 继续 {SESSION 名} 的工作」。
48
+ * v1.29.2(R1):可选 task focus 段——简短任务焦点文字传入重建后会话(不含历史,
49
+ * 历史完整在 SESSION.md;让重建后的自己第一时间理解本轮焦点,降低冷启动认知成本)。
48
50
  * @param root - CCC 根
49
51
  * @param sessionName - 当前 use 的宁静号 SESSION 名(如 S142);无激活则通用指令
50
52
  * @param activeMdPath - 持久轨迹 SESSION.md 绝对路径(保持原位)
51
53
  * @param anchorMessages - first-anchor 协议消息序列(缺省 DEFAULT_ANCHOR_MESSAGES;可注入测试)
54
+ * @param focus - 可选任务焦点(单行化 + ≤200 字消毒;无则不输出 focus 段——向后兼容)
52
55
  */
53
- export declare function buildRebuildAnchor(root: string, sessionName: string, activeMdPath: string, anchorMessages?: string[]): string;
56
+ export declare function buildRebuildAnchor(root: string, sessionName: string, activeMdPath: string, anchorMessages?: string[], focus?: string | null): string;
57
+ /**
58
+ * 任务焦点行消毒(纯函数可单测):单行化(换行/回车 → 空格)+ 控制字符清除 +
59
+ * ≤200 字截断。空/纯空白 → null(不输出 focus 段)。防 LLM 传多行注入伪造锚点结构。
60
+ */
61
+ export declare function sanitizeFocusLine(focus: string | null | undefined): string | null;
54
62
  export interface RebuildResult {
55
63
  /** 是否成功排队(turn 结束时执行清空重建) */
56
64
  queued: boolean;
@@ -73,6 +81,8 @@ interface PendingRebuild {
73
81
  summary: string;
74
82
  /** 持久轨迹 SESSION.md(重建后重命名标题的编号/日期来源) */
75
83
  mdPath: string;
84
+ /** v1.29.2(R1):任务焦点(可选——重建后会话理解当前任务;不含历史) */
85
+ focus?: string;
76
86
  /** 排队时间(防陈旧队列误清空——超时丢弃) */
77
87
  queuedAt: number;
78
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.29.1",
3
+ "version": "1.29.2",
4
4
  "description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
5
5
  "license": "MIT",
6
6
  "repository": {