Skip to content

feat(integrations): windowed injection-cooldown parity everywhere - #63

Closed
perfectra1n wants to merge 6 commits into
eleboucher:mainfrom
perfectra1n:integration-parity
Closed

perfectra1n wants to merge 6 commits into
eleboucher:mainfrom
perfectra1n:integration-parity

Conversation

@perfectra1n

Copy link
Copy Markdown
Contributor

Stacked on #62 (last of the stack).

The enforce core's semantics now hold across every integration:

  • openwebui had no dedupe at all — it re-served identical memories every turn. It gains the standard per-chat windowed cooldown (handshake-served knobs, 30min/3-prompt defaults), exclude_ids wire exclusion with the retry-once-and-latch old-server fallback, and the content-identity bypass.
  • opencode suppressed forever within a session — the attention-decay failure the windowed design exists to avoid. Both shipped entry points move to windowed entries; behavior change flagged in its README (0/0 restores forever).
  • openclaw's prompt-count window was inert. It now counts completed agent turns (agent_end) — the once-per-turn signal that host can honestly maintain; hosts without it degrade to time-only, never forever.
  • hermes tracked ids without content identity, so an in-place-updated memory stayed withheld for the full window. It adopts the content_hash identity + mismatch bypass, and gains a golden-vector runner replaying vectors/enforcement.json — the Python drift gate.

Suite counts: opencode 84→115, openwebui 37→48, hermes 74→84, openclaw 75+70→77+72; plugin/client/pi untouched.

Two cuts to SessionStart's fixed overhead:

The directive shrinks from ~2,070 to ~640 chars. It was a full copy of
the save policy whose canonical text already ships in the MCP server
instructions every session — the hook copy only needs to re-arm the
habit, so it is now explicitly the abridged trigger and the mcp.go
serverInstructions text is the single source of truth.

Emission now depends on payload.source instead of firing on every path:
startup/clear (and hosts without a source) get the directive; resume
gets nothing — Claude Code replays previously injected hook text on
resume, so re-emitting was pure duplication; compact gets only the
compact-recovery nudge — the context was rebuilt, but the MCP server
instructions persist in the system prompt, so re-sending the save
policy duplicated them too.

Net per-session cost of the directive drops ~72% on startup and to zero
on resume/compact.
…ow it

Closes the loop the activity feed was missing: recall serves were logged
server-side BEFORE the client's dedupe/cooldown gates ran, so the feed
over-reported what reached model context and made recall look like it
was spamming the same memories.

Server: POST /v1/activity/injected accepts a per-surface beacon
(injected ids + token/char estimates + suppressed counts by reason),
recorded as a new 'inject' activity event through the best-effort event
funnel, counted into memini_injected_memories_total{surface,result} and
memini_injected_tokens_total{surface}. The activity regrouping joins
inject reports onto nearby recall serves (same namespace, 5-minute
window — recall events carry no session id) and annotates each served
memory with injected: true/false; ABSENT means no report covered the
serve, so old data and non-reporting integrations render unchanged.

Client (Claude Code/Codex hooks): after composing stdout, each
injecting surface (briefing / prompt / pretool) sends one bounded
beacon — AbortSignal 500ms, awaited before exit, every error swallowed,
never touches stdout — gated by the new inject_telemetry setting
(MEMINI_INJECT_TELEMETRY, default on, served via the handshake like its
siblings).

UI: recall rows now read "served N → injected M" with a 'not
injected' chip on reported-suppressed memories; inject events render
their surface, per-reason suppression counts, and the token estimate.
Grafana gains an Injection efficiency row (outcomes by result, injected
by surface, injected tokens/hour).
…emory_get

Injection payloads switch from truncated-content blobs to an index the
model can expand on demand.

Server: a shared internal/api/render package projects concise content
(summary if present, else a word/sentence-boundary cut — no more
mid-sentence artifacts) behind response_format=concise on /v1/search
and format=concise on the briefing; the briefing gains
children=summary|full|none, with summary matching the isolation
contract docs always claimed (titles/counts only) while the admin UI
keeps full. Every memory now carries content_hash (16-hex sha256 of
full content||summary) in every format, and GET /v1/memories/{id} +
MCP memory_get resolve unambiguous id prefixes >= 8 hex chars (409
lists the collisions; mutations stay verbatim-id-only).

Client: hooks request concise (400-fallback for older servers), render
hits as one-liners with a trailing [m:<id8>] handle, and teach the
fetch path with a single comment line — only on blocks that actually
truncated. The briefing Recent section drops to strict index mode
(120 code points + age + handle). Dedupe identity now prefers the
server content_hash, so a memory served full in the briefing and
concise in a recall dedupes as the same memory — the regression that
made concise unsafe before.

Tail drops now say what they dropped: [+N more — memory_recall for
detail].
The inject_*_max_tok knobs stop being silent client-side trims and
become request budgets the server honors: /v1/search gains max_tokens
(fill in final rank order, first result always fits, response reports
omitted) and the briefing gains ?max_tokens (fill pinned -> facts ->
procedures -> recent, whole items only, response reports omitted).
Estimation matches the client's approxTokens recipe over the content
the response actually ships (concise text in concise mode) plus a
10-token per-item skeleton overhead.

What the budget drops is visible: recall activity events record
budget_omitted, and the hooks render the server's count in the
[+N more — memory_recall for detail] footer. Client-side fitByTokens
stays wired as the old-server fallback; a 400 on max_tokens strips it
first in the existing newest-field-first retry chain.

Defaults come off unbounded: briefing 600 tokens, recall 250, pretool
200 per file — sized so the median injection is untouched and the
multi-KB tail is what gets clipped.
… vectors

The injection-limiting primitives — suppression windows, seen-set merge,
token budgeting, render formats, identity/fingerprints, the beacon
payload — lived as hand-written .mjs in the Claude plugin and were
copy-pasted (and drifting) per integration. They now live once, as pure
functions in packages/memini-client/src/enforce/ (no fs, no env, no
clock — state and now are parameters), bundled into the plugin via the
existing _client.gen.mjs path; _shared.mjs keeps only the Node adapters
(file I/O, transport) and re-exports the same names, so hooks and their
202 tests are untouched.

packages/memini-client/vectors/enforcement.json pins 54 golden cases
generated once from the original implementations — the cross-language
contract any future port (hermes is Python) is tested against.

pi switches its local copies to the core. Two real drifts surfaced:
its injectedIdentity had fallen behind (no content_hash preference) and
its fitByTokens lacked the partial-fit trim — both adopted; pi-specific
render wordings its tests pin stay local.
The shared enforce core's semantics now hold across every integration,
not just the Claude/Codex plugin and pi:

openwebui had NO dedupe — it re-served identical memories every turn.
It gains the standard per-chat windowed cooldown (30min/3-prompt
defaults from the handshake settings), exclude_ids wire exclusion with
the old-server retry latch, and the content-identity bypass.

opencode suppressed forever within a session — the attention-decay
failure the windowed design exists to avoid. Both shipped entry points
move to windowed entries honoring inject_cooldown_ms/prompts;
re-reminding after the windows lapse is a deliberate, README-flagged
behavior change (0/0 restores forever).

openclaw's prompt-count window was inert. It now counts what that host
can honestly count — completed agent turns (agent_end) — so steps never
burn the window; a host without the signal degrades to time-only.

hermes tracked injected ids without content identity, so an updated
memory stayed withheld for the full window. It adopts the content_hash
identity + mismatch bypass, and gains a golden-vector runner replaying
packages/memini-client/vectors/enforcement.json — the cross-language
drift gate for its Python port.
@eleboucher

Copy link
Copy Markdown
Owner

Originally by eleboucher on 2026-07-19 10:25 UTC — view on source

/sync

@eleboucher

Copy link
Copy Markdown
Owner

Promoted to the canonical Forgejo repository (PR #153), which is the source of truth. Further review and merging happen there.

@eleboucher eleboucher closed this Jul 19, 2026
eleboucher pushed a commit that referenced this pull request Jul 19, 2026
> _Originally by **perfectra1n** on 2026-07-19 09:05 UTC — [view on source](#57

Adds a `memini_http_response_bytes` histogram (chi route pattern + method, buckets 256B–256K) observed from the bytes the wrapped ResponseWriter already counts, plus a p50/p90 dashboard panel for the recall and briefing routes.

**Why:** the hook-driven injection pipeline ships several MB of `/v1/search` payload per day (measured on a production deployment: mean 6.8KB, p90 11.8KB per search response, ~1,300 searches/day), but only request counts and latency were measured. Response volume — the thing injection tuning changes — was invisible. This is the baseline metric for the injection-efficiency series.

Standalone; no behavior change. The series: #58 (directive slimming) → #59 (injection telemetry) → #60 (progressive disclosure) → #61 (server budgets) → #62 (enforcement core) → #63 (integration parity), plus #64 (floors, stacked on #56) and #65 (capture hygiene) — each independently mergeable in order.

<!-- forgesync:src=github;host=github.com;repo=eleboucher/memini;kind=pull_request;id=57 -->

Co-authored-by: perfectra1n <[email protected]>
Reviewed-on: https://git.erwanleboucher.dev/eleboucher/memini/pulls/156
eleboucher added a commit that referenced this pull request Jul 19, 2026
…tion cooldown parity (#163)

> _Originally by **perfectra1n** on 2026-07-19 16:29 UTC — [view on source](#67

This combines the four PRs from the injection series that were still open after the token-budget stack landed (your 151/154/156): #56 (server rank floor), #62 (shared enforcement core), #63 (cooldown parity for the integrations), and #64 (0.5 floors, no more Glob/Grep recall). They were developed as two parallel stacks that both rewrite the client enforcement code, so rebasing each one separately would have handed you four rounds of overlapping conflicts. This branch reconciles them once, on top of current main, with everything re-tested against the merged result.

## Why these still matter

The merged budget stack caps how much an injection can ship. This half of the series decides what deserves to ship at all:

* The floor moves to the server. `min_rank_score` floors the final composite score (the one the activity feed shows), so one knob value means the same thing in every integration, and floored hits stay visible in the feed (dimmed, marked as filtered) instead of silently vanishing client-side.
* The floor default goes to 0.5 and pretool stops recalling on Glob/Grep. On the live deployment, pattern-derived queries ("Grep on `<pattern>`") were the bulk of near-zero-signal serves, and each one costs a server embed plus rerank.
* The enforcement primitives (identity hashing, the cooldown predicate and its state algebra, token budgeting, rendering, telemetry) move into `packages/memini-client/src/enforce/`, pinned by golden vectors, so pi/openclaw import them directly and the hooks consume the committed bundle. One implementation instead of five drifting copies.
* The integrations get the same windowed cooldown the Claude Code hooks already have: a memory suppressed while in its time or prompt window, re-admitted when both lapse, content changes always resurfacing.

## Where combining them was real work (worth a closer look in review)

* The pretool exclude_ids latch (#56) was written against the old `_shared.mjs` function bodies, which #62 moved into `enforce/seen.ts`. The latch is implemented in the new home: `recordInjected` stamps `r: 0` and resets an earned latch, `pretoolExcludeIds` reuses the exact `cooldownIds` window predicate, and `normInjectedEntry` preserves `r` without backfilling it on read. That last bit is deliberate: the golden vectors were cut from the pre-latch implementation, and not backfilling keeps every vector byte-identical, so `vectors/enforcement.json` is untouched by this branch.
* Recall pipeline order is now finalize, floor, linked expansion, budget. The floor runs on the final score before expansion (linked hits carry a synthetic score a post-expansion floor would nullify); the budget trims last so it sees exactly the list the caller would receive. The activity event carries `floored`, `budget_omitted`, and `excluded_count` side by side, and the feed's recall headline composes both features ("served 2 -> injected 1", "served 0 / 3 floored").
* The old-server 400 fallback keeps the strip-one-field-per-retry chain from the budget stack, with `min_rank_score` joining at the front: min_rank_score, then max_tokens, then exclude_ids, then response_format. Since the 0.5 floor now rides every search, a server that predates it costs one extra retry on the first shape; every intermediate shape is valid, and a stripped floor falls back to the client-side filter.
* The opencode and openclaw floor ports predate the parity refactor, so they were re-seated onto the parity versions: v1 keeps its cooldown-derived exclude_ids, v2 and openclaw keep the windowed seen state, and openclaw wires the floor knob alongside both cooldown dimensions.
* Defaults merged rather than fought over: the landed token budgets (600 briefing / 250 recall / 200 pretool) stay, the min-score floors go to 0.5, Glob/Grep leave the default pretool allowlist (listing them restores the old behavior).

## Testing

* `go test ./...` green.
* Client core 180/0, enforcement vectors untouched and passing.
* Claude hooks 215/0, opencode 118/0, openclaw 151/0, hermes 87 OK, openwebui 48 OK.
* pi: 94 pass, 1 fail. The failing test ("precompact and shutdown checkpoints are bounded, gated, ...") fails identically on current main, so it is pre-existing and not from this branch. Happy to chase it separately.
* All generated artifacts (api.gen.go, `_client.gen.mjs`, UI catalog, docs reference) regenerated, zero drift.

Supersedes #56, #62, #63, and #64. The commits keep the original series structure (core, parity, floor stack, defaults) in case you want to review or land them piecewise anyway.

<!-- forgesync:src=github;host=github.com;repo=eleboucher/memini;kind=pull_request;id=67 -->

Co-authored-by: perfectra1n <[email protected]>
Co-authored-by: Erwan Leboucher <[email protected]>
Reviewed-on: https://git.erwanleboucher.dev/eleboucher/memini/pulls/163
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants