@triflux/remote 10.48.1 → 10.50.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/hub/server.mjs +0 -122
- package/hub/team/claude-daemon-control.mjs +54 -6
- package/hub/team/claude-transcript.mjs +240 -0
- package/hub/team/cli/commands/start/index.mjs +17 -3
- package/hub/team/cli/commands/start/parse-args.mjs +1 -0
- package/hub/team/session-context.mjs +203 -0
- package/hub/team/uds-orchestrator.mjs +63 -1
- package/hub/team/worktree-lifecycle.mjs +0 -1
- package/package.json +2 -3
- package/scripts/lib/doctor-env-checks.mjs +0 -85
- package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
- package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
- package/scripts/lib/mcp-guard-engine.mjs +44 -271
- package/scripts/lib/mcp-manifest.mjs +2 -2
- package/hub/mac-focus.mjs +0 -94
- package/hub/mac-tray.swift +0 -144
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/public/tray.html +0 -756
- package/hub/tray-lifecycle.mjs +0 -138
- package/hub/tray-runtime.mjs +0 -265
- package/hub/tray-state.mjs +0 -524
- package/hub/tray.mjs +0 -527
- package/scripts/lib/cross-review-utils.mjs +0 -59
- package/scripts/lib/hook-utils.mjs +0 -16
- package/scripts/lib/keyword-rules.mjs +0 -219
- package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
- package/scripts/lib/mcp-gateway-servers.mjs +0 -70
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +0 -35
package/hub/server.mjs
CHANGED
|
@@ -22,13 +22,7 @@ import {
|
|
|
22
22
|
ListToolsRequestSchema,
|
|
23
23
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
24
24
|
import { runCollect } from "../cto/collect.mjs";
|
|
25
|
-
import { resolveLakeRootDir } from "../cto/lake-root.mjs";
|
|
26
|
-
import { runStatus as runCtoStatus } from "../cto/status.mjs";
|
|
27
25
|
import { createModuleLogger } from "../scripts/lib/logger.mjs";
|
|
28
|
-
import {
|
|
29
|
-
inspectRegistry,
|
|
30
|
-
inspectRegistryStatus,
|
|
31
|
-
} from "../scripts/lib/mcp-guard-engine.mjs";
|
|
32
26
|
import { broker as brokerInstance, reloadBroker } from "@triflux/core/hub/account-broker.mjs";
|
|
33
27
|
import { createAdaptiveEngine } from "@triflux/core/hub/adaptive.mjs";
|
|
34
28
|
import { createAssignCallbackServer } from "@triflux/core/hub/assign-callbacks.mjs";
|
|
@@ -56,7 +50,6 @@ import {
|
|
|
56
50
|
recordWorker,
|
|
57
51
|
snapshot as traceSnapshot,
|
|
58
52
|
} from "@triflux/core/hub/lib/trace-recorder.mjs";
|
|
59
|
-
import { focusSessionOnMac } from "./mac-focus.mjs";
|
|
60
53
|
import { logQuotaRefreshFailures } from "@triflux/core/hub/middleware/quota-middleware.mjs";
|
|
61
54
|
import { wrapRequestHandler } from "@triflux/core/hub/middleware/request-logger.mjs";
|
|
62
55
|
import { createPipeServer } from "./pipe.mjs";
|
|
@@ -79,9 +72,6 @@ import {
|
|
|
79
72
|
} from "./team/synapse-registry.mjs";
|
|
80
73
|
import { registerTeamBridge } from "@triflux/core/hub/team-bridge.mjs";
|
|
81
74
|
import { createTools } from "./tools.mjs";
|
|
82
|
-
import { spawnTrayForHub } from "./tray-lifecycle.mjs";
|
|
83
|
-
import { getRuntimeStatus } from "./tray-runtime.mjs";
|
|
84
|
-
import { buildTrayStatePayload } from "./tray-state.mjs";
|
|
85
75
|
import { createDelegatorMcpWorker } from "./workers/delegator-mcp.mjs";
|
|
86
76
|
|
|
87
77
|
registerTeamBridge(nativeProxy);
|
|
@@ -101,7 +91,6 @@ const LOOPBACK_REMOTE_ADDRESSES = new Set([
|
|
|
101
91
|
const ALLOWED_ORIGIN_RE =
|
|
102
92
|
/^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i;
|
|
103
93
|
const PROJECT_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
104
|
-
const CANONICAL_PROJECT_ROOT = resolveLakeRootDir(PROJECT_ROOT) || PROJECT_ROOT;
|
|
105
94
|
const PUBLIC_DIR = resolve(join(PROJECT_ROOT, "hub", "public"));
|
|
106
95
|
const CACHE_DIR = join(homedir(), ".claude", "cache");
|
|
107
96
|
const HUB_DEFAULT_PORT = 27888;
|
|
@@ -498,7 +487,6 @@ function isPublicPath(path) {
|
|
|
498
487
|
return (
|
|
499
488
|
PUBLIC_PATHS.has(path) ||
|
|
500
489
|
path === "/dashboard" ||
|
|
501
|
-
path === "/tray.html" ||
|
|
502
490
|
path === "/api/qos-stats" ||
|
|
503
491
|
path.startsWith("/public/")
|
|
504
492
|
);
|
|
@@ -861,41 +849,10 @@ function getBrokerPublicSnapshot(currentBroker = brokerInstance) {
|
|
|
861
849
|
return currentBroker.publicSnapshot();
|
|
862
850
|
}
|
|
863
851
|
|
|
864
|
-
async function getTrayCtoStatus() {
|
|
865
|
-
try {
|
|
866
|
-
return await runCtoStatus(["--json"], {
|
|
867
|
-
rootDir: CANONICAL_PROJECT_ROOT,
|
|
868
|
-
stdout: { write() {} },
|
|
869
|
-
});
|
|
870
|
-
} catch (error) {
|
|
871
|
-
return { error: error?.message || String(error) };
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function getTrayMcpStatus() {
|
|
876
|
-
const registryState = inspectRegistry();
|
|
877
|
-
if (!registryState.exists || !registryState.valid) {
|
|
878
|
-
return {
|
|
879
|
-
registry_path: registryState.path,
|
|
880
|
-
server_count: 0,
|
|
881
|
-
rows: [],
|
|
882
|
-
error: registryState.errors?.join("; ") || "MCP registry unavailable",
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
const status = inspectRegistryStatus(registryState.registry);
|
|
886
|
-
return {
|
|
887
|
-
registry_path: registryState.path,
|
|
888
|
-
server_count: Object.keys(registryState.registry?.servers || {}).length,
|
|
889
|
-
rows: status.rows,
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
|
|
893
852
|
function resolvePublicFilePath(path) {
|
|
894
853
|
let relativePath = null;
|
|
895
854
|
if (path === "/dashboard") {
|
|
896
855
|
relativePath = "dashboard.html";
|
|
897
|
-
} else if (path === "/tray.html") {
|
|
898
|
-
relativePath = "tray.html";
|
|
899
856
|
} else if (path.startsWith("/public/")) {
|
|
900
857
|
relativePath = path.slice("/public/".length);
|
|
901
858
|
}
|
|
@@ -1441,76 +1398,6 @@ export async function startHub({
|
|
|
1441
1398
|
});
|
|
1442
1399
|
}
|
|
1443
1400
|
|
|
1444
|
-
if (path === "/api/tray-state" && req.method === "GET") {
|
|
1445
|
-
if (!isLoopbackRemoteAddress(req.socket.remoteAddress)) {
|
|
1446
|
-
return writeJson(res, 403, { ok: false, error: "Loopback only" });
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
const qos = getQosStatsPayload();
|
|
1450
|
-
const synapseSnapshot = synapseRegistry.snapshot();
|
|
1451
|
-
const broker = getBrokerPublicSnapshot(brokerInstance);
|
|
1452
|
-
const runtimeStatus = getRuntimeStatus({
|
|
1453
|
-
projectRoots: [
|
|
1454
|
-
...new Set([CANONICAL_PROJECT_ROOT, PROJECT_ROOT].filter(Boolean)),
|
|
1455
|
-
],
|
|
1456
|
-
});
|
|
1457
|
-
const [ctoStatus, mcpStatus, hubStatus] = await Promise.all([
|
|
1458
|
-
getTrayCtoStatus(),
|
|
1459
|
-
Promise.resolve(getTrayMcpStatus()),
|
|
1460
|
-
pipe.executeQuery("status", {
|
|
1461
|
-
scope: "hub",
|
|
1462
|
-
include_metrics: true,
|
|
1463
|
-
}),
|
|
1464
|
-
]);
|
|
1465
|
-
const mergedCtoStatus = {
|
|
1466
|
-
...(ctoStatus || {}),
|
|
1467
|
-
roles: hubStatus?.data?.roles || ctoStatus?.roles || {},
|
|
1468
|
-
};
|
|
1469
|
-
|
|
1470
|
-
return writeJson(
|
|
1471
|
-
res,
|
|
1472
|
-
200,
|
|
1473
|
-
buildTrayStatePayload({
|
|
1474
|
-
hub: {
|
|
1475
|
-
id: String(sessionId),
|
|
1476
|
-
pid: process.pid,
|
|
1477
|
-
port,
|
|
1478
|
-
url: buildHubUrl(host, port),
|
|
1479
|
-
projectRoot: CANONICAL_PROJECT_ROOT,
|
|
1480
|
-
},
|
|
1481
|
-
qos,
|
|
1482
|
-
synapseSnapshot,
|
|
1483
|
-
broker,
|
|
1484
|
-
ctoStatus: mergedCtoStatus,
|
|
1485
|
-
mcpStatus,
|
|
1486
|
-
runtimeStatus,
|
|
1487
|
-
}),
|
|
1488
|
-
);
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
if (path === "/api/focus-session" && req.method === "POST") {
|
|
1492
|
-
if (!isLoopbackRemoteAddress(req.socket.remoteAddress)) {
|
|
1493
|
-
return writeJson(res, 403, { ok: false, error: "Loopback only" });
|
|
1494
|
-
}
|
|
1495
|
-
let body;
|
|
1496
|
-
try {
|
|
1497
|
-
body = await parseBody(req);
|
|
1498
|
-
} catch (_e) {
|
|
1499
|
-
return writeJson(res, 400, {
|
|
1500
|
-
error: "Invalid JSON or Body too large",
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
const { sessionId, udsId } = body || {};
|
|
1505
|
-
const result = await focusSessionOnMac({
|
|
1506
|
-
sessionId,
|
|
1507
|
-
udsId,
|
|
1508
|
-
pid: body?.pid,
|
|
1509
|
-
agentId: body?.agentId ?? body?.agent_id,
|
|
1510
|
-
});
|
|
1511
|
-
return writeJson(res, 200, { ok: result.ok !== false, focus: result });
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
1401
|
// Redacted peer-discovery surface. Returns co-located live peers (same
|
|
1515
1402
|
// cwd / worktree) with raw cwd/pid stripped — only label/hash + booleans.
|
|
1516
1403
|
if (path === "/synapse/peers" && req.method === "GET") {
|
|
@@ -3110,15 +2997,6 @@ if (selfRun) {
|
|
|
3110
2997
|
process.exit(0);
|
|
3111
2998
|
return;
|
|
3112
2999
|
}
|
|
3113
|
-
try {
|
|
3114
|
-
const trayPath = fileURLToPath(new URL("./tray.mjs", import.meta.url));
|
|
3115
|
-
const tray = spawnTrayForHub({ trayPath });
|
|
3116
|
-
if (tray.status === "started") {
|
|
3117
|
-
hubLog.info({ pid: tray.pid }, "tray.auto_started");
|
|
3118
|
-
}
|
|
3119
|
-
} catch (error) {
|
|
3120
|
-
hubLog.warn({ err: error }, "tray.auto_start_failed");
|
|
3121
|
-
}
|
|
3122
3000
|
const shutdown = async (signal) => {
|
|
3123
3001
|
hubLog.info({ signal }, "hub.stopping");
|
|
3124
3002
|
try {
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
removeClaudeSessionProjection,
|
|
14
14
|
writeClaudeSessionProjection,
|
|
15
15
|
} from "./claude-session-projection.mjs";
|
|
16
|
+
import {
|
|
17
|
+
findClaudeTranscript,
|
|
18
|
+
readClaudeTranscript,
|
|
19
|
+
} from "./claude-transcript.mjs";
|
|
16
20
|
|
|
17
21
|
export function resolveClaudeConfigDir(env = process.env) {
|
|
18
22
|
if (env.CLAUDE_CONFIG_DIR) return path.resolve(env.CLAUDE_CONFIG_DIR);
|
|
@@ -223,6 +227,21 @@ function sessionsFromDaemonList(listResponse) {
|
|
|
223
227
|
return extractClaudeAgentSessions(listResponse);
|
|
224
228
|
}
|
|
225
229
|
|
|
230
|
+
async function addSessionContexts(sessions, candidate) {
|
|
231
|
+
return await Promise.all(
|
|
232
|
+
sessions.map(async (session) => {
|
|
233
|
+
const transcript = await findClaudeTranscript({
|
|
234
|
+
configDir: candidate.configDir,
|
|
235
|
+
sourceConfigDir: candidate.sourceConfigDir,
|
|
236
|
+
sessionId: session.sessionId,
|
|
237
|
+
cwd: session.cwd,
|
|
238
|
+
});
|
|
239
|
+
const snapshot = await readClaudeTranscript(transcript).catch(() => null);
|
|
240
|
+
return { ...session, context: snapshot?.context ?? null };
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
226
245
|
function errorMessage(error) {
|
|
227
246
|
return error?.message || String(error);
|
|
228
247
|
}
|
|
@@ -322,12 +341,21 @@ export async function probeClaudeDaemonCandidates({
|
|
|
322
341
|
sessionId,
|
|
323
342
|
timeoutMs = 6000,
|
|
324
343
|
tmpRoot = "/tmp",
|
|
344
|
+
includeContext = false,
|
|
345
|
+
candidateSourceConfigDir,
|
|
325
346
|
} = {}) {
|
|
326
|
-
const
|
|
347
|
+
const discovered = await buildClaudeDaemonDiscoveryCandidates({
|
|
327
348
|
configDir,
|
|
328
349
|
env,
|
|
329
350
|
tmpRoot,
|
|
330
351
|
});
|
|
352
|
+
const candidates = candidateSourceConfigDir
|
|
353
|
+
? discovered.filter(
|
|
354
|
+
(candidate) =>
|
|
355
|
+
(candidate.sourceConfigDir ?? candidate.configDir) ===
|
|
356
|
+
candidateSourceConfigDir,
|
|
357
|
+
)
|
|
358
|
+
: discovered;
|
|
331
359
|
const callerProvenance = detectCallerProvenance(env);
|
|
332
360
|
const targetRequested = Boolean(short || sessionId);
|
|
333
361
|
const results = [];
|
|
@@ -339,7 +367,10 @@ export async function probeClaudeDaemonCandidates({
|
|
|
339
367
|
{ proto: 1, op: "list" },
|
|
340
368
|
{ timeoutMs },
|
|
341
369
|
);
|
|
342
|
-
const
|
|
370
|
+
const normalized = sessionsFromDaemonList(list);
|
|
371
|
+
const sessions = includeContext
|
|
372
|
+
? await addSessionContexts(normalized, candidate)
|
|
373
|
+
: normalized;
|
|
343
374
|
const ok = list?.ok !== false;
|
|
344
375
|
results.push({
|
|
345
376
|
ok,
|
|
@@ -1038,10 +1069,10 @@ function defaultClaudeAttachCompletionMatched(
|
|
|
1038
1069
|
);
|
|
1039
1070
|
}
|
|
1040
1071
|
|
|
1041
|
-
function writeClaudeAttachInput(socket, input) {
|
|
1072
|
+
function writeClaudeAttachInput(socket, input, onSubmitted) {
|
|
1042
1073
|
socket.write("\x15");
|
|
1043
1074
|
socket.write(`\x1b[200~${String(input)}\x1b[201~`);
|
|
1044
|
-
socket.write("\r");
|
|
1075
|
+
socket.write("\r", onSubmitted);
|
|
1045
1076
|
}
|
|
1046
1077
|
|
|
1047
1078
|
function requiresPostInputTransition(handshake) {
|
|
@@ -1066,6 +1097,7 @@ export function attachClaudeDaemonSession({
|
|
|
1066
1097
|
timeoutMs = 30_000,
|
|
1067
1098
|
completionQuiescenceMs = DEFAULT_ATTACH_COMPLETION_QUIESCENCE_MS,
|
|
1068
1099
|
completionMatched = defaultClaudeAttachCompletionMatched,
|
|
1100
|
+
noWait = false,
|
|
1069
1101
|
} = {}) {
|
|
1070
1102
|
if (!controlSock) throw new Error("controlSock is required");
|
|
1071
1103
|
return new Promise((resolve, reject) => {
|
|
@@ -1198,8 +1230,24 @@ export function attachClaudeDaemonSession({
|
|
|
1198
1230
|
{ cols },
|
|
1199
1231
|
);
|
|
1200
1232
|
responseStartOffset = stream.length;
|
|
1201
|
-
|
|
1202
|
-
|
|
1233
|
+
inputSent = noWait ? null : true;
|
|
1234
|
+
writeClaudeAttachInput(
|
|
1235
|
+
socket,
|
|
1236
|
+
input,
|
|
1237
|
+
noWait
|
|
1238
|
+
? (error) => {
|
|
1239
|
+
if (error) fail(error);
|
|
1240
|
+
else {
|
|
1241
|
+
inputSent = true;
|
|
1242
|
+
finish({
|
|
1243
|
+
timedOut: false,
|
|
1244
|
+
matchedCompletion: false,
|
|
1245
|
+
closed: false,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
: undefined,
|
|
1250
|
+
);
|
|
1203
1251
|
}, initialDrainMs);
|
|
1204
1252
|
}
|
|
1205
1253
|
} else {
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
5
|
+
import { modelContext } from "./session-context.mjs";
|
|
6
|
+
|
|
7
|
+
const transcriptCache = new Map();
|
|
8
|
+
|
|
9
|
+
export async function findClaudeTranscript({
|
|
10
|
+
configDir,
|
|
11
|
+
sourceConfigDir,
|
|
12
|
+
sessionId,
|
|
13
|
+
cwd,
|
|
14
|
+
}) {
|
|
15
|
+
if (!configDir || !sessionId || /[/\\]/u.test(sessionId)) return null;
|
|
16
|
+
for (const dir of new Set([configDir, sourceConfigDir].filter(Boolean))) {
|
|
17
|
+
const projectsDir = path.join(dir, "projects");
|
|
18
|
+
const preferred = cwd
|
|
19
|
+
? path.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-")
|
|
20
|
+
: null;
|
|
21
|
+
const projects = [
|
|
22
|
+
...(preferred ? [preferred] : []),
|
|
23
|
+
...(await fs.readdir(projectsDir).catch(() => [])).filter(
|
|
24
|
+
(project) => project !== preferred,
|
|
25
|
+
),
|
|
26
|
+
];
|
|
27
|
+
for (const project of projects) {
|
|
28
|
+
const candidate = path.join(projectsDir, project, `${sessionId}.jsonl`);
|
|
29
|
+
try {
|
|
30
|
+
await fs.access(candidate);
|
|
31
|
+
return candidate;
|
|
32
|
+
} catch {
|
|
33
|
+
// Continue looking for this session.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function messageText(content) {
|
|
41
|
+
if (typeof content === "string") return content;
|
|
42
|
+
if (!Array.isArray(content)) return "";
|
|
43
|
+
return content
|
|
44
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
45
|
+
.map((part) => part.text)
|
|
46
|
+
.join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function beginTurn(state) {
|
|
50
|
+
state.userSeen = true;
|
|
51
|
+
state.queued = false;
|
|
52
|
+
state.response = "";
|
|
53
|
+
state.error = null;
|
|
54
|
+
state.turnEnded = false;
|
|
55
|
+
state.sectionClosed = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function acceptEntry(state, entry, marker) {
|
|
59
|
+
if (entry?.isSidechain) return;
|
|
60
|
+
const message = entry?.message;
|
|
61
|
+
if (entry?.type === "system" && entry.subtype === "compact_boundary") {
|
|
62
|
+
state.compactCount += 1;
|
|
63
|
+
state.compact = {
|
|
64
|
+
preTokens: entry.compactMetadata?.preTokens ?? null,
|
|
65
|
+
postTokens: entry.compactMetadata?.postTokens ?? null,
|
|
66
|
+
};
|
|
67
|
+
state.estimatedContextTokens = state.compact.postTokens;
|
|
68
|
+
state.measuredAt = entry.timestamp || null;
|
|
69
|
+
}
|
|
70
|
+
if (entry?.type === "assistant") {
|
|
71
|
+
if (entry.isApiErrorMessage === true || message?.model === "<synthetic>") {
|
|
72
|
+
if (state.userSeen && !state.sectionClosed && entry.isApiErrorMessage) {
|
|
73
|
+
state.error = messageText(message?.content) || "Claude API error";
|
|
74
|
+
state.turnEnded = true;
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const usage = message?.usage;
|
|
79
|
+
if (usage) {
|
|
80
|
+
state.estimatedContextTokens =
|
|
81
|
+
(usage.input_tokens || 0) +
|
|
82
|
+
(usage.cache_read_input_tokens || 0) +
|
|
83
|
+
(usage.cache_creation_input_tokens || 0);
|
|
84
|
+
state.model = message.model || state.model;
|
|
85
|
+
state.measuredAt = entry.timestamp || state.measuredAt;
|
|
86
|
+
}
|
|
87
|
+
if (state.userSeen && !state.sectionClosed) {
|
|
88
|
+
const text = messageText(message?.content);
|
|
89
|
+
if (text) state.response = text;
|
|
90
|
+
if (message?.stop_reason === "end_turn") state.turnEnded = true;
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (entry?.type === "user") {
|
|
95
|
+
const content = message?.content;
|
|
96
|
+
// Tool results are not a new human turn.
|
|
97
|
+
const humanText =
|
|
98
|
+
typeof content === "string" ||
|
|
99
|
+
(Array.isArray(content) && content.some((part) => part?.type === "text"))
|
|
100
|
+
? messageText(content)
|
|
101
|
+
: "";
|
|
102
|
+
const marked =
|
|
103
|
+
marker &&
|
|
104
|
+
(humanText.includes(marker) ||
|
|
105
|
+
JSON.stringify(
|
|
106
|
+
entry.attachments ?? message?.attachments ?? [],
|
|
107
|
+
).includes(marker));
|
|
108
|
+
if (humanText || marked) {
|
|
109
|
+
if (!marker || marked) beginTurn(state);
|
|
110
|
+
else if (state.userSeen) state.sectionClosed = true;
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
entry?.type === "attachment" &&
|
|
116
|
+
entry.attachment?.type === "queued_command" &&
|
|
117
|
+
marker &&
|
|
118
|
+
JSON.stringify(entry.attachment).includes(marker)
|
|
119
|
+
) {
|
|
120
|
+
beginTurn(state);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (entry?.type === "queue-operation") {
|
|
124
|
+
const marked =
|
|
125
|
+
marker &&
|
|
126
|
+
typeof entry.content === "string" &&
|
|
127
|
+
entry.content.includes(marker);
|
|
128
|
+
if (marked && entry.operation === "enqueue") {
|
|
129
|
+
state.queued = true;
|
|
130
|
+
state.queuedContent = entry.content;
|
|
131
|
+
} else if (
|
|
132
|
+
state.queued &&
|
|
133
|
+
(marked || entry.content === state.queuedContent)
|
|
134
|
+
) {
|
|
135
|
+
if (entry.operation === "remove") beginTurn(state);
|
|
136
|
+
} else if (marked && entry.operation === "remove") {
|
|
137
|
+
beginTurn(state);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
state.userSeen &&
|
|
143
|
+
!state.sectionClosed &&
|
|
144
|
+
entry?.type === "system" &&
|
|
145
|
+
entry.subtype === "turn_duration"
|
|
146
|
+
) {
|
|
147
|
+
state.turnEnded = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function acceptLine(state, line, marker) {
|
|
152
|
+
try {
|
|
153
|
+
acceptEntry(state, JSON.parse(line), marker);
|
|
154
|
+
return true;
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function readClaudeTranscript(transcriptPath, { requestId } = {}) {
|
|
161
|
+
if (!transcriptPath) return null;
|
|
162
|
+
const marker = requestId ? `[tfx-live req=${requestId}]` : null;
|
|
163
|
+
const key = `${transcriptPath}\0${requestId || ""}`;
|
|
164
|
+
let stat;
|
|
165
|
+
try {
|
|
166
|
+
stat = await fs.stat(transcriptPath);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error?.code === "ENOENT") return null;
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
let state = transcriptCache.get(key);
|
|
172
|
+
if (
|
|
173
|
+
!state ||
|
|
174
|
+
state.ino !== stat.ino ||
|
|
175
|
+
stat.size < state.offset ||
|
|
176
|
+
(stat.size === state.offset && stat.mtimeMs !== state.mtimeMs)
|
|
177
|
+
) {
|
|
178
|
+
state = {
|
|
179
|
+
ino: stat.ino,
|
|
180
|
+
offset: 0,
|
|
181
|
+
pending: "",
|
|
182
|
+
decoder: new StringDecoder("utf8"),
|
|
183
|
+
userSeen: false,
|
|
184
|
+
queued: false,
|
|
185
|
+
queuedContent: null,
|
|
186
|
+
response: "",
|
|
187
|
+
error: null,
|
|
188
|
+
turnEnded: false,
|
|
189
|
+
sectionClosed: false,
|
|
190
|
+
estimatedContextTokens: null,
|
|
191
|
+
model: null,
|
|
192
|
+
measuredAt: null,
|
|
193
|
+
compactCount: 0,
|
|
194
|
+
compact: null,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
if (stat.size > state.offset) {
|
|
199
|
+
const stream = createReadStream(transcriptPath, {
|
|
200
|
+
start: state.offset,
|
|
201
|
+
end: stat.size - 1,
|
|
202
|
+
});
|
|
203
|
+
for await (const chunk of stream) {
|
|
204
|
+
const lines = (state.pending + state.decoder.write(chunk)).split("\n");
|
|
205
|
+
state.pending = lines.pop();
|
|
206
|
+
for (const line of lines) acceptLine(state, line, marker);
|
|
207
|
+
}
|
|
208
|
+
if (state.pending && acceptLine(state, state.pending, marker))
|
|
209
|
+
state.pending = "";
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
transcriptCache.delete(key);
|
|
213
|
+
if (error?.code === "ENOENT") return null;
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
state.offset = stat.size;
|
|
217
|
+
state.mtimeMs = stat.mtimeMs;
|
|
218
|
+
transcriptCache.set(key, state);
|
|
219
|
+
return {
|
|
220
|
+
context:
|
|
221
|
+
state.estimatedContextTokens === null && state.model === null
|
|
222
|
+
? null
|
|
223
|
+
: {
|
|
224
|
+
...modelContext(
|
|
225
|
+
"claude",
|
|
226
|
+
state.model,
|
|
227
|
+
state.estimatedContextTokens,
|
|
228
|
+
),
|
|
229
|
+
model: state.model,
|
|
230
|
+
measuredAt: state.measuredAt,
|
|
231
|
+
},
|
|
232
|
+
userSeen: state.userSeen,
|
|
233
|
+
response: state.response,
|
|
234
|
+
error: state.error,
|
|
235
|
+
turnEnded: state.turnEnded,
|
|
236
|
+
sectionClosed: state.sectionClosed,
|
|
237
|
+
compactCount: state.compactCount,
|
|
238
|
+
compact: state.compact,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -72,9 +72,19 @@ function renderTmuxInstallHelp() {
|
|
|
72
72
|
|
|
73
73
|
export { parseTeamArgs };
|
|
74
74
|
|
|
75
|
+
export function resolveTeamWorkers({ agents, task, assigns = [] }) {
|
|
76
|
+
if (assigns.length > 0) {
|
|
77
|
+
return {
|
|
78
|
+
agents: assigns.map((assign) => assign.cli),
|
|
79
|
+
subtasks: assigns.map((assign) => assign.prompt),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { agents, subtasks: decomposeTask(task, agents.length) };
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
export async function teamStart(args = []) {
|
|
76
86
|
const {
|
|
77
|
-
agents,
|
|
87
|
+
agents: requestedAgents,
|
|
78
88
|
lead,
|
|
79
89
|
layout,
|
|
80
90
|
teammateMode,
|
|
@@ -100,11 +110,16 @@ export async function teamStart(args = []) {
|
|
|
100
110
|
rawTask ||
|
|
101
111
|
(assigns.length > 0 ? assigns.map((a) => a.prompt).join(" + ") : "");
|
|
102
112
|
if (!task) return printStartUsage();
|
|
113
|
+
const { agents, subtasks } = resolveTeamWorkers({
|
|
114
|
+
agents: requestedAgents,
|
|
115
|
+
task,
|
|
116
|
+
assigns,
|
|
117
|
+
});
|
|
103
118
|
|
|
104
119
|
console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
|
|
105
120
|
|
|
106
121
|
// P1b: 워커 수 계산 — 단일 워커 headless에는 Hub 불필요
|
|
107
|
-
const workerCount =
|
|
122
|
+
const workerCount = agents.length;
|
|
108
123
|
const needsHub = workerCount >= 2 || teammateMode !== "headless";
|
|
109
124
|
|
|
110
125
|
let hub = null;
|
|
@@ -126,7 +141,6 @@ export async function teamStart(args = []) {
|
|
|
126
141
|
}
|
|
127
142
|
|
|
128
143
|
const sessionId = `tfx-multi-${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`;
|
|
129
|
-
const subtasks = decomposeTask(task, agents.length);
|
|
130
144
|
const hubUrl = hub?.url || getDefaultHubUrl();
|
|
131
145
|
const { mode: effectiveMode, warnings: modeWarnings } =
|
|
132
146
|
resolveEffectiveMode(teammateMode);
|