@@ -362,15 +362,18 @@ def _resolve_aligned_team_name(
362362 A typed outer tuple would LEAK the RuntimeError/TypeError above and break
363363 never-raises, which is why the outer guard is a bare ``except Exception``.
364364
365- NOTE — ``session_id`` is NOT a raise source here. It is used ONLY as a
366- string compared against ``config.json['leadSessionId']`` (and an empty
367- check); it is NEVER composed into a ``Path``. So a path-unsafe raw
368- ``session_id`` (embedded ``/`` or NUL) does NOT raise in this function —
369- it simply never equals any stored ``leadSessionId`` -> NO MATCH ->
370- ``default``. The path-safety gate is applied instead to the matched DIR
371- NAME (``is_safe_path_component`` below), which IS used as a path segment.
372- The bare-except precedents in this module are ``persist_context`` and
373- ``heal_context_if_missing``.
365+ NOTE — ``session_id`` is NOT an uncaught raise source here. In the
366+ identity-match loop it is used ONLY as a string compared against
367+ ``config.json['leadSessionId']`` (and an empty check). In the branch-2
368+ fallthrough below it IS composed into a ``Path`` (``teams_root /
369+ session_id``), but ONLY after ``is_safe_path_component(session_id)`` gates
370+ it as the FIRST conjunct — so a path-unsafe raw ``session_id`` (embedded
371+ ``/`` or NUL) is rejected by that gate and falls through to ``default``,
372+ never reaching the composition. The subsequent ``is_dir()`` / ``exists()``
373+ probes raise at most an ``OSError``-family error, which the outer bare
374+ ``except`` catches. The path-safety gate also guards the matched DIR NAME
375+ in the loop (``is_safe_path_component`` there). The bare-except precedents
376+ in this module are ``persist_context`` and ``heal_context_if_missing``.
374377
375378 PERF (SessionStart hot-path scan cost): on a MATCH the scan stops at the
376379 first matching dir; on NO MATCH it iterates EVERY team dir under
@@ -427,15 +430,40 @@ def _resolve_aligned_team_name(
427430 # This dir is unreadable / malformed — skip it, keep scanning
428431 # the rest. A single bad sibling must not abort detection.
429432 continue
433+ # Branch-2: config-less full-UUID divergence (Desktop child / older-CLI
434+ # / print). The identity-match loop above missed because no team dir
435+ # carries a config.json with this leadSessionId — but the platform may
436+ # still have created teams/<session_id>/ (full UUID) with inboxes/ and
437+ # no config.json. Anchor on the harness-invariant session_id directly:
438+ # if a real own-session substrate exists, resolve to it. is_safe_path_
439+ # component(session_id) is the FIRST conjunct (guard-order — short-
440+ # circuit before any FS probe; a path-unsafe session_id never reaches a
441+ # Path composition). The inboxes/ | file-edits.json witness proves the
442+ # platform built a genuine team substrate for this session (not a bare
443+ # dir). Unreachable under new-CLI: the platform names the dir
444+ # session-<id8>, so teams/<full-uuid>/ never exists (steady-state AND
445+ # the ~38s cold-start) -> CLI byte-identical.
446+ if (
447+ is_safe_path_component (session_id )
448+ and (teams_root / session_id ).is_dir ()
449+ and (
450+ (teams_root / session_id / "inboxes" ).is_dir ()
451+ or (teams_root / session_id / "file-edits.json" ).exists ()
452+ )
453+ ):
454+ return session_id
430455 return fallback
431456 except Exception :
432457 # TOTAL fail-safe: home-resolution RuntimeError (get_claude_config_dir
433458 # -> Path.home, the teams_dir=None branch), a non-str teams_dir
434459 # TypeError (Path(teams_dir)), or any other unexpected error -> the
435- # persisted/computed default. (session_id is NOT a raise source here —
436- # it is only string-compared to leadSessionId, never composed into a
437- # Path; see the NOTE in the docstring above.) NEVER raises —
438- # get_team_name and the heal path depend on this contract.
460+ # persisted/computed default. (In the identity-match loop session_id is
461+ # only string-compared to leadSessionId; the branch-2 fallthrough DOES
462+ # compose teams_root / session_id, but only AFTER is_safe_path_component
463+ # gates it, and the is_dir()/exists() probes raise at most an
464+ # OSError-family error that THIS except catches — so session_id is still
465+ # not an uncaught raise source; see the NOTE in the docstring above.)
466+ # NEVER raises — get_team_name and the heal path depend on this contract.
439467 if default is not None :
440468 return default
441469 try :
@@ -479,12 +507,30 @@ def get_team_name() -> str:
479507 return _aligned_cache
480508 # Read the persisted SSOT first. An empty value is the fail-closed signal
481509 # (see the security-gate note above) — short-circuit BEFORE identity-match.
510+ #
511+ # LATENT COUPLING (the config-less fix depends on this): the
512+ # _resolve_aligned_team_name BRANCH-2 config-less fallback (session-id-
513+ # anchored teams/<uuid>/ resolution) is reached ONLY through the non-empty
514+ # path below. So branch-2's config-less reachability DEPENDS on the
515+ # persisted team_name being non-empty here. That holds today because
516+ # session_init persists a non-empty computed default (generate_team_name ->
517+ # session-<id8>, threaded as the resolver default at session_init main()).
518+ # If a future change ever persisted an EMPTY team_name, this short-circuit
519+ # would return '' BEFORE branch-2 ran and the config-less Desktop/SDK fix
520+ # would SILENTLY stop firing (the deadlock would return) — with no error,
521+ # because '' is the legitimate fail-closed "team unknown -> refuse" signal.
522+ # Do NOT "fix" that by recovering a team from an empty SSOT here: that would
523+ # break the deliberate fail-closed gate (test_empty_ssot_team_fails_closed_
524+ # both_modes). The correct invariant to preserve is upstream: keep the
525+ # persisted SSOT non-empty for a real session.
482526 ctx_team = get_pact_context ().get ("team_name" , "" )
483527 if not ctx_team :
484528 _aligned_cache = ""
485529 return _aligned_cache
486530 # Non-empty SSOT: identity-match can UPGRADE it to the real platform dir
487- # (or no-op back to ctx_team on a cold-start / no-match).
531+ # (or no-op back to ctx_team on a cold-start / no-match). This is also the
532+ # ONLY path that reaches the branch-2 config-less fallback (see the LATENT
533+ # COUPLING note above).
488534 resolved = _resolve_aligned_team_name (get_session_id (), default = ctx_team )
489535 _aligned_cache = resolved .lower ()
490536 return _aligned_cache
0 commit comments