Reproduction steps:
- Run Station v0.97 with a synthetic Team approval request whose stored
record has quorum: 2 and status: "pending".
- Create two valid, independently signed
approval_responseenvelopes for
that exact request_envelope_id and action_hash, one from each of two
approvers.
- Deliver the two responses concurrently through two normal relay poll
calls. This is reachable when the automatic relay poll thread and the
session-gated POST /api/relay/poll overlap.
- Use an ephemeral relay HTTP stub that returns one response event to each
poll call and reports both event acknowledgements as accepted. Do not make
any provider request.
- Inspect the approval record and the relay acknowledgement list.
- Control case: deliver the same two signed responses sequentially.
Expected:
Two distinct valid approvals for a quorum-2 request must be retained, the
request must become approved, and both relay events may be acknowledged only
after their handlers have been processed successfully.
Actual:
The sequential control consistently becomes approved. Under concurrent
delivery, one handler raises FileNotFoundError while both relay events are
still acknowledged. The surviving approval record remains pending with
only one approval:
sequential_approved: 20/20
concurrent approval_status: pending
concurrent approval_count: 1
handler_errors: ['FileNotFoundError']
ack_ids: [['evt1'], ['evt2']]
The same lost-write behavior is deterministic in direct handler tests: 20/20
concurrent trials leave the quorum pending, while 20/20 sequential controls
reach approved.
Station version (railcall version):
station-v0.97
Affected code path:
workbench/primitives/team_approval.py,_handle_incoming_response() (approximately lines 413–432) performs a
read-modify-write of one shared request file. _jwrite() uses the fixed
temporary name <request>.json.tmp and os.replace() without a lock.
workbench/primitives/team_mesh.py, receive() (approximately lines
283–296), catches handler exceptions, writes a handler_error line, and
still returns True. The relay poller therefore treats the failed response
as accepted. workbench/relay_client.py, poll_and_ack() (approximately
lines 187–214), then includes the event in the acknowledgement list.
Security / integrity impact:
A valid approver response can be permanently lost after the relay event is
acknowledged. A quorum-required financial or governance action can remain
pending even though the required approvers responded. The failure is a
governance-availability and audit-integrity problem: the relay says the
response was accepted, while the local signed approval state does not contain
it. It can also force operators to reissue the request and repeat the approval
ceremony.
No provider request, financial write, credential, or external destination was
used.
Deterministic under the documented concurrent-poll path: Yes.
Counter-evidence checked:
- Sequential delivery of the same valid signatures succeeds 20/20.
- Both signatures and the action/request bindings verify before the write.
- The issue is not duplicate-signer validation; the two signers are distinct.
- The failure is not a relay callback rejection: the callback path catches the
write exception and returns an acknowledgement, exactly as the source says.
- The closest fixed Community thread concerns cursor advancement after a
rejected callback, not concurrent approval-file read-modify-write loss.
Suggested fix:
Serialize approval-record updates with a per-request lock, or use a storage
operation that atomically merges distinct signer entries. The relay callback
must return False/raise to keep an event pending if durable approval state
was not committed; a handler_error must not be acknowledged as success.
AFFECTED VERSION:
station-v0.97