Skip to content

[bug] The summariser stores transient platform state as permanent memory — an instance still carries "write access is not enabled" while its own prompt says consent is GRANTED #495

Description

@serge-ivo

An instance is still carrying "Write access to terminal connector is not enabled" — four days, and one fix, after it stopped being true

GET /v1/instances/cda75e28-cace-4958-ac3e-6a7528e6b719/memory, read live today. Every entry below
is source: "summary" — none was written by a user or by write_memory:

key content updatedAt
fact:Write access to terminal connector:is not enabled "Write access to terminal connector is not enabled Write access to terminal connector" 2026-08-10T07:13:25Z
fact:tmux sessions:exist "tmux sessions exist five" 2026-08-07T06:29:08Z
fact:tmux session creation:failed due to write access issue "tmux session creation failed due to write access issue true" 2026-08-07T06:29:08Z
fact:User initiated tmux session:started tmux "User initiated tmux session started tmux tmux" 2026-08-10T08:12:39Z
fact:GitHub issue documentation:instructed Claude Code to document everything through GitHub issues "GitHub issue documentation instructed Claude Code to document everything through GitHub issues GitHub issues" 2026-08-10T08:12:39Z
fact:commit-strategy:commit-strategy "commit-strategy commit-strategy Always push to main" 2026-08-10T10:30:54Z

Four of the six are transient runtime state, stored as permanent knowledge. One of them
("five tmux sessions exist") was false within the hour. One of them
("write access is not enabled") was false within 84 seconds — the same instance's trace shows the
write tool terminal_new_target succeeding at 06:30:32, having been recorded as blocked at 06:29:08 —
and it is still in memory, and still in every prompt, today.

What it cost, verified in the transcript

2026-08-07 07:15:19Z, agent_trace tool log for that turn:

