fix: persist session usage metadata on suppressed replies
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import type { TemplateContext } from "../templating.js";
|
import type { TemplateContext } from "../templating.js";
|
||||||
|
import { loadSessionStore, saveSessionStore, type SessionEntry } from "../../config/sessions.js";
|
||||||
import type { FollowupRun, QueueSettings } from "./queue.js";
|
import type { FollowupRun, QueueSettings } from "./queue.js";
|
||||||
import { createMockTypingController } from "./test-helpers.js";
|
import { createMockTypingController } from "./test-helpers.js";
|
||||||
|
|
||||||
@@ -38,8 +42,12 @@ vi.mock("./queue.js", async () => {
|
|||||||
|
|
||||||
import { runReplyAgent } from "./agent-runner.js";
|
import { runReplyAgent } from "./agent-runner.js";
|
||||||
|
|
||||||
function createRun(messageProvider = "slack") {
|
function createRun(
|
||||||
|
messageProvider = "slack",
|
||||||
|
opts: { storePath?: string; sessionKey?: string } = {},
|
||||||
|
) {
|
||||||
const typing = createMockTypingController();
|
const typing = createMockTypingController();
|
||||||
|
const sessionKey = opts.sessionKey ?? "main";
|
||||||
const sessionCtx = {
|
const sessionCtx = {
|
||||||
Provider: messageProvider,
|
Provider: messageProvider,
|
||||||
OriginatingTo: "channel:C1",
|
OriginatingTo: "channel:C1",
|
||||||
@@ -53,7 +61,7 @@ function createRun(messageProvider = "slack") {
|
|||||||
enqueuedAt: Date.now(),
|
enqueuedAt: Date.now(),
|
||||||
run: {
|
run: {
|
||||||
sessionId: "session",
|
sessionId: "session",
|
||||||
sessionKey: "main",
|
sessionKey,
|
||||||
messageProvider,
|
messageProvider,
|
||||||
sessionFile: "/tmp/session.jsonl",
|
sessionFile: "/tmp/session.jsonl",
|
||||||
workspaceDir: "/tmp",
|
workspaceDir: "/tmp",
|
||||||
@@ -85,6 +93,8 @@ function createRun(messageProvider = "slack") {
|
|||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
typing,
|
typing,
|
||||||
sessionCtx,
|
sessionCtx,
|
||||||
|
sessionKey,
|
||||||
|
storePath: opts.storePath,
|
||||||
defaultModel: "anthropic/claude-opus-4-5",
|
defaultModel: "anthropic/claude-opus-4-5",
|
||||||
resolvedVerboseLevel: "off",
|
resolvedVerboseLevel: "off",
|
||||||
isNewSession: false,
|
isNewSession: false,
|
||||||
@@ -141,4 +151,34 @@ describe("runReplyAgent messaging tool suppression", () => {
|
|||||||
|
|
||||||
expect(result).toMatchObject({ text: "hello world!" });
|
expect(result).toMatchObject({ text: "hello world!" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("persists usage even when replies are suppressed", async () => {
|
||||||
|
const storePath = path.join(
|
||||||
|
await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-session-store-")),
|
||||||
|
"sessions.json",
|
||||||
|
);
|
||||||
|
const sessionKey = "main";
|
||||||
|
const entry: SessionEntry = { sessionId: "session", updatedAt: Date.now() };
|
||||||
|
await saveSessionStore(storePath, { [sessionKey]: entry });
|
||||||
|
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||||
|
payloads: [{ text: "hello world!" }],
|
||||||
|
messagingToolSentTexts: ["different message"],
|
||||||
|
messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }],
|
||||||
|
meta: {
|
||||||
|
agentMeta: {
|
||||||
|
usage: { input: 10, output: 5 },
|
||||||
|
model: "claude-opus-4-5",
|
||||||
|
provider: "anthropic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await createRun("slack", { storePath, sessionKey });
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
const store = loadSessionStore(storePath, { skipCache: true });
|
||||||
|
expect(store[sessionKey]?.totalTokens ?? 0).toBeGreaterThan(0);
|
||||||
|
expect(store[sessionKey]?.model).toBe("claude-opus-4-5");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { setCliSessionId } from "../../agents/cli-session.js";
|
|
||||||
import { lookupContextTokens } from "../../agents/context.js";
|
import { lookupContextTokens } from "../../agents/context.js";
|
||||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||||
import { resolveModelAuthMode } from "../../agents/model-auth.js";
|
import { resolveModelAuthMode } from "../../agents/model-auth.js";
|
||||||
@@ -38,6 +37,7 @@ import { resolveBlockStreamingCoalescing } from "./block-streaming.js";
|
|||||||
import { createFollowupRunner } from "./followup-runner.js";
|
import { createFollowupRunner } from "./followup-runner.js";
|
||||||
import { enqueueFollowupRun, type FollowupRun, type QueueSettings } from "./queue.js";
|
import { enqueueFollowupRun, type FollowupRun, type QueueSettings } from "./queue.js";
|
||||||
import { createReplyToModeFilterForChannel, resolveReplyToMode } from "./reply-threading.js";
|
import { createReplyToModeFilterForChannel, resolveReplyToMode } from "./reply-threading.js";
|
||||||
|
import { persistSessionUsageUpdate } from "./session-usage.js";
|
||||||
import { incrementCompactionCount } from "./session-updates.js";
|
import { incrementCompactionCount } from "./session-updates.js";
|
||||||
import type { TypingController } from "./typing.js";
|
import type { TypingController } from "./typing.js";
|
||||||
import { createTypingSignaler } from "./typing-mode.js";
|
import { createTypingSignaler } from "./typing-mode.js";
|
||||||
@@ -365,6 +365,30 @@ export async function runReplyAgent(params: {
|
|||||||
await Promise.allSettled(pendingToolTasks);
|
await Promise.allSettled(pendingToolTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usage = runResult.meta.agentMeta?.usage;
|
||||||
|
const modelUsed = runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||||
|
const providerUsed =
|
||||||
|
runResult.meta.agentMeta?.provider ?? fallbackProvider ?? followupRun.run.provider;
|
||||||
|
const cliSessionId = isCliProvider(providerUsed, cfg)
|
||||||
|
? runResult.meta.agentMeta?.sessionId?.trim()
|
||||||
|
: undefined;
|
||||||
|
const contextTokensUsed =
|
||||||
|
agentCfgContextTokens ??
|
||||||
|
lookupContextTokens(modelUsed) ??
|
||||||
|
activeSessionEntry?.contextTokens ??
|
||||||
|
DEFAULT_CONTEXT_TOKENS;
|
||||||
|
|
||||||
|
await persistSessionUsageUpdate({
|
||||||
|
storePath,
|
||||||
|
sessionKey,
|
||||||
|
usage,
|
||||||
|
modelUsed,
|
||||||
|
providerUsed,
|
||||||
|
contextTokensUsed,
|
||||||
|
systemPromptReport: runResult.meta.systemPromptReport,
|
||||||
|
cliSessionId,
|
||||||
|
});
|
||||||
|
|
||||||
// Drain any late tool/block deliveries before deciding there's "nothing to send".
|
// Drain any late tool/block deliveries before deciding there's "nothing to send".
|
||||||
// Otherwise, a late typing trigger (e.g. from a tool callback) can outlive the run and
|
// Otherwise, a late typing trigger (e.g. from a tool callback) can outlive the run and
|
||||||
// keep the typing indicator stuck.
|
// keep the typing indicator stuck.
|
||||||
@@ -395,19 +419,6 @@ export async function runReplyAgent(params: {
|
|||||||
|
|
||||||
await signalTypingIfNeeded(replyPayloads, typingSignals);
|
await signalTypingIfNeeded(replyPayloads, typingSignals);
|
||||||
|
|
||||||
const usage = runResult.meta.agentMeta?.usage;
|
|
||||||
const modelUsed = runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
|
||||||
const providerUsed =
|
|
||||||
runResult.meta.agentMeta?.provider ?? fallbackProvider ?? followupRun.run.provider;
|
|
||||||
const cliSessionId = isCliProvider(providerUsed, cfg)
|
|
||||||
? runResult.meta.agentMeta?.sessionId?.trim()
|
|
||||||
: undefined;
|
|
||||||
const contextTokensUsed =
|
|
||||||
agentCfgContextTokens ??
|
|
||||||
lookupContextTokens(modelUsed) ??
|
|
||||||
activeSessionEntry?.contextTokens ??
|
|
||||||
DEFAULT_CONTEXT_TOKENS;
|
|
||||||
|
|
||||||
if (isDiagnosticsEnabled(cfg) && hasNonzeroUsage(usage)) {
|
if (isDiagnosticsEnabled(cfg) && hasNonzeroUsage(usage)) {
|
||||||
const input = usage.input ?? 0;
|
const input = usage.input ?? 0;
|
||||||
const output = usage.output ?? 0;
|
const output = usage.output ?? 0;
|
||||||
@@ -445,72 +456,6 @@ export async function runReplyAgent(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storePath && sessionKey) {
|
|
||||||
if (hasNonzeroUsage(usage)) {
|
|
||||||
try {
|
|
||||||
await updateSessionStoreEntry({
|
|
||||||
storePath,
|
|
||||||
sessionKey,
|
|
||||||
update: async (entry) => {
|
|
||||||
const input = usage.input ?? 0;
|
|
||||||
const output = usage.output ?? 0;
|
|
||||||
const promptTokens = input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
|
||||||
const patch: Partial<SessionEntry> = {
|
|
||||||
inputTokens: input,
|
|
||||||
outputTokens: output,
|
|
||||||
totalTokens: promptTokens > 0 ? promptTokens : (usage.total ?? input),
|
|
||||||
modelProvider: providerUsed,
|
|
||||||
model: modelUsed,
|
|
||||||
contextTokens: contextTokensUsed ?? entry.contextTokens,
|
|
||||||
systemPromptReport: runResult.meta.systemPromptReport ?? entry.systemPromptReport,
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
};
|
|
||||||
if (cliSessionId) {
|
|
||||||
const nextEntry = { ...entry, ...patch };
|
|
||||||
setCliSessionId(nextEntry, providerUsed, cliSessionId);
|
|
||||||
return {
|
|
||||||
...patch,
|
|
||||||
cliSessionIds: nextEntry.cliSessionIds,
|
|
||||||
claudeCliSessionId: nextEntry.claudeCliSessionId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return patch;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logVerbose(`failed to persist usage update: ${String(err)}`);
|
|
||||||
}
|
|
||||||
} else if (modelUsed || contextTokensUsed) {
|
|
||||||
try {
|
|
||||||
await updateSessionStoreEntry({
|
|
||||||
storePath,
|
|
||||||
sessionKey,
|
|
||||||
update: async (entry) => {
|
|
||||||
const patch: Partial<SessionEntry> = {
|
|
||||||
modelProvider: providerUsed ?? entry.modelProvider,
|
|
||||||
model: modelUsed ?? entry.model,
|
|
||||||
contextTokens: contextTokensUsed ?? entry.contextTokens,
|
|
||||||
systemPromptReport: runResult.meta.systemPromptReport ?? entry.systemPromptReport,
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
};
|
|
||||||
if (cliSessionId) {
|
|
||||||
const nextEntry = { ...entry, ...patch };
|
|
||||||
setCliSessionId(nextEntry, providerUsed, cliSessionId);
|
|
||||||
return {
|
|
||||||
...patch,
|
|
||||||
cliSessionIds: nextEntry.cliSessionIds,
|
|
||||||
claudeCliSessionId: nextEntry.claudeCliSessionId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return patch;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logVerbose(`failed to persist model/context update: ${String(err)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseUsageRaw =
|
const responseUsageRaw =
|
||||||
activeSessionEntry?.responseUsage ??
|
activeSessionEntry?.responseUsage ??
|
||||||
(sessionKey ? activeSessionStore?.[sessionKey]?.responseUsage : undefined);
|
(sessionKey ? activeSessionStore?.[sessionKey]?.responseUsage : undefined);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import type { SessionEntry } from "../../config/sessions.js";
|
import { loadSessionStore, saveSessionStore, type SessionEntry } from "../../config/sessions.js";
|
||||||
import type { FollowupRun } from "./queue.js";
|
import type { FollowupRun } from "./queue.js";
|
||||||
import { createMockTypingController } from "./test-helpers.js";
|
import { createMockTypingController } from "./test-helpers.js";
|
||||||
|
|
||||||
@@ -195,4 +195,47 @@ describe("createFollowupRunner messaging tool dedupe", () => {
|
|||||||
|
|
||||||
expect(onBlockReply).not.toHaveBeenCalled();
|
expect(onBlockReply).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("persists usage even when replies are suppressed", async () => {
|
||||||
|
const storePath = path.join(
|
||||||
|
await fs.mkdtemp(path.join(tmpdir(), "clawdbot-followup-usage-")),
|
||||||
|
"sessions.json",
|
||||||
|
);
|
||||||
|
const sessionKey = "main";
|
||||||
|
const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() };
|
||||||
|
const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
|
||||||
|
await saveSessionStore(storePath, sessionStore);
|
||||||
|
|
||||||
|
const onBlockReply = vi.fn(async () => {});
|
||||||
|
runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||||
|
payloads: [{ text: "hello world!" }],
|
||||||
|
messagingToolSentTexts: ["different message"],
|
||||||
|
messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }],
|
||||||
|
meta: {
|
||||||
|
agentMeta: {
|
||||||
|
usage: { input: 10, output: 5 },
|
||||||
|
model: "claude-opus-4-5",
|
||||||
|
provider: "anthropic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const runner = createFollowupRunner({
|
||||||
|
opts: { onBlockReply },
|
||||||
|
typing: createMockTypingController(),
|
||||||
|
typingMode: "instant",
|
||||||
|
sessionEntry,
|
||||||
|
sessionStore,
|
||||||
|
sessionKey,
|
||||||
|
storePath,
|
||||||
|
defaultModel: "anthropic/claude-opus-4-5",
|
||||||
|
});
|
||||||
|
|
||||||
|
await runner(baseQueuedRun("slack"));
|
||||||
|
|
||||||
|
expect(onBlockReply).not.toHaveBeenCalled();
|
||||||
|
const store = loadSessionStore(storePath, { skipCache: true });
|
||||||
|
expect(store[sessionKey]?.totalTokens ?? 0).toBeGreaterThan(0);
|
||||||
|
expect(store[sessionKey]?.model).toBe("claude-opus-4-5");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,12 +4,7 @@ import { lookupContextTokens } from "../../agents/context.js";
|
|||||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||||
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
||||||
import { hasNonzeroUsage } from "../../agents/usage.js";
|
import { resolveAgentIdFromSessionKey, type SessionEntry } from "../../config/sessions.js";
|
||||||
import {
|
|
||||||
resolveAgentIdFromSessionKey,
|
|
||||||
type SessionEntry,
|
|
||||||
updateSessionStoreEntry,
|
|
||||||
} from "../../config/sessions.js";
|
|
||||||
import type { TypingMode } from "../../config/types.js";
|
import type { TypingMode } from "../../config/types.js";
|
||||||
import { logVerbose } from "../../globals.js";
|
import { logVerbose } from "../../globals.js";
|
||||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||||
@@ -26,6 +21,7 @@ import {
|
|||||||
} from "./reply-payloads.js";
|
} from "./reply-payloads.js";
|
||||||
import { resolveReplyToMode } from "./reply-threading.js";
|
import { resolveReplyToMode } from "./reply-threading.js";
|
||||||
import { isRoutableChannel, routeReply } from "./route-reply.js";
|
import { isRoutableChannel, routeReply } from "./route-reply.js";
|
||||||
|
import { persistSessionUsageUpdate } from "./session-usage.js";
|
||||||
import { incrementCompactionCount } from "./session-updates.js";
|
import { incrementCompactionCount } from "./session-updates.js";
|
||||||
import type { TypingController } from "./typing.js";
|
import type { TypingController } from "./typing.js";
|
||||||
import { createTypingSignaler } from "./typing-mode.js";
|
import { createTypingSignaler } from "./typing-mode.js";
|
||||||
@@ -190,6 +186,26 @@ export function createFollowupRunner(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (storePath && sessionKey) {
|
||||||
|
const usage = runResult.meta.agentMeta?.usage;
|
||||||
|
const modelUsed = runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||||
|
const contextTokensUsed =
|
||||||
|
agentCfgContextTokens ??
|
||||||
|
lookupContextTokens(modelUsed) ??
|
||||||
|
sessionEntry?.contextTokens ??
|
||||||
|
DEFAULT_CONTEXT_TOKENS;
|
||||||
|
|
||||||
|
await persistSessionUsageUpdate({
|
||||||
|
storePath,
|
||||||
|
sessionKey,
|
||||||
|
usage,
|
||||||
|
modelUsed,
|
||||||
|
providerUsed: fallbackProvider,
|
||||||
|
contextTokensUsed,
|
||||||
|
logLabel: "followup",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const payloadArray = runResult.payloads ?? [];
|
const payloadArray = runResult.payloads ?? [];
|
||||||
if (payloadArray.length === 0) return;
|
if (payloadArray.length === 0) return;
|
||||||
const sanitizedPayloads = payloadArray.flatMap((payload) => {
|
const sanitizedPayloads = payloadArray.flatMap((payload) => {
|
||||||
@@ -245,56 +261,6 @@ export function createFollowupRunner(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storePath && sessionKey) {
|
|
||||||
const usage = runResult.meta.agentMeta?.usage;
|
|
||||||
const modelUsed = runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
|
||||||
const contextTokensUsed =
|
|
||||||
agentCfgContextTokens ??
|
|
||||||
lookupContextTokens(modelUsed) ??
|
|
||||||
sessionEntry?.contextTokens ??
|
|
||||||
DEFAULT_CONTEXT_TOKENS;
|
|
||||||
|
|
||||||
if (hasNonzeroUsage(usage)) {
|
|
||||||
try {
|
|
||||||
await updateSessionStoreEntry({
|
|
||||||
storePath,
|
|
||||||
sessionKey,
|
|
||||||
update: async (entry) => {
|
|
||||||
const input = usage.input ?? 0;
|
|
||||||
const output = usage.output ?? 0;
|
|
||||||
const promptTokens = input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
|
||||||
return {
|
|
||||||
inputTokens: input,
|
|
||||||
outputTokens: output,
|
|
||||||
totalTokens: promptTokens > 0 ? promptTokens : (usage.total ?? input),
|
|
||||||
modelProvider: fallbackProvider ?? entry.modelProvider,
|
|
||||||
model: modelUsed,
|
|
||||||
contextTokens: contextTokensUsed ?? entry.contextTokens,
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logVerbose(`failed to persist followup usage update: ${String(err)}`);
|
|
||||||
}
|
|
||||||
} else if (modelUsed || contextTokensUsed) {
|
|
||||||
try {
|
|
||||||
await updateSessionStoreEntry({
|
|
||||||
storePath,
|
|
||||||
sessionKey,
|
|
||||||
update: async (entry) => ({
|
|
||||||
modelProvider: fallbackProvider ?? entry.modelProvider,
|
|
||||||
model: modelUsed ?? entry.model,
|
|
||||||
contextTokens: contextTokensUsed ?? entry.contextTokens,
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logVerbose(`failed to persist followup model/context update: ${String(err)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendFollowupPayloads(finalPayloads, queued);
|
await sendFollowupPayloads(finalPayloads, queued);
|
||||||
} finally {
|
} finally {
|
||||||
typing.markRunComplete();
|
typing.markRunComplete();
|
||||||
|
|||||||
92
src/auto-reply/reply/session-usage.ts
Normal file
92
src/auto-reply/reply/session-usage.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { setCliSessionId } from "../../agents/cli-session.js";
|
||||||
|
import { hasNonzeroUsage, type NormalizedUsage } from "../../agents/usage.js";
|
||||||
|
import {
|
||||||
|
type SessionSystemPromptReport,
|
||||||
|
type SessionEntry,
|
||||||
|
updateSessionStoreEntry,
|
||||||
|
} from "../../config/sessions.js";
|
||||||
|
import { logVerbose } from "../../globals.js";
|
||||||
|
|
||||||
|
export async function persistSessionUsageUpdate(params: {
|
||||||
|
storePath?: string;
|
||||||
|
sessionKey?: string;
|
||||||
|
usage?: NormalizedUsage;
|
||||||
|
modelUsed?: string;
|
||||||
|
providerUsed?: string;
|
||||||
|
contextTokensUsed?: number;
|
||||||
|
systemPromptReport?: SessionSystemPromptReport;
|
||||||
|
cliSessionId?: string;
|
||||||
|
logLabel?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { storePath, sessionKey } = params;
|
||||||
|
if (!storePath || !sessionKey) return;
|
||||||
|
|
||||||
|
const label = params.logLabel ? `${params.logLabel} ` : "";
|
||||||
|
if (hasNonzeroUsage(params.usage)) {
|
||||||
|
try {
|
||||||
|
await updateSessionStoreEntry({
|
||||||
|
storePath,
|
||||||
|
sessionKey,
|
||||||
|
update: async (entry) => {
|
||||||
|
const input = params.usage?.input ?? 0;
|
||||||
|
const output = params.usage?.output ?? 0;
|
||||||
|
const promptTokens =
|
||||||
|
input + (params.usage?.cacheRead ?? 0) + (params.usage?.cacheWrite ?? 0);
|
||||||
|
const patch: Partial<SessionEntry> = {
|
||||||
|
inputTokens: input,
|
||||||
|
outputTokens: output,
|
||||||
|
totalTokens: promptTokens > 0 ? promptTokens : (params.usage?.total ?? input),
|
||||||
|
modelProvider: params.providerUsed ?? entry.modelProvider,
|
||||||
|
model: params.modelUsed ?? entry.model,
|
||||||
|
contextTokens: params.contextTokensUsed ?? entry.contextTokens,
|
||||||
|
systemPromptReport: params.systemPromptReport ?? entry.systemPromptReport,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
if (params.cliSessionId) {
|
||||||
|
const nextEntry = { ...entry, ...patch };
|
||||||
|
setCliSessionId(nextEntry, params.providerUsed, params.cliSessionId);
|
||||||
|
return {
|
||||||
|
...patch,
|
||||||
|
cliSessionIds: nextEntry.cliSessionIds,
|
||||||
|
claudeCliSessionId: nextEntry.claudeCliSessionId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return patch;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logVerbose(`failed to persist ${label}usage update: ${String(err)}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.modelUsed || params.contextTokensUsed) {
|
||||||
|
try {
|
||||||
|
await updateSessionStoreEntry({
|
||||||
|
storePath,
|
||||||
|
sessionKey,
|
||||||
|
update: async (entry) => {
|
||||||
|
const patch: Partial<SessionEntry> = {
|
||||||
|
modelProvider: params.providerUsed ?? entry.modelProvider,
|
||||||
|
model: params.modelUsed ?? entry.model,
|
||||||
|
contextTokens: params.contextTokensUsed ?? entry.contextTokens,
|
||||||
|
systemPromptReport: params.systemPromptReport ?? entry.systemPromptReport,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
if (params.cliSessionId) {
|
||||||
|
const nextEntry = { ...entry, ...patch };
|
||||||
|
setCliSessionId(nextEntry, params.providerUsed, params.cliSessionId);
|
||||||
|
return {
|
||||||
|
...patch,
|
||||||
|
cliSessionIds: nextEntry.cliSessionIds,
|
||||||
|
claudeCliSessionId: nextEntry.claudeCliSessionId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return patch;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logVerbose(`failed to persist ${label}model/context update: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user