Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
19ad5e1
feat(runtime): add a decision model seam with opt-in and fail-open
AlexStocks Sep 25, 2026
35bf44e
test(runtime): satisfy the global type check for the decision seam
AlexStocks Sep 25, 2026
10b9505
feat(memory): gate writes on decision-model evidence sufficiency
AlexStocks Sep 25, 2026
99c79a5
fix(runtime): wire the memory write gate to configuration
AlexStocks Sep 25, 2026
f5bb829
refactor(runtime): drop the dead handoff escalation switch
AlexStocks Sep 26, 2026
c01cb53
test(memory): assert the write-gate warning event as a set membership
AlexStocks Sep 26, 2026
61e6aee
refactor(runtime): drop the unused handoff decision label
AlexStocks Sep 26, 2026
ecf92f0
fix(memory): surface write gate holds across memory paths
AlexStocks Sep 26, 2026
52f5efc
chore(api): regenerate memory flush response models
AlexStocks Sep 26, 2026
29d5500
fix(memory): send complete gate evidence
AlexStocks Sep 27, 2026
9640c2b
Merge remote-tracking branch 'origin/master' into feat/memory-write-gate
AlexStocks Sep 27, 2026
fd84faf
chore(runtime): merge master into memory write gate
AlexStocks Sep 27, 2026
f7667fd
fix(memory): preserve write gate evidence semantics
AlexStocks Sep 27, 2026
09437bc
Merge remote-tracking branch 'origin/master' into feat/memory-write-gate
AlexStocks Sep 28, 2026
b629207
test(topic-memory): stabilize R8 worker timeout
AlexStocks Sep 28, 2026
47e30df
fix(memory): preserve write-gate evidence boundaries
AlexStocks Sep 29, 2026
5f79b07
test(memory): align write-gate regressions
AlexStocks Sep 29, 2026
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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ POWERCONTEXT_SERVER_RUNTIME_DREAM_MAX_PENDING_PER_SCOPE=32
# is absent and makes no model call.
# POWERCONTEXT_SERVER_RUNTIME_DECISION_ASSISTANCE_ENABLED=true

# Memory write gate: opt-in evidence-sufficiency check before a write commits. Disabled by
# default; when unset no gate runs and no extra model call is made.
# POWERCONTEXT_SERVER_RUNTIME_MEMORY_WRITE_GATE_ENABLED=true
# Direction only (which verdict means the cited evidence is insufficient): "yes" or "no".
# POWERCONTEXT_SERVER_RUNTIME_MEMORY_WRITE_GATE_HOLD_ON=yes
# Optional confidence floor below which a hold becomes a written-but-annotated change.
# POWERCONTEXT_SERVER_RUNTIME_MEMORY_WRITE_GATE_THRESHOLD=0.5

# Recall-sufficiency gate. Disabled by default; enabling expands thin recall up to two rounds.
# POWERCONTEXT_SERVER_RUNTIME_RECALL_GATE_ENABLED=true
# POWERCONTEXT_SERVER_RUNTIME_RECALL_GATE_MAX_ROUNDS=2
Expand Down
11 changes: 11 additions & 0 deletions openapi/powercontext.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8153,6 +8153,17 @@ components:
memory:
$ref: "#/components/schemas/ArtifactReference"
nullable: true
held_count:
type: integer
minimum: 0
default: 0
description: Number of source windows held by the Memory write gate.
hold_codes:
type: array
items:
type: string
default: []
description: Structured Memory write gate refusal codes for held windows.
FlushTopicMemoryRequest:
type: object
additionalProperties: false
Expand Down
12 changes: 12 additions & 0 deletions src/powercontext/builtin/artifacts/memory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
MemoryEntryInactiveError,
MemoryEntryNotFoundError,
MemoryLayerError,
MemoryWriteRejectedError,
)
from powercontext.builtin.artifacts.memory.extraction import (
DefaultMemoryEvidenceProjector,
Expand Down Expand Up @@ -78,7 +79,12 @@
MemorySearchChannels,
MemorySearchRequest,
MemoryUnitOfWork,
MemoryWriteAssessment,
MemoryWriteGate,
MemoryWriteGateRequest,
MemoryWritePlan,
MemoryWriteRejectionCode,
MemoryWriteVerdict,
)
from powercontext.builtin.artifacts.memory.reranking import (
MEMORY_RERANK_INSTRUCTIONS,
Expand Down Expand Up @@ -158,7 +164,13 @@
"MemoryService",
"MemoryUnitOfWork",
"MemoryUsedSearchMode",
"MemoryWriteAssessment",
"MemoryWriteGate",
"MemoryWriteGateRequest",
"MemoryWritePlan",
"MemoryWriteRejectedError",
"MemoryWriteRejectionCode",
"MemoryWriteVerdict",
"memory_extraction_instructions",
"memory_extraction_instructions_version",
]
14 changes: 14 additions & 0 deletions src/powercontext/builtin/artifacts/memory/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,19 @@ def __init__(self, code: str) -> None:
super().__init__(messages.get(code, f"invalid memory citation: {code}"))


class MemoryWriteRejectedError(MemoryLayerError, RuntimeError):
"""A structured, caller-visible refusal to apply one Memory write.

``code`` and ``reason`` carry the gate's decision to the host, so a refused write is
observable rather than silently dropped.
"""

def __init__(self, code: str, reason: str | None = None) -> None:
self.code = code
self.reason = reason
detail = "" if reason is None else f": {reason}"
super().__init__(f"memory write was rejected ({code}){detail}")


class MemoryBackendConfigurationError(MemoryLayerError, RuntimeError):
"""Raised when a repository cannot satisfy its declared configuration."""
65 changes: 64 additions & 1 deletion src/powercontext/builtin/artifacts/memory/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
from __future__ import annotations

from contextlib import AbstractAsyncContextManager
from dataclasses import dataclass
from enum import StrEnum
from typing import Protocol

from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict

from powercontext.artifacts import Artifact, ArtifactRef
from powercontext.builtin.artifacts.memory.models import (
Expand Down Expand Up @@ -65,11 +67,72 @@ class MemoryCommit(BaseModel):
projections: tuple[MemoryProjection, ...]


class MemoryWriteRejectionCode(StrEnum):
"""Structured, caller-visible vocabulary for a held Memory write.

Names mirror the evidence-selection vocabulary so a host can branch on one stable set of
codes instead of parsing prose.
"""

NEEDS_EVIDENCE = "needs_evidence"
EVIDENCE_LIMIT_EXCEEDED = "evidence_limit_exceeded"
INSUFFICIENT_COVERAGE = "insufficient_coverage"


class MemoryWriteVerdict(StrEnum):
"""The complete verdict vocabulary a Memory write gate may produce."""

ACCEPT = "accept"
FLAG = "flag"
HOLD = "hold"


class MemoryWriteAssessment(BaseModel):
"""One gate verdict with the structured refusal a caller can observe.

``HOLD`` always carries both a ``code`` and a ``reason``: a refused write is visible to
its caller, never silently dropped. ``ACCEPT``/``FLAG`` leave ``code`` unset.
"""

model_config = ConfigDict(frozen=True)

verdict: MemoryWriteVerdict
policy_id: str
code: MemoryWriteRejectionCode | None = None
reason: str | None = None
used_fallback: bool = False


@dataclass(frozen=True, slots=True)
class MemoryWriteGateRequest:
"""A bounded projection of one pending Memory write for sufficiency judgement."""

candidates: tuple[str, ...]
evidence: tuple[str, ...]
expected_revision: int | None = None


class MemoryWriteGate(Protocol):
"""Judge whether a pending Memory write is supported by its cited evidence.

A gate only classifies: it never writes, approves, rejects, or deletes anything. A missing
or failing gate must be treated by callers as a pass-through, never as a hold.
"""

policy_id: str

async def assess(self, request: MemoryWriteGateRequest, /) -> MemoryWriteAssessment:
"""Return one verdict for a candidate set and its bounded evidence projection."""

...


class MemoryWritePlan(BaseModel):
"""A side-effect-free result that can be committed in an outer transaction."""

result: Memory | None
commit: MemoryCommit | None
decision: MemoryWriteAssessment | None = None


class MemorySearchRequest(BaseModel):
Expand Down
Loading
Loading