@clawos-dev/clawd 0.2.56-beta.90.047fd4f → 0.2.56-beta.91.df217af

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.
Files changed (2) hide show
  1. package/dist/cli.cjs +430 -188
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -4404,6 +4404,10 @@ var init_methods = __esm({
4404
4404
  "persona:issueToken",
4405
4405
  "persona:revokeToken",
4406
4406
  "persona:listSubSessions",
4407
+ // owner-side bootstrap RPC:拉 (personaId, token) tuple 下所有 listener sub-session
4408
+ // 元数据。后续增量靠 persona:listenerChat:* push 维护,本 RPC 仅在 owner ws connect /
4409
+ // reconnect 时一次性补齐 cache。
4410
+ "persona:listListenerChatsForToken",
4407
4411
  "persona:appendOwnerMessage",
4408
4412
  "info",
4409
4413
  "ping"
@@ -8595,7 +8599,7 @@ var init_zod = __esm({
8595
8599
  });
8596
8600
 
8597
8601
  // ../protocol/src/persona-schemas.ts
8598
- var PersonaTokenEntrySchema, PersonaFileSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, PersonaIssueTokenArgsSchema, PersonaRevokeTokenArgsSchema, PersonaAppendOwnerMessageArgsSchema, ChatSummarySchema, ChatListRequestSchema, ChatListResponseSchema, ChatRenameRequestSchema, ChatRenameResponseSchema, ChatDeleteRequestSchema, ChatDeleteResponseSchema;
8602
+ var PersonaTokenEntrySchema, PersonaFileSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, PersonaIssueTokenArgsSchema, PersonaRevokeTokenArgsSchema, PersonaAppendOwnerMessageArgsSchema, FRAME_TYPE_CHAT_OPEN, FRAME_TYPE_CHAT_RENAME, FRAME_TYPE_CHAT_DELETE, FRAME_TYPE_CHAT_LIST, FRAME_TYPE_CHAT_CREATED, FRAME_TYPE_CHAT_RENAMED, FRAME_TYPE_CHAT_DELETED, FRAME_TYPE_PERSONA_LISTENER_CHAT_CREATED, FRAME_TYPE_PERSONA_LISTENER_CHAT_RENAMED, FRAME_TYPE_PERSONA_LISTENER_CHAT_DELETED, FRAME_TYPE_PERSONA_LISTENER_STATUS, ChatSummarySchema, ChatOpenRequestSchema, ChatRenameRequestSchema, ChatDeleteRequestSchema, ChatRenameResponseSchema, ChatDeleteResponseSchema, ChatListPushSchema, ChatCreatedFrameSchema, ChatRenamedFrameSchema, ChatDeletedFrameSchema, PersonaListenerChatCreatedFrameSchema, PersonaListenerChatRenamedFrameSchema, PersonaListenerChatDeletedFrameSchema, PersonaListenerStatusFrameSchema, PersonaListListenerChatsForTokenArgsSchema, PersonaListListenerChatsForTokenResponseSchema;
8599
8603
  var init_persona_schemas = __esm({
8600
8604
  "../protocol/src/persona-schemas.ts"() {
8601
8605
  "use strict";
@@ -8654,47 +8658,116 @@ var init_persona_schemas = __esm({
8654
8658
  subSessionId: external_exports.string().min(1),
8655
8659
  text: external_exports.string().min(1)
8656
8660
  });
8661
+ FRAME_TYPE_CHAT_OPEN = "chat:open";
8662
+ FRAME_TYPE_CHAT_RENAME = "chat:rename";
8663
+ FRAME_TYPE_CHAT_DELETE = "chat:delete";
8664
+ FRAME_TYPE_CHAT_LIST = "chat:list";
8665
+ FRAME_TYPE_CHAT_CREATED = "chat:created";
8666
+ FRAME_TYPE_CHAT_RENAMED = "chat:renamed";
8667
+ FRAME_TYPE_CHAT_DELETED = "chat:deleted";
8668
+ FRAME_TYPE_PERSONA_LISTENER_CHAT_CREATED = "persona:listenerChat:created";
8669
+ FRAME_TYPE_PERSONA_LISTENER_CHAT_RENAMED = "persona:listenerChat:renamed";
8670
+ FRAME_TYPE_PERSONA_LISTENER_CHAT_DELETED = "persona:listenerChat:deleted";
8671
+ FRAME_TYPE_PERSONA_LISTENER_STATUS = "persona:listenerStatus";
8657
8672
  ChatSummarySchema = external_exports.object({
8658
8673
  sessionId: external_exports.string().min(1),
8659
- /** 派生 sessionId 时使用的原始 chatId('default' root chat 保留值) */
8674
+ /** 派生 sessionId 时使用的原始 chatId(永远三元组 hash 派生,无 default 特例) */
8660
8675
  chatId: external_exports.string().min(1),
8661
8676
  /** sub-session label,listener 视为 chat 名 */
8662
8677
  label: external_exports.string(),
8663
8678
  /** SessionFile.createdAt(ISO string) */
8664
- createdAt: external_exports.string(),
8665
- /** 是否为 default chat(sessionId 等于 ${personaId}-${tokenHash12} 时为 true) */
8666
- isDefault: external_exports.boolean()
8679
+ createdAt: external_exports.string()
8667
8680
  });
8668
- ChatListRequestSchema = external_exports.object({
8669
- type: external_exports.literal("chat:list"),
8681
+ ChatOpenRequestSchema = external_exports.object({
8682
+ type: external_exports.literal(FRAME_TYPE_CHAT_OPEN),
8683
+ /** 客户端生成的 chatId(uuid 推荐);daemon 用三元组派生 sub-session sessionId */
8684
+ chatId: external_exports.string().min(1),
8685
+ /** 不存在时是否自动创建。true = 创建(典型:新建对话);false/缺省 = 命中已有,否则 NOT_FOUND */
8686
+ autoCreate: external_exports.boolean().optional(),
8670
8687
  requestId: external_exports.string().min(1).optional()
8671
8688
  });
8672
- ChatListResponseSchema = external_exports.object({
8673
- type: external_exports.literal("chat:list"),
8674
- requestId: external_exports.string().min(1).optional(),
8675
- chats: external_exports.array(ChatSummarySchema)
8676
- });
8677
8689
  ChatRenameRequestSchema = external_exports.object({
8678
- type: external_exports.literal("chat:rename"),
8690
+ type: external_exports.literal(FRAME_TYPE_CHAT_RENAME),
8679
8691
  requestId: external_exports.string().min(1).optional(),
8680
8692
  sessionId: external_exports.string().min(1),
8681
8693
  label: external_exports.string().min(1)
8682
8694
  });
8683
- ChatRenameResponseSchema = external_exports.object({
8684
- type: external_exports.literal("chat:rename"),
8685
- requestId: external_exports.string().min(1).optional(),
8686
- ok: external_exports.literal(true)
8687
- });
8688
8695
  ChatDeleteRequestSchema = external_exports.object({
8689
- type: external_exports.literal("chat:delete"),
8696
+ type: external_exports.literal(FRAME_TYPE_CHAT_DELETE),
8690
8697
  requestId: external_exports.string().min(1).optional(),
8691
8698
  sessionId: external_exports.string().min(1)
8692
8699
  });
8700
+ ChatRenameResponseSchema = external_exports.object({
8701
+ type: external_exports.literal(FRAME_TYPE_CHAT_RENAME),
8702
+ requestId: external_exports.string().min(1).optional(),
8703
+ ok: external_exports.literal(true)
8704
+ });
8693
8705
  ChatDeleteResponseSchema = external_exports.object({
8694
- type: external_exports.literal("chat:delete"),
8706
+ type: external_exports.literal(FRAME_TYPE_CHAT_DELETE),
8695
8707
  requestId: external_exports.string().min(1).optional(),
8696
8708
  ok: external_exports.literal(true)
8697
8709
  });
8710
+ ChatListPushSchema = external_exports.object({
8711
+ type: external_exports.literal(FRAME_TYPE_CHAT_LIST),
8712
+ chats: external_exports.array(ChatSummarySchema)
8713
+ });
8714
+ ChatCreatedFrameSchema = external_exports.object({
8715
+ type: external_exports.literal(FRAME_TYPE_CHAT_CREATED),
8716
+ sessionId: external_exports.string().min(1),
8717
+ chatId: external_exports.string().min(1),
8718
+ label: external_exports.string().optional(),
8719
+ /** ISO timestamp */
8720
+ createdAt: external_exports.string().min(1)
8721
+ });
8722
+ ChatRenamedFrameSchema = external_exports.object({
8723
+ type: external_exports.literal(FRAME_TYPE_CHAT_RENAMED),
8724
+ sessionId: external_exports.string().min(1),
8725
+ label: external_exports.string().min(1)
8726
+ });
8727
+ ChatDeletedFrameSchema = external_exports.object({
8728
+ type: external_exports.literal(FRAME_TYPE_CHAT_DELETED),
8729
+ sessionId: external_exports.string().min(1)
8730
+ });
8731
+ PersonaListenerChatCreatedFrameSchema = external_exports.object({
8732
+ type: external_exports.literal(FRAME_TYPE_PERSONA_LISTENER_CHAT_CREATED),
8733
+ personaId: external_exports.string().min(1),
8734
+ /** owner 同时持有 token 字符串:sidebar 已经按 token 维度渲染 listener leaf,
8735
+ * push 帧用 token 直接定位归属 leaf。token 不是密钥,owner 本来就持有。 */
8736
+ token: external_exports.string().min(1),
8737
+ sessionId: external_exports.string().min(1),
8738
+ chatId: external_exports.string().min(1),
8739
+ label: external_exports.string().optional(),
8740
+ createdAt: external_exports.string().min(1)
8741
+ });
8742
+ PersonaListenerChatRenamedFrameSchema = external_exports.object({
8743
+ type: external_exports.literal(FRAME_TYPE_PERSONA_LISTENER_CHAT_RENAMED),
8744
+ personaId: external_exports.string().min(1),
8745
+ token: external_exports.string().min(1),
8746
+ sessionId: external_exports.string().min(1),
8747
+ label: external_exports.string().min(1)
8748
+ });
8749
+ PersonaListenerChatDeletedFrameSchema = external_exports.object({
8750
+ type: external_exports.literal(FRAME_TYPE_PERSONA_LISTENER_CHAT_DELETED),
8751
+ personaId: external_exports.string().min(1),
8752
+ token: external_exports.string().min(1),
8753
+ sessionId: external_exports.string().min(1),
8754
+ /** 原始 chatId(用于 owner UI 比对 personaView.chatId 清理 stale selection) */
8755
+ chatId: external_exports.string().min(1)
8756
+ });
8757
+ PersonaListenerStatusFrameSchema = external_exports.object({
8758
+ type: external_exports.literal(FRAME_TYPE_PERSONA_LISTENER_STATUS),
8759
+ personaId: external_exports.string().min(1),
8760
+ token: external_exports.string().min(1),
8761
+ status: external_exports.enum(["online", "offline"]),
8762
+ activeWsCount: external_exports.number().int().nonnegative()
8763
+ });
8764
+ PersonaListListenerChatsForTokenArgsSchema = external_exports.object({
8765
+ personaId: external_exports.string().min(1),
8766
+ token: external_exports.string().min(1)
8767
+ });
8768
+ PersonaListListenerChatsForTokenResponseSchema = external_exports.object({
8769
+ chats: external_exports.array(ChatSummarySchema)
8770
+ });
8698
8771
  }
8699
8772
  });
8700
8773
 
@@ -8798,9 +8871,10 @@ var init_schemas = __esm({
8798
8871
  // owner-mode persona session 身份标识;UI 用它在 SessionList 过滤 + jump,
8799
8872
  // daemon 用它做 idempotent dedupe + spawn 时派生 ctx.personaMode='owner'
8800
8873
  ownerPersonaId: external_exports.string().min(1).optional(),
8801
- // listener-mode sub-session 的原始 chatId(owner-mode 不设;旧数据缺省)
8802
- // alice chat:list 拿到 chatId 后用它发 auth 帧重连同一 sub-session;
8803
- // daemon 派生 sessionId 是单向 hash,必须保留原始 chatId 才能让 alice 重建连接
8874
+ // listener-mode sub-session 的原始 chatId(owner-mode 不写;listener-scope 强制写入)。
8875
+ // 派生 sessionId 是单向 hash `${personaId}-${tokenHash12}-${chatHash8}`,必须保留原始 chatId
8876
+ // 才能让 alice 重连同一 sub-session(持久化在 alice localStorage 之外,由 daemon push 同步)。
8877
+ // optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
8804
8878
  chatId: external_exports.string().min(1).optional(),
8805
8879
  createdAt: external_exports.string().min(1),
8806
8880
  updatedAt: external_exports.string().min(1)
@@ -9233,8 +9307,7 @@ var init_schemas = __esm({
9233
9307
  AuthRequestFrameSchema = external_exports.object({
9234
9308
  type: external_exports.literal("auth"),
9235
9309
  token: external_exports.string().min(1),
9236
- scheme: external_exports.literal("bearer").optional(),
9237
- chatId: external_exports.string().optional()
9310
+ scheme: external_exports.literal("bearer").optional()
9238
9311
  });
9239
9312
  AuthOkFrameSchema = external_exports.object({
9240
9313
  type: external_exports.literal("auth:ok"),
@@ -17989,16 +18062,21 @@ var PersonaManager = class {
17989
18062
  return updated;
17990
18063
  }
17991
18064
  /**
17992
- * 拿到(或按需创建)该 token 对应的 sub-session。subSessionId 由 personaId + token 哈希派生
17993
- * 保证 idempotent:同一 token 永远复用同一个 sub-session。
18065
+ * 拿到(或按需创建)该 (token, chatId) 对应的 sub-session。subSessionId 由 personaId + token +
18066
+ * chatId 三元组 hash 派生,保证 idempotent:同一 tuple 永远复用同一个 sub-session。
18067
+ *
18068
+ * chatId 必填 —— listener 协议从 v2 起拆 phase:客户端 auth 后通过 chat:open(chatId, autoCreate?)
18069
+ * 显式进入某个 chat;不再有 'default' 默认值。chatId 通常是客户端生成的 uuid,落 listener 端
18070
+ * 持久化由客户端负责(daemon 不替客户端记忆"最近哪个 chat")。
18071
+ *
17994
18072
  * 创建路径:开启 idleKillEnabled,cwd 取 persona 目录,不再硬编码 permission mode / tool allowlist
17995
18073
  * (sandbox 约束移到 persona dir 下的 .claude/settings.json,由 OS-level Seatbelt 执行)
17996
18074
  */
17997
18075
  getOrCreateSubSession(personaId, token, chatId) {
17998
18076
  const persona = this.deps.registry.get(personaId);
17999
18077
  if (!persona) throw new Error(`persona not found: ${personaId}`);
18000
- const normalizedChatId = chatId && chatId.length > 0 ? chatId : "default";
18001
- const subSessionId = this.deriveSubSessionId(personaId, token, normalizedChatId);
18078
+ if (!chatId) throw new Error("chatId is required");
18079
+ const subSessionId = this.deriveSubSessionId(personaId, token, chatId);
18002
18080
  const scope = { kind: "persona", personaId, mode: "listener" };
18003
18081
  const existing = this.deps.sessionManager.readForScope(subSessionId, scope);
18004
18082
  if (existing) {
@@ -18013,10 +18091,28 @@ var PersonaManager = class {
18013
18091
  tool: "claude",
18014
18092
  label: subLabel,
18015
18093
  model: persona.model,
18016
- chatId: normalizedChatId
18094
+ chatId
18017
18095
  });
18018
18096
  return { sessionFile, isNew: true };
18019
18097
  }
18098
+ /**
18099
+ * 检查 (token, chatId) 对应的 sub-session 是否存在;不存在返回 null。
18100
+ * chat:open(autoCreate=false) 路径用:listener 想"只切已有 chat"时不能撞 auto-create。
18101
+ */
18102
+ getSubSession(personaId, token, chatId) {
18103
+ if (!chatId) return null;
18104
+ const subSessionId = this.deriveSubSessionId(personaId, token, chatId);
18105
+ const scope = { kind: "persona", personaId, mode: "listener" };
18106
+ return this.deps.sessionManager.readForScope(subSessionId, scope);
18107
+ }
18108
+ /**
18109
+ * 校验 sessionId 是否属于 (personaId, token) tuple(前 12 字符 tokenHash 一致)。
18110
+ * chat:rename / chat:delete 用:listener 只能操作自己 tuple 内的 chat。
18111
+ */
18112
+ tokenPrefixFor(personaId, token) {
18113
+ const tokenHash = import_node_crypto3.default.createHash("sha256").update(token).digest("hex").slice(0, 12);
18114
+ return `${personaId}-${tokenHash}`;
18115
+ }
18020
18116
  /**
18021
18117
  * 老板插话:把"老板的话"作为 meta-text + metaSource='owner' 推到 sub-session 的事件流。
18022
18118
  * 委托 SessionManager.injectOwnerMessage 路由到 reducer 'inject-owner-text' input
@@ -18045,13 +18141,11 @@ var PersonaManager = class {
18045
18141
  throw new Error(`failed to generate unique personaId for label=${label}`);
18046
18142
  }
18047
18143
  /**
18048
- * subSessionId 派生:
18049
- * - chatId='default' 二元组 `${personaId}-${tokenHash12}` (root 兼容,旧路径不变)
18050
- * - chatId 非 default → 三元组 `${personaId}-${tokenHash12}-${chatHash8}`
18144
+ * subSessionId 派生:永远三元组 `${personaId}-${tokenHash12}-${chatHash8}`,无 default 特例。
18145
+ * v1 实现保留 default 二元组兼容已废除。
18051
18146
  */
18052
18147
  deriveSubSessionId(personaId, token, chatId) {
18053
18148
  const tokenHash = import_node_crypto3.default.createHash("sha256").update(token).digest("hex").slice(0, 12);
18054
- if (chatId === "default") return `${personaId}-${tokenHash}`;
18055
18149
  const chatHash = import_node_crypto3.default.createHash("sha256").update(chatId).digest("hex").slice(0, 8);
18056
18150
  return `${personaId}-${tokenHash}-${chatHash}`;
18057
18151
  }
@@ -19180,9 +19274,14 @@ var PersonaBoundHandler = class {
19180
19274
  this.deps = deps;
19181
19275
  }
19182
19276
  deps;
19277
+ // tuple fan-out 表:tokenPrefix → 同 tuple 所有 listener ws 的 send fn 集合
19278
+ tupleSubscribers = /* @__PURE__ */ new Map();
19279
+ // tuple → 当前活跃 ws 数;从 0→1 emit online;从 1→0 emit offline
19280
+ listenerWsCount = /* @__PURE__ */ new Map();
19183
19281
  handle(ws, personaId) {
19282
+ let phase = "pre-auth";
19184
19283
  let scope = null;
19185
- let unsubscribe = null;
19284
+ let unsubscribeSession = null;
19186
19285
  const send = (frame) => {
19187
19286
  try {
19188
19287
  ws.send(JSON.stringify(frame));
@@ -19195,6 +19294,39 @@ var PersonaBoundHandler = class {
19195
19294
  if (requestId) errFrame.requestId = requestId;
19196
19295
  send(errFrame);
19197
19296
  };
19297
+ const cleanupTupleRegistration = () => {
19298
+ if (!scope) return;
19299
+ const subs = this.tupleSubscribers.get(scope.tokenPrefix);
19300
+ if (subs) {
19301
+ subs.delete(send);
19302
+ if (subs.size === 0) this.tupleSubscribers.delete(scope.tokenPrefix);
19303
+ }
19304
+ const before = this.listenerWsCount.get(scope.tokenPrefix) ?? 0;
19305
+ const after = before - 1;
19306
+ if (after <= 0) {
19307
+ this.listenerWsCount.delete(scope.tokenPrefix);
19308
+ this.deps.ownerBroadcast(
19309
+ PersonaListenerStatusFrameSchema.parse({
19310
+ type: FRAME_TYPE_PERSONA_LISTENER_STATUS,
19311
+ personaId: scope.personaId,
19312
+ token: scope.token,
19313
+ status: "offline",
19314
+ activeWsCount: 0
19315
+ })
19316
+ );
19317
+ } else {
19318
+ this.listenerWsCount.set(scope.tokenPrefix, after);
19319
+ this.deps.ownerBroadcast(
19320
+ PersonaListenerStatusFrameSchema.parse({
19321
+ type: FRAME_TYPE_PERSONA_LISTENER_STATUS,
19322
+ personaId: scope.personaId,
19323
+ token: scope.token,
19324
+ status: "online",
19325
+ activeWsCount: after
19326
+ })
19327
+ );
19328
+ }
19329
+ };
19198
19330
  ws.on("message", (raw) => {
19199
19331
  let parsedRaw;
19200
19332
  try {
@@ -19208,7 +19340,7 @@ var PersonaBoundHandler = class {
19208
19340
  return;
19209
19341
  }
19210
19342
  const frame = parsedRaw;
19211
- if (!scope) {
19343
+ if (phase === "pre-auth") {
19212
19344
  const authParse = AuthRequestFrameSchema.safeParse(frame);
19213
19345
  if (!authParse.success) {
19214
19346
  ws.close(4400, "PROTOCOL_VIOLATION");
@@ -19226,30 +19358,33 @@ var PersonaBoundHandler = class {
19226
19358
  ws.close(4404, "PERSONA_DELETED");
19227
19359
  return;
19228
19360
  }
19229
- let subSessionFile;
19230
- try {
19231
- const { sessionFile } = this.deps.personaManager.getOrCreateSubSession(
19361
+ const tokenPrefix = this.deps.personaManager.tokenPrefixFor(personaId, token);
19362
+ scope = { personaId, token, tokenPrefix };
19363
+ phase = "tuple-watch";
19364
+ let subs = this.tupleSubscribers.get(tokenPrefix);
19365
+ if (!subs) {
19366
+ subs = /* @__PURE__ */ new Set();
19367
+ this.tupleSubscribers.set(tokenPrefix, subs);
19368
+ }
19369
+ subs.add(send);
19370
+ const nextCount = (this.listenerWsCount.get(tokenPrefix) ?? 0) + 1;
19371
+ this.listenerWsCount.set(tokenPrefix, nextCount);
19372
+ this.deps.ownerBroadcast(
19373
+ PersonaListenerStatusFrameSchema.parse({
19374
+ type: FRAME_TYPE_PERSONA_LISTENER_STATUS,
19232
19375
  personaId,
19233
19376
  token,
19234
- authParse.data.chatId
19235
- );
19236
- subSessionFile = sessionFile;
19237
- } catch (err) {
19238
- this.deps.logger.warn(
19239
- `persona getOrCreateSubSession failed: ${err.message}`
19240
- );
19241
- ws.close(1011, "INTERNAL");
19242
- return;
19243
- }
19244
- scope = { personaId, subSessionId: subSessionFile.sessionId };
19377
+ status: "online",
19378
+ activeWsCount: nextCount
19379
+ })
19380
+ );
19245
19381
  send({ type: "auth:ok" });
19246
- send({
19247
- type: "session:info",
19248
- sessionId: subSessionFile.sessionId,
19249
- label: persona.label,
19250
- cwd: this.deps.personaStore.personaDirPath(personaId),
19251
- ...persona.model ? { model: persona.model } : {}
19252
- });
19382
+ send(
19383
+ ChatListPushSchema.parse({
19384
+ type: FRAME_TYPE_CHAT_LIST,
19385
+ chats: this.listChatsForTuple(personaId, tokenPrefix)
19386
+ })
19387
+ );
19253
19388
  return;
19254
19389
  }
19255
19390
  const type = frame.type;
@@ -19262,8 +19397,183 @@ var PersonaBoundHandler = class {
19262
19397
  send({ type: "pong", at: Date.now() });
19263
19398
  return;
19264
19399
  }
19400
+ if (!scope) {
19401
+ ws.close(4400, "PROTOCOL_VIOLATION");
19402
+ return;
19403
+ }
19404
+ if (type === "chat:open") {
19405
+ const parsed = ChatOpenRequestSchema.safeParse(frame);
19406
+ if (!parsed.success) {
19407
+ sendError(requestId, "VALIDATION_ERROR", parsed.error.message);
19408
+ return;
19409
+ }
19410
+ const { chatId, autoCreate } = parsed.data;
19411
+ let sessionFile;
19412
+ let isNew = false;
19413
+ try {
19414
+ if (autoCreate) {
19415
+ const r = this.deps.personaManager.getOrCreateSubSession(scope.personaId, scope.token, chatId);
19416
+ sessionFile = r.sessionFile;
19417
+ isNew = r.isNew;
19418
+ } else {
19419
+ sessionFile = this.deps.personaManager.getSubSession(scope.personaId, scope.token, chatId);
19420
+ if (!sessionFile) {
19421
+ sendError(requestId, "SESSION_NOT_FOUND", `chatId not found: ${chatId}`);
19422
+ return;
19423
+ }
19424
+ }
19425
+ } catch (err) {
19426
+ sendError(requestId, "INTERNAL", err.message);
19427
+ return;
19428
+ }
19429
+ if (unsubscribeSession && scope.activeSubSessionId !== sessionFile.sessionId) {
19430
+ unsubscribeSession();
19431
+ unsubscribeSession = null;
19432
+ }
19433
+ if (!unsubscribeSession) {
19434
+ unsubscribeSession = this.deps.sessionManager.subscribe(
19435
+ sessionFile.sessionId,
19436
+ { kind: "persona", personaId: scope.personaId, mode: "listener" },
19437
+ (eventFrame) => send(eventFrame)
19438
+ );
19439
+ }
19440
+ scope.activeSubSessionId = sessionFile.sessionId;
19441
+ phase = "chat-active";
19442
+ const persona = this.deps.registry.get(scope.personaId);
19443
+ const infoFrame = {
19444
+ type: "session:info",
19445
+ sessionId: sessionFile.sessionId,
19446
+ chatId,
19447
+ label: persona?.label ?? "",
19448
+ cwd: sessionFile.cwd
19449
+ };
19450
+ if (persona?.model) infoFrame.model = persona.model;
19451
+ if (requestId) infoFrame.requestId = requestId;
19452
+ send(infoFrame);
19453
+ send({
19454
+ type: "session:status",
19455
+ sessionId: sessionFile.sessionId,
19456
+ status: this.deps.sessionManager.getCurrentStatus(sessionFile.sessionId)
19457
+ });
19458
+ if (isNew) {
19459
+ const createdBase = {
19460
+ sessionId: sessionFile.sessionId,
19461
+ chatId,
19462
+ label: sessionFile.label,
19463
+ createdAt: sessionFile.createdAt
19464
+ };
19465
+ this.fanoutTuple(
19466
+ scope.tokenPrefix,
19467
+ ChatCreatedFrameSchema.parse({ type: FRAME_TYPE_CHAT_CREATED, ...createdBase })
19468
+ );
19469
+ this.deps.ownerBroadcast(
19470
+ PersonaListenerChatCreatedFrameSchema.parse({
19471
+ type: FRAME_TYPE_PERSONA_LISTENER_CHAT_CREATED,
19472
+ personaId: scope.personaId,
19473
+ token: scope.token,
19474
+ ...createdBase
19475
+ })
19476
+ );
19477
+ }
19478
+ return;
19479
+ }
19480
+ if (type === "chat:rename") {
19481
+ const parsed = ChatRenameRequestSchema.safeParse(frame);
19482
+ if (!parsed.success) {
19483
+ sendError(requestId, "VALIDATION_ERROR", parsed.error.message);
19484
+ return;
19485
+ }
19486
+ const { sessionId: targetId, label } = parsed.data;
19487
+ if (!this.isInTuple(targetId, scope.tokenPrefix)) {
19488
+ sendError(requestId, "FORBIDDEN", "sessionId out of (persona, token) scope");
19489
+ return;
19490
+ }
19491
+ try {
19492
+ this.deps.sessionManager.renameForScope({
19493
+ sessionId: targetId,
19494
+ scope: { kind: "persona", personaId: scope.personaId, mode: "listener" },
19495
+ label
19496
+ });
19497
+ } catch (err) {
19498
+ const e = err;
19499
+ sendError(requestId, e.code ?? "INTERNAL", e.message ?? String(err));
19500
+ return;
19501
+ }
19502
+ if (requestId) send({ type: FRAME_TYPE_CHAT_RENAME, requestId, ok: true });
19503
+ this.fanoutTuple(
19504
+ scope.tokenPrefix,
19505
+ ChatRenamedFrameSchema.parse({ type: FRAME_TYPE_CHAT_RENAMED, sessionId: targetId, label })
19506
+ );
19507
+ this.deps.ownerBroadcast(
19508
+ PersonaListenerChatRenamedFrameSchema.parse({
19509
+ type: FRAME_TYPE_PERSONA_LISTENER_CHAT_RENAMED,
19510
+ personaId: scope.personaId,
19511
+ token: scope.token,
19512
+ sessionId: targetId,
19513
+ label
19514
+ })
19515
+ );
19516
+ return;
19517
+ }
19518
+ if (type === "chat:delete") {
19519
+ const parsed = ChatDeleteRequestSchema.safeParse(frame);
19520
+ if (!parsed.success) {
19521
+ sendError(requestId, "VALIDATION_ERROR", parsed.error.message);
19522
+ return;
19523
+ }
19524
+ const { sessionId: targetId } = parsed.data;
19525
+ if (!this.isInTuple(targetId, scope.tokenPrefix)) {
19526
+ sendError(requestId, "FORBIDDEN", "sessionId out of (persona, token) scope");
19527
+ return;
19528
+ }
19529
+ const before = this.deps.sessionManager.readForScope(targetId, {
19530
+ kind: "persona",
19531
+ personaId: scope.personaId,
19532
+ mode: "listener"
19533
+ });
19534
+ const chatIdForFrame = before?.chatId ?? "unknown";
19535
+ try {
19536
+ this.deps.sessionManager.deleteForScope({
19537
+ sessionId: targetId,
19538
+ scope: { kind: "persona", personaId: scope.personaId, mode: "listener" }
19539
+ });
19540
+ } catch (err) {
19541
+ const e = err;
19542
+ sendError(requestId, e.code ?? "INTERNAL", e.message ?? String(err));
19543
+ return;
19544
+ }
19545
+ if (requestId) send({ type: FRAME_TYPE_CHAT_DELETE, requestId, ok: true });
19546
+ this.fanoutTuple(
19547
+ scope.tokenPrefix,
19548
+ ChatDeletedFrameSchema.parse({ type: FRAME_TYPE_CHAT_DELETED, sessionId: targetId })
19549
+ );
19550
+ this.deps.ownerBroadcast(
19551
+ PersonaListenerChatDeletedFrameSchema.parse({
19552
+ type: FRAME_TYPE_PERSONA_LISTENER_CHAT_DELETED,
19553
+ personaId: scope.personaId,
19554
+ token: scope.token,
19555
+ sessionId: targetId,
19556
+ chatId: chatIdForFrame
19557
+ })
19558
+ );
19559
+ if (scope.activeSubSessionId === targetId) {
19560
+ unsubscribeSession?.();
19561
+ unsubscribeSession = null;
19562
+ scope.activeSubSessionId = void 0;
19563
+ phase = "tuple-watch";
19564
+ }
19565
+ return;
19566
+ }
19567
+ if (phase !== "chat-active" || !scope.activeSubSessionId) {
19568
+ sendError(
19569
+ requestId,
19570
+ "METHOD_NOT_ALLOWED",
19571
+ `${typeof type === "string" ? type : "unknown"} requires chat:open first`
19572
+ );
19573
+ return;
19574
+ }
19265
19575
  const requireScopedSession = () => {
19266
- if (frame.sessionId !== scope.subSessionId) {
19576
+ if (frame.sessionId !== scope.activeSubSessionId) {
19267
19577
  sendError(requestId, "FORBIDDEN", "sessionId out of scope");
19268
19578
  return false;
19269
19579
  }
@@ -19275,33 +19585,6 @@ var PersonaBoundHandler = class {
19275
19585
  mode: "listener"
19276
19586
  });
19277
19587
  switch (type) {
19278
- case "session:subscribe": {
19279
- if (!requireScopedSession()) return;
19280
- if (unsubscribe) unsubscribe();
19281
- unsubscribe = this.deps.sessionManager.subscribe(
19282
- scope.subSessionId,
19283
- listenerScope(),
19284
- (eventFrame) => send(eventFrame)
19285
- );
19286
- if (requestId)
19287
- send({ type: "subscribed", requestId, sessionId: scope.subSessionId });
19288
- const currentStatus = this.deps.sessionManager.getCurrentStatus(scope.subSessionId);
19289
- send({
19290
- type: "session:status",
19291
- sessionId: scope.subSessionId,
19292
- status: currentStatus
19293
- });
19294
- return;
19295
- }
19296
- case "session:unsubscribe": {
19297
- if (!requireScopedSession()) return;
19298
- if (unsubscribe) {
19299
- unsubscribe();
19300
- unsubscribe = null;
19301
- }
19302
- if (requestId) send({ type: "unsubscribed", requestId, sessionId: scope.subSessionId });
19303
- return;
19304
- }
19305
19588
  case "session:send": {
19306
19589
  if (!requireScopedSession()) return;
19307
19590
  const text = frame.text;
@@ -19311,7 +19594,7 @@ var PersonaBoundHandler = class {
19311
19594
  }
19312
19595
  try {
19313
19596
  this.deps.sessionManager.sendForScope({
19314
- sessionId: scope.subSessionId,
19597
+ sessionId: scope.activeSubSessionId,
19315
19598
  scope: listenerScope(),
19316
19599
  text
19317
19600
  });
@@ -19326,10 +19609,11 @@ var PersonaBoundHandler = class {
19326
19609
  if (!requireScopedSession()) return;
19327
19610
  try {
19328
19611
  this.deps.sessionManager.resetForScope({
19329
- sessionId: scope.subSessionId,
19612
+ sessionId: scope.activeSubSessionId,
19330
19613
  scope: listenerScope()
19331
19614
  });
19332
- if (requestId) send({ type: "session:info", sessionId: scope.subSessionId, requestId });
19615
+ if (requestId)
19616
+ send({ type: "session:info", sessionId: scope.activeSubSessionId, requestId });
19333
19617
  } catch (err) {
19334
19618
  const e = err;
19335
19619
  sendError(requestId, e.code ?? "INTERNAL", e.message ?? String(err));
@@ -19342,7 +19626,7 @@ var PersonaBoundHandler = class {
19342
19626
  const offset = typeof frame.offset === "number" && frame.offset >= 0 ? frame.offset : 0;
19343
19627
  try {
19344
19628
  const page = this.deps.sessionManager.readHistoryPageForScope(
19345
- scope.subSessionId,
19629
+ scope.activeSubSessionId,
19346
19630
  listenerScope(),
19347
19631
  limit,
19348
19632
  offset
@@ -19362,94 +19646,6 @@ var PersonaBoundHandler = class {
19362
19646
  }
19363
19647
  return;
19364
19648
  }
19365
- case "chat:list": {
19366
- const tokenPart = scope.subSessionId.slice(
19367
- scope.personaId.length + 1,
19368
- scope.personaId.length + 1 + 12
19369
- );
19370
- const tokenPrefix = `${scope.personaId}-${tokenPart}`;
19371
- try {
19372
- const all = this.deps.sessionManager.listPersonaSessions(scope.personaId, "listener");
19373
- const sameTuple = all.filter(
19374
- (s) => s.sessionId === tokenPrefix || s.sessionId.startsWith(`${tokenPrefix}-`)
19375
- );
19376
- const chats = sameTuple.map((s) => ({
19377
- sessionId: s.sessionId,
19378
- chatId: s.chatId ?? (s.sessionId === tokenPrefix ? "default" : "unknown"),
19379
- label: s.label ?? "",
19380
- createdAt: s.createdAt,
19381
- isDefault: s.sessionId === tokenPrefix
19382
- }));
19383
- if (requestId) send({ type: "chat:list", requestId, chats });
19384
- else send({ type: "chat:list", chats });
19385
- } catch (err) {
19386
- const e = err;
19387
- sendError(requestId, e.code ?? "INTERNAL", e.message ?? String(err));
19388
- }
19389
- return;
19390
- }
19391
- case "chat:rename": {
19392
- const targetId = frame.sessionId;
19393
- const newLabel = frame.label;
19394
- if (typeof targetId !== "string" || targetId.length === 0) {
19395
- sendError(requestId, "VALIDATION_ERROR", "sessionId must be non-empty string");
19396
- return;
19397
- }
19398
- if (typeof newLabel !== "string" || newLabel.length === 0) {
19399
- sendError(requestId, "VALIDATION_ERROR", "label must be non-empty string");
19400
- return;
19401
- }
19402
- const tokenPart = scope.subSessionId.slice(
19403
- scope.personaId.length + 1,
19404
- scope.personaId.length + 1 + 12
19405
- );
19406
- const tokenPrefix = `${scope.personaId}-${tokenPart}`;
19407
- const sameTuple = targetId === tokenPrefix || targetId.startsWith(`${tokenPrefix}-`);
19408
- if (!sameTuple) {
19409
- sendError(requestId, "FORBIDDEN", "sessionId out of (persona, token) scope");
19410
- return;
19411
- }
19412
- try {
19413
- this.deps.sessionManager.renameForScope({
19414
- sessionId: targetId,
19415
- scope: listenerScope(),
19416
- label: newLabel
19417
- });
19418
- if (requestId) send({ type: "chat:rename", requestId, ok: true });
19419
- } catch (err) {
19420
- const e = err;
19421
- sendError(requestId, e.code ?? "INTERNAL", e.message ?? String(err));
19422
- }
19423
- return;
19424
- }
19425
- case "chat:delete": {
19426
- const targetId = frame.sessionId;
19427
- if (typeof targetId !== "string" || targetId.length === 0) {
19428
- sendError(requestId, "VALIDATION_ERROR", "sessionId must be non-empty string");
19429
- return;
19430
- }
19431
- const tokenPart = scope.subSessionId.slice(
19432
- scope.personaId.length + 1,
19433
- scope.personaId.length + 1 + 12
19434
- );
19435
- const tokenPrefix = `${scope.personaId}-${tokenPart}`;
19436
- const sameTuple = targetId === tokenPrefix || targetId.startsWith(`${tokenPrefix}-`);
19437
- if (!sameTuple) {
19438
- sendError(requestId, "FORBIDDEN", "sessionId out of (persona, token) scope");
19439
- return;
19440
- }
19441
- try {
19442
- this.deps.sessionManager.deleteForScope({
19443
- sessionId: targetId,
19444
- scope: listenerScope()
19445
- });
19446
- if (requestId) send({ type: "chat:delete", requestId, ok: true });
19447
- } catch (err) {
19448
- const e = err;
19449
- sendError(requestId, e.code ?? "INTERNAL", e.message ?? String(err));
19450
- }
19451
- return;
19452
- }
19453
19649
  default:
19454
19650
  sendError(
19455
19651
  requestId,
@@ -19460,10 +19656,43 @@ var PersonaBoundHandler = class {
19460
19656
  }
19461
19657
  });
19462
19658
  ws.on("close", () => {
19463
- unsubscribe?.();
19464
- unsubscribe = null;
19659
+ unsubscribeSession?.();
19660
+ unsubscribeSession = null;
19661
+ cleanupTupleRegistration();
19465
19662
  });
19466
19663
  }
19664
+ // ---- helpers ----
19665
+ fanoutTuple(tokenPrefix, frame) {
19666
+ const subs = this.tupleSubscribers.get(tokenPrefix);
19667
+ if (!subs) return;
19668
+ for (const fn of subs) {
19669
+ try {
19670
+ fn(frame);
19671
+ } catch {
19672
+ }
19673
+ }
19674
+ }
19675
+ isInTuple(sessionId, tokenPrefix) {
19676
+ return sessionId === tokenPrefix || sessionId.startsWith(`${tokenPrefix}-`);
19677
+ }
19678
+ listChatsForTuple(personaId, tokenPrefix) {
19679
+ const all = this.deps.sessionManager.listPersonaSessions(personaId, "listener");
19680
+ return all.filter((s) => s.sessionId === tokenPrefix || s.sessionId.startsWith(`${tokenPrefix}-`)).map((s) => ({
19681
+ sessionId: s.sessionId,
19682
+ chatId: s.chatId ?? "unknown",
19683
+ label: s.label ?? "",
19684
+ createdAt: s.createdAt
19685
+ }));
19686
+ }
19687
+ // ---------------- owner-side helpers ----------------
19688
+ /**
19689
+ * owner-side bootstrap RPC:拉 (personaId, token) tuple 下所有 listener sub-session 元数据。
19690
+ * 由 handlers/persona.ts persona:listListenerChatsForToken 调用。
19691
+ */
19692
+ listChatsForOwner(personaId, token) {
19693
+ const tokenPrefix = this.deps.personaManager.tokenPrefixFor(personaId, token);
19694
+ return this.listChatsForTuple(personaId, tokenPrefix);
19695
+ }
19467
19696
  };
19468
19697
 
19469
19698
  // src/transport/auth.ts
@@ -20882,7 +21111,7 @@ function buildMetaHandlers(deps) {
20882
21111
  // src/handlers/persona.ts
20883
21112
  init_protocol();
20884
21113
  function buildPersonaHandlers(deps) {
20885
- const { personaManager, personaRegistry, sessionManager } = deps;
21114
+ const { personaManager, personaRegistry, sessionManager, personaBoundHandler } = deps;
20886
21115
  const create = async (frame) => {
20887
21116
  const { type: _type, requestId: _requestId, ...rest } = frame;
20888
21117
  const args = PersonaCreateArgsSchema.parse(rest);
@@ -20945,6 +21174,13 @@ function buildPersonaHandlers(deps) {
20945
21174
  response: { type: "persona:appendOwnerMessage", ok: true }
20946
21175
  };
20947
21176
  };
21177
+ const listListenerChatsForToken = async (frame) => {
21178
+ const args = PersonaListListenerChatsForTokenArgsSchema.parse(frame);
21179
+ const chats = personaBoundHandler.listChatsForOwner(args.personaId, args.token);
21180
+ return {
21181
+ response: { type: "persona:listListenerChatsForToken", chats }
21182
+ };
21183
+ };
20948
21184
  return {
20949
21185
  "persona:create": create,
20950
21186
  "persona:list": list,
@@ -20954,6 +21190,7 @@ function buildPersonaHandlers(deps) {
20954
21190
  "persona:issueToken": issueToken,
20955
21191
  "persona:revokeToken": revokeToken,
20956
21192
  "persona:listSubSessions": listSubSessions,
21193
+ "persona:listListenerChatsForToken": listListenerChatsForToken,
20957
21194
  "persona:appendOwnerMessage": appendOwnerMessage
20958
21195
  };
20959
21196
  }
@@ -20971,7 +21208,8 @@ function buildMethodHandlers(deps) {
20971
21208
  ...buildPersonaHandlers({
20972
21209
  personaManager: deps.personaManager,
20973
21210
  personaRegistry: deps.personaRegistry,
20974
- sessionManager: deps.manager
21211
+ sessionManager: deps.manager,
21212
+ personaBoundHandler: deps.personaBoundHandler
20975
21213
  })
20976
21214
  };
20977
21215
  }
@@ -21079,6 +21317,16 @@ async function startDaemon(config) {
21079
21317
  sessionManager: manager
21080
21318
  });
21081
21319
  let currentTunnelUrl = null;
21320
+ const personaBoundHandler = new PersonaBoundHandler({
21321
+ registry: personaRegistry,
21322
+ personaManager,
21323
+ personaStore,
21324
+ sessionManager: manager,
21325
+ logger,
21326
+ ownerBroadcast: (frame) => {
21327
+ transport?.broadcastAll(frame);
21328
+ }
21329
+ });
21082
21330
  const handlers = buildMethodHandlers({
21083
21331
  manager,
21084
21332
  workspace,
@@ -21090,15 +21338,9 @@ async function startDaemon(config) {
21090
21338
  store,
21091
21339
  personaManager,
21092
21340
  personaRegistry,
21341
+ personaBoundHandler,
21093
21342
  getTunnelUrl: () => currentTunnelUrl
21094
21343
  });
21095
- const personaBoundHandler = new PersonaBoundHandler({
21096
- registry: personaRegistry,
21097
- personaManager,
21098
- personaStore,
21099
- sessionManager: manager,
21100
- logger
21101
- });
21102
21344
  wsServer = new LocalWsServer({
21103
21345
  host: config.host,
21104
21346
  port: config.port,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.56-beta.90.047fd4f",
3
+ "version": "0.2.56-beta.91.df217af",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",