triflux 10.60.0 → 10.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -29,6 +29,8 @@ tfx-live ask --cli codex --session cx1 --prompt "현재 변경사항을 요약
|
|
|
29
29
|
tfx-live stop --cli codex --session cx1
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
macOS의 iTerm2에서 로컬 tmux 세션을 `start`하면 리드가 있는 창에 attach 탭이나 패인을 연다. `LC_TERMINAL=iTerm2` 또는 `TERM_PROGRAM=iTerm.app`일 때만 적용하며, 원격과 tmux 세션이 없는 daemon 경로는 제외하고, ssh 로 들어온 셸처럼 리드가 이 기기의 iTerm2 창에 보이지 않으면 열지 않는다. 같은 `--group NAME`은 한 탭에서 세로로 나누고 다른 그룹은 새 탭을 연다. `--group`을 생략하면 대상 CLI인 `claude`, `codex`, `agy`를 그룹으로 쓴다. `stop --session`은 tmux 종료 뒤 남은 iTerm 패인도 닫는다. 연동이 실패해도 세션 시작과 종료는 계속하며 결과의 `itermWarning`에 원인을 남긴다. 탭 제목은 iTerm2 Python 런타임으로 고정하며 처음 쓸 때 iTerm2 Scripts 폴더에 스크립트 하나를 설치하고, 런타임이 없으면 제목만 바뀌지 않는다.
|
|
33
|
+
|
|
32
34
|
새 세션 이름은 `<월>.<일> <주제>`로 짓는다. `start --name`을 생략하면 자동으로 만들고 `nameGenerated: true`를 보고한다. `--resume` 또는 `--resume-last`는 `--name`을 명시한 경우에만 이름을 적용한다. 승계 세션에는 기존 이름 뒤에 ` 2`, 이후 ` 3`을 붙인다. 이름 적용을 요청한 경우 결과의 `nameApplied: false`는 미적용을 뜻한다. agy의 `namePending: true`는 첫 일반 ask에서 적용할 이름이 보류됐다는 뜻이다. Codex `start`는 `/rename` 제출을 입력창이 빌 때까지 확인하고, 이름으로 찾은 thread UUID를 `threadId`로 돌려주며 tmux 세션 옵션 `@tfx_codex_thread`에 남긴다. 로컬 Codex `--resume-last`는 Codex가 고른 thread를 사후에 찾는다. 띄운 뒤 공유 app-server daemon에 로드된 같은 cwd의 thread 중 rollout에 그 시각 이후의 resume 기록(`thread_settings_applied`)이 생긴 것이 정확히 하나이고 1.5초 뒤에도 같을 때만 `threadId`로 쓴다. 이 확인은 5초 안에 끝난다. TUI가 daemon을 쓰지 않거나 같은 cwd에서 동시에 resume이 일어나면 `null`이다. 기존 Codex UDS thread는 `tfx-live rename --cli codex --transport uds --thread ID --name "<이름>"`으로 이름을 바꾼다.
|
|
33
35
|
|
|
34
36
|
## agy 세션
|
package/bin/tfx-live.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { mkdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import {
|
|
6
|
+
chmod,
|
|
6
7
|
mkdir,
|
|
7
8
|
open,
|
|
8
9
|
readdir,
|
|
@@ -93,7 +94,7 @@ const BUG_REPORT_DIR =
|
|
|
93
94
|
function usage(command) {
|
|
94
95
|
const lines = [
|
|
95
96
|
"Usage:",
|
|
96
|
-
" tfx-live start --session NAME [--name NAME] [--cli codex|claude|agy] [--sandbox] [--unset-env PATTERNS] [--model ID] [--effort TIER] [--cwd DIR] [--remote HOST] [--resume ID] [--resume-last 1] [--ready-timeout 30] [--poll-interval 1500]",
|
|
97
|
+
" tfx-live start --session NAME [--name NAME] [--group NAME] [--cli codex|claude|agy] [--sandbox] [--unset-env PATTERNS] [--model ID] [--effort TIER] [--cwd DIR] [--remote HOST] [--resume ID] [--resume-last 1] [--ready-timeout 30] [--poll-interval 1500]",
|
|
97
98
|
" tfx-live ask --session NAME[:WINDOW.PANE] --prompt TEXT [--cli codex|claude|agy] [--if-busy wait|fail|interrupt|queue] [--busy-timeout 60] [--timeout 60] [--remote HOST] [--settle 1500] [--poll-interval 1500]",
|
|
98
99
|
" tfx-live ask --cli codex [--transport queue|tmux] (--session NAME[:WINDOW.PANE] | --thread UUID) --prompt TEXT [--from NAME] [--timeout 60] [--no-wait]",
|
|
99
100
|
" Codex ask defaults to the app-server queue (thread/queue/add, `codex queue --message` only when add is unavailable; delivered when the TUI picks it up); tmux paste is the reported fallback and the path for slash commands.",
|
|
@@ -2470,6 +2471,154 @@ async function resumedCodexThread(
|
|
|
2470
2471
|
return matches.length === 1 ? matches[0] : null;
|
|
2471
2472
|
}
|
|
2472
2473
|
|
|
2474
|
+
const ITERM_TITLE_SCRIPT_NAME = "com.triflux.live-title.py";
|
|
2475
|
+
const ITERM_TITLE_SCRIPT = String.raw`#!/usr/bin/env python3
|
|
2476
|
+
import sys
|
|
2477
|
+
|
|
2478
|
+
import iterm2
|
|
2479
|
+
|
|
2480
|
+
|
|
2481
|
+
async def main(connection):
|
|
2482
|
+
session_id, group, name = sys.argv[1:]
|
|
2483
|
+
app = await iterm2.async_get_app(connection)
|
|
2484
|
+
session = app.get_session_by_id(session_id)
|
|
2485
|
+
if session is None:
|
|
2486
|
+
raise RuntimeError(f"iTerm session not found: {session_id}")
|
|
2487
|
+
update = iterm2.LocalWriteOnlyProfile()
|
|
2488
|
+
update.set_allow_title_setting(False)
|
|
2489
|
+
update.set_name(name)
|
|
2490
|
+
await session.async_set_profile_properties(update)
|
|
2491
|
+
await session.tab.async_set_variable("user.tfxLiveGroup", group)
|
|
2492
|
+
await session.tab.async_set_title(r"\(user.tfxLiveGroup)")
|
|
2493
|
+
await session.async_set_variable("user.tfxLiveTitleApplied", "1")
|
|
2494
|
+
|
|
2495
|
+
|
|
2496
|
+
iterm2.run_until_complete(main)
|
|
2497
|
+
`;
|
|
2498
|
+
|
|
2499
|
+
function appleScriptString(value) {
|
|
2500
|
+
if (/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(value))
|
|
2501
|
+
throw new Error("iTerm values must not contain control characters");
|
|
2502
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function shouldUseIterm(
|
|
2506
|
+
remote,
|
|
2507
|
+
platform = process.platform,
|
|
2508
|
+
env = process.env,
|
|
2509
|
+
) {
|
|
2510
|
+
return (
|
|
2511
|
+
!remote &&
|
|
2512
|
+
platform === "darwin" &&
|
|
2513
|
+
(env.LC_TERMINAL === "iTerm2" || env.TERM_PROGRAM === "iTerm.app")
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
async function openItermSession(session, group) {
|
|
2518
|
+
const groupLiteral = appleScriptString(group);
|
|
2519
|
+
const sessionLiteral = appleScriptString(session);
|
|
2520
|
+
const leaderId = appleScriptString(
|
|
2521
|
+
process.env.ITERM_SESSION_ID?.split(":")[1] ?? "",
|
|
2522
|
+
);
|
|
2523
|
+
// tmux 안의 리드는 ITERM_SESSION_ID 가 tmux 를 처음 띄운 터미널 값이라, 지금 붙어 있는 클라이언트 tty 로 창을 찾는다.
|
|
2524
|
+
const clients = process.env.TMUX_PANE
|
|
2525
|
+
? await runTmux(null, [
|
|
2526
|
+
"list-clients",
|
|
2527
|
+
"-t",
|
|
2528
|
+
process.env.TMUX_PANE,
|
|
2529
|
+
"-F",
|
|
2530
|
+
"#{client_activity} #{client_tty}",
|
|
2531
|
+
]).catch(() => ({ stdout: "" }))
|
|
2532
|
+
: { stdout: "" };
|
|
2533
|
+
const leaderTty = appleScriptString(
|
|
2534
|
+
clients.stdout
|
|
2535
|
+
.trim()
|
|
2536
|
+
.split("\n")
|
|
2537
|
+
.filter(Boolean)
|
|
2538
|
+
.map((line) => line.split(" "))
|
|
2539
|
+
.sort((a, b) => Number(b[0]) - Number(a[0]))[0]?.[1] ?? "",
|
|
2540
|
+
);
|
|
2541
|
+
const { stdout: tmuxPath } = await execFileAsync("which", ["tmux"], {
|
|
2542
|
+
timeout: 2000,
|
|
2543
|
+
});
|
|
2544
|
+
const command = appleScriptString(
|
|
2545
|
+
`${shellQuote([realpathSync(tmuxPath.trim()), "attach-session", "-t"])} '=${session.replace(/'/g, `'"'"'`)}'`,
|
|
2546
|
+
);
|
|
2547
|
+
const script = `tell application "iTerm2"
|
|
2548
|
+
set ttyWindow to missing value
|
|
2549
|
+
set idWindow to missing value
|
|
2550
|
+
repeat with candidateWindow in windows
|
|
2551
|
+
repeat with candidateTab in tabs of candidateWindow
|
|
2552
|
+
repeat with candidateSession in sessions of candidateTab
|
|
2553
|
+
if ${leaderTty} is not "" and tty of candidateSession is ${leaderTty} then set ttyWindow to candidateWindow
|
|
2554
|
+
if ${leaderId} is not "" and id of candidateSession is ${leaderId} then set idWindow to candidateWindow
|
|
2555
|
+
end repeat
|
|
2556
|
+
end repeat
|
|
2557
|
+
end repeat
|
|
2558
|
+
-- 리드가 이 기기의 iTerm 창에 없으면(ssh 로 LC_TERMINAL 만 넘어온 경우 등) 열지 않는다.
|
|
2559
|
+
if ttyWindow is not missing value then
|
|
2560
|
+
set leaderWindow to ttyWindow
|
|
2561
|
+
else if idWindow is not missing value then
|
|
2562
|
+
set leaderWindow to idWindow
|
|
2563
|
+
else
|
|
2564
|
+
return ""
|
|
2565
|
+
end if
|
|
2566
|
+
set groupSession to missing value
|
|
2567
|
+
repeat with candidateTab in tabs of leaderWindow
|
|
2568
|
+
repeat with candidateSession in sessions of candidateTab
|
|
2569
|
+
try
|
|
2570
|
+
tell candidateSession to set candidateGroup to variable named "user.tfx_group"
|
|
2571
|
+
if candidateGroup is ${groupLiteral} then
|
|
2572
|
+
set groupSession to candidateSession
|
|
2573
|
+
exit repeat
|
|
2574
|
+
end if
|
|
2575
|
+
end try
|
|
2576
|
+
end repeat
|
|
2577
|
+
if groupSession is not missing value then exit repeat
|
|
2578
|
+
end repeat
|
|
2579
|
+
set didSplit to groupSession is not missing value
|
|
2580
|
+
if didSplit then
|
|
2581
|
+
tell groupSession to set newSession to split vertically with default profile command ${command}
|
|
2582
|
+
else
|
|
2583
|
+
tell leaderWindow to set newTab to create tab with default profile command ${command}
|
|
2584
|
+
set newSession to current session of newTab
|
|
2585
|
+
end if
|
|
2586
|
+
tell newSession
|
|
2587
|
+
set variable named "user.tfx_group" to ${groupLiteral}
|
|
2588
|
+
set variable named "user.tfx_session" to ${sessionLiteral}
|
|
2589
|
+
end tell
|
|
2590
|
+
return (id of newSession) & linefeed & (didSplit as text)
|
|
2591
|
+
end tell`;
|
|
2592
|
+
const { stdout } = await execFileAsync("/usr/bin/osascript", ["-e", script], {
|
|
2593
|
+
timeout: 5000,
|
|
2594
|
+
maxBuffer: 64 * 1024,
|
|
2595
|
+
});
|
|
2596
|
+
if (!stdout.trim()) return null;
|
|
2597
|
+
const [sessionId, split] = stdout.trim().split("\n");
|
|
2598
|
+
if (!sessionId || !["true", "false"].includes(split))
|
|
2599
|
+
throw new Error("iTerm did not return a session ID and split status");
|
|
2600
|
+
return { sessionId, group, split: split === "true" };
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
async function closeItermSession(sessionId) {
|
|
2604
|
+
const script = `tell application "iTerm2"
|
|
2605
|
+
repeat with candidateWindow in windows
|
|
2606
|
+
repeat with candidateTab in tabs of candidateWindow
|
|
2607
|
+
repeat with candidateSession in sessions of candidateTab
|
|
2608
|
+
if id of candidateSession is ${appleScriptString(sessionId)} then
|
|
2609
|
+
tell candidateSession to close
|
|
2610
|
+
return
|
|
2611
|
+
end if
|
|
2612
|
+
end repeat
|
|
2613
|
+
end repeat
|
|
2614
|
+
end repeat
|
|
2615
|
+
end tell`;
|
|
2616
|
+
await execFileAsync("/usr/bin/osascript", ["-e", script], {
|
|
2617
|
+
timeout: 5000,
|
|
2618
|
+
maxBuffer: 64 * 1024,
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2473
2622
|
async function doStart(adapter, opts) {
|
|
2474
2623
|
const {
|
|
2475
2624
|
session,
|
|
@@ -2657,6 +2806,68 @@ async function doStart(adapter, opts) {
|
|
|
2657
2806
|
"@tfx_codex_thread",
|
|
2658
2807
|
threadId,
|
|
2659
2808
|
]).catch(() => {});
|
|
2809
|
+
let iterm;
|
|
2810
|
+
let itermWarning;
|
|
2811
|
+
if (ready && shouldUseIterm(remote)) {
|
|
2812
|
+
try {
|
|
2813
|
+
iterm = await openItermSession(session, opts.group || adapter.cli);
|
|
2814
|
+
if (iterm) {
|
|
2815
|
+
await runTmux(null, [
|
|
2816
|
+
"set-option",
|
|
2817
|
+
"-t",
|
|
2818
|
+
session,
|
|
2819
|
+
"@tfx_iterm_session",
|
|
2820
|
+
iterm.sessionId,
|
|
2821
|
+
]);
|
|
2822
|
+
const scriptsDir = pathJoin(
|
|
2823
|
+
homedir(),
|
|
2824
|
+
"Library",
|
|
2825
|
+
"Application Support",
|
|
2826
|
+
"iTerm2",
|
|
2827
|
+
"Scripts",
|
|
2828
|
+
);
|
|
2829
|
+
const scriptPath = pathJoin(scriptsDir, ITERM_TITLE_SCRIPT_NAME);
|
|
2830
|
+
await mkdir(scriptsDir, { recursive: true });
|
|
2831
|
+
const installed = await readFile(scriptPath, "utf8").catch((error) => {
|
|
2832
|
+
if (error.code === "ENOENT") return null;
|
|
2833
|
+
throw error;
|
|
2834
|
+
});
|
|
2835
|
+
if (installed !== ITERM_TITLE_SCRIPT)
|
|
2836
|
+
await writeFile(scriptPath, ITERM_TITLE_SCRIPT, { mode: 0o700 });
|
|
2837
|
+
await chmod(scriptPath, 0o700);
|
|
2838
|
+
const titleArgs = [iterm.sessionId, iterm.group, opts.name ?? session]
|
|
2839
|
+
.map(appleScriptString)
|
|
2840
|
+
.join(", ");
|
|
2841
|
+
// 절대 경로는 최초 설치 직후의 메뉴 캐시 지연을 피하고, 적용 표식은 비동기 실패를 잡는다.
|
|
2842
|
+
const titleScript = `tell application "iTerm2"
|
|
2843
|
+
set titleSession to missing value
|
|
2844
|
+
repeat with candidateWindow in windows
|
|
2845
|
+
repeat with candidateTab in tabs of candidateWindow
|
|
2846
|
+
repeat with candidateSession in sessions of candidateTab
|
|
2847
|
+
if id of candidateSession is ${appleScriptString(iterm.sessionId)} then set titleSession to candidateSession
|
|
2848
|
+
end repeat
|
|
2849
|
+
end repeat
|
|
2850
|
+
end repeat
|
|
2851
|
+
if titleSession is missing value then error "iTerm title session not found"
|
|
2852
|
+
launch API script named ${appleScriptString(scriptPath)} arguments {${titleArgs}}
|
|
2853
|
+
repeat 30 times
|
|
2854
|
+
try
|
|
2855
|
+
tell titleSession to set titleApplied to variable named "user.tfxLiveTitleApplied"
|
|
2856
|
+
if titleApplied is "1" then return
|
|
2857
|
+
end try
|
|
2858
|
+
delay 0.1
|
|
2859
|
+
end repeat
|
|
2860
|
+
error "iTerm title update was not confirmed; check the iTerm2 Python runtime and Script Console"
|
|
2861
|
+
end tell`;
|
|
2862
|
+
await execFileAsync("/usr/bin/osascript", ["-e", titleScript], {
|
|
2863
|
+
timeout: 5000,
|
|
2864
|
+
maxBuffer: 64 * 1024,
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
} catch (error) {
|
|
2868
|
+
itermWarning = error.message;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2660
2871
|
return addLoginGuard(adapter, raw, {
|
|
2661
2872
|
cli: adapter.cli,
|
|
2662
2873
|
session,
|
|
@@ -2673,6 +2884,8 @@ async function doStart(adapter, opts) {
|
|
|
2673
2884
|
: {}),
|
|
2674
2885
|
unsetEnv: opts.unsetEnv ?? [],
|
|
2675
2886
|
unsetEnvVerified,
|
|
2887
|
+
...(iterm ? { iterm } : {}),
|
|
2888
|
+
...(itermWarning ? { itermWarning } : {}),
|
|
2676
2889
|
...(unsetEnvWarning ? { unsetEnvWarning } : {}),
|
|
2677
2890
|
...(name && adapter.cli === "codex" && !nameApplied
|
|
2678
2891
|
? {
|
|
@@ -3567,12 +3780,36 @@ async function doStop(adapter, opts) {
|
|
|
3567
3780
|
throw new Error(
|
|
3568
3781
|
"stop --session accepts only a session name; pass the session name without a pane target",
|
|
3569
3782
|
);
|
|
3783
|
+
let itermSessionId;
|
|
3784
|
+
let itermWarning;
|
|
3785
|
+
if (shouldUseIterm(remote)) {
|
|
3786
|
+
try {
|
|
3787
|
+
const { stdout } = await runTmux(null, [
|
|
3788
|
+
"show-options",
|
|
3789
|
+
"-qv",
|
|
3790
|
+
"-t",
|
|
3791
|
+
session,
|
|
3792
|
+
"@tfx_iterm_session",
|
|
3793
|
+
]);
|
|
3794
|
+
itermSessionId = stdout.trim();
|
|
3795
|
+
} catch (error) {
|
|
3796
|
+
itermWarning = error.message;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3570
3799
|
await runTmux(remote, ["kill-session", "-t", session]);
|
|
3800
|
+
if (itermSessionId) {
|
|
3801
|
+
try {
|
|
3802
|
+
await closeItermSession(itermSessionId);
|
|
3803
|
+
} catch (error) {
|
|
3804
|
+
itermWarning = error.message;
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3571
3807
|
return {
|
|
3572
3808
|
cli: adapter.cli,
|
|
3573
3809
|
session,
|
|
3574
3810
|
remote: remote ?? null,
|
|
3575
3811
|
stopped: true,
|
|
3812
|
+
...(itermWarning ? { itermWarning } : {}),
|
|
3576
3813
|
};
|
|
3577
3814
|
}
|
|
3578
3815
|
|
|
@@ -3705,6 +3942,7 @@ function startOpts(flags) {
|
|
|
3705
3942
|
model: flags.model,
|
|
3706
3943
|
effort: flags.effort,
|
|
3707
3944
|
name: flags.name,
|
|
3945
|
+
group: flags.group,
|
|
3708
3946
|
sandbox: Object.hasOwn(flags, "sandbox"),
|
|
3709
3947
|
unsetEnv,
|
|
3710
3948
|
readyTimeoutMs: secondsFlag(
|
|
@@ -5147,6 +5385,7 @@ export {
|
|
|
5147
5385
|
ADAPTERS,
|
|
5148
5386
|
agyStepText,
|
|
5149
5387
|
agyWaitVerdict,
|
|
5388
|
+
appleScriptString,
|
|
5150
5389
|
buildLaunchKeys,
|
|
5151
5390
|
buildPeerClosurePrompt,
|
|
5152
5391
|
buildRemoteLiveCommand,
|
|
@@ -5173,6 +5412,7 @@ export {
|
|
|
5173
5412
|
resolveAskTransport,
|
|
5174
5413
|
resolveCodexDaemonSocket,
|
|
5175
5414
|
resumedCodexThread,
|
|
5415
|
+
shouldUseIterm,
|
|
5176
5416
|
splitTmuxTarget,
|
|
5177
5417
|
stopOpts,
|
|
5178
5418
|
tmuxBufferName,
|
package/package.json
CHANGED
package/skills/tfx-live/SKILL.md
CHANGED
|
@@ -25,6 +25,8 @@ tfx-live ask --cli codex --session cx1 --prompt "현재 변경사항을 요약
|
|
|
25
25
|
tfx-live stop --cli codex --session cx1
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
macOS의 iTerm2에서 로컬 tmux 세션을 `start`하면 리드가 있는 창에 attach 탭이나 패인을 연다. `LC_TERMINAL=iTerm2` 또는 `TERM_PROGRAM=iTerm.app`일 때만 적용하며, 원격과 tmux 세션이 없는 daemon 경로는 제외하고, ssh 로 들어온 셸처럼 리드가 이 기기의 iTerm2 창에 보이지 않으면 열지 않는다. 같은 `--group NAME`은 한 탭에서 세로로 나누고 다른 그룹은 새 탭을 연다. `--group`을 생략하면 대상 CLI인 `claude`, `codex`, `agy`를 그룹으로 쓴다. `stop --session`은 tmux 종료 뒤 남은 iTerm 패인도 닫는다. 연동이 실패해도 세션 시작과 종료는 계속하며 결과의 `itermWarning`에 원인을 남긴다. 탭 제목은 iTerm2 Python 런타임으로 고정하며 처음 쓸 때 iTerm2 Scripts 폴더에 스크립트 하나를 설치하고, 런타임이 없으면 제목만 바뀌지 않는다.
|
|
29
|
+
|
|
28
30
|
새 세션 이름은 `<월>.<일> <주제>`로 짓는다. `start --name`을 생략하면 자동으로 만들고 `nameGenerated: true`를 보고한다. `--resume` 또는 `--resume-last`는 `--name`을 명시한 경우에만 이름을 적용한다. 승계 세션에는 기존 이름 뒤에 ` 2`, 이후 ` 3`을 붙인다. 이름 적용을 요청한 경우 결과의 `nameApplied: false`는 미적용을 뜻한다. agy의 `namePending: true`는 첫 일반 ask에서 적용할 이름이 보류됐다는 뜻이다. Codex `start`는 `/rename` 제출을 입력창이 빌 때까지 확인하고, 이름으로 찾은 thread UUID를 `threadId`로 돌려주며 tmux 세션 옵션 `@tfx_codex_thread`에 남긴다. 로컬 Codex `--resume-last`는 Codex가 고른 thread를 사후에 찾는다. 띄운 뒤 공유 app-server daemon에 로드된 같은 cwd의 thread 중 rollout에 그 시각 이후의 resume 기록(`thread_settings_applied`)이 생긴 것이 정확히 하나이고 1.5초 뒤에도 같을 때만 `threadId`로 쓴다. 이 확인은 5초 안에 끝난다. TUI가 daemon을 쓰지 않거나 같은 cwd에서 동시에 resume이 일어나면 `null`이다. 기존 Codex UDS thread는 `tfx-live rename --cli codex --transport uds --thread ID --name "<이름>"`으로 이름을 바꾼다.
|
|
29
31
|
|
|
30
32
|
## agy 세션
|