@shgroup/dsh-serenity-hooks 1.93.0 → 1.95.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.93.0",
3
+ "version": "1.95.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -8,14 +8,34 @@
8
8
  * 调度器只掌握**会话 live 与否**,而 **`live ≠ 在跑`**——
9
9
  * 一条会话可以 live 而空闲(本会话此刻就是活的反例)。
10
10
  *
11
- * ## 怎么夹出来(用**既有的**两个契约事件,不需要新宿主能力)
11
+ * ## 怎么夹出来(用**既有的**契约事件,不需要新宿主能力)
12
12
  *
13
13
  * | 事件 | 动作 | 出处 |
14
14
  * |---|---|---|
15
- * | `agent/created` | 标记「在跑」 | `host/contract.ts`(dsp 已在 `seams/context.ts` 订阅)。⚠️ v0.1.7 前名为 `agent/session-start`;新事件 `@mode = serial` ⇒ 处理函数须快速返回 |
16
- * | `agent/turn-stopping` | 清除标记 | `host/contract.ts:277`(dsp 已在 `trajectory/rebuild.ts:442` 等 4 处订阅) |
17
- * | `agent/disposed` | **清该载体的项** | `host/contract.ts:280` |
18
- * | `session/disposed` | **清该载体的项** | `host/contract.ts:283` |
15
+ * | `agent/status` = `running` | 标记「在跑」;`idle` ⇒ 清除 | 宿主 `AgentLoop.setPhase`:`emit("agent/status", { status })`,**只在状态真的变化时发** |
16
+ * | `agent/turn-stopping` | 清除标记 | `host/contract.ts`(dsp 已在 `trajectory/rebuild.ts` 等 4 处订阅) |
17
+ * | `agent/disposed` | **清该载体的项** | `host/contract.ts` |
18
+ * | `session/disposed` | **清该载体的项** | `host/contract.ts` |
19
+ *
20
+ * ## 🔴🔴 D243(2026-10-24):**⛔ 不许用 `agent/created` 标记「在跑」**
21
+ *
22
+ * **修前**:`agent/created` 也 `markTurnRunning(...)`。**为什么这是错的** —— 该事件是
23
+ * `AgentRegistry.announce(agent, source)` 发的,而 `source` 明确包含 **`resume`**:
24
+ * 宿主**每次重启**都会把 persisted 会话重新载入并 announce ⇒ 每个被**载入**的会话都被打成"在跑",
25
+ * 而**它那一轮早随旧进程死掉了** ⇒ `turn-stopping` 永不到达 ⇒ 该假标记活满 `TURN_TTL_MS`(30 min)。
26
+ *
27
+ * **代价(两个面,都已实测)**:
28
+ * - **面一**:CRO 守卫拿它否决唤起 ⇒ **重启后聋 30 分钟**(guard 已在自己域绕开)。
29
+ * - **面二**:`cognitive_trajectory interrupted` 拿它当排除集 ⇒ **把最该被接走的会话藏起来**
30
+ * (实测:判据命中 5 条,生产入口报「检查 5 · 命中 0」)。
31
+ *
32
+ * **判据**:`agent/created` 证明的是「**载体被发布了**」,**不是**「有轮次在跑」。
33
+ * 「载入」与「在跑」互不蕴含(与 D225 同族:*字段名/字面形态不是语义*)。
34
+ * ⇒ **唯一权威 = `agent/status`**(宿主 `status` getter:`phase.kind === 'idle' | 'maintenance'`
35
+ * 才是 idle,其余皆 running),且它在**每次真实状态跃迁**时发 —— 正是我们要的粒度。
36
+ *
37
+ * **代价与兜底**:删掉 `agent/created` 后,本表在"宿主重启后、某会话真正开跑之前"为空
38
+ * ⇒ 落到文件尾那条**便宜**的误判方向(报"没跑"),不会被拿来藏会话。
19
39
  *
20
40
  * ## 🔴 清理设计(**这是本模块最容易被做坏的地方**)
21
41
  *
@@ -50,7 +70,7 @@
50
70
  */
51
71
  /** 一次「在跑」记录的 TTL(见文件头的方向性判据) */
52
72
  export declare const TURN_TTL_MS: number;
53
- /** 标记「在跑」(`agent/created` + `agent/status=running` 调用) */
73
+ /** 标记「在跑」(**唯一**生产调用点 = `agent/status=running`;另供测试与 CRO 自检置位) */
54
74
  export declare function markTurnRunning(sessionId: string, nowMs: number, turn?: number | null): void;
55
75
  /** 清除「在跑」(`turn-stopping` / disposed 调用) */
56
76
  export declare function clearTurnRunning(sessionId: string): void;
@@ -80,10 +100,13 @@ export declare function __resetCroTurnsForTest(): void;
80
100
  * 装配追踪(`index.ts` apply 调用):订阅四个既有事件 + 拆卸。
81
101
  *
82
102
  * **为什么订阅四个而不是两个**(R↓):
83
- * · 前两个(`agent/created` / `turn-stopping`)负责**语义**——"在不在跑";
103
+ * · 前两个(`agent/status` / `turn-stopping`)负责**语义**——"在不在跑";
84
104
  * · 后两个(`agent/disposed` / `session/disposed`)负责**生命周期**——
