From 61b4fecdaf0b546743fa42186f8887b72c14c64a Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sun, 10 May 2026 20:26:18 +0200 Subject: [PATCH] fix(notices+db): complete NOTICE_KIND tagging, fix slice-dep query, cap error storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOTICE_KIND tagging: - auto.js: ctrl-c-pause (USER_VISIBLE), auto-start-failed/session-lock-lost/ stopAuto/debug-summary-written (SYSTEM_NOTICE), auto-no-command-ctx (USER_VISIBLE) - loop.js: model-policy-blocked SYSTEM_NOTICE→BLOCKING_NOTICE (user must act), solver-eval results/infra-stop/consecutive-cooldowns (SYSTEM_NOTICE), phase-timeout/credential-cooldown-wait/iteration-error (TOOL_NOTICE); fix import order - register-hooks.js: destructive-command (TOOL_NOTICE), gemini-preflight (SYSTEM_NOTICE) - provider-error-pause.js: auto-resume (TOOL_NOTICE), scheduled-resume (SYSTEM_NOTICE), permanent-pause (BLOCKING_NOTICE) - uok-parity-summary.js: parity warning (SYSTEM_NOTICE) sf-db fixes: - getActiveSliceFromDb: use slice_dependencies junction table instead of json_each(s.depends) — junction table is kept in sync by syncSliceDependencies - capErrorForStorage: cap UOK run error blobs at 4 KB; excess spills to .sf/runtime/errors/.txt to prevent DB bloat from large stack traces ARCHITECTURE.md: - Document DB-first invariant; remove .sf/DECISIONS.md/.REQUIREMENTS.md/.KNOWLEDGE.md from tracked-file list (they are rendered projections, not authoritative sources) - Add .sf/traces/ and .sf/metrics.db to gitignored list - Update system-context assembly order to show DB-sourced decisions/requirements - Correct system-context.ts → system-context.js Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ARCHITECTURE.md | 59 +++++++++++-------- src/resources/extensions/sf/auto.js | 25 ++++++-- src/resources/extensions/sf/auto/loop.js | 45 ++++++++++++-- .../extensions/sf/bootstrap/register-hooks.js | 8 +++ .../extensions/sf/provider-error-pause.js | 15 ++++- src/resources/extensions/sf/sf-db.js | 31 +++++++--- .../extensions/sf/uok-parity-summary.js | 6 +- 7 files changed, 146 insertions(+), 43 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6e7dec2fd..35c169a6b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,46 +28,43 @@ Singularity Forge (SF) is the product. It runs long-horizon coding work through **Tracked in git** (travel with the branch, per ADR-001): ``` -.sf/milestones/ — roadmaps, plans, summaries, task plans +.sf/milestones/ — roadmaps, plans, summaries, task plans (rendered projections from DB) .sf/PROJECT.md — project overview -.sf/DECISIONS.md — architectural decisions register -.sf/REQUIREMENTS.md — requirements register -.sf/QUEUE.md — work queue / backlog -.sf/KNOWLEDGE.md — project-specific rules for agents ``` **Gitignored** (runtime/ephemeral — managed by `ensureGitInfoExclude()` in `.git/info/exclude`): ``` .sf/activity/ — JSONL session dumps -.sf/audit/ — audit trail entries +.sf/audit/ — audit trail entries (primary: events.jsonl) .sf/exec/ — in-flight execution state .sf/forensics/ — crash forensics .sf/journal/ — SF journal entries .sf/model-benchmarks/ — model benchmark results .sf/parallel/ — parallel dispatch coordination .sf/reports/ — generated reports -.sf/runtime/ — dispatch records, timeout tracking +.sf/runtime/ — dispatch records, timeout tracking, error spill files +.sf/traces/ — per-session trace JSONL (gate runs, git ops); latest symlink .sf/worktrees/ — git worktree working directories .sf/auto.lock — crash detection sentinel -.sf/metrics.json — token/cost accumulator +.sf/metrics.db — token/cost metrics (dedicated DB, separate from sf.db) .sf/sf.db* — SQLite canonical structured state, priority order, validation/gate state, and UOK ledgers -.sf/STATE.md — derived state cache -.sf/notifications.jsonl, .sf/routing-history.json, .sf/self-feedback.jsonl, .sf/repo-meta.json ``` The symlink case uses a blanket `.sf` gitignore pattern (git cannot traverse symlinks). The directory case uses granular patterns so planning artifacts remain trackable. +**DB-first invariant:** `sf.db` is the single source of truth for all structured state (milestones, slices, tasks, decisions, requirements, memories, self-feedback). Markdown files under `.sf/` are rendered projections or human-editable inputs — they are never the authoritative source when the DB is open. Agents write to DB via tool calls (`save_decision`, `save_knowledge`, `save_requirement`, `update_requirement`), not by appending to `.md` files. + ## Key flows **Autonomous dispatch loop** (`src/resources/extensions/sf/auto/`): -1. UOK reconciles the DB-backed ledger, projections, and runtime diagnostics into a typed state snapshot -2. Controller selects the next dispatch unit (research, plan, implement, verify, etc.) from canonical state -3. A fresh agent context is started with the task plan injected via `system-context.ts` +1. UOK reconciles the DB-backed ledger and runtime diagnostics into a typed state snapshot +2. Controller selects the next dispatch unit (research, plan, implement, verify, etc.) from canonical DB state +3. A fresh agent context is started with the task plan injected via `system-context.js` 4. Agent writes artifacts to disk, commits, exits 5. UOK records completion/recovery, updates projections, and repeats until milestone completes or a gate fails -**System context assembly** (`bootstrap/system-context.ts`): -`PREFERENCES.md` → `KNOWLEDGE.md` → `ARCHITECTURE.md` → `CODEBASE.md` → code intelligence → memories → worktree/VCS blocks +**System context assembly** (`bootstrap/system-context.js`): +`PREFERENCES.md` → project knowledge (DB memories table) → `ARCHITECTURE.md` → `CODEBASE.md` → code intelligence → active decisions (DB) → active requirements (DB) → self-feedback (DB) → worktree/VCS blocks **Write gate** (`bootstrap/write-gate.ts`): All file writes in autonomous mode pass through a gate. Protected files (CLAUDE.md, CODEBASE.md, certain spec files) require explicit override. @@ -102,6 +99,7 @@ PhaseDiscuss → PhasePlan → PhaseExecute → PhaseMerge → PhaseComplete - Failed gates can trigger automatic remediation slices (new plan → execute loop) - Stuck-loop detection: if the same unit repeats without progress after N attempts, invoke recovery protocol (timeout, manual review, or skip) - Crash recovery: `.sf/auto.lock` sentinel + `sf.db` WAL enables recovery from agent crash mid-phase +- Run errors are capped at 4 KB in `uok_runs.error`; payloads exceeding that spill to `.sf/runtime/errors/.txt` ## Gate Verdict Semantics @@ -115,6 +113,8 @@ Every gate runs in parallel and returns one of three verdicts: **Critical rule:** `omitted` must have a one-line reason (e.g., "no auth surface"). Unexplained omitted verdicts are treated as failures and re-dispatched with explicit instruction to pick `passed` or `failed`. +Gate run history is written to `.sf/traces/.jsonl` (append-only JSONL, not DB). Gate circuit-breaker state lives in the `gate_circuit_breakers` table in `sf.db`. + ## Outcome Learning for Model Selection UOK tracks model success/failure per task-type using Bayesian updating: @@ -130,21 +130,28 @@ P(model_i succeeds | task_type) = (successes + prior) / (total_trials + prior_we - Prior weights prevent early abandonment (new models get benefit of the doubt) - Used by `benchmark-selector.ts` to route future similar tasks to higher-scoring models -**Current limitation:** Learning updates episodically (per-task), not continuously; recovery paths don't feed learning back. - ## Self-Evolution Mechanisms ### Self-Report Collection -Agents and gates file `sf_self_report` with anomalies during dispatch: -- Example: "validation-reviewer prompt lacks explicit rubric for criterion vs. implementation gap" -- Reports captured in `upstream-feedback.jsonl` and `.sf/SELF-FEEDBACK.md` (when dogfooding) -- **Status:** Collection works; triage pipeline incomplete (reports not automatically processed into fixes) +Agents and gates file issues via the `report_issue` tool during dispatch: +- Reports stored in `self_feedback` table in `sf.db` +- Triage pipeline (`triage-self-feedback.js`) runs at session start to cluster and prioritize entries +- High/critical entries surfaced in system context for the next planning round +- **Status:** Collection and triage injection are active ### Knowledge Compounding -`KNOWLEDGE.md` stores judgment-log entries from completed slices: -- Format: `[when] [what] [why] [confidence]` (e.g., "Python 3.12 incompatible with X library — avoid for now") -- Persists across milestones; can be injected into future dispatch prompts -- **Status:** Storage works; injection not automatic (requires manual configuration) +Knowledge entries are stored in the `memories` table in `sf.db` (category: `knowledge`): +- Agents write via `save_knowledge` tool (not by appending to files) +- Injected into agent prompts via `system-context.js` (DB query, keyword-scoped, budget-capped) +- `knowledge-compounding.js` distills high-confidence judgment-log entries after each milestone close +- **Status:** Storage, injection, and compounding are all active + +### Requirement Promotion +`requirement-promoter.js` sweeps `self_feedback` entries at session start: +- Clusters recurring feedback by kind (count ≥ 5 or spanning ≥ 3 milestones) +- Promotes clusters to the `requirements` table via `upsertRequirement` +- Promoted entries are marked resolved in `self_feedback` +- **Status:** Active ### Gate-Based Pattern Detection Gates can detect and report repeated failure patterns (e.g., "same requirement-validation failure in S01 and S03") @@ -155,6 +162,6 @@ Gates can detect and report repeated failure patterns (e.g., "same requirement-v - UOK and the dispatch controller are pure TypeScript — no LLM decisions in the dispatch loop itself. - Each dispatch unit runs in a fresh context — no cross-turn state accumulation. - Planning artifacts are tracked in git; runtime artifacts are never committed. -- DB-backed state is the only executable truth for migrated milestones: planning hierarchy, `sequence` priority/order, validation assessments, gate runs, quality gates, UOK runtime policy, and outcome ledgers all come from SQLite. Markdown/JSON projections are human views, exports, diagnostics, or explicit recovery/import inputs; normal runtime does not fall back to them when `.sf/sf.db` exists and opens. +- **DB-first:** `sf.db` is the only executable truth. Agents read decisions, requirements, and knowledge from DB-injected context; they write back via tool calls. `.md` projection files are rendered outputs, not inputs. - `SF_RUNTIME_PATTERNS` in `gitignore.ts` is the canonical source of truth for runtime paths. `git-service.ts` (`RUNTIME_EXCLUSION_PATHS`) and `worktree-manager.ts` (`SKIP_*` arrays) must stay synchronized with it. - The user is the end-gate. SF delivers for review, not to production. diff --git a/src/resources/extensions/sf/auto.js b/src/resources/extensions/sf/auto.js index b9336bcf6..0bb4cdb39 100644 --- a/src/resources/extensions/sf/auto.js +++ b/src/resources/extensions/sf/auto.js @@ -228,7 +228,10 @@ function registerCtrlCInterceptor(ctx) { _ctrlCUnsubscribe = ctx.ui.onTerminalInput((data) => { if (data !== "\x03") return undefined; if (!s.active) return undefined; - ctx.ui.notify("Ctrl+C received — pausing autonomous mode.", "info"); + ctx.ui.notify("Ctrl+C received — pausing autonomous mode.", "info", { + noticeKind: NOTICE_KIND.USER_VISIBLE, + dedupe_key: "auto-ctrl-c-pause", + }); void pauseAuto(ctx, null, "ctrl-c-interrupt"); return { consume: true }; }); @@ -319,7 +322,9 @@ function normalizeSessionFilePath(raw) { export function startAutoDetached(ctx, pi, base, verboseMode, options) { void startAuto(ctx, pi, base, verboseMode, options).catch((err) => { const message = getErrorMessage(err); - ctx.ui.notify(`Auto-start failed: ${message}`, "error"); + ctx.ui.notify(`Auto-start failed: ${message}`, "error", { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + }); logWarning("engine", `auto start error: ${message}`, { file: "auto.ts" }); debugLog("auto-start-failed", { error: message }); }); @@ -715,7 +720,10 @@ function handleLostSessionLock(ctx, lockStatus) { : lockStatus?.failureReason === "compromised" ? `Session lock (${lockFilePath}) was compromised during heartbeat checks (PID ${process.pid}). This can happen after long event loop stalls during subagent execution.${recoverySuggestion}` : `Session lock lost (${lockFilePath}). Stopping gracefully.${recoverySuggestion}`; - ctx?.ui.notify(message, "error"); + ctx?.ui.notify(message, "error", { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: `session-lock-lost:${lockStatus?.failureReason ?? "unknown"}`, + }); ctx?.ui.setStatus("sf-auto", undefined); safeSetWidget(ctx, "sf-progress", undefined); ctx?.ui.setFooter(undefined); @@ -973,8 +981,10 @@ export async function stopAuto(ctx, pi, reason) { reason !== undefined && reason.toLowerCase().includes("block"); const stopMeta = { kind: "terminal", + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, ...(isBlocked ? { blocking: true } : {}), source: "workflow", + dedupe_key: "autonomous-stopped-ledger-summary", }; const ledger = getLedger(); if (ledger && ledger.units.length > 0) { @@ -1014,7 +1024,10 @@ export async function stopAuto(ctx, pi, reason) { if (isDebugEnabled()) { const logPath = writeDebugSummary(); if (logPath) { - ctx?.ui.notify(`Debug log written → ${logPath}`, "info"); + ctx?.ui.notify(`Debug log written → ${logPath}`, "info", { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: "debug-summary-written", + }); } } } catch (e) { @@ -1447,6 +1460,10 @@ export async function startAuto(ctx, pi, base, verboseMode, options) { ctx.ui.notify( "Autonomous mode requires a command context with newSession. Run /autonomous once first, then use the keyboard shortcut.", "warning", + { + noticeKind: NOTICE_KIND.USER_VISIBLE, + dedupe_key: "auto-no-command-ctx", + }, ); debugLog("startAuto", { phase: "no-command-ctx", skipping: true }); return; diff --git a/src/resources/extensions/sf/auto/loop.js b/src/resources/extensions/sf/auto/loop.js index 5306a3ae5..9ade23f77 100644 --- a/src/resources/extensions/sf/auto/loop.js +++ b/src/resources/extensions/sf/auto/loop.js @@ -14,6 +14,7 @@ import { ModelPolicyDispatchBlockedError } from "../auto-model-selection.js"; import { runAutomaticAutonomousSolverEval } from "../autonomous-solver-eval.js"; import { debugLog } from "../debug-logger.js"; import { resolveEngine } from "../engine-resolver.js"; +import { NOTICE_KIND } from "../notification-store.js"; import { sfRoot } from "../paths.js"; import { getDatabase } from "../sf-db.js"; import { @@ -22,7 +23,6 @@ import { } from "../uok/execution-graph.js"; import { resolveUokFlags } from "../uok/flags.js"; import { clearRunawayRecoveredRuntimeRecords } from "../uok/unit-runtime.js"; -import { NOTICE_KIND } from "../notification-store.js"; import { logWarning } from "../workflow-logger.js"; import { COOLDOWN_FALLBACK_WAIT_MS, @@ -384,16 +384,28 @@ async function runExitSolverEval(ctx, s, deps, iteration) { ctx.ui.notify( `Autonomous solver eval recorded: ${result.report.reportPath}`, "info", + { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: "autonomous-solver-eval-recorded", + }, ); } else if (result.ok && result.report) { ctx.ui.notify( `Autonomous solver eval wrote ${result.report.reportPath}, but DB evidence was not recorded.`, "warning", + { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: "autonomous-solver-eval-db-miss", + }, ); } else if (!result.ok) { ctx.ui.notify( `Autonomous solver eval did not record: ${result.error}`, "warning", + { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: `autonomous-solver-eval-fail:${String(result.error ?? "").slice(0, 120)}`, + }, ); } debugLog("autoLoop", { @@ -406,7 +418,10 @@ async function runExitSolverEval(ctx, s, deps, iteration) { }); } catch (err) { const message = err instanceof Error ? err.message : String(err); - ctx.ui.notify(`Autonomous solver eval hook failed: ${message}`, "warning"); + ctx.ui.notify(`Autonomous solver eval hook failed: ${message}`, "warning", { + noticeKind: NOTICE_KIND.TOOL_NOTICE, + dedupe_key: `solver-eval-hook-fail:${message.slice(0, 120)}`, + }); debugLog("autoLoop", { phase: "solver-eval-auto-failed", iteration, @@ -929,6 +944,10 @@ export async function autoLoop(ctx, pi, s, deps) { ctx.ui.notify( `Health issues detected with slice references — queuing reassess-roadmap instead of pausing.`, "warning", + { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: "doctor-health-reassess-roadmap", + }, ); const { buildReassessRoadmapPrompt } = await import( "../auto-prompts.js" @@ -1128,7 +1147,7 @@ export async function autoLoop(ctx, pi, s, deps) { `Autonomous mode paused: model-policy denied dispatch for ${loopErr.unitType}/${loopErr.unitId}. ${msg}`, "error", { - noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + noticeKind: NOTICE_KIND.BLOCKING_NOTICE, dedupe_key: `model-policy-blocked:${loopErr.unitType}:${loopErr.unitId}`, }, ); @@ -1171,6 +1190,7 @@ export async function autoLoop(ctx, pi, s, deps) { ctx.ui.notify( `Autonomous mode stopped: infrastructure error ${infraCode} — ${msg}`, "error", + { noticeKind: NOTICE_KIND.SYSTEM_NOTICE }, ); await deps.stopAuto( ctx, @@ -1187,6 +1207,10 @@ export async function autoLoop(ctx, pi, s, deps) { ctx.ui.notify( `Phase "${phaseName}" timed out (${loopState.consecutiveFinalizeTimeouts} consecutive) — skipping iteration and continuing.`, "warning", + { + noticeKind: NOTICE_KIND.TOOL_NOTICE, + dedupe_key: `phase-timeout:${phaseName}`, + }, ); debugLog("autoLoop", { phase: "phase-timeout", @@ -1217,6 +1241,7 @@ export async function autoLoop(ctx, pi, s, deps) { ctx.ui.notify( `Autonomous mode stopped: ${consecutiveCooldowns} consecutive credential cooldowns — rate limit or quota may be persistently exhausted.`, "error", + { noticeKind: NOTICE_KIND.SYSTEM_NOTICE }, ); await deps.stopAuto( ctx, @@ -1234,6 +1259,10 @@ export async function autoLoop(ctx, pi, s, deps) { ctx.ui.notify( `Credentials in cooldown (${consecutiveCooldowns}/${MAX_COOLDOWN_RETRIES}) — waiting ${Math.round(waitMs / 1000)}s before retrying.`, "warning", + { + noticeKind: NOTICE_KIND.TOOL_NOTICE, + dedupe_key: "autonomous-credential-cooldown-wait", + }, ); await new Promise((resolve) => setTimeout(resolve, waitMs)); finishTurn("retry", "timeout", msg); @@ -1257,6 +1286,7 @@ export async function autoLoop(ctx, pi, s, deps) { ctx.ui.notify( `Autonomous mode stopped: ${consecutiveErrors} consecutive iteration failures:\n${errorHistory}`, "error", + { noticeKind: NOTICE_KIND.SYSTEM_NOTICE, merge: false }, ); await deps.stopAuto( ctx, @@ -1270,11 +1300,18 @@ export async function autoLoop(ctx, pi, s, deps) { ctx.ui.notify( `Iteration error (attempt ${consecutiveErrors}): ${msg}. Invalidating caches and retrying.`, "warning", + { + noticeKind: NOTICE_KIND.TOOL_NOTICE, + dedupe_key: `iteration-error-retry:${msg.slice(0, 240)}`, + }, ); deps.invalidateAllCaches(); } else { // 1st error: log and retry — transient failures happen - ctx.ui.notify(`Iteration error: ${msg}. Retrying.`, "warning"); + ctx.ui.notify(`Iteration error: ${msg}. Retrying.`, "warning", { + noticeKind: NOTICE_KIND.TOOL_NOTICE, + dedupe_key: `iteration-error-retry:${msg.slice(0, 240)}`, + }); } finishTurn("retry", "execution", msg); } diff --git a/src/resources/extensions/sf/bootstrap/register-hooks.js b/src/resources/extensions/sf/bootstrap/register-hooks.js index 1543cdad7..ca921b9aa 100644 --- a/src/resources/extensions/sf/bootstrap/register-hooks.js +++ b/src/resources/extensions/sf/bootstrap/register-hooks.js @@ -1137,6 +1137,10 @@ export function registerHooks(pi, ecosystemHandlers = []) { ctx.ui.notify( `Destructive command detected: ${classification.labels.join(", ")}`, "warning", + { + noticeKind: NOTICE_KIND.TOOL_NOTICE, + dedupe_key: `destructive-command:${classification.labels.join(",")}`, + }, ); } } @@ -1504,6 +1508,10 @@ export function registerHooks(pi, ecosystemHandlers = []) { ctx.ui.notify( `Gemini preflight: ${formatTokenCount(totalTokens)} tokens (${pct}% of ${formatTokenCount(contextWindow)} context).`, "warning", + { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: `gemini-preflight:${resolvedModel.id}`, + }, ); } } diff --git a/src/resources/extensions/sf/provider-error-pause.js b/src/resources/extensions/sf/provider-error-pause.js index 108cf6f27..759df2658 100644 --- a/src/resources/extensions/sf/provider-error-pause.js +++ b/src/resources/extensions/sf/provider-error-pause.js @@ -5,6 +5,8 @@ * an automatic resume after a delay. For permanent errors (auth, billing), * pauses indefinitely — user must manually resume. */ +import { NOTICE_KIND } from "./notification-store.js"; + export async function pauseAutoForProviderError( ui, errorDetail, @@ -24,6 +26,10 @@ export async function pauseAutoForProviderError( ui.notify( `${reason}${errorDetail}. Resuming automatically in ${delaySec}s...`, "warning", + { + noticeKind: NOTICE_KIND.TOOL_NOTICE, + dedupe_key: `provider-error-auto-resume:${options.isRateLimit ? "rate" : "transient"}`, + }, ); await pause(); // Schedule resume after the delay. @@ -31,13 +37,20 @@ export async function pauseAutoForProviderError( const resumeMsg = options.isRateLimit ? "Rate limit window elapsed. Resuming autonomous mode." : "Server error recovery delay elapsed. Resuming autonomous mode."; - ui.notify(resumeMsg, "info"); + ui.notify(resumeMsg, "info", { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: "provider-error-scheduled-resume", + }); options.resume(); }, options.retryAfterMs); } else { ui.notify( `Autonomous mode paused due to provider error${errorDetail}`, "warning", + { + noticeKind: NOTICE_KIND.BLOCKING_NOTICE, + dedupe_key: `provider-pause:${String(errorDetail).slice(0, 120)}`, + }, ); await pause(); } diff --git a/src/resources/extensions/sf/sf-db.js b/src/resources/extensions/sf/sf-db.js index 6bf906412..8579753cb 100644 --- a/src/resources/extensions/sf/sf-db.js +++ b/src/resources/extensions/sf/sf-db.js @@ -5135,17 +5135,19 @@ export function getActiveMilestoneFromDb() { } export function getActiveSliceFromDb(milestoneId) { if (!currentDb) return null; - // Single query: find the first non-complete slice whose dependencies are all satisfied. - // Uses json_each() to expand the JSON depends array and checks each dep is complete. + // Find the first non-complete slice whose dependencies are all satisfied. + // Uses the slice_dependencies junction table (kept in sync by syncSliceDependencies). const row = currentDb .prepare(`SELECT s.* FROM slices s WHERE s.milestone_id = :mid AND s.status NOT IN ('complete', 'done', 'skipped') AND NOT EXISTS ( - SELECT 1 FROM json_each(s.depends) AS dep - WHERE dep.value NOT IN ( - SELECT id FROM slices WHERE milestone_id = :mid AND status IN ('complete', 'done', 'skipped') - ) + SELECT 1 FROM slice_dependencies d + WHERE d.milestone_id = :mid + AND d.slice_id = s.id + AND d.depends_on_slice_id NOT IN ( + SELECT id FROM slices WHERE milestone_id = :mid AND status IN ('complete', 'done', 'skipped') + ) ) ORDER BY s.sequence, s.id LIMIT 1`) @@ -5894,6 +5896,21 @@ export function recordUokRunStart(entry) { ":updated_at": now, }); } +const MAX_ERROR_STORED_BYTES = 4096; +function capErrorForStorage(error, runId) { + if (!error || error.length <= MAX_ERROR_STORED_BYTES) return error; + try { + const errDir = join(dirname(currentPath), "runtime", "errors"); + mkdirSync(errDir, { recursive: true }); + writeFileSync(join(errDir, `${runId}.txt`), error, "utf-8"); + } catch { + // non-fatal — best-effort spill + } + const head = error.slice(0, 2048); + const tail = error.slice(-2048); + const dropped = error.length - MAX_ERROR_STORED_BYTES; + return `${head}\n\n[...${dropped} chars truncated — full error in .sf/runtime/errors/${runId}.txt]\n\n${tail}`; +} export function recordUokRunExit(entry) { if (!currentDb) return; const now = entry.endedAt ?? new Date().toISOString(); @@ -5918,7 +5935,7 @@ export function recordUokRunExit(entry) { ":status": entry.status ?? "ok", ":started_at": entry.startedAt ?? now, ":ended_at": now, - ":error": entry.error ?? null, + ":error": entry.error ? capErrorForStorage(entry.error, entry.runId) : null, ":flags_json": JSON.stringify(entry.flags ?? {}), ":updated_at": now, }); diff --git a/src/resources/extensions/sf/uok-parity-summary.js b/src/resources/extensions/sf/uok-parity-summary.js index fcffe2e4b..7da17eba6 100644 --- a/src/resources/extensions/sf/uok-parity-summary.js +++ b/src/resources/extensions/sf/uok-parity-summary.js @@ -4,6 +4,7 @@ import { hasCurrentParityWarning, writeParityReport, } from "./uok/parity-report.js"; +import { NOTICE_KIND } from "./notification-store.js"; /** * Read the last UOK parity report from /.sf/runtime/uok-parity-report.json * and surface any divergences or errors via ctx.ui?.notify?.(). @@ -31,7 +32,10 @@ export async function summarizeParityReport(basePath, ctx, pi) { `UOK parity report shows ${mismatches} critical mismatch${mismatches === 1 ? "" : "es"}, ` + `${errors} error${errors === 1 ? "" : "s"} since ${report.generatedAt}. ` + `Inspect .sf/runtime/uok-parity-report.json.`; - ctx.ui?.notify?.(msg, "warning"); + ctx.ui?.notify?.(msg, "warning", { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: "uok-parity-report-current-warning", + }); } else { const pathCount = Object.keys(report.paths ?? {}).length; pi?.logInfo?.(