Skip to content

fix(tell-agent): follow the agent directory so new sessions get a pill - #158

Merged
omercnet merged 2 commits into
omercnet:mainfrom
qinkangdeid:fix/tell-agent-observe-agents
Sep 23, 2026
Merged

omercnet merged 2 commits into
omercnet:mainfrom
qinkangdeid:fix/tell-agent-observe-agents

Conversation

@qinkangdeid

Copy link
Copy Markdown
Contributor

Problem

On Paseo 0.9.x, the "Tell agent" composer pill only appears on agent sessions that already existed when the plugin loaded. Start a new agent session after the plugin has loaded and it gets no pill; reload the plugin and the pill appears.

Cause

Since Paseo 0.9 (getpaseo/paseo#4596), client.paseo.agents.subscribe(handler) only registers a local listener on observations opened by that same API instance. The v0.9.1 SDK docs (public-docs/sdk/events.md) state:

client.agents.subscribe() and agent-handle subscribe() add local listeners to observations owned by that API instance. They do not request data.

contributeAgentMessaging relied on agents.subscribe() plus a one-shot loadAgents() seed at plugin startup, so it never learned about agents created afterwards. Measured on a 0.9.1 daemon with two API instances side by side for 60 seconds: the one that only called subscribe() received 0 agent_update messages, while the one using list({ subscribe: {} }) received 20.

The context-mode plugin in this repo already follows its pills through a list({ subscribe: {} }) observation; this applies the same approach to tell-agent.

Fix

  • client/agents.ts: new followAgentDirectory(paseo, follower). On 0.9 clients it opens an observation with list({ sort: updated_at desc, page: 200, subscribe: {}, signal }) and drives the follower from subscription.subscribe({ snapshot, update }). The snapshot only covers the first page, so remaining pages are read with plain list() calls under the existing 200 × 10 cap; updates that land while later pages are being read override what those pages say, and a fresh snapshot after a reconnect invalidates any in-flight continuation read. Once the directory is read completely, agents absent from it get their pill removed; if the read is truncated or fails midway, only upserts are applied. When the client releases a failed observation it is reopened with 2 s → 60 s backoff. On cleanup the abort signal is aborted and the subscription released.
  • client/message-agent.tsx: pills are now driven by followAgentDirectory, reusing the existing syncAgent (no re-registration while workspace is unchanged) and removePill. The popover's target list still loads on demand via useQuery + loadAgents() paging and is unchanged.

Compatibility

The 0.9 observation path is gated on typeof paseo.observeEvents === "function". observeEvents was added to the SDK in v0.9.0-beta.1 together with the new owned-subscription model, so it is a reliable presence check. This matters because the plugin manifest allows ^0.8.0 || ^0.9.0-beta.1: a 0.8 client must not send subscribe at all — per docs/protocol-compatibility.md in Paseo v0.9.1, "Old clients keep their existing wire shapes and slot behavior", and the legacy connection keeps a single subscription slot per kind where a later send overwrites the earlier one. A plugin-initiated subscribe on such a connection would replace the host app's own agents subscription. When observeEvents is absent, the original agents.subscribe() + loadAgents() path is kept unchanged. (The context-mode plugin uses the observation unconditionally; it may have the same issue on 0.8 clients, which this PR deliberately does not touch.)

Relation to #153

#153 ("avoid unsupported client type import") also touches tell-agent, but only the type-import header of client/agents.ts — it derives PaseoAgentListResult/PaseoApi types from @getpaseo/plugin/client instead of importing them from @getpaseo/client. This PR keeps the @getpaseo/client import and adds PaseoAgentUpdate to it, so the two branches conflict textually in that import block. The conflict is mechanical to resolve (apply #153's derivation and derive the update type the same way, e.g. from PaseoApi["agents"]["subscribe"]); nothing else overlaps — message-agent.tsx and tests/agents.test.ts are untouched by #153.

Testing

With bun 1.4.2 (bun install --frozen-lockfile at the repo root), the CI jobs that apply to tell-agent:

$ cd tell-agent
$ npm run check        # biome check .
Checked 12 files in 9ms. No fixes applied.
$ npm run typecheck    # tsc --noEmit
$ npm run test         # vitest run
Test Files  3 passed (3)
     Tests  12 passed (12)

tests/agents.test.ts (new) covers the 0.9 single-page, multi-page, update-overrides-page, cap-truncation, reconnect-invalidates-paging, and failure-reopen paths, plus the 0.8 legacy path (no subscribe sent, agents.subscribe() + one-shot list). tell-agent defines no build or plugin-validation scripts in CI, so none were run.

Copilot AI lite review requested due to automatic review settings September 23, 2026 10:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Two critical and two moderate issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity · 2 Medium severity

Open (4)
What changed in this PR

Adds live agent-directory tracking so Tell Agent pills appear for newly created sessions while preserving Paseo 0.8 compatibility.

Changes:

  • Adds observation-based paging, reconciliation, reconnect handling, and legacy fallback.
  • Drives composer pills from directory updates.
  • Adds coverage for paging, failures, reconnects, and compatibility.
File Reviewed changes and findings
tell-agent/​tests/​agents.test.ts Adds coverage for directory following and compatibility behavior.
tell-agent/​client/​message-agent.tsx Connects pill lifecycle to the new directory follower.
tell-agent/​client/​agents.ts Implements directory following. Findings: unsupported @getpaseo/client type dependency (critical, 2 votes); stale continuation after observation failure (critical, 2 votes); unreleased subscription during cleanup (moderate, 3 votes); continuation reads do not use the lifetime signal (moderate, 2 votes).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tell-agent/client/agents.ts Outdated
Comment thread tell-agent/client/agents.ts
Comment thread tell-agent/client/agents.ts
Comment thread tell-agent/client/agents.ts Outdated
@omercnet

Copy link
Copy Markdown
Owner

Thanks for the careful directory-follower implementation and paging coverage. The core diagnosis is right.

I narrowed the compatibility boundary to Paseo 0.9. observeEvents identifies the client API shape, not whether the connected daemon negotiated owned subscriptions. Retaining a 0.8 manifest floor would still allow legacy last-query-wins directory coupling on a 0.9 app connected to a 0.8 daemon.

The focused correction now requires Paseo 0.9, removes the legacy fallback, restores the supported plugin-client type boundary, invalidates a pending continuation after terminal observation failure, and propagates the lifetime signal to continuation reads. The added regression proves a pending page cannot apply stale directory state after the observation fails.

This keeps #158 limited to Tell Agent. The analogous migrations belong in separate plugin PRs.

qinkangdeid and others added 2 commits September 23, 2026 19:08
Since Paseo 0.9 (getpaseo/paseo#4596), agents.subscribe(handler) only
registers a local listener on that API instance's observations and does
not request data from the daemon; the plugin also seeds once via
loadAgents() at startup, so agents created afterwards only get the
"Tell agent" pill after the plugin is reloaded.

- client/agents.ts: add followAgentDirectory. It feature-detects via
  paseo.observeEvents (added in 0.9.0-beta.1 together with the new
  subscription model). On 0.9 clients it opens an observation with
  list({ sort: updated_at desc, page: 200, subscribe: {}, signal });
  the snapshot (first connect and every reconnect) only covers the
  first page, so remaining pages are read with plain list() calls
  under the existing 200x10 cap, updates arriving meanwhile override
  stale page data, and a fresh snapshot after a reconnect invalidates
  in-flight continuation reads. Pills for agents missing from the
  fully-read snapshot are removed; on truncation or continuation
  failure only upsert is performed. When the observation is released
  by the client it is reopened with 2s to 60s backoff; on stop the
  signal is aborted and the subscription released. On 0.8 clients the
  subscribe option is not sent (the legacy connection keeps one
  subscription slot per kind and later sends overwrite earlier ones,
  which would clobber the host app's own agent subscription), so the
  original agents.subscribe() + loadAgents() path is kept.
- message-agent.tsx: pills are now driven by followAgentDirectory,
  reusing syncAgent (no re-registration when the workspace is
  unchanged); the popover target list still loads on demand via
  useQuery and is unchanged.
- tests/agents.test.ts: cover the 0.9 single-page, multi-page, update
  override, cap truncation, reconnect invalidation, and failure
  reopen paths, plus the 0.8 legacy path.
@omercnet
omercnet force-pushed the fix/tell-agent-observe-agents branch from 6dcac8d to 6a3b733 Compare September 23, 2026 19:10
@omercnet
omercnet merged commit aadeaac into omercnet:main Sep 23, 2026
9 checks passed
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.

3 participants