apus sessions

List stored conversation sessions.

Session lists are not channel/provider liveness checks. They show persisted conversation rows from session stores. A quiet Discord, Slack, Telegram, or other channel can reconnect successfully without creating a new session row until a message is processed. Use apus channels status --probe, apus status --deep, or apus health --verbose when you need live channel connectivity.

apus sessions
apus sessions --agent work
apus sessions --all-agents
apus sessions --active 120
apus sessions --limit 25
apus sessions --store ./tmp/sessions.json
apus sessions --json

apus sessions list is an explicit spelling of the default listing action and accepts the same flags.

Human-readable lists and cleanup previews use terminal-width tables. Long model names and flags wrap without being truncated, and Unicode keys stay aligned. Long keys show their beginning and end; use apus sessions --json for complete session keys.

Token counts below 1,000 appear as whole numbers; larger counts use compact k or m labels. JSON output retains exact numeric counts.

The context denominator follows the selected model and runtime, using the local Gateway's published catalog, saved metadata, configured limits, or a matching verified run budget. Reading a session list does not start model discovery. Unknown capacity is ? in the table and null in JSON, with no percentage. If Gateway inspection or its catalog read fails, the command warns and still lists stored sessions using saved or configured capacity.

Flags:

Flag Description
--agent <id> One configured agent store (required for multiple explicit agents).
--all-agents Aggregate all configured agent stores.
--store <path> Legacy store selector path (cannot combine with --all-agents).
--active <minutes> Only show sessions updated within the past N minutes.
--limit <n|all> Max rows to output (default 100; all restores full output).
--json Machine-readable output.
--verbose Verbose logging.

--store accepts the documented legacy selector form, including sessions.json and suffixless custom selectors. Apus resolves that selector to its physical SQLite target, verifies the target exists and is usable, and reports the physical path it actually read. Combine it with --agent <id> when you must select the configured agent that owns the store.

--agent and --store require non-blank values. Selection errors exit non-zero and use the standard CLI JSON failure envelope when --json is set.

apus sessions and the Gateway sessions.list RPC are bounded by default so large long-lived stores cannot monopolize the CLI process or Gateway event loop. The CLI returns the newest 100 sessions by default; pass --limit <n> for a smaller/larger window or --limit all when you intentionally need the full store. JSON responses include totalCount, limitApplied, and hasMore when callers need to show that more rows exist.

JSON session rows include color when a session color is set (for example, "color": "blue"). Uncolored sessions and sessions whose color was cleared omit the field.

RPC clients can pass configuredAgentsOnly: true to keep the broad combined discovery source but return only rows for agents currently present in config. Control UI uses that mode by default so deleted or disk-only agent stores do not reappear in the Sessions view.

--all-agents reads configured agent stores. Gateway and ACP session discovery are broader: they also include SQLite stores resolved from configured agent roots or a templated session.store root. Legacy selector paths must resolve inside the agent root; symlinks and out-of-root paths are skipped.

apus sessions --all-agents --json:

{
  "path": null,
  "stores": [
    { "agentId": "main", "path": "/home/user/.apus/agents/main/agent/apus-agent.sqlite" },
    { "agentId": "work", "path": "/home/user/.apus/agents/work/agent/apus-agent.sqlite" }
  ],
  "allAgents": true,
  "count": 2,
  "totalCount": 2,
  "limitApplied": 100,
  "hasMore": false,
  "activeMinutes": null,
  "sessions": [
    { "agentId": "main", "key": "agent:main:main", "model": "openai/gpt-5.6-sol" },
    { "agentId": "work", "key": "agent:work:main", "model": "anthropic/claude-sonnet-4-6" }
  ]
}

Import transcripts

Preserve native session catalog transcripts in the running Gateway's durable session store:

apus sessions import claude <thread-id>
apus sessions import codex <thread-id> --source-home <home-id>
apus sessions import claude <thread-id> --host <host-id> --agent work
apus sessions import --all --json
apus sessions import --all --catalog claude --limit 20 --dry-run
apus sessions import --all --host <host-id>

