skills

26 skillsgithubanonymous
all skillsmemory-pulse-check

memory-pulse-check

Use when asked whether automatic memory feeding is working, when the memory count seems stuck, when a session should have produced memories but did not, or for a periodic health check of the capture-sweep-distill-write pipeline. Verifies the gateway distiller loop end to end. NOT for deduping or pruning a bloated store (memory-curation), not for fixing the extractor, not for releasing an image (release-verify).

raw markdown

Body

# Memory pulse-check (is the automatic feed actually feeding?)

The pipeline has FIVE stages, each with its own silent failure mode:

capture -> sweep select -> attachment gate -> transcript extraction -> add_memory write

A failure at ANY stage looks identical to the user: the count is not moving. Diagnose stage by stage; do not skip to fixes.

## Stage 1 - gates (config + attachment)

1. Deployed env (kubectl get deploy <gw> -n <ns> -o custom-columns=ENV:.spec.template.spec.containers[0].env): must carry DISTILLER_ENABLED=true, DISTILLER_MEMORY_SERVER (registry name), DISTILLER_MEMORY_PROBE_TOOL.
2. The key must be memory-ATTACHED: an mcp_server_creds row for the memory server. No row = the sweep SQL join never selects the key's rows.
3. The memory server must be in the enabled registry snapshot.

## Stage 2 - the sweep is selecting

grep 'distill sweep' on the app container logs. Healthy: 'N conversation rows + M canonical rows due, K attempted'.
- No such lines: housekeeping loop dead, or the offpeak_only gate closed (sweep waits for off-peak).
- due>0 attempted=0: rows gate-skipped or leased by another replica - read the skip lines.
- 'claimed by another replica' lines: normal under HPA scale-out with the 2026-10-11 lease fix.

## Stage 3 - extraction renders (the silent starver)

The classic failure: 'transcript collapsed to N chars from M messages' - the extractor rendered nothing. History: type==text-only filtering missed Responses-native part types (input_text/output_text) and reasoning summaries (fixed 2026-10-11); double-encoded rows (a JSONB string instead of an array, or a char-spread list) collapsed even harder (fixed same day). If a collapse line fires on a CURRENT build, the shape-sample line under it names the surviving gap (roles + content types, no bytes). Fix the extractor; NEVER mark a collapsed row distilled (a marker with no write = permanent silent data loss).

## Stage 4 - writes land (ok=True)

grep 'ok=True detail=' on the app container. Each line = one add_memory write to the CALLER KEY's memory dataset. ok=False: read the detail - registry missing vs RAG 4xx vs timeout. Repeated ok=False on one thread = a stuck row; check the memory server pod.

## Stage 5 - the dataset grows

get_dataset_info(document_count): snapshot, compare an hour later. Healthy under active chat: plus 1..3 docs per distilled session (mm-rag chunks long summaries). Flat count while ok=True lines exist = writes land in a DIFFERENT dataset than the one watched - resolve the key's X-Memory-Dataset stamp (enrolled per-key datasets are not the operator's own store).

## Known-failure catalogue (all hit in production 2026-10-09..11)

| Symptom in logs | Root cause | Status |
|---|---|---|
| collapsed to 17 chars, many messages | part-type filter / double-encoded rows | fixed (extractor hardening + recovery) |
| collapsed + marker written (old builds) | marked-distilled on empty render | fixed (deferral + epoch sentinel) |
| thread distilled twice, summaries seconds apart | HPA scale-out: N pods sweep, no claim | fixed (lease claim upsert) |
| thread re-distilled every turn | no content-hash skip | fixed (distilled_content_hash) |
| rows never due again | marker default now() beat row state | fixed (epoch sentinel on gate-skip INSERT) |
| prune deleted un-distilled rows | prune ignored gate-skip state | fixed (gate_failed_at IS NULL guard) |

When a NEW failure appears, add its row here - the catalogue is the skill's memory.

## Duplicate pairs (dedup belongs to memory-curation)

Pre-lease duplicate summaries (same session, same kind, seconds apart) are FUNCTIONAL duplicates with DIFFERENT wording (independent LLM generations) - text-similarity dedupe will NOT catch them; identify by session + kind + timestamp proximity. Deleting stored memories is a memory-curation operation (it owns id mapping and safety rails) - hand the cleanup there once the lease fix is deployed; do not improvise deletions during a pulse check.

## The 60-second recipe (run in order)

1. get_dataset_info: snapshot document_count.
2. Sweep lines: grep 'distill sweep' - attempted vs due; any collapsed or shape-sample lines?
3. Write lines: count ok=True since the snapshot; ok=False details if any.
4. get_dataset_info again: did the count move by roughly the ok=True count (times chunking)?
5. Verdict: moving = healthy. Flat + ok=True = wrong dataset watched. Flat + no ok=True = extraction or gate stage. No sweep lines = housekeeping or offpeak stage.

Verification truth chain for a JUST-DEPLOYED fix: pod image tag, then in-container __version__ (cat the init file, exec allowlist permitting), then stage 2-4 log lines. A deployed tag is a claim, not evidence (release-verify owns the full chain).

## Boundaries

- Store cleanup, dedupe, prune: memory-curation.
- Image build and rollout verification chain: release-verify.
- The distiller PLUGIN (provider, model, profile): harness-update territory; gateway-side extraction fixes are ordinary repo work.
- Deleting memories during a pulse check: NEVER. Diagnose, then hand to memory-curation.