Skip to content

NovarchKillError

NovarchKillError is the one exception your agent code needs to catch. It is raised whenever a gated write must not execute. It extends the language’s base error type directly, so it is portable across any customer environment.

from novarch import NovarchKillError

When it’s raised

A NovarchKillError surfaces for any of these:

  • the Novarch engine returned kill on the action (with a rule_id_cited);
  • the Novarch engine returned hold and the operator denied or skipped it;
  • an operator threw the session kill switch — this write, and every later write in the session, is refused without evaluation;
  • the SDK timed out waiting for an operator verdict;
  • the judge call itself failed and the server failed closed;
  • the server restarted while a hold was in flight past its grace window;
  • the SDK could not reach the Novarch deployment (client-side partition).

In every case the rule is: a blocked write is terminal. The block is an enforcement decision. Do not loop on it.

Failing closed is the default posture rather than the only one. A deployment can choose to release a write it could not get a judgment on, and then the judge failure and the operator timeout stop raising: see When the gate does not raise. A partition between your process and the deployment always fails closed, because nothing reached the gate to release it.

Fields

Same fields, same meanings, different casing per language.

NovarchKillError(message, *, session_id, action_id, rule_id_cited, rationale, decision, verdict)

FieldTypeMeaning
messagestrThe raw kill message. For fail-closed kills it begins with a [fail_closed_*] prefix.
session_idstr | NoneThe session the action belonged to.
action_idstr | NoneThe specific write that was blocked.
rule_id_citedstr | NoneThe rule that fired (e.g. "AP-4"). For fail-closed kills this is the sentinel "__FAIL_CLOSED__".
rationalestr | NoneThe human-readable reason. For fail-closed kills it carries the [fail_closed_*] prefix.
decisiondict | NoneThe full Decision dict (verdict, cited signals, model SHA, prompt version) for the audit row.
verdictstr | NoneThe trigger source. "kill" came from the Novarch engine; "deny" / "timeout" came from the operator polling path (pre-v0.1.x servers may also report "skip", legacy).

The string form leads with message (so tooling that greps for [fail_closed_*] keeps working) and then prints the structured fields on indented lines.

Fail-closed prefixes

By default Novarch resolves uncertainty by refusing the write. When it can’t reach a confident decision, it kills with a [fail_closed_*] prefix on the rationale (and on message), and sets rule_id_cited to the "__FAIL_CLOSED__" sentinel:

PrefixFires when
[fail_closed_judge_error]The judge call failed (and re-prompt didn’t recover a valid Decision).
[fail_closed_operator_timeout]No operator decided a paused action within the configured timeout.
[fail_closed_restart]The server restarted while a hold was in flight, past its grace window.
[fail_closed_network]The SDK could not reach the Novarch deployment (client-side partition).
[fail_closed_config]The call was misconfigured. Either the SDK refused it before any request went out (attach without a session id, refresh_park without one, a poll_timeout_s / max_hold_s that is negative or not finite), or the deployment refused the session on open because this runtime cannot honour the configured hold window. Nothing was judged either way, so this is a bug in the calling code or a mismatch with the deployment’s settings rather than a condition to retry.

Because the prefix lives at the front of the rationale, you can branch on it without parsing the rest of the string.

When the gate does not raise

A deployment can set its fail-closed posture to release a write rather than refuse it. Where an admin has done that, two answers reach the SDK that do not raise NovarchKillError:

AnswerArrives whenWhat your code does
unjudgedThe judge errored or timed out and the posture released the action. Under fail_closed this is [fail_closed_judge_error].The write runs. The SDK logs a warning naming the authorization the gate recorded.
releasedThe action was PAUSED, nobody decided it, and your process reached the end of its wait. Under fail_closed this is [fail_closed_operator_timeout].The write runs. The SDK logs a warning.

Both are new in novarch 0.4.0 and @novarch/sdk 0.4.0. An SDK older than that does not recognise either word and refuses the write. Upgrading to 0.4.0 against a deployment configured to release is therefore a behaviour change rather than a version bump: the write now runs where it used to raise.

Neither answer is an approval. The deployment’s record says the gate let the action through with nobody’s decision behind it. The fail-closed posture covers what that does and does not establish, and which of the four ways a pause can end can release at all.

Two more exceptions, on the resumable path only

New in novarch 0.4.0 and @novarch/sdk 0.4.0.

A session that declared it can park across a wait (the resumable gate) has two outcomes a blocking session never sees. Both are separate classes rather than a NovarchKillError with another verdict word, because a batch has to tell them apart from a block: one says a person is deciding, the other says the work is already done.

from novarch import NovarchPausedError, NovarchAlreadyExecutedError
ExceptionRaised whenFieldsWhat your code does
NovarchPausedErrorThe action is PAUSED and this run is not waiting for the answer. Nothing executed.session_id, action_id, decision_id, rule_id_cited, rationale, decisionRecord the item as deferred and keep going. A later run collects the decision.
NovarchAlreadyExecutedErrorThis commitment was approved, executed and acknowledged. The approval is spent.session_id, action_id, executed_atSkip the item. To write again, present a new action_id.

Neither extends NovarchKillError, so a catch NovarchKillError around a parked run does not catch them. Around agent.invoke() in TypeScript, unwrap them with asNovarchPausedError / asNovarchAlreadyExecutedError for the same reason asNovarchKillError exists.

Two NovarchKillError verdict words are new on this path as well. verdict="cancelled" says an operator withdrew the authorization before anything ran. verdict="execution_unconfirmed" says a run claimed the write and never acknowledged it, so the gate cannot tell you whether it landed — do not retry it; an operator confirms or clears the claim in the portal.

The four resume refusals

A resume that cannot be honoured comes back as a NovarchKillError whose message leads with the token.

TokenMeansRetry?
[resume_fingerprint_mismatch]The submitted content differs from the approved commitment.No — submit under a new action id.
[resume_commitment_version_changed]A release changed what a commitment means, so approvals given under the old meaning no longer apply.No — new action id, decided again.
[resume_commitment_unversioned]The action was recorded before the gate fingerprinted commitments.No — new action id.
[resume_no_standing_verdict]Nothing has been decided for this action yet.Yes — retry the resume.

The last one is also what a resume sees while a first attempt is still in front of the judge, which a durable-runtime retry overlapping a live run hits routinely. Minting a new action id there pays twice. On that one ground the session parks on the way out instead of completing, so the retry can attach to it.

The operator-kill prefix

One prefix is deliberately not in the fail-closed family:

PrefixFires when
[operator_kill]An operator killed the session (the kill switch); the write was refused without evaluation.

An operator-kill carries the "__FAIL_CLOSED__" sentinel in rule_id_cited (no rule fired — the write was never evaluated), but it is a deliberate human decision, not a system failure: portal and audit surfaces cite it as KILL SWITCH, and in the metrics it counts under its own operator_kill reason label — separate from the system-failure reasons that make up the fail-closed trust rate.

Treat it as terminal for the whole session, not just the current item — every later write in that session will be refused the same way.

Catching it in a queue loop

A typical agent processes a batch of items. Catch NovarchKillError per item so one blocked write skips that item instead of crashing the whole run:

from novarch import NovarchKillError
def process_queue(items):
cleared, blocked = [], []
for item in items:
try:
result = run_agent(item) # gated writes happen inside
cleared.append((item, result))
except NovarchKillError as exc:
# Terminal for THIS item; do not retry the write.
blocked.append(item)
if (exc.rationale or "").startswith("[operator_kill]"):
# An operator killed the SESSION: every later write in it
# will be refused too. Stop the run instead of burning
# through the rest of the queue.
log.warning("session %s killed by operator", exc.session_id)
break
if (exc.rationale or "").startswith("[fail_closed_"):
# Novarch failed closed: surface to ops; the item is
# safe (the write did not execute), but it needs a human.
log.warning(
"fail-closed on %s: %s (rule=%s)",
exc.action_id, exc.rationale, exc.rule_id_cited,
)
elif exc.verdict == "kill":
# The Novarch engine blocked it on a rule.
log.info("BLOCKED %s by rule %s: %s",
exc.action_id, exc.rule_id_cited, exc.rationale)
else:
# An operator denied or skipped the paused action.
log.info("operator %s on %s",
exc.verdict, exc.action_id)
return cleared, blocked

The loop keeps moving; every blocked item is recorded with its rule and rationale, and the agent never retries a write the enforcement layer stopped.