The catalog IDs for Claude Code and Codex are claude and codex. Other catalogs with transcript reads use the same command. For one transcript without --host, the CLI discovers Gateway hosts in that catalog: it prefers gateway:local when present, otherwise uses the only Gateway host. If no unambiguous Gateway host exists, pass --host <host-id> explicitly. Use the catalog row's host and sourceHomeId to select another machine or native tool home. --agent chooses the destination agent. Imports support Gateway-local, paired-node, macOS app, and Linux app sources that the caller may read.

Import creates an ordinary Apus session containing untrusted reference material. Imported copies start as drafts, visible only to their creator and Gateway admins. Publish the copy through the session sharing controls to share it; re-importing preserves its current visibility. When drafts are disabled, imported copies follow the Gateway's default visibility. It preserves the catalog's projected messages independently of native tool cleanup; it does not resume the native session or bind the copy to its model or machine. Repeating the same import reuses the copy and appends only new items. Each import reads up to 50,000 items and 64 MiB, keeping the newest history when the bound is reached. Existing per-item text limits still apply. An incomplete result warns that older history was omitted. See Session catalogs.

--all pages through visible catalog rows and imports them sequentially. New copies use the catalog row name as their title; re-importing preserves an existing copy's title. Names are trimmed and limited to 500 UTF-16 code units. --catalog and --host filter sources; --limit <n> caps the total number of sessions attempted. --dry-run lists selected sources without importing them and does not predict item counts or run write authorization checks. A failure for one source does not stop the remaining sources. The command exits non-zero if any import or catalog page fails.

Connection overrides are --url, --token, --password, and --timeout <ms> (default five minutes per request). Human output reports imported, updated, or unchanged sessions with item counts and a final summary. --json emits one envelope with ok, operation: "import", dryRun, results, and summary. Successful results include the source locator, status, sessionKey, importedItems, totalItems, complete, and created; dry-run results have status: "would_import". Failed results include status: "failed" and error.

Archive sessions

Archive one or more sessions through the running Gateway:

apus sessions archive "agent:main:scratch-1"
apus sessions archive "agent:main:scratch-1" "agent:main:scratch-2"
apus sessions archive "agent:work:scratch-1" --agent work
apus sessions archive "agent:main:scratch-1" --dry-run
apus sessions archive "agent:main:scratch-1" --json

Archive uses the same sessions.patch lifecycle operation as the Control UI. It keeps the transcript, marks the session archived, and removes the session from the default active list. For a cloud-worker session with an active placement, the Gateway first stops the worker, reconciles its workspace, and reclaims the environment. If the placement is still transitioning or failed without proof that its environment is gone, the session remains unarchived; wait for the placement to settle, then retry. Agent main sessions remain protected. Already archived sessions are successful no-ops. Use --dry-run to validate every key and preview the result without changing session state.

Archive reasons are assigned automatically and displayed as human-readable text in the Control UI. Explicit archive commands record manual; maintenance-owned archives record their owning trigger. Missing reasons remain protected as legacy state. Age-retention archives also remain protected under disk pressure. Only sessions explicitly archived by maxEntries are eligible for automatic deletion after cheaper cleanup tiers are exhausted.

Delete sessions

Delete one or more sessions through the running Gateway:

apus sessions delete "agent:main:scratch-1"
apus sessions delete "agent:main:scratch-1" "agent:main:scratch-2" --yes
apus sessions delete "agent:work:scratch-1" --agent work --yes
apus sessions delete "agent:main:scratch-1" --dry-run
apus sessions delete "agent:main:scratch-1" --yes --json

Repeated keys are processed once, in first-occurrence order, after surrounding whitespace is removed. This also applies to sessions archive.

Delete is destructive. In an interactive terminal it asks once before deleting the valid keys. Non-interactive and --json deletion requires --yes. Use --dry-run first when scripting a bulk cleanup.

