@shgroup/dsh-serenity-hooks 1.39.1 → 1.39.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/README.md CHANGED
@@ -270,10 +270,11 @@ DSH 一个进程可以同时带多个工作区,每个工作区各自对接自
270
270
 
271
271
  **定时唤醒**:用 `container_trajectory send-later` 登记一条"唤醒" = 未来某时刻 + 一条消息——可以给自己预约,也可以唤醒别的轨迹;落在工作区内的 `AGENT_SESSIONS/wake-registry.json`(可读可审计),到点由中心调度器投递,**不阻塞、不等待、也不回执**。
272
272
 
273
- **即时投递**:用 `container_trajectory send-now` 把一条消息**现在**就递过去——目标在内存里就**立即注入**;不在内存里就**冷载入后投递**(**等效于直接唤醒**,但**不等调度器的 5 分钟节拍**)。它**有同步回执**(告诉你走了 live 还是冷载入),**不落注册表**(那张表专管"未来时刻")。
273
+ **即时投递**:用 `container_trajectory send-now` 把一条消息**现在**就递过去,并且**不排队**——目标在内存里且**正在跑轮次**,就 `steer` **当场注入当前轮**;目标空闲就**立即起一轮**;目标不在内存里就**冷载入后投递**(**等效于直接唤醒**,但**不等调度器的 5 分钟节拍**)。它**有同步回执**(告诉你走了 live 还是冷载入),**不落注册表**(那张表专管"未来时刻")。
274
274
 
275
275
  - 两者只差两处:**时刻**(现在 / 未来)与**回执**(有 / 无)。"预约"天生是 fire-and-forget(不可回收、不回执),"递话"则是一次调用一次答复——**各自语义干净,不把两种语义塞进同一个动作**(🔴 **更名**:这对动作原名 `wake-later` / `send-message`,现名 **`send-later` / `send-now`**——族名取共享词干 `send-`、轴取 `-later`/`-now`,因为「唤醒」是两条路径**共有**的属性,不配做区分;**硬切无别名**)
276
- - ⚠️ **回执只到"已入队"**:它**不表示**目标已经执行或答复;目标正在跑轮次时,消息在**轮次边界**生效(**不打断当前轮**)。要确证"目标真的动了",得看它自己的 `SESSION.md`
276
+ - ⚠️ **回执只到"已注入 / 已起轮"**:它**不表示**目标已经执行或答复。要确证"目标真的动了",得看它自己的 `SESSION.md`(**不得凭回执结案**)
277
+ - ⚠️ **唯一的例外路径**:`steer` 不可用时**退回排队**(fail-safe:**宁可排队,不可静默丢消息**)——此时回执文案写明"steer 不可用 ⇒ 退回 followup"。**`send-later` 不受影响**:预约的本职是"到点唤起",在跑时排队不打断当前轮是它被实证验收过的行为
277
278
 
278
279
  - **目标没打开也能唤醒**:会话不在内存里时先把它载入再投递(**冷唤醒**);载入不了则条目留在登记表里并记下原因,**不静默丢弃**
279
280
  - **"周期"归工作区自己**:要一轮接一轮,就由**收到唤醒的那条轨迹**在每轮结束时再排一次下一轮——ACC 只提供"到点投递一条消息"这个原语,不替工作区决定节拍(焦点、节拍、偏见都由工作区自定;任意条轨迹并行、互不干扰)
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.39.1",
3
+ "version": "1.39.2",
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/index.js CHANGED
@@ -2,11 +2,11 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { a as hostSettings, c as hostWebServer, i as hostSessions, n as hostInjected, o as hostSubagents, r as hostService, s as hostWeb, t as hostAgents } from "./access-fiehjxV6.js";
3
3
  import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$1, f as readExclusiveTools, h as resolveInside, i as findGitRoot, l as pathInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readHandymanConfig, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-NlLr_sxy.js";
4
4
  import { i as cccRootForExec, n as agentCwdFor, o as listCccs, r as cccRootForCwd, t as NO_CCC_FROM_AGENT_CWD } from "./ccc-roots-Bd_SjEs7.js";
5
+ import { C as localFileStamp, S as isoLocal, _ as sessionsRoot, a as DEFAULT_SESSION_SCOPE, b as summarize, c as createSession, d as getActiveSessionInfo, f as listSessions, g as sessionEvents$1, h as resolveSessionByTitle, i as resolveSessionTrajectoryLabel, l as extractSessionMdPathFromText, m as readActiveSessionMd, o as TRAJECTORY_ACTIONS, p as parseSessionContextFromEvents, r as readLastBound, s as clearActiveSessionInfo, t as appendBound, u as findSession, v as setActiveSessionInfo, x as useSession, y as showSession } from "./trajectory-bound-OyuNkn6e.js";
5
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-B2ajFypI.js";
6
7
  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-CEHL5cek.js";
7
8
  import { a as unregisterSkiffSession$1, i as skiffSessionSnapshot$1, n as skiffRoleFor$1, r as skiffSessionInfo$1, t as registerSkiffSession$1 } from "./skiff-registry-FTjoWQTQ.js";
8
- import { _ as sessionsRoot, a as DEFAULT_SESSION_SCOPE, b as summarize, c as createSession, d as getActiveSessionInfo, f as listSessions, g as sessionEvents$1, h as resolveSessionByTitle, i as resolveSessionTrajectoryLabel, l as extractSessionMdPathFromText, m as readActiveSessionMd, o as TRAJECTORY_ACTIONS, p as parseSessionContextFromEvents, r as readLastBound, s as clearActiveSessionInfo, t as appendBound, u as findSession, v as setActiveSessionInfo, x as useSession, y as showSession } from "./trajectory-bound-CApZRbvx.js";
9
- import { a as WAKE_CATCH_UP_MS, i as registerDisposer, n as sendToTrajectory, o as addWake, r as wakeSchedulerState, s as listWakes, t as registerWakeScheduler } from "./wake-scheduler-B50eu0jb.js";
9
+ import { a as WAKE_CATCH_UP_MS, i as registerDisposer, n as sendToTrajectory, o as addWake, r as wakeSchedulerState, s as listWakes, t as registerWakeScheduler } from "./wake-scheduler-D0HDjEO8.js";
10
10
  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-DMDUoQum.js";
11
11
  import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-CtFYm53S.js";
12
12
  import z from "@deepseek-ai/schemastery";
@@ -73,7 +73,7 @@ function getFileInfo(absPath, name) {
73
73
  type: detectFileType(stat),
74
74
  size: stat.size,
75
75
  sizeHuman: humanSize(stat.size),
76
- mtime: stat.mtime.toISOString()
76
+ mtime: isoLocal(stat.mtime)
77
77
  };
