@botbuddy/cli 1.59.6 → 1.59.7
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/package.json +1 -1
- package/src/usage/codex.mjs +55 -12
- package/src/usage/sync.mjs +26 -2
package/package.json
CHANGED
package/src/usage/codex.mjs
CHANGED
|
@@ -62,20 +62,57 @@ function deltaUsage(total, previous) {
|
|
|
62
62
|
|
|
63
63
|
// Only derived values are kept in ctx (it is persisted in the cursor file):
|
|
64
64
|
// the cwd's HMAC and the ticket key its last segment names — never the path.
|
|
65
|
-
|
|
65
|
+
//
|
|
66
|
+
// BOT-1988: the branch is the checkout's, read at SYNC time by `branchFor(cwd)`
|
|
67
|
+
// (the rollout records git metadata only in session_meta, and 84 of 217 local
|
|
68
|
+
// rollouts had none). It is an estimate — the checkout may have switched branch
|
|
69
|
+
// since the turn ran — so a row carrying a derived or carried branch is stamped
|
|
70
|
+
// attribution_confidence 'inferred'. A cwd change never nulls a known branch
|
|
71
|
+
// unless the new cwd names a DIFFERENT ticket (another worktree): the resolver
|
|
72
|
+
// returns null for a removed checkout, and a subdirectory of the same checkout
|
|
73
|
+
// keeps its branch.
|
|
74
|
+
const usableBranch = (value) => {
|
|
75
|
+
const label = safeLabel(value);
|
|
76
|
+
return label && label !== "HEAD" ? label : null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function setCwd(ctx, cwd, hmac, tenantFor, repoFor = () => null, branchFor = () => null) {
|
|
66
80
|
const value = str(cwd);
|
|
67
81
|
if (!value) return;
|
|
68
82
|
const cwdHmac = hmac(value);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
const changed = Boolean(ctx.cwd_hmac && ctx.cwd_hmac !== cwdHmac);
|
|
84
|
+
const cwdTicket = inferTicketKey(null, value);
|
|
85
|
+
if (changed) {
|
|
86
|
+
// A later turn in ANOTHER directory: the session_meta git metadata described
|
|
87
|
+
// the old one. Re-derive the repo; the commit is unknown.
|
|
74
88
|
ctx.commit_sha = null;
|
|
75
89
|
ctx.repo = repoFor(value) ?? null;
|
|
90
|
+
const derived = usableBranch(branchFor(value));
|
|
91
|
+
if (derived) {
|
|
92
|
+
ctx.git_branch = derived;
|
|
93
|
+
ctx.branch_estimated = true;
|
|
94
|
+
} else if (ctx.git_branch) {
|
|
95
|
+
const carriedTicket = inferTicketKey(ctx.git_branch, null);
|
|
96
|
+
if (cwdTicket && carriedTicket && cwdTicket !== carriedTicket) {
|
|
97
|
+
ctx.git_branch = null;
|
|
98
|
+
ctx.branch_estimated = false;
|
|
99
|
+
} else {
|
|
100
|
+
ctx.branch_estimated = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else if (!ctx.git_branch || ctx.branch_estimated) {
|
|
104
|
+
// First cwd of the thread, a cursor context an older CLI persisted with no
|
|
105
|
+
// branch, or an estimate from an earlier sync (the checkout may have switched
|
|
106
|
+
// branch since): resolve it with THIS sync's resolver, which caches per cwd.
|
|
107
|
+
// The rollout's own git branch (branch_estimated false) is never replaced.
|
|
108
|
+
const derived = usableBranch(branchFor(value));
|
|
109
|
+
if (derived) {
|
|
110
|
+
ctx.git_branch = derived;
|
|
111
|
+
ctx.branch_estimated = true;
|
|
112
|
+
}
|
|
76
113
|
}
|
|
77
114
|
ctx.cwd_hmac = cwdHmac;
|
|
78
|
-
ctx.cwd_ticket =
|
|
115
|
+
ctx.cwd_ticket = cwdTicket;
|
|
79
116
|
ctx.cwd_tenant = tenantFor(value); // a tenant slug (or null), never the path
|
|
80
117
|
}
|
|
81
118
|
|
|
@@ -86,7 +123,7 @@ const NONE = Object.freeze({ envelopes: [], refused: 0 });
|
|
|
86
123
|
* @param {(value: string) => string} opts.hmac
|
|
87
124
|
* @param {string} opts.environment
|
|
88
125
|
*/
|
|
89
|
-
export function createCodexAdapter({ hmac, environment, tenantFor = () => null, repoFor = () => null }) {
|
|
126
|
+
export function createCodexAdapter({ hmac, environment, tenantFor = () => null, repoFor = () => null, branchFor = () => null }) {
|
|
90
127
|
function parseCodexRecord(record, { offset, sourceHmac, ctx }) {
|
|
91
128
|
if (!record || typeof record !== "object") return NONE;
|
|
92
129
|
const payload = record.payload && typeof record.payload === "object" ? record.payload : null;
|
|
@@ -101,16 +138,22 @@ export function createCodexAdapter({ hmac, environment, tenantFor = () => null,
|
|
|
101
138
|
ctx.subagent_thread = thread && root && thread !== root ? thread
|
|
102
139
|
: payload.thread_source === "subagent" && thread ? thread : null;
|
|
103
140
|
if (safeLabel(payload.cli_version)) ctx.cli_version = safeLabel(payload.cli_version);
|
|
104
|
-
setCwd(ctx, payload.cwd, hmac, tenantFor, repoFor);
|
|
105
141
|
const git = payload.git && typeof payload.git === "object" ? payload.git : {};
|
|
106
|
-
|
|
142
|
+
const metaBranch = safeLabel(git.branch);
|
|
143
|
+
setCwd(ctx, payload.cwd, hmac, tenantFor, repoFor, branchFor);
|
|
144
|
+
// The rollout's own git block wins over the sync-time resolver.
|
|
145
|
+
if (metaBranch) {
|
|
146
|
+
ctx.git_branch = metaBranch;
|
|
147
|
+
ctx.branch_estimated = false;
|
|
148
|
+
}
|
|
149
|
+
ctx.git_branch = ctx.git_branch ?? null;
|
|
107
150
|
ctx.commit_sha = commitSha(git.commit_hash) ?? ctx.commit_sha ?? null;
|
|
108
151
|
ctx.repo = normalizeRepo(git.repository_url) ?? ctx.repo ?? null;
|
|
109
152
|
return NONE;
|
|
110
153
|
}
|
|
111
154
|
if (record.type === "turn_context") {
|
|
112
155
|
if (modelLabel(payload.model)) ctx.model = modelLabel(payload.model);
|
|
113
|
-
setCwd(ctx, payload.cwd, hmac, tenantFor, repoFor);
|
|
156
|
+
setCwd(ctx, payload.cwd, hmac, tenantFor, repoFor, branchFor);
|
|
114
157
|
return NONE;
|
|
115
158
|
}
|
|
116
159
|
if (record.type === "response_item") {
|
|
@@ -206,7 +249,7 @@ export function createCodexAdapter({ hmac, environment, tenantFor = () => null,
|
|
|
206
249
|
repo: ctx.repo ?? null,
|
|
207
250
|
commit_sha: ctx.commit_sha ?? null,
|
|
208
251
|
ticket_key_inferred: ticket,
|
|
209
|
-
attribution_confidence: ticket ? "inferred" : null,
|
|
252
|
+
attribution_confidence: ticket || (ctx.git_branch && ctx.branch_estimated) ? "inferred" : null,
|
|
210
253
|
environment,
|
|
211
254
|
cwd_hmac: ctx.cwd_hmac ?? null,
|
|
212
255
|
source_hmac: sourceHmac,
|
package/src/usage/sync.mjs
CHANGED
|
@@ -164,6 +164,30 @@ export function createRepoResolver({ exec = execFileSync, env = process.env } =
|
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* BOT-1988: cwd → the branch its checkout has checked out NOW (sync time), or
|
|
169
|
+
* null for a detached HEAD, a removed checkout or a non-repo directory. Codex
|
|
170
|
+
* rollouts record git metadata only in session_meta, so this is the Codex
|
|
171
|
+
* adapter's estimate for every other turn. Cached per cwd; repository-location
|
|
172
|
+
* variables stripped (a lefthook pre-push run exports the pushing repo's).
|
|
173
|
+
*/
|
|
174
|
+
export function createBranchResolver({ exec = execFileSync, env = process.env } = {}) {
|
|
175
|
+
const gitEnv = { ...env };
|
|
176
|
+
for (const name of GIT_REPOSITORY_ENV) delete gitEnv[name];
|
|
177
|
+
const cache = new Map();
|
|
178
|
+
return (cwd) => {
|
|
179
|
+
if (cache.has(cwd)) return cache.get(cwd);
|
|
180
|
+
let branch = null;
|
|
181
|
+
try {
|
|
182
|
+
branch = String(exec("git", ["-c", "core.fsmonitor=false", "-C", cwd, "symbolic-ref", "--quiet", "--short", "HEAD"], {
|
|
183
|
+
encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"], env: gitEnv,
|
|
184
|
+
})).trim() || null;
|
|
185
|
+
} catch { branch = null; }
|
|
186
|
+
cache.set(cwd, branch);
|
|
187
|
+
return branch;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
167
191
|
/**
|
|
168
192
|
* cwd → the tenant bound to its checkout (BOT-1902 tenant routing), or null
|
|
169
193
|
* when unbound. The binding is the checkout's committed `.botbuddy-agent.json`:
|
|
@@ -346,7 +370,7 @@ export async function syncUsage(opts, deps = {}) {
|
|
|
346
370
|
const {
|
|
347
371
|
env = process.env, home = homedir(), cwd = process.cwd(), now = () => Date.now(),
|
|
348
372
|
fetchImpl = fetch, resolveAuth = defaultResolveAuth, hostname = systemHostname,
|
|
349
|
-
roots = transcriptRoots(env, home), repoFor = createRepoResolver(), cliVersion = "0.0.0",
|
|
373
|
+
roots = transcriptRoots(env, home), repoFor = createRepoResolver(), branchFor = createBranchResolver({ env }), cliVersion = "0.0.0",
|
|
350
374
|
tenantFor = createTenantResolver({ env }), resolveTarget = defaultResolveTarget, readLines = readCompleteLines,
|
|
351
375
|
serverUrl = env.BOTBUDDY_SERVER_URL || DEFAULT_SERVER_URL, statePath = cursorPath(home),
|
|
352
376
|
} = deps;
|
|
@@ -438,7 +462,7 @@ export async function syncUsage(opts, deps = {}) {
|
|
|
438
462
|
receipt.providers.push(provider);
|
|
439
463
|
receipt.files_scanned += files.length;
|
|
440
464
|
const adapter = provider === "codex"
|
|
441
|
-
? createCodexAdapter({ hmac, environment, tenantFor, repoFor })
|
|
465
|
+
? createCodexAdapter({ hmac, environment, tenantFor, repoFor, branchFor })
|
|
442
466
|
: createClaudeAdapter({ hmac, environment, repoFor, tenantFor });
|
|
443
467
|
const run = {
|
|
444
468
|
host: hostname(), trigger: opts.trigger, provider, cli_version: cliVersion,
|