Skip to content

Sessions in one folder no longer overwrite each other's tasks; a blocked prompt gives answers back - #5

Merged
tzafrir merged 18 commits into
mainfrom
claude/project-thread-66doi9
Oct 18, 2026
Merged

tzafrir merged 18 commits into
mainfrom
claude/project-thread-66doi9

Conversation

@tzafrir

@tzafrir tzafrir commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Requested by Tzafrir · project thread

Before: two sessions open in one folder shared one store value, and each heartbeat wrote the whole value back. If they interleaved, one session could put back a stale copy of the other's slot and lose an answer it had just saved (#3). Separately, answers riding a prompt the user typed were marked delivered before the prompt went on, so if a hook further down blocked that prompt (an organization's UserPromptSubmit hook, say), they were lost (#4).

After: each session's slot has a store key of its own, and only that session writes it, so no session can write over another's tasks. A session that takes on another's tasks notes them in its own slot, in the same write, so a crash at any point leaves nothing to take on twice, and a session back from sleep lets go of what another took meanwhile, unless the session it took them from has taken them back since. A session writes nothing until it has read the project, so a store that fails as it joins can't make it write over tasks of its own it hasn't taken on yet. After /clear or a resume in place, the session it becomes takes on what the one it left held; resumed into another folder, it starts there as a new session would, and acts on no task until it has read that folder. Answers riding a typed prompt that a hook drops, or takes them out of, or whose call fails, go back to waiting and are sent with the next prompt or Send now; a toast says so where the engine doesn't already.

How: hooks/project.ts holds the layout: keysOf (a root too long for the store's 256-character keys goes by its hash), projectOf (every slot, and what other sessions took from each, gathered once as read, a note less what was taken back since), adopt (what a joining session takes on) and prune (which notes and leftover slots can go). register.tsx runs its store work one piece at a time, writes only the session's own key, and reads the project only when joining, on the heartbeat, on assigning, or when a press, a prompt or a tool call finds the session asleep; no piece of store work holds up a press, a prompt, a tool call or the start more than 5 seconds. A session takes the project on in one change of its task list, so a press meanwhile is kept, and loadedFor in its state names the session whose tasks the process holds, so a hot reload keeps them. Updates are claimed in one change of the task list before any prompt or row carries them, and passOn gives back those whose head didn't enter. 0.1.4's one-value layout is read as it stands, and changed only to take its sessions' slots out and keep its count ahead while a 0.1.4 session lives there. Anything stored that isn't a slot, a task, a count or a note is skipped. Bumped to 0.1.5 and updated docs/DESIGN.md.

Tests: 125 kit tests pass (claude plugin test .), among them writes and presses landing between a read and a write, a crash between taking tasks on and cleaning up, sleep, resume and /clear (with a failed end write, a store that failed as the session joined, and a resume into another folder), tasks taken back, a store that stops answering, 0.1.4 sessions alive beside this version, and hooks beneath that drop, strip or rewrite a prompt. Each fix was reverted on its own (122 reverts) to check that a test fails, and every one does. tsc -p ., claude plugin validate . and tests/live/test_pane.py against Claude Code 2.1.296 pass.

Fixes #3
Fixes #4

🤖 Generated with Claude Code

https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr

…folder can't write over each other's tasks

Fixes #3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
A prompt the user types while answers wait marks them delivered and passes
on. When a hook further down drops that prompt, or the call fails, the
answers now wait again for the next send, as the mod's own send already did.

Fixes #4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
@tzafrir tzafrir self-assigned this Oct 15, 2026
@tzafrir
tzafrir marked this pull request as ready for review October 15, 2026 12:47
claude added 16 commits October 17, 2026 19:40
… same write

Review of the per-session keys found ways a task could still be lost or
shown twice, a count that could go back, and store work too slow or too
fragile for the paths that wait on it. Now:

- Each session writes only its own key. Taking on another session's tasks
  is one write of the taker's slot, which notes what it took; the slots it
  took from leave after that, only if unchanged since read. A crash
  anywhere in between leaves nothing to take on twice. A session back from
  sleep, or resumed, lets go of what the notes name, before a press, a
  prompt or a tool call acts on its tasks.
- Each slot carries its session's task count, and a slot leaves the store
  only once a later one carries it, so numbers never go back.
- 0.1.4's value is read as it stands, and changed only to take its
  sessions' slots out and, while a 0.1.4 session lives there, to raise the
  count it numbers from; each change is made to the value read just before.
- Ending is one write and no read, waited for through half the end's
  budget at most. After /clear or a resume in place, the session joins the
  project again.
- A press or a prompt writes the slot and reads nothing, unless the
  session has slept. The mod's sends go one at a time.
- A root too long for the store's 256-character keys goes by its hash.
- An answer riding a typed prompt counts as sent only if its opening line
  entered; else it waits again. A toast says so, but for a prompt of the
  user's own that a hook dropped: the engine shows why.
- The live check seeds the store by its file name, so no session has to
  write one first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
A third review found a typed prompt that could carry an answer Send now
was already sending, store work that could hold up a prompt for good, a
/clear that could hide the tasks it meant to take on, and notes that
could leave with a month-old slot. Now:

- An update is claimed, marked delivered in one change of the task
  list, before any prompt or row carries it, so no other path sends it
  meanwhile. A row the running turn refuses gives it back, to go when
  the turn ends, or at once if the turn ended meanwhile.
- A prompt carries an update if the update's head entered (`#5 <title>:`);
  a hook beneath that takes some out gives back those alone. An answer
  typed in the prompt box whose task left meanwhile goes as typed.
- A press, a prompt or a tool call waits on the store 5 seconds at most;
  the work finishes on its own, in its turn, and until it does nothing
  else waits on the store.
- After /clear or a resume in place, the session takes on what the one
  it left holds, from memory and ended, whether or not its end write
  landed.
- A slot that leaves after a month hands its own notes of what it took
  to the session that removes it, so none of those is taken on twice.
- An entry in 0.1.4's value that isn't a slot or a note is skipped.
- A project gathers its slots, and the tasks each session's were taken
  on from by others, once as read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
A fourth review found a /clear whose first read failed could hold its
tasks twice, a session resumed then that could write over its own slot,
store work that could still wait or pile up, a 0.1.4 note that could
leave before the slot let its tasks go, and typed answers that could go
twice, or count as carried when they weren't. Now:

- A session that can't read the project as it joins writes nothing until
  it has; the heartbeat, assigning and its end read it again. It then
  takes on what the store holds for it, with what it held meanwhile: the
  tasks of the session it left, or those it assigned.
- Raising 0.1.4's count when assigning waits on the store 5 seconds at
  most, and while store work given up on is out, the heartbeat asks for
  no more.
- A slot that lets go of tasks a 0.1.4 note names is written before the
  note leaves, assigning too.
- An answer typed in the prompt box while Claude is idle carries the
  responses a batch was about to send; when another send took it as it
  was saved, the prompt is dropped, saying it went.
- A digest's heads are looked for beside a prompt's words, a message's
  in its words.
- list_tasks marks what it shows heard through the same claim as every
  send.
- A note in a slot that isn't one is skipped, and a key that goes on
  with `#` past the prefix is another folder's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
…task

A fifth review found that taking the project on replaced the task list
with a copy taken a moment before, that a hot reload before a failed
read was retried forgot whose tasks the process held, that one stored
task that isn't one could stop a session reading its project (and so,
now, writing), and that a slot leaving could leave a 0.1.4 copy of it to
pass for it. Now:

- A session takes the project on in one change of the task list,
  against the list as it stands, so a press meanwhile is kept.
- The session whose tasks the process holds is the one it last took the
  project on as (`loadedFor`, in the session's state): a hot reload keeps
  it, and moving to another folder lets it go.
- Anything in a slot that isn't a task, a count or a note of tasks taken
  is skipped, in 0.1.4's first layout too.
- A slot that leaves takes with it any copy of it in 0.1.4's value.
- While a 0.1.4 session lives in the project, the heartbeat raises its
  count only when the value just read is behind, with no second read.
- A slot's notes are carried over in one place, and the store's shapes
  checked by one test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
…t assigned

A sixth review found that /clear in a session that never read the
project dropped what it had assigned (round five's `loadedFor` names no
session then), and that a session taking its own task back from a quiet
session that had taken it let the task go again at its next heartbeat,
leaving it in no pane. Now:

- When no session of the process has read the project, what its
  sessions assigned is taken on beside what the store holds for each,
  the sessions before this one ended.
- A session resumed into another folder holds none of this folder's
  tasks, and counts none of its sessions'.
- A note of tasks taken is outweighed by a later note of the session it
  took them from taking them back; the taker drops it when it comes back
  and lets them go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
…lder

A seventh review found that a session taking its task back let it go
again when the write noting it failed, that /clear in a session that
never read the project wrote over the slot of a session resumed
elsewhere meanwhile, that a session resumed into another folder kept
this folder's count, open answer and drafts, and that a session whose
task was taken back while it was gone still wrote an empty note of it.
Now:

- The heartbeat reads the project with the session's own slot as it
  holds it, so a note the store doesn't have yet still counts.
- When no session of the process has read the project, a session of it
  that another process has resumed since keeps its slot: what it
  assigned goes on in this one's.
- A session resumed into another folder starts there as a new one
  would: its count, the answer it was typing and its drafts stay behind.
- A note emptied by a take-back is dropped, so a session that holds
  nothing else writes nothing as it joins.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
An eighth review found that a store stuck as a session started held the
start up for as long as it stayed stuck, that /clear in a session that
never read the project took a session resumed elsewhere, quiet there,
for ended (so its tasks could wait in two panes once it woke), that a
session resumed into another folder could raise a 0.1.4 count there
before reading that project, and that a session whose writes failed
while it held a task never stored its count once the task was answered.
Now:

- Joining waits on the store 5 seconds at most, as a press, a prompt or
  a tool call does: what the session takes on shows once it answers.
- A session of the process that the store shows quiet keeps its slot,
  and the note of what was taken from it waits for it to come back.
- A session that hasn't read the project changes nothing of 0.1.4's.
- A session has a slot from its first write, landed or not, so a later
  write carries its count.

The tests now type an answer without sending it where they mean to, and
aim the slow read at the read they stage; a session that never held a
task is checked to leave no slot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
… the end

A ninth review found three gaps in a session that reads the project only
after joining: it dropped its own tasks it was done with, so a task it
took on could get the number of one its Claude had withdrawn; read only
as it ended, it took on the tasks of a quiet session that might come
back, leaving them in the slot of a session that had just exited; and
read on the heartbeat, it opened no pane for what it took on. Now:

- A session's own tasks it is done with stay in its list as it takes
  the project on, and no task it takes on gets one of their numbers.
- Taking the project on as it ends, a session takes nothing from a quiet
  session; what it holds of a session of its process that the store
  shows quiet, or alive, goes on in its own slot.
- A first read on the heartbeat, assigning or waking shows the project
  as joining does: the pane opens for what was taken on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
A tenth review found:

- A session that first read the project on its heartbeat and then failed
  to write it showed none of what it took on: the status line and the
  pane stayed as they were. Its write now fails quietly, as the
  heartbeat's own does, and the next heartbeat makes it.
- A 0.1.4 note of this session's tasks names numbers only, and a 0.1.4
  session's write can put it back after it was read: a task taken on
  with one of those numbers was then let go. Such a task now gets a new
  number.
- Resumed in another folder, the session kept its selected task and its
  notes of the first folder until it read the new one. It now starts
  there with neither.

The id lists in toasts come from one helper, and the comments on keys,
notes and the folder change say what the code does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
An eleventh review, and the reverts that no test caught, found:

- The start and the heartbeat waited on opening the pane as on the
  store: a surface slow to open it counted as a stalled store, and
  turned the heartbeat off until it opened. The redraw now waits apart,
  5 seconds at most, and never stalls the store.
- A Send now row the last conversation had not read could wake Claude
  in the next one, and a batch of presses or a send held for the turn's
  end crossed into another folder. Each now stays with its conversation.
- Two fixes had no test of their own once the folder change started
  clean: resumed into another folder, the session takes on the slot the
  session it left has there; and after /clear, a session that hasn't
  read the project leaves 0.1.4's value alone. Both have one now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
A twelfth review found that a resumed session holding nothing left its
slot in the store as its last run left it, ended, until its first
heartbeat: joining read the slot but didn't count it as the session's,
as the heartbeat does. Joining now does, so the session writes its slot
as it joins, its count in it.

The design doc says that a session that never read the project reads it
at its end before the write that marks its slot ended.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
A thirteenth review found two gaps while the store is slow after /clear,
when the new session's join waits in the queue behind the old one's end
write:

- A session that ended before its join came up lost its end mark: the
  join cleared it, so the session read the project as a live one,
  taking on quiet sessions' tasks, and its end write left its slot alive
  until it went stale. A count of ends now tells the join whether its
  session ended meanwhile, and keeps the mark if so.
- A task Claude assigned before the join was stamped with the old
  session's id, so it showed as from an earlier session in the pane and
  in Claude's context for good. Tasks now take the conversation's id
  from the engine, and the pane and context compare against it.

The design doc says what a slow join does, and that the 5-second bound
applies to each piece of store work a press or prompt waits on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
A fourteenth review found:

- A hook beneath that rewords a response's head (masks a secret in a
  task's title, say) made its prompt enter without it, so the response
  went back as not sent; the turn that prompt started then ended with it
  waiting, and it went again, turn after turn. A response given back
  once now counts as heard the next time, with a toast saying so.
- A session that ended after /clear before its join had even started
  (the end coming before the clock ran the join) was taken for alive:
  the join now gets the count of ends from the end that scheduled it.
- An assign refused for repeating an open task, or for the open limit,
  wrote nothing, so tasks let go of in its read stayed in the stored
  slot until the next heartbeat. The read now writes the slot when it
  lets a task go.
- Resumed into another folder while the store was slow, a task Claude
  assigned before the join was dropped there and could land in the
  folder it left. Claude can't assign until the join is done, and is
  told to try again in a moment.

The live check fails with a clear message if its folder's path is too
long for the store keys it seeds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
A fifteenth review found that round 14's fixes went too far:

- A response given back once counted as heard the next time, so one a
  hook beneath took out entirely was lost on the second try. Responses
  are given back every time again, as before; one given back by a
  prompt that entered is held from the send at the end of a turn, which
  is what went round and round, and goes with Send now or the next
  prompt.
- The assign check compared the session's root with the one it joined,
  but /cd, a host's directory change and a worktree move change the root
  too, with no join: after one, no task could be assigned. A join into
  another folder now raises a flag until it has read that folder, and
  while it is up the session acts on none of the tasks it holds of the
  folder it left: Claude can't assign, list or withdraw a task, no
  response goes to Claude, and no prompt or context carries one.
- A session that ended before its join ran still got a heartbeat, and
  the pane opened for it. Neither happens now.

A 0.1.4 note whose task was let go of in a write that failed is checked
to leave only once the slot is written without it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
A sixteenth review found:

- list_tasks marked heard every response of a task it listed, one that
  came in after it read the list among them: Claude saw the task open
  and never heard the answer. It now marks only the responses it shows,
  as main did, and a later one goes as any other.

The design doc and comments say exactly what a response a hook gave
back does as a turn ends: it goes along with others, never alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
A seventeenth review found:

- Resumed into another folder and straight back while the store was
  slow, the session measured the second move from where the queue had
  got to, not from the folder the first move was for. It saw no move,
  so between the two joins Claude listed and assigned in the middle
  folder, and the second join dropped what it assigned. A move is now
  measured from the folder the latest join is for, set as it is asked.
- A response held for Send now was still held after a move to another
  folder: a task there with the same number and time waited as if a
  hook had kept it out. The move now clears the held responses with
  the rest of what the session tracked of the folder it left.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR9Scezktm9yfS27HZMNgr
@tzafrir
tzafrir merged commit c4a0579 into main Oct 18, 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.

Answers sent with a typed prompt stay delivered when that prompt is blocked Sessions in the same folder can overwrite each other's stored tasks

2 participants