@clawos-dev/clawd 0.2.385 → 0.2.386

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/dist/cli.cjs CHANGED
@@ -6613,17 +6613,19 @@ var init_shift_internal = __esm({
6613
6613
  });
6614
6614
 
6615
6615
  // ../protocol/src/channel.ts
6616
- var ChannelSchema, nonBlank, ChannelCreateArgsSchema, ChannelUpdateArgsSchema, ChannelDeleteArgsSchema, ChannelListResultSchema, ChannelResultSchema, ChannelDeleteResultSchema;
6616
+ var MASTER_PERSONA_ID, ChannelSchema, nonBlank, ChannelCreateArgsSchema, ChannelUpdateArgsSchema, ChannelDeleteArgsSchema, ChannelListResultSchema, ChannelResultSchema, ChannelDeleteResultSchema;
6617
6617
  var init_channel = __esm({
6618
6618
  "../protocol/src/channel.ts"() {
6619
6619
  "use strict";
6620
6620
  init_zod();
6621
+ MASTER_PERSONA_ID = "persona-master";
6621
6622
  ChannelSchema = external_exports.object({
6622
6623
  id: external_exports.string().min(1),
6623
6624
  name: external_exports.string().min(1),
6624
6625
  /** 该频道里可被派活的 persona */
6625
6626
  memberPersonaIds: external_exports.array(external_exports.string().min(1)),
6626
- /** 人类成员(contact deviceId);M2 只存不用,P1 异步询问才消费 */
6627
+ /** 人类成员(contact deviceId)。跨设备派活的成员边界认这张名单(personaDispatch 校验),
6628
+ * P1 人类异步询问也用它 */
6627
6629
  memberContactDeviceIds: external_exports.array(external_exports.string().min(1)),
6628
6630
  createdAt: external_exports.string().min(1),
6629
6631
  updatedAt: external_exports.string().min(1)
@@ -6672,7 +6674,18 @@ var init_topic = __esm({
6672
6674
  });
6673
6675
  TopicLineMetaSchema = external_exports.object({
6674
6676
  rejectCount: external_exports.number().int().min(0),
6675
- accepted: external_exports.boolean().default(false)
6677
+ accepted: external_exports.boolean().default(false),
6678
+ /**
6679
+ * Master 在 watcher 判 fail 的情况下仍判合格的次数。
6680
+ *
6681
+ * 为什么要单独数:watcher 的 fail 若能被一句「这是可观测性缺口」无声绕过,核验就形同
6682
+ * 虚设——实测一个话题三条线的 watcher 全报 fail、无一打回,Master 自己下场补证后判了合格。
6683
+ * 覆盖本身不禁止(watcher 确实会误判),但必须留痕、必须计数、反复覆盖必须惊动老板。
6684
+ *
6685
+ * optional 而非 default(0):store 不按 schema parse(见 topic-store),存量 lineMeta
6686
+ * 里本来就没有这个字段,读侧一律 `?? 0`。
6687
+ */
6688
+ overrideCount: external_exports.number().int().min(0).optional()
6676
6689
  });
6677
6690
  TopicSchema = external_exports.object({
6678
6691
  id: external_exports.string().min(1),
@@ -6742,7 +6755,17 @@ var init_topic = __esm({
6742
6755
  /** 凭什么判合格(逐条标准对应到什么证据)——与打回同样必填 */
6743
6756
  reason: nonBlank2,
6744
6757
  /** watcher 认定的产出物,一并进影响面 */
6745
- artifacts: external_exports.array(nonBlank2).optional()
6758
+ artifacts: external_exports.array(nonBlank2).optional(),
6759
+ /**
6760
+ * 这条线的 watcher 核验结论。**必填**:不填等于允许「压根没跑 watcher 就发合格证」,
6761
+ * 而合格与打回之所以要对称落账,就是为了让老板事后分得出「判过且过了」和「没judge过」。
6762
+ */
6763
+ watcherVerdict: external_exports.enum(["pass", "fail"]),
6764
+ /** watcher 判 fail 却仍要判合格时,凭什么推翻它(fail 时必填) */
6765
+ overrideReason: nonBlank2.optional()
6766
+ }).refine((v2) => v2.watcherVerdict !== "fail" || Boolean(v2.overrideReason), {
6767
+ message: "watcherVerdict=fail \u65F6\u5FC5\u987B\u7ED9 overrideReason\uFF08\u63A8\u7FFB watcher \u7684\u7406\u7531\uFF09",
6768
+ path: ["overrideReason"]
6746
6769
  });
6747
6770
  TopicCloseArgsSchema = external_exports.object({
6748
6771
  topicId: external_exports.string().min(1),
@@ -6769,27 +6792,25 @@ var init_topic = __esm({
6769
6792
  acceptedLines: external_exports.number().int().min(1),
6770
6793
  totalLines: external_exports.number().int().min(1),
6771
6794
  /** 全部线都合格了 → 可以收敛上报。由 daemon 数,不让 Master 自己盘 */
6772
- allAccepted: external_exports.boolean()
6795
+ allAccepted: external_exports.boolean(),
6796
+ /** 本话题累计推翻 watcher 的次数(含这一次) */
6797
+ overrides: external_exports.number().int().min(0),
6798
+ /**
6799
+ * 反复推翻 watcher = 取证链路有系统性问题(如 worker 用 heredoc 写文件导致正文
6800
+ * 写得没法验),该让老板知道并去修验收标准,而不是每条线都靠 Master 补证。
6801
+ */
6802
+ needsOwnerAttention: external_exports.boolean()
6773
6803
  });
6774
6804
  TopicResultSchema = external_exports.object({ topic: TopicSchema });
6775
6805
  }
6776
6806
  });
6777
6807
 
6778
6808
  // ../protocol/src/master.ts
6779
- var LineDigestArgsSchema, LineDigestResultSchema, AUTHORITY_ACTIONS, AuthorityActionSchema, AuthorityRuleSchema, AuthorityTableSchema, AuthorityCheckArgsSchema, AuthorityCheckResultSchema;
6809
+ var AUTHORITY_ACTIONS, AuthorityActionSchema, AuthorityRuleSchema, AuthorityTableSchema, AuthorityCheckArgsSchema, AuthorityCheckResultSchema;
6780
6810
  var init_master = __esm({
6781
6811
  "../protocol/src/master.ts"() {
6782
6812
  "use strict";
6783
6813
  init_zod();
6784
- LineDigestArgsSchema = external_exports.object({
6785
- /** 一条线 = 一条 dispatch;daemon 内部据此解析 worker session 与 transcript */
6786
- dispatchId: external_exports.string().min(1)
6787
- });
6788
- LineDigestResultSchema = external_exports.object({
6789
- digest: external_exports.string(),
6790
- /** 抽取时是否因超上限被截断——watcher 据此避免"仅凭没看到就判 fail" */
6791
- truncated: external_exports.boolean()
6792
- });
6793
6814
  AUTHORITY_ACTIONS = ["ask", "allow", "deny"];
6794
6815
  AuthorityActionSchema = external_exports.enum(AUTHORITY_ACTIONS);
6795
6816
  AuthorityRuleSchema = external_exports.object({
@@ -8476,18 +8497,19 @@ var init_methods = __esm({
8476
8497
  args: TopicRejectArgsSchema
8477
8498
  },
8478
8499
  "topic:accept": {
8479
- summary: "\u5224\u67D0\u6761\u7EBF\u5408\u683C\uFF1A\u7F6E accepted \u5E76\u540C\u6B65\u843D\u4E00\u6761\u6D41\u6C34\uFF1Breason \u5FC5\u586B\uFF08\u4E0E\u6253\u56DE\u5BF9\u79F0\uFF0C\u53EA\u8BB0\u6253\u56DE\u7B49\u4E8E\u53EA\u8BB0\u5206\u5B50\uFF09\u3002\u8FD4\u56DE acceptedLines / totalLines / allAccepted\uFF0C\u6536\u655B\u65F6\u673A\u7531 daemon \u6570\u4E0D\u7531 Master \u76D8",
8480
- args: TopicAcceptArgsSchema
8500
+ summary: "\u5224\u67D0\u6761\u7EBF\u5408\u683C\uFF1A\u7F6E accepted \u5E76\u540C\u6B65\u843D\u4E00\u6761\u6D41\u6C34\uFF1Breason \u4E0E watcherVerdict \u5FC5\u586B\uFF08\u4E0E\u6253\u56DE\u5BF9\u79F0\uFF0C\u53EA\u8BB0\u6253\u56DE\u7B49\u4E8E\u53EA\u8BB0\u5206\u5B50\uFF09\u3002watcherVerdict=fail \u65F6\u5FC5\u987B\u7ED9 overrideReason\uFF0Cdaemon \u5355\u72EC\u8BA1\u63A8\u7FFB\u6B21\u6570\u3002\u8FD4\u56DE acceptedLines / totalLines / allAccepted / overrides / needsOwnerAttention\uFF0C\u6536\u655B\u65F6\u673A\u7531 daemon \u6570\u4E0D\u7531 Master \u76D8",
8501
+ args: TopicAcceptArgsSchema,
8502
+ argsConstraints: {
8503
+ // watcherVerdict=fail 时 overrideReason 必填(.refine 的谓词不可序列化,这里显式声明)
8504
+ if: { properties: { watcherVerdict: { const: "fail" } }, required: ["watcherVerdict"] },
8505
+ then: { required: ["overrideReason"] }
8506
+ }
8481
8507
  },
8482
8508
  "topic:close": {
8483
8509
  summary: "\u6536\u655B\u8BDD\u9898\uFF1Astate \u7F6E done / archived",
8484
8510
  args: TopicCloseArgsSchema
8485
8511
  },
8486
- // ---- line:digest / master:*(核验与授权,Master 经 clawd-rpc 桥调)----
8487
- "line:digest": {
8488
- summary: "\u628A\u4E00\u6761\u7EBF\uFF08dispatch\uFF09\u7684 worker transcript \u62BD\u6210 \u226420KB digest\uFF1A\u6539\u52A8\u8FC7\u7684\u6587\u4EF6 / \u6267\u884C\u8FC7\u7684\u547D\u4EE4 / \u6210\u529F\u547D\u4EE4\u8F93\u51FA\u6458\u5F55 / \u62A5\u9519\u805A\u5408 / \u5C3E\u90E8 assistant \u8F93\u51FA\u3002\u6838\u9A8C subagent \u7528\u5B83\uFF0C\u907F\u514D\u76F4\u63A5\u8BFB\u53EF\u8FBE 10MB \u7684\u539F\u59CB\u6D41",
8489
- args: LineDigestArgsSchema
8490
- },
8512
+ // ---- master:*(授权查表,Master 经 clawd-rpc 桥调)----
8491
8513
  "master:authorityCheck": {
8492
8514
  summary: "\u67E5 Master \u7684\u6388\u6743\u8BBE\u5B9A\uFF08\u5168\u5C40\u4E00\u5957\uFF09\uFF1A\u8FD4\u56DE ask / allow / deny\u3002\u8868\u7F3A\u5931\u3001\u8BFB\u574F\u3001\u7ED3\u6784\u4E0D\u5BF9\u3001action \u62FC\u9519\u3001id \u672A\u767B\u8BB0\uFF0C\u4E00\u5F8B\u88C1\u51B3 ask",
8493
8515
  args: AuthorityCheckArgsSchema
@@ -14597,7 +14619,7 @@ var init_claude_history = __esm({
14597
14619
  /**
14598
14620
  * 定位一条 session 的 transcript 文件;不存在返回 null。
14599
14621
  *
14600
- * 对外开这一口是为了让 adapter 之外的消费者(`line:digest`)**不必自己拼路径**:
14622
+ * 对外开这一口是为了让 adapter 之外的消费者**不必自己拼路径**:
14601
14623
  * 「按 cwd 直取 → 按 tsid 全局兜底」这套 relocation 逻辑曾被抄成第二份,
14602
14624
  * 规则一改就静默漂移。路径编码(cwdToHashDir)与兜底顺序全仓只此一处。
14603
14625
  *
@@ -52691,7 +52713,7 @@ function createTopicStore(deps) {
52691
52713
  const touch = (t) => {
52692
52714
  t.updatedAt = new Date(deps.now()).toISOString();
52693
52715
  };
52694
- const lineMetaOf = (t, lineId) => t.lineMeta[lineId] ??= { rejectCount: 0, accepted: false };
52716
+ const lineMetaOf = (t, lineId) => t.lineMeta[lineId] ??= { rejectCount: 0, accepted: false, overrideCount: 0 };
52695
52717
  return {
52696
52718
  load: file.load,
52697
52719
  flushNow: file.flushNow,
@@ -52748,6 +52770,15 @@ function createTopicStore(deps) {
52748
52770
  file.scheduleFlush();
52749
52771
  return meta;
52750
52772
  },
52773
+ bumpOverride(topicId, lineId) {
52774
+ const t = get(topicId);
52775
+ if (!t) return void 0;
52776
+ const meta = lineMetaOf(t, lineId);
52777
+ meta.overrideCount = (meta.overrideCount ?? 0) + 1;
52778
+ touch(t);
52779
+ file.scheduleFlush();
52780
+ return meta;
52781
+ },
52751
52782
  markAccepted(topicId, lineId) {
52752
52783
  const t = get(topicId);
52753
52784
  if (!t) return void 0;
@@ -52757,7 +52788,7 @@ function createTopicStore(deps) {
52757
52788
  file.scheduleFlush();
52758
52789
  return meta;
52759
52790
  },
52760
- close(topicId, state) {
52791
+ setState(topicId, state) {
52761
52792
  const t = get(topicId);
52762
52793
  if (!t) return void 0;
52763
52794
  t.state = state;
@@ -54387,6 +54418,21 @@ function buildPersonaDispatchHandlers(deps) {
54387
54418
  );
54388
54419
  }
54389
54420
  }
54421
+ const topicId = args.meta?.topicId;
54422
+ if (typeof topicId === "string" && topicId && deps.getTopicMembership) {
54423
+ const members = deps.getTopicMembership(topicId);
54424
+ if (!members) {
54425
+ throw new ClawdError(ERROR_CODES.INVALID_PARAM, `topic not found: ${topicId}`);
54426
+ }
54427
+ const who = args.targetDeviceId ?? targetPersona;
54428
+ const isMember = args.targetDeviceId ? members.deviceIds.includes(args.targetDeviceId) : members.personaIds.includes(targetPersona);
54429
+ if (!isMember) {
54430
+ throw new ClawdError(
54431
+ ERROR_CODES.INVALID_PARAM,
54432
+ `${who} is not a member of the channel behind topic ${topicId} \u2014\u2014 \u5148\u628A\u5B83\u52A0\u8FDB\u9891\u9053\u518D\u6D3E\u6D3B`
54433
+ );
54434
+ }
54435
+ }
54390
54436
  if (args.targetDeviceId) {
54391
54437
  if (ctx?.principal.kind === "guest") {
54392
54438
  throw new ClawdError(
@@ -61658,7 +61704,6 @@ var METHOD_GRANT_MAP = {
61658
61704
  "topic:reject": ADMIN_ANY,
61659
61705
  "topic:accept": ADMIN_ANY,
61660
61706
  "topic:close": ADMIN_ANY,
61661
- "line:digest": ADMIN_ANY,
61662
61707
  "master:authorityCheck": ADMIN_ANY,
61663
61708
  // project:*(sidebar 项目分区,spec 2026-08-17):registry 是 owner 本机资源,owner-only 一刀切
61664
61709
  "project:list": ADMIN_ANY,
@@ -61825,7 +61870,7 @@ function computeMethodAccess(args) {
61825
61870
  }
61826
61871
 
61827
61872
  // src/version.ts
61828
- var version = "0.2.385".length > 0 ? "0.2.385" : "dev";
61873
+ var version = "0.2.386".length > 0 ? "0.2.386" : "dev";
61829
61874
 
61830
61875
  // src/cli-probe/probe.ts
61831
61876
  var fs66 = __toESM(require("fs"), 1);
@@ -64017,6 +64062,7 @@ function buildChannelHandlers(deps) {
64017
64062
  // src/handlers/topic.ts
64018
64063
  init_protocol();
64019
64064
  var REJECT_ESCALATE_AT = 2;
64065
+ var OVERRIDE_ESCALATE_AT = 2;
64020
64066
  function joinLines(records, lineMeta) {
64021
64067
  const out = [];
64022
64068
  for (const r of records) {
@@ -64104,8 +64150,9 @@ function buildTopicHandlers(deps) {
64104
64150
  },
64105
64151
  "topic:reject": async (frame) => {
64106
64152
  const args = TopicRejectArgsSchema.parse(frame);
64107
- mustGet(args.topicId);
64153
+ const topic = mustGet(args.topicId);
64108
64154
  const { rejectCount } = deps.topicStore.bumpReject(args.topicId, args.lineId);
64155
+ if (topic.state === "done") deps.topicStore.setState(args.topicId, "running");
64109
64156
  deps.topicStore.appendActivity(args.topicId, {
64110
64157
  actor: "master",
64111
64158
  action: `\u6253\u56DE ${args.lineId}\uFF08\u7B2C ${rejectCount} \u6B21\uFF09`,
@@ -64129,206 +64176,49 @@ function buildTopicHandlers(deps) {
64129
64176
  );
64130
64177
  }
64131
64178
  deps.topicStore.markAccepted(args.topicId, args.lineId);
64179
+ if (args.watcherVerdict === "fail") {
64180
+ deps.topicStore.bumpOverride(args.topicId, args.lineId);
64181
+ deps.topicStore.appendActivity(args.topicId, {
64182
+ actor: "master",
64183
+ action: `\u63A8\u7FFB watcher \u5BF9 ${args.lineId} \u7684 fail \u5224\u5B9A`,
64184
+ target: args.lineId,
64185
+ reason: args.overrideReason
64186
+ });
64187
+ }
64132
64188
  deps.topicStore.appendActivity(args.topicId, {
64133
64189
  actor: "master",
64134
- action: `\u5224\u5B9A ${args.lineId} \u5408\u683C`,
64190
+ action: `\u5224\u5B9A ${args.lineId} \u5408\u683C\uFF08watcher: ${args.watcherVerdict}\uFF09`,
64135
64191
  target: args.lineId,
64136
64192
  reason: args.reason,
64137
64193
  ...args.artifacts ? { artifacts: args.artifacts } : {}
64138
64194
  });
64139
64195
  const lines = linesOf(mustGet(args.topicId));
64140
64196
  const acceptedLines = lines.filter((l) => l.accepted).length;
64197
+ const allAccepted = lines.length > 0 && acceptedLines === lines.length;
64198
+ if (allAccepted) deps.topicStore.setState(args.topicId, "done");
64199
+ const overrides = Object.values(mustGet(args.topicId).lineMeta).reduce(
64200
+ (n, m2) => n + (m2.overrideCount ?? 0),
64201
+ 0
64202
+ );
64141
64203
  return {
64142
64204
  response: TopicAcceptResultSchema.parse({
64143
64205
  acceptedLines,
64144
64206
  totalLines: lines.length,
64145
- allAccepted: acceptedLines === lines.length
64207
+ allAccepted,
64208
+ overrides,
64209
+ needsOwnerAttention: overrides >= OVERRIDE_ESCALATE_AT
64146
64210
  })
64147
64211
  };
64148
64212
  },
64149
64213
  "topic:close": async (frame) => {
64150
64214
  const args = TopicCloseArgsSchema.parse(frame);
64151
64215
  mustGet(args.topicId);
64152
- const topic = deps.topicStore.close(args.topicId, args.state);
64216
+ const topic = deps.topicStore.setState(args.topicId, args.state);
64153
64217
  return { response: TopicResultSchema.parse({ topic }) };
64154
64218
  }
64155
64219
  };
64156
64220
  }
64157
64221
 
64158
- // src/handlers/line.ts
64159
- init_protocol();
64160
- init_claude_history();
64161
-
64162
- // src/channel/digest.ts
64163
- var QUOTA = { files: 0.14, commands: 0.12, outputs: 0.22, errors: 0.24, tail: 0.28 };
64164
- var WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
64165
- var READ_TOOLS = /* @__PURE__ */ new Set(["Read", "NotebookRead"]);
64166
- var DIGEST_DEFAULTS = { tailAssistant: 12, maxBytes: 2e4 };
64167
- var firstLine = (s) => String(s ?? "").split("\n")[0].trim();
64168
- var errSignature = (s) => firstLine(s).replace(/\d+/g, "N").replace(/(^|[\s'"`([])\/[^\s'"`)\]]+/g, "$1<path>").slice(0, 200);
64169
- var blocksOf = (rec4) => {
64170
- const c = rec4?.message?.content;
64171
- return Array.isArray(c) ? c : [];
64172
- };
64173
- function takeWithin(lines, budget) {
64174
- if (!lines.length) return { out: ["- \uFF08\u65E0\uFF09"], clipped: false };
64175
- const out = [];
64176
- let used = 0;
64177
- for (let i = 0; i < lines.length; i++) {
64178
- const size = Buffer.byteLength(lines[i], "utf8") + 1;
64179
- if (used + size > budget && out.length) {
64180
- out.push(`- \u2026\u672C\u8282\u8FD8\u6709 ${lines.length - i} \u6761\u672A\u5217\u51FA\uFF08\u672C\u8282\u914D\u989D ${budget} \u5B57\u8282\u5DF2\u6EE1\uFF09`);
64181
- return { out, clipped: true };
64182
- }
64183
- out.push(lines[i]);
64184
- used += size;
64185
- }
64186
- return { out, clipped: false };
64187
- }
64188
- function clampBytes(text, limit) {
64189
- if (Buffer.byteLength(text, "utf8") <= limit) return text;
64190
- let lo = 0;
64191
- let hi = text.length;
64192
- while (lo < hi) {
64193
- const mid = Math.ceil((lo + hi) / 2);
64194
- if (Buffer.byteLength(text.slice(0, mid), "utf8") <= limit) lo = mid;
64195
- else hi = mid - 1;
64196
- }
64197
- return text.slice(0, lo);
64198
- }
64199
- function buildDigest(records, opts = {}) {
64200
- const maxBytes = opts.maxBytes ?? DIGEST_DEFAULTS.maxBytes;
64201
- const tailAssistant = opts.tailAssistant ?? DIGEST_DEFAULTS.tailAssistant;
64202
- const filesWritten = /* @__PURE__ */ new Set();
64203
- const filesRead = /* @__PURE__ */ new Set();
64204
- const commands = /* @__PURE__ */ new Map();
64205
- const errors = /* @__PURE__ */ new Map();
64206
- const outputs = [];
64207
- const pendingCmd = /* @__PURE__ */ new Map();
64208
- let lastCmd = null;
64209
- const tail = [];
64210
- let toolCalls = 0;
64211
- for (const rec4 of records) {
64212
- for (const b2 of blocksOf(rec4)) {
64213
- if (b2.type === "tool_use") {
64214
- toolCalls += 1;
64215
- const fp = b2.input?.file_path ?? b2.input?.notebook_path;
64216
- if (fp && b2.name && WRITE_TOOLS.has(b2.name)) filesWritten.add(String(fp));
64217
- else if (fp && b2.name && READ_TOOLS.has(b2.name)) filesRead.add(String(fp));
64218
- if (b2.name === "Bash" && b2.input?.command) {
64219
- const c = firstLine(b2.input.command);
64220
- commands.set(c, (commands.get(c) ?? 0) + 1);
64221
- if (b2.id) pendingCmd.set(b2.id, c);
64222
- lastCmd = c;
64223
- }
64224
- } else if (b2.type === "tool_result" && !b2.is_error) {
64225
- const cmd = b2.tool_use_id ? pendingCmd.get(b2.tool_use_id) : lastCmd;
64226
- if (cmd) {
64227
- const text2 = typeof b2.content === "string" ? b2.content : JSON.stringify(b2.content ?? "");
64228
- const flat = text2.replace(/\s+/g, " ").trim();
64229
- if (flat) {
64230
- outputs.push(`- ${cmd} \u2192 \u2026${flat.slice(-220)}`);
64231
- if (outputs.length > 40) outputs.shift();
64232
- }
64233
- if (b2.tool_use_id) pendingCmd.delete(b2.tool_use_id);
64234
- }
64235
- } else if (b2.type === "tool_result" && b2.is_error) {
64236
- const text2 = typeof b2.content === "string" ? b2.content : JSON.stringify(b2.content ?? "");
64237
- const sig = errSignature(text2);
64238
- const prev = errors.get(sig);
64239
- if (prev) prev.count += 1;
64240
- else errors.set(sig, { count: 1, sample: firstLine(text2).slice(0, 300) });
64241
- } else if (b2.type === "text" && rec4.type === "assistant") {
64242
- tail.push(b2.text ?? "");
64243
- if (tail.length > tailAssistant) tail.shift();
64244
- }
64245
- }
64246
- }
64247
- const budget = (k2) => Math.max(200, Math.floor(maxBytes * QUOTA[k2]));
64248
- let anyClipped = false;
64249
- const take = (lines, k2) => {
64250
- const r = takeWithin(lines, budget(k2));
64251
- if (r.clipped) anyClipped = true;
64252
- return r.out;
64253
- };
64254
- const errLines = [...errors.entries()].sort((x, y2) => y2[1].count - x[1].count).map(([, v2]) => `- \xD7${v2.count} ${v2.sample}`);
64255
- const cmdLines = [...commands.entries()].map(([c, n]) => n > 1 ? `- \xD7${n} ${c}` : `- ${c}`);
64256
- const out = [
64257
- "# worker transcript digest",
64258
- ...opts.transcriptPath ? [`transcript: ${opts.transcriptPath}`] : [],
64259
- `\u5DE5\u5177\u8C03\u7528\u603B\u6570: ${toolCalls}\uFF5C\u5199\u6587\u4EF6 ${filesWritten.size} \u4E2A\uFF5C\u8BFB\u6587\u4EF6 ${filesRead.size} \u4E2A\uFF5C\u4E0D\u540C\u547D\u4EE4 ${commands.size} \u6761\uFF5C\u62A5\u9519\u7B7E\u540D ${errors.size} \u79CD`,
64260
- "",
64261
- "## \u6539\u52A8\u8FC7\u7684\u6587\u4EF6\uFF08\u5F71\u54CD\u9762\uFF1B\u53EA\u7EDF\u8BA1 Write/Edit/MultiEdit/NotebookEdit\uFF09",
64262
- ...take([...filesWritten].map((f) => `- ${f}`), "files"),
64263
- "",
64264
- "## \u6267\u884C\u8FC7\u7684\u547D\u4EE4\uFF08\u91CD\u590D\u7684\u6298\u53E0\u8BA1\u6570\uFF09",
64265
- ...take(cmdLines, "commands"),
64266
- "",
64267
- "## \u62A5\u9519\uFF08\u6309\u7B7E\u540D\u805A\u5408\uFF1B\u91CD\u590D\u6B21\u6570\u9AD8 = \u649E\u5899\uFF09",
64268
- ...take(errLines, "errors"),
64269
- "",
64270
- "## \u6210\u529F\u547D\u4EE4\u7684\u8F93\u51FA\u6458\u5F55\uFF08\u5404\u53D6\u5C3E\u90E8\uFF1B\u5224\u300C\u6D4B\u8BD5\u771F\u8DD1\u8FC7\u6CA1\u3001\u8FC7\u4E86\u51E0\u6761\u300D\u770B\u8FD9\u91CC\uFF09",
64271
- ...take(outputs, "outputs"),
64272
- "",
64273
- `## \u6700\u540E ${tail.length} \u6BB5 assistant \u8F93\u51FA\uFF08\u4FDD\u7559\u539F\u6587\u6362\u884C\uFF0C\u6BCF\u6BB5\u622A\u65AD\u81F3 500 \u5B57\uFF09`,
64274
- // 只留首行会把 worker 的收尾报告砍成「做完了」这种没信息量的一句,
64275
- // 而那正是判「假完成」最需要的原文。
64276
- ...take(tail.map((t) => `- ${String(t ?? "").slice(0, 500)}`), "tail")
64277
- ];
64278
- let text = out.join("\n");
64279
- let truncated = anyClipped;
64280
- if (Buffer.byteLength(text, "utf8") > maxBytes) {
64281
- text = clampBytes(text, maxBytes);
64282
- text += `
64283
-
64284
- \u26A0\uFE0F digest \u5DF2\u622A\u65AD\uFF08\u4E0A\u9650 ${maxBytes} \u5B57\u8282\uFF09\u2014\u2014\u672A\u89C1\u5168\u90E8\u5185\u5BB9\uFF0C\u5224\u5B9A"\u6CA1\u505A"\u524D\u5148\u8981\u6C42 worker \u81EA\u8BC1\u3002`;
64285
- truncated = true;
64286
- } else if (anyClipped) {
64287
- text += `
64288
-
64289
- \u26A0\uFE0F digest \u5DF2\u622A\u65AD\uFF08\u6709\u5C0F\u8282\u8D85\u51FA\u914D\u989D\uFF0C\u89C1\u4E0A\u65B9\u300C\u672A\u5217\u51FA\u300D\u6807\u6CE8\uFF09\u2014\u2014\u672A\u89C1\u5168\u90E8\u5185\u5BB9\uFF0C\u5224\u5B9A"\u6CA1\u505A"\u524D\u5148\u8981\u6C42 worker \u81EA\u8BC1\u3002`;
64290
- }
64291
- return { digest: text, truncated };
64292
- }
64293
-
64294
- // src/handlers/line.ts
64295
- function buildLineHandlers(deps) {
64296
- return {
64297
- "line:digest": async (frame) => {
64298
- const args = LineDigestArgsSchema.parse(frame);
64299
- const rec4 = deps.dispatchStore.get(args.dispatchId);
64300
- if (!rec4) {
64301
- throw new ClawdError(ERROR_CODES.INVALID_PARAM, `dispatch not found: ${args.dispatchId}`);
64302
- }
64303
- if (!rec4.workerSessionId) {
64304
- throw new ClawdError(
64305
- ERROR_CODES.INVALID_PARAM,
64306
- `\u8BE5\u7EBF\u8FD8\u6CA1\u6709 worker \u4F1A\u8BDD\uFF08spawn \u672A\u5B8C\u6210\uFF09\uFF0C\u6682\u65F6\u65E0\u6CD5\u6838\u9A8C: ${args.dispatchId}`
64307
- );
64308
- }
64309
- const sf = deps.getSessionFile(rec4.workerSessionId);
64310
- if (!sf?.toolSessionId) {
64311
- throw new ClawdError(
64312
- ERROR_CODES.INVALID_PARAM,
64313
- `worker \u4F1A\u8BDD\u5C1A\u672A\u4EA7\u751F transcript\uFF08\u8FD8\u6CA1\u8DD1\u8D77\u6765\uFF09: ${rec4.workerSessionId}`
64314
- );
64315
- }
64316
- const file = deps.history.resolveTranscriptFile(sf.cwd, sf.toolSessionId);
64317
- if (!file) {
64318
- throw new ClawdError(
64319
- ERROR_CODES.INVALID_PARAM,
64320
- `transcript \u4E0D\u5B58\u5728\uFF08worker \u53EF\u80FD\u8FD8\u6CA1\u5199\u76D8\uFF09: ${sf.toolSessionId}`
64321
- );
64322
- }
64323
- return {
64324
- response: LineDigestResultSchema.parse(
64325
- buildDigest(streamJsonlLines(file), { transcriptPath: file })
64326
- )
64327
- };
64328
- }
64329
- };
64330
- }
64331
-
64332
64222
  // src/handlers/master.ts
64333
64223
  init_protocol();
64334
64224
 
@@ -64964,12 +64854,6 @@ function buildMethodHandlers(deps) {
64964
64854
  ...buildMasterHandlers({
64965
64855
  authorityTablePath: import_node_path67.default.join(deps.masterDataDir, "master-authority.json")
64966
64856
  }),
64967
- ...buildLineHandlers({
64968
- dispatchStore: deps.dispatchStore,
64969
- getSessionFile: deps.getWorkerSessionFile,
64970
- // transcript 定位/读取复用 history:* 那个同一个 reader,不另起一份路径规则
64971
- history: deps.history
64972
- }),
64973
64857
  ...buildTopicHandlers({
64974
64858
  topicStore: deps.topicStore,
64975
64859
  channelStore: deps.channelStore,
@@ -66440,17 +66324,12 @@ async function startDaemon(config) {
66440
66324
  channelStore,
66441
66325
  topicStore,
66442
66326
  dispatchStore,
66443
- // line:digest 用:worker session 的 cwd / toolSessionId(transcript 定位的输入)
66444
- getWorkerSessionFile: (sessionId) => {
66445
- const f = manager.findOwnedSession(sessionId);
66446
- return f ? { cwd: f.cwd, ...f.toolSessionId ? { toolSessionId: f.toolSessionId } : {} } : void 0;
66447
- },
66448
66327
  masterDataDir: config.dataDir,
66449
66328
  // 建话题即起该话题的 Master session(1:1)。走与 session:create 同一条 manager.create
66450
66329
  // 链路,不复制会话创建逻辑;creatorPrincipalId 用本机 deviceId(owner ctx 的 principal.id)。
66451
66330
  createMasterSession: ({ label }) => {
66452
66331
  const { response } = manager.create(
66453
- { ownerPersonaId: "persona-master", label },
66332
+ { ownerPersonaId: MASTER_PERSONA_ID, label },
66454
66333
  authFile.deviceId,
66455
66334
  ownerDisplayName
66456
66335
  );
@@ -66683,6 +66562,15 @@ async function startDaemon(config) {
66683
66562
  const dispatchHandlers = buildPersonaDispatchHandlers({
66684
66563
  personaDispatchManager,
66685
66564
  store: dispatchStore,
66565
+ // 话题工作线的派活边界:topicId → 它所属频道的两张成员名单。频道 / 话题任一不存在
66566
+ // 都返回 undefined,由 handler 判成「话题不存在」拒掉(编 topicId 绕不过去)。
66567
+ getTopicMembership: (topicId) => {
66568
+ const t = topicStore.get(topicId);
66569
+ if (!t) return void 0;
66570
+ const ch = channelStore.get(t.channelId);
66571
+ if (!ch) return void 0;
66572
+ return { personaIds: [...ch.memberPersonaIds], deviceIds: [...ch.memberContactDeviceIds] };
66573
+ },
66686
66574
  // 续派(spec 2026-07-21):任务包注入 worker session,复用投递同款注入路径。
66687
66575
  sendFollowUp: (workerSessionId, text) => manager.injectIntoSession(workerSessionId, text),
66688
66576
  logger,
@@ -21820,7 +21820,8 @@ var ChannelSchema = external_exports.object({
21820
21820
  name: external_exports.string().min(1),
21821
21821
  /** 该频道里可被派活的 persona */
21822
21822
  memberPersonaIds: external_exports.array(external_exports.string().min(1)),
21823
- /** 人类成员(contact deviceId);M2 只存不用,P1 异步询问才消费 */
21823
+ /** 人类成员(contact deviceId)。跨设备派活的成员边界认这张名单(personaDispatch 校验),
21824
+ * P1 人类异步询问也用它 */
21824
21825
  memberContactDeviceIds: external_exports.array(external_exports.string().min(1)),
21825
21826
  createdAt: external_exports.string().min(1),
21826
21827
  updatedAt: external_exports.string().min(1)
@@ -21861,7 +21862,18 @@ var TopicActivityEntrySchema = external_exports.object({
21861
21862
  });
21862
21863
  var TopicLineMetaSchema = external_exports.object({
21863
21864
  rejectCount: external_exports.number().int().min(0),
21864
- accepted: external_exports.boolean().default(false)
21865
+ accepted: external_exports.boolean().default(false),
21866
+ /**
21867
+ * Master 在 watcher 判 fail 的情况下仍判合格的次数。
21868
+ *
21869
+ * 为什么要单独数:watcher 的 fail 若能被一句「这是可观测性缺口」无声绕过,核验就形同
21870
+ * 虚设——实测一个话题三条线的 watcher 全报 fail、无一打回,Master 自己下场补证后判了合格。
21871
+ * 覆盖本身不禁止(watcher 确实会误判),但必须留痕、必须计数、反复覆盖必须惊动老板。
21872
+ *
21873
+ * optional 而非 default(0):store 不按 schema parse(见 topic-store),存量 lineMeta
21874
+ * 里本来就没有这个字段,读侧一律 `?? 0`。
21875
+ */
21876
+ overrideCount: external_exports.number().int().min(0).optional()
21865
21877
  });
21866
21878
  var TopicSchema = external_exports.object({
21867
21879
  id: external_exports.string().min(1),
@@ -21931,7 +21943,17 @@ var TopicAcceptArgsSchema = external_exports.object({
21931
21943
  /** 凭什么判合格(逐条标准对应到什么证据)——与打回同样必填 */
21932
21944
  reason: nonBlank2,
21933
21945
  /** watcher 认定的产出物,一并进影响面 */
21934
- artifacts: external_exports.array(nonBlank2).optional()
21946
+ artifacts: external_exports.array(nonBlank2).optional(),
21947
+ /**
21948
+ * 这条线的 watcher 核验结论。**必填**:不填等于允许「压根没跑 watcher 就发合格证」,
21949
+ * 而合格与打回之所以要对称落账,就是为了让老板事后分得出「判过且过了」和「没judge过」。
21950
+ */
21951
+ watcherVerdict: external_exports.enum(["pass", "fail"]),
21952
+ /** watcher 判 fail 却仍要判合格时,凭什么推翻它(fail 时必填) */
21953
+ overrideReason: nonBlank2.optional()
21954
+ }).refine((v) => v.watcherVerdict !== "fail" || Boolean(v.overrideReason), {
21955
+ message: "watcherVerdict=fail \u65F6\u5FC5\u987B\u7ED9 overrideReason\uFF08\u63A8\u7FFB watcher \u7684\u7406\u7531\uFF09",
21956
+ path: ["overrideReason"]
21935
21957
  });
21936
21958
  var TopicCloseArgsSchema = external_exports.object({
21937
21959
  topicId: external_exports.string().min(1),
@@ -21958,20 +21980,18 @@ var TopicAcceptResultSchema = external_exports.object({
21958
21980
  acceptedLines: external_exports.number().int().min(1),
21959
21981
  totalLines: external_exports.number().int().min(1),
21960
21982
  /** 全部线都合格了 → 可以收敛上报。由 daemon 数,不让 Master 自己盘 */
21961
- allAccepted: external_exports.boolean()
21983
+ allAccepted: external_exports.boolean(),
21984
+ /** 本话题累计推翻 watcher 的次数(含这一次) */
21985
+ overrides: external_exports.number().int().min(0),
21986
+ /**
21987
+ * 反复推翻 watcher = 取证链路有系统性问题(如 worker 用 heredoc 写文件导致正文
21988
+ * 写得没法验),该让老板知道并去修验收标准,而不是每条线都靠 Master 补证。
21989
+ */
21990
+ needsOwnerAttention: external_exports.boolean()
21962
21991
  });
21963
21992
  var TopicResultSchema = external_exports.object({ topic: TopicSchema });
21964
21993
 
21965
21994
  // ../protocol/src/master.ts
21966
- var LineDigestArgsSchema = external_exports.object({
21967
- /** 一条线 = 一条 dispatch;daemon 内部据此解析 worker session 与 transcript */
21968
- dispatchId: external_exports.string().min(1)
21969
- });
21970
- var LineDigestResultSchema = external_exports.object({
21971
- digest: external_exports.string(),
21972
- /** 抽取时是否因超上限被截断——watcher 据此避免"仅凭没看到就判 fail" */
21973
- truncated: external_exports.boolean()
21974
- });
21975
21995
  var AUTHORITY_ACTIONS = ["ask", "allow", "deny"];
21976
21996
  var AuthorityActionSchema = external_exports.enum(AUTHORITY_ACTIONS);
21977
21997
  var AuthorityRuleSchema = external_exports.object({
@@ -23537,18 +23557,19 @@ var METHOD_DOCS = {
23537
23557
  args: TopicRejectArgsSchema
23538
23558
  },
23539
23559
  "topic:accept": {
23540
- summary: "\u5224\u67D0\u6761\u7EBF\u5408\u683C\uFF1A\u7F6E accepted \u5E76\u540C\u6B65\u843D\u4E00\u6761\u6D41\u6C34\uFF1Breason \u5FC5\u586B\uFF08\u4E0E\u6253\u56DE\u5BF9\u79F0\uFF0C\u53EA\u8BB0\u6253\u56DE\u7B49\u4E8E\u53EA\u8BB0\u5206\u5B50\uFF09\u3002\u8FD4\u56DE acceptedLines / totalLines / allAccepted\uFF0C\u6536\u655B\u65F6\u673A\u7531 daemon \u6570\u4E0D\u7531 Master \u76D8",
23541
- args: TopicAcceptArgsSchema
23560
+ summary: "\u5224\u67D0\u6761\u7EBF\u5408\u683C\uFF1A\u7F6E accepted \u5E76\u540C\u6B65\u843D\u4E00\u6761\u6D41\u6C34\uFF1Breason \u4E0E watcherVerdict \u5FC5\u586B\uFF08\u4E0E\u6253\u56DE\u5BF9\u79F0\uFF0C\u53EA\u8BB0\u6253\u56DE\u7B49\u4E8E\u53EA\u8BB0\u5206\u5B50\uFF09\u3002watcherVerdict=fail \u65F6\u5FC5\u987B\u7ED9 overrideReason\uFF0Cdaemon \u5355\u72EC\u8BA1\u63A8\u7FFB\u6B21\u6570\u3002\u8FD4\u56DE acceptedLines / totalLines / allAccepted / overrides / needsOwnerAttention\uFF0C\u6536\u655B\u65F6\u673A\u7531 daemon \u6570\u4E0D\u7531 Master \u76D8",
23561
+ args: TopicAcceptArgsSchema,
23562
+ argsConstraints: {
23563
+ // watcherVerdict=fail 时 overrideReason 必填(.refine 的谓词不可序列化,这里显式声明)
23564
+ if: { properties: { watcherVerdict: { const: "fail" } }, required: ["watcherVerdict"] },
23565
+ then: { required: ["overrideReason"] }
23566
+ }
23542
23567
  },
23543
23568
  "topic:close": {
23544
23569
  summary: "\u6536\u655B\u8BDD\u9898\uFF1Astate \u7F6E done / archived",
23545
23570
  args: TopicCloseArgsSchema
23546
23571
  },
23547
- // ---- line:digest / master:*(核验与授权,Master 经 clawd-rpc 桥调)----
23548
- "line:digest": {
23549
- summary: "\u628A\u4E00\u6761\u7EBF\uFF08dispatch\uFF09\u7684 worker transcript \u62BD\u6210 \u226420KB digest\uFF1A\u6539\u52A8\u8FC7\u7684\u6587\u4EF6 / \u6267\u884C\u8FC7\u7684\u547D\u4EE4 / \u6210\u529F\u547D\u4EE4\u8F93\u51FA\u6458\u5F55 / \u62A5\u9519\u805A\u5408 / \u5C3E\u90E8 assistant \u8F93\u51FA\u3002\u6838\u9A8C subagent \u7528\u5B83\uFF0C\u907F\u514D\u76F4\u63A5\u8BFB\u53EF\u8FBE 10MB \u7684\u539F\u59CB\u6D41",
23550
- args: LineDigestArgsSchema
23551
- },
23572
+ // ---- master:*(授权查表,Master 经 clawd-rpc 桥调)----
23552
23573
  "master:authorityCheck": {
23553
23574
  summary: "\u67E5 Master \u7684\u6388\u6743\u8BBE\u5B9A\uFF08\u5168\u5C40\u4E00\u5957\uFF09\uFF1A\u8FD4\u56DE ask / allow / deny\u3002\u8868\u7F3A\u5931\u3001\u8BFB\u574F\u3001\u7ED3\u6784\u4E0D\u5BF9\u3001action \u62FC\u9519\u3001id \u672A\u767B\u8BB0\uFF0C\u4E00\u5F8B\u88C1\u51B3 ask",
23554
23575
  args: AuthorityCheckArgsSchema