total-recall-brain 3.43.3 → 3.44.0
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/docs/reference/cli-reference.md +3 -1
- package/package.json +3 -3
- package/scaffold/.agent/skills/plugin/SKILL.md +7 -2
- package/scaffold/.agent/skills/plugin/evals/README.md +5 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/07-missing-reuse.mjs +11 -0
- package/scaffold/.agent/skills/plugin/evals/cases/08-unresolved-owner-command.mjs +12 -0
- package/scaffold/.agent/skills/plugin/evals/cases/09-missing-brick.mjs +12 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +4 -3
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +1 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +12 -2
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +59 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +8 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +14 -13
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-gh-login.mjs +102 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +18 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +157 -13
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +161 -17
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +14 -13
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +14 -13
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +14 -13
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +14 -13
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +14 -13
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +38 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +38 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +14 -13
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +85 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +104 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +12 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +15 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +38 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +42 -11
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +40 -13
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +249 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +3 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +16 -8
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +1 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +5 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +27 -11
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +28 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +110 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +322 -40
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +14 -13
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +14 -13
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/assets/scaffold.json +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +1 -1
- package/src/cli/connect.mjs +2 -29
- package/src/cli/context.mjs +1 -0
- package/src/cli/ingest-okf.mjs +4 -4
- package/src/cli/release.mjs +4 -1
- package/src/cli/update.mjs +4 -1
- package/src/core/automatic-brain-updates.mjs +11 -6
- package/src/core/brain-updates.mjs +11 -1
- package/src/core/context-compactor.mjs +7 -2
- package/src/core/context-compiler.mjs +6 -1
- package/src/core/context-policy.mjs +7 -3
- package/src/core/daemon-control.mjs +22 -0
- package/src/core/global-brain-updates.mjs +1 -1
- package/src/core/mesh-brain-updates.mjs +2 -1
- package/src/core/server-restart.mjs +128 -7
- package/src/core/ssss-kernel-bridge.mjs +1 -2
- package/src/core/surface.mjs +3 -3
- package/src/core/task-context.mjs +9 -8
- package/src/core/validated-write.mjs +4 -0
- package/src/core/vault-files.mjs +8 -2
- package/src/core/vault.mjs +1 -1
- package/src/server/index.mjs +3 -1
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
* Exiting an unsupervised server is not a restart, it is an outage, so
|
|
13
13
|
* supervision is proven (the job's reported PID is ours) rather than assumed.
|
|
14
14
|
*/
|
|
15
|
-
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
+
import { processIdentity } from './startup-health.mjs';
|
|
19
|
+
import { runningServerStatus } from './running-server.mjs';
|
|
20
|
+
|
|
21
|
+
const GLOBAL_PACKAGE = 'total-recall-brain';
|
|
22
|
+
const HOLD_LIMIT_MS = 6 * 60 * 60 * 1000;
|
|
18
23
|
|
|
19
24
|
const LAUNCHD_LABELS = ['com.totalrecall.brain', 'com.totalrecall.server'];
|
|
20
25
|
const SYSTEMD_UNITS = ['total-recall-server', 'total-recall-server.service'];
|
|
@@ -47,30 +52,33 @@ function systemdPidFor(unit) {
|
|
|
47
52
|
|
|
48
53
|
/**
|
|
49
54
|
* Prove that a supervisor owns THIS process and will restart it.
|
|
50
|
-
*
|
|
55
|
+
* `scope` says whose supervisor it is: `user` belongs to one login, `system` to the machine.
|
|
56
|
+
* @returns {{supervised: boolean, kind: string|null, label: string|null, scope: string|null, reason: string}}
|
|
51
57
|
*/
|
|
52
58
|
export function detectSupervisor(pid = process.pid) {
|
|
53
59
|
// An explicit opt-in for hosts running under a supervisor we cannot query
|
|
54
60
|
// (docker restart policies, pm2, runit). The operator asserts respawn.
|
|
55
61
|
if (process.env.TR_SUPERVISED === '1') {
|
|
56
|
-
return { supervised: true, kind: 'declared', label: 'TR_SUPERVISED=1', reason: 'declared by operator' };
|
|
62
|
+
return { supervised: true, kind: 'declared', label: 'TR_SUPERVISED=1', scope: null, reason: 'declared by operator' };
|
|
57
63
|
}
|
|
58
64
|
if (process.platform === 'darwin') {
|
|
59
65
|
for (const label of LAUNCHD_LABELS) {
|
|
60
66
|
const jobPid = launchdPidFor(label);
|
|
61
67
|
if (jobPid === pid) {
|
|
62
|
-
|
|
68
|
+
// launchctl answers for the caller's domain, so as root this is a system daemon.
|
|
69
|
+
const scope = process.getuid?.() === 0 ? 'system' : 'user';
|
|
70
|
+
return { supervised: true, kind: 'launchd', label, scope, reason: `launchd job ${label} owns pid ${pid}` };
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
|
-
return { supervised: false, kind: null, label: null, reason: 'no launchd job reports this pid' };
|
|
73
|
+
return { supervised: false, kind: null, label: null, scope: null, reason: 'no launchd job reports this pid' };
|
|
66
74
|
}
|
|
67
75
|
for (const unit of SYSTEMD_UNITS) {
|
|
68
76
|
const found = systemdPidFor(unit);
|
|
69
77
|
if (found && found.pid === pid) {
|
|
70
|
-
return { supervised: true, kind: 'systemd', label: unit, reason: `systemd ${found.scope} unit ${unit} owns pid ${pid}` };
|
|
78
|
+
return { supervised: true, kind: 'systemd', label: unit, scope: found.scope, reason: `systemd ${found.scope} unit ${unit} owns pid ${pid}` };
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
|
-
return { supervised: false, kind: null, label: null, reason: 'no systemd unit reports this pid' };
|
|
81
|
+
return { supervised: false, kind: null, label: null, scope: null, reason: 'no systemd unit reports this pid' };
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
/**
|
|
@@ -128,3 +136,116 @@ export function packageVersionOnDisk(rootDir) {
|
|
|
128
136
|
return null;
|
|
129
137
|
}
|
|
130
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Can maintenance replace the server on this host, and how?
|
|
142
|
+
*
|
|
143
|
+
* `npm install --global` moves the files under a running server and nothing else
|
|
144
|
+
* moves the process, so a host that serves from that install stays on the release it
|
|
145
|
+
* booted on. Replacing it has a gap, so maintenance does it only where a gap is allowed
|
|
146
|
+
* and a restart is certain to help:
|
|
147
|
+
* - a per-user supervisor owns the process. A LaunchAgent or user unit belongs to one
|
|
148
|
+
* login; a system service is production, and `deploy release` replaces that with no gap.
|
|
149
|
+
* - the process runs the global install's entry, and that install is not a checkout
|
|
150
|
+
* (auto-pull owns a checkout) and is at the release. Otherwise a restart starts the
|
|
151
|
+
* same old code, or code nobody released.
|
|
152
|
+
*
|
|
153
|
+
* @returns {{method: 'restart', kind: string, label: string, pid: number}|{method: 'deploy'|'none', reason: string}}
|
|
154
|
+
*/
|
|
155
|
+
export function planServerReplacement({ brainDir, globalRoot, latest, env = process.env, identity = processIdentity, supervisor = detectSupervisor, versionOnDisk = packageVersionOnDisk } = {}) {
|
|
156
|
+
const leave = (reason, method = 'none') => ({ method, reason });
|
|
157
|
+
if (env.TR_SERVER_AUTO_RESTART === '0') return leave('TR_SERVER_AUTO_RESTART=0 leaves this server to the operator.');
|
|
158
|
+
if (!globalRoot) return leave('The global npm root could not be read, so what a restart would start is unknown.');
|
|
159
|
+
const install = path.join(globalRoot, GLOBAL_PACKAGE);
|
|
160
|
+
const observed = identity(path.join(brainDir, 'server.pid'), path.join(install, 'src', 'server', 'index.mjs'));
|
|
161
|
+
if (!observed.pid) return leave('The process serving this brain could not be identified.');
|
|
162
|
+
const owner = supervisor(observed.pid);
|
|
163
|
+
if (owner.supervised && owner.scope === 'system') return leave(`${owner.label} is a system service: replace it with total-recall deploy release.`, 'deploy');
|
|
164
|
+
if (observed.status !== 'running') return leave(`The server does not run from the global install${observed.entry ? ` (it runs ${observed.entry})` : ''}, so a restart would not bring it to the release.`);
|
|
165
|
+
if (fs.existsSync(path.join(install, '.git'))) return leave('The global install is a link to a git checkout, which auto-pull keeps current.');
|
|
166
|
+
const onDisk = versionOnDisk(install);
|
|
167
|
+
if (onDisk !== latest) return leave(`The global install is ${onDisk ?? 'unreadable'} and the release is ${latest}.`);
|
|
168
|
+
if (!owner.supervised || owner.scope !== 'user') return leave(`Nothing maintenance can drive would bring the server back: ${owner.reason}.`);
|
|
169
|
+
return { method: 'restart', kind: owner.kind, label: owner.label, pid: observed.pid };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Connections open to the server right now, or null when that cannot be read. */
|
|
173
|
+
export function openServerConnections({ pid, port, platform = process.platform, run = spawnSync } = {}) {
|
|
174
|
+
const options = { encoding: 'utf8', timeout: 10000 };
|
|
175
|
+
if (platform === 'linux') {
|
|
176
|
+
const result = run('ss', ['-tnH', 'state', 'established', 'sport', '=', `:${port}`], options);
|
|
177
|
+
if (result.error || result.status !== 0) return null;
|
|
178
|
+
return result.stdout.split('\n').filter((line) => line.trim()).length;
|
|
179
|
+
}
|
|
180
|
+
const result = run('lsof', ['-nP', '-a', '-p', String(pid), '-iTCP', '-sTCP:ESTABLISHED', '-Fn'], options);
|
|
181
|
+
if (result.error) return null;
|
|
182
|
+
// lsof exits 1 when nothing matches, and also on a real failure, which is the one that writes to stderr.
|
|
183
|
+
if (result.status !== 0 && (result.stdout?.trim() || result.stderr?.trim())) return null;
|
|
184
|
+
return (result.stdout || '').split('\n').filter((line) => line.startsWith('n') && line.split('->')[0].endsWith(`:${port}`)).length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The command that stops the server and starts it again, or null when the job is not there. */
|
|
188
|
+
function restartCommand(plan, { run, uid }) {
|
|
189
|
+
if (plan.kind === 'systemd') return ['systemctl', ['--user', 'restart', plan.label]];
|
|
190
|
+
// `gui` exists while the user is logged in; a Mac with no login session has only `user`.
|
|
191
|
+
const target = [`gui/${uid}/${plan.label}`, `user/${uid}/${plan.label}`]
|
|
192
|
+
.find((candidate) => run('launchctl', ['print', candidate], { encoding: 'utf8', timeout: 10000 }).status === 0);
|
|
193
|
+
// kickstart -k delivers SIGTERM and waits (measured, launchd does not document it), so the
|
|
194
|
+
// server drains and releases its PID lock, and it needs no KeepAlive to come back.
|
|
195
|
+
return target ? ['launchctl', ['kickstart', '-k', target]] : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Bring a stale server to the release, when `planServerReplacement` allows it.
|
|
200
|
+
*
|
|
201
|
+
* An open connection holds this back so nobody's work is cut off, for as long as
|
|
202
|
+
* `holdLimitMs`: an open dashboard tab must not keep a host on an old release forever.
|
|
203
|
+
* Replaced means /health reports the release, never that the command returned.
|
|
204
|
+
*
|
|
205
|
+
* @returns the server status to report, with `replacement` saying what happened.
|
|
206
|
+
*/
|
|
207
|
+
export async function replaceStaleServer({
|
|
208
|
+
brainDir, server, globalRoot, previous, uid = process.getuid?.(), plan = planServerReplacement, connections = openServerConnections,
|
|
209
|
+
run = spawnSync, observe = runningServerStatus, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now,
|
|
210
|
+
idleWaitMs = 30000, healthWaitMs = 120000, pollMs = 2000, holdLimitMs = HOLD_LIMIT_MS,
|
|
211
|
+
} = {}) {
|
|
212
|
+
let replacement;
|
|
213
|
+
try {
|
|
214
|
+
replacement = plan({ brainDir, globalRoot, latest: server.latest });
|
|
215
|
+
if (replacement.method !== 'restart') return { ...server, replacement };
|
|
216
|
+
|
|
217
|
+
const port = new URL(server.url).port;
|
|
218
|
+
const idleBy = now() + idleWaitMs;
|
|
219
|
+
let open = connections({ pid: replacement.pid, port });
|
|
220
|
+
while (open !== 0 && now() < idleBy) {
|
|
221
|
+
await sleep(pollMs);
|
|
222
|
+
open = connections({ pid: replacement.pid, port });
|
|
223
|
+
}
|
|
224
|
+
if (open !== 0) {
|
|
225
|
+
const heldSince = previous?.replacement?.state === 'held' ? previous.replacement.held_since : now();
|
|
226
|
+
if (now() - heldSince < holdLimitMs) {
|
|
227
|
+
const reason = open === null ? 'Open connections could not be read.' : `${open} connection(s) are open on the server.`;
|
|
228
|
+
return { ...server, replacement: { ...replacement, state: 'held', held_since: heldSince, reason } };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const command = restartCommand(replacement, { run, uid });
|
|
233
|
+
if (!command) return { ...server, replacement: { ...replacement, state: 'failed', reason: `launchd has no job ${replacement.label} for this user.` } };
|
|
234
|
+
const restarted = run(command[0], command[1], { encoding: 'utf8', timeout: 60000 });
|
|
235
|
+
if (restarted.error || restarted.status !== 0) {
|
|
236
|
+
const reason = `${command[0]} ${command[1].join(' ')} failed: ${restarted.error?.message || restarted.stderr?.trim() || `exit ${restarted.status}`}`;
|
|
237
|
+
return { ...server, replacement: { ...replacement, state: 'failed', reason } };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const healthyBy = now() + healthWaitMs;
|
|
241
|
+
let after;
|
|
242
|
+
do {
|
|
243
|
+
await sleep(pollMs);
|
|
244
|
+
after = await observe({ brainDir, latest: server.latest, timeoutMs: 5000 });
|
|
245
|
+
} while (after.status !== 'current' && now() < healthyBy);
|
|
246
|
+
if (after.status === 'current') return { ...after, replacement: { ...replacement, state: 'replaced', from: server.running } };
|
|
247
|
+
return { ...after, replacement: { ...replacement, state: 'failed', reason: `/health did not report ${server.latest} within ${healthWaitMs / 1000} seconds of the restart.` } };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return { ...server, replacement: { ...replacement, state: 'failed', reason: error.message } };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -235,9 +235,8 @@ export function isProtocolPath(vfsPath = '') {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
function stringifyDocument(data, body) {
|
|
238
|
-
// Prefer gray-matter when available; fall back to simple frontmatter dump.
|
|
239
238
|
try {
|
|
240
|
-
return matter.stringify(body || '', data);
|
|
239
|
+
return matter.stringify({ content: body || '', data });
|
|
241
240
|
} catch {
|
|
242
241
|
const yaml = Object.entries(data)
|
|
243
242
|
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
package/src/core/surface.mjs
CHANGED
|
@@ -620,7 +620,7 @@ const CLIENT_SHIMS = {
|
|
|
620
620
|
'hermes-agent': ['AGENTS.md'],
|
|
621
621
|
dsh: ['AGENTS.md'],
|
|
622
622
|
'deepseek-harness': ['AGENTS.md'],
|
|
623
|
-
openclaw: ['
|
|
623
|
+
openclaw: ['AGENTS.md']
|
|
624
624
|
};
|
|
625
625
|
|
|
626
626
|
/**
|
|
@@ -739,13 +739,13 @@ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultD
|
|
|
739
739
|
*/
|
|
740
740
|
/**
|
|
741
741
|
* The rule nodes a project's instruction surfaces are built from: the global
|
|
742
|
-
* brain's rules plus the project's own. A project
|
|
742
|
+
* brain's rules plus the project's own. A project rule with the same slug
|
|
743
743
|
* wins, so a repo can override a global rule; everything else in the global
|
|
744
744
|
* vault stays search-only. Without this every repo carried only its own vault,
|
|
745
745
|
* and a rule saved with `remember --global` reached no repo at all.
|
|
746
746
|
*/
|
|
747
747
|
export function mergeGlobalRuleNodes(projectNodes, globalNodes) {
|
|
748
|
-
const own = new Set(projectNodes.map((n) => n.slug));
|
|
748
|
+
const own = new Set(projectNodes.filter((n) => RULE_CATEGORIES.has(n.category)).map((n) => n.slug));
|
|
749
749
|
const inherited = globalNodes
|
|
750
750
|
.filter((n) => RULE_CATEGORIES.has(n.category) && !own.has(n.slug))
|
|
751
751
|
.map((n) => ({ ...n, _layer: 'global' }));
|
|
@@ -9,7 +9,9 @@ import fs from 'node:fs';
|
|
|
9
9
|
const hash = text => crypto.createHash('sha256').update(String(text)).digest('hex');
|
|
10
10
|
const STOP = new Set('a an and are as at be before by can do for from how i in is it of on or our should so that the their this to use we what when which with you'.split(' '));
|
|
11
11
|
export const taskTerms = task => [...new Set((String(task).toLowerCase().match(/[\p{L}\p{N}_-]{3,}/gu) || []).filter(term => !STOP.has(term)))];
|
|
12
|
-
|
|
12
|
+
// Task terms contain only letters, digits, underscores and hyphens. Hyphens also delimit source-reference segments.
|
|
13
|
+
const taskMatchers = task => taskTerms(task).map(term => new RegExp(`(?<![\\p{L}\\p{N}_])${term}(?![\\p{L}\\p{N}_])`, 'iu'));
|
|
14
|
+
const overlap = (matchers, text) => matchers.filter(matcher => matcher.test(text)).length / Math.max(1, matchers.length);
|
|
13
15
|
export const summarizeRetrieval = summary => summary ? { mode: summary.mode, selected: summary.selected,
|
|
14
16
|
candidates: summary.candidates || 0, providers: (summary.coverage || []).map(row => row.provider),
|
|
15
17
|
...(summary.duplicatePassages ? { duplicatePassages: summary.duplicatePassages } : {}),
|
|
@@ -18,11 +20,11 @@ export const summarizeRetrieval = summary => summary ? { mode: summary.mode, sel
|
|
|
18
20
|
|
|
19
21
|
/** Preserve the best matching line and bounded neighboring text, including a match late in a long body. */
|
|
20
22
|
export function extractTaskPassage(text, task, { maxChars = 1000, anchorLine } = {}) {
|
|
21
|
-
const lines = String(text || '').split('\n'),
|
|
23
|
+
const lines = String(text || '').split('\n'), matchers = taskMatchers(task);
|
|
22
24
|
let anchor = Number.isInteger(anchorLine) ? Math.max(0, Math.min(lines.length - 1, anchorLine - 1)) : 0;
|
|
23
25
|
if (!Number.isInteger(anchorLine)) {
|
|
24
26
|
let best = -1;
|
|
25
|
-
lines.forEach((line, index) => { const score = overlap(
|
|
27
|
+
lines.forEach((line, index) => { const score = overlap(matchers, line); if (score > best) { best = score; anchor = index; } });
|
|
26
28
|
}
|
|
27
29
|
let start = Math.max(0, anchor - 2), end = Math.min(lines.length, anchor + 3);
|
|
28
30
|
while (lines.slice(start, end).join('\n').length > maxChars && (start < anchor || end > anchor + 1)) {
|
|
@@ -32,8 +34,7 @@ export function extractTaskPassage(text, task, { maxChars = 1000, anchorLine } =
|
|
|
32
34
|
let passage = lines.slice(start, end).join('\n');
|
|
33
35
|
let startColumn = 1;
|
|
34
36
|
if (passage.length > maxChars) {
|
|
35
|
-
const
|
|
36
|
-
const hits = terms.map(term => lower.indexOf(term)).filter(index => index >= 0);
|
|
37
|
+
const hits = matchers.map(matcher => passage.search(matcher)).filter(index => index >= 0);
|
|
37
38
|
const focus = hits.length ? Math.min(...hits) : 0;
|
|
38
39
|
const offset = Math.max(0, Math.min(passage.length - maxChars, focus - Math.floor(maxChars / 3)));
|
|
39
40
|
passage = passage.slice(offset, offset + maxChars); startColumn += offset;
|
|
@@ -50,8 +51,8 @@ function renderEvidence(item) {
|
|
|
50
51
|
export async function retrieveTaskKnowledge({ query, nodes = [], derivedDir, indexScopes,
|
|
51
52
|
projectRoot = process.cwd(), actions = [], semantic = true, budget = 1200, limit = 6,
|
|
52
53
|
embedding = getEmbedding, loadIndex = loadEmbeddingsIndex, providers = retrievePluginContext } = {}) {
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
54
|
+
const matchers = taskMatchers(query);
|
|
55
|
+
if (!matchers.length) return { contributions: [], summary: { mode: 'none', selected: 0, diagnostics: [], coverage: [] }, queryVector: null };
|
|
55
56
|
const diagnostics = [], indexes = new Map();
|
|
56
57
|
const scopes = indexScopes || (derivedDir ? [{ layer: '*', derivedDir }] : []);
|
|
57
58
|
for (const scope of scopes) {
|
|
@@ -77,7 +78,7 @@ export async function retrieveTaskKnowledge({ query, nodes = [], derivedDir, ind
|
|
|
77
78
|
if (stale) staleVectors++;
|
|
78
79
|
const valid = !stale && Array.isArray(entry?.embedding) && Array.isArray(queryVector) && entry.embedding.length === queryVector.length;
|
|
79
80
|
if (valid) vectors++;
|
|
80
|
-
const lexical = overlap(
|
|
81
|
+
const lexical = overlap(matchers, `${node.title || ''}\n${body}`);
|
|
81
82
|
const similarity = valid ? cosineSimilarity(queryVector, entry.embedding) : 0;
|
|
82
83
|
const score = valid ? Math.max(0, similarity) * .75 + lexical * .25 : lexical * .7;
|
|
83
84
|
return { node, layer, body, score, relevant: lexical > 0 || similarity >= .35 };
|
|
@@ -15,6 +15,8 @@ import { processOperationAsync } from './operation-validator.mjs';
|
|
|
15
15
|
import { safeStringify, isSafeVaultName } from './vault.mjs';
|
|
16
16
|
import { invalidate } from './vault-cache.mjs';
|
|
17
17
|
|
|
18
|
+
export const supportsExpectedBeforeHash = true;
|
|
19
|
+
|
|
18
20
|
/**
|
|
19
21
|
* Normalize a vault node so package universal frontmatter + TR memory v2 pass.
|
|
20
22
|
*/
|
|
@@ -101,6 +103,7 @@ function buildEnvelope(node, options = {}) {
|
|
|
101
103
|
const {
|
|
102
104
|
workspaceId = 'default',
|
|
103
105
|
idempotencyKey,
|
|
106
|
+
expectedBeforeHash,
|
|
104
107
|
dryRun = false,
|
|
105
108
|
path: pathOverride,
|
|
106
109
|
} = options;
|
|
@@ -124,6 +127,7 @@ function buildEnvelope(node, options = {}) {
|
|
|
124
127
|
path: vfsPath,
|
|
125
128
|
workspace_id: workspaceId,
|
|
126
129
|
content,
|
|
130
|
+
...(expectedBeforeHash === undefined ? {} : { expected_before_hash: expectedBeforeHash }),
|
|
127
131
|
dry_run: !!dryRun,
|
|
128
132
|
actor: { role: options.agentRole || 'system' },
|
|
129
133
|
};
|
package/src/core/vault-files.mjs
CHANGED
|
@@ -8,6 +8,12 @@ export function isSafeVaultName(name) {
|
|
|
8
8
|
|
|
9
9
|
export function atomicWrite(filePath, content) {
|
|
10
10
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
try {
|
|
12
|
+
fs.writeFileSync(tmpPath, content, 'utf8');
|
|
13
|
+
fs.renameSync(tmpPath, filePath);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// A leftover copy is untracked and unignored, so a commit of everything would publish it.
|
|
16
|
+
fs.rmSync(tmpPath, { force: true });
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
13
19
|
}
|
package/src/core/vault.mjs
CHANGED
|
@@ -33,7 +33,7 @@ function stripUndefined(obj) {
|
|
|
33
33
|
* @returns {string} The serialized markdown with frontmatter
|
|
34
34
|
*/
|
|
35
35
|
export function safeStringify(content, data) {
|
|
36
|
-
return matter.stringify(content ?? '', stripUndefined(data ?? {}));
|
|
36
|
+
return matter.stringify({ content: content ?? '', data: stripUndefined(data ?? {}) });
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
package/src/server/index.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { readProcessCommand, entryPathHint, shouldHonorPidLock } from '../core/p
|
|
|
34
34
|
import { createCoverageCache } from './health-coverage.mjs';
|
|
35
35
|
import { drainActiveEmbeddings } from './routes/sessions.mjs';
|
|
36
36
|
import { agentDir as configAgentDir, brainDir as configBrainDir, port as configPort, host as configHost, nodeEnv, instructionsFilePath } from '../core/config.mjs';
|
|
37
|
-
import { getDaemonStatus, ensureDaemonRunning } from '../core/daemon-control.mjs';
|
|
37
|
+
import { getDaemonStatus, ensureDaemonRunning, clearStaleDaemonLock } from '../core/daemon-control.mjs';
|
|
38
38
|
import { deploymentHealth } from '../core/running-server.mjs';
|
|
39
39
|
|
|
40
40
|
|
|
@@ -1004,6 +1004,8 @@ if (HOST !== '127.0.0.1' && HOST !== '0.0.0.0') {
|
|
|
1004
1004
|
|
|
1005
1005
|
// Unref the interval so it doesn't block process shutdown
|
|
1006
1006
|
daemonWatchdogInterval.unref();
|
|
1007
|
+
} else if (nodeEnv !== 'test') {
|
|
1008
|
+
clearStaleDaemonLock();
|
|
1007
1009
|
}
|
|
1008
1010
|
|
|
1009
1011
|
// ─── Cloudflare Tunnel Auto-Start ───
|