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

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
@@ -247,6 +247,7 @@ var init_events = __esm({
247
247
  SESSION_STATUS_VALUES = [
248
248
  "idle",
249
249
  "running",
250
+ "running-idle",
250
251
  "stopped",
251
252
  "error",
252
253
  "observing"
@@ -5996,7 +5997,7 @@ var init_feishu_auth = __esm({
5996
5997
  });
5997
5998
 
5998
5999
  // ../protocol/src/dispatch.ts
5999
- var DispatchOutcomeSchema, DispatchRunArgsSchema, DispatchRunResponseSchema, DispatchCompleteArgsSchema;
6000
+ var DispatchOutcomeSchema, DispatchRunArgsSchema, DispatchCompleteArgsSchema;
6000
6001
  var init_dispatch = __esm({
6001
6002
  "../protocol/src/dispatch.ts"() {
6002
6003
  "use strict";
@@ -6017,21 +6018,10 @@ var init_dispatch = __esm({
6017
6018
  prompt: external_exports.string(),
6018
6019
  // 跨设备 dispatch:非空 = A 角色,转发到该 contact deviceId 的 peer daemon;
6019
6020
  // 省略 = 本地 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()
6021
+ targetDeviceId: external_exports.string().min(1).optional()
6033
6022
  });
6034
6023
  DispatchCompleteArgsSchema = external_exports.object({
6024
+ dispatchId: external_exports.string().min(1),
6035
6025
  outcome: DispatchOutcomeSchema
6036
6026
  });
6037
6027
  }
@@ -41563,6 +41553,8 @@ function buildSpawnContext(state, deps) {
41563
41553
  env.CLAWD_SESSION_ID = file.sessionId;
41564
41554
  const daemonUrl = deps.getDaemonUrl?.() ?? null;
41565
41555
  if (daemonUrl) env.CLAWD_DAEMON_URL = daemonUrl;
41556
+ const dispatchId = deps.lookupDispatchByBSessionId?.(file.sessionId);
41557
+ if (dispatchId) env.CLAWD_DISPATCH_ID = dispatchId;
41566
41558
  const personaId = file.ownerPersonaId;
41567
41559
  if (personaId) env.CLAWD_PERSONA_ID = personaId;
41568
41560
  const dispatchMcpConfigPath2 = deps.getDispatchMcpConfigPath?.() ?? null;
@@ -42368,14 +42360,14 @@ var SessionRunner = class {
42368
42360
  // 单栏 refactor (spec 2026-06-02 §5.1): cc 子进程 env 注入 CLAWD_DAEMON_URL,让 assistant
42369
42361
  // curl daemon HTTP RPC adapter (/api/rpc/<method>) 触发管理操作。null = HTTP adapter 未启。
42370
42362
  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 配对。
42363
+ // Persona dispatch:透传 dispatch.mcp.json 路径闭包 + B→dispatchId 反查闭包。
42364
+ // reducer.buildSpawnContext CLAWD_DISPATCH_ID env / 派生 SpawnContext.dispatchMcpConfigPath
42374
42365
  getDispatchMcpConfigPath: this.hooks.getDispatchMcpConfigPath,
42375
42366
  getShiftMcpConfigPath: this.hooks.getShiftMcpConfigPath,
42376
42367
  getInboxMcpConfigPath: this.hooks.getInboxMcpConfigPath,
42377
42368
  // Ticket MCP:透传 ticket.mcp.json 路径闭包(reducer 内做 persona-ticket-manager gating)
42378
42369
  getTicketMcpConfigPath: this.hooks.getTicketMcpConfigPath,
42370
+ lookupDispatchByBSessionId: this.hooks.lookupDispatchByBSessionId,
42379
42371
  // ReadyGate v2:透传 mode 让 reducer send / ready-detected 分支决定走暂存队列还是直写
42380
42372
  mode: this.hooks.mode,
42381
42373
  // [RG-DBG] 注入 logger 让 reducer rgDbg 走 pino 进 clawd.log;定位完跟 rgDbg 一起删
@@ -42801,10 +42793,11 @@ function compressFrameForWire(frame) {
42801
42793
  switch (status) {
42802
42794
  case "spawning":
42803
42795
  case "running":
42804
- // sub-session(persona)专属内部状态:进程仍活着等 idle-kill;wire 协议看到的是 'running'
42805
- case "running-idle":
42806
42796
  compressed = "running";
42807
42797
  break;
42798
+ case "running-idle":
42799
+ compressed = "running-idle";
42800
+ break;
42808
42801
  case "stopping":
42809
42802
  case "stopped":
42810
42803
  compressed = "stopped";
@@ -42829,8 +42822,9 @@ function compressStatus(status) {
42829
42822
  switch (status) {
42830
42823
  case "spawning":
42831
42824
  case "running":
42832
- case "running-idle":
42833
42825
  return "running";
42826
+ case "running-idle":
42827
+ return "running-idle";
42834
42828
  case "stopping":
42835
42829
  case "stopped":
42836
42830
  return "stopped";
@@ -42859,22 +42853,9 @@ You may read this file with your Read tool to understand context. You do not hav
42859
42853
 
42860
42854
  When done, call the MCP tool \`mcp__clawd-dispatch__personaDispatchComplete\` with your result:
42861
42855
  - 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}
42871
-
42872
- Source conversation (jsonl, same path as before, may contain new messages):
42873
- ${args.sourceJsonlPath}
42856
+ - Failure: { isFailure: true, reason: "..." }
42874
42857
 
42875
- When done, call \`mcp__clawd-dispatch__personaDispatchComplete\` again with your result:
42876
- - Success: { text: "...", filePaths?: ["abs/path", ...] }
42877
- - Failure: { isFailure: true, reason: "..." }`;
42858
+ dispatchId for this task: ${args.dispatchId}`;
42878
42859
  }
42879
42860
  function derivePersonaSpawnCwd(file, personaRoot) {
42880
42861
  const personaId = file.ownerPersonaId;
@@ -43195,13 +43176,13 @@ var SessionManager = class {
43195
43176
  // 单栏 refactor (spec 2026-06-02 §5.1): 透传 daemon HTTP RPC base URL 闭包,
43196
43177
  // reducer 把它注入 cc 子进程 env CLAWD_DAEMON_URL.
43197
43178
  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 配对。
43179
+ // Persona dispatch (Task 7): dispatch.mcp.json 路径 + B sessionId → dispatchId 反查
43180
+ // 闭包透传给 reducer,让 cc spawn 加 --mcp-config flag + B session CLAWD_DISPATCH_ID env.
43201
43181
  getDispatchMcpConfigPath: this.deps.dispatchMcpConfigPath ? () => this.deps.dispatchMcpConfigPath ?? null : void 0,
43202
43182
  getTicketMcpConfigPath: this.deps.ticketMcpConfigPath ? () => this.deps.ticketMcpConfigPath ?? null : void 0,
43203
43183
  getShiftMcpConfigPath: this.deps.shiftMcpConfigPath ? () => this.deps.shiftMcpConfigPath ?? null : void 0,
43204
43184
  getInboxMcpConfigPath: this.deps.inboxMcpConfigPath ? () => this.deps.inboxMcpConfigPath ?? null : void 0,
43185
+ lookupDispatchByBSessionId: this.deps.personaDispatchManager ? (bSid) => this.deps.personaDispatchManager?.lookupDispatchByBSessionId(bSid) : void 0,
43205
43186
  // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
43206
43187
  onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
43207
43188
  scope,
@@ -44041,11 +44022,10 @@ var SessionManager = class {
44041
44022
  * Persona dispatch: 启动 B session 并投递任务包当第一条 user message。
44042
44023
  *
44043
44024
  * - 复用 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 回传
44025
+ * - SessionFile 上写 dispatchedFromSessionId 关联回 A(UI 折叠用;mirror 一侧 strip
44026
+ * - 跟 PersonaDispatchManager 双向登记 dispatchId ↔ B sessionId,让后续 reducer
44027
+ * buildSpawnContext 能反查到 dispatchId CLAWD_DISPATCH_ID env
44028
+ * - inject-owner-text 任务包;包尾教 B personaDispatchComplete tool 回传
44049
44029
  *
44050
44030
  * cwd 派生与 SessionManager.create 的 owner persona 路径一致(derivePersonaSpawnCwd)。
44051
44031
  */
@@ -44100,7 +44080,8 @@ var SessionManager = class {
44100
44080
  });
44101
44081
  const taskPack = buildDispatchTaskPack({
44102
44082
  prompt: args.prompt,
44103
- sourceJsonlPath: args.sourceJsonlPath
44083
+ sourceJsonlPath: args.sourceJsonlPath,
44084
+ dispatchId: args.dispatchId
44104
44085
  });
44105
44086
  runner.input({ kind: "command", command: { kind: "send", text: taskPack } });
44106
44087
  this.deps.logger?.info("dispatch.createDispatchedSession.task-pack-sent", {
@@ -44109,59 +44090,6 @@ var SessionManager = class {
44109
44090
  });
44110
44091
  return { sessionId };
44111
44092
  }
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
44093
  /**
44166
44094
  * shift v1 (spec 2026-06-24-clawd-shift):到点 fire 时由 ShiftScheduler 调,
44167
44095
  * 起一个新 cc session 跑 prompt(fire-and-forget,不绑 dispatchId / 不挂 caller)。
@@ -47568,7 +47496,6 @@ var PersonaDispatchManager = class {
47568
47496
  dispatchId,
47569
47497
  sourceSessionId: args.sourceSessionId,
47570
47498
  targetPersona: args.targetPersona,
47571
- bSessionId: args.bSessionId,
47572
47499
  waiters: [],
47573
47500
  outcome: null
47574
47501
  });
@@ -47599,22 +47526,10 @@ var PersonaDispatchManager = class {
47599
47526
  if (!state) throw new Error(`unknown dispatchId: ${dispatchId}`);
47600
47527
  state.bSessionId = bSessionId;
47601
47528
  }
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) {
47529
+ /** reducer buildSpawnContext 调,按 B 的 sessionId 反查 dispatchId(用于注 CLAWD_DISPATCH_ID env) */
47530
+ lookupDispatchByBSessionId(bSessionId) {
47614
47531
  for (const state of this.map.values()) {
47615
- if (state.bSessionId === bSessionId && state.outcome === null) {
47616
- return state.dispatchId;
47617
- }
47532
+ if (state.bSessionId === bSessionId) return state.dispatchId;
47618
47533
  }
47619
47534
  return void 0;
47620
47535
  }
@@ -48213,28 +48128,6 @@ function canAccessPersona(grants, personaId, action) {
48213
48128
  }
48214
48129
 
48215
48130
  // 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
48131
  function buildPersonaDispatchHandlers(deps) {
48239
48132
  const { personaDispatchManager: mgr, spawnB, logger } = deps;
48240
48133
  const run = async (frame, _client, ctx) => {
@@ -48253,21 +48146,15 @@ function buildPersonaDispatchHandlers(deps) {
48253
48146
  }
48254
48147
  logger?.info("dispatch.run.forward", {
48255
48148
  targetDeviceId: args.targetDeviceId,
48256
- targetPersona: args.targetPersona,
48257
- hasTargetSessionId: Boolean(args.targetSessionId)
48149
+ targetPersona: args.targetPersona
48258
48150
  });
48259
- const { outcome: outcome2, dispatchedSessionId } = await deps.forwardToPeer({
48151
+ const outcome2 = await deps.forwardToPeer({
48260
48152
  targetDeviceId: args.targetDeviceId,
48261
48153
  targetPersona: args.targetPersona,
48262
- prompt: args.prompt,
48263
- targetSessionId: args.targetSessionId
48154
+ prompt: args.prompt
48264
48155
  });
48265
48156
  return {
48266
- response: {
48267
- type: "personaDispatch:run:ok",
48268
- outcome: outcome2,
48269
- ...dispatchedSessionId ? { dispatchedSessionId } : {}
48270
- }
48157
+ response: { type: "personaDispatch:run:ok", outcome: outcome2 }
48271
48158
  };
48272
48159
  }
48273
48160
  if (ctx?.principal.kind === "guest") {
@@ -48278,65 +48165,34 @@ function buildPersonaDispatchHandlers(deps) {
48278
48165
  `persona not dispatchable: ${args.targetPersona}`
48279
48166
  );
48280
48167
  }
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
48168
  const { dispatchId: dispatchId2 } = mgr.start({
48300
- sourceSessionId: guestSourceId,
48301
- targetPersona: args.targetPersona,
48302
- bSessionId: bSessionId2
48169
+ sourceSessionId: ctx.principal.id,
48170
+ targetPersona: args.targetPersona
48303
48171
  });
48304
48172
  logger?.info("dispatch.run.received.guest", {
48305
48173
  dispatchId: dispatchId2,
48306
- sourcePrincipal: guestSourceId,
48174
+ sourcePrincipal: ctx.principal.id,
48307
48175
  targetPersona: args.targetPersona,
48308
- promptLen: args.prompt.length,
48309
- route: route2
48176
+ promptLen: args.prompt.length
48310
48177
  });
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) {
48178
+ void spawnB({
48179
+ dispatchId: dispatchId2,
48180
+ sourceSessionId: ctx.principal.id,
48181
+ targetPersona: args.targetPersona,
48182
+ prompt: args.prompt,
48183
+ guestPrincipalId: ctx.principal.id,
48184
+ guestDisplayName: ctx.principal.displayName
48185
+ }).then(() => logger?.info("dispatch.spawnB.ok", { dispatchId: dispatchId2 })).catch((err) => {
48326
48186
  const reason = err instanceof Error ? err.message : String(err);
48327
- logger?.warn("dispatch.spawnB.failed", { dispatchId: dispatchId2, route: route2, reason });
48187
+ logger?.warn("dispatch.spawnB.failed", { dispatchId: dispatchId2, reason });
48328
48188
  mgr.complete(dispatchId2, {
48329
48189
  kind: "failure",
48330
48190
  reason: `failed to spawn B: ${reason}`
48331
48191
  });
48332
- }
48192
+ });
48333
48193
  const outcome2 = await mgr.wait(dispatchId2);
48334
48194
  return {
48335
- response: {
48336
- type: "personaDispatch:run:ok",
48337
- outcome: outcome2,
48338
- ...resolvedBSessionId2 ? { dispatchedSessionId: resolvedBSessionId2 } : {}
48339
- }
48195
+ response: { type: "personaDispatch:run:ok", outcome: outcome2 }
48340
48196
  };
48341
48197
  }
48342
48198
  if (!sourceSessionId) {
@@ -48344,61 +48200,31 @@ function buildPersonaDispatchHandlers(deps) {
48344
48200
  "personaDispatch:run requires sessionId (caller must pass x-clawd-session-id header)"
48345
48201
  );
48346
48202
  }
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
48203
  const { dispatchId } = mgr.start({
48371
48204
  sourceSessionId,
48372
- targetPersona: args.targetPersona,
48373
- bSessionId
48205
+ targetPersona: args.targetPersona
48374
48206
  });
48375
48207
  logger?.info("dispatch.run.received", {
48376
48208
  dispatchId,
48377
48209
  sourceSessionId,
48378
48210
  targetPersona: args.targetPersona,
48379
- promptLen: args.prompt.length,
48380
- route
48211
+ promptLen: args.prompt.length
48381
48212
  });
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) {
48213
+ void spawnB({
48214
+ dispatchId,
48215
+ sourceSessionId,
48216
+ targetPersona: args.targetPersona,
48217
+ prompt: args.prompt
48218
+ }).then(() => {
48219
+ logger?.info("dispatch.spawnB.ok", { dispatchId });
48220
+ }).catch((err) => {
48395
48221
  const reason = err instanceof Error ? err.message : String(err);
48396
- logger?.warn("dispatch.spawnB.failed", { dispatchId, route, reason });
48222
+ logger?.warn("dispatch.spawnB.failed", { dispatchId, reason });
48397
48223
  mgr.complete(dispatchId, {
48398
48224
  kind: "failure",
48399
48225
  reason: `failed to spawn B: ${reason}`
48400
48226
  });
48401
- }
48227
+ });
48402
48228
  logger?.info("dispatch.run.waiting", { dispatchId });
48403
48229
  const outcome = await mgr.wait(dispatchId);
48404
48230
  logger?.info("dispatch.run.resolved", {
@@ -48406,34 +48232,17 @@ function buildPersonaDispatchHandlers(deps) {
48406
48232
  outcomeKind: outcome.kind
48407
48233
  });
48408
48234
  return {
48409
- response: {
48410
- type: "personaDispatch:run:ok",
48411
- outcome,
48412
- ...resolvedBSessionId ? { dispatchedSessionId: resolvedBSessionId } : {}
48413
- }
48235
+ response: { type: "personaDispatch:run:ok", outcome }
48414
48236
  };
48415
48237
  };
48416
48238
  const complete = async (frame) => {
48417
48239
  const { type: _t, requestId: _r, ...rest } = frame;
48418
- const sessionId = typeof rest.sessionId === "string" ? rest.sessionId : void 0;
48419
48240
  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
48241
  logger?.info("dispatch.complete.received", {
48432
- dispatchId,
48433
- sessionId,
48242
+ dispatchId: args.dispatchId,
48434
48243
  outcomeKind: args.outcome.kind
48435
48244
  });
48436
- mgr.complete(dispatchId, args.outcome);
48245
+ mgr.complete(args.dispatchId, args.outcome);
48437
48246
  return {
48438
48247
  response: { type: "personaDispatch:complete:ok" }
48439
48248
  };
@@ -48463,37 +48272,25 @@ async function forwardDispatchToPeer(args) {
48463
48272
  authorization: `Bearer ${args.contact.connectToken}`
48464
48273
  },
48465
48274
  // 注意:不带 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
- })
48275
+ body: JSON.stringify({ targetPersona: args.targetPersona, prompt: args.prompt })
48472
48276
  });
48473
48277
  } catch (err) {
48474
48278
  const msg = err instanceof Error ? err.message : String(err);
48475
- return { outcome: { kind: "failure", reason: `forward to peer failed: ${msg}` } };
48279
+ return { kind: "failure", reason: `forward to peer failed: ${msg}` };
48476
48280
  }
48477
48281
  let json;
48478
48282
  try {
48479
48283
  json = await res.json();
48480
48284
  } catch {
48481
48285
  return {
48482
- outcome: {
48483
- kind: "failure",
48484
- reason: `peer returned non-JSON response (HTTP ${res.status})`
48485
- }
48286
+ kind: "failure",
48287
+ reason: `peer returned non-JSON response (HTTP ${res.status})`
48486
48288
  };
48487
48289
  }
48488
48290
  if (json.ok === false) {
48489
- return {
48490
- outcome: { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` }
48491
- };
48291
+ return { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` };
48492
48292
  }
48493
- return {
48494
- outcome: json.result.outcome,
48495
- dispatchedSessionId: json.result.dispatchedSessionId
48496
- };
48293
+ return json.result.outcome;
48497
48294
  }
48498
48295
  async function forwardInboxPostToPeer(args) {
48499
48296
  const f = args.fetchImpl ?? fetch;
@@ -57831,9 +57628,9 @@ async function startDaemon(config) {
57831
57628
  // 127.0.0.1(不是 config.host)—— cc 跑在本机,loopback 最稳;外部访问限制 + http-router
57832
57629
  // 的 isLoopback 兜底已确保安全。
57833
57630
  getDaemonUrl: () => `http://127.0.0.1:${config.port}`,
57834
- // Persona dispatch: manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
57835
- // - personaDispatchManager: createDispatchedSession / resumeDispatchedSession 用它
57836
- // registerBSession;complete handler findInflightDispatchByBSessionId 反查回 dispatchId
57631
+ // Persona dispatch (Task 8): manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
57632
+ // - personaDispatchManager: manager.createDispatchedSession 用它 registerBSession;
57633
+ // reducer 通过闭包反查 dispatchId CLAWD_DISPATCH_ID env
57837
57634
  // - dispatchMcpConfigPath: reducer 透传到 SpawnContext,cc spawn 加 --mcp-config flag。
57838
57635
  personaDispatchManager,
57839
57636
  dispatchMcpConfigPath: dispatchMcpConfigPath2,
@@ -58215,8 +58012,7 @@ async function startDaemon(config) {
58215
58012
  logger.info("dispatch.spawnB.start", {
58216
58013
  dispatchId: args.dispatchId,
58217
58014
  targetPersona: args.targetPersona,
58218
- sourceSessionId: args.sourceSessionId,
58219
- route: args.route
58015
+ sourceSessionId: args.sourceSessionId
58220
58016
  });
58221
58017
  const sourceFile = manager.findOwnedSession(args.sourceSessionId);
58222
58018
  if (!sourceFile && !args.guestPrincipalId) {
@@ -58235,23 +58031,9 @@ async function startDaemon(config) {
58235
58031
  logger.info("dispatch.spawnB.source-resolved", {
58236
58032
  dispatchId: args.dispatchId,
58237
58033
  sourceJsonlPath,
58238
- hasToolSessionId: Boolean(sourceFile?.toolSessionId),
58239
- route: args.route
58034
+ hasToolSessionId: Boolean(sourceFile?.toolSessionId)
58240
58035
  });
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({
58036
+ manager.createDispatchedSession({
58255
58037
  dispatchId: args.dispatchId,
58256
58038
  sourceSessionId: args.sourceSessionId,
58257
58039
  targetPersona: args.targetPersona,
@@ -58261,30 +58043,24 @@ async function startDaemon(config) {
58261
58043
  guestPrincipalId: args.guestPrincipalId,
58262
58044
  guestDisplayName: args.guestDisplayName
58263
58045
  });
58264
- return { bSessionId: sessionId };
58265
58046
  },
58266
58047
  // A 角色:从 ContactStore 取 peer 可达 URL + connect token,转发到对端 daemon /rpc。
58267
- forwardToPeer: async ({ targetDeviceId, targetPersona, prompt, targetSessionId }) => {
58048
+ forwardToPeer: async ({ targetDeviceId, targetPersona, prompt }) => {
58268
58049
  const contact = contactStore.get(targetDeviceId);
58269
58050
  if (!contact || !contact.remoteUrl || !contact.connectToken) {
58270
58051
  return {
58271
- outcome: {
58272
- kind: "failure",
58273
- reason: `unknown or unreachable contact: ${targetDeviceId}`
58274
- }
58052
+ kind: "failure",
58053
+ reason: `unknown or unreachable contact: ${targetDeviceId}`
58275
58054
  };
58276
58055
  }
58277
58056
  return forwardDispatchToPeer({
58278
58057
  contact: { remoteUrl: contact.remoteUrl, connectToken: contact.connectToken },
58279
58058
  targetPersona,
58280
- prompt,
58281
- targetSessionId
58059
+ prompt
58282
58060
  });
58283
58061
  },
58284
58062
  // B 角色:判断 targetPersona 是否 public(跨设备授权边界,private 拒)。
58285
- getPersonaPublic: (personaId) => personaRegistry.get(personaId)?.public ?? false,
58286
- // targetSessionId 复用路径的权限校验数据源:读 B session 的 SessionFile。
58287
- findOwnedSession: (sessionId) => manager.findOwnedSession(sessionId)
58063
+ getPersonaPublic: (personaId) => personaRegistry.get(personaId)?.public ?? false
58288
58064
  });
58289
58065
  handlers = { ...handlers, ...dispatchHandlers };
58290
58066
  const shiftHandlers = buildShiftInternalHandlers({