Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/internal/recall-extraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,11 @@ activation promotes them to `accepted` and archives the retired generation's
still-automatic entries in the same transaction, so the served corpus switches
atomically. Retiring a generation likewise archives its still-automatic entries.
Entries a human has touched (any review state other than `unreviewed_auto`) are
never moved by these flips.
never moved by these flips. In particular, `human_reviewed` entries remain
accepted and `human_rejected` entries remain archived across activation and
retirement. Privacy retraction and content-digest reset cleanup also target only
`unreviewed_auto`; neither human state is deleted or regenerated by those
machine lifecycle paths.

## Entry mapping

Expand Down
31 changes: 22 additions & 9 deletions docs/recall.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ The current implementation is local and SQLite-only. The CLI provides:

The top-level **Recall** page has two tabs:

- **Corpus** is a read-only browser for distilled entries. It shows extraction
coverage and generation state, and filters entries by text, project, entry
type, generation, and review state. Expand an entry to inspect its body,
trigger, uncertainty, provenance metadata, and evidence links back to the
source transcript.
- **Corpus** is a browser and review surface for distilled entries. It shows
extraction coverage and generation state, and filters entries by text,
project, entry type, generation, and review state. Expand an entry to inspect
its body, trigger, uncertainty, provenance metadata, evidence links back to
the source transcript, and review controls for automatic entries.
- **Generated insights** creates and stores longer reports over an explicit
session scope. Its form always shows the date range, project, session agent,
automated-session scope, report template, generator, and optional focus used
Expand Down Expand Up @@ -86,13 +86,25 @@ The Corpus tab is not available through PostgreSQL or DuckDB stores, so those
read-only servers open Recall on Generated insights instead. On the local SQLite
UI, Session Vital Signs also includes a read-only Recall panel for the open
session and links each evidence range back to the transcript. Corpus population,
review, extraction-generation management, and ranked querying remain CLI and
HTTP API workflows.
extraction-generation management, and ranked querying remain CLI and HTTP API
workflows.

The daemon exposes the same inspection and query operations over its HTTP API.
Ordinary queries record measurement data when the SQLite store is writable, but
read-only archives remain queryable without recording.

### Review extracted entries

Expand an accepted `unreviewed_auto` entry in the Corpus table to approve or
archive it. **Approve** immediately marks the entry `human_reviewed`; approval
is disabled when its source evidence has been revoked. **Archive** asks for
confirmation, then marks the entry `human_rejected` and removes it from the
served Recall corpus.

Both decisions are durable human states and are not reversed by later
extraction-generation changes. This surface deliberately has no entry editing,
bulk review, or undo action.

## Vector and hybrid retrieval

`recall query` and `recall brief` accept `--mode lexical`, `--mode vector`, or
Expand Down Expand Up @@ -208,11 +220,12 @@ or manufacture stable message IDs and digests. Evidence must belong to the same
source session as its entry. These checks run through the shared insertion and
reviewed-import boundaries rather than through a separate model write path.

Entries have one of four review states:
Entries have one of five review states:

| Review state | Meaning |
| ----------------- | ------------------------------------------------------- |
| `human_reviewed` | Explicitly accepted through the reviewed import surface |
| `human_reviewed` | Explicitly approved by a human |
| `human_rejected` | Explicitly rejected and archived by a human |
| `unreviewed_auto` | Generated or omitted review decision |
| `calibrated_auto` | Automated output from a calibrated future policy |
| `eval_raw` | Quarantined evaluation material |
Expand Down
13 changes: 13 additions & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,19 @@
"recall_page_all_generations": "All generations",
"recall_page_review_filter": "Review state",
"recall_page_all_review_states": "All review states",
"recall_page_review_state_human_reviewed": "Human approved",
"recall_page_review_state_human_rejected": "Human rejected",
"recall_page_review_state_unreviewed_auto": "Unreviewed automatic",
"recall_page_review_state_calibrated_auto": "Calibrated automatic",
"recall_page_review_state_eval_raw": "Evaluation raw",
"recall_page_review_approve": "Approve",
"recall_page_review_archive": "Archive",
"recall_page_review_approve_disabled": "Approval is unavailable because the source evidence was revoked.",
"recall_page_review_archive_title": "Archive Recall entry",
"recall_page_review_archive_message": "Archive “{title}” as rejected? It will remain outside the served Recall corpus.",
"recall_page_review_cancel": "Cancel",
"recall_page_review_close": "Close archive confirmation",
"recall_page_review_error": "Could not review this Recall entry: {error}",
"recall_page_table_label": "Recall entries",
"recall_page_fact_column": "Fact",
"recall_page_project_column": "Project",
Expand Down
13 changes: 13 additions & 0 deletions frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,19 @@
"recall_page_all_generations": "Toutes les générations",
"recall_page_review_filter": "État de révision",
"recall_page_all_review_states": "Tous les états de révision",
"recall_page_review_state_human_reviewed": "Approuvé par un humain",
"recall_page_review_state_human_rejected": "Rejeté par un humain",
"recall_page_review_state_unreviewed_auto": "Automatique non vérifié",
"recall_page_review_state_calibrated_auto": "Automatique calibré",
"recall_page_review_state_eval_raw": "Évaluation brute",
"recall_page_review_approve": "Approuver",
"recall_page_review_archive": "Archiver",
"recall_page_review_approve_disabled": "L’approbation est indisponible car les preuves sources ont été révoquées.",
"recall_page_review_archive_title": "Archiver l’entrée Recall",
"recall_page_review_archive_message": "Archiver « {title} » comme rejetée ? Elle restera exclue du corpus Recall servi.",
"recall_page_review_cancel": "Annuler",
"recall_page_review_close": "Fermer la confirmation d’archivage",
"recall_page_review_error": "Impossible de vérifier cette entrée Recall : {error}",
"recall_page_table_label": "Entrées de rappel",
"recall_page_fact_column": "Fait",
"recall_page_project_column": "Projet",
Expand Down
13 changes: 13 additions & 0 deletions frontend/messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,19 @@
"recall_page_all_generations": "모든 세대",
"recall_page_review_filter": "검토 상태",
"recall_page_all_review_states": "모든 검토 상태",
"recall_page_review_state_human_reviewed": "사람이 승인함",
"recall_page_review_state_human_rejected": "사람이 거부함",
"recall_page_review_state_unreviewed_auto": "검토되지 않은 자동 항목",
"recall_page_review_state_calibrated_auto": "보정된 자동 항목",
"recall_page_review_state_eval_raw": "원시 평가",
"recall_page_review_approve": "승인",
"recall_page_review_archive": "보관",
"recall_page_review_approve_disabled": "원본 근거가 철회되어 승인할 수 없습니다.",
"recall_page_review_archive_title": "Recall 항목 보관",
"recall_page_review_archive_message": "“{title}”을(를) 거부됨으로 보관하시겠습니까? 제공되는 Recall 코퍼스에서 계속 제외됩니다.",
"recall_page_review_cancel": "취소",
"recall_page_review_close": "보관 확인 닫기",
"recall_page_review_error": "이 Recall 항목을 검토할 수 없습니다: {error}",
"recall_page_table_label": "리콜 항목",
"recall_page_fact_column": "사실",
"recall_page_project_column": "프로젝트",
Expand Down
13 changes: 13 additions & 0 deletions frontend/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,19 @@
"recall_page_all_generations": "所有代次",
"recall_page_review_filter": "审核状态",
"recall_page_all_review_states": "所有审核状态",
"recall_page_review_state_human_reviewed": "人工已批准",
"recall_page_review_state_human_rejected": "人工已拒绝",
"recall_page_review_state_unreviewed_auto": "未审核的自动条目",
"recall_page_review_state_calibrated_auto": "已校准的自动条目",
"recall_page_review_state_eval_raw": "原始评估",
"recall_page_review_approve": "批准",
"recall_page_review_archive": "归档",
"recall_page_review_approve_disabled": "由于来源证据已被撤销,无法批准。",
"recall_page_review_archive_title": "归档 Recall 条目",
"recall_page_review_archive_message": "将“{title}”归档为已拒绝吗?它将继续排除在提供服务的 Recall 语料库之外。",
"recall_page_review_cancel": "取消",
"recall_page_review_close": "关闭归档确认",
"recall_page_review_error": "无法审核此 Recall 条目:{error}",
"recall_page_table_label": "召回条目",
"recall_page_fact_column": "事实",
"recall_page_project_column": "项目",
Expand Down
13 changes: 13 additions & 0 deletions frontend/messages/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,19 @@
"recall_page_all_generations": "所有代次",
"recall_page_review_filter": "審核狀態",
"recall_page_all_review_states": "所有審核狀態",
"recall_page_review_state_human_reviewed": "人工已核准",
"recall_page_review_state_human_rejected": "人工已拒絕",
"recall_page_review_state_unreviewed_auto": "未審核的自動項目",
"recall_page_review_state_calibrated_auto": "已校準的自動項目",
"recall_page_review_state_eval_raw": "原始評估",
"recall_page_review_approve": "核准",
"recall_page_review_archive": "封存",
"recall_page_review_approve_disabled": "來源證據已撤銷,因此無法核准。",
"recall_page_review_archive_title": "封存 Recall 項目",
"recall_page_review_archive_message": "要將「{title}」封存為已拒絕嗎?它仍會排除在提供服務的 Recall 語料庫之外。",
"recall_page_review_cancel": "取消",
"recall_page_review_close": "關閉封存確認",
"recall_page_review_error": "無法審核此 Recall 項目:{error}",
"recall_page_table_label": "召回項目",
"recall_page_fact_column": "事實",
"recall_page_project_column": "專案",
Expand Down
97 changes: 63 additions & 34 deletions frontend/src/lib/api/recall.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import {
afterEach,
describe,
expect,
it,
vi,
} from "vite-plus/test";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import {
activateRecallExtractionGeneration,
fetchRecallEntries,
fetchRecallExtractionProgress,
reviewRecallEntry,
retireRecallExtractionGeneration,
} from "./recall.js";

