push architecture slice 5: extraction/practice registries announce activity-changed; dock subscribes (debounced) — the adaptive poll and every chat-view dock nudge retire

This commit is contained in:
amosblomqvist
2026-10-02 17:31:35 +03:00
parent a38c400eb7
commit d6450e260a
6 changed files with 164 additions and 102 deletions
+15 -3
View File
@@ -25,6 +25,7 @@ import type {
} from "../../core/contracts.ts";
import { getGraph, mergeFindings, type DerivedGraph, type EmissionStats, type GraphDeps } from "../../core/graph.ts";
import type { GraphStore } from "../../core/ports.ts";
import { emit } from "../../core/events.ts";
import { settleGraphLayout } from "../../core/layout-settle.ts";
import { batchRuns, coverageOf, messageViewLines } from "../../core/coverage.ts";
import { teacherProseBlocks } from "../../core/coverage.ts";
@@ -722,6 +723,17 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
usage: prev?.usage ?? {}, // phase changes never lose usage
controller: prev?.controller ?? new AbortController(),
});
// Every phase transition announces the dock (push S5, 2026-10-02):
// notify-after-write — the registry is the feed's truth.
emit("activity-changed", {});
}
/** Run over (done, failed, aborted — every exit is a finally): the row
* leaves the feed. ONE seam for all delete sites so no exit path can
* miss the announce (push S5, 2026-10-02). */
function finishExtraction(jobKey: string): void {
runningExtractions.delete(jobKey);
emit("activity-changed", {});
}
/** Accumulate one billed step into the run's registry entry (live). */
@@ -899,7 +911,7 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
persistRunUsage(jobKey, Math.round(performance.now() - t0), undefined);
throw err;
} finally {
runningExtractions.delete(jobKey);
finishExtraction(jobKey);
}
}
@@ -1062,7 +1074,7 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
throw err;
} finally {
inFlight.delete(chatId);
runningExtractions.delete(chatId); // done or failed — no longer running
finishExtraction(chatId); // done or failed — no longer running
}
}
@@ -1217,7 +1229,7 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
throw err;
} finally {
inFlight.delete(id);
runningExtractions.delete(id); // done or failed — no longer running
finishExtraction(id); // done or failed — no longer running
}
}
+19 -5
View File
@@ -43,6 +43,7 @@
*/
import type { GraphStore, PracticeStore } from "../../core/ports.ts";
import { emit } from "../../core/events.ts";
import type { Problem, ProblemSet, ReasoningChain } from "../../core/contracts.ts";
import { chainsForSelection } from "../../core/practice.ts";
import { getGraph } from "../../core/graph.ts";
@@ -197,9 +198,20 @@ function registerRow(
controller: new AbortController(),
};
practiceRuns().set(key, row);
// The dock learns the row at the moment of cause (push S5, 2026-10-02):
// notify-after-write — the registry is the feed's truth.
emit("activity-changed", {});
return row;
}
/** Row over (settled, failed, aborted — every exit): the row leaves the
* feed. ONE seam for all delete sites so no exit path can miss the
* announce (push S5, 2026-10-02). */
function finishPractice(key: string): void {
practiceRuns().delete(key);
emit("activity-changed", {});
}
/** DEDUP INPUT (computed ONCE at creation, passed to partitioner and
* every generator): ready problems from the most recent ≤5 sets sharing
* at least one selected node. */
@@ -318,7 +330,7 @@ export function createPracticePipeline(deps: PracticePipelineDeps) {
partitions = submitted.partitions;
} catch (err) {
await markSetError(set.id, isAbortLike(err) || controller.signal.aborted ? "Aborted" : messageOf(err));
practiceRuns().delete(partitionRow.key);
finishPractice(partitionRow.key);
return;
}
@@ -340,7 +352,7 @@ export function createPracticePipeline(deps: PracticePipelineDeps) {
deps.practiceStore.saveSet(cur);
return cur;
});
practiceRuns().delete(partitionRow.key);
finishPractice(partitionRow.key);
if (!saved) return; // deleted mid-run — stop quietly
const genRows = stubs.map((stub, i) =>
@@ -367,6 +379,7 @@ export function createPracticePipeline(deps: PracticePipelineDeps) {
throw err;
}
row.phase = "generating";
emit("activity-changed", {}); // in-place phase write — announce it
const generated = await agents.runProblemGenerator({
target: stub.chains,
fullChains: set.chains,
@@ -404,7 +417,7 @@ export function createPracticePipeline(deps: PracticePipelineDeps) {
deps.practiceStore.saveSet(cur);
});
} finally {
practiceRuns().delete(row.key);
finishPractice(row.key);
}
}),
);
@@ -535,6 +548,7 @@ export function createPracticePipeline(deps: PracticePipelineDeps) {
await withBgLlmSlot(controller.signal, async () => {
if (controller.signal.aborted) return; // dropped — no grade, no error state
row.phase = "grading";
emit("activity-changed", {}); // in-place phase write — announce it
const set = deps.practiceStore.getSet(setId);
if (!set) return;
const problem = set.problems.find((p) => p.id === problemId);
@@ -571,7 +585,7 @@ export function createPracticePipeline(deps: PracticePipelineDeps) {
// stands — one attempt; the UI shows "grade interrupted").
console.error(`[practice] grading failed for ${problemId}:`, isAbortLike(err) ? "aborted" : messageOf(err));
} finally {
practiceRuns().delete(row.key);
finishPractice(row.key);
}
}
@@ -588,7 +602,7 @@ export function createPracticePipeline(deps: PracticePipelineDeps) {
const row = practiceRuns().get(key);
if (!row) return;
row.controller.abort();
practiceRuns().delete(key);
finishPractice(key);
}
/** SET-level abort — composes from row aborts (every live row of the