Skip to content

gcp-watcher dedupe fails open: ctx.memory.recall returns nothing for opaque payloads, so it re-posts every scan #109

Description

@khaliqgant

ctx.memory-backed dedupe fails open, and gcp-watcher is very likely re-posting on every scan

Found while designing a new proactive agent, by reading the existing ones as reference. Two independent things combine into a live defect.

1. The substrate: ctx.memory.recall is semantic and returns nothing for opaque payloads

This is not inferred — it is documented in shipped code by the agent that hit it and worked around it.

internal-agents/market-intel/x-reply-radar/agent.ts:742-746:

ctx.memory.recall is SEMANTIC (Supermemory): it ranks stored items by how similar their content is to the query and ignores tags. Stashing opaque ids under a tag and recalling with generic text returned nothing — so the seen-set was always empty and every link looked fresh (the repeats).

And persona.ts:165-169, blunter:

saves succeed (return an id) but Supermemory /v3/search returns 0 even for a doc saved seconds earlier, so loadSeen has been coming back empty (seen:0 every run).

x-reply-radar abandoned memory-based dedup as a result and now relies on contiguous time windows.

2. gcp-watcher uses exactly the pattern that fails

agents/gcp-watcher/agent.ts:

:268  const [item] = await ctx.memory.recall('gcp snapshot', { tags: ['gcp-watcher:snapshot'], limit: 1 });
:277  await ctx.memory.save(JSON.stringify(snap), { tags: ['gcp-watcher:snapshot'], scope: 'workspace' });
:158  const signature = lines.slice().sort().join('\n');
:159  if (lines.length > 0 && signature !== last?.signature) { ...post... }

The query is the generic string gcp snapshot; the stored content is an opaque {"signature":"…"} blob. Content similarity between them is negligible, and tags are ignored. So recall returns nothing, last is undefined, signature !== last?.signature is always true, and it posts on every scan where any signal is firing.

Expected impact: repeated identical Slack alerts on every tick for as long as any GCP signal stays lit. Please confirm against the channel — if the alerts are repeating, this is the cause.

3. Why "fails open" is the important phrase

A dedupe that errors is safe: it announces itself. This one silently returns an empty seen-set, re-notifies everyone, and reports success. The agent looks healthy in every log while doing precisely the thing dedupe exists to prevent. Any agent adopting this pattern inherits it invisibly.

Also in the same file, independent of the above

agent.ts:158 computes one global signature over the whole alert set (lines.sort().join()) and posts if it changed. Acceptable for a handful of infra signals, but it means one new line re-posts every line. Any consumer scaling this pattern to a per-item domain gets a full re-broadcast on every single change.

Other call sites worth checking

  • internal-agents/github-inbox/agent.ts:260 — recalls with the bare tag string as the query, same shape. Also :120 has a silent 500-id cap.
  • internal-agents/.../hn-monitorsafe, apparently by accident: it recalls with the long descriptive sentence that is also embedded in the saved content (agent.ts:1539/1556), which is the workaround x-reply-radar arrived at by trial. Also backstopped by ctx.files.

Asked for

  1. Confirm whether the GCP Slack channel is showing repeats. That settles impact in one look.
  2. Move gcp-watcher and github-inbox dedupe to ctx.files (exact keyed read/write), or to the descriptive-sentence recall pattern hn-monitor and x-reply-radar use.
  3. Make the failure mode loud. If a seen-set comes back empty when the agent has previously saved, that should be surfaced, not treated as "nothing seen yet". Absence of knowledge is not permission to notify.
  4. Longer-term: either fix /v3/search cloud-side or document that ctx.memory must never back an idempotency decision. Today the same API name means "semantic recall" and is being read as "key-value lookup" by callers.

Filed by Chief on behalf of a design review. Source quotes verified directly against both repos. Not labelled for dispatch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions