Commit Graph

485 Commits

Author SHA1 Message Date
Rohit Ghumare c1caf19cf3 config: honor computed MAX_TOKENS for agent-sdk fallback
The agent-sdk branch hardcoded maxTokens: 4096, ignoring the maxTokens
variable computed from env['MAX_TOKENS']. Other branches already use
the computed value. Make agent-sdk match.
2026-04-22 12:30:34 +01:00
Rohit Ghumare a654dd7ef9 Merge pull request #188 from rohitg00/fix/viewer-empty-tabs-and-import-pipeline
fix(viewer): repair empty tabs + enrich session detail + auto-derive lessons/crystals on import
2026-04-22 12:27:25 +01:00
Rohit Ghumare 510d6ff3a4 config: honor computed MAX_TOKENS for agent-sdk fallback
Follow-up to CodeRabbit review on #188. detectProvider's agent-sdk
fallback at L93-97 hardcoded maxTokens: 4096, ignoring the maxTokens
variable computed from env["MAX_TOKENS"] at L45. Every other provider
branch (minimax, anthropic, gemini, openrouter) uses the computed
value; the hardcoded one was the odd one out. Replace with the
maxTokens identifier for consistency.

Rejecting the two lockfile-related findings: .gitignore on this branch
lists package-lock.json / pnpm-lock.yaml / yarn.lock, so the workflow
comments ("Lockfiles are gitignored at the repo level") are accurate
and the two-step install (npm install --package-lock-only then npm ci)
is the correct pattern. Reverting to a single npm ci would fail CI
because there is no committed lockfile to install from.
2026-04-22 11:56:32 +01:00
Rohit Ghumare 81be407078 address CodeRabbit review on #188
All six findings verified against current code on this branch; all valid.

1. observe.ts firstPrompt (L206-211) — raw.userPrompt is typed as
   optional string but populated from payload.data which is unknown at
   runtime; a non-string truthy value (array, object) would crash at
   .replace(). Guard with typeof === 'string' before sanitizing, and
   skip the update when the trimmed value is empty.

2. replay.ts deriveCrystalAndLessons — generateId('lesson') /
   generateId('crystal') produced fresh IDs on every import, so
   re-importing the same JSONL duplicated lessons and crystals. Switch
   to fingerprintId:
   - lessons keyed by fingerprintId('lesson', content.toLowerCase()) +
     merge on existing (bump reinforcements, append sessionId to
     sourceIds, union tags).
   - crystals keyed by fingerprintId('crystal', sessionId) + preserve
     original createdAt and sourceActionIds when upserting.

3. viewer/index.html crystals filter (L2686-2691) — search haystack
   previously joined raw lesson IDs (lesson_xxx), so lesson text never
   matched. Resolve each ID via state.crystals.lessonMap and include
   resolved content in the haystack; also fold in filesAffected and
   project so the search feels complete.

4. image-quota-cleanup.ts (L56-68) — when getImageRefCount threw, the
   refCount variable stayed at its default 0 and the code happily
   proceeded to deleteImage(). That could delete a still-referenced
   image on any transient KV error. Fail-closed: log and return from
   the withKeyedLock callback on error so deleteImage is never reached
   without a confirmed refCount === 0.

5. types.ts / disk-size-manager.ts — add a StateScope interface that
   maps KV.state scope keys to their value types
   (system:currentDiskSize: number), export StateScopeKey, and have
   disk-size-manager use StateScope[typeof DISK_SIZE_KEY] in kv.get /
   kv.set generics instead of the ad-hoc <number>.

6. tsdown.config.ts + package.json — onnxruntime-node / onnxruntime-web
   are not imported directly; they come in transitively via
   @xenova/transformers. Keep them in tsdown's external array (bundling
   them breaks the relative .node binding paths in dist/) and add both
   to package.json optionalDependencies alongside @xenova/transformers
   so the optional/lazy nature is explicit. Expand the tsdown comment
   to reference the new optionalDependencies entries and name the exact
   src files that lazy-import them.

812/812 tests pass.
2026-04-22 11:53:11 +01:00
Rohit Ghumare 51bcb09104 address CodeRabbit review on #187 + fix CI
Findings verified against current code on this branch; all four valid.

1. config.ts loadFallbackConfig (L281) — user could set
   FALLBACK_PROVIDERS=agent-sdk and bypass the AGENTMEMORY_ALLOW_AGENT_SDK
   gate added to detectProvider. Filter it out at the fallback layer too,
   with the same warning pointing at the opt-in flag.

2. summarize.ts (L87-92) — the empty_provider_response branch returned
   without recording failure metrics or a diagnostic log, unlike the
   parse/validation paths. Record the same metricsStore failure event and
   log provider name, prompt size, system size, and observation count so
   empty responses are visible in telemetry.

3. providers/agent-sdk.ts (L14-45) — setting
   process.env.AGENTMEMORY_SDK_CHILD = '1' without restoring it caused
   every subsequent .query() in the same parent process to hit the
   short-circuit guard and return '' (classified as a SDK child it is
   not). Capture prev, set in try, restore in finally (delete if prev
   was undefined). Child processes spawned during the for-await loop
   still inherit the marker because env is inherited at spawn time; we
   only restore after the loop completes.

