Skip to content

refactor(workflow-executor): keep HTTP and wire formats out of the automated-inbox business code - #1951

Merged
Scra3 merged 12 commits into
feature/prd-1183-runtime-automation-pollerfrom
feature/prd-1441-automation-poller-separate-concerns
Oct 2, 2026
Merged

Scra3 merged 12 commits into
feature/prd-1183-runtime-automation-pollerfrom
feature/prd-1441-automation-poller-separate-concerns

Conversation

@Scra3

@Scra3 Scra3 commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

fixes PRD-1441

Targets the integration branch feature/prd-1183-runtime-automation-poller (#1906). Refactor only: no behaviour change, no contract change.

Why

The rest of the executor keeps transport out of the business code: AgentPort and WorkflowPort speak domain types, and adapters map the wire (run-to-available-step-mapper.ts). The automated-inbox code did not:

  • automation-poller.ts imported AgentHttpError and classified HTTP failures itself (401/403, 502-504, 408/429, network error codes, status 0);
  • AutomationPort / SegmentReaderPort were typed with Server* wire types;
  • the poller validated the timezone (copy of the run mapper's) and passed the raw service-account profile to the reader;
  • it knew the lianas that serve capabilities, the not_in operator, the 150 excluded-ids query-string bound, the composite-key rule, and split packed ids itself;
  • errors.ts imported agent-client to parse agent error bodies;
  • one 820-line class held the lease heartbeat, the sweep scheduling and the per-inbox logic.

What moves where

Concern Now
Agent failure classification (status / errno → forbidden · unreachable · overloaded · failed) adapters/agent-errors.ts → SegmentReadError (extends AgentPortError, same message)
Agent error body parsing, ` agent error:` suffix
Can known ids be excluded (composite key, > 150, lianas with capabilities, not_in declared) SegmentReaderPort.exclusionUnavailableReason, same order of checks, capabilities read in exactly the same cases
Wire config → domain inbox (StepUser, timezone fallback) ForestServerAutomationPort (toAutomatedInbox); timezone fallback shared with the run mapper (adapters/project-timezone.ts)
Packed record ids src/record-id.ts (was adapters/record-id-serializer.ts)
Domain types src/types/automation.ts; the two ports import only these
Failure kind → reported reason, "may be an operator refusal" policy automation/read-failure.ts (domain)
Reconciliation and paging rules automation/reconciliation.ts (pure, no port, no logger)
Lease trust (30 s window) automation/lease-keeper.ts (no I/O)
One inbox: reads, padding fallback, sync, logs automation/inbox-poll.ts
Lifecycle, heartbeat, bounded sweep of 5 automation/automation-poller.ts

git grep finds no @forestadmin/agent-client, @forestadmin/forestadmin-client or server-types import left in src/automation/, src/types/automation.ts, the two automation ports, errors.ts or record-id.ts.

Unchanged

HTTP calls (paths, query strings, bodies), sync bodies, log messages, levels and fields, retries and timings, the orchestrator contract. The first commit adds test/integration/automation-sweep.test.ts, which drives the real poller through the real ForestServerAutomationPort (mocked ServerUtils) and the real AgentClientSegmentReader (nock). It pins sync bodies, every agent request and the poll / read-failure logs for: 403, refused connection, not_in refused (400) then padding, 429 (no padding), unknown liana, composite key with 0 and N known records, 151 known records, null and invalid timezone, reconciliation and the JWT claims, an assignments route answering 404. It was written against the pre-refactor code and only its import line changed since.

Two things worth knowing:

  • AgentPortError (public export) now takes the agent message as an optional third argument instead of reading it off the cause. Every internal construction goes through agentPortError() / segmentReadError() and produces the same message; a host that builds new AgentPortError(op, cause) itself no longer gets the | agent error: suffix.
  • server-types.ts keeps its automation types: it is the contract mirror PRD-1177 keeps in sync with the server. The sync body is now typed against ServerAutomatedInboxSyncRequest.

Out of scope, pre-existing: ports/activity-log-port.ts imports two string unions from forestadmin-client.

Tests

Suite: 2058 → 2412 tests (2405 passed, 7 skipped). Every commit is green on its own. Tests moved with the logic; nothing was weakened:

  • Classification (the 17 HTTP / errno rows of the poller table + 408, 429, 600) → test/adapters/agent-errors.test.ts; the poller keeps one wiring case per behaviour.
  • Agent error body / suffix (errors.test.ts) → agent-errors.test.ts through the factory; errors.test.ts now pins AgentPortError itself.
  • Exclusion rules (lianas, 150 boundary, composite key, not_in declared, no capabilities call when nothing is known) → test/adapters/agent-client-segment-reader.test.ts; the poller keeps one it.each over the four reasons and capabilities-unreadable.
  • Wire → domain mapping (StepUser, timezone null / absent / invalid / valid, fields copied, unreadable config skipped) → test/adapters/forest-server-automation-port.test.ts.
  • Failure policy (toReadFailure, mayBeOperatorRefusal) → test/automation/read-failure.test.ts.
  • New: test/automation/reconciliation.test.ts (assignment state × run state × run id cross product, paging, chunking) and test/automation/lease-keeper.test.ts (30 s boundaries). Four sabotages are caught only by these: auto-canceled, the doing + live-run branch, and both 30 s boundaries.
  • The poller tests run on domain fakes and no longer import agent-client.

Every move was checked by sabotage (one change at a time, restored byte-for-byte): dropping 502 / ECONNABORTED / 429, 150 → 149, dropping a liana, skipping the 0-known shortcut, in instead of not_in, mapper dropping user / liana / keeping the raw timezone, newCandidates ignoring found ids, the page cap, the lease boundaries; each turned at least one test red.

Two Opus reviews ran before the first push (behaviour preservation, separation and tests). Their fixes are in the last commit: the capabilities catch only takes a SegmentReadError again (a malformed capabilities answer fails the candidate read as before), explicit mapping without a cast, sync body typed against the wire contract, policy moved to read-failure.ts, LeaseKeeper methods named after what they return, duplicated tables trimmed.

🤖 Generated with Claude Code

Note

Move automated-inbox poller and adapters off server HTTP/wire types onto shared domain types

  • Adds an automation module with AutomationPoller (15s lease heartbeats, up to 5 concurrent inbox polls, draining on stop), LeaseKeeper (30s trust expiry), InboxPoll, and reconciliation/candidate helpers in reconciliation.ts
  • Defines shared contracts (AutomatedInbox, InboxAssignment, SegmentDescriptor, SegmentReadError, InboxSyncReport) in automation.ts; ports and adapters like forest-server-automation-port.ts now translate wire payloads into these domain types
  • Replaces listFieldOperators on the segment-reader port with an exclusionUnavailableReason preflight that rejects composite keys, unknown lianas, fields without a not-in operator, and more than 150 known records
  • Moves agent error classification and SegmentReadError construction into agent-errors.ts, classifying failures as forbidden, unreachable, overloaded, or failed with optional HTTP status
  • Behavioral Change: AgentPortError in errors.ts no longer extracts agent detail from 5xx response bodies itself — callers must pass the optional agent message; the exported agentErrorDetail helper is removed, and record-id-serializer moved to record-id.ts

Changes since #1951 opened

  • Refactored parametrized tests in reconciliation test suite to precompute expected boolean flags [32e424e]

Macroscope summarized eb6c0f3.

alban bertolini and others added 10 commits October 1, 2026 20:50
…ugh the real adapters

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n record-id helper

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…een runs and automated inboxes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ilures, not the poller

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… not in the domain errors

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…own records can be excluded

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd map the wire in the adapters

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…le, lease, inbox poll and pure rules

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s and the lease trust window

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Oct 1, 2026

Copy link
Copy Markdown

PRD-1441

@qltysh

qltysh Bot commented Oct 1, 2026

Copy link
Copy Markdown

9 new issues

Tool Category Rule Count
qlty Structure Function with many parameters (count = 4): buildFilters 4
qlty Structure Function with many returns (count = 5): exclusionUnavailableReason 3
qlty Structure Function with high complexity (count = 13): tick 2

@qltysh

qltysh Bot commented Oct 1, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

Unable to calculate total coverage change because base branch coverage was not found.

Modified Files with Diff Coverage (17)

RatingFile% DiffUncovered Line #s
New file Coverage rating: A
packages/workflow-executor/src/automation/reconciliation.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/adapters/agent-errors.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/automation/read-failure.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/automation/automation-poller.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/build-workflow-executor.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/record-id.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/automation/inbox-poll.ts100.0%
New file Coverage rating: A
...-executor/src/adapters/forestadmin-client-activity-log-port.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/http/pending-data-validators.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/automation/lease-keeper.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/adapters/project-timezone.ts100.0%
New file Coverage rating: A
.../workflow-executor/src/adapters/agent-client-segment-reader.ts100.0%
New file Coverage rating: A
...ages/workflow-executor/src/adapters/agent-client-agent-port.ts100.0%
New file Coverage rating: A
...orkflow-executor/src/adapters/forest-server-automation-port.ts100.0%
New file Coverage rating: A
...workflow-executor/src/adapters/run-to-available-step-mapper.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/errors.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/http/step-serializer.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@Scra3 Scra3 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Spec (PRD-1441): conforms. Every concern the ticket lists now lives in an adapter or a pure domain module, and the end-to-end sweep test pinned before the refactor still passes unchanged.

Comment thread packages/workflow-executor/test/automation/read-failure.test.ts Outdated
… and classification tables

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

@matthv matthv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Spec (PRD-1441): conforms. Every path the production wiring reaches keeps its HTTP calls, sync bodies and logs.

Comment thread packages/workflow-executor/test/automation/reconciliation.test.ts Outdated
timezone: toProjectTimezone(config.timezone),
liana: config.liana,
segment: config.segment,
user: toStepUser(config.serviceAccountProfile),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Claude Opus 5.5 (claude-opus-5-5): This diff meets the criteria for a security review, so please run /security-review locally before merging.

Triggers
  • forest-server-automation-port.ts:64 builds the service-account identity the agent is called as, from the orchestrator's config.
  • agent-client-segment-reader.ts:129 signs the per-call agent token for that identity.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ran /security-review on this PR's diff (4a4d450…32e424e8): no finding. The identity is the same orchestrator-supplied profile through the unchanged toStepUser, now called once in toAutomatedInbox after the zod parse instead of in the reader; mintStepToken and its claims (snake_case aliases, scope: 'step-execution', 5 min TTL) are untouched, nothing new from the response enters the token, and no log or sync body carries the token, the secrets or the user object.

…ion cross-product tables

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

@matthv matthv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved after /verify-fixes run 1951-20261002135925-matthv-verify: 2 findings of 1951-20261001211330-Scra3 and 1951-20261002133556-matthv closed or accepted, none reopened.

@Scra3
Scra3 merged commit 2928d2c into feature/prd-1183-runtime-automation-poller Oct 2, 2026
37 checks passed
@Scra3
Scra3 deleted the feature/prd-1441-automation-poller-separate-concerns branch October 2, 2026 14:10
Scra3 added a commit that referenced this pull request Oct 2, 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.

2 participants