capture a forked session's real id from its init event

A --fork-session branch is spawned without --session-id, so claude mints
a new session id that only arrives in the init event; the ManagedSession
was left holding its placeholder. StreamJsonSession now captures
session_id from the first event that carries it and exposes it via
claudeSessionId / sessionIdResolved; the orchestrator folds that back into
ManagedSession.sessionId (idempotent for normal sessions). A fork can now
itself be resumed or forked.

T-185.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-31 12:21:56 +02:00
co-authored by Claude
parent d0ad8ed4aa
commit e866da9fd8
7 changed files with 79 additions and 4 deletions
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:clide/builtin/claude/src/session_orchestrator.dart';
@@ -12,6 +13,7 @@ class _FakeProc implements StreamJsonProcess {
bool killed = false;
@override
Stream<String> get lines => _ctl.stream;
void emit(String line) => _ctl.add(line);
@override
void writeLine(String line) => writes.add(line);
@override
@@ -41,6 +43,26 @@ void main() {
expect(orch.byId('teammate:tyre')!.role, 'teammate:tyre');
});
test('folds the real claude session id from the init event into the session (T-185)', () async {
final m = await orch.spawn(SpawnSpec(
id: 'fork-x',
role: 'teammate',
sessionId: 'placeholder-uuid',
cwd: '/repo',
forkSourceSessionId: 'source-uuid',
));
expect(m.sessionId, 'placeholder-uuid'); // starts as the placeholder
created.last.emit(jsonEncode({
'type': 'system',
'subtype': 'init',
'session_id': 'real-fork-id',
'model': 'claude-opus-4-8',
'permissionMode': 'default',
}));
await Future<void>.delayed(Duration.zero);
expect(m.sessionId, 'real-fork-id'); // updated to the branch's real id
});
test('spawn is idempotent on id — no second process', () async {
final a = await orch.spawn(spec('primary'));
final b = await orch.spawn(spec('primary'));
@@ -206,6 +206,21 @@ void main() {
expect(statuses, hasLength(1));
});
test('captures the claude session id from the first event carrying it (T-185)', () async {
final ids = <String>[];
session.sessionIdResolved.listen(ids.add);
proc.emit(jsonEncode({
'type': 'system',
'subtype': 'init',
'session_id': 'sess-abc',
'model': 'claude-opus-4-7',
'permissionMode': 'default',
}));
await Future<void>.delayed(Duration.zero);
expect(session.claudeSessionId, 'sess-abc');
expect(ids, ['sess-abc']);
});
group('live cost/context from result events (T-168)', () {
test('result event with total_cost_usd populates cost field', () async {
proc.emit(resultEvent(cost: 0.042));