practice restructure — per-problem jobs + ONE shared background-LLM pool (owner ruling 2026-09-29)
Rulings: each problem generation is its own visible dock job (the extraction per-chunk pattern), and practice shares extraction's semaphore — one process-wide 3-permit pool for ALL background LLM jobs; total background LLM concurrency bounded at 3. A. SHARED POOL: new src/agent/subagents/bg-llm-semaphore.ts — globalThis __lhLlmSlots, PERMIT_COUNT 3 (the ruling), the fresh-take/handed-off-permit deadlock discipline preserved for BOTH callers. extraction.ts's withExtractSlot is now a thin wrapper (job-row signal → withBgLlmSlot); __lhExtractSlots and __lhPracticeSlots both die. The partitioner now acquires a permit too (it's a background LLM job — it didn't before). B. REGISTRY ROWS: the aggregate setId:generation / setId:grading rows are replaced by setId:partition (phase partitioning, label 'Partitioning — title'), setId:gen:problemId (queued→generating, 'Problem i/N — title'), setId:grade:problemId (queued→grading, 'Grading i/N — title'). Each row carries its OWN abort controller and its OWN usage (one PracticeStepLog per row — the aggregate partitioner/generator/grader folding dies). Row key = feed id = abort key (the dock dup-key law). C. ABORT SEMANTICS: abortPracticeRow(key) routes by shape — gen row: THAT problem only (error 'Aborted', siblings continue, set settles ready with one errored problem); partition row: the whole run; grade row: that grade drops (answer kept, re-submittable). abortPractice(setId) composes from row aborts + the synchronous persisted marks (a set-abort's verdict stands before the settles re-read it — the settle step keeps an existing error status). /api/activity/abort kind practice now takes the ROW key (charset admits ':'). D. FEED + DOCK: ActivityPractice/FeedPractice carry per-row identity (id = row key, problemIndex/problemTotal); dock blobs render per row, labels carry the i/N; the aggregate row is gone. E. Boot sweep unchanged at set grain; the live-row check is now a key-prefix scan (any live row of the set protects it). The updatedAt heartbeat covers the new row scheme as-is. Tests (20 in the pipeline suite): per-problem row shape/uniqueness/ i-N labels (partition row frozen behind a gated partitioner; per-row usage ISOLATION pin — b's log never shows on c's row); abort-one-gen (sibling completes, set ready with one errored problem); partition- row abort; queued grade-row abort (dead-ticket path: answer kept, re-submittable, sibling lands); shared-pool identity + contention (practice queues behind extraction-style holders); registry/dock feed mapping. Test isolation hardened: unique per-store set ids, afterEach registry/lock/pool reset (a timed-out test can no longer poison the rest). architecture.md: jobs-pattern paragraph rewritten (per-problem rows, shared pool, abort routing). 1149/1149, tsc 0, lint 0, build 0.
This commit is contained in:
@@ -56,14 +56,19 @@ export interface ActivityExtraction {
|
||||
startedAt: number;
|
||||
}
|
||||
|
||||
/** A live practice run in the dock's feed — mirrors ActivityExtraction
|
||||
* (kind names which pipeline half: generation or grading). */
|
||||
/** A live practice ROW in the dock's feed — per-problem, matching the
|
||||
* extraction per-chunk pattern (owner ruling, 2026-09-29). id is the
|
||||
* ROW KEY (`${setId}:partition` | `${setId}:gen:${pid}` |
|
||||
* `${setId}:grade:${pid}`) — the dock's abort target; problemIndex/
|
||||
* problemTotal ride when the row belongs to a fan-out. */
|
||||
export interface ActivityPractice {
|
||||
id: string;
|
||||
kind: "generation" | "grading";
|
||||
title: string;
|
||||
phase: PracticePhase;
|
||||
startedAt: number;
|
||||
problemIndex?: number;
|
||||
problemTotal?: number;
|
||||
}
|
||||
|
||||
/** The dock's whole world in one payload: every running process, every view.
|
||||
@@ -111,11 +116,13 @@ export function activityFeed(): {
|
||||
startedAt: run.startedAt,
|
||||
}));
|
||||
const practice: ActivityPractice[] = practiceStatuses().map((row) => ({
|
||||
id: row.setId,
|
||||
id: row.key,
|
||||
kind: row.kind,
|
||||
title: row.label,
|
||||
phase: row.phase,
|
||||
startedAt: row.startedAt,
|
||||
problemIndex: row.problemIndex,
|
||||
problemTotal: row.problemTotal,
|
||||
}));
|
||||
// oldest-first on last activity: the stable blob order (ISO strings sort chronologically)
|
||||
jobs.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
|
||||
|
||||
Reference in New Issue
Block a user