Skip to content
Merged
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
67 changes: 61 additions & 6 deletions apps/elf-api/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ use elf_service::{
KnowledgePageLintRequest, KnowledgePageLintResponse, KnowledgePageRebuildRequest,
KnowledgePageRebuildResponse, KnowledgePageResponse, KnowledgePageSearchRequest,
KnowledgePageSearchResponse, KnowledgePagesListRequest, KnowledgePagesListResponse,
ListRequest, ListResponse, MemoryHistoryGetRequest, MemoryHistoryResponse, NoteFetchRequest,
ListRequest, ListResponse, MemoryCorrectionAction, MemoryCorrectionRequest,
MemoryCorrectionResponse, MemoryHistoryGetRequest, MemoryHistoryResponse, NoteFetchRequest,
NoteFetchResponse, NoteProvenanceBundleResponse, NoteProvenanceGetRequest, PayloadLevel,
PublishNoteRequest, QueryPlan, RankingRequestOverride, RebuildReport, RecallDebugPanelRequest,
RecallDebugPanelResponse, SearchDetailsRequest, SearchDetailsResult, SearchExplainRequest,
Expand Down Expand Up @@ -163,11 +164,12 @@ const VIEWER_HTML: &str = include_str!("../static/viewer.html");
trace_item_get,
admin_graph_predicates_list,
admin_graph_predicate_patch,
admin_graph_predicate_alias_add,
admin_graph_predicate_aliases_list,
admin_note_provenance_get,
admin_note_history_get,
),
admin_graph_predicate_alias_add,
admin_graph_predicate_aliases_list,
admin_note_provenance_get,
admin_note_history_get,
admin_note_correction_apply,
),
components(schemas(
AdminIngestionProfileDefaultResponseV2,
AdminIngestionProfileDefaultSetBody,
Expand Down Expand Up @@ -483,6 +485,14 @@ struct NotePatchRequest {
ttl_days: Option<i64>,
}

#[derive(Clone, Debug, Deserialize)]
struct AdminNoteCorrectionBody {
action: MemoryCorrectionAction,
reason: String,
source_ref: Value,
restore_version_id: Option<Uuid>,
}

#[derive(Clone, Debug, Deserialize)]
struct AdminGraphPredicatesListQuery {
scope: Option<String>,
Expand Down Expand Up @@ -791,6 +801,7 @@ pub fn admin_router(state: AppState) -> Router {
)
.route("/v2/admin/notes/{note_id}/provenance", routing::get(admin_note_provenance_get))
.route("/v2/admin/notes/{note_id}/history", routing::get(admin_note_history_get))
.route("/v2/admin/notes/{note_id}/corrections", routing::post(admin_note_correction_apply))
.with_state(state)
.layer(DefaultBodyLimit::max(MAX_REQUEST_BYTES))
.layer(middleware::from_fn_with_state(auth_state, admin_auth_middleware));
Expand Down Expand Up @@ -2851,6 +2862,50 @@ async fn admin_note_history_get(
Ok(Json(response))
}

#[utoipa::path(
post,
path = "/v2/admin/notes/{note_id}/corrections",
tag = "admin",
params(("note_id" = Uuid, Path, description = "Note ID.")),
request_body = Value,
responses(
(status = 200, description = "Memory correction was applied.", body = Value),
(status = 400, description = "Invalid request.", body = ErrorBody),
(status = 401, description = "Authentication required.", body = ErrorBody),
(status = 403, description = "Admin access required.", body = ErrorBody),
(status = 404, description = "Note was not found.", body = ErrorBody),
(status = 500, description = "Internal error.", body = ErrorBody),
)
)]
async fn admin_note_correction_apply(
State(state): State<AppState>,
headers: HeaderMap,
Path(note_id): Path<Uuid>,
payload: Result<Json<AdminNoteCorrectionBody>, JsonRejection>,
) -> Result<Json<MemoryCorrectionResponse>, ApiError> {
let ctx = RequestContext::from_headers(&headers)?;
let Json(payload) = payload.map_err(|err| {
tracing::warn!(error = %err, "Invalid request payload.");

json_error(StatusCode::BAD_REQUEST, "INVALID_REQUEST", "Invalid request payload.", None)
})?;
let response = state
.service
.memory_correction_apply(MemoryCorrectionRequest {
tenant_id: ctx.tenant_id,
project_id: ctx.project_id,
actor_agent_id: ctx.agent_id,
note_id,
action: payload.action,
reason: payload.reason,
source_ref: payload.source_ref,
restore_version_id: payload.restore_version_id,
})
.await?;

Ok(Json(response))
}

#[utoipa::path(
post,
path = "/v2/admin/consolidation/runs",
Expand Down
1 change: 1 addition & 0 deletions apps/elf-api/tests/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,7 @@ async fn openapi_json_route_serves_generated_contract() {
assert_openapi_method(&spec, "/v2/admin/consolidation/proposals", "get");
assert_openapi_method(&spec, "/v2/admin/consolidation/proposals/{proposal_id}", "get");
assert_openapi_method(&spec, "/v2/admin/consolidation/proposals/{proposal_id}/review", "post");
assert_openapi_method(&spec, "/v2/admin/notes/{note_id}/corrections", "post");
assert_openapi_method(&spec, "/v2/admin/knowledge/pages/rebuild", "post");
assert_openapi_method(&spec, "/v2/admin/knowledge/pages", "get");
assert_openapi_method(&spec, "/v2/admin/knowledge/pages/search", "post");
Expand Down
40 changes: 34 additions & 6 deletions docs/spec/system_consolidation_proposals_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ resource: docs/spec/system_consolidation_proposals_v1.md
status: active
authority: normative
owner: spec
last_verified: 2026-06-18
last_verified: 2026-06-22
tags:
- docs
- spec
Expand Down Expand Up @@ -37,10 +37,11 @@ Related inputs:
Consolidation output is derived and reviewable. It must never destructively rewrite
authoritative source notes, events, docs, traces, graph facts, or search traces.

The authoritative source-of-truth remains the ELF Core storage defined by
The raw evidence source-of-truth remains the ELF Core storage defined by
`docs/spec/system_elf_memory_service_v2.md`. Consolidation stores proposals over
immutable input snapshots. A proposal may later create or update a derived artifact,
but source evidence remains inspectable and unchanged.
immutable input snapshots. A reviewed proposal may later create or update an approved
operational memory record or another derived artifact, but source evidence remains
inspectable and unchanged.

## Contract Schema

Expand Down Expand Up @@ -261,8 +262,10 @@ Allowed review transitions:

Terminal states are `rejected`, `applied`, and `archived`.

`applied` means the proposal has been approved and marked as applied to the derived
target. It does not mean authoritative source memory was changed.
`applied` means the proposal has been approved and marked as applied to the target.
For `create_derived_note` and `update_derived_note`, applying the proposal creates or
updates an approved operational memory note. It never mutates the raw source notes,
docs, events, traces, graph facts, or source pointers that justified the proposal.

Operator review actions map to the lifecycle states:

Expand All @@ -275,6 +278,29 @@ Operator review actions map to the lifecycle states:
Every review transition must write an append-only audit event with proposal id, run id,
reviewer agent id, action, prior state, next state, optional comment, and timestamp.

## Memory Promotion Apply Contract

`create_derived_note` and `update_derived_note` are the only proposal apply intents
that may write to `memory_notes`. They are the Memory Inbox promotion path:

- The proposal must first receive an explicit `apply` review action. If it starts from
`proposed`, the service records both `approve` and `apply` review events.
- The proposed payload must validate as a normal memory note payload with English text,
allowed type, allowed writable scope, and finite confidence/importance scores.
- The promoted memory note must be `active`, write `memory_note_versions`, and enqueue
the normal indexing outbox `UPSERT`.
- The promoted note `source_ref` must use `elf.memory_promotion/v1` and include
proposal id, run id, apply intent, source refs, source snapshot, lineage,
unsupported-claim flags, review actor/comment/timestamp, and any payload-level
source ref.
- The proposal `target_ref` must be updated to `elf.memory_record_ref/v1` with
`kind = "note"` and the accepted memory `id`.
- `update_derived_note` may update only an active target note identified by
`target_ref.id` or `target_ref.note_id`.

Promoted memory records are approved operational memory. They do not replace or edit
the Source Library/raw evidence that supplied `source_refs`.

## Service Boundary

The first implementation exposes fixture-driven service flows:
Expand All @@ -286,6 +312,8 @@ The first implementation exposes fixture-driven service flows:
- get a consolidation proposal
- transition proposal review state through `approve`, `apply`, `discard`, and `defer`
actions with review-event readback
- apply reviewed `create_derived_note` and `update_derived_note` proposals into
approved memory notes with `target_ref` readback

These flows must not call LLM, embedding, rerank, or external provider adapters.

Expand Down
49 changes: 43 additions & 6 deletions docs/spec/system_elf_memory_service_v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ resource: docs/spec/system_elf_memory_service_v2.md
status: active
authority: normative
owner: spec
last_verified: 2026-06-18
last_verified: 2026-06-22
tags:
- docs
- spec
Expand Down Expand Up @@ -1107,6 +1107,11 @@ Behavior:
- Review action values are `approve`, `apply`, `discard`, and `defer`.
- `apply` records an approval transition before the applied transition when a proposal
starts from `proposed`.
- For `create_derived_note` and `update_derived_note`, `apply` promotes the reviewed
proposal into an approved operational memory note. The promoted note source ref
must use `elf.memory_promotion/v1`, include the proposal source refs and review
actor/timestamp, write `memory_note_versions`, enqueue `UPSERT`, and update the
proposal `target_ref` to `elf.memory_record_ref/v1` with `kind = "note"`.
- Every review action writes append-only review audit events returned by proposal
detail readback.
- `GET /v2/admin/dreaming/review-queue` exposes
Expand All @@ -1120,8 +1125,37 @@ Behavior:
limited to approved tag or duplicate-merge candidates with no lint or source
mutation request.
- These endpoints must not call LLM, embedding, rerank, or external provider adapters.
- They must not mutate authoritative source notes, docs, events, traces, graph facts,
or search traces.
- They must not mutate raw source notes, docs, events, traces, graph facts, or search
traces. Applied note proposals may create or update approved operational memory
records only through the explicit review path above.

Admin memory correction loop:
- POST /v2/admin/notes/{note_id}/corrections

Body:
{
"action": "supersede|delete|restore",
"reason": "non-empty reviewer or policy reason",
"source_ref": { "...": "non-empty source or review reference" },
"restore_version_id": "uuid|null"
}

Behavior:
- `supersede` sets the note status to `deprecated`, writes a `DEPRECATE`
`memory_note_versions` row, stores `elf.memory_correction/v1` source-ref evidence,
and enqueues an indexing `DELETE` so normal recall suppresses the note.
- `delete` sets the note status to `deleted`, writes a `DELETE`
`memory_note_versions` row, stores `elf.memory_correction/v1` source-ref evidence,
and enqueues an indexing `DELETE`.
- `restore` restores the latest prior active snapshot from a `DELETE` or `DEPRECATE`
version, or the supplied `restore_version_id`, writes a `RESTORE`
`memory_note_versions` row, stores `elf.memory_correction/v1` source-ref evidence,
and enqueues an indexing `UPSERT`.
- Correction actions require a non-empty reason and non-empty JSON object
`source_ref`. They do not mutate raw source notes, docs, events, traces, graph
facts, or source pointers.
- Normal recall remains active-only; `deprecated` and `deleted` notes are visible
through provenance/history or explicit non-active list filters, not ordinary search.

Admin recall/debug panel:
- POST /v2/admin/recall-debug/panel
Expand Down Expand Up @@ -1580,7 +1614,7 @@ Response:
"events": [
{
"event_id": "string",
"event_type": "add|update|ignore|reject|expire|delete|derived|applied|invalidated|related",
"event_type": "add|update|ignore|reject|expire|delete|derived|applied|superseded|restored|defer|invalidated|related",
"subject_type": "note",
"note_id": "uuid",
"source_table": "string",
Expand All @@ -1602,8 +1636,11 @@ Notes:
- History events are a chronological read-only projection over durable source tables.
- Ingest decisions that produce note versions should set `note_version_id` so history
can link the decision to the resulting note version.
- Derived, applied, and invalidated events come from consolidation proposals and
review events that reference the note in `source_refs`.
- Derived, applied, rejected, and deferred events come from consolidation proposals
and review events that reference the note in `source_refs` or the proposal
`target_ref`.
- Superseded, deleted, and restored events come from correction-backed note version
rows.

============================================================
15. HTTP API (PUBLIC)
Expand Down
Loading