@stagewhisper/stagewhisper 0.60.0 → 0.64.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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.js +1163 -91
- package/openclaw.plugin.json +29 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -252,8 +252,8 @@ function resolveGatewayConfig(api) {
|
|
|
252
252
|
function isResponsesEndpointEnabled(api) {
|
|
253
253
|
const cfg = api.config;
|
|
254
254
|
const gw = cfg?.gateway ?? {};
|
|
255
|
-
const
|
|
256
|
-
const endpoints =
|
|
255
|
+
const http2 = gw?.http ?? {};
|
|
256
|
+
const endpoints = http2?.endpoints ?? {};
|
|
257
257
|
const responses = endpoints?.responses ?? {};
|
|
258
258
|
return responses?.enabled === true;
|
|
259
259
|
}
|
|
@@ -431,7 +431,7 @@ function createHasher(hashCons) {
|
|
|
431
431
|
hashC.create = () => hashCons();
|
|
432
432
|
return hashC;
|
|
433
433
|
}
|
|
434
|
-
function
|
|
434
|
+
function randomBytes2(bytesLength = 32) {
|
|
435
435
|
if (crypto2 && typeof crypto2.getRandomValues === "function") {
|
|
436
436
|
return crypto2.getRandomValues(new Uint8Array(bytesLength));
|
|
437
437
|
}
|
|
@@ -2003,7 +2003,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
2003
2003
|
});
|
|
2004
2004
|
const { prehash } = eddsaOpts;
|
|
2005
2005
|
const { BASE, Fp: Fp2, Fn: Fn2 } = Point;
|
|
2006
|
-
const
|
|
2006
|
+
const randomBytes4 = eddsaOpts.randomBytes || randomBytes2;
|
|
2007
2007
|
const adjustScalarBytes2 = eddsaOpts.adjustScalarBytes || ((bytes) => bytes);
|
|
2008
2008
|
const domain = eddsaOpts.domain || ((data, ctx, phflag) => {
|
|
2009
2009
|
_abool2(phflag, "phflag");
|
|
@@ -2085,7 +2085,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
2085
2085
|
signature: 2 * _size,
|
|
2086
2086
|
seed: _size
|
|
2087
2087
|
};
|
|
2088
|
-
function randomSecretKey(seed =
|
|
2088
|
+
function randomSecretKey(seed = randomBytes4(lengths.seed)) {
|
|
2089
2089
|
return _abytes2(seed, lengths.seed, "seed");
|
|
2090
2090
|
}
|
|
2091
2091
|
function keygen(seed) {
|
|
@@ -2280,7 +2280,7 @@ function montgomery(curveDef) {
|
|
|
2280
2280
|
const is25519 = type === "x25519";
|
|
2281
2281
|
if (!is25519 && type !== "x448")
|
|
2282
2282
|
throw new Error("invalid type");
|
|
2283
|
-
const randomBytes_ = rand ||
|
|
2283
|
+
const randomBytes_ = rand || randomBytes2;
|
|
2284
2284
|
const montgomeryBits = is25519 ? 255 : 448;
|
|
2285
2285
|
const fieldLen = is25519 ? 32 : 56;
|
|
2286
2286
|
const Gu = is25519 ? BigInt(9) : BigInt(5);
|
|
@@ -3547,7 +3547,7 @@ var init_cryptoNode2 = __esm({
|
|
|
3547
3547
|
});
|
|
3548
3548
|
|
|
3549
3549
|
// node_modules/.pnpm/@noble+ciphers@1.3.0/node_modules/@noble/ciphers/esm/webcrypto.js
|
|
3550
|
-
function
|
|
3550
|
+
function randomBytes3(bytesLength = 32) {
|
|
3551
3551
|
if (crypto3 && typeof crypto3.getRandomValues === "function") {
|
|
3552
3552
|
return crypto3.getRandomValues(new Uint8Array(bytesLength));
|
|
3553
3553
|
}
|
|
@@ -3591,7 +3591,7 @@ function generateUUID() {
|
|
|
3591
3591
|
}
|
|
3592
3592
|
function seal(key, senderRole, sessionId, correlationId, contentType, plaintext) {
|
|
3593
3593
|
const messageId = generateUUID();
|
|
3594
|
-
const nonce =
|
|
3594
|
+
const nonce = randomBytes3(24);
|
|
3595
3595
|
const metadata = {
|
|
3596
3596
|
version: ENVELOPE_VERSION,
|
|
3597
3597
|
sender_role: senderRole,
|
|
@@ -3666,7 +3666,7 @@ var init_crypto = __esm({
|
|
|
3666
3666
|
this.publicKey = x25519.getPublicKey(secretKey);
|
|
3667
3667
|
}
|
|
3668
3668
|
static generate() {
|
|
3669
|
-
const secretKey =
|
|
3669
|
+
const secretKey = randomBytes3(32);
|
|
3670
3670
|
return new _IdentityKeypair(secretKey);
|
|
3671
3671
|
}
|
|
3672
3672
|
static fromSecretBytes(bytes) {
|
|
@@ -3987,6 +3987,987 @@ var stagewhisperPlugin = {
|
|
|
3987
3987
|
}
|
|
3988
3988
|
};
|
|
3989
3989
|
|
|
3990
|
+
// src/http-transport.ts
|
|
3991
|
+
import http from "node:http";
|
|
3992
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
3993
|
+
import { randomUUID, 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 = 16e3;
|
|
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 normalizeOrigin(url) {
|
|
4059
|
+
let parsed;
|
|
4060
|
+
try {
|
|
4061
|
+
parsed = new URL(url);
|
|
4062
|
+
} catch {
|
|
4063
|
+
return null;
|
|
4064
|
+
}
|
|
4065
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
4066
|
+
return parsed.origin.toLowerCase();
|
|
4067
|
+
}
|
|
4068
|
+
function allowedCallbackOrigins() {
|
|
4069
|
+
const raw = process.env["STAGEWHISPER_ALLOW_CALLBACK_URLS"];
|
|
4070
|
+
if (!raw) return /* @__PURE__ */ new Set();
|
|
4071
|
+
const origins = raw.split(",").map((entry) => normalizeOrigin(entry.trim())).filter((origin) => origin !== null);
|
|
4072
|
+
return new Set(origins);
|
|
4073
|
+
}
|
|
4074
|
+
function isAllowedCallbackUrl(url) {
|
|
4075
|
+
let parsed;
|
|
4076
|
+
try {
|
|
4077
|
+
parsed = new URL(url);
|
|
4078
|
+
} catch {
|
|
4079
|
+
return false;
|
|
4080
|
+
}
|
|
4081
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
4082
|
+
if (parsed.pathname !== "/" && parsed.pathname !== "") return false;
|
|
4083
|
+
if (parsed.search || parsed.hash) return false;
|
|
4084
|
+
if (allowedCallbackOrigins().has(parsed.origin.toLowerCase())) return true;
|
|
4085
|
+
if (allowedIngressHosts().size === 0 && isLoopbackCallbackUrl(url)) return true;
|
|
4086
|
+
return false;
|
|
4087
|
+
}
|
|
4088
|
+
function allowedIngressHosts() {
|
|
4089
|
+
const raw = process.env["STAGEWHISPER_ALLOW_INGRESS_HOSTS"];
|
|
4090
|
+
if (!raw) return /* @__PURE__ */ new Set();
|
|
4091
|
+
const hosts = raw.split(",").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
|
|
4092
|
+
return new Set(hosts);
|
|
4093
|
+
}
|
|
4094
|
+
function isAllowedHostHeader(host) {
|
|
4095
|
+
if (!host) return false;
|
|
4096
|
+
const trimmed = host.trim().toLowerCase();
|
|
4097
|
+
if (!trimmed) return false;
|
|
4098
|
+
const colonIdx = trimmed.lastIndexOf(":");
|
|
4099
|
+
const hostname = colonIdx === -1 ? trimmed : trimmed.slice(0, colonIdx);
|
|
4100
|
+
const port = colonIdx === -1 ? "" : trimmed.slice(colonIdx + 1);
|
|
4101
|
+
if (port && !/^\d+$/.test(port)) return false;
|
|
4102
|
+
if (ALLOWED_HOSTNAMES.has(hostname)) return true;
|
|
4103
|
+
return allowedIngressHosts().has(hostname);
|
|
4104
|
+
}
|
|
4105
|
+
function buildChatId(sessionId, reason) {
|
|
4106
|
+
if (reason === "transcript_chunk") return `sw:${sessionId}:reasoning`;
|
|
4107
|
+
if (reason === "chat_message") return `sw:${sessionId}:chat`;
|
|
4108
|
+
return null;
|
|
4109
|
+
}
|
|
4110
|
+
function buildCallbackUrl(baseUrl, taskId) {
|
|
4111
|
+
return `${baseUrl.replace(/\/$/, "")}/tasks/${taskId}`;
|
|
4112
|
+
}
|
|
4113
|
+
function applyPrelude(text, prelude) {
|
|
4114
|
+
if (!prelude) return text;
|
|
4115
|
+
return `[Context: ${prelude}]
|
|
4116
|
+
|
|
4117
|
+
${text}`;
|
|
4118
|
+
}
|
|
4119
|
+
function validateHttpTaskRequest(body) {
|
|
4120
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
4121
|
+
return { ok: false, error: "body must be a JSON object" };
|
|
4122
|
+
}
|
|
4123
|
+
const obj = body;
|
|
4124
|
+
const task_id = obj["task_id"];
|
|
4125
|
+
if (typeof task_id !== "string" || task_id.length === 0) {
|
|
4126
|
+
return { ok: false, error: "task_id must be a non-empty string" };
|
|
4127
|
+
}
|
|
4128
|
+
if (!TASK_ID_REGEX.test(task_id)) {
|
|
4129
|
+
return { ok: false, error: "task_id must match ^[0-9a-f-]{36}$" };
|
|
4130
|
+
}
|
|
4131
|
+
const session_id = obj["session_id"];
|
|
4132
|
+
if (typeof session_id !== "string" || session_id.length === 0) {
|
|
4133
|
+
return { ok: false, error: "session_id must be a non-empty string" };
|
|
4134
|
+
}
|
|
4135
|
+
if (session_id.length > MAX_SESSION_ID_CHARS) {
|
|
4136
|
+
return {
|
|
4137
|
+
ok: false,
|
|
4138
|
+
error: `session_id must be <= ${MAX_SESSION_ID_CHARS} chars`
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
const reason = obj["reason"];
|
|
4142
|
+
if (typeof reason !== "string" || reason.length === 0) {
|
|
4143
|
+
return { ok: false, error: "reason must be a non-empty string" };
|
|
4144
|
+
}
|
|
4145
|
+
if (!ALLOWED_REASONS.has(reason)) {
|
|
4146
|
+
return {
|
|
4147
|
+
ok: false,
|
|
4148
|
+
error: `reason must be one of ${Array.from(ALLOWED_REASONS).join(", ")}`
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4151
|
+
const payload = obj["payload"];
|
|
4152
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
4153
|
+
return { ok: false, error: "payload must be an object" };
|
|
4154
|
+
}
|
|
4155
|
+
const payloadObj = payload;
|
|
4156
|
+
const text = payloadObj["text"];
|
|
4157
|
+
if (typeof text !== "string") {
|
|
4158
|
+
return { ok: false, error: "payload.text must be a string" };
|
|
4159
|
+
}
|
|
4160
|
+
if (text.length > MAX_PAYLOAD_TEXT_CHARS) {
|
|
4161
|
+
return {
|
|
4162
|
+
ok: false,
|
|
4163
|
+
error: `payload.text must be <= ${MAX_PAYLOAD_TEXT_CHARS} chars`
|
|
4164
|
+
};
|
|
4165
|
+
}
|
|
4166
|
+
let callback;
|
|
4167
|
+
const callbackRaw = obj["callback"];
|
|
4168
|
+
if (callbackRaw !== void 0 && callbackRaw !== null) {
|
|
4169
|
+
if (typeof callbackRaw !== "object" || Array.isArray(callbackRaw)) {
|
|
4170
|
+
return { ok: false, error: "callback must be an object" };
|
|
4171
|
+
}
|
|
4172
|
+
const cbObj = callbackRaw;
|
|
4173
|
+
const cbUrl = cbObj["url"];
|
|
4174
|
+
const cbToken = cbObj["token"];
|
|
4175
|
+
if (typeof cbUrl !== "string" || cbUrl.length === 0) {
|
|
4176
|
+
return { ok: false, error: "callback.url must be a non-empty string" };
|
|
4177
|
+
}
|
|
4178
|
+
if (!isAllowedCallbackUrl(cbUrl)) {
|
|
4179
|
+
return {
|
|
4180
|
+
ok: false,
|
|
4181
|
+
error: "callback.url must be a loopback base URL (http://127.0.0.1:PORT) or an origin listed in STAGEWHISPER_ALLOW_CALLBACK_URLS, with no path"
|
|
4182
|
+
};
|
|
4183
|
+
}
|
|
4184
|
+
if (typeof cbToken !== "string" || cbToken.length < 16) {
|
|
4185
|
+
return {
|
|
4186
|
+
ok: false,
|
|
4187
|
+
error: "callback.token must be a string of length >= 16"
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
callback = { url: cbUrl, token: cbToken };
|
|
4191
|
+
}
|
|
4192
|
+
const chatIdRaw = obj["chat_id"];
|
|
4193
|
+
const chat_id = typeof chatIdRaw === "string" && chatIdRaw.length > 0 ? chatIdRaw : buildChatId(session_id, reason) ?? void 0;
|
|
4194
|
+
const req = {
|
|
4195
|
+
task_id,
|
|
4196
|
+
session_id,
|
|
4197
|
+
reason,
|
|
4198
|
+
...chat_id ? { chat_id } : {},
|
|
4199
|
+
occurred_at: typeof obj["occurred_at"] === "string" ? obj["occurred_at"] : void 0,
|
|
4200
|
+
payload: {
|
|
4201
|
+
text,
|
|
4202
|
+
ts_start_ms: typeof payloadObj["ts_start_ms"] === "number" ? payloadObj["ts_start_ms"] : void 0,
|
|
4203
|
+
ts_end_ms: typeof payloadObj["ts_end_ms"] === "number" ? payloadObj["ts_end_ms"] : void 0,
|
|
4204
|
+
is_final: typeof payloadObj["is_final"] === "boolean" ? payloadObj["is_final"] : void 0,
|
|
4205
|
+
user_message_id: typeof payloadObj["user_message_id"] === "string" ? payloadObj["user_message_id"] : void 0
|
|
4206
|
+
},
|
|
4207
|
+
...callback ? { callback } : {}
|
|
4208
|
+
};
|
|
4209
|
+
return { ok: true, req };
|
|
4210
|
+
}
|
|
4211
|
+
function httpTaskRequestToTaskPayload(req, overrides) {
|
|
4212
|
+
if (req.reason === "system_prelude") return null;
|
|
4213
|
+
const text = overrides?.text ?? req.payload.text;
|
|
4214
|
+
const evidence = {
|
|
4215
|
+
transcript_excerpt: text
|
|
4216
|
+
};
|
|
4217
|
+
if (typeof req.payload.ts_start_ms === "number") {
|
|
4218
|
+
evidence["ts_start_ms"] = req.payload.ts_start_ms;
|
|
4219
|
+
}
|
|
4220
|
+
if (typeof req.payload.ts_end_ms === "number") {
|
|
4221
|
+
evidence["ts_end_ms"] = req.payload.ts_end_ms;
|
|
4222
|
+
}
|
|
4223
|
+
if (typeof req.payload.is_final === "boolean") {
|
|
4224
|
+
evidence["is_final"] = req.payload.is_final;
|
|
4225
|
+
}
|
|
4226
|
+
if (typeof req.payload.user_message_id === "string") {
|
|
4227
|
+
evidence["user_message_id"] = req.payload.user_message_id;
|
|
4228
|
+
}
|
|
4229
|
+
if (req.chat_id) {
|
|
4230
|
+
evidence["chat_id"] = req.chat_id;
|
|
4231
|
+
}
|
|
4232
|
+
const isChat = req.reason === "chat_message";
|
|
4233
|
+
return {
|
|
4234
|
+
id: req.task_id,
|
|
4235
|
+
session_id: req.session_id,
|
|
4236
|
+
title: isChat ? `Chat message in session ${req.session_id}` : `Transcript chunk from session ${req.session_id}`,
|
|
4237
|
+
request_text: text,
|
|
4238
|
+
action_type: req.reason,
|
|
4239
|
+
status: "delivered",
|
|
4240
|
+
evidence_payload: evidence,
|
|
4241
|
+
created_at: req.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
// src/http-transport.ts
|
|
4246
|
+
var IDEMPOTENCY_MAX_SIZE = 1024;
|
|
4247
|
+
var IDEMPOTENCY_TTL_MS = 5 * 60 * 1e3;
|
|
4248
|
+
var CALLBACK_TIMEOUT_MS = 5e3;
|
|
4249
|
+
var CALLBACK_MAX_ATTEMPTS = 4;
|
|
4250
|
+
var CALLBACK_RETRY_BASE_MS = 250;
|
|
4251
|
+
var POLL_INTERVAL_MS = 500;
|
|
4252
|
+
function resolveRuntimeEvents(api) {
|
|
4253
|
+
const runtime = api.runtime;
|
|
4254
|
+
const events = runtime?.events;
|
|
4255
|
+
if (!events) return null;
|
|
4256
|
+
if (typeof events.onSessionTranscriptUpdate !== "function") return null;
|
|
4257
|
+
return events;
|
|
4258
|
+
}
|
|
4259
|
+
var INCOMING_PATH = "/v1/incoming";
|
|
4260
|
+
var PING_PATH = "/v1/ping";
|
|
4261
|
+
function constantTimeTokenEqual(provided, expected) {
|
|
4262
|
+
const a = Buffer2.from(provided);
|
|
4263
|
+
const b = Buffer2.from(expected);
|
|
4264
|
+
if (a.length !== b.length) {
|
|
4265
|
+
const sink = Buffer2.alloc(Math.max(a.length, b.length));
|
|
4266
|
+
timingSafeEqual(sink, sink);
|
|
4267
|
+
return false;
|
|
4268
|
+
}
|
|
4269
|
+
return timingSafeEqual(a, b);
|
|
4270
|
+
}
|
|
4271
|
+
function extractBearerToken(header) {
|
|
4272
|
+
if (!header) return null;
|
|
4273
|
+
const match = /^Bearer\s+(\S.*)$/i.exec(header.trim());
|
|
4274
|
+
if (!match) return null;
|
|
4275
|
+
return match[1]?.trim() ?? null;
|
|
4276
|
+
}
|
|
4277
|
+
function isLoopbackAddress(addr) {
|
|
4278
|
+
if (!addr) return false;
|
|
4279
|
+
if (addr === "127.0.0.1" || addr === "::1") return true;
|
|
4280
|
+
if (addr === "::ffff:127.0.0.1") return true;
|
|
4281
|
+
return false;
|
|
4282
|
+
}
|
|
4283
|
+
function readBody(req, maxBytes) {
|
|
4284
|
+
return new Promise((resolve, reject) => {
|
|
4285
|
+
const chunks = [];
|
|
4286
|
+
let total = 0;
|
|
4287
|
+
req.on("data", (chunk) => {
|
|
4288
|
+
total += chunk.length;
|
|
4289
|
+
if (total > maxBytes) {
|
|
4290
|
+
reject(new Error("body_too_large"));
|
|
4291
|
+
req.destroy();
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
4294
|
+
chunks.push(chunk);
|
|
4295
|
+
});
|
|
4296
|
+
req.on("end", () => {
|
|
4297
|
+
resolve(Buffer2.concat(chunks).toString("utf8"));
|
|
4298
|
+
});
|
|
4299
|
+
req.on("error", (err) => reject(err));
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
function jsonResponse(status, body) {
|
|
4303
|
+
const serialized = JSON.stringify(body);
|
|
4304
|
+
return {
|
|
4305
|
+
status,
|
|
4306
|
+
headers: {
|
|
4307
|
+
"Content-Type": "application/json",
|
|
4308
|
+
"Content-Length": String(Buffer2.byteLength(serialized))
|
|
4309
|
+
},
|
|
4310
|
+
body: serialized
|
|
4311
|
+
};
|
|
4312
|
+
}
|
|
4313
|
+
function extractContentFromMessage(msg) {
|
|
4314
|
+
const content = msg["content"];
|
|
4315
|
+
if (typeof content === "string") return content;
|
|
4316
|
+
if (Array.isArray(content)) {
|
|
4317
|
+
for (const part of content) {
|
|
4318
|
+
if (typeof part === "object" && part !== null && part["type"] === "text" && typeof part["text"] === "string") {
|
|
4319
|
+
return part["text"];
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
return null;
|
|
4324
|
+
}
|
|
4325
|
+
function createHttpTransport(options) {
|
|
4326
|
+
const { api } = options;
|
|
4327
|
+
const host = options.host ?? "127.0.0.1";
|
|
4328
|
+
const port = options.port ?? 8765;
|
|
4329
|
+
const token = options.token;
|
|
4330
|
+
const callbackFetch = options.callbackFetch ?? fetch;
|
|
4331
|
+
if (!token || token.length < 16) {
|
|
4332
|
+
throw new Error("http-transport: token must be at least 16 characters");
|
|
4333
|
+
}
|
|
4334
|
+
const idempotency = /* @__PURE__ */ new Map();
|
|
4335
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
4336
|
+
const chatTasks = /* @__PURE__ */ new Map();
|
|
4337
|
+
const pendingPreludes = /* @__PURE__ */ new Map();
|
|
4338
|
+
function rememberResult(taskId, status, body) {
|
|
4339
|
+
idempotency.set(taskId, {
|
|
4340
|
+
status,
|
|
4341
|
+
body,
|
|
4342
|
+
expiresAt: Date.now() + IDEMPOTENCY_TTL_MS
|
|
4343
|
+
});
|
|
4344
|
+
while (idempotency.size > IDEMPOTENCY_MAX_SIZE) {
|
|
4345
|
+
const oldestKey = idempotency.keys().next().value;
|
|
4346
|
+
if (oldestKey === void 0) break;
|
|
4347
|
+
idempotency.delete(oldestKey);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
function evictExpired() {
|
|
4351
|
+
const now = Date.now();
|
|
4352
|
+
for (const [key, entry] of idempotency) {
|
|
4353
|
+
if (entry.expiresAt <= now) idempotency.delete(key);
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
function recordTerminalCallback(taskId, callback, body) {
|
|
4357
|
+
const entry = idempotency.get(taskId);
|
|
4358
|
+
if (!entry) return;
|
|
4359
|
+
entry.terminal = { callback, body };
|
|
4360
|
+
}
|
|
4361
|
+
function consumePrelude(sessionId) {
|
|
4362
|
+
const prelude = pendingPreludes.get(sessionId);
|
|
4363
|
+
if (prelude !== void 0) pendingPreludes.delete(sessionId);
|
|
4364
|
+
return prelude;
|
|
4365
|
+
}
|
|
4366
|
+
async function collectAssistantRepliesAfterMarker(sessionKey, markers) {
|
|
4367
|
+
const session = await api.runtime.subagent.getSessionMessages({
|
|
4368
|
+
sessionKey,
|
|
4369
|
+
limit: 100
|
|
4370
|
+
});
|
|
4371
|
+
const messages = session.messages;
|
|
4372
|
+
let markerIndex = -1;
|
|
4373
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
4374
|
+
const msg = messages[i];
|
|
4375
|
+
if (msg["role"] !== "user") continue;
|
|
4376
|
+
const text = extractContentFromMessage(msg) ?? "";
|
|
4377
|
+
if (markers.some((m) => text.includes(m))) {
|
|
4378
|
+
markerIndex = i;
|
|
4379
|
+
break;
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
if (markerIndex === -1) return [];
|
|
4383
|
+
const replies = [];
|
|
4384
|
+
for (let j = markerIndex + 1; j < messages.length; j++) {
|
|
4385
|
+
const msg = messages[j];
|
|
4386
|
+
const role = msg["role"];
|
|
4387
|
+
if (role === "user") break;
|
|
4388
|
+
if (role === "assistant" || role === "model") {
|
|
4389
|
+
const text = extractContentFromMessage(msg);
|
|
4390
|
+
if (text) replies.push(text);
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
return replies;
|
|
4394
|
+
}
|
|
4395
|
+
async function postCallback(callback, taskId, body) {
|
|
4396
|
+
const url = buildCallbackUrl(callback.url, taskId);
|
|
4397
|
+
const serialized = JSON.stringify(body);
|
|
4398
|
+
let lastError = null;
|
|
4399
|
+
for (let attempt = 0; attempt < CALLBACK_MAX_ATTEMPTS; attempt++) {
|
|
4400
|
+
const controller = new AbortController();
|
|
4401
|
+
const timeoutId = setTimeout(() => controller.abort(), CALLBACK_TIMEOUT_MS);
|
|
4402
|
+
try {
|
|
4403
|
+
const res = await callbackFetch(url, {
|
|
4404
|
+
method: "POST",
|
|
4405
|
+
headers: {
|
|
4406
|
+
"Content-Type": "application/json",
|
|
4407
|
+
Authorization: `Bearer ${callback.token}`
|
|
4408
|
+
},
|
|
4409
|
+
body: serialized,
|
|
4410
|
+
signal: controller.signal
|
|
4411
|
+
});
|
|
4412
|
+
clearTimeout(timeoutId);
|
|
4413
|
+
if (res.ok) return;
|
|
4414
|
+
lastError = new Error(`callback returned status ${res.status}`);
|
|
4415
|
+
if (res.status >= 400 && res.status < 500) {
|
|
4416
|
+
throw lastError;
|
|
4417
|
+
}
|
|
4418
|
+
} catch (err) {
|
|
4419
|
+
clearTimeout(timeoutId);
|
|
4420
|
+
lastError = err;
|
|
4421
|
+
}
|
|
4422
|
+
if (attempt + 1 < CALLBACK_MAX_ATTEMPTS) {
|
|
4423
|
+
await new Promise(
|
|
4424
|
+
(r) => setTimeout(r, CALLBACK_RETRY_BASE_MS * 2 ** attempt)
|
|
4425
|
+
);
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "callback_failed"));
|
|
4429
|
+
}
|
|
4430
|
+
async function redeliverTerminalCallback(record, taskId) {
|
|
4431
|
+
try {
|
|
4432
|
+
await postCallback(record.callback, taskId, record.body);
|
|
4433
|
+
api.logger.info(
|
|
4434
|
+
`[stagewhisper-http] redelivered terminal callback for ${taskId}`
|
|
4435
|
+
);
|
|
4436
|
+
} catch (err) {
|
|
4437
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4438
|
+
api.logger.error(
|
|
4439
|
+
`[stagewhisper-http] terminal callback redelivery failed for ${taskId}: ${errMsg}`
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
async function runReplyTaskAsync(taskRequest, kind) {
|
|
4444
|
+
const callback = taskRequest.callback;
|
|
4445
|
+
if (!callback) {
|
|
4446
|
+
api.logger.error(
|
|
4447
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} missing callback after validation`
|
|
4448
|
+
);
|
|
4449
|
+
return;
|
|
4450
|
+
}
|
|
4451
|
+
const userMessageId = taskRequest.payload.user_message_id;
|
|
4452
|
+
const effectiveText = applyPrelude(
|
|
4453
|
+
taskRequest.payload.text,
|
|
4454
|
+
consumePrelude(taskRequest.session_id)
|
|
4455
|
+
);
|
|
4456
|
+
const taskPayload = httpTaskRequestToTaskPayload(taskRequest, {
|
|
4457
|
+
text: effectiveText
|
|
4458
|
+
});
|
|
4459
|
+
if (!taskPayload) {
|
|
4460
|
+
api.logger.error(
|
|
4461
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} produced no payload`
|
|
4462
|
+
);
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
const peerId = `sw:${taskRequest.session_id}:${kind}`;
|
|
4466
|
+
const sessionKey = buildAgentSessionKey({
|
|
4467
|
+
agentId: "default",
|
|
4468
|
+
channel: "stagewhisper",
|
|
4469
|
+
peer: { kind: "direct", id: peerId },
|
|
4470
|
+
dmScope: "per-channel-peer"
|
|
4471
|
+
});
|
|
4472
|
+
const chatIdReason = kind === "chat" ? "chat_message" : "transcript_chunk";
|
|
4473
|
+
const chatId = taskRequest.chat_id ?? buildChatId(taskRequest.session_id, chatIdReason);
|
|
4474
|
+
const markers = [`StageWhisper task: ${taskRequest.task_id}`];
|
|
4475
|
+
if (userMessageId) markers.push(`[StageWhisper chat: ${userMessageId}]`);
|
|
4476
|
+
let forwardedCount = 0;
|
|
4477
|
+
const postMessage = async (replyText) => {
|
|
4478
|
+
const messageId = randomUUID();
|
|
4479
|
+
const body = {
|
|
4480
|
+
task_id: taskRequest.task_id,
|
|
4481
|
+
session_id: taskRequest.session_id,
|
|
4482
|
+
chat_id: chatId,
|
|
4483
|
+
user_message_id: userMessageId ?? null,
|
|
4484
|
+
message_id: messageId,
|
|
4485
|
+
status: "message",
|
|
4486
|
+
reply_text: replyText,
|
|
4487
|
+
occurred_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4488
|
+
};
|
|
4489
|
+
recordTerminalCallback(taskRequest.task_id, callback, body);
|
|
4490
|
+
try {
|
|
4491
|
+
await postCallback(callback, taskRequest.task_id, body);
|
|
4492
|
+
api.logger.info(
|
|
4493
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} forwarded message ${messageId}`
|
|
4494
|
+
);
|
|
4495
|
+
} catch (err) {
|
|
4496
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4497
|
+
api.logger.error(
|
|
4498
|
+
`[stagewhisper-http] callback POST failed for ${taskRequest.task_id}: ${errMsg}`
|
|
4499
|
+
);
|
|
4500
|
+
}
|
|
4501
|
+
};
|
|
4502
|
+
const postTyping = async () => {
|
|
4503
|
+
try {
|
|
4504
|
+
await postCallback(callback, taskRequest.task_id, {
|
|
4505
|
+
task_id: taskRequest.task_id,
|
|
4506
|
+
session_id: taskRequest.session_id,
|
|
4507
|
+
chat_id: chatId,
|
|
4508
|
+
user_message_id: userMessageId ?? null,
|
|
4509
|
+
status: "typing",
|
|
4510
|
+
label: "thinking"
|
|
4511
|
+
});
|
|
4512
|
+
} catch (err) {
|
|
4513
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4514
|
+
api.logger.warn(
|
|
4515
|
+
`[stagewhisper-http] typing callback failed for ${taskRequest.task_id}: ${errMsg}`
|
|
4516
|
+
);
|
|
4517
|
+
}
|
|
4518
|
+
};
|
|
4519
|
+
const events = resolveRuntimeEvents(api);
|
|
4520
|
+
let unsubscribeTranscript = null;
|
|
4521
|
+
let pollDone = false;
|
|
4522
|
+
let flushing = null;
|
|
4523
|
+
let flushQueued = false;
|
|
4524
|
+
let wakePoll = () => {
|
|
4525
|
+
};
|
|
4526
|
+
const sleepUntilPoll = () => new Promise((resolve) => {
|
|
4527
|
+
const timer = setTimeout(() => {
|
|
4528
|
+
wakePoll = () => {
|
|
4529
|
+
};
|
|
4530
|
+
resolve();
|
|
4531
|
+
}, POLL_INTERVAL_MS);
|
|
4532
|
+
wakePoll = () => {
|
|
4533
|
+
clearTimeout(timer);
|
|
4534
|
+
wakePoll = () => {
|
|
4535
|
+
};
|
|
4536
|
+
resolve();
|
|
4537
|
+
};
|
|
4538
|
+
});
|
|
4539
|
+
const runFlushOnce = async () => {
|
|
4540
|
+
try {
|
|
4541
|
+
const replies = await collectAssistantRepliesAfterMarker(
|
|
4542
|
+
sessionKey,
|
|
4543
|
+
markers
|
|
4544
|
+
);
|
|
4545
|
+
while (forwardedCount < replies.length) {
|
|
4546
|
+
const reply = replies[forwardedCount];
|
|
4547
|
+
forwardedCount += 1;
|
|
4548
|
+
if (reply.trim()) await postMessage(reply);
|
|
4549
|
+
}
|
|
4550
|
+
} catch (err) {
|
|
4551
|
+
api.logger.warn(
|
|
4552
|
+
`[stagewhisper-http] session flush failed for ${taskRequest.task_id}: ${err}`
|
|
4553
|
+
);
|
|
4554
|
+
}
|
|
4555
|
+
};
|
|
4556
|
+
const flushFromSession = async () => {
|
|
4557
|
+
if (flushing) {
|
|
4558
|
+
flushQueued = true;
|
|
4559
|
+
await flushing;
|
|
4560
|
+
return;
|
|
4561
|
+
}
|
|
4562
|
+
flushing = (async () => {
|
|
4563
|
+
await runFlushOnce();
|
|
4564
|
+
while (flushQueued) {
|
|
4565
|
+
flushQueued = false;
|
|
4566
|
+
await runFlushOnce();
|
|
4567
|
+
}
|
|
4568
|
+
})();
|
|
4569
|
+
try {
|
|
4570
|
+
await flushing;
|
|
4571
|
+
} finally {
|
|
4572
|
+
flushing = null;
|
|
4573
|
+
}
|
|
4574
|
+
};
|
|
4575
|
+
try {
|
|
4576
|
+
const dispatch = await dispatchTaskToAgent({
|
|
4577
|
+
api,
|
|
4578
|
+
task: taskPayload,
|
|
4579
|
+
deliver: true,
|
|
4580
|
+
idempotencyKey: `sw-http-task-${taskRequest.task_id}`,
|
|
4581
|
+
sessionPeerId: peerId
|
|
4582
|
+
});
|
|
4583
|
+
api.logger.info(
|
|
4584
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} dispatched (runId: ${dispatch.runId})`
|
|
4585
|
+
);
|
|
4586
|
+
const runId = dispatch.runId;
|
|
4587
|
+
await postTyping();
|
|
4588
|
+
if (events?.onSessionTranscriptUpdate) {
|
|
4589
|
+
unsubscribeTranscript = events.onSessionTranscriptUpdate((update) => {
|
|
4590
|
+
if (update.sessionKey && update.sessionKey !== sessionKey) return;
|
|
4591
|
+
const message = update.message;
|
|
4592
|
+
const role = message?.["role"];
|
|
4593
|
+
if (role !== void 0 && role !== "assistant" && role !== "model") {
|
|
4594
|
+
return;
|
|
4595
|
+
}
|
|
4596
|
+
void flushFromSession();
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
let pollLoop = Promise.resolve();
|
|
4600
|
+
if (!events?.onSessionTranscriptUpdate) {
|
|
4601
|
+
pollLoop = (async () => {
|
|
4602
|
+
while (!pollDone) {
|
|
4603
|
+
await sleepUntilPoll();
|
|
4604
|
+
if (pollDone) break;
|
|
4605
|
+
await flushFromSession();
|
|
4606
|
+
}
|
|
4607
|
+
})();
|
|
4608
|
+
}
|
|
4609
|
+
const waitResult = await api.runtime.subagent.waitForRun({
|
|
4610
|
+
runId
|
|
4611
|
+
});
|
|
4612
|
+
pollDone = true;
|
|
4613
|
+
wakePoll();
|
|
4614
|
+
await pollLoop;
|
|
4615
|
+
await flushFromSession();
|
|
4616
|
+
if (waitResult.status === "error") {
|
|
4617
|
+
api.logger.error(
|
|
4618
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} agent error: ${waitResult.error}`
|
|
4619
|
+
);
|
|
4620
|
+
try {
|
|
4621
|
+
await postCallback(callback, taskRequest.task_id, {
|
|
4622
|
+
task_id: taskRequest.task_id,
|
|
4623
|
+
session_id: taskRequest.session_id,
|
|
4624
|
+
chat_id: chatId,
|
|
4625
|
+
user_message_id: userMessageId ?? null,
|
|
4626
|
+
status: "errored",
|
|
4627
|
+
error_code: "agent_error",
|
|
4628
|
+
error_message: waitResult.error ?? "agent run error"
|
|
4629
|
+
});
|
|
4630
|
+
} catch (postErr) {
|
|
4631
|
+
api.logger.error(
|
|
4632
|
+
`[stagewhisper-http] errored callback failed for ${taskRequest.task_id}: ${postErr}`
|
|
4633
|
+
);
|
|
4634
|
+
}
|
|
4635
|
+
} else {
|
|
4636
|
+
api.logger.info(
|
|
4637
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} turn settled (${waitResult.status})`
|
|
4638
|
+
);
|
|
4639
|
+
}
|
|
4640
|
+
} catch (err) {
|
|
4641
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4642
|
+
api.logger.error(
|
|
4643
|
+
`[stagewhisper-http] ${kind} task ${taskRequest.task_id} threw: ${errMsg}`
|
|
4644
|
+
);
|
|
4645
|
+
try {
|
|
4646
|
+
await postCallback(callback, taskRequest.task_id, {
|
|
4647
|
+
task_id: taskRequest.task_id,
|
|
4648
|
+
session_id: taskRequest.session_id,
|
|
4649
|
+
chat_id: chatId,
|
|
4650
|
+
user_message_id: userMessageId ?? null,
|
|
4651
|
+
status: "errored",
|
|
4652
|
+
error_code: "execution_error",
|
|
4653
|
+
error_message: errMsg
|
|
4654
|
+
});
|
|
4655
|
+
} catch (postErr) {
|
|
4656
|
+
api.logger.error(
|
|
4657
|
+
`[stagewhisper-http] errored callback failed for ${taskRequest.task_id}: ${postErr}`
|
|
4658
|
+
);
|
|
4659
|
+
}
|
|
4660
|
+
} finally {
|
|
4661
|
+
pollDone = true;
|
|
4662
|
+
wakePoll();
|
|
4663
|
+
unsubscribeTranscript?.();
|
|
4664
|
+
inflight.delete(taskRequest.task_id);
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
async function handleChatMessageRequest(taskReq) {
|
|
4668
|
+
if (!taskReq.callback) {
|
|
4669
|
+
return jsonResponse(400, {
|
|
4670
|
+
error: "chat_message requires callback {url, token}"
|
|
4671
|
+
});
|
|
4672
|
+
}
|
|
4673
|
+
const ackBody = JSON.stringify({ status: "accepted", task_id: taskReq.task_id });
|
|
4674
|
+
const response = {
|
|
4675
|
+
status: 202,
|
|
4676
|
+
headers: {
|
|
4677
|
+
"Content-Type": "application/json",
|
|
4678
|
+
"Content-Length": String(Buffer2.byteLength(ackBody))
|
|
4679
|
+
},
|
|
4680
|
+
body: ackBody
|
|
4681
|
+
};
|
|
4682
|
+
rememberResult(taskReq.task_id, response.status, response.body);
|
|
4683
|
+
const task = runReplyTaskAsync(taskReq, "chat");
|
|
4684
|
+
chatTasks.set(taskReq.task_id, task);
|
|
4685
|
+
void task.finally(() => {
|
|
4686
|
+
if (chatTasks.get(taskReq.task_id) === task) {
|
|
4687
|
+
chatTasks.delete(taskReq.task_id);
|
|
4688
|
+
}
|
|
4689
|
+
});
|
|
4690
|
+
return response;
|
|
4691
|
+
}
|
|
4692
|
+
async function handleTranscriptChunkRequest(taskReq) {
|
|
4693
|
+
if (taskReq.payload.is_final !== true) {
|
|
4694
|
+
const response2 = jsonResponse(202, {
|
|
4695
|
+
status: "accepted",
|
|
4696
|
+
task_id: taskReq.task_id,
|
|
4697
|
+
dispatched: false
|
|
4698
|
+
});
|
|
4699
|
+
rememberResult(taskReq.task_id, response2.status, response2.body);
|
|
4700
|
+
inflight.delete(taskReq.task_id);
|
|
4701
|
+
return response2;
|
|
4702
|
+
}
|
|
4703
|
+
if (!taskReq.callback) {
|
|
4704
|
+
try {
|
|
4705
|
+
const effectiveText = applyPrelude(
|
|
4706
|
+
taskReq.payload.text,
|
|
4707
|
+
consumePrelude(taskReq.session_id)
|
|
4708
|
+
);
|
|
4709
|
+
const taskPayload = httpTaskRequestToTaskPayload(taskReq, {
|
|
4710
|
+
text: effectiveText
|
|
4711
|
+
});
|
|
4712
|
+
if (!taskPayload) {
|
|
4713
|
+
return jsonResponse(400, { error: "invalid_payload_for_reason" });
|
|
4714
|
+
}
|
|
4715
|
+
const reasoningPeerId = `sw:${taskReq.session_id}:reasoning`;
|
|
4716
|
+
const dispatch = await dispatchTaskToAgent({
|
|
4717
|
+
api,
|
|
4718
|
+
task: taskPayload,
|
|
4719
|
+
deliver: true,
|
|
4720
|
+
idempotencyKey: `sw-http-task-${taskReq.task_id}`,
|
|
4721
|
+
sessionPeerId: reasoningPeerId
|
|
4722
|
+
});
|
|
4723
|
+
api.logger.info(
|
|
4724
|
+
`[stagewhisper-http] dispatched one-way task ${taskReq.task_id} (runId: ${dispatch.runId})`
|
|
4725
|
+
);
|
|
4726
|
+
const response2 = jsonResponse(200, {
|
|
4727
|
+
status: "accepted",
|
|
4728
|
+
task_id: taskReq.task_id,
|
|
4729
|
+
run_id: dispatch.runId,
|
|
4730
|
+
dispatched: true
|
|
4731
|
+
});
|
|
4732
|
+
rememberResult(taskReq.task_id, response2.status, response2.body);
|
|
4733
|
+
return response2;
|
|
4734
|
+
} catch (err) {
|
|
4735
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4736
|
+
api.logger.error(
|
|
4737
|
+
`[stagewhisper-http] dispatch failed for ${taskReq.task_id}: ${errMsg}`
|
|
4738
|
+
);
|
|
4739
|
+
return jsonResponse(503, { error: "dispatch_failed", detail: errMsg });
|
|
4740
|
+
} finally {
|
|
4741
|
+
inflight.delete(taskReq.task_id);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
const ackBody = JSON.stringify({ status: "accepted", task_id: taskReq.task_id });
|
|
4745
|
+
const response = {
|
|
4746
|
+
status: 202,
|
|
4747
|
+
headers: {
|
|
4748
|
+
"Content-Type": "application/json",
|
|
4749
|
+
"Content-Length": String(Buffer2.byteLength(ackBody))
|
|
4750
|
+
},
|
|
4751
|
+
body: ackBody
|
|
4752
|
+
};
|
|
4753
|
+
rememberResult(taskReq.task_id, response.status, response.body);
|
|
4754
|
+
const task = runReplyTaskAsync(taskReq, "reasoning");
|
|
4755
|
+
chatTasks.set(taskReq.task_id, task);
|
|
4756
|
+
void task.finally(() => {
|
|
4757
|
+
if (chatTasks.get(taskReq.task_id) === task) {
|
|
4758
|
+
chatTasks.delete(taskReq.task_id);
|
|
4759
|
+
}
|
|
4760
|
+
});
|
|
4761
|
+
return response;
|
|
4762
|
+
}
|
|
4763
|
+
function handleSystemPreludeRequest(taskReq) {
|
|
4764
|
+
pendingPreludes.set(taskReq.session_id, taskReq.payload.text);
|
|
4765
|
+
const response = jsonResponse(202, {
|
|
4766
|
+
status: "accepted",
|
|
4767
|
+
task_id: taskReq.task_id
|
|
4768
|
+
});
|
|
4769
|
+
rememberResult(taskReq.task_id, response.status, response.body);
|
|
4770
|
+
inflight.delete(taskReq.task_id);
|
|
4771
|
+
return response;
|
|
4772
|
+
}
|
|
4773
|
+
async function dispatchRoute(req) {
|
|
4774
|
+
if (!isLoopbackAddress(req.remoteAddress)) {
|
|
4775
|
+
api.logger.warn(
|
|
4776
|
+
`[stagewhisper-http] rejecting non-loopback connection from ${req.remoteAddress}`
|
|
4777
|
+
);
|
|
4778
|
+
return jsonResponse(403, { error: "non_loopback_rejected" });
|
|
4779
|
+
}
|
|
4780
|
+
if (!isAllowedHostHeader(req.host)) {
|
|
4781
|
+
api.logger.warn(
|
|
4782
|
+
`[stagewhisper-http] rejecting disallowed Host header: ${req.host ?? "<missing>"}`
|
|
4783
|
+
);
|
|
4784
|
+
return jsonResponse(403, { error: "invalid_host" });
|
|
4785
|
+
}
|
|
4786
|
+
const providedToken = extractBearerToken(req.authorization);
|
|
4787
|
+
if (!providedToken || !constantTimeTokenEqual(providedToken, token)) {
|
|
4788
|
+
return jsonResponse(401, { error: "invalid_token" });
|
|
4789
|
+
}
|
|
4790
|
+
if (req.method === "POST" && req.url === PING_PATH) {
|
|
4791
|
+
return jsonResponse(200, { ok: true });
|
|
4792
|
+
}
|
|
4793
|
+
if (req.method === "POST" && req.url === INCOMING_PATH) {
|
|
4794
|
+
let parsed;
|
|
4795
|
+
try {
|
|
4796
|
+
parsed = req.body.length === 0 ? {} : JSON.parse(req.body);
|
|
4797
|
+
} catch (err) {
|
|
4798
|
+
return jsonResponse(400, {
|
|
4799
|
+
error: `invalid_json: ${err instanceof Error ? err.message : String(err)}`
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
const validation = validateHttpTaskRequest(parsed);
|
|
4803
|
+
if (!validation.ok) {
|
|
4804
|
+
return jsonResponse(400, { error: validation.error });
|
|
4805
|
+
}
|
|
4806
|
+
const taskReq = validation.req;
|
|
4807
|
+
evictExpired();
|
|
4808
|
+
const cached = idempotency.get(taskReq.task_id);
|
|
4809
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
4810
|
+
if (cached.terminal) {
|
|
4811
|
+
const terminal = cached.terminal;
|
|
4812
|
+
const redelivery = redeliverTerminalCallback(terminal, taskReq.task_id);
|
|
4813
|
+
chatTasks.set(taskReq.task_id, redelivery);
|
|
4814
|
+
void redelivery.finally(() => {
|
|
4815
|
+
if (chatTasks.get(taskReq.task_id) === redelivery) {
|
|
4816
|
+
chatTasks.delete(taskReq.task_id);
|
|
4817
|
+
}
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
return {
|
|
4821
|
+
status: cached.status,
|
|
4822
|
+
headers: {
|
|
4823
|
+
"Content-Type": "application/json",
|
|
4824
|
+
"Content-Length": String(Buffer2.byteLength(cached.body)),
|
|
4825
|
+
"X-Idempotent-Replay": "true"
|
|
4826
|
+
},
|
|
4827
|
+
body: cached.body
|
|
4828
|
+
};
|
|
4829
|
+
}
|
|
4830
|
+
if (inflight.has(taskReq.task_id)) {
|
|
4831
|
+
return jsonResponse(503, { error: "task_in_flight" });
|
|
4832
|
+
}
|
|
4833
|
+
inflight.add(taskReq.task_id);
|
|
4834
|
+
if (taskReq.reason === "chat_message") {
|
|
4835
|
+
return handleChatMessageRequest(taskReq);
|
|
4836
|
+
}
|
|
4837
|
+
if (taskReq.reason === "system_prelude") {
|
|
4838
|
+
return handleSystemPreludeRequest(taskReq);
|
|
4839
|
+
}
|
|
4840
|
+
return handleTranscriptChunkRequest(taskReq);
|
|
4841
|
+
}
|
|
4842
|
+
return jsonResponse(404, { error: "not_found" });
|
|
4843
|
+
}
|
|
4844
|
+
async function handleRealRequest(req, res) {
|
|
4845
|
+
const method = req.method ?? "";
|
|
4846
|
+
const url = req.url ?? "";
|
|
4847
|
+
const authHeader = req.headers["authorization"];
|
|
4848
|
+
const authorization = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
4849
|
+
const hostHeader = req.headers["host"];
|
|
4850
|
+
const hostValue = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
|
|
4851
|
+
const remoteAddress = req.socket.remoteAddress;
|
|
4852
|
+
let body = "";
|
|
4853
|
+
if (method === "POST") {
|
|
4854
|
+
try {
|
|
4855
|
+
body = await readBody(req, MAX_BODY_BYTES);
|
|
4856
|
+
} catch (err) {
|
|
4857
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4858
|
+
const response = msg === "body_too_large" ? jsonResponse(413, { error: "body_too_large" }) : jsonResponse(400, { error: `invalid_body: ${msg}` });
|
|
4859
|
+
writeResponse(res, response);
|
|
4860
|
+
return;
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
const result = await dispatchRoute({
|
|
4864
|
+
method,
|
|
4865
|
+
url,
|
|
4866
|
+
authorization,
|
|
4867
|
+
host: hostValue,
|
|
4868
|
+
remoteAddress,
|
|
4869
|
+
body
|
|
4870
|
+
});
|
|
4871
|
+
writeResponse(res, result);
|
|
4872
|
+
}
|
|
4873
|
+
function writeResponse(res, response) {
|
|
4874
|
+
res.statusCode = response.status;
|
|
4875
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
4876
|
+
res.setHeader(key, value);
|
|
4877
|
+
}
|
|
4878
|
+
res.end(response.body);
|
|
4879
|
+
}
|
|
4880
|
+
let server = null;
|
|
4881
|
+
return {
|
|
4882
|
+
async start() {
|
|
4883
|
+
if (server) return;
|
|
4884
|
+
const srv = http.createServer((req, res) => {
|
|
4885
|
+
handleRealRequest(req, res).catch((err) => {
|
|
4886
|
+
api.logger.error(`[stagewhisper-http] unhandled error: ${err}`);
|
|
4887
|
+
if (!res.headersSent) {
|
|
4888
|
+
try {
|
|
4889
|
+
writeResponse(res, jsonResponse(500, { error: "internal_error" }));
|
|
4890
|
+
} catch {
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4893
|
+
});
|
|
4894
|
+
});
|
|
4895
|
+
server = srv;
|
|
4896
|
+
await new Promise((resolve, reject) => {
|
|
4897
|
+
const onError = (err) => {
|
|
4898
|
+
srv.off("listening", onListening);
|
|
4899
|
+
reject(err);
|
|
4900
|
+
};
|
|
4901
|
+
const onListening = () => {
|
|
4902
|
+
srv.off("error", onError);
|
|
4903
|
+
resolve();
|
|
4904
|
+
};
|
|
4905
|
+
srv.once("error", onError);
|
|
4906
|
+
srv.once("listening", onListening);
|
|
4907
|
+
srv.listen(port, host);
|
|
4908
|
+
});
|
|
4909
|
+
const addr = srv.address();
|
|
4910
|
+
api.logger.info(
|
|
4911
|
+
`[stagewhisper-http] listening on http://${host}:${typeof addr === "object" && addr ? addr.port : port}`
|
|
4912
|
+
);
|
|
4913
|
+
},
|
|
4914
|
+
async stop() {
|
|
4915
|
+
const srv = server;
|
|
4916
|
+
if (!srv) return;
|
|
4917
|
+
server = null;
|
|
4918
|
+
await new Promise((resolve) => {
|
|
4919
|
+
srv.close(() => resolve());
|
|
4920
|
+
srv.closeAllConnections?.();
|
|
4921
|
+
});
|
|
4922
|
+
const pending = Array.from(chatTasks.values());
|
|
4923
|
+
if (pending.length > 0) {
|
|
4924
|
+
await Promise.allSettled(pending);
|
|
4925
|
+
}
|
|
4926
|
+
idempotency.clear();
|
|
4927
|
+
inflight.clear();
|
|
4928
|
+
chatTasks.clear();
|
|
4929
|
+
pendingPreludes.clear();
|
|
4930
|
+
api.logger.info("[stagewhisper-http] stopped");
|
|
4931
|
+
},
|
|
4932
|
+
address() {
|
|
4933
|
+
const addr = server?.address();
|
|
4934
|
+
if (typeof addr === "object" && addr !== null) return addr;
|
|
4935
|
+
return null;
|
|
4936
|
+
},
|
|
4937
|
+
async handleSyntheticRequest(input) {
|
|
4938
|
+
const headers = input.headers ?? {};
|
|
4939
|
+
const authorization = headers["Authorization"] ?? headers["authorization"] ?? void 0;
|
|
4940
|
+
const hostValue = headers["Host"] ?? headers["host"] ?? "127.0.0.1";
|
|
4941
|
+
const remoteAddress = input.remoteAddress ?? "127.0.0.1";
|
|
4942
|
+
const body = input.body ?? "";
|
|
4943
|
+
return dispatchRoute({
|
|
4944
|
+
method: input.method,
|
|
4945
|
+
url: input.url,
|
|
4946
|
+
authorization,
|
|
4947
|
+
host: hostValue,
|
|
4948
|
+
remoteAddress,
|
|
4949
|
+
body
|
|
4950
|
+
});
|
|
4951
|
+
},
|
|
4952
|
+
whenChatTaskSettled(taskId) {
|
|
4953
|
+
return chatTasks.get(taskId);
|
|
4954
|
+
}
|
|
4955
|
+
};
|
|
4956
|
+
}
|
|
4957
|
+
|
|
4958
|
+
// src/pairing.ts
|
|
4959
|
+
import { Buffer as Buffer3 } from "node:buffer";
|
|
4960
|
+
import { randomBytes } from "node:crypto";
|
|
4961
|
+
var PAIRING_CODE_PREFIX = "stagewhisper-pair:v1:";
|
|
4962
|
+
function encodePairingCode(url, token, label) {
|
|
4963
|
+
const payload = JSON.stringify({ url, token, label });
|
|
4964
|
+
const encoded = Buffer3.from(payload, "utf8").toString("base64url");
|
|
4965
|
+
return `${PAIRING_CODE_PREFIX}${encoded}`;
|
|
4966
|
+
}
|
|
4967
|
+
function generateRelayToken() {
|
|
4968
|
+
return randomBytes(32).toString("base64url");
|
|
4969
|
+
}
|
|
4970
|
+
|
|
3990
4971
|
// src/runtime.ts
|
|
3991
4972
|
var runtimeStore = createPluginRuntimeStore(
|
|
3992
4973
|
"StageWhisper plugin runtime not initialized"
|
|
@@ -3996,7 +4977,7 @@ var getRuntime = runtimeStore.getRuntime;
|
|
|
3996
4977
|
|
|
3997
4978
|
// src/service.ts
|
|
3998
4979
|
init_client();
|
|
3999
|
-
import { randomUUID } from "node:crypto";
|
|
4980
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4000
4981
|
|
|
4001
4982
|
// src/health.ts
|
|
4002
4983
|
var DEGRADED_THRESHOLD = 3;
|
|
@@ -4278,41 +5259,13 @@ function createRelayService(api) {
|
|
|
4278
5259
|
};
|
|
4279
5260
|
}
|
|
4280
5261
|
}
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
lines.push(`**${task.title}**`);
|
|
4284
|
-
lines.push("");
|
|
4285
|
-
lines.push(task.request_text);
|
|
4286
|
-
if (task.evidence_payload) {
|
|
4287
|
-
const evidence = task.evidence_payload;
|
|
4288
|
-
if (evidence["transcript_excerpt"]) {
|
|
4289
|
-
lines.push("");
|
|
4290
|
-
lines.push(`Context: ${evidence["transcript_excerpt"]}`);
|
|
4291
|
-
}
|
|
4292
|
-
if (evidence["signal_summary"]) {
|
|
4293
|
-
lines.push(`Signal: ${evidence["signal_summary"]}`);
|
|
4294
|
-
}
|
|
4295
|
-
if (evidence["tone_summary"]) {
|
|
4296
|
-
lines.push(`Tone: ${evidence["tone_summary"]}`);
|
|
4297
|
-
}
|
|
4298
|
-
if (evidence["playbook_label"]) {
|
|
4299
|
-
lines.push(`Playbook: ${evidence["playbook_label"]}`);
|
|
4300
|
-
}
|
|
4301
|
-
}
|
|
4302
|
-
lines.push("");
|
|
4303
|
-
lines.push(`Action type: ${task.action_type}`);
|
|
4304
|
-
lines.push(`StageWhisper task: ${task.id}`);
|
|
4305
|
-
lines.push(`Session: ${task.session_id}`);
|
|
4306
|
-
return lines.join("\n");
|
|
4307
|
-
}
|
|
4308
|
-
function isTestTask(task) {
|
|
4309
|
-
return task.action_type === "connectivity_test";
|
|
4310
|
-
}
|
|
5262
|
+
const buildTaskMessage2 = buildTaskMessage;
|
|
5263
|
+
const isTestTask2 = isTestTask;
|
|
4311
5264
|
async function updateStatus(client, task, status) {
|
|
4312
|
-
if (
|
|
5265
|
+
if (isTestTask2(task)) return;
|
|
4313
5266
|
await client.updateTaskStatus(task.id, status);
|
|
4314
5267
|
}
|
|
4315
|
-
function
|
|
5268
|
+
function extractContentFromMessage2(msg) {
|
|
4316
5269
|
const content = msg["content"];
|
|
4317
5270
|
if (typeof content === "string") return content;
|
|
4318
5271
|
if (Array.isArray(content)) {
|
|
@@ -4330,7 +5283,7 @@ function createRelayService(api) {
|
|
|
4330
5283
|
if (!msg) continue;
|
|
4331
5284
|
const role = msg["role"];
|
|
4332
5285
|
if (role !== "assistant" && role !== "model") continue;
|
|
4333
|
-
return
|
|
5286
|
+
return extractContentFromMessage2(msg);
|
|
4334
5287
|
}
|
|
4335
5288
|
return null;
|
|
4336
5289
|
}
|
|
@@ -4381,13 +5334,13 @@ function createRelayService(api) {
|
|
|
4381
5334
|
for (let i = 0; i < messages.length; i++) {
|
|
4382
5335
|
const msg = messages[i];
|
|
4383
5336
|
if (msg["role"] !== "user") continue;
|
|
4384
|
-
const text =
|
|
5337
|
+
const text = extractContentFromMessage2(msg) ?? "";
|
|
4385
5338
|
if (!text.includes(`StageWhisper task: ${taskId}`)) continue;
|
|
4386
5339
|
for (let j = i + 1; j < messages.length; j++) {
|
|
4387
5340
|
const reply = messages[j];
|
|
4388
5341
|
const role = reply["role"];
|
|
4389
5342
|
if (role === "assistant" || role === "model") {
|
|
4390
|
-
return
|
|
5343
|
+
return extractContentFromMessage2(reply);
|
|
4391
5344
|
}
|
|
4392
5345
|
if (role === "user") break;
|
|
4393
5346
|
}
|
|
@@ -4423,13 +5376,13 @@ function createRelayService(api) {
|
|
|
4423
5376
|
for (let i = 0; i < messages.length; i++) {
|
|
4424
5377
|
const msg = messages[i];
|
|
4425
5378
|
if (msg["role"] !== "user") continue;
|
|
4426
|
-
const text =
|
|
5379
|
+
const text = extractContentFromMessage2(msg) ?? "";
|
|
4427
5380
|
if (!text.includes(marker)) continue;
|
|
4428
5381
|
for (let j = i + 1; j < messages.length; j++) {
|
|
4429
5382
|
const next = messages[j];
|
|
4430
5383
|
const role = next["role"];
|
|
4431
5384
|
if (role === "assistant" || role === "model") {
|
|
4432
|
-
return
|
|
5385
|
+
return extractContentFromMessage2(next);
|
|
4433
5386
|
}
|
|
4434
5387
|
if (role === "user") break;
|
|
4435
5388
|
}
|
|
@@ -4505,7 +5458,7 @@ function createRelayService(api) {
|
|
|
4505
5458
|
}
|
|
4506
5459
|
const isByo = !!task.is_byo_encrypted;
|
|
4507
5460
|
const effectiveTask = isByo ? decryptTaskFields(task, client) : task;
|
|
4508
|
-
const messageContent =
|
|
5461
|
+
const messageContent = buildTaskMessage2(effectiveTask);
|
|
4509
5462
|
const peerId = `sw-session-${task.session_id}`;
|
|
4510
5463
|
const sessionKey = buildAgentSessionKey({
|
|
4511
5464
|
agentId: "default",
|
|
@@ -4548,7 +5501,7 @@ function createRelayService(api) {
|
|
|
4548
5501
|
});
|
|
4549
5502
|
}
|
|
4550
5503
|
async function handleTestTask(task, client) {
|
|
4551
|
-
const messageContent =
|
|
5504
|
+
const messageContent = buildTaskMessage2(task);
|
|
4552
5505
|
const sessionKey = buildAgentSessionKey({
|
|
4553
5506
|
agentId: "default",
|
|
4554
5507
|
channel: "stagewhisper",
|
|
@@ -4558,7 +5511,7 @@ function createRelayService(api) {
|
|
|
4558
5511
|
sessionKey,
|
|
4559
5512
|
message: messageContent,
|
|
4560
5513
|
deliver: false,
|
|
4561
|
-
idempotencyKey:
|
|
5514
|
+
idempotencyKey: randomUUID2()
|
|
4562
5515
|
});
|
|
4563
5516
|
api.logger.info(`Test task ${task.id} dispatched (runId: ${result.runId})`);
|
|
4564
5517
|
const waitResult = await api.runtime.subagent.waitForRun({
|
|
@@ -4581,7 +5534,7 @@ function createRelayService(api) {
|
|
|
4581
5534
|
async function handleTask(task, client) {
|
|
4582
5535
|
api.logger.info(`Received task: ${task.title} (${task.id})`);
|
|
4583
5536
|
try {
|
|
4584
|
-
if (
|
|
5537
|
+
if (isTestTask2(task)) {
|
|
4585
5538
|
await handleTestTask(task, client);
|
|
4586
5539
|
} else {
|
|
4587
5540
|
await handleNormalTask(task, client);
|
|
@@ -5411,20 +6364,73 @@ function createRelayService(api) {
|
|
|
5411
6364
|
}
|
|
5412
6365
|
|
|
5413
6366
|
// plugin-main.ts
|
|
6367
|
+
function resolveTransportMode(api) {
|
|
6368
|
+
const pluginCfg = api.pluginConfig ?? {};
|
|
6369
|
+
const fromConfig = pluginCfg["transport"];
|
|
6370
|
+
if (fromConfig === "http" || fromConfig === "sse") return fromConfig;
|
|
6371
|
+
const envOverride = process.env["STAGEWHISPER_TRANSPORT"];
|
|
6372
|
+
if (envOverride === "http" || envOverride === "sse") return envOverride;
|
|
6373
|
+
return "sse";
|
|
6374
|
+
}
|
|
6375
|
+
function resolveHttpTransportToken(pluginCfg, env = process.env) {
|
|
6376
|
+
if (typeof pluginCfg["httpToken"] === "string" && pluginCfg["httpToken"]) {
|
|
6377
|
+
return pluginCfg["httpToken"];
|
|
6378
|
+
}
|
|
6379
|
+
if (env["STAGEWHISPER_HTTP_TOKEN"]) {
|
|
6380
|
+
return env["STAGEWHISPER_HTTP_TOKEN"];
|
|
6381
|
+
}
|
|
6382
|
+
if (typeof pluginCfg["relayToken"] === "string") {
|
|
6383
|
+
return pluginCfg["relayToken"];
|
|
6384
|
+
}
|
|
6385
|
+
return "";
|
|
6386
|
+
}
|
|
6387
|
+
function createHttpTransportService(api) {
|
|
6388
|
+
const pluginCfg = api.pluginConfig ?? {};
|
|
6389
|
+
const host = typeof pluginCfg["httpHost"] === "string" ? pluginCfg["httpHost"] : process.env["STAGEWHISPER_HTTP_HOST"] ?? "127.0.0.1";
|
|
6390
|
+
const portRaw = pluginCfg["httpPort"];
|
|
6391
|
+
const port = typeof portRaw === "number" ? portRaw : typeof portRaw === "string" ? Number(portRaw) : Number(process.env["STAGEWHISPER_HTTP_PORT"]) || 8765;
|
|
6392
|
+
const token = resolveHttpTransportToken(pluginCfg);
|
|
6393
|
+
let transport = null;
|
|
6394
|
+
return {
|
|
6395
|
+
id: "stagewhisper-http-transport",
|
|
6396
|
+
async start(_ctx) {
|
|
6397
|
+
if (!token || token.length < 16) {
|
|
6398
|
+
api.logger.warn(
|
|
6399
|
+
"StageWhisper HTTP transport requires `httpToken` (>=16 chars) in plugin config or STAGEWHISPER_HTTP_TOKEN env var \u2014 listener not started."
|
|
6400
|
+
);
|
|
6401
|
+
return;
|
|
6402
|
+
}
|
|
6403
|
+
transport = createHttpTransport({ api, host, port, token });
|
|
6404
|
+
try {
|
|
6405
|
+
await transport.start();
|
|
6406
|
+
api.logger.info(`StageWhisper HTTP transport started on ${host}:${port}`);
|
|
6407
|
+
} catch (err) {
|
|
6408
|
+
api.logger.error(`StageWhisper HTTP transport failed to start: ${err}`);
|
|
6409
|
+
transport = null;
|
|
6410
|
+
}
|
|
6411
|
+
},
|
|
6412
|
+
async stop(_ctx) {
|
|
6413
|
+
if (transport) {
|
|
6414
|
+
await transport.stop();
|
|
6415
|
+
transport = null;
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
};
|
|
6419
|
+
}
|
|
5414
6420
|
async function ensureResponsesEndpoint(api) {
|
|
5415
6421
|
try {
|
|
5416
6422
|
const cfg = await api.runtime.config.loadConfig();
|
|
5417
6423
|
const gw = cfg["gateway"] ?? {};
|
|
5418
|
-
const
|
|
5419
|
-
const endpoints =
|
|
6424
|
+
const http2 = gw["http"] ?? {};
|
|
6425
|
+
const endpoints = http2["endpoints"] ?? {};
|
|
5420
6426
|
const responses = endpoints["responses"] ?? {};
|
|
5421
6427
|
if (responses["enabled"] === true) return;
|
|
5422
6428
|
const auth = gw["auth"] ?? {};
|
|
5423
6429
|
if (auth["mode"] === "none" && !auth["token"] && !auth["password"]) return;
|
|
5424
6430
|
responses["enabled"] = true;
|
|
5425
6431
|
endpoints["responses"] = responses;
|
|
5426
|
-
|
|
5427
|
-
gw["http"] =
|
|
6432
|
+
http2["endpoints"] = endpoints;
|
|
6433
|
+
gw["http"] = http2;
|
|
5428
6434
|
cfg["gateway"] = gw;
|
|
5429
6435
|
await api.runtime.config.writeConfigFile(cfg);
|
|
5430
6436
|
api.logger.info(
|
|
@@ -5442,13 +6448,7 @@ var plugin_main_default = definePluginEntry({
|
|
|
5442
6448
|
api.registerCli(
|
|
5443
6449
|
({ program }) => {
|
|
5444
6450
|
const sw = program.command("stagewhisper").description("StageWhisper integration");
|
|
5445
|
-
sw.command("pair").description(
|
|
5446
|
-
"Pair with StageWhisper using a pairing code from the desktop app"
|
|
5447
|
-
).requiredOption("--code <code>", "Pairing code from Settings \u2192 Assistant").option(
|
|
5448
|
-
"--api-url <url>",
|
|
5449
|
-
"StageWhisper backend URL",
|
|
5450
|
-
"https://api.stagewhisper.io"
|
|
5451
|
-
).option("--label <label>", "Label for this OpenClaw host", "OpenClaw").option("--no-enable-responses", "Skip enabling the gateway OpenResponses HTTP API").action(
|
|
6451
|
+
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(
|
|
5452
6452
|
async (opts) => {
|
|
5453
6453
|
const { StageWhisperClient: StageWhisperClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
5454
6454
|
const { IdentityKeypair: IdentityKeypair2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
@@ -5493,26 +6493,33 @@ var plugin_main_default = definePluginEntry({
|
|
|
5493
6493
|
const authMode = gwAuth["mode"];
|
|
5494
6494
|
const hasToken = typeof gwAuth["token"] === "string" && gwAuth["token"].length > 0;
|
|
5495
6495
|
if (authMode === "none" && !hasToken) {
|
|
5496
|
-
console.warn(
|
|
6496
|
+
console.warn(
|
|
6497
|
+
" \u26A0 gateway.auth.mode is 'none' with no token \u2014 skipping HTTP API enablement."
|
|
6498
|
+
);
|
|
5497
6499
|
console.warn(" Set a gateway auth token first for reasoning to work.\n");
|
|
5498
6500
|
} else {
|
|
5499
|
-
const
|
|
5500
|
-
const endpoints =
|
|
6501
|
+
const http2 = gw["http"] ?? {};
|
|
6502
|
+
const endpoints = http2["endpoints"] ?? {};
|
|
5501
6503
|
const responses = endpoints["responses"] ?? {};
|
|
5502
6504
|
responses["enabled"] = true;
|
|
5503
6505
|
endpoints["responses"] = responses;
|
|
5504
|
-
|
|
5505
|
-
gw["http"] =
|
|
6506
|
+
http2["endpoints"] = endpoints;
|
|
6507
|
+
gw["http"] = http2;
|
|
5506
6508
|
cfg["gateway"] = gw;
|
|
5507
6509
|
}
|
|
5508
6510
|
}
|
|
5509
6511
|
await api.runtime.config.writeConfigFile(cfg);
|
|
5510
|
-
console.log(
|
|
5511
|
-
|
|
5512
|
-
\u2713 Paired with StageWhisper (${result.label})`
|
|
5513
|
-
);
|
|
6512
|
+
console.log(`
|
|
6513
|
+
\u2713 Paired with StageWhisper (${result.label})`);
|
|
5514
6514
|
console.log(" Config saved. Restart the gateway to activate:\n");
|
|
5515
6515
|
console.log(" openclaw gateway restart\n");
|
|
6516
|
+
console.log(
|
|
6517
|
+
" For local HTTP transport (transport: http), the desktop authenticates"
|
|
6518
|
+
);
|
|
6519
|
+
console.log(
|
|
6520
|
+
" against the loopback listener with this same relay token \u2014 no separate"
|
|
6521
|
+
);
|
|
6522
|
+
console.log(" httpToken is required unless you set one explicitly.\n");
|
|
5516
6523
|
} catch (err) {
|
|
5517
6524
|
console.error(`
|
|
5518
6525
|
\u2717 Pairing failed: ${err}
|
|
@@ -5521,9 +6528,62 @@ var plugin_main_default = definePluginEntry({
|
|
|
5521
6528
|
}
|
|
5522
6529
|
}
|
|
5523
6530
|
);
|
|
5524
|
-
sw.command("
|
|
5525
|
-
"
|
|
5526
|
-
).option("--
|
|
6531
|
+
sw.command("pair-code").description(
|
|
6532
|
+
"Generate a StageWhisper Free pairing code for the local HTTP transport (no backend)"
|
|
6533
|
+
).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) => {
|
|
6534
|
+
try {
|
|
6535
|
+
const cfg = await api.runtime.config.loadConfig();
|
|
6536
|
+
const plugins = cfg["plugins"] ?? {};
|
|
6537
|
+
const entries = plugins["entries"] ?? {};
|
|
6538
|
+
const swEntry = entries["stagewhisper"] ?? {};
|
|
6539
|
+
const swConfig = swEntry["config"] ?? {};
|
|
6540
|
+
const host = typeof swConfig["httpHost"] === "string" && swConfig["httpHost"] ? swConfig["httpHost"] : "127.0.0.1";
|
|
6541
|
+
const port = opts.port ? Number(opts.port) : typeof swConfig["httpPort"] === "number" ? swConfig["httpPort"] : 8765;
|
|
6542
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
6543
|
+
console.error(`
|
|
6544
|
+
\u2717 Invalid port ${port} (must be 1024-65535)
|
|
6545
|
+
`);
|
|
6546
|
+
process.exit(1);
|
|
6547
|
+
}
|
|
6548
|
+
let token = typeof swConfig["httpToken"] === "string" ? swConfig["httpToken"] : "";
|
|
6549
|
+
if (token.length < 16) {
|
|
6550
|
+
token = generateRelayToken();
|
|
6551
|
+
}
|
|
6552
|
+
const label = (opts.label ?? swConfig["label"] ?? "OpenClaw").trim() || "OpenClaw";
|
|
6553
|
+
swConfig["transport"] = "http";
|
|
6554
|
+
swConfig["httpHost"] = host;
|
|
6555
|
+
swConfig["httpPort"] = port;
|
|
6556
|
+
swConfig["httpToken"] = token;
|
|
6557
|
+
swConfig["label"] = label;
|
|
6558
|
+
swEntry["config"] = swConfig;
|
|
6559
|
+
entries["stagewhisper"] = swEntry;
|
|
6560
|
+
plugins["entries"] = entries;
|
|
6561
|
+
cfg["plugins"] = plugins;
|
|
6562
|
+
await api.runtime.config.writeConfigFile(cfg);
|
|
6563
|
+
const relayUrl = (opts.url ?? "").trim() || `http://${host}:${port}`;
|
|
6564
|
+
const code = encodePairingCode(relayUrl, token, label);
|
|
6565
|
+
console.log("\nStageWhisper Free pairing code:\n");
|
|
6566
|
+
console.log(` ${code}
|
|
6567
|
+
`);
|
|
6568
|
+
console.log("Paste it into StageWhisper Free under Settings \u2192 Connection.");
|
|
6569
|
+
console.log(
|
|
6570
|
+
"Restart the gateway so the HTTP transport listens: openclaw gateway restart"
|
|
6571
|
+
);
|
|
6572
|
+
if (relayUrl.startsWith("http://127.0.0.1")) {
|
|
6573
|
+
console.log(
|
|
6574
|
+
"\nRunning on a remote host? Tunnel the port from the machine running Free:"
|
|
6575
|
+
);
|
|
6576
|
+
console.log(` ssh -L ${port}:127.0.0.1:${port} <this-host>
|
|
6577
|
+
`);
|
|
6578
|
+
}
|
|
6579
|
+
} catch (err) {
|
|
6580
|
+
console.error(`
|
|
6581
|
+
\u2717 Failed to generate pairing code: ${err}
|
|
6582
|
+
`);
|
|
6583
|
+
process.exit(1);
|
|
6584
|
+
}
|
|
6585
|
+
});
|
|
6586
|
+
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) => {
|
|
5527
6587
|
try {
|
|
5528
6588
|
const cfg = await api.runtime.config.loadConfig();
|
|
5529
6589
|
const plugins = cfg["plugins"] ?? {};
|
|
@@ -5541,15 +6601,19 @@ var plugin_main_default = definePluginEntry({
|
|
|
5541
6601
|
}
|
|
5542
6602
|
if (!opts.keepResponses) {
|
|
5543
6603
|
const gw = cfg["gateway"];
|
|
5544
|
-
const
|
|
5545
|
-
const endpoints =
|
|
6604
|
+
const http2 = gw?.["http"];
|
|
6605
|
+
const endpoints = http2?.["endpoints"];
|
|
5546
6606
|
const responses = endpoints?.["responses"];
|
|
5547
6607
|
if (responses?.["enabled"] === true) {
|
|
5548
6608
|
delete responses["enabled"];
|
|
5549
|
-
if (Object.keys(responses).length === 0 && endpoints)
|
|
5550
|
-
|
|
5551
|
-
if (
|
|
5552
|
-
|
|
6609
|
+
if (Object.keys(responses).length === 0 && endpoints)
|
|
6610
|
+
delete endpoints["responses"];
|
|
6611
|
+
if (endpoints && Object.keys(endpoints).length === 0 && http2)
|
|
6612
|
+
delete http2["endpoints"];
|
|
6613
|
+
if (http2 && Object.keys(http2).length === 0 && gw) delete gw["http"];
|
|
6614
|
+
console.log(
|
|
6615
|
+
" \u2139 Disabled gateway.http.endpoints.responses. Use --keep-responses to preserve it."
|
|
6616
|
+
);
|
|
5553
6617
|
}
|
|
5554
6618
|
}
|
|
5555
6619
|
await api.runtime.config.writeConfigFile(cfg);
|
|
@@ -5563,7 +6627,11 @@ var plugin_main_default = definePluginEntry({
|
|
|
5563
6627
|
process.exit(1);
|
|
5564
6628
|
}
|
|
5565
6629
|
});
|
|
5566
|
-
sw.command("reasoning-check").description("Test reasoning capability against the local OpenResponses endpoint").option(
|
|
6630
|
+
sw.command("reasoning-check").description("Test reasoning capability against the local OpenResponses endpoint").option(
|
|
6631
|
+
"--model <model>",
|
|
6632
|
+
"Model to use (omit to use your configured default)",
|
|
6633
|
+
"openclaw/default"
|
|
6634
|
+
).action(async (opts) => {
|
|
5567
6635
|
const { callOpenResponses: callOpenResponses2, isResponsesEndpointEnabled: isResponsesEndpointEnabled2 } = await Promise.resolve().then(() => (init_openresponses(), openresponses_exports));
|
|
5568
6636
|
const modelLabel = opts.model === "openclaw/default" ? "default (configured)" : opts.model;
|
|
5569
6637
|
const cfg = api.config;
|
|
@@ -5578,7 +6646,9 @@ var plugin_main_default = definePluginEntry({
|
|
|
5578
6646
|
console.log(` responses.enabled: ${responsesEnabled ? "\u2713 true" : "\u2717 false"}`);
|
|
5579
6647
|
if (!responsesEnabled) {
|
|
5580
6648
|
console.warn("\n\u26A0 responses.enabled is false in the running config.");
|
|
5581
|
-
console.warn(
|
|
6649
|
+
console.warn(
|
|
6650
|
+
" The plugin auto-enables it on startup \u2014 restart the gateway if you haven't:"
|
|
6651
|
+
);
|
|
5582
6652
|
console.warn(" openclaw gateway restart\n");
|
|
5583
6653
|
}
|
|
5584
6654
|
if (!hasToken) {
|
|
@@ -5652,9 +6722,7 @@ Testing reasoning with model: ${modelLabel}`);
|
|
|
5652
6722
|
} catch (err) {
|
|
5653
6723
|
const elapsed = Date.now() - start;
|
|
5654
6724
|
console.error(`\u2717 Reasoning check failed after ${elapsed}ms`);
|
|
5655
|
-
console.error(
|
|
5656
|
-
` Error: ${err instanceof Error ? err.message : String(err)}`
|
|
5657
|
-
);
|
|
6725
|
+
console.error(` Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
5658
6726
|
process.exitCode = 1;
|
|
5659
6727
|
}
|
|
5660
6728
|
});
|
|
@@ -5663,9 +6731,7 @@ Testing reasoning with model: ${modelLabel}`);
|
|
|
5663
6731
|
const configured = !!(cfg?.["integrationId"] && cfg?.["relayToken"]);
|
|
5664
6732
|
if (!configured) {
|
|
5665
6733
|
console.log("\nStageWhisper: not paired\n");
|
|
5666
|
-
console.log(
|
|
5667
|
-
" Run: openclaw stagewhisper pair --code <CODE> [--api-url <URL>]\n"
|
|
5668
|
-
);
|
|
6734
|
+
console.log(" Run: openclaw stagewhisper pair --code <CODE> [--api-url <URL>]\n");
|
|
5669
6735
|
console.log(
|
|
5670
6736
|
" Get the pairing code from StageWhisper desktop: Settings \u2192 Assistant \u2192 Generate Pairing Code\n"
|
|
5671
6737
|
);
|
|
@@ -5699,10 +6765,16 @@ StageWhisper:`);
|
|
|
5699
6765
|
if (api.registrationMode !== "full") return;
|
|
5700
6766
|
ensureResponsesEndpoint(api);
|
|
5701
6767
|
setRuntime(api.runtime);
|
|
5702
|
-
const
|
|
5703
|
-
api.
|
|
6768
|
+
const transportMode = resolveTransportMode(api);
|
|
6769
|
+
api.logger.info(`StageWhisper transport: ${transportMode}`);
|
|
6770
|
+
if (transportMode === "http") {
|
|
6771
|
+
api.registerService(createHttpTransportService(api));
|
|
6772
|
+
} else {
|
|
6773
|
+
api.registerService(createRelayService(api));
|
|
6774
|
+
}
|
|
5704
6775
|
}
|
|
5705
6776
|
});
|
|
5706
6777
|
export {
|
|
5707
|
-
plugin_main_default as default
|
|
6778
|
+
plugin_main_default as default,
|
|
6779
|
+
resolveHttpTransportToken
|
|
5708
6780
|
};
|