Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ AMB takes a smaller path: local SQLite, explicit namespaces, inspectable records
- Coordination signals: `claim -> extend -> ack / expire / reclaim` without pretending to be a scheduler.
- Governed learning: runtime learning can be staged as policy-gated learning candidates before promotion into durable records.
- Context assembly: startup and task-time context can be compiled from procedures, concepts, beliefs, gotchas, and linked support without adding more MCP tools.
- Proof discipline: release contract checks, public-surface checks, onboarding checks, benchmark snapshots, and `228 passed`.
- Proof discipline: release contract checks, public-surface checks, onboarding checks, benchmark snapshots, and `243 passed`.

## Who It Is For

Expand Down Expand Up @@ -156,7 +156,7 @@ Some MCP clients generate one static input schema per tool and may send signal-o

## Proof Snapshot

`0.14.0` is the governed learning-candidate gate release while keeping the public tool surface stable.
`0.14.1` is the governed learning-candidate hardening release while keeping the public tool surface stable.

| Track | Current signal |
|---|---|
Expand All @@ -166,7 +166,7 @@ Some MCP clients generate one static input schema per tool and may send signal-o
| Learning candidates | policy-gated staging records are suppressed from normal recall, browse, export, and stats until explicitly reviewed |
| Signal contention | `signal_contention_case_pass_rate = 1.0`, `duplicate_active_claim_count = 0` |
| Adversarial memory governance | `adversarial_case_count = 6`, `adversarial_task_count = 7`, `adversarial_governed_task_pass_rate = 1.0`, `adversarial_governed_blocked_record_leak_rate = 0.0` |
| Test suite | `228 passed` |
| Test suite | `243 passed` |

<details>
<summary>Release contract facts</summary>
Expand Down
6 changes: 3 additions & 3 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ AMB 选择更小的路径:本地 SQLite、显式 namespace、可检查记录
- Coordination signals:`claim -> extend -> ack / expire / reclaim`,但不假装自己是 scheduler。
- Governed learning:runtime learning 可以先进入 policy-gated learning candidate staging,再经过 review/promotion 变成 durable records。
- Context assembly:startup 和 task-time context 可以从 procedure、concept、belief、gotcha 和 linked support 编译出来,不需要增加更多 MCP tools。
- Proof discipline:release contract、public-surface check、onboarding check、benchmark snapshot,以及 `228 passed`。
- Proof discipline:release contract、public-surface check、onboarding check、benchmark snapshot,以及 `243 passed`。

## 适合谁

Expand Down Expand Up @@ -156,7 +156,7 @@ bridge 暴露 `10` public MCP tools:

## Proof Snapshot

`0.14.0` 是 governed learning-candidate gate release,同时保持 public tool surface 稳定。
`0.14.1` 是 governed learning-candidate hardening release,同时保持 public tool surface 稳定。

| Track | Current signal |
|---|---|
Expand All @@ -166,7 +166,7 @@ bridge 暴露 `10` public MCP tools:
| Learning candidates | policy-gated staging records 默认不进入 normal recall、browse、export 和 stats,只有显式 review tag 才会出现 |
| Signal contention | `signal_contention_case_pass_rate = 1.0`, `duplicate_active_claim_count = 0` |
| Adversarial memory governance | `adversarial_case_count = 6`, `adversarial_task_count = 7`, `adversarial_governed_task_pass_rate = 1.0`, `adversarial_governed_blocked_record_leak_rate = 0.0` |
| Test suite | `228 passed` |
| Test suite | `243 passed` |

<details>
<summary>Release contract facts</summary>
Expand Down
12 changes: 6 additions & 6 deletions docs/PRODUCTION-STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

Last updated: 2026-05-26 (America/Toronto)

This maintainer note describes the released `0.14.0` governed learning-candidate shape plus the validation snapshot used to support it.
This maintainer note describes the released `0.14.1` governed learning-candidate hardening shape plus the validation snapshot used to support it.

## Released 0.14.0 Runtime Shape
## Released 0.14.1 Runtime Shape

`agent-memory-bridge` now has these cooperating layers:

Expand All @@ -23,7 +23,7 @@ This maintainer note describes the released `0.14.0` governed learning-candidate

## Verified On 2026-05-26

- `pytest` passes: `228 passed`
- `pytest` passes: `243 passed`
- targeted learning-candidate tests cover policy decisions, hidden review records, forged-decision rejection, and public-surface stability
- deterministic proof reports `4/4` checks passed
- deterministic proof and benchmark both report `relation_metadata_passed = true`
Expand Down Expand Up @@ -79,7 +79,7 @@ This maintainer note describes the released `0.14.0` governed learning-candidate
- the CLI can now render config snippets for generic stdio MCP, Codex, Cursor, Cline, Claude Code, Claude Desktop, and Antigravity
- `doctor` and `verify` provide local install confidence without touching live bridge state

## What 0.14.0 Actually Means
## What 0.14.1 Actually Means

- the public MCP surface is still the same small bridge
- runtime learning can be proposed as a policy-gated candidate instead of becoming ordinary durable memory immediately
Expand All @@ -102,7 +102,7 @@ The release still does **not** mean:
- exactly-once distributed coordination
- that every MCP client is fully verified just because the generic stdio contract is stable

## Pressure Points After 0.14.0
## Pressure Points After 0.14.1

The most important remaining gaps are:

Expand All @@ -116,7 +116,7 @@ The most important remaining gaps are:

## Maintainer Read

`0.14.0` keeps the public MCP surface small while adding the missing staging layer between runtime learning and durable AMB memory. The project now reads as a general MCP memory product with local proof for memory, task assembly, procedure governance, onboarding, signal ownership, and governed learning writeback.
`0.14.1` keeps the public MCP surface small while hardening the 0.14 governed-learning boundary between runtime learning and durable AMB memory. The project now reads as a general MCP memory product with local proof for memory, task assembly, procedure governance, onboarding, signal ownership, and governed learning writeback.

It now behaves like:

Expand Down
42 changes: 42 additions & 0 deletions docs/v0.14.1-announcement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# v0.14.1 - Governance Hardening

Agent Memory Bridge 0.14.1 hardens the 0.14 governed learning-candidate release after post-release review found schema, storage, and governance edge cases.

## Thesis

`0.14.1 = the 0.14 learning-candidate boundary made safer under static MCP schemas, concurrent SQLite startup, and promotion/review bypass attempts.`

## What Changed

- Normalized empty optional text fields from static-schema MCP clients at the server and storage boundaries.
- Allowed durable `kind="memory"` writes to tolerate placeholder empty `expires_at` while keeping direct `ttl_seconds` misuse strict.
- Normalized empty `claim_signal` filters so placeholder `signal_id`, `tags_any`, or `correlation_id` values do not accidentally block eligible pending signals.
- Hardened schema migrations against SQL identifier injection by validating table and column identifiers before `PRAGMA table_info(...)`.
- Made `ensure_column(...)` tolerate duplicate-column races from concurrent migration attempts.
- Added SQLite `PRAGMA busy_timeout=5000` on bridge connections.
- Wrapped FTS table rebuild in a savepoint and added a concurrency smoke regression.
- Added an indexed `is_learning_candidate` visibility column and legacy backfill so learning-candidate suppression is exact and no longer depends on broad string scans.
- Blocked direct `promote(...)` on learning-candidate records and conservative forged `record_type: learning-candidate` content.
- Added MCP-boundary regressions for static-schema `store`, `claim_signal`, `recall`, and `export` behavior.

## Evidence

Current release snapshot:

- `pytest`: `243 passed`
- targeted hardening subset: 59 targeted checks passed in `tests/test_v0141_hardening.py`, `tests/test_storage.py`, and `tests/test_learning_candidates.py`
- release contract: passed
- public surface contract: passed
- onboarding contract: passed
- `pip check`: passed
- public MCP tools: `10`

## Boundaries

The public MCP surface remains unchanged at `10` tools:

- `store`, `recall`, `browse`, `stats`
- `forget`, `promote`, `export`
- `claim_signal`, `extend_signal_lease`, `ack_signal`

This release does not turn AMB into a scheduler, distributed lock service, hosted backend, or automatic durable writeback path. The FTS startup regression is a smoke-level concurrency check, not a proof of all multi-process lock interleavings.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "agent-memory-bridge"
version = "0.14.0"
version = "0.14.1"
description = "Persistent engineering memory for coding agents over MCP"
readme = "README.md"
license = "MIT"
Expand Down
3 changes: 2 additions & 1 deletion src/agent_mem_bridge/learning_candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any

from .learning_policy import evaluate_learning_candidate
from .repository import LEARNING_CANDIDATE_TAG

VALID_CANDIDATE_STATUSES = {"pending", "needs_review", "approved", "rejected", "expired"}
STORABLE_DECISIONS = {"allow", "needs_review"}
Expand Down Expand Up @@ -54,7 +55,7 @@ def store_learning_candidate(
title_claim = truncate(claim or candidate_ref, limit=72)
content = build_learning_candidate_record(candidate, decision, candidate_status=status)
tags = [
"kind:learning-candidate",
LEARNING_CANDIDATE_TAG,
f"candidate_status:{status}",
f"source_runtime:{source_runtime}",
f"authority_class:{authority_class}",
Expand Down
11 changes: 9 additions & 2 deletions src/agent_mem_bridge/promotion.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
from typing import Any

from .repository import MemoryRow, fetch_row_by_id, merge_tags, normalize_content
from .repository import LEARNING_CANDIDATE_TAG, MemoryRow, fetch_row_by_id, merge_tags, normalize_content


PROMOTABLE_RECORD_TYPES = {"learn", "gotcha", "domain-note"}
Expand All @@ -26,6 +26,8 @@ def promote_entry(store: Any, memory_id: str, to_kind: str) -> dict[str, Any]:
raise ValueError("only kind=memory entries can be promoted")

source = MemoryRow.from_sqlite(row)
if is_learning_candidate_record(source):
raise ValueError("learning candidates cannot be promoted directly; review and store durable memory explicitly")
current_record_type = record_type_for_row(source)
if current_record_type == target_kind:
store._log("promote", {"id": cleaned_id, "changed": False, "reason": "already-target-kind"})
Expand Down Expand Up @@ -71,7 +73,7 @@ def promote_entry(store: Any, memory_id: str, to_kind: str) -> dict[str, Any]:
conn.execute(
"""
UPDATE memories
SET title = ?, content = ?, tags_json = ?, content_hash = ?
SET title = ?, content = ?, tags_json = ?, is_learning_candidate = 0, content_hash = ?
WHERE id = ?
""",
(
Expand Down Expand Up @@ -149,6 +151,11 @@ def record_type_for_row(row: MemoryRow) -> str:
return parse_structured_record(row.content).get("record_type", "memory")


def is_learning_candidate_record(row: MemoryRow) -> bool:
fields = parse_structured_record(row.content)
return LEARNING_CANDIDATE_TAG in row.tags or fields.get("record_type") == "learning-candidate"


def build_promoted_item(row: MemoryRow, *, target_kind: str, current_record_type: str) -> dict[str, Any]:
fields = parse_structured_record(row.content)
claim = fields.get("claim") or row.title or row.content
Expand Down
3 changes: 1 addition & 2 deletions src/agent_mem_bridge/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,7 @@ def build_filters(

include_learning_candidates = should_include_learning_candidates(tags_any)
if not include_learning_candidates:
clauses.append(f"{prefix}tags_json NOT LIKE ? ESCAPE '\\'")
params.append('%"kind:learning-candidate"%')
clauses.append(f"COALESCE({prefix}is_learning_candidate, 0) = 0")

if kind is not None:
clauses.append(f"{prefix}kind = ?")
Expand Down
14 changes: 11 additions & 3 deletions src/agent_mem_bridge/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
HASHTAG_RE = re.compile(r"(?<!\w)#([A-Za-z][A-Za-z0-9_/-]*)")
WIKILINK_RE = re.compile(r"\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]")
ALLOWED_KINDS = {"memory", "signal"}
LEARNING_CANDIDATE_TAG = "kind:learning-candidate"
MEMORY_ROW_SELECT = """
id,
namespace,
Expand All @@ -36,6 +37,7 @@
lease_expires_at,
expires_at,
acknowledged_at,
is_learning_candidate,
created_at
"""

Expand Down Expand Up @@ -63,6 +65,7 @@ class MemoryRow:
lease_expires_at: str | None
expires_at: str | None
acknowledged_at: str | None
is_learning_candidate: bool
created_at: str

@classmethod
Expand All @@ -89,6 +92,7 @@ def from_sqlite(cls, row: sqlite3.Row) -> "MemoryRow":
lease_expires_at=row["lease_expires_at"],
expires_at=row["expires_at"],
acknowledged_at=row["acknowledged_at"],
is_learning_candidate=bool(row["is_learning_candidate"]),
created_at=row["created_at"],
)

Expand Down Expand Up @@ -117,6 +121,7 @@ def as_dict(self) -> dict[str, Any]:
"lease_expires_at": self.lease_expires_at,
"expires_at": self.expires_at,
"acknowledged_at": self.acknowledged_at,
"is_learning_candidate": self.is_learning_candidate,
"created_at": self.created_at,
"relations": relation_metadata["relations"],
"valid_from": relation_metadata["valid_from"],
Expand Down Expand Up @@ -172,6 +177,7 @@ def store_entry(

normalized_content = normalize_content(cleaned_content)
payload_tags = merge_tags(tags, title=title, content=cleaned_content)
is_learning_candidate = int(LEARNING_CANDIDATE_TAG in payload_tags)
content_hash = hashlib.sha256(normalized_content.encode("utf-8")).hexdigest()
resolved_expires_at = resolve_signal_expiry(expires_at=expires_at, ttl_seconds=ttl_seconds)
signal_status = "pending" if cleaned_kind == "signal" else None
Expand Down Expand Up @@ -233,9 +239,10 @@ def store_entry(
lease_expires_at,
expires_at,
acknowledged_at,
is_learning_candidate,
content_hash,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
memory_id,
Expand All @@ -259,6 +266,7 @@ def store_entry(
None,
resolved_expires_at,
None,
is_learning_candidate,
content_hash,
created_at,
),
Expand Down Expand Up @@ -355,10 +363,10 @@ def stats_for_namespace(store: Any, namespace: str) -> dict[str, Any]:
{MEMORY_ROW_SELECT}
FROM memories
WHERE namespace = ?
AND tags_json NOT LIKE ? ESCAPE '\\'
AND is_learning_candidate = 0
ORDER BY created_at ASC
""",
(cleaned_namespace, '%"kind:learning-candidate"%'),
(cleaned_namespace,),
).fetchall()

kind_counts = {kind: 0 for kind in sorted(ALLOWED_KINDS)}
Expand Down
Loading
Loading