fix(feedback): Phase 2 follow-up — Codex review MAJORs + NIT
Independent review surfaced three majors and one nit. All actioned. MAJOR 1 — profile scoping: `decompose_and_store` and `extract_tasks_from_transcript_cmd` now accept an optional `profile_id` (wired from `profilesStore.activeProfileId` in MicroSteps.svelte and DictationPage.svelte), and thread it into the feedback-retrieval query so per-profile decomposition styles do not leak into each other. `record_feedback` gets the same treatment. MAJOR 2 — prompt-budget regression on long inputs: New `trim_to_budget` helper + `FEW_SHOT_CHAR_BUDGET = 2000` char cap in `src-tauri/src/commands/tasks.rs`. Retrieval still pulls up to 5 rows but they are char-counted and truncated against the budget before being sent to the LLM. Char cost matches the `Input: ...\n Good output: ...` render path so the budget maps cleanly to ~570 Qwen3 tokens, well inside the 8192-context reserve after the 512- or 768-token response allocation. Oldest-first drop order (iteration stops at cost exceeded) preserves the most recent correction which is the one carrying the user's live preference. MAJOR 3 — inline edit stale-rollback race: `saveEdit` in MicroSteps.svelte now stamps a monotonic per-step `saveToken`. Each edit bumps the token; on failure the rollback only fires if `saveToken[step.id] === myToken`, so a slow-failing first save can no longer overwrite a faster successful second save. NIT — retrieval ordering stability: `list_feedback_examples` ORDER BY now `created_at DESC, id DESC`. SQLite timestamp precision is one second; without the secondary key, bursty feedback within the same second would select non-deterministically. Also: malformed `context_json` now warns via eprintln! rather than disappearing silently — Codex minor. All green: cargo build + 249 tests + clippy -D warnings + fmt + svelte-check (0/0) + npm run build.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { ListTree, Check, Timer, Loader2, ThumbsUp, ThumbsDown, Pencil } from 'lucide-svelte';
|
||||
import { profilesStore } from '$lib/stores/profiles.svelte.ts';
|
||||
|
||||
let { parentTaskId, parentTaskText = '', reduceMotion = false } = $props();
|
||||
|
||||
@@ -24,6 +25,13 @@
|
||||
let editing = $state<Record<string, boolean>>({});
|
||||
let draft = $state<Record<string, string>>({});
|
||||
|
||||
// Monotonic token per-step. Each time the user kicks off a save we
|
||||
// bump the token; the pending save remembers its token and only
|
||||
// rolls back if the failure belongs to the still-current edit.
|
||||
// Without this, a slow first save that eventually fails will stomp
|
||||
// a faster second save that already committed.
|
||||
let saveToken = $state<Record<string, number>>({});
|
||||
|
||||
async function loadSubtasks() {
|
||||
loading = true;
|
||||
error = '';
|
||||
@@ -40,7 +48,10 @@
|
||||
decomposing = true;
|
||||
error = '';
|
||||
try {
|
||||
subtasks = await invoke<Subtask[]>('decompose_and_store', { parentTaskId });
|
||||
subtasks = await invoke<Subtask[]>('decompose_and_store', {
|
||||
parentTaskId,
|
||||
profileId: profilesStore.activeProfileId,
|
||||
});
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
} finally {
|
||||
@@ -94,6 +105,7 @@
|
||||
originalText: step.text,
|
||||
correctedText: null,
|
||||
contextJson: feedbackContextJson(),
|
||||
profileId: profilesStore.activeProfileId,
|
||||
},
|
||||
});
|
||||
} catch (_) { /* feedback capture is best-effort, never fatal */ }
|
||||
@@ -116,6 +128,10 @@
|
||||
const original = step.text;
|
||||
// Update in-memory first so the UI is snappy; roll back if the
|
||||
// persistence call fails so we never show stale-but-different text.
|
||||
// The monotonic `myToken` guards against a stale rollback stomping
|
||||
// a fresher successful save that landed while we were waiting.
|
||||
const myToken = (saveToken[step.id] ?? 0) + 1;
|
||||
saveToken = { ...saveToken, [step.id]: myToken };
|
||||
const idx = subtasks.findIndex(s => s.id === step.id);
|
||||
if (idx >= 0) subtasks[idx] = { ...subtasks[idx], text: next };
|
||||
try {
|
||||
@@ -132,10 +148,18 @@
|
||||
originalText: original,
|
||||
correctedText: next,
|
||||
contextJson: feedbackContextJson(),
|
||||
profileId: profilesStore.activeProfileId,
|
||||
},
|
||||
}).catch(() => {});
|
||||
} catch (_) {
|
||||
if (idx >= 0) subtasks[idx] = { ...subtasks[idx], text: original };
|
||||
// Only roll back if our token is still the most recent — a later
|
||||
// edit that already succeeded must not be overwritten.
|
||||
if (saveToken[step.id] === myToken) {
|
||||
const rollbackIdx = subtasks.findIndex(s => s.id === step.id);
|
||||
if (rollbackIdx >= 0) {
|
||||
subtasks[rollbackIdx] = { ...subtasks[rollbackIdx], text: original };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -487,7 +487,10 @@
|
||||
if (settings.aiTier === "tasks" && llmLoaded) {
|
||||
markGenerating("Extracting tasks");
|
||||
try {
|
||||
const items = await invoke("extract_tasks_from_transcript_cmd", { transcript: text });
|
||||
const items = await invoke("extract_tasks_from_transcript_cmd", {
|
||||
transcript: text,
|
||||
profileId: profilesStore.activeProfileId,
|
||||
});
|
||||
markGenerationDone(true);
|
||||
return items.map((taskText) => ({ text: taskText }));
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user