Skip to content

Outbound gateway: replace provider policy matrices with per-recipient traffic control - #216

Closed
DevNexsler wants to merge 69 commits into
crystaldba:mainfrom
DevNexsler:feat/traffic-control
Closed

DevNexsler wants to merge 69 commits into
crystaldba:mainfrom
DevNexsler:feat/traffic-control

Conversation

@DevNexsler

Copy link
Copy Markdown

Summary

  • Replaces the 4-gate provider policy layer (provider→operation matrix, provider→intent matrix, provider derivation as policy, alias hard-fail) with two traffic-control gates: per-recipient in-flight lease + context-staleness watermark. Old policy layer archived in src/postgres_mcp/outbound_gateway/archive/ with a postmortem of the wake-25789 failure (Melody Haddix, silent quarantine, Maint-Manager tickets 1691/1693).
  • Modes via OUTBOUND_TRAFFIC_CONTROL env: off / shadow (default; logs would-blocks, sends anyway) / enforce. Fail-open on gate malfunction; kill switch remains the only fail-closed control.
  • override=true bypasses staleness only (never lease/kill switch), audit-logged with the overridden message id. Free-form intent_kind metadata (no more enum rejection of escalation). Field-level validation errors. Lease and dependency-wait blocks defer non-terminally (self-healing via worker resume); stale-context blocks terminalize with an informative detail the calling agent can act on.
  • Post-dispatch exceptions no longer escape execute()/resume() — restores the invariant the Comm-Data-Store reconciler's rejection-prefix rule depends on (companion PR in Comm-Data-Store).

Spec: ~/.hermes/docs/superpowers/specs/2026-08-27-outbound-gateway-traffic-control-design.md

Deploy notes

  • Merge companion Comm-Data-Store PR first (reconciler + migration 149; apply migration BEFORE reconciler restart).
  • Roll out with OUTBOUND_TRAFFIC_CONTROL=shadow, watch traffic control shadow would-block warnings 3-5 days, then enforce. Matrix env vars (OUTBOUND_PROVIDER_OPERATIONS_JSON, OUTBOUND_PROVIDER_INTENTS_JSON, OUTBOUND_ENABLED_INTENTS_JSON) are ignored by this code; delete at cutover (last prod values preserved in archive/README.md).

Known follow-ups (before enforce cutover)

  • Hoist the _adapter() lookup above _dispatch_stage's try/except (or assert adapter coverage at build_runtime): a no-adapter config error is currently log-only instead of raising. Fails safe (wake held open + ERROR auto-ticketed) but violates the pre/post-dispatch boundary.
  • Fully autonomous override after an enforce-block requires a CDS migration: successor-action creation is gated on an operator resolution precondition (resolve_outbound_action_from_evidence requires state='manual_review'). Today override works when an operator resolution exists; otherwise it fails soft with clear detail.

Testing

  • 687 unit tests passing (12 commits, red-green TDD per task); ruff clean on all touched files.
  • Claim/transition sequencing verified line-by-line against Comm-Data-Store migrations 067/068/088/115/120.
  • Final whole-branch review + adversarial fix wave (2 criticals found and fixed: staleness self-block, shared internal recipient key).

🤖 Generated with Claude Code

Adds an MCP tool that fuzzy-searches across messages, participants,
channels, and transcripts in a single call. Uses ILIKE filtering
(GIN-index-accelerated) with similarity() ranking. Supports OR/AND
modes and validates minimum keyword length.
First-class MCP tool wrapping the migration-026 outbound intent lock SQL
functions so agents stop hand-writing lock SQL through psql/asyncpg (measured:
~6% of all agent tokens, 152 friction events/week — see Comm-Data-Store
docs/proposals/outbound-lock-tool.md).

- One `outbound_lock` tool, op = acquire | complete | release | check.
- Writable connection lives in the tool (unrestricted driver); message ids /
  proxy emails travel as JSON args, never inline SQL -> kills the terminal
  security-scanner false positives.
- Strict JSON in/out.
- is_duplicate_send_evidence = (completed_at set AND request_ref set),
  independent of released_at — encoded in every response so the
  released-vs-completed misread (2026-07-01 near-miss) is structurally
  impossible.
- Pure evidence-semantics helpers + a driver-injected op runner in
  outbound_lock.py (unit tested, 12 cases); live acquire->block->complete->
  post-block-with-evidence->check->release round-trip verified against the DB.

