@clawos-dev/clawd 0.2.248 → 0.2.250

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
@@ -5132,12 +5132,21 @@ var init_schemas = __esm({
5132
5132
  SessionListArgs = external_exports.object({
5133
5133
  /** true = 只返回 mirror(originOwnerPrincipalId 非空,即「我在朋友 daemon 上创建」的本机存证)。 */
5134
5134
  originOnly: external_exports.boolean().optional(),
5135
+ /**
5136
+ * 按 own persona 窄化(2026-07-21 self-persona-sessions 追加):只留
5137
+ * `s.ownerPersonaId === args.ownerPersonaId` 的 sessions。
5138
+ * - mirror 行(`ownerPersonaId` 恒 null)天然排除
5139
+ * - 与 `originOnly=true` 同传恒返 0
5140
+ * - 老 daemon 不识别时忽略字段返回全量,客户端消费侧本就自带过滤/截断,天然向后兼容
5141
+ */
5142
+ ownerPersonaId: external_exports.string().min(1).optional(),
5135
5143
  /** 按 updatedAt desc(list 的既有排序)截断前 N 条。 */
5136
5144
  limit: external_exports.number().int().positive().optional(),
5137
5145
  /**
5138
- * 分页起点(2026-07-18 android-tasks-tab 迭代追加):originOnly 过滤后、limit 截断前跳过
5139
- * N 条。与 limit 组合 = updatedAt desc 序上的一页。响应带 total(过滤后总数,见 handler)
5140
- * 供客户端判 hasMore。老 daemon 不识别时忽略 → 回全量,客户端按全量兼容。
5146
+ * 分页起点(2026-07-18 android-tasks-tab 迭代追加):originOnly / ownerPersonaId 过滤后、
5147
+ * limit 截断前跳过前 N 条。与 limit 组合 = updatedAt desc 序上的一页。响应带 total
5148
+ * (过滤后总数,见 handler)供客户端判 hasMore。老 daemon 不识别时忽略 → 回全量,
5149
+ * 客户端按全量兼容。
5141
5150
  */
5142
5151
  offset: external_exports.number().int().nonnegative().optional()
5143
5152
  });
@@ -6204,7 +6213,12 @@ var init_dispatch = __esm({
6204
6213
  })
6205
6214
  ]);
