@stagewhisper/stagewhisper 0.55.0 → 0.59.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,27 @@ 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
+ }
101
122
  async heartbeat(capabilities) {
102
123
  const res = await fetch(
103
124
  `${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/heartbeat`,
@@ -4166,6 +4187,24 @@ function createRelayService(api) {
4166
4187
  const COMPLETED_JOB_TTL_MS = 5 * 60 * 1e3;
4167
4188
  const COMPLETED_JOB_MAX_SIZE = 5e3;
4168
4189
  const processingReasoningJobs = /* @__PURE__ */ new Set();
4190
+ const chatSessionQueues = /* @__PURE__ */ new Map();
4191
+ function enqueueChat(sessionId, work) {
4192
+ const prev = chatSessionQueues.get(sessionId) ?? Promise.resolve();
4193
+ const next = prev.catch(() => {
4194
+ }).then(async () => {
4195
+ try {
4196
+ await work();
4197
+ } catch (err) {
4198
+ api.logger.error(`Chat queue work failed: ${err}`);
4199
+ }
4200
+ });
4201
+ chatSessionQueues.set(sessionId, next);
4202
+ void next.then(() => {
4203
+ if (chatSessionQueues.get(sessionId) === next) {
4204
+ chatSessionQueues.delete(sessionId);
4205
+ }
4206
+ });
4207
+ }
4169
4208
  function evictStaleCompletedJobs() {
4170
4209
  const cutoff = Date.now() - COMPLETED_JOB_TTL_MS;
4171
4210
  for (const [jobId, completedAt] of completedReasoningJobs) {
@@ -4322,6 +4361,34 @@ function createRelayService(api) {
4322
4361
  }
4323
4362
  return null;
4324
4363
  }
4364
+ async function extractReplyForChatMessage(sessionKey, userMessageId, maxAttempts = 3) {
4365
+ const marker = `[StageWhisper chat: ${userMessageId}]`;
4366
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4367
+ if (attempt > 0) {
4368
+ await new Promise((r) => setTimeout(r, 1500));
4369
+ }
4370
+ const session = await api.runtime.subagent.getSessionMessages({
4371
+ sessionKey,
4372
+ limit: 100
4373
+ });
4374
+ const messages = session.messages;
4375
+ for (let i = 0; i < messages.length; i++) {
4376
+ const msg = messages[i];
4377
+ if (msg["role"] !== "user") continue;
4378
+ const text = extractContentFromMessage(msg) ?? "";
4379
+ if (!text.includes(marker)) continue;
4380
+ for (let j = i + 1; j < messages.length; j++) {
4381
+ const next = messages[j];
4382
+ const role = next["role"];
4383
+ if (role === "assistant" || role === "model") {
4384
+ return extractContentFromMessage(next);
4385
+ }
4386
+ if (role === "user") break;
4387
+ }
4388
+ }
4389
+ }
4390
+ return null;
4391
+ }
4325
4392
  function decryptTaskFields(task, client) {
4326
4393
  const keypair = client.pluginKeypair;
4327
4394
  const desktopPub = client.desktopPublicKey;
@@ -4746,6 +4813,141 @@ function createRelayService(api) {
4746
4813
  api.logger.error(`Failed to post BYO reasoning result for ${job.job_id}: ${postErr}`);
4747
4814
  }
4748
4815
  }
4816
+ function looksLikeBYOEnvelope(s) {
4817
+ const trimmed = s.trimStart();
4818
+ if (!trimmed.startsWith("{")) return false;
4819
+ return trimmed.includes('"version"') && trimmed.includes('"sender_role"') && trimmed.includes('"ciphertext"');
4820
+ }
4821
+ function decryptChatPrompt(rawContent, client) {
4822
+ const keypair = client.pluginKeypair;
4823
+ const desktopPub = client.desktopPublicKey;
4824
+ if (!keypair || !desktopPub) {
4825
+ return { error: "missing_byo_keys" };
4826
+ }
4827
+ try {
4828
+ const envelope = JSON.parse(rawContent);
4829
+ const envelopeKey = keypair.deriveEnvelopeKey(desktopPub);
4830
+ const plaintextBytes = open(envelopeKey, envelope);
4831
+ return {
4832
+ plaintext: new TextDecoder().decode(plaintextBytes),
4833
+ envelopeKey
4834
+ };
4835
+ } catch (err) {
4836
+ return { error: `decryption_error: ${err}` };
4837
+ }
4838
+ }
4839
+ function sealChatReply(envelopeKey, sessionId, userMessageId, replyText) {
4840
+ const envelope = seal(
4841
+ envelopeKey,
4842
+ "plugin",
4843
+ sessionId,
4844
+ `chat-reply:${userMessageId}`,
4845
+ "task_content",
4846
+ new TextEncoder().encode(replyText)
4847
+ );
4848
+ return JSON.stringify(envelope);
4849
+ }
4850
+ async function handleChatMessage(envelope, client) {
4851
+ const userMessageId = envelope.user_message_id;
4852
+ const sessionId = envelope.session_id;
4853
+ const rawContent = envelope.content;
4854
+ if (!userMessageId || !sessionId || !rawContent) {
4855
+ api.logger.warn(
4856
+ `Skipping chat message dispatch \u2014 missing fields (user_message_id=${userMessageId}, session_id=${sessionId})`
4857
+ );
4858
+ return;
4859
+ }
4860
+ api.logger.info(
4861
+ `Received chat message: ${userMessageId} (session: ${sessionId})`
4862
+ );
4863
+ let plaintextContent = rawContent;
4864
+ let envelopeKey = null;
4865
+ if (looksLikeBYOEnvelope(rawContent)) {
4866
+ const decryptResult = decryptChatPrompt(rawContent, client);
4867
+ if ("error" in decryptResult) {
4868
+ api.logger.error(
4869
+ `Chat ${userMessageId} BYO decrypt failed: ${decryptResult.error}`
4870
+ );
4871
+ try {
4872
+ await client.postChatReply(
4873
+ userMessageId,
4874
+ "(BYO decrypt failed)",
4875
+ {
4876
+ status: "errored",
4877
+ errorCode: decryptResult.error.startsWith("missing_") ? "missing_byo_keys" : "decryption_error",
4878
+ errorMessage: decryptResult.error
4879
+ }
4880
+ );
4881
+ } catch (postErr) {
4882
+ api.logger.error(`Failed to report BYO error: ${postErr}`);
4883
+ }
4884
+ return;
4885
+ }
4886
+ plaintextContent = decryptResult.plaintext;
4887
+ envelopeKey = decryptResult.envelopeKey;
4888
+ }
4889
+ const sessionKey = buildAgentSessionKey({
4890
+ agentId: "default",
4891
+ channel: "stagewhisper",
4892
+ peer: { kind: "direct", id: `sw-chat-${sessionId}` }
4893
+ });
4894
+ const finalizeReply = async (replyText, options) => {
4895
+ const outbound = envelopeKey ? sealChatReply(envelopeKey, sessionId, userMessageId, replyText) : replyText;
4896
+ await client.postChatReply(userMessageId, outbound, options);
4897
+ };
4898
+ const decoratedPrompt = `${plaintextContent}
4899
+
4900
+ [StageWhisper chat: ${userMessageId}]`;
4901
+ try {
4902
+ const result = await api.runtime.subagent.run({
4903
+ sessionKey,
4904
+ message: decoratedPrompt,
4905
+ deliver: true,
4906
+ idempotencyKey: `sw-chat-${userMessageId}`
4907
+ });
4908
+ const waitResult = await api.runtime.subagent.waitForRun({
4909
+ runId: result.runId,
4910
+ timeoutMs: 12e4
4911
+ });
4912
+ if (waitResult.status === "ok") {
4913
+ const reply = await extractReplyForChatMessage(sessionKey, userMessageId);
4914
+ if (reply) {
4915
+ await finalizeReply(reply);
4916
+ api.logger.info(`Chat message ${userMessageId} replied`);
4917
+ } else {
4918
+ api.logger.warn(
4919
+ `Chat message ${userMessageId} completed but no reply found`
4920
+ );
4921
+ await finalizeReply("(no reply produced)", {
4922
+ status: "errored",
4923
+ errorCode: "no_reply",
4924
+ errorMessage: "Agent run produced no reply"
4925
+ });
4926
+ }
4927
+ } else {
4928
+ api.logger.error(
4929
+ `Agent run failed for chat message ${userMessageId}: ${waitResult.error}`
4930
+ );
4931
+ await finalizeReply("(agent run failed)", {
4932
+ status: "errored",
4933
+ errorCode: "agent_error",
4934
+ errorMessage: waitResult.error ?? "Agent run failed"
4935
+ });
4936
+ }
4937
+ } catch (err) {
4938
+ const errMsg = err instanceof Error ? err.message : String(err);
4939
+ api.logger.error(`Failed to process chat message ${userMessageId}: ${errMsg}`);
4940
+ try {
4941
+ await finalizeReply("(execution error)", {
4942
+ status: "errored",
4943
+ errorCode: "execution_error",
4944
+ errorMessage: errMsg
4945
+ });
4946
+ } catch (postErr) {
4947
+ api.logger.error(`Failed to report chat failure: ${postErr}`);
4948
+ }
4949
+ }
4950
+ }
4749
4951
  async function handleReasoningJob(job, client) {
4750
4952
  const hydrated = await hydrateReasoningJobEnvelope(
4751
4953
  job,
@@ -4914,6 +5116,16 @@ function createRelayService(api) {
4914
5116
  handleReasoningJob(envelope, client).catch(
4915
5117
  (err) => api.logger.error(`Error handling reasoning job: ${err}`)
4916
5118
  );
5119
+ } else if (envelope.event_type === "chat_message_dispatched") {
5120
+ const chatEnv = envelope;
5121
+ enqueueChat(
5122
+ chatEnv.session_id,
5123
+ () => handleChatMessage(chatEnv, client)
5124
+ );
5125
+ } else if (envelope.event_type !== void 0 && envelope.id === void 0) {
5126
+ api.logger.warn(
5127
+ `Ignoring unknown relay envelope: ${JSON.stringify(envelope.event_type)}`
5128
+ );
4917
5129
  } else {
4918
5130
  const task = envelope;
4919
5131
  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.55.0",
5
+ "version": "0.59.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.55.0",
3
+ "version": "0.59.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin that connects StageWhisper live calls to your AI assistant",
6
6
  "license": "MIT",