Skip to content

LCM assemble can reintroduce OpenAI-invalid call_id|fc_id pairs after transcript sanitization #46

Description

@Grynn

Summary

When OpenClaw is using the lossless-claw context engine with an OpenAI Responses model, assembled LCM context can re-introduce call_id|fc_id / fc_* function-call identifiers after transcript sanitization has already run.

This can trigger provider-side errors like:

Invalid 'input[86].id': 'fc_VlMFjbiUmP_P7YRExGpwuqcZ3Wxmoy6g522VwS00A3TOSbsgVftCkLT0bdYrd'. Expected an ID that contains letters, numbers, underscores, or dashes, but this value contained additional characters.

Why this happens

OpenClaw already knows about this failure mode:

  • src/agents/pi-embedded-helpers/openai.ts
    • downgradeOpenAIFunctionCallReasoningPairs()
    • comment says OpenAI can reject replayed function_call items with fc_* ids when the matching reasoning item is absent

And sanitizeSessionHistory() applies those downgrades before the model call:

  • src/agents/pi-embedded-runner/google.ts

But in the main run path, contextEngine.assemble() happens after sanitizeSessionHistory():

  • src/agents/pi-embedded-runner/run/attempt.ts

If lossless-claw returns assembled messages from storage, those messages are not re-sanitized for OpenAI Responses afterwards.

In lossless-claw, the assembler currently only does pairing repair on return:

  • src/assembler.ts
    • returns sanitizeToolUseResultPairing(rawMessages)

That means OpenAI-invalid replay shapes can be reintroduced by the context engine even if the live session history was already sanitized earlier in the pipeline.

Practical impact

This seems more likely when freshTailCount is large, because more recent raw assistant/tool-call turns are preserved and replayed.

In my case, reducing freshTailCount from 250 to 80 mitigated the issue, but that is just a workaround.

Suggested fix

One of:

  1. Preferred: In OpenClaw core, re-run the OpenAI transcript sanitizers after contextEngine.assemble() and before replacing active session messages.
  2. Or, inside lossless-claw, avoid returning replayed OpenAI function-call ids that depend on matching reasoning state being present in the same assistant turn.

Possible reproduction

  • Use OpenClaw with lossless-claw as context engine
  • Use an OpenAI Responses model
  • Have prior assistant turns containing tool call ids shaped like call_xxx|fc_xxx
  • Let LCM assemble those older turns from storage into the prompt
  • Provider rejects replayed input because the fc_* id is no longer valid in the reconstructed context

Notes

I’m filing this against lossless-claw because the context engine is the component reintroducing the problematic shape, but the durable fix may belong in OpenClaw core after assemble().

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