Skip to content

Fix cron session key collision: set replyToId to job ID for per-job session key distinction #57

@alexey-pelykh

Description

@alexey-pelykh

Problem

When cron jobs run through ChannelBridge, the ChannelMessage built by buildCronChannelMessage() does not include any job-specific identifier in its key fields. As a result:

  1. REMOTECLAW_SESSION_KEY env var — ChannelBridge passes formatSessionKeyString(buildSessionKey(message)) to the CLI subprocess. Without a distinguishing field, all cron jobs get the same session key string (e.g., "<delivery-channel>:system:_"), making it impossible for the MCP server or debugging tools to tell which cron job is running.

  2. replyToId is unsetbuildSessionKey() maps message.replyToId to SessionKey.threadId. Since cron messages don't set replyToId, the threadId component is always "_".

Note: The functional session-isolation bug (cross-job session contamination) is already mitigated by createSessionMapAdapter() in run.ts, which bypasses SessionMap's key-based lookup entirely and bridges directly to the per-job cron session store. This issue addresses the observability gap and ensures the message metadata is semantically correct.

Current State

buildCronChannelMessage() (src/cron/isolated-agent/run.ts ~line 101)

function buildCronChannelMessage(params: {
  job: CronJob;
  commandBody: string;
  resolvedDelivery: { channel?: string; to?: string; accountId?: string };
  timestamp: number;
  messageToolHints: string[] | undefined;
}): ChannelMessage {
  return {
    id: params.job.id ?? crypto.randomUUID(),
    text: params.commandBody,
    from: params.resolvedDelivery.accountId ?? "system",
    channelId: params.resolvedDelivery.to ?? "",
    provider: params.resolvedDelivery.channel ?? "cron",
    timestamp: params.timestamp,
    messageToolHints: params.messageToolHints?.length ? params.messageToolHints : undefined,
  };
}

No replyToId is set → buildSessionKey() produces threadId: undefined → session key ends with ":_" for every cron job.

buildSessionKey() (src/middleware/channel-bridge.ts ~line 204)

export function buildSessionKey(message: ChannelMessage): SessionKey {
  return {
    channelId: message.channelId,
    userId: message.from,
    threadId: message.replyToId,  // undefined for cron → "_"
  };
}

formatKey() (src/middleware/session-map.ts ~line 110)

function formatKey(key: SessionKey): string {
  return `${key.channelId}:${key.userId}:${key.threadId ?? "_"}`;
}

Session adapter (already correct)

createSessionMapAdapter() in run.ts correctly bridges per-job session IDs from the cron session store to the SessionMap interface, so the functional collision doesn't happen. setCliSessionId() / getCliSessionId() persist CLI session IDs on the per-job cronSession.sessionEntry.

Required Changes

1. Set replyToId to the cron job ID (~1 line)

In buildCronChannelMessage(), add:

replyToId: `cron:${params.job.id}`,

This flows through buildSessionKey()threadId: "cron:daily-review"REMOTECLAW_SESSION_KEY becomes e.g. "telegram:system:cron:daily-review".

2. Add unit test for distinct session keys (~20 lines)

In src/cron/isolated-agent/run.channel-bridge.test.ts, add a test that:

  • Builds two ChannelMessage objects for different cron jobs via buildCronChannelMessage()
  • Passes each through buildSessionKey()
  • Asserts the resulting SessionKey.threadId values are different
  • Asserts the full formatted keys are different

Acceptance Criteria

  • buildCronChannelMessage() sets replyToId: cron:${params.job.id}``
  • Different cron jobs produce distinct REMOTECLAW_SESSION_KEY values in the CLI subprocess environment
  • Unit test verifies distinct session keys per cron job
  • pnpm build passes
  • Existing cron tests pass (no regressions)

Files to Touch

File Change
src/cron/isolated-agent/run.ts Add replyToId to buildCronChannelMessage() return
src/cron/isolated-agent/run.channel-bridge.test.ts Add session key distinction test

Context

  • ChannelMessage type: src/middleware/types.ts (line ~163) — replyToId?: string | undefined already exists
  • Session adapter: createSessionMapAdapter() in src/cron/isolated-agent/run.ts (~line 75) — handles functional isolation; this issue fixes metadata/observability

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