6206
6215
  DispatchRunArgsSchema = external_exports.object({
6207
- targetPersona: external_exports.string().min(1),
6216
+ // 新开必填;续派(dispatchId 非空)时可省略——targetPersonaId 从台账行取。
6217
+ targetPersona: external_exports.string().min(1).optional(),
6218
+ // 续派(spec 2026-07-21,单 dispatchId 语义:一件事一行一 id):非空 = 把 prompt
6219
+ // 作为新 turn 打进该 dispatch 的原 worker session 接着干(台账行复位 running)。
6220
+ // 省略 = 新开一件事,daemon 铸新 id。
6221
+ dispatchId: external_exports.string().min(1).optional(),
6208
6222
  prompt: external_exports.string(),
6209
6223
  // 跨设备 dispatch:非空 = A 角色,转发到该 contact deviceId 的 peer daemon;
6210
6224
  // 省略 = 本地 dispatch / B 角色本地执行。A 转发给 B 时 body 不带此字段。
@@ -6214,6 +6228,10 @@ var init_dispatch = __esm({
6214
6228
  // tools/claude.ts CLAUDE_MODEL_IDS);handler 层做二次校验,非法值 fail-fast。
6215
6229
  // protocol 层保持 string 便于未来跨 tool 扩展(届时 union 收合法字面量)。
6216
6230
  model: external_exports.string().min(1).optional()
6231
+ }).refine((v2) => v2.targetPersona !== void 0 || v2.dispatchId !== void 0, {
6232
+ message: "either targetPersona (new dispatch) or dispatchId (follow-up) is required"
6233
+ }).refine((v2) => !(v2.dispatchId !== void 0 && v2.targetDeviceId !== void 0), {
6234
+ message: "cross-device dispatch cannot be continued (dispatchId + targetDeviceId are mutually exclusive)"
6217
6235
  });
6218
6236
  DispatchCompleteArgsSchema = external_exports.object({
6219
6237
  dispatchId: external_exports.string().min(1),
@@ -41917,6 +41935,10 @@ tool \u4F1A**\u7ACB\u5373\u8FD4\u56DE** dispatchId\uFF08\u5F02\u6B65\u59D4\u6D3E
41917
41935
  - \u4E00\u53E5\u8BDD\u91CC\u591A\u4E2A \`@persona/<id>\` \u65F6\u9009\u7B2C\u4E00\u4E2A\uFF08tool \u4E00\u6B21\u53EA\u5904\u7406\u4E00\u4E2A target\uFF09
41918
41936
  - \u59D4\u6D3E\u5931\u8D25\uFF08tool_result.is_error=true\uFF09\u65F6\u5982\u5B9E\u544A\u8BC9\u7528\u6237\u539F\u56E0\uFF0C**\u4E0D\u8981\u91CD\u8BD5**
41919
41937
 
41938
+ ### \u7EED\u6D3E\uFF08\u540C\u4E00\u4EF6\u4E8B\u7684\u540E\u7EED\u6307\u4EE4\uFF09
41939
+
41940
+ \u7528\u6237\u5BF9\u4E00\u4EF6**\u5DF2\u59D4\u6D3E**\u7684\u4E8B\u4E0B\u540E\u7EED\u6307\u4EE4\u65F6\uFF0C\u8C03 \`personaDispatch({ dispatchId: <\u8BE5\u4E8B\u7684 dispatchId>, prompt: <\u65B0\u6307\u4EE4> })\`\uFF08targetPersona \u53EF\u7701\u7565\uFF09\u2014\u2014\u65B0\u6307\u4EE4\u4F1A\u8FDB\u5165\u539F worker session \u63A5\u7740\u5E72\uFF0C\u4E0A\u4E0B\u6587\u4E0D\u4E22\u3002dispatchId \u7528\u4F60\u9996\u6B21\u59D4\u6D3E\u65F6\u62FF\u5230\u7684\u90A3\u4E2A\uFF1B\u4E0D\u786E\u5B9A\u662F\u54EA\u4EF6\u4E8B\u5C31\u5148\u8C03 \`personaDispatchList\` \u67E5\u3002
41941
+
41920
41942
  ### \u8DE8\u8BBE\u5907\u59D4\u6D3E\uFF08remote persona\uFF09
41921
41943
 
41922
41944
  \u5F53 @ token \u5F62\u5982 \`@persona/<id>@<deviceId>\`\uFF08\u7B2C\u4E8C\u6BB5\u662F\u8054\u7CFB\u4EBA**\u8BBE\u5907\u7684 deviceId**\uFF0CUI \u9009\u8054\u7CFB\u4EBA\u65F6\u5DF2\u89E3\u6790\u597D\u586B\u5165\uFF09\u65F6\uFF0C\u662F\u628A\u4EFB\u52A1\u59D4\u6D3E\u7ED9\u90A3\u53F0\u8BBE\u5907\u4E0A\u7684 persona\u3002\u9664 targetPersona / prompt \u5916\u518D\u5E26\u4E00\u4E2A\u53C2\u6570\uFF1A
@@ -44126,9 +44148,25 @@ var SessionManager = class {
44126
44148
  if (pending.length === 0) return;
44127
44149
  const state = this.tryGetState(sourceSessionId);
44128
44150
  if (state && compressStatus(state.status) === "running") return;
44151
+ if (!this.injectIntoSession(sourceSessionId, formatDeliveryText(pending))) return;
44152
+ const nowIso3 = new Date(this.deps.now?.() ?? Date.now()).toISOString();
44153
+ for (const r of pending) store.update(r.dispatchId, { deliveredAt: nowIso3 });
44154
+ this.deps.logger?.info("dispatch.deliver.ok", {
44155
+ sourceSessionId,
44156
+ count: pending.length,
44157
+ dispatchIds: pending.map((r) => r.dispatchId)
44158
+ });
44159
+ }
44160
+ /**
44161
+ * 把一段 plain text 以 owner sender 注入指定 session 当新 turn——dispatch 结果投递
44162
+ * (deliverPending)与续派任务包(handlers/persona-dispatch 经 sendFollowUp 闭包)共用。
44163
+ * session 文件不存在或 send 抛错 → false(记 log,不 throw)。直接 runner.input
44164
+ * (无 collector),frames 经 routeFromRunner 正常广播上 wire。
44165
+ */
44166
+ injectIntoSession(sessionId, text) {
44129
44167
  if (!this.deps.ownerPrincipalId) {
44130
- this.deps.logger?.warn("dispatch.deliver.no-owner-principal", { sourceSessionId });
44131
- return;
44168
+ this.deps.logger?.warn("dispatch.inject.no-owner-principal", { sessionId });
44169
+ return false;
44132
44170
  }
44133
44171
  const sender = principalToSender(
44134
44172
  makeOwnerPrincipal(
@@ -44138,34 +44176,24 @@ var SessionManager = class {
44138
44176
  )
44139
44177
  );
44140
44178
  try {
44141
- const file = this.findOwnedSession(sourceSessionId);
44179
+ const file = this.findOwnedSession(sessionId);
44142
44180
  if (!file) {
44143
- this.deps.logger?.info("dispatch.deliver.source-gone", { sourceSessionId });
44144
- return;
44181
+ this.deps.logger?.info("dispatch.inject.session-gone", { sessionId });
44182
+ return false;
44145
44183
  }
44146
44184
  const runner = this.ensureRunnerForScope(file, this.scopeForFile(file));
44147
44185
  runner.input({
44148
44186
  kind: "command",
44149
- command: {
44150
- kind: "send",
44151
- text: formatDeliveryText(pending),
44152
- senderPrincipal: sender
44153
- }
44187
+ command: { kind: "send", text, senderPrincipal: sender }
44154
44188
  });
44189
+ return true;
44155
44190
  } catch (err) {
44156
- this.deps.logger?.warn("dispatch.deliver.send-failed", {
44157
- sourceSessionId,
44191
+ this.deps.logger?.warn("dispatch.inject.send-failed", {
44192
+ sessionId,
44158
44193
  reason: err instanceof Error ? err.message : String(err)
44159
44194
  });
44160
- return;
44195
+ return false;
44161
44196
  }
44162
- const nowIso3 = new Date(this.deps.now?.() ?? Date.now()).toISOString();
44163
- for (const r of pending) store.update(r.dispatchId, { deliveredAt: nowIso3 });
44164
- this.deps.logger?.info("dispatch.deliver.ok", {
44165
- sourceSessionId,
44166
- count: pending.length,
44167
- dispatchIds: pending.map((r) => r.dispatchId)
44168
- });
44169
44197
  }
44170
44198
  /**
44171
44199
  * 只读拿 SessionState;session 不存在返 null(不 throw)。
@@ -48321,6 +48349,21 @@ var PersonaDispatchManager = class {
48321
48349
  state.waiters.push(d);
48322
48350
  return d.promise;
48323
48351
  }
48352
+ /**
48353
+ * 续派(spec 2026-07-21):complete 已清掉 map 条目,续派前重建——bSessionId 映射
48354
+ * 保证 worker 进程死后 respawn 时 reducer lookupDispatchByBSessionId 仍注回同一
48355
+ * CLAWD_DISPATCH_ID env。台账行的复位由 handler 直接走 store(本方法只管内存态)。
48356
+ */
48357
+ reopen(dispatchId, args) {
48358
+ const existing = this.map.get(dispatchId);
48359
+ this.map.set(dispatchId, {
48360
+ dispatchId,
48361
+ sourceSessionId: args.sourceSessionId,
48362
+ targetPersona: args.targetPersona,
48363
+ bSessionId: args.workerSessionId,
48364
+ waiters: existing?.waiters ?? []
48365
+ });
48366
+ }
48324
48367
  /** SessionManager 在 createDispatchedSession 时调,建立 dispatchId → B sessionId 映射 */
48325
48368
  registerBSession(dispatchId, bSessionId) {
48326
48369
  const state = this.map.get(dispatchId);
@@ -49054,6 +49097,17 @@ function canAccessPersona(grants, personaId, action) {
49054
49097
 
49055
49098
  // src/handlers/persona-dispatch.ts
49056
49099
  init_claude();
49100
+ function buildFollowUpTaskPack(dispatchId, prompt) {
49101
+ return [
49102
+ `[Follow-up instruction for dispatch ${dispatchId}]`,
49103
+ "",
49104
+ `Task: ${prompt}`,
49105
+ "",
49106
+ `This is a follow-up turn in the same dispatched task (dispatchId=${dispatchId}).`,
49107
+ "When you finish this round, report back by calling personaDispatchComplete again,",
49108
+ "exactly as you did before."
49109
+ ].join("\n");
49110
+ }
49057
49111
  function buildPersonaDispatchHandlers(deps) {
49058
49112
  const { personaDispatchManager: mgr, spawnB, logger } = deps;
49059
49113
  const run = async (frame, _client, ctx) => {
@@ -49066,6 +49120,76 @@ function buildPersonaDispatchHandlers(deps) {
49066
49120
  `model '${args.model}' not supported; allowed: ${CLAUDE_MODEL_IDS.join(", ")}`
49067
49121
  );
49068
49122
  }
49123
+ if (args.dispatchId) {
49124
+ if (ctx?.principal.kind === "guest") {
49125
+ throw new ClawdError(
49126
+ ERROR_CODES.UNAUTHORIZED,
49127
+ "follow-up dispatch cannot be initiated by a guest"
49128
+ );
49129
+ }
49130
+ if (!sourceSessionId) {
49131
+ throw new Error(
49132
+ "personaDispatch:run requires sessionId (caller must pass x-clawd-session-id header)"
49133
+ );
49134
+ }
49135
+ const rec3 = deps.store.get(args.dispatchId);
49136
+ if (!rec3) {
49137
+ throw new ClawdError(ERROR_CODES.INVALID_PARAM, `dispatch not found: ${args.dispatchId}`);
49138
+ }
49139
+ if (!rec3.workerSessionId) {
49140
+ throw new ClawdError(
49141
+ ERROR_CODES.INVALID_PARAM,
49142
+ `dispatch ${args.dispatchId} has no worker session and cannot be continued (cross-device or spawn incomplete)`
49143
+ );
49144
+ }
49145
+ if (args.targetPersona && args.targetPersona !== rec3.targetPersonaId) {
49146
+ throw new ClawdError(
49147
+ ERROR_CODES.INVALID_PARAM,
49148
+ `targetPersona mismatch: dispatch ${args.dispatchId} belongs to ${rec3.targetPersonaId}`
49149
+ );
49150
+ }
49151
+ if (!deps.sendFollowUp) {
49152
+ throw new Error("follow-up dispatch not wired (sendFollowUp missing)");
49153
+ }
49154
+ deps.store.update(args.dispatchId, {
49155
+ status: "running",
49156
+ outcome: void 0,
49157
+ completedAt: void 0,
49158
+ deliveredAt: void 0,
49159
+ taskText: args.prompt.slice(0, TASK_TEXT_MAX)
49160
+ });
49161
+ mgr.reopen(args.dispatchId, {
49162
+ sourceSessionId,
49163
+ targetPersona: rec3.targetPersonaId,
49164
+ workerSessionId: rec3.workerSessionId
49165
+ });
49166
+ logger?.info("dispatch.run.follow-up", {
49167
+ dispatchId: args.dispatchId,
49168
+ workerSessionId: rec3.workerSessionId,
49169
+ promptLen: args.prompt.length
49170
+ });
49171
+ const ok = deps.sendFollowUp(
49172
+ rec3.workerSessionId,
49173
+ buildFollowUpTaskPack(args.dispatchId, args.prompt)
49174
+ );
49175
+ if (!ok) {
49176
+ mgr.complete(args.dispatchId, {
49177
+ kind: "failure",
49178
+ reason: "worker session unavailable for follow-up"
49179
+ });
49180
+ throw new ClawdError(
49181
+ ERROR_CODES.SESSION_NOT_FOUND,
49182
+ `worker session unavailable; follow-up failed for dispatch ${args.dispatchId}`
49183
+ );
49184
+ }
49185
+ return {
49186
+ response: { type: "personaDispatch:run:ok", dispatchId: args.dispatchId }
49187
+ };
49188
+ }
49189
+ const targetPersona = args.targetPersona;
49190
+ if (!targetPersona) {
49191
+ throw new ClawdError(ERROR_CODES.INVALID_PARAM, "targetPersona is required for a new dispatch");
49192
+ }
49069
49193
  if (args.targetDeviceId) {
49070
49194
  if (ctx?.principal.kind === "guest") {
49071
49195
  throw new ClawdError(
@@ -49083,17 +49207,17 @@ function buildPersonaDispatchHandlers(deps) {
49083
49207
  }
49084
49208
  const { dispatchId: dispatchId2 } = mgr.start({
49085
49209
  sourceSessionId,
49086
- targetPersona: args.targetPersona,
49210
+ targetPersona,
49087
49211
  taskText: args.prompt
49088
49212
  });
49089
49213
  logger?.info("dispatch.run.forward", {
49090
49214
  dispatchId: dispatchId2,
49091
49215
  targetDeviceId: args.targetDeviceId,
49092
- targetPersona: args.targetPersona
49216
+ targetPersona
49093
49217
  });
49094
49218
  void deps.forwardToPeer({
49095
49219
  targetDeviceId: args.targetDeviceId,
49096
- targetPersona: args.targetPersona,
49220
+ targetPersona,
49097
49221
  prompt: args.prompt,
49098
49222
  ...args.model ? { model: args.model } : {}
49099
49223
  }).then((outcome) => mgr.complete(dispatchId2, outcome)).catch((err) => {
@@ -49106,28 +49230,28 @@ function buildPersonaDispatchHandlers(deps) {
49106
49230
  };
49107
49231
  }
49108
49232
  if (ctx?.principal.kind === "guest") {
49109
- const isPublic = deps.getPersonaPublic?.(args.targetPersona) ?? false;
49110
- if (!isPublic || !canAccessPersona(ctx.grants, args.targetPersona, "send")) {
49233
+ const isPublic = deps.getPersonaPublic?.(targetPersona) ?? false;
49234
+ if (!isPublic || !canAccessPersona(ctx.grants, targetPersona, "send")) {
49111
49235
  throw new ClawdError(
49112
49236
  ERROR_CODES.UNAUTHORIZED,
49113
- `persona not dispatchable: ${args.targetPersona}`
49237
+ `persona not dispatchable: ${targetPersona}`
49114
49238
  );
49115
49239
  }
49116
49240
  const { dispatchId: dispatchId2 } = mgr.start({
49117
49241
  sourceSessionId: ctx.principal.id,
49118
- targetPersona: args.targetPersona,
49242
+ targetPersona,
49119
49243
  taskText: args.prompt
49120
49244
  });
49121
49245
  logger?.info("dispatch.run.received.guest", {
49122
49246
  dispatchId: dispatchId2,
49123
49247
  sourcePrincipal: ctx.principal.id,
49124
- targetPersona: args.targetPersona,
49248
+ targetPersona,
49125
49249
  promptLen: args.prompt.length
49126
49250
  });
49127
49251
  void spawnB({
49128
49252
  dispatchId: dispatchId2,
49129
49253
  sourceSessionId: ctx.principal.id,
49130
- targetPersona: args.targetPersona,
49254
+ targetPersona,
49131
49255
  prompt: args.prompt,
49132
49256
  guestPrincipalId: ctx.principal.id,
49133
49257
  guestDisplayName: ctx.principal.displayName,
@@ -49154,19 +49278,19 @@ function buildPersonaDispatchHandlers(deps) {
49154
49278
  }
49155
49279
  const { dispatchId } = mgr.start({
49156
49280
  sourceSessionId,
49157
- targetPersona: args.targetPersona,
49281
+ targetPersona,
49158
49282
  taskText: args.prompt
49159
49283
  });
49160
49284
  logger?.info("dispatch.run.received", {
49161
49285
  dispatchId,
49162
49286
  sourceSessionId,
49163
- targetPersona: args.targetPersona,
49287
+ targetPersona,
49164
49288
  promptLen: args.prompt.length
49165
49289
  });
49166
49290
  void spawnB({
49167
49291
  dispatchId,
49168
49292
  sourceSessionId,
49169
- targetPersona: args.targetPersona,
49293
+ targetPersona,
49170
49294
  prompt: args.prompt,
49171
49295
  ...args.model ? { model: args.model } : {}
49172
49296
  }).then(() => {
@@ -50698,6 +50822,9 @@ var EXPOSED_RPC_METHODS = [
50698
50822
  "peerExec.run",
50699
50823
  "personaDispatch.complete",
50700
50824
  "personaDispatch.list",
50825
+ // persona 清单(spec 2026-07-21):两级路由第一级——发起方 cc(loopback owner 语义)
50826
+ // 用 curl 拿全量 personaId+label 紧凑清单,再按需读候选 CLAUDE.md 首段做终选。
50827
+ "persona.list",
50701
50828
  // clawd-shift v1 (spec 2026-06-24-clawd-shift):cc 子进程内的 shift MCP server 调
50702
50829
  // 这 5 个 method 管理 owner 的 shift(fire-and-forget 排班)。鉴权靠 body.personaId
50703
50830
  // (mcp-server 从 process.env.CLAWD_PERSONA_ID 读,源 = SessionFile.ownerPersonaId)。
@@ -54227,6 +54354,7 @@ function buildSessionHandlers(deps) {
54227
54354
  const narrow = (sessions) => {
54228
54355
  let out = sessions;
54229
54356
  if (args.originOnly) out = out.filter((s) => s.originOwnerPrincipalId != null);
54357
+ if (args.ownerPersonaId) out = out.filter((s) => s.ownerPersonaId === args.ownerPersonaId);
54230
54358
  const total = out.length;
54231
54359
  const offset = args.offset ?? 0;
54232
54360
  if (offset > 0 || args.limit != null) {
@@ -59600,6 +59728,8 @@ async function startDaemon(config) {
59600
59728
  const dispatchHandlers = buildPersonaDispatchHandlers({
59601
59729
  personaDispatchManager,
59602
59730
  store: dispatchStore,
59731
+ // 续派(spec 2026-07-21):任务包注入 worker session,复用投递同款注入路径。
59732
+ sendFollowUp: (workerSessionId, text) => manager.injectIntoSession(workerSessionId, text),
59603
59733
  logger,
59604
59734
  spawnB: async (args) => {
59605
59735
  logger.info("dispatch.spawnB.start", {
@@ -21770,12 +21770,21 @@ var SessionCreateArgs = external_exports.object({
21770
21770
  var SessionListArgs = external_exports.object({
21771
21771
  /** true = 只返回 mirror(originOwnerPrincipalId 非空,即「我在朋友 daemon 上创建」的本机存证)。 */
21772
21772
  originOnly: external_exports.boolean().optional(),
21773
+ /**
21774
+ * 按 own persona 窄化(2026-07-21 self-persona-sessions 追加):只留
21775
+ * `s.ownerPersonaId === args.ownerPersonaId` 的 sessions。
21776
+ * - mirror 行(`ownerPersonaId` 恒 null)天然排除
21777
+ * - 与 `originOnly=true` 同传恒返 0
21778
+ * - 老 daemon 不识别时忽略字段返回全量,客户端消费侧本就自带过滤/截断,天然向后兼容
21779
+ */
21780
+ ownerPersonaId: external_exports.string().min(1).optional(),
21773
21781
  /** 按 updatedAt desc(list 的既有排序)截断前 N 条。 */
21774
21782
  limit: external_exports.number().int().positive().optional(),
21775
21783
  /**
21776
- * 分页起点(2026-07-18 android-tasks-tab 迭代追加):originOnly 过滤后、limit 截断前跳过
21777
- * N 条。与 limit 组合 = updatedAt desc 序上的一页。响应带 total(过滤后总数,见 handler)
21778
- * 供客户端判 hasMore。老 daemon 不识别时忽略 → 回全量,客户端按全量兼容。
21784
+ * 分页起点(2026-07-18 android-tasks-tab 迭代追加):originOnly / ownerPersonaId 过滤后、
21785
+ * limit 截断前跳过前 N 条。与 limit 组合 = updatedAt desc 序上的一页。响应带 total
21786
+ * (过滤后总数,见 handler)供客户端判 hasMore。老 daemon 不识别时忽略 → 回全量,
21787
+ * 客户端按全量兼容。
21779
21788
  */
21780
21789
  offset: external_exports.number().int().nonnegative().optional()
21781
21790
  });
@@ -22699,7 +22708,12 @@ var DispatchOutcomeSchema = external_exports.discriminatedUnion("kind", [
22699
22708
  })
22700
22709
  ]);
22701
22710
  var DispatchRunArgsSchema = external_exports.object({
22702
- targetPersona: external_exports.string().min(1),
22711
+ // 新开必填;续派(dispatchId 非空)时可省略——targetPersonaId 从台账行取。
22712
+ targetPersona: external_exports.string().min(1).optional(),
22713
+ // 续派(spec 2026-07-21,单 dispatchId 语义:一件事一行一 id):非空 = 把 prompt
22714
+ // 作为新 turn 打进该 dispatch 的原 worker session 接着干(台账行复位 running)。
22715
+ // 省略 = 新开一件事,daemon 铸新 id。
22716
+ dispatchId: external_exports.string().min(1).optional(),
22703
22717
  prompt: external_exports.string(),
22704
22718
  // 跨设备 dispatch:非空 = A 角色,转发到该 contact deviceId 的 peer daemon;
22705
22719
  // 省略 = 本地 dispatch / B 角色本地执行。A 转发给 B 时 body 不带此字段。
@@ -22709,6 +22723,10 @@ var DispatchRunArgsSchema = external_exports.object({
22709
22723
  // tools/claude.ts CLAUDE_MODEL_IDS);handler 层做二次校验,非法值 fail-fast。
22710
22724
  // protocol 层保持 string 便于未来跨 tool 扩展(届时 union 收合法字面量)。
22711
22725
  model: external_exports.string().min(1).optional()
22726
+ }).refine((v) => v.targetPersona !== void 0 || v.dispatchId !== void 0, {
22727
+ message: "either targetPersona (new dispatch) or dispatchId (follow-up) is required"
22728
+ }).refine((v) => !(v.dispatchId !== void 0 && v.targetDeviceId !== void 0), {
22729
+ message: "cross-device dispatch cannot be continued (dispatchId + targetDeviceId are mutually exclusive)"
22712
22730
  });
22713
22731
  var DispatchCompleteArgsSchema = external_exports.object({
22714
22732
  dispatchId: external_exports.string().min(1),
@@ -23416,6 +23434,7 @@ async function handlePersonaDispatchToolCall(input, ctx) {
23416
23434
  const f = ctx.fetch ?? fetch;
23417
23435
  log("personaDispatch tool entered", {
23418
23436
  targetPersona: input.targetPersona,
23437
+ dispatchId: input.dispatchId,
23419
23438
  promptLen: input.prompt.length,
23420
23439
  sessionId: ctx.sessionId,
23421
23440
  daemonUrl: ctx.daemonUrl
@@ -23443,8 +23462,11 @@ async function handlePersonaDispatchToolCall(input, ctx) {
23443
23462
  "x-clawd-session-id": ctx.sessionId
23444
23463
  },
23445
23464
  body: JSON.stringify({
23446
- targetPersona: input.targetPersona,
23465
+ // 续派时可省 targetPersona(daemon 从台账取)
23466
+ ...input.targetPersona ? { targetPersona: input.targetPersona } : {},
23447
23467
  prompt: input.prompt,
23468
+ // 续派(spec 2026-07-21):非空 = 把 prompt 打进该 dispatch 的原 worker session。
23469
+ ...input.dispatchId ? { dispatchId: input.dispatchId } : {},
23448
23470
  // 跨设备:非空时透传,daemon run handler 据此走 A 角色转发分支。
23449
23471
  ...input.targetDeviceId ? { targetDeviceId: input.targetDeviceId } : {},
23450
23472
  // T-20: 可选 override B session 使用的 claude 模型;MCP tool 层已 enum 校验,
@@ -23478,12 +23500,13 @@ async function handlePersonaDispatchToolCall(input, ctx) {
23478
23500
  };
23479
23501
  }
23480
23502
  const dispatchId = json.result.dispatchId;
23481
- log("personaDispatch submitted", { dispatchId });
23503
+ log("personaDispatch submitted", { dispatchId, followUp: Boolean(input.dispatchId) });
23504
+ const head = input.dispatchId ? `Follow-up sent for dispatch ${dispatchId} \u2014 the new instruction was delivered into the same worker session.` : `Task dispatched to ${input.targetPersona} (dispatchId=${dispatchId}).`;
23482
23505
  return {
23483
23506
  content: [
23484
23507
  {
23485
23508
  type: "text",
23486
- text: `Task dispatched to ${input.targetPersona} (dispatchId=${dispatchId}). The result will be delivered back into this session when the target persona finishes \u2014 you do NOT need to wait or poll. Tell the user the task has been delegated and end your turn. Use personaDispatchList only if the user later asks about progress.`
23509
+ text: `${head} The result will be delivered back into this session when the target persona finishes \u2014 you do NOT need to wait or poll. Tell the user the task has been delegated and end your turn. Use personaDispatchList only if the user later asks about progress.`
23487
23510
  }
23488
23511
  ]
23489
23512
  };
@@ -23610,9 +23633,14 @@ async function main() {
23610
23633
  "personaDispatch",
23611
23634
  {
23612
23635
  title: "Dispatch task to another persona",
23613
- 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. Returns immediately with a dispatchId; the result will be delivered back into your session automatically when the target persona finishes \u2014 do not wait or poll. Use personaDispatchList to check progress if asked.",
23636
+ 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. Returns immediately with a dispatchId; the result will be delivered back into your session automatically when the target persona finishes \u2014 do not wait or poll. To continue an existing dispatched task (follow-up instruction into the same worker session), pass its dispatchId and omit targetPersona. Use personaDispatchList to check progress if asked.",
23614
23637
  inputSchema: {
23615
- targetPersona: external_exports.string().describe("persona id (e.g. persona-app-builder)"),
23638
+ targetPersona: external_exports.string().optional().describe(
23639
+ "persona id (e.g. persona-app-builder); omit when continuing an existing dispatch via dispatchId"
23640
+ ),
23641
+ dispatchId: external_exports.string().optional().describe(
23642
+ "continue an existing dispatched task: the prompt is delivered into the same worker session as a new turn (context preserved); model cannot be changed on follow-up (the worker session keeps its original model)"
23643
+ ),
23616
23644
  prompt: external_exports.string().describe(
23617
23645
  "the task to perform, with the @persona/<id> token stripped out"
23618
23646
  ),