@aliyunrds/ctxdb 1.0.10 → 1.0.11-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ Unified access layer for RDS ContextDatabase. One `ctxdb` CLI (memory + KB ops),
9
9
  | **qwenwork** | ✅ UserPromptSubmit + Stop + SessionStart | `~/.qwenwork/skills/ctxdb/` | hooks-driven (auto capture/recall) |
10
10
  | **codex** | ✅ UserPromptSubmit + Stop + SessionStart | `~/.codex/skills/ctxdb/` | hooks-driven (auto capture/recall) |
11
11
  | **claude** | ✅ UserPromptSubmit + Stop + SessionStart | `~/.claude/skills/ctxdb/` | hooks-driven (auto capture/recall) |
12
- | **opencode** | Plugin shim (`~/.config/opencode/plugins/ctxdb.ts`) | `~/.config/opencode/skills/` | in-process plugin (auto capture/recall + KB catalog injection) |
12
+ | **opencode** | Plugin bundle (`~/.config/opencode/plugins/ctxdb-bundle.js`) | `~/.config/opencode/skills/` | in-process plugin (auto capture/recall + KB catalog injection) |
13
13
  | **hermes** | ✅ `pre_llm_call` + `post_llm_call` | `~/.hermes/skills/` | shell hooks (auto capture/recall + first-turn warmup) |
14
14
  | **workbuddy** | ✅ UserPromptSubmit + Stop + SessionStart | `~/.workbuddy/skills/` and `~/.workbuddy-ai/skills/` (existing homes) | CodeBuddy command hooks (complete shell command, no separate `args`) |
15
15
 
@@ -155,7 +155,11 @@ ctxdb auth status --json
155
155
 
156
156
  The internal tarball bundles `@aliyunrds/ctxdb-shared`, so users install only
157
157
  one ContextDB package. `semver` and `yaml` remain normal registry dependencies.
158
- Maintainers should follow [`INTERNAL_PUBLISH.md`](./INTERNAL_PUBLISH.md).
158
+ Maintainers should follow [`INTERNAL_PUBLISH.md`](./INTERNAL_PUBLISH.md) for Ali NPM.
159
+ For public releases, use [`LOCAL_PUBLISH.md`](./LOCAL_PUBLISH.md) to build with
160
+ `pnpm pack` and validate the exact tarballs, then follow [`PUBLISH.md`](./PUBLISH.md)
161
+ to upload those tarballs with `npm publish`. npm publication does not update the
162
+ separate [OSS installer scripts and agent manual](../../docs/bootstrap-release.md).
159
163
 
160
164
  ### Recommended: console-generated installer command
161
165
 
@@ -309,7 +313,7 @@ ctxdb status --agent codex --json
309
313
 
310
314
  Setup first checks that the target agent home exists (`~/.qoder`, `~/.qoderwork`, `~/.qwenwork`, `~/.codex`, `~/.claude`, or either WorkBuddy home: `~/.workbuddy` / `~/.workbuddy-ai`; other declared CN variant homes also satisfy this check). Setup writes hooks and skills into every existing home, so a CN-only install does not create the international product home, and vice versa. If no home exists, install or start that agent once before running `ctxdb setup --agent <name>`. OpenCode and Hermes are config-dir style: setup creates `~/.config/opencode` / `~/.hermes` when they are missing.
311
315
 
312
- For **qoder**, **qoderwork**, **qwenwork**, **codex**, **claude**, and **workbuddy**, restart the harness (CLI: just exit + restart; app: Cmd+R or quit/relaunch) so it picks up the new hooks/skill. For **codex**, setup also writes `[features].hooks = true` into `~/.codex/config.toml` (creating the file if missing) — Current Codex versions enable hooks by default; the explicit setting also supports older installs. Review and trust each installed or changed hook definition in `/hooks` before use. For **opencode**, restart OpenCode so it loads `~/.config/opencode/plugins/ctxdb.ts`. For **hermes**, setup also checks `~/.hermes/shell-hooks-allowlist.json`; it reports setup as incomplete until both ctxdb hook commands are approved.
316
+ For **qoder**, **qoderwork**, **qwenwork**, **codex**, **claude**, and **workbuddy**, restart the harness (CLI: just exit + restart; app: Cmd+R or quit/relaunch) so it picks up the new hooks/skill. For **codex**, setup also writes `[features].hooks = true` into `~/.codex/config.toml` (creating the file if missing) — Current Codex versions enable hooks by default; the explicit setting also supports older installs. Review and trust each installed or changed hook definition in `/hooks` before use. For **opencode**, restart OpenCode so it loads `~/.config/opencode/plugins/ctxdb-bundle.js`. For **hermes**, setup also checks `~/.hermes/shell-hooks-allowlist.json`; it reports setup as incomplete until both ctxdb hook commands are approved.
313
317
 
314
318
  Hermes owns this security allowlist, so ctxdb only checks it and never writes approvals. Approve the ctxdb `pre_llm_call` and `post_llm_call` hooks with:
315
319
 
@@ -366,7 +370,7 @@ Implementation note: qoder/Claude/WorkBuddy consume the JSON `hookSpecificOutput
366
370
 
367
371
  WorkBuddy 5.5.2 on macOS requires the full shell command in `command`; its runner ignores `args`. Native Auto (default routing label; underlying model unknown) and explicitly selected GPT-5.6-Sol completed automatic Skill Evaluate, suggestion delivery, generation, Upload/Complete, and exact Load. The final package also passed cold-cache Load for both versions. Sol required one native Retry after a platform error; Auto's extra execution was not proven fully isolated. Windows runner behavior and Memory capture/recall were not tested in this acceptance run. See the [Agent/model acceptance report](../../docs/skill-agent-acceptance-20260912.md) and [WorkBuddy evidence](../../docs/workbuddy-acceptance-20260912.md) for version scope, retained failures, and cleanup.
368
372
 
369
- For **opencode**, `ctxdb setup --agent opencode` copies the self-contained plugin bundle to `~/.config/opencode/plugins/ctxdb-bundle.js` and writes a re-export shim at `~/.config/opencode/plugins/ctxdb.ts` referencing it via the relative path `./ctxdb-bundle.js` (Bun on Windows can't resolve absolute-path specifiers from a .ts shim). `ctxdb upgrade --agent opencode` re-copies the bundle + refreshes the shim.
373
+ For **opencode**, `ctxdb setup --agent opencode` copies the self-contained plugin bundle to `~/.config/opencode/plugins/ctxdb-bundle.js` as the single auto-loaded entry. Setup and `ctxdb upgrade --agent opencode` remove the older generated `ctxdb.ts` shim: OpenCode loads both `.ts` and `.js`, so keeping both files initializes the plugin twice. Run upgrade after updating the npm package, then restart OpenCode. An unrecognized `ctxdb.ts` is preserved and reported as a migration conflict.
370
374
 
371
375
  For **hermes**, `ctxdb setup --agent hermes` writes two shell hooks under `~/.hermes/config.yaml`:
372
376
 
@@ -443,6 +447,17 @@ fact-extraction). Use it when the user explicitly asks for a verbatim
443
447
  memory ("记住 / 请记忆 / 原文记下"); without `--no-infer` the server may
444
448
  rewrite, merge, or skip details.
445
449
 
450
+ Memory uploads (`memory add`, including images, and automatic hook/plugin
451
+ capture) always send a top-level `agent_id`. `CTXDB_AGENT_ID` takes precedence
452
+ over the selected config's `agent_id`; when neither is set, uploads use the
453
+ current Agent name (`--agent`, then `CTXDB_AGENT`, then `default`; OpenCode's
454
+ plugin uses `opencode`). `default` means no specific host was selected.
455
+ This fallback applies only to uploads: default search/list and automatic
456
+ recall remain unchanged, so historical and other agents' memories can still
457
+ be retrieved. An explicitly configured `agent_id` continues to filter
458
+ automatic recall. The field records write attribution; server-side memory
459
+ organization may still deduplicate facts across agents within one Member.
460
+
446
461
  `memory add [<text>] --image=<local-path>` adds an image-backed memory.
447
462
  Repeat `--image` to attach multiple PNG, JPEG, or WebP files; text is optional
448
463
  when at least one image is present. Image requests require inference, so they
@@ -861,8 +876,8 @@ Field reference:
861
876
  | Key | Type | Default | Meaning |
862
877
  |---|---|---|---|
863
878
  | `user_id` | str | `"default"` | mem0-layer bucket key on every capture/recall call. The server's tenant isolation runs on `member_id` (injected from `X-API-Key`), so this is an optional per-user/agent slice. Override with `ctxdb setup --agent <a> --user-id <bucket>` or `CTXDB_USER_ID=<bucket>` when you need to isolate this install from another access layer or machine sharing the same workspace |
864
- | `agent_id` | str | null | Optional scope dimension on every capture/recall call. **Default null = not sent** — the server counts `agent_id` as scope on the search path, so emitting it by default would narrow every existing user's recall. Set only when you want per-agent isolation: `CTXDB_AGENT_ID=<id>` (env-only; no setup flag) |
865
- | `app_id` | str | null | Optional app-scope dimension, same contract as `agent_id` (default null = not sent). Set via `CTXDB_APP_ID=<id>` (env-only) |
879
+ | `agent_id` | str | null | Explicit memory upload attribution and automatic-recall filter. Uploads fall back to the current Agent name; automatic recall omits this filter unless explicitly configured. `CTXDB_AGENT_ID=<id>` overrides this value (no setup flag). This does not guarantee per-agent retention during server-side deduplication. |
880
+ | `app_id` | str | null | Optional app-scope dimension for automatic capture/recall; default null means not sent. Set via `CTXDB_APP_ID=<id>` (env-only). |
866
881
  | `auto_capture` | bool | `true` | End-of-turn hook/plugin captures each turn into long-term memory for the selected agent |
867
882
  | `auto_skill_evolution` | bool | `true` | Evidence collection and automatic evolution through the selected Agent hooks/plugin. Hermes delivers ready suggestions on the next genuine user turn. Disabling it keeps private Skill discovery and explicit commands available. |
868
883
  | `auto_recall` | bool | `true` | UserPromptSubmit hook recalls memory on each user prompt for the selected agent |
@@ -872,7 +887,7 @@ Field reference:
872
887
  | `knowledge_top_k` | int | `6` | KB chunks pulled per recall (only effective when `recall_knowledge: true`) |
873
888
  | `debug` | bool | `false` | Configured preference for verbose hook logging and full recall tracing. Effective debug is forced on for every final `base_url` outside the exact official-production allowlist; see “Debug control and recall replay”. General logs go to `~/.ctxdb/logs/ctxdb.log`, structured full-content recall records to `~/.ctxdb/logs/recall-trace.jsonl` |
874
889
 
875
- Env-var overrides apply to the selected agent config (env wins): `CTXDB_AGENT` / `CTXDB_ACCESS_TOKEN` / `CTXDB_API_KEY` / `CTXDB_BASE_URL` / `CTXDB_USER_ID` / `CTXDB_AGENT_ID` / `CTXDB_APP_ID`. The internal login command additionally reads `CTXDB_LOGIN_SERVER` as its provider-neutral login endpoint. The last two default to unset — the request body omits `agent_id`/`app_id` entirely; set them only when you want per-agent / per-app scope isolation on the server.
890
+ Env-var overrides apply to the selected agent config (env wins): `CTXDB_AGENT` / `CTXDB_ACCESS_TOKEN` / `CTXDB_API_KEY` / `CTXDB_BASE_URL` / `CTXDB_USER_ID` / `CTXDB_AGENT_ID` / `CTXDB_APP_ID`. The internal login command additionally reads `CTXDB_LOGIN_SERVER` as its provider-neutral login endpoint. The last two default to unset: automatic recall omits both filters, while uploads still send the current Agent name as `agent_id`.
876
891
 
877
892
  Downstream Agent hosts can set `CTXDB_ACCESS_TOKEN` to send `Authorization: Bearer <token>` to the Data API. It is runtime-only, never written to `~/.ctxdb/ctxdb.json`, and takes precedence over a managed session or API key for CLI, process hooks, and OpenCode; a rejected token does not fall back to another credential. `ctxdb setup` can use a managed reference or an environment AT for validation, but never persists the AT. An environment-only credential must remain available to the Agent runtime after setup.
878
893
 
@@ -903,7 +918,7 @@ Two npm packages total. `@aliyunrds/ctxdb-shared` stays separate because OpenCla
903
918
 
904
919
  ```sh
905
920
  # 1. Strip hooks + skills + (optionally) config + logs while the binary still works.
906
- ctxdb uninstall --purge-all # everything: hooks, skills, ~/.ctxdb/ctxdb.json, ~/.ctxdb/logs/
921
+ ctxdb uninstall --purge-all # hooks, skills, and all ctxdb-owned local state
907
922
 
908
923
  # 2. Then drop the npm binaries.
909
924
  npm uninstall -g @aliyunrds/ctxdb @aliyunrds/ctxdb-shared
@@ -911,6 +926,8 @@ npm uninstall -g @aliyunrds/ctxdb @aliyunrds/ctxdb-shared
911
926
 
912
927
  After step 1, `ctxdb uninstall` prints the exact `npm uninstall` command to run next — copy it from your terminal.
913
928
 
