Skip to content

fix(a2a): carry #778 reply deadline to production seat - #1

Merged
RuniThomsen merged 1 commit into
mainfrom
wolf/778-reply-deadline-production
Aug 22, 2026
Merged

RuniThomsen merged 1 commit into
mainfrom
wolf/778-reply-deadline-production

Conversation

@RuniThomsen

Copy link
Copy Markdown
Collaborator

Production lane for NousResearch#778

Carries the already-reviewed upstream candidate from NousResearch#91686 onto the user-owned fork that backs this Mac's Hermes seat.

Change

  • profile-native gateway.platforms.a2a.extra.reply_timeout
  • finite positive parsing with safe fallback
  • configured reply/subscription waits
  • per-task orphan watchdog deadlines that outlive both reply and routed-agent timeouts

Review

One independent reviewer examined the upstream diff at 5419150c839698c493958b66be5f477dffa1ee16; the upstream PR records the review discussion. This fork commit is a clean cherry-pick of that exact change onto current fork main.

Verification

  • scripts/run_tests.sh tests/plugins/test_a2a_plugin.py — 111 passed
  • production profile already has gateway.platforms.a2a.extra.reply_timeout: 900

Deployment

After merge, the exact merged adapter/protocol will be loaded into ai.hermes.gateway-wolf, then the running process and effective 900-second/watchdog values will be read back.

@RuniThomsen

Copy link
Copy Markdown
Collaborator Author

Reviewer gate: reviewer-a inspected the identical upstream candidate at 5419150 and confirmed the parsing, reply-wait, store, and watchdog behavior; discussion: NousResearch#91686 (comment). Production cherry-pick c6374cc is 111/111 green.

@RuniThomsen
RuniThomsen merged commit ff6958d into main Aug 22, 2026
@RuniThomsen
RuniThomsen deleted the wolf/778-reply-deadline-production branch August 22, 2026 08:43
RuniThomsen added a commit that referenced this pull request Aug 23, 2026
Reviewed once and verified 111/111. Makes the 900-second Hermes-seat reply deadline and matching orphan watchdog config-native.
RuniThomsen pushed a commit that referenced this pull request Sep 24, 2026
RuniThomsen pushed a commit that referenced this pull request Sep 24, 2026
…, with or without the multiplex flag

Two authority gaps in served_profile_child_env (NousResearch#111617 review, andrexibiza P1 #1/#2,
kvnloo finding 1):

- The base was hermes_subprocess_env(inherit_credentials=True) = the launch environ's
  provider credentials; strip_launch_profile_env only knows names with .env/source
  provenance, so a key systemd/Compose/the shell injected into the launch process
  survived into profile B's child whenever B did not define the same name. Now a ROUTED
  target scrubs every Tier-1/Tier-2 credential from the base regardless of provenance
  before B's own scope is overlaid (the child boundary gets get_secret's contract: a
  scoped miss is no credential, never ambient fallback). The launch profile's own child
  keeps its env. bot_relay's base=os.environ goes through the same scrub.
- strip_launch_profile_env / the scrub keyed on is_multiplex_active(); the Desktop and
  dashboard backends serve ?profile=B by installing the HERMES_HOME override without
  that flag, so B's slash worker / helper children kept A's .env and settings. The
  authority test is now "is the target a routed home" (target != process home).
- _build_browser_env resolved the passthrough keys via get_secret, which falls through
  to os.environ on a scoped miss while multiplexing is inactive: a routed B with no
  Firecrawl key got A's. Under serves_routed_profile() the bound scope is the only source.
- served_profile_child_env(inherit_credentials=True) with no target and no scope bound
  under multiplex minted with the launch credentials (key_cmd TTL refresh on a worker
  thread); it now raises UnscopedSecretError like get_secret.

tests/tui_gateway/test_served_profile_child_env_authority.py: ambient-only A key + B
missing it (mux on), flag-off routed B (helper child + browser), real child observation.
3/3 red on base.
RuniThomsen pushed a commit that referenced this pull request Sep 24, 2026
…mple misses after delivery (NousResearch#105861)

The post-delivery fire-claim check is a single sample of jobs.json. When it
missed on a run whose notice had already reached the platform,
_record_fire_ownership_lost() overwrote the delivered success with
'Interrupted by shutdown before terminal completion.' -> last_status: error,
and the operator's health watchdog then alerted on every tick for a job that
was working (NousResearch#105861, 3 confirmed incidents).

Suggested fix #1: when the run's delivery actually completed (delivery
attempted, no delivery error, non-empty terminal response), log the ownership
loss as a warning and fall through to _finish_completed_run() -- whose
owner-fenced mark_job_run() is the authoritative claim check, recording ok
while the claim is still held and nothing at all when it really is gone.

A claim lost *during* a side-effect fence (delivery did not complete), a failed
delivery, a run that never delivered, and a claim lost before delivery all keep
the ownership-loss error path unchanged.

Review follow-up: fire_claim_lost ORs the sampled claim with the transport-level
cancel_event, so the flag alone can't say which one fired. _run_one_job_body now
also receives transport_cancel and the post-delivery exemption is scoped to the
sampled path only -- an explicit transport cancel (dashboard drain / shutdown)
stays fail-closed and still records the interrupted run. Regression added:
test_transport_cancel_during_delivery_stays_fail_closed sets the external event
during delivery and asserts last_status=error /
'Interrupted by shutdown before terminal completion.'

Review follow-up 2: the transport side of that flag can't be read off the event
either. _FireOwnership.lost() latched a sampled loss by calling set() on
fire_claim_lost, which with a transport cancel in play is
_CombinedCancelEvent(lost_ownership, cancel_event) -- and _CombinedCancelEvent.set()
sets every source it ORs. A pure sampled miss therefore made the transport event
read as cancelled, the exemption above was unreachable, and the drain event the
*caller* owns was mutated by this run's bookkeeping. _FireOwnership now also
receives the sampled source itself (sampled_claim_lost, threaded from
run_one_job's lost_ownership) and latches that one event: the combined wrapper
keeps its OR semantics and is_set() still reports the loss, so the in-flight
agent / pre-run script is interrupted exactly as before -- only the source
identity is preserved. Regression added:
test_sampled_miss_leaves_an_idle_transport_event_alone (real claimed job,
successful delivery, miss_at_sample=2, initially unset threading.Event) asserts
last_status=ok, last_error=None and the transport event untouched; it fails on
the previous head with the reported symptom.
RuniThomsen pushed a commit that referenced this pull request Sep 24, 2026
compact_ledger, gc_blobs and list_entries each stamped the same prelude:
read the ledger, catch (OSError, UnicodeError), warn "skill_ledger: ledger
unreadable (%s); <what>", bail (tools/skill_ledger.py:305-310, :341-346,
:412-417 — simplify reuse #1). Fold them into a private sibling
_read_ledger(what, *, quiet_missing=False) -> Optional[bytes] that reads
bytes, validates the UTF-8 decode and warns once unless quiet_missing and
the ledger is merely absent (list_entries: a fresh install has no ledger).

Returns bytes rather than str so compact_ledger keeps reporting the on-disk
size in bytes_before instead of re-encoding. Warning texts ("compaction
skipped", "blob GC skipped", "listing empty") are unchanged, so the
existing invariant tests pass verbatim.
RuniThomsen pushed a commit that referenced this pull request Sep 24, 2026
test_list_entries_is_silent_when_the_ledger_is_merely_missing checked
`"listing empty" not in caplog.text` (tests/tools/test_skill_ledger_delta.py:40-44;
simplify quality #1) — a negative substring match that goes vacuously
green if the message is ever reworded, and stays green if a *different*
warning fires. Assert on caplog.records at WARNING or above instead, which
checks the actual invariant (no warning for a merely absent ledger)
independent of wording.
RuniThomsen pushed a commit that referenced this pull request Sep 24, 2026
…sweeps

gc_events and gc_worker_logs each carried the same three lines (int()
coerce, `< 0` check, raise ValueError(_NEGATIVE_RETENTION_MSG.format(...)));
only the message string was hoisted. Replace the constant with a
sibling-local helper `_retention_seconds(older_than_seconds) -> int` that
owns coerce + check + raise, and call it from both sweeps.

WHY: the comment explaining the rule ("a negative window puts the cutoff in
the future, so 'older than cutoff' matches everything") is the reason for
the check, so it belongs on the check rather than on a format string; and
a third sweep now has one place to reuse instead of a block to paste.
Message text is unchanged, so the existing
`pytest.raises(ValueError, match="older_than_seconds")` tests pass as-is.

Finding: simplify/D.reuse.md #1 + simplify/D.quality.md #1
(hermes_cli/kanban_db.py:4285-4287, :4303-4305).

Proof: mutating the helper's `< 0` to `< -10**12` fails
test_gc_events_rejects_negative_window and
test_gc_worker_logs_rejects_negative_window (DID NOT RAISE); head green.
RuniThomsen pushed a commit that referenced this pull request Sep 24, 2026
Loop run 7 at 22e8184 went red on "no sandbox process survives quit #1":
a `git fetch origin --filter=blob:none --stdin` (+ its index-pack) was
still running 60 s after quit. That is git's lazy promisor fetch: the dev
checkout is a blob:none partial clone, and a git read by the backend
needed a missing blob, so git went to the network, and the fetch outlived
the backend (reported as a finding; same class as the gh probe). CI
checkouts are not partial clones, so the spawned app gets
GIT_NO_LAZY_FETCH=1: dev runs stay offline and deterministic like CI.
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