@inetafrica/open-claudia 3.1.0 → 3.1.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.1.2 — On-demand recall query + cold-walker fallback fix
4
+
5
+ Two things fold in: a new `recall query` CLI so the working agent can ask long-term memory a question mid-task, and a latent reliability fix in the recall walker's fallback path.
6
+
7
+ - **New `open-claudia recall query "<question>"`** — the agent's side door into the seed → graph → walker pipeline. Where per-turn recall seeds on the *user's* message, this seeds the graph with the agent's OWN working context (via `--context`), walks the typed-edge graph with the **active-provider** judge, and returns the kept pack sections with why-bullets and pointers. It closes the documented cold-start hole: mid-build, when the vocabulary you're using shares no words with the pack that governs the work, ordinary recall never resurfaces it — an explicit query carrying your working context does. Read-only (no edge reinforcement), no pre-gate (an explicit ask always runs full), and fail-open (a failed judge returns the whole candidate list unjudged rather than nothing). Flags: `--context`, `--json`, `--no-walker`, `--no-episodes`, `--limit`, `--timeout`.
8
+ - **Cold-walker outputSchema 400 fix** — the recall walker prefers a warm persistent judge (Claude only) and falls back to a cold spawn otherwise. The cold path was passing an array-typed `outputSchema`, which the Anthropic API turns into a custom-tool `input_schema` and rejects with `400 input_schema.type: Input should be 'object'`. So any time the warm walker errored and fell back, recall silently 400'd and failed-open — judgement quietly disabled on the fallback path. Removed the schema entirely (the warm path never sent one, and `walk()` already extracts the JSON array from freeform text, filters to known ids, and clamps scores), so freeform is the proven contract on both paths.
9
+ - **Agent-space / OpenClaw:** new `core/recall/query.js` (`query()` + `mergeMatches`), `excerptFor` exported from the discoverer, `recall query` wired into `bin/recall.js`. New hermetic `test-recall-query.js` in the pretest chain (unjudged fail-open, judged keep-only, judged-empty, garbage fail-open, context-seed merge, blank-question reject).
10
+
11
+ ## v3.1.1 — /logout and switch-account: re-auth is finally reachable from chat
12
+
13
+ Reported live: with Claude or Codex already connected, there was **no way to sign into a different account** — `/login` showed "connected" and tapping a model just switched models, because the auth-aware picker (correctly, since v3.0.36) only starts a login flow when a provider is *not* authenticated. And `/clear_oauth_token` alone didn't help: it clears the bot-held token but the CLI credential store keeps `authStatus` reading "authenticated", so the picker still never offered a sign-in. Re-auth needs a *full* sign-out first — this release adds it.
14
+
15
+ - **New `/logout [claude|codex]`** (owner-only) — a full sign-out: runs `claude auth logout` / `codex logout` against the CLI credential store **and** (for Claude) clears the bot-held OAuth token from `.env`, process env, and vault in the same step. Partial clears were exactly the trap. Reports each step honestly (including "CLI not found — store untouched"), then offers a **Sign in** button so the fresh login is one tap away. Without an argument it shows provider buttons.
16
+ - **`/login` picker grows a 🔁 Switch account button** under each *connected* provider. Tapping it asks first — showing who is currently signed in (email where the CLI exposes it, else the auth method: OAuth token source / ChatGPT account / API key) and warning that the bot loses that provider until the new login completes — then signs out and chains straight into the fresh login flow. A stray tap can't kill a working auth: the destructive step is behind the explicit confirm.
17
+ - **`/clear_oauth_token` is now documented as the narrow tool** (bot token only); `/logout claude` is the full story.
18
+ - **Agent-space / OpenClaw:** new auth-flow primitives `logoutClaude`/`logoutCodex`/`providerLogout`/`canLogout`/`providerAccountSummary` (core/auth-flow.js), shared executor `performProviderLogout` + `startProviderLogin` (core/handlers.js), new callback family `sw:`/`lo:`/`losw:`/`li:` (core/actions.js, owner-gated). New test `test-logout.js` in the pretest chain.
19
+
3
20
  ## v3.1.0 — Memory becomes required: safe-mode boot, crash-loop rescue, and /downgrade
4
21
 
5
22
  Three waves folded into one release, graduated to a minor because boot semantics change. Wave 0: the memory substrate is now **required** — a missing `node:sqlite` boots into safe mode instead of silently degrading, a boot crash loop lands there too, and a new `/downgrade` completes the rescue-from-chat story. Wave 1: the memory-substrate revival, root-caused from a live incident — a pod ran 16 days repeating corrected branding mistakes because its entire memory substrate silently never initialized. Wave 2: the first fix batch from the 2026-07-20 blind code review (18 verified findings; deeper architectural items ship in later batches).
package/README.md CHANGED
@@ -208,10 +208,11 @@ When you select a project, the last conversation is automatically resumed. Tap "
208
208
  | Command | Description |
209
209
  |---------|-------------|
210
210
  | `/auth_status` | Reports redacted Claude auth status and whether an OAuth token is configured |