85
105
  * 载体没了必须清项,否则表按载体只增(本模块的头号风险)。
86
106
  *
107
+ * 🔴 **D243:`agent/created` 已于 2026-10-24 从订阅清单中移除**(原先共 5 个)。
108
+ * 理由与代价见**文件头 D243 段**;一句话:它是「载体被发布」的信,不是「轮次在跑」的信。
109
+ *
87
110
  * ⚠️ **失败不抛**:任一事件通道缺失 ⇒ 响亮日志 + 该订阅跳过(**apply 不可成为启动单点**,
88
111
  * 与 `registerDeepseekVisionPatch` 等既有装配同款纪律)。
89
112
  *
package/lib/index.js CHANGED
@@ -4,7 +4,7 @@ import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as
4
4
  import { A as namingTitleFor, B as summarize, C as createSession, E as getActiveSessionInfo, F as resolveSessionByTitle, G as readTrajectoryFile, H as useSession, I as sessionEvents, J as localHuman, K as isoLocal, L as sessionsRoot, M as parseTrajectoryCode, O as listActiveTrajectories, P as readTrajectoryUid, R as setActiveSessionInfo, S as clearActiveSessionInfo, T as findSession, U as TRAJECTORY_FILENAME, V as trajectoryDescOf, W as TRAJECTORY_LEGACY_FILENAMES, Y as localIdStamp, b as UID_FILENAME, c as replacesDefaultIdentity, i as listTrajectoryConstraints, j as parseSessionContextFromEvents, k as listSessions, l as trajectoryCodeForSession, n as findDirNameByCode, o as readTrajectoryConstraintAt, q as localFileStamp, r as isTrajectoryConstrained, s as readTrajectoryConstraintByCode, t as TRAJECTORY_CONSTRAINT_FILENAME, u as trajectoryConstraintForSession, v as COGNITIVE_TRAJECTORY_ACTIONS, w as extractSessionMdPathFromText, x as archiveSession, z as showSession } from "./profile-zxakCNXy.js";
5
5
  import { n as checkLocalstoreGitCompliance, o as readStore, s as runLocalStore, t as LOCALSTORE_SCOPES } from "./service-DDmHSglV.js";
6
6
  import { _ as resolveConstraintSystemPrompt, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as profileWriteWhitelist, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as ACC_VERSION, y as findPackageRoot } from "./message-source-BAMwXu6i.js";
7
- import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-D5cEPEUY.js";
7
+ import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as deliverToTrajectory, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as createTrajectoryAgent, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-DFNtReQY.js";
8
8
  import { i as sessionsRootDir, n as findSessionLog, r as hasSessionLogById } from "./cleanup-CPl1SSaK.js";
9
9
  import { i as runImBridge, n as isImWiredTrajectory, o as readSimpleSettings, r as registerImChannel, s as registerSettingsSection, t as imChannelIds } from "./channels-CuCsUfcY.js";
10
10
  import { a as readFeishuCredential, c as readWeixinSettings, d as weixinInboundDir, f as weixinSessionIdForTrajectory, i as matchWeixinRoute, n as extractWeixinText, r as hasVoiceItem, s as readWeixinCredential, t as extractWeixinMedia, u as sanitizeFileName$1 } from "./route-DxIKWeN9.js";
@@ -8438,14 +8438,34 @@ function registerDisposer(ctx, label, cleanup) {
8438
8438
  * 调度器只掌握**会话 live 与否**,而 **`live ≠ 在跑`**——
8439
8439
  * 一条会话可以 live 而空闲(本会话此刻就是活的反例)。
8440
8440
  *
8441
- * ## 怎么夹出来(用**既有的**两个契约事件,不需要新宿主能力)
8441
+ * ## 怎么夹出来(用**既有的**契约事件,不需要新宿主能力)
8442
8442
  *
8443
8443
  * | 事件 | 动作 | 出处 |
8444
8444
  * |---|---|---|
8445
- * | `agent/created` | 标记「在跑」 | `host/contract.ts`(dsp 已在 `seams/context.ts` 订阅)。⚠️ v0.1.7 前名为 `agent/session-start`;新事件 `@mode = serial` ⇒ 处理函数须快速返回 |
8446
- * | `agent/turn-stopping` | 清除标记 | `host/contract.ts:277`(dsp 已在 `trajectory/rebuild.ts:442` 等 4 处订阅) |
8447
- * | `agent/disposed` | **清该载体的项** | `host/contract.ts:280` |
8448
- * | `session/disposed` | **清该载体的项** | `host/contract.ts:283` |
8445
+ * | `agent/status` = `running` | 标记「在跑」;`idle` ⇒ 清除 | 宿主 `AgentLoop.setPhase`:`emit("agent/status", { status })`,**只在状态真的变化时发** |
8446
+ * | `agent/turn-stopping` | 清除标记 | `host/contract.ts`(dsp 已在 `trajectory/rebuild.ts` 等 4 处订阅) |
8447
+ * | `agent/disposed` | **清该载体的项** | `host/contract.ts` |
8448
+ * | `session/disposed` | **清该载体的项** | `host/contract.ts` |
8449
+ *
8450
+ * ## 🔴🔴 D243(2026-10-24):**⛔ 不许用 `agent/created` 标记「在跑」**
8451
+ *
8452
+ * **修前**:`agent/created` 也 `markTurnRunning(...)`。**为什么这是错的** —— 该事件是
8453
+ * `AgentRegistry.announce(agent, source)` 发的,而 `source` 明确包含 **`resume`**:
8454
+ * 宿主**每次重启**都会把 persisted 会话重新载入并 announce ⇒ 每个被**载入**的会话都被打成"在跑",
8455
+ * 而**它那一轮早随旧进程死掉了** ⇒ `turn-stopping` 永不到达 ⇒ 该假标记活满 `TURN_TTL_MS`(30 min)。
8456
+ *
8457
+ * **代价(两个面,都已实测)**:
8458
+ * - **面一**:CRO 守卫拿它否决唤起 ⇒ **重启后聋 30 分钟**(guard 已在自己域绕开)。
8459
+ * - **面二**:`cognitive_trajectory interrupted` 拿它当排除集 ⇒ **把最该被接走的会话藏起来**
8460
+ * (实测:判据命中 5 条,生产入口报「检查 5 · 命中 0」)。
8461
+ *
8462
+ * **判据**:`agent/created` 证明的是「**载体被发布了**」,**不是**「有轮次在跑」。
8463
+ * 「载入」与「在跑」互不蕴含(与 D225 同族:*字段名/字面形态不是语义*)。
8464
+ * ⇒ **唯一权威 = `agent/status`**(宿主 `status` getter:`phase.kind === 'idle' | 'maintenance'`
8465
+ * 才是 idle,其余皆 running),且它在**每次真实状态跃迁**时发 —— 正是我们要的粒度。
8466
+ *
8467
+ * **代价与兜底**:删掉 `agent/created` 后,本表在"宿主重启后、某会话真正开跑之前"为空
8468
+ * ⇒ 落到文件尾那条**便宜**的误判方向(报"没跑"),不会被拿来藏会话。
8449
8469
  *
8450
8470
  * ## 🔴 清理设计(**这是本模块最容易被做坏的地方**)
8451
8471
  *
@@ -8483,9 +8503,9 @@ const TURN_TTL_MS = 18e5;
8483
8503
  /**
8484
8504
  * 进程内的「正在跑」表(键 = 载体 dsh 会话 id)。
8485
8505
  *
8486
- * ⚠️ **仅内存**:进程重启即空 —— 这是**可接受的**,因为重启后宿主会重发
8487
- * `agent/created`(或至少下一次 `turn-stopping` 会清掉残留),且空表 = "没有在跑",
8488
- * 落到上面那条**便宜**的误判方向。
8506
+ * ⚠️ **仅内存**:进程重启即空 —— 这是**可接受的,且是刻意选的**:
8507
+ * 空表 = "没有在跑" ⇒ 落到上面那条**便宜**的误判方向(漏排是硬故障、多排是常态成本)。
8508
+ * 🔴 **D243**:刻意**不**在重启后用 `agent/created` 去"补"这张表 —— 那会把「载入」当「在跑」。
8489
8509
  */
8490
8510
  const runningBySession = /* @__PURE__ */ new Map();
8491
8511
  /** 取载体 id(事件 payload 的 `agent` 面;取不到 ⇒ null) */
@@ -8493,7 +8513,7 @@ function sessionIdOf$2(agent) {
8493
8513
  const id = agent?.session?.id;
8494
8514
  return typeof id === "string" && id !== "" ? id : null;
8495
8515
  }
8496
- /** 标记「在跑」(`agent/created` + `agent/status=running` 调用) */
8516
+ /** 标记「在跑」(**唯一**生产调用点 = `agent/status=running`;另供测试与 CRO 自检置位) */
8497
8517
  function markTurnRunning(sessionId, nowMs, turn = null) {
8498
8518
  if (!sessionId) return;
8499
8519
  runningBySession.set(sessionId, {
@@ -8541,10 +8561,13 @@ function runningCarriersOf(boundSessionIds, nowMs, ttlMs = TURN_TTL_MS) {
8541
8561
  * 装配追踪(`index.ts` apply 调用):订阅四个既有事件 + 拆卸。
8542
8562
  *
8543
8563
  * **为什么订阅四个而不是两个**(R↓):
8544
- * · 前两个(`agent/created` / `turn-stopping`)负责**语义**——"在不在跑";
8564
+ * · 前两个(`agent/status` / `turn-stopping`)负责**语义**——"在不在跑";
8545
8565
  * · 后两个(`agent/disposed` / `session/disposed`)负责**生命周期**——
8546
8566
  * 载体没了必须清项,否则表按载体只增(本模块的头号风险)。
8547
8567
  *
8568
+ * 🔴 **D243:`agent/created` 已于 2026-10-24 从订阅清单中移除**(原先共 5 个)。
8569
+ * 理由与代价见**文件头 D243 段**;一句话:它是「载体被发布」的信,不是「轮次在跑」的信。
8570
+ *
8548
8571
  * ⚠️ **失败不抛**:任一事件通道缺失 ⇒ 响亮日志 + 该订阅跳过(**apply 不可成为启动单点**,
8549
8572
  * 与 `registerDeepseekVisionPatch` 等既有装配同款纪律)。
8550
8573
  *
@@ -8574,16 +8597,11 @@ function registerCroTurnTracking(ctx) {
8574
8597
  console.log(`[serenity-hooks] ✗ CRO turn 追踪未装配(${event} 不可用): ${String(err?.message ?? err)}`);
8575
8598
  }
8576
8599
  };
8577
- on("agent/created", (payload) => {
8578
- const id = sessionIdOf$2(payload?.agent);
8579
- if (id) markTurnRunning(id, Date.now());
8580
- });
8581
8600
  on("agent/status", (payload) => {
8582
8601
  const p = payload;
8583
8602
  const id = sessionIdOf$2(p?.agent);
8584
8603
  if (!id) return;
8585
- const turn = typeof p.turn === "number" ? p.turn : null;
8586
- if (p.status === "running") markTurnRunning(id, Date.now(), turn);
8604
+ if (p.status === "running") markTurnRunning(id, Date.now());
8587
8605
  else if (p.status === "idle") clearTurnRunning(id);
8588
8606
  });
8589
8607
  on("agent/turn-stopping", (payload) => {
@@ -20231,15 +20249,22 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
20231
20249
  try {
20232
20250
  const sessionId = weixinSessionIdForTrajectory(trajectory);
20233
20251
  /**
20234
- * 🔴 **同会话串行化**(2026-10-21,修"同一条消息被处理两轮"的第二个缺陷)。
20252
+ * 🔴 **同会话串行化**(2026-10-21 引入;**2026-10-24 理由已变,见下**)。
20253
+ *
20254
+ * ## 原理由(已失效,留档 R↓)
20255
+ * `askTrajectory` 的成功判据是"`assistant/message` **条数增长**",两条入站并发时
20256
+ * 后一条的快照包含前一条尚未落盘的产出 ⇒ 前一轮的产出满足后一条的判据 ⇒
20257
+ * 兜底可能拿到**错轮**的答案(真机实测:把内部过程话发给了家人)。
20235
20258
  *
20236
- * `askTrajectory` 的成功判据是"`assistant/message` **条数增长**"。两条入站并发时,
20237
- * 后一条的快照包含前一条尚未落盘的产出 ⇒ **前一轮的产出会满足后一条的成功判据**
20238
- * ⇒ 兜底/`reply-fallback` 可能拿到**错轮**的答案(真机实测:把内部过程话发给了家人)。
20259
+ * ## 🔴 现在的理由(owner 2026-10-24 令「正在跑就直接进、不排队」之后)
20260
+ * 入站改走 `deliverToTrajectory`(**不等回答**)⇒ 上面那条"错轮答案"的路径**已不存在**。
20261
+ * 锁**仍然需要**,但它现在防的是另一件事:
20262
+ * **`createTrajectoryAgent` 的"查-没有-再建"** —— 两条入站同时到达且都发现"没有载体"时,
20263
+ * 会各自建一个 ⇒ **同一轨迹两个载体**(撞 D-族「一条轨迹至多一个载体」的底线)。
20264
+ * ⇒ 锁的**覆盖范围应当只包住"投递"这一步**,⛔ 不该再包住整轮等待
20265
+ * (那正是 owner 说的"排队")。投递本身是同步的几十微秒,串行化代价可忽略。
20239
20266
  *
20240
- * 锁的粒度 = **一个会话同一时刻只跑一条入站**(不同轨迹各跑各的,互不拖慢)。
20241
- * ⚠️ 与投递侧"不排队"不是一回事:`send-now` 的"当场注入"是**投递**语义,
20242
- * 这里排的是**处理**语义 —— 两者不冲突。
20267
+ * ⚠️ 粒度 = **一个会话同一时刻只处理一条入站的投递**(不同轨迹各跑各的)。
20243
20268
  */
20244
20269
  releaseLock = await acquireSessionLock(sessionId);
20245
20270
  const existing = getTrajectoryAgent(sessionId);
@@ -20355,7 +20380,53 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
20355
20380
  })).catch((err) => {
20356
20381
  console.log(`[serenity-hooks] im hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
20357
20382
  });
20358
- await askTrajectory(ctx, ref.agent, question);
20383
+ /**
20384
+ * 🔴🔴 **2026-10-24(owner 令):正在跑就**直接进**,不排队。**
20385
+ *
20386
+ * owner 原话:「当用户消息进来,且绑定轨迹正在活动时,消息是排队而不是直接进入,
20387
+ * 应当改为直接进入,这样确保来回通信的流畅性」。
20388
+ *
20389
+ * **修前**:`await askTrajectory(...)` = `followup()`(排队语义)+ **等整轮跑完**。
20390
+ * ⇒ 用户连说两句,第二句要等第一句那一整轮结束才被看见 ⇒ **来回通信卡顿**。
20391
+ *
20392
+ * **为什么现在可以不等**:D238(同日,出站只剩 `im-bridge`)之后,
20393
+ * 那个 `answer` **已经没有消费方**(上一版这里就是 `void result`)——
20394
+ * 整轮等待**只剩"把后一条堵在前面"这一个副作用**。
20395
+ *
20396
+ * **投递形态**(`deliverToTrajectory`,与 `interrupted.ts` 的 `createResumeOne` 同判据):
20397
+ * 在跑 ⇒ `steer`(当场进当前轮,模型同一次生成里就看到);
20398
+ * 空闲 ⇒ `followup`(立刻起一轮);`steer` 缺失 ⇒ 排队也比丢掉好。
20399
+ */
20400
+ const delivered = deliverToTrajectory(ref.agent, question);
20401
+ /**
20402
+ * 🔴🔴 **2026-10-24(owner 令,D238):这里不再有任何"桥转发最终文本"的分支。**
20403
+ *
20404
+ * 删掉的两段(历史留档,R↓):
20405
+ * ① `manualOutput` 的兜底转发(原 `fallbackOnNoSend` + `source:'reply-fallback'`);
20406
+ * ② 自动模式的直接转发(原 `io.sendText` + `source:'reply'` 缺省)。
20407
+ *
20408
+ * **留下的唯一事实**:投递之后**什么都不发** —— 出站只可能由 agent
20409
+ * 自己调 `im-bridge` 产生(`source:'proactive'`)。
20410
+ *
20411
+ * ⚠️ `delivered.how` 要记:它回答"这条消息是怎么进去的"
20412
+ * (`steer` = 当场进当前轮 / `followup` = 排进下一轮 / `none` = 无通道)。
20413
+ * **不记它就无法从日志上分清"不排队"到底生效了没有**(D216 同族)。
20414
+ */
20415
+ if (delivered.how === "none") console.log(`[serenity-hooks] im 投递失败:会话 ${sessionId} 既无 steer 也无 followup(宿主契约漂移)`);
20416
+ else console.log(`[serenity-hooks] im 投递:会话 ${sessionId} → ${delivered.how}(不排队;出站走 im-bridge)`);
20417
+ /**
20418
+ * 🔴🔴 **2026-10-24(owner 令「不排队」):锁在这里就放,⛔ 不再等整轮。**
20419
+ *
20420
+ * 投递已在上一步**同步**完成(`steer` / `followup` 都是同步调用)⇒
20421
+ * 锁要保护的"查载体-没有-再建"那一段**已经结束**。
20422
+ * ⇒ 从这里往后(typingStop、日志、hook 回调)没有任何"必须串行"的事,
20423
+ * 让锁继续持有 = **把下一条消息堵在这里** = owner 说的"排队"。
20424
+ *
20425
+ * ⚠️ 早期释放是**安全**的:下面 `finally` 里的 `releaseLock?.()` 有 `released` 幂等判据
20426
+ * (见 `acquireSessionLock` 的返回),重复调用是空操作 ⇒ 不会二次释放。
20427
+ */
20428
+ releaseLock?.();
20429
+ releaseLock = null;
20359
20430
  } finally {
20360
20431
  await io.typingStop?.({ toUserId: fromUserId });
20361
20432
  }
@@ -142,47 +142,6 @@ function trajectoryPromptBlock(text) {
142
142
  ].join("\n");
143
143
  }
144
144
  //#endregion
145
- //#region src/trajectory/carrier.ts
146
- /**
147
- * 等待 agent 空闲或销毁。
148
- * @param ctx 宿主插件上下文(事件通道来源;**结构化访问**,故类型为 `unknown`,见文件头 I2 段)
149
- * @param agent 目标 agent
150
- * @returns 空闲、agent 销毁、或会话销毁时结算(永不 reject)
151
- */
152
- function waitAgentIdle(ctx, agent) {
153
- return new Promise((resolve) => {
154
- let settled = false;
155
- const disposers = [];
156
- const finish = () => {
157
- if (settled) return;
158
- settled = true;
159
- for (const dispose of disposers) try {
160
- dispose();
161
- } catch {}
162
- resolve();
163
- };
164
- const subscribe = (name, cb) => {
165
- try {
166
- const off = ctx.on?.(name, cb);
167
- if (typeof off === "function") disposers.push(off);
168
- } catch {}
169
- };
170
- subscribe("agent/status", (payload) => {
171
- const p = payload;
172
- if (p?.agent === agent && p?.status === "idle") finish();
173
- });
174
- subscribe("agent/disposed", (payload) => {
175
- if (payload?.agent === agent) finish();
176
- });
177
- const sessionId = agent.session?.id;
178
- subscribe("session/disposed", (session) => {
179
- const id = session?.id;
180
- if (typeof id === "string" && id === sessionId) finish();
181
- });
182
- if (agent.status === "idle") finish();
183
- });
184
- }
185
- //#endregion
186
145
  //#region src/skills/discovery.ts
187
146
  /**
188
147
  * skills/discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
@@ -2528,136 +2487,52 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
2528
2487
  };
2529
2488
  }
2530
2489
  }
2531
- /** 会话事件读取(兼容 `snapshotEvents()` 与测试替身的 `.events`) */
2532
- function sessionEvents(session) {
2533
- const s = session;
2534
- if (typeof s.snapshotEvents === "function") return s.snapshotEvents();
2535
- return s.events ?? [];
2536
- }
2537
- /** 读会话最后一个 assistant/message 文本 */
2538
- function lastAssistantText(agent) {
2539
- const events = sessionEvents(agent.session);
2540
- for (let i = events.length - 1; i >= 0; i--) {
2541
- const e = events[i];
2542
- if (e && e.type === "assistant/message") {
2543
- const text = (e.data?.message?.content ?? e.data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
2544
- if (text) return text;
2545
- }
2546
- }
2547
- return "";
2548
- }
2549
- /** 本轮等待上限(毫秒):超时即判「本轮无产出」——**绝不**把上一轮的旧答案当本轮答案 */
2550
- const ASK_TURN_TIMEOUT_MS = 18e4;
2551
- /** 会话里 `assistant/message` 的条数(判「本轮是否真的产出了回答」) */
2552
- function countAssistantMessages(session) {
2553
- let n = 0;
2554
- for (const e of sessionEvents(session)) if (e?.type === "assistant/message") n += 1;
2555
- return n;
2556
- }
2557
- /**
2558
- * 会话当前轮号(最后一个 `turn/start` 的 `turn`;取不到 ⇒ `null`)。
2559
- *
2560
- * 🔴 为什么需要它(2026-10-21,"同一条消息被处理两轮"的第二处缺陷):
2561
- * 判"本轮是否产出"用的是 **`assistant/message` 条数增长**。而在**并发入站**下,
2562
- * 后一条的快照里,**前一条那一轮**还在继续产出 assistant 消息 ⇒ 计数照样增长
2563
- * ⇒ 后一条会把**别人那一轮的文本**当成自己的答案(真机实测:兜底把内部过程话发给了家人)。
2564
- * ⇒ 增长必须限定在「**这一轮开始之后**新出现的事件」里。
2565
- */
2566
- function currentTurnOfSession(session) {
2567
- const events = sessionEvents(session);
2568
- for (let i = events.length - 1; i >= 0; i--) {
2569
- const e = events[i];
2570
- if (e?.type === "turn/start") {
2571
- const t = e.data?.turn;
2572
- return typeof t === "number" && Number.isFinite(t) ? t : null;
2573
- }
2574
- }
2575
- return null;
2576
- }
2577
- function snapshotSession(session) {
2578
- return {
2579
- turn: currentTurnOfSession(session),
2580
- assistantCount: countAssistantMessages(session),
2581
- eventCount: sessionEvents(session).length
2582
- };
2583
- }
2584
2490
  /**
2585
- * **本轮**(快照之后)是否产出了 assistant 消息。
2491
+ * 把一条用户消息**投进**载体会话 —— 🔴 **2026-10-24 owner 令:正在跑就直接进,不排队**。
2586
2492
  *
2587
- * 判据顺序(每一条都是被真账打出来的):
2588
- * ① 只数**快照之后**新增的事件 ⇒ 前一轮续跑的产出**不进账**(2026-10-21);
2589
- * ② 若快照时的轮号已知,且现在的轮号变了 ⇒ 那是**别的轮**在产出 ⇒ 不算我的;
2590
- * ③ 条数确实增长 ⇒ 成功。
2591
- */
2592
- function grewWithFreshAssistant(session, snap) {
2593
- const events = sessionEvents(session);
2594
- if (events.length <= snap.eventCount) return false;
2595
- const now = currentTurnOfSession(session);
2596
- if (snap.turn !== null && now !== null && now !== snap.turn) return false;
2597
- let fresh = 0;
2598
- for (let i = snap.eventCount; i < events.length; i++) if (events[i]?.type === "assistant/message") fresh += 1;
2599
- if (fresh > 0) return true;
2600
- return countAssistantMessages(session) > snap.assistantCount;
2601
- }
2602
- /**
2603
- * 等**本轮**产出一条 assistant 消息。
2493
+ * ## owner 原话
2494
+ * 「**当用户消息进来,且绑定轨迹正在活动时,消息是排队而不是直接进入,应当改为直接进入,
2495
+ * 这样确保来回通信的流畅性**」。
2604
2496
  *
2605
- * ## 为什么不能用 `waitAgentIdle` 单独当判据(2026-10-19 复读缺陷)
2497
+ * ## 修前为什么是"排队"
2498
+ * 入站走 {@link askTrajectory}:它 `followup()`(**排队语义**,等本轮结束才开新一轮)
2499
+ * 然后**等整轮跑完**读答案。而 D238(2026-10-24,出站只剩 `im-bridge`)之后
2500
+ * 那个答案**已经没有消费方**(调用点是 `void result`)⇒
2501
+ * **整轮等待只剩副作用:把后一条消息堵在前一条那一轮后面**。
2606
2502
  *
2607
- * `waitAgentIdle` 的首行是 `if (agent.status === 'idle') finish()` —— 而 `followup()`
2608
- * 是**异步投递**:刚投出去的这一刻 agent 状态**仍是 idle** ⇒ 等待**立刻结算** ⇒
2609
- * `lastAssistantText()` 读到的是**上一轮的** `assistant/message` ⇒ 桥把它当"本轮答案"转发。
2610
- * 而当时若 CCC 又开了 `fallbackOnNoSend`(该开关已于 D238 退役),同一条旧答案会被反复重发(实测三次间隔 300~400ms、逐字相同)。
2503
+ * ## 判据(与 `trajectory/interrupted.ts` 的 `createResumeOne` 同源)
2504
+ * - **`agent.status === 'running'` 且 `steer` 可用** ⇒ `steer`:**当场注入当前轮**
2505
+ * (模型在同一次生成里就看到这句话 ⇒ 来回通信连续)。
2506
+ * - 否则 `followup`:空闲 ⇒ 立刻起一轮;`steer` 缺失 ⇒ 排队也比丢掉好。
2611
2507
  *
2612
- * ## 判据(`idle` 只是"可以开始看"的信号,不是成功信号)
2508
+ * ## 为什么本函数**不等**回答
2509
+ * ① 答案没人读(D238 之后出站只能由 agent 自己调 `im-bridge`);
2510
+ * ② **等待正是"排队"的机制本体** —— 不等,锁就只覆盖投递那一下,
2511
+ * 下一条消息才能也当场进去。
2512
+ * ⇒ 语义从"**问一句、等回答**"变成"**把话递进去**"。这也更贴近 IM 的真实形态:
2513
+ * 用户是连续说话的,不是一问一答的请求-响应。
2613
2514
  *
2614
- * ① 期望值 = 调用前 `assistant/message` 的**条数**;
2615
- * ② 成功 ⟺ 该计数**增长**;
2616
- * ③ 超时或有界轮询耗尽 ⇒ `false`(调用方据此给空答案,兜底不触发)。
2515
+ * ⚠️ **与 {@link askTrajectory} 的分工**:后者保留"问 + 等本轮答案"的完整语义
2516
+ * (仍有消费方/测试在用);本函数是**入站投递**专用。
2617
2517
  */
2618
- async function waitForTurnAssistant(ctx, agent, before, opts = {}) {
2619
- const timeoutMs = opts.timeoutMs ?? ASK_TURN_TIMEOUT_MS;
2620
- /**
2621
- * 快照兼容两种入参:数字(旧调用方的"条数")与 `SessionSnapshot`(新,带轮号与事件位)。
2622
- * 🔴 数字形态**仍然可用**,因为既有测试与调用点按数字传,而它们的语义("条数增长")
2623
- * 在本轮修法之后依然成立 —— 只是**并发**时不够。给 `SessionSnapshot` 才拿到全部判别力。
2624
- */
2625
- const snap = typeof before === "number" ? {
2626
- turn: null,
2627
- assistantCount: before,
2628
- eventCount: 0
2629
- } : before;
2630
- const grew = () => typeof before === "number" ? countAssistantMessages(agent.session) > before : grewWithFreshAssistant(agent.session, snap);
2631
- if (await Promise.race([waitAgentIdle(ctx, agent).then(() => true), new Promise((resolve) => {
2632
- setTimeout(() => resolve(false), timeoutMs);
2633
- })])) return grew();
2634
- return grew();
2635
- }
2636
- /**
2637
- * 提问一轮:followup → 等**本轮**回答 → 读答案。
2638
- *
2639
- * ## 🔴 2026-10-21:快照从"条数"升级为"(轮号, 条数, 事件位)"
2640
- *
2641
- * 并发入站时,后一条的快照里**前一条那一轮还在产出** ⇒ 旧判据(条数增长)会被满足
2642
- * ⇒ 后一条把**别人那一轮的文本**当自己的答案。新快照让"增长"只在**本轮的窗口内**才成立。
2643
- * 配套:调用侧(`im/inbound-event.ts`)给**同一会话**加了串行锁 —— 两层都要,缺一不可:
2644
- * 锁防"同时开两轮",快照防"锁被绕过时(如别的路径直接调本函数)把错轮的文本当答案"。
2645
- *
2646
- * @param options.timeoutMs 本轮等待上限(测试注入;生产用缺省)
2647
- */
2648
- async function askTrajectory(ctx, agent, question, options) {
2649
- const beforeSnapshot = snapshotSession(agent.session);
2650
- agent.followup(createUserMessage({
2518
+ function deliverToTrajectory(agent, text) {
2519
+ const a = agent;
2520
+ const msg = createUserMessage({
2651
2521
  content: [{
2652
2522
  type: "text",
2653
- text: question
2523
+ text
2654
2524
  }],
2655
2525
  source: PLUGIN_SOURCE
2656
- }));
2657
- return {
2658
- answer: await waitForTurnAssistant(ctx, agent, beforeSnapshot, { timeoutMs: options?.timeoutMs }) ? lastAssistantText(agent) : "",
2659
- sessionId: String(agent.session.id ?? "")
2660
- };
2526
+ });
2527
+ if (a.status === "running" && typeof a.steer === "function") {
2528
+ a.steer(msg);
2529
+ return { how: "steer" };
2530
+ }
2531
+ if (typeof a.followup === "function") {
2532
+ a.followup(msg);
2533
+ return { how: "followup" };
2534
+ }
2535
+ return { how: "none" };
2661
2536
  }
2662
2537
  /**
2663
2538
  * 轨迹约束会话的轨迹纪律参与判定:**不是 轨迹约束恒 true**(正常参与);
@@ -2692,4 +2567,4 @@ function trajectoryConstraintMsmGate(root, sessionId, action, name) {
2692
2567
  return {};
2693
2568
  }
2694
2569
  //#endregion
2695
- export { buildResourceIndex as A, verifyPassword as C, recordToolUsage as D, verifyTotpCode as E, hasTrajectoryPrompt as F, isSafeSkillName as M, truncateContent as N, REFERENCE_DIR as O, TRAJECTORY_PROMPT_FILENAME as P, toWire as S, MECHANISM_PORTS as T, registerEntrySkillSectionGlobal as _, trajectoryConstraintFor as a, projectKnownWorkspaces as b, trajectoryWorkspaceMdPath as c, CHEAP_WORKER_GUIDE as d, providerAvailabilityError as f, registerEntrySkillSection as g, registerCognitiveTrajectorySkillSection as h, getTrajectoryAgent as i, buildResourceNameSuffix as j, SKILL_ONTOLOGY_FILE as k, unregisterTrajectorySession as l, splitModel as m, createTrajectoryAgent as n, trajectoryConstraintMsmGate as o, requireWhitelistedModel as p, ensureWorkspacePromptSection as r, trajectoryConstraintParticipates as s, askTrajectory as t, workspaceCognitiveTrajectoryLine as u, applyWirePatch as v, FACE_PORTS as w, readAdvancedSettings as x, migrateLegacyLocalstore as y };
2570
+ export { buildResourceIndex as A, verifyPassword as C, recordToolUsage as D, verifyTotpCode as E, hasTrajectoryPrompt as F, isSafeSkillName as M, truncateContent as N, REFERENCE_DIR as O, TRAJECTORY_PROMPT_FILENAME as P, toWire as S, MECHANISM_PORTS as T, registerEntrySkillSectionGlobal as _, trajectoryConstraintFor as a, projectKnownWorkspaces as b, trajectoryWorkspaceMdPath as c, CHEAP_WORKER_GUIDE as d, providerAvailabilityError as f, registerEntrySkillSection as g, registerCognitiveTrajectorySkillSection as h, getTrajectoryAgent as i, buildResourceNameSuffix as j, SKILL_ONTOLOGY_FILE as k, unregisterTrajectorySession as l, splitModel as m, deliverToTrajectory as n, trajectoryConstraintMsmGate as o, requireWhitelistedModel as p, ensureWorkspacePromptSection as r, trajectoryConstraintParticipates as s, createTrajectoryAgent as t, workspaceCognitiveTrajectoryLine as u, applyWirePatch as v, FACE_PORTS as w, readAdvancedSettings as x, migrateLegacyLocalstore as y };
@@ -137,9 +137,45 @@ export declare function waitForTurnAssistant(ctx: unknown, agent: Agent, before:
137
137
  timeoutMs?: number;
138
138
  pollMs?: number;
139
139
  }): Promise<boolean>;
140
+ /**
141
+ * 把一条用户消息**投进**载体会话 —— 🔴 **2026-10-24 owner 令:正在跑就直接进,不排队**。
142
+ *
143
+ * ## owner 原话
144
+ * 「**当用户消息进来,且绑定轨迹正在活动时,消息是排队而不是直接进入,应当改为直接进入,
145
+ * 这样确保来回通信的流畅性**」。
146
+ *
147
+ * ## 修前为什么是"排队"
148
+ * 入站走 {@link askTrajectory}:它 `followup()`(**排队语义**,等本轮结束才开新一轮)
149
+ * 然后**等整轮跑完**读答案。而 D238(2026-10-24,出站只剩 `im-bridge`)之后
150
+ * 那个答案**已经没有消费方**(调用点是 `void result`)⇒
151
+ * **整轮等待只剩副作用:把后一条消息堵在前一条那一轮后面**。
152
+ *
153
+ * ## 判据(与 `trajectory/interrupted.ts` 的 `createResumeOne` 同源)
154
+ * - **`agent.status === 'running'` 且 `steer` 可用** ⇒ `steer`:**当场注入当前轮**
155
+ * (模型在同一次生成里就看到这句话 ⇒ 来回通信连续)。
156
+ * - 否则 `followup`:空闲 ⇒ 立刻起一轮;`steer` 缺失 ⇒ 排队也比丢掉好。
157
+ *
158
+ * ## 为什么本函数**不等**回答
159
+ * ① 答案没人读(D238 之后出站只能由 agent 自己调 `im-bridge`);
160
+ * ② **等待正是"排队"的机制本体** —— 不等,锁就只覆盖投递那一下,
161
+ * 下一条消息才能也当场进去。
162
+ * ⇒ 语义从"**问一句、等回答**"变成"**把话递进去**"。这也更贴近 IM 的真实形态:
163
+ * 用户是连续说话的,不是一问一答的请求-响应。
164
+ *
165
+ * ⚠️ **与 {@link askTrajectory} 的分工**:后者保留"问 + 等本轮答案"的完整语义
166
+ * (仍有消费方/测试在用);本函数是**入站投递**专用。
167
+ */
168
+ export declare function deliverToTrajectory(agent: Agent, text: string): {
169
+ how: string;
170
+ };
140
171
  /**
141
172
  * 提问一轮:followup → 等**本轮**回答 → 读答案。
142
173
  *
174
+ * ⚠️ **2026-10-24:本函数已不再是 IM 入站的投递路径**(改用 {@link deliverToTrajectory},
175
+ * 因为 owner 要求"正在跑就直接进、不排队",且 D238 之后答案已无消费方)。
176
+ * 保留它是因为"问一句并等这一轮的答案"这个语义本身仍然有效(测试与将来的同步调用方)。
177
+ * ⛔ **不要把它改回入站路径** —— 那会把排队一起带回来。
178
+ *
143
179
  * ## 🔴 2026-10-21:快照从"条数"升级为"(轮号, 条数, 事件位)"
144
180
  *
145
181
  * 并发入站时,后一条的快照里**前一条那一轮还在产出** ⇒ 旧判据(条数增长)会被满足
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.93.0",
3
+ "version": "1.95.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {