---
name: index-gateway-memory
description: >
  教第三方用 Sparse Agent Memory 的 HTTP API 完成记忆：用 Auto-Coder Chat
  的 API Key 鉴权，在自己的项目里写入、查询、定位、校验、导出一份 Computer Index。
  Use when a third party or their agent must store or recall memory through this
  gateway, or asks about 记忆, 写入记忆, 查询索引, /api/projects, or the Sparse Agent Memory
  API. Not for deploying or operating the gateway.
---

# 用 Sparse Agent Memory 完成记忆

先了解功能和用法：[云端记忆使用说明与示例](https://sparse-agent.com/cases/agent-memory.html)。本文提供程序接入说明。

同一份说明发布在 Sparse Agent Memory 官网的 `/docs/memory`，打开就能看，不需要登录。Markdown 原文在 `/docs/memory.md`。

这份说明给**调用方**，不是给部署这份服务的人。调用方是某个 Auto-Coder Chat 账户，或替这个账户做事的程序。记忆按 Chat 用户隔离：同一个账户的多把 API Key 看见同一批项目；别的账户即使猜到项目名，也只会得到「没有这个项目」。

服务不接收用户名。项目名由调用方起，命名空间由服务端从 Chat 用户派生。不要请求 `/v1/...`，那不是这层 API。

下文用 `$GATEWAY` 表示服务的公网 origin，例如 `https://example.com`，不要带路径，也不要自己拼端口以外的东西。用 `$API_KEY` 表示 Chat 账户设置里创建的 `ak_…`。Key 只放在 `Authorization` 头里，不要写进 URL、日志或仓库。

## 先选定调用方式

索引请求的执行名额由服务端管理员配置，默认全站 20 个并发任务，满额后按到达顺序排队（等待队列最多 256 个请求）。普通 HTTP 调用等待后返回原有 JSON；调用方设置的超时须覆盖排队和执行，不要在超时后盲目重发写请求，先回读确认。

需要显示等待进度时，在索引请求中加 `Accept: application/x-ndjson`。服务端每行返回一个 JSON 事件：`{"state":"queued","message":"你的请求正在排队","queue":{"position":1,"max_concurrency":20,"running":20,"queued":1}}`；获得名额后为 `running`；最终为 `{"state":"completed","http_status":200,"result":{...}}`。以最终 `http_status` 与 `result` 判断成功；流本身的 HTTP 200 不代表任务成功。`position` 从 1 起，前面等待人数为 position−1。断开连接会移除尚未执行的请求；已经开始的任务仍保留名额到执行结束。

管理员入口只对 Chat 已验证邮箱 `allwefantasy@gmail.com` 开放：`GET /api/admin/config` 查看并发及队列数量，`PUT /api/admin/config` 提交 `{"max_concurrency":N}`（1–1000）。保存立即生效且重启保留；Cookie 写请求沿用 Origin/CSRF 要求，普通账号返回 403。


机器和脚本用 Bearer。不需要 Cookie，不需要 `Origin`，不需要 `X-CSRF-Token`。

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

浏览器页面用 Cookie。`POST /api/auth/login` 必须带与部署 origin 一致的 `Origin`（生产环境是 `https` 域名，不是调用方自己的 Host）。登录响应里的 `csrf_token` 要放进之后每一次 Cookie 写请求的 `X-CSRF-Token`。`GET /api/auth/me` 也会返回当前会话的 `csrf_token`。Bearer 请求不要走这条。

部署启用了浏览器 SSO 时，人打开 `GET /api/auth/sso/start` 并跟随跳转即可。失败会回到 `/?sso_error=`，值只可能是 `denied`、`state`、`failed`、`unavailable`、`rate`。程序不要依赖 SSO。

## 记住一件事

一条记忆是某个**项目**里的一整包 Computer Index，不是一次聊天记录。查询怎么打分取决于本服务启用的查询运行时：启用托管运行时时，查询用你自己的 JEV（必要时核实**包内**叶片证据，不会读包外文件）或你已保存的原生模型（只依据索引元数据与结构化 id 打分，不读正文）；只有旧版/未启用运行时的部署才退回逐层词法遍历。无论哪种模式，都要把能被想起来的话说进节点的 `name`、`description`、`tags`，以及 `metadata` 里的 `summary`、`intent_phrases`、`aliases`、`actions`。正文放在节点 `path` 指向的包内文件，等查中之后再读。

用户说「帮我记忆……」而没有指定 Project 时，直接使用 `Default`。服务会在首次使用时幂等创建它，重复调用保留已有内容；不需要先让用户手工建项目。HTTP 接口可省略项目路径段，例如 `PUT /api/projects/index`、`POST /api/projects/query`，都使用本人 `Default`。显式 `/api/projects/Default/...` 也会自动创建。调用现有 `auto-coder.index.cloud` CLI 时，代理给命令传 `--project Default`。用户明确指定其他项目时使用该名字；旧的小写 `default` 与 `Default` 是不同项目，旧索引保留。

一个项目一次只存一包。`PUT` 是整包替换，包里没列出的旧文件会被删掉。追加记忆前必须先导出已有包，再合并内容和索引节点后 PUT，不能只上传新记忆而清掉旧内容。需要独立整包时可使用不同项目名。

项目名按字符计，最多 128 个，区分大小写，不能有首尾空白、`/`、`\`、控制字符，也不能是 `.` 或 `..`。中文按一个字符计。URL 里这一段必须做 path 转义。

写入顺序：

1. 未指定 Project 时使用 `Default`：可直接调用省略项目的接口，也可 `POST /api/projects` 提交 `{}` 或空正文。指定名字时先 `GET /api/projects`，没有目标项目再提交 `{"project":"<名字>"}` 创建。重复创建是幂等的，`created` 为 false 时不会清空已有索引。
2. 准备 `{"files":{...}}`。必须有 `root.yaml`。路径是包内相对路径，用 `/`，不能是绝对路径、`..`、`~`、`$`、空段，也不能有以 `.` 开头的段。每个节点的 `path` 同样要落在这包里面。不要写 `metadata.project_path`。
3. `PUT /api/projects/<名字>/index`。成功时旧包已被替换。返回的 `files` 是路径列表，不是正文。校验失败时旧包还在，可以再导出确认。

最小的一包长这样。`type: index` 指向另一页 YAML，`type: file` 指向正文。

```json
{
  "files": {
    "root.yaml": "apiVersion: autocoder.dream/v1\nkind: ComputerIndex\nmetadata:\n  name: root\n  description: 个人记忆\nnodes:\n  - name: Knowledge\n    description: 可复用的做法\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: 做法\nnodes:\n  - name: 刷新 VPN\n    description: Windows 上重新连接 VPN 配置并确认路由\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: 重连 VPN 配置后检查默认路由\n      intent_phrases: [vpn 断了怎么办, 刷新 vpn]\n",
    "notes/vpn.md": "# 刷新 VPN\n\n重连配置，然后确认默认路由已经回来。\n"
  }
}
```

包的上限：最多 256 个文件，单文件 UTF-8 不超过 1 MiB，正文合计不超过 8 MiB，单条路径不超过 240 字符。网关对这次 PUT 的外层限制是 12 MiB；超过上游上限会拒绝，且不会改掉旧索引。

## 想起一件事

`POST /api/projects/<名字>/query`。正文只允许这些字段：

| 字段 | 含义 |
|---|---|
| `query` | 字符串，最长 4000 字符。可以空，但空的时候至少要有一个过滤条件 |
| `limit` | 整数 1–100，默认 10 |
| `kind` | 字符串或字符串数组，匹配节点的语义种类，如 `howto` |
| `since` | `YYYY-MM-DD` 或 ISO 日期时间，只保留该日及之后的结果 |
| `status` | 字符串或字符串数组，匹配条目的 status |
| `project` | 在条目的名称、描述、chain 和相关元数据里做子串过滤。它不是 URL 里的项目名 |
| `path` | 在路径字段上做子串过滤 |
| `knowledge_rag` | 布尔值。本服务未开启 Knowledge RAG，传 `true` 也不会检索，结果里的 `knowledge_rag_result` 仍是 `{}` |

`mode`、`model`、`backend`、`runtime`、`index_dir`、`root`、`effort` 以及其他未列出的字段都会 400。查询正文**不能**选择模式、后端或运行时：启用托管运行时时，服务按「本人 JEV > 本人已保存模型」选择，两项都没配置才以 409 `configuration required` 拒绝；已配置但凭据无效/损坏时返回明确错误，绝不静默改用其他身份，也绝不悄悄改跑另一个引擎。损坏的本人 JEV 不会改用模型。只有旧版/未启用运行时的部署才走逐层词法遍历。

读结果时只用这些相对信息：`results[]` 的 `name`、`description`、`path`、`target_type`、`tags`、`metadata`、`chain`、`score`、`semantic_kind`。`path` 是包内路径。`target_type` 为 `file` 时，正文在导出包的 `files[path]`；为 `index` 时，`path` 是下一页 YAML，继续在包里打开它。

启用托管运行时时，响应里多一个 `query_runtime`：`mode` 为 `jev` 或 `model`，`source` 为 `personal_jev` 或 `model`，只说明本次实际用了哪条路径，不含路径、Key 或后端细节，调用方不用自己指定。

当 `root_traversal.strategy` 为 `ranked_traversal` 时，要按 `root_traversal` 自己的 `terminals`、`status`、`budget`、`stats` 读：非 ok 的 `status` 表示这次遍历没有成功完成，但响应里仍可能带着已验证的部分 terminal，所以既要报出准确的 `status`，也要报出这些部分结果，不能当成完整成功，也不要在同一个请求里静默换引擎或重走一遍。带 `model_ranking` 时，要说明它的 `status`、候选覆盖与截断情况；只有评到分的条目才是模型结果，未评分的词法命中不算模型产出。

`index_dir`、`root_path`、`resolved_path`、`document_path`、`read_next`、`yaml_files` 经常是**服务器上的绝对路径**。不要在调用方机器上打开，也不要把它们发回去。需要正文时 `GET /api/projects/<名字>/index`，用 `files` 这张相对路径表。

只有当 `root_traversal.strategy == "progressive_root_traversal"` 时，`results` 为空或 `root_traversal.required` 为 true 才按同样规则走 `root_traversal.candidates[]` 的 `path`。`strategy` 是 `root_traversal` 内部的字段（写法是 `root_traversal.strategy`），不是响应里的顶层字段。`root_traversal.strategy == "ranked_traversal"` 时不要这样走：那种响应按它自己的 `terminals`、`status`、`budget`、`stats` 读，`required` 只表示该引擎没有给出 terminal，不要据此重走一遍非 ok 的 ranked 引擎。响应里的 `help_command` 是引擎自己的句子，调用方不要在网关机器上执行它。

已经知道包内路径时，用 locate，不要再搜：

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

`fs_path` 与 `xpath` 必须二选一。`fs_path` 也是包内相对路径，规则和写入路径相同。XPath 作用在索引节点上，例如 `//node[contains(@description, 'VPN')]`。

## 维护

- 备份：`GET .../index`，把返回的 `files` 存下来。点号开头的文件和目录不会出现在导出里。
- 健康：`GET .../status`。看 `healthy`、`status`、`validation_ok`、`issues`。`recommended_action` 为 `build` 或 `optimize` 只是建议；本 API 没有构建或优化动作，要改内容就改包再 PUT。
- 结构校验：`POST .../validate`，正文 `{}` 或省略。`{"require_knowledge_structure": true}` 会额外要求知识根结构，失败是 400，且不改索引。校验是只读的。
- 换包前先导出。不合法的 PUT 会留下旧包。

## 接口一览

除健康检查、官网文档、登录和 SSO 跳转外，下列接口都要鉴权。写请求在 Bearer 下只需要 Key；在 Cookie 下还要 `Origin` 和 `X-CSRF-Token`。登录本身只要 `Origin`，当时还没有 CSRF。

| 方法 | 路径 | 作用 |
|---|---|---|
| GET | `/healthz` | 不需登录。`{"ok":true,"checks":{"db":"ok","upstream":"ok"}}`，失败时 HTTP 503 |
| GET | `/` | 给人用的页面。页头有指向本文的链接 |
| GET | `/docs` | 不需登录，跳到 `/docs/memory` |
| GET | `/docs/memory` | 不需登录。本文的网页 |
| GET | `/docs/memory.md` | 不需登录。本文的 Markdown |
| POST | `/api/auth/login` | `{"api_key":"ak_…"}` → Cookie，以及 `csrf_token`、`account.email` |
| GET | `/api/auth/sso/start` | 仅在部署启用 SSO 时存在，302 到 Chat |
| GET | `/api/auth/sso/callback` | SSO 回跳。成功 303 到 `/`，失败 303 到 `/?sso_error=<枚举>` |
| POST | `/api/auth/logout` | 只撤销本服务的会话 |
| GET | `/api/auth/me` | 已登录：`authenticated:true` 和 email；Cookie 会话另有 `csrf_token`。未登录也是 200，`authenticated:false`，并带 `sso_enabled`。身份服务不可达是 503，`identity_status:"unavailable"`，此时不算已登录 |
| GET | `/api/projects` | `{"ok":true,"username":"u_<64位hex>","projects":["Default",...]}`。`username` 是不透明命名空间，不要拿去拼 URL |
| POST | `/api/projects` | 创建。省略 `project`（`{}` 或空正文）使用 `Default`。响应含 `project`、`created`、`user_created`、`project_created` |
| GET / POST / PUT | `/api/projects/<操作>` | 省略项目名时使用本人 `Default`；支持 `status`、`validate`、`query`、`locate`、`index`，方法与下面的带项目接口一致 |
| GET | `/api/projects/{p}/status` | 健康摘要 |
| POST | `/api/projects/{p}/validate` | 结构校验 |
| POST | `/api/projects/{p}/query` | 回忆 |
| POST | `/api/projects/{p}/locate` | 按包内路径或 XPath 取节点 |
| GET | `/api/projects/{p}/index` | 导出整包 |
| PUT | `/api/projects/{p}/index` | 整包替换。成功响应的 `files` 是路径数组 |
| GET | `/api/me/model-config` | `configured` 为 false，或 `provider`、`alias`、`model_label`、`updated_at`。永不返回 Key |
| PUT | `/api/me/model-config` | `{"provider":"deepseek"\|"openrouter","api_key":"..."}`。首次或更换 provider 必须带 Key；同一 provider 留空则沿用已存 Key。Key 最长 512，不能含空白 |
| DELETE | `/api/me/model-config` | 清除模型设置，幂等 |
| GET | `/api/me/jev-config` | 本人 JEV 安全状态：`configured`、`effective_source`（`personal_jev`/`model`/`unconfigured`）、`provider`/`provider_label`（固定 TypeSafe JEV）、`updated_at`。永不返回 Key、密文或路径 |
| PUT | `/api/me/jev-config` | `{"api_key":"..."}`（只接受这一个字段，≤8KiB）。首次保存必须带 Key；已保存后留空表示沿用原 Key，不会清空 |
| DELETE | `/api/me/jev-config` | 只清除本人 JEV，幂等；清除后生效来源是本人模型，没有模型则为 `unconfigured` |

模型与 JEV 设置**不是记忆内容**：导入、导出、定位、校验、状态查询都是确定性的，不做任何推断，也不调用模型或 JEV。它们只在**查询**时生效——启用托管运行时时，模型是「本人 JEV > 本人已保存模型」里的第二级。保存 Key 不会把文件编进索引，也不会改变已写入的包。不要为了让一条记忆生效去调用它们。

## 错误怎么读

网关自己拒绝时，`error` 是字符串：

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

索引服务拒绝时，正文原样返回，`error` 是对象：

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

| HTTP | 含义 |
|---|---|
| 401 | 没有 Key、Key 无效或已撤销、会话失效。`error` 可能是 `not authenticated`、`identity credential is invalid or revoked`、`session expired or revoked`、`identity changed` |
| 403 | Cookie 写请求的 Origin 不可信，或 CSRF 不对 |
| 404 | 项目不存在。别人的项目也是 404，这不表示你探测到了对方 |
| 400 | 名字、正文或包不合法。常见 `code`：`invalid_request`、`invalid_bundle`、`invalid_name`、`index_invalid`、`confinement` |
| 409 | `name_conflict`，存储里的名字和命名空间键不一致；启用托管运行时时，也可能是查询缺少可用配置（`configuration required`，本人 JEV 与本人模型都没有）——先保存 JEV Key 或模型设置再查 |
| 413 | 正文超过对应接口的上限 |
| 429 | 登录过频（同一 IP 每分钟 10 次），或同时进行的索引操作超过 32 个 |
| 502 | 索引服务不可用，或成功响应超过 33 MiB。后者不会返回被截断的 JSON |
| 503 | Chat 暂时不可用。索引数据还在，只是这次没法确认你是谁 |

上游错误信息里的服务器数据目录会被改写成 `<index-home>`。成功响应不改写，所以才会带上服务器绝对路径；忽略它们。

## 一次完整调用

```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":"刷新 vpn","kind":"howto","limit":10}' \
  "$GATEWAY/api/projects/memory/query"
```

查到节点后，用同一次导出的 `files["notes/vpn.md"]` 取正文，不要请求服务器路径。


## 账号准入与邀请

普通用户首次登录需要一次性邀请码；管理员和已绑定账号直接登录。发布前已有账号保留索引和个人配置，初始继续邀请额度为 0。管理员为 William 的 `allwefantasy@gmail.com`，权限固定到 Chat 稳定账号身份，邮箱变更不会转移权限。

新成员可用页面的 Chat SSO 登录，也可同源 POST `/api/auth/login` 提交 `api_key` 与 `invitation_code`。SSO 首次登录通过同源 POST `/api/auth/sso/start` 提交 `invitation_code`，再跳转返回的 `url`；码只通过 POST 和邀请链接 fragment 传递，不放进查询参数。未提供码返回 `403 / invite_required`，无效、已领取或过期返回 `403 / invite_invalid`，禁用账号为 `403 / account_disabled`。已有成员的普通 API Key 可继续用于 Bearer 调用。

Sparse Agent 使用经 Chat userinfo 验证的独立 `mem_at_` 记忆授权时免邀请码，要求 `aud=auto-coder-index`、`client_id=sparse-agent-gateway-memory`、有效期和操作 scope 均有效；只访问自己索引及范围允许的模型配置。该豁免不产生网页成员、邀请额度或管理员权限，调用方声明的 User-Agent/X-Sparse-Agent 不能获得豁免。明确禁用的账号和失效凭据仍被拒绝。Chat 与 Sparse 的配套授权已于 2026-10-06 上线；服务端凭据仅保存在两个服务端，不进入浏览器或 Agent。公开登录客户端不能签发记忆授权，旧授权需在工作台重新连接，原索引保留。

管理员页面的「邀请管理」支持额度 0–100、层级 0–10和 1–720 小时有效期（默认 168）。普通用户继续邀请默认关闭；额度 3、层级 1 表示 A 累计邀请 3 人，B 不能继续邀请。未领取码占名额，过期或撤销释放，已领取额度不退回；降低授权或关闭继续邀请会撤销不再符合授权的未领取码。既有下级账号及私人索引保留。

| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET / POST | `/api/invitations` | 已注册用户查看自己的记录，或在额度/层级内生成码；创建体为 `{"invite_quota":0,"invite_depth":0,"expires_hours":168}`，原码只返回一次 |
| DELETE | `/api/invitations/{id}` | 创建者或管理员撤销未领取码 |
| GET | `/api/admin/access` | 管理员读取成员授权和邀请开关 |
| PUT | `/api/admin/invitation-policy` | 管理员提交 `{"members_may_invite":true}` |
| PUT | `/api/admin/members/{id}` | 管理员提交完整 `{"invite_quota":3,"invite_depth":1,"disabled":false}` |

Cookie 写请求仍需同源 Origin 与 CSRF；记忆授权不能调用这些管理接口。


## 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.