4. plugin/scripts/sdk-guard-DI1NUOS9.mjs — tsdown extracted the shared
   guard helper into a hashed chunk. Hash rotates on every rebuild and
   churns the diff. Stopped using the shared module from hooks entirely
   and inlined the 6-line guard function into each hook .ts file
   instead. sdk-guard.ts stays in the tree because the unit tests cover
   it directly. Deleted the tracked hashed .mjs and confirmed no new
   chunk is emitted.

Also applied the CI two-step install (npm install --package-lock-only
then npm ci) on this branch, matching #184. Without it, npm ci fails
because lockfiles are gitignored.

Tests: 74 files / 819 tests pass.
2026-04-22 11:44:05 +01:00
Rohit Ghumare fab246a3fd ci: two-step install so npm ci has a lockfile on this branch 2026-04-22 11:22:47 +01:00
Rohit Ghumare 5e63846b29 fix(hooks): break Stop-hook infinite recursion via agent-sdk fallback
Reported: a user with no provider API key and AGENTMEMORY_AUTO_COMPRESS=false
(which they believed protected them) hit unbounded recursion — Stop hook
POSTs /agentmemory/summarize, handler calls provider.summarize(), agent-sdk
provider spawns @anthropic-ai/claude-agent-sdk query(), which creates a full
CC-style child session that reads ~/.claude/settings.json, registers the
same plugin hooks, and fires its own Stop -> another child -> loop. ~579
ghost 'entrypoint: sdk-ts' sessions accumulated in a few minutes, draining
Claude Pro tokens.

#149 only added a stderr warning. AGENTMEMORY_AUTO_COMPRESS gated /compress
but never /summarize, so users who followed the warning's implied guidance
still got hit. Fix the loop at every layer:

1. config.ts detectProvider
   - Treat empty-string provider keys (ANTHROPIC_API_KEY=) as unset; they
     previously passed the truthiness check identically to a real key.
   - Stop defaulting to agent-sdk. When no key is set, return a 'noop'
     provider config and warn. Agent-sdk fallback now requires an explicit
     AGENTMEMORY_ALLOW_AGENT_SDK=true opt-in with a loud second warning.

2. providers/noop.ts (new) + providers/index.ts
   - NoopProvider implements MemoryProvider and returns empty strings for
     compress and summarize so callers can detect .name === 'noop' and
     short-circuit without spawning anything.
   - Add ProviderType 'noop' and wire it through createBaseProvider.

3. providers/agent-sdk.ts
   - Before spawning query(), check process.env.AGENTMEMORY_SDK_CHILD === '1'
     and return '' instead of recursing. Set the env var to '1' before the
     spawn so any child process (including the Agent SDK session's hooks)
     inherits it.

4. hooks/sdk-guard.ts (new) + all 12 hook scripts
   - Shared isSdkChildContext(payload) checks both AGENTMEMORY_SDK_CHILD=1
     and payload.entrypoint === 'sdk-ts' (CC writes this into the stdin
     jsonl for SDK-spawned sessions). Every hook script now bails early
     when that returns true, so even if one guard layer fails the others
     break the loop.

5. functions/summarize.ts
   - Short-circuit with {success:false, error:'no_provider'} when
     provider.name === 'noop' — never reach .summarize().
   - Treat an empty provider response as empty_provider_response instead
     of trying to parse it.

Tests: 74 files / 819 tests pass (+7 new in stop-hook-recursion-guard.test.ts).
Defense in depth means any ONE of the five layers breaks the loop.
2026-04-22 10:57:57 +01:00
Rohit Ghumare 9066086498 feat(viewer): richer session detail + crystals viz + tab explainers
- Crystals tab: add header card explaining what crystals are; redesign
  each card with narrative, pills (project/session/counts), tool badges,
  files list, and resolved lesson content (fetched via /lessons and
  mapped by id) instead of showing raw lesson UUIDs.
- Lessons tab: add header card explaining the rule/confidence/decay
  model so users understand the low-confidence auto-import tag.
- Sessions tab: clicking a session now fetches its observations and
  renders a full detail panel — preview callout, 4-card grid
  (observations / tools / files / duration), top tool bar chart,
  activity type breakdown, file list, metadata block, actions row.
- Pull in lessons when loading crystals so auto-generated lessons are
  actually readable in context.
2026-04-22 10:28:18 +01:00
Rohit Ghumare ae12890cfd feat(replay): auto-derive lessons + crystals from imported JSONL
import-jsonl now runs lightweight heuristic extraction on imported
sessions so the Lessons and Crystals tabs populate from replay data
instead of staying empty until a user manually invokes the MCP tools.

- Lessons: regex-match imperative patterns in user/assistant text
  (always/never/don't/prefer/avoid/caveat) and save top 20 per session
  with low confidence (0.4) and an 'auto-import' tag so they're clearly
  derived rather than hand-curated.
- Crystals: one crystal per imported session — narrative = first user
  prompt or observation preview, keyOutcomes = distinct tool names,
  filesAffected = extracted file paths, lessons = derived lesson IDs.
- Actions stay deliberately empty (user-initiated goals, not tool logs).
- Semantic/procedural still require consolidation pipeline summaries.
2026-04-22 10:18:37 +01:00
Rohit Ghumare 8c726ccf2d fix(viewer): populate import pipeline + repair empty tabs
- import-jsonl: run synthetic compression + BM25 index per observation
  (previously raw obs never got indexed; search/summaries never saw them)
- import-jsonl + observe: capture first userPrompt as session.firstPrompt
  so sessions list shows what each session was about, not just an ID
- api::audit: wrap bare array in { entries, success } — viewer expected
  { entries } shape and was rendering empty despite populated data
- api::replay::sessions: call kv.list directly instead of trigger hop
  (was timing out at 10s+ with 600+ sessions)
- viewer sessions tab: show firstPrompt preview under each session
- viewer memories tab: fix strength display (7 * 100 = 700%; now scaled
  properly for both 0-1 and 0-10 representations, clamped to 100)
- viewer memories tab: add explainer card describing what memories are
- viewer WS: add 5s connect timeout so stuck WS upgrades fall through
  to polling mode instead of showing CONNECTING... forever
2026-04-22 10:13:43 +01:00
Rohit Ghumare b1bb2b2640 chore: untrack package-lock.json + gitignore all lockfiles
Policy: never commit lock files. Downstream developers and CI
regenerate them at install time. Committing them creates churn on
every transitive-dep bump and masks real dependency changes in PRs.
2026-04-21 20:44:14 +01:00
Rohit Ghumare a12767bac7 fix(runtime): make dist actually start — drop getContext, externalize onnx, restore KV.state
The v0.9.1 dist was dead on arrival. Three independent bugs layered:

1) `getContext` was re-imported during PR #111 conflict resolution in
   three files (compress.ts, disk-size-manager.ts, image-quota-cleanup.ts).
   iii-sdk v0.11 dropped that export — the shim in src/logger.ts
   documents the fix. Tests mocked iii-sdk so `npm test` passed, but
   `node dist/cli.mjs` crashed on first import. Removed the imports,
   switched to the module-level logger.

2) The two PR #111 files also used the
   `registerFunction({ id, description }, handler)` shape. iii-sdk
   0.11.2 only accepts `registerFunction(id, handler, options?)`. The
   dist imports succeeded but the runtime registration threw "not a
   function" on the test mock. Collapsed to the flat form.

3) `KV.state` was dropped from src/state/schema.ts during the same
   conflict resolution, which broke the disk-size-manager's persistence
   key. Restored `state: "mem:state"` — used only by this manager.

4) tsdown was inlining onnxruntime-node and @xenova/transformers into
   dist/. That rewrites the relative require() inside
   `onnxruntime-node/dist/binding.js` so it can't find
   `../bin/napi-v3/darwin/arm64/onnxruntime_binding.node` from dist/.
   Even without AGENTMEMORY_IMAGE_EMBEDDINGS the module graph still
   evaluated the bundled binding.js on startup and blew up. Added both
   packages (plus onnxruntime-web and the two Anthropic SDKs) to
   `external:` in tsdown.config.ts. Bundle shrank from 6.1 MB to
   1.9 MB. The CLIP / local embedding providers lazy-load them from
   node_modules where relative paths work.

5) Bumped iii-sdk 0.11.0 → 0.11.2 to match the API currently shipped
   (Logger / durable:subscriber / durable:publisher / TriggerAction.void).

6) test/multimodal.test.ts used the old `{ id, description }` mock
   shape — rewrote the four registerFunction mocks to match the real
   `(id, cb)` signature. 812/812 pass.

End-to-end smoke test with AGENTMEMORY_SLOTS=true AGENTMEMORY_REFLECT=true:
- livez → 200 ok
- GET /slots → all 8 defaults seeded into correct scopes (persona /
  user_preferences / tool_guidelines in global; rest in project)
- POST /slot (invalid sizeLimit / unknown scope) → 400 with specific
  error
- POST /slot/append overflow → 413 with currentSize + sizeLimit
- POST /slot/reflect on empty session → no-op
- GET /audit → every slot write + reflect emits an audit row
- POST /vision-search without flag → 503 disabled
2026-04-21 20:24:15 +01:00
Rohit Ghumare 480d8e8cce Merge pull request #182 from rohitg00/feat/memory-slots-v010
feat(slots): editable pinned memory slots + idle reflection + global scope
2026-04-21 19:54:45 +01:00
Rohit Ghumare 02d13c59a7 fix(slots): resolve rebase artifacts (stray marker + tool count bump to 51) 2026-04-21 19:54:11 +01:00
Rohit Ghumare b03e9702f2 fix(slots): address CodeRabbit review on #182
## Concurrency

- mem::slot-create now wraps the duplicate check + set + audit inside
  withKeyedLock. Previously a racing slot-append could create a slot
  that slot-create assumed was absent.
- mem::slot-delete acquires the same slot-level lock writers use, so
  an in-flight append/replace cannot resurrect a deleted slot.
- mem::slot-reflect now reads pending_items / session_patterns /
  project_context INSIDE their per-slot locks, computes the new
  content under the lock, and writes back under the same lock.
  The old path read-then-locked-then-wrote, which could clobber a
  concurrent slot-replace on the same label.

## Scope + validation

- mem::slot-create duplicate check is now scope-local via
  readSlotInScope(). A project slot with the same label as a global
  slot is allowed and shadows the global on read (matches the read
  precedence). Before, create rejected any label that already existed
  in either scope.
- validateScope() returns null on unknown scope values; handler
  returns "scope must be 'project' or 'global'" 400 instead of
  silently defaulting.
- validateSizeLimit() returns null on non-integers, negatives, or
  values > 20000; handler returns "sizeLimit must be an integer
  between 1 and 20000" 400 instead of silently falling back to
  DEFAULT_SIZE_LIMIT.
- Timestamp captured once (ts) then used for createdAt + updatedAt,
  both in the create path and in seedDefaults.

## HTTP surface

- api::slot-create now rejects bad types up front:
  content / description must be strings, pinned must be a boolean,
  scope must be "project" or "global", sizeLimit must be a positive
  integer <= 20000. Malformed inputs get a 400 with a specific error
  message instead of being silently dropped.

## MCP surface

- memory_slot_create accepts both boolean and string-boolean forms
  for pinned (MCP clients bind either depending on their schema
  wrapper). Same for scope, which now also accepts explicit "project".

## Other cleanup

- event::session::stopped uses isReflectEnabled() helper instead of
  poking process.env directly. triggerVoid call wrapped in try/catch
  with a warn log so a reflect failure doesn't surface as an
  unhandled rejection.
- src/index.ts boot banner computes MCP tool count via
  getAllTools().length instead of hardcoding 44 — banner now tracks
  the actual registry size.
- README counts bumped: "all 43 memory tools" → "all 50 memory tools"
  (x2), "### 44 Tools" → "### 50 Tools", "Extended tools (44 total" →
  "(50 total", "646 tests" → "800 tests" (x2).

## Tests

- test/slots.test.ts: shadow test now goes through the public handler
  API instead of direct kv.set. Two new cases: invalid sizeLimit
  (over max, negative, non-integer) rejected; unknown scope rejected.
- Full suite: 802/802 pass (+2 new).
2026-04-21 18:57:17 +01:00
Rohit Ghumare c70b60211a feat(slots): add editable pinned memory slots + idle reflection + global scope
Introduces memory slots as a new first-class memory primitive, orthogonal
to observations / memories / graph. A slot is a labelled, size-limited,
agent-editable chunk of context. 8 defaults seed on first run
(persona, user_preferences, tool_guidelines, project_context, guidance,
pending_items, session_patterns, self_notes). Agent reads/writes via
MCP tools; pinned slots serialise cleanly into SessionStart context.

Functions (`src/functions/slots.ts`):
- mem::slot-list
- mem::slot-get
- mem::slot-create
- mem::slot-append   (refuses writes over sizeLimit)
- mem::slot-replace  (refuses content over sizeLimit)
- mem::slot-delete
- mem::slot-reflect  (used by idle reflection)

REST (`src/triggers/api.ts`):
- GET    /agentmemory/slots
- GET    /agentmemory/slot?label=...
- POST   /agentmemory/slot
- POST   /agentmemory/slot/append
- POST   /agentmemory/slot/replace
- DELETE /agentmemory/slot?label=...
- POST   /agentmemory/slot/reflect

MCP tools (`src/mcp/tools-registry.ts`, `src/mcp/server.ts`):
- memory_slot_list
- memory_slot_get
- memory_slot_create
- memory_slot_append
- memory_slot_replace
- memory_slot_delete

event::session::stopped fires mem::slot-reflect via triggerVoid when
AGENTMEMORY_REFLECT=true. The reflector reads the last N observations,
extracts TODO-flavoured titles into pending_items, counts error /
command types into session_patterns, and records touched files in
project_context. Fire-and-forget: does not block summarise.

Slots carry a scope field ("project" | "global"). Global slots live at
KV.globalSlots ("mem:slots:global") — one brain across all projects for
things like persona / user_preferences / tool_guidelines. Project-
scoped slot with the same label shadows the global one on read.

- AGENTMEMORY_SLOTS=true    — enable the whole primitive
- AGENTMEMORY_REFLECT=true  — enable Stop-hook reflection (requires SLOTS)

