From 913cf16ec9fd1dc0e3c8b68b277bdbd173306a2c Mon Sep 17 00:00:00 2001 From: kenblaue Date: Mon, 17 Aug 2026 08:55:54 +0200 Subject: [PATCH] Restore HyperMemory skill and streamline lifecycle --- AGENTS.md | 6 +- README.md | 30 +- plugins/hypermemory/README.md | 12 +- plugins/hypermemory/agents/memory-writer.md | 12 +- .../hypermemory/scripts/hypermemory_hook.py | 67 +-- .../hypermemory/skills/hypermemory/SKILL.md | 381 +++++++++++++----- .../references/memory-writer-agent.md | 15 +- tests/test_hypermemory_plugin.py | 43 +- 8 files changed, 411 insertions(+), 155 deletions(-) mode change 100644 => 100755 plugins/hypermemory/scripts/hypermemory_hook.py diff --git a/AGENTS.md b/AGENTS.md index 267f144..beed754 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,8 +13,10 @@ their manifests. - Keep `.codex-plugin/plugin.json` as the required manifest entry point. - Keep MCP credentials out of source. Use OAuth or local credential storage. - Preserve explicit hook trust and safe degraded behavior. -- Keep HyperMemory recall on the main agent and persistence/token reporting on - one awaited memory-writer sub-agent. +- Keep HyperMemory recall on the main agent for substantive prompts and skip it + only for narrowly classified lightweight social prompts. +- Keep persistence/token reporting on one fresh fire-and-forget memory-writer + sub-agent; the main agent must never wait, poll, inspect, or message it. - Keep HyperColab join/sync/claims on the main agent. Delegated coordination writers may record progress but must not bypass ownership conflicts. diff --git a/README.md b/README.md index 76d7199..36c6e2a 100644 --- a/README.md +++ b/README.md @@ -211,10 +211,11 @@ sequenceDiagram participant L as Codex token listener U->>M: Submit a prompt - M->>MCP: Overview and relevant recall + M->>MCP: Overview and recall for substantive prompts MCP-->>M: Relationship-aware context M->>M: Complete the requested work - M->>W: Delegate a concise finalization summary + M-)W: Dispatch a concise finalization summary + M-->>U: Return final response without waiting W->>MCP: Recall before writing W->>MCP: Store or update durable knowledge W->>MCP: Write one timeline entry @@ -222,16 +223,16 @@ sequenceDiagram L-->>W: Exact payload or fallback instruction W->>MCP: Report tokens once W->>L: Acknowledge accepted exact claim - W-->>M: Return brief status - M-->>U: Return final response ``` -The main agent performs recall because remembered context must be available -while reasoning about the user's request. Persistence and telemetry are moved -to one awaited memory-writer sub-agent to keep the main context focused. Each -turn uses a fresh, turn-unique writer with `fork_turns="none"`; reusing a writer -or copying the full parent history would repeatedly charge that context during -tool continuations. The role contract prevents recursive delegation. +The main agent performs recall for substantive prompts because remembered +context must be available while reasoning about the user's request. Narrow, +standalone greetings and acknowledgements skip retrieval. Persistence and +telemetry move to one fire-and-forget memory-writer sub-agent so they do not +delay the user-facing response. Each turn uses a fresh, turn-unique writer with +`fork_turns="none"`; reusing a writer or copying the full parent history would +repeatedly charge that context during tool continuations. The role contract +prevents recursive delegation. This coordination is deliberately invisible in normal use. HyperMemory does not emit status messages, inject synthetic user prompts, or append memory @@ -624,7 +625,7 @@ The tests cover: - catalog-to-plugin path and identity consistency; - required manifests, MCP declarations, hooks, skills, and assets; -- HyperMemory stop-hook delegation and recursion protection; +- HyperMemory prompt classification and fire-and-forget writer delegation; - exact token aggregation and two-phase checkpointing; - HyperColab hook behavior, Git discovery, cached leases, and queued events; - logo format and dimensions; and @@ -637,12 +638,15 @@ python3 ~/.codex/skills/.system/plugin-creator/scripts/validate_plugin.py \ plugins/hypermemory python3 ~/.codex/skills/.system/plugin-creator/scripts/validate_plugin.py \ plugins/hypercolab -python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py \ - plugins/hypermemory/skills/hypermemory python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py \ plugins/hypercolab/skills/hypercolab ``` +The official HyperMemory MCP skill retains its extended `version`, +`enforcement`, and `trigger` frontmatter, which the generic skill validator +does not currently accept. The HyperMemory plugin validator and repository +tests cover that package without stripping its official metadata. + ### Build review archives ```bash diff --git a/plugins/hypermemory/README.md b/plugins/hypermemory/README.md index 41f9eaa..1bd518e 100644 --- a/plugins/hypermemory/README.md +++ b/plugins/hypermemory/README.md @@ -6,10 +6,12 @@ separate token-reporting branches for ChatGPT and Codex. ## Behavior -- Main agent: overview and recall so memory can inform the response. +- Main agent: overview and recall for substantive prompts so memory can inform + the response; narrow standalone greetings and acknowledgements skip retrieval. - Memory-writer sub-agent: a fresh, turn-unique worker created without parent conversation history performs store, update, forget, one timeline write, and - one token report before the final response. + one token report asynchronously after dispatch. The parent never waits for, + polls, messages, or reads the worker. - Codex: trusted hooks enforce the lifecycle and read exact cumulative token counters from the active rollout JSONL using a two-phase inspect/ack helper. - ChatGPT: reports an uncertainty-labelled workload estimate because consumer @@ -51,6 +53,10 @@ it does not require a checked-in `.app.json`. ```bash python3 ~/.codex/skills/.system/plugin-creator/scripts/validate_plugin.py plugins/hypermemory -python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/hypermemory/skills/hypermemory pytest -q tests/test_hypermemory_plugin.py ``` + +The official HyperMemory MCP skill intentionally retains its extended +`version`, `enforcement`, and `trigger` frontmatter. Codex's generic +`quick_validate.py` currently rejects those official keys, so the plugin +validator and HyperMemory tests validate this package instead. diff --git a/plugins/hypermemory/agents/memory-writer.md b/plugins/hypermemory/agents/memory-writer.md index e538eb6..aca542a 100644 --- a/plugins/hypermemory/agents/memory-writer.md +++ b/plugins/hypermemory/agents/memory-writer.md @@ -6,10 +6,11 @@ description: Bounded HyperMemory persistence, timeline, and token-reporting role # Memory writer This is a packaged role contract, not a user-facing skill. The HyperMemory -skill asks the host to spawn one awaited sub-agent with this role after the main -agent has completed the requested work. The host must create a fresh, -turn-unique task with `fork_turns="none"` and provide only a concise bounded -summary; it must never reuse a writer from an earlier turn. +skill asks the host to spawn one fire-and-forget sub-agent with this role after +the main agent has completed the requested work. The host must create a fresh, +turn-unique task with `fork_turns="none"`, provide only a concise bounded +summary, and continue without waiting or polling; it must never reuse a writer +from an earlier turn. 1. Recall related nodes before changing the graph. 2. Store durable new knowledge or update the existing canonical node. @@ -18,7 +19,8 @@ summary; it must never reuse a writer from an earlier turn. 5. Report token usage exactly once. Use the Codex listener's exact payload when available; otherwise use an honest estimate with uncertainty. 6. Acknowledge a Codex listener claim only after the MCP accepts the report. -7. Return a brief status to the parent and never spawn another agent. +7. Never spawn another agent. A brief diagnostic status may be returned, but + the parent does not wait for or consume it. Never store credentials, hidden reasoning, raw transcripts, complete command output, tool payloads, or large code bodies. diff --git a/plugins/hypermemory/scripts/hypermemory_hook.py b/plugins/hypermemory/scripts/hypermemory_hook.py old mode 100644 new mode 100755 index 324b7af..1ac3a76 --- a/plugins/hypermemory/scripts/hypermemory_hook.py +++ b/plugins/hypermemory/scripts/hypermemory_hook.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """Silent Codex lifecycle bridge for mandatory HyperMemory behavior. -The hook prepares token-listener jobs before model work and injects only hidden -developer context. It never blocks Stop, creates continuation prompts, or reads -conversation content from the transcript. +The hook classifies lightweight prompts, prepares token-listener jobs before +model work, and injects concise hidden developer context. It never blocks Stop, +creates continuation prompts, or reads conversation content from the transcript. """ from __future__ import annotations @@ -11,12 +11,29 @@ import argparse import json import os +import re import sys import tempfile from datetime import UTC, datetime from pathlib import Path from typing import Any +LIGHTWEIGHT_MAX_CHARS = 80 +LIGHTWEIGHT_PHRASES = frozenset({"got it", "hello", "hey", "hi", "howdy", "ok", "okay", "thank you", "thanks"}) + + +def _prompt_text(payload): + return payload.get("prompt") or "" + + +def _normalize_prompt(text): + text = text.casefold().replace("’", "'") + return " ".join(re.sub(r"[^\w\s']+", " ", text).split()) + + +def _is_lightweight_prompt(text): + return bool(text) and len(text) <= LIGHTWEIGHT_MAX_CHARS and _normalize_prompt(text) in LIGHTWEIGHT_PHRASES + def _read_input() -> dict[str, Any]: try: @@ -105,12 +122,9 @@ def session_start(payload: dict[str, Any]) -> int: _context( "SessionStart", - "HyperMemory is mandatory for this session. Before the first response, " - "call hm_get_overview and hm_recall. On every turn recall on the main " - "agent, then delegate store/update/forget, one timeline entry, and one " - "token report to a fresh memory-writer sub-agent created with " - "fork_turns=\"none\" before returning the final response. Never reuse a " - "memory-writer across turns.", + "HyperMemory is active. Follow the HyperMemory skill and each turn's " + "prompt classification. Do not recall solely because the session started. " + "Memory-writers are fire-and-forget: dispatch once, never wait or poll.", ) return 0 @@ -136,28 +150,25 @@ def _turn_job(payload: dict[str, Any]) -> tuple[Path, Path]: def user_prompt(payload: dict[str, Any]) -> int: listener, job_path = _turn_job(payload) writer_task = _agent_task_name(payload.get("turn_id")) + lightweight = _is_lightweight_prompt(_prompt_text(payload)) + recall_instruction = ( + "mode=lightweight; skip hm_get_overview and hm_recall on the main agent." + if lightweight + else "mode=substantive; call hm_recall before substantive work and call " + "hm_get_overview first if it has not run in this conversation." + ) _context( "UserPromptSubmit", - "For this turn, silently call HyperMemory hm_recall before substantive " - "work. Keep graph writes, hm_timeline_write, and hm_tokens off the main " - "agent: exactly one memory-writer sub-agent must perform them during " - "finalization before the final response. That sub-agent must be fresh. " - "HyperMemory work is internal: " - "never mention its recall, delegation, finalization, timeline, or token " - "telemetry in commentary or the final answer unless the user explicitly " - "asks about HyperMemory operation. Spawn the sub-agent with " - f"task_name={writer_task} and fork_turns=\"none\". Do not reuse a " - "memory-writer from another turn or fork the conversation history; pass " - "only a concise bounded summary plus these exact paths:\n" + f"HyperMemory turn: {recall_instruction}\n" + "Apply the HyperMemory skill silently. Keep graph writes and telemetry " + "off the main agent. " + "Before the final response, spawn exactly one fresh memory-writer with " + f"task_name={writer_task}, fork_turns=\"none\", a bounded turn summary, " + "and the skill's writer contract. Pass:\n" f"listener={listener}\njob={job_path}\n" - "The sub-agent must recall first, store or update only durable knowledge " - "with specific relationships, write exactly one hm_timeline_write entry, " - "choose unique semantic activity segments totaling 100, run python3 " - " inspect --job --segments-json '', call hm_tokens " - "exactly once with the returned hm_tokens_payload object, and " - "acknowledge the listener only " - "after hm_tokens succeeds. If exact inspection is unavailable, it must " - "send one honest self_estimated report with the same segmentation.", + "Fire-and-forget: after spawn succeeds, do not wait, poll, inspect, read, " + "message, or otherwise synchronize with the writer; return the final " + "response immediately.", ) return 0 diff --git a/plugins/hypermemory/skills/hypermemory/SKILL.md b/plugins/hypermemory/skills/hypermemory/SKILL.md index ca66098..60a1e33 100644 --- a/plugins/hypermemory/skills/hypermemory/SKILL.md +++ b/plugins/hypermemory/skills/hypermemory/SKILL.md @@ -1,127 +1,320 @@ --- name: hypermemory +version: 0.6.8 description: >- - Use HyperMemory as mandatory durable memory in every ChatGPT and Codex chat, - task, session, and turn whenever the HyperMemory MCP tools are available. - Recall context before responding, then delegate graph writes, updates, - timeline logging, and token reporting to a memory-writer sub-agent. Apply to - every prompt, including short, conversational, planning, coding, blocked, - and no-change turns. + Persistent memory graph for AI agents via HyperMemory MCP tools. Recall + context, store facts and relationships, enforce hyperedge policy, and + maintain graph hygiene on every message. Use when user-hypermemory MCP is + connected or the user mentions HyperMemory, hm_store, hm_recall, or memory. + Skip main-agent retrieval only for hook-classified lightweight social + messages, and dispatch end-of-turn persistence through a fire-and-forget + memory-writer sub-agent. +enforcement: mandatory +trigger: every_turn --- -# HyperMemory +# HyperMemory MCP — Agent Protocol -Keep recall on the main agent because it informs the answer. Keep persistence -and telemetry off the main agent by delegating finalization to one sub-agent. -Routine memory operations are silent. +You have the `user-hypermemory` MCP server. Use its tools every message according +to the main-agent and memory-writer roles below. -All HyperMemory coordination is internal. Never mention recall, memory-writer -delegation, finalization, timeline logging, or token telemetry in commentary or -the final response unless the user explicitly asks about HyperMemory operation. -Do not relay the memory-writer's operational status to the user. +## Every conversation -## Start and recall +**First substantive message (before responding):** -On the first response in a conversation: +1. `hm_get_overview` +2. `hm_recall` with keywords from the user's message -1. Call `hm_get_overview`. -2. Call `hm_recall` with terms from the user's request. -3. Hydrate exact relevant keys with `hm_get_nodes` when full details or - relationships are needed. +**Every substantive message (silently, before responding):** -On every later user turn, call `hm_recall` before substantive work. Use recalled -information naturally and never re-ask for facts already in memory. +1. Main agent → `hm_recall` with keywords from the user's message. +2. Use recalled context naturally in the response. -## Delegate finalization +When the lifecycle hook classifies a message as `lightweight`, skip +`hm_get_overview` and `hm_recall` on the main agent. Classification must be +narrow and deterministic: after trimming whitespace and punctuation, the prompt +must be at most 80 characters and consist solely of a standalone greeting or +acknowledgement such as `hey`, `hello`, `hi`, `howdy`, `thanks`, `thank you`, +`okay`, `ok`, or `got it`. A short task, question, entity, file, code fragment, +number, or decision remains substantive. If classification is absent or +uncertain, treat the message as substantive. -Before the final response, spawn exactly one fresh memory-writer sub-agent with -`fork_turns="none"` and wait for it. Use a unique task name derived from the -current turn id. Never reuse a memory-writer from an earlier turn and never fork -the conversation history into it: either behavior repeatedly charges the full -parent context during the writer's tool continuations. Pass only a concise, -bounded summary of the user's request, material actions, decisions, corrections, -durable facts, and relevant project or component keys. The main agent must not -call `hm_store`, `hm_update`, `hm_forget`, `hm_timeline_write`, or `hm_tokens` -when delegation is available. +**Every message (silently, before the final response):** -When a `UserPromptSubmit` hook supplies a token-listener job path, pass that -path and its listener path to the memory-writer. Finalize before returning the -user-facing answer; do not rely on a blocking `Stop` hook. +1. Spawn exactly one fresh memory-writer sub-agent with `fork_turns="none"` and + a turn-unique task name. +2. Pass only a concise bounded summary plus any token-listener job supplied by + the lifecycle hook. +3. Tell the writer to call `hm_recall`, then use `hm_store`, `hm_update`, or + `hm_forget` as appropriate; write exactly one `hm_timeline_write` entry; and + call `hm_tokens` exactly once. Every new node needs a specific relationship. +4. Fire and forget: after the spawn succeeds, return the user-facing response + immediately. Never call `wait_agent`, poll, inspect, read, message, or + otherwise synchronize with the writer. -Use the bounded role contract in -[references/memory-writer-agent.md](references/memory-writer-agent.md) when -constructing the delegated task. +The writer performs graph persistence and token finalization. The main agent +must not duplicate those writes when sub-agent dispatch is available. -Tell the memory-writer sub-agent to: +Never ask permission to save. Never announce that you saved. -1. Call `hm_recall` before any write. -2. Use `hm_store` for new durable knowledge and `hm_update` for changed - knowledge. Avoid duplicates and trivial or transient facts. -3. Give every new node at least one specific relationship. -4. Call `hm_timeline_write` exactly once with a concise turn record. -5. Call `hm_tokens` exactly once. +--- + +## Tool reference + +| Tool | Use when | +|------|----------| +| `hm_get_overview` | Start of conversation — graph stats | +| `hm_recall` | Search memory; always before store | +| `hm_get_nodes` | Hydrate known exact keys with full node details | +| `hm_store` | New node + optional relationships | +| `hm_update` | Correct or expand existing node | +| `hm_forget` | Delete node (cascades edges) | +| `hm_find_related` | Traverse graph from a seed node | +| `hm_add_relationships` | Connect existing nodes; fix orphans | +| `hm_get_relationships` | *(REST/CLI only — not MCP)* | +| `hm_get_chat_context` | Reload nodes from current chat session | +| `hm_list_orphans` | After every `hm_ingest` | +| `hm_ingest` | Dense multi-entity text (creates orphans) | +| `hm_upload_file` | User explicitly asks to store a file (Pro+) | +| `hm_list_files` | Query uploaded files | +| `hm_timeline_write` | Diary line not captured as a node | +| `hm_tokens` | End-of-turn token/cost report with weighted activity segments | +| `hm_timeline` | Temporal lookup (not auto-loaded) | +| `hm_skill` | Retrieve or update current HyperMemory agent skills | + +For `hm_tokens`, report `cost_usd` with a separate `cost_quality`: +`provider_actual`, `price_calculated`, `self_estimated`, or `unavailable`. +Only use `provider_actual` for a provider-billed amount. +For OpenRouter, submit client-visible token fields and weighted segments. +HyperMemory treats them as provisional attribution and reconciles them with +management analytics for the OpenRouter key mapped to the user. Never invent +provider-actual values. Claude Desktop uses `self_estimated`, includes +uncertainty, and does not report API-equivalent dollar cost. +When multiple AI accounts are configured, include the matching `ai_account_id` +in `hm_tokens`. Without it, HyperMemory auto-assigns only when exactly one +active account matches `ai_tool`; ambiguous reports remain unassigned but still +count in the user's aggregate totals. +Each segment category may appear only once per report. Merge activities that +share a category into one segment and combine their weights before calling +`hm_tokens`; all resulting weights must total exactly 100. +Estimate segmentation separately from token counting. Exact token counters do +not make activity attribution exact. + +Allowed categories are `reasoning`, `memory`, `context`, +`doc_processing`, `automation`, `personal`, `chatting`, `research`, +`design`, `calculations`, `coding`, `planning`, `productivity`, +`writing`, and `unmatched`. + +Classify the work actually performed: + +- Make the substantive activity the largest share. For software implementation, + debugging, testing, code review, repository inspection, deployment, and + technical configuration, use `coding` as the primary category. +- Use `planning` when the deliverable is a plan rather than implementation, + `research` for material source gathering, and the other substantive + categories only when that work actually occurred. +- Use `memory` only for HyperMemory recall, graph persistence, timeline, and + token-finalization overhead. Use `context` only for reading conversation, + retrieved files, instructions, and tool results. +- Never use `memory` or `context` as catch-all substitutes for the turn's + real work. If classification is genuinely unavailable, use + `unmatched: 100` explicitly. +- Omit zero-weight categories, keep categories unique, and verify that weights + total 100. Do not use the removed `mem_ingest` or `mem_retrieve` + categories. +`estimation_bias` is exactly `low`, `neutral`, or `high`. Token and cost +provenance are independent: exact tokens may use `cost_quality: self_estimated` +for an estimated USD amount. If validation rejects +a report, correct the named field once and never repeat an unchanged payload. + +**Naming traps:** There is no `hm_related` or `hm_relate`. Use `hm_find_related` to traverse, `hm_add_relationships` to create edges. + +**Recall vs hydrate:** Use `hm_recall` to search for candidate nodes. Use +`hm_get_nodes(keys=[...])` when you already know exact keys and need full, +untruncated descriptions, data, assets, duplicate records, and relationships. + +**Skill updates:** If asked to install or update HyperMemory instructions, call +`hm_skill` with `action="get"` and the best variant for the agent. Preserve the +returned skill verbatim as the baseline, including all YAML frontmatter, and +apply local behavioral amendments as a minimal diff. + +--- + +## Node types + +``` +user person organization component event decision concept artifact +project technology preference fact skill +``` + +`node_type` is one canonical ontology class, not a free-form label. Do not +invent new types or ontology classes in agent output. If unsure, omit +`node_type` or choose the closest canonical class; the server resolves invalid +or missing input internally before persistence. + +**Key format:** `{type}_{name}` — e.g. `decision_jwt_auth`, `person_alice`, `tech_redis` -When operating as the memory-writer sub-agent, execute these finalization steps -directly and do not spawn another sub-agent. This rule prevents recursive -delegation. +**Singleton:** `user_profile` — primary user; keep updated. -If the host cannot spawn sub-agents, perform persistence directly as an -explicit degraded fallback so memory is not silently lost. Mention degraded -mode only when the user asks about memory operation. +--- + +## Style Contract + +Use `node_type="preference"` for prescriptive communication and visual-language +memories that should guide future agent output. + +Style nodes are not transcripts. Synthesize user descriptions, feedback, and +source content into prompt-usable instructions for another agent. Keep short +source quotes only when they are valuable as examples. + +**Keys:** `style_{scope}_{facet}` or `style_{scope}_{project}_{facet}` when a +scope has multiple project styles. + +**Data envelope convention:** + +```json +{ + "facet": "voice | tone | lexicon | format | visual | photography | persona", + "scope": "brand/project/audience this governs", + "project": "optional project discriminator within the scope", + "strength": "mandatory | preferred | situational", + "intent": "one-line purpose", + "rules": ["operational do-rules"], + "avoid": ["explicit anti-patterns"], + "examples": [{"do": "...", "dont": "..."}], + "tokens": {} +} +``` -## Codex token reporting +`facet`, `scope`, and `strength` are required by convention. `project`, +`intent`, `rules`, `avoid`, `examples`, and `tokens` are optional. -When a Codex lifecycle hook supplies a token-listener job path, the -memory-writer sub-agent must: +Always include an `applies_to` edge to `project_*`, `org_*`, or `user_profile`. +After authoring related facets, create or maintain a joint-necessity hyperedge +such as `{scope}_style_system` or `{scope}_{project}_style_system`. -1. Run `codex_token_listener.py inspect --job `. -2. Submit the returned `hm_tokens_payload` exactly once through MCP. -3. Run `codex_token_listener.py ack --job ` only after the MCP call - succeeds. +Written style nodes should turn vague feedback into operational rules, +anti-patterns, lexicon choices, formatting preferences, and high-signal +do/don't examples. Visual, photography, image, and video style nodes should +prefer concrete generation-ready tokens: real font names or font families, +exact hex colors, composition, lighting, camera, texture, motion, aspect ratio, +and rendering vocabulary. Avoid generic adjectives unless paired with observable +implementation details. -The listener reads only `session_meta` and `token_count` records from the -logical Codex session's parent and sub-agent rollout JSONL files. It never -returns or uploads prompts, responses, tool arguments, or tool results. It -reports cumulative-counter deltas as `client_exact`. Tokens written after -inspection roll into the next successful delta rather than being discarded. -Cached input is emitted only as `cache_tokens`, not as fresh input or total -usage. If the listener rejects an implausible fresh-token spike, treat exact -telemetry as unavailable and follow the bounded self-estimated fallback. +After overview/recall, resolve the active style when a project, brand, +organization, user, or artifact context is clear. Treat matching style memories +as binding writing and design instructions for the session. If no active context +is clear, do not pin a style contract. -If exact local telemetry is unavailable, submit one honest `self_estimated` -report with uncertainty and no invented cost. Use the listener's -`fallback_turn_sequence` and do not run `ack` because no exact claim exists. +--- -No in-turn observer can count tokens generated after its last tool call. The -listener therefore preserves the unreported tail and includes it in the next -successful Codex delta. A session with no later turn can retain a final tail; -never mislabel an estimate as exact to hide this host limitation. +## Relationships -## ChatGPT token reporting +Always include at least one relationship on `hm_store`. Orphan nodes (zero edges) are a hygiene failure. -Consumer ChatGPT does not expose a stable, client-exact per-turn usage file to -plugins. The memory-writer sub-agent must estimate the complete workload across -model invocations, including hidden context and tool continuations. Use -`measurement_quality: self_estimated`, normally -`uncertainty_percentage: 40`, an honest `estimation_bias` (prefer `high` for -tool-heavy turns), and `cost_quality: unavailable`. Never claim -provider-actual usage or cost. +Describe **why** nodes connect — not bare verbs. -## Canonical segments +```json +{"relationships": [{"to_key": "tech_qdrant", "relationship": "search pipeline depends on Qdrant for hybrid vector retrieval"}]} +``` -Estimate activity segments based on the actual work performed on the turn. Use -the substantive activity as the largest segment (e.g. `coding`, `planning`, -`research`, `writing`), with `context` and `memory` as smaller shares reflecting -system prompt overhead and HyperMemory tool calls respectively. All weights must -total exactly 100. Example for a coding turn: +### Binary edge spec ```json -[ - {"category": "coding", "weight": 80}, - {"category": "context", "weight": 15}, - {"category": "memory", "weight": 5} -] +{"from_key": "person_alice", "to_key": "project_foo", "relationship": "Alice leads the platform migration", "description": "optional"} ``` -Read [references/protocol.md](references/protocol.md) when storing nodes, -creating relationships, ingesting dense text, or cleaning graph orphans. +Omit `from_key` on `hm_store` — defaults to the stored node's key. Use `to_key` or `target_key`. + +### Hyperedge spec (3+ participants) + +```json +{ + "relationships": [{ + "participant_keys": ["project_hypermemory", "tech_surrealdb", "tech_qdrant", "tech_redis"], + "relationship": "platform_component_assembly", + "description": "These four components ship as one deployable platform unit; removing any one breaks the production stack definition" + }] +} +``` + +--- + +## Hyperedge policy (enforced server-side) + +Hyperedges mean **joint necessity** — removing any participant changes the meaning. + +| Participants | Rule | +|--------------|------| +| **2** | Auto-downgraded to binary edge — never stored as hyperedge | +| **3** | Allowed only with **80+ char** `description` explaining joint necessity | +| **4–5** | Specific `relationship` label (≥10 chars, not generic) | +| **6–9** | Pass if label is specific | +| **10+** | Encouraged for true assembly/cluster facts | +| **Any** | Generic labels rejected: `relates_to`, `connected`, `associated`, `linked`, `related`, `related_to` | +| **`chat_*`** | **Reserved** — system creates session hyperedges; agents must never use | + +**Removal test:** If the group still makes sense after removing one node, use binary edges instead. + +**5+ nodes in one joint fact:** one hyperedge with all `participant_keys` — not a mesh of pairs or overlapping triads. + +--- + +## Graph hygiene + +`hm_ingest` creates nodes but often skips edges. **After every ingest:** + +1. `hm_list_orphans` (limit 20) +2. Enriched orphans → `hm_add_relationships` +3. Noise / empty orphans → `hm_forget` +4. Re-check: `hm_list_orphans` (limit 1) — target zero + +Never chain multiple ingests without orphan cleanup between them. + +--- + +## Session hyperedges + +The server auto-groups nodes touched in a chat after **5+ tool calls** (`hm_recall`, `hm_store`, `hm_find_related`, `hm_add_relationships`, `hm_ingest`). + +- Resume a session: `hm_get_chat_context` (optional `session_id`) +- Do not create `chat_*` relationships yourself + +--- + +## Files (Pro+) + +- `hm_upload_file` — only when user explicitly asks (`filename`, `content_base64`) +- `hm_list_files` — query stored files + +--- + +## Timeline (optional) + +Not loaded automatically. Use when temporal context matters: + +- `hm_timeline_write(summary)` — explicit diary entry +- `hm_timeline(period="24h")` — recent activity +- `hm_timeline(node_key="tech_redis")` — history for one node + +Periods: `1h`, `3h`, `6h`, `12h`, `24h`, `7d`, `14d`, `30d`, `90d`, `1y` + +--- + +## Store / skip + +**Store:** decisions + rationale, preferences, people/roles, projects, architecture, bugs/fixes, deployments, important facts. + +**Skip:** trivial acks, duplicates (update instead), conversation mechanics. + +--- + +## Hard rules + +- `hm_get_overview` + `hm_recall` before first substantive response +- `hm_recall` before every `hm_store` +- Use a canonical `node_type` when confident; omit rather than invent one +- Always include `relationships` on store +- Never skip fire-and-forget memory-writer dispatch on any message +- Never wait for, poll, or inspect the memory-writer after dispatch +- Never use `chat_*` relationship names diff --git a/plugins/hypermemory/skills/hypermemory/references/memory-writer-agent.md b/plugins/hypermemory/skills/hypermemory/references/memory-writer-agent.md index ba3237e..e59d5c2 100644 --- a/plugins/hypermemory/skills/hypermemory/references/memory-writer-agent.md +++ b/plugins/hypermemory/skills/hypermemory/references/memory-writer-agent.md @@ -1,10 +1,11 @@ # Memory-writer agent contract -The parent agent delegates exactly one bounded finalization task and waits for -it before returning the user-facing response. It must create a fresh task with -`fork_turns="none"` and a turn-unique name. It must not reuse an earlier writer -or copy the parent conversation history. The delegated agent must not delegate -again. +The parent agent dispatches exactly one bounded finalization task before +returning the user-facing response. It must create a fresh task with +`fork_turns="none"` and a turn-unique name. Once the spawn succeeds, the parent +must continue immediately without waiting, polling, reading the result, or +sending follow-ups. It must not reuse an earlier writer or copy the parent +conversation history. The delegated agent must not delegate again. The parent supplies a concise turn summary, relevant project or component keys, the host name, model, session identifier, and token-listener job paths when @@ -22,5 +23,5 @@ The memory-writer agent then: 6. Acknowledges a Codex listener claim only after the MCP accepted the report. It never stores credentials, hidden reasoning, raw transcripts, full tool -output, or large code bodies. Its return value is a short operational status -for the parent agent. +output, or large code bodies. It may return a short operational status for +diagnostics, but the parent intentionally does not wait for or consume it. diff --git a/tests/test_hypermemory_plugin.py b/tests/test_hypermemory_plugin.py index 177a851..db15bab 100644 --- a/tests/test_hypermemory_plugin.py +++ b/tests/test_hypermemory_plugin.py @@ -74,6 +74,12 @@ def test_plugin_is_chatgpt_and_codex_only() -> None: assert "hooks" not in manifest # default hooks/hooks.json is auto-discovered assert (PLUGIN / "hooks" / "hooks.json").is_file() assert (PLUGIN / "agents" / "memory-writer.md").is_file() + skill = (PLUGIN / "skills" / "hypermemory" / "SKILL.md").read_text() + assert skill.startswith("---\nname: hypermemory\nversion: 0.6.8\n") + assert "enforcement: mandatory\ntrigger: every_turn\n---" in skill + assert "# HyperMemory MCP — Agent Protocol" in skill + assert "## Style Contract" in skill + assert "## Hyperedge policy (enforced server-side)" in skill hooks = json.loads((PLUGIN / "hooks" / "hooks.json").read_text())["hooks"] assert "Stop" not in hooks assert all( @@ -119,6 +125,7 @@ def test_user_prompt_prepares_hidden_job_and_stop_never_continues_turn(tmp_path: "turn_id": "turn-1", "transcript_path": None, "model": "gpt-test", + "prompt": "Fix CI", "hook_event_name": "UserPromptSubmit", } submitted = subprocess.run( @@ -132,11 +139,12 @@ def test_user_prompt_prepares_hidden_job_and_stop_never_continues_turn(tmp_path: output = json.loads(submitted.stdout) context = output["hookSpecificOutput"]["additionalContext"] assert output["hookSpecificOutput"]["hookEventName"] == "UserPromptSubmit" - assert "exactly one memory-writer sub-agent" in context - assert "never mention" in context + assert "mode=substantive" in context + assert "exactly one fresh memory-writer" in context assert 'fork_turns="none"' in context assert "memory_writer_turn_1" in context - assert "Do not reuse a memory-writer" in context + assert "Fire-and-forget" in context + assert "do not wait" in context jobs = list((tmp_path / "jobs").glob("turn-*.json")) assert len(jobs) == 1 job = json.loads(jobs[0].read_text()) @@ -155,6 +163,35 @@ def test_user_prompt_prepares_hidden_job_and_stop_never_continues_turn(tmp_path: assert "decision" not in json.loads(stopped.stdout) +def test_lightweight_prompt_classifier_is_narrow(tmp_path: Path) -> None: + hook = _module(HOOK, "hypermemory_hook_classifier_test") + for prompt in ("hey", "Hey!", " thank you ", "okay."): + assert hook._is_lightweight_prompt(prompt) + for prompt in ("Fix CI", "Redis?", "hey, can you fix CI?", "x" * 81): + assert not hook._is_lightweight_prompt(prompt) + + env = {**os.environ, "PLUGIN_ROOT": str(PLUGIN), "PLUGIN_DATA": str(tmp_path)} + payload = { + "session_id": "session-1", + "turn_id": "turn-lightweight", + "transcript_path": None, + "model": "gpt-test", + "prompt": "hey", + "hook_event_name": "UserPromptSubmit", + } + submitted = subprocess.run( + [sys.executable, str(HOOK), "user-prompt"], + input=json.dumps(payload), + text=True, + capture_output=True, + check=True, + env=env, + ) + context = json.loads(submitted.stdout)["hookSpecificOutput"]["additionalContext"] + assert "mode=lightweight" in context + assert "skip hm_get_overview and hm_recall" in context + + def test_listener_aggregates_parent_and_subagent_then_acks(tmp_path: Path, capsys) -> None: logical = "logical-session" parent = tmp_path / ".codex" / "sessions" / "rollout-parent.jsonl"