@dzhechkov/harness-core 0.8.36 → 0.8.37

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 (106) hide show
  1. package/.dz-manifest.json +195 -75
  2. package/README.md +235 -8
  3. package/dist/agentdb-index.d.ts +87 -7
  4. package/dist/agentdb-index.d.ts.map +1 -1
  5. package/dist/agentdb-index.js +416 -57
  6. package/dist/agentdb-index.js.map +1 -1
  7. package/dist/apply-leg.d.ts +19 -1
  8. package/dist/apply-leg.d.ts.map +1 -1
  9. package/dist/apply-leg.js +187 -36
  10. package/dist/apply-leg.js.map +1 -1
  11. package/dist/codex-rollouts.d.ts +118 -0
  12. package/dist/codex-rollouts.d.ts.map +1 -0
  13. package/dist/codex-rollouts.js +297 -0
  14. package/dist/codex-rollouts.js.map +1 -0
  15. package/dist/cost-ledger.d.ts +56 -4
  16. package/dist/cost-ledger.d.ts.map +1 -1
  17. package/dist/cost-ledger.js +176 -20
  18. package/dist/cost-ledger.js.map +1 -1
  19. package/dist/cross-family-control.d.ts +345 -0
  20. package/dist/cross-family-control.d.ts.map +1 -0
  21. package/dist/cross-family-control.js +802 -0
  22. package/dist/cross-family-control.js.map +1 -0
  23. package/dist/debt-ratchet.d.ts +53 -0
  24. package/dist/debt-ratchet.d.ts.map +1 -0
  25. package/dist/debt-ratchet.js +107 -0
  26. package/dist/debt-ratchet.js.map +1 -0
  27. package/dist/embedding-config.d.ts +42 -0
  28. package/dist/embedding-config.d.ts.map +1 -1
  29. package/dist/embedding-config.js +106 -10
  30. package/dist/embedding-config.js.map +1 -1
  31. package/dist/feature-adr-checkpoints.d.ts +6 -0
  32. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  33. package/dist/feature-adr-checkpoints.js +29 -0
  34. package/dist/feature-adr-checkpoints.js.map +1 -1
  35. package/dist/feature-adr-decision-recall.d.ts +2 -2
  36. package/dist/feature-adr-decision-recall.d.ts.map +1 -1
  37. package/dist/feature-adr-decision-recall.js +5 -3
  38. package/dist/feature-adr-decision-recall.js.map +1 -1
  39. package/dist/feature-adr-envelope.d.ts +96 -0
  40. package/dist/feature-adr-envelope.d.ts.map +1 -0
  41. package/dist/feature-adr-envelope.js +183 -0
  42. package/dist/feature-adr-envelope.js.map +1 -0
  43. package/dist/feature-adr-routing.d.ts +64 -0
  44. package/dist/feature-adr-routing.d.ts.map +1 -1
  45. package/dist/feature-adr-routing.js +122 -2
  46. package/dist/feature-adr-routing.js.map +1 -1
  47. package/dist/feature-adr-stage-canon.d.ts +79 -0
  48. package/dist/feature-adr-stage-canon.d.ts.map +1 -0
  49. package/dist/feature-adr-stage-canon.js +117 -0
  50. package/dist/feature-adr-stage-canon.js.map +1 -0
  51. package/dist/index.d.ts +19 -9
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +13 -5
  54. package/dist/index.js.map +1 -1
  55. package/dist/loop-blobs.generated.js +4 -4
  56. package/dist/loop-blobs.generated.js.map +1 -1
  57. package/dist/mutation-gate.d.ts +51 -0
  58. package/dist/mutation-gate.d.ts.map +1 -1
  59. package/dist/mutation-gate.js +295 -0
  60. package/dist/mutation-gate.js.map +1 -1
  61. package/dist/qe-bridge.d.ts.map +1 -1
  62. package/dist/qe-bridge.js +4 -2
  63. package/dist/qe-bridge.js.map +1 -1
  64. package/dist/qe-findings.d.ts +107 -0
  65. package/dist/qe-findings.d.ts.map +1 -0
  66. package/dist/qe-findings.js +417 -0
  67. package/dist/qe-findings.js.map +1 -0
  68. package/dist/recap.d.ts +1 -1
  69. package/dist/recap.d.ts.map +1 -1
  70. package/dist/recap.js +4 -2
  71. package/dist/recap.js.map +1 -1
  72. package/dist/round.d.ts +74 -1
  73. package/dist/round.d.ts.map +1 -1
  74. package/dist/round.js +112 -4
  75. package/dist/round.js.map +1 -1
  76. package/dist/run-records.d.ts +60 -0
  77. package/dist/run-records.d.ts.map +1 -1
  78. package/dist/run-records.js +244 -2
  79. package/dist/run-records.js.map +1 -1
  80. package/dist/score.d.ts +44 -1
  81. package/dist/score.d.ts.map +1 -1
  82. package/dist/score.js +78 -5
  83. package/dist/score.js.map +1 -1
  84. package/package.json +1 -1
  85. package/sbom.json +374 -74
  86. package/src/agentdb-index.ts +423 -60
  87. package/src/apply-leg.ts +187 -36
  88. package/src/codex-rollouts.ts +374 -0
  89. package/src/cost-ledger.ts +232 -24
  90. package/src/cross-family-control.ts +960 -0
  91. package/src/debt-ratchet.ts +143 -0
  92. package/src/embedding-config.ts +131 -10
  93. package/src/feature-adr-checkpoints.ts +29 -0
  94. package/src/feature-adr-decision-recall.ts +6 -4
  95. package/src/feature-adr-envelope.ts +242 -0
  96. package/src/feature-adr-routing.ts +139 -2
  97. package/src/feature-adr-stage-canon.ts +141 -0
  98. package/src/index.ts +60 -6
  99. package/src/loop-blobs.generated.ts +4 -4
  100. package/src/mutation-gate.ts +316 -0
  101. package/src/qe-bridge.ts +4 -2
  102. package/src/qe-findings.ts +463 -0
  103. package/src/recap.ts +10 -3
  104. package/src/round.ts +165 -6
  105. package/src/run-records.ts +282 -2
  106. package/src/score.ts +115 -6
package/src/apply-leg.ts CHANGED
@@ -190,8 +190,26 @@ export function probeRecallEngine(socketPath: string, timeoutMs = 1000): Promise
190
190
  * the cosine-fallback `scored` array are filtered to exclude `domain === 'apply-leg-probe'` unless
191
191
  * the request carried `probe: true` — a probe's own beacon still needs to reach ITS query, only a
192
192
  * REAL prompt must never see it.
193
+ *
194
+ * Bumped 10→11 (feature `embed-daemon-memory`, ADR-001 D1): the daemon now tries core's OWN cached
195
+ * embedder (`resolveAgentdbEmbedder`) first — one pipeline per process — building a private one only
196
+ * when core cannot hand one over; `ready` gained the ` embedder=<core-shared|own-fallback>` suffix
197
+ * (see `features/embed-daemon-memory/07_code_changes/change_manifest.md` for the full T3 note; this
198
+ * paragraph was missing from the bump history and is added here for the record, fix round 1).
199
+ *
200
+ * Bumped 11→12 (`embed-daemon-memory`, fix round 1 — F1/F2, Codex #1b/#3): (a) F1 — the own-fallback
201
+ * pipeline now reads the STORE's own dtype manifest before building (absent = fp32 legacy, a
202
+ * present-but-unrecognized value fails the request loudly instead of silently building fp32 against
203
+ * a q8 store); `ready`'s own-fallback branch gains ` dtype=<fp32|q8|error>`. (b) F2 — a core-shared
204
+ * STARTUP init failure no longer falls back to building an own pipeline (which risked a second live
205
+ * embedder once a later hybrid call succeeded): `embedderSource` stays `'core-shared'` and `embed`
206
+ * re-resolves `core.resolveAgentdbEmbedder` per request instead; `ready` names a startup failure
207
+ * inline as `core-shared (init failed: <msg>, will retry per request)`; `warmUpHybridEngine()` is
208
+ * skipped entirely in own-fallback mode. (c) `answerRecall`'s `embed(prompt)` call is now wrapped so
209
+ * either failure mode degrades to the SAME honest `cosine-fallback` reply shape as every other
210
+ * failure, never a bare protocol `{error}`.
193
211
  */
194
- export const APPLY_LEG_VERSION = 10;
212
+ export const APPLY_LEG_VERSION = 12;
195
213
 
