@tablation/crew 0.129.1 → 0.130.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.
Files changed (2) hide show
  1. package/dist/cli.js +161 -59
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1259,6 +1259,14 @@ var claudePair = {
1259
1259
  removePair: (o) => removeClaudeMcp({ bin: o.bin ?? "claude", dryRun: o.dryRun })
1260
1260
  };
1261
1261
  var inferenceBaseUrl = (baseUrl, workspaceId) => `${baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/inference`;
1262
+ function inferenceHeaders(inf) {
1263
+ const clean = (v) => v.replace(/[\r\n]+/g, " ").trim();
1264
+ const lines2 = [
1265
+ ...inf.ticket ? [`X-Crew-Ticket: ${clean(inf.ticket)}`] : [],
1266
+ ...inf.runId ? [`X-Crew-Run: ${clean(inf.runId)}`] : []
1267
+ ];
1268
+ return lines2.length > 0 ? lines2.join("\n") : void 0;
1269
+ }
1262
1270
  function workspaceSettings(baseUrl) {
1263
1271
  const blank = BILLING_VARS_TO_UNSET.filter((v) => v !== "ANTHROPIC_AUTH_TOKEN");
1264
1272
  return JSON.stringify({ env: { ...Object.fromEntries(blank.map((v) => [v, ""])), ANTHROPIC_BASE_URL: baseUrl } });
@@ -1362,7 +1370,11 @@ var claudeAdapter = {
1362
1370
  // variables are already unset at exec (`unsetEnv`), and a spec-level `env:` entry must not
1363
1371
  // smuggle one back in.
1364
1372
  ...spec.auth === "workspace" ? withoutProviderVars(spec.env) : spec.env,
1365
- ...spec.auth === "workspace" && spec.inference ? { ANTHROPIC_BASE_URL: spec.inference.baseUrl, ANTHROPIC_AUTH_TOKEN: spec.inference.token } : {}
1373
+ ...spec.auth === "workspace" && spec.inference ? {
1374
+ ANTHROPIC_BASE_URL: spec.inference.baseUrl,
1375
+ ANTHROPIC_AUTH_TOKEN: spec.inference.token,
1376
+ ...inferenceHeaders(spec.inference) ? { ANTHROPIC_CUSTOM_HEADERS: inferenceHeaders(spec.inference) } : {}
1377
+ } : {}
1366
1378
  };
1367
1379
  },
1368
1380
  provider: () => "anthropic",
@@ -3210,7 +3222,7 @@ function parseRepoConfig(text2, file) {
3210
3222
  if (!MATCHES.includes(vmatch)) {
3211
3223
  throw new RepoConfigError(`${file}: release.verify.match must be commit|version (got "${vmatch}")`);
3212
3224
  }
3213
- const num = (v, key, dflt) => {
3225
+ const num2 = (v, key, dflt) => {
3214
3226
  if (v === void 0 || v === null) return dflt;
3215
3227
  const n = Number(v);
3216
3228
  if (!Number.isFinite(n) || n <= 0) {
@@ -3220,8 +3232,8 @@ function parseRepoConfig(text2, file) {
3220
3232
  };
3221
3233
  const verify = {
3222
3234
  match: vmatch,
3223
- timeoutSeconds: num(raw.release?.verify?.timeoutSeconds, "timeoutSeconds", 600),
3224
- intervalSeconds: num(raw.release?.verify?.intervalSeconds, "intervalSeconds", 15)
3235
+ timeoutSeconds: num2(raw.release?.verify?.timeoutSeconds, "timeoutSeconds", 600),
3236
+ intervalSeconds: num2(raw.release?.verify?.intervalSeconds, "intervalSeconds", 15)
3225
3237
  };
3226
3238
  if (verify.intervalSeconds > verify.timeoutSeconds) {
3227
3239
  throw new RepoConfigError(
@@ -3794,7 +3806,7 @@ function parseReleaseOverride(raw, where, file) {
3794
3806
  if (versioning !== void 0 && !VERSIONINGS.includes(versioning)) {
3795
3807
  throw new ConfigError(`${file}: ${where}.release.versioning must be auto|none (got "${raw.versioning}")`);
3796
3808
  }
3797
- const num = (v, key) => {
3809
+ const num2 = (v, key) => {
3798
3810
  if (v === void 0 || v === null) return void 0;
3799
3811
  const n = Number(v);
3800
3812
  if (!Number.isFinite(n) || n <= 0) {
@@ -3802,8 +3814,8 @@ function parseReleaseOverride(raw, where, file) {
3802
3814
  }
3803
3815
  return n;
3804
3816
  };
3805
- const timeoutSeconds = num(raw.verify?.timeoutSeconds, "timeoutSeconds");
3806
- const intervalSeconds = num(raw.verify?.intervalSeconds, "intervalSeconds");
3817
+ const timeoutSeconds = num2(raw.verify?.timeoutSeconds, "timeoutSeconds");
3818
+ const intervalSeconds = num2(raw.verify?.intervalSeconds, "intervalSeconds");
3807
3819
  const isolateMax = parseIsolateMax(
3808
3820
  raw.isolateFailures?.maxCandidates,
3809
3821
  `${where}.release.isolateFailures`,
@@ -5496,20 +5508,20 @@ function branchForIssue(cwd, issueKey, templates = { name: "issue-{number}" }, r
5496
5508
  return null;
5497
5509
  }
5498
5510
  function branchCandidates(issueKey, templates, render2) {
5499
- const num = issueKey.replace(/^\D+/, "");
5500
- const unpadded = String(Number.parseInt(num, 10));
5511
+ const num2 = issueKey.replace(/^\D+/, "");
5512
+ const unpadded = String(Number.parseInt(num2, 10));
5501
5513
  const candidates2 = [];
5502
5514
  for (const template of [templates.name, templates.push].filter(Boolean)) {
5503
5515
  const exact = render2(template);
5504
5516
  candidates2.push(exact);
5505
- if (num !== unpadded) candidates2.push(exact.replace(num, unpadded));
5517
+ if (num2 !== unpadded) candidates2.push(exact.replace(num2, unpadded));
5506
5518
  if (template.includes("{slug}")) {
5507
5519
  const prefix = render2(template.slice(0, template.indexOf("{slug}")));
5508
5520
  if (prefix) candidates2.push(`${prefix}*`);
5509
5521
  }
5510
5522
  }
5511
- candidates2.push(`issue-${num}`);
5512
- if (num !== unpadded) candidates2.push(`issue-${unpadded}`);
5523
+ candidates2.push(`issue-${num2}`);
5524
+ if (num2 !== unpadded) candidates2.push(`issue-${unpadded}`);
5513
5525
  return [...new Set(candidates2)];
5514
5526
  }
5515
5527
  function remoteBranchForIssue(cwd, remote, issueKey, templates = { name: "issue-{number}" }, render2 = (t) => t.replace(/\{number\}/g, issueKey.replace(/^\D+/, ""))) {
@@ -5580,8 +5592,8 @@ function deleteRemoteBranch(cwd, remote, branch) {
5580
5592
  if (!remoteBranchExists(cwd, remote, branch)) return true;
5581
5593
  return gitOk(cwd, ["push", remote, "--delete", branch]) !== null;
5582
5594
  }
5583
- function worktreeForNumber(cwd, num) {
5584
- const re = new RegExp(`(?:^|-)${num}$`);
5595
+ function worktreeForNumber(cwd, num2) {
5596
+ const re = new RegExp(`(?:^|-)${num2}$`);
5585
5597
  return worktrees(cwd).find((w) => re.test(basename5(w.path))) ?? null;
5586
5598
  }
5587
5599
  var commitBodies = (cwd, range) => gitOk(cwd, ["log", range, "--format=%B"]) ?? "";
@@ -6475,7 +6487,7 @@ import { readFileSync as readFileSync26, existsSync as existsSync25, statSync as
6475
6487
  import { StringDecoder } from "string_decoder";
6476
6488
  import { finished } from "stream/promises";
6477
6489
  import { join as join29, dirname as dirname14, basename as basename9 } from "path";
6478
- import { createHash as createHash5 } from "crypto";
6490
+ import { createHash as createHash5, randomUUID } from "crypto";
6479
6491
 
6480
6492
  // src/state.ts
6481
6493
  import { readFileSync as readFileSync19, writeFileSync as writeFileSync14, existsSync as existsSync18, mkdirSync as mkdirSync13, rmSync as rmSync10, statSync as statSync7, readdirSync as readdirSync5 } from "fs";
@@ -9606,8 +9618,8 @@ async function noteTakeover(tracker, result, seat, ships, myShipName, continuing
9606
9618
  }
9607
9619
  }
9608
9620
  function originBranchSha(dir, t, rev) {
9609
- const num = (t.issue_tag ?? t.issue_id).replace(/^\D+/, "");
9610
- const names = [...new Set([(t.issue_tag ?? "").toLowerCase(), `issue-${num}`].filter(Boolean))];
9621
+ const num2 = (t.issue_tag ?? t.issue_id).replace(/^\D+/, "");
9622
+ const names = [...new Set([(t.issue_tag ?? "").toLowerCase(), `issue-${num2}`].filter(Boolean))];
9611
9623
  for (const n of names) {
9612
9624
  const sha = rev(dir, `origin/${n}`);
9613
9625
  if (sha) return sha.slice(0, 7);
@@ -9669,8 +9681,8 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9669
9681
  unlock();
9670
9682
  }
9671
9683
  }
9672
- const num = t.issue_id.replace(/^\D+/, "");
9673
- const existing = worktreeForNumber(dir, num);
9684
+ const num2 = t.issue_id.replace(/^\D+/, "");
9685
+ const existing = worktreeForNumber(dir, num2);
9674
9686
  const located = locateBranchForTicket(dir, cfg, t, role);
9675
9687
  const baseRef = hasRemote ? `${remote}/${base}` : base;
9676
9688
  if (existing && !existing.prunable && existsSync21(existing.path)) {
@@ -10167,6 +10179,49 @@ async function guardRunStrays(route2, ship, tracker, role, workingId, since2, lo
10167
10179
  }
10168
10180
 
10169
10181
  // src/agent-log.ts
10182
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : Number(v) || 0;
10183
+ function sumInferenceRows(rows) {
10184
+ const t = { calls: 0, tokensIn: 0, tokensOut: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: 0 };
10185
+ for (const r of rows) {
10186
+ t.calls++;
10187
+ t.tokensIn += num(r.input_tokens);
10188
+ t.tokensOut += num(r.output_tokens);
10189
+ t.cacheReadTokens += num(r.cache_read_tokens);
10190
+ t.cacheWriteTokens += num(r.cache_write_tokens);
10191
+ t.costUsd += num(r.cost_usd);
10192
+ }
10193
+ return t;
10194
+ }
10195
+ var INFERENCE_PAGE = 1e3;
10196
+ async function listInferenceRows(target, modelId, runId) {
10197
+ const filters = encodeURIComponent(JSON.stringify({
10198
+ combinator: "AND",
10199
+ conditions: [
10200
+ { columnName: "run_id", operator: "EQ", value: runId },
10201
+ { columnName: "client", operator: "EQ", value: "inference" }
10202
+ ]
10203
+ }));
10204
+ const out = [];
10205
+ for (let offset = 0; ; offset += INFERENCE_PAGE) {
10206
+ const page = await request(
10207
+ target,
10208
+ "GET",
10209
+ `/data-models/${modelId}/records?limit=${INFERENCE_PAGE}&offset=${offset}&filters=${filters}`
10210
+ );
10211
+ out.push(...page);
10212
+ if (page.length < INFERENCE_PAGE) return out;
10213
+ }
10214
+ }
10215
+ async function inferenceTotalsFor(target, runId) {
10216
+ const model = await request(
10217
+ target,
10218
+ "GET",
10219
+ `/data-models/agent_log?workspaceId=${encodeURIComponent(target.workspaceId)}`
10220
+ );
10221
+ if (!(await columnsOf(target, model.id)).has("run_id")) return void 0;
10222
+ const rows = await listInferenceRows(target, model.id, runId);
10223
+ return rows.length > 0 ? sumInferenceRows(rows) : void 0;
10224
+ }
10170
10225
  var AgentLogUnreachableError = class extends Error {
10171
10226
  };
10172
10227
  var AgentLogHttpError = class extends Error {
@@ -10331,19 +10386,20 @@ async function reportAgentRun(target, run3) {
10331
10386
  promptVersion: run3.promptVersion,
10332
10387
  promptSha: run3.promptSha
10333
10388
  };
10389
+ const { runId, ...legacyBody } = { ...body, runId: run3.runId };
10334
10390
  const workItem = target.tickets && ticketRecordId ? { workItemTable: target.tickets.issuesModelId, workItemRecordId: ticketRecordId } : {};
10335
10391
  let entry;
10336
10392
  try {
10337
- entry = await post(target, "/agents/log", { ...body, ...workItem, harness: run3.harness, provider: run3.provider });
10393
+ entry = await post(target, "/agents/log", { ...body, ...runId ? { runId } : {}, ...workItem, harness: run3.harness, provider: run3.provider });
10338
10394
  } catch (e) {
10339
10395
  if (!(e instanceof AgentLogHttpError)) throw new AgentLogUnreachableError(e.message);
10340
10396
  if (e.status >= 500) throw new AgentLogUnreachableError(e.message);
10341
- if (e.status !== 400 || !run3.harness && !run3.provider && !ticketRecordId) {
10397
+ if (e.status !== 400 || !run3.harness && !run3.provider && !ticketRecordId && !runId) {
10342
10398
  e.row = true;
10343
10399
  throw e;
10344
10400
  }
10345
10401
  try {
10346
- entry = await post(target, "/agents/log", body);
10402
+ entry = await post(target, "/agents/log", legacyBody);
10347
10403
  } catch (e2) {
10348
10404
  if (e2 instanceof AgentLogHttpError && e2.status < 500) e2.row = true;
10349
10405
  throw e2;
@@ -10362,10 +10418,21 @@ async function reportAgentRun(target, run3) {
10362
10418
  const bounces = await countAcceptanceBounces(target, target.tickets.issuesModelId, ticketRecordId);
10363
10419
  if (bounces !== null) await writeHandoffBounces(target, entry.id, bounces);
10364
10420
  }
10365
- const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run3) : void 0;
10421
+ let counterWarning;
10422
+ let counted = run3;
10423
+ if (runId && target.tickets && ticketRecordId) {
10424
+ try {
10425
+ const totals = await inferenceTotalsFor(target, runId);
10426
+ if (totals) counted = { ...run3, tokensIn: totals.tokensIn, tokensOut: totals.tokensOut, cacheReadTokens: totals.cacheReadTokens, cacheWriteTokens: totals.cacheWriteTokens, costUsd: totals.costUsd };
10427
+ else counted = { ...run3, ...run3.harnessTotals };
10428
+ } catch (e) {
10429
+ counterWarning = `could not sum run ${runId}'s inference rows for ${run3.ticketReference}: ${e.message}`;
10430
+ }
10431
+ }
10432
+ const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, counted) : void 0;
10366
10433
  return {
10367
10434
  agentLogId: entry.id,
10368
- ...counters?.warning ? { counterWarning: counters.warning } : {},
10435
+ ...counters?.warning || counterWarning ? { counterWarning: [counterWarning, counters?.warning].filter(Boolean).join("; ") } : {},
10369
10436
  ...counters?.retry ? { counterRetry: counters.retry } : {}
10370
10437
  };
10371
10438
  }
@@ -11941,6 +12008,7 @@ function planAgentRun(o) {
11941
12008
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
11942
12009
  const { model } = seat;
11943
12010
  let toolchain = seat.toolchain;
12011
+ let workspaceRunId;
11944
12012
  if (toolchain.auth === "workspace") {
11945
12013
  if (!o.apiKey || o.seatKind === "ship") {
11946
12014
  throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, which needs the seat's own agent key \u2014 this seat would fall back to the ship key (run \`crew agents sync\`)`);
@@ -11948,7 +12016,8 @@ function planAgentRun(o) {
11948
12016
  if (!o.route.resolved) {
11949
12017
  throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, but route ${o.route.route} is not connected (run \`crew connect\`)`);
11950
12018
  }
11951
- toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey } };
12019
+ workspaceRunId = o.runId ?? randomUUID();
12020
+ toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey, ticket: o.ticket, runId: workspaceRunId } };
11952
12021
  }
11953
12022
  const adapter = adapterFor(toolchain.harness);
11954
12023
  const code = codeBridgeFor(o, adapter.harness);
@@ -12045,7 +12114,8 @@ function planAgentRun(o) {
12045
12114
  routeName: o.route.route,
12046
12115
  agentLog,
12047
12116
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
12048
- promptSha
12117
+ promptSha,
12118
+ ...workspaceRunId ? { workspaceRunId } : {}
12049
12119
  };
12050
12120
  }
12051
12121
  function describePlan(p) {
@@ -12099,7 +12169,8 @@ function identityOf(plan, adapterHarness) {
12099
12169
  streamPath: plan.streamPath,
12100
12170
  agentLog: plan.agentLog ? withoutKey(plan.agentLog) : void 0,
12101
12171
  promptVersion: plan.promptVersion,
12102
- promptSha: plan.promptSha
12172
+ promptSha: plan.promptSha,
12173
+ ...plan.workspaceRunId ? { workspaceRunId: plan.workspaceRunId } : {}
12103
12174
  };
12104
12175
  }
12105
12176
  async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMinutes) {
@@ -12406,11 +12477,22 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
12406
12477
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
12407
12478
  cycles: obs.cycles,
12408
12479
  model: id.model,
12409
- tokensIn: result?.inputTokens,
12410
- tokensOut: result?.outputTokens,
12411
- cacheReadTokens: result?.cacheReadTokens,
12412
- cacheWriteTokens: result?.cacheWriteTokens,
12413
- costUsd: result?.costUsd,
12480
+ // CREW-2980: under `auth: workspace` the endpoint's rows are the accounting; the run row
12481
+ // keeps turns only, and `reportAgentRun` sums those rows into the ticket counters.
12482
+ ...id.workspaceRunId ? { runId: id.workspaceRunId } : {
12483
+ tokensIn: result?.inputTokens,
12484
+ tokensOut: result?.outputTokens,
12485
+ cacheReadTokens: result?.cacheReadTokens,
12486
+ cacheWriteTokens: result?.cacheWriteTokens,
12487
+ costUsd: result?.costUsd
12488
+ },
12489
+ ...id.workspaceRunId ? { harnessTotals: {
12490
+ tokensIn: result?.inputTokens,
12491
+ tokensOut: result?.outputTokens,
12492
+ cacheReadTokens: result?.cacheReadTokens,
12493
+ cacheWriteTokens: result?.cacheWriteTokens,
12494
+ costUsd: result?.costUsd
12495
+ } } : {},
12414
12496
  // Both come from a finished run: without the result line (interrupted) neither is trustworthy.
12415
12497
  ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
12416
12498
  ...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
@@ -12537,7 +12619,8 @@ function spawnAgent(plan, emit, hintMoved) {
12537
12619
  eventsPath: plan.eventsPath,
12538
12620
  agentLog: id.agentLog,
12539
12621
  promptVersion: plan.promptVersion,
12540
- promptSha: plan.promptSha
12622
+ promptSha: plan.promptSha,
12623
+ ...plan.workspaceRunId ? { workspaceRunId: plan.workspaceRunId } : {}
12541
12624
  };
12542
12625
  try {
12543
12626
  writeFileSync19(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
@@ -12792,14 +12875,14 @@ function describeRun(run3, route2, seatIds, worktreeOf = (d, n) => {
12792
12875
  }) {
12793
12876
  const ownRoute = run3.route === routeSlug(route2.route);
12794
12877
  const repoDir = run3.repo ? reposOf(route2).find((t) => t.name === run3.repo)?.dir : void 0;
12795
- const num = run3.ticket?.replace(/^\D+/, "") ?? "";
12878
+ const num2 = run3.ticket?.replace(/^\D+/, "") ?? "";
12796
12879
  return {
12797
12880
  role: run3.role,
12798
12881
  seat: ownRoute ? seatIds[run3.role] ?? null : null,
12799
12882
  ticket: run3.ticket,
12800
12883
  ticketRecordId: run3.ticketRecordId,
12801
12884
  repo: run3.repo ?? null,
12802
- worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
12885
+ worktree: repoDir && num2 ? worktreeOf(repoDir, num2) : null,
12803
12886
  stream: run3.streamPath,
12804
12887
  pid: run3.pid,
12805
12888
  startedAt: run3.spawnedAt,
@@ -12855,8 +12938,8 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
12855
12938
  const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
12856
12939
  const t = await tracker.ticket(run3.ticketRecordId);
12857
12940
  const target = repoTargetFor(route2, t.repo_id);
12858
- const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
12859
- const wt = target && num ? worktreeForNumber(target.dir, num) : null;
12941
+ const num2 = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
12942
+ const wt = target && num2 ? worktreeForNumber(target.dir, num2) : null;
12860
12943
  let dirty = null;
12861
12944
  try {
12862
12945
  if (wt) dirty = status(wt.path).length > 0;
@@ -18132,10 +18215,10 @@ function dedupeChangelogEntries(entries, ticket) {
18132
18215
  let key = line;
18133
18216
  if (ref) {
18134
18217
  const prefix = ref[1];
18135
- const num = ref[2];
18136
- const canonical = own && own[2] === num ? own[1] : prefix;
18137
- line = `${canonical}-${num}${line.slice(ref[0].length)}`;
18138
- key = `${canonical}-${num}`;
18218
+ const num2 = ref[2];
18219
+ const canonical = own && own[2] === num2 ? own[1] : prefix;
18220
+ line = `${canonical}-${num2}${line.slice(ref[0].length)}`;
18221
+ key = `${canonical}-${num2}`;
18139
18222
  }
18140
18223
  if (seen.has(key)) continue;
18141
18224
  seen.add(key);
@@ -21343,10 +21426,24 @@ async function showLogEntry(route2, entryId, opts = {}) {
21343
21426
  } catch (e) {
21344
21427
  throw new LogbookError(`no Agent Log entry ${entryId} in this workspace (${e.message})`);
21345
21428
  }
21346
- if (!opts.withPrompt) return { entry, reconstruction: { status: "not_recorded", reason: "not requested" } };
21429
+ let inference;
21430
+ if (entry.run_id && entry.client !== "inference") {
21431
+ try {
21432
+ const rows = await listInferenceRows(
21433
+ { baseUrl: route2.baseUrl, apiKey: resolveApiKey(route2), userAgent: opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" },
21434
+ agentLogModel.id,
21435
+ entry.run_id
21436
+ );
21437
+ if (rows.length > 0) inference = sumInferenceRows(rows);
21438
+ } catch {
21439
+ }
21440
+ }
21441
+ const withInference = inference ? { inference } : {};
21442
+ if (!opts.withPrompt) return { entry, ...withInference, reconstruction: { status: "not_recorded", reason: "not requested" } };
21347
21443
  if (!entry.agent_id || !entry.prompt_version) {
21348
21444
  return {
21349
21445
  entry,
21446
+ ...withInference,
21350
21447
  reconstruction: {
21351
21448
  status: "not_recorded",
21352
21449
  reason: !entry.agent_id ? "this entry has no linked Agents row (predates crew agents sync being run for its role)" : "this entry predates ISSUE-377/529 \u2014 no prompt_version was recorded"
@@ -21358,12 +21455,13 @@ async function showLogEntry(route2, entryId, opts = {}) {
21358
21455
  try {
21359
21456
  historyEntries = await fetchHistoryEntries(route2, agentsModel.id, entry.agent_id, opts.userAgent, 200);
21360
21457
  } catch (e) {
21361
- return { entry, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
21458
+ return { entry, ...withInference, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
21362
21459
  }
21363
21460
  const match = historyEntries.find((h) => h.history_id === entry.prompt_version);
21364
21461
  if (!match) {
21365
21462
  return {
21366
21463
  entry,
21464
+ ...withInference,
21367
21465
  reconstruction: {
21368
21466
  status: "approximate",
21369
21467
  reason: `history entry ${entry.prompt_version} no longer exists on the Agents row (purged, or the row itself was recreated)`
@@ -21375,6 +21473,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
21375
21473
  const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
21376
21474
  return {
21377
21475
  entry,
21476
+ ...withInference,
21378
21477
  reconstruction: {
21379
21478
  status: exact ? "exact" : "approximate",
21380
21479
  historyId: match.history_id,
@@ -21998,8 +22097,8 @@ var BRANCH_STEM = /^(?:.*\/)?([^/]+?-\d+)(?:-.*)?$/;
21998
22097
  function ticketForWorktree(w, byNumber, contract) {
21999
22098
  if (w.branch) {
22000
22099
  const stem = BRANCH_STEM.exec(w.branch)?.[1];
22001
- const num = stem && /-(\d+)$/.exec(stem)?.[1];
22002
- const t = num ? byNumber.get(num) : void 0;
22100
+ const num2 = stem && /-(\d+)$/.exec(stem)?.[1];
22101
+ const t = num2 ? byNumber.get(num2) : void 0;
22003
22102
  if (t && ticketStems(t).has(stem)) return t;
22004
22103
  }
22005
22104
  const dir = basename13(w.path);
@@ -22179,11 +22278,11 @@ function deployedBranchStems(tickets, contract) {
22179
22278
  }
22180
22279
  function ticketStems(t) {
22181
22280
  const stems = /* @__PURE__ */ new Set();
22182
- const num = t.issue_id.replace(/^\D+/, "");
22183
- const unpadded = String(Number.parseInt(num, 10));
22281
+ const num2 = t.issue_id.replace(/^\D+/, "");
22282
+ const unpadded = String(Number.parseInt(num2, 10));
22184
22283
  const prefixes = ["issue", t.project_issue_prefix?.toLowerCase()].filter((p) => !!p);
22185
22284
  for (const p of prefixes) {
22186
- stems.add(`${p}-${num}`);
22285
+ stems.add(`${p}-${num2}`);
22187
22286
  stems.add(`${p}-${unpadded}`);
22188
22287
  }
22189
22288
  return stems;
@@ -24161,8 +24260,8 @@ async function reconcileForRoute(ctx, o) {
24161
24260
  if (!memberId) return;
24162
24261
  const t = await tracker.ticket(sc.ticketRecordId);
24163
24262
  const target = repoTargetFor(route2, t.repo_id);
24164
- const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24165
- const wt = target && num ? worktreeForNumber(target.dir, num) : null;
24263
+ const num2 = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24264
+ const wt = target && num2 ? worktreeForNumber(target.dir, num2) : null;
24166
24265
  let dirty = null;
24167
24266
  try {
24168
24267
  if (wt) dirty = status(wt.path).length > 0;
@@ -24196,8 +24295,8 @@ async function postRunKilled(route2, tracker, ticketId, memberId, limitMinutes,
24196
24295
  try {
24197
24296
  const t = await tracker.ticket(ticketId);
24198
24297
  const target = repoTargetFor(route2, t.repo_id);
24199
- const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24200
- const wt = target && num ? worktreeForNumber(target.dir, num) : null;
24298
+ const num2 = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24299
+ const wt = target && num2 ? worktreeForNumber(target.dir, num2) : null;
24201
24300
  let dirty = null;
24202
24301
  try {
24203
24302
  if (wt) dirty = status(wt.path).length > 0;
@@ -25545,10 +25644,10 @@ async function handlePeriscopeMcp(d, req, res, segment) {
25545
25644
  function findPeriscope(sidecars, segment) {
25546
25645
  const m = /^(?:([A-Za-z]+)-)?(\d+)$/.exec(segment);
25547
25646
  if (!m) return void 0;
25548
- const num = String(Number(m[2]));
25647
+ const num2 = String(Number(m[2]));
25549
25648
  return sidecars.find((sc) => {
25550
25649
  const t = /^(?:([A-Za-z]+)-)?0*(\d+)$/.exec(sc.ticket);
25551
- if (!t || t[2] !== num) return false;
25650
+ if (!t || t[2] !== num2) return false;
25552
25651
  return !m[1] || !t[1] || m[1].toLowerCase() === t[1].toLowerCase();
25553
25652
  });
25554
25653
  }
@@ -28636,7 +28735,7 @@ ${cu.tail.trimEnd()}
28636
28735
  );
28637
28736
  }
28638
28737
  } else {
28639
- const { entry, reconstruction } = await showLogEntry(target, entryId, {
28738
+ const { entry, inference, reconstruction } = await showLogEntry(target, entryId, {
28640
28739
  userAgent: cfg.ship.userAgent,
28641
28740
  withPrompt: flag("prompt")
28642
28741
  });
@@ -28650,6 +28749,9 @@ model: ${entry.model ?? "?"}
28650
28749
  harness: ${entry.harness ?? "?"}
28651
28750
  provider: ${entry.provider ?? "?"}
28652
28751
  ` + (entry.num_turns != null ? `turns: ${entry.num_turns}
28752
+ ` : "") + (inference ? `calls: ${inference.calls}
28753
+ tokens: in ${inference.tokensIn}, out ${inference.tokensOut}, cache read ${inference.cacheReadTokens}, cache write ${inference.cacheWriteTokens}
28754
+ cost: $${inference.costUsd.toFixed(4)}
28653
28755
  ` : "") + (entry.prefix_tokens != null ? `prefix: ${entry.prefix_tokens} tokens
28654
28756
  ` : "") + (formatBeforeEdit(entry) ? `before edit: ${formatBeforeEdit(entry)}
28655
28757
  ` : "")
@@ -29692,7 +29794,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
29692
29794
  process.stderr.write("drop: need a ticket number (326, ISSUE-326, or TABL-326)\n");
29693
29795
  process.exit(2);
29694
29796
  }
29695
- const num = Number(n);
29797
+ const num2 = Number(n);
29696
29798
  const repos = await resolvedRepos(route2);
29697
29799
  const candidates2 = repos.map((r) => ({ repo: r, found: worktreeForNumber(r.dir, n) })).filter((c) => c.found !== null);
29698
29800
  if (candidates2.length === 0) {
@@ -29713,9 +29815,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
29713
29815
  const wt = found.path;
29714
29816
  try {
29715
29817
  const trD = new Tracker(route2, cfg.ship);
29716
- const tD = (await trD.ticketsByNumbers([n])).find((t) => t.issue_id.replace(/^\D+/, "") === String(num));
29818
+ const tD = (await trD.ticketsByNumbers([n])).find((t) => t.issue_id.replace(/^\D+/, "") === String(num2));
29717
29819
  if (tD && tD[trD.contract.columns.status] === trD.contract.statuses.draft) {
29718
- process.stderr.write(`drop: ${tD.issue_tag ?? tD.issue_id} is a draft ticket \u2014 its worktree belongs to a periscope; end it with \`crew periscope down ${num} --keep\` or \`--discard\`
29820
+ process.stderr.write(`drop: ${tD.issue_tag ?? tD.issue_id} is a draft ticket \u2014 its worktree belongs to a periscope; end it with \`crew periscope down ${num2} --keep\` or \`--discard\`
29719
29821
  `);
29720
29822
  process.exit(2);
29721
29823
  }
@@ -29725,7 +29827,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
29725
29827
  name: target.config.branch.name,
29726
29828
  push: target.config.branch.push
29727
29829
  }, (t) => renderBranchName(t, { key: `ISSUE-${n}`, lowercase: target.config.branch.lowercase }));
29728
- const pids = pidsInWorktree(wt, num);
29830
+ const pids = pidsInWorktree(wt, num2);
29729
29831
  if (pids.length) {
29730
29832
  emit.emit(`${dryRun ? "would kill" : "killing"} processes still inside ${wt}: ${pids.join(" ")}`);
29731
29833
  if (!dryRun) await killGently(pids);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.129.1",
3
+ "version": "0.130.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",