total-recall-brain 3.41.4 → 3.41.5
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/bin/total-recall.mjs +3 -2
- package/docs/reference/cli-reference.md +18 -0
- package/frontend/dist/assets/{index-CRzjQLu7.js → index-WT3gs-x6.js} +24 -24
- package/frontend/dist/index.html +1 -1
- package/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3165 -87
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +32 -12
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +294 -46
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +190 -9
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3087 -71
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
- package/src/cli/compact.mjs +108 -0
- package/src/cli/context.mjs +52 -23
- package/src/cli/forget.mjs +2 -0
- package/src/cli/release.mjs +386 -0
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/context-compactor.mjs +143 -0
- package/src/core/preflight-gate.mjs +2 -2
- package/src/core/preflight-receipt.mjs +1 -1
- package/src/core/surface.mjs +1 -1
- package/src/server/invoke-durability.mjs +39 -0
- package/src/server/invoke-write-child.mjs +25 -0
- package/src/server/routes/invoke.mjs +7 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** Budgeted task capsules: rank rules, keep verbatim text, and account for every rule left out. */
|
|
2
|
+
|
|
3
|
+
import { estimateTokens } from './context-policy.mjs';
|
|
4
|
+
import { cosineSimilarity } from './embeddings.mjs';
|
|
5
|
+
|
|
6
|
+
const DUPLICATE_SIMILARITY = 0.92;
|
|
7
|
+
const REFERENCE_SHARE = 0.4;
|
|
8
|
+
const TOP_FULL_SHARE = 0.3;
|
|
9
|
+
const SUMMARY_MAX_CHARS = 200;
|
|
10
|
+
const SUMMARY_MIN_CHARS = 80;
|
|
11
|
+
// A period here does not end a sentence.
|
|
12
|
+
const SENTENCE_END = /(?<!\be\.g)(?<!\bi\.e)(?<!\bvs)[.!?](?=\s|$)/g;
|
|
13
|
+
|
|
14
|
+
/** The leading sentences of a rule, verbatim. Never rewords, because rewording can change a constraint. */
|
|
15
|
+
export function summarizeRule(text) {
|
|
16
|
+
const flat = String(text || '').replace(/^#+\s.*$/gm, ' ').replace(/\s+/g, ' ').trim();
|
|
17
|
+
if (flat.length <= SUMMARY_MAX_CHARS) return flat;
|
|
18
|
+
let end = 0;
|
|
19
|
+
for (const match of flat.matchAll(SENTENCE_END)) {
|
|
20
|
+
const candidate = match.index + 1;
|
|
21
|
+
if (candidate > SUMMARY_MAX_CHARS) break;
|
|
22
|
+
end = candidate;
|
|
23
|
+
if (end >= SUMMARY_MIN_CHARS) return flat.slice(0, end);
|
|
24
|
+
}
|
|
25
|
+
if (end) return flat.slice(0, end);
|
|
26
|
+
const cut = flat.lastIndexOf(' ', SUMMARY_MAX_CHARS);
|
|
27
|
+
return `${flat.slice(0, cut > 0 ? cut : SUMMARY_MAX_CHARS)}…`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Relevance alone orders the text; ties fall to importance, then slug, so output is stable. */
|
|
31
|
+
function rank(items) {
|
|
32
|
+
return [...items].sort((a, b) =>
|
|
33
|
+
(b.score || 0) - (a.score || 0) ||
|
|
34
|
+
(b.importance || 3) - (a.importance || 3) ||
|
|
35
|
+
a.slug.localeCompare(b.slug));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findDuplicates(ranked) {
|
|
39
|
+
const duplicateOf = new Map();
|
|
40
|
+
const normalize = text => String(text).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
41
|
+
ranked.forEach((item, i) => {
|
|
42
|
+
if (item.priority === 'absolute') return;
|
|
43
|
+
for (const earlier of ranked.slice(0, i)) {
|
|
44
|
+
if (duplicateOf.has(earlier.slug)) continue;
|
|
45
|
+
const same = normalize(item.body) === normalize(earlier.body) ||
|
|
46
|
+
(item.vector && earlier.vector && cosineSimilarity(item.vector, earlier.vector) >= DUPLICATE_SIMILARITY);
|
|
47
|
+
if (same) { duplicateOf.set(item.slug, earlier.slug); return; }
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return duplicateOf;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Assign each rule a tier so the printed output fits `budget`, measuring the real
|
|
55
|
+
* render after every change. `header(counts, tokens)` supplies the status lines.
|
|
56
|
+
*/
|
|
57
|
+
export function compactRules(items, { budget, expand = [], header, complete = false }) {
|
|
58
|
+
if (!Number.isFinite(budget) || budget < 1) throw new Error('Compact budget must be a positive token estimate');
|
|
59
|
+
const ranked = rank(items);
|
|
60
|
+
const known = new Set(ranked.map(item => item.slug));
|
|
61
|
+
const unknown = expand.filter(slug => !known.has(slug));
|
|
62
|
+
if (unknown.length) throw new Error(`Cannot expand rules not in this capsule: ${unknown.join(', ')}`);
|
|
63
|
+
const duplicateOf = findDuplicates(ranked.filter(item => !expand.includes(item.slug)));
|
|
64
|
+
const tiers = new Map(ranked.map(item => [item.slug, 'omitted']));
|
|
65
|
+
|
|
66
|
+
const render = () => {
|
|
67
|
+
const of = tier => ranked.filter(item => tiers.get(item.slug) === tier);
|
|
68
|
+
const full = of('full'), summary = of('summary'), reference = of('reference'), duplicate = of('duplicate');
|
|
69
|
+
const counts = { rules: ranked.length, full: full.length, summarized: summary.length, referenced: reference.length,
|
|
70
|
+
duplicate: duplicate.length, omitted: of('omitted').length };
|
|
71
|
+
const blocks = [
|
|
72
|
+
...full.map(item => item.text.trim()),
|
|
73
|
+
...summary.map(item => `[${item.slug}] ${summarizeRule(item.body)}`),
|
|
74
|
+
];
|
|
75
|
+
if (reference.length) blocks.push(`Referenced: ${reference.map(item => item.slug).join(', ')}`);
|
|
76
|
+
if (duplicate.length) blocks.push(`Duplicates: ${duplicate.map(item => `${item.slug} (of ${duplicateOf.get(item.slug)})`).join(', ')}`);
|
|
77
|
+
const body = blocks.join('\n\n---\n\n');
|
|
78
|
+
// The token count sits inside the text it counts, so iterate to a stable value.
|
|
79
|
+
let tokens = estimateTokens(body);
|
|
80
|
+
let context = '';
|
|
81
|
+
for (let i = 0; i < 5; i++) {
|
|
82
|
+
context = `${header(counts, tokens)}${body ? `\n${body}` : ''}`;
|
|
83
|
+
const measured = estimateTokens(`${context}\n`);
|
|
84
|
+
if (measured === tokens) break;
|
|
85
|
+
tokens = measured;
|
|
86
|
+
}
|
|
87
|
+
return { context, counts, tokens };
|
|
88
|
+
};
|
|
89
|
+
const used = () => render().tokens;
|
|
90
|
+
const tryTier = (item, tier, limit = budget) => {
|
|
91
|
+
const previous = tiers.get(item.slug);
|
|
92
|
+
tiers.set(item.slug, tier);
|
|
93
|
+
if (used() <= limit) return true;
|
|
94
|
+
tiers.set(item.slug, previous);
|
|
95
|
+
return false;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// A complete capsule measures its own, higher floor below, so a retry at the reported number succeeds.
|
|
99
|
+
const floor = used();
|
|
100
|
+
if (!complete && floor > budget) {
|
|
101
|
+
const error = new Error(`Budget ${budget} cannot hold the capsule header; the minimum is ${floor}`);
|
|
102
|
+
error.requiredBudget = floor;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const slug of expand) {
|
|
107
|
+
if (!tryTier(ranked.find(item => item.slug === slug), 'full')) {
|
|
108
|
+
const error = new Error(`Budget ${budget} cannot hold expanded rule ${slug}; raise --budget or expand fewer rules`);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// A complete capsule accounts for every rule before ranking spends any budget, so
|
|
113
|
+
// ranking decides how much text a rule gets and never whether it applies.
|
|
114
|
+
if (complete) {
|
|
115
|
+
for (const item of ranked) {
|
|
116
|
+
if (tiers.get(item.slug) !== 'omitted') continue;
|
|
117
|
+
tiers.set(item.slug, duplicateOf.has(item.slug) ? 'duplicate' : item.priority === 'absolute' ? 'summary' : 'reference');
|
|
118
|
+
}
|
|
119
|
+
const completeFloor = used();
|
|
120
|
+
if (completeFloor > budget) {
|
|
121
|
+
const error = new Error(`Budget ${budget} cannot hold every rule; the minimum is ${completeFloor}`);
|
|
122
|
+
error.requiredBudget = completeFloor;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const candidates = ranked.filter(item => !duplicateOf.has(item.slug) && tiers.get(item.slug) !== 'full');
|
|
127
|
+
|
|
128
|
+
// Slugs alone must not consume a small budget: the top rules need their text.
|
|
129
|
+
const referenceLimit = used() + Math.floor((budget - used()) * REFERENCE_SHARE);
|
|
130
|
+
// Absolute rules are named first, so a small budget still shows they exist.
|
|
131
|
+
const unplaced = candidates.filter(item => tiers.get(item.slug) === 'omitted');
|
|
132
|
+
const namedFirst = [...unplaced.filter(item => item.priority === 'absolute'), ...unplaced.filter(item => item.priority !== 'absolute')];
|
|
133
|
+
for (const item of namedFirst) if (!tryTier(item, 'reference', referenceLimit)) break;
|
|
134
|
+
const topLimit = used() + Math.floor((budget - used()) * TOP_FULL_SHARE);
|
|
135
|
+
for (const item of candidates) tryTier(item, 'full', topLimit);
|
|
136
|
+
for (const item of candidates) if (tiers.get(item.slug) !== 'full') tryTier(item, 'summary');
|
|
137
|
+
for (const item of candidates) if (tiers.get(item.slug) !== 'full') tryTier(item, 'full');
|
|
138
|
+
for (const item of namedFirst) if (tiers.get(item.slug) === 'omitted') tryTier(item, 'reference');
|
|
139
|
+
for (const slug of duplicateOf.keys()) tryTier(ranked.find(item => item.slug === slug), 'duplicate');
|
|
140
|
+
|
|
141
|
+
const { context, counts, tokens } = render();
|
|
142
|
+
return { context, counts, tokens, tiers: Object.fromEntries(tiers) };
|
|
143
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Rules, in order:
|
|
7
7
|
* 1. Tools that only display or discover (`meta`, `ui`) are always open.
|
|
8
8
|
* 2. Reads of skill files are open: reading them is how the preflight is earned.
|
|
9
|
-
* 3. A single plain Total Recall preflight, context, recall or help command is
|
|
9
|
+
* 3. A single plain Total Recall preflight, context, compact, recall or help command is
|
|
10
10
|
* open, so the gate can always be satisfied or repaired. Chaining, piping,
|
|
11
11
|
* substitution and redirects are not plain, so the exemption cannot carry
|
|
12
12
|
* other work and a capsule cannot be truncated through a pipe.
|
|
@@ -21,7 +21,7 @@ export const TOOL_CLASSES = ['shell', 'read', 'write', 'search', 'web', 'mcp', '
|
|
|
21
21
|
export const SWEEP_CONSULT_MS = 15 * 60 * 1000;
|
|
22
22
|
|
|
23
23
|
const SKILL_PATH = /\/\.(agent|claude)\/skills\//;
|
|
24
|
-
const SATISFYING = /^(cd\s+\S+\s*&&\s*)?(npx\s+)?(total-recall|node\s+\S*bin\/total-recall\.mjs)\s+(tr-preflight|preflight|context|recall|help|--help)(\s+[^;&|`<>$\n]*)?$/;
|
|
24
|
+
const SATISFYING = /^(cd\s+\S+\s*&&\s*)?(npx\s+)?(total-recall|node\s+\S*bin\/total-recall\.mjs)\s+(tr-preflight|preflight|context|compact|recall|help|--help)(\s+[^;&|`<>$\n]*)?$/;
|
|
25
25
|
|
|
26
26
|
/** True when the whole command is one plain Total Recall invocation that satisfies or repairs the gate. */
|
|
27
27
|
export function isSatisfyingCommand(command) {
|
|
@@ -98,7 +98,7 @@ export function acknowledge({ root, task = '', actions = 'read,edit,test', maxAg
|
|
|
98
98
|
const context = run(['context', task || '(no task given)', '--action', actions], root);
|
|
99
99
|
if (!/ready:true/.test(context.out)) {
|
|
100
100
|
return { ok: false, problems: ['total-recall context did not return ready:true',
|
|
101
|
-
context.code === 2 ? 'exit 2: capsule overflow;
|
|
101
|
+
context.code === 2 ? 'exit 2: capsule overflow; the budget cannot hold every rule even compacted, so retry with the required_budget it reports' : `context exit=${context.code ?? '?'}`] };
|
|
102
102
|
}
|
|
103
103
|
const version = (context.out.match(/version:([0-9a-f]+)/) || [])[1] || null;
|
|
104
104
|
const rules = run(['rules', 'verify'], root);
|
package/src/core/surface.mjs
CHANGED
|
@@ -427,7 +427,7 @@ Inspect all core capabilities, installed plugins, and commands.
|
|
|
427
427
|
|
|
428
428
|
2. RUN TOTAL RECALL CONTEXT BEFORE ACTING:
|
|
429
429
|
\`total-recall context "task description" --action <actions>\`
|
|
430
|
-
Read the
|
|
430
|
+
Read the returned capsule and require ready:true. Unknown rule applicability is conservatively required. Over budget it compacts automatically and every listed rule still applies: read a referenced rule with \`total-recall compact "task" --expand <slug>\` before relying on it. If it exits 2, retry with the required_budget it reports.
|
|
431
431
|
|
|
432
432
|
Total Recall is the agent's working memory, toolset, and single source of truth across all decisions. Refresh at every changed task, action or project, and after a memory/skill edit. Do not infer applicability from semantic similarity. CLI retrieval is local and works without the server. Skills have compact entrypoints; load only relevant references. Canonical vault writes use the validated CLI.
|
|
433
433
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The restricted write child runs under Node's permission model, which disables
|
|
3
|
+
* fsync. Durability is therefore the unconfined parent's job: before a write is
|
|
4
|
+
* reported as successful, it flushes every file the child changed and the
|
|
5
|
+
* directories that hold them.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
// File timestamps can round down, so a change made at the start of the run still counts.
|
|
11
|
+
const MTIME_SLACK_MS = 2000;
|
|
12
|
+
|
|
13
|
+
function flush(target) {
|
|
14
|
+
const fd = fs.openSync(target, 'r');
|
|
15
|
+
try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Flush files under `roots` modified since `sinceMs`, then their directories, and return how many files were flushed. */
|
|
19
|
+
export function syncChangedFiles(roots, sinceMs) {
|
|
20
|
+
let files = 0;
|
|
21
|
+
const walk = dir => {
|
|
22
|
+
let changed = false;
|
|
23
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
24
|
+
if (entry.isSymbolicLink()) continue;
|
|
25
|
+
const target = path.join(dir, entry.name);
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
if (walk(target)) changed = true;
|
|
28
|
+
} else if (entry.isFile() && fs.statSync(target).mtimeMs >= sinceMs - MTIME_SLACK_MS) {
|
|
29
|
+
flush(target);
|
|
30
|
+
files++;
|
|
31
|
+
changed = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (changed) flush(dir);
|
|
35
|
+
return changed;
|
|
36
|
+
};
|
|
37
|
+
for (const root of roots) if (fs.existsSync(root)) walk(root);
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
/** Structured trusted mutation entrypoint. Never accepts executable code. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// Node.js Permission Model disables fsync/fsyncSync APIs, so this child cannot
|
|
5
|
+
// flush its own writes. The unconfined parent flushes them before reporting
|
|
6
|
+
// success (invoke-durability.mjs); without that step these writes would not survive a crash.
|
|
7
|
+
if (fs.fsyncSync) {
|
|
8
|
+
const origFsyncSync = fs.fsyncSync;
|
|
9
|
+
fs.fsyncSync = (fd) => {
|
|
10
|
+
try { return origFsyncSync(fd); }
|
|
11
|
+
catch (err) {
|
|
12
|
+
if (err.message && err.message.includes('fsync API is disabled when Permission Model is enabled')) return;
|
|
13
|
+
throw err;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (fs.fsync) {
|
|
18
|
+
const origFsync = fs.fsync;
|
|
19
|
+
fs.fsync = (fd, cb) => {
|
|
20
|
+
origFsync(fd, (err) => {
|
|
21
|
+
if (err && err.message && err.message.includes('fsync API is disabled when Permission Model is enabled')) return cb?.(null);
|
|
22
|
+
return cb?.(err);
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
2
27
|
import { createConfinedFact } from '../core/confined-memory.mjs';
|
|
3
28
|
|
|
4
29
|
try {
|
|
@@ -15,6 +15,7 @@ import { composedCommandDirectories } from '../../core/composed-command-resoluti
|
|
|
15
15
|
import { invokeAuthority, authorizeInvoke, operationAllowed } from '../invoke-policy.mjs';
|
|
16
16
|
import { restrictedBrainTargets } from '../invoke-targets.mjs';
|
|
17
17
|
import { INVOKE_LIMITS, InvokeError, executeInvokeChild, requireSuccessfulRun, invokeEnvironment, absoluteTarget } from '../invoke-execution.mjs';
|
|
18
|
+
import { syncChangedFiles } from '../invoke-durability.mjs';
|
|
18
19
|
|
|
19
20
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
20
21
|
// Share across router instances in this server; do not race projection refreshes.
|
|
@@ -214,6 +215,7 @@ const HANDLERS = {
|
|
|
214
215
|
const { category, title, content, tags, scope, timeoutMs } = params;
|
|
215
216
|
if (context.authority.mode === 'restricted') {
|
|
216
217
|
const brain = context.selectedBrain;
|
|
218
|
+
const startedAt = Date.now();
|
|
217
219
|
const run = await context.run({ entry: path.join(root, 'src/server/invoke-write-child.mjs'),
|
|
218
220
|
cwd: root, env: invokeEnvironment(false), timeoutMs, nodeArgs: ['--max-old-space-size=128', '--permission',
|
|
219
221
|
`--allow-fs-read=${path.join(root, 'src')}`, `--allow-fs-read=${path.join(root, 'node_modules')}`,
|
|
@@ -231,6 +233,11 @@ const HANDLERS = {
|
|
|
231
233
|
result || { committed: 'unknown', indexed: false });
|
|
232
234
|
}
|
|
233
235
|
if (!result?.committed || !result?.indexed) throw new InvokeError(502, 'invalid-owner-output', 'Mutation returned invalid output.', { committed: 'unknown', indexed: false });
|
|
236
|
+
try {
|
|
237
|
+
syncChangedFiles([path.join(brain, 'memory-vault'), path.join(brain, 'memory-derived')], startedAt);
|
|
238
|
+
} catch {
|
|
239
|
+
throw new InvokeError(502, 'durability-unconfirmed', 'Fact was written but could not be flushed to disk; check it before retrying.', result);
|
|
240
|
+
}
|
|
234
241
|
return result;
|
|
235
242
|
}
|
|
236
243
|
if (!content || typeof content !== 'string') throw new Error('ssss.write needs content.');
|