@dzhechkov/harness-core 0.8.33 → 0.8.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/apply-leg.ts CHANGED
@@ -28,9 +28,83 @@
28
28
  */
29
29
 
30
30
  import { existsSync, readFileSync } from 'node:fs';
31
+ import { connect as netConnect } from 'node:net';
31
32
  import { join } from 'node:path';
32
33
  import { hookCommandsOf } from './managed-hooks.js';
33
34
 
35
+ /**
36
+ * FR-6 (feature `hook-recall-hybrid-parity`, ADR-001 C-4): send ONE `op: recall` probe to a LIVE
37
+ * embed daemon socket and report the `engine` it answers with (`'hybrid'` | `'cosine-fallback'`) —
38
+ * `dz doctor` prints this so an operator can SEE which engine is actually serving prompts, rather
39
+ * than trusting the daemon's mere presence. Honest-degrade contract, matching every other doctor
40
+ * probe: a non-socket path (e.g. a plain file, as every non-live doctor fixture in this repo uses),
41
+ * a connection error, an unparsable reply, or a timeout all resolve to `undefined` — NEVER a thrown
42
+ * error, and never distinguishable from "no daemon" in the caller's output (the existing "socket
43
+ * present/absent" line already carries that half of the truth).
44
+ *
45
+ * `timeoutMs` defaults to 1000 ms — comfortably above the documented `HOOK_RECALL_BUDGET_MS` default
46
+ * (500 ms): under that default, a cold `recallHybrid` semantic leg routinely exceeds the budget in
47
+ * this environment (MEASURED — see the manifest's NFR-1 discussion), so the daemon's OWN answer
48
+ * time is closer to ~500-550 ms than to the socket round-trip cost alone; a shorter probe timeout
49
+ * would silently miss a live, correctly-answering daemon and report no engine at all.
50
+ *
51
+ * AM-8 (fix round 1): lives HERE, not in `operations.ts` — this module already owns the daemon's
52
+ * wire protocol (`recallHookSource`/`embedDaemonSource`'s generated `op: recall` handshake) and its
53
+ * socket-path resolution; `operations.ts`'s `runDoctor` reaches it via a dynamic `import()`
54
+ * (matching its existing `embed-socket-path.js` import one line above the call site) rather than
55
+ * duplicating a second, independent `node:net` IO surface in a file whose job is orchestration, not
56
+ * protocol.
57
+ */
58
+ export function probeRecallEngine(socketPath: string, timeoutMs = 1000): Promise<string | undefined> {
59
+ return new Promise((resolvePromise) => {
60
+ let settled = false;
61
+ const done = (v: string | undefined): void => {
62
+ if (settled) return;
63
+ settled = true;
64
+ try {
65
+ sock.destroy();
66
+ } catch {
67
+ /* already gone */
68
+ }
69
+ resolvePromise(v);
70
+ };
71
+ let sock: ReturnType<typeof netConnect>;
72
+ try {
73
+ sock = netConnect(socketPath);
74
+ } catch {
75
+ resolvePromise(undefined);
76
+ return;
77
+ }
78
+ const timer = setTimeout(() => done(undefined), timeoutMs);
79
+ timer.unref?.();
80
+ let buf = '';
81
+ sock.on('connect', () => {
82
+ try {
83
+ sock.write(`${JSON.stringify({ op: 'recall', prompt: 'dz doctor probe', limit: 1 })}\n`);
84
+ } catch {
85
+ done(undefined);
86
+ }
87
+ });
88
+ sock.on('data', (chunk: Buffer) => {
89
+ buf += chunk.toString('utf-8');
90
+ const nl = buf.indexOf('\n');
91
+ if (nl === -1) return;
92
+ clearTimeout(timer);
93
+ try {
94
+ const msg = JSON.parse(buf.slice(0, nl)) as { engine?: unknown };
95
+ // Codex round-3: only the protocol's own vocabulary is reported; anything else is "unknown" (undefined)
96
+ done(msg.engine === 'hybrid' || msg.engine === 'cosine-fallback' || msg.engine === 'none' ? msg.engine : undefined);
97
+ } catch {
98
+ done(undefined);
99
+ }
100
+ });
101
+ sock.on('error', () => {
102
+ clearTimeout(timer);
103
+ done(undefined);
104
+ });
105
+ });
106
+ }
107
+
34
108
  /**
35
109
  * Version stamped into BOTH generated helper files as `// dz-apply-leg-version: N` (line 2, right
36
110
  * after the shebang). Bump on ANY change to {@link recallHookSource} or {@link embedDaemonSource}'s
@@ -48,8 +122,26 @@ import { hookCommandsOf } from './managed-hooks.js';
48
122
  * compiled module), the daemon writes a `.dz/embed.sock.path` pointer when it picks the tmpdir-short
49
123
  * branch, and `ready` is now printed only after `existsSync(SOCKET)` confirms the bind actually
50
124
  * landed (previously logged unconditionally, before `listen` even ran).
125
+ *
126
+ * Bumped 4→5 (feature `hook-recall-hybrid-parity`, ADR-001 D1/D2): the daemon's `op: recall`
127
+ * handler now tries core's `recallHybrid` FIRST — under a time budget (`HOOK_RECALL_BUDGET_MS`,
128
+ * default 500 ms) — via the SAME `CORE_DIST_DIR` + `loadCoreModule` mechanism the hook already
129
+ * used only for its policy modules; on budget overrun, engine error, or no resolvable core module
130
+ * it falls back to today's brute-force cosine, honestly labelled `engine: 'cosine-fallback'` with a
131
+ * `reason`. The hook now reads `engine`/`reason` off the daemon's reply (stderr-only, never
132
+ * context) and applies its relevance floor to the NEW `score` format when `engine === 'hybrid'`,
133
+ * preserving today's cosine-calibrated floor unchanged for the `cosine-fallback` path.
134
+ *
135
+ * Bumped 5→6 (`hook-recall-hybrid-parity`, fix round 1 — AM-1/AM-2/AM-3/AM-5): the daemon now
136
+ * (a) fires a fire-and-forget engine warm-up before `listen()` (AM-1) so the first REAL `op: recall`
137
+ * is less likely to pay a cold `resolveAgentdbEmbedder` init; (b) arms the budget timer BEFORE
138
+ * `loadCoreModule()`, not after (AM-2, wall clock from request receipt); (c) treats ANY failure
139
+ * past the budget race — a malformed hit, `patternRecordId()` throwing — as an honest cosine
140
+ * fallback rather than a bare protocol error (AM-3); (d) reports the RAW core RRF score, unchanged,
141
+ * instead of a locally re-normalized [0,1] value (AM-5) — the hook's own `HOOK_SCORE_FLOOR` default
142
+ * moved from `0.01` to `0.005` to match (see that constant's own comment for the measurement).
51
143
  */