Wraps the existing concurrency-safe functions unchanged; no schema change.
DevNexsler and others added 28 commits August 5, 2026 21:01
…eview fixes

- Provider used for enabled_operations_by_provider/enabled_intents_by_provider
  gating is now "tenantcloud" for the four TenantCloud operations regardless
  of the wake's message shape. _provider() previously fell through to the
  wake's message_source, so a TenantCloud execute on a non-TenantCloud-shaped
  wake (e.g. an email-origin wake) was rejected by the allowlist gate -- the
  same wake-shape coupling this feature removes, surviving one layer up.
- canonical_context/canonical_scope no longer bake a Python None into the
  literal string "None" for tenantcloud_claim_id when a wake has no claim
  linkage; store.py's lock_intent and tenantcloud_shared's idempotency key
  read this value directly into persisted, immutable identity strings.
- Restored suggest_targets()-based coverage for the wake self-consistency
  checks (source/provider/claim linkage/state/owner/family, entity id
  format/overflow/alias-conflict, thread-channel agreement) that used to
  raise via the removed execute-blocking gate and are now return {} instead;
  the per-operation "required ids for this operation" cases don't translate
  since suggest_targets is operation-agnostic.
- Restored the cross-operation target-smuggling test for the three id
  fields still illegal on LeadStatusArguments (lead_id is now legitimate
  there; thread_id/request_id/property_id/unit_id are not).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Agents choose the outbound target. TenantCloud operations now carry
thread_id/lead_id/property_id+unit_id/request_id as typed arguments,
validated for format only. Target derivation is demoted from an
enforcement gate to an advisory 'suggest' verb on the same tool.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
suggest_targets() (the advisory-only TenantCloud target hint) was still
gating on the retired enforcement rules: tenantcloud_api ownership,
claimed/running state, and event-source/message-source/channel-type
agreement. In production 0 of 142 claims are tenantcloud_api-owned and
only 22/142 are active, so suggest returned {} for nearly every real
wake. It also only ever read tenantcloud_event_claims.entity_ids, which
0 of 142 real claims populate -- the ids actually live in
entity_scope_key's `prefix:id` shape.

Removes the ownership/state/source/channel gating (suggest is read-only
and advisory, so a legacy-owned or completed claim's hint is exactly as
safe as an active one) while keeping the checks that prevent emitting a
wrong id: claim-identity agreement and family-correct keys for
entity_ids, and prefix/parse validation for entity_scope_key. Adds
entity_scope_key to WakeEventRecord/the wake-event query and a parser
covering the real production prefix distribution (lead, tenantcloud:lead,
tc-lead, tenantcloud-lead, tenantcloud-prospect, tenantcloud:maintenance-
request; property slugs and phone numbers contribute nothing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An execute whose write was rejected before the provider ever processed
it (authentication_unavailable) is already classified
DEFINITIVE_NON_ACCEPTANCE+retryable=True by _from_execution and, via
service.py's existing generic routing, retries cleanly. But
_from_reconciliation only ever reached that same provably-safe-to-retry
conclusion for the two TenantCloud *status* operations (via
definitive_absence_detail); for the two *create* operations
(message.send, maintenance.create), definitive_absence_detail is None,
so a reconcile-time authentication_unavailable result fell through to
the generic ambiguous bucket and could ride the ambiguous-create path
toward reconcile/manual_review -- even though a reconciliation read
that cannot authenticate proves nothing was written, exactly like the
invoke-time case.

Adds the same authentication_unavailable check to _from_reconciliation,
ahead of and independent from definitive_absence_detail, so it applies
uniformly across all four TenantCloud operations. service.py itself did
not need to change: _finish_observation already promotes any
retryable DEFINITIVE_NON_ACCEPTANCE observation to RETRY_READY
regardless of whether it arrived via dispatch or reconcile, and
ALLOWED_TRANSITIONS already permits RECONCILING -> RETRY_READY --
retryable already reaches that decision point correctly for every
disposition the adapter hands it. The ambiguous-create and
non-retryable-rejection cases are pinned unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two defects found by the live production canary: suggest returned {} for
every real wake (it still gated on the removed ownership rules and read
provider ids from entity_ids, which production never populates -- they
live in entity_scope_key), and a reconcile whose own read could not
authenticate escalated the two create operations toward manual review
instead of retrying a request that provably never dispatched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rolls the TenantCloud target-derivation pattern (Task 1) to the remaining
four providers. Each of email.send, quo.sms.send, cliq.channel.post,
cliq.chat.post, calendar.create, calendar.update, and calendar.delete now
carries a required agent-supplied target field (to_address, to_phone,
channel_or_chat_id, calendar_id) instead of having its recipient/channel
derived from wake data. _target() reads the value straight off
request.arguments and always returns verified=True, matching the
TenantCloud precedent. The retired wake-side derivation (proxy/direct/
participant email resolution, phone resolution) moves behind
suggest_targets() as an advisory hint rather than being deleted, so an
agent that doesn't know an id can still ask what the wake implies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Agents name the outbound target for every provider. email/quo/cliq/calendar
now carry to_address/to_phone/channel_or_chat_id/calendar_id as required
typed arguments, validated for format only. Recipient derivation moves
behind the advisory suggest verb. Quo's real send destination
(recipient_phone) comes from the agent too, closing the shared-line
wrong-recipient hazard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CalendarAdapter._parse looked for the event under data.content, but
Agent Email's request_status envelope carries it under data.event.
The old code silently fell through to regexing the human-formatted
status text, whose embedded result is JSON-encoded with literal \n
instead of real newlines, so the UID/URL regexes never matched and a
successfully created event was reported as
calendar_reconciliation_inconclusive.

Read the event object directly out of the structured payload (walking
nested objects the way CliqAdapter._parse already does) and only fall
back to the text/regex path when no structured event is present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CalendarAdapter reported calendar_reconciliation_inconclusive for events the
provider had actually created. Agent Email's async queue returns the event as
data.event in the structured payload, but _parse looked for data.content and
fell back to regexing the status text, whose newlines are JSON-escaped. Read
the event fields directly, keeping the text path as a fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
calendar.update and calendar.delete were the last two outbound-gateway
operations still deriving their event identity solely from the wake
payload, which production data shows is always empty (zero raw_events
rows carry calendar_event_url), making both operations unreachable.

Split CalendarDescriptionArguments into CalendarCreateArguments (unchanged
shape) and CalendarUpdateArguments (calendar_id, event_url, etag,
event_uid, description), and extended CalendarDeleteArguments with the
same event_url/etag/event_uid fields. event_uid defaults to the CalDAV
event_url's basename when omitted. context.py now prefers the
agent-supplied event_url/etag/event_uid, falling back to the wake-derived
values only when the agent omits them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
calendar.update and calendar.delete took the event identity only from the
wake payload, which no production wake carries -- both were unreachable.
Take event_url/etag (and an optional event_uid, derived from the URL
basename otherwise) from agent arguments, keeping the wake values as a
fallback. Completes the agent-supplied-targets migration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A delete payload carries {deleted:true,event_url} and no event object, so
the uid-seeking walk missed it and successful deletions settled as unknown.
Accept the deletion confirmation, deriving the uid from the URL basename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TenantCloud lead status updates latched dead on 2026-08-10. Three leads
(2413486, 2417344, 2417650) stranded, every attempt
tenantcloud_auth_rejected_before_dispatch, and the only thing that cleared it
was restarting the container.

Cause is a lifetime mismatch, not a bug in the facade. The Comm-Data-Store
AuthRefreshBudget is SCAN-LOCAL by design (see that repo's
docs/superpowers/specs/2026-07-21-tenantcloud-inactive-runner-refresh-design.md):
it permits exactly one token refresh, then caches that token for the budget's
lifetime so a short batch does not re-read a frozen token per endpoint.
Correct for a scan; wrong for a daemon.

_build_tenantcloud_adapter() constructed one TenantCloudClient inside
build_runtime() and the adapter reused it for the container's whole life, so
"scan-local" became "forever":

    Firefox lapses once -> facade spends its single refresh -> caches that
    token -> serves it on every later call, never re-reading localStorage.
    Ten minutes later the cached token is dead, the budget is spent, and
    every write fails until someone restarts the container.

That shape explains the evidence: 13 successes then permanent failure (a latch,
not degradation), no code or config change at the boundary, RestartCount=0 with
40+ hours uptime, and a live get_token returning ttl -132.

Fix: share `control` and `auth` (stateless with respect to token lifetime) and
build the CLIENT per operation, restoring the lifetime the budget assumes. A
single operation still uses one facade, so the pre-write readback and the write
stay in the same scan and the one-refresh anti-storm reservation still holds
within a dispatch.

TenantCloudAdapter now takes `mutations_factory` only. The instance
constructor is deleted rather than deprecated: accepting a ready-made facade is
exactly how the latch was created, and an API that still permits it relies on
every future caller remembering. The 16 existing call sites pass
`mutations_factory=lambda: double`.

Six tests. The load-bearing one asserts on the real builder and fails for the
right reason -- with the old wiring it reports "got 1 client for 2 operations",
not a TypeError. Verified red/green by toggling only server.py with the adapter
held constant, including a simulated regression where the factory closes over a
shared instance. Three tests guard the design that must NOT be relaxed:
one facade per operation (no double auth per dispatch), doubles supplied
through the factory, and no instance constructor.

No Comm-Data-Store change. AuthRefreshBudget behaves exactly as specified.

663 passed, 9 skipped, 1 xfailed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Dockerfile does `ADD . /app`, and there was no .dockerignore, so whatever
sat in the tree a human happened to build from went into the release artifact.
The image running in production carries 22 stale .pyc files from a worktree's
__pycache__ that the main checkout never had. Nothing was broken by it -- all
51 .py files compare byte-identical against a fresh build from main -- but it
meant two images built from the same commit did not compare equal, which is
exactly the property you want when checking what is deployed.

Ignores bytecode, VCS and tool caches. Leaves .venv alone on purpose: the host
checkout has one, it is currently copied in and then reconciled by `uv sync
--frozen --no-dev`, and excluding it changes what the builder resolves. That
is worth doing, but as its own verified change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rrors

ExecuteRequest.intent_kind is now a plain casefolded string (<=64 chars,
non-empty) instead of the IntentKind enum, so agents can attach any
intent label without pydantic rejecting it outright (the string that
burned wake 25789). IntentKind is kept as the vocabulary for
SLOT_REQUIRED_INTENTS and routing maps. appointment_slot is required
when the intent is in SLOT_REQUIRED_INTENTS, forbidden only when the
intent is a KNOWN IntentKind not in that set, and permitted-but-optional
for unknown/free-form intents. ALLOWED_COMBINATIONS is no longer
enforced by validate_matrix (Task 5 archives the constant itself).
Added ExecuteRequest.override: bool = False.

server.py's ValidationError branch now surfaces the failing field
location and message instead of a generic "invalid outbound action
request", with a hint listing valid ActionRole/Operation values for
those two fields.

Rippled the intent_kind: IntentKind -> str change through
ActionContext (context.py) and every `.value` access on
request.intent_kind / context.intent_kind across context.py, store.py,
evidence.py, and service.py, since plain str has no .value attribute.

Deleted tests describing the now-removed matrix enforcement:
- test_role_operation_intent_matrix_fails_closed (test_models.py)
- test_tenantcloud_role_operation_intent_matrix_fails_closed (test_models.py)

Also dropped the {"intent_kind": "freeform"} case from
test_adapter_arguments_and_enums_reject_unknown_values, since an
unknown intent_kind is no longer a rejection.
OutboundActionRecord.intent_kind (service.py) and store.py's _record()
reconstruction both still coerced the persisted intent_kind column
through IntentKind(...), so a free-form intent (e.g. "escalation")
accepted by ExecuteRequest would raise ValueError the moment it was
reloaded from the ledger -- exactly the reconcile/resume crash-recovery
path that rebuilds ExecuteRequest off this record on every restart.

Changed OutboundActionRecord.intent_kind: IntentKind -> str and
store.py's _record() to pass the column through as a plain string,
matching ActionContext.intent_kind and ExecuteRequest.intent_kind.
Dropped the now-unused IntentKind imports from both modules. No other
IntentKind(...) coercions remain in store/worker/state_machine
(server.py's two IntentKind(item) calls are deliberately untouched --
they validate operator-supplied OUTBOUND_PROVIDER_INTENTS_JSON config
allowlists, not persisted request/record data).

Added test_free_form_intent_reconstructs_from_a_persisted_row_without_raising
to test_action_store.py as a regression guard.
Move retired policy-layer code (provider operation/intent allowlists, matrix
combination validator, and env-var parsers) to archive/provider_policy.py with
comprehensive README documenting the observed failure (wake 25789, 2026-08-24),
root-cause analysis, and revival requirements.

- Created archive/__init__.py and archive/provider_policy.py with moved code
- Created archive/README.md with failure narrative and env var documentation
- Removed DEFAULT_ENABLED_OPERATIONS_BY_PROVIDER, DEFAULT_ENABLED_INTENTS,
  DEFAULT_ENABLED_INTENTS_BY_PROVIDER, and parser functions from server.py
- Removed ALLOWED_COMBINATIONS from models.py
- Updated test imports to reference archive module for archived-behavior tests
- All live code (_provider(), _enabled_operations(), FEATURE_POLICY) remains

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Restore ALLOWED_COMBINATIONS and DEFAULT_ENABLED_* constants as byte-for-byte
  verbatim code with original enum-based definitions and type annotations
- Move real imports (ActionRole, IntentKind, Operation) to module level, remove
  TYPE_CHECKING guard to ensure archive module imports cleanly
- Remove unused IntentKind import from server.py (F401)
- Delete tautological assertions from test_default_email_routing_matches_* that
  merely verify constant definitions haven't changed (tautologies propping up
  live test with archive imports)
