@causyne/agent 0.0.0-stage → 1.10.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/LICENSE +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- package/src/vfeTracker.js +220 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { cortexDb } from './vectorDb.js';
|
|
2
|
+
|
|
3
|
+
// Diagnostics, off by default: console output during a turn overwrites the
|
|
4
|
+
// prompt's live region. See src/cortex/provisioner.js for the full reason.
|
|
5
|
+
function debugLog(message) {
|
|
6
|
+
if (process.env.CAUSYNE_DEBUG !== '1') return;
|
|
7
|
+
process.stderr.write(`[causyne] ${message}\n`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
// V5 Cortex — Tiered Auto-Condensation
|
|
12
|
+
//
|
|
13
|
+
// When the sliding window amputates older messages, that knowledge must not
|
|
14
|
+
// simply evaporate: continuity across sessions is what turns a stateless text
|
|
15
|
+
// predictor into something with a useful retained history.
|
|
16
|
+
//
|
|
17
|
+
// The original implementation fired an untracked LLM call on the user's own
|
|
18
|
+
// BYOK key at every single amputation, feeding it the entire dropped chunk —
|
|
19
|
+
// which on a saturated 400,000-character window could cost more than the
|
|
20
|
+
|
|
21
|
+
// displayed. The answer is not to switch memory formation off; it is to make it
|
|
22
|
+
// tiered:
|
|
23
|
+
//
|
|
24
|
+
// Tier 1 (always, free) — a deterministic structural summary is written to
|
|
25
|
+
// the Cortex with no LLM involved. The brain always
|
|
26
|
+
// learns something, on every provider, at zero cost.
|
|
27
|
+
// Tier 2 (gated, billed) — semantic LLM condensation, but only when the
|
|
28
|
+
// dropped chunk is materially large, capped per task,
|
|
29
|
+
// with bounded input and fully tracked usage.
|
|
30
|
+
|
|
31
|
+
const MAX_CONDENSE_CHARS = 24000;
|
|
32
|
+
const MIN_CHARS_FOR_LLM_PASS = 6000;
|
|
33
|
+
const DEFAULT_MAX_LLM_PASSES_PER_TASK = 3;
|
|
34
|
+
|
|
35
|
+
function serialiseContent(content) {
|
|
36
|
+
// An assistant message that only carries tool_calls has no content at all,
|
|
37
|
+
// and JSON.stringify(undefined) returns undefined rather than a string.
|
|
38
|
+
if (content == null) return '';
|
|
39
|
+
return typeof content === 'string' ? content : (JSON.stringify(content) || '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tier 1: structural facts extracted without any model call.
|
|
44
|
+
* Records what was attempted, which files were touched and what failed —
|
|
45
|
+
* exactly the things the agent asks itself again ten steps later.
|
|
46
|
+
*/
|
|
47
|
+
function buildStructuralSummary(droppedMessages) {
|
|
48
|
+
const toolCounts = new Map();
|
|
49
|
+
const touchedPaths = new Set();
|
|
50
|
+
const failures = [];
|
|
51
|
+
const userTurns = [];
|
|
52
|
+
|
|
53
|
+
for (const msg of droppedMessages) {
|
|
54
|
+
const content = serialiseContent(msg.content);
|
|
55
|
+
|
|
56
|
+
if (msg.role === 'user' && typeof msg.content === 'string' && msg.content.length < 600) {
|
|
57
|
+
userTurns.push(msg.content.trim());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (msg.role === 'tool') {
|
|
61
|
+
if (msg.name) toolCounts.set(msg.name, (toolCounts.get(msg.name) || 0) + 1);
|
|
62
|
+
|
|
63
|
+
// Absolute paths are the highest-value breadcrumb in a dropped chunk.
|
|
64
|
+
const paths = content.match(/\/[\w.\-/]+\.[a-zA-Z0-9]{1,6}/g);
|
|
65
|
+
if (paths) for (const p of paths.slice(0, 6)) touchedPaths.add(p);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(content);
|
|
69
|
+
if (parsed && parsed.success === false && parsed.error) {
|
|
70
|
+
failures.push(`${msg.name || 'tool'}: ${String(parsed.error).slice(0, 140)}`);
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// Non-JSON tool output carries no structured verdict; skip it.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (userTurns.length > 0) {
|
|
80
|
+
parts.push(`Requests in this segment: ${userTurns.slice(0, 3).join(' | ')}`);
|
|
81
|
+
}
|
|
82
|
+
if (toolCounts.size > 0) {
|
|
83
|
+
const tools = [...toolCounts.entries()]
|
|
84
|
+
.sort((a, b) => b[1] - a[1])
|
|
85
|
+
.slice(0, 10)
|
|
86
|
+
.map(([name, count]) => `${name}x${count}`)
|
|
87
|
+
.join(', ');
|
|
88
|
+
parts.push(`Tools used: ${tools}`);
|
|
89
|
+
}
|
|
90
|
+
if (touchedPaths.size > 0) {
|
|
91
|
+
parts.push(`Files touched: ${[...touchedPaths].slice(0, 12).join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
if (failures.length > 0) {
|
|
94
|
+
parts.push(`Failures encountered: ${failures.slice(0, 4).join(' | ')}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return parts.length > 0 ? parts.join('. ') : '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {Array} droppedMessages Messages leaving the active window.
|
|
102
|
+
* @param {object} llm Provider used for the optional Tier 2 pass.
|
|
103
|
+
* @param {object} tokenTracker Required for Tier 2 so usage is never hidden.
|
|
104
|
+
* @param {object} options { maxLlmPasses, passesUsed, enableLlmPass }
|
|
105
|
+
* @returns {Promise<{ structural: boolean, semantic: boolean }>}
|
|
106
|
+
*/
|
|
107
|
+
export async function condenseAndSave(droppedMessages, llm, tokenTracker = null, options = {}) {
|
|
108
|
+
const result = { structural: false, semantic: false };
|
|
109
|
+
if (!droppedMessages || droppedMessages.length === 0) return result;
|
|
110
|
+
|
|
111
|
+
// ---- Tier 1: always-on, zero cost -------------------------------------
|
|
112
|
+
try {
|
|
113
|
+
const structural = buildStructuralSummary(droppedMessages);
|
|
114
|
+
if (structural) {
|
|
115
|
+
await cortexDb.saveCondensedMemory(structural);
|
|
116
|
+
result.structural = true;
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
debugLog(`[Auto-Condenser] Structural condensation failed: ${e.message}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- Tier 2: gated semantic pass --------------------------------------
|
|
123
|
+
if (!llm || options.enableLlmPass === false) return result;
|
|
124
|
+
|
|
125
|
+
const maxPasses = Number.isInteger(options.maxLlmPasses)
|
|
126
|
+
? options.maxLlmPasses
|
|
127
|
+
: DEFAULT_MAX_LLM_PASSES_PER_TASK;
|
|
128
|
+
if ((options.passesUsed || 0) >= maxPasses) return result;
|
|
129
|
+
|
|
130
|
+
let conversationText = droppedMessages.map(m => {
|
|
131
|
+
let content = serialiseContent(m.content);
|
|
132
|
+
// Tool payloads dominate the volume and are the least useful thing to
|
|
133
|
+
// summarise verbatim.
|
|
134
|
+
if (content.length > 2000) content = content.slice(0, 2000) + ' …[truncated]';
|
|
135
|
+
return `${m.role.toUpperCase()}: ${content}`;
|
|
136
|
+
}).join('\n\n');
|
|
137
|
+
|
|
138
|
+
// A trivial chunk is not worth a model call; Tier 1 already captured it.
|
|
139
|
+
if (conversationText.length < MIN_CHARS_FOR_LLM_PASS) return result;
|
|
140
|
+
|
|
141
|
+
if (conversationText.length > MAX_CONDENSE_CHARS) {
|
|
142
|
+
conversationText = conversationText.slice(-MAX_CONDENSE_CHARS);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const prompt = `You are an AI Memory Condenser for an autonomous agent.
|
|
147
|
+
Below is a chunk of an older conversation between the User and the Assistant that is being pushed out of the short-term memory window due to length constraints.
|
|
148
|
+
Your task is to summarize the KEY FACTS, DECISIONS, or OUTCOMES from this conversation snippet into 1-3 concise bullet points.
|
|
149
|
+
- Do NOT include pleasantries, filler, or transient errors that were immediately fixed.
|
|
150
|
+
- Only include structural knowledge, architectural decisions, and final outcomes that the agent might need later.
|
|
151
|
+
- If there is nothing permanently useful in this chunk, reply with exactly: SKIP
|
|
152
|
+
|
|
153
|
+
Conversation Snippet:
|
|
154
|
+
${conversationText}
|
|
155
|
+
|
|
156
|
+
Provide only the bullet points.`;
|
|
157
|
+
|
|
158
|
+
// maxTokens capped deliberately. The prompt asks for one to three bullet
|
|
159
|
+
// points; without a ceiling a model that misreads the instruction can
|
|
160
|
+
// return a full re-summary of the transcript it was given, at completion
|
|
161
|
+
// rates. Module 4 stage 1 of the scaling plan: cap the output of anything
|
|
162
|
+
// whose shape you already know.
|
|
163
|
+
const response = await llm.chat([{ role: 'user', content: prompt }], {
|
|
164
|
+
temperature: 0.1,
|
|
165
|
+
maxTokens: 400
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// This call spends the user's own BYOK credits. It used to bypass the
|
|
169
|
+
|
|
170
|
+
// real bill.
|
|
171
|
+
if (tokenTracker && response && response.usage) {
|
|
172
|
+
await tokenTracker.trackUsage(
|
|
173
|
+
response.usage.promptTokens || 0,
|
|
174
|
+
response.usage.completionTokens || 0,
|
|
175
|
+
null,
|
|
176
|
+
response.usage
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (response && response.content) {
|
|
181
|
+
const summary = response.content.trim();
|
|
182
|
+
if (summary && summary !== 'SKIP' && summary.length > 10) {
|
|
183
|
+
await cortexDb.saveCondensedMemory(summary);
|
|
184
|
+
result.semantic = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
debugLog(`[Auto-Condenser] Semantic condensation failed: ${e.message}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { causyneHome } from '../utils/paths.js';
|
|
6
|
+
|
|
7
|
+
const CORTEX_DIR = path.join(causyneHome(), 'cortex');
|
|
8
|
+
|
|
9
|
+
// Read-only check. The legacy implementation silently ran npm during memory
|
|
10
|
+
// initialization. Optional native packages now require explicit manual setup.
|
|
11
|
+
export async function provisionCortexDependencies() {
|
|
12
|
+
for (const name of ['better-sqlite3', '@xenova/transformers']) {
|
|
13
|
+
if (!fs.existsSync(path.join(CORTEX_DIR, 'node_modules', name))) {
|
|
14
|
+
throw new Error('Optional Cortex dependencies are not installed. Automatic installation is disabled; see docs/OPTIONAL_CAPABILITIES.md. Standard local session memory remains available.');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function requireCortexModule(moduleName) {
|
|
20
|
+
if (!['better-sqlite3', '@xenova/transformers'].includes(moduleName)) {
|
|
21
|
+
throw new Error(`Unsupported Cortex module: ${moduleName}`);
|
|
22
|
+
}
|
|
23
|
+
const require = createRequire(path.join(CORTEX_DIR, 'package.json'));
|
|
24
|
+
const resolved = require.resolve(moduleName);
|
|
25
|
+
const mod = await import(pathToFileURL(resolved).href);
|
|
26
|
+
return mod.default || mod;
|
|
27
|
+
}
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { requireCortexModule, provisionCortexDependencies } from './provisioner.js';
|
|
6
|
+
import { causyneHome } from '../utils/paths.js';
|
|
7
|
+
|
|
8
|
+
// Diagnostics, off by default: console output during a turn overwrites the
|
|
9
|
+
// prompt's live region. See src/cortex/provisioner.js for the full reason.
|
|
10
|
+
function debugLog(message) {
|
|
11
|
+
if (process.env.CAUSYNE_DEBUG !== '1') return;
|
|
12
|
+
process.stderr.write(`[causyne] ${message}\n`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// We will load these dynamically after provisioning
|
|
17
|
+
let Database;
|
|
18
|
+
let pipeline;
|
|
19
|
+
let env;
|
|
20
|
+
|
|
21
|
+
// Environment configuration is now done dynamically in init()
|
|
22
|
+
|
|
23
|
+
const CORTEX_DIR = causyneHome();
|
|
24
|
+
const CORTEX_DB_FILE = path.join(CORTEX_DIR, 'cortex.db');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How long a memory keeps half its weight. Thirty days.
|
|
28
|
+
*
|
|
29
|
+
* A lesson from last week is usually about the code as it is now; one from
|
|
30
|
+
* March is usually about code that no longer exists. Decay does not delete it —
|
|
31
|
+
* it just stops an ancient memory outranking a recent one on a tie.
|
|
32
|
+
*/
|
|
33
|
+
const RECENCY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* OBLIVION adaptive memory stability constants:
|
|
37
|
+
* S = BASE_HALF_LIFE * (1 + hits * REINFORCEMENT_ALPHA) * tierMultiplier
|
|
38
|
+
* R = e^(-t/S) — adaptive retention that rewards retrieved memories with slower decay
|
|
39
|
+
*/
|
|
40
|
+
const REINFORCEMENT_ALPHA = 0.2;
|
|
41
|
+
|
|
42
|
+
const TIER_MULTIPLIERS = {
|
|
43
|
+
condensed: 2.0, // L1: Topic summaries persist longer
|
|
44
|
+
creative: 1.5, // L2: Creative successes are valuable
|
|
45
|
+
mistake: 1.0 // L3: Mistakes decay normally unless reinforced
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Similarity below this is noise, whatever the recency. */
|
|
49
|
+
const MIN_SIMILARITY = 0.5;
|
|
50
|
+
|
|
51
|
+
/** Rows scored per search. Beyond this the newest win, so search stays O(1)-ish. */
|
|
52
|
+
const MAX_CANDIDATES = 500;
|
|
53
|
+
|
|
54
|
+
/** Ceiling on stored memories per workspace before the oldest are retired. */
|
|
55
|
+
const MAX_MEMORIES_PER_WORKSPACE = 2000;
|
|
56
|
+
|
|
57
|
+
/** The dedup key: same lesson, same row, however many times it is learned. */
|
|
58
|
+
function fingerprintOf(text) {
|
|
59
|
+
const normalised = String(text || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
60
|
+
return crypto.createHash('sha256').update(normalised).digest('hex').slice(0, 32);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Type as a COLUMN, not as a prefix a LIKE has to scan for. */
|
|
64
|
+
function kindOf(text) {
|
|
65
|
+
const value = String(text || '');
|
|
66
|
+
if (value.startsWith('CREATIVE SUCCESS:')) return 'creative';
|
|
67
|
+
if (value.startsWith('[PAST MISTAKE')) return 'mistake';
|
|
68
|
+
return 'condensed';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class VectorDB {
|
|
72
|
+
constructor() {
|
|
73
|
+
this.extractor = null;
|
|
74
|
+
this.isInitialized = false;
|
|
75
|
+
this.db = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async ensureDb() {
|
|
79
|
+
if (this.db) return;
|
|
80
|
+
|
|
81
|
+
if (!fs.existsSync(CORTEX_DIR)) {
|
|
82
|
+
fs.mkdirSync(CORTEX_DIR, { recursive: true, mode: 0o700 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Ensure dependencies are provisioned first
|
|
86
|
+
await provisionCortexDependencies();
|
|
87
|
+
|
|
88
|
+
if (!Database) {
|
|
89
|
+
Database = await requireCortexModule('better-sqlite3');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Initialize SQLite Database
|
|
93
|
+
this.db = new Database(CORTEX_DB_FILE);
|
|
94
|
+
|
|
95
|
+
// Create the task_memories table if it doesn't exist
|
|
96
|
+
// embedding_vector is stored as a JSON string array of floats
|
|
97
|
+
this.db.exec(`
|
|
98
|
+
CREATE TABLE IF NOT EXISTS task_memories (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
text TEXT NOT NULL,
|
|
101
|
+
embedding_vector TEXT NOT NULL,
|
|
102
|
+
timestamp INTEGER NOT NULL
|
|
103
|
+
)
|
|
104
|
+
`);
|
|
105
|
+
|
|
106
|
+
this.migrateSchema();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Bring an existing store up to the current schema, additively.
|
|
111
|
+
*
|
|
112
|
+
* WHAT THE ORIGINAL SCHEMA COULD NOT DO, measured on a real store of 11 rows:
|
|
113
|
+
*
|
|
114
|
+
* - NO WORKSPACE. search() scanned every memory ever recorded, so a lesson
|
|
115
|
+
* learned in one client's repository was injected into another client's
|
|
116
|
+
* task. That is a relevance problem and a CONFIDENTIALITY problem, and it
|
|
117
|
+
* is the same class of bug that made one global chat_history.json resume
|
|
118
|
+
* the wrong project's conversation.
|
|
119
|
+
* - NO DEDUPLICATION. 11 rows held 3 distinct lessons — 73% duplicates. A
|
|
120
|
+
* top-3 search could return the same lesson three times, so the agent paid
|
|
121
|
+
* for three slots and received one.
|
|
122
|
+
* - TIMESTAMP STORED, NEVER USED. A memory from months ago ranked exactly
|
|
123
|
+
* as high as one from yesterday.
|
|
124
|
+
* - TYPE BY STRING PREFIX. Creative patterns were told apart from mistakes
|
|
125
|
+
* with `LIKE 'CREATIVE SUCCESS:%'` — a full table scan against a
|
|
126
|
+
* convention nothing enforced.
|
|
127
|
+
* - NO INDEX beyond the primary key.
|
|
128
|
+
*
|
|
129
|
+
* ALTER TABLE ADD COLUMN is the only migration used: it cannot lose a row,
|
|
130
|
+
* and an interrupted upgrade leaves a store that still reads.
|
|
131
|
+
*/
|
|
132
|
+
migrateSchema() {
|
|
133
|
+
const columns = new Set(this.db.prepare('PRAGMA table_info(task_memories)').all().map(c => c.name));
|
|
134
|
+
|
|
135
|
+
if (!columns.has('workspace')) this.db.exec("ALTER TABLE task_memories ADD COLUMN workspace TEXT DEFAULT ''");
|
|
136
|
+
if (!columns.has('kind')) this.db.exec("ALTER TABLE task_memories ADD COLUMN kind TEXT DEFAULT 'mistake'");
|
|
137
|
+
if (!columns.has('fingerprint')) this.db.exec('ALTER TABLE task_memories ADD COLUMN fingerprint TEXT');
|
|
138
|
+
if (!columns.has('hits')) this.db.exec('ALTER TABLE task_memories ADD COLUMN hits INTEGER DEFAULT 0');
|
|
139
|
+
|
|
140
|
+
// Backfill the two derived columns for rows written before they existed.
|
|
141
|
+
// Existing memories keep workspace '' — global — because we genuinely do
|
|
142
|
+
// not know where they came from, and guessing would attach one project's
|
|
143
|
+
// lesson to another. '' matches every workspace, which is the old
|
|
144
|
+
// behaviour, so nothing a user already relies on disappears.
|
|
145
|
+
const stale = this.db.prepare('SELECT id, text FROM task_memories WHERE fingerprint IS NULL').all();
|
|
146
|
+
if (stale.length > 0) {
|
|
147
|
+
const setRow = this.db.prepare('UPDATE task_memories SET fingerprint = ?, kind = ? WHERE id = ?');
|
|
148
|
+
const run = this.db.transaction((rows) => {
|
|
149
|
+
for (const row of rows) setRow.run(fingerprintOf(row.text), kindOf(row.text), row.id);
|
|
150
|
+
});
|
|
151
|
+
run(stale);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ONE-TIME COLLAPSE of duplicates written before fingerprints existed.
|
|
155
|
+
//
|
|
156
|
+
// The real store held 11 rows carrying 3 distinct lessons — five copies
|
|
157
|
+
// each of two of them. That is not merely wasted disk: a top-3 retrieval
|
|
158
|
+
// returned the same lesson three times, so the agent paid for three slots
|
|
159
|
+
// and learned one thing. The newest row of each group survives and inherits
|
|
160
|
+
// the group's total hit count, because reinforcement is real evidence that
|
|
161
|
+
// the lesson matters.
|
|
162
|
+
const groups = this.db.prepare(
|
|
163
|
+
`SELECT fingerprint, workspace, COUNT(*) c FROM task_memories
|
|
164
|
+
WHERE fingerprint IS NOT NULL
|
|
165
|
+
GROUP BY fingerprint, workspace HAVING c > 1`
|
|
166
|
+
).all();
|
|
167
|
+
|
|
168
|
+
if (groups.length > 0) {
|
|
169
|
+
const collapse = this.db.transaction((rows) => {
|
|
170
|
+
for (const group of rows) {
|
|
171
|
+
const copies = this.db.prepare(
|
|
172
|
+
`SELECT id, hits FROM task_memories
|
|
173
|
+
WHERE fingerprint = ? AND workspace = ? ORDER BY timestamp DESC`
|
|
174
|
+
).all(group.fingerprint, group.workspace);
|
|
175
|
+
|
|
176
|
+
const keep = copies[0];
|
|
177
|
+
const totalHits = copies.reduce((sum, row) => sum + (row.hits || 0), 0);
|
|
178
|
+
this.db.prepare('UPDATE task_memories SET hits = ? WHERE id = ?').run(totalHits, keep.id);
|
|
179
|
+
for (const row of copies.slice(1)) {
|
|
180
|
+
this.db.prepare('DELETE FROM task_memories WHERE id = ?').run(row.id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
collapse(groups);
|
|
185
|
+
debugLog(`[CORTEX] collapsed ${groups.length} duplicate group(s)`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_memories_ws_time ON task_memories(workspace, timestamp DESC)');
|
|
189
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_memories_kind ON task_memories(kind, timestamp DESC)');
|
|
190
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_memories_fingerprint ON task_memories(fingerprint, workspace)');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async init() {
|
|
194
|
+
if (this.isInitialized) return;
|
|
195
|
+
|
|
196
|
+
await this.ensureDb();
|
|
197
|
+
|
|
198
|
+
if (!pipeline || !env) {
|
|
199
|
+
const transformers = await requireCortexModule('@xenova/transformers');
|
|
200
|
+
pipeline = transformers.pipeline;
|
|
201
|
+
env = transformers.env;
|
|
202
|
+
|
|
203
|
+
// Optimization: Allow local model caching
|
|
204
|
+
env.localModelPath = path.join(causyneHome(), 'models');
|
|
205
|
+
env.allowRemoteModels = false; // Model provisioning is an explicit setup step.
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Load the lightweight feature extraction pipeline
|
|
209
|
+
try {
|
|
210
|
+
this.extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
|
|
211
|
+
this.isInitialized = true;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
debugLog(`[CORTEX] Failed to initialize neural engine: ${error.message}`);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async embed(text) {
|
|
219
|
+
if (!this.isInitialized) await this.init();
|
|
220
|
+
|
|
221
|
+
// Generate embeddings
|
|
222
|
+
const output = await this.extractor(text, { pooling: 'mean', normalize: true });
|
|
223
|
+
return Array.from(output.data);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
cosineSimilarity(vecA, vecB) {
|
|
227
|
+
let dotProduct = 0;
|
|
228
|
+
let normA = 0;
|
|
229
|
+
let normB = 0;
|
|
230
|
+
for (let i = 0; i < vecA.length; i++) {
|
|
231
|
+
dotProduct += vecA[i] * vecB[i];
|
|
232
|
+
normA += vecA[i] * vecA[i];
|
|
233
|
+
normB += vecB[i] * vecB[i];
|
|
234
|
+
}
|
|
235
|
+
if (normA === 0 || normB === 0) return 0;
|
|
236
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A mistake and how it was recovered from. Scoped to the project it happened in.
|
|
241
|
+
*
|
|
242
|
+
* `workspace` defaults to the current directory rather than to '' — a lesson
|
|
243
|
+
* about THIS repository's build is not a lesson about every repository, and
|
|
244
|
+
* cross-project leakage is the whole reason retrieval had to be scoped.
|
|
245
|
+
*/
|
|
246
|
+
async saveMemory(taskGoal, errorEncountered, resolution, workspace = process.cwd()) {
|
|
247
|
+
const searchableText = `Goal: ${taskGoal}\nError: ${errorEncountered}`;
|
|
248
|
+
const fullText = `[PAST MISTAKE RECOVERED]\nGoal: ${taskGoal}\nError: ${errorEncountered}\nResolution: ${resolution}`;
|
|
249
|
+
const result = await this.upsertMemory(fullText, { workspace, kind: 'mistake', searchableText });
|
|
250
|
+
debugLog(`[CORTEX] mistake ${result.created ? 'recorded' : 'reinforced'}`);
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** A condensed run of chat history, kept against the project it summarises. */
|
|
255
|
+
async saveCondensedMemory(summaryText, workspace = process.cwd()) {
|
|
256
|
+
const fullText = `[CONDENSED CHAT HISTORY]\n${summaryText}`;
|
|
257
|
+
return this.upsertMemory(fullText, { workspace, kind: 'condensed' });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async search(queryText, topK = 3, options = {}) {
|
|
261
|
+
await this.ensureDb();
|
|
262
|
+
|
|
263
|
+
const workspace = String(options.workspace ?? process.cwd() ?? '');
|
|
264
|
+
const rows = this.db.prepare(
|
|
265
|
+
`SELECT id, text, embedding_vector, timestamp, kind, hits
|
|
266
|
+
FROM task_memories
|
|
267
|
+
WHERE workspace = ? OR workspace = ''
|
|
268
|
+
ORDER BY timestamp DESC
|
|
269
|
+
LIMIT ?`
|
|
270
|
+
).all(workspace, MAX_CANDIDATES);
|
|
271
|
+
|
|
272
|
+
if (rows.length === 0) return [];
|
|
273
|
+
|
|
274
|
+
const queryEmbedding = await this.embed(queryText);
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
|
|
277
|
+
const scored = [];
|
|
278
|
+
for (const memory of rows) {
|
|
279
|
+
let similarity;
|
|
280
|
+
try {
|
|
281
|
+
similarity = this.cosineSimilarity(queryEmbedding, JSON.parse(memory.embedding_vector));
|
|
282
|
+
} catch (e) {
|
|
283
|
+
continue; // a corrupt vector is skipped, never fatal
|
|
284
|
+
}
|
|
285
|
+
if (similarity <= MIN_SIMILARITY) continue;
|
|
286
|
+
|
|
287
|
+
const ageMs = Math.max(0, now - (memory.timestamp || 0));
|
|
288
|
+
// OBLIVION: S = BASE_HALF_LIFE * (1 + hits * REINFORCEMENT_ALPHA) * tierMultiplier
|
|
289
|
+
// R = e^(-t/S) — adaptive retention that rewards reinforced memories with slower decay
|
|
290
|
+
const tierMult = TIER_MULTIPLIERS[memory.kind] || 1.0;
|
|
291
|
+
const stability = Math.max(1, RECENCY_HALF_LIFE_MS * (1 + (memory.hits || 0) * REINFORCEMENT_ALPHA) * tierMult);
|
|
292
|
+
const retention = Math.exp(-ageMs / stability);
|
|
293
|
+
scored.push({
|
|
294
|
+
id: memory.id,
|
|
295
|
+
text: memory.text,
|
|
296
|
+
kind: memory.kind,
|
|
297
|
+
timestamp: memory.timestamp,
|
|
298
|
+
hits: memory.hits || 0,
|
|
299
|
+
similarity,
|
|
300
|
+
retention,
|
|
301
|
+
score: similarity * retention
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
scored.sort((a, b) => b.score - a.score);
|
|
306
|
+
const matches = scored.slice(0, topK);
|
|
307
|
+
|
|
308
|
+
// Reinforce retrieved memories: incrementing hits increases memory stability S
|
|
309
|
+
// in OBLIVION formula, preserving high-utility memories against decay.
|
|
310
|
+
if (matches.length > 0) {
|
|
311
|
+
try {
|
|
312
|
+
const bump = this.db.prepare('UPDATE task_memories SET hits = hits + 1 WHERE id = ?');
|
|
313
|
+
const run = this.db.transaction((ids) => { for (const id of ids) bump.run(id); });
|
|
314
|
+
run(matches.map(m => m.id));
|
|
315
|
+
} catch (e) { /* telemetry must never fail a retrieval */ }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return matches;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Write a memory, or refresh the one that already says this.
|
|
323
|
+
*
|
|
324
|
+
* A store of 11 rows held 3 distinct lessons. Recording the same lesson again
|
|
325
|
+
* is not new knowledge — it is evidence that the lesson matters, so the
|
|
326
|
+
* existing row's timestamp and hit count move instead of a duplicate landing
|
|
327
|
+
* next to it. Duplicates do not merely waste space: they crowd each other out
|
|
328
|
+
* of a top-3, so the agent pays for three slots and receives one lesson.
|
|
329
|
+
*/
|
|
330
|
+
async upsertMemory(text, { workspace = '', kind = null, searchableText = null } = {}) {
|
|
331
|
+
await this.ensureDb();
|
|
332
|
+
|
|
333
|
+
const fingerprint = fingerprintOf(text);
|
|
334
|
+
const scope = String(workspace || '');
|
|
335
|
+
|
|
336
|
+
const existing = this.db.prepare(
|
|
337
|
+
'SELECT id FROM task_memories WHERE fingerprint = ? AND workspace = ?'
|
|
338
|
+
).get(fingerprint, scope);
|
|
339
|
+
|
|
340
|
+
if (existing) {
|
|
341
|
+
this.db.prepare('UPDATE task_memories SET timestamp = ?, hits = hits + 1 WHERE id = ?')
|
|
342
|
+
.run(Date.now(), existing.id);
|
|
343
|
+
debugLog(`[CORTEX] memory reinforced rather than duplicated: ${fingerprint.slice(0, 8)}`);
|
|
344
|
+
return { id: existing.id, created: false };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const embedding = await this.embed(searchableText || text);
|
|
348
|
+
const id = `mem_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`;
|
|
349
|
+
|
|
350
|
+
this.db.prepare(
|
|
351
|
+
`INSERT INTO task_memories (id, text, embedding_vector, timestamp, workspace, kind, fingerprint, hits)
|
|
352
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0)`
|
|
353
|
+
).run(id, text, JSON.stringify(embedding), Date.now(), scope, kind || kindOf(text), fingerprint);
|
|
354
|
+
|
|
355
|
+
this.pruneWorkspace(scope);
|
|
356
|
+
return { id, created: true };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Retire the least useful memories once a workspace passes its ceiling.
|
|
361
|
+
*
|
|
362
|
+
* Oldest first, and never-retrieved before ever-retrieved: a memory the agent
|
|
363
|
+
* has actually used is worth more than one that has sat there since August.
|
|
364
|
+
* Called on write rather than on a timer — nothing may run in the background
|
|
365
|
+
* (ZM-01), and a write is exactly when the store can have grown.
|
|
366
|
+
*/
|
|
367
|
+
pruneWorkspace(workspace, max = MAX_MEMORIES_PER_WORKSPACE) {
|
|
368
|
+
try {
|
|
369
|
+
const { c } = this.db.prepare('SELECT COUNT(*) c FROM task_memories WHERE workspace = ?').get(workspace);
|
|
370
|
+
if (c <= max) return 0;
|
|
371
|
+
const result = this.db.prepare(
|
|
372
|
+
`DELETE FROM task_memories WHERE id IN (
|
|
373
|
+
SELECT id FROM task_memories WHERE workspace = ?
|
|
374
|
+
ORDER BY hits ASC, timestamp ASC LIMIT ?
|
|
375
|
+
)`
|
|
376
|
+
).run(workspace, c - max);
|
|
377
|
+
return result.changes;
|
|
378
|
+
} catch (e) {
|
|
379
|
+
return 0;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Everything stored, for `/cortex`. Vectors excluded — they are unreadable. */
|
|
384
|
+
async inventory(options = {}) {
|
|
385
|
+
await this.ensureDb();
|
|
386
|
+
const workspace = options.workspace;
|
|
387
|
+
const rows = workspace
|
|
388
|
+
? this.db.prepare(
|
|
389
|
+
`SELECT id, text, kind, timestamp, hits, workspace FROM task_memories
|
|
390
|
+
WHERE workspace = ? OR workspace = '' ORDER BY timestamp DESC LIMIT 200`
|
|
391
|
+
).all(String(workspace))
|
|
392
|
+
: this.db.prepare(
|
|
393
|
+
'SELECT id, text, kind, timestamp, hits, workspace FROM task_memories ORDER BY timestamp DESC LIMIT 200'
|
|
394
|
+
).all();
|
|
395
|
+
return rows;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Counts, for a one-line answer to "is my digital brain actually on?". */
|
|
399
|
+
async stats() {
|
|
400
|
+
await this.ensureDb();
|
|
401
|
+
const total = this.db.prepare('SELECT COUNT(*) c FROM task_memories').get().c;
|
|
402
|
+
const byKind = this.db.prepare('SELECT kind, COUNT(*) c FROM task_memories GROUP BY kind').all();
|
|
403
|
+
const workspaces = this.db.prepare("SELECT COUNT(DISTINCT workspace) c FROM task_memories WHERE workspace != ''").get().c;
|
|
404
|
+
const newest = this.db.prepare('SELECT MAX(timestamp) t FROM task_memories').get().t || 0;
|
|
405
|
+
const distinct = this.db.prepare('SELECT COUNT(DISTINCT fingerprint) c FROM task_memories').get().c;
|
|
406
|
+
return { total, distinct, byKind, workspaces, newest };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Forget one memory, or every memory in a workspace. */
|
|
410
|
+
async forget({ id = null, workspace = null } = {}) {
|
|
411
|
+
await this.ensureDb();
|
|
412
|
+
if (id) return this.db.prepare('DELETE FROM task_memories WHERE id = ?').run(id).changes;
|
|
413
|
+
if (workspace !== null) {
|
|
414
|
+
return this.db.prepare('DELETE FROM task_memories WHERE workspace = ?').run(String(workspace)).changes;
|
|
415
|
+
}
|
|
416
|
+
return 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* A design approach that worked, so the next one deliberately differs.
|
|
421
|
+
*
|
|
422
|
+
* Global on purpose (workspace ''): the anti-slop directive is about not
|
|
423
|
+
* repeating YOURSELF, and repeating the same palette in a different repo is
|
|
424
|
+
* still repeating yourself.
|
|
425
|
+
*/
|
|
426
|
+
async saveCreativePattern(pattern) {
|
|
427
|
+
const text = `CREATIVE SUCCESS: Built "${pattern.project}" using ${pattern.designApproach} style. Colors: ${pattern.colorPalette}. Fonts: ${pattern.fonts}.`;
|
|
428
|
+
const result = await this.upsertMemory(text, { workspace: '', kind: 'creative' });
|
|
429
|
+
debugLog(`[CORTEX] creative pattern ${result.created ? 'saved' : 'reinforced'}: ${pattern.designApproach}`);
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Anti-Slop: Retrieve recent creative patterns for the "DO NOT REPEAT" directive
|
|
434
|
+
async getRecentCreativePatterns(limit = 5) {
|
|
435
|
+
await this.ensureDb();
|
|
436
|
+
try {
|
|
437
|
+
const results = this.db.prepare(
|
|
438
|
+
'SELECT text FROM task_memories WHERE kind = ? ORDER BY timestamp DESC LIMIT ?'
|
|
439
|
+
).all('creative', limit);
|
|
440
|
+
return results.map(r => r.text);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export const cortexDb = new VectorDB();
|