@clawos-dev/clawd 0.2.191 → 0.2.192-beta.386.33a5833

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
@@ -5996,7 +5996,7 @@ var init_feishu_auth = __esm({
5996
5996
  });
5997
5997
 
5998
5998
  // ../protocol/src/dispatch.ts
5999
- var DispatchOutcomeSchema, DispatchRunArgsSchema, DispatchCompleteArgsSchema;
5999
+ var DispatchOutcomeSchema, DispatchRunArgsSchema, DispatchRunResponseSchema, DispatchCompleteArgsSchema;
6000
6000
  var init_dispatch = __esm({
6001
6001
  "../protocol/src/dispatch.ts"() {
6002
6002
  "use strict";
@@ -6017,10 +6017,21 @@ var init_dispatch = __esm({
6017
6017
  prompt: external_exports.string(),
6018
6018
  // 跨设备 dispatch:非空 = A 角色,转发到该 contact deviceId 的 peer daemon;
6019
6019
  // 省略 = 本地 dispatch / B 角色本地执行。A 转发给 B 时 body 不带此字段。
6020
- targetDeviceId: external_exports.string().min(1).optional()
6020
+ targetDeviceId: external_exports.string().min(1).optional(),
6021
+ // 精确寻址已知 B session(复用其上下文):
6022
+ // - 本地(targetDeviceId 缺省):daemon 校验目标为本机 session 且
6023
+ // dispatchedFromSessionId 匹配 A 的 sessionId(防越权)
6024
+ // - 跨设备(targetDeviceId 非空):peer daemon 在其本地按同规则校验
6025
+ // creatorPrincipalId === A.deviceId
6026
+ // - 省略 = 新建 B session
6027
+ targetSessionId: external_exports.string().min(1).optional()
6028
+ });
6029
+ DispatchRunResponseSchema = external_exports.object({
6030
+ type: external_exports.literal("personaDispatch:run:ok"),
6031
+ outcome: DispatchOutcomeSchema,
6032
+ dispatchedSessionId: external_exports.string().min(1).optional()
6021
6033
  });
6022
6034
  DispatchCompleteArgsSchema = external_exports.object({
6023
- dispatchId: external_exports.string().min(1),
6024
6035
  outcome: DispatchOutcomeSchema
6025
6036
  });
6026
6037
  }
@@ -41552,8 +41563,6 @@ function buildSpawnContext(state, deps) {
41552
41563
  env.CLAWD_SESSION_ID = file.sessionId;
41553
41564
  const daemonUrl = deps.getDaemonUrl?.() ?? null;
41554
41565
  if (daemonUrl) env.CLAWD_DAEMON_URL = daemonUrl;
41555
- const dispatchId = deps.lookupDispatchByBSessionId?.(file.sessionId);
41556
- if (dispatchId) env.CLAWD_DISPATCH_ID = dispatchId;
41557
41566
  const personaId = file.ownerPersonaId;
41558
41567
  if (personaId) env.CLAWD_PERSONA_ID = personaId;
41559
41568
  const dispatchMcpConfigPath2 = deps.getDispatchMcpConfigPath?.() ?? null;
@@ -42359,14 +42368,14 @@ var SessionRunner = class {
42359
42368
  // 单栏 refactor (spec 2026-06-02 §5.1): cc 子进程 env 注入 CLAWD_DAEMON_URL,让 assistant
42360
42369
  // curl daemon HTTP RPC adapter (/api/rpc/<method>) 触发管理操作。null = HTTP adapter 未启。
42361
42370
  getDaemonUrl: this.hooks.getDaemonUrl,
42362
- // Persona dispatch:透传 dispatch.mcp.json 路径闭包 + B→dispatchId 反查闭包。
42363
- // reducer.buildSpawnContext CLAWD_DISPATCH_ID env / 派生 SpawnContext.dispatchMcpConfigPath
42371
+ // Persona dispatch:透传 dispatch.mcp.json 路径闭包,让 cc spawn 加 --mcp-config flag
42372
+ // 使两侧 cc 都能看到 personaDispatch / personaDispatchComplete tool(按 session 身份分工用哪个)。
42373
+ // dispatchId 不注 cc env——complete handler 用 sessionId 反查 in-flight dispatchId 配对。
42364
42374
  getDispatchMcpConfigPath: this.hooks.getDispatchMcpConfigPath,
42365
42375
  getShiftMcpConfigPath: this.hooks.getShiftMcpConfigPath,
42366
42376
  getInboxMcpConfigPath: this.hooks.getInboxMcpConfigPath,
42367
42377
  // Ticket MCP:透传 ticket.mcp.json 路径闭包(reducer 内做 persona-ticket-manager gating)
42368
42378
  getTicketMcpConfigPath: this.hooks.getTicketMcpConfigPath,
42369
- lookupDispatchByBSessionId: this.hooks.lookupDispatchByBSessionId,
42370
42379
  // ReadyGate v2:透传 mode 让 reducer send / ready-detected 分支决定走暂存队列还是直写
42371
42380
  mode: this.hooks.mode,
42372
42381
  // [RG-DBG] 注入 logger 让 reducer rgDbg 走 pino 进 clawd.log;定位完跟 rgDbg 一起删
@@ -42850,9 +42859,22 @@ You may read this file with your Read tool to understand context. You do not hav
42850
42859
 
42851
42860
  When done, call the MCP tool \`mcp__clawd-dispatch__personaDispatchComplete\` with your result:
42852
42861
  - Success: { text: "...", filePaths?: ["abs/path", ...] }
42853
- - Failure: { isFailure: true, reason: "..." }
42862
+ - Failure: { isFailure: true, reason: "..." }`;
42863
+ }
42864
+ function buildDispatchContinuationPack(args) {
42865
+ return `[Dispatched from owner \u2014 follow-up query]
42866
+
42867
+ Although you reported back on the previous task, the owner has a follow-up question. Continue from where you left off.
42868
+
42869
+ Owner's new message:
42870
+ ${args.prompt}
42854
42871
 
42855
- dispatchId for this task: ${args.dispatchId}`;
42872
+ Source conversation (jsonl, same path as before, may contain new messages):
42873
+ ${args.sourceJsonlPath}
42874
+
42875
+ When done, call \`mcp__clawd-dispatch__personaDispatchComplete\` again with your result:
42876
+ - Success: { text: "...", filePaths?: ["abs/path", ...] }
42877
+ - Failure: { isFailure: true, reason: "..." }`;
42856
42878
  }
42857
42879
  function derivePersonaSpawnCwd(file, personaRoot) {
42858
42880
  const personaId = file.ownerPersonaId;
@@ -43173,13 +43195,13 @@ var SessionManager = class {
43173
43195
  // 单栏 refactor (spec 2026-06-02 §5.1): 透传 daemon HTTP RPC base URL 闭包,
43174
43196
  // reducer 把它注入 cc 子进程 env CLAWD_DAEMON_URL.
43175
43197
  getDaemonUrl: this.deps.getDaemonUrl,
43176
- // Persona dispatch (Task 7): dispatch.mcp.json 路径 + B sessionId → dispatchId 反查
43177
- // 闭包透传给 reducer,让 cc spawn 加 --mcp-config flag + B session CLAWD_DISPATCH_ID env.
43198
+ // Persona dispatch: dispatch.mcp.json 路径闭包透传给 reducer,让 cc spawn
43199
+ // --mcp-config flag dispatch MCP server(两侧 cc 都挂,按 session 身份分工调 tool)。
43200
+ // dispatchId 不再走 cc env——complete handler 用 sessionId 反查 in-flight dispatchId 配对。
43178
43201
  getDispatchMcpConfigPath: this.deps.dispatchMcpConfigPath ? () => this.deps.dispatchMcpConfigPath ?? null : void 0,
43179
43202
  getTicketMcpConfigPath: this.deps.ticketMcpConfigPath ? () => this.deps.ticketMcpConfigPath ?? null : void 0,
43180
43203
  getShiftMcpConfigPath: this.deps.shiftMcpConfigPath ? () => this.deps.shiftMcpConfigPath ?? null : void 0,
43181
43204
  getInboxMcpConfigPath: this.deps.inboxMcpConfigPath ? () => this.deps.inboxMcpConfigPath ?? null : void 0,
43182
- lookupDispatchByBSessionId: this.deps.personaDispatchManager ? (bSid) => this.deps.personaDispatchManager?.lookupDispatchByBSessionId(bSid) : void 0,
43183
43205
  // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
43184
43206
  onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
43185
43207
  scope,
@@ -44019,10 +44041,11 @@ var SessionManager = class {
44019
44041
  * Persona dispatch: 启动 B session 并投递任务包当第一条 user message。
44020
44042
  *
44021
44043
  * - 复用 persona-owner 创建路径(cwd=personaDir、scope=persona-owner、不沙箱)
44022
- * - SessionFile 上写 dispatchedFromSessionId 关联回 A(UI 折叠用;mirror 一侧 strip
44023
- * - 跟 PersonaDispatchManager 双向登记 dispatchId ↔ B sessionId,让后续 reducer
44024
- * buildSpawnContext 能反查到 dispatchId CLAWD_DISPATCH_ID env
44025
- * - inject-owner-text 任务包;包尾教 B personaDispatchComplete tool 回传
44044
+ * - SessionFile 上写 dispatchedFromSessionId 关联回 A(UI 折叠用;mirror 一侧 strip
44045
+ * targetSessionId 复用路径也按此字段校验寻址来源)
44046
+ * - PersonaDispatchManager 登记 dispatchId B sessionId(complete handler 用
44047
+ * findInflightDispatchByBSessionId 反查回 dispatchId 配对回 A waiter)
44048
+ * - 投任务包;包尾教 B 用 personaDispatchComplete tool 回传
44026
44049
  *
44027
44050
  * cwd 派生与 SessionManager.create 的 owner persona 路径一致(derivePersonaSpawnCwd)。
44028
44051
  */
@@ -44077,8 +44100,7 @@ var SessionManager = class {
44077
44100
  });
44078
44101
  const taskPack = buildDispatchTaskPack({
44079
44102
  prompt: args.prompt,
44080
- sourceJsonlPath: args.sourceJsonlPath,
44081
- dispatchId: args.dispatchId
44103
+ sourceJsonlPath: args.sourceJsonlPath
44082
44104
  });
44083
44105
  runner.input({ kind: "command", command: { kind: "send", text: taskPack } });
44084
44106
  this.deps.logger?.info("dispatch.createDispatchedSession.task-pack-sent", {
@@ -44087,6 +44109,59 @@ var SessionManager = class {
44087
44109
  });
44088
44110
  return { sessionId };
44089
44111
  }
44112
+ /**
44113
+ * targetSessionId 精确寻址已知 B session 复用其上下文。投 continuation pack 给 runner。
44114
+ *
44115
+ * 关键差异 vs createDispatchedSession:
44116
+ * - 不 newSessionId() / 不写 SessionFile —— B session 复用(走 findOwnedSession)
44117
+ * - 权限校验:本地 dispatch → SessionFile.dispatchedFromSessionId === A 的 sessionId;
44118
+ * 跨设备 B 角色 → SessionFile.creatorPrincipalId === A 的 deviceId
44119
+ * - 跟 PersonaDispatchManager 重新登记(新 dispatchId 复用同 bSessionId)
44120
+ * - ensureRunnerForScope: cc 活复用;cc 死时首次 send 触发 spawn,jsonl 已存在自动 --resume
44121
+ *
44122
+ * @param callerIdentity 权限校验依据:
44123
+ * - kind='local': A 的 sessionId(本机 dispatch),校验 dispatchedFromSessionId 匹配
44124
+ * - kind='guest': A 的 deviceId(跨设备转发进来的 A),校验 creatorPrincipalId 匹配
44125
+ */
44126
+ resumeDispatchedSession(args) {
44127
+ if (!this.deps.personaDispatchManager) {
44128
+ throw new Error("resumeDispatchedSession: personaDispatchManager missing in ManagerDeps");
44129
+ }
44130
+ const file = this.findOwnedSession(args.bSessionId);
44131
+ if (!file) {
44132
+ throw new Error(`resumeDispatchedSession: B session ${args.bSessionId} not found`);
44133
+ }
44134
+ if (args.callerIdentity.kind === "local") {
44135
+ if (file.dispatchedFromSessionId !== args.callerIdentity.sourceSessionId) {
44136
+ throw new Error(
44137
+ `resumeDispatchedSession: session ${args.bSessionId} not dispatched by caller (dispatchedFromSessionId mismatch)`
44138
+ );
44139
+ }
44140
+ } else {
44141
+ if (file.creatorPrincipalId !== args.callerIdentity.sourcePrincipalId) {
44142
+ throw new Error(
44143
+ `resumeDispatchedSession: session ${args.bSessionId} not dispatched by caller device (creatorPrincipalId mismatch)`
44144
+ );
44145
+ }
44146
+ }
44147
+ const scope = this.scopeForFile(file);
44148
+ this.deps.personaDispatchManager.registerBSession(args.dispatchId, args.bSessionId);
44149
+ this.deps.logger?.info("dispatch.resumeDispatchedSession.registered", {
44150
+ dispatchId: args.dispatchId,
44151
+ bSessionId: args.bSessionId
44152
+ });
44153
+ const runner = this.ensureRunnerForScope(file, scope);
44154
+ const pack = buildDispatchContinuationPack({
44155
+ prompt: args.prompt,
44156
+ sourceJsonlPath: args.sourceJsonlPath
44157
+ });
44158
+ runner.input({ kind: "command", command: { kind: "send", text: pack } });
44159
+ this.deps.logger?.info("dispatch.resumeDispatchedSession.continuation-sent", {
44160
+ dispatchId: args.dispatchId,
44161
+ bSessionId: args.bSessionId
44162
+ });
44163
+ return { sessionId: args.bSessionId };
44164
+ }
44090
44165
  /**
44091
44166
  * shift v1 (spec 2026-06-24-clawd-shift):到点 fire 时由 ShiftScheduler 调,
44092
44167
  * 起一个新 cc session 跑 prompt(fire-and-forget,不绑 dispatchId / 不挂 caller)。
@@ -47493,6 +47568,7 @@ var PersonaDispatchManager = class {
47493
47568
  dispatchId,
47494
47569
  sourceSessionId: args.sourceSessionId,
47495
47570
  targetPersona: args.targetPersona,
47571
+ bSessionId: args.bSessionId,
47496
47572
  waiters: [],
47497
47573
  outcome: null
47498
47574
  });
@@ -47523,10 +47599,22 @@ var PersonaDispatchManager = class {
47523
47599
  if (!state) throw new Error(`unknown dispatchId: ${dispatchId}`);
47524
47600
  state.bSessionId = bSessionId;
47525
47601
  }
47526
- /** reducer buildSpawnContext 调,按 B 的 sessionId 反查 dispatchId(用于注 CLAWD_DISPATCH_ID env) */
47527
- lookupDispatchByBSessionId(bSessionId) {
47602
+ /**
47603
+ * 按 B sessionId 找当前 in-flight(未 resolved)的 dispatchId。complete handler 用这个把
47604
+ * B 的 outcome 配对回 A 的 waiter —— dispatchId 不再走 cc env,全靠
47605
+ * (bSessionId → in-flight dispatchId) 反查。
47606
+ *
47607
+ * 同一 bSessionId 不会同时有多个 in-flight:A 拿不到 outcome 不会发起新 dispatch;
47608
+ * 老板规则下 A 一次只能挂一个 waiter。
47609
+ *
47610
+ * 历史 entry(已 resolved)暂不清 —— 单 daemon lifetime 累积可接受;未来用量大了加
47611
+ * TTL GC(complete 后 N min 删 entry)。
47612
+ */
47613
+ findInflightDispatchByBSessionId(bSessionId) {
47528
47614
  for (const state of this.map.values()) {
47529
- if (state.bSessionId === bSessionId) return state.dispatchId;
47615
+ if (state.bSessionId === bSessionId && state.outcome === null) {
47616
+ return state.dispatchId;
47617
+ }
47530
47618
  }
47531
47619
  return void 0;
47532
47620
  }
@@ -48125,6 +48213,28 @@ function canAccessPersona(grants, personaId, action) {
48125
48213
  }
48126
48214
 
48127
48215
  // src/handlers/persona-dispatch.ts
48216
+ function assertLocalReuseAllowed(bFile, sourceSessionId) {
48217
+ if (bFile.dispatchedFromSessionId !== sourceSessionId) {
48218
+ throw new ClawdError(
48219
+ ERROR_CODES.UNAUTHORIZED,
48220
+ `targetSessionId not dispatched by caller (dispatchedFromSessionId mismatch)`
48221
+ );
48222
+ }
48223
+ }
48224
+ function assertGuestReuseAllowed(bFile, sourcePrincipalId, targetPersona) {
48225
+ if (bFile.ownerPersonaId !== targetPersona) {
48226
+ throw new ClawdError(
48227
+ ERROR_CODES.UNAUTHORIZED,
48228
+ `targetSessionId does not belong to targetPersona`
48229
+ );
48230
+ }
48231
+ if (bFile.creatorPrincipalId !== sourcePrincipalId) {
48232
+ throw new ClawdError(
48233
+ ERROR_CODES.UNAUTHORIZED,
48234
+ `targetSessionId not dispatched by caller device (creatorPrincipalId mismatch)`
48235
+ );
48236
+ }
48237
+ }
48128
48238
  function buildPersonaDispatchHandlers(deps) {
48129
48239
  const { personaDispatchManager: mgr, spawnB, logger } = deps;
48130
48240
  const run = async (frame, _client, ctx) => {
@@ -48143,15 +48253,21 @@ function buildPersonaDispatchHandlers(deps) {
48143
48253
  }
48144
48254
  logger?.info("dispatch.run.forward", {
48145
48255
  targetDeviceId: args.targetDeviceId,
48146
- targetPersona: args.targetPersona
48256
+ targetPersona: args.targetPersona,
48257
+ hasTargetSessionId: Boolean(args.targetSessionId)
48147
48258
  });
48148
- const outcome2 = await deps.forwardToPeer({
48259
+ const { outcome: outcome2, dispatchedSessionId } = await deps.forwardToPeer({
48149
48260
  targetDeviceId: args.targetDeviceId,
48150
48261
  targetPersona: args.targetPersona,
48151
- prompt: args.prompt
48262
+ prompt: args.prompt,
48263
+ targetSessionId: args.targetSessionId
48152
48264
  });
48153
48265
  return {
48154
- response: { type: "personaDispatch:run:ok", outcome: outcome2 }
48266
+ response: {
48267
+ type: "personaDispatch:run:ok",
48268
+ outcome: outcome2,
48269
+ ...dispatchedSessionId ? { dispatchedSessionId } : {}
48270
+ }
48155
48271
  };
48156
48272
  }
48157
48273
  if (ctx?.principal.kind === "guest") {
@@ -48162,34 +48278,65 @@ function buildPersonaDispatchHandlers(deps) {
48162
48278
  `persona not dispatchable: ${args.targetPersona}`
48163
48279
  );
48164
48280
  }
48281
+ const guestSourceId = ctx.principal.id;
48282
+ let bSessionId2;
48283
+ let route2 = "new";
48284
+ if (args.targetSessionId) {
48285
+ if (!deps.findOwnedSession) {
48286
+ throw new Error("targetSessionId reuse not wired (findOwnedSession missing)");
48287
+ }
48288
+ const bFile = deps.findOwnedSession(args.targetSessionId);
48289
+ if (!bFile) {
48290
+ throw new ClawdError(
48291
+ ERROR_CODES.SESSION_NOT_FOUND,
48292
+ `targetSessionId not found: ${args.targetSessionId}`
48293
+ );
48294
+ }
48295
+ assertGuestReuseAllowed(bFile, guestSourceId, args.targetPersona);
48296
+ route2 = "resume";
48297
+ bSessionId2 = args.targetSessionId;
48298
+ }
48165
48299
  const { dispatchId: dispatchId2 } = mgr.start({
48166
- sourceSessionId: ctx.principal.id,
48167
- targetPersona: args.targetPersona
48300
+ sourceSessionId: guestSourceId,
48301
+ targetPersona: args.targetPersona,
48302
+ bSessionId: bSessionId2
48168
48303
  });
48169
48304
  logger?.info("dispatch.run.received.guest", {
48170
48305
  dispatchId: dispatchId2,
48171
- sourcePrincipal: ctx.principal.id,
48306
+ sourcePrincipal: guestSourceId,
48172
48307
  targetPersona: args.targetPersona,
48173
- promptLen: args.prompt.length
48308
+ promptLen: args.prompt.length,
48309
+ route: route2
48174
48310
  });
48175
- void spawnB({
48176
- dispatchId: dispatchId2,
48177
- sourceSessionId: ctx.principal.id,
48178
- targetPersona: args.targetPersona,
48179
- prompt: args.prompt,
48180
- guestPrincipalId: ctx.principal.id,
48181
- guestDisplayName: ctx.principal.displayName
48182
- }).then(() => logger?.info("dispatch.spawnB.ok", { dispatchId: dispatchId2 })).catch((err) => {
48311
+ let resolvedBSessionId2 = bSessionId2;
48312
+ try {
48313
+ const { bSessionId: minted } = await spawnB({
48314
+ dispatchId: dispatchId2,
48315
+ sourceSessionId: guestSourceId,
48316
+ targetPersona: args.targetPersona,
48317
+ prompt: args.prompt,
48318
+ route: route2,
48319
+ bSessionId: bSessionId2,
48320
+ guestPrincipalId: guestSourceId,
48321
+ guestDisplayName: ctx.principal.displayName
48322
+ });
48323
+ resolvedBSessionId2 = minted;
48324
+ logger?.info("dispatch.spawnB.ok", { dispatchId: dispatchId2, route: route2 });
48325
+ } catch (err) {
48183
48326
  const reason = err instanceof Error ? err.message : String(err);
48184
- logger?.warn("dispatch.spawnB.failed", { dispatchId: dispatchId2, reason });
48327
+ logger?.warn("dispatch.spawnB.failed", { dispatchId: dispatchId2, route: route2, reason });
48185
48328
  mgr.complete(dispatchId2, {
48186
48329
  kind: "failure",
48187
48330
  reason: `failed to spawn B: ${reason}`
48188
48331
  });
48189
- });
48332
+ }
48190
48333
  const outcome2 = await mgr.wait(dispatchId2);
48191
48334
  return {
48192
- response: { type: "personaDispatch:run:ok", outcome: outcome2 }
48335
+ response: {
48336
+ type: "personaDispatch:run:ok",
48337
+ outcome: outcome2,
48338
+ ...resolvedBSessionId2 ? { dispatchedSessionId: resolvedBSessionId2 } : {}
48339
+ }
48193
48340
  };
48194
48341
  }
48195
48342
  if (!sourceSessionId) {
@@ -48197,31 +48344,61 @@ function buildPersonaDispatchHandlers(deps) {
48197
48344
  "personaDispatch:run requires sessionId (caller must pass x-clawd-session-id header)"
48198
48345
  );
48199
48346
  }
48347
+ let bSessionId;
48348
+ let route = "new";
48349
+ if (args.targetSessionId) {
48350
+ if (!deps.findOwnedSession) {
48351
+ throw new Error("targetSessionId reuse not wired (findOwnedSession missing)");
48352
+ }
48353
+ const bFile = deps.findOwnedSession(args.targetSessionId);
48354
+ if (!bFile) {
48355
+ throw new ClawdError(
48356
+ ERROR_CODES.SESSION_NOT_FOUND,
48357
+ `targetSessionId not found: ${args.targetSessionId}`
48358
+ );
48359
+ }
48360
+ if (bFile.ownerPersonaId !== args.targetPersona) {
48361
+ throw new ClawdError(
48362
+ ERROR_CODES.UNAUTHORIZED,
48363
+ `targetSessionId does not belong to targetPersona`
48364
+ );
48365
+ }
48366
+ assertLocalReuseAllowed(bFile, sourceSessionId);
48367
+ route = "resume";
48368
+ bSessionId = args.targetSessionId;
48369
+ }
48200
48370
  const { dispatchId } = mgr.start({
48201
48371
  sourceSessionId,
48202
- targetPersona: args.targetPersona
48372
+ targetPersona: args.targetPersona,
48373
+ bSessionId
48203
48374
  });
48204
48375
  logger?.info("dispatch.run.received", {
48205
48376
  dispatchId,
48206
48377
  sourceSessionId,
48207
48378
  targetPersona: args.targetPersona,
48208
- promptLen: args.prompt.length
48379
+ promptLen: args.prompt.length,
48380
+ route
48209
48381
  });
48210
- void spawnB({
48211
- dispatchId,
48212
- sourceSessionId,
48213
- targetPersona: args.targetPersona,
48214
- prompt: args.prompt
48215
- }).then(() => {
48216
- logger?.info("dispatch.spawnB.ok", { dispatchId });
48217
- }).catch((err) => {
48382
+ let resolvedBSessionId = bSessionId;
48383
+ try {
48384
+ const { bSessionId: minted } = await spawnB({
48385
+ dispatchId,
48386
+ sourceSessionId,
48387
+ targetPersona: args.targetPersona,
48388
+ prompt: args.prompt,
48389
+ route,
48390
+ bSessionId
48391
+ });
48392
+ resolvedBSessionId = minted;
48393
+ logger?.info("dispatch.spawnB.ok", { dispatchId, route });
48394
+ } catch (err) {
48218
48395
  const reason = err instanceof Error ? err.message : String(err);
48219
- logger?.warn("dispatch.spawnB.failed", { dispatchId, reason });
48396
+ logger?.warn("dispatch.spawnB.failed", { dispatchId, route, reason });
48220
48397
  mgr.complete(dispatchId, {
48221
48398
  kind: "failure",
48222
48399
  reason: `failed to spawn B: ${reason}`
48223
48400
  });
48224
- });
48401
+ }
48225
48402
  logger?.info("dispatch.run.waiting", { dispatchId });
48226
48403
  const outcome = await mgr.wait(dispatchId);
48227
48404
  logger?.info("dispatch.run.resolved", {
@@ -48229,17 +48406,34 @@ function buildPersonaDispatchHandlers(deps) {
48229
48406
  outcomeKind: outcome.kind
48230
48407
  });
48231
48408
  return {
48232
- response: { type: "personaDispatch:run:ok", outcome }
48409
+ response: {
48410
+ type: "personaDispatch:run:ok",
48411
+ outcome,
48412
+ ...resolvedBSessionId ? { dispatchedSessionId: resolvedBSessionId } : {}
48413
+ }
48233
48414
  };
48234
48415
  };
48235
48416
  const complete = async (frame) => {
48236
48417
  const { type: _t, requestId: _r, ...rest } = frame;
48418
+ const sessionId = typeof rest.sessionId === "string" ? rest.sessionId : void 0;
48237
48419
  const args = DispatchCompleteArgsSchema.parse(rest);
48420
+ if (!sessionId) {
48421
+ throw new Error(
48422
+ "personaDispatch:complete requires sessionId (caller must pass x-clawd-session-id header)"
48423
+ );
48424
+ }
48425
+ const dispatchId = mgr.findInflightDispatchByBSessionId(sessionId);
48426
+ if (!dispatchId) {
48427
+ throw new Error(
48428
+ `no in-flight dispatch for session ${sessionId} (already completed or B was never dispatched)`
48429
+ );
48430
+ }
48238
48431
  logger?.info("dispatch.complete.received", {
48239
- dispatchId: args.dispatchId,
48432
+ dispatchId,
48433
+ sessionId,
48240
48434
  outcomeKind: args.outcome.kind
48241
48435
  });
48242
- mgr.complete(args.dispatchId, args.outcome);
48436
+ mgr.complete(dispatchId, args.outcome);
48243
48437
  return {
48244
48438
  response: { type: "personaDispatch:complete:ok" }
48245
48439
  };
@@ -48269,25 +48463,37 @@ async function forwardDispatchToPeer(args) {
48269
48463
  authorization: `Bearer ${args.contact.connectToken}`
48270
48464
  },
48271
48465
  // 注意:不带 targetDeviceId —— B 端据此判定为本地执行(B 角色)。
48272
- body: JSON.stringify({ targetPersona: args.targetPersona, prompt: args.prompt })
48466
+ // targetSessionId 透传:peer 那台机会按 creatorPrincipalId === A.deviceId 校验。
48467
+ body: JSON.stringify({
48468
+ targetPersona: args.targetPersona,
48469
+ prompt: args.prompt,
48470
+ ...args.targetSessionId ? { targetSessionId: args.targetSessionId } : {}
48471
+ })
48273
48472
  });
48274
48473
  } catch (err) {
48275
48474
  const msg = err instanceof Error ? err.message : String(err);
48276
- return { kind: "failure", reason: `forward to peer failed: ${msg}` };
48475
+ return { outcome: { kind: "failure", reason: `forward to peer failed: ${msg}` } };
48277
48476
  }
48278
48477
  let json;
48279
48478
  try {
48280
48479
  json = await res.json();
48281
48480
  } catch {
48282
48481
  return {
48283
- kind: "failure",
48284
- reason: `peer returned non-JSON response (HTTP ${res.status})`
48482
+ outcome: {
48483
+ kind: "failure",
48484
+ reason: `peer returned non-JSON response (HTTP ${res.status})`
48485
+ }
48285
48486
  };
48286
48487
  }
48287
48488
  if (json.ok === false) {
48288
- return { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` };
48489
+ return {
48490
+ outcome: { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` }
48491
+ };
48289
48492
  }
48290
- return json.result.outcome;
48493
+ return {
48494
+ outcome: json.result.outcome,
48495
+ dispatchedSessionId: json.result.dispatchedSessionId
48496
+ };
48291
48497
  }
48292
48498
  async function forwardInboxPostToPeer(args) {
48293
48499
  const f = args.fetchImpl ?? fetch;
@@ -57625,9 +57831,9 @@ async function startDaemon(config) {
57625
57831
  // 127.0.0.1(不是 config.host)—— cc 跑在本机,loopback 最稳;外部访问限制 + http-router
57626
57832
  // 的 isLoopback 兜底已确保安全。
57627
57833
  getDaemonUrl: () => `http://127.0.0.1:${config.port}`,
57628
- // Persona dispatch (Task 8): manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
57629
- // - personaDispatchManager: manager.createDispatchedSession 用它 registerBSession;
57630
- // reducer 通过闭包反查 dispatchId CLAWD_DISPATCH_ID env
57834
+ // Persona dispatch: manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
57835
+ // - personaDispatchManager: createDispatchedSession / resumeDispatchedSession 用它
57836
+ // registerBSession;complete handler findInflightDispatchByBSessionId 反查回 dispatchId
57631
57837
  // - dispatchMcpConfigPath: reducer 透传到 SpawnContext,cc spawn 加 --mcp-config flag。
57632
57838
  personaDispatchManager,
57633
57839
  dispatchMcpConfigPath: dispatchMcpConfigPath2,
@@ -58009,7 +58215,8 @@ async function startDaemon(config) {
58009
58215
  logger.info("dispatch.spawnB.start", {
58010
58216
  dispatchId: args.dispatchId,
58011
58217
  targetPersona: args.targetPersona,
58012
- sourceSessionId: args.sourceSessionId
58218
+ sourceSessionId: args.sourceSessionId,
58219
+ route: args.route
58013
58220
  });
58014
58221
  const sourceFile = manager.findOwnedSession(args.sourceSessionId);
58015
58222
  if (!sourceFile && !args.guestPrincipalId) {
@@ -58028,9 +58235,23 @@ async function startDaemon(config) {
58028
58235
  logger.info("dispatch.spawnB.source-resolved", {
58029
58236
  dispatchId: args.dispatchId,
58030
58237
  sourceJsonlPath,
58031
- hasToolSessionId: Boolean(sourceFile?.toolSessionId)
58238
+ hasToolSessionId: Boolean(sourceFile?.toolSessionId),
58239
+ route: args.route
58032
58240
  });
58033
- manager.createDispatchedSession({
58241
+ if (args.route === "resume") {
58242
+ if (!args.bSessionId) {
58243
+ throw new Error("resume route requires bSessionId");
58244
+ }
58245
+ const { sessionId: sessionId2 } = manager.resumeDispatchedSession({
58246
+ dispatchId: args.dispatchId,
58247
+ bSessionId: args.bSessionId,
58248
+ prompt: args.prompt,
58249
+ sourceJsonlPath,
58250
+ callerIdentity: args.guestPrincipalId ? { kind: "guest", sourcePrincipalId: args.guestPrincipalId } : { kind: "local", sourceSessionId: args.sourceSessionId }
58251
+ });
58252
+ return { bSessionId: sessionId2 };
58253
+ }
58254
+ const { sessionId } = manager.createDispatchedSession({
58034
58255
  dispatchId: args.dispatchId,
58035
58256
  sourceSessionId: args.sourceSessionId,
58036
58257
  targetPersona: args.targetPersona,
@@ -58040,24 +58261,30 @@ async function startDaemon(config) {
58040
58261
  guestPrincipalId: args.guestPrincipalId,
58041
58262
  guestDisplayName: args.guestDisplayName
58042
58263
  });
58264
+ return { bSessionId: sessionId };
58043
58265
  },
58044
58266
  // A 角色:从 ContactStore 取 peer 可达 URL + connect token,转发到对端 daemon /rpc。
58045
- forwardToPeer: async ({ targetDeviceId, targetPersona, prompt }) => {
58267
+ forwardToPeer: async ({ targetDeviceId, targetPersona, prompt, targetSessionId }) => {
58046
58268
  const contact = contactStore.get(targetDeviceId);
58047
58269
  if (!contact || !contact.remoteUrl || !contact.connectToken) {
58048
58270
  return {
58049
- kind: "failure",
58050
- reason: `unknown or unreachable contact: ${targetDeviceId}`
58271
+ outcome: {
58272
+ kind: "failure",
58273
+ reason: `unknown or unreachable contact: ${targetDeviceId}`
58274
+ }
58051
58275
  };
58052
58276
  }
58053
58277
  return forwardDispatchToPeer({
58054
58278
  contact: { remoteUrl: contact.remoteUrl, connectToken: contact.connectToken },
58055
58279
  targetPersona,
58056
- prompt
58280
+ prompt,
58281
+ targetSessionId
58057
58282
  });
58058
58283
  },
58059
58284
  // B 角色:判断 targetPersona 是否 public(跨设备授权边界,private 拒)。
58060
- getPersonaPublic: (personaId) => personaRegistry.get(personaId)?.public ?? false
58285
+ getPersonaPublic: (personaId) => personaRegistry.get(personaId)?.public ?? false,
58286
+ // targetSessionId 复用路径的权限校验数据源:读 B session 的 SessionFile。
58287
+ findOwnedSession: (sessionId) => manager.findOwnedSession(sessionId)
58061
58288
  });
58062
58289
  handlers = { ...handlers, ...dispatchHandlers };
58063
58290
  const shiftHandlers = buildShiftInternalHandlers({