@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 +27 -9
- package/dist/{chunk-LASAMQQY.js → chunk-CA7XEF4X.js} +4 -1
- package/dist/{chunk-QUPLZ7KF.js → chunk-G3MDJEX5.js} +1 -1
- package/dist/chunk-SR3M7AL5.js +10 -0
- package/dist/cli/main.js +137 -97
- package/dist/hooks/hermes-post-llm-call.js +2 -1
- package/dist/hooks/stop.js +2 -1
- package/dist/opencode/index.js +75 -54
- package/dist/workers/version-check.js +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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`
|
|
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 |
|
|
865
|
-
| `app_id` | str | null | Optional app-scope dimension
|
|
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
|
|
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 #
|
|
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
|
-
-
|
|
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
|
|
432
|
+
return !["update", "upgrade", "uninstall", "teardown"].includes(command);
|
|
433
433
|
}
|
|
434
434
|
function beginUpdateNotification(argv, options) {
|
|
435
435
|
try {
|
package/dist/cli/main.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
beginUpdateNotification,
|
|
4
4
|
completeUpdateNotification,
|
|
5
5
|
runSelfUpdate
|
|
6
|
-
} from "../chunk-
|
|
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
|
|
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
|
-
|
|
849
|
+
writeOpencodePlugin(absEntry);
|
|
846
850
|
steps.push({
|
|
847
851
|
step: "install-opencode-shim",
|
|
848
852
|
ok: true,
|
|
849
|
-
detail: `${
|
|
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
|
|
897
|
-
"Restart OpenCode to load the plugin. `ctxdb upgrade
|
|
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
|
-
|
|
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
|
|
1270
|
-
|
|
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
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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:
|
|
1308
|
-
detail:
|
|
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
|
|
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
|
|
1423
|
-
const path =
|
|
1424
|
-
if (
|
|
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:
|
|
1455
|
+
detail: "opencode legacy shim requires migration \u2014 run `ctxdb upgrade --agent opencode` and restart OpenCode"
|
|
1441
1456
|
};
|
|
1442
1457
|
}
|
|
1443
|
-
|
|
1444
|
-
if (!target) {
|
|
1458
|
+
if (!existsSync(path)) {
|
|
1445
1459
|
return {
|
|
1446
|
-
installed:
|
|
1460
|
+
installed: false,
|
|
1447
1461
|
nodePaths: [],
|
|
1448
1462
|
nodeOk: false,
|
|
1449
|
-
detail:
|
|
1463
|
+
detail: "opencode plugin not installed \u2014 run `ctxdb setup --agent opencode`"
|
|
1450
1464
|
};
|
|
1451
1465
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
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: [
|
|
1471
|
+
nodePaths: [path],
|
|
1457
1472
|
nodeOk: false,
|
|
1458
|
-
detail: `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
|
|
1713
|
-
|
|
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
|
|
1719
|
-
function
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1739
|
+
const steps = [];
|
|
1733
1740
|
for (const p of [shimPath, bundlePath]) {
|
|
1734
|
-
if (!existsSync(p)) continue;
|
|
1735
1741
|
try {
|
|
1736
1742
|
unlinkSync(p);
|
|
1737
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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: [
|
|
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" }
|
package/dist/hooks/stop.js
CHANGED
package/dist/opencode/index.js
CHANGED
|
@@ -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
|
-
|
|
23688
|
-
|
|
23689
|
-
|
|
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
|
-
|
|
23707
|
-
|
|
23708
|
-
if (
|
|
23709
|
-
|
|
23710
|
-
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliyunrds/ctxdb",
|
|
3
|
-
"version": "1.0.
|
|
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",
|