@stagewhisper/stagewhisper 0.59.0 → 0.61.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
@@ -119,6 +119,54 @@ var init_client = __esm({
119
119
  throw new Error(`Chat reply failed (${res.status}): ${text}`);
120
120
  }
121
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
+ }
122
170
  async heartbeat(capabilities) {
123
171
  const res = await fetch(
124
172
  `${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/heartbeat`,
@@ -204,8 +252,8 @@ function resolveGatewayConfig(api) {
204
252
  function isResponsesEndpointEnabled(api) {
205
253
  const cfg = api.config;
206
254
  const gw = cfg?.gateway ?? {};
207
- const http = gw?.http ?? {};
208
- const endpoints = http?.endpoints ?? {};
255
+ const http2 = gw?.http ?? {};
256
+ const endpoints = http2?.endpoints ?? {};
209
257
  const responses = endpoints?.responses ?? {};
210
258
  return responses?.enabled === true;
211
259
  }
@@ -383,7 +431,7 @@ function createHasher(hashCons) {
383
431
  hashC.create = () => hashCons();
384
432
  return hashC;
385
433
  }
386
- function randomBytes(bytesLength = 32) {
434
+ function randomBytes2(bytesLength = 32) {
387
435
  if (crypto2 && typeof crypto2.getRandomValues === "function") {
388
436
  return crypto2.getRandomValues(new Uint8Array(bytesLength));
389
437
  }
@@ -1955,7 +2003,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
1955
2003
  });
1956
2004
  const { prehash } = eddsaOpts;
1957
2005
  const { BASE, Fp: Fp2, Fn: Fn2 } = Point;
1958
- const randomBytes3 = eddsaOpts.randomBytes || randomBytes;
2006
+ const randomBytes4 = eddsaOpts.randomBytes || randomBytes2;
1959
2007
  const adjustScalarBytes2 = eddsaOpts.adjustScalarBytes || ((bytes) => bytes);
1960
2008
  const domain = eddsaOpts.domain || ((data, ctx, phflag) => {
1961
2009
  _abool2(phflag, "phflag");
@@ -2037,7 +2085,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
2037
2085
  signature: 2 * _size,
2038
2086
  seed: _size
2039
2087
  };
2040
- function randomSecretKey(seed = randomBytes3(lengths.seed)) {
2088
+ function randomSecretKey(seed = randomBytes4(lengths.seed)) {
2041
2089
  return _abytes2(seed, lengths.seed, "seed");
2042
2090
  }
2043
2091
  function keygen(seed) {
@@ -2232,7 +2280,7 @@ function montgomery(curveDef) {
2232
2280
  const is25519 = type === "x25519";
2233
2281
  if (!is25519 && type !== "x448")
2234
2282
  throw new Error("invalid type");
2235
- const randomBytes_ = rand || randomBytes;
2283
+ const randomBytes_ = rand || randomBytes2;
2236
2284
  const montgomeryBits = is25519 ? 255 : 448;
2237
2285
  const fieldLen = is25519 ? 32 : 56;
2238
2286
  const Gu = is25519 ? BigInt(9) : BigInt(5);
@@ -3499,7 +3547,7 @@ var init_cryptoNode2 = __esm({
3499
3547
  });
3500
3548
 
3501
3549
  // node_modules/.pnpm/@noble+ciphers@1.3.0/node_modules/@noble/ciphers/esm/webcrypto.js
3502
- function randomBytes2(bytesLength = 32) {
3550
+ function randomBytes3(bytesLength = 32) {
3503
3551
  if (crypto3 && typeof crypto3.getRandomValues === "function") {
3504
3552
  return crypto3.getRandomValues(new Uint8Array(bytesLength));
3505
3553
  }
@@ -3543,7 +3591,7 @@ function generateUUID() {
3543
3591
  }
3544
3592
  function seal(key, senderRole, sessionId, correlationId, contentType, plaintext) {
3545
3593
  const messageId = generateUUID();
3546
- const nonce = randomBytes2(24);
3594
+ const nonce = randomBytes3(24);
3547
3595
  const metadata = {
3548
3596
  version: ENVELOPE_VERSION,
3549
3597
  sender_role: senderRole,
@@ -3618,7 +3666,7 @@ var init_crypto = __esm({
3618
3666
  this.publicKey = x25519.getPublicKey(secretKey);
3619
3667
  }
3620
3668
  static generate() {
3621
- const secretKey = randomBytes2(32);
3669
+ const secretKey = randomBytes3(32);
3622
3670
  return new _IdentityKeypair(secretKey);
3623
3671
  }
3624
3672
  static fromSecretBytes(bytes) {
@@ -3939,6 +3987,858 @@ var stagewhisperPlugin = {
3939
3987
  }
3940
3988
  };
3941
3989
 
3990
+ // src/http-transport.ts
3991
+ import http from "node:http";
3992
+ import { Buffer as Buffer2 } from "node:buffer";
3993
+ import { timingSafeEqual } from "node:crypto";
3994
+
3995
+ // src/core.ts
3996
+ var TASK_ID_REGEX = /^[0-9a-f-]{36}$/;
3997
+ var LOOPBACK_CALLBACK_URL_REGEX = /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?\/?$/;
3998
+ var ALLOWED_REASONS = /* @__PURE__ */ new Set([
3999
+ "transcript_chunk",
4000
+ "chat_message",
4001
+ "system_prelude"
4002
+ ]);
4003
+ var MAX_BODY_BYTES = 262144;
4004
+ var MAX_PAYLOAD_TEXT_CHARS = 8e3;
4005
+ var MAX_SESSION_ID_CHARS = 128;
4006
+ var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
4007
+ function isTestTask(task) {
4008
+ return task.action_type === "connectivity_test";
4009
+ }
4010
+ function buildTaskMessage(task) {
4011
+ const lines = [];
4012
+ lines.push(`**${task.title}**`);
4013
+ lines.push("");
4014
+ lines.push(task.request_text);
4015
+ if (task.evidence_payload) {
4016
+ const evidence = task.evidence_payload;
4017
+ if (evidence["transcript_excerpt"]) {
4018
+ lines.push("");
4019
+ lines.push(`Context: ${evidence["transcript_excerpt"]}`);
4020
+ }
4021
+ if (evidence["signal_summary"]) {
4022
+ lines.push(`Signal: ${evidence["signal_summary"]}`);
4023
+ }
4024
+ if (evidence["tone_summary"]) {
4025
+ lines.push(`Tone: ${evidence["tone_summary"]}`);
4026
+ }
4027
+ if (evidence["playbook_label"]) {
4028
+ lines.push(`Playbook: ${evidence["playbook_label"]}`);
4029
+ }
4030
+ }
4031
+ lines.push("");
4032
+ lines.push(`Action type: ${task.action_type}`);
4033
+ lines.push(`StageWhisper task: ${task.id}`);
4034
+ lines.push(`Session: ${task.session_id}`);
4035
+ return lines.join("\n");
4036
+ }
4037
+ async function dispatchTaskToAgent(options) {
4038
+ const { api, task } = options;
4039
+ const peerId = options.sessionPeerId ?? (isTestTask(task) ? `sw-test-${task.id}` : `sw-session-${task.session_id}`);
4040
+ const sessionKey = buildAgentSessionKey({
4041
+ agentId: "default",
4042
+ channel: "stagewhisper",
4043
+ peer: { kind: "direct", id: peerId },
4044
+ dmScope: "per-channel-peer"
4045
+ });
4046
+ const messageContent = buildTaskMessage(task);
4047
+ const result = await api.runtime.subagent.run({
4048
+ sessionKey,
4049
+ message: messageContent,
4050
+ deliver: options.deliver ?? !isTestTask(task),
4051
+ idempotencyKey: options.idempotencyKey ?? `sw-task-${task.id}`
4052
+ });
4053
+ return { runId: result.runId, sessionKey };
4054
+ }
4055
+ function isLoopbackCallbackUrl(url) {
4056
+ return LOOPBACK_CALLBACK_URL_REGEX.test(url);
4057
+ }
4058
+ function isAllowedHostHeader(host) {
4059
+ if (!host) return false;
4060
+ const trimmed = host.trim().toLowerCase();
4061
+ if (!trimmed) return false;
4062
+ const colonIdx = trimmed.lastIndexOf(":");
4063
+ const hostname = colonIdx === -1 ? trimmed : trimmed.slice(0, colonIdx);
4064
+ const port = colonIdx === -1 ? "" : trimmed.slice(colonIdx + 1);
4065
+ if (!ALLOWED_HOSTNAMES.has(hostname)) return false;
4066
+ if (port && !/^\d+$/.test(port)) return false;
4067
+ return true;
4068
+ }
4069
+ function buildChatId(sessionId, reason) {
4070
+ if (reason === "transcript_chunk") return `sw:${sessionId}:reasoning`;
4071
+ if (reason === "chat_message") return `sw:${sessionId}:chat`;
4072
+ return null;
4073
+ }
4074
+ function buildCallbackUrl(baseUrl, taskId) {
4075
+ return `${baseUrl.replace(/\/$/, "")}/tasks/${taskId}`;
4076
+ }
4077
+ function applyPrelude(text, prelude) {
4078
+ if (!prelude) return text;
4079
+ return `[Context: ${prelude}]
4080
+
4081
+ ${text}`;
4082
+ }
4083
+ function validateHttpTaskRequest(body) {
4084
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
4085
+ return { ok: false, error: "body must be a JSON object" };
4086
+ }
4087
+ const obj = body;
4088
+ const task_id = obj["task_id"];
4089
+ if (typeof task_id !== "string" || task_id.length === 0) {
4090
+ return { ok: false, error: "task_id must be a non-empty string" };
4091
+ }
4092
+ if (!TASK_ID_REGEX.test(task_id)) {
4093
+ return { ok: false, error: "task_id must match ^[0-9a-f-]{36}$" };
4094
+ }
4095
+ const session_id = obj["session_id"];
4096
+ if (typeof session_id !== "string" || session_id.length === 0) {
4097
+ return { ok: false, error: "session_id must be a non-empty string" };
4098
+ }
4099
+ if (session_id.length > MAX_SESSION_ID_CHARS) {
4100
+ return {
4101
+ ok: false,
4102
+ error: `session_id must be <= ${MAX_SESSION_ID_CHARS} chars`
4103
+ };
4104
+ }
4105
+ const reason = obj["reason"];
4106
+ if (typeof reason !== "string" || reason.length === 0) {
4107
+ return { ok: false, error: "reason must be a non-empty string" };
4108
+ }
4109
+ if (!ALLOWED_REASONS.has(reason)) {
4110
+ return {
4111
+ ok: false,
4112
+ error: `reason must be one of ${Array.from(ALLOWED_REASONS).join(", ")}`
4113
+ };
4114
+ }
4115
+ const payload = obj["payload"];
4116
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
4117
+ return { ok: false, error: "payload must be an object" };
4118
+ }
4119
+ const payloadObj = payload;
4120
+ const text = payloadObj["text"];
4121
+ if (typeof text !== "string") {
4122
+ return { ok: false, error: "payload.text must be a string" };
4123
+ }
4124
+ if (text.length > MAX_PAYLOAD_TEXT_CHARS) {
4125
+ return {
4126
+ ok: false,
4127
+ error: `payload.text must be <= ${MAX_PAYLOAD_TEXT_CHARS} chars`
4128
+ };
4129
+ }
4130
+ let callback;
4131
+ const callbackRaw = obj["callback"];
4132
+ if (callbackRaw !== void 0 && callbackRaw !== null) {
4133
+ if (typeof callbackRaw !== "object" || Array.isArray(callbackRaw)) {
4134
+ return { ok: false, error: "callback must be an object" };
4135
+ }
4136
+ const cbObj = callbackRaw;
4137
+ const cbUrl = cbObj["url"];
4138
+ const cbToken = cbObj["token"];
4139
+ if (typeof cbUrl !== "string" || cbUrl.length === 0) {
4140
+ return { ok: false, error: "callback.url must be a non-empty string" };
4141
+ }
4142
+ if (!isLoopbackCallbackUrl(cbUrl)) {
4143
+ return {
4144
+ ok: false,
4145
+ error: "callback.url must be a loopback base URL (http://127.0.0.1:PORT or http://localhost:PORT) with no path"
4146
+ };
4147
+ }
4148
+ if (typeof cbToken !== "string" || cbToken.length < 16) {
4149
+ return {
4150
+ ok: false,
4151
+ error: "callback.token must be a string of length >= 16"
4152
+ };
4153
+ }
4154
+ callback = { url: cbUrl, token: cbToken };
4155
+ }
4156
+ const chatIdRaw = obj["chat_id"];
4157
+ const chat_id = typeof chatIdRaw === "string" && chatIdRaw.length > 0 ? chatIdRaw : buildChatId(session_id, reason) ?? void 0;
4158
+ const req = {
4159
+ task_id,
4160
+ session_id,
4161
+ reason,
4162
+ ...chat_id ? { chat_id } : {},
4163
+ occurred_at: typeof obj["occurred_at"] === "string" ? obj["occurred_at"] : void 0,
4164
+ payload: {
4165
+ text,
4166
+ ts_start_ms: typeof payloadObj["ts_start_ms"] === "number" ? payloadObj["ts_start_ms"] : void 0,
4167
+ ts_end_ms: typeof payloadObj["ts_end_ms"] === "number" ? payloadObj["ts_end_ms"] : void 0,
4168
+ is_final: typeof payloadObj["is_final"] === "boolean" ? payloadObj["is_final"] : void 0,
4169
+ user_message_id: typeof payloadObj["user_message_id"] === "string" ? payloadObj["user_message_id"] : void 0
4170
+ },
4171
+ ...callback ? { callback } : {}
4172
+ };
4173
+ return { ok: true, req };
4174
+ }
4175
+ function httpTaskRequestToTaskPayload(req, overrides) {
4176
+ if (req.reason === "system_prelude") return null;
4177
+ const text = overrides?.text ?? req.payload.text;
4178
+ const evidence = {
4179
+ transcript_excerpt: text
4180
+ };
4181
+ if (typeof req.payload.ts_start_ms === "number") {
4182
+ evidence["ts_start_ms"] = req.payload.ts_start_ms;
4183
+ }
4184
+ if (typeof req.payload.ts_end_ms === "number") {
4185
+ evidence["ts_end_ms"] = req.payload.ts_end_ms;
4186
+ }
4187
+ if (typeof req.payload.is_final === "boolean") {
4188
+ evidence["is_final"] = req.payload.is_final;
4189
+ }
4190
+ if (typeof req.payload.user_message_id === "string") {
4191
+ evidence["user_message_id"] = req.payload.user_message_id;
4192
+ }
4193
+ if (req.chat_id) {
4194
+ evidence["chat_id"] = req.chat_id;
4195
+ }
4196
+ const isChat = req.reason === "chat_message";
4197
+ return {
4198
+ id: req.task_id,
4199
+ session_id: req.session_id,
4200
+ title: isChat ? `Chat message in session ${req.session_id}` : `Transcript chunk from session ${req.session_id}`,
4201
+ request_text: text,
4202
+ action_type: req.reason,
4203
+ status: "delivered",
4204
+ evidence_payload: evidence,
4205
+ created_at: req.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString()
4206
+ };
4207
+ }
4208
+
4209
+ // src/http-transport.ts
4210
+ var IDEMPOTENCY_MAX_SIZE = 1024;
4211
+ var IDEMPOTENCY_TTL_MS = 5 * 60 * 1e3;
4212
+ var CALLBACK_TIMEOUT_MS = 5e3;
4213
+ var CALLBACK_MAX_ATTEMPTS = 4;
4214
+ var CALLBACK_RETRY_BASE_MS = 250;
4215
+ var SUBAGENT_WAIT_TIMEOUT_MS = 12e4;
4216
+ var INCOMING_PATH = "/v1/incoming";
4217
+ var PING_PATH = "/v1/ping";
4218
+ function constantTimeTokenEqual(provided, expected) {
4219
+ const a = Buffer2.from(provided);
4220
+ const b = Buffer2.from(expected);
4221
+ if (a.length !== b.length) {
4222
+ const sink = Buffer2.alloc(Math.max(a.length, b.length));
4223
+ timingSafeEqual(sink, sink);
4224
+ return false;
4225
+ }
4226
+ return timingSafeEqual(a, b);
4227
+ }
4228
+ function extractBearerToken(header) {
4229
+ if (!header) return null;
4230
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
4231
+ if (!match) return null;
4232
+ return match[1]?.trim() ?? null;
4233
+ }
4234
+ function isLoopbackAddress(addr) {
4235
+ if (!addr) return false;
4236
+ if (addr === "127.0.0.1" || addr === "::1") return true;
4237
+ if (addr === "::ffff:127.0.0.1") return true;
4238
+ return false;
4239
+ }
4240
+ function readBody(req, maxBytes) {
4241
+ return new Promise((resolve, reject) => {
4242
+ const chunks = [];
4243
+ let total = 0;
4244
+ req.on("data", (chunk) => {
4245
+ total += chunk.length;
4246
+ if (total > maxBytes) {
4247
+ reject(new Error("body_too_large"));
4248
+ req.destroy();
4249
+ return;
4250
+ }
4251
+ chunks.push(chunk);
4252
+ });
4253
+ req.on("end", () => {
4254
+ resolve(Buffer2.concat(chunks).toString("utf8"));
4255
+ });
4256
+ req.on("error", (err) => reject(err));
4257
+ });
4258
+ }
4259
+ function jsonResponse(status, body) {
4260
+ const serialized = JSON.stringify(body);
4261
+ return {
4262
+ status,
4263
+ headers: {
4264
+ "Content-Type": "application/json",
4265
+ "Content-Length": String(Buffer2.byteLength(serialized))
4266
+ },
4267
+ body: serialized
4268
+ };
4269
+ }
4270
+ function extractContentFromMessage(msg) {
4271
+ const content = msg["content"];
4272
+ if (typeof content === "string") return content;
4273
+ if (Array.isArray(content)) {
4274
+ for (const part of content) {
4275
+ if (typeof part === "object" && part !== null && part["type"] === "text" && typeof part["text"] === "string") {
4276
+ return part["text"];
4277
+ }
4278
+ }
4279
+ }
4280
+ return null;
4281
+ }
4282
+ function createHttpTransport(options) {
4283
+ const { api } = options;
4284
+ const host = options.host ?? "127.0.0.1";
4285
+ const port = options.port ?? 8765;
4286
+ const token = options.token;
4287
+ const callbackFetch = options.callbackFetch ?? fetch;
4288
+ if (!token || token.length < 16) {
4289
+ throw new Error("http-transport: token must be at least 16 characters");
4290
+ }
4291
+ const idempotency = /* @__PURE__ */ new Map();
4292
+ const inflight = /* @__PURE__ */ new Set();
4293
+ const chatTasks = /* @__PURE__ */ new Map();
4294
+ const pendingPreludes = /* @__PURE__ */ new Map();
4295
+ function rememberResult(taskId, status, body) {
4296
+ idempotency.set(taskId, {
4297
+ status,
4298
+ body,
4299
+ expiresAt: Date.now() + IDEMPOTENCY_TTL_MS
4300
+ });
4301
+ while (idempotency.size > IDEMPOTENCY_MAX_SIZE) {
4302
+ const oldestKey = idempotency.keys().next().value;
4303
+ if (oldestKey === void 0) break;
4304
+ idempotency.delete(oldestKey);
4305
+ }
4306
+ }
4307
+ function evictExpired() {
4308
+ const now = Date.now();
4309
+ for (const [key, entry] of idempotency) {
4310
+ if (entry.expiresAt <= now) idempotency.delete(key);
4311
+ }
4312
+ }
4313
+ function recordTerminalCallback(taskId, callback, body) {
4314
+ const entry = idempotency.get(taskId);
4315
+ if (!entry) return;
4316
+ entry.terminal = { callback, body };
4317
+ }
4318
+ function consumePrelude(sessionId) {
4319
+ const prelude = pendingPreludes.get(sessionId);
4320
+ if (prelude !== void 0) pendingPreludes.delete(sessionId);
4321
+ return prelude;
4322
+ }
4323
+ async function extractReplyForChatTask(sessionKey, taskId, userMessageId, maxAttempts = 3) {
4324
+ const markers = [`StageWhisper task: ${taskId}`];
4325
+ if (userMessageId) markers.push(`[StageWhisper chat: ${userMessageId}]`);
4326
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4327
+ if (attempt > 0) {
4328
+ await new Promise((r) => setTimeout(r, 1e3));
4329
+ }
4330
+ const session = await api.runtime.subagent.getSessionMessages({
4331
+ sessionKey,
4332
+ limit: 50
4333
+ });
4334
+ const messages = session.messages;
4335
+ for (let i = 0; i < messages.length; i++) {
4336
+ const msg = messages[i];
4337
+ if (msg["role"] !== "user") continue;
4338
+ const text = extractContentFromMessage(msg) ?? "";
4339
+ if (!markers.some((m) => text.includes(m))) continue;
4340
+ for (let j = i + 1; j < messages.length; j++) {
4341
+ const reply = messages[j];
4342
+ const role = reply["role"];
4343
+ if (role === "assistant" || role === "model") {
4344
+ return extractContentFromMessage(reply);
4345
+ }
4346
+ if (role === "user") break;
4347
+ }
4348
+ }
4349
+ }
4350
+ return null;
4351
+ }
4352
+ async function postCallback(callback, taskId, body) {
4353
+ const url = buildCallbackUrl(callback.url, taskId);
4354
+ const serialized = JSON.stringify(body);
4355
+ let lastError = null;
4356
+ for (let attempt = 0; attempt < CALLBACK_MAX_ATTEMPTS; attempt++) {
4357
+ const controller = new AbortController();
4358
+ const timeoutId = setTimeout(() => controller.abort(), CALLBACK_TIMEOUT_MS);
4359
+ try {
4360
+ const res = await callbackFetch(url, {
4361
+ method: "POST",
4362
+ headers: {
4363
+ "Content-Type": "application/json",
4364
+ Authorization: `Bearer ${callback.token}`
4365
+ },
4366
+ body: serialized,
4367
+ signal: controller.signal
4368
+ });
4369
+ clearTimeout(timeoutId);
4370
+ if (res.ok) return;
4371
+ lastError = new Error(`callback returned status ${res.status}`);
4372
+ if (res.status >= 400 && res.status < 500) {
4373
+ throw lastError;
4374
+ }
4375
+ } catch (err) {
4376
+ clearTimeout(timeoutId);
4377
+ lastError = err;
4378
+ }
4379
+ if (attempt + 1 < CALLBACK_MAX_ATTEMPTS) {
4380
+ await new Promise(
4381
+ (r) => setTimeout(r, CALLBACK_RETRY_BASE_MS * 2 ** attempt)
4382
+ );
4383
+ }
4384
+ }
4385
+ throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "callback_failed"));
4386
+ }
4387
+ async function redeliverTerminalCallback(record, taskId) {
4388
+ try {
4389
+ await postCallback(record.callback, taskId, record.body);
4390
+ api.logger.info(
4391
+ `[stagewhisper-http] redelivered terminal callback for ${taskId}`
4392
+ );
4393
+ } catch (err) {
4394
+ const errMsg = err instanceof Error ? err.message : String(err);
4395
+ api.logger.error(
4396
+ `[stagewhisper-http] terminal callback redelivery failed for ${taskId}: ${errMsg}`
4397
+ );
4398
+ }
4399
+ }
4400
+ async function runReplyTaskAsync(taskRequest, kind) {
4401
+ const callback = taskRequest.callback;
4402
+ if (!callback) {
4403
+ api.logger.error(
4404
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} missing callback after validation`
4405
+ );
4406
+ return;
4407
+ }
4408
+ const userMessageId = taskRequest.payload.user_message_id;
4409
+ const effectiveText = applyPrelude(
4410
+ taskRequest.payload.text,
4411
+ consumePrelude(taskRequest.session_id)
4412
+ );
4413
+ const taskPayload = httpTaskRequestToTaskPayload(taskRequest, {
4414
+ text: effectiveText
4415
+ });
4416
+ if (!taskPayload) {
4417
+ api.logger.error(
4418
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} produced no payload`
4419
+ );
4420
+ return;
4421
+ }
4422
+ const peerId = `sw:${taskRequest.session_id}:${kind}`;
4423
+ const sessionKey = buildAgentSessionKey({
4424
+ agentId: "default",
4425
+ channel: "stagewhisper",
4426
+ peer: { kind: "direct", id: peerId },
4427
+ dmScope: "per-channel-peer"
4428
+ });
4429
+ const chatIdReason = kind === "chat" ? "chat_message" : "transcript_chunk";
4430
+ const chatId = taskRequest.chat_id ?? buildChatId(taskRequest.session_id, chatIdReason);
4431
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
4432
+ const sendCallback = async (body) => {
4433
+ recordTerminalCallback(taskRequest.task_id, callback, body);
4434
+ try {
4435
+ await postCallback(callback, taskRequest.task_id, body);
4436
+ } catch (err) {
4437
+ const errMsg = err instanceof Error ? err.message : String(err);
4438
+ api.logger.error(
4439
+ `[stagewhisper-http] callback POST failed for ${taskRequest.task_id}: ${errMsg}`
4440
+ );
4441
+ }
4442
+ };
4443
+ try {
4444
+ const dispatch = await dispatchTaskToAgent({
4445
+ api,
4446
+ task: taskPayload,
4447
+ deliver: true,
4448
+ idempotencyKey: `sw-http-task-${taskRequest.task_id}`,
4449
+ sessionPeerId: peerId
4450
+ });
4451
+ api.logger.info(
4452
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} dispatched (runId: ${dispatch.runId})`
4453
+ );
4454
+ const waitResult = await api.runtime.subagent.waitForRun({
4455
+ runId: dispatch.runId,
4456
+ timeoutMs: SUBAGENT_WAIT_TIMEOUT_MS
4457
+ });
4458
+ if (waitResult.status === "ok") {
4459
+ const reply = await extractReplyForChatTask(
4460
+ sessionKey,
4461
+ taskRequest.task_id,
4462
+ userMessageId
4463
+ );
4464
+ if (reply) {
4465
+ await sendCallback({
4466
+ task_id: taskRequest.task_id,
4467
+ session_id: taskRequest.session_id,
4468
+ chat_id: chatId,
4469
+ user_message_id: userMessageId ?? null,
4470
+ status: "completed",
4471
+ reply_text: reply,
4472
+ occurred_at: occurredAt
4473
+ });
4474
+ api.logger.info(
4475
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} callback delivered`
4476
+ );
4477
+ } else if (kind === "reasoning") {
4478
+ await sendCallback({
4479
+ task_id: taskRequest.task_id,
4480
+ session_id: taskRequest.session_id,
4481
+ chat_id: chatId,
4482
+ user_message_id: userMessageId ?? null,
4483
+ status: "silent",
4484
+ occurred_at: occurredAt
4485
+ });
4486
+ api.logger.info(
4487
+ `[stagewhisper-http] reasoning task ${taskRequest.task_id} produced no signal (silent)`
4488
+ );
4489
+ } else {
4490
+ await sendCallback({
4491
+ task_id: taskRequest.task_id,
4492
+ session_id: taskRequest.session_id,
4493
+ chat_id: chatId,
4494
+ user_message_id: userMessageId ?? null,
4495
+ status: "errored",
4496
+ error_code: "no_reply",
4497
+ error_message: "Agent run produced no reply",
4498
+ occurred_at: occurredAt
4499
+ });
4500
+ api.logger.warn(
4501
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} produced no reply`
4502
+ );
4503
+ }
4504
+ } else {
4505
+ await sendCallback({
4506
+ task_id: taskRequest.task_id,
4507
+ session_id: taskRequest.session_id,
4508
+ chat_id: chatId,
4509
+ user_message_id: userMessageId ?? null,
4510
+ status: "errored",
4511
+ error_code: "agent_error",
4512
+ error_message: waitResult.error ?? `agent run ${waitResult.status}`,
4513
+ occurred_at: occurredAt
4514
+ });
4515
+ api.logger.error(
4516
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} agent failed: ${waitResult.error}`
4517
+ );
4518
+ }
4519
+ } catch (err) {
4520
+ const errMsg = err instanceof Error ? err.message : String(err);
4521
+ api.logger.error(
4522
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} threw: ${errMsg}`
4523
+ );
4524
+ await sendCallback({
4525
+ task_id: taskRequest.task_id,
4526
+ session_id: taskRequest.session_id,
4527
+ chat_id: chatId,
4528
+ user_message_id: userMessageId ?? null,
4529
+ status: "errored",
4530
+ error_code: "execution_error",
4531
+ error_message: errMsg,
4532
+ occurred_at: occurredAt
4533
+ });
4534
+ } finally {
4535
+ inflight.delete(taskRequest.task_id);
4536
+ }
4537
+ }
4538
+ async function handleChatMessageRequest(taskReq) {
4539
+ if (!taskReq.callback) {
4540
+ return jsonResponse(400, {
4541
+ error: "chat_message requires callback {url, token}"
4542
+ });
4543
+ }
4544
+ const ackBody = JSON.stringify({ status: "accepted", task_id: taskReq.task_id });
4545
+ const response = {
4546
+ status: 202,
4547
+ headers: {
4548
+ "Content-Type": "application/json",
4549
+ "Content-Length": String(Buffer2.byteLength(ackBody))
4550
+ },
4551
+ body: ackBody
4552
+ };
4553
+ rememberResult(taskReq.task_id, response.status, response.body);
4554
+ const task = runReplyTaskAsync(taskReq, "chat");
4555
+ chatTasks.set(taskReq.task_id, task);
4556
+ void task.finally(() => {
4557
+ if (chatTasks.get(taskReq.task_id) === task) {
4558
+ chatTasks.delete(taskReq.task_id);
4559
+ }
4560
+ });
4561
+ return response;
4562
+ }
4563
+ async function handleTranscriptChunkRequest(taskReq) {
4564
+ if (taskReq.payload.is_final !== true) {
4565
+ const response2 = jsonResponse(202, {
4566
+ status: "accepted",
4567
+ task_id: taskReq.task_id,
4568
+ dispatched: false
4569
+ });
4570
+ rememberResult(taskReq.task_id, response2.status, response2.body);
4571
+ inflight.delete(taskReq.task_id);
4572
+ return response2;
4573
+ }
4574
+ if (!taskReq.callback) {
4575
+ try {
4576
+ const effectiveText = applyPrelude(
4577
+ taskReq.payload.text,
4578
+ consumePrelude(taskReq.session_id)
4579
+ );
4580
+ const taskPayload = httpTaskRequestToTaskPayload(taskReq, {
4581
+ text: effectiveText
4582
+ });
4583
+ if (!taskPayload) {
4584
+ return jsonResponse(400, { error: "invalid_payload_for_reason" });
4585
+ }
4586
+ const reasoningPeerId = `sw:${taskReq.session_id}:reasoning`;
4587
+ const dispatch = await dispatchTaskToAgent({
4588
+ api,
4589
+ task: taskPayload,
4590
+ deliver: true,
4591
+ idempotencyKey: `sw-http-task-${taskReq.task_id}`,
4592
+ sessionPeerId: reasoningPeerId
4593
+ });
4594
+ api.logger.info(
4595
+ `[stagewhisper-http] dispatched one-way task ${taskReq.task_id} (runId: ${dispatch.runId})`
4596
+ );
4597
+ const response2 = jsonResponse(200, {
4598
+ status: "accepted",
4599
+ task_id: taskReq.task_id,
4600
+ run_id: dispatch.runId,
4601
+ dispatched: true
4602
+ });
4603
+ rememberResult(taskReq.task_id, response2.status, response2.body);
4604
+ return response2;
4605
+ } catch (err) {
4606
+ const errMsg = err instanceof Error ? err.message : String(err);
4607
+ api.logger.error(
4608
+ `[stagewhisper-http] dispatch failed for ${taskReq.task_id}: ${errMsg}`
4609
+ );
4610
+ return jsonResponse(503, { error: "dispatch_failed", detail: errMsg });
4611
+ } finally {
4612
+ inflight.delete(taskReq.task_id);
4613
+ }
4614
+ }
4615
+ const ackBody = JSON.stringify({ status: "accepted", task_id: taskReq.task_id });
4616
+ const response = {
4617
+ status: 202,
4618
+ headers: {
4619
+ "Content-Type": "application/json",
4620
+ "Content-Length": String(Buffer2.byteLength(ackBody))
4621
+ },
4622
+ body: ackBody
4623
+ };
4624
+ rememberResult(taskReq.task_id, response.status, response.body);
4625
+ const task = runReplyTaskAsync(taskReq, "reasoning");
4626
+ chatTasks.set(taskReq.task_id, task);
4627
+ void task.finally(() => {
4628
+ if (chatTasks.get(taskReq.task_id) === task) {
4629
+ chatTasks.delete(taskReq.task_id);
4630
+ }
4631
+ });
4632
+ return response;
4633
+ }
4634
+ function handleSystemPreludeRequest(taskReq) {
4635
+ pendingPreludes.set(taskReq.session_id, taskReq.payload.text);
4636
+ const response = jsonResponse(202, {
4637
+ status: "accepted",
4638
+ task_id: taskReq.task_id
4639
+ });
4640
+ rememberResult(taskReq.task_id, response.status, response.body);
4641
+ inflight.delete(taskReq.task_id);
4642
+ return response;
4643
+ }
4644
+ async function dispatchRoute(req) {
4645
+ if (!isLoopbackAddress(req.remoteAddress)) {
4646
+ api.logger.warn(
4647
+ `[stagewhisper-http] rejecting non-loopback connection from ${req.remoteAddress}`
4648
+ );
4649
+ return jsonResponse(403, { error: "non_loopback_rejected" });
4650
+ }
4651
+ if (!isAllowedHostHeader(req.host)) {
4652
+ api.logger.warn(
4653
+ `[stagewhisper-http] rejecting disallowed Host header: ${req.host ?? "<missing>"}`
4654
+ );
4655
+ return jsonResponse(403, { error: "invalid_host" });
4656
+ }
4657
+ const providedToken = extractBearerToken(req.authorization);
4658
+ if (!providedToken || !constantTimeTokenEqual(providedToken, token)) {
4659
+ return jsonResponse(401, { error: "invalid_token" });
4660
+ }
4661
+ if (req.method === "POST" && req.url === PING_PATH) {
4662
+ return jsonResponse(200, { ok: true });
4663
+ }
4664
+ if (req.method === "POST" && req.url === INCOMING_PATH) {
4665
+ let parsed;
4666
+ try {
4667
+ parsed = req.body.length === 0 ? {} : JSON.parse(req.body);
4668
+ } catch (err) {
4669
+ return jsonResponse(400, {
4670
+ error: `invalid_json: ${err instanceof Error ? err.message : String(err)}`
4671
+ });
4672
+ }
4673
+ const validation = validateHttpTaskRequest(parsed);
4674
+ if (!validation.ok) {
4675
+ return jsonResponse(400, { error: validation.error });
4676
+ }
4677
+ const taskReq = validation.req;
4678
+ evictExpired();
4679
+ const cached = idempotency.get(taskReq.task_id);
4680
+ if (cached && cached.expiresAt > Date.now()) {
4681
+ if (cached.terminal) {
4682
+ const terminal = cached.terminal;
4683
+ const redelivery = redeliverTerminalCallback(terminal, taskReq.task_id);
4684
+ chatTasks.set(taskReq.task_id, redelivery);
4685
+ void redelivery.finally(() => {
4686
+ if (chatTasks.get(taskReq.task_id) === redelivery) {
4687
+ chatTasks.delete(taskReq.task_id);
4688
+ }
4689
+ });
4690
+ }
4691
+ return {
4692
+ status: cached.status,
4693
+ headers: {
4694
+ "Content-Type": "application/json",
4695
+ "Content-Length": String(Buffer2.byteLength(cached.body)),
4696
+ "X-Idempotent-Replay": "true"
4697
+ },
4698
+ body: cached.body
4699
+ };
4700
+ }
4701
+ if (inflight.has(taskReq.task_id)) {
4702
+ return jsonResponse(503, { error: "task_in_flight" });
4703
+ }
4704
+ inflight.add(taskReq.task_id);
4705
+ if (taskReq.reason === "chat_message") {
4706
+ return handleChatMessageRequest(taskReq);
4707
+ }
4708
+ if (taskReq.reason === "system_prelude") {
4709
+ return handleSystemPreludeRequest(taskReq);
4710
+ }
4711
+ return handleTranscriptChunkRequest(taskReq);
4712
+ }
4713
+ return jsonResponse(404, { error: "not_found" });
4714
+ }
4715
+ async function handleRealRequest(req, res) {
4716
+ const method = req.method ?? "";
4717
+ const url = req.url ?? "";
4718
+ const authHeader = req.headers["authorization"];
4719
+ const authorization = Array.isArray(authHeader) ? authHeader[0] : authHeader;
4720
+ const hostHeader = req.headers["host"];
4721
+ const hostValue = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
4722
+ const remoteAddress = req.socket.remoteAddress;
4723
+ let body = "";
4724
+ if (method === "POST") {
4725
+ try {
4726
+ body = await readBody(req, MAX_BODY_BYTES);
4727
+ } catch (err) {
4728
+ const msg = err instanceof Error ? err.message : String(err);
4729
+ const response = msg === "body_too_large" ? jsonResponse(413, { error: "body_too_large" }) : jsonResponse(400, { error: `invalid_body: ${msg}` });
4730
+ writeResponse(res, response);
4731
+ return;
4732
+ }
4733
+ }
4734
+ const result = await dispatchRoute({
4735
+ method,
4736
+ url,
4737
+ authorization,
4738
+ host: hostValue,
4739
+ remoteAddress,
4740
+ body
4741
+ });
4742
+ writeResponse(res, result);
4743
+ }
4744
+ function writeResponse(res, response) {
4745
+ res.statusCode = response.status;
4746
+ for (const [key, value] of Object.entries(response.headers)) {
4747
+ res.setHeader(key, value);
4748
+ }
4749
+ res.end(response.body);
4750
+ }
4751
+ let server = null;
4752
+ return {
4753
+ async start() {
4754
+ if (server) return;
4755
+ const srv = http.createServer((req, res) => {
4756
+ handleRealRequest(req, res).catch((err) => {
4757
+ api.logger.error(`[stagewhisper-http] unhandled error: ${err}`);
4758
+ if (!res.headersSent) {
4759
+ try {
4760
+ writeResponse(res, jsonResponse(500, { error: "internal_error" }));
4761
+ } catch {
4762
+ }
4763
+ }
4764
+ });
4765
+ });
4766
+ server = srv;
4767
+ await new Promise((resolve, reject) => {
4768
+ const onError = (err) => {
4769
+ srv.off("listening", onListening);
4770
+ reject(err);
4771
+ };
4772
+ const onListening = () => {
4773
+ srv.off("error", onError);
4774
+ resolve();
4775
+ };
4776
+ srv.once("error", onError);
4777
+ srv.once("listening", onListening);
4778
+ srv.listen(port, host);
4779
+ });
4780
+ const addr = srv.address();
4781
+ api.logger.info(
4782
+ `[stagewhisper-http] listening on http://${host}:${typeof addr === "object" && addr ? addr.port : port}`
4783
+ );
4784
+ },
4785
+ async stop() {
4786
+ const srv = server;
4787
+ if (!srv) return;
4788
+ server = null;
4789
+ await new Promise((resolve) => {
4790
+ srv.close(() => resolve());
4791
+ srv.closeAllConnections?.();
4792
+ });
4793
+ const pending = Array.from(chatTasks.values());
4794
+ if (pending.length > 0) {
4795
+ await Promise.allSettled(pending);
4796
+ }
4797
+ idempotency.clear();
4798
+ inflight.clear();
4799
+ chatTasks.clear();
4800
+ pendingPreludes.clear();
4801
+ api.logger.info("[stagewhisper-http] stopped");
4802
+ },
4803
+ address() {
4804
+ const addr = server?.address();
4805
+ if (typeof addr === "object" && addr !== null) return addr;
4806
+ return null;
4807
+ },
4808
+ async handleSyntheticRequest(input) {
4809
+ const headers = input.headers ?? {};
4810
+ const authorization = headers["Authorization"] ?? headers["authorization"] ?? void 0;
4811
+ const hostValue = headers["Host"] ?? headers["host"] ?? "127.0.0.1";
4812
+ const remoteAddress = input.remoteAddress ?? "127.0.0.1";
4813
+ const body = input.body ?? "";
4814
+ return dispatchRoute({
4815
+ method: input.method,
4816
+ url: input.url,
4817
+ authorization,
4818
+ host: hostValue,
4819
+ remoteAddress,
4820
+ body
4821
+ });
4822
+ },
4823
+ whenChatTaskSettled(taskId) {
4824
+ return chatTasks.get(taskId);
4825
+ }
4826
+ };
4827
+ }
4828
+
4829
+ // src/pairing.ts
4830
+ import { Buffer as Buffer3 } from "node:buffer";
4831
+ import { randomBytes } from "node:crypto";
4832
+ var PAIRING_CODE_PREFIX = "stagewhisper-pair:v1:";
4833
+ function encodePairingCode(url, token, label) {
4834
+ const payload = JSON.stringify({ url, token, label });
4835
+ const encoded = Buffer3.from(payload, "utf8").toString("base64url");
4836
+ return `${PAIRING_CODE_PREFIX}${encoded}`;
4837
+ }
4838
+ function generateRelayToken() {
4839
+ return randomBytes(32).toString("base64url");
4840
+ }
4841
+
3942
4842
  // src/runtime.ts
3943
4843
  var runtimeStore = createPluginRuntimeStore(
3944
4844
  "StageWhisper plugin runtime not initialized"
@@ -4230,41 +5130,13 @@ function createRelayService(api) {
4230
5130
  };
4231
5131
  }
4232
5132
  }
4233
- function buildTaskMessage(task) {
4234
- const lines = [];
4235
- lines.push(`**${task.title}**`);
4236
- lines.push("");
4237
- lines.push(task.request_text);
4238
- if (task.evidence_payload) {
4239
- const evidence = task.evidence_payload;
4240
- if (evidence["transcript_excerpt"]) {
4241
- lines.push("");
4242
- lines.push(`Context: ${evidence["transcript_excerpt"]}`);
4243
- }
4244
- if (evidence["signal_summary"]) {
4245
- lines.push(`Signal: ${evidence["signal_summary"]}`);
4246
- }
4247
- if (evidence["tone_summary"]) {
4248
- lines.push(`Tone: ${evidence["tone_summary"]}`);
4249
- }
4250
- if (evidence["playbook_label"]) {
4251
- lines.push(`Playbook: ${evidence["playbook_label"]}`);
4252
- }
4253
- }
4254
- lines.push("");
4255
- lines.push(`Action type: ${task.action_type}`);
4256
- lines.push(`StageWhisper task: ${task.id}`);
4257
- lines.push(`Session: ${task.session_id}`);
4258
- return lines.join("\n");
4259
- }
4260
- function isTestTask(task) {
4261
- return task.action_type === "connectivity_test";
4262
- }
5133
+ const buildTaskMessage2 = buildTaskMessage;
5134
+ const isTestTask2 = isTestTask;
4263
5135
  async function updateStatus(client, task, status) {
4264
- if (isTestTask(task)) return;
5136
+ if (isTestTask2(task)) return;
4265
5137
  await client.updateTaskStatus(task.id, status);
4266
5138
  }
4267
- function extractContentFromMessage(msg) {
5139
+ function extractContentFromMessage2(msg) {
4268
5140
  const content = msg["content"];
4269
5141
  if (typeof content === "string") return content;
4270
5142
  if (Array.isArray(content)) {
@@ -4282,7 +5154,7 @@ function createRelayService(api) {
4282
5154
  if (!msg) continue;
4283
5155
  const role = msg["role"];
4284
5156
  if (role !== "assistant" && role !== "model") continue;
4285
- return extractContentFromMessage(msg);
5157
+ return extractContentFromMessage2(msg);
4286
5158
  }
4287
5159
  return null;
4288
5160
  }
@@ -4333,13 +5205,13 @@ function createRelayService(api) {
4333
5205
  for (let i = 0; i < messages.length; i++) {
4334
5206
  const msg = messages[i];
4335
5207
  if (msg["role"] !== "user") continue;
4336
- const text = extractContentFromMessage(msg) ?? "";
5208
+ const text = extractContentFromMessage2(msg) ?? "";
4337
5209
  if (!text.includes(`StageWhisper task: ${taskId}`)) continue;
4338
5210
  for (let j = i + 1; j < messages.length; j++) {
4339
5211
  const reply = messages[j];
4340
5212
  const role = reply["role"];
4341
5213
  if (role === "assistant" || role === "model") {
4342
- return extractContentFromMessage(reply);
5214
+ return extractContentFromMessage2(reply);
4343
5215
  }
4344
5216
  if (role === "user") break;
4345
5217
  }
@@ -4375,13 +5247,13 @@ function createRelayService(api) {
4375
5247
  for (let i = 0; i < messages.length; i++) {
4376
5248
  const msg = messages[i];
4377
5249
  if (msg["role"] !== "user") continue;
4378
- const text = extractContentFromMessage(msg) ?? "";
5250
+ const text = extractContentFromMessage2(msg) ?? "";
4379
5251
  if (!text.includes(marker)) continue;
4380
5252
  for (let j = i + 1; j < messages.length; j++) {
4381
5253
  const next = messages[j];
4382
5254
  const role = next["role"];
4383
5255
  if (role === "assistant" || role === "model") {
4384
- return extractContentFromMessage(next);
5256
+ return extractContentFromMessage2(next);
4385
5257
  }
4386
5258
  if (role === "user") break;
4387
5259
  }
@@ -4457,7 +5329,7 @@ function createRelayService(api) {
4457
5329
  }
4458
5330
  const isByo = !!task.is_byo_encrypted;
4459
5331
  const effectiveTask = isByo ? decryptTaskFields(task, client) : task;
4460
- const messageContent = buildTaskMessage(effectiveTask);
5332
+ const messageContent = buildTaskMessage2(effectiveTask);
4461
5333
  const peerId = `sw-session-${task.session_id}`;
4462
5334
  const sessionKey = buildAgentSessionKey({
4463
5335
  agentId: "default",
@@ -4500,7 +5372,7 @@ function createRelayService(api) {
4500
5372
  });
4501
5373
  }
4502
5374
  async function handleTestTask(task, client) {
4503
- const messageContent = buildTaskMessage(task);
5375
+ const messageContent = buildTaskMessage2(task);
4504
5376
  const sessionKey = buildAgentSessionKey({
4505
5377
  agentId: "default",
4506
5378
  channel: "stagewhisper",
@@ -4533,7 +5405,7 @@ function createRelayService(api) {
4533
5405
  async function handleTask(task, client) {
4534
5406
  api.logger.info(`Received task: ${task.title} (${task.id})`);
4535
5407
  try {
4536
- if (isTestTask(task)) {
5408
+ if (isTestTask2(task)) {
4537
5409
  await handleTestTask(task, client);
4538
5410
  } else {
4539
5411
  await handleNormalTask(task, client);
@@ -4898,6 +5770,15 @@ function createRelayService(api) {
4898
5770
  const decoratedPrompt = `${plaintextContent}
4899
5771
 
4900
5772
  [StageWhisper chat: ${userMessageId}]`;
5773
+ if (envelopeKey === null) {
5774
+ await handleChatMessageStreaming(
5775
+ client,
5776
+ sessionKey,
5777
+ userMessageId,
5778
+ decoratedPrompt
5779
+ );
5780
+ return;
5781
+ }
4901
5782
  try {
4902
5783
  const result = await api.runtime.subagent.run({
4903
5784
  sessionKey,
@@ -4948,6 +5829,134 @@ function createRelayService(api) {
4948
5829
  }
4949
5830
  }
4950
5831
  }
5832
+ async function handleChatMessageStreaming(client, sessionKey, userMessageId, decoratedPrompt) {
5833
+ let assistantId;
5834
+ try {
5835
+ const started = await client.startChatReply(userMessageId);
5836
+ assistantId = started.id;
5837
+ } catch (err) {
5838
+ const errMsg = err instanceof Error ? err.message : String(err);
5839
+ api.logger.error(
5840
+ `Chat ${userMessageId} failed to start streaming reply: ${errMsg}`
5841
+ );
5842
+ try {
5843
+ await client.postChatReply(userMessageId, "(streaming start failed)", {
5844
+ status: "errored",
5845
+ errorCode: "stream_start_failed",
5846
+ errorMessage: errMsg
5847
+ });
5848
+ } catch (postErr) {
5849
+ api.logger.error(
5850
+ `Chat ${userMessageId} fallback errored post failed: ${postErr}`
5851
+ );
5852
+ }
5853
+ return;
5854
+ }
5855
+ let ackedText = "";
5856
+ const sendDelta = async (full) => {
5857
+ if (!full || full.length <= ackedText.length) return;
5858
+ if (!full.startsWith(ackedText)) return;
5859
+ const diff = full.slice(ackedText.length);
5860
+ if (!diff) return;
5861
+ try {
5862
+ await client.postChatReplyDelta(assistantId, diff);
5863
+ ackedText = full;
5864
+ } catch (err) {
5865
+ api.logger.warn(`Chat ${userMessageId} delta post failed: ${err}`);
5866
+ }
5867
+ };
5868
+ const finalizeErrored = async (content, errorCode, errorMessage) => {
5869
+ try {
5870
+ await client.completeChatReply(assistantId, {
5871
+ status: "errored",
5872
+ content: content || void 0,
5873
+ errorCode,
5874
+ errorMessage
5875
+ });
5876
+ } catch (postErr) {
5877
+ api.logger.error(
5878
+ `Chat ${userMessageId} finalize errored failed: ${postErr}`
5879
+ );
5880
+ }
5881
+ };
5882
+ let pollDone = false;
5883
+ const pollInterval = 400;
5884
+ const pollLoop = (async () => {
5885
+ while (!pollDone) {
5886
+ await new Promise((r) => setTimeout(r, pollInterval));
5887
+ if (pollDone) break;
5888
+ try {
5889
+ const partial = await extractReplyForChatMessage(
5890
+ sessionKey,
5891
+ userMessageId,
5892
+ 1
5893
+ );
5894
+ if (partial) await sendDelta(partial);
5895
+ } catch (err) {
5896
+ api.logger.warn(`Chat ${userMessageId} poll failed: ${err}`);
5897
+ }
5898
+ }
5899
+ })();
5900
+ try {
5901
+ const result = await api.runtime.subagent.run({
5902
+ sessionKey,
5903
+ message: decoratedPrompt,
5904
+ deliver: true,
5905
+ idempotencyKey: `sw-chat-${userMessageId}`
5906
+ });
5907
+ const waitResult = await api.runtime.subagent.waitForRun({
5908
+ runId: result.runId,
5909
+ timeoutMs: 12e4
5910
+ });
5911
+ pollDone = true;
5912
+ await pollLoop;
5913
+ const finalReply = await extractReplyForChatMessage(
5914
+ sessionKey,
5915
+ userMessageId
5916
+ );
5917
+ if (waitResult.status !== "ok") {
5918
+ api.logger.error(
5919
+ `Agent run failed for chat ${userMessageId}: ${waitResult.error}`
5920
+ );
5921
+ await finalizeErrored(
5922
+ finalReply ?? ackedText,
5923
+ "agent_error",
5924
+ waitResult.error ?? "Agent run failed"
5925
+ );
5926
+ return;
5927
+ }
5928
+ if (!finalReply) {
5929
+ api.logger.warn(
5930
+ `Chat message ${userMessageId} completed but no reply found`
5931
+ );
5932
+ await finalizeErrored(
5933
+ ackedText,
5934
+ "no_reply",
5935
+ "Agent run produced no reply"
5936
+ );
5937
+ return;
5938
+ }
5939
+ try {
5940
+ await client.completeChatReply(assistantId, { content: finalReply });
5941
+ api.logger.info(`Chat message ${userMessageId} streamed`);
5942
+ } catch (completeErr) {
5943
+ const errMsg = completeErr instanceof Error ? completeErr.message : String(completeErr);
5944
+ api.logger.error(
5945
+ `Chat ${userMessageId} complete failed: ${errMsg}`
5946
+ );
5947
+ await finalizeErrored(finalReply, "complete_failed", errMsg);
5948
+ }
5949
+ } catch (err) {
5950
+ pollDone = true;
5951
+ try {
5952
+ await pollLoop;
5953
+ } catch {
5954
+ }
5955
+ const errMsg = err instanceof Error ? err.message : String(err);
5956
+ api.logger.error(`Failed to stream chat ${userMessageId}: ${errMsg}`);
5957
+ await finalizeErrored(ackedText, "execution_error", errMsg);
5958
+ }
5959
+ }
4951
5960
  async function handleReasoningJob(job, client) {
4952
5961
  const hydrated = await hydrateReasoningJobEnvelope(
4953
5962
  job,
@@ -5226,20 +6235,73 @@ function createRelayService(api) {
5226
6235
  }
5227
6236
 
5228
6237
  // plugin-main.ts
6238
+ function resolveTransportMode(api) {
6239
+ const pluginCfg = api.pluginConfig ?? {};
6240
+ const fromConfig = pluginCfg["transport"];
6241
+ if (fromConfig === "http" || fromConfig === "sse") return fromConfig;
6242
+ const envOverride = process.env["STAGEWHISPER_TRANSPORT"];
6243
+ if (envOverride === "http" || envOverride === "sse") return envOverride;
6244
+ return "sse";
6245
+ }
6246
+ function resolveHttpTransportToken(pluginCfg, env = process.env) {
6247
+ if (typeof pluginCfg["httpToken"] === "string" && pluginCfg["httpToken"]) {
6248
+ return pluginCfg["httpToken"];
6249
+ }
6250
+ if (env["STAGEWHISPER_HTTP_TOKEN"]) {
6251
+ return env["STAGEWHISPER_HTTP_TOKEN"];
6252
+ }
6253
+ if (typeof pluginCfg["relayToken"] === "string") {
6254
+ return pluginCfg["relayToken"];
6255
+ }
6256
+ return "";
6257
+ }
6258
+ function createHttpTransportService(api) {
6259
+ const pluginCfg = api.pluginConfig ?? {};
6260
+ const host = typeof pluginCfg["httpHost"] === "string" ? pluginCfg["httpHost"] : process.env["STAGEWHISPER_HTTP_HOST"] ?? "127.0.0.1";
6261
+ const portRaw = pluginCfg["httpPort"];
6262
+ const port = typeof portRaw === "number" ? portRaw : typeof portRaw === "string" ? Number(portRaw) : Number(process.env["STAGEWHISPER_HTTP_PORT"]) || 8765;
6263
+ const token = resolveHttpTransportToken(pluginCfg);
6264
+ let transport = null;
6265
+ return {
6266
+ id: "stagewhisper-http-transport",
6267
+ async start(_ctx) {
6268
+ if (!token || token.length < 16) {
6269
+ api.logger.warn(
6270
+ "StageWhisper HTTP transport requires `httpToken` (>=16 chars) in plugin config or STAGEWHISPER_HTTP_TOKEN env var \u2014 listener not started."
6271
+ );
6272
+ return;
6273
+ }
6274
+ transport = createHttpTransport({ api, host, port, token });
6275
+ try {
6276
+ await transport.start();
6277
+ api.logger.info(`StageWhisper HTTP transport started on ${host}:${port}`);
6278
+ } catch (err) {
6279
+ api.logger.error(`StageWhisper HTTP transport failed to start: ${err}`);
6280
+ transport = null;
6281
+ }
6282
+ },
6283
+ async stop(_ctx) {
6284
+ if (transport) {
6285
+ await transport.stop();
6286
+ transport = null;
6287
+ }
6288
+ }
6289
+ };
6290
+ }
5229
6291
  async function ensureResponsesEndpoint(api) {
5230
6292
  try {
5231
6293
  const cfg = await api.runtime.config.loadConfig();
5232
6294
  const gw = cfg["gateway"] ?? {};
5233
- const http = gw["http"] ?? {};
5234
- const endpoints = http["endpoints"] ?? {};
6295
+ const http2 = gw["http"] ?? {};
6296
+ const endpoints = http2["endpoints"] ?? {};
5235
6297
  const responses = endpoints["responses"] ?? {};
5236
6298
  if (responses["enabled"] === true) return;
5237
6299
  const auth = gw["auth"] ?? {};
5238
6300
  if (auth["mode"] === "none" && !auth["token"] && !auth["password"]) return;
5239
6301
  responses["enabled"] = true;
5240
6302
  endpoints["responses"] = responses;
5241
- http["endpoints"] = endpoints;
5242
- gw["http"] = http;
6303
+ http2["endpoints"] = endpoints;
6304
+ gw["http"] = http2;
5243
6305
  cfg["gateway"] = gw;
5244
6306
  await api.runtime.config.writeConfigFile(cfg);
5245
6307
  api.logger.info(
@@ -5257,13 +6319,7 @@ var plugin_main_default = definePluginEntry({
5257
6319
  api.registerCli(
5258
6320
  ({ program }) => {
5259
6321
  const sw = program.command("stagewhisper").description("StageWhisper integration");
5260
- sw.command("pair").description(
5261
- "Pair with StageWhisper using a pairing code from the desktop app"
5262
- ).requiredOption("--code <code>", "Pairing code from Settings \u2192 Assistant").option(
5263
- "--api-url <url>",
5264
- "StageWhisper backend URL",
5265
- "https://api.stagewhisper.io"
5266
- ).option("--label <label>", "Label for this OpenClaw host", "OpenClaw").option("--no-enable-responses", "Skip enabling the gateway OpenResponses HTTP API").action(
6322
+ sw.command("pair").description("Pair with StageWhisper using a pairing code from the desktop app").requiredOption("--code <code>", "Pairing code from Settings \u2192 Assistant").option("--api-url <url>", "StageWhisper backend URL", "https://api.stagewhisper.io").option("--label <label>", "Label for this OpenClaw host", "OpenClaw").option("--no-enable-responses", "Skip enabling the gateway OpenResponses HTTP API").action(
5267
6323
  async (opts) => {
5268
6324
  const { StageWhisperClient: StageWhisperClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
5269
6325
  const { IdentityKeypair: IdentityKeypair2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
@@ -5308,26 +6364,33 @@ var plugin_main_default = definePluginEntry({
5308
6364
  const authMode = gwAuth["mode"];
5309
6365
  const hasToken = typeof gwAuth["token"] === "string" && gwAuth["token"].length > 0;
5310
6366
  if (authMode === "none" && !hasToken) {
5311
- console.warn(" \u26A0 gateway.auth.mode is 'none' with no token \u2014 skipping HTTP API enablement.");
6367
+ console.warn(
6368
+ " \u26A0 gateway.auth.mode is 'none' with no token \u2014 skipping HTTP API enablement."
6369
+ );
5312
6370
  console.warn(" Set a gateway auth token first for reasoning to work.\n");
5313
6371
  } else {
5314
- const http = gw["http"] ?? {};
5315
- const endpoints = http["endpoints"] ?? {};
6372
+ const http2 = gw["http"] ?? {};
6373
+ const endpoints = http2["endpoints"] ?? {};
5316
6374
  const responses = endpoints["responses"] ?? {};
5317
6375
  responses["enabled"] = true;
5318
6376
  endpoints["responses"] = responses;
5319
- http["endpoints"] = endpoints;
5320
- gw["http"] = http;
6377
+ http2["endpoints"] = endpoints;
6378
+ gw["http"] = http2;
5321
6379
  cfg["gateway"] = gw;
5322
6380
  }
5323
6381
  }
5324
6382
  await api.runtime.config.writeConfigFile(cfg);
5325
- console.log(
5326
- `
5327
- \u2713 Paired with StageWhisper (${result.label})`
5328
- );
6383
+ console.log(`
6384
+ \u2713 Paired with StageWhisper (${result.label})`);
5329
6385
  console.log(" Config saved. Restart the gateway to activate:\n");
5330
6386
  console.log(" openclaw gateway restart\n");
6387
+ console.log(
6388
+ " For local HTTP transport (transport: http), the desktop authenticates"
6389
+ );
6390
+ console.log(
6391
+ " against the loopback listener with this same relay token \u2014 no separate"
6392
+ );
6393
+ console.log(" httpToken is required unless you set one explicitly.\n");
5331
6394
  } catch (err) {
5332
6395
  console.error(`
5333
6396
  \u2717 Pairing failed: ${err}
@@ -5336,9 +6399,62 @@ var plugin_main_default = definePluginEntry({
5336
6399
  }
5337
6400
  }
5338
6401
  );
5339
- sw.command("unpair").description(
5340
- "Remove StageWhisper pairing (run before `openclaw plugins uninstall`)"
5341
- ).option("--keep-responses", "Keep the OpenResponses HTTP API enabled after unpair").action(async (opts) => {
6402
+ sw.command("pair-code").description(
6403
+ "Generate a StageWhisper Free pairing code for the local HTTP transport (no backend)"
6404
+ ).option("--url <url>", "Relay URL StageWhisper Free should reach this gateway at").option("--port <port>", "Loopback port for the HTTP transport listener").option("--label <label>", "Label shown in StageWhisper Free", "OpenClaw").action(async (opts) => {
6405
+ try {
6406
+ const cfg = await api.runtime.config.loadConfig();
6407
+ const plugins = cfg["plugins"] ?? {};
6408
+ const entries = plugins["entries"] ?? {};
6409
+ const swEntry = entries["stagewhisper"] ?? {};
6410
+ const swConfig = swEntry["config"] ?? {};
6411
+ const host = typeof swConfig["httpHost"] === "string" && swConfig["httpHost"] ? swConfig["httpHost"] : "127.0.0.1";
6412
+ const port = opts.port ? Number(opts.port) : typeof swConfig["httpPort"] === "number" ? swConfig["httpPort"] : 8765;
6413
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
6414
+ console.error(`
6415
+ \u2717 Invalid port ${port} (must be 1024-65535)
6416
+ `);
6417
+ process.exit(1);
6418
+ }
6419
+ let token = typeof swConfig["httpToken"] === "string" ? swConfig["httpToken"] : "";
6420
+ if (token.length < 16) {
6421
+ token = generateRelayToken();
6422
+ }
6423
+ const label = (opts.label ?? swConfig["label"] ?? "OpenClaw").trim() || "OpenClaw";
6424
+ swConfig["transport"] = "http";
6425
+ swConfig["httpHost"] = host;
6426
+ swConfig["httpPort"] = port;
6427
+ swConfig["httpToken"] = token;
6428
+ swConfig["label"] = label;
6429
+ swEntry["config"] = swConfig;
6430
+ entries["stagewhisper"] = swEntry;
6431
+ plugins["entries"] = entries;
6432
+ cfg["plugins"] = plugins;
6433
+ await api.runtime.config.writeConfigFile(cfg);
6434
+ const relayUrl = (opts.url ?? "").trim() || `http://${host}:${port}`;
6435
+ const code = encodePairingCode(relayUrl, token, label);
6436
+ console.log("\nStageWhisper Free pairing code:\n");
6437
+ console.log(` ${code}
6438
+ `);
6439
+ console.log("Paste it into StageWhisper Free under Settings \u2192 Connection.");
6440
+ console.log(
6441
+ "Restart the gateway so the HTTP transport listens: openclaw gateway restart"
6442
+ );
6443
+ if (relayUrl.startsWith("http://127.0.0.1")) {
6444
+ console.log(
6445
+ "\nRunning on a remote host? Tunnel the port from the machine running Free:"
6446
+ );
6447
+ console.log(` ssh -L ${port}:127.0.0.1:${port} <this-host>
6448
+ `);
6449
+ }
6450
+ } catch (err) {
6451
+ console.error(`
6452
+ \u2717 Failed to generate pairing code: ${err}
6453
+ `);
6454
+ process.exit(1);
6455
+ }
6456
+ });
6457
+ sw.command("unpair").description("Remove StageWhisper pairing (run before `openclaw plugins uninstall`)").option("--keep-responses", "Keep the OpenResponses HTTP API enabled after unpair").action(async (opts) => {
5342
6458
  try {
5343
6459
  const cfg = await api.runtime.config.loadConfig();
5344
6460
  const plugins = cfg["plugins"] ?? {};
@@ -5356,15 +6472,19 @@ var plugin_main_default = definePluginEntry({
5356
6472
  }
5357
6473
  if (!opts.keepResponses) {
5358
6474
  const gw = cfg["gateway"];
5359
- const http = gw?.["http"];
5360
- const endpoints = http?.["endpoints"];
6475
+ const http2 = gw?.["http"];
6476
+ const endpoints = http2?.["endpoints"];
5361
6477
  const responses = endpoints?.["responses"];
5362
6478
  if (responses?.["enabled"] === true) {
5363
6479
  delete responses["enabled"];
5364
- if (Object.keys(responses).length === 0 && endpoints) delete endpoints["responses"];
5365
- if (endpoints && Object.keys(endpoints).length === 0 && http) delete http["endpoints"];
5366
- if (http && Object.keys(http).length === 0 && gw) delete gw["http"];
5367
- console.log(" \u2139 Disabled gateway.http.endpoints.responses. Use --keep-responses to preserve it.");
6480
+ if (Object.keys(responses).length === 0 && endpoints)
6481
+ delete endpoints["responses"];
6482
+ if (endpoints && Object.keys(endpoints).length === 0 && http2)
6483
+ delete http2["endpoints"];
6484
+ if (http2 && Object.keys(http2).length === 0 && gw) delete gw["http"];
6485
+ console.log(
6486
+ " \u2139 Disabled gateway.http.endpoints.responses. Use --keep-responses to preserve it."
6487
+ );
5368
6488
  }
5369
6489
  }
5370
6490
  await api.runtime.config.writeConfigFile(cfg);
@@ -5378,7 +6498,11 @@ var plugin_main_default = definePluginEntry({
5378
6498
  process.exit(1);
5379
6499
  }
5380
6500
  });
5381
- sw.command("reasoning-check").description("Test reasoning capability against the local OpenResponses endpoint").option("--model <model>", "Model to use (omit to use your configured default)", "openclaw/default").action(async (opts) => {
6501
+ sw.command("reasoning-check").description("Test reasoning capability against the local OpenResponses endpoint").option(
6502
+ "--model <model>",
6503
+ "Model to use (omit to use your configured default)",
6504
+ "openclaw/default"
6505
+ ).action(async (opts) => {
5382
6506
  const { callOpenResponses: callOpenResponses2, isResponsesEndpointEnabled: isResponsesEndpointEnabled2 } = await Promise.resolve().then(() => (init_openresponses(), openresponses_exports));
5383
6507
  const modelLabel = opts.model === "openclaw/default" ? "default (configured)" : opts.model;
5384
6508
  const cfg = api.config;
@@ -5393,7 +6517,9 @@ var plugin_main_default = definePluginEntry({
5393
6517
  console.log(` responses.enabled: ${responsesEnabled ? "\u2713 true" : "\u2717 false"}`);
5394
6518
  if (!responsesEnabled) {
5395
6519
  console.warn("\n\u26A0 responses.enabled is false in the running config.");
5396
- console.warn(" The plugin auto-enables it on startup \u2014 restart the gateway if you haven't:");
6520
+ console.warn(
6521
+ " The plugin auto-enables it on startup \u2014 restart the gateway if you haven't:"
6522
+ );
5397
6523
  console.warn(" openclaw gateway restart\n");
5398
6524
  }
5399
6525
  if (!hasToken) {
@@ -5467,9 +6593,7 @@ Testing reasoning with model: ${modelLabel}`);
5467
6593
  } catch (err) {
5468
6594
  const elapsed = Date.now() - start;
5469
6595
  console.error(`\u2717 Reasoning check failed after ${elapsed}ms`);
5470
- console.error(
5471
- ` Error: ${err instanceof Error ? err.message : String(err)}`
5472
- );
6596
+ console.error(` Error: ${err instanceof Error ? err.message : String(err)}`);
5473
6597
  process.exitCode = 1;
5474
6598
  }
5475
6599
  });
@@ -5478,9 +6602,7 @@ Testing reasoning with model: ${modelLabel}`);
5478
6602
  const configured = !!(cfg?.["integrationId"] && cfg?.["relayToken"]);
5479
6603
  if (!configured) {
5480
6604
  console.log("\nStageWhisper: not paired\n");
5481
- console.log(
5482
- " Run: openclaw stagewhisper pair --code <CODE> [--api-url <URL>]\n"
5483
- );
6605
+ console.log(" Run: openclaw stagewhisper pair --code <CODE> [--api-url <URL>]\n");
5484
6606
  console.log(
5485
6607
  " Get the pairing code from StageWhisper desktop: Settings \u2192 Assistant \u2192 Generate Pairing Code\n"
5486
6608
  );
@@ -5514,10 +6636,16 @@ StageWhisper:`);
5514
6636
  if (api.registrationMode !== "full") return;
5515
6637
  ensureResponsesEndpoint(api);
5516
6638
  setRuntime(api.runtime);
5517
- const service = createRelayService(api);
5518
- api.registerService(service);
6639
+ const transportMode = resolveTransportMode(api);
6640
+ api.logger.info(`StageWhisper transport: ${transportMode}`);
6641
+ if (transportMode === "http") {
6642
+ api.registerService(createHttpTransportService(api));
6643
+ } else {
6644
+ api.registerService(createRelayService(api));
6645
+ }
5519
6646
  }
5520
6647
  });
5521
6648
  export {
5522
- plugin_main_default as default
6649
+ plugin_main_default as default,
6650
+ resolveHttpTransportToken
5523
6651
  };