78
78
  } catch {
79
79
  return {
@@ -356,7 +356,7 @@ function runCcFs(root, args) {
356
356
  `path: ${safeRel(root, absPath)}`,
357
357
  `type: ${fileType}`,
358
358
  `size: ${stat.size} (${humanSize(stat.size)})`,
359
- `mtime: ${stat.mtime.toISOString()}`,
359
+ `mtime: ${isoLocal(stat.mtime)}`,
360
360
  `mode: ${modeStr}`,
361
361
  `uid: ${stat.uid}`,
362
362
  `gid: ${stat.gid}`
@@ -552,7 +552,7 @@ async function runKit(root, args, hostCtx) {
552
552
  case "time": {
553
553
  const now = /* @__PURE__ */ new Date();
554
554
  return {
555
- now_iso: now.toISOString(),
555
+ now_iso: isoLocal(now),
556
556
  now_local: now.toString(),
557
557
  epoch_ms: now.getTime()
558
558
  };
@@ -965,7 +965,7 @@ Directory of ACC capabilities — each area lists where to go for details (guide
965
965
  专属工具(**默认对所有 CCC 隐藏**,只有在自己 exclusiveTools 里点名的 CCC 可见)→ acc-diag(运行态诊断)
966
966
  ④ 角色与对外面 → container_admin role(Skiff 认知子集角色:guide/validate/apply/list——skiff_admin 已并入)
967
967
  ACP(程序化 JSON-RPC 3100)/ Skiff 问答页(公网 ask)——面板「外部能力」组
968
- ⑤ 自主与接入 → container_trajectory send-now(跨轨迹的**即时**递送:live 目标立即注入,非 live 冷载入——等效于直接 wake;**有同步回执**)
968
+ ⑤ 自主与接入 → container_trajectory send-now(跨轨迹的**即时**递送,**不排队**:live 目标在跑就当场注入当前轮、空闲则立即起轮;非 live 冷载入——等效于直接 wake;**有同步回执**)
969
969
  container_trajectory send-later(跨轨迹的"未来时刻 + 一条 message";fire-and-forget,无回执无回收)
970
970
  weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:msm("weixin-doctor", ["guide"]))
971
971
  ⑥ CCC 配置总览 → container_admin msm ccc-config(9 段:handyman/sessionKeeper/localstore/hooks/safeMode/skiff/trajectory/weixin/**exclusiveTools**)
@@ -1257,7 +1257,27 @@ function loadMsmEntries(root) {
1257
1257
  function findEntry(root, name) {
1258
1258
  return loadMsmEntries(root).find((e) => e.name === name) ?? null;
1259
1259
  }
1260
- /** 扫描各 skill scripts/ 下的非测试脚本(DC-M3 正向基准,对齐 osp) */
1260
+ /**
1261
+ * 扫描各 skill `scripts/` 下的**未注册候选**(DC-M3 正向基准,对齐 osp)。
1262
+ *
1263
+ * 🔴 **入口判据**(S151 轮 112 跨轨迹信号 → S142 实测精化,2026-09-17;取证见 S142 §0q):
1264
+ * · **入口 ⟺ 有 shebang(`^#!`) ∨ 声明了 `function main(`** —— **`process.argv` 单独不算**。
1265
+ * 反例(实测):`home-desk/scripts/lib/desk-dispatch.ts` 含 **5** 处 `process.argv`,
1266
+ * 但无 shebang / 无 `main()`,且被 `desk-window.ts`、`desk-vision.ts` **import** ⇒ **它是库**。
1267
+ * · **库 ⟺ 无 shebang ∧ 无 `main()` ∧ 被其它脚本 import** ⇒ **不是 MSM 候选,不上报**。
1268
+ * ⚠️ **"有 `.test.ts`" 不是入口证据** —— 库**同样**有测试
1269
+ * (`home-serenity/scripts/dev-kit/dev-kit.test.ts` 开头自述"**共享库单测**")。
1270
+ * 📌 实现只测**前两条**(shebang / `main()`):第三条"被 import"是**描述性**的,不作判据 ——
1271
+ * 因为**无 CLI 入口的文件本来就不可能被 `msm()` 调用**,上报它只会得到一条无法处置的欠账。
1272
+ * 🔗 **与 DC-M2 自洽**:DC-M2 本就要求**已注册**脚本必须有 main() 守卫 ⇒ 把"无入口"的文件
1273
+ * 排除出候选,与注册表的契约**同向**(两条判据不会互相打架)。
1274
+ *
1275
+ * 🔴 **本函数只扫 `scripts/` 一层(非递归),这是当前刻意的语义**(嵌套子域对容器不可见;
1276
+ * 见 CCC `docs/t024-msm-skill-inventory.md` §3b)。**若将来改为递归,上面的判据就是护栏**:
1277
+ * 嵌套候选实测 **8 个**(`dev-kit/` 5 + `home-desk/scripts/lib/` 3)**全部是库**——
1278
+ * 不按判据过滤,一次递归就会制造 8 条假欠账,并可能诱使后续轮次把库注册成**假 MSM**。
1279
+ * ⇒ **改递归前先读本条注释。**
1280
+ */
1261
1281
  function scanSkillScripts(root) {
1262
1282
  const out = [];
1263
1283
  const skillsDir = join(root, ".opencode", "skills");
@@ -1265,10 +1285,34 @@ function scanSkillScripts(root) {
1265
1285
  for (const skill of readdirSync(skillsDir)) {
1266
1286
  const scriptsDir = join(skillsDir, skill, "scripts");
1267
1287
  if (!existsSync(scriptsDir)) continue;
1268
- for (const f of readdirSync(scriptsDir)) if (/\.(ts|js|mjs)$/.test(f) && !/\.(test|spec)\./.test(f)) out.push(join(".opencode", "skills", skill, "scripts", f));
1288
+ for (const f of readdirSync(scriptsDir)) {
1289
+ const abs = join(scriptsDir, f);
1290
+ let isFile = false;
1291
+ try {
1292
+ isFile = statSync(abs).isFile();
1293
+ } catch {
1294
+ continue;
1295
+ }
1296
+ if (!isFile) continue;
1297
+ if (!/\.(ts|js|mjs)$/.test(f)) continue;
1298
+ if (/\.(test|spec)\./.test(f)) continue;
1299
+ if (!hasCliEntry(abs)) continue;
1300
+ out.push(join(".opencode", "skills", skill, "scripts", f));
1301
+ }
1269
1302
  }
1270
1303
  return out.sort();
1271
1304
  }
1305
+ /** 入口判据的机械实现(判据、反例与递归化护栏见 `scanSkillScripts` 注释) */
1306
+ function hasCliEntry(absPath) {
1307
+ let text;
1308
+ try {
1309
+ text = readFileSync(absPath, "utf-8");
1310
+ } catch {
1311
+ return true;
1312
+ }
1313
+ if (/^#!/.test(text)) return true;
1314
+ return /\b(?:async\s+)?function\s+main\s*\(/.test(text);
1315
+ }
1272
1316
  /**
1273
1317
  * **注册表「质量契约」判据(DC-M1~M4)** —— 具名出口(C5 观察面归一,2026-09-15)。
1274
1318
  *
@@ -1666,7 +1710,7 @@ function writeProgress(root, label, p) {
1666
1710
  writeFileSync(json, JSON.stringify({
1667
1711
  ...p,
1668
1712
  status: p.status ?? "running",
1669
- updated: (/* @__PURE__ */ new Date()).toISOString()
1713
+ updated: isoLocal()
1670
1714
  }, null, 2) + "\n", "utf-8");
1671
1715
  const lines = [
1672
1716
  `# handyman: ${label}`,
@@ -1694,7 +1738,7 @@ function writeFailedStatus(root, label, info) {
1694
1738
  status: "failed",
1695
1739
  errorCode: info.errorCode,
1696
1740
  errorMessage: info.errorMessage,
1697
- updated: (/* @__PURE__ */ new Date()).toISOString(),
1741
+ updated: isoLocal(),
1698
1742
  lastResponse: prev?.lastResponse ?? ""
1699
1743
  }, null, 2) + "\n", "utf-8");
1700
1744
  }
@@ -1821,7 +1865,7 @@ function listActiveHandymen(root) {
1821
1865
  });
1822
1866
  } catch {}
1823
1867
  }
