triflux 10.34.0 → 10.35.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/bin/tfx-live.mjs CHANGED
@@ -34,11 +34,11 @@ function usage() {
34
34
  "Usage:",
35
35
  " tfx-live start --session NAME [--cli codex|claude] [--cwd DIR] [--remote HOST] [--resume ID] [--resume-last 1] [--ready-timeout 30] [--poll-interval 1500]",
36
36
  " tfx-live ask --session NAME --prompt TEXT [--cli codex|claude] [--timeout 60] [--remote HOST] [--settle 1500] [--poll-interval 1500]",
37
- " tfx-live ask --transport uds|auto (--short SHORT | --session-id ID) --prompt TEXT [--bridge ABS] [--session NAME (auto fallback)] [--timeout 60]",
37
+ " tfx-live ask --transport uds|auto (--short SHORT | --session-id ID) --prompt TEXT [--config-dir DIR] [--bridge ABS] [--session NAME (auto fallback)] [--timeout 60]",
38
38
  " transport: auto is the default for Claude when --short/--session-id is present; otherwise tmux. bridge path: --bridge > $TFX_BRIDGE > $TFX_REPO_ROOT/hub/bridge.mjs > bundled Triflux hub/bridge.mjs.",
39
- " tfx-live interrupt --session NAME [--cli codex|claude] [--transport tmux|uds|auto] [--short SHORT | --session-id ID] [--bridge ABS] [--timeout 5]",
39
+ " tfx-live interrupt --session NAME [--cli codex|claude] [--transport tmux|uds|auto] [--short SHORT | --session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 5]",
40
40
  " tfx-live stop --session NAME [--cli codex|claude] [--remote HOST]",
41
- " tfx-live probe [--short SHORT] [--session-id ID] [--bridge ABS] [--timeout 10]",
41
+ " tfx-live probe [--short SHORT] [--session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 10]",
42
42
  " tfx-live converse --session NAME --prompts-file PATH [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500]",
43
43
  " 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]",
44
44
  " tfx-live peer [--cli-a codex] [--cli-b claude] [--session-a peerA] [--session-b peerB] [--transport-a tmux|uds|auto] [--transport-b tmux|uds|auto] [--short-a SHORT] [--short-b SHORT] [--session-id-a ID] [--session-id-b ID] [--bridge ABS] [--remote HOST] [--cwd DIR] [--rounds 4] [--mode counting|freeform] [--seed TEXT] [--timeout 60]",
@@ -787,13 +787,19 @@ async function probeDaemon(bridgePath, ref, timeoutMs) {
787
787
  const payload = {};
788
788
  if (ref?.short) payload.short = ref.short;
789
789
  if (ref?.sessionId) payload.sessionId = ref.sessionId;
790
+ if (ref?.configDir) payload.configDir = ref.configDir;
790
791
  const result = await callBridgeVerb(
791
792
  bridgePath,
792
793
  "daemon-probe",
793
794
  payload,
794
795
  timeoutMs,
795
796
  );
796
- return { ok: result?.ok === true, sessions: result?.sessions, raw: result };
797
+ return {
798
+ ok: result?.ok === true,
799
+ reason: result?.reason,
800
+ sessions: result?.sessions,
801
+ raw: result,
802
+ };
797
803
  } catch (error) {
798
804
  return { ok: false, reason: error.message };
799
805
  }
@@ -931,10 +937,11 @@ async function doAsk(adapter, opts) {
931
937
  }
932
938
 
933
939
  async function doAskViaDaemon(opts, meta = {}) {
934
- const { bridgePath, prompt, timeoutMs, short, sessionId } = opts;
940
+ const { bridgePath, prompt, timeoutMs, short, sessionId, configDir } = opts;
935
941
  const payload = { prompt, timeoutMs };
936
942
  if (short) payload.short = short;
937
943
  if (sessionId) payload.sessionId = sessionId;
944
+ if (configDir) payload.configDir = configDir;
938
945
 
939
946
  const result = await callBridgeVerb(
940
947
  bridgePath,
@@ -956,12 +963,35 @@ async function doAskViaDaemon(opts, meta = {}) {
956
963
  timedOut: result?.timedOut === true,
957
964
  closed: result?.closed === true,
958
965
  inputSent: result?.inputSent === true,
966
+ daemon: result?.daemon ?? null,
967
+ daemons: result?.daemons ?? [],
968
+ matches: result?.matches ?? [],
969
+ candidateResults: result?.candidateResults ?? [],
970
+ callerProvenance: result?.callerProvenance ?? null,
959
971
  done: matchedCompletion,
960
972
  ...(result?.error ? { error: result.error } : {}),
961
973
  ...meta,
962
974
  };
963
975
  }
964
976
 
977
+ function daemonProbeTargetAttachable(probe, opts) {
978
+ if (!probe?.ok) return false;
979
+ // New bridge responses carry `target`; daemon-control owns selection policy.
980
+ if (probe.raw?.target) return true;
981
+ // Compatibility only for older bridge binaries that list sessions but do not
982
+ // expose `target` yet. Do not add new selection semantics here.
983
+ if (!Array.isArray(probe.sessions)) return false;
984
+ return probe.sessions.some(
985
+ (entry) =>
986
+ (opts.short && entry?.short === opts.short) ||
987
+ (opts.sessionId &&
988
+ (entry?.sessionId === opts.sessionId ||
989
+ entry?.session_id === opts.sessionId ||
990
+ entry?.dispatch?.sessionId === opts.sessionId ||
991
+ entry?.d?.sessionId === opts.sessionId)),
992
+ );
993
+ }
994
+
965
995
  async function ensureTmuxSession(adapter, opts) {
966
996
  if (!opts.session) return false;
967
997
  try {
@@ -1004,6 +1034,13 @@ function writeUdsBugReport(reason, context) {
1004
1034
  .map((entry) => entry?.short)
1005
1035
  .filter(Boolean)
1006
1036
  : [],
1037
+ // Diagnostic-only bridge metadata; callers should not depend on this
1038
+ // shape as a versioned control contract.
1039
+ daemon: context.probe.raw?.daemon ?? null,
1040
+ daemons: context.probe.raw?.daemons ?? [],
1041
+ matches: context.probe.raw?.matches ?? [],
1042
+ candidateResults: context.probe.raw?.candidateResults ?? [],
1043
+ callerProvenance: context.probe.raw?.callerProvenance ?? null,
1007
1044
  }
1008
1045
  : null,
1009
1046
  attachError: context.attachError ?? null,
@@ -1052,26 +1089,23 @@ async function runTmuxFallback(adapter, opts, reason, udsResult) {
1052
1089
  async function doAskAuto(adapter, opts) {
1053
1090
  const probe = await probeDaemon(
1054
1091
  opts.bridgePath,
1055
- { short: opts.short, sessionId: opts.sessionId },
1092
+ { short: opts.short, sessionId: opts.sessionId, configDir: opts.configDir },
1056
1093
  opts.timeoutMs,
1057
1094
  );
1058
1095
  // daemon-probe returns the full session list (it does not filter by the
1059
1096
  // requested short), so confirm the target is actually attachable here rather
1060
1097
  // than firing a doomed attach at a missing short.
1061
- const targetAttachable =
1062
- probe.ok &&
1063
- Array.isArray(probe.sessions) &&
1064
- probe.sessions.some(
1065
- (entry) =>
1066
- (opts.short && entry?.short === opts.short) ||
1067
- (opts.sessionId && entry?.sessionId === opts.sessionId),
1068
- );
1098
+ const targetAttachable = daemonProbeTargetAttachable(probe, opts);
1069
1099
 
1070
1100
  if (targetAttachable) {
1071
- const udsResult = await doAskViaDaemon(opts, {
1072
- transportSelected: "uds",
1073
- transportProbe: "ok",
1074
- });
1101
+ const daemonConfigDir = opts.configDir ?? probe.raw?.daemon?.configDir;
1102
+ const udsResult = await doAskViaDaemon(
1103
+ { ...opts, configDir: daemonConfigDir },
1104
+ {
1105
+ transportSelected: "uds",
1106
+ transportProbe: "ok",
1107
+ },
1108
+ );
1075
1109
  if (udsResult.matchedCompletion) {
1076
1110
  return udsResult;
1077
1111
  }
@@ -1139,10 +1173,11 @@ async function doInterruptViaTmux(adapter, opts) {
1139
1173
  }
1140
1174
 
1141
1175
  async function doInterruptViaDaemon(opts, meta = {}) {
1142
- const { bridgePath, timeoutMs, short, sessionId } = opts;
1176
+ const { bridgePath, timeoutMs, short, sessionId, configDir } = opts;
1143
1177
  const payload = { timeoutMs };
1144
1178
  if (short) payload.short = short;
1145
1179
  if (sessionId) payload.sessionId = sessionId;
1180
+ if (configDir) payload.configDir = configDir;
1146
1181
  const result = await callBridgeVerb(
1147
1182
  bridgePath,
1148
1183
  "daemon-interrupt",
@@ -1161,6 +1196,11 @@ async function doInterruptViaDaemon(opts, meta = {}) {
1161
1196
  inputSent: result?.inputSent === true,
1162
1197
  timedOut: result?.timedOut === true,
1163
1198
  closed: result?.closed === true,
1199
+ daemon: result?.daemon ?? null,
1200
+ daemons: result?.daemons ?? [],
1201
+ matches: result?.matches ?? [],
1202
+ candidateResults: result?.candidateResults ?? [],
1203
+ callerProvenance: result?.callerProvenance ?? null,
1164
1204
  ...(result?.error ? { error: result.error } : {}),
1165
1205
  ...meta,
1166
1206
  };
@@ -1269,6 +1309,7 @@ function askOpts(flags, adapter) {
1269
1309
  transport === "tmux" ? requireFlag(flags, "session") : flags.session,
1270
1310
  short,
1271
1311
  sessionId,
1312
+ configDir: flags["config-dir"],
1272
1313
  transport,
1273
1314
  bridgePath: resolveBridgePath(flags),
1274
1315
  prompt: requireFlag(flags, "prompt"),
@@ -1295,6 +1336,7 @@ function interruptOpts(flags, adapter) {
1295
1336
  transport === "tmux" ? requireFlag(flags, "session") : flags.session,
1296
1337
  short,
1297
1338
  sessionId,
1339
+ configDir: flags["config-dir"],
1298
1340
  transport,
1299
1341
  bridgePath: resolveBridgePath(flags),
1300
1342
  remote: flags.remote,
@@ -1326,6 +1368,7 @@ async function probe(flags) {
1326
1368
  const payload = {};
1327
1369
  if (flags.short) payload.short = flags.short;
1328
1370
  if (flags["session-id"]) payload.sessionId = flags["session-id"];
1371
+ if (flags["config-dir"]) payload.configDir = flags["config-dir"];
1329
1372
  const timeoutMs = secondsFlag(flags, "timeout", 10_000);
1330
1373
  printJson(
1331
1374
  await callBridgeVerb(
package/cto/status.mjs CHANGED
@@ -29,7 +29,61 @@ function toIsoTime(value) {
29
29
  return null;
30
30
  }
31
31
 
32
+ function shortHash(value) {
33
+ const str = String(value ?? "");
34
+ let h = 5381;
35
+ for (let i = 0; i < str.length; i++) {
36
+ h = (h * 33) ^ str.charCodeAt(i);
37
+ }
38
+ return (h >>> 0).toString(36);
39
+ }
40
+
41
+ function pathLabel(value) {
42
+ const str = String(value ?? "").replace(/[/\\]+$/u, "");
43
+ if (!str) return "";
44
+ const segments = str.split(/[/\\]+/u);
45
+ return segments[segments.length - 1] || "";
46
+ }
47
+
48
+ export function deriveRepoRootFromCwd(cwd) {
49
+ if (typeof cwd !== "string" || !cwd) return "";
50
+ if (cwd.includes("\\") || /^[A-Za-z]:/u.test(cwd)) return cwd;
51
+
52
+ const normalized = cwd.replace(/\/+$/u, "");
53
+ const claudeMarker = "/.claude/worktrees/";
54
+ const claudeIndex = normalized.indexOf(claudeMarker);
55
+ if (claudeIndex > 0) {
56
+ const suffix = normalized.slice(claudeIndex + claudeMarker.length);
57
+ if (/^[^/]+(?:\/|$)/u.test(suffix)) {
58
+ return normalized.slice(0, claudeIndex);
59
+ }
60
+ }
61
+
62
+ const codexMarker = "/.codex-swarm/";
63
+ const codexIndex = normalized.indexOf(codexMarker);
64
+ if (codexIndex > 0) {
65
+ const suffix = normalized.slice(codexIndex + codexMarker.length);
66
+ if (/^wt-[^/]+(?:\/|$)/u.test(suffix)) {
67
+ return normalized.slice(0, codexIndex);
68
+ }
69
+ }
70
+
71
+ return cwd;
72
+ }
73
+
74
+ function redactedCwdFields(cwd) {
75
+ if (typeof cwd !== "string" || !cwd) return {};
76
+ const repoRoot = deriveRepoRootFromCwd(cwd);
77
+ return {
78
+ cwdLabel: pathLabel(cwd),
79
+ cwdHash: shortHash(cwd),
80
+ repoRootLabel: pathLabel(repoRoot),
81
+ repoRootHash: shortHash(repoRoot),
82
+ };
83
+ }
84
+
32
85
  function normalizeLiveSession(session) {
86
+ const cwd = typeof session?.cwd === "string" ? session.cwd : "";
33
87
  return {
34
88
  sessionId: String(session?.sessionId || ""),
35
89
  host: typeof session?.host === "string" ? session.host : "local",
@@ -48,6 +102,7 @@ function normalizeLiveSession(session) {
48
102
  started_at: toIsoTime(
49
103
  session?.started_at ?? session?.startedAt ?? session?.lastHeartbeat,
50
104
  ),
105
+ ...redactedCwdFields(cwd),
51
106
  };
52
107
  }
53
108
 
@@ -164,7 +219,27 @@ function deriveActiveShards(current, overlay) {
164
219
  return shards.map(normalizeActiveShard).filter((shard) => shard.shard_name);
165
220
  }
166
221
 
222
+ function deriveLiveSessionGroups(liveSessions) {
223
+ const groups = new Map();
224
+ for (const session of liveSessions || []) {
225
+ if (!session?.repoRootHash) continue;
226
+ if (!groups.has(session.repoRootHash)) {
227
+ groups.set(session.repoRootHash, {
228
+ repoRootLabel: session.repoRootLabel || "",
229
+ repoRootHash: session.repoRootHash,
230
+ session_count: 0,
231
+ sessions: [],
232
+ });
233
+ }
234
+ const group = groups.get(session.repoRootHash);
235
+ group.session_count += 1;
236
+ group.sessions.push(session.sessionId);
237
+ }
238
+ return [...groups.values()];
239
+ }
240
+
167
241
  function projectStatus(current, overlay) {
242
+ const liveSessions = overlay.live_sessions;
168
243
  return {
169
244
  schema_version: current?.schema_version || SCHEMA_VERSION,
170
245
  generated_at: current?.generated_at || null,
@@ -172,7 +247,8 @@ function projectStatus(current, overlay) {
172
247
  sources: current?.sources || {},
173
248
  summary: current?.summary || {},
174
249
  ledger_tail: Array.isArray(current?.ledger_tail) ? current.ledger_tail : [],
175
- live_sessions: overlay.live_sessions,
250
+ live_sessions: liveSessions,
251
+ live_session_groups: deriveLiveSessionGroups(liveSessions),
176
252
  active_shards: deriveActiveShards(current, overlay),
177
253
  };
178
254
  }
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { argv, exit, stdin, stdout } from "node:process";
4
+ import { pathToFileURL } from "node:url";
5
+ import { drainPendingSynapse as defaultDrainPendingSynapse } from "../hub/team/synapse-http.mjs";
6
+ import {
7
+ heartbeatInteractiveSession as defaultHeartbeatInteractiveSession,
8
+ registerInteractiveSession as defaultRegisterInteractiveSession,
9
+ } from "./session-start-fast.mjs";
10
+
11
+ // hub-ensure is loaded lazily so the byte-identical packages/core mirror of this
12
+ // file loads cleanly. packages/core mirrors scripts/lib only (not scripts/*), so a
13
+ // static `../scripts/hub-ensure.mjs` import would make the core copy throw
14
+ // ERR_MODULE_NOT_FOUND at load time. Mirrors codex-session-hook.mjs's pattern.
15
+ async function defaultHubEnsureRun(stdinData) {
16
+ const { run } = await import(
17
+ new URL("../scripts/hub-ensure.mjs", import.meta.url).href
18
+ );
19
+ return run(stdinData);
20
+ }
21
+
22
+ function parsePayload(stdinData) {
23
+ try {
24
+ const raw = typeof stdinData === "string" ? stdinData : "";
25
+ return raw.trim()
26
+ ? { ok: true, payload: JSON.parse(raw) }
27
+ : { ok: false, payload: {} };
28
+ } catch {
29
+ return { ok: false, payload: {} };
30
+ }
31
+ }
32
+
33
+ // Antigravity hook payloads use camelCase system metadata (conversationId,
34
+ // workspacePaths) rather than Codex's session_id/cwd. registerInteractiveSession
35
+ // and heartbeatInteractiveSession parse the Codex shape, so we adapt the agy
36
+ // payload into that shape before delegating. conversationId is the stable
37
+ // per-conversation UUID (== session identity); the first mounted workspace path
38
+ // is the effective cwd.
39
+ export function toSessionPayload(payload) {
40
+ const sessionId = String(payload?.conversationId || "").trim();
41
+ const workspacePaths = Array.isArray(payload?.workspacePaths)
42
+ ? payload.workspacePaths
43
+ : [];
44
+ const cwd =
45
+ typeof workspacePaths[0] === "string" && workspacePaths[0]
46
+ ? workspacePaths[0]
47
+ : process.cwd();
48
+ return JSON.stringify({ session_id: sessionId, cwd });
49
+ }
50
+
51
+ // agy has no distinct SessionStart / UserPromptSubmit events. PreInvocation
52
+ // fires before every model call, so the per-conversation invocationNum gates
53
+ // register (first call == session start) vs heartbeat (subsequent calls).
54
+ // An explicit argv mode (register|heartbeat) overrides, mirroring the codex hook.
55
+ export function normalizeMode(argvMode, payload) {
56
+ const direct = String(argvMode || "")
57
+ .trim()
58
+ .toLowerCase();
59
+ if (direct === "register" || direct === "heartbeat") return direct;
60
+
61
+ const invocationNum = Number(payload?.invocationNum);
62
+ if (Number.isFinite(invocationNum)) {
63
+ return invocationNum <= 1 ? "register" : "heartbeat";
64
+ }
65
+ // Unknown invocation index: register is idempotent and also ensures the hub,
66
+ // so it is the safe default for a first-contact payload.
67
+ return "register";
68
+ }
69
+
70
+ export async function runAgySessionHook(stdinData, opts = {}) {
71
+ const output = "{}\n";
72
+ const parsed = parsePayload(stdinData);
73
+ if (!parsed.ok) {
74
+ if (opts.writeStdout !== false) stdout.write(output);
75
+ return output;
76
+ }
77
+
78
+ const sessionPayload = toSessionPayload(parsed.payload);
79
+ const sessionId = JSON.parse(sessionPayload).session_id;
80
+ // No conversation id means nothing to register; stay a silent no-op.
81
+ if (!sessionId) {
82
+ if (opts.writeStdout !== false) stdout.write(output);
83
+ return output;
84
+ }
85
+
86
+ const mode = normalizeMode(opts.argvMode ?? argv[2], parsed.payload);
87
+ const hubEnsureRun = opts.hubEnsureRun || defaultHubEnsureRun;
88
+ const registerInteractiveSession =
89
+ opts.registerInteractiveSession || defaultRegisterInteractiveSession;
90
+ const heartbeatInteractiveSession =
91
+ opts.heartbeatInteractiveSession || defaultHeartbeatInteractiveSession;
92
+ const drainPendingSynapse =
93
+ opts.drainPendingSynapse || defaultDrainPendingSynapse;
94
+
95
+ try {
96
+ if (mode === "register") {
97
+ try {
98
+ await hubEnsureRun(sessionPayload);
99
+ } catch {}
100
+ try {
101
+ registerInteractiveSession(sessionPayload);
102
+ } catch {}
103
+ try {
104
+ await drainPendingSynapse(1000);
105
+ } catch {}
106
+ } else if (mode === "heartbeat") {
107
+ try {
108
+ heartbeatInteractiveSession(sessionPayload);
109
+ } catch {}
110
+ try {
111
+ await drainPendingSynapse(500);
112
+ } catch {}
113
+ }
114
+ } catch {
115
+ // agy session hooks are observational and must never block the session.
116
+ }
117
+
118
+ if (opts.writeStdout !== false) {
119
+ stdout.write(output);
120
+ }
121
+ return output;
122
+ }
123
+
124
+ function readStdin() {
125
+ return new Promise((resolve) => {
126
+ let data = "";
127
+ stdin.setEncoding("utf8");
128
+ stdin.on("data", (chunk) => {
129
+ data += chunk;
130
+ });
131
+ stdin.on("end", () => resolve(data));
132
+ stdin.on("error", () => resolve(data));
133
+ });
134
+ }
135
+
136
+ if (argv[1] && import.meta.url === pathToFileURL(argv[1]).href) {
137
+ const stdinData = await readStdin();
138
+ await runAgySessionHook(stdinData);
139
+ exit(0);
140
+ }
package/hub/bridge.mjs CHANGED
@@ -1186,6 +1186,36 @@ function daemonAttachErrorResult(error) {
1186
1186
  };
1187
1187
  }
1188
1188
 
1189
+ function daemonAttachProbeFailureResult(probe) {
1190
+ return {
1191
+ ok: false,
1192
+ text: "",
1193
+ raw: "",
1194
+ responseRaw: "",
1195
+ matchedCompletion: false,
1196
+ timedOut: false,
1197
+ closed: false,
1198
+ inputSent: false,
1199
+ error: probe?.error || probe?.reason || "Claude daemon unavailable",
1200
+ reason: probe?.reason || "daemon-unavailable",
1201
+ daemon: probe?.daemon ?? null,
1202
+ daemons: probe?.daemons ?? [],
1203
+ matches: probe?.matches ?? [],
1204
+ candidateResults: probe?.candidateResults ?? [],
1205
+ callerProvenance: probe?.callerProvenance ?? null,
1206
+ };
1207
+ }
1208
+
1209
+ function daemonProbeMetadata(probe) {
1210
+ return {
1211
+ daemon: probe?.daemon ?? null,
1212
+ daemons: probe?.daemons ?? [],
1213
+ matches: probe?.matches ?? [],
1214
+ candidateResults: probe?.candidateResults ?? [],
1215
+ callerProvenance: probe?.callerProvenance ?? null,
1216
+ };
1217
+ }
1218
+
1189
1219
  function daemonInterruptErrorResult(error) {
1190
1220
  return {
1191
1221
  ok: false,
@@ -1197,6 +1227,22 @@ function daemonInterruptErrorResult(error) {
1197
1227
  };
1198
1228
  }
1199
1229
 
1230
+ function daemonInterruptProbeFailureResult(probe) {
1231
+ return {
1232
+ ok: false,
1233
+ done: false,
1234
+ aborted: false,
1235
+ reason: probe?.reason || "interrupt_failed",
1236
+ inputSent: false,
1237
+ error: probe?.error || probe?.reason || "Claude daemon unavailable",
1238
+ daemon: probe?.daemon ?? null,
1239
+ daemons: probe?.daemons ?? [],
1240
+ matches: probe?.matches ?? [],
1241
+ candidateResults: probe?.candidateResults ?? [],
1242
+ callerProvenance: probe?.callerProvenance ?? null,
1243
+ };
1244
+ }
1245
+
1200
1246
  function numericOption(value, fallback) {
1201
1247
  const parsed = Number(value);
1202
1248
  return Number.isFinite(parsed) ? parsed : fallback;
@@ -1205,35 +1251,15 @@ function numericOption(value, fallback) {
1205
1251
  async function cmdDaemonProbe(args) {
1206
1252
  try {
1207
1253
  const payload = readBridgePayload(args);
1208
- const {
1209
- deriveClaudeDaemonPaths,
1210
- extractClaudeAgentSessions,
1211
- findDaemonJobBySessionId,
1212
- findDaemonJobByShort,
1213
- sendClaudeControlRequest,
1214
- } = await loadDaemonControl();
1215
- const daemonPaths = deriveClaudeDaemonPaths({
1254
+ const { probeClaudeDaemonCandidates } = await loadDaemonControl();
1255
+ const probe = await probeClaudeDaemonCandidates({
1216
1256
  configDir: payload.configDir,
1257
+ env: process.env,
1258
+ short: payload.short,
1259
+ sessionId: payload.sessionId,
1260
+ timeoutMs: numericOption(payload.timeoutMs, 6000),
1217
1261
  });
1218
- const list = await sendClaudeControlRequest(
1219
- daemonPaths.controlSock,
1220
- { proto: 1, op: "list" },
1221
- { timeoutMs: numericOption(payload.timeoutMs, 6000) },
1222
- );
1223
- const target = payload.sessionId
1224
- ? findDaemonJobBySessionId(list, payload.sessionId)
1225
- : payload.short
1226
- ? findDaemonJobByShort(list, payload.short)
1227
- : null;
1228
- const sessions = extractClaudeAgentSessions(list);
1229
-
1230
- return emitJson({
1231
- ok: list?.ok !== false,
1232
- controlSock: daemonPaths.controlSock,
1233
- sessions,
1234
- target: target || undefined,
1235
- error: list?.ok === false ? list?.error : undefined,
1236
- });
1262
+ return emitJson(probe);
1237
1263
  } catch (error) {
1238
1264
  return emitJson(daemonErrorResult(error));
1239
1265
  }
@@ -1246,33 +1272,49 @@ async function cmdDaemonAttach(args) {
1246
1272
 
1247
1273
  const {
1248
1274
  attachClaudeDaemonSession,
1249
- deriveClaudeDaemonPaths,
1250
- findDaemonJobBySessionId,
1251
- sendClaudeControlRequest,
1275
+ buildDaemonControlAuth,
1276
+ probeClaudeDaemonCandidates,
1252
1277
  } = await loadDaemonControl();
1253
- const daemonPaths = deriveClaudeDaemonPaths({
1278
+ const probe = await probeClaudeDaemonCandidates({
1254
1279
  configDir: payload.configDir,
1280
+ env: process.env,
1281
+ short: payload.short,
1282
+ sessionId: payload.sessionId,
1283
+ timeoutMs: numericOption(payload.timeoutMs, 6000),
1255
1284
  });
1256
- let short = payload.short;
1257
- if (!short && payload.sessionId) {
1258
- const list = await sendClaudeControlRequest(
1259
- daemonPaths.controlSock,
1260
- { proto: 1, op: "list" },
1261
- { timeoutMs: numericOption(payload.timeoutMs, 6000) },
1285
+ if (!probe.ok) return emitJson(daemonAttachProbeFailureResult(probe));
1286
+ const short = payload.short ?? probe.target?.short;
1287
+ if (!short) {
1288
+ return emitJson(
1289
+ daemonAttachProbeFailureResult({
1290
+ ...probe,
1291
+ ok: false,
1292
+ reason: "target-not-found",
1293
+ error: "short or resolvable sessionId is required",
1294
+ }),
1262
1295
  );
1263
- const target = findDaemonJobBySessionId(list, payload.sessionId);
1264
- short = target?.short;
1265
1296
  }
1266
- if (!short) throw new Error("short or resolvable sessionId is required");
1267
-
1268
- const result = await attachClaudeDaemonSession({
1269
- controlSock: daemonPaths.controlSock,
1270
- short,
1271
- input: payload.prompt,
1272
- cols: numericOption(payload.cols, undefined),
1273
- rows: numericOption(payload.rows, undefined),
1274
- timeoutMs: numericOption(payload.timeoutMs, 30_000),
1275
- });
1297
+ const controlAuth = await buildDaemonControlAuth(
1298
+ probe.daemon?.configDir ?? payload.configDir,
1299
+ );
1300
+
1301
+ let result;
1302
+ try {
1303
+ result = await attachClaudeDaemonSession({
1304
+ controlSock: probe.controlSock,
1305
+ short,
1306
+ input: payload.prompt,
1307
+ ...controlAuth,
1308
+ cols: numericOption(payload.cols, undefined),
1309
+ rows: numericOption(payload.rows, undefined),
1310
+ timeoutMs: numericOption(payload.timeoutMs, 30_000),
1311
+ });
1312
+ } catch (error) {
1313
+ return emitJson({
1314
+ ...daemonAttachErrorResult(error),
1315
+ ...daemonProbeMetadata(probe),
1316
+ });
1317
+ }
1276
1318
 
1277
1319
  return emitJson({
1278
1320
  ok: result.matchedCompletion === true,
@@ -1285,6 +1327,7 @@ async function cmdDaemonAttach(args) {
1285
1327
  inputSent: result.inputSent === true,
1286
1328
  error:
1287
1329
  result.handshake?.ok === false ? result.handshake?.error : undefined,
1330
+ ...daemonProbeMetadata(probe),
1288
1331
  });
1289
1332
  } catch (error) {
1290
1333
  return emitJson(daemonAttachErrorResult(error));
@@ -1296,33 +1339,49 @@ async function cmdDaemonInterrupt(args) {
1296
1339
  const payload = readBridgePayload(args);
1297
1340
 
1298
1341
  const {
1299
- deriveClaudeDaemonPaths,
1300
- findDaemonJobBySessionId,
1342
+ buildDaemonControlAuth,
1301
1343
  interruptClaudeDaemonSession,
1302
- sendClaudeControlRequest,
1344
+ probeClaudeDaemonCandidates,
1303
1345
  } = await loadDaemonControl();
1304
- const daemonPaths = deriveClaudeDaemonPaths({
1346
+ const probe = await probeClaudeDaemonCandidates({
1305
1347
  configDir: payload.configDir,
1348
+ env: process.env,
1349
+ short: payload.short,
1350
+ sessionId: payload.sessionId,
1351
+ timeoutMs: numericOption(payload.timeoutMs, 6000),
1306
1352
  });
1307
- let short = payload.short;
1308
- if (!short && payload.sessionId) {
1309
- const list = await sendClaudeControlRequest(
1310
- daemonPaths.controlSock,
1311
- { proto: 1, op: "list" },
1312
- { timeoutMs: numericOption(payload.timeoutMs, 6000) },
1353
+ if (!probe.ok) return emitJson(daemonInterruptProbeFailureResult(probe));
1354
+ const short = payload.short ?? probe.target?.short;
1355
+ if (!short) {
1356
+ return emitJson(
1357
+ daemonInterruptProbeFailureResult({
1358
+ ...probe,
1359
+ ok: false,
1360
+ reason: "target-not-found",
1361
+ error: "short or resolvable sessionId is required",
1362
+ }),
1313
1363
  );
1314
- const target = findDaemonJobBySessionId(list, payload.sessionId);
1315
- short = target?.short;
1316
1364
  }
1317
- if (!short) throw new Error("short or resolvable sessionId is required");
1318
-
1319
- const result = await interruptClaudeDaemonSession({
1320
- controlSock: daemonPaths.controlSock,
1321
- short,
1322
- cols: numericOption(payload.cols, undefined),
1323
- rows: numericOption(payload.rows, undefined),
1324
- timeoutMs: numericOption(payload.timeoutMs, 5000),
1325
- });
1365
+ const controlAuth = await buildDaemonControlAuth(
1366
+ probe.daemon?.configDir ?? payload.configDir,
1367
+ );
1368
+
1369
+ let result;
1370
+ try {
1371
+ result = await interruptClaudeDaemonSession({
1372
+ controlSock: probe.controlSock,
1373
+ short,
1374
+ ...controlAuth,
1375
+ cols: numericOption(payload.cols, undefined),
1376
+ rows: numericOption(payload.rows, undefined),
1377
+ timeoutMs: numericOption(payload.timeoutMs, 5000),
1378
+ });
1379
+ } catch (error) {
1380
+ return emitJson({
1381
+ ...daemonInterruptErrorResult(error),
1382
+ ...daemonProbeMetadata(probe),
1383
+ });
1384
+ }
1326
1385
  const aborted = result.inputSent === true;
1327
1386
 
1328
1387
  return emitJson({
@@ -1336,6 +1395,7 @@ async function cmdDaemonInterrupt(args) {
1336
1395
  inputSent: result.inputSent === true,
1337
1396
  error:
1338
1397
  result.handshake?.ok === false ? result.handshake?.error : undefined,
1398
+ ...daemonProbeMetadata(probe),
1339
1399
  });
1340
1400
  } catch (error) {
1341
1401
  return emitJson(daemonInterruptErrorResult(error));