@maudecode/openclaw-cowtail 0.12.3 → 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
@@ -13874,6 +13874,20 @@ var openclawPluginMessageUpdateCommandSchema = requireOpenClawRenderableContent(
13874
13874
  actions: exports_external.array(openclawActionDraftSchema).optional(),
13875
13875
  deliveryState: openclawDeliveryStateSchema.optional()
13876
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 }));
13877
13891
  var openclawIosNewThreadCommandSchema = exports_external.object({
13878
13892
  type: exports_external.literal("ios_new_thread"),
13879
13893
  requestId: openclawRequestIdSchema,
@@ -13932,6 +13946,7 @@ var openclawActionResultCommandSchema = exports_external.object({
13932
13946
  var openclawRealtimeClientMessageSchema = exports_external.union([
13933
13947
  openclawPluginMessageCommandSchema,
13934
13948
  openclawPluginMessageUpdateCommandSchema,
13949
+ openclawMessageStreamSnapshotCommandSchema,
13935
13950
  openclawIosNewThreadCommandSchema,
13936
13951
  openclawIosReplyCommandSchema,
13937
13952
  openclawIosActionCommandSchema,
@@ -13954,6 +13969,7 @@ var openclawRealtimeErrorSchema = exports_external.object({
13954
13969
  });
13955
13970
  var openclawRealtimeServerMessageSchema = exports_external.union([
13956
13971
  openclawEventEnvelopeSchema,
13972
+ openclawMessageStreamSnapshotServerMessageSchema,
13957
13973
  openclawRealtimeAckSchema,
13958
13974
  openclawRealtimeErrorSchema
13959
13975
  ]);
@@ -14212,6 +14228,12 @@ class CowtailRealtimeClient {
14212
14228
  idempotencyKey: command.idempotencyKey ?? `cowtail:request:${requestId}`
14213
14229
  }).then((result) => result.sequence);
14214
14230
  }
14231
+ sendOpenClawStreamSnapshot(command) {
14232
+ return this.#sendTransientCommand({
14233
+ ...command,
14234
+ requestId: this.#requestIdFactory()
14235
+ });
14236
+ }
14215
14237
  #connect() {
14216
14238
  if (!this.#started || this.#socket) {
14217
14239
  return;
@@ -14311,6 +14333,9 @@ class CowtailRealtimeClient {
14311
14333
  this.#rejectPendingRequest(message);
14312
14334
  return;
14313
14335
  }
14336
+ if (message.type === "openclaw_message_stream_snapshot") {
14337
+ return;
14338
+ }
14314
14339
  await this.#onEvent(message);
14315
14340
  await this.#stateStore.writeLastSeenSequence(message.sequence);
14316
14341
  }
@@ -14387,6 +14412,19 @@ class CowtailRealtimeClient {
14387
14412
  }
14388
14413
  });
14389
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
+ }
14390
14428
  #resolvePendingRequest(requestId, sequence, payload) {
14391
14429
  const pending = this.#pendingRequests.get(requestId);
