# Agent session archive

> Import selected Codex, Claude Code, OpenClaw, and Hermes sessions into a dedicated, redacted, searchable archive that stays separate from your curated vault, manually or through opt-in automation (a Claude Code SessionEnd hook or a daemon schedule).

Section: Guides  
Canonical: https://gno.sh/docs/sessions  
Markdown: https://gno.sh/docs/sessions.md

GNO can make selected local agent conversations searchable while keeping who said what, when, and where. Supported harnesses are Codex, Claude Code, OpenClaw, and Hermes. Each import writes a sanitized archive you own, then indexes it through the ordinary JSONL record path, so search, query, ask, get, and Context Capsules work on it without a separate ranking engine.

Imported dialogue is source evidence, not truth. An assistant suggestion is labelled as assistant output and never shown as a user decision. Nothing is promoted to memory: turning something from a session into a standing fact stays an explicit `gno remember` call. See [Memory](https://gno.sh/docs/memory).

## Manual by default

- A fresh install imports nothing. There is no watcher for sessions, and no hook, schedule, or background job runs until you switch on automation for a profile.
- Every import is an explicit command, Web UI action, `gno_sessions_import` call, REST request, or SDK call, until you explicitly enable [automation](#automation-opt-in) for a profile.
- Hooks and scheduled imports are opt-in, one profile and one trigger at a time. Installing, upgrading, repairing, or restarting GNO never enables them, and GNO never installs or starts a service for you.
- Import reads the original session files and databases read-only and never modifies them. SQLite stores are opened with a read-only connection inside one read transaction; no raw copy is made.

## Archive versus curated vault

Your curated vault holds notes you chose to keep. Session logs are high-volume, repetitive, and full of assistant guesses. The recommended setup therefore keeps them in a dedicated archive outside the vault, with its own config file and its own named index. Plain `gno update`, `gno search`, and your curated MCP or Web UI server never read the archive unless you opt in.

- **Dedicated pair** — one archive config (for example `~/gno-sessions/archive.yml`) bound to one named index (for example `sessions`). Every archive command passes both: `gno --config ~/gno-sessions/archive.yml --index sessions <command>`.
- **Binding is enforced** — the archive config with a different --index, or the archive index opened with any other config (including the default config and cross-index get), fails with `SESSIONS_BINDING_MISMATCH`.
- **Protected placement** — `sessions init` refuses the default config file and the `default` index. The archive root must sit outside GNO's config, data, and cache directories, so `gno reset`, index cleanup, and uninstall cannot delete it.
- **Not inside curated collections** — `sessions init` also refuses an archive root inside a folder your default (curated) config already indexes. A vault folder no curated collection covers is fine; to search sessions from the curated index, opt in with mixed retrieval after init.
- **Separate collections per boundary** — use one archive collection per privacy or domain boundary, and optionally per project. Archive collections are not memory-managed.

## Set up an archive

Preview what exists on this machine first. Discovery reports session roots, unit counts, sizes, and sampled format versions. It never imports and needs no archive.

```
gno sessions discover
```

Discovery looks in `$CODEX_HOME/sessions`, else `~/.codex/sessions`; `~/.claude/projects` and `$CLAUDE_CONFIG_DIR/projects`; `$OPENCLAW_STATE_DIR` or `~/.openclaw`; and `$HERMES_HOME` or `~/.hermes`. Then create the archive and register the sources you approve:

```
gno --config ~/gno-sessions/archive.yml --index sessions sessions init --archive ~/gno-sessions/archive --collection sessions-work
gno --config ~/gno-sessions/archive.yml --index sessions sessions source add codex --harness codex --path ~/.codex/sessions --collection sessions-work
gno --config ~/gno-sessions/archive.yml --index sessions sessions source add claude --harness claude-code --path ~/.claude/projects --collection sessions-work
```

\--archive must be an absolute directory (the shell expands \~). Init is idempotent, records the archive binding in the named index immediately, refuses an index that already holds other collections, and never retargets a config already bound to another index or root. Source IDs and collection names are lowercase letters, digits, hyphens, and underscores. Registering a source never imports.

Route projects to their own collections with `--project <prefix>=<collection>`, repeatable. Threads recorded under that working-directory prefix land in the mapped collection:

```
gno --config ~/gno-sessions/archive.yml --index sessions sessions source add codex-client --harness codex --path ~/.codex/sessions --collection sessions-work --project ~/work/client-a=sessions-client-a
```

A thread whose working directories map to different collections is quarantined (`skipped_policy`, reason `mixed_domain`) instead of being written to the less restricted one. Add a --project mapping that resolves all its directories to one collection and the next import archives it. Changing a source's collection or mappings re-imports the affected threads on the next run. Removing a source with `sessions source remove <id>` keeps its archive.

## Import

```
gno --config ~/gno-sessions/archive.yml --index sessions sessions import --source codex --dry-run
gno --config ~/gno-sessions/archive.yml --index sessions sessions import --source codex
gno --config ~/gno-sessions/archive.yml --index sessions embed
```

- `--dry-run` parses and reports, including redaction counts and destination collections, without writing archive files, checkpoint state, or index rows.
- `--limit <n>` bounds how many changed units one run processes; unchanged units do not count and the rest are reported as deferred.
- Explicit files: `sessions import <paths...> --collection <name> [--format codex|claude-code|openclaw|hermes]`. Paths must be absolute; `--collection` is rejected together with `--source`.
- Import updates the lexical index but does not embed. Run embed on the archive pair for semantic search; the receipt reports the embedding backlog.

Reruns are idempotent. A unit is skipped when its fingerprint and the parser, redaction, and archive-format versions are unchanged; a changed unit is re-rendered and compared byte for byte. Turn IDs stay stable across appends, so a growing session updates its archive file and the index reconciles its records. Imports on one archive serialize; a second concurrent import fails as busy.

## Search the archive

Session search is ordinary search, query, ask, get, and Context Capsules against the archive pair. Broad search in your curated index does not see archives.

```
gno --config ~/gno-sessions/archive.yml --index sessions query "why did we pick sqlite" --category harness/codex --author human
gno --config ~/gno-sessions/archive.yml --index sessions search "migration plan" --tags-all project/api,role/human
```

Harness and project identity are tags, so one project stays searchable across agents:

- `harness/codex`, `harness/claude-code`, `harness/openclaw`, `harness/hermes`
- `project/<basename>` and `project-id/<hash>`, which tells apart projects that share a folder name
- `role/human`, `role/assistant`; the author field is `human` or `assistant`
- `session-kind/main`, `subagent`, `fork`, or `continuation`, plus `session` on every record

## Opt-in mixed retrieval

Nothing federates automatically, and there is no cross-index server context. To search sessions from your curated index, register an archive collection folder in the curated config yourself:

```
gno collection add ~/gno-sessions/archive/sessions-work --name sessions-work --pattern '**/*.jsonl'
```

Then copy that collection's `recordAdapters.jsonl.fieldMapping` block from the archive config into the new collection entry in the curated config, so author, tags, session, and date metadata keep working:

```
collections:
  - name: sessions-work
    path: /absolute/path/to/gno-sessions/archive/sessions-work
    pattern: "**/*.jsonl"
    recordAdapters:
      jsonl:
        fieldMapping:
          id: /id
          title: /title
          body: /body
          author: /author
          categories: /categories
          sessionId: /sessionId
          threadId: /threadId
          dateFields:
            recorded: /recordedAt
```

From then on, session records take part in that index's ordinary retrieval, graph, listing, and tag behaviour, and any agent connected to the curated index can read them. Only do this for collections whose content you are comfortable exposing there.

## What is imported and what is excluded

Human speech is kept only where the harness structurally marks it as human; assistant final text is kept. Excluded by default:

- Tool calls and tool results
- Reasoning and thinking blocks
- Injected instructions, context, and system reminders
- Compaction summaries and copied fork or continuation history
- A subagent's task prompt written by its parent agent
- Prompts of programmatic runs (`codex exec`, Claude Code SDK and `claude -p` sessions), because a program or another agent may have written them; their assistant replies are archived

Slash commands a person typed in Claude Code are archived as `/name args`. Forks and subagents stay distinct threads linked to their parent. Text that imitates another speaker inside a message stays quoted content. Unknown record kinds produce bounded, content-free drift diagnostics. The archive is a record of dialogue, not a complete execution audit.

## Provenance and citation

Each archived turn is one record. Its body starts with `Human:` or `Assistant:` and ends with one `Provenance:` line whose fields are separated by `· `:

- For assistant turns, the marker _assistant output, not a user decision_
- `recorded unknown` when the source has no timestamp; a known time is the record date
- The source profile ID
- The native locator: file name plus line, or database row; never a host path
- The logical turn ID

Titles read like `Human · Codex · api`. Thread and session IDs, harness, project and role travel as record fields and tags. Cite session evidence with the record's `gno://` URI; URIs from the archive index carry `?index=sessions`.

## Receipts and partial outcomes

Every import returns the same receipt on the CLI, MCP, REST, and SDK. Receipts, status, and diagnostics carry no session content and no host paths.

- **status** — `complete`, `partial`, `failed`, or `nothing_to_do`. Any incomplete, failed, unsupported, or deferred unit, or a failed lexical sync, makes the run partial; a run in which nothing imported and nothing is incomplete is failed. A partial import is never reported as complete. A rerun over unchanged sources counts their archived threads as unchanged.
- **Thread counts** — imported, updated, unchanged, and skipped by policy (mixed\_domain, over\_limit).
- **Unit counts** — unsupported, incomplete, failed, and deferred.
- **Turn counts** — human, assistant, redactions, skipped injected content, skipped copied history, and over-limit turns.
- **Per-unit outcomes** with safe locators and reasons such as `truncated_tail`, `format_drift`, `snapshot_read_failed`, `permission_denied`, `source_missing`, and `format_not_recognised`.
- **Index state** — lexical readiness and the embedding backlog.

A file still being written (its final line is cut) or one showing format drift is incomplete: readable threads are archived, the checkpoint does not advance, and the next run retries it. Archive writes are atomic, so an interrupted import heals on rerun without duplicates. Units count as complete only after the index sync succeeds; a failed or interrupted sync is retried by the next import. Units with malformed records stay incomplete. A thread withheld by policy (quarantined, or at a limit, which is skipped whole rather than archived truncated) has any earlier archived copy removed from search. When a registered source root is missing or cannot be read, import still rescans its retained archive for new redaction rules, then fails with SESSIONS\_SOURCE\_UNAVAILABLE (exit 2) instead of reporting the source as up to date. A directory or session file inside a source that cannot be read is a failed unit (a directory has the locator ".", since its name can be a host path), so the run is partial, never nothing\_to\_do, and those units keep their checkpoints until they are read.

Error codes and exit statuses are listed under [Error codes](#error-codes).

## Supported formats and versions

Parsers are verified against the store layouts below. Where no live store was available, the table says so.

| Harness     | Store                                                                                                                      | Verified against                                                                                                                                                                      |
| ----------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Codex       | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`                                                                             | Local store structure, CLI 0.58 through 0.156.1 (September 2026). Covers the legacy user/agent message events (0.150 and earlier) and completed-item speech events (0.151 and later). |
| Claude Code | `~/.claude/projects/<dir>/<session>.jsonl`, subagent files under `<session>/subagents/`, and `$CLAUDE_CONFIG_DIR/projects` | Local store structure, CLI 2.1.2xx (September 2026). Files without the origin field use a conservative fallback that could not be verified against a real file.                       |
| OpenClaw    | `~/.openclaw/agents/<agent>/agent/openclaw-agent.sqlite` (schema 23) and legacy `agents/<agent>/sessions/*.jsonl`          | Upstream v2026.9.6 source definitions, tested with synthetic fixtures; not verified against a live installation. zstd-compressed events are supported.                                |
| Hermes      | `~/.hermes/state.db` and `profiles/<name>/state.db`                                                                        | Hermes v0.19.0 schema (version 22) from the installed source, tested with synthetic fixtures; no populated store was available.                                                       |

Consumer chat exports keep using the generic JSONL and transcript [export adapters](https://gno.sh/docs/file-export-adapters). Account connectors are out of scope.

Limits: a source unit up to 512 MiB; a JSONL line up to 32 MiB (larger lines are skipped and counted); a turn up to 256 Ki characters (skipped and counted); up to 20,000 turns and 64 MiB of archive per thread (larger threads are skipped as over\_limit); up to 100,000 units per source per run.

## Backup, retention, and pruning

- Durable data is the archive root (plain JSONL files plus the `.gno-sessions/state.json` checkpoint) and the archive config. Back them up like any folder.
- The SQLite index is disposable. Rebuild it with `gno --config ~/gno-sessions/archive.yml --index sessions update` and embed again. A lost checkpoint only costs a full re-render on the next import; the archive files are the source of truth.
- Deleting or rotating a source file never deletes its archive. Status reports it as sourceUnavailable.
- `sessions prune --source <id>` previews archived units whose source is gone; `--apply` deletes exactly those archive files and syncs the index. An archive file that a still-present unit references is never pruned, so a session file the harness moved keeps its archive. Prune needs a complete listing: when part of the source cannot be read, it fails with SESSIONS\_SOURCE\_UNAVAILABLE rather than treating unread units as deleted. If the index sync fails, prune records nothing and the next prune retries.
- A parser upgrade reparses only when the source still exists; otherwise status reports staleParser and keeps the archive as is. A new parser cannot recover content from rotated or deleted sources. A redaction-rule upgrade or a change to your redaction literals applies on the next import: units with a source are re-rendered, and archives without one are rescanned in place.

```
gno --config ~/gno-sessions/archive.yml --index sessions sessions status
gno --config ~/gno-sessions/archive.yml --index sessions sessions prune --source codex
gno --config ~/gno-sessions/archive.yml --index sessions sessions prune --source codex --apply
```

## Automation (opt-in)

Automation imports new session turns without you running `sessions import` each time. It is off until you switch it on, and it only ever calls the same importer as a manual import: the same sources, destinations, redaction, checkpoints, and receipts. Installing, upgrading, repairing, or restarting GNO never enables it, and a paused trigger stays paused.

Two triggers exist, each switched on separately for one **profile**:

| Trigger                     | What fires it                               | What it does in the foreground                  |
| --------------------------- | ------------------------------------------- | ----------------------------------------------- |
| Claude Code SessionEnd hook | Claude Code ending a session                | Marks the profile pending on disk, then returns |
| Daemon schedule             | `gno daemon` on this archive, every cadence | Marks the profile pending when due              |

Neither trigger imports anything by itself. The import runs later, in one of two places:

- `gno daemon` on the archive's config and index drains pending profiles at startup and every 30 seconds.
- `gno sessions automation run <profile>` runs a profile now, in the foreground.

```
Claude Code SessionEnd ──► gno sessions hook ──┐
                                               ├─► pending marker (per profile,
daemon schedule tick (due) ────────────────────┘    durable, coalesced)
                                                         │
                        gno daemon tick / automation run ▼
                                          manual importer (sessions import)
```

With no daemon running, hook events stay pending (reported as `pending, not yet archived`) until a daemon starts or you run the profile. `gno serve` never drains pending work or runs schedules.

### Support matrix

| Trigger                           | Status                                                                                                                                        |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Claude Code SessionEnd hook       | Supported. Verified with Claude Code 2.1.280: SessionEnd fires for non-interactive `claude -p` runs.                                          |
| Claude Code interactive `/exit`   | Unverified: whether SessionEnd fires when an interactive session ends with /exit was not verified. The daemon schedule catches up either way. |
| Codex, OpenClaw, and Hermes hooks | Not supported. `enable --hook codex` fails with `SESSIONS_UNSUPPORTED_INTEGRATION`; import manually or on a daemon schedule.                  |
| Pre-compaction triggers           | Not supported in any harness. Manual or daemon schedule only.                                                                                 |
| Daemon schedule                   | Supported for every registered source; ticks only inside `gno daemon` on the archive's config and index.                                      |

### Quick start

```
A="gno --config ~/gno-sessions/archive.yml --index sessions"

# 1. A profile names registered sources. It enables nothing.
$A sessions automation set claude --source claude-code

# 2. Review what would run: sources, destinations, the exact hook command,
#    the settings file it edits, and the daemon prerequisite.
$A sessions automation preview claude

# 3. Switch on the Claude Code SessionEnd hook, the schedule, or both.
$A sessions automation enable claude --hook claude-code
$A sessions automation enable claude --schedule --cadence 30m

# 4. Start the daemon that performs the imports (GNO never starts it for you).
$A daemon --detach

# 5. Watch it work.
$A sessions status
```

### Profiles

A profile lives in the archive config under `sessions.automation`:

```
sessions:
  index: sessions
  archiveRoot: /absolute/path/to/gno-sessions/archive
  sources:
    - id: claude-code
      harness: claude-code
      path: /absolute/path/to/.claude/projects
      collection: sessions-work
  automation:
    - id: claude
      sources: [claude-code]
      hook:
        harness: claude-code
        enabled: true
        settings: /absolute/path/to/.claude/settings.json
      schedule:
        enabled: true
        cadence: 30m
      limit: 200 # optional: changed units per source per run
      retries: 3 # optional: automatic retries after a failed run
```

- `sources` must be registered sources (1 to 64). Destinations, project mappings, and privacy boundaries stay on those sources; a profile cannot add a source or change where turns land. Reconfigure with `sessions automation set`.
- `hook` and `schedule` appear only after `enable`, and `enabled` changes only through `enable` and `disable` (or your own edit of the file).
- `limit` (default 200) bounds each run. When more changed units wait, the rest stay pending and the next daemon tick continues. `retries` is 0 to 10 (default 3).
- At most 16 profiles per archive.

Run state (pending markers, last run, next due time, daemon heartbeat) is machine-written to `<archiveRoot>/.gno-sessions/automation.json` with mode `0600`. It is not configuration; deleting it only forgets pending work and history.

### Claude Code SessionEnd hook

`enable --hook claude-code` adds one entry to a Claude Code settings file, by default `$CLAUDE_CONFIG_DIR/settings.json`, else `~/.claude/settings.json`, which is created when missing. Another file can be chosen only with the CLI (`--settings <absolute path>`), and it must already exist; the Web UI and REST always use the recorded or default file:

```
{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "GNO_DATA_DIR='…' GNO_CACHE_DIR='…' '/path/to/bun' run '/path/to/gno/src/index.ts' --config '/absolute/path/to/gno-sessions/archive.yml' --index 'sessions' sessions hook claude-code --profile 'claude'",
            "timeout": 5
          }
        ]
      }
    ]
  }
}
```

- **Ownership** — GNO recognises its entry by that command (this archive config and profile). It never edits or removes other hooks, other settings, or entries of other profiles. The previous file is kept as `settings.json.bak`, and a settings file that is not valid JSON is left untouched with an error.
- **Absolute and bound** — the command pins this GNO runtime, its data and cache directories, and the archive config/index pair, so neither PATH nor the session's working directory can redirect it. Re-run `enable` after moving GNO or changing `GNO_DATA_DIR`; it repairs the entry in place. Enabling with a different `--settings` file first removes the entry from the previous file.
- **Tiny foreground** — the hook reads the event, rechecks that the profile's hook is still enabled, writes the pending marker durably (fsync), and exits. It does not parse sessions, import, embed, or use the network. It waits at most 1 second for the marker lock and takes about 70 ms end to end; Claude Code's timeout of 5 seconds is a backstop.
- **Honest outcome** — the hook prints one content-free line: `accepted (profile claude pending, not yet archived; …)`, `skipped (…)`, or `not accepted (…); nothing was archived` with a non-zero exit. It never reports archival. An unknown profile is reported as `skipped (… unknown_profile)`, a profile whose hook is off as `skipped (… hook_disabled)`.
- **Kill switch** — `GNO_SESSIONS_HOOKS=off` (or `0`) in Claude Code's environment makes every installed hook return immediately.
- **The payload is only a trigger** — the session ID, transcript path, and working directory Claude Code sends are not used to choose sources, collections, or privacy routing; the import covers the profile's registered sources.

Verified against Claude Code 2.1.280: SessionEnd fires for non-interactive `claude -p` runs with the fields `session_id`, `transcript_path`, `cwd`, `hook_event_name`, `reason`, and `prompt_id`, and the hook finishes inside Claude Code's default 1.5-second SessionEnd budget. Whether SessionEnd fires when an interactive session ends with `/exit` was not verified; the schedule catches up either way.

### Daemon schedule

- **Daemon only** — schedules tick only inside `gno daemon` running on the archive's config and index. Enabling a schedule does not install or start a service; start the daemon yourself (for example `daemon --detach`, or under your own login item or service manager).
- **Elapsed cadence** — `--cadence` uses the daemon cadence grammar `<n>s|m|h|d`, from `1m` to `30d`. It is elapsed time, not a wall-clock time of day, so daylight-saving changes do not move it. There is no cron syntax.
- **First run** — one cadence after `enable`, like the daemon findings pass.
- **Missed runs coalesce** — after sleep, a stopped daemon, or a restart, all missed intervals become one run. A clock moved backwards never pushes the next run more than one cadence away.
- **Times** — run state stores UTC instants; `sessions status` shows them in your local timezone and names it.

Only one resident process (`gno serve` or `gno daemon`) may use a GNO data directory. If your curated `gno serve` or `gno daemon` already runs, the archive daemon cannot start on the same data directory (`Resident runtime already active`). Give the archive its own data directory and use it for every archive command, including `enable`:

```
export GNO_DATA_DIR=~/gno-sessions/data
gno --config ~/gno-sessions/archive.yml --index sessions update   # rebuilds the disposable index there
gno --config ~/gno-sessions/archive.yml --index sessions sessions automation enable claude --hook claude-code
gno --config ~/gno-sessions/archive.yml --index sessions daemon --detach
```

### Run now

`gno sessions automation run <profile>` runs one profile through the importer immediately and records the outcome in automation status, whether or not any trigger is enabled. An explicit run resets any backoff and retry budget. It returns `not_started` (`busy`) only while another automation run of the same profile is in progress; a manual import holding the archive shows as a failed run with reason `busy`, retried with backoff.

### Status

`gno sessions status` (and every status surface) reports, per profile:

| Field           | Meaning                                                                       |
| --------------- | ----------------------------------------------------------------------------- |
| `state`         | `off`, `idle`, `pending`, `running`, `retrying`, `partial`, or `failed`       |
| `hook`          | Harness, enabled, and whether the owned entry is present in its settings file |
| `schedule`      | Enabled, cadence, and `nextDueAt` (null unless a daemon is running)           |
| `pending`       | Since when, and which triggers admitted it                                    |
| `running`       | A live run and when it started                                                |
| `lastRun`       | Triggers, times, outcome, reason code, thread and unit counts                 |
| `lastSuccessAt` | Last run that completed fully (`complete` or `up_to_date`)                    |
| `retryAt`       | When a failed run is retried                                                  |
| `recovery`      | The next action to take, when one is needed                                   |

A schedule whose cadence was hand-edited to an invalid value (for example `banana`, or `5s` below the minimum) is reported with `enabled: false`, a warning, and a recovery action (in status and in `preview`), and never runs until you fix it with `sessions automation set … --cadence`.

The daemon writes its heartbeat every 30 seconds on its own timer, so it stays `running` during its initial sync and during long imports. The `daemon` block reports `running` only with a fresh heartbeat from a live daemon on this archive, `stale` for a live process that stopped ticking, and `not_running` otherwise; then the schedule shows `not running: no daemon` instead of a due time.

A profile in `failed` state keeps its admitted work in the JSON `pending` field, but the text status and the Web UI show the failure and its recovery action instead of a pending line, because that work only runs after you fix the cause.

A run's outcome is `complete`, `up_to_date` (a verified no-op: every source was already current), `partial` (incomplete or deferred units, or a failed index sync; the next run retries them), or `failed`. Missing or unreadable sources, stale heartbeats, and failed runs never appear as a successful completion. Status, logs, and receipts carry counts, IDs, and reason codes only: no session content and no host paths.

### Failures, retries, and recovery

| Reason code             | Cause                                                                                       | What happens                                                                                                                                                              |
| ----------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `busy`                  | Another import or index writer holds the archive or index (including a locked SQLite index) | A running daemon retries it with backoff (1m, 2m, 4m, … up to 30m), `retries` times; without a daemon, run it again yourself. `automation run` exits 4 (`SESSIONS_BUSY`). |
| `runtime_error`         | Filesystem or index failure                                                                 | Retried with backoff                                                                                                                                                      |
| `interrupted`           | The process running it died                                                                 | Pending work kept; the next daemon tick reruns it                                                                                                                         |
| `source_revoked`        | A profile source is no longer registered                                                    | No automatic retry; fix the profile or re-register the source                                                                                                             |
| `source_unavailable`    | A source or the archive directory is missing or unreadable                                  | No automatic retry; fix it, then `automation run`                                                                                                                         |
| `invalid_configuration` | Collections, binding, or source settings no longer match                                    | No automatic retry; correct the config                                                                                                                                    |
| `import_failed`         | Every processed unit failed                                                                 | No automatic retry; check the receipt of `automation run`                                                                                                                 |

Schedule ticks and hook events never reset a pending backoff, and never clear a failure marked "no automatic retry". Once the automatic retries of a `busy` or `runtime_error` failure are used up, the next scheduled tick or hook event starts a fresh retry budget. A failure that needs correction stays blocked until you act: `sessions automation run`, `set`, or `enable` clear it.

A trigger that could not be admitted (lock not acquired in time, disk full, archive directory removed) is reported as not accepted and leaves no pending marker. An accepted trigger is never dropped silently: it stays pending until a run consumes it, and a trigger that arrives while a run is in progress starts another run afterwards. Imports are idempotent, so a rerun after a crash creates no duplicate turns and never changes provenance.

### Pause, disable, and remove

```
$A sessions automation disable claude            # pause everything
$A sessions automation disable claude --hook     # only the hook
$A sessions automation disable claude --schedule # only the schedule
$A sessions automation remove claude             # uninstall and delete the profile
```

- `disable` switches the trigger off first, then removes the owned hook entry and clears pending work admitted by that trigger. A run already in progress finishes its bounded batch; nothing new starts.
- `remove` uninstalls the owned hook entry and deletes the profile and its run state. While a run is in progress it refuses with SESSIONS\_BUSY and changes nothing, so the run stays visible: pause first, then remove once status shows the run finished. It fails, and keeps the profile, if the Claude Code settings file cannot be read, so the entry never outlives the profile that can remove it.
- Archived conversations are always kept. Remove them with [prune](#backup-and-retention) or by deleting archive files.

Automation creates no tasks, reminders, notifications, or OS services, promotes nothing to remembered facts, and makes no network calls.

## Privacy and redaction limits

Everything stays on your machine. New writes happen only in the archive root you chose (archive files, checkpoint state, and automation run state in `.gno-sessions/automation.json`), the archive config (including its sessions.automation block), and the named archive index. When you enable the Claude Code hook, GNO also edits one owned entry in the Claude Code settings file and keeps the previous file as settings.json.bak. Automation makes no network calls. Collection egress policy, local\_only by default, applies to archive collections like any other.

Redaction runs before anything is persisted: archive text, titles, labels, metadata, locators, receipts, and diagnostics. It replaces common credential shapes with markers such as `[REDACTED:api-key]`: AWS key IDs, GitHub tokens, sk- style API keys, Stripe and Slack tokens, Google API keys, JWTs, bearer tokens, credentials in URLs, password= or api\_key: style assignments, and private key blocks. A value detected once is redacted everywhere in that thread. Add your own exact strings (at least 4 characters) to the archive config; a change takes effect on the next import, which re-renders units whose source still exists and rescans archives whose source is gone in place:

```
sessions:
  redaction:
    literals:
      - internal.example.corp
      - my-project-codename
```

**Redaction is best effort.** It does not promise to catch every secret or personal detail in free text. Treat the archive as sensitive, keep it local\_only unless you have reviewed it, and review before mixing it into a curated index.

Remote REST and MCP errors never include host paths: a filesystem or index failure surfaces as SESSIONS\_RUNTIME\_FAILURE with a fixed message.

## Web UI, MCP, REST, and SDK

**Web UI** — start a server on the archive pair and open `/sessions` to manage sources and the archive destination, preview a dry run (redactions, destination collections), import manually with partial-outcome details, and search sessions with harness, project, and role filters and explicit Human and Assistant badges. Only one server or daemon may own a GNO data directory, so while your curated server runs, give the archive server its own GNO\_DATA\_DIR and a free port. Normal curated servers do not attach the archive.

The Automation panel shows each profile off by default, separate hook and schedule switches (each confirmed after a preview of sources, destinations, the settings file, and the daemon prerequisite), daemon availability, pending, running, partial, and failed state, last success, Run now, Pause, and Remove. Run now works for any allowed client; profile changes, Pause, and Remove are same-host only. While a run is in progress the page refreshes its status every 2 seconds, so the profile shows running, and after each action keyboard focus moves to the resulting status, switch, or error. Status requests to a gno serve that is itself running an import can take several seconds. The server itself never drains pending work or runs schedules.

```
gno --config ~/gno-sessions/archive.yml --index sessions serve --port 3100
```

**MCP** — a server started on the archive pair offers `gno_sessions_status` (read, including automation status), `gno_sessions_import`, and `gno_sessions_automation_run` (write tools, requiring `--enable-write`; without it they are not advertised, like the other write tools) in the full profile only, not core. Import takes `sourceId`, optional `dryRun`, and `limit`; any other key, such as paths, is rejected. The run tool takes a configured `profileId` only. There is no MCP discovery tool, so remote clients never learn host directories, and hooks and schedules cannot be enabled over MCP.

**REST** — on a server started with the archive pair: `GET /api/sessions/status` and `POST /api/sessions/import` with `{sourceId, dryRun?, limit?}`; paths are rejected. The import runs in a separate process, so the server keeps answering health, status, and Web UI requests while it runs. `POST /api/sessions/automation/run` with `{profileId}` is open to any allowed client too, and runs its imports the same way. `GET /api/sessions/discover`, `POST /api/sessions/sources`, `DELETE /api/sessions/sources/:id`, `POST /api/sessions/init`, and every automation profile change (`PUT` and `DELETE /api/sessions/automation/:id`, `GET …/preview`, `POST …/enable`, `POST …/disable`) answer only a same-host client (a local process; cross-origin browser pages are refused) and return 403 otherwise. Errors carry the specific code in `details.sessionsCode`; see [Error codes](#error-codes).

**SDK** — open a client on the archive pair:

```
import { createGnoClient } from "@gmickel/gno"

const client = await createGnoClient({
  configPath: "/absolute/path/to/archive.yml",
  indexName: "sessions",
})
const preview = await client.importSessions({ sourceId: "codex", dryRun: true })
const receipt = await client.importSessions({ sourceId: "codex" })
const status = await client.sessionsStatus()
// Run a configured automation profile now (hooks and schedules are enabled
// from the CLI or a same-host client only).
const run = await client.runSessionsAutomation({ profileId: "claude" })
await client.close()
```

`client.discoverSessions()` previews local roots, and `importSessions` also accepts `{ paths, collection, format? }` for explicit files. `status.automation` carries daemon state and per-profile hook and schedule status; the SDK can run a configured profile but cannot switch hooks or schedules on. Errors are `GnoSdkError` with code VALIDATION or RUNTIME and the sessions code in `details.code`.

## Troubleshooting

- **SESSIONS\_BINDING\_MISMATCH** — the config and index do not belong together. Pass the archive's --config with its --index, and use your normal config (no --config) for curated work. From sessions init, it means the index already holds other collections or belongs to another archive: pick a new index name. See [Archive versus curated vault](#archive-vs-vault).
- **SESSIONS\_NOT\_CONFIGURED** — the config has no sessions block, usually because a curated config or server was used. Run the command, or start the server, on the archive pair.
- **sessions init refuses the archive root** — the root lies inside GNO's own directories or inside a folder your default config already indexes. Choose a folder outside both.
- **SESSIONS\_BUSY (exit 4, HTTP 409)** — another import holds the archive lock. Wait for it to finish and rerun.
- **partial with truncated\_tail** — the session was still being written. The readable part is archived and the next import retries the unit.
- **format\_drift or unknown record kinds** — the harness wrote records this parser does not know. Readable threads are archived and the unit is retried; check the supported versions above.
- **snapshot\_read\_failed** — a SQLite store could not be read in one read-only snapshot, often during a harness migration or lock. Rerun when the agent is idle.
- **mixed\_domain** — the thread was quarantined because its working directories map to different collections. Add a --project mapping; the next import archives it.
- **Lexical sync failed** — archive files were written but the index sync did not finish. Units are not recorded as complete, so rerun the import.
- **SESSIONS\_SOURCE\_UNAVAILABLE** — the registered path is missing or unreadable (exit 2). The archive is kept; check the source on the host with sessions status, or remove it.
- **Hook says accepted but nothing was archived** — expected until a run: the hook only marks the profile pending. Start gno daemon on the archive's config and index, or run `gno sessions automation run <profile>`.
- **Status says not running: no daemon** — no daemon on this archive has written a heartbeat recently. Start one; if it fails with Resident runtime already active, another gno serve or gno daemon owns the data directory: give the archive its own [data directory](#daemon-schedule).
- **Hook entry missing or installed: false** — someone edited the Claude Code settings file. Run `gno sessions automation enable <profile> --hook claude-code` again; it repairs only its own entry.
- **SESSIONS\_UNSUPPORTED\_INTEGRATION** — only the Claude Code SessionEnd hook is supported. Use a daemon schedule for other harnesses.
- **Sessions missing from search** — expected on the curated index. Search with the archive's --config and --index, run embed on the pair before query or vsearch, or set up [mixed retrieval](#mixed-retrieval).

## Error codes

| Code                                                                            | Meaning                                                            | CLI exit | HTTP | REST / SDK kind |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -------- | ---- | --------------- |
| `SESSIONS_NOT_CONFIGURED`                                                       | The config has no sessions block                                   | 1        | 400  | VALIDATION      |
| `SESSIONS_BINDING_MISMATCH`                                                     | Archive config and index used apart                                | 1        | 400  | VALIDATION      |
| `SESSIONS_SELECTION_REQUIRED`, `SESSIONS_DESTINATION_REQUIRED`                  | No source or paths selected; path import without a collection      | 1        | 400  | VALIDATION      |
| `SESSIONS_UNKNOWN_SOURCE`, `SESSIONS_UNKNOWN_COLLECTION`                        | Unregistered source ID or archive collection                       | 1        | 400  | VALIDATION      |
| `SESSIONS_UNKNOWN_PROFILE`                                                      | Unknown automation profile ID                                      | 1        | 400  | VALIDATION      |
| `SESSIONS_UNSUPPORTED_INTEGRATION`                                              | Hook for a harness without a verified integration                  | 1        | 400  | VALIDATION      |
| `SESSIONS_UNSAFE_PATH`, `SESSIONS_UNSUPPORTED_FORMAT`, `SESSIONS_INVALID_INPUT` | Unsafe path, unknown harness, or malformed input                   | 1        | 400  | VALIDATION      |
| `SESSIONS_SOURCE_UNAVAILABLE`                                                   | Source path missing or unreadable (no host path when remote)       | 2        | 500  | RUNTIME         |
| `SESSIONS_BUSY`                                                                 | Another import holds the archive lock                              | 4        | 409  | BUSY / RUNTIME  |
| `SESSIONS_RUNTIME_FAILURE`                                                      | Filesystem or index failure on REST/MCP (fixed, path-free message) | —        | 500  | RUNTIME         |

An import whose status is `failed` exits 2, except a selection with no supported session format, which exits 1 with SESSIONS\_UNSUPPORTED\_FORMAT. An automation run whose outcome is failed, and a hook that was not accepted, also exit 2. The CLI JSON envelope and REST errors carry the code in `details.sessionsCode`; MCP returns it in `structuredContent.error`; the SDK puts it in `details.code`. A same-host refusal on REST is 403 FORBIDDEN without a sessions code.
