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

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, DispatchRunResponseSchema, DispatchCompleteArgsSchema;
5999
+ var DispatchOutcomeSchema, DispatchRunArgsSchema, DispatchCompleteArgsSchema;
6000
6000
  var init_dispatch = __esm({
6001
6001
  "../protocol/src/dispatch.ts"() {
6002
6002
  "use strict";
@@ -6017,21 +6017,10 @@ 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(),
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()
6020
+ targetDeviceId: external_exports.string().min(1).optional()
6033
6021
  });
6034
6022
  DispatchCompleteArgsSchema = external_exports.object({
6023
+ dispatchId: external_exports.string().min(1),
6035
6024
  outcome: DispatchOutcomeSchema
6036
6025
  });
6037
6026
  }
@@ -41563,6 +41552,8 @@ function buildSpawnContext(state, deps) {
41563
41552
  env.CLAWD_SESSION_ID = file.sessionId;
41564
41553
  const daemonUrl = deps.getDaemonUrl?.() ?? null;
41565
41554
  if (daemonUrl) env.CLAWD_DAEMON_URL = daemonUrl;
41555
+ const dispatchId = deps.lookupDispatchByBSessionId?.(file.sessionId);
41556
+ if (dispatchId) env.CLAWD_DISPATCH_ID = dispatchId;
41566
41557
  const personaId = file.ownerPersonaId;
41567
41558
  if (personaId) env.CLAWD_PERSONA_ID = personaId;
41568
41559
  const dispatchMcpConfigPath2 = deps.getDispatchMcpConfigPath?.() ?? null;
@@ -42368,14 +42359,14 @@ var SessionRunner = class {
42368
42359
  // 单栏 refactor (spec 2026-06-02 §5.1): cc 子进程 env 注入 CLAWD_DAEMON_URL,让 assistant
42369
42360
  // curl daemon HTTP RPC adapter (/api/rpc/<method>) 触发管理操作。null = HTTP adapter 未启。
42370
42361
  getDaemonUrl: this.hooks.getDaemonUrl,
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 配对。
42362
+ // Persona dispatch:透传 dispatch.mcp.json 路径闭包 + B→dispatchId 反查闭包。
42363
+ // reducer.buildSpawnContext CLAWD_DISPATCH_ID env / 派生 SpawnContext.dispatchMcpConfigPath
42374
42364
  getDispatchMcpConfigPath: this.hooks.getDispatchMcpConfigPath,
42375
42365
  getShiftMcpConfigPath: this.hooks.getShiftMcpConfigPath,
42376
42366
  getInboxMcpConfigPath: this.hooks.getInboxMcpConfigPath,
42377
42367
  // Ticket MCP:透传 ticket.mcp.json 路径闭包(reducer 内做 persona-ticket-manager gating)
42378
42368
  getTicketMcpConfigPath: this.hooks.getTicketMcpConfigPath,
42369
+ lookupDispatchByBSessionId: this.hooks.lookupDispatchByBSessionId,
42379
42370
  // ReadyGate v2:透传 mode 让 reducer send / ready-detected 分支决定走暂存队列还是直写
42380
42371
  mode: this.hooks.mode,
42381
42372
  // [RG-DBG] 注入 logger 让 reducer rgDbg 走 pino 进 clawd.log;定位完跟 rgDbg 一起删
@@ -42859,22 +42850,9 @@ You may read this file with your Read tool to understand context. You do not hav
42859
42850
 
42860
42851
  When done, call the MCP tool \`mcp__clawd-dispatch__personaDispatchComplete\` with your result:
42861
42852
  - Success: { text: "...", filePaths?: ["abs/path", ...] }
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}
42853
+ - Failure: { isFailure: true, reason: "..." }
42871
42854
 
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: "..." }`;
42855
+ dispatchId for this task: ${args.dispatchId}`;
42878
42856
  }
42879
42857
  function derivePersonaSpawnCwd(file, personaRoot) {
42880
42858
  const personaId = file.ownerPersonaId;
@@ -43195,13 +43173,13 @@ var SessionManager = class {
43195
43173
  // 单栏 refactor (spec 2026-06-02 §5.1): 透传 daemon HTTP RPC base URL 闭包,
43196
43174
  // reducer 把它注入 cc 子进程 env CLAWD_DAEMON_URL.
43197
43175
  getDaemonUrl: this.deps.getDaemonUrl,
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 配对。
43176
+ // Persona dispatch (Task 7): dispatch.mcp.json 路径 + B sessionId → dispatchId 反查
43177
+ // 闭包透传给 reducer,让 cc spawn 加 --mcp-config flag + B session CLAWD_DISPATCH_ID env.
43201
43178
  getDispatchMcpConfigPath: this.deps.dispatchMcpConfigPath ? () => this.deps.dispatchMcpConfigPath ?? null : void 0,
43202
43179
  getTicketMcpConfigPath: this.deps.ticketMcpConfigPath ? () => this.deps.ticketMcpConfigPath ?? null : void 0,
43203
43180
  getShiftMcpConfigPath: this.deps.shiftMcpConfigPath ? () => this.deps.shiftMcpConfigPath ?? null : void 0,
43204
43181
  getInboxMcpConfigPath: this.deps.inboxMcpConfigPath ? () => this.deps.inboxMcpConfigPath ?? null : void 0,
43182
+ lookupDispatchByBSessionId: this.deps.personaDispatchManager ? (bSid) => this.deps.personaDispatchManager?.lookupDispatchByBSessionId(bSid) : void 0,
43205
43183
  // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
43206
43184
  onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
43207
43185
  scope,
@@ -44041,11 +44019,10 @@ var SessionManager = class {
44041
44019
  * Persona dispatch: 启动 B session 并投递任务包当第一条 user message。
44042
44020
  *
44043
44021
  * - 复用 persona-owner 创建路径(cwd=personaDir、scope=persona-owner、不沙箱)
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 回传
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 回传
44049
44026
  *
44050
44027
  * cwd 派生与 SessionManager.create 的 owner persona 路径一致(derivePersonaSpawnCwd)。
44051
44028
  */
@@ -44100,7 +44077,8 @@ var SessionManager = class {
44100
44077
  });
44101
44078
  const taskPack = buildDispatchTaskPack({
44102
44079
  prompt: args.prompt,
44103
- sourceJsonlPath: args.sourceJsonlPath
44080
+ sourceJsonlPath: args.sourceJsonlPath,
44081
+ dispatchId: args.dispatchId
44104
44082
  });
44105
44083
  runner.input({ kind: "command", command: { kind: "send", text: taskPack } });
44106
44084
  this.deps.logger?.info("dispatch.createDispatchedSession.task-pack-sent", {
@@ -44109,59 +44087,6 @@ var SessionManager = class {
44109
44087
  });
44110
44088
  return { sessionId };
44111
44089
  }
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
- }
44165
44090
  /**
44166
44091
  * shift v1 (spec 2026-06-24-clawd-shift):到点 fire 时由 ShiftScheduler 调,
44167
44092
  * 起一个新 cc session 跑 prompt(fire-and-forget,不绑 dispatchId / 不挂 caller)。
@@ -47568,7 +47493,6 @@ var PersonaDispatchManager = class {
47568
47493
  dispatchId,
47569
47494
  sourceSessionId: args.sourceSessionId,
47570
47495
  targetPersona: args.targetPersona,
47571
- bSessionId: args.bSessionId,
47572
47496
  waiters: [],
47573
47497
  outcome: null
47574
47498
  });
@@ -47599,22 +47523,10 @@ var PersonaDispatchManager = class {
47599
47523
  if (!state) throw new Error(`unknown dispatchId: ${dispatchId}`);
47600
47524
  state.bSessionId = bSessionId;
47601
47525
  }
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) {
47526
+ /** reducer buildSpawnContext 调,按 B 的 sessionId 反查 dispatchId(用于注 CLAWD_DISPATCH_ID env) */
47527
+ lookupDispatchByBSessionId(bSessionId) {
47614
47528
  for (const state of this.map.values()) {
47615
- if (state.bSessionId === bSessionId && state.outcome === null) {
47616
- return state.dispatchId;
47617
- }
47529
+ if (state.bSessionId === bSessionId) return state.dispatchId;
47618
47530
  }
47619
47531
  return void 0;
47620
47532
  }
@@ -48213,28 +48125,6 @@ function canAccessPersona(grants, personaId, action) {
48213
48125
  }
48214
48126
 
48215
48127
  // 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
- }
48238
48128
  function buildPersonaDispatchHandlers(deps) {
48239
48129
  const { personaDispatchManager: mgr, spawnB, logger } = deps;
48240
48130
  const run = async (frame, _client, ctx) => {
@@ -48253,21 +48143,15 @@ function buildPersonaDispatchHandlers(deps) {
48253
48143
  }
48254
48144
  logger?.info("dispatch.run.forward", {
48255
48145
  targetDeviceId: args.targetDeviceId,
48256
- targetPersona: args.targetPersona,
48257
- hasTargetSessionId: Boolean(args.targetSessionId)
48146
+ targetPersona: args.targetPersona
48258
48147
  });
48259
- const { outcome: outcome2, dispatchedSessionId } = await deps.forwardToPeer({
48148
+ const outcome2 = await deps.forwardToPeer({
48260
48149
  targetDeviceId: args.targetDeviceId,
48261
48150
  targetPersona: args.targetPersona,
48262
- prompt: args.prompt,
48263
- targetSessionId: args.targetSessionId
48151
+ prompt: args.prompt
48264
48152
  });
48265
48153
  return {
48266
- response: {
48267
- type: "personaDispatch:run:ok",
48268
- outcome: outcome2,
48269
- ...dispatchedSessionId ? { dispatchedSessionId } : {}
48270
- }
48154
+ response: { type: "personaDispatch:run:ok", outcome: outcome2 }
48271
48155
  };
48272
48156
  }
48273
48157
  if (ctx?.principal.kind === "guest") {
@@ -48278,65 +48162,34 @@ function buildPersonaDispatchHandlers(deps) {
48278
48162
  `persona not dispatchable: ${args.targetPersona}`
48279
48163
  );
48280
48164
  }
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
- }
48299
48165
  const { dispatchId: dispatchId2 } = mgr.start({
48300
- sourceSessionId: guestSourceId,
48301
- targetPersona: args.targetPersona,
48302
- bSessionId: bSessionId2
48166
+ sourceSessionId: ctx.principal.id,
48167
+ targetPersona: args.targetPersona
48303
48168
  });
48304
48169
  logger?.info("dispatch.run.received.guest", {
48305
48170
  dispatchId: dispatchId2,
48306
- sourcePrincipal: guestSourceId,
48171
+ sourcePrincipal: ctx.principal.id,
48307
48172
  targetPersona: args.targetPersona,
48308
- promptLen: args.prompt.length,
48309
- route: route2
48173
+ promptLen: args.prompt.length
48310
48174
  });
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) {
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) => {
48326
48183
  const reason = err instanceof Error ? err.message : String(err);
48327
- logger?.warn("dispatch.spawnB.failed", { dispatchId: dispatchId2, route: route2, reason });
48184
+ logger?.warn("dispatch.spawnB.failed", { dispatchId: dispatchId2, reason });
48328
48185
  mgr.complete(dispatchId2, {
48329
48186
  kind: "failure",
48330
48187
  reason: `failed to spawn B: ${reason}`
48331
48188
  });
48332
- }
48189
+ });
48333
48190
  const outcome2 = await mgr.wait(dispatchId2);
48334
48191
  return {
48335
- response: {
48336
- type: "personaDispatch:run:ok",
48337
- outcome: outcome2,
48338
- ...resolvedBSessionId2 ? { dispatchedSessionId: resolvedBSessionId2 } : {}
48339
- }
48192
+ response: { type: "personaDispatch:run:ok", outcome: outcome2 }
48340
48193
  };
48341
48194
  }
48342
48195
  if (!sourceSessionId) {
@@ -48344,61 +48197,31 @@ function buildPersonaDispatchHandlers(deps) {
48344
48197
  "personaDispatch:run requires sessionId (caller must pass x-clawd-session-id header)"
48345
48198
  );
48346
48199
  }
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
- }
48370
48200
  const { dispatchId } = mgr.start({
48371
48201
  sourceSessionId,
48372
- targetPersona: args.targetPersona,
48373
- bSessionId
48202
+ targetPersona: args.targetPersona
48374
48203
  });
48375
48204
  logger?.info("dispatch.run.received", {
48376
48205
  dispatchId,
48377
48206
  sourceSessionId,
48378
48207
  targetPersona: args.targetPersona,
48379
- promptLen: args.prompt.length,
48380
- route
48208
+ promptLen: args.prompt.length
48381
48209
  });
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) {
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) => {
48395
48218
  const reason = err instanceof Error ? err.message : String(err);
48396
- logger?.warn("dispatch.spawnB.failed", { dispatchId, route, reason });
48219
+ logger?.warn("dispatch.spawnB.failed", { dispatchId, reason });
48397
48220
  mgr.complete(dispatchId, {
48398
48221
  kind: "failure",
48399
48222
  reason: `failed to spawn B: ${reason}`
48400
48223
  });
48401
- }
48224
+ });
48402
48225
  logger?.info("dispatch.run.waiting", { dispatchId });
48403
48226
  const outcome = await mgr.wait(dispatchId);
48404
48227
  logger?.info("dispatch.run.resolved", {
@@ -48406,34 +48229,17 @@ function buildPersonaDispatchHandlers(deps) {
48406
48229
  outcomeKind: outcome.kind
48407
48230
  });
48408
48231
  return {
48409
- response: {
48410
- type: "personaDispatch:run:ok",
48411
- outcome,
48412
- ...resolvedBSessionId ? { dispatchedSessionId: resolvedBSessionId } : {}
48413
- }
48232
+ response: { type: "personaDispatch:run:ok", outcome }
48414
48233
  };
48415
48234
  };
48416
48235
  const complete = async (frame) => {
48417
48236
  const { type: _t, requestId: _r, ...rest } = frame;
48418
- const sessionId = typeof rest.sessionId === "string" ? rest.sessionId : void 0;
48419
48237
  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
- }
48431
48238
  logger?.info("dispatch.complete.received", {
48432
- dispatchId,
48433
- sessionId,
48239
+ dispatchId: args.dispatchId,
48434
48240
  outcomeKind: args.outcome.kind
48435
48241
  });
48436
- mgr.complete(dispatchId, args.outcome);
48242
+ mgr.complete(args.dispatchId, args.outcome);
48437
48243
  return {
48438
48244
  response: { type: "personaDispatch:complete:ok" }
48439
48245
  };
@@ -48463,37 +48269,25 @@ async function forwardDispatchToPeer(args) {
48463
48269
  authorization: `Bearer ${args.contact.connectToken}`
48464
48270
  },
48465
48271
  // 注意:不带 targetDeviceId —— B 端据此判定为本地执行(B 角色)。
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
- })
48272
+ body: JSON.stringify({ targetPersona: args.targetPersona, prompt: args.prompt })
48472
48273
  });
48473
48274
  } catch (err) {
48474
48275
  const msg = err instanceof Error ? err.message : String(err);
48475
- return { outcome: { kind: "failure", reason: `forward to peer failed: ${msg}` } };
48276
+ return { kind: "failure", reason: `forward to peer failed: ${msg}` };
48476
48277
  }
48477
48278
  let json;
48478
48279
  try {
48479
48280
  json = await res.json();
48480
48281
  } catch {
48481
48282
  return {
48482
- outcome: {
48483
- kind: "failure",
48484
- reason: `peer returned non-JSON response (HTTP ${res.status})`
48485
- }
48283
+ kind: "failure",
48284
+ reason: `peer returned non-JSON response (HTTP ${res.status})`
48486
48285
  };
48487
48286
  }
48488
48287
  if (json.ok === false) {
48489
- return {
48490
- outcome: { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` }
48491
- };
48288
+ return { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` };
48492
48289
  }
48493
- return {
48494
- outcome: json.result.outcome,
48495
- dispatchedSessionId: json.result.dispatchedSessionId
48496
- };
48290
+ return json.result.outcome;
48497
48291
  }
48498
48292
  async function forwardInboxPostToPeer(args) {
48499
48293
  const f = args.fetchImpl ?? fetch;
@@ -57831,9 +57625,9 @@ async function startDaemon(config) {
57831
57625
  // 127.0.0.1(不是 config.host)—— cc 跑在本机,loopback 最稳;外部访问限制 + http-router
57832
57626
  // 的 isLoopback 兜底已确保安全。
57833
57627
  getDaemonUrl: () => `http://127.0.0.1:${config.port}`,
57834
- // Persona dispatch: manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
57835
- // - personaDispatchManager: createDispatchedSession / resumeDispatchedSession 用它
57836
- // registerBSession;complete handler findInflightDispatchByBSessionId 反查回 dispatchId
57628
+ // Persona dispatch (Task 8): manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
57629
+ // - personaDispatchManager: manager.createDispatchedSession 用它 registerBSession;
57630
+ // reducer 通过闭包反查 dispatchId CLAWD_DISPATCH_ID env
57837
57631
  // - dispatchMcpConfigPath: reducer 透传到 SpawnContext,cc spawn 加 --mcp-config flag。
57838
57632
  personaDispatchManager,
57839
57633
  dispatchMcpConfigPath: dispatchMcpConfigPath2,
@@ -58215,8 +58009,7 @@ async function startDaemon(config) {
58215
58009
  logger.info("dispatch.spawnB.start", {
58216
58010
  dispatchId: args.dispatchId,
58217
58011
  targetPersona: args.targetPersona,
58218
- sourceSessionId: args.sourceSessionId,
58219
- route: args.route
58012
+ sourceSessionId: args.sourceSessionId
58220
58013
  });
58221
58014
  const sourceFile = manager.findOwnedSession(args.sourceSessionId);
58222
58015
  if (!sourceFile && !args.guestPrincipalId) {
@@ -58235,23 +58028,9 @@ async function startDaemon(config) {
58235
58028
  logger.info("dispatch.spawnB.source-resolved", {
58236
58029
  dispatchId: args.dispatchId,
58237
58030
  sourceJsonlPath,
58238
- hasToolSessionId: Boolean(sourceFile?.toolSessionId),
58239
- route: args.route
58031
+ hasToolSessionId: Boolean(sourceFile?.toolSessionId)
58240
58032
  });
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({
58033
+ manager.createDispatchedSession({
58255
58034
  dispatchId: args.dispatchId,
58256
58035
  sourceSessionId: args.sourceSessionId,
58257
58036
  targetPersona: args.targetPersona,
@@ -58261,30 +58040,24 @@ async function startDaemon(config) {
58261
58040
  guestPrincipalId: args.guestPrincipalId,
58262
58041
  guestDisplayName: args.guestDisplayName
58263
58042
  });
58264
- return { bSessionId: sessionId };
58265
58043
  },
58266
58044
  // A 角色:从 ContactStore 取 peer 可达 URL + connect token,转发到对端 daemon /rpc。
58267
- forwardToPeer: async ({ targetDeviceId, targetPersona, prompt, targetSessionId }) => {
58045
+ forwardToPeer: async ({ targetDeviceId, targetPersona, prompt }) => {
58268
58046
  const contact = contactStore.get(targetDeviceId);
58269
58047
  if (!contact || !contact.remoteUrl || !contact.connectToken) {
58270
58048
  return {
58271
- outcome: {
58272
- kind: "failure",
58273
- reason: `unknown or unreachable contact: ${targetDeviceId}`
58274
- }
58049
+ kind: "failure",
58050
+ reason: `unknown or unreachable contact: ${targetDeviceId}`
58275
58051
  };
58276
58052
  }
58277
58053
  return forwardDispatchToPeer({
58278
58054
  contact: { remoteUrl: contact.remoteUrl, connectToken: contact.connectToken },
58279
58055
  targetPersona,
58280
- prompt,
58281
- targetSessionId
58056
+ prompt
58282
58057
  });
58283
58058
  },
58284
58059
  // B 角色:判断 targetPersona 是否 public(跨设备授权边界,private 拒)。
58285
- getPersonaPublic: (personaId) => personaRegistry.get(personaId)?.public ?? false,
58286
- // targetSessionId 复用路径的权限校验数据源:读 B session 的 SessionFile。
58287
- findOwnedSession: (sessionId) => manager.findOwnedSession(sessionId)
58060
+ getPersonaPublic: (personaId) => personaRegistry.get(personaId)?.public ?? false
58288
58061
  });
58289
58062
  handlers = { ...handlers, ...dispatchHandlers };
58290
58063
  const shiftHandlers = buildShiftInternalHandlers({