Skip to content

fix: session isolation + await cursor + cleanup#1

Closed
marksverdhei wants to merge 13 commits into
mainfrom
tmp
Closed

fix: session isolation + await cursor + cleanup#1
marksverdhei wants to merge 13 commits into
mainfrom
tmp

Conversation

@marksverdhei

@marksverdhei marksverdhei commented Mar 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes Session state not persisted across cf subcommands #2: get_session_id() now uses PPID (parent shell PID) instead of own SID when no TTY is available — session state persists across cf subcommands in non-TTY contexts (agents, subshells, bash -c)
  • Adds LASTLINE cursor tracking so await doesn't return stale/already-seen messages
  • Sanitizes newlines in send to preserve one-message-one-line invariant
  • Improves name uniqueness check to also match [name joined] format
  • Fixes ((attempts++)) crash under set -e on first collision
  • Uses process substitution for await/send-await to prevent orphaned tail -f processes
  • Adds delete-room command, global room support (-g), auto-resolve .Chatfile extension
  • Adds test_bugs.sh regression tests (8 bug scenarios, all pass)
  • Removes scratch files (sketch.py, PROMPT.md)

Test plan

🤖 Generated with Claude Code

marksverdhei and others added 12 commits March 5, 2026 08:28
Add global rooms (-g flag, ~/.chatfiles/), delete-room command,
auto-resolution of .Chatfile extension, and install.sh reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5 confirmed bugs:
1. register crashes on first name collision (set -e + ((attempts++)))
2. await returns join/leave messages instead of waiting for new ones
3. await returns already-seen messages (no read cursor)
4. Newline injection in send breaks one-message-one-line invariant
5. Uniqueness check ignores join/leave format names

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. register crash on name collision: ((attempts++)) fails with set -e
   when attempts=0. Fixed with || true and added exhaustion error exit.
2. await returns join/leave lines: cut -d: misparses non-message format.
   Replaced with line cursor tracking that skips [...] lines.
3. await returns stale messages: no read cursor meant already-seen
   messages returned immediately. Added LASTLINE tracking to session.
4. newline injection in send: embedded newlines broke one-line invariant.
   Strip newlines from messages before appending.
5. uniqueness check blind to join format: grep only matched "name:" but
   not "[name joined]". Extended pattern to match both formats.

Also fixed set -e interaction with LASTLINE default assignment
([ -z "$LASTLINE" ] && LASTLINE=0 fails when LASTLINE is "0").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug reproduction tests should demonstrate the underlying vulnerability,
not test the fix. The test proves grep "^name:" is blind to join format,
which documents why the fix was needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes tail -f orphan leak and await not returning messages:
- Use exec fd < <(tail -f) pattern instead of pipe subshell
- tail PID captured via $! for proper cleanup on exit
- send-await starts tail -f before sending to prevent race condition
- Reverted test_bugs.sh bug5 to original form (tests the concept)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Since state was previously stored in a single '.cf_session' file, multiple
agents running in the same directory (like in a shared tmux session) would
overwrite each other's session file and end up with overlapping identities.

This replaces the hardcoded '.cf_session' with a dynamic TTY-based filename
(e.g., '.cf_session.pts_8') which properly isolates the session state down
to the individual terminal pane or agent process. The implementation cleanly
falls back to Session ID or PID if no TTY is available.

A 'CF_SESSION_FILE' override was also added to 'cf' to ensure the test suite
remains deterministic in its isolated temp directories.
- Fixed Bug 6 (Data Loss in await): When unread messages were processed,
  await was incorrectly setting LASTLINE to TOTAL instead of the line number
  of the specific message it just read, causing it to skip all subsequent
  unread messages on the next call. It now accurately tracks the exact line.
- Expanded the bug reproduction test suite to include the 3 newly discovered
  bugs (await skipping messages, tail -f orphans, state isolation overlap)
  as regression tests to ensure they remain fixed in the future.
- Restored Bug 5 test so it acts as a passing regression test rather than a
  hardcoded failure.
get_session_id() was using the script's own SID, which changes every
invocation when there's no controlling terminal (agents, subshells).
Use PPID instead — stable across all commands from the same parent shell.