Expand All @@ -18,36 +13,39 @@ afterEach(() => {

describe("fetchRecallEntries", () => {
it("sends every corpus-browser filter to the Recall API", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(
JSON.stringify({
entries: [],
trusted_only: false,
next_cursor: "cursor-2",
result_cap: 500,
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
));
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
entries: [],
trusted_only: false,
next_cursor: "cursor-2",
result_cap: 500,
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
),
);
vi.stubGlobal("fetch", fetchMock);

const page = await fetchRecallEntries({
query: "bounded pass",
project: "project-a",
type: "decision",
sourceRunId: "generation-a",
status: "archived",
reviewState: "human_reviewed",
limit: 75,
});

expect(fetchMock).toHaveBeenCalledOnce();
const url = new URL(String(fetchMock.mock.calls[0]?.[0]),
window.location.origin);
const url = new URL(String(fetchMock.mock.calls[0]?.[0]), window.location.origin);
expect(url.pathname).toBe("/api/v1/recall/entries");
expect(Object.fromEntries(url.searchParams)).toEqual({
limit: "75",
q: "bounded pass",
project: "project-a",
type: "decision",
source_run_id: "generation-a",
status: "archived",
review_state: "human_reviewed",
});
expect(page).toEqual({
Expand All @@ -58,16 +56,44 @@ describe("fetchRecallEntries", () => {
});
});

describe("reviewRecallEntry", () => {
it("posts one encoded review action and returns the updated entry", async () => {
const updated = {
id: "entry one",
status: "archived",
review_state: "human_rejected",
};
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify(updated), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);

await expect(reviewRecallEntry("entry one", "archive")).resolves.toEqual(updated);
expect(fetchMock).toHaveBeenCalledWith(
"/api/v1/recall/entries/entry%20one/review",
expect.objectContaining({
method: "POST",
body: JSON.stringify({ action: "archive" }),
}),
);
});
});

describe("fetchRecallExtractionProgress", () => {
it("sends bounded generation, state, and cursor filters", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(
JSON.stringify({
generation_fingerprint: "generation-a",
progress: [],
next_cursor: "progress-cursor-2",
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
));
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
generation_fingerprint: "generation-a",
progress: [],
next_cursor: "progress-cursor-2",
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
),
);
vi.stubGlobal("fetch", fetchMock);

const page = await fetchRecallExtractionProgress({
Expand All @@ -78,8 +104,7 @@ describe("fetchRecallExtractionProgress", () => {
});

expect(fetchMock).toHaveBeenCalledOnce();
const url = new URL(String(fetchMock.mock.calls[0]?.[0]),
window.location.origin);
const url = new URL(String(fetchMock.mock.calls[0]?.[0]), window.location.origin);
expect(url.pathname).toBe("/api/v1/recall/extraction/progress");
expect(Object.fromEntries(url.searchParams)).toEqual({
limit: "25",
Expand All @@ -97,9 +122,11 @@ describe("fetchRecallExtractionProgress", () => {

describe("Recall extraction generation actions", () => {
it("activates the configured generation", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(null, {
status: 204,
}));
const fetchMock = vi.fn().mockResolvedValue(
new Response(null, {
status: 204,
}),
);
vi.stubGlobal("fetch", fetchMock);

await activateRecallExtractionGeneration();
Expand All @@ -111,9 +138,11 @@ describe("Recall extraction generation actions", () => {
});

it("retires one generation without a force option", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(null, {
status: 204,
}));
const fetchMock = vi.fn().mockResolvedValue(
new Response(null, {
status: 204,
}),
);
vi.stubGlobal("fetch", fetchMock);

await retireRecallExtractionGeneration("generation old");
Expand Down
Loading