- Move test_explicit_empty_provider_scope_fails_closed (genuine archived-behavior
  test) to new tests/unit/outbound_gateway/test_archive_provider_policy.py
- Remove archive imports from test_server.py; no test files import archive code
  except test_archive_provider_policy.py
- Fix import sorting in archive/provider_policy.py and test_archive_provider_policy.py
- Rewrite archive/README.md closing paragraph: gateway gating now depends on
  pydantic shape validation (ExecuteRequest model) and kill switch, not policy
  matrices; fine-grained traffic control in Task 6

All ruff checks pass on touched files. Tests: 457 passed, 5 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d add override remediation path

- CRITICAL: enforce-mode block on a fresh RECEIVED action now runs
  prepare_outbound_action_and_acquire_lock() (the same call the normal
  preflight READY path uses) before claim()+definitive_fail(), because
  claim_outbound_action's live whitelist excludes 'received'
  (Comm-Data-Store migrations/068_outbound_gateway_observability.sql:156-159).
  Previously this raised an unhandled 55P03 in production instead of
  returning a blocked PublicResult. FakeStore.claim() now enforces the same
  state whitelist so the fake catches this regression.
- CRITICAL: override=true resend of a traffic-blocked DEFINITIVE_FAILED
  action now attempts the same successor-action remediation path operator
  remediation uses (create_outbound_remediation_context /
  retry_of_action_id / next effect_ordinal), instead of hitting the
  terminal early-return and staying permanently stuck. That function's own
  precondition (an evidence-resolved outbound_action_resolutions row) can
  only be satisfied by an operator today, so a resend without one fails
  soft with a clear detail instead of crashing on the DB's unhandled
  precondition-violation exception.