1824
- out.sort((a, b) => a.updated < b.updated ? 1 : -1);
1868
+ out.sort((a, b) => Date.parse(b.updated) - Date.parse(a.updated));
1825
1869
  return out;
1826
1870
  }
1827
1871
  //#endregion
@@ -2808,7 +2852,7 @@ async function runHandymanJob(ctx, opts) {
2808
2852
  done: false,
2809
2853
  label,
2810
2854
  model,
2811
- updated: (/* @__PURE__ */ new Date()).toISOString(),
2855
+ updated: isoLocal(),
2812
2856
  lastResponse
2813
2857
  };
2814
2858
  writeProgress(root, label, progress);
@@ -2824,7 +2868,7 @@ async function runHandymanJob(ctx, opts) {
2824
2868
  done,
2825
2869
  label,
2826
2870
  model,
2827
- updated: (/* @__PURE__ */ new Date()).toISOString(),
2871
+ updated: isoLocal(),
2828
2872
  lastResponse,
2829
2873
  status: done ? "done" : "running"
2830
2874
  });
@@ -3125,7 +3169,7 @@ function renderText$3(value) {
3125
3169
  function createTrajectoryTool(ctx) {
3126
3170
  return defineTool({
3127
3171
  name: "container_trajectory",
3128
- description: "Trajectory (AGENT_SESSIONS/ — the persistent body of a trajectory; the dsh conversation is only its rebuildable carrier). Lifecycle + delivery: list (inventory with stats and anomaly marks) / show (read one SESSION.md) / create / use (activate for this conversation, with inline integrity check) / rebuild (clear-and-rebuild the current conversation in place, Ship of Theseus) / send-now (deliver ONE message to a trajectory RIGHT NOW — live: injected immediately; not live: cold-resumed, i.e. equivalent to a direct wake; returns a synchronous receipt) / send-later (schedule ONE message to any trajectory at a future instant — fire-and-forget: no receipt, no recall, no panel). create requires --desc <desc> [--goal] or --issue <ticket> (exactly one) plus --summary (≤20 chars). use requires --summary (≤20 chars) and may pass --force to switch away from the currently-bound trajectory. rebuild requires --summary (next-phase summary ≤20 chars) + optional --note (task focus for the rebuilt self). send-later requires target (S### or AGENT_SESSIONS dir name) + at (RFC3339 or +30m/+2h) + message. send-now requires target + message. Its receipt means the message was QUEUED into the target (it does NOT mean the target ran or replied; a busy target applies it at its next round boundary). The summary is appended to the dsh session title (S###-YYYY-MM-DD-<summary>); the S### id and date stay server-derived.",
3172
+ description: "Trajectory (AGENT_SESSIONS/ — the persistent body of a trajectory; the dsh conversation is only its rebuildable carrier). Lifecycle + delivery: list (inventory with stats and anomaly marks) / show (read one SESSION.md) / create / use (activate for this conversation, with inline integrity check) / rebuild (clear-and-rebuild the current conversation in place, Ship of Theseus) / send-now (deliver ONE message to a trajectory RIGHT NOW — live: injected immediately; not live: cold-resumed, i.e. equivalent to a direct wake; returns a synchronous receipt) / send-later (schedule ONE message to any trajectory at a future instant — fire-and-forget: no receipt, no recall, no panel). create requires --desc <desc> [--goal] or --issue <ticket> (exactly one) plus --summary (≤20 chars). use requires --summary (≤20 chars) and may pass --force to switch away from the currently-bound trajectory. rebuild requires --summary (next-phase summary ≤20 chars) + optional --note (task focus for the rebuilt self). send-later requires target (S### or AGENT_SESSIONS dir name) + at (RFC3339 or +30m/+2h) + message. send-now requires target + message. It delivers IMMEDIATELY (never queued behind the current turn): a busy target is steered mid-turn so it reads the message in the CURRENT turn, and an idle target starts a turn right away. Its receipt means the message was injected into the target (it does NOT mean the target ran or replied). The summary is appended to the dsh session title (S###-YYYY-MM-DD-<summary>); the S### id and date stay server-derived.",
3129
3173
  parameters: {
3130
3174
  action: {
3131
3175
  type: "string",
@@ -3295,8 +3339,8 @@ function createTrajectoryTool(ctx) {
3295
3339
  output: [
3296
3340
  "✓ 已即时投递",
3297
3341
  ` · ${res.detail}`,
3298
- " · 语义:消息**已进入目标队列**;目标若正在跑轮次,则在**轮次边界**生效(不打断当前轮)。",
3299
- " · ⚠️ 回执只到「已入队」——**不表示**目标已执行或已答复。判\"目标真的动了\"须看它自己的 SESSION.md(文件级判据)。"
3342
+ " · 语义:**不排队** —— 目标正在跑轮次则**当场注入当前轮**(steer),空闲则立即起一轮(followup)。",
3343
+ " · ⚠️ 回执只到「已注入/已入队」——**不表示**目标已执行或已答复。判\"目标真的动了\"须看它自己的 SESSION.md(文件级判据)。"
3300
3344
  ].join("\n")
3301
3345
  };
3302
3346
  }
@@ -4049,12 +4093,12 @@ function syncSafeModeRestriction(agent, root) {
4049
4093
  const dispose = agent.ctx.tools.restrict({ deny: [...SAFE_MODE_DENY_TOOLS] });
4050
4094
  safeModeRestrictions.set(key, dispose);
4051
4095
  restrictDiag.lastKey = key;
4052
- restrictDiag.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
4096
+ restrictDiag.lastAttemptAt = isoLocal();
4053
4097
  restrictDiag.lastSuccess = true;
4054
4098
  restrictDiag.lastError = null;
4055
4099
  } catch (e) {
4056
4100
  restrictDiag.lastKey = key;
4057
- restrictDiag.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
4101
+ restrictDiag.lastAttemptAt = isoLocal();
4058
4102
  restrictDiag.lastSuccess = false;
4059
4103
  restrictDiag.lastError = e.message;
4060
4104
  console.error(`[serenity-hooks] restrict 失败 (key=${key}):`, e.message);
@@ -6394,7 +6438,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
6394
6438
  function setSafeMode(root, on) {
6395
6439
  const marker = resolve(root, SAFE_MODE_MARKER);
6396
6440
  if (on) {
6397
- if (!existsSync(marker)) writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
6441
+ if (!existsSync(marker)) writeFileSync(marker, isoLocal() + "\n", "utf-8");
6398
6442
  } else rmSync(marker, { force: true });
6399
6443
  return { on: isSafeModeOn(root) };
6400
6444
  }
@@ -6458,7 +6502,7 @@ function saveImageToTmp(root, mediaType, data) {
6458
6502
  const dir = join(root, IMAGE_UPLOAD_DIR);
6459
6503
  mkdirSync(dir, { recursive: true });
6460
6504
  const ext = EXT_BY_MEDIA[mediaType] ?? "img";
6461
- const filename = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
6505
+ const filename = `${localFileStamp()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
6462
6506
  writeFileSync(join(dir, filename), bytes);
6463
6507
  return `${IMAGE_UPLOAD_DIR}/${filename}`;
6464
6508
  }
@@ -6486,7 +6530,7 @@ function saveFileToTmp(root, fileName, data) {
6486
6530
  const dir = join(root, FILE_UPLOAD_DIR);
6487
6531
  mkdirSync(dir, { recursive: true });
6488
6532
  const safeName = sanitizeFileName(fileName);
6489
- const filename = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}-${safeName}`;
6533
+ const filename = `${localFileStamp()}-${Math.random().toString(36).slice(2, 8)}-${safeName}`;
6490
6534
  writeFileSync(join(dir, filename), bytes);
6491
6535
  return `${FILE_UPLOAD_DIR}/${filename}`;
6492
6536
  }
@@ -6997,7 +7041,7 @@ function registerStatusApi(ctx, opts = {}) {
6997
7041
  candidates: candidates.map((c) => ({
6998
7042
  id: c.id,
6999
7043
  project: c.project,
7000
- lastActive: new Date(c.lastActiveMs).toISOString()
7044
+ lastActive: isoLocal(c.lastActiveMs)
7001
7045
  }))
7002
7046
  });
7003
7047
  return;
@@ -8853,7 +8897,7 @@ function writeRebuildDiag(root, entry) {
8853
8897
  if (entry.event === "rebuilt") s.rebuiltCount += 1;
8854
8898
  if (entry.event === "ttl-dropped") s.droppedCount += 1;
8855
8899
  if (entry.event === "failed") s.failedCount += 1;
8856
- s.lastTs = (/* @__PURE__ */ new Date()).toISOString();
8900
+ s.lastTs = isoLocal();
8857
8901
  s.lastSessionId = entry.sessionId;
8858
8902
  s.lastEvent = entry.event;
8859
8903
  s.detail = entry.detail;
@@ -9029,7 +9073,7 @@ function registerRebuildTurnHook(ctx) {
9029
9073
  writeRebuildDiag(resolveSerenityRootFor(agent), {
9030
9074
  sessionId: id,
9031
9075
  event: "ttl-dropped",
9032
- detail: `queuedAt=${new Date(pending.queuedAt).toISOString()} older than ${PENDING_TTL_MS / 6e4}min TTL — turn did not end in time`
9076
+ detail: `queuedAt=${isoLocal(pending.queuedAt)} older than ${PENDING_TTL_MS / 6e4}min TTL — turn did not end in time`
9033
9077
  });
9034
9078
  return;
9035
9079
  }
@@ -14271,7 +14315,7 @@ function containerStatus(opts = {}) {
14271
14315
  */
14272
14316
  /** 人读渲染一行时钟状态("为何没有 tick" 的第一手判据) */
14273
14317
  function renderClock(label, s) {
14274
- const ts = (ms) => ms === null ? "(从未)" : new Date(ms).toISOString();
14318
+ const ts = (ms) => ms === null ? "(从未)" : isoLocal(ms);
14275
14319
  const parts = [
14276
14320
  `armed=${s.armed}`,
14277
14321
  `全局闸=${s.enabled}`,
package/lib/time.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * time.ts — ACC **时间呈现的单一真相源**(S142 2026-09-17,所有者令:「统一 ACC 所有使用时间的地方使用当地时区」)
3
+ *
4
+ * ── 存在的理由:**把"时刻"与"呈现"分开**(R↓)
5
+ *
6
+ * · **时刻(instant)** = epoch 毫秒(`Date.now()`)—— **与时区无关**。
7
+ * 比较 / 差值 / TTL / 调度 / TOTP 一律照旧,**不得"本地化"**:
8
+ * 把时刻换成"当地钟面"会让它**不可比**(跨偏移、跨夏令时都会错)。
9
+ * · **呈现(rendition)** = 给人看、或存给人看的字符串 —— **一律当地时区 + 显式偏移**,
10
+ * 如 `2026-09-17T17:20:00.123+08:00`。
11
+ * ⚠️ **偏移不能省**(这是本模块最容易被"简化"掉的一点):省了就丢掉"这是哪一刻",
12
+ * 而且**跨格式的字符串排序会错**(`...T09:00:00Z` 与 `...T17:00:00+08:00` 是同一刻,
13
+ * 字典序却相反)。带偏移的 RFC3339 **同时**满足"当地钟面"与"可解析、可比较"。
14
+ * · **当地** = 运行机器的本地时区(node 在 `Asia/Shanghai` 即 `+08:00`)—— **不硬编码时区名**,
15
+ * 否则换机器/换地区就错。偏移**按被格式化的那个时刻求**(`getTimezoneOffset` 随夏令时变化)。
16
+ *
17
+ * ── 用法边界(本模块**只**做呈现;不做时刻运算)
18
+ * ✅ `isoLocal()` / `localDate()` / `localDateTime()` / `localHuman()` / `localFileStamp()`
19
+ * ❌ 不要用它们做排序/比较/差值 —— 那些用 `Date.now()` / `Date.parse()` / epoch 运算。
20
+ *
21
+ * 出处:所有者 2026-09-17 令(统一当地时区 + 注入消息带当前时间);设计判据见 S142 §0r。
22
+ */
23
+ /**
24
+ * 该**时刻**的 UTC 偏移(`+08:00` / `-05:00`)。
25
+ * ⚠️ 必须传入要格式化的那个 date —— 偏移随夏令时变化,不能"取当前的偏移去格式化另一个时刻"。
26
+ */
27
+ export declare function tzOffset(date?: Date): string;
28
+ /**
29
+ * **RFC3339 当地时区**(带毫秒与偏移):`2026-09-17T17:20:00.123+08:00`。
30
+ * 存储与会话间交换的**默认呈现**(可被 `Date.parse` 原样解析回同一时刻)。
31
+ */
32
+ export declare function isoLocal(value?: Date | number): string;
33
+ /** 当地日期 `YYYY-MM-DD`(目录名/日期前缀) */
34
+ export declare function localDate(value?: Date | number): string;
35
+ /** 当地 `YYYY-MM-DD HH:mm:ss`(人读;**不带偏移**,只用于已经明确"是本地时间"的展示位) */
36
+ export declare function localDateTime(value?: Date | number): string;
37
+ /** 当地 `YYYY-MM-DD HH:mm`(人读,到分钟) */
38
+ export declare function localDateTimeMinutes(value?: Date | number): string;
39
+ /**
40
+ * **人读 + 偏移**:`2026-09-17 17:20:00 +08:00`。
41
+ * 给"注入给 agent 的文本"用 —— 既要一眼看懂当地钟面,又不丢"这是哪一刻"。
42
+ */
43
+ export declare function localHuman(value?: Date | number): string;
44
+ /**
45
+ * 文件名/ID 用的紧凑当地戳:`2026-09-17T172000`(可排序、无冒号/点,跨平台安全)。
46
+ * 与 CCC 的文件命名底线一致(`<ISO8601 本地>-<短随机>`)。
47
+ */
48
+ export declare function localFileStamp(value?: Date | number): string;
49
+ /** 唤醒条目 id 的当地戳部分:`YYYYMMDD-HHmm`(`w-<stamp>-<hex>`) */
50
+ export declare function localIdStamp(value?: Date | number): string;
@@ -1,6 +1,92 @@
1
1
  import { r as cccRootForCwd } from "./ccc-roots-Bd_SjEs7.js";
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
4
+ //#region src/time.ts
5
+ /**
6
+ * time.ts — ACC **时间呈现的单一真相源**(S142 2026-09-17,所有者令:「统一 ACC 所有使用时间的地方使用当地时区」)
7
+ *
8
+ * ── 存在的理由:**把"时刻"与"呈现"分开**(R↓)
9
+ *
10
+ * · **时刻(instant)** = epoch 毫秒(`Date.now()`)—— **与时区无关**。
11
+ * 比较 / 差值 / TTL / 调度 / TOTP 一律照旧,**不得"本地化"**:
12
+ * 把时刻换成"当地钟面"会让它**不可比**(跨偏移、跨夏令时都会错)。
13
+ * · **呈现(rendition)** = 给人看、或存给人看的字符串 —— **一律当地时区 + 显式偏移**,
14
+ * 如 `2026-09-17T17:20:00.123+08:00`。
15
+ * ⚠️ **偏移不能省**(这是本模块最容易被"简化"掉的一点):省了就丢掉"这是哪一刻",
16
+ * 而且**跨格式的字符串排序会错**(`...T09:00:00Z` 与 `...T17:00:00+08:00` 是同一刻,
17
+ * 字典序却相反)。带偏移的 RFC3339 **同时**满足"当地钟面"与"可解析、可比较"。
18
+ * · **当地** = 运行机器的本地时区(node 在 `Asia/Shanghai` 即 `+08:00`)—— **不硬编码时区名**,
19
+ * 否则换机器/换地区就错。偏移**按被格式化的那个时刻求**(`getTimezoneOffset` 随夏令时变化)。
20
+ *
21
+ * ── 用法边界(本模块**只**做呈现;不做时刻运算)
22
+ * ✅ `isoLocal()` / `localDate()` / `localDateTime()` / `localHuman()` / `localFileStamp()`
23
+ * ❌ 不要用它们做排序/比较/差值 —— 那些用 `Date.now()` / `Date.parse()` / epoch 运算。
24
+ *
25
+ * 出处:所有者 2026-09-17 令(统一当地时区 + 注入消息带当前时间);设计判据见 S142 §0r。
26
+ */
27
+ /** 两位补零(超两位按原样,用于毫秒 3 位) */
28
+ function pad(n, width = 2) {
29
+ return String(n).padStart(width, "0");
30
+ }
31
+ /**
32
+ * 该**时刻**的 UTC 偏移(`+08:00` / `-05:00`)。
33
+ * ⚠️ 必须传入要格式化的那个 date —— 偏移随夏令时变化,不能"取当前的偏移去格式化另一个时刻"。
34
+ */
35
+ function tzOffset(date = /* @__PURE__ */ new Date()) {
36
+ const minutesEast = -date.getTimezoneOffset();
37
+ const sign = minutesEast >= 0 ? "+" : "-";
38
+ const abs = Math.abs(minutesEast);
39
+ return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
40
+ }
41
+ /** 归一成 Date(接受 Date 或 epoch 毫秒) */
42
+ function toDate(value = Date.now()) {
43
+ return value instanceof Date ? value : new Date(value);
44
+ }
45
+ /**
46
+ * **RFC3339 当地时区**(带毫秒与偏移):`2026-09-17T17:20:00.123+08:00`。
47
+ * 存储与会话间交换的**默认呈现**(可被 `Date.parse` 原样解析回同一时刻)。
48
+ */
49
+ function isoLocal(value = Date.now()) {
50
+ const d = toDate(value);
51
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}` + tzOffset(d);
52
+ }
53
+ /** 当地日期 `YYYY-MM-DD`(目录名/日期前缀) */
54
+ function localDate(value = Date.now()) {
55
+ const d = toDate(value);
56
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
57
+ }
58
+ /** 当地 `YYYY-MM-DD HH:mm:ss`(人读;**不带偏移**,只用于已经明确"是本地时间"的展示位) */
59
+ function localDateTime(value = Date.now()) {
60
+ const d = toDate(value);
61
+ return `${localDate(d)} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
62
+ }
63
+ /** 当地 `YYYY-MM-DD HH:mm`(人读,到分钟) */
64
+ function localDateTimeMinutes(value = Date.now()) {
65
+ const d = toDate(value);
66
+ return `${localDate(d)} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
67
+ }
68
+ /**
69
+ * **人读 + 偏移**:`2026-09-17 17:20:00 +08:00`。
70
+ * 给"注入给 agent 的文本"用 —— 既要一眼看懂当地钟面,又不丢"这是哪一刻"。
71
+ */
72
+ function localHuman(value = Date.now()) {
73
+ const d = toDate(value);
74
+ return `${localDateTime(d)} ${tzOffset(d)}`;
75
+ }
76
+ /**
77
+ * 文件名/ID 用的紧凑当地戳:`2026-09-17T172000`(可排序、无冒号/点,跨平台安全)。
78
+ * 与 CCC 的文件命名底线一致(`<ISO8601 本地>-<短随机>`)。
79
+ */
80
+ function localFileStamp(value = Date.now()) {
81
+ const d = toDate(value);
82
+ return `${localDate(d)}T${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
83
+ }
84
+ /** 唤醒条目 id 的当地戳部分:`YYYYMMDD-HHmm`(`w-<stamp>-<hex>`) */
85
+ function localIdStamp(value = Date.now()) {
86
+ const d = toDate(value);
87
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
88
+ }
89
+ //#endregion
4
90
  //#region src/trajectory-ops.ts
5
91
  /**
6
92
  * trajectory-ops.ts — 轨迹工具纯操作层(零 DSH 依赖,可独立单测)
@@ -142,7 +228,7 @@ function showSession(root, key) {
142
228
  }
143
229
  /** 生成 SESSION.md 模板(对齐 osp:goal 写入目标段,时间戳 YYYY-MM-DD HH:mm) */
144
230
  function sessionMdTemplate(title, id, goal, now) {
145
- const ts = now.toISOString().slice(0, 16).replace("T", " ");
231
+ const ts = localDateTimeMinutes(now);
146
232
  return `# SESSION: ${title}\n- ID: ${id}\n\n## 目标\n${goal ?? "(待补充)"}\n\n## 状态\n- [ ] 进行中\n\n## 关键决策\n| # | 决策 | 理由 |\n|---|------|------|\n| 1 | | |\n\n## 进度记录\n- ${ts} — 创建\n\n## 产出物\n- \n\n## 未解决的问题\n- \n`;
147
233
  }
148
234
  /** create 子命令(对齐 osp createSession:--desc/--issue 二选一 + dry-run + 长度限制) */
@@ -156,7 +242,7 @@ function createSession(opts) {
156
242
  const { root, desc, issue, goal, dryRun } = opts;
157
243
  const sessionsDir = sessionsRoot(root);
158
244
  const now = /* @__PURE__ */ new Date();
159
- const datePrefix = now.toISOString().slice(0, 10);
245
+ const datePrefix = localDate(now);
160
246
  if (!desc && !issue) throw new Error("create requires either --desc or --issue");
161
247
  if (desc && issue) throw new Error("--desc and --issue are mutually exclusive");
162
248
  if (issue) {
@@ -594,4 +680,4 @@ function appendBound(session, action, rec) {
594
680
  }
595
681
  }
596
682
  //#endregion
597
- export { sessionsRoot as _, DEFAULT_SESSION_SCOPE as a, summarize as b, createSession as c, getActiveSessionInfo as d, listSessions as f, sessionEvents as g, resolveSessionByTitle as h, resolveSessionTrajectoryLabel as i, extractSessionMdPathFromText as l, readActiveSessionMd as m, listBoundSessionIds as n, TRAJECTORY_ACTIONS as o, parseSessionContextFromEvents as p, readLastBound as r, clearActiveSessionInfo as s, appendBound as t, findSession as u, setActiveSessionInfo as v, useSession as x, showSession as y };
683
+ export { localFileStamp as C, isoLocal as S, localIdStamp as T, sessionsRoot as _, DEFAULT_SESSION_SCOPE as a, summarize as b, createSession as c, getActiveSessionInfo as d, listSessions as f, sessionEvents as g, resolveSessionByTitle as h, resolveSessionTrajectoryLabel as i, extractSessionMdPathFromText as l, readActiveSessionMd as m, listBoundSessionIds as n, TRAJECTORY_ACTIONS as o, parseSessionContextFromEvents as p, readLastBound as r, clearActiveSessionInfo as s, appendBound as t, findSession as u, setActiveSessionInfo as v, localHuman as w, useSession as x, showSession as y };
@@ -22,11 +22,11 @@ export declare const WAKE_CATCH_UP_MS: number;
22
22
  type WakeState = 'pending' | 'delivered' | 'missed' | 'cancelled';
23
23
  /** 一条唤醒条目 */
24
24
  export interface WakeEntry {
25
- /** 稳定 id(不复用;`w-<UTC 到分钟>-<4 hex>`) */
25
+ /** 稳定 id(不复用;`w-<当地到分钟>-<4 hex>`) */
26
26
  id: string;
27
27
  /** 目标 trajectory:`S###` 或完整 AGENT_SESSIONS 目录名(编码无关,与绑定锚一致) */
28
28
  target: string;
29
- /** 绝对时刻(UTC RFC3339,如 `2026-09-14T01:00:00.000Z`) */
29
+ /** 绝对时刻(当地 RFC3339 带偏移,如 `2026-09-14T09:00:00.000+08:00`) */
30
30
  at: string;
31
31
  /** 唤醒 message(投递给目标 trajectory 的正文) */
32
32
  message: string;
@@ -34,13 +34,13 @@ export interface WakeEntry {
34
34
  state: WakeState;
35
35
  /** 发起者(轨迹标识,如 `S142`;可见性/审计用) */
36
36
  createdBy: string;
37
- /** 登记时刻(UTC RFC3339 */
37
+ /** 登记时刻(当地 RFC3339 带偏移) */
38
38
  createdAt: string;
39
39
  /** 已尝试投递次数 */
40
40
  attempts: number;
41
41
  /** 最近一次投递结果(人读;失败必须留痕——禁止静默) */
42
42
  lastResult: string | null;
43
- /** 投递成功时刻(UTC RFC3339 */
43
+ /** 投递成功时刻(当地 RFC3339 带偏移) */
44
44
  deliveredAt: string | null;
45
45
  }
46
46
  /** 注册表文件形态 */
@@ -59,7 +59,7 @@ export declare function loadWakeRegistry(root: string): {
59
59
  registry: WakeRegistry;
60
60
  error: string | null;
61
61
  };
62
- /** 条目 id:`w-<UTC 到分钟>-<4 hex>`(可读 + 低碰撞;不复用由"物理移除"保证) */
62
+ /** 条目 id:`w-<当地到分钟>-<4 hex>`(可读 + 低碰撞;不复用由"物理移除"保证) */
63
63
  export declare function newWakeId(nowMs: number, rand?: () => number): string;
64
64
  /**
65
65
  * 解析唤醒时刻。接受两种写法(设计文档 §7):
@@ -1,6 +1,6 @@
1
1
  import { r as hostService, t as hostAgents } from "./access-fiehjxV6.js";
2
2
  import { o as listCccs } from "./ccc-roots-Bd_SjEs7.js";
3
- import { _ as sessionsRoot, n as listBoundSessionIds, u as findSession } from "./trajectory-bound-CApZRbvx.js";
3
+ import { S as isoLocal, T as localIdStamp, _ as sessionsRoot, n as listBoundSessionIds, u as findSession, w as localHuman } from "./trajectory-bound-OyuNkn6e.js";
4
4
  import { t as readSimpleSettings } from "./settings-section-DMDUoQum.js";
5
5
  import { basename, dirname, join } from "node:path";
6
6
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -115,10 +115,9 @@ function saveWakeRegistry(root, registry) {
115
115
  };
116
116
  }
117
117
  }
118
- /** 条目 id:`w-<UTC 到分钟>-<4 hex>`(可读 + 低碰撞;不复用由"物理移除"保证) */
118
+ /** 条目 id:`w-<当地到分钟>-<4 hex>`(可读 + 低碰撞;不复用由"物理移除"保证) */
119
119
  function newWakeId(nowMs, rand = Math.random) {
120
- const iso = new Date(nowMs).toISOString();
121
- return `w-${`${iso.slice(0, 10).replace(/-/g, "")}-${iso.slice(11, 16).replace(":", "")}`}-${Math.floor(rand() * 65536).toString(16).padStart(4, "0")}`;
120
+ return `w-${localIdStamp(nowMs)}-${Math.floor(rand() * 65536).toString(16).padStart(4, "0")}`;
122
121
  }
123
122
  /**
124
123
  * 解析唤醒时刻。接受两种写法(设计文档 §7):
@@ -187,11 +186,11 @@ function addWake(root, input) {
187
186
  const entry = {
188
187
  id: newWakeId(input.nowMs),
189
188
  target,
190
- at: new Date(parsed.ms).toISOString(),
189
+ at: isoLocal(parsed.ms),
191
190
  message,
192
191
  state: "pending",
193
192
  createdBy: input.createdBy,
194
- createdAt: new Date(input.nowMs).toISOString(),
193
+ createdAt: isoLocal(input.nowMs),
195
194
  attempts: 0,
196
195
  lastResult: null,
197
196
  deliveredAt: null
@@ -606,6 +605,7 @@ async function acquireWakeAgent(ctx, root, dirName) {
606
605
  function buildWakeText(entry, target) {
607
606
  return [
608
607
  `[trajectory 唤醒] 到点(登记于 ${entry.createdAt},发起者 ${entry.createdBy || "未知"},id ${entry.id})。`,
608
+ `当前时间:${localHuman()}(当地时区)。`,
609
609
  "",
610
610
  `身份锚定:继续 ${target.dirName} 的 trajectory(SESSION.md: ${target.mdPath})。`,
611
611
  "",
@@ -654,29 +654,33 @@ async function deliverWake(ctx, root, entry) {
654
654
  }
655
655
  /**
656
656
  * 即时消息正文(**刻意不复用** {@link buildWakeText}):唤醒文本以「**到点**(登记于…)」开头,
657
- * 对即时投递那是**错的事实**。本函数显式区分"这是即时消息、不是到点唤醒"
658
- * 并写明"消息已入队、若此前在跑轮次则本轮结束后生效"(防目标误判时序)。
657
+ * 对即时投递那是**错的事实**。本函数显式区分"这是即时消息、不是到点唤醒"
658
+ *
659
+ * ⚠️ **v1.39.2 起"已入队"这句话必须删掉**(S142 §0r,所有者令「不排队、直接即时注入」):
660
+ * 旧文案写"消息已进入你的队列;你此前若正在跑轮次,它在本轮结束后生效"——
661
+ * 那是**旧的 followup 语义**;现在在跑的目标走 `steer`(当场注入当前轮),
662
+ * 留着这句话就是**注入一条假陈述**(比没有更坏:目标会误判自己看到它的时机)。
659
663
  * @param target 目标轨迹(目录名 + SESSION.md)
660
664
  * @param message 递送正文
661
665
  * @param sender 发起者轨迹标识(供目标与人类识别来源)
662
666
  */
663
667
  function buildSendText(target, message, sender) {
664
668
  return [
665
- `[即时消息] 来自 ${sender || "未知轨迹"}。`,
669
+ `[即时消息] 来自 ${sender || "未知轨迹"}。当前时间:${localHuman()}(当地时区)。`,
666
670
  "",
667
671
  `身份锚定:继续 ${target.dirName} 的 trajectory(SESSION.md: ${target.mdPath})。`,
668
672
  "",
669
673
  "消息:",
670
674
  message,
671
675
  "",
672
- "说明:这是**即时投递**(不是到点唤醒)——消息已进入你的队列;你此前若正在跑轮次,它在本轮结束后生效。处理完照常把进展写入 SESSION.md。"
676
+ "说明:这是**即时投递**(不是到点唤醒)——它**不排队**:目标正在跑轮次就**当场注入当前轮**,目标空闲则立即起一轮。处理完照常把进展写入 SESSION.md。"
673
677
  ].join("\n");
674
678
  }
675
679
  /**
676
680
  * 即时投递(`container_trajectory send-now`)——与 `send-later`(预约)**共用取用通路**(`acquireWakeAgent`),
677
681
  * 区别只在**时刻**(现在 vs 未来)与**回执**(有 vs 无):
678
682
  * · `send-later`(原 `wake-later`):未来时刻 + 一条 message → 落注册表 → 本文件的调度器到点投递(**fire-and-forget,无回执**)
679
- * · `send-now`(原 `send-message`):**此刻**把一条 message 递进目标队列 → **同步回执** → **不落注册表**
683
+ * · `send-now`(原 `send-message`):**此刻**把一条 message 递给目标 → **同步回执** → **不落注册表**
680
684
  *
681
685
  * **为什么冷路径落在这里(R↓,2026-09-16 所有者裁决)**:`addWake` 显式拒绝 `at ≤ now`
682
686
  * ("时刻必须在未来"——那是"**预约**"语义的地基,不许放宽)⇒ 即时投递**不能**靠一条
@@ -684,10 +688,20 @@ function buildSendText(target, message, sender) {
684
688
  * 必须直接走 {@link acquireWakeAgent}(live 优先 → `sessionController.resolveAgent` 冷载入)。
685
689
  * 这正对应所有者要的「**会话不活跃则等效于直接 wake**」:**同一条冷载入通路**,只是**不等 tick**。
686
690
  *
687
- * ⚠️ **承诺边界(不许含糊)**:回执只到「**已入队**」(`followup` 未抛错),
688
- * **到不了**"目标已执行/已答复" —— 与 `deliverWake` 同一层。目标若正在跑轮次,
689
- * 消息**排在轮次边界**(**不打断当前轮**)。判"目标真的动了"必须用**文件级判据**
690
- * (它自己的 SESSION.md),**不得凭回执结案**(§12.B 的 `delivered ≠ 跑了一轮`)。
691
+ * ⚠️ **承诺边界(不许含糊)**:回执只到「**已注入 / 已起轮**」(宿主调用未抛错),
692
+ * **到不了**"目标已执行/已答复" —— 与 `deliverWake` 同一层。
693
+ * "目标真的动了"必须用**文件级判据**(它自己的 SESSION.md),
694
+ * **不得凭回执结案**(§12.B 的 `delivered ≠ 跑了一轮`)。
695
+ *
696
+ * 🔴 **v1.39.2 语义变更(S142 §0r,所有者 2026-09-17 令:「希望 send message 时不排队,
697
+ * 可以直接作为提示词即时注入进去」)**:
698
+ * 旧实现一律 `followup`(宿主注释:*Queue an ordinary follow-up turn*)⇒ 目标**正在跑轮次**时
699
+ * 消息**排在轮次边界**(即旧文案的"不打断当前轮")。现按宿主 `agent.status` 分流:
700
+ * · `status === 'idle'` ⇒ `followup`(宿主**同步**把 status 置 running ⇒ **立即起轮**,本就不排队)
701
+ * · 否则(在跑) ⇒ **`steer`** —— **当场注入当前轮**,这才是"不排队"的关键路径
702
+ * ⚠️ **fail-safe**:`steer` 抛错 ⇒ **退回 `followup`**:**宁可排队,不可静默丢消息**。
703
+ * ⚠️ **只改 `send-now`**:`send-later`({@link deliverWake})**保持 `followup`** ——
704
+ * 预约的本职是"到点唤起",且在跑时排队不打断是它被实证验收过的行为,不顺手动它。
691
705
  *
692
706
  * **归属**:本函数放在调度器侧而非工具侧 —— "怎么把一句话送到一条轨迹"是**调度器的知识**,
693
707
  * 工具只该知道"有这个动作"。这样 `trajectory.ts` 完全不碰调度器内部(v1.35.0 硬约束①:
@@ -711,24 +725,48 @@ async function sendToTrajectory(ctx, root, target, message, sender) {
711
725
  ok: false,
712
726
  detail: acquired.error
713
727
  };
728
+ const agent = acquired.agent;
729
+ const make = () => createUserMessage({
730
+ content: [{
731
+ type: "text",
732
+ text: buildSendText(t, message, sender)
733
+ }],
734
+ source: PLUGIN_SOURCE
735
+ });
736
+ if (agent.status === "idle") {
737
+ try {
738
+ agent.followup(make());
739
+ } catch (err) {
740
+ return {
741
+ ok: false,
742
+ detail: `投递失败(${acquired.how}): ${String(err?.message ?? err)}`
743
+ };
744
+ }
745
+ return {
746
+ ok: true,
747
+ detail: `已即时投递 ${t.dirName}(${acquired.how};目标空闲 ⇒ followup 立即起轮)`
748
+ };
749
+ }
714
750
  try {
715
- acquired.agent.followup(createUserMessage({
716
- content: [{
717
- type: "text",
718
- text: buildSendText(t, message, sender)
719
- }],
720
- source: PLUGIN_SOURCE
721
- }));
751
+ agent.steer(make());
752
+ return {
753
+ ok: true,
754
+ detail: `已即时注入 ${t.dirName}(${acquired.how};目标在跑 steer 当场注入当前轮)`
755
+ };
722
756
  } catch (err) {
757
+ try {
758
+ agent.followup(make());
759
+ } catch (err2) {
760
+ return {
761
+ ok: false,
762
+ detail: `投递失败(${acquired.how}): steer 与 followup 均失败: ${String(err?.message ?? err)} / ${String(err2?.message ?? err2)}`
763
+ };
764
+ }
723
765
  return {
724
- ok: false,
725
- detail: `投递失败(${acquired.how}): ${String(err?.message ?? err)}`
766
+ ok: true,
767
+ detail: `已投递 ${t.dirName}(${acquired.how};steer 不可用 ⇒ 退回 followup): ${String(err?.message ?? err)}`
726
768
  };
727
769
  }
728
- return {
729
- ok: true,
730
- detail: `已即时投递 ${t.dirName}(${acquired.how})`
731
- };
732
770
  }
733
771
  /**
734
772
  * 本 tick 要扫的 CCC 根集合(**并集**)。
@@ -777,7 +815,7 @@ async function runWakeTick(ctx) {
777
815
  state: res.ok ? "delivered" : "pending",
778
816
  attempts: e.attempts + 1,
779
817
  lastResult: res.detail,
780
- deliveredAt: res.ok ? (/* @__PURE__ */ new Date()).toISOString() : null
818
+ deliveredAt: res.ok ? isoLocal() : null
781
819
  });
782
820
  log.push(`· ${root}: ${e.id} → ${res.ok ? "delivered" : `重试(${res.detail})`}`);
783
821
  }
@@ -72,8 +72,12 @@ export declare function buildWakeText(entry: WakeEntry, target: WakeTarget): str
72
72
  export declare function deliverWake(ctx: Context, root: string, entry: WakeEntry): Promise<WakeDeliveryResult>;
73
73
  /**
74
74
  * 即时消息正文(**刻意不复用** {@link buildWakeText}):唤醒文本以「**到点**(登记于…)」开头,
75
- * 对即时投递那是**错的事实**。本函数显式区分"这是即时消息、不是到点唤醒"
76
- * 并写明"消息已入队、若此前在跑轮次则本轮结束后生效"(防目标误判时序)。
75
+ * 对即时投递那是**错的事实**。本函数显式区分"这是即时消息、不是到点唤醒"
76
+ *
77
+ * ⚠️ **v1.39.2 起"已入队"这句话必须删掉**(S142 §0r,所有者令「不排队、直接即时注入」):
78
+ * 旧文案写"消息已进入你的队列;你此前若正在跑轮次,它在本轮结束后生效"——
79
+ * 那是**旧的 followup 语义**;现在在跑的目标走 `steer`(当场注入当前轮),
80
+ * 留着这句话就是**注入一条假陈述**(比没有更坏:目标会误判自己看到它的时机)。
77
81
  * @param target 目标轨迹(目录名 + SESSION.md)
78
82
  * @param message 递送正文
79
83
  * @param sender 发起者轨迹标识(供目标与人类识别来源)
@@ -83,7 +87,7 @@ export declare function buildSendText(target: WakeTarget, message: string, sende
83
87
  * 即时投递(`container_trajectory send-now`)——与 `send-later`(预约)**共用取用通路**(`acquireWakeAgent`),
84
88
  * 区别只在**时刻**(现在 vs 未来)与**回执**(有 vs 无):
85
89
  * · `send-later`(原 `wake-later`):未来时刻 + 一条 message → 落注册表 → 本文件的调度器到点投递(**fire-and-forget,无回执**)
86
- * · `send-now`(原 `send-message`):**此刻**把一条 message 递进目标队列 → **同步回执** → **不落注册表**
90
+ * · `send-now`(原 `send-message`):**此刻**把一条 message 递给目标 → **同步回执** → **不落注册表**
87
91
  *
88
92
  * **为什么冷路径落在这里(R↓,2026-09-16 所有者裁决)**:`addWake` 显式拒绝 `at ≤ now`
89
93
  * ("时刻必须在未来"——那是"**预约**"语义的地基,不许放宽)⇒ 即时投递**不能**靠一条
@@ -91,10 +95,20 @@ export declare function buildSendText(target: WakeTarget, message: string, sende
91
95
  * 必须直接走 {@link acquireWakeAgent}(live 优先 → `sessionController.resolveAgent` 冷载入)。
92
96
  * 这正对应所有者要的「**会话不活跃则等效于直接 wake**」:**同一条冷载入通路**,只是**不等 tick**。
93
97
  *
94
- * ⚠️ **承诺边界(不许含糊)**:回执只到「**已入队**」(`followup` 未抛错),
95
- * **到不了**"目标已执行/已答复" —— 与 `deliverWake` 同一层。目标若正在跑轮次,
96
- * 消息**排在轮次边界**(**不打断当前轮**)。判"目标真的动了"必须用**文件级判据**
97
- * (它自己的 SESSION.md),**不得凭回执结案**(§12.B 的 `delivered ≠ 跑了一轮`)。
98
+ * ⚠️ **承诺边界(不许含糊)**:回执只到「**已注入 / 已起轮**」(宿主调用未抛错),
99
+ * **到不了**"目标已执行/已答复" —— 与 `deliverWake` 同一层。
100
+ * "目标真的动了"必须用**文件级判据**(它自己的 SESSION.md),
101
+ * **不得凭回执结案**(§12.B 的 `delivered ≠ 跑了一轮`)。
102
+ *
103
+ * 🔴 **v1.39.2 语义变更(S142 §0r,所有者 2026-09-17 令:「希望 send message 时不排队,
104
+ * 可以直接作为提示词即时注入进去」)**:
105
+ * 旧实现一律 `followup`(宿主注释:*Queue an ordinary follow-up turn*)⇒ 目标**正在跑轮次**时
106
+ * 消息**排在轮次边界**(即旧文案的"不打断当前轮")。现按宿主 `agent.status` 分流:
107
+ * · `status === 'idle'` ⇒ `followup`(宿主**同步**把 status 置 running ⇒ **立即起轮**,本就不排队)
108
+ * · 否则(在跑) ⇒ **`steer`** —— **当场注入当前轮**,这才是"不排队"的关键路径
109
+ * ⚠️ **fail-safe**:`steer` 抛错 ⇒ **退回 `followup`**:**宁可排队,不可静默丢消息**。
110
+ * ⚠️ **只改 `send-now`**:`send-later`({@link deliverWake})**保持 `followup`** ——
111
+ * 预约的本职是"到点唤起",且在跑时排队不打断是它被实证验收过的行为,不顺手动它。
98
112
  *
99
113
  * **归属**:本函数放在调度器侧而非工具侧 —— "怎么把一句话送到一条轨迹"是**调度器的知识**,
100
114
  * 工具只该知道"有这个动作"。这样 `trajectory.ts` 完全不碰调度器内部(v1.35.0 硬约束①:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.39.1",
3
+ "version": "1.39.2",
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.5-rc.2。",
5
5
  "license": "MIT",
6
6
  "repository": {