@syengup/friday-channel-next 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 SyengUp
3
+ Copyright (c) 2026 SyengUp
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syengup/friday-channel-next",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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({