Skip to content

fix(worktree): skip rescue for noise-only worktrees#397

Merged
dimakis merged 1 commit into
mainfrom
session/2026-06-25-90eaf3eb17db
Jun 25, 2026
Merged

fix(worktree): skip rescue for noise-only worktrees#397
dimakis merged 1 commit into
mainfrom
session/2026-06-25-90eaf3eb17db

Conversation

@dimakis

@dimakis dimakis commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Worktree rescue was creating draft PRs for sessions that had no meaningful work — just the .mitzo-session marker file. This caused 15+ empty "Rescued" PRs to pile up in mgmt.
  • Added a noise-file check after staging: if all staged files are in RESCUE_NOISE_FILES (currently just .mitzo-session), the rescue bails out early — no commit, no push, no PR.
  • The set is extensible if other noise markers appear in the future.

Test plan

  • New test: skips rescue when only .mitzo-session is staged
  • New test: skips rescue when no files are staged
  • New test: proceeds with rescue when meaningful files exist alongside noise
  • All 15 existing + new rescue-worktree tests pass

🤖 Generated with Claude Code

@dimakis

dimakis commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

Centaur Review

Found 4 issue(s) (1 warning).

packages/client/src/sse-connection.ts

Solid multi-pronged fix for replay storms and message loss. The worktree noise-file filtering and shouldSync/replay-cap changes are clean and well-tested. The SSE POST retry mechanism works but should guard against permanent HTTP failures to prevent infinite re-queue cycles.

  • 🟡 unsafe_assumptions (L336): Re-queues on ALL non-ok responses (!res.ok), including permanent failures like 400 Bad Request or 403 Forbidden. These will never succeed but will cycle through pendingSends on every reconnect indefinitely. Consider limiting re-queue to transient statuses (404, 502, 503) or adding a max-retry counter to avoid infinite retry loops for permanently rejected messages. [fixable]
  • 🔵 unsafe_assumptions (L352): flushPendingSends calls doPost without await. If a flushed POST fails and a new reconnect completes before the failed doPost's catch handler runs, the re-queued message won't be included in the next flush (it pushes to pendingSends after the new flush already cleared and iterated). This is a narrow timing window but could cause a message to sit in pendingSends until a third reconnect. [fixable]

packages/protocol/src/event-store.ts

Solid multi-pronged fix for replay storms and message loss. The worktree noise-file filtering and shouldSync/replay-cap changes are clean and well-tested. The SSE POST retry mechanism works but should guard against permanent HTTP failures to prevent infinite re-queue cycles.

  • 🔵 missing_tests (L617): New getHeadSeq method has no unit test in the protocol package's event-store test suite. It is only mocked in the connection-registry tests. A dedicated test covering the three cases (session with events, empty session, non-existent session) would be valuable given this is a SQL query backing a replay-storm prevention mechanism. [fixable]

server/index.ts

Solid multi-pronged fix for replay storms and message loss. The worktree noise-file filtering and shouldSync/replay-cap changes are clean and well-tested. The SSE POST retry mechanism works but should guard against permanent HTTP failures to prevent infinite re-queue cycles.

  • 🔵 style (L375): Comment says SSE is the 'Default transport' and WS is 'opt-in via localStorage', but this is a comment-only change in a PR titled 'fix(worktree): skip rescue for noise-only worktrees'. Consider splitting transport documentation updates into their own commit/PR to keep the changeset focused.

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 5 issue(s) (2 warning).

server/worktree.ts

Solid defensive PR addressing replay storms and noise worktrees. Two substantive issues: basename() in noise-file check is overly broad (should be strict equality), and silently capping replay depth without a client-side gap signal may cause subtle data loss on reconnect.

  • 🟡 bugs (L470): basename(f) matches .mitzo-session at any directory depth (e.g. subdir/.mitzo-session). Since .mitzo-session is only created at the worktree root, a simple equality check f === '.mitzo-session' would be more precise and avoid false positives if a file with the same name ever appears in a subdirectory. [fixable]

packages/harness/src/connection-registry.ts

