Skip to content

Commit bd1855a

Browse files
fix(cloud): repair exported empty sessions
1 parent 629d324 commit bd1855a

2 files changed

Lines changed: 45 additions & 13 deletions

File tree

docs/engram-cloud/troubleshooting.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ You may also see the failure only during the final server push, even after `doct
103103
write chunk: cloud: push chunk ...: status 400: invalid push payload: sessions[N].directory is required
104104
```
105105

106-
It means a historical `session` mutation in `sync_mutations` is missing `directory`, a local `sessions` row for the project still has an empty/null `directory`, 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 or local session rows may still need repair before first cloud upload.
106+
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`, 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 or local session rows may still need repair before first cloud upload.
107107

108108
### Safe path: helper script
109109

@@ -144,7 +144,7 @@ If doctor reveals another legacy blocker after each repair, use loop mode after
144144
tools/repair-missing-session-directory.sh --apply --interactive --all <project>
145145
```
146146

147-
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 for the project still have empty/null `directory`, loop mode applies that fallback repair and reruns doctor once more. It still stops on unsupported blockers, project mismatches, or observation payloads that cannot be fully inferred. In non-interactive loop mode, rerun with `--interactive` when the script asks for human-provided observation fields.
147+
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. It still stops on unsupported blockers, project mismatches, or observation payloads that cannot be fully inferred. In non-interactive loop mode, rerun with `--interactive` when the script asks for human-provided observation fields.
148148

149149
If one-shot mode finds no doctor blocker but reports local sessions with empty/null directory, preview and apply the fallback explicitly:
150150

@@ -176,7 +176,7 @@ For session repairs, the script patches one legacy row in `sync_mutations` by ad
176176
"directory": "/absolute/path/to/project"
177177
```
178178

179-
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 for the requested project where `directory IS NULL OR directory = ''`; it does not modify `sync_mutations`.
179+
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`.
180180

181181
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:
182182

