Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,12 @@ test-offload-sessions.sh
# npm pack / release tarballs (never commit packaged outputs)
*.tgz
*.tar.gz

# Local development notes (contributor-only, not shipped)
docs/superpowers/

# Per-developer Claude Code project instructions (contributor-only)
CLAUDE.md

# Plugin build output
claude-code-plugin/dist/
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,48 @@

---

## [Unreleased]

### 📦 新功能

- **Claude Code + Codex CLI 插件**(`claude-code-plugin/`):通过 Claude Code `/plugin install tdai-memory` 或 Codex CLI marketplace 一键启用,不修改用户 `~/.claude/settings.json` 或 `~/.codex/config.toml`。提供 3 个 hooks(`SessionStart` 异步预热、`UserPromptSubmit` 同步召回并通过 `additionalContext` 注入、`Stop` 异步捕获),3 个 slash skills(`/memory-search`、`/memory-status`、`/memory-clear-session`),以及一个总览 skill `tdai-memory`。Daemon 通过 `gateway-entry.ts` wrapper 绑定父进程生命周期。插件携带双 manifest(`.claude-plugin/plugin.json` 与 `.codex-plugin/plugin.json`),共享同一份 `hooks/hooks.json` 与 `skills/`。Claude Code(v2026.4+)是当前的一等宿主,端到端完整可用;Codex CLI(v0.130+)在 schema 层(hook 事件名、handler config 字段、`${CLAUDE_PLUGIN_ROOT}` 环境变量)已对齐,但当前部分阻塞——三层 blocker 详见 `claude-code-plugin/README.md`(discovery 层 [openai/codex#22078](https://github.com/openai/codex/issues/22078)、`async` 行为层 Codex 未实现、transcript 解析层只支持 cc 格式)。

### 🔧 兼容性 / 安全增强

- **Gateway 可选 Bearer Token 鉴权**:当设置 `TDAI_GATEWAY_TOKEN` 环境变量时,Gateway 要求所有非 OPTIONS 请求带 `Authorization: Bearer <token>`。未设置时行为不变,与 Hermes 完全向后兼容。Claude Code 插件每次 spawn daemon 时生成随机 256-bit token 写入权限 0600 文件。Bearer 字符串比较升级为 `crypto.timingSafeEqual`,Scheme 关键字按 RFC 6750 §2.1 大小写不敏感匹配(`Bearer`/`bearer`/`BEARER` 均可),401 响应携带 `WWW-Authenticate: Bearer realm="tdai-gateway"`。
- **Token 通过文件路径(`TDAI_TOKEN_PATH`)传递给 daemon 子进程**,不再注入到 `TDAI_GATEWAY_TOKEN` 环境变量。后者会随 execve() 写入子进程初始 environment block,使 token 暴露于 `/proc/<pid>/environ` 与 `ps -E`;改为文件传递后只剩 0o600 token 文件这一面,daemon 加载时还会校验文件 owner uid。
- **daemon 主机绑定加固**:cli.ts 启动时拒绝非 loopback 的 `TDAI_GATEWAY_HOST`,除非显式 `TDAI_GATEWAY_ALLOW_REMOTE=1` 打开开关;防止误把记忆端口曝露到 LAN/公网。
- **新增 `tdai-memory-gateway` bin**(`./dist/src/gateway/cli.mjs`):作为独立可执行 Gateway entry point,支持 `SIGTERM/SIGINT` 优雅关闭、可选父进程 PID liveness 探活(`TDAI_CC_PID` 环境变量,轮询间隔 15s)。供 Claude Code / Codex CLI 插件通过 `npx tdai-memory-gateway` 调用,无需把 npm 依赖打包进插件。
- **daemon 进程管理重写**:基于 `O_CREAT|O_EXCL` 的 `spawn.lock` 互斥,并发触发的 SessionStart / UserPromptSubmit / Stop hook 中只有一个会真正 spawn,其余复用结果,根本性解决双 daemon / 端口与 token 错配问题;`state.json` 改 tmp + rename 原子写;`ensureRunning` 复用旧 daemon 前校验 `state.ccPid` 与当前 cc 一致,避免跨用户/跨会话错用旧 daemon;spawn 时显式设置 `cwd` 与 `TDAI_DATA_DIR` 注入,避免数据目录受 hook 进程 cwd 漂移影响;token 文件权限校验在 Windows 上跳过 `0o077` 位检测(Node `fs` 在 Win 下返回固定 mode 会误报),改用 NTFS ACL。
- **`$ARGUMENTS` 命令注入面收敛**:cc 当前对 SKILL.md ``!`...` `` 块内的 `$ARGUMENTS` 执行字面 `replaceAll`,用户输入 `foo"; curl evil; "` 可注入到 shell(详见 anthropics/claude-code#16163)。重写 `memory-search/SKILL.md` 去掉 ``!`...` `` bash 块,改为引导 Claude 以 heredoc 通过 Bash 工具向 `hook.mjs search-stdin` 的 stdin 喂查询,用户输入不再经过 shell 词法解析。

### 🐛 修复

- **Stop hook 反复重写 L0**:之前每次 Stop 都向 `/capture` 全量发送最近 10 个 turn,而 Gateway 端 `originalUserMessageCount` 位置切片与 `afterTimestamp` 游标都缺失(`CaptureRequest` 不携带这两个字段),导致长会话前 N 个 turn 在每次 Stop 时反复写入 L0,污染 FTS5 与向量索引。改为基于 `$CLAUDE_PLUGIN_DATA/cursors/<sessionId>.json` 持久化的 `lastSentIndex` 取增量,首次发送以 50 turn 封顶,cursor 文件 tmp + rename 原子写。
- **CJK 召回退化**:底层 2-gram 停用词表此前包含 `我们/你们/他们/这个/那个/可以/有没/没有/就是/不是` 等普通双字实义词,"我们的部署方案" 被切成 `[们的, 的部, 部署, 署方, 方案]`、丢失 "我们" 锚点 token,中文查询召回受损。停用词表缩到真正低信息量的疑问/连接片段。
- **transcript 等待逻辑**:Stop hook 等待 cc 落盘从硬 sleep(800ms) 改为 `waitForTranscriptStable(2s)`:每 100ms 轮询 `stat().size`,连续两次相同字节数即视为 flush 完成;慢盘场景更稳。
- **L0 jsonl 直查内存压力**:`searchL0JsonlDirect` 从 `readFile` 整体加载改为 `readline + createReadStream` 流式扫描,避免长会话 jsonl 触发 OOM;文件遍历从字符串排序+reverse(依赖 `YYYY-MM-DD.jsonl` 命名)改为 mtime 倒序,对 cc UUID 命名也工作正常。
- **GatewayClient silent-failure 可观测**:所有 catch 块新增 `logPath` 失败追加,handleStatus 在 `/memory-status` 输出 `hook.log` / `daemon.log` 路径;daemon spawn 的 stdio stderr 重定向到 `daemon.log` 替代静默丢弃。
- **Codex CLI plugin 端 hooks 注册补全**:`.codex-plugin/plugin.json` 之前只声明了 `"skills": "./skills/"`,缺 `"hooks": "./hooks/hooks.json"` —— Codex CLI 与 Claude Code 不同,plugin-local hooks 不走"约定俗成路径",而是强制从 manifest 的 `hooks` 字段读取(见 `codex-rs/core-plugins/src/manifest.rs::RawPluginManifest`)。补上字段后 schema 层全部对齐:`SessionStart`/`UserPromptSubmit`/`Stop` 事件名、`command`/`timeout`/`statusMessage` handler 字段、`${CLAUDE_PLUGIN_ROOT}` 环境变量在 Codex 端都能解析(discovery.rs 注入了 `CLAUDE_PLUGIN_ROOT` backcompat alias,同时配 `PLUGIN_ROOT` 新名)。**注意 schema 层兼容 ≠ runtime 行为对齐**:Codex 解析 `async` 字段但实际硬编码为 sync 执行(`HookExecutionMode::Sync`,`core/src/hook_runtime.rs` 与 `hooks/src/engine/` 都没有消费 `r#async` 字段的代码),与 cc 的真异步行为不同;详见 README 中的 Codex 状态说明。

### ✅ 测试

- `auth.test.ts`:从 5 个 case 扩展到 14 个,覆盖鉴权对所有 POST 业务端点的矩阵、Bearer scheme 大小写、mangled Authorization 头、`WWW-Authenticate` 响应。
- `hook.test.ts`:新增 cursor 增量、无新 turn 跳过 captureTurn、`MAX_CAPTURE_TURNS=50` 边界 3 个 case,且把 stop describe 整体 stub `CLAUDE_PLUGIN_DATA` 到 mkdtemp 隔离 cursor 状态。
- `daemon.test.ts`:新增 `ensureRunning` 拒绝 ccPid 不匹配旧 state 的回归。

### 📚 文档

- `claude-code-plugin/README.md` 与 `README_CN.md`:安装、配置、数据布局、排障与安全模型完整说明,新增 `TDAI_TOKEN_PATH` / `TDAI_GATEWAY_ALLOW_REMOTE` / `TDAI_GATEWAY_CORS_ORIGIN` / Windows 兼容性说明。
- `claude-code-plugin/README.md` 与 `README_CN.md`:Codex CLI 安装段下重写 "Codex CLI 当前状态:部分阻塞" 小节,披露与 cc 对等之前的三层 blocker:
1. **Discovery 层(上游阻塞)**:`source_type = "local"` 安装受上游 [openai/codex#22078](https://github.com/openai/codex/issues/22078) 影响,manifest 解析正常但 `skills/` 与 `hooks/hooks.json` 在运行时被静默丢弃,hook 根本不会触发;
2. **`async` 行为层(Codex 不实现)**:Codex 解析 `async` 字段但 `HookRunSummary` 硬编码 `HookExecutionMode::Sync`,cc 端 `SessionStart`/`Stop` 上的 `async: true + timeout: 30` 在 Codex 修复 #22078 后会变成同步 30s 阻塞;计划用单独 `hooks/codex-hooks.json` 差异化 timeout(待办);
3. **Transcript 解析层(plugin 端未适配)**:Codex rollout jsonl schema `{timestamp, type, payload}` 与 cc transcript `{type, message, sessionId, parentUuid, …}` 完全不同,当前 `lib/transcript.ts` 仅解析 cc 格式,即使 Stop 在 Codex 上触发也会静默生成空 capture;Codex parser 是后续工作,等 #22078 修复后基于真实 Codex session 实现。

当前 Codex 上真正能用的部分:manifest 解析、`/plugin` 可见可切换、`lib/daemon.ts` 宿主无关 daemon spawn 与 cc 共用同一段代码。同时同步降级 README 与 CHANGELOG 顶部"双宿主对齐"的过度乐观表述。

---

## [0.3.4] - 2026-05-12

### 🐛 修复
Expand Down
11 changes: 11 additions & 0 deletions claude-code-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "tdai-memory",
"version": "0.1.0",
"description": "Long-term + symbolic short-term memory for Claude Code, powered by TencentDB Agent Memory.",
"homepage": "https://github.com/Tencent/TencentDB-Agent-Memory",
"license": "MIT",
"author": {
"name": "李冠辰",
"email": "liguanchen@xiaomi.com"
}
}
41 changes: 41 additions & 0 deletions claude-code-plugin/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "tdai-memory",
"version": "0.1.0",
"description": "Long-term + symbolic short-term memory for AI coding agents, powered by TencentDB Agent Memory.",
"author": {
"name": "李冠辰",
"email": "liguanchen@xiaomi.com"
},
"homepage": "https://github.com/Tencent/TencentDB-Agent-Memory",
"repository": "https://github.com/Tencent/TencentDB-Agent-Memory",
"license": "MIT",
"keywords": [
"memory",
"long-term-memory",
"short-term-memory",
"ai-memory",
"vector-search",
"sqlite-vec",
"persona",
"scene-extraction"
],
"skills": "./skills/",
"hooks": "./hooks/hooks.json",
"interface": {
"displayName": "TDAI Memory",
"shortDescription": "Long-term + short-term memory for AI coding agents",
"longDescription": "Adds long-term memory and symbolic short-term memory to Codex CLI: automatic recall before every prompt (relevant past memories injected via additionalContext), automatic capture after every turn (L0 conversation written, L1/L2/L3 atoms/scenarios/persona extracted in the background), and manual control via skills (memory-search, memory-status, memory-clear-session). Memory is partitioned per project (hash of cwd) by default. The daemon runs locally on 127.0.0.1 with a Bearer-token-protected HTTP API (file mode 0600).",
"developerName": "TencentDB Agent Memory contributors",
"category": "Productivity",
"capabilities": [
"Read",
"Write"
],
"brandColor": "#3B82F6",
"defaultPrompt": [
"Do you remember what we discussed about this project?",
"Search my memory for the migration plan we made last week",
"What were my preferences for the API design?"
]
}
}
125 changes: 125 additions & 0 deletions claude-code-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# TencentDB Agent Memory — Coding Agent Plugin

Long-term + symbolic short-term memory for [Claude Code](https://claude.com/claude-code) and [OpenAI Codex CLI](https://developers.openai.com/codex/cli), powered by [TencentDB Agent Memory](https://github.com/Tencent/TencentDB-Agent-Memory).

The plugin ships dual manifests (`.claude-plugin/plugin.json` and `.codex-plugin/plugin.json`) and reuses the same `hooks/hooks.json` and `skills/`. Claude Code (v2026.4+) and Codex CLI (v0.130+) share the hook protocol at the schema layer (event names, handler config fields, `${CLAUDE_PLUGIN_ROOT}` env var). **Claude Code is the first-class target today; Codex CLI is partially blocked** — see the [Codex CLI](#codex-cli) install section below for current status.

[中文版](./README_CN.md)

## What this gives you

- **Automatic recall** before every prompt — relevant past memories injected into context
- **Automatic capture** after every turn — L0 conversation written, L1/L2/L3 extracted in the background
- **Manual control** via slash skills: `/memory-search`, `/memory-status`, `/memory-clear-session`
- **Project-level isolation** by default (sessionKey = hash of cwd) — your `react-app` memories don't leak into your `golang-svc` work
- **Bearer-secured local daemon** — no plaintext localhost API

## Installation

### Prerequisite

Install the gateway runtime (the `tdai-memory-gateway` bin) globally — the plugin spawns the daemon via `npx tdai-memory-gateway`:

```bash
npm install -g @tencentdb-agent-memory/memory-tencentdb
```

This npm package contains the actual `TdaiGateway` (SQLite + sqlite-vec + LLM pipeline). The plugin itself is a thin shim that owns hooks, skills, and the per-session sessionKey — it does NOT bundle the heavy deps.

### Claude Code

```bash
/plugin install tdai-memory
```

### Codex CLI

```bash
codex plugin marketplace add <marketplace-url>
# then enable in the TUI: /plugin → toggle tdai-memory
```

(Once published to the Codex marketplace, this becomes a one-liner.)

> **Codex CLI status (≤ v0.130): partially blocked.** Three layered blockers separate the current Codex experience from Claude Code parity:
>
> 1. **Plugin discovery (upstream blocker).** `source_type = "local"` marketplace installs are affected by Codex issue [openai/codex#22078](https://github.com/openai/codex/issues/22078): the manifest parses, the plugin appears in `/plugin` and is toggleable, but the declared `skills/` and `hooks/hooks.json` are silently dropped at runtime. Hooks never fire on Codex today.
>
> 2. **`async` hook field is parsed but not honored.** Codex deserializes the `async` field on hook commands (`codex-rs/config/src/hook_config.rs::HookHandlerConfig::Command`), but no code in `core/src/hook_runtime.rs` or `hooks/src/engine/` consumes it — `HookRunSummary` is hardcoded to `HookExecutionMode::Sync`. Our `SessionStart` and `Stop` hooks declare `async: true, timeout: 30`. Once #22078 ships, this means a Codex session start will block synchronously on first-run daemon spawn, and every Stop will block on capture. Planned mitigation: a separate `hooks/codex-hooks.json` referenced from `.codex-plugin/plugin.json` with shorter timeouts.
>
> 3. **`lib/transcript.ts` only parses the Claude Code transcript format.** Codex records sessions to `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-*.jsonl` with shape `{timestamp, type: "session_meta" | …, payload: {…}}`, completely different from Claude Code's `{type, message, sessionId, parentUuid, …}`. Even if Stop fired on Codex, capture would silently produce empty turns. A Codex JSONL parser is planned once #22078 lets us validate end-to-end against a live Codex session.
>
> **What works today on Codex:** `.codex-plugin/plugin.json` is parsed correctly, the plugin is visible and toggleable in `/plugin`, and the shared daemon spawn / discovery logic in `lib/daemon.ts` is host-agnostic (same code path as Claude Code). Use Claude Code for actual memory functionality; track #22078 for the upstream fix.

---

No `~/.claude/settings.json` or `~/.codex/config.toml` mutation. The first time a session starts after installation, the plugin spawns the local daemon (via `npx tdai-memory-gateway`) on port 8421–8430 with a randomly generated Bearer token. State persists under `${CLAUDE_PLUGIN_DATA}`.

## Configuration

The plugin reads these optional environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `TDAI_SESSION_KEY` | `hash(cwd)` | Override the per-project memory partition |
| `TDAI_TOKEN_PATH` | auto-generated 0o600 file | Path to a file the daemon reads the Bearer token from (preferred over `TDAI_GATEWAY_TOKEN`; the env-var form puts the token into `/proc/<pid>/environ` and `ps -E`) |
| `TDAI_GATEWAY_TOKEN` | unset | Bearer token via env (fallback for the Hermes sidecar mode) |
| `TDAI_GATEWAY_HOST` | `127.0.0.1` | Daemon bind host. Non-loopback values are refused unless `TDAI_GATEWAY_ALLOW_REMOTE=1` is set, to avoid exposing the memory port to the LAN. |
| `TDAI_GATEWAY_ALLOW_REMOTE` | unset | Opt-in switch required to bind a non-loopback `TDAI_GATEWAY_HOST` |
| `TDAI_GATEWAY_CORS_ORIGIN` | unset | When set, enables CORS with the given Origin; the default disables CORS so cross-origin pages cannot probe the daemon's port. |
| `TDAI_GATEWAY_COMMAND` | `npx` | Override daemon spawn command (advanced; e.g. `node /path/to/cli.mjs` for development) |

Most users never need to set any of these. `TDAI_SESSION_KEY=shared-with-other-project` is the most common power-user override.

## Data location

- `${CLAUDE_PLUGIN_DATA}/state.json` — daemon PID + port (tmp+rename atomic)
- `${CLAUDE_PLUGIN_DATA}/token` — Bearer token (chmod 600, owner-uid checked)
- `${CLAUDE_PLUGIN_DATA}/spawn.lock` — O_CREAT|O_EXCL daemon-spawn mutex (stale after 60s)
- `${CLAUDE_PLUGIN_DATA}/cursors/<sessionId>.json` — per-cc-session `lastSentIndex` so Stop only POSTs new turns
- `${CLAUDE_PLUGIN_DATA}/memory-tdai/` — SQLite + sqlite-vec database, scene blocks, persona snapshots
- `${CLAUDE_PLUGIN_DATA}/hook.log` — hook diagnostic log (gateway-client failures, etc.)
- `${CLAUDE_PLUGIN_DATA}/daemon.log` — daemon stderr/stdout (cold-start crashes, etc.)

## How it works

```
User prompt → UserPromptSubmit hook → POST /recall → cc injects context
cc replies → Stop hook → POST /capture → L0 + L1/L2/L3 pipeline
Session end → daemon detects parent cc exit → graceful shutdown
```

All hook handlers fail silently (writing to `hook.log`) — memory is never on the critical path of your conversation.

## Troubleshooting

**`/memory-status` says "unreachable"**:
- Check `${CLAUDE_PLUGIN_DATA}/hook.log` (gateway-client request failures) and `${CLAUDE_PLUGIN_DATA}/daemon.log` (daemon cold-start crashes)
- Restart your cc session — the SessionStart hook re-probes and re-spawns the daemon

**Multiple cc terminals on the same project**:
- All terminals share one daemon. The first to launch spawns it; subsequent terminals discover and reuse it via `state.json`.

**Memory doesn't recall what I expect**:
- Run `/memory-search <topic>` directly to see what's stored
- Note that L1/L2/L3 extraction runs asynchronously — fresh conversations may need a few minutes before they appear in recall

## Security model

- The daemon listens only on `127.0.0.1` by default. Non-loopback `TDAI_GATEWAY_HOST` is refused unless `TDAI_GATEWAY_ALLOW_REMOTE=1` is also set.
- Every request requires `Authorization: Bearer <token>`. Comparison is timing-safe; the scheme keyword is RFC 6750 §2.1 case-insensitive; 401 responses include `WWW-Authenticate: Bearer realm="tdai-gateway"`.
- The token is generated freshly at each daemon spawn, written to `${CLAUDE_PLUGIN_DATA}/token` (chmod 600), and passed to the daemon child process **by file path** (`TDAI_TOKEN_PATH`) rather than as an env var, so the token does not surface via `/proc/<pid>/environ` or `ps -E`. Token-file owner is checked against the current uid on read.
- The `memory-search` skill passes the user query to the daemon over **stdin** via a heredoc, never as a shell argv element — this avoids the literal-`replaceAll` `$ARGUMENTS` injection surface in cc (anthropics/claude-code#16163).
- On Windows the 0o077 mode check is skipped (Node's `fs` returns fixed mode bits there); the OS-provided NTFS ACL on the token file is relied on instead.

## Building from source

```bash
pnpm install
pnpm build:cc-plugin
pnpm test:cc-plugin
```

## License

MIT — see [LICENSE](../LICENSE).
Loading