@maudecode/openclaw-cowtail 0.12.1 → 0.13.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/dist/index.js CHANGED
@@ -13844,6 +13844,7 @@ var openclawActionSubmittedEventSchema = openclawEventEnvelopeSchema.extend({
13844
13844
  payload: jsonObjectSchema
13845
13845
  });
13846
13846
  var openclawRequestIdSchema = nonEmptyStringSchema;
13847
+ var openclawIdempotencyKeySchema = nonEmptyStringSchema;
13847
13848
  var openclawActionDraftSchema = exports_external.object({
13848
13849
  label: nonEmptyStringSchema,
13849
13850
  kind: nonEmptyStringSchema,
@@ -13852,6 +13853,7 @@ var openclawActionDraftSchema = exports_external.object({
13852
13853
  var openclawPluginMessageCommandSchema = requireOpenClawRenderableContent(exports_external.object({
13853
13854
  type: exports_external.literal("openclaw_message"),
13854
13855
  requestId: openclawRequestIdSchema,
13856
+ idempotencyKey: openclawIdempotencyKeySchema,
13855
13857
  sessionKey: nonEmptyStringSchema,
13856
13858
  title: nonEmptyStringSchema.optional(),
13857
13859
  text: openclawMessageTextSchema,
@@ -13864,6 +13866,7 @@ var openclawPluginMessageCommandSchema = requireOpenClawRenderableContent(export
13864
13866
  var openclawPluginMessageUpdateCommandSchema = requireOpenClawRenderableContent(exports_external.object({
13865
13867
  type: exports_external.literal("openclaw_message_update"),
13866
13868
  requestId: openclawRequestIdSchema,
13869
+ idempotencyKey: openclawIdempotencyKeySchema,
13867
13870
  messageId: nonEmptyStringSchema,
13868
13871
  text: openclawMessageTextSchema,
13869
13872
  links: exports_external.array(openclawLinkSchema).optional(),
@@ -13871,49 +13874,71 @@ var openclawPluginMessageUpdateCommandSchema = requireOpenClawRenderableContent(
13871
13874
  actions: exports_external.array(openclawActionDraftSchema).optional(),
13872
13875
  deliveryState: openclawDeliveryStateSchema.optional()
13873
13876
  }));
13877
+ var openclawMessageStreamSnapshotCommandBaseSchema = exports_external.object({
13878
+ type: exports_external.literal("openclaw_message_stream_snapshot"),
13879
+ requestId: openclawRequestIdSchema,
13880
+ streamId: nonEmptyStringSchema,
13881
+ sessionKey: nonEmptyStringSchema,
13882
+ threadId: nonEmptyStringSchema,
13883
+ text: openclawMessageTextSchema,
13884
+ links: exports_external.array(openclawLinkSchema).default([]),
13885
+ toolCalls: exports_external.array(openclawToolCallRecordSchema).default([]),
13886
+ isFinal: exports_external.boolean(),
13887
+ updatedAt: timestampSchema
13888
+ });
13889
+ var openclawMessageStreamSnapshotCommandSchema = requireOpenClawRenderableContent(openclawMessageStreamSnapshotCommandBaseSchema);
13890
+ var openclawMessageStreamSnapshotServerMessageSchema = requireOpenClawRenderableContent(openclawMessageStreamSnapshotCommandBaseSchema.omit({ requestId: true }));
13874
13891
  var openclawIosNewThreadCommandSchema = exports_external.object({
13875
13892
  type: exports_external.literal("ios_new_thread"),
13876
13893
  requestId: openclawRequestIdSchema,
13894
+ idempotencyKey: openclawIdempotencyKeySchema,
13877
13895
  title: nonEmptyStringSchema.optional(),
13878
13896
  text: nonEmptyStringSchema
13879
13897
  });
13880
13898
  var openclawIosReplyCommandSchema = exports_external.object({
13881
13899
  type: exports_external.literal("ios_reply"),
13882
13900
  requestId: openclawRequestIdSchema,
13901
+ idempotencyKey: openclawIdempotencyKeySchema,
13883
13902
  threadId: nonEmptyStringSchema,
13884
13903
  text: nonEmptyStringSchema
13885
13904
  });
13886
13905
  var openclawIosActionCommandSchema = exports_external.object({
13887
13906
  type: exports_external.literal("ios_action"),
13888
13907
  requestId: openclawRequestIdSchema,
13908
+ idempotencyKey: openclawIdempotencyKeySchema,
13889
13909
  actionId: nonEmptyStringSchema,
13890
13910
  payload: jsonObjectSchema
13891
13911
  });
13892
13912
  var openclawIosMarkThreadReadCommandSchema = exports_external.object({
13893
13913
  type: exports_external.literal("ios_mark_thread_read"),
13894
13914
  requestId: openclawRequestIdSchema,
13915
+ idempotencyKey: openclawIdempotencyKeySchema,
13895
13916
  threadId: nonEmptyStringSchema
13896
13917
  });
13897
13918
  var openclawIosRenameThreadCommandSchema = exports_external.object({
13898
13919
  type: exports_external.literal("ios_rename_thread"),
13899
13920
  requestId: openclawRequestIdSchema,
13921
+ idempotencyKey: openclawIdempotencyKeySchema,
13900
13922
  threadId: nonEmptyStringSchema,
13901
13923
  title: nonEmptyStringSchema
13902
13924
  });
13903
13925
  var openclawIosDeleteThreadCommandSchema = exports_external.object({
13904
13926
  type: exports_external.literal("ios_delete_thread"),
13905
13927
  requestId: openclawRequestIdSchema,
13928
+ idempotencyKey: openclawIdempotencyKeySchema,
13906
13929
  threadId: nonEmptyStringSchema
13907
13930
  });
13908
13931
  var openclawSessionBoundCommandSchema = exports_external.object({
13909
13932
  type: exports_external.literal("openclaw_session_bound"),
13910
13933
  requestId: openclawRequestIdSchema,
13934
+ idempotencyKey: openclawIdempotencyKeySchema,
13911
13935
  threadId: nonEmptyStringSchema,
13912
13936
  sessionKey: nonEmptyStringSchema
13913
13937
  });
13914
13938
  var openclawActionResultCommandSchema = exports_external.object({
13915
13939
  type: exports_external.literal("openclaw_action_result"),
13916
13940
  requestId: openclawRequestIdSchema,
13941
+ idempotencyKey: openclawIdempotencyKeySchema,
13917
13942
  actionId: nonEmptyStringSchema,
13918
13943
  state: exports_external.enum(["submitted", "failed", "expired"]),
13919
13944
  resultMetadata: jsonObjectSchema.optional()
@@ -13921,6 +13946,7 @@ var openclawActionResultCommandSchema = exports_external.object({
13921
13946
  var openclawRealtimeClientMessageSchema = exports_external.union([
13922
13947
  openclawPluginMessageCommandSchema,
13923
13948
  openclawPluginMessageUpdateCommandSchema,
13949
+ openclawMessageStreamSnapshotCommandSchema,
13924
13950
  openclawIosNewThreadCommandSchema,
13925
13951
  openclawIosReplyCommandSchema,
13926
13952
  openclawIosActionCommandSchema,
@@ -13943,6 +13969,7 @@ var openclawRealtimeErrorSchema = exports_external.object({
13943
13969
  });
13944
13970
  var openclawRealtimeServerMessageSchema = exports_external.union([
13945
13971
  openclawEventEnvelopeSchema,
13972
+ openclawMessageStreamSnapshotServerMessageSchema,
13946
13973
  openclawRealtimeAckSchema,
13947
13974
  openclawRealtimeErrorSchema
13948
13975
  ]);
@@ -14164,35 +14191,49 @@ class CowtailRealtimeClient {
14164
14191
  this.#closeSocket();
14165
14192
  }
14166
14193
  sendOpenClawMessage(command) {
14194
+ const requestId = this.#requestIdFactory();
14167
14195
  return this.#sendCommand({
14168
14196
  ...command,
14169
- requestId: this.#requestIdFactory(),
14197
+ requestId,
14198
+ idempotencyKey: command.idempotencyKey ?? `cowtail:request:${requestId}`,
14170
14199
  links: command.links ?? [],
14171
14200
  toolCalls: command.toolCalls ?? [],
14172
14201
  actions: command.actions ?? []
14173
14202
  });
14174
14203
  }
14175
14204
  sendOpenClawMessageUpdate(command) {
14205
+ const requestId = this.#requestIdFactory();
14176
14206
  return this.#sendCommand({
14177
14207
  ...command,
14178
- requestId: this.#requestIdFactory(),
14208
+ requestId,
14209
+ idempotencyKey: command.idempotencyKey ?? `cowtail:request:${requestId}`,
14179
14210
  ...command.links ? { links: command.links } : {},
14180
14211
  ...command.toolCalls ? { toolCalls: command.toolCalls } : {},
14181
14212
  ...command.actions ? { actions: command.actions } : {}
14182
14213
  });
14183
14214
  }
14184
14215
  sendSessionBound(command) {
14216
+ const requestId = this.#requestIdFactory();
14185
14217
  return this.#sendCommand({
14186
14218
  ...command,
14187
- requestId: this.#requestIdFactory()
14219
+ requestId,
14220
+ idempotencyKey: command.idempotencyKey ?? `cowtail:request:${requestId}`
14188
14221
  }).then((result) => result.sequence);
14189
14222
  }
14190
14223
  sendActionResult(command) {
14224
+ const requestId = this.#requestIdFactory();
14191
14225
  return this.#sendCommand({
14192
14226
  ...command,
14193
- requestId: this.#requestIdFactory()
14227
+ requestId,
14228
+ idempotencyKey: command.idempotencyKey ?? `cowtail:request:${requestId}`
14194
14229
  }).then((result) => result.sequence);
14195
14230
  }
14231
+ sendOpenClawStreamSnapshot(command) {
14232
+ return this.#sendTransientCommand({
14233
+ ...command,
14234
+ requestId: this.#requestIdFactory()
14235
+ });
14236
+ }
14196
14237
  #connect() {
14197
14238
  if (!this.#started || this.#socket) {
14198
14239
  return;
@@ -14292,6 +14333,9 @@ class CowtailRealtimeClient {
14292
14333
  this.#rejectPendingRequest(message);
14293
14334
  return;
14294
14335
  }
14336
+ if (message.type === "openclaw_message_stream_snapshot") {
14337
+ return;
14338
+ }
14295
14339
  await this.#onEvent(message);
14296
14340
  await this.#stateStore.writeLastSeenSequence(message.sequence);
14297
14341
  }
@@ -14368,6 +14412,19 @@ class CowtailRealtimeClient {
14368
14412
  }
14369
14413
  });
14370
14414
  }
14415
+ async#sendTransientCommand(command) {
14416
+ const socket = this.#socket;
14417
+ const handshake = this.#handshake;
14418
+ if (!socket || !handshake) {
14419
+ throw new Error("Cowtail websocket is disconnected");
14420
+ }
14421
+ await handshake.promise;
14422
+ const currentSocket = this.#socket;
14423
+ if (!currentSocket || socket !== currentSocket || handshake !== this.#handshake || !this.#isSocketOpen(currentSocket)) {
14424
+ throw new Error("Cowtail websocket is disconnected");
14425
+ }
14426
+ currentSocket.send(JSON.stringify(command));
14427
+ }
14371
14428
  #resolvePendingRequest(requestId, sequence, payload) {
14372
14429
  const pending = this.#pendingRequests.get(requestId);
14373
14430
  if (!pending) {
@@ -14472,6 +14529,8 @@ var CHANNEL_LABEL = "Cowtail";
14472
14529
  var SENDER_LABEL = "Cowtail iOS";
14473
14530
  var SENDER_ID = "cowtail-ios";
14474
14531
  var SENDER_ADDRESS = "cowtail:ios";
14532
+ var STREAM_MIN_INTERVAL_MS = 45;
14533
+ var STREAM_MIN_CHARS_DELTA = 3;
14475
14534
  async function handleCowtailEvent(params) {
14476
14535
  const { event, account, client, runtime, logger } = params;
14477
14536
  switch (event.type) {
@@ -14498,6 +14557,7 @@ async function handleCowtailEvent(params) {
14498
14557
  if (!thread.sessionKey) {
14499
14558
  await client.sendSessionBound({
14500
14559
  type: "openclaw_session_bound",
14560
+ idempotencyKey: `cowtail:session-bound:${thread.id}`,
14501
14561
  threadId: thread.id,
14502
14562
  sessionKey: route.sessionKey
14503
14563
  });
@@ -14566,6 +14626,7 @@ async function handleCowtailEvent(params) {
14566
14626
  });
14567
14627
  await client.sendActionResult({
14568
14628
  type: "openclaw_action_result",
14629
+ idempotencyKey: `cowtail:action-result:${action.id}:${dispatchSucceeded ? "submitted" : "failed"}`,
14569
14630
  actionId: action.id,
14570
14631
  state: dispatchSucceeded ? "submitted" : "failed"
14571
14632
  });
@@ -14573,6 +14634,7 @@ async function handleCowtailEvent(params) {
14573
14634
  logger?.error?.(`Cowtail action dispatch failed: ${errorMessage(error48)}`);
14574
14635
  await client.sendActionResult({
14575
14636
  type: "openclaw_action_result",
14637
+ idempotencyKey: `cowtail:action-result:${action.id}:failed`,
14576
14638
  actionId: action.id,
14577
14639
  state: "failed"
14578
14640
  });
@@ -14602,18 +14664,32 @@ async function dispatchCowtailTextTurn(params) {
14602
14664
  body,
14603
14665
  timestamp: message.createdAt
14604
14666
  });
14605
- const streamState = { text: "", links: [], toolCalls: [] };
14667
+ const streamState = {
14668
+ idempotencyKey: `cowtail:reply:${message.id}`,
14669
+ streamId: `cowtail:stream:${message.id}`,
14670
+ updateIndex: 0,
14671
+ toolFallbackIndex: 0,
14672
+ toolFallbackIds: {},
14673
+ text: "",
14674
+ links: [],
14675
+ toolCalls: []
14676
+ };
14606
14677
  await recordCowtailInboundSessionAndDispatchReply({
14607
14678
  cfg: runtime.config.loadConfig(),
14608
14679
  channel: CHANNEL_ID,
14609
14680
  accountId: account.accountId,
14610
14681
  agentId: route.agentId,
14682
+ client,
14683
+ logger,
14611
14684
  route,
14685
+ thread,
14612
14686
  storePath,
14613
14687
  ctxPayload,
14614
14688
  runtime,
14689
+ streamState,
14615
14690
  deliver: async (payload, info, rawPayload) => deliverCowtailReply({
14616
14691
  client,
14692
+ logger,
14617
14693
  route,
14618
14694
  thread,
14619
14695
  payload,
@@ -14630,6 +14706,9 @@ async function dispatchCowtailTextTurn(params) {
14630
14706
  });
14631
14707
  await finalizeCowtailStreamedReply({
14632
14708
  client,
14709
+ logger,
14710
+ route,
14711
+ thread,
14633
14712
  streamState
14634
14713
  });
14635
14714
  }
@@ -14658,18 +14737,32 @@ async function dispatchCowtailActionTurn(params) {
14658
14737
  timestamp
14659
14738
  });
14660
14739
  let dispatchFailed = false;
14661
- const streamState = { text: "", links: [], toolCalls: [] };
14740
+ const streamState = {
14741
+ idempotencyKey: `cowtail:action:${action.id}`,
14742
+ streamId: `cowtail:action-stream:${action.id}`,
14743
+ updateIndex: 0,
14744
+ toolFallbackIndex: 0,
14745
+ toolFallbackIds: {},
14746
+ text: "",
14747
+ links: [],
14748
+ toolCalls: []
14749
+ };
14662
14750
  await recordCowtailInboundSessionAndDispatchReply({
14663
14751
  cfg: runtime.config.loadConfig(),
14664
14752
  channel: CHANNEL_ID,
14665
14753
  accountId: account.accountId,
14666
14754
  agentId: route.agentId,
14755
+ client,
14756
+ logger,
14667
14757
  route,
14758
+ thread,
14668
14759
  storePath,
14669
14760
  ctxPayload,
14670
14761
  runtime,
14762
+ streamState,
14671
14763
  deliver: async (replyPayload, info, rawPayload) => deliverCowtailReply({
14672
14764
  client,
14765
+ logger,
14673
14766
  route,
14674
14767
  thread,
14675
14768
  payload: replyPayload,
@@ -14687,6 +14780,9 @@ async function dispatchCowtailActionTurn(params) {
14687
14780
  });
14688
14781
  await finalizeCowtailStreamedReply({
14689
14782
  client,
14783
+ logger,
14784
+ route,
14785
+ thread,
14690
14786
  streamState
14691
14787
  });
14692
14788
  return !dispatchFailed;
@@ -14720,7 +14816,79 @@ async function recordCowtailInboundSessionAndDispatchReply(params) {
14720
14816
  },
14721
14817
  replyOptions: {
14722
14818
  disableBlockStreaming: false,
14723
- onModelSelected
14819
+ onModelSelected,
14820
+ onPartialReply: (payload) => {
14821
+ deliverCowtailPartialReply({
14822
+ client: params.client,
14823
+ logger: params.logger,
14824
+ route: params.route,
14825
+ thread: params.thread,
14826
+ payload: normalizeCowtailReplyPayload(payload),
14827
+ streamState: params.streamState
14828
+ });
14829
+ },
14830
+ onToolStart: (payload) => {
14831
+ deliverCowtailToolProgress({
14832
+ client: params.client,
14833
+ logger: params.logger,
14834
+ route: params.route,
14835
+ thread: params.thread,
14836
+ streamState: params.streamState,
14837
+ payload,
14838
+ requireStableId: true
14839
+ });
14840
+ },
14841
+ onItemEvent: (payload) => {
14842
+ deliverCowtailToolProgress({
14843
+ client: params.client,
14844
+ logger: params.logger,
14845
+ route: params.route,
14846
+ thread: params.thread,
14847
+ streamState: params.streamState,
14848
+ payload
14849
+ });
14850
+ },
14851
+ onCommandOutput: (payload) => {
14852
+ deliverCowtailToolProgress({
14853
+ client: params.client,
14854
+ logger: params.logger,
14855
+ route: params.route,
14856
+ thread: params.thread,
14857
+ streamState: params.streamState,
14858
+ payload
14859
+ });
14860
+ },
14861
+ onPatchSummary: (payload) => {
14862
+ deliverCowtailToolProgress({
14863
+ client: params.client,
14864
+ logger: params.logger,
14865
+ route: params.route,
14866
+ thread: params.thread,
14867
+ streamState: params.streamState,
14868
+ payload
14869
+ });
14870
+ },
14871
+ onApprovalEvent: (payload) => {
14872
+ deliverCowtailToolProgress({
14873
+ client: params.client,
14874
+ logger: params.logger,
14875
+ route: params.route,
14876
+ thread: params.thread,
14877
+ streamState: params.streamState,
14878
+ payload
14879
+ });
14880
+ },
14881
+ onToolResult: (payload) => {
14882
+ deliverCowtailToolProgress({
14883
+ client: params.client,
14884
+ logger: params.logger,
14885
+ route: params.route,
14886
+ thread: params.thread,
14887
+ streamState: params.streamState,
14888
+ payload,
14889
+ status: "complete"
14890
+ });
14891
+ }
14724
14892
  }
14725
14893
  });
14726
14894
  }
@@ -14740,11 +14908,22 @@ async function deliverCowtailReply(params) {
14740
14908
  rawPayload: params.rawPayload,
14741
14909
  streamState: params.streamState
14742
14910
  });
14743
- params.streamState.toolCalls = [...params.streamState.toolCalls, toolCall];
14911
+ upsertCowtailToolCall(params.streamState, toolCall);
14744
14912
  const messageText = params.streamState.text;
14913
+ emitCowtailStreamSnapshot({
14914
+ client: params.client,
14915
+ logger: params.logger,
14916
+ route: params.route,
14917
+ thread: params.thread,
14918
+ streamState: params.streamState,
14919
+ text: currentLiveCowtailStreamText(params.streamState),
14920
+ isFinal: false,
14921
+ updatedAt: Date.now()
14922
+ });
14745
14923
  if (!params.streamState.messageId) {
14746
- const result = await params.client.sendOpenClawMessage({
14924
+ const result2 = await params.client.sendOpenClawMessage({
14747
14925
  type: "openclaw_message",
14926
+ idempotencyKey: params.streamState.idempotencyKey,
14748
14927
  sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
14749
14928
  title: params.thread.title,
14750
14929
  text: messageText,
@@ -14754,15 +14933,16 @@ async function deliverCowtailReply(params) {
14754
14933
  actions: [],
14755
14934
  deliveryState: "pending"
14756
14935
  });
14757
- const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
14758
- if (!messageId) {
14936
+ const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14937
+ if (!messageId2) {
14759
14938
  return;
14760
14939
  }
14761
- params.streamState.messageId = messageId;
14940
+ params.streamState.messageId = messageId2;
14762
14941
  return;
14763
14942
  }
14764
14943
  await params.client.sendOpenClawMessageUpdate({
14765
14944
  type: "openclaw_message_update",
14945
+ idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
14766
14946
  messageId: params.streamState.messageId,
14767
14947
  text: messageText,
14768
14948
  links,
@@ -14776,9 +14956,20 @@ async function deliverCowtailReply(params) {
14776
14956
  }
14777
14957
  if (kind === "block") {
14778
14958
  params.streamState.text = appendReplyBlock(params.streamState.text, text);
14959
+ emitCowtailStreamSnapshot({
14960
+ client: params.client,
14961
+ logger: params.logger,
14962
+ route: params.route,
14963
+ thread: params.thread,
14964
+ streamState: params.streamState,
14965
+ text: params.streamState.text,
14966
+ isFinal: false,
14967
+ updatedAt: Date.now()
14968
+ });
14779
14969
  if (!params.streamState.messageId) {
14780
- const result = await params.client.sendOpenClawMessage({
14970
+ const result2 = await params.client.sendOpenClawMessage({
14781
14971
  type: "openclaw_message",
14972
+ idempotencyKey: params.streamState.idempotencyKey,
14782
14973
  sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
14783
14974
  title: params.thread.title,
14784
14975
  text: params.streamState.text,
@@ -14788,15 +14979,16 @@ async function deliverCowtailReply(params) {
14788
14979
  actions: [],
14789
14980
  deliveryState: "pending"
14790
14981
  });
14791
- const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
14792
- if (!messageId) {
14982
+ const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14983
+ if (!messageId2) {
14793
14984
  return;
14794
14985
  }
14795
- params.streamState.messageId = messageId;
14986
+ params.streamState.messageId = messageId2;
14796
14987
  return;
14797
14988
  }
14798
14989
  await params.client.sendOpenClawMessageUpdate({
14799
14990
  type: "openclaw_message_update",
14991
+ idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
14800
14992
  messageId: params.streamState.messageId,
14801
14993
  text: params.streamState.text,
14802
14994
  links,
@@ -14809,6 +15001,7 @@ async function deliverCowtailReply(params) {
14809
15001
  params.streamState.text = text;
14810
15002
  await params.client.sendOpenClawMessageUpdate({
14811
15003
  type: "openclaw_message_update",
15004
+ idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
14812
15005
  messageId: params.streamState.messageId,
14813
15006
  text,
14814
15007
  links,
@@ -14816,11 +15009,23 @@ async function deliverCowtailReply(params) {
14816
15009
  actions: [],
14817
15010
  deliveryState: "sent"
14818
15011
  });
15012
+ emitCowtailStreamSnapshot({
15013
+ client: params.client,
15014
+ logger: params.logger,
15015
+ route: params.route,
15016
+ thread: params.thread,
15017
+ streamState: params.streamState,
15018
+ text,
15019
+ isFinal: true,
15020
+ updatedAt: Date.now()
15021
+ });
14819
15022
  params.streamState.completed = true;
14820
15023
  return;
14821
15024
  }
14822
- await params.client.sendOpenClawMessage({
15025
+ params.streamState.text = text;
15026
+ const result = await params.client.sendOpenClawMessage({
14823
15027
  type: "openclaw_message",
15028
+ idempotencyKey: params.streamState.idempotencyKey,
14824
15029
  sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
14825
15030
  title: params.thread.title,
14826
15031
  text,
@@ -14830,6 +15035,21 @@ async function deliverCowtailReply(params) {
14830
15035
  actions: [],
14831
15036
  deliveryState: "sent"
14832
15037
  });
15038
+ const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
15039
+ if (!messageId) {
15040
+ return;
15041
+ }
15042
+ params.streamState.messageId = messageId;
15043
+ emitCowtailStreamSnapshot({
15044
+ client: params.client,
15045
+ logger: params.logger,
15046
+ route: params.route,
15047
+ thread: params.thread,
15048
+ streamState: params.streamState,
15049
+ text,
15050
+ isFinal: true,
15051
+ updatedAt: Date.now()
15052
+ });
14833
15053
  params.streamState.completed = true;
14834
15054
  }
14835
15055
  async function finalizeCowtailStreamedReply(params) {
@@ -14838,6 +15058,7 @@ async function finalizeCowtailStreamedReply(params) {
14838
15058
  }
14839
15059
  await params.client.sendOpenClawMessageUpdate({
14840
15060
  type: "openclaw_message_update",
15061
+ idempotencyKey: nextStreamUpdateIdempotencyKey(params.streamState),
14841
15062
  messageId: params.streamState.messageId,
14842
15063
  text: params.streamState.text,
14843
15064
  links: params.streamState.links,
@@ -14845,10 +15066,125 @@ async function finalizeCowtailStreamedReply(params) {
14845
15066
  actions: [],
14846
15067
  deliveryState: "sent"
14847
15068
  });
15069
+ emitCowtailStreamSnapshot({
15070
+ client: params.client,
15071
+ logger: params.logger,
15072
+ route: params.route,
15073
+ thread: params.thread,
15074
+ streamState: params.streamState,
15075
+ text: params.streamState.text,
15076
+ isFinal: true,
15077
+ updatedAt: Date.now()
15078
+ });
14848
15079
  params.streamState.completed = true;
14849
15080
  }
15081
+ function deliverCowtailPartialReply(params) {
15082
+ const text = params.payload.text;
15083
+ if (params.streamState.failed || typeof text !== "string" || !text.trim()) {
15084
+ return;
15085
+ }
15086
+ const now = Date.now();
15087
+ const previous = params.streamState.lastSnapshotText ?? "";
15088
+ const changedChars = Math.abs(text.length - previous.length);
15089
+ const sentRecently = params.streamState.lastSnapshotSentAt !== undefined && now - params.streamState.lastSnapshotSentAt < STREAM_MIN_INTERVAL_MS;
15090
+ if (text === previous || sentRecently && changedChars < STREAM_MIN_CHARS_DELTA) {
15091
+ return;
15092
+ }
15093
+ emitCowtailStreamSnapshot({
15094
+ client: params.client,
15095
+ logger: params.logger,
15096
+ route: params.route,
15097
+ thread: params.thread,
15098
+ streamState: params.streamState,
15099
+ text,
15100
+ isFinal: false,
15101
+ updatedAt: now
15102
+ });
15103
+ }
15104
+ function deliverCowtailToolProgress(params) {
15105
+ if (params.streamState.failed) {
15106
+ return;
15107
+ }
15108
+ const payload = readRecord(params.payload) ?? {};
15109
+ const stableId = readString(payload.itemId) ?? readString(payload.toolCallId) ?? readString(payload.id);
15110
+ if (params.requireStableId && !stableId) {
15111
+ return;
15112
+ }
15113
+ const id = stableId ?? fallbackCowtailToolCallId(params.streamState, payload);
15114
+ const name = readString(payload.name) ?? readString(payload.title) ?? readString(payload.command) ?? "tool_result";
15115
+ const status = params.status ?? normalizeCowtailToolCallStatus(payload.status) ?? normalizeCowtailToolCallStatus(payload.phase) ?? "running";
15116
+ const result = readCowtailToolProgressResult(payload);
15117
+ const args = readRecord(payload.args) ?? readRecord(payload.input);
15118
+ const now = Date.now();
15119
+ const currentText = currentLiveCowtailStreamText(params.streamState);
15120
+ const nextToolCall = {
15121
+ id,
15122
+ name,
15123
+ ...args ? { args } : {},
15124
+ ...result !== undefined ? { result } : {},
15125
+ status,
15126
+ ...status === "complete" || status === "error" ? { completedAt: now } : {},
15127
+ insertedAtContentLength: currentText.length,
15128
+ contentSnapshotAtStart: currentText
15129
+ };
15130
+ upsertCowtailToolCall(params.streamState, nextToolCall);
15131
+ emitCowtailStreamSnapshot({
15132
+ client: params.client,
15133
+ logger: params.logger,
15134
+ route: params.route,
15135
+ thread: params.thread,
15136
+ streamState: params.streamState,
15137
+ text: currentText,
15138
+ isFinal: false,
15139
+ updatedAt: now
15140
+ });
15141
+ }
15142
+ function emitCowtailStreamSnapshot(params) {
15143
+ if (params.streamState.failed || !params.text.trim() && params.streamState.links.length === 0 && params.streamState.toolCalls.length === 0) {
15144
+ return;
15145
+ }
15146
+ params.streamState.liveText = params.text;
15147
+ params.streamState.lastSnapshotText = params.text;
15148
+ params.streamState.lastSnapshotSentAt = params.updatedAt;
15149
+ params.client.sendOpenClawStreamSnapshot({
15150
+ type: "openclaw_message_stream_snapshot",
15151
+ streamId: params.streamState.streamId,
15152
+ sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
15153
+ threadId: params.thread.id,
15154
+ text: params.text,
15155
+ links: params.streamState.links.map((link) => ({ ...link })),
15156
+ toolCalls: structuredClone(params.streamState.toolCalls),
15157
+ isFinal: params.isFinal,
15158
+ updatedAt: params.updatedAt
15159
+ }).catch((error48) => {
15160
+ params.logger?.warn?.(`Cowtail stream snapshot send failed: ${errorMessage(error48)}`);
15161
+ });
15162
+ }
15163
+ function currentLiveCowtailStreamText(streamState) {
15164
+ return streamState.liveText ?? streamState.text;
15165
+ }
15166
+ function upsertCowtailToolCall(streamState, toolCall) {
15167
+ const index = streamState.toolCalls.findIndex((candidate) => candidate.id === toolCall.id);
15168
+ if (index < 0) {
15169
+ streamState.toolCalls = [...streamState.toolCalls, toolCall];
15170
+ return;
15171
+ }
15172
+ const existing = streamState.toolCalls[index];
15173
+ streamState.toolCalls[index] = {
15174
+ ...existing,
15175
+ ...toolCall,
15176
+ insertedAtContentLength: existing.insertedAtContentLength ?? toolCall.insertedAtContentLength,
15177
+ contentSnapshotAtStart: existing.contentSnapshotAtStart ?? toolCall.contentSnapshotAtStart
15178
+ };
15179
+ }
14850
15180
  function recordCreatedOpenClawMessageId(streamState, result) {
14851
15181
  if (result.payload?.dropped === true) {
15182
+ if (result.payload.duplicate === true) {
15183
+ const duplicateMessageId = result.payload.messageId;
15184
+ if (typeof duplicateMessageId === "string" && duplicateMessageId.trim()) {
15185
+ return duplicateMessageId;
15186
+ }
15187
+ }
14852
15188
  streamState.failed = true;
14853
15189
  streamState.completed = true;
14854
15190
  return;
@@ -14860,6 +15196,10 @@ function recordCreatedOpenClawMessageId(streamState, result) {
14860
15196
  streamState.failed = true;
14861
15197
  throw new Error("Cowtail realtime ack missing messageId for streamed OpenClaw reply");
14862
15198
  }
15199
+ function nextStreamUpdateIdempotencyKey(streamState) {
15200
+ streamState.updateIndex += 1;
15201
+ return `${streamState.idempotencyKey}:update:${streamState.updateIndex}`;
15202
+ }
14863
15203
  function resolveCowtailReplyLinks(payload) {
14864
15204
  const urls = [
14865
15205
  ...Array.isArray(payload.mediaUrls) ? payload.mediaUrls : [],
@@ -14932,6 +15272,48 @@ function readRecord(value) {
14932
15272
  function readToolCallStatus(value) {
14933
15273
  return value === "pending" || value === "running" || value === "complete" || value === "error" ? value : undefined;
14934
15274
  }
15275
+ function normalizeCowtailToolCallStatus(value) {
15276
+ const status = readToolCallStatus(value);
15277
+ if (status) {
15278
+ return status;
15279
+ }
15280
+ if (value === "start" || value === "started" || value === "working") {
15281
+ return "running";
15282
+ }
15283
+ if (value === "end" || value === "done" || value === "completed" || value === "success") {
15284
+ return "complete";
15285
+ }
15286
+ if (value === "failed" || value === "failure") {
15287
+ return "error";
15288
+ }
15289
+ return;
15290
+ }
15291
+ function readCowtailToolProgressResult(payload) {
15292
+ const stringResult = readString(payload.output) ?? readString(payload.summary) ?? readString(payload.message) ?? readString(payload.progressText) ?? readString(payload.text);
15293
+ if (stringResult !== undefined) {
15294
+ return stringResult;
15295
+ }
15296
+ if (payload.result !== undefined) {
15297
+ return payload.result;
15298
+ }
15299
+ const structuredPatch = {
15300
+ ...Array.isArray(payload.added) ? { added: payload.added } : {},
15301
+ ...Array.isArray(payload.modified) ? { modified: payload.modified } : {},
15302
+ ...Array.isArray(payload.deleted) ? { deleted: payload.deleted } : {}
15303
+ };
15304
+ return Object.keys(structuredPatch).length > 0 ? structuredPatch : undefined;
15305
+ }
15306
+ function fallbackCowtailToolCallId(streamState, payload) {
15307
+ const key = readString(payload.name) ?? readString(payload.title) ?? readString(payload.command) ?? JSON.stringify(Object.keys(payload).sort());
15308
+ const existing = streamState.toolFallbackIds[key];
15309
+ if (existing) {
15310
+ return existing;
15311
+ }
15312
+ streamState.toolFallbackIndex += 1;
15313
+ const next = `tool-${streamState.toolFallbackIndex}`;
15314
+ streamState.toolFallbackIds[key] = next;
15315
+ return next;
15316
+ }
14935
15317
  function readTimestamp(value) {
14936
15318
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
14937
15319
  }