@clawos-dev/clawd 0.2.410 → 0.2.412

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
@@ -6630,19 +6630,23 @@ var init_shift_internal = __esm({
6630
6630
  });
6631
6631
 
6632
6632
  // ../protocol/src/channel.ts
6633
- var MASTER_PERSONA_ID, ChannelSchema, nonBlank, ChannelCreateArgsSchema, ChannelUpdateArgsSchema, ChannelDeleteArgsSchema, ChannelListResultSchema, ChannelResultSchema, ChannelDeleteResultSchema;
6633
+ var MASTER_PERSONA_ID, ChannelMemberPersonaSchema, ChannelSchema, nonBlank, ChannelCreateArgsSchema, ChannelUpdateArgsSchema, ChannelDeleteArgsSchema, ChannelListResultSchema, ChannelResultSchema, ChannelDeleteResultSchema;
6634
6634
  var init_channel = __esm({
6635
6635
  "../protocol/src/channel.ts"() {
6636
6636
  "use strict";
6637
6637
  init_zod();
6638
6638
  MASTER_PERSONA_ID = "persona-master";
6639
+ ChannelMemberPersonaSchema = external_exports.object({
6640
+ personaId: external_exports.string().min(1),
6641
+ deviceId: external_exports.string().min(1).optional()
6642
+ });
6639
6643
  ChannelSchema = external_exports.object({
6640
6644
  id: external_exports.string().min(1),
6641
6645
  name: external_exports.string().min(1),
6642
- /** 该频道里可被派活的 persona */
6643
- memberPersonaIds: external_exports.array(external_exports.string().min(1)),
6644
- /** 人类成员(contact deviceId)。跨设备派活的成员边界认这张名单(personaDispatch 校验),
6645
- * P1 人类异步询问也用它 */
6646
+ /** 该频道里可被派活的 persona 全集(本机 + 远程) */
6647
+ memberPersonas: external_exports.array(ChannelMemberPersonaSchema),
6648
+ /** 人类成员(contact deviceId):可发 DM。**不隐含任何派活权限** —— 跨设备派活认的是
6649
+ * 上面那张 memberPersonas。P1 人类异步询问也用它 */
6646
6650
  memberContactDeviceIds: external_exports.array(external_exports.string().min(1)),
6647
6651
  createdAt: external_exports.string().min(1),
6648
6652
  updatedAt: external_exports.string().min(1)
@@ -6650,13 +6654,13 @@ var init_channel = __esm({
6650
6654
  nonBlank = external_exports.string().trim().min(1);
6651
6655
  ChannelCreateArgsSchema = external_exports.object({
6652
6656
  name: nonBlank,
6653
- memberPersonaIds: external_exports.array(external_exports.string().min(1)).default([]),
6657
+ memberPersonas: external_exports.array(ChannelMemberPersonaSchema).default([]),
6654
6658
  memberContactDeviceIds: external_exports.array(external_exports.string().min(1)).default([])
6655
6659
  });
6656
6660
  ChannelUpdateArgsSchema = external_exports.object({
6657
6661
  channelId: external_exports.string().min(1),
6658
6662
  name: nonBlank.optional(),
6659
- memberPersonaIds: external_exports.array(external_exports.string().min(1)).optional(),
6663
+ memberPersonas: external_exports.array(ChannelMemberPersonaSchema).optional(),
6660
6664
  memberContactDeviceIds: external_exports.array(external_exports.string().min(1)).optional()
6661
6665
  });
6662
6666
  ChannelDeleteArgsSchema = external_exports.object({ channelId: external_exports.string().min(1) });
@@ -14365,22 +14369,49 @@ var init_tool_result_extra = __esm({
14365
14369
  function cwdToHashDir(cwd) {
14366
14370
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
14367
14371
  }
14368
- function newestSubagentMtimeMs(projectsRoot, cwd, toolSessionId) {
14369
- const dir = import_node_path5.default.join(projectsRoot, cwdToHashDir(cwd), toolSessionId, "subagents");
14372
+ function subagentsDirCandidates(projectsRoot, cwd, toolSessionId) {
14373
+ const out = [];
14374
+ const seen = /* @__PURE__ */ new Set();
14375
+ const add = (dir) => {
14376
+ if (seen.has(dir)) return;
14377
+ try {
14378
+ if (!import_node_fs6.default.statSync(dir).isDirectory()) return;
14379
+ } catch {
14380
+ return;
14381
+ }
14382
+ seen.add(dir);
14383
+ out.push(dir);
14384
+ };
14385
+ add(import_node_path5.default.join(projectsRoot, cwdToHashDir(cwd), toolSessionId, "subagents"));
14370
14386
  let entries;
14371
14387
  try {
14372
- entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
14388
+ entries = import_node_fs6.default.readdirSync(projectsRoot, { withFileTypes: true });
14373
14389
  } catch {
14374
- return null;
14390
+ return out;
14391
+ }
14392
+ for (const ent of entries) {
14393
+ if (!ent.isDirectory()) continue;
14394
+ add(import_node_path5.default.join(projectsRoot, ent.name, toolSessionId, "subagents"));
14375
14395
  }
14396
+ return out;
14397
+ }
14398
+ function newestSubagentMtimeMs(projectsRoot, cwd, toolSessionId) {
14376
14399
  let newest = null;
14377
- for (const e of entries) {
14378
- if (!e.isFile()) continue;
14379
- if (!e.name.startsWith("agent-") || !e.name.endsWith(".jsonl")) continue;
14400
+ for (const dir of subagentsDirCandidates(projectsRoot, cwd, toolSessionId)) {
14401
+ let entries;
14380
14402
  try {
14381
- const m2 = import_node_fs6.default.statSync(import_node_path5.default.join(dir, e.name)).mtimeMs;
14382
- if (newest === null || m2 > newest) newest = m2;
14403
+ entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
14383
14404
  } catch {
14405
+ continue;
14406
+ }
14407
+ for (const e of entries) {
14408
+ if (!e.isFile()) continue;
14409
+ if (!e.name.startsWith("agent-") || !e.name.endsWith(".jsonl")) continue;
14410
+ try {
14411
+ const m2 = import_node_fs6.default.statSync(import_node_path5.default.join(dir, e.name)).mtimeMs;
14412
+ if (newest === null || m2 > newest) newest = m2;
14413
+ } catch {
14414
+ }
14384
14415
  }
14385
14416
  }
14386
14417
  return newest;
@@ -14934,38 +14965,41 @@ var init_claude_history = __esm({
14934
14965
  return { messages: this.readSubagentFromMainJsonl(args.cwd, args.toolSessionId, args.subagentId) };
14935
14966
  }
14936
14967
  // 独立目录路径:<projectsRoot>/<cwdHash>/<toolSessionId>/subagents/*.jsonl
14937
- // 返回 null 表示目录不存在(调用方回退旧实现);返回空数组表示目录存在但无 jsonl
14968
+ // 返回 null 表示一个候选目录都没有(调用方回退旧实现);返回空数组表示目录在但无 jsonl。
14969
+ // [cwd 漂移] 下取**所有候选目录的并集**:漂移前后的 agent 都该列出来。同名 agentId 只可能
14970
+ // 是同一个 agent(id 全局唯一),取先遇到的那份(byCwd 优先)。
14938
14971
  listSubagentsFromDirectory(cwd, toolSessionId) {
14939
- const dir = import_node_path5.default.join(
14940
- this.resolveSessionDir(cwd, toolSessionId),
14941
- toolSessionId,
14942
- "subagents"
14943
- );
14944
- let entries;
14945
- try {
14946
- entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
14947
- } catch (err) {
14948
- if (err.code === "ENOENT") return null;
14949
- return null;
14950
- }
14972
+ const dirs = subagentsDirCandidates(this.projectsRoot, cwd, toolSessionId);
14973
+ if (dirs.length === 0) return null;
14951
14974
  const out = [];
14952
- for (const e of entries) {
14953
- if (!e.isFile()) continue;
14954
- if (!e.name.startsWith("agent-") || !e.name.endsWith(".jsonl")) continue;
14955
- const subagentId = e.name.slice("agent-".length, -".jsonl".length);
14956
- const filePath = import_node_path5.default.join(dir, e.name);
14957
- const lines = readJsonlLines(filePath);
14958
- let firstText = "";
14959
- let messageCount = 0;
14960
- for (const line of lines) {
14961
- const msg = messageFromJsonl(line);
14962
- if (!msg) continue;
14963
- messageCount += 1;
14964
- if (!firstText && msg.kind === "user-text" && msg.text) {
14965
- firstText = msg.text.slice(0, 120);
14975
+ const seen = /* @__PURE__ */ new Set();
14976
+ for (const dir of dirs) {
14977
+ let entries;
14978
+ try {
14979
+ entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
14980
+ } catch {
14981
+ continue;
14982
+ }
14983
+ for (const e of entries) {
14984
+ if (!e.isFile()) continue;
14985
+ if (!e.name.startsWith("agent-") || !e.name.endsWith(".jsonl")) continue;
14986
+ const subagentId = e.name.slice("agent-".length, -".jsonl".length);
14987
+ if (seen.has(subagentId)) continue;
14988
+ seen.add(subagentId);
14989
+ const filePath = import_node_path5.default.join(dir, e.name);
14990
+ const lines = readJsonlLines(filePath);
14991
+ let firstText = "";
14992
+ let messageCount = 0;
14993
+ for (const line of lines) {
14994
+ const msg = messageFromJsonl(line);
14995
+ if (!msg) continue;
14996
+ messageCount += 1;
14997
+ if (!firstText && msg.kind === "user-text" && msg.text) {
14998
+ firstText = msg.text.slice(0, 120);
14999
+ }
14966
15000
  }
15001
+ out.push({ subagentId, summary: firstText, messageCount });
14967
15002
  }
14968
- out.push({ subagentId, summary: firstText, messageCount });
14969
15003
  }
14970
15004
  return out;
14971
15005
  }
@@ -15002,28 +15036,26 @@ var init_claude_history = __esm({
15002
15036
  messageCount: buckets.get(id)?.count ?? 0
15003
15037
  }));
15004
15038
  }
15005
- // 独立文件路径:agent-<subagentId>.jsonl;文件不存在返回 null 让调用方回退旧实现
15039
+ // 独立文件路径:agent-<subagentId>.jsonl;所有候选目录里都没有才返回 null,让调用方
15040
+ // 回退去扫主 jsonl 的 sidechain。[cwd 漂移] 下要的就是「这个文件落在哪一侧」——
15041
+ // 只看某一个目录在不在,会把漂移侧的 agent 判成不存在。
15006
15042
  readSubagentFromFile(cwd, toolSessionId, subagentId) {
15007
- const file = import_node_path5.default.join(
15008
- this.resolveSessionDir(cwd, toolSessionId),
15009
- toolSessionId,
15010
- "subagents",
15011
- `agent-${subagentId}.jsonl`
15012
- );
15013
- let exists = false;
15014
- try {
15015
- exists = import_node_fs6.default.statSync(file).isFile();
15016
- } catch {
15017
- return null;
15018
- }
15019
- if (!exists) return null;
15020
- const lines = readJsonlLines(file);
15021
- const messages = [];
15022
- for (const line of lines) {
15023
- const msg = messageFromJsonl(line);
15024
- if (msg) messages.push(msg);
15043
+ for (const dir of subagentsDirCandidates(this.projectsRoot, cwd, toolSessionId)) {
15044
+ const file = import_node_path5.default.join(dir, `agent-${subagentId}.jsonl`);
15045
+ try {
15046
+ if (!import_node_fs6.default.statSync(file).isFile()) continue;
15047
+ } catch {
15048
+ continue;
15049
+ }
15050
+ const lines = readJsonlLines(file);
15051
+ const messages = [];
15052
+ for (const line of lines) {
15053
+ const msg = messageFromJsonl(line);
15054
+ if (msg) messages.push(msg);
15055
+ }
15056
+ return messages;
15025
15057
  }
15026
- return messages;
15058
+ return null;
15027
15059
  }
15028
15060
  /**
15029
15061
  * 遍历 session JSONL 找所有 `type:'file-history-snapshot'` 条目,按 messageId 建表
@@ -52969,12 +53001,13 @@ var import_node_path20 = __toESM(require("path"), 1);
52969
53001
  function createJsonStore(deps) {
52970
53002
  let items = [];
52971
53003
  let flushTimer = null;
53004
+ const version2 = deps.version ?? 1;
52972
53005
  async function flushNow() {
52973
53006
  if (flushTimer) {
52974
53007
  clearTimeout(flushTimer);
52975
53008
  flushTimer = null;
52976
53009
  }
52977
- const content = { version: 1, [deps.key]: items };
53010
+ const content = { version: version2, [deps.key]: items };
52978
53011
  await import_promises2.default.mkdir(import_node_path20.default.dirname(deps.filePath), { recursive: true });
52979
53012
  const tmp = `${deps.filePath}.tmp-${deps.now()}-${Math.floor(Math.random() * 1e6)}`;
52980
53013
  await import_promises2.default.writeFile(tmp, JSON.stringify(content, null, 2), "utf8");
@@ -52995,10 +53028,24 @@ function createJsonStore(deps) {
52995
53028
  throw e;
52996
53029
  }
52997
53030
  const parsed = JSON.parse(raw);
52998
- if (parsed.version !== 1) {
52999
- throw new Error(`${deps.key}.json: unsupported version ${String(parsed.version)}`);
53031
+ const found = parsed.version;
53032
+ if (found === version2) {
53033
+ items = parsed[deps.key] ?? [];
53034
+ return;
53035
+ }
53036
+ if (typeof found === "number" && found < version2 && deps.migrate) {
53037
+ items = deps.migrate(parsed[deps.key] ?? [], found);
53038
+ await flushNow().catch((err) => deps.onFlushError?.(err));
53039
+ return;
53000
53040
  }
53001
- items = parsed[deps.key] ?? [];
53041
+ if (typeof found === "number" && found > version2) {
53042
+ throw new Error(
53043
+ `${deps.key}.json \u7531\u66F4\u65B0\u7248\u672C\u7684 clawd \u5199\u5165\uFF08\u6587\u4EF6 version=${found}\uFF0C\u672C\u673A\u53EA\u8BA4\u5230 ${version2}\uFF09\uFF0C\u8BF7\u5347\u7EA7 clawd`
53044
+ );
53045
+ }
53046
+ throw new Error(
53047
+ `${deps.key}.json: unsupported version ${String(found)}\uFF08\u672C\u673A version=${version2}\uFF09`
53048
+ );
53002
53049
  },
53003
53050
  scheduleFlush() {
53004
53051
  if (flushTimer) return;
@@ -53012,7 +53059,31 @@ function createJsonStore(deps) {
53012
53059
 
53013
53060
  // src/channel/store.ts
53014
53061
  function createChannelStore(deps) {
53015
- const file = createJsonStore({ ...deps, key: "channels" });
53062
+ const file = createJsonStore({
53063
+ ...deps,
53064
+ key: "channels",
53065
+ version: 2,
53066
+ // v1 → v2(2026-08-26):成员表换形,本机与远程 persona 并进一张 memberPersonas。
53067
+ // memberContactDeviceIds 原样留着 —— 它的语义从「可派活给这台设备上的任意 persona」
53068
+ // 收窄成「可发 DM」,收窄不需要动数据。
53069
+ //
53070
+ // 存量频道因此失去跨设备派活能力,这是有意的:要补偿就得在这里去拉对方 daemon 的
53071
+ // persona 列表(对方不一定在线),而「他家所有 persona 自动进名单」正是要废掉的
53072
+ // 那个隐含推导。老板重新勾一次即可。
53073
+ //
53074
+ // ⚠️ 写法注意(给下一次 v2→v3 的人):这里是**先解构后覆盖** —— `...rest` 会把
53075
+ // 旧行里的同名字段一起带进来,紧跟着的赋值再盖掉它。加新字段时确认清楚是想覆盖
53076
+ // 还是想保留,别无意中把 rest 里已有的值清空。
53077
+ migrate: (rows) => rows.map((c) => {
53078
+ const { memberPersonaIds, ...rest } = c;
53079
+ return {
53080
+ ...rest,
53081
+ memberPersonas: (memberPersonaIds ?? []).map((personaId2) => ({
53082
+ personaId: personaId2
53083
+ }))
53084
+ };
53085
+ })
53086
+ });
53016
53087
  const get = (id) => file.all().find((c) => c.id === id);
53017
53088
  return {
53018
53089
  load: file.load,
@@ -53024,7 +53095,7 @@ function createChannelStore(deps) {
53024
53095
  const c = {
53025
53096
  id: deps.genId(),
53026
53097
  name: input.name,
53027
- memberPersonaIds: input.memberPersonaIds,
53098
+ memberPersonas: input.memberPersonas,
53028
53099
  memberContactDeviceIds: input.memberContactDeviceIds,
53029
53100
  createdAt: iso,
53030
53101
  updatedAt: iso
@@ -53037,7 +53108,7 @@ function createChannelStore(deps) {
53037
53108
  const c = get(id);
53038
53109
  if (!c) return void 0;
53039
53110
  if (patch.name !== void 0) c.name = patch.name;
53040
- if (patch.memberPersonaIds !== void 0) c.memberPersonaIds = patch.memberPersonaIds;
53111
+ if (patch.memberPersonas !== void 0) c.memberPersonas = patch.memberPersonas;
53041
53112
  if (patch.memberContactDeviceIds !== void 0) {
53042
53113
  c.memberContactDeviceIds = patch.memberContactDeviceIds;
53043
53114
  }
@@ -54848,9 +54919,11 @@ function buildPersonaDispatchHandlers(deps) {
54848
54919
  if (!members) {
54849
54920
  throw new ClawdError(ERROR_CODES.INVALID_PARAM, `topic not found: ${topicId}`);
54850
54921
  }
54851
- const who = args.targetDeviceId ?? targetPersona;
54852
- const isMember = args.targetDeviceId ? members.deviceIds.includes(args.targetDeviceId) : members.personaIds.includes(targetPersona);
54922
+ const isMember = members.personas.some(
54923
+ (m2) => m2.personaId === targetPersona && m2.deviceId === args.targetDeviceId
54924
+ );
54853
54925
  if (!isMember) {
54926
+ const who = args.targetDeviceId ? `${targetPersona}@${args.targetDeviceId}` : targetPersona;
54854
54927
  throw new ClawdError(
54855
54928
  ERROR_CODES.INVALID_PARAM,
54856
54929
  `${who} is not a member of the channel behind topic ${topicId} \u2014\u2014 \u5148\u628A\u5B83\u52A0\u8FDB\u9891\u9053\u518D\u6D3E\u6D3B`
@@ -62305,7 +62378,7 @@ function computeMethodAccess(args) {
62305
62378
  }
62306
62379
 
62307
62380
  // src/version.ts
62308
- var version = "0.2.410".length > 0 ? "0.2.410" : "dev";
62381
+ var version = "0.2.412".length > 0 ? "0.2.412" : "dev";
62309
62382
 
62310
62383
  // src/cli-probe/probe.ts
62311
62384
  var fs66 = __toESM(require("fs"), 1);
@@ -64456,8 +64529,8 @@ function buildShiftHandlers(deps) {
64456
64529
 
64457
64530
  // src/handlers/channel.ts
64458
64531
  init_protocol();
64459
- function withMaster(ids) {
64460
- return ids.includes(MASTER_PERSONA_ID) ? ids : [MASTER_PERSONA_ID, ...ids];
64532
+ function withMaster(members) {
64533
+ return members.some((m2) => !m2.deviceId && m2.personaId === MASTER_PERSONA_ID) ? members : [{ personaId: MASTER_PERSONA_ID }, ...members];
64461
64534
  }
64462
64535
  function buildChannelHandlers(deps) {
64463
64536
  return {
@@ -64468,7 +64541,7 @@ function buildChannelHandlers(deps) {
64468
64541
  const args = ChannelCreateArgsSchema.parse(frame);
64469
64542
  const channel = deps.channelStore.create({
64470
64543
  name: args.name,
64471
- memberPersonaIds: withMaster(args.memberPersonaIds),
64544
+ memberPersonas: withMaster(args.memberPersonas),
64472
64545
  memberContactDeviceIds: args.memberContactDeviceIds
64473
64546
  });
64474
64547
  return { response: ChannelResultSchema.parse({ channel }) };
@@ -64478,7 +64551,7 @@ function buildChannelHandlers(deps) {
64478
64551
  const channel = deps.channelStore.update(args.channelId, {
64479
64552
  ...args.name !== void 0 ? { name: args.name } : {},
64480
64553
  // 改名单同样过 withMaster:编辑路径把 Master 摘掉,效果跟建一个没 Master 的频道一样
64481
- ...args.memberPersonaIds !== void 0 ? { memberPersonaIds: withMaster(args.memberPersonaIds) } : {},
64554
+ ...args.memberPersonas !== void 0 ? { memberPersonas: withMaster(args.memberPersonas) } : {},
64482
64555
  ...args.memberContactDeviceIds !== void 0 ? { memberContactDeviceIds: args.memberContactDeviceIds } : {}
64483
64556
  });
64484
64557
  if (!channel) {
@@ -67085,14 +67158,17 @@ async function startDaemon(config) {
67085
67158
  const dispatchHandlers = buildPersonaDispatchHandlers({
67086
67159
  personaDispatchManager,
67087
67160
  store: dispatchStore,
67088
- // 话题工作线的派活边界:topicId → 它所属频道的两张成员名单。频道 / 话题任一不存在
67089
- // 都返回 undefined,由 handler 判成「话题不存在」拒掉(编 topicId 绕不过去)。
67161
+ // 话题工作线的派活边界:topicId → 它所属频道的 persona 成员名单。频道 / 话题任一
67162
+ // 不存在都返回 undefined,由 handler 判成「话题不存在」拒掉(编 topicId 绕不过去)。
67163
+ //
67164
+ // 只给 persona 名单:memberContactDeviceIds 是「可发 DM」的人类名单,不参与派活判定。
67165
+ // 在这里一并带上它,下一个读代码的人会以为它也是闸的一部分(P1 接 DM 时再按需要加)。
67090
67166
  getTopicMembership: (topicId) => {
67091
67167
  const t = topicStore.get(topicId);
67092
67168
  if (!t) return void 0;
67093
67169
  const ch = channelStore.get(t.channelId);
67094
67170
  if (!ch) return void 0;
67095
- return { personaIds: [...ch.memberPersonaIds], deviceIds: [...ch.memberContactDeviceIds] };
67171
+ return { personas: [...ch.memberPersonas] };
67096
67172
  },
67097
67173
  // 续派(spec 2026-07-21):任务包注入 worker session,复用投递同款注入路径。
67098
67174
  sendFollowUp: (workerSessionId, text, meta) => manager.injectIntoSession(workerSessionId, text, meta),
@@ -21832,13 +21832,17 @@ var ShiftInternalHistoryArgs = external_exports.object({
21832
21832
  });
21833
21833
 
21834
21834
  // ../protocol/src/channel.ts
21835
+ var ChannelMemberPersonaSchema = external_exports.object({
21836
+ personaId: external_exports.string().min(1),
21837
+ deviceId: external_exports.string().min(1).optional()
21838
+ });
21835
21839
  var ChannelSchema = external_exports.object({
21836
21840
  id: external_exports.string().min(1),
21837
21841
  name: external_exports.string().min(1),
21838
- /** 该频道里可被派活的 persona */
21839
- memberPersonaIds: external_exports.array(external_exports.string().min(1)),
21840
- /** 人类成员(contact deviceId)。跨设备派活的成员边界认这张名单(personaDispatch 校验),
21841
- * P1 人类异步询问也用它 */
21842
+ /** 该频道里可被派活的 persona 全集(本机 + 远程) */
21843
+ memberPersonas: external_exports.array(ChannelMemberPersonaSchema),
21844
+ /** 人类成员(contact deviceId):可发 DM。**不隐含任何派活权限** —— 跨设备派活认的是
21845
+ * 上面那张 memberPersonas。P1 人类异步询问也用它 */
21842
21846
  memberContactDeviceIds: external_exports.array(external_exports.string().min(1)),
21843
21847
  createdAt: external_exports.string().min(1),
21844
21848
  updatedAt: external_exports.string().min(1)
@@ -21846,13 +21850,13 @@ var ChannelSchema = external_exports.object({
21846
21850
  var nonBlank = external_exports.string().trim().min(1);
21847
21851
  var ChannelCreateArgsSchema = external_exports.object({
21848
21852
  name: nonBlank,
21849
- memberPersonaIds: external_exports.array(external_exports.string().min(1)).default([]),
21853
+ memberPersonas: external_exports.array(ChannelMemberPersonaSchema).default([]),
21850
21854
  memberContactDeviceIds: external_exports.array(external_exports.string().min(1)).default([])
21851
21855
  });
21852
21856
  var ChannelUpdateArgsSchema = external_exports.object({
21853
21857
  channelId: external_exports.string().min(1),
21854
21858
  name: nonBlank.optional(),
21855
- memberPersonaIds: external_exports.array(external_exports.string().min(1)).optional(),
21859
+ memberPersonas: external_exports.array(ChannelMemberPersonaSchema).optional(),
21856
21860
  memberContactDeviceIds: external_exports.array(external_exports.string().min(1)).optional()
21857
21861
  });
21858
21862
  var ChannelDeleteArgsSchema = external_exports.object({ channelId: external_exports.string().min(1) });
@@ -62,10 +62,18 @@ watcher 有真工具——它会去 worker 的工作目录跑全量测试、导
62
62
  `meta.lineId` 认领),老板在话题工作区也看不到它。
63
63
 
64
64
  **只能派给本频道的成员。** 派活对象不在频道名单里,daemon 直接拒(`not a member of the channel`)——
65
- 频道成员就是这个话题的参与方边界,不是一张摆设名单。名单在 `channel:list` 的
66
- `memberPersonaIds`(本机 persona)与 `memberContactDeviceIds`(联系人设备,跨设备派活认它),
67
- 话题的 `channelId` 从 `topic:get` 拿。缺人手就**停下问老板**要不要把某个 persona 加进频道,
68
- 不要绕道派给名单外的 persona。
65
+ 频道成员就是这个话题的参与方边界,不是一张摆设名单。话题的 `channelId` 从 `topic:get` 拿,
66
+ 名单在 `channel:list` 的两个字段里,**它们不是一回事**:
67
+
68
+ - `memberPersonas` —— 可派活的 persona **全集**。每项形如 `{ personaId, deviceId? }`:没有
69
+ `deviceId` 就是本机 persona,有 `deviceId` 就是那台联系人设备上公开给老板的 persona。
70
+ 派它时把这两个值原样填进 `personaDispatch` 的 `targetPersona` / `targetDeviceId`
71
+ - `memberContactDeviceIds` —— **人类**成员,只表示「可以给这个人发 DM」
72
+
73
+ **联系人在名单里不等于他家的 persona 能派。** 要派某台设备上的 persona,那个 persona 必须
74
+ 自己出现在 `memberPersonas` 里(连同它的 `deviceId`)。名单外的一律拒。
75
+
76
+ 缺人手就**停下问老板**要不要把某个 persona 加进频道,不要绕道派给名单外的 persona。
69
77
 
70
78
  打回次数**不要**自己往 `meta` 里记 —— daemon 在 `topic:reject` / `topic:accept` 里数,
71
79
  你手记一份只会跟它漂。拿到 `dispatchId` 后立刻落账(见「落账纪律」)。