All Governor’s Log Technical Essay / Runtime Governance

The Policy Was Right. The Run Was Wrong.

We fixed a false governance violation in LangGraph and ended up learning something more important: a control can have the right rule and still be wrong if it is attached to the wrong execution.

Sentience Governor is a local-first governance runtime for AI agents. Its job is to create an independent record of what an agent said it intended to do, what it actually did, and where those two diverged.

That makes false confidence dangerous in both directions. Missing a real violation is bad. Inventing one is also bad, because if a governance system tells you an agent crossed a boundary when it did not, you will eventually stop trusting the governance system. And a governance system you have learned to discount is worse than none, because you are still paying for it.

We found exactly that problem in our LangGraph integration. The interesting part was not the policy rule.

The policy was right. The run was wrong.

One agent run became three governance sessions

LangGraph fires chain-level callbacks at multiple levels of a graph: one for the graph itself, and one for each node inside it. Our handler treated every on_chain_start as the beginning of a new governed execution, so a single .invoke() of a two-node graph could produce three starts, and Sentience read those as three separate governance sessions.

That broke something fundamental. The declared intent belonged to the first session, so later tool calls could land in sessions that had no intent baseline at all. From the policy evaluator’s point of view the result was internally consistent: I have a tool call, I have no declaration saying this tool is allowed, therefore flag it.

Except the user had declared the tool. The declaration and the action had simply been attached to different governance sessions.

Once we isolated it, the measurement was clean. Same tool, same declared intent. Flat chain: no POL-001. Nested graph: false POL-001. The variable was not policy. It was execution structure.

Governance is also an identity problem

The first instinct was to fix callback handling. But working through the failure, the deeper issue got harder to ignore. A governance system has to know more than who the agent is. It has to know which execution an action belongs to, and those are not the same identity.

Our session registry had historically encoded a simple assumption: one active governance session per agent_id. That works when one actor runs one task at a time. LangGraph broke it. Two legitimate root invocations could overlap, belonging to the same durable agent identity while representing completely separate governed executions, and opening the second could force-close the first.

So the model had to become this:

agent
  ├── governed execution A
  └── governed execution B

Then parallel branches exposed another layer. Two branches inside the same execution can have overlapping LLM turns. If token counts, model identifiers, providers, or llm_turn_id live in one shared slot, one branch can quietly inherit telemetry from another. The record still looks perfectly well formed. It is just wrong.

That gave us a more useful hierarchy for thinking about runtime governance:

agent identity
  └── governed execution
        ├── branch A
        │     └── LLM turn
        └── branch B
              └── LLM turn

This is becoming a core thesis for how I think about runtime governance: trustworthy governance depends on preserving the binding from intent, through execution, to action and evidence.

Intent belongs to the governed execution. Actions have to resolve back to that execution. Turn-level telemetry may belong deeper still, to a branch. Lose those bindings and a deterministic policy engine can produce the wrong answer while behaving exactly as designed.

Then we found the same failure in our own tests

We write defect-driving tests before implementing the fix. For this release there were 23 new tests covering the LangGraph behaviour, committed red so the fix would be measured rather than asserted.

Against the broken implementation, 13 failed and 10 passed. Seven of those ten were intentional regression controls that were supposed to stay green.

The remaining three were not. They were written to expose exactly the contamination we were chasing, their names said so, and they passed anyway. The interleaving they used did not force the condition they described.

That was uncomfortable, and I want to be precise about what we did with it, because the honest answer is less tidy than the neat one. We did not rewrite them. They are shipped as they are, and the fix makes them green along with everything else. What changed is that we stopped counting them as evidence. The cross-execution and cross-branch guarantees rest on the four tests that genuinely discriminate, and we wrote down which three do not, so nobody later reads a suite of 23 green tests and assumes 23 independent proofs.

A green test with the right name is not evidence. If we had looked only at the label and the final suite result, we could have convinced ourselves we had covered behaviour we never actually exercised.

The principle was becoming familiar. The check can be correct, and the binding between the check and the thing you believe it is testing can still be wrong.

Then two more green guards failed the same way

Preparing the release, we started looking at our own release controls with the same suspicion.

We have a gate designed to catch removed features that are still presented as active in the documentation. It was green. Meanwhile the user-facing changelog had missed the release that removed sentience-sync and still described it as a command you could run.

Why did the gate not catch it? Because changelog files were exempt. That made sense for historical release records: you do not want a scanner complaining that an old entry truthfully describes a command that existed at the time. But the exemption matched on filename, so it covered the current user-facing changelog too. The gate worked exactly as written. It was simply not attached to the surface we thought it protected.

Then we found another. Our public-surface scanner looks for material that should not cross from a private repository into the open-source one. It knew about private directory paths. It did not know about the private repository’s name, which had been sitting in a public Makefile header since the repository split, in plain view, passing every check.

Nothing mysterious about either one once seen. In both cases the rule and the thing being governed were misaligned.

So we started making the guards fail on purpose

For the guards involved in this release, passing is no longer enough. We mutation-tested them: plant the exact thing the guard is supposed to catch, run it, and require it to fail. Then restore the file byte-for-byte, run it again, and require it to pass. We did the same to the changelog gate. Remove the current version entry and the release check has to fail.

One of those mutations paid for itself immediately. Broadening a pattern to catch a bare command name also made it match our mailing list, which shares that name and still exists, so the guard started failing on a true statement. We would not have found that by watching it pass.

A guard that has only ever been observed succeeding tells you surprisingly little. Watching it reject the condition it exists to reject is much stronger evidence.

What changed in v0.3.0.2

Governance is now scoped to the root invocation. Nested callbacks describe structure inside that execution instead of creating phantom sessions, so declared intent stays attached to the governed run. A declared tool inside a nested graph no longer triggers the false POL-001, and a genuinely undeclared tool still does. That second half is held by a test that has to stay green through every future change.

SentienceCallbackHandler can also govern overlapping root invocations independently, and parallel branches no longer exchange turn-level telemetry. If you are not running concurrent executions, nothing changes for you: the session registry still closes a prior session by default, and only the LangChain handler opts into anything else.

There is one boundary worth stating plainly. SentienceMiddleware does not receive the run identifiers needed to make the same guarantee, so it remains one middleware instance per agent run. With more than one run open it reports a governance error rather than guessing which execution owns the tool call. That is deliberate. After spending a release removing incorrect attribution, guessing would be precisely the wrong lesson to take.

The thing I am taking away

I started this release thinking we had a policy bug. We did not. The evaluator was looking at exactly what we gave it. The problem was that we had bound the action to the wrong execution context, and once we saw that pattern we found versions of it in our tests, our documentation gate, and our public-repository guard.

That changes how I think about governance. Writing the rule is only part of the job. You also have to prove the rule is looking at the right actor, the right execution, the right branch, the right action, and the right state. A control can be perfectly deterministic and still be confidently wrong if any one of those bindings is wrong.

So here is a question I now think is worth asking of any guardrail, whether it governs AI agents, production systems, or your own release process: when did you last watch it fail for the exact thing it is supposed to catch?

If the answer is never, that green check may mean less than you think.

v0.3.0.2 is available now:

pip install -U sentience-governor