- IMPORTANT: resume() (worker.py's dependency_wait/prepared/retry_ready
  path -- the highest-staleness-risk case, longest wait) now runs the same
  traffic gate as execute(), with override forced False (no ExecuteRequest
  is available to resume()).
- MINOR: constructing the service with traffic_mode in {shadow, enforce}
  and no traffic_probe now logs a warning -- previously it silently
  behaved like 'off'.

Adds OutboundActionRecord.error_category (read from the outbound_actions
row) and ActionStore.remediate_traffic_block().
…inal

outbound_action_transition_allowed() (Comm-Data-Store migrations/
067_outbound_action_gateway.sql:346-389) has no dependency_wait ->
definitive_failed edge. An enforce-mode block on a DEPENDENCY_WAIT row
(reachable via execute() when a fresh RECEIVED action's prepare() hits a
contended intent lock, or via resume() on a worker-picked
dependency_wait action) was calling claim()+definitive_fail()
unconditionally, which would raise the DB's unhandled 'invalid outbound
definitive failure state' in production.

_check_traffic() now defers instead of terminalizing when the claimable
row is DEPENDENCY_WAIT: same pattern _preflight()'s READY branch already
uses for lock contention -- return the blocked PublicResult (PENDING
status, reason/detail from the verdict) with the row left in
DEPENDENCY_WAIT. A deferred block is still a block; the row stays
legally re-drivable and the worker's next resume() re-runs the same
gate, so lock contention clearing or an override resend both naturally
un-defer it.

