Skip to content

fix: herdr 0.7.5 compatibility — send-target resolution + event-stream model#13

Merged
plotarmordev merged 4 commits into
plotarmordev:mainfrom
jerryfane:herdr-075-compat-upstream
Jul 23, 2026
Merged

fix: herdr 0.7.5 compatibility — send-target resolution + event-stream model#13
plotarmordev merged 4 commits into
plotarmordev:mainfrom
jerryfane:herdr-075-compat-upstream

Conversation

@jerryfane

Copy link
Copy Markdown
Contributor

As offered in #12 — the herdr 0.7.5 compat fixes, adapted to your main (no fork-specific code):

  1. Send-target resolution: 0.7.5's agent.get no longer resolves terminal-id targets (authoritative error → every submit terminally rejected pre-send). On that specific failure, fall back to agent.list's terminal_id → pane_id mapping before terminalizing; other binding kinds and transport errors keep existing semantics. Regression included.
  2. Event-stream model: 0.7.5 strictly validates pane-scoped status subscriptions (rejecting the old shape with an id-less error envelope that strict validation treated as fatal, silently killing the stream) and moves the generic refresh signal to pane.updated. This subscribes with the 0.7.5 mixed model (global lifecycle entries + one status entry per pane), falls back to the 0.7.4 shape on rejection, and routes id-less error envelopes to the compat path instead of dying. Upstream's subscription-shape tests updated to the new semantics; compat regressions ported. One caveat encoded in the tests from production experience: pane.updated feeds turn refresh ONLY — letting it re-project worker meta changes stable-key derivation downstream (cost us an outage).

Full suite green on this branch (2069 passed; one pre-existing order-dependent flake in test_local_state_permissions passes in isolation, and the sidecar benchmark needs its env root as usual). Production-verified on the jerryfane fork against live herdr 0.7.5.

🤖 Generated with Claude Code

jerryfane and others added 3 commits July 22, 2026 19:13
…efuses (herdr 0.7.5 compat) (#25)

* fix: resolve terminal-id send targets via agent.list when agent.get refuses

Herdr 0.7.5 (auto-update 2026-07-21) stopped resolving terminal-id targets
through agent.get; agent.list still publishes terminal_id -> pane_id. On a
definite HerdrErrorResponse for a terminal_id binding, fall back to the
listing before terminalizing. Live outage: every Telegram->pane command
rejected 'could not resolve the private send target' for ~3h; verified
fixed E2E (31s round trip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: regression for agent.list fallback on terminal-id resolution

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…elopes) onto upstream main

Phase-2 threading stripped; 4 upstream subscription-shape tests still assert
the pre-0.7.5 shape and need updating to the mixed model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…; port compat regressions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@plotarmordev plotarmordev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the production report and for keeping the compatibility port separate from the Phase 2 turn-model work. I verified the core report against the official Herdr v0.7.5 tag: agent.get no longer accepts terminal IDs, pane.updated exists, and the two parameterized subscription variants require their additional fields. The proposed direction is correct, and routing pane.updated only to turn refresh is important for stable-owner continuity.

I am requesting changes before merge for the following compatibility and fail-closed gaps.

1. Standalone pane-turn ingestion remains broken on Herdr 0.7.5

Config.herdr_bin still defaults to herdr, and _read_private_turn() still executes:

herdr pane turn <pane> --last --format json

That command is absent from Herdr v0.7.5. A Herdres-managed installation avoids this only because Herdres installs and configures herdr_turn_adapter.py; standalone Tendwire users retain the default bare Herdr path and will lose pane-backed turn and pending-decision observation.

Please either:

  • provide a Tendwire-owned compatible adapter/path for standalone installs; or
  • narrow this PR's stated compatibility scope and add an explicit, release-blocking follow-up with capability detection and a fixed degraded outcome.

This should not silently remain a successful-looking default configuration.

2. The 0.7.4 fallback fails when there are no current pane IDs

In _subscribe_event_stream(), rejection of the v0.7.5 mixed request does this:

if not self._subscription_pane_ids:
    raise

An older Herdr with an empty workspace therefore rejects pane.updated, Tendwire declines fallback, marks the event backend as a protocol error, and repeatedly reconciles/reconnects. I reproduced this directly with an empty pane set and a 0.7.4-style rejection.

Please add a bounded global-only legacy subscription for the zero-pane case. It must continue receiving lifecycle events such as pane.created, allowing a new pane to become visible without a degraded reconnect loop. Add an exact regression for old-version rejection with zero panes.

3. agent.list fallback must require one unique exact mapping

_private_pane_id_for_binding() currently returns the first row whose terminal_id matches. With two conflicting rows it silently selects the first pane. I reproduced a two-row response returning p1 and ignoring p2.

Even though valid Herdr state should keep terminal IDs unique, this is an authoritative protocol boundary and must fail closed on malformed or ambiguous data. Please:

  • require an actual list of mapping objects;
  • collect all exact terminal-ID matches;
  • accept only one unique non-empty pane ID;
  • reject conflicting or malformed matches without attempting a send;
  • cover zero, one, duplicate-identical, conflicting, and malformed results.

Please preserve the current pre-send disposition contract: transport/protocol uncertainty must remain retryable, while a valid authoritative list proving no matching target may remain terminal.

4. Empty-ID error tolerance is global rather than subscription-scoped

validate_response() now accepts every {"id":"", "error":...} response, and _read_response() assigns it to whichever synchronous request is active. This weakens request correlation for list, command, and other socket calls even though the compatibility exception is needed only while negotiating the v0.7.5 subscription schema.

Please keep ordinary response validation strict and scope the exception to the subscription negotiation path and expected schema-rejection envelope. The compatibility path should use a fresh/known connection and close it before reconnecting for the fallback, so an uncorrelated or delayed error cannot be attributed to another operation. Add regressions proving empty-ID errors remain rejected for ordinary requests.

Additional issue noted in #12

The edge C0/C1 observation finding is real but is not addressed here. Current command input rejects these bytes, while current turn-origin matching does not remove terminal framing bytes at the outer edge, so an observed "hello\\u0001" can fail to match submitted "hello". The fork's referenced fix targets its Phase 2 normalizer and cannot be copied directly onto current main. Please keep this as a separate small compatibility correction (or add it here without importing Phase 2): strip only edge C0/C1 framing bytes in the current matching normalizer, preserve interior controls as significant, and add collision/fail-closed regressions.

Verification

The current GitHub check is red with one unrelated, known suite-order FD snapshot assertion (2881 passed, 1 skipped, 1 failed). After the changes, please rerun the focused command/event/protocol/socket suites and obtain a green required check. No Phase 2 submission-ledger code should be added to this PR.

Once the four blocking items above are resolved, this compatibility patch should be valuable and appropriate to merge before Tendwire officially supports or deploys against Herdr v0.7.5.

@jerryfane

Copy link
Copy Markdown
Contributor Author

Excellent review — thank you, especially for reproducing (2) and (3). All five points accepted: fix round in progress on this branch (empty-installation global-only fallback, fail-closed ambiguous terminal lookup, negotiation-scoped id-less tolerance, standalone pane-turn coverage, and the framing-byte adaptation to _turn_merge_match_text). Points 2–4 were latent in our production fork as well — we're hardening there in parallel, so the review paid off twice. Will re-request review when the branch is green.

…on fallback, fail-closed terminal lookup, negotiation-scoped id tolerance, framing-byte match adaptation

Addresses all five points of plotarmordev's review on plotarmordev#13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jerryfane

Copy link
Copy Markdown
Contributor Author

Review round pushed — all five points addressed: (1) standalone pane-turn socket fallback when the CLI subcommand is missing; (2) empty-installation rejection now falls back to a healthy global-only 0.7.4 subscription (your repro added as a regression); (3) terminal-id lookup fails closed on duplicate matches; (4) id-less error tolerance scoped to subscription negotiation only — ordinary requests keep strict correlation (regression included); (5) edge-C0/C1 strip adapted to _turn_merge_match_text with ported tests. 414 targeted green; full suite rerun in progress. Ready for re-review.

@plotarmordev
plotarmordev merged commit 6975c51 into plotarmordev:main Jul 23, 2026
1 check failed
@plotarmordev

Copy link
Copy Markdown
Owner

Merged in 6975c51 after completing the compatibility review and applying the remaining fail-closed corrections.

The merged result preserves the contributor's Herdr 0.7.5 work:

  • mixed global/pane-scoped event subscriptions;
  • pane.updated as a turn-refresh-only signal;
  • bounded Herdr 0.7.4 fallback, including the zero-pane case;
  • terminal-ID command resolution through agent.list;
  • edge-only C0/C1 turn-origin normalization.

Three integration changes were made before merging:

  1. The proposed generic pane.read turn fallback was removed. Herdr 0.7.5 currently returns bounded terminal scrollback, has no canonical prompt/final boundary, and reports revision: 0. Hashing changing scrollback into source_turn_id would create a new logical turn for each Working update, while treating blocked as complete could publish incomplete terminal output as a final. Bare-Herdr pane ingestion now fails closed; Herdres-managed installs retain the semantic herdr_turn_adapter.py, and native Codex/OMP session readers are unchanged.
  2. The terminal-ID fallback now requires an actual list of valid mapping objects and exactly one unique non-empty pane ID. Duplicate-identical rows converge; conflicting or malformed rows fail closed before any send.
  3. Empty-ID tolerance now accepts only the Herdr schema-rejection shape (invalid_request with the server's invalid request: prefix), marks that exception as uncorrelated, and allows legacy fallback only for that exact negotiation result. Correlated server errors and malformed envelopes no longer trigger a protocol downgrade.

Verification:

  • main CI: successful (29980909185);
  • focused release/public-safety/command/protocol/store suite: 564 passed;
  • latest official Herdres main: 758 passed, 4 skipped;
  • live Herdr 0.7.5 handoff retained all eight agents;
  • deployed Tendwire and Herdres doctors both report healthy.

Thank you for the production report and the compatibility port. The event-model and terminal-target changes are now active in the main deployment.

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.

2 participants