Skip to content

feat: OS-released state lock and recovery of interrupted writes (#41) - #54

Merged
lippdev merged 4 commits into
mainfrom
claude/quirky-lovelace-pybgg3
Sep 28, 2026
Merged

lippdev merged 4 commits into
mainfrom
claude/quirky-lovelace-pybgg3

Conversation

@lippdev

@lippdev lippdev commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Resumo

Refs #41. Decisão registrada no ADR 0019 (docs/decisions/0019-state-lock-and-recovery.md).

  • Trava do sistema: as pastas de estado workspace.lock e claude-native.lock usam agora File::try_lock (flock em Unix), mantido enquanto o estado está aberto.
    • O kernel libera a trava quando o processo termina, inclusive com kill -9. Um crash não exige mais remover arquivos à mão.
    • Um processo vivo nunca é desalojado: a abertura concorrente é recusada com o PID de quem segura a trava.
    • O arquivo da trava fica no disco e guarda só o PID. Um symlink no lugar dele é recusado.
  • Escrita interrompida: com a trava obtida, um *.new remanescente vem de uma gravação que parou antes do rename.
    • Ele é preservado como *.new.recovered-<n> e nunca é carregado nem apagado.
    • O último snapshot completo é o que abre.
    • A interface avisa onde o arquivo ficou: um aviso na sessão da demo e uma linha na conversa do Claude oculto.
  • Durabilidade: o temporário é criado com permissão 0600 e passa por sync_all antes do rename. Depois do rename, o diretório também passa por sync_all. Em macOS, o sync_all usa F_FULLFSYNC.
  • O código fica compartilhado em src/workspace/private.rs, usado pelo ClaudeStore e pelo Store da demo.

Correção de acompanhamento (e8a9310): vem da revisão do Pullfrog no #53, já integrado. No editor, ↑/↓ agora preservam a coluna em células de exibição, também com caracteres largos. O teste cobre esse caso.

Validação

  • cargo fmt --all -- --check, cargo clippy --locked --all-targets -- -D warnings, cargo test --locked e cargo build --locked passaram localmente em Linux.
  • Os scripts PTY e o check_bundle.py também passaram.
  • Testes do módulo: processo vivo recusado com PID, trava antiga que não bloqueia, symlink recusado, temporário remanescente que bloqueia a escrita até ser recuperado e permissões 0600.
  • Teste da demo: simula um processo morto entre a escrita do temporário e o rename. O arquivo parcial fica preservado, o estado confirmado abre e a sessão mostra o aviso.
  • Scripts PTY: agora conferem que a trava foi liberada, com flock não bloqueante, em vez de exigir que o arquivo tenha sumido.
  • check_claude_hidden_pty.py: faz kill -9 da Bee, deixa um claude-native.new parcial e reabre com --resume sem limpeza manual.
  • Latência (cargo test --release -- --ignored --nocapture snapshot_latency, Linux, disco virtual):
    • 64 KiB: mediana 0,6 ms, máximo 23,6 ms;
    • 16 MiB: mediana 28,5 ms, máximo 130,6 ms.

Limitações

  • Sistemas de arquivos de rede não são suportados.
  • Uma versão anterior, que apagava a trava, não respeita o flock se for aberta na mesma pasta.
  • A persistência continua síncrona.
  • Houve apenas autorrevisão.

Documentos atualizados: docs/specs/workspace.md, docs/MANUAL_TESTS.md (item 47), ADR 0018 (referência ao ADR 0019), ADR 0019 e docs/EXECUTION.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MUyLMVv9GbqWRByEZ7LgEb

Both workspace state folders now use an advisory lock (File::try_lock)
held while open: the kernel releases it on exit or kill, so a crash needs
no manual cleanup and a live process is never displaced. A leftover
temporary snapshot is kept aside as *.new.recovered-<n>, never loaded or
deleted, and the UI reports it. Snapshots sync the file and the directory.
Decision and measured latency in ADR 0019.

Refs #41

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUyLMVv9GbqWRByEZ7LgEb
@pullfrog

pullfrog Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Pullfrog  | View workflow run | via Pullfrog | Using GPT Luna | 𝕏

Up/Down counted characters while the cursor is placed in display cells,
so wide characters (e.g. CJK) moved the cursor to a different visual
column. Columns are now measured in cells and never land inside a wide
character. Follow-up to the Pullfrog review of the merged PR #53.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUyLMVv9GbqWRByEZ7LgEb
The Ubuntu CI read the snapshot before the Enter was saved and hit an
IndexError instead of waiting; the directory fsync added in this PR
widened that window. The predicate now waits for a first event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUyLMVv9GbqWRByEZ7LgEb
A fork by another thread keeps a copy of a just-closed descriptor until
its exec, so the flock could still look held right after a store closed.
Parallel tests failed 7 of 60 runs with "em uso"; acquisition now retries
for up to 500 ms (0 of 60). A process that really holds the lock is
still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUyLMVv9GbqWRByEZ7LgEb
@lippdev
lippdev merged commit ade5003 into main Sep 28, 2026
2 checks passed
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.

2 participants