Delete uses the same sessions.delete lifecycle operation as the Control UI, with transcript cleanup enabled. The Gateway removes the live session row, transcript generations, session-owned runtime state, bindings, boards, and other lifecycle artifacts. For ordinary sessions it retains the transcript as a verified .jsonl.deleted.<timestamp> archive; incognito transcripts are removed without an archive. Retained deleted-session archives can remain eligible for memory search. To remove indexed memories, run apus memory forget --agent <agent-id> --session <id-or-key> on the Gateway host or container using that Gateway's state and configuration. Select the agent that owned the deleted session, including for global keys. Memory cleanup runs locally; deleting through --url or a configured remote Gateway does not forward the cleanup command to that Gateway. See Memory forget for preview and deletion details.

If a managed worktree cannot be removed safely, the command reports the preserved branch and path for manual cleanup.

Both lifecycle commands:

Lifecycle commands look up each requested key directly, including cron run sessions hidden from the Gateway's general session list. Dry-run uses those Gateway facts to report protected agent-main sessions as failed, even when the CLI uses different local session settings. Already archived sessions remain successful archive no-ops. Dry-run does not execute all Gateway lifecycle checks: global previews can still show an archive or delete action that the Gateway refuses. Explicitly selected non-default global deletion remains supported. The real archive or delete request is authoritative.

Agent-qualified aliases keep their selected owner through lookup and mutation. For example, sessions delete agent:work:main --yes still targets work when the Gateway describes that session using the canonical key global.

Example mixed-result JSON:

{
  "ok": false,
  "operation": "archive",
  "dryRun": false,
  "results": [
    { "key": "agent:main:scratch-1", "ok": true, "status": "archived" },
    {
      "key": "agent:main:missing",
      "ok": false,
      "status": "not_found",
      "error": "Session not found. Run apus sessions list --json to choose a valid key."
    }
  ]
}

Tail trajectory progress

apus sessions tail
apus sessions tail --follow
apus sessions tail --session-key "agent:main:telegram:direct:123" --tail 25
apus sessions --agent work tail --follow
apus sessions --all-agents tail --follow

apus sessions tail renders recent runtime trajectory events as compact progress lines. Without --session-key, it asks the configured Gateway for running sessions and tails matching local sessions first, then falls back to the most recently active stored session. If the Gateway is unreachable, it prints Gateway unreachable: showing the most recently active session. An explicit --store skips the Gateway lookup, orders by activity, and prints explicit store: ordered by activity. An explicit --session-key selects only that local session without a Gateway lookup. --tail <count> controls how many existing events print before follow mode; default 80, and 0 starts at the current end. --follow keeps watching the selected SQLite-backed sessions. Session keys use fixed-width terminal columns, with long keys truncated at whole grapheme boundaries so CJK characters, combining accents, and joined emoji keep progress lines aligned.

A fully qualified --session-key selects its agent only when --agent, --store, and --all-agents are absent. An explicitly empty or whitespace-only --agent is rejected instead of selecting an inferred agent.

An explicit --session-key that matches no stored session exits non-zero with guidance for listing valid keys, and an empty or whitespace-only --session-key is rejected. Without a key, an empty selection prints No sessions found. and exits successfully, including with --follow.

The progress view is intentionally conservative: prompt text, tool arguments, and tool result bodies are not printed. Tool calls show the tool name with {...redacted...}; tool results show status such as ok, error, or done; model completion lines show provider/model and terminal status. Provider failures and turns without delivery show error; cancellation shows aborted, timeouts show timeout, and successful completions (including delivered partial replies) show done.

Export a trajectory bundle

apus sessions export-trajectory --session-key "agent:main:telegram:direct:123" --workspace .
apus sessions export-trajectory --session-key "agent:main:telegram:direct:123" --output bug-123 --json

This is the command path used by the /export-trajectory slash command after the owner approves the exec request. The output directory is always resolved inside .apus/trajectory-exports/ under the selected workspace. The file list in text and JSON output reports only artifacts written to the bundle.

For stored keys without an agent prefix, such as global, export uses the configured default or sole agent. Pass --agent <id> when multiple agents are configured without a default.

Cleanup maintenance