52
- export const APPLY_LEG_VERSION = 4;
144
+ export const APPLY_LEG_VERSION = 6;
53
145
 
54
146
  /**
55
147
  * Parse the `dz-apply-leg-version` stamp from a deployed helper file. Unlike
@@ -176,8 +268,34 @@ function resolveEffectiveEmbedSocketPath(projectRoot, env) {
176
268
  }
177
269
  const SOCKET = resolveEffectiveEmbedSocketPath(PROJECT, process.env).path;
178
270
  const USAGE_LOG = process.env.DZ_RECALL_USAGE_LOG || path.join(PROJECT, '.dz', 'recall-usage.jsonl');
271
+ // Measured (2026-09-14, apply-leg-socket.test.ts): an ordinary hook round-trip (spawn + one socket
272
+ // op) took 81-121 ms; 800 ms leaves a wide margin for a loaded daemon while still bounding the AM-2
273
+ // worst case — a daemon synchronously blocked never replies at all, so THIS timeout (not the
274
+ // daemon's own internal budget race, which cannot preempt synchronous work) is what actually
275
+ // rescues the hook from hanging.
179
276
  const TIMEOUT_MS = Number(process.env.DZ_RECALL_HOOK_TIMEOUT_MS || 800);
180
277
 
278
+ // FR-5 (hook-recall-hybrid-parity, ADR-001 D2): the RRF-based \`score\` the daemon returns for
279
+ // \`engine: 'hybrid'\` is NOT on the cosine scale DEFAULT_RECALL_FLOORS (recall-hook-policy.ts) was
280
+ // calibrated on — applying the cosine floor to an RRF score would either admit everything or cut
281
+ // everything, so the hybrid path gets its OWN floor, applied to BOTH languages alike (the RRF score
282
+ // carries no language-baseline shift the way raw cosine did).
283
+ //
284
+ // AM-5 (fix round 1), MEASURED not a placeholder: recallHybrid(RRF_K=60) over a live 14-lesson
285
+ // fixture (this environment, 2026-09-14 — reproducer in the manifest's Fix-round 1 section) shows
286
+ // raw RRF score is only WEAKLY discriminating per-hit: an exact-lexical-match hit scored 0.03252
287
+ // (both legs agree at rank 0), but a genuinely IRRELEVANT query ("xkcd banana quantum toaster
288
+ // nonsense") still returned a top hit at 0.01639 — HIGHER than several truly relevant tail hits in
289
+ // OTHER queries (0.01471-0.01538). This is structural, not a fixture artifact: RRF encodes RANK,
290
+ // not similarity, and a nearest-neighbor search always returns SOME top-1 even for a garbage query.
291
+ // A raw-score floor therefore cannot cleanly separate signal from noise at the per-hit level the
292
+ // way the cosine floor does — true filtering here has to come from \`limit\` and \`selectHookHits\`'s
293
+ // own budget, not from this number. The floor's honest job is only to reject a DEGENERATE score
294
+ // (zero/negative/NaN from a malformed hit), so it is set well BELOW the measured noise floor
295
+ // (0.01471) rather than attempting to rank-filter — deliberately permissive, matching ADR-001 D2's
296
+ // stated intent that an exact lexical match (FR-4) must never be defeated by an unmeasured cutoff.
297
+ const HOOK_SCORE_FLOOR = Number(process.env.DZ_RECALL_HOOK_SCORE_FLOOR || 0.005);
298
+
181
299
  const safe = (fn, fb) => {
182
300
  try {
183
301
  return fn();
@@ -293,6 +411,9 @@ function readLogTail(chain, file) {
293
411
  }, chain && chain.EMPTY_LOG_TAIL);
294
412
  }
295
413
 
414
+ // FR-6 (hook-recall-hybrid-parity): the reply now carries \`engine\`/\`reason\` alongside \`hits\` —
415
+ // returned as a small object rather than the bare hit array, so the caller can apply the RIGHT
416
+ // floor (FR-5) and print the engine to stderr ONLY (never into the injected context, FR-2).
296
417
  function askDaemon(prompt) {
297
418
  return new Promise((resolve) => {
298
419
  if (!fs.existsSync(SOCKET)) return resolve(undefined);
@@ -314,7 +435,15 @@ function askDaemon(prompt) {
314
435
  if (nl === -1) return;
315
436
  clearTimeout(timer);
316
437
  const msg = safe(() => JSON.parse(buf.slice(0, nl)), undefined);
317
- done(msg && Array.isArray(msg.hits) ? msg.hits : undefined);
438
+ done(
439
+ msg && Array.isArray(msg.hits)
440
+ ? {
441
+ hits: msg.hits,
442
+ engine: typeof msg.engine === 'string' ? msg.engine : undefined,
443
+ reason: typeof msg.reason === 'string' ? msg.reason : undefined,
444
+ }
445
+ : undefined,
446
+ );
318
447
  });
319
448
  sock.on('error', () => {
320
449
  clearTimeout(timer);
@@ -506,8 +635,8 @@ async function main() {
506
635
  const policy = await loadPolicy();
507
636
  if (!policy) return emitContext(debt);
508
637
 
509
- const hits = await askDaemon(prompt);
510
- if (!hits) {
638
+ const daemonReply = await askDaemon(prompt);
639
+ if (!daemonReply) {
511
640
  // SELF-HEAL (2026-07-28): the daemon is started at SessionStart only, so when it dies mid-way
512
641
  // through a long-lived session NOTHING restarts it — the apply leg was silently dead for 19
513
642
  // days (MEASURED: recall-usage.jsonl last record 2026-07-09, socket absent). Spawn it
@@ -515,9 +644,19 @@ async function main() {
515
644
  reviveDaemon();
516
645
  return emitContext(debt);
517
646
  }
647
+ const { hits, engine, reason } = daemonReply;
648
+ // FR-6/FR-2: the engine (and, on fallback, why) is the caller's business, not the model's — it
649
+ // NEVER rides into additionalContext, only stderr, which Claude Code does not read as context.
650
+ if (typeof engine === 'string') {
651
+ safe(() => process.stderr.write(\`[dz-recall] engine=\${engine}\${reason ? \` reason=\${reason}\` : ''}\\n\`));
652
+ }
518
653
  if (hits.length === 0) return emitContext(debt); // daemon alive, nothing relevant — silence is correct
519
654
 
520
- const selection = policy.selectHookHits(prompt, hits);
655
+ // FR-5 (ADR-001 D2): a hybrid-engine reply carries an RRF-based score — its OWN floor, applied to
656
+ // both languages. A cosine-fallback reply (or an old daemon that never sent \`engine\` at all)
657
+ // keeps today's cosine-calibrated DEFAULT_RECALL_FLOORS untouched.
658
+ const floorOpts = engine === 'hybrid' ? { floors: { ru: HOOK_SCORE_FLOOR, en: HOOK_SCORE_FLOOR } } : {};
659
+ const selection = policy.selectHookHits(prompt, hits, floorOpts);
521
660
  // lesson-quarantine AM-2: an excluded hypothesis is logged, never a silent context shrink.
522
661
  if (typeof selection.quarantinedExcluded === 'number' && selection.quarantinedExcluded > 0) {
523
662
  try {
@@ -577,13 +716,22 @@ export function resolveIdleMs(raw: number): ResolvedIdleMs {
577
716
 
578
717
  /**
579
718
  * Generate `.claude/helpers/dz-embed-daemon.mjs`. Behaviourally identical to the pre-existing
580
- * hand-committed hub file except for: the version stamp (new, line 2) and `resolveDeps()`, which
581
- * now tries `@huggingface/transformers` before falling back to `@xenova/transformers` — AM-3,
719
+ * hand-committed hub file except for: the version stamp (new, line 2), `resolveDeps()`, which
720
+ * tries `@huggingface/transformers` before falling back to `@xenova/transformers` — AM-3,
582
721
  * dz-harness-hub issue #10 defect 3: `agentdb >= 3.0.0-alpha` depends on the former, and an older
583
722
  * agentdb install still carries the latter, so probing only one name silently starved the daemon
584
- * on either side of that agentdb version boundary.
723
+ * on either side of that agentdb version boundary — and (feature `hook-recall-hybrid-parity`,
724
+ * ADR-001 D1) the `op: recall` handler, which now tries core's `recallHybrid` under a time budget
725
+ * before falling back to the brute-force cosine below.
726
+ *
727
+ * `coreDistDir` (new parameter, ADR-001 D1) is baked in exactly like {@link recallHookSource}'s own
728
+ * parameter of the same name — the FIRST resolve candidate for `loadCoreModule`. `null` (the
729
+ * default, and what every existing zero-arg call site gets) is the same PORTABLE marker
730
+ * `recallHookSource(null)` uses: `loadCoreModule` falls through to the project-relative fallback
731
+ * candidates, resolved from `DZ_PROJECT_ROOT`/`cwd()` at daemon RUNTIME, which is correct in any
732
+ * clone and for any consumer whose `harness-core` install is reachable under its own project tree.
585
733
  */
