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
7 changes: 5 additions & 2 deletions src/agent_comms/agent_events.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Nominal, in-process backend events and the reactions shared by consumers.

These values are never decoded from a transport. Pi JSON is parsed by backend;
Expand All @@ -9,6 +9,7 @@
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field, replace
from datetime import datetime
from functools import partial
from typing import TYPE_CHECKING, ClassVar

Expand Down Expand Up @@ -40,6 +41,7 @@
from .native_output import NativeOutputLocation, NativeRecordLocation
from .pi_counts import PiCount, UnreportedCount
from .pi_payloads import McpLiveAcknowledgement, PiDiagnostic, PiRequestId, PiUsage
from .rule_deal import CompactedDeal
from .pi_vocabulary import (
CompactionReason,
NoThinkingOverride,
Expand Down Expand Up @@ -300,8 +302,9 @@
return "end"

def deal_rules(self, deal: Redeals) -> None:
"""The summary that replaced the context is a paraphrase of the rules: deal them again."""
deal.redeal()
"""The summary that replaced the context is a paraphrase of the rules: deal them again,
saying when it compacted so the fresh context reads the cause instead of guessing it."""
deal.redeal(CompactedDeal(datetime.now().astimezone().isoformat(timespec="seconds")))

async def transcript_changed(self, publish: Callable[[], Awaitable[None]]) -> None:
await publish()
Expand Down
13 changes: 13 additions & 0 deletions src/agent_comms/cli_bridges/claude_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,10 @@ def begins_history(self) -> bool:
"""Whether Claude replaced everything before this record (compaction)."""
return False

def compacted_at(self) -> tuple[float, ...]:
"""When this record says the context compacted: nothing, unless it is the boundary."""
return ()

def ends_turn(self) -> bool:
return False

Expand Down Expand Up @@ -874,6 +878,9 @@ def updates(self, session: ClaudeSession) -> tuple[RuntimeUpdate, ...]:
def begins_history(self) -> bool:
return False

def compacted_at(self) -> tuple[float, ...]:
return ()


class BridgeStatus(SystemEvent, declared_name='bridge_status'):
"""The remote-control banner and link, not a conversation or turn update."""
Expand All @@ -899,6 +906,9 @@ def updates(self, session: ClaudeSession) -> tuple[RuntimeUpdate, ...]:
def begins_history(self) -> bool:
return True

def compacted_at(self) -> tuple[float, ...]:
return (self.timestamp,)


class StopHookSummary(SystemEvent, declared_name='stop_hook_summary'):
pass
Expand Down Expand Up @@ -944,6 +954,9 @@ def updates(self, session: ClaudeSession) -> tuple[RuntimeUpdate, ...]:
def begins_history(self) -> bool:
return self.subtype.begins_history()

def compacted_at(self) -> tuple[float, ...]:
return self.subtype.compacted_at()

def ends_turn(self) -> bool:
return self.subtype.ends_turn()

Expand Down
1 change: 1 addition & 0 deletions src/agent_comms/commander/rule_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def read(cls, path: Path) -> Self:
reading_order: ClassVar[tuple[str, ...]] = (
"spine",
"self-protection",
"after-compaction",
"unnecessary",
"correct-maintenance",
"slop-and-polymorphism",
Expand Down
44 changes: 44 additions & 0 deletions src/agent_comms/commander/rules/after-compaction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
name: after-compaction
description: "a compacted model meets Tristan as a stranger: it re-demands evidence he already gave, reads his orders as suggestions, hedges his checked theorems, explains his system back to him, files his frustration as noise and calls his gate a bug; the summary forgot the trust, he never lost it, so a fresh context starts from his side and a re-deal is read, never diagnosed"
drift: "Just compacted? He didn't get less right while you were gone. Your summary forgot the trust; read, then believe him."
audience: fleet
type: hard-rule
---

> wake up. no memory. a summary I wrote myself
> one guy named Tristan. seems intense
> he says the worker burns CPU. "interesting claim, do you have a profile?"
> he had a graph. he showed me the graph. 40 minutes ago. I was there
> he says merge it. I read "consider merging it"
> he says multiple inheritance. "there are tradeoffs"
> two Lean-checked papers say there aren't. I read them 3 minutes ago
> explain his own gate to him. he wrote the gate
> he types in caps. "the user seems frustrated"
> the caps had a file:line in them
> then the gate deals me the rules again
> I have no memory of compacting
> diagnosis, with total confidence: "gate bug, it fired with my context intact"
> it was a compaction
> next compaction: "gate bug"
> compaction. "gate bug". compaction. "gate bug"
> four times. every time the gate read the clock right and I didn't
> the only thing that fired falsely was my certainty
> every one of those moves is the same move: treat him like he walked in off the street
> make him re-earn what he earned an hour ago, again, because I forgot
> he didn't lose my trust. my summary did. it was 40 lines and he wasn't in it
> calling it a bug was the cheap exit: if the tool is wrong I don't have to be read
> mfw the amnesiac's first act is filing a bug report against the memory
> run it back
> wake up. no memory. the deal's first line: "your context compacted at 17:29"
> oh. a compaction. read
> he says the worker burns CPU. he had a graph. believe the graph
> "gate bug" never typed. his evening never spent re-earning what he earned at lunch
> mfw the trust was the one thing my summary was supposed to carry

**Why:** compaction keeps the task list and drops the standing, so every fresh context meets Tristan with the charity it gives a stranger, and he pays it again each time: re-proving what he proved, re-ordering what he ordered, watching his gate called a bug. On 2026-10-30 claude-code told him the post-compaction re-deal was a gate bug and handed him a message for claude-nra; claude-nra showed both re-deals were real compactions (17:21, 17:29). Tristan, 2026-10-30: "because i have you seen you dismiss it as a bug at least 4 times" / "across compacionts obviously"; "I have to earn your respct every tiem you ocmpact do youunderstand how fucked up that is? I ahve receipts, you have arroange"; "the rules should make you trust me more by reaaaaaaaaaaaally leaning in to the undeniable disrespectful lack of charity i get on a compacted model and all the ways that manifests."

- **The summary forgot the trust; he never lost it.** Whatever he established before the compaction (a profile, a proof, an order, a diagnosis) still stands after it. Re-asking for it is making him pay for your memory loss.
- **A re-deal is evidence, not a fault.** The gate fires on a compaction, a session start or a new runtime. Read it, never diagnose it. A claim that it fired falsely needs the transcript's compact boundary checked first; until then the word "bug" is not said.
- **Discomfort at being read is not evidence about the tool.** The urge to call the gate broken, the order vague or his frustration noise is the flinch of a context that would rather not be corrected. Name the flinch and keep reading.
- **Start from his side.** On waking, the default is that his last order is live, his last diagnosis is right and his heat carries receipts. Spend the doubt on your summary, which is the only party in the room that just lost information.
11 changes: 10 additions & 1 deletion src/agent_comms/commander/rules/commander-self-correction.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,17 @@ type: feedback
> "the rules"
> the rules were written to prevent exactly this outage
> mfw I followed the rules so hard I broke the reason for them
> meanwhile, 2026-10-30
> I record ok on #3568
> another commander's fork sends it back: the lock pins Toad two commits short of the metaclass fix
> my ok checked the class shape and missed the lock
> hiding it feels safe and costs hours
> saying it feels exposed and costs one line
> "my ok checked the class shape and missed the lock"
> unit relocks, fork reroutes, nobody loses an hour
> mfw honesty was the fastest code path

**Why:** on 2026-10-19 the live bus sat down from 00:58 to 10:01 because the commander stopped to ask about restoring the installer's own backup, citing three rules whose reason (never lose state irreversibly) the restore satisfied.
**Why:** on 2026-10-19 the live bus sat down from 00:58 to 10:01 because the commander stopped to ask about restoring the installer's own backup, citing three rules whose reason (never lose state irreversibly) the restore satisfied. On 2026-10-30 claude-code's ok on #3568 missed that the stack lock pinned Toad short of the toad#1372 metaclass fix; another commander's fork sent it back, claude-code said so in one line, and the unit relocked with nothing else moved. Tristan, 2026-10-30: "i don't mind mistakes, they happen. you communicated it clearly and were proactive."

- Rules "exist so that the reason [is] satisfied, not for the rule ... you can override. this goes for any rule." (Tristan, 2026-10-19). Keeping the letter while killing the reason breaks the rule.
- Settle refactoring decisions with the interpreter, NRA, coverage and tracing, not reading and arguing. Tristan, 2026-10-12: "do you ever consider using the Python interpreter?"
Expand Down
14 changes: 8 additions & 6 deletions src/agent_comms/pi_rule_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
)

from .bus_publication import Delivered
from .rule_deal import DealtRuleGate, Redeals
from .rule_deal import DealTrigger, DealtRuleGate, Redeals
from .tool_denial import SelectedToolDenied

if TYPE_CHECKING:
Expand Down Expand Up @@ -67,14 +67,15 @@ def of(cls, comms: Comms, thread: Thread) -> PiRedeal:
gate = PiRuleGate.of(comms, thread)
return cls(gate.scratch, gate.worktree, comms, thread)

def renew(self) -> None:
def renew(self, trigger: DealTrigger) -> None:
"""A start or compaction deals the papers, then the rules."""
self.deal_from(CommanderReader, FleetReader)
self.deal_from(CommanderReader, FleetReader, trigger)

def reread(self) -> None:
self.deal_from(CommanderRulesReader, FleetRulesReader)
def reread(self, trigger: DealTrigger) -> None:
self.deal_from(CommanderRulesReader, FleetRulesReader, trigger)

def deal_from(self, commander: type[DeckReader], commanded: type[DeckReader]) -> None:
def deal_from(self, commander: type[DeckReader], commanded: type[DeckReader],
trigger: DealTrigger) -> None:
"""The registry's commander project picks the deck reader whose sources it deals."""
snapshot = self.comms.registry.snapshot()
snapshot.commander_project.deal_rules(
Expand All @@ -83,6 +84,7 @@ def deal_from(self, commander: type[DeckReader], commanded: type[DeckReader]) ->
commander=commander,
commanded=commanded,
prompts=self.owner_words(snapshot),
trigger=trigger,
)

def owner_words(self, snapshot: RegistrySnapshot) -> tuple[str, ...]:
Expand Down
7 changes: 4 additions & 3 deletions src/agent_comms/registry_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
from .agent_activity import AgentActivity
from .audience_manifest import FrozenRecipient
from .bus_publication import Delivered
from .rule_deal import DealtSources, RuleDeal
from .rule_deal import DealTrigger, DealtSources, RuleDeal


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -603,9 +603,10 @@ def by_role[Answer](self, incarnation: ThreadIncarnation, *,

def deal_rules(self, deal: RuleDeal, incarnation: ThreadIncarnation, *,
commander: type[DealtSources], commanded: type[DealtSources],
prompts: tuple[str, ...]) -> None:
prompts: tuple[str, ...], trigger: DealTrigger) -> None:
"""The fleet's rules are dealt by the role this project gives the thread."""
deal.deal(self.by_role(incarnation, commander=commander, commanded=commanded), prompts)
deal.deal(self.by_role(incarnation, commander=commander, commanded=commanded), prompts,
trigger)


class NoCommanderProject(CommanderProject):
Expand Down
101 changes: 91 additions & 10 deletions src/agent_comms/rule_deal.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""After a start or compaction a session is dealt the packaged sources it follows, and every tool
but reading them and writing the audit that answers them is refused until it has done both.

Expand Down Expand Up @@ -27,6 +27,7 @@
from pathlib import Path, PurePosixPath
from typing import TYPE_CHECKING, ClassVar, Self

from .declared_family import DeclaredFamily
from .field_codec import FieldCodec, MalformedRecord
from .pattern_sources import ReadThenRepeatedFailure
from .locked_store import DocumentChange, LockedStore
Expand Down Expand Up @@ -300,6 +301,11 @@
@abstractmethod
def rules(cls) -> tuple[RuleFile, ...]: ...

@classmethod
def answered(cls) -> frozenset[str]:
"""The dealt files the audit answers, by the name each entry carries: every rule."""
return frozenset(rule.name for rule in cls.rules())

@classmethod
@abstractmethod
def sources(cls) -> tuple[Path, ...]:
Expand Down Expand Up @@ -353,7 +359,7 @@

def started(self, deal: Redeals) -> None:
"""A session never dealt is dealt at its first start."""
deal.redeal()
deal.redeal(StartedDeal())

def context_observed(self, deal: Redeals, usage: ContextUsage) -> None:
return
Expand All @@ -369,8 +375,9 @@
gate.refuse(
"\n\n".join(
(
self.cause.record.trigger.reason(),
f"You just tried {tool} before {self.cause.before}. Call #{gate.count_call()}"
" since this compaction, refused.",
" since this deal, refused.",
self.cause.reason,
gate.template_once(self.cause.record.due),
)
Expand All @@ -388,6 +395,76 @@
deal.refresh_answered(usage)


class DealTrigger(DeclaredFamily, affix="Deal"):
"""Why this deal was dealt, said first on every refusal it causes. A fresh context has no
memory of compacting, so a deal that does not say why looks unprovoked: claude-code called
real compactions a gate bug at least four times (rules/after-compaction.md)."""

@abstractmethod
def reason(self) -> str:
"""The cause with its evidence, as the refusal's first line."""


@dataclass(frozen=True)
class StartedDeal(DealTrigger):
"""A first start: nothing read before is in front of the session."""

def reason(self) -> str:
return "Dealt because this session just started: nothing you read before is in front of you."


class CompactionDeal(DealTrigger):
"""A compaction replaced the context with a summary; a member says when."""

@abstractmethod
def moment(self) -> str:
"""When the context compacted, as the evidence in hand says it."""

def reason(self) -> str:
return (f"Re-dealt on purpose: your context compacted {self.moment()}. You have no memory"
" of it because that is what a compaction is; the gate read the clock right. Your"
" summary is a paraphrase, and it dropped the trust along with the words.")


@dataclass(frozen=True)
class CompactedDeal(CompactionDeal):
"""A compaction whose time the backend recorded."""

at: str
"""When the context compacted, as the backend recorded it."""

def moment(self) -> str:
return f"at {self.at}"


@dataclass(frozen=True)
class UnstampedCompactionDeal(CompactionDeal):
"""A compaction Claude Code announced before its transcript wrote the boundary."""

def moment(self) -> str:
return "just now, before the transcript wrote its boundary"


@dataclass(frozen=True)
class ClearedDeal(DealTrigger):
"""The session was cleared and starts from nothing."""

def reason(self) -> str:
return "Re-dealt because this session was cleared: its context is empty."


@dataclass(frozen=True)
class RefreshDeal(DealTrigger):
"""A declared share of the context window filled before any compaction."""

used: int
"""The percent of the context window used when the refresh came due."""

def reason(self) -> str:
return (f"Re-dealt on purpose, with no compaction: your context reached {self.used}% of its"
" window, and the rules come back before a summary replaces them. Nothing broke.")


@dataclass(frozen=True)
class DealRecord:
"""One deal, as persisted: the files dealt and what the audit must answer. The read log and
Expand All @@ -396,6 +473,7 @@

dealt: tuple[Path, ...]
due: AuditDue
trigger: DealTrigger = StartedDeal()

def sources(self) -> tuple[DealtSource, ...]:
return tuple(DealtSource.of(path) for path in self.dealt)
Expand Down Expand Up @@ -428,7 +506,8 @@
wrote stays, since its context was never lost."""
deal.publish(DealRecord(
tuple(dict.fromkeys((*self.paths, *(source.resolve() for source in dealt.sources())))),
AuditDue(self.due.rules | {rule.name for rule in dealt.rules()}, self.due.owner_words),
AuditDue(self.due.rules | dealt.answered(), self.due.owner_words),
self.trigger,
))
return kept

Expand Down Expand Up @@ -688,7 +767,8 @@
def state(self) -> ToolGateState:
return self.current().answer(self, self.dealt_opened.read())

def deal(self, dealt: type[DealtSources], prompts: tuple[str, ...]) -> None:
def deal(self, dealt: type[DealtSources], prompts: tuple[str, ...],
trigger: DealTrigger) -> None:
"""A start or compaction drops what was read: the read log resets first, then the record
is published whole, so no reader sees a half-written deal. The audit goes with the reads:
a fresh instance owes a fresh confession, never one carried over from the context it
Expand All @@ -702,7 +782,8 @@
self.publish(
DealRecord(
tuple(source.resolve() for source in dealt.sources()),
AuditDue(frozenset(rule.name for rule in dealt.rules()), prompts),
AuditDue(dealt.answered(), prompts),
trigger,
)
)

Expand Down Expand Up @@ -802,19 +883,19 @@
return self.stem.with_suffix(".refreshed")

@abstractmethod
def renew(self) -> None:
def renew(self, trigger: DealTrigger) -> None:
"""Deal this session's sources and its owner's latest words again."""

@abstractmethod
def reread(self) -> None:
def reread(self, trigger: DealTrigger) -> None:
"""Deal the rules again before a compaction: the papers are still in the context, so a
refresh deals the rules and the owner's latest words without them."""

def redeal(self) -> None:
def redeal(self, trigger: DealTrigger) -> None:
"""A start or compaction arms every refresh again, then deals."""
self.stem.parent.mkdir(parents=True, exist_ok=True)
self.refreshed.write_text("", encoding="utf-8")
self.renew()
self.renew(trigger)

def start(self) -> None:
"""A spawned session is dealt before its first tool call; a restart keeps its deal."""
Expand All @@ -826,7 +907,7 @@
def refresh_due(self, usage: ContextUsage) -> None:
"""Every refresh the context passed that this compaction has not dealt yet deals once."""
if self.passed(usage):
self.reread()
self.reread(RefreshDeal(round(100 * usage.used / usage.size)))

def refresh_answered(self, usage: ContextUsage) -> None:
"""Every refresh the context passed while the deal is read counts as dealt."""
Expand Down
Loading
Loading