Run maintenance now instead of waiting for the next write cycle. Without --store, cleanup delegates to the reachable Gateway. Destructive local cleanup requires exclusive offline maintenance ownership; if a Gateway or another live owner holds the state, the command names that owner and refuses. Run cleanup without --store to use the Gateway, or stop the Gateway and other owners first. Dry-run previews remain available while the Gateway is running:

apus sessions cleanup --dry-run
apus sessions cleanup --agent work --dry-run
apus sessions cleanup --all-agents --dry-run
apus sessions cleanup --enforce
apus sessions cleanup --enforce --active-key "agent:main:telegram:direct:123"
apus sessions cleanup --dry-run --fix-dm-scope
apus sessions cleanup --json

apus sessions cleanup uses session.maintenance settings from config (Configuration reference):

Optional cold transcript extraction has its own background worker and Run now action in Settings → Agent Defaults → Session. It uses session.maintenance.coldStorage.afterDays and preserves inactive transcripts in authoritative compressed files. The cleanup command's reset/deletion archive retention does not delete those cold files.

Flags:

Flag Description
--dry-run Preview how many entries would be pruned/capped without writing. In text mode, prints a per-session action table (Action, Key, Age, Model, Flags) plus a summary grouped by session label.
--enforce Apply maintenance even when session.maintenance.mode is warn.
--fix-missing Remove legacy entries whose archived transcript artifacts are missing or header-only/empty, even if they would not normally age/count out yet.
--fix-dm-scope When session.dmScope is main, retire stale peer-keyed direct-DM rows left behind by earlier per-peer, per-channel-peer, or per-account-channel-peer routing. Use --dry-run first; applying removes those rows from SQLite and preserves their legacy transcript artifacts as deleted archives.
--active-key <key> Protect a specific active key from automatic maintenance. It still counts toward maxEntries. Durable external conversation pointers, such as group sessions and thread-scoped chat sessions, are also kept by age/count/disk-budget maintenance.
--agent <id> Run cleanup for one configured agent store.
--all-agents Run cleanup for all configured agent stores.
--store <path> Run locally against a specific SQLite database or legacy store selector path.
--json Print a JSON summary. With --all-agents, output includes one summary per store.

When a Gateway is reachable, non-dry-run cleanup for configured agent stores is sent through the Gateway so it shares the same session-store writer as runtime traffic. Use --store <path> for explicit offline repair of a SQLite database or legacy store selector.

Automatic offline fallback applies only when the configured local Gateway cannot be reached before connecting. A failed remote Gateway connection or APUS_GATEWAY_URL override exits with an error and leaves local stores alone, including when the selected URL uses a loopback SSH tunnel. Restore the remote connection or use --store <path> to explicitly select a local store.

When the selected store's parent directory is named agent, transcript artifacts live in the sibling sessions directory. This also applies to custom paths: /backup/agent/sessions.json selects /backup/agent/apus-agent.sqlite, whose archives live in /backup/sessions. Cleanup measures and prunes that same artifact directory whether you select the legacy path or the SQLite file.

Offline cleanup loads trusted, permitted harness plugins so their session-owned resources are reclaimed with the deleted rows, even if the agent now uses a different model. Explicitly disabled or untrusted plugins are not run. If their resources may remain, cleanup prints a warning on stderr without changing the JSON result. Dry runs do not load harness plugins.

Applied artifact cleanup counts only successful file removals. If a file cannot be deleted, it contributes no freed bytes and remains part of disk usage. Unreferenced artifact cleanup and legacy disk-budget enforcement continue with other eligible files. Canonical SQLite archive pruning stops after a deletion error to retain its database recovery copy. If usage stays above the target, check filesystem permissions and retry after resolving the deletion failure.

apus sessions cleanup --all-agents --dry-run --json:

{
  "allAgents": true,
  "mode": "warn",
  "dryRun": true,
  "stores": [
    {
      "agentId": "main",
      "storePath": "/home/user/.apus/agents/main/agent/apus-agent.sqlite",
      "beforeCount": 120,
      "afterCount": 80,
      "missing": 0,
      "dmScopeRetired": 0,
      "pruned": 40,
      "capped": 0
    },
    {
      "agentId": "work",
      "storePath": "/home/user/.apus/agents/work/agent/apus-agent.sqlite",
      "beforeCount": 18,
      "afterCount": 18,
      "missing": 0,
      "dmScopeRetired": 0,
      "pruned": 0,
      "capped": 0
    }
  ]
}

Test cleanup on a copy

Use a separate state directory to measure cleanup before changing a live installation. Create a WAL-aware SQLite backup or a coordinated stopped-state copy; copying only a live database's main .sqlite file can omit committed WAL data. Preserve the agent ID, directory layout, and relevant session artifacts when preparing the copy. Use ordinary copied files, not symlinks or hard links to live state.

Prepare apus.json inside the copy with the maintenance settings you want to test and the copied agent's configuration. Keep its paths and plugin configuration isolated from the live installation. The following example selects the copied main agent database explicitly; adjust the directory and agent ID to match your copy:

(
  export APUS_STATE_DIR="$HOME/apus-state-copy"
  export APUS_CONFIG_PATH="$APUS_STATE_DIR/apus.json"
  copied_db="$APUS_STATE_DIR/agents/main/agent/apus-agent.sqlite"

  apus sessions cleanup --store "$copied_db" --dry-run --json
)

Review the preview, then apply cleanup and compact the copied database:

(
  export APUS_STATE_DIR="$HOME/apus-state-copy"
  export APUS_CONFIG_PATH="$APUS_STATE_DIR/apus.json"
  copied_db="$APUS_STATE_DIR/agents/main/agent/apus-agent.sqlite"

  apus sessions cleanup --store "$copied_db" --enforce --json
  apus doctor --session-sqlite compact --session-sqlite-agent main --session-sqlite-store "$copied_db" --json
)

Explicit --store cleanup stays local. Destructive cleanup and Doctor require their targets inside APUS_STATE_DIR and exclusive ownership of that state directory; the live Gateway can continue using its separate original state. Set --session-sqlite-agent to the copied database's owner; an explicit Doctor store selector otherwise defaults to main.

archive-age, archive-dashboard, and archive-cap change session metadata while retaining transcript rows. Disk-budget cleanup can replace eligible history with compressed archives, whose canonical payload remains in SQLite. Doctor's compact step then reclaims free database pages with VACUUM and reports before/after database and WAL sizes. It does not choose more history to delete. Compare physical sizes and retained history, not only session counts; protected data can keep usage above the configured budget. See store maintenance and retention for the archive ownership and protection rules.

Compact a session

Reclaim context budget for a wedged or oversized session. apus sessions compact <key> is the first-class wrapper around the sessions.compact Gateway RPC and requires a running Gateway.

apus sessions compact "agent:main:main"
apus sessions compact "agent:main:main" --max-lines 200
apus sessions compact "agent:work:main" --agent work --json

The command exits non-zero when the Gateway reports a failed compaction or is unreachable, so crons and scripts never mistake a silent no-op for success. A missing session is an error in both modes. An existing session with no history or nothing to truncate remains an explicit no-op.

apus agent --message '/compact ...' is not a compaction path. Slash commands from the CLI are rejected by the authorized-sender check; that invocation exits non-zero with guidance pointing here instead of silently no-opping.

sessions.compact RPC

apus gateway call sessions.compact --params '<json>' accepts:

Field Type Required Description
key string yes Session key to compact (for example agent:main:main).
agentId string no Agent id that owns the session (for global keys).
maxLines integer ≥ 1 no Truncate to the last N lines instead of LLM summarization.

Example LLM-summarize response:

{
  "ok": true,
  "key": "agent:main:main",
  "compacted": true,
  "result": { "tokensBefore": 243868, "tokensAfter": 34941 }
}

Example truncate response (--max-lines 200):

{
  "ok": true,
  "key": "agent:main:main",
  "compacted": true,
  "kept": 200
}

Related