Skip to content

fix(agent): stop outgoing-reply notifications polluting the inbox snapshot - #18

Closed
terafin wants to merge 1 commit into
vbcherepanov:mainfrom
intarweb:fix/relay-persistence
Closed

fix(agent): stop outgoing-reply notifications polluting the inbox snapshot#18
terafin wants to merge 1 commit into
vbcherepanov:mainfrom
intarweb:fix/relay-persistence

Conversation

@terafin

@terafin terafin commented Jul 14, 2026

Copy link
Copy Markdown

Summary

Synthetic outgoing-reply notifications (MessageID: "reply-<taskID>") are one-shot — delivered in real time via OnIncoming and consumed by the caller. Their TaskID is an outgoing task that never exists in s.tasks, so several inbox-cleanup paths missed them: they lingered in the persisted inbox snapshot and re-surfaced on every read/wake.

Fix (internal/agent/store.go)

Path Change
CompleteTask Drop matching inbox entries before the s.tasks lookup, so a synthetic reply (absent from s.tasks) is cleared instead of returning ErrTaskNotFound and leaving the entry behind.
appendSyntheticReply Skip contentless completions (a bare ack with no reply text) — the terminal state is already recorded on the task, so an empty record is pure noise.
evictTerminal Evict aged one-shot reply records by identity (MessageID prefix) past a short delivery TTL, since the by-taskID drop never reaches them.
PeekInbox Consume one-shot reply records on read so a peek (vs a drain) doesn't leave them to re-surface. Genuine incoming task messages are untouched — peek stays non-destructive.

Plus internal/assets/hook/a2a-inbox-hook.sh: select the live-PID inbox and prune stale-PID snapshot files.

Tests

Adds table tests in store_test.go covering all four paths — each fails on the pre-fix code and passes with the fix:

  • TestCompleteTaskClearsOutgoingReplyNotification
  • TestIngestOutgoingTerminalSkipsEmptyReply
  • TestEvictTerminalReapsDeliveredOutgoingReplies
  • TestPeekConsumesOutgoingReply

go test ./... green on go 1.25.0. No wire-protocol / internal/a2a/ files touched.

…pshot

Synthetic outgoing-reply notifications (MessageID "reply-<taskID>") are
one-shot: delivered in real time via OnIncoming and consumed by the caller.
Their TaskID is an *outgoing* task that never exists in s.tasks, so several
inbox-cleanup paths missed them and they lingered in the persisted snapshot,
re-surfacing on every read/wake. Four fixes:

- CompleteTask: drop matching inbox entries *before* the s.tasks lookup, so a
  synthetic reply (absent from s.tasks) is cleared instead of returning
  ErrTaskNotFound and leaving the entry behind.
- appendSyntheticReply: skip contentless completions (a bare ack with no reply
  text) — the terminal state is already on the task, so an empty record is
  pure noise.
- evictTerminal: evict aged one-shot reply records by identity (MessageID
  prefix) past a short delivery TTL, since the by-taskID drop never reaches
  them.
- PeekInbox: consume one-shot reply records on read so a peek (vs a drain)
  doesn't leave them to re-surface; genuine incoming task messages are
  untouched, keeping peek non-destructive.

Adds table tests covering all four paths and updates the inbox hook to select
the live-PID inbox and prune stale-PID snapshot files.

Signed-off-by: terafin <jwood@me.com>
@intarweb intarweb closed this by deleting the head repository Jul 20, 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