@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.
Files changed (97) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.js +182 -0
  3. package/dist/src/agent/abort-run.d.ts +1 -0
  4. package/dist/src/agent/abort-run.js +11 -0
  5. package/dist/src/agent/active-runs.d.ts +9 -0
  6. package/dist/src/agent/active-runs.js +20 -0
  7. package/dist/src/agent/dispatch-bridge.d.ts +5 -0
  8. package/dist/src/agent/dispatch-bridge.js +12 -0
  9. package/dist/src/agent/media-bridge.d.ts +4 -0
  10. package/dist/src/agent/media-bridge.js +21 -0
  11. package/dist/src/agent/subagent-registry.d.ts +68 -0
  12. package/dist/src/agent/subagent-registry.js +142 -0
  13. package/dist/src/agent-forward-runtime.d.ts +17 -0
  14. package/dist/src/agent-forward-runtime.js +16 -0
  15. package/dist/src/agent-run-context-bridge.d.ts +13 -0
  16. package/dist/src/agent-run-context-bridge.js +23 -0
  17. package/dist/src/channel-actions.d.ts +13 -0
  18. package/dist/src/channel-actions.js +101 -0
  19. package/dist/src/channel.d.ts +6 -0
  20. package/dist/src/channel.js +248 -0
  21. package/dist/src/collect-message-media-paths.d.ts +11 -0
  22. package/dist/src/collect-message-media-paths.js +143 -0
  23. package/dist/src/config.d.ts +15 -0
  24. package/dist/src/config.js +39 -0
  25. package/dist/src/friday-inbound-stats.d.ts +2 -0
  26. package/dist/src/friday-inbound-stats.js +8 -0
  27. package/dist/src/friday-session.d.ts +40 -0
  28. package/dist/src/friday-session.js +395 -0
  29. package/dist/src/host-config.d.ts +1 -0
  30. package/dist/src/host-config.js +15 -0
  31. package/dist/src/http/handlers/cancel.d.ts +2 -0
  32. package/dist/src/http/handlers/cancel.js +33 -0
  33. package/dist/src/http/handlers/device-approve.d.ts +2 -0
  34. package/dist/src/http/handlers/device-approve.js +125 -0
  35. package/dist/src/http/handlers/files-download.d.ts +10 -0
  36. package/dist/src/http/handlers/files-download.js +210 -0
  37. package/dist/src/http/handlers/files-upload.d.ts +8 -0
  38. package/dist/src/http/handlers/files-upload.js +136 -0
  39. package/dist/src/http/handlers/files.d.ts +75 -0
  40. package/dist/src/http/handlers/files.js +305 -0
  41. package/dist/src/http/handlers/messages.d.ts +34 -0
  42. package/dist/src/http/handlers/messages.js +476 -0
  43. package/dist/src/http/handlers/models-list.d.ts +10 -0
  44. package/dist/src/http/handlers/models-list.js +113 -0
  45. package/dist/src/http/handlers/nodes-approve.d.ts +2 -0
  46. package/dist/src/http/handlers/nodes-approve.js +146 -0
  47. package/dist/src/http/handlers/sessions-delete.d.ts +2 -0
  48. package/dist/src/http/handlers/sessions-delete.js +49 -0
  49. package/dist/src/http/handlers/sessions-settings.d.ts +2 -0
  50. package/dist/src/http/handlers/sessions-settings.js +71 -0
  51. package/dist/src/http/handlers/sse.d.ts +2 -0
  52. package/dist/src/http/handlers/sse.js +70 -0
  53. package/dist/src/http/handlers/status.d.ts +2 -0
  54. package/dist/src/http/handlers/status.js +29 -0
  55. package/dist/src/http/middleware/auth.d.ts +13 -0
  56. package/dist/src/http/middleware/auth.js +29 -0
  57. package/dist/src/http/middleware/body.d.ts +2 -0
  58. package/dist/src/http/middleware/body.js +24 -0
  59. package/dist/src/http/middleware/cors.d.ts +2 -0
  60. package/dist/src/http/middleware/cors.js +11 -0
  61. package/dist/src/http/server.d.ts +19 -0
  62. package/dist/src/http/server.js +87 -0
  63. package/dist/src/logging.d.ts +7 -0
  64. package/dist/src/logging.js +28 -0
  65. package/dist/src/run-metadata.d.ts +25 -0
  66. package/dist/src/run-metadata.js +139 -0
  67. package/dist/src/runtime.d.ts +13 -0
  68. package/dist/src/runtime.js +5 -0
  69. package/dist/src/session/session-manager.d.ts +22 -0
  70. package/dist/src/session/session-manager.js +190 -0
  71. package/dist/src/session-usage-snapshot.d.ts +23 -0
  72. package/dist/src/session-usage-snapshot.js +65 -0
  73. package/dist/src/sse/emitter.d.ts +59 -0
  74. package/dist/src/sse/emitter.js +219 -0
  75. package/dist/src/sse/offline-queue.d.ts +26 -0
  76. package/dist/src/sse/offline-queue.js +134 -0
  77. package/dist/src/vendor/runtime-store.d.ts +26 -0
  78. package/dist/src/vendor/runtime-store.js +60 -0
  79. package/index.ts +10 -4
  80. package/package.json +11 -10
  81. package/src/agent/subagent-registry.ts +195 -0
  82. package/src/channel.ts +6 -4
  83. package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
  84. package/src/e2e/subagent.e2e.test.ts +502 -0
  85. package/src/friday-session.ts +140 -1
  86. package/src/http/handlers/device-approve.test.ts +0 -1
  87. package/src/http/handlers/device-approve.ts +0 -2
  88. package/src/http/handlers/files-download.ts +4 -1
  89. package/src/http/handlers/files.ts +7 -4
  90. package/src/http/handlers/messages.ts +54 -4
  91. package/src/http/handlers/models-list.ts +24 -2
  92. package/src/http/handlers/nodes-approve.test.ts +288 -0
  93. package/src/http/handlers/nodes-approve.ts +189 -0
  94. package/src/http/server.ts +5 -0
  95. package/src/openclaw.d.ts +5 -0
  96. package/src/sse/emitter.ts +1 -1
  97. package/src/test-support/mock-runtime.ts +2 -0
