@stagewhisper/stagewhisper 0.57.0 → 0.60.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
@@ -98,6 +98,75 @@ var init_client = __esm({
98
98
  throw new Error(`Reply failed (${res.status}): ${text}`);
99
99
  }
100
100
  }
101
+ async postChatReply(userMessageId, content, options) {
102
+ const body = {
103
+ content,
104
+ status: options?.status ?? "completed"
105
+ };
106
+ if (options?.errorCode) body.error_code = options.errorCode;
107
+ if (options?.errorMessage) body.error_message = options.errorMessage;
108
+ if (options?.metadata) body.metadata_ = options.metadata;
109
+ const res = await fetch(
110
+ `${this.baseUrl}/api/v1/openclaw/chat/messages/${userMessageId}/reply`,
111
+ {
112
+ method: "POST",
113
+ headers: this.headers(),
114
+ body: JSON.stringify(body)
115
+ }
116
+ );
117
+ if (!res.ok) {
118
+ const text = await res.text();
119
+ throw new Error(`Chat reply failed (${res.status}): ${text}`);
120
+ }
121
+ }
122
+ async startChatReply(userMessageId) {
123
+ const res = await fetch(
124
+ `${this.baseUrl}/api/v1/openclaw/chat/messages/${userMessageId}/reply/start`,
125
+ {
126
+ method: "POST",
127
+ headers: this.headers()
128
+ }
129
+ );
130
+ if (!res.ok) {
131
+ const text = await res.text();
132
+ throw new Error(`Chat reply start failed (${res.status}): ${text}`);
133
+ }
134
+ return await res.json();
135
+ }
136
+ async postChatReplyDelta(assistantMessageId, delta) {
137
+ const res = await fetch(
138
+ `${this.baseUrl}/api/v1/openclaw/chat/messages/${assistantMessageId}/reply/delta`,
139
+ {
140
+ method: "POST",
141
+ headers: this.headers(),
142
+ body: JSON.stringify({ delta })
143
+ }
144
+ );
145
+ if (!res.ok) {
146
+ const text = await res.text();
147
+ throw new Error(`Chat reply delta failed (${res.status}): ${text}`);
148
+ }
149
+ }
150
+ async completeChatReply(assistantMessageId, options) {
151
+ const body = {
152
+ status: options?.status ?? "completed"
153
+ };
154
+ if (options?.content !== void 0) body.content = options.content;
155
+ if (options?.errorCode) body.error_code = options.errorCode;
156
+ if (options?.errorMessage) body.error_message = options.errorMessage;
157
+ const res = await fetch(
158
+ `${this.baseUrl}/api/v1/openclaw/chat/messages/${assistantMessageId}/reply/complete`,
159
+ {
160
+ method: "POST",
161
+ headers: this.headers(),
162
+ body: JSON.stringify(body)
163
+ }
164
+ );
165
+ if (!res.ok) {
166
+ const text = await res.text();
167
+ throw new Error(`Chat reply complete failed (${res.status}): ${text}`);
168
+ }
169
+ }
101
170
  async heartbeat(capabilities) {
102
171
  const res = await fetch(
103
172
  `${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/heartbeat`,
@@ -4166,6 +4235,24 @@ function createRelayService(api) {
4166
4235
  const COMPLETED_JOB_TTL_MS = 5 * 60 * 1e3;
4167
4236
  const COMPLETED_JOB_MAX_SIZE = 5e3;
4168
4237
  const processingReasoningJobs = /* @__PURE__ */ new Set();
4238
+ const chatSessionQueues = /* @__PURE__ */ new Map();
4239
+ function enqueueChat(sessionId, work) {
4240
+ const prev = chatSessionQueues.get(sessionId) ?? Promise.resolve();
4241
+ const next = prev.catch(() => {
4242
+ }).then(async () => {
4243
+ try {
4244
+ await work();
4245
+ } catch (err) {
4246
+ api.logger.error(`Chat queue work failed: ${err}`);
4247
+ }
4248
+ });
4249
+ chatSessionQueues.set(sessionId, next);
4250
+ void next.then(() => {
4251
+ if (chatSessionQueues.get(sessionId) === next) {
4252
+ chatSessionQueues.delete(sessionId);
4253
+ }
4254
+ });
4255
+ }
4169
4256
  function evictStaleCompletedJobs() {
4170
4257
  const cutoff = Date.now() - COMPLETED_JOB_TTL_MS;
4171
4258
  for (const [jobId, completedAt] of completedReasoningJobs) {
@@ -4322,6 +4409,34 @@ function createRelayService(api) {
4322
4409
  }
4323
4410
  return null;
4324
4411
  }
4412
+ async function extractReplyForChatMessage(sessionKey, userMessageId, maxAttempts = 3) {
4413
+ const marker = `[StageWhisper chat: ${userMessageId}]`;
4414
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4415
+ if (attempt > 0) {
4416
+ await new Promise((r) => setTimeout(r, 1500));
4417
+ }
4418
+ const session = await api.runtime.subagent.getSessionMessages({
4419
+ sessionKey,
4420
+ limit: 100
4421
+ });
4422
+ const messages = session.messages;
4423
+ for (let i = 0; i < messages.length; i++) {
4424
+ const msg = messages[i];
4425
+ if (msg["role"] !== "user") continue;
4426
+ const text = extractContentFromMessage(msg) ?? "";
4427
+ if (!text.includes(marker)) continue;
4428
+ for (let j = i + 1; j < messages.length; j++) {
4429
+ const next = messages[j];
4430
+ const role = next["role"];
4431
+ if (role === "assistant" || role === "model") {
4432
+ return extractContentFromMessage(next);
4433
+ }
4434
+ if (role === "user") break;
4435
+ }
4436
+ }
4437
+ }
4438
+ return null;
4439
+ }
4325
4440
  function decryptTaskFields(task, client) {
4326
4441
  const keypair = client.pluginKeypair;
4327
4442
  const desktopPub = client.desktopPublicKey;
@@ -4746,6 +4861,278 @@ function createRelayService(api) {
4746
4861
  api.logger.error(`Failed to post BYO reasoning result for ${job.job_id}: ${postErr}`);
4747
4862
  }
4748
4863
  }
4864
+ function looksLikeBYOEnvelope(s) {
4865
+ const trimmed = s.trimStart();
4866
+ if (!trimmed.startsWith("{")) return false;
4867
+ return trimmed.includes('"version"') && trimmed.includes('"sender_role"') && trimmed.includes('"ciphertext"');
4868
+ }
4869
+ function decryptChatPrompt(rawContent, client) {
4870
+ const keypair = client.pluginKeypair;
4871
+ const desktopPub = client.desktopPublicKey;
4872
+ if (!keypair || !desktopPub) {
4873
+ return { error: "missing_byo_keys" };
4874
+ }
4875
+ try {
4876
+ const envelope = JSON.parse(rawContent);
4877
+ const envelopeKey = keypair.deriveEnvelopeKey(desktopPub);
4878
+ const plaintextBytes = open(envelopeKey, envelope);
4879
+ return {
4880
+ plaintext: new TextDecoder().decode(plaintextBytes),
4881
+ envelopeKey
4882
+ };
4883
+ } catch (err) {
4884
+ return { error: `decryption_error: ${err}` };
4885
+ }
4886
+ }
4887
+ function sealChatReply(envelopeKey, sessionId, userMessageId, replyText) {
4888
+ const envelope = seal(
4889
+ envelopeKey,
4890
+ "plugin",
4891
+ sessionId,
4892
+ `chat-reply:${userMessageId}`,
4893
+ "task_content",
4894
+ new TextEncoder().encode(replyText)
4895
+ );
4896
+ return JSON.stringify(envelope);
4897
+ }
4898
+ async function handleChatMessage(envelope, client) {
4899
+ const userMessageId = envelope.user_message_id;
4900
+ const sessionId = envelope.session_id;
4901
+ const rawContent = envelope.content;
4902
+ if (!userMessageId || !sessionId || !rawContent) {
4903
+ api.logger.warn(
4904
+ `Skipping chat message dispatch \u2014 missing fields (user_message_id=${userMessageId}, session_id=${sessionId})`
4905
+ );
4906
+ return;
4907
+ }
4908
+ api.logger.info(
4909
+ `Received chat message: ${userMessageId} (session: ${sessionId})`
4910
+ );
4911
+ let plaintextContent = rawContent;
4912
+ let envelopeKey = null;
4913
+ if (looksLikeBYOEnvelope(rawContent)) {
4914
+ const decryptResult = decryptChatPrompt(rawContent, client);
4915
+ if ("error" in decryptResult) {
4916
+ api.logger.error(
4917
+ `Chat ${userMessageId} BYO decrypt failed: ${decryptResult.error}`
4918
+ );
4919
+ try {
4920
+ await client.postChatReply(
4921
+ userMessageId,
4922
+ "(BYO decrypt failed)",
4923
+ {
4924
+ status: "errored",
4925
+ errorCode: decryptResult.error.startsWith("missing_") ? "missing_byo_keys" : "decryption_error",
4926
+ errorMessage: decryptResult.error
4927
+ }
4928
+ );
4929
+ } catch (postErr) {
4930
+ api.logger.error(`Failed to report BYO error: ${postErr}`);
4931
+ }
4932
+ return;
4933
+ }
4934
+ plaintextContent = decryptResult.plaintext;
4935
+ envelopeKey = decryptResult.envelopeKey;
4936
+ }
4937
+ const sessionKey = buildAgentSessionKey({
4938
+ agentId: "default",
4939
+ channel: "stagewhisper",
4940
+ peer: { kind: "direct", id: `sw-chat-${sessionId}` }
4941
+ });
4942
+ const finalizeReply = async (replyText, options) => {
4943
+ const outbound = envelopeKey ? sealChatReply(envelopeKey, sessionId, userMessageId, replyText) : replyText;
4944
+ await client.postChatReply(userMessageId, outbound, options);
4945
+ };
4946
+ const decoratedPrompt = `${plaintextContent}
4947
+
4948
+ [StageWhisper chat: ${userMessageId}]`;
4949
+ if (envelopeKey === null) {
4950
+ await handleChatMessageStreaming(
4951
+ client,
4952
+ sessionKey,
4953
+ userMessageId,
4954
+ decoratedPrompt
4955
+ );
4956
+ return;
4957
+ }
4958
+ try {
4959
+ const result = await api.runtime.subagent.run({
4960
+ sessionKey,
4961
+ message: decoratedPrompt,
4962
+ deliver: true,
4963
+ idempotencyKey: `sw-chat-${userMessageId}`
4964
+ });
4965
+ const waitResult = await api.runtime.subagent.waitForRun({
4966
+ runId: result.runId,
4967
+ timeoutMs: 12e4
4968
+ });
4969
+ if (waitResult.status === "ok") {
4970
+ const reply = await extractReplyForChatMessage(sessionKey, userMessageId);
4971
+ if (reply) {
4972
+ await finalizeReply(reply);
4973
+ api.logger.info(`Chat message ${userMessageId} replied`);
4974
+ } else {
4975
+ api.logger.warn(
4976
+ `Chat message ${userMessageId} completed but no reply found`
4977
+ );
4978
+ await finalizeReply("(no reply produced)", {
4979
+ status: "errored",
4980
+ errorCode: "no_reply",
4981
+ errorMessage: "Agent run produced no reply"
4982
+ });
4983
+ }
4984
+ } else {
4985
+ api.logger.error(
4986
+ `Agent run failed for chat message ${userMessageId}: ${waitResult.error}`
4987
+ );
4988
+ await finalizeReply("(agent run failed)", {
4989
+ status: "errored",
4990
+ errorCode: "agent_error",
4991
+ errorMessage: waitResult.error ?? "Agent run failed"
4992
+ });
4993
+ }
4994
+ } catch (err) {
4995
+ const errMsg = err instanceof Error ? err.message : String(err);
4996
+ api.logger.error(`Failed to process chat message ${userMessageId}: ${errMsg}`);
4997
+ try {
4998
+ await finalizeReply("(execution error)", {
4999
+ status: "errored",
5000
+ errorCode: "execution_error",
5001
+ errorMessage: errMsg
5002
+ });
5003
+ } catch (postErr) {
5004
+ api.logger.error(`Failed to report chat failure: ${postErr}`);
5005
+ }
5006
+ }
5007
+ }
5008
+ async function handleChatMessageStreaming(client, sessionKey, userMessageId, decoratedPrompt) {
5009
+ let assistantId;
5010
+ try {
5011
+ const started = await client.startChatReply(userMessageId);
5012
+ assistantId = started.id;
5013
+ } catch (err) {
5014
+ const errMsg = err instanceof Error ? err.message : String(err);
5015
+ api.logger.error(
5016
+ `Chat ${userMessageId} failed to start streaming reply: ${errMsg}`
5017
+ );
5018
+ try {
5019
+ await client.postChatReply(userMessageId, "(streaming start failed)", {
5020
+ status: "errored",
5021
+ errorCode: "stream_start_failed",
5022
+ errorMessage: errMsg
5023
+ });
5024
+ } catch (postErr) {
5025
+ api.logger.error(
5026
+ `Chat ${userMessageId} fallback errored post failed: ${postErr}`
5027
+ );
5028
+ }
5029
+ return;
5030
+ }
5031
+ let ackedText = "";
5032
+ const sendDelta = async (full) => {
5033
+ if (!full || full.length <= ackedText.length) return;
5034
+ if (!full.startsWith(ackedText)) return;
5035
+ const diff = full.slice(ackedText.length);
5036
+ if (!diff) return;
5037
+ try {
5038
+ await client.postChatReplyDelta(assistantId, diff);
5039
+ ackedText = full;
5040
+ } catch (err) {
5041
+ api.logger.warn(`Chat ${userMessageId} delta post failed: ${err}`);
5042
+ }
5043
+ };
5044
+ const finalizeErrored = async (content, errorCode, errorMessage) => {
5045
+ try {
5046
+ await client.completeChatReply(assistantId, {
5047
+ status: "errored",
5048
+ content: content || void 0,
5049
+ errorCode,
5050
+ errorMessage
5051
+ });
5052
+ } catch (postErr) {
5053
+ api.logger.error(
5054
+ `Chat ${userMessageId} finalize errored failed: ${postErr}`
5055
+ );
5056
+ }
5057
+ };
5058
+ let pollDone = false;
5059
+ const pollInterval = 400;
5060
+ const pollLoop = (async () => {
5061
+ while (!pollDone) {
5062
+ await new Promise((r) => setTimeout(r, pollInterval));
5063
+ if (pollDone) break;
5064
+ try {
5065
+ const partial = await extractReplyForChatMessage(
5066
+ sessionKey,
5067
+ userMessageId,
5068
+ 1
5069
+ );
5070
+ if (partial) await sendDelta(partial);
5071
+ } catch (err) {
5072
+ api.logger.warn(`Chat ${userMessageId} poll failed: ${err}`);
5073
+ }
5074
+ }
5075
+ })();
5076
+ try {
5077
+ const result = await api.runtime.subagent.run({
5078
+ sessionKey,
5079
+ message: decoratedPrompt,
5080
+ deliver: true,
5081
+ idempotencyKey: `sw-chat-${userMessageId}`
5082
+ });
5083
+ const waitResult = await api.runtime.subagent.waitForRun({
5084
+ runId: result.runId,
5085
+ timeoutMs: 12e4
5086
+ });
5087
+ pollDone = true;
5088
+ await pollLoop;
5089
+ const finalReply = await extractReplyForChatMessage(
5090
+ sessionKey,
5091
+ userMessageId
5092
+ );
5093
+ if (waitResult.status !== "ok") {
5094
+ api.logger.error(
5095
+ `Agent run failed for chat ${userMessageId}: ${waitResult.error}`
5096
+ );
5097
+ await finalizeErrored(
5098
+ finalReply ?? ackedText,
5099
+ "agent_error",
5100
+ waitResult.error ?? "Agent run failed"
5101
+ );
5102
+ return;
5103
+ }
5104
+ if (!finalReply) {
5105
+ api.logger.warn(
5106
+ `Chat message ${userMessageId} completed but no reply found`
5107
+ );
5108
+ await finalizeErrored(
5109
+ ackedText,
5110
+ "no_reply",
5111
+ "Agent run produced no reply"
5112
+ );
5113
+ return;
5114
+ }
5115
+ try {
5116
+ await client.completeChatReply(assistantId, { content: finalReply });
5117
+ api.logger.info(`Chat message ${userMessageId} streamed`);
5118
+ } catch (completeErr) {
5119
+ const errMsg = completeErr instanceof Error ? completeErr.message : String(completeErr);
5120
+ api.logger.error(
5121
+ `Chat ${userMessageId} complete failed: ${errMsg}`
5122
+ );
5123
+ await finalizeErrored(finalReply, "complete_failed", errMsg);
5124
+ }
5125
+ } catch (err) {
5126
+ pollDone = true;
5127
+ try {
5128
+ await pollLoop;
5129
+ } catch {
5130
+ }
5131
+ const errMsg = err instanceof Error ? err.message : String(err);
5132
+ api.logger.error(`Failed to stream chat ${userMessageId}: ${errMsg}`);
5133
+ await finalizeErrored(ackedText, "execution_error", errMsg);
5134
+ }
5135
+ }
4749
5136
  async function handleReasoningJob(job, client) {
4750
5137
  const hydrated = await hydrateReasoningJobEnvelope(
4751
5138
  job,
@@ -4914,6 +5301,16 @@ function createRelayService(api) {
4914
5301
  handleReasoningJob(envelope, client).catch(
4915
5302
  (err) => api.logger.error(`Error handling reasoning job: ${err}`)
4916
5303
  );
5304
+ } else if (envelope.event_type === "chat_message_dispatched") {
5305
+ const chatEnv = envelope;
5306
+ enqueueChat(
5307
+ chatEnv.session_id,
5308
+ () => handleChatMessage(chatEnv, client)
5309
+ );
5310
+ } else if (envelope.event_type !== void 0 && envelope.id === void 0) {
5311
+ api.logger.warn(
5312
+ `Ignoring unknown relay envelope: ${JSON.stringify(envelope.event_type)}`
5313
+ );
4917
5314
  } else {
4918
5315
  const task = envelope;
4919
5316
  await handleTask(task, client);
@@ -2,7 +2,7 @@
2
2
  "id": "stagewhisper",
3
3
  "name": "StageWhisper",
4
4
  "description": "Turn live call moments into assistant tasks via StageWhisper",
5
- "version": "0.57.0",
5
+ "version": "0.60.0",
6
6
  "channels": [
7
7
  "stagewhisper"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stagewhisper/stagewhisper",
3
- "version": "0.57.0",
3
+ "version": "0.60.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin that connects StageWhisper live calls to your AI assistant",
6
6
  "license": "MIT",