Skip to content

fix(agents): gate owned-write publish on pre-append fingerprint (#86572)#86584

Closed
ubehera wants to merge 2 commits into
openclaw:mainfrom
ubehera:fix/session-takeover-refresh-before-hook-lock
Closed

fix(agents): gate owned-write publish on pre-append fingerprint (#86572)#86584
ubehera wants to merge 2 commits into
openclaw:mainfrom
ubehera:fix/session-takeover-refresh-before-hook-lock

Conversation

@ubehera

@ubehera ubehera commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Converts the embedded session-takeover fence's post-write handling from a fail-open unconditional fence refresh into a fail-closed, trust-gated owned-write publish.

On current main, after a lane's own message persists, onMessagePersisted → refreshAfterOwnedSessionWrite() unconditionally re-reads the session file and trusts whatever is on disk as the new fence baseline. If an external mutation interleaves between releaseForPrompt (fence = F0) and the lane's own append (F1 → F2), that external mutation is silently laundered into the fence and the takeover detector never trips.

This PR captures the file fingerprint immediately before the lane's append (beforeMessagePersist) and records the post-write state as owned only when that pre-append fingerprint was trusted (matches the active fence, is a benign transcript-only advance, or is a previously-trusted state). Otherwise it refuses to record — so an interleaved external mutation can no longer be laundered, and the fence still trips.

Closes #86572. Complementary to #87159: that PR closes the cross-lane file-ownership race; this PR closes the same-lane publish boundary. The two enforce orthogonal invariants.

Fix shape (vs. #86572's proposal)

#86572 proposed hoisting the withOwnedSessionTranscriptWrites ALS scope to span agent.prompt(). This PR instead gates at the publish boundary (onMessagePersisted): pi's session writes already funnel through the guarded sessionManager.appendMessage seam, so the trust decision belongs in the controller that owns the fence, not in the ALS scope. This keeps the trust decision at the ownership boundary and avoids classifying unrelated listener writes as owned.

Rebase note (force-pushed, head 43baac80c5)

Rebased onto current upstream/main, picking up:

  • #87159 — canonical session-file ownership indexing
  • #87409 — removed installSessionExternalHookWriteLock / installSessionEventWriteLock, moved hook locking into the controller

Production context — corrected

Correction (see @htuyer121's comment): an earlier revision of this PR cited @htuyer121's EmbeddedAttemptSessionTakeoverError corpus as evidence the class "continues firing after #87159." That framing was inaccurate and the reporter has corrected it.

So there is no current production signal for the specific same-lane path this PR hardens. Per the reporter's own recommendation, this PR should be weighed on its source-level proof, not on that corpus. This is defense-in-depth hardening of the publish boundary — not a fix for an actively-firing production class.

Real behavior proof

  • Behavior addressed: Converts the post-owned-write fence update from fail-open (unconditional refresh that can launder an interleaved external mutation into the fence) to fail-closed (record-as-owned only when the pre-append fingerprint was trusted). The behavioral delta versus current main is the mixed-interleave case (Scenario D): main launders the external mutation into the fence; this PR refuses to, so the takeover still trips. Clean same-lane writes (Scenario B) behave identically to main.

  • Real environment tested: Source-level runtime harness on rebased branch 43baac80c5: macOS 26.5, Node 26.0.0, branched off current upstream/main (post-Fix embedded session file ownership race #87159 + fix(agents): move session write lock into owned session runtime #87409); loads the patched EmbeddedAttemptSessionLockController from source via dynamic import and drives four runtime scenarios against real temp session JSONL files via fs.appendFile and controller.withSessionWriteLock.

  • Exact steps or command run after this patch: git fetch origin && git checkout fix/session-takeover-refresh-before-hook-lock (confirm at 43baac80c5), then node --import tsx /Users/umank/code/openclaw-tickets/proof/run-refresh-before-hook-lock-proof.mjs /Users/umank/code/openclaw. Checked-in regression: node scripts/run-vitest.mjs src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts.

  • Evidence after fix: terminal capture from the rebased branch follows. Note: in the harness, Scenario A's "pre-fix" label means the controller is simply not informed of the write (neither main's refreshAfterOwnedSessionWrite nor this PR's publishOwnedPostMessageWrite is called) — it demonstrates the controller mechanics, not a literal main reproduction. The behavioral delta over main is Scenario D.

============================================================
PR #86584 — publish-on-onMessagePersisted proof (Issue #86572)
============================================================
source under test: src/agents/embedded-agent-runner/run/attempt.session-lock.ts
                   publishOwnedPostMessageWrite(beforeWrite)
                   gated on isTrustedSessionFileState(key, beforeWrite)

-------- Scenario A — pre-fix: pi-style write WITHOUT publish (trips takeover) --------
  [   0.13ms] setup      mode=pre-fix
  [   0.15ms] lifecycle  releaseForPrompt() — capture fence F0 + mark trusted
  [   0.47ms] pi-write   pi appendFileSync (F0 → F1) — no publish call
  [   0.60ms] hook       controller.withSessionWriteLock(beforeToolCallBody)
  [   1.62ms] hook       RESULT: TAKEOVER (EmbeddedAttemptSessionTakeoverError: session file changed while embedded prompt lock was released: /var/folders/hw/b1qn975s06qg...)
  [   1.64ms] state      hasSessionTakeover() = true

-------- Scenario B — post-fix: pi-style write WITH publish (hook fires cleanly) --------
  [   0.05ms] lifecycle  releaseForPrompt() — capture fence F0 + mark trusted
  [   0.18ms] pi-write   pi appendFileSync (F0 → F1) with pre-write captured
  [   0.27ms] publish    publishOwnedPostMessageWrite(F0) — F0 is trusted, record F1 as owned
  [   0.34ms] hook       controller.withSessionWriteLock(beforeToolCallBody)
  [   0.46ms] hook       RESULT: hook fired successfully (hookOutcome.fired=true)
  [   0.47ms] state      hasSessionTakeover() = false

-------- Scenario C — negative: external write bypasses publish (still trips) --------
  [   0.12ms] pi-write   EXTERNAL appendFileSync (F0 → F1) — no publish
  [   0.34ms] hook       RESULT: TAKEOVER (EmbeddedAttemptSessionTakeoverError)
  [   0.35ms] state      hasSessionTakeover() = true

-------- Scenario D — mixed interleave: external THEN pi write + publish (still trips) --------
  [   0.12ms] external   EXTERNAL appendFileSync first (F0 → F1)
  [   0.20ms] pi-write   pi appendFileSync second (F1 → F2) — pre-write captured = F1
  [   0.28ms] publish    publishOwnedPostMessageWrite(F1) — F1 NOT trusted, skip (fail closed)
  [   0.50ms] hook       RESULT: TAKEOVER (EmbeddedAttemptSessionTakeoverError)
  [   0.51ms] state      hasSessionTakeover() = true

============================================================
Summary
============================================================
  Scenario A (no fix):       hook fired=false, threw=TAKEOVER
  Scenario B (with fix):     hook fired=true, threw=no
  Scenario C (negative):     hook fired=false, threw=TAKEOVER
  Scenario D (mixed):        hook fired=false, threw=TAKEOVER

RESULT: PASS — all four scenarios behaved as expected.
  • Observed result after fix: Scenarios A/B/C demonstrate the gate mechanics; Scenario D is the behavioral improvement over mainmain's unconditional refresh would record the combined F2 (external + pi) state as the trusted fence and silently absorb the external mutation, whereas the trust gate sees the untrusted pre-append baseline (F1) and refuses to record, so the takeover fence still fires. The checked-in regression covers the publish / skip / mixed-interleave cases directly.

  • What was not tested:

    1. A live runtime reproduction against @htuyer121's corpus on the rebased branch — that corpus is pre-Fix embedded session file ownership race #87159 (see corrected timeline above), so it cannot corroborate the post-Fix embedded session file ownership race #87159 same-lane path.
    2. Known limitation: the trust gate captures the pre-append fingerprint immediately before appendFileSync. A concurrent external writer appending benign-looking transcript JSONL inside that narrow window could still be absorbed. Preventing concurrent writers to the same session file is the responsibility of Fix embedded session file ownership race #87159's file-ownership boundary, not this publish-boundary gate.

Compatibility / Risk

  • publishOwnedPostMessageWrite(beforeWrite) is an additive method on EmbeddedAttemptSessionLockController.
  • beforeMessagePersist is an additive optional opt on the session guard. Backward compatible.
  • The trust gate is fail-closed: any baseline mismatch refuses to record. Cannot launder external mutations.

Related


This PR is AI-assisted. Code authored with Claude.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S triage: mock-only-proof Candidate: PR proof only shows tests, mocks, snapshots, lint, typecheck, or CI. labels May 25, 2026
@clawsweeper

clawsweeper Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge. Reviewed June 24, 2026, 3:07 PM ET / 19:07 UTC.

Summary
The PR adds a pre-append session-file fingerprint callback for guarded message persistence, replaces unconditional owned-write fence refresh with trust-gated publishing, and adds focused session-lock regression tests.

PR surface: Source +126, Tests +60. Total +186 across 5 files.

Reproducibility: yes. source-level reproduction is high confidence. The PR proof exercises the message append interleave, and current main plus related reports show sibling runtime-owned session entry paths sharing the same released-lock fence invariant.

Review metrics: 1 noteworthy metric.

  • Current-main publication seams: 1 PR message callback changed; 3 sibling seam families still relevant. Message, compaction/custom, snapshot, and prompt-stream transcript writes all share the session-file ownership invariant before merge.

Stored data model
Persistent data-model change detected: serialized state: src/agents/session-tool-result-guard-wrapper.ts, serialized state: src/agents/session-tool-result-guard.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #86572
Summary: This PR is a candidate interim fix for the canonical same-lane EmbeddedAttemptSessionTakeoverError tracker, while the broader durable storage/fence removal remains under the SQLite migration issue.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🦞 diamond lobster
Patch quality: 🧂 unranked krab
Result: blocked by patch quality or review findings.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Rebase onto current main and resolve the conflict.
  • Port the trust gate through the current owned session-file publication APIs instead of only the message callback.
  • [P2] Add or update regressions covering message, compaction/custom, snapshot, and prompt-stream transcript entries during prompt-lock release.

Risk before merge

  • [P1] The branch is currently CONFLICTING/DIRTY against main, so the submitted diff is not the real merge result.
  • [P1] Current main has snapshot, compaction/custom, and prompt-stream transcript publication paths sharing the same fence invariant; a message-only rebase can leave non-message owned writes outside the trust gate.
  • [P1] Maintainers still need to choose whether to land another JSONL fence-hardening slice now or defer this class to the active SQLite session/transcript migration.

Maintainer options:

  1. Port Gate Into Current Publication APIs (recommended)
    Rebase and move the trust decision into current owned session-file publication paths so all runtime-owned entries share one fence invariant before merge.
  2. Defer To SQLite Migration
    If maintainers do not want another JSONL fence stopgap, pause or close this PR and keep the remaining work under the active session/transcript SQLite migration tracker.
  3. Accept Message-Only Hardening With Follow-Up
    Maintainers could intentionally land only the message path after rebase, but should explicitly own the remaining non-message false-takeover surface as tracked follow-up risk.

Next step before merge

  • [P2] Human review is needed because the branch conflicts with current main and maintainers must choose the current publication-boundary fix or defer to the SQLite migration path.

Security
Cleared: The diff is confined to TypeScript session-lock runtime and tests and does not add dependency, workflow, secret, package, or external code-execution surfaces.

Review findings

  • [P1] Preserve current owned session publication — src/agents/embedded-agent-runner/run/attempt.ts:1979-1983
Review details

Best possible solution:

Rebase and integrate the trust gate through current owned session-file publication primitives for messages, compaction/custom entries, snapshot publication, and prompt-stream transcript writes, or explicitly defer this JSONL fence hardening to the SQLite migration.

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

Yes, source-level reproduction is high confidence. The PR proof exercises the message append interleave, and current main plus related reports show sibling runtime-owned session entry paths sharing the same released-lock fence invariant.

Is this the best way to solve the issue?

No, not as submitted. The pre-append trust gate is a plausible interim fix, but the branch is stale and must be integrated with current main's broader owned session-file publication APIs or deliberately deferred.

Full review comments:

  • [P1] Preserve current owned session publication — src/agents/embedded-agent-runner/run/attempt.ts:1979-1983
    This branch wires the trust gate only through appendMessage. Current main also publishes owned session-file writes through compaction/custom, snapshot, and prompt-stream transcript paths, so a rebase that keeps this message-only shape can leave runtime-owned non-message entries unowned during the prompt-release window and keep causing false takeovers.
    Confidence: 0.9

Overall correctness: patch is incorrect
Overall confidence: 0.87

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against eac1d3349c29.

Label changes

Label justifications:

  • P1: The PR targets an embedded-agent session-state failure that can abort active turns and drop replies in real agent workflows.
  • merge-risk: 🚨 session-state: The diff changes how prompt-window session-file writes are recorded as owned, and an incomplete rebase can preserve false takeovers or misclassify session mutations.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦞 diamond lobster and patch quality is 🧂 unranked krab.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (terminal): The PR body includes after-fix terminal output from a source-level runtime harness against real temp session JSONL files for the mixed-interleave message path; it does not prove the stale current-main sibling surfaces.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes after-fix terminal output from a source-level runtime harness against real temp session JSONL files for the mixed-interleave message path; it does not prove the stale current-main sibling surfaces.
Evidence reviewed

PR surface:

Source +126, Tests +60. Total +186 across 5 files.

View PR surface stats
Area Files Added Removed Net
Source 4 139 13 +126
Tests 1 78 18 +60
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 5 217 31 +186

What I checked:

Likely related people:

  • joshavant: Authored and merged the complementary embedded session-file ownership race fix that touched the same session-lock and attempt files. (role: adjacent fix owner; confidence: high; commits: 3349fe21bbde, 3d61038bc9cf, fdbd854cba4b; files: src/agents/embedded-agent-runner/run/attempt.session-lock.ts, src/agents/embedded-agent-runner/run/attempt.ts)
  • steipete: Authored the session write-lock move into owned session runtime, touching the same embedded runner lock and agent-session boundaries. (role: recent adjacent contributor; confidence: high; commits: 5f68291f4f54, 50049d27d608, d03d521dab0f; files: src/agents/embedded-agent-runner/run/attempt.session-lock.ts, src/agents/embedded-agent-runner/run/attempt.ts, src/agents/sessions/agent-session.ts)
  • jalehman: Authored and is assigned to the open SQLite session/transcript migration tracker that maintainers cite as the durable path for removing this JSONL fence class. (role: session/transcript migration coordinator; confidence: high; commits: 00a75db4280b, d216f7c876dd, 7a0d36f3d0e1; files: src/config/sessions/transcript-write-context.ts, src/config/sessions/session-accessor.ts)
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.

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.

@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: mock-only-proof Candidate: PR proof only shows tests, mocks, snapshots, lint, typecheck, or CI. labels May 25, 2026
@ubehera

ubehera commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

PR body updated with harness output (run-refresh-before-hook-lock-proof.mjs) showing deterministic before/after on a real temp session JSONL: pre-fix throws EmbeddedAttemptSessionTakeoverError, post-fix hook fires cleanly with hasSessionTakeover()=false.

@clawsweeper

clawsweeper Bot commented May 25, 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:

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels May 25, 2026
@clawsweeper

clawsweeper Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

✨ Hatched: 🌱 uncommon Gilded Proofling

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.

Rarity: 🌱 uncommon.
Trait: polishes edge cases.
Image traits: location diff observatory; accessory release bell; palette pearl, teal, and neon green; mood curious; pose peeking out from the egg shell; shell frosted glass shell; lighting moonlit rim light; background soft code-shaped tiles.
Share on X: post this hatch
Copy: My PR egg hatched a 🌱 uncommon Gilded Proofling in ClawSweeper.

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.

@ubehera

ubehera commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up: the checks-node-agentic-agents failure on this PR is a pre-existing flake on main, not caused by this PR.

The failing test is src/agents/model-catalog-visibility.test.ts:29 > resolveVisibleModelCatalog > limits visible catalog to provider wildcard entries after default discoveryError: Test timed out in 120000ms.

The same test, same line, same timeout fails on main CI run 26412030433 (push fix(security): audit Claude permission overrides under YOLO (#86557)) at 17:18Z — 13 minutes before this PR's run. The most recent successful main run (#86571 at 17:24Z) happens to pass it; the flake is timing-dependent.

This PR touches src/agents/pi-embedded-runner/run/attempt.session-lock.ts (+1 line) and the matching test file — completely separate code path from the model-catalog-visibility test. Local node scripts/run-vitest.mjs src/agents/pi-embedded-runner/run/attempt.session-lock.test.ts exits 0.

Per AGENTS.md "If [a failing check is] unrelated on latest origin/main, say so with scoped proof" — flagging this so it doesn't become a maintainer-side speed bump.

@clawsweeper clawsweeper Bot added P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels May 25, 2026
ubehera added a commit to ubehera/openclaw that referenced this pull request May 25, 2026
…gated)

Replaces the prior refreshBeforeLock approach (91ba92c) per ClawSweeper
review on PR openclaw#86584. The earlier design used a blanket
refreshAfterOwnedSessionWrite() right before each external-hook lock
acquisition; that approach could silently accept genuine external mutations
because it advanced the fence without proving the new state was the lane's
own write.

New approach: route pi's _persist writes through the existing
ownedSessionFileWrites machinery instead of blind fence refreshes.

- Add EmbeddedAttemptSessionLockController.publishOwnedPostMessageWrite()
  (sync). Called from the onMessagePersisted callback right after pi's
  sessionManager.appendMessage -> _persist -> appendFileSync completes.
  Records the post-write fingerprint as an OWNED write in the
  process-shared ownedSessionFileWrites map so subsequent
  assertSessionFileFence calls accept the lane's own writes via the
  owned-write match path.

- Trust-gate: publishOwnedPostMessageWrite only records the new state
  when the prior fenceFingerprint is in trustedSessionFileStates (set at
  releaseForPrompt time). External mutations that bypass the callback are
  never recorded as owned and continue to trip
  EmbeddedAttemptSessionTakeoverError correctly.

- Revert installSessionExternalHookWriteLock's refreshBeforeLock param and
  the shared waitBeforeLock closure. Each installLockableFunction call goes
  back to waitBeforeLock: () => waitForSessionEventQueue(session) directly.

- runEmbeddedAttempt's onMessagePersisted callback now calls
  sessionLockController.publishOwnedPostMessageWrite() instead of
  refreshAfterOwnedSessionWrite().

Tests (5 cases under "embedded attempt session lock lifecycle"):

- "accepts pi-style writes published via publishOwnedPostMessageWrite
  before beforeToolCall fires" — positive case.
- "trips takeover on a same-file external write that bypasses
  publishOwnedPostMessageWrite" — NEGATIVE case the bot explicitly
  required ("add a negative test for an external same-file append before
  a hook lock"). Verifies fail-closed invariant preserved.
- "allows multiple prompt turns with pi-style writes published per turn"
  — multi-turn case, three iterations, no false takeover.
- "does not classify a different session file's writes as owned by this
  controller" — cross-file isolation.
- "does not hang when reacquireAfterPrompt rejects after a pi-style
  direct write" — abort/error path.

78 cases across 2 test files green; pnpm check:changed clean.

Closes openclaw#86572.
@ubehera ubehera changed the title fix(agents): refresh fence fingerprint before external-hook write lock (#86572) fix(agents): publish pi's owned writes via onMessagePersisted (#86572) May 25, 2026
@ubehera

ubehera commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

Redesigned per ClawSweeper review on 91ba92c9ab (force-pushed to d824be0298).

Change in approach:

  • Old (91ba92c9ab): installSessionExternalHookWriteLock got a refreshBeforeLock param that called refreshAfterOwnedSessionWrite() before each hook acquired the lock. That blanket-refreshed the fence even for genuine external mutations, weakening takeover detection.
  • New (d824be0298): A new publishOwnedPostMessageWrite() method is wired into onMessagePersisted (the existing pi _persist callback point). It records the post-write fingerprint as owned in ownedSessionFileWrites, gated on isTrustedSessionFileState(sessionFileFenceKey, fenceFingerprint). External mutations that bypass the callback never get published; the fence still trips on them via assertSessionFileFence's existing owned-write match path.

Addresses bot guidance verbatim:

"Please only advance the fence for writes proven to be this controller's own pi/session-manager persistence, and add a negative test for an external same-file append before a hook lock."

  • Trust gate: The new method only records when the prior fenceFingerprint is in trustedSessionFileStates (set at releaseForPrompt time). Same-file external mutations cannot bypass this — they don't go through onMessagePersisted.
  • Negative test added: trips takeover on a same-file external write that bypasses publishOwnedPostMessageWrite — verifies the fail-closed invariant.

Proof: Three-scenario harness covers pre-fix bug, post-fix success, and the negative case. Verbatim output in the PR body.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented May 25, 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 removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 25, 2026
@clawsweeper clawsweeper Bot added status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels May 25, 2026
@ubehera

ubehera commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

CI status: checks-node-core-runtime-cron-service fails on the unrelated test src/cron/service/timer.regression.test.ts:989records per-job start time and duration for batched due jobs. Assertion: expected 70 to be 50 // Object.is equality.

This is a timing-sensitive flake in the cron-service batched-due-jobs path (uses a mutable let now shared between two synchronous mock job runs). It has no dependency on the files this PR touches (src/agents/pi-embedded-runner/run/attempt.session-lock.ts, attempt.session-lock.test.ts, attempt.ts).

All other checks pass on d824be0298, including checks-node-agentic-agents (which covers the embedded-runner test surface this PR modifies).

@ubehera ubehera changed the title fix(agents): publish pi's owned writes via onMessagePersisted (#86572) fix(agents): gate owned-write publish on pre-append fingerprint (#86572) May 25, 2026
@ubehera

ubehera commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed 584ba0e65b addressing ClawSweeper's correctness finding on d824be0298.

The bot's exact concern, addressed:

"This records the current fingerprint as owned whenever the previous fence fingerprint was trusted. If another lane/process appends to the same session file after releaseForPrompt() and then this lane appends its pi message before onMessagePersisted() runs, current contains both writes; recording that combined fingerprint makes the later hook-lock fence accept the external mutation as owned."

Fix shape:

  • publishOwnedPostMessageWrite() now takes a beforeWrite: SessionFileFingerprint | undefined argument and gates trust on the pre-append fingerprint, not on the current fenceFingerprint.
  • The pre-append fingerprint is captured by a new beforeMessagePersist hook in session-tool-result-guard, invoked before originalAppend(message). The snapshot flows back to the caller through onMessagePersisted's new context arg (typed unknown to keep the guard module-agnostic).
  • runEmbeddedAttempt wires beforeMessagePersist: () => readSessionFileFingerprintSync(params.sessionFile) and forwards the snapshot to publishOwnedPostMessageWrite(beforeWriteSnapshot).

Mixed-interleaving negative test added:

it("trips takeover on a mixed external-then-pi append (publish refuses to launder external mutation)", async () => {
  await controller.releaseForPrompt();                            // F0 trusted
  await fs.appendFile(sessionFile, '...external...', "utf8");     // F0 -> F1
  const beforeWrite = readSessionFileFingerprintSync(sessionFile);// F1
  await fs.appendFile(sessionFile, '...pi-after-external...');    // F1 -> F2
  controller.publishOwnedPostMessageWrite(beforeWrite);           // F1 NOT trusted: skip
  await expect(session.agent.beforeToolCall()).rejects.toBeInstanceOf(
    EmbeddedAttemptSessionTakeoverError,                          // fail closed
  );
});

Proof harness updated to a 4-scenario harness; Scenario D is the mixed-interleave fail-closed verification. All 4 PASS. Verbatim stdout in the PR body.

80 tests across the session-lock surface green (+1 from the new mixed-interleave test). 46 cases across the session-tool-result-guard surfaces also green — no regressions from the new beforeMessagePersist opt.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented May 25, 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:

@ubehera

ubehera commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

Force-pushed 43baac80c5 — rebased onto current upstream/main and added the checked-in regression coverage flagged in the last pass (the "0 new test cases added in the latest diff" metric / maintainer option "Ask For Checked-In Regression Coverage").

Added (src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts): a new describe("publishOwnedPostMessageWrite trust gate (#86572)") block with 3 cases that drive the publish path directly:

  1. skips publish → trips — a same-lane write that never calls publishOwnedPostMessageWrite still trips the fence (fail-closed default; publish is the required opt-in).
  2. publish + trusted baseline → accepts — pre-append fingerprint F0 is trusted, so the lane's own write is recorded as owned and the hook fires cleanly (hasSessionTakeover() === false).
  3. mixed interleave → still trips — an external write advances F0→F1 first, so the captured baseline (F1) was never trusted; publishOwnedPostMessageWrite refuses to record the combined F2 state and the takeover fence still fires. This is the same-lane/mixed-interleave scenario from the proof harness, now checked in.

Harness Scenario C (pure external advance) is intentionally not duplicated — it is already covered by the existing rejects post-prompt writes when another owner advances the session file test.

Verification on the rebased branch (43baac80c5):

  • node scripts/run-vitest.mjs src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts → 45/45 pass.
  • Full touched surface (attempt.session-lock + session-tool-result-guard{,.transcript-events,.tool-result-persist-hook}) → 113/113 pass.
  • oxlint exit 0; oxfmt --check clean.
  • Mutation-checked for teeth: test 2 fails if the owned-write recording is removed; test 3 fails if the fail-closed trust gate is removed — confirming each test guards the fix rather than restating current behavior.

No source behavior changed in this push — the diff vs the prior head is the rebase plus the three test cases.

@clawsweeper

clawsweeper Bot commented Jun 2, 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:

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 2, 2026
@ubehera

ubehera commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Latest-head broad gate — pnpm check:changed green on Linux

Ran the current-head broad changed gate (the latest-head verification flagged in the last review) on 43baac80c5, via crabbox local-container (Linux, ubuntu:26.04, Node 24), scoped to this PR's 5 changed files:

node scripts/crabbox-wrapper.mjs run --provider local-container -- \
  env CI=1 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 \
  OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm check:changed

Result: exit=0, 0 failures (command 3m08s / total 5m36s). Changed-gate scope confirmed = exactly the 5 changed files (lanes=core, coreTests); no unrelated drift. Green steps:

  • typecheck core + typecheck core tests
  • lint core changed files — Found 0 warnings and 0 errors
  • dependency pin guard (464 specs, 0 violations), package patch guard
  • conflict markers, changelog attributions, plugin-sdk / extension wildcard re-export guards, duplicate-coverage, runtime import cycles, media / sidecar / webhook / pairing guards

Test execution (separate from check:changed): focused run-vitest on the touched surface is green — attempt.session-lock.test.ts 45/45 (including the 3 new publishOwnedPostMessageWrite trust-gate cases) plus the three session-tool-result-guard suites; 103 tests across 4 files. All GitHub checks on 43baac80c5 are green as well.

Proof note: this ran on a local-container Linux runner (Node 24, CI-matching), not the brokered runner. A maintainer can re-run the brokered broad gate for the canonical environment if desired.

@htuyer121

Copy link
Copy Markdown

Timeline correction on the production corroboration attributed to me — my 30-event corpus is PRE-#87159 in our install, not "post-#87159 still firing."

Thanks for carrying the production data. One clarification I should make before it weighs on the merge, because the current PR body reads our corpus as the class "continues firing in real installs after #87159 landed" / "post-#87159 code" — that isn't accurate for our install:

So our corpus documents the class while it was active pre-#87159; it isn't evidence that it persists after #87159. The "rate accelerating / spread widening" in my 05-28 comment was the tail of the pre-upgrade period and shouldn't be read as current.

What our data can't do is corroborate that the same-lane path this PR targets is still firing post-#87159 — once the cross-lane boundary is closed, our production window can't isolate a same-lane-only race. So I'd weight this PR on its own source-level proof (the 4-scenario harness — A trips pre-fix, B clean post-fix, C/D fail-closed — independently demonstrates the same-lane race and the trust gate's fail-closed behavior), rather than on our corpus as a post-#87159 signal. The 30 anonymized session .jsonl files (all pre-#87159) remain available if a diff-driven repro is still useful.

@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 2, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 2, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 2, 2026
@ubehera

ubehera commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @htuyer121 — appreciate the careful correction, and you're right. I've updated the PR body accordingly: the 30-event corpus is now described as pre-#87159 (build 2026.5.26), and the "continues firing after #87159" framing is gone. The "Production context" section now leads with your correction — zero events over ~6 days post-#87159, and the point that a same-lane-only race can't be isolated from production once #87159 closes the cross-lane boundary — and explicitly weights the PR on its source-level proof rather than the corpus, per your recommendation.

For clarity on what the PR now claims: defense-in-depth hardening of the same-lane publish boundary (fail-open unconditional fence refresh → fail-closed trust gate), not a fix for an actively-firing production class. The meaningful source-level scenario is the mixed-interleave case (an external mutation interleaved with the lane's own write being laundered into the fence), which the trust gate refuses to record.

The offer of the anonymized .jsonl files is still genuinely useful for a future diff-driven repro — thank you for that.

@ubehera

ubehera commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

One cleanup commit plus a PR-body correction since the last pass on 43baac80c5. New head b06e2b72d0 (fast-forward, not a force-push — the reviewed feature commit 43baac80c5 is untouched).

1. Removed dead refreshAfterOwnedSessionWrite (method + interface entry) — src/agents/embedded-agent-runner/run/attempt.session-lock.ts

This is upstream main's method (introduced in 2bb00f6726 "fix(agents): fence embedded session writes", carried through the #85341 rename). Its only caller was onMessagePersisted, which this PR's core fix already rewires to publishOwnedPostMessageWrite(beforeWriteSnapshot) (attempt.ts). Superseding that single call site left the method with zero production references — rg -uu (including generated/ignored files) confirms no remaining source reference on this branch; the only hits are regenerated build artifacts and git internals. Removing it completes the refactor to one canonical path: the fail-open unconditional refresh is exactly the behavior this PR replaces, so leaving it defined would strand a dead fail-open path beside the new fail-closed gate. Not a plugin-sdk export (the type is internal; its only structural consumer, attempt-abort.ts, uses Pick<…, "releaseHeldLockForAbort">, unaffected).

2. Reworked the two tests that referenced itattempt.session-lock.test.ts

  • Rewrote "keeps post-provider transcript writes owned after prompt stream returns" onto the production method publishOwnedPostMessageWrite(beforeWrite), capturing the pre-append fingerprint exactly as beforeMessagePersist does. This preserves its unique post-reacquireAfterPrompt coverage and is mutation-sensitive: if the publish were a no-op, acquireForCleanup's assertSessionFileFence sees current=F1 ≠ fence=F0, trips, and the test fails. Added a comment marking it as the deliberate post-reacquire path.
  • Deleted "refreshes the prompt fence after an owned session manager append" as redundant — it tested the removed method, and its production-API analog is the existing "accepts a same-lane write published with a trusted pre-append fingerprint" trust-gate case.

3. PR body corrected (no code impact)

Removed the inaccurate "@htuyer121's corpus shows the class continues firing after #87159" framing per their correction (corpus is pre-#87159; zero events post-#87159). Reframed the headline as fail-open → fail-closed publish-boundary hardening, and added a Fix shape vs. #86572 note (publish boundary, not the proposed ALS-scope hoist) plus a Known limitation note (the narrow pre-append window, owned by #87159's file-ownership boundary).

Verification on b06e2b72d0:

  • node scripts/run-vitest.mjs src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts44/44 (was 45; −1 from the redundant-test deletion).
  • oxlint exit 0; oxfmt --check clean.
  • crabbox local-container pnpm check:changed (Ubuntu 26.04, Node 24) → exit=0 (command 3m16s / total 5m40s): typecheck core + core tests clean, lint 0 warnings / 0 errors on the 5 changed files, 0 runtime import cycles, all guards (dependency pins, package patches, media/sidecar/webhook/pairing) pass.

Net −23 LOC (source −7, tests −16). No production behavior change vs the prior head — this commit removes a now-dead path that the fix had already superseded and refreshes its coverage onto the production seam.

@clawsweeper

clawsweeper Bot commented Jun 2, 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:

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 2, 2026
@sparkli1022-lab

sparkli1022-lab commented Jun 5, 2026

Copy link
Copy Markdown

Coverage note for the publish-boundary fix shape.

A runtime-owned custom session entry can advance the same session file during the prompt-lock release window before the next message append.

Observed ordering from a packaged [email protected] Docker gateway run:

  • thinking_level_change
  • custom entry model-snapshot
  • user message
  • subsequent fence check raised EmbeddedAttemptSessionTakeoverError: session file changed while embedded prompt lock was released

If the owned-write publish path is applied only to sessionManager.appendMessage, the later user-message append captures a pre-write fingerprint that already includes the unpublished custom entry. The fail-closed trust gate then refuses to publish the message append, so the fence still trips.

In this scenario, the publish-boundary coverage needs to include runtime-owned custom session entries as well as message entries, or those custom writes need an equivalent ownership/locking path so they cannot advance the session file unrecorded during the prompt-lock release window.

@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. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Jun 15, 2026
@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

Closing due to inactivity.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.

@ubehera

ubehera commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Closing-state update (2026-07-18): this branch should remain closed rather than be rebased.

Its message-only publish gate was incomplete against the later compaction/custom, snapshot, and prompt-stream ownership seams. Current main subsequently covered those boundaries through #85341 and #90775, then replaced active JSON/JSONL session and transcript persistence with SQLite in #98236. The canonical issue #86572 was closed after that storage change.

If EmbeddedAttemptSessionTakeoverError still reproduces on current main or a post-#98236 release, please file a fresh report with current logs. This stale branch is no longer the right fix surface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: M stale Marked as stale due to inactivity 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.

Hoist withOwnedSessionTranscriptWrites ALS scope to span agent.prompt() to fix vanilla-openclaw same-lane fence trip

5 participants