Skip to content

cron: encode sessionKey when deriving jobId to support channel-native conversation IDs #64030

@BrilliantWang

Description

@BrilliantWang

Problem

runtime.channel.routing.buildAgentSessionKey(...) passes the channel-provided id segment through verbatim. For DingTalk, that segment is the platform's native conversationId, which routinely contains /. A real example:

agent:main:dingtalk:group:cid3tmd4xb19xjfk/wogxwy2a==

This sessionKey is valid everywhere else in the runtime (routing, dedup, session locks, the agent session store, logs), but cron rejects it at job creation:

src/cron/session-target.ts:8-10

if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("\0")) {
  throw new Error(INVALID_CRON_SESSION_TARGET_ID_ERROR);
}

The same guard is mirrored in src/cron/run-log.ts:58-67 (assertSafeCronRunLogJobId).

Impact

Cron jobs cannot target any DingTalk group conversation. This is the entire group-chat surface of the DingTalk channel — not a corner case. Other channels whose native IDs are base64-ish are likely to hit the same wall.

Root cause

The guard exists for a real reason: src/cron/run-log.ts:69-78 builds run-log filesystem paths directly from jobId:

const resolvedPath = path.resolve(runsDir, `${safeJobId}.jsonl`);

And jobId is derived from sessionKey. So a / in sessionKey would become a real path-traversal vector. The blocklist is correct defense; the design choice that forced it — using a sessionKey-derived string directly as a filename — is where the mismatch lives.

Notably, openclaw's own agent session store does not have this problem. src/config/sessions/types.ts:315 generates a random UUID:

const sessionId = patch.sessionId ?? existing?.sessionId ?? crypto.randomUUID();

and src/config/sessions/paths.ts:248-251 uses that UUID as the filename, keeping sessionKey as an opaque map key inside sessions.json. Cron is the only subsystem that couples the filesystem name to sessionKey's character set.

Proposed fix (minimal)

Encode sessionKey when deriving jobId, e.g.:

const jobId = `cron:${base64url(sessionKey)}`;
  • Scope: one derivation site in the cron module.
  • assertSafeCronSessionTargetId can drop the / \ check on sessionKey (it no longer touches the filesystem directly). assertSafeCronRunLogJobId stays as a belt-and-suspenders guard against anything outside the base64url alphabet.
  • Existing jobs are unaffected — they keep their old jobIds and continue reading their old run-log files. New jobs use the encoded format. No migration, no double-write, no schema change.
  • base64url is preferred over a hash because it's reversible: operators can still decode a run-log filename back to the originating sessionKey when debugging.

Alternative considered

Normalizing inside buildAgentSessionKey itself — rejected. It would change every existing persisted sessionKey across all channels and all subsystems (dedup, session locks, the agent session store's sessions.json keys), requiring a versioned prefix and migration for a problem that only cron actually has.

Repro

  1. Receive a message in any DingTalk group chat.
  2. Attempt to create a cron job with session:<that sessionKey> as the target.
  3. INVALID_CRON_SESSION_TARGET_ID_ERROR is thrown at job creation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions