Skip to content

Commit 08aea1b

Browse files
committed
fix(kb-watcher): include health signals in signature — stale 'pending audit' banner
User report: sidebar Status section showed '1 background audit still running' even though the session's auditStatus on disk was 'done'. Root cause: KbWatcher's change-detection signature was counts-only (memories|decisions|safety|backlog|questions). When a session's auditStatus transitioned 'pending' → 'done' (auditor just finished), the counts didn't move, signature didn't change, listener didn't fire, sidebar kept showing the stale banner. Fix: signatureOf() now folds in three health signals — pendingAudits, lastAuditError timestamp, lastHandoffMtimeMs. Any of them changing triggers a listener fire on the next 5-second poll tick. Sidebar re-reads health and re-renders, banner disappears the moment the auditor flips meta.json to 'done'. Why not push the WHOLE health snapshot on every tick: re-rendering the webview costs nothing visible (DOM diff is identical when state didn't change), but if we always pushed regardless, the agent's auto-bootstrap path would chatter the webview on every poll for no signal — better to gate on a compact signature.
1 parent 918ac8d commit 08aea1b

1 file changed

Lines changed: 30 additions & 8 deletions

File tree

extension/src/kb-watcher.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,32 @@ function emptyCounts(): KbCounts {
3030
}
3131

3232
/** Compact signature for change detection. Only fires the listener
33-
* callback when at least one count actually moved — keeps the sidebar
34-
* from re-rendering on every 5-second poll tick when nothing changed. */
35-
function signatureOf(c: KbCounts): string {
36-
return `${c.memories}|${c.decisions}|${c.safety}|${c.backlog}|${c.questions}`;
33+
* callback when at least one tracked value actually moved — keeps the
34+
* sidebar from re-rendering on every 5-second poll tick when nothing
35+
* changed.
36+
*
37+
* Includes BOTH KB counts AND lightweight health signals (pending
38+
* audit count + most-recent handoff mtime). Without health in the
39+
* signature, transitions like "1 pending audit → 0" (auditor just
40+
* finished) wouldn't trigger a refresh — the sidebar's pending banner
41+
* would stay up forever until something else moved counts. This was
42+
* the actual bug the user hit: closing the session set auditStatus to
43+
* "pending" → auditor finished → "done", but the banner stayed.
44+
*/
45+
function signatureOf(c: KbCounts, healthSig: string): string {
46+
return `${c.memories}|${c.decisions}|${c.safety}|${c.backlog}|${c.questions}|${healthSig}`;
47+
}
48+
49+
/** Compact view of health used by signatureOf. We don't want the whole
50+
* HealthSnapshot in the signature — just the bits that, if they
51+
* change, mean the sidebar render output also changes. */
52+
function healthSignature(workspaceRoot: string): string {
53+
// Lazy require to avoid circular deps with health-reader if it ever
54+
// grows to import from kb-watcher.
55+
// eslint-disable-next-line @typescript-eslint/no-require-imports
56+
const { readHealth } = require("./health-reader.js") as typeof import("./health-reader.js");
57+
const h = readHealth(workspaceRoot);
58+
return `${h.pendingAudits}|${h.lastAuditError?.whenIso ?? ""}|${h.lastHandoffMtimeMs ?? ""}`;
3759
}
3860

3961
function countFilesIn(dir: string, suffix = ".md"): number {
@@ -192,11 +214,11 @@ export class KbWatcher implements vscode.Disposable {
192214
if (existsSync(join(workspaceRoot, ".axme-code"))) {
193215
this.startContentWatcher(workspaceRoot);
194216
const c = readCounts(workspaceRoot);
195-
this.lastSig = signatureOf(c);
217+
this.lastSig = signatureOf(c, healthSignature(workspaceRoot));
196218
onChange(c);
197219
} else {
198220
onChange(emptyCounts());
199-
this.lastSig = signatureOf(emptyCounts());
221+
this.lastSig = signatureOf(emptyCounts(), "");
200222
this.startRootWatcher(workspaceRoot);
201223
}
202224
this.startPolling();
@@ -227,7 +249,7 @@ export class KbWatcher implements vscode.Disposable {
227249
try { this.creationListener?.(); } catch { /* swallow */ }
228250
}
229251
const counts = readCounts(this.workspaceRoot);
230-
const sig = signatureOf(counts);
252+
const sig = signatureOf(counts, healthSignature(this.workspaceRoot));
231253
if (sig !== this.lastSig) {
232254
this.lastSig = sig;
233255
this.listener(counts);
@@ -260,7 +282,7 @@ export class KbWatcher implements vscode.Disposable {
260282
if (!this.workspaceRoot || !this.listener) return;
261283
try { statSync(join(this.workspaceRoot, ".axme-code")); } catch { return; }
262284
const counts = readCounts(this.workspaceRoot);
263-
const sig = signatureOf(counts);
285+
const sig = signatureOf(counts, healthSignature(this.workspaceRoot));
264286
if (sig !== this.lastSig) {
265287
this.lastSig = sig;
266288
this.listener(counts);

0 commit comments

Comments
 (0)