From cfde3de0c212a56c2dfb73b187f48505a6be8bea Mon Sep 17 00:00:00 2001 From: OneStepAt4time Date: Wed, 13 May 2026 11:36:47 +0200 Subject: [PATCH 1/3] fix(monitor): replace dots in project-hash slug so JSONL is found for .claude workdirs (#3286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code's project-directory slug replaces every `.` in a path segment with `-`. Aegis's computeProjectHash preserved the dot, so workdirs like `/.claude/worktrees/x` produced a slug that didn't match the actual `~/.claude/projects/-...-aegis--claude-worktrees-x/` directory. discoverFromFilesystemFallback then looked in the wrong folder, the monitor never found the JSONL, and channels.message('message.assistant') never fired — so assistant replies never reached Telegram / Slack / webhooks / the legacy transcript endpoint. Add a `.replace(/\./g, '-')` to segment sanitization and an assertion covering the .claude case. Closes #3286 --- src/__tests__/path-utils-909.test.ts | 5 +++++ src/path-utils.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/__tests__/path-utils-909.test.ts b/src/__tests__/path-utils-909.test.ts index 323ad5e4b..80d6224d2 100644 --- a/src/__tests__/path-utils-909.test.ts +++ b/src/__tests__/path-utils-909.test.ts @@ -21,4 +21,9 @@ describe('Issue #909: computeProjectHash cross-platform normalization', () => { it('returns fallback for empty input', () => { expect(computeProjectHash('')).toBe('-'); }); + + it('replaces dots in segments to match Claude Code slug (Issue #3286)', () => { + expect(computeProjectHash('/home/me/.claude/repo')).toBe('-home-me--claude-repo'); + expect(computeProjectHash('/Users/x/Documents/aegis/.claude/worktrees/foo')).toBe('-Users-x-Documents-aegis--claude-worktrees-foo'); + }); }); diff --git a/src/path-utils.ts b/src/path-utils.ts index 8cf2a4853..2d36515e7 100644 --- a/src/path-utils.ts +++ b/src/path-utils.ts @@ -15,7 +15,7 @@ export function computeProjectHash(workDir: string): string { const segments = withLowerDrive .split('/') .filter(Boolean) - .map((segment) => segment.replace(/:/g, '').replace(/\s+/g, '-')); + .map((segment) => segment.replace(/:/g, '').replace(/\s+/g, '-').replace(/\./g, '-')); if (segments.length === 0) return '-'; return `-${segments.join('-')}`; From faf1c9dd4d0a3170562efd995f692d8add36bb48 Mon Sep 17 00:00:00 2001 From: OneStepAt4time Date: Wed, 13 May 2026 11:58:37 +0200 Subject: [PATCH 2/3] fix(monitor): forward messages when watcher hasn't started for THIS session (#3286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The poll-cycle hand-off between checkSession and the JsonlWatcher missed historical entries discovered via the filesystem-fallback path. The guard `!this.jsonlWatcher` only checked whether the watcher *instance* existed (it always does, once initialized). On the discovery poll — where readMessagesForMonitor's fallback just set jsonlPath and read every existing JSONL entry — those entries were dropped, then the watcher attached at end-of-file and saw no further changes for content that pre-dated its attachment. Switch to `!this.jsonlWatcher?.isWatching(session.id)` so checkSession forwards historical entries on the exact poll where the fallback discovers the JSONL. The watcher takes over for subsequent appends. Verified end-to-end with the Telegram channel: assistant messages now fan out to channels for ACP sessions whose workdir was discovered via the filesystem fallback (combined with the slug-mismatch fix in the previous commit). Closes #3286 (combined with previous slug-mismatch fix). --- src/monitor.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/monitor.ts b/src/monitor.ts index 7a0bb892f..84ac8f26e 100644 --- a/src/monitor.ts +++ b/src/monitor.ts @@ -627,8 +627,11 @@ export class SessionMonitor { const result = await this.sessions.readMessagesForMonitor(session.id); const prevStatus = this.lastStatus.get(session.id); - // Forward messages only when watcher is NOT active (fallback polling path) - if (!this.jsonlWatcher && result.messages.length > 0) { + // Forward messages only when watcher is NOT active for THIS session (#3286). + // Checking the watcher instance alone misses the discovery poll where the + // fallback just set jsonlPath but the watcher hasn't started yet — entries + // read here would otherwise be dropped before the watcher subscribes. + if (!this.jsonlWatcher?.isWatching(session.id) && result.messages.length > 0) { this.rateLimitedSessions.delete(session.id); for (const msg of result.messages) { await this.forwardMessage(session, msg); From fa4119885c50550dc7d0ea60e5186b6d52ad7359 Mon Sep 17 00:00:00 2001 From: Emanuele <106186915+OneStepAt4time@users.noreply.github.com> Date: Wed, 13 May 2026 12:47:38 +0200 Subject: [PATCH 3/3] chore: trigger CI refresh