@shgroup/dsh-serenity-hooks 1.48.1 → 1.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.48.1",
3
+ "version": "1.49.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag;后两个按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.5-rc.2(deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/client.js CHANGED
@@ -4741,7 +4741,7 @@ Instruction-following style:
4741
4741
  })
4742
4742
  ]
4743
4743
  }),
4744
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4744
+ control !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4745
4745
  className: "ss-rowControl",
4746
4746
  children: control
4747
4747
  }),
@@ -4804,7 +4804,6 @@ Instruction-following style:
4804
4804
  set("acpHttpPort", Math.min(65535, Math.max(1024, Math.round(v))));
4805
4805
  };
4806
4806
  const gatewayOn = value?.gatewayEnabled ?? false;
4807
- const rebuildOn = value?.rebuildEnabled ?? true;
4808
4807
  const thresholdK = value?.rebuildThresholdK ?? 400;
4809
4808
  const skiffOn = value?.skiffEnabled ?? false;
4810
4809
  const skiffPort = value?.skiffDebugPort ?? 3099;
@@ -4862,19 +4861,15 @@ Instruction-following style:
4862
4861
  open: openRebuild,
4863
4862
  onToggle: setOpenRebuild,
4864
4863
  expandHint: "配置",
4865
- control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
4866
- checked: rebuildOn,
4867
- onChange: (on) => toggle("rebuildEnabled", on)
4868
- }),
4869
4864
  detail: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4870
4865
  className: "ss-detailStack",
4871
4866
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4872
4867
  className: "ss-detailIntro",
4873
- children: "上下文超限自动重建(container_trajectory rebuild,v1.33 起 logbook 并入 trajectory):\n· 机制:agent 上下文占用达到阈值时,由 LLM 主动调用重建\n· 语义:完全丢弃当前 dsh 会话 + 自动新建 + 注入「继续原 SESSION 的工作」\n· 效果:SESSION.md 原位不动,认知轨迹延续,上下文归零\n· 关闭后:上下文超限时不再提示,可能导致会话卡顿或丢失"
4868
+ children: "上下文超限自动重建(container_trajectory rebuild,v1.33 起 logbook 并入 trajectory):\n· 机制:agent 上下文占用达到阈值时,由 LLM 主动调用重建\n· 语义:完全丢弃当前 dsh 会话 + 自动新建 + 注入「继续原 SESSION 的工作」\n· 效果:SESSION.md 原位不动,认知轨迹延续,上下文归零\n· 无总开关(v1.49.0 砍掉):长期验证下它已能很好地代替上下文压缩;\n 要调敏感度就改下面的阈值"
4874
4869
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
4875
4870
  title: "重建阈值",
4876
4871
  desc: "上下文占用达到该 K 数值时提示(单位 K token,默认 400)",
4877
- help: "重建触发阈值(需求①:K 数值,默认 400K,范围 50~4000K):\n· 含义:上下文 projected tokens 达到 thresholdK × 1000 时触发提醒\n· 纯绝对数值(不再依赖窗口比例)\n· 迁移:旧版 0~1 比例(settings.yaml rebuildThreshold)已废弃被忽略\n· 调低更早触发 / 调高更晚触发;仅「超限重建」开启时有意义",
4872
+ help: "重建触发阈值(需求①:K 数值,默认 400K,范围 50~4000K):\n· 含义:上下文 projected tokens 达到 thresholdK × 1000 时触发提醒\n· 纯绝对数值(不再依赖窗口比例)\n· 迁移:旧版 0~1 比例(settings.yaml rebuildThreshold)已废弃被忽略\n· 调低更早触发 / 调高更晚触发(v1.49.0 起无总开关,它恒生效)",
4878
4873
  control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4879
4874
  className: "ss-portInput",
4880
4875
  type: "number",
@@ -4882,7 +4877,6 @@ Instruction-following style:
4882
4877
  max: 4e3,
4883
4878
  step: 50,
4884
4879
  value: thresholdK,
4885
- disabled: !rebuildOn,
4886
4880
  title: "重建阈值(K token,默认 400)",
4887
4881
  onChange: (e) => setThreshold(Number(e.target.value))
4888
4882
  })
package/lib/git-ops.d.ts CHANGED
@@ -2,13 +2,23 @@
2
2
  * git-ops.ts — container_git 纯操作层(cc_git → container_git,v1.30;零 DSH 依赖)
3
3
  *
4
4
  * 行为对齐 osp(opencode-serenity-plugin/src/git/cc-git-tool.ts)——osp 是 ACC 工具 spec:
5
- * - status:JSON {clean, files:[{status,file}], summary}
6
- * - commit:git add -A + commit -m;clean 返回 '(nothing to commit — working tree clean)'
7
- * - push:无 remote 报错;非快进 → [REJECTED] + 操作建议(绝不自动 force)
5
+ * - status:JSON {clean, files:[{status,file}], summary, ahead, behind}
6
+ * - commit:默认「只提交指定 paths」;未给 paths ⇒ 整仓 add -A(并在回执里显式告知)
7
+ * - commit:**默认随后 push**(`noPush` 可关);push 结果如实附在回执里
8
+ * - push:成功判据 = **`git push --porcelain` 的 stdout 里读到 ref 行**(绝不采信"stderr 为空")
8
9
  * - pull:git fetch + merge --ff-only;up-to-date / [REJECTED] 建议
9
10
  * - log:--oneline [-n <count>]
10
11
  * - diff:git diff [--cached] [<ref>] [-- <path>]
11
12
  * 保留 dsp 增强:localstore git 合规联动(S134:deny 且 .gitignore 未覆盖 → status 提示 / commit 拒绝)。
13
+ *
14
+ * 🔴 2026-09-26 修(tiangong-serenity 报障):**push 曾把"超时被杀"报成成功**。
15
+ * 机制(**实测**,非推断):`spawnSync` 超时**不设 `err.killed`**(实测 `code=ETIMEDOUT` /
16
+ * `signal=SIGTERM` / `status=null` / `stderr` 长度 0),而 `execFileSync` **成功时丢弃 stderr**;
17
+ * 于是"超时被杀"与"真成功"都交出 `{stdout:'', stderr:''}` —— 旧判据 `if (!stderr) → 成功`
18
+ * 把两者判成同一件事,回显 `Pushed to origin/<branch>` 而远程 ref 从未变。
19
+ * ⇒ 本版三处硬修:① `git()` 交出**结构化结果**(ok/timedOut/status),守卫一律看它,不再看
20
+ * "stderr 是否为空";② push 的成功判据改为**可核验事实**(`--porcelain` 的 ref 行);
21
+ * ③ 超时/未确认一律**不报成功**,并给出 `git ls-remote` 核验指令。
12
22
  */
13
23
  import type { JsonValue } from './json.js';
14
24
  type GitAction = 'status' | 'commit' | 'push' | 'log' | 'pull' | 'diff';
@@ -16,6 +26,10 @@ export declare const GIT_ACTIONS: readonly GitAction[];
16
26
  interface GitArgs {
17
27
  action: GitAction;
18
28
  message?: string;
29
+ /** commit:只提交这些路径(不传 ⇒ 整仓 `add -A`) */
30
+ paths?: string[];
31
+ /** commit:只提交到本地、不随后推送(默认提交后自动推送) */
32
+ noPush?: boolean;
19
33
  /** log 条数(对齐 osp 参数 n,默认 10,max 100) */
20
34
  count?: number;
21
35
  /** diff: 显示暂存区变更(--cached) */
package/lib/index.d.ts CHANGED
@@ -121,10 +121,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
121
121
  enabled: z<boolean, boolean, "defined">;
122
122
  }>>, "plain">;
123
123
  rebuild: z<Schemastery.ObjectS<NoInfer<{
124
- enabled: z<boolean, boolean, "defined">;
125
124
  thresholdK: z<number, number, "defined">;
126
125
  }>>, Schemastery.ObjectT<NoInfer<{
127
- enabled: z<boolean, boolean, "defined">;
128
126
  thresholdK: z<number, number, "defined">;
129
127
  }>>, "plain">;
130
128
  skiff: z<Schemastery.ObjectS<NoInfer<{
@@ -157,7 +155,6 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
157
155
  enabled: z<boolean, boolean, "defined">;
158
156
  }>>, "plain">;
159
157
  gatewayEnabled: z<boolean, boolean, "volatile">;
160
- rebuildEnabled: z<boolean, boolean, "volatile">;
161
158
  rebuildThresholdK: z<number, number, "volatile">;
162
159
  skiffEnabled: z<boolean, boolean, "volatile">;
163
160
  skiffDebugPort: z<number, number, "volatile">;
@@ -184,10 +181,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
184
181
  enabled: z<boolean, boolean, "defined">;
185
182
  }>>, "plain">;
186
183
  rebuild: z<Schemastery.ObjectS<NoInfer<{
187
- enabled: z<boolean, boolean, "defined">;
188
184
  thresholdK: z<number, number, "defined">;
189
185
  }>>, Schemastery.ObjectT<NoInfer<{
190
- enabled: z<boolean, boolean, "defined">;
191
186
  thresholdK: z<number, number, "defined">;
192
187
  }>>, "plain">;
193
188
  skiff: z<Schemastery.ObjectS<NoInfer<{
@@ -220,7 +215,6 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
220
215
  enabled: z<boolean, boolean, "defined">;
221
216
  }>>, "plain">;
222
217
  gatewayEnabled: z<boolean, boolean, "volatile">;
223
- rebuildEnabled: z<boolean, boolean, "volatile">;
224
218
  rebuildThresholdK: z<number, number, "volatile">;
225
219
  skiffEnabled: z<boolean, boolean, "volatile">;
226
220
  skiffDebugPort: z<number, number, "volatile">;
package/lib/index.js CHANGED
@@ -6,7 +6,7 @@ import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSe
6
6
  import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-CWMRa36W.js";
7
7
  import { a as unregisterSkiffSession$1, i as skiffSessionSnapshot$1, n as skiffRoleFor$1, o as ACC_VERSION, r as skiffSessionInfo$1, t as registerSkiffSession$1 } from "./skiff-registry-BjIX_KD1.js";
8
8
  import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-KqQA6pkr.js";
9
- import { a as FACE_PORTS, c as MECHANISM_PORTS, i as ACP_HTTP_PORT, l as SKIFF_DEBUG_PORT, n as registerSettingsSection, o as GATEWAY_PORT, s as MAIN_WEB_PORT, t as readSimpleSettings, u as WEIXIN_SEND_PORT } from "./settings-section-DxMwlCv9.js";
9
+ import { a as FACE_PORTS, c as MECHANISM_PORTS, i as ACP_HTTP_PORT, l as SKIFF_DEBUG_PORT, n as registerSettingsSection, o as GATEWAY_PORT, s as MAIN_WEB_PORT, t as readSimpleSettings, u as WEIXIN_SEND_PORT } from "./settings-section-B-KOyOYn.js";
10
10
  import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-CtFYm53S.js";
11
11
  import z from "@deepseek-ai/schemastery";
