@dzhechkov/harness-core 0.3.135 → 0.3.136
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 +41 -17
- package/README.md +1 -0
- package/dist/compounding.d.ts +109 -0
- package/dist/compounding.d.ts.map +1 -0
- package/dist/compounding.js +211 -0
- package/dist/compounding.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +24 -0
- package/dist/operations.js.map +1 -1
- package/dist/recall-usage.d.ts +21 -0
- package/dist/recall-usage.d.ts.map +1 -1
- package/dist/recall-usage.js +27 -3
- package/dist/recall-usage.js.map +1 -1
- package/package.json +5 -5
- package/sbom.json +76 -16
- package/src/compounding.ts +314 -0
- package/src/index.ts +5 -0
- package/src/operations.ts +24 -0
- package/src/recall-usage.ts +45 -3
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dz compounding` — does the learning loop actually PAY? (feature compounding, scout C2)
|
|
3
|
+
*
|
|
4
|
+
* Ported from rUv's darwin-mode (`security/compounding.ts`, `security/ablation.ts`,
|
|
5
|
+
* `bench/{stats,promotion}.ts`) with an honesty split the port map demanded:
|
|
6
|
+
* - the STATS machinery ports verbatim (seeded mulberry32, bootstrap lower-95, decidePromotion,
|
|
7
|
+
* the min-n >= 5 rule — darwin's own FDR calibration shows n=3 gives a 33% false-discovery rate);
|
|
8
|
+
* - darwin's MEASUREMENT legs do NOT port: its FP-drop leg ignores the passed corpus (a fixture),
|
|
9
|
+
* `withoutMemory` is hard-coded 0, and "warm" is injected state — theatrical, exactly what this
|
|
10
|
+
* repo's claim-check culture forbids. The measurements here are dz-native, over data that exists.
|
|
11
|
+
*
|
|
12
|
+
* The report NEVER fakes a verdict: a gate without enough samples says INSUFFICIENT_DATA — after the
|
|
13
|
+
* 2026-07-28 inventory found the apply-leg log dead for 19 days, "no data" is a finding, not a pass.
|
|
14
|
+
*
|
|
15
|
+
* Everything here is PURE: callers gather facts (files, store rows); this module only computes.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ── Seeded statistics (verbatim-shape port from darwin-mode bench/stats.ts) ──
|
|
19
|
+
|
|
20
|
+
/** Deterministic PRNG — same seed, same stream, byte-identical reports. */
|
|
21
|
+
export function mulberry32(seed: number): () => number {
|
|
22
|
+
let a = seed >>> 0;
|
|
23
|
+
return () => {
|
|
24
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
25
|
+
let t = a;
|
|
26
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
27
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
28
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const BOOTSTRAP_RESAMPLES = 5000;
|
|
33
|
+
/** Below this many samples PER ARM a comparison is noise: darwin's own FDR calibration measured a
|
|
34
|
+
* 0.332 empirical false-discovery rate at n=3. */
|
|
35
|
+
export const MIN_SAMPLES_PER_ARM = 5;
|
|
36
|
+
|
|
37
|
+
export interface BootstrapDelta {
|
|
38
|
+
readonly meanDelta: number;
|
|
39
|
+
/** 2.5th percentile of the resampled deltas — the promotion decision reads THIS, not the mean. */
|
|
40
|
+
readonly lower95: number;
|
|
41
|
+
readonly samples: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Paired bootstrap over per-item deltas (b[i] - a[i]). */
|
|
45
|
+
export function bootstrapDelta(a: readonly number[], b: readonly number[], seed = 42): BootstrapDelta | null {
|
|
46
|
+
// PAIRED means paired: unequal lengths silently truncated a decisive observation and promoted on
|
|
47
|
+
// the remainder; a sparse/NaN entry is not an observation at all (Codex #9).
|
|
48
|
+
if (a.length !== b.length || a.length === 0) return null;
|
|
49
|
+
if (![...a, ...b].every((x) => typeof x === 'number' && Number.isFinite(x))) return null;
|
|
50
|
+
const n = a.length;
|
|
51
|
+
const deltas: number[] = [];
|
|
52
|
+
for (let i = 0; i < n; i++) deltas.push((b[i] ?? 0) - (a[i] ?? 0));
|
|
53
|
+
const rand = mulberry32(seed);
|
|
54
|
+
const means: number[] = [];
|
|
55
|
+
for (let r = 0; r < BOOTSTRAP_RESAMPLES; r++) {
|
|
56
|
+
let sum = 0;
|
|
57
|
+
for (let i = 0; i < n; i++) sum += deltas[Math.floor(rand() * n)] ?? 0;
|
|
58
|
+
means.push(sum / n);
|
|
59
|
+
}
|
|
60
|
+
means.sort((x, y) => x - y);
|
|
61
|
+
const meanDelta = deltas.reduce((s, d) => s + d, 0) / n;
|
|
62
|
+
// Conservative nearest-rank percentile: ceil(B*p)-1. floor(B*p) sat one slot ABOVE the 2.5th
|
|
63
|
+
// percentile and could flip a reject into a promote at the boundary (Codex #10 — a defect darwin
|
|
64
|
+
// itself inherits; ported faithfully was still ported wrong).
|
|
65
|
+
const lower95 = means[Math.max(0, Math.ceil(BOOTSTRAP_RESAMPLES * 0.025) - 1)] ?? 0;
|
|
66
|
+
return { meanDelta, lower95, samples: n };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type PromotionVerdict = 'promote' | 'reject' | 'insufficient-data';
|
|
70
|
+
|
|
71
|
+
/** Darwin's decision rule: a positive mean is not enough — the LOWER bound must clear zero. */
|
|
72
|
+
export function decidePromotion(delta: BootstrapDelta | null, minDelta = 0): PromotionVerdict {
|
|
73
|
+
// NaN samples compared false against the minimum and PROMOTED (Codex #9) — every field must be a
|
|
74
|
+
// real number and the count a real integer before any decision exists.
|
|
75
|
+
if (
|
|
76
|
+
delta === null ||
|
|
77
|
+
!Number.isInteger(delta.samples) ||
|
|
78
|
+
delta.samples < MIN_SAMPLES_PER_ARM ||
|
|
79
|
+
!Number.isFinite(delta.meanDelta) ||
|
|
80
|
+
!Number.isFinite(delta.lower95) ||
|
|
81
|
+
!Number.isFinite(minDelta)
|
|
82
|
+
) {
|
|
83
|
+
return 'insufficient-data';
|
|
84
|
+
}
|
|
85
|
+
return delta.meanDelta > minDelta && delta.lower95 > 0 ? 'promote' : 'reject';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── The dz-native facts the CLI gathers ─────────────────────────────
|
|
89
|
+
|
|
90
|
+
export interface LessonRow {
|
|
91
|
+
readonly dzId: string;
|
|
92
|
+
readonly uses: number;
|
|
93
|
+
readonly quarantined: boolean;
|
|
94
|
+
readonly reward: number | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface UsageEvent {
|
|
98
|
+
readonly dzId: string;
|
|
99
|
+
readonly ts: string;
|
|
100
|
+
readonly query?: string;
|
|
101
|
+
readonly runId?: string;
|
|
102
|
+
/** One id per PROMPT: the hook writes one row per injected hit (up to 3 per prompt), and counting
|
|
103
|
+
* rows as independent replay pairs fabricated readiness (Codex #1). */
|
|
104
|
+
readonly eventId?: string;
|
|
105
|
+
/** A truncated query cannot reproduce the original recall — it must not count (Codex #3). */
|
|
106
|
+
readonly queryTruncated?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface GuardEvent {
|
|
110
|
+
readonly ts: string;
|
|
111
|
+
readonly verdict: string;
|
|
112
|
+
readonly rules: readonly string[]; // violated rule ids
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface CompoundingFacts {
|
|
116
|
+
readonly lessons: readonly LessonRow[];
|
|
117
|
+
readonly usage: readonly UsageEvent[];
|
|
118
|
+
readonly guard: readonly GuardEvent[];
|
|
119
|
+
readonly nowTs: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── The report ──────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export interface PoolPayoff {
|
|
125
|
+
readonly total: number;
|
|
126
|
+
/** Ever surfaced by the APPLY leg (hook injection) — the strict payoff bar. */
|
|
127
|
+
readonly injectedEver: number;
|
|
128
|
+
/** Touched by ANY recall path (store `uses` counter). */
|
|
129
|
+
readonly touchedEver: number;
|
|
130
|
+
readonly neverTouched: number;
|
|
131
|
+
readonly quarantined: number;
|
|
132
|
+
/** Fraction of the pool that is write-only under the strict bar. */
|
|
133
|
+
readonly writeOnlyRatio: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface GuardRuleTrajectory {
|
|
137
|
+
readonly rule: string;
|
|
138
|
+
readonly firstHalfViolations: number;
|
|
139
|
+
readonly secondHalfViolations: number;
|
|
140
|
+
readonly firstHalfAudits: number;
|
|
141
|
+
readonly secondHalfAudits: number;
|
|
142
|
+
/** Improvement is judged on the RATE (violations per audit), not raw counts: ten violations in a
|
|
143
|
+
* hundred early audits vs one in one late audit is a WORSENING, not progress (Codex #7). */
|
|
144
|
+
readonly improved: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type ReadinessVerdict = 'ready' | 'insufficient-data';
|
|
148
|
+
|
|
149
|
+
export interface ReplayReadiness {
|
|
150
|
+
/** UNIQUE, untruncated prompt events — the pairs a cold-vs-warm replay needs. */
|
|
151
|
+
readonly replayablePairs: number;
|
|
152
|
+
readonly minNeeded: number;
|
|
153
|
+
/** READINESS only. `promote`/`reject` exist solely after a real cold/warm A-B has been run and
|
|
154
|
+
* bootstrapped — readiness must never look like a result (Codex #1). */
|
|
155
|
+
readonly verdict: ReadinessVerdict;
|
|
156
|
+
readonly note: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface InstrumentationHealth {
|
|
160
|
+
readonly lastUsageTs: string | null;
|
|
161
|
+
readonly gapDays: number | null;
|
|
162
|
+
/** True when the newest usage record is recent enough to trust the leg is alive. */
|
|
163
|
+
readonly applyLegLive: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface CompoundingReport {
|
|
167
|
+
readonly pool: PoolPayoff;
|
|
168
|
+
readonly guardTrajectory: readonly GuardRuleTrajectory[];
|
|
169
|
+
readonly replay: ReplayReadiness;
|
|
170
|
+
readonly instrumentation: InstrumentationHealth;
|
|
171
|
+
/** The one-line honest answer. */
|
|
172
|
+
readonly verdict: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const APPLY_LEG_STALE_DAYS = 7;
|
|
176
|
+
|
|
177
|
+
export function assembleCompoundingReport(facts: CompoundingFacts): CompoundingReport {
|
|
178
|
+
const { lessons, usage, guard } = facts;
|
|
179
|
+
|
|
180
|
+
// 1. Pool payoff — the "loops need all three legs" question, quantified.
|
|
181
|
+
const injectedIds = new Set(usage.map((u) => u.dzId));
|
|
182
|
+
const injectedEver = lessons.filter((l) => injectedIds.has(l.dzId)).length;
|
|
183
|
+
const touchedEver = lessons.filter((l) => l.uses > 0 || injectedIds.has(l.dzId)).length;
|
|
184
|
+
const total = lessons.length;
|
|
185
|
+
const pool: PoolPayoff = {
|
|
186
|
+
total,
|
|
187
|
+
injectedEver,
|
|
188
|
+
touchedEver,
|
|
189
|
+
neverTouched: total - touchedEver,
|
|
190
|
+
quarantined: lessons.filter((l) => l.quarantined).length,
|
|
191
|
+
writeOnlyRatio: total === 0 ? 0 : (total - injectedEver) / total,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// 2. Guard trajectory — do the same mistakes recur less over time? Split the record span in half
|
|
195
|
+
// by TIME (not by count: a busy afternoon must not masquerade as an era).
|
|
196
|
+
// Only events with a PARSEABLE timestamp participate; a span of zero has no halves (Codex #7).
|
|
197
|
+
const guardTimed = guard
|
|
198
|
+
.map((g) => ({ ...g, ms: Date.parse(g.ts) }))
|
|
199
|
+
.filter((g) => Number.isFinite(g.ms))
|
|
200
|
+
.sort((a, b) => a.ms - b.ms);
|
|
201
|
+
const trajectory: GuardRuleTrajectory[] = [];
|
|
202
|
+
const t0 = guardTimed.length > 0 ? guardTimed[0]!.ms : 0;
|
|
203
|
+
const t1 = guardTimed.length > 0 ? guardTimed[guardTimed.length - 1]!.ms : 0;
|
|
204
|
+
if (guardTimed.length >= 2 && t1 > t0) {
|
|
205
|
+
const mid = t0 + (t1 - t0) / 2;
|
|
206
|
+
let firstAudits = 0;
|
|
207
|
+
let secondAudits = 0;
|
|
208
|
+
for (const g of guardTimed) {
|
|
209
|
+
if (g.ms <= mid) firstAudits += 1;
|
|
210
|
+
else secondAudits += 1;
|
|
211
|
+
}
|
|
212
|
+
const perRule = new Map<string, { first: number; second: number }>();
|
|
213
|
+
for (const g of guardTimed) {
|
|
214
|
+
const inFirst = g.ms <= mid;
|
|
215
|
+
for (const rule of g.rules) {
|
|
216
|
+
const e = perRule.get(rule) ?? { first: 0, second: 0 };
|
|
217
|
+
if (inFirst) e.first += 1;
|
|
218
|
+
else e.second += 1;
|
|
219
|
+
perRule.set(rule, e);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Both halves must contain OBSERVATIONS for a rate comparison to mean anything.
|
|
223
|
+
if (firstAudits > 0 && secondAudits > 0) {
|
|
224
|
+
for (const [rule, e] of [...perRule.entries()].sort()) {
|
|
225
|
+
const firstRate = e.first / firstAudits;
|
|
226
|
+
const secondRate = e.second / secondAudits;
|
|
227
|
+
trajectory.push({
|
|
228
|
+
rule,
|
|
229
|
+
firstHalfViolations: e.first,
|
|
230
|
+
secondHalfViolations: e.second,
|
|
231
|
+
firstHalfAudits: firstAudits,
|
|
232
|
+
secondHalfAudits: secondAudits,
|
|
233
|
+
improved: secondRate < firstRate,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 3. Replay readiness — cold-vs-warm needs (query -> injected lesson) pairs. They were never
|
|
240
|
+
// recorded before 2026-07-28, so this gate REPORTS accrual instead of faking a verdict.
|
|
241
|
+
const replayKeys = new Set<string>();
|
|
242
|
+
for (const u of usage) {
|
|
243
|
+
if (typeof u.query !== 'string' || u.query.trim() === '') continue;
|
|
244
|
+
if (u.queryTruncated === true) continue; // a prefix is not the prompt
|
|
245
|
+
// one prompt = one pair, however many hits it injected
|
|
246
|
+
replayKeys.add(u.eventId ?? `${u.runId ?? ''}|${u.ts}|${u.query}`);
|
|
247
|
+
}
|
|
248
|
+
const replayablePairs = replayKeys.size;
|
|
249
|
+
const replay: ReplayReadiness = {
|
|
250
|
+
replayablePairs,
|
|
251
|
+
minNeeded: MIN_SAMPLES_PER_ARM,
|
|
252
|
+
verdict: replayablePairs >= MIN_SAMPLES_PER_ARM ? 'ready' : 'insufficient-data',
|
|
253
|
+
note:
|
|
254
|
+
replayablePairs >= MIN_SAMPLES_PER_ARM
|
|
255
|
+
? `${replayablePairs} unique prompt event(s) recorded — a cold-vs-warm replay can now be RUN (readiness, not a result)`
|
|
256
|
+
: `${replayablePairs} unique prompt event(s); ${MIN_SAMPLES_PER_ARM} needed — queries are recorded as of 2026-07-28, data is accruing`,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// 4. Instrumentation health — "no data" must be a finding, never a silent pass.
|
|
260
|
+
// Liveness compares RAW milliseconds (a 7d23h gap floored to "7 days" read as live), rejects
|
|
261
|
+
// garbage timestamps, and treats a FUTURE timestamp beyond small clock skew as evidence of a
|
|
262
|
+
// broken clock, not of liveness (Codex #8).
|
|
263
|
+
const usageTimed = usage
|
|
264
|
+
.map((u) => ({ ts: u.ts, ms: Date.parse(u.ts) }))
|
|
265
|
+
.filter((u) => Number.isFinite(u.ms))
|
|
266
|
+
.sort((a, b) => a.ms - b.ms);
|
|
267
|
+
const lastUsage = usageTimed.length > 0 ? usageTimed[usageTimed.length - 1]! : null;
|
|
268
|
+
const nowMs = Date.parse(facts.nowTs);
|
|
269
|
+
const CLOCK_SKEW_MS = 60_000;
|
|
270
|
+
const gapMs = lastUsage && Number.isFinite(nowMs) ? nowMs - lastUsage.ms : null;
|
|
271
|
+
const gapValid = gapMs !== null && gapMs >= -CLOCK_SKEW_MS;
|
|
272
|
+
const instrumentation: InstrumentationHealth = {
|
|
273
|
+
lastUsageTs: lastUsage?.ts ?? null,
|
|
274
|
+
gapDays: gapValid ? Math.max(0, Math.floor(gapMs / 86_400_000)) : null,
|
|
275
|
+
applyLegLive: gapValid && gapMs <= APPLY_LEG_STALE_DAYS * 86_400_000,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const improvedRules = trajectory.filter((t) => t.improved).length;
|
|
279
|
+
const verdict = [
|
|
280
|
+
`pool: ${injectedEver}/${total} lessons ever injected (${Math.round(pool.writeOnlyRatio * 100)}% write-only under the strict bar)`,
|
|
281
|
+
trajectory.length > 0 ? `guard: ${improvedRules}/${trajectory.length} rules recur less in the later half` : 'guard: not enough history',
|
|
282
|
+
`cold-vs-warm: ${replay.verdict === 'insufficient-data' ? 'INSUFFICIENT DATA (accruing)' : 'READY to measure'}`,
|
|
283
|
+
instrumentation.applyLegLive ? 'apply leg: live' : 'apply leg: STALE — fix the instrumentation before trusting anything above',
|
|
284
|
+
].join(' · ');
|
|
285
|
+
|
|
286
|
+
return { pool, guardTrajectory: trajectory, replay, instrumentation, verdict };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function renderCompoundingReport(r: CompoundingReport): string {
|
|
290
|
+
const out: string[] = [];
|
|
291
|
+
out.push('dz compounding — does the learning loop pay? (honest report: gates without data say so)');
|
|
292
|
+
out.push('');
|
|
293
|
+
out.push(` POOL PAYOFF: ${r.pool.total} lessons · ${r.pool.injectedEver} ever injected by the apply leg · ${r.pool.touchedEver} touched by any recall · ${r.pool.neverTouched} never touched · ${r.pool.quarantined} quarantined`);
|
|
294
|
+
out.push(` write-only ratio (strict bar): ${(r.pool.writeOnlyRatio * 100).toFixed(0)}%`);
|
|
295
|
+
out.push('');
|
|
296
|
+
if (r.guardTrajectory.length > 0) {
|
|
297
|
+
out.push(' GUARD TRAJECTORY (violations, first half vs second half of the audit span):');
|
|
298
|
+
for (const t of r.guardTrajectory) {
|
|
299
|
+
out.push(` ${t.improved ? '↓' : '·'} ${t.rule}: ${t.firstHalfViolations} → ${t.secondHalfViolations}`);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
out.push(' GUARD TRAJECTORY: not enough audit history to split');
|
|
303
|
+
}
|
|
304
|
+
out.push('');
|
|
305
|
+
out.push(` COLD-VS-WARM REPLAY: ${r.replay.note}`);
|
|
306
|
+
out.push(
|
|
307
|
+
` INSTRUMENTATION: last apply-leg record ${r.instrumentation.lastUsageTs ?? 'never'}` +
|
|
308
|
+
(r.instrumentation.gapDays !== null ? ` (${r.instrumentation.gapDays}d ago)` : '') +
|
|
309
|
+
` — ${r.instrumentation.applyLegLive ? 'live' : 'STALE'}`,
|
|
310
|
+
);
|
|
311
|
+
out.push('');
|
|
312
|
+
out.push(` VERDICT: ${r.verdict}`);
|
|
313
|
+
return out.join('\n');
|
|
314
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -321,3 +321,8 @@ export * from './delivery-check.js';
|
|
|
321
321
|
// Skill-registration gate (feature skills-verify, ADR-001) — static layout scan + the deterministic
|
|
322
322
|
// `system/init` listing. Fail-closed: an unobservable registration is `inconclusive`, never `pass`.
|
|
323
323
|
export * from './skills-verify.js';
|
|
324
|
+
|
|
325
|
+
// Learning-loop payoff measurement (feature compounding, scout C2) — seeded stats ported verbatim
|
|
326
|
+
// from darwin-mode; measurements are dz-native and NEVER fake a verdict (INSUFFICIENT_DATA is a
|
|
327
|
+
// finding, not a pass).
|
|
328
|
+
export * from './compounding.js';
|
package/src/operations.ts
CHANGED
|
@@ -749,6 +749,30 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
|
|
|
749
749
|
detail: `deployed writer v${deployed} < current v${AGENTDB_WRITER_VERSION} — re-run dz setup to upgrade (no --force needed)`,
|
|
750
750
|
});
|
|
751
751
|
}
|
|
752
|
+
// APPLY-LEG LIVENESS (2026-07-28): the recall hook injects lessons only while the embed daemon's
|
|
753
|
+
// socket is alive, and the daemon is started at SessionStart only — when it died mid-way through a
|
|
754
|
+
// long-lived session the whole apply leg went silently dark for 19 days (MEASURED:
|
|
755
|
+
// recall-usage.jsonl last record 2026-07-09 with the socket absent). A dead leg must be VISIBLE.
|
|
756
|
+
try {
|
|
757
|
+
const settingsPath = join(root, '.claude', 'settings.json');
|
|
758
|
+
if (existsSync(settingsPath)) {
|
|
759
|
+
const settingsText = readFileSync(settingsPath, 'utf-8');
|
|
760
|
+
const applyLegWired = settingsText.includes('recall-hook.cjs') && settingsText.includes('dz-embed-daemon.mjs');
|
|
761
|
+
if (applyLegWired) {
|
|
762
|
+
const sockAlive = existsSync(join(root, '.dz', 'embed.sock'));
|
|
763
|
+
checks.push({
|
|
764
|
+
name: 'apply-leg alive (embed daemon)',
|
|
765
|
+
ok: sockAlive,
|
|
766
|
+
detail: sockAlive
|
|
767
|
+
? 'embed.sock present — recall injection can run'
|
|
768
|
+
: 'embed.sock ABSENT: the recall hook is wired but cannot inject (the hook self-heals on the next prompt; a persistent absence means the daemon cannot start)',
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
} catch {
|
|
773
|
+
/* doctor never throws on a diagnostic */
|
|
774
|
+
}
|
|
775
|
+
|
|
752
776
|
// Version drift between the local agentdb copy and the .mcp.json pin (gap G7)
|
|
753
777
|
try {
|
|
754
778
|
const localVer = (JSON.parse(readFileSync(join(root, 'node_modules', 'agentdb', 'package.json'), 'utf-8')) as { version?: string }).version;
|
package/src/recall-usage.ts
CHANGED
|
@@ -12,13 +12,31 @@
|
|
|
12
12
|
export const RECALL_USAGE_LOG_RELATIVE = '.dz/recall-usage.jsonl';
|
|
13
13
|
export const RECALL_USAGE_LOG_MAX_BYTES = 1_048_576;
|
|
14
14
|
export const RECALL_USAGE_COMPACT_TARGET_BYTES = Math.floor(RECALL_USAGE_LOG_MAX_BYTES * 0.75);
|
|
15
|
+
/** Newest query-bearing read rows survive compaction verbatim — they are the replay corpus. */
|
|
16
|
+
export const RECALL_USAGE_REPLAY_KEEP = 500;
|
|
15
17
|
|
|
16
18
|
export interface RecallUsageReadRecord {
|
|
17
19
|
readonly dzId: string;
|
|
18
20
|
readonly score: number;
|
|
19
21
|
readonly ts: string;
|
|
22
|
+
/**
|
|
23
|
+
* The PROMPT the lesson was injected into (truncated). Without it the log can say a lesson was
|
|
24
|
+
* used but never say FOR WHAT — which made cold-vs-warm replay unbuildable from 39 recorded
|
|
25
|
+
* events (MEASURED, compounding data inventory 2026-07-28). `.dz/` is git-ignored, so a truncated
|
|
26
|
+
* query stays on this machine.
|
|
27
|
+
*/
|
|
28
|
+
readonly query?: string;
|
|
29
|
+
/** The session/run the injection happened in — lets replay group events into runs. */
|
|
30
|
+
readonly runId?: string;
|
|
31
|
+
/** One id per PROMPT (the hook writes one row per injected hit — up to 3 per prompt). */
|
|
32
|
+
readonly eventId?: string;
|
|
33
|
+
/** True when the stored query is a PREFIX of the real prompt — not replayable. */
|
|
34
|
+
readonly queryTruncated?: boolean;
|
|
20
35
|
}
|
|
21
36
|
|
|
37
|
+
/** Query text is capped so a pasted wall of text cannot bloat the log. */
|
|
38
|
+
export const RECALL_USAGE_QUERY_MAX_CHARS = 200;
|
|
39
|
+
|
|
22
40
|
export interface RecallUsageAggregateRecord {
|
|
23
41
|
readonly kind: 'aggregate';
|
|
24
42
|
readonly dzId: string;
|
|
@@ -90,6 +108,10 @@ export function formatRecallUsageRecord(input: {
|
|
|
90
108
|
readonly dzId?: unknown;
|
|
91
109
|
readonly score?: unknown;
|
|
92
110
|
readonly ts?: unknown;
|
|
111
|
+
readonly query?: unknown;
|
|
112
|
+
readonly runId?: unknown;
|
|
113
|
+
readonly eventId?: unknown;
|
|
114
|
+
readonly queryTruncated?: unknown;
|
|
93
115
|
}): string | undefined {
|
|
94
116
|
const rec = normalizeReadRecord(input);
|
|
95
117
|
return rec === undefined ? undefined : `${JSON.stringify(rec)}\n`;
|
|
@@ -198,8 +220,16 @@ export function compactRecallUsageLog(
|
|
|
198
220
|
const maxBytes = validMax(opts.maxBytes ?? RECALL_USAGE_LOG_MAX_BYTES);
|
|
199
221
|
const targetBytes = validTarget(opts.targetBytes ?? Math.floor(maxBytes * 0.75), maxBytes);
|
|
200
222
|
const compactedAt = validTs(opts.compactedAt) ? opts.compactedAt : new Date(0).toISOString();
|
|
201
|
-
const
|
|
202
|
-
const
|
|
223
|
+
const parsed = parseRecallUsageLog(text).records;
|
|
224
|
+
const stats = aggregateRecallUsage(parsed);
|
|
225
|
+
// The query-bearing read rows are the ONLY inputs a cold-vs-warm replay has — aggregating them
|
|
226
|
+
// away silently reset the accruing corpus at the size threshold (Codex #4). Keep the NEWEST ones
|
|
227
|
+
// (bounded) verbatim alongside the aggregates.
|
|
228
|
+
const replayRows = parsed
|
|
229
|
+
.filter((r): r is RecallUsageReadRecord => !('kind' in r) && typeof (r as RecallUsageReadRecord).query === 'string')
|
|
230
|
+
.sort((a, b) => a.ts.localeCompare(b.ts))
|
|
231
|
+
.slice(-RECALL_USAGE_REPLAY_KEEP);
|
|
232
|
+
const lines = [...stats.map((s) => aggregateLine(s, compactedAt)), ...replayRows.map((r) => JSON.stringify(r))];
|
|
203
233
|
let out = joinLines(lines);
|
|
204
234
|
if (byteLength(out) <= maxBytes) return out;
|
|
205
235
|
|
|
@@ -232,7 +262,19 @@ function normalizeReadRecord(value: unknown): RecallUsageReadRecord | undefined
|
|
|
232
262
|
if (typeof dzId !== 'string' || dzId.trim() === '') return undefined;
|
|
233
263
|
if (typeof score !== 'number' || !Number.isFinite(score)) return undefined;
|
|
234
264
|
if (!validTs(ts)) return undefined;
|
|
235
|
-
|
|
265
|
+
const query = value['query'];
|
|
266
|
+
const runId = value['runId'];
|
|
267
|
+
return {
|
|
268
|
+
dzId: dzId.trim(),
|
|
269
|
+
score,
|
|
270
|
+
ts,
|
|
271
|
+
...(typeof query === 'string' && query.trim() !== ''
|
|
272
|
+
? { query: query.slice(0, RECALL_USAGE_QUERY_MAX_CHARS) }
|
|
273
|
+
: {}),
|
|
274
|
+
...(typeof runId === 'string' && runId.trim() !== '' ? { runId: runId.trim() } : {}),
|
|
275
|
+
...(typeof value['eventId'] === 'string' && (value['eventId'] as string).trim() !== '' ? { eventId: (value['eventId'] as string).trim() } : {}),
|
|
276
|
+
...(value['queryTruncated'] === true ? { queryTruncated: true } : {}),
|
|
277
|
+
};
|
|
236
278
|
}
|
|
237
279
|
|
|
238
280
|
function normalizeAggregateRecord(value: Record<string, unknown>): RecallUsageAggregateRecord | undefined {
|