@syengup/friday-channel-next 0.0.35 → 0.0.38
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.d.ts +4 -0
- package/dist/index.js +182 -0
- package/dist/src/agent/abort-run.d.ts +1 -0
- package/dist/src/agent/abort-run.js +11 -0
- package/dist/src/agent/active-runs.d.ts +9 -0
- package/dist/src/agent/active-runs.js +20 -0
- package/dist/src/agent/dispatch-bridge.d.ts +5 -0
- package/dist/src/agent/dispatch-bridge.js +12 -0
- package/dist/src/agent/media-bridge.d.ts +4 -0
- package/dist/src/agent/media-bridge.js +21 -0
- package/dist/src/agent/subagent-registry.d.ts +68 -0
- package/dist/src/agent/subagent-registry.js +142 -0
- package/dist/src/agent-forward-runtime.d.ts +17 -0
- package/dist/src/agent-forward-runtime.js +16 -0
- package/dist/src/agent-run-context-bridge.d.ts +13 -0
- package/dist/src/agent-run-context-bridge.js +23 -0
- package/dist/src/channel-actions.d.ts +13 -0
- package/dist/src/channel-actions.js +101 -0
- package/dist/src/channel.d.ts +6 -0
- package/dist/src/channel.js +248 -0
- package/dist/src/collect-message-media-paths.d.ts +11 -0
- package/dist/src/collect-message-media-paths.js +143 -0
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +39 -0
- package/dist/src/friday-inbound-stats.d.ts +2 -0
- package/dist/src/friday-inbound-stats.js +8 -0
- package/dist/src/friday-session.d.ts +40 -0
- package/dist/src/friday-session.js +395 -0
- package/dist/src/host-config.d.ts +1 -0
- package/dist/src/host-config.js +15 -0
- package/dist/src/http/handlers/cancel.d.ts +2 -0
- package/dist/src/http/handlers/cancel.js +33 -0
- package/dist/src/http/handlers/device-approve.d.ts +2 -0
- package/dist/src/http/handlers/device-approve.js +125 -0
- package/dist/src/http/handlers/files-download.d.ts +10 -0
- package/dist/src/http/handlers/files-download.js +210 -0
- package/dist/src/http/handlers/files-upload.d.ts +8 -0
- package/dist/src/http/handlers/files-upload.js +136 -0
- package/dist/src/http/handlers/files.d.ts +75 -0
- package/dist/src/http/handlers/files.js +305 -0
- package/dist/src/http/handlers/messages.d.ts +34 -0
- package/dist/src/http/handlers/messages.js +476 -0
- package/dist/src/http/handlers/models-list.d.ts +10 -0
- package/dist/src/http/handlers/models-list.js +113 -0
- package/dist/src/http/handlers/nodes-approve.d.ts +2 -0
- package/dist/src/http/handlers/nodes-approve.js +146 -0
- package/dist/src/http/handlers/sessions-delete.d.ts +2 -0
- package/dist/src/http/handlers/sessions-delete.js +49 -0
- package/dist/src/http/handlers/sessions-settings.d.ts +2 -0
- package/dist/src/http/handlers/sessions-settings.js +71 -0
- package/dist/src/http/handlers/sse.d.ts +2 -0
- package/dist/src/http/handlers/sse.js +70 -0
- package/dist/src/http/handlers/status.d.ts +2 -0
- package/dist/src/http/handlers/status.js +29 -0
- package/dist/src/http/middleware/auth.d.ts +13 -0
- package/dist/src/http/middleware/auth.js +29 -0
- package/dist/src/http/middleware/body.d.ts +2 -0
- package/dist/src/http/middleware/body.js +24 -0
- package/dist/src/http/middleware/cors.d.ts +2 -0
- package/dist/src/http/middleware/cors.js +11 -0
- package/dist/src/http/server.d.ts +19 -0
- package/dist/src/http/server.js +87 -0
- package/dist/src/logging.d.ts +7 -0
- package/dist/src/logging.js +28 -0
- package/dist/src/run-metadata.d.ts +25 -0
- package/dist/src/run-metadata.js +139 -0
- package/dist/src/runtime.d.ts +13 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/session/session-manager.d.ts +22 -0
- package/dist/src/session/session-manager.js +190 -0
- package/dist/src/session-usage-snapshot.d.ts +23 -0
- package/dist/src/session-usage-snapshot.js +65 -0
- package/dist/src/sse/emitter.d.ts +59 -0
- package/dist/src/sse/emitter.js +219 -0
- package/dist/src/sse/offline-queue.d.ts +26 -0
- package/dist/src/sse/offline-queue.js +134 -0
- package/dist/src/vendor/runtime-store.d.ts +26 -0
- package/dist/src/vendor/runtime-store.js +60 -0
- package/index.ts +10 -4
- package/package.json +11 -10
- package/src/agent/subagent-registry.ts +195 -0
- package/src/channel.ts +6 -4
- package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
- package/src/e2e/subagent.e2e.test.ts +502 -0
- package/src/friday-session.ts +140 -1
- package/src/http/handlers/device-approve.test.ts +0 -1
- package/src/http/handlers/device-approve.ts +0 -2
- package/src/http/handlers/files-download.ts +4 -1
- package/src/http/handlers/files.ts +7 -4
- package/src/http/handlers/messages.ts +54 -4
- package/src/http/handlers/models-list.ts +24 -2
- package/src/http/handlers/nodes-approve.test.ts +288 -0
- package/src/http/handlers/nodes-approve.ts +189 -0
- package/src/http/server.ts +5 -0
- package/src/openclaw.d.ts +5 -0
- package/src/sse/emitter.ts +1 -1
- package/src/test-support/mock-runtime.ts +2 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { sseEmitter } from "./sse/emitter.js";
|
|
2
|
+
import { getFridayAgentForwardRuntime } from "./agent-forward-runtime.js";
|
|
3
|
+
import { toSessionStoreKey } from "./session/session-manager.js";
|
|
4
|
+
import { getOpenClawAgentRunContext } from "./agent-run-context-bridge.js";
|
|
5
|
+
import { observeAgentEventForActiveRuns } from "./agent/active-runs.js";
|
|
6
|
+
import { getRunMetadata, ingestAgentEventMetadata } from "./run-metadata.js";
|
|
7
|
+
import { buildSessionUsageSnapshot } from "./session-usage-snapshot.js";
|
|
8
|
+
import { lookupByRunId, registerSessionKeyForRun, registerSpawnIntent, consumeSpawnIntent, ensureSubagentFromSpawnTool, registerEnded as registerSubagentEnded, } from "./agent/subagent-registry.js";
|
|
9
|
+
/** Last `data.text` per run for `stream: "thinking"` — OpenClaw core may send cumulative `delta`; we rewrite true increments for the app. */
|
|
10
|
+
const lastThinkingTextByRun = new Map();
|
|
11
|
+
function commonPrefixLength(a, b) {
|
|
12
|
+
const len = Math.min(a.length, b.length);
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i < len && a.charCodeAt(i) === b.charCodeAt(i))
|
|
15
|
+
i++;
|
|
16
|
+
return i;
|
|
17
|
+
}
|
|
18
|
+
/** Vitest-only: clears per-run reasoning text cache used for incremental `delta` rewriting. */
|
|
19
|
+
export function resetThinkingStreamAccumStateForTest() {
|
|
20
|
+
lastThinkingTextByRun.clear();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* OpenClaw `runId` → device UUID (uppercase).
|
|
24
|
+
* When `lifecycle.end` / `error` is emitted, the gateway may call `clearAgentRunContext` before this extension's
|
|
25
|
+
* `onAgentEvent` runs; combined with stripped `sessionKey` for non–Control-UI-visible runs, `forwardAgentEventRaw`
|
|
26
|
+
* would otherwise return early and never forward the terminal lifecycle frame.
|
|
27
|
+
*/
|
|
28
|
+
const openClawRunIdToDeviceId = new Map();
|
|
29
|
+
/** Vitest-only */
|
|
30
|
+
export function resetOpenClawRunDeviceMappingForTest() {
|
|
31
|
+
openClawRunIdToDeviceId.clear();
|
|
32
|
+
}
|
|
33
|
+
/** Parse deviceId from a Friday Next channel sessionKey (friday-{deviceId} or legacy agent:main:friday-*). */
|
|
34
|
+
export function deviceIdFromSessionKey(sessionKey) {
|
|
35
|
+
const m1 = sessionKey.match(/^friday-next-(.+)$/i);
|
|
36
|
+
if (m1)
|
|
37
|
+
return m1[1] ?? null;
|
|
38
|
+
const m2 = sessionKey.match(/^agent:main:friday-next-(.+)$/i);
|
|
39
|
+
return m2 ? m2[1] ?? null : null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* When the app uses a plain `sessionKey` (e.g. `main` → `agent:main:main` in the gateway),
|
|
43
|
+
* sub-agent / announce runs still emit `onAgentEvent` with that store key — not `friday-{deviceId}`.
|
|
44
|
+
* Each POST /friday-next/messages registers both the raw and store keys so forwards and tool hooks resolve.
|
|
45
|
+
*/
|
|
46
|
+
const sessionKeyToDeviceId = new Map();
|
|
47
|
+
/** Gateway / store session keys → app's history `sessionKey` (verbatim from POST). */
|
|
48
|
+
const gatewayKeyToHistorySessionKey = new Map();
|
|
49
|
+
/** deviceId → latest app history sessionKey (verbatim from POST). */
|
|
50
|
+
const deviceIdToLatestHistorySessionKey = new Map();
|
|
51
|
+
/** Last device that called POST /friday-next/messages (same gateway process). Used for cron/outbound when `to` is placeholder and the app is offline (no SSE). */
|
|
52
|
+
let lastRegisteredFridayDeviceId;
|
|
53
|
+
function normalizeFridaySessionKeyCase(sk) {
|
|
54
|
+
return /^friday-next-|^agent:main:friday-next-/i.test(sk) || /^agent:main:friday-next:direct:/i.test(sk)
|
|
55
|
+
? sk.toLowerCase()
|
|
56
|
+
: sk;
|
|
57
|
+
}
|
|
58
|
+
export function registerFridaySessionDeviceMapping(rawSessionKey, deviceId) {
|
|
59
|
+
const sk = rawSessionKey.trim();
|
|
60
|
+
const did = deviceId.trim().toUpperCase();
|
|
61
|
+
if (!sk || !did)
|
|
62
|
+
return;
|
|
63
|
+
const storeKey = toSessionStoreKey(sk);
|
|
64
|
+
for (const k of new Set([
|
|
65
|
+
sk,
|
|
66
|
+
storeKey,
|
|
67
|
+
normalizeFridaySessionKeyCase(sk),
|
|
68
|
+
normalizeFridaySessionKeyCase(storeKey),
|
|
69
|
+
])) {
|
|
70
|
+
sessionKeyToDeviceId.set(k, did);
|
|
71
|
+
gatewayKeyToHistorySessionKey.set(k, sk);
|
|
72
|
+
}
|
|
73
|
+
deviceIdToLatestHistorySessionKey.set(did, sk);
|
|
74
|
+
lastRegisteredFridayDeviceId = did;
|
|
75
|
+
}
|
|
76
|
+
/** In-process fallback for tool hooks / telemetry (same idea as outbound sole-device). */
|
|
77
|
+
export function getLastRegisteredFridayDeviceId() {
|
|
78
|
+
return lastRegisteredFridayDeviceId;
|
|
79
|
+
}
|
|
80
|
+
/** Resolve device for gateway `sessionKey` (friday-style or last POST mapping). */
|
|
81
|
+
export function resolveFridayDeviceIdForSessionKey(sessionKey) {
|
|
82
|
+
const mapped = sessionKeyToDeviceId.get(sessionKey) ??
|
|
83
|
+
sessionKeyToDeviceId.get(toSessionStoreKey(sessionKey)) ??
|
|
84
|
+
sessionKeyToDeviceId.get(normalizeFridaySessionKeyCase(sessionKey)) ??
|
|
85
|
+
sessionKeyToDeviceId.get(normalizeFridaySessionKeyCase(toSessionStoreKey(sessionKey)));
|
|
86
|
+
if (mapped)
|
|
87
|
+
return mapped;
|
|
88
|
+
return deviceIdFromSessionKey(sessionKey);
|
|
89
|
+
}
|
|
90
|
+
function historySessionKeyForGatewaySessionKey(sk) {
|
|
91
|
+
return (gatewayKeyToHistorySessionKey.get(sk) ??
|
|
92
|
+
gatewayKeyToHistorySessionKey.get(toSessionStoreKey(sk)) ??
|
|
93
|
+
gatewayKeyToHistorySessionKey.get(normalizeFridaySessionKeyCase(sk)) ??
|
|
94
|
+
gatewayKeyToHistorySessionKey.get(normalizeFridaySessionKeyCase(toSessionStoreKey(sk))));
|
|
95
|
+
}
|
|
96
|
+
/** Tool hooks / core may pass gateway store keys; resolve app's POST sessionKey. */
|
|
97
|
+
export function resolveFridayHistorySessionKey(gatewaySessionKey) {
|
|
98
|
+
const sk = gatewaySessionKey.trim();
|
|
99
|
+
if (!sk)
|
|
100
|
+
return undefined;
|
|
101
|
+
return historySessionKeyForGatewaySessionKey(sk);
|
|
102
|
+
}
|
|
103
|
+
/** Resolve latest known app sessionKey by deviceId (from last POST). */
|
|
104
|
+
export function latestHistorySessionKeyForDeviceId(deviceId) {
|
|
105
|
+
return deviceIdToLatestHistorySessionKey.get(deviceId.trim().toUpperCase());
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Session key hint for outbound delivery when ctx has no `sessionKey` (typical cron).
|
|
109
|
+
* Uses in-process mapping only (no plugin-side history files).
|
|
110
|
+
*/
|
|
111
|
+
export function resolveHistorySessionKeyForFridayDevice(deviceId) {
|
|
112
|
+
const did = deviceId.trim().toUpperCase();
|
|
113
|
+
if (!did || did.toLowerCase() === "friday-next")
|
|
114
|
+
return undefined;
|
|
115
|
+
const fromMemory = latestHistorySessionKeyForDeviceId(did);
|
|
116
|
+
if (fromMemory)
|
|
117
|
+
return fromMemory;
|
|
118
|
+
return `agent:main:friday-next-${did}`;
|
|
119
|
+
}
|
|
120
|
+
const DEFAULT_SESSION_STORE_AGENT_ID = "main";
|
|
121
|
+
function mergeRunMetadataIntoLifecycleEnd(runId, base) {
|
|
122
|
+
const meta = getRunMetadata(runId);
|
|
123
|
+
if (!meta)
|
|
124
|
+
return base;
|
|
125
|
+
const extra = {};
|
|
126
|
+
if (typeof meta.modelName === "string" && meta.modelName.trim()) {
|
|
127
|
+
extra.modelName = meta.modelName.trim();
|
|
128
|
+
}
|
|
129
|
+
if (typeof meta.totalTokens === "number" && Number.isFinite(meta.totalTokens) && meta.totalTokens > 0) {
|
|
130
|
+
extra.totalTokens = Math.floor(meta.totalTokens);
|
|
131
|
+
}
|
|
132
|
+
if (typeof meta.contextTokensUsed === "number" &&
|
|
133
|
+
Number.isFinite(meta.contextTokensUsed) &&
|
|
134
|
+
meta.contextTokensUsed > 0) {
|
|
135
|
+
extra.contextTokensUsed = Math.floor(meta.contextTokensUsed);
|
|
136
|
+
}
|
|
137
|
+
if (typeof meta.contextWindowMax === "number" &&
|
|
138
|
+
Number.isFinite(meta.contextWindowMax) &&
|
|
139
|
+
meta.contextWindowMax > 0) {
|
|
140
|
+
extra.contextWindowMax = Math.floor(meta.contextWindowMax);
|
|
141
|
+
}
|
|
142
|
+
if (Object.keys(extra).length === 0)
|
|
143
|
+
return base;
|
|
144
|
+
return { ...base, ...extra };
|
|
145
|
+
}
|
|
146
|
+
function tryReadSessionUsageFromStore(sessionKeyForStore) {
|
|
147
|
+
const access = getFridayAgentForwardRuntime();
|
|
148
|
+
if (!access)
|
|
149
|
+
return undefined;
|
|
150
|
+
try {
|
|
151
|
+
const cfg = access.getConfig();
|
|
152
|
+
const storeConfig = cfg?.session?.store;
|
|
153
|
+
const storePath = access.resolveStorePath(storeConfig, { agentId: DEFAULT_SESSION_STORE_AGENT_ID });
|
|
154
|
+
const store = access.loadSessionStore(storePath, { skipCache: true });
|
|
155
|
+
const canonical = toSessionStoreKey(sessionKeyForStore);
|
|
156
|
+
const entry = store[canonical] ?? store[sessionKeyForStore.trim()];
|
|
157
|
+
if (!entry || typeof entry !== "object")
|
|
158
|
+
return undefined;
|
|
159
|
+
return buildSessionUsageSnapshot(entry);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function completeAgentEventForward(params) {
|
|
166
|
+
const { evt, sk, deviceIdRaw, outgoingData, isTerminalLifecycle, subagentMeta } = params;
|
|
167
|
+
observeAgentEventForActiveRuns({ stream: evt.stream, runId: evt.runId, data: outgoingData });
|
|
168
|
+
const deviceId = deviceIdRaw.toUpperCase();
|
|
169
|
+
const targetRunId = sseEmitter.getLastRunIdForDevice(deviceId) ?? evt.runId;
|
|
170
|
+
const directToDevice = !sseEmitter.hasTrackedDevices(targetRunId);
|
|
171
|
+
const payload = {
|
|
172
|
+
runId: evt.runId,
|
|
173
|
+
seq: evt.seq,
|
|
174
|
+
ts: evt.ts,
|
|
175
|
+
stream: evt.stream,
|
|
176
|
+
data: outgoingData,
|
|
177
|
+
sessionKey: evt.sessionKey ?? sk,
|
|
178
|
+
};
|
|
179
|
+
if (directToDevice)
|
|
180
|
+
payload.deviceId = deviceId;
|
|
181
|
+
if (subagentMeta)
|
|
182
|
+
payload.subagent = subagentMeta;
|
|
183
|
+
sseEmitter.broadcastToRun(targetRunId, { type: "agent", data: payload });
|
|
184
|
+
if (isTerminalLifecycle) {
|
|
185
|
+
openClawRunIdToDeviceId.delete(evt.runId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Resolve the real device UUID for Friday outbound (`sendText` / `sendMedia`).
|
|
190
|
+
*/
|
|
191
|
+
export function resolveFridayDeviceIdForOutbound(to, rawCtx) {
|
|
192
|
+
const trimmed = (to ?? "").trim();
|
|
193
|
+
if (trimmed && trimmed.toLowerCase() !== "friday-next") {
|
|
194
|
+
return trimmed;
|
|
195
|
+
}
|
|
196
|
+
const sk = (typeof rawCtx?.requesterSessionKey === "string" && rawCtx.requesterSessionKey.trim()) ||
|
|
197
|
+
(typeof rawCtx?.sessionKey === "string" && rawCtx.sessionKey.trim()) ||
|
|
198
|
+
"";
|
|
199
|
+
if (sk) {
|
|
200
|
+
const fromSession = resolveFridayDeviceIdForSessionKey(sk);
|
|
201
|
+
if (fromSession)
|
|
202
|
+
return fromSession;
|
|
203
|
+
}
|
|
204
|
+
const sole = sseEmitter.getSoleConnectedDeviceId();
|
|
205
|
+
if (sole)
|
|
206
|
+
return sole;
|
|
207
|
+
if (lastRegisteredFridayDeviceId)
|
|
208
|
+
return lastRegisteredFridayDeviceId;
|
|
209
|
+
return trimmed || "friday-next";
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Forward global OpenClaw agent events to the Friday SSE connection (transparent).
|
|
213
|
+
*
|
|
214
|
+
* Asynchronous follow-up runs still reach the device via `getLastRunIdForDevice` when the parent run
|
|
215
|
+
* is no longer tracked.
|
|
216
|
+
*/
|
|
217
|
+
export function forwardAgentEventRaw(evt) {
|
|
218
|
+
ingestAgentEventMetadata(evt.runId, evt.data);
|
|
219
|
+
let sk = typeof evt.sessionKey === "string" ? evt.sessionKey.trim() : "";
|
|
220
|
+
if (!sk) {
|
|
221
|
+
const ctx = getOpenClawAgentRunContext(evt.runId);
|
|
222
|
+
const fromCtx = typeof ctx?.sessionKey === "string" ? ctx.sessionKey.trim() : "";
|
|
223
|
+
if (fromCtx)
|
|
224
|
+
sk = fromCtx;
|
|
225
|
+
}
|
|
226
|
+
let deviceIdRaw = sk ? resolveFridayDeviceIdForSessionKey(sk) : null;
|
|
227
|
+
if (!deviceIdRaw) {
|
|
228
|
+
const mapped = openClawRunIdToDeviceId.get(evt.runId);
|
|
229
|
+
if (mapped)
|
|
230
|
+
deviceIdRaw = mapped;
|
|
231
|
+
}
|
|
232
|
+
// Subagent runs have their deviceId in the subagent registry
|
|
233
|
+
if (!deviceIdRaw) {
|
|
234
|
+
const sub = lookupByRunId(evt.runId);
|
|
235
|
+
if (sub)
|
|
236
|
+
deviceIdRaw = sub.deviceId;
|
|
237
|
+
}
|
|
238
|
+
if (!deviceIdRaw)
|
|
239
|
+
return;
|
|
240
|
+
if (!sk) {
|
|
241
|
+
sk =
|
|
242
|
+
latestHistorySessionKeyForDeviceId(deviceIdRaw) ?? `friday-next-${deviceIdRaw}`;
|
|
243
|
+
}
|
|
244
|
+
openClawRunIdToDeviceId.set(evt.runId, deviceIdRaw.toUpperCase());
|
|
245
|
+
// Register sessionKey → runId so we can resolve parentRunId
|
|
246
|
+
if (sk && evt.stream === "lifecycle" && evt.data.phase === "start") {
|
|
247
|
+
registerSessionKeyForRun(sk, evt.runId);
|
|
248
|
+
}
|
|
249
|
+
// ── sessions_spawn tool → subagent lifecycle (replaces hooks) ──
|
|
250
|
+
const isSpawnTool = evt.stream === "tool" && (evt.data.name === "sessions_spawn" || evt.data.name === "task");
|
|
251
|
+
// Phase 1: spawning — tool.start with taskName in args
|
|
252
|
+
if (isSpawnTool && evt.data.phase === "start") {
|
|
253
|
+
const toolCallId = typeof evt.data.toolCallId === "string" ? evt.data.toolCallId : "";
|
|
254
|
+
const args = evt.data.args;
|
|
255
|
+
const label = typeof args?.taskName === "string" ? args.taskName : undefined;
|
|
256
|
+
if (toolCallId) {
|
|
257
|
+
const intent = registerSpawnIntent({
|
|
258
|
+
toolCallId,
|
|
259
|
+
label,
|
|
260
|
+
deviceId: deviceIdRaw,
|
|
261
|
+
parentRunId: evt.runId,
|
|
262
|
+
requesterSessionKey: sk || undefined,
|
|
263
|
+
});
|
|
264
|
+
sseEmitter.broadcast({
|
|
265
|
+
type: "subagent",
|
|
266
|
+
data: {
|
|
267
|
+
phase: "spawning",
|
|
268
|
+
childSessionKey: null,
|
|
269
|
+
runId: null,
|
|
270
|
+
label: intent.label ?? null,
|
|
271
|
+
parentRunId: intent.parentRunId,
|
|
272
|
+
depth: intent.depth,
|
|
273
|
+
deviceId: intent.deviceId,
|
|
274
|
+
},
|
|
275
|
+
}, intent.deviceId);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Phase 2: spawned — tool.result with childSessionKey + runId
|
|
279
|
+
if (isSpawnTool && evt.data.phase === "result") {
|
|
280
|
+
const details = evt.data.result?.details;
|
|
281
|
+
if (details?.childSessionKey) {
|
|
282
|
+
const toolCallId = typeof evt.data.toolCallId === "string" ? evt.data.toolCallId : "";
|
|
283
|
+
const intent = toolCallId ? consumeSpawnIntent(toolCallId) : undefined;
|
|
284
|
+
const label = details.taskName ||
|
|
285
|
+
intent?.label ||
|
|
286
|
+
(typeof evt.data.meta === "string"
|
|
287
|
+
? evt.data.meta
|
|
288
|
+
: undefined);
|
|
289
|
+
const entry = ensureSubagentFromSpawnTool({
|
|
290
|
+
childSessionKey: details.childSessionKey,
|
|
291
|
+
bareRunId: details.runId,
|
|
292
|
+
label,
|
|
293
|
+
deviceId: deviceIdRaw,
|
|
294
|
+
parentRunId: intent?.parentRunId ?? evt.runId,
|
|
295
|
+
requesterSessionKey: sk,
|
|
296
|
+
depth: intent?.depth,
|
|
297
|
+
});
|
|
298
|
+
const compoundRunId = entry.runId ?? evt.runId;
|
|
299
|
+
sseEmitter.trackDeviceForRun(entry.deviceId, compoundRunId);
|
|
300
|
+
sseEmitter.broadcast({
|
|
301
|
+
type: "subagent",
|
|
302
|
+
data: {
|
|
303
|
+
phase: "spawned",
|
|
304
|
+
runId: compoundRunId,
|
|
305
|
+
childSessionKey: entry.childSessionKey,
|
|
306
|
+
label: entry.label ?? null,
|
|
307
|
+
parentRunId: entry.parentRunId ?? null,
|
|
308
|
+
depth: entry.depth,
|
|
309
|
+
deviceId: entry.deviceId,
|
|
310
|
+
},
|
|
311
|
+
}, entry.deviceId);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const subagentEntry = lookupByRunId(evt.runId);
|
|
315
|
+
// Only annotate events that originate from the subagent itself
|
|
316
|
+
// (sessionKey matches childSessionKey). Main-agent delivery events
|
|
317
|
+
// share the announce runId but have a different sessionKey.
|
|
318
|
+
const isSubagentOwnEvent = subagentEntry && sk && subagentEntry.childSessionKey === sk;
|
|
319
|
+
const subagentMeta = isSubagentOwnEvent
|
|
320
|
+
? { label: subagentEntry.label, parentRunId: subagentEntry.parentRunId, depth: subagentEntry.depth }
|
|
321
|
+
: undefined;
|
|
322
|
+
let outgoingData = { ...evt.data };
|
|
323
|
+
if (evt.stream === "thinking") {
|
|
324
|
+
const currentText = typeof evt.data.text === "string" ? evt.data.text : "";
|
|
325
|
+
const prior = lastThinkingTextByRun.get(evt.runId) ?? "";
|
|
326
|
+
const prefixLen = commonPrefixLength(prior, currentText);
|
|
327
|
+
const delta = currentText.slice(prefixLen);
|
|
328
|
+
lastThinkingTextByRun.set(evt.runId, currentText);
|
|
329
|
+
outgoingData = {
|
|
330
|
+
...evt.data,
|
|
331
|
+
text: currentText,
|
|
332
|
+
delta,
|
|
333
|
+
reasoningPrefixChars: prefixLen,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
else if (evt.stream === "lifecycle") {
|
|
337
|
+
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
|
338
|
+
if (phase === "end") {
|
|
339
|
+
outgoingData = mergeRunMetadataIntoLifecycleEnd(evt.runId, outgoingData);
|
|
340
|
+
}
|
|
341
|
+
if (phase === "end" || phase === "error") {
|
|
342
|
+
lastThinkingTextByRun.delete(evt.runId);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const lifecyclePhase = evt.stream === "lifecycle" && typeof evt.data.phase === "string" ? evt.data.phase : "";
|
|
346
|
+
const isTerminalLifecycle = evt.stream === "lifecycle" && (lifecyclePhase === "end" || lifecyclePhase === "error");
|
|
347
|
+
// Emit subagent ended SSE when a subagent run terminates
|
|
348
|
+
if (isTerminalLifecycle && isSubagentOwnEvent && subagentEntry.status !== "ended") {
|
|
349
|
+
const outcome = lifecyclePhase === "error" ? "error" : "ok";
|
|
350
|
+
const errorStr = lifecyclePhase === "error" ? String(evt.data.error ?? "unknown") : undefined;
|
|
351
|
+
const ended = registerSubagentEnded({ runId: evt.runId, outcome, error: errorStr });
|
|
352
|
+
if (ended) {
|
|
353
|
+
sseEmitter.broadcast({
|
|
354
|
+
type: "subagent",
|
|
355
|
+
data: {
|
|
356
|
+
phase: "ended",
|
|
357
|
+
runId: ended.runId ?? evt.runId ?? null,
|
|
358
|
+
childSessionKey: ended.childSessionKey,
|
|
359
|
+
label: ended.label ?? null,
|
|
360
|
+
parentRunId: ended.parentRunId ?? null,
|
|
361
|
+
depth: ended.depth,
|
|
362
|
+
deviceId: ended.deviceId,
|
|
363
|
+
outcome: ended.outcome ?? null,
|
|
364
|
+
error: ended.error ?? null,
|
|
365
|
+
},
|
|
366
|
+
}, ended.deviceId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (isTerminalLifecycle && getFridayAgentForwardRuntime()) {
|
|
370
|
+
setImmediate(() => {
|
|
371
|
+
let data = outgoingData;
|
|
372
|
+
const usage = tryReadSessionUsageFromStore(sk);
|
|
373
|
+
if (usage) {
|
|
374
|
+
data = { ...outgoingData, sessionUsage: usage };
|
|
375
|
+
}
|
|
376
|
+
completeAgentEventForward({
|
|
377
|
+
evt,
|
|
378
|
+
sk,
|
|
379
|
+
deviceIdRaw,
|
|
380
|
+
outgoingData: data,
|
|
381
|
+
isTerminalLifecycle: true,
|
|
382
|
+
subagentMeta,
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
completeAgentEventForward({
|
|
388
|
+
evt,
|
|
389
|
+
sk,
|
|
390
|
+
deviceIdRaw,
|
|
391
|
+
outgoingData,
|
|
392
|
+
isTerminalLifecycle,
|
|
393
|
+
subagentMeta,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getHostOpenClawConfigSnapshot(config: unknown): unknown;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function isHostConfigLoader(value) {
|
|
2
|
+
return (!!value &&
|
|
3
|
+
typeof value === "object" &&
|
|
4
|
+
typeof value.loadConfig === "function");
|
|
5
|
+
}
|
|
6
|
+
export function getHostOpenClawConfigSnapshot(config) {
|
|
7
|
+
if (!isHostConfigLoader(config))
|
|
8
|
+
return {};
|
|
9
|
+
try {
|
|
10
|
+
return config.loadConfig();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { abortRun } from "../../agent/abort-run.js";
|
|
2
|
+
import { sseEmitter } from "../../sse/emitter.js";
|
|
3
|
+
import { readJsonBody } from "../middleware/body.js";
|
|
4
|
+
import { extractBearerToken } from "../middleware/auth.js";
|
|
5
|
+
export async function handleCancel(req, res) {
|
|
6
|
+
if (req.method !== "POST") {
|
|
7
|
+
res.statusCode = 405;
|
|
8
|
+
res.setHeader("Content-Type", "application/json");
|
|
9
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
const token = extractBearerToken(req);
|
|
13
|
+
if (!token) {
|
|
14
|
+
res.statusCode = 401;
|
|
15
|
+
res.setHeader("Content-Type", "application/json");
|
|
16
|
+
res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const body = await readJsonBody(req);
|
|
20
|
+
const runId = typeof body?.runId === "string" ? body.runId.trim() : "";
|
|
21
|
+
if (!runId) {
|
|
22
|
+
res.statusCode = 400;
|
|
23
|
+
res.setHeader("Content-Type", "application/json");
|
|
24
|
+
res.end(JSON.stringify({ error: "Missing runId" }));
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
await abortRun(runId);
|
|
28
|
+
sseEmitter.untrackRun(runId);
|
|
29
|
+
res.statusCode = 200;
|
|
30
|
+
res.setHeader("Content-Type", "application/json");
|
|
31
|
+
res.end(JSON.stringify({ ok: true, runId, cancelled: true }));
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { readJsonBody } from "../middleware/body.js";
|
|
3
|
+
import { extractBearerToken } from "../middleware/auth.js";
|
|
4
|
+
import { createFridayNextLogger } from "../../logging.js";
|
|
5
|
+
const EXEC_ENV = process.platform === "win32"
|
|
6
|
+
? process.env
|
|
7
|
+
: { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:${process.env.PATH ?? ""}` };
|
|
8
|
+
function execAsync(command, timeoutMs) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const child = exec(command, { encoding: "utf-8", timeout: timeoutMs, maxBuffer: 1024 * 1024, env: EXEC_ENV }, (error, stdout, stderr) => {
|
|
11
|
+
if (error) {
|
|
12
|
+
reject(error);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
resolve({ stdout, stderr });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
child.stdout?.on("data", () => { });
|
|
19
|
+
child.stderr?.on("data", () => { });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function handleDeviceApprove(req, res) {
|
|
23
|
+
const log = createFridayNextLogger("device-approve");
|
|
24
|
+
if (req.method !== "POST") {
|
|
25
|
+
res.statusCode = 405;
|
|
26
|
+
res.setHeader("Content-Type", "application/json");
|
|
27
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
const token = extractBearerToken(req);
|
|
31
|
+
if (!token) {
|
|
32
|
+
res.statusCode = 401;
|
|
33
|
+
res.setHeader("Content-Type", "application/json");
|
|
34
|
+
res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const body = await readJsonBody(req);
|
|
38
|
+
if (!body) {
|
|
39
|
+
res.statusCode = 400;
|
|
40
|
+
res.setHeader("Content-Type", "application/json");
|
|
41
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
const rawDeviceId = typeof body.deviceId === "string" ? body.deviceId : "";
|
|
45
|
+
if (!rawDeviceId.trim()) {
|
|
46
|
+
res.statusCode = 400;
|
|
47
|
+
res.setHeader("Content-Type", "application/json");
|
|
48
|
+
res.end(JSON.stringify({ error: "Missing required field: deviceId" }));
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const normalizedDeviceId = rawDeviceId.trim().toUpperCase();
|
|
52
|
+
let listStdout;
|
|
53
|
+
try {
|
|
54
|
+
const result = await execAsync("openclaw devices list --json", 15000);
|
|
55
|
+
listStdout = result.stdout;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const stderr = err?.stderr?.trim();
|
|
59
|
+
log.error(`devices list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
res.statusCode = 502;
|
|
61
|
+
res.setHeader("Content-Type", "application/json");
|
|
62
|
+
res.end(JSON.stringify({ error: "Failed to list devices from gateway", detail: stderr || undefined }));
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
let listData;
|
|
66
|
+
try {
|
|
67
|
+
listData = JSON.parse(listStdout);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
log.error(`devices list returned invalid JSON: ${listStdout.slice(0, 200)}`);
|
|
71
|
+
res.statusCode = 502;
|
|
72
|
+
res.setHeader("Content-Type", "application/json");
|
|
73
|
+
res.end(JSON.stringify({ error: "Unexpected response from gateway device list" }));
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const pending = listData.pending ?? [];
|
|
77
|
+
const match = pending.find((entry) => entry.deviceId.trim().toUpperCase() === normalizedDeviceId);
|
|
78
|
+
if (!match) {
|
|
79
|
+
res.statusCode = 404;
|
|
80
|
+
res.setHeader("Content-Type", "application/json");
|
|
81
|
+
res.end(JSON.stringify({
|
|
82
|
+
error: "No pending device found for this deviceId",
|
|
83
|
+
deviceId: normalizedDeviceId,
|
|
84
|
+
}));
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const requestId = match.requestId;
|
|
88
|
+
log.info(`approving deviceId=${normalizedDeviceId} requestId=${requestId}`);
|
|
89
|
+
let approveStdout;
|
|
90
|
+
try {
|
|
91
|
+
const result = await execAsync(`openclaw devices approve ${requestId} --json`, 15000);
|
|
92
|
+
approveStdout = result.stdout;
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const stderr = err?.stderr?.trim();
|
|
96
|
+
log.error(`devices approve failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
97
|
+
res.statusCode = 502;
|
|
98
|
+
res.setHeader("Content-Type", "application/json");
|
|
99
|
+
res.end(JSON.stringify({
|
|
100
|
+
error: "Device approval command failed",
|
|
101
|
+
detail: stderr || (err instanceof Error ? err.message : "Unknown error"),
|
|
102
|
+
}));
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
let approveData;
|
|
106
|
+
try {
|
|
107
|
+
approveData = JSON.parse(approveStdout);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
log.error(`devices approve returned non-JSON: ${approveStdout.slice(0, 200)}`);
|
|
111
|
+
res.statusCode = 502;
|
|
112
|
+
res.setHeader("Content-Type", "application/json");
|
|
113
|
+
res.end(JSON.stringify({ error: "Unexpected response from device approval" }));
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
res.statusCode = 200;
|
|
117
|
+
res.setHeader("Content-Type", "application/json");
|
|
118
|
+
res.end(JSON.stringify({
|
|
119
|
+
ok: true,
|
|
120
|
+
deviceId: normalizedDeviceId,
|
|
121
|
+
requestId: approveData.requestId,
|
|
122
|
+
approvedAtMs: approveData.device?.approvedAtMs,
|
|
123
|
+
}));
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File download handler for GET /friday-next/files/:id
|
|
3
|
+
*
|
|
4
|
+
* Sources checked (in order):
|
|
5
|
+
* 1. In-memory file index (POST /friday-next/files and resolved attachments this session)
|
|
6
|
+
* 2. Plugin-root `attachments/` on disk (same basename as URL token; survives restarts)
|
|
7
|
+
* 3. OpenClaw media buffer (~/.openclaw/media/inbound/<id>)
|
|
8
|
+
*/
|
|
9
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
10
|
+
export declare function handleFilesDownload(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|