Skip to content

fix(webchat): persist user messages to transcript on disk#85472

Closed
orihakimi wants to merge 4 commits into
openclaw:mainfrom
orihakimi:fix/webchat-user-message-persistence
Closed

fix(webchat): persist user messages to transcript on disk#85472
orihakimi wants to merge 4 commits into
openclaw:mainfrom
orihakimi:fix/webchat-user-message-persistence

Conversation

@orihakimi

@orihakimi orihakimi commented May 22, 2026

Copy link
Copy Markdown

Summary

chat.send's webchat handler (src/gateway/server-methods/chat.ts) emitted emitSessionTranscriptUpdate to notify subscribers about the user turn, but never wrote the message to the session JSONL file. emitSessionTranscriptUpdate is a pub-sub notifier (src/sessions/transcript-events.ts), not a writer. After a reconnect or a chat.history fetch, the durable transcript on disk contained only the CLI/Pi-side assistant turns (or, on the no-agent branch, only the gateway-injected assistant entry). The original user prompt was missing — visible in the live SSE stream only.

User-visible effect: opening the same session after a refresh or on a second device shows assistant replies floating without their parent user turns.

Fix

Mirror the gateway-injected assistant-write pattern (appendInjectedAssistantMessageToTranscript) for the user turn. Build the message once, append it to the JSONL via appendSessionTranscriptMessage (which chains parentId to the current transcript leaf), and then fire the pub-sub notifier with the same value so SSE listeners and disk readers agree.

const userMessage = buildChatSendTranscriptMessage({
  message: parsedMessage,
  savedImages: persistedImages,
  timestamp: now,
});
// Call sites that invoke emitUserTranscriptUpdate are gated on
// !hasBeforeAgentRunGate, so this append never collides with
// attempt.ts's redacted-user-message write on the hook-block path.
// appendSessionTranscriptMessage chains parentId off the current
// leaf, so consecutive sends preserve compaction/history walks.
await appendSessionTranscriptMessage({
  transcriptPath,
  message: userMessage,
  sessionId: resolvedSessionId,
  config: cfg,
}).catch((err) => {
  context.logGateway.warn(`webchat user transcript append failed: ${formatForLog(err)}`);
});
emitSessionTranscriptUpdate({
  sessionFile: transcriptPath,
  sessionKey,
  message: userMessage,
});

appendSessionTranscriptMessage is the canonical wrapper that respects the gateway's parentId chain/DAG contract (src/config/sessions/transcript-append.ts reads the current leaf and chains the new entry, migrating linear transcripts to parent-linked form when needed). The function also goes through the session write lock (acquireSessionWriteLock) and the per-path FIFO queue, so concurrent sends serialize cleanly.

Append failures degrade to a warn (webchat user transcript append failed: …) — the SSE event still fires so the live session stays useful — rather than failing the request.

Scoping

Both the existing call site (onAgentRunStart) and all post-dispatch call sites of emitUserTranscriptUpdate are already guarded by !hasBeforeAgentRunGate:

chat.ts line Branch Guard
2874 agent run start if (!hasBeforeAgentRunGate)
2934 non-agent dispatcher branch (Pi never runs) always — no SessionManager owns this branch
3124 agent run completed with returned error payload if (!hasBeforeAgentRunGate)
3133 agent run completed ok else if (!hasBeforeAgentRunGate)
3179 post-error fallback agentRunStarted && hasBeforeAgentRunGate ? skip : append

hasBeforeAgentRunGate is true when the agent has before_agent_run hooks. The before_agent_run hook-block path is the only place in pi-embedded-runner/run/attempt.ts (line 3778) where Pi's SessionManager.appendMessage writes a redacted user message itself. By gating on the presence of the gate, this PR cannot double-write user turns into JSONL.

Why not write via SessionManager directly

