pi-cursor-sdk 0.4.0 → 0.5.0
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/CHANGELOG.md +30 -0
- package/README.md +65 -13
- package/dist/context-window-cache.js +0 -6
- package/dist/context.js +8 -0
- package/dist/cursor-cloud-lifecycle.js +122 -98
- package/dist/cursor-durable-fs.js +97 -3
- package/dist/cursor-fallback-models.generated.js +288 -1
- package/dist/cursor-fallback-warning.js +2 -2
- package/dist/cursor-http1.js +8 -8
- package/dist/cursor-live-run-coordinator.js +9 -13
- package/dist/cursor-native-replay-routing.js +1 -1
- package/dist/cursor-native-tool-display-registration.js +46 -22
- package/dist/cursor-native-tool-display-state.js +57 -22
- package/dist/cursor-native-tool-display-tools.js +5 -6
- package/dist/cursor-pi-tool-bridge-constants.js +23 -2
- package/dist/cursor-pi-tool-bridge-run.js +7 -3
- package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
- package/dist/cursor-pi-tool-bridge.js +31 -9
- package/dist/cursor-provider-binding.js +58 -0
- package/dist/cursor-provider-errors.js +28 -7
- package/dist/cursor-provider-lazy.js +23 -11
- package/dist/cursor-provider-live-run-drain.js +15 -12
- package/dist/cursor-provider-run-finalizer.js +19 -8
- package/dist/cursor-provider-turn-coordinator.js +1 -0
- package/dist/cursor-provider-turn-display-router.js +4 -0
- package/dist/cursor-provider-turn-emit.js +1 -0
- package/dist/cursor-provider-turn-finalize.js +29 -20
- package/dist/cursor-provider-turn-prepare.js +24 -11
- package/dist/cursor-provider-turn-runner.js +37 -11
- package/dist/cursor-provider-turn-send.js +22 -9
- package/dist/cursor-provider-turn-summary.js +89 -0
- package/dist/cursor-provider.js +9 -3
- package/dist/cursor-request-provenance.js +42 -0
- package/dist/cursor-ripgrep-path.js +14 -3
- package/dist/cursor-runtime-state.js +50 -43
- package/dist/cursor-sdk-billed-usage.js +54 -75
- package/dist/cursor-sdk-event-debug-session.js +5 -6
- package/dist/cursor-sdk-event-debug.js +9 -6
- package/dist/cursor-sdk-process-error-guard.js +8 -7
- package/dist/cursor-sdk-runtime.js +13 -0
- package/dist/cursor-session-agent-cleanup.js +2 -2
- package/dist/cursor-session-agent-lifecycle.js +18 -9
- package/dist/cursor-session-agent-lineage.js +28 -12
- package/dist/cursor-session-agent-resume.js +51 -35
- package/dist/cursor-session-agent.js +9 -9
- package/dist/cursor-session-compaction-prep.js +2 -7
- package/dist/cursor-session-scope.js +71 -39
- package/dist/cursor-session-settings.js +61 -0
- package/dist/cursor-skill-tool.js +32 -49
- package/dist/cursor-state.js +72 -62
- package/dist/cursor-usage-accounting.js +13 -70
- package/dist/cursor-usage-command.js +89 -0
- package/dist/cursor-usage-ledger.js +512 -0
- package/dist/index.js +11 -19
- package/dist/model-discovery.js +11 -19
- package/docs/cursor-dogfood-checklist.md +3 -3
- package/docs/cursor-live-smoke-checklist.md +15 -15
- package/docs/cursor-model-ux-spec.md +59 -18
- package/docs/cursor-native-tool-replay.md +5 -3
- package/docs/cursor-native-tool-visual-audit.md +8 -8
- package/docs/cursor-testing-lessons.md +47 -23
- package/docs/cursor-tool-surfaces.md +1 -1
- package/docs/platform-smoke-implementation.md +10 -6
- package/docs/platform-smoke.md +35 -46
- package/package.json +6 -6
- package/scripts/cloud-runtime-smoke.mjs +4 -2
- package/scripts/local-resume-smoke.mjs +1 -1
- package/scripts/platform-smoke/card-detect.mjs +2 -8
- package/scripts/platform-smoke/targets.mjs +8 -15
- package/scripts/platform-smoke/visual-evidence.mjs +7 -2
- package/scripts/tmux-live-smoke.sh +57 -90
- package/src/context-window-cache.ts +0 -6
- package/src/context.ts +8 -0
- package/src/cursor-cloud-lifecycle.ts +124 -98
- package/src/cursor-durable-fs.ts +65 -3
- package/src/cursor-fallback-models.generated.ts +288 -1
- package/src/cursor-fallback-warning.ts +2 -2
- package/src/cursor-http1.ts +8 -8
- package/src/cursor-live-run-coordinator.ts +12 -13
- package/src/cursor-native-replay-routing.ts +2 -1
- package/src/cursor-native-tool-display-registration.ts +48 -24
- package/src/cursor-native-tool-display-state.ts +65 -21
- package/src/cursor-native-tool-display-tools.ts +7 -4
- package/src/cursor-pi-tool-bridge-constants.ts +27 -2
- package/src/cursor-pi-tool-bridge-run.ts +11 -3
- package/src/cursor-pi-tool-bridge-snapshot.ts +1 -1
- package/src/cursor-pi-tool-bridge.ts +26 -9
- package/src/cursor-provider-binding.ts +62 -0
- package/src/cursor-provider-errors.ts +27 -7
- package/src/cursor-provider-lazy.ts +21 -15
- package/src/cursor-provider-live-run-drain.ts +20 -13
- package/src/cursor-provider-run-finalizer.ts +24 -15
- package/src/cursor-provider-turn-coordinator.ts +2 -0
- package/src/cursor-provider-turn-display-router.ts +5 -0
- package/src/cursor-provider-turn-emit.ts +3 -1
- package/src/cursor-provider-turn-finalize.ts +38 -23
- package/src/cursor-provider-turn-prepare.ts +23 -14
- package/src/cursor-provider-turn-runner.ts +40 -13
- package/src/cursor-provider-turn-send.ts +23 -15
- package/src/cursor-provider-turn-summary.ts +94 -0
- package/src/cursor-provider-turn-types.ts +37 -4
- package/src/cursor-provider.ts +25 -3
- package/src/cursor-request-provenance.ts +64 -0
- package/src/cursor-ripgrep-path.ts +15 -5
- package/src/cursor-runtime-state.ts +53 -48
- package/src/cursor-sdk-billed-usage.ts +54 -102
- package/src/cursor-sdk-event-debug-session.ts +5 -5
- package/src/cursor-sdk-event-debug.ts +10 -6
- package/src/cursor-sdk-process-error-guard.ts +7 -7
- package/src/cursor-sdk-runtime.ts +14 -0
- package/src/cursor-session-agent-cleanup.ts +2 -2
- package/src/cursor-session-agent-lifecycle.ts +14 -9
- package/src/cursor-session-agent-lineage.ts +26 -12
- package/src/cursor-session-agent-resume.ts +49 -36
- package/src/cursor-session-agent.ts +13 -10
- package/src/cursor-session-compaction-prep.ts +2 -7
- package/src/cursor-session-scope.ts +87 -39
- package/src/cursor-session-settings.ts +72 -0
- package/src/cursor-skill-tool.ts +37 -61
- package/src/cursor-state.ts +74 -67
- package/src/cursor-usage-accounting.ts +17 -82
- package/src/cursor-usage-command.ts +76 -0
- package/src/cursor-usage-ledger.ts +500 -0
- package/src/index.ts +14 -23
- package/src/model-discovery.ts +14 -22
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { closeSync, constants, fsyncSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { createRegularFileExclusive } from "./cursor-durable-fs.js";
|
|
4
|
+
import { fetchCursorSdkAgentUsage } from "./cursor-sdk-billed-usage.js";
|
|
5
|
+
import { resolveCursorApiKey } from "./cursor-api-key.js";
|
|
6
|
+
import { loadCursorSdk } from "./cursor-sdk-runtime.js";
|
|
7
|
+
import { readCursorUsageView, refreshCursorUsageView } from "./cursor-usage-ledger.js";
|
|
8
|
+
const HELP = [
|
|
9
|
+
"/cursor-usage — current branch telemetry (native footer unchanged)",
|
|
10
|
+
"/cursor-usage refresh — public getUsage only; up to 32 agents, 5 seconds each; no model sends or automatic polling",
|
|
11
|
+
"/cursor-usage export <new-file.json> — exclusive private JSON export; never overwrites a file",
|
|
12
|
+
"/cursor-usage help — this help",
|
|
13
|
+
"Storage ceiling: 16MiB per origin session. At the ceiling or after an incomplete trailing frame, new sends fail before spend. A torn tail is retained unchanged; view/export preserve validated prior frames and mark accounting incomplete. Malformed complete frames remain fail-closed.",
|
|
14
|
+
"Recovery: export readable facts and retain the original journal/session unchanged. Use Pi's native fork to preserve the conversation under a new session origin, or start a new native session. Never delete, truncate, or rewrite journals or native sessions to continue the old origin.",
|
|
15
|
+
"Other-branch facts have valid native claims elsewhere in this session; unclaimed facts have no surviving claim here and are not branch usage. Inherited records without a claim in this session retain unknown ownership; no sibling sessions are scanned. Fileless sessions are ephemeral.",
|
|
16
|
+
].join("\n");
|
|
17
|
+
export function renderCursorUsageView(view) {
|
|
18
|
+
return [
|
|
19
|
+
`Cursor usage — ${view.durability}; persistence ${view.persistenceStatus}; current branch telemetry`,
|
|
20
|
+
view.nativeConfiguredEstimates,
|
|
21
|
+
`Recorded corrected LOCAL raw tokens: ${view.correctedRawTotal.totalTokens} (input ${view.correctedRawTotal.inputTokens}, cache read ${view.correctedRawTotal.cacheReadTokens}, cache write ${view.correctedRawTotal.cacheWriteTokens}, output ${view.correctedRawTotal.outputTokens}).`,
|
|
22
|
+
`Original telemetry: ${view.records.filter(record => record.kind === "raw").length} raw turns; normalization unavailable/invalid: ${view.records.filter(record => record.kind === "raw" && !record.corrected).length}; ${view.records.filter(record => record.kind === "terminal").length} terminal facts (not added to raw).`,
|
|
23
|
+
`Other-branch journal records: ${view.otherBranchRecords.length}; unclaimed records: ${view.unclaimedRecords.length}; inherited records with unknown claim ownership: ${view.inheritedUnclaimedRecords.length}; inherited journal gaps: ${view.lineageGaps.length}; incomplete trailing frames: ${view.incompleteJournals.length}. Export retains these facts without billing them to this branch.`,
|
|
24
|
+
...view.lineageGaps,
|
|
25
|
+
...view.agents.slice(0, 32).map(agent => {
|
|
26
|
+
const missing = agent.billingStatus === "ambiguous-shared-lineage" ? "ambiguous" : agent.billingStatus === "unavailable" ? "unavailable" : agent.billingStatus === "pending" ? "pending" : "unknown";
|
|
27
|
+
const remainder = agent.billingStatus === "ambiguous-shared-lineage" ? "ambiguous" : agent.observationsByOrigin.some(observation => observation.incompleteHistory) ? "unknown (incomplete run history)" : agent.aggregateOnly?.totalTokens ?? missing;
|
|
28
|
+
return [
|
|
29
|
+
`${agent.agentId}: ${agent.billingStatus}${agent.unknownHistory ? "; unknown historical baseline" : ""}`,
|
|
30
|
+
agent.sharedAcrossBranches ? "; shared lineage, not attributable to this branch" : "; whole-agent facts, no client-run billing join",
|
|
31
|
+
`. Latest known billed tokens: ${agent.latestAggregate?.usage.totalTokens ?? missing}; SDK charged cents: ${agent.latestAggregate ? agent.latestAggregate.cost?.chargedCents ?? "not reported" : missing}; aggregate-only tokens: ${remainder}.`,
|
|
32
|
+
...agent.observationsByOrigin.map(observation => {
|
|
33
|
+
const absent = observation.lastAttempt.status === "unavailable" ? "unavailable" : "unknown";
|
|
34
|
+
return ` Origin ${observation.origin.sessionId}: ${observation.lastAttempt.status}${observation.incompleteHistory ? "; incomplete run history" : ""}; latest known billed tokens ${observation.latestAggregate?.usage.totalTokens ?? absent}; charged cents ${observation.latestAggregate ? observation.latestAggregate.cost?.chargedCents ?? "not reported" : absent}.`;
|
|
35
|
+
}),
|
|
36
|
+
].join("");
|
|
37
|
+
}),
|
|
38
|
+
...(view.agents.length > 32 ? [`${view.agents.length - 32} further agents retained in export.`] : []),
|
|
39
|
+
"Snapshots are observations, not final invoices. Multiple origin journals have no qualified global revision ordering. Cloud raw normalization is unqualified.",
|
|
40
|
+
`Journal ceiling: ${view.journalLimit}. See /cursor-usage help for recovery. Export contains no prompts or tool content.`,
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
export function registerCursorUsageCommand(pi) {
|
|
44
|
+
pi.registerCommand("cursor-usage", {
|
|
45
|
+
description: "View Cursor telemetry; refresh public billing or export to a new JSON file",
|
|
46
|
+
handler: async (args, ctx) => {
|
|
47
|
+
const show = (message, error = false) => {
|
|
48
|
+
if (ctx.hasUI)
|
|
49
|
+
ctx.ui.notify(message, error ? "error" : "info");
|
|
50
|
+
else
|
|
51
|
+
process.stderr.write(`${message}\n`); // Keep non-UI stdout/RPC framing untouched.
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
const argument = args.trim();
|
|
55
|
+
if (["help", "-h", "--help"].includes(argument)) {
|
|
56
|
+
show(HELP);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (argument && argument !== "refresh" && !argument.startsWith("export ")) {
|
|
60
|
+
show(HELP, true);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let view = readCursorUsageView(pi, ctx);
|
|
64
|
+
if (argument === "refresh") {
|
|
65
|
+
const { Agent } = await loadCursorSdk();
|
|
66
|
+
const apiKey = resolveCursorApiKey(await ctx.modelRegistry.getApiKeyForProvider("cursor"));
|
|
67
|
+
view = await refreshCursorUsageView(pi, ctx, agentId => fetchCursorSdkAgentUsage({ getUsage: () => Agent.getUsage(agentId, { apiKey }) }));
|
|
68
|
+
}
|
|
69
|
+
else if (argument.startsWith("export ")) {
|
|
70
|
+
const path = resolve(ctx.cwd, argument.slice(7).trim());
|
|
71
|
+
const fd = createRegularFileExclusive(path, constants.O_WRONLY, 0o600);
|
|
72
|
+
try {
|
|
73
|
+
writeFileSync(fd, `${JSON.stringify(view, null, 2)}\n`);
|
|
74
|
+
fsyncSync(fd);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
show(`Cursor usage exported to ${path}.`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
show(renderCursorUsageView(view));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
show("Cursor usage could not be read, persisted, refreshed, or exported. Existing files were not replaced; accounting may be incomplete. See /cursor-usage help for storage limits and recovery.", true);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
import { appendDurableFrame, fsyncExistingRegularFile, readDurableFramePrefix } from "./cursor-durable-fs.js";
|
|
5
|
+
import { fetchCursorSdkAgentUsage, readCursorBilledSnapshot, readCursorUsageTokens } from "./cursor-sdk-billed-usage.js";
|
|
6
|
+
import { asRecord } from "./cursor-record-utils.js";
|
|
7
|
+
const CUSTOM_TYPE = "pi-cursor-sdk:usage-v1";
|
|
8
|
+
const CLAIM_TYPE = "pi-cursor-sdk:usage-origin-v1";
|
|
9
|
+
const JOURNAL_LIMIT = "16MiB per origin session";
|
|
10
|
+
const bindings = new WeakMap();
|
|
11
|
+
const failures = new WeakMap();
|
|
12
|
+
const ephemeral = new WeakMap();
|
|
13
|
+
const tokenKeys = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "totalTokens"];
|
|
14
|
+
const zero = () => ({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0 });
|
|
15
|
+
const ownerKey = (origin) => JSON.stringify([origin.sessionFile, origin.sessionId]);
|
|
16
|
+
function originOf(ctx) {
|
|
17
|
+
const manager = ctx.sessionManager;
|
|
18
|
+
return { sessionFile: manager.getSessionFile(), sessionId: manager.getSessionId(), anchorId: manager.getLeafId() };
|
|
19
|
+
}
|
|
20
|
+
function journalPath(origin) {
|
|
21
|
+
if (!origin.sessionFile)
|
|
22
|
+
return undefined;
|
|
23
|
+
const digest = createHash("sha256").update(origin.sessionId).digest("hex").slice(0, 32);
|
|
24
|
+
return join(dirname(origin.sessionFile), `cursor-usage-${digest}.journal`);
|
|
25
|
+
}
|
|
26
|
+
const storageKey = (origin) => journalPath(origin) ?? origin.sessionId;
|
|
27
|
+
function text(value) {
|
|
28
|
+
return typeof value === "string" && value.length > 0 && value.length <= 4096 && !/[\x00-\x1f]/.test(value);
|
|
29
|
+
}
|
|
30
|
+
function only(record, keys) {
|
|
31
|
+
return Object.keys(record).every(key => keys.includes(key));
|
|
32
|
+
}
|
|
33
|
+
function validateOrigin(value) {
|
|
34
|
+
const origin = asRecord(value);
|
|
35
|
+
if (!origin || !only(origin, ["sessionFile", "sessionId", "anchorId"]) || !text(origin.sessionId) ||
|
|
36
|
+
(origin.sessionFile !== undefined && (!text(origin.sessionFile) || !isAbsolute(origin.sessionFile))) ||
|
|
37
|
+
(origin.anchorId !== null && !text(origin.anchorId)))
|
|
38
|
+
throw new Error("invalid Cursor usage origin");
|
|
39
|
+
return structuredClone(value);
|
|
40
|
+
}
|
|
41
|
+
function cleanSelection(value) {
|
|
42
|
+
const selection = asRecord(value);
|
|
43
|
+
if (!selection || !only(selection, ["id", "params"]) || !text(selection.id) || (selection.params !== undefined && !Array.isArray(selection.params)))
|
|
44
|
+
throw new Error("invalid Cursor model selection");
|
|
45
|
+
return {
|
|
46
|
+
id: selection.id,
|
|
47
|
+
...(Array.isArray(selection.params) ? { params: selection.params.map(item => {
|
|
48
|
+
const parameter = asRecord(item);
|
|
49
|
+
if (!parameter || !only(parameter, ["id", "value"]) || !text(parameter.id) || !text(parameter.value))
|
|
50
|
+
throw new Error("invalid Cursor model parameter");
|
|
51
|
+
return { id: parameter.id, value: parameter.value };
|
|
52
|
+
}) } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function cleanModel(value) {
|
|
56
|
+
const model = asRecord(value);
|
|
57
|
+
const cost = asRecord(model?.cost);
|
|
58
|
+
const rateKeys = ["input", "output", "cacheRead", "cacheWrite"];
|
|
59
|
+
const ratesValid = (rates) => rateKeys.every(key => typeof rates[key] === "number" && Number.isFinite(rates[key]) && rates[key] >= 0);
|
|
60
|
+
if (!model || !only(model, ["id", "provider", "cost"]) || !text(model.id) || !text(model.provider) || !cost || !only(cost, [...rateKeys, "tiers"]) || !ratesValid(cost))
|
|
61
|
+
throw new Error("invalid Cursor model pricing");
|
|
62
|
+
if (cost.tiers !== undefined && (!Array.isArray(cost.tiers) || !cost.tiers.every(item => {
|
|
63
|
+
const tier = asRecord(item);
|
|
64
|
+
return tier && only(tier, [...rateKeys, "inputTokensAbove"]) && ratesValid(tier) && Number.isSafeInteger(tier.inputTokensAbove) && tier.inputTokensAbove >= 0;
|
|
65
|
+
})))
|
|
66
|
+
throw new Error("invalid Cursor model pricing tiers");
|
|
67
|
+
return structuredClone(value);
|
|
68
|
+
}
|
|
69
|
+
function validateRecord(value) {
|
|
70
|
+
const record = asRecord(value);
|
|
71
|
+
const fields = {
|
|
72
|
+
start: ["data"], raw: ["reported", "reportedTotalSource", "normalization", "corrected"], run: ["runId", "requestId"],
|
|
73
|
+
terminal: ["status", "waitUsage", "handleUsage", "invalidReportedUsage"], billing: ["observation"], refresh: ["agentId", "sourceOrigin", "observation"],
|
|
74
|
+
};
|
|
75
|
+
if (!record || record.version !== 1 || !text(record.id) || !text(record.turnId) || !text(record.timestamp) || !Number.isFinite(Date.parse(record.timestamp)) ||
|
|
76
|
+
!fields[String(record.kind)] || !only(record, ["version", "id", "turnId", "timestamp", "origin", "kind", ...fields[String(record.kind)]]))
|
|
77
|
+
throw new Error("invalid Cursor usage journal record");
|
|
78
|
+
validateOrigin(record.origin);
|
|
79
|
+
let valid = false;
|
|
80
|
+
switch (record.kind) {
|
|
81
|
+
case "start": {
|
|
82
|
+
const data = asRecord(record.data);
|
|
83
|
+
valid = !!data && only(data, ["agentId", "runtime", "model", "modelSelection", "purpose", "storeIdentity", "resumed", "newlyCreated"]) && text(data.agentId) &&
|
|
84
|
+
["local", "cloud"].includes(String(data.runtime)) && ["normal", "compaction", "tree"].includes(String(data.purpose)) &&
|
|
85
|
+
typeof data.resumed === "boolean" && typeof data.newlyCreated === "boolean" && !(data.resumed && data.newlyCreated) && (data.storeIdentity === undefined || text(data.storeIdentity));
|
|
86
|
+
if (valid) {
|
|
87
|
+
cleanModel(data.model);
|
|
88
|
+
cleanSelection(data.modelSelection);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "raw":
|
|
93
|
+
valid = !!readCursorUsageTokens(record.reported) && ["sdk", "derived-additive"].includes(String(record.reportedTotalSource)) &&
|
|
94
|
+
["local-full-prompt", "invalid-local-partition", "cloud-unqualified"].includes(String(record.normalization)) &&
|
|
95
|
+
(record.normalization === "local-full-prompt" ? !!readCursorUsageTokens(record.corrected) : record.corrected === undefined);
|
|
96
|
+
break;
|
|
97
|
+
case "run":
|
|
98
|
+
valid = (record.runId === undefined || text(record.runId)) && (record.requestId === undefined || text(record.requestId));
|
|
99
|
+
break;
|
|
100
|
+
case "terminal":
|
|
101
|
+
valid = (record.invalidReportedUsage === undefined || typeof record.invalidReportedUsage === "boolean") && ["success", "error", "abort", "abandon"].includes(String(record.status)) &&
|
|
102
|
+
(record.waitUsage === undefined || !!readCursorUsageTokens(record.waitUsage)) && (record.handleUsage === undefined || !!readCursorUsageTokens(record.handleUsage));
|
|
103
|
+
break;
|
|
104
|
+
case "refresh":
|
|
105
|
+
if (!text(record.agentId))
|
|
106
|
+
throw new Error("invalid Cursor usage refresh agent");
|
|
107
|
+
validateOrigin(record.sourceOrigin);
|
|
108
|
+
case "billing": {
|
|
109
|
+
const observation = asRecord(record.observation);
|
|
110
|
+
if (!observation)
|
|
111
|
+
break;
|
|
112
|
+
if (observation.status === "unavailable") {
|
|
113
|
+
valid = only(observation, ["status", "reason"]) && ["timeout", "unsupported", "request-failed", "invalid-response"].includes(String(observation.reason));
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
const snapshot = readCursorBilledSnapshot({ usage: observation.usage, cost: observation.cost, runs: observation.upsertRuns });
|
|
117
|
+
const deleted = observation.deletedRunIds;
|
|
118
|
+
valid = observation.status === "observed-pending-settlement" && only(observation, ["status", "usage", "cost", "upsertRuns", "deletedRunIds"]) &&
|
|
119
|
+
!!snapshot && Array.isArray(deleted) && deleted.every(id => text(id) && id.length <= 256) &&
|
|
120
|
+
new Set(deleted).size === deleted.length && !snapshot.runs.some(row => deleted.includes(row.runId));
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!valid)
|
|
125
|
+
throw new Error("invalid Cursor usage journal payload");
|
|
126
|
+
const clean = structuredClone(value);
|
|
127
|
+
if ((clean.kind === "billing" || clean.kind === "refresh") && clean.observation.status === "observed-pending-settlement") {
|
|
128
|
+
const snapshot = readCursorBilledSnapshot({ usage: clean.observation.usage, cost: clean.observation.cost, runs: clean.observation.upsertRuns });
|
|
129
|
+
clean.observation = { ...clean.observation, usage: snapshot.usage, cost: snapshot.cost, upsertRuns: snapshot.runs };
|
|
130
|
+
}
|
|
131
|
+
if (clean.kind === "raw") {
|
|
132
|
+
clean.reported = readCursorUsageTokens(clean.reported);
|
|
133
|
+
clean.corrected = readCursorUsageTokens(clean.corrected);
|
|
134
|
+
}
|
|
135
|
+
if (clean.kind === "terminal") {
|
|
136
|
+
clean.waitUsage = readCursorUsageTokens(clean.waitUsage);
|
|
137
|
+
clean.handleUsage = readCursorUsageTokens(clean.handleUsage);
|
|
138
|
+
}
|
|
139
|
+
return clean;
|
|
140
|
+
}
|
|
141
|
+
function applyBillingRevision(revision, previous) {
|
|
142
|
+
if (revision.status === "unavailable")
|
|
143
|
+
return revision;
|
|
144
|
+
const rows = new Map(previous?.runs.map(row => [row.runId, row]));
|
|
145
|
+
for (const id of revision.deletedRunIds)
|
|
146
|
+
rows.delete(id);
|
|
147
|
+
for (const row of revision.upsertRuns)
|
|
148
|
+
rows.set(row.runId, row);
|
|
149
|
+
const snapshot = readCursorBilledSnapshot({ usage: revision.usage, cost: revision.cost, runs: [...rows.values()] });
|
|
150
|
+
if (!snapshot)
|
|
151
|
+
throw new Error("invalid Cursor billing revision history");
|
|
152
|
+
return { status: "observed-pending-settlement", snapshot };
|
|
153
|
+
}
|
|
154
|
+
function latestAgentSnapshot(records, agentId) {
|
|
155
|
+
const turns = new Map();
|
|
156
|
+
let snapshot;
|
|
157
|
+
for (const record of records) {
|
|
158
|
+
if (record.kind === "start")
|
|
159
|
+
turns.set(record.turnId, record.data.agentId);
|
|
160
|
+
if (record.kind === "refresh")
|
|
161
|
+
turns.set(record.turnId, record.agentId);
|
|
162
|
+
if ((record.kind === "billing" || record.kind === "refresh") && turns.get(record.turnId) === agentId) {
|
|
163
|
+
const observation = applyBillingRevision(record.observation, snapshot);
|
|
164
|
+
if (observation.status === "observed-pending-settlement")
|
|
165
|
+
snapshot = observation.snapshot;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return snapshot;
|
|
169
|
+
}
|
|
170
|
+
function billingRevision(observation, previous) {
|
|
171
|
+
if (observation.status === "unavailable")
|
|
172
|
+
return observation;
|
|
173
|
+
const { usage, cost, runs } = observation.snapshot;
|
|
174
|
+
const oldRows = new Map(previous?.runs.map(row => [row.runId, row]));
|
|
175
|
+
const newIds = new Set(runs.map(row => row.runId));
|
|
176
|
+
return {
|
|
177
|
+
status: observation.status, usage, cost,
|
|
178
|
+
upsertRuns: runs.filter(row => JSON.stringify(oldRows.get(row.runId)) !== JSON.stringify(row)),
|
|
179
|
+
deletedRunIds: [...oldRows.keys()].filter(id => !newIds.has(id)),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function validateHistory(values, origin) {
|
|
183
|
+
const records = values.map(validateRecord);
|
|
184
|
+
const ids = new Set();
|
|
185
|
+
const starts = new Map();
|
|
186
|
+
for (const record of records) {
|
|
187
|
+
if (record.origin.sessionId !== origin.sessionId || ids.has(record.id))
|
|
188
|
+
throw new Error("Cursor usage journal origin or identity mismatch");
|
|
189
|
+
ids.add(record.id);
|
|
190
|
+
if (record.kind === "start" || record.kind === "refresh") {
|
|
191
|
+
if (starts.has(record.turnId))
|
|
192
|
+
throw new Error("duplicate Cursor usage turn");
|
|
193
|
+
starts.set(record.turnId, record);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const start = starts.get(record.turnId);
|
|
197
|
+
if (!start || JSON.stringify(start.origin) !== JSON.stringify(record.origin))
|
|
198
|
+
throw new Error("Cursor usage lineage mismatch");
|
|
199
|
+
if (record.kind === "raw" && record.corrected) {
|
|
200
|
+
const expected = record.reported.inputTokens - record.reported.cacheReadTokens - record.reported.cacheWriteTokens;
|
|
201
|
+
if (start.kind !== "start" || start.data.runtime !== "local" || record.corrected.inputTokens !== expected ||
|
|
202
|
+
record.corrected.totalTokens !== expected + record.reported.cacheReadTokens + record.reported.cacheWriteTokens + record.reported.outputTokens ||
|
|
203
|
+
["outputTokens", "cacheReadTokens", "cacheWriteTokens"].some(key => record.corrected[key] !== record.reported[key]))
|
|
204
|
+
throw new Error("Cursor usage raw partition mismatch");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const snapshots = new Map();
|
|
209
|
+
for (const record of records)
|
|
210
|
+
if (record.kind === "billing" || record.kind === "refresh") {
|
|
211
|
+
const start = starts.get(record.turnId);
|
|
212
|
+
const agentId = start.kind === "start" ? start.data.agentId : start.agentId;
|
|
213
|
+
const observation = applyBillingRevision(record.observation, snapshots.get(agentId));
|
|
214
|
+
if (observation.status === "observed-pending-settlement")
|
|
215
|
+
snapshots.set(agentId, observation.snapshot);
|
|
216
|
+
}
|
|
217
|
+
return records;
|
|
218
|
+
}
|
|
219
|
+
function readOrigin(pi, origin) {
|
|
220
|
+
const path = journalPath(origin);
|
|
221
|
+
return validateHistory(path ? readDurableFramePrefix(path).frames : ephemeral.get(pi)?.get(origin.sessionId) ?? [], origin);
|
|
222
|
+
}
|
|
223
|
+
function persist(pi, record) {
|
|
224
|
+
const path = journalPath(record.origin);
|
|
225
|
+
if (path) {
|
|
226
|
+
// ponytail: bounded 16MiB journals are revalidated per append; migrate to indexed framed storage when this explicit ceiling is reached.
|
|
227
|
+
appendDurableFrame(path, record, values => { validateHistory(values, record.origin); });
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
let sessions = ephemeral.get(pi);
|
|
231
|
+
if (!sessions) {
|
|
232
|
+
sessions = new Map();
|
|
233
|
+
ephemeral.set(pi, sessions);
|
|
234
|
+
}
|
|
235
|
+
const records = sessions.get(record.origin.sessionId) ?? [];
|
|
236
|
+
validateHistory([...records, record], record.origin);
|
|
237
|
+
records.push(structuredClone(record));
|
|
238
|
+
sessions.set(record.origin.sessionId, records);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function currentMatches(pi, origin) {
|
|
242
|
+
const ctx = bindings.get(pi);
|
|
243
|
+
if (!ctx)
|
|
244
|
+
return false;
|
|
245
|
+
const current = originOf(ctx);
|
|
246
|
+
return ownerKey(current) === ownerKey(origin) && (origin.anchorId ? ctx.sessionManager.getBranch().some(entry => entry.id === origin.anchorId) : current.anchorId === null);
|
|
247
|
+
}
|
|
248
|
+
function claimOrigin(pi, captured, turnId) {
|
|
249
|
+
if (!currentMatches(pi, captured))
|
|
250
|
+
throw new Error("Cursor usage origin changed before operation");
|
|
251
|
+
pi.appendEntry(CLAIM_TYPE, { version: 1, turnId, origin: captured });
|
|
252
|
+
const current = bindings.get(pi);
|
|
253
|
+
const entry = current.sessionManager.getLeafEntry();
|
|
254
|
+
if (entry?.type !== "custom" || entry.customType !== CLAIM_TYPE || asRecord(entry.data)?.turnId !== turnId)
|
|
255
|
+
throw new Error("Cursor usage origin claim was not appended");
|
|
256
|
+
const origin = originOf(current);
|
|
257
|
+
if (ownerKey(origin) !== ownerKey(captured))
|
|
258
|
+
throw new Error("Cursor usage origin changed during claim");
|
|
259
|
+
if (origin.sessionFile && existsSync(origin.sessionFile) && !fsyncExistingRegularFile(origin.sessionFile))
|
|
260
|
+
throw new Error("Cursor usage origin claim was not durable");
|
|
261
|
+
return origin;
|
|
262
|
+
}
|
|
263
|
+
export function captureCursorUsageRecorder(pi, ctx) {
|
|
264
|
+
const captured = originOf(ctx);
|
|
265
|
+
bindings.set(pi, ctx);
|
|
266
|
+
const notePersistenceFailure = (_error) => {
|
|
267
|
+
let owners = failures.get(pi);
|
|
268
|
+
if (!owners) {
|
|
269
|
+
owners = new Set();
|
|
270
|
+
failures.set(pi, owners);
|
|
271
|
+
}
|
|
272
|
+
const key = ownerKey(captured);
|
|
273
|
+
if (owners.has(key))
|
|
274
|
+
return;
|
|
275
|
+
owners.add(key);
|
|
276
|
+
if (currentMatches(pi, captured))
|
|
277
|
+
bindings.get(pi)?.ui.notify("Cursor usage persistence failed; accounting is incomplete. New sends require a recorded origin. See /cursor-usage help for recovery.", "warning");
|
|
278
|
+
};
|
|
279
|
+
return { notePersistenceFailure, async start(input) {
|
|
280
|
+
const turnId = randomUUID();
|
|
281
|
+
const data = {
|
|
282
|
+
agentId: input.agent.agentId, runtime: input.runtime,
|
|
283
|
+
model: cleanModel({ id: input.model.id, provider: input.model.provider, cost: input.model.cost }),
|
|
284
|
+
modelSelection: cleanSelection(input.modelSelection), purpose: input.purpose,
|
|
285
|
+
storeIdentity: input.storeIdentity, resumed: input.resumed, newlyCreated: input.newlyCreated,
|
|
286
|
+
};
|
|
287
|
+
if (input.resumed && input.newlyCreated)
|
|
288
|
+
throw new Error("Cursor usage lineage cannot be both resumed and newly created");
|
|
289
|
+
// Unique public branch claim before spend; ordinary optional mirrors cannot establish ownership.
|
|
290
|
+
const origin = claimOrigin(pi, captured, turnId);
|
|
291
|
+
const write = (event) => {
|
|
292
|
+
const record = { ...event, version: 1, id: randomUUID(), turnId, timestamp: new Date().toISOString(), origin: structuredClone(origin) };
|
|
293
|
+
persist(pi, record);
|
|
294
|
+
if (currentMatches(pi, origin)) {
|
|
295
|
+
try {
|
|
296
|
+
pi.appendEntry(CUSTOM_TYPE, record);
|
|
297
|
+
}
|
|
298
|
+
catch { /* Fsynced journal and unique native claim are authoritative. */ }
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
write({ kind: "start", data });
|
|
302
|
+
const recordSnapshot = async () => {
|
|
303
|
+
const observation = await fetchCursorSdkAgentUsage(input.agent);
|
|
304
|
+
write({ kind: "billing", observation: billingRevision(observation, latestAgentSnapshot(readOrigin(pi, origin), input.agent.agentId)) });
|
|
305
|
+
return observation;
|
|
306
|
+
};
|
|
307
|
+
return {
|
|
308
|
+
observeRawTurn(value) {
|
|
309
|
+
const raw = asRecord(value);
|
|
310
|
+
if (!raw)
|
|
311
|
+
return;
|
|
312
|
+
const reported = readCursorUsageTokens({ ...raw, totalTokens: raw.totalTokens ?? Number(raw.inputTokens) + Number(raw.outputTokens) + Number(raw.cacheReadTokens) + Number(raw.cacheWriteTokens) });
|
|
313
|
+
if (!reported)
|
|
314
|
+
throw new Error("invalid Cursor raw numeric usage");
|
|
315
|
+
const reportedTotalSource = raw.totalTokens === undefined ? "derived-additive" : "sdk";
|
|
316
|
+
if (input.runtime === "cloud") {
|
|
317
|
+
write({ kind: "raw", reported, reportedTotalSource, normalization: "cloud-unqualified" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const inputTokens = reported.inputTokens - reported.cacheReadTokens - reported.cacheWriteTokens;
|
|
321
|
+
const corrected = readCursorUsageTokens({ ...reported, inputTokens, totalTokens: inputTokens + reported.cacheReadTokens + reported.cacheWriteTokens + reported.outputTokens });
|
|
322
|
+
write({ kind: "raw", reported, reportedTotalSource, normalization: corrected ? "local-full-prompt" : "invalid-local-partition", ...(corrected ? { corrected } : {}) });
|
|
323
|
+
},
|
|
324
|
+
async recordRun(ids) { write({ kind: "run", ...ids }); },
|
|
325
|
+
async recordTerminal(result) {
|
|
326
|
+
const waitUsage = readCursorUsageTokens(result.waitUsage);
|
|
327
|
+
const handleUsage = readCursorUsageTokens(result.handleUsage);
|
|
328
|
+
write({ kind: "terminal", status: result.status, waitUsage, handleUsage,
|
|
329
|
+
...((result.waitUsage !== undefined && !waitUsage) || (result.handleUsage !== undefined && !handleUsage) ? { invalidReportedUsage: true } : {}) });
|
|
330
|
+
return result.status === "abandon" ? undefined : recordSnapshot();
|
|
331
|
+
},
|
|
332
|
+
recordSnapshot, notePersistenceFailure,
|
|
333
|
+
};
|
|
334
|
+
} };
|
|
335
|
+
}
|
|
336
|
+
/** Append chronology owns revisions. Wall clocks are display metadata, never reconciliation authority. */
|
|
337
|
+
export function readCursorUsageView(pi, ctx) {
|
|
338
|
+
const origin = originOf(ctx);
|
|
339
|
+
const branch = ctx.sessionManager.getBranch();
|
|
340
|
+
const mirrors = branch.flatMap(entry => entry.type === "custom" && entry.customType === CUSTOM_TYPE ? [validateRecord(entry.data)] : []);
|
|
341
|
+
const readClaims = (entries) => entries.flatMap(entry => {
|
|
342
|
+
if (entry.type !== "custom" || entry.customType !== CLAIM_TYPE)
|
|
343
|
+
return [];
|
|
344
|
+
const data = asRecord(entry.data);
|
|
345
|
+
if (!data || !only(data, ["version", "turnId", "origin"]) || data.version !== 1 || !text(data.turnId))
|
|
346
|
+
throw new Error("invalid Cursor usage origin claim");
|
|
347
|
+
return [{ entryId: entry.id, turnId: data.turnId, origin: validateOrigin(data.origin) }];
|
|
348
|
+
});
|
|
349
|
+
const claims = readClaims(branch);
|
|
350
|
+
const sessionClaims = readClaims(ctx.sessionManager.getEntries());
|
|
351
|
+
const origins = new Map();
|
|
352
|
+
for (const inherited of [...claims.map(claim => claim.origin), ...mirrors.map(record => record.origin), origin])
|
|
353
|
+
origins.set(storageKey(inherited), inherited);
|
|
354
|
+
const groups = new Map();
|
|
355
|
+
const incompleteGroups = new Set();
|
|
356
|
+
const lineageGaps = [];
|
|
357
|
+
const incompleteJournals = [];
|
|
358
|
+
for (const [key, owner] of origins) {
|
|
359
|
+
let records;
|
|
360
|
+
try {
|
|
361
|
+
const path = journalPath(owner);
|
|
362
|
+
const prefix = path ? readDurableFramePrefix(path) : undefined;
|
|
363
|
+
records = prefix ? validateHistory(prefix.frames, owner) : readOrigin(pi, owner);
|
|
364
|
+
if (prefix?.incompleteTail)
|
|
365
|
+
incompleteJournals.push(owner.sessionId);
|
|
366
|
+
if (!records.length && (mirrors.some(record => storageKey(record.origin) === key) || claims.some(claim => storageKey(claim.origin) === key))) {
|
|
367
|
+
records = mirrors.filter(record => storageKey(record.origin) === key);
|
|
368
|
+
incompleteGroups.add(key);
|
|
369
|
+
lineageGaps.push(`Origin ${owner.sessionId}: journal missing or without recognized frames; only native branch claim/mirror facts remain, accounting is incomplete.`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
if (key === storageKey(origin))
|
|
374
|
+
throw new Error("current Cursor usage journal unavailable or invalid");
|
|
375
|
+
// The inherited branch is the authority for these mirror facts, not a directory guess or sibling scan.
|
|
376
|
+
records = mirrors.filter(record => storageKey(record.origin) === key);
|
|
377
|
+
incompleteGroups.add(key);
|
|
378
|
+
lineageGaps.push(`Inherited origin ${owner.sessionId}: journal unavailable; preserved branch mirrors may be incomplete.`);
|
|
379
|
+
}
|
|
380
|
+
groups.set(key, records);
|
|
381
|
+
}
|
|
382
|
+
const all = [...groups.values()].flat();
|
|
383
|
+
const claimedTurns = (items) => new Set(items.filter(claim => all.some(record => (record.kind === "start" || record.kind === "refresh") && record.turnId === claim.turnId && record.origin.anchorId === claim.entryId && ownerKey(record.origin) === ownerKey(claim.origin))).map(claim => claim.turnId));
|
|
384
|
+
const selected = claimedTurns(claims);
|
|
385
|
+
const knownClaims = claimedTurns(sessionClaims);
|
|
386
|
+
const records = all.filter(record => selected.has(record.turnId));
|
|
387
|
+
const outsideBranch = all.filter(record => !selected.has(record.turnId));
|
|
388
|
+
const otherBranchRecords = outsideBranch.filter(record => knownClaims.has(record.turnId));
|
|
389
|
+
const unclaimedRecords = outsideBranch.filter(record => !knownClaims.has(record.turnId) && storageKey(record.origin) === storageKey(origin));
|
|
390
|
+
const inheritedUnclaimedRecords = outsideBranch.filter(record => !knownClaims.has(record.turnId) && storageKey(record.origin) !== storageKey(origin));
|
|
391
|
+
const agents = new Map();
|
|
392
|
+
const turnAgents = new Map();
|
|
393
|
+
const correctedRawTotal = zero();
|
|
394
|
+
for (const record of all) {
|
|
395
|
+
if (record.kind === "start")
|
|
396
|
+
turnAgents.set(record.turnId, record.data.agentId);
|
|
397
|
+
if (record.kind === "refresh")
|
|
398
|
+
turnAgents.set(record.turnId, record.agentId);
|
|
399
|
+
}
|
|
400
|
+
for (const record of records) {
|
|
401
|
+
if (record.kind === "raw" && record.corrected)
|
|
402
|
+
for (const key of tokenKeys) {
|
|
403
|
+
correctedRawTotal[key] += record.corrected[key];
|
|
404
|
+
if (!Number.isSafeInteger(correctedRawTotal[key]))
|
|
405
|
+
throw new Error("Cursor usage total exceeds safe integer range");
|
|
406
|
+
}
|
|
407
|
+
const agentId = record.kind === "start" ? record.data.agentId : record.kind === "refresh" ? record.agentId : undefined;
|
|
408
|
+
if (agentId && !agents.has(agentId))
|
|
409
|
+
agents.set(agentId, {
|
|
410
|
+
agentId, unknownHistory: true, sharedAcrossBranches: false, billingStatus: "pending", observationsByOrigin: [],
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
for (const agent of agents.values()) {
|
|
414
|
+
const agentRecords = all.filter(record => turnAgents.get(record.turnId) === agent.agentId);
|
|
415
|
+
agent.sharedAcrossBranches = agentRecords.some(record => !selected.has(record.turnId)) || new Set(agentRecords.map(record => ownerKey(record.origin))).size > 1;
|
|
416
|
+
agent.unknownHistory = !agentRecords.some(record => record.kind === "start" && record.data.newlyCreated);
|
|
417
|
+
for (const [key, group] of groups) {
|
|
418
|
+
let observation;
|
|
419
|
+
const incompleteHistory = incompleteGroups.has(key);
|
|
420
|
+
const rows = new Map();
|
|
421
|
+
for (const record of group)
|
|
422
|
+
if ((record.kind === "billing" || record.kind === "refresh") && turnAgents.get(record.turnId) === agent.agentId) {
|
|
423
|
+
const revision = record.observation;
|
|
424
|
+
let wholeAgent = observation?.wholeAgent;
|
|
425
|
+
let latestAggregate = observation?.latestAggregate;
|
|
426
|
+
if (revision.status === "observed-pending-settlement") {
|
|
427
|
+
latestAggregate = { usage: revision.usage, cost: revision.cost };
|
|
428
|
+
if (!incompleteHistory) {
|
|
429
|
+
for (const id of revision.deletedRunIds)
|
|
430
|
+
rows.delete(id);
|
|
431
|
+
for (const row of revision.upsertRuns)
|
|
432
|
+
rows.set(row.runId, row);
|
|
433
|
+
wholeAgent = readCursorBilledSnapshot({ ...latestAggregate, runs: [...rows.values()] });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
observation = { origin: record.origin, lastAttempt: revision, latestAggregate, wholeAgent, incompleteHistory };
|
|
437
|
+
}
|
|
438
|
+
if (observation)
|
|
439
|
+
agent.observationsByOrigin.push(observation);
|
|
440
|
+
}
|
|
441
|
+
if (agent.observationsByOrigin.length > 1) {
|
|
442
|
+
agent.billingStatus = "ambiguous-shared-lineage";
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const observation = agent.observationsByOrigin[0];
|
|
446
|
+
if (!observation)
|
|
447
|
+
continue;
|
|
448
|
+
agent.lastAttempt = observation.lastAttempt;
|
|
449
|
+
agent.latestAggregate = observation.latestAggregate;
|
|
450
|
+
agent.wholeAgent = observation.wholeAgent;
|
|
451
|
+
agent.billingStatus = observation.lastAttempt.status;
|
|
452
|
+
if (!agent.wholeAgent || observation.incompleteHistory)
|
|
453
|
+
continue;
|
|
454
|
+
const remainder = { ...agent.wholeAgent.usage };
|
|
455
|
+
for (const row of agent.wholeAgent.runs)
|
|
456
|
+
for (const key of tokenKeys)
|
|
457
|
+
remainder[key] -= row.usage[key];
|
|
458
|
+
agent.aggregateOnly = remainder;
|
|
459
|
+
agent.costReconciliation = "not-reported";
|
|
460
|
+
if (agent.wholeAgent.cost && agent.wholeAgent.runs.every(row => row.cost)) {
|
|
461
|
+
const cost = {
|
|
462
|
+
rawCostCents: agent.wholeAgent.cost.rawCostCents - agent.wholeAgent.runs.reduce((sum, row) => sum + row.cost.rawCostCents, 0),
|
|
463
|
+
chargedCents: agent.wholeAgent.cost.chargedCents - agent.wholeAgent.runs.reduce((sum, row) => sum + row.cost.chargedCents, 0),
|
|
464
|
+
};
|
|
465
|
+
if (cost.rawCostCents >= 0 && cost.chargedCents >= 0) {
|
|
466
|
+
agent.aggregateOnlyCost = cost;
|
|
467
|
+
agent.costReconciliation = "observed";
|
|
468
|
+
}
|
|
469
|
+
else
|
|
470
|
+
agent.costReconciliation = "inconsistent";
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
version: 1, durability: origin.sessionFile ? "durable" : "ephemeral", origin,
|
|
475
|
+
persistenceStatus: failures.get(pi)?.has(ownerKey(origin)) || lineageGaps.length || incompleteJournals.length ? "incomplete" : "ok",
|
|
476
|
+
lineageGaps, incompleteJournals, journalLimit: JOURNAL_LIMIT, records, otherBranchRecords, unclaimedRecords, inheritedUnclaimedRecords, agents: [...agents.values()], correctedRawTotal,
|
|
477
|
+
nativeConfiguredEstimates: "Native Pi /stats retains configured-price estimates for emitted messages, not full Cursor invoice totals. Raw telemetry and whole-agent billed snapshots are separate overlapping sources; do not sum them.",
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
export async function refreshCursorUsageView(pi, ctx, fetchAgent) {
|
|
481
|
+
const view = readCursorUsageView(pi, ctx);
|
|
482
|
+
bindings.set(pi, ctx);
|
|
483
|
+
for (const agent of view.agents.slice(0, 32)) {
|
|
484
|
+
const source = view.records.find(record => (record.kind === "start" && record.data.agentId === agent.agentId) || (record.kind === "refresh" && record.agentId === agent.agentId));
|
|
485
|
+
if (!source)
|
|
486
|
+
continue;
|
|
487
|
+
const turnId = randomUUID();
|
|
488
|
+
const origin = claimOrigin(pi, view.origin, turnId);
|
|
489
|
+
// Refresh belongs to the invoking branch, never to an inherited or switched sibling journal.
|
|
490
|
+
const record = {
|
|
491
|
+
version: 1, id: randomUUID(), turnId, timestamp: new Date().toISOString(), origin,
|
|
492
|
+
kind: "refresh", agentId: agent.agentId, sourceOrigin: source.origin, observation: billingRevision(await fetchAgent(agent.agentId), latestAgentSnapshot(readOrigin(pi, origin), agent.agentId)),
|
|
493
|
+
};
|
|
494
|
+
persist(pi, record);
|
|
495
|
+
if (currentMatches(pi, origin)) {
|
|
496
|
+
try {
|
|
497
|
+
pi.appendEntry(CUSTOM_TYPE, record);
|
|
498
|
+
}
|
|
499
|
+
catch { /* Unique claim retains ownership. */ }
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return readCursorUsageView(pi, ctx);
|
|
503
|
+
}
|
|
504
|
+
export function registerCursorUsageLedger(pi) {
|
|
505
|
+
const capture = (_event, ctx) => { bindings.set(pi, ctx); };
|
|
506
|
+
pi.on("session_start", capture);
|
|
507
|
+
pi.on("session_tree", capture);
|
|
508
|
+
pi.on("session_compact", capture);
|
|
509
|
+
pi.on("before_agent_start", capture);
|
|
510
|
+
pi.on("session_info_changed", capture);
|
|
511
|
+
pi.on("session_shutdown", () => { bindings.delete(pi); });
|
|
512
|
+
}
|