12
12
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -611,13 +611,23 @@ function createKitTool(ctx) {
611
611
  * git-ops.ts — container_git 纯操作层(cc_git → container_git,v1.30;零 DSH 依赖)
612
612
  *
613
613
  * 行为对齐 osp(opencode-serenity-plugin/src/git/cc-git-tool.ts)——osp 是 ACC 工具 spec:
614
- * - status:JSON {clean, files:[{status,file}], summary}
615
- * - commit:git add -A + commit -m;clean 返回 '(nothing to commit — working tree clean)'
616
- * - push:无 remote 报错;非快进 → [REJECTED] + 操作建议(绝不自动 force)
614
+ * - status:JSON {clean, files:[{status,file}], summary, ahead, behind}
615
+ * - commit:默认「只提交指定 paths」;未给 paths ⇒ 整仓 add -A(并在回执里显式告知)
616
+ * - commit:**默认随后 push**(`noPush` 可关);push 结果如实附在回执里
617
+ * - push:成功判据 = **`git push --porcelain` 的 stdout 里读到 ref 行**(绝不采信"stderr 为空")
617
618
  * - pull:git fetch + merge --ff-only;up-to-date / [REJECTED] 建议
618
619
  * - log:--oneline [-n <count>]
619
620
  * - diff:git diff [--cached] [<ref>] [-- <path>]
620
621
  * 保留 dsp 增强:localstore git 合规联动(S134:deny 且 .gitignore 未覆盖 → status 提示 / commit 拒绝)。
622
+ *
623
+ * 🔴 2026-09-26 修(tiangong-serenity 报障):**push 曾把"超时被杀"报成成功**。
624
+ * 机制(**实测**,非推断):`spawnSync` 超时**不设 `err.killed`**(实测 `code=ETIMEDOUT` /
625
+ * `signal=SIGTERM` / `status=null` / `stderr` 长度 0),而 `execFileSync` **成功时丢弃 stderr**;
626
+ * 于是"超时被杀"与"真成功"都交出 `{stdout:'', stderr:''}` —— 旧判据 `if (!stderr) → 成功`
627
+ * 把两者判成同一件事,回显 `Pushed to origin/<branch>` 而远程 ref 从未变。
628
+ * ⇒ 本版三处硬修:① `git()` 交出**结构化结果**(ok/timedOut/status),守卫一律看它,不再看
629
+ * "stderr 是否为空";② push 的成功判据改为**可核验事实**(`--porcelain` 的 ref 行);
630
+ * ③ 超时/未确认一律**不报成功**,并给出 `git ls-remote` 核验指令。
621
631
  */
622
632
  const GIT_ACTIONS = [
623
633
  "status",
@@ -627,48 +637,132 @@ const GIT_ACTIONS = [
627
637
  "pull",
628
638
  "diff"
629
639
  ];
630
- /** git 操作超时(ms)——网络路径(push/pull/fetch)可能挂起(GCM 弹认证框等),
631
- * 无 timeout 会冻结 Node 事件循环 / DSH 3080 server(Windows 审计问题 12) */
632
- const GIT_TIMEOUT_MS = 3e4;
633
- function git(root, args) {
640
+ /** 本地动作超时(ms) */
641
+ const DEFAULT_LOCAL_TIMEOUT_MS = 3e4;
642
+ /** 网络动作(fetch/push/pull)超时(ms)——大体量推送可能远超 30s(tiangong 实例:>60s) */
643
+ const DEFAULT_NET_TIMEOUT_MS = 18e4;
644
+ /**
645
+ * 超时可覆盖(`DSH_localTimeout()` / `DSH_netTimeout()`)。
646
+ * 🔵 两个用途:① 慢远端/大体量推送的用户可自行放宽 ② 测试可在**不真等 180s** 的前提下
647
+ * 驱动"超时"这条路径(本仓对它的回归测试就靠这个缝)。
648
+ * ⚠️ **每次调用现读**(不在模块加载时固化)——否则测试改了 env 也影响不到已加载的模块。
649
+ */
650
+ function timeoutMs(envName, dflt) {
651
+ const v = Number(process.env[envName] ?? "");
652
+ return Number.isFinite(v) && v > 0 ? v : dflt;
653
+ }
654
+ const localTimeout = () => timeoutMs("DSH_GIT_TIMEOUT_MS", DEFAULT_LOCAL_TIMEOUT_MS);
655
+ const netTimeout = () => timeoutMs("DSH_GIT_NET_TIMEOUT_MS", DEFAULT_NET_TIMEOUT_MS);
656
+ /**
657
+ * 执行 git 并交出**结构化结果**。
658
+ * 🔴 超时判据必须用 `code==='ETIMEDOUT' || signal!=null && status==null`——
659
+ * `spawnSync` 超时**不设 `err.killed`**(2026-09-26 实测;旧守卫 `err.killed ? … : err.stderr`
660
+ * 因此是死代码,超时被当成"stderr 为空 = 成功")。
661
+ */
662
+ function git(root, args, timeoutMs = localTimeout()) {
634
663
  try {
664
+ const stdout = execFileSync("git", args, {
665
+ cwd: root,
666
+ encoding: "utf8",
667
+ stdio: [
668
+ "ignore",
669
+ "pipe",
670
+ "pipe"
671
+ ],
672
+ maxBuffer: 1048576,
673
+ timeout: timeoutMs
674
+ });
635
675
  return {
636
- stdout: execFileSync("git", args, {
637
- cwd: root,
638
- encoding: "utf8",
639
- stdio: [
640
- "ignore",
641
- "pipe",
642
- "pipe"
643
- ],
644
- maxBuffer: 1048576,
645
- timeout: GIT_TIMEOUT_MS
646
- }).trimEnd(),
647
- stderr: ""
676
+ ok: true,
677
+ stdout: String(stdout).trimEnd(),
678
+ stderr: "",
679
+ timedOut: false,
680
+ status: 0
648
681
  };
649
682
  } catch (err) {
683
+ const timedOut = err?.killed === true || err?.code === "ETIMEDOUT" || err?.signal != null && err?.status == null;
650
684
  return {
651
- stdout: (err.stdout?.toString() ?? "").trimEnd(),
652
- stderr: err.killed ? `git operation timed out (${GIT_TIMEOUT_MS / 1e3}s)` : (err.stderr?.toString() ?? "").trimEnd()
685
+ ok: false,
686
+ stdout: (err?.stdout?.toString() ?? "").trimEnd(),
687
+ stderr: (err?.stderr?.toString() ?? "").trimEnd(),
688
+ timedOut,
689
+ status: typeof err?.status === "number" ? err.status : null
653
690
  };
654
691
  }
655
692
  }
656
693
  function getCurrentBranch(root) {
657
- const { stdout } = git(root, [
694
+ const r = git(root, [
658
695
  "rev-parse",
659
696
  "--abbrev-ref",
660
697
  "HEAD"
661
698
  ]);
662
- if (!stdout) throw new Error("container_git: cannot determine current branch");
663
- return stdout;
699
+ if (!r.ok || !r.stdout) throw new Error("container_git: cannot determine current branch");
700
+ return r.stdout;
664
701
  }
665
702
  function hasChanges(root) {
666
- const { stdout } = git(root, ["status", "--porcelain"]);
667
- return stdout.length > 0;
703
+ return git(root, [
704
+ "-c",
705
+ "core.quotepath=false",
706
+ "status",
707
+ "--porcelain"
708
+ ]).stdout.length > 0;
668
709
  }
669
710
  function hasRemote(root) {
670
- const { stdout } = git(root, ["remote"]);
671
- return stdout.length > 0;
711
+ return git(root, ["remote"]).stdout.length > 0;
712
+ }
713
+ /** 推送成功与否的**唯一**核验指令(给使用者,也写进回执) */
714
+ function verifyHint(branch) {
715
+ return `🔴 核验(唯一判据):git ls-remote origin ${branch},或 git fetch && git rev-parse origin/${branch} —— 与本地 HEAD 比对。**不要采信任何回执。**`;
716
+ }
717
+ const REJECTED_TEXT = (branch) => `[REJECTED] Push to origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local branch is behind. Suggested actions:\n 1. Use bash: git fetch origin ${branch}\n 2. Inspect remote changes: git log HEAD..origin/${branch}\n 3. Merge or rebase: git merge origin/${branch} or git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add ... && git commit\n 5. Push again: container_git push`;
718
+ /**
719
+ * 推送并把结果**如实**翻成文本(`hard` = 作为独立动作时应抛错)。
720
+ * 🔴 成功判据 = `--porcelain` 的 **stdout 里有 ref 行**;退出码 0 但读不到 ref 行 ⇒ **不视为成功**。
721
+ */
722
+ function pushCore(root) {
723
+ if (!hasRemote(root)) return {
724
+ text: "⚠️ 未配置 remote ⇒ **未推送**(本地提交不受影响)。加 remote: git remote add origin <url>",
725
+ hard: true
726
+ };
727
+ const branch = getCurrentBranch(root);
728
+ git(root, [
729
+ "fetch",
730
+ "origin",
731
+ branch
732
+ ], netTimeout());
733
+ const r = git(root, [
734
+ "push",
735
+ "--porcelain",
736
+ "origin",
737
+ branch
738
+ ], netTimeout());
739
+ if (r.timedOut) return {
740
+ text: `[TIMEOUT] git push 在 ${netTimeout() / 1e3}s 内未结束 —— 🔴 **推送是否成功【未确认】**(既未成功也未失败)。\n原始输出: ${r.stdout || "(空)"}\n${r.stderr ? `stderr: ${r.stderr}\n` : ""}${verifyHint(branch)}`,
741
+ hard: true
742
+ };
743
+ const refLines = r.stdout.split("\n").filter((l) => /^[ =*+!-]\t/.test(l));
744
+ if (refLines.some((l) => l.startsWith("!"))) return {
745
+ text: REJECTED_TEXT(branch),
746
+ hard: false
747
+ };
748
+ const updated = refLines.filter((l) => /^[ *+-]/.test(l));
749
+ const upToDate = refLines.length > 0 && refLines.every((l) => l.startsWith("="));
750
+ if (updated.length > 0 && !upToDate) return {
751
+ text: `✅ 已推送 origin/${branch}\n${updated.join("\n")}`,
752
+ hard: false
753
+ };
754
+ if (upToDate) return {
755
+ text: `origin/${branch} 已是最新(无可推送的新提交)\n${refLines.join("\n")}`,
756
+ hard: false
757
+ };
758
+ if (!r.ok) return {
759
+ text: `[FAILED] git push 退出码 ${r.status ?? "未知"} —— **未推送**\n${r.stderr || r.stdout || "(无输出)"}\n${verifyHint(branch)}`,
760
+ hard: true
761
+ };
762
+ return {
763
+ text: `[UNVERIFIED] git push 退出码 0,但**未从 --porcelain 读到 ref 行** ⇒ 🔴 **不视为成功**。\n原始输出: ${r.stdout || "(空)"}\n${verifyHint(branch)}`,
764
+ hard: true
765
+ };
672
766
  }
673
767
  function runGit(root, args) {
674
768
  switch (args.action) {
@@ -679,7 +773,8 @@ function runGit(root, args) {
679
773
  "status",
680
774
  "--porcelain"
681
775
  ]);
682
- if (r.stderr) throw new Error(`status failed: ${r.stderr.trim()}`);
776
+ if (r.timedOut) throw new Error(`status timed out (${localTimeout() / 1e3}s)`);
777
+ if (!r.ok) throw new Error(`status failed: ${r.stderr || r.stdout || "(no output)"}`);
683
778
  const files = (r.stdout ? r.stdout.split("\n") : []).map((line) => {
684
779
  return {
685
780
  status: line.slice(0, 2).trim() || "??",
@@ -691,6 +786,21 @@ function runGit(root, args) {
691
786
  files,
692
787
  summary: files.length === 0 ? "(clean)" : `${files.length} file(s) with changes`
693
788
  };
789
+ if (hasRemote(root)) try {
790
+ const branch = getCurrentBranch(root);
791
+ const rr = git(root, [
792
+ "rev-list",
793
+ "--left-right",
794
+ "--count",
795
+ `origin/${branch}...HEAD`
796
+ ]);
797
+ if (rr.ok && /^\d+\s+\d+$/.test(rr.stdout)) {
798
+ const [behind, ahead] = rr.stdout.split(/\s+/).map(Number);
799
+ out.ahead = ahead;
800
+ out.behind = behind;
801
+ if (ahead > 0) out.summary = `${out.summary} | 🔴 本地领先 origin/${branch} **${ahead} 笔未推送**`;
802
+ }
803
+ } catch {}
694
804
  const ls = checkLocalstoreGitCompliance(root);
695
805
  if (!ls.ok) out.warning = ls.reason;
696
806
  return out;
@@ -699,36 +809,44 @@ function runGit(root, args) {
699
809
  const ls = checkLocalstoreGitCompliance(root);
700
810
  if (!ls.ok) throw new Error(ls.reason);
701
811
  if (!args.message || args.message.trim() === "") throw new Error("container_git commit: missing required arg \"message\"");
702
- if (!hasChanges(root)) return "(nothing to commit — working tree clean)";
703
- const addResult = git(root, ["add", "-A"]);
704
- if (addResult.stderr) throw new Error(`container_git commit: git add failed\n${addResult.stderr}`);
705
- const commitResult = git(root, [
812
+ const paths = (args.paths ?? []).map((p) => p.trim()).filter((p) => p !== "");
813
+ const scoped = paths.length > 0;
814
+ if (!scoped && !hasChanges(root)) return "(nothing to commit — working tree clean)";
815
+ const addR = git(root, scoped ? [
816
+ "add",
817
+ "--",
818
+ ...paths
819
+ ] : ["add", "-A"]);
820
+ if (addR.timedOut) throw new Error(`container_git commit: git add timed out (${localTimeout() / 1e3}s)`);
821
+ if (!addR.ok) throw new Error(`container_git commit: git add failed\n${addR.stderr || addR.stdout}`);
822
+ const commitR = git(root, scoped ? [
823
+ "commit",
824
+ "-m",
825
+ args.message,
826
+ "--",
827
+ ...paths
828
+ ] : [
706
829
  "commit",
707
830
  "-m",
708
831
  args.message
709
832
  ]);
710
- if (commitResult.stderr && commitResult.stderr.includes("nothing to commit")) return "(nothing to commit — working tree clean)";
711
- if (commitResult.stderr && !commitResult.stdout) throw new Error(`container_git commit: git commit failed\n${commitResult.stderr}`);
712
- return (commitResult.stdout || commitResult.stderr || "committed").trimEnd();
833
+ if (commitR.timedOut) throw new Error(`container_git commit: git commit timed out (${localTimeout() / 1e3}s)`);
834
+ if (`${commitR.stdout}\n${commitR.stderr}`.includes("nothing to commit")) return "(nothing to commit — working tree clean)";
835
+ if (!commitR.ok) throw new Error(`container_git commit: git commit failed\n${commitR.stderr || commitR.stdout}`);
836
+ const lines = [(commitR.stdout || commitR.stderr || "committed").trimEnd()];
837
+ if (!scoped) lines.push("ℹ️ 本次是**整仓**提交(未传 `paths`)—— 包含工作树里**所有**改动,可能含其它轨迹的在途文件。要只提交指定路径 ⇒ 传 `paths`。");
838
+ if (args.noPush) {
839
+ lines.push("⚠️ 按 `noPush` 只提交到**本地** —— 尚未推送到 origin。推送:container_git push");
840
+ return lines.join("\n");
841
+ }
842
+ const p = pushCore(root);
843
+ lines.push(p.hard ? `🔴 **已本地提交,但未推送** ——\n${p.text}` : p.text);
844
+ return lines.join("\n");
713
845
  }
714
846
  case "push": {
715
- if (!hasRemote(root)) throw new Error("container_git push: no remote configured. Add one with:\n git remote add origin <url>");
716
- const branch = getCurrentBranch(root);
717
- try {
718
- git(root, [
719
- "fetch",
720
- "origin",
721
- branch
722
- ]);
723
- } catch {}
724
- const { stdout, stderr } = git(root, [
725
- "push",
726
- "origin",
727
- branch
728
- ]);
729
- if (stderr.includes("non-fast-forward") || stderr.includes("rejected") || stderr.includes("[rejected]")) return `[REJECTED] Push to origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local branch is behind. Suggested actions:\n 1. Use bash: git fetch origin ${branch}\n 2. Inspect remote changes: git log HEAD..origin/${branch}\n 3. Merge or rebase: git merge origin/${branch} or git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add ... && git commit\n 5. Push again: container_git push`;
730
- if (!stderr || stderr.includes("->") || stderr === "") return stdout || `Pushed to origin/${branch}`;
731
- throw new Error(`container_git push failed:\n${stderr}`);
847
+ const p = pushCore(root);
848
+ if (p.hard) throw new Error(p.text);
849
+ return p.text;
732
850
  }
733
851
  case "pull": {
734
852
  if (!hasRemote(root)) throw new Error("container_git pull: no remote configured. Add one with:\n git remote add origin <url>");
@@ -737,26 +855,29 @@ function runGit(root, args) {
737
855
  "fetch",
738
856
  "origin",
739
857
  branch
740
- ]);
741
- if (fetchResult.stderr) return `[WARN] fetch had stderr:\n${fetchResult.stderr}`;
858
+ ], netTimeout());
859
+ if (fetchResult.timedOut) return `[TIMEOUT] git fetch 在 ${netTimeout() / 1e3}s 内未结束 —— **是否已拉到【未确认】**;未做合并。`;
860
+ if (!fetchResult.ok) return `[WARN] fetch failed:\n${fetchResult.stderr || fetchResult.stdout}`;
742
861
  const revResult = git(root, [
743
862
  "rev-list",
744
863
  "--count",
745
864
  "HEAD..FETCH_HEAD"
746
865
  ]);
747
- if (revResult.stderr) return `[WARN] cannot check ahead count:\n${revResult.stderr}`;
866
+ if (!revResult.ok) return `[WARN] cannot check ahead count:\n${revResult.stderr || revResult.stdout}`;
748
867
  if (revResult.stdout === "0" || revResult.stdout === "") return "Already up to date.";
749
868
  const mergeResult = git(root, [
750
869
  "merge",
751
870
  "--ff-only",
752
871
  "FETCH_HEAD"
753
872
  ]);
754
- if (!mergeResult.stderr) {
873
+ if (mergeResult.timedOut) return `[TIMEOUT] git merge 在 ${localTimeout() / 1e3}s 内未结束 —— **合并是否完成【未确认】**。`;
874
+ if (mergeResult.ok) {
755
875
  const msg = mergeResult.stdout || "Pulled successfully.";
756
876
  return msg.endsWith("\n") ? msg.trimEnd() : msg;
757
877
  }
758
- if (mergeResult.stderr.includes("non-fast-forward") || mergeResult.stderr.includes("Not possible to fast-forward") || mergeResult.stderr.includes("rejected") || mergeResult.stderr.includes("could not be applied")) return `[REJECTED] Pull from origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local history diverged from remote (non-fast-forward). Suggested actions:\n 1. Inspect differences: container_git log HEAD..origin/${branch}\n 2. Merge manually with bash: git merge origin/${branch}\n 3. Or rebase: git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add <file> && git commit\n 5. Push: container_git push`;
759
- throw new Error(`container_git pull failed:\n${mergeResult.stderr}`);
878
+ const mErr = `${mergeResult.stderr}\n${mergeResult.stdout}`;
879
+ if (mErr.includes("non-fast-forward") || mErr.includes("Not possible to fast-forward") || mErr.includes("rejected") || mErr.includes("could not be applied")) return `[REJECTED] Pull from origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local history diverged from remote (non-fast-forward). Suggested actions:\n 1. Inspect differences: container_git log HEAD..origin/${branch}\n 2. Merge manually with bash: git merge origin/${branch}\n 3. Or rebase: git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add <file> && git commit\n 5. Push: container_git push`;
880
+ throw new Error(`container_git pull failed:\n${mergeResult.stderr || mergeResult.stdout}`);
760
881
  }
761
882
  case "log": {
762
883
  const n = Math.min(Math.max(args.count ?? 10, 1), 100);
@@ -768,7 +889,8 @@ function runGit(root, args) {
768
889
  "-n",
769
890
  String(n)
770
891
  ]);
771
- if (r.stderr) throw new Error(`git log failed: ${r.stderr.trim()}`);
892
+ if (r.timedOut) throw new Error(`git log timed out (${localTimeout() / 1e3}s)`);
893
+ if (!r.ok) throw new Error(`git log failed: ${r.stderr || r.stdout || "(no output)"}`);
772
894
  if (!r.stdout) return "(no commits)";
773
895
  return r.stdout;
774
896
  }
@@ -777,10 +899,11 @@ function runGit(root, args) {
777
899
  if (args.staged) diffArgs.push("--cached");
778
900
  if (args.ref) diffArgs.push(args.ref);
779
901
  if (args.path) diffArgs.push("--", args.path);
780
- const { stdout, stderr } = git(root, diffArgs);
781
- if (stderr) return `[WARN] git diff had stderr:\n${stderr}`;
782
- if (!stdout) return "(no diff)";
783
- return stdout;
902
+ const r = git(root, diffArgs);
903
+ if (r.timedOut) throw new Error(`git diff timed out (${localTimeout() / 1e3}s)`);
904
+ if (!r.ok) return `[WARN] git diff failed:\n${r.stderr || r.stdout || "(no output)"}`;
905
+ if (!r.stdout) return "(no diff)";
906
+ return r.stdout;
784
907
  }
785
908
  default: throw new Error(`Unknown action: ${args.action}`);
786
909
  }
@@ -798,7 +921,7 @@ function renderText$7(value) {
798
921
  }
799
922
  const gitTool = defineTool({
800
923
  name: "container_git",
801
- description: "CCC git operations (cc-git semantics, aligned with osp). status/commit/push/log/pull/diff; non-fast-forward push/pull outputs [REJECTED] + suggested actions (never auto-force). merge/rebase/conflict resolution go through bash.",
924
+ description: "CCC git operations (cc-git semantics, aligned with osp). status/commit/push/log/pull/diff. commit 默认**随后自动 push**(noPush 可关);commit 传 paths ⇒ 只提交这些路径(不传=整仓 add -A,回执会显式告知)。push 成功判据 = --porcelain 的 ref 行(**超时/未确认一律不报成功**,回执给 `git ls-remote` 核验指令);非快进 push/pull 输出 [REJECTED] + 建议(never auto-force)。merge/rebase/conflict resolution go through bash.",
802
925
  parameters: {
803
926
  action: {
804
927
  type: "string",
@@ -810,6 +933,15 @@ const gitTool = defineTool({
810
933
  type: "string",
811
934
  description: "Commit message (required for commit)"
812
935
  },
936
+ paths: {
937
+ type: "array",
938
+ items: { type: "string" },
939
+ description: "commit: 只提交这些路径(如 [\"docs/a.md\",\"src/b.ts\"])。不传 = 整仓 `git add -A`(会吸走其它轨迹的在途文件,回执会显式提示)"
940
+ },
941
+ noPush: {
942
+ type: "boolean",
943
+ description: "commit: 只提交到本地,不随后推送(默认提交后自动推送)"
944
+ },
813
945
  count: {
814
946
  type: "integer",
815
947
  description: "Log count (default 10, max 100; runtime clamp 1-100)"
@@ -9103,7 +9235,12 @@ const EVENT_LABEL = {
9103
9235
  /** 敏感输出边界守卫(原 SERENITY OUTPUT GUARD) */
9104
9236
  guard: "BOUNDARY GUARD",
9105
9237
  /** SESSION.md 体积超限重写提醒(v1.31.1,S142 用户需求) */
9106
- compaction: "LOGBOOK COMPACTION"
9238
+ compaction: "LOGBOOK COMPACTION",
9239
+ /** **交界便签(REBUILD-TODO.md)体积超限清理提醒**(v1.49.0,owner 2026-09-26 需求)。
9240
+ *
9241
+ * 🔴 **刻意与 `compaction` 分开**:那条要求"**重写 SESSION.md 并保留 EAP 骨架**",
9242
+ * 本条只要求"**清理便签**"——共用一个 token 会让模型误以为要动 SESSION.md。 */
9243
+ scratchPrune: "SCRATCH PRUNE"
9107
9244
  };
9108
9245
  /** 家族标识(所有动态注入统一前缀) */
9109
9246
  const ASSISTANT_PREFIX = "TRAJECTORY-ASSISTANT";
@@ -9114,16 +9251,24 @@ function eventToken(event) {
9114
9251
  /** ACK 确认码前缀(recorded/skipped 语义不变,仅家族名更新) */
9115
9252
  const ACK_PREFIX = `${ASSISTANT_PREFIX}-recorded`;
9116
9253
  /**
9117
- * rebuild 交接区块标题(v1.31.1,S142 用户需求)。
9254
+ * rebuild 交界便签的**文件名**(v1.49.0;owner 2026-09-26 需求)。
9255
+ *
9256
+ * ## 它取代了什么(owner 原话:*"砍掉 in-flight,这正是目的"*)
9257
+ * v1.31.1 的交界协议要求把手头事项写进 **SESSION.md 末尾**的 `## In-flight (rebuild handover)` 段。
9258
+ * 实测的代价(S142 自身取证):In-flight 段曾积 **12 块**(含多份**已被取代**的交接),
9259
+ * 而 SESSION.md 是**有格式义务**的持久文件(7 段固定 + 粒度受 EAP 约束)⇒
9260
+ * ① 持久文件被临时物污染 ② 每次整理都要付"压缩+归档+保真"三件套(该文件曾达 292 KB)。
9118
9261
  *
9119
- * 用途:rebuild 提醒要求模型把**当前手头事项**写到 SESSION.md **末尾**的这个小标题之下;
9120
- * rebuild 锚点随后要求重建后的自己去读这一段并处理。两侧引用**同一个常量**——
9121
- * 标题一旦漂移,读侧就找不到写侧写的东西(单一真相源)。
9262
+ * ⇒ 分家:**只用一次的交界材料**写进这份**临时便签**(无格式义务、无留档义务、可随时丢弃)。
9122
9263
  *
9123
- * 形态(用户拍板 Q2):**固定英文标题行**(不是 HTML 注释锚)——人读友好;ACC 写死英文,
9124
- * 因为 SESSION.md 的正文语言归 CCC,但两侧的**机械锚点**必须逐字一致。
9264
+ * ## 🔴 三处引用同一常量(单一真相源)
9265
+ * **写侧** = `seams/keeper.ts` 的 `rebuildReminderText()`|**读侧** = `rebuild.ts` 的重建锚点|
9266
+ * **体积提醒** = `seams/keeper.ts` 的 `rebuildTodoReminderText()`。
9267
+ * 文件名一旦漂移,写侧写的东西读侧就找不到 —— **且没有任何机械信号会报警**。
9268
+ *
9269
+ * 🔴 **不得改名**(owner 已具名)。形态 = **固定文件名**(人读友好;比照旧 IN_FLIGHT_HEADING 的取舍)。
9125
9270
  */
9126
- const IN_FLIGHT_HEADING = "## In-flight (rebuild handover)";
9271
+ const REBUILD_TODO_FILENAME = "REBUILD-TODO.md";
9127
9272
  //#endregion
9128
9273
  //#region src/seams/keeper.ts
9129
9274
  const SCORES = {
@@ -9196,12 +9341,15 @@ function reminderText(code) {
9196
9341
  * 使用者若要查看当前占用/阈值:看面板(`rebuildThresholdK` 滑块 + 状态显示)——那是给人看的。
9197
9342
  *
9198
9343
  * v1.31.1 交接协议(S142 用户需求"要求 LLM 将当前手头事项写在 SESSION.md 尾部,并要求
9199
- * rebuild 后去读并处理"):**写侧** = 这里(两条文案都要求把 in-flight 事项写在
9200
- * SESSION.md 末尾的 `IN_FLIGHT_HEADING` 之下);**读侧** = rebuild 锚点(rebuild.ts)。
9201
- * 两侧共用 trajectory-assistant 的标题常量(单一真相源)。
9344
+ * rebuild 后去读并处理"):**写侧** = 这里;**读侧** = rebuild 锚点(rebuild.ts)。
9345
+ * 两侧共用 trajectory-assistant 的常量(单一真相源)。
9346
+ *
9347
+ * 🔴 **v1.49.0(owner 2026-09-26 裁决:"砍掉 in-flight,这正是目的")**:写侧**不再**指向
9348
+ * SESSION.md 末尾的段,改指**独立便签文件** `REBUILD-TODO_FILENAME`(轨迹目录下)——
9349
+ * 理由见该常量的头注(持久文件 vs 只用一次的交界材料必须分家)。
9202
9350
  */
9203
9351
  function rebuildReminderText(escalated = false) {
9204
- const handover = ` Before rebuilding, write your current in-flight items (the exact step you are in the middle of, what is not finished yet, and what the next action is) at the very end of SESSION.md under the heading "${IN_FLIGHT_HEADING}" — the rebuilt conversation reads that section first and continues from it.`;
9352
+ const handover = ` Before rebuilding, write your current in-flight items (the exact step you are in the middle of, what is not finished yet, and what the next action is) into "${REBUILD_TODO_FILENAME}" in this trajectory's directory — create it if it does not exist. That file is a scratch note for the handover only: no format obligation, no archival duty, safe to discard at any time. The rebuilt conversation reads it first and continues from it.`;
9205
9353
  if (escalated) return `${eventToken("limitMandatory")} Context usage has reached the configured rebuild threshold — you have been reminded repeatedly and have NOT called the container_trajectory rebuild action. This is now mandatory: STOP at the current task step, preserve valuable cognition into the CCC skills (or write a new-skill proposal into SESSION.md), then call container_trajectory rebuild immediately, passing --summary "<content summary ≤20 chars>" (required; the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild).${handover} The conversation will be cleared and rebuilt in place; SESSION.md is the persistent trajectory and stays in place — identity continues from it. Do not continue working without rebuilding; this reminder persists until you call container_trajectory rebuild.`;
9206
9354
  return `${eventToken("limit")} Context usage has reached the configured rebuild threshold. This session is the rebuildable carrier of the trajectory: SESSION.md is the persistent body, this conversation is only a temporary work copy. Before rebuilding: if this conversation produced valuable cognition, revise the relevant existing skill of this CCC (structure it with eap); if a new skill is warranted, write a short proposal into SESSION.md for the user to review — do not create it yourself.${handover} ACT NOW: at the next natural pause (end of the current task step), call the container_trajectory rebuild action — passing --summary "<content summary ≤20 chars>" describing the next work phase (required; the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild) — to clear and rebuild this conversation: the current copy is discarded, identity continues from SESSION.md. If you are in the middle of an unbreakable step, continue it, then rebuild at its end. Do not ignore this; rebuild is the expected action, not an option.`;
9207
9355
  }
@@ -9311,6 +9459,37 @@ function trajectoryCompactionReminderText(input) {
9311
9459
  if (escalated) return `${eventToken("compaction")} ${facts} — you have been reminded repeatedly and have NOT compacted SESSION.md. This is now mandatory: STOP the current task step, load eap (praxis eap), rewrite SESSION.md, then resume. ${principles} ${frontmatterGuard} This reminder persists until the file is under the limit.`;
9312
9460
  return `${eventToken("compaction")} ${facts}. Pause the current work and compact this trajectory's log (SESSION.md) before continuing — that log is the persistent body of this trajectory, and its size is operational entropy (H_op) the container pays on every read. ${principles} ${frontmatterGuard} After rewriting, resume the paused work; this reminder stops once the file is under the limit.`;
9313
9461
  }
9462
+ /** 连续超限轮数达此值 → 语气升级(对齐 COMPACTION_ESCALATE_AFTER / REBUILD_ESCALATE_AFTER) */
9463
+ const REBUILD_TODO_ESCALATE_AFTER = 3;
9464
+ const rebuildTodoStates = /* @__PURE__ */ new Map();
9465
+ /**
9466
+ * 交界便签超限提醒文案(v1.49.0)。
9467
+ *
9468
+ * ## 与 SESSION.md 档的**语义差别**(刻意,不是疏漏)
9469
+ *
9470
+ * | | SESSION.md 超限(`trajectoryCompactionReminderText`) | 便签超限(本条) |
9471
+ * |---|---|---|
9472
+ * | 动作 | **重写**(compaction) | **清理/丢弃**(prune) |
9473
+ * | 结构义务 | 保 EAP 骨架 + 4 条重写原则 + frontmatter 护栏 | **无** |
9474
+ * | 强度 | 升级后**强制**("STOP…必须先重写") | 升级后**语气加重,但动作仍只是"清理"** |
9475
+ * | 授权 | — | 🔴 **明确授权整份删除** |
9476
+ *
9477
+ * ## 归属(D23)
9478
+ * 机制(文件名 / 上限 / 注入时机)归 **ACC**;**写什么、怎么组织、何时丢**归 **CCC**。
9479
+ * 🔴 owner 裁决(逐字):*"rebuild 提示 Agent 去写,清空我们就不干涉了,只管最大量"* ⇒
9480
+ * **ACC 只提醒,不做任何写/删/归档操作**(自动删 = 数据丢失风险:rebuild 时序上新我还没读就被删)。
9481
+ *
9482
+ * ## 为什么要求"参照 eap"
9483
+ * owner 裁决(逐字):*"不规定,但要求参照 eap 来编写(这样质量会好点)"* ⇒
9484
+ * **不规定内部结构**(不给骨架),但**要求按 EAP 写**(显式 / 可重建 / 稳定)。
9485
+ */
9486
+ function rebuildTodoReminderText(input) {
9487
+ const { sizeKB, limitKB, mdPath, escalated = false } = input;
9488
+ const facts = `${REBUILD_TODO_FILENAME} is ${sizeKB} KB (limit ${limitKB} KB): ${mdPath}`;
9489
+ const action = "This file is a scratch note for the rebuild handover — it carries no archival duty and you may discard it wholesale at any time. Prune it now, writing in the spirit of eap (praxis eap): (1) delete what is already done; (2) delete what has been superseded; (3) delete what is already recorded in SESSION.md — that file is the trajectory's persistent body, this one only carries what the next self needs once. Do not restructure it into a formal document: the only mechanical constraint on this file is its size.";
9490
+ if (escalated) return `${eventToken("scratchPrune")} ${facts} — you have been reminded repeatedly and have NOT pruned it. Prune it now. ${action} This reminder persists until the file is under the limit.`;
9491
+ return `${eventToken("scratchPrune")} ${facts}. ${action} This reminder stops once the file is under the limit.`;
9492
+ }
9314
9493
  /** 每 agent 一个跟踪器(进程内存态,agent token 维度) */
9315
9494
  const trackers = /* @__PURE__ */ new Map();
9316
9495
  /** 连续超阈值轮数达此值 → 升级为 [TRAJECTORY-ESCALATED] 强制语气(此后持续升级催,直到 rebuild) */
@@ -9350,7 +9529,7 @@ function registerKeeper(ctx, opts = {}) {
9350
9529
  text: reminderText(code)
9351
9530
  });
9352
9531
  }
9353
- if (skiffRebuild && readSimpleSettings().rebuildEnabled) {
9532
+ if (skiffRebuild) {
9354
9533
  const session = exec.agent?.session;
9355
9534
  if (session) {
9356
9535
  const pressure = readContextPressure(ctx, session);
@@ -9393,6 +9572,27 @@ function registerKeeper(ctx, opts = {}) {
9393
9572
  } else compactionStates.delete(scope);
9394
9573
  }
9395
9574
  }
9575
+ if (skiffTrajectoryEnabled(root, sessionId, "session")) {
9576
+ const scope = sessionId ?? "global";
9577
+ const session = exec.agent?.session;
9578
+ const mdPath = resolveActiveSessionMdPath(root, scope, session);
9579
+ const todoPath = mdPath ? join(dirname(mdPath), REBUILD_TODO_FILENAME) : null;
9580
+ const size = todoPath ? readFileSize(todoPath) : null;
9581
+ if (todoPath && size !== null && size > 10240) {
9582
+ const st = rebuildTodoStates.get(scope) ?? { consecutive: 0 };
9583
+ st.consecutive += 1;
9584
+ blocks.push({
9585
+ type: "text",
9586
+ text: rebuildTodoReminderText({
9587
+ sizeKB: Math.round(size / 1024),
9588
+ limitKB: 10,
9589
+ mdPath: todoPath,
9590
+ escalated: st.consecutive >= REBUILD_TODO_ESCALATE_AFTER
9591
+ })
9592
+ });
9593
+ rebuildTodoStates.set(scope, st);
9594
+ } else rebuildTodoStates.delete(scope);
9595
+ }
9396
9596
  if (blocks.length === 0) return downstream;
9397
9597
  const reminderAll = createUserMessage({
9398
9598
  content: blocks,
@@ -10049,7 +10249,7 @@ function registerStatusApi(ctx, opts = {}) {
10049
10249
  return;
10050
10250
  }
10051
10251
  const settings = readAdvancedSettings();
10052
- const { readSimpleSettings } = await import("./settings-section-DxMwlCv9.js").then((n) => n.r);
10252
+ const { readSimpleSettings } = await import("./settings-section-B-KOyOYn.js").then((n) => n.r);
10053
10253
  const simple = readSimpleSettings();
10054
10254
  const allowed = settings.publicAsk.allowed;
10055
10255
  const port = simple.acpHttpPort ?? ACP_HTTP_PORT;
@@ -12040,6 +12240,8 @@ function stripAckSuffix(text) {
12040
12240
  */
12041
12241
  function buildRebuildAnchor(root, sessionName, activeMdPath, anchorMessages = DEFAULT_ANCHOR_MESSAGES, focus) {
12042
12242
  const rel = activeMdPath.startsWith(root) ? activeMdPath.slice(root.length + 1) : activeMdPath;
12243
+ const todoAbs = join(dirname(activeMdPath), REBUILD_TODO_FILENAME);
12244
+ const todoRel = todoAbs.startsWith(root) ? todoAbs.slice(root.length + 1) : todoAbs;
12043
12245
  const sessionDir = basename(dirname(activeMdPath));
12044
12246
  const sessionLine = sessionDir !== "AGENT_SESSIONS" ? `- Serenity session: ${sessionName !== "" ? `${sessionName} (${sessionDir})` : sessionDir}` : null;
12045
12247
  const focusLine = sanitizeFocusLine(focus);
@@ -12052,7 +12254,7 @@ function buildRebuildAnchor(root, sessionName, activeMdPath, anchorMessages = DE
12052
12254
  `- Persistent trajectory — SESSION.md path: ${rel}`,
12053
12255
  ` (the trajectory's persistent body — stays in place through rebuilds)`,
12054
12256
  `- Read that SESSION.md first (goal/decisions/progress/unresolved), then continue from the last checkpoint.`,
12055
- `- Then read the "${IN_FLIGHT_HEADING}" section at the very end of SESSION.md and work through those in-flight items (the previous self paused mid-task and handed them over). If that section is missing, infer the in-flight items from the latest progress entries.`,
12257
+ `- Then read the handover scratch note: ${todoRel} (path: ${todoAbs}) — the previous self paused mid-task and left its in-flight items there. That file carries no format obligation and may be discarded at any time; it is NOT the authoritative state. If it is missing, infer the in-flight items from SESSION.md's latest progress entries.`,
12056
12258
  ...focusLine ? [`- Task focus: ${focusLine}`] : []
12057
12259
  ].join("\n");
12058
12260
  }
@@ -12158,7 +12360,6 @@ function writeRebuildDiag(root, entry) {
12158
12360
  * @returns 排队结果;rebuild.enabled=false 或非 CCC 抛错
12159
12361
  */
12160
12362
  async function queueRebuild(ctx, opts) {
12161
- if (!readSimpleSettings().rebuildEnabled) throw new Error("container_trajectory rebuild is disabled (rebuild.enabled=false — enable it in the dsh settings panel)");
12162
12363
  const { root, note, summary, dshSessionId } = opts;
12163
12364
  const session = ctx.sessions?.get?.(dshSessionId);
12164
12365
  if (!session) throw new Error(`Unable to locate dsh session ${dshSessionId} (session may be closed)`);
@@ -18567,10 +18768,7 @@ const Config = z.object({
18567
18768
  env: z.boolean().default(true),
18568
18769
  opencodeSkills: z.boolean().default(true),
18569
18770
  gateway: z.object({ enabled: z.boolean().default(false) }),
18570
- rebuild: z.object({
18571
- enabled: z.boolean().default(true),
18572
- thresholdK: z.number().min(50).max(4e3).default(400)
18573
- }),
18771
+ rebuild: z.object({ thresholdK: z.number().min(50).max(4e3).default(400) }),
18574
18772
  skiff: z.object({
18575
18773
  enabled: z.boolean().default(false),
18576
18774
  debugPort: z.number().min(1024).max(65535).default(SKIFF_DEBUG_PORT)
@@ -18583,7 +18781,6 @@ const Config = z.object({
18583
18781
  opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
18584
18782
  visionPatch: z.object({ enabled: z.boolean().default(true) }),
18585
18783
  gatewayEnabled: z.boolean().volatile(),
18586
- rebuildEnabled: z.boolean().volatile(),
18587
18784
  rebuildThresholdK: z.number().min(50).max(4e3).volatile(),
18588
18785
  skiffEnabled: z.boolean().volatile(),
18589
18786
  skiffDebugPort: z.number().min(1024).max(65535).volatile(),
@@ -48,9 +48,12 @@ export declare function reminderText(code: string): string;
48
48
  * 使用者若要查看当前占用/阈值:看面板(`rebuildThresholdK` 滑块 + 状态显示)——那是给人看的。
49
49
  *
50
50
  * v1.31.1 交接协议(S142 用户需求"要求 LLM 将当前手头事项写在 SESSION.md 尾部,并要求
51
- * rebuild 后去读并处理"):**写侧** = 这里(两条文案都要求把 in-flight 事项写在
52
- * SESSION.md 末尾的 `IN_FLIGHT_HEADING` 之下);**读侧** = rebuild 锚点(rebuild.ts)。
53
- * 两侧共用 trajectory-assistant 的标题常量(单一真相源)。
51
+ * rebuild 后去读并处理"):**写侧** = 这里;**读侧** = rebuild 锚点(rebuild.ts)。
52
+ * 两侧共用 trajectory-assistant 的常量(单一真相源)。
53
+ *
54
+ * 🔴 **v1.49.0(owner 2026-09-26 裁决:"砍掉 in-flight,这正是目的")**:写侧**不再**指向
55
+ * SESSION.md 末尾的段,改指**独立便签文件** `REBUILD-TODO_FILENAME`(轨迹目录下)——
56
+ * 理由见该常量的头注(持久文件 vs 只用一次的交界材料必须分家)。
54
57
  */
55
58
  export declare function rebuildReminderText(escalated?: boolean): string;
56
59
  /** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
@@ -110,6 +113,51 @@ export declare function trajectoryCompactionReminderText(input: {
110
113
  mdPath: string;
111
114
  escalated?: boolean;
112
115
  }): string;
116
+ /**
117
+ * 交界便签体积上限(KB)—— **硬编码,刻意不设配置键**。
118
+ *
119
+ * 🔴 owner 2026-09-26 裁决(逐字):*"不需要设定,很多设定项我想砍掉了,好的功能用久了就发现
120
+ * 离不开,就不需要了"* ⇒ **不提供 `sessionKeeper.rebuildTodoMaxKB`**(设计件 Q4 的原倾向被否)。
121
+ * 对照:SESSION.md 的上限**保留可配置**(既有面,本版不动)。
122
+ */
123
+ export declare const DEFAULT_REBUILD_TODO_MAX_KB = 10;
124
+ interface RebuildTodoState {
125
+ /** 连续超限轮数(文件降回限内即清零 → 清理完成后提醒自动停止) */
126
+ consecutive: number;
127
+ }
128
+ /** 测试/调试:查看交界便签超限提醒状态 */
129
+ export declare function rebuildTodoStateSnapshot(): ReadonlyMap<string, RebuildTodoState>;
130
+ /** 会话销毁/测试重置:清理超限计数(防 per-会话 Map 无界增长) */
131
+ export declare function forgetRebuildTodoState(scope: string): void;
132
+ /** 测试辅助:清空交界便签提醒状态 */
133
+ export declare function __resetRebuildTodoForTest(): void;
134
+ /**
135
+ * 交界便签超限提醒文案(v1.49.0)。
136
+ *
137
+ * ## 与 SESSION.md 档的**语义差别**(刻意,不是疏漏)
138
+ *
139
+ * | | SESSION.md 超限(`trajectoryCompactionReminderText`) | 便签超限(本条) |
140
+ * |---|---|---|
141
+ * | 动作 | **重写**(compaction) | **清理/丢弃**(prune) |
142
+ * | 结构义务 | 保 EAP 骨架 + 4 条重写原则 + frontmatter 护栏 | **无** |
143
+ * | 强度 | 升级后**强制**("STOP…必须先重写") | 升级后**语气加重,但动作仍只是"清理"** |
144
+ * | 授权 | — | 🔴 **明确授权整份删除** |
145
+ *
146
+ * ## 归属(D23)
147
+ * 机制(文件名 / 上限 / 注入时机)归 **ACC**;**写什么、怎么组织、何时丢**归 **CCC**。
148
+ * 🔴 owner 裁决(逐字):*"rebuild 提示 Agent 去写,清空我们就不干涉了,只管最大量"* ⇒
149
+ * **ACC 只提醒,不做任何写/删/归档操作**(自动删 = 数据丢失风险:rebuild 时序上新我还没读就被删)。
150
+ *
151
+ * ## 为什么要求"参照 eap"
152
+ * owner 裁决(逐字):*"不规定,但要求参照 eap 来编写(这样质量会好点)"* ⇒
153
+ * **不规定内部结构**(不给骨架),但**要求按 EAP 写**(显式 / 可重建 / 稳定)。
154
+ */
155
+ export declare function rebuildTodoReminderText(input: {
156
+ sizeKB: number;
157
+ limitKB: number;
158
+ mdPath: string;
159
+ escalated?: boolean;
160
+ }): string;
113
161
  interface KeeperRegistration {
114
162
  /** 缺省阈值(serenity.json sessionKeeper.threshold 优先,其次此值) */
115
163
  defaultThreshold?: number;
@@ -70,7 +70,6 @@ var settings_section_exports = /* @__PURE__ */ __exportAll({
70
70
  function defaultSimpleSettings() {
71
71
  return {
72
72
  gatewayEnabled: false,
73
- rebuildEnabled: true,
74
73
  rebuildThresholdK: 400,
75
74
  skiffEnabled: false,
76
75
  skiffDebugPort: SKIFF_DEBUG_PORT,
@@ -116,7 +115,6 @@ function simpleSettingsFromConfig(config) {
116
115
  const d = defaultSimpleSettings();
117
116
  return {
118
117
  gatewayEnabled: unwrapVolatile(config.gatewayEnabled) ?? config.gateway?.enabled ?? d.gatewayEnabled,
119
- rebuildEnabled: unwrapVolatile(config.rebuildEnabled) ?? config.rebuild?.enabled ?? d.rebuildEnabled,
120
118
  rebuildThresholdK: unwrapVolatile(config.rebuildThresholdK) ?? config.rebuild?.thresholdK ?? d.rebuildThresholdK,
121
119
  skiffEnabled: unwrapVolatile(config.skiffEnabled) ?? config.skiff?.enabled ?? d.skiffEnabled,
122
120
  skiffDebugPort: unwrapVolatile(config.skiffDebugPort) ?? config.skiff?.debugPort ?? d.skiffDebugPort,
@@ -49,7 +49,6 @@ interface SimpleConfigFragment {
49
49
  enabled?: boolean;
50
50
  };
51
51
  rebuild?: {
52
- enabled?: boolean;
53
52
  thresholdK?: number;
54
53
  };
55
54
  /** F4 Skiff(实验性):调试服务启停(人工) */
@@ -63,7 +62,6 @@ interface SimpleConfigFragment {
63
62
  httpPort?: number;
64
63
  };
65
64
  gatewayEnabled?: PanelBoolean;
66
- rebuildEnabled?: PanelBoolean;
67
65
  rebuildThresholdK?: PanelNumber;
68
66
  skiffEnabled?: PanelBoolean;
69
67
  skiffDebugPort?: PanelNumber;
@@ -81,8 +79,6 @@ export declare const SERENITY_SETTINGS_NS = "serenity-hooks";
81
79
  interface SerenitySimpleSettings {
82
80
  /** F1 双端口网关总开关 */
83
81
  gatewayEnabled: boolean;
84
- /** F2 超限重建总开关 */
85
- rebuildEnabled: boolean;
86
82
  /** F2 触发阈值(需求① S142 用户拍板:百分比比例 → K 数值;projectedTokens ≥ thresholdK*1000 触发) */
87
83
  rebuildThresholdK: number;
88
84
  /** F4 Skiff 调试服务总开关(实验性;默认关——不随插件加载自动启动,人工开启) */
@@ -33,6 +33,11 @@ export declare const EVENT_LABEL: {
33
33
  readonly guard: "BOUNDARY GUARD";
34
34
  /** SESSION.md 体积超限重写提醒(v1.31.1,S142 用户需求) */
35
35
  readonly compaction: "LOGBOOK COMPACTION";
36
+ /** **交界便签(REBUILD-TODO.md)体积超限清理提醒**(v1.49.0,owner 2026-09-26 需求)。
37
+ *
38
+ * 🔴 **刻意与 `compaction` 分开**:那条要求"**重写 SESSION.md 并保留 EAP 骨架**",
39
+ * 本条只要求"**清理便签**"——共用一个 token 会让模型误以为要动 SESSION.md。 */
40
+ readonly scratchPrune: "SCRATCH PRUNE";
36
41
  };
37
42
  /** 家族标识(所有动态注入统一前缀) */
38
43
  export declare const ASSISTANT_PREFIX = "TRAJECTORY-ASSISTANT";
@@ -42,16 +47,24 @@ export declare function eventToken(event: keyof typeof EVENT_LABEL): string;
42
47
  export declare const ACK_PREFIX = "TRAJECTORY-ASSISTANT-recorded";
43
48
  export declare const ACK_SKIP_PREFIX = "TRAJECTORY-ASSISTANT-skipped";
44
49
  /**
45
- * rebuild 交接区块标题(v1.31.1,S142 用户需求)。
50
+ * rebuild 交界便签的**文件名**(v1.49.0;owner 2026-09-26 需求)。
46
51
  *
47
- * 用途:rebuild 提醒要求模型把**当前手头事项**写到 SESSION.md **末尾**的这个小标题之下;
48
- * rebuild 锚点随后要求重建后的自己去读这一段并处理。两侧引用**同一个常量**——
49
- * 标题一旦漂移,读侧就找不到写侧写的东西(单一真相源)。
52
+ * ## 它取代了什么(owner 原话:*"砍掉 in-flight,这正是目的"*)
53
+ * v1.31.1 的交界协议要求把手头事项写进 **SESSION.md 末尾**的 `## In-flight (rebuild handover)` 段。
54
+ * 实测的代价(S142 自身取证):In-flight 段曾积 **12 块**(含多份**已被取代**的交接),
55
+ * 而 SESSION.md 是**有格式义务**的持久文件(7 段固定 + 粒度受 EAP 约束)⇒
56
+ * ① 持久文件被临时物污染 ② 每次整理都要付"压缩+归档+保真"三件套(该文件曾达 292 KB)。
50
57
  *
51
- * 形态(用户拍板 Q2):**固定英文标题行**(不是 HTML 注释锚)——人读友好;ACC 写死英文,
52
- * 因为 SESSION.md 的正文语言归 CCC,但两侧的**机械锚点**必须逐字一致。
58
+ * ⇒ 分家:**只用一次的交界材料**写进这份**临时便签**(无格式义务、无留档义务、可随时丢弃)。
59
+ *
60
+ * ## 🔴 三处引用同一常量(单一真相源)
61
+ * **写侧** = `seams/keeper.ts` 的 `rebuildReminderText()`|**读侧** = `rebuild.ts` 的重建锚点|
62
+ * **体积提醒** = `seams/keeper.ts` 的 `rebuildTodoReminderText()`。
63
+ * 文件名一旦漂移,写侧写的东西读侧就找不到 —— **且没有任何机械信号会报警**。
64
+ *
65
+ * 🔴 **不得改名**(owner 已具名)。形态 = **固定文件名**(人读友好;比照旧 IN_FLIGHT_HEADING 的取舍)。
53
66
  */
54
- export declare const IN_FLIGHT_HEADING = "## In-flight (rebuild handover)";
67
+ export declare const REBUILD_TODO_FILENAME = "REBUILD-TODO.md";
55
68
  /** 风格档位:plain(默认,精确文本)/ metaphor(借星舰词——产品隐喻非游戏词) */
56
69
  type TrajectoryStyle = 'plain' | 'metaphor';
57
70
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.48.1",
3
+ "version": "1.49.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1。",
5
5
  "license": "MIT",
6
6
  "repository": {