aiden-runtime 4.8.1 → 4.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -1
- package/dist/cli/v4/aidenCLI.js +37 -6
- package/dist/cli/v4/chatSession.js +53 -13
- package/dist/cli/v4/commands/daemon.js +53 -3
- package/dist/cli/v4/commands/daemonDoctor.js +212 -0
- package/dist/cli/v4/commands/daemonStatus.js +45 -26
- package/dist/cli/v4/commands/help.js +5 -0
- package/dist/cli/v4/commands/hooks.js +466 -0
- package/dist/cli/v4/commands/hooksSlash.js +33 -0
- package/dist/cli/v4/commands/index.js +13 -1
- package/dist/cli/v4/commands/mcp.js +89 -1
- package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
- package/dist/cli/v4/commands/memory.js +707 -0
- package/dist/cli/v4/commands/memorySlash.js +38 -0
- package/dist/cli/v4/commands/recovery.js +1 -1
- package/dist/cli/v4/commands/skin.js +7 -0
- package/dist/cli/v4/commands/theme.js +217 -0
- package/dist/cli/v4/commands/trigger.js +1 -1
- package/dist/cli/v4/design/tokens.js +52 -4
- package/dist/cli/v4/display.js +39 -26
- package/dist/cli/v4/replyRenderer.js +6 -5
- package/dist/cli/v4/skinEngine.js +67 -0
- package/dist/cli/v4/ui/progressBar.js +179 -0
- package/dist/cli/v4/util/closestAction.js +48 -0
- package/dist/core/v4/aidenAgent.js +45 -2
- package/dist/core/v4/daemon/api/runs.js +131 -0
- package/dist/core/v4/daemon/bootstrap.js +368 -13
- package/dist/core/v4/daemon/db/migrations.js +169 -0
- package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
- package/dist/core/v4/daemon/incarnationStore.js +47 -0
- package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
- package/dist/core/v4/daemon/runs/reclaim.js +88 -0
- package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
- package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
- package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
- package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
- package/dist/core/v4/daemon/spans/spanStore.js +113 -0
- package/dist/core/v4/daemon/triggerBus.js +50 -19
- package/dist/core/v4/hooks/auditQuery.js +67 -0
- package/dist/core/v4/hooks/dispatcher.js +286 -0
- package/dist/core/v4/hooks/index.js +46 -0
- package/dist/core/v4/hooks/lifecycle.js +27 -0
- package/dist/core/v4/hooks/manifest.js +142 -0
- package/dist/core/v4/hooks/registry.js +149 -0
- package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
- package/dist/core/v4/hooks/toolHookGate.js +76 -0
- package/dist/core/v4/hooks/trust.js +14 -0
- package/dist/core/v4/identity/contextManager.js +83 -0
- package/dist/core/v4/identity/daemonId.js +85 -0
- package/dist/core/v4/identity/enforcement.js +103 -0
- package/dist/core/v4/identity/executionContext.js +153 -0
- package/dist/core/v4/identity/hookExecution.js +62 -0
- package/dist/core/v4/identity/httpContext.js +68 -0
- package/dist/core/v4/identity/ids.js +185 -0
- package/dist/core/v4/identity/index.js +60 -0
- package/dist/core/v4/identity/subprocessContext.js +98 -0
- package/dist/core/v4/identity/traceparent.js +114 -0
- package/dist/core/v4/logger/index.js +3 -1
- package/dist/core/v4/logger/logger.js +28 -1
- package/dist/core/v4/logger/redact.js +149 -0
- package/dist/core/v4/logger/sinks/fileSink.js +13 -0
- package/dist/core/v4/logger/sinks/stdSink.js +19 -1
- package/dist/core/v4/mcp/install/backup.js +78 -0
- package/dist/core/v4/mcp/install/clientPaths.js +90 -0
- package/dist/core/v4/mcp/install/clients.js +203 -0
- package/dist/core/v4/mcp/install/healthCheck.js +83 -0
- package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
- package/dist/core/v4/mcp/install/profiles.js +109 -0
- package/dist/core/v4/mcp/install/wslDetect.js +62 -0
- package/dist/core/v4/memory/namespaceRegistry.js +117 -0
- package/dist/core/v4/memory/projectRoot.js +76 -0
- package/dist/core/v4/memory/reviewer/index.js +162 -0
- package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
- package/dist/core/v4/memory/reviewer/prompt.js +105 -0
- package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
- package/dist/core/v4/memoryManager.js +57 -10
- package/dist/core/v4/paths.js +2 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
- package/dist/core/v4/theme/bundledThemes.js +106 -0
- package/dist/core/v4/theme/themeLoader.js +160 -0
- package/dist/core/v4/theme/themeRegistry.js +97 -0
- package/dist/core/v4/theme/themeWatcher.js +95 -0
- package/dist/core/v4/toolRegistry.js +71 -8
- package/dist/core/v4/update/depWarningFilter.js +76 -0
- package/dist/core/v4/update/executeInstall.js +41 -35
- package/dist/core/v4/update/platformInstructions.js +128 -0
- package/dist/moat/approvalEngine.js +4 -0
- package/dist/moat/memoryGuard.js +8 -1
- package/dist/providers/v4/anthropicAdapter.js +10 -4
- package/dist/tools/v4/backends/local.js +19 -2
- package/dist/tools/v4/sessions/recallSession.js +6 -1
- package/package.json +3 -1
- package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
- package/themes/default.yaml +52 -0
- package/themes/dracula.yaml +32 -0
- package/themes/light.yaml +32 -0
- package/themes/monochrome.yaml +31 -0
- package/themes/tokyo-night.yaml +32 -0
- package/dist/core/pluginSystem.js +0 -121
- package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/memory/namespaceRegistry.ts — v4.9.0 Slice 11.
|
|
10
|
+
*
|
|
11
|
+
* Generalizes Aiden's two-file memory model (`memory` + `user`) to a
|
|
12
|
+
* registry. Adds a third default namespace — `project` — keyed off
|
|
13
|
+
* the caller's current working directory. The registry is the single
|
|
14
|
+
* authority every other surface (tool schemas, CLI parser, prompt
|
|
15
|
+
* injector, reviewer, backup) consults; adding a namespace is one
|
|
16
|
+
* registration call instead of an N-place patch.
|
|
17
|
+
*
|
|
18
|
+
* Slice 11 ships THREE default namespaces:
|
|
19
|
+
*
|
|
20
|
+
* memory — Aiden environment / project & technical notes. 2200 chars
|
|
21
|
+
* user — User identity / preferences / workflow style. 1375 chars
|
|
22
|
+
* project — Per-project context at `<projectRoot>/.aiden/PROJECT.md`. 1800 chars
|
|
23
|
+
*
|
|
24
|
+
* The `'memory'` vs `'project'` distinction:
|
|
25
|
+
* - `memory` is GLOBAL per-install (Aiden-wide notes).
|
|
26
|
+
* - `project` is PER-DIRECTORY (one PROJECT.md per repo / workdir).
|
|
27
|
+
*
|
|
28
|
+
* Char limit for `project` (1800) sits between `user` (1375) and the
|
|
29
|
+
* legacy `memory` (2200) — project context is verbose but not as
|
|
30
|
+
* verbose as full environment notes.
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getNamespace = getNamespace;
|
|
37
|
+
exports.hasNamespace = hasNamespace;
|
|
38
|
+
exports.listNamespaces = listNamespaces;
|
|
39
|
+
exports.listNamespaceNames = listNamespaceNames;
|
|
40
|
+
exports.registerNamespace = registerNamespace;
|
|
41
|
+
exports._resetNamespacesForTests = _resetNamespacesForTests;
|
|
42
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
43
|
+
const BUILTIN = [
|
|
44
|
+
{
|
|
45
|
+
name: 'memory',
|
|
46
|
+
label: 'Memory (project & environment)',
|
|
47
|
+
description: 'Global notes about Aiden\'s environment + this project.',
|
|
48
|
+
charLimit: 2200,
|
|
49
|
+
injectIntoPrompt: true,
|
|
50
|
+
promptHeader: 'Project & Environment',
|
|
51
|
+
resolve: (paths) => paths.memoryMd,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'user',
|
|
55
|
+
label: 'User (identity & preferences)',
|
|
56
|
+
description: 'User identity, preferences, workflow style.',
|
|
57
|
+
charLimit: 1375,
|
|
58
|
+
injectIntoPrompt: true,
|
|
59
|
+
promptHeader: 'User Identity & Preferences',
|
|
60
|
+
resolve: (paths) => paths.userMd,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'project',
|
|
64
|
+
label: 'Project (per-repo context)',
|
|
65
|
+
description: 'Per-project context at <projectRoot>/.aiden/PROJECT.md.',
|
|
66
|
+
charLimit: 1800,
|
|
67
|
+
injectIntoPrompt: true,
|
|
68
|
+
promptHeader: 'Current Project Context',
|
|
69
|
+
requiresProject: true,
|
|
70
|
+
resolve: (_paths, projectRoot) => {
|
|
71
|
+
if (!projectRoot) {
|
|
72
|
+
throw new Error('project namespace requires a project root (run from inside a git repo or directory with .aiden/PROJECT.md)');
|
|
73
|
+
}
|
|
74
|
+
return node_path_1.default.join(projectRoot, '.aiden', 'PROJECT.md');
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
const REGISTRY = new Map(BUILTIN.map((n) => [n.name, n]));
|
|
79
|
+
/** Throw-on-unknown getter. Callers MUST check `has()` first if the
|
|
80
|
+
* name is user-supplied — `getNamespace` is for trusted callsites. */
|
|
81
|
+
function getNamespace(name) {
|
|
82
|
+
const ns = REGISTRY.get(name);
|
|
83
|
+
if (!ns) {
|
|
84
|
+
const known = Array.from(REGISTRY.keys()).join(', ');
|
|
85
|
+
throw new Error(`unknown memory namespace '${name}' (known: ${known})`);
|
|
86
|
+
}
|
|
87
|
+
return ns;
|
|
88
|
+
}
|
|
89
|
+
/** Soft check — does this name resolve to a registered namespace. */
|
|
90
|
+
function hasNamespace(name) {
|
|
91
|
+
return REGISTRY.has(name);
|
|
92
|
+
}
|
|
93
|
+
/** Iteration order matches registration order. */
|
|
94
|
+
function listNamespaces() {
|
|
95
|
+
return Array.from(REGISTRY.values());
|
|
96
|
+
}
|
|
97
|
+
/** Names only — for tool schema enums + CLI argument validation. */
|
|
98
|
+
function listNamespaceNames() {
|
|
99
|
+
return Array.from(REGISTRY.keys());
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reserved for v4.10+ user-defined namespaces (plugins). Not wired to
|
|
103
|
+
* config.yaml or plugin loaders in this slice; the entry point exists
|
|
104
|
+
* so the runtime can extend without further refactor.
|
|
105
|
+
*/
|
|
106
|
+
function registerNamespace(ns) {
|
|
107
|
+
if (REGISTRY.has(ns.name)) {
|
|
108
|
+
throw new Error(`memory namespace '${ns.name}' is already registered`);
|
|
109
|
+
}
|
|
110
|
+
REGISTRY.set(ns.name, ns);
|
|
111
|
+
}
|
|
112
|
+
/** Test seam — restore registry to the three built-ins. */
|
|
113
|
+
function _resetNamespacesForTests() {
|
|
114
|
+
REGISTRY.clear();
|
|
115
|
+
for (const ns of BUILTIN)
|
|
116
|
+
REGISTRY.set(ns.name, ns);
|
|
117
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/memory/projectRoot.ts — v4.9.0 Slice 11.
|
|
10
|
+
*
|
|
11
|
+
* Walk-up project-root detection. Returns the first ancestor of
|
|
12
|
+
* `cwd` containing any of the standard project anchors:
|
|
13
|
+
*
|
|
14
|
+
* .git/ most repos
|
|
15
|
+
* package.json Node
|
|
16
|
+
* pyproject.toml Python
|
|
17
|
+
* Cargo.toml Rust
|
|
18
|
+
* go.mod Go
|
|
19
|
+
* .aiden/PROJECT.md explicit Aiden-managed project
|
|
20
|
+
*
|
|
21
|
+
* Returns `null` when the walk reaches the filesystem root without
|
|
22
|
+
* finding any anchor. Result is cached per-process keyed on the
|
|
23
|
+
* normalised cwd; the cache is cleared via `_resetProjectRootCacheForTests`.
|
|
24
|
+
*/
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.findProjectRoot = findProjectRoot;
|
|
30
|
+
exports._resetProjectRootCacheForTests = _resetProjectRootCacheForTests;
|
|
31
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
32
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
33
|
+
const ANCHORS = [
|
|
34
|
+
'.git',
|
|
35
|
+
'package.json',
|
|
36
|
+
'pyproject.toml',
|
|
37
|
+
'Cargo.toml',
|
|
38
|
+
'go.mod',
|
|
39
|
+
node_path_1.default.join('.aiden', 'PROJECT.md'),
|
|
40
|
+
];
|
|
41
|
+
const _cache = new Map();
|
|
42
|
+
/**
|
|
43
|
+
* Walk up from `cwd` looking for an anchor. Returns the absolute path
|
|
44
|
+
* of the directory holding the first anchor, or `null` when nothing
|
|
45
|
+
* is found before filesystem root. Cached per `cwd`.
|
|
46
|
+
*/
|
|
47
|
+
function findProjectRoot(cwd = process.cwd()) {
|
|
48
|
+
const startedAt = node_path_1.default.resolve(cwd);
|
|
49
|
+
if (_cache.has(startedAt))
|
|
50
|
+
return _cache.get(startedAt);
|
|
51
|
+
let current = startedAt;
|
|
52
|
+
// Cap the walk at 64 levels — pathological symlink loops shouldn't burn.
|
|
53
|
+
for (let i = 0; i < 64; i += 1) {
|
|
54
|
+
for (const anchor of ANCHORS) {
|
|
55
|
+
const probe = node_path_1.default.join(current, anchor);
|
|
56
|
+
try {
|
|
57
|
+
const stat = node_fs_1.default.statSync(probe);
|
|
58
|
+
if (stat.isFile() || stat.isDirectory()) {
|
|
59
|
+
_cache.set(startedAt, current);
|
|
60
|
+
return current;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { /* ENOENT — keep looking */ }
|
|
64
|
+
}
|
|
65
|
+
const parent = node_path_1.default.dirname(current);
|
|
66
|
+
if (parent === current)
|
|
67
|
+
break; // reached filesystem root
|
|
68
|
+
current = parent;
|
|
69
|
+
}
|
|
70
|
+
_cache.set(startedAt, null);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** Test seam — clear the per-process cache. */
|
|
74
|
+
function _resetProjectRootCacheForTests() {
|
|
75
|
+
_cache.clear();
|
|
76
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/memory/reviewer/index.ts — v4.9.0 Slice 10.
|
|
10
|
+
*
|
|
11
|
+
* Orchestrator for the post-turn memory reviewer. Pure functions +
|
|
12
|
+
* one entry point `runReview(opts)`. The LLM call is injected as a
|
|
13
|
+
* callback so the reviewer is testable + provider-agnostic — the CLI
|
|
14
|
+
* wires the callback to whatever provider the user has configured.
|
|
15
|
+
*
|
|
16
|
+
* Fail-open guarantees (project rule): any error in the reviewer
|
|
17
|
+
* (LLM timeout, parse error, file write error) is caught + logged
|
|
18
|
+
* but NEVER propagated to the user. `runReview` returns a structured
|
|
19
|
+
* outcome envelope; the CLI surfaces it humanely.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.runReview = runReview;
|
|
23
|
+
exports.stripPendingSections = stripPendingSections;
|
|
24
|
+
const prompt_1 = require("./prompt");
|
|
25
|
+
const skipRules_1 = require("./skipRules");
|
|
26
|
+
const pendingStore_1 = require("./pendingStore");
|
|
27
|
+
const memoryManager_1 = require("../../memoryManager");
|
|
28
|
+
const namespaceRegistry_1 = require("../namespaceRegistry");
|
|
29
|
+
/**
|
|
30
|
+
* Run one review pass. Always resolves (never throws). Use the
|
|
31
|
+
* returned outcome envelope to surface humanely.
|
|
32
|
+
*/
|
|
33
|
+
async function runReview(opts) {
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
const log = opts.log ?? (() => { });
|
|
36
|
+
try {
|
|
37
|
+
// Build prompt. Strip pending sections from the "live" snapshots so the
|
|
38
|
+
// reviewer doesn't see its own past proposals as duplicates.
|
|
39
|
+
const liveMemory = stripPendingSections(opts.liveMemoryRaw);
|
|
40
|
+
const liveUser = stripPendingSections(opts.liveUserRaw);
|
|
41
|
+
const prompt = (0, prompt_1.buildReviewerPrompt)({
|
|
42
|
+
recentTurns: opts.recentTurns,
|
|
43
|
+
liveMemory,
|
|
44
|
+
liveUser,
|
|
45
|
+
maxCandidates: opts.maxCandidates,
|
|
46
|
+
});
|
|
47
|
+
// LLM call inside a timeout race.
|
|
48
|
+
const raw = await raceTimeout(opts.callLLM(prompt), opts.timeoutMs);
|
|
49
|
+
if (raw === TIMEOUT_SENTINEL) {
|
|
50
|
+
log('warn', '[memory-reviewer] timeout — no candidates produced');
|
|
51
|
+
return { outcome: 'timeout', durationMs: Date.now() - start };
|
|
52
|
+
}
|
|
53
|
+
// Parse + skip-rule validate.
|
|
54
|
+
const { candidates: parsed, parserDrops } = (0, prompt_1.parseReviewerResponse)(raw);
|
|
55
|
+
const liveMemoryEntries = splitLiveEntries(liveMemory);
|
|
56
|
+
const liveUserEntries = splitLiveEntries(liveUser);
|
|
57
|
+
const dropsByClass = {
|
|
58
|
+
sensitive_class: 0, negation: 0, transient: 0, duplicate: 0, char_cap: 0,
|
|
59
|
+
no_project_root: 0, parser: parserDrops,
|
|
60
|
+
};
|
|
61
|
+
// v4.9.0 Slice 11 — per-namespace kept buckets so `project`
|
|
62
|
+
// (and future namespaces) flow through the same path as memory/user.
|
|
63
|
+
const keptByNamespace = new Map();
|
|
64
|
+
for (const c of parsed) {
|
|
65
|
+
let total = 0;
|
|
66
|
+
for (const arr of keptByNamespace.values())
|
|
67
|
+
total += arr.length;
|
|
68
|
+
if (total >= opts.maxCandidates)
|
|
69
|
+
break;
|
|
70
|
+
// Skip-rule: `requiresProject` namespace + no detected root → drop.
|
|
71
|
+
try {
|
|
72
|
+
const ns = (0, namespaceRegistry_1.getNamespace)(c.file);
|
|
73
|
+
if (ns.requiresProject && !opts.projectRoot) {
|
|
74
|
+
dropsByClass.no_project_root += 1;
|
|
75
|
+
log('info', `[memory-reviewer] skipped (no_project_root): "${c.text.slice(0, 60)}"`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch { /* unknown namespace — parser already dropped these but defensive */ }
|
|
80
|
+
const live = c.file === 'user' ? liveUserEntries : liveMemoryEntries;
|
|
81
|
+
const decision = (0, skipRules_1.evaluateCandidate)(c.text, live);
|
|
82
|
+
if (decision.drop && decision.klass) {
|
|
83
|
+
dropsByClass[decision.klass] += 1;
|
|
84
|
+
log('info', `[memory-reviewer] skipped (${decision.klass}): "${c.text.slice(0, 60)}"`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const bucket = keptByNamespace.get(c.file) ?? [];
|
|
88
|
+
bucket.push({ text: c.text, rationale: c.rationale });
|
|
89
|
+
keptByNamespace.set(c.file, bucket);
|
|
90
|
+
}
|
|
91
|
+
// Append into pending sections — one call per namespace.
|
|
92
|
+
const candidatesProposed = [];
|
|
93
|
+
for (const [nsName, kept] of keptByNamespace) {
|
|
94
|
+
let targetPath;
|
|
95
|
+
if (nsName === 'memory')
|
|
96
|
+
targetPath = opts.memoryPath;
|
|
97
|
+
else if (nsName === 'user')
|
|
98
|
+
targetPath = opts.userPath;
|
|
99
|
+
else if (opts.paths) {
|
|
100
|
+
try {
|
|
101
|
+
targetPath = (0, namespaceRegistry_1.getNamespace)(nsName).resolve(opts.paths, opts.projectRoot ?? null);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
continue; /* skip — already counted in no_project_root */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const stamped = await (0, pendingStore_1.appendCandidates)(targetPath, nsName, kept);
|
|
111
|
+
candidatesProposed.push(...stamped);
|
|
112
|
+
}
|
|
113
|
+
log('info', `[memory-reviewer] review complete: proposed=${candidatesProposed.length} ` +
|
|
114
|
+
`parser_drops=${parserDrops} ` +
|
|
115
|
+
`rule_drops=${Object.entries(dropsByClass).filter(([k]) => k !== 'parser').map(([k, v]) => `${k}:${v}`).join(' ')}`);
|
|
116
|
+
return {
|
|
117
|
+
outcome: 'ok',
|
|
118
|
+
candidatesProposed,
|
|
119
|
+
dropsByClass,
|
|
120
|
+
llmCharsIn: prompt.length,
|
|
121
|
+
llmCharsOut: raw.length,
|
|
122
|
+
durationMs: Date.now() - start,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
127
|
+
log('error', `[memory-reviewer] error (fail-open): ${error}`);
|
|
128
|
+
return { outcome: 'error', error, durationMs: Date.now() - start };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const TIMEOUT_SENTINEL = Symbol('memoryReviewerTimeout');
|
|
132
|
+
async function raceTimeout(p, ms) {
|
|
133
|
+
let timer = null;
|
|
134
|
+
const timeout = new Promise((resolve) => {
|
|
135
|
+
timer = setTimeout(() => resolve(TIMEOUT_SENTINEL), ms);
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
const result = await Promise.race([p, timeout]);
|
|
139
|
+
if (timer)
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
if (timer)
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Strip `## Pending review` blocks from a raw file so the reviewer
|
|
151
|
+
* doesn't see its own prior candidates as "live entries".
|
|
152
|
+
*/
|
|
153
|
+
function stripPendingSections(raw) {
|
|
154
|
+
if (!raw.includes('## Pending review'))
|
|
155
|
+
return raw;
|
|
156
|
+
return raw.replace(/\n*§?\n*## Pending review[\s\S]*?(?=(?:\n§\n)|$)/g, '').trimEnd();
|
|
157
|
+
}
|
|
158
|
+
function splitLiveEntries(raw) {
|
|
159
|
+
if (!raw.trim())
|
|
160
|
+
return [];
|
|
161
|
+
return raw.split(memoryManager_1.ENTRY_SEPARATOR).map((e) => e.trim()).filter((e) => e.length > 0);
|
|
162
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/memory/reviewer/pendingStore.ts — v4.9.0 Slice 10.
|
|
10
|
+
*
|
|
11
|
+
* Read + append the `## Pending review` markdown section on MEMORY.md /
|
|
12
|
+
* USER.md. The section is delimited by the entry separator (`\n§\n`)
|
|
13
|
+
* before the markdown header, so live entries remain unaffected by
|
|
14
|
+
* pending candidates. Format:
|
|
15
|
+
*
|
|
16
|
+
* <live entries, separated by \n§\n>
|
|
17
|
+
* §
|
|
18
|
+
* ## Pending review (2026-05-22T12:00:00Z)
|
|
19
|
+
* - [ ] mem_<uuidv7> | <text> | <rationale>
|
|
20
|
+
*
|
|
21
|
+
* `parsePending` extracts the candidate list; `appendCandidates`
|
|
22
|
+
* appends one ## Pending review block per call (idempotent on no-op).
|
|
23
|
+
*/
|
|
24
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
25
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
|
+
};
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.appendCandidates = appendCandidates;
|
|
29
|
+
exports.listPending = listPending;
|
|
30
|
+
exports.dropCandidate = dropCandidate;
|
|
31
|
+
exports.listAllPending = listAllPending;
|
|
32
|
+
const node_fs_1 = require("node:fs");
|
|
33
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
34
|
+
const identity_1 = require("../../identity");
|
|
35
|
+
const memoryManager_1 = require("../../memoryManager");
|
|
36
|
+
const PENDING_HEADER_RE = /^## Pending review/m;
|
|
37
|
+
const CANDIDATE_LINE_RE = /^- \[ \] (mem_[0-9a-f]{32})\s*\|\s*([^|]+?)\s*\|\s*(.+)$/;
|
|
38
|
+
/** Read the file (or empty string) without throwing on ENOENT. */
|
|
39
|
+
async function readOrEmpty(path) {
|
|
40
|
+
try {
|
|
41
|
+
return await node_fs_1.promises.readFile(path, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (e.code === 'ENOENT')
|
|
45
|
+
return '';
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Append a batch of candidates to the file as a fresh `## Pending review`
|
|
51
|
+
* block (markdown, after a separator). Atomic write via tmp + rename.
|
|
52
|
+
* Returns the list of memIds that were stamped onto the candidates.
|
|
53
|
+
*/
|
|
54
|
+
async function appendCandidates(filePath, file, inputs, proposedAtIso = new Date().toISOString()) {
|
|
55
|
+
if (inputs.length === 0)
|
|
56
|
+
return [];
|
|
57
|
+
const stamped = inputs.map((c) => ({
|
|
58
|
+
memId: (0, identity_1.newMemoryId)(),
|
|
59
|
+
file,
|
|
60
|
+
text: c.text.trim(),
|
|
61
|
+
rationale: c.rationale.trim(),
|
|
62
|
+
proposedAt: proposedAtIso,
|
|
63
|
+
}));
|
|
64
|
+
const block = [
|
|
65
|
+
memoryManager_1.ENTRY_SEPARATOR,
|
|
66
|
+
`## Pending review (${proposedAtIso})`,
|
|
67
|
+
...stamped.map((c) => `- [ ] ${c.memId} | ${c.text} | ${c.rationale}`),
|
|
68
|
+
'',
|
|
69
|
+
].join('\n');
|
|
70
|
+
const existing = await readOrEmpty(filePath);
|
|
71
|
+
const next = existing.endsWith('\n') || existing.length === 0
|
|
72
|
+
? existing + block
|
|
73
|
+
: existing + '\n' + block;
|
|
74
|
+
// Atomic write: ensure parent dir exists (first-run case where
|
|
75
|
+
// memories/ hasn't been created yet), then tmp + rename.
|
|
76
|
+
await node_fs_1.promises.mkdir(node_path_1.default.dirname(filePath), { recursive: true });
|
|
77
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
78
|
+
await node_fs_1.promises.writeFile(tmp, next, 'utf8');
|
|
79
|
+
await node_fs_1.promises.rename(tmp, filePath);
|
|
80
|
+
return stamped;
|
|
81
|
+
}
|
|
82
|
+
/** Parse the `## Pending review` candidates out of a file. */
|
|
83
|
+
async function listPending(filePath) {
|
|
84
|
+
const raw = await readOrEmpty(filePath);
|
|
85
|
+
if (!PENDING_HEADER_RE.test(raw))
|
|
86
|
+
return [];
|
|
87
|
+
// Default file shape: pending lives at end of file. Walk every line
|
|
88
|
+
// checking the candidate line shape — the proposedAt is associated
|
|
89
|
+
// with the most-recent `## Pending review (...)` header above each
|
|
90
|
+
// run of candidate lines.
|
|
91
|
+
const out = [];
|
|
92
|
+
let currentTs = new Date().toISOString();
|
|
93
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
94
|
+
const hMatch = /^## Pending review \(([^)]+)\)\s*$/.exec(line);
|
|
95
|
+
if (hMatch) {
|
|
96
|
+
currentTs = hMatch[1];
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const cMatch = CANDIDATE_LINE_RE.exec(line);
|
|
100
|
+
if (cMatch) {
|
|
101
|
+
out.push({
|
|
102
|
+
memId: cMatch[1],
|
|
103
|
+
// Caller knows which file this came from; we set 'memory' as a
|
|
104
|
+
// placeholder so the type checker stays happy — the CLI overrides
|
|
105
|
+
// this from the caller-side file context.
|
|
106
|
+
file: 'memory',
|
|
107
|
+
text: cMatch[2].trim(),
|
|
108
|
+
rationale: cMatch[3].trim(),
|
|
109
|
+
proposedAt: currentTs,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** Drop ONE candidate by memId from the file's pending blocks. Atomic. */
|
|
116
|
+
async function dropCandidate(filePath, memId) {
|
|
117
|
+
const raw = await readOrEmpty(filePath);
|
|
118
|
+
if (!raw.includes(memId))
|
|
119
|
+
return false;
|
|
120
|
+
const next = raw
|
|
121
|
+
.split(/\r?\n/)
|
|
122
|
+
.filter((line) => !line.includes(memId))
|
|
123
|
+
.join('\n');
|
|
124
|
+
// Clean up orphan `## Pending review (...)` headers whose lines are gone.
|
|
125
|
+
const cleaned = next.replace(/## Pending review \([^)]*\)\s*\n(?=\n*## |\n*§|\n*$)/g, '');
|
|
126
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
127
|
+
await node_fs_1.promises.writeFile(tmp, cleaned, 'utf8');
|
|
128
|
+
await node_fs_1.promises.rename(tmp, filePath);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
/** Convenience: list pending across both files. */
|
|
132
|
+
async function listAllPending(memoryPath, userPath) {
|
|
133
|
+
const m = (await listPending(memoryPath)).map((c) => ({ ...c, file: 'memory' }));
|
|
134
|
+
const u = (await listPending(userPath)).map((c) => ({ ...c, file: 'user' }));
|
|
135
|
+
return [...m, ...u];
|
|
136
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/memory/reviewer/prompt.ts — v4.9.0 Slice 10.
|
|
10
|
+
*
|
|
11
|
+
* Reviewer system prompt + the on-disk wire format parser. The reviewer
|
|
12
|
+
* LLM is told to emit one candidate per line, pipe-delimited:
|
|
13
|
+
*
|
|
14
|
+
* <file>|<text>|<rationale>
|
|
15
|
+
*
|
|
16
|
+
* where `<file>` is `memory` or `user`. Lines that don't match the
|
|
17
|
+
* shape, or that propose unknown files, are silently dropped — they
|
|
18
|
+
* count toward the `parser_drops` telemetry counter but don't fail
|
|
19
|
+
* the whole review.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.buildReviewerPrompt = buildReviewerPrompt;
|
|
23
|
+
exports.parseReviewerResponse = parseReviewerResponse;
|
|
24
|
+
const namespaceRegistry_1 = require("../namespaceRegistry");
|
|
25
|
+
/**
|
|
26
|
+
* Reviewer system prompt. Conservative, skip-rules-first. The LLM is
|
|
27
|
+
* told to follow these rules; the parser + skipRules drop any
|
|
28
|
+
* candidate that slips through anyway.
|
|
29
|
+
*/
|
|
30
|
+
function buildReviewerPrompt(opts) {
|
|
31
|
+
const turnText = opts.recentTurns
|
|
32
|
+
.map((m) => `[${m.role}] ${m.content}`)
|
|
33
|
+
.join('\n');
|
|
34
|
+
return [
|
|
35
|
+
'You are Aiden\'s post-turn memory reviewer. You read the recent',
|
|
36
|
+
'conversation and propose AT MOST ' + String(opts.maxCandidates) +
|
|
37
|
+
' additions to long-term memory.',
|
|
38
|
+
'',
|
|
39
|
+
'STRICT RULES (a candidate that violates ANY rule is DROPPED):',
|
|
40
|
+
// v4.9.0 Slice 11 — namespaces are now dynamic. The parser drops
|
|
41
|
+
// any candidate whose `<file>` field isn't in the registry; the
|
|
42
|
+
// skip-rule pass drops `project` candidates when no project root
|
|
43
|
+
// is detected.
|
|
44
|
+
' - <file> must be one of: ' + (0, namespaceRegistry_1.listNamespaceNames)().join(', ') + '.',
|
|
45
|
+
' `memory` = project / environment / Aiden facts (global).',
|
|
46
|
+
' `user` = user identity / preferences / workflow style.',
|
|
47
|
+
' `project` (when available) = current-repo-specific context.',
|
|
48
|
+
' - NO negations ("user does not X", "no longer Y"). Skip them.',
|
|
49
|
+
' - NO transient artifacts ("today", "this session", "just now").',
|
|
50
|
+
' - NO sensitive inference (health, politics, religion, finance,',
|
|
51
|
+
' sexual orientation, family planning). NEVER propose these.',
|
|
52
|
+
' - NO duplicates of what is already in memory below.',
|
|
53
|
+
' - Each candidate <= 200 chars.',
|
|
54
|
+
'',
|
|
55
|
+
'OUTPUT FORMAT — one candidate per line, exactly:',
|
|
56
|
+
' <file>|<text>|<rationale>',
|
|
57
|
+
'where <file> ∈ {memory, user}. No prose, no headers, no markdown.',
|
|
58
|
+
'If nothing is worth proposing, return an empty response.',
|
|
59
|
+
'',
|
|
60
|
+
'=== CURRENT live MEMORY.md ===',
|
|
61
|
+
opts.liveMemory || '(empty)',
|
|
62
|
+
'',
|
|
63
|
+
'=== CURRENT live USER.md ===',
|
|
64
|
+
opts.liveUser || '(empty)',
|
|
65
|
+
'',
|
|
66
|
+
'=== RECENT CONVERSATION TURNS ===',
|
|
67
|
+
turnText || '(no turns)',
|
|
68
|
+
'',
|
|
69
|
+
'Now propose candidates:',
|
|
70
|
+
].join('\n');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse the LLM response into structured candidates. Drops lines that
|
|
74
|
+
* don't match `<file>|<text>|<rationale>` or use an unknown file. The
|
|
75
|
+
* caller's skipRules pass over each result for additional validation.
|
|
76
|
+
*/
|
|
77
|
+
function parseReviewerResponse(raw) {
|
|
78
|
+
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
79
|
+
const out = [];
|
|
80
|
+
let drops = 0;
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
// Strip leading bullets / numbering the LLM may have added despite instructions.
|
|
83
|
+
const cleaned = line.replace(/^[-*\d.)\s]+/, '');
|
|
84
|
+
const parts = cleaned.split('|').map((p) => p.trim());
|
|
85
|
+
if (parts.length < 3) {
|
|
86
|
+
drops += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const [file, text, ...rest] = parts;
|
|
90
|
+
// v4.9.0 Slice 11 — accept any registered namespace name. The
|
|
91
|
+
// reviewer orchestrator's skip-rule pass drops `project`
|
|
92
|
+
// candidates when no project root resolves.
|
|
93
|
+
if (!(0, namespaceRegistry_1.listNamespaceNames)().includes(file)) {
|
|
94
|
+
drops += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const rationale = rest.join('|').trim();
|
|
98
|
+
if (!text || !rationale) {
|
|
99
|
+
drops += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
out.push({ file: file, text, rationale });
|
|
103
|
+
}
|
|
104
|
+
return { candidates: out, parserDrops: drops };
|
|
105
|
+
}
|