@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 +1217 -89
- package/openclaw.plugin.json +29 -1
- package/package.json +1 -1
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
|
|
208
|
-
const 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
|
|
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
|
|
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 =
|
|
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 ||
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
4234
|
-
|
|
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 (
|
|
5136
|
+
if (isTestTask2(task)) return;
|
|
4265
5137
|
await client.updateTaskStatus(task.id, status);
|
|
4266
5138
|
}
|
|
4267
|
-
function
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
|
5234
|
-
const 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
|
-
|
|
5242
|
-
gw["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(
|
|
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
|
|
5315
|
-
const 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
|
-
|
|
5320
|
-
gw["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("
|
|
5340
|
-
"
|
|
5341
|
-
).option("--
|
|
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
|
|
5360
|
-
const 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)
|
|
5365
|
-
|
|
5366
|
-
if (
|
|
5367
|
-
|
|
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(
|
|
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(
|
|
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
|
|
5518
|
-
api.
|
|
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
|
};
|