thumbgate 0.9.9
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/.claude-plugin/README.md +134 -0
- package/.claude-plugin/bundle/icon.png +0 -0
- package/.claude-plugin/bundle/icon.svg +18 -0
- package/.claude-plugin/bundle/server/index.js +24 -0
- package/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.well-known/mcp/server-card.json +231 -0
- package/LICENSE +21 -0
- package/README.md +375 -0
- package/adapters/README.md +9 -0
- package/adapters/amp/skills/rlhf-feedback/SKILL.md +22 -0
- package/adapters/chatgpt/INSTALL.md +83 -0
- package/adapters/chatgpt/openapi.yaml +1281 -0
- package/adapters/claude/.mcp.json +14 -0
- package/adapters/codex/config.toml +9 -0
- package/adapters/gemini/function-declarations.json +224 -0
- package/adapters/mcp/server-stdio.js +788 -0
- package/adapters/opencode/opencode.json +15 -0
- package/bin/cli.js +1483 -0
- package/bin/memory.sh +64 -0
- package/bin/obsidian-sync.sh +20 -0
- package/bin/postinstall.js +37 -0
- package/config/build-metadata.json +4 -0
- package/config/e2e-critical-flows.json +45 -0
- package/config/gate-templates.json +77 -0
- package/config/gates/claim-verification.json +29 -0
- package/config/gates/computer-use.json +39 -0
- package/config/gates/default.json +117 -0
- package/config/github-about.json +25 -0
- package/config/mcp-allowlists.json +135 -0
- package/config/model-tiers.json +33 -0
- package/config/partner-routing.json +132 -0
- package/config/policy-bundles/constrained-v1.json +64 -0
- package/config/policy-bundles/default-v1.json +91 -0
- package/config/rubrics/default-v1.json +52 -0
- package/config/skill-packs/react-testing.json +23 -0
- package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
- package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
- package/config/skill-specs/pr-reviewer.json +9 -0
- package/config/skill-specs/release-status.json +9 -0
- package/config/skill-specs/ticket-triage.json +9 -0
- package/config/subagent-profiles.json +32 -0
- package/config/tessl-tiles.json +29 -0
- package/config/thumbgate-settings.managed.json +12 -0
- package/openapi/openapi.yaml +1281 -0
- package/package.json +286 -0
- package/plugins/amp-skill/INSTALL.md +52 -0
- package/plugins/amp-skill/SKILL.md +64 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
- package/plugins/claude-codex-bridge/.mcp.json +12 -0
- package/plugins/claude-codex-bridge/INSTALL.md +43 -0
- package/plugins/claude-codex-bridge/README.md +46 -0
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
- package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
- package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
- package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
- package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
- package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
- package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
- package/plugins/claude-skill/INSTALL.md +55 -0
- package/plugins/claude-skill/SKILL.md +46 -0
- package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
- package/plugins/codex-profile/.mcp.json +12 -0
- package/plugins/codex-profile/AGENTS.md +20 -0
- package/plugins/codex-profile/INSTALL.md +66 -0
- package/plugins/codex-profile/README.md +37 -0
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
- package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
- package/plugins/cursor-marketplace/LICENSE +21 -0
- package/plugins/cursor-marketplace/README.md +124 -0
- package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
- package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
- package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
- package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
- package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
- package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
- package/plugins/cursor-marketplace/mcp.json +12 -0
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
- package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
- package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
- package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
- package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
- package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
- package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
- package/plugins/gemini-extension/INSTALL.md +92 -0
- package/plugins/gemini-extension/gemini_prompt.txt +14 -0
- package/plugins/gemini-extension/tool_contract.json +45 -0
- package/plugins/opencode-profile/INSTALL.md +57 -0
- package/public/assets/instagram-card.png +0 -0
- package/public/assets/tiktok-agent-memory.mp4 +0 -0
- package/public/blog.html +400 -0
- package/public/dashboard.html +1093 -0
- package/public/guide.html +317 -0
- package/public/index.html +1195 -0
- package/public/learn/agent-harness-pattern.html +180 -0
- package/public/learn/ai-agent-persistent-memory.html +202 -0
- package/public/learn/learn.css +45 -0
- package/public/learn/mcp-pre-action-gates-explained.html +172 -0
- package/public/learn/stop-ai-agent-force-push.html +134 -0
- package/public/learn/vibe-coding-safety-net.html +142 -0
- package/public/learn.html +213 -0
- package/public/lessons.html +650 -0
- package/public/vercel.json +8 -0
- package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
- package/scripts/a2ui-engine.js +73 -0
- package/scripts/access-anomaly-detector.js +12 -0
- package/scripts/adk-consolidator.js +266 -0
- package/scripts/agent-readiness.js +220 -0
- package/scripts/agent-security-hardening.js +227 -0
- package/scripts/agentic-data-pipeline.js +847 -0
- package/scripts/analytics-report.js +328 -0
- package/scripts/analytics-window.js +158 -0
- package/scripts/async-job-runner.js +1001 -0
- package/scripts/audit-trail.js +398 -0
- package/scripts/auto-promote-gates.js +293 -0
- package/scripts/auto-wire-hooks.js +316 -0
- package/scripts/autonomous-sales-agent.js +39 -0
- package/scripts/autoresearch-runner.js +216 -0
- package/scripts/background-agent-governance.js +237 -0
- package/scripts/behavioral-extraction.js +93 -0
- package/scripts/belief-update.js +84 -0
- package/scripts/billing.js +2438 -0
- package/scripts/bot-detector.js +50 -0
- package/scripts/budget-guard.js +173 -0
- package/scripts/build-claude-mcpb.js +189 -0
- package/scripts/build-metadata.js +97 -0
- package/scripts/check-congruence.js +322 -0
- package/scripts/cli-feedback.js +135 -0
- package/scripts/cli-telemetry.js +87 -0
- package/scripts/cloudflare-dynamic-sandbox.js +315 -0
- package/scripts/code-reasoning.js +350 -0
- package/scripts/codegraph-context.js +466 -0
- package/scripts/commercial-offer.js +56 -0
- package/scripts/computer-use-firewall.js +250 -0
- package/scripts/context-engine.js +694 -0
- package/scripts/contextfs.js +1287 -0
- package/scripts/conversation-context.js +119 -0
- package/scripts/creator-campaigns.js +239 -0
- package/scripts/daemon-manager.js +108 -0
- package/scripts/daily-digest.js +11 -0
- package/scripts/dashboard-render-spec.js +395 -0
- package/scripts/dashboard.js +1058 -0
- package/scripts/data-governance.js +173 -0
- package/scripts/delegation-runtime.js +900 -0
- package/scripts/deploy-gcp.sh +44 -0
- package/scripts/deploy-policy.js +231 -0
- package/scripts/disagreement-mining.js +315 -0
- package/scripts/dispatch-brief.js +159 -0
- package/scripts/distribution-surfaces.js +44 -0
- package/scripts/dpo-optimizer.js +206 -0
- package/scripts/ensure-repo-bootstrap.js +129 -0
- package/scripts/ephemeral-agent-store.js +219 -0
- package/scripts/eval-harness.js +56 -0
- package/scripts/evolution-state.js +241 -0
- package/scripts/experiment-tracker.js +267 -0
- package/scripts/export-databricks-bundle.js +242 -0
- package/scripts/export-dpo-pairs.js +344 -0
- package/scripts/export-kto-pairs.js +309 -0
- package/scripts/export-training.js +450 -0
- package/scripts/failure-diagnostics.js +558 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-fallback.js +110 -0
- package/scripts/feedback-history-distiller.js +391 -0
- package/scripts/feedback-inbox-read.js +162 -0
- package/scripts/feedback-loop.js +1887 -0
- package/scripts/feedback-paths.js +145 -0
- package/scripts/feedback-quality.js +139 -0
- package/scripts/feedback-root-consolidator.js +238 -0
- package/scripts/feedback-schema.js +426 -0
- package/scripts/feedback-session.js +286 -0
- package/scripts/feedback-to-memory.js +185 -0
- package/scripts/feedback-to-rules.js +164 -0
- package/scripts/filesystem-search.js +405 -0
- package/scripts/funnel-analytics.js +35 -0
- package/scripts/gate-satisfy.js +42 -0
- package/scripts/gate-stats.js +116 -0
- package/scripts/gate-templates.js +70 -0
- package/scripts/gates-engine.js +816 -0
- package/scripts/generate-paperbanana-diagrams.sh +99 -0
- package/scripts/generate-pretool-hook.sh +40 -0
- package/scripts/github-about.js +350 -0
- package/scripts/github-outreach.js +65 -0
- package/scripts/gtm-revenue-loop.js +520 -0
- package/scripts/hallucination-detector.js +226 -0
- package/scripts/hf-papers.js +317 -0
- package/scripts/history-distiller.js +200 -0
- package/scripts/hook-auto-capture.sh +100 -0
- package/scripts/hook-stop-pr-thread-check.sh +68 -0
- package/scripts/hook-stop-self-score.sh +51 -0
- package/scripts/hook-stop-verify-deploy.sh +31 -0
- package/scripts/hook-thumbgate-cache-updater.js +48 -0
- package/scripts/hook-verify-before-done.sh +20 -0
- package/scripts/hosted-config.js +156 -0
- package/scripts/hybrid-feedback-context.js +675 -0
- package/scripts/install-mcp.js +159 -0
- package/scripts/intent-router.js +392 -0
- package/scripts/internal-agent-bootstrap.js +490 -0
- package/scripts/jsonl-watcher.js +155 -0
- package/scripts/lesson-db.js +613 -0
- package/scripts/lesson-inference.js +310 -0
- package/scripts/lesson-retrieval.js +95 -0
- package/scripts/lesson-rotation.js +137 -0
- package/scripts/lesson-search.js +644 -0
- package/scripts/lesson-synthesis.js +196 -0
- package/scripts/license.js +50 -0
- package/scripts/local-model-profile.js +384 -0
- package/scripts/markdown-escape.js +12 -0
- package/scripts/marketing-experiment.js +671 -0
- package/scripts/mcp-config.js +149 -0
- package/scripts/mcp-policy.js +99 -0
- package/scripts/memalign-recall.js +111 -0
- package/scripts/memory-firewall.js +222 -0
- package/scripts/memory-migration.js +296 -0
- package/scripts/meta-policy.js +190 -0
- package/scripts/metered-billing.js +16 -0
- package/scripts/model-tier-router.js +301 -0
- package/scripts/money-watcher.js +71 -0
- package/scripts/multi-hop-recall.js +240 -0
- package/scripts/natural-language-harness.js +330 -0
- package/scripts/obsidian-export.js +713 -0
- package/scripts/operational-dashboard.js +103 -0
- package/scripts/operational-summary.js +93 -0
- package/scripts/optimize-context.js +17 -0
- package/scripts/org-dashboard.js +201 -0
- package/scripts/partner-orchestration.js +146 -0
- package/scripts/per-step-scoring.js +165 -0
- package/scripts/perplexity-marketing.js +466 -0
- package/scripts/pii-scanner.js +153 -0
- package/scripts/plan-gate.js +154 -0
- package/scripts/post-everywhere.js +308 -0
- package/scripts/post-to-x-retry.sh +22 -0
- package/scripts/post-to-x.js +369 -0
- package/scripts/pr-manager.js +236 -0
- package/scripts/predictive-insights.js +356 -0
- package/scripts/principle-extractor.js +162 -0
- package/scripts/pro-features.js +40 -0
- package/scripts/pro-local-dashboard.js +174 -0
- package/scripts/problem-detail.js +53 -0
- package/scripts/product-feedback.js +134 -0
- package/scripts/profile-router.js +245 -0
- package/scripts/prompt-dlp.js +221 -0
- package/scripts/prompt-guard.js +83 -0
- package/scripts/prove-adapters.js +863 -0
- package/scripts/prove-attribution.js +365 -0
- package/scripts/prove-automation.js +653 -0
- package/scripts/prove-autoresearch.js +304 -0
- package/scripts/prove-claim-verification.js +277 -0
- package/scripts/prove-cloudflare-sandbox.js +163 -0
- package/scripts/prove-data-pipeline.js +410 -0
- package/scripts/prove-data-quality.js +227 -0
- package/scripts/prove-evolution.js +352 -0
- package/scripts/prove-harnesses.js +287 -0
- package/scripts/prove-intelligence.js +259 -0
- package/scripts/prove-lancedb.js +371 -0
- package/scripts/prove-local-intelligence.js +342 -0
- package/scripts/prove-loop-closure.js +263 -0
- package/scripts/prove-predictive-insights.js +357 -0
- package/scripts/prove-runtime.js +350 -0
- package/scripts/prove-seo-gsd.js +234 -0
- package/scripts/prove-settings.js +279 -0
- package/scripts/prove-subway-upgrades.js +277 -0
- package/scripts/prove-tessl.js +229 -0
- package/scripts/prove-training-export.js +327 -0
- package/scripts/prove-workflow-contract.js +116 -0
- package/scripts/prove-xmemory.js +332 -0
- package/scripts/publish-decision.js +133 -0
- package/scripts/pulse.js +80 -0
- package/scripts/rate-limiter.js +125 -0
- package/scripts/reddit-dm-outreach.js +182 -0
- package/scripts/reddit-monitor-cron.sh +26 -0
- package/scripts/reflector-agent.js +221 -0
- package/scripts/reminder-engine.js +132 -0
- package/scripts/revenue-status.js +472 -0
- package/scripts/risk-scorer.js +459 -0
- package/scripts/rlaif-self-audit.js +129 -0
- package/scripts/rlhf_session_start.sh +32 -0
- package/scripts/rubric-engine.js +230 -0
- package/scripts/schedule-manager.js +251 -0
- package/scripts/secret-scanner.js +414 -0
- package/scripts/self-heal.js +147 -0
- package/scripts/self-healing-check.js +188 -0
- package/scripts/semantic-layer.js +98 -0
- package/scripts/seo-gsd.js +1153 -0
- package/scripts/settings-hierarchy.js +214 -0
- package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
- package/scripts/skill-exporter.js +262 -0
- package/scripts/skill-generator.js +446 -0
- package/scripts/skill-materializer.js +134 -0
- package/scripts/skill-packs.js +136 -0
- package/scripts/skill-proposer.js +99 -0
- package/scripts/skill-quality-tracker.js +282 -0
- package/scripts/slo-alert-engine.js +14 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/social-analytics/db/schema.sql +32 -0
- package/scripts/social-analytics/db/social-analytics.db +0 -0
- package/scripts/social-analytics/digest.js +256 -0
- package/scripts/social-analytics/generate-instagram-card.js +97 -0
- package/scripts/social-analytics/instagram-thumbgate-post.js +107 -0
- package/scripts/social-analytics/load-env.js +46 -0
- package/scripts/social-analytics/mcp-server.js +289 -0
- package/scripts/social-analytics/normalizer.js +580 -0
- package/scripts/social-analytics/notify.js +162 -0
- package/scripts/social-analytics/poll-all.js +92 -0
- package/scripts/social-analytics/pollers/github.js +195 -0
- package/scripts/social-analytics/pollers/instagram.js +253 -0
- package/scripts/social-analytics/pollers/linkedin.js +330 -0
- package/scripts/social-analytics/pollers/plausible.js +247 -0
- package/scripts/social-analytics/pollers/reddit.js +306 -0
- package/scripts/social-analytics/pollers/threads.js +233 -0
- package/scripts/social-analytics/pollers/tiktok.js +203 -0
- package/scripts/social-analytics/pollers/x.js +227 -0
- package/scripts/social-analytics/pollers/youtube.js +304 -0
- package/scripts/social-analytics/pollers/zernio.js +183 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +98 -0
- package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
- package/scripts/social-analytics/publishers/devto.js +122 -0
- package/scripts/social-analytics/publishers/instagram.js +317 -0
- package/scripts/social-analytics/publishers/linkedin.js +294 -0
- package/scripts/social-analytics/publishers/reddit.js +390 -0
- package/scripts/social-analytics/publishers/threads.js +275 -0
- package/scripts/social-analytics/publishers/tiktok.js +217 -0
- package/scripts/social-analytics/publishers/x.js +259 -0
- package/scripts/social-analytics/publishers/youtube.js +223 -0
- package/scripts/social-analytics/publishers/zernio.js +378 -0
- package/scripts/social-analytics/run-digest.js +34 -0
- package/scripts/social-analytics/store.js +257 -0
- package/scripts/social-analytics/utm.js +143 -0
- package/scripts/social-pipeline.js +2628 -0
- package/scripts/social-quality-gate.js +18 -0
- package/scripts/social-reply-monitor.js +445 -0
- package/scripts/status-dashboard.js +155 -0
- package/scripts/statusline-lesson.js +16 -0
- package/scripts/statusline-tower.js +8 -0
- package/scripts/statusline.sh +116 -0
- package/scripts/stripe-live-status.js +115 -0
- package/scripts/subagent-profiles.js +79 -0
- package/scripts/sync-gh-secrets-from-env.sh +70 -0
- package/scripts/sync-github-about.js +52 -0
- package/scripts/sync-version.js +447 -0
- package/scripts/synthetic-dpo.js +234 -0
- package/scripts/telemetry-analytics.js +821 -0
- package/scripts/tessl-export.js +371 -0
- package/scripts/test-coverage.js +120 -0
- package/scripts/thompson-sampling.js +417 -0
- package/scripts/thumbgate-search.js +189 -0
- package/scripts/tool-kpi-tracker.js +12 -0
- package/scripts/tool-registry.js +811 -0
- package/scripts/train_from_feedback.py +933 -0
- package/scripts/user-profile.js +78 -0
- package/scripts/validate-feedback.js +581 -0
- package/scripts/validate-workflow-contract.js +287 -0
- package/scripts/vector-store.js +197 -0
- package/scripts/verification-loop.js +291 -0
- package/scripts/verify-obsidian-setup.sh +269 -0
- package/scripts/verify-run.js +269 -0
- package/scripts/webhook-delivery.js +62 -0
- package/scripts/weekly-auto-post.js +124 -0
- package/scripts/workflow-runs.js +154 -0
- package/scripts/workflow-sprint-intake.js +475 -0
- package/scripts/workspace-evolver.js +374 -0
- package/scripts/x-autonomous-marketing.js +139 -0
- package/scripts/xmemory-lite.js +405 -0
- package/skills/agent-memory/SKILL.md +97 -0
- package/skills/rlhf-feedback/SKILL.md +49 -0
- package/skills/solve-architecture-autonomy/SKILL.md +17 -0
- package/skills/solve-architecture-autonomy/tool.js +33 -0
- package/skills/thumbgate/SKILL.md +114 -0
- package/src/api/server.js +4206 -0
|
@@ -0,0 +1,1287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ContextFS
|
|
4
|
+
*
|
|
5
|
+
* Persistent, file-system-native context store implementing:
|
|
6
|
+
* - Constructor: build relevant context pack
|
|
7
|
+
* - Loader: enforce bounded context size
|
|
8
|
+
* - Evaluator: record pack outcome for learning loop
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
15
|
+
const {
|
|
16
|
+
retrieveHierarchicalDocuments,
|
|
17
|
+
shouldUseHierarchicalRetrieval,
|
|
18
|
+
} = require('./xmemory-lite');
|
|
19
|
+
|
|
20
|
+
const CONTEXTFS_RETRIEVAL_VERSION = 'xmemory-lite-v1';
|
|
21
|
+
|
|
22
|
+
function getFeedbackBaseDir() {
|
|
23
|
+
return resolveFeedbackDir();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const FEEDBACK_DIR = getFeedbackBaseDir();
|
|
27
|
+
const CONTEXTFS_ROOT = process.env.THUMBGATE_CONTEXTFS_DIR
|
|
28
|
+
|| (FEEDBACK_DIR.endsWith('contextfs') ? FEEDBACK_DIR : path.join(FEEDBACK_DIR, 'contextfs'));
|
|
29
|
+
|
|
30
|
+
const NAMESPACES = {
|
|
31
|
+
rawHistory: 'raw_history',
|
|
32
|
+
memoryError: path.join('memory', 'error'),
|
|
33
|
+
memoryLearning: path.join('memory', 'learning'),
|
|
34
|
+
rules: 'rules',
|
|
35
|
+
tools: 'tools',
|
|
36
|
+
provenance: 'provenance',
|
|
37
|
+
session: 'session',
|
|
38
|
+
research: 'research',
|
|
39
|
+
};
|
|
40
|
+
const DEFAULT_SEARCH_NAMESPACES = [
|
|
41
|
+
NAMESPACES.memoryError,
|
|
42
|
+
NAMESPACES.memoryLearning,
|
|
43
|
+
NAMESPACES.rules,
|
|
44
|
+
NAMESPACES.rawHistory,
|
|
45
|
+
];
|
|
46
|
+
const NAMESPACE_ALIAS_MAP = new Map([
|
|
47
|
+
...Object.entries(NAMESPACES).map(([key, value]) => [key, value]),
|
|
48
|
+
...Object.values(NAMESPACES).map((value) => [value, value]),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const PACK_TEMPLATES = {
|
|
52
|
+
'bug-investigation': {
|
|
53
|
+
namespaces: ['memoryError', 'rules'],
|
|
54
|
+
maxItems: 10,
|
|
55
|
+
maxChars: 8000,
|
|
56
|
+
queryPrefix: 'bug failure error crash',
|
|
57
|
+
},
|
|
58
|
+
'session-resume': {
|
|
59
|
+
namespaces: ['session', 'memoryLearning', 'rules'],
|
|
60
|
+
maxItems: 8,
|
|
61
|
+
maxChars: 6000,
|
|
62
|
+
queryPrefix: 'session context resume',
|
|
63
|
+
},
|
|
64
|
+
'sales-call-prep': {
|
|
65
|
+
namespaces: ['memoryLearning', 'rules'],
|
|
66
|
+
maxItems: 6,
|
|
67
|
+
maxChars: 4000,
|
|
68
|
+
queryPrefix: 'value proof evidence workflow',
|
|
69
|
+
},
|
|
70
|
+
'competitor-scan': {
|
|
71
|
+
namespaces: ['memoryLearning', 'memoryError'],
|
|
72
|
+
maxItems: 8,
|
|
73
|
+
maxChars: 6000,
|
|
74
|
+
queryPrefix: 'competitor comparison alternative',
|
|
75
|
+
},
|
|
76
|
+
'research-brief': {
|
|
77
|
+
namespaces: ['research'],
|
|
78
|
+
maxItems: 10,
|
|
79
|
+
maxChars: 8000,
|
|
80
|
+
queryPrefix: 'research paper',
|
|
81
|
+
},
|
|
82
|
+
'autoresearch-brief': {
|
|
83
|
+
namespaces: ['research', 'memoryLearning', 'rules'],
|
|
84
|
+
maxItems: 12,
|
|
85
|
+
maxChars: 10000,
|
|
86
|
+
queryPrefix: 'research benchmark experiment reliability',
|
|
87
|
+
},
|
|
88
|
+
'gtm-research': {
|
|
89
|
+
namespaces: ['research', 'memoryLearning'],
|
|
90
|
+
maxItems: 10,
|
|
91
|
+
maxChars: 8000,
|
|
92
|
+
queryPrefix: 'marketing conversion acquisition research',
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function ensureDir(dirPath) {
|
|
97
|
+
if (!fs.existsSync(dirPath)) {
|
|
98
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ensureContextFs() {
|
|
103
|
+
Object.values(NAMESPACES).forEach((subPath) => {
|
|
104
|
+
ensureDir(path.join(CONTEXTFS_ROOT, subPath));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function nowIso() {
|
|
109
|
+
return new Date().toISOString();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function inferNamespaceFromPath(filePath) {
|
|
113
|
+
if (!filePath) return '';
|
|
114
|
+
const relativeDir = path.relative(CONTEXTFS_ROOT, path.dirname(filePath));
|
|
115
|
+
if (!relativeDir || relativeDir.startsWith('..')) return '';
|
|
116
|
+
return relativeDir;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toSlug(input) {
|
|
120
|
+
return String(input || 'item')
|
|
121
|
+
.toLowerCase()
|
|
122
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
123
|
+
.replace(/^-+|-+$/g, '')
|
|
124
|
+
.slice(0, 80) || 'item';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function writeJson(filePath, payload) {
|
|
128
|
+
ensureDir(path.dirname(filePath));
|
|
129
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function appendJsonl(filePath, payload) {
|
|
133
|
+
ensureDir(path.dirname(filePath));
|
|
134
|
+
fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readJsonl(filePath) {
|
|
138
|
+
if (!fs.existsSync(filePath)) return [];
|
|
139
|
+
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
140
|
+
if (!raw) return [];
|
|
141
|
+
return raw
|
|
142
|
+
.split('\n')
|
|
143
|
+
.map((line) => {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(line);
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
.filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function listJsonFiles(dirPath) {
|
|
154
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
155
|
+
const files = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
156
|
+
const out = [];
|
|
157
|
+
files.forEach((entry) => {
|
|
158
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
159
|
+
if (entry.isDirectory()) {
|
|
160
|
+
out.push(...listJsonFiles(fullPath));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
164
|
+
out.push(fullPath);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function tokenizeQuery(query) {
|
|
171
|
+
return String(query || '')
|
|
172
|
+
.toLowerCase()
|
|
173
|
+
.split(/[^a-z0-9]+/)
|
|
174
|
+
.filter(Boolean);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function uniqueTokens(tokens) {
|
|
178
|
+
return Array.from(new Set(tokens));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function querySimilarity(tokensA, tokensB) {
|
|
182
|
+
const setA = new Set(uniqueTokens(tokensA));
|
|
183
|
+
const setB = new Set(uniqueTokens(tokensB));
|
|
184
|
+
if (setA.size === 0 && setB.size === 0) return 1;
|
|
185
|
+
if (setA.size === 0 || setB.size === 0) return 0;
|
|
186
|
+
|
|
187
|
+
let intersection = 0;
|
|
188
|
+
for (const token of setA) {
|
|
189
|
+
if (setB.has(token)) intersection += 1;
|
|
190
|
+
}
|
|
191
|
+
const union = setA.size + setB.size - intersection;
|
|
192
|
+
return union === 0 ? 0 : intersection / union;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildSemanticCacheKey({ namespaces, maxItems, maxChars }) {
|
|
196
|
+
return JSON.stringify({
|
|
197
|
+
retrievalVersion: CONTEXTFS_RETRIEVAL_VERSION,
|
|
198
|
+
namespaces: normalizeNamespaces(namespaces),
|
|
199
|
+
maxItems,
|
|
200
|
+
maxChars,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getSemanticCacheConfig() {
|
|
205
|
+
const enabled = process.env.THUMBGATE_SEMANTIC_CACHE_ENABLED !== 'false';
|
|
206
|
+
const thresholdRaw = Number(process.env.THUMBGATE_SEMANTIC_CACHE_THRESHOLD || '0.7');
|
|
207
|
+
const ttlSecondsRaw = Number(process.env.THUMBGATE_SEMANTIC_CACHE_TTL_SECONDS || '86400');
|
|
208
|
+
const threshold = Number.isFinite(thresholdRaw) ? Math.min(1, Math.max(0, thresholdRaw)) : 0.7;
|
|
209
|
+
const ttlSeconds = Number.isFinite(ttlSecondsRaw) ? Math.max(60, ttlSecondsRaw) : 86400;
|
|
210
|
+
return { enabled, threshold, ttlSeconds };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getSemanticCachePath() {
|
|
214
|
+
return path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'semantic-cache.jsonl');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function loadSemanticCacheEntries() {
|
|
218
|
+
return readJsonl(getSemanticCachePath());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function appendSemanticCacheEntry(entry) {
|
|
222
|
+
appendJsonl(getSemanticCachePath(), entry);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getSourceHash(namespaces) {
|
|
226
|
+
const hasher = crypto.createHash('sha256');
|
|
227
|
+
const normalizedNamespaces = normalizeNamespaces(namespaces);
|
|
228
|
+
|
|
229
|
+
for (const ns of normalizedNamespaces) {
|
|
230
|
+
const dirPath = path.join(CONTEXTFS_ROOT, ns);
|
|
231
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
232
|
+
|
|
233
|
+
const files = fs.readdirSync(dirPath).sort();
|
|
234
|
+
for (const file of files) {
|
|
235
|
+
if (file.endsWith('.json') || file.endsWith('.jsonl') || file.endsWith('.md')) {
|
|
236
|
+
const filePath = path.join(dirPath, file);
|
|
237
|
+
try {
|
|
238
|
+
const stats = fs.statSync(filePath);
|
|
239
|
+
hasher.update(`${file}:${stats.mtimeMs}:${stats.size}`);
|
|
240
|
+
} catch {
|
|
241
|
+
// Skip if file disappeared
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return hasher.digest('hex');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function findSemanticCacheHit({ query, namespaces, maxItems, maxChars }) {
|
|
250
|
+
const { enabled, threshold, ttlSeconds } = getSemanticCacheConfig();
|
|
251
|
+
if (!enabled) return null;
|
|
252
|
+
|
|
253
|
+
const entries = loadSemanticCacheEntries();
|
|
254
|
+
if (entries.length === 0) return null;
|
|
255
|
+
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const queryTokens = tokenizeQuery(query);
|
|
258
|
+
const key = buildSemanticCacheKey({ namespaces, maxItems, maxChars });
|
|
259
|
+
const currentSourceHash = getSourceHash(namespaces);
|
|
260
|
+
|
|
261
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
262
|
+
const entry = entries[i];
|
|
263
|
+
if (!entry || entry.key !== key || !entry.pack) continue;
|
|
264
|
+
|
|
265
|
+
// Zero-Waste Caching: validate source hash
|
|
266
|
+
if (entry.sourceHash !== currentSourceHash) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const createdMs = new Date(entry.timestamp || 0).getTime();
|
|
271
|
+
if (Number.isFinite(createdMs) && now - createdMs > ttlSeconds * 1000) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const score = querySimilarity(queryTokens, Array.isArray(entry.tokens) ? entry.tokens : []);
|
|
276
|
+
if (score >= threshold) {
|
|
277
|
+
return {
|
|
278
|
+
score,
|
|
279
|
+
entry,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function recordProvenance(event) {
|
|
288
|
+
ensureContextFs();
|
|
289
|
+
const payload = {
|
|
290
|
+
id: `prov_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
291
|
+
timestamp: nowIso(),
|
|
292
|
+
...event,
|
|
293
|
+
};
|
|
294
|
+
appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'events.jsonl'), payload);
|
|
295
|
+
return payload;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function writeContextObject({ namespace, title, content, tags = [], source, ttl = null, metadata = {} }) {
|
|
299
|
+
ensureContextFs();
|
|
300
|
+
|
|
301
|
+
const id = `${Date.now()}_${toSlug(title)}`;
|
|
302
|
+
const filePath = path.join(CONTEXTFS_ROOT, namespace, `${id}.json`);
|
|
303
|
+
|
|
304
|
+
const doc = {
|
|
305
|
+
id,
|
|
306
|
+
namespace,
|
|
307
|
+
title,
|
|
308
|
+
content,
|
|
309
|
+
tags,
|
|
310
|
+
source: source || 'unknown',
|
|
311
|
+
ttl,
|
|
312
|
+
metadata,
|
|
313
|
+
createdAt: nowIso(),
|
|
314
|
+
lastUsedAt: null,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
writeJson(filePath, doc);
|
|
318
|
+
indexContextObject(doc, filePath);
|
|
319
|
+
|
|
320
|
+
recordProvenance({
|
|
321
|
+
type: 'context_object_created',
|
|
322
|
+
namespace,
|
|
323
|
+
objectId: id,
|
|
324
|
+
source: doc.source,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
id,
|
|
329
|
+
namespace,
|
|
330
|
+
filePath,
|
|
331
|
+
document: doc,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function mergeMetadata(existingMetadata = {}, incomingMetadata = {}) {
|
|
336
|
+
const merged = { ...existingMetadata };
|
|
337
|
+
|
|
338
|
+
Object.entries(incomingMetadata).forEach(([key, value]) => {
|
|
339
|
+
if (value === undefined) return;
|
|
340
|
+
|
|
341
|
+
const currentValue = merged[key];
|
|
342
|
+
if (Array.isArray(currentValue) && Array.isArray(value)) {
|
|
343
|
+
merged[key] = [...new Set([...currentValue, ...value])];
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
merged[key] = value;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return merged;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function normalizeTagList(tags) {
|
|
354
|
+
return Array.isArray(tags)
|
|
355
|
+
? [...new Set(tags.map((tag) => String(tag)))]
|
|
356
|
+
.sort()
|
|
357
|
+
: [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function findExistingContextObject({ namespace, title, content, tags = [], source }) {
|
|
361
|
+
ensureContextFs();
|
|
362
|
+
|
|
363
|
+
const expectedTags = normalizeTagList(tags);
|
|
364
|
+
const dirPath = path.join(CONTEXTFS_ROOT, namespace);
|
|
365
|
+
const files = listJsonFiles(dirPath).sort();
|
|
366
|
+
|
|
367
|
+
for (const filePath of files) {
|
|
368
|
+
try {
|
|
369
|
+
const doc = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
370
|
+
if (doc.title !== title || doc.content !== content || doc.source !== source) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (JSON.stringify(normalizeTagList(doc.tags)) !== JSON.stringify(expectedTags)) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
filePath,
|
|
380
|
+
document: doc,
|
|
381
|
+
};
|
|
382
|
+
} catch {
|
|
383
|
+
// Ignore malformed entries while searching for exact duplicates.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function upsertContextObject({ namespace, title, content, tags = [], source, ttl = null, metadata = {} }) {
|
|
391
|
+
const existing = findExistingContextObject({
|
|
392
|
+
namespace,
|
|
393
|
+
title,
|
|
394
|
+
content,
|
|
395
|
+
tags,
|
|
396
|
+
source,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
if (!existing) {
|
|
400
|
+
return writeContextObject({
|
|
401
|
+
namespace,
|
|
402
|
+
title,
|
|
403
|
+
content,
|
|
404
|
+
tags,
|
|
405
|
+
source,
|
|
406
|
+
ttl,
|
|
407
|
+
metadata,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const mergedDocument = {
|
|
412
|
+
...existing.document,
|
|
413
|
+
ttl: ttl === null ? existing.document.ttl || null : ttl,
|
|
414
|
+
metadata: mergeMetadata(existing.document.metadata || {}, metadata),
|
|
415
|
+
};
|
|
416
|
+
writeJson(existing.filePath, mergedDocument);
|
|
417
|
+
|
|
418
|
+
recordProvenance({
|
|
419
|
+
type: 'context_object_deduped',
|
|
420
|
+
namespace,
|
|
421
|
+
objectId: existing.document.id,
|
|
422
|
+
source: source || existing.document.source || 'unknown',
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
id: mergedDocument.id,
|
|
427
|
+
namespace,
|
|
428
|
+
filePath: existing.filePath,
|
|
429
|
+
document: mergedDocument,
|
|
430
|
+
deduped: true,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function registerFeedback(feedbackEvent, memoryRecord = null) {
|
|
435
|
+
ensureContextFs();
|
|
436
|
+
|
|
437
|
+
const raw = writeContextObject({
|
|
438
|
+
namespace: NAMESPACES.rawHistory,
|
|
439
|
+
title: `feedback_${feedbackEvent.signal}_${feedbackEvent.id}`,
|
|
440
|
+
content: JSON.stringify(feedbackEvent),
|
|
441
|
+
tags: feedbackEvent.tags || [],
|
|
442
|
+
source: 'feedback-event',
|
|
443
|
+
metadata: {
|
|
444
|
+
signal: feedbackEvent.signal,
|
|
445
|
+
actionType: feedbackEvent.actionType,
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
let memory = null;
|
|
450
|
+
if (memoryRecord) {
|
|
451
|
+
const namespace = memoryRecord.category === 'error'
|
|
452
|
+
? NAMESPACES.memoryError
|
|
453
|
+
: NAMESPACES.memoryLearning;
|
|
454
|
+
memory = upsertContextObject({
|
|
455
|
+
namespace,
|
|
456
|
+
title: memoryRecord.title,
|
|
457
|
+
content: memoryRecord.content,
|
|
458
|
+
tags: memoryRecord.tags || [],
|
|
459
|
+
source: 'feedback-memory',
|
|
460
|
+
metadata: {
|
|
461
|
+
category: memoryRecord.category,
|
|
462
|
+
sourceFeedbackId: memoryRecord.sourceFeedbackId,
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return { raw, memory };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function registerPreventionRules(markdown, metadata = {}) {
|
|
471
|
+
return writeContextObject({
|
|
472
|
+
namespace: NAMESPACES.rules,
|
|
473
|
+
title: `prevention_rules_${new Date().toISOString().slice(0, 10)}`,
|
|
474
|
+
content: markdown,
|
|
475
|
+
tags: ['rules', 'prevention'],
|
|
476
|
+
source: 'feedback-loop',
|
|
477
|
+
metadata,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function normalizeNamespaces(namespaces) {
|
|
482
|
+
if (!Array.isArray(namespaces) || namespaces.length === 0) {
|
|
483
|
+
return [...DEFAULT_SEARCH_NAMESPACES];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const normalized = [];
|
|
487
|
+
namespaces.forEach((rawValue) => {
|
|
488
|
+
const value = String(rawValue || '').trim();
|
|
489
|
+
const mapped = NAMESPACE_ALIAS_MAP.get(value);
|
|
490
|
+
if (!mapped) {
|
|
491
|
+
const err = new Error(`Unsupported namespace: ${value}`);
|
|
492
|
+
err.code = 'INVALID_NAMESPACE';
|
|
493
|
+
throw err;
|
|
494
|
+
}
|
|
495
|
+
if (!normalized.includes(mapped)) {
|
|
496
|
+
normalized.push(mapped);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
return normalized;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function loadCandidates(namespaces) {
|
|
504
|
+
ensureContextFs();
|
|
505
|
+
const selected = normalizeNamespaces(namespaces);
|
|
506
|
+
|
|
507
|
+
const docs = [];
|
|
508
|
+
|
|
509
|
+
selected.forEach((namespace) => {
|
|
510
|
+
const dir = path.join(CONTEXTFS_ROOT, namespace);
|
|
511
|
+
const files = listJsonFiles(dir);
|
|
512
|
+
files.forEach((filePath) => {
|
|
513
|
+
try {
|
|
514
|
+
const payload = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
515
|
+
docs.push({
|
|
516
|
+
...payload,
|
|
517
|
+
namespace,
|
|
518
|
+
});
|
|
519
|
+
} catch {
|
|
520
|
+
// ignore malformed files
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
return docs;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function scoreDocument(doc, queryTokens) {
|
|
529
|
+
let score = 0;
|
|
530
|
+
|
|
531
|
+
const haystack = `${doc.title || ''} ${doc.content || ''} ${(doc.tags || []).join(' ')}`.toLowerCase();
|
|
532
|
+
|
|
533
|
+
queryTokens.forEach((token) => {
|
|
534
|
+
if (token.length > 2 && haystack.includes(token)) {
|
|
535
|
+
score += 3;
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
if (doc.namespace.includes('memory/error')) score += 1;
|
|
540
|
+
if (doc.namespace.includes('memory/learning')) score += 1;
|
|
541
|
+
|
|
542
|
+
if (doc.createdAt) {
|
|
543
|
+
const ageMs = Date.now() - new Date(doc.createdAt).getTime();
|
|
544
|
+
if (Number.isFinite(ageMs)) {
|
|
545
|
+
const hours = ageMs / (1000 * 60 * 60);
|
|
546
|
+
if (hours < 24) score += 2;
|
|
547
|
+
else if (hours < 24 * 7) score += 1;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return score;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function buildStructuredContext(doc) {
|
|
555
|
+
const structuredContext = {
|
|
556
|
+
rawContent: doc.content || '',
|
|
557
|
+
reasoning: null,
|
|
558
|
+
whatWentWrong: null,
|
|
559
|
+
whatToChange: null,
|
|
560
|
+
rubricFailure: null,
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const lines = (doc.content || '').split('\n');
|
|
564
|
+
for (const line of lines) {
|
|
565
|
+
if (line.startsWith('Reasoning:')) structuredContext.reasoning = line.replace('Reasoning:', '').trim();
|
|
566
|
+
else if (line.startsWith('What went wrong:')) structuredContext.whatWentWrong = line.replace('What went wrong:', '').trim();
|
|
567
|
+
else if (line.startsWith('How to avoid:')) structuredContext.whatToChange = line.replace('How to avoid:', '').trim();
|
|
568
|
+
else if (line.startsWith('Rubric failing criteria:')) structuredContext.rubricFailure = line.replace('Rubric failing criteria:', '').trim();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return structuredContext;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function measureDocumentChars(doc) {
|
|
575
|
+
return `${doc.title || ''}\n${doc.content || ''}`.length;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function selectFlatContextItems(candidates, maxItems, maxChars) {
|
|
579
|
+
const selected = [];
|
|
580
|
+
let usedChars = 0;
|
|
581
|
+
let skippedByMaxChars = 0;
|
|
582
|
+
|
|
583
|
+
for (const item of candidates) {
|
|
584
|
+
if (selected.length >= maxItems) break;
|
|
585
|
+
|
|
586
|
+
const snippetLength = measureDocumentChars(item.doc);
|
|
587
|
+
if (usedChars + snippetLength > maxChars) {
|
|
588
|
+
skippedByMaxChars += 1;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
selected.push({
|
|
593
|
+
id: item.doc.id,
|
|
594
|
+
namespace: item.doc.namespace,
|
|
595
|
+
title: item.doc.title,
|
|
596
|
+
structuredContext: buildStructuredContext(item.doc),
|
|
597
|
+
tags: item.doc.tags || [],
|
|
598
|
+
score: item.score,
|
|
599
|
+
});
|
|
600
|
+
usedChars += snippetLength;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
items: selected,
|
|
605
|
+
usedChars,
|
|
606
|
+
skippedByMaxChars,
|
|
607
|
+
retrieval: {
|
|
608
|
+
strategy: 'flat',
|
|
609
|
+
themeCount: 0,
|
|
610
|
+
semanticCount: 0,
|
|
611
|
+
selectedThemes: [],
|
|
612
|
+
selectedSemanticGroups: [],
|
|
613
|
+
representativeCount: selected.length,
|
|
614
|
+
expandedEpisodes: 0,
|
|
615
|
+
queryCoverage: null,
|
|
616
|
+
initialCoverage: null,
|
|
617
|
+
coverageTarget: null,
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/* ── Memex-style Indexed Memory ────────────────────────────────── */
|
|
623
|
+
|
|
624
|
+
const MEMEX_INDEX_FILE = 'memex-index.jsonl';
|
|
625
|
+
|
|
626
|
+
function getMemexIndexPath() {
|
|
627
|
+
return path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, MEMEX_INDEX_FILE);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function buildIndexEntry(doc, filePath) {
|
|
631
|
+
const resolvedNamespace = doc.namespace || inferNamespaceFromPath(filePath);
|
|
632
|
+
return {
|
|
633
|
+
id: doc.id,
|
|
634
|
+
namespace: resolvedNamespace,
|
|
635
|
+
title: doc.title || '',
|
|
636
|
+
tags: doc.tags || [],
|
|
637
|
+
digest: String(doc.content || '').slice(0, 120),
|
|
638
|
+
createdAt: doc.createdAt || nowIso(),
|
|
639
|
+
stableRef: filePath,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function indexContextObject(doc, filePath) {
|
|
644
|
+
const entry = buildIndexEntry(doc, filePath);
|
|
645
|
+
appendJsonl(getMemexIndexPath(), entry);
|
|
646
|
+
return entry;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function loadMemexIndex() {
|
|
650
|
+
return readJsonl(getMemexIndexPath());
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function dereferenceEntry(entry) {
|
|
654
|
+
if (!entry || !entry.stableRef) return null;
|
|
655
|
+
try {
|
|
656
|
+
return JSON.parse(fs.readFileSync(entry.stableRef, 'utf-8'));
|
|
657
|
+
} catch {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function searchMemexIndex({ query = '', maxResults = 10, namespaces = [] } = {}) {
|
|
663
|
+
const index = loadMemexIndex();
|
|
664
|
+
const tokens = tokenizeQuery(query);
|
|
665
|
+
const nsFilter = namespaces.length > 0 ? new Set(normalizeNamespaces(namespaces)) : null;
|
|
666
|
+
|
|
667
|
+
const scored = index
|
|
668
|
+
.filter((entry) => {
|
|
669
|
+
const entryNamespace = entry.namespace || inferNamespaceFromPath(entry.stableRef);
|
|
670
|
+
return !nsFilter || nsFilter.has(entryNamespace);
|
|
671
|
+
})
|
|
672
|
+
.map((entry) => {
|
|
673
|
+
const entryNamespace = entry.namespace || inferNamespaceFromPath(entry.stableRef);
|
|
674
|
+
const haystack = `${entry.title} ${entry.digest} ${(entry.tags || []).join(' ')}`.toLowerCase();
|
|
675
|
+
let score = 0;
|
|
676
|
+
tokens.forEach((t) => { if (t.length > 2 && haystack.includes(t)) score += 3; });
|
|
677
|
+
if (entryNamespace.includes('memory/error')) score += 1;
|
|
678
|
+
if (entryNamespace.includes('memory/learning')) score += 1;
|
|
679
|
+
if (entry.createdAt) {
|
|
680
|
+
const hours = (Date.now() - new Date(entry.createdAt).getTime()) / 3_600_000;
|
|
681
|
+
if (Number.isFinite(hours)) {
|
|
682
|
+
if (hours < 24) score += 2;
|
|
683
|
+
else if (hours < 168) score += 1;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return { entry: { ...entry, namespace: entryNamespace }, score };
|
|
687
|
+
})
|
|
688
|
+
.filter((x) => x.score > 0)
|
|
689
|
+
.sort((a, b) => b.score - a.score)
|
|
690
|
+
.slice(0, maxResults);
|
|
691
|
+
|
|
692
|
+
return scored.map((x) => ({ ...x.entry, _score: x.score }));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function constructMemexPack({ query = '', maxItems = 8, maxChars = 6000, namespaces = [] } = {}) {
|
|
696
|
+
const normalizedNamespaces = normalizeNamespaces(namespaces);
|
|
697
|
+
const hits = searchMemexIndex({ query, maxResults: maxItems * 2, namespaces: normalizedNamespaces });
|
|
698
|
+
|
|
699
|
+
const items = [];
|
|
700
|
+
let usedChars = 0;
|
|
701
|
+
const dereferenced = [];
|
|
702
|
+
let skippedByMaxChars = 0;
|
|
703
|
+
|
|
704
|
+
for (const hit of hits) {
|
|
705
|
+
if (items.length >= maxItems) break;
|
|
706
|
+
const full = dereferenceEntry(hit);
|
|
707
|
+
if (!full) continue;
|
|
708
|
+
|
|
709
|
+
const snippetLength = measureDocumentChars(full);
|
|
710
|
+
if (usedChars + snippetLength > maxChars) {
|
|
711
|
+
skippedByMaxChars += 1;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
items.push({
|
|
716
|
+
id: full.id,
|
|
717
|
+
namespace: hit.namespace,
|
|
718
|
+
title: full.title,
|
|
719
|
+
structuredContext: buildStructuredContext(full),
|
|
720
|
+
tags: full.tags || [],
|
|
721
|
+
score: hit._score,
|
|
722
|
+
});
|
|
723
|
+
usedChars += snippetLength;
|
|
724
|
+
dereferenced.push(hit.id);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const visibility = {
|
|
728
|
+
itemCount: items.length,
|
|
729
|
+
sourceCandidateCount: hits.length,
|
|
730
|
+
hiddenCount: Math.max(hits.length - items.length, 0),
|
|
731
|
+
maxItemsHit: hits.length > maxItems && items.length >= maxItems,
|
|
732
|
+
maxCharsHit: skippedByMaxChars > 0,
|
|
733
|
+
skippedByMaxChars,
|
|
734
|
+
remainingCharBudget: Math.max(maxChars - usedChars, 0),
|
|
735
|
+
visibleTitles: items.slice(0, 5).map((item) => item.title),
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const packId = `memex_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
739
|
+
const pack = {
|
|
740
|
+
packId,
|
|
741
|
+
query,
|
|
742
|
+
maxItems,
|
|
743
|
+
maxChars,
|
|
744
|
+
usedChars,
|
|
745
|
+
namespaces: normalizedNamespaces,
|
|
746
|
+
createdAt: nowIso(),
|
|
747
|
+
items,
|
|
748
|
+
indexHits: hits.length,
|
|
749
|
+
dereferencedCount: dereferenced.length,
|
|
750
|
+
visibility,
|
|
751
|
+
cache: { hit: false },
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
|
|
755
|
+
recordProvenance({
|
|
756
|
+
type: 'memex_pack_constructed',
|
|
757
|
+
packId,
|
|
758
|
+
query,
|
|
759
|
+
indexHits: hits.length,
|
|
760
|
+
dereferencedCount: dereferenced.length,
|
|
761
|
+
usedChars,
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
return pack;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function constructContextPack({ query = '', maxItems = 8, maxChars = 6000, namespaces = [] } = {}) {
|
|
768
|
+
const normalizedNamespaces = normalizeNamespaces(namespaces);
|
|
769
|
+
const tokens = tokenizeQuery(query);
|
|
770
|
+
const sourceHash = getSourceHash(normalizedNamespaces);
|
|
771
|
+
|
|
772
|
+
const cacheHit = findSemanticCacheHit({
|
|
773
|
+
query,
|
|
774
|
+
namespaces: normalizedNamespaces,
|
|
775
|
+
maxItems,
|
|
776
|
+
maxChars,
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
if (cacheHit) {
|
|
780
|
+
const packId = `pack_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
781
|
+
const cachedPack = cacheHit.entry.pack;
|
|
782
|
+
const pack = {
|
|
783
|
+
...cachedPack,
|
|
784
|
+
packId,
|
|
785
|
+
query,
|
|
786
|
+
createdAt: nowIso(),
|
|
787
|
+
cache: {
|
|
788
|
+
hit: true,
|
|
789
|
+
similarity: Number(cacheHit.score.toFixed(4)),
|
|
790
|
+
matchedQuery: cacheHit.entry.query,
|
|
791
|
+
sourcePackId: cachedPack.packId,
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
|
|
796
|
+
recordProvenance({
|
|
797
|
+
type: 'context_pack_cache_hit',
|
|
798
|
+
packId,
|
|
799
|
+
sourcePackId: cachedPack.packId,
|
|
800
|
+
query,
|
|
801
|
+
similarity: Number(cacheHit.score.toFixed(4)),
|
|
802
|
+
itemCount: Array.isArray(pack.items) ? pack.items.length : 0,
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
return pack;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const candidates = loadCandidates(normalizedNamespaces)
|
|
809
|
+
.map((doc) => ({ doc, score: scoreDocument(doc, tokens) }))
|
|
810
|
+
.sort((a, b) => b.score - a.score);
|
|
811
|
+
|
|
812
|
+
const hierarchicalRetrievalEnabled = shouldUseHierarchicalRetrieval(normalizedNamespaces);
|
|
813
|
+
const selection = hierarchicalRetrievalEnabled
|
|
814
|
+
? retrieveHierarchicalDocuments({
|
|
815
|
+
documents: candidates.map((candidate) => candidate.doc),
|
|
816
|
+
query,
|
|
817
|
+
maxItems,
|
|
818
|
+
maxChars,
|
|
819
|
+
scorer: scoreDocument,
|
|
820
|
+
measureDocument: measureDocumentChars,
|
|
821
|
+
})
|
|
822
|
+
: selectFlatContextItems(candidates, maxItems, maxChars);
|
|
823
|
+
|
|
824
|
+
const selected = selection.items.map((doc) => ({
|
|
825
|
+
id: doc.id,
|
|
826
|
+
namespace: doc.namespace,
|
|
827
|
+
title: doc.title,
|
|
828
|
+
structuredContext: buildStructuredContext(doc),
|
|
829
|
+
tags: doc.tags || [],
|
|
830
|
+
score: scoreDocument(doc, tokens),
|
|
831
|
+
}));
|
|
832
|
+
const usedChars = selection.usedChars;
|
|
833
|
+
const skippedByMaxChars = selection.skippedByMaxChars;
|
|
834
|
+
|
|
835
|
+
const visibility = {
|
|
836
|
+
itemCount: selected.length,
|
|
837
|
+
sourceCandidateCount: candidates.length,
|
|
838
|
+
hiddenCount: Math.max(candidates.length - selected.length, 0),
|
|
839
|
+
maxItemsHit: candidates.length > maxItems && selected.length >= maxItems,
|
|
840
|
+
maxCharsHit: skippedByMaxChars > 0,
|
|
841
|
+
skippedByMaxChars,
|
|
842
|
+
remainingCharBudget: Math.max(maxChars - usedChars, 0),
|
|
843
|
+
visibleTitles: selected.slice(0, 5).map((item) => item.title),
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const packId = `pack_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
847
|
+
const pack = {
|
|
848
|
+
packId,
|
|
849
|
+
query,
|
|
850
|
+
maxItems,
|
|
851
|
+
maxChars,
|
|
852
|
+
usedChars,
|
|
853
|
+
namespaces: normalizedNamespaces,
|
|
854
|
+
createdAt: nowIso(),
|
|
855
|
+
items: selected,
|
|
856
|
+
visibility,
|
|
857
|
+
cache: {
|
|
858
|
+
hit: false,
|
|
859
|
+
},
|
|
860
|
+
sourceHash,
|
|
861
|
+
retrieval: selection.retrieval,
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
|
|
865
|
+
appendSemanticCacheEntry({
|
|
866
|
+
id: `cache_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
867
|
+
timestamp: nowIso(),
|
|
868
|
+
key: buildSemanticCacheKey({
|
|
869
|
+
namespaces: normalizedNamespaces,
|
|
870
|
+
maxItems,
|
|
871
|
+
maxChars,
|
|
872
|
+
}),
|
|
873
|
+
query,
|
|
874
|
+
tokens,
|
|
875
|
+
sourceHash,
|
|
876
|
+
pack,
|
|
877
|
+
});
|
|
878
|
+
recordProvenance({
|
|
879
|
+
type: 'context_pack_constructed',
|
|
880
|
+
packId,
|
|
881
|
+
query,
|
|
882
|
+
itemCount: selected.length,
|
|
883
|
+
usedChars,
|
|
884
|
+
sourceHash,
|
|
885
|
+
retrievalStrategy: selection.retrieval.strategy,
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
return pack;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function evaluateContextPack({ packId, outcome, signal = null, notes = '', rubricEvaluation = null }) {
|
|
892
|
+
const evaluation = {
|
|
893
|
+
id: `eval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
894
|
+
packId,
|
|
895
|
+
outcome,
|
|
896
|
+
signal,
|
|
897
|
+
notes,
|
|
898
|
+
rubricEvaluation,
|
|
899
|
+
timestamp: nowIso(),
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'evaluations.jsonl'), evaluation);
|
|
903
|
+
recordProvenance({
|
|
904
|
+
type: 'context_pack_evaluated',
|
|
905
|
+
packId,
|
|
906
|
+
outcome,
|
|
907
|
+
signal,
|
|
908
|
+
rubricPromotionEligible: rubricEvaluation ? rubricEvaluation.promotionEligible : null,
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
return evaluation;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function getProvenance(limit = 50) {
|
|
915
|
+
const eventsPath = path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'events.jsonl');
|
|
916
|
+
const events = readJsonl(eventsPath);
|
|
917
|
+
return events.slice(-limit);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Write a session handoff primer to contextfs/session/primer.json.
|
|
922
|
+
* Captures git state, last task, next step, and blockers so the next
|
|
923
|
+
* session starts with full context — no manual primer.md needed.
|
|
924
|
+
*/
|
|
925
|
+
function writeSessionHandoff({ project, branch, lastTask, nextStep, blockers, openFiles, customContext } = {}) {
|
|
926
|
+
ensureDir(path.join(CONTEXTFS_ROOT, NAMESPACES.session));
|
|
927
|
+
|
|
928
|
+
let gitContext = {};
|
|
929
|
+
try {
|
|
930
|
+
const { execSync } = require('child_process');
|
|
931
|
+
const cwd = process.cwd();
|
|
932
|
+
gitContext = {
|
|
933
|
+
branch: branch || execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8' }).trim(),
|
|
934
|
+
lastCommits: execSync('git log --oneline -5', { cwd, encoding: 'utf8' }).trim().split('\n'),
|
|
935
|
+
modifiedFiles: execSync('git diff --name-only HEAD~1 2>/dev/null || echo ""', { cwd, encoding: 'utf8' }).trim().split('\n').filter(Boolean),
|
|
936
|
+
status: execSync('git status --short', { cwd, encoding: 'utf8' }).trim().split('\n').filter(Boolean),
|
|
937
|
+
};
|
|
938
|
+
} catch (_) {
|
|
939
|
+
gitContext = { branch: branch || 'unknown', lastCommits: [], modifiedFiles: [], status: [] };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const primer = {
|
|
943
|
+
id: `session_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`,
|
|
944
|
+
timestamp: nowIso(),
|
|
945
|
+
project: project || path.basename(process.cwd()),
|
|
946
|
+
git: gitContext,
|
|
947
|
+
lastTask: lastTask || null,
|
|
948
|
+
nextStep: nextStep || null,
|
|
949
|
+
blockers: Array.isArray(blockers) ? blockers : (blockers ? [blockers] : []),
|
|
950
|
+
openFiles: Array.isArray(openFiles) ? openFiles : [],
|
|
951
|
+
customContext: customContext || null,
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const primerPath = path.join(CONTEXTFS_ROOT, NAMESPACES.session, 'primer.json');
|
|
955
|
+
fs.writeFileSync(primerPath, JSON.stringify(primer, null, 2));
|
|
956
|
+
|
|
957
|
+
// Sync to primer.md if it exists
|
|
958
|
+
const mdPrimerPath = path.join(process.cwd(), 'primer.md');
|
|
959
|
+
if (fs.existsSync(mdPrimerPath)) {
|
|
960
|
+
try {
|
|
961
|
+
let md = fs.readFileSync(mdPrimerPath, 'utf8');
|
|
962
|
+
if (primer.lastTask) {
|
|
963
|
+
md = md.replace(/## Last Completed Task\n- .*/, `## Last Completed Task\n- ${primer.lastTask}`);
|
|
964
|
+
}
|
|
965
|
+
if (primer.nextStep) {
|
|
966
|
+
md = md.replace(/## Exact Next Step\n- .*/, `## Exact Next Step\n- ${primer.nextStep}`);
|
|
967
|
+
}
|
|
968
|
+
if (primer.blockers.length > 0) {
|
|
969
|
+
md = md.replace(/## Open Blockers\n(?:- .*\n)*/, `## Open Blockers\n${primer.blockers.map(b => `- ${b}`).join('\n')}\n`);
|
|
970
|
+
}
|
|
971
|
+
fs.writeFileSync(mdPrimerPath, md);
|
|
972
|
+
|
|
973
|
+
// Trigger full memory refresh (Layer 3, 4, 5)
|
|
974
|
+
const { execSync } = require('child_process');
|
|
975
|
+
execSync('./bin/memory.sh', { stdio: 'ignore' });
|
|
976
|
+
} catch (e) {
|
|
977
|
+
console.error('Warning: Failed to sync to primer.md:', e.message);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
recordProvenance({
|
|
982
|
+
action: 'session_handoff',
|
|
983
|
+
source: 'session',
|
|
984
|
+
detail: `Handoff: ${primer.lastTask || 'no task'} → ${primer.nextStep || 'no next step'}`,
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
return primer;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Read the most recent session handoff primer.
|
|
992
|
+
*/
|
|
993
|
+
function readSessionHandoff() {
|
|
994
|
+
const primerPath = path.join(CONTEXTFS_ROOT, NAMESPACES.session, 'primer.json');
|
|
995
|
+
if (!fs.existsSync(primerPath)) return null;
|
|
996
|
+
try {
|
|
997
|
+
return JSON.parse(fs.readFileSync(primerPath, 'utf8'));
|
|
998
|
+
} catch (_) {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// ---------------------------------------------------------------------------
|
|
1004
|
+
// Multi-Hop Agentic Retrieval (Context-1 inspired)
|
|
1005
|
+
// ---------------------------------------------------------------------------
|
|
1006
|
+
|
|
1007
|
+
/** Default max retrieval hops before stopping. */
|
|
1008
|
+
const MAX_HOPS = 3;
|
|
1009
|
+
/** Minimum coverage score (0–1) to stop early. */
|
|
1010
|
+
const COVERAGE_THRESHOLD = 0.6;
|
|
1011
|
+
/** Minimum relevance score for an item to survive pruning. */
|
|
1012
|
+
const PRUNE_SCORE_FLOOR = 2;
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Compute query coverage: what fraction of query tokens appear in the
|
|
1016
|
+
* assembled context items. Returns 0–1.
|
|
1017
|
+
*/
|
|
1018
|
+
function computeCoverage(queryTokens, items) {
|
|
1019
|
+
if (queryTokens.length === 0) return 1;
|
|
1020
|
+
const contextText = items.map((i) =>
|
|
1021
|
+
`${i.title || ''} ${i.structuredContext ? i.structuredContext.rawContent || '' : ''} ${(i.tags || []).join(' ')}`
|
|
1022
|
+
).join(' ').toLowerCase();
|
|
1023
|
+
const contextTokens = new Set(contextText.split(/[^a-z0-9]+/).filter(Boolean));
|
|
1024
|
+
let covered = 0;
|
|
1025
|
+
for (const token of queryTokens) {
|
|
1026
|
+
if (token.length > 2 && contextTokens.has(token)) covered++;
|
|
1027
|
+
}
|
|
1028
|
+
return covered / queryTokens.length;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Prune weak chunks: re-score all items against query and drop any below
|
|
1033
|
+
* PRUNE_SCORE_FLOOR. Returns the survivors sorted by score descending.
|
|
1034
|
+
*/
|
|
1035
|
+
function pruneWeakChunks(items, queryTokens) {
|
|
1036
|
+
return items
|
|
1037
|
+
.map((item) => {
|
|
1038
|
+
const haystack = `${item.title || ''} ${item.structuredContext ? item.structuredContext.rawContent || '' : ''} ${(item.tags || []).join(' ')}`.toLowerCase();
|
|
1039
|
+
let score = 0;
|
|
1040
|
+
for (const token of queryTokens) {
|
|
1041
|
+
if (token.length > 2 && haystack.includes(token)) score += 3;
|
|
1042
|
+
}
|
|
1043
|
+
if (item.namespace && item.namespace.includes('memory/')) score += 1;
|
|
1044
|
+
return { item, score };
|
|
1045
|
+
})
|
|
1046
|
+
.filter((x) => x.score >= PRUNE_SCORE_FLOOR)
|
|
1047
|
+
.sort((a, b) => b.score - a.score)
|
|
1048
|
+
.map((x) => ({ ...x.item, score: x.score }));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Refine a query based on what's already been retrieved — extract tokens
|
|
1053
|
+
* from retrieved items that weren't in the original query (expansion),
|
|
1054
|
+
* then combine with original tokens for the next hop.
|
|
1055
|
+
*/
|
|
1056
|
+
function refineQuery(originalTokens, items) {
|
|
1057
|
+
const original = new Set(originalTokens);
|
|
1058
|
+
const expansion = new Set();
|
|
1059
|
+
for (const item of items) {
|
|
1060
|
+
const itemTokens = tokenizeQuery(
|
|
1061
|
+
`${item.title || ''} ${(item.tags || []).join(' ')}`
|
|
1062
|
+
);
|
|
1063
|
+
for (const t of itemTokens) {
|
|
1064
|
+
if (t.length > 3 && !original.has(t)) expansion.add(t);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
// Take top 3 expansion terms (by length, as a proxy for specificity)
|
|
1068
|
+
const sorted = [...expansion].sort((a, b) => b.length - a.length).slice(0, 3);
|
|
1069
|
+
return [...originalTokens, ...sorted];
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Multi-hop context pack construction. Iteratively retrieves, prunes weak
|
|
1074
|
+
* chunks, checks coverage, and refines the query until coverage is
|
|
1075
|
+
* sufficient or max hops are reached.
|
|
1076
|
+
*
|
|
1077
|
+
* @param {Object} opts
|
|
1078
|
+
* @param {string} opts.query - Search query
|
|
1079
|
+
* @param {number} [opts.maxItems=8] - Max items in final pack
|
|
1080
|
+
* @param {number} [opts.maxChars=6000] - Max chars budget
|
|
1081
|
+
* @param {string[]} [opts.namespaces=[]] - Namespaces to search
|
|
1082
|
+
* @param {number} [opts.maxHops=MAX_HOPS] - Max retrieval iterations
|
|
1083
|
+
* @returns {Object} Context pack with hop metadata
|
|
1084
|
+
*/
|
|
1085
|
+
function constructMultiHopPack({ query = '', maxItems = 8, maxChars = 6000, namespaces = [], maxHops = MAX_HOPS } = {}) {
|
|
1086
|
+
const normalizedNamespaces = normalizeNamespaces(namespaces);
|
|
1087
|
+
const originalTokens = tokenizeQuery(query);
|
|
1088
|
+
const sourceHash = getSourceHash(normalizedNamespaces);
|
|
1089
|
+
|
|
1090
|
+
// Check cache first (same as single-hop)
|
|
1091
|
+
const cacheHit = findSemanticCacheHit({ query, namespaces: normalizedNamespaces, maxItems, maxChars });
|
|
1092
|
+
if (cacheHit) {
|
|
1093
|
+
const packId = `mhop_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1094
|
+
return { ...cacheHit.entry.pack, packId, query, createdAt: nowIso(), cache: { hit: true, similarity: Number(cacheHit.score.toFixed(4)), sourcePackId: cacheHit.entry.pack.packId } };
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const allCandidates = loadCandidates(normalizedNamespaces);
|
|
1098
|
+
let currentTokens = [...originalTokens];
|
|
1099
|
+
let accumulatedItems = [];
|
|
1100
|
+
let usedChars = 0;
|
|
1101
|
+
const hopLog = [];
|
|
1102
|
+
const seenIds = new Set();
|
|
1103
|
+
|
|
1104
|
+
for (let hop = 0; hop < maxHops; hop++) {
|
|
1105
|
+
// Score candidates with current (possibly refined) query
|
|
1106
|
+
const scored = allCandidates
|
|
1107
|
+
.filter((doc) => !seenIds.has(doc.id))
|
|
1108
|
+
.map((doc) => ({ doc, score: scoreDocument(doc, currentTokens) }))
|
|
1109
|
+
.filter((x) => x.score > 0)
|
|
1110
|
+
.sort((a, b) => b.score - a.score);
|
|
1111
|
+
|
|
1112
|
+
// Take top candidates that fit within budget
|
|
1113
|
+
const hopItems = [];
|
|
1114
|
+
for (const { doc, score } of scored) {
|
|
1115
|
+
if (accumulatedItems.length + hopItems.length >= maxItems) break;
|
|
1116
|
+
const snippetLen = measureDocumentChars(doc);
|
|
1117
|
+
if (usedChars + snippetLen > maxChars) continue;
|
|
1118
|
+
|
|
1119
|
+
const item = {
|
|
1120
|
+
id: doc.id,
|
|
1121
|
+
namespace: doc.namespace,
|
|
1122
|
+
title: doc.title,
|
|
1123
|
+
structuredContext: buildStructuredContext(doc),
|
|
1124
|
+
tags: doc.tags || [],
|
|
1125
|
+
score,
|
|
1126
|
+
};
|
|
1127
|
+
hopItems.push(item);
|
|
1128
|
+
usedChars += snippetLen;
|
|
1129
|
+
seenIds.add(doc.id);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
accumulatedItems.push(...hopItems);
|
|
1133
|
+
|
|
1134
|
+
// Prune weak chunks from the accumulated set
|
|
1135
|
+
const beforePrune = accumulatedItems.length;
|
|
1136
|
+
accumulatedItems = pruneWeakChunks(accumulatedItems, originalTokens);
|
|
1137
|
+
const pruned = beforePrune - accumulatedItems.length;
|
|
1138
|
+
|
|
1139
|
+
// Recalculate usedChars after pruning
|
|
1140
|
+
usedChars = accumulatedItems.reduce((sum, item) => {
|
|
1141
|
+
const content = item.structuredContext ? item.structuredContext.rawContent || '' : '';
|
|
1142
|
+
return sum + `${item.title || ''}\n${content}`.length;
|
|
1143
|
+
}, 0);
|
|
1144
|
+
|
|
1145
|
+
// Check coverage
|
|
1146
|
+
const coverage = computeCoverage(originalTokens, accumulatedItems);
|
|
1147
|
+
|
|
1148
|
+
hopLog.push({
|
|
1149
|
+
hop: hop + 1,
|
|
1150
|
+
newItems: hopItems.length,
|
|
1151
|
+
prunedItems: pruned,
|
|
1152
|
+
totalItems: accumulatedItems.length,
|
|
1153
|
+
coverage: Number(coverage.toFixed(3)),
|
|
1154
|
+
queryTokenCount: currentTokens.length,
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
// Stop if coverage is good or we have enough items
|
|
1158
|
+
if (coverage >= COVERAGE_THRESHOLD || accumulatedItems.length >= maxItems) break;
|
|
1159
|
+
|
|
1160
|
+
// Refine query for next hop
|
|
1161
|
+
currentTokens = refineQuery(originalTokens, accumulatedItems);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const visibility = {
|
|
1165
|
+
itemCount: accumulatedItems.length,
|
|
1166
|
+
sourceCandidateCount: allCandidates.length,
|
|
1167
|
+
hiddenCount: Math.max(allCandidates.length - accumulatedItems.length, 0),
|
|
1168
|
+
maxItemsHit: accumulatedItems.length >= maxItems,
|
|
1169
|
+
maxCharsHit: usedChars >= maxChars,
|
|
1170
|
+
remainingCharBudget: Math.max(maxChars - usedChars, 0),
|
|
1171
|
+
visibleTitles: accumulatedItems.slice(0, 5).map((i) => i.title),
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
const packId = `mhop_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1175
|
+
const pack = {
|
|
1176
|
+
packId,
|
|
1177
|
+
query,
|
|
1178
|
+
maxItems,
|
|
1179
|
+
maxChars,
|
|
1180
|
+
usedChars,
|
|
1181
|
+
namespaces: normalizedNamespaces,
|
|
1182
|
+
createdAt: nowIso(),
|
|
1183
|
+
items: accumulatedItems,
|
|
1184
|
+
visibility,
|
|
1185
|
+
cache: { hit: false },
|
|
1186
|
+
sourceHash,
|
|
1187
|
+
retrieval: {
|
|
1188
|
+
strategy: 'multi-hop',
|
|
1189
|
+
hops: hopLog,
|
|
1190
|
+
totalHops: hopLog.length,
|
|
1191
|
+
finalCoverage: hopLog.length > 0 ? hopLog[hopLog.length - 1].coverage : 0,
|
|
1192
|
+
},
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
|
|
1196
|
+
appendSemanticCacheEntry({
|
|
1197
|
+
id: `cache_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1198
|
+
timestamp: nowIso(),
|
|
1199
|
+
key: buildSemanticCacheKey({ namespaces: normalizedNamespaces, maxItems, maxChars }),
|
|
1200
|
+
query,
|
|
1201
|
+
tokens: originalTokens,
|
|
1202
|
+
sourceHash,
|
|
1203
|
+
pack,
|
|
1204
|
+
});
|
|
1205
|
+
recordProvenance({
|
|
1206
|
+
type: 'multi_hop_pack_constructed',
|
|
1207
|
+
packId,
|
|
1208
|
+
query,
|
|
1209
|
+
itemCount: accumulatedItems.length,
|
|
1210
|
+
hops: hopLog.length,
|
|
1211
|
+
finalCoverage: pack.retrieval.finalCoverage,
|
|
1212
|
+
usedChars,
|
|
1213
|
+
sourceHash,
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
return pack;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function constructTemplatedPack({ template, query } = {}) {
|
|
1220
|
+
const config = PACK_TEMPLATES[template];
|
|
1221
|
+
if (!config) {
|
|
1222
|
+
throw new Error(`Unknown pack template: "${template}". Available: ${Object.keys(PACK_TEMPLATES).join(', ')}`);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const fullQuery = `${config.queryPrefix} ${query || ''}`.trim();
|
|
1226
|
+
const pack = constructContextPack({
|
|
1227
|
+
query: fullQuery,
|
|
1228
|
+
namespaces: config.namespaces,
|
|
1229
|
+
maxItems: config.maxItems,
|
|
1230
|
+
maxChars: config.maxChars,
|
|
1231
|
+
});
|
|
1232
|
+
pack.template = template;
|
|
1233
|
+
return pack;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function listPackTemplates() {
|
|
1237
|
+
return Object.entries(PACK_TEMPLATES).map(([name, config]) => ({
|
|
1238
|
+
name,
|
|
1239
|
+
namespaces: config.namespaces,
|
|
1240
|
+
maxItems: config.maxItems,
|
|
1241
|
+
maxChars: config.maxChars,
|
|
1242
|
+
queryPrefix: config.queryPrefix,
|
|
1243
|
+
}));
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
module.exports = {
|
|
1247
|
+
CONTEXTFS_ROOT,
|
|
1248
|
+
NAMESPACES,
|
|
1249
|
+
ensureContextFs,
|
|
1250
|
+
recordProvenance,
|
|
1251
|
+
writeContextObject,
|
|
1252
|
+
upsertContextObject,
|
|
1253
|
+
registerFeedback,
|
|
1254
|
+
registerPreventionRules,
|
|
1255
|
+
normalizeNamespaces,
|
|
1256
|
+
constructContextPack,
|
|
1257
|
+
evaluateContextPack,
|
|
1258
|
+
getProvenance,
|
|
1259
|
+
readJsonl,
|
|
1260
|
+
DEFAULT_SEARCH_NAMESPACES,
|
|
1261
|
+
tokenizeQuery,
|
|
1262
|
+
querySimilarity,
|
|
1263
|
+
findSemanticCacheHit,
|
|
1264
|
+
getSemanticCacheConfig,
|
|
1265
|
+
buildIndexEntry,
|
|
1266
|
+
loadMemexIndex,
|
|
1267
|
+
dereferenceEntry,
|
|
1268
|
+
searchMemexIndex,
|
|
1269
|
+
constructMemexPack,
|
|
1270
|
+
writeSessionHandoff,
|
|
1271
|
+
readSessionHandoff,
|
|
1272
|
+
PACK_TEMPLATES,
|
|
1273
|
+
constructTemplatedPack,
|
|
1274
|
+
listPackTemplates,
|
|
1275
|
+
constructMultiHopPack,
|
|
1276
|
+
computeCoverage,
|
|
1277
|
+
pruneWeakChunks,
|
|
1278
|
+
refineQuery,
|
|
1279
|
+
MAX_HOPS,
|
|
1280
|
+
COVERAGE_THRESHOLD,
|
|
1281
|
+
PRUNE_SCORE_FLOOR,
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
if (require.main === module) {
|
|
1285
|
+
ensureContextFs();
|
|
1286
|
+
console.log(`ContextFS ready at ${CONTEXTFS_ROOT}`);
|
|
1287
|
+
}
|