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
@@ -108,10 +108,11 @@ class ManagedSession {
final String id;
final String role;
/// The clide-internal session id. For fork sessions this is the placeholder
/// UUID passed via [SpawnSpec.sessionId]; the real claude-assigned session id
/// arrives in the `init` event and is not yet captured here (T-172 follow-up).
final String sessionId;
/// The claude session id. Starts as [SpawnSpec.sessionId] (the `--session-id`
/// we passed, or a placeholder for a `--fork-session` branch) and is updated
/// to the real claude-assigned id once the session's `init` event resolves it
/// (T-185) — so a fork exposes the branch's actual id, not the placeholder.
String sessionId;
/// The working directory this session was spawned in. Retained so forks and
/// the UI can reference the source context (T-172).
@@ -226,6 +227,15 @@ class ClaudeSessionOrchestrator extends ChangeNotifier {
forkSourceSessionId: spec.forkSourceSessionId,
);
_sessions[spec.id] = managed;
// Fold the real claude-assigned session id back in once the init event
// resolves it (T-185) — matters for forks, whose sessionId starts as a
// placeholder. Idempotent for normal sessions (same id we passed).
session.sessionIdResolved.listen((id) {
if (managed.sessionId != id) {
managed.sessionId = id;
notifyListeners();
}
});
notifyListeners();
return managed;
}
@@ -203,8 +203,10 @@ class StreamJsonSession {
final List<McpServer> _mcpServers;
final _items = StreamController<ConversationItem>.broadcast();
final _statusCtl = StreamController<SessionStatus>.broadcast();
final _sessionIdCtl = StreamController<String>.broadcast();
StreamSubscription<String>? _sub;
SessionStatus _status = const SessionStatus();
String? _claudeSessionId;
int _localSeq = 0;
/// Token-by-token streaming state (T-168, wire shape verified by T-184).
@@ -272,6 +274,16 @@ class StreamJsonSession {
/// of the user's own messages).
Stream<ConversationItem> get items => _items.stream;
/// The claude-assigned session id, resolved from the first event that carries
/// `session_id` (the `init` event). For a session started with `--session-id`
/// this equals the id we passed; for a `--fork-session` branch (T-185) it is
/// the NEW id claude minted, which the orchestrator folds back into the
/// [ManagedSession]. Null until the init event arrives.
String? get claudeSessionId => _claudeSessionId;
/// Fires once with the resolved [claudeSessionId] when the init event lands.
Stream<String> get sessionIdResolved => _sessionIdCtl.stream;
/// Session status (model / permission-mode / context tokens), on change.
Stream<SessionStatus> get statusStream => _statusCtl.stream;
@@ -306,6 +318,16 @@ class StreamJsonSession {
} catch (_) {
return;
}
// Capture the claude-assigned session id from the first event that carries
// it (the init event). For a --fork-session branch this is the NEW id, which
// the orchestrator folds back into the ManagedSession (T-185).
if (_claudeSessionId == null) {
final sid = ev['session_id'];
if (sid is String && sid.isNotEmpty) {
_claudeSessionId = sid;
_sessionIdCtl.add(sid);
}
}
// Control-channel requests (permission asks, AskUserQuestion) must be
// routed out of the normal event stream and answered (D-78).
if (ev['type'] == 'control_request') {
@@ -652,6 +674,7 @@ class StreamJsonSession {
await _proc.kill();
await _items.close();
await _statusCtl.close();
await _sessionIdCtl.close();
await _pendingCtl.close();
await _busyCtl.close();
}