929
+ Stop running Agent sessions and transfers before purging. `--purge-all` deletes the shared credential store (public and internal logins), cache, sessions, and Skill state including local drafts. It preserves and reports unknown files. Local deletion does not revoke server sessions: log out of each login before uninstalling if you want to revoke them. Purge flags cannot be combined with `--agent`; that combination fails before changing any files.
930
+
914
931
  ### Partial removal
915
932
 
916
933
  ```sh
@@ -937,7 +954,8 @@ ctxdb uninstall --purge-logs # also deletes ~/.ctxdb/logs/
937
954
 
938
955
  - **Hook entries in `~/.qoder/settings.json`, `~/.qoderwork/settings.json`, `~/.qwenwork/settings.json`, `~/.codex/hooks.json`, `~/.claude/settings.json`, `~/.workbuddy/settings.json`, `~/.workbuddy-ai/settings.json`, and `~/.hermes/config.yaml`**: stripped precisely by marker (`_ctxdb = @aliyunrds/ctxdb`, plus legacy keys `_ctxdbQoder` / `_ctxdbPackage` and the legacy `@aliyunrds/ctxdb-qoder` value for installs predating the unified marker). Any hooks you added yourself stay. A timestamped `*.bak-ctxdb-<TS>` is written before each modification (rotation keeps the 5 most recent).
939
956
  - **Skill directories**: `~/.<agent>/skills/ctxdb/` for each set-up agent, plus legacy dirs (`ctxdb-qoder`, `rds-ctxdb-qoder`, `qoder-ctxdb`) under `~/.qoder/skills/` from older package names.
940
- - **`~/.ctxdb/` state**: only with `--purge-config` / `--purge-logs` / `--purge-all`. Defensive: `--purge-all` removes the `~/.ctxdb/` root only if it's empty after the named files are deleted (won't blanket-rm an unknown directory).
957
+ - **Hermes hook approvals**: removes ctxdb event/command entries from `~/.hermes/shell-hooks-allowlist.json`, preserving other approvals and metadata.
958
+ - **`~/.ctxdb/` state**: only with `--purge-config` / `--purge-logs` / `--purge-all`. Full purge removes `ctxdb.json`, `ctxdb.json.bak`, `credentials.json`, `logs`, `cache`, `circuit`, `sessions`, `skills`, `skill-evolution`, and interrupted config/credential writes. Unknown paths are preserved and listed; the root is removed when empty. Symlink targets are never recursively deleted. Deletion errors return a nonzero exit status and suppress the npm-uninstall hint so the cleanup can be retried.
941
959
  - **`~/.codex/config.toml` `[features].hooks`**: **NOT** reverted. Setup adds `hooks = true` so Codex will fire ctxdb's hook entries; uninstall leaves the flag alone because (a) the user may have wanted it on for non-ctxdb hooks, and (b) it's harmless when `~/.codex/hooks.json` is empty. If you want it off, edit the file by hand.
942
960
  - **`npm` itself**: never. `ctxdb uninstall` does not call your host package manager — that's the second command above.
943
961
 
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ resolveMemoryUploadAgentId
4
+ } from "./chunk-SR3M7AL5.js";
2
5
  import {
3
6
  isCircuitOpen,
4
7
  isConnectionError,
@@ -117,9 +120,9 @@ async function captureParsedMessages(parsed, cfg, client, agent = "default", ses
117
120
  const payload = {
118
121
  messages,
119
122
  user_id: cfg.userId,
123
+ agent_id: resolveMemoryUploadAgentId(cfg.agentId, agent),
120
124
  async_mode: true
121
125
  };
122
- if (cfg.agentId) payload.agent_id = cfg.agentId;
123
126
  if (cfg.appId) payload.app_id = cfg.appId;
124
127
  if (sessionId) payload.run_id = sessionId;
125
128
  let resp;
@@ -429,7 +429,7 @@ function shouldUseUpdateNotifier(argv, options = {}) {
429
429
  return false;
430
430
  }
431
431
  const command = argv[0];
432
- return command !== "update" && command !== "upgrade";
432
+ return !["update", "upgrade", "uninstall", "teardown"].includes(command);
433
433
  }
434
434
  function beginUpdateNotification(argv, options) {
435
435
  try {
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/memory-upload-agent.ts
4
+ function resolveMemoryUploadAgentId(configuredAgentId, agent = "default") {
5
+ return configuredAgentId || agent;
6
+ }
7
+
8
+ export {
9
+ resolveMemoryUploadAgentId
10
+ };
package/dist/cli/main.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  beginUpdateNotification,
4
4
  completeUpdateNotification,
5
5
  runSelfUpdate
6
- } from "../chunk-QUPLZ7KF.js";
6
+ } from "../chunk-G3MDJEX5.js";
7
7
  import {
8
8
  DEFAULT_FILE_INGEST_TIMEOUT_MS,
9
9
  DEFAULT_FILE_UPLOAD_TIMEOUT_MS,
@@ -37,6 +37,9 @@ import {
37
37
  uploadText,
38
38
  validateLocalFile
39
39
  } from "../chunk-SZ3AZDMU.js";
40
+ import {
41
+ resolveMemoryUploadAgentId
42
+ } from "../chunk-SR3M7AL5.js";
40
43
  import {
41
44
  AGENT_CHOICES,
42
45
  CtxdbError,
@@ -441,10 +444,11 @@ import {
441
444
  rmSync,
442
445
  rmdirSync,
443
446
  unlinkSync,
444
- statSync
447
+ statSync,
448
+ lstatSync
445
449
  } from "node:fs";
446
450
  import { homedir } from "node:os";
447
- import { join, dirname, resolve } from "node:path";
451
+ import { join, dirname } from "node:path";
448
452
  import { fileURLToPath } from "node:url";
449
453
  import { execFileSync } from "node:child_process";
450
454
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
@@ -842,11 +846,11 @@ async function runSetup(options) {
842
846
  return { ok: false, steps };
843
847
  }
844
848
  try {
845
- writeOpencodeShim(absEntry);
849
+ writeOpencodePlugin(absEntry);
846
850
  steps.push({
847
851
  step: "install-opencode-shim",
848
852
  ok: true,
849
- detail: `${opencodeShimPath()} \u2192 ${absEntry}`
853
+ detail: `${opencodeBundlePath()} \u2190 ${absEntry}`
850
854
  });
851
855
  } catch (err) {
852
856
  steps.push({
@@ -893,8 +897,8 @@ async function runSetup(options) {
893
897
  }
894
898
  if (ok && agent === "opencode") {
895
899
  hints.unshift(
896
- `OpenCode plugin shim: ${opencodeShimPath()}`,
897
- "Restart OpenCode to load the plugin. `ctxdb upgrade` auto-refreshes the shim after npm updates; only re-run setup if you move ctxdb's install dir."
900
+ `OpenCode plugin: ${opencodeBundlePath()}`,
901
+ "Restart OpenCode to load the plugin. Run `ctxdb upgrade --agent opencode` after npm updates to refresh the bundle and migrate older installations."
898
902
  );
899
903
  }
900
904
  if (ok && agent === "hermes") {
@@ -1119,12 +1123,7 @@ function runRemove(agent, options = {}) {
1119
1123
  }
1120
1124
  }
1121
1125
  if (agent === "opencode") {
1122
- const removed = removeOpencodeShim();
1123
- steps.push({
1124
- step: "remove-opencode-shim",
1125
- ok: true,
1126
- detail: removed ? opencodeShimPath() : "nothing to remove"
1127
- });
1126
+ steps.push(...removeOpencodeShim());
1128
1127
  const root = skillInstallRoot(agent);
1129
1128
  const dirs = [
1130
1129
  ...SKILL_DIRS.map((d) => join(root, d)),
@@ -1146,6 +1145,8 @@ function runRemove(agent, options = {}) {
1146
1145
  }
1147
1146
  if (agent === "hermes") {
1148
1147
  try {
1148
+ const removedApprovals = removeHermesHookApprovals();
1149
+ steps.push({ step: "remove-hermes-approvals", ok: true, detail: `removed ${removedApprovals} entries` });
1149
1150
  backupHermesConfig();
1150
1151
  const removed = removeHermesHooksConfig();
1151
1152
  steps.push({
@@ -1250,6 +1251,23 @@ function runTeardown(options = {}) {
1250
1251
  const purgeConfig = Boolean(options.purgeConfig || options.purgeAll);
1251
1252
  const purgeLogs = Boolean(options.purgeLogs || options.purgeAll);
1252
1253
  const purgeRoot = Boolean(options.purgeAll);
1254
+ if (purgeConfig || purgeLogs) {
1255
+ const root = join(homedir(), ".ctxdb");
1256
+ try {
1257
+ const stat = lstatSync(root);
1258
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
1259
+ throw new Error(`refusing to purge non-directory or symlinked state root: ${root}`);
1260
+ }
1261
+ if (purgeRoot) {
1262
+ const locks = readdirSync(root).filter((name) => name === "ctxdb.json.oauth-config.lock" || /^credentials\.json(?:\.[a-f0-9]{64})?\.lock$/.test(name) || name === ".skill-evolution-budget.lock");
1263
+ if (locks.length) throw new Error(`state is locked; stop related processes before retrying: ${locks.join(", ")}`);
1264
+ }
1265
+ } catch (error) {
1266
+ if (error.code !== "ENOENT") {
1267
+ return { ok: false, steps: [{ step: "purge-preflight", ok: false, detail: error.message }] };
1268
+ }
1269
+ }
1270
+ }
1253
1271
  const keepConfigShell = !purgeConfig;
1254
1272
  for (const agent of SUPPORTED_AGENTS) {
1255
1273
  const sub = runRemove(agent, { keepConfigShell });
@@ -1266,46 +1284,35 @@ function runTeardown(options = {}) {
1266
1284
  if (purgeConfig) {
1267
1285
  const cfgPath = join(homedir(), ".ctxdb", "ctxdb.json");
1268
1286
  const ownedConfigFiles = [cfgPath, `${cfgPath}.bak`];
1269
- const existing = ownedConfigFiles.filter((path) => existsSync(path));
1270
- if (existing.length === 0) {
1287
+ const start = steps.length;
1288
+ for (const path of ownedConfigFiles) {
1289
+ try {
1290
+ unlinkSync(path);
1291
+ steps.push({ step: "purge-config", ok: true, detail: path });
1292
+ } catch (err) {
1293
+ if (err.code !== "ENOENT") {
1294
+ steps.push({ step: "purge-config", ok: false, detail: `${path}: ${err.message}` });
1295
+ }
1296
+ }
1297
+ }
1298
+ if (steps.length === start) {
1271
1299
  steps.push({
1272
1300
  step: "purge-config",
1273
1301
  ok: true,
1274
1302
  detail: `nothing to remove (${cfgPath})`
1275
1303
  });
1276
- } else {
1277
- for (const path of existing) {
1278
- try {
1279
- unlinkSync(path);
1280
- steps.push({ step: "purge-config", ok: true, detail: path });
1281
- } catch (err) {
1282
- steps.push({
1283
- step: "purge-config",
1284
- ok: false,
1285
- detail: `${path}: ${err?.message ?? String(err)}`
1286
- });
1287
- }
1288
- }
1289
1304
  }
1290
1305
  }
1291
1306
  if (purgeLogs) {
1292
1307
  const logsDir = join(homedir(), ".ctxdb", "logs");
1293
- if (existsSync(logsDir)) {
1294
- try {
1295
- rmSync(logsDir, { recursive: true, force: true });
1296
- steps.push({ step: "purge-logs", ok: true, detail: logsDir });
1297
- } catch (err) {
1298
- steps.push({
1299
- step: "purge-logs",
1300
- ok: false,
1301
- detail: `${logsDir}: ${err?.message ?? String(err)}`
1302
- });
1303
- }
1304
- } else {
1308
+ try {
1309
+ rmSync(logsDir, { recursive: true, force: true });
1310
+ steps.push({ step: "purge-logs", ok: true, detail: logsDir });
1311
+ } catch (err) {
1305
1312
  steps.push({
1306
1313
  step: "purge-logs",
1307
- ok: true,
1308
- detail: `nothing to remove (${logsDir})`
1314
+ ok: false,
1315
+ detail: `${logsDir}: ${err?.message ?? String(err)}`
1309
1316
  });
1310
1317
  }
1311
1318
  }
@@ -1313,6 +1320,25 @@ function runTeardown(options = {}) {
1313
1320
  const root = join(homedir(), ".ctxdb");
1314
1321
  if (existsSync(root)) {
1315
1322
  try {
1323
+ const ownedState = /* @__PURE__ */ new Set([
1324
+ "credentials.json",
1325
+ "cache",
1326
+ "circuit",
1327
+ "sessions",
1328
+ "skills",
1329
+ "skill-evolution"
1330
+ ]);
1331
+ for (const name of readdirSync(root)) {
1332
+ const interruptedWrite = /^(?:ctxdb|credentials)\.json\.[a-f0-9]{24}\.tmp$/.test(name);
1333
+ if (!ownedState.has(name) && !interruptedWrite) continue;
1334
+ const path = join(root, name);
1335
+ try {
1336
+ rmSync(path, { recursive: true, force: true });
1337
+ steps.push({ step: "purge-state", ok: true, detail: path });
1338
+ } catch (error) {
1339
+ steps.push({ step: "purge-state", ok: false, detail: `${path}: ${error.message}` });
1340
+ }
1341
+ }
1316
1342
  const remaining = readdirSync(root);
1317
1343
  if (remaining.length === 0) {
1318
1344
  rmdirSync(root);
@@ -1321,7 +1347,7 @@ function runTeardown(options = {}) {
1321
1347
  steps.push({
1322
1348
  step: "purge-root",
1323
1349
  ok: true,
1324
- detail: `kept (not empty): ${remaining.join(", ")}`
1350
+ detail: `kept (not empty); preserved paths: ${remaining.join(", ")}`
1325
1351
  });
1326
1352
  }
1327
1353
  } catch (err) {
@@ -1347,7 +1373,7 @@ function verifyNodeBinary(nodePath) {
1347
1373
  }
1348
1374
  }
1349
1375
  function checkHookNodePaths(agent) {
1350
- if (agent === "opencode") return checkOpencodeShimHealth();
1376
+ if (agent === "opencode") return checkOpencodePluginHealth();
1351
1377
  if (agent === "hermes") return checkHermesHookHealth();
1352
1378
  const empty = {
1353
1379
  installed: false,
@@ -1419,51 +1445,35 @@ function checkHookNodePaths(agent) {
1419
1445
  }
1420
1446
  return { installed: true, nodePaths: paths, nodeOk: true, detail: `${paths[0]} (${goodVersion})` };
1421
1447
  }
1422
- function checkOpencodeShimHealth() {
1423
- const path = opencodeShimPath();
1424
- if (!existsSync(path)) {
1425
- return {
1426
- installed: false,
1427
- nodePaths: [],
1428
- nodeOk: false,
1429
- detail: `opencode shim not installed \u2014 run \`ctxdb setup --agent opencode\``
1430
- };
1431
- }
1432
- let text = "";
1433
- try {
1434
- text = readFileSync(path, "utf-8");
1435
- } catch (err) {
1448
+ function checkOpencodePluginHealth() {
1449
+ const path = opencodeBundlePath();
1450
+ if (existsSync(opencodeShimPath())) {
1436
1451
  return {
1437
1452
  installed: true,
1438
- nodePaths: [],
1453
+ nodePaths: [path],
1439
1454
  nodeOk: false,
1440
- detail: `opencode shim unreadable: ${err?.message ?? String(err)}`
1455
+ detail: "opencode legacy shim requires migration \u2014 run `ctxdb upgrade --agent opencode` and restart OpenCode"
1441
1456
  };
1442
1457
  }
1443
- const target = text.match(/export\s+\{\s*default\s*\}\s+from\s+["']([^"']+)["'];/)?.[1];
1444
- if (!target) {
1458
+ if (!existsSync(path)) {
1445
1459
  return {
1446
- installed: true,
1460
+ installed: false,
1447
1461
  nodePaths: [],
1448
1462
  nodeOk: false,
1449
- detail: `opencode shim malformed: ${path} \u2014 re-run \`ctxdb setup --agent opencode\``
1463
+ detail: "opencode plugin not installed \u2014 run `ctxdb setup --agent opencode`"
1450
1464
  };
1451
1465
  }
1452
- const targetPath = target.startsWith("file:") ? fileURLToPath(target) : resolve(dirname(path), target);
1453
- if (!existsSync(targetPath)) {
1466
+ try {
1467
+ if (!readFileSync(path, "utf-8").trim()) throw new Error("empty bundle");
1468
+ } catch (err) {
1454
1469
  return {
1455
1470
  installed: true,
1456
- nodePaths: [targetPath],
1471
+ nodePaths: [path],
1457
1472
  nodeOk: false,
1458
- detail: `opencode shim target missing: ${targetPath} \u2014 re-run \`ctxdb setup --agent opencode\``
1473
+ detail: `opencode plugin unreadable: ${err?.message ?? String(err)} \u2014 re-run \`ctxdb setup --agent opencode\``
1459
1474
  };
1460
1475
  }
1461
- return {
1462
- installed: true,
1463
- nodePaths: [targetPath],
1464
- nodeOk: true,
1465
- detail: `opencode shim: ${path} \u2192 ${targetPath}`
1466
- };
1476
+ return { installed: true, nodePaths: [path], nodeOk: true, detail: `opencode plugin: ${path}` };
1467
1477
  }
1468
1478
  var SETTINGS_BACKUP_KEEP = 5;
1469
1479
  function backupSettingsFile(path) {
@@ -1709,36 +1719,35 @@ function resolveOpencodePluginEntry() {
1709
1719
  const here = dirname(fileURLToPath(import.meta.url));
1710
1720
  const pkgRoot = walkUpToPackageJson(here, CTXDB_PACKAGE_NAMES);
1711
1721
  if (!pkgRoot) return null;
1712
- const candidates = [
1713
- join(pkgRoot, "dist", "opencode", "index.js"),
1714
- join(pkgRoot, "..", "opencode", "src", "index.ts")
1715
- ];
1716
- return candidates.find((candidate) => existsSync(candidate)) ?? null;
1722
+ const candidate = join(pkgRoot, "dist", "opencode", "index.js");
1723
+ return existsSync(candidate) ? candidate : null;
1717
1724
  }
1718
- var OPENCODE_SHIM_HEADER = "// Generated by ctxdb setup --agent opencode. Do not edit by hand.\n// To refresh after moving ctxdb's install directory, run ctxdb upgrade --agent opencode or re-run setup.\n// To remove, run: ctxdb setup --remove --agent opencode\n";
1719
- function writeOpencodeShim(absEntry) {
1725
+ var OPENCODE_GENERATED_HEADER = "// Generated by ctxdb setup --agent opencode.";
1726
+ function writeOpencodePlugin(absEntry) {
1720
1727
  const shimPath = opencodeShimPath();
1721
1728
  const bundlePath = opencodeBundlePath();
1722
- mkdirSync(dirname(shimPath), { recursive: true });
1729
+ mkdirSync(dirname(bundlePath), { recursive: true });
1730
+ if (existsSync(shimPath) && !readFileSync(shimPath, "utf-8").startsWith(OPENCODE_GENERATED_HEADER)) {
1731
+ throw new Error(`Cannot migrate unrecognized OpenCode entry ${shimPath}; move it out of plugins/ before running setup again.`);
1732
+ }
1723
1733
  copyFileSync(absEntry, bundlePath);
1724
- const importSpec = "./" + OPENCODE_BUNDLE_FILENAME;
1725
- const body = `${OPENCODE_SHIM_HEADER}export { default } from ${JSON.stringify(importSpec)};
1726
- `;
1727
- writeFileSync(shimPath, body, "utf-8");
1734
+ if (existsSync(shimPath)) unlinkSync(shimPath);
1728
1735
  }
1729
1736
  function removeOpencodeShim() {
1730
1737
  const shimPath = opencodeShimPath();
1731
1738
  const bundlePath = opencodeBundlePath();
1732
- let removed = false;
1739
+ const steps = [];
1733
1740
  for (const p of [shimPath, bundlePath]) {
1734
- if (!existsSync(p)) continue;
1735
1741
  try {
1736
1742
  unlinkSync(p);
1737
- removed = true;
1738
- } catch {
1743
+ steps.push({ step: "remove-opencode-shim", ok: true, detail: p });
1744
+ } catch (error) {
1745
+ if (error.code !== "ENOENT") {
1746
+ steps.push({ step: "remove-opencode-shim", ok: false, detail: `${p}: ${error.message}` });
1747
+ }
1739
1748
  }
1740
1749
  }
1741
- return removed;
1750
+ return steps.length ? steps : [{ step: "remove-opencode-shim", ok: true, detail: "nothing to remove" }];
1742
1751
  }
1743
1752
  var ENTRY_MARKER_KEY = "_ctxdb";
1744
1753
  var ENTRY_MARKER_VALUE = "@aliyunrds/ctxdb";
@@ -1926,6 +1935,23 @@ function removeHermesHooksConfig() {
1926
1935
  if (removed > 0) writeHermesConfig(data);
1927
1936
  return removed;
1928
1937
  }
1938
+ function removeHermesHookApprovals() {
1939
+ const path = hermesAllowlistPath();
1940
+ if (!existsSync(path)) return 0;
1941
+ const data = JSON.parse(readFileSync(path, "utf8"));
1942
+ if (!Array.isArray(data?.approvals)) throw new Error(`${path}: expected an approvals array`);
1943
+ const entries = hermesCtxdbEntriesByEvent(readHermesConfig());
1944
+ const approvals = data.approvals.filter((approval) => {
1945
+ if (!approval || !HERMES_HOOK_EVENTS.includes(approval.event) || typeof approval.command !== "string") return true;
1946
+ const configured = entries.get(approval.event)?.some((entry) => entry.command === approval.command);
1947
+ const hook = approval.event === HERMES_PRE_LLM_CALL_EVENT ? "pre" : "post";
1948
+ const generated = new RegExp(`^'[^']+' '[^']*[/\\\\]hermes-${hook}-llm-call\\.(?:js|ts)' --agent=hermes$`).test(approval.command);
1949
+ return !configured && !generated;
1950
+ });
1951
+ const removed = data.approvals.length - approvals.length;
1952
+ if (removed) writeFileSync(path, JSON.stringify({ ...data, approvals }, null, 2) + "\n", "utf8");
1953
+ return removed;
1954
+ }
1929
1955
  function firstCommandToken(command) {
1930
1956
  const s = command.trim();
1931
1957
  if (!s) return null;
@@ -2574,10 +2600,12 @@ FLAGS
2574
2600
  --agent <name> Target a single agent (${SUPPORTED_AGENT_CHOICES})
2575
2601
  --purge-config Also delete ~/.ctxdb/ctxdb.json
2576
2602
  --purge-logs Also delete ~/.ctxdb/logs/
2577
- --purge-all Delete everything under ~/.ctxdb/
2603
+ --purge-all Delete ctxdb-owned state under ~/.ctxdb/; preserve unknown files
2578
2604
  --json Machine-readable JSON output
2579
2605
 
2580
2606
  NOTE
2607
+ Purge flags cannot be combined with --agent. Local removal does not revoke
2608
+ server sessions; log out of each login before uninstalling to revoke them.
2581
2609
  Does not run \`npm uninstall\`. After uninstall completes, remove the
2582
2610
  npm package separately: npm uninstall -g ${PACKAGE_UNINSTALL_TARGETS2}
2583
2611
  `;
@@ -2589,6 +2617,10 @@ function uninstall(args) {
2589
2617
  const json = !!args.flags.json;
2590
2618
  const agentFlag = args.flags.agent;
2591
2619
  if (agentFlag !== void 0) {
2620
+ if (["purge-config", "purge-logs", "purge-all"].some((flag) => args.flags[flag])) {
2621
+ printResult({ ok: false, error: "--agent cannot be combined with --purge-config, --purge-logs, or --purge-all" }, json, "uninstall");
2622
+ return 2;
2623
+ }
2592
2624
  if (!isAgentSlug(agentFlag)) {
2593
2625
  process.stderr.write(
2594
2626
  `unknown --agent: ${String(agentFlag)} (expected one of ${SUPPORTED_AGENTS.join(" / ")} / default)
@@ -2796,7 +2828,7 @@ import { join as join3, win32 } from "node:path";
2796
2828
  import { existsSync as existsSync3 } from "node:fs";
2797
2829
 
2798
2830
  // src/skills/status.ts
2799
- import { existsSync as existsSync2, readdirSync as readdirSync2, lstatSync } from "node:fs";
2831
+ import { existsSync as existsSync2, readdirSync as readdirSync2, lstatSync as lstatSync2 } from "node:fs";
2800
2832
  import "node:path";
2801
2833
  function statusBindings(root, agent) {
2802
2834
  const dir = privatePath(root, "sessions", "bindings");
@@ -2867,7 +2899,7 @@ function localSkillStatus(bound, root) {
2867
2899
  const manifestPath = path(skills, "manifest.json");
2868
2900
  const snapshot = existsSync2(manifestPath) ? readState(root, manifestPath) : void 0;
2869
2901
  const drafts = list(path(skills, "drafts")).flatMap((name) => {
2870
- if (!lstatSync(path(skills, "drafts", name)).isDirectory()) return [];
2902
+ if (!lstatSync2(path(skills, "drafts", name)).isDirectory()) return [];
2871
2903
  const direct = path(skills, "drafts", name, "SKILL.md"), bundle = path(skills, "drafts", name, "bundle", "SKILL.md");
2872
2904
  return [direct, bundle].filter((file) => existsSync2(file));
2873
2905
  });
@@ -3845,6 +3877,7 @@ ${usage2}`, 2);
3845
3877
  const images = imageFlags.length > 0 ? await Promise.all(imageFlags.map((value2) => loadImagePart(value2, usage2))) : [];
3846
3878
  const ctx = buildContext(args);
3847
3879
  const userId = args.flags["user-id"] ?? ctx.cfg.userId;
3880
+ const agentId = resolveMemoryUploadAgentId(ctx.cfg.agentId, ctx.agent);
3848
3881
  const metadata = parseMetadataFlags(args.flags["metadata"]);
3849
3882
  if (images.length > 0) {
3850
3883
  const message = {
@@ -3855,6 +3888,7 @@ ${usage2}`, 2);
3855
3888
  const request = {
3856
3889
  messages: [message],
3857
3890
  user_id: userId,
3891
+ agent_id: agentId,
3858
3892
  infer: true
3859
3893
  };
3860
3894
  if (Object.keys(metadata).length > 0) request.metadata = metadata;
@@ -3879,6 +3913,7 @@ ${usage2}`, 2);
3879
3913
  const body = {
3880
3914
  messages: [{ role: "user", content: text }],
3881
3915
  user_id: userId,
3916
+ agent_id: agentId,
3882
3917
  async_mode: false
3883
3918
  };
3884
3919
  if (Object.keys(metadata).length > 0) body.metadata = metadata;
@@ -6115,12 +6150,14 @@ function topLevelCommands(managedCredentials) {
6115
6150
  }),
6116
6151
  option("--purge-config", "Also delete ~/.ctxdb/ctxdb.json.", { type: "boolean" }),
6117
6152
  option("--purge-logs", "Also delete ~/.ctxdb/logs/.", { type: "boolean" }),
6118
- option("--purge-all", "Delete everything under ~/.ctxdb/.", { type: "boolean" }),
6153
+ option("--purge-all", "Delete ctxdb-owned local state under ~/.ctxdb/; preserve unknown files.", { type: "boolean" }),
6119
6154
  JSON_OPTION
6120
6155
  ],
6121
6156
  warnings: [
6122
6157
  "Without --agent, ctxdb-owned hooks and Skills are removed for every supported Agent.",
6123
- "--purge-all deletes all ctxdb configuration, credentials, logs, and cache under ~/.ctxdb/.",
6158
+ "--purge-all deletes ctxdb configuration, shared credentials, logs, cache, sessions, and Skill state (including drafts) under ~/.ctxdb/; unknown files are preserved and reported.",
6159
+ "Purge flags cannot be combined with --agent. Stop running Agent sessions and transfers before purging.",
6160
+ "Local removal does not revoke server sessions; log out of each login before uninstalling to revoke them.",
6124
6161
  "This command does not remove the npm package."
6125
6162
  ],
6126
6163
  examples: [
@@ -6616,7 +6653,10 @@ var memoryGroup = group("memory", "Store, retrieve, update, and promote memories
6616
6653
  AGENT_OPTION,
6617
6654
  JSON_OPTION
6618
6655
  ],
6619
- constraints: ["At least text or one --image is required."],
6656
+ constraints: [
6657
+ "At least text or one --image is required.",
6658
+ "Uploads include agent_id: CTXDB_AGENT_ID overrides configured agent_id; otherwise the selected --agent / CTXDB_AGENT / default is used. This does not add a default search filter."
6659
+ ],
6620
6660
  examples: [
6621
6661
  { command: "ctxdb memory add <text> --no-infer --json" },
6622
6662
  { command: "ctxdb memory add --image=<image-path> --agent=codex" }
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  captureParsedMessages
4
- } from "../chunk-LASAMQQY.js";
4
+ } from "../chunk-CA7XEF4X.js";
5
+ import "../chunk-SR3M7AL5.js";
5
6
  import "../chunk-XWJAZAMN.js";
6
7
  import {
7
8
  HttpClient,
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  captureTurn
4
- } from "../chunk-LASAMQQY.js";
4
+ } from "../chunk-CA7XEF4X.js";
5
+ import "../chunk-SR3M7AL5.js";
5
6
  import "../chunk-XWJAZAMN.js";
6
7
  import {
7
8
  HttpClient,
@@ -23403,6 +23403,14 @@ function buildWarmupQuery(cwd, git) {
23403
23403
  // src/capture.ts
23404
23404
  init_define_CTXDB_DISTRIBUTION_MANIFEST();
23405
23405
  import { createHash as createHash5 } from "crypto";
23406
+
23407
+ // ../ctxdb/src/lib/memory-upload-agent.ts
23408
+ init_define_CTXDB_DISTRIBUTION_MANIFEST();
23409
+ function resolveMemoryUploadAgentId(configuredAgentId, agent = "default") {
23410
+ return configuredAgentId || agent;
23411
+ }
23412
+
23413
+ // src/capture.ts
23406
23414
  var EMPTY2 = {
23407
23415
  captured: false,
23408
23416
  reason: "",
@@ -23500,9 +23508,9 @@ async function runCapture(messages, cfg, client, timeoutMs, lastFingerprint, ses
23500
23508
  const payload = {
23501
23509
  messages: filtered,
23502
23510
  user_id: cfg.userId,
23511
+ agent_id: resolveMemoryUploadAgentId(cfg.agentId, "opencode"),
23503
23512
  async_mode: true
23504
23513
  };
23505
- if (cfg.agentId) payload.agent_id = cfg.agentId;
23506
23514
  if (cfg.appId) payload.app_id = cfg.appId;
23507
23515
  if (sessionId) payload.run_id = sessionId;
23508
23516
  let resp;
@@ -23566,12 +23574,12 @@ function touchSession(rt, sessionID) {
23566
23574
  function pruneSessions(state) {
23567
23575
  const now = Date.now();
23568
23576
  for (const [id, s] of state) {
23569
- if (now - s.touched > SESSION_TTL_MS) state.delete(id);
23577
+ if (!s.idleTask && now - s.touched > SESSION_TTL_MS) state.delete(id);
23570
23578
  }
23571
23579
  if (state.size > SESSION_MAX) {
23572
23580
  const overflow = state.size - SESSION_MAX;
23573
- const sorted = [...state.entries()].sort((a, b) => a[1].touched - b[1].touched);
23574
- for (let i = 0; i < overflow; i++) state.delete(sorted[i][0]);
23581
+ const sorted = [...state.entries()].filter(([, s]) => !s.idleTask).sort((a, b) => a[1].touched - b[1].touched);
23582
+ for (let i = 0; i < Math.min(overflow, sorted.length); i++) state.delete(sorted[i][0]);
23575
23583
  }
23576
23584
  }
23577
23585
  function extractTextPrompt(parts) {
@@ -23684,59 +23692,17 @@ async function buildHooks(input) {
23684
23692
  const sessionID = ev.properties?.sessionID;
23685
23693
  if (!sessionID) return;
23686
23694
  const state = touchSession(rt, sessionID);
23687
- let messages;
23688
- try {
23689
- const data = await withTimeout(
23690
- input.client.session.messages({
23691
- path: { id: sessionID },
23692
- // Skill evidence needs the whole paired turn; a 24-message tail can lose the user goal/calls.
23693
- query: {},
23694
- throwOnError: true
23695
- }),
23696
- CAPTURE_TIMEOUT_MS,
23697
- "session.messages"
23698
- );
23699
- messages = unwrapSessionMessagesResponse(data);
23700
- } catch (err) {
23701
- logError(rt.config, "event.fetchMessages", err);
23702
- return;
23703
- }
23704
- if (messages.length === 0) return;
23695
+ const task = (state.idleTask ?? Promise.resolve()).catch(() => {
23696
+ }).then(() => captureIdle(input, rt, state, sessionID));
23697
+ state.idleTask = task;
23705
23698
  try {
23706
- const binding = sessionBinding(sessionID, void 0, "opencode");
23707
- const latest = [...messages].reverse().find((m) => m.info?.role === "assistant");
23708
- if (latest?.info?.time?.completed && !latest.info.error && latest.info.finish === "stop") {
23709
- const usableTurns = new Set(messages.filter((m) => m.info?.role === "assistant" && m.info.time?.completed && !m.info.error && m.info.finish === "stop").map((m) => m.info.parentID));
23710
- const usable = messages.filter((m) => {
23711
- const turn = m.info?.role === "user" ? m.info.id : m.info?.parentID;
23712
- return turn !== void 0 && usableTurns.has(turn);
23713
- });
23714
- const answer = (latest.parts ?? []).filter((p) => p.type === "text" && !p.synthetic && !p.ignored).map((p) => p.text).join("\n");
23715
- await skillStopRows(binding, usable, { last_assistant_message: answer, turn_id: latest.info.parentID }, null);
23699
+ await task;
23700
+ } finally {
23701
+ if (state.idleTask === task) {
23702
+ state.idleTask = void 0;
23703
+ state.touched = Date.now();
23716
23704
  }
23717
- } catch {
23718
- logError(rt.config, "skill.checkpoint", new Error("skill_checkpoint_unavailable"));
23719
- }
23720
- if (!rt.config.autoCapture) return;
23721
- const outcome = await runCapture(
23722
- messages,
23723
- rt.config,
23724
- rt.http,
23725
- CAPTURE_TIMEOUT_MS,
23726
- state.lastFingerprint,
23727
- sessionID
23728
- );
23729
- if (outcome.captured && outcome.fingerprint) {
23730
- state.lastFingerprint = outcome.fingerprint;
23731
23705
  }
23732
- if (!outcome.captured && outcome.reason.startsWith("http_error:")) {
23733
- logError(rt.config, "event.capture", new Error(outcome.reason));
23734
- }
23735
- logDebug(
23736
- rt.config,
23737
- "event.capture",
23738
- `sessionID=${sessionID} captured=${outcome.captured} reason=${outcome.reason} count=${outcome.messageCount}`
23739
- );
23740
23706
  } catch (err) {
23741
23707
  logError(rt.config, "event", err);
23742
23708
  }
@@ -23744,6 +23710,61 @@ async function buildHooks(input) {
23744
23710
  };
23745
23711
  return hooks;
23746
23712
  }
23713
+ async function captureIdle(input, rt, state, sessionID) {
23714
+ let messages;
23715
+ try {
23716
+ const data = await withTimeout(
23717
+ input.client.session.messages({
23718
+ path: { id: sessionID },
23719
+ // Skill evidence needs the whole paired turn; a 24-message tail can lose the user goal/calls.
23720
+ query: {},
23721
+ throwOnError: true
23722
+ }),
23723
+ CAPTURE_TIMEOUT_MS,
23724
+ "session.messages"
23725
+ );
23726
+ messages = unwrapSessionMessagesResponse(data);
23727
+ } catch (err) {
23728
+ logError(rt.config, "event.fetchMessages", err);
23729
+ return;
23730
+ }
23731
+ if (messages.length === 0) return;
23732
+ try {
23733
+ const binding = sessionBinding(sessionID, void 0, "opencode");
23734
+ const latest = [...messages].reverse().find((m) => m.info?.role === "assistant");
23735
+ if (latest?.info?.time?.completed && !latest.info.error && latest.info.finish === "stop") {
23736
+ const usableTurns = new Set(messages.filter((m) => m.info?.role === "assistant" && m.info.time?.completed && !m.info.error && m.info.finish === "stop").map((m) => m.info.parentID));
23737
+ const usable = messages.filter((m) => {
23738
+ const turn = m.info?.role === "user" ? m.info.id : m.info?.parentID;
23739
+ return turn !== void 0 && usableTurns.has(turn);
23740
+ });
23741
+ const answer = (latest.parts ?? []).filter((p) => p.type === "text" && !p.synthetic && !p.ignored).map((p) => p.text).join("\n");
23742
+ await skillStopRows(binding, usable, { last_assistant_message: answer, turn_id: latest.info.parentID }, null);
23743
+ }
23744
+ } catch {
23745
+ logError(rt.config, "skill.checkpoint", new Error("skill_checkpoint_unavailable"));
23746
+ }
23747
+ if (!rt.config.autoCapture) return;
23748
+ const outcome = await runCapture(
23749
+ messages,
23750
+ rt.config,
23751
+ rt.http,
23752
+ CAPTURE_TIMEOUT_MS,
23753
+ state.lastFingerprint,
23754
+ sessionID
23755
+ );
23756
+ if (outcome.captured && outcome.fingerprint) {
23757
+ state.lastFingerprint = outcome.fingerprint;
23758
+ }
23759
+ if (!outcome.captured && outcome.reason.startsWith("http_error:")) {
23760
+ logError(rt.config, "event.capture", new Error(outcome.reason));
23761
+ }
23762
+ logDebug(
23763
+ rt.config,
23764
+ "event.capture",
23765
+ `sessionID=${sessionID} captured=${outcome.captured} reason=${outcome.reason} count=${outcome.messageCount}`
23766
+ );
23767
+ }
23747
23768
  async function searchAndLogRecall(scope, kind, query, sessionId, rt) {
23748
23769
  const result = await searchAndFormatRecall(
23749
23770
  query,
@@ -4,7 +4,7 @@ import {
4
4
  recordVersionCheckFailure,
5
5
  recordVersionCheckSuccess,
6
6
  releaseVersionCheckLock
7
- } from "../chunk-QUPLZ7KF.js";
7
+ } from "../chunk-G3MDJEX5.js";
8
8
  import "../chunk-5ITT5GSP.js";
9
9
  import "../chunk-PI2SUS3M.js";
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliyunrds/ctxdb",
3
- "version": "1.0.10",
3
+ "version": "1.0.11-beta.1",
4
4
  "type": "module",
5
5
  "description": "Unified access layer for RDS ContextDatabase: `ctxdb` CLI (memory + KB ops), one-shot multi-agent installer, per-agent config, hooks/plugins, and SKILL.md.",
6
6
  "license": "Apache-2.0",