Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ CORTEX_MIRROR_PATH=./cortex-mirror.sqlite

# --- Next.js / wallet UI ---

# Reown AppKit (WalletConnect). Omit to use injected MetaMask only.
# NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
# The console uses injected wallets only (MetaMask / Rabby / EIP-6963) — no
# WalletConnect/Reown projectId needed.
# NEXT_PUBLIC_BRAGA_RPC=https://braga.hoodi.arkiv.network/rpc
# NEXT_PUBLIC_APP_URL=http://localhost:3000

Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
![Cortex landing page — Darwinian memory for AI agents on Arkiv Braga](./assets/landing-page.png)

> **Darwinian memory for AI agents** — observations that earn longer expiration when
> cited; useless ones decay for free on Arkiv. Encrypted at rest with a key derived
> from your wallet. _(AI + Privacy · Arkiv × ETHNS Builder Challenge)_
> cited; useless ones decay for free on Arkiv. Encrypted at rest with a wallet-derived
> key (client-side in the plugin). _(AI + Privacy · Arkiv × ETHNS Builder Challenge)_

| | Link |
|---|------|
Expand All @@ -24,7 +24,7 @@ One repo — not a monorepo. `app/` + `src/` + `cortex-plugin/` ship together.
|--------|----------|
| **Write** | RaBitQ-compress embeddings (1536-d → ~198 B), seal payload with wallet-derived AES-256-GCM, `createEntity` on Braga with **1 h** starting expiration |
| **Recall** | Hybrid search: Arkiv attributes + local mirror + RaBitQ distance — no vector DB |
| **Reinforce** | Every `act(..., citations=[...])` fires **accumulative** `extend` (remaining lease + 24 h), so useful memories grow; stale ones evict via L1Block |
| **Reinforce** | Every `act(..., citations=[...])` fires **accumulative** `extend` (additive on Braga: +24 h baseline, scaled up to 2.5× by the memory's proven utility), so useful memories grow; stale ones evict via L1Block |
| **Prove** | Merkle Mountain Range over decisions; roots **anchored on Arkiv** |

Agent surface: **`recall(query, k)`** and **`act(action, citations[])`** only.
Expand Down Expand Up @@ -171,9 +171,10 @@ sequenceDiagram
Engine->>Chain: query attributes
Engine-->>Agent: memory IDs
Agent->>Engine: act with citations
Engine->>Chain: extendEntity remaining plus 24h
Engine->>Chain: extendEntity additive plus reinforcement
Engine->>Chain: anchor MMR root
Engine-->>SSE: memory.cited and arkiv.rpc
Note over Agent,Chain: act() is optimistic — enqueues; the worker anchors the extend async (no synchronous extend tx)

Note over Chain: Uncited memories evict via L1Block
```
Expand All @@ -182,7 +183,7 @@ sequenceDiagram

**Tiers:** working (1h) → episodic (≥2 cites, +7d) → semantic (≥5 cites · 3 sessions, 1y rule).

**Extend math:** `newBtl = remaining + reinforcement` (strict increase — naïve `+24h` reverts when remaining > 24 h).
**Extend math:** deployed Braga `extend` is **additive** — `expiresAt += expiresIn` (verified on-chain 2026-05-25). Each citation adds exactly its reinforcement (+24 h working, +7 d episodic), so useful memories accumulate lease while uncited ones decay. (The `remaining + reinforcement` REPLACE formula belongs to the future `EntityRegistry.sol`, not the live precompile — see `src/darwinian/extend.ts`.)

---

Expand Down
3 changes: 3 additions & 0 deletions app/decay/[entityKey]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"use client";

export { default } from "@/ui/decay-receipt-page";
374 changes: 114 additions & 260 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cortex-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cortex-memory",
"version": "0.1.2",
"version": "0.1.3",
"description": "Every Claude Code session becomes a sovereign memory node: auto-captured before compaction and saved to Arkiv, recalled at session start, owned by your wallet. Reuses the Cortex decay-aware memory engine.",
"author": {
"name": "Cortex"
Expand Down
79 changes: 46 additions & 33 deletions cortex-plugin/dist/cortex-hook-capture.js
Original file line number Diff line number Diff line change
Expand Up @@ -5355,18 +5355,30 @@ var init_embeddings = __esm(() => {
});

// scripts/cortex-hook-capture.ts
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
import { join as join2, basename } from "path";
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
import { join as join2 } from "path";
import { homedir as homedir2 } from "os";
import { execFileSync, spawn } from "child_process";
var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
var MAX_SUMMARY_CHARS = 6000;
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf-8");
import { spawn } from "child_process";
import { createHash } from "crypto";

// src/lib/project-identity.ts
import { execFileSync } from "child_process";
import { basename } from "path";
function normalizeRemote(url) {
if (!url)
return null;
let s = url.trim().replace(/\.git\/?$/, "");
const scp = !s.includes("://") ? s.match(/^[^@]+@([^:]+):(.+)$/) : null;
if (scp)
return `${scp[1]}/${scp[2]}`.replace(/\/+$/, "");
try {
const u = new URL(s);
const path = u.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
if (u.hostname && path)
return `${u.hostname}/${path}`;
} catch {}
const trimmed = s.replace(/\/+$/, "");
return trimmed || null;
}
function resolveProject(cwd) {
try {
Expand All @@ -5385,21 +5397,16 @@ function resolveProject(cwd) {
return "unknown-project";
}
}
function normalizeRemote(url) {
if (!url)
return null;
let s = url.trim();
s = s.replace(/\.git$/, "");
const scp = s.match(/^[^@]+@([^:]+):(.+)$/);
if (scp)
return `${scp[1]}/${scp[2]}`;
try {
const u = new URL(s);
const path = u.pathname.replace(/^\/+/, "");
if (u.hostname && path)
return `${u.hostname}/${path}`;
} catch {}
return s || null;

// scripts/cortex-hook-capture.ts
var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
var MAX_SUMMARY_CHARS = 6000;
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf-8");
}
function parseTranscript(raw) {
const out = [];
Expand Down Expand Up @@ -5488,13 +5495,13 @@ function buildSummary(msgs, project, sessionId) {
summary = summary.slice(0, MAX_SUMMARY_CHARS) + `
\u2026(truncated)`;
}
return summary;
return { text: summary, meaningful: goals.length > 0 || decisions.length > 0 };
}
function firstMeaningfulLine(text) {
for (const raw of text.split(`
`)) {
const s = raw.trim().replace(/\s+/g, " ");
if (!s || s.startsWith("/") || s.startsWith("```") || s.startsWith("<"))
if (!s || s.startsWith("/") || s.startsWith("```") || s.startsWith("</") || s.startsWith("<!"))
continue;
if (s.length < 4)
continue;
Expand Down Expand Up @@ -5524,8 +5531,11 @@ function dataDir() {
}
function queuePending(p) {
const dir = dataDir();
const file = join2(dir, `${safeName(p.project)}__${safeName(p.sessionId)}.json`);
writeFileSync2(file, JSON.stringify(p, null, 2), "utf-8");
const tag = createHash("sha1").update(`${p.project}\x00${p.sessionId}`).digest("hex").slice(0, 8);
const file = join2(dir, `${safeName(p.project)}__${safeName(p.sessionId)}__${tag}.json`);
const tmp = `${file}.${process.pid}.tmp`;
writeFileSync2(tmp, JSON.stringify(p, null, 2), "utf-8");
renameSync2(tmp, file);
return file;
}
function safeName(s) {
Expand Down Expand Up @@ -5553,22 +5563,25 @@ async function main() {
const project = resolveProject(cwd);
const title = `Session summary \u2014 ${eventName} \u2014 ${sessionId}`;
let summary = "";
let meaningful = false;
try {
const path = event.transcript_path;
if (path && existsSync2(path)) {
let raw = readFileSync2(path, "utf-8");
if (raw.length > MAX_TRANSCRIPT_BYTES)
raw = raw.slice(-MAX_TRANSCRIPT_BYTES);
const msgs = parseTranscript(raw);
summary = buildSummary(msgs, project, sessionId);
const built = buildSummary(msgs, project, sessionId);
summary = built.text;
meaningful = built.meaningful;
} else {
log(`transcript_path missing or not found: ${path}`);
}
} catch (err) {
log("transcript parse failed:", err);
}
if (!summary.trim()) {
log(`no summary produced for project=${project} session=${sessionId}; nothing to capture.`);
if (!summary.trim() || !meaningful) {
log(`no meaningful summary for project=${project} session=${sessionId}; nothing to capture.`);
return;
}
const file = queuePending({
Expand Down
72 changes: 42 additions & 30 deletions cortex-plugin/dist/cortex-hook-recall.js
Original file line number Diff line number Diff line change
Expand Up @@ -25302,22 +25302,28 @@ var init_recall = __esm(() => {

// scripts/cortex-hook-recall.ts
import { readdirSync, existsSync as existsSync2, statSync } from "fs";
import { join as join2, basename } from "path";
import { join as join2 } from "path";
import { homedir as homedir2 } from "os";
import { execFileSync, spawn } from "child_process";
var HOOK_BUDGET_MS = 8000;
var RECALL_K = 5;
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin)
chunks.push(chunk);
return Buffer.concat(chunks).toString("utf-8");
}
function withTimeout2(p, ms) {
return Promise.race([
p,
new Promise((_, rej) => setTimeout(() => rej(new Error("cortex-recall timeout")), ms))
]);
import { spawn } from "child_process";

// src/lib/project-identity.ts
import { execFileSync } from "child_process";
import { basename } from "path";
function normalizeRemote(url) {
if (!url)
return null;
let s = url.trim().replace(/\.git\/?$/, "");
const scp = !s.includes("://") ? s.match(/^[^@]+@([^:]+):(.+)$/) : null;
if (scp)
return `${scp[1]}/${scp[2]}`.replace(/\/+$/, "");
try {
const u = new URL(s);
const path = u.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
if (u.hostname && path)
return `${u.hostname}/${path}`;
} catch {}
const trimmed = s.replace(/\/+$/, "");
return trimmed || null;
}
function resolveProject(cwd) {
try {
Expand All @@ -25336,20 +25342,21 @@ function resolveProject(cwd) {
return "unknown-project";
}
}
function normalizeRemote(url) {
if (!url)
return null;
let s = url.trim().replace(/\.git$/, "");
const scp = s.match(/^[^@]+@([^:]+):(.+)$/);
if (scp)
return `${scp[1]}/${scp[2]}`;
try {
const u = new URL(s);
const path = u.pathname.replace(/^\/+/, "");
if (u.hostname && path)
return `${u.hostname}/${path}`;
} catch {}
return s || null;

// scripts/cortex-hook-recall.ts
var HOOK_BUDGET_MS = 8000;
var RECALL_K = 5;
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin)
chunks.push(chunk);
return Buffer.concat(chunks).toString("utf-8");
}
function withTimeout2(p, ms) {
return Promise.race([
p,
new Promise((_, rej) => setTimeout(() => rej(new Error("cortex-recall timeout")), ms))
]);
}
function deriveQuery(cwd, project) {
const recent = recentFiles(cwd, 8);
Expand Down Expand Up @@ -25451,7 +25458,12 @@ async function main() {
try {
const { recall: recall2 } = await Promise.resolve().then(() => (init_recall(), exports_recall));
const query2 = deriveQuery(cwd, project);
const hits = await withTimeout2(recall2({ query: query2, k: RECALL_K, project }), HOOK_BUDGET_MS);
const hits = await withTimeout2(recall2({
query: query2,
k: RECALL_K,
project,
...event.session_id ? { sessionId: event.session_id } : {}
}), HOOK_BUDGET_MS);
if (!hits || hits.length === 0) {
if (queued > 0) {
process.stdout.write(`Cortex: syncing ${queued} queued memory write(s) for ${project} in the background. No prior memories surfaced yet.
Expand Down
Loading
Loading