bind each Claude pane to its own session id (T-146)
A regression from T-137: every pane rendered the newest .jsonl in the workspace dir, so concurrent sessions collided — a secondary tab showed the primary's conversation. Each pane now spawns claude with its own --session-id (a transcript is named <session-id>.jsonl), tails that exact file via TranscriptReader's file: param, and uses a per-session MessageBus channel so controllers don't cross-talk. The primary's id is deterministic from its session name (stable → it resumes across restarts, like /resume off the same history file); secondaries get a fresh random id so a clean session is always available. The reader now waits for the bound file to appear rather than throwing. Migration: an existing tmux session created before this (no --session-id, claude chose its own id) must be killed once (claude.kill-all-sessions) so the next spawn binds the controlled id. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,4 +52,28 @@ void main() {
|
||||
expect(primarySessionName(long), primarySessionName(long));
|
||||
});
|
||||
});
|
||||
|
||||
group('claude session ids (T-146)', () {
|
||||
final uuidRe = RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
||||
|
||||
test('primary id is a valid v4-format UUID', () {
|
||||
expect(primarySessionId('/home/me/clide'), matches(uuidRe));
|
||||
});
|
||||
|
||||
test('primary id is deterministic per repo (resumes across restarts)', () {
|
||||
expect(primarySessionId('/home/me/clide'), primarySessionId('/home/me/clide'));
|
||||
});
|
||||
|
||||
test('different repos get different primary ids', () {
|
||||
expect(primarySessionId('/home/me/clide'), isNot(primarySessionId('/home/me/other')));
|
||||
});
|
||||
|
||||
test('fresh ids are valid UUIDs and unique per call (clean secondaries)', () {
|
||||
final a = freshSessionId();
|
||||
final b = freshSessionId();
|
||||
expect(a, matches(uuidRe));
|
||||
expect(b, matches(uuidRe));
|
||||
expect(a, isNot(b));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -743,6 +743,35 @@ void main() {
|
||||
|
||||
expect(collected.whereType<AssistantTextMessage>().single.text, 'from the explicit file');
|
||||
});
|
||||
|
||||
test('explicit file: waits without error until the file appears', () async {
|
||||
// claude writes <session-id>.jsonl shortly after spawn (T-146); the
|
||||
// reader must poll without throwing until it exists, then stream it.
|
||||
final dir = await Directory.systemTemp.createTemp('late_file_');
|
||||
addTearDown(() => dir.delete(recursive: true));
|
||||
final target = File('${dir.path}/agent-late.jsonl');
|
||||
|
||||
final reader = TranscriptReader(
|
||||
'/unused',
|
||||
projectsBase: '/nonexistent',
|
||||
pollInterval: const Duration(milliseconds: 20),
|
||||
file: target.path,
|
||||
);
|
||||
final collected = <ConversationItem>[];
|
||||
final sub = reader.stream.listen(collected.add);
|
||||
|
||||
// Let it poll a few times against the missing file — must not throw.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 80));
|
||||
expect(collected, isEmpty);
|
||||
|
||||
// Now the file appears.
|
||||
writeLines(target, [assistantText('a1', 'arrived late')]);
|
||||
await pumpUntil(() => collected.isNotEmpty);
|
||||
|
||||
await sub.cancel();
|
||||
await reader.dispose();
|
||||
expect(collected.whereType<AssistantTextMessage>().single.text, 'arrived late');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user