@ijfw/memory-server 1.4.4 → 1.5.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/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +1 -1
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +550 -14
- package/src/cross-orchestrator.js +1016 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +554 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +152 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/obsidian-parser.js +91 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +10 -0
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +249 -0
- package/src/orchestrator/review.js +38 -3
- package/src/orchestrator/runtime-loop.js +430 -0
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1764 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +656 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -0
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +603 -59
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +94 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +41 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +41 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +278 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
package/src/server.js
CHANGED
|
@@ -33,9 +33,28 @@ import { checkPrompt } from './prompt-check.js';
|
|
|
33
33
|
import { applyCaps, CAP_CONTENT } from './caps.js';
|
|
34
34
|
import { ensureSchemaHeader, SCHEMA_HEADER } from './schema.js';
|
|
35
35
|
import { searchCorpus } from './search-bm25.js';
|
|
36
|
+
// r17 (cold-tier wire-up): hybrid BM25+vector ranking when IJFW_VECTORS=on AND
|
|
37
|
+
// @xenova/transformers is installed. Silent BM25-only fallback otherwise.
|
|
38
|
+
// Lives in its own module so tests can drive the rerank helper without
|
|
39
|
+
// importing server.js (whose stdio bootstrap would hang the test runner).
|
|
40
|
+
import { maybeRerankWithVectors } from './search-hybrid.js';
|
|
36
41
|
import { crossProjectSearch } from './cross-project-search.js';
|
|
37
42
|
// R2-E -- single source of truth for markdown/HTML/control-char defanger.
|
|
38
43
|
import { sanitizeContent } from './sanitizer.js';
|
|
44
|
+
// H5.5 / H5.6 — ingest-time fact extraction + semantic dedup. Closes
|
|
45
|
+
// memory-engine.md competitor gaps (mem0/Zep extract facts; Graphiti dedups).
|
|
46
|
+
// Both are pure-JS, zero-LLM, deterministic.
|
|
47
|
+
import { extractFacts, factToJsonl } from './memory/fact-extractor.js';
|
|
48
|
+
import { findNearDuplicate, readDedupConfig } from './memory/dedup.js';
|
|
49
|
+
// v1.5.0 audit H5.4 — Graphiti-style bi-temporal validity. Lets storing a
|
|
50
|
+
// contradictory fact close the prior's valid_to instead of accumulating.
|
|
51
|
+
import {
|
|
52
|
+
openTemporalDbSync,
|
|
53
|
+
storeFactBitemporal,
|
|
54
|
+
getValidAt as temporalGetValidAt,
|
|
55
|
+
getHistory as temporalGetHistory,
|
|
56
|
+
getAllFactsWithWindows as temporalGetAllFactsWithWindows,
|
|
57
|
+
} from './memory/temporal.js';
|
|
39
58
|
// 1.1.6: update tools (cap 8 -> 10) -- token-issuance + OOB terminal confirm.
|
|
40
59
|
// Per CLAUDE.md policy: future growth triggers retirement review, not raise.
|
|
41
60
|
import { ijfwUpdateCheck, TOOL_DEF as UPDATE_CHECK_TOOL } from './update-check.js';
|
|
@@ -394,8 +413,11 @@ function readOr(filepath, fallback = '') {
|
|
|
394
413
|
// --- Append helper (atomic for entries < PIPE_BUF; append-only growth) ---
|
|
395
414
|
//
|
|
396
415
|
// We rely on POSIX O_APPEND atomicity for entries under 4KB. Sanitized
|
|
397
|
-
// entries are bounded at MAX_STORE_LENGTH=
|
|
398
|
-
// keeps each *line* well under 4KB after sanitization
|
|
416
|
+
// entries are bounded at MAX_STORE_LENGTH=4096 chars (CAP_CONTENT in caps.js),
|
|
417
|
+
// but the entry header keeps each *line* well under 4KB after sanitization
|
|
418
|
+
// (single-line collapse). Audit MED #8 / F-COR-1: doc-vs-code parity fix --
|
|
419
|
+
// the prior text said 5000, which had drifted from the actual cap.
|
|
420
|
+
|
|
399
421
|
function appendLine(filepath, line) {
|
|
400
422
|
try {
|
|
401
423
|
if (!existsSync(filepath)) {
|
|
@@ -593,6 +615,105 @@ function getRecentJournalEntries(count = 5) {
|
|
|
593
615
|
return entries.slice(-count).join('\n');
|
|
594
616
|
}
|
|
595
617
|
|
|
618
|
+
// H5.6 — dedup needs `recents` shaped as { id, content } for findNearDuplicate.
|
|
619
|
+
// We synthesize id from the timestamp (project-journal entries are unique by ts
|
|
620
|
+
// down to ms; collisions would only happen on near-simultaneous writes which
|
|
621
|
+
// our atomic-append + fs flush ordering already serialise).
|
|
622
|
+
function getRecentMemoriesForDedup(limit = 50) {
|
|
623
|
+
const journal = readOr(join(MEMORY_DIR, 'project-journal.md'));
|
|
624
|
+
if (!journal) return [];
|
|
625
|
+
const lines = journal.split('\n').filter(l => /^- \[\d{4}-/.test(l));
|
|
626
|
+
// Most-recent-last in the file → reverse so findNearDuplicate sees newest first.
|
|
627
|
+
const slice = lines.slice(-limit).reverse();
|
|
628
|
+
return slice.map(line => {
|
|
629
|
+
// Format: "- [<iso>] <body>" → { id: iso, content: body }
|
|
630
|
+
const m = line.match(/^- \[([^\]]+)\]\s*(.*)$/);
|
|
631
|
+
if (!m) return null;
|
|
632
|
+
return { id: m[1], content: m[2] };
|
|
633
|
+
}).filter(Boolean);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// H5.5 — sidecar file for structured facts (one JSON object per line).
|
|
637
|
+
// Append-only; consumed by handleRecall({context_hint:'facts'}).
|
|
638
|
+
const FACTS_FILE = join(MEMORY_DIR, 'facts.jsonl');
|
|
639
|
+
|
|
640
|
+
// Stable short id for joining a fact back to its journal entry. We don't have
|
|
641
|
+
// a uuid; the journal-line text itself + ts is unique enough for cross-ref.
|
|
642
|
+
function factMemoryIdFor(journalEntryText) {
|
|
643
|
+
return 'm-' + createHash('sha256')
|
|
644
|
+
.update(String(journalEntryText) + ':' + Date.now())
|
|
645
|
+
.digest('hex')
|
|
646
|
+
.slice(0, 10);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function appendFactsToSidecar(facts, meta) {
|
|
650
|
+
if (!Array.isArray(facts) || facts.length === 0) return { ok: true, written: 0 };
|
|
651
|
+
try {
|
|
652
|
+
const lines = facts.map(f => factToJsonl(f, meta)).join('\n') + '\n';
|
|
653
|
+
appendFileSync(FACTS_FILE, lines);
|
|
654
|
+
return { ok: true, written: facts.length };
|
|
655
|
+
} catch (err) {
|
|
656
|
+
// Non-fatal: facts are augmentation, not source-of-truth. Journal already
|
|
657
|
+
// captured the raw memory.
|
|
658
|
+
return { ok: false, code: err.code || 'EUNKNOWN', message: err.message };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// v1.5.0 audit H5.4 — sibling SQL store for bi-temporal facts. Lives next to
|
|
663
|
+
// facts.jsonl so the JSONL sidecar (append-only timeline log) and the SQL
|
|
664
|
+
// table (queryable point-in-time view) stay co-located. Lazy-opened on first
|
|
665
|
+
// use so a project that never stores a memory never pays the better-sqlite3
|
|
666
|
+
// load cost.
|
|
667
|
+
const FACTS_DB_FILE = join(MEMORY_DIR, 'facts.db');
|
|
668
|
+
let _factsDbHandle = null;
|
|
669
|
+
function getFactsDb() {
|
|
670
|
+
if (_factsDbHandle) return _factsDbHandle;
|
|
671
|
+
try {
|
|
672
|
+
_factsDbHandle = openTemporalDbSync(FACTS_DB_FILE);
|
|
673
|
+
return _factsDbHandle;
|
|
674
|
+
} catch (err) {
|
|
675
|
+
// Non-fatal. JSONL sidecar still gets written; we just lose the bi-temporal
|
|
676
|
+
// SQL view for this process. Surface via stderr so operators see the
|
|
677
|
+
// degradation but the user-facing store result still says "ok".
|
|
678
|
+
try {
|
|
679
|
+
process.stderr.write(`[ijfw temporal] facts.db unavailable (${err.code || err.message}); SQL fact view degraded\n`);
|
|
680
|
+
} catch { /* stderr may be detached */ }
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// writeFactsBitemporal -- for each extracted fact, close any prior currently-
|
|
686
|
+
// valid fact with the same (subject, predicate) but different object, then
|
|
687
|
+
// insert the new fact. Wrapped in a per-fact transaction inside temporal.js's
|
|
688
|
+
// storeFactBitemporal helper. Best-effort: a SQL failure logs to stderr but
|
|
689
|
+
// never breaks the journal-or-JSONL path.
|
|
690
|
+
function writeFactsBitemporal(facts, meta) {
|
|
691
|
+
if (!Array.isArray(facts) || facts.length === 0) return { ok: true, written: 0, invalidated: 0 };
|
|
692
|
+
const db = getFactsDb();
|
|
693
|
+
if (!db) return { ok: false, code: 'ENOFACTSDB', written: 0, invalidated: 0 };
|
|
694
|
+
let written = 0;
|
|
695
|
+
let invalidated = 0;
|
|
696
|
+
for (const f of facts) {
|
|
697
|
+
try {
|
|
698
|
+
const r = storeFactBitemporal(db, {
|
|
699
|
+
subject: f.subject,
|
|
700
|
+
predicate: f.predicate,
|
|
701
|
+
object: f.object,
|
|
702
|
+
confidence: f.confidence,
|
|
703
|
+
memory_id: meta && meta.memory_id,
|
|
704
|
+
source: meta && meta.source,
|
|
705
|
+
}, meta && meta.ts);
|
|
706
|
+
invalidated += r.invalidated;
|
|
707
|
+
if (!r.deduped) written += 1;
|
|
708
|
+
} catch (err) {
|
|
709
|
+
try {
|
|
710
|
+
process.stderr.write(`[ijfw temporal] storeFactBitemporal failed: ${err.message}\n`);
|
|
711
|
+
} catch { /* stderr may be detached */ }
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return { ok: true, written, invalidated };
|
|
715
|
+
}
|
|
716
|
+
|
|
596
717
|
// --- Cross-project registry (Phase 3) ---
|
|
597
718
|
//
|
|
598
719
|
// Registry lines look like: <abs-path> | <sha256-12> | <first-seen-iso>
|
|
@@ -635,55 +756,81 @@ function readProjectMemory(projectPath) {
|
|
|
635
756
|
};
|
|
636
757
|
}
|
|
637
758
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
for (const entry of readRegistry()) {
|
|
645
|
-
const tag = basename(entry.path);
|
|
646
|
-
const mem = readProjectMemory(entry.path);
|
|
647
|
-
for (const [src, content] of Object.entries(mem)) {
|
|
648
|
-
if (!content) continue;
|
|
649
|
-
const lines = content.split('\n');
|
|
650
|
-
for (let i = 0; i < lines.length; i++) {
|
|
651
|
-
const line = lines[i];
|
|
652
|
-
if (line.trim().length === 0) continue;
|
|
653
|
-
const score = keywords.filter(k => line.toLowerCase().includes(k)).length;
|
|
654
|
-
if (score > 0) {
|
|
655
|
-
results.push({
|
|
656
|
-
source: `${src}@${tag}`,
|
|
657
|
-
line: i + 1,
|
|
658
|
-
content: `[project:${tag}] ${line.trim().substring(0, 200)}`,
|
|
659
|
-
score
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
results.sort((a, b) => b.score - a.score);
|
|
666
|
-
return results.slice(0, limit);
|
|
667
|
-
}
|
|
759
|
+
// v1.5.1 H1.5 (audit memory-engine.md F-FUN-4): the legacy naive-keyword-count
|
|
760
|
+
// `searchAcrossProjects` was removed. `scope:'all'` now routes to the BM25-
|
|
761
|
+
// ranked `crossProjectSearch` in cross-project-search.js (which already
|
|
762
|
+
// returns the same `{ source, line, content, score }` shape via the
|
|
763
|
+
// `[project:<name>]` content prefix). Two parallel cross-project surfaces
|
|
764
|
+
// existed; the worse one was the default. Now there is one.
|
|
668
765
|
|
|
669
766
|
// --- Search ---
|
|
670
767
|
// P5.1 / H4 -- BM25 ranking over line-level docs. Source tags and line
|
|
671
768
|
// numbers preserved so callers get the same output shape; scoring is
|
|
672
769
|
// BM25 (IDF + TF + length-normalized) with per-source boost. Team tier
|
|
673
770
|
// ranks first via a score bump for ties.
|
|
674
|
-
|
|
771
|
+
//
|
|
772
|
+
// r17 (cold-tier wire-up): when IJFW_VECTORS=on AND @xenova/transformers is
|
|
773
|
+
// installed AND the model is loadable, the BM25 top-K is reranked via cosine
|
|
774
|
+
// similarity over the snippet text (blended weights wBm25=0.6, wVec=0.4 from
|
|
775
|
+
// vectors.js defaults). Async because embedder load + embed() are async. The
|
|
776
|
+
// `opts.embedder` parameter lets tests inject a mock embedder without
|
|
777
|
+
// installing @xenova/transformers.
|
|
778
|
+
|
|
779
|
+
// v1.5.0 wire-W1.C — lazy memory.db handle for the embedding cache.
|
|
780
|
+
// Opens once per process and reuses thereafter. Returns null when the
|
|
781
|
+
// project has no .ijfw/index/memory.db (e.g. fresh checkout that never
|
|
782
|
+
// stored a memory) so the rerank still falls back to live embed.
|
|
783
|
+
let _memoryDbForRerank = null;
|
|
784
|
+
async function getMemoryDbForRerank() {
|
|
785
|
+
if (_memoryDbForRerank) return _memoryDbForRerank;
|
|
786
|
+
try {
|
|
787
|
+
const { openDb } = await import('./memory/fts5.js');
|
|
788
|
+
_memoryDbForRerank = await openDb(PROJECT_DIR);
|
|
789
|
+
return _memoryDbForRerank;
|
|
790
|
+
} catch {
|
|
791
|
+
_memoryDbForRerank = null;
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function searchMemory(query, limit = 10, scope = 'project', opts = {}) {
|
|
675
797
|
limit = Math.min(Math.max(1, limit | 0), MAX_SEARCH_RESULTS);
|
|
676
|
-
if (scope === 'all')
|
|
798
|
+
if (scope === 'all') {
|
|
799
|
+
// v1.5.1 H1.5 (audit memory-engine.md F-FUN-4): use BM25-ranked
|
|
800
|
+
// crossProjectSearch, not the legacy naive keyword-count scan.
|
|
801
|
+
const projects = readRegistry();
|
|
802
|
+
if (projects.length === 0) return [];
|
|
803
|
+
return crossProjectSearch(query, projects, readProjectMemory, { limit });
|
|
804
|
+
}
|
|
677
805
|
|
|
678
806
|
const sources = [
|
|
679
|
-
{ name: 'team', content: readTeamKnowledge(), boost: 1.25 },
|
|
680
|
-
{ name: 'knowledge', content: readKnowledgeBase(), boost: 1.15 },
|
|
681
|
-
{ name: 'journal', content: readOr(join(MEMORY_DIR, 'project-journal.md')), boost: 1.0 },
|
|
682
|
-
{ name: 'handoff', content: readHandoff(), boost: 1.1 },
|
|
683
|
-
{ name: 'global', content: readGlobalKnowledge(), boost: 0.95 },
|
|
684
|
-
{ name: 'claude-native', content: readNativeClaudeMemory(), boost: 0.95 },
|
|
807
|
+
{ name: 'team', content: readTeamKnowledge(), boost: 1.25, path: join(MEMORY_DIR, 'team-knowledge.md') },
|
|
808
|
+
{ name: 'knowledge', content: readKnowledgeBase(), boost: 1.15, path: join(MEMORY_DIR, 'knowledge.md') },
|
|
809
|
+
{ name: 'journal', content: readOr(join(MEMORY_DIR, 'project-journal.md')), boost: 1.0, path: join(MEMORY_DIR, 'project-journal.md') },
|
|
810
|
+
{ name: 'handoff', content: readHandoff(), boost: 1.1, path: join(MEMORY_DIR, 'handoff.md') },
|
|
811
|
+
{ name: 'global', content: readGlobalKnowledge(), boost: 0.95, path: null },
|
|
812
|
+
{ name: 'claude-native', content: readNativeClaudeMemory(), boost: 0.95, path: null },
|
|
685
813
|
];
|
|
686
814
|
|
|
815
|
+
// v1.5.0 audit MED #12 (memory-engine.md F-FUN-5): recency decay on the
|
|
816
|
+
// boosted map. Each source file has an mtime; per-result age (days since
|
|
817
|
+
// mtime) feeds Math.exp(-ageDays / 90), so a 90-day-old source decays
|
|
818
|
+
// by ~1/e (0.37) and a 1-year-old source by ~0.018. Fresh entries
|
|
819
|
+
// (< 1 day) stay essentially unchanged (~0.99). Sources without a file
|
|
820
|
+
// (global, claude-native) get no decay (multiplier 1.0).
|
|
821
|
+
const RECENCY_HALFLIFE_DAYS = 90;
|
|
822
|
+
const nowMs = Date.now();
|
|
823
|
+
const sourceDecay = new Map();
|
|
824
|
+
for (const src of sources) {
|
|
825
|
+
if (!src.path) { sourceDecay.set(src.name, 1); continue; }
|
|
826
|
+
let ageDays = 0;
|
|
827
|
+
try {
|
|
828
|
+
const st = statSync(src.path);
|
|
829
|
+
ageDays = Math.max(0, (nowMs - st.mtimeMs) / 86400000);
|
|
830
|
+
} catch { ageDays = 0; }
|
|
831
|
+
sourceDecay.set(src.name, Math.exp(-ageDays / RECENCY_HALFLIFE_DAYS));
|
|
832
|
+
}
|
|
833
|
+
|
|
687
834
|
const docs = [];
|
|
688
835
|
const meta = new Map();
|
|
689
836
|
for (const src of sources) {
|
|
@@ -702,19 +849,46 @@ function searchMemory(query, limit = 10, scope = 'project') {
|
|
|
702
849
|
const ranked = searchCorpus(query, docs, { limit: limit * 3 });
|
|
703
850
|
if (ranked.length === 0) return [];
|
|
704
851
|
|
|
705
|
-
|
|
852
|
+
// r17: cold-tier hybrid rerank. Pure no-op when vectors disabled OR
|
|
853
|
+
// embedder unavailable. Never throws into the caller.
|
|
854
|
+
//
|
|
855
|
+
// v1.5.0 wire-W1.C: when the caller didn't supply a db handle but the
|
|
856
|
+
// memory.db exists for this project, open it lazily + thread through so
|
|
857
|
+
// the embedding cache backs the rerank. The default modelId mirrors
|
|
858
|
+
// vectors.js DEFAULT_MODEL so first-call writes match cache reads from
|
|
859
|
+
// the same process on later calls.
|
|
860
|
+
//
|
|
861
|
+
// r20-MED fix: previously the lazy-open was SKIPPED whenever opts.embedder
|
|
862
|
+
// was supplied (intended as a test-seam guard). That meant any caller
|
|
863
|
+
// passing a custom embedder (e.g. an HTTP-backed one) lost the cache.
|
|
864
|
+
// Now: always lazy-open when !opts.db. Tests that want to disable the
|
|
865
|
+
// cache pass opts.db = null explicitly.
|
|
866
|
+
const rerankOpts = { ...opts };
|
|
867
|
+
if (!rerankOpts.db && rerankOpts.db !== null) {
|
|
868
|
+
try {
|
|
869
|
+
rerankOpts.db = await getMemoryDbForRerank();
|
|
870
|
+
} catch { /* memory db unavailable -- skip cache, fall back to live embed */ }
|
|
871
|
+
}
|
|
872
|
+
if (!rerankOpts.modelId) {
|
|
873
|
+
rerankOpts.modelId = process.env.IJFW_VECTORS_MODEL || 'Xenova/all-MiniLM-L6-v2';
|
|
874
|
+
}
|
|
875
|
+
const reranked = await maybeRerankWithVectors(query, ranked, rerankOpts);
|
|
876
|
+
|
|
877
|
+
const boosted = reranked.map(r => {
|
|
706
878
|
const m = meta.get(r.id);
|
|
879
|
+
const decay = sourceDecay.get(m.source) ?? 1;
|
|
707
880
|
return {
|
|
708
881
|
source: m.source,
|
|
709
882
|
line: m.line,
|
|
710
883
|
content: (r.snippet || '').substring(0, 200),
|
|
711
|
-
score: r.score * (m.boost || 1),
|
|
884
|
+
score: r.score * (m.boost || 1) * decay,
|
|
712
885
|
};
|
|
713
886
|
});
|
|
714
887
|
boosted.sort((a, b) => b.score - a.score);
|
|
715
888
|
return boosted.slice(0, limit);
|
|
716
889
|
}
|
|
717
890
|
|
|
891
|
+
|
|
718
892
|
// --- DESIGN picker (1.2.0 Phase 5) ---
|
|
719
893
|
// MCP-only delivery of the 12-template design catalog for OpenCode / Qwen
|
|
720
894
|
// Code / Kimi Code / OpenClaw / Aider. No new tool -- served via existing
|
|
@@ -805,7 +979,7 @@ const TOOLS = [
|
|
|
805
979
|
inputSchema: {
|
|
806
980
|
type: 'object',
|
|
807
981
|
properties: {
|
|
808
|
-
content: { type: 'string', description: 'Full statement of what to remember. Max
|
|
982
|
+
content: { type: 'string', description: 'Full statement of what to remember. Max 4096 chars. Sanitised on storage.' },
|
|
809
983
|
type: { type: 'string', enum: VALID_MEMORY_TYPES, description: 'Memory tier: decision or pattern -> knowledge base (frontmatter). handoff -> overwrites handoff.md. preference -> project-namespaced global. observation -> journal only.' },
|
|
810
984
|
summary: { type: 'string', description: 'Optional 1-line summary (≤80 chars). Used as the frontmatter name for decisions/patterns.' },
|
|
811
985
|
why: { type: 'string', description: 'Optional rationale -- why this decision was made. Populates the Why section in the knowledge base entry.' },
|
|
@@ -844,6 +1018,21 @@ const TOOLS = [
|
|
|
844
1018
|
required: []
|
|
845
1019
|
}
|
|
846
1020
|
},
|
|
1021
|
+
{
|
|
1022
|
+
// v1.5.0 M5 (INT.6) -- bi-temporal facts MCP surface.
|
|
1023
|
+
name: 'ijfw_memory_facts',
|
|
1024
|
+
description: 'Query the bi-temporal facts table (subject/predicate/object timeline with valid_from / valid_to). Default: current-valid rows only. Pass history=true for full timeline; pass valid_at=<ISO-8601> for point-in-time. Subject + predicate are required.',
|
|
1025
|
+
inputSchema: {
|
|
1026
|
+
type: 'object',
|
|
1027
|
+
properties: {
|
|
1028
|
+
subject: { type: 'string', description: 'Fact subject (e.g. "v1.5.0").' },
|
|
1029
|
+
predicate: { type: 'string', description: 'Fact predicate (e.g. "ship_date").' },
|
|
1030
|
+
valid_at: { type: 'string', description: 'Optional ISO-8601 timestamp. Returns rows whose validity window covers this instant.' },
|
|
1031
|
+
history: { type: 'boolean', description: 'If true, return all rows (current + invalidated) ordered DESC by valid_from.' }
|
|
1032
|
+
},
|
|
1033
|
+
required: ['subject', 'predicate']
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
847
1036
|
{
|
|
848
1037
|
name: 'ijfw_prompt_check',
|
|
849
1038
|
description: 'Call on the first turn when the user prompt is short (<30 tokens) or likely vague. Returns whether the prompt is under-specified and a sharpening suggestion. Deterministic regex detector -- no LLM call. Use for Codex/Cursor/Windsurf/Copilot/Gemini where pre-prompt hooks are not available.',
|
|
@@ -893,6 +1082,46 @@ const TOOLS = [
|
|
|
893
1082
|
},
|
|
894
1083
|
required: ['command'],
|
|
895
1084
|
},
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
// v1.5.0 T13: ijfw_state — single MCP face for the state-SDK verb facade.
|
|
1088
|
+
// Absorbs the retired ijfw_subagent_post_done tool (post-done IS a state
|
|
1089
|
+
// transition → reachable as the `subagent.post-done` verb). All 20 frozen
|
|
1090
|
+
// verbs from STATE-SDK-CONTRACT §7 are reachable through this one tool,
|
|
1091
|
+
// keeping the MCP cap at 12/12. The same `query(verb, payload, ctx)` core
|
|
1092
|
+
// is also exposed as a JS import and a CLI colon-namespace (`ijfw state:<verb>`).
|
|
1093
|
+
name: 'ijfw_state',
|
|
1094
|
+
description: 'State-SDK verb facade — invoke any of the 20 frozen verbs (workflow.*, wave.*, phase.*, subagent.*, event.emit, telemetry.record, roster.*, extension.set-active, decision.add, blocker.*, state.replay, state.validate) over the canonical physical state files. Single MCP face for the state-SDK; subagent.post-done is the verb that absorbed the retired ijfw_subagent_post_done tool. Returns the verb result with `ok` + `verbId` + verb-specific fields (see STATE-SDK-CONTRACT §7).',
|
|
1095
|
+
inputSchema: {
|
|
1096
|
+
type: 'object',
|
|
1097
|
+
properties: {
|
|
1098
|
+
verb: { type: 'string', description: 'Verb name from the frozen 20-verb registry (e.g. "workflow.get", "wave.advance", "subagent.post-done", "state.validate").' },
|
|
1099
|
+
payload: { type: 'object', description: 'Verb-specific payload (see STATE-SDK-CONTRACT §7 for each verb signature). Defaults to {} when omitted.' },
|
|
1100
|
+
projectRoot: { type: 'string', description: 'Project root for ctx (defaults to process.cwd()).' },
|
|
1101
|
+
subagentId: { type: 'string', description: 'Subagent id stamped on event/telemetry records (defaults to "parent").' },
|
|
1102
|
+
homeDir: { type: 'string', description: 'Home dir override for the homedir-scope active-extension file (defaults to process.env.HOME / USERPROFILE / os.homedir()).' },
|
|
1103
|
+
},
|
|
1104
|
+
required: ['verb'],
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
// v1.5.0-major W12-C N03: Trident-as-a-service. Multi-lens consensus
|
|
1109
|
+
// convergence (lock-in #47 — canonical Phase E). Dispatches all 3 lenses
|
|
1110
|
+
// (codex/gemini/claude by default) in parallel; if verdicts diverge,
|
|
1111
|
+
// re-runs with a CYCLE_SUMMARY of the disagreement until consensus or
|
|
1112
|
+
// maxIterations (default 3). Stall breaker halts on byte-identical
|
|
1113
|
+
// iterations. Fills the 12th tool-cap slot.
|
|
1114
|
+
name: 'ijfw_cross_audit_converge',
|
|
1115
|
+
description: 'Multi-lens Trident audit with consensus convergence loop. Dispatches codex/gemini/claude in parallel against a commit range, detects verdict divergence, and re-runs with a cycle summary until consensus or maxIterations. Returns {verdict, iterations, findings, divergence?, stalled?}. Verdict: PASS / CONDITIONAL / FAIL / consensus_failed / UNREACHABLE.',
|
|
1116
|
+
inputSchema: {
|
|
1117
|
+
type: 'object',
|
|
1118
|
+
properties: {
|
|
1119
|
+
commitRange: { type: 'string', description: 'Git commit range to audit (e.g. "HEAD~1..HEAD", "main..feature/x"). Required.' },
|
|
1120
|
+
maxIterations: { type: 'number', description: 'Max convergence iterations (default 3). 1 → single-shot (fallback mode).' },
|
|
1121
|
+
lenses: { type: 'array', items: { type: 'string' }, description: 'Lens ids to dispatch (default ["codex","gemini","claude"]).' },
|
|
1122
|
+
},
|
|
1123
|
+
required: ['commitRange'],
|
|
1124
|
+
},
|
|
896
1125
|
}
|
|
897
1126
|
];
|
|
898
1127
|
|
|
@@ -901,8 +1130,8 @@ const TOOLS = [
|
|
|
901
1130
|
function handleRecall({ context_hint, detail_level = 'standard', from_project }) {
|
|
902
1131
|
// Cross-project explicit pull. We bypass current-project sources and read
|
|
903
1132
|
// the target project's knowledge/handoff/journal directly. Search queries
|
|
904
|
-
// are routed through
|
|
905
|
-
// recall here is for "give me everything from X."
|
|
1133
|
+
// are routed through crossProjectSearch (BM25) via scope:'all' on the
|
|
1134
|
+
// search tool; recall here is for "give me everything from X."
|
|
906
1135
|
if (from_project) {
|
|
907
1136
|
const target = resolveProject(from_project);
|
|
908
1137
|
if (!target) {
|
|
@@ -949,6 +1178,95 @@ function handleRecall({ context_hint, detail_level = 'standard', from_project })
|
|
|
949
1178
|
return { text: getRecentJournalEntries(10) || 'No decisions recorded yet.' };
|
|
950
1179
|
}
|
|
951
1180
|
|
|
1181
|
+
// H5.5 / v1.5.0 H5.4 — structured facts feed.
|
|
1182
|
+
// context_hint === 'facts' -> only currently-valid facts (SQL
|
|
1183
|
+
// getValidAt(now)); fallback to raw
|
|
1184
|
+
// facts.jsonl if the SQL table is
|
|
1185
|
+
// empty/unavailable (back-compat for
|
|
1186
|
+
// pre-H5.4 installations).
|
|
1187
|
+
// context_hint === 'facts:history' -> full timeline including invalidated
|
|
1188
|
+
// rows (with their valid_from/valid_to
|
|
1189
|
+
// windows). If the hint is followed by
|
|
1190
|
+
// ":subject/predicate" we narrow to
|
|
1191
|
+
// that pair; otherwise return every
|
|
1192
|
+
// row.
|
|
1193
|
+
if (context_hint === 'facts' || (typeof context_hint === 'string' && context_hint.startsWith('facts:history'))) {
|
|
1194
|
+
const isHistory = typeof context_hint === 'string' && context_hint.startsWith('facts:history');
|
|
1195
|
+
try {
|
|
1196
|
+
const db = getFactsDb();
|
|
1197
|
+
if (db) {
|
|
1198
|
+
if (isHistory) {
|
|
1199
|
+
// Optional ":subject/predicate" narrow-down. Spec: "if subject+
|
|
1200
|
+
// predicate keys can be inferred from the recall query; else return
|
|
1201
|
+
// all facts with their validity windows".
|
|
1202
|
+
const tail = context_hint.slice('facts:history'.length).replace(/^:/, '').trim();
|
|
1203
|
+
let rows;
|
|
1204
|
+
if (tail) {
|
|
1205
|
+
const [subj, pred] = tail.split('/').map(s => s && s.trim()).filter(Boolean);
|
|
1206
|
+
if (subj && pred) {
|
|
1207
|
+
rows = temporalGetHistory(db, subj, pred);
|
|
1208
|
+
} else {
|
|
1209
|
+
rows = temporalGetAllFactsWithWindows(db);
|
|
1210
|
+
}
|
|
1211
|
+
} else {
|
|
1212
|
+
rows = temporalGetAllFactsWithWindows(db);
|
|
1213
|
+
}
|
|
1214
|
+
if (!rows || rows.length === 0) {
|
|
1215
|
+
return { text: 'No structured facts extracted yet. Store memories with key:value lines, "X uses Y", or "decided to ..." phrases to populate.' };
|
|
1216
|
+
}
|
|
1217
|
+
const lines = rows.map(r => JSON.stringify({
|
|
1218
|
+
subject: r.subject,
|
|
1219
|
+
predicate: r.predicate,
|
|
1220
|
+
object: r.object,
|
|
1221
|
+
confidence: r.confidence,
|
|
1222
|
+
valid_from: r.valid_from,
|
|
1223
|
+
valid_to: r.valid_to,
|
|
1224
|
+
memory_id: r.memory_id,
|
|
1225
|
+
source: r.source,
|
|
1226
|
+
}));
|
|
1227
|
+
if (detail_level === 'summary') {
|
|
1228
|
+
const tailLines = lines.slice(-5).join('\n');
|
|
1229
|
+
return { text: `## Fact history (${lines.length} rows)\n${tailLines}` };
|
|
1230
|
+
}
|
|
1231
|
+
return { text: `## Fact history (${lines.length} rows)\n${lines.join('\n')}` };
|
|
1232
|
+
}
|
|
1233
|
+
// Default: currently-valid facts only.
|
|
1234
|
+
const rows = temporalGetValidAt(db, new Date().toISOString());
|
|
1235
|
+
if (rows && rows.length > 0) {
|
|
1236
|
+
const lines = rows.map(r => JSON.stringify({
|
|
1237
|
+
subject: r.subject,
|
|
1238
|
+
predicate: r.predicate,
|
|
1239
|
+
object: r.object,
|
|
1240
|
+
confidence: r.confidence,
|
|
1241
|
+
valid_from: r.valid_from,
|
|
1242
|
+
memory_id: r.memory_id,
|
|
1243
|
+
source: r.source,
|
|
1244
|
+
}));
|
|
1245
|
+
if (detail_level === 'summary') {
|
|
1246
|
+
const tail = lines.slice(-5).join('\n');
|
|
1247
|
+
return { text: `## Currently-valid facts (${lines.length})\n${tail}` };
|
|
1248
|
+
}
|
|
1249
|
+
return { text: `## Currently-valid facts (${lines.length})\n${lines.join('\n')}` };
|
|
1250
|
+
}
|
|
1251
|
+
// SQL table empty -- fall through to JSONL back-compat path below.
|
|
1252
|
+
}
|
|
1253
|
+
if (!existsSync(FACTS_FILE)) {
|
|
1254
|
+
return { text: 'No structured facts extracted yet. Store memories with key:value lines, "X uses Y", or "decided to ..." phrases to populate.' };
|
|
1255
|
+
}
|
|
1256
|
+
const raw = readFileSync(FACTS_FILE, 'utf8');
|
|
1257
|
+
// detail_level === 'summary' → just count + a sample tail. Keeps token
|
|
1258
|
+
// usage bounded for session-start hydration.
|
|
1259
|
+
if (detail_level === 'summary') {
|
|
1260
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
1261
|
+
const tail = lines.slice(-5).join('\n');
|
|
1262
|
+
return { text: `## Structured facts (${lines.length} total)\n${tail}` };
|
|
1263
|
+
}
|
|
1264
|
+
return { text: raw || 'No structured facts extracted yet.' };
|
|
1265
|
+
} catch (err) {
|
|
1266
|
+
return { text: `Facts feed unreadable: ${err.code || err.message}`, isError: true };
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
952
1270
|
const results = searchMemory(context_hint);
|
|
953
1271
|
if (results.length === 0) return { text: `No memories matching: ${context_hint}` };
|
|
954
1272
|
return { text: results.map(r => `[${r.source}] ${r.content}`).join('\n') };
|
|
@@ -997,12 +1315,53 @@ function handleStore({ content, type, tags = [], summary, why, how_to_apply }) {
|
|
|
997
1315
|
const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
|
|
998
1316
|
const journalEntry = `**${type}**${tagStr}: ${safeSummary || safeContent.substring(0, 200)}`;
|
|
999
1317
|
|
|
1318
|
+
// H5.6 — Semantic dedup BEFORE append. If this memory is a near-duplicate
|
|
1319
|
+
// of one already in the last N journal entries, short-circuit and return
|
|
1320
|
+
// the existing entry's id. handoff is exempt (always overwrites a single
|
|
1321
|
+
// file by design; deduping would silently drop a handoff swap).
|
|
1322
|
+
const dedupCfg = readDedupConfig();
|
|
1323
|
+
if (dedupCfg.enabled && type !== 'handoff') {
|
|
1324
|
+
const recents = getRecentMemoriesForDedup(dedupCfg.windowSize);
|
|
1325
|
+
// Dedup against the FULL journal-entry line shape -- that's what the next
|
|
1326
|
+
// call would see in `recents`, so comparing apples to apples.
|
|
1327
|
+
const dup = findNearDuplicate(journalEntry, recents);
|
|
1328
|
+
if (dup) {
|
|
1329
|
+
// Spec: emit a stderr line so the user/agent sees the elision.
|
|
1330
|
+
try {
|
|
1331
|
+
process.stderr.write(`[ijfw memory] dedup'd similar entry; keeping prior ${dup.match.id}\n`);
|
|
1332
|
+
} catch { /* stderr may be detached in test harness */ }
|
|
1333
|
+
return {
|
|
1334
|
+
text: `Dedup'd: similar memory already exists (${dup.match.id}, similarity=${dup.similarity.toFixed(2)}). Not appended.`,
|
|
1335
|
+
deduped: true,
|
|
1336
|
+
existing_id: dup.match.id,
|
|
1337
|
+
similarity: dup.similarity,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1000
1342
|
// 1. Always append to journal (one-line timeline). Hard failure → report.
|
|
1001
1343
|
const journalResult = appendToJournal(journalEntry);
|
|
1002
1344
|
if (!journalResult.ok) {
|
|
1003
1345
|
return { text: `Memory journal is not writable (${journalResult.code}) -- check .ijfw/ directory permissions and retry.`, isError: true };
|
|
1004
1346
|
}
|
|
1005
1347
|
|
|
1348
|
+
// H5.5 — Fact extraction AFTER successful append. Best-effort: a failure
|
|
1349
|
+
// here is logged in the return text but does NOT poison the store result.
|
|
1350
|
+
// Memory-id ties facts.jsonl rows back to their journal entry.
|
|
1351
|
+
const factMeta = {
|
|
1352
|
+
ts: new Date().toISOString(),
|
|
1353
|
+
memory_id: factMemoryIdFor(journalEntry),
|
|
1354
|
+
source: `memory_store:${type}`,
|
|
1355
|
+
};
|
|
1356
|
+
const facts = extractFacts(safeContent);
|
|
1357
|
+
appendFactsToSidecar(facts, factMeta);
|
|
1358
|
+
// v1.5.0 audit H5.4 — mirror to bi-temporal SQL store. For each fact,
|
|
1359
|
+
// closes any prior currently-valid fact with the same (subject, predicate)
|
|
1360
|
+
// but different object before inserting. Same-object stores are a no-op.
|
|
1361
|
+
// Wrapped in a per-fact transaction inside temporal.js. Best-effort: any
|
|
1362
|
+
// failure is logged to stderr but never breaks the journal-or-JSONL path.
|
|
1363
|
+
writeFactsBitemporal(facts, factMeta);
|
|
1364
|
+
|
|
1006
1365
|
// 2. Type-specific secondary writes. Each tracked so we report partial
|
|
1007
1366
|
// success accurately rather than lying about "stored."
|
|
1008
1367
|
const failures = [];
|
|
@@ -1107,22 +1466,60 @@ async function handlePrelude({ detail_level = 'summary' } = {}) {
|
|
|
1107
1466
|
const updateNudge = composeUpdateNudge();
|
|
1108
1467
|
if (updateNudge) parts.push(updateNudge, '');
|
|
1109
1468
|
|
|
1469
|
+
// v1.5.0 memory-moat M3 (INT.4): surface top-K recently-successful skills
|
|
1470
|
+
// at session start. The Wayland-pattern "more-you-use-it-better-it-gets"
|
|
1471
|
+
// feedback loop: every skill execution writes to skill_telemetry via the
|
|
1472
|
+
// state-SDK telemetry.record verb (INT.3); this block reads top-5 by
|
|
1473
|
+
// success-count and surfaces them as a hint to the model. Best-effort:
|
|
1474
|
+
// an unmigrated db, empty telemetry, or read failure all skip the block
|
|
1475
|
+
// silently — never breaks the prelude.
|
|
1476
|
+
try {
|
|
1477
|
+
const { topKSuccessfulSkills } = await import('./orchestrator/skill-telemetry.js');
|
|
1478
|
+
const Database = (await import('better-sqlite3')).default;
|
|
1479
|
+
const { join: joinP } = await import('node:path');
|
|
1480
|
+
const root = process.env.IJFW_PROJECT_DIR || process.cwd();
|
|
1481
|
+
const dbPath = joinP(root, '.ijfw', 'index', 'memory.db');
|
|
1482
|
+
if (existsSync(dbPath)) {
|
|
1483
|
+
const db = new Database(dbPath, { readonly: true });
|
|
1484
|
+
try {
|
|
1485
|
+
const top = topKSuccessfulSkills(db, { k: 5 });
|
|
1486
|
+
if (top.length > 0) {
|
|
1487
|
+
const names = top.map((r) => `${r.skill_id} (${r.success_count}×)`).join(', ');
|
|
1488
|
+
parts.push(
|
|
1489
|
+
'<ijfw-recommended-skills>',
|
|
1490
|
+
`Observed success this project: ${names}`,
|
|
1491
|
+
'</ijfw-recommended-skills>',
|
|
1492
|
+
'',
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
} finally {
|
|
1496
|
+
try { db.close(); } catch { /* best-effort */ }
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
} catch { /* best-effort; never block the prelude */ }
|
|
1500
|
+
|
|
1110
1501
|
// 1.2.0 Phase 5: surface the DESIGN picker to platforms without a skills tree.
|
|
1111
1502
|
// Skip when the project already has a DESIGN.md (contract exists; no picker).
|
|
1503
|
+
// Built into a standalone block so the abstention path below can re-emit it:
|
|
1504
|
+
// a fresh project with no memory AND no DESIGN.md is exactly when the picker
|
|
1505
|
+
// matters most, so it must survive the thin-memory short-circuit.
|
|
1506
|
+
let designPickerBlock = '';
|
|
1112
1507
|
try {
|
|
1113
1508
|
if (!existsSync(join(PROJECT_DIR, 'DESIGN.md'))) {
|
|
1114
1509
|
const names = DESIGN_TEMPLATE_CATALOG.map(([n]) => n);
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1510
|
+
designPickerBlock = [
|
|
1511
|
+
'## Design picker',
|
|
1512
|
+
'No DESIGN.md in project. 12 curated templates available:',
|
|
1513
|
+
names.slice(0, 5).join(', ') + ',',
|
|
1514
|
+
names.slice(5, 10).join(', ') + ',',
|
|
1515
|
+
names.slice(10).join(', ') + '.',
|
|
1516
|
+
'',
|
|
1517
|
+
'Pick one: ijfw_memory_recall({context_hint: "design_template:<name>"}).',
|
|
1518
|
+
'Full catalog with descriptions: ijfw_memory_recall({context_hint: "design_template"}).',
|
|
1519
|
+
].join('\n');
|
|
1124
1520
|
}
|
|
1125
|
-
} catch { /*
|
|
1521
|
+
} catch { /* project dir unreadable -- skip picker block */ }
|
|
1522
|
+
if (designPickerBlock) parts.push(designPickerBlock, '');
|
|
1126
1523
|
|
|
1127
1524
|
// Team knowledge first -- shared decisions/patterns/stack rank above personal.
|
|
1128
1525
|
const team = readTeamKnowledge();
|
|
@@ -1221,16 +1618,51 @@ async function handlePrelude({ detail_level = 'summary' } = {}) {
|
|
|
1221
1618
|
// Best-effort; never fail the prelude on memory-feedback issues.
|
|
1222
1619
|
}
|
|
1223
1620
|
|
|
1621
|
+
// v1.5.0 audit-MED-update-M8 (F-REL-2): surface the last-N partial-deploy
|
|
1622
|
+
// alerts so a half-deployed extension is visible at next session-start.
|
|
1623
|
+
// Wrapped in try/catch — alert read failure must NEVER fail the prelude.
|
|
1624
|
+
try {
|
|
1625
|
+
const { renderDeployAlertsForPrelude } = await import('./deploy-alerts.js');
|
|
1626
|
+
const block = await renderDeployAlertsForPrelude({ limit: 10 });
|
|
1627
|
+
if (block && typeof block === 'string' && block.length > 0) {
|
|
1628
|
+
parts.push(block);
|
|
1629
|
+
}
|
|
1630
|
+
} catch {
|
|
1631
|
+
// Best-effort; never fail the prelude on deploy-alert read issues.
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1224
1634
|
parts.push('</ijfw-memory>');
|
|
1225
1635
|
|
|
1226
1636
|
const text = parts.join('\n');
|
|
1227
1637
|
if (text.length < 60) {
|
|
1228
1638
|
return { text: 'Fresh project -- no memory stored yet. Proceed normally.' };
|
|
1229
1639
|
}
|
|
1640
|
+
|
|
1641
|
+
// v1.5.0 audit MED #11 (memory-engine.md F-FUN-7): abstention.
|
|
1642
|
+
// If memory exists but the body content is thin AND there are no recent
|
|
1643
|
+
// journal entries, surface an honest abstention so the LLM doesn't try
|
|
1644
|
+
// to over-fit a half-page of stale frontmatter to the user's prompt.
|
|
1645
|
+
// Threshold: total content chars below MIN_CONTENT_CHARS for the
|
|
1646
|
+
// knowledge/team/handoff sources combined AND zero recent journal lines.
|
|
1647
|
+
const MIN_CONTENT_CHARS = 200;
|
|
1648
|
+
const knowledgeChars = (knowledge ? knowledge.length : 0) + (team ? team.length : 0) + (handoff ? handoff.length : 0);
|
|
1649
|
+
const recentLines = recent ? recent.split('\n').filter(l => l.trim()).length : 0;
|
|
1650
|
+
if (knowledgeChars < MIN_CONTENT_CHARS && recentLines === 0) {
|
|
1651
|
+
const abstain = [
|
|
1652
|
+
'<ijfw-memory>',
|
|
1653
|
+
'Memory present but nothing relevant to your prompt -- proceed and I\'ll store any decisions you make.',
|
|
1654
|
+
];
|
|
1655
|
+
// The DESIGN picker is independent of memory richness — preserve it
|
|
1656
|
+
// through the abstention path, otherwise a fresh project never sees it.
|
|
1657
|
+
if (designPickerBlock) abstain.push('', designPickerBlock);
|
|
1658
|
+
abstain.push('</ijfw-memory>');
|
|
1659
|
+
return { text: abstain.join('\n') };
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1230
1662
|
return { text };
|
|
1231
1663
|
}
|
|
1232
1664
|
|
|
1233
|
-
function handleSearch({ query, limit = 10, scope = 'project', label }) {
|
|
1665
|
+
async function handleSearch({ query, limit = 10, scope = 'project', label }) {
|
|
1234
1666
|
if (scope === 'sandbox') {
|
|
1235
1667
|
if (label) {
|
|
1236
1668
|
const content = readFromSandbox(label);
|
|
@@ -1258,7 +1690,49 @@ function handleSearch({ query, limit = 10, scope = 'project', label }) {
|
|
|
1258
1690
|
}
|
|
1259
1691
|
if (query.length > 500) query = query.substring(0, 500);
|
|
1260
1692
|
if (scope !== 'project' && scope !== 'all') scope = 'project';
|
|
1261
|
-
|
|
1693
|
+
|
|
1694
|
+
// v1.5.0 memory-moat M1 (INT.5): "dv:" prefix routes to the declarative
|
|
1695
|
+
// Dataview-grade query mode. Returns structured rows from the FTS5 +
|
|
1696
|
+
// memory_links/_tags/_meta join populated at write time by M1.3 /
|
|
1697
|
+
// INT.1. Best-effort: errors fall through to the standard NL/FTS5 path
|
|
1698
|
+
// so the new mode never breaks existing callers.
|
|
1699
|
+
if (query.startsWith('dv:')) {
|
|
1700
|
+
try {
|
|
1701
|
+
const body = query.slice(3).trim();
|
|
1702
|
+
const dvMod = await import('./memory/query-dataview.js');
|
|
1703
|
+
const fts5Mod = await import('./memory/fts5.js');
|
|
1704
|
+
const root = process.env.IJFW_PROJECT_DIR || process.cwd();
|
|
1705
|
+
const db = await fts5Mod.openDb(root);
|
|
1706
|
+
try {
|
|
1707
|
+
const parsed = dvMod.parseDataviewQuery(body);
|
|
1708
|
+
const result = dvMod.runDataviewQuery(db, parsed);
|
|
1709
|
+
const rows = result.rows.slice(0, limit);
|
|
1710
|
+
if (rows.length === 0) {
|
|
1711
|
+
return { text: `No results for dataview query: "${body}"`, mode: 'dataview', parsed };
|
|
1712
|
+
}
|
|
1713
|
+
// Render in the existing text shape so MCP clients that expect
|
|
1714
|
+
// a single string field keep working.
|
|
1715
|
+
return {
|
|
1716
|
+
text: rows
|
|
1717
|
+
.map((r) => `[id:${r.id} source:${r.source || '?'} created:${r.created_at}] ${(r.body || '').slice(0, 200)}`)
|
|
1718
|
+
.join('\n'),
|
|
1719
|
+
mode: 'dataview',
|
|
1720
|
+
parsed,
|
|
1721
|
+
rowCount: rows.length,
|
|
1722
|
+
};
|
|
1723
|
+
} finally {
|
|
1724
|
+
try { fts5Mod.closeDb(db); } catch { /* best-effort */ }
|
|
1725
|
+
}
|
|
1726
|
+
} catch (e) {
|
|
1727
|
+
return {
|
|
1728
|
+
text: `Dataview query failed: ${e && e.message ? e.message : String(e)}`,
|
|
1729
|
+
mode: 'dataview',
|
|
1730
|
+
isError: true,
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const results = await searchMemory(query, limit, scope);
|
|
1262
1736
|
if (results.length === 0) {
|
|
1263
1737
|
const where = scope === 'all' ? ' across all projects' : '';
|
|
1264
1738
|
return { text: `No results for: "${query}"${where}` };
|
|
@@ -1471,11 +1945,68 @@ function handleMessage(msg) {
|
|
|
1471
1945
|
result = { text: JSON.stringify(r, null, 2), isError: !!(r && r.error) };
|
|
1472
1946
|
break;
|
|
1473
1947
|
}
|
|
1948
|
+
case 'ijfw_state': {
|
|
1949
|
+
// v1.5.0 T13: single MCP face for the state-SDK. Routes every call
|
|
1950
|
+
// into the same `query(verb, payload, ctx)` core that the JS module
|
|
1951
|
+
// (`./orchestrator/state-sdk.js`) and the CLI colon-namespace
|
|
1952
|
+
// (`ijfw state:<verb>`) use. The retired `ijfw_subagent_post_done`
|
|
1953
|
+
// tool is now reachable as the `subagent.post-done` verb.
|
|
1954
|
+
const a = args || {};
|
|
1955
|
+
if (typeof a.verb !== 'string' || a.verb.length === 0) {
|
|
1956
|
+
result = { text: JSON.stringify({ ok: false, error: 'verb (string) is required' }), isError: true };
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
try {
|
|
1960
|
+
const { query } = await import('./orchestrator/state-sdk.js');
|
|
1961
|
+
const payload = (a.payload && typeof a.payload === 'object') ? a.payload : {};
|
|
1962
|
+
const ctx = {
|
|
1963
|
+
projectRoot: typeof a.projectRoot === 'string' && a.projectRoot.length > 0
|
|
1964
|
+
? a.projectRoot
|
|
1965
|
+
: process.cwd(),
|
|
1966
|
+
};
|
|
1967
|
+
if (typeof a.subagentId === 'string' && a.subagentId.length > 0) ctx.subagentId = a.subagentId;
|
|
1968
|
+
if (typeof a.homeDir === 'string' && a.homeDir.length > 0) ctx.homeDir = a.homeDir;
|
|
1969
|
+
const r = await query(a.verb, payload, ctx);
|
|
1970
|
+
// A verdict-fail refusal (Model 4) is the verb's correct hard-block —
|
|
1971
|
+
// surface `isError: true` so the orchestrator-LLM treats it as a
|
|
1972
|
+
// hard stop rather than an advisory note (mirrors the prior
|
|
1973
|
+
// ijfw_subagent_post_done `block: true` contract).
|
|
1974
|
+
const refused = r && r.refused === true;
|
|
1975
|
+
result = { text: JSON.stringify(r, null, 2), isError: !!refused };
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
const msg = err && err.message ? err.message : String(err);
|
|
1978
|
+
result = { text: JSON.stringify({ ok: false, error: msg }), isError: true };
|
|
1979
|
+
}
|
|
1980
|
+
break;
|
|
1981
|
+
}
|
|
1474
1982
|
case 'ijfw_update_apply': {
|
|
1475
1983
|
const r = ijfwUpdateApply(args || {});
|
|
1476
1984
|
result = { text: JSON.stringify(r, null, 2), isError: r && r.status === 'error' };
|
|
1477
1985
|
break;
|
|
1478
1986
|
}
|
|
1987
|
+
case 'ijfw_cross_audit_converge': {
|
|
1988
|
+
// v1.5.0-major W12-C N03: Trident-as-a-service.
|
|
1989
|
+
const a = args || {};
|
|
1990
|
+
if (!a.commitRange || typeof a.commitRange !== 'string') {
|
|
1991
|
+
result = { text: JSON.stringify({ error: 'commitRange (string) is required' }), isError: true };
|
|
1992
|
+
break;
|
|
1993
|
+
}
|
|
1994
|
+
const { runPhaseEConverge, defaultConvergeDispatch } = await import('./cross-orchestrator.js');
|
|
1995
|
+
try {
|
|
1996
|
+
const r = await runPhaseEConverge({
|
|
1997
|
+
commitRange: a.commitRange,
|
|
1998
|
+
maxIterations: typeof a.maxIterations === 'number' ? a.maxIterations : 3,
|
|
1999
|
+
lenses: Array.isArray(a.lenses) && a.lenses.length > 0 ? a.lenses : undefined,
|
|
2000
|
+
dispatch: defaultConvergeDispatch,
|
|
2001
|
+
projectRoot: process.cwd(),
|
|
2002
|
+
});
|
|
2003
|
+
const isErr = r.verdict === 'consensus_failed' || r.verdict === 'FAIL' || r.verdict === 'UNREACHABLE';
|
|
2004
|
+
result = { text: JSON.stringify(r, null, 2), isError: isErr };
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
result = { text: JSON.stringify({ error: err && err.message ? err.message : String(err) }), isError: true };
|
|
2007
|
+
}
|
|
2008
|
+
break;
|
|
2009
|
+
}
|
|
1479
2010
|
case 'ijfw_memory_recall':
|
|
1480
2011
|
result = handleRecall(args || {});
|
|
1481
2012
|
emitRecallObservation(args || {});
|
|
@@ -1504,7 +2035,7 @@ function handleMessage(msg) {
|
|
|
1504
2035
|
break;
|
|
1505
2036
|
}
|
|
1506
2037
|
}
|
|
1507
|
-
result = handleSearch(searchArgs);
|
|
2038
|
+
result = await handleSearch(searchArgs);
|
|
1508
2039
|
break;
|
|
1509
2040
|
}
|
|
1510
2041
|
case 'ijfw_memory_status':
|
|
@@ -1513,6 +2044,12 @@ function handleMessage(msg) {
|
|
|
1513
2044
|
case 'ijfw_memory_prelude':
|
|
1514
2045
|
result = await handlePrelude(args || {});
|
|
1515
2046
|
break;
|
|
2047
|
+
case 'ijfw_memory_facts': {
|
|
2048
|
+
// v1.5.0 M5 (INT.6) -- surface bi-temporal facts read path.
|
|
2049
|
+
const mod = await import('./memory-facts-handler.js');
|
|
2050
|
+
result = await mod.handleMemoryFacts(args || {});
|
|
2051
|
+
break;
|
|
2052
|
+
}
|
|
1516
2053
|
case 'ijfw_metrics':
|
|
1517
2054
|
result = handleMetrics(args || {});
|
|
1518
2055
|
break;
|
|
@@ -1665,4 +2202,11 @@ process.on('unhandledRejection', (err) => {
|
|
|
1665
2202
|
// Export for tests (Node ESM allows this -- only consumed when imported, not on stdio run)
|
|
1666
2203
|
// gatePermissionAndQuota is exported inline at its declaration above (B16/SEC-M-03)
|
|
1667
2204
|
// so test-server-quota-integration.js can drive it without spinning a server.
|
|
1668
|
-
|
|
2205
|
+
// H5.5 / H5.6 — expose handleStore/handleRecall + path/helpers so the ingest
|
|
2206
|
+
// integration test can drive the full pipeline without spawning a subprocess.
|
|
2207
|
+
export {
|
|
2208
|
+
sanitizeContent, atomicWrite, readMarkdownFile, PROJECT_HASH,
|
|
2209
|
+
handleStore, handleRecall, handleSearch, handlePrelude,
|
|
2210
|
+
MEMORY_DIR, FACTS_FILE, FACTS_DB_FILE,
|
|
2211
|
+
getFactsDb,
|
|
2212
|
+
};
|