Solid defensive PR addressing replay storms and noise worktrees. Two substantive issues: basename() in noise-file check is overly broad (should be strict equality), and silently capping replay depth without a client-side gap signal may cause subtle data loss on reconnect.

  • 🟡 unsafe_assumptions (L248): When the replay gap is capped, events between the old and new cursor are silently dropped with no client-side notification. The comment says clients can lazy-load via REST API, but there is no gap signal (e.g. events_skipped message) sent to the client, so the client has no way to know it missed events unless it independently tracks seq continuity. Consider sending a gap notification so the client can trigger a full restore. [fixable]

server/chat.ts

Solid defensive PR addressing replay storms and noise worktrees. Two substantive issues: basename() in noise-file check is overly broad (should be strict equality), and silently capping replay depth without a client-side gap signal may cause subtle data loss on reconnect.

  • 🔵 missing_tests (L1269): The new try/catch around queryInstance.interrupt() has no test coverage. A test where interrupt rejects (e.g. vi.fn().mockRejectedValue(new Error('not ready'))) should verify that the function still returns true and the prompt is still pushed to inputQueue. [fixable]

packages/harness/__tests__/connection-registry.test.ts

Solid defensive PR addressing replay storms and noise worktrees. Two substantive issues: basename() in noise-file check is overly broad (should be strict equality), and silently capping replay depth without a client-side gap signal may cause subtle data loss on reconnect.

  • 🔵 missing_tests (L507): The 'caps replay depth' test doesn't verify the log.warn call or that the cursor is actually persisted (subsequent sync rounds should start from the new cursor, not re-cap). A follow-up assertion after a second timer advance would confirm the cursor update sticks. [fixable]

server/index.ts

Solid defensive PR addressing replay storms and noise worktrees. Two substantive issues: basename() in noise-file check is overly broad (should be strict equality), and silently capping replay depth without a client-side gap signal may cause subtle data loss on reconnect.

  • 🔵 style (L372): The comment update ('SSE Chat Transport (primary)' and 'Default transport') is documentation about the transport migration status — useful context but may go stale. Minor nit, no action needed.

Comment thread server/worktree.ts Outdated
timeout: WORKTREE_GIT_TIMEOUT_MS,
}).trim();
const files = staged ? staged.split('\n') : [];
if (files.length === 0 || files.every((f) => RESCUE_NOISE_FILES.has(basename(f)))) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: basename(f) matches .mitzo-session at any directory depth (e.g. subdir/.mitzo-session). Since .mitzo-session is only created at the worktree root, a simple equality check f === '.mitzo-session' would be more precise and avoid false positives if a file with the same name ever appears in a subdirectory. [fixable]

connectionId,
sessionId,
oldCursor: cursor,
newCursor,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 unsafe_assumptions: When the replay gap is capped, events between the old and new cursor are silently dropped with no client-side notification. The comment says clients can lazy-load via REST API, but there is no gap signal (e.g. events_skipped message) sent to the client, so the client has no way to know it missed events unless it independently tracks seq continuity. Consider sending a gap notification so the client can trigger a full restore. [fixable]

Comment thread server/chat.ts Outdated
await Promise.allSettled(stops);
}
await session.queryInstance.interrupt();
try {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 missing_tests: The new try/catch around queryInstance.interrupt() has no test coverage. A test where interrupt rejects (e.g. vi.fn().mockRejectedValue(new Error('not ready'))) should verify that the function still returns true and the prompt is still pushed to inputQueue. [fixable]

const sessionIds = calls.map((c: unknown[]) => c[0]);
expect(sessionIds).toContain('sess-a');
expect(sessionIds).toContain('sess-b');

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 missing_tests: The 'caps replay depth' test doesn't verify the log.warn call or that the cursor is actually persisted (subsequent sync rounds should start from the new cursor, not re-cap). A follow-up assertion after a second timer advance would confirm the cursor update sticks. [fixable]

Comment thread server/index.ts Outdated
};

// ─── SSE Chat Transport ──────────────────────────────────────────────────────
// ─── SSE Chat Transport (primary) ────────────────────────────────────────────

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 style: The comment update ('SSE Chat Transport (primary)' and 'Default transport') is documentation about the transport migration status — useful context but may go stale. Minor nit, no action needed.

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 5 issue(s) (1 warning).

packages/client/src/sse-connection.ts

Solid defensive fixes for replay storms and noise-only worktree rescue. Main concern: the SSE send POST re-queue has no retry limit or backoff, risking infinite retries for persistently failing messages.

  • 🟡 bugs (L338): Re-queuing in doPost bypasses the MAX_PENDING_SENDS cap that guards send(). If a send POST fails persistently (e.g. 400 from malformed payload), it will be re-queued on every reconnect indefinitely with no retry limit, attempt counter, or backoff. The queue won't grow unbounded within a single flush (since flushPendingSends swaps the array), but across reconnect cycles a poisoned message retries forever. [fixable]

packages/client/src/__tests__/sse-connection.test.ts

Solid defensive fixes for replay storms and noise-only worktree rescue. Main concern: the SSE send POST re-queue has no retry limit or backoff, risking infinite retries for persistently failing messages.

  • 🔵 missing_tests (L878): No test covers the case where a re-queued send fails again on the next reconnect cycle. This is the scenario that would expose the unbounded retry issue — a message that always returns 400 should eventually be discarded or capped. [fixable]

server/chat.ts

Solid defensive fixes for replay storms and noise-only worktree rescue. Main concern: the SSE send POST re-queue has no retry limit or backoff, risking infinite retries for persistently failing messages.

  • 🔵 missing_tests (L1269): The new try/catch around queryInstance.interrupt() has no test covering the error path. The existing interrupt tests in send-to-chat.test.ts mock interrupt() to succeed. A test where interrupt() rejects would verify the error is caught and logged without crashing the session. [fixable]

server/index.ts

Solid defensive fixes for replay storms and noise-only worktree rescue. Main concern: the SSE send POST re-queue has no retry limit or backoff, risking infinite retries for persistently failing messages.

  • 🔵 style (L111): The shouldSync allowlist (ACTIVE || STARTING || CREATED) is correct but fragile — adding a new syncable state requires remembering to update this check. Consider a denylist (state !== 'ENDED' && state !== 'SUSPENDED' && ...) that matches the comment, or import the states as a const set, so new states default to synced rather than silently dropped. [fixable]

server/worktree.ts

Solid defensive fixes for replay storms and noise-only worktree rescue. Main concern: the SSE send POST re-queue has no retry limit or backoff, risking infinite retries for persistently failing messages.

  • 🔵 unsafe_assumptions (L462): The RESCUE_NOISE_FILES set only contains .mitzo-session, which is correct since that file is only created at the worktree root. But the set is declared at module scope — if future noise files are added at subdirectory paths, git diff --cached --name-only would return them as subdir/file and exact-match would miss them. A comment noting the root-only assumption would help future maintainers. [fixable]

Comment thread packages/client/src/sse-connection.ts Outdated
});
if (!res.ok && endpoint === 'send') {
// Message delivery failed (e.g. 404 from stale connectionId).
// Re-queue so it flushes on the next successful reconnect.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: Re-queuing in doPost bypasses the MAX_PENDING_SENDS cap that guards send(). If a send POST fails persistently (e.g. 400 from malformed payload), it will be re-queued on every reconnect indefinitely with no retry limit, attempt counter, or backoff. The queue won't grow unbounded within a single flush (since flushPendingSends swaps the array), but across reconnect cycles a poisoned message retries forever. [fixable]


// ─── POST retry on failure ───────────────────────────────────────────────

it('re-queues send POST on HTTP error (e.g. 404 from stale connectionId)', async () => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 missing_tests: No test covers the case where a re-queued send fails again on the next reconnect cycle. This is the scenario that would expose the unbounded retry issue — a message that always returns 400 should eventually be discarded or capped. [fixable]

Comment thread server/chat.ts Outdated
await Promise.allSettled(stops);
}
await session.queryInstance.interrupt();
try {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 missing_tests: The new try/catch around queryInstance.interrupt() has no test covering the error path. The existing interrupt tests in send-to-chat.test.ts mock interrupt() to succeed. A test where interrupt() rejects would verify the error is caught and logged without crashing the session. [fixable]

Comment thread server/index.ts Outdated
const state = eventStore.getSessionState(sessionId);
// Only sync sessions that are actively running or starting.
// ENDED/SUSPENDED/DETACHED/CLOSING don't need delivery retries.
return state === 'ACTIVE' || state === 'STARTING' || state === 'CREATED';

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 style: The shouldSync allowlist (ACTIVE || STARTING || CREATED) is correct but fragile — adding a new syncable state requires remembering to update this check. Consider a denylist (state !== 'ENDED' && state !== 'SUSPENDED' && ...) that matches the comment, or import the states as a const set, so new states default to synced rather than silently dropped. [fixable]

Comment thread server/worktree.ts
return { success: false, error: err instanceof Error ? err.message : String(err) };
}

// 1b. Check if staged changes contain meaningful work (not just noise markers)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 unsafe_assumptions: The RESCUE_NOISE_FILES set only contains .mitzo-session, which is correct since that file is only created at the worktree root. But the set is declared at module scope — if future noise files are added at subdirectory paths, git diff --cached --name-only would return them as subdir/file and exact-match would miss them. A comment noting the root-only assumption would help future maintainers. [fixable]

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 4 issue(s) (2 warning).

server/worktree.ts

The core transport fixes (SSE retry, replay cap, interrupt guard) are solid and well-tested. The main issue is that noise-only worktrees become permanently stuck in cleanup — cleanupStaleWorktrees needs to handle the 'no meaningful changes' rescue result by removing the worktree instead of skipping it.

  • 🟡 bugs (L470): Noise-only worktrees become permanently stuck. cleanupStaleWorktrees calls hasUncommittedWork (which reports dirty for .mitzo-session), then rescueDirtyWorktree returns { success: false, error: 'no meaningful changes to rescue' }. The cleanup treats this identically to a real rescue failure — it skips the worktree and posts to inbox. On subsequent cycles, alreadyNotified is true so rescue is never retried, and the worktree is never removed. Pre-PR, these worktrees were rescued (commit+push+PR); post-PR, they accumulate on disk forever. Fix: cleanupStaleWorktrees should detect the 'no meaningful changes' case and remove the worktree directly, since there's nothing worth saving. [fixable]

packages/harness/__tests__/connection-registry.test.ts

The core transport fixes (SSE retry, replay cap, interrupt guard) are solid and well-tested. The main issue is that noise-only worktrees become permanently stuck in cleanup — cleanupStaleWorktrees needs to handle the 'no meaningful changes' rescue result by removing the worktree instead of skipping it.

  • 🟡 missing_tests (L535): The 'caps replay depth when cursor is far behind head' test (line 511) is missing registry.stopPeriodicSync() at the end, unlike every other periodic sync test in this describe block. This leaks a setInterval timer that could cause flaky test behavior if timer state bleeds between tests. [fixable]

packages/protocol/src/event-store.ts

The core transport fixes (SSE retry, replay cap, interrupt guard) are solid and well-tested. The main issue is that noise-only worktrees become permanently stuck in cleanup — cleanupStaleWorktrees needs to handle the 'no meaningful changes' rescue result by removing the worktree instead of skipping it.

  • 🔵 missing_tests (L618): The new getHeadSeq method has no direct unit test in packages/protocol/__tests__/event-store.test.ts. It's only indirectly tested via mocked getHeadSeq in connection-registry tests. A bug in the SQL query (SELECT MAX(seq)) or null handling would not be caught. Add a test covering: session with events returns max seq, session with no events returns 0, unknown session returns 0. [fixable]

server/index.ts

The core transport fixes (SSE retry, replay cap, interrupt guard) are solid and well-tested. The main issue is that noise-only worktrees become permanently stuck in cleanup — cleanupStaleWorktrees needs to handle the 'no meaningful changes' rescue result by removing the worktree instead of skipping it.

  • 🔵 style (L107): The shouldSync callback returns false when getSessionState returns null (session not in DB). This is likely correct, but worth a brief comment since the implicit null→false behavior is non-obvious and could silently skip sync for sessions whose state record was lost (e.g. after DB corruption). [fixable]

Comment thread server/worktree.ts
timeout: WORKTREE_GIT_TIMEOUT_MS,
}).trim();
const files = staged ? staged.split('\n') : [];
if (files.length === 0 || files.every((f) => RESCUE_NOISE_FILES.has(f))) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: Noise-only worktrees become permanently stuck. cleanupStaleWorktrees calls hasUncommittedWork (which reports dirty for .mitzo-session), then rescueDirtyWorktree returns { success: false, error: 'no meaningful changes to rescue' }. The cleanup treats this identically to a real rescue failure — it skips the worktree and posts to inbox. On subsequent cycles, alreadyNotified is true so rescue is never retried, and the worktree is never removed. Pre-PR, these worktrees were rescued (commit+push+PR); post-PR, they accumulate on disk forever. Fix: cleanupStaleWorktrees should detect the 'no meaningful changes' case and remove the worktree directly, since there's nothing worth saving. [fixable]

