@stagewhisper/stagewhisper 0.37.0 → 0.39.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.
@@ -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.37.0",
5
+ "version": "0.39.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.37.0",
3
+ "version": "0.39.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin that connects StageWhisper live calls to your AI assistant",
6
6
  "license": "MIT",
package/src/channel.ts CHANGED
@@ -4,7 +4,6 @@ import {
4
4
  DEFAULT_ACCOUNT_ID,
5
5
  } from "openclaw/plugin-sdk/core";
6
6
  import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
7
- import { StageWhisperClient } from "./client.js";
8
7
 
9
8
  export type StageWhisperAccount = {
10
9
  accountId: string | null;
@@ -147,24 +146,31 @@ export const stagewhisperPlugin = createChatChannelPlugin<StageWhisperAccount>(
147
146
  threading: { topLevelReplyToMode: "reply" },
148
147
 
149
148
  outbound: {
150
- base: { deliveryMode: "direct" },
149
+ base: {
150
+ deliveryMode: "direct",
151
+ resolveTarget: (params: {
152
+ cfg?: OpenClawConfig;
153
+ to?: string;
154
+ allowFrom?: string[];
155
+ accountId?: string | null;
156
+ mode?: string;
157
+ }) => {
158
+ if (!params.to) return { ok: false as const, error: new Error("No delivery target") };
159
+ return { ok: true as const, to: params.to };
160
+ },
161
+ },
151
162
  attachedResults: {
152
163
  channel: "stagewhisper",
153
164
  sendText: async (ctx) => {
154
- const account = resolveAccount(ctx.cfg, ctx.accountId);
155
- const client = new StageWhisperClient(
156
- account.apiBaseUrl,
157
- account.integrationId,
158
- account.relayToken,
159
- );
160
-
161
- const taskId = ctx.threadId as string | undefined;
162
- if (!taskId) {
163
- return { messageId: `sw-noop-${Date.now()}`, ok: true };
165
+ const target = (ctx as Record<string, unknown>).to as string | undefined ?? "";
166
+ if (target.startsWith("sw-session-")) {
167
+ return { messageId: `sw-relay-ack-${Date.now()}`, ok: true };
164
168
  }
165
-
166
- await client.postReply(taskId, ctx.text);
167
- return { messageId: `sw-reply-${taskId}-${Date.now()}`, ok: true };
169
+ console.warn(
170
+ `[stagewhisper] sendText called for unrecognised target "${target}"; ` +
171
+ `StageWhisper channel is inbound-only task replies are routed by the relay service`,
172
+ );
173
+ return { messageId: `sw-dropped-${Date.now()}`, ok: false };
168
174
  },
169
175
  },
170
176
  },
package/src/service.ts CHANGED
@@ -78,7 +78,7 @@ export function createRelayService(api: OpenClawPluginApi) {
78
78
  }
79
79
 
80
80
  function isTestTask(task: TaskPayload): boolean {
81
- return task.action_type === "test";
81
+ return task.action_type === "connectivity_test";
82
82
  }
83
83
 
84
84
  async function updateStatus(
@@ -90,6 +90,27 @@ export function createRelayService(api: OpenClawPluginApi) {
90
90
  await client.updateTaskStatus(task.id, status);
91
91
  }
92
92
 
93
+ function extractContentFromMessage(
94
+ msg: Record<string, unknown>,
95
+ ): string | null {
96
+ const content = msg["content"];
97
+ if (typeof content === "string") return content;
98
+
99
+ if (Array.isArray(content)) {
100
+ for (const part of content) {
101
+ if (
102
+ typeof part === "object" &&
103
+ part !== null &&
104
+ (part as Record<string, unknown>)["type"] === "text" &&
105
+ typeof (part as Record<string, unknown>)["text"] === "string"
106
+ ) {
107
+ return (part as Record<string, unknown>)["text"] as string;
108
+ }
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+
93
114
  function extractAssistantReply(
94
115
  messages: unknown[],
95
116
  ): string | null {
@@ -98,20 +119,39 @@ export function createRelayService(api: OpenClawPluginApi) {
98
119
  if (!msg) continue;
99
120
  const role = msg["role"];
100
121
  if (role !== "assistant" && role !== "model") continue;
122
+ return extractContentFromMessage(msg);
123
+ }
124
+ return null;
125
+ }
101
126
 
102
- const content = msg["content"];
103
- if (typeof content === "string") return content;
104
-
105
- if (Array.isArray(content)) {
106
- for (const part of content) {
107
- if (
108
- typeof part === "object" &&
109
- part !== null &&
110
- (part as Record<string, unknown>)["type"] === "text" &&
111
- typeof (part as Record<string, unknown>)["text"] === "string"
112
- ) {
113
- return (part as Record<string, unknown>)["text"] as string;
127
+ async function extractReplyForTask(
128
+ sessionKey: string,
129
+ taskId: string,
130
+ maxAttempts: number = 3,
131
+ ): Promise<string | null> {
132
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
133
+ if (attempt > 0) {
134
+ await new Promise((r) => setTimeout(r, 1500));
135
+ }
136
+ const session = await api.runtime.subagent.getSessionMessages({
137
+ sessionKey,
138
+ limit: 50,
139
+ });
140
+ const messages = session.messages as Record<string, unknown>[];
141
+
142
+ for (let i = 0; i < messages.length; i++) {
143
+ const msg = messages[i];
144
+ if (msg["role"] !== "user") continue;
145
+ const text = extractContentFromMessage(msg) ?? "";
146
+ if (!text.includes(`StageWhisper task: ${taskId}`)) continue;
147
+
148
+ for (let j = i + 1; j < messages.length; j++) {
149
+ const reply = messages[j];
150
+ const role = reply["role"];
151
+ if (role === "assistant" || role === "model") {
152
+ return extractContentFromMessage(reply);
114
153
  }
154
+ if (role === "user") break;
115
155
  }
116
156
  }
117
157
  }
@@ -136,71 +176,121 @@ export function createRelayService(api: OpenClawPluginApi) {
136
176
  return null;
137
177
  }
138
178
 
139
- async function handleTask(
179
+ async function handleNormalTask(
140
180
  task: TaskPayload,
141
181
  client: StageWhisperClient,
142
182
  ): Promise<void> {
143
- api.logger.info(`Received task: ${task.title} (${task.id})`);
144
-
145
183
  try {
146
184
  await updateStatus(client, task, "delivered");
147
185
  } catch (err) {
148
- api.logger.warn(`Failed to mark task as delivered: ${err}`);
186
+ api.logger.warn(`Failed to mark task as delivered, skipping to prevent duplicates: ${err}`);
187
+ return;
149
188
  }
150
189
 
151
190
  const messageContent = buildTaskMessage(task);
191
+ const peerId = `sw-session-${task.session_id}`;
192
+ const sessionKey = buildAgentSessionKey({
193
+ agentId: "default",
194
+ channel: "stagewhisper",
195
+ peer: { kind: "direct", id: peerId },
196
+ });
197
+
198
+ const result = await api.runtime.subagent.run({
199
+ sessionKey,
200
+ message: messageContent,
201
+ deliver: true,
202
+ idempotencyKey: randomUUID(),
203
+ });
204
+
205
+ api.logger.info(
206
+ `Task ${task.id} dispatched to agent session (runId: ${result.runId})`,
207
+ );
152
208
 
153
209
  try {
154
- const sessionKey = buildAgentSessionKey({
155
- agentId: "default",
156
- channel: "stagewhisper",
157
- peer: { kind: "direct", id: `sw-task-${task.id}` },
158
- });
210
+ await updateStatus(client, task, "running");
211
+ } catch (err) {
212
+ api.logger.warn(`Failed to mark task as running: ${err}`);
213
+ }
159
214
 
160
- const result = await api.runtime.subagent.run({
161
- sessionKey,
162
- message: messageContent,
163
- deliver: false,
164
- idempotencyKey: randomUUID(),
215
+ api.runtime.subagent
216
+ .waitForRun({ runId: result.runId, timeoutMs: 120_000 })
217
+ .then(async (waitResult) => {
218
+ if (waitResult.status === "ok") {
219
+ const reply = await extractReplyForTask(sessionKey, task.id);
220
+ if (reply) {
221
+ await client.postReply(task.id, reply);
222
+ api.logger.info(`Task ${task.id} completed with reply`);
223
+ } else {
224
+ api.logger.warn(`Task ${task.id} completed but no reply found`);
225
+ }
226
+ await updateStatus(client, task, "completed").catch(() => {});
227
+ } else {
228
+ api.logger.error(
229
+ `Agent run failed for task ${task.id}: ${waitResult.error}`,
230
+ );
231
+ await updateStatus(client, task, "failed").catch(() => {});
232
+ }
233
+ })
234
+ .catch(async (err) => {
235
+ api.logger.error(`Failed to track task ${task.id}: ${err}`);
236
+ await updateStatus(client, task, "failed").catch(() => {});
165
237
  });
238
+ }
166
239
 
167
- api.logger.info(
168
- `Task ${task.id} dispatched to agent session (runId: ${result.runId})`,
169
- );
240
+ async function handleTestTask(
241
+ task: TaskPayload,
242
+ client: StageWhisperClient,
243
+ ): Promise<void> {
244
+ const messageContent = buildTaskMessage(task);
245
+ const sessionKey = buildAgentSessionKey({
246
+ agentId: "default",
247
+ channel: "stagewhisper",
248
+ peer: { kind: "direct", id: `sw-test-${task.id}` },
249
+ });
170
250
 
171
- try {
172
- await updateStatus(client, task, "running");
173
- } catch (err) {
174
- api.logger.warn(`Failed to mark task as running: ${err}`);
251
+ const result = await api.runtime.subagent.run({
252
+ sessionKey,
253
+ message: messageContent,
254
+ deliver: false,
255
+ idempotencyKey: randomUUID(),
256
+ });
257
+
258
+ api.logger.info(
259
+ `Test task ${task.id} dispatched (runId: ${result.runId})`,
260
+ );
261
+
262
+ const waitResult = await api.runtime.subagent.waitForRun({
263
+ runId: result.runId,
264
+ timeoutMs: 120_000,
265
+ });
266
+
267
+ if (waitResult.status === "ok") {
268
+ const reply = await extractReplyWithRetry(sessionKey);
269
+ if (reply) {
270
+ await client.testReply(task.id, reply);
271
+ api.logger.info(`Test task ${task.id} completed with reply`);
272
+ } else {
273
+ api.logger.warn(`Test task ${task.id} completed but no reply found`);
274
+ await client.testReply(task.id, "(no reply extracted)");
175
275
  }
276
+ } else {
277
+ api.logger.error(
278
+ `Agent run failed for test task ${task.id}: ${waitResult.error}`,
279
+ );
280
+ }
281
+ }
176
282
 
177
- const waitResult = await api.runtime.subagent.waitForRun({
178
- runId: result.runId,
179
- timeoutMs: 120_000,
180
- });
283
+ async function handleTask(
284
+ task: TaskPayload,
285
+ client: StageWhisperClient,
286
+ ): Promise<void> {
287
+ api.logger.info(`Received task: ${task.title} (${task.id})`);
181
288
 
182
- if (waitResult.status === "ok") {
183
- const reply = await extractReplyWithRetry(sessionKey);
184
- if (reply) {
185
- if (isTestTask(task)) {
186
- await client.testReply(task.id, reply);
187
- } else {
188
- await client.postReply(task.id, reply);
189
- }
190
- api.logger.info(`Task ${task.id} completed with reply`);
191
- } else {
192
- api.logger.warn(`Task ${task.id} completed but no assistant reply found`);
193
- if (isTestTask(task)) {
194
- await client.testReply(task.id, "(no reply extracted)");
195
- } else {
196
- await updateStatus(client, task, "completed").catch(() => {});
197
- }
198
- }
289
+ try {
290
+ if (isTestTask(task)) {
291
+ await handleTestTask(task, client);
199
292
  } else {
200
- api.logger.error(
201
- `Agent run failed for task ${task.id}: ${waitResult.error}`,
202
- );
203
- await updateStatus(client, task, "failed").catch(() => {});
293
+ await handleNormalTask(task, client);
204
294
  }
205
295
  } catch (err) {
206
296
  api.logger.error(`Failed to process task ${task.id}: ${err}`);
@@ -256,11 +346,9 @@ export function createRelayService(api: OpenClawPluginApi) {
256
346
 
257
347
  try {
258
348
  const task = JSON.parse(jsonStr) as TaskPayload;
259
- handleTask(task, client).catch((err) => {
260
- api.logger.error(`Unhandled error processing task: ${err}`);
261
- });
262
- } catch (parseErr) {
263
- api.logger.warn(`Failed to parse stream event: ${parseErr}`);
349
+ await handleTask(task, client);
350
+ } catch (err) {
351
+ api.logger.error(`Error processing task from stream: ${err}`);
264
352
  }
265
353
  }
266
354
  }