@clawos-dev/clawd 0.2.141 → 0.2.142-beta.297.e4b56a8

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
@@ -5952,10 +5952,12 @@ var init_dispatch = __esm({
5952
5952
  ]);
5953
5953
  DispatchRunArgsSchema = external_exports.object({
5954
5954
  targetPersona: external_exports.string().min(1),
5955
- prompt: external_exports.string()
5955
+ prompt: external_exports.string(),
5956
+ // mode='continue' 时 daemon 反查 (sourceSessionId, targetPersona) 最近 B session 复用;
5957
+ // 'new' 或 undefined(默认)= 当前行为,新建 B。
5958
+ mode: external_exports.enum(["new", "continue"]).optional()
5956
5959
  });
5957
5960
  DispatchCompleteArgsSchema = external_exports.object({
5958
- dispatchId: external_exports.string().min(1),
5959
5961
  outcome: DispatchOutcomeSchema
5960
5962
  });
5961
5963
  }
@@ -31802,8 +31804,6 @@ function buildSpawnContext(state, deps) {
31802
31804
  env.CLAWD_SESSION_ID = file.sessionId;
31803
31805
  const daemonUrl = deps.getDaemonUrl?.() ?? null;
31804
31806
  if (daemonUrl) env.CLAWD_DAEMON_URL = daemonUrl;
31805
- const dispatchId = deps.lookupDispatchByBSessionId?.(file.sessionId);
31806
- if (dispatchId) env.CLAWD_DISPATCH_ID = dispatchId;
31807
31807
  const dispatchMcpConfigPath2 = deps.getDispatchMcpConfigPath?.() ?? null;
31808
31808
  const ctx = {
31809
31809
  cwd: file.cwd,
@@ -32566,10 +32566,9 @@ var SessionRunner = class {
32566
32566
  // 单栏 refactor (spec 2026-06-02 §5.1): cc 子进程 env 注入 CLAWD_DAEMON_URL,让 assistant
32567
32567
  // curl daemon HTTP RPC adapter (/api/rpc/<method>) 触发管理操作。null = HTTP adapter 未启。
32568
32568
  getDaemonUrl: this.hooks.getDaemonUrl,
32569
- // Persona dispatch:透传 dispatch.mcp.json 路径闭包 + B→dispatchId 反查闭包。
32570
- // reducer.buildSpawnContext CLAWD_DISPATCH_ID env / 派生 SpawnContext.dispatchMcpConfigPath
32569
+ // Persona dispatch:透传 dispatch.mcp.json 路径闭包;cc spawn --mcp-config 让两侧
32570
+ // cc 都能看到 personaDispatch / personaDispatchComplete tool(按 session 身份区分用哪个)。
32571
32571
  getDispatchMcpConfigPath: this.hooks.getDispatchMcpConfigPath,
32572
- lookupDispatchByBSessionId: this.hooks.lookupDispatchByBSessionId,
32573
32572
  // ReadyGate v2:透传 mode 让 reducer send / ready-detected 分支决定走暂存队列还是直写
32574
32573
  mode: this.hooks.mode,
32575
32574
  // [RG-DBG] 注入 logger 让 reducer rgDbg 走 pino 进 clawd.log;定位完跟 rgDbg 一起删
@@ -33041,9 +33040,22 @@ You may read this file with your Read tool to understand context. You do not hav
33041
33040
 
33042
33041
  When done, call the MCP tool \`mcp__clawd-dispatch__personaDispatchComplete\` with your result:
33043
33042
  - Success: { text: "...", filePaths?: ["abs/path", ...] }
33044
- - Failure: { isFailure: true, reason: "..." }
33043
+ - Failure: { isFailure: true, reason: "..." }`;
33044
+ }
33045
+ function buildDispatchContinuationPack(args) {
33046
+ return `[Dispatched from owner \u2014 follow-up query]
33047
+
33048
+ Although you reported back on the previous task, the owner has a follow-up question. Continue from where you left off.
33045
33049
 
33046
- dispatchId for this task: ${args.dispatchId}`;
33050
+ Owner's new message:
33051
+ ${args.prompt}
33052
+
33053
+ Source conversation (jsonl, same path as before, may contain new messages):
33054
+ ${args.sourceJsonlPath}
33055
+
33056
+ When done, call \`mcp__clawd-dispatch__personaDispatchComplete\` again with your result:
33057
+ - Success: { text: "...", filePaths?: ["abs/path", ...] }
33058
+ - Failure: { isFailure: true, reason: "..." }`;
33047
33059
  }
33048
33060
  function derivePersonaSpawnCwd(file, personaRoot) {
33049
33061
  const personaId = file.ownerPersonaId;
@@ -33245,6 +33257,23 @@ var SessionManager = class {
33245
33257
  const file = this.findOwnedSession(sessionId);
33246
33258
  return file ? this.scopeForFile(file) : null;
33247
33259
  }
33260
+ /**
33261
+ * 反查 (sourceSessionId, targetPersona) 最近一次被 dispatched 出的 B session。
33262
+ * mode='continue' 路径用:命中 → 复用该 B;不命中 → fallback 走新建路径。
33263
+ *
33264
+ * 反查源是 SessionStore 持久化字段(dispatchedFromSessionId + ownerPersonaId),不引入新表,
33265
+ * daemon 重启不丢。同对历史多条按 updatedAt 取最近(reducer 写入时持续更新)。
33266
+ */
33267
+ findLatestDispatchedSession(args) {
33268
+ const candidates = this.listAllOwned().filter(
33269
+ (f) => f.dispatchedFromSessionId === args.sourceSessionId && f.ownerPersonaId === args.targetPersona
33270
+ );
33271
+ if (candidates.length === 0) return null;
33272
+ candidates.sort(
33273
+ (a, b2) => a.updatedAt > b2.updatedAt ? -1 : a.updatedAt < b2.updatedAt ? 1 : 0
33274
+ );
33275
+ return candidates[0];
33276
+ }
33248
33277
  // 合并 default + 每个 persona 的 owner/ + guests/<capId>/ —— 桌面 App 主 session 列表入口.
33249
33278
  // guest sessions 由 handlers/session.ts list 处按 ctx.principal.kind === 'guest' 做权限过滤
33250
33279
  // (creatorPrincipalId === capId), 这里仅负责数据源汇总.
@@ -33351,10 +33380,9 @@ var SessionManager = class {
33351
33380
  // 单栏 refactor (spec 2026-06-02 §5.1): 透传 daemon HTTP RPC base URL 闭包,
33352
33381
  // reducer 把它注入 cc 子进程 env CLAWD_DAEMON_URL.
33353
33382
  getDaemonUrl: this.deps.getDaemonUrl,
33354
- // Persona dispatch (Task 7): dispatch.mcp.json 路径 + B sessionId dispatchId 反查
33355
- // 闭包透传给 reducer,让 cc spawn --mcp-config flag + B session 注 CLAWD_DISPATCH_ID env.
33383
+ // Persona dispatch: dispatch.mcp.json 路径闭包透传给 reducer,让 cc spawn --mcp-config flag。
33384
+ // dispatchId 不再走 cc env——complete handler sessionId 反查 in-flight dispatchId 配对。
33356
33385
  getDispatchMcpConfigPath: this.deps.dispatchMcpConfigPath ? () => this.deps.dispatchMcpConfigPath ?? null : void 0,
33357
- lookupDispatchByBSessionId: this.deps.personaDispatchManager ? (bSid) => this.deps.personaDispatchManager?.lookupDispatchByBSessionId(bSid) : void 0,
33358
33386
  // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
33359
33387
  onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
33360
33388
  scope,
@@ -34172,10 +34200,11 @@ var SessionManager = class {
34172
34200
  * Persona dispatch: 启动 B session 并投递任务包当第一条 user message。
34173
34201
  *
34174
34202
  * - 复用 persona-owner 创建路径(cwd=personaDir、scope=persona-owner、不沙箱)
34175
- * - SessionFile 上写 dispatchedFromSessionId 关联回 A(UI 折叠用;mirror 一侧 strip
34176
- * - PersonaDispatchManager 双向登记 dispatchId ↔ B sessionId,让后续 reducer
34177
- * buildSpawnContext 能反查到 dispatchId CLAWD_DISPATCH_ID env
34178
- * - inject-owner-text 任务包;包尾教 B personaDispatchComplete tool 回传
34203
+ * - SessionFile 上写 dispatchedFromSessionId 关联回 A(UI 折叠用;mirror 一侧 strip
34204
+ * mode='continue' 路径 findLatestDispatchedSession 也按此字段反查复用)
34205
+ * - PersonaDispatchManager 登记 dispatchId B sessionId(complete handler 用
34206
+ * findInflightDispatchByBSessionId 反查回 dispatchId 配对 A waiter)
34207
+ * - 投任务包;包尾教 B 用 personaDispatchComplete tool 回传
34179
34208
  *
34180
34209
  * cwd 派生与 SessionManager.create 的 owner persona 路径一致(derivePersonaSpawnCwd)。
34181
34210
  */
@@ -34218,8 +34247,7 @@ var SessionManager = class {
34218
34247
  });
34219
34248
  const taskPack = buildDispatchTaskPack({
34220
34249
  prompt: args.prompt,
34221
- sourceJsonlPath: args.sourceJsonlPath,
34222
- dispatchId: args.dispatchId
34250
+ sourceJsonlPath: args.sourceJsonlPath
34223
34251
  });
34224
34252
  runner.input({ kind: "command", command: { kind: "send", text: taskPack } });
34225
34253
  this.deps.logger?.info("dispatch.createDispatchedSession.task-pack-sent", {
@@ -34228,6 +34256,41 @@ var SessionManager = class {
34228
34256
  });
34229
34257
  return { sessionId };
34230
34258
  }
34259
+ /**
34260
+ * mode='continue' 复用已有 B session,投 continuation pack 给 runner。
34261
+ *
34262
+ * 关键差异 vs createDispatchedSession:
34263
+ * - 不 newSessionId() / 不 storeFor(scope).write()——SessionFile 复用
34264
+ * - 跟 PersonaDispatchManager 重新登记 dispatchId → bSessionId(新 dispatchId、同 bSessionId)
34265
+ * - ensureRunnerForScope 在 runner 不存在时建;存在则复用
34266
+ * - runner.input send 投 continuationPack:runner / reducer 内部决定 cc 死活;
34267
+ * cc 活 → 自然续;cc 死 → spawn 时 jsonl 已存在自动 --resume <toolSessionId>
34268
+ */
34269
+ resumeDispatchedSession(args) {
34270
+ if (!this.deps.personaDispatchManager) {
34271
+ throw new Error("resumeDispatchedSession: personaDispatchManager missing in ManagerDeps");
34272
+ }
34273
+ const file = this.findOwnedSession(args.bSessionId);
34274
+ if (!file) {
34275
+ throw new Error(`resumeDispatchedSession: B session ${args.bSessionId} not found`);
34276
+ }
34277
+ const scope = this.scopeForFile(file);
34278
+ this.deps.personaDispatchManager.registerBSession(args.dispatchId, args.bSessionId);
34279
+ this.deps.logger?.info("dispatch.resumeDispatchedSession.registered", {
34280
+ dispatchId: args.dispatchId,
34281
+ bSessionId: args.bSessionId
34282
+ });
34283
+ const runner = this.ensureRunnerForScope(file, scope);
34284
+ const pack = buildDispatchContinuationPack({
34285
+ prompt: args.prompt,
34286
+ sourceJsonlPath: args.sourceJsonlPath
34287
+ });
34288
+ runner.input({ kind: "command", command: { kind: "send", text: pack } });
34289
+ this.deps.logger?.info("dispatch.resumeDispatchedSession.continuation-sent", {
34290
+ dispatchId: args.dispatchId,
34291
+ bSessionId: args.bSessionId
34292
+ });
34293
+ }
34231
34294
  /**
34232
34295
  * 老板插话:把 'inject-owner-text' input 喂给对应 runner,
34233
34296
  * runner 不存在时按 scope 懒创建。
@@ -36929,6 +36992,7 @@ var PersonaDispatchManager = class {
36929
36992
  dispatchId,
36930
36993
  sourceSessionId: args.sourceSessionId,
36931
36994
  targetPersona: args.targetPersona,
36995
+ bSessionId: args.bSessionId,
36932
36996
  waiters: [],
36933
36997
  outcome: null
36934
36998
  });
@@ -36959,10 +37023,22 @@ var PersonaDispatchManager = class {
36959
37023
  if (!state) throw new Error(`unknown dispatchId: ${dispatchId}`);
36960
37024
  state.bSessionId = bSessionId;
36961
37025
  }
36962
- /** reducer buildSpawnContext 调,按 B 的 sessionId 反查 dispatchId(用于注 CLAWD_DISPATCH_ID env) */
36963
- lookupDispatchByBSessionId(bSessionId) {
37026
+ /**
37027
+ * 按 B sessionId 找当前 in-flight(未 resolved)的 dispatchId。
37028
+ * complete handler 用这个把 B 的 outcome 配对回 A 的 waiter——dispatchId 不再走 cc env,
37029
+ * 全靠 (bSessionId → in-flight dispatchId) 反查。
37030
+ *
37031
+ * 同一 bSessionId 不会同时有多个 in-flight(A 拿不到 outcome 不会发起新 dispatch;
37032
+ * mode='continue' 入口已防御重入)。
37033
+ *
37034
+ * 历史 entry(已 resolved)暂不清——单 daemon lifetime 累积可接受;未来用量大了加 TTL GC
37035
+ * (complete 后 5min 删 entry)。
37036
+ */
37037
+ findInflightDispatchByBSessionId(bSessionId) {
36964
37038
  for (const state of this.map.values()) {
36965
- if (state.bSessionId === bSessionId) return state.dispatchId;
37039
+ if (state.bSessionId === bSessionId && state.outcome === null) {
37040
+ return state.dispatchId;
37041
+ }
36966
37042
  }
36967
37043
  return void 0;
36968
37044
  }
@@ -36994,6 +37070,15 @@ function writeDispatchMcpConfig(args) {
36994
37070
 
36995
37071
  // src/handlers/persona-dispatch.ts
36996
37072
  init_protocol();
37073
+ var NO_PREVIOUS_PREFIX = "(no previous session found, started fresh)";
37074
+ function prefixNoPreviousOutcome(outcome) {
37075
+ if (outcome.kind === "success") {
37076
+ return { ...outcome, text: `${NO_PREVIOUS_PREFIX}
37077
+
37078
+ ${outcome.text}` };
37079
+ }
37080
+ return { ...outcome, reason: `${NO_PREVIOUS_PREFIX} ${outcome.reason}` };
37081
+ }
36997
37082
  function buildPersonaDispatchHandlers(deps) {
36998
37083
  const { personaDispatchManager: mgr, spawnB, logger } = deps;
36999
37084
  const run = async (frame) => {
@@ -37005,26 +37090,66 @@ function buildPersonaDispatchHandlers(deps) {
37005
37090
  "personaDispatch:run requires sessionId (caller must pass x-clawd-session-id header)"
37006
37091
  );
37007
37092
  }
37093
+ const requestedMode = args.mode ?? "new";
37094
+ let route = "new";
37095
+ let bSessionId;
37096
+ let noPrevious = false;
37097
+ if (requestedMode === "continue") {
37098
+ const bFile = deps.findLatestDispatchedSession?.({
37099
+ sourceSessionId,
37100
+ targetPersona: args.targetPersona
37101
+ });
37102
+ if (bFile) {
37103
+ const inflight = deps.findInflightDispatchByBSessionId?.(bFile.sessionId);
37104
+ if (inflight) {
37105
+ logger?.warn("dispatch.run.continue.busy", {
37106
+ sourceSessionId,
37107
+ targetPersona: args.targetPersona,
37108
+ bSessionId: bFile.sessionId,
37109
+ inflightDispatchId: inflight
37110
+ });
37111
+ return {
37112
+ response: {
37113
+ type: "personaDispatch:run:ok",
37114
+ outcome: {
37115
+ kind: "failure",
37116
+ reason: "previous dispatch to this persona session not yet complete"
37117
+ }
37118
+ }
37119
+ };
37120
+ }
37121
+ route = "continue";
37122
+ bSessionId = bFile.sessionId;
37123
+ } else {
37124
+ noPrevious = true;
37125
+ }
37126
+ }
37008
37127
  const { dispatchId } = mgr.start({
37009
37128
  sourceSessionId,
37010
- targetPersona: args.targetPersona
37129
+ targetPersona: args.targetPersona,
37130
+ bSessionId
37011
37131
  });
37012
37132
  logger?.info("dispatch.run.received", {
37013
37133
  dispatchId,
37014
37134
  sourceSessionId,
37015
37135
  targetPersona: args.targetPersona,
37016
- promptLen: args.prompt.length
37136
+ promptLen: args.prompt.length,
37137
+ requestedMode,
37138
+ route,
37139
+ noPrevious
37017
37140
  });
37018
37141
  void spawnB({
37019
37142
  dispatchId,
37020
37143
  sourceSessionId,
37021
37144
  targetPersona: args.targetPersona,
37022
- prompt: args.prompt
37145
+ prompt: args.prompt,
37146
+ route,
37147
+ bSessionId
37023
37148
  }).then(() => {
37024
- logger?.info("dispatch.spawnB.ok", { dispatchId });
37149
+ logger?.info("dispatch.spawnB.ok", { dispatchId, route });
37025
37150
  }).catch((err) => {
37026
37151
  const reason = err instanceof Error ? err.message : String(err);
37027
- logger?.warn("dispatch.spawnB.failed", { dispatchId, reason });
37152
+ logger?.warn("dispatch.spawnB.failed", { dispatchId, route, reason });
37028
37153
  mgr.complete(dispatchId, {
37029
37154
  kind: "failure",
37030
37155
  reason: `failed to spawn B: ${reason}`
@@ -37036,18 +37161,32 @@ function buildPersonaDispatchHandlers(deps) {
37036
37161
  dispatchId,
37037
37162
  outcomeKind: outcome.kind
37038
37163
  });
37164
+ const finalOutcome = noPrevious ? prefixNoPreviousOutcome(outcome) : outcome;
37039
37165
  return {
37040
- response: { type: "personaDispatch:run:ok", outcome }
37166
+ response: { type: "personaDispatch:run:ok", outcome: finalOutcome }
37041
37167
  };
37042
37168
  };
37043
37169
  const complete = async (frame) => {
37044
37170
  const { type: _t, requestId: _r, ...rest } = frame;
37171
+ const sessionId = typeof rest.sessionId === "string" ? rest.sessionId : void 0;
37045
37172
  const args = DispatchCompleteArgsSchema.parse(rest);
37173
+ if (!sessionId) {
37174
+ throw new Error(
37175
+ "personaDispatch:complete requires sessionId (caller must pass x-clawd-session-id header)"
37176
+ );
37177
+ }
37178
+ const dispatchId = mgr.findInflightDispatchByBSessionId(sessionId);
37179
+ if (!dispatchId) {
37180
+ throw new Error(
37181
+ `no in-flight dispatch for session ${sessionId} (already completed or B was never dispatched)`
37182
+ );
37183
+ }
37046
37184
  logger?.info("dispatch.complete.received", {
37047
- dispatchId: args.dispatchId,
37185
+ dispatchId,
37186
+ sessionId,
37048
37187
  outcomeKind: args.outcome.kind
37049
37188
  });
37050
- mgr.complete(args.dispatchId, args.outcome);
37189
+ mgr.complete(dispatchId, args.outcome);
37051
37190
  return {
37052
37191
  response: { type: "personaDispatch:complete:ok" }
37053
37192
  };
@@ -45723,9 +45862,9 @@ async function startDaemon(config) {
45723
45862
  // 127.0.0.1(不是 config.host)—— cc 跑在本机,loopback 最稳;外部访问限制 + http-router
45724
45863
  // 的 isLoopback 兜底已确保安全。
45725
45864
  getDaemonUrl: () => `http://127.0.0.1:${config.port}`,
45726
- // Persona dispatch (Task 8): manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
45727
- // - personaDispatchManager: manager.createDispatchedSession 用它 registerBSession;
45728
- // reducer 通过闭包反查 dispatchId CLAWD_DISPATCH_ID env
45865
+ // Persona dispatch: manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
45866
+ // - personaDispatchManager: createDispatchedSession / resumeDispatchedSession 用它
45867
+ // registerBSession;complete handler findInflightDispatchByBSessionId 反查回 dispatchId
45729
45868
  // - dispatchMcpConfigPath: reducer 透传到 SpawnContext,cc spawn 加 --mcp-config flag。
45730
45869
  personaDispatchManager,
45731
45870
  dispatchMcpConfigPath: dispatchMcpConfigPath2,
@@ -46054,11 +46193,16 @@ async function startDaemon(config) {
46054
46193
  const dispatchHandlers = buildPersonaDispatchHandlers({
46055
46194
  personaDispatchManager,
46056
46195
  logger,
46196
+ // continue 路径反查(命中 → 走 resumeDispatchedSession;不命中 → fallback 新建 + outcome 前缀)
46197
+ findLatestDispatchedSession: (q) => manager.findLatestDispatchedSession(q),
46198
+ // continue 路径防御:B 仍有 in-flight 时直接 fail(老板规则不会触发,但兜底)
46199
+ findInflightDispatchByBSessionId: (sid) => personaDispatchManager.findInflightDispatchByBSessionId(sid),
46057
46200
  spawnB: async (args) => {
46058
46201
  logger.info("dispatch.spawnB.start", {
46059
46202
  dispatchId: args.dispatchId,
46060
46203
  targetPersona: args.targetPersona,
46061
- sourceSessionId: args.sourceSessionId
46204
+ sourceSessionId: args.sourceSessionId,
46205
+ route: args.route
46062
46206
  });
46063
46207
  const sourceFile = manager.findOwnedSession(args.sourceSessionId);
46064
46208
  if (!sourceFile) {
@@ -46074,15 +46218,28 @@ async function startDaemon(config) {
46074
46218
  logger.info("dispatch.spawnB.source-resolved", {
46075
46219
  dispatchId: args.dispatchId,
46076
46220
  sourceJsonlPath,
46077
- hasToolSessionId: Boolean(sourceFile.toolSessionId)
46078
- });
46079
- manager.createDispatchedSession({
46080
- dispatchId: args.dispatchId,
46081
- sourceSessionId: args.sourceSessionId,
46082
- targetPersona: args.targetPersona,
46083
- prompt: args.prompt,
46084
- sourceJsonlPath
46221
+ hasToolSessionId: Boolean(sourceFile.toolSessionId),
46222
+ route: args.route
46085
46223
  });
46224
+ if (args.route === "continue") {
46225
+ if (!args.bSessionId) {
46226
+ throw new Error("continue route requires bSessionId");
46227
+ }
46228
+ manager.resumeDispatchedSession({
46229
+ dispatchId: args.dispatchId,
46230
+ bSessionId: args.bSessionId,
46231
+ prompt: args.prompt,
46232
+ sourceJsonlPath
46233
+ });
46234
+ } else {
46235
+ manager.createDispatchedSession({
46236
+ dispatchId: args.dispatchId,
46237
+ sourceSessionId: args.sourceSessionId,
46238
+ targetPersona: args.targetPersona,
46239
+ prompt: args.prompt,
46240
+ sourceJsonlPath
46241
+ });
46242
+ }
46086
46243
  }
46087
46244
  });
46088
46245
  handlers = { ...handlers, ...dispatchHandlers };
@@ -39574,9 +39574,11 @@ async function handlePersonaDispatchToolCall(input, ctx) {
39574
39574
  "content-type": "application/json",
39575
39575
  "x-clawd-session-id": ctx.sessionId
39576
39576
  },
39577
+ // input.mode === undefined → JSON.stringify omit;daemon parse 用 .optional() 接住缺省。
39577
39578
  body: JSON.stringify({
39578
39579
  targetPersona: input.targetPersona,
39579
- prompt: input.prompt
39580
+ prompt: input.prompt,
39581
+ mode: input.mode
39580
39582
  })
39581
39583
  });
39582
39584
  } catch (err) {
@@ -39626,16 +39628,16 @@ async function handlePersonaDispatchCompleteToolCall(input, ctx) {
39626
39628
  isFailure: Boolean(input.isFailure),
39627
39629
  textLen: input.text?.length ?? 0,
39628
39630
  filePathCount: input.filePaths?.length ?? 0,
39629
- dispatchId: ctx.dispatchId,
39631
+ sessionId: ctx.sessionId,
39630
39632
  daemonUrl: ctx.daemonUrl
39631
39633
  });
39632
- if (!ctx.dispatchId) {
39633
- log("personaDispatchComplete CLAWD_DISPATCH_ID missing");
39634
+ if (!ctx.sessionId) {
39635
+ log("personaDispatchComplete CLAWD_SESSION_ID missing");
39634
39636
  return {
39635
39637
  content: [
39636
39638
  {
39637
39639
  type: "text",
39638
- text: "Error: CLAWD_DISPATCH_ID env not set; this session was not dispatched."
39640
+ text: "Error: CLAWD_SESSION_ID env not set; daemon cannot resolve in-flight dispatch without sessionId."
39639
39641
  }
39640
39642
  ],
39641
39643
  isError: true
@@ -39650,14 +39652,15 @@ async function handlePersonaDispatchCompleteToolCall(input, ctx) {
39650
39652
  filePaths: input.filePaths
39651
39653
  };
39652
39654
  const url = `${ctx.daemonUrl}/api/rpc/personaDispatch.complete`;
39653
- log("personaDispatchComplete pre-fetch", { url, hasSessionId: Boolean(ctx.sessionId) });
39654
- const headers = { "content-type": "application/json" };
39655
- if (ctx.sessionId) headers["x-clawd-session-id"] = ctx.sessionId;
39655
+ log("personaDispatchComplete pre-fetch", { url, sessionId: ctx.sessionId });
39656
39656
  try {
39657
39657
  const res = await f(url, {
39658
39658
  method: "POST",
39659
- headers,
39660
- body: JSON.stringify({ dispatchId: ctx.dispatchId, outcome })
39659
+ headers: {
39660
+ "content-type": "application/json",
39661
+ "x-clawd-session-id": ctx.sessionId
39662
+ },
39663
+ body: JSON.stringify({ outcome })
39661
39664
  });
39662
39665
  log("personaDispatchComplete post-fetch", { url, status: res.status });
39663
39666
  } catch (err) {
@@ -39681,12 +39684,10 @@ async function handlePersonaDispatchCompleteToolCall(input, ctx) {
39681
39684
  async function main() {
39682
39685
  const daemonUrl = process.env.CLAWD_DAEMON_URL;
39683
39686
  const sessionId = process.env.CLAWD_SESSION_ID;
39684
- const dispatchId = process.env.CLAWD_DISPATCH_ID;
39685
39687
  log("boot", {
39686
39688
  pid: process.pid,
39687
39689
  hasDaemonUrl: Boolean(daemonUrl),
39688
39690
  hasSessionId: Boolean(sessionId),
39689
- hasDispatchId: Boolean(dispatchId),
39690
39691
  logPath: DISPATCH_LOG ?? "(stderr)"
39691
39692
  });
39692
39693
  if (!daemonUrl) {
@@ -39703,11 +39704,14 @@ async function main() {
39703
39704
  "personaDispatch",
39704
39705
  {
39705
39706
  title: "Dispatch task to another persona",
39706
- description: "Delegate a task to another clawd persona. Use this when the user message contains `@persona/<id>` to hand the task off to that persona. The tool blocks until the other persona finishes and returns its result.",
39707
+ description: "Delegate a task to another clawd persona. Use this when the user message contains `@persona/<id>` to hand the task off to that persona. The tool blocks until the other persona finishes and returns its result. Pass mode='continue' when the user's follow-up message is clearly a continuation of the task you already dispatched to this persona (the previous B session is resumed with its full context). Omit (or pass 'new') for an independent new task. Default is 'new'.",
39707
39708
  inputSchema: {
39708
39709
  targetPersona: external_exports.string().describe("persona id (e.g. persona-bug-fixer)"),
39709
39710
  prompt: external_exports.string().describe(
39710
39711
  "the task to perform, with the @persona/<id> token stripped out"
39712
+ ),
39713
+ mode: external_exports.enum(["new", "continue"]).optional().describe(
39714
+ "continue = resume the previous B session for this (sourceSession, targetPersona) pair; new (default) = fresh B session"
39711
39715
  )
39712
39716
  }
39713
39717
  },
@@ -39726,8 +39730,8 @@ async function main() {
39726
39730
  }
39727
39731
  },
39728
39732
  // sessionId 透传:complete RPC 必须带 x-clawd-session-id header(rpc-adapter 强制)。
39729
- // B session cc env CLAWD_SESSION_ID = BsessionId
39730
- async (input) => handlePersonaDispatchCompleteToolCall(input, { daemonUrl, sessionId, dispatchId })
39733
+ // daemon 用该 sessionId 反查 PersonaDispatchManager 里该 B in-flight dispatchId 配对回 A waiter
39734
+ async (input) => handlePersonaDispatchCompleteToolCall(input, { daemonUrl, sessionId })
39731
39735
  );
39732
39736
  await server.connect(new StdioServerTransport());
39733
39737
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.141",
3
+ "version": "0.2.142-beta.297.e4b56a8",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",