@syengup/friday-channel-next 0.1.3 → 0.1.5

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.
@@ -420,38 +420,43 @@ export function forwardAgentEventRaw(evt) {
420
420
  }, ended.deviceId);
421
421
  }
422
422
  }
423
- // Build sessionUsage: llm_output hook (primary, no race) → store read (fallback).
424
- if (isTerminalLifecycle) {
425
- const llmUsage = consumeRunUsage(evt.runId);
426
- const memUsage = buildSessionUsageFromRunMetadata(evt.runId);
427
- const hasRealTokens = llmUsage?.tokens && Object.keys(llmUsage.tokens).length > 1;
428
- if (hasRealTokens) {
429
- const usage = mergeUsage(llmUsage, memUsage);
423
+ // Build sessionUsage: store (cumulative session totals) → llm_output (per-run fallback).
424
+ if (isTerminalLifecycle && getFridayAgentForwardRuntime()) {
425
+ // Defer to let store write complete, then read cumulative totals.
426
+ // llm_output data is per-run; store is cumulative across rounds.
427
+ setTimeout(() => {
428
+ let data = outgoingData;
429
+ const storeUsage = tryReadSessionUsageFromStore(sk);
430
+ const llmUsage = consumeRunUsage(evt.runId);
431
+ const memUsage = buildSessionUsageFromRunMetadata(evt.runId);
432
+ let usage;
433
+ if (storeUsage) {
434
+ // Store provides cumulative session totals. Supplement with
435
+ // fresher model/provider from llm_output when available.
436
+ usage = storeUsage;
437
+ if (llmUsage?.modelId)
438
+ usage.modelId = llmUsage.modelId;
439
+ if (llmUsage?.modelProvider)
440
+ usage.modelProvider = llmUsage.modelProvider;
441
+ }
442
+ else {
443
+ // First message in session — store not yet written, fall back
444
+ // to per-run llm_output + RunMetadata.
445
+ usage = mergeUsage(llmUsage, memUsage);
446
+ }
430
447
  if (usage) {
431
- outgoingData = { ...outgoingData, sessionUsage: usage };
448
+ data = { ...outgoingData, sessionUsage: usage };
432
449
  }
433
- }
434
- else if (getFridayAgentForwardRuntime()) {
435
- // llm_output hook fires async ~20ms after lifecycle.end.
436
- // Wait 100ms then re-check before falling back to store read.
437
- setTimeout(() => {
438
- let data = outgoingData;
439
- const retryLlm = consumeRunUsage(evt.runId);
440
- const usage = mergeUsage(retryLlm, memUsage) ?? tryReadSessionUsageFromStore(sk);
441
- if (usage) {
442
- data = { ...outgoingData, sessionUsage: usage };
443
- }
444
- completeAgentEventForward({
445
- evt,
446
- sk,
447
- deviceIdRaw,
448
- outgoingData: data,
449
- isTerminalLifecycle: true,
450
- subagentMeta,
451
- });
452
- }, 100);
453
- return;
454
- }
450
+ completeAgentEventForward({
451
+ evt,
452
+ sk,
453
+ deviceIdRaw,
454
+ outgoingData: data,
455
+ isTerminalLifecycle: true,
456
+ subagentMeta,
457
+ });
458
+ }, 100);
459
+ return;
455
460
  }
456
461
  completeAgentEventForward({
457
462
  evt,
@@ -45,6 +45,20 @@ export async function handleDeviceApprove(req, res) {
45
45
  }
46
46
  const match = pairing.pending.find((entry) => entry.deviceId.trim().toUpperCase() === normalizedDeviceId);
47
47
  if (!match) {
48
+ // Gateway may have already auto-approved the device (e.g. mode="local").
49
+ // Check the paired list before returning 404.
50
+ const pairedDevice = (pairing.paired ?? []).find((entry) => entry.deviceId.trim().toUpperCase() === normalizedDeviceId);
51
+ if (pairedDevice) {
52
+ res.statusCode = 200;
53
+ res.setHeader("Content-Type", "application/json");
54
+ res.end(JSON.stringify({
55
+ ok: true,
56
+ deviceId: normalizedDeviceId,
57
+ alreadyApproved: true,
58
+ approvedAtMs: pairedDevice.approvedAtMs,
59
+ }));
60
+ return true;
61
+ }
48
62
  res.statusCode = 404;
49
63
  res.setHeader("Content-Type", "application/json");
50
64
  res.end(JSON.stringify({
@@ -7,5 +7,5 @@ export function applyCorsHeaders(res) {
7
7
  return;
8
8
  res.setHeader("Access-Control-Allow-Origin", cfg.corsAllowOrigin || "*");
9
9
  res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Last-Event-ID");
10
- res.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,OPTIONS");
10
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
11
11
  }
@@ -11,7 +11,6 @@ import { handleFilesDownload } from "./handlers/files-download.js";
11
11
  import { handleCancel } from "./handlers/cancel.js";
12
12
  import { handleDeviceApprove } from "./handlers/device-approve.js";
13
13
  import { handleNodesApprove } from "./handlers/nodes-approve.js";
14
- import { handleSessionsDelete } from "./handlers/sessions-delete.js";
15
14
  import { handleSessionsSettings } from "./handlers/sessions-settings.js";
16
15
  import { handleModelsList } from "./handlers/models-list.js";
17
16
  import { handleStatus } from "./handlers/status.js";
@@ -56,9 +55,6 @@ async function handleFridayNextRoute(req, res) {
56
55
  if (req.method === "POST" && pathname === "/friday-next/nodes-approve") {
57
56
  return await handleNodesApprove(req, res);
58
57
  }
59
- if (req.method === "DELETE" && pathname === "/friday-next/sessions") {
60
- return await handleSessionsDelete(req, res);
61
- }
62
58
  if ((req.method === "PUT" || req.method === "GET") && pathname === "/friday-next/sessions/settings") {
63
59
  return await handleSessionsSettings(req, res);
64
60
  }
@@ -4,13 +4,6 @@ export declare function splitModelRef(modelRef: string): {
4
4
  };
5
5
  export declare function toSessionStoreKey(rawSessionKey: string): string;
6
6
  export declare function ensureSessionLevels(sessionKey: string, reasoningLevel: string, thinkingLevel: string, historyDir?: string): void;
7
- export declare function resolveSessionsDir(historyDir?: string): string;
8
- export interface DeleteSessionResult {
9
- sessionKey: string;
10
- sessionId?: string;
11
- transcriptDeleted?: boolean;
12
- }
13
- export declare function deleteFridaySession(sessionKey: string, historyDir?: string): DeleteSessionResult;
14
7
  export interface FridaySessionSettings {
15
8
  reasoningLevel?: string;
16
9
  thinkingLevel?: string;
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import os from "node:os";
3
- import { readFileSync, writeFileSync, unlinkSync } from "node:fs";
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
4
  const FRIDAY_AGENT_ID = "main";
5
5
  const SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
6
6
  function deriveOpenClawBaseDir(historyDir) {
@@ -96,43 +96,6 @@ function upsertSessionEntry(data, fileKey, sessionKey) {
96
96
  export function ensureSessionLevels(sessionKey, reasoningLevel, thinkingLevel, historyDir) {
97
97
  setSessionSettings(sessionKey, { reasoningLevel, thinkingLevel }, historyDir);
98
98
  }
99
- export function resolveSessionsDir(historyDir) {
100
- const base = deriveOpenClawBaseDir(historyDir);
101
- return join(base, "agents/main/sessions");
102
- }
103
- export function deleteFridaySession(sessionKey, historyDir) {
104
- const result = { sessionKey };
105
- const sessionsFile = resolveSessionsFilePath(historyDir);
106
- const data = readSessionsData(sessionsFile);
107
- if (!data)
108
- return result;
109
- const fileKey = toSessionStoreKey(sessionKey);
110
- const entry = data[fileKey];
111
- if (!entry)
112
- return result;
113
- const sessionId = typeof entry["sessionId"] === "string" ? entry["sessionId"] : undefined;
114
- const sessionFilePath = typeof entry["sessionFile"] === "string" ? entry["sessionFile"] : undefined;
115
- result.sessionId = sessionId;
116
- if (sessionFilePath) {
117
- try {
118
- unlinkSync(sessionFilePath);
119
- result.transcriptDeleted = true;
120
- }
121
- catch { /* gone already */ }
122
- }
123
- if (sessionId) {
124
- const dir = resolveSessionsDir(historyDir);
125
- for (const suffix of [".trajectory.jsonl", ".trajectory-path.json"]) {
126
- try {
127
- unlinkSync(join(dir, `${sessionId}${suffix}`));
128
- }
129
- catch { /* optional */ }
130
- }
131
- }
132
- delete data[fileKey];
133
- writeSessionsData(sessionsFile, data);
134
- return result;
135
- }
136
99
  export function setSessionSettings(sessionKey, settings, historyDir) {
137
100
  try {
138
101
  const sessionsFile = resolveSessionsFilePath(historyDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syengup/friday-channel-next",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "OpenClaw Friday Next Apple channel plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -200,8 +200,20 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
200
200
  expect("reasoningPrefixChars" in (payload.data as object)).toBe(false);
201
201
  });
202
202
 
203
- it("builds sessionUsage from llm_output accumulated usage on lifecycle end", () => {
204
- // Simulate llm_output hook accumulating usage across API calls.
203
+ it("builds sessionUsage from store (cumulative) with llm_output fallback", async () => {
204
+ // No store entry falls back to llm_output per-run data.
205
+ setFridayAgentForwardRuntime({
206
+ runtime: {
207
+ config: { current: () => ({ session: {} }) },
208
+ agent: {
209
+ session: {
210
+ resolveStorePath: () => "/tmp/sessions.json",
211
+ loadSessionStore: vi.fn(() => ({})),
212
+ },
213
+ },
214
+ },
215
+ } as never);
216
+
205
217
  accumulateRunUsage(runId, { input: 100, output: 50, cacheRead: 10, total: 150 }, "my-model", "openai");
206
218
  accumulateRunUsage(runId, { input: 30, output: 10, cacheRead: 0, total: 40 }, "my-model", "openai");
207
219
 
@@ -213,15 +225,18 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
213
225
  data: { phase: "end" },
214
226
  });
215
227
 
216
- // Lifecycle events are synchronous now (no file I/O wait).
228
+ // Deferred 100ms not broadcast yet.
229
+ expect(sseEmitter.broadcastToRun).not.toHaveBeenCalled();
230
+ await new Promise<void>((resolve) => setTimeout(resolve, 150));
231
+
217
232
  expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(1);
218
233
  const forwarded = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[0][1].data;
219
234
  expect(forwarded.stream).toBe("lifecycle");
220
235
  const sessionUsage = (forwarded.data as Record<string, unknown>).sessionUsage as Record<string, unknown>;
221
236
  expect(sessionUsage).toBeDefined();
237
+ // llm_output fallback — per-run totals.
222
238
  expect(sessionUsage.modelId).toBe("my-model");
223
239
  expect(sessionUsage.modelProvider).toBe("openai");
224
- // Accumulated totals across both API calls.
225
240
  expect((sessionUsage.tokens as Record<string, unknown>).input).toBe(130);
226
241
  expect((sessionUsage.tokens as Record<string, unknown>).output).toBe(60);
227
242
  expect((sessionUsage.tokens as Record<string, unknown>).cacheRead).toBe(10);
@@ -229,50 +244,63 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
229
244
  expect((sessionUsage.tokens as Record<string, unknown>).totalFresh).toBe(true);
230
245
  });
231
246
 
232
- it("merges llm_output usage with RunMetadata for sessionUsage on lifecycle end", () => {
233
- // Simulate llm_output hook.
234
- accumulateRunUsage(runId, { input: 500, output: 100, cacheRead: 200, cacheWrite: 0, total: 800 }, "llm-model", "llm-provider");
235
-
236
- // Send an agent event that populates RunMetadata (model, context window).
237
- forwardAgentEventRaw({
238
- runId,
239
- seq: 1,
240
- stream: "assistant",
241
- sessionKey,
242
- data: {
243
- model: "agent-model",
244
- provider: "agent-provider",
245
- usage: { input: 999, total: 999 },
246
- contextWindow: 100000,
247
+ it("prefers store cumulative totals over llm_output per-run data", async () => {
248
+ const storeKey = toSessionStoreKey(sessionKey);
249
+ setFridayAgentForwardRuntime({
250
+ runtime: {
251
+ config: { current: () => ({ session: {} }) },
252
+ agent: {
253
+ session: {
254
+ resolveStorePath: () => "/tmp/sessions.json",
255
+ loadSessionStore: vi.fn(() => ({
256
+ [storeKey]: {
257
+ model: "store-model",
258
+ modelProvider: "old-provider",
259
+ inputTokens: 5000,
260
+ outputTokens: 2000,
261
+ totalTokens: 99999,
262
+ totalTokensFresh: true,
263
+ contextTokens: 128000,
264
+ estimatedCostUsd: 0.05,
265
+ cacheRead: 100,
266
+ cacheWrite: 50,
267
+ },
268
+ })),
269
+ },
270
+ },
247
271
  },
248
- });
272
+ } as never);
273
+
274
+ // llm_output has fresher model/provider but per-run (smaller) tokens.
275
+ accumulateRunUsage(runId, { input: 500, output: 100, cacheRead: 200, total: 800 }, "llm-model", "llm-provider");
249
276
 
250
277
  forwardAgentEventRaw({
251
278
  runId,
252
- seq: 2,
279
+ seq: 1,
253
280
  stream: "lifecycle",
254
281
  sessionKey,
255
282
  data: { phase: "end" },
256
283
  });
257
284
 
258
- // Assistant (1st) + lifecycle.end (2nd, synchronous).
259
- expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(2);
260
- const lifecycleCall = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[1];
261
- const lifecycleData = (lifecycleCall[1] as { data: { data: Record<string, unknown> } }).data.data;
262
- const sessionUsage = lifecycleData.sessionUsage as Record<string, unknown> | undefined;
285
+ expect(sseEmitter.broadcastToRun).not.toHaveBeenCalled();
286
+ await new Promise<void>((resolve) => setTimeout(resolve, 150));
287
+
288
+ expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(1);
289
+ const forwarded = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[0][1].data;
290
+ const sessionUsage = (forwarded.data as Record<string, unknown>).sessionUsage as Record<string, unknown>;
263
291
  expect(sessionUsage).toBeDefined();
264
- // llm_output tokens win (authoritative per-API-call data).
265
- expect(sessionUsage!.modelId).toBe("llm-model");
266
- expect(sessionUsage!.modelProvider).toBe("llm-provider");
267
- expect((sessionUsage!.tokens as Record<string, unknown>).input).toBe(500);
268
- expect((sessionUsage!.tokens as Record<string, unknown>).output).toBe(100);
269
- expect((sessionUsage!.tokens as Record<string, unknown>).cacheRead).toBe(200);
270
- expect((sessionUsage!.tokens as Record<string, unknown>).total).toBe(800);
271
- // Context from RunMetadata (not available from llm_output).
272
- expect((sessionUsage!.context as Record<string, unknown>).windowMax).toBe(100000);
292
+ // Store cumulative totals win.
293
+ expect((sessionUsage.tokens as Record<string, unknown>).input).toBe(5000);
294
+ expect((sessionUsage.tokens as Record<string, unknown>).output).toBe(2000);
295
+ expect((sessionUsage.tokens as Record<string, unknown>).total).toBe(99999);
296
+ // Model/provider from llm_output (fresher) override store.
297
+ expect(sessionUsage.modelId).toBe("llm-model");
298
+ expect(sessionUsage.modelProvider).toBe("llm-provider");
299
+ expect(sessionUsage.estimatedCostUsd).toBe(0.05);
300
+ expect((sessionUsage.context as Record<string, unknown>).windowMax).toBe(128000);
273
301
  });
274
302
 
275
- it("falls back to store read when llm_output has no data (deferred)", async () => {
303
+ it("uses store cumulative totals when llm_output has no data", async () => {
276
304
  const storeKey = toSessionStoreKey(sessionKey);
277
305
  const store: Record<string, Record<string, unknown>> = {
278
306
  [storeKey]: {
@@ -483,38 +483,40 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
483
483
  }
484
484
  }
485
485
 
486
- // Build sessionUsage: llm_output hook (primary, no race) → store read (fallback).
487
- if (isTerminalLifecycle) {
488
- const llmUsage = consumeRunUsage(evt.runId);
489
- const memUsage = buildSessionUsageFromRunMetadata(evt.runId);
490
- const hasRealTokens = llmUsage?.tokens && Object.keys(llmUsage.tokens).length > 1;
491
-
492
- if (hasRealTokens) {
493
- const usage = mergeUsage(llmUsage, memUsage);
486
+ // Build sessionUsage: store (cumulative session totals) → llm_output (per-run fallback).
487
+ if (isTerminalLifecycle && getFridayAgentForwardRuntime()) {
488
+ // Defer to let store write complete, then read cumulative totals.
489
+ // llm_output data is per-run; store is cumulative across rounds.
490
+ setTimeout(() => {
491
+ let data = outgoingData;
492
+ const storeUsage = tryReadSessionUsageFromStore(sk);
493
+ const llmUsage = consumeRunUsage(evt.runId);
494
+ const memUsage = buildSessionUsageFromRunMetadata(evt.runId);
495
+ let usage: FridaySessionUsagePayload | undefined;
496
+ if (storeUsage) {
497
+ // Store provides cumulative session totals. Supplement with
498
+ // fresher model/provider from llm_output when available.
499
+ usage = storeUsage;
500
+ if (llmUsage?.modelId) usage.modelId = llmUsage.modelId;
501
+ if (llmUsage?.modelProvider) usage.modelProvider = llmUsage.modelProvider;
502
+ } else {
503
+ // First message in session — store not yet written, fall back
504
+ // to per-run llm_output + RunMetadata.
505
+ usage = mergeUsage(llmUsage, memUsage);
506
+ }
494
507
  if (usage) {
495
- outgoingData = { ...outgoingData, sessionUsage: usage };
508
+ data = { ...outgoingData, sessionUsage: usage };
496
509
  }
497
- } else if (getFridayAgentForwardRuntime()) {
498
- // llm_output hook fires async ~20ms after lifecycle.end.
499
- // Wait 100ms then re-check before falling back to store read.
500
- setTimeout(() => {
501
- let data = outgoingData;
502
- const retryLlm = consumeRunUsage(evt.runId);
503
- const usage = mergeUsage(retryLlm, memUsage) ?? tryReadSessionUsageFromStore(sk);
504
- if (usage) {
505
- data = { ...outgoingData, sessionUsage: usage };
506
- }
507
- completeAgentEventForward({
508
- evt,
509
- sk,
510
- deviceIdRaw,
511
- outgoingData: data,
512
- isTerminalLifecycle: true,
513
- subagentMeta,
514
- });
515
- }, 100);
516
- return;
517
- }
510
+ completeAgentEventForward({
511
+ evt,
512
+ sk,
513
+ deviceIdRaw,
514
+ outgoingData: data,
515
+ isTerminalLifecycle: true,
516
+ subagentMeta,
517
+ });
518
+ }, 100);
519
+ return;
518
520
  }
519
521
 
520
522
  completeAgentEventForward({
@@ -59,6 +59,23 @@ export async function handleDeviceApprove(
59
59
  );
60
60
 
61
61
  if (!match) {
62
+ // Gateway may have already auto-approved the device (e.g. mode="local").
63
+ // Check the paired list before returning 404.
64
+ const pairedDevice = (pairing.paired ?? []).find(
65
+ (entry) => entry.deviceId.trim().toUpperCase() === normalizedDeviceId,
66
+ );
67
+ if (pairedDevice) {
68
+ res.statusCode = 200;
69
+ res.setHeader("Content-Type", "application/json");
70
+ res.end(JSON.stringify({
71
+ ok: true,
72
+ deviceId: normalizedDeviceId,
73
+ alreadyApproved: true,
74
+ approvedAtMs: (pairedDevice as any).approvedAtMs,
75
+ }));
76
+ return true;
77
+ }
78
+
62
79
  res.statusCode = 404;
63
80
  res.setHeader("Content-Type", "application/json");
64
81
  res.end(JSON.stringify({
@@ -8,5 +8,5 @@ export function applyCorsHeaders(res: ServerResponse): void {
8
8
  if (!cfg.corsEnabled) return;
9
9
  res.setHeader("Access-Control-Allow-Origin", cfg.corsAllowOrigin || "*");
10
10
  res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Last-Event-ID");
11
- res.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,OPTIONS");
11
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
12
12
  }
@@ -13,7 +13,6 @@ import { handleFilesDownload } from "./handlers/files-download.js";
13
13
  import { handleCancel } from "./handlers/cancel.js";
14
14
  import { handleDeviceApprove } from "./handlers/device-approve.js";
15
15
  import { handleNodesApprove } from "./handlers/nodes-approve.js";
16
- import { handleSessionsDelete } from "./handlers/sessions-delete.js";
17
16
  import { handleSessionsSettings } from "./handlers/sessions-settings.js";
18
17
  import { handleModelsList } from "./handlers/models-list.js";
19
18
  import { handleStatus } from "./handlers/status.js";
@@ -70,10 +69,6 @@ async function handleFridayNextRoute(
70
69
  return await handleNodesApprove(req, res);
71
70
  }
72
71
 
73
- if (req.method === "DELETE" && pathname === "/friday-next/sessions") {
74
- return await handleSessionsDelete(req, res);
75
- }
76
-
77
72
  if ((req.method === "PUT" || req.method === "GET") && pathname === "/friday-next/sessions/settings") {
78
73
  return await handleSessionsSettings(req, res);
79
74
  }
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import os from "node:os";
3
- import { readFileSync, writeFileSync, unlinkSync } from "node:fs";
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
4
 
5
5
  const FRIDAY_AGENT_ID = "main";
6
6
  const SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
@@ -111,50 +111,6 @@ export function ensureSessionLevels(
111
111
  setSessionSettings(sessionKey, { reasoningLevel, thinkingLevel }, historyDir);
112
112
  }
113
113
 
114
- export function resolveSessionsDir(historyDir?: string): string {
115
- const base = deriveOpenClawBaseDir(historyDir);
116
- return join(base, "agents/main/sessions");
117
- }
118
-
119
- export interface DeleteSessionResult {
120
- sessionKey: string;
121
- sessionId?: string;
122
- transcriptDeleted?: boolean;
123
- }
124
-
125
- export function deleteFridaySession(
126
- sessionKey: string,
127
- historyDir?: string,
128
- ): DeleteSessionResult {
129
- const result: DeleteSessionResult = { sessionKey };
130
- const sessionsFile = resolveSessionsFilePath(historyDir);
131
- const data = readSessionsData(sessionsFile);
132
- if (!data) return result;
133
-
134
- const fileKey = toSessionStoreKey(sessionKey);
135
- const entry = data[fileKey];
136
- if (!entry) return result;
137
-
138
- const sessionId = typeof entry["sessionId"] === "string" ? entry["sessionId"] : undefined;
139
- const sessionFilePath = typeof entry["sessionFile"] === "string" ? entry["sessionFile"] : undefined;
140
- result.sessionId = sessionId;
141
-
142
- if (sessionFilePath) {
143
- try { unlinkSync(sessionFilePath); result.transcriptDeleted = true; } catch { /* gone already */ }
144
- }
145
-
146
- if (sessionId) {
147
- const dir = resolveSessionsDir(historyDir);
148
- for (const suffix of [".trajectory.jsonl", ".trajectory-path.json"]) {
149
- try { unlinkSync(join(dir, `${sessionId}${suffix}`)); } catch { /* optional */ }
150
- }
151
- }
152
-
153
- delete data[fileKey];
154
- writeSessionsData(sessionsFile, data);
155
- return result;
156
- }
157
-
158
114
  export interface FridaySessionSettings {
159
115
  reasoningLevel?: string;
160
116
  thinkingLevel?: string;
@@ -1,59 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- import { deleteFridaySession, toSessionStoreKey } from "../../session/session-manager.js";
3
- import { getActiveRunIds } from "../../agent/active-runs.js";
4
- import { abortRun } from "../../agent/abort-run.js";
5
- import { getRunRoute } from "../../run-metadata.js";
6
- import { sseEmitter } from "../../sse/emitter.js";
7
- import { readJsonBody } from "../middleware/body.js";
8
- import { extractBearerToken } from "../middleware/auth.js";
9
-
10
- async function cancelActiveRunsForSession(sessionKey: string): Promise<string[]> {
11
- const storeKey = toSessionStoreKey(sessionKey);
12
- const cancelled: string[] = [];
13
- for (const runId of getActiveRunIds()) {
14
- const route = getRunRoute(runId);
15
- if (route?.sessionKey === storeKey) {
16
- await abortRun(runId);
17
- sseEmitter.untrackRun(runId);
18
- cancelled.push(runId);
19
- }
20
- }
21
- return cancelled;
22
- }
23
-
24
- export async function handleSessionsDelete(
25
- req: IncomingMessage,
26
- res: ServerResponse,
27
- ): Promise<boolean> {
28
- if (req.method !== "DELETE") {
29
- res.statusCode = 405;
30
- res.setHeader("Content-Type", "application/json");
31
- res.end(JSON.stringify({ error: "Method Not Allowed" }));
32
- return true;
33
- }
34
-
35
- const token = extractBearerToken(req);
36
- if (!token) {
37
- res.statusCode = 401;
38
- res.setHeader("Content-Type", "application/json");
39
- res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
40
- return true;
41
- }
42
-
43
- const body = await readJsonBody(req);
44
- const sessionKey = typeof body?.sessionKey === "string" ? body.sessionKey.trim() : "";
45
- if (!sessionKey) {
46
- res.statusCode = 400;
47
- res.setHeader("Content-Type", "application/json");
48
- res.end(JSON.stringify({ error: "Missing required field: sessionKey" }));
49
- return true;
50
- }
51
-
52
- const cancelledRuns = await cancelActiveRunsForSession(sessionKey);
53
- const result = deleteFridaySession(sessionKey);
54
-
55
- res.statusCode = 200;
56
- res.setHeader("Content-Type", "application/json");
57
- res.end(JSON.stringify({ ok: true, ...result, cancelledRuns }));
58
- return true;
59
- }