@inetafrica/open-claudia 3.0.44 β 3.1.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/CHANGELOG.md +46 -0
- package/README.md +12 -7
- package/bin/tool.js +15 -5
- package/bot.js +78 -3
- package/core/actions.js +22 -0
- package/core/auth-flow.js +68 -0
- package/core/boot-health.js +84 -0
- package/core/doctor-memory.js +213 -0
- package/core/doctor.js +5 -0
- package/core/dream.js +11 -9
- package/core/entities.js +47 -3
- package/core/handlers.js +160 -2
- package/core/keyring.js +24 -4
- package/core/memory-mutation-queue.js +42 -11
- package/core/pack-review.js +60 -4
- package/core/packs.js +47 -3
- package/core/recall/discoverer.js +22 -6
- package/core/recall/graph.js +84 -52
- package/core/recall/tuning.js +1 -2
- package/core/router.js +18 -0
- package/core/runner.js +21 -0
- package/core/safe-mode.js +227 -0
- package/core/state.js +2 -2
- package/core/system-prompt.js +43 -3
- package/core/tool-graph.js +99 -71
- package/core/tools.js +3 -2
- package/core/turn-observer.js +23 -1
- package/package.json +3 -3
- package/test-extractive-injection.js +21 -0
- package/test-memory-mutation-queue.js +33 -3
- package/test-provider-compaction.js +5 -0
- package/test-provider-core-prompt.js +3 -0
- package/test-provider-language.js +59 -1
- package/test-provider-recall.js +33 -0
- package/test-recall-discoverer.js +8 -0
- package/test-recall-graph.js +66 -11
- package/test-tool-graph.js +67 -16
- package/test-tool-manifest.js +32 -0
- package/test-tools.js +31 -0
package/core/tools.js
CHANGED
|
@@ -30,6 +30,7 @@ const fs = require("fs");
|
|
|
30
30
|
const path = require("path");
|
|
31
31
|
const crypto = require("crypto");
|
|
32
32
|
const CONFIG_DIR = require("../config-dir");
|
|
33
|
+
const { atomicWriteFileSync } = require("./fsutil");
|
|
33
34
|
|
|
34
35
|
const TOOLS_DIR = process.env.TOOLS_DIR ? path.resolve(process.env.TOOLS_DIR) : path.join(CONFIG_DIR, "tools");
|
|
35
36
|
|
|
@@ -90,7 +91,7 @@ function readUsage() {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
function writeUsage(u) {
|
|
93
|
-
try { ensureDir();
|
|
94
|
+
try { ensureDir(); atomicWriteFileSync(USAGE_FILE, JSON.stringify(u), { mode: 0o600 }); }
|
|
94
95
|
catch (e) { /* best-effort */ }
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -106,7 +107,7 @@ function writeState(name, s) {
|
|
|
106
107
|
const file = p.state || path.join(TOOLS_DIR, sanitizeName(name), "state.json");
|
|
107
108
|
try {
|
|
108
109
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
109
|
-
|
|
110
|
+
atomicWriteFileSync(file, JSON.stringify(s, null, 1), { mode: 0o600 });
|
|
110
111
|
} catch (e) { /* best-effort */ }
|
|
111
112
|
}
|
|
112
113
|
|
package/core/turn-observer.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
const { chatContext, currentAdapter } = require("./context");
|
|
14
14
|
const { send } = require("./io");
|
|
15
15
|
|
|
16
|
+
// Consecutive recall failures on one channel before the owner is alerted (B5).
|
|
17
|
+
const RECALL_FAILURE_ALERT_AFTER = 3;
|
|
18
|
+
|
|
16
19
|
function esc(s) {
|
|
17
20
|
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
18
21
|
}
|
|
@@ -71,8 +74,27 @@ function createTurnObserver({ state, store }) {
|
|
|
71
74
|
// π§ /recall + π§ /tooltrace banners, fired once the prompt bundle (and its
|
|
72
75
|
// recall metadata) exists β before the provider spawns, like v2.15.
|
|
73
76
|
const onRecallSurfaced = (metadata) => {
|
|
74
|
-
if (guarded) return;
|
|
75
77
|
const r = metadata;
|
|
78
|
+
// B5: recall fails OPEN (the turn proceeds without memory), so a persistent
|
|
79
|
+
// breakage is invisible unless someone counts. Track consecutive failures
|
|
80
|
+
// on channel state and tell the owner once per streak β a one-off transient
|
|
81
|
+
// stays quiet, an outage does not.
|
|
82
|
+
if (r && r.reason === "optional_recall_unavailable") {
|
|
83
|
+
state.recallFailureStreak = (state.recallFailureStreak || 0) + 1;
|
|
84
|
+
if (settings.showRecall) {
|
|
85
|
+
notify("banner:recall", "π§ Recall FAILED this turn β I'm answering without pack/entity/tool memory. See bot log: [recall] builder failed.");
|
|
86
|
+
}
|
|
87
|
+
if (state.recallFailureStreak === RECALL_FAILURE_ALERT_AFTER) {
|
|
88
|
+
notify("alert:recall-failure", [
|
|
89
|
+
`β οΈ Memory recall has failed ${state.recallFailureStreak} turns in a row β I'm running without pack/entity/tool context.`,
|
|
90
|
+
r.detail ? `Last error: ${r.detail}` : null,
|
|
91
|
+
"Check the bot log for \"[recall] builder failed\". I'll stay quiet until it recovers or breaks again.",
|
|
92
|
+
].filter(Boolean).join("\n"));
|
|
93
|
+
}
|
|
94
|
+
} else if (r && !r.omitted) {
|
|
95
|
+
state.recallFailureStreak = 0;
|
|
96
|
+
}
|
|
97
|
+
if (guarded) return;
|
|
76
98
|
if (!r || r.omitted) return;
|
|
77
99
|
const fmt = (arr, icon) => (arr || []).map((x) => (x.why ? `${icon} <b>${esc(x.name)}</b> β ${esc(x.why)}` : `${icon} <b>${esc(x.name)}</b>`));
|
|
78
100
|
if (settings.showRecall) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
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,7 +9,7 @@
|
|
|
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-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",
|
|
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
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"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"license": "MIT",
|
|
133
133
|
"type": "commonjs",
|
|
134
134
|
"engines": {
|
|
135
|
-
"node": ">=
|
|
135
|
+
"node": ">=24.0.0"
|
|
136
136
|
},
|
|
137
137
|
"dependencies": {
|
|
138
138
|
"node-cron": "^4.2.1",
|
|
@@ -67,4 +67,25 @@ assert.ok(/#### Stance/.test(outHeadline), "headline still shows Stance");
|
|
|
67
67
|
assert.ok(!outHeadline.includes("The ArgoCD sync for payments"), "headline does NOT inline State (unchanged legacy behavior)");
|
|
68
68
|
assert.ok(/not shown/.test(outHeadline), "headline still points to hidden sections");
|
|
69
69
|
|
|
70
|
+
// ββ LOCKED hoist: a Stance line carrying LOCKED (uppercase) is pulled under
|
|
71
|
+
// a binding-constraints header in every mode; prose "locked" stays put ββ
|
|
72
|
+
const lockedPack = {
|
|
73
|
+
...pack,
|
|
74
|
+
dir: "coders-africa",
|
|
75
|
+
sections: {
|
|
76
|
+
...pack.sections,
|
|
77
|
+
Stance: "Audience-first design.\nLOCKED: heading font is Marcellus, body font is Nunito.\nThe staging door is locked after 18:00.",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const outLockedX = formatPackForContext(lockedPack, packsLib, { extractive: true, matchedLines: [] });
|
|
81
|
+
assert.ok(/BINDING CONSTRAINTS \(LOCKED/.test(outLockedX), "extractive mode hoists LOCKED lines under the binding header");
|
|
82
|
+
assert.ok(/- LOCKED: heading font is Marcellus, body font is Nunito\./.test(outLockedX), "the LOCKED line is bulleted verbatim");
|
|
83
|
+
const bindingIdx = outLockedX.indexOf("BINDING CONSTRAINTS");
|
|
84
|
+
assert.ok(bindingIdx !== -1 && bindingIdx < outLockedX.indexOf("Audience-first design."), "binding block leads the Stance section");
|
|
85
|
+
assert.ok(outLockedX.includes("The staging door is locked after 18:00."), "lowercase 'locked' prose is not hoisted");
|
|
86
|
+
assert.strictEqual((outLockedX.match(/heading font is Marcellus/g) || []).length, 1, "hoisted line is moved, not duplicated");
|
|
87
|
+
assert.ok(/BINDING CONSTRAINTS \(LOCKED/.test(formatPackForContext(lockedPack, packsLib, { headline: true })), "headline mode hoists too");
|
|
88
|
+
assert.ok(/BINDING CONSTRAINTS \(LOCKED/.test(formatPackForContext(lockedPack, packsLib, {})), "full mode hoists too");
|
|
89
|
+
assert.ok(!/BINDING CONSTRAINTS/.test(out), "packs without LOCKED lines are untouched");
|
|
90
|
+
|
|
70
91
|
console.log("extractive injection OK");
|
|
@@ -15,6 +15,7 @@ fs.mkdirSync(process.env.OPEN_CLAUDIA_CONFIG_DIR, { recursive: true });
|
|
|
15
15
|
const {
|
|
16
16
|
MemoryMutationQueue,
|
|
17
17
|
MutationQueueError,
|
|
18
|
+
inspectLock,
|
|
18
19
|
} = require("./core/memory-mutation-queue");
|
|
19
20
|
const { atomicWriteFileSync } = require("./core/fsutil");
|
|
20
21
|
|
|
@@ -62,14 +63,12 @@ async function main() {
|
|
|
62
63
|
journalFile,
|
|
63
64
|
maxPending: 2,
|
|
64
65
|
lockPollMs: 5,
|
|
65
|
-
staleLockMs: 10,
|
|
66
66
|
});
|
|
67
67
|
const liveContender = new MemoryMutationQueue({
|
|
68
68
|
lockDir: liveLockDir,
|
|
69
69
|
journalFile,
|
|
70
70
|
maxPending: 2,
|
|
71
71
|
lockPollMs: 5,
|
|
72
|
-
staleLockMs: 10,
|
|
73
72
|
lockTimeoutMs: 50,
|
|
74
73
|
});
|
|
75
74
|
const liveMutation = liveOwner.enqueue("long-dream", async () => liveGate.promise);
|
|
@@ -92,11 +91,42 @@ async function main() {
|
|
|
92
91
|
lockDir: deadLockDir,
|
|
93
92
|
journalFile,
|
|
94
93
|
lockPollMs: 5,
|
|
95
|
-
staleLockMs: 10,
|
|
96
94
|
lockTimeoutMs: 200,
|
|
97
95
|
});
|
|
98
96
|
assert.strictEqual(await deadRecovery.enqueue("recover-dead-lock", async () => "recovered"), "recovered");
|
|
99
97
|
|
|
98
|
+
// Container pid-1 reuse: a lock left by a CRASHED incarnation of this same
|
|
99
|
+
// pid (different bootId) must be broken immediately β pid-liveness alone
|
|
100
|
+
// reads it as held forever, which starved reviewers on restarting pods.
|
|
101
|
+
const rebootLockDir = path.join(root, "reboot-stale.lock");
|
|
102
|
+
fs.mkdirSync(rebootLockDir);
|
|
103
|
+
fs.writeFileSync(path.join(rebootLockDir, "owner.json"),
|
|
104
|
+
JSON.stringify({ token: "prev", pid: process.pid, bootId: "previous-incarnation" }));
|
|
105
|
+
const rebootRecovery = new MemoryMutationQueue({
|
|
106
|
+
lockDir: rebootLockDir,
|
|
107
|
+
journalFile,
|
|
108
|
+
lockPollMs: 5,
|
|
109
|
+
lockTimeoutMs: 200,
|
|
110
|
+
});
|
|
111
|
+
assert.strictEqual(await rebootRecovery.enqueue("recover-reboot-lock", async () => "recovered"), "recovered",
|
|
112
|
+
"a lock from a previous incarnation sharing our pid is broken immediately");
|
|
113
|
+
assert.strictEqual(
|
|
114
|
+
inspectLock(rebootLockDir).held, false,
|
|
115
|
+
"inspectLock reports a released lock as not held");
|
|
116
|
+
|
|
117
|
+
// Orphan: mkdir succeeded but owner.json never landed (crash mid-acquire).
|
|
118
|
+
const orphanLockDir = path.join(root, "orphan.lock");
|
|
119
|
+
fs.mkdirSync(orphanLockDir);
|
|
120
|
+
fs.utimesSync(orphanLockDir, old, old);
|
|
121
|
+
assert.strictEqual(inspectLock(orphanLockDir).stale, true, "an aged ownerless lock dir reads as stale");
|
|
122
|
+
const orphanRecovery = new MemoryMutationQueue({
|
|
123
|
+
lockDir: orphanLockDir,
|
|
124
|
+
journalFile,
|
|
125
|
+
lockPollMs: 5,
|
|
126
|
+
lockTimeoutMs: 200,
|
|
127
|
+
});
|
|
128
|
+
assert.strictEqual(await orphanRecovery.enqueue("recover-orphan-lock", async () => "recovered"), "recovered");
|
|
129
|
+
|
|
100
130
|
const failureQueue = new MemoryMutationQueue({
|
|
101
131
|
lockDir: path.join(root, "failure.lock"),
|
|
102
132
|
journalFile,
|
|
@@ -122,6 +122,11 @@ async function immutableSuccessProbe() {
|
|
|
122
122
|
assert.strictEqual(contexts[1].permissionMode, "plan");
|
|
123
123
|
assert.strictEqual(contexts[1].maxTurns, 1);
|
|
124
124
|
assert.strictEqual(contexts[1].timeoutMs, contexts[0].timeoutMs);
|
|
125
|
+
assert.strictEqual(contexts[0].purpose, "compact-summary");
|
|
126
|
+
assert.strictEqual(contexts[0].recallQuery, null, "summary stage carries no recall query");
|
|
127
|
+
assert.strictEqual(contexts[1].purpose, "compact-seed");
|
|
128
|
+
assert.ok(String(contexts[1].recallQuery || "").includes("A sufficiently detailed condensed continuation"),
|
|
129
|
+
"seed recall query carries the condensed brief, not seed-prompt boilerplate");
|
|
125
130
|
const restrictedEnv = runner.decorateForegroundInvocation(
|
|
126
131
|
{ env: { FIXTURE_SAFE: "1" } },
|
|
127
132
|
contexts[0],
|
|
@@ -164,6 +164,9 @@ async function probe() {
|
|
|
164
164
|
assert.strictEqual(omitted.userPrompt, "RECALL FAILURE USER SENTINEL");
|
|
165
165
|
assert.strictEqual(omitted.recallMetadata.status, "omitted");
|
|
166
166
|
assert.strictEqual(omitted.recallMetadata.omitted, true);
|
|
167
|
+
assert.strictEqual(omitted.recallMetadata.reason, "optional_recall_unavailable");
|
|
168
|
+
assert.strictEqual(omitted.recallMetadata.detail, "fixture optional recall failure",
|
|
169
|
+
"the swallowed recall error surfaces in metadata for the owner alert");
|
|
167
170
|
assert.ok(omitted.coreInstructions.includes("## Delivery"));
|
|
168
171
|
assert.ok(omitted.dynamicContext.includes("## Runtime state"));
|
|
169
172
|
|
|
@@ -99,7 +99,65 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
99
99
|
// ids all forced fresh) and the refused prompt is auto-retried exactly ONCE in
|
|
100
100
|
// a fresh session behind a β»οΈ notice. A refusal on already-fresh context gets
|
|
101
101
|
// no retry (the new message itself is the problem); capture runs never retry.
|
|
102
|
-
|
|
102
|
+
// 3.0.45 approved by Sumeet 2026-07-20 ("Ok so you ready to implement?"): first
|
|
103
|
+
// fix batch from the 2026-07-20 blind code review. Recall-graph decay moves to
|
|
104
|
+
// decay-on-read (the nightly rewrite compounded β a 60d half-life emptied in
|
|
105
|
+
// ~10 nights; one-time user_version 0β1 restamp, dream-tunable half-life);
|
|
106
|
+
// keyring/tool-state/tuning writes become crash-safe (atomic temp+fsync+rename,
|
|
107
|
+
// keyring .bak snapshot with read-through repair); recall failures alert after
|
|
108
|
+
// 3 consecutive memoryless turns instead of degrading silently; the recall
|
|
109
|
+
// pre-gate strips pleasantry prefixes so "ok deploy β¦ now" recalls; tool-run
|
|
110
|
+
// creds are preflighted BEFORE an approval token is consumed (a missing key no
|
|
111
|
+
// longer burns the human's Approve); tool-graph parsing splits on | and &;
|
|
112
|
+
// raw NUL bytes in discoverer.js/state.js become \u0000 escapes; README
|
|
113
|
+
// corrected β discoverer is the shipped default engine, classic the opt-out.
|
|
114
|
+
// 3.0.45 second wave folded in per Sumeet 2026-07-20 ("i think just wok
|
|
115
|
+
// through all these features and fold them into .45?"): the memory-substrate
|
|
116
|
+
// revival. Root cause: pods on node:20 have no node:sqlite, so pack FTS, the
|
|
117
|
+
// recall graph and episodes silently never initialized β rahilcoder ran 16
|
|
118
|
+
// days with seedsTotal=0 while repeating corrected branding mistakes, and its
|
|
119
|
+
// reviewer starved 87% of runs on a stale mutation lock (container pid-1
|
|
120
|
+
// reuse made a crashed incarnation's lock look alive forever). Ships:
|
|
121
|
+
// Dockerfile.base FROM node:24-slim (+ CI build image node:24); a /doctor
|
|
122
|
+
// memory-health section (substrate hard-fail, graph stats, 24h recall
|
|
123
|
+
// pipeline, reviewer health, mutation lock, deferred reviews, corpus caps,
|
|
124
|
+
// disk) with a deduped boot-time substrate alert and a post-upgrade doctor
|
|
125
|
+
// gate on all three /upgrade paths; a pure-JS lexical seed floor under
|
|
126
|
+
// matchPacks/matchEntities (recall degrades to keyword matching, never to
|
|
127
|
+
// OFF β parity-tested with and without the sim-no-sqlite shim); the
|
|
128
|
+
// mutation-queue lock gains a boot id + ownerless-lock grace so pid-1 reuse
|
|
129
|
+
// can't wedge it, plus a reviewer retry+defer seatbelt; LOCKED Stance lines
|
|
130
|
+
// hoist under a BINDING CONSTRAINTS header and the reviewer must carry them
|
|
131
|
+
// verbatim and journal violations; compaction recall is fixed (summary
|
|
132
|
+
// meta-turn skips recall; the fresh seed matches on condensed brief + open
|
|
133
|
+
// task titles, not prompt boilerplate).
|
|
134
|
+
// 3.1.0 approved by Sumeet 2026-07-20 ("yes that's fine but if in hard boot
|
|
135
|
+
// loop it shoyuld give the user a way to get back online and diagnose or fall
|
|
136
|
+
// back into a safe mode or something to have it fixed or set up or downgrade,
|
|
137
|
+
// actually i think that's improtant as well to have a downgrade, i think add
|
|
138
|
+
// this all in and let's bump the minor to 3.1.0"): the substrate-revival
|
|
139
|
+
// fold graduates to a minor because boot semantics change. node:sqlite is now
|
|
140
|
+
// REQUIRED β a missing substrate boots into SAFE MODE (adapters + diagnostic
|
|
141
|
+
// slash commands only; no agent turns, crons, or memory writes) instead of
|
|
142
|
+
// silently degrading; MEMORY_ALLOW_DEGRADED=1 or a one-shot /safemode
|
|
143
|
+
// continue is the explicit keyword-only escape hatch. A boot crash loop (3
|
|
144
|
+
// consecutive boots dying before 5min stable uptime) also lands in safe mode
|
|
145
|
+
// so the operator keeps a live chat to rescue from instead of a dead pod.
|
|
146
|
+
// New /downgrade rolls back to the previously-running version from boot
|
|
147
|
+
// history (strict semver-validated target); /upgrade//downgrade clear the
|
|
148
|
+
// crash counter as deliberate fix attempts. package.json engines >=24.
|
|
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
|
+
assert.strictEqual(pkg.version, "3.1.1", "release version must remain unchanged without explicit approval");
|
|
103
161
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
104
162
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
105
163
|
}
|
package/test-provider-recall.js
CHANGED
|
@@ -253,6 +253,39 @@ async function main() {
|
|
|
253
253
|
recall.getEngine = originalGetEngine;
|
|
254
254
|
assert.strictEqual(admittedProvider, "codex");
|
|
255
255
|
|
|
256
|
+
// ββ compaction recall policy: the summary meta-turn skips recall entirely;
|
|
257
|
+
// the seed turn matches on the runner-supplied recallQuery (condensed
|
|
258
|
+
// brief + task titles), never the seed prompt's boilerplate ββ
|
|
259
|
+
const summarySkip = await systemPrompt.buildOptionalRecallContext(
|
|
260
|
+
"Summarize this conversation for a fresh compacted continuation.",
|
|
261
|
+
{
|
|
262
|
+
state: mutableState,
|
|
263
|
+
runContext: { provider: "claude", purpose: "compact-summary", project: { name: "project-a", dir: root }, sessionId: null, runId: "compact-sum" },
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
assert.strictEqual(summarySkip.metadata.status, "omitted", "summary turn skips recall");
|
|
267
|
+
assert.strictEqual(summarySkip.metadata.reason, "compaction_meta_turn");
|
|
268
|
+
assert.strictEqual(summarySkip.dynamicContext, "");
|
|
269
|
+
|
|
270
|
+
let seedQueryText = null;
|
|
271
|
+
recall.getEngine = () => ({
|
|
272
|
+
name: "fixture",
|
|
273
|
+
async run(ctx) {
|
|
274
|
+
seedQueryText = ctx.userText;
|
|
275
|
+
return { packBlock: "", entityBlock: "", toolBlock: "", episodeBlock: "", packMatches: [], entityMatches: [] };
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
await systemPrompt.buildOptionalRecallContext(
|
|
279
|
+
"This is a compacted continuation of a previous Open Claudia session. Treat the following summary as the prior conversation context.",
|
|
280
|
+
{
|
|
281
|
+
state: mutableState,
|
|
282
|
+
runContext: { provider: "claude", purpose: "compact-seed", recallQuery: "coders africa branding fonts\nFinalize v3.0.45", project: { name: "project-a", dir: root }, sessionId: null, runId: "compact-seed" },
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
recall.getEngine = originalGetEngine;
|
|
286
|
+
assert.strictEqual(seedQueryText, "coders africa branding fonts\nFinalize v3.0.45",
|
|
287
|
+
"seed recall matches on recallQuery, not the seed prompt");
|
|
288
|
+
|
|
256
289
|
for (const file of ["core/recall-filter.js", "core/recall/discoverer.js", "core/recall/warm-walker.js"]) {
|
|
257
290
|
const source = fs.readFileSync(path.join(__dirname, file), "utf8");
|
|
258
291
|
assert.ok(!source.includes("spawnSubagent"), `${file} no longer uses the compatibility sub-agent path`);
|
|
@@ -53,6 +53,14 @@ assert.strictEqual(disc.recallTier("restart bot", 3), "seeds", "short command
|
|
|
53
53
|
assert.strictEqual(disc.recallTier("show fup logs", 2), "seeds", "β€4 words β seeds-only tier");
|
|
54
54
|
assert.strictEqual(disc.recallTier("yes we need to see the 100", 2), "full", "substantive yes-prefixed turn β full");
|
|
55
55
|
assert.strictEqual(disc.recallTier("how do I deploy the mobile app", 4), "full", "substantive β full");
|
|
56
|
+
// B2: a pleasantry prefix is stripped BEFORE tiering, so a short real request
|
|
57
|
+
// wearing an ack is no longer swallowed by the β€4-word pleasantry skip.
|
|
58
|
+
assert.strictEqual(disc.recallTier("ok deploy kazee now", 3), "seeds", "ack-prefixed 4-word command β seeds, not skip");
|
|
59
|
+
assert.strictEqual(disc.recallTier("yes deploy it", 4), "seeds", "approval + command β seeds, not skip");
|
|
60
|
+
assert.strictEqual(disc.recallTier("thanks! restart euvy bot", 3), "seeds", "thanks-prefixed command β seeds, not skip");
|
|
61
|
+
assert.strictEqual(disc.needsRecall("ok deploy kazee now", 0), true, "prefixed command needs recall");
|
|
62
|
+
assert.strictEqual(disc.recallTier("ok thanks sounds good perfect", 5), "skip", "stacked pure pleasantries skip at any length");
|
|
63
|
+
assert.strictEqual(disc.recallTier("morning check the kazee deploys", 2), "seeds", "greeting stripped β remainder tiers as a short command");
|
|
56
64
|
|
|
57
65
|
// --- shared stub corpus + helpers ---
|
|
58
66
|
const packs = {
|
package/test-recall-graph.js
CHANGED
|
@@ -7,6 +7,9 @@ const path = require("path");
|
|
|
7
7
|
|
|
8
8
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "recall-graph-"));
|
|
9
9
|
process.env.RECALL_GRAPH_DB = path.join(tmp, "graph.db");
|
|
10
|
+
// Pin the read-time decay half-life so assertions are deterministic (env pin
|
|
11
|
+
// beats any tuning file the dream may have written).
|
|
12
|
+
process.env.RECALL_DECAY_HALF_LIFE_DAYS = "30";
|
|
10
13
|
|
|
11
14
|
const graph = require("./core/recall/graph");
|
|
12
15
|
|
|
@@ -51,18 +54,33 @@ for (const [, v] of act) assert.ok(v.activation > 0 && v.hop >= 1, "activation h
|
|
|
51
54
|
// seeds with no edges produce nothing
|
|
52
55
|
assert.strictEqual(graph.expand([{ id: "pack:orphanseed", score: 5 }]).size, 0);
|
|
53
56
|
|
|
54
|
-
// --- decay (never below structural floor) ---
|
|
55
|
-
graph.addEdge("pack:d", "pack:e", "parent"); // base weight 3, no last_reinforced β
|
|
56
|
-
graph.reinforce("pack:d", "pack:f", 10);
|
|
57
|
-
// backdate the reinforced edge well past the half-life
|
|
57
|
+
// --- decay-on-read (never below structural floor; reads never rewrite storage) ---
|
|
58
|
+
graph.addEdge("pack:d", "pack:e", "parent"); // base weight 3, no last_reinforced β never decays
|
|
59
|
+
graph.reinforce("pack:d", "pack:f", 10); // stored 11, stamped now
|
|
58
60
|
const db = graph.openDb();
|
|
59
|
-
const
|
|
60
|
-
db.prepare("UPDATE edges SET last_reinforced=? WHERE src=? AND dst=?").run(
|
|
61
|
-
graph.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
const oneHL = new Date(Date.now() - 30 * 86400000).toISOString(); // exactly one pinned half-life
|
|
62
|
+
db.prepare("UPDATE edges SET last_reinforced=? WHERE src=? AND dst=?").run(oneHL, "pack:d", "pack:f");
|
|
63
|
+
const dfEff = graph.neighbors("pack:d").find((n) => n.node === "pack:f").weight;
|
|
64
|
+
assert.ok(dfEff > 5.3 && dfEff < 5.7, `one half-life on read β 5.5 (${dfEff})`);
|
|
65
|
+
const deEff = graph.neighbors("pack:d").find((n) => n.node === "pack:e").weight;
|
|
66
|
+
assert.strictEqual(deEff, 3, "structural edge with no reinforcement never decays");
|
|
67
|
+
// far past the half-life the effective weight clamps at the type's base
|
|
68
|
+
const longAgo = new Date(Date.now() - 400 * 86400000).toISOString();
|
|
69
|
+
db.prepare("UPDATE edges SET last_reinforced=? WHERE src=? AND dst=?").run(longAgo, "pack:d", "pack:f");
|
|
70
|
+
const floored = graph.neighbors("pack:d").find((n) => n.node === "pack:f").weight;
|
|
71
|
+
assert.strictEqual(floored, 1, "effective weight clamps at the related floor");
|
|
72
|
+
// storage untouched by all those reads
|
|
73
|
+
assert.strictEqual(graph.allEdges().find((e) => e.src === "pack:d" && e.dst === "pack:f").weight, 11,
|
|
74
|
+
"reads never rewrite the stored weight");
|
|
75
|
+
|
|
76
|
+
// REGRESSION (P1 compounding decay): the nightly weight-rewrite path is gone.
|
|
77
|
+
assert.strictEqual(graph.decay, undefined, "decay() rewrite removed β decay is read-time only");
|
|
78
|
+
|
|
79
|
+
// reinforcement settles accrued decay, THEN bumps β never bumps the stale stored value
|
|
80
|
+
db.prepare("UPDATE edges SET last_reinforced=? WHERE src=? AND dst=?").run(oneHL, "pack:d", "pack:f");
|
|
81
|
+
graph.reinforce("pack:d", "pack:f", 1); // effective β 5.5 + 1
|
|
82
|
+
const settledDF = graph.allEdges().find((e) => e.src === "pack:d" && e.dst === "pack:f").weight;
|
|
83
|
+
assert.ok(settledDF > 6.3 && settledDF < 6.7, `settle-then-bump β 6.5, not 12 (${settledDF})`);
|
|
66
84
|
|
|
67
85
|
// --- structural sync from a stubbed corpus ---
|
|
68
86
|
const packsStub = {
|
|
@@ -88,7 +106,44 @@ const pruned = graph.pruneOrphans(packsStub, entStub);
|
|
|
88
106
|
assert.ok(pruned >= 1, "orphan edge pruned");
|
|
89
107
|
assert.ok(!graph.allEdges().some((e) => e.src === "pack:ghost"), "ghost edge gone");
|
|
90
108
|
|
|
109
|
+
// --- REGRESSION (P1): repeated nightly tend() must leave stored weights
|
|
110
|
+
// bit-identical β the old decay() rewrite shrank them again every night
|
|
111
|
+
// (compounding: full age reapplied to an already-decayed weight). ---
|
|
112
|
+
graph.reinforce("pack:kazee-mobile", "pack:kazee", 9); // stored 10, stamped now
|
|
113
|
+
db.prepare("UPDATE edges SET last_reinforced=? WHERE src=? AND dst=? AND type='related'")
|
|
114
|
+
.run(oneHL, "pack:kazee-mobile", "pack:kazee");
|
|
115
|
+
const kmStored = () => graph.allEdges()
|
|
116
|
+
.find((e) => e.src === "pack:kazee-mobile" && e.dst === "pack:kazee" && e.type === "related").weight;
|
|
117
|
+
const before3 = kmStored();
|
|
118
|
+
for (let i = 0; i < 3; i++) graph.tend(packsStub, entStub);
|
|
119
|
+
assert.strictEqual(kmStored(), before3, "three nightly tends never rewrite a reinforced weight");
|
|
120
|
+
const kmEff = graph.neighbors("pack:kazee-mobile")
|
|
121
|
+
.find((n) => n.node === "pack:kazee" && n.type === "related").weight;
|
|
122
|
+
assert.ok(kmEff > 4.7 && kmEff < 5.3, `effective after 3 tends is still one half-life β 5 (${kmEff})`);
|
|
123
|
+
|
|
91
124
|
// parseLinks
|
|
92
125
|
assert.deepStrictEqual(graph.parseLinks("see [[Foo-Bar]] and [[baz]]"), ["foo-bar", "baz"]);
|
|
93
126
|
|
|
127
|
+
// --- v1 migration: a legacy DB (pre decay-on-read) gets its clock restarted on
|
|
128
|
+
// first open β stored weights are treated as current, nothing else changes. ---
|
|
129
|
+
graph._resetForTest();
|
|
130
|
+
process.env.RECALL_GRAPH_DB = path.join(tmp, "legacy.db");
|
|
131
|
+
{
|
|
132
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
133
|
+
const raw = new DatabaseSync(process.env.RECALL_GRAPH_DB);
|
|
134
|
+
raw.exec(`CREATE TABLE edges (src TEXT NOT NULL, dst TEXT NOT NULL, type TEXT NOT NULL,
|
|
135
|
+
weight REAL NOT NULL DEFAULT 1, last_reinforced TEXT, created TEXT, PRIMARY KEY (src, dst, type))`);
|
|
136
|
+
raw.prepare("INSERT INTO edges (src, dst, type, weight, last_reinforced, created) VALUES (?,?,?,?,?,?)")
|
|
137
|
+
.run("pack:l1", "pack:l2", "related", 4, new Date(Date.now() - 200 * 86400000).toISOString(), new Date().toISOString());
|
|
138
|
+
raw.close();
|
|
139
|
+
}
|
|
140
|
+
delete require.cache[require.resolve("./core/recall/graph")];
|
|
141
|
+
const graph2 = require("./core/recall/graph");
|
|
142
|
+
const legacy = graph2.allEdges().find((e) => e.src === "pack:l1");
|
|
143
|
+
assert.ok(Date.parse(legacy.last_reinforced) > Date.now() - 60000, "migration restamps legacy reinforced rows");
|
|
144
|
+
assert.strictEqual(legacy.weight, 4, "migration leaves the stored weight untouched");
|
|
145
|
+
const legacyEff = graph2.neighbors("pack:l1").find((n) => n.node === "pack:l2").weight;
|
|
146
|
+
assert.ok(legacyEff > 3.9, `restamped legacy row reads at ~full stored weight (${legacyEff})`);
|
|
147
|
+
graph2._resetForTest();
|
|
148
|
+
|
|
94
149
|
console.log("recall graph OK");
|
package/test-tool-graph.js
CHANGED
|
@@ -53,25 +53,35 @@ graph.addFollow("ensure-src", "ensure-dst"); // weight 1, last_reinforced=null
|
|
|
53
53
|
const ensured = graph.followers("ensure-src");
|
|
54
54
|
assert.strictEqual(ensured[0].weight, 1, "ensure edge weighs 1");
|
|
55
55
|
|
|
56
|
-
// ββ decay: backdate a reinforced edge far into the past
|
|
56
|
+
// ββ read-time decay + prune: backdate a reinforced edge far into the past β
|
|
57
|
+
// reads hide it immediately, prune() physically deletes it (no floor) ββ
|
|
57
58
|
const db = graph.openDb();
|
|
58
59
|
const longAgo = new Date(Date.now() - 365 * 86400000).toISOString();
|
|
59
60
|
db.prepare("UPDATE tool_edges SET last_reinforced=? WHERE src=? AND dst=?").run(longAgo, "auth", "list");
|
|
60
|
-
|
|
61
|
+
assert.ok(!graph.followers("auth").some((r) => r.name === "list"), "faded edge invisible to reads before any prune");
|
|
62
|
+
let res = graph.prune({ halfLifeDays: 1, pruneBelow: 0.15 });
|
|
61
63
|
assert.ok(res.pruned >= 1, "a year-old edge under the floor is pruned");
|
|
62
|
-
assert.ok(!graph.
|
|
64
|
+
assert.ok(!graph.allEdges().some((e) => e.src === "auth" && e.dst === "list"), "pruned edge physically gone");
|
|
63
65
|
|
|
64
|
-
// the ensure edge (last_reinforced=null)
|
|
65
|
-
assert.ok(graph.followers("ensure-src").some((r) => r.name === "ensure-dst"), "un-reinforced edge survives
|
|
66
|
+
// the ensure edge (last_reinforced=null) neither fades on read nor prunes
|
|
67
|
+
assert.ok(graph.followers("ensure-src").some((r) => r.name === "ensure-dst"), "un-reinforced edge survives prune");
|
|
66
68
|
|
|
67
|
-
// ββ decay that reduces but keeps an edge
|
|
69
|
+
// ββ read-time decay that reduces but keeps an edge; storage never rewritten ββ
|
|
68
70
|
graph.addFollow("warm-a", "warm-b", { bump: 10 });
|
|
69
|
-
const
|
|
70
|
-
db.prepare("UPDATE tool_edges SET last_reinforced=? WHERE src=? AND dst=?").run(
|
|
71
|
-
graph.decay({ halfLifeDays: 30, pruneBelow: 0.15 }); // one half-life β ~5
|
|
71
|
+
const oneHL = new Date(Date.now() - 45 * 86400000).toISOString(); // one default half-life
|
|
72
|
+
db.prepare("UPDATE tool_edges SET last_reinforced=? WHERE src=? AND dst=?").run(oneHL, "warm-a", "warm-b");
|
|
72
73
|
const warm = graph.followers("warm-a")[0];
|
|
73
74
|
assert.ok(warm && warm.name === "warm-b", "warm edge survives a single half-life");
|
|
74
|
-
assert.ok(warm.weight
|
|
75
|
+
assert.ok(warm.weight > 4.7 && warm.weight < 5.3, `read-time weight β half of stored (${warm.weight})`);
|
|
76
|
+
assert.strictEqual(graph.allEdges().find((e) => e.src === "warm-a").weight, 10, "reads never rewrite the stored weight");
|
|
77
|
+
|
|
78
|
+
// ββ settle-then-bump: reinforcing a decayed edge bumps its EFFECTIVE weight ββ
|
|
79
|
+
graph.addFollow("warm-a", "warm-b", { bump: 1 }); // β 5 + 1
|
|
80
|
+
const settled = graph.allEdges().find((e) => e.src === "warm-a" && e.dst === "warm-b").weight;
|
|
81
|
+
assert.ok(settled > 5.7 && settled < 6.3, `settle-then-bump β 6, not 11 (${settled})`);
|
|
82
|
+
|
|
83
|
+
// REGRESSION (P1 compounding decay): the nightly weight-rewrite path is gone.
|
|
84
|
+
assert.strictEqual(graph.decay, undefined, "decay() rewrite removed β prune() only deletes");
|
|
75
85
|
|
|
76
86
|
// ββ removeEdge ββ
|
|
77
87
|
assert.strictEqual(graph.removeEdge("warm-a", "warm-b"), true, "removeEdge succeeds");
|
|
@@ -84,10 +94,11 @@ const removed = graph.pruneOrphans(fakeLib);
|
|
|
84
94
|
assert.ok(removed >= 1, "edge pointing at an unregistered tool ('dead') is pruned");
|
|
85
95
|
assert.ok(!graph.followers("live").length, "liveβdead removed because dead is not a live tool");
|
|
86
96
|
|
|
87
|
-
// ββ tend() is the nightly entry:
|
|
97
|
+
// ββ tend() is the nightly entry: prunes faded + orphaned edges, returns stats ββ
|
|
88
98
|
const t = graph.tend(fakeLib);
|
|
89
99
|
assert.ok(typeof t.edges === "number" && typeof t.nodes === "number", "tend returns stats");
|
|
90
|
-
assert.ok(typeof t.pruned === "number"
|
|
100
|
+
assert.ok(typeof t.pruned === "number", "tend returns prune count");
|
|
101
|
+
assert.strictEqual(t.decayed, undefined, "tend has no decay pass (decay is read-time)");
|
|
91
102
|
|
|
92
103
|
// ββ stats / allEdges sanity ββ
|
|
93
104
|
const s = graph.stats();
|
|
@@ -111,6 +122,16 @@ assert.strictEqual(runs[0].name, "spaces", "first run is spaces");
|
|
|
111
122
|
assert.strictEqual(runs[0].shape, "spaces docs --task-id <id> <path>", "first run shaped");
|
|
112
123
|
assert.strictEqual(runs[1].name, "other", "second run is other (args did not bleed across &&)");
|
|
113
124
|
assert.strictEqual(graph.parseToolRuns("ls -la").length, 0, "non-tool command yields no runs");
|
|
125
|
+
// B17: every shell composition operator splits β the single pipe was missed,
|
|
126
|
+
// so piped compositions taught no follows-edge.
|
|
127
|
+
const piped = graph.parseToolRuns("open-claudia tool run kticket list | open-claudia tool run report weekly");
|
|
128
|
+
assert.deepStrictEqual(piped.map((r) => r.name), ["kticket", "report"], "single-pipe composition parses both runs");
|
|
129
|
+
assert.strictEqual(piped[0].shape, "kticket list", "piped first run's args don't bleed into the second");
|
|
130
|
+
const bg = graph.parseToolRuns("open-claudia tool run a1 go & open-claudia tool run b2 go");
|
|
131
|
+
assert.deepStrictEqual(bg.map((r) => r.name), ["a1", "b2"], "background & splits too");
|
|
132
|
+
const quotedPipe = graph.parseToolRuns('open-claudia tool run kchat send "a | b" && open-claudia tool run other list');
|
|
133
|
+
assert.deepStrictEqual(quotedPipe.map((r) => r.name), ["kchat", "other"], "a pipe inside a quoted argument does not split");
|
|
134
|
+
assert.strictEqual(quotedPipe[0].shape, "kchat send <text>", "quoted argument still masks to <text>");
|
|
114
135
|
|
|
115
136
|
// ββ recordPackTool: insert, then bump accumulates; packTools surfaces strongest ββ
|
|
116
137
|
graph.recordPackTool("san-marco-agreements", "spaces", { command: "spaces docs --task-id <id> <path>", bump: 1 });
|
|
@@ -132,13 +153,15 @@ assert.ok(graph.toolPacks("spaces").some((u) => u.pack === "san-marco-agreements
|
|
|
132
153
|
assert.strictEqual(graph.stats().packToolEdges, graph.allPackToolEdges().length, "stats packToolEdges matches allPackToolEdges");
|
|
133
154
|
assert.ok(graph.stats().packToolEdges >= 2, "at least two packβtool edges present");
|
|
134
155
|
|
|
135
|
-
// ββ
|
|
156
|
+
// ββ prune: a year-old packβtool edge fades on read and is physically deleted ββ
|
|
136
157
|
const ptDb = graph.openDb();
|
|
137
158
|
ptDb.prepare("UPDATE pack_tool_edges SET last_reinforced=? WHERE pack=? AND tool=?")
|
|
138
159
|
.run(new Date(Date.now() - 365 * 86400000).toISOString(), "san-marco-agreements", "spaces");
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
assert.
|
|
160
|
+
assert.strictEqual(graph.packTools("san-marco-agreements").length, 0, "faded packβtool edges invisible to reads");
|
|
161
|
+
const pruneRes = graph.prune({ halfLifeDays: 1, pruneBelow: 0.15 });
|
|
162
|
+
assert.ok(pruneRes.pruned >= 1, "stale packβtool edge pruned");
|
|
163
|
+
assert.strictEqual(graph.allPackToolEdges().filter((e) => e.pack === "san-marco-agreements").length, 0,
|
|
164
|
+
"pruned packβtool rows physically gone");
|
|
142
165
|
|
|
143
166
|
// ββ pruneOrphans: drops edges for a removed tool and (with packsLib) a removed pack ββ
|
|
144
167
|
graph.recordPackTool("live-pack", "live-tool", { command: "live-tool go", bump: 1 });
|
|
@@ -152,5 +175,33 @@ assert.ok(graph.packTools("live-pack").some((u) => u.tool === "live-tool"), "liv
|
|
|
152
175
|
assert.ok(!graph.packTools("live-pack").some((u) => u.tool === "ghost-tool"), "missing-tool edge gone");
|
|
153
176
|
assert.strictEqual(graph.packTools("ghost-pack").length, 0, "missing-pack edge gone");
|
|
154
177
|
|
|
178
|
+
// ββ v1 migration: legacy DBs (pre decay-on-read) get their clocks restarted on
|
|
179
|
+
// first open β stored weights are treated as current in both tables ββ
|
|
155
180
|
graph._resetForTest();
|
|
181
|
+
process.env.TOOL_GRAPH_DB = path.join(tmp, "legacy.db");
|
|
182
|
+
{
|
|
183
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
184
|
+
const raw = new DatabaseSync(process.env.TOOL_GRAPH_DB);
|
|
185
|
+
const old = new Date(Date.now() - 200 * 86400000).toISOString();
|
|
186
|
+
raw.exec(`CREATE TABLE tool_edges (src TEXT NOT NULL, dst TEXT NOT NULL,
|
|
187
|
+
weight REAL NOT NULL DEFAULT 1, last_reinforced TEXT, created TEXT, PRIMARY KEY (src, dst))`);
|
|
188
|
+
raw.exec(`CREATE TABLE pack_tool_edges (pack TEXT NOT NULL, tool TEXT NOT NULL,
|
|
189
|
+
command TEXT NOT NULL DEFAULT '', intent TEXT NOT NULL DEFAULT '',
|
|
190
|
+
weight REAL NOT NULL DEFAULT 1, last_reinforced TEXT, created TEXT, PRIMARY KEY (pack, tool, command))`);
|
|
191
|
+
raw.prepare("INSERT INTO tool_edges (src, dst, weight, last_reinforced, created) VALUES (?,?,?,?,?)")
|
|
192
|
+
.run("l-a", "l-b", 3, old, old);
|
|
193
|
+
raw.prepare("INSERT INTO pack_tool_edges (pack, tool, command, weight, last_reinforced, created) VALUES (?,?,?,?,?,?)")
|
|
194
|
+
.run("l-pack", "l-tool", "l-tool go", 2, old, old);
|
|
195
|
+
raw.close();
|
|
196
|
+
}
|
|
197
|
+
delete require.cache[require.resolve("./core/tool-graph")];
|
|
198
|
+
const graph2 = require("./core/tool-graph");
|
|
199
|
+
const lEdge = graph2.allEdges().find((e) => e.src === "l-a");
|
|
200
|
+
const lPt = graph2.allPackToolEdges().find((e) => e.pack === "l-pack");
|
|
201
|
+
assert.ok(Date.parse(lEdge.last_reinforced) > Date.now() - 60000, "migration restamps legacy tool edges");
|
|
202
|
+
assert.ok(Date.parse(lPt.last_reinforced) > Date.now() - 60000, "migration restamps legacy packβtool edges");
|
|
203
|
+
assert.strictEqual(lEdge.weight, 3, "stored weight untouched by migration");
|
|
204
|
+
assert.strictEqual(graph2.followers("l-a")[0].weight, 3, "restamped edge reads at full stored weight");
|
|
205
|
+
graph2._resetForTest();
|
|
206
|
+
|
|
156
207
|
console.log("tool-graph OK");
|