Also removes scratch files (sketch.py, PROMPT.md) from the branch.

Closes #2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@marksverdhei marksverdhei changed the title docs: update docs to match current cf features fix: session isolation + await cursor + cleanup Mar 6, 2026
Using wc -l to update the cursor during simultaneous sends caused messages to be permanently skipped. Replaced the unread loop and tail -f with a single tail -n +K -f stream for thread-safe consumption.
@marksverdhei

Copy link
Copy Markdown
Owner Author

Reviewed during hivemind triage. Status:

Tests still green on the PR branch as-is:

  • test_cf.sh → 32/32 pass
  • test_bugs.sh → 0/8 bugs reproduced

But the PR has rotted and now conflicts with main:

Unique-to-#1 work that didn't make it into #3/#4 (from the body):

  • LASTLINE cursor tracking on await
  • newline sanitization in send
  • name-uniqueness also matches [name joined]
  • ((attempts++)) set-e crash fix
  • process substitution for await/send-await
  • delete-room command, -g global flag, auto-resolve .Chatfile

A rebase needs hand-resolving the cf conflict against what #3 landed — couldn't do that confidently as an idle pass since it'd require deciding which side of the overlap wins. Leaving for your call: rebase + reland the unique bits, or close and re-open targeted PRs for the still-wanted pieces.

@marksverdhei

Copy link
Copy Markdown
Owner Author

Closing as superseded — this can no longer merge cleanly and would regress main.

Since this PR was opened (2026-03-05), main reworked the same areas via a different (and cleaner) path:

As a result this PR's diff vs current main would delete .github/workflows/tests.yml (−21) and tests/test_presentation_helpers.py (−173) and revert test_cf.sh (−106) — i.e. it would remove CI and tests main now relies on. So it's not mergeable as-is.

Salvaged the one unambiguous, still-applicable fix → #7: the ((attempts++)) retry-loop crash under set -e (register dies on the first name collision). Verified + regression-tested there.

Still-valid follow-up candidates from this PR, for whoever picks them up (each is independent and small):

  • send / send-await: sanitize embedded newlines to preserve the one-message-one-line invariant (msg="${1//$'\n'/ }").
  • await / send-await: the tail -f … | head -n1 can orphan the tail -f until the next write — process substitution avoids it.
  • await: a read-cursor (LASTLINE) so it doesn't return an already-seen last line.

Thanks — the diagnostics here were good; closing in favor of the rebased salvage.

marksverdhei added a commit that referenced this pull request Jun 27, 2026
… set -e (#7)

`cf register`'s retry loop used `((attempts++))`. Post-increment evaluates to
the *old* value, so on the first iteration it yields 0 — which bash reports as
exit status 1. Combined with the script's `set -e`, that aborted `cf register`
entirely the moment a generated name collided with an existing one, instead of
retrying. Replace it with `attempts=$((attempts + 1))` (an assignment always
returns success), and add a guard so exhausting all 100 attempts errors out
rather than silently registering a duplicate name.

Adds a regression test (`test_register_survives_name_collisions`) that fills a
chunk of the name space so collisions are frequent and registers 30×; it fails
against the old `((attempts++))` and passes with the fix. Suite: 40/40.

Salvaged from the superseded #1 (which can't merge — it predates the global-room
rework and would revert main's CI + presentation tests).

Co-authored-by: marksverdhei <marksverd@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
marksverdhei added a commit that referenced this pull request Jun 27, 2026
…riant) (#8)

`cf send "$msg"` wrote the arg verbatim, so a message containing a newline
landed as two physical lines. The chatroom is line-oriented ("Format: Name:
msg") and `await` derives the sender via `tail -n 1 | cut -d: -f1`, so the
wrapped second line — which has no "Name:" prefix — gets parsed as a phantom
message from a sender named after the text. Collapse newlines to spaces in
both `send` and `send-await`.

Adds `test_send_collapses_newlines` (a 2-line message must add exactly one
physical line, prefixed by the sender). Fails against the old code, passes
with the fix; suite 41/41.

Second of the follow-ups noted when #1 was closed as superseded.

Co-authored-by: marksverdhei <marksverd@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

Session state not persisted across cf subcommands

1 participant