Skip to content

fix(mcp): stop classifying devices as offline in list_devices#3299

Merged
saddlepaddle merged 1 commit into
mainfrom
saddlepaddle/satya/fix/device-offline-worktree-selection
Apr 9, 2026
Merged

fix(mcp): stop classifying devices as offline in list_devices#3299
saddlepaddle merged 1 commit into
mainfrom
saddlepaddle/satya/fix/device-offline-worktree-selection

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Apr 9, 2026

Summary

  • Bug: device reported offline ~60 seconds after the desktop app started, regardless of whether the user was on a non-main worktree. Juston's Slack repro narrowed it to the Slack agent rendering status: offline for his device.
  • Root cause: PR Remove device heartbeat polling to reduce Vercel costs #2904 deliberately removed the 30s device heartbeat (desktop now calls device.registerDevice once at startup) because executeOnDevice already handles unreachable devices via its command-poll timeout. PR [codex] Fix list_devices MCP schema #2988 ("fix list_devices MCP schema") reintroduced an isOnline field computed from a 60s lastSeenAt window without restoring heartbeats, so every device always looked offline after ~60s of runtime.
  • Fix: revert the isOnline/includeOffline reintroduction in list_devices (keep PR [codex] Fix list_devices MCP schema #2988's schema hardening — datetime, deviceTypeValues enum, nullable deviceName). Update the Slack integration's fetchAgentContext to stop rendering the removed status, and refresh apps/api/MCP_TOOLS.md, apps/docs/content/docs/mcp.mdx, and the stubbed superset devices list CLI flag to match.
  • Drive-by: packages/mcp/src/tools/devices/list-devices/list-devices.test.ts had a mock.module("../../utils", ...) that replaced the module with a partial stub. Because mock.module is process-global in bun test, it bled into start-agent-session.test.ts and stripped the executeOnDevice export. The test file now spreads the real module before overriding getMcpContext.

Test plan

  • bun run lint
  • bun run typecheck
  • bun run test (all 8 turbo tasks green; @superset/mcp 7/7 pass including the previously-broken start-agent-session suite)
  • Manual: with a desktop device registered and the app idle >60s, ask the Slack bot to list devices — the response should no longer say "offline"

Summary by cubic

Stop marking devices as offline. list_devices now returns all registered devices without an online status, so Slack no longer shows “status: offline” after ~60s.

  • Bug Fixes
    • Removed isOnline and includeOffline from list_devices; continue returning lastSeenAt.
    • Slack agent: call list_devices with {} and stop rendering status text.
    • Docs and CLI: updated descriptions; removed CLI includeOffline flag and the “status” column.
    • Tests: fixed mock.module bleed by preserving real exports when overriding getMcpContext.

Written for commit 8e376e8. Summary will update on new commits.

Summary by CodeRabbit

  • Changes

    • Device lists now display all registered devices in your organization instead of limiting to online devices only.
    • Removed online/offline status indicators from device displays across all interfaces.
    • Simplified device listing by removing filtering options.
  • Documentation

    • Updated device tool documentation to reflect the new device listing scope.

PR #2904 deliberately removed device heartbeat polling (desktop calls
device.registerDevice once at startup) because executeOnDevice already
handles unreachable devices via its command-poll timeout. PR #2988
reintroduced an isOnline field computed from a 60s lastSeenAt window
without restoring heartbeats, so every device looked offline ~60s after
app start — surfacing in the Slack integration agent as "status: offline".

Revert the isOnline reintroduction (keep #2988's schema hardening), drop
includeOffline from the input, update the Slack run-agent to stop
rendering the removed status, and refresh the docs + stubbed CLI flag.
Also fix a pre-existing mock-module bleed in list-devices.test.ts that
was stripping executeOnDevice from start-agent-session.test.ts.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

The list_devices MCP tool interface is simplified by removing the includeOffline input parameter and the isOnline output field. The tool now returns all registered devices instead of filtering by online status. This change cascades across the tool implementation, tests, API integrations, CLI command definitions, and documentation.

Changes

Cohort / File(s) Summary
Core Tool Implementation
packages/mcp/src/tools/devices/list-devices/list-devices.ts, packages/mcp/src/tools/devices/list-devices/list-devices.test.ts
Removed includeOffline input parameter and isOnline output field. Tool now returns all registered devices. Updated test fixtures and assertions to reflect new behavior; adjusted module mocking to preserve real utility exports.
MCP Tool Contract Documentation
apps/api/MCP_TOOLS.md
Updated list_devices tool description from "List online devices" to "List registered devices," removed includeOffline from input schema, and removed isOnline field from device output object.
Tool Integration
apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts
Removed includeOffline: true argument from list_devices invocation and adjusted device context formatting to exclude online/offline status rendering.
CLI Command Definition
packages/cli/src/commands/devices/list/command.ts
Removed includeOffline option from command interface and removed status column from table display schema, leaving only deviceName, deviceType, and lastSeen.
Product Documentation
apps/docs/content/docs/mcp.mdx
Updated list_devices tool description from "List online devices in your organization" to "List registered devices in your organization."

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Devices now list without the "are you there?"
No online/offline status floating in air,
All registered friends in a simplified way,
Hop along, agents—no filtering delays!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix(mcp): stop classifying devices as offline in list_devices' clearly and concisely summarizes the main change—removing the isOnline field from list_devices to fix the bug where devices appeared offline after ~60 seconds.
Description check ✅ Passed The PR description includes all required sections: a detailed Summary explaining the bug and root cause, Related Issues context (PR #2904 and #2988), Type of Change (Bug fix), Testing section with completed checklist items, and Additional Notes with drive-by fix details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch saddlepaddle/satya/fix/device-offline-worktree-selection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 9, 2026

Greptile Summary

This PR fixes a regression where devices were incorrectly shown as offline ~60 seconds after the desktop app started. PR #2904 removed the 30s heartbeat (desktop now calls registerDevice once at startup), but PR #2988 reintroduced an isOnline field computed from a 60s lastSeenAt window without restoring heartbeats — making every device appear offline after ~60s.

Key changes:

  • list-devices.ts: Strips isOnline computation and the includeOffline filter entirely; all registered devices are now returned unconditionally, consistent with the heartbeat-less design.
  • list-devices.test.ts: Fixes a mock.module process-global leak in bun test that was stripping executeOnDevice from the ../../utils module for sibling test suites (spreads real exports before overriding getMcpContext).
  • run-agent.ts: Updates fetchAgentContext device rendering to drop the removed status field so the Slack bot no longer labels devices as "offline".
  • command.ts: Removes the stub --include-offline CLI flag added alongside the removed feature.
  • MCP_TOOLS.md / mcp.mdx: Partially refreshed — some residual inaccuracies remain (schema nullability, missing ownerEmail, stale heartbeat language, and a misleading "who's online" example).

Confidence Score: 4/5

Safe to merge — the core bug fix is correct and well-tested; only documentation inaccuracies remain.

The runtime bug fix (removing the 60s isOnline window) is correct, the bun test mock leak fix is sound, and all 7 MCP tests pass. The only open items are documentation inconsistencies in MCP_TOOLS.md (stale heartbeat language, schema nullability, missing ownerEmail) and a misleading example in mcp.mdx — none of these affect runtime behavior.

apps/api/MCP_TOOLS.md — schema block for list_devices is still partially out of sync with the implementation (nullable fields, missing ownerEmail, stale heartbeat constraint).

Vulnerabilities

No security concerns identified. The change removes a time-based isOnline filter from a read-only listing tool — all devices returned are already scoped to the caller's organizationId via the existing where clause, so the broader result set does not expose cross-organization data.

Important Files Changed

Filename Overview
packages/mcp/src/tools/devices/list-devices/list-devices.ts Removes isOnline computation and includeOffline filter from the device query; all registered devices are now returned regardless of lastSeenAt, matching the heartbeat-less design of PR #2904.
packages/mcp/src/tools/devices/list-devices/list-devices.test.ts Fixes a process-global mock.module leak by spreading real exports of ../../utils before overriding getMcpContext, preventing executeOnDevice from being stripped in sibling test suites.
apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts Updates fetchAgentContext device rendering to drop the removed status field; device lines now show only name, id, and owner — no "offline" label.
packages/cli/src/commands/devices/list/command.ts Removes the --include-offline stub option added alongside the removed isOnline field; display columns updated to drop the obsolete status column.
apps/api/MCP_TOOLS.md Partially refreshed: isOnline/includeOffline removed, but the listDevicesOutput schema still shows deviceName and ownerName as non-nullable and is missing the ownerEmail field; Device Targeting section still references the removed "heartbeat within last 60s" constraint.
apps/docs/content/docs/mcp.mdx Docs updated to remove isOnline/includeOffline references, but the example prompt "Show me who's online in my team" still implies device online-status visibility.

Sequence Diagram

sequenceDiagram
    participant Slack as Slack Bot
    participant Agent as fetchAgentContext
    participant MCP as MCP list_devices
    participant DB as DB (devicePresence)

    Slack->>Agent: fetch context for agent session
    Agent->>MCP: callTool("list_devices", {})
    MCP->>DB: SELECT deviceId, deviceName, deviceType, lastSeenAt, userId, name, email WHERE organizationId = ctx.organizationId ORDER BY lastSeenAt DESC
    DB-->>MCP: all registered devices (no time filter)
    MCP-->>Agent: { devices: [...] }
    Note over Agent: Renders: "- DeviceName (id: ..., owner: ...)"
    Note over Agent: (no status field — "offline" label removed)
    Agent-->>Slack: context string injected into system prompt
Loading

Comments Outside Diff (2)

  1. apps/api/MCP_TOOLS.md, line 15-18 (link)

    P2 Stale "heartbeat within last 60s" language

    This bullet still says a device must be online via heartbeat within the last 60 seconds to receive commands, but PR Remove device heartbeat polling to reduce Vercel costs #2904 deliberately removed the 30s heartbeat — the desktop now only calls registerDevice once at startup. The "within last 60s" constraint is no longer enforced, and executeOnDevice handles unreachable devices via its command-poll timeout instead. The description should be updated to reflect the actual behavior.

  2. apps/docs/content/docs/mcp.mdx, line 274 (link)

    P2 Example references removed "online" concept

    The example prompt "Show me who's online in my team" implies that list_devices returns online/offline status, but that's exactly what this PR removes. Since no status field is returned anymore, this example would produce a confusing result (or lead a user to believe the feature still exists). Consider updating to match current behavior.

Reviews (1): Last reviewed commit: "fix(mcp): stop classifying devices as of..." | Re-trigger Greptile

Comment thread apps/api/MCP_TOOLS.md
Comment on lines 168 to 179
@@ -176,7 +174,6 @@ const listDevicesOutput = z.object({
ownerId: z.string().uuid().describe("User who owns this device"),
ownerName: z.string().describe("Name of device owner"),
lastSeenAt: z.string().datetime(),
isOnline: z.boolean(),
})),
});
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 listDevicesOutput schema is out of sync with the implementation

The schema documented here no longer matches the actual Zod schema in packages/mcp/src/tools/devices/list-devices/list-devices.ts. Three differences:

  1. deviceName is z.string().nullable() in the implementation, but shown as non-nullable here.
  2. ownerName is z.string().nullable() in the implementation, but shown as non-nullable here.
  3. ownerEmail: z.string() is present in the implementation but is missing entirely from this schema block.

The PR description says this file was refreshed, but the schema block was not updated to match.

Suggested change
const listDevicesOutput = z.object({
devices: z.array(z.object({
deviceId: z.string(),
deviceName: z.string().nullable(),
deviceType: z.enum(["desktop", "mobile", "web"]),
lastSeenAt: z.string().datetime(),
ownerId: z.string(),
ownerName: z.string().nullable(),
ownerEmail: z.string(),
})),
});

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/api/MCP_TOOLS.md (1)

163-179: ⚠️ Potential issue | 🟡 Minor

Sync listDevicesOutput docs with the actual tool schema.

The documented output is stale: deviceName/ownerName are nullable in the implementation, and ownerEmail is returned but not documented.

📝 Proposed doc fix
 const listDevicesOutput = z.object({
   devices: z.array(z.object({
     deviceId: z.string(),
-    deviceName: z.string(),
+    deviceName: z.string().nullable(),
     deviceType: z.enum(["desktop", "mobile", "web"]),
     ownerId: z.string().uuid().describe("User who owns this device"),
-    ownerName: z.string().describe("Name of device owner"),
+    ownerName: z.string().nullable().describe("Name of device owner"),
+    ownerEmail: z.string().email().describe("Email of device owner"),
     lastSeenAt: z.string().datetime(),
   })),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/MCP_TOOLS.md` around lines 163 - 179, The docs for list_devices are
out of sync with the tool schema: update the listDevicesOutput schema block so
deviceName and ownerName are nullable (or optional) to match the implementation
and add ownerEmail to the returned fields; specifically edit the
listDevicesOutput definition used in the docs (referencing listDevicesOutput,
deviceName, ownerName, ownerEmail) to reflect the actual types (nullable strings
for deviceName/ownerName and include ownerEmail as a string/uuid/email as per
implementation).
apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts (1)

406-420: ⚠️ Potential issue | 🟠 Major

Bound device context before injecting it into the system prompt.

Now that list_devices returns all registered devices, this section can grow unbounded and degrade latency/cost or hit token limits in larger orgs.

💡 Proposed fix (truncate + summarize)
+const MAX_AGENT_CONTEXT_DEVICES = 50;
+
 async function fetchAgentContext({
 	mcpClient,
 	userId,
@@
 	if (devicesData?.devices?.length) {
-		const lines = devicesData.devices.map(
+		const totalDevices = devicesData.devices.length;
+		const visibleDevices = devicesData.devices.slice(0, MAX_AGENT_CONTEXT_DEVICES);
+		const lines = visibleDevices.map(
 			(d) =>
 				`- ${d.deviceName ?? "Unknown"} (id: ${d.deviceId}, owner: ${d.ownerName ?? d.ownerEmail})`,
 		);
-		sections.push(`Devices:\n${lines.join("\n")}`);
+		const remainder =
+			totalDevices > visibleDevices.length
+				? `\n- ...and ${totalDevices - visibleDevices.length} more`
+				: "";
+		sections.push(`Devices (${totalDevices} total):\n${lines.join("\n")}${remainder}`);
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts`
around lines 406 - 420, The devices block currently injects an unbounded list
from devicesResult.structuredContent (devicesData.devices) directly into
sections, which can blow up prompt size; modify the logic in run-agent.ts that
builds sections to truncate/summarize before pushing: if
devicesData?.devices.length exceeds a safe limit (e.g., 20), include only a
sample (first N) formatted with the existing template and append a summary line
like "…and X more devices (total Y)"; alternatively call a new helper (e.g.,
summarizeDevices or summarizeDevicesList) to produce a short aggregate (owner
counts or truncated list) and use that output instead of the full array,
ensuring the final sections.push uses the bounded string.
🧹 Nitpick comments (1)
packages/mcp/src/tools/devices/list-devices/list-devices.test.ts (1)

131-167: Add a nullable-name fixture to lock schema hardening behavior.

Current assertions cover only non-null names; adding a case for deviceName: null and ownerName: null would protect the nullable output contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mcp/src/tools/devices/list-devices/list-devices.test.ts` around
lines 131 - 167, Add a new test case (or extend the existing "returns every
registered device regardless of lastSeenAt") that uses createTool()/handler to
return a device record where deviceName and ownerName are null; ensure the mock
selectMock payload includes device entries with deviceName: null and ownerName:
null and then assert result.structuredContent?.devices contains those entries
(with lastSeenAt still expect.any(String) and ownerName/deviceName === null).
Also verify the output schema parsing still succeeds by reusing
outputSchema.parse(result.structuredContent) as in the existing test so the
nullable-name contract is locked in.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/api/MCP_TOOLS.md`:
- Around line 163-179: The docs for list_devices are out of sync with the tool
schema: update the listDevicesOutput schema block so deviceName and ownerName
are nullable (or optional) to match the implementation and add ownerEmail to the
returned fields; specifically edit the listDevicesOutput definition used in the
docs (referencing listDevicesOutput, deviceName, ownerName, ownerEmail) to
reflect the actual types (nullable strings for deviceName/ownerName and include
ownerEmail as a string/uuid/email as per implementation).

In `@apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts`:
- Around line 406-420: The devices block currently injects an unbounded list
from devicesResult.structuredContent (devicesData.devices) directly into
sections, which can blow up prompt size; modify the logic in run-agent.ts that
builds sections to truncate/summarize before pushing: if
devicesData?.devices.length exceeds a safe limit (e.g., 20), include only a
sample (first N) formatted with the existing template and append a summary line
like "…and X more devices (total Y)"; alternatively call a new helper (e.g.,
summarizeDevices or summarizeDevicesList) to produce a short aggregate (owner
counts or truncated list) and use that output instead of the full array,
ensuring the final sections.push uses the bounded string.

---

Nitpick comments:
In `@packages/mcp/src/tools/devices/list-devices/list-devices.test.ts`:
- Around line 131-167: Add a new test case (or extend the existing "returns
every registered device regardless of lastSeenAt") that uses
createTool()/handler to return a device record where deviceName and ownerName
are null; ensure the mock selectMock payload includes device entries with
deviceName: null and ownerName: null and then assert
result.structuredContent?.devices contains those entries (with lastSeenAt still
expect.any(String) and ownerName/deviceName === null). Also verify the output
schema parsing still succeeds by reusing
outputSchema.parse(result.structuredContent) as in the existing test so the
nullable-name contract is locked in.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6db04215-834b-4e1a-bb25-8b124a9b9d60

📥 Commits

Reviewing files that changed from the base of the PR and between d1ea876 and 8e376e8.

📒 Files selected for processing (6)
  • apps/api/MCP_TOOLS.md
  • apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts
  • apps/docs/content/docs/mcp.mdx
  • packages/cli/src/commands/devices/list/command.ts
  • packages/mcp/src/tools/devices/list-devices/list-devices.test.ts
  • packages/mcp/src/tools/devices/list-devices/list-devices.ts

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/MCP_TOOLS.md">

<violation number="1" location="apps/api/MCP_TOOLS.md:167">
P2: `listDevicesOutput` schema in MCP_TOOLS.md is still out of sync with the implementation after this refresh: `deviceName` and `ownerName` should be `.nullable()`, and `ownerEmail: z.string()` is missing entirely. The implementation in `list-devices.ts` and the type annotation in `run-agent.ts` both confirm these three differences.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/api/MCP_TOOLS.md
@@ -161,12 +161,10 @@ const listTaskStatusesOutput = z.object({
These tools write to `agent_commands` table and poll for results.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

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

P2: listDevicesOutput schema in MCP_TOOLS.md is still out of sync with the implementation after this refresh: deviceName and ownerName should be .nullable(), and ownerEmail: z.string() is missing entirely. The implementation in list-devices.ts and the type annotation in run-agent.ts both confirm these three differences.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/MCP_TOOLS.md, line 167:

<comment>`listDevicesOutput` schema in MCP_TOOLS.md is still out of sync with the implementation after this refresh: `deviceName` and `ownerName` should be `.nullable()`, and `ownerEmail: z.string()` is missing entirely. The implementation in `list-devices.ts` and the type annotation in `run-agent.ts` both confirm these three differences.</comment>

<file context>
@@ -161,12 +161,10 @@ const listTaskStatusesOutput = z.object({
-const listDevicesInput = z.object({
-  includeOffline: z.boolean().default(false).describe("Include recently offline devices"),
-});
+const listDevicesInput = z.object({});
 
 const listDevicesOutput = z.object({
</file context>
Fix with Cubic

@saddlepaddle saddlepaddle merged commit a8f560d into main Apr 9, 2026
15 checks passed
MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request Apr 10, 2026
…et-sh#3299)

PR superset-sh#2904 deliberately removed device heartbeat polling (desktop calls
device.registerDevice once at startup) because executeOnDevice already
handles unreachable devices via its command-poll timeout. PR superset-sh#2988
reintroduced an isOnline field computed from a 60s lastSeenAt window
without restoring heartbeats, so every device looked offline ~60s after
app start — surfacing in the Slack integration agent as "status: offline".

Revert the isOnline reintroduction (keep superset-sh#2988's schema hardening), drop
includeOffline from the input, update the Slack run-agent to stop
rendering the removed status, and refresh the docs + stubbed CLI flag.
Also fix a pre-existing mock-module bleed in list-devices.test.ts that
was stripping executeOnDevice from start-agent-session.test.ts.
@Kitenite Kitenite deleted the saddlepaddle/satya/fix/device-offline-worktree-selection branch April 13, 2026 16:35
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.

1 participant