Use this guide when local Engram saves work but cloud sync does not advance. The safest rule is simple: local SQLite is the source of truth; cloud is replication. Back up the local database before repairing anything.
Run these commands first:
engram version
engram cloud status
engram cloud upgrade doctor --project <project>
engram sync --cloud --status --project <project>Check three things:
| Signal | Healthy value | What it means |
|---|---|---|
engram version |
Latest release on client and server | Client/server version skew can block old chunk sync |
cloud status |
configured + auth ready | CLI has a server URL and runtime token |
doctor |
ready or actionable repair |
Local metadata is safe to upload |
last_acked_seq |
Advances after sync | Cloud accepted the pending journal |
If the dashboard shows 0 observations but local saves exist, the cloud server has not accepted the client's pending sync yet. Do not delete local data.
Use the supported commands exactly:
engram cloud config --server https://your-cloud-host
engram cloud status
engram cloud enroll <project>
engram sync --cloud --project <project>engram cloud config --status is not the documented status command. Use engram cloud status.
Cloud auth token is runtime config:
export ENGRAM_CLOUD_TOKEN="your-token"The local ~/.engram/cloud.json stores the server URL. The token is intentionally read from the environment.
This error means the legacy chunk upload endpoint rejected a payload because the client-provided chunk_id did not match the server-computed canonical hash.
Upgrade both sides to v1.14.8 or newer:
brew update
brew upgrade engram
engram versionRedeploy or restart the cloud server so the server binary also runs v1.14.8 or newer.
Then retry:
engram sync --cloud --project <project>
engram sync --cloud --status --project <project>In v1.14.8, the server treats the client chunk_id as advisory. The server validates and canonicalizes the payload, computes its own chunk ID, stores using the server-computed ID, and returns that ID. Valid payloads no longer get blocked by client/server canonicalization drift.
This is the common legacy manual-save blocker:
session payload directory is required and cannot be inferred from local state (seq=N entity=session op=upsert)
Another legacy blocker can appear for observation upserts:
observation payload missing required upsert fields: title (seq=N entity=observation op=upsert)
canonicalize cloud chunk: mutations[768]: observation payload title is required for upsert
You may also see the failure only during the final server push, even after doctor says the project is ready:
write chunk: cloud: push chunk ...: status 400: invalid push payload: sessions[N].directory is required
write chunk: cloud: push chunk ...: status 400: invalid push payload: observations[N].title is required
It means a historical session mutation in sync_mutations is missing directory, a local sessions row included in the project export still has an empty/null directory, a local observations row included in the project export is missing a cloud-push required field, or a historical observation mutation is missing one of the required upsert fields: sync_id, session_id, type, title, content, or scope. Newer Engram versions write these fields correctly, but old journal rows, local session rows, or exported local observation rows may still need repair before first cloud upload.
Engram includes a temporary rescue helper. Despite the historical file name, it repairs missing session directories in both sync_mutations payloads and local sessions.directory rows, missing observation payload fields, and exported local observations rows whose required cloud-push fields are empty:
tools/repair-missing-session-directory.sh <project>Run it from inside the real project directory for session directory repairs. Observation repairs do not require a directory argument. Dry-run is the default.
cd /absolute/path/to/project
tools/repair-missing-session-directory.sh <project>Review the preview. For session repairs, confirm the detected Directory: is correct. For observation repairs, confirm the Local observation row is the authoritative local data. If an observation field such as title is still missing and the local row cannot fully infer it, preview an interactive repair first:
tools/repair-missing-session-directory.sh --interactive --seq 1677 sias-appThe interactive mode shows the mutation payload, matching local observation row when available, and a short content excerpt so you can provide only the missing required observation fields. Dry-run is still the default, so review the patched payload first. When it looks correct, rerun with --apply --interactive:
tools/repair-missing-session-directory.sh --apply --interactive --seq 1677 sias-appFor non-interactive repairs that can be inferred completely from local state, apply directly:
tools/repair-missing-session-directory.sh --apply <project>If doctor reveals another legacy blocker after each repair, use loop mode after you have reviewed a one-shot dry-run:
tools/repair-missing-session-directory.sh --apply --interactive --all <project>Loop mode repairs exactly one supported blocker (entity=session|observation op=upsert), reruns engram cloud upgrade doctor --project <project>, then repeats until doctor no longer reports a supported blocker. If doctor reports ready but local sessions rows included in the project export still have empty/null directory, loop mode applies that fallback repair and reruns doctor once more. If exported local observations rows are missing required fields such as title, loop mode applies that fallback repair next and reruns doctor again. It still stops on unsupported blockers, project mismatches, or observation data that cannot be fully inferred. In non-interactive loop mode, rerun with --interactive when the script asks for human-provided observation fields.
If one-shot mode finds no doctor blocker but reports local sessions with empty/null directory, preview and apply the fallback explicitly:
tools/repair-missing-session-directory.sh --fix-empty-sessions <project>
tools/repair-missing-session-directory.sh --apply --fix-empty-sessions <project>If final push fails with observations[N].title is required even though doctor is ready, preview and apply the exported observation fallback explicitly:
tools/repair-missing-session-directory.sh --fix-empty-observations <project>
tools/repair-missing-session-directory.sh --apply --fix-empty-observations <project>--fix-exported runs both exported-row fallbacks in one shot:
tools/repair-missing-session-directory.sh --fix-exported <project>
tools/repair-missing-session-directory.sh --apply --fix-exported <project>Use --max to cap the number of repairs and avoid accidental infinite loops. The default is 20:
tools/repair-missing-session-directory.sh --apply --interactive --all --max 5 <project>Then rerun the normal flow:
engram cloud upgrade doctor --project <project>
engram cloud upgrade repair --project <project> --dry-run
engram cloud upgrade repair --project <project> --apply
engram sync --cloud --project <project>For session repairs, the script patches one legacy row in sync_mutations by adding a JSON field:
"directory": "/absolute/path/to/project"It also updates sessions.directory only when the matching session row exists and its directory is empty. In the fallback path for sessions[N].directory is required, it updates only local sessions rows included in the requested project export scope where directory IS NULL OR directory = ''; it does not modify sync_mutations.
For observation repairs, the script reads the authoritative local row from observations using payload.sync_id or entity_key, then fills only missing or empty fields in the mutation payload:
sync_id, session_id, type, title, content, scope
It does not invent values in non-interactive mode. If the local observation row is missing, or the required payload fields would still be empty after patching, the script exits non-zero without modifying the database. Use --interactive when you need to provide missing observation values manually after reviewing the payload and content excerpt.
For exported local observation repairs, the script changes only observations rows that belong to the requested project export scope:
- rows where
ifnull(project, '') = '<project>' - rows with empty
projectwhosesession_idbelongs to asessions.project = '<project>'session
It does not touch sync_mutations. Safe inferred fields are filled as follows: empty sync_id becomes the row id, empty type becomes manual, empty title is inferred from non-empty content, and empty scope becomes project. Empty session_id is always blocked. Empty content is blocked unless you run with --interactive and provide it yourself.
It never changes last_acked_seq, never deletes mutations, and creates a timestamped database backup before each applied blocker. Loop mode can be noisy because it intentionally keeps one backup per repair.
If you do not pass --seq, the script runs:
engram cloud upgrade doctor --project <project>and extracts the first matching blocker:
seq=N entity=session op=upsert
seq=N entity=observation op=upsert
If you already know the sequence:
tools/repair-missing-session-directory.sh --seq 873 <project>
tools/repair-missing-session-directory.sh --apply --seq 873 <project>Loop mode does not accept --seq; it always asks doctor for the next supported blocker each iteration.
Precedence:
- Explicit directory argument.
git rev-parse --show-toplevelfrom the current directory.pwd.
The directory must be absolute. Good examples:
/home/user/work/sias-app
/Users/user/work/sias-app
C:/Users/user/work/sias-app
Bad example:
sias-app
On Windows/Git Bash, prefer forward slashes (C:/Users/user/work/sias-app) to avoid JSON and SQL escaping problems.
Use this when you are not currently inside the project directory:
tools/repair-missing-session-directory.sh --apply --seq 873 sias-app C:/Users/user/work/sias-appIf you want to inspect before using the helper:
sqlite3 ~/.engram/engram.db "select seq, entity, op, entity_key, payload from sync_mutations where seq = 873;"
sqlite3 ~/.engram/engram.db "select id, project, directory from sessions where id = 'manual-save-current';"
sqlite3 ~/.engram/engram.db "select id, project, started_at, directory from sessions where project = '<project>' and (directory is null or directory = '');"
sqlite3 ~/.engram/engram.db "select s.id, s.project, s.started_at, s.directory from sessions s where (s.directory is null or s.directory = '') and (s.project = '<project>' or s.id in (select session_id from observations where ifnull(project, '') = '<project>' union select session_id from user_prompts where ifnull(project, '') = '<project>'));"
sqlite3 ~/.engram/engram.db "select o.id, ifnull(o.sync_id, '') as sync_id, ifnull(o.session_id, '') as session_id, ifnull(o.project, '') as project, ifnull(o.type, '') as type, ifnull(o.title, '') as title, ifnull(o.scope, '') as scope from observations o where (ifnull(o.project, '') = '<project>' or (ifnull(o.project, '') = '' and o.session_id in (select id from sessions where ifnull(project, '') = '<project>'))) and (ifnull(o.sync_id, '') = '' or ifnull(o.session_id, '') = '' or ifnull(o.type, '') = '' or ifnull(o.title, '') = '' or ifnull(o.content, '') = '' or ifnull(o.scope, '') = '');"Do not manually edit SQLite without a backup.
transport_failed is a wrapper around network, auth, server, or payload errors. Look for the concrete error message below it.
| Concrete error | Next step |
|---|---|
chunk_id does not match payload content hash |
Upgrade client and server to v1.14.8 or newer |
session payload directory is required |
Run the missing session directory helper |
sessions[N].directory is required |
Run the missing session directory helper with --fix-empty-sessions or --all |
observation payload title is required for upsert |
Run the missing session directory helper; it also repairs missing observation payload fields from local observations |
observations[N].title is required |
Run the missing session directory helper with --fix-empty-observations or --all |
401 or auth_required |
Check ENGRAM_CLOUD_TOKEN on the client and server |
403 or policy_forbidden |
Check ENGRAM_CLOUD_ALLOWED_PROJECTS on the server |
server_unsupported |
Redeploy a cloud server with mutation endpoints |
After any repair, verify in this order:
engram cloud status
engram cloud upgrade doctor --project <project>
engram cloud upgrade repair --project <project> --dry-run
engram cloud upgrade repair --project <project> --apply
engram sync --cloud --project <project>
engram sync --cloud --status --project <project>Expected result:
doctorno longer reports the same blocker.sync --cloudcompletes without canonicalization errors.last_acked_seqadvances.- Dashboard stats stop showing
0once data has been accepted by cloud.
- Do not delete
sync_mutationsrows to make the error disappear. - Do not edit
last_acked_seqmanually. - Do not invent a relative
directorylikesias-app. - Do not assume dashboard
0means local data is gone. - Do not run repair without a database backup.
- Cloud setup: Quickstart
- Full command reference: DOCS.md — Cloud CLI
- Autosync details: DOCS.md — Cloud Autosync