镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3ef76de
feat(usage): break usage down by project, thread, account and environ…
github-actions[bot] Oct 12, 2026
dcd633b
fix(web): rows under a provider's accounts open down to their threads
github-actions[bot] Oct 12, 2026
1529dfa
fix(web): sub-agents follow the chosen sort
github-actions[bot] Oct 12, 2026
eef8418
merge: resolve #15902 conflicts with main
saphid Oct 17, 2026
74554b2
fix(usage): preserve attribution and retry failed snapshots
saphid Oct 20, 2026
d9a135c
merge: preserve usage attribution alongside current main diagnostics
saphid Oct 20, 2026
850fd70
fix(usage): retain Other totals and folder attribution
saphid Oct 20, 2026
198efb9
merge: retain usage attribution and Codex tier recovery on current main
saphid Oct 20, 2026
34bbd49
fix: integrate current main while preserving PR 15902 behavior
saphid Oct 23, 2026
c693087
fix: integrate current main while preserving PR 15902 behavior
saphid Oct 23, 2026
c478742
merge: retain current usage parsers and responsive controls with attr…
saphid Oct 31, 2026
31f3764
Merge commit '79f1fadfc6020b820d541ca4691d2feebb15dd1b' into ready/ge…
saphid Oct 31, 2026
744c319
fix(usage): migrate warm advisor caches to attribution format v11
saphid Oct 31, 2026
53f29b3
fix(usage): retain current navigation and React lifecycle rules
saphid Oct 31, 2026
af143f3
fix(usage): keep date controls reachable across window sizes
maria-rcks Oct 31, 2026
0d08670
Merge remote-tracking branch 'upstream/main' into agent/usage-redesign
saphid Nov 4, 2026
7d55f39
fix(web): offline usage respects the usage model filter
saphid Nov 4, 2026
ea79fa3
fix(web): saved usage keeps one read per window and model filter
saphid Nov 4, 2026
ae58372
chore: merge current main into usage redesign
saphid Nov 11, 2026
90cf4c0
fix(server): scope synthetic usage threads by account
saphid Nov 11, 2026
2dc76c9
fix(web): expose selected usage date range
saphid Nov 11, 2026
e673734
test(web): remove stale usage provider expectations
saphid Nov 11, 2026
893e624
fix(server): keep removing superseded usage scan caches
saphid Nov 11, 2026
f7811b0
chore: merge main into usage redesign
saphid Nov 11, 2026
86c8425
chore: merge main into usage redesign
saphid Nov 12, 2026
9905323
fix(shared): prefer fresh complete usage scans
saphid Nov 12, 2026
26e6572
fix(web): require exact saved usage windows
saphid Nov 12, 2026
8135101
fix(server): make copied usage attribution deterministic
saphid Nov 12, 2026
8046ab7
fix(shared): keep attributed scans unless they drop records
saphid Nov 12, 2026
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
5 changes: 5 additions & 0 deletions apps/server/src/provider/Drivers/claudeUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type ProviderUsageReader,
type TranscriptUsageFormat,
type UsageRecord,
workingDirectoryHook,
} from "@t3tools/provider-core/server/usage";
import * as HostProcess from "@t3tools/shared/HostProcess";

Expand Down Expand Up @@ -115,12 +116,16 @@ export const claudeUsageFormat: TranscriptUsageFormat<void> = {
timestamp: true,
requestId: true,
sessionId: true,
cwd: true,
costUSD: true,
message: { id: true, model: true, usage: true },
},
usageMarkers: ['"usage"'],
parseLine: (line) => parseClaudeLine(line),
parseProjected: (projected) => parseClaudeRecord(projected),
// Every record names the folder, but the first usage line may come after
// records without usage, so any record naming it is read until one does.
workingDirectory: workingDirectoryHook(['"cwd"'], (record) => record["cwd"]),
};

export const claudeUsageReader: ProviderUsageReader<ClaudeSettings, Path.Path> = {
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/provider/Drivers/codexUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type TranscriptUsageFormat,
type UsageRecord,
type UsageSpeed,
workingDirectoryHook,
} from "@t3tools/provider-core/server/usage";
import type { UsageTokenTotals } from "@t3tools/contracts";

Expand Down Expand Up @@ -416,6 +417,7 @@ export const codexUsageFormat: TranscriptUsageFormat<CodexScanState> = {
type: true,
id: true,
session_id: true,
cwd: true,
model_provider: true,
turn_id: true,
model: true,
Expand All @@ -440,6 +442,15 @@ export const codexUsageFormat: TranscriptUsageFormat<CodexScanState> = {
readMalformedRecords: (state) => state.malformedRecords,
// A rollout that moved after it was read leaves a copy in both places.
sharedSessionsAcrossFiles: true,
// The rollout's first `session_meta` is its own session; a fork repeats its
// ancestors' metas after it, and the engine keeps the first folder.
workingDirectory: workingDirectoryHook(['"session_meta"'], (record, state: CodexScanState) => {
if (state.sawSessionMeta) return null;
const payload = record["payload"];
return record["type"] === "session_meta" && typeof payload === "object" && payload !== null
? (payload as Record<string, unknown>)["cwd"]
: null;
}),
};

export const codexUsageReader: ProviderUsageReader<
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ import { MuseUsageHome } from "@t3tools/provider-muse/server";
import * as CursorAccountReader from "@t3tools/provider-cursor/server/CursorAccountReader";
import * as CursorUsageAccounts from "@t3tools/provider-cursor/server/CursorUsageAccounts";
import * as UsageService from "./usage/UsageService.ts";
import * as UsageAttribution from "./usage/UsageAttribution.ts";
import * as CodexUsageHistory from "./usage/CodexUsageHistory.ts";
import * as RuntimeLayer from "./orchestration-v2/runtimeLayer.ts";
import * as ProjectStore from "./orchestration-v2/ProjectStore.ts";
Expand Down Expand Up @@ -300,6 +301,7 @@ const layerUsage = UsageService.layer.pipe(
CursorUsageAccounts.layer.pipe(Layer.provide(CursorAccountReader.layer)),
).pipe(Layer.provide(ProviderHostLive.layer.pipe(Layer.provide(ServerSecretStore.layer)))),
),
Layer.provide(UsageAttribution.layer.pipe(Layer.provide(ProjectStore.layer))),
Layer.provide(layerServerSettings),
);

