Skip to content

feat: daemon SDK MVP — connectDaemon, DaemonConnection, DaemonSession#48

Open
varin-nair-factory wants to merge 3 commits into
mainfrom
vn/daemon-sdk-mvp
Open

feat: daemon SDK MVP — connectDaemon, DaemonConnection, DaemonSession#48
varin-nair-factory wants to merge 3 commits into
mainfrom
vn/daemon-sdk-mvp

Conversation

@varin-nair-factory
Copy link
Copy Markdown
Contributor

@varin-nair-factory varin-nair-factory commented May 22, 2026

What

Adds daemon mode as a sibling to the existing exec-based run() / createSession() API. The daemon SDK connects to a running droid daemon over WebSocket and supports both interactive streaming and fire-and-forget headless delegation.

Usage Examples

1. Interactive streaming (connect to a computer via relay)

import { connectDaemon, DroidMessageType, MachineType } from "@factory/droid-sdk";

const daemon = await connectDaemon({
  machine: { type: MachineType.Computer, computerId: "my-desktop" },
  apiKey: factoryApiKey,
});

const session = await daemon.createSession({ cwd: "/my/project" });

for await (const msg of session.stream("Fix the failing tests.")) {
  switch (msg.type) {
    case DroidMessageType.Assistant:
      console.log(msg.text);
      break;
    case DroidMessageType.ToolCall:
      console.log(`[Tool] ${msg.toolUse.name}`);
      break;
    case DroidMessageType.Result:
      console.log(`Done in ${msg.durationMs}ms`);
      break;
  }
}

await session.close();
await daemon.close();

2. Fire-and-forget delegation (Slack/Linear pattern)

import { connectDaemon, AutonomyLevel, MachineType } from "@factory/droid-sdk";

const daemon = await connectDaemon({
  machine: { type: MachineType.Ephemeral, sandboxId, workspaceId },
  apiKey: factoryApiKey,
});

try {
  const session = await daemon.createSession({
    cwd: "/home/user/repo",
    autonomyLevel: AutonomyLevel.High,
    title: "Fix failing tests",
    sessionSource: { platform: "slack", delegationSessionId: threadTs },
  });

  // Returns immediately after daemon ACK — no streaming
  await session.send("Fix the failing tests and open a PR.");
} finally {
  await daemon.close(); // daemon keeps working on the session
}

3. Multi-turn session with follow-up

import { connectDaemon, MachineType } from "@factory/droid-sdk";

const daemon = await connectDaemon({
  machine: { type: MachineType.Computer, computerId: "dev-machine" },
  apiKey: factoryApiKey,
});

const session = await daemon.createSession({ cwd: "/app" });

for await (const msg of session.stream("Add rate limiting to /users.")) {
  // consume first turn
}

for await (const msg of session.stream("Now add tests for it.")) {
  if (msg.type === "assistant") console.log(msg.text);
}

await session.close();
await daemon.close();

4. Resume and interrupt an existing session

import { connectDaemon, MachineType } from "@factory/droid-sdk";

const daemon = await connectDaemon({
  machine: { type: MachineType.Ephemeral, sandboxId, workspaceId },
  apiKey: factoryApiKey,
});

// Resume a previously created session
const session = await daemon.resumeSession(existingSessionId);
await session.send("Also add input validation.");

// Or interrupt a running session
await daemon.interruptSession(runningSessionId);

await daemon.close();

New Public API

  • connectDaemon(options?) — entry point. Resolves SDKMachineConfig to a WebSocket URL, connects, authenticates.
  • DaemonConnectioncreateSession(), resumeSession(), interruptSession(), close()
  • DaemonSessionstream() for interactive use, send() for fire-and-forget
  • WebSocketTransportDroidClientTransport implementation over WebSocket
  • MachineType enum (Ephemeral, Computer, Local)
  • SDKMachineConfig type — discriminated union matching mono-alpha's MachineConfig

Design

  • SharedTransportMultiplexer broadcasts WebSocket messages to multiple DroidClient instances sharing one connection
  • Reuses the entire existing ProtocolEngine / DroidClient / MessageBridge / StreamStateTracker stack — daemon sessions produce the same DroidStreamEvent types as exec mode
  • URL resolution: MachineType.Ephemeral → e2b sandbox URL, MachineType.Computer → Factory relay URL
  • Authentication via daemon.authenticate JSON-RPC handshake (matches mono-alpha protocol)

Consumer Coverage

This MVP covers 100% of headless consumer workflows in mono-alpha:

  • Slack — 6/6 workflows (create, follow-up, interrupt, AskUser)
  • Linear — all daemon-backed workflows
  • Automations — fully
  • Computer provisioning — fully
  • Backend REST API — 5/9 endpoints (gaps: archiveSession, getSessionMessages)

Files

File Purpose
src/daemon/types.ts Types, enums, constants
src/daemon/transport.ts WebSocketTransport with retry + backoff
src/daemon/session.ts DaemonSessionstream() and send()
src/daemon/connection.ts connectDaemon(), DaemonConnection, SharedTransportMultiplexer
src/daemon/index.ts Barrel export
docs/daemon-sdk-api-design.md Full API design + consumer migration breakdown

Validation

  • 679 tests passing (646 existing + 33 new)
  • Zero regressions
  • Typecheck, lint, format all clean

New dependency

  • ws (Node.js WebSocket client) + @types/ws

Adds daemon mode as a sibling to the existing exec-based API. The daemon
SDK connects to a running droid daemon over WebSocket and supports both
interactive streaming and fire-and-forget headless delegation.

New public API:
- connectDaemon() — resolve SDKMachineConfig to WebSocket URL, authenticate
- DaemonConnection — createSession, resumeSession, interruptSession, close
- DaemonSession — stream() for interactive use, send() for fire-and-forget
- WebSocketTransport — DroidClientTransport over WebSocket (ws package)
- MachineType enum, SDKMachineConfig type

Key design:
- SharedTransportMultiplexer broadcasts messages to multiple DroidClient
  instances sharing one WebSocket connection
- Same DroidStreamEvent/MessageBridge/StreamStateTracker stack as exec mode
- URL resolution: MachineType.Ephemeral → e2b sandbox, Computer → relay

Includes API design doc (docs/daemon-sdk-api-design.md) with consumer
migration breakdown for Slack, Linear, automations, and REST API.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@varin-nair-factory varin-nair-factory self-assigned this May 22, 2026
varin-nair-factory and others added 2 commits May 22, 2026 18:25
… routing, token injection

Three critical fixes to make the daemon SDK work with the actual daemon protocol:

1. Method prefix remapping: ProtocolEngine now supports a methodPrefix option
   that remaps droid.* to daemon.* on outgoing requests and daemon.* to droid.*
   on incoming messages. DroidClient passes this through to ProtocolEngine.

2. Session-aware multiplexer: SharedTransportMultiplexer now routes responses
   by matching request IDs, and notifications/server-requests by sessionId.
   This enables correct concurrent multi-session support.

3. Token/sessionId injection: DaemonConnection injects the auth token into
   initialize_session and load_session params (daemon requires it). DroidClient
   injects sessionId into all session-scoped requests when in daemon mode.

Verified against live daemon with 16/16 stress tests passing:
- Basic connect + auth, create session + stream, multi-turn context,
  interrupt, concurrent sessions, error handling, notifications.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Export ensureLocalDaemon and resolveLocalAuthToken from daemon barrel
- Update FACTORY_PROTOCOL_VERSION test to match 1.51.0
- Add local daemon URL resolution tests
- Add local daemon export tests

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
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