@maudecode/openclaw-cowtail 0.13.0 → 0.14.0

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 ?? []
@@ -14668,6 +14694,7 @@ async function dispatchCowtailTextTurn(params) {
14668
14694
  idempotencyKey: `cowtail:reply:${message.id}`,
14669
14695
  streamId: `cowtail:stream:${message.id}`,
14670
14696
  updateIndex: 0,
14697
+ snapshotSequence: 0,
14671
14698
  toolFallbackIndex: 0,
14672
14699
  toolFallbackIds: {},
14673
14700
  text: "",
@@ -14741,6 +14768,7 @@ async function dispatchCowtailActionTurn(params) {
14741
14768
  idempotencyKey: `cowtail:action:${action.id}`,
14742
14769
  streamId: `cowtail:action-stream:${action.id}`,
14743
14770
  updateIndex: 0,
14771
+ snapshotSequence: 0,
14744
14772
  toolFallbackIndex: 0,
14745
14773
  toolFallbackIds: {},
14746
14774
  text: "",
@@ -14921,28 +14949,17 @@ async function deliverCowtailReply(params) {
14921
14949
  updatedAt: Date.now()
14922
14950
  });
14923
14951
  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,
14952
+ await createDurableCowtailReply(params, {
14929
14953
  text: messageText,
14930
- authorLabel: "OpenClaw",
14931
14954
  links,
14932
- toolCalls: params.streamState.toolCalls,
14933
- actions: [],
14934
14955
  deliveryState: "pending"
14935
14956
  });
14936
- const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14937
- if (!messageId2) {
14938
- return;
14939
- }
14940
- params.streamState.messageId = messageId2;
14941
14957
  return;
14942
14958
  }
14943
14959
  await params.client.sendOpenClawMessageUpdate({
14944
14960
  type: "openclaw_message_update",
14945
14961
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
14962
+ streamId: params.streamState.streamId,
14946
14963
  messageId: params.streamState.messageId,
14947
14964
  text: messageText,
14948
14965
  links,
@@ -14967,28 +14984,17 @@ async function deliverCowtailReply(params) {
14967
14984
  updatedAt: Date.now()
14968
14985
  });
14969
14986
  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,
14987
+ await createDurableCowtailReply(params, {
14975
14988
  text: params.streamState.text,
14976
- authorLabel: "OpenClaw",
14977
14989
  links,
14978
- toolCalls: params.streamState.toolCalls,
14979
- actions: [],
14980
14990
  deliveryState: "pending"
14981
14991
  });
14982
- const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14983
- if (!messageId2) {
14984
- return;
14985
- }
14986
- params.streamState.messageId = messageId2;
14987
14992
  return;
14988
14993
  }
