Skip to content

fix(session): make transcript replacements crash-safe - #2094

Merged
kevincodex1 merged 6 commits into
Gitlawb:mainfrom
chioarub:fix/atomic-transcript-replace
Aug 7, 2026
Merged

fix(session): make transcript replacements crash-safe#2094
kevincodex1 merged 6 commits into
Gitlawb:mainfrom
chioarub:fix/atomic-transcript-replace

Conversation

@chioarub

@chioarub chioarub commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a focused same-directory atomic replacement helper that writes an exclusive private temp file, flushes it, preserves the requested mode and symlink target semantics, and renames without unlinking the live target
  • use atomic replacement for middle/slow tombstone removal and foreground/subagent transcript hydration while retaining a durable direct truncate for final-line removal
  • serialize complete-file rewrites with queued, direct, synchronous, and speculation-accept appends so no writer can land on an inode that a concurrent replacement will hide
  • preserve unaffected JSONL bytes, malformed lines, ordering, newline conventions, the existing 50 MiB slow-scan guard, and bounded streaming behavior

Impact

  • user-facing impact: interrupted transcript rewrites can no longer leave resume history truncated or partially hydrated; failed subagent hydration leaves that agent's previous transcript intact and reports incomplete hydration
  • developer/maintainer impact: fault injection covers every commit boundary, and the shared helper remains limited to complete transcript replacement paths

Testing

  • bun run build
  • bun run smoke
  • bun run check — build, smoke, and dead-code checks passed; the full suite reported 7,837 passed, 2 skipped, and the same 41 local baseline failures as upstream/main
  • focused tests: bun test --feature=UNATTENDED_RETRY --max-concurrency=1 src/utils/atomicReplace.test.ts src/utils/sessionStorage.atomicReplace.test.ts src/utils/sessionStorage.test.ts src/utils/sessionStorage.liteTag.test.ts src/utils/file.test.ts src/utils/fsOperations.mkdir.test.ts src/commands/resume/resume.test.tsx — 90 passed
  • bun run test:provider — 1,479 passed
  • npm run test:provider-recommendation — 129 passed
  • bun run typecheck
  • bun run typecheck:type-tests
  • bun run security:pr-scan -- --base upstream/main --head HEAD
  • bun run --cwd web typecheck
  • bun run --cwd web build

Review focus

  • start with src/utils/atomicReplace.ts for the pre-rename preservation and post-rename durability boundaries
  • then review the per-file operation queue in src/utils/sessionStorage.ts, especially enqueue-time barriers, single-flight draining, and failure settlement
  • the larger test diff is the deterministic fault, concurrency, byte-preservation, hydration, and resume-compatibility matrix for those paths

Notes

  • reviewed CONTRIBUTING.md and AGENTS.md
  • provider/model path tested: N/A (session storage only)
  • screenshots attached (if UI changed): N/A (no UI changes)
  • follow-up work or known limitations: remote managed settings and the separate assistant-history cache intentionally remain candidates for independent adoption

Summary by CodeRabbit

  • New Features

    • Improved session transcript persistence with safer atomic updates, ordered writes, and file locking.
    • Added support for recording accepted speculation entries.
    • Preserved transcript data, file permissions, and symlink behavior during updates.
    • Improved remote session hydration and independent subagent writes.
  • Bug Fixes

    • Prevented partial or corrupted transcript files during interruptions and write failures.
    • Improved cleanup and error reporting when persistence operations fail.
    • Preserved malformed or incomplete transcript lines where possible.
    • Improved consistency during concurrent transcript updates.

Complete transcript rewrites could truncate live JSONL files before preserved data was durable, risking unrecoverable resume history after an interrupted write. Commit replacements through exclusive sibling temp files and serialize them with all transcript append paths so readers observe either the old file or the complete replacement.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dc2b5d81-2c9f-4785-90fe-66bb326961dc

📥 Commits

Reviewing files that changed from the base of the PR and between 50d652d and 6e35e84.

📒 Files selected for processing (1)
  • src/utils/sessionStorage.atomicReplace.test.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: smoke-and-tests (22)
  • GitHub Check: typecheck
  • GitHub Check: smoke-and-tests (24.11.x)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

TypeScript code in this repository must use strict mode and ESM imports.

**/*.{ts,tsx}: Add or update tests when a TypeScript or TSX change affects behavior.
Run the relevant TypeScript validation checks for changed code, including bun run typecheck and, when applicable, bun run typecheck:type-tests.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Preserve existing repository patterns unless intentionally refactoring them.
Keep changes small, readable, and focused; avoid broad rewrites or unrelated cleanup.
Do not reformat unrelated files, and keep comments useful and concise.
Update documentation when setup, commands, or user-facing behavior changes.
Review AI-generated changes for correctness, style consistency, unnecessary noise, and adherence to project architecture before submitting them.
Provider changes must follow the documented integration patterns in docs/integrations/overview.md and the focused guides under docs/integrations/how-to/.
When changing provider behavior, avoid breaking third-party providers and test the exact provider/model path changed when possible.
Provider pull requests must explicitly identify affected providers, limitations, and follow-up work.
Run the narrowest meaningful validation command for the touched area, and ensure relevant CI checks pass before merging.
Use bun install to install dependencies and the repository's Bun scripts for building, testing, smoke testing, and development.
Dependency changes require a concrete project benefit such as a bug fix, security issue, or approved feature; preference alone is insufficient.
Do not change the project's language, core runtime, or dependency stack, or introduce a new runtime, without prior maintainer agreement.
Keep each pull request focused on one issue or clearly scoped improvement and avoid bundling unrelated fixes, features, or refactors.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts

⚙️ CodeRabbit configuration file

**/*: Apply the OpenClaude maintainer review rubric from AGENTS.md. Review the current diff, not stale discussion context. Separate real blockers from suggestions. Do not request changes for vague style churn. Treat approval as merge-ready from CodeRabbit's side, pending required human review and GitHub Checks. If checks are failing or unavailable, say so clearly instead of implying the PR is fully ready.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts
{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}

⚙️ CodeRabbit configuration file

{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}: Review tests for meaningful coverage of the changed behavior, isolation of global/env/config state, async cleanup, fake timers, provider profile leaks, and Windows-compatible assumptions. Block when risky runtime changes lack focused regression coverage or tests assert implementation details while missing the user-visible behavior.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts
🔇 Additional comments (1)
src/utils/sessionStorage.atomicReplace.test.ts (1)

705-705: LGTM!


📝 Walkthrough

Walkthrough

The change adds atomic file replacement, transcript locking, ordered session-storage rewrites, atomic hydration, and speculation-accept persistence. It also adds tests for failure handling, concurrency, symlink behavior, modes, and transcript ordering.

Changes

Transcript persistence

Layer / File(s) Summary
Atomic replacement utility and validation
src/utils/atomicReplace.ts, src/utils/atomicReplace.test.ts
Adds atomic replacement for direct and streamed data with mode handling, symlink resolution, flushing, aborts, cleanup, directory syncing, and fault injection.
Transcript file locking
src/utils/transcriptFileLock.ts, src/entrypoints/sdk/sessions.ts, src/utils/sessionStorage.ts
Adds asynchronous and synchronous per-target locks. Session appends and synchronous metadata writes use these locks.
Queued transcript rewrites and tombstone removal
src/utils/sessionStorage.ts, src/utils/sessionStorage.atomicReplace.test.ts
Adds typed queues, rewrite barriers, incremental JSONL scanning, byte-range preservation, and atomic rewrites.
Hydration and speculation persistence
src/utils/sessionStorage.ts, src/services/PromptSuggestion/speculation.ts, src/utils/sessionStorage.atomicReplace.test.ts
Hydration writes foreground and subagent transcripts atomically and records failures. Speculation accepts use recordSpeculationAccept.

Estimated code review effort: 5 (Critical) | ~90 minutes

🚥 Pre-merge checks | ✅ 7
✅ Passed checks (7 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, scoped to session transcripts, and accurately describes the crash-safe replacement changes.
Description check ✅ Passed The description includes all required sections and provides clear change scope, impact, testing results, and notes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Risk Surface Disclosed ✅ Passed The PR changes local transcript persistence; existing ingress calls and credentials remain unchanged, and speculation/SDK changes do not alter execution or permission policy. No listed blocker appl...
No Hidden Policy Change ✅ Passed The PR scope is transcript/session storage. It adds no routing, permission, trust-model, or network path changes; diagnostics are hydration-only, no-PII events gated by CLAUDE_CODE_DIAGNOSTICS_FILE.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/PromptSuggestion/speculation.ts (1)

799-803: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Non-blocking: the .catch handler can no longer observe a write failure.

recordSpeculationAccept calls getProject().appendEntry(entry). For speculation-accept, appendEntry runs void this.enqueueWrite(sessionFile, entry) and returns once the entry is queued (src/utils/sessionStorage.ts Line 1800). The promise settles before the file write happens.

The previous implementation appended directly, so this handler reported real write failures. Now a failed write is reported by the drain through logError, and this debug message is unreachable for that case.

Either drop the handler, or change the message so it does not claim a transcript write failed.

♻️ Proposed change
     void recordSpeculationAccept(entry).catch(() => {
       logForDebugging(
-        '[Speculation] Failed to write speculation-accept to transcript',
+        '[Speculation] Failed to queue speculation-accept for the transcript',
       )
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/PromptSuggestion/speculation.ts` around lines 799 - 803, Update
the recordSpeculationAccept call in the speculation-accept flow because its
promise only confirms enqueueing, not completion of the transcript write. Either
remove the unreachable catch handler or revise its log message so it describes
failure to queue/record the speculation acceptance rather than claiming the
transcript write failed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/atomicReplace.test.ts`:
- Around line 100-122: Add focused tests in the atomic replacement test suite
covering both untested options: verify replaceFileAtomic uses the full flush
path when passed flush: 'full', and verify preserveMode: false applies 0o600 to
an existing target instead of retaining its current mode. Keep the cases short
and follow the existing Windows guard and mode assertions.

In `@src/utils/sessionStorage.atomicReplace.test.ts`:
- Around line 494-498: The atomic-replace tests use fragile wall-clock sleeps
for drain completion. Update the waits around removalResult and the
corresponding wait near line 531 to use setTranscriptRewriteHooksForTesting or
an explicit flushSessionStorage await; if retaining timeouts, make them
substantially longer than FLUSH_INTERVAL_MS and document the dependency.
- Around line 322-339: Guard the symlink-and-mode test named “middle tombstone
preserves restrictive mode and follows the live symlink target” by returning
immediately when process.platform is win32, matching the existing guards in
atomicReplace.test.ts. Keep the Unix-specific symlink creation and permission
assertion unchanged for other platforms.
- Around line 244-246: Lower the writeCount threshold assertion in the streaming
test near stat(filePath) so it verifies multiple bounded writes without
depending on the current 64 KiB chunk size; retain the existing size and
suffix-content assertions.

In `@src/utils/sessionStorage.ts`:
- Around line 2227-2246: The foreground hydration paths must reject empty
fetched transcripts before replacing the local file. In
src/utils/sessionStorage.ts lines 2227-2246, update the flow after
getSessionLogs to return false when remoteLogs.length is zero, before
replaceTranscriptFile; apply the same guard to events before
replaceTranscriptFile at lines 2295-2300, matching the existing
subagentEvents.length > 0 behavior.
- Around line 1500-1503: In the tombstone rewrite catch near the best-effort
handling, add a debug log using the existing logForDebugging utility, including
the caught error and enough context to identify the failed rewrite. Preserve the
current swallowing behavior and surrounding atomic replacement logic.
- Around line 1428-1499: Revalidate the transcript size immediately before each
replaceFileAtomic call in the tombstone-removal flow, including the tail-span
and slow-path branches. Compare the fresh stat size with the original fileSize;
if it changed, abort the replacement (or otherwise preserve the newly appended
data) instead of passing stale bounds to streamTranscriptRanges. Use the
existing sessionFile and file-handle/stat mechanisms without changing truncation
behavior.

---

Outside diff comments:
In `@src/services/PromptSuggestion/speculation.ts`:
- Around line 799-803: Update the recordSpeculationAccept call in the
speculation-accept flow because its promise only confirms enqueueing, not
completion of the transcript write. Either remove the unreachable catch handler
or revise its log message so it describes failure to queue/record the
speculation acceptance rather than claiming the transcript write failed.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 537b6387-afd6-453f-b458-94ca7c8e0f41

📥 Commits

Reviewing files that changed from the base of the PR and between 63fda83 and 69d5db5.

📒 Files selected for processing (5)
  • src/services/PromptSuggestion/speculation.ts
  • src/utils/atomicReplace.test.ts
  • src/utils/atomicReplace.ts
  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/utils/sessionStorage.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: smoke-and-tests (22)
  • GitHub Check: smoke-and-tests (24.11.x)
  • GitHub Check: typecheck
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

TypeScript code in this repository must use strict mode and ESM imports.

**/*.{ts,tsx}: Add or update tests when a TypeScript or TSX change affects behavior.
Run the relevant TypeScript validation checks for changed code, including bun run typecheck and, when applicable, bun run typecheck:type-tests.

Files:

  • src/services/PromptSuggestion/speculation.ts
  • src/utils/atomicReplace.test.ts
  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/utils/sessionStorage.ts
  • src/utils/atomicReplace.ts
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Preserve existing repository patterns unless intentionally refactoring them.
Keep changes small, readable, and focused; avoid broad rewrites or unrelated cleanup.
Do not reformat unrelated files, and keep comments useful and concise.
Update documentation when setup, commands, or user-facing behavior changes.
Review AI-generated changes for correctness, style consistency, unnecessary noise, and adherence to project architecture before submitting them.
Provider changes must follow the documented integration patterns in docs/integrations/overview.md and the focused guides under docs/integrations/how-to/.
When changing provider behavior, avoid breaking third-party providers and test the exact provider/model path changed when possible.
Provider pull requests must explicitly identify affected providers, limitations, and follow-up work.
Run the narrowest meaningful validation command for the touched area, and ensure relevant CI checks pass before merging.
Use bun install to install dependencies and the repository's Bun scripts for building, testing, smoke testing, and development.
Dependency changes require a concrete project benefit such as a bug fix, security issue, or approved feature; preference alone is insufficient.
Do not change the project's language, core runtime, or dependency stack, or introduce a new runtime, without prior maintainer agreement.
Keep each pull request focused on one issue or clearly scoped improvement and avoid bundling unrelated fixes, features, or refactors.

Files:

  • src/services/PromptSuggestion/speculation.ts
  • src/utils/atomicReplace.test.ts
  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/utils/sessionStorage.ts
  • src/utils/atomicReplace.ts

⚙️ CodeRabbit configuration file

**/*: Apply the OpenClaude maintainer review rubric from AGENTS.md. Review the current diff, not stale discussion context. Separate real blockers from suggestions. Do not request changes for vague style churn. Treat approval as merge-ready from CodeRabbit's side, pending required human review and GitHub Checks. If checks are failing or unavailable, say so clearly instead of implying the PR is fully ready.

Files:

  • src/services/PromptSuggestion/speculation.ts
  • src/utils/atomicReplace.test.ts
  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/utils/sessionStorage.ts
  • src/utils/atomicReplace.ts
{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}

⚙️ CodeRabbit configuration file

{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}: Review tests for meaningful coverage of the changed behavior, isolation of global/env/config state, async cleanup, fake timers, provider profile leaks, and Windows-compatible assumptions. Block when risky runtime changes lack focused regression coverage or tests assert implementation details while missing the user-visible behavior.

Files:

  • src/utils/atomicReplace.test.ts
  • src/utils/sessionStorage.atomicReplace.test.ts
🔇 Additional comments (15)
src/utils/atomicReplace.ts (3)

78-111: LGTM!


124-169: LGTM!


198-285: LGTM!

src/utils/atomicReplace.test.ts (1)

47-61: LGTM!

Also applies to: 155-240

src/utils/sessionStorage.ts (8)

194-349: LGTM!


375-439: LGTM!


946-1010: LGTM!


1027-1045: LGTM!


1073-1200: LGTM!


1382-1397: LGTM!


2306-2365: LGTM!


1066-1071: 🗄️ Data Integrity & Integration

No change needed: _deferSynchronousAppend does not create lost deferred appends on shutdown.

When reAppendSessionMetadata reaches an active rewrite barrier, it queues a direct append. flush() waits through activeDrain and all writeQueues, so a process that waits on flushSessionStorage after that metadata append cannot exit before the enqueue.

			> Likely an incorrect or invalid review comment.
src/utils/sessionStorage.atomicReplace.test.ts (2)

116-164: LGTM!


166-229: LGTM!

Also applies to: 341-411, 543-618, 659-717

src/services/PromptSuggestion/speculation.ts (1)

3-3: LGTM!

Also applies to: 48-48

Comment thread src/utils/atomicReplace.test.ts
Comment thread src/utils/sessionStorage.atomicReplace.test.ts
Comment thread src/utils/sessionStorage.atomicReplace.test.ts
Comment thread src/utils/sessionStorage.atomicReplace.test.ts Outdated
Comment thread src/utils/sessionStorage.ts
Comment thread src/utils/sessionStorage.ts Outdated
Comment thread src/utils/sessionStorage.ts
Abort tombstone commits when the scanned transcript changes before replacement, and keep existing local history when remote foreground hydration returns no entries. Harden the associated portability, option coverage, queue timing, and diagnostics.
Pass the explicit optional subagent reader in the empty-hydration regression so a fresh TypeScript build sees the complete helper signature.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/atomicReplace.ts`:
- Around line 263-269: The freshness check and destructive mutation are not
coordinated, allowing external writers to append between validation and
replacement or truncation. Introduce a cooperative per-target writer lock or
revision protocol spanning snapshot, copy, freshness validation, and mutation;
update src/utils/atomicReplace.ts lines 263-269 and src/utils/sessionStorage.ts
lines 429-432 so all transcript writers, including handle.truncate(boundary),
hold the same coordination mechanism. Add a deterministic regression test that
appends after freshness validation but before the destructive operation and
verifies the append is preserved.

In `@src/utils/sessionStorage.atomicReplace.test.ts`:
- Line 689: Update the setInternalEventReader call in the test setup to provide
both required reader callbacks, adding an empty subagent reader while preserving
the existing main reader that returns an empty array.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1a9d3e6d-5df4-4b66-9ca8-40760197411f

📥 Commits

Reviewing files that changed from the base of the PR and between 69d5db5 and 47345fc.

📒 Files selected for processing (5)
  • src/services/PromptSuggestion/speculation.ts
  • src/utils/atomicReplace.test.ts
  • src/utils/atomicReplace.ts
  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/utils/sessionStorage.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: smoke-and-tests (24.11.x)
  • GitHub Check: smoke-and-tests (22)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

TypeScript code in this repository must use strict mode and ESM imports.

**/*.{ts,tsx}: Add or update tests when a TypeScript or TSX change affects behavior.
Run the relevant TypeScript validation checks for changed code, including bun run typecheck and, when applicable, bun run typecheck:type-tests.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/services/PromptSuggestion/speculation.ts
  • src/utils/atomicReplace.ts
  • src/utils/atomicReplace.test.ts
  • src/utils/sessionStorage.ts
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Preserve existing repository patterns unless intentionally refactoring them.
Keep changes small, readable, and focused; avoid broad rewrites or unrelated cleanup.
Do not reformat unrelated files, and keep comments useful and concise.
Update documentation when setup, commands, or user-facing behavior changes.
Review AI-generated changes for correctness, style consistency, unnecessary noise, and adherence to project architecture before submitting them.
Provider changes must follow the documented integration patterns in docs/integrations/overview.md and the focused guides under docs/integrations/how-to/.
When changing provider behavior, avoid breaking third-party providers and test the exact provider/model path changed when possible.
Provider pull requests must explicitly identify affected providers, limitations, and follow-up work.
Run the narrowest meaningful validation command for the touched area, and ensure relevant CI checks pass before merging.
Use bun install to install dependencies and the repository's Bun scripts for building, testing, smoke testing, and development.
Dependency changes require a concrete project benefit such as a bug fix, security issue, or approved feature; preference alone is insufficient.
Do not change the project's language, core runtime, or dependency stack, or introduce a new runtime, without prior maintainer agreement.
Keep each pull request focused on one issue or clearly scoped improvement and avoid bundling unrelated fixes, features, or refactors.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/services/PromptSuggestion/speculation.ts
  • src/utils/atomicReplace.ts
  • src/utils/atomicReplace.test.ts
  • src/utils/sessionStorage.ts

⚙️ CodeRabbit configuration file

**/*: Apply the OpenClaude maintainer review rubric from AGENTS.md. Review the current diff, not stale discussion context. Separate real blockers from suggestions. Do not request changes for vague style churn. Treat approval as merge-ready from CodeRabbit's side, pending required human review and GitHub Checks. If checks are failing or unavailable, say so clearly instead of implying the PR is fully ready.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/services/PromptSuggestion/speculation.ts
  • src/utils/atomicReplace.ts
  • src/utils/atomicReplace.test.ts
  • src/utils/sessionStorage.ts
{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}

⚙️ CodeRabbit configuration file

{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}: Review tests for meaningful coverage of the changed behavior, isolation of global/env/config state, async cleanup, fake timers, provider profile leaks, and Windows-compatible assumptions. Block when risky runtime changes lack focused regression coverage or tests assert implementation details while missing the user-visible behavior.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/utils/atomicReplace.test.ts
🔇 Additional comments (2)
src/utils/atomicReplace.test.ts (1)

124-141: LGTM!

src/services/PromptSuggestion/speculation.ts (1)

793-803: LGTM!

Comment on lines +263 to +269
if (
options.expectedTargetSize !== undefined &&
(await stat(targetPath)).size !== options.expectedTargetSize
) {
throw new Error('Atomic replacement target changed before commit')
}
await rename(tempPath, targetPath)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make the freshness check atomic with the file mutation.

The size check does not protect the interval before rename or truncate. If an unqueued writer appends after stat() returns, rename replaces the file without that append, or truncate(boundary) removes it. The session-storage code explicitly supports external SDK writers, so this can lose transcript entries.

  • src/utils/atomicReplace.ts#L263-L269: use a cooperative per-target writer lock or revision protocol that spans snapshot, copy, freshness validation, and rename. Require every transcript writer to participate.
  • src/utils/sessionStorage.ts#L429-L432: hold the same coordination mechanism through the size validation and handle.truncate(boundary).
  • Add a deterministic regression test that injects an append after freshness validation and before the destructive operation.
📍 Affects 2 files
  • src/utils/atomicReplace.ts#L263-L269 (this comment)
  • src/utils/sessionStorage.ts#L429-L432
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/atomicReplace.ts` around lines 263 - 269, The freshness check and
destructive mutation are not coordinated, allowing external writers to append
between validation and replacement or truncation. Introduce a cooperative
per-target writer lock or revision protocol spanning snapshot, copy, freshness
validation, and mutation; update src/utils/atomicReplace.ts lines 263-269 and
src/utils/sessionStorage.ts lines 429-432 so all transcript writers, including
handle.truncate(boundary), hold the same coordination mechanism. Add a
deterministic regression test that appends after freshness validation but before
the destructive operation and verifies the append is preserved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update

The current head already coordinates the freshness check and destructive mutation under the same per-target transcript lock, and the focused concurrency regressions pass.

Addressed

  • Atomic freshness and mutation — Transcript replacement and final-line truncation run under the resolved-target lock; session, synchronous metadata, and SDK append paths acquire or queue behind that same lock. The abort signal is checked at the destructive boundary, and the post-validation external-append regression passes. — 50d652d, 1043e10

Comment thread src/utils/sessionStorage.atomicReplace.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/utils/sessionStorage.atomicReplace.test.ts (1)

689-689: ⚠️ Potential issue | 🔴 Critical

Pass a callable subagent reader.

setInternalEventReader requires subagentReader: InternalEventReader. Passing undefined still violates the strict TypeScript signature and keeps the test build broken.

Proposed fix
-  setInternalEventReader(async () => [], undefined)
+  setInternalEventReader(async () => [], async () => [])

Run bun run typecheck after the change.

As per coding guidelines: TypeScript code in this repository must use strict mode. As per path instructions: run the narrowest relevant checks, including type checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/sessionStorage.atomicReplace.test.ts` at line 689, Update the
setInternalEventReader call in the test to provide a callable
InternalEventReader as its subagentReader argument instead of undefined,
preserving the existing empty-result behavior. Run the narrowest relevant
checks, including bun run typecheck.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/utils/sessionStorage.atomicReplace.test.ts`:
- Line 689: Update the setInternalEventReader call in the test to provide a
callable InternalEventReader as its subagentReader argument instead of
undefined, preserving the existing empty-result behavior. Run the narrowest
relevant checks, including bun run typecheck.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cb261af9-cf98-4aeb-9cee-8331fa0b062a

📥 Commits

Reviewing files that changed from the base of the PR and between 47345fc and 91df767.

📒 Files selected for processing (1)
  • src/utils/sessionStorage.atomicReplace.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: smoke-and-tests (24.11.x)
  • GitHub Check: smoke-and-tests (22)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

TypeScript code in this repository must use strict mode and ESM imports.

**/*.{ts,tsx}: Add or update tests when a TypeScript or TSX change affects behavior.
Run the relevant TypeScript validation checks for changed code, including bun run typecheck and, when applicable, bun run typecheck:type-tests.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Preserve existing repository patterns unless intentionally refactoring them.
Keep changes small, readable, and focused; avoid broad rewrites or unrelated cleanup.
Do not reformat unrelated files, and keep comments useful and concise.
Update documentation when setup, commands, or user-facing behavior changes.
Review AI-generated changes for correctness, style consistency, unnecessary noise, and adherence to project architecture before submitting them.
Provider changes must follow the documented integration patterns in docs/integrations/overview.md and the focused guides under docs/integrations/how-to/.
When changing provider behavior, avoid breaking third-party providers and test the exact provider/model path changed when possible.
Provider pull requests must explicitly identify affected providers, limitations, and follow-up work.
Run the narrowest meaningful validation command for the touched area, and ensure relevant CI checks pass before merging.
Use bun install to install dependencies and the repository's Bun scripts for building, testing, smoke testing, and development.
Dependency changes require a concrete project benefit such as a bug fix, security issue, or approved feature; preference alone is insufficient.
Do not change the project's language, core runtime, or dependency stack, or introduce a new runtime, without prior maintainer agreement.
Keep each pull request focused on one issue or clearly scoped improvement and avoid bundling unrelated fixes, features, or refactors.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts

⚙️ CodeRabbit configuration file

**/*: Apply the OpenClaude maintainer review rubric from AGENTS.md. Review the current diff, not stale discussion context. Separate real blockers from suggestions. Do not request changes for vague style churn. Treat approval as merge-ready from CodeRabbit's side, pending required human review and GitHub Checks. If checks are failing or unavailable, say so clearly instead of implying the PR is fully ready.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts
{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}

⚙️ CodeRabbit configuration file

{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}: Review tests for meaningful coverage of the changed behavior, isolation of global/env/config state, async cleanup, fake timers, provider profile leaks, and Windows-compatible assumptions. Block when risky runtime changes lack focused regression coverage or tests assert implementation details while missing the user-visible behavior.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts

Hold a same-directory cooperative lock across transcript replacement and final-line truncation, and make session plus SDK append paths participate. Exercise the post-validation/pre-rename race deterministically so external appends land after the complete commit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/sessionStorage.atomicReplace.test.ts`:
- Around line 315-328: Make the race in the
setAtomicReplaceFaultInjectorForTesting test deterministic by pausing the rename
stage with a deferred gate. Release that gate only after a lock- or
append-acquisition test hook confirms renameSession is queued behind the held
transcript lock, then assert appendPromise remains pending before
removeTranscriptMessage commits. After the tombstone commit and
flushSessionStorage, release the gate and await appendPromise to verify the
append completes afterward.

In `@src/utils/sessionStorage.ts`:
- Line 3515: Update the synchronous directory creation in appendEntryToFile to
pass recursive: true alongside mode: 0o700, matching the sibling call sites so
repeated writes to an existing session directory succeed. Add a focused
regression test that invokes appendEntryToFile twice for the same session
directory and verifies both entries are written.

In `@src/utils/transcriptFileLock.ts`:
- Around line 137-156: Update withTranscriptFileLockSync and the shared
lock-state handling so a nonzero heldLockCounts entry never by itself authorizes
the reentrancy shortcut; only the current operation’s own explicit reentrancy
context may bypass reacquisition. Ensure async acquisition in
withTranscriptFileLock cannot satisfy the synchronous check, while preserving
legitimate nested synchronous calls, including symlink-resolved targets. Add a
regression test covering an async rewrite on a symlinked transcript that awaits
before issuing synchronous appendEntryToFile, and verify the appended line
survives.
- Around line 80-94: Update asyncLockOptions to provide a safe onCompromised
callback in the proper-lockfile options, preventing a compromised lock during an
in-flight transcript rewrite suspension from throwing and terminating the CLI.
Keep the existing stale, update, and retry settings unchanged.
- Around line 96-116: The synchronous acquireTranscriptLockSync retry path must
not use Atomics.wait on the main thread. Replace the blocking wait with an
awaitable/non-blocking retry mechanism, or change the caller to await an
asynchronous lock-acquisition flow, while preserving the ELOCKED deadline,
backoff, and stale-lock options.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 88afa299-08f6-45cf-b566-70c14c3b2e73

📥 Commits

Reviewing files that changed from the base of the PR and between 91df767 and 50d652d.

📒 Files selected for processing (5)
  • src/entrypoints/sdk/sessions.ts
  • src/utils/atomicReplace.ts
  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/utils/sessionStorage.ts
  • src/utils/transcriptFileLock.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: smoke-and-tests (22)
  • GitHub Check: smoke-and-tests (24.11.x)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

TypeScript code in this repository must use strict mode and ESM imports.

**/*.{ts,tsx}: Add or update tests when a TypeScript or TSX change affects behavior.
Run the relevant TypeScript validation checks for changed code, including bun run typecheck and, when applicable, bun run typecheck:type-tests.

Files:

  • src/entrypoints/sdk/sessions.ts
  • src/utils/atomicReplace.ts
  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/utils/transcriptFileLock.ts
  • src/utils/sessionStorage.ts
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Preserve existing repository patterns unless intentionally refactoring them.
Keep changes small, readable, and focused; avoid broad rewrites or unrelated cleanup.
Do not reformat unrelated files, and keep comments useful and concise.
Update documentation when setup, commands, or user-facing behavior changes.
Review AI-generated changes for correctness, style consistency, unnecessary noise, and adherence to project architecture before submitting them.
Provider changes must follow the documented integration patterns in docs/integrations/overview.md and the focused guides under docs/integrations/how-to/.
When changing provider behavior, avoid breaking third-party providers and test the exact provider/model path changed when possible.
Provider pull requests must explicitly identify affected providers, limitations, and follow-up work.
Run the narrowest meaningful validation command for the touched area, and ensure relevant CI checks pass before merging.
Use bun install to install dependencies and the repository's Bun scripts for building, testing, smoke testing, and development.
Dependency changes require a concrete project benefit such as a bug fix, security issue, or approved feature; preference alone is insufficient.
Do not change the project's language, core runtime, or dependency stack, or introduce a new runtime, without prior maintainer agreement.
Keep each pull request focused on one issue or clearly scoped improvement and avoid bundling unrelated fixes, features, or refactors.

Files:

  • src/entrypoints/sdk/sessions.ts
  • src/utils/atomicReplace.ts
  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/utils/transcriptFileLock.ts
  • src/utils/sessionStorage.ts

⚙️ CodeRabbit configuration file

**/*: Apply the OpenClaude maintainer review rubric from AGENTS.md. Review the current diff, not stale discussion context. Separate real blockers from suggestions. Do not request changes for vague style churn. Treat approval as merge-ready from CodeRabbit's side, pending required human review and GitHub Checks. If checks are failing or unavailable, say so clearly instead of implying the PR is fully ready.

Files:

  • src/entrypoints/sdk/sessions.ts
  • src/utils/atomicReplace.ts
  • src/utils/sessionStorage.atomicReplace.test.ts
  • src/utils/transcriptFileLock.ts
  • src/utils/sessionStorage.ts
src/{components/permissions,utils/permissions,hooks/toolPermission,tools,entrypoints/sdk}/**

⚙️ CodeRabbit configuration file

src/{components/permissions,utils/permissions,hooks/toolPermission,tools,entrypoints/sdk}/**: Review permission prompts, auto-allow logic, sandbox behavior, SDK permission schemas, shell/PowerShell execution, and background execution paths as security-sensitive. Block on bypasses, unclear trust boundaries, unsafe path handling, missing user visibility, or changes that broaden allowed behavior without an explicit maintainer decision.

Files:

  • src/entrypoints/sdk/sessions.ts
{bin/**,scripts/**,package.json,src/setup.ts,src/main.tsx,src/entrypoints/**}

⚙️ CodeRabbit configuration file

{bin/**,scripts/**,package.json,src/setup.ts,src/main.tsx,src/entrypoints/**}: Review install, launcher, build, packaging, startup, and entrypoint changes for cross-platform compatibility, tracked-source rewrites, env/config precedence, and release safety. Block on changes that can break Windows/macOS/Linux startup or publish unexpected artifacts.

Files:

  • src/entrypoints/sdk/sessions.ts
{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}

⚙️ CodeRabbit configuration file

{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}: Review tests for meaningful coverage of the changed behavior, isolation of global/env/config state, async cleanup, fake timers, provider profile leaks, and Windows-compatible assumptions. Block when risky runtime changes lack focused regression coverage or tests assert implementation details while missing the user-visible behavior.

Files:

  • src/utils/sessionStorage.atomicReplace.test.ts
🔇 Additional comments (13)
src/utils/sessionStorage.atomicReplace.test.ts (1)

25-25: LGTM!

Also applies to: 40-40

src/utils/atomicReplace.ts (3)

200-222: LGTM!


224-272: LGTM!


273-293: LGTM!

src/utils/transcriptFileLock.ts (3)

12-68: LGTM!


70-78: LGTM!


158-164: LGTM!

src/entrypoints/sdk/sessions.ts (1)

21-21: LGTM!

Also applies to: 226-229

src/utils/sessionStorage.ts (5)

903-909: LGTM!

Also applies to: 925-927


1020-1029: LGTM!

Also applies to: 1083-1083


1129-1204: LGTM!


1385-1410: LGTM!


1412-1525: LGTM!

Comment thread src/utils/sessionStorage.atomicReplace.test.ts
const line = jsonStringify(entry) + '\n'
try {
if (project?._deferSynchronousAppend(fullPath, line)) return
fs.mkdirSync(dirname(fullPath), { mode: 0o700 })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Blocking: mkdirSync is missing recursive: true and will throw EEXIST on every synchronous metadata append.

Line 3515 calls fs.mkdirSync(dirname(fullPath), { mode: 0o700 }). Without recursive: true, mkdirSync throws EEXIST when the directory already exists — which is the normal case for any session that has written even one entry. Nothing in appendEntryToFile catches it.

The two sibling call sites added in this PR both pass the flag:

  • appendDirectlyToFile at Line 1025: { recursive: true, mode: 0o700 }
  • appendJsonlEntry in src/entrypoints/sdk/sessions.ts at Line 226: { mode: 0o700, recursive: true }

_deferSynchronousAppend returns true only while a rewrite barrier is active, so the ordinary path reaches this line directly. The blast radius is every synchronous metadata write from reAppendSessionMetadata: custom-title, tag, last-prompt, agent-name, agent-color, agent-setting, mode, worktree-state, pr-link, goal-state, and session-branch. Those run during compaction and on session exit, so --resume loses its metadata and the exit path throws.

Add a focused regression test that calls appendEntryToFile twice against an existing session directory and asserts both lines are present.

As per coding guidelines: "Add or update tests when a TypeScript or TSX change affects behavior."

🐛 Proposed fix
   if (project?._deferSynchronousAppend(fullPath, line)) return
-  fs.mkdirSync(dirname(fullPath), { mode: 0o700 })
+  fs.mkdirSync(dirname(fullPath), { mode: 0o700, recursive: true })
   withTranscriptFileLockSync(fullPath, () => {
     fs.appendFileSync(fullPath, line, { mode: 0o600 })
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fs.mkdirSync(dirname(fullPath), { mode: 0o700 })
fs.mkdirSync(dirname(fullPath), { mode: 0o700, recursive: true })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/sessionStorage.ts` at line 3515, Update the synchronous directory
creation in appendEntryToFile to pass recursive: true alongside mode: 0o700,
matching the sibling call sites so repeated writes to an existing session
directory succeed. Add a focused regression test that invokes appendEntryToFile
twice for the same session directory and verifies both entries are written.

Source: Coding guidelines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update

No code change is needed here because this call uses the repository filesystem abstraction rather than Node's raw mkdirSync.

Not changed

  • Recursive synchronous directory creation — FsOperations.mkdirSync intentionally has no recursive option and its Node implementation always calls node:fs with recursive: true. Existing directories are therefore a documented no-op, and the focused filesystem mkdir tests pass; adding recursive at this call site would duplicate the abstraction and fail its type contract.

Comment thread src/utils/transcriptFileLock.ts Outdated
Comment on lines +80 to +94
function asyncLockOptions(targetPath: string) {
return {
lockfilePath: `${targetPath}.lock`,
realpath: false,
stale: TRANSCRIPT_LOCK_STALE_MS,
update: 5_000,
retries: {
retries: 240,
factor: 1.1,
minTimeout: 5,
maxTimeout: 250,
randomize: true,
},
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- local lockfile wrapper ---'
fd -t f 'lockfile.ts' src -x cat -n

echo '--- installed proper-lockfile version ---'
fd -t f 'package.json' -d 1 -x jq '.dependencies["proper-lockfile"], .devDependencies["proper-lockfile"]' {}

echo '--- default onCompromised in the installed package ---'
fd -t d 'proper-lockfile' node_modules -d 3 -x rg -n -C 4 'onCompromised|compromised' {}

echo '--- existing onCompromised usage elsewhere in the repo ---'
rg -n -C 3 'onCompromised' --type=ts

Repository: Gitlawb/openclaude

Length of output: 2962


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- transcriptFileLock.ts ---'
fd -t f 'transcriptFileLock\.ts$' src -x sh -c 'echo "### {}"; wc -l "{}"; cat -n "{}"'

echo '--- lockfile wrapper files ---'
fd -t f 'lockfile.ts$|serverLockInfo\.ts$' src

echo '--- proper-lockfile usages/options ---'
rg -n -C 3 'lock\(|withTranscriptFileLock|asyncLockOptions|TRANSCRIPT_LOCK_STALE_MS|onCompromised' src

echo '--- package lock/version info ---'
if [ -f package.json ]; then jq '{dependencies,devDependencies}' package.json; fi
if [ -f bun.lockb ]; then bun run --bun --no-script-run bun lock --check 2>&1 || true; fi
if [ -f bun.lock ]; then echo "--- bun.lock matches ---"; rg -n '"proper-lockfile"|proper-lockfile' bun.lock | head -40; fi

echo '--- fetch proper-lockfile package source snippets from registry if available ---'
python3 - <<'PY'
import json, urllib.request, re
url='https://registry.npmjs.org/proper-lockfile/4.1.2'
try:
    data=json.load(urllib.request.urlopen(url, timeout=20))
    print('version', data.get('version'))
    print('dist', data.get('dist',{}).get('tarball'))
except Exception as e:
    print('registry fetch failed:', type(e).__name__, str(e))
PY

Repository: Gitlawb/openclaude

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
cd "$tmp"

echo '--- fetch proper-lockfile 4.1.2 package source snippets ---'
npm pack proper-lockfile@4.1.2 --json 2>/dev/null | jq -r '.[0].filename' > pkg.txt || true
tarball="$(cat pkg.txt)"
if [ -n "$tarball" ]; then
  tar -xzf "$tarball"
  echo '--- files ---'
  find package -maxdepth 2 -type f | sed 's#^./##' | sort | head -100
  echo '--- lockfile implementation ---'
  fd -t f 'lockfile|index' package -x sh -c 'echo "### {}"; wc -l "{}"; sed -n "1,220p" "{}"'
  echo '--- compromised/onCompromised occurrences ---'
  rg -n -C 3 'onCompromised|compromised|stale' package/src package/lib package 2>/dev/null || true
fi

echo '--- behavioral probe: inspect package default value/source snippets as text ---'
if [ -n "$tarball" ]; then
  python3 - <<'PY' "$tarball"
import json, os, re, tarfile
tarball = os.sys.argv[1]
text = open('package/src/lockfile.ts').read()
print('has onCompromised rethrow:', bool(re.search(r'onCompromised\s*[=:].*\n?.*\n?.*rethrow|function\s+\w*\s*[^{]*\{\s*\n\s*throw', text)))
for m in re.finditer(r'onCompromised', text):
    start=max(0,m.start()-300); end=min(len(text),m.end()+450)
    line=text[:start].count('\n')+1
    print(f'--- match line {line} ---')
    print(text[start:end])
PY
fi

Repository: Gitlawb/openclaude

Length of output: 29749


Handle lock compromise in asyncLockOptions.

proper-lockfile@4.1.2 defaults onCompromised to throw err, and the stale/5s update timer invokes it after an in-flight transcript rewrite suspension. Add a safe onCompromised callback to avoid killing the CLI when the lock becomes compromised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/transcriptFileLock.ts` around lines 80 - 94, Update
asyncLockOptions to provide a safe onCompromised callback in the proper-lockfile
options, preventing a compromised lock during an in-flight transcript rewrite
suspension from throwing and terminating the CLI. Keep the existing stale,
update, and retry settings unchanged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update

Lock compromise is handled without an uncaught process-level throw on the current head.

Addressed

  • Compromised asynchronous transcript lock — asyncLockOptions supplies onCompromised and aborts the operation's controller. Replacement and final-line truncation check that signal at their destructive boundary, release tolerates ERELEASED only for a compromised lock, and the rename-boundary abort regression passes. — 1043e10

Comment on lines +96 to +116
function acquireTranscriptLockSync(targetPath: string): () => void {
const deadline = Date.now() + TRANSCRIPT_LOCK_WAIT_MS
let retryDelay = 5

while (true) {
try {
return lockfile.lockSync(targetPath, {
lockfilePath: `${targetPath}.lock`,
realpath: false,
stale: TRANSCRIPT_LOCK_STALE_MS,
update: 5_000,
})
} catch (error) {
if (getErrnoCode(error) !== 'ELOCKED' || Date.now() >= deadline) {
throw error
}
Atomics.wait(syncWaitBuffer, 0, 0, retryDelay)
retryDelay = Math.min(retryDelay * 2, 100)
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- lockSync option handling in the installed package ---'
fd -t d 'proper-lockfile' node_modules -d 3 -x rg -n -C 6 'lockSync|options\.update|update' {}

echo '--- local wrapper surface ---'
fd -t f 'lockfile.ts' src -x cat -n

echo '--- other Atomics.wait usage in the repo ---'
rg -n -C 3 'Atomics\.wait' --type=ts

echo '--- declared runtime ---'
fd -t f 'package.json' -d 1 -x jq '{engines: .engines, packageManager: .packageManager}' {}

Repository: Gitlawb/openclaude

Length of output: 314


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- relevant files ---'
fd -t f 'src/utils/transcriptFileLock.ts|src/(sessions|sessionStorage)\.ts|package.json|bun.lockb|bun.lock|AGENTS.md|README.md|docs/integrations/overview.md' . -d 4

echo '--- src/utils/transcriptFileLock.ts outline and contents ---'
if [ -f src/utils/transcriptFileLock.ts ]; then
  ast-grep outline src/utils/transcriptFileLock.ts --view compact || true
  wc -l src/utils/transcriptFileLock.ts
  cat -n src/utils/transcriptFileLock.ts
fi

echo '--- lockfile usages in src ---'
rg -n -C 3 'acquireTranscriptLockSync|releaseTranscriptLockSync|lockSync|lock\(asynchronous|Atomics\.wait|transcriptFileLock' src || true

echo '--- proper-lockfile occurrences in repo files ---'
rg -n -C 2 '"proper-lockfile"|proper-lockfile|lockfile' package.json bun.lock bun.lockb yarn.lock pnpm-lock.yaml package-lock.json 2>/dev/null || true

echo '--- package runtime metadata ---'
if [ -f package.json ]; then jq '{type, engines, packageManager, scripts}' package.json; fi

Repository: Gitlawb/openclaude

Length of output: 20726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/utils/lockfile.ts ---'
cat -n src/utils/lockfile.ts

echo '--- local lockfile exports/usages around lockSync ---'
rg -n -C 5 'function acquire.*LockSync|lockSync\(file|getLockfile|module\.exports|export \* as lockfile|proper-lockfile|LockOptions|UnlockOptions' src/utils/transcriptFileLock.ts src/utils/lockfile.ts package.json bun.lock

echo '--- fetch exact proper-lockfile 4.1.2 source for lockSync/update handling ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cd "$tmpdir"
git init -q
git ls-remote --tags https://github.com/moxystudio/node-proper-lockfile.git 'refs/tags/v4.1.2' | cut -f1

sha="$(git ls-remote --tags https://github.com/moxystudio/node-proper-lockfile.git 'refs/tags/v4.1.2' | cut -f1)"
git fetch -q --depth=1 origin "$sha"
git checkout -q FETCH_HEAD

printf '\n--- lockSync definitions/usages ---\n'
rg -n -C 8 'lockSync|OptionTypes|update.*Sync|options\.update|updateTimeout|lockfile|proper-lockfile' .

printf '\n--- updated-lockfile source excerpts ---\n'
fd -t f -d 4 . | xargs grep -n -C 5 'updateTimeout\|update:|lockSync'

Repository: Gitlawb/openclaude

Length of output: 10375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

echo '--- lockSync/update handling from npm registry tarball for proper-lockfile@4.1.2 ---'
cd "$tmpdir"
tarball="$(npm view proper-lockfile@4.1.2 dist.tarball 2>/dev/null || true)"
printf '%s\n' "$tarball"
if [ -n "$tarball" ]; then
  curl -fsSL "$tarball" -o proper-lockfile.tgz
  tar -xzf proper-lockfile.tgz
  rg -n -C 8 'lockSync|options\.update|updateTimeout|update:|stale' package || true
  echo '--- package.json ---'
  jq '{name, version, main, types}' package/package.json
fi

echo '--- Bun Atomics.wait main-thread availability ---'
node - <<'JS' || true
const ok = typeof globalThis.Atomics?.wait === 'function';
console.log(JSON.stringify({nodeAtomicsWaitAvailable: ok}));
JS

Repository: Gitlawb/openclaude

Length of output: 24616


🌐 Web query:

Bun documentation Atomics wait main thread Atomics.wait unavailable worker threads

💡 Result:

In Bun, as in other JavaScript environments, the use of Atomics.wait is restricted by the threading model of the agent [1][2]. Atomics.wait is a blocking operation designed for use in worker threads, not the main thread [1][3]. Because it puts the calling agent to sleep to wait for notification or a timeout, attempting to call it on the main thread will throw a TypeError, as the main thread cannot be blocked [1][2][3]. For scenarios where you need to perform wait-like operations on the main thread without blocking its event loop, you should use Atomics.waitAsync instead [4][3]. This method is non-blocking, returns a Promise, and is specifically designed to be safe for use in agents where blocking is not permitted, such as the main thread [4][5][6]. Contrary to the suggestion that Atomics.wait is unavailable in worker threads, it is fully supported and intended for use within them [7][3]. Bun developers actively use Atomics.wait within worker threads to implement synchronization primitives like mutexes [7][8][9]. Recent maintenance in Bun has even focused on ensuring that worker termination correctly interrupts threads that are currently parked in an Atomics.wait call to prevent process hangs [8][9].

Citations:


Don’t retry a synchronous exclusive lock from the main thread with Atomics.wait.

lockSync will accept update: 5_000 and still use the lockfile's mtime for staleness; the only blocker is that Atomics.wait on Bun’s main thread throws and keeps acquireTranscriptLockSync from retrying, so another process can appear locked while this one has given up after the first ELOCKED. Use an awaitable/non-blocking retry for the synchronous wrapper, or make the call site wait asynchronously.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/transcriptFileLock.ts` around lines 96 - 116, The synchronous
acquireTranscriptLockSync retry path must not use Atomics.wait on the main
thread. Replace the blocking wait with an awaitable/non-blocking retry
mechanism, or change the caller to await an asynchronous lock-acquisition flow,
while preserving the ELOCKED deadline, backoff, and stale-lock options.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update

No change is needed because the reported runtime premise does not hold for the supported server-side runtimes used by this CLI.

Not changed

  • Bounded synchronous lock retry — A main-thread Atomics.wait probe returned timed-out after the requested bounded wait on both Bun 1.3.14 and Node 25.5.0; neither runtime threw. This wrapper is intentionally synchronous, so converting its retry to an awaitable flow would change the caller contract without fixing a reproduced failure.

Comment on lines +137 to +156
/** Synchronous counterpart for shutdown and metadata append call sites. */
export function withTranscriptFileLockSync<T>(
requestedPath: string,
operation: () => T,
): T {
const targetPath = resolveTranscriptMutationTargetSync(requestedPath)
if ((heldLockCounts.get(targetPath) ?? 0) > 0) return operation()

const release = acquireTranscriptLockSync(targetPath)
incrementHeldLock(targetPath)
try {
return operation()
} finally {
try {
release()
} finally {
decrementHeldLock(targetPath)
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Blocking: the sync reentrancy shortcut can skip the lock while an unrelated async rewrite holds it.

withTranscriptFileLock increments heldLockCounts[targetPath] at Line 125 for the whole duration of the async operation. withTranscriptFileLockSync treats any nonzero count for that path as "already held by me" and runs operation() with no lock at all.

heldLockCounts records that some code in this process holds the lock. It does not record that this caller holds it. Those are different facts. During the await operation() inside runRewriteOperation, the event loop is free, so a synchronous append can enter withTranscriptFileLockSync for the same file and take the unlocked branch while replaceFileAtomic is mid-flight. The append lands on the old inode and rename then discards it — the exact loss this PR is meant to prevent.

The _deferSynchronousAppend barrier in sessionStorage.ts covers most of this, but not all of it: rewriteBarrierFiles is keyed on the unresolved fullPath, while heldLockCounts is keyed on the resolved symlink target. When the transcript is a symlink, the barrier check misses and the reentrancy check hits. The PR explicitly supports symlinked transcripts, so this is a live path, not a hypothetical.

Scope the reentrancy marker to the operation that acquired the lock rather than to the file globally. An AsyncLocalStorage context, or an explicit reentrancy token that only withTranscriptFileLockSync sets for its own nested calls, both work. Do not let async acquisition satisfy the sync check.

Add a regression test: start an async rewrite on a symlinked transcript, and during its await, issue a synchronous appendEntryToFile to the symlink path. Assert the appended line survives.

🐛 Sketch of the ownership fix
-const heldLockCounts = new Map<string, number>()
+// Locks held by *synchronous* frames on this call stack. Async holders must
+// not appear here: an in-flight async rewrite is not the sync caller, and
+// treating it as one skips the lock during the rewrite's await window.
+const syncHeldLockCounts = new Map<string, number>()
   const targetPath = await resolveTranscriptMutationTarget(requestedPath)
   const release = await lockfile.lock(targetPath, asyncLockOptions(targetPath))
-  incrementHeldLock(targetPath)
   try {
     return await operation()
   } finally {
-    try {
-      await release()
-    } finally {
-      decrementHeldLock(targetPath)
-    }
+    await release()
   }
 }
   const targetPath = resolveTranscriptMutationTargetSync(requestedPath)
-  if ((heldLockCounts.get(targetPath) ?? 0) > 0) return operation()
+  if ((syncHeldLockCounts.get(targetPath) ?? 0) > 0) return operation()
 
   const release = acquireTranscriptLockSync(targetPath)
-  incrementHeldLock(targetPath)
+  incrementSyncHeldLock(targetPath)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/transcriptFileLock.ts` around lines 137 - 156, Update
withTranscriptFileLockSync and the shared lock-state handling so a nonzero
heldLockCounts entry never by itself authorizes the reentrancy shortcut; only
the current operation’s own explicit reentrancy context may bypass
reacquisition. Ensure async acquisition in withTranscriptFileLock cannot satisfy
the synchronous check, while preserving legitimate nested synchronous calls,
including symlink-resolved targets. Add a regression test covering an async
rewrite on a symlinked transcript that awaits before issuing synchronous
appendEntryToFile, and verify the appended line survives.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update

The current head prevents an unrelated synchronous append from treating an active asynchronous rewrite as reentrant.

Addressed

  • Synchronous reentrancy during asynchronous rewrite — Asynchronous and synchronous lock ownership now use separate maps. The synchronous append barrier also resolves symlink aliases when checking for an active asynchronous lock, queues the append, and the symlinked-transcript regression verifies the appended line survives. — 1043e10

Separate async and synchronous lock ownership so unrelated sync appends cannot bypass an in-flight replacement. Route aliased in-process appends through the queue, propagate lock compromise through AbortSignal, and cover both symlink-alias and rename-boundary races.
@chioarub

chioarub commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Update

Pushed follow-up commits 47345fc through 1043e10 to preserve existing transcripts on empty foreground hydration, coordinate transcript rewrites with session and SDK append writers, and fix the fresh-checkout typecheck. The latest follow-up scopes lock ownership correctly across symlink aliases and turns lock compromise into a pre-commit abort.

Addressed

  • Fresh-checkout TypeScript failure (CI) — The new CCR regression now passes an empty callable subagent reader as the second argument required by setInternalEventReader. Reproduced from the hosted TS2554 and follow-up TS2345 findings, then verified with both the repository command and a fully completed incremental-cache-disabled TypeScript run. — 6e35e84
  • Atomic replacement option coverage (review) — Added focused cases for flush: 'full' and preserveMode: false, including the expected private 0600 mode on an existing file. — 47345fc
  • Bounded-streaming write-count assertion (review) — Lowered the deliberately loose lower bound and documented that the assertion proves multiple bounded writes without coupling the test to the current 64 KiB chunk size. — 47345fc
  • Windows symlink and mode test (review) — Guarded the POSIX-only symlink and permission case on Windows, matching the atomic-helper tests. — 47345fc
  • Queue tests using wall-clock sleeps (review) — Removed both timer-based waits. The failure case now awaits the already-settled removal directly, and the long-rewrite case relies on synchronous enqueueing before releasing its deterministic rewrite hook. — 47345fc
  • External append during tombstone replacement (review) — A cooperative same-directory per-transcript lock now spans the full tombstone snapshot/copy/check/rename sequence and the final-line validation/truncate sequence. Session queue appends, synchronous metadata appends, and portable SDK title/tag appends all participate. The existing size check remains defense in depth. A deterministic regression starts the real SDK rename path after final validation but before rename, verifies that it waits for the lock, then confirms the complete tombstone commit plus appended metadata and cleanup of both temp and lock paths. — 1043e10
  • Lock compromise callback (review) — Added a non-throwing onCompromised callback that aborts the active mutation through an AbortSignal. The signal now reaches queued rewrites, hydration, append writers, and final-line truncation, and atomic replacement rechecks it immediately before rename. A deterministic rename-boundary regression verifies that compromise preserves the original and cleans the temp file. — 1043e10
  • Async lock mistaken for synchronous reentrancy (review) — Separated async and synchronous ownership maps so only a genuinely nested synchronous frame can bypass reacquisition. Synchronous metadata appends now resolve aliases, detect an async holder, and enter the existing queue instead. The new symlink-target regression failed before the fix and now proves the metadata append survives the replacement. — 1043e10
  • Silent tombstone-removal failures (review) — The best-effort catch now emits a debug diagnostic with the error while retaining the previous non-throwing behavior and without logging transcript content. — 47345fc
  • Empty foreground hydration replacing local history (review) — Both v1 and CCR v2 foreground hydration reject empty entry lists before atomic replacement. The regression covers both paths and verifies that an existing transcript remains byte-for-byte unchanged while hydration reports failure. — 47345fc
  • Speculation-accept failure wording (review) — Updated the fallback message to describe failure to queue the entry instead of claiming that the asynchronous transcript write itself failed. — 47345fc

Not changed

  • Recursive synchronous directory creation (review) — No code change is needed because this call uses the repository FsOperations.mkdirSync abstraction, not Node's raw mkdirSync. Its interface documents recursive creation, its implementation always sets recursive: true, and it already handles EEXIST; the focused filesystem and repeated metadata-write tests pass.
  • Synchronous lock retry with Atomics.wait (review) — Kept the bounded synchronous retry after direct probes under the repository's Bun 1.3.14 runtime and Node both returned timed-out instead of throwing on the main thread. Same-process async holders are now detected and deferred before this path; the blocking retry remains only for an external process, which continues independently while this synchronous API waits.

Validation: the focused atomic replacement, hydration, concurrency, filesystem, and resume-loader matrix passes 96/96; TypeScript, a cache-disabled TypeScript run, and type-test checks pass; build, smoke, dead-code, and security intent checks pass. The full suite reports 7,843 passed, 2 skipped, and the same 41 pre-existing local baseline failures recorded before this follow-up. All review and CI items were addressed or explicitly verified as not requiring a change.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevincodex1
kevincodex1 merged commit d834904 into Gitlawb:main Aug 7, 2026
4 checks passed
@chioarub
chioarub deleted the fix/atomic-transcript-replace branch August 9, 2026 18:58
hotmanxp pushed a commit to hotmanxp/openclaude that referenced this pull request Aug 16, 2026
* fix(session): make transcript replacements crash-safe

Complete transcript rewrites could truncate live JSONL files before preserved data was durable, risking unrecoverable resume history after an interrupted write. Commit replacements through exclusive sibling temp files and serialize them with all transcript append paths so readers observe either the old file or the complete replacement.

* fix(session): preserve concurrent transcript updates

Abort tombstone commits when the scanned transcript changes before replacement, and keep existing local history when remote foreground hydration returns no entries. Harden the associated portability, option coverage, queue timing, and diagnostics.

* test(session): match hydration reader signature

Pass the explicit optional subagent reader in the empty-hydration regression so a fresh TypeScript build sees the complete helper signature.

* fix(session): coordinate transcript writers across processes

Hold a same-directory cooperative lock across transcript replacement and final-line truncation, and make session plus SDK append paths participate. Exercise the post-validation/pre-rename race deterministically so external appends land after the complete commit.

* test(session): provide empty hydration subagent reader

* fix(session): scope transcript lock ownership

Separate async and synchronous lock ownership so unrelated sync appends cannot bypass an in-flight replacement. Route aliased in-process appends through the queue, propagate lock compromise through AbortSignal, and cover both symlink-alias and rename-boundary races.

(cherry picked from commit d834904)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants