Repository navigation
Expand file tree
/
Copy pathshpool.bashrc
More file actions
1684 lines (1649 loc) · 84.2 KB
/
Copy pathshpool.bashrc
File metadata and controls
1684 lines (1649 loc) · 84.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# shellcheck shell=bash
# ---- session-kit shpool integration -----------------------------------------
# Source this block from ~/.bashrc. Installed releases keep this file immutable.
if [[ ${__SESSION_KIT_SHPOOL_BASHRC_LOADED:-0} == 1 ]]; then
return 0
fi
__SESSION_KIT_SHPOOL_BASHRC_LOADED=1
export PATH="$HOME/.cargo/bin:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
# Keep AI TUI output in normal terminal history. The session journal is the
# reconnect source; provider-native transcripts remain the conversation source.
export CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1
__sk_state_root=${XDG_STATE_HOME:-"$HOME/.local/state"}
__sk_kit_state=${SESSION_KIT_STATE_DIR:-"$__sk_state_root/session-kit"}
__sk_journal_root=${SESSION_KIT_JOURNAL_DIR:-"$__sk_state_root/shpool-journal"}
# A disabled journal is still a launch-ready shell. Mark it explicitly so the
# one-shot provider record below is consumed without starting `script`.
if [[ -n ${SHPOOL_SESSION_NAME:-} && -z ${SHPOOL_JOURNAL:-} && $- == *i* \
&& -e $HOME/.no_shpool_journal ]]; then
export SHPOOL_JOURNAL=disabled
fi
# New sessions write one append-only segment for their entire live lifetime.
# Active segments are never replaced or trimmed. Existing legacy sessions keep
# their already-open writer and are handled by the recovery map.
if [[ -n ${SHPOOL_SESSION_NAME:-} && -z ${SHPOOL_JOURNAL:-} && $- == *i* && -t 1 \
&& ! -e $HOME/.no_shpool_journal ]]; then
__sk_session_journal="$__sk_journal_root/$SHPOOL_SESSION_NAME"
__sk_journal_ready=1
mkdir -p "$__sk_session_journal" || __sk_journal_ready=0
if (( __sk_journal_ready )); then
chmod 700 "$__sk_session_journal" || __sk_journal_ready=0
fi
if (( __sk_journal_ready )); then
export SHPOOL_JOURNAL="$__sk_session_journal/segment-000001.raw"
( umask 077; : >> "$SHPOOL_JOURNAL" ) || __sk_journal_ready=0
chmod 600 "$SHPOOL_JOURNAL" || __sk_journal_ready=0
fi
if (( ! __sk_journal_ready )); then
echo "[session-kit: journal unavailable; continuing without capture]" >&2
export SHPOOL_JOURNAL=disabled
exec bash -i
fi
if [[ $(uname -s 2>/dev/null) == Darwin ]]; then
script -q -F -a "$SHPOOL_JOURNAL" bash -i
else
script -qfa "$SHPOOL_JOURNAL" -c "bash -i"
fi
__sk_script_rc=$?
if (( __sk_script_rc != 0 )); then
echo "[session-kit: journal process failed; continuing without capture]" >&2
export SHPOOL_JOURNAL=disabled
exec bash -i
fi
unset __sk_script_rc __sk_journal_ready
builtin exit
fi
if [[ -n ${SHPOOL_SESSION_NAME:-} && $- == *i* ]]; then
# A refusal that only reaches this session's screen is invisible the moment
# the session is closed. Three launches started nothing one night and the
# only way to know WHY was to attach before somebody closed them. Every
# decision this block makes now lands on the action log, named by session.
__sk_log_launch() {
local __sk_log_core=${SESSION_KIT_INVENTORY_CORE:-${__sk_inventory_core:-}}
if [[ -z $__sk_log_core ]]; then
# The earliest refusals happen before the record block resolves the
# release, and those are exactly the ones nobody could diagnose. Derive
# it the same way that block does, from this file's own location.
local __sk_log_root
__sk_log_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd -P) ||
__sk_log_root=""
[[ -z $__sk_log_root ]] || __sk_log_core=$__sk_log_root/lib/session_inventory.py
fi
[[ -n $__sk_log_core && -f $__sk_log_core ]] || return 0
python3 "$__sk_log_core" action-log launch "$1" \
--session "$SHPOOL_SESSION_NAME" >/dev/null 2>&1 || true
}
__sk_start_dir=${SESSION_KIT_START_DIR:-"$__sk_state_root/shpool-start"}
__sk_start="$__sk_start_dir/$SHPOOL_SESSION_NAME"
__sk_expected="$__sk_start.expected"
__sk_launch="$__sk_start.launch"
__sk_account="$__sk_start.account"
# The shell can start before `sp` has WRITTEN the main record at all, the
# session exists the moment shpool spawns it, and on a loaded box the
# launcher's first write can land a second later. A one-shot existence
# check here made that launch permanently silent. The wait is bounded so a
# session with no launcher (a bare shpool attach) costs three seconds once.
if [[ -n ${SHPOOL_JOURNAL:-} && ! -r $__sk_start ]]; then
for __sk_arm_attempt in {1..30}; do
[[ -r $__sk_start ]] && break
sleep 0.1
done
fi
if [[ -n ${SHPOOL_JOURNAL:-} && -r $__sk_start ]]; then
# The shell can start before `sp` has captured its exact generation. Wait
# only for an atomically written sidecar; an unarmed or stale main record
# never launches a provider.
# 3 seconds covers a healthy launch; a loaded box can take longer to arm
# (a lost-by-milliseconds launch once left a relaunch hanging on a
# plain shell). At the 3-second mark, a FRESH main record proves a launch
# is still in progress, and only that case earns the longer wait, a
# stale record still costs later shells 3 seconds, not 30.
for __sk_arm_attempt in {1..300}; do
[[ -r $__sk_expected ]] && break
if (( __sk_arm_attempt == 30 )); then
__sk_arm_started=$(stat -c %Y "$__sk_start" 2>/dev/null ||
stat -f %m "$__sk_start" 2>/dev/null || echo 0)
(( $(date +%s) - __sk_arm_started < 60 )) || break
fi
sleep 0.1
done
unset __sk_arm_started
# An exhausted wait means sp died between writing the start record and
# arming it. Every later shell in this session would silently stall 3s
# here; say it once instead.
#
# Except for a shell session, where a plain shell IS what was asked for:
# `sp new shell` arms its record and then clears it once the shell is
# open, so a missing sidecar there is the ordinary end of a healthy
# launch. Warning about it called a working session broken.
if [[ ! -r $__sk_expected ]]; then
__sk_start_provider=
if [[ -r $__sk_start ]]; then
IFS=$'\t' read -r __sk_start_provider __sk_start_rest < "$__sk_start" ||
__sk_start_provider=
fi
if [[ -r $__sk_start && $__sk_start_provider != shell ]]; then
echo "[session-kit: launch record incomplete; starting a plain shell]"
__sk_log_launch refused_record_incomplete
fi
unset __sk_start_provider __sk_start_rest
fi
fi
if [[ -n ${SHPOOL_JOURNAL:-} && -r $__sk_start && -r $__sk_expected ]]; then
__sk_provider= __sk_cwd= __sk_uuid= __sk_launch_mode=
__sk_side_provider= __sk_side_cwd= __sk_side_uuid= __sk_side_launch_mode=
__sk_boot_id= __sk_started= __sk_shell_pid= __sk_shell_start=
__sk_daemon_pid= __sk_daemon_start=
__sk_requested_model= __sk_launch_key=
__sk_launch_provider= __sk_launch_cwd= __sk_launch_boot=
__sk_launch_started= __sk_launch_shell_pid= __sk_launch_shell_start=
__sk_launch_daemon_pid= __sk_launch_daemon_start=
__sk_launch_record_ok=1
__sk_record_shape_ok=0
if python3 - "$__sk_start" "$__sk_expected" <<'PY'
import sys
expected_tabs = ({2, 3}, {8, 9})
for path, allowed in zip(sys.argv[1:], expected_tabs):
with open(path, "rb") as handle:
payload = handle.read(8193)
if (
not payload
or len(payload) > 8192
or not payload.endswith(b"\n")
or payload.count(b"\n") != 1
or b"\r" in payload
or b"\x1c" in payload
or payload[:-1].count(b"\t") not in allowed
):
raise SystemExit(1)
PY
then
IFS= read -r __sk_start_line < "$__sk_start"
IFS= read -r __sk_expected_line < "$__sk_expected"
__sk_start_line=${__sk_start_line//$'\t'/$'\034'}
__sk_expected_line=${__sk_expected_line//$'\t'/$'\034'}
IFS=$'\034' read -r __sk_provider __sk_cwd __sk_uuid __sk_launch_mode <<<"$__sk_start_line"
IFS=$'\034' read -r __sk_side_provider __sk_side_cwd \
__sk_boot_id __sk_started __sk_shell_pid __sk_shell_start \
__sk_daemon_pid __sk_daemon_start __sk_side_uuid __sk_side_launch_mode <<<"$__sk_expected_line"
__sk_record_shape_ok=1
fi
if [[ -z $__sk_launch_mode ]]; then
if [[ -n $__sk_uuid ]]; then __sk_launch_mode=resume; else __sk_launch_mode=new; fi
fi
if [[ -z $__sk_side_launch_mode ]]; then
if [[ -n $__sk_side_uuid ]]; then __sk_side_launch_mode=resume; else __sk_side_launch_mode=new; fi
fi
# Session shells are spawned by the shpool daemon and carry NO kit
# environment, so the release must be derived from this file's own
# location (resolving the `current` link pins the release active at
# session start). The env overrides remain for tests and tooling.
__sk_release_root=${SESSION_KIT_RELEASE_DIR:-}
if [[ -z $__sk_release_root ]]; then
__sk_release_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd -P) ||
__sk_release_root=""
fi
__sk_inventory_core=${SESSION_KIT_INVENTORY_CORE:-$__sk_release_root/lib/session_inventory.py}
unset __sk_release_root
if [[ -e $__sk_launch || -L $__sk_launch ]]; then
__sk_launch_record_ok=0
if [[ -f $__sk_launch && ! -L $__sk_launch ]] &&
python3 - "$__sk_launch" <<'PY'
import os
import stat
import sys
path = sys.argv[1]
info = os.lstat(path)
with open(path, "rb") as stream:
payload = stream.read(8193)
if (
not stat.S_ISREG(info.st_mode)
or info.st_uid != os.geteuid()
or stat.S_IMODE(info.st_mode) != 0o600
or not payload
or len(payload) > 8192
or not payload.endswith(b"\n")
or payload.count(b"\n") != 1
or payload[:-1].count(b"\t") != 9
or b"\r" in payload
or b"\x1c" in payload
):
raise SystemExit(1)
PY
then
# TAB IS IFS WHITESPACE, so `IFS=$'\t' read` collapses a run of
# tabs and drops the empty fields between them. This record's
# fourth field is the launch key, and it is EMPTY for every launch
# nobody passed --launch-key to. The six generation fields then
# shift one place left: the boot id lands in the key, the daemon
# start lands nowhere, and the cross-check below refuses a record
# that was armed perfectly. Every `sp new --model` without a key
# ended as a shell with no provider because of this line.
# The records read above avoid it by switching to a
# non-whitespace delimiter first; this one does the same.
IFS= read -r __sk_launch_line < "$__sk_launch"
__sk_launch_line=${__sk_launch_line//$'\t'/$'\034'}
IFS=$'\034' read -r __sk_launch_provider __sk_launch_cwd \
__sk_requested_model __sk_launch_key __sk_launch_boot \
__sk_launch_started __sk_launch_shell_pid __sk_launch_shell_start \
__sk_launch_daemon_pid __sk_launch_daemon_start <<<"$__sk_launch_line"
__sk_validated_model=$(python3 "$__sk_inventory_core" validate-worker-model \
"$__sk_launch_provider" "$__sk_requested_model" 2>/dev/null || true)
if [[ $__sk_validated_model == "$__sk_requested_model" &&
( -z $__sk_launch_key || $__sk_launch_key =~ ^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$ ) ]]; then
__sk_launch_record_ok=1
fi
unset __sk_validated_model
fi
fi
if [[ -n ${SESSION_KIT_BOOT_ID_FILE:-} ]]; then
__sk_current_boot_id=$(command cat -- "$SESSION_KIT_BOOT_ID_FILE" 2>/dev/null || true)
elif [[ $(uname -s 2>/dev/null) == Darwin && -f $__sk_inventory_core ]]; then
__sk_current_boot_id=$(python3 "$__sk_inventory_core" platform boot-id 2>/dev/null || true)
else
__sk_current_boot_id=$(command cat -- /proc/sys/kernel/random/boot_id 2>/dev/null || true)
fi
__sk_current_boot_id=${__sk_current_boot_id//$'\r'/}
__sk_current_boot_id=${__sk_current_boot_id//$'\n'/}
__sk_generation_ok=0
__sk_proc_identity() {
local __sk_pid=$1 __sk_stat __sk_tail
local -a __sk_fields
if [[ $(uname -s 2>/dev/null) == Darwin ]]; then
[[ -f $__sk_inventory_core ]] || return 1
python3 "$__sk_inventory_core" platform process-info "$__sk_pid" 2>/dev/null |
command tr '\t' ' '
return
fi
[[ -r /proc/$__sk_pid/stat ]] || return 1
__sk_stat=$(<"/proc/$__sk_pid/stat") || return 1
__sk_tail=${__sk_stat##*) }
read -r -a __sk_fields <<<"$__sk_tail"
[[ ${#__sk_fields[@]} -ge 20 ]] || return 1
printf '%s %s\n' "${__sk_fields[1]}" "${__sk_fields[19]}"
}
__sk_launch_mode_ok=0
if [[ $__sk_launch_mode == new && -z $__sk_uuid ]]; then
__sk_launch_mode_ok=1
elif [[ ( $__sk_launch_mode == resume || $__sk_launch_mode == fork ) &&
$__sk_provider =~ ^(claude|codex)$ &&
$__sk_uuid =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]]; then
__sk_launch_mode_ok=1
fi
if (( __sk_record_shape_ok && __sk_launch_mode_ok && __sk_launch_record_ok )) &&
[[ $__sk_provider == "$__sk_side_provider" &&
$__sk_cwd == "$__sk_side_cwd" &&
$__sk_uuid == "$__sk_side_uuid" &&
$__sk_launch_mode == "$__sk_side_launch_mode" &&
-n $__sk_boot_id && $__sk_boot_id == "$__sk_current_boot_id" &&
$__sk_started =~ ^[0-9]+$ &&
$__sk_shell_pid =~ ^[0-9]+$ && $__sk_shell_start =~ ^[0-9]+$ &&
$__sk_daemon_pid =~ ^[0-9]+$ && $__sk_daemon_start =~ ^[0-9]+$ ]]; then
if [[ -n $__sk_requested_model ]] &&
[[ $__sk_launch_provider != "$__sk_provider" ||
$__sk_launch_cwd != "$__sk_cwd" ||
$__sk_launch_boot != "$__sk_boot_id" ||
$__sk_launch_started != "$__sk_started" ||
$__sk_launch_shell_pid != "$__sk_shell_pid" ||
$__sk_launch_shell_start != "$__sk_shell_start" ||
$__sk_launch_daemon_pid != "$__sk_daemon_pid" ||
$__sk_launch_daemon_start != "$__sk_daemon_start" ]]; then
__sk_launch_record_ok=0
fi
__sk_walk_pid=$$
for __sk_walk_depth in {1..6}; do
read -r __sk_walk_ppid __sk_walk_start < <(__sk_proc_identity "$__sk_walk_pid") || break
if [[ $__sk_walk_pid == "$__sk_shell_pid" && $__sk_walk_start == "$__sk_shell_start" ]]; then
read -r __sk_parent_ppid __sk_parent_start < <(__sk_proc_identity "$__sk_walk_ppid") || break
if [[ $__sk_walk_ppid == "$__sk_daemon_pid" && $__sk_parent_start == "$__sk_daemon_start" ]]; then
__sk_generation_ok=1
fi
break
fi
__sk_walk_pid=$__sk_walk_ppid
done
fi
(( __sk_launch_record_ok )) || __sk_generation_ok=0
if (( __sk_generation_ok )); then
unset SESSION_KIT_ACCOUNT_ALIAS
export SESSION_KIT_ACCOUNT_CAPABLE=0
if [[ -e $__sk_account || -L $__sk_account ]]; then
__sk_account_ok=0
__sk_account_provider= __sk_account_alias= __sk_account_profile=
if [[ -f $__sk_account && ! -L $__sk_account ]] &&
python3 - "$__sk_account" <<'PY'
import os
import stat
import sys
path = sys.argv[1]
info = os.lstat(path)
with open(path, "rb") as stream:
payload = stream.read(257)
if (
not stat.S_ISREG(info.st_mode)
or info.st_uid != os.geteuid()
or stat.S_IMODE(info.st_mode) != 0o600
or not payload.endswith(b"\n")
or payload.count(b"\n") != 1
or payload[:-1].count(b"\t") != 1
or b"\r" in payload
or b"\x1c" in payload
):
raise SystemExit(1)
PY
then
IFS=$'\t' read -r __sk_account_provider __sk_account_alias < "$__sk_account"
if [[ $__sk_account_provider == "$__sk_provider" &&
$__sk_account_alias =~ ^[a-z][a-z0-9_-]{0,11}$ ]]; then
__sk_account_json=$(python3 "$__sk_inventory_core" account resume-profile \
"$__sk_account_provider" "$__sk_account_alias" 2>/dev/null || true)
__sk_account_profile=$(python3 -c '
import json,sys
try:
value=json.loads(sys.stdin.read())
except ValueError:
raise SystemExit(1)
profile=value.get("profile_dir")
if value.get("provider") != sys.argv[1] or value.get("alias") != sys.argv[2]:
raise SystemExit(1)
if not isinstance(profile,str) or not profile.startswith("/") or "\n" in profile or "\r" in profile:
raise SystemExit(1)
print(profile)
' "$__sk_provider" "$__sk_account_alias" <<<"$__sk_account_json" 2>/dev/null || true)
if [[ $__sk_account_profile == /* ]]; then
export SESSION_KIT_ACCOUNT_ALIAS=$__sk_account_alias
export SESSION_KIT_ACCOUNT_CAPABLE=1
if [[ $__sk_provider == claude ]]; then
export CLAUDE_CONFIG_DIR=$__sk_account_profile
else
export CODEX_HOME=$__sk_account_profile
export SESSION_KIT_CODEX_HOME=$__sk_account_profile
fi
__sk_account_ok=1
fi
fi
fi
if (( ! __sk_account_ok )); then
echo "[session-kit: selected account profile is unsafe or no longer matches its login; provider not started]" >&2
__sk_log_launch refused_account_unsafe
__sk_generation_ok=0
fi
unset __sk_account_json
fi
fi
if (( ! __sk_generation_ok )); then
echo "[session-kit: stale or mismatched launch record retained; provider not started]" >&2
__sk_log_launch refused_generation_mismatch
elif [[ $__sk_cwd == /* && -d $__sk_cwd ]]; then
if ! cd -- "$__sk_cwd"; then
echo "[session-kit: launch directory is unavailable; launch record retained for retry]" >&2
__sk_log_launch refused_directory_unavailable
else
__sk_provider_launched=0
__sk_provider_exited=0
__sk_provider_rc=0
__sk_lifecycle_uuid=
__sk_lifecycle_intake=
__sk_lifecycle_generation=
__sk_model_args=()
__sk_launch_model=$__sk_requested_model
__sk_model_carried=0
# An account switch relaunches this conversation by re-entering here
# through `exec bash -i`, and the launch record that carried the model
# was consumed at first launch. Without this the provider is started
# with no --model at all and chooses its own: the model changing by
# itself, which is the one thing the model rule says never happens
# quietly. SESSION_KIT_CARRIED_MODEL is set by that switch and nowhere
# else -- it comes from the same process that launched the provider,
# not from a file anything else can write -- and it is still put
# through the provider's validator before it is used.
if [[ -n ${SESSION_KIT_CARRIED_MODEL:-} ]]; then
if [[ -z $__sk_launch_model ]]; then
__sk_carried_model=$(python3 "$__sk_inventory_core" validate-worker-model \
"$__sk_provider" "$SESSION_KIT_CARRIED_MODEL" 2>/dev/null || true)
if [[ -n $__sk_carried_model &&
$__sk_carried_model == "$SESSION_KIT_CARRIED_MODEL" ]]; then
__sk_launch_model=$__sk_carried_model
__sk_model_carried=1
else
# Said before the session starts, never discovered afterwards.
echo "[session-kit: this session was started on ${SESSION_KIT_CARRIED_MODEL}, and that could not be carried across the restart; it comes back on ${__sk_provider}'s own default. Use sp change-model to put it back.]" >&2
fi
unset __sk_carried_model
fi
unset SESSION_KIT_CARRIED_MODEL
fi
if [[ -n $__sk_launch_model ]]; then
__sk_model_args=(--model "$__sk_launch_model")
export SESSION_KIT_REQUESTED_MODEL=$__sk_launch_model
if (( __sk_model_carried )); then
# A carried relaunch is a new launch and has no key of its own; the
# old one belongs to a launch that already happened.
unset SESSION_KIT_LAUNCH_IDEMPOTENCY_KEY
else
export SESSION_KIT_LAUNCH_IDEMPOTENCY_KEY=$__sk_launch_key
fi
else
unset SESSION_KIT_REQUESTED_MODEL SESSION_KIT_LAUNCH_IDEMPOTENCY_KEY
fi
unset __sk_model_carried
case "$__sk_provider" in
claude)
if ! command -v claude >/dev/null 2>&1; then
echo "[session-kit: Claude is unavailable; launch record retained for retry]" >&2
__sk_log_launch refused_provider_missing
elif [[ $__sk_launch_mode != new ]] || \
__sk_new_claude_uuid=$(python3 -c 'import uuid; print(uuid.uuid4())'); then
__sk_consumed_records=("$__sk_start" "$__sk_expected")
[[ -z $__sk_requested_model ]] || __sk_consumed_records+=("$__sk_launch")
[[ ! -e $__sk_account ]] || __sk_consumed_records+=("$__sk_account")
command rm -- "${__sk_consumed_records[@]}"
unset __sk_consumed_records
__sk_provider_launched=1
__sk_log_launch started
if [[ $__sk_launch_mode == new ]]; then
__sk_lifecycle_uuid=$__sk_new_claude_uuid
else
__sk_lifecycle_uuid=$__sk_uuid
fi
# A Claude window renames only through its SessionStart/prompt
# hooks, so a session that boots before its name intent exists
# (fresh uuid, or a pre-baked resume) shows a stale title until
# the provider restarts. The marker lets the picker request one
# safe bounce once a name exists, same contract as Codex.
__sk_claude_intent_uuid=$__sk_uuid
[[ $__sk_launch_mode == new ]] && __sk_claude_intent_uuid=$__sk_new_claude_uuid
if [[ -n $__sk_claude_intent_uuid && \
! -f "$HOME/.claude/sessions/$__sk_claude_intent_uuid.nameintent" ]]; then
( umask 077
mkdir -p "$__sk_state_root/session-kit/provider-untitled" &&
printf '%s\n' \
"$__sk_current_boot_id:$__sk_shell_pid:$__sk_shell_start:$RANDOM:$EPOCHREALTIME" \
> "$__sk_state_root/session-kit/provider-untitled/$SHPOOL_SESSION_NAME"
) 2>/dev/null || true
fi
unset __sk_claude_intent_uuid
# A relaunch through `exec bash -i` re-enters here with the
# previous launch's exports intact; a stale project-dir name from
# another directory or profile must never survive into this one.
unset CLAUDE_CODE_PROJECT_DIR_NAME
__sk_claude_cmd=claude
if [[ ${SESSION_KIT_PROJECT_DIR_NAME:-} != off && -n ${CLAUDE_CONFIG_DIR:-} ]]; then
__sk_pdn_helper=${__sk_inventory_core%/*}/sessionkit_inventory/project_dir_name.py
if [[ -f $__sk_pdn_helper ]]; then
# The version proof must name the exact file this shell then
# executes. A bare `claude` can re-resolve differently, a
# stale command hash, a launcher symlink retargeted after the
# proof, and an older executable silently ignores the export
# after the directory has already been renamed (review lane
# rv-pdn-1, 2026-08-17). One realpath is proved, and an armed
# export launches exactly that path.
hash -d claude 2>/dev/null
__sk_claude_real=$(command -v claude 2>/dev/null) &&
__sk_claude_real=$(python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' \
"$__sk_claude_real" 2>/dev/null) || __sk_claude_real=
if [[ -n $__sk_claude_real && -x $__sk_claude_real ]]; then
__sk_pdn=$(python3 "$__sk_pdn_helper" --profile "$CLAUDE_CONFIG_DIR" \
--cwd "$PWD" --claude-command "$__sk_claude_real" 2>/dev/null) || __sk_pdn=
# The helper already refused anything unprovable; the pattern
# guard here only keeps a mangled pipe from exporting garbage.
if [[ $__sk_pdn =~ ^[A-Za-z0-9_-]{1,64}$ ]]; then
export CLAUDE_CODE_PROJECT_DIR_NAME=$__sk_pdn
__sk_claude_cmd=$__sk_claude_real
fi
unset __sk_pdn
fi
unset __sk_claude_real
fi
unset __sk_pdn_helper
fi
__sk_mcp_args=()
__sk_resume_degraded=0
while :; do
# A resume of a conversation this profile does not hold can only
# fail. The pre-bake mints its throwaway inside one profile and
# this shell resolves its own; if they ever disagree, the launch
# records are already consumed, so the old code ran `--resume`,
# got "No conversation found", took that for a crash, reopened
# the SAME conversation once, crashed again, and left an open
# window with no provider in it at all. A conversation nothing
# can find is nothing to lose: start a fresh one instead, which
# still takes its colour from the kit at attach.
if [[ $__sk_launch_mode == resume && $__sk_resume_degraded == 0 ]] &&
! compgen -G "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects/*/$__sk_uuid.jsonl" >/dev/null 2>&1; then
__sk_resume_degraded=1
if __sk_new_claude_uuid=$(python3 -c 'import uuid; print(uuid.uuid4())'); then
__sk_launch_mode=new
__sk_lifecycle_uuid=$__sk_new_claude_uuid
__sk_log_launch degraded_resume_to_new
echo "[session-kit: the pre-started conversation was not in this profile; opening a new one instead]" >&2
else
echo "[session-kit: Claude session identity could not be allocated; resuming as asked]" >&2
fi
fi
case "$__sk_launch_mode" in
new) "$__sk_claude_cmd" "${__sk_model_args[@]}" --session-id "$__sk_new_claude_uuid" "${__sk_mcp_args[@]}"; __sk_provider_rc=$? ;;
resume) "$__sk_claude_cmd" "${__sk_model_args[@]}" --resume "$__sk_uuid" "${__sk_mcp_args[@]}"; __sk_provider_rc=$? ;;
fork) "$__sk_claude_cmd" "${__sk_model_args[@]}" --resume "$__sk_uuid" --fork-session "${__sk_mcp_args[@]}"; __sk_provider_rc=$? ;;
esac
# Same rule after the fact: a resume that exited nonzero without
# its conversation being anywhere this profile can see is not a
# crash to reopen, it is a launch to redo as a new session. Once
# only -- never a loop.
if [[ $__sk_launch_mode == resume && $__sk_provider_rc -ne 0 && $__sk_resume_degraded == 0 ]] &&
! compgen -G "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects/*/$__sk_uuid.jsonl" >/dev/null 2>&1 &&
__sk_new_claude_uuid=$(python3 -c 'import uuid; print(uuid.uuid4())'); then
__sk_resume_degraded=1
__sk_launch_mode=new
__sk_lifecycle_uuid=$__sk_new_claude_uuid
__sk_log_launch degraded_resume_to_new
echo "[session-kit: that conversation could not be resumed; opening a new one instead]" >&2
continue
fi
# A kit-requested bounce relaunches the SAME conversation once,
# so the fresh process boots through SessionStart with its name
# intent applied. Any other exit falls through to the normal
# provider-exit handling.
__sk_bounce="$__sk_state_root/session-kit/provider-bounce/$SHPOOL_SESSION_NAME"
if [[ -f $__sk_bounce && ! -L $__sk_bounce ]]; then
__sk_bounce_uuid=$(command head -c 64 -- "$__sk_bounce" 2>/dev/null | tr -cd '0-9a-fA-F-')
if [[ $__sk_bounce_uuid =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]]; then
__sk_uuid=$__sk_bounce_uuid
__sk_launch_mode=resume
# RENAMED, not removed. The marker is the only thing keeping
# this session in the person's list while the kit has its
# window shut, and there is no moment in the relaunch at
# which this shell can prove the replacement is up -- it
# blocks on the provider. So the marker stays as a receipt
# whose generated suffix identifies this exact request.
# Request and receipt names both read as bouncing, so the
# row stays visible either way; the rename is what
# stops this same block from bouncing forever, because the
# next read finds no marker under the first name.
#
# A generated receipt NAME rather than an emptied file or a
# reusable path binds settlement to one request generation.
# Collection captures the eligible names before it reads the
# process table, so an older sighting cannot name or delete a
# receipt installed after that read began.
#
# What ends the bounce is a positive sighting of the
# replacement window, which only collection can make. If the
# replacement never comes up, nothing clears it and the row
# stays visible, which is the correct way to be wrong.
__sk_bounce_generation=$(command sed -n '3p' -- "$__sk_bounce" 2>/dev/null | tr -cd 'A-Za-z0-9')
if [[ $__sk_bounce_generation =~ ^[A-Za-z0-9]{6,32}$ ]]; then
__sk_bounce_receipt="$__sk_bounce.taken.$__sk_bounce_generation"
else
# A request written before generated receipts shipped is
# still honoured. Its reusable legacy receipt is never
# settled by collection, so an already-live old shell can
# never have its protection removed by stale evidence.
__sk_bounce_receipt="$__sk_bounce.taken"
fi
command mv -f -- "$__sk_bounce" "$__sk_bounce_receipt" 2>/dev/null ||
{ : > "$__sk_bounce"; } 2>/dev/null || true
unset __sk_bounce_uuid __sk_bounce_generation __sk_bounce_receipt __sk_bounce
continue
fi
command rm -f -- "$__sk_bounce"
unset __sk_bounce_uuid
fi
# The provider loop is over, so a marker under either name can
# never be consumed or settled again. Left behind, it would keep
# this id reading as bouncing for as long as the state directory
# survives -- the sweep would get it eventually, but only after
# the grace, and only once the session is gone from the listing.
command rm -f -- "$__sk_bounce" "$__sk_bounce.taken"
for __sk_bounce_receipt in "$__sk_bounce".taken.*; do
[[ -e $__sk_bounce_receipt || -L $__sk_bounce_receipt ]] || continue
command rm -f -- "$__sk_bounce_receipt"
done
unset __sk_bounce_receipt
unset __sk_bounce
break
done
else
echo "[session-kit: Claude session identity could not be allocated; launch record retained for retry]" >&2
fi
unset __sk_new_claude_uuid __sk_mcp_args __sk_resume_degraded
;;
codex)
if ! command -v codex >/dev/null 2>&1; then
echo "[session-kit: Codex is unavailable; launch record retained for retry]" >&2
__sk_log_launch refused_provider_missing
else
# A managed session is launched with nobody watching it. Codex's
# startup upgrade prompt blocks on a keypress that never comes, so
# the session sits at "setup incomplete" forever and its
# conversation never loads. Suppressed explicitly here rather than
# relying on user config.
__sk_codex_no_update=(-c check_for_update_on_startup=false)
# A private per-repository coordination config can opt future Codex
# sessions into a local App Server plus an exact-thread broker.
# Existing direct sessions are never changed. Invalid, unsafe, or
# absent config fails back to the normal direct TUI launch.
__sk_coord_gate=
__sk_coord_broker=
__sk_coord_config="$HOME/.config/session-kit/coordination.json"
if [[ -f $__sk_coord_config && ! -L $__sk_coord_config ]]; then
__sk_coord_gate=$(python3 - "$__sk_coord_config" "$__sk_cwd" <<'PY'
import json
import os
import stat
import sys
config_path, launch_cwd = sys.argv[1:]
try:
metadata = os.lstat(config_path)
if (
not stat.S_ISREG(metadata.st_mode)
or metadata.st_uid != os.geteuid()
or metadata.st_mode & 0o022
or metadata.st_size > 8192
):
raise ValueError("unsafe config")
with open(config_path, encoding="utf-8") as stream:
config = json.load(stream)
repo_root = os.path.realpath(config["repo_root"])
broker = config["codex_broker"]
if (
config.get("codex_app_server") is not True
or not os.path.isabs(broker)
or not os.path.isfile(broker)
or os.path.islink(broker)
):
raise ValueError("inactive config")
broker_metadata = os.stat(broker)
if broker_metadata.st_uid != os.geteuid() or broker_metadata.st_mode & 0o022:
raise ValueError("unsafe broker")
# "codex_app_server_all" arms the App Server for every cwd so any managed
# Codex window is reachable. The exact-thread broker stays repo-scoped: a
# session outside the repository gets the server alone. Without the key the
# gate is repo-only, exactly as before.
if os.path.realpath(launch_cwd) != repo_root:
if config.get("codex_app_server_all") is not True:
raise ValueError("inactive config")
print("-")
else:
print(broker)
except (KeyError, OSError, TypeError, ValueError, json.JSONDecodeError):
pass
PY
)
fi
# An absolute path arms the server and its broker; "-" arms the
# server alone. Any other answer leaves the gate shut.
case $__sk_coord_gate in
/*) __sk_coord_broker=$__sk_coord_gate ;;
-) ;;
*) __sk_coord_gate= ;;
esac
__sk_codex_remote=()
__sk_app_server_pid=
__sk_broker_pid=
__sk_app_socket=
if [[ -n $__sk_coord_gate ]]; then
__sk_app_dir=$(python3 "$__sk_inventory_core" platform app-server-dir \
"$__sk_state_root" "$SHPOOL_SESSION_NAME" 2>/dev/null || true)
if [[ -z $__sk_app_dir ]]; then
# The private directory chain refused, so the gate quietly used
# the direct TUI and every reachability feature stayed dark for
# the life of the window. Name the reason once, on stderr, and
# in this session's App Server log when one already exists.
python3 - "$__sk_state_root" "$SHPOOL_SESSION_NAME" <<'PY' || true
import os
import stat
import sys
state_root, session_id = sys.argv[1:]
line = "[session-kit: Codex App Server disabled: ~/.local/state must be mode 0700]"
try:
metadata = os.lstat(state_root)
private = (
stat.S_ISDIR(metadata.st_mode)
and metadata.st_uid == os.geteuid()
and stat.S_IMODE(metadata.st_mode) == 0o700
)
except OSError:
private = False
if private:
line = (
"[session-kit: Codex App Server disabled: "
"its private state directory is unavailable]"
)
print(line, file=sys.stderr)
if not session_id or "/" in session_id or session_id.startswith("."):
raise SystemExit(0)
log = os.path.join(
state_root, "session-kit", "app-server", session_id, "app-server.log"
)
flags = os.O_WRONLY | os.O_APPEND | getattr(os, "O_CLOEXEC", 0)
flags |= getattr(os, "O_NOFOLLOW", 0)
try:
descriptor = os.open(log, flags)
except OSError:
raise SystemExit(0)
try:
log_metadata = os.fstat(descriptor)
if stat.S_ISREG(log_metadata.st_mode) and log_metadata.st_uid == os.geteuid():
os.write(descriptor, (line + "\n").encode())
finally:
os.close(descriptor)
PY
fi
if [[ -n $__sk_app_dir ]]; then
__sk_app_socket="$__sk_app_dir/app.sock"
__sk_app_log="$__sk_app_dir/app-server.log"
__sk_broker_log="$__sk_app_dir/broker.log"
if [[ -L $__sk_app_log || -L $__sk_broker_log ]]; then
echo "[session-kit: refusing symlinked App Server log]" >&2
elif ( umask 077
python3 - "$__sk_app_log" "$__sk_broker_log" <<'PY'
import os, stat, sys
for path in sys.argv[1:]:
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND | getattr(os, "O_CLOEXEC", 0)
flags |= getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(path, flags, 0o600)
try:
metadata = os.fstat(descriptor)
if (
not stat.S_ISREG(metadata.st_mode)
or metadata.st_uid != os.geteuid()
or metadata.st_nlink != 1
):
raise OSError("unsafe App Server log")
os.fchmod(descriptor, 0o600)
finally:
os.close(descriptor)
PY
) && [[ ! -L $__sk_app_log && ! -L $__sk_broker_log ]] &&
[[ -f $__sk_app_log && -f $__sk_broker_log ]] &&
chmod 600 -- "$__sk_app_log" "$__sk_broker_log" &&
[[ ! -e $__sk_app_socket ]]; then
codex "${__sk_codex_no_update[@]}" app-server \
--listen "unix://$__sk_app_socket" \
>>"$__sk_app_log" 2>&1 &
__sk_app_server_pid=$!
for __sk_app_attempt in {1..50}; do
[[ -S $__sk_app_socket ]] && break
kill -0 "$__sk_app_server_pid" 2>/dev/null || break
sleep 0.1
done
if [[ -S $__sk_app_socket ]] && \
kill -0 "$__sk_app_server_pid" 2>/dev/null; then
chmod 600 -- "$__sk_app_socket" 2>/dev/null || true
__sk_codex_remote=(--remote "unix://$__sk_app_socket")
if [[ -n $__sk_coord_broker ]]; then
__sk_broker_args=(
--socket "$__sk_app_socket"
--repo "$__sk_cwd"
--server-pid "$__sk_app_server_pid"
)
[[ $__sk_launch_mode == resume && -n $__sk_uuid ]] && \
__sk_broker_args+=(--thread "$__sk_uuid")
python3 "$__sk_coord_broker" "${__sk_broker_args[@]}" \
>>"$__sk_broker_log" 2>&1 &
__sk_broker_pid=$!
fi
else
if [[ -n $__sk_app_server_pid ]]; then
kill "$__sk_app_server_pid" 2>/dev/null || true
wait "$__sk_app_server_pid" 2>/dev/null || true
fi
__sk_app_server_pid=
echo "[session-kit: Codex App Server unavailable; using direct TUI]" >&2
fi
fi
fi
fi
# Codex has no per-thread color; the session's kit color rides in
# as a per-launch theme override (status line, thread-title item).
# Resumes and forks color from the conversation's effective color;
# a brand-new session has no conversation ID yet, so it launches
# with a color picked from the shpool session name, the collector
# adopts that pick as the conversation's override once the ID
# exists, keeping window, picker, and future resumes identical.
# Fail-open: unknown color or missing theme file launches plain.
__sk_consumed_records=("$__sk_start" "$__sk_expected")
[[ -z $__sk_requested_model ]] || __sk_consumed_records+=("$__sk_launch")
[[ ! -e $__sk_account ]] || __sk_consumed_records+=("$__sk_account")
command rm -- "${__sk_consumed_records[@]}"
unset __sk_consumed_records
__sk_provider_launched=1
# A Codex process that boots before a real thread title exists
# keeps showing the conversation ID for that process's life. The
# marker lets the picker request one safe provider bounce once a
# title exists, including a resumed thread that was still untitled.
if [[ $__sk_launch_mode == new ]] ||
! python3 "$__sk_inventory_core" codex-bounce-title --read-only "$__sk_uuid" >/dev/null 2>&1; then
( umask 077
mkdir -p "$__sk_state_root/session-kit/provider-untitled" &&
printf '%s\n' \
"$__sk_current_boot_id:$__sk_shell_pid:$__sk_shell_start:$RANDOM:$EPOCHREALTIME" \
> "$__sk_state_root/session-kit/provider-untitled/$SHPOOL_SESSION_NAME"
) 2>/dev/null || true
fi
while :; do
__sk_codex_theme=()
# The kit owns the tab name on both providers (K3). Codex writes
# its own OSC title from tui.terminal_title, so the kit hands it
# the item list it deployed -- the state glyph plus the thread
# name, which is the string the kit writes when a session is
# named -- as a per-launch override. The personal
# ~/.codex/config.toml is never touched: an override on the
# command line wins for this process only (verified against
# codex-cli 0.145.0, 2026-08-13). Kill switch, both providers:
# SESSION_KIT_TAB_TITLE=off.
__sk_codex_title=()
__sk_codex_title_switch=${SESSION_KIT_TAB_TITLE:-}
# Same rule as sk_tab_title and doctor: surrounding whitespace
# is immaterial, and every spelling of "off" disables titles.
__sk_codex_title_switch=${__sk_codex_title_switch#"${__sk_codex_title_switch%%[![:space:]]*}"}
__sk_codex_title_switch=${__sk_codex_title_switch%"${__sk_codex_title_switch##*[![:space:]]}"}
if [[ ${__sk_codex_title_switch,,} != off ]]; then
__sk_codex_title_items='["activity", "thread"]'
__sk_codex_title_file=${SESSION_KIT_CODEX_HOME:-${CODEX_HOME:-$HOME/.codex}}/session-kit/terminal-title.toml
if [[ -f $__sk_codex_title_file && ! -L $__sk_codex_title_file ]]; then
# Parsed as real TOML, not matched as text. A value like
# ["thread] passes any reasonable regex and is unterminated
# TOML -- passing it through on `-c` would stop Codex from
# starting at all, turning an owner's typo in a file the kit
# deployed into a session that never opens. Anything this
# cannot parse into a list of known-shaped item names falls
# back to the built-in value, and the same file is checked by
# `session-kit doctor`.
__sk_codex_title_deployed=$(
python3 - "$__sk_codex_title_file" <<'SKTITLE' 2>/dev/null
import re
import sys
try:
import tomllib
except ImportError: # Python 3.10 has no parser; read the kit's own dialect.
tomllib = None
def _value(raw):
raw = raw.strip()
if raw.startswith("[") and raw.endswith("]"):
inner = raw[1:-1].strip()
if not inner:
return []
items = []
for part in inner.split(","):
part = part.strip()
if len(part) >= 2 and part[0] == part[-1] and part[0] in "\"'":
items.append(part[1:-1])
else:
raise ValueError("shape")
return items
if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in "\"'":
return raw[1:-1]
raise ValueError("shape")
def parse(text):
if tomllib is not None:
return tomllib.loads(text).get("tui", {}).get("terminal_title")
parsed = {}
current = None
for raw in text.splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
header = re.fullmatch(r"\[([A-Za-z0-9_-]+)\]", line)
if header:
current = parsed.setdefault(header.group(1), {})
if not isinstance(current, dict):
raise ValueError("shape")
continue
pair = re.fullmatch(r"([A-Za-z0-9_-]+)\s*=\s*(.+)", line)
if not pair:
raise ValueError("shape")
(current if current is not None else parsed)[pair.group(1)] = _value(
pair.group(2)
)
tui = parsed.get("tui")
return tui.get("terminal_title") if isinstance(tui, dict) else None
try:
with open(sys.argv[1], "r", encoding="utf-8") as handle:
value = parse(handle.read())
except Exception:
raise SystemExit(1)
if not isinstance(value, list) or not value or len(value) > 12:
raise SystemExit(1)
items = []
for item in value:
if not isinstance(item, str) or not re.fullmatch(r"[a-z][a-z-]{0,31}", item):
raise SystemExit(1)
items.append(item)
print("[" + ", ".join('"%s"' % item for item in items) + "]")
SKTITLE
) || __sk_codex_title_deployed=
if [[ -n $__sk_codex_title_deployed ]]; then
__sk_codex_title_items=$__sk_codex_title_deployed
fi
unset __sk_codex_title_deployed
fi
__sk_codex_title=(-c "tui.terminal_title=$__sk_codex_title_items")
unset __sk_codex_title_items __sk_codex_title_file
fi
unset __sk_codex_title_switch
__sk_theme_color=
if [[ -f $__sk_inventory_core ]]; then
if [[ -n $__sk_uuid ]]; then
__sk_theme_color=$(python3 "$__sk_inventory_core" color effective codex "$__sk_uuid" 2>/dev/null || true)
else
__sk_theme_color=$(python3 "$__sk_inventory_core" color launch-pick "$SHPOOL_SESSION_NAME" 2>/dev/null || true)
fi
fi
case "$__sk_theme_color" in
lime|magenta|silver|sand|sky|sea)
__sk_codex_home=${SESSION_KIT_CODEX_HOME:-${CODEX_HOME:-$HOME/.codex}}
if [[ -r $__sk_codex_home/themes/sk-$__sk_theme_color.tmTheme ]]; then
__sk_codex_theme=(-c "tui.theme=\"sk-$__sk_theme_color\"")
fi
unset __sk_codex_home
;;
esac
unset __sk_theme_color
case "$__sk_launch_mode" in
new)
# A prompt staged by `sp new --prompt-file` seeds the session
# directly (the intake ceremony is retired; the seeded launch
# is not). Consume-once: the handoff never survives a launch.
__sk_prompt_handoff="$__sk_start_dir/$SHPOOL_SESSION_NAME.prompt"
__sk_seed_prompt=
if [[ -f $__sk_prompt_handoff && ! -L $__sk_prompt_handoff ]]; then
__sk_seed_prompt=$(command cat -- "$__sk_prompt_handoff" 2>/dev/null) ||
__sk_seed_prompt=
command rm -f -- "$__sk_prompt_handoff"
fi
if [[ -n $__sk_seed_prompt ]]; then
codex "${__sk_codex_no_update[@]}" "${__sk_codex_theme[@]}" \
"${__sk_codex_title[@]}" \
"${__sk_model_args[@]}" "${__sk_codex_remote[@]}" --no-alt-screen \
-- "$__sk_seed_prompt"
else
codex "${__sk_codex_no_update[@]}" "${__sk_codex_theme[@]}" \
"${__sk_codex_title[@]}" \
"${__sk_model_args[@]}" "${__sk_codex_remote[@]}" --no-alt-screen
fi
__sk_provider_rc=$?
unset __sk_prompt_handoff __sk_seed_prompt
;;
resume) codex "${__sk_codex_no_update[@]}" "${__sk_codex_theme[@]}" "${__sk_codex_title[@]}" "${__sk_model_args[@]}" "${__sk_codex_remote[@]}" --no-alt-screen resume "$__sk_uuid"; __sk_provider_rc=$? ;;
fork) codex "${__sk_codex_no_update[@]}" "${__sk_codex_theme[@]}" "${__sk_codex_title[@]}" "${__sk_model_args[@]}" "${__sk_codex_remote[@]}" --no-alt-screen fork "$__sk_uuid"; __sk_provider_rc=$? ;;
esac
if [[ $__sk_launch_mode != new ]]; then
__sk_lifecycle_uuid=$__sk_uuid
fi
# A kit-requested bounce relaunches the SAME conversation once,
# so the fresh process boots with its title and theme. Any other
# exit falls through to the normal provider-exit handling.
__sk_bounce="$__sk_state_root/session-kit/provider-bounce/$SHPOOL_SESSION_NAME"
if [[ -f $__sk_bounce && ! -L $__sk_bounce ]]; then
__sk_bounce_uuid=$(command head -c 64 -- "$__sk_bounce" 2>/dev/null | tr -cd '0-9a-fA-F-')
if [[ $__sk_bounce_uuid =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]]; then
__sk_uuid=$__sk_bounce_uuid
__sk_launch_mode=resume