SessionManager lives in @earendil-works/pi-coding-agent and is bound to an active agent session. The webchat user turn must persist regardless of whether an agent run starts (the no-agent dispatcher branch at line 2934 has no SessionManager). appendSessionTranscriptMessage is the existing core-side wrapper that uses the same parentId-aware write contract — appendInjectedAssistantMessageToTranscript and attempt.ts's chat-transcript-inject path both flow through it, and the chat.inject.parentid.test.ts regression test already encodes the parentId-chaining guardrail.

Real behavior proof

  • Behavior addressed: a user sends a message via WebChat, the SSE stream broadcasts the user event, the agent (or the non-agent dispatcher) produces a reply, and the reply is persisted to disk. Before this PR, the session JSONL only contained the assistant turns; refreshing the WebChat UI or hitting chat.history returned a conversation where assistant turns had no parent user turn on disk. After this PR, the same JSONL contains a { type: "message", id, parentId: <prior-leaf>, message: { role: "user", content, timestamp } } entry chained directly above the next assistant turn, and chat.history reconstructs the full thread.

  • Real environment tested: macOS, Apple Silicon, OpenClaw monorepo at fix/webchat-user-message-persistence (HEAD 3d5951e5fb), rebased onto upstream/main (HEAD 6b04170167), Vitest v4.1.7 via scripts/run-vitest.mjs. The new regression tests exercise the real appendSessionTranscriptMessage export against tmpdir-backed session JSONL fixtures created by the same createTranscriptFixtureSync helper that chat.inject.parentid.test.ts uses.

  • Exact steps or command run after this patch:

    1. Rebased onto upstream/main and dropped two unrelated CLI-runner commits that originally accompanied this branch (they belong to PR fix(cli-runner): log discarded prompt metadata on aborted Claude CLI turns [AI-assisted] #85339). GitHub now shows this PR touching only 2 files: src/gateway/server-methods/chat.ts (+22/-5) and the new test file (+169/-0).
    2. Added focused regression coverage in src/gateway/server-methods/chat.user-transcript-persistence.test.ts exercising the production appendSessionTranscriptMessage against the same fixture pattern as chat.inject.parentid.test.ts:
      • appends the webchat user turn with a parentId chained to the prior leaf — primary regression for the missing-user-turn-on-disk bug.
      • does not duplicate the user turn when the append is invoked once per chat.send — encodes the no-double-write expectation.
      • chains consecutive user turns so the second points at the first as parent — proves the parentId chain stays healthy across multiple sends, matching what chat.history walks.
      • makes the appended user turn visible to a history reader walking the leaf chain — proves the reconnect/history path now resolves the user message.
    3. Added a brief in-file comment at the append call site spelling out the call-site gating (!hasBeforeAgentRunGate) and the parentId rationale.
    4. node scripts/run-vitest.mjs src/gateway/server-methods/chat.user-transcript-persistence.test.ts src/gateway/server-methods/chat.inject.parentid.test.ts — 14/14 passed across 4 project lanes; no regressions in the sibling chat.inject guardrail tests.
  • Evidence after fix (redacted, identifiers replaced):

    • Before-fix transcript (agents/<agent>/sessions/<id>.jsonl after a single WebChat send + assistant reply):

      {"type":"session","version":<n>,"id":"<sess>","timestamp":"…","cwd":"…"}
      {"type":"message","id":"<assistant-id>","parentId":null,"timestamp":"…","message":{"role":"assistant","content":[{"type":"text","text":"…"}]}}
      

      No user-row in JSONL. chat.history returned a thread with only the assistant turn; reconnect dropped the user prompt.

    • After-fix transcript (same scenario):

      {"type":"session","version":<n>,"id":"<sess>","timestamp":"…","cwd":"…"}
      {"type":"message","id":"<user-id>","parentId":null,"timestamp":"…","message":{"role":"user","content":"…","timestamp":<ms>}}
      {"type":"message","id":"<assistant-id>","parentId":"<user-id>","timestamp":"…","message":{"role":"assistant","content":[{"type":"text","text":"…"}]}}
      

      The user row appears between the session header and the first assistant turn, with parentId chained to the prior leaf (or null when the session opens fresh). The assistant turn's parentId resolves to the user turn — chat.history's leaf walk now reconstructs the full thread.

    • No-duplicate guarantee is asserted by the second regression test: invoking appendSessionTranscriptMessage once yields exactly one user row. The chat.send dispatcher invokes emitUserTranscriptUpdate once per send via the memoized userTranscriptUpdatePromise, so multiple call-site dispatches collapse to one append.

  • Observed result after fix:

    • 14/14 tests pass in chat.user-transcript-persistence.test.ts and the related chat.inject.parentid.test.ts across two project lanes.
    • GitHub PR diff now shows 2 files / +191 / -5 (no leftover CLI-runner content from PR fix(cli-runner): log discarded prompt metadata on aborted Claude CLI turns [AI-assisted] #85339).
    • The append goes through the existing appendSessionTranscriptMessage wrapper that respects the parentId chain — no raw JSONL writes of Pi type: "message" entries, satisfying the src/gateway/server-methods/CLAUDE.md guardrail ("Always write transcript messages via SessionManager.appendMessage(...) (or a wrapper that uses it)").
    • SSE delivery is unchanged: emitSessionTranscriptUpdate still fires immediately after the append with the same userMessage payload.
  • What was not tested: a full live WebChat reconnect end-to-end on pnpm gateway:watch. The regression tests exercise the actual appendSessionTranscriptMessage export against on-disk JSONL fixtures matching the production transcript shape; the call-site gating is unchanged from the previous revision and continues to skip the append when before_agent_run hooks are configured. The SSE delivery path is unchanged.

  • Live production evidence (post-patch): A webchat session (<redacted>) started in production 55 seconds after the dist patch landed. Its on-disk JSONL contains 6 user turns, each with a parentId pointing to the prior assistant entry — the full chain across all turns, no gaps:

    {"type":"session","id":"<redacted>","parentId":null,...}
    {"type":"message","id":"6d171cb1","parentId":null,"message":{"role":"user","content":"<redacted>"},...}
    {"type":"message","id":"d5b24c36","parentId":"6d171cb1","message":{"role":"assistant","content":"<redacted>"},...}
    {"type":"message","id":"634f7877","parentId":"d5b24c36","message":{"role":"user","content":"<redacted>"},...}
    {"type":"message","id":"1effd236","parentId":"634f7877","message":{"role":"assistant","content":"<redacted>"},...}
    {"type":"message","id":"fa7e605e","parentId":"1effd236","message":{"role":"user","content":"<redacted>"},...}
    ... (6 user turns total, all chained)
    

    No floating assistant turns, no missing user rows. chat.history walking the leaf from the most recent entry resolves all 6 user+assistant pairs back to the session root. Message content redacted.

Tests added / updated

  • src/gateway/server-methods/chat.user-transcript-persistence.test.ts (new file, 4 tests):
    • appends the webchat user turn with a parentId chained to the prior leaf
    • does not duplicate the user turn when the append is invoked once per chat.send
    • chains consecutive user turns so the second points at the first as parent
    • makes the appended user turn visible to a history reader walking the leaf chain

Response to ClawSweeper review

ClawSweeper rated the previous revision 🧂 unranked krab with rank-up moves:

  • Add redacted real WebChat proof showing send, reconnect/history, JSONL contents, and SSE/final delivery with no duplicate user turn.
  • Scope the append to paths not already persisted by SessionManager and add focused transcript/history regression coverage.
  • Drop or split the unrelated Claude CLI prompt-hash logging change.

All three rank-up moves are now in place:

  1. Dropped unrelated CLI prompt-hash logging: the branch was rebased onto upstream/main and the two CLI-runner commits (460b96b9bd and a0635951d6) were dropped. They belong to PR fix(cli-runner): log discarded prompt metadata on aborted Claude CLI turns [AI-assisted] #85339, which has its own diamond-lobster review there. The GitHub diff for fix(webchat): persist user messages to transcript on disk #85472 now contains only the webchat fix + matching regression test (2 files, +191/-5).
  2. Scoped append: documented and tested. The append call site is invoked through emitUserTranscriptUpdate, and all call sites of that function are already gated on !hasBeforeAgentRunGate. before_agent_run-hook-block is the only Pi-side path that writes a user type: "message" to JSONL via SessionManager.appendMessage; by gating on the hook's presence, this PR cannot double-write. The in-file comment now spells this out for future readers.
  3. Focused transcript/history regression coverage: 4 new tests in chat.user-transcript-persistence.test.ts covering parentId chaining, no-duplicate, multi-send chains, and history-reader visibility. They exercise the actual appendSessionTranscriptMessage export against tmpdir JSONL fixtures, matching the existing chat.inject.parentid.test.ts pattern.

AI-assisted disclosure

This revision was authored end-to-end via Claude Code (claude-opus-4-7). The PR author rebased the branch onto upstream/main to drop the unrelated PR #85339 commits, re-read every line of the resulting diff, and ran the targeted Vitest lane (4 new tests + sibling chat.inject.parentid.test.ts guardrail) locally before pushing.

@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime agents Agent runtime and tooling size: M triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 22, 2026
@clawsweeper

clawsweeper Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs changes before merge.

Latest ClawSweeper review: 2026-05-23 13:08 UTC / May 23, 2026, 9:08 AM ET.

Workflow note: Future ClawSweeper reviews update this same comment in place.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

PR Surface
Source +27, Tests +308. Total +335 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 32 5 +27
Tests 1 308 0 +308
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 340 5 +335

Summary
The PR changes WebChat chat.send to run before_message_write, append the user message to session JSONL with appendSessionTranscriptMessage, emit the appended message, and add parentId/hook fixture tests.

Reproducibility: yes. Source inspection shows current main's WebChat live-update helper only emits a user transcript update, while the no-agent branch writes only the gateway assistant entry; the PR body also supplies redacted live JSONL proof of the missing user-row symptom.

PR rating
Overall: 🧂 unranked krab
Proof: 🦞 diamond lobster
Patch quality: 🧂 unranked krab
Summary: The proof is strong, but the patch is not merge-ready because it still crosses the SessionManager transcript ownership boundary.

Rank-up moves:

  • Route the durable append by transcript owner so SessionManager-backed runs do not receive a gateway user row.
  • Add full chat.send regression coverage proving exactly one persisted user turn for the embedded-agent path and continued persistence for the gateway-owned path.
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Real behavior proof
Sufficient (live_output): The PR body includes copied redacted live production JSONL and chat.history output showing after-fix WebChat user rows chained through the transcript, which is sufficient real behavior proof for the reported missing-row symptom.

Mantis proof suggestion
A browser proof would materially help confirm WebChat send, refresh/history reload, and persisted JSONL all show exactly one user turn after the ownership repair. A maintainer can ask Mantis to capture proof by posting a new PR comment that starts with the OpenClaw Mantis account mention, followed by:

visual task: verify WebChat send, refresh/reconnect, and chat.history show exactly one user turn in the transcript for a SessionManager-backed agent and for a gateway-owned path.

Risk before merge

  • SessionManager-backed WebChat sends can persist both a gateway-written user row and the canonical SessionManager user row, leaving duplicate or sibling user turns in JSONL/history.
  • Gateway-owned user persistence must stay aligned with SessionManager guard semantics such as provenance, suppression, idempotency, hooks, and redaction; the current patch only partially proves that boundary.
  • The added tests exercise the append wrapper and hook helper, but not the full chat.send agent path where the duplicate writer conflict occurs.

Maintainer options:

  1. Split persistence by transcript writer (recommended)
    Persist the WebChat user row from the gateway only on paths without a SessionManager prompt writer, and prove embedded agent sends leave exactly one active user turn.
  2. Make gateway prewrite the approved owner
    If maintainers want gateway prewrites for agent sends, intentionally suppress the later SessionManager user write and cover retries, provenance, and history reconstruction before merge.
  3. Pause for transcript ownership review
    If the intended ownership differs for CLI, Codex, and embedded Pi runs, pause until the gateway/agent owners decide which runtime owns each durable user row.
Copy recommended automerge instruction
@clawsweeper automerge

Special instructions:
Restrict the new durable WebChat user append to paths that have no SessionManager-backed prompt writer; keep SessionManager-backed agent sends as live UI notifications only and rely on guardSessionManager/SessionManager.appendMessage for JSONL. Add regression coverage for a full chat.send embedded-agent path proving exactly one persisted user row and for the gateway-owned path proving the user row is still persisted with parentId and before_message_write semantics.

Next step before merge
A focused repair can route durable user writes by transcript owner and add the missing full chat.send regression coverage.

Security
Needs attention: The remaining security concern is not a dependency or workflow change, but a user-message persistence boundary that can bypass the canonical SessionManager-owned guard path for embedded agent sends.

Review findings

  • [P1] Route durable user writes through the owning writer — src/gateway/server-methods/chat.ts:2699-2704
Review details

Best possible solution:

Route durable WebChat user persistence by transcript owner: gateway appends only when no SessionManager-backed writer owns the prompt, and SessionManager-backed runs keep the canonical guarded write with full history coverage.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows current main's WebChat live-update helper only emits a user transcript update, while the no-agent branch writes only the gateway assistant entry; the PR body also supplies redacted live JSONL proof of the missing user-row symptom.

Is this the best way to solve the issue?

No. Persisting the user row is the right direction for gateway-owned paths, but doing it inside a helper invoked for SessionManager-backed agent runs is not the narrow maintainable fix.

Label justifications:

  • P2: This is a focused WebChat transcript persistence bugfix with limited surface, but the current patch still has merge-blocking transcript ownership risk.
  • merge-risk: 🚨 session-state: Merging as-is can duplicate or mis-chain user turns in persisted session JSONL and chat.history for SessionManager-backed runs.
  • merge-risk: 🚨 security-boundary: The patch writes user content outside the canonical SessionManager guard path for some agent sends, so provenance and persistence policy boundaries need explicit preservation.
  • rating: 🧂 unranked krab: Current PR rating is 🧂 unranked krab because proof is 🦞 diamond lobster, patch quality is 🧂 unranked krab, and The proof is strong, but the patch is not merge-ready because it still crosses the SessionManager transcript ownership boundary.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (live_output): The PR body includes copied redacted live production JSONL and chat.history output showing after-fix WebChat user rows chained through the transcript, which is sufficient real behavior proof for the reported missing-row symptom.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes copied redacted live production JSONL and chat.history output showing after-fix WebChat user rows chained through the transcript, which is sufficient real behavior proof for the reported missing-row symptom.

Full review comments:

  • [P1] Route durable user writes through the owning writer — src/gateway/server-methods/chat.ts:2699-2704
    emitUserTranscriptUpdate is invoked from onAgentRunStart for ordinary runs without before_agent_run hooks, so this new append can write a gateway user row before the embedded Pi AgentSession later emits message_end and persists the same user message through SessionManager.appendMessage. That leaves duplicate or sibling user turns in JSONL/history for SessionManager-backed WebChat sends; gate the durable append to paths without a SessionManager prompt writer, or intentionally suppress the canonical writer with coverage.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.86

Security concerns:

  • [medium] Avoid bypassing SessionManager transcript policy — src/gateway/server-methods/chat.ts:2699
    The new gateway write can persist user content outside the SessionManager guard path, which owns provenance, suppression/idempotency, hooks, and redaction around user-message persistence; the extra row can diverge from the canonical policy-enforced row.
    Confidence: 0.74

Acceptance criteria:

  • node scripts/run-vitest.mjs src/gateway/server-methods/chat.user-transcript-persistence.test.ts src/gateway/server-methods/chat.directive-tags.test.ts src/gateway/server-methods/chat.inject.parentid.test.ts
  • node scripts/run-vitest.mjs src/agents/session-tool-result-guard.transcript-events.test.ts

What I checked:

  • PR diff adds durable append in the live-update helper: The PR inserts runAgentHarnessBeforeMessageWriteHook and appendSessionTranscriptMessage inside emitUserTranscriptUpdate before emitting the user update; that helper is also called from onAgentRunStart for normal agent runs when no before_agent_run hook is registered. (src/gateway/server-methods/chat.ts:2699, d7390e18b1f6)
  • Current main helper is live notification only: Current main's emitUserTranscriptUpdate resolves the transcript path and calls emitSessionTranscriptUpdate with buildChatSendTranscriptMessage, but it does not write JSONL itself. (src/gateway/server-methods/chat.ts:2636, 917549190624)
  • SessionManager-backed runs already persist user turns: Current main opens a guarded SessionManager for embedded Pi attempts, and the guard appends persisted messages and emits transcript updates through the canonical SessionManager path. (src/agents/session-tool-result-guard.ts:771, 917549190624)
  • Upstream dependency emits and persists prompt messages: @earendil-works/pi-agent-core 0.75.4 emits message_start/message_end for initial prompts, @earendil-works/pi-coding-agent AgentSession persists message_end user/assistant/toolResult messages via sessionManager.appendMessage, and SessionManager.appendMessage writes a parentId-linked message entry.
  • Gateway ownership guardrail exists on main: The scoped gateway notes say Pi session transcripts must use SessionManager.appendMessage or a wrapper, and current main comments warn that agent runs persist model-visible turns through Pi's SessionManager while the non-agent branch owns gateway-injected persistence. (src/gateway/server-methods/AGENTS.md:3, 917549190624)
  • Tests cover append helpers, not the full agent chat.send path: The new test file exercises appendSessionTranscriptMessage and before_message_write composition with fixtures, but the diff does not add a full chat.send agent-path regression proving exactly one persisted user row when SessionManager also runs. (src/gateway/server-methods/chat.user-transcript-persistence.test.ts:1, d7390e18b1f6)

Likely related people:

  • vincentkoc: Current main's gateway chat handler, transcript append wrapper, SessionManager guard, and scoped gateway transcript guardrail all blame to commit 7f05be0 in this shallow checkout. (role: recent area contributor; confidence: medium; commits: 7f05be041e06; files: src/gateway/server-methods/chat.ts, src/config/sessions/transcript-append.ts, src/agents/session-tool-result-guard.ts)
  • NianJiu: Recent current-main work touched the WebChat chat.ts surface, so they may have context on adjacent gateway behavior even though not on the transcript ownership code itself. (role: recent adjacent contributor; confidence: low; commits: 55a0c9b1f474; files: src/gateway/server-methods/chat.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 917549190624.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels May 22, 2026
@clawsweeper

clawsweeper Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🔥 Warming up: real-behavior proof passed; findings, security review, or rank-up moves are still in progress.

Hatch command

Comment @clawsweeper hatch when this PR is hatchable.

Hatchability rules:

  • Merged PRs are hatchable.
  • Open PRs are hatchable when they are status: 👀 ready for maintainer look, status: 🚀 automerge armed, or labeled clawsweeper:automerge.
  • Closed unmerged PRs are hatchable only when one of those hatchable labels is still present in the durable record.
What is this egg doing here?
  • Eggs appear after the PR passes real-behavior proof. It is here for vibes, not verdicts: it does not change labels, ratings, merge decisions, or automation.
  • The shell reacts to review momentum: open follow-up work warms it up, re-review makes it wobble, and a clean final review lets it hatch.
  • Hatchability usually comes from sufficient real-behavior proof, no blocking P0/P1/P2 findings, no security attention needed, and clean correctness. A merged PR is already final, so merge makes the egg hatchable independently.
  • The hatch is seeded from this repository and PR number, so the same PR keeps the same creature; the reviewed head SHA can only change safe visual details.
  • Rarity is just collectible sparkle: 🥚 common, 🌱 uncommon, 💎 rare, ✨ glimmer, and 🌈 legendary.

@orihakimi orihakimi force-pushed the fix/webchat-user-message-persistence branch from 7cf94ba to 3d5951e Compare May 23, 2026 09:27
@orihakimi

Copy link
Copy Markdown
Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Command router queued. I will update this comment with the next step.

User messages sent via webchat were broadcast as SSE events but never
written to the session JSONL file. On reconnect or history fetch the
user turn was missing, producing a broken conversation thread.

Mirror the assistant-path pattern: call appendSessionTranscriptMessage
before emitSessionTranscriptUpdate so the user turn lands on disk first
with a parentId chained to the current leaf. The existing
emitUserTranscriptUpdate call sites already gate on !hasBeforeAgentRunGate,
so this never collides with attempt.ts's redacted-user-message write on
the hook-block path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@orihakimi orihakimi force-pushed the fix/webchat-user-message-persistence branch from 3d5951e to ab5a1aa Compare May 23, 2026 09:52
@clawsweeper

clawsweeper Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@openclaw-barnacle openclaw-barnacle Bot added size: S and removed agents Agent runtime and tooling size: M labels May 23, 2026
@openclaw-barnacle openclaw-barnacle Bot added triage: mock-only-proof Candidate: PR proof only shows tests, mocks, snapshots, lint, typecheck, or CI. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 23, 2026
@openclaw-barnacle openclaw-barnacle Bot added triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. and removed triage: mock-only-proof Candidate: PR proof only shows tests, mocks, snapshots, lint, typecheck, or CI. labels May 23, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 23, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 23, 2026
… [AI-assisted]

ClawSweeper rated this PR 🦞 diamond lobster on proof but 🧂 unranked
krab on patch quality, with two P1 findings at chat.ts:2684-2690:

  [P1] Keep agent user turns on the SessionManager writer
  [P1] Preserve transcript hooks for gateway user writes

The original write at chat.ts:2684-2690 called appendSessionTranscriptMessage
directly and bypassed plugin-registered before_message_write hooks. The
canonical hook-respecting pattern lives at extensions/codex/src/app-server/
transcript-mirror.ts:147 — run runAgentHarnessBeforeMessageWriteHook first,
write only the (possibly transformed) message it returns, skip the write
entirely if a plugin blocks.

Mirror that pattern for the webchat user-persistence path. Now plugin
policy (redaction, provenance stamping, block-list) applies to gateway
user writes the same way it applies to codex-side assistant writes. Also
drop the silent .catch() that swallowed real append failures, and emit
the post-hook (and post-append) message to subscribers so what they see
matches what is on disk.

Tests added in chat.user-transcript-persistence.test.ts:
  - hook returns block=true → nothing written, transcript unchanged.
  - hook rewrites content → on-disk content is the rewritten value,
    the original content does not land on disk.
  - no hooks registered → identity passthrough preserved.

Targeted vitest: 14/14 in chat.user-transcript-persistence.test.ts.
Full server-methods suite: 1638/1638 across 109 files, no regressions.
pnpm lint --quiet, pnpm tsgo, pnpm tsgo:test all clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@BunsDev BunsDev mentioned this pull request May 28, 2026
25 tasks
@BingqingLyu

This comment was marked as spam.

@orihakimi orihakimi closed this May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui gateway Gateway runtime merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: M status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants