agentwrangler 0.1.1-next.afd4e62 → 0.1.1-next.fbbbe7f

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 (90) hide show
  1. package/README.md +162 -82
  2. package/dist/apply/jobs.js +3 -13
  3. package/dist/daemon/http.js +0 -2
  4. package/dist/daemon/index.js +6 -8
  5. package/dist/daemon/router.js +6 -63
  6. package/dist/detector/context-history-retention.js +0 -49
  7. package/dist/detector/detectors/d10_catalog_footprint.js +2 -2
  8. package/dist/detector/detectors/d1_ctx_always_loaded.js +9 -9
  9. package/dist/detector/detectors/d2_session_long_full_context.js +2 -2
  10. package/dist/detector/detectors/d4_model_mismatch.js +3 -3
  11. package/dist/detector/detectors/d6_tool_result_bloat.js +1 -1
  12. package/dist/detector/detectors/d7_loop_retry_waste.js +1 -1
  13. package/dist/detector/detectors/d8_cache_write_churn.js +2 -2
  14. package/dist/detector/detectors/d9_idle_background_session.js +1 -1
  15. package/dist/detector/measurement.js +3 -14
  16. package/dist/hook/precompact-checkpoint-hook.mjs +13 -26
  17. package/dist/ingest/pricing.js +0 -15
  18. package/dist/query/api/cost-per-success.js +1 -13
  19. package/dist/query/api/delivery.js +3 -25
  20. package/dist/query/api/effectiveness.js +5 -30
  21. package/dist/query/api/efficiency-headroom.js +14 -12
  22. package/dist/query/api/outcomes.js +1 -2
  23. package/dist/query/api/overview.js +6 -90
  24. package/dist/query/api/recommendations-ledger.js +14 -9
  25. package/dist/query/api/recommendations.js +1 -21
  26. package/dist/query/settings-store.js +0 -4
  27. package/dist/query/spend.js +2 -64
  28. package/dist/ui/assets/{BarChart-CMfobGNC.js → BarChart-p1usctxJ.js} +3 -3
  29. package/dist/ui/assets/BriefsPage-CneqLVGC.js +2 -0
  30. package/dist/ui/assets/CacheWriteSpikesChart-zNRZbzy9.js +1 -0
  31. package/dist/ui/assets/Chip-Bu6udQGq.js +1 -0
  32. package/dist/ui/assets/ComposedChart-8D5E-Po_.js +1 -0
  33. package/dist/ui/assets/{EmptyState-DDZZlBIw.js → EmptyState-CvQeJjjG.js} +1 -1
  34. package/dist/ui/assets/FlavorDecomposition-xcMLA50f.js +1 -0
  35. package/dist/ui/assets/FrictionCell-OafGLGxT.js +2 -0
  36. package/dist/ui/assets/GlossaryPage-BmOP3Syj.js +1 -0
  37. package/dist/ui/assets/HotSessionsPage-FoxuuDLF.js +1 -0
  38. package/dist/ui/assets/{InfoTip-CE9f04Fa.js → InfoTip-Dx7BHpBu.js} +1 -1
  39. package/dist/ui/assets/{Legend-CjlVvByY.js → Legend--q6-rQGk.js} +3 -3
  40. package/dist/ui/assets/{Line-CVFZTFsG.js → Line-DzVtUwF4.js} +1 -1
  41. package/dist/ui/assets/OverviewPage-BjSQYNvS.js +2 -0
  42. package/dist/ui/assets/RecommendationsPage-CRTpzcHs.js +2 -0
  43. package/dist/ui/assets/{Scatter-DCdt0o6A.js → Scatter-Cr_hCkjG.js} +1 -1
  44. package/dist/ui/assets/SessionDetailPage-Dd3u8kVI.js +1 -0
  45. package/dist/ui/assets/SettingsPage-BPVQyjlc.js +25 -0
  46. package/dist/ui/assets/{Skeleton-DEF-0Fbx.js → Skeleton-CcD61x7q.js} +1 -1
  47. package/dist/ui/assets/SpendPercentileChip-BaQhJQAn.js +1 -0
  48. package/dist/ui/assets/TrendChart-Baz3GGgE.js +1 -0
  49. package/dist/ui/assets/WorkspaceDetailPage-yk4z_EtQ.js +1 -0
  50. package/dist/ui/assets/WorkspacesPage-BDtclXJP.js +1 -0
  51. package/dist/ui/assets/{CartesianChart-xsHi_mRg.js → chart-theme-JH5aQymO.js} +36 -36
  52. package/dist/ui/assets/graphicalItemSelectors-CCbl8AKC.js +1 -0
  53. package/dist/ui/assets/{index-kCU7neaT.js → index-DRhoA51c.js} +3 -3
  54. package/dist/ui/assets/{index-D_IJsvkJ.css → index-SrfNUeBZ.css} +1 -1
  55. package/dist/ui/assets/{prompt-templates-CVzMex9L.js → prompt-templates-DhR4Qoy9.js} +4 -4
  56. package/dist/ui/assets/{useExperimentalActions-BOEFCZJz.js → useExperimentalActions-BPDdswCN.js} +1 -1
  57. package/dist/ui/index.html +2 -2
  58. package/package.json +1 -1
  59. package/dist/daemon/effect-pass.js +0 -24
  60. package/dist/db/migrations/018_effect_cycles.sql +0 -109
  61. package/dist/effects/api-contract.js +0 -1
  62. package/dist/effects/engine.js +0 -574
  63. package/dist/effects/gate.js +0 -2
  64. package/dist/effects/index.js +0 -5
  65. package/dist/effects/legacy.js +0 -28
  66. package/dist/effects/registry.js +0 -152
  67. package/dist/effects/sql-observer.js +0 -238
  68. package/dist/effects/store.js +0 -181
  69. package/dist/effects/types.js +0 -2
  70. package/dist/query/api/effect-service.js +0 -308
  71. package/dist/query/api/esf-observations.js +0 -254
  72. package/dist/ui/assets/BriefsPage-CnWsYSEI.js +0 -2
  73. package/dist/ui/assets/CacheWriteSpikesChart-Fufucloe.js +0 -1
  74. package/dist/ui/assets/Chip-B18SATzv.js +0 -1
  75. package/dist/ui/assets/ComposedChart-C9ExQN46.js +0 -1
  76. package/dist/ui/assets/FlavorDecomposition-DaFuiY-s.js +0 -1
  77. package/dist/ui/assets/FrictionCell-Cf7n9pZ5.js +0 -2
  78. package/dist/ui/assets/GlossaryPage-Ch9vnV-9.js +0 -1
  79. package/dist/ui/assets/HotSessionsPage-CMVND16B.js +0 -1
  80. package/dist/ui/assets/OverviewPage-CZPvN1Rh.js +0 -2
  81. package/dist/ui/assets/RecommendationsPage-CKD8i6pV.js +0 -2
  82. package/dist/ui/assets/SessionDetailPage-CqbwihMb.js +0 -1
  83. package/dist/ui/assets/SettingsPage-Wjg7im_R.js +0 -25
  84. package/dist/ui/assets/SpendPercentileChip-BNvG7aFY.js +0 -1
  85. package/dist/ui/assets/TrendChart-vycHn5rQ.js +0 -1
  86. package/dist/ui/assets/WorkspaceDetailPage-SSW09ztI.js +0 -1
  87. package/dist/ui/assets/WorkspacesPage-NHvgHsMQ.js +0 -1
  88. package/dist/ui/assets/chart-theme-DcuvcaLU.js +0 -1
  89. package/dist/ui/assets/graphicalItemSelectors-DXHqMBmB.js +0 -1
  90. package/dist/ui/assets/rec-sessions-ggz9MYgP.js +0 -1
@@ -81,12 +81,12 @@ export const d2Detector = {
81
81
  scopeKey: `D2|global|${formula.model}`,
82
82
  category: "CONTEXT",
83
83
  scope_workspace_id: null,
84
- lever: "Use /clear between unrelated tasks, split long work into separate sessions, and avoid automatic compaction while you are working.",
84
+ lever: "/clear between unrelated tasks; split long work; avoid mid-task /compact.",
85
85
  target_metric: "avg_context_per_turn",
86
86
  modeled_savings_u_per_wk: savingsU,
87
87
  modeled_formula: formula,
88
88
  evidence: {
89
- title: `Split ${n} long session${n === 1 ? "" : "s"} this week`,
89
+ title: `Shorten sessions: ${n} long-context run${n === 1 ? "" : "s"} this week`,
90
90
  qualifying_session_count: qualifying.length,
91
91
  qualifying_turn_count: qualifyingTurnCount,
92
92
  session_ids: sessionIds,
@@ -192,7 +192,7 @@ export const d4Detector = {
192
192
  ? {
193
193
  withheld: true,
194
194
  withheld_reason: `Sonnet weekly cap is the binding constraint (Sonnet util ${bindingSonnet.utilization} >= all-models ${perModelSnapshot.seven_day_util}) — routing Opus->Sonnet would worsen it`,
195
- title: `Model change is not recommended: ${mismatchPct}% of Opus turns have large context and little output`,
195
+ title: `[withheld] Route Opus→Sonnet: ${mismatchPct}% of turns are high-context low-output`,
196
196
  }
197
197
  : perModelSnapshot && sonnetEntries && sonnetEntries.length > 0
198
198
  ? { cap_attribution: "all_models_or_opus_binds" }
@@ -202,7 +202,7 @@ export const d4Detector = {
202
202
  category: "MODEL",
203
203
  scope_workspace_id: workspace_id,
204
204
  // Advisory gate (W0.3): which cap binds is NOT inferable from JSONL. Conditional lever.
205
- lever: "Check /usage first. If your overall, Opus, or 5-hour limit is filling, these large-context Opus turns with little output may be suitable for Sonnet. Do not switch if your Sonnet weekly limit is the one filling.",
205
+ lever: "If your all-models / Opus / 5h cap is the one binding — check /usage — these high-context low-output Opus turns are Sonnet-movable. This does NOT help, and can hurt, if your Sonnet-specific weekly cap is the binding constraint.",
206
206
  target_metric: "model_mix_opus_fraction",
207
207
  // Advisory gate: suppress the crisp $/wk headline until live /usage cap-attribution exists.
208
208
  modeled_savings_u_per_wk: null,
@@ -210,7 +210,7 @@ export const d4Detector = {
210
210
  // Destructure out result_usd_per_wk so the advisory formula carries no crisp $/wk figure.
211
211
  modeled_formula: (({ result_usd_per_wk: _, ...rest }) => ({ ...rest, kind: "ADVISORY" }))(formula),
212
212
  evidence: {
213
- title: `Review model choice: ${mismatchPct}% of Opus turns have large context and little output`,
213
+ title: `Route Opus→Sonnet: ${mismatchPct}% of turns are high-context low-output`,
214
214
  workspace_id,
215
215
  total_opus_turns_per_week: totalOpus,
216
216
  mismatch_turns_per_week: mismatchCount,
@@ -262,7 +262,7 @@ export const d6Detector = {
262
262
  modeled_savings_u_per_wk: modeledSavingsU,
263
263
  modeled_formula: formula,
264
264
  evidence: {
265
- title: `Reduce large tool results: ${Math.round(bloatShare * 100)}% of this session's context`,
265
+ title: `Trim tool output: ${Math.round(bloatShare * 100)}% bloat share in session`,
266
266
  session_id: row.session_id,
267
267
  workspace_id: row.workspace_id,
268
268
  tool_result_bytes: row.tool_result_bytes,
@@ -299,7 +299,7 @@ export const d7Detector = {
299
299
  expression: "cap-weighted exposure of turns owning repeat-excess events; not an avoidable-token or USD savings estimate",
300
300
  },
301
301
  evidence: {
302
- title: `Stop repeated attempts: ${flaggedTurns.size} affected turn${flaggedTurns.size === 1 ? "" : "s"} in this session`,
302
+ title: `Break retry loops: ${flaggedTurns.size} flagged turn${flaggedTurns.size === 1 ? "" : "s"} in session`,
303
303
  session_id: sessionId,
304
304
  workspace_id: session.workspaceId,
305
305
  loop_flagged_event_count: flaggedEventIds.size,
@@ -125,7 +125,7 @@ export const d8Detector = {
125
125
  // TTL-regime facet: is this session dominated by 5m-tier creation?
126
126
  const fiveMinCreation = events.reduce((s, e) => s + e.cache_write_5m, 0);
127
127
  const regime5m = totalCreation > 0 && fiveMinCreation / totalCreation >= D8_REGIME_5M_SHARE;
128
- const baseLever = "Before a long break, use /clear or resume from a summary. Make instruction-file changes between sessions so they do not rebuild the cache while you work.";
128
+ const baseLever = "Use /clear (or resume-from-summary) before idling past the cache TTL, and batch prefix/CLAUDE.md edits to a session boundary so they don't invalidate the warm cache mid-session.";
129
129
  const lever = regime5m
130
130
  ? `${baseLever} This session's creation is mostly 5m-tier — enable the 1h cache regime (ENABLE_PROMPT_CACHING_1H) where long pauses are unavoidable.`
131
131
  : baseLever;
@@ -138,7 +138,7 @@ export const d8Detector = {
138
138
  modeled_savings_u_per_wk: savingsU,
139
139
  modeled_formula: formula,
140
140
  evidence: {
141
- title: `Avoid repeated cache rebuilds: ${events.length} after a long pause`,
141
+ title: `Reduce cache-write churn: ${events.length} re-write${events.length === 1 ? "" : "s"} in session`,
142
142
  session_id: sessionId,
143
143
  workspace_id,
144
144
  churn_event_count: events.length,
@@ -62,7 +62,7 @@ export const d9Detector = {
62
62
  modeled_savings_u_per_wk: null,
63
63
  modeled_formula: d9Formula(sidechainCap, D9_UNPRODUCTIVE_FRACTION),
64
64
  evidence: {
65
- title: `Review background-agent work: ${Math.round(share * 100)}% of estimated limit use`,
65
+ title: `Review background fan-out: ${Math.round(share * 100)}% cap-weighted sidechain`,
66
66
  workspace_id: row.workspace_id,
67
67
  sidechain_cap_weighted_tokens: sidechainCap,
68
68
  total_cap_weighted_tokens: totalCap,
@@ -20,8 +20,6 @@
20
20
  *
21
21
  * Injected `now: Date` everywhere — never call new Date() in this module.
22
22
  */
23
- import { ESF_EFFECT_WRITER_ENABLED } from "../effects/gate.js";
24
- import { PREMIUM_MODEL_SQL } from "../ingest/pricing.js";
25
23
  import { GLOBAL_WORKSPACE_ID } from "./context-probe.js";
26
24
  import { isD1SourceBackedRecommendation, parseD1SourceIdentity } from "./d1-source-identity.js";
27
25
  /** Wait ≥1 day after adoption before entering MEASURING. */
@@ -84,14 +82,14 @@ function avgFloorContext(db, scopeWorkspaceId, fromIso, toIso) {
84
82
  /**
85
83
  * D4 signal: routing-adherence score = ROUND(100 × (1 − premium_share)) over
86
84
  * non-sidechain reconciled turns in [fromIso, toIso), where premium = model
87
- * matches PREMIUM_MODEL_SQL (Opus/Fable/Mythos). The data-model's "mechanical turn" classifier is not yet
85
+ * contains 'opus'. The data-model's "mechanical turn" classifier is not yet
88
86
  * shipped, so all reconciled turns are the mechanical set (conservative proxy —
89
87
  * documented in the W4 build digest).
90
88
  */
91
89
  function routingAdherenceScore(db, scopeWorkspaceId, fromIso, toIso) {
92
90
  const scoped = scopeWorkspaceId !== null;
93
91
  const sql = `SELECT COUNT(*) AS n,
94
- AVG(CASE WHEN ${PREMIUM_MODEL_SQL} THEN 1.0 ELSE 0.0 END) AS premium_share
92
+ AVG(CASE WHEN model LIKE '%opus%' THEN 1.0 ELSE 0.0 END) AS premium_share
95
93
  FROM turns
96
94
  WHERE is_sidechain = 0 AND provisional = 0 AND ts >= ? AND ts < ? ${scoped ? "AND workspace_id = ?" : ""}`;
97
95
  const params = scoped ? [fromIso, toIso, scopeWorkspaceId] : [fromIso, toIso];
@@ -136,8 +134,7 @@ function handlerFor(rec) {
136
134
  }
137
135
  /** Warning-class recs (D5 / LIMIT / explicit NONE) skip the lifecycle entirely (design §2d). */
138
136
  export function isWarningClass(rec) {
139
- return (rec.detector_id === "D5" ||
140
- rec.category === "LIMIT" ||
137
+ return (rec.category === "LIMIT" ||
141
138
  rec.target_metric === "NONE" ||
142
139
  rec.target_metric === "forecast_margin");
143
140
  }
@@ -366,9 +363,6 @@ function closeMeasurement(db, rec, now, result) {
366
363
  */
367
364
  export function runMeasurementPass(db, now, opts) {
368
365
  const result = { to_measuring: 0, verdicts: 0, skipped: 0 };
369
- // The versioned writer leaves every historical W4 row unchanged, including open rows.
370
- if (ESF_EFFECT_WRITER_ENABLED)
371
- return result;
372
366
  if (!tableExists(db, "context_inventory_history")) {
373
367
  console.warn("W4: context_inventory_history not available; measurement pass skipped");
374
368
  return result;
@@ -386,16 +380,11 @@ export function runMeasurementPass(db, now, opts) {
386
380
  }
387
381
  }
388
382
  try {
389
- // A code-gate rollback must not let W4 take over an already-versioned recommendation.
390
- const versionedExclusion = tableExists(db, "effect_cycles")
391
- ? "AND NOT EXISTS (SELECT 1 FROM effect_cycles c WHERE c.rec_id=recommendations.rec_id)"
392
- : "";
393
383
  const rows = db
394
384
  .prepare(`SELECT rec_id, detector_id, category, scope_workspace_id, evidence_json,
395
385
  target_metric, state, adopted_at
396
386
  FROM recommendations
397
387
  WHERE state IN ('ADOPTED', 'MEASURING') AND adopted_at IS NOT NULL
398
- ${versionedExclusion}
399
388
  ORDER BY adopted_at ASC, rec_id ASC`)
400
389
  .all();
401
390
  for (const rec of rows) {
@@ -11,7 +11,6 @@
11
11
  * committed). The hook writes no transcript content to stdout/stderr.
12
12
  */
13
13
 
14
- import { randomBytes } from "node:crypto";
15
14
  import * as fs from "node:fs";
16
15
  import * as os from "node:os";
17
16
  import * as path from "node:path";
@@ -20,11 +19,9 @@ import { fileURLToPath } from "node:url";
20
19
 
21
20
  const MAX_COUNT = 20;
22
21
  const MAX_BYTES = 500 * 1024 * 1024; // 500 MiB across all snapshots
23
- const MAX_COLLISION_ATTEMPTS = 16;
24
22
 
25
- // A per-process counter preserves local write ordering. The random token prevents
26
- // separately spawned hooks with the same session and millisecond from choosing the
27
- // same name; exclusive creation below is the final no-overwrite guard.
23
+ // Monotonic counter so two firings in the same millisecond get distinct filenames
24
+ // that still sort in write order (the timestamp dominates; this breaks same-ms ties).
28
25
  let snapshotCounter = 0;
29
26
 
30
27
  /** The directory snapshots are written to (overridable for tests). */
@@ -36,8 +33,7 @@ export function checkpointDir() {
36
33
  export function snapshotName(sessionId, now) {
37
34
  snapshotCounter = (snapshotCounter + 1) % 1_000_000;
38
35
  const stamp = now.toISOString().replace(/[:.]/g, "-");
39
- const counter = String(snapshotCounter).padStart(6, "0");
40
- return `${encodeURIComponent(sessionId)}-${stamp}-${counter}-${randomBytes(6).toString("hex")}.jsonl`;
36
+ return `${encodeURIComponent(sessionId)}-${stamp}-${String(snapshotCounter).padStart(6, "0")}.jsonl`;
41
37
  }
42
38
 
43
39
  /**
@@ -47,22 +43,14 @@ export function snapshotName(sessionId, now) {
47
43
  export function writeCheckpoint(transcriptPath, sessionId, dir, now = new Date()) {
48
44
  if (typeof transcriptPath !== "string" || !fs.existsSync(transcriptPath)) return null;
49
45
  fs.mkdirSync(dir, { recursive: true });
50
- for (let attempt = 0; attempt < MAX_COLLISION_ATTEMPTS; attempt += 1) {
51
- const dest = path.join(dir, snapshotName(sessionId, now));
52
- try {
53
- fs.copyFileSync(transcriptPath, dest, fs.constants.COPYFILE_EXCL);
54
- } catch (error) {
55
- if (error?.code === "EEXIST") continue;
56
- throw error;
57
- }
58
- try {
59
- fs.chmodSync(dest, 0o600);
60
- } catch {
61
- // chmod is a no-op boundary on Windows; the user-profile ACL is the equivalent guard.
62
- }
63
- return dest;
46
+ const dest = path.join(dir, snapshotName(sessionId, now));
47
+ fs.copyFileSync(transcriptPath, dest);
48
+ try {
49
+ fs.chmodSync(dest, 0o600);
50
+ } catch {
51
+ // chmod is a no-op boundary on Windows; the user-profile ACL is the equivalent guard.
64
52
  }
65
- return null;
53
+ return dest;
66
54
  }
67
55
 
68
56
  /**
@@ -70,10 +58,9 @@ export function writeCheckpoint(transcriptPath, sessionId, dir, now = new Date()
70
58
  * not create. Malformed .jsonl files are deliberately left alone by retention.
71
59
  */
72
60
  function snapshotTimestamp(name) {
73
- const match =
74
- /-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z-\d{6}(?:-[a-f0-9]{12})?\.jsonl$/.exec(
75
- name,
76
- );
61
+ const match = /-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z-\d{6}\.jsonl$/.exec(
62
+ name,
63
+ );
77
64
  if (!match) return null;
78
65
  const [, year, month, day, hour, minute, second, millisecond] = match;
79
66
  const timestamp = Date.parse(
@@ -62,21 +62,6 @@ export function modelTier(model) {
62
62
  }
63
63
  return null;
64
64
  }
65
- /**
66
- * SQL predicate matching premium-priced models on a `model` column: Opus plus
67
- * the Fable/Mythos families (Fable's $10/$50 base is 2× Opus). Shared by every
68
- * premium-share aggregate so the family list can't drift between queries.
69
- */
70
- export const PREMIUM_MODEL_SQL = "(model LIKE '%opus%' OR model LIKE '%fable%' OR model LIKE '%mythos%')";
71
- /**
72
- * Input list price ($/MTok) for a model, for ordering model lists
73
- * most-expensive first. Derived from LIST_PRICES via modelTier so a new tier
74
- * automatically sorts by its price; unpriceable models return 0 (sort last).
75
- */
76
- export function modelInputListPrice(model) {
77
- const tier = modelTier(model);
78
- return tier === null ? 0 : (LIST_PRICES[tier]?.[0] ?? 0);
79
- }
80
65
  function addDaysIso(iso, days) {
81
66
  const d = new Date(iso);
82
67
  d.setUTCDate(d.getUTCDate() + days);
@@ -21,7 +21,6 @@ export function getCostPerSuccess(db, workspaceId, from, to) {
21
21
  to,
22
22
  workspaceId,
23
23
  workspaceId,
24
- workspaceId,
25
24
  from,
26
25
  to,
27
26
  workspaceId,
@@ -47,14 +46,6 @@ export function getCostPerSuccess(db, workspaceId, from, to) {
47
46
  JOIN merged_work_items mwi ON mwi.work_item_id = swl.work_item_id
48
47
  WHERE 1 = 1${sessionFilter}
49
48
  ),
50
- merged_session_links AS (
51
- SELECT swl.session_id, COUNT(DISTINCT swl.work_item_id) AS merged_pr_links
52
- FROM session_work_links swl
53
- JOIN merged_work_items mwi ON mwi.work_item_id = swl.work_item_id
54
- JOIN sessions s ON s.session_id = swl.session_id
55
- WHERE 1 = 1${sessionFilter}
56
- GROUP BY swl.session_id
57
- ),
58
49
  in_window_sessions AS (
59
50
  SELECT s.session_id, s.cost_equiv_u
60
51
  FROM sessions s
@@ -73,8 +64,6 @@ export function getCostPerSuccess(db, workspaceId, from, to) {
73
64
  SELECT
74
65
  (SELECT COUNT(*) FROM merged_work_items) AS merged_pr_count,
75
66
  (SELECT COUNT(*) FROM closed_unmerged_work_items) AS closed_unmerged_count,
76
- (SELECT COUNT(*) FROM merged_session_links) AS unique_linked_session_count,
77
- (SELECT COUNT(*) FROM merged_session_links WHERE merged_pr_links > 1) AS shared_linked_session_count,
78
67
  CASE WHEN (SELECT COUNT(*) FROM merged_work_items) = 0 THEN NULL
79
68
  ELSE (SELECT COALESCE(SUM(cost_equiv_u), 0) FROM merged_session_costs) * 1.0
80
69
  / (SELECT COUNT(*) FROM merged_work_items)
@@ -102,14 +91,13 @@ export function getCostPerSuccess(db, workspaceId, from, to) {
102
91
  };
103
92
  return buildResponse(data, {
104
93
  claim_kind: "OBS_PROXY",
105
- metric_definition_version: "esf-1",
106
94
  n: data.merged_pr_count,
107
95
  window: { from, to },
108
96
  qualification: {
109
97
  provisional_excluded: false,
110
98
  unpriced_turns: 0,
111
99
  claim_kinds_count: 1,
112
- note: "Directional (OBS_PROXY): Unique linked-session cost / merged PRs. The terminal-date PR cohort counts each linked session once, even when it links to multiple merged PRs; shared sessions are coverage, not per-PR allocation. Full linked-session lifecycle cost is used, while linkage_coverage_pct is a separate session-start coverage observation. Unknown pricing is not free. Survivorship bias and reviewer dependence remain.",
100
+ note: "Directional (OBS_PROXY): survivorship bias (heavy-spend sessions that never open a PR are invisible); reviewer-dependence (merge is a human decision, not a quality guarantee); linkage-coverage cap (only linkage_coverage_pct% of in-window sessions are linked to a PR, so unlinked spend is excluded). cost_per_merged_pr_u uses lifecycle attribution: each merged PR carries the full cost of every linked session whenever it ran, so narrowing the window changes the PR population, not the per-PR cost.",
113
101
  },
114
102
  ...(workspaceId === null ? {} : { drilldown_ids: { workspace_id: workspaceId } }),
115
103
  });
@@ -44,20 +44,8 @@ export function getDeliveryMetrics(db, opts) {
44
44
  FROM tool_events te
45
45
  WHERE te.session_id = iws.session_id
46
46
  AND te.commit_sha IS NOT NULL
47
- ) AND s.state = 'RECONCILED' AS is_abandoned_session,
48
- s.state = 'LIVE'
49
- AND EXISTS (
50
- SELECT 1 FROM tool_events te
51
- WHERE te.session_id = iws.session_id
52
- AND te.tool_name IN ('Bash', 'Edit', 'Write', 'NotebookEdit')
53
- )
54
- AND NOT EXISTS (
55
- SELECT 1 FROM tool_events te
56
- WHERE te.session_id = iws.session_id
57
- AND te.commit_sha IS NOT NULL
58
- ) AS is_live_session
47
+ ) AS is_abandoned_session
59
48
  FROM in_window_sessions iws
60
- JOIN sessions s USING (session_id)
61
49
  ),
62
50
  session_spend AS (
63
51
  SELECT session_id,
@@ -79,16 +67,7 @@ export function getDeliveryMetrics(db, opts) {
79
67
  CASE WHEN COALESCE(SUM(ss.spend_u), 0) = 0 THEN NULL
80
68
  ELSE SUM(CASE WHEN sf.is_abandoned_session THEN ss.spend_u ELSE 0 END) * 1.0
81
69
  / SUM(ss.spend_u)
82
- END AS abandoned_spend_share,
83
- COALESCE(SUM(sf.is_abandoned_session), 0) AS no_commit_activity_session_count,
84
- COALESCE(SUM(CASE WHEN sf.is_abandoned_session THEN ss.spend_u ELSE 0 END), 0)
85
- AS no_commit_activity_spend_u,
86
- CASE WHEN COALESCE(SUM(ss.spend_u), 0) = 0 THEN NULL
87
- ELSE SUM(CASE WHEN sf.is_abandoned_session THEN ss.spend_u ELSE 0 END) * 1.0
88
- / SUM(ss.spend_u)
89
- END AS no_commit_activity_spend_share,
90
- COALESCE(SUM(CASE WHEN sf.is_live_session THEN 1 ELSE 0 END), 0)
91
- AS live_session_excluded_from_no_commit_activity_count
70
+ END AS abandoned_spend_share
92
71
  FROM session_flags sf
93
72
  JOIN session_spend ss USING (session_id)`)
94
73
  .get(...params);
@@ -100,14 +79,13 @@ export function getDeliveryMetrics(db, opts) {
100
79
  };
101
80
  return buildResponse(data, {
102
81
  claim_kind: "OBS_PROXY",
103
- metric_definition_version: "esf-1",
104
82
  n: data.commit_session_count,
105
83
  window: { from: opts.from, to: opts.to },
106
84
  qualification: {
107
85
  provisional_excluded: true,
108
86
  unpriced_turns: 0,
109
87
  claim_kinds_count: 1,
110
- note: "Commit SHA presence is an observed delivery proxy. No-commit activity means RECONCILED Bash/Edit/Write/NotebookEdit activity without an observed commit; LIVE sessions remain in total spend but are excluded from that activity cohort. Legacy abandoned_* fields are compatibility aliases, not an abandonment finding.",
88
+ note: "Commit SHA presence is an observed delivery proxy.",
111
89
  },
112
90
  ...(opts.workspaceId === null ? {} : { drilldown_ids: { workspace_id: opts.workspaceId } }),
113
91
  });
@@ -34,17 +34,11 @@ export function computeSessionDelivery(db, sessionId) {
34
34
  turns_to_first_commit = countRow.n;
35
35
  }
36
36
  const sessRow = db
37
- .prepare(`SELECT user_turn_count, state,
38
- EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = sessions.session_id
39
- AND te.tool_name IN ('Bash','Write','Edit','NotebookEdit')) AS has_activity
40
- FROM sessions WHERE session_id = ?`)
37
+ .prepare("SELECT user_turn_count, state FROM sessions WHERE session_id = ?")
41
38
  .get(sessionId);
42
39
  const user_turn_count = sessRow?.user_turn_count ?? 0;
43
40
  const state = sessRow?.state ?? "";
44
- const deep_abandoned = user_turn_count >= 10 &&
45
- firstCommitTs === null &&
46
- state === "RECONCILED" &&
47
- sessRow?.has_activity === 1;
41
+ const deep_abandoned = user_turn_count >= 10 && firstCommitTs === null && state === "RECONCILED";
48
42
  return { turns_to_first_commit, deep_abandoned };
49
43
  }
50
44
  // ---------------------------------------------------------------------------
@@ -89,9 +83,8 @@ export function getAbandonedSpendSplit(db, opts) {
89
83
  FROM tool_events te
90
84
  WHERE te.session_id = iws.session_id
91
85
  AND te.commit_sha IS NOT NULL
92
- ) AND s.state = 'RECONCILED' AS is_abandoned_session
86
+ ) AS is_abandoned_session
93
87
  FROM in_window_sessions iws
94
- JOIN sessions s USING (session_id)
95
88
  ),
96
89
  session_spend AS (
97
90
  SELECT session_id,
@@ -152,24 +145,15 @@ export function getClosureProxy(db, opts) {
152
145
  unresolved_count: 0,
153
146
  pending_count: 0,
154
147
  resolved_share: null,
155
- no_later_workspace_session_count: 0,
156
- later_workspace_session_count: 0,
157
- no_later_workspace_session_share: null,
158
148
  window_hours: windowHours,
159
149
  workspace_id: opts.workspaceId,
160
150
  });
161
151
  if (candidates.length === 0) {
162
152
  return buildResponse(emptyProxy(), {
163
153
  claim_kind: "EXPERIMENTAL",
164
- metric_definition_version: "esf-1",
165
154
  n: 0,
166
155
  window: { from: opts.now, to: opts.now },
167
- qualification: {
168
- provisional_excluded: false,
169
- unpriced_turns: 0,
170
- claim_kinds_count: 1,
171
- note: `${note} No later workspace session is an observation, not task resolution.`,
172
- },
156
+ qualification: { provisional_excluded: false, unpriced_turns: 0, claim_kinds_count: 1, note },
173
157
  ...(opts.workspaceId === null ? {} : { drilldown_ids: { workspace_id: opts.workspaceId } }),
174
158
  });
175
159
  }
@@ -220,23 +204,14 @@ export function getClosureProxy(db, opts) {
220
204
  unresolved_count,
221
205
  pending_count,
222
206
  resolved_share,
223
- no_later_workspace_session_count: resolved_count,
224
- later_workspace_session_count: unresolved_count,
225
- no_later_workspace_session_share: resolved_share,
226
207
  window_hours: windowHours,
227
208
  workspace_id: opts.workspaceId,
228
209
  };
229
210
  return buildResponse(proxy, {
230
211
  claim_kind: "EXPERIMENTAL",
231
- metric_definition_version: "esf-1",
232
212
  n: no_commit_session_count,
233
213
  window: { from: opts.now, to: opts.now },
234
- qualification: {
235
- provisional_excluded: false,
236
- unpriced_turns: 0,
237
- claim_kinds_count: 1,
238
- note: `${note} "Resolved" and "unresolved" are legacy aliases for no later workspace session and a later workspace session; neither establishes task resolution.`,
239
- },
214
+ qualification: { provisional_excluded: false, unpriced_turns: 0, claim_kinds_count: 1, note },
240
215
  ...(opts.workspaceId === null ? {} : { drilldown_ids: { workspace_id: opts.workspaceId } }),
241
216
  });
242
217
  }
@@ -37,19 +37,22 @@ export function getEfficiencyHeadroom(db, opts) {
37
37
  FROM turns
38
38
  WHERE ts >= ? AND ts < ? AND provisional = 0`)
39
39
  .get(from, to);
40
- const opportunities = db
41
- .prepare(`SELECT rec_id, modeled_savings_u_per_wk
42
- FROM recommendations
43
- WHERE state IN ('PROPOSED', 'ADOPTED') AND modeled_savings_u_per_wk IS NOT NULL
44
- ORDER BY rec_id`)
45
- .all();
46
40
  const headroom_u_per_wk = recs.sum_u ?? 0;
47
41
  const actual_u_per_wk = spend.actual_u;
48
- // Individual recommendation models overlap and their weekly horizon differs from
49
- // the trailing spend window, so a combined percentage would be misleading.
50
- const headroom_pct = null;
42
+ const openRecsExist = recs.open_total > 0;
43
+ // Null when: no spend (avoid /0 → ∞/NaN), or open recs exist but none carries a
44
+ // savings figure (an all-null modeled set is not a defensible ceiling).
45
+ let headroom_pct;
46
+ if (actual_u_per_wk === 0) {
47
+ headroom_pct = null;
48
+ }
49
+ else if (openRecsExist && recs.with_savings === 0) {
50
+ headroom_pct = null;
51
+ }
52
+ else {
53
+ headroom_pct = headroom_u_per_wk / actual_u_per_wk;
54
+ }
51
55
  const data = {
52
- opportunities,
53
56
  headroom_u_per_wk,
54
57
  actual_u_per_wk,
55
58
  headroom_pct,
@@ -59,14 +62,13 @@ export function getEfficiencyHeadroom(db, opts) {
59
62
  };
60
63
  return buildResponse(data, {
61
64
  claim_kind: "EXPERIMENTAL",
62
- metric_definition_version: "esf-1",
63
65
  n: recs.with_savings,
64
66
  window: { from, to },
65
67
  qualification: {
66
68
  provisional_excluded: true,
67
69
  unpriced_turns: 0,
68
70
  claim_kinds_count: 1,
69
- note: "Individual modeled weekly opportunities are not summed: recommendations can overlap and their weekly horizon differs from the trailing spend window.",
71
+ note: "Modeled headroom — if every open recommendation were applied; built from unvalidated per-detector fractions.",
70
72
  },
71
73
  });
72
74
  }
@@ -11,7 +11,6 @@
11
11
  *
12
12
  * SQL references: Data Model v2 §2 lines 261-275 / 303-314 / 329-334.
13
13
  */
14
- import { PREMIUM_MODEL_SQL } from "../../ingest/pricing.js";
15
14
  import { getQueryDb } from "../db-context.js";
16
15
  import { buildResponse } from "../envelope.js";
17
16
  // ---------------------------------------------------------------------------
@@ -137,7 +136,7 @@ export function listWorkspaceOutcomes() {
137
136
  const lrByWs = new Map(lrRows.map((r) => [r.workspace_id, r.linkage_rate]));
138
137
  const adherenceRows = db
139
138
  .prepare(`SELECT workspace_id, COUNT(*) AS n,
140
- AVG(CASE WHEN ${PREMIUM_MODEL_SQL} THEN 1.0 ELSE 0.0 END) AS premium_share
139
+ AVG(CASE WHEN model LIKE '%opus%' THEN 1.0 ELSE 0.0 END) AS premium_share
141
140
  FROM turns
142
141
  WHERE is_sidechain = 0 AND provisional = 0
143
142
  GROUP BY workspace_id`)