Gateway: fix post-compaction amnesia for injected messages (web chat)#12283
Conversation
Thread: unknown When: 2026-02-08 20:08 CST Repo: https://github.com/openclaw/openclaw.git Branch: codex/wip/2026-02-09/compact-post-compaction-parentid-fix Problem - Post-compaction turns sometimes lost the compaction summary + kept suffix in the *next* provider request. - Root cause was session graph corruption: gateway appended "stopReason: injected" transcript lines via raw JSONL writes without `parentId`. - Pi's `SessionManager.buildSessionContext()` walks the `parentId` chain from the current leaf; missing `parentId` can sever the active branch and hide the compaction entry. Fix - Use `SessionManager.appendMessage(...)` for injected assistant transcript writes so `parentId` is set to the current leaf. - Route `chat.inject` through the same helper to avoid duplicating the broken raw JSONL append logic. Why This Matters - The compaction algorithm may be correct, but if the leaf chain is broken right after compaction, the provider payload cannot include the summary/suffix "shape" Pi expects. Testing - pnpm test src/agents/pi-embedded-helpers.post-compaction-shape.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.post-context.test.ts - pnpm build Notes - This is provider-shape agnostic: it fixes transcript structure so Anthropic/Gemini/etc all see the same post-compaction context. Resume - If post-compaction looks wrong again, inspect the session transcript for entries missing `parentId` immediately after `type: compaction`.
Additional Comments (1)
In the transcript-append failure fallback, the message you broadcast still uses Prompt To Fix With AIThis is a comment left during a code review.
Path: src/gateway/server-methods/chat.ts
Line: 569:575
Comment:
**Invalid injected stopReason**
In the transcript-append failure fallback, the message you broadcast still uses `stopReason: "injected"` (and the older minimal `usage` shape). This path is reachable whenever `appendAssistantTranscriptMessage(...)` fails, so the PR can still emit a non-enum Pi stopReason despite the stated fix. Consider aligning this fallback with the new injected-message shape (e.g., `stopReason: "stop"`, plus the same `usage` fields you now write in the happy path).
How can I resolve this? If you propose a fix, please make it concise. |
| vi.doMock("../session-utils.js", async (importOriginal) => { | ||
| const original = await importOriginal(); | ||
| return { | ||
| ...original, |
There was a problem hiding this comment.
Mock may not apply
vi.doMock("../session-utils.js", ...) is declared after the file has already imported GatewayRequestContext from ./types.js. If ./types.js (or anything it imports) eagerly imports ../session-utils.js, that module will be loaded before the mock is registered, and the test will end up exercising the real loadSessionEntry instead of the mocked one. To make this deterministic, register mocks before any imports that could pull in session-utils, or switch to vi.mock at the top-level / vi.resetModules() + dynamic imports so ./chat.js and its dependencies load after the mock is installed.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-methods/chat.inject.parentid.test.ts
Line: 28:31
Comment:
**Mock may not apply**
`vi.doMock("../session-utils.js", ...)` is declared *after* the file has already imported `GatewayRequestContext` from `./types.js`. If `./types.js` (or anything it imports) eagerly imports `../session-utils.js`, that module will be loaded before the mock is registered, and the test will end up exercising the real `loadSessionEntry` instead of the mocked one. To make this deterministic, register mocks before any imports that could pull in `session-utils`, or switch to `vi.mock` at the top-level / `vi.resetModules()` + dynamic imports so `./chat.js` and its dependencies load after the mock is installed.
How can I resolve this? If you propose a fix, please make it concise.…12283) * Gateway: preserve Pi transcript parentId for injected messages Thread: unknown When: 2026-02-08 20:08 CST Repo: https://github.com/openclaw/openclaw.git Branch: codex/wip/2026-02-09/compact-post-compaction-parentid-fix Problem - Post-compaction turns sometimes lost the compaction summary + kept suffix in the *next* provider request. - Root cause was session graph corruption: gateway appended "stopReason: injected" transcript lines via raw JSONL writes without `parentId`. - Pi's `SessionManager.buildSessionContext()` walks the `parentId` chain from the current leaf; missing `parentId` can sever the active branch and hide the compaction entry. Fix - Use `SessionManager.appendMessage(...)` for injected assistant transcript writes so `parentId` is set to the current leaf. - Route `chat.inject` through the same helper to avoid duplicating the broken raw JSONL append logic. Why This Matters - The compaction algorithm may be correct, but if the leaf chain is broken right after compaction, the provider payload cannot include the summary/suffix "shape" Pi expects. Testing - pnpm test src/agents/pi-embedded-helpers.post-compaction-shape.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.post-context.test.ts - pnpm build Notes - This is provider-shape agnostic: it fixes transcript structure so Anthropic/Gemini/etc all see the same post-compaction context. Resume - If post-compaction looks wrong again, inspect the session transcript for entries missing `parentId` immediately after `type: compaction`. * Gateway: guardrail test for transcript parentId (chat.inject) * Gateway: guardrail against raw transcript appends (chat.ts) * Gateway: add local AGENTS.md note to preserve Pi transcript parentId chain * Changelog: note gateway post-compaction amnesia fix * Gateway: store injected transcript messages with valid stopReason * Gateway: use valid stopReason in injected fallback
…12283) * Gateway: preserve Pi transcript parentId for injected messages Thread: unknown When: 2026-02-08 20:08 CST Repo: https://github.com/openclaw/openclaw.git Branch: codex/wip/2026-02-09/compact-post-compaction-parentid-fix Problem - Post-compaction turns sometimes lost the compaction summary + kept suffix in the *next* provider request. - Root cause was session graph corruption: gateway appended "stopReason: injected" transcript lines via raw JSONL writes without `parentId`. - Pi's `SessionManager.buildSessionContext()` walks the `parentId` chain from the current leaf; missing `parentId` can sever the active branch and hide the compaction entry. Fix - Use `SessionManager.appendMessage(...)` for injected assistant transcript writes so `parentId` is set to the current leaf. - Route `chat.inject` through the same helper to avoid duplicating the broken raw JSONL append logic. Why This Matters - The compaction algorithm may be correct, but if the leaf chain is broken right after compaction, the provider payload cannot include the summary/suffix "shape" Pi expects. Testing - pnpm test src/agents/pi-embedded-helpers.post-compaction-shape.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.post-context.test.ts - pnpm build Notes - This is provider-shape agnostic: it fixes transcript structure so Anthropic/Gemini/etc all see the same post-compaction context. Resume - If post-compaction looks wrong again, inspect the session transcript for entries missing `parentId` immediately after `type: compaction`. * Gateway: guardrail test for transcript parentId (chat.inject) * Gateway: guardrail against raw transcript appends (chat.ts) * Gateway: add local AGENTS.md note to preserve Pi transcript parentId chain * Changelog: note gateway post-compaction amnesia fix * Gateway: store injected transcript messages with valid stopReason * Gateway: use valid stopReason in injected fallback
Integrated upstream improvements: - CRITICAL: Fix bundled hooks broken since 2026.2.2 (openclaw#9295) - Grok web search provider (xAI) with inline citations - Telegram video note support with tests and docs - QMD model cache sharing optimization (openclaw#12114) - Context overflow false positive fix (openclaw#2078) - Model failover 400 status handling (openclaw#1879) - Dynamic config loading per-message (openclaw#11372) - Gateway post-compaction amnesia fix (openclaw#12283) - Skills watcher: ignore Python venvs and caches - Telegram send recovery from stale thread IDs - Cron job parameter recovery (openclaw#12124) - Auto-reply weekday timestamps (openclaw#12438) - Utility consolidation refactoring (PNG, JSON, errors) - Cross-platform test normalization (openclaw#12212) - macOS Nix defaults support (openclaw#12205) Preserved DEV enhancements: - Docker multi-stage build with enhanced tooling (gh, gog, obsidian-cli, uv, nano-pdf, mcporter, qmd) - Comprehensive .env.example documentation (371 lines) - Multi-environment docker-compose support (DEV/PRD) - GOG/Tailscale integration - Fork-sync and openclaw-docs skills - UI config editor (Svelte) - Fork workflow documentation Merge strategy: Cherry-picked 22 upstream commits, preserved DEV Docker architecture. Docker files unchanged: Dockerfile, docker-compose.yml, docker-setup.sh, .env.example Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
…12283) * Gateway: preserve Pi transcript parentId for injected messages Thread: unknown When: 2026-02-08 20:08 CST Repo: https://github.com/openclaw/openclaw.git Branch: codex/wip/2026-02-09/compact-post-compaction-parentid-fix Problem - Post-compaction turns sometimes lost the compaction summary + kept suffix in the *next* provider request. - Root cause was session graph corruption: gateway appended "stopReason: injected" transcript lines via raw JSONL writes without `parentId`. - Pi's `SessionManager.buildSessionContext()` walks the `parentId` chain from the current leaf; missing `parentId` can sever the active branch and hide the compaction entry. Fix - Use `SessionManager.appendMessage(...)` for injected assistant transcript writes so `parentId` is set to the current leaf. - Route `chat.inject` through the same helper to avoid duplicating the broken raw JSONL append logic. Why This Matters - The compaction algorithm may be correct, but if the leaf chain is broken right after compaction, the provider payload cannot include the summary/suffix "shape" Pi expects. Testing - pnpm test src/agents/pi-embedded-helpers.post-compaction-shape.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.post-context.test.ts - pnpm build Notes - This is provider-shape agnostic: it fixes transcript structure so Anthropic/Gemini/etc all see the same post-compaction context. Resume - If post-compaction looks wrong again, inspect the session transcript for entries missing `parentId` immediately after `type: compaction`. * Gateway: guardrail test for transcript parentId (chat.inject) * Gateway: guardrail against raw transcript appends (chat.ts) * Gateway: add local AGENTS.md note to preserve Pi transcript parentId chain * Changelog: note gateway post-compaction amnesia fix * Gateway: store injected transcript messages with valid stopReason * Gateway: use valid stopReason in injected fallback
…12283) * Gateway: preserve Pi transcript parentId for injected messages Thread: unknown When: 2026-02-08 20:08 CST Repo: https://github.com/openclaw/openclaw.git Branch: codex/wip/2026-02-09/compact-post-compaction-parentid-fix Problem - Post-compaction turns sometimes lost the compaction summary + kept suffix in the *next* provider request. - Root cause was session graph corruption: gateway appended "stopReason: injected" transcript lines via raw JSONL writes without `parentId`. - Pi's `SessionManager.buildSessionContext()` walks the `parentId` chain from the current leaf; missing `parentId` can sever the active branch and hide the compaction entry. Fix - Use `SessionManager.appendMessage(...)` for injected assistant transcript writes so `parentId` is set to the current leaf. - Route `chat.inject` through the same helper to avoid duplicating the broken raw JSONL append logic. Why This Matters - The compaction algorithm may be correct, but if the leaf chain is broken right after compaction, the provider payload cannot include the summary/suffix "shape" Pi expects. Testing - pnpm test src/agents/pi-embedded-helpers.post-compaction-shape.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.post-context.test.ts - pnpm build Notes - This is provider-shape agnostic: it fixes transcript structure so Anthropic/Gemini/etc all see the same post-compaction context. Resume - If post-compaction looks wrong again, inspect the session transcript for entries missing `parentId` immediately after `type: compaction`. * Gateway: guardrail test for transcript parentId (chat.inject) * Gateway: guardrail against raw transcript appends (chat.ts) * Gateway: add local AGENTS.md note to preserve Pi transcript parentId chain * Changelog: note gateway post-compaction amnesia fix * Gateway: store injected transcript messages with valid stopReason * Gateway: use valid stopReason in injected fallback
…12283) * Gateway: preserve Pi transcript parentId for injected messages Thread: unknown When: 2026-02-08 20:08 CST Repo: https://github.com/openclaw/openclaw.git Branch: codex/wip/2026-02-09/compact-post-compaction-parentid-fix Problem - Post-compaction turns sometimes lost the compaction summary + kept suffix in the *next* provider request. - Root cause was session graph corruption: gateway appended "stopReason: injected" transcript lines via raw JSONL writes without `parentId`. - Pi's `SessionManager.buildSessionContext()` walks the `parentId` chain from the current leaf; missing `parentId` can sever the active branch and hide the compaction entry. Fix - Use `SessionManager.appendMessage(...)` for injected assistant transcript writes so `parentId` is set to the current leaf. - Route `chat.inject` through the same helper to avoid duplicating the broken raw JSONL append logic. Why This Matters - The compaction algorithm may be correct, but if the leaf chain is broken right after compaction, the provider payload cannot include the summary/suffix "shape" Pi expects. Testing - pnpm test src/agents/pi-embedded-helpers.post-compaction-shape.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.post-context.test.ts - pnpm build Notes - This is provider-shape agnostic: it fixes transcript structure so Anthropic/Gemini/etc all see the same post-compaction context. Resume - If post-compaction looks wrong again, inspect the session transcript for entries missing `parentId` immediately after `type: compaction`. * Gateway: guardrail test for transcript parentId (chat.inject) * Gateway: guardrail against raw transcript appends (chat.ts) * Gateway: add local AGENTS.md note to preserve Pi transcript parentId chain * Changelog: note gateway post-compaction amnesia fix * Gateway: store injected transcript messages with valid stopReason * Gateway: use valid stopReason in injected fallback
…ick openclaw#12283) Cherry-pick upstream fix for post-compaction amnesia caused by injected messages (like /status) breaking the session parentId chain. - Use SessionManager.appendMessage() instead of raw JSONL appends - This ensures injected messages are attached to current leaf via parentId - Fixes context reset when /status or other commands inject messages Original: 0cf93b8 by @Takhoffman
…12283) * Gateway: preserve Pi transcript parentId for injected messages Thread: unknown When: 2026-02-08 20:08 CST Repo: https://github.com/openclaw/openclaw.git Branch: codex/wip/2026-02-09/compact-post-compaction-parentid-fix Problem - Post-compaction turns sometimes lost the compaction summary + kept suffix in the *next* provider request. - Root cause was session graph corruption: gateway appended "stopReason: injected" transcript lines via raw JSONL writes without `parentId`. - Pi's `SessionManager.buildSessionContext()` walks the `parentId` chain from the current leaf; missing `parentId` can sever the active branch and hide the compaction entry. Fix - Use `SessionManager.appendMessage(...)` for injected assistant transcript writes so `parentId` is set to the current leaf. - Route `chat.inject` through the same helper to avoid duplicating the broken raw JSONL append logic. Why This Matters - The compaction algorithm may be correct, but if the leaf chain is broken right after compaction, the provider payload cannot include the summary/suffix "shape" Pi expects. Testing - pnpm test src/agents/pi-embedded-helpers.post-compaction-shape.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.post-context.test.ts - pnpm build Notes - This is provider-shape agnostic: it fixes transcript structure so Anthropic/Gemini/etc all see the same post-compaction context. Resume - If post-compaction looks wrong again, inspect the session transcript for entries missing `parentId` immediately after `type: compaction`. * Gateway: guardrail test for transcript parentId (chat.inject) * Gateway: guardrail against raw transcript appends (chat.ts) * Gateway: add local AGENTS.md note to preserve Pi transcript parentId chain * Changelog: note gateway post-compaction amnesia fix * Gateway: store injected transcript messages with valid stopReason * Gateway: use valid stopReason in injected fallback
* Gateway: preserve Pi transcript parentId for injected messages Thread: unknown When: 2026-02-08 20:08 CST Repo: https://github.com/openclaw/openclaw.git Branch: codex/wip/2026-02-09/compact-post-compaction-parentid-fix Problem - Post-compaction turns sometimes lost the compaction summary + kept suffix in the *next* provider request. - Root cause was session graph corruption: gateway appended "stopReason: injected" transcript lines via raw JSONL writes without `parentId`. - Pi's `SessionManager.buildSessionContext()` walks the `parentId` chain from the current leaf; missing `parentId` can sever the active branch and hide the compaction entry. Fix - Use `SessionManager.appendMessage(...)` for injected assistant transcript writes so `parentId` is set to the current leaf. - Route `chat.inject` through the same helper to avoid duplicating the broken raw JSONL append logic. Why This Matters - The compaction algorithm may be correct, but if the leaf chain is broken right after compaction, the provider payload cannot include the summary/suffix "shape" Pi expects. Testing - pnpm test src/agents/pi-embedded-helpers.post-compaction-shape.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.post-context.test.ts - pnpm build Notes - This is provider-shape agnostic: it fixes transcript structure so Anthropic/Gemini/etc all see the same post-compaction context. Resume - If post-compaction looks wrong again, inspect the session transcript for entries missing `parentId` immediately after `type: compaction`. * Gateway: guardrail test for transcript parentId (chat.inject) * Gateway: guardrail against raw transcript appends (chat.ts) * Gateway: add local AGENTS.md note to preserve Pi transcript parentId chain * Changelog: note gateway post-compaction amnesia fix * Gateway: store injected transcript messages with valid stopReason * Gateway: use valid stopReason in injected fallback
Cherry-pick upstream fix for post-compaction amnesia caused by injected messages (like /status) breaking the session parentId chain. - Use SessionManager.appendMessage() instead of raw JSONL appends - Ensures injected messages are attached to current leaf via parentId - Fixes context reset when /status or other commands inject messages Original: 0cf93b8 by @Takhoffman
…v2026.2.9 - A1: fix post-compaction amnesia — use SessionManager.appendMessage() instead of raw fs.appendFileSync in chat.ts to preserve parentId chain (openclaw#12283) - A2: recover from context overflow caused by oversized tool results — pre-emptive 400K char cap + session-level truncation fallback (openclaw#11579) - A3: fix bundled hooks broken since tsdown migration — add hook handler entries + correct dist path (openclaw#9295) - A4: prevent false positive context overflow detection — require colon in match (openclaw#2078) - A5: treat HTTP 400 as failover-eligible for model fallback (openclaw#1879)
Problem
Gateway transcript injection was appending raw JSONL messages without a
parentId. Pi builds context by walking theparentIdchain from the leaf, so this could sever the branch and make the post-compaction session look empty (missing summary and kept suffix), across channels (Telegram included).Fix
SessionManager.appendMessage(...)soparentIdis preserved.stopinstead of a non-enum value).Guardrails
chat.injectproduces a transcript entry withparentId.src/gateway/server-methods/chat.tsreintroduces rawfs.appendFileSync(...transcriptPath...)writes.User-facing impact
No more post-compaction amnesia: sessions keep the compacted summary plus the kept recent conversation, so the next user turn continues with the expected context.
Testing
pnpm buildpnpm checkpnpm testManual Test (Web Chat)
/compact.Verified via MITM-captured provider payloads and by inspecting the Pi session
.jsonllogs to confirm the post-compaction context includes the compaction summary plus the kept suffix.Notes
Greptile Overview
Greptile Summary
This PR fixes a Gateway transcript injection regression where injected assistant messages were appended as raw JSONL without Pi’s
parentId, which could sever the transcript DAG and make post-compaction sessions appear empty. The fix routes injected transcript writes through Pi’sSessionManager.appendMessage(...)soparentIdis preserved, and normalizes injectedstopReasonto a valid Pi enum value ("stop").It also adds two Vitest guardrails:
chat.injectresults in a transcript entry that includesparentIdfs.appendFileSync(transcriptPath, ...)raw transcript appends insrc/gateway/server-methods/chat.ts.A changelog entry and a local
AGENTS.mdnote document the issue and the expected invariant for Pi transcripts.Confidence Score: 4/5