triflux 10.47.0 → 10.48.0
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.
- package/CLAUDE.md +68 -106
- package/bin/tfx-live.mjs +279 -19
- package/config/routing-policy.json +1 -1
- package/hooks/codex-session-hook.mjs +7 -0
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/lib/codex-session-registry.mjs +306 -0
- package/hub/workers/codex-mcp.mjs +1 -1
- package/hud/context-monitor.mjs +1 -1
- package/package.json +1 -1
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +4 -4
- package/scripts/lib/agent-route-policy.mjs +13 -13
- package/scripts/lib/codex-profile-config.mjs +10 -7
- package/scripts/setup.mjs +5 -5
- package/scripts/tfx-route.sh +22 -20
- package/skills/tfx-auto/SKILL.md +1 -1
- package/skills/tfx-live/SKILL.md +28 -0
- package/skills/tfx-plan/SKILL.md +2 -2
- package/skills/tfx-profile/SKILL.md +5 -5
- package/skills/tfx-setup/SKILL.md +1 -1
- package/tui/codex-profile.mjs +1 -0
- package/tui/setup.mjs +2 -2
package/bin/tfx-live.mjs
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { mkdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
mkdir,
|
|
6
|
+
open,
|
|
7
|
+
readdir,
|
|
8
|
+
readFile,
|
|
9
|
+
rm,
|
|
10
|
+
writeFile,
|
|
11
|
+
} from "node:fs/promises";
|
|
5
12
|
import { homedir, tmpdir } from "node:os";
|
|
6
13
|
import { join as pathJoin, resolve as pathResolve } from "node:path";
|
|
7
14
|
import { fileURLToPath } from "node:url";
|
|
8
15
|
import { promisify } from "node:util";
|
|
9
16
|
import { runHygiene } from "../cto/hygiene.mjs";
|
|
10
17
|
import { notifyCtoHygieneOnce } from "../cto/hygiene-notify.mjs";
|
|
18
|
+
import {
|
|
19
|
+
codexThreadNames,
|
|
20
|
+
readCodexSessionRecords,
|
|
21
|
+
registryDir,
|
|
22
|
+
} from "../hub/lib/codex-session-registry.mjs";
|
|
11
23
|
import { resolveHardCeilingMs } from "../hub/lib/worker-lifecycle.mjs";
|
|
12
24
|
import { createNotifier } from "../hub/team/notify.mjs";
|
|
13
25
|
import {
|
|
@@ -58,7 +70,8 @@ function usage() {
|
|
|
58
70
|
" tfx-live interrupt --session NAME [--cli codex|claude] [--transport tmux|uds|auto] [--short SHORT | --session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 5]",
|
|
59
71
|
" tfx-live stop --session NAME [--cli codex|claude] [--remote HOST]",
|
|
60
72
|
" tfx-live probe [--short SHORT] [--session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 10]",
|
|
61
|
-
" tfx-live list-sessions --cli codex [--transport tmux|uds] [--codex-socket PATH|default] [--cwd DIR] [--remote HOST (tmux)]",
|
|
73
|
+
" tfx-live list-sessions --cli codex|claude [--transport tmux|uds] [--codex-socket PATH|default] [--cwd DIR] [--remote HOST (codex tmux only)]",
|
|
74
|
+
" Claude list-sessions supports local tmux only; --transport uds requires --cli codex.",
|
|
62
75
|
" tfx-live converse --session NAME --prompts-file PATH [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500]",
|
|
63
76
|
" tfx-live goal-driven --session NAME --goal TEXT [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500] [--max-rounds 8] [--done-token DONE]",
|
|
64
77
|
" tfx-live peer [--cli-a codex] [--cli-b claude] [--model-a ID] [--model-b ID] [--effort-a TIER] [--effort-b TIER] [--session-a NAME[:WINDOW.PANE]] [--session-b NAME[:WINDOW.PANE]] [--attach-a] [--attach-b] [--transport-a tmux|uds|auto] [--transport-b tmux|uds|auto] [--short-a SHORT] [--short-b SHORT] [--session-id-a ID] [--session-id-b ID] [--thread-a ID|auto] [--thread-b ID|auto] [--codex-socket-a PATH|default] [--codex-socket-b PATH|default] [--bridge ABS] [--remote HOST] [--cwd DIR] [--if-busy wait|fail] [--if-busy-a POLICY] [--if-busy-b POLICY] [--busy-timeout 60] [--max-turn SECONDS] [--rounds 4] [--mode counting|freeform] [--seed TEXT] [--timeout 60]",
|
|
@@ -538,7 +551,26 @@ async function inspectCodexTmuxPane(pane, deps = {}) {
|
|
|
538
551
|
};
|
|
539
552
|
}
|
|
540
553
|
|
|
541
|
-
|
|
554
|
+
const TMUX_DISCOVERY_FORMAT = [
|
|
555
|
+
"#{session_name}",
|
|
556
|
+
"#{session_created}",
|
|
557
|
+
"#{session_attached}",
|
|
558
|
+
"#{window_index}",
|
|
559
|
+
"#{pane_index}",
|
|
560
|
+
"#{pane_current_path}",
|
|
561
|
+
"#{pane_current_command}",
|
|
562
|
+
"#{pane_start_command}",
|
|
563
|
+
"#{pane_tty}",
|
|
564
|
+
"#{pane_pid}",
|
|
565
|
+
"#{pane_id}",
|
|
566
|
+
].join("\t");
|
|
567
|
+
|
|
568
|
+
function parseCodexTmuxSessions(
|
|
569
|
+
stdout,
|
|
570
|
+
cwd = null,
|
|
571
|
+
codexTargets = null,
|
|
572
|
+
paneThreads = new Map(),
|
|
573
|
+
) {
|
|
542
574
|
const cwdFilter = normalizeCwd(cwd);
|
|
543
575
|
const sessions = new Map();
|
|
544
576
|
|
|
@@ -553,6 +585,9 @@ function parseCodexTmuxSessions(stdout, cwd = null, codexTargets = null) {
|
|
|
553
585
|
paneCwd,
|
|
554
586
|
currentCommand,
|
|
555
587
|
startCommand,
|
|
588
|
+
,
|
|
589
|
+
,
|
|
590
|
+
paneId,
|
|
556
591
|
] = line.split("\t");
|
|
557
592
|
if (
|
|
558
593
|
!session ||
|
|
@@ -577,6 +612,7 @@ function parseCodexTmuxSessions(stdout, cwd = null, codexTargets = null) {
|
|
|
577
612
|
target: `${session}:${windowIndex}.${paneIndex}`,
|
|
578
613
|
cwd: normalizedPaneCwd,
|
|
579
614
|
command: currentCommand,
|
|
615
|
+
...(paneThreads.get(paneId) ?? {}),
|
|
580
616
|
};
|
|
581
617
|
if (existing) {
|
|
582
618
|
if (!existing.cwd && normalizedPaneCwd) {
|
|
@@ -608,27 +644,15 @@ async function discoverCodexTmuxSessions(
|
|
|
608
644
|
{ cwd = null, remote = null } = {},
|
|
609
645
|
deps = {},
|
|
610
646
|
) {
|
|
611
|
-
const format = [
|
|
612
|
-
"#{session_name}",
|
|
613
|
-
"#{session_created}",
|
|
614
|
-
"#{session_attached}",
|
|
615
|
-
"#{window_index}",
|
|
616
|
-
"#{pane_index}",
|
|
617
|
-
"#{pane_current_path}",
|
|
618
|
-
"#{pane_current_command}",
|
|
619
|
-
"#{pane_start_command}",
|
|
620
|
-
"#{pane_tty}",
|
|
621
|
-
"#{pane_pid}",
|
|
622
|
-
].join("\t");
|
|
623
|
-
|
|
624
647
|
try {
|
|
625
648
|
const { stdout } = await (deps.runTmux ?? runTmux)(remote, [
|
|
626
649
|
"list-panes",
|
|
627
650
|
"-a",
|
|
628
651
|
"-F",
|
|
629
|
-
|
|
652
|
+
TMUX_DISCOVERY_FORMAT,
|
|
630
653
|
]);
|
|
631
654
|
const codexTargets = new Set();
|
|
655
|
+
const panePids = new Map();
|
|
632
656
|
for (const line of String(stdout).split(/\r?\n/)) {
|
|
633
657
|
if (!line) continue;
|
|
634
658
|
const [
|
|
@@ -642,18 +666,62 @@ async function discoverCodexTmuxSessions(
|
|
|
642
666
|
startCommand,
|
|
643
667
|
paneTty,
|
|
644
668
|
panePid,
|
|
669
|
+
paneId,
|
|
645
670
|
] = line.split("\t");
|
|
671
|
+
panePids.set(paneId, Number(panePid));
|
|
646
672
|
const result = await inspectCodexTmuxPane(
|
|
647
673
|
{ currentCommand, startCommand, paneTty, panePid, remote },
|
|
648
674
|
deps,
|
|
649
675
|
);
|
|
650
676
|
if (result.isCodex) codexTargets.add(`${session}:${window}.${pane}`);
|
|
651
677
|
}
|
|
678
|
+
const paneThreads = new Map();
|
|
679
|
+
if (!remote) {
|
|
680
|
+
const env = deps.env ?? process.env;
|
|
681
|
+
const records = (deps.readCodexSessionRecords ?? readCodexSessionRecords)(
|
|
682
|
+
{
|
|
683
|
+
dir: registryDir(env),
|
|
684
|
+
},
|
|
685
|
+
);
|
|
686
|
+
const parents = new Map();
|
|
687
|
+
try {
|
|
688
|
+
const processes = await (deps.psExec ?? execFileAsync)(
|
|
689
|
+
"ps",
|
|
690
|
+
["-eo", "pid=,ppid="],
|
|
691
|
+
{ timeout: 1000, maxBuffer: MAX_BUFFER },
|
|
692
|
+
);
|
|
693
|
+
for (const line of String(processes.stdout).split(/\r?\n/)) {
|
|
694
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s*$/);
|
|
695
|
+
if (match) parents.set(Number(match[1]), Number(match[2]));
|
|
696
|
+
}
|
|
697
|
+
} catch {
|
|
698
|
+
// Without ancestry evidence only the pane process itself can match.
|
|
699
|
+
}
|
|
700
|
+
const names = codexThreadNames({
|
|
701
|
+
indexPath: pathJoin(
|
|
702
|
+
env.CODEX_HOME || pathJoin(env.HOME || homedir(), ".codex"),
|
|
703
|
+
"session_index.jsonl",
|
|
704
|
+
),
|
|
705
|
+
});
|
|
706
|
+
for (const record of records) {
|
|
707
|
+
const panePid = panePids.get(record.tmuxPane);
|
|
708
|
+
if (!Number.isSafeInteger(panePid) || panePid <= 0) continue;
|
|
709
|
+
let ancestor = record.pid;
|
|
710
|
+
for (let depth = 0; depth < 8 && ancestor !== panePid; depth++) {
|
|
711
|
+
ancestor = parents.get(ancestor);
|
|
712
|
+
}
|
|
713
|
+
if (ancestor !== panePid) continue;
|
|
714
|
+
paneThreads.set(record.tmuxPane, {
|
|
715
|
+
threadId: record.sessionId,
|
|
716
|
+
name: names.get(record.sessionId) ?? null,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
652
720
|
return {
|
|
653
721
|
ok: true,
|
|
654
722
|
cli: "codex",
|
|
655
723
|
cwd: normalizeCwd(cwd),
|
|
656
|
-
sessions: parseCodexTmuxSessions(stdout, cwd, codexTargets),
|
|
724
|
+
sessions: parseCodexTmuxSessions(stdout, cwd, codexTargets, paneThreads),
|
|
657
725
|
};
|
|
658
726
|
} catch (error) {
|
|
659
727
|
return {
|
|
@@ -667,6 +735,185 @@ async function discoverCodexTmuxSessions(
|
|
|
667
735
|
}
|
|
668
736
|
}
|
|
669
737
|
|
|
738
|
+
function isSessionPidAlive(pid) {
|
|
739
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
740
|
+
try {
|
|
741
|
+
process.kill(pid, 0);
|
|
742
|
+
return true;
|
|
743
|
+
} catch (error) {
|
|
744
|
+
return error.code === "EPERM";
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function claudeConversationTitle(sessionId, env) {
|
|
749
|
+
if (!sessionId || /[/\\]/.test(sessionId)) return null;
|
|
750
|
+
const projectsDir =
|
|
751
|
+
env.TFX_CLAUDE_PROJECTS_DIR ||
|
|
752
|
+
pathJoin(env.HOME || homedir(), ".claude", "projects");
|
|
753
|
+
let projects;
|
|
754
|
+
try {
|
|
755
|
+
projects = await readdir(projectsDir);
|
|
756
|
+
} catch {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
for (const project of projects.sort()) {
|
|
760
|
+
let handle;
|
|
761
|
+
try {
|
|
762
|
+
handle = await open(
|
|
763
|
+
pathJoin(projectsDir, project, `${sessionId}.jsonl`),
|
|
764
|
+
"r",
|
|
765
|
+
);
|
|
766
|
+
const stat = await handle.stat();
|
|
767
|
+
if (!stat.isFile()) continue;
|
|
768
|
+
const length = Math.min(stat.size, 262144);
|
|
769
|
+
const buffer = Buffer.alloc(length);
|
|
770
|
+
const start = stat.size - length;
|
|
771
|
+
let total = 0;
|
|
772
|
+
while (total < length) {
|
|
773
|
+
const { bytesRead } = await handle.read(
|
|
774
|
+
buffer,
|
|
775
|
+
total,
|
|
776
|
+
length - total,
|
|
777
|
+
start + total,
|
|
778
|
+
);
|
|
779
|
+
if (!bytesRead) break;
|
|
780
|
+
total += bytesRead;
|
|
781
|
+
}
|
|
782
|
+
const lines = buffer.subarray(0, total).toString("utf8").split("\n");
|
|
783
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
784
|
+
try {
|
|
785
|
+
const record = JSON.parse(lines[i]);
|
|
786
|
+
if (record?.type !== "custom-title" && record?.type !== "ai-title")
|
|
787
|
+
continue;
|
|
788
|
+
const title = record.customTitle ?? record.aiTitle;
|
|
789
|
+
if (typeof title === "string" && title.trim()) return title.trim();
|
|
790
|
+
} catch {
|
|
791
|
+
// The tail may start mid-line or end with a concurrent partial write.
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
} catch {
|
|
795
|
+
// Missing or unreadable transcripts cannot break session discovery.
|
|
796
|
+
} finally {
|
|
797
|
+
await handle?.close().catch(() => {});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function discoverClaudeTmuxSessions({ cwd = null } = {}, deps = {}) {
|
|
804
|
+
const result = {
|
|
805
|
+
ok: true,
|
|
806
|
+
cli: "claude",
|
|
807
|
+
cwd: normalizeCwd(cwd),
|
|
808
|
+
sessions: [],
|
|
809
|
+
};
|
|
810
|
+
let stdout;
|
|
811
|
+
try {
|
|
812
|
+
({ stdout } = await (deps.runTmux ?? runTmux)(null, [
|
|
813
|
+
"list-panes",
|
|
814
|
+
"-a",
|
|
815
|
+
"-F",
|
|
816
|
+
TMUX_DISCOVERY_FORMAT,
|
|
817
|
+
]));
|
|
818
|
+
} catch (error) {
|
|
819
|
+
return {
|
|
820
|
+
...result,
|
|
821
|
+
ok: false,
|
|
822
|
+
reason: "tmux-unavailable",
|
|
823
|
+
error: error.message,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
const panes = new Map();
|
|
827
|
+
for (const line of String(stdout).split(/\r?\n/)) {
|
|
828
|
+
const [session, , , window, pane, paneCwd, , , , pid, paneId] =
|
|
829
|
+
line.split("\t");
|
|
830
|
+
if (paneId)
|
|
831
|
+
panes.set(paneId, {
|
|
832
|
+
session,
|
|
833
|
+
target: `${session}:${window}.${pane}`,
|
|
834
|
+
cwd: normalizeCwd(paneCwd),
|
|
835
|
+
pid: Number(pid),
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
const parents = new Map();
|
|
839
|
+
try {
|
|
840
|
+
const processes = await (deps.psExec ?? execFileAsync)(
|
|
841
|
+
"ps",
|
|
842
|
+
["-eo", "pid=,ppid="],
|
|
843
|
+
{
|
|
844
|
+
timeout: 1000,
|
|
845
|
+
maxBuffer: MAX_BUFFER,
|
|
846
|
+
},
|
|
847
|
+
);
|
|
848
|
+
for (const line of String(processes.stdout).split(/\r?\n/)) {
|
|
849
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s*$/);
|
|
850
|
+
if (match) parents.set(Number(match[1]), Number(match[2]));
|
|
851
|
+
}
|
|
852
|
+
} catch {
|
|
853
|
+
// Without ancestry evidence only the pane process itself can match.
|
|
854
|
+
}
|
|
855
|
+
const env = deps.env ?? process.env;
|
|
856
|
+
const dir =
|
|
857
|
+
env.TFX_CLAUDE_SESSIONS_DIR ||
|
|
858
|
+
pathJoin(env.HOME || homedir(), ".claude", "sessions");
|
|
859
|
+
let files;
|
|
860
|
+
try {
|
|
861
|
+
files = await readdir(dir);
|
|
862
|
+
} catch {
|
|
863
|
+
return result;
|
|
864
|
+
}
|
|
865
|
+
for (const file of files.sort()) {
|
|
866
|
+
if (!file.endsWith(".json")) continue;
|
|
867
|
+
try {
|
|
868
|
+
const record = JSON.parse(await readFile(pathJoin(dir, file), "utf8"));
|
|
869
|
+
// Match from the right: session names may themselves contain ':' and '.'.
|
|
870
|
+
const coordinate =
|
|
871
|
+
typeof record.tmux === "string"
|
|
872
|
+
? record.tmux.match(/^(.*):@(\d+)\.(%\d+)$/)
|
|
873
|
+
: null;
|
|
874
|
+
if (
|
|
875
|
+
!coordinate ||
|
|
876
|
+
typeof record.sessionId !== "string" ||
|
|
877
|
+
!record.sessionId ||
|
|
878
|
+
!Number.isSafeInteger(record.pid) ||
|
|
879
|
+
record.pid <= 0
|
|
880
|
+
)
|
|
881
|
+
continue;
|
|
882
|
+
const pane = panes.get(coordinate[3]);
|
|
883
|
+
if (!pane || (result.cwd && pane.cwd !== result.cwd)) continue;
|
|
884
|
+
if (!(deps.isAlive ?? isSessionPidAlive)(record.pid)) continue;
|
|
885
|
+
let ancestor = record.pid;
|
|
886
|
+
for (let depth = 0; depth < 4 && ancestor !== pane.pid; depth++) {
|
|
887
|
+
ancestor = parents.get(ancestor);
|
|
888
|
+
}
|
|
889
|
+
if (ancestor !== pane.pid) continue;
|
|
890
|
+
const name = record.name ?? null;
|
|
891
|
+
const title =
|
|
892
|
+
record.nameSource === "derived" ||
|
|
893
|
+
!name ||
|
|
894
|
+
(typeof name === "string" && !name.trim())
|
|
895
|
+
? await claudeConversationTitle(record.sessionId, env)
|
|
896
|
+
: name;
|
|
897
|
+
result.sessions.push({
|
|
898
|
+
session: pane.session,
|
|
899
|
+
target: pane.target,
|
|
900
|
+
paneId: coordinate[3],
|
|
901
|
+
cwd: pane.cwd,
|
|
902
|
+
pid: record.pid,
|
|
903
|
+
sessionId: record.sessionId,
|
|
904
|
+
short: record.sessionId.slice(0, 8),
|
|
905
|
+
name,
|
|
906
|
+
title,
|
|
907
|
+
nameSource: record.nameSource ?? null,
|
|
908
|
+
status: record.status ?? null,
|
|
909
|
+
});
|
|
910
|
+
} catch {
|
|
911
|
+
// A malformed or concurrently removed record cannot break discovery.
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return result;
|
|
915
|
+
}
|
|
916
|
+
|
|
670
917
|
function sleep(ms) {
|
|
671
918
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
672
919
|
}
|
|
@@ -2536,8 +2783,20 @@ async function probe(flags) {
|
|
|
2536
2783
|
|
|
2537
2784
|
async function listSessions(flags) {
|
|
2538
2785
|
const adapter = selectAdapter(flags);
|
|
2786
|
+
if (adapter.cli === "claude") {
|
|
2787
|
+
if (flags.remote || flags.transport === "uds") {
|
|
2788
|
+
throw new Error(
|
|
2789
|
+
"Claude list-sessions supports local tmux only; --remote and --transport uds are unavailable",
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
if (flags.transport && !["tmux", "auto"].includes(flags.transport)) {
|
|
2793
|
+
throw new Error("Claude list-sessions --transport must be tmux or auto");
|
|
2794
|
+
}
|
|
2795
|
+
printJson(await discoverClaudeTmuxSessions({ cwd: flags.cwd }));
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2539
2798
|
if (adapter.cli !== "codex") {
|
|
2540
|
-
throw new Error("list-sessions
|
|
2799
|
+
throw new Error("list-sessions supports --cli codex or --cli claude");
|
|
2541
2800
|
}
|
|
2542
2801
|
if (flags.transport === "uds") {
|
|
2543
2802
|
const { listCodexAppServerThreads } = await import(
|
|
@@ -3550,6 +3809,7 @@ export {
|
|
|
3550
3809
|
createPeerSignalController,
|
|
3551
3810
|
ctoHygieneNotify,
|
|
3552
3811
|
derivePeerStatus,
|
|
3812
|
+
discoverClaudeTmuxSessions,
|
|
3553
3813
|
discoverCodexTmuxSessions,
|
|
3554
3814
|
doAskViaTmux,
|
|
3555
3815
|
extractAssistantResponse,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { argv, exit, stdin, stdout } from "node:process";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { writeCodexSessionRecord } from "../hub/lib/codex-session-registry.mjs";
|
|
6
7
|
import { drainPendingSynapse as defaultDrainPendingSynapse } from "../hub/team/synapse-http.mjs";
|
|
7
8
|
import {
|
|
8
9
|
heartbeatInteractiveSession as defaultHeartbeatInteractiveSession,
|
|
@@ -145,6 +146,7 @@ export async function runCodexSessionHook(stdinData, opts = {}) {
|
|
|
145
146
|
const mode = parsed.ok
|
|
146
147
|
? normalizeMode(opts.argvMode ?? argv[2], parsed.payload)
|
|
147
148
|
: "";
|
|
149
|
+
const writeSessionRecord = opts.writeSessionRecord || writeCodexSessionRecord;
|
|
148
150
|
const hubEnsureRun = opts.hubEnsureRun || defaultHubEnsureRun;
|
|
149
151
|
const registerInteractiveSession =
|
|
150
152
|
opts.registerInteractiveSession || defaultRegisterInteractiveSession;
|
|
@@ -157,6 +159,11 @@ export async function runCodexSessionHook(stdinData, opts = {}) {
|
|
|
157
159
|
|
|
158
160
|
try {
|
|
159
161
|
await runHookSideEffectsWithStdoutSuppressed(async () => {
|
|
162
|
+
if (mode === "register" || mode === "heartbeat") {
|
|
163
|
+
try {
|
|
164
|
+
writeSessionRecord(parsed.payload);
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
160
167
|
if (mode === "register") {
|
|
161
168
|
try {
|
|
162
169
|
launchPresenceRegistration(parsed.payload);
|