211
- | `/login` | Starts a Claude login flow, sends the URL/code, accepts paste-back codes (`/auth_code <code>`, `/cancel_auth`) |
211
+ | `/login` | Starts a Claude login flow, sends the URL/code, accepts paste-back codes (`/auth_code <code>`, `/cancel_auth`). Connected providers show a **🔁 Switch account** button that signs out and starts a fresh login |
212
+ | `/logout [claude\|codex]` | Full sign-out: clears the CLI credential store **and** (for Claude) the bot-held OAuth token, then offers a sign-in button. Without an argument, shows provider buttons |
212
213
  | `/setup_token` | Runs `claude setup-token`; the resulting OAuth token is stored without echoing it |
213
214
  | `/use_oauth_token [<token>]` | Stores `CLAUDE_CODE_OAUTH_TOKEN` for non-interactive runs; the message is deleted when possible. Without an argument, send the token as the next message |
214
- | `/clear_oauth_token` | Removes the stored OAuth token |
215
+ | `/clear_oauth_token` | Removes the stored OAuth token only (`/logout claude` for a full sign-out) |
215
216
 
216
217
  Tokens are redacted from chat output and logs.
217
218
 
package/bin/recall.js CHANGED
@@ -1,16 +1,26 @@
1
1
  // open-claudia recall-stats [--json]
2
2
  // open-claudia recall graph [--sync] [--json]
3
+ // open-claudia recall query "<question>" [--context "<text>"] [--json]
3
4
  //
4
5
  // Inspect the recall system: discoverer metrics (precision/miss-rescue/noise,
5
- // latency, cost) and the typed-edge graph (node/edge counts, sync).
6
+ // latency, cost), the typed-edge graph (node/edge counts, sync), and the
7
+ // on-demand walker query (ask long-term memory a question mid-task).
6
8
 
7
9
  const path = require("path");
8
10
 
9
11
  const HELP = `
10
- Inspect the memory recall system.
12
+ Inspect and query the memory recall system.
11
13
 
12
14
  open-claudia recall-stats [--json] Discoverer metrics summary
13
15
  open-claudia recall graph [--sync] Recall-graph stats (optionally re-sync structural edges first)
16
+ open-claudia recall query "<question>" On-demand memory lookup: FTS seeds + graph expansion +
17
+ walker judgement → kept sections with why-bullets
18
+ --context "<text>" Working context to seed alongside the question (what you're doing now)
19
+ --json Machine-readable result
20
+ --no-walker Skip the LLM judge; print the raw seed+graph candidate list
21
+ --no-episodes Skip past-conversation transcript hits
22
+ --limit <n> Pack seed width (default 6)
23
+ --timeout <ms> Walker timeout (default 25000)
14
24
 
15
25
  The discoverer engine logs one line per turn to recall-metrics.jsonl and a
16
26
  rolling summary to recall-metrics-summary.json. Switch engines per chat with
@@ -59,10 +69,66 @@ function graph(args) {
59
69
  for (const [type, n] of Object.entries(s.byType || {})) console.log(` ${type}: ${n}`);
60
70
  }
61
71
 
72
+ async function queryCmd(args) {
73
+ const flags = { context: "", json: false, noWalker: false, noEpisodes: false, limit: 0, timeout: 0 };
74
+ const positional = [];
75
+ for (let i = 0; i < args.length; i++) {
76
+ const a = args[i];
77
+ if (a === "--json") flags.json = true;
78
+ else if (a === "--no-walker") flags.noWalker = true;
79
+ else if (a === "--no-episodes") flags.noEpisodes = true;
80
+ else if (a === "--context") flags.context = args[++i] || "";
81
+ else if (a === "--limit") flags.limit = Number(args[++i]);
82
+ else if (a === "--timeout") flags.timeout = Number(args[++i]);
83
+ else positional.push(a);
84
+ }
85
+ const question = positional.join(" ").trim();
86
+ if (!question) {
87
+ console.error('Usage: open-claudia recall query "<question>" [--context "<text>"] [--json] [--no-walker] [--no-episodes] [--limit <n>] [--timeout <ms>]');
88
+ process.exit(1);
89
+ }
90
+ // The walker reads its timeout from env at module load — set before require.
91
+ if (flags.timeout > 0) process.env.RECALL_DISCOVERER_TIMEOUT_MS = String(flags.timeout);
92
+ const lib = require(path.join(__dirname, "..", "core", "recall", "query"));
93
+ let res;
94
+ try {
95
+ res = await lib.query(question, {
96
+ context: flags.context,
97
+ walker: !flags.noWalker,
98
+ episodes: !flags.noEpisodes,
99
+ limit: flags.limit || undefined,
100
+ });
101
+ } catch (e) {
102
+ console.error(`recall query failed: ${e.message}`);
103
+ process.exit(1);
104
+ }
105
+ if (flags.json) { console.log(JSON.stringify(res, null, 2)); return; }
106
+ const w = res.walker || {};
107
+ console.log(`Recall query — ${res.results.length} kept of ${res.candidates} candidate(s) (seeds: ${res.seeds.packs} pack, ${res.seeds.entities} entity, ${res.seeds.tools} tool; +${res.activated} graph-activated)`);
108
+ console.log(res.judged
109
+ ? `Walker: ${w.provider || "?"}${w.model ? `/${w.model}` : ""} (${w.source}, ${w.ms}ms)`
110
+ : `Walker: ${w.source === "off" ? "off" : "failed"} — UNJUDGED candidate list (fail-open)`);
111
+ for (const r of res.results) {
112
+ console.log("");
113
+ const score = r.score == null ? "" : ` (score ${r.score})`;
114
+ const viaTag = r.activated ? ` [via ${r.via || "graph"}]` : "";
115
+ console.log(`■ ${r.kind}: ${r.name}${score}${viaTag}`);
116
+ if (r.why) console.log(` why: ${r.why}`);
117
+ if (r.kind === "pack" && r.lines && r.lines.length) {
118
+ for (const l of r.lines) console.log(` > [${l.section}:${l.lineno}] ${l.text}`);
119
+ } else if (r.excerpt) {
120
+ console.log(` > ${r.excerpt.replace(/\n/g, " ").slice(0, 300)}`);
121
+ }
122
+ if (r.pointer) console.log(` ↳ ${r.pointer}`);
123
+ }
124
+ if (!res.results.length) console.log('\n(nothing relevant — rephrase, add --context, or use --no-walker to see raw candidates)');
125
+ }
126
+
62
127
  function run(args) {
63
128
  const sub = args[0];
64
129
  if (sub === "--help" || sub === "help") { console.log(HELP); process.exit(0); }
65
130
  if (sub === "graph") { graph(args.slice(1)); return; }
131
+ if (sub === "query") { queryCmd(args.slice(1)); return; }
66
132
  // default / "stats": metrics summary
67
133
  stats(args.filter((a) => a !== "stats"));
68
134
  }
package/core/actions.js CHANGED
@@ -22,6 +22,7 @@ const { finishOnboarding } = require("./onboarding");
22
22
  const {
23
23
  startSession, switchProviderState, startNewConversation,
24
24
  endCurrentSession, activeContinueOptions, applyProviderSetting,
25
+ performProviderLogout, startProviderLogin,
25
26
  } = require("./handlers");
26
27
  const providerRegistry = require("./providers");
27
28
  const jobs = require("./jobs");
@@ -29,6 +30,7 @@ const scheduler = require("./scheduler");
29
30
  const { sideChatCoordinator } = require("./side-chat");
30
31
  const {
31
32
  claudeProviderAuthStatus, runClaudeAuthCommand, runCodexLoginStatus, runCodexDeviceLogin,
33
+ canLogout, providerAccountSummary,
32
34
  } = require("./auth-flow");
33
35
 
34
36
  const REMOVED_PROVIDER_CALLBACK_MESSAGE = "This button is from an older Open Claudia release. Cursor Agent support has been removed. Open /backend to choose Claude Code or OpenAI Codex.";
@@ -350,6 +352,26 @@ async function handleAction(envelope) {
350
352
  await send(switched ? `Switched to ${beLabel}.\nModel: ${model}` : `Model: ${model}`);
351
353
  return;
352
354
  }
355
+ // Switch-account / sign-out / sign-in buttons (from the /login picker and
356
+ // /logout). sw: asks first — a stray tap must not kill a working auth.
357
+ if (d.startsWith("sw:") || d.startsWith("lo:") || d.startsWith("losw:") || d.startsWith("li:")) {
358
+ if (!isChatOwner(envelope.channelId)) return send("Owner only — provider auth is shared across users.");
359
+ const providerId = d.slice(d.indexOf(":") + 1);
360
+ if (!canLogout(providerId)) return send(`No sign-out flow for "${providerId}". Open /login again.`);
361
+ const label = providerId === "claude" ? "Claude" : "Codex";
362
+ if (d.startsWith("sw:")) {
363
+ const account = providerAccountSummary(providerId);
364
+ return send(
365
+ `${label} is signed in as: ${account}\n\nSign out and start a fresh sign-in? The bot loses ${label} until the new login completes.`,
366
+ { keyboard: { inline_keyboard: [[
367
+ { text: `Yes — switch ${label} account`, callback_data: `losw:${providerId}` },
368
+ { text: "Cancel", callback_data: "noop" },
369
+ ]] } },
370
+ );
371
+ }
372
+ if (d.startsWith("li:")) return startProviderLogin(providerId);
373
+ return performProviderLogout(providerId, { thenLogin: d.startsWith("losw:") });
374
+ }
353
375
  if (d.startsWith("m:")) {
354
376
  state.settings.model = d.slice(2) === "default" ? null : d.slice(2);
355
377
  await send(`Model: ${state.settings.model || "default"}`);
package/core/auth-flow.js CHANGED
@@ -494,6 +494,69 @@ async function runCodexDeviceLogin() {
494
494
  });
495
495
  }
496
496
 
497
+ // ── Logout / switch account ────────────────────────────────────────
498
+ // Full sign-out clears BOTH credential sources. A partial clear is the
499
+ // re-auth trap: authStatus stays "authenticated" via the other source, so
500
+ // the /login picker never offers a fresh sign-in (the v3.0.36 side effect).
501
+
502
+ function logoutClaude(deps = {}) {
503
+ const steps = [];
504
+ const tokenInfo = (deps.getClaudeOAuthToken || getClaudeOAuthToken)();
505
+ if (tokenInfo.value) {
506
+ (deps.clearClaudeOAuthToken || clearClaudeOAuthToken)();
507
+ steps.push(`Bot OAuth token cleared (${tokenInfo.source})`);
508
+ }
509
+ const binary = deps.binary !== undefined ? deps.binary : claudeExecutable();
510
+ if (!binary) {
511
+ steps.push("Claude CLI not found — CLI credential store untouched");
512
+ return { ok: steps.length > 1 || !!tokenInfo.value, steps };
513
+ }
514
+ const result = (deps.runCommand || runCommand)(binary, ["auth", "logout"], { env: claudeSubprocessEnv(), timeout: 15000 });
515
+ steps.push(result.ok ? "Claude CLI store signed out" : `Claude CLI logout: ${summarizeAuthOutput(result.output, false)}`);
516
+ return { ok: result.ok || !!tokenInfo.value, steps };
517
+ }
518
+
519
+ function logoutCodex(deps = {}) {
520
+ const binary = deps.binary !== undefined ? deps.binary : codexExecutable();
521
+ if (!binary) return { ok: false, steps: ["Codex CLI not found — nothing to sign out"] };
522
+ const result = (deps.runCommand || runCommand)(binary, ["logout"], { env: codexSubprocessEnv(), timeout: 15000 });
523
+ return { ok: result.ok, steps: [result.ok ? "Codex CLI store signed out" : `Codex logout: ${summarizeAuthOutput(result.output, false)}`] };
524
+ }
525
+
526
+ const PROVIDER_LOGOUTS = { claude: logoutClaude, codex: logoutCodex };
527
+
528
+ function canLogout(providerId) {
529
+ return Object.prototype.hasOwnProperty.call(PROVIDER_LOGOUTS, providerId);
530
+ }
531
+
532
+ function providerLogout(providerId, deps = {}) {
533
+ const fn = PROVIDER_LOGOUTS[providerId];
534
+ if (!fn) return { ok: false, steps: [`No sign-out flow for provider "${providerId}"`] };
535
+ return fn(deps);
536
+ }
537
+
538
+ // Short "who is signed in" line for confirm prompts — best effort, only
539
+ // what the CLI exposes.
540
+ function providerAccountSummary(providerId) {
541
+ if (providerId === "claude") {
542
+ const tokenInfo = getClaudeOAuthToken();
543
+ if (tokenInfo.value) return `OAuth token (${tokenInfo.source})`;
544
+ const output = runClaudeAuthStatusDiagnostic();
545
+ const email = (String(output).match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i) || [null])[0];
546
+ return email || "CLI credential store";
547
+ }
548
+ if (providerId === "codex") {
549
+ const status = runCodexLoginStatus();
550
+ if (!status.ok) return "not signed in";
551
+ const email = (String(status.output).match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i) || [null])[0];
552
+ if (email) return email;
553
+ if (/chatgpt/i.test(status.output)) return "ChatGPT account";
554
+ if (/api key/i.test(status.output)) return "API key";
555
+ return "CLI credential store";
556
+ }
557
+ return "unknown";
558
+ }
559
+
497
560
  module.exports = {
498
561
  CLAUDE_OAUTH_TOKEN_KEY, CLAUDE_OAUTH_VAULT_KEY,
499
562
  getClaudeOAuthToken,
@@ -527,4 +590,9 @@ module.exports = {
527
590
  sendCodexAuthStatusSummary,
528
591
  saveCodexApiKeyWithCli,
529
592
  runCodexDeviceLogin,
593
+ logoutClaude,
594
+ logoutCodex,
595
+ canLogout,
596
+ providerLogout,
597
+ providerAccountSummary,
530
598
  };
package/core/handlers.js CHANGED
@@ -48,6 +48,7 @@ const {
48
48
  runClaudeAuthCommand, clearPendingClaudeAuth,
49
49
  sendCodexAuthStatusSummary, runCodexDeviceLogin, clearPendingCodexAuth,
50
50
  saveCodexApiKeyWithCli,
51
+ canLogout, providerLogout, providerAccountSummary,
51
52
  } = require("./auth-flow");
52
53
  const providerRegistry = require("./providers");
53
54
 
@@ -1086,13 +1087,16 @@ function buildModelPicker({ authAware = false, state = currentState(), registry
1086
1087
  const rows = [];
1087
1088
  for (const provider of registry.listProviders()) {
1088
1089
  let authMarker = "";
1090
+ let connected = false;
1089
1091
  if (authAware) {
1090
- let connected = false;
1091
1092
  try { connected = provider.authStatus().state === "authenticated"; } catch (_) {}
1092
1093
  authMarker = connected ? " · connected" : " · tap to connect";
1093
1094
  }
1094
1095
  const availableMarker = providerAvailability(provider) ? "" : " · unavailable";
1095
1096
  rows.push([{ text: `── ${provider.label}${availableMarker}${authMarker} ──`, callback_data: "noop" }]);
1097
+ if (authAware && connected && canLogout(provider.id)) {
1098
+ rows.push([{ text: "🔁 Switch account", callback_data: `sw:${provider.id}` }]);
1099
+ }
1096
1100
  const choices = provider.modelChoices();
1097
1101
  for (let index = 0; index < choices.length; index += 3) {
1098
1102
  rows.push(choices.slice(index, index + 3).map((model) => ({
@@ -1928,6 +1932,43 @@ register({
1928
1932
  },
1929
1933
  });
1930
1934
 
1935
+ async function startProviderLogin(providerId) {
1936
+ if (providerId === "claude") return runClaudeAuthCommand(["auth", "login", "--claudeai"], "Claude login");
1937
+ if (providerId === "codex") return runCodexDeviceLogin();
1938
+ return send(`No login flow for provider "${providerId}".`);
1939
+ }
1940
+
1941
+ async function performProviderLogout(providerId, { thenLogin = false } = {}) {
1942
+ const provider = providerFromRegistry(providerId);
1943
+ const label = provider ? provider.label : providerId;
1944
+ const result = providerLogout(providerId);
1945
+ await send(`${label} sign-out:\n${result.steps.map((s) => `• ${s}`).join("\n")}`);
1946
+ if (thenLogin) return startProviderLogin(providerId);
1947
+ await send(`Sign in with a different account when ready — tap below or use /login.`, {
1948
+ keyboard: { inline_keyboard: [[{ text: `Sign in ${label}`, callback_data: `li:${providerId}` }]] },
1949
+ });
1950
+ }
1951
+
1952
+ module.exports.startProviderLogin = startProviderLogin;
1953
+ module.exports.performProviderLogout = performProviderLogout;
1954
+
1955
+ register({
1956
+ name: "logout", description: "Sign out of a provider (full: CLI store + bot token)", args: "[claude|codex]", ownerOnly: true,
1957
+ handler: async (env, { tail }) => {
1958
+ if (!ownerEnv(env)) return send("Owner only — provider auth is shared across users.");
1959
+ const raw = (tail || "").trim().toLowerCase();
1960
+ const target = ({ gpt: "codex", openai: "codex", chatgpt: "codex", anthropic: "claude" })[raw] || raw;
1961
+ if (canLogout(target)) return performProviderLogout(target);
1962
+ if (target) return send(`Unknown provider "${target}". Use /logout claude or /logout codex.`);
1963
+ return send("Sign out of which provider? This clears the CLI credential store (and Claude's bot token) so /login offers a fresh sign-in.", {
1964
+ keyboard: { inline_keyboard: [[
1965
+ { text: "Claude", callback_data: "lo:claude" },
1966
+ { text: "Codex", callback_data: "lo:codex" },
1967
+ ]] },
1968
+ });
1969
+ },
1970
+ });
1971
+
1931
1972
  register({
1932
1973
  name: "setup_token", description: "Create Claude OAuth token", ownerOnly: true,
1933
1974
  handler: async (env) => {
@@ -282,6 +282,13 @@ async function runWalker(prompt, options = {}) {
282
282
  };
283
283
  } catch (e) { /* fall back to cold spawn below */ }
284
284
  }
285
+ // No outputSchema: a provider structured-output schema becomes a custom-tool
286
+ // input_schema, which the Anthropic API requires to be an OBJECT — the
287
+ // walker's top-level array schema is rejected 400 ("input_schema.type: Input
288
+ // should be 'object'"), so the cold path used to fail-open on every fallback.
289
+ // The warm path never sends a schema and the walk() parser already extracts
290
+ // the JSON array from freeform text, filters to known ids, and clamps scores,
291
+ // so freeform is the proven contract on both paths.
285
292
  const result = await runUtility(prompt, {
286
293
  purpose: "recall",
287
294
  provider: selection.providerId,
@@ -291,14 +298,6 @@ async function runWalker(prompt, options = {}) {
291
298
  legacyClaudeModelEnv: "RECALL_DISCOVERER_MODEL",
292
299
  systemPrompt: WALKER_SYSTEM,
293
300
  readOnly: true,
294
- outputSchema: {
295
- type: "array",
296
- items: {
297
- type: "object",
298
- required: ["id"],
299
- properties: { id: { type: "string" }, why: { type: "string" }, score: { type: "number" } },
300
- },
301
- },
302
301
  timeoutMs: WALKER_TIMEOUT_MS,
303
302
  cwd: options.cwd || process.cwd(),
304
303
  });
@@ -779,4 +778,4 @@ async function run(ctx) {
779
778
  };
780
779
  }
781
780
 
782
- module.exports = { name: "discoverer", run, runWalker, needsRecall, recallTier, walk, cascadeVerdict, cokeptPairs, scoutToolGaps, renderToolGaps, episodesAllowedForSpeaker };
781
+ module.exports = { name: "discoverer", run, runWalker, needsRecall, recallTier, walk, excerptFor, cascadeVerdict, cokeptPairs, scoutToolGaps, renderToolGaps, episodesAllowedForSpeaker };
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+
3
+ // On-demand recall: the working agent's side door into the same
4
+ // seed → graph-expansion → walker pipeline the discoverer runs per turn.
5
+ // `open-claudia recall query "<question>" [--context "<working context>"]`
6
+ //
7
+ // The per-turn engine seeds from the USER's message; mid-build the agent's
8
+ // actual question often shares zero words with the governing pack (measured
9
+ // 2026-07-16: the embeddings-measurement stance never resurfaced during the
10
+ // build it governed). Here the agent seeds with its own question + working
11
+ // context, and graph edges + walker judgement bridge the vocabulary gap.
12
+ //
13
+ // Deliberate differences from the per-turn engine:
14
+ // - no pre-gate: an explicit query is always intentional (always "full").
15
+ // - read-only: never reinforces edges, never warms co-kept pairs — a
16
+ // query is a lookup, not a turn.
17
+ // - cold walker only: a one-shot CLI process must not own a warm child.
18
+ // - fail-open returns ALL candidates marked unjudged — the caller is an
19
+ // agent that can judge for itself; an empty answer helps nobody.
20
+
21
+ const graph = require("./graph");
22
+ const metrics = require("./metrics");
23
+ const tuning = require("./tuning");
24
+ const discoverer = require("./discoverer");
25
+
26
+ let transcriptIndex = null;
27
+ try { transcriptIndex = require("../transcript-index"); } catch (e) {}
28
+
29
+ const COLD_ONLY_WARM = { isEnabled: () => false };
30
+ const EXCERPT_CHARS = 600;
31
+
32
+ function mergeMatches(primary, secondary, keyOf) {
33
+ const out = primary.map((m) => ({ ...m, origin: "user" }));
34
+ const seen = new Set(primary.map(keyOf));
35
+ for (const m of secondary) {
36
+ if (seen.has(keyOf(m))) continue;
37
+ out.push({ ...m, origin: "context" });
38
+ }
39
+ return out;
40
+ }
41
+
42
+ function clip(s, n) { return s && s.length > n ? s.slice(0, n) + "…" : (s || ""); }
43
+
44
+ async function query(question, opts = {}) {
45
+ const packsLib = opts.packsLib || require("../packs");
46
+ const entitiesLib = opts.entitiesLib || require("../entities");
47
+ const toolsLib = opts.toolsLib || require("../tools");
48
+ const started = Date.now();
49
+ const q = String(question || "").trim();
50
+ if (!q) throw new Error("recall query needs a question");
51
+ const context = String(opts.context || "").trim();
52
+ const packLimit = Number(opts.limit) > 0 ? Math.min(Number(opts.limit), 12) : 6;
53
+
54
+ // Seed via FTS on the question, then the working context (deduped).
55
+ let packSeeds = [];
56
+ let entSeeds = [];
57
+ try {
58
+ packSeeds = mergeMatches(
59
+ packsLib.matchPacks(q, { limit: packLimit }),
60
+ context ? packsLib.matchPacks(context, { limit: packLimit }) : [],
61
+ (m) => m.dir,
62
+ );
63
+ } catch (e) {}
64
+ try {
65
+ entSeeds = mergeMatches(
66
+ entitiesLib.matchEntities(q, { limit: 4 }),
67
+ context ? entitiesLib.matchEntities(context, { limit: 4 }) : [],
68
+ (m) => m.slug,
69
+ );
70
+ } catch (e) {}
71
+ const toolSeeds = new Map();
72
+ try {
73
+ for (const t of toolsLib.matchTools(q, { limit: 5 })) toolSeeds.set(t.name, { ...t, origin: "user" });
74
+ if (context) {
75
+ for (const t of toolsLib.matchTools(context, { limit: 5 })) {
76
+ if (!toolSeeds.has(t.name)) toolSeeds.set(t.name, { ...t, origin: "context" });
77
+ }
78
+ }
79
+ } catch (e) {}
80
+
81
+ // Spreading activation from seeds across the typed-edge graph.
82
+ const seedNodes = [
83
+ ...packSeeds.map((m) => ({ id: `pack:${m.dir}`, score: m.score || 2 })),
84
+ ...entSeeds.map((m) => ({ id: `entity:${m.slug}`, score: m.score || 2 })),
85
+ ];
86
+ const seedIds = new Set(seedNodes.map((s) => s.id));
87
+ let activated = new Map();
88
+ if (graph.available()) {
89
+ try { graph.syncFromCorpus(packsLib, entitiesLib); } catch (e) {}
90
+ try { activated = graph.expand(seedNodes, {}); } catch (e) {}
91
+ }
92
+
93
+ // Candidates: question seeds by score, context seeds, then graph-activated.
94
+ const seedRank = new Map();
95
+ for (const m of packSeeds) seedRank.set(`pack:${m.dir}`, { score: m.score || 2, origin: m.origin });
96
+ for (const m of entSeeds) seedRank.set(`entity:${m.slug}`, { score: m.score || 2, origin: m.origin });
97
+ const orderedIds = [...seedIds].sort((a, b) => {
98
+ const ra = seedRank.get(a) || {}, rb = seedRank.get(b) || {};
99
+ const ua = ra.origin === "user" ? 0 : 1, ub = rb.origin === "user" ? 0 : 1;
100
+ return ua - ub || (rb.score || 0) - (ra.score || 0);
101
+ });
102
+ for (const [id] of [...activated.entries()].sort((x, y) => (y[1].activation || 0) - (x[1].activation || 0))) {
103
+ if (!seedIds.has(id)) orderedIds.push(id);
104
+ }
105
+ const candidates = [];
106
+ for (const id of orderedIds.slice(0, tuning.get("walkerMaxCandidates"))) {
107
+ const base = discoverer.excerptFor(id, packsLib, entitiesLib);
108
+ if (!base) continue;
109
+ const act = activated.get(id);
110
+ candidates.push({ ...base, activated: !seedIds.has(id), via: act ? act.via : null });
111
+ }
112
+
113
+ // Tools: direct seeds plus tools declared by any candidate pack.
114
+ const candPackDirs = new Set();
115
+ for (const c of candidates) if (c.id.startsWith("pack:")) candPackDirs.add(c.id.slice(5));
116
+ const toolCand = new Map();
117
+ for (const [name] of toolSeeds) toolCand.set(name, { name, via: null });
118
+ if (candPackDirs.size) {
119
+ try {
120
+ for (const t of toolsLib.listTools()) {
121
+ if (t.pack && candPackDirs.has(t.pack) && !toolCand.has(t.name)) {
122
+ toolCand.set(t.name, { name: t.name, via: `pack:${t.pack}` });
123
+ }
124
+ }
125
+ } catch (e) {}
126
+ }
127
+ for (const c of toolCand.values()) {
128
+ const base = discoverer.excerptFor(`tool:${c.name}`, packsLib, entitiesLib);
129
+ if (!base) continue;
130
+ candidates.push({ ...base, activated: !!c.via, via: c.via });
131
+ }
132
+
133
+ // Episodes: FTS over past transcripts. The CLI has no speaker context, so
134
+ // the R6 owner gate is open by design (the CLI runs as the owner on their
135
+ // own box); bot-side callers still pass through the per-turn engine's gate.
136
+ if (opts.episodes !== false && transcriptIndex && transcriptIndex.available()) {
137
+ try {
138
+ const { hits } = transcriptIndex.search(q, { limit: tuning.get("episodeLimit") });
139
+ for (const h of (hits || [])) {
140
+ const terms = [...String(h.snip || "").matchAll(/>>(.+?)<</g)].map((m) => m[1]).slice(0, 4);
141
+ candidates.push({
142
+ id: `episode:${require("path").basename(String(h.file || ""), ".jsonl")}:${h.line}`,
143
+ name: `Past conversation${h.project ? ` (${h.project})` : ""}${h.ts ? ` ${String(h.ts).slice(0, 10)}` : ""}`,
144
+ description: "",
145
+ excerpt: clip(String(h.snip || "").replace(/>>|<</g, ""), EXCERPT_CHARS),
146
+ activated: true,
147
+ via: "transcript",
148
+ _terms: terms,
149
+ });
150
+ }
151
+ } catch (e) { /* episodic recall is best-effort */ }
152
+ }
153
+
154
+ // Walker judgement (cold spawn only). opts.walker === false skips the spawn.
155
+ let walkRes = { kept: null, scores: null, source: "off", walkerMs: 0, provider: null, model: null, costUsd: null };
156
+ if (opts.walker !== false && candidates.length) {
157
+ walkRes = await discoverer.walk(q, context, candidates, {
158
+ warmWalker: COLD_ONLY_WARM,
159
+ cwd: opts.cwd,
160
+ ...(opts.walkerOptions || {}),
161
+ });
162
+ }
163
+ const whyMap = walkRes.kept;
164
+ const scores = walkRes.scores;
165
+
166
+ const results = [];
167
+ for (const c of candidates) {
168
+ if (whyMap && !whyMap.has(c.id)) continue;
169
+ const kind = c.id.startsWith("pack:") ? "pack"
170
+ : c.id.startsWith("entity:") ? "entity"
171
+ : c.id.startsWith("tool:") ? "tool" : "episode";
172
+ const r = {
173
+ id: c.id,
174
+ kind,
175
+ name: c.name,
176
+ why: whyMap ? whyMap.get(c.id) || "" : "",
177
+ score: scores && scores.has(c.id) ? scores.get(c.id) : null,
178
+ activated: !!c.activated,
179
+ via: c.via || null,
180
+ excerpt: c.excerpt || "",
181
+ };
182
+ if (kind === "pack") {
183
+ const dir = c.id.slice(5);
184
+ r.pointer = `open-claudia pack show ${dir}`;
185
+ try {
186
+ const hits = packsLib.searchLines([q, context].filter(Boolean).join(" "), { dir, k: 8 });
187
+ if (hits && hits.length) r.lines = hits.map((h) => ({ section: h.section, lineno: h.lineno, text: h.text }));
188
+ } catch (e) { /* a pack with no line hits still shows its excerpt */ }
189
+ } else if (kind === "entity") {
190
+ r.pointer = `open-claudia entity show ${c.id.slice(7)}`;
191
+ } else if (kind === "tool") {
192
+ r.pointer = `open-claudia tool show ${c.id.slice(5)}`;
193
+ } else {
194
+ const qterms = (c._terms && c._terms.length
195
+ ? c._terms.join(" ")
196
+ : String(c.excerpt).split(/\s+/).slice(0, 4).join(" ")).replace(/"/g, "");
197
+ r.pointer = `open-claudia transcript-search "${qterms}" --all`;
198
+ }
199
+ results.push(r);
200
+ }
201
+ results.sort((a, b) => (b.score ?? -1) - (a.score ?? -1));
202
+
203
+ const out = {
204
+ question: q,
205
+ hasContext: !!context,
206
+ seeds: { packs: packSeeds.length, entities: entSeeds.length, tools: toolSeeds.size },
207
+ activated: activated.size,
208
+ candidates: candidates.length,
209
+ judged: !!whyMap,
210
+ walker: {
211
+ source: walkRes.source,
212
+ ms: walkRes.walkerMs || 0,
213
+ provider: walkRes.provider || null,
214
+ model: walkRes.model || null,
215
+ },
216
+ results,
217
+ latencyMs: Date.now() - started,
218
+ };
219
+
220
+ try {
221
+ metrics.logTurn({
222
+ engine: "query",
223
+ provider: walkRes.provider || "",
224
+ model: walkRes.model || "",
225
+ query: q,
226
+ tier: "query",
227
+ seeds: seedNodes,
228
+ activated: [...activated.entries()].map(([id, a]) => ({ id, activation: a.activation, hop: a.hop })),
229
+ cand: candidates.map((c) => ({ id: c.id, act: !!c.activated })),
230
+ kept: results.map((r) => ({ id: r.id, why: r.why, score: r.score })),
231
+ gated: false,
232
+ latencyMs: out.latencyMs,
233
+ walkerMs: walkRes.walkerMs,
234
+ walker: walkRes.source,
235
+ costUsd: walkRes.costUsd || 0,
236
+ });
237
+ } catch (e) {}
238
+
239
+ return out;
240
+ }
241
+
242
+ module.exports = { query, mergeMatches };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -9,8 +9,8 @@
9
9
  "scripts": {
10
10
  "setup": "node setup.js",
11
11
  "start": "node bot.js",
12
- "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js",
13
- "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js"
12
+ "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js",
13
+ "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js"
14
14
  },
15
15
  "files": [
16
16
  "bot.js",
@@ -146,7 +146,28 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
146
146
  // New /downgrade rolls back to the previously-running version from boot
147
147
  // history (strict semver-validated target); /upgrade//downgrade clear the
148
148
  // crash counter as deliberate fix attempts. package.json engines >=24.
149
- assert.strictEqual(pkg.version, "3.1.0", "release version must remain unchanged without explicit approval");
149
+ // 3.1.1 approved by Sumeet 2026-07-20: he reported the re-auth trap ("wjen i
150
+ // want to reauth a new account for say claude or gpt and i do /login and
151
+ // select it but i am still logged in it just shows me the same model instead
152
+ // of a reatuh and there is no logut option?") and green-lit the proposed fix
153
+ // (deferred once behind the smart-brenda slash-menu check, then resumed).
154
+ // Ships: /logout [claude|codex] full sign-out — CLI credential store AND the
155
+ // bot-held Claude token in one step (partial clears were the trap: any
156
+ // surviving source keeps authStatus "authenticated" so the /login picker
157
+ // never re-offers a sign-in); a 🔁 Switch account button on connected
158
+ // providers in /login behind an explicit confirm that names the signed-in
159
+ // account where the CLI exposes it, then chains logout → fresh login.
160
+ // 3.1.2 approved by Sumeet 2026-07-20 ("If everything you have works and is
161
+ // ready fold it in and push all out in a new version"): on-demand recall query
162
+ // CLI (`open-claudia recall query "<question>"`) — seeds the graph with the
163
+ // agent's OWN working context (not the user's words), walks it with the
164
+ // active-provider judge, returns pack sections + why-bullets; closes the
165
+ // cold-start hole where mid-build vocabulary shared no words with the governing
166
+ // pack. Also fixes a latent cold-walker bug: the warm→cold fallback sent an
167
+ // array-typed outputSchema the Anthropic API rejects 400, so every fallback
168
+ // silently failed-open — removed; freeform JSON parse is the proven contract on
169
+ // both paths.
170
+ assert.strictEqual(pkg.version, "3.1.2", "release version must remain unchanged without explicit approval");
150
171
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
151
172
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
152
173
  }