14392
14430
  if (!pending) {
@@ -14491,6 +14529,8 @@ var CHANNEL_LABEL = "Cowtail";
14491
14529
  var SENDER_LABEL = "Cowtail iOS";
14492
14530
  var SENDER_ID = "cowtail-ios";
14493
14531
  var SENDER_ADDRESS = "cowtail:ios";
14532
+ var STREAM_MIN_INTERVAL_MS = 45;
14533
+ var STREAM_MIN_CHARS_DELTA = 3;
14494
14534
  async function handleCowtailEvent(params) {
14495
14535
  const { event, account, client, runtime, logger } = params;
14496
14536
  switch (event.type) {
@@ -14626,7 +14666,10 @@ async function dispatchCowtailTextTurn(params) {
14626
14666
  });
14627
14667
  const streamState = {
14628
14668
  idempotencyKey: `cowtail:reply:${message.id}`,
14669
+ streamId: `cowtail:stream:${message.id}`,
14629
14670
  updateIndex: 0,
14671
+ toolFallbackIndex: 0,
14672
+ toolFallbackIds: {},
14630
14673
  text: "",
14631
14674
  links: [],
14632
14675
  toolCalls: []
@@ -14636,12 +14679,17 @@ async function dispatchCowtailTextTurn(params) {
14636
14679
  channel: CHANNEL_ID,
14637
14680
  accountId: account.accountId,
14638
14681
  agentId: route.agentId,
14682
+ client,
14683
+ logger,
14639
14684
  route,
14685
+ thread,
14640
14686
  storePath,
14641
14687
  ctxPayload,
14642
14688
  runtime,
14689
+ streamState,
14643
14690
  deliver: async (payload, info, rawPayload) => deliverCowtailReply({
14644
14691
  client,
14692
+ logger,
14645
14693
  route,
14646
14694
  thread,
14647
14695
  payload,
@@ -14658,6 +14706,9 @@ async function dispatchCowtailTextTurn(params) {
14658
14706
  });
14659
14707
  await finalizeCowtailStreamedReply({
14660
14708
  client,
14709
+ logger,
14710
+ route,
14711
+ thread,
14661
14712
  streamState
14662
14713
  });
14663
14714
  }
@@ -14688,7 +14739,10 @@ async function dispatchCowtailActionTurn(params) {
14688
14739
  let dispatchFailed = false;
14689
14740
  const streamState = {
14690
14741
  idempotencyKey: `cowtail:action:${action.id}`,
14742
+ streamId: `cowtail:action-stream:${action.id}`,
14691
14743
  updateIndex: 0,
14744
+ toolFallbackIndex: 0,
14745
+ toolFallbackIds: {},
14692
14746
  text: "",
14693
14747
  links: [],
14694
14748
  toolCalls: []
@@ -14698,12 +14752,17 @@ async function dispatchCowtailActionTurn(params) {
14698
14752
  channel: CHANNEL_ID,
14699
14753
  accountId: account.accountId,
14700
14754
  agentId: route.agentId,
14755
+ client,
14756
+ logger,
14701
14757
  route,
14758
+ thread,
14702
14759
  storePath,
14703
14760
  ctxPayload,
14704
14761
  runtime,
14762
+ streamState,
14705
14763
  deliver: async (replyPayload, info, rawPayload) => deliverCowtailReply({
14706
14764
  client,
14765
+ logger,
14707
14766
  route,
14708
14767
  thread,
14709
14768
  payload: replyPayload,
@@ -14721,6 +14780,9 @@ async function dispatchCowtailActionTurn(params) {
14721
14780
  });
14722
14781
  await finalizeCowtailStreamedReply({
14723
14782
  client,
14783
+ logger,
14784
+ route,
14785
+ thread,
14724
14786
  streamState
14725
14787
  });
14726
14788
  return !dispatchFailed;
@@ -14754,7 +14816,79 @@ async function recordCowtailInboundSessionAndDispatchReply(params) {
14754
14816
  },
14755
14817
  replyOptions: {
14756
14818
  disableBlockStreaming: false,
14757
- 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
+ }
14758
14892
  }
14759
14893
  });
14760
14894
  }
@@ -14774,10 +14908,20 @@ async function deliverCowtailReply(params) {
14774
14908
  rawPayload: params.rawPayload,
14775
14909
  streamState: params.streamState
14776
14910
  });
14777
- params.streamState.toolCalls = [...params.streamState.toolCalls, toolCall];
14911
+ upsertCowtailToolCall(params.streamState, toolCall);
14778
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
+ });
14779
14923
  if (!params.streamState.messageId) {
14780
- const result = await params.client.sendOpenClawMessage({
14924
+ const result2 = await params.client.sendOpenClawMessage({
14781
14925
  type: "openclaw_message",
14782
14926
  idempotencyKey: params.streamState.idempotencyKey,
14783
14927
  sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
@@ -14789,11 +14933,11 @@ async function deliverCowtailReply(params) {
14789
14933
  actions: [],
14790
14934
  deliveryState: "pending"
14791
14935
  });
14792
- const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
14793
- if (!messageId) {
14936
+ const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14937
+ if (!messageId2) {
14794
14938
  return;
14795
14939
  }
14796
- params.streamState.messageId = messageId;
14940
+ params.streamState.messageId = messageId2;
14797
14941
  return;
14798
14942
  }
14799
14943
  await params.client.sendOpenClawMessageUpdate({
@@ -14812,8 +14956,18 @@ async function deliverCowtailReply(params) {
14812
14956
  }
14813
14957
  if (kind === "block") {
14814
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
+ });
14815
14969
  if (!params.streamState.messageId) {
14816
- const result = await params.client.sendOpenClawMessage({
14970
+ const result2 = await params.client.sendOpenClawMessage({
14817
14971
  type: "openclaw_message",
14818
14972
  idempotencyKey: params.streamState.idempotencyKey,
14819
14973
  sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
@@ -14825,11 +14979,11 @@ async function deliverCowtailReply(params) {
14825
14979
  actions: [],
14826
14980
  deliveryState: "pending"
14827
14981
  });
14828
- const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
14829
- if (!messageId) {
14982
+ const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14983
+ if (!messageId2) {
14830
14984
  return;
14831
14985
  }
14832
- params.streamState.messageId = messageId;
14986
+ params.streamState.messageId = messageId2;
14833
14987
  return;
14834
14988
  }
14835
14989
  await params.client.sendOpenClawMessageUpdate({
@@ -14855,10 +15009,21 @@ async function deliverCowtailReply(params) {
14855
15009
  actions: [],
14856
15010
  deliveryState: "sent"
14857
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
+ });
14858
15022
  params.streamState.completed = true;
14859
15023
  return;
14860
15024
  }
14861
- await params.client.sendOpenClawMessage({
15025
+ params.streamState.text = text;
15026
+ const result = await params.client.sendOpenClawMessage({
14862
15027
  type: "openclaw_message",
14863
15028
  idempotencyKey: params.streamState.idempotencyKey,
14864
15029
  sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
@@ -14870,6 +15035,21 @@ async function deliverCowtailReply(params) {
14870
15035
  actions: [],
14871
15036
  deliveryState: "sent"
14872
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
+ });
14873
15053
  params.streamState.completed = true;
14874
15054
  }
14875
15055
  async function finalizeCowtailStreamedReply(params) {
@@ -14886,8 +15066,117 @@ async function finalizeCowtailStreamedReply(params) {
14886
15066
  actions: [],
14887
15067
  deliveryState: "sent"
14888
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
+ });
14889
15079
  params.streamState.completed = true;
14890
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
+ }
14891
15180
  function recordCreatedOpenClawMessageId(streamState, result) {
14892
15181
  if (result.payload?.dropped === true) {
14893
15182
  if (result.payload.duplicate === true) {
@@ -14983,6 +15272,48 @@ function readRecord(value) {
14983
15272
  function readToolCallStatus(value) {
14984
15273
  return value === "pending" || value === "running" || value === "complete" || value === "error" ? value : undefined;
14985
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
+ }
14986
15317
  function readTimestamp(value) {
14987
15318
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
14988
15319
  }
@@ -13874,6 +13874,20 @@ var openclawPluginMessageUpdateCommandSchema = requireOpenClawRenderableContent(
13874
13874
  actions: exports_external.array(openclawActionDraftSchema).optional(),
13875
13875
  deliveryState: openclawDeliveryStateSchema.optional()
13876
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 }));
13877
13891
  var openclawIosNewThreadCommandSchema = exports_external.object({
13878
13892
  type: exports_external.literal("ios_new_thread"),
13879
13893
  requestId: openclawRequestIdSchema,
@@ -13932,6 +13946,7 @@ var openclawActionResultCommandSchema = exports_external.object({
13932
13946
  var openclawRealtimeClientMessageSchema = exports_external.union([
13933
13947
  openclawPluginMessageCommandSchema,
13934
13948
  openclawPluginMessageUpdateCommandSchema,
13949
+ openclawMessageStreamSnapshotCommandSchema,
13935
13950
  openclawIosNewThreadCommandSchema,
13936
13951
  openclawIosReplyCommandSchema,
13937
13952
  openclawIosActionCommandSchema,
@@ -13954,6 +13969,7 @@ var openclawRealtimeErrorSchema = exports_external.object({
13954
13969
  });
13955
13970
  var openclawRealtimeServerMessageSchema = exports_external.union([
13956
13971
  openclawEventEnvelopeSchema,
13972
+ openclawMessageStreamSnapshotServerMessageSchema,
13957
13973
  openclawRealtimeAckSchema,
13958
13974
  openclawRealtimeErrorSchema
13959
13975
  ]);
@@ -14212,6 +14228,12 @@ class CowtailRealtimeClient {
14212
14228
  idempotencyKey: command.idempotencyKey ?? `cowtail:request:${requestId}`
14213
14229
  }).then((result) => result.sequence);
14214
14230
  }
14231
+ sendOpenClawStreamSnapshot(command) {
14232
+ return this.#sendTransientCommand({
14233
+ ...command,
14234
+ requestId: this.#requestIdFactory()
14235
+ });
14236
+ }
14215
14237
  #connect() {
14216
14238
  if (!this.#started || this.#socket) {
14217
14239
  return;
@@ -14311,6 +14333,9 @@ class CowtailRealtimeClient {
14311
14333
  this.#rejectPendingRequest(message);
14312
14334
  return;
14313
14335
  }
14336
+ if (message.type === "openclaw_message_stream_snapshot") {
14337
+ return;
14338
+ }
14314
14339
  await this.#onEvent(message);
14315
14340
  await this.#stateStore.writeLastSeenSequence(message.sequence);
14316
14341
  }
@@ -14387,6 +14412,19 @@ class CowtailRealtimeClient {
14387
14412
  }
14388
14413
  });
14389
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
+ }
14390
14428
  #resolvePendingRequest(requestId, sequence, payload) {
14391
14429
  const pending = this.#pendingRequests.get(requestId);
14392
14430
  if (!pending) {
@@ -14491,6 +14529,8 @@ var CHANNEL_LABEL = "Cowtail";
14491
14529
  var SENDER_LABEL = "Cowtail iOS";
14492
14530
  var SENDER_ID = "cowtail-ios";
14493
14531
  var SENDER_ADDRESS = "cowtail:ios";
14532
+ var STREAM_MIN_INTERVAL_MS = 45;
14533
+ var STREAM_MIN_CHARS_DELTA = 3;
14494
14534
  async function handleCowtailEvent(params) {
14495
14535
  const { event, account, client, runtime, logger } = params;
14496
14536
  switch (event.type) {
@@ -14626,7 +14666,10 @@ async function dispatchCowtailTextTurn(params) {
14626
14666
  });
14627
14667
  const streamState = {
14628
14668
  idempotencyKey: `cowtail:reply:${message.id}`,
14669
+ streamId: `cowtail:stream:${message.id}`,
14629
14670
  updateIndex: 0,
14671
+ toolFallbackIndex: 0,
14672
+ toolFallbackIds: {},
14630
14673
  text: "",
14631
14674
  links: [],
14632
14675
  toolCalls: []
@@ -14636,12 +14679,17 @@ async function dispatchCowtailTextTurn(params) {
14636
14679
  channel: CHANNEL_ID,
14637
14680
  accountId: account.accountId,
14638
14681
  agentId: route.agentId,
14682
+ client,
14683
+ logger,
14639
14684
  route,
14685
+ thread,
14640
14686
  storePath,
14641
14687
  ctxPayload,
14642
14688
  runtime,
14689
+ streamState,
14643
14690
  deliver: async (payload, info, rawPayload) => deliverCowtailReply({
14644
14691
  client,
14692
+ logger,
14645
14693
  route,
14646
14694
  thread,
14647
14695
  payload,
@@ -14658,6 +14706,9 @@ async function dispatchCowtailTextTurn(params) {
14658
14706
  });
14659
14707
  await finalizeCowtailStreamedReply({
14660
14708
  client,
14709
+ logger,
14710
+ route,
14711
+ thread,
14661
14712
  streamState
14662
14713
  });
14663
14714
  }
@@ -14688,7 +14739,10 @@ async function dispatchCowtailActionTurn(params) {
14688
14739
  let dispatchFailed = false;
14689
14740
  const streamState = {
14690
14741
  idempotencyKey: `cowtail:action:${action.id}`,
14742
+ streamId: `cowtail:action-stream:${action.id}`,
14691
14743
  updateIndex: 0,
14744
+ toolFallbackIndex: 0,
14745
+ toolFallbackIds: {},
14692
14746
  text: "",
14693
14747
  links: [],
14694
14748
  toolCalls: []
@@ -14698,12 +14752,17 @@ async function dispatchCowtailActionTurn(params) {
14698
14752
  channel: CHANNEL_ID,
14699
14753
  accountId: account.accountId,
14700
14754
  agentId: route.agentId,
14755
+ client,
14756
+ logger,
14701
14757
  route,
14758
+ thread,
14702
14759
  storePath,
14703
14760
  ctxPayload,
14704
14761
  runtime,
14762
+ streamState,
14705
14763
  deliver: async (replyPayload, info, rawPayload) => deliverCowtailReply({
14706
14764
  client,
14765
+ logger,
14707
14766
  route,
14708
14767
  thread,
14709
14768
  payload: replyPayload,
@@ -14721,6 +14780,9 @@ async function dispatchCowtailActionTurn(params) {
14721
14780
  });
14722
14781
  await finalizeCowtailStreamedReply({
14723
14782
  client,
14783
+ logger,
14784
+ route,
14785
+ thread,
14724
14786
  streamState
14725
14787
  });
14726
14788
  return !dispatchFailed;
@@ -14754,7 +14816,79 @@ async function recordCowtailInboundSessionAndDispatchReply(params) {
14754
14816
  },
14755
14817
  replyOptions: {
14756
14818
  disableBlockStreaming: false,
14757
- 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
+ }
14758
14892
  }
14759
14893
  });
14760
14894
  }
@@ -14774,10 +14908,20 @@ async function deliverCowtailReply(params) {
14774
14908
  rawPayload: params.rawPayload,
14775
14909
  streamState: params.streamState
14776
14910
  });
14777
- params.streamState.toolCalls = [...params.streamState.toolCalls, toolCall];
14911
+ upsertCowtailToolCall(params.streamState, toolCall);
14778
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
+ });
14779
14923
  if (!params.streamState.messageId) {
14780
- const result = await params.client.sendOpenClawMessage({
14924
+ const result2 = await params.client.sendOpenClawMessage({
14781
14925
  type: "openclaw_message",
14782
14926
  idempotencyKey: params.streamState.idempotencyKey,
14783
14927
  sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
@@ -14789,11 +14933,11 @@ async function deliverCowtailReply(params) {
14789
14933
  actions: [],
14790
14934
  deliveryState: "pending"
14791
14935
  });
14792
- const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
14793
- if (!messageId) {
14936
+ const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14937
+ if (!messageId2) {
14794
14938
  return;
14795
14939
  }
14796
- params.streamState.messageId = messageId;
14940
+ params.streamState.messageId = messageId2;
14797
14941
  return;
14798
14942
  }
14799
14943
  await params.client.sendOpenClawMessageUpdate({
@@ -14812,8 +14956,18 @@ async function deliverCowtailReply(params) {
14812
14956
  }
14813
14957
  if (kind === "block") {
14814
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
+ });
14815
14969
  if (!params.streamState.messageId) {
14816
- const result = await params.client.sendOpenClawMessage({
14970
+ const result2 = await params.client.sendOpenClawMessage({
14817
14971
  type: "openclaw_message",
14818
14972
  idempotencyKey: params.streamState.idempotencyKey,
14819
14973
  sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
@@ -14825,11 +14979,11 @@ async function deliverCowtailReply(params) {
14825
14979
  actions: [],
14826
14980
  deliveryState: "pending"
14827
14981
  });
14828
- const messageId = recordCreatedOpenClawMessageId(params.streamState, result);
14829
- if (!messageId) {
14982
+ const messageId2 = recordCreatedOpenClawMessageId(params.streamState, result2);
14983
+ if (!messageId2) {
14830
14984
  return;
14831
14985
  }
14832
- params.streamState.messageId = messageId;
14986
+ params.streamState.messageId = messageId2;
14833
14987
  return;
14834
14988
  }
14835
14989
  await params.client.sendOpenClawMessageUpdate({
@@ -14855,10 +15009,21 @@ async function deliverCowtailReply(params) {
14855
15009
  actions: [],
14856
15010
  deliveryState: "sent"
14857
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
+ });
14858
15022
  params.streamState.completed = true;
14859
15023
  return;
14860
15024
  }
14861
- await params.client.sendOpenClawMessage({
15025
+ params.streamState.text = text;
15026
+ const result = await params.client.sendOpenClawMessage({
14862
15027
  type: "openclaw_message",
14863
15028
  idempotencyKey: params.streamState.idempotencyKey,
14864
15029
  sessionKey: params.thread.sessionKey ?? params.route.sessionKey,
@@ -14870,6 +15035,21 @@ async function deliverCowtailReply(params) {
14870
15035
  actions: [],
14871
15036
  deliveryState: "sent"
14872
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
+ });
14873
15053
  params.streamState.completed = true;
14874
15054
  }
14875
15055
  async function finalizeCowtailStreamedReply(params) {
@@ -14886,8 +15066,117 @@ async function finalizeCowtailStreamedReply(params) {
14886
15066
  actions: [],
14887
15067
  deliveryState: "sent"
14888
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
+ });
14889
15079
  params.streamState.completed = true;
14890
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
+ }
14891
15180
  function recordCreatedOpenClawMessageId(streamState, result) {
14892
15181
  if (result.payload?.dropped === true) {
14893
15182
  if (result.payload.duplicate === true) {
@@ -14983,6 +15272,48 @@ function readRecord(value) {
14983
15272
  function readToolCallStatus(value) {
14984
15273
  return value === "pending" || value === "running" || value === "complete" || value === "error" ? value : undefined;
14985
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
+ }
14986
15317
  function readTimestamp(value) {
14987
15318
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
14988
15319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maudecode/openclaw-cowtail",
3
- "version": "0.12.3",
3
+ "version": "0.13.0",
4
4
  "description": "Cowtail channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "repository": {