Skip to content

Commit f99db8a

Browse files
refactor(usage): Antigravity usage is a reader on its driver
The Antigravity reader moves next to AntigravityDriver as a scan reader. Its parsed-database cache and root resolution live in an AntigravityUsage service, and profile roots come from the configured instances the aggregator passes in. Scan readers may fail the read with UsageReadError, which keeps the profile-resolution failure as before. UsageService now names no provider: it keys shared scans on the whole settings snapshot, since readers read settings of their own. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent aec4a9f commit f99db8a

10 files changed

Lines changed: 564 additions & 485 deletions

File tree

‎apps/server/src/provider/Drivers/AntigravityDriver.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from "../../textGeneration/AntigravityTextGeneration.ts";
2727
import { makeAntigravityAuth, type AntigravityAuth } from "../AntigravityAuth.ts";
2828
import * as AntigravityInstallation from "../AntigravityInstallation.ts";
29+
import * as AntigravityUsage from "./AntigravityUsage.ts";
2930
import {
3031
antigravityAuthConfigIssue,
3132
antigravityAuthLabel,
@@ -80,11 +81,16 @@ export type AntigravityDriverEnv =
8081
| ProviderEventLoggers.ProviderEventLoggers;
8182

8283
/** Each instance owns its Google profile. Executable releases are shared by the environment. */
83-
export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityDriverEnv> = {
84+
export const AntigravityDriver: ProviderDriver<
85+
AntigravitySettings,
86+
AntigravityDriverEnv,
87+
AntigravityUsage.AntigravityUsage
88+
> = {
8489
driverKind: DRIVER,
8590
metadata: { displayName: "Antigravity", supportsMultipleInstances: true },
8691
configSchema: AntigravitySettings,
8792
defaultConfig: () => decodeSettings({}),
93+
usage: AntigravityUsage.antigravityUsageReader,
8894
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
8995
Effect.gen(function* () {
9096
const crypto = yield* Crypto.Crypto;
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* AntigravityUsage - Antigravity's usage history as a usage source.
3+
*
4+
* Antigravity keeps conversations in SQLite databases under its data
5+
* directories and each instance's profile. Parsed databases stay in memory
6+
* while a database and its WAL keep the same `(size, mtime, ctime)`, so a
7+
* warm scan decodes only what changed.
8+
*
9+
* @module provider/Drivers/AntigravityUsage
10+
*/
11+
import * as NodeOS from "node:os";
12+
13+
import { type AntigravitySettings, UsageReadError } from "@t3tools/contracts";
14+
import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
15+
import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
16+
import type {
17+
ProviderUsageInstance,
18+
ProviderUsageReader,
19+
ProviderUsageScan,
20+
} from "@t3tools/provider-core/server/usage";
21+
import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
22+
import * as Context from "effect/Context";
23+
import * as Crypto from "effect/Crypto";
24+
import * as Effect from "effect/Effect";
25+
import * as FileSystem from "effect/FileSystem";
26+
import * as Layer from "effect/Layer";
27+
import * as Path from "effect/Path";
28+
29+
import { resolveAntigravityInstanceDirectories } from "../antigravityAuthSupport.ts";
30+
import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts";
31+
32+
export class AntigravityUsage extends Context.Service<
33+
AntigravityUsage,
34+
{
35+
/** Every Antigravity store: the data directories and each configured instance's profile. */
36+
readonly scan: (input: {
37+
readonly instances: ReadonlyArray<ProviderUsageInstance<AntigravitySettings>>;
38+
readonly windowStartMs: number;
39+
}) => Effect.Effect<ReadonlyArray<ProviderUsageScan>, UsageReadError>;
40+
}
41+
>()("t3/provider/Drivers/AntigravityUsage") {}
42+
43+
const make = Effect.gen(function* () {
44+
const crypto = yield* Crypto.Crypto;
45+
const fileSystem = yield* FileSystem.FileSystem;
46+
const path = yield* Path.Path;
47+
const host = yield* ProviderHost.ProviderHost;
48+
const hostEnvironment = yield* HostProcessEnvironment;
49+
const cache = makeAntigravityUsageCache();
50+
51+
/** `ANTIGRAVITY_DATA_DIR` (comma-separated) or the defaults, canonicalized. */
52+
const dataRoots = Effect.gen(function* () {
53+
const home = NodeOS.homedir();
54+
const configured = hostEnvironment["ANTIGRAVITY_DATA_DIR"]
55+
?.split(",")
56+
.map((value) => value.trim())
57+
.filter(Boolean);
58+
const defaults = [
59+
...["antigravity", "antigravity-cli", "antigravity-ide", "antigravity-backup"].map((name) =>
60+
path.join(home, ".gemini", name),
61+
),
62+
path.join(home, ".config", "antigravity"),
63+
];
64+
const canonical = new Set<string>();
65+
for (const root of configured?.length ? configured : defaults) {
66+
const resolved = path.resolve(expandHomePath(root));
67+
canonical.add(
68+
yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)),
69+
);
70+
}
71+
return [...canonical];
72+
});
73+
74+
const scan: AntigravityUsage["Service"]["scan"] = Effect.fn("AntigravityUsage.scan")(function* ({
75+
instances,
76+
windowStartMs,
77+
}) {
78+
const roots = yield* dataRoots;
79+
// Only configured instances have a profile; the implicit default never ran.
80+
for (const { instanceId } of instances.filter((instance) => instance.configured)) {
81+
const directories = yield* resolveAntigravityInstanceDirectories(
82+
host.paths.stateDir,
83+
instanceId,
84+
).pipe(
85+
Effect.provideService(Crypto.Crypto, crypto),
86+
Effect.provideService(Path.Path, path),
87+
Effect.mapError(
88+
(cause) =>
89+
new UsageReadError({
90+
reason: "scanFailed",
91+
detail: "Antigravity profile directory could not be resolved.",
92+
cause,
93+
}),
94+
),
95+
);
96+
roots.push(path.join(directories.profile, "antigravity-acp"));
97+
}
98+
const conversationDirs = new Set<string>();
99+
for (const root of roots) {
100+
const resolvedRoot = yield* fileSystem.realPath(root).pipe(Effect.orElseSucceed(() => root));
101+
const nested = path.join(resolvedRoot, "conversations");
102+
const dir = (yield* fileSystem
103+
.exists(nested)
104+
.pipe(Effect.catchCause(() => Effect.succeed(false))))
105+
? nested
106+
: resolvedRoot;
107+
conversationDirs.add(yield* fileSystem.realPath(dir).pipe(Effect.orElseSucceed(() => dir)));
108+
}
109+
const result = yield* Effect.promise(() =>
110+
readAntigravityUsage([...conversationDirs], windowStartMs, cache),
111+
);
112+
const scanned: ProviderUsageScan[] = [];
113+
for (const dir of conversationDirs) {
114+
const exists = yield* fileSystem
115+
.exists(dir)
116+
.pipe(Effect.catchCause(() => Effect.succeed(false)));
117+
const failed = result.errors.some(
118+
(error) => error === dir || error.startsWith(`${dir}${path.sep}`),
119+
);
120+
scanned.push({
121+
dir,
122+
files: !exists && !failed ? null : result.files.filter((file) => file.root === dir),
123+
status: failed ? "partial" : "ok",
124+
...(failed ? { message: "Some Antigravity history could not be read." } : {}),
125+
});
126+
}
127+
return scanned;
128+
});
129+
130+
return AntigravityUsage.of({ scan });
131+
});
132+
133+
export const layer = Layer.effect(AntigravityUsage, make);
134+
135+
export const antigravityUsageReader: ProviderUsageReader<AntigravitySettings, AntigravityUsage> = {
136+
kind: "scan",
137+
provider: "antigravity",
138+
scan: ({ instances, windowStartMs }) =>
139+
AntigravityUsage.pipe(Effect.flatMap((usage) => usage.scan({ instances, windowStartMs }))),
140+
};

0 commit comments

Comments
 (0)