196
214
  /**
197
215
  * Parse the `dz-apply-leg-version` stamp from a deployed helper file. Unlike
@@ -1027,6 +1045,18 @@ function socketAlive(path) {
1027
1045
  });
1028
1046
  }
1029
1047
 
1048
+ // ADR-001 (embed-daemon-memory, D1/T3): better-sqlite3 is resolved INDEPENDENTLY of the embedder now
1049
+ // — the cosine-fallback leg always needs it to read the mirror, regardless of whether the embedder
1050
+ // itself comes from core (embedder=core-shared) or this daemon's own pipeline (embedder=own-fallback).
1051
+ // Splitting it out of the old resolveDeps() lets the daemon start on core-shared alone when only
1052
+ // better-sqlite3 (not transformers) is locally resolvable.
1053
+ function resolveDatabase() {
1054
+ const req = createRequire(join(PROJECT, 'package.json'));
1055
+ return req('better-sqlite3');
1056
+ }
1057
+
1058
+ // own-fallback ONLY (ADR-001 D1): resolves the transformers module for the daemon's OWN pipeline,
1059
+ // built only when core has no resolveAgentdbEmbedder to share (no core module, or it errored).
1030
1060
  function resolveDeps() {
1031
1061
  const req = createRequire(join(PROJECT, 'package.json'));
1032
1062
  // agentdb >= 3.0.0-alpha depends on '@huggingface/transformers' (the '@xenova/transformers'
@@ -1054,7 +1084,7 @@ function resolveDeps() {
1054
1084
  if (transformers === undefined) {
1055
1085
  throw new Error('neither @huggingface/transformers nor @xenova/transformers could be resolved (' + String((lastErr && lastErr.message) || lastErr) + ')');
1056
1086
  }
1057
- return { Database: req('better-sqlite3'), transformers };
1087
+ return { transformers };
1058
1088
  }
1059
1089
 
1060
1090
  const cos = (a, b) => {
@@ -1119,17 +1149,151 @@ async function main() {
1119
1149
 
1120
1150
  const started = Date.now();
1121
1151
  const model = configuredModel();
1122
- let deps;
1152
+
1153
+ // better-sqlite3 is required regardless of which embedder answers a request (ADR-001, T3) — the
1154
+ // cosine-fallback leg always reads the mirror through it.
1155
+ let Database;
1123
1156
  try {
1124
- deps = resolveDeps();
1157
+ Database = resolveDatabase();
1125
1158
  } catch (err) {
1126
1159
  log('deps unavailable — not starting:', err?.message ?? err);
1127
1160
  process.exit(0); // never a hard failure: the hook degrades to silence
1128
1161
  }
1129
1162
 
1130
- const { pipeline } = await import(deps.transformers);
1131
- const extractor = await pipeline('feature-extraction', model);
1132
- const embed = async (text) => Array.from((await extractor(text, { pooling: 'mean', normalize: true })).data);
1163
+ // ADR-001 D1 (embed-daemon-memory): the daemon SHARES core's single per-process embedder and
1164
+ // builds a private pipeline only when core cannot hand one over AT ALL — no core module, or a core
1165
+ // build too old to export \`resolveAgentdbEmbedder\` (F2, fix round 1, Codex #3). A STARTUP init
1166
+ // FAILURE (core exists, has the export, but the call itself errored — offline model cache, a
1167
+ // corrupted manifest) is NEVER treated as "core unavailable": own-fallback must not be built in
1168
+ // that case, because a LATER hybrid call could succeed and stand up a SECOND, independent pipeline
1169
+ // in the same process — the exact "two live embedders" defect the review named. Instead
1170
+ // \`embedderSource\` STAYS \`'core-shared'\` and \`embed\` re-resolves \`core.resolveAgentdbEmbedder\`
1171
+ // on EVERY call: core's own per-process cache (agentdb-index.ts) makes a repeat call after SUCCESS
1172
+ // effectively free, and evicts its own entry on failure — so a transient startup failure can heal
1173
+ // on a later request without this daemon ever building an embedder of its own.
1174
+ //
1175
+ // The shared embedder is still PROBED EAGERLY, before \`ready\`, for the same reason the own
1176
+ // pipeline always was: the cosine-fallback leg must answer inside the hook budget on the FIRST
1177
+ // request too (parity AC-2: hybrid delayed by 3000 ms, reply < 2000 ms). MEASURED 2026-09-16
1178
+ // (change_manifest.md, deviation B): a LAZY resolve put the cold pipeline init (2000-3600 ms) on
1179
+ // the first fallback reply — 4/5 runs at 2063-2146 ms. The probe's OUTCOME only decides the
1180
+ // \`ready\`-log wording now (F2) — it never gates \`embedderSource\` or builds an own-fallback
1181
+ // pipeline. The starved-budget parity test that used to lean on a cold engine now injects
1182
+ // DZ_EMBED_HYBRID_DELAY_MS itself, so its premise holds by construction, not by cold timing.
1183
+ let embed;
1184
+ let embedderSource;
1185
+ let embedderReadyDetail = '';
1186
+ let ownFallbackDtypeLabel;
1187
+ const startupCore = await loadCoreModule();
1188
+ const coreHasSharedEmbedder = startupCore !== undefined && typeof startupCore.resolveAgentdbEmbedder === 'function';
1189
+ if (coreHasSharedEmbedder) {
1190
+ embedderSource = 'core-shared';
1191
+ embed = async (text) => {
1192
+ const shared = await startupCore.resolveAgentdbEmbedder(PROJECT);
1193
+ if (!shared || typeof shared.embed !== 'function' || 'error' in shared) {
1194
+ throw new Error((shared && shared.error) || 'resolveAgentdbEmbedder returned no embed()');
1195
+ }
1196
+ return shared.embed(text);
1197
+ };
1198
+ let startupProbe;
1199
+ try {
1200
+ startupProbe = await startupCore.resolveAgentdbEmbedder(PROJECT);
1201
+ } catch (err) {
1202
+ startupProbe = { error: err?.message ?? String(err) };
1203
+ }
1204
+ if (!startupProbe || typeof startupProbe.embed !== 'function' || 'error' in startupProbe) {
1205
+ const msg = (startupProbe && startupProbe.error) || 'resolveAgentdbEmbedder returned no embed()';
1206
+ log('core-shared embedder init failed at startup, will retry per request:', msg);
1207
+ embedderReadyDetail = \` (init failed: \${msg}, will retry per request)\`;
1208
+ }
1209
+ }
1210
+ if (embed === undefined) {
1211
+ // own-fallback: ONLY when core has no resolveAgentdbEmbedder to share at all (no core module, or
1212
+ // a fake/old core, as in test fixtures) — never as a reaction to a startup init error (above).
1213
+ let deps;
1214
+ try {
1215
+ deps = resolveDeps();
1216
+ } catch (err) {
1217
+ log('deps unavailable — not starting:', err?.message ?? err);
1218
+ process.exit(0); // never a hard failure: the hook degrades to silence
1219
+ }
1220
+ // F1 (fix round 1, Codex #1b): the daemon has no core to ask, so it reads the STORE's own dtype
1221
+ // manifest directly — an own pipeline built blindly at fp32 would silently compare vectors from
1222
+ // two different spaces against a q8 store. Absent manifest = fp32 (legacy — matches every store
1223
+ // predating this feature, same discipline as embedding-config.ts's readEmbedManifest). A manifest
1224
+ // that IS present but names neither known dtype is NEVER folded into that same fp32 case — this
1225
+ // daemon fails loudly for that request (embed() throws below, caught honestly by answerRecall)
1226
+ // rather than silently mixing dtype spaces.
1227
+ const manifestPath = join(PROJECT, '.dz', 'agentdb.db.embed-manifest.json');
1228
+ let ownDtype = 'fp32';
1229
+ let ownDtypeError;
1230
+ let manifestIsFile = false;
1231
+ try {
1232
+ manifestIsFile = statSync(manifestPath).isFile(); // a non-file at the sidecar path is "absent", as in core
1233
+ } catch {
1234
+ manifestIsFile = false;
1235
+ }
1236
+ if (manifestIsFile) {
1237
+ try {
1238
+ const raw = JSON.parse(readFileSync(manifestPath, 'utf-8'))?.dtype;
1239
+ if (raw === 'fp32' || raw === 'q8') {
1240
+ ownDtype = raw;
1241
+ } else if (raw !== undefined) {
1242
+ ownDtypeError = String(raw);
1243
+ }
1244
+ } catch (err) {
1245
+ // Lead delta after Codex r2 (HIGH): a manifest that EXISTS but cannot be parsed is NOT
1246
+ // "absent" — refusing beats guessing fp32 over a q8 store (same rule as core's readError).
1247
+ ownDtypeError = 'unreadable manifest: ' + (err?.message ?? String(err));
1248
+ }
1249
+ }
1250
+ embedderSource = 'own-fallback';
1251
+ if (ownDtypeError !== undefined) {
1252
+ log('manifest dtype unknown:', ownDtypeError);
1253
+ ownFallbackDtypeLabel = 'error';
1254
+ const dtypeErrMsg = \`manifest dtype unknown: \${ownDtypeError}\`;
1255
+ embed = async () => { throw new Error(dtypeErrMsg); };
1256
+ } else {
1257
+ ownFallbackDtypeLabel = ownDtype;
1258
+ const { pipeline } = await import(deps.transformers);
1259
+ const extractor = await pipeline('feature-extraction', model, ownDtype === 'q8' ? { dtype: 'q8' } : {});
1260
+ embed = async (text) => Array.from((await extractor(text, { pooling: 'mean', normalize: true })).data);
1261
+ }
1262
+ }
1263
+
1264
+ // AM-1 (fix round 1): warm resolveAgentdbEmbedder — cached PER PROCESS since db1521ba (cold
1265
+ // ~2-3.6 s, warm ~1 ms, MEASURED, see the manifest's T8/AM-1 discussion) — OFF the request path,
1266
+ // so the first REAL \`op: recall\` is not the one that pays the cold init. Fired fire-and-forget as
1267
+ // early as main() can (moved up from right-before-listen, T3/embed-daemon-memory: every ms of
1268
+ // extra head start matters against a multi-second cold cost — see change_manifest.md), never
1269
+ // awaited by startup: this is a best-effort head start, not a guarantee — a request landing in
1270
+ // the window before it completes still pays the (possibly-partial, since it JOINS the same
1271
+ // in-flight resolveAgentdbEmbedder promise, D1) cold cost, and a warm-up failure (no core module,
1272
+ // engine error) is silently swallowed — never-block applies to startup exactly as it does to a
1273
+ // request. Measured: the slowest cold resolveAgentdbEmbedder init observed in this environment
1274
+ // was 3653 ms (T8 log, 2026-09-14) — 10 s leaves a wide margin without risking an unbounded
1275
+ // warm-up hang.
1276
+ const WARMUP_TIMEOUT_MS = 10_000;
1277
+ async function warmUpHybridEngine() {
1278
+ const core = await loadCoreModule();
1279
+ if (core === undefined) return;
1280
+ const guard = new Promise((resolve) => {
1281
+ const t = setTimeout(resolve, WARMUP_TIMEOUT_MS);
1282
+ t.unref?.();
1283
+ });
1284
+ // An empty-string query still exercises the FULL semantic leg (embed + engine.search), which is
1285
+ // exactly what needs warming; recallHybrid degrades any error inside it honestly, so nothing
1286
+ // here needs its own try/catch beyond the outer .catch(() => {}) at the call site below.
1287
+ await Promise.race([core.recallHybrid(PROJECT, '', { limit: 1, mode: 'hook', deferExposures: true }), guard]);
1288
+ }
1289
+ // Fire-and-forget, never awaited — main() proceeds immediately regardless of warm-up outcome.
1290
+ // F2 (fix round 1, Codex #3): warm-up specifically primes the HYBRID leg's own use of
1291
+ // resolveAgentdbEmbedder — pointless when this daemon has no core embedder to share (own-fallback
1292
+ // has no core module, or a fake/old core in tests) AND risks standing up a SECOND, unrelated
1293
+ // pipeline via whatever recallHybrid does internally in that case. Skipped entirely in own-fallback.
1294
+ if (embedderSource === 'core-shared') {
1295
+ warmUpHybridEngine().catch(() => {});
1296
+ }
1133
1297
 
1134
1298
  // READ-ONLY. This process must never be the writer that tears the file for a concurrent reader.
1135
1299
  const dbPath = join(PROJECT, '.dz', 'agentdb.db');
@@ -1137,7 +1301,7 @@ async function main() {
1137
1301
 
1138
1302
  function loadPatterns() {
1139
1303
  if (!existsSync(dbPath)) return [];
1140
- const db = new deps.Database(dbPath, { readonly: true, fileMustExist: true });
1304
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
1141
1305
  try {
1142
1306
  const ph = DZ_TASK_TYPES.map(() => '?').join(',');
1143
1307
  // NOTE: the vector mirror carries no \`domain\` — that column lives in the lexical store. An
@@ -1249,36 +1413,22 @@ async function main() {
1249
1413
  patternsAt = Date.now();
1250
1414
  }
1251
1415
  if (patterns.length === 0) return { hits: [], engine: 'cosine-fallback', reason: hybrid.reason };
1252
- const qv = await embed(prompt);
1416
+ // F1/F2 (fix round 1): \`embed()\` can now THROW honestly (own-fallback with an unrecognized
1417
+ // manifest dtype, F1; core-shared re-resolution failing on this exact request, F2) — caught here
1418
+ // so it degrades to the SAME honest cosine-fallback shape every other failure gets, never a bare
1419
+ // protocol {error} reply reaching the socket handler's outer catch (the AM-3 defect class).
1420
+ let qv;
1421
+ try {
1422
+ qv = await embed(prompt);
1423
+ } catch (err) {
1424
+ return { hits: [], engine: 'cosine-fallback', reason: \`embedder unavailable: \${err?.message ?? err}\` };
1425
+ }
1253
1426
  const scored = patterns.map((p) => ({ dzId: p.dzId, pattern: p.pattern, score: cos(qv, p.vec), domain: p.domain, ...(p.quarantined ? { quarantined: true } : {}) }));
1254
1427
  scored.sort((a, b) => b.score - a.score);
1255
1428
  // AM-3: same domain exclusion as the hybrid leg, applied before slicing for the same reason.
1256
1429
  return { hits: filterProbeHits(scored, probe).slice(0, limit), engine: 'cosine-fallback', reason: hybrid.reason };
1257
1430
  }
1258
1431
 
1259
- // AM-1 (fix round 1): warm resolveAgentdbEmbedder — cached PER PROCESS since db1521ba (cold
1260
- // ~2-3.6 s, warm ~1 ms, MEASURED, see the manifest's T8/AM-1 discussion) — OFF the request path,
1261
- // so the first REAL \`op: recall\` is not the one that pays the cold init. Fired fire-and-forget
1262
- // right before \`listen()\` below, never awaited by startup: this is a best-effort head start, not
1263
- // a guarantee — a request landing in the few-hundred-ms window before it completes still pays the
1264
- // cold cost exactly as before this amendment, and a warm-up failure (no core module, engine
1265
- // error) is silently swallowed — never-block applies to startup exactly as it does to a request.
1266
- // Measured: the slowest cold resolveAgentdbEmbedder init observed in this environment was 3653 ms
1267
- // (T8 log, 2026-09-14) — 10 s leaves a wide margin without risking an unbounded warm-up hang.
1268
- const WARMUP_TIMEOUT_MS = 10_000;
1269
- async function warmUpHybridEngine() {
1270
- const core = await loadCoreModule();
1271
- if (core === undefined) return;
1272
- const guard = new Promise((resolve) => {
1273
- const t = setTimeout(resolve, WARMUP_TIMEOUT_MS);
1274
- t.unref?.();
1275
- });
1276
- // An empty-string query still exercises the FULL semantic leg (embed + engine.search), which is
1277
- // exactly what needs warming; recallHybrid degrades any error inside it honestly, so nothing
1278
- // here needs its own try/catch beyond the outer .catch(() => {}) at the call site below.
1279
- await Promise.race([core.recallHybrid(PROJECT, '', { limit: 1, mode: 'hook', deferExposures: true }), guard]);
1280
- }
1281
-
1282
1432
  let idleTimer;
1283
1433
  let lastActivityAt = Date.now();
1284
1434
  const touch = () => {
@@ -1353,9 +1503,6 @@ async function main() {
1353
1503
 
1354
1504
  for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => shutdown(0));
1355
1505
 
1356
- // AM-1: fire-and-forget, never awaited — bind proceeds immediately regardless of warm-up outcome.
1357
- warmUpHybridEngine().catch(() => {});
1358
-
1359
1506
  // FR-3 ("absence of a receipt is not success"): \`ready\` is printed ONLY after \`listen\`'s callback
1360
1507
  // AND a fresh \`existsSync(SOCKET)\` both confirm the socket file is actually on disk — a caller
1361
1508
  // that greps stderr for "ready" must never see it for a socket that silently failed to bind.
@@ -1397,7 +1544,11 @@ async function main() {
1397
1544
  return bindFailed(\`could not publish socket pointer: \${err?.message ?? err}\`);
1398
1545
  }
1399
1546
  }
1400
- log(\`ready: \${patterns.length} pattern vectors, model \${model}, socket \${SOCKET}\`);
1547
+ // F1/F2 (fix round 1): own-fallback names ITS resolved dtype (\`dtype=<fp32|q8|error>\`, F1); a
1548
+ // core-shared daemon that failed its startup probe names that too, inline (\`(init failed: …,
1549
+ // will retry per request)\`, F2) — both make the honest state observable from \`ready\` alone.
1550
+ const embedderReadyLabel = embedderSource === 'own-fallback' ? \`\${embedderSource} dtype=\${ownFallbackDtypeLabel}\` : \`\${embedderSource}\${embedderReadyDetail}\`;
1551
+ log(\`ready: \${patterns.length} pattern vectors, model \${model}, socket \${SOCKET} embedder=\${embedderReadyLabel}\`);
1401
1552
  touch();
1402
1553
  });
1403
1554
  server.on('error', (err) => bindFailed(err?.message ?? String(err)));
@@ -0,0 +1,374 @@
1
+ /**
2
+ * A pure reader for Codex CLI rollout logs (feature `measurement-integrity`, ADR-001 D3).
3
+ *
4
+ * A `dz feature-adr-record --kind ledger` row for a Codex coder/reviewer stage carries
5
+ * `tokens: null` in 130 of 156 recorded rows (Step 0, 2026-09-16) even though the spend is sitting
6
+ * right there on disk: Codex writes one JSONL file per session at
7
+ * `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl`, and nothing in the pipeline reads it. The
8
+ * pipeline dispatches Codex without an explicit session id (`codex exec -C <repo> -m <id> …`), so the
9
+ * only way to join a ledger row to the rollout that produced it is a WINDOW match: the stage's own
10
+ * start/end time, its `cwd`, and its model.
11
+ *
12
+ * PURE — this module never opens `~/.codex/sessions` itself; the CLI reads the files and hands their
13
+ * TEXT to {@link parseCodexRollout}. It must never gain a `node:fs` import (the `core-boundary`
14
+ * ratchet, `test/core-boundary.test.ts`, pins the current file/import count).
15
+ *
16
+ * ## A measured schema correction (read before touching the parser)
17
+ *
18
+ * Step 0's assessment described the usage record as `type: "token_count"`, keyed
19
+ * `payload.info.total_token_usage`. A live probe of this machine's `~/.codex/sessions` (2026-09-16,
20
+ * `cli_version: "0.154.0"`, every rollout from the last two days) found NO such record — the CURRENT
21
+ * shape is `type: "token_usage_record"`, keyed `payload.usage`, with the same five sub-fields
22
+ * (`input_tokens`, `cached_input_tokens`, `output_tokens`, `reasoning_output_tokens`,
23
+ * `total_tokens`). The model id lives on `type: "turn_context"`'s `payload.model` (not on
24
+ * `session_meta`, as Step 0 assumed), and `cwd` is carried by BOTH `session_meta.payload.cwd` and
25
+ * `turn_context.payload.cwd`. Rather than build against a shape that no longer exists on this
26
+ * machine, {@link parseCodexRollout} accepts BOTH the documented legacy shape and the measured
27
+ * current one — Codex CLI versions drift the schema (C-2: this module depends on no version beyond
28
+ * the fields it reads), and a reader that understands only a shape nothing on disk still emits would
29
+ * fail FR-5 at the exact thing it exists to fix.
30
+ *
31
+ * @packageDocumentation
32
+ */
33
+
34
+ interface RawRecord {
35
+ readonly type?: unknown;
36
+ readonly timestamp?: unknown;
37
+ readonly ts?: unknown;
38
+ readonly payload?: unknown;
39
+ }
40
+
41
+ function isRecord(v: unknown): v is Record<string, unknown> {
42
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
43
+ }
44
+
45
+ function nonEmptyString(v: unknown): string | null {
46
+ return typeof v === 'string' && v.length > 0 ? v : null;
47
+ }
48
+
49
+ function finiteNonNegative(v: unknown): number {
50
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : 0;
51
+ }
52
+
53
+ /** Epoch ms from a record's own `timestamp` (current schema) or `ts` (legacy/defensive), or `null`. */
54
+ function recordTimeMs(rec: Record<string, unknown>): number | null {
55
+ const raw = rec['timestamp'] ?? rec['ts'];
56
+ if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
57
+ if (typeof raw === 'string') {
58
+ const ms = Date.parse(raw);
59
+ return Number.isFinite(ms) ? ms : null;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function isoOrNull(ms: number | null): string | null {
65
+ if (ms === null || !Number.isFinite(ms) || Math.abs(ms) > 8.64e15) return null;
66
+ try {
67
+ return new Date(ms).toISOString();
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ export interface CodexRolloutTotals {
74
+ readonly input: number;
75
+ readonly cachedInput: number;
76
+ readonly output: number;
77
+ readonly reasoning: number;
78
+ readonly total: number;
79
+ }
80
+
81
+ /**
82
+ * measurement-integrity fix-round-1/F5 (Codex r1 HIGH #5): one TURN of a session — the span between
83
+ * one `turn_context` record and the next (or the file's last record, for the final turn). A turn
84
+ * carries its OWN model/cwd (from ITS `turn_context`) and, when a usage-bearing record (`token_count`
85
+ * / `token_usage_record`) was seen while this turn was current, that record's totals — `null` when no
86
+ * such record fell inside this turn's interval (nothing to attribute to it).
87
+ */
88
+ export interface CodexRolloutTurn {
89
+ readonly model: string | null;
90
+ readonly cwd: string | null;
91
+ readonly startedAt: string | null;
92
+ readonly endedAt: string | null;
93
+ readonly totals: CodexRolloutTotals | null;
94
+ }
95
+
96
+ export interface CodexRollout {
97
+ readonly id: string;
98
+ readonly cwd: string | null;
99
+ readonly model: string | null;
100
+ /** ISO, or `null` when no record in the file carried a parseable timestamp. */
101
+ readonly startedAt: string | null;
102
+ readonly endedAt: string | null;
103
+ readonly totals: CodexRolloutTotals;
104
+ /** measurement-integrity fix-round-1/F5: `'turn'` when the file carried at least one `turn_context`
105
+ * record (the measured current schema always does) — {@link matchCodexRollouts} then matches at
106
+ * TURN granularity, never against this whole session's wide interval. `'session'` when the schema
107
+ * gave no turn boundaries at all (the legacy shape Step 0 documented) — matching honestly falls
108
+ * back to the whole-session interval, and that fact travels with the result rather than being
109
+ * silently assumed away. */
110
+ readonly granularity: 'turn' | 'session';
111
+ /** turns whose open or close boundary carried no timestamp — reported, never matched. */
112
+ readonly unmatchableTurns: number;
113
+ /** Empty when `granularity === 'session'`. */
114
+ readonly turns: readonly CodexRolloutTurn[];
115
+ }
116
+
117
+ export interface CodexRolloutParseError {
118
+ readonly error: string;
119
+ }
120
+
121
+ /** Pull `{input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens, total_tokens}`
122
+ * (both schemas use these five field names) out of a usage-bearing sub-object. */
123
+ function totalsFrom(usage: Record<string, unknown>): CodexRolloutTotals {
124
+ return {
125
+ input: finiteNonNegative(usage['input_tokens']),
126
+ cachedInput: finiteNonNegative(usage['cached_input_tokens']),
127
+ output: finiteNonNegative(usage['output_tokens']),
128
+ reasoning: finiteNonNegative(usage['reasoning_output_tokens']),
129
+ total: finiteNonNegative(usage['total_tokens']),
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Parse ONE rollout file's full text into a {@link CodexRollout}. Pure, never-throws; a corrupt line
135
+ * is skipped exactly the way `extractCostSamples` (`cost-ledger.ts`) skips one.
136
+ *
137
+ * `fileName`, when given, is used ONLY as a last-resort `id` source (the `rollout-<ts>-<uuid>.jsonl`
138
+ * name's own uuid) when no `session_meta` record carried one — never trusted over the file's own
139
+ * content.
140
+ */
141
+ export function parseCodexRollout(text: string, fileName?: string): CodexRollout | CodexRolloutParseError {
142
+ if (typeof text !== 'string' || text.trim().length === 0) {
143
+ return { error: 'empty rollout text' };
144
+ }
145
+
146
+ let id: string | null = null;
147
+ let cwd: string | null = null;
148
+ let sessionMetaModel: string | null = null;
149
+ let turnContextModel: string | null = null;
150
+ let firstMs: number | null = null;
151
+ let lastMs: number | null = null;
152
+ let lastTotals: CodexRolloutTotals | null = null;
153
+ let sawAnyRecord = false;
154
+
155
+ // measurement-integrity fix-round-1/F5 (Codex r1 HIGH #5): each `turn_context` record OPENS a new
156
+ // turn, in file order. `open` is the turn currently being built; `turns` collects CLOSED ones. A
157
+ // turn closes when the NEXT `turn_context` is seen (its `endedAt` is that boundary's own
158
+ // timestamp) or, for the LAST open turn, at end-of-file (`endedAt` = the last record's timestamp).
159
+ // A usage-bearing record is attached to whichever turn is open at its own timestamp — `null` stays
160
+ // on a turn that never saw one, so a caller can tell "nothing to attribute here" from "attributed
161
+ // zero".
162
+ // Lead delta after Codex r2 (#5 PARTIAL, new HIGH #1): a turn's totals are the DELTA of the
163
+ // session-cumulative usage between its open and close (the record's own usage counter is
164
+ // cumulative for the session — assigning the last cumulative total to a turn made the second turn
165
+ // carry the first one's tokens). `startedMs: null` marks a turn opened by a `turn_context` WITHOUT
166
+ // a timestamp: it still closes the previous turn (so no usage can leak into it) but can never be
167
+ // matched to a window — the rollout reports it under `unmatchableTurns`.
168
+ interface OpenTurn { model: string | null; cwd: string | null; startedMs: number | null; baseline: CodexRolloutTotals | null; totals: CodexRolloutTotals | null }
169
+ const closedTurns: CodexRolloutTurn[] = [];
170
+ let open: OpenTurn | null = null;
171
+
172
+ let unmatchableTurns = 0;
173
+ const closeOpenTurn = (endMs: number | null): void => {
174
+ if (open === null) return;
175
+ if (open.startedMs === null || endMs === null) unmatchableTurns += 1;
176
+ closedTurns.push({
177
+ model: open.model,
178
+ cwd: open.cwd,
179
+ startedAt: open.startedMs === null ? null : isoOrNull(open.startedMs),
180
+ endedAt: endMs === null ? null : isoOrNull(endMs),
181
+ totals: open.totals,
182
+ });
183
+ };
184
+ const deltaTotals = (now: CodexRolloutTotals, base: CodexRolloutTotals | null): CodexRolloutTotals => {
185
+ if (base === null) return now;
186
+ const d = (a: number, b: number): number => (a - b >= 0 ? a - b : a); // a counter that went DOWN is per-record, not cumulative
187
+ return { input: d(now.input, base.input), cachedInput: d(now.cachedInput, base.cachedInput), output: d(now.output, base.output), reasoning: d(now.reasoning, base.reasoning), total: d(now.total, base.total) };
188
+ };
189
+
190
+ for (const line of text.split('\n')) {
191
+ if (line.length === 0) continue;
192
+ let rec: unknown;
193
+ try {
194
+ rec = JSON.parse(line);
195
+ } catch {
196
+ continue; // corrupt line — skip, never throw
197
+ }
198
+ if (!isRecord(rec)) continue;
199
+ sawAnyRecord = true;
200
+
201
+ const ms = recordTimeMs(rec);
202
+ if (ms !== null) {
203
+ firstMs = firstMs === null ? ms : Math.min(firstMs, ms);
204
+ lastMs = lastMs === null ? ms : Math.max(lastMs, ms);
205
+ }
206
+
207
+ const type = rec['type'];
208
+ const payload = isRecord(rec['payload']) ? rec['payload'] : null;
209
+ if (payload === null) continue;
210
+
211
+ if (type === 'session_meta') {
212
+ if (id === null) id = nonEmptyString(payload['session_id']) ?? nonEmptyString(payload['id']);
213
+ if (cwd === null) cwd = nonEmptyString(payload['cwd']);
214
+ // Step 0's documented (legacy, not observed live on this machine) shape put `model` directly on
215
+ // `session_meta` — accepted here too, but `turnContextModel` always wins at the end (below)
216
+ // since that is what the measured current schema actually carries.
217
+ if (sessionMetaModel === null) sessionMetaModel = nonEmptyString(payload['model']);
218
+ } else if (type === 'turn_context') {
219
+ if (turnContextModel === null) turnContextModel = nonEmptyString(payload['model']);
220
+ if (cwd === null) cwd = nonEmptyString(payload['cwd']);
221
+ // Close the previous open turn AT this boundary (even when the boundary has no timestamp —
222
+ // the previous turn must stop absorbing usage), then open the new one.
223
+ closeOpenTurn(ms);
224
+ open = { model: nonEmptyString(payload['model']), cwd: nonEmptyString(payload['cwd']) ?? cwd, startedMs: ms, baseline: lastTotals, totals: null };
225
+ }
226
+
227
+ // Legacy shape (Step 0's documented one, not observed live on this machine 2026-09-16):
228
+ // `type: "token_count"`, `payload.info.total_token_usage`.
229
+ if (type === 'token_count') {
230
+ const info = isRecord(payload['info']) ? payload['info'] : null;
231
+ const usage = info !== null && isRecord(info['total_token_usage']) ? info['total_token_usage'] : null;
232
+ if (usage !== null) {
233
+ const t = totalsFrom(usage);
234
+ if (open !== null) open.totals = deltaTotals(t, open.baseline);
235
+ lastTotals = t;
236
+ }
237
+ }
238
+ // Current shape (measured live, cli_version 0.154.0): `type: "token_usage_record"`,
239
+ // `payload.usage`.
240
+ if (type === 'token_usage_record') {
241
+ const usage = isRecord(payload['usage']) ? payload['usage'] : null;
242
+ if (usage !== null) {
243
+ const t = totalsFrom(usage);
244
+ if (open !== null) open.totals = deltaTotals(t, open.baseline);
245
+ lastTotals = t;
246
+ }
247
+ }
248
+ }
249
+ if (open !== null && lastMs !== null) closeOpenTurn(lastMs);
250
+
251
+ if (!sawAnyRecord) return { error: 'no parseable JSON lines in rollout text' };
252
+ if (id === null) {
253
+ // Last resort: the uuid embedded in `rollout-<ts>-<uuid>.jsonl` — never invented, only read back.
254
+ // A plain "greedy dash" regex would stop at the uuid's OWN internal dashes (its 8-4-4-4-12 hex
255
+ // groups), so this matches the canonical uuid shape explicitly rather than "everything after the
256
+ // last dash".
257
+ const m = typeof fileName === 'string'
258
+ ? /([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.jsonl$/.exec(fileName)
259
+ : null;
260
+ id = m !== null ? (m[1] ?? null) : null;
261
+ }
262
+ if (id === null) return { error: 'no session_meta record and no id in fileName — cannot identify this rollout' };
263
+ if (lastTotals === null) {
264
+ return { error: 'no token_count or token_usage_record entry — nothing to attribute' };
265
+ }
266
+
267
+ return {
268
+ id,
269
+ cwd,
270
+ model: turnContextModel ?? sessionMetaModel,
271
+ startedAt: isoOrNull(firstMs),
272
+ endedAt: isoOrNull(lastMs),
273
+ totals: lastTotals,
274
+ granularity: closedTurns.length > 0 ? 'turn' : 'session',
275
+ unmatchableTurns,
276
+ turns: closedTurns,
277
+ };
278
+ }
279
+
280
+ export type CodexRolloutMatch =
281
+ | { readonly status: 'none' }
282
+ | { readonly status: 'one'; readonly rollout: CodexRollout }
283
+ | { readonly status: 'ambiguous'; readonly candidates: readonly CodexRollout[] };
284
+
285
+ export interface CodexRolloutMatchWindow {
286
+ /** ISO instant — the window's lower bound. */
287
+ readonly from: string;
288
+ /** ISO instant — the window's upper bound. */
289
+ readonly to: string;
290
+ /** Exact match against {@link CodexRollout.cwd}, when given. */
291
+ readonly cwd?: string;
292
+ /** Exact match against {@link CodexRollout.model}, when given. */
293
+ readonly model?: string;
294
+ }
295
+
296
+ /**
297
+ * measurement-integrity fix-round-1/F5 (Codex r1 HIGH #5): every candidate window `matchCodexRollouts`
298
+ * may attribute spend to, at the SHARPEST granularity `parseCodexRollout` could recover from the
299
+ * file. For a `granularity: 'turn'` rollout this is one candidate PER TURN THAT ACTUALLY CARRIES
300
+ * USAGE (a turn nothing was ever attributed to yields no candidate — there is nothing honest to
301
+ * report for it); for `granularity: 'session'` it is exactly one candidate, the whole file, exactly
302
+ * as this reader behaved before this fix.
303
+ *
304
+ * This is the fix for the CRITICAL scenario the Codex review named: the OLD matcher tested the
305
+ * whole session's `[startedAt, endedAt]` against the query window, so ANY brief overlap with that wide
306
+ * interval could attribute an entire multi-turn session's cumulative spend (and, potentially, another
307
+ * turn's DIFFERENT model) to one stage. Scoping candidates to turns means two turns of the SAME
308
+ * session that only one of them overlaps the window can no longer collide — and two turns that BOTH
309
+ * overlap it correctly produce two candidates, which the caller below turns into `ambiguous` rather
310
+ * than an arbitrary pick (this is also where "the model of every usage-bearing turn matching a window
311
+ * must agree" ends up enforced: two turns with different models can only both match by being two
312
+ * SEPARATE candidates, which is ambiguous by construction — there is no path where a mismatch is
313
+ * silently resolved to one of them).
314
+ */
315
+ function candidateViewsOf(r: CodexRollout): readonly CodexRollout[] {
316
+ if (r.granularity === 'session') return [r];
317
+ const out: CodexRollout[] = [];
318
+ for (const turn of r.turns) {
319
+ if (turn.totals === null) continue; // nothing was ever attributed to this turn — not a candidate
320
+ out.push({
321
+ id: r.id,
322
+ unmatchableTurns: r.unmatchableTurns,
323
+ cwd: turn.cwd,
324
+ model: turn.model,
325
+ startedAt: turn.startedAt,
326
+ endedAt: turn.endedAt,
327
+ totals: turn.totals,
328
+ granularity: 'turn',
329
+ turns: [turn],
330
+ });
331
+ }
332
+ return out;
333
+ }
334
+
335
+ /**
336
+ * Which candidate VIEWS (session-level, or — per {@link candidateViewsOf} — turn-level whenever the
337
+ * schema recovered turn boundaries) have an interval that OVERLAPS the given `[from, to]` window
338
+ * (never nearest-in-time — ADR-001 D3 rejects "closest by clock" because two reviews back to back
339
+ * would attribute one's spend to the other). A candidate with no parseable timestamps never matches —
340
+ * an unattributable interval is not a wildcard.
341
+ *
342
+ * `0` matches → `{status:'none'}`. `1` → `{status:'one', rollout}`. `>1` → `{status:'ambiguous',
343
+ * candidates}` — NEVER an arbitrary pick of "the first" (NFR-3). `>1` also covers the case where two
344
+ * DIFFERENT turns (of the same or different rollouts) overlap the window with different models — that
345
+ * disagreement can never resolve to a lone `'one'`, it always surfaces as `'ambiguous'`.
346
+ */
347
+ export function matchCodexRollouts(
348
+ rollouts: readonly CodexRollout[],
349
+ window: CodexRolloutMatchWindow,
350
+ ): CodexRolloutMatch {
351
+ const fromMs = Date.parse(window.from);
352
+ const toMs = Date.parse(window.to);
353
+ if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs > toMs) return { status: 'none' };
354
+
355
+ const candidates: CodexRollout[] = [];
356
+ for (const r of rollouts) {
357
+ for (const view of candidateViewsOf(r)) {
358
+ if (view.startedAt === null || view.endedAt === null) continue;
359
+ const startMs = Date.parse(view.startedAt);
360
+ const endMs = Date.parse(view.endedAt);
361
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) continue;
362
+ // Lead delta after Codex r2 (#5): the turn must START inside the window — a turn that merely
363
+ // brushes the window's edge (any-overlap) is exactly how a neighbouring dispatch's turn leaks in.
364
+ if (startMs < fromMs || startMs > toMs) continue;
365
+ if (window.cwd !== undefined && view.cwd !== window.cwd) continue;
366
+ if (window.model !== undefined && view.model !== window.model) continue;
367
+ candidates.push(view);
368
+ }
369
+ }
370
+
371
+ if (candidates.length === 0) return { status: 'none' };
372
+ if (candidates.length === 1) return { status: 'one', rollout: candidates[0]! };
373
+ return { status: 'ambiguous', candidates };
374
+ }