@dzhechkov/harness-core 0.8.32 → 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/.dz-manifest.json +107 -47
- package/README.md +108 -0
- package/dist/agentdb-index.d.ts +17 -6
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +61 -17
- package/dist/agentdb-index.js.map +1 -1
- package/dist/apply-leg.d.ts +68 -5
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +466 -37
- package/dist/apply-leg.js.map +1 -1
- package/dist/embed-socket-path.d.ts +65 -0
- package/dist/embed-socket-path.d.ts.map +1 -0
- package/dist/embed-socket-path.js +100 -0
- package/dist/embed-socket-path.js.map +1 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +105 -3
- package/dist/operations.js.map +1 -1
- package/dist/packed-install-smoke.d.ts +108 -0
- package/dist/packed-install-smoke.d.ts.map +1 -0
- package/dist/packed-install-smoke.js +172 -0
- package/dist/packed-install-smoke.js.map +1 -0
- package/dist/publish-sibling-drift.d.ts +139 -0
- package/dist/publish-sibling-drift.d.ts.map +1 -0
- package/dist/publish-sibling-drift.js +408 -0
- package/dist/publish-sibling-drift.js.map +1 -0
- package/dist/publish.d.ts +44 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +243 -24
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +16 -0
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +1 -0
- package/dist/qe-bridge.js.map +1 -1
- package/dist/release.d.ts +91 -0
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +317 -21
- package/dist/release.js.map +1 -1
- package/dist/setup.d.ts +36 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +96 -2
- package/dist/setup.js.map +1 -1
- package/dist/vector-tier.d.ts +27 -2
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +112 -3
- package/dist/vector-tier.js.map +1 -1
- package/package.json +23 -23
- package/sbom.json +196 -46
- package/src/agentdb-index.ts +65 -17
- package/src/apply-leg.ts +461 -37
- package/src/embed-socket-path.ts +113 -0
- package/src/index.ts +44 -3
- package/src/operations.ts +104 -3
- package/src/packed-install-smoke.ts +273 -0
- package/src/publish-sibling-drift.ts +498 -0
- package/src/publish.ts +280 -25
- package/src/qe-bridge.ts +14 -0
- package/src/release.ts +366 -19
- package/src/setup.ts +108 -2
- package/src/vector-tier.ts +147 -5
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
|
|
@@ -41,8 +115,33 @@ import { hookCommandsOf } from './managed-hooks.js';
|
|
|
41
115
|
* `recallHookSource`'s `loadCoreModule` candidate list is now built with a conditional spread so a
|
|
42
116
|
* `null` `coreDistDir` (the hub's own portable marker, C-6/finding-6) degrades cleanly to the
|
|
43
117
|
* project-relative fallbacks — the generated BYTES changed for every caller, hub and consumer alike.
|
|
118
|
+
*
|
|
119
|
+
* Bumped 3→4 (feature `embed-socket-short-path`): both generated files now resolve the socket path
|
|
120
|
+
* through the same `DZ_EMBED_SOCKET` → project-path-if-short → tmpdir-short-hash logic as
|
|
121
|
+
* {@link resolveEmbedSocketPath} (inlined as TEXT in both — a template string cannot `import` a
|
|
122
|
+
* compiled module), the daemon writes a `.dz/embed.sock.path` pointer when it picks the tmpdir-short
|
|
123
|
+
* branch, and `ready` is now printed only after `existsSync(SOCKET)` confirms the bind actually
|
|
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).
|
|
44
143
|
*/
|
|
45
|
-
export const APPLY_LEG_VERSION =
|
|
144
|
+
export const APPLY_LEG_VERSION = 6;
|
|
46
145
|
|
|
47
146
|
/**
|
|
48
147
|
* Parse the `dz-apply-leg-version` stamp from a deployed helper file. Unlike
|
|
@@ -98,6 +197,11 @@ export function bakedCoreDistDirOf(content: string): string | null | undefined {
|
|
|
98
197
|
* Behaviourally identical to the pre-existing hand-committed hub file except for: the version
|
|
99
198
|
* stamp (new, line 2) and the candidate list in `loadCoreModule` (baked path first when present,
|
|
100
199
|
* `/usr/lib/...` dropped — FR-3).
|
|
200
|
+
*
|
|
201
|
+
* Also carries the `embed-socket-short-path` fix (FR-1/FR-2): `SOCKET` is resolved through the
|
|
202
|
+
* same env → project-path-if-short → tmpdir-short-hash logic as
|
|
203
|
+
* {@link "./embed-socket-path.js".resolveEmbedSocketPath}, inlined as text and preferring an
|
|
204
|
+
* on-disk pointer when the resolver itself would land on the tmpdir-short branch.
|
|
101
205
|
*/
|
|
102
206
|
export function recallHookSource(coreDistDir: string | null): string {
|
|
103
207
|
return `#!/usr/bin/env node
|
|
@@ -123,14 +227,75 @@ export function recallHookSource(coreDistDir: string | null): string {
|
|
|
123
227
|
const net = require('node:net');
|
|
124
228
|
const path = require('node:path');
|
|
125
229
|
const fs = require('node:fs');
|
|
230
|
+
const crypto = require('node:crypto');
|
|
231
|
+
const os = require('node:os');
|
|
126
232
|
const { pathToFileURL } = require('node:url');
|
|
127
233
|
|
|
128
234
|
const PROJECT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
129
235
|
const CORE_DIST_DIR = ${coreDistDir === null ? 'null' : JSON.stringify(coreDistDir)};
|
|
130
|
-
|
|
236
|
+
|
|
237
|
+
// embed-socket-short-path (FR-1/FR-2): byte-for-byte the same logic as \`resolveEmbedSocketPath\` /
|
|
238
|
+
// \`resolveEffectiveEmbedSocketPath\` in \`embed-socket-path.ts\` — inlined as TEXT because this file
|
|
239
|
+
// is standalone and cannot \`import\` a compiled core module (apply-leg-twins.test.ts pins the copies
|
|
240
|
+
// to the same behavior as the daemon's own inlined copy).
|
|
241
|
+
const EMBED_SOCKET_PATH_BYTES_LIMIT = 100;
|
|
242
|
+
function resolveEmbedSocketPath(projectRoot, env) {
|
|
243
|
+
const fromEnv = env.DZ_EMBED_SOCKET;
|
|
244
|
+
if (typeof fromEnv === 'string' && fromEnv !== '') return { path: fromEnv, reason: 'env' };
|
|
245
|
+
const projectPath = path.join(projectRoot, '.dz', 'embed.sock');
|
|
246
|
+
if (Buffer.byteLength(projectPath, 'utf8') <= EMBED_SOCKET_PATH_BYTES_LIMIT) return { path: projectPath, reason: 'project' };
|
|
247
|
+
const hash = crypto.createHash('sha1').update(projectRoot).digest('hex').slice(0, 12);
|
|
248
|
+
const uid = String(process.getuid?.() ?? 'u');
|
|
249
|
+
const shortPath = path.join(os.tmpdir(), \`dz-\${uid}\`, \`embed-\${hash}.sock\`);
|
|
250
|
+
const tooLong = Buffer.byteLength(shortPath, 'utf8') > EMBED_SOCKET_PATH_BYTES_LIMIT;
|
|
251
|
+
return tooLong ? { path: shortPath, reason: 'tmpdir-short', tooLong: true } : { path: shortPath, reason: 'tmpdir-short' };
|
|
252
|
+
}
|
|
253
|
+
function readEmbedSocketPointer(projectRoot) {
|
|
254
|
+
const pointerPath = path.join(projectRoot, '.dz', 'embed.sock.path');
|
|
255
|
+
if (!fs.existsSync(pointerPath)) return undefined;
|
|
256
|
+
try {
|
|
257
|
+
const raw = fs.readFileSync(pointerPath, 'utf-8').trim();
|
|
258
|
+
return raw !== '' ? raw : undefined;
|
|
259
|
+
} catch {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function resolveEffectiveEmbedSocketPath(projectRoot, env) {
|
|
264
|
+
const resolved = resolveEmbedSocketPath(projectRoot, env);
|
|
265
|
+
if (resolved.reason !== 'tmpdir-short') return resolved;
|
|
266
|
+
const pointer = readEmbedSocketPointer(projectRoot);
|
|
267
|
+
return pointer !== undefined && fs.existsSync(pointer) ? { path: pointer, reason: 'tmpdir-short' } : resolved;
|
|
268
|
+
}
|
|
269
|
+
const SOCKET = resolveEffectiveEmbedSocketPath(PROJECT, process.env).path;
|
|
131
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.
|
|
132
276
|
const TIMEOUT_MS = Number(process.env.DZ_RECALL_HOOK_TIMEOUT_MS || 800);
|
|
133
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
|
+
|
|
134
299
|
const safe = (fn, fb) => {
|
|
135
300
|
try {
|
|
136
301
|
return fn();
|
|
@@ -246,6 +411,9 @@ function readLogTail(chain, file) {
|
|
|
246
411
|
}, chain && chain.EMPTY_LOG_TAIL);
|
|
247
412
|
}
|
|
248
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).
|
|
249
417
|
function askDaemon(prompt) {
|
|
250
418
|
return new Promise((resolve) => {
|
|
251
419
|
if (!fs.existsSync(SOCKET)) return resolve(undefined);
|
|
@@ -267,7 +435,15 @@ function askDaemon(prompt) {
|
|
|
267
435
|
if (nl === -1) return;
|
|
268
436
|
clearTimeout(timer);
|
|
269
437
|
const msg = safe(() => JSON.parse(buf.slice(0, nl)), undefined);
|
|
270
|
-
done(
|
|
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
|
+
);
|
|
271
447
|
});
|
|
272
448
|
sock.on('error', () => {
|
|
273
449
|
clearTimeout(timer);
|
|
@@ -459,8 +635,8 @@ async function main() {
|
|
|
459
635
|
const policy = await loadPolicy();
|
|
460
636
|
if (!policy) return emitContext(debt);
|
|
461
637
|
|
|
462
|
-
const
|
|
463
|
-
if (!
|
|
638
|
+
const daemonReply = await askDaemon(prompt);
|
|
639
|
+
if (!daemonReply) {
|
|
464
640
|
// SELF-HEAL (2026-07-28): the daemon is started at SessionStart only, so when it dies mid-way
|
|
465
641
|
// through a long-lived session NOTHING restarts it — the apply leg was silently dead for 19
|
|
466
642
|
// days (MEASURED: recall-usage.jsonl last record 2026-07-09, socket absent). Spawn it
|
|
@@ -468,9 +644,19 @@ async function main() {
|
|
|
468
644
|
reviveDaemon();
|
|
469
645
|
return emitContext(debt);
|
|
470
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
|
+
}
|
|
471
653
|
if (hits.length === 0) return emitContext(debt); // daemon alive, nothing relevant — silence is correct
|
|
472
654
|
|
|
473
|
-
|
|
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);
|
|
474
660
|
// lesson-quarantine AM-2: an excluded hypothesis is logged, never a silent context shrink.
|
|
475
661
|
if (typeof selection.quarantinedExcluded === 'number' && selection.quarantinedExcluded > 0) {
|
|
476
662
|
try {
|
|
@@ -530,13 +716,22 @@ export function resolveIdleMs(raw: number): ResolvedIdleMs {
|
|
|
530
716
|
|
|
531
717
|
/**
|
|
532
718
|
* Generate `.claude/helpers/dz-embed-daemon.mjs`. Behaviourally identical to the pre-existing
|
|
533
|
-
* hand-committed hub file except for: the version stamp (new, line 2)
|
|
534
|
-
*
|
|
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,
|
|
535
721
|
* dz-harness-hub issue #10 defect 3: `agentdb >= 3.0.0-alpha` depends on the former, and an older
|
|
536
722
|
* agentdb install still carries the latter, so probing only one name silently starved the daemon
|
|
537
|
-
* 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.
|
|
538
733
|
*/
|
|
539
|
-
export function embedDaemonSource(): string {
|
|
734
|
+
export function embedDaemonSource(coreDistDir: string | null = null): string {
|
|
540
735
|
return `#!/usr/bin/env node
|
|
541
736
|
// dz-apply-leg-version: ${APPLY_LEG_VERSION}
|
|
542
737
|
/**
|
|
@@ -558,18 +753,28 @@ export function embedDaemonSource(): string {
|
|
|
558
753
|
* - it exits on SIGINT/SIGTERM/SIGHUP and unlinks its socket.
|
|
559
754
|
*
|
|
560
755
|
* PROTOCOL — newline-delimited JSON over a unix socket:
|
|
561
|
-
* → {"op":"recall","prompt":"…","limit":8}
|
|
756
|
+
* → {"op":"recall","prompt":"…","limit":8} ← {"hits":[{"dzId","pattern","score","domain"}],"engine":"hybrid"|"cosine-fallback","reason"?}
|
|
562
757
|
* → {"op":"ping"} ← {"ok":true,"model":"…","uptimeMs":N}
|
|
563
758
|
* → {"op":"stop"} ← {"ok":true} (then exits)
|
|
564
759
|
*
|
|
565
|
-
* \`
|
|
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.
|
|
566
768
|
*/
|
|
567
769
|
|
|
568
770
|
import { createServer } from 'node:net';
|
|
569
|
-
import { existsSync, unlinkSync, readFileSync } from 'node:fs';
|
|
570
|
-
import { join } from 'node:path';
|
|
771
|
+
import { existsSync, unlinkSync, readFileSync, mkdirSync, writeFileSync, renameSync, statSync } from 'node:fs';
|
|
772
|
+
import { join, dirname } from 'node:path';
|
|
571
773
|
import { createRequire } from 'node:module';
|
|
572
774
|
import { connect } from 'node:net';
|
|
775
|
+
import { createHash } from 'node:crypto';
|
|
776
|
+
import { tmpdir } from 'node:os';
|
|
777
|
+
import { pathToFileURL } from 'node:url';
|
|
573
778
|
|
|
574
779
|
const log = (...a) => console.error('[dz-embed]', ...a);
|
|
575
780
|
|
|
@@ -577,7 +782,84 @@ const log = (...a) => console.error('[dz-embed]', ...a);
|
|
|
577
782
|
console.log = (...a) => console.error(...a);
|
|
578
783
|
|
|
579
784
|
const PROJECT = process.env['DZ_PROJECT_ROOT'] ?? process.cwd();
|
|
580
|
-
|
|
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
|
+
|
|
843
|
+
// embed-socket-short-path (FR-1): a unix socket path has a hard platform limit on \`sun_path\`
|
|
844
|
+
// (Linux 108 bytes incl. NUL, macOS 104) — a deeply nested project's \`.dz/embed.sock\` can exceed
|
|
845
|
+
// it, and \`listen()\` then fails while every OTHER part of the daemon looks healthy. This mirrors
|
|
846
|
+
// {@link resolveEmbedSocketPath} in \`embed-socket-path.ts\` byte-for-byte (this file is generated
|
|
847
|
+
// TEXT, standalone, and cannot \`import\` a compiled core module — apply-leg-twins.test.ts pins the
|
|
848
|
+
// two copies to the same behavior).
|
|
849
|
+
const EMBED_SOCKET_PATH_BYTES_LIMIT = 100;
|
|
850
|
+
function resolveEmbedSocketPath(projectRoot, env) {
|
|
851
|
+
const fromEnv = env['DZ_EMBED_SOCKET'];
|
|
852
|
+
if (typeof fromEnv === 'string' && fromEnv !== '') return { path: fromEnv, reason: 'env' };
|
|
853
|
+
const projectPath = join(projectRoot, '.dz', 'embed.sock');
|
|
854
|
+
if (Buffer.byteLength(projectPath, 'utf8') <= EMBED_SOCKET_PATH_BYTES_LIMIT) return { path: projectPath, reason: 'project' };
|
|
855
|
+
const hash = createHash('sha1').update(projectRoot).digest('hex').slice(0, 12);
|
|
856
|
+
const uid = String(process.getuid?.() ?? 'u');
|
|
857
|
+
const shortPath = join(tmpdir(), \`dz-\${uid}\`, \`embed-\${hash}.sock\`);
|
|
858
|
+
const tooLong = Buffer.byteLength(shortPath, 'utf8') > EMBED_SOCKET_PATH_BYTES_LIMIT;
|
|
859
|
+
return tooLong ? { path: shortPath, reason: 'tmpdir-short', tooLong: true } : { path: shortPath, reason: 'tmpdir-short' };
|
|
860
|
+
}
|
|
861
|
+
const { path: SOCKET, reason: SOCKET_REASON, tooLong: SOCKET_TOO_LONG } = resolveEmbedSocketPath(PROJECT, process.env);
|
|
862
|
+
const SOCKET_POINTER = join(PROJECT, '.dz', 'embed.sock.path');
|
|
581
863
|
// AM-10 (issue #10 defect 7): a setTimeout delay is a 32-bit signed int in Node — anything
|
|
582
864
|
// above 2_147_483_647 ms silently becomes ~1ms (a "30 days" idle setting exited in ~1 second,
|
|
583
865
|
// MEASURED). 0/NaN/negative disables the idle exit outright rather than firing immediately.
|
|
@@ -734,7 +1016,116 @@ async function main() {
|
|
|
734
1016
|
|
|
735
1017
|
let patterns = loadPatterns();
|
|
736
1018
|
let patternsAt = Date.now();
|
|
737
|
-
|
|
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
|
+
}
|
|
738
1129
|
|
|
739
1130
|
let idleTimer;
|
|
740
1131
|
let lastActivityAt = Date.now();
|
|
@@ -770,24 +1161,8 @@ async function main() {
|
|
|
770
1161
|
sock.write(JSON.stringify({ ok: true }) + '\\n');
|
|
771
1162
|
return shutdown(0);
|
|
772
1163
|
} else if (msg.op === 'recall') {
|
|
773
|
-
// Reload the mirror if it changed on disk (a \`dz teach\` between turns).
|
|
774
|
-
if (Date.now() - patternsAt > 5000) {
|
|
775
|
-
try {
|
|
776
|
-
patterns = loadPatterns();
|
|
777
|
-
} catch {
|
|
778
|
-
/* keep the previous snapshot */
|
|
779
|
-
}
|
|
780
|
-
patternsAt = Date.now();
|
|
781
|
-
}
|
|
782
1164
|
const prompt = typeof msg.prompt === 'string' ? msg.prompt : '';
|
|
783
|
-
|
|
784
|
-
reply = { hits: [] };
|
|
785
|
-
} else {
|
|
786
|
-
const qv = await embed(prompt);
|
|
787
|
-
const scored = patterns.map((p) => ({ dzId: p.dzId, pattern: p.pattern, score: cos(qv, p.vec), ...(p.quarantined ? { quarantined: true } : {}) }));
|
|
788
|
-
scored.sort((a, b) => b.score - a.score);
|
|
789
|
-
reply = { hits: scored.slice(0, Math.min(Number(msg.limit) || 8, 32)) };
|
|
790
|
-
}
|
|
1165
|
+
reply = await answerRecall(prompt, msg.limit);
|
|
791
1166
|
} else {
|
|
792
1167
|
reply = { error: \`unknown op \${String(msg.op)}\` };
|
|
793
1168
|
}
|
|
@@ -815,16 +1190,65 @@ async function main() {
|
|
|
815
1190
|
} catch {
|
|
816
1191
|
/* ignore */
|
|
817
1192
|
}
|
|
1193
|
+
try {
|
|
1194
|
+
// Only OUR pointer is removed — one that already names another daemon's socket stays.
|
|
1195
|
+
if (SOCKET_REASON === 'tmpdir-short' && existsSync(SOCKET_POINTER) && readFileSync(SOCKET_POINTER, 'utf-8').trim() === SOCKET) unlinkSync(SOCKET_POINTER);
|
|
1196
|
+
} catch {
|
|
1197
|
+
/* ignore */
|
|
1198
|
+
}
|
|
818
1199
|
process.exit(code);
|
|
819
1200
|
}
|
|
820
1201
|
|
|
821
1202
|
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => shutdown(0));
|
|
822
1203
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1204
|
+
// AM-1: fire-and-forget, never awaited — bind proceeds immediately regardless of warm-up outcome.
|
|
1205
|
+
warmUpHybridEngine().catch(() => {});
|
|
1206
|
+
|
|
1207
|
+
// FR-3 ("absence of a receipt is not success"): \`ready\` is printed ONLY after \`listen\`'s callback
|
|
1208
|
+
// AND a fresh \`existsSync(SOCKET)\` both confirm the socket file is actually on disk — a caller
|
|
1209
|
+
// that greps stderr for "ready" must never see it for a socket that silently failed to bind.
|
|
1210
|
+
function bindFailed(detail) {
|
|
1211
|
+
log(\`bind failed: \${detail} (path \${Buffer.byteLength(SOCKET, 'utf8')} bytes)\`);
|
|
1212
|
+
process.exit(3);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// A fresh project's SOCKET directory (\`.dz/\`, or a caller-injected DZ_EMBED_SOCKET's own parent)
|
|
1216
|
+
// may not exist yet — \`dz setup\` usually creates \`.dz/\` before this daemon ever runs, but a
|
|
1217
|
+
// missing parent must not surface as an opaque platform EACCES/ENOENT when a plain mkdir fixes it.
|
|
1218
|
+
if (SOCKET_TOO_LONG) bindFailed('even the tmpdir-short fallback exceeds the unix socket path limit — set DZ_EMBED_SOCKET to a short path');
|
|
1219
|
+
try {
|
|
1220
|
+
// Lead edit after Codex review (finding 2): the private per-user directory is created 0700 and
|
|
1221
|
+
// verified — a socket in a world-writable tmpdir could be pre-bound or hijacked by a neighbour.
|
|
1222
|
+
mkdirSync(dirname(SOCKET), { recursive: true, mode: 0o700 });
|
|
1223
|
+
if (SOCKET_REASON === 'tmpdir-short') {
|
|
1224
|
+
const st = statSync(dirname(SOCKET));
|
|
1225
|
+
const ownUid = typeof process.getuid === 'function' ? process.getuid() : st.uid;
|
|
1226
|
+
if (st.uid !== ownUid || (st.mode & 0o077) !== 0) bindFailed(\`socket directory \${dirname(SOCKET)} is not private (uid \${st.uid}, mode \${(st.mode & 0o777).toString(8)})\`);
|
|
1227
|
+
}
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
if (SOCKET_REASON === 'tmpdir-short') bindFailed(\`cannot prepare socket directory: \${err?.message ?? err}\`);
|
|
1230
|
+
/* project branch: listen() below reports the real reason if the directory truly cannot be created */
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
server.listen(SOCKET, () => {
|
|
1234
|
+
if (!existsSync(SOCKET)) return bindFailed('socket file missing after listen');
|
|
1235
|
+
if (SOCKET_REASON === 'tmpdir-short') {
|
|
1236
|
+
// Lead edit after Codex review (finding 3): the pointer is published ATOMICALLY (tmp + rename)
|
|
1237
|
+
// and BEFORE \`ready\` — a reader never sees a half-written or missing pointer after \`ready\`;
|
|
1238
|
+
// a pointer that cannot be published is a bind failure, not a warning.
|
|
1239
|
+
try {
|
|
1240
|
+
mkdirSync(join(PROJECT, '.dz'), { recursive: true });
|
|
1241
|
+
const tmp = \`\${SOCKET_POINTER}.\${process.pid}.tmp\`;
|
|
1242
|
+
writeFileSync(tmp, SOCKET);
|
|
1243
|
+
renameSync(tmp, SOCKET_POINTER);
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
return bindFailed(\`could not publish socket pointer: \${err?.message ?? err}\`);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
log(\`ready: \${patterns.length} pattern vectors, model \${model}, socket \${SOCKET}\`);
|
|
1249
|
+
touch();
|
|
827
1250
|
});
|
|
1251
|
+
server.on('error', (err) => bindFailed(err?.message ?? String(err)));
|
|
828
1252
|
}
|
|
829
1253
|
|
|
830
1254
|
main().catch((err) => {
|