@maudecode/openclaw-cowtail 0.13.0 → 0.14.1

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/README.md CHANGED
@@ -7,3 +7,7 @@ Load it from an OpenClaw config with `plugins.entries.cowtail.path` pointing at
7
7
  Configure `channels.cowtail.url` with the Cowtail realtime WebSocket endpoint and `channels.cowtail.bridgeToken` with the same bridge token configured on the Cowtail realtime service.
8
8
 
9
9
  V1 supports a single owner and OpenClaw's default `main` agent only.
10
+
11
+ ## Realtime event scope
12
+
13
+ The plugin receives Cowtail events that require OpenClaw work: iOS-created threads, iOS replies, and submitted actions. Cowtail thread lifecycle updates such as read state, rename, and delete stay local to Cowtail/iOS because OpenClaw v1 has no channel-level state mutation contract for those operations.
package/dist/index.js CHANGED
@@ -13801,6 +13801,7 @@ var openclawThreadRecordSchema = exports_external.object({
13801
13801
  var openclawMessageRecordBaseSchema = exports_external.object({
13802
13802
  id: nonEmptyStringSchema,
13803
13803
  threadId: nonEmptyStringSchema,
13804
+ streamId: nonEmptyStringSchema.optional(),
13804
13805
  direction: openclawMessageDirectionSchema,
13805
13806
  authorLabel: nonEmptyStringSchema.optional(),
13806
13807
  text: openclawMessageTextSchema,
@@ -13855,13 +13856,16 @@ var openclawPluginMessageCommandSchema = requireOpenClawRenderableContent(export
13855
13856
  requestId: openclawRequestIdSchema,
13856
13857
  idempotencyKey: openclawIdempotencyKeySchema,
13857
13858
  sessionKey: nonEmptyStringSchema,
13859
+ threadId: nonEmptyStringSchema.optional(),
13860
+ threadHint: nonEmptyStringSchema.optional(),
13858
13861
  title: nonEmptyStringSchema.optional(),
13859
13862
  text: openclawMessageTextSchema,
13860
13863
  authorLabel: nonEmptyStringSchema.optional(),
13861
13864
  links: exports_external.array(openclawLinkSchema).default([]),
13862
13865
  toolCalls: exports_external.array(openclawToolCallRecordSchema).default([]),
13863
13866
  actions: exports_external.array(openclawActionDraftSchema).default([]),
13864
- deliveryState: openclawDeliveryStateSchema.optional()
13867
+ deliveryState: openclawDeliveryStateSchema.optional(),
13868
+ streamId: nonEmptyStringSchema
13865
13869
  }));
13866
13870
  var openclawPluginMessageUpdateCommandSchema = requireOpenClawRenderableContent(exports_external.object({
13867
13871
  type: exports_external.literal("openclaw_message_update"),
@@ -13872,7 +13876,8 @@ var openclawPluginMessageUpdateCommandSchema = requireOpenClawRenderableContent(
13872
13876
  links: exports_external.array(openclawLinkSchema).optional(),
13873
13877
  toolCalls: exports_external.array(openclawToolCallRecordSchema).optional(),
13874
13878
  actions: exports_external.array(openclawActionDraftSchema).optional(),
13875
- deliveryState: openclawDeliveryStateSchema.optional()
13879
+ deliveryState: openclawDeliveryStateSchema.optional(),
13880
+ streamId: nonEmptyStringSchema
13876
13881
  }));
13877
13882
  var openclawMessageStreamSnapshotCommandBaseSchema = exports_external.object({
13878
13883
  type: exports_external.literal("openclaw_message_stream_snapshot"),
@@ -13884,6 +13889,7 @@ var openclawMessageStreamSnapshotCommandBaseSchema = exports_external.object({
13884
13889
  links: exports_external.array(openclawLinkSchema).default([]),
13885
13890
  toolCalls: exports_external.array(openclawToolCallRecordSchema).default([]),
13886
13891
  isFinal: exports_external.boolean(),
13892
+ snapshotSequence: exports_external.number().int().nonnegative(),
13887
13893
  updatedAt: timestampSchema
13888
13894
  });
13889
13895
  var openclawMessageStreamSnapshotCommandSchema = requireOpenClawRenderableContent(openclawMessageStreamSnapshotCommandBaseSchema);
@@ -13956,11 +13962,18 @@ var openclawRealtimeClientMessageSchema = exports_external.union([
13956
13962
  openclawSessionBoundCommandSchema,
13957
13963
  openclawActionResultCommandSchema
13958
13964
  ]);
13965
+ var openclawRealtimeAckPayloadSchema = exports_external.object({
13966
+ threadId: nonEmptyStringSchema.optional(),
13967
+ messageId: nonEmptyStringSchema.optional(),
13968
+ dropped: exports_external.literal(true).optional(),
13969
+ duplicate: exports_external.literal(true).optional(),
13970
+ reason: nonEmptyStringSchema.optional()
13971
+ });
13959
13972
  var openclawRealtimeAckSchema = exports_external.object({
13960
13973
  type: exports_external.literal("ack"),
13961
13974
  requestId: openclawRequestIdSchema,
13962
13975
  sequence: openclawSequenceSchema.optional(),
13963
- payload: jsonObjectSchema.optional()
13976
+ payload: openclawRealtimeAckPayloadSchema.optional()
13964
13977
  });
13965
13978
  var openclawRealtimeErrorSchema = exports_external.object({
13966
13979
  type: exports_external.literal("realtime_error"),
@@ -14009,6 +14022,13 @@ var openclawEventReplayResponseSchema = exports_external.object({
14009
14022
  ok: exports_external.literal(true),
14010
14023
  events: exports_external.array(openclawEventEnvelopeSchema)
14011
14024
  });
14025
+ var openclawPushNotificationPayloadSchema = exports_external.object({
14026
+ kind: exports_external.literal("openclaw"),
14027
+ version: exports_external.literal(1),
14028
+ threadId: nonEmptyStringSchema,
14029
+ messageId: nonEmptyStringSchema,
14030
+ url: nonEmptyStringSchema.optional()
14031
+ });
14012
14032
  // ../protocol/src/fixes.ts
14013
14033
  var fixScopeSchema = exports_external.enum(fixScopes);
14014
14034
  var fixCreateRequestSchema = exports_external.object({
@@ -14064,11 +14084,12 @@ var healthResponseSchema = exports_external.object({
14064
14084
  storageUnit: nonEmptyStringSchema
14065
14085
  });
14066
14086
  // ../protocol/src/push.ts
14087
+ var pushEnvironmentSchema = exports_external.enum(["development", "production"]);
14067
14088
  var pushRegisterRequestSchema = exports_external.object({
14068
14089
  identityToken: nonEmptyStringSchema,
14069
14090
  deviceToken: nonEmptyStringSchema,
14070
14091
  platform: nonEmptyStringSchema.optional(),
14071
- environment: nonEmptyStringSchema.optional(),
14092
+ environment: pushEnvironmentSchema.optional(),
14072
14093
  deviceName: nonEmptyStringSchema.optional()
14073
14094
  });
14074
14095
  var pushRegisterResponseSchema = exports_external.object({
@@ -14077,6 +14098,7 @@ var pushRegisterResponseSchema = exports_external.object({
14077
14098
  id: nonEmptyStringSchema
14078
14099
  });
14079
14100
  var pushUnregisterRequestSchema = exports_external.object({
14101
+ identityToken: nonEmptyStringSchema,
14080
14102
  deviceToken: nonEmptyStringSchema
14081
14103
  });
14082
14104
  var pushUnregisterResponseSchema = exports_external.object({
@@ -14100,6 +14122,7 @@ var pushResultSchema = exports_external.object({
14100
14122
  userId: nonEmptyStringSchema,
14101
14123
  sent: exports_external.number().int().nonnegative(),
14102
14124
  failed: exports_external.number().int().nonnegative(),
14125
+ skipped: exports_external.number().int().nonnegative().default(0),
14103
14126
  results: exports_external.array(jsonObjectSchema)
14104
14127
  });
14105
14128
  // ../protocol/src/responses.ts
@@ -14120,7 +14143,8 @@ var usersListEntrySchema = exports_external.object({
14120
14143
  enabledDeviceCount: exports_external.number().int().nonnegative()
14121
14144
  });
14122
14145
  var userDeviceSchema = exports_external.object({
14123
- deviceToken: nonEmptyStringSchema,
14146
+ id: nonEmptyStringSchema,
14147
+ deviceTokenPreview: nonEmptyStringSchema,
14124
14148
  platform: nonEmptyStringSchema,
14125
14149
  environment: nonEmptyStringSchema,
14126
14150
  enabled: exports_external.boolean(),
@@ -14192,10 +14216,12 @@ class CowtailRealtimeClient {
14192
14216
  }
14193
14217
  sendOpenClawMessage(command) {
14194
14218
  const requestId = this.#requestIdFactory();
14219
+ const idempotencyKey = command.idempotencyKey ?? `cowtail:request:${requestId}`;
14195
14220
  return this.#sendCommand({
14196
14221
  ...command,
14197
14222
  requestId,
14198
- idempotencyKey: command.idempotencyKey ?? `cowtail:request:${requestId}`,
14223
+ idempotencyKey,
14224
+ streamId: command.streamId ?? idempotencyKey,
14199
14225
  links: command.links ?? [],
14200
14226
  toolCalls: command.toolCalls ?? [],
14201
14227
  actions: command.actions ?? []
@@ -14519,6 +14545,30 @@ function normalizeCowtailTarget(raw) {
14519
14545
  function buildCowtailTarget(threadId) {
14520
14546
  return `cowtail:${normalizeCowtailTarget(threadId)}`;
14521
14547
  }
14548
+ function resolveCowtailThreadIdFromSessionKey(sessionKey) {
14549
+ const raw = (sessionKey ?? "").trim();
14550
+ if (!raw) {
14551
+ return;
14552
+ }
14553
+ const parts = raw.split(":").filter(Boolean);
14554
+ if (parts.length < 4 || parts[0]?.toLowerCase() !== "agent") {
14555
+ return;
14556
+ }
14557
+ const cowtailIndex = parts.findIndex((part, index) => index >= 2 && part.toLowerCase() === "cowtail");
14558
+ if (cowtailIndex < 0) {
14559
+ if (parts[2]?.toLowerCase() !== "direct") {
14560
+ return;
14561
+ }
14562
+ const directTarget = normalizeCowtailTarget(parts.slice(3).join(":"));
14563
+ return directTarget || undefined;
14564
+ }
14565
+ const rest = parts.slice(cowtailIndex + 1);
14566
+ const first = rest[0]?.toLowerCase();
14567
+ const second = rest[1]?.toLowerCase();
14568
+ const target = first === "direct" || first === "group" || first === "channel" ? rest.slice(1).join(":") : second === "direct" || second === "group" || second === "channel" ? rest.slice(2).join(":") : rest.join(":");
14569
+ const normalized = normalizeCowtailTarget(target);
14570
+ return normalized || undefined;
14571
+ }
14522
14572
  function isSupportedCowtailAgent(agentId) {
14523
14573
  return (agentId ?? "main").trim() === "main";
14524
14574
  }
@@ -14555,10 +14605,11 @@ async function handleCowtailEvent(params) {
14555
14605
  return;
14556
14606
  }
14557
14607
  if (!thread.sessionKey) {
14608
+ const threadId = resolveCowtailThreadId({ route, thread });
14558
14609
  await client.sendSessionBound({
14559
14610
  type: "openclaw_session_bound",
14560
- idempotencyKey: `cowtail:session-bound:${thread.id}`,
14561
- threadId: thread.id,
14611
+ idempotencyKey: `cowtail:session-bound:${threadId}`,
14612
+ threadId,
14562
14613
  sessionKey: route.sessionKey
14563
14614
  });
14564
14615
  }
@@ -14668,6 +14719,7 @@ async function dispatchCowtailTextTurn(params) {
14668
14719
  idempotencyKey: `cowtail:reply:${message.id}`,
14669
14720
  streamId: `cowtail:stream:${message.id}`,
14670
14721
  updateIndex: 0,
14722
+ snapshotSequence: 0,
14671
14723
  toolFallbackIndex: 0,
14672
14724
  toolFallbackIds: {},
14673
14725
  text: "",
@@ -14741,6 +14793,7 @@ async function dispatchCowtailActionTurn(params) {
14741
14793
  idempotencyKey: `cowtail:action:${action.id}`,
14742
14794
  streamId: `cowtail:action-stream:${action.id}`,
14743
14795
  updateIndex: 0,
14796
+ snapshotSequence: 0,
14744
14797
  toolFallbackIndex: 0,
14745
14798
  toolFallbackIds: {},
14746
14799
  text: "",
@@ -14921,28 +14974,17 @@ async function deliverCowtailReply(params) {
14921
14974
  updatedAt: Date.now()
14922
14975
  });
14923
14976
  if (!params.streamState.messageId) {
14924
- const result2 = await params.client.sendOpenClawMessage({
14925
- type: "openclaw_message",
14926
- idempotencyKey: params.streamState.idempotencyKey,
14927
- sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
14928
- title: params.thread.title,
14977
+ await createDurableCowtailReply(params, {
14929
14978
  text: messageText,
14930
- authorLabel: "OpenClaw",
14931
14979
  links,
14932
- toolCalls: params.streamState.toolCalls,
14933
- actions: [],
14934
14980
  deliveryState: "pending"
14935
14981
  });
14936
- const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14937
- if (!messageId2) {
14938
- return;
14939
- }
14940
- params.streamState.messageId = messageId2;
14941
14982
  return;
14942
14983
  }
14943
14984
  await params.client.sendOpenClawMessageUpdate({
14944
14985
  type: "openclaw_message_update",
14945
14986
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
14987
+ streamId: params.streamState.streamId,
14946
14988
  messageId: params.streamState.messageId,
14947
14989
  text: messageText,
14948
14990
  links,
@@ -14967,28 +15009,17 @@ async function deliverCowtailReply(params) {
14967
15009
  updatedAt: Date.now()
14968
15010
  });
14969
15011
  if (!params.streamState.messageId) {
14970
- const result2 = await params.client.sendOpenClawMessage({
14971
- type: "openclaw_message",
14972
- idempotencyKey: params.streamState.idempotencyKey,
14973
- sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
14974
- title: params.thread.title,
15012
+ await createDurableCowtailReply(params, {
14975
15013
  text: params.streamState.text,
14976
- authorLabel: "OpenClaw",
14977
15014
  links,
14978
- toolCalls: params.streamState.toolCalls,
14979
- actions: [],
14980
15015
  deliveryState: "pending"
14981
15016
  });
14982
- const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14983
- if (!messageId2) {
14984
- return;
14985
- }
14986
- params.streamState.messageId = messageId2;
14987
15017
  return;
14988
15018
  }
14989
15019
  await params.client.sendOpenClawMessageUpdate({
14990
15020
  type: "openclaw_message_update",
14991
15021
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
15022
+ streamId: params.streamState.streamId,
14992
15023
  messageId: params.streamState.messageId,
14993
15024
  text: params.streamState.text,
14994
15025
  links,
@@ -15002,6 +15033,7 @@ async function deliverCowtailReply(params) {
15002
15033
  await params.client.sendOpenClawMessageUpdate({
15003
15034
  type: "openclaw_message_update",
15004
15035
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
15036
+ streamId: params.streamState.streamId,
15005
15037
  messageId: params.streamState.messageId,
15006
15038
  text,
15007
15039
  links,
@@ -15023,23 +15055,14 @@ async function deliverCowtailReply(params) {
15023
15055
  return;
15024
15056
  }
15025
15057
  params.streamState.text = text;
15026
- const result = await params.client.sendOpenClawMessage({
15027
- type: "openclaw_message",
15028
- idempotencyKey: params.streamState.idempotencyKey,
15029
- sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
15030
- title: params.thread.title,
15058
+ const messageId = await createDurableCowtailReply(params, {
15031
15059
  text,
15032
- authorLabel: "OpenClaw",
15033
15060
  links,
15034
- toolCalls: params.streamState.toolCalls,
15035
- actions: [],
15036
15061
  deliveryState: "sent"
15037
15062
  });
15038
- const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
15039
15063
  if (!messageId) {
15040
15064
  return;
15041
15065
  }
15042
- params.streamState.messageId = messageId;
15043
15066
  emitCowtailStreamSnapshot({
15044
15067
  client: params.client,
15045
15068
  logger: params.logger,
@@ -15052,6 +15075,35 @@ async function deliverCowtailReply(params) {
15052
15075
  });
15053
15076
  params.streamState.completed = true;
15054
15077
  }
15078
+ async function createDurableCowtailReply(params, message) {
15079
+ try {
15080
+ const routedThreadId = resolveCowtailThreadIdFromSessionKey(params.route.sessionKey);
15081
+ const result = await params.client.sendOpenClawMessage({
15082
+ type: "openclaw_message",
15083
+ idempotencyKey: params.streamState.idempotencyKey,
15084
+ streamId: params.streamState.streamId,
15085
+ sessionKey: params.route.sessionKey,
15086
+ ...routedThreadId ? { threadId: routedThreadId } : {},
15087
+ title: params.thread.title,
15088
+ text: message.text,
15089
+ authorLabel: "OpenClaw",
15090
+ links: message.links,
15091
+ toolCalls: params.streamState.toolCalls,
15092
+ actions: [],
15093
+ deliveryState: message.deliveryState
15094
+ });
15095
+ const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
15096
+ if (!messageId) {
15097
+ failCowtailStreamBeforeDurableCreate(params);
15098
+ return;
15099
+ }
15100
+ params.streamState.messageId = messageId;
15101
+ return messageId;
15102
+ } catch (error48) {
15103
+ failCowtailStreamBeforeDurableCreate(params);
15104
+ throw error48;
15105
+ }
15106
+ }
15055
15107
  async function finalizeCowtailStreamedReply(params) {
15056
15108
  if (params.streamState.failed || params.streamState.completed || !params.streamState.messageId || !params.streamState.text.trim() && params.streamState.toolCalls.length === 0) {
15057
15109
  return;
@@ -15059,6 +15111,7 @@ async function finalizeCowtailStreamedReply(params) {
15059
15111
  await params.client.sendOpenClawMessageUpdate({
15060
15112
  type: "openclaw_message_update",
15061
15113
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
15114
+ streamId: params.streamState.streamId,
15062
15115
  messageId: params.streamState.messageId,
15063
15116
  text: params.streamState.text,
15064
15117
  links: params.streamState.links,
@@ -15140,26 +15193,46 @@ function deliverCowtailToolProgress(params) {
15140
15193
  });
15141
15194
  }
15142
15195
  function emitCowtailStreamSnapshot(params) {
15143
- if (params.streamState.failed || !params.text.trim() && params.streamState.links.length === 0 && params.streamState.toolCalls.length === 0) {
15196
+ if (params.streamState.failed && params.allowFailed !== true || !params.text.trim() && params.streamState.links.length === 0 && params.streamState.toolCalls.length === 0) {
15144
15197
  return;
15145
15198
  }
15146
15199
  params.streamState.liveText = params.text;
15147
15200
  params.streamState.lastSnapshotText = params.text;
15148
15201
  params.streamState.lastSnapshotSentAt = params.updatedAt;
15202
+ params.streamState.snapshotSequence += 1;
15149
15203
  params.client.sendOpenClawStreamSnapshot({
15150
15204
  type: "openclaw_message_stream_snapshot",
15151
15205
  streamId: params.streamState.streamId,
15152
- sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
15153
- threadId: params.thread.id,
15206
+ sessionKey: params.route.sessionKey,
15207
+ threadId: resolveCowtailThreadId(params),
15154
15208
  text: params.text,
15155
15209
  links: params.streamState.links.map((link) => ({ ...link })),
15156
15210
  toolCalls: structuredClone(params.streamState.toolCalls),
15157
15211
  isFinal: params.isFinal,
15212
+ snapshotSequence: params.streamState.snapshotSequence,
15158
15213
  updatedAt: params.updatedAt
15159
15214
  }).catch((error48) => {
15160
15215
  params.logger?.warn?.(`Cowtail stream snapshot send failed: ${errorMessage(error48)}`);
15161
15216
  });
15162
15217
  }
15218
+ function failCowtailStreamBeforeDurableCreate(params) {
15219
+ if (!params.streamState.messageId && params.streamState.snapshotSequence > 0) {
15220
+ const text = params.streamState.text.trim() ? params.streamState.text : currentLiveCowtailStreamText(params.streamState);
15221
+ emitCowtailStreamSnapshot({
15222
+ client: params.client,
15223
+ logger: params.logger,
15224
+ route: params.route,
15225
+ thread: params.thread,
15226
+ streamState: params.streamState,
15227
+ text,
15228
+ isFinal: true,
15229
+ updatedAt: Date.now(),
15230
+ allowFailed: true
15231
+ });
15232
+ }
15233
+ params.streamState.failed = true;
15234
+ params.streamState.completed = true;
15235
+ }
15163
15236
  function currentLiveCowtailStreamText(streamState) {
15164
15237
  return streamState.liveText ?? streamState.text;
15165
15238
  }
@@ -15357,7 +15430,7 @@ function buildCowtailInboundBody(params) {
15357
15430
  };
15358
15431
  }
15359
15432
  function finalizeInboundContext(params) {
15360
- const target = buildCowtailTarget(params.thread.id);
15433
+ const target = buildCowtailTarget(resolveCowtailThreadId(params));
15361
15434
  return params.runtime.channel.reply.finalizeInboundContext({
15362
15435
  Body: params.body,
15363
15436
  BodyForAgent: params.text,
@@ -15401,14 +15474,17 @@ function resolveThreadRoute(params) {
15401
15474
  sessionKey: params.thread.sessionKey
15402
15475
  };
15403
15476
  }
15404
- const route = params.runtime.channel.routing.resolveAgentRoute({
15405
- cfg: params.runtime.config.loadConfig(),
15406
- channel: CHANNEL_ID,
15477
+ const threadId = normalizeCowtailTarget(params.thread.id);
15478
+ if (!threadId) {
15479
+ params.logger?.warn?.("Cowtail thread_created event has a blank thread id");
15480
+ return null;
15481
+ }
15482
+ const cfg = params.runtime.config.loadConfig();
15483
+ const route = resolveCowtailAgentRoute({
15484
+ cfg,
15485
+ threadId,
15407
15486
  accountId: params.account.accountId,
15408
- peer: {
15409
- kind: "direct",
15410
- id: buildCowtailTarget(params.thread.id)
15411
- }
15487
+ runtime: params.runtime
15412
15488
  });
15413
15489
  if (!isSupportedCowtailAgent(route.agentId)) {
15414
15490
  params.logger?.warn?.(`Cowtail thread ${params.thread.id} resolved unsupported agent ${route.agentId}`);
@@ -15419,6 +15495,45 @@ function resolveThreadRoute(params) {
15419
15495
  sessionKey: route.sessionKey
15420
15496
  };
15421
15497
  }
15498
+ function resolveCowtailAgentRoute(params) {
15499
+ const rawRoute = resolveAgentRouteForPeer({
15500
+ cfg: params.cfg,
15501
+ accountId: params.accountId,
15502
+ runtime: params.runtime,
15503
+ peerId: params.threadId
15504
+ });
15505
+ if (isPeerBindingRoute(rawRoute)) {
15506
+ return rawRoute;
15507
+ }
15508
+ const prefixedThreadId = buildCowtailTarget(params.threadId);
15509
+ if (prefixedThreadId === params.threadId) {
15510
+ return rawRoute;
15511
+ }
15512
+ const prefixedRoute = resolveAgentRouteForPeer({
15513
+ cfg: params.cfg,
15514
+ accountId: params.accountId,
15515
+ runtime: params.runtime,
15516
+ peerId: prefixedThreadId
15517
+ });
15518
+ return isPeerBindingRoute(prefixedRoute) ? prefixedRoute : rawRoute;
15519
+ }
15520
+ function resolveAgentRouteForPeer(params) {
15521
+ return params.runtime.channel.routing.resolveAgentRoute({
15522
+ cfg: params.cfg,
15523
+ channel: CHANNEL_ID,
15524
+ accountId: params.accountId,
15525
+ peer: {
15526
+ kind: "direct",
15527
+ id: params.peerId
15528
+ }
15529
+ });
15530
+ }
15531
+ function isPeerBindingRoute(route) {
15532
+ return route.matchedBy === "binding.peer";
15533
+ }
15534
+ function resolveCowtailThreadId(params) {
15535
+ return resolveCowtailThreadIdFromSessionKey(params.route.sessionKey) ?? normalizeCowtailTarget(params.thread.id);
15536
+ }
15422
15537
  function errorMessage(error48) {
15423
15538
  return error48 instanceof Error ? error48.message : String(error48);
15424
15539
  }
@@ -15436,23 +15551,29 @@ function resolveCowtailTarget(to) {
15436
15551
  if (!normalizedTarget) {
15437
15552
  throw new Error("Cowtail target must not be blank");
15438
15553
  }
15439
- return buildCowtailTarget(normalizedTarget);
15554
+ return normalizedTarget;
15440
15555
  }
15441
15556
  async function sendCowtailText(params) {
15442
15557
  const { client, to } = params;
15443
15558
  const text = ensureNonBlankText(params.text);
15444
15559
  const target = resolveCowtailTarget(to);
15560
+ const sessionKey = buildCowtailTarget(target);
15445
15561
  const result = await client.sendOpenClawMessage({
15446
15562
  type: "openclaw_message",
15447
- sessionKey: target,
15563
+ sessionKey,
15564
+ threadHint: target,
15448
15565
  text,
15449
15566
  links: [],
15450
15567
  actions: []
15451
15568
  });
15569
+ const messageId = result.payload?.messageId;
15570
+ if (typeof messageId !== "string" || messageId.trim() === "") {
15571
+ throw new Error("Cowtail did not acknowledge a durable message id");
15572
+ }
15452
15573
  return {
15453
15574
  channel: "cowtail",
15454
- messageId: String(result.sequence ?? result.requestId),
15455
- to: target
15575
+ messageId,
15576
+ to: buildCowtailTarget(target)
15456
15577
  };
15457
15578
  }
15458
15579
 
@@ -13801,6 +13801,7 @@ var openclawThreadRecordSchema = exports_external.object({
13801
13801
  var openclawMessageRecordBaseSchema = exports_external.object({
13802
13802
  id: nonEmptyStringSchema,
13803
13803
  threadId: nonEmptyStringSchema,
13804
+ streamId: nonEmptyStringSchema.optional(),
13804
13805
  direction: openclawMessageDirectionSchema,
13805
13806
  authorLabel: nonEmptyStringSchema.optional(),
13806
13807
  text: openclawMessageTextSchema,
@@ -13855,13 +13856,16 @@ var openclawPluginMessageCommandSchema = requireOpenClawRenderableContent(export
13855
13856
  requestId: openclawRequestIdSchema,
13856
13857
  idempotencyKey: openclawIdempotencyKeySchema,
13857
13858
  sessionKey: nonEmptyStringSchema,
13859
+ threadId: nonEmptyStringSchema.optional(),
13860
+ threadHint: nonEmptyStringSchema.optional(),
13858
13861
  title: nonEmptyStringSchema.optional(),
13859
13862
  text: openclawMessageTextSchema,
13860
13863
  authorLabel: nonEmptyStringSchema.optional(),
13861
13864
  links: exports_external.array(openclawLinkSchema).default([]),
13862
13865
  toolCalls: exports_external.array(openclawToolCallRecordSchema).default([]),
13863
13866
  actions: exports_external.array(openclawActionDraftSchema).default([]),
13864
- deliveryState: openclawDeliveryStateSchema.optional()
13867
+ deliveryState: openclawDeliveryStateSchema.optional(),
13868
+ streamId: nonEmptyStringSchema
13865
13869
  }));
13866
13870
  var openclawPluginMessageUpdateCommandSchema = requireOpenClawRenderableContent(exports_external.object({
13867
13871
  type: exports_external.literal("openclaw_message_update"),
@@ -13872,7 +13876,8 @@ var openclawPluginMessageUpdateCommandSchema = requireOpenClawRenderableContent(
13872
13876
  links: exports_external.array(openclawLinkSchema).optional(),
13873
13877
  toolCalls: exports_external.array(openclawToolCallRecordSchema).optional(),
13874
13878
  actions: exports_external.array(openclawActionDraftSchema).optional(),
13875
- deliveryState: openclawDeliveryStateSchema.optional()
13879
+ deliveryState: openclawDeliveryStateSchema.optional(),
13880
+ streamId: nonEmptyStringSchema
13876
13881
  }));
13877
13882
  var openclawMessageStreamSnapshotCommandBaseSchema = exports_external.object({
13878
13883
  type: exports_external.literal("openclaw_message_stream_snapshot"),
@@ -13884,6 +13889,7 @@ var openclawMessageStreamSnapshotCommandBaseSchema = exports_external.object({
13884
13889
  links: exports_external.array(openclawLinkSchema).default([]),
13885
13890
  toolCalls: exports_external.array(openclawToolCallRecordSchema).default([]),
13886
13891
  isFinal: exports_external.boolean(),
13892
+ snapshotSequence: exports_external.number().int().nonnegative(),
13887
13893
  updatedAt: timestampSchema
13888
13894
  });
13889
13895
  var openclawMessageStreamSnapshotCommandSchema = requireOpenClawRenderableContent(openclawMessageStreamSnapshotCommandBaseSchema);
@@ -13956,11 +13962,18 @@ var openclawRealtimeClientMessageSchema = exports_external.union([
13956
13962
  openclawSessionBoundCommandSchema,
13957
13963
  openclawActionResultCommandSchema
13958
13964
  ]);
13965
+ var openclawRealtimeAckPayloadSchema = exports_external.object({
13966
+ threadId: nonEmptyStringSchema.optional(),
13967
+ messageId: nonEmptyStringSchema.optional(),
13968
+ dropped: exports_external.literal(true).optional(),
13969
+ duplicate: exports_external.literal(true).optional(),
13970
+ reason: nonEmptyStringSchema.optional()
13971
+ });
13959
13972
  var openclawRealtimeAckSchema = exports_external.object({
13960
13973
  type: exports_external.literal("ack"),
13961
13974
  requestId: openclawRequestIdSchema,
13962
13975
  sequence: openclawSequenceSchema.optional(),
13963
- payload: jsonObjectSchema.optional()
13976
+ payload: openclawRealtimeAckPayloadSchema.optional()
13964
13977
  });
13965
13978
  var openclawRealtimeErrorSchema = exports_external.object({
13966
13979
  type: exports_external.literal("realtime_error"),
@@ -14009,6 +14022,13 @@ var openclawEventReplayResponseSchema = exports_external.object({
14009
14022
  ok: exports_external.literal(true),
14010
14023
  events: exports_external.array(openclawEventEnvelopeSchema)
14011
14024
  });
14025
+ var openclawPushNotificationPayloadSchema = exports_external.object({
14026
+ kind: exports_external.literal("openclaw"),
14027
+ version: exports_external.literal(1),
14028
+ threadId: nonEmptyStringSchema,
14029
+ messageId: nonEmptyStringSchema,
14030
+ url: nonEmptyStringSchema.optional()
14031
+ });
14012
14032
  // ../protocol/src/fixes.ts
14013
14033
  var fixScopeSchema = exports_external.enum(fixScopes);
14014
14034
  var fixCreateRequestSchema = exports_external.object({
@@ -14064,11 +14084,12 @@ var healthResponseSchema = exports_external.object({
14064
14084
  storageUnit: nonEmptyStringSchema
14065
14085
  });
14066
14086
  // ../protocol/src/push.ts
14087
+ var pushEnvironmentSchema = exports_external.enum(["development", "production"]);
14067
14088
  var pushRegisterRequestSchema = exports_external.object({
14068
14089
  identityToken: nonEmptyStringSchema,
14069
14090
  deviceToken: nonEmptyStringSchema,
14070
14091
  platform: nonEmptyStringSchema.optional(),
14071
- environment: nonEmptyStringSchema.optional(),
14092
+ environment: pushEnvironmentSchema.optional(),
14072
14093
  deviceName: nonEmptyStringSchema.optional()
14073
14094
  });
14074
14095
  var pushRegisterResponseSchema = exports_external.object({
@@ -14077,6 +14098,7 @@ var pushRegisterResponseSchema = exports_external.object({
14077
14098
  id: nonEmptyStringSchema
14078
14099
  });
14079
14100
  var pushUnregisterRequestSchema = exports_external.object({
14101
+ identityToken: nonEmptyStringSchema,
14080
14102
  deviceToken: nonEmptyStringSchema
14081
14103
  });
14082
14104
  var pushUnregisterResponseSchema = exports_external.object({
@@ -14100,6 +14122,7 @@ var pushResultSchema = exports_external.object({
14100
14122
  userId: nonEmptyStringSchema,
14101
14123
  sent: exports_external.number().int().nonnegative(),
14102
14124
  failed: exports_external.number().int().nonnegative(),
14125
+ skipped: exports_external.number().int().nonnegative().default(0),
14103
14126
  results: exports_external.array(jsonObjectSchema)
14104
14127
  });
14105
14128
  // ../protocol/src/responses.ts
@@ -14120,7 +14143,8 @@ var usersListEntrySchema = exports_external.object({
14120
14143
  enabledDeviceCount: exports_external.number().int().nonnegative()
14121
14144
  });
14122
14145
  var userDeviceSchema = exports_external.object({
14123
- deviceToken: nonEmptyStringSchema,
14146
+ id: nonEmptyStringSchema,
14147
+ deviceTokenPreview: nonEmptyStringSchema,
14124
14148
  platform: nonEmptyStringSchema,
14125
14149
  environment: nonEmptyStringSchema,
14126
14150
  enabled: exports_external.boolean(),
@@ -14192,10 +14216,12 @@ class CowtailRealtimeClient {
14192
14216
  }
14193
14217
  sendOpenClawMessage(command) {
14194
14218
  const requestId = this.#requestIdFactory();
14219
+ const idempotencyKey = command.idempotencyKey ?? `cowtail:request:${requestId}`;
14195
14220
  return this.#sendCommand({
14196
14221
  ...command,
14197
14222
  requestId,
14198
- idempotencyKey: command.idempotencyKey ?? `cowtail:request:${requestId}`,
14223
+ idempotencyKey,
14224
+ streamId: command.streamId ?? idempotencyKey,
14199
14225
  links: command.links ?? [],
14200
14226
  toolCalls: command.toolCalls ?? [],
14201
14227
  actions: command.actions ?? []
@@ -14519,6 +14545,30 @@ function normalizeCowtailTarget(raw) {
14519
14545
  function buildCowtailTarget(threadId) {
14520
14546
  return `cowtail:${normalizeCowtailTarget(threadId)}`;
14521
14547
  }
14548
+ function resolveCowtailThreadIdFromSessionKey(sessionKey) {
14549
+ const raw = (sessionKey ?? "").trim();
14550
+ if (!raw) {
14551
+ return;
14552
+ }
14553
+ const parts = raw.split(":").filter(Boolean);
14554
+ if (parts.length < 4 || parts[0]?.toLowerCase() !== "agent") {
14555
+ return;
14556
+ }
14557
+ const cowtailIndex = parts.findIndex((part, index) => index >= 2 && part.toLowerCase() === "cowtail");
14558
+ if (cowtailIndex < 0) {
14559
+ if (parts[2]?.toLowerCase() !== "direct") {
14560
+ return;
14561
+ }
14562
+ const directTarget = normalizeCowtailTarget(parts.slice(3).join(":"));
14563
+ return directTarget || undefined;
14564
+ }
14565
+ const rest = parts.slice(cowtailIndex + 1);
14566
+ const first = rest[0]?.toLowerCase();
14567
+ const second = rest[1]?.toLowerCase();
14568
+ const target = first === "direct" || first === "group" || first === "channel" ? rest.slice(1).join(":") : second === "direct" || second === "group" || second === "channel" ? rest.slice(2).join(":") : rest.join(":");
14569
+ const normalized = normalizeCowtailTarget(target);
14570
+ return normalized || undefined;
14571
+ }
14522
14572
  function isSupportedCowtailAgent(agentId) {
14523
14573
  return (agentId ?? "main").trim() === "main";
14524
14574
  }
@@ -14555,10 +14605,11 @@ async function handleCowtailEvent(params) {
14555
14605
  return;
14556
14606
  }
14557
14607
  if (!thread.sessionKey) {
14608
+ const threadId = resolveCowtailThreadId({ route, thread });
14558
14609
  await client.sendSessionBound({
14559
14610
  type: "openclaw_session_bound",
14560
- idempotencyKey: `cowtail:session-bound:${thread.id}`,
14561
- threadId: thread.id,
14611
+ idempotencyKey: `cowtail:session-bound:${threadId}`,
14612
+ threadId,
14562
14613
  sessionKey: route.sessionKey
14563
14614
  });
14564
14615
  }
@@ -14668,6 +14719,7 @@ async function dispatchCowtailTextTurn(params) {
14668
14719
  idempotencyKey: `cowtail:reply:${message.id}`,
14669
14720
  streamId: `cowtail:stream:${message.id}`,
14670
14721
  updateIndex: 0,
14722
+ snapshotSequence: 0,
14671
14723
  toolFallbackIndex: 0,
14672
14724
  toolFallbackIds: {},
14673
14725
  text: "",
@@ -14741,6 +14793,7 @@ async function dispatchCowtailActionTurn(params) {
14741
14793
  idempotencyKey: `cowtail:action:${action.id}`,
14742
14794
  streamId: `cowtail:action-stream:${action.id}`,
14743
14795
  updateIndex: 0,
14796
+ snapshotSequence: 0,
14744
14797
  toolFallbackIndex: 0,
14745
14798
  toolFallbackIds: {},
14746
14799
  text: "",
@@ -14921,28 +14974,17 @@ async function deliverCowtailReply(params) {
14921
14974
  updatedAt: Date.now()
14922
14975
  });
14923
14976
  if (!params.streamState.messageId) {
14924
- const result2 = await params.client.sendOpenClawMessage({
14925
- type: "openclaw_message",
14926
- idempotencyKey: params.streamState.idempotencyKey,
14927
- sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
14928
- title: params.thread.title,
14977
+ await createDurableCowtailReply(params, {
14929
14978
  text: messageText,
14930
- authorLabel: "OpenClaw",
14931
14979
  links,
14932
- toolCalls: params.streamState.toolCalls,
14933
- actions: [],
14934
14980
  deliveryState: "pending"
14935
14981
  });
14936
- const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14937
- if (!messageId2) {
14938
- return;
14939
- }
14940
- params.streamState.messageId = messageId2;
14941
14982
  return;
14942
14983
  }
14943
14984
  await params.client.sendOpenClawMessageUpdate({
14944
14985
  type: "openclaw_message_update",
14945
14986
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
14987
+ streamId: params.streamState.streamId,
14946
14988
  messageId: params.streamState.messageId,
14947
14989
  text: messageText,
14948
14990
  links,
@@ -14967,28 +15009,17 @@ async function deliverCowtailReply(params) {
14967
15009
  updatedAt: Date.now()
14968
15010
  });
14969
15011
  if (!params.streamState.messageId) {
14970
- const result2 = await params.client.sendOpenClawMessage({
14971
- type: "openclaw_message",
14972
- idempotencyKey: params.streamState.idempotencyKey,
14973
- sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
14974
- title: params.thread.title,
15012
+ await createDurableCowtailReply(params, {
14975
15013
  text: params.streamState.text,
14976
- authorLabel: "OpenClaw",
14977
15014
  links,
14978
- toolCalls: params.streamState.toolCalls,
14979
- actions: [],
14980
15015
  deliveryState: "pending"
14981
15016
  });
14982
- const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14983
- if (!messageId2) {
14984
- return;
14985
- }
14986
- params.streamState.messageId = messageId2;
14987
15017
  return;
14988
15018
  }
14989
15019
  await params.client.sendOpenClawMessageUpdate({
14990
15020
  type: "openclaw_message_update",
14991
15021
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
15022
+ streamId: params.streamState.streamId,
14992
15023
  messageId: params.streamState.messageId,
14993
15024
  text: params.streamState.text,
14994
15025
  links,
@@ -15002,6 +15033,7 @@ async function deliverCowtailReply(params) {
15002
15033
  await params.client.sendOpenClawMessageUpdate({
15003
15034
  type: "openclaw_message_update",
15004
15035
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
15036
+ streamId: params.streamState.streamId,
15005
15037
  messageId: params.streamState.messageId,
15006
15038
  text,
15007
15039
  links,
@@ -15023,23 +15055,14 @@ async function deliverCowtailReply(params) {
15023
15055
  return;
15024
15056
  }
15025
15057
  params.streamState.text = text;
15026
- const result = await params.client.sendOpenClawMessage({
15027
- type: "openclaw_message",
15028
- idempotencyKey: params.streamState.idempotencyKey,
15029
- sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
15030
- title: params.thread.title,
15058
+ const messageId = await createDurableCowtailReply(params, {
15031
15059
  text,
15032
- authorLabel: "OpenClaw",
15033
15060
  links,
15034
- toolCalls: params.streamState.toolCalls,
15035
- actions: [],
15036
15061
  deliveryState: "sent"
15037
15062
  });
15038
- const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
15039
15063
  if (!messageId) {
15040
15064
  return;
15041
15065
  }
15042
- params.streamState.messageId = messageId;
15043
15066
  emitCowtailStreamSnapshot({
15044
15067
  client: params.client,
15045
15068
  logger: params.logger,
@@ -15052,6 +15075,35 @@ async function deliverCowtailReply(params) {
15052
15075
  });
15053
15076
  params.streamState.completed = true;
15054
15077
  }
15078
+ async function createDurableCowtailReply(params, message) {
15079
+ try {
15080
+ const routedThreadId = resolveCowtailThreadIdFromSessionKey(params.route.sessionKey);
15081
+ const result = await params.client.sendOpenClawMessage({
15082
+ type: "openclaw_message",
15083
+ idempotencyKey: params.streamState.idempotencyKey,
15084
+ streamId: params.streamState.streamId,
15085
+ sessionKey: params.route.sessionKey,
15086
+ ...routedThreadId ? { threadId: routedThreadId } : {},
15087
+ title: params.thread.title,
15088
+ text: message.text,
15089
+ authorLabel: "OpenClaw",
15090
+ links: message.links,
15091
+ toolCalls: params.streamState.toolCalls,
15092
+ actions: [],
15093
+ deliveryState: message.deliveryState
15094
+ });
15095
+ const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
15096
+ if (!messageId) {
15097
+ failCowtailStreamBeforeDurableCreate(params);
15098
+ return;
15099
+ }
15100
+ params.streamState.messageId = messageId;
15101
+ return messageId;
15102
+ } catch (error48) {
15103
+ failCowtailStreamBeforeDurableCreate(params);
15104
+ throw error48;
15105
+ }
15106
+ }
15055
15107
  async function finalizeCowtailStreamedReply(params) {
15056
15108
  if (params.streamState.failed || params.streamState.completed || !params.streamState.messageId || !params.streamState.text.trim() && params.streamState.toolCalls.length === 0) {
15057
15109
  return;
@@ -15059,6 +15111,7 @@ async function finalizeCowtailStreamedReply(params) {
15059
15111
  await params.client.sendOpenClawMessageUpdate({
15060
15112
  type: "openclaw_message_update",
15061
15113
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
15114
+ streamId: params.streamState.streamId,
15062
15115
  messageId: params.streamState.messageId,
15063
15116
  text: params.streamState.text,
15064
15117
  links: params.streamState.links,
@@ -15140,26 +15193,46 @@ function deliverCowtailToolProgress(params) {
15140
15193
  });
15141
15194
  }
15142
15195
  function emitCowtailStreamSnapshot(params) {
15143
- if (params.streamState.failed || !params.text.trim() && params.streamState.links.length === 0 && params.streamState.toolCalls.length === 0) {
15196
+ if (params.streamState.failed && params.allowFailed !== true || !params.text.trim() && params.streamState.links.length === 0 && params.streamState.toolCalls.length === 0) {
15144
15197
  return;
15145
15198
  }
15146
15199
  params.streamState.liveText = params.text;
15147
15200
  params.streamState.lastSnapshotText = params.text;
15148
15201
  params.streamState.lastSnapshotSentAt = params.updatedAt;
15202
+ params.streamState.snapshotSequence += 1;
15149
15203
  params.client.sendOpenClawStreamSnapshot({
15150
15204
  type: "openclaw_message_stream_snapshot",
15151
15205
  streamId: params.streamState.streamId,
15152
- sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
15153
- threadId: params.thread.id,
15206
+ sessionKey: params.route.sessionKey,
15207
+ threadId: resolveCowtailThreadId(params),
15154
15208
  text: params.text,
15155
15209
  links: params.streamState.links.map((link) => ({ ...link })),
15156
15210
  toolCalls: structuredClone(params.streamState.toolCalls),
15157
15211
  isFinal: params.isFinal,
15212
+ snapshotSequence: params.streamState.snapshotSequence,
15158
15213
  updatedAt: params.updatedAt
15159
15214
  }).catch((error48) => {
15160
15215
  params.logger?.warn?.(`Cowtail stream snapshot send failed: ${errorMessage(error48)}`);
15161
15216
  });
15162
15217
  }
15218
+ function failCowtailStreamBeforeDurableCreate(params) {
15219
+ if (!params.streamState.messageId && params.streamState.snapshotSequence > 0) {
15220
+ const text = params.streamState.text.trim() ? params.streamState.text : currentLiveCowtailStreamText(params.streamState);
15221
+ emitCowtailStreamSnapshot({
15222
+ client: params.client,
15223
+ logger: params.logger,
15224
+ route: params.route,
15225
+ thread: params.thread,
15226
+ streamState: params.streamState,
15227
+ text,
15228
+ isFinal: true,
15229
+ updatedAt: Date.now(),
15230
+ allowFailed: true
15231
+ });
15232
+ }
15233
+ params.streamState.failed = true;
15234
+ params.streamState.completed = true;
15235
+ }
15163
15236
  function currentLiveCowtailStreamText(streamState) {
15164
15237
  return streamState.liveText ?? streamState.text;
15165
15238
  }
@@ -15357,7 +15430,7 @@ function buildCowtailInboundBody(params) {
15357
15430
  };
15358
15431
  }
15359
15432
  function finalizeInboundContext(params) {
15360
- const target = buildCowtailTarget(params.thread.id);
15433
+ const target = buildCowtailTarget(resolveCowtailThreadId(params));
15361
15434
  return params.runtime.channel.reply.finalizeInboundContext({
15362
15435
  Body: params.body,
15363
15436
  BodyForAgent: params.text,
@@ -15401,14 +15474,17 @@ function resolveThreadRoute(params) {
15401
15474
  sessionKey: params.thread.sessionKey
15402
15475
  };
15403
15476
  }
15404
- const route = params.runtime.channel.routing.resolveAgentRoute({
15405
- cfg: params.runtime.config.loadConfig(),
15406
- channel: CHANNEL_ID,
15477
+ const threadId = normalizeCowtailTarget(params.thread.id);
15478
+ if (!threadId) {
15479
+ params.logger?.warn?.("Cowtail thread_created event has a blank thread id");
15480
+ return null;
15481
+ }
15482
+ const cfg = params.runtime.config.loadConfig();
15483
+ const route = resolveCowtailAgentRoute({
15484
+ cfg,
15485
+ threadId,
15407
15486
  accountId: params.account.accountId,
15408
- peer: {
15409
- kind: "direct",
15410
- id: buildCowtailTarget(params.thread.id)
15411
- }
15487
+ runtime: params.runtime
15412
15488
  });
15413
15489
  if (!isSupportedCowtailAgent(route.agentId)) {
15414
15490
  params.logger?.warn?.(`Cowtail thread ${params.thread.id} resolved unsupported agent ${route.agentId}`);
@@ -15419,6 +15495,45 @@ function resolveThreadRoute(params) {
15419
15495
  sessionKey: route.sessionKey
15420
15496
  };
15421
15497
  }
15498
+ function resolveCowtailAgentRoute(params) {
15499
+ const rawRoute = resolveAgentRouteForPeer({
15500
+ cfg: params.cfg,
15501
+ accountId: params.accountId,
15502
+ runtime: params.runtime,
15503
+ peerId: params.threadId
15504
+ });
15505
+ if (isPeerBindingRoute(rawRoute)) {
15506
+ return rawRoute;
15507
+ }
15508
+ const prefixedThreadId = buildCowtailTarget(params.threadId);
15509
+ if (prefixedThreadId === params.threadId) {
15510
+ return rawRoute;
15511
+ }
15512
+ const prefixedRoute = resolveAgentRouteForPeer({
15513
+ cfg: params.cfg,
15514
+ accountId: params.accountId,
15515
+ runtime: params.runtime,
15516
+ peerId: prefixedThreadId
15517
+ });
15518
+ return isPeerBindingRoute(prefixedRoute) ? prefixedRoute : rawRoute;
15519
+ }
15520
+ function resolveAgentRouteForPeer(params) {
15521
+ return params.runtime.channel.routing.resolveAgentRoute({
15522
+ cfg: params.cfg,
15523
+ channel: CHANNEL_ID,
15524
+ accountId: params.accountId,
15525
+ peer: {
15526
+ kind: "direct",
15527
+ id: params.peerId
15528
+ }
15529
+ });
15530
+ }
15531
+ function isPeerBindingRoute(route) {
15532
+ return route.matchedBy === "binding.peer";
15533
+ }
15534
+ function resolveCowtailThreadId(params) {
15535
+ return resolveCowtailThreadIdFromSessionKey(params.route.sessionKey) ?? normalizeCowtailTarget(params.thread.id);
15536
+ }
15422
15537
  function errorMessage(error48) {
15423
15538
  return error48 instanceof Error ? error48.message : String(error48);
15424
15539
  }
@@ -15436,23 +15551,29 @@ function resolveCowtailTarget(to) {
15436
15551
  if (!normalizedTarget) {
15437
15552
  throw new Error("Cowtail target must not be blank");
15438
15553
  }
15439
- return buildCowtailTarget(normalizedTarget);
15554
+ return normalizedTarget;
15440
15555
  }
15441
15556
  async function sendCowtailText(params) {
15442
15557
  const { client, to } = params;
15443
15558
  const text = ensureNonBlankText(params.text);
15444
15559
  const target = resolveCowtailTarget(to);
15560
+ const sessionKey = buildCowtailTarget(target);
15445
15561
  const result = await client.sendOpenClawMessage({
15446
15562
  type: "openclaw_message",
15447
- sessionKey: target,
15563
+ sessionKey,
15564
+ threadHint: target,
15448
15565
  text,
15449
15566
  links: [],
15450
15567
  actions: []
15451
15568
  });
15569
+ const messageId = result.payload?.messageId;
15570
+ if (typeof messageId !== "string" || messageId.trim() === "") {
15571
+ throw new Error("Cowtail did not acknowledge a durable message id");
15572
+ }
15452
15573
  return {
15453
15574
  channel: "cowtail",
15454
- messageId: String(result.sequence ?? result.requestId),
15455
- to: target
15575
+ messageId,
15576
+ to: buildCowtailTarget(target)
15456
15577
  };
15457
15578
  }
15458
15579
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maudecode/openclaw-cowtail",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "Cowtail channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "repository": {