586
- export function embedDaemonSource(): string {
734
+ export function embedDaemonSource(coreDistDir: string | null = null): string {
587
735
  return `#!/usr/bin/env node
588
736
  // dz-apply-leg-version: ${APPLY_LEG_VERSION}
589
737
  /**
@@ -605,11 +753,18 @@ export function embedDaemonSource(): string {
605
753
  * - it exits on SIGINT/SIGTERM/SIGHUP and unlinks its socket.
606
754
  *
607
755
  * PROTOCOL — newline-delimited JSON over a unix socket:
608
- * → {"op":"recall","prompt":"…","limit":8} ← {"hits":[{"dzId","pattern","score","domain"}]}
756
+ * → {"op":"recall","prompt":"…","limit":8} ← {"hits":[{"dzId","pattern","score","domain"}],"engine":"hybrid"|"cosine-fallback","reason"?}
609
757
  * → {"op":"ping"} ← {"ok":true,"model":"…","uptimeMs":N}
610
758
  * → {"op":"stop"} ← {"ok":true} (then exits)
611
759
  *
612
- * \`score\` is COSINE RELEVANCE in [0,1] — never the teaching reward. The caller applies the floor.
760
+ * ADR-001 (feature \`hook-recall-hybrid-parity\`): \`op: recall\` first tries core's \`recallHybrid\`
761
+ * (same engine \`dz recall\` uses) under \`HOOK_RECALL_BUDGET_MS\` (default 500 ms, < the hook's own
762
+ * 800 ms timeout); on budget overrun, engine error, or no resolvable core module it falls back to
763
+ * the brute-force cosine below. \`score\` on \`engine:"hybrid"\` is the RAW core RRF score, UNCHANGED
764
+ * (AM-5, fix round 1) — the exact same number \`dz recall --json\` reports as \`relevance\`, never
765
+ * locally re-normalized; on \`engine:"cosine-fallback"\` it is COSINE RELEVANCE in [0,1] as before —
766
+ * two DIFFERENT scales, never the teaching reward either way. The caller applies the right floor
767
+ * for whichever scale \`engine\` names.
613
768
  */
614
769
 
615
770
  import { createServer } from 'node:net';
@@ -619,6 +774,7 @@ import { createRequire } from 'node:module';
619
774
  import { connect } from 'node:net';
620
775
  import { createHash } from 'node:crypto';
621
776
  import { tmpdir } from 'node:os';
777
+ import { pathToFileURL } from 'node:url';
622
778
 
623
779
  const log = (...a) => console.error('[dz-embed]', ...a);
624
780
 
@@ -626,6 +782,64 @@ const log = (...a) => console.error('[dz-embed]', ...a);
626
782
  console.log = (...a) => console.error(...a);
627
783
 
628
784
  const PROJECT = process.env['DZ_PROJECT_ROOT'] ?? process.cwd();
785
+
786
+ // ADR-001 (hook-recall-hybrid-parity, D1): the SAME candidate-list resolution the hook uses for its
787
+ // own policy modules — the baked \`coreDistDir\` first (a real install's absolute dist path), then
788
+ // project-relative fallbacks resolved from PROJECT at RUNTIME. \`null\` (the hub's own portable
789
+ // marker, matching \`recallHookSource(null)\`) skips straight to the fallbacks. Loaded ONCE and
790
+ // memoized (\`coreModulePromise\`) — a fresh \`import()\` per recall would defeat FR-3's "opened once".
791
+ const CORE_DIST_DIR = ${coreDistDir === null ? 'null' : JSON.stringify(coreDistDir)};
792
+ let coreModulePromise;
793
+ function loadCoreModule() {
794
+ if (coreModulePromise !== undefined) return coreModulePromise;
795
+ coreModulePromise = (async () => {
796
+ const candidates = [
797
+ ...(CORE_DIST_DIR ? [join(CORE_DIST_DIR, 'index.js')] : []),
798
+ join(PROJECT, 'node_modules', '@dzhechkov', 'harness-core', 'dist', 'index.js'),
799
+ join(PROJECT, 'packages', '@dzhechkov', 'harness-core', 'dist', 'index.js'),
800
+ ];
801
+ for (const c of candidates) {
802
+ if (!existsSync(c)) continue;
803
+ try {
804
+ const mod = await import(pathToFileURL(c).href);
805
+ if (typeof mod.recallHybrid === 'function' && typeof mod.patternRecordId === 'function') return mod;
806
+ } catch {
807
+ /* try the next candidate */
808
+ }
809
+ }
810
+ return undefined;
811
+ })();
812
+ return coreModulePromise;
813
+ }
814
+
815
+ // FR-2 (hook-recall-hybrid-parity): the hybrid leg is time-boxed so a slow/cold engine can never make ONE prompt pay the full
816
+ // cost — it falls back to the warm cosine below instead. 500 ms leaves the hook's own 800 ms
817
+ // timeout (recallHookSource's TIMEOUT_MS) headroom for the socket round-trip itself.
818
+ // Measured 2026-09-14 (nfr1-measure.mjs, real 743-pattern store, warm-up on, n=100 x2): hybrid p50 60-65 ms,
819
+ // p95 100-180 ms, max 374 ms; 500 ms ≈ 3x p95 and stays under the hook's own 800 ms client timeout.
820
+ const HOOK_RECALL_BUDGET_MS = Number(process.env['HOOK_RECALL_BUDGET_MS'] || 500);
821
+ // Codex round-2 (2026-09-14): a hybrid attempt that LOST the race keeps running in the background —
822
+ // this cap keeps a burst of slow requests from stacking unbounded engine work; past it, requests answer
823
+ // with cosine at once, honestly labelled. Real cancellation needs worker isolation (backlog 14c1316b).
824
+ const HYBRID_MAX_IN_FLIGHT = (() => {
825
+ const raw = Number(process.env['HOOK_HYBRID_MAX_IN_FLIGHT'] || 2);
826
+ // Codex round-3: NaN/Infinity/0/negative must not silently disable the cap — fall back to 2.
827
+ return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : 2;
828
+ })();
829
+ let hybridInFlight = 0;
830
+ // Test-only fault injection (AC-2): a positive value delays the hybrid leg so the budget can be
831
+ // PROVEN to fire without a real slow engine. Absent/0 in every real deployment.
832
+ const DZ_EMBED_HYBRID_DELAY_MS = Number(process.env['DZ_EMBED_HYBRID_DELAY_MS'] || 0);
833
+ // AM-5 (fix round 1): the daemon used to re-normalize recallHybrid's raw RRF score into [0,1] with
834
+ // its OWN copy of vector-tier.ts's RRF_K constant — two numbers that could silently drift apart
835
+ // (this file is standalone generated text and cannot \`import\` the compiled core constant), AND a
836
+ // scale \`dz recall --json\`'s own \`relevance\` field (cli.ts: \`relevance: … h.score …\`) never
837
+ // applies — so the hook's floor and the CLI's floor were never comparable numbers even though both
838
+ // ultimately came from the same recallHybrid() call. Fixed: the daemon now reports \`h.score\`
839
+ // UNCHANGED — the exact raw core score \`dz recall --json\` already reports as \`relevance\` — so a
840
+ // floor calibrated against one is valid against the other (AM-4's parity test asserts the two are
841
+ // literally equal, not merely proportional).
842
+
629
843
  // embed-socket-short-path (FR-1): a unix socket path has a hard platform limit on \`sun_path\`
630
844
  // (Linux 108 bytes incl. NUL, macOS 104) — a deeply nested project's \`.dz/embed.sock\` can exceed
631
845
  // it, and \`listen()\` then fails while every OTHER part of the daemon looks healthy. This mirrors
@@ -803,6 +1017,116 @@ async function main() {
803
1017
  let patterns = loadPatterns();
804
1018
  let patternsAt = Date.now();
805
1019
 
1020
+ // ADR-001 (hook-recall-hybrid-parity, D1): try core's recallHybrid FIRST, budget-bounded.
1021
+ // AM-2 (fix round 1, wall clock from request receipt): the budget timer is armed BEFORE
1022
+ // \`loadCoreModule()\` runs, not after it resolves — the FIRST call's dynamic \`import()\` cost used
1023
+ // to be spent OUTSIDE the race, so a slow/cold module resolution could add its own latency on top
1024
+ // of the full \`HOOK_RECALL_BUDGET_MS\` window instead of eating into it.
1025
+ // NAMED LIMIT (AM-2): \`Promise.race\` cannot PREEMPT synchronous work — if \`core.recallHybrid\`
1026
+ // (or anything it calls) blocks the event loop synchronously, this race does not return until
1027
+ // that work finishes, budget or not; Node has no cooperative-preemption primitive for that. The
1028
+ // budget only bounds work that yields the event loop somewhere (every real I/O/await in
1029
+ // recallHybrid does). The hook's OWN client-side \`TIMEOUT_MS\` (recallHookSource, 800 ms) is the
1030
+ // actual backstop against a synchronously-blocked daemon: it times out the SOCKET, not the
1031
+ // daemon's internal race, so the hook always returns promptly even if this promise never does.
1032
+ //
1033
+ // \`hybridRecall\`'s own promise is left running past a timeout loss (never awaited a second time)
1034
+ // — its \`.catch\` below only silences a LATE rejection so a slow, eventually-failing engine call
1035
+ // can never become an unhandled-rejection crash for this long-lived process.
1036
+ async function hybridRecall(prompt, limit) {
1037
+ if (hybridInFlight >= HYBRID_MAX_IN_FLIGHT) {
1038
+ return { ok: false, reason: \`hybrid saturated (\${hybridInFlight} attempt(s) still in flight, cap \${HYBRID_MAX_IN_FLIGHT})\` };
1039
+ }
1040
+ const TIMED_OUT = Symbol('timed-out');
1041
+ let timer;
1042
+ const budget = new Promise((resolve) => {
1043
+ timer = setTimeout(() => resolve(TIMED_OUT), HOOK_RECALL_BUDGET_MS);
1044
+ timer.unref?.();
1045
+ });
1046
+ hybridInFlight += 1;
1047
+ const attempt = (async () => {
1048
+ const core = await loadCoreModule();
1049
+ if (core === undefined) return { unavailable: true };
1050
+ if (DZ_EMBED_HYBRID_DELAY_MS > 0) await new Promise((r) => setTimeout(r, DZ_EMBED_HYBRID_DELAY_MS));
1051
+ const result = await core.recallHybrid(PROJECT, prompt, { limit, mode: 'hook', deferExposures: true });
1052
+ return { unavailable: false, result, core };
1053
+ })();
1054
+ // the in-flight count follows the UNDERLYING attempt, not the race: a timed-out attempt still
1055
+ // occupies its slot until it settles (that is the whole point of the cap)
1056
+ attempt.then(() => { hybridInFlight -= 1; }, () => { hybridInFlight -= 1; });
1057
+ attempt.catch(() => {}); // swallow a rejection that arrives AFTER the budget already won the race
1058
+ try {
1059
+ const raced = await Promise.race([attempt, budget]);
1060
+ clearTimeout(timer);
1061
+ if (raced === TIMED_OUT) return { ok: false, reason: \`budget exceeded (\${HOOK_RECALL_BUDGET_MS} ms)\` };
1062
+ if (raced.unavailable) return { ok: false, reason: 'core module unavailable' };
1063
+ // AM-3 (fix round 1): everything past the race — reading result.hits, a hit missing its
1064
+ // required fields, patternRecordId() throwing on a malformed pattern — is now INSIDE this
1065
+ // try, so any such failure falls back to cosine with an honest \`reason\` instead of reaching
1066
+ // the socket handler's outer catch, which used to turn it into a bare protocol {error} reply
1067
+ // (never engine:'cosine-fallback') — the exact defect this amendment fixes.
1068
+ const { result, core } = raced;
1069
+ const hits = (result.hits || []).map((h) => ({
1070
+ dzId: core.patternRecordId(h.pattern),
1071
+ pattern: h.pattern.pattern,
1072
+ score: h.score, // AM-5: raw core score, unchanged — the same number \`dz recall --json\` reports as \`relevance\`
1073
+ domain: h.pattern.domain,
1074
+ ...(h.quarantined ? { quarantined: true } : {}),
1075
+ }));
1076
+ return { ok: true, hits: hits.slice(0, limit) };
1077
+ } catch (err) {
1078
+ clearTimeout(timer);
1079
+ return { ok: false, reason: \`recallHybrid failed: \${err?.message ?? err}\` };
1080
+ }
1081
+ }
1082
+
1083
+ /** \`op: recall\`'s whole answer: hybrid first (budget-bounded), cosine fallback on ANY failure —
1084
+ * always honestly labelled with \`engine\`/\`reason\` (FR-2/FR-6). */
1085
+ async function answerRecall(prompt, limitRaw) {
1086
+ const limit = Math.min(Number(limitRaw) || 8, 32);
1087
+ if (prompt.trim() === '') return { hits: [], engine: 'none', reason: 'empty prompt' }; // Codex round-2: every reply carries \`engine\`
1088
+ const hybrid = await hybridRecall(prompt, limit);
1089
+ if (hybrid.ok) return { hits: hybrid.hits, engine: 'hybrid' };
1090
+ // Reload the cosine mirror if it changed on disk (a \`dz teach\` between turns) — the SAME
1091
+ // staleness window as before this feature, just checked only when actually falling back.
1092
+ if (Date.now() - patternsAt > 5000) {
1093
+ try {
1094
+ patterns = loadPatterns();
1095
+ } catch {
1096
+ /* keep the previous snapshot */
1097
+ }
1098
+ patternsAt = Date.now();
1099
+ }
1100
+ if (patterns.length === 0) return { hits: [], engine: 'cosine-fallback', reason: hybrid.reason };
1101
+ const qv = await embed(prompt);
1102
+ const scored = patterns.map((p) => ({ dzId: p.dzId, pattern: p.pattern, score: cos(qv, p.vec), ...(p.quarantined ? { quarantined: true } : {}) }));
1103
+ scored.sort((a, b) => b.score - a.score);
1104
+ return { hits: scored.slice(0, limit), engine: 'cosine-fallback', reason: hybrid.reason };
1105
+ }
1106
+
1107
+ // AM-1 (fix round 1): warm resolveAgentdbEmbedder — cached PER PROCESS since db1521ba (cold
1108
+ // ~2-3.6 s, warm ~1 ms, MEASURED, see the manifest's T8/AM-1 discussion) — OFF the request path,
1109
+ // so the first REAL \`op: recall\` is not the one that pays the cold init. Fired fire-and-forget
1110
+ // right before \`listen()\` below, never awaited by startup: this is a best-effort head start, not
1111
+ // a guarantee — a request landing in the few-hundred-ms window before it completes still pays the
1112
+ // cold cost exactly as before this amendment, and a warm-up failure (no core module, engine
1113
+ // error) is silently swallowed — never-block applies to startup exactly as it does to a request.
1114
+ // Measured: the slowest cold resolveAgentdbEmbedder init observed in this environment was 3653 ms
1115
+ // (T8 log, 2026-09-14) — 10 s leaves a wide margin without risking an unbounded warm-up hang.
1116
+ const WARMUP_TIMEOUT_MS = 10_000;
1117
+ async function warmUpHybridEngine() {
1118
+ const core = await loadCoreModule();
1119
+ if (core === undefined) return;
1120
+ const guard = new Promise((resolve) => {
1121
+ const t = setTimeout(resolve, WARMUP_TIMEOUT_MS);
1122
+ t.unref?.();
1123
+ });
1124
+ // An empty-string query still exercises the FULL semantic leg (embed + engine.search), which is
1125
+ // exactly what needs warming; recallHybrid degrades any error inside it honestly, so nothing
1126
+ // here needs its own try/catch beyond the outer .catch(() => {}) at the call site below.
1127
+ await Promise.race([core.recallHybrid(PROJECT, '', { limit: 1, mode: 'hook', deferExposures: true }), guard]);
1128
+ }
1129
+
806
1130
  let idleTimer;
807
1131
  let lastActivityAt = Date.now();
808
1132
  const touch = () => {
@@ -837,24 +1161,8 @@ async function main() {
837
1161
  sock.write(JSON.stringify({ ok: true }) + '\\n');
838
1162
  return shutdown(0);
839
1163
  } else if (msg.op === 'recall') {
840
- // Reload the mirror if it changed on disk (a \`dz teach\` between turns).
841
- if (Date.now() - patternsAt > 5000) {
842
- try {
843
- patterns = loadPatterns();
844
- } catch {
845
- /* keep the previous snapshot */
846
- }
847
- patternsAt = Date.now();
848
- }
849
1164
  const prompt = typeof msg.prompt === 'string' ? msg.prompt : '';
850
- if (prompt.trim() === '' || patterns.length === 0) {
851
- reply = { hits: [] };
852
- } else {
853
- const qv = await embed(prompt);
854
- const scored = patterns.map((p) => ({ dzId: p.dzId, pattern: p.pattern, score: cos(qv, p.vec), ...(p.quarantined ? { quarantined: true } : {}) }));
855
- scored.sort((a, b) => b.score - a.score);
856
- reply = { hits: scored.slice(0, Math.min(Number(msg.limit) || 8, 32)) };
857
- }
1165
+ reply = await answerRecall(prompt, msg.limit);
858
1166
  } else {
859
1167
  reply = { error: \`unknown op \${String(msg.op)}\` };
860
1168
  }
@@ -893,6 +1201,9 @@ async function main() {
893
1201
 
894
1202
  for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => shutdown(0));
895
1203
 
1204
+ // AM-1: fire-and-forget, never awaited — bind proceeds immediately regardless of warm-up outcome.
1205
+ warmUpHybridEngine().catch(() => {});
1206
+
896
1207
  // FR-3 ("absence of a receipt is not success"): \`ready\` is printed ONLY after \`listen\`'s callback
897
1208
  // AND a fresh \`existsSync(SOCKET)\` both confirm the socket file is actually on disk — a caller
898
1209
  // that greps stderr for "ready" must never see it for a socket that silently failed to bind.
package/src/index.ts CHANGED
@@ -303,7 +303,7 @@ export {
303
303
  segmentRun,
304
304
  } from './eta.js';
305
305
  export type { CheckpointObservation, EtaEstimate, EtaInput, IncompleteCoverageSample, RunSegment, StageDurationSample, StageSample } from './eta.js';
306
- export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses, clearAgentdbQuarantine, deleteAgentdbByDzIds, readAgentdbRowsByTaskType, DZ_OWNED_TASK_TYPES, ensureAgentdbSchema } from './agentdb-index.js';
306
+ export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, resetAgentdbEmbedderCache, getAgentdbEmbedderCacheStats, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses, clearAgentdbQuarantine, deleteAgentdbByDzIds, readAgentdbRowsByTaskType, DZ_OWNED_TASK_TYPES, ensureAgentdbSchema } from './agentdb-index.js';
307
307
  export type { AgentdbSearchHit, AgentdbSearchResult, AgentdbImportRow } from './agentdb-index.js';
308
308
  export { DEFAULT_EMBED_MODEL, LEGACY_EMBED_MODEL, DEFAULT_EMBED_DIM, KNOWN_EMBED_DIMS, resolveEmbedModel, readEmbedManifest, writeEmbedManifest, embedManifestPath, legacyEmbedManifest } from './embedding-config.js';
309
309
  export type { EmbedModelConfig, EmbedModelSource, EmbedManifest } from './embedding-config.js';
@@ -742,6 +742,8 @@ export {
742
742
  buildReleaseNotes,
743
743
  releaseTagName,
744
744
  firstOutputLine,
745
+ testsFailureDetail,
746
+ outputTail,
745
747
  RELEASE_GATE_ORDER,
746
748
  RELEASE_TIMEOUTS,
747
749
  } from './release.js';
@@ -763,13 +765,19 @@ export type {
763
765
  } from './release.js';
764
766
  export { formatPublishError } from './publish.js';
765
767
  // Sibling-drift + packed-install-smoke gates (feature publish-sibling-drift-gate, ADR-001).
766
- export { detectSiblingDrift } from './publish-sibling-drift.js';
768
+ export { detectSiblingDrift, parseNpmPackInventory } from './publish-sibling-drift.js';
767
769
  export type {
768
770
  SiblingDriftStatus,
771
+ InventorySource,
769
772
  SiblingDriftResult,
770
773
  FetchedPublished,
771
774
  FetchPublished,
772
775
  DetectSiblingDriftOptions,
776
+ PackInventory,
777
+ PackInventoryUnavailable,
778
+ LocalInventoryResult,
779
+ PackedTree,
780
+ LocalInventory,
773
781
  } from './publish-sibling-drift.js';
774
782
  export { planPackedInstallSmoke, judgePackedInstallSmoke, packedTarballName } from './packed-install-smoke.js';
775
783
  export type {
package/src/operations.ts CHANGED
@@ -1437,18 +1437,34 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
1437
1437
  const WRITER_EVENTS = ['SessionStart', 'SessionEnd', 'PreCompact'];
1438
1438
  let eventsWithWriter: string[] = [];
1439
1439
  let settingsReadable = false;
1440
+ // Lead edit after Codex review (finding 1, 2026-09-13): three states, not a boolean — an
1441
+ // EXISTING settings.json that cannot be parsed is "unknowable", never "agrees".
1442
+ const settingsPath = join(root, '.claude', 'settings.json');
1443
+ const settingsPresent = existsSync(settingsPath);
1440
1444
  try {
1441
- const settings = JSON.parse(readFileSync(join(root, '.claude', 'settings.json'), 'utf-8')) as {
1445
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')) as {
1442
1446
  hooks?: Record<string, unknown[]>;
1443
1447
  };
1444
- settingsReadable = true;
1445
1448
  eventsWithWriter = WRITER_EVENTS.filter((ev) => (settings.hooks?.[ev] ?? []).some((h) => commandsOf(h).some(invokesWriter)));
1449
+ // Lead edit after Codex round 2 (new finding 2): "readable" means the hooks were actually
1450
+ // INSPECTED — a parseable but malformed shape (`null`, a non-array event) throws inside the
1451
+ // traversal and must land in the "cannot be compared" row, never in the OK row.
1452
+ settingsReadable = true;
1446
1453
  } catch {
1447
1454
  }
1455
+ const settingsUnreadable = settingsPresent && !settingsReadable;
1448
1456
  const allWired = eventsWithWriter.length === WRITER_EVENTS.length;
1449
1457
  const hooksInvokeAgentdbWriter = eventsWithWriter.length > 0;
1450
1458
 
1451
- if (configuredMemoryBackend === 'agentdb' && !allWired) {
1459
+ if (settingsUnreadable) {
1460
+ // Lead edit after Codex round 2: precedence — an unparseable settings.json is diagnosed FIRST for
1461
+ // both backends; the hooks are UNKNOWABLE, so neither "not wired" nor "in agreement" may be claimed.
1462
+ checks.push({
1463
+ name: 'memory hooks match config',
1464
+ ok: false,
1465
+ detail: `.dz/config.json says memory.backend=${configuredMemoryBackend} but .claude/settings.json exists and could not be parsed or inspected (invalid JSON or a malformed hooks shape) — the hooks cannot be compared; fix the file (or re-run dz setup)`,
1466
+ });
1467
+ } else if (configuredMemoryBackend === 'agentdb' && !allWired) {
1452
1468
  checks.push({
1453
1469
  name: 'memory hooks match config',
1454
1470
  ok: false,
@@ -1460,6 +1476,15 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
1460
1476
  ok: false,
1461
1477
  detail: '.dz/config.json says memory.backend=jsonl but SessionStart/SessionEnd/PreCompact hooks still invoke agentdb-writer.mjs — run: dz setup --target claude-code --memory jsonl (or --memory agentdb to keep agentdb and bring the config back in sync)',
1462
1478
  });
1479
+ } else {
1480
+ // the OK receipt comes from the SAME comparison that produces the red rows (all three events observed)
1481
+ checks.push({
1482
+ name: 'memory hooks match config',
1483
+ ok: true,
1484
+ detail: configuredMemoryBackend === 'agentdb'
1485
+ ? 'memory.backend=agentdb — SessionStart/SessionEnd/PreCompact all invoke .dz/agentdb-writer.mjs'
1486
+ : `memory.backend=jsonl — SessionStart/SessionEnd/PreCompact do not invoke .dz/agentdb-writer.mjs${settingsPresent ? '' : ' (no .claude/settings.json — no hooks at all)'}`,
1487
+ });
1463
1488
  }
1464
1489
  }
1465
1490
  // configuredMemoryBackend === 'unknown': no .dz/config.json yet — nothing to compare, same
@@ -1498,11 +1523,23 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
1498
1523
  resolved.reason === 'tmpdir-short'
1499
1524
  ? ` (tmpdir-short: project path ${projectPathBytes} bytes > ${EMBED_SOCKET_PATH_BYTES_LIMIT})`
1500
1525
  : '';
1526
+ // FR-6 (hook-recall-hybrid-parity): a socket that merely EXISTS is not proof of what it
1527
+ // answers with — probe it. A non-live fixture (a plain file, no listener) fails the probe
1528
+ // near-instantly and this note stays empty, so every pre-existing detail string here is
1529
+ // untouched.
1530
+ // AM-8 (fix round 1): the socket-speaking probe itself now lives in apply-leg.ts (the
1531
+ // module that already owns the daemon's wire protocol + socket-path resolution) rather
1532
+ // than duplicating a `node:net` IO surface here — dynamic `import()`, same pattern as
1533
+ // `embed-socket-path.js` two lines above, so operations.ts carries no new top-level IO
1534
+ // import for this.
1535
+ const { probeRecallEngine } = await import('./apply-leg.js');
1536
+ const engine = sockAlive ? await probeRecallEngine(resolved.path) : undefined;
1537
+ const engineNote = engine !== undefined ? ` (engine: ${engine})` : '';
1501
1538
  checks.push({
1502
1539
  name: 'apply-leg alive (embed daemon)',
1503
1540
  ok: sockAlive,
1504
1541
  detail: sockAlive
1505
- ? `embed socket present at ${resolved.path}${tmpdirNote} — recall injection can run`
1542
+ ? `embed socket present at ${resolved.path}${tmpdirNote}${engineNote} — recall injection can run`
1506
1543
  : `embed socket ABSENT at ${resolved.path}: the recall hook is wired but cannot inject (the hook self-heals on the next prompt; a persistent absence means the daemon cannot start)`,
1507
1544
  });
1508
1545
  }