@stagewhisper/stagewhisper 0.36.0 → 0.38.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.36.0",
5
+ "version": "0.38.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.36.0",
3
+ "version": "0.38.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
@@ -147,7 +147,19 @@ export const stagewhisperPlugin = createChatChannelPlugin<StageWhisperAccount>(
147
147
  threading: { topLevelReplyToMode: "reply" },
148
148
 
149
149
  outbound: {
150
- base: { deliveryMode: "direct" },
150
+ base: {
151
+ deliveryMode: "direct",
152
+ resolveTarget: (params: {
153
+ cfg?: OpenClawConfig;
154
+ to?: string;
155
+ allowFrom?: string[];
156
+ accountId?: string | null;
157
+ mode?: string;
158
+ }) => {
159
+ if (!params.to) return { ok: false as const, error: new Error("No delivery target") };
160
+ return { ok: true as const, to: params.to };
161
+ },
162
+ },
151
163
  attachedResults: {
152
164
  channel: "stagewhisper",
153
165
  sendText: async (ctx) => {
@@ -158,7 +170,10 @@ export const stagewhisperPlugin = createChatChannelPlugin<StageWhisperAccount>(
158
170
  account.relayToken,
159
171
  );
160
172
 
161
- const taskId = ctx.threadId as string | undefined;
173
+ const target = (ctx as Record<string, unknown>).to as string | undefined;
174
+ const threadId = ctx.threadId as string | undefined;
175
+ const raw = target ?? threadId ?? "";
176
+ const taskId = raw.replace(/^sw-task-/, "");
162
177
  if (!taskId) {
163
178
  return { messageId: `sw-noop-${Date.now()}`, ok: true };
164
179
  }
package/src/client.ts CHANGED
@@ -118,4 +118,19 @@ export class StageWhisperClient {
118
118
  streamHeaders(): Record<string, string> {
119
119
  return { Authorization: `Bearer ${this.relayToken}` };
120
120
  }
121
+
122
+ async testReply(testId: string, content: string): Promise<void> {
123
+ const res = await fetch(
124
+ `${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/test-reply`,
125
+ {
126
+ method: "POST",
127
+ headers: this.headers(),
128
+ body: JSON.stringify({ test_id: testId, content }),
129
+ },
130
+ );
131
+ if (!res.ok) {
132
+ const text = await res.text();
133
+ throw new Error(`Test reply failed (${res.status}): ${text}`);
134
+ }
135
+ }
121
136
  }
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(
@@ -96,19 +96,50 @@ export function createRelayService(api: OpenClawPluginApi) {
96
96
  for (let i = messages.length - 1; i >= 0; i--) {
97
97
  const msg = messages[i] as Record<string, unknown> | undefined;
98
98
  if (!msg) continue;
99
- if (msg["role"] === "assistant" && typeof msg["content"] === "string") {
100
- return msg["content"];
99
+ const role = msg["role"];
100
+ if (role !== "assistant" && role !== "model") continue;
101
+
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;
114
+ }
115
+ }
101
116
  }
102
117
  }
103
118
  return null;
104
119
  }
105
120
 
106
- async function handleTask(
121
+ async function extractReplyWithRetry(
122
+ sessionKey: string,
123
+ maxAttempts: number = 3,
124
+ ): Promise<string | null> {
125
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
126
+ if (attempt > 0) {
127
+ await new Promise((r) => setTimeout(r, 1500));
128
+ }
129
+ const session = await api.runtime.subagent.getSessionMessages({
130
+ sessionKey,
131
+ limit: 20,
132
+ });
133
+ const reply = extractAssistantReply(session.messages);
134
+ if (reply) return reply;
135
+ }
136
+ return null;
137
+ }
138
+
139
+ async function handleNormalTask(
107
140
  task: TaskPayload,
108
141
  client: StageWhisperClient,
109
142
  ): Promise<void> {
110
- api.logger.info(`Received task: ${task.title} (${task.id})`);
111
-
112
143
  try {
113
144
  await updateStatus(client, task, "delivered");
114
145
  } catch (err) {
@@ -116,57 +147,101 @@ export function createRelayService(api: OpenClawPluginApi) {
116
147
  }
117
148
 
118
149
  const messageContent = buildTaskMessage(task);
150
+ const sessionKey = buildAgentSessionKey({
151
+ agentId: "default",
152
+ channel: "stagewhisper",
153
+ peer: { kind: "direct", id: `sw-task-${task.id}` },
154
+ });
155
+
156
+ const result = await api.runtime.subagent.run({
157
+ sessionKey,
158
+ message: messageContent,
159
+ deliver: true,
160
+ idempotencyKey: randomUUID(),
161
+ });
162
+
163
+ api.logger.info(
164
+ `Task ${task.id} dispatched to agent session (runId: ${result.runId})`,
165
+ );
119
166
 
120
167
  try {
121
- const sessionKey = buildAgentSessionKey({
122
- agentId: "default",
123
- channel: "stagewhisper",
124
- peer: { kind: "direct", id: `sw-task-${task.id}` },
125
- });
168
+ await updateStatus(client, task, "running");
169
+ } catch (err) {
170
+ api.logger.warn(`Failed to mark task as running: ${err}`);
171
+ }
126
172
 
127
- const result = await api.runtime.subagent.run({
128
- sessionKey,
129
- message: messageContent,
130
- deliver: false,
131
- idempotencyKey: randomUUID(),
173
+ api.runtime.subagent
174
+ .waitForRun({ runId: result.runId, timeoutMs: 120_000 })
175
+ .then(async (waitResult) => {
176
+ if (waitResult.status === "ok") {
177
+ await updateStatus(client, task, "completed").catch(() => {});
178
+ api.logger.info(`Task ${task.id} completed`);
179
+ } else {
180
+ api.logger.error(
181
+ `Agent run failed for task ${task.id}: ${waitResult.error}`,
182
+ );
183
+ await updateStatus(client, task, "failed").catch(() => {});
184
+ }
185
+ })
186
+ .catch((err) => {
187
+ api.logger.error(`Failed to track task ${task.id}: ${err}`);
132
188
  });
189
+ }
133
190
 
134
- api.logger.info(
135
- `Task ${task.id} dispatched to agent session (runId: ${result.runId})`,
136
- );
191
+ async function handleTestTask(
192
+ task: TaskPayload,
193
+ client: StageWhisperClient,
194
+ ): Promise<void> {
195
+ const messageContent = buildTaskMessage(task);
196
+ const sessionKey = buildAgentSessionKey({
197
+ agentId: "default",
198
+ channel: "stagewhisper",
199
+ peer: { kind: "direct", id: `sw-test-${task.id}` },
200
+ });
137
201
 
138
- try {
139
- await updateStatus(client, task, "running");
140
- } catch (err) {
141
- api.logger.warn(`Failed to mark task as running: ${err}`);
142
- }
202
+ const result = await api.runtime.subagent.run({
203
+ sessionKey,
204
+ message: messageContent,
205
+ deliver: false,
206
+ idempotencyKey: randomUUID(),
207
+ });
143
208
 
144
- const waitResult = await api.runtime.subagent.waitForRun({
145
- runId: result.runId,
146
- timeoutMs: 120_000,
147
- });
209
+ api.logger.info(
210
+ `Test task ${task.id} dispatched (runId: ${result.runId})`,
211
+ );
148
212
 
149
- if (waitResult.status === "ok") {
150
- const session = await api.runtime.subagent.getSessionMessages({
151
- sessionKey,
152
- limit: 20,
153
- });
213
+ const waitResult = await api.runtime.subagent.waitForRun({
214
+ runId: result.runId,
215
+ timeoutMs: 120_000,
216
+ });
154
217
 
155
- const reply = extractAssistantReply(session.messages);
156
- if (reply) {
157
- if (!isTestTask(task)) {
158
- await client.postReply(task.id, reply);
159
- }
160
- api.logger.info(`Task ${task.id} completed with reply`);
161
- } else {
162
- api.logger.warn(`Task ${task.id} completed but no assistant reply found`);
163
- await updateStatus(client, task, "completed").catch(() => {});
164
- }
218
+ if (waitResult.status === "ok") {
219
+ const reply = await extractReplyWithRetry(sessionKey);
220
+ if (reply) {
221
+ await client.testReply(task.id, reply);
222
+ api.logger.info(`Test task ${task.id} completed with reply`);
165
223
  } else {
166
- api.logger.error(
167
- `Agent run failed for task ${task.id}: ${waitResult.error}`,
168
- );
169
- await updateStatus(client, task, "failed").catch(() => {});
224
+ api.logger.warn(`Test task ${task.id} completed but no reply found`);
225
+ await client.testReply(task.id, "(no reply extracted)");
226
+ }
227
+ } else {
228
+ api.logger.error(
229
+ `Agent run failed for test task ${task.id}: ${waitResult.error}`,
230
+ );
231
+ }
232
+ }
233
+
234
+ async function handleTask(
235
+ task: TaskPayload,
236
+ client: StageWhisperClient,
237
+ ): Promise<void> {
238
+ api.logger.info(`Received task: ${task.title} (${task.id})`);
239
+
240
+ try {
241
+ if (isTestTask(task)) {
242
+ await handleTestTask(task, client);
243
+ } else {
244
+ await handleNormalTask(task, client);
170
245
  }
171
246
  } catch (err) {
172
247
  api.logger.error(`Failed to process task ${task.id}: ${err}`);
@@ -222,11 +297,9 @@ export function createRelayService(api: OpenClawPluginApi) {
222
297
 
223
298
  try {
224
299
  const task = JSON.parse(jsonStr) as TaskPayload;
225
- handleTask(task, client).catch((err) => {
226
- api.logger.error(`Unhandled error processing task: ${err}`);
227
- });
228
- } catch (parseErr) {
229
- api.logger.warn(`Failed to parse stream event: ${parseErr}`);
300
+ await handleTask(task, client);
301
+ } catch (err) {
302
+ api.logger.error(`Error processing task from stream: ${err}`);
230
303
  }
231
304
  }
232
305
  }