@clawos-dev/clawd 0.2.721 → 0.2.722

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.
@@ -32211,7 +32211,7 @@ var DispatchRunArgsSchema = external_exports.object({
32211
32211
  }).refine((v) => v.targetPersona !== void 0 || v.dispatchId !== void 0, {
32212
32212
  message: "either targetPersona (new dispatch) or dispatchId (follow-up) is required"
32213
32213
  }).refine((v) => !(v.dispatchId !== void 0 && v.targetDeviceId !== void 0), {
32214
- message: "cross-device dispatch cannot be continued (dispatchId + targetDeviceId are mutually exclusive)"
32214
+ message: "follow-ups take only dispatchId; the target device comes from the ledger row (dispatchId + targetDeviceId are mutually exclusive)"
32215
32215
  }).refine((v) => !(v.tool !== void 0 && v.targetDeviceId !== void 0), {
32216
32216
  message: "engine override is not supported for cross-device dispatch (tool + targetDeviceId are mutually exclusive)"
32217
32217
  }).refine((v) => !(v.dispatchId !== void 0 && (v.tool !== void 0 || v.model !== void 0)), {
@@ -32230,6 +32230,10 @@ var DispatchRecordSchema = external_exports.object({
32230
32230
  targetPersonaId: external_exports.string().min(1),
32231
32231
  /** A-side target device attribution; absence means local for existing records. Forwarding strips it from the B-side local ledger. */
32232
32232
  targetDeviceId: external_exports.string().min(1).optional(),
32233
+ /** A-side cross-device rows: the B-side ledger id, used for follow-ups and for resuming polls after an A restart. */
32234
+ remoteDispatchId: external_exports.string().min(1).optional(),
32235
+ /** The A-side round B last accepted; a follow-up reopened but not yet accepted has remoteRound < round, so a restart must not resume polling it. */
32236
+ remoteRound: external_exports.number().int().positive().optional(),
32233
32237
  /** B 的 sessionId;registerBSession 后回填 */
32234
32238
  workerSessionId: external_exports.string().min(1).optional(),
32235
32239
  taskText: external_exports.string(),
@@ -34315,7 +34319,7 @@ var METHOD_DOCS = {
34315
34319
  args: PersonaIdArgsSchema
34316
34320
  },
34317
34321
  "personaDispatch:run": {
34318
- summary: "\u628A\u4EFB\u52A1\u59D4\u6D3E\u7ED9\u53E6\u4E00\u4E2A persona\uFF08\u53EF\u8DE8\u8BBE\u5907\uFF09\uFF0C\u5F02\u6B65\u79D2\u56DE { dispatchId }\u3002targetPersona \u4E0E dispatchId **\u81F3\u5C11\u7ED9\u4E00\u4E2A**\uFF08\u524D\u8005\u65B0\u5F00\u3001\u540E\u8005\u7EED\u6D3E\uFF09\uFF1BdispatchId \u4E0E targetDeviceId **\u4E92\u65A5**",
34322
+ summary: "\u628A\u4EFB\u52A1\u59D4\u6D3E\u7ED9\u53E6\u4E00\u4E2A persona\uFF08\u53EF\u8DE8\u8BBE\u5907\uFF09\uFF0C\u5F02\u6B65\u79D2\u56DE { dispatchId }\u3002targetPersona \u4E0E dispatchId **\u81F3\u5C11\u7ED9\u4E00\u4E2A**\uFF08\u524D\u8005\u65B0\u5F00\u3001\u540E\u8005\u7EED\u6D3E\uFF09\uFF1BdispatchId \u4E0E targetDeviceId **\u4E92\u65A5**\uFF08\u8DE8\u8BBE\u5907\u7EED\u6D3E\u4E5F\u53EA\u4F20 dispatchId\uFF0C\u8BBE\u5907\u4ECE\u53F0\u8D26\u53D6\uFF09",
34319
34323
  args: DispatchRunArgsSchema,
34320
34324
  argsConstraints: {
34321
34325
  anyOf: [{ required: ["targetPersona"] }, { required: ["dispatchId"] }],
package/dist/cli.cjs CHANGED
@@ -7247,7 +7247,7 @@ var init_dispatch = __esm({
7247
7247
  }).refine((v3) => v3.targetPersona !== void 0 || v3.dispatchId !== void 0, {
7248
7248
  message: "either targetPersona (new dispatch) or dispatchId (follow-up) is required"
7249
7249
  }).refine((v3) => !(v3.dispatchId !== void 0 && v3.targetDeviceId !== void 0), {
7250
- message: "cross-device dispatch cannot be continued (dispatchId + targetDeviceId are mutually exclusive)"
7250
+ message: "follow-ups take only dispatchId; the target device comes from the ledger row (dispatchId + targetDeviceId are mutually exclusive)"
7251
7251
  }).refine((v3) => !(v3.tool !== void 0 && v3.targetDeviceId !== void 0), {
7252
7252
  message: "engine override is not supported for cross-device dispatch (tool + targetDeviceId are mutually exclusive)"
7253
7253
  }).refine((v3) => !(v3.dispatchId !== void 0 && (v3.tool !== void 0 || v3.model !== void 0)), {
@@ -7266,6 +7266,10 @@ var init_dispatch = __esm({
7266
7266
  targetPersonaId: external_exports.string().min(1),
7267
7267
  /** A-side target device attribution; absence means local for existing records. Forwarding strips it from the B-side local ledger. */
7268
7268
  targetDeviceId: external_exports.string().min(1).optional(),
7269
+ /** A-side cross-device rows: the B-side ledger id, used for follow-ups and for resuming polls after an A restart. */
7270
+ remoteDispatchId: external_exports.string().min(1).optional(),
7271
+ /** The A-side round B last accepted; a follow-up reopened but not yet accepted has remoteRound < round, so a restart must not resume polling it. */
7272
+ remoteRound: external_exports.number().int().positive().optional(),
7269
7273
  /** B 的 sessionId;registerBSession 后回填 */
7270
7274
  workerSessionId: external_exports.string().min(1).optional(),
7271
7275
  taskText: external_exports.string(),
@@ -9587,7 +9591,7 @@ var init_methods = __esm({
9587
9591
  args: PersonaIdArgsSchema
9588
9592
  },
9589
9593
  "personaDispatch:run": {
9590
- summary: "\u628A\u4EFB\u52A1\u59D4\u6D3E\u7ED9\u53E6\u4E00\u4E2A persona\uFF08\u53EF\u8DE8\u8BBE\u5907\uFF09\uFF0C\u5F02\u6B65\u79D2\u56DE { dispatchId }\u3002targetPersona \u4E0E dispatchId **\u81F3\u5C11\u7ED9\u4E00\u4E2A**\uFF08\u524D\u8005\u65B0\u5F00\u3001\u540E\u8005\u7EED\u6D3E\uFF09\uFF1BdispatchId \u4E0E targetDeviceId **\u4E92\u65A5**",
9594
+ summary: "\u628A\u4EFB\u52A1\u59D4\u6D3E\u7ED9\u53E6\u4E00\u4E2A persona\uFF08\u53EF\u8DE8\u8BBE\u5907\uFF09\uFF0C\u5F02\u6B65\u79D2\u56DE { dispatchId }\u3002targetPersona \u4E0E dispatchId **\u81F3\u5C11\u7ED9\u4E00\u4E2A**\uFF08\u524D\u8005\u65B0\u5F00\u3001\u540E\u8005\u7EED\u6D3E\uFF09\uFF1BdispatchId \u4E0E targetDeviceId **\u4E92\u65A5**\uFF08\u8DE8\u8BBE\u5907\u7EED\u6D3E\u4E5F\u53EA\u4F20 dispatchId\uFF0C\u8BBE\u5907\u4ECE\u53F0\u8D26\u53D6\uFF09",
9591
9595
  args: DispatchRunArgsSchema,
9592
9596
  argsConstraints: {
9593
9597
  anyOf: [{ required: ["targetPersona"] }, { required: ["dispatchId"] }],
@@ -50884,15 +50888,15 @@ var SessionManager = class {
50884
50888
  });
50885
50889
  }
50886
50890
  /**
50887
- * 以 owner 身份注入新 turn;无 session 或发送失败返回 false,帧走异步广播。
50891
+ * 默认以 owner 身份注入新 turn(跨设备续派传 guest sender,与首轮执行身份一致);无 session 或发送失败返回 false,帧走异步广播。
50888
50892
  * meta 来源分型须与历史信封分类一致。sender 表达执行身份,origin 表达消息来源,不能混用。
50889
50893
  */
50890
- injectIntoSession(sessionId, text, meta) {
50894
+ injectIntoSession(sessionId, text, meta, sender) {
50891
50895
  if (!this.deps.ownerPrincipalId) {
50892
50896
  this.deps.logger?.warn("dispatch.inject.no-owner-principal", { sessionId });
50893
50897
  return false;
50894
50898
  }
50895
- const sender = principalToSender(
50899
+ const senderPrincipal = sender ?? principalToSender(
50896
50900
  makeOwnerPrincipal(
50897
50901
  this.deps.ownerPrincipalId,
50898
50902
  this.deps.ownerDisplayName ?? "Owner",
@@ -50908,7 +50912,7 @@ var SessionManager = class {
50908
50912
  const runner = this.ensureRunnerForScope(file, this.scopeForFile(file));
50909
50913
  runner.input({
50910
50914
  kind: "command",
50911
- command: { kind: "send", text, senderPrincipal: sender, ...meta ? { meta } : {} }
50915
+ command: { kind: "send", text, senderPrincipal, ...meta ? { meta } : {} }
50912
50916
  });
50913
50917
  return true;
50914
50918
  } catch (err) {
@@ -56926,6 +56930,9 @@ var CLOCK_JUMP_THRESHOLD_MS = 5 * 6e4;
56926
56930
 
56927
56931
  // src/dispatch/store.ts
56928
56932
  var TASK_TEXT_MAX = 500;
56933
+ function isAwaitingPeer(r2) {
56934
+ return r2.status === "running" && !!r2.targetDeviceId && !!r2.remoteDispatchId && r2.remoteRound === (r2.round ?? 1);
56935
+ }
56929
56936
  function createDispatchStore(deps) {
56930
56937
  let records = [];
56931
56938
  let flushTimer = null;
@@ -57004,7 +57011,7 @@ function createDispatchStore(deps) {
57004
57011
  function markInterruptedAsFailed(nowIso3) {
57005
57012
  let touched = false;
57006
57013
  for (const r2 of records) {
57007
- if (r2.status === "running") {
57014
+ if (r2.status === "running" && !isAwaitingPeer(r2)) {
57008
57015
  r2.status = "failed";
57009
57016
  r2.outcome = {
57010
57017
  kind: "failure",
@@ -66798,6 +66805,38 @@ function canAccessPersona(grants, personaId2, action) {
66798
66805
 
66799
66806
  // src/handlers/persona-dispatch.ts
66800
66807
  init_claude();
66808
+ function forwardInBackground(deps, dispatchId, a2) {
66809
+ if (!deps.forwardToPeer) {
66810
+ throw new Error("cross-device dispatch not wired (forwardToPeer missing)");
66811
+ }
66812
+ void deps.forwardToPeer({
66813
+ ...a2,
66814
+ onSubmitted: (remoteDispatchId) => deps.store.update(dispatchId, { remoteDispatchId, remoteRound: deps.store.get(dispatchId)?.round ?? 1 })
66815
+ }).then((outcome) => deps.personaDispatchManager.complete(dispatchId, outcome)).catch((err) => {
66816
+ const reason = err instanceof Error ? err.message : String(err);
66817
+ deps.logger?.warn("dispatch.forward.failed", { dispatchId, reason });
66818
+ deps.personaDispatchManager.complete(dispatchId, { kind: "failure", reason });
66819
+ });
66820
+ }
66821
+ function resumePeerDispatches(deps) {
66822
+ for (const r2 of deps.store.list()) {
66823
+ if (!isAwaitingPeer(r2) || !r2.targetDeviceId || !r2.remoteDispatchId) continue;
66824
+ deps.logger?.info("dispatch.forward.resume", { dispatchId: r2.dispatchId, targetDeviceId: r2.targetDeviceId });
66825
+ forwardInBackground(deps, r2.dispatchId, {
66826
+ targetDeviceId: r2.targetDeviceId,
66827
+ prompt: "",
66828
+ remoteDispatchId: r2.remoteDispatchId,
66829
+ resume: true
66830
+ });
66831
+ }
66832
+ }
66833
+ function visibleRecord(store, dispatchId, ctx) {
66834
+ const rec6 = store.get(dispatchId);
66835
+ if (!rec6 || ctx?.principal.kind === "guest" && rec6.sourceSessionId !== ctx.principal.id) {
66836
+ throw new ClawdError(ERROR_CODES.DISPATCH_NOT_FOUND, `unknown dispatchId: ${dispatchId}`);
66837
+ }
66838
+ return rec6;
66839
+ }
66801
66840
  function buildFollowUpTaskPack(dispatchId, prompt, round, from) {
66802
66841
  return formatDispatchTaskEnvelope({
66803
66842
  dispatchId,
@@ -66816,6 +66855,12 @@ function buildFollowUpTaskPack(dispatchId, prompt, round, from) {
66816
66855
  }
66817
66856
  function buildPersonaDispatchHandlers(deps) {
66818
66857
  const { personaDispatchManager: mgr, spawnB, logger } = deps;
66858
+ const assertGuestMayDispatch = (ctx, personaId2) => {
66859
+ const isPublic = deps.getPersonaPublic?.(personaId2) ?? false;
66860
+ if (!isPublic || !canAccessPersona(ctx.grants, personaId2, "send")) {
66861
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `persona not dispatchable: ${personaId2}`);
66862
+ }
66863
+ };
66819
66864
  const run = async (frame, _client, ctx) => {
66820
66865
  const { type: _t2, requestId: _r2, ...rest } = frame;
66821
66866
  const rawSessionId = typeof rest.sessionId === "string" && rest.sessionId.length > 0 ? rest.sessionId : void 0;
@@ -66835,44 +66880,68 @@ function buildPersonaDispatchHandlers(deps) {
66835
66880
  );
66836
66881
  }
66837
66882
  if (args.dispatchId) {
66838
- if (ctx?.principal.kind === "guest") {
66839
- throw new ClawdError(
66840
- ERROR_CODES.UNAUTHORIZED,
66841
- "follow-up dispatch cannot be initiated by a guest"
66842
- );
66843
- }
66844
- const rec6 = deps.store.get(args.dispatchId);
66883
+ const guest = ctx?.principal.kind === "guest" ? ctx : void 0;
66884
+ const rec6 = guest ? visibleRecord(deps.store, args.dispatchId, guest) : deps.store.get(args.dispatchId);
66845
66885
  if (!rec6) {
66846
66886
  throw new ClawdError(ERROR_CODES.INVALID_PARAM, `dispatch not found: ${args.dispatchId}`);
66847
66887
  }
66848
- if (!rec6.workerSessionId) {
66888
+ if (guest) assertGuestMayDispatch(guest, rec6.targetPersonaId);
66889
+ if (args.targetPersona && args.targetPersona !== rec6.targetPersonaId) {
66849
66890
  throw new ClawdError(
66850
66891
  ERROR_CODES.INVALID_PARAM,
66851
- `dispatch ${args.dispatchId} has no worker session and cannot be continued (cross-device or spawn incomplete)`
66892
+ `targetPersona mismatch: dispatch ${args.dispatchId} belongs to ${rec6.targetPersonaId}`
66852
66893
  );
66853
66894
  }
66854
- if (args.targetPersona && args.targetPersona !== rec6.targetPersonaId) {
66895
+ const dispatchId2 = args.dispatchId;
66896
+ const reopenRow = () => {
66897
+ const nextRound2 = (rec6.round ?? 1) + 1;
66898
+ deps.store.update(dispatchId2, {
66899
+ status: "running",
66900
+ outcome: void 0,
66901
+ completedAt: void 0,
66902
+ deliveredAt: void 0,
66903
+ taskText: args.prompt.slice(0, TASK_TEXT_MAX),
66904
+ round: nextRound2,
66905
+ ...args.brief ? { taskBrief: args.brief } : {},
66906
+ ...args.meta ? { meta: { ...rec6.meta ?? {}, ...args.meta } } : {}
66907
+ });
66908
+ return nextRound2;
66909
+ };
66910
+ if (rec6.targetDeviceId) {
66911
+ if (!rec6.remoteDispatchId) {
66912
+ throw new ClawdError(
66913
+ ERROR_CODES.INVALID_PARAM,
66914
+ `dispatch ${args.dispatchId} was not accepted by the peer and cannot be continued`
66915
+ );
66916
+ }
66917
+ reopenRow();
66918
+ logger?.info("dispatch.run.follow-up.forward", {
66919
+ dispatchId: args.dispatchId,
66920
+ targetDeviceId: rec6.targetDeviceId,
66921
+ promptLen: args.prompt.length
66922
+ });
66923
+ forwardInBackground(deps, args.dispatchId, {
66924
+ targetDeviceId: rec6.targetDeviceId,
66925
+ prompt: args.prompt,
66926
+ ...args.brief ? { brief: args.brief } : {},
66927
+ remoteDispatchId: rec6.remoteDispatchId
66928
+ });
66929
+ return {
66930
+ response: { type: "personaDispatch:run:ok", dispatchId: args.dispatchId }
66931
+ };
66932
+ }
66933
+ if (!rec6.workerSessionId) {
66855
66934
  throw new ClawdError(
66856
66935
  ERROR_CODES.INVALID_PARAM,
66857
- `targetPersona mismatch: dispatch ${args.dispatchId} belongs to ${rec6.targetPersonaId}`
66936
+ `dispatch ${args.dispatchId} has no worker session and cannot be continued (spawn incomplete)`
66858
66937
  );
66859
66938
  }
66860
66939
  if (!deps.sendFollowUp) {
66861
66940
  throw new Error("follow-up dispatch not wired (sendFollowUp missing)");
66862
66941
  }
66863
- const nextRound = (rec6.round ?? 1) + 1;
66864
- deps.store.update(args.dispatchId, {
66865
- status: "running",
66866
- outcome: void 0,
66867
- completedAt: void 0,
66868
- deliveredAt: void 0,
66869
- taskText: args.prompt.slice(0, TASK_TEXT_MAX),
66870
- round: nextRound,
66871
- ...args.brief ? { taskBrief: args.brief } : {},
66872
- ...args.meta ? { meta: { ...rec6.meta ?? {}, ...args.meta } } : {}
66873
- });
66942
+ const nextRound = reopenRow();
66874
66943
  mgr.reopen(args.dispatchId, {
66875
- sourceSessionId,
66944
+ sourceSessionId: guest ? guest.principal.id : sourceSessionId,
66876
66945
  targetPersona: rec6.targetPersonaId,
66877
66946
  workerSessionId: rec6.workerSessionId
66878
66947
  });
@@ -66888,10 +66957,13 @@ function buildPersonaDispatchHandlers(deps) {
66888
66957
  args.prompt,
66889
66958
  nextRound,
66890
66959
  // Legacy ledger rows may lack attribution; fall back to a current session lookup.
66891
- rec6.sourcePersonaId ?? deps.getSessionPersonaId?.(sourceSessionId)
66960
+ // Cross-device rows carry the caller's display name, as in the first round.
66961
+ guest ? guest.principal.displayName : rec6.sourcePersonaId ?? deps.getSessionPersonaId?.(sourceSessionId)
66892
66962
  ),
66893
66963
  // Realtime metadata and history envelope classification both use dispatch-task.
66894
- "dispatch-task"
66964
+ "dispatch-task",
66965
+ // Keep the first round's execution identity: a guest worker must not continue as the owner.
66966
+ guest ? makeGuestSender(guest.principal.id, guest.principal.displayName) : void 0
66895
66967
  );
66896
66968
  if (!ok) {
66897
66969
  mgr.complete(args.dispatchId, {
@@ -66949,9 +67021,6 @@ function buildPersonaDispatchHandlers(deps) {
66949
67021
  "cross-device forward cannot be initiated by a guest"
66950
67022
  );
66951
67023
  }
66952
- if (!deps.forwardToPeer) {
66953
- throw new Error("cross-device dispatch not wired (forwardToPeer missing)");
66954
- }
66955
67024
  const { dispatchId: dispatchId2 } = mgr.start({
66956
67025
  sourceSessionId,
66957
67026
  targetPersona,
@@ -66966,28 +67035,20 @@ function buildPersonaDispatchHandlers(deps) {
66966
67035
  targetDeviceId: args.targetDeviceId,
66967
67036
  targetPersona
66968
67037
  });
66969
- void deps.forwardToPeer({
67038
+ forwardInBackground(deps, dispatchId2, {
66970
67039
  targetDeviceId: args.targetDeviceId,
66971
67040
  targetPersona,
66972
67041
  prompt: args.prompt,
67042
+ ...args.brief ? { brief: args.brief } : {},
67043
+ ...args.sessionLabel ? { sessionLabel: args.sessionLabel } : {},
66973
67044
  ...args.model ? { model: args.model } : {}
66974
- }).then((outcome) => mgr.complete(dispatchId2, outcome)).catch((err) => {
66975
- const reason = err instanceof Error ? err.message : String(err);
66976
- logger?.warn("dispatch.forward.failed", { dispatchId: dispatchId2, reason });
66977
- mgr.complete(dispatchId2, { kind: "failure", reason });
66978
67045
  });
66979
67046
  return {
66980
67047
  response: { type: "personaDispatch:run:ok", dispatchId: dispatchId2 }
66981
67048
  };
66982
67049
  }
66983
67050
  if (ctx?.principal.kind === "guest") {
66984
- const isPublic = deps.getPersonaPublic?.(targetPersona) ?? false;
66985
- if (!isPublic || !canAccessPersona(ctx.grants, targetPersona, "send")) {
66986
- throw new ClawdError(
66987
- ERROR_CODES.UNAUTHORIZED,
66988
- `persona not dispatchable: ${targetPersona}`
66989
- );
66990
- }
67051
+ assertGuestMayDispatch(ctx, targetPersona);
66991
67052
  const { dispatchId: dispatchId2 } = mgr.start({
66992
67053
  sourceSessionId: ctx.principal.id,
66993
67054
  targetPersona,
@@ -67076,11 +67137,7 @@ function buildPersonaDispatchHandlers(deps) {
67076
67137
  const get = async (frame, _client, ctx) => {
67077
67138
  const { type: _t2, requestId: _r2, ...rest } = frame;
67078
67139
  const args = DispatchGetArgsSchema.parse(rest);
67079
- const rec6 = deps.store.get(args.dispatchId);
67080
- const denied = ctx?.principal.kind === "guest" && rec6?.sourceSessionId !== ctx.principal.id;
67081
- if (!rec6 || denied) {
67082
- throw new ClawdError(ERROR_CODES.DISPATCH_NOT_FOUND, `unknown dispatchId: ${args.dispatchId}`);
67083
- }
67140
+ const rec6 = visibleRecord(deps.store, args.dispatchId, ctx);
67084
67141
  return {
67085
67142
  response: {
67086
67143
  type: "personaDispatch:get:ok",
@@ -67123,40 +67180,15 @@ async function forwardDispatchToPeer(args) {
67123
67180
  const f2 = args.fetchImpl ?? fetch;
67124
67181
  const sleep2 = args.sleepImpl ?? defaultSleep;
67125
67182
  const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
67126
- const url = `${base}/rpc/personaDispatch:run`;
67127
- let res;
67128
- try {
67129
- res = await f2(url, {
67130
- method: "POST",
67131
- headers: {
67132
- "content-type": "application/json",
67133
- authorization: `Bearer ${args.contact.connectToken}`
67134
- },
67135
- // 注意:不带 targetDeviceId —— B 端据此判定为本地执行(B 角色)。
67136
- body: JSON.stringify({
67137
- targetPersona: args.targetPersona,
67138
- prompt: args.prompt,
67139
- ...args.model ? { model: args.model } : {}
67140
- })
67141
- });
67142
- } catch (err) {
67143
- const msg = err instanceof Error ? err.message : String(err);
67144
- return { kind: "failure", reason: `forward to peer failed: ${msg}` };
67145
- }
67146
- let json;
67147
- try {
67148
- json = await res.json();
67149
- } catch {
67150
- return {
67151
- kind: "failure",
67152
- reason: `peer returned non-JSON response (HTTP ${res.status})`
67153
- };
67154
- }
67155
- if (json.ok === false) {
67156
- return { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` };
67183
+ let dispatchId;
67184
+ if (args.resume && args.remoteDispatchId) {
67185
+ dispatchId = args.remoteDispatchId;
67186
+ } else {
67187
+ const submitted = await submitRun(f2, base, args);
67188
+ if ("outcome" in submitted) return submitted.outcome;
67189
+ dispatchId = submitted.dispatchId;
67190
+ args.onSubmitted?.(dispatchId);
67157
67191
  }
67158
- if ("outcome" in json.result) return json.result.outcome;
67159
- const dispatchId = json.result.dispatchId;
67160
67192
  let elapsed = 0;
67161
67193
  let unreachableFor = 0;
67162
67194
  while (elapsed < DISPATCH_POLL_TOTAL_MS) {
@@ -67208,6 +67240,40 @@ async function forwardDispatchToPeer(args) {
67208
67240
  }
67209
67241
  return { kind: "failure", reason: "dispatch polling exceeded 24h (TIMEOUT)" };
67210
67242
  }
67243
+ async function submitRun(f2, base, args) {
67244
+ const body = args.remoteDispatchId ? { dispatchId: args.remoteDispatchId, prompt: args.prompt, ...args.brief ? { brief: args.brief } : {} } : {
67245
+ targetPersona: args.targetPersona,
67246
+ prompt: args.prompt,
67247
+ ...args.brief ? { brief: args.brief } : {},
67248
+ ...args.sessionLabel ? { sessionLabel: args.sessionLabel } : {},
67249
+ ...args.model ? { model: args.model } : {}
67250
+ };
67251
+ let res;
67252
+ try {
67253
+ res = await f2(`${base}/rpc/personaDispatch:run`, {
67254
+ method: "POST",
67255
+ headers: {
67256
+ "content-type": "application/json",
67257
+ authorization: `Bearer ${args.contact.connectToken}`
67258
+ },
67259
+ body: JSON.stringify(body)
67260
+ });
67261
+ } catch (err) {
67262
+ const msg = err instanceof Error ? err.message : String(err);
67263
+ return { outcome: { kind: "failure", reason: `forward to peer failed: ${msg}` } };
67264
+ }
67265
+ let json;
67266
+ try {
67267
+ json = await res.json();
67268
+ } catch {
67269
+ return { outcome: { kind: "failure", reason: `peer returned non-JSON response (HTTP ${res.status})` } };
67270
+ }
67271
+ if (json.ok === false) {
67272
+ return { outcome: { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` } };
67273
+ }
67274
+ if ("outcome" in json.result) return { outcome: json.result.outcome };
67275
+ return { dispatchId: json.result.dispatchId };
67276
+ }
67211
67277
  async function forwardInboxPostToPeer(args) {
67212
67278
  const f2 = args.fetchImpl ?? fetch;
67213
67279
  const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
@@ -76060,7 +76126,7 @@ function computeMethodAccess(args) {
76060
76126
  }
76061
76127
 
76062
76128
  // src/version.ts
76063
- var version = "0.2.721".length > 0 ? "0.2.721" : "dev";
76129
+ var version = "0.2.722".length > 0 ? "0.2.722" : "dev";
76064
76130
 
76065
76131
  // src/cli-probe/probe.ts
76066
76132
  var fs87 = __toESM(require("fs"), 1);
@@ -79676,6 +79742,19 @@ async function startDaemon(config) {
79676
79742
  issueWebLaunchUrl
79677
79743
  })
79678
79744
  };
79745
+ const forwardToPeer = async ({ targetDeviceId, ...rest }) => {
79746
+ const contact = contactStore.get(targetDeviceId);
79747
+ if (!contact || !contact.remoteUrl || !contact.connectToken) {
79748
+ return {
79749
+ kind: "failure",
79750
+ reason: `unknown or unreachable contact: ${targetDeviceId}`
79751
+ };
79752
+ }
79753
+ return forwardDispatchToPeer({
79754
+ contact: { remoteUrl: contact.remoteUrl, connectToken: contact.connectToken },
79755
+ ...rest
79756
+ });
79757
+ };
79679
79758
  const dispatchHandlers = buildPersonaDispatchHandlers({
79680
79759
  personaDispatchManager,
79681
79760
  store: dispatchStore,
@@ -79687,7 +79766,7 @@ async function startDaemon(config) {
79687
79766
  if (!ch) return void 0;
79688
79767
  return { personas: [...ch.memberPersonas] };
79689
79768
  },
79690
- sendFollowUp: (workerSessionId, text, meta) => manager.injectIntoSession(workerSessionId, text, meta),
79769
+ sendFollowUp: (workerSessionId, text, meta, sender) => manager.injectIntoSession(workerSessionId, text, meta, sender),
79691
79770
  getSessionPersonaId: (sessionId) => manager.findOwnedSession(sessionId)?.ownerPersonaId,
79692
79771
  logger,
79693
79772
  spawnB: async (args) => {
@@ -79718,26 +79797,13 @@ async function startDaemon(config) {
79718
79797
  ...args.label ? { label: args.label } : {}
79719
79798
  });
79720
79799
  },
79721
- forwardToPeer: async ({ targetDeviceId, targetPersona, prompt, model }) => {
79722
- const contact = contactStore.get(targetDeviceId);
79723
- if (!contact || !contact.remoteUrl || !contact.connectToken) {
79724
- return {
79725
- kind: "failure",
79726
- reason: `unknown or unreachable contact: ${targetDeviceId}`
79727
- };
79728
- }
79729
- return forwardDispatchToPeer({
79730
- contact: { remoteUrl: contact.remoteUrl, connectToken: contact.connectToken },
79731
- targetPersona,
79732
- prompt,
79733
- ...model ? { model } : {}
79734
- });
79735
- },
79800
+ forwardToPeer,
79736
79801
  getPersonaPublic: (personaId2) => personaRegistry.get(personaId2)?.public ?? false,
79737
79802
  getPersonaDefaults: (personaId2) => personaRegistry.get(personaId2),
79738
79803
  getCapabilities: (tool, opts) => manager.getCapabilities(tool, opts)
79739
79804
  });
79740
79805
  Object.assign(handlers, dispatchHandlers);
79806
+ resumePeerDispatches({ personaDispatchManager, store: dispatchStore, forwardToPeer, logger });
79741
79807
  const shiftHandlers = buildShiftInternalHandlers({
79742
79808
  store: shiftStore,
79743
79809
  ownerIdProvider: () => ownerPrincipalId