---
name: index-gateway-memory
description: >
  Use the Sparse Agent Memory HTTP API to store, query, locate, validate
  and export a Computer Index in a Chat user's own projects. Authenticate with
  an Auto-Coder Chat API key. Use when a third party or their agent needs cloud
  memory, or asks about /api/projects or the Sparse Agent Memory API.
  This guide is for callers, not gateway deployment or operations.
---

# Memory with Sparse Agent Memory

Start with the [cloud memory guide and examples](https://sparse-agent.com/cases/agent-memory.en.html). This document covers program integration.

This guide is public at `/docs/memory?lang=en`. No sign-in is required.
The Markdown edition is available at `/docs/memory.md?lang=en`. Use the
language selector to switch between Chinese and English.

This guide is for **callers**: an Auto-Coder Chat account or a program acting
for that account. Memory is isolated by Chat user. All API keys belonging to
one account see the same projects. Other accounts receive “project not found”
even if they guess a project name.

The service does not accept a username. Callers choose project names; the
server derives the namespace from the verified Chat user. Do not call
`/v1/...`; those are not this gateway's routes.

Below, `$GATEWAY` is the public origin, such as `https://example.com`, without
a path. `$API_KEY` is an `ak_…` key created in Chat account settings. Send
the key only in the Authorization header. Keep it out of URLs, logs and repositories.

## Choose an authentication method

The administrator controls concurrent index tasks: 20 across the service by
default. Additional tasks queue in arrival order, with at most 256 waiting
requests. Regular HTTP requests wait and then return the usual JSON response.
Set timeouts to cover both waiting and execution. After a timed-out write,
read back the result before retrying.

For progress, send `Accept: application/x-ndjson` on an index request. Each
line is a JSON event. A queued event has `state: "queued"` and a `queue`
object containing `position`, `max_concurrency`, `running` and `queued`.
For example, position 1 means no request is ahead of yours. Execution changes
the state to `running`. The final event is:

```json
{"state":"completed","http_status":200,"result":{}}
```

Use the final `http_status` and `result` to determine success. HTTP 200 on
the stream itself does not mean the task succeeded. Disconnecting removes a
request that has not started; a running task retains its execution slot until
it finishes.

The administrator identified through Chat (William's account is
`allwefantasy@gmail.com`) may read `GET /api/admin/config` and update
`PUT /api/admin/config` with `{"max_concurrency":N}`, where N is 1–1000.
The update takes effect immediately and persists across restarts. Cookie
writes require Origin and CSRF verification; ordinary accounts receive 403.
Administrator privileges are bound to the stable Chat account identity,
not a caller-supplied email.

Programs and scripts use Bearer authentication. They do not need cookies,
`Origin` or `X-CSRF-Token`.

```bash
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json"
```

Browser pages use cookies. `POST /api/auth/login` must include an `Origin`
matching the deployed gateway origin (the production HTTPS domain, not the
caller's host). Send the login response's `csrf_token` as
`X-CSRF-Token` on every subsequent cookie-authenticated write.
`GET /api/auth/me` also returns the current session's CSRF token.
Bearer callers do not use this flow.

When browser SSO is enabled, users can open `GET /api/auth/sso/start` and
follow the redirect. Failures return to `/?sso_error=` with an enumerated
value such as `denied`, `state`, `failed`, `unavailable` or `rate`.
Programs should use Bearer authentication rather than depend on browser SSO.

## Store a memory

A memory is a complete Computer Index bundle in a **project**, rather than
a chat transcript. Query ranking depends on the configured runtime. With
the managed runtime, your own JEV may verify leaf evidence **inside the
bundle**, or your saved native model ranks index metadata and structured IDs
without reading document bodies. Only older deployments without that runtime
use progressive lexical traversal. In every mode, put searchable descriptions
in node `name`, `description`, `tags` and metadata such as `summary`,
`intent_phrases`, `aliases` and `actions`. Store document bodies in bundle
files addressed by node `path`, and read them after retrieving a matching node.

If the user asks to remember something without specifying a project, use
`Default`. The service creates it idempotently on first use and preserves
existing contents on repeated calls. No manual project creation is necessary.
HTTP routes may omit the project segment: `PUT /api/projects/index` and
`POST /api/projects/query` use your own Default project. Explicit
`/api/projects/Default/...` routes also create it when necessary. With the
existing `auto-coder.index.cloud` CLI, pass `--project Default`.
Honor an explicitly chosen project name. Lowercase `default` and
`Default` are different projects; old indexes remain intact.

A project stores one bundle at a time. **PUT replaces the entire bundle.**
Existing files missing from the new bundle are deleted. Before appending a
memory, export the current bundle, merge its documents and index nodes, and
then PUT the merged bundle. Uploading only the new memory would erase the old
contents. Use a different project for an independent bundle.

Project names are case-sensitive and limited to 128 characters. They cannot
have leading or trailing whitespace, `/`, `\`, control characters, or be
`.` or `..`. A Chinese character counts as one character. URL-encode the
project name as a path segment.

To write:

1. Use Default when no project is specified: call a route without the project
   segment, or POST `{}` or an empty body to `/api/projects`. For a named
   project, first GET `/api/projects` and create it with
   `{"project":"<name>"}` if absent. Repeated creation is idempotent;
   `created: false` does not clear the existing index.
2. Prepare `{"files":{...}}` including `root.yaml`. Paths are relative to
   the bundle and use `/`. Absolute paths, `..`, `~`, `$`, empty
   segments and segments starting with `.` are forbidden. Each node's
   `path` must also remain inside the bundle. Do not set
   `metadata.project_path`.
3. PUT `/api/projects/<name>/index`. On success, the old bundle has been
   replaced. The response's `files` is a list of paths, not document bodies.
   Failed validation leaves the old bundle intact; export it to confirm.

A minimal bundle uses `type: index` for another YAML page and
`type: file` for a document:

```json
{
  "files": {
    "root.yaml": "apiVersion: autocoder.dream/v1\nkind: ComputerIndex\nmetadata:\n  name: root\n  description: Personal memory\nnodes:\n  - name: Knowledge\n    description: Reusable instructions\n    path: knowledge.yaml\n    type: index\n    metadata:\n      kind: knowledge\n",
    "knowledge.yaml": "apiVersion: autocoder.dream/v1\nkind: ComputerIndex\nmetadata:\n  name: Knowledge\n  description: Instructions\nnodes:\n  - name: Refresh VPN\n    description: Reconnect the VPN on Windows and verify routing\n    path: notes/vpn.md\n    type: file\n    tags: [vpn, windows]\n    metadata:\n      kind: howto\n      knowledge_id: howto.vpn.refresh\n      summary: Check the default route after reconnecting the VPN\n      intent_phrases: [vpn disconnected, refresh vpn]\n",
    "notes/vpn.md": "# Refresh VPN\n\nReconnect the profile, then verify that the default route is restored.\n"
  }
}
```

Limits: 256 files, 1 MiB of UTF-8 per file, 8 MiB of total contents, and
240 characters per path. The gateway's outer PUT body limit is 12 MiB.
Exceeding an upstream limit is rejected without modifying the old index.

## Recall a memory

POST `/api/projects/<name>/query`. Only these body fields are accepted:

| Field | Meaning |
|---|---|
| `query` | String, at most 4000 characters. An empty query requires at least one filter |
| `limit` | Integer 1–100; default 10 |
| `kind` | String or array of strings matching a node's semantic kind, such as `howto` |
| `since` | `YYYY-MM-DD` or an ISO date-time; retain results on or after that date |
| `status` | String or array of strings matching an entry's status |
| `project` | Substring filter over entry names, descriptions, chain and related metadata. This is not the URL project name |
| `path` | Substring filter over path fields |
| `knowledge_rag` | Boolean. Knowledge RAG is disabled on this service; even `true` leaves `knowledge_rag_result` as `{}` |

`mode`, `model`, `backend`, `runtime`, `index_dir`, `root`, `effort`
and all other unlisted fields receive 400. The request **cannot select a
mode, backend or runtime**. The managed runtime chooses your JEV first, then
your saved model. If both are absent, it returns 409
`configuration required`. Invalid or damaged credentials produce an explicit
error; the service does not silently change identity or engines. Damaged
personal JEV configuration does not fall back to the model. Only deployments
without the managed runtime use progressive lexical traversal.

Read bundle-relative information from `results[]`: `name`, `description`,
`path`, `target_type`, `tags`, `metadata`, `chain`, `score` and
`semantic_kind`. For `target_type: "file"`, read the body from the exported
bundle's `files[path]`. For `target_type: "index"`, open the next YAML page
at that path in the bundle.

The managed runtime adds `query_runtime`: `mode` is `jev` or `model`,
and `source` is `personal_jev` or `model`. These describe the actual query
route and contain no keys, server paths or backend details.

When `root_traversal.strategy` is `ranked_traversal`, read that object's
`terminals`, `status`, `budget` and `stats`. A non-`ok` status means
the traversal did not finish successfully, although verified partial
terminals may be present. Report the precise status and those partial
results. Do not describe it as complete success, switch engines silently,
or repeat traversal within the same request. If `model_ranking` is present,
report its status, candidate coverage and truncation. Only scored entries
are model results; unscored lexical hits are not model output.

`index_dir`, `root_path`, `resolved_path`, `document_path`, `read_next`
and `yaml_files` often contain **absolute server paths**. Do not open them
on the caller's machine or send them back. To get document bodies, GET
`/api/projects/<name>/index` and use its relative `files` map.

Only with `root_traversal.strategy == "progressive_root_traversal"` should
you follow `root_traversal.candidates[].path` when results are empty or
`root_traversal.required` is true. Strategy belongs inside
`root_traversal`, not at the response's top level. Do not apply progressive
traversal rules to a ranked result: `required` only means that engine
returned no terminal, and does not authorize rerunning a failed ranked
engine. Do not execute the engine's `help_command` on the gateway machine.

When you already know a bundle path, use locate rather than search:

```json
{"fs_path":"notes/vpn.md","limit":10}
```

Provide exactly one of `fs_path` or `xpath`. `fs_path` follows the same
bundle-relative path rules as writes. XPath selects index nodes, for example
`//node[contains(@description, 'VPN')]`.

## Maintenance

- Backup: GET `.../index` and save the returned `files`. Files and
  directories starting with a dot are excluded from exports.
- Health: GET `.../status` and read `healthy`, `status`, `validation_ok`
  and `issues`. A `recommended_action` of `build` or `optimize` is a
  suggestion; this API has no build or optimize operation. To edit contents,
  change the bundle and PUT it.
- Validation: POST `.../validate` with `{}` or no body.
  `{"require_knowledge_structure":true}` additionally requires the Knowledge
  root structure. Validation is read-only; failure returns 400 without
  changing the index.
- Export before replacing a bundle. Invalid PUT requests preserve the old one.

## Endpoints

All routes below except health, public pages, login and SSO require
authentication. Bearer writes need only the key. Cookie writes also need
`Origin` and `X-CSRF-Token`. Login itself needs Origin but no CSRF token,
because no session exists yet.

| Method | Path | Purpose |
|---|---|---|
| GET | `/healthz` | Public. `{"ok":true,"checks":{"db":"ok","upstream":"ok"}}`; HTTP 503 on failure |
| GET | `/` | Human workspace, with a link to this guide |
| GET | `/docs` | Public redirect to `/docs/memory`; supports `?lang=en` |
| GET | `/docs/memory` | Public HTML guide; `?lang=en` selects English, `?lang=zh` Chinese |
| GET | `/docs/memory.md` | Public Markdown guide, with the same language parameter |
| POST | `/api/auth/login` | `{"api_key":"ak_…"}` → cookie, `csrf_token` and `account.email` |
| GET | `/api/auth/sso/start` | When enabled, redirects to Chat |
| GET | `/api/auth/sso/callback` | Successful SSO returns 303 to `/`; failure to `/?sso_error=<enum>` |
| POST | `/api/auth/logout` | Revokes only this gateway session |
| GET | `/api/auth/me` | Authenticated status and email; cookie sessions include `csrf_token`. Anonymous responses are also 200 with `authenticated:false` and `sso_enabled`. Unreachable identity service returns 503 and `identity_status:"unavailable"`, not an authenticated state |
| GET | `/api/projects` | `{"ok":true,"username":"u_<64 hex digits>","projects":["Default",...]}`. Username is opaque; do not build URLs from it |
| POST | `/api/projects` | Create a project. Empty body or omitted `project` uses Default. Response includes `project`, `created`, `user_created`, `project_created` |
| GET / POST / PUT | `/api/projects/<operation>` | Use your Default when the project name is omitted. Supports `status`, `validate`, `query`, `locate`, `index` with the methods below |
| GET | `/api/projects/{p}/status` | Health summary |
| POST | `/api/projects/{p}/validate` | Structural validation |
| POST | `/api/projects/{p}/query` | Recall |
| POST | `/api/projects/{p}/locate` | Retrieve nodes by bundle path or XPath |
| GET | `/api/projects/{p}/index` | Export the entire bundle |
| PUT | `/api/projects/{p}/index` | Replace the entire bundle; response `files` is an array of paths |
| GET | `/api/me/model-config` | `configured:false`, or `provider`, `alias`, `model_label`, `updated_at`. Never returns a key |
| PUT | `/api/me/model-config` | `{"provider":"deepseek"\|"openrouter","api_key":"..."}`. First save or changing provider requires a key; blank keeps the saved key for the same provider. At most 512 characters, without whitespace |
| DELETE | `/api/me/model-config` | Idempotently clear model settings |
| GET | `/api/me/jev-config` | Safe personal JEV status: `configured`, `effective_source` (`personal_jev`/`model`/`unconfigured`), TypeSafe `provider`/`provider_label`, `updated_at`. No keys, ciphertext or server paths |
| PUT | `/api/me/jev-config` | `{"api_key":"..."}` only, ≤8 KiB. First save requires a key; a later blank value keeps it |
| DELETE | `/api/me/jev-config` | Idempotently clear personal JEV only. Effective source becomes your model or `unconfigured` |

Model and JEV settings **are not memory contents**. Import, export, locate,
validate and status are deterministic and do not call a model or JEV.
Settings apply only to **queries**: your saved model is the second choice
after your own JEV. Saving a key does not add a file to the index or change
an existing bundle. Do not modify model settings merely to store a memory.

## Interpret errors

Gateway errors use a string:

```json
{"ok":false,"error":"not authenticated"}
```

Upstream index errors retain their response body, with an object:

```json
{"ok":false,"error":{"code":"index_invalid","message":"..."}}
```

| HTTP | Meaning |
|---|---|
| 401 | Missing, invalid or revoked key; expired session; changed identity. Possible errors include `not authenticated`, `identity credential is invalid or revoked`, `session expired or revoked`, `identity changed` |
| 403 | Untrusted Origin or invalid CSRF for cookie writes; access restrictions also use 403 |
| 404 | Project absent. Another user's project also returns 404 |
| 400 | Invalid name, body or bundle. Common codes: `invalid_request`, `invalid_bundle`, `invalid_name`, `index_invalid`, `confinement` |
| 409 | `name_conflict` between stored names and namespace keys; or `configuration required` for managed queries without personal JEV or model. Save a query configuration first |
| 413 | Request body exceeds the route's limit |
| 429 | Too many login attempts (10 per IP per minute), or more than 32 simultaneous index operations |
| 502 | Upstream unavailable or successful response exceeds 33 MiB. Oversized JSON is not returned as a truncated body |
| 503 | Chat temporarily unavailable. Index data remains, but identity cannot be verified for this request |

Upstream error messages replace the server data directory with
`<index-home>`. Successful responses are not rewritten and may include
absolute server paths; ignore those paths.

## A complete example

```bash
curl -s -H "Authorization: Bearer $API_KEY" "$GATEWAY/api/projects"

curl -s -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"project":"memory"}' "$GATEWAY/api/projects"

curl -s -X PUT -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d @bundle.json "$GATEWAY/api/projects/memory/index"

curl -s -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"query":"refresh vpn","kind":"howto","limit":10}' \
  "$GATEWAY/api/projects/memory/query"
```

After retrieving a node, read its body from the same exported bundle's
`files["notes/vpn.md"]`, rather than request a server path.

## Account access and invitations

New ordinary users need a single-use invitation; administrators and existing
members sign in directly. Accounts from before the invitation release retain
their indexes and personal configuration, initially with invitation quota 0.
William's administrator account is `allwefantasy@gmail.com`. Its role is
bound to the stable Chat account identity; an email change does not transfer it.

New members can sign in with Chat SSO or POST `api_key` and
`invitation_code` to the same-origin `/api/auth/login`. For first-time SSO,
POST `invitation_code` to the same-origin `/api/auth/sso/start`, then follow
the returned `url`. Codes are sent only via POST and invitation-link
fragments, never query parameters. Missing codes return
`403 / invite_required`; invalid, claimed or expired codes return
`403 / invite_invalid`; disabled accounts return
`403 / account_disabled`. Existing members can continue using ordinary
Chat API keys for Bearer requests.

Sparse Agent's independent `mem_at_` memory grants are exempt from invitations
only after Chat userinfo verifies `aud=auto-coder-index`,
`client_id=sparse-agent-gateway-memory`, expiration and operation scope.
These grants access only the user's indexes and permitted model configuration.
They do not create web members, invitation quota or administrator privileges.
Caller-supplied User-Agent or X-Sparse-Agent headers do not grant an exemption.
Disabled accounts and invalid credentials remain rejected. Chat and Sparse
support went live on 2026-10-06. Server credentials stay on the two servers,
never in a browser or agent. Public login clients cannot issue memory grants;
older grants require reconnecting in the workspace while preserving indexes.

Invitation management supports quota 0–100, depth 0–10 and expiration
1–720 hours (default 168). Member invitations are off by default.
Quota 3 and depth 1 means A can invite a total of 3 people; B cannot invite
others. Pending codes reserve slots; expiration or revocation releases unused
slots. Claimed slots are not refunded. Reducing permissions or disabling
member invitations revokes unclaimed codes that no longer qualify.
Existing descendant accounts and private indexes remain intact.

| Method | Path | Purpose |
|---|---|---|
| GET / POST | `/api/invitations` | Members view their records or create within quota/depth. Body: `{"invite_quota":0,"invite_depth":0,"expires_hours":168}`. The raw code is returned only once |
| DELETE | `/api/invitations/{id}` | Creator or administrator revokes a pending code |
| GET | `/api/admin/access` | Administrator reads member permissions and invitation policy |
| PUT | `/api/admin/invitation-policy` | Administrator sends `{"members_may_invite":true}` |
| PUT | `/api/admin/members/{id}` | Administrator sends complete `{"invite_quota":3,"invite_depth":1,"disabled":false}` |

Cookie writes still require same-origin Origin and CSRF. Memory grants cannot
call these management routes.

## Conditional model updates for Sparse cloud memory

PUT `/api/me/model-config` accepts an optional `expected_updated_at` string.
Read the current `updated_at` first; use an empty string only when no model
exists. A changed or deleted configuration returns 409 and is preserved.
Verified Sparse offline grants require `memory:model` for model access and
remain restricted to their existing memory operation scope.