Expand Down
149 changes: 149 additions & 0 deletions apps/server/src/usage/UsageAttribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { ProjectId, ThreadId } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as SqlClient from "effect/sql/SqlClient";
import * as SqlSchema from "effect/sql/SqlSchema";

import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";
import type { NativeThreadRef, T3ThreadRef, UsageAttributionIndex } from "./usageThreadIndex.ts";

export class UsageAttributionError extends Schema.TaggedError<UsageAttributionError>()(
"UsageAttributionError",
{
/** Which read failed. */
stage: Schema.Literals(["providerThreads", "subagents", "threads", "projects"]),
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Could not read ${this.stage} for usage attribution.`;
}
}

const failedAt =
(stage: UsageAttributionError["stage"]) =>
<A, E, R>(read: Effect.Effect<A, E, R>) =>
read.pipe(Effect.mapError((cause) => new UsageAttributionError({ stage, cause })));

const NativeRow = Schema.Struct({
driver: Schema.String,
nativeId: Schema.String,
threadId: ThreadId,
instanceId: Schema.NullOr(Schema.String),
});

const ThreadRow = Schema.Struct({
threadId: ThreadId,
projectId: ProjectId,
title: Schema.NullOr(Schema.String),
parentThreadId: Schema.NullOr(ThreadId),
});

/**
* Reads which T3 thread each provider session and provider sub-agent belongs
* to, with thread lineage and every project, including archived and deleted
* ones so their past usage keeps its place.
*/
export class UsageAttribution extends Context.Service<
UsageAttribution,
{ readonly read: Effect.Effect<UsageAttributionIndex, UsageAttributionError> }
>()("t3/usage/UsageAttribution") {}

const make = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
const projectStore = yield* ProjectStore.ProjectStoreV2;

const selectNativeThreads = SqlSchema.findAll({
Request: Schema.Void,
Result: NativeRow,
execute: () => sql`
SELECT
driver,
json_extract(payload_json, '$.nativeThreadRef.nativeId') AS "nativeId",
thread_id AS "threadId",
provider_instance_id AS "instanceId"
FROM orchestration_v2_projection_provider_threads
WHERE thread_id IS NOT NULL
AND driver IS NOT NULL
AND json_extract(payload_json, '$.nativeThreadRef.nativeId') IS NOT NULL
`,
});

const selectNativeSubagents = SqlSchema.findAll({
Request: Schema.Void,
Result: NativeRow,
execute: () => sql`
SELECT
driver,
json_extract(payload_json, '$.nativeTaskRef.nativeId') AS "nativeId",
child_thread_id AS "threadId",
provider_instance_id AS "instanceId"
FROM orchestration_v2_projection_subagents
WHERE child_thread_id IS NOT NULL
AND driver IS NOT NULL
AND json_extract(payload_json, '$.nativeTaskRef.nativeId') IS NOT NULL
`,
});

const selectThreads = SqlSchema.findAll({
Request: Schema.Void,
Result: ThreadRow,
execute: () => sql`
SELECT
thread_id AS "threadId",
project_id AS "projectId",
title,
json_extract(payload_json, '$.lineage.parentThreadId') AS "parentThreadId"
FROM orchestration_v2_projection_threads
`,
});

const byNativeId = (rows: ReadonlyArray<typeof NativeRow.Type>) => {
const map = new Map<string, NativeThreadRef[]>();
for (const row of rows) {
const key = `${row.driver}\u0000${row.nativeId}`;
const entries = map.get(key) ?? [];
entries.push({ threadId: row.threadId, instanceId: row.instanceId });
map.set(key, entries);
}
return map;
};

const read = Effect.all(
[
selectNativeThreads(undefined).pipe(failedAt("providerThreads")),
selectNativeSubagents(undefined).pipe(failedAt("subagents")),
selectThreads(undefined).pipe(failedAt("threads")),
projectStore.list({ includeDeleted: true }).pipe(failedAt("projects")),
],
{ concurrency: 1 },
).pipe(
Effect.map(([nativeThreads, nativeSubagents, threads, projects]) => ({
nativeThreads: byNativeId(nativeThreads),
nativeSubagents: byNativeId(nativeSubagents),
threads: new Map<string, T3ThreadRef>(
threads.map((row) => [
row.threadId,
{
projectId: row.projectId,
title: row.title?.trim() || null,
parentThreadId: row.parentThreadId,
},
]),
),
projects: projects.map((project) => ({
projectId: project.projectId,
title: project.title,
workspaceRoot: project.workspaceRoot,
deleted: project.deletedAt !== null,
})),
})),
Effect.withSpan("UsageAttribution.read"),
);

return UsageAttribution.of({ read });
});

export const layer = Layer.effect(UsageAttribution, make);
Loading
Loading