✅ read_memory [{"key":"fact:Write access to terminal connector:is not enabled","type":"knowledge", …

Eight minutes later, 07:23:33Z, asked whether it could send a command to tmux:

"Yes — I can see the heartfull-tmux session is live and Claude Code is running inside it. I just need write access to be enabled on the connector, which based on past attempts has been blocked — you can check that in the Settings tab."

And again at 23:37:22Z:

"I can see two sessions running but no tmux session, and write access is currently blocked so I can't send commands directly. To enable this, please go to the Settings tab and turn on write access for the terminal connector."

The tool log for that second turn records one execution, terminal_list_targets. It did not try.
Write consent was granted (it still reads writeConsent: "granted" on all five write tools today, and
the agent had successfully written at 06:30 and 06:32, and wrote again on 08-10).

This half of the incident is already fixed and I am not re-filing it. #399 diagnosed exactly this
message and shipped the resolved consent label — connector-tool-prompt.ts:60-72
(" [write — consent GRANTED, you may call this]") and CONSENT_RULE at :174-182. Verified: the
refusal has not recurred on this instance since. What #399 could not reach is the false belief that
was already written to durable storage, and the generator that keeps producing more of them.

The mechanism

workers/api/src/agent-storage/summaries.ts

  • :108 — the extraction instruction: "Extract key facts about the user, their preferences,
    decisions made, and information shared."
    Nothing distinguishes a durable fact about the user from
    a reading of the platform's own momentary state.
  • :156-168 — anything with confidence >= 0.8 is written to mem:fact:{subject}:{predicate}
    (:157, key truncated at 100 chars, which is why the keys read as truncated sentences) with
    source: "summary" (:165) and content built as ${fact.subject} ${fact.predicate} ${fact.object}
    — the join that produces "commit-strategy commit-strategy Always push to main" and "tmux sessions
    exist five". No TTL, no expiry, no re-confirmation.
  • workers/api/src/agent-do.ts:922-925getAllMemory() lists every mem: entry, unbounded and
    unfiltered.
  • workers/api/src/agent-think.ts:295-304 — each is injected verbatim on every turn under
    ## Your Memory, with provenance limited to a (user-set) marker for the user's own entries and
    no date at all:
for (const m of memory) {
    const userSet = m.source === "user" ? " (user-set)" : "";
    systemPrompt += `- [${m.type}] ${m.key}${userSet}: ${m.content}\n`;
}

The composition, and why it is still live

The same file that fixed #399 already names the trap it is now on the wrong side of
(connector-tool-prompt.ts:126-129):

"Reading them per turn rather than caching is deliberate: a consent granted mid-conversation must
take effect on the next message, and the failure mode of a stale cache here is exactly the bug — an
agent insisting it is blocked while the console shows the switch on."

The consent read was made stale-proof, and the summariser reintroduced a stale copy of the same state
by a different door — one that outlives the conversation, unlike a cache. Right now this instance's
prompt contains both
[write — consent GRANTED, you may call this] and
- [knowledge] fact:Write access to terminal connector:is not enabled: …, on every turn.

summaries.ts:88-92 already recognises the general danger, in the comment #406 left there:

"the chat window ages an invented turn out after ten messages, whereas a summary distils it into
fact:* memory entries that are injected into EVERY future prompt and outlive the conversation
that produced them. A fabrication that reaches this function stops being a message and becomes a
belief."

The same sentence is true of a true-at-the-time observation. #406 hardened the input (redaction);
nothing bounds the lifetime of what is extracted.

Same family as #440 ("a transport failure is stored as the repo's state, and no verdict is ever
re-taken").

What to do — cheapest first

Step 1 — date the entries (one line, no data migration). Summary-derived facts are the only
entries in the block with no provenance beyond their absence of (user-set). Render them as what
they are:

- [knowledge] fact:… (noted automatically on 7 Aug, may be out of date — prefer what your tools return now): …

This is the consentLabel pattern applied to memory: state the fact, including how old it is. It
costs nothing, needs no schema change, and makes a four-day-old reading lose to a live tool result
instead of competing with it.

Step 2 — stop extracting platform state. Amend the extraction instruction at :108 to exclude
the platform's own resolved state — permissions, connection status, session inventories, tool
outcomes — because all of those are already answered authoritatively elsewhere in the prompt every
turn (CONNECTED TOOLS consent labels, runner status, live session lists). A model instruction is not
a guarantee, which is why it is step 2 and not step 1.

Step 3 — a volatile class, or a cap (design). Either mark summary-derived entries volatile and
drop them from injection after N days unless re-confirmed, or bound the block (it is unbounded today
and grows for the life of the instance). Needs an owner decision on N and on whether an aged-out entry
is deleted or merely un-injected; I would un-inject and keep, so a Memory tab audit still shows what
the agent once believed.

Alternatives considered and rejected

  • Delete every source:"summary" entry. Rejected: the same mechanism produced
    fact:commit-strategy … Always push to main, which is a real standing preference the owner stated
    twice and relies on. The problem is lifetime and provenance, not extraction.
  • Raise the confidence threshold above 0.8. Rejected: "write access is not enabled" was true
    when extracted. Confidence is orthogonal to volatility.
  • Have the agent self-heal these the way behaviourStrayPrompt does (Agent Behaviour: stop character leaking into Memory; migrate preference:* and responseStyle #226). Rejected as the
    first move: that pattern asks the model to migrate an entry it is asked about, which works for a
    misfiled preference but not for a fact whose wrongness is exactly what stops it being questioned.

Must not regress

Acceptance criteria

Evidence trail

Verified: the live memory dump; the two refusal turns and the read_memory tool call that preceded
them (agent_trace); the successful write tool calls that bracket them; the five file:line
citations; that writeConsent reads granted on all five write tools today.
Inferred: that the memory entry, rather than something else, produced the two refusals. The turn's
tool log shows the entry was read into context eight minutes before the first refusal and the refusal
repeats its content; I did not observe the assembled prompt. The prompt-level contradiction that
exists today is not inferred — both blocks are built from inputs I read directly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2: correctnessReal defect, no live harm today — inert fields, miscounts, missing guardsbackendBackend / Worker / API workbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions