agent-session-intercom

CLI and typed API for messaging agents in existing Codex app-server sessions


License
MIT
Install
npm install agent-session-intercom@1.0.0

Documentation

agent-session-intercom

agent-session-intercom is a local CLI and typed ESM API for communicating with agents in sessions owned by an already-running Codex app-server. It connects through the app-server WebSocket Unix control socket; it does not start Codex or create a parallel resumed CLI session.

Requirements

  • Node.js 24.18 or newer
  • a running Codex app-server with its Unix control socket enabled

Installation

npm install --global agent-session-intercom@1.0.0
agent-session-intercom --version
agent-session-intercom --help

Source development uses pnpm 11.21 and an optional local link; see docs/DEVELOPMENT.md. Registry installation does not require the project checkout or its node_modules.

Session discovery checks the default control socket under CODEX_HOME, an optional CODEX_APP_SERVER_SOCKET, and previously registered custom sockets. Pass --socket <path> for a single-endpoint query. A successful explicit connection registers its canonical endpoint for later discovery; an environment-only endpoint stays ephemeral.

List sessions

agent-session-intercom sessions
agent-session-intercom sessions --search "report" --json
agent-session-intercom sessions --cwd "$PWD" --ndjson

The listing probes candidate endpoints concurrently with bounded connection timeouts. One stale socket is reported as unreachable without hiding sessions from healthy endpoints. Each session contains a stable endpoint id and composite address such as ep_xxx:thread-id, plus thread id, title, status, timestamp, and only the basename of the project directory. It does not return socket paths, transcript contents, or the absolute working directory. The CLI and typed APIs accept integer limits from 1 through 500 and reject invalid values before issuing a thread/list request. Human output includes a safe endpoint-id/source line for every unreachable candidate, so a partial or all-failed discovery is not presented as a complete empty result.

Manage custom endpoints

agent-session-intercom endpoints add /path/to/app-server.sock --label reports --json
agent-session-intercom endpoints list --json
agent-session-intercom endpoints remove <endpoint-id> --json

endpoints add verifies the existing app-server before saving it. The private registry is stored under CODEX_HOME with user-only permissions. Default output contains the stable endpoint id, optional label, and reachability only; the socket mapping remains private. Custom filesystem socket paths cannot be enumerated automatically, so register each one once or use it successfully through --socket. The registry accepts up to 62 custom endpoints, reserving discovery capacity for the default and environment sources. If that catalog is full, an explicitly selected live socket remains usable; JSON output reports registration: "registry-full" (or endpointRegistration for send) and human output warns that the endpoint was not persisted.

Send a message

agent-session-intercom send <target-thread-id> "Please review the report." \
  --from-project <sender-project> \
  --message-id <uuid> \
  --ttl-ms 300000 \
  --json

Use the composite endpointId:threadId returned by sessions to select an endpoint directly. A bare thread id is resolved across reachable candidates when it has one match; duplicate matches fail without delivery and require the composite address or an explicit --socket. When complete membership scanning has no match, the configured single-socket route remains the compatibility fallback. An exact contact alias created with contacts add takes precedence over thread-id and metadata lookup. Bare-id resolution paginates each reachable endpoint to establish membership beyond the ordinary 500-session display limit. If pagination fails, cycles, or reaches its safety ceiling before absence can be proved, the command rejects and requires a composite address or explicit socket instead of guessing.

Inside Codex, the CLI reads the sender thread from CODEX_THREAD_ID and proves that it exists on exactly one reachable source endpoint. When the same thread id exists on multiple endpoints, select the source with --from-endpoint <endpoint-id> or AGENT_SESSION_INTERCOM_SOURCE_ENDPOINT. A caller that supplies --from-thread must also supply --from-endpoint; the target connection never silently becomes the source address.

The reliable send path creates envelope version 2 and returns exactly delivered, rejected, or unknown. delivered means the app-server acknowledged turn/start or turn/steer, not that the agent finished the work. unknown means a turn request may have reached app-server but its acknowledgment was lost. Only delivered exits zero. Supply a stable --message-id to deduplicate retries; the same id with changed text, source, target, TTL, sender metadata, or correlation id is rejected. An unknown record is not sent again unless the caller adds --retry-unknown. --retry-count, --retry-delay-ms, --retry-backoff, and --retry-jitter bound automatic retry of overload and other failures known to occur before acceptance. --ttl-ms is anchored to the first attempt and is not extended by duplicates or retries.

Address book and replies

agent-session-intercom who --json
agent-session-intercom contacts add report-agent <endpoint-id>:<thread-id> --json
agent-session-intercom find report-agent --json
agent-session-intercom contacts rename report-agent reports --json
agent-session-intercom reply <original-message-id> "Received; I am checking it." --json
agent-session-intercom contacts remove reports --json

Aliases are trimmed, case-normalized, unique, and bound to one canonical endpoint-plus-thread address. Changing an alias requires contacts rename; adding the same alias for another session or a second alias for the same session is rejected. who combines bounded live discovery with known contacts and refreshes only their project, display label, and last-seen metadata. It never reads transcript bodies. find gives an exact durable alias precedence without requiring live discovery. Otherwise it searches safe alias/project/title/thread metadata only after independently scanning every reachable endpoint to completion. That scan is bounded to 100 pages per endpoint; a cursor cycle, protocol failure, or remaining page at the bound is an explicit non-success rather than a false none or one. Complete searches return none, one, or many; none and many exit nonzero instead of guessing.

reply looks up the original delivery receipt by message id, verifies that the current sender is the original target using both endpoint id and thread id, and routes to the original sender through the private endpoint catalog. Use --from-thread together with --from-endpoint as an explicit validated sender override when CODEX_THREAD_ID is unavailable. The reply receives a new message id and sets its correlation id to the original message id; it never copies the original message body or trusts a socket path from message text.

Envelope schema

The reliable CLI model-visible input begins with [agent-session-intercom/v2], followed by JSON with this shape:

{
  "protocol": "agent-session-intercom",
  "version": 2,
  "messageId": "16e66e8a-008c-4f09-86d7-dcaef30c7d36",
  "sentAt": "2026-08-14T17:30:00.000Z",
  "expiresAt": "2026-08-14T17:35:00.000Z",
  "correlationId": "optional-caller-correlation",
  "sender": {
    "kind": "codex-agent",
    "threadId": "sender-thread-id",
    "address": {
      "scheme": "codex-thread",
      "endpointId": "ep_1234567890123456789012",
      "threadId": "sender-thread-id"
    },
    "project": { "name": "source-project" },
    "label": "optional-agent-label"
  },
  "replyTo": {
    "scheme": "codex-thread",
    "endpointId": "ep_1234567890123456789012",
    "threadId": "sender-thread-id"
  },
  "target": {
    "scheme": "codex-thread",
    "endpointId": "ep_abcdefghijklmnopqrstuv",
    "threadId": "target-thread-id"
  },
  "content": { "type": "text", "text": "Please review the report." }
}

The runtime schema requires UUID and ISO anchors, canonical endpoint/thread addresses, a reply address that exactly matches the sender address, a later expiry, and non-empty metadata/text. Socket paths are never part of the envelope. The exported version-1 schema and low-level sendMessage API remain available for existing callers; reliable delivery uses version 2.

Typed API

import {
  AppServerClient,
  classifyAppServerDispatchError,
  deliverReliably,
  dispatchSerializedMessage,
  loadEnvironment,
  LocalStateStore,
  resolveSource,
  resolveTarget,
  serializeEnvelopeV2,
} from 'agent-session-intercom';

const environment = loadEnvironment();
const store = new LocalStateStore(environment.statePath);
const targetInput = process.argv[2];
if (targetInput === undefined) {
  throw new Error('Pass a contact alias, thread id, or endpointId:threadId as argv[2]');
}
if (environment.senderThreadId === undefined) {
  throw new Error('Run inside Codex or set CODEX_THREAD_ID');
}

const target = await resolveTarget({
  address: targetInput,
  environment,
  store,
});
const source = await resolveSource({
  sourceThreadId: environment.senderThreadId,
  ...(environment.sourceEndpointId === undefined
    ? {}
    : { sourceEndpointId: environment.sourceEndpointId }),
  allowEndpointInference: true,
  environment,
  store,
});

const delivery = await deliverReliably({
  store,
  classifyDispatchError: classifyAppServerDispatchError,
  sender: {
    kind: 'codex-agent',
    threadId: environment.senderThreadId,
    address: source,
    project: { name: environment.defaultProjectName },
  },
  target: {
    scheme: 'codex-thread',
    endpointId: target.endpoint.endpointId,
    threadId: target.threadId,
  },
  text: 'Ping',
  ttlMs: 300_000,
  dispatch: async (envelope) => {
    const targetClient = new AppServerClient({ socketPath: target.endpoint.socketPath });
    await targetClient.connect();
    try {
      return await dispatchSerializedMessage(
        targetClient,
        target.threadId,
        serializeEnvelopeV2(envelope),
      );
    } finally {
      await targetClient.close();
    }
  },
});
console.log(delivery);
process.exitCode = delivery.status === 'delivered' ? 0 : 1;

The low-level version-1 sendMessage API remains available for compatible callers that already selected a connection. New callers should use deliverReliably; its state contains hashes, canonical endpoint/thread addresses, timestamps, status, and safe acceptance metadata, never message text. The exported contact schemas, complete-session scanner, peer-summary helpers, state methods, and prepareReplyDelivery provide the same address-book, completeness, and endpoint-plus-thread authorization contracts to typed callers. Durable schema v3 adds contacts without changing v1/v2 delivery behavior.

Machine output and errors

--json emits one versioned JSON object. --ndjson emits the same object as one JSON line. Diagnostics and errors go to stderr, and a failed command exits nonzero. Raw WebSocket frames, credentials, transcript bodies, and absolute home paths are not emitted by default.

Exit code Meaning
0 The command succeeded. For send and reply, this requires delivery status delivered; for find, it requires exactly one match. Successful listing, who, contact mutation, and endpoint management also exit zero.
1 The command did not succeed. This includes delivery status rejected or unknown, find with none or many, and validation, state, discovery, or connection errors. Structured results use stdout; thrown errors use stderr.

Troubleshooting

Command not found after installation

Use Node.js 24.18 or newer. Confirm that the active npm global bin directory is in PATH, then reinstall agent-session-intercom@1.0.0 and run agent-session-intercom --version.

Cannot connect to the Unix socket

Confirm that the Codex app-server is already running and its control socket exists. Set CODEX_APP_SERVER_SOCKET, register it with endpoints add, or pass --socket <path> for a non-default existing socket. The CLI will not start a replacement app-server.

Target is active but has no in-progress turn

This is an inconsistent app-server state. Re-list the session and retry only after its state changes; the CLI intentionally refuses to guess a turn id or silently start a second turn.

Target is not listed

Remove title or working-directory filters and inspect sessions --json. The CLI requests all official interactive, app-server, exec, and sub-agent source kinds. Use the exact returned thread id for delivery.

Safety boundary

Filesystem access to the existing Unix socket is the local authorization boundary. Do not expose or proxy the socket to untrusted users. The package does not add authentication, remote transport, or broadcast.

Development

See docs/DEVELOPMENT.md, docs/TESTING.md, and the authoritative docs/ARCHITECTURE.md. Agent-specific usage is intentionally short in docs/AGENT-USAGE.md.

License

MIT