Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Notable changes to Swarmail. Versions follow [semantic versioning](https://semver.org).

## Unreleased

- A prompt no longer retries a pending registration whose edit checkout has been deleted. The server refuses a
worktree that is gone, so the hook warned on every prompt until the session ended. The pending entry is dropped;
the session's next edit registers it again.

## 0.5.1 - 2026-10-09

- A session whose usual name is already held by another session in a project
Expand Down
11 changes: 8 additions & 3 deletions src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { renameOver } from "./files.ts";
import { stateHome, t3Home } from "./paths.ts";
import {
closeSync,
existsSync,
mkdirSync,
openSync,
readdirSync,
Expand Down Expand Up @@ -412,10 +413,14 @@ export function openRegistry<S extends RegisterState = RegisterState>(
if (endedSince(state, since)) {
return;
}
pending = state.pending;
if (state.ended) {
// A retry for an edit checkout that has since been deleted can never succeed: the server refuses a
// worktree that is gone, and the hook would warn on every prompt. A later edit registers again.
const stale = state.pending?.worktree !== undefined && !existsSync(state.pending.worktree);
pending = stale ? undefined : state.pending;
if (stale || state.ended) {
const { ended, ...rest } = state;
writeState(file, rest as S);
const { pending: dropped, ...withoutPending } = rest;
writeState(file, (stale ? withoutPending : rest) as S);
}
});
return pending;
Expand Down
40 changes: 32 additions & 8 deletions test/registry.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { openRegistry } from "../src/registry.ts";
Expand Down Expand Up @@ -92,7 +92,11 @@ test("readers skip unreadable or foreign state and leftover temporary files", ()
});

test("a changed edit checkout refreshes registration and survives a failed retry", () => {
withRegistry((registry) => {
withRegistry((registry, dir) => {
const a = join(dir, "a");
const b = join(dir, "b");
mkdirSync(a);
mkdirSync(b);
let up = true;
const calls = [];
const register = (project, name) => {
Expand All @@ -107,24 +111,24 @@ test("a changed edit checkout refreshes registration and survives a failed retry
worktree,
register,
});
settle("/w/a");
settle("/w/a");
settle(a);
settle(a);
expect(calls).toHaveLength(1);
up = false;
expect(settle("/w/b").after.worktrees["/r"]).toBe("/w/a");
expect(settle(b).after.worktrees["/r"]).toBe(a);
expect(registry.resume("edit", 0)).toEqual({
project: "/r",
tag: "[claude:edit cwd:/launch]",
worktree: "/w/b",
worktree: b,
});
expect(settle(undefined).after.pending).toEqual({
project: "/r",
tag: "[claude:edit cwd:/launch]",
worktree: "/w/b",
worktree: b,
});
up = true;
const pending = registry.resume("edit", 0);
expect(settle(pending.worktree).after.worktrees["/r"]).toBe("/w/b");
expect(settle(pending.worktree).after.worktrees["/r"]).toBe(b);
expect(registry.read("edit").pending).toBeUndefined();
expect(calls).toEqual([
["/r", null],
Expand All @@ -134,6 +138,26 @@ test("a changed edit checkout refreshes registration and survives a failed retry
});
});

test("a prompt drops a pending registration whose edit checkout was deleted", () => {
withRegistry((registry, dir) => {
const gone = join(dir, "gone");
mkdirSync(gone);
const settle = (worktree) =>
registry.settle("s1", {
since: 0,
project: "/r",
tag: "[t1]",
worktree,
register: () => null,
});
expect(settle(gone).after.pending).toEqual({ project: "/r", tag: "[t1]", worktree: gone });
expect(registry.resume("s1", 0)).toEqual({ project: "/r", tag: "[t1]", worktree: gone });
rmSync(gone, { recursive: true });
expect(registry.resume("s1", 0)).toBeUndefined();
expect(registry.read("s1").pending).toBeUndefined();
});
});

test("registration orderings preserve pending edit locations across supervisor calls", () => {
withRegistry((registry, dir) => {
const events = [
Expand Down
Loading