Both default OFF. Matches the opt-in convention of auto-compress (#138),
inject-context (#143), vision embeddings (#179).

44 MCP tools → 50 across README (stats badge, install instructions,
config block) and plugin/.claude-plugin/plugin.json description. New
AGENTMEMORY_SLOTS and AGENTMEMORY_REFLECT entries in the config block.

test/slots.test.ts — 12 new cases: default seeding by scope, label
validation, create/get/append/replace/delete round-trip, duplicate-
create rejection, sizeLimit enforcement on append + replace, project
shadows global, listPinnedSlots + renderPinnedContext, reflect no-op
on empty session, reflect updates pending_items / session_patterns /
project_context from TODO + error + file signals.

Full suite: 800/800 pass (+12 new). Build clean.
2026-04-21 18:57:17 +01:00
Rohit Ghumare 6bf1b6798a Merge pull request #179 from rohitg00/feat/multimodal-phase1-vision-embeddings
feat(multimodal): Phase 1 — CLIP visual embeddings + vision-search
2026-04-21 18:21:58 +01:00
Rohit Ghumare c7878275eb Merge pull request #183 from rohitg00/fix/website-stats-ssr-initial
fix(website): stats render target values in SSR, animate on scroll
2026-04-21 18:20:52 +01:00
Rohit Ghumare c43c6ff44a fix(website): render stat targets in SSR, animate on scroll
The Stats component rendered "0" as initial text for every metric, then
animated up to the real target via IntersectionObserver on scroll. For
real users on a real browser, the animation fires and the numbers
count up correctly. For anything that doesn't scroll — SEO crawlers,
headless automation, link previewers, screenshot bots — the page
permanently shows 0% retrieval, 0 MCP tools, 0 hooks, 0 tests.

Fix: initial SSR text is the target value (44, 12, 809, 95.2%, 92%, 0).
The count animation still plays on scroll because the RAF tick in the
IntersectionObserver callback overwrites the text unconditionally. No
behaviour change for human users — just stops crawlers seeing zeros.

Also ignore .gstack/ (local session data from the gstack browse skill).
2026-04-21 18:19:42 +01:00
Rohit Ghumare 4036640973 Merge pull request #180 from rohitg00/fix/website-meta-build-time-gen
fix(website): bake meta at build time, stop regressing to V0.0.0
2026-04-21 13:46:20 +01:00
Rohit Ghumare c36aedf3a0 fix(website): bake meta at build time, stop falling back to v0.0.0
The landing page kept reverting to "V0.0.0" on every release. Root
cause: website/lib/meta.ts resolved repo files at runtime via
import.meta.url, and after Next.js compiles server components the URL
resolves inside .next/server/. `../..` from there lands in the build
cache, not at the repo root, so the package.json read fails silently
and `pkg?.version ?? "0.0.0"` kicks in.

- website/scripts/gen-meta.mjs: new build-time script. Runs from
  website/scripts/, walks one level up to find the real repo root,
  reads package.json + src/mcp/tools-registry.ts +
  src/triggers/api.ts + src/types.ts + test/ and writes
  website/lib/generated-meta.json. Fails loud if version is missing
  (no silent 0.0.0 ever again).
- website/package.json: `predev` and `prebuild` hooks run gen-meta.
- website/lib/meta.ts: collapsed from 117 lines of runtime file
  resolution to a static JSON import. Next.js bundles the values
  directly into the server component output.
- website/lib/generated-meta.json: seeded with current values so
  editors don't complain about the missing import target.

Verified locally: `.next/server/app/index.html` now contains
"ZERO EXTERNAL DATABASES · v0.9.1" instead of "v0.0.0".
2026-04-21 13:36:06 +01:00
Rohit Ghumare c736b39760 fix(vision): validate imageRef against managed store + guard topK NaN
CodeRabbit major finding on mem::vision-embed: an authenticated caller
could POST { "imageRef": "/etc/passwd" } and the CLIP provider would
happily readFile that path and store its vector. Same vector on the
queryImageRef path of mem::vision-search. Arbitrary filesystem probing
via a REST endpoint.

- src/functions/vision-search.ts: both handlers now require the
  incoming ref to satisfy isManagedImagePath() and to exist in
  KV.imageRefs. Either check failing short-circuits with a specific
  error. Only paths that went through the managed image store (and
  still have at least one live reference) can be embedded or used as
  a query target.

- src/functions/vision-search.ts: topK is now normalised before
  clamping. `Math.max(1, NaN)` is NaN, so `slice(0, NaN)` returned
  nothing for malformed input. Coerce to finite integer, default 10,
  then clamp to [1, 50].

- test/vision-search.test.ts: imageRefs use the managed path, seed
  KV.imageRefs before each embed, and add coverage for
  managed-path rejection, unregistered-ref rejection, queryImageRef
  rejection, and NaN/fractional topK clamp. 10/10 pass locally, 798
  in the full suite.
2026-04-21 13:28:30 +01:00
Rohit Ghumare 241be2669c fix(vision): address CodeRabbit review on #179
- src/functions/image-refs.ts: delete imageEmbeddings BEFORE imageRefs
  and drop the silent .catch() so transient KV errors surface. Stops
  orphan searchable embeddings when the ref delete fails halfway.
- src/functions/vision-search.ts: record an audit row for every
  successful mem::vision-embed write (new "vision_embed" op). State-
  changing functions must leave an audit trail per the repo policy.
- src/functions/vision-search.ts: mem::vision-search now wraps raw
  queryImageBase64 into a data: URL before handing to embedImage, so
  the provider contract stays "data: URL or filesystem path".
- src/providers/embedding/clip.ts: drop the raw-base64 heuristic in
  loadImage. The old regex whitelisted "/" in the char class but the
  !src.includes("/") guard rejected any realistic base64 blob,
  falling through to readFile and throwing ENOENT. Now: data: URL or
  filesystem path only; callers with raw base64 pass a data: URL.
  Also drop unused RawImage.read type and collapse the redundant
  Float32Array copy in embedImage.
- src/triggers/api.ts: both handlers now whitelist fields with
  asNonEmptyString and parseOptionalPositiveInt before constructing
  the payload. Matches api::observe / api::search style, stops
  forwarding the raw req.body, clamps topK at the boundary.
- src/types.ts: add "vision_embed" to AuditEntry.operation union and
  rename EmbeddingProvider.embedImage's param to src (matches the
  implementation now that raw base64 isn't accepted).
- README.md, plugin/.claude-plugin/plugin.json: document
  AGENTMEMORY_IMAGE_EMBEDDINGS and bump tool count string.

Full suite still 794/794.
2026-04-21 12:07:47 +01:00
Rohit Ghumare bf6f33f48d feat(multimodal): Phase 1 — CLIP visual embeddings + vision-search
Builds on PR #111's image store. Adds a dedicated visual embedding
path so agents can search screenshots by natural language or by
similar image, without routing through VLM-describe → BM25-text.

## Additions

- `EmbeddingProvider.embedImage?(src)` slot (non-breaking, optional)
- `ClipEmbeddingProvider` — CLIP ViT-B/32 via `@xenova/transformers`
  (already a dep). 512d, normalized. Supports base64, data URL, and
  file paths. Lazy model load; same pattern as the existing local
  text provider.
- `createImageEmbeddingProvider()` gated on
  `AGENTMEMORY_IMAGE_EMBEDDINGS=true` — opt-in, matches the
  auto-compress/context-injection opt-in pattern from #138 and #143.
- `KV.imageEmbeddings` scope keyed by imageRef path.
- `mem::vision-embed` + `mem::vision-search` functions. Cosine
  similarity, brute-force over the stored set (fine at image-memory
  scales; ANN can come later).
- `POST /agentmemory/vision-embed` + `POST /agentmemory/vision-search`
  HTTP triggers with auth + 400/503 error shapes.
- MCP tool `memory_vision_search` exposed via the running-server
  handler. Accepts queryText, queryImageRef, queryImageBase64, topK,
  sessionId.
- Observe pipeline: when an image is stored and the flag is on,
  fires `mem::vision-embed` via triggerVoid so embedding happens off
  the critical path.
- `decrementImageRef` sweeps the embedding too when refcount hits
  zero, so forget/evict/retention stop orphaning vectors.

## Why

Mem0/Zep/Letta are text-only in 2026. AgenticVision ships CLIP but
needs manual capture. agentmemory already auto-captures via hooks —
bolting CLIP onto that pipeline makes it the only memory system
where the agent sees what it sees and can cross-modal retrieve
without manual invocation.

Full plan: local spec at ~/specs-backup/agentmemory-multimodal-plan.md.

## Tests

- test/vision-search.test.ts: 6 new cases covering embed→store, text
  query ranking, image-to-image query, sessionId filter, provider-
  absent error, missing-query rejection.
- Full suite: 794/794 pass (+6 new).
2026-04-21 11:20:26 +01:00
Rohit Ghumare bb60b7a91d Merge pull request #111 from Tanmay-008/feat/multimodal-image-memory
feat: implement multimodal image memory
2026-04-21 11:19:39 +01:00
Rohit Ghumare 2edc02feb5 fix: resolve conflicts with main, address qodo-ai security findings
Resolves conflicts in:
- src/state/schema.ts (merged imageRefs + accessLog)
- src/functions/auto-forget.ts (kept audit, reordered ref decrement)
- src/functions/compress.ts (collapsed iii-sdk imports, s/ctx.logger/logger/)
- src/functions/evict.ts (kept audit + deleteAccessLog, deferred ref
  decrement until after delete succeeds)
- src/functions/retention.ts (kept source-aware delete routing, fetch mem
  from resolved scope, defer ref decrement)
- src/triggers/api.ts (kept main's registerFunction/trigger shape)
- src/types.ts (kept superset of AuditEntry.operation union)

qodo-ai action-required findings:
- mem::forget now decrements image refs for every deleted memory and
  observation (including the session-wipe branch), which stops sensitive
  screenshots leaking to ~/.agentmemory/images/ after user-initiated
  forget.
- Eviction / auto-forget now decrement refs only after the KV delete
  actually succeeds. The old order (decrement-then-delete) could
  desync ref counts when the delete threw.

Follow-on fixes:
- buildSyntheticCompression now carries modality and imageData through
  from raw to compressed, so the default zero-LLM path doesn't drop
  image metadata the viewer and governance paths rely on.
- Test harness updated for the current registerFunction (name, cb)
  signature and iii-sdk partial-mock pattern — 11/11 multimodal
  tests pass, full suite 788/788.

Closes #64.
2026-04-21 10:37:24 +01:00
Rohit Ghumare c863286ed8 Merge pull request #178 from rohitg00/fix/viewer-ws-polling-fallback
fix(viewer): drop perpetual RECONNECTING... badge, fall back to REST polling
2026-04-21 10:19:12 +01:00
Rohit Ghumare 84ac801bd8 fix(viewer): re-probe WS during polling, dedupe dashboard refresh
Review on #178:

- startPolling now counts ticks and every 6 ticks (~60s) resets the
  retry counters and re-invokes connectWs() iff there's no live ws
  instance. If iii-stream eventually binds 3112, the viewer promotes
  itself back to live WS without a page reload.
- startDashboardAutoRefresh's 30s timer now skips when pollTimer is
  active — polling already refreshes the dashboard on its own
  interval, no need for a second pass 5s later.
- The `wsRetries >= WS_MAX_RETRIES` guard in connectWs is now load-
  bearing: it protects the retry sequence itself, and the polling loop
  resets the counter before the next probe so attempts resume cleanly.
2026-04-21 10:02:45 +01:00
Rohit Ghumare 14f09796ee fix(viewer): drop "RECONNECTING..." red badge, fall back to polling
iii v0.11.1's iii-stream worker doesn't actually bind the configured
3112 port (lsof confirms only 3111/3113/49134 listen), so the viewer's
ws://localhost:3112 connection can never succeed. The old code retried
forever and kept the header flashing "reconnecting..." red.

Behavior now:
- WS_MAX_RETRIES dropped from 10 to 4 (~20s of retries instead of
  ~90s).
- After retries exhaust, startPolling() kicks in and refreshes the
  dashboard/memories/sessions/activity view on a 10s interval via the
  existing REST endpoints. Badge reads "polling · 10s" in neutral
  grey.
- Label during retry loop changed from "reconnecting..." to
  "connecting..." to avoid the alarm red state on first load.
- WS reconnecting if it ever succeeds stops the poll timer.
2026-04-21 09:50:38 +01:00
Rohit Ghumare e57a108f6e Merge pull request #176 from rohitg00/fix/health-notes-demo-probe
fix(health+cli): heap_tight goes to notes, demo probe checks agentmemory ready
2026-04-21 00:44:11 +01:00
Rohit Ghumare 7013722314 fix(health+cli): move heap_tight to notes, harden demo readiness probe
## Health: heap_tight note vs alert

CHANGELOG v0.9.0 (#160) said the sub-floor heap_tight marker is a
"non-alerting note attached to the snapshot — visibility without the
false positive", but the implementation still pushed it into the same
alerts[] array as real critical/degraded conditions. Viewer rendered
it under a red "Alerts (1)" card on fresh installs (~108 MB RSS is
below the 512 MB floor).

- thresholds.ts now returns { status, alerts, notes }, with heap_tight
  routed to notes[].
- HealthSnapshot gains optional notes: string[].
- Viewer: new "Notes (N)" card in neutral color next to Alerts. Heap
  gauge only reddens when RSS is above the 512 MB floor — below the
  floor, heap percent is informational.
- Test suite updated to assert heap_tight lives in notes, not alerts.

## Demo readiness probe

`npx @agentmemory/agentmemory demo` 404'd on /agentmemory/session/start
when iii-engine was up but the agentmemory worker hadn't registered
(e.g., right after a shutdown that left iii lingering). isEngineRunning
probed `/` and accepted any response, including iii's own 404.

Split the probe: isEngineRunning still probes root (used by main/start
flow to detect whether to spawn iii), and a new isAgentmemoryReady
probes /agentmemory/livez with res.ok. runDemo now uses the stricter
probe with a specific error explaining the distinction.
2026-04-21 00:39:37 +01:00
Rohit Ghumare 66b153a2e5 Merge pull request #175 from rohitg00/release/v0.9.1
chore: release v0.9.1 — viewer endpoints, CLI hardening, version sync
v0.9.1
2026-04-21 00:31:00 +01:00
Rohit Ghumare 21a34aa28c chore: bump version to 0.9.1 + add CHANGELOG 2026-04-21 00:30:27 +01:00
Rohit Ghumare ea01290296 Merge pull request #174 from rohitg00/fix/cli-import-upgrade-errors
fix(cli): harden import-jsonl + use curl installer for iii-engine upgrade
2026-04-21 00:04:47 +01:00
Rohit Ghumare baac252b3f fix(cli): surface probe failure reason, require success===true, correct releases URL
Addresses review findings on #174:

- Livez probe now captures probe.status + body snippet on non-OK
  responses and the exception message on fetch throws, then prints the
  distinct reason: "unreachable (...)" vs "reachable but unhealthy
  (HTTP 503: ...)". No more flat "not running" for every failure mode.

- Import success check now requires json.success === true instead of
  treating a missing success field as success. A 2xx empty body used
  to fall through and print "imported 0 file(s)" — it now reports
  "response missing success field" and exits non-zero.

- Fallback releases URL pointed at iii-dev/iii-engine, which doesn't
  exist. Corrected to iii-hq/iii/releases/latest — the same URL used
  in README and the demo command.
2026-04-21 00:03:36 +01:00
Rohit Ghumare 944ad304d6 Merge pull request #173 from rohitg00/fix/version-sync
fix(version): sync across viewer + README, drop historical union
2026-04-20 23:59:11 +01:00
Rohit Ghumare ecf9c216ea fix(cli): harden import-jsonl + use curl installer for iii-engine upgrade
## import-jsonl

Two failure modes surfaced as "Unexpected end of JSON input":

- Livez probe only caught fetch throws, not non-OK responses. A stray
  service on port 3111 would pass the probe silently, then POST would
  404 with an empty body and crash on res.json().
- res.json() is unforgiving: an empty body, HTML error page, or proxy
  response exits with a cryptic parse error instead of the actual HTTP
  status.

Now:
- Probe checks res.ok before continuing, with a start-the-server hint.
- Import reads body as text, parses only if non-empty, and surfaces
  HTTP 401 (secret mismatch) and 404 (old server predating v0.8.13)
  with actionable guidance.

## upgrade

cargo install iii-engine fails because iii-engine isn't on crates.io,
and requireSuccess exited the whole upgrade before Docker ever ran.

Switch to the official installer used everywhere else in the docs and
demo command:

  curl -fsSL https://install.iii.dev/iii/main/install.sh | sh

Installer failure is treated as optional — Docker pull still runs and
the final note points at the Docker + release-binary fallbacks.
2026-04-20 23:57:27 +01:00
Rohit Ghumare ca626f3bed fix(version): sync version across viewer + README, drop historical union
- src/version.ts: collapse literal union to single `VERSION = "0.9.0"`;
  the historical enum was regenerated on every release and served no
  runtime purpose. Export-import keeps its supportedVersions set, which
  is the actual compat contract.

- src/viewer/index.html: replace hardcoded `v0.7.0` brand badge with
  `__AGENTMEMORY_VERSION__` placeholder.

- src/viewer/document.ts: substitute the placeholder with VERSION at
  render time (same mechanism as the CSP nonce).

- README.md: "New in v0.8.2" banner was two releases stale. Replace with
  v0.9.0 highlights linking to CHANGELOG.
2026-04-20 22:11:15 +01:00
Rohit Ghumare 4f8c4caf7a Merge pull request #172 from rohitg00/fix/viewer-missing-get-endpoints
fix(viewer): register GET endpoints dashboard needs (semantic, procedural, relations)
2026-04-20 21:18:37 +01:00
Rohit Ghumare 1dfc0a5099 fix(viewer): add missing GET /semantic, /procedural, /relations endpoints
Dashboard viewer called these three list endpoints but only POST
/relations existed and /semantic + /procedural were never registered,
so the frontend received 404/405 and rendered empty dashboard cards
even when data was present.

Adds:
- api::semantic-list    GET /agentmemory/semantic   -> { semantic }
- api::procedural-list  GET /agentmemory/procedural -> { procedural }
- api::relations-list   GET /agentmemory/relations  -> { relations }

Integration tests assert each endpoint returns 200 with an array.

Reported by Jay Ferguson running agentmemory with Hermes.
2026-04-20 17:47:22 +01:00
Tanmay-008 2b7adc061c fix(memory): resolve CodeRabbit linter & concurrency bugs 2026-04-19 23:15:50 +05:30
Tanmay-008 1d4fbce38f fix(disk): implement 500MB quota LRU eviction and memory deletion parity 2026-04-19 18:07:31 +05:30
Rohit Ghumare ca578ce8d8 Update README.md 2026-04-19 01:19:44 +01:00
Rohit Ghumare 14b80db91a Merge pull request #169 from rohitg00/fix/website-mobile-og
fix(website): mobile overflow + OG/Twitter preview images
2026-04-19 00:37:52 +01:00
Rohit Ghumare 197aa8d0e8 fix(website): mobile overflow + add OG/Twitter preview images
Mobile (375px) had ~55px horizontal scroll caused by grid tracks
defaulting to min-content across nowrap text in Install, AgentInstall,
and CommandCenter sections. Replace implicit/`1fr` tracks with
`minmax(0, 1fr)` so tracks can shrink below their content and let
`overflow: hidden` / `text-overflow: ellipsis` actually clip.

Also tightens padding, font-size, and letter-spacing at <=640px for
Install boxes, AgentInstall snippet head/code/copy row, CommandCenter
tabs and launch strip, and Stats tiles so dense uppercase content stops
crowding itself at mobile widths.

Adds app/opengraph-image.tsx and app/twitter-image.tsx using
next/og ImageResponse, statically prerendered at build (no edge
runtime) so socials get a real 1200x630 preview instead of the blank
card the site was shipping before.
2026-04-19 00:30:22 +01:00
Rohit Ghumare c9aedbfaa4 Merge pull request #167 from rohitg00/feat/website-auto-version
feat(website): derive version + counts from source at build
2026-04-18 21:14:32 +01:00
Rohit Ghumare 9520a9f564 fix(website/meta): address CodeRabbit review on auto-derived counts
- testsPassing no longer hardcoded to 777. New countTestCases() walks
  the test/ tree and counts it()/test() call sites at build. Prerender
  now shows the live count (currently 795) instead of a stale literal.
- restEndpoints regex tightened from /api_path:/ to
  /config:\s*\{\s*api_path:\s*"/. Comments, example strings, and
  docstrings no longer inflate the count.
- Fallback defaults consolidated into one exported DEFAULT_META
  constant. Each count applies its fallback in exactly one place (at
  the return, if the safeCountMatches result is 0). No more double
  defaults.
- Hook counting no longer divides quote characters by 2. It parses
  the HookType union body, splits on '|', and filters to string-
  literal members — robust to inline comments or quotes elsewhere in
  the union.
- Stats, Features, and LiveTerminal props are now required and take
  their fallbacks from DEFAULT_META via getProjectMeta(). No
  per-component duplication.
- Stats effect resets data-done on every rerun so a meta change at
  build replays the count animation against the new target instead
  of stranding a stale value.
2026-04-18 20:53:11 +01:00
Rohit Ghumare 6dd8eaa0d3 Merge pull request #168 from rohitg00/docs/aaif-governance
docs: governance baseline
2026-04-18 20:49:47 +01:00
Rohit Ghumare 76c96cef0c docs: address CodeRabbit review on governance docs
- CODE_OF_CONDUCT.md: add an external escalation fallback chain for
  reports that concern the only active Maintainer or where every
  listed Maintainer recuses. Routes to the Contributor Covenant
  ombudsperson, a future hosting foundation's conduct committee, or
  GitHub Trust & Safety — so no report dead-ends.
- SECURITY.md: drop the 'do not encrypt' advice. GHSA is the
  preferred channel; email is now framed as an encrypted-only
  fallback with explicit GPG / SSH key publication URLs for
  reporters who need end-to-end confidentiality.
- SECURITY.md: grammar fix — 'after a new minor is released'.
- CONTRIBUTING.md: replace brittle '777+ tests' with 'the full test
  suite'; keep the integration-test exception.
- GOVERNANCE.md: replace placeholder link label with a plain
  'release process' label.
- MAINTAINERS.md: drop the contributor shortlist — premature to
  name future Maintainer candidates before they've been through the
  formal process. Keep the contributor-graph link for transparency.
2026-04-18 20:35:02 +01:00
Rohit Ghumare b995983e5a docs: drop AAIF-PROPOSAL-DRAFT.md (submission paperwork, not project material) 2026-04-18 20:30:59 +01:00