14989
14994
  await params.client.sendOpenClawMessageUpdate({
14990
14995
  type: "openclaw_message_update",
14991
14996
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
14997
+ streamId: params.streamState.streamId,
14992
14998
  messageId: params.streamState.messageId,
14993
14999
  text: params.streamState.text,
14994
15000
  links,
@@ -15002,6 +15008,7 @@ async function deliverCowtailReply(params) {
15002
15008
  await params.client.sendOpenClawMessageUpdate({
15003
15009
  type: "openclaw_message_update",
15004
15010
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
15011
+ streamId: params.streamState.streamId,
15005
15012
  messageId: params.streamState.messageId,
15006
15013
  text,
15007
15014
  links,
@@ -15023,23 +15030,14 @@ async function deliverCowtailReply(params) {
15023
15030
  return;
15024
15031
  }
15025
15032
  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,
15033
+ const messageId = await createDurableCowtailReply(params, {
15031
15034
  text,
15032
- authorLabel: "OpenClaw",
15033
15035
  links,
15034
- toolCalls: params.streamState.toolCalls,
15035
- actions: [],
15036
15036
  deliveryState: "sent"
15037
15037
  });
15038
- const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
15039
15038
  if (!messageId) {
15040
15039
  return;
15041
15040
  }
15042
- params.streamState.messageId = messageId;
15043
15041
  emitCowtailStreamSnapshot({
15044
15042
  client: params.client,
15045
15043
  logger: params.logger,
@@ -15052,6 +15050,33 @@ async function deliverCowtailReply(params) {
15052
15050
  });
15053
15051
  params.streamState.completed = true;
15054
15052
  }
15053
+ async function createDurableCowtailReply(params, message) {
15054
+ try {
15055
+ const result = await params.client.sendOpenClawMessage({
15056
+ type: "openclaw_message",
15057
+ idempotencyKey: params.streamState.idempotencyKey,
15058
+ streamId: params.streamState.streamId,
15059
+ sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
15060
+ title: params.thread.title,
15061
+ text: message.text,
15062
+ authorLabel: "OpenClaw",
15063
+ links: message.links,
15064
+ toolCalls: params.streamState.toolCalls,
15065
+ actions: [],
15066
+ deliveryState: message.deliveryState
15067
+ });
15068
+ const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
15069
+ if (!messageId) {
15070
+ failCowtailStreamBeforeDurableCreate(params);
15071
+ return;
15072
+ }
15073
+ params.streamState.messageId = messageId;
15074
+ return messageId;
15075
+ } catch (error48) {
15076
+ failCowtailStreamBeforeDurableCreate(params);
15077
+ throw error48;
15078
+ }
15079
+ }
15055
15080
  async function finalizeCowtailStreamedReply(params) {
15056
15081
  if (params.streamState.failed || params.streamState.completed || !params.streamState.messageId || !params.streamState.text.trim() && params.streamState.toolCalls.length === 0) {
15057
15082
  return;
@@ -15059,6 +15084,7 @@ async function finalizeCowtailStreamedReply(params) {
15059
15084
  await params.client.sendOpenClawMessageUpdate({
15060
15085
  type: "openclaw_message_update",
15061
15086
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
15087
+ streamId: params.streamState.streamId,
15062
15088
  messageId: params.streamState.messageId,
15063
15089
  text: params.streamState.text,
15064
15090
  links: params.streamState.links,
@@ -15140,12 +15166,13 @@ function deliverCowtailToolProgress(params) {
15140
15166
  });
15141
15167
  }
15142
15168
  function emitCowtailStreamSnapshot(params) {
15143
- if (params.streamState.failed || !params.text.trim() && params.streamState.links.length === 0 && params.streamState.toolCalls.length === 0) {
15169
+ if (params.streamState.failed && params.allowFailed !== true || !params.text.trim() && params.streamState.links.length === 0 && params.streamState.toolCalls.length === 0) {
15144
15170
  return;
15145
15171
  }
15146
15172
  params.streamState.liveText = params.text;
15147
15173
  params.streamState.lastSnapshotText = params.text;
15148
15174
  params.streamState.lastSnapshotSentAt = params.updatedAt;
15175
+ params.streamState.snapshotSequence += 1;
15149
15176
  params.client.sendOpenClawStreamSnapshot({
15150
15177
  type: "openclaw_message_stream_snapshot",
15151
15178
  streamId: params.streamState.streamId,
@@ -15155,11 +15182,30 @@ function emitCowtailStreamSnapshot(params) {
15155
15182
  links: params.streamState.links.map((link) => ({ ...link })),
15156
15183
  toolCalls: structuredClone(params.streamState.toolCalls),
15157
15184
  isFinal: params.isFinal,
15185
+ snapshotSequence: params.streamState.snapshotSequence,
15158
15186
  updatedAt: params.updatedAt
15159
15187
  }).catch((error48) => {
15160
15188
  params.logger?.warn?.(`Cowtail stream snapshot send failed: ${errorMessage(error48)}`);
15161
15189
  });
15162
15190
  }
15191
+ function failCowtailStreamBeforeDurableCreate(params) {
15192
+ if (!params.streamState.messageId && params.streamState.snapshotSequence > 0) {
15193
+ const text = params.streamState.text.trim() ? params.streamState.text : currentLiveCowtailStreamText(params.streamState);
15194
+ emitCowtailStreamSnapshot({
15195
+ client: params.client,
15196
+ logger: params.logger,
15197
+ route: params.route,
15198
+ thread: params.thread,
15199
+ streamState: params.streamState,
15200
+ text,
15201
+ isFinal: true,
15202
+ updatedAt: Date.now(),
15203
+ allowFailed: true
15204
+ });
15205
+ }
15206
+ params.streamState.failed = true;
15207
+ params.streamState.completed = true;
15208
+ }
15163
15209
  function currentLiveCowtailStreamText(streamState) {
15164
15210
  return streamState.liveText ?? streamState.text;
15165
15211
  }
@@ -15436,23 +15482,29 @@ function resolveCowtailTarget(to) {
15436
15482
  if (!normalizedTarget) {
15437
15483
  throw new Error("Cowtail target must not be blank");
15438
15484
  }
15439
- return buildCowtailTarget(normalizedTarget);
15485
+ return normalizedTarget;
15440
15486
  }
15441
15487
  async function sendCowtailText(params) {
15442
15488
  const { client, to } = params;
15443
15489
  const text = ensureNonBlankText(params.text);
15444
15490
  const target = resolveCowtailTarget(to);
15491
+ const sessionKey = buildCowtailTarget(target);
15445
15492
  const result = await client.sendOpenClawMessage({
15446
15493
  type: "openclaw_message",
15447
- sessionKey: target,
15494
+ sessionKey,
15495
+ threadHint: target,
15448
15496
  text,
15449
15497
  links: [],
15450
15498
  actions: []
15451
15499
  });
15500
+ const messageId = result.payload?.messageId;
15501
+ if (typeof messageId !== "string" || messageId.trim() === "") {
15502
+ throw new Error("Cowtail did not acknowledge a durable message id");
15503
+ }
15452
15504
  return {
15453
15505
  channel: "cowtail",
15454
- messageId: String(result.sequence ?? result.requestId),
15455
- to: target
15506
+ messageId,
15507
+ to: buildCowtailTarget(target)
15456
15508
  };
15457
15509
  }
15458
15510
 
@@ -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 ?? []
@@ -14668,6 +14694,7 @@ async function dispatchCowtailTextTurn(params) {
14668
14694
  idempotencyKey: `cowtail:reply:${message.id}`,
14669
14695
  streamId: `cowtail:stream:${message.id}`,
14670
14696
  updateIndex: 0,
14697
+ snapshotSequence: 0,
14671
14698
  toolFallbackIndex: 0,
14672
14699
  toolFallbackIds: {},
14673
14700
  text: "",
@@ -14741,6 +14768,7 @@ async function dispatchCowtailActionTurn(params) {
14741
14768
  idempotencyKey: `cowtail:action:${action.id}`,
14742
14769
  streamId: `cowtail:action-stream:${action.id}`,
14743
14770
  updateIndex: 0,
14771
+ snapshotSequence: 0,
14744
14772
  toolFallbackIndex: 0,
14745
14773
  toolFallbackIds: {},
14746
14774
  text: "",
@@ -14921,28 +14949,17 @@ async function deliverCowtailReply(params) {
14921
14949
  updatedAt: Date.now()
14922
14950
  });
14923
14951
  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,
14952
+ await createDurableCowtailReply(params, {
14929
14953
  text: messageText,
14930
- authorLabel: "OpenClaw",
14931
14954
  links,
14932
- toolCalls: params.streamState.toolCalls,
14933
- actions: [],
14934
14955
  deliveryState: "pending"
14935
14956
  });
14936
- const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14937
- if (!messageId2) {
14938
- return;
14939
- }
14940
- params.streamState.messageId = messageId2;
14941
14957
  return;
14942
14958
  }
14943
14959
  await params.client.sendOpenClawMessageUpdate({
14944
14960
  type: "openclaw_message_update",
14945
14961
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
14962
+ streamId: params.streamState.streamId,
14946
14963
  messageId: params.streamState.messageId,
14947
14964
  text: messageText,
14948
14965
  links,
@@ -14967,28 +14984,17 @@ async function deliverCowtailReply(params) {
14967
14984
  updatedAt: Date.now()
14968
14985
  });
14969
14986
  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,
14987
+ await createDurableCowtailReply(params, {
14975
14988
  text: params.streamState.text,
14976
- authorLabel: "OpenClaw",
14977
14989
  links,
14978
- toolCalls: params.streamState.toolCalls,
14979
- actions: [],
14980
14990
  deliveryState: "pending"
14981
14991
  });
14982
- const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14983
- if (!messageId2) {
14984
- return;
14985
- }
14986
- params.streamState.messageId = messageId2;
14987
14992
  return;
14988
14993
  }
14989
14994
  await params.client.sendOpenClawMessageUpdate({
14990
14995
  type: "openclaw_message_update",
14991
14996
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
14997
+ streamId: params.streamState.streamId,
14992
14998
  messageId: params.streamState.messageId,
14993
14999
  text: params.streamState.text,
14994
15000
  links,
@@ -15002,6 +15008,7 @@ async function deliverCowtailReply(params) {
15002
15008
  await params.client.sendOpenClawMessageUpdate({
15003
15009
  type: "openclaw_message_update",
15004
15010
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
15011
+ streamId: params.streamState.streamId,
15005
15012
  messageId: params.streamState.messageId,
15006
15013
  text,
15007
15014
  links,
@@ -15023,23 +15030,14 @@ async function deliverCowtailReply(params) {
15023
15030
  return;
15024
15031
  }
15025
15032
  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,
15033
+ const messageId = await createDurableCowtailReply(params, {
15031
15034
  text,
15032
- authorLabel: "OpenClaw",
15033
15035
  links,
15034
- toolCalls: params.streamState.toolCalls,
15035
- actions: [],
15036
15036
  deliveryState: "sent"
15037
15037
  });
15038
- const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
15039
15038
  if (!messageId) {
15040
15039
  return;
15041
15040
  }
15042
- params.streamState.messageId = messageId;
15043
15041
  emitCowtailStreamSnapshot({
15044
15042
  client: params.client,
15045
15043
  logger: params.logger,
@@ -15052,6 +15050,33 @@ async function deliverCowtailReply(params) {
15052
15050
  });
15053
15051
  params.streamState.completed = true;
15054
15052
  }
15053
+ async function createDurableCowtailReply(params, message) {
15054
+ try {
15055
+ const result = await params.client.sendOpenClawMessage({
15056
+ type: "openclaw_message",
15057
+ idempotencyKey: params.streamState.idempotencyKey,
15058
+ streamId: params.streamState.streamId,
15059
+ sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
15060
+ title: params.thread.title,
15061
+ text: message.text,
15062
+ authorLabel: "OpenClaw",
15063
+ links: message.links,
15064
+ toolCalls: params.streamState.toolCalls,
15065
+ actions: [],
15066
+ deliveryState: message.deliveryState
15067
+ });
15068
+ const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
15069
+ if (!messageId) {
15070
+ failCowtailStreamBeforeDurableCreate(params);
15071
+ return;
15072
+ }
15073
+ params.streamState.messageId = messageId;
15074
+ return messageId;
15075
+ } catch (error48) {
15076
+ failCowtailStreamBeforeDurableCreate(params);
15077
+ throw error48;
15078
+ }
15079
+ }
15055
15080
  async function finalizeCowtailStreamedReply(params) {
15056
15081
  if (params.streamState.failed || params.streamState.completed || !params.streamState.messageId || !params.streamState.text.trim() && params.streamState.toolCalls.length === 0) {
15057
15082
  return;
@@ -15059,6 +15084,7 @@ async function finalizeCowtailStreamedReply(params) {
15059
15084
  await params.client.sendOpenClawMessageUpdate({
15060
15085
  type: "openclaw_message_update",
15061
15086
  idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
15087
+ streamId: params.streamState.streamId,
15062
15088
  messageId: params.streamState.messageId,
15063
15089
  text: params.streamState.text,
15064
15090
  links: params.streamState.links,
@@ -15140,12 +15166,13 @@ function deliverCowtailToolProgress(params) {
15140
15166
  });
15141
15167
  }
15142
15168
  function emitCowtailStreamSnapshot(params) {
15143
- if (params.streamState.failed || !params.text.trim() && params.streamState.links.length === 0 && params.streamState.toolCalls.length === 0) {
15169
+ if (params.streamState.failed && params.allowFailed !== true || !params.text.trim() && params.streamState.links.length === 0 && params.streamState.toolCalls.length === 0) {
15144
15170
  return;
15145
15171
  }
15146
15172
  params.streamState.liveText = params.text;
15147
15173
  params.streamState.lastSnapshotText = params.text;
15148
15174
  params.streamState.lastSnapshotSentAt = params.updatedAt;
15175
+ params.streamState.snapshotSequence += 1;
15149
15176
  params.client.sendOpenClawStreamSnapshot({
15150
15177
  type: "openclaw_message_stream_snapshot",
15151
15178
  streamId: params.streamState.streamId,
@@ -15155,11 +15182,30 @@ function emitCowtailStreamSnapshot(params) {
15155
15182
  links: params.streamState.links.map((link) => ({ ...link })),
15156
15183
  toolCalls: structuredClone(params.streamState.toolCalls),
15157
15184
  isFinal: params.isFinal,
15185
+ snapshotSequence: params.streamState.snapshotSequence,
15158
15186
  updatedAt: params.updatedAt
15159
15187
  }).catch((error48) => {
15160
15188
  params.logger?.warn?.(`Cowtail stream snapshot send failed: ${errorMessage(error48)}`);
15161
15189
  });
15162
15190
  }
15191
+ function failCowtailStreamBeforeDurableCreate(params) {
15192
+ if (!params.streamState.messageId && params.streamState.snapshotSequence > 0) {
15193
+ const text = params.streamState.text.trim() ? params.streamState.text : currentLiveCowtailStreamText(params.streamState);
15194
+ emitCowtailStreamSnapshot({
15195
+ client: params.client,
15196
+ logger: params.logger,
15197
+ route: params.route,
15198
+ thread: params.thread,
15199
+ streamState: params.streamState,
15200
+ text,
15201
+ isFinal: true,
15202
+ updatedAt: Date.now(),
15203
+ allowFailed: true
15204
+ });
15205
+ }
15206
+ params.streamState.failed = true;
15207
+ params.streamState.completed = true;
15208
+ }
15163
15209
  function currentLiveCowtailStreamText(streamState) {
15164
15210
  return streamState.liveText ?? streamState.text;
15165
15211
  }
@@ -15436,23 +15482,29 @@ function resolveCowtailTarget(to) {
15436
15482
  if (!normalizedTarget) {
15437
15483
  throw new Error("Cowtail target must not be blank");
15438
15484
  }
15439
- return buildCowtailTarget(normalizedTarget);
15485
+ return normalizedTarget;
15440
15486
  }
15441
15487
  async function sendCowtailText(params) {
15442
15488
  const { client, to } = params;
15443
15489
  const text = ensureNonBlankText(params.text);
15444
15490
  const target = resolveCowtailTarget(to);
15491
+ const sessionKey = buildCowtailTarget(target);
15445
15492
  const result = await client.sendOpenClawMessage({
15446
15493
  type: "openclaw_message",
15447
- sessionKey: target,
15494
+ sessionKey,
15495
+ threadHint: target,
15448
15496
  text,
15449
15497
  links: [],
15450
15498
  actions: []
15451
15499
  });
15500
+ const messageId = result.payload?.messageId;
15501
+ if (typeof messageId !== "string" || messageId.trim() === "") {
15502
+ throw new Error("Cowtail did not acknowledge a durable message id");
15503
+ }
15452
15504
  return {
15453
15505
  channel: "cowtail",
15454
- messageId: String(result.sequence ?? result.requestId),
15455
- to: target
15506
+ messageId,
15507
+ to: buildCowtailTarget(target)
15456
15508
  };
15457
15509
  }
15458
15510
 
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.0",
4
4
  "description": "Cowtail channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "repository": {