@@ -252,6 +252,7 @@ If you want to inspect before using the helper:
252252
sqlite3 ~/.engram/engram.db "select seq, entity, op, entity_key, payload from sync_mutations where seq = 873;"
253253
sqlite3 ~/.engram/engram.db "select id, project, directory from sessions where id = 'manual-save-current';"
254254
sqlite3 ~/.engram/engram.db "select id, project, started_at, directory from sessions where project = '<project>' and (directory is null or directory = '');"
255+
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>'));"
255256
```
256257

257258
Do not manually edit SQLite without a backup.

tools/repair-missing-session-directory.sh

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -284,28 +284,50 @@ empty_session_count() {
284284
printf '0\n'
285285
return 0
286286
fi
287+
scope_predicate=$(empty_session_scope_predicate)
288+
sqlite_scalar "SELECT COUNT(*) FROM sessions WHERE ifnull(directory, '') = '' AND $scope_predicate;"
289+
}
290+
291+
empty_session_scope_predicate() {
287292
PROJECT_SQL=$(sql_escape "$PROJECT")
288-
sqlite_scalar "SELECT COUNT(*) FROM sessions WHERE project = '$PROJECT_SQL' AND ifnull(directory, '') = '';"
293+
predicate="(project = '$PROJECT_SQL'"
294+
295+
if have_table observations; then
296+
predicate="$predicate OR id IN (
297+
SELECT session_id FROM observations
298+
WHERE ifnull(project, '') = '$PROJECT_SQL'
299+
OR (ifnull(project, '') = '' AND session_id IN (SELECT id FROM sessions WHERE project = '$PROJECT_SQL'))
300+
)"
301+
fi
302+
303+
if have_table user_prompts; then
304+
predicate="$predicate OR id IN (
305+
SELECT session_id FROM user_prompts
306+
WHERE ifnull(project, '') = '$PROJECT_SQL'
307+
OR (ifnull(project, '') = '' AND session_id IN (SELECT id FROM sessions WHERE project = '$PROJECT_SQL'))
308+
)"
309+
fi
310+
311+
printf '%s\n' "$predicate)"
289312
}
290313

291314
preview_empty_sessions() {
292-
PROJECT_SQL=$(sql_escape "$PROJECT")
315+
scope_predicate=$(empty_session_scope_predicate)
293316
info "Affected local sessions:"
294317
if have_column sessions started_at; then
295-
sqlite_box "SELECT id, project, started_at, ifnull(directory, '') AS directory FROM sessions WHERE project = '$PROJECT_SQL' AND ifnull(directory, '') = '' ORDER BY ifnull(started_at, ''), id;" || true
318+
sqlite_box "SELECT id, project, started_at, ifnull(directory, '') AS directory FROM sessions WHERE ifnull(directory, '') = '' AND $scope_predicate ORDER BY ifnull(started_at, ''), id;" || true
296319
else
297-
sqlite_box "SELECT id, project, ifnull(directory, '') AS directory FROM sessions WHERE project = '$PROJECT_SQL' AND ifnull(directory, '') = '' ORDER BY id;" || true
320+
sqlite_box "SELECT id, project, ifnull(directory, '') AS directory FROM sessions WHERE ifnull(directory, '') = '' AND $scope_predicate ORDER BY id;" || true
298321
fi
299322
}
300323

301324
repair_empty_sessions() {
302325
have_table sessions || die "sessions table not found in DB: $DB"
303-
PROJECT_SQL=$(sql_escape "$PROJECT")
304326
missing_count=$(empty_session_count)
305327

306328
if [ "$missing_count" = "0" ]; then
307-
info "No local sessions with empty/null directory found for project '$PROJECT'."
308-
return 1
329+
info "No local sessions in project export scope with empty/null directory found for project '$PROJECT'."
330+
return 0
309331
fi
310332

311333
DIRECTORY=$(resolve_directory)
@@ -332,12 +354,13 @@ repair_empty_sessions() {
332354
cp "$DB" "$backup"
333355
info ""
334356
info "Backup created: $backup"
357+
scope_predicate=$(empty_session_scope_predicate)
335358

336359
sqlite3 -batch "$DB" "BEGIN;
337360
UPDATE sessions
338361
SET directory = '$DIRECTORY_SQL'
339-
WHERE project = '$PROJECT_SQL'
340-
AND ifnull(directory, '') = '';
362+
WHERE ifnull(directory, '') = ''
363+
AND $scope_predicate;
341364
COMMIT;"
342365

343366
remaining_count=$(empty_session_count)
@@ -351,7 +374,7 @@ handle_no_supported_blocker() {
351374
missing_count=$(empty_session_count)
352375

353376
if [ "$missing_count" != "0" ]; then
354-
info "No supported sync_mutations blocker found, but $missing_count local session row(s) for project '$PROJECT' have empty/null directory."
377+
info "No supported sync_mutations blocker found, but $missing_count local session row(s) in project export scope for '$PROJECT' have empty/null directory."
355378
if [ "$FIX_EMPTY_SESSIONS" -eq 1 ] || [ "$ALL" -eq 1 ]; then
356379
repair_empty_sessions
357380
return 0
@@ -361,6 +384,9 @@ handle_no_supported_blocker() {
361384
fi
362385

363386
info "No supported session/observation upsert blocker found."
387+
if [ "$FIX_EMPTY_SESSIONS" -eq 1 ]; then
388+
info "No local sessions in project export scope with empty/null directory found for project '$PROJECT'."
389+
fi
364390
[ "$context" = "loop" ] && info "Loop mode complete."
365391
return 1
366392
}
@@ -369,6 +395,11 @@ if [ ! "$SEQ" ] || [ "$ALL" -eq 1 ]; then
369395
require_cmd engram
370396
fi
371397

398+
if [ "$FIX_EMPTY_SESSIONS" -eq 1 ] && [ "$ALL" -ne 1 ] && [ ! "$SEQ" ]; then
399+
repair_empty_sessions
400+
exit 0
401+
fi
402+
372403
if [ ! "$SEQ" ] && [ "$ALL" -ne 1 ]; then
373404
tmp=${TMPDIR:-/tmp}/engram-doctor-output.$$.txt
374405
trap 'rm -f "$tmp"' EXIT HUP INT TERM

0 commit comments

Comments
 (0)