@ijfw/memory-server 1.3.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/bin/ijfw +27 -0
- package/bin/ijfw-dashboard +180 -0
- package/bin/ijfw-dispatch-plan +41 -0
- package/bin/ijfw-memorize +273 -0
- package/bin/ijfw-memory +51 -0
- package/fixtures/demo-target.js +28 -0
- package/package.json +53 -0
- package/src/api-client.js +190 -0
- package/src/audit-roster.js +315 -0
- package/src/caps.js +37 -0
- package/src/cold-scan-runner.mjs +37 -0
- package/src/compute/edges.js +155 -0
- package/src/compute/extract.js +560 -0
- package/src/compute/fts5.js +420 -0
- package/src/compute/graph-auto-index.js +191 -0
- package/src/compute/graph-lock.js +114 -0
- package/src/compute/index.js +18 -0
- package/src/compute/migration-runner.js +116 -0
- package/src/compute/migrations/001-initial.js +23 -0
- package/src/compute/migrations/002-porter-stemming-source.js +139 -0
- package/src/compute/migrations/003-tier-semantic.js +69 -0
- package/src/compute/migrations/004-kg-tables.js +83 -0
- package/src/compute/migrations/005-stale-candidate.js +72 -0
- package/src/compute/python-resolver.js +106 -0
- package/src/compute/runner-vm.js +185 -0
- package/src/compute/runner.js +416 -0
- package/src/compute/sandbox-detect.js +122 -0
- package/src/compute/sandbox-linux.js +164 -0
- package/src/compute/sandbox-macos.js +167 -0
- package/src/compute/sandbox-windows.js +63 -0
- package/src/compute/schema.sql +118 -0
- package/src/compute/staleness.js +239 -0
- package/src/compute/synonyms.js +367 -0
- package/src/compute/traverse.js +180 -0
- package/src/cost/aggregator.js +229 -0
- package/src/cost/pricing.js +134 -0
- package/src/cost/readers/claude.js +179 -0
- package/src/cost/readers/codex.js +131 -0
- package/src/cost/readers/gemini.js +111 -0
- package/src/cost/savings.js +243 -0
- package/src/cross-dispatcher.js +437 -0
- package/src/cross-orchestrator-cli.js +1885 -0
- package/src/cross-orchestrator.js +598 -0
- package/src/cross-project-search.js +114 -0
- package/src/dashboard-client.html +1180 -0
- package/src/dashboard-server.js +895 -0
- package/src/design-companion.js +81 -0
- package/src/dispatch/colon-syntax.js +732 -0
- package/src/dispatch-planner.js +235 -0
- package/src/dream/cooldown.js +105 -0
- package/src/dream/runner.mjs +373 -0
- package/src/dream/staleness-wiring.js +195 -0
- package/src/feedback-detector.js +57 -0
- package/src/hero-line.js +115 -0
- package/src/importers/claude-mem.js +152 -0
- package/src/importers/cli.js +311 -0
- package/src/importers/common.js +84 -0
- package/src/importers/discover.js +235 -0
- package/src/importers/rtk.js +107 -0
- package/src/intent-router.js +221 -0
- package/src/lib/atomic-io.js +201 -0
- package/src/lib/cache.js +33 -0
- package/src/lib/npm-view.js +104 -0
- package/src/lib/status-card.js +95 -0
- package/src/lib/token.js +85 -0
- package/src/memory/fts5.js +349 -0
- package/src/memory/migration-runner.js +116 -0
- package/src/memory/migrations/001-fts5-init.js +26 -0
- package/src/memory/migrations/002-tier-semantic.js +60 -0
- package/src/memory/migrations/003-stale-candidate.js +60 -0
- package/src/memory/reader.js +300 -0
- package/src/memory/recall-counter.js +76 -0
- package/src/memory/schema.sql +79 -0
- package/src/memory/search.js +431 -0
- package/src/memory/staleness.js +237 -0
- package/src/memory/tier-promotion.js +377 -0
- package/src/memory/tokenize.js +63 -0
- package/src/project-type-detector.js +866 -0
- package/src/prompt-check.js +171 -0
- package/src/ralph-allowlist.js +88 -0
- package/src/receipts.js +129 -0
- package/src/redactor.js +107 -0
- package/src/sandbox.js +275 -0
- package/src/sanitizer.js +69 -0
- package/src/scan-resume.js +167 -0
- package/src/schema.js +82 -0
- package/src/search-bm25.js +108 -0
- package/src/server.js +1414 -0
- package/src/swarm-config.js +80 -0
- package/src/trident/dispatch.js +211 -0
- package/src/trident/lens-health.js +253 -0
- package/src/update-apply.js +79 -0
- package/src/update-check.js +136 -0
- package/src/vectors.js +178 -0
- package/templates/design/bento-grid.md +84 -0
- package/templates/design/brutalist-luxe.md +82 -0
- package/templates/design/cinematic-dark.md +82 -0
- package/templates/design/data-dense-dashboard.md +88 -0
- package/templates/design/editorial-warm.md +81 -0
- package/templates/design/glassmorphic.md +84 -0
- package/templates/design/magazine-editorial.md +84 -0
- package/templates/design/maximalist-vibrant.md +85 -0
- package/templates/design/neo-swiss-tech.md +85 -0
- package/templates/design/swiss-minimal.md +80 -0
- package/templates/design/terminal-native.md +83 -0
- package/templates/design/warm-organic.md +84 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// IJFW v1.3.0 Alpha -- D3 dream-cycle runner.
|
|
3
|
+
//
|
|
4
|
+
// Replaces the legacy 5-session deferral
|
|
5
|
+
// (SESSION_NUM % 5 == 0 -> .ijfw/.startup-flags) with INLINE
|
|
6
|
+
// consolidation at SessionEnd via detached spawn. Mirrors the Phase 3
|
|
7
|
+
// cold-scan-runner.mjs contract: tiny CLI adapter, fire-and-forget,
|
|
8
|
+
// best-effort. Any throw is swallowed so a broken dream cycle never
|
|
9
|
+
// surfaces a non-zero exit to the parent SessionEnd hook.
|
|
10
|
+
//
|
|
11
|
+
// Sequence per run:
|
|
12
|
+
// 1. Cooldown check (4h via .ijfw/.dream-state.json) -- skip on hit.
|
|
13
|
+
// 2. Optional: invoke D1's tier-promotion module when present
|
|
14
|
+
// (Working -> Episodic, Episodic -> Semantic, Working -> Procedural
|
|
15
|
+
// per D-PILLAR-SPEC.md). Absent during D1 build window -> log and
|
|
16
|
+
// degrade to a "no promotion module" line.
|
|
17
|
+
// 3. Optional: invoke the existing project-journal -> knowledge.md
|
|
18
|
+
// consolidation policy (claude/commands/consolidate.md). When the
|
|
19
|
+
// Claude command itself runs as the consolidator (host=claude),
|
|
20
|
+
// runner.mjs records the dream-cycle attempt and returns; the
|
|
21
|
+
// heavy work happens in the slash command. For Codex/Gemini/
|
|
22
|
+
// Wayland/Hermes hosts, runner.mjs is the only consolidator.
|
|
23
|
+
// 4. Mark completion via cooldown.markCompleted() so the next
|
|
24
|
+
// SessionEnd within 4h is skipped.
|
|
25
|
+
//
|
|
26
|
+
// All output lands in `<repoRoot>/.ijfw/logs/dream-<timestamp>.log`.
|
|
27
|
+
//
|
|
28
|
+
// Usage:
|
|
29
|
+
// node runner.mjs --project-root <path> [--host <name>] [--reason <txt>]
|
|
30
|
+
//
|
|
31
|
+
// Discipline:
|
|
32
|
+
// - ESM, zero deps.
|
|
33
|
+
// - LC_ALL=C semantics (we only emit ASCII).
|
|
34
|
+
// - process.exit(0) on every code path -- the parent hook depends on
|
|
35
|
+
// a clean exit even for cooldown skips.
|
|
36
|
+
|
|
37
|
+
import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'node:fs';
|
|
38
|
+
import { join, dirname } from 'node:path';
|
|
39
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
|
+
import { isOnCooldown, markCompleted } from './cooldown.js';
|
|
41
|
+
|
|
42
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
43
|
+
const __dirname = dirname(__filename);
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// argv parsing
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const argv = process.argv.slice(2);
|
|
50
|
+
const opts = {
|
|
51
|
+
projectRoot: null,
|
|
52
|
+
host: 'unknown',
|
|
53
|
+
reason: 'session_end',
|
|
54
|
+
sessionId: process.env.IJFW_SESSION_ID || null,
|
|
55
|
+
};
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
const a = argv[i];
|
|
58
|
+
if (a === '--project-root') opts.projectRoot = argv[++i];
|
|
59
|
+
else if (a === '--host') opts.host = argv[++i];
|
|
60
|
+
else if (a === '--reason') opts.reason = argv[++i];
|
|
61
|
+
else if (a === '--session-id') opts.sessionId = argv[++i];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!opts.projectRoot) process.exit(0);
|
|
65
|
+
|
|
66
|
+
const stateDir = join(opts.projectRoot, '.ijfw');
|
|
67
|
+
const logDir = join(stateDir, 'logs');
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// log helper -- best-effort, never throws
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function isoStamp() {
|
|
74
|
+
return new Date().toISOString();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function logFilename() {
|
|
78
|
+
// dream-2026-05-08T10-15-32Z.log
|
|
79
|
+
return `dream-${isoStamp().replace(/[:]/g, '-')}.log`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const LOG_PATH = join(logDir, logFilename());
|
|
83
|
+
|
|
84
|
+
function log(line) {
|
|
85
|
+
try {
|
|
86
|
+
mkdirSync(logDir, { recursive: true });
|
|
87
|
+
appendFileSync(LOG_PATH, `[${isoStamp()}] ${line}\n`, 'utf8');
|
|
88
|
+
} catch {
|
|
89
|
+
// never throw out of the runner
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Cooldown gate
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
if (isOnCooldown(stateDir)) {
|
|
98
|
+
log(`skip: cooldown active (host=${opts.host}, reason=${opts.reason})`);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
log(`start: host=${opts.host}, reason=${opts.reason}, project=${opts.projectRoot}`);
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Step 1: D1 tier-promotion (when available)
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
//
|
|
108
|
+
// D1 (Wave 2 Agent E) lands `mcp-server/src/memory/tier-promotion.js`
|
|
109
|
+
// with the deterministic promotion rules from D-PILLAR-SPEC.md §1. We
|
|
110
|
+
// import-by-URL so a missing module is a soft skip rather than a top-
|
|
111
|
+
// level ESM resolve failure.
|
|
112
|
+
//
|
|
113
|
+
// Expected surface (per D-PILLAR-SPEC.md + brief):
|
|
114
|
+
// export async function runTierPromotion({ projectRoot, log })
|
|
115
|
+
// -> { promoted, superseded, skipped }
|
|
116
|
+
//
|
|
117
|
+
// Any contract drift is caught and logged; the dream cycle still
|
|
118
|
+
// continues to step 2.
|
|
119
|
+
|
|
120
|
+
async function runTierPromotion() {
|
|
121
|
+
// Resolve tier-promotion module (D1, Agent E). Search both the
|
|
122
|
+
// bundled mcp-server/src/memory layout and the repo-local layout so
|
|
123
|
+
// the runner works whether it's invoked from ~/.ijfw or the dev repo.
|
|
124
|
+
const candidates = [
|
|
125
|
+
join(__dirname, '..', 'memory', 'tier-promotion.js'),
|
|
126
|
+
join(opts.projectRoot, 'mcp-server', 'src', 'memory', 'tier-promotion.js'),
|
|
127
|
+
];
|
|
128
|
+
let modulePath = null;
|
|
129
|
+
for (const cand of candidates) {
|
|
130
|
+
if (existsSync(cand)) { modulePath = cand; break; }
|
|
131
|
+
}
|
|
132
|
+
if (!modulePath) {
|
|
133
|
+
log('tier-promotion: module not present (D1 in flight) -- skip step');
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Resolve the matching fts5.js (provides openDb / closeDb).
|
|
138
|
+
const fts5Path = join(dirname(modulePath), 'fts5.js');
|
|
139
|
+
let openDb, closeDb;
|
|
140
|
+
try {
|
|
141
|
+
const fts5 = await import(pathToFileURL(fts5Path).href);
|
|
142
|
+
openDb = fts5.openDb;
|
|
143
|
+
closeDb = fts5.closeDb;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
log(`tier-promotion: fts5 module not loadable (${err && err.message ? err.message : err}) -- skip step`);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (typeof openDb !== 'function' || typeof closeDb !== 'function') {
|
|
149
|
+
log('tier-promotion: fts5 module missing openDb/closeDb -- skip step');
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Load the promotion module and dispatch all four promotions in
|
|
154
|
+
// sequence. Each promotion is wrapped individually so a single
|
|
155
|
+
// failure (e.g. supersession scan throws on bad data) doesn't abort
|
|
156
|
+
// the others.
|
|
157
|
+
let mod;
|
|
158
|
+
try {
|
|
159
|
+
mod = await import(pathToFileURL(modulePath).href);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
log(`tier-promotion: import failed: ${err && err.message ? err.message : err}`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Two-shape contract:
|
|
166
|
+
// 1. New shape: `runTierPromotion({ projectRoot, log })` (preferred,
|
|
167
|
+
// future-proof if Agent E refactors to a single entry point).
|
|
168
|
+
// 2. Current shape (Agent E v1): named exports
|
|
169
|
+
// `promoteWorkingToEpisodic(db)` + `promoteEpisodicToSemantic(db)`
|
|
170
|
+
// that take a DB handle.
|
|
171
|
+
if (typeof mod.runTierPromotion === 'function') {
|
|
172
|
+
try {
|
|
173
|
+
const out = await mod.runTierPromotion({ projectRoot: opts.projectRoot, log });
|
|
174
|
+
log(`tier-promotion: ok ${JSON.stringify(out || {})}`);
|
|
175
|
+
return out;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
log(`tier-promotion: runTierPromotion failed: ${err && err.message ? err.message : err}`);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Per-export shape -- open DB, dispatch, close.
|
|
183
|
+
let db;
|
|
184
|
+
try {
|
|
185
|
+
db = await openDb(opts.projectRoot);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
// No DB yet (fresh project, schema not initialised). Treat as
|
|
188
|
+
// skip rather than failure -- consolidation has nothing to do.
|
|
189
|
+
log(`tier-promotion: db open skipped (${err && err.message ? err.message : err})`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const totals = { we: null, es: null };
|
|
194
|
+
try {
|
|
195
|
+
if (typeof mod.promoteWorkingToEpisodic === 'function') {
|
|
196
|
+
// Working->Episodic requires a session_id. When the runner was
|
|
197
|
+
// invoked without one (e.g. test fixture), skip the rollup with a
|
|
198
|
+
// clear log line rather than passing through Agent E's
|
|
199
|
+
// "session_id required" diagnostic on every dream cycle.
|
|
200
|
+
if (!opts.sessionId) {
|
|
201
|
+
totals.we = { skipped: 'session_id absent' };
|
|
202
|
+
} else {
|
|
203
|
+
try { totals.we = mod.promoteWorkingToEpisodic(db, { session_id: opts.sessionId }); }
|
|
204
|
+
catch (err) { totals.we = { error: String(err && err.message || err) }; }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (typeof mod.promoteEpisodicToSemantic === 'function') {
|
|
208
|
+
try { totals.es = mod.promoteEpisodicToSemantic(db); }
|
|
209
|
+
catch (err) { totals.es = { error: String(err && err.message || err) }; }
|
|
210
|
+
}
|
|
211
|
+
// GA real fix-wave F4: Working->Procedural was missing from the
|
|
212
|
+
// dream-cycle dispatch. The function exists in tier-promotion.js
|
|
213
|
+
// (per D-PILLAR-SPEC §1) but the runner only fired We + Es,
|
|
214
|
+
// leaving the Procedural tier orphaned. Wire it here.
|
|
215
|
+
//
|
|
216
|
+
// Source signal per spec: TaskUpdate completed events with duration
|
|
217
|
+
// >= 5min + matching git commit window. This runner does NOT yet
|
|
218
|
+
// have a TaskUpdate event source (event ledger lands as part of the
|
|
219
|
+
// Working observation pipeline; deeper integration with TaskUpdate
|
|
220
|
+
// events is queued). When no source events are available, the
|
|
221
|
+
// promotion call is a clean no-op (returns { promoted: 0 } since
|
|
222
|
+
// status !== 'completed' is the early-return path inside the
|
|
223
|
+
// promotion function). Wiring the call here means the moment the
|
|
224
|
+
// event source goes live the promotion fires automatically.
|
|
225
|
+
//
|
|
226
|
+
// Pattern: the runner calls promoteWorkingToProcedural with a
|
|
227
|
+
// SYNTHETIC zero-shape envelope. This exercises the dispatch path,
|
|
228
|
+
// returns { promoted: 0 }, and lands a log line so the wiring is
|
|
229
|
+
// observable. When TaskUpdate events become available, callers can
|
|
230
|
+
// pass the real envelope and promotion will fire.
|
|
231
|
+
if (typeof mod.promoteWorkingToProcedural === 'function') {
|
|
232
|
+
try {
|
|
233
|
+
// Discover any pending TaskUpdate completed events. The
|
|
234
|
+
// event source is not yet wired into the alpha runner; lookup
|
|
235
|
+
// returns an empty list, in which case we exercise the
|
|
236
|
+
// dispatch path with a deterministic zero-shape call so the
|
|
237
|
+
// tier-promotion log line confirms the wiring is live.
|
|
238
|
+
const taskEvents = discoverTaskUpdateEvents(db);
|
|
239
|
+
if (taskEvents.length === 0) {
|
|
240
|
+
// No-op call to confirm the dispatch path exists. The
|
|
241
|
+
// function early-returns on status !== 'completed'.
|
|
242
|
+
totals.wp = mod.promoteWorkingToProcedural(db, {
|
|
243
|
+
status: 'noop',
|
|
244
|
+
task_completed_event_window_hours: 24,
|
|
245
|
+
});
|
|
246
|
+
} else {
|
|
247
|
+
let promotedTotal = 0;
|
|
248
|
+
const errors = [];
|
|
249
|
+
for (const ev of taskEvents) {
|
|
250
|
+
try {
|
|
251
|
+
const r = mod.promoteWorkingToProcedural(db, ev);
|
|
252
|
+
promotedTotal += Number(r.promoted || 0);
|
|
253
|
+
if (Array.isArray(r.errors)) errors.push(...r.errors);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
errors.push(String(err && err.message || err));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
totals.wp = { promoted: promotedTotal, errors, sources: taskEvents.length };
|
|
259
|
+
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
totals.wp = { error: String(err && err.message || err) };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
log(`tier-promotion: working->episodic=${JSON.stringify(totals.we)} episodic->semantic=${JSON.stringify(totals.es)} working->procedural=${JSON.stringify(totals.wp)}`);
|
|
265
|
+
|
|
266
|
+
// GA-B1: Episodic->Semantic supersession fires D4 cascading staleness
|
|
267
|
+
// BFS over the symbol graph. Wire is best-effort -- a missing graph,
|
|
268
|
+
// missing entity match, or empty supersession array all skip silently.
|
|
269
|
+
// The runner is a SessionEnd best-effort consolidator; staleness
|
|
270
|
+
// propagation is value-add, not integrity-critical.
|
|
271
|
+
const superseded = totals.es && Array.isArray(totals.es.superseded)
|
|
272
|
+
? totals.es.superseded
|
|
273
|
+
: [];
|
|
274
|
+
if (superseded.length > 0) {
|
|
275
|
+
try {
|
|
276
|
+
const wiringMod = await import('./staleness-wiring.js');
|
|
277
|
+
const sum = await wiringMod.propagateStaleForSupersessions({
|
|
278
|
+
projectRoot: opts.projectRoot,
|
|
279
|
+
supersededEntries: superseded,
|
|
280
|
+
log,
|
|
281
|
+
});
|
|
282
|
+
totals.staleness = sum;
|
|
283
|
+
log(`staleness-wiring: superseded=${sum.superseded_count} entities=${sum.entities_resolved} nodes=${sum.nodes_propagated} flagged=${sum.flagged_total}`);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
log(`staleness-wiring: failed (${err && err.message ? err.message : err})`);
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
log('staleness-wiring: no Episodic->Semantic supersessions this cycle -- skip');
|
|
289
|
+
}
|
|
290
|
+
} finally {
|
|
291
|
+
try { closeDb(db); } catch { /* best effort */ }
|
|
292
|
+
}
|
|
293
|
+
return totals;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// F4 helper: discover TaskUpdate completed events for Procedural promotion.
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
//
|
|
300
|
+
// D-PILLAR-SPEC §1 Working->Procedural promotion fires from TaskUpdate
|
|
301
|
+
// completed events with duration >= 5min and matching git commit window.
|
|
302
|
+
// The alpha runner does not yet have a dedicated TaskUpdate event source
|
|
303
|
+
// in the working memory ledger (events arrive via observation bodies but
|
|
304
|
+
// are not parsed into structured task envelopes). When the event source
|
|
305
|
+
// goes live, this function will return real envelopes; until then, it
|
|
306
|
+
// returns an empty array so the runner exercises the dispatch path
|
|
307
|
+
// without firing real promotions.
|
|
308
|
+
//
|
|
309
|
+
// Returns: Array<TaskUpdateEnvelope> per the contract documented in
|
|
310
|
+
// mcp-server/src/memory/tier-promotion.js#promoteWorkingToProcedural.
|
|
311
|
+
// Each envelope: { task_id, status, start_ts, end_ts, body, session_id }.
|
|
312
|
+
|
|
313
|
+
function discoverTaskUpdateEvents(_db) {
|
|
314
|
+
// Alpha: zero-shape source. F4 wires the dispatch path; the source
|
|
315
|
+
// table itself is queued (deeper TaskUpdate ledger integration).
|
|
316
|
+
// Returning [] keeps the call path exercised + the runner's log line
|
|
317
|
+
// visible without firing spurious promotions on synthetic data.
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// Step 2: legacy journal -> knowledge consolidation pass
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
//
|
|
325
|
+
// Pre-D1 the canonical consolidator is the Claude /consolidate slash
|
|
326
|
+
// command (claude/commands/consolidate.md). For non-Claude hosts the
|
|
327
|
+
// runner records the attempt + writes a placeholder pattern-extraction
|
|
328
|
+
// summary so SessionEnd has a positive trail to point at. The actual
|
|
329
|
+
// scan runs against `.ijfw/memory/project-journal.md`.
|
|
330
|
+
|
|
331
|
+
function safeJournalSummary() {
|
|
332
|
+
try {
|
|
333
|
+
const journalPath = join(stateDir, 'memory', 'project-journal.md');
|
|
334
|
+
if (!existsSync(journalPath)) return { entries: 0, sessions: 0 };
|
|
335
|
+
const raw = readFileSync(journalPath, 'utf8');
|
|
336
|
+
const lines = raw.split('\n').filter((l) => l.startsWith('- ['));
|
|
337
|
+
const sessions = new Set();
|
|
338
|
+
for (const l of lines) {
|
|
339
|
+
const m = l.match(/#(\d+)/);
|
|
340
|
+
if (m) sessions.add(m[1]);
|
|
341
|
+
}
|
|
342
|
+
return { entries: lines.length, sessions: sessions.size };
|
|
343
|
+
} catch {
|
|
344
|
+
return { entries: 0, sessions: 0 };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Driver
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
(async () => {
|
|
353
|
+
try {
|
|
354
|
+
const summary = safeJournalSummary();
|
|
355
|
+
log(`journal: ${summary.entries} entries across ${summary.sessions} sessions`);
|
|
356
|
+
|
|
357
|
+
await runTierPromotion();
|
|
358
|
+
|
|
359
|
+
const ok = markCompleted(stateDir);
|
|
360
|
+
log(`mark-completed: ${ok ? 'ok' : 'failed (non-fatal)'}`);
|
|
361
|
+
log('end: clean');
|
|
362
|
+
} catch (err) {
|
|
363
|
+
// Defensive: any unexpected throw lands in the log but never
|
|
364
|
+
// surfaces a non-zero exit to the parent hook.
|
|
365
|
+
try {
|
|
366
|
+
log(`end: caught ${err && err.message ? err.message : err}`);
|
|
367
|
+
} catch {
|
|
368
|
+
// give up silently
|
|
369
|
+
}
|
|
370
|
+
} finally {
|
|
371
|
+
process.exit(0);
|
|
372
|
+
}
|
|
373
|
+
})();
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// IJFW v1.3.0 -- D3 dream-cycle staleness wiring (GA fix-wave GA-B1).
|
|
2
|
+
//
|
|
3
|
+
// Source authority: PRD-v2 section 9 Pillar D D4 + .planning/1.3.0/D-PILLAR-SPEC.md
|
|
4
|
+
// section 2 (cascading staleness propagation) + GA fix-wave finding GA-B1.
|
|
5
|
+
//
|
|
6
|
+
// PROBLEM (pre-fix-wave): the dream-cycle runner calls D1's
|
|
7
|
+
// promoteEpisodicToSemantic to fire the Episodic -> Semantic supersession,
|
|
8
|
+
// but then stops. propagateStale (D4) ships as a primitive with full
|
|
9
|
+
// grader coverage and zero production callers. End-to-end the chain
|
|
10
|
+
// Working -> Episodic -> Semantic (supersession) -> propagateStale
|
|
11
|
+
// does not fire.
|
|
12
|
+
//
|
|
13
|
+
// FIX: this module is invoked by mcp-server/src/dream/runner.mjs after
|
|
14
|
+
// promoteEpisodicToSemantic returns a non-empty `superseded` array. For
|
|
15
|
+
// each superseded Episodic body:
|
|
16
|
+
// 1. Re-extract entities (D2 extractEntities) from the body.
|
|
17
|
+
// 2. Open the compute db (where kg_nodes / kg_edges live).
|
|
18
|
+
// 3. Acquire .graph-write.lock for the propagation window.
|
|
19
|
+
// 4. For each clean entity, look up kg_nodes by (kind, name).
|
|
20
|
+
// 5. Call propagateStale on each found id.
|
|
21
|
+
// 6. Aggregate the per-supersession envelopes into a single summary.
|
|
22
|
+
//
|
|
23
|
+
// LOCK: .graph-write.lock is acquired ONCE for the whole batch so we
|
|
24
|
+
// don't thrash the lock file for every node lookup. propagateStale
|
|
25
|
+
// itself does not acquire (per its module header) -- the caller owns
|
|
26
|
+
// the window.
|
|
27
|
+
//
|
|
28
|
+
// FAILURE: every step is wrapped so a single bad supersession can't
|
|
29
|
+
// abort the dream cycle. The runner is best-effort consolidation, not
|
|
30
|
+
// integrity-critical work.
|
|
31
|
+
|
|
32
|
+
import { extractEntities } from '../compute/extract.js';
|
|
33
|
+
import { propagateStale } from '../compute/staleness.js';
|
|
34
|
+
import { propagateStaleMemory } from '../memory/staleness.js';
|
|
35
|
+
import { acquireGraphWriteLock } from '../compute/graph-lock.js';
|
|
36
|
+
import { openDb as openComputeDb, closeDb as closeComputeDb } from '../compute/fts5.js';
|
|
37
|
+
import { openDb as openMemoryDb, closeDb as closeMemoryDb } from '../memory/fts5.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* propagateStaleForSupersessions({ projectRoot, supersededEntries, log }) -> summary
|
|
41
|
+
*
|
|
42
|
+
* Walks each superseded Episodic record:
|
|
43
|
+
* - extractEntities(body) -> entity list
|
|
44
|
+
* - filter clean entities (redacted=0)
|
|
45
|
+
* - for each, query kg_nodes by (kind, name) for an existing id
|
|
46
|
+
* - propagateStale(db, id) under a held .graph-write.lock
|
|
47
|
+
*
|
|
48
|
+
* Returns:
|
|
49
|
+
* {
|
|
50
|
+
* superseded_count: number, // input length
|
|
51
|
+
* entities_resolved: number, // total clean entities mapped to kg_nodes
|
|
52
|
+
* nodes_propagated: number, // total propagateStale invocations that fired
|
|
53
|
+
* flagged_total: number, // sum of envelope.flagged_count across calls
|
|
54
|
+
* errors: string[], // accumulated swallowed errors (for log)
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* `log(msg)` is the runner's log helper (best-effort, never throws). If
|
|
58
|
+
* absent, defaults to a no-op so this module is safe to call from
|
|
59
|
+
* non-runner contexts (e.g. unit tests).
|
|
60
|
+
*/
|
|
61
|
+
export async function propagateStaleForSupersessions({ projectRoot, supersededEntries, log }) {
|
|
62
|
+
const summary = {
|
|
63
|
+
superseded_count: 0,
|
|
64
|
+
entities_resolved: 0,
|
|
65
|
+
nodes_propagated: 0,
|
|
66
|
+
flagged_total: 0,
|
|
67
|
+
flagged_compute: 0,
|
|
68
|
+
flagged_memory: 0,
|
|
69
|
+
errors: [],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const logger = typeof log === 'function' ? log : () => {};
|
|
73
|
+
const entries = Array.isArray(supersededEntries) ? supersededEntries : [];
|
|
74
|
+
summary.superseded_count = entries.length;
|
|
75
|
+
if (entries.length === 0) return summary;
|
|
76
|
+
if (typeof projectRoot !== 'string' || !projectRoot) {
|
|
77
|
+
summary.errors.push('propagateStaleForSupersessions: projectRoot required');
|
|
78
|
+
return summary;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let computeDb = null;
|
|
82
|
+
let memDb = null;
|
|
83
|
+
let lock = null;
|
|
84
|
+
try {
|
|
85
|
+
computeDb = await openComputeDb(projectRoot);
|
|
86
|
+
if (!hasGraphTables(computeDb)) {
|
|
87
|
+
logger('propagateStaleForSupersessions: kg_nodes table absent (compute migrations not run); skip');
|
|
88
|
+
return summary;
|
|
89
|
+
}
|
|
90
|
+
// GA real fix-wave F2: also open memory db so we can propagate to
|
|
91
|
+
// memory_entries.stale_candidate. Open is best-effort -- a missing
|
|
92
|
+
// memory db (fresh project, dream cycle running before any memory
|
|
93
|
+
// ingest) just means no memory rows exist yet to flag, which is
|
|
94
|
+
// fine. Keep going with compute-only propagation in that case.
|
|
95
|
+
try {
|
|
96
|
+
memDb = await openMemoryDb(projectRoot);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
logger(`propagateStaleForSupersessions: memory db open skipped (${err && err.message ? err.message : err}); compute-only propagation`);
|
|
99
|
+
memDb = null;
|
|
100
|
+
}
|
|
101
|
+
lock = acquireGraphWriteLock(projectRoot, { waitMs: 5000 });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
summary.errors.push(`propagateStaleForSupersessions: setup failed (${err && err.message ? err.message : err})`);
|
|
104
|
+
if (lock) { try { lock.released(); } catch { /* ignore */ } }
|
|
105
|
+
if (computeDb) { try { closeComputeDb(computeDb); } catch { /* ignore */ } }
|
|
106
|
+
if (memDb) { try { closeMemoryDb(memDb); } catch { /* ignore */ } }
|
|
107
|
+
return summary;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const lookupNode = computeDb.prepare(
|
|
112
|
+
`SELECT id, redacted FROM kg_nodes WHERE kind = ? AND name = ?`
|
|
113
|
+
);
|
|
114
|
+
for (const sup of entries) {
|
|
115
|
+
const body = sup && typeof sup.body === 'string' ? sup.body : '';
|
|
116
|
+
if (!body) continue;
|
|
117
|
+
let entities;
|
|
118
|
+
try {
|
|
119
|
+
entities = extractEntities(body, { minMentions: 1 });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
summary.errors.push(`extractEntities for episodic id=${sup.id}: ${err && err.message ? err.message : err}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!entities || entities.length === 0) continue;
|
|
125
|
+
|
|
126
|
+
// Map each clean entity to a kg_node id; redacted entities are
|
|
127
|
+
// skipped (they never seed kg_nodes per D-PILLAR-SPEC section 3).
|
|
128
|
+
const nodeIds = new Set();
|
|
129
|
+
for (const ent of entities) {
|
|
130
|
+
if (ent.redacted) continue;
|
|
131
|
+
try {
|
|
132
|
+
const row = lookupNode.get(ent.kind, ent.name);
|
|
133
|
+
if (!row) continue;
|
|
134
|
+
if (Number(row.redacted) === 1) continue;
|
|
135
|
+
nodeIds.add(Number(row.id));
|
|
136
|
+
} catch (err) {
|
|
137
|
+
summary.errors.push(`lookupNode ${ent.kind}:${ent.name}: ${err && err.message ? err.message : err}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
summary.entities_resolved += nodeIds.size;
|
|
141
|
+
if (nodeIds.size === 0) continue;
|
|
142
|
+
|
|
143
|
+
// Fire propagateStale on each resolved kg_node id. Each call
|
|
144
|
+
// walks BFS depth_cap=2 and flags downstream observations on
|
|
145
|
+
// BOTH the compute store (raw + compiled) AND the memory store
|
|
146
|
+
// (memory_entries.stale_candidate). Pre-F2 only the compute side
|
|
147
|
+
// ran; the memory column existed but was never written.
|
|
148
|
+
for (const id of nodeIds) {
|
|
149
|
+
try {
|
|
150
|
+
const env = propagateStale(computeDb, id);
|
|
151
|
+
summary.nodes_propagated++;
|
|
152
|
+
summary.flagged_compute += Number(env.flagged_count || 0);
|
|
153
|
+
summary.flagged_total += Number(env.flagged_count || 0);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
summary.errors.push(`propagateStale id=${id}: ${err && err.message ? err.message : err}`);
|
|
156
|
+
}
|
|
157
|
+
// GA real fix-wave F2: memory-side propagation. Walks the same
|
|
158
|
+
// compute kg BFS frontier but writes to memory_entries.
|
|
159
|
+
if (memDb) {
|
|
160
|
+
try {
|
|
161
|
+
const memEnv = propagateStaleMemory(memDb, computeDb, id);
|
|
162
|
+
summary.flagged_memory += Number(memEnv.flagged_count || 0);
|
|
163
|
+
summary.flagged_total += Number(memEnv.flagged_count || 0);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
summary.errors.push(`propagateStaleMemory id=${id}: ${err && err.message ? err.message : err}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} finally {
|
|
171
|
+
if (lock) { try { lock.released(); } catch { /* best-effort */ } }
|
|
172
|
+
if (computeDb) { try { closeComputeDb(computeDb); } catch { /* best-effort */ } }
|
|
173
|
+
if (memDb) { try { closeMemoryDb(memDb); } catch { /* best-effort */ } }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
logger(`propagateStaleForSupersessions: superseded=${summary.superseded_count} entities=${summary.entities_resolved} nodes=${summary.nodes_propagated} flagged=${summary.flagged_total} (compute=${summary.flagged_compute} memory=${summary.flagged_memory}) errors=${summary.errors.length}`);
|
|
177
|
+
return summary;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- helpers --------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
function hasGraphTables(db) {
|
|
183
|
+
try {
|
|
184
|
+
const row = db.prepare(
|
|
185
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='kg_nodes'`
|
|
186
|
+
).get();
|
|
187
|
+
return !!row;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const __test = { hasGraphTables };
|
|
194
|
+
|
|
195
|
+
export default { propagateStaleForSupersessions };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// --- Feedback detector (W3.7 / H3) ---
|
|
2
|
+
//
|
|
3
|
+
// Deterministic detection of user-feedback phrases that should be promoted
|
|
4
|
+
// to feedback memories at session end: corrections ("don't X"), confirmations
|
|
5
|
+
// ("yes that was right"), and preference drifts ("keep doing Y").
|
|
6
|
+
//
|
|
7
|
+
// The session-end synthesizer (W3.9) reads these as structured signals and
|
|
8
|
+
// asks the LLM to generalize them into feedback entries with Why + How-to-apply.
|
|
9
|
+
//
|
|
10
|
+
// Pure regex, no LLM. High precision, low recall.
|
|
11
|
+
|
|
12
|
+
const PATTERNS = [
|
|
13
|
+
// Corrections -- "don't X", "stop X", "not that", "no/wrong"
|
|
14
|
+
{ kind: 'correction', re: /\bdon'?t\s+(do|add|use|call|write|include|keep|ever)\b/i },
|
|
15
|
+
{ kind: 'correction', re: /\bstop\s+(doing|adding|using|calling|writing|including)\b/i },
|
|
16
|
+
{ kind: 'correction', re: /\b(?:no|not|wrong|nope)[,.!]/i },
|
|
17
|
+
{ kind: 'correction', re: /\bdon'?t do (?:that|this|it)\b/i },
|
|
18
|
+
|
|
19
|
+
// Confirmations -- "yes that was right", "perfect", "exactly"
|
|
20
|
+
{ kind: 'confirmation', re: /\b(?:yes|yep|yup)[,.!]?\s+(?:that|this)\s+(?:was|is)\s+(?:right|correct|good|great|perfect)\b/i },
|
|
21
|
+
{ kind: 'confirmation', re: /\b(?:perfect|exactly|spot on|nailed it|great job|well done)\b/i },
|
|
22
|
+
{ kind: 'confirmation', re: /\bkeep doing (?:that|this|it)\b/i },
|
|
23
|
+
|
|
24
|
+
// Preferences -- "I prefer X", "from now on X"
|
|
25
|
+
{ kind: 'preference', re: /\bI prefer\b/i },
|
|
26
|
+
{ kind: 'preference', re: /\bfrom now on[, ]/i },
|
|
27
|
+
{ kind: 'preference', re: /\b(?:always|never) (?:do|use|add|include)\b/i },
|
|
28
|
+
|
|
29
|
+
// Generalization cues -- "every time", "each X"
|
|
30
|
+
{ kind: 'rule', re: /\b(?:every time|each time|whenever|any time)\b/i },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export function detectFeedback(prompt) {
|
|
34
|
+
if (typeof prompt !== 'string' || !prompt) return [];
|
|
35
|
+
const hits = [];
|
|
36
|
+
for (const { kind, re } of PATTERNS) {
|
|
37
|
+
const m = prompt.match(re);
|
|
38
|
+
if (m) {
|
|
39
|
+
hits.push({ kind, phrase: m[0].trim(), context: snippet(prompt, m.index ?? 0, 120) });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Deduplicate by kind -- one signal per kind per prompt is enough for synthesis.
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
return hits.filter(h => {
|
|
45
|
+
if (seen.has(h.kind)) return false;
|
|
46
|
+
seen.add(h.kind);
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function snippet(text, start, width) {
|
|
52
|
+
const from = Math.max(0, start - 20);
|
|
53
|
+
const to = Math.min(text.length, start + width);
|
|
54
|
+
const prefix = from > 0 ? '…' : '';
|
|
55
|
+
const suffix = to < text.length ? '…' : '';
|
|
56
|
+
return prefix + text.slice(from, to).replace(/\s+/g, ' ').trim() + suffix;
|
|
57
|
+
}
|