curl --request POST \
--url http://127.0.0.1:4319/memory/reconcile \
--header 'Content-Type: application/json' \
--data '
{
"mode": "dry_run",
"trigger": "manual",
"passes": [],
"budgetUsd": 49
}
'import requests
url = "http://127.0.0.1:4319/memory/reconcile"
payload = {
"mode": "dry_run",
"trigger": "manual",
"passes": [],
"budgetUsd": 49
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({mode: 'dry_run', trigger: 'manual', passes: [], budgetUsd: 49})
};
fetch('http://127.0.0.1:4319/memory/reconcile', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "http://127.0.0.1:4319/memory/reconcile"
payload := strings.NewReader("{\n \"mode\": \"dry_run\",\n \"trigger\": \"manual\",\n \"passes\": [],\n \"budgetUsd\": 49\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "4319",
CURLOPT_URL => "http://127.0.0.1:4319/memory/reconcile",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'mode' => 'dry_run',
'trigger' => 'manual',
'passes' => [
],
'budgetUsd' => 49
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}require 'uri'
require 'net/http'
url = URI("http://127.0.0.1:4319/memory/reconcile")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"mode\": \"dry_run\",\n \"trigger\": \"manual\",\n \"passes\": [],\n \"budgetUsd\": 49\n}"
response = http.request(request)
puts response.read_bodyHttpResponse<String> response = Unirest.post("http://127.0.0.1:4319/memory/reconcile")
.header("Content-Type", "application/json")
.body("{\n \"mode\": \"dry_run\",\n \"trigger\": \"manual\",\n \"passes\": [],\n \"budgetUsd\": 49\n}")
.asString();using RestSharp;
var options = new RestClientOptions("http://127.0.0.1:4319/memory/reconcile");
var client = new RestClient(options);
var request = new RestRequest("");
request.AddJsonBody("{\n \"mode\": \"dry_run\",\n \"trigger\": \"manual\",\n \"passes\": [],\n \"budgetUsd\": 49\n}", false);
var response = await client.PostAsync(request);
Console.WriteLine("{0}", response.Content);
{
"run": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"mode": "dry_run",
"trigger": "manual",
"status": "running",
"scanned": 123,
"retired": 123,
"folded": 123,
"questions": 123,
"conflictsRaised": 123,
"possible": 123,
"llmCalls": 123,
"spentUsd": 123,
"spentInputTokens": 123,
"spentOutputTokens": 123,
"error": "<string>",
"startedAt": "2023-11-07T05:31:56Z",
"finishedAt": "2023-11-07T05:31:56Z",
"revertedAt": "2023-11-07T05:31:56Z"
},
"actions": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"runId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"kind": "retire",
"class": "duplicate_content",
"memoryId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"reason": "identical content to 41b0e2d8, which is older and stays active",
"applied": true,
"surfaced": true,
"decision": "approved",
"mergeId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"conflictId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"candidateId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z"
}
],
"estimate": {
"window": 123,
"llmCalls": 123,
"inputTokens": 123,
"outputTokens": 123,
"model": "google/gemini-2.5-flash",
"usd": 123
},
"passes": [
"<string>"
],
"heldBack": {
"retire": 123,
"question": 123,
"conflict": 123
},
"entities": {
"examined": 123,
"pairs": 123,
"merged": 123,
"queued": 123,
"deferred": 123,
"skipped": 123
},
"arbitration": {
"calls": 123,
"folded": 123,
"rejected": 123,
"unsure": 123
},
"autoResolved": {
"examined": 123,
"resolved": 123
},
"recheck": {
"window": 123,
"calls": 123,
"claimed": 123,
"verified": 123,
"remaining": 123,
"spentUsd": 123,
"spentInputTokens": 123,
"spentOutputTokens": 123,
"stoppedBy": "budget"
}
}{
"error": "invalid request body",
"issues": [
{
"path": "query",
"message": "query must be a non-empty string"
}
]
}{
"error": "reconciliation is set to report only (RECONCILE_ENABLED=0). Remove it from your memloom config and restart the daemon to let a run act."
}{
"error": "memloom: a reconcile run is already going. Watch it in the Console, or wait for it to finish."
}Run the consolidation pass
Reviews the store, repairs what SQL proves wrong, folds duplicate entity names, and
asks about the rest. Every run is one undoable unit: the report names the run id, and
/memory/reconcile/{id}/revert puts back exactly what it did.
mode defaults to dry_run, which changes nothing and never calls a model, returning
what the paid passes would have cost in estimate. The memloom reconcile CLI
command defaults the other way and applies.
Passes come from the saved settings unless passes overrides them: two free ones on,
three paid ones off. With the contradiction re-check on a run takes minutes, so use the
stream variant rather than holding this request open. See
Reconciliation.
curl --request POST \
--url http://127.0.0.1:4319/memory/reconcile \
--header 'Content-Type: application/json' \
--data '
{
"mode": "dry_run",
"trigger": "manual",
"passes": [],
"budgetUsd": 49
}
'import requests
url = "http://127.0.0.1:4319/memory/reconcile"
payload = {
"mode": "dry_run",
"trigger": "manual",
"passes": [],
"budgetUsd": 49
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({mode: 'dry_run', trigger: 'manual', passes: [], budgetUsd: 49})
};
fetch('http://127.0.0.1:4319/memory/reconcile', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "http://127.0.0.1:4319/memory/reconcile"
payload := strings.NewReader("{\n \"mode\": \"dry_run\",\n \"trigger\": \"manual\",\n \"passes\": [],\n \"budgetUsd\": 49\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "4319",
CURLOPT_URL => "http://127.0.0.1:4319/memory/reconcile",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'mode' => 'dry_run',
'trigger' => 'manual',
'passes' => [
],
'budgetUsd' => 49
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}require 'uri'
require 'net/http'
url = URI("http://127.0.0.1:4319/memory/reconcile")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"mode\": \"dry_run\",\n \"trigger\": \"manual\",\n \"passes\": [],\n \"budgetUsd\": 49\n}"
response = http.request(request)
puts response.read_bodyHttpResponse<String> response = Unirest.post("http://127.0.0.1:4319/memory/reconcile")
.header("Content-Type", "application/json")
.body("{\n \"mode\": \"dry_run\",\n \"trigger\": \"manual\",\n \"passes\": [],\n \"budgetUsd\": 49\n}")
.asString();using RestSharp;
var options = new RestClientOptions("http://127.0.0.1:4319/memory/reconcile");
var client = new RestClient(options);
var request = new RestRequest("");
request.AddJsonBody("{\n \"mode\": \"dry_run\",\n \"trigger\": \"manual\",\n \"passes\": [],\n \"budgetUsd\": 49\n}", false);
var response = await client.PostAsync(request);
Console.WriteLine("{0}", response.Content);
{
"run": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"mode": "dry_run",
"trigger": "manual",
"status": "running",
"scanned": 123,
"retired": 123,
"folded": 123,
"questions": 123,
"conflictsRaised": 123,
"possible": 123,
"llmCalls": 123,
"spentUsd": 123,
"spentInputTokens": 123,
"spentOutputTokens": 123,
"error": "<string>",
"startedAt": "2023-11-07T05:31:56Z",
"finishedAt": "2023-11-07T05:31:56Z",
"revertedAt": "2023-11-07T05:31:56Z"
},
"actions": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"runId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"kind": "retire",
"class": "duplicate_content",
"memoryId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"reason": "identical content to 41b0e2d8, which is older and stays active",
"applied": true,
"surfaced": true,
"decision": "approved",
"mergeId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"conflictId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"candidateId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z"
}
],
"estimate": {
"window": 123,
"llmCalls": 123,
"inputTokens": 123,
"outputTokens": 123,
"model": "google/gemini-2.5-flash",
"usd": 123
},
"passes": [
"<string>"
],
"heldBack": {
"retire": 123,
"question": 123,
"conflict": 123
},
"entities": {
"examined": 123,
"pairs": 123,
"merged": 123,
"queued": 123,
"deferred": 123,
"skipped": 123
},
"arbitration": {
"calls": 123,
"folded": 123,
"rejected": 123,
"unsure": 123
},
"autoResolved": {
"examined": 123,
"resolved": 123
},
"recheck": {
"window": 123,
"calls": 123,
"claimed": 123,
"verified": 123,
"remaining": 123,
"spentUsd": 123,
"spentInputTokens": 123,
"spentOutputTokens": 123,
"stoppedBy": "budget"
}
}{
"error": "invalid request body",
"issues": [
{
"path": "query",
"message": "query must be a non-empty string"
}
]
}{
"error": "reconciliation is set to report only (RECONCILE_ENABLED=0). Remove it from your memloom config and restart the daemon to let a run act."
}{
"error": "memloom: a reconcile run is already going. Watch it in the Console, or wait for it to finish."
}Headers
The owner this request acts for. Applies to every owner-scoped endpoint, reads as well
as writes. Omit it and the daemon's own default owner answers (see GET /health), which
is what a single-owner install has always used. A value that is not a uuid is refused
with 400 rather than coerced.
Body
What to run. Every field is optional.
dry_run reports and changes nothing, and never calls a model. apply acts.
dry_run, apply Recorded on the run, so the history says who started it.
manual, idle, startup Run exactly these passes, ignoring the saved settings. Omit to use the settings, which is what every shipped surface does. This overrides the settings for the three paid passes too, so a caller can start a run that spends money while the Settings toggles are off. RECONCILE_ENABLED=0 still refuses it.
invariants, entities, llm_entities, llm_conflicts, llm_recheck Keep re-checking past the per-run ceiling until nothing is due or this much has been billed, measured against the provider's own reported cost. Omit for one page and stop, which is the default. Capped at 50 because a typo here spends real money.
x <= 50Response
What the run found and what it changed.
One run's own words. This is what the CLI prints and the Settings tab renders.
One run, and what it did.
Hide child attributes
Hide child attributes
dry_run, apply manual, idle, startup running, success, error, aborted Active memories the run looked at.
Memories staled. Never deleted, and undo puts them back.
Entities folded into another.
Findings reported rather than fixed.
Unconfirmed contradictions the re-check recorded. Not conflicts until a human says so.
What the run actually cost, from the provider's own billed figures. Written per call, so it survives a crash mid-sweep.
Why a run with status error failed. Null otherwise.
Hide child attributes
Hide child attributes
retire and fold changed state. question and conflict ask you. possible is an unconfirmed contradiction from the re-check.
retire, question, conflict, fold, possible The detector behind it.
"duplicate_content"
"identical content to 41b0e2d8, which is older and stays active"
True only when an applying run actually changed something here.
False when the finding was recorded but held back by a per-run cap.
approved, rejected, snoozed On a fold, the merge record the undo reverses.
On a conflict, the queue row this finding became.
On a possible contradiction, the older belief of the pair.
What re-checking everything currently due would cost, from the real prompt template and the actual content lengths. A dry run reports this instead of spending it. One run is capped at 200 beliefs, so on a store with a backlog this is what several runs add up to.
Hide child attributes
Hide child attributes
Active memories due a contradiction re-check.
One per memory in the window.
"google/gemini-2.5-flash"
Null for a model whose rates memloom does not know, rather than a confident wrong number.
The passes this run actually ran.
What one resolution pass did.
Hide child attributes
Hide child attributes
Entities considered.
Pairs that survived bucketing and were judged.
Folds applied without asking.
Uncertain folds written to the conflicts queue.
Uncertain folds not written because the queue is already full. Reported rather than dropped silently, so "nothing queued" never hides "there was more to ask about". A later pass picks them up.
Pairs already merged, queued, or settled as distinct.
Present when the contradiction re-check ran.
Hide child attributes
Hide child attributes
Beliefs this run swept.
Contradictions the model claimed, before the quotes were checked.
Findings whose quotes were found in both memories, and were recorded.
Beliefs still due when the run stopped.
Billed for this pass, from the provider's own figures.
Why it stopped short. cap is the per-run ceiling with no budget set, budget is the spend limit, aborted is a stop, unpriced means a budget was set but the provider reported no cost so the run refused to page on blind, and failed means every call in a page failed. Null means nothing is due any more.
budget, aborted, cap, unpriced, failed