aide-helix-memory · git:20260909.cac77c1 · 2026-09-09 · sha256 78791afd452c9ff2

aide-helix-memory git:20260909.cac77c1A

Immutable. This exact content is served forever at /api/v1/blob/78791afd452c9ff2.

---
name: aide-helix-memory
description: The Helix Memory subsystem — AIDE's deterministic, model-free 30-day project memory (up to 1 year+ with retention rollups). X1 event spine (memory-spine.mjs), X2 semantic join (helix-join.mjs patterns.jsonl + [learned] injection), X3 retention rollup (helix-retention.mjs months/years). Wired into the daemon via routes/memory.ts refresh-on-read cascade and surfaced by the system-map helix_memory card. Use when wiring, debugging, or extending project memory, when the system-map helix card reports offline, when [learned] lines are missing from chat, when patterns.jsonl never appears, or when adding any new work-event source to memory.
---

# AIDE Helix Memory

Deterministic project memory built from event sources AIDE ALREADY captures (the
cipher-state bus + ships.log). ZERO model involvement, zero new deps, single
source of truth shared between the legacy daemon and the TS arch server.

## Architecture (three tiers)

- **X1 spine** — `harness/memory-spine.mjs`. Merges `.aide/cipher-state.jsonl`
  (ship/approval/rejection/abort) + `.aide/metrics/ships.log` (ship_intent) into
  one chronological event stream, rolls up per-LOCAL-calendar-day digests under
  `.aide/memory/days/YYYY-MM-DD.json`. Bucketing uses LOCAL date of the event
  timestamp (NOT ISO/UTC — tests that build events with `toISOString()` misdate
  by timezone). Digest counts ships/files_touched/approvals/rejections/aborts/
  ship_intents, sums tools_used per tool, caps highlights at 10 x 220 chars.
- **X2 join** — `harness/helix-join.mjs`. Reads day digests, extracts pattern-
  candidates, writes `.aide/memory/patterns.jsonl`. Extraction rules require
  EVIDENCE: P1 tool affinity needs >= 3 uses AND >= 60% approval; P2 file
  affinity needs the file in >= 3 different days' highlights; P3 recurring
  highlight needs >= 2 different days. Lifecycle: >= 3 rejections (most recent)
  -> demoted; last_seen > 30 days ago -> archived. Exports `refresh()`,
  `listActive()` (returns `[learned] ...` lines for chat injection),
  `recordFeedback()`, `status()`. IMPORTANT: `refresh()` on thin/empty data is
  CORRECT to return 0 patterns — that is honest, not a bug. Patterns only
  materialize once the evidence thresholds are crossed.
- **X3 retention** — `harness/helix-retention.mjs`. Idempotent additive rollup.
  A month earns `.aide/memory/months/YYYY-MM.json` once the month is > 30 days
  old; a year earns `.aide/memory/years/YYYY.json` once > 365 days old. Day
  digests are never deleted; summaries redrive from them. Exports `rollup()`,
  `readSummary(kind, key)`, `status()`. rollup() is idempotent — running twice
  yields identical written lists.

## Wired paths (VERIFIED 2026-09-10 — do not regress)

- **Refresh cascade lives in the memory route** — `node/src/routes/memory.ts`
  `createMemoryService().listDigests()` refresh-on-read:
  1. `spine.refreshDayDigests()` (X1, bounded to requested window)
  2. fire-and-forget `runHelixCascade()` (X2 `helixJoin.refresh()` then
     X3 `helixRetention.rollup()`) — best-effort, 1.5s timeout per step, a
     failing stage is logged and never blocks the digest response (armor:
     component isolation).
  The route is registered via `routesForMemory(createMemoryService(workspace))`
  in `node/src/openapi.ts` and exposed through the production facade
  (`common/facade-route-map.json` `/api/memory -> ts`).
- **System-map helix card** — `node/src/services/system-map.mjs` `probeHelixMemory()`
  reads the REAL artifacts: counts `.aide/memory/days/*.json`, `months/*.json`,
  `years/*.json` and lines in `patterns.jsonl`. Reports `live` when any exist.

## Traps / dead paths (already fixed — never reintroduce)

- **NO `.aide/memory/helix.jsonl`** exists and nothing ever writes it. The
  historical probe-reading it could never report live. Fixed 2026-09-10.
- **`signals.json` is NOT an expert**. `node/src/services/system-map.mjs`
  `probeMicroExperts()` must exclude `.aide/experts/signals.json` (the
  signal-intensity cache) from the manifest count. Fixed 2026-09-10.
- **NO `.aide/logs/agent-events.jsonl`** exists. `probeAgentLoop()` reads the
  REAL artifact: `.aide/agent-loop-sessions/*.json` (written by routes/agent.ts).
  Fixed 2026-09-10.
- **X3 was a stub** (defined helpers, exported nothing) before 2026-09-10. If a
  `helix_retention` subsystem ever has zero exports again, that is the stub
  resurfacing.
- **Tests**: `tests/unit/test-helix-cascade.mjs` (unit), `tests/arch/
  helix-wiring-runtime.test.ts` (full server end-to-end: digit read drives
  X1+X2+X3, system-map cards live, read-only probe assertions).
- **TS7016**: tests importing harness `.mjs` modules must use
  `createRequire` (like tests/arch/helix-wiring-runtime.test.ts), not a
  bare ESM import with a `typeof import(...)` cast.

## Adding a new work-event source

1. Normalize it in `memory-spine.mjs` `readWorkEvents()` into `{at, kind,
   detail}` (kind in ship|approval|rejection|abort|ship_intent).
2. Add the source file read side-by-side in the `Promise.all` there.
3. If the new kind needs counting, extend `buildDayDigest()`.
4. Follow the X2/X3 cascade automatically (they consume day digests).
5. Prove: run `node --test tests/unit/test-memory-spine.mjs tests/unit/
   test-helix-cascade.mjs tests/arch/helix-wiring-runtime.test.ts`.

## Manual live probe

Drive the cascade directly against a workspace (no server needed):

```js
const { refresh } = await import('./harness/helix-join.mjs');
const { rollup, status } = await import('./harness/helix-retention.mjs');
const ws = 'E:\\aide-sovereign-workbench';
console.log(await refresh(ws));
console.log(await rollup(ws));
console.log(await status(ws));
```

system-map card check (real workspace):

```js
const { createSystemMapService } = await import('./node/src/services/system-map.mjs');
const snap = await createSystemMapService({ workspace: 'E:\\aide-sovereign-workbench' }).getSnapshot();
console.log(snap.subsystems.find(s => s.id === 'helix_memory'));
```