@@ -0,0 +1,4 @@
1
+ export { fridayNextChannelPlugin } from "./src/channel.js";
2
+ export { setFridayNextRuntime } from "./src/runtime.js";
3
+ declare const _default: any;
4
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,182 @@
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
2
+ import { fridayNextChannelPlugin } from "./src/channel.js";
3
+ import { setFridayNextRuntime } from "./src/runtime.js";
4
+ import { resolveFridayNextConfig } from "./src/config.js";
5
+ import { getHostOpenClawConfigSnapshot } from "./src/host-config.js";
6
+ import { registerFridayNextHttpRoutes } from "./src/http/server.js";
7
+ import { getFridayNextRuntime } from "./src/runtime.js";
8
+ import { sseEmitter } from "./src/sse/emitter.js";
9
+ import { forwardAgentEventRaw, getLastRegisteredFridayDeviceId, resolveFridayDeviceIdForSessionKey, } from "./src/friday-session.js";
10
+ import { setFridayAgentForwardRuntime } from "./src/agent-forward-runtime.js";
11
+ import { getOpenClawAgentRunContext } from "./src/agent-run-context-bridge.js";
12
+ export { fridayNextChannelPlugin } from "./src/channel.js";
13
+ export { setFridayNextRuntime } from "./src/runtime.js";
14
+ /** `api.on` returns void — register tool hooks at most once per process. */
15
+ let fridayNextToolHooksRegistered = false;
16
+ let disposeAgentEventListener = null;
17
+ /**
18
+ * Track the last `api` instance on which HTTP routes were registered.
19
+ * When the health-monitor restarts the plugin, `registerFull` receives a fresh `api` whose
20
+ * old routes are gone — we must re-register. A WeakRef lets us distinguish "same api,
21
+ * re-entered" (skip) from "new api after restart" (re-register).
22
+ */
23
+ let lastApiRoutesRegistered = null;
24
+ function deviceIdFromToolContext(ctx) {
25
+ if (ctx.runId) {
26
+ const d = sseEmitter.getDeviceIdByRunId(ctx.runId);
27
+ if (d)
28
+ return d;
29
+ }
30
+ const sk = typeof ctx.sessionKey === "string" && ctx.sessionKey.trim()
31
+ ? ctx.sessionKey.trim()
32
+ : (ctx.runId ? getOpenClawAgentRunContext(ctx.runId)?.sessionKey?.trim() : undefined) ?? "";
33
+ if (sk) {
34
+ const d = resolveFridayDeviceIdForSessionKey(sk);
35
+ if (d)
36
+ return d;
37
+ }
38
+ const sole = sseEmitter.getSoleConnectedDeviceId();
39
+ if (sole)
40
+ return sole;
41
+ const last = getLastRegisteredFridayDeviceId();
42
+ if (last)
43
+ return last;
44
+ return null;
45
+ }
46
+ function isFridaySessionKey(sk) {
47
+ return /^friday-next-/i.test(sk) || /^agent:main:friday-next-/i.test(sk);
48
+ }
49
+ function shouldForwardToolEventToFriday(ctx) {
50
+ if (ctx.runId) {
51
+ if (sseEmitter.getDeviceIdByRunId(ctx.runId))
52
+ return true;
53
+ const runSk = getOpenClawAgentRunContext(ctx.runId)?.sessionKey?.trim() ?? "";
54
+ if (runSk) {
55
+ if (resolveFridayDeviceIdForSessionKey(runSk))
56
+ return true;
57
+ if (isFridaySessionKey(runSk))
58
+ return true;
59
+ }
60
+ }
61
+ const sk = typeof ctx.sessionKey === "string" ? ctx.sessionKey.trim() : "";
62
+ if (sk) {
63
+ if (resolveFridayDeviceIdForSessionKey(sk))
64
+ return true;
65
+ if (isFridaySessionKey(sk))
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ export default defineChannelPluginEntry({
71
+ id: "friday-next",
72
+ name: "Friday Next",
73
+ description: "Friday Next Apple 应用通道",
74
+ plugin: fridayNextChannelPlugin,
75
+ setRuntime: setFridayNextRuntime,
76
+ registerFull: (api) => {
77
+ setFridayAgentForwardRuntime(api);
78
+ const sameApi = lastApiRoutesRegistered?.deref() === api;
79
+ if (!sameApi) {
80
+ lastApiRoutesRegistered = new WeakRef(api);
81
+ registerFridayNextHttpRoutes(api);
82
+ }
83
+ else {
84
+ const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
85
+ sseEmitter.setBacklogLimit(cfg.sseBacklogPerDevice);
86
+ }
87
+ disposeAgentEventListener?.();
88
+ disposeAgentEventListener = api.runtime.events.onAgentEvent((evt) => {
89
+ forwardAgentEventRaw({
90
+ runId: evt.runId,
91
+ seq: evt.seq,
92
+ ts: evt.ts,
93
+ stream: evt.stream,
94
+ data: evt.data,
95
+ sessionKey: evt.sessionKey,
96
+ });
97
+ });
98
+ if (fridayNextToolHooksRegistered) {
99
+ return;
100
+ }
101
+ fridayNextToolHooksRegistered = true;
102
+ api.on("subagent_delivery_target", (event) => {
103
+ if (!event.expectsCompletionMessage)
104
+ return;
105
+ const ch = event.requesterOrigin?.channel?.trim().toLowerCase();
106
+ if (ch !== "friday-next")
107
+ return;
108
+ const sk = event.requesterSessionKey?.trim();
109
+ if (!sk)
110
+ return;
111
+ const raw = resolveFridayDeviceIdForSessionKey(sk);
112
+ if (!raw)
113
+ return;
114
+ const to = raw.toUpperCase();
115
+ return {
116
+ origin: {
117
+ channel: "friday-next",
118
+ accountId: event.requesterOrigin?.accountId?.trim() || "default",
119
+ to,
120
+ },
121
+ };
122
+ });
123
+ api.on("before_tool_call", (event, ctx) => {
124
+ if (!shouldForwardToolEventToFriday(ctx))
125
+ return;
126
+ const deviceId = deviceIdFromToolContext(ctx);
127
+ const runId = ctx.runId ?? "(unknown)";
128
+ const logLine = (detail) => {
129
+ const ts = new Date().toISOString();
130
+ console.error(`[Friday-HOOK] [${ts}] [TOOL_CALL] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`);
131
+ };
132
+ if (!deviceId) {
133
+ logLine("SKIP_no_deviceId");
134
+ return;
135
+ }
136
+ logLine("START");
137
+ sseEmitter.broadcastToolEvent(deviceId.toUpperCase(), runId, {
138
+ type: "tool-hook",
139
+ data: {
140
+ when: "before",
141
+ runId,
142
+ deviceId: deviceId.toUpperCase(),
143
+ sessionKey: ctx.sessionKey,
144
+ toolName: event.toolName,
145
+ params: event.params,
146
+ ts: Date.now(),
147
+ },
148
+ });
149
+ });
150
+ api.on("after_tool_call", (event, ctx) => {
151
+ if (!shouldForwardToolEventToFriday(ctx))
152
+ return;
153
+ const deviceId = deviceIdFromToolContext(ctx);
154
+ const runId = ctx.runId ?? "(unknown)";
155
+ const logLine = (detail) => {
156
+ const ts = new Date().toISOString();
157
+ console.error(`[Friday-HOOK] [${ts}] [TOOL_DONE] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`);
158
+ };
159
+ if (!deviceId) {
160
+ logLine("SKIP_no_deviceId");
161
+ return;
162
+ }
163
+ logLine("END");
164
+ const normalizedDeviceId = deviceId.toUpperCase();
165
+ sseEmitter.broadcastToolEvent(normalizedDeviceId, runId, {
166
+ type: "tool-hook",
167
+ data: {
168
+ when: "after",
169
+ runId,
170
+ deviceId: normalizedDeviceId,
171
+ sessionKey: ctx.sessionKey,
172
+ toolName: event.toolName,
173
+ toolCallId: event.toolCallId,
174
+ error: event.error ?? null,
175
+ result: event.result,
176
+ durationMs: event.durationMs ?? null,
177
+ ts: Date.now(),
178
+ },
179
+ });
180
+ });
181
+ },
182
+ });
@@ -0,0 +1 @@
1
+ export declare function abortRun(runId: string): Promise<void>;
@@ -0,0 +1,11 @@
1
+ export async function abortRun(runId) {
2
+ if (process.env.VITEST !== "true") {
3
+ try {
4
+ const { abortAgentHarnessRun } = await import("openclaw/plugin-sdk/agent-harness");
5
+ abortAgentHarnessRun(runId);
6
+ }
7
+ catch {
8
+ // optional at runtime
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,9 @@
1
+ /** Tracks agent runs that have emitted lifecycle `phase: start` without matching `end`/`error`. */
2
+ export declare function observeAgentEventForActiveRuns(evt: {
3
+ stream: string;
4
+ runId: string;
5
+ data: Record<string, unknown>;
6
+ }): void;
7
+ export declare function getActiveRunIds(): string[];
8
+ export declare function getActiveRunCount(): number;
9
+ export declare function resetActiveRunsForTest(): void;
@@ -0,0 +1,20 @@
1
+ /** Tracks agent runs that have emitted lifecycle `phase: start` without matching `end`/`error`. */
2
+ const active = new Set();
3
+ export function observeAgentEventForActiveRuns(evt) {
4
+ if (evt.stream !== "lifecycle")
5
+ return;
6
+ const phase = evt.data.phase;
7
+ if (phase === "start")
8
+ active.add(evt.runId);
9
+ if (phase === "end" || phase === "error")
10
+ active.delete(evt.runId);
11
+ }
12
+ export function getActiveRunIds() {
13
+ return [...active];
14
+ }
15
+ export function getActiveRunCount() {
16
+ return active.size;
17
+ }
18
+ export function resetActiveRunsForTest() {
19
+ active.clear();
20
+ }
@@ -0,0 +1,5 @@
1
+ type DispatchFn = (args: unknown) => Promise<unknown> | unknown;
2
+ export declare function runFridayDispatch(args: Parameters<DispatchFn>[0]): ReturnType<DispatchFn>;
3
+ export declare function __setMockFridayDispatchForTests(fn: DispatchFn): void;
4
+ export declare function __resetMockFridayDispatchForTests(): void;
5
+ export {};
@@ -0,0 +1,12 @@
1
+ let overrideDispatch = null;
2
+ export function runFridayDispatch(args) {
3
+ if (overrideDispatch)
4
+ return overrideDispatch(args);
5
+ return import("openclaw/plugin-sdk/reply-dispatch-runtime").then((m) => m.dispatchReplyWithDispatcher(args));
6
+ }
7
+ export function __setMockFridayDispatchForTests(fn) {
8
+ overrideDispatch = fn;
9
+ }
10
+ export function __resetMockFridayDispatchForTests() {
11
+ overrideDispatch = null;
12
+ }
@@ -0,0 +1,4 @@
1
+ export declare function saveInboundMediaBuffer(buffer: Buffer, mimeType: string): Promise<{
2
+ id: string;
3
+ path: string;
4
+ }>;
@@ -0,0 +1,21 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ export async function saveInboundMediaBuffer(buffer, mimeType) {
6
+ try {
7
+ const sdk = await import("openclaw/plugin-sdk/media-store");
8
+ const saved = await sdk.saveMediaBuffer(buffer, mimeType, "inbound");
9
+ if (saved?.id && saved?.path)
10
+ return { id: saved.id, path: saved.path };
11
+ }
12
+ catch {
13
+ // fallback for tests or stripped runtime
14
+ }
15
+ const id = crypto.randomUUID();
16
+ const dir = path.join(os.tmpdir(), "friday-next-media");
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ const p = path.join(dir, id);
19
+ fs.writeFileSync(p, buffer);
20
+ return { id, path: p };
21
+ }
@@ -0,0 +1,68 @@
1
+ export type SubagentStatus = "spawning" | "running" | "ended";
2
+ export type SubagentOutcome = "ok" | "error" | "timeout" | "killed" | "reset" | "deleted";
3
+ export type SubagentEntry = {
4
+ childSessionKey: string;
5
+ runId?: string;
6
+ parentRunId?: string;
7
+ deviceId: string;
8
+ label?: string;
9
+ depth: number;
10
+ status: SubagentStatus;
11
+ outcome?: SubagentOutcome;
12
+ error?: string;
13
+ };
14
+ export type SubagentMeta = {
15
+ label?: string;
16
+ parentRunId?: string;
17
+ depth: number;
18
+ };
19
+ type SpawnIntent = {
20
+ label?: string;
21
+ parentRunId: string;
22
+ deviceId: string;
23
+ depth: number;
24
+ requesterSessionKey?: string;
25
+ };
26
+ /** Called by forwardAgentEventRaw on lifecycle start so we can resolve parentRunId. */
27
+ export declare function registerSessionKeyForRun(sessionKey: string, runId: string): void;
28
+ /** Look up subagent entry by any runId form (announce compound or bare). */
29
+ export declare function lookupByRunId(runId: string): SubagentEntry | undefined;
30
+ /** Look up subagent entry by childSessionKey. */
31
+ export declare function lookupByChildSessionKey(key: string): SubagentEntry | undefined;
32
+ /**
33
+ * Called on sessions_spawn item.start (before tool execution).
34
+ * Stores the spawn intent so spawning SSE can be emitted immediately.
35
+ */
36
+ export declare function registerSpawnIntent(params: {
37
+ toolCallId: string;
38
+ label?: string;
39
+ deviceId: string;
40
+ parentRunId: string;
41
+ requesterSessionKey?: string;
42
+ }): SpawnIntent;
43
+ /** Consume a previously registered spawn intent. Returns undefined if none. */
44
+ export declare function consumeSpawnIntent(toolCallId: string): SpawnIntent | undefined;
45
+ /**
46
+ * Called when forwardAgentEventRaw sees a sessions_spawn tool result.
47
+ * Extracts childSessionKey, runId, taskName and registers the subagent.
48
+ */
49
+ export declare function ensureSubagentFromSpawnTool(params: {
50
+ childSessionKey: string;
51
+ bareRunId?: string;
52
+ label?: string;
53
+ deviceId: string;
54
+ parentRunId: string;
55
+ requesterSessionKey?: string;
56
+ depth?: number;
57
+ }): SubagentEntry;
58
+ /** Mark subagent as ended. Resolves by bare/compound runId or childSessionKey. */
59
+ export declare function registerEnded(params: {
60
+ runId?: string;
61
+ childSessionKey?: string;
62
+ outcome?: SubagentOutcome;
63
+ error?: string;
64
+ }): SubagentEntry | undefined;
65
+ /** Extract subagent metadata for SSE annotation. */
66
+ export declare function subagentMeta(entry: SubagentEntry): SubagentMeta;
67
+ export declare function resetForTest(): void;
68
+ export {};
@@ -0,0 +1,142 @@
1
+ const byChildSessionKey = new Map();
2
+ const byRunId = new Map();
3
+ const byToolCallId = new Map();
4
+ const sessionKeyToRunId = new Map();
5
+ /** Called by forwardAgentEventRaw on lifecycle start so we can resolve parentRunId. */
6
+ export function registerSessionKeyForRun(sessionKey, runId) {
7
+ if (!sessionKey || !runId)
8
+ return;
9
+ sessionKeyToRunId.set(sessionKey, runId);
10
+ }
11
+ function resolveRunIdForSessionKey(sessionKey) {
12
+ return sessionKeyToRunId.get(sessionKey);
13
+ }
14
+ /**
15
+ * Parse OpenClaw announce compound runId:
16
+ * announce:v<version>:<sessionKey>:<bareRunId>
17
+ * Example:
18
+ * announce:v1:agent:main:subagent:uuid:runId
19
+ * → { childSessionKey: "agent:main:subagent:uuid", bareRunId: "runId" }
20
+ */
21
+ const ANNOUNCE_RUN_ID_RE = /^announce:v\d+:(agent:.+?):([^:]+)$/;
22
+ function parseAnnounceRunId(runId) {
23
+ const m = runId.match(ANNOUNCE_RUN_ID_RE);
24
+ if (!m)
25
+ return null;
26
+ return { childSessionKey: m[1] ?? "", bareRunId: m[2] ?? "" };
27
+ }
28
+ /** Look up subagent entry by any runId form (announce compound or bare). */
29
+ export function lookupByRunId(runId) {
30
+ const direct = byRunId.get(runId);
31
+ if (direct)
32
+ return direct;
33
+ const parsed = parseAnnounceRunId(runId);
34
+ if (parsed) {
35
+ const byChild = byChildSessionKey.get(parsed.childSessionKey);
36
+ if (byChild) {
37
+ // Also register the compound runId for future fast lookup
38
+ byRunId.set(runId, byChild);
39
+ return byChild;
40
+ }
41
+ return byRunId.get(parsed.bareRunId);
42
+ }
43
+ return undefined;
44
+ }
45
+ /** Look up subagent entry by childSessionKey. */
46
+ export function lookupByChildSessionKey(key) {
47
+ return byChildSessionKey.get(key);
48
+ }
49
+ /**
50
+ * Called on sessions_spawn item.start (before tool execution).
51
+ * Stores the spawn intent so spawning SSE can be emitted immediately.
52
+ */
53
+ export function registerSpawnIntent(params) {
54
+ let depth = 1;
55
+ if (params.requesterSessionKey) {
56
+ const parent = byChildSessionKey.get(params.requesterSessionKey);
57
+ if (parent)
58
+ depth = parent.depth + 1;
59
+ }
60
+ const intent = {
61
+ label: params.label,
62
+ parentRunId: params.parentRunId,
63
+ deviceId: params.deviceId.toUpperCase(),
64
+ depth,
65
+ requesterSessionKey: params.requesterSessionKey,
66
+ };
67
+ byToolCallId.set(params.toolCallId, intent);
68
+ return intent;
69
+ }
70
+ /** Consume a previously registered spawn intent. Returns undefined if none. */
71
+ export function consumeSpawnIntent(toolCallId) {
72
+ const intent = byToolCallId.get(toolCallId);
73
+ byToolCallId.delete(toolCallId);
74
+ return intent;
75
+ }
76
+ /**
77
+ * Called when forwardAgentEventRaw sees a sessions_spawn tool result.
78
+ * Extracts childSessionKey, runId, taskName and registers the subagent.
79
+ */
80
+ export function ensureSubagentFromSpawnTool(params) {
81
+ const existing = byChildSessionKey.get(params.childSessionKey);
82
+ if (existing) {
83
+ if (params.bareRunId && !existing.runId) {
84
+ existing.runId = params.bareRunId;
85
+ byRunId.set(params.bareRunId, existing);
86
+ sessionKeyToRunId.set(params.childSessionKey, params.bareRunId);
87
+ }
88
+ existing.status = "running";
89
+ return existing;
90
+ }
91
+ let depth = params.depth ?? 1;
92
+ if (depth <= 1 && params.requesterSessionKey) {
93
+ const parent = byChildSessionKey.get(params.requesterSessionKey);
94
+ if (parent)
95
+ depth = parent.depth + 1;
96
+ }
97
+ const entry = {
98
+ childSessionKey: params.childSessionKey,
99
+ runId: params.bareRunId,
100
+ parentRunId: params.parentRunId,
101
+ deviceId: params.deviceId.toUpperCase(),
102
+ label: params.label || undefined,
103
+ depth,
104
+ status: "running",
105
+ };
106
+ byChildSessionKey.set(params.childSessionKey, entry);
107
+ if (params.bareRunId) {
108
+ byRunId.set(params.bareRunId, entry);
109
+ sessionKeyToRunId.set(params.childSessionKey, params.bareRunId);
110
+ }
111
+ return entry;
112
+ }
113
+ /** Mark subagent as ended. Resolves by bare/compound runId or childSessionKey. */
114
+ export function registerEnded(params) {
115
+ let entry;
116
+ if (params.runId)
117
+ entry = lookupByRunId(params.runId);
118
+ if (!entry && params.childSessionKey)
119
+ entry = byChildSessionKey.get(params.childSessionKey);
120
+ if (!entry)
121
+ return undefined;
122
+ entry.status = "ended";
123
+ if (params.outcome)
124
+ entry.outcome = params.outcome;
125
+ if (params.error)
126
+ entry.error = params.error;
127
+ return entry;
128
+ }
129
+ /** Extract subagent metadata for SSE annotation. */
130
+ export function subagentMeta(entry) {
131
+ return {
132
+ label: entry.label,
133
+ parentRunId: entry.parentRunId,
134
+ depth: entry.depth,
135
+ };
136
+ }
137
+ export function resetForTest() {
138
+ byChildSessionKey.clear();
139
+ byRunId.clear();
140
+ byToolCallId.clear();
141
+ sessionKeyToRunId.clear();
142
+ }
@@ -0,0 +1,17 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
2
+ export type FridayAgentForwardRuntime = {
3
+ resolveStorePath: (store?: string, opts?: {
4
+ agentId?: string;
5
+ }) => string;
6
+ loadSessionStore: (path: string, options?: {
7
+ skipCache?: boolean;
8
+ maintenanceConfig?: unknown;
9
+ clone?: boolean;
10
+ }) => Record<string, unknown>;
11
+ getConfig: () => unknown;
12
+ };
13
+ /** Called from `registerFull` so terminal lifecycle forwards can read `sessions.json` after persist. */
14
+ export declare function setFridayAgentForwardRuntime(api: OpenClawPluginApi): void;
15
+ export declare function getFridayAgentForwardRuntime(): FridayAgentForwardRuntime | null;
16
+ /** Vitest-only */
17
+ export declare function resetFridayAgentForwardRuntimeForTest(): void;
@@ -0,0 +1,16 @@
1
+ let forwardRuntime = null;
2
+ /** Called from `registerFull` so terminal lifecycle forwards can read `sessions.json` after persist. */
3
+ export function setFridayAgentForwardRuntime(api) {
4
+ forwardRuntime = {
5
+ resolveStorePath: api.runtime.agent.session.resolveStorePath,
6
+ loadSessionStore: api.runtime.agent.session.loadSessionStore,
7
+ getConfig: () => api.runtime.config.current(),
8
+ };
9
+ }
10
+ export function getFridayAgentForwardRuntime() {
11
+ return forwardRuntime;
12
+ }
13
+ /** Vitest-only */
14
+ export function resetFridayAgentForwardRuntimeForTest() {
15
+ forwardRuntime = null;
16
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Read OpenClaw agent run context (sessionKey, …) from the same global singleton
3
+ * as `src/infra/agent-events.ts` (`Symbol.for("openclaw.agentEvents.state")`).
4
+ *
5
+ * When a run is hidden from Control UI, `emitAgentEvent` strips `sessionKey` from
6
+ * the listener payload, but `runContextById` still holds it — we need that for
7
+ * Friday SSE and tool hooks without importing the `openclaw` package from this folder.
8
+ */
9
+ export type OpenClawAgentRunContextBridge = {
10
+ sessionKey?: string;
11
+ isControlUiVisible?: boolean;
12
+ };
13
+ export declare function getOpenClawAgentRunContext(runId: string): OpenClawAgentRunContextBridge | undefined;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Read OpenClaw agent run context (sessionKey, …) from the same global singleton
3
+ * as `src/infra/agent-events.ts` (`Symbol.for("openclaw.agentEvents.state")`).
4
+ *
5
+ * When a run is hidden from Control UI, `emitAgentEvent` strips `sessionKey` from
6
+ * the listener payload, but `runContextById` still holds it — we need that for
7
+ * Friday SSE and tool hooks without importing the `openclaw` package from this folder.
8
+ */
9
+ const AGENT_EVENT_STATE_KEY = Symbol.for("openclaw.agentEvents.state");
10
+ function getAgentEventState() {
11
+ const raw = globalThis[AGENT_EVENT_STATE_KEY];
12
+ if (!raw || typeof raw !== "object")
13
+ return undefined;
14
+ const runContextById = raw.runContextById;
15
+ if (!(runContextById instanceof Map))
16
+ return undefined;
17
+ return { runContextById };
18
+ }
19
+ export function getOpenClawAgentRunContext(runId) {
20
+ if (!runId)
21
+ return undefined;
22
+ return getAgentEventState()?.runContextById.get(runId);
23
+ }
@@ -0,0 +1,13 @@
1
+ type MessageActionCtx = {
2
+ action: string;
3
+ params: Record<string, unknown>;
4
+ mediaReadFile?: (filePath: string) => Promise<Buffer>;
5
+ sessionKey?: string | null;
6
+ requesterSenderId?: string | null;
7
+ };
8
+ export declare function describeMessageActions(): {
9
+ actions: readonly ["send", "channel-info", "channel-list"];
10
+ capabilities: readonly ["text", "media"];
11
+ };
12
+ export declare function handleMessageAction(ctx: MessageActionCtx): Promise<unknown>;
13
+ export {};