Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/upstream-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,5 @@ The review cursor stays unchanged; unrelated sources in this range remain unclas

| PR cache reuse and quota efficiency / `6d1d549441be84f19696ab59ed7e2fbf305280d4` | `f4600d77dd7c2fa9f10e8f4500882e427fcc7e26` (#11888), `d612d12b8bb278af97b0029f8d48b2403a5f9ee6` (#12168) | Adopted applicable behavior: scoped cache revisions, canonical detail reuse, deduplicated refreshes and bounded quota probes. Preserve Pylon environment fences and observed GraphQL consumption. Exclude account-router-specific hooks and Forgejo tests: those dependencies are absent in Pylon; their independent source decisions remain open. No cross-environment credential routing introduced. | Cycle [#625](https://github.com/pylon-code/pylon/issues/625), [PR #626](https://github.com/pylon-code/pylon/pull/626); direct adversarial self-review, focused backend/client regressions and scoped types/lint. |
| Authenticated GitHub PR media / `6d1d549441be84f19696ab59ed7e2fbf305280d4` | `32e8b2584556c0c55ce0d1d8f72b506cb771d60d` (#11706) | Adopted with Pylon path/context guards retained. Signed assets use the active server GitHub CLI account per request; bounded GitHub/CDN redirects strip credentials off credentialed hosts, stream ranges, constrain MIME and sandbox SVG. Older servers retain public fallback. GitHub Enterprise media unsupported. | Cycle [#625](https://github.com/pylon-code/pylon/issues/625), [PR #629](https://github.com/pylon-code/pylon/pull/629); direct adversarial self-review, credential/redirect/range regressions, scoped checks and browser evidence. |

| Retained usage totals and newest shared scan / `7ed3f711caa36485001538bbe1cb0017b84aa7fe` | `d4d5d12e8ba086cfbf79ca3adeb4156b46ead665` (#10315), `675869d2b0f9e059d5950b80390c0cad1cb1590e` (#12304) | Adopted with Pylon adaptation: retain saved usage after transcript cleanup or moves and across restart; claim a shared fingerprint from the newest scan with a stable ID tie-break. Configured and standalone Antigravity homes retain their profile identity, partial status and per-directory SQLite limits. Stale source identities expire when no current or retained records use them. Existing provider-level wire buckets cannot split costs by individual home when two environments each own distinct and shared homes; that cross-environment overlap remains outside this port and needs source-attributed buckets. The 90-day retention bound remains. Cursor unchanged. | Cycle [#736](https://github.com/pylon-code/pylon/issues/736), [PR #762](https://github.com/pylon-code/pylon/pull/762); 46 focused tests and scoped server checks. |
261 changes: 257 additions & 4 deletions apps/server/src/usage/UsageService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import * as NodeSqlite from "node:sqlite";
import { assert, describe, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
import { mergeUsage } from "@t3tools/shared/usageMerge";
import {
EnvironmentId,
ProviderDriverKind,
ProviderInstanceId,
ServerProviderInstancesMutationId,
Expand Down Expand Up @@ -148,14 +150,15 @@ describe("UsageService", () => {
[
{ type: "session_meta", payload: { id: "codex-account-session" } },
{ type: "turn_context", payload: { model: "gpt-5.6-sol" } },
{
// A-B-A at one timestamp must preserve both equal A events.
...[11, 12, 11].map((outputTokens) => ({
type: "event_msg",
timestamp: "2026-08-01T10:00:00Z",
payload: {
type: "token_count",
info: { last_token_usage: { input_tokens: 10, output_tokens: 11 } },
info: { last_token_usage: { input_tokens: 10, output_tokens: outputTokens } },
},
},
})),
]
.map((line) => encodeUnknownJsonString(line))
.join("\n") + "\n",
Expand Down Expand Up @@ -215,7 +218,21 @@ describe("UsageService", () => {
),
);
const summary = yield* service.readSummary(WINDOW);
assert.strictEqual(totalOutputTokens(summary), 36);
assert.strictEqual(totalOutputTokens(summary), 59);
yield* Effect.promise(() =>
NodeFSP.rename(
NodePath.join(codexHome, "sessions", "rollout.jsonl"),
NodePath.join(codexHome, "sessions", "moved.jsonl"),
),
);
const moved = yield* service.readSummary(WINDOW);
assert.deepStrictEqual(moved.buckets, summary.buckets);
yield* Effect.promise(() =>
NodeFSP.rm(NodePath.join(codexHome, "sessions"), { recursive: true }),
);
const removed = yield* service.readSummary(WINDOW);
assert.deepStrictEqual(removed.buckets, summary.buckets);

const sources = summary.sources.filter((source) => source.status === "ok");
assert.strictEqual(sources.length, 4);
assert.strictEqual(
Expand Down Expand Up @@ -458,6 +475,128 @@ describe("UsageService", () => {
}).pipe(Effect.scoped),
);

it.live("preserves saved tokens, costs and sessions after transcript cleanup and restart", () =>
Effect.gen(function* () {
const { transcript, settings, home } = yield* setup;
const alias = NodePath.join(home, "claude-alias");
yield* Effect.promise(() =>
NodeFSP.symlink(NodePath.join(home, "claude"), alias, "junction"),
);
const content = claudeLine(1, 5);
yield* Effect.promise(() => NodeFSP.writeFile(transcript, content));
yield* Effect.gen(function* () {
const service = yield* UsageService.make;
const first = yield* service.readSummary(WINDOW);
assert.strictEqual(totalOutputTokens(first), 5);
assert.isAbove(first.buckets[0]?.costUsd ?? 0, 0);

yield* Effect.promise(() => NodeFSP.rm(transcript));
const deleted = yield* service.readSummary(WINDOW);
assert.deepStrictEqual(deleted.buckets, first.buckets);
assert.deepStrictEqual(deleted.sources, first.sources);

const restarted = yield* UsageService.make;
const restored = yield* restarted.readSummary(WINDOW);
assert.deepStrictEqual(restored.buckets, first.buckets);
assert.deepStrictEqual(restored.sources, first.sources);

// A moved transcript must not count the saved usage twice.
yield* Effect.promise(() => NodeFSP.writeFile(transcript + ".jsonl", content));
const moved = yield* restarted.readSummary(WINDOW);
assert.deepStrictEqual(moved.buckets, first.buckets);
assert.strictEqual(moved.sources[0]?.distinctSessions, 1);

const replacementProjects = NodePath.join(home, "replacement-projects");
yield* Effect.promise(() => NodeFSP.mkdir(replacementProjects));
yield* Effect.promise(() =>
NodeFSP.rm(NodePath.join(home, "claude", "projects"), { recursive: true }),
);
const afterRootCleanup = yield* UsageService.make;
const missingRoot = yield* afterRootCleanup.readSummary(WINDOW);
assert.deepStrictEqual(missingRoot.buckets, first.buckets);
assert.strictEqual(missingRoot.sources[0]?.distinctSessions, 1);
assert.strictEqual(missingRoot.sources[0]?.status, "ok");
assert.deepStrictEqual(missingRoot.sources[0]?.fingerprint, first.sources[0]?.fingerprint);
yield* Effect.promise(async () => {
const projects = NodePath.join(home, "claude", "projects");
await NodeFSP.rename(replacementProjects, projects);
await NodeFSP.writeFile(NodePath.join(projects, "new.jsonl"), claudeLine(2, 7));
});
const recreated = yield* afterRootCleanup.readSummary(WINDOW);
assert.strictEqual(totalOutputTokens(recreated), 12);
assert.deepStrictEqual(recreated.sources[0]?.fingerprint, first.sources[0]?.fingerprint);

const merged = mergeUsage(
[
{
environmentId: EnvironmentId.make("cleanup-test"),
label: "test",
summary: recreated,
},
{
environmentId: EnvironmentId.make("other-environment"),
label: "before cleanup",
summary: first,
},
],
missingRoot.contractVersion,
);
assert.strictEqual(merged.outputTokens, 12);
assert.strictEqual(merged.sessions, 1);
assert.strictEqual(merged.costUsd, recreated.buckets[0]?.costUsd);

const outsideWindow = yield* restarted.readSummary({
...WINDOW,
sinceDay: UsageDay.make("2026-08-02"),
});
assert.deepStrictEqual(outsideWindow.buckets, []);
assert.strictEqual(outsideWindow.sources[0]?.distinctSessions, 0);
}).pipe(
Effect.provide(
serviceLayers({
prefix: "usage-service-cleanup-test",
home,
settings: { providers: { ...settings.providers, claudeAgent: { homePath: alias } } },
ratesDocument: {
"claude-fable-5": { input_cost_per_token: 1e-5, output_cost_per_token: 5e-5 },
},
}),
),
);
}).pipe(Effect.scoped),
);

it.live("drops stale source identities after an empty account home is replaced", () =>
Effect.gen(function* () {
const { home, settings } = yield* setup;
const replacement = NodePath.join(home, "replacement-claude");
yield* Effect.promise(() =>
NodeFSP.mkdir(NodePath.join(replacement, "projects"), { recursive: true }),
);
yield* Effect.gen(function* () {
const config = yield* ServerConfig.ServerConfig;
const settingsService = yield* ServerSettings.ServerSettingsService;
const service = yield* UsageService.make;
yield* service.readSummary(WINDOW);
const cachePath = NodePath.join(config.stateDir, "usage-scan-cache.json");
const oldDir = NodePath.join(home, "claude", "projects");
assert.include(yield* Effect.promise(() => NodeFSP.readFile(cachePath, "utf8")), oldDir);

yield* settingsService.updateSettings({
providers: { claudeAgent: { homePath: replacement } },
});
yield* service.readSummary(WINDOW);
const persisted = yield* Effect.promise(() => NodeFSP.readFile(cachePath, "utf8"));
assert.notInclude(persisted, oldDir);
assert.include(persisted, NodePath.join(replacement, "projects"));
}).pipe(
Effect.provide(
serviceLayers({ prefix: "usage-service-source-prune-test", home, settings }),
),
);
}).pipe(Effect.scoped),
);

it.live("does not share an in-flight scan after custom prices change", () =>
Effect.gen(function* () {
const { transcript, settings, home } = yield* setup;
Expand Down Expand Up @@ -769,6 +908,120 @@ describe("UsageService", () => {
),
);

it.live("retains Antigravity usage after conversation cleanup and service restart", () =>
Effect.gen(function* () {
yield* setup;
const config = yield* ServerConfig.ServerConfig;
const instanceId = ProviderInstanceId.make("antigravity-retained");
const profileDir = resolveAntigravityProfileDirectory(config.stateDir, instanceId);
const convDir = NodePath.join(profileDir, "antigravity-acp", "conversations");
yield* Effect.promise(() => NodeFSP.mkdir(convDir, { recursive: true }));
const dbPath = NodePath.join(convDir, "retained.db");
seedAntigravityDb(dbPath, [
{
idx: 0,
blob: encodeSyntheticGenMetadataBlob({
modelName: "gemini-2.5-pro",
timestampSeconds: 1785578400n,
inputTokens: 100,
outputTokens: 40,
}),
},
]);

const service = yield* UsageService.make;
const first = yield* service.readSummary(WINDOW);
yield* Effect.promise(() => NodeFSP.rm(convDir, { recursive: true }));
const afterCleanup = yield* service.readSummary(WINDOW);
const restarted = yield* UsageService.make;
const afterRestart = yield* restarted.readSummary(WINDOW);

for (const [phase, summary] of [
["cleanup", afterCleanup],
["restart", afterRestart],
] as const) {
assert.deepStrictEqual(summary.buckets, first.buckets, phase);
const source = summary.sources.find(
(item) =>
item.fingerprint.provider === "antigravity" &&
item.fingerprint.resolvedHomePath === convDir,
);
assert.isDefined(source);
assert.strictEqual(source.status, "ok");
assert.strictEqual(source.scannedFiles, 1);
}
}).pipe(
Effect.provide(
serviceLayers({
prefix: "usage-service-antigravity-retained-test",
home: "",
settings: {
providers: {},
providerInstances: {
[ProviderInstanceId.make("antigravity-retained")]: {
driver: "antigravity",
enabled: true,
},
},
},
}),
),
Effect.scoped,
),
);

it.live(
"retains standalone Antigravity history after its directory is removed and service restarts",
() =>
Effect.gen(function* () {
yield* setup;
const config = yield* ServerConfig.ServerConfig;
const convDir = NodePath.join(config.stateDir, "standalone-antigravity", "conversations");
yield* Effect.promise(() => NodeFSP.mkdir(convDir, { recursive: true }));
seedAntigravityDb(NodePath.join(convDir, "session.db"), [
{
idx: 0,
blob: encodeSyntheticGenMetadataBlob({
modelName: "gemini-2.5-pro",
timestampSeconds: 1785578400n,
inputTokens: 100,
outputTokens: 40,
}),
},
]);

yield* Effect.gen(function* () {
const firstService = yield* UsageService.make;
const first = yield* firstService.readSummary(WINDOW);
assert.isTrue(first.buckets.some((bucket) => bucket.provider === "antigravity"));
yield* Effect.promise(() => NodeFSP.rm(convDir, { recursive: true }));

for (const summary of [
yield* firstService.readSummary(WINDOW),
yield* (yield* UsageService.make).readSummary(WINDOW),
]) {
assert.deepStrictEqual(summary.buckets, first.buckets);
const source = summary.sources.find(
(item) =>
item.fingerprint.provider === "antigravity" &&
item.fingerprint.resolvedHomePath === convDir,
);
assert.isDefined(source);
assert.strictEqual(source.status, "ok");
}
}).pipe(Effect.provideService(UsageService.StandaloneAntigravityConversations, convDir));
}).pipe(
Effect.provide(
serviceLayers({
prefix: "usage-service-standalone-antigravity-retained-test",
home: "",
settings: { providers: {}, providerInstances: {} },
}),
),
Effect.scoped,
),
);

it.live(
"reports honest missing status when configured Antigravity instance has no conversations dir on disk",
() =>
Expand Down
Loading
Loading