// Should have fetched from 9800 (head - 200), not from 0
const calls = (store.getEventsAfter as ReturnType<typeof vi.fn>).mock.calls;
expect(calls[0][1]).toBe(9800); // afterSeq should be head - MAX_REPLAY_GAP
});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 missing_tests: The 'caps replay depth when cursor is far behind head' test (line 511) is missing registry.stopPeriodicSync() at the end, unlike every other periodic sync test in this describe block. This leaks a setInterval timer that could cause flaky test behavior if timer state bleeds between tests. [fixable]

Comment thread packages/protocol/src/event-store.ts Outdated
}

/** Return the highest seq number for a session, or 0 if no events exist. */
getHeadSeq(sessionId: string): number {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 missing_tests: The new getHeadSeq method has no direct unit test in packages/protocol/__tests__/event-store.test.ts. It's only indirectly tested via mocked getHeadSeq in connection-registry tests. A bug in the SQL query (SELECT MAX(seq)) or null handling would not be caught. Add a test covering: session with events returns max seq, session with no events returns 0, unknown session returns 0. [fixable]

Comment thread server/index.ts Outdated
getEventsAfter: (sessionId, afterSeq, limit) =>
eventStore.getEventsAfter(sessionId, afterSeq, limit),
isSessionActive: (sessionId) => eventStore.getSession(sessionId)?.isActive ?? false,
shouldSync: (sessionId) => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 style: The shouldSync callback returns false when getSessionState returns null (session not in DB). This is likely correct, but worth a brief comment since the implicit null→false behavior is non-obvious and could silently skip sync for sessions whose state record was lost (e.g. after DB corruption). [fixable]

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 7 issue(s) (2 warning).

server/index.ts

Solid defensive PR addressing three real failure modes (replay storms, noise-file rescues, interrupt crashes). The re-queue logic in SseConnection lacks backoff/retry-count enforcement on the doPost path which could cause spin under persistent server errors, but the bounded queue prevents memory issues. No critical bugs found.

  • 🟡 bugs (L111): shouldSync returns false when getSessionState returns null (unknown/missing session). This means a session that hasn't been registered in the EventStore yet (e.g. race between watch and session creation) will be silently skipped by periodic sync. The previous isSessionActive had the same behavior (defaulted to false via ?? false), so this isn't a regression — but worth noting as a latent issue. Consider treating null state as sync-eligible. [fixable]

packages/client/src/sse-connection.ts

Solid defensive PR addressing three real failure modes (replay storms, noise-file rescues, interrupt crashes). The re-queue logic in SseConnection lacks backoff/retry-count enforcement on the doPost path which could cause spin under persistent server errors, but the bounded queue prevents memory issues. No critical bugs found.

  • 🟡 unsafe_assumptions (L337): doPost re-queues failed sends into pendingSends, but flushPendingSends does not await doPost calls (line 357). If the flush fires and all POSTs fail (e.g. server is up but returns 500), messages silently accumulate back into pendingSends without any backoff or retry-count cap. The MAX_PENDING_SENDS=100 limit in send() does not apply to re-queued items pushed from doPost — only to items pushed via the public send() method — so a persistent server-side error could cause the same messages to cycle indefinitely through flush→fail→requeue. [fixable]

packages/client/src/__tests__/sse-connection.test.ts

Solid defensive PR addressing three real failure modes (replay storms, noise-file rescues, interrupt crashes). The re-queue logic in SseConnection lacks backoff/retry-count enforcement on the doPost path which could cause spin under persistent server errors, but the bounded queue prevents memory issues. No critical bugs found.

  • 🔵 missing_tests: No test verifies behavior when a re-queued send fails again on the second flush. This would exercise the flush→fail→requeue→flush cycle and confirm no infinite loop or unbounded growth. [fixable]

packages/protocol/src/event-store.ts

Solid defensive PR addressing three real failure modes (replay storms, noise-file rescues, interrupt crashes). The re-queue logic in SseConnection lacks backoff/retry-count enforcement on the doPost path which could cause spin under persistent server errors, but the bounded queue prevents memory issues. No critical bugs found.

  • 🔵 missing_tests (L618): getHeadSeq is a new public method on EventStore but has no unit test. Should verify: returns 0 for unknown session, returns correct max seq for a session with events, and returns correct value after new events are appended. [fixable]

packages/harness/src/connection-registry.ts

Solid defensive PR addressing three real failure modes (replay storms, noise-file rescues, interrupt crashes). The re-queue logic in SseConnection lacks backoff/retry-count enforcement on the doPost path which could cause spin under persistent server errors, but the bounded queue prevents memory issues. No critical bugs found.

  • 🔵 bugs (L242): The replay gap check head - cursor > MAX_REPLAY_GAP performs integer subtraction without clamping. If getHeadSeq returns 0 (no events) and cursor is 0, the gap is 0 which is fine. But if a buggy adapter returns a negative head, the subtraction could produce unexpected results. Minor — the real EventStore.getHeadSeq returns row?.head ?? 0 so this can't happen in practice. [fixable]

server/worktree.ts

Solid defensive PR addressing three real failure modes (replay storms, noise-file rescues, interrupt crashes). The re-queue logic in SseConnection lacks backoff/retry-count enforcement on the doPost path which could cause spin under persistent server errors, but the bounded queue prevents memory issues. No critical bugs found.

  • 🔵 style (L466): The noise-file check step is labeled '1b' in test comments but has no step comment in the actual code (unlike steps 1, 2, 3, 4 which have numbered comments). Adding a consistent step label would improve readability. [fixable]

server/__tests__/rescue-worktree.test.ts

Solid defensive PR addressing three real failure modes (replay storms, noise-file rescues, interrupt crashes). The re-queue logic in SseConnection lacks backoff/retry-count enforcement on the doPost path which could cause spin under persistent server errors, but the bounded queue prevents memory issues. No critical bugs found.

  • 🔵 missing_tests: No test covers the case where the git diff --cached --name-only command itself fails (the outer catch block at worktree.ts line 484). The comment says 'proceed with rescue anyway' — a test confirming this fallthrough behavior would be valuable. [fixable]

Comment thread server/index.ts Outdated
const state = eventStore.getSessionState(sessionId);
// Only sync sessions that are actively running or starting.
// ENDED/SUSPENDED/DETACHED/CLOSING don't need delivery retries.
return state === 'ACTIVE' || state === 'STARTING' || state === 'CREATED';

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: shouldSync returns false when getSessionState returns null (unknown/missing session). This means a session that hasn't been registered in the EventStore yet (e.g. race between watch and session creation) will be silently skipped by periodic sync. The previous isSessionActive had the same behavior (defaulted to false via ?? false), so this isn't a regression — but worth noting as a latent issue. Consider treating null state as sync-eligible. [fixable]

Comment thread packages/client/src/sse-connection.ts Outdated
body: JSON.stringify(body),
});
if (!res.ok && endpoint === 'send') {
// Message delivery failed (e.g. 404 from stale connectionId).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 unsafe_assumptions: doPost re-queues failed sends into pendingSends, but flushPendingSends does not await doPost calls (line 357). If the flush fires and all POSTs fail (e.g. server is up but returns 500), messages silently accumulate back into pendingSends without any backoff or retry-count cap. The MAX_PENDING_SENDS=100 limit in send() does not apply to re-queued items pushed from doPost — only to items pushed via the public send() method — so a persistent server-side error could cause the same messages to cycle indefinitely through flush→fail→requeue. [fixable]

Comment thread packages/protocol/src/event-store.ts Outdated
}

/** Return the highest seq number for a session, or 0 if no events exist. */
getHeadSeq(sessionId: string): number {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 missing_tests: getHeadSeq is a new public method on EventStore but has no unit test. Should verify: returns 0 for unknown session, returns correct max seq for a session with events, and returns correct value after new events are appended. [fixable]

// Prevents replay storms after server restart or long disconnects.
if (this.eventStore.getHeadSeq) {
const head = this.eventStore.getHeadSeq(sessionId);
if (head - cursor > MAX_REPLAY_GAP) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 bugs: The replay gap check head - cursor > MAX_REPLAY_GAP performs integer subtraction without clamping. If getHeadSeq returns 0 (no events) and cursor is 0, the gap is 0 which is fine. But if a buggy adapter returns a negative head, the subtraction could produce unexpected results. Minor — the real EventStore.getHeadSeq returns row?.head ?? 0 so this can't happen in practice. [fixable]

Comment thread server/worktree.ts
return { success: false, error: err instanceof Error ? err.message : String(err) };
}

// 1b. Check if staged changes contain meaningful work (not just noise markers)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 style: The noise-file check step is labeled '1b' in test comments but has no step comment in the actual code (unlike steps 1, 2, 3, 4 which have numbered comments). Adding a consistent step label would improve readability. [fixable]

Worktree rescue was creating draft PRs for sessions with no meaningful
work — just the .mitzo-session marker file. Added a noise-file check
after staging: if all staged files are in RESCUE_NOISE_FILES, bail out
early instead of committing/pushing/creating a PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dimakis dimakis force-pushed the session/2026-06-25-90eaf3eb17db branch from 9d9d8c0 to 1858d72 Compare June 25, 2026 22:54
@dimakis dimakis merged commit b7631c9 into main Jun 25, 2026
1 check passed

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 3 issue(s) (1 warning).

server/worktree.ts

The noise-file filter correctly prevents useless rescue PRs, but callers still treat the result as a rescue failure — noise-only worktrees will accumulate on disk because stale GC re-encounters them every cycle.

  • 🟡 bugs (L489): When rescue returns { success: false, error: 'no meaningful changes to rescue' }, both callers (cleanupStaleWorktrees at line ~682 and cleanupSessionWorktrees in chat.ts at line ~1319) treat this identically to a real rescue failure: the worktree is preserved on disk, and in the stale-GC path an inbox notification is posted. Since hasUncommittedWork still detects .mitzo-session as dirty (it's not in .gitignore), stale GC will re-encounter the same worktree every cycle, re-attempt rescue, get the same 'no meaningful changes' result, and preserve it indefinitely. Noise-only worktrees will accumulate on disk. Consider either (a) returning a distinct signal (e.g. { success: false, noMeaningfulChanges: true }) so callers can safely remove the worktree, or (b) filtering noise files in hasUncommittedWork so the worktree is seen as clean. [fixable]

server/__tests__/rescue-worktree.test.ts

The noise-file filter correctly prevents useless rescue PRs, but callers still treat the result as a rescue failure — noise-only worktrees will accumulate on disk because stale GC re-encounters them every cycle.

  • 🔵 style (L277): Three new test cases have comments saying 'Step 1: git add -A' (lines 277, 296, 317) but the implementation at worktree.ts:468 uses git add -u. The distinction matters (tracked-only vs all files). Inaccurate test comments can mislead future maintainers about what the code actually does. [fixable]
  • 🔵 missing_tests: No test covers the outer catch at worktree.ts:491 where git diff --cached --name-only itself throws. The code deliberately proceeds with rescue in that case ('safer than skipping'), but this fail-open behavior is untested. [fixable]

Comment thread server/worktree.ts
} catch {
// Non-fatal — worktree is about to be removed anyway
}
return { success: false, error: 'no meaningful changes to rescue' };

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: When rescue returns { success: false, error: 'no meaningful changes to rescue' }, both callers (cleanupStaleWorktrees at line ~682 and cleanupSessionWorktrees in chat.ts at line ~1319) treat this identically to a real rescue failure: the worktree is preserved on disk, and in the stale-GC path an inbox notification is posted. Since hasUncommittedWork still detects .mitzo-session as dirty (it's not in .gitignore), stale GC will re-encounter the same worktree every cycle, re-attempt rescue, get the same 'no meaningful changes' result, and preserve it indefinitely. Noise-only worktrees will accumulate on disk. Consider either (a) returning a distinct signal (e.g. { success: false, noMeaningfulChanges: true }) so callers can safely remove the worktree, or (b) filtering noise files in hasUncommittedWork so the worktree is seen as clean. [fixable]

it('skips rescue when only noise files (.mitzo-session) are staged', () => {
// Step 0: getRepoRemote
mockExecFileSync.mockReturnValueOnce('git@github.com:dimakis/mgmt.git\n' as never);
// Step 1: git add -A

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 style: Three new test cases have comments saying 'Step 1: git add -A' (lines 277, 296, 317) but the implementation at worktree.ts:468 uses git add -u. The distinction matters (tracked-only vs all files). Inaccurate test comments can mislead future maintainers about what the code actually does. [fixable]

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.

1 participant