FakeStore.definitive_fail() now enforces the real transition-table guard
(raises when expected_state has no edge to definitive_failed) so this
class of regression is caught by the suite going forward.

Also corrects the prior report's claim that
resolve_outbound_action_from_evidence is operator-only: it IS granted to
comm_data_store_runtime (migrations/079_runtime_tenantcloud_privilege_boundary.sql:427,459-475).
The real reason override-remediation can't be fully autonomous is that
function's own precondition -- state='manual_review' plus authoritative
provider non-acceptance evidence -- which a policy-driven traffic block
never has, not a privilege wall.
… pollution, shared internal key, terminal lease block, silent override, post-dispatch crash escape

CRITICAL 1 -- staleness self-block: newest_activity_after's ledger query
had no action_id exclusion, and execute() persists the action's own
durable row (created_at=now()) *before* the traffic gate runs, so every
real send found its own just-created row as "newer outbound activity"
and stale-blocked itself (shadow mode = 100% false positives). Threaded
exclude_action_id through TrafficProbe, check_traffic, the repository
implementation, and every fake/test double; the ledger query now
excludes the calling action_id, same as in_flight_actions already did.

CRITICAL 1b -- never-dispatched rows pollute staleness: the ledger query
excluded only state='rejected', so a traffic-blocked definitive_failed
row (or any other never-dispatched terminal) counted as "outbound
activity" and could cascade false stale_context blocks onto later sends
to the same recipient. Now only rows that plausibly reached a provider
count: WHERE (dispatch_started_at IS NOT NULL OR state = 'completed').
dispatch_started_at is set exactly at the prepared->dispatching
transition (Comm-Data-Store migrations/067_outbound_action_gateway.sql:774-775).

CRITICAL 2 -- shared internal recipient key: every internal_notification
without a TenantCloud claim used the single literal prospect_id
"internal:none" -- one lease and one staleness watermark for every
escalation across every wake, so concurrent escalations to different
Cliq channels cross-blocked each other. context.py now keys on the
resolved Cliq channel/chat id (f"internal:{target.target_id}") for that
case, falling back to arguments.channel_or_chat_id.

IMPORTANT 6 -- terminal lease block: a deterministic action_id plus a
terminal DEFINITIVE_FAILED meant a seconds-long lease overlap could brick
a resend forever (override never bypasses a lease, only staleness).
enforce+lease_held now defers exactly like the existing DEPENDENCY_WAIT
path -- no claim()/definitive_fail(), the row stays non-terminal and
worker-visible, and the worker's next resume() re-runs the gate and
self-heals once the lease clears. stale_context blocks remain terminal
(a conscious agent decision is still required there).

IMPORTANT 4 -- silent override: override used to short-circuit before
ever fetching newer activity, so an overridden staleness block left
nothing auditable. It now still fetches newest_activity_after (log-only,
no ledger schema change) and logs a WARNING naming what got overridden
when the lease is clear but newer activity exists.

IMPORTANT (post-dispatch crash escape) -- a post-accept provider
exception (e.g. a network timeout after the provider already accepted
the HTTP request) had no try/except on the interactive execute()/
resume() path, so it propagated as a raised error. FastMCP wraps that as
"Error executing tool outbound_action: ...", and the CDS reconciler's
rejection-prefix rule would then uncount a REAL send, letting the wake
complete while the message was actually delivered. execute() and
resume() now share a _dispatch_stage() helper that wraps everything from
_dispatch()/_resume_dependency()/_preflight() onward (NOT context load/
validation, which must still raise) in try/except, logs ERROR, and
returns the row's already-durable state (DISPATCHING/RECONCILING with a
lease) for the existing lease-expiry/reconcile/worker machinery to
recover -- restoring the invariant that an MCP error wrapper strictly
implies "rejected before any provider interaction".

MINORS: (a) archive/README.md no longer claims pydantic enforces a
role+operation+intent match (it only validates operation<->arguments
shape) and no longer calls traffic control future work. (b) NULL
messages.direction now reports "unknown" instead of falsely defaulting
to "inbound". (c) VALID_TRAFFIC_MODES now has one definition
(traffic_control.py), imported by both service.py and server.py instead
of each keeping its own copy.
@DevNexsler

Copy link
Copy Markdown
Author

Opened against wrong repo (fork parent) by mistake — please disregard.

@DevNexsler DevNexsler closed this Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant