triflux 10.39.1 → 10.41.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/cto/status.mjs +11 -0
- package/hub/bridge.mjs +23 -1
- package/hub/cli-adapter-base.mjs +11 -1
- package/hub/codex-adapter.mjs +13 -5
- package/hub/pipe.mjs +39 -1
- package/hub/public/tray.html +195 -98
- package/hub/router.mjs +534 -4
- package/hub/server.mjs +39 -5
- package/hub/team/execution-mode.mjs +9 -1
- package/hub/tools.mjs +21 -1
- package/hub/tray-state.mjs +24 -2
- package/hub/workers/claude-worker.mjs +3 -0
- package/package.json +1 -1
- package/scripts/lib/codex-profile-config.mjs +83 -2
- package/scripts/tfx-route-worker.mjs +5 -0
- package/scripts/tfx-route.sh +27 -1
- package/skills/tfx-auto/SKILL.md +12 -8
- package/skills/tfx-consensus/SKILL.md +1 -0
- package/skills/tfx-debate/SKILL.md +2 -2
- package/skills/tfx-panel/SKILL.md +1 -1
package/cto/status.mjs
CHANGED
|
@@ -55,6 +55,17 @@ export function deriveRepoRootFromCwd(cwd) {
|
|
|
55
55
|
if (cwd.includes("\\") || /^[A-Za-z]:/u.test(cwd)) return cwd;
|
|
56
56
|
|
|
57
57
|
const normalized = cwd.replace(/\/+$/u, "");
|
|
58
|
+
const gitWorktreeMarker = "/.worktrees/";
|
|
59
|
+
const gitWorktreeIndex = normalized.indexOf(gitWorktreeMarker);
|
|
60
|
+
if (gitWorktreeIndex > 0) {
|
|
61
|
+
const suffix = normalized.slice(
|
|
62
|
+
gitWorktreeIndex + gitWorktreeMarker.length,
|
|
63
|
+
);
|
|
64
|
+
if (/^[^/]+(?:\/|$)/u.test(suffix)) {
|
|
65
|
+
return normalized.slice(0, gitWorktreeIndex);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
const claudeMarker = "/.claude/worktrees/";
|
|
59
70
|
const claudeIndex = normalized.indexOf(claudeMarker);
|
|
60
71
|
if (claudeIndex > 0) {
|
package/hub/bridge.mjs
CHANGED
|
@@ -119,6 +119,11 @@ const HUB_OPERATIONS = Object.freeze({
|
|
|
119
119
|
action: "publish",
|
|
120
120
|
httpPath: "/bridge/publish",
|
|
121
121
|
},
|
|
122
|
+
takeoverRole: {
|
|
123
|
+
transport: "command",
|
|
124
|
+
action: "takeover_role",
|
|
125
|
+
httpPath: "/bridge/takeover-role",
|
|
126
|
+
},
|
|
122
127
|
sendInput: {
|
|
123
128
|
transport: "command",
|
|
124
129
|
action: "send_input",
|
|
@@ -365,6 +370,7 @@ export function parseArgs(argv) {
|
|
|
365
370
|
args: argv,
|
|
366
371
|
options: {
|
|
367
372
|
agent: { type: "string" },
|
|
373
|
+
"agent-id": { type: "string" },
|
|
368
374
|
cli: { type: "string" },
|
|
369
375
|
timeout: { type: "string" },
|
|
370
376
|
topics: { type: "string" },
|
|
@@ -387,6 +393,7 @@ export function parseArgs(argv) {
|
|
|
387
393
|
claim: { type: "boolean" },
|
|
388
394
|
actor: { type: "string" },
|
|
389
395
|
command: { type: "string" },
|
|
396
|
+
role: { type: "string" },
|
|
390
397
|
"session-id": { type: "string" },
|
|
391
398
|
reason: { type: "string" },
|
|
392
399
|
type: { type: "string" },
|
|
@@ -703,6 +710,19 @@ async function cmdPublish(args) {
|
|
|
703
710
|
return emitJson(result || unavailableResult());
|
|
704
711
|
}
|
|
705
712
|
|
|
713
|
+
async function cmdTakeoverRole(args) {
|
|
714
|
+
const role = args.role || args[1] || "cto";
|
|
715
|
+
const agentId = args.agent || args["agent-id"] || args[2];
|
|
716
|
+
const outcome = await requestHub(HUB_OPERATIONS.takeoverRole, {
|
|
717
|
+
role,
|
|
718
|
+
agent_id: agentId,
|
|
719
|
+
reason: args.reason || "manual",
|
|
720
|
+
requested_by: args["requested-by"] || "bridge",
|
|
721
|
+
});
|
|
722
|
+
const result = outcome?.result;
|
|
723
|
+
return emitJson(result || unavailableResult());
|
|
724
|
+
}
|
|
725
|
+
|
|
706
726
|
async function cmdSendInput(args) {
|
|
707
727
|
const outcome = await requestHub(HUB_OPERATIONS.sendInput, {
|
|
708
728
|
session_id: args["session-id"],
|
|
@@ -1554,6 +1574,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1554
1574
|
return await cmdHandoff(args);
|
|
1555
1575
|
case "publish":
|
|
1556
1576
|
return await cmdPublish(args);
|
|
1577
|
+
case "takeover-role":
|
|
1578
|
+
return await cmdTakeoverRole(args);
|
|
1557
1579
|
case "send-input":
|
|
1558
1580
|
return await cmdSendInput(args);
|
|
1559
1581
|
case "context":
|
|
@@ -1610,7 +1632,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1610
1632
|
return await cmdRetryStatus(args);
|
|
1611
1633
|
default:
|
|
1612
1634
|
console.error(
|
|
1613
|
-
"사용법: bridge.mjs <register|result|control|handoff|publish|send-input|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping|delegator-delegate|delegator-reply|delegator-status|hitl-request|hitl-submit|hitl-pending|daemon-probe|daemon-attach|daemon-interrupt|retry-run|retry-status> [--옵션]",
|
|
1635
|
+
"사용법: bridge.mjs <register|result|control|handoff|publish|takeover-role|send-input|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping|delegator-delegate|delegator-reply|delegator-status|hitl-request|hitl-submit|hitl-pending|daemon-probe|daemon-attach|daemon-interrupt|retry-run|retry-status> [--옵션]",
|
|
1614
1636
|
);
|
|
1615
1637
|
process.exit(1);
|
|
1616
1638
|
}
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { execSync, spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
|
|
7
|
+
import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
|
|
7
8
|
import { writePromptToTmpFile } from "./lib/prompt-tmp.mjs";
|
|
8
9
|
import { IS_WINDOWS, killProcess } from "./platform.mjs";
|
|
9
10
|
|
|
@@ -172,7 +173,12 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
172
173
|
} = opts;
|
|
173
174
|
|
|
174
175
|
const parts = ["codex"];
|
|
175
|
-
|
|
176
|
+
// Select the effort profile via `-c` config overrides instead of
|
|
177
|
+
// `--profile <name>`. codex 0.134+ rejects `--profile X` whenever config.toml
|
|
178
|
+
// still contains an inline [profiles.X] table (and codex re-injects such
|
|
179
|
+
// tables when it rewrites config.toml), so the `-c model=.. -c
|
|
180
|
+
// model_reasoning_effort=..` form is immune and mutates no config.
|
|
181
|
+
const profileOverrides = profile ? codexProfileConfigOverrides(profile) : [];
|
|
176
182
|
|
|
177
183
|
if (FEATURES.execSubcommand) {
|
|
178
184
|
parts.push("exec");
|
|
@@ -182,6 +188,8 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
182
188
|
parts.push("--output-last-message", resultFile);
|
|
183
189
|
}
|
|
184
190
|
if (FEATURES.colorNever) parts.push("--color", "never");
|
|
191
|
+
for (const override of profileOverrides)
|
|
192
|
+
parts.push("-c", shellQuote(override));
|
|
185
193
|
// NOTE: `codex exec`는 --cwd 플래그를 지원하지 않는다. Node spawn의 cwd
|
|
186
194
|
// 옵션으로 child process의 working directory를 제어한다 (conductor.mjs 참조).
|
|
187
195
|
// opts.cwd는 기록용으로만 받아두고 CLI command에는 반영하지 않는다.
|
|
@@ -194,6 +202,8 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
194
202
|
} else {
|
|
195
203
|
parts.push("--dangerously-bypass-approvals-and-sandbox");
|
|
196
204
|
if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
|
|
205
|
+
for (const override of profileOverrides)
|
|
206
|
+
parts.push("-c", shellQuote(override));
|
|
197
207
|
}
|
|
198
208
|
|
|
199
209
|
const useStdin = resolveStdinPromptMode(stdinPrompt);
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
|
+
import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
|
|
4
5
|
import {
|
|
5
6
|
buildExecCommand,
|
|
6
7
|
createResult,
|
|
@@ -72,14 +73,21 @@ function buildAttempts(opts, preflight) {
|
|
|
72
73
|
// ── Launch script ───────────────────────────────────────────────
|
|
73
74
|
|
|
74
75
|
function createLaunchScriptText(opts) {
|
|
75
|
-
const parts = [
|
|
76
|
-
|
|
77
|
-
parts.push(
|
|
76
|
+
const parts = [
|
|
77
|
+
"codex",
|
|
78
78
|
"exec",
|
|
79
79
|
"--dangerously-bypass-approvals-and-sandbox",
|
|
80
80
|
"--skip-git-repo-check",
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
];
|
|
82
|
+
// Select the effort profile via `-c` config overrides instead of
|
|
83
|
+
// `--profile <name>` (see buildExecCommand): codex 0.134+ rejects `--profile
|
|
84
|
+
// X` whenever config.toml still contains an inline [profiles.X] table.
|
|
85
|
+
if (opts.profile) {
|
|
86
|
+
for (const override of codexProfileConfigOverrides(opts.profile)) {
|
|
87
|
+
parts.push("-c", shellQuote(override));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
parts.push('$(cat "$PROMPT_FILE")');
|
|
83
91
|
return [
|
|
84
92
|
"#!/usr/bin/env bash",
|
|
85
93
|
"set -euo pipefail",
|
package/hub/pipe.mjs
CHANGED
|
@@ -29,6 +29,17 @@ function normalizeTopics(topics) {
|
|
|
29
29
|
return topics.map((topic) => String(topic || "").trim()).filter(Boolean);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function normalizeReplayTopics(topics) {
|
|
33
|
+
if (Array.isArray(topics)) return normalizeTopics(topics);
|
|
34
|
+
if (typeof topics === "string") {
|
|
35
|
+
return topics
|
|
36
|
+
.split(/[,\s]+/u)
|
|
37
|
+
.map((topic) => topic.trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
function getTeamBridgeMethod(methodName) {
|
|
33
44
|
const method = getTeamBridge()?.[methodName];
|
|
34
45
|
return typeof method === "function" ? method : null;
|
|
@@ -266,6 +277,12 @@ export function createPipeServer({
|
|
|
266
277
|
return result;
|
|
267
278
|
}
|
|
268
279
|
|
|
280
|
+
case "takeover_role": {
|
|
281
|
+
const result = router.takeoverRole(payload);
|
|
282
|
+
if (client) touchClient(client);
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
269
286
|
case "handoff": {
|
|
270
287
|
const result = router.handleHandoff(payload);
|
|
271
288
|
if (client) touchClient(client);
|
|
@@ -454,13 +471,34 @@ export function createPipeServer({
|
|
|
454
471
|
return pending.slice(0, maxMessages);
|
|
455
472
|
}
|
|
456
473
|
|
|
474
|
+
const requestedTopics = normalizeReplayTopics(payload.topics);
|
|
475
|
+
const auditTopics = new Set(requestedTopics);
|
|
476
|
+
if (auditTopics.size === 0) {
|
|
477
|
+
const persistedTopics = store?.getAgent?.(agentId)?.topics || [];
|
|
478
|
+
for (const topic of persistedTopics) auditTopics.add(topic);
|
|
479
|
+
if (
|
|
480
|
+
typeof router.canReplayMessageForAgent === "function" &&
|
|
481
|
+
router.canReplayMessageForAgent(agentId, {
|
|
482
|
+
to_agent: "topic:cto",
|
|
483
|
+
topic: "cto",
|
|
484
|
+
})
|
|
485
|
+
) {
|
|
486
|
+
auditTopics.add("cto");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
457
490
|
const audit = store.getAuditMessagesForAgent(agentId, {
|
|
458
491
|
max_messages: maxMessages,
|
|
459
|
-
include_topics:
|
|
492
|
+
include_topics: Array.from(auditTopics),
|
|
460
493
|
});
|
|
461
494
|
const byId = new Map();
|
|
462
495
|
for (const message of [...pending, ...audit]) {
|
|
463
496
|
if (!message?.id || byId.has(message.id)) continue;
|
|
497
|
+
if (
|
|
498
|
+
typeof router.canReplayMessageForAgent === "function" &&
|
|
499
|
+
!router.canReplayMessageForAgent(agentId, message)
|
|
500
|
+
)
|
|
501
|
+
continue;
|
|
464
502
|
byId.set(message.id, message);
|
|
465
503
|
}
|
|
466
504
|
return Array.from(byId.values())
|
package/hub/public/tray.html
CHANGED
|
@@ -259,27 +259,31 @@
|
|
|
259
259
|
`;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
262
|
+
function liveFallbackRows(live) {
|
|
263
|
+
return live.map(session => ({
|
|
264
|
+
sessionId: session.sessionId,
|
|
265
|
+
status: session.phase || 'active',
|
|
266
|
+
agent_id: session.agent_id || session.agent?.id || '',
|
|
267
|
+
agent: session.agent || null,
|
|
268
|
+
role: session.role || '',
|
|
269
|
+
roles: session.roles || [],
|
|
270
|
+
metadata: session.metadata || {},
|
|
271
|
+
promptText: session.promptText || '',
|
|
272
|
+
prompt: session.prompt || '',
|
|
273
|
+
userPrompt: session.userPrompt || '',
|
|
274
|
+
handoffPrompt: session.handoffPrompt || '',
|
|
275
|
+
command: session.command || '',
|
|
273
276
|
source: session.source || '',
|
|
274
277
|
conversationId: session.conversationId || '',
|
|
275
278
|
focusable: session.focusable !== false,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
taskSummary: session.taskSummary || session.command || (session.pid ? `PID ${session.pid}` : ''),
|
|
280
|
+
pid: session.pid || '',
|
|
281
|
+
cwd: session.cwd || session.host || 'local runtime',
|
|
282
|
+
worktreePath: session.worktreePath || '',
|
|
283
|
+
elapsed: session.elapsed || '',
|
|
284
|
+
started_at: session.started_at || null,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
283
287
|
|
|
284
288
|
function compactText(value, fallback = '') {
|
|
285
289
|
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
@@ -293,12 +297,20 @@
|
|
|
293
297
|
return path.replace(/^\/Users\/[^/]+/, '~');
|
|
294
298
|
}
|
|
295
299
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
function normalizeProjectPath(value) {
|
|
301
|
+
const path = String(value || '').trim();
|
|
302
|
+
if (!path) return '';
|
|
303
|
+
if (path === '/' || /^\/+$/u.test(path) || /^[A-Za-z]:[\\/]$/u.test(path)) return path;
|
|
304
|
+
return path.replace(/[\\/]+$/u, '') || path;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function worktreeFamilyRoot(path) {
|
|
308
|
+
const normalized = normalizeProjectPath(path);
|
|
309
|
+
const parts = normalized.split(/[\\/]/u);
|
|
310
|
+
const index = parts.lastIndexOf('.worktrees');
|
|
311
|
+
if (index <= 0) return '';
|
|
312
|
+
return parts.slice(0, index).join(normalized.includes('\\') ? '\\' : '/') || '';
|
|
313
|
+
}
|
|
302
314
|
|
|
303
315
|
function projectName(value) {
|
|
304
316
|
const path = normalizeProjectPath(value);
|
|
@@ -306,12 +318,14 @@
|
|
|
306
318
|
return path.split(/[\\/]/u).filter(Boolean).pop() || path;
|
|
307
319
|
}
|
|
308
320
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
321
|
+
function projectPathFor(row, hub, fallback = '') {
|
|
322
|
+
const cwd = normalizeProjectPath(String(row?.cwd || row?.worktreePath || '').trim());
|
|
323
|
+
const rawRoot = normalizeProjectPath(String(hub?.projectRoot || fallback || '').trim());
|
|
324
|
+
const familyRoot = worktreeFamilyRoot(rawRoot) || rawRoot;
|
|
325
|
+
if (familyRoot && cwd && (cwd === familyRoot || cwd.startsWith(`${familyRoot}/.worktrees/`) || cwd.startsWith(`${familyRoot}/worktrees/`))) return familyRoot;
|
|
326
|
+
if (cwd && cwd !== 'local' && cwd !== 'local runtime') return worktreeFamilyRoot(cwd) || cwd;
|
|
327
|
+
return normalizeProjectPath(familyRoot || cwd || 'local');
|
|
328
|
+
}
|
|
315
329
|
|
|
316
330
|
function getProjectGroup(groups, projectPath) {
|
|
317
331
|
const key = normalizeProjectPath(projectPath) || 'local';
|
|
@@ -319,33 +333,86 @@
|
|
|
319
333
|
return groups.get(key);
|
|
320
334
|
}
|
|
321
335
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
for (const session of activeSessions) {
|
|
326
|
-
getProjectGroup(groups, projectPathFor(session, hub, fallbackProject)).ctos.push(session);
|
|
327
|
-
}
|
|
336
|
+
function ctoRoleFromPayload(cto) {
|
|
337
|
+
return cto?.roles?.cto || cto?.succession?.cto || null;
|
|
338
|
+
}
|
|
328
339
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
getProjectGroup(groups, projectPathFor(worker, hub, fallbackProject)).workers.push(worker);
|
|
333
|
-
}
|
|
340
|
+
function normalizedRoleValue(value) {
|
|
341
|
+
return String(value || '').trim().toLowerCase();
|
|
342
|
+
}
|
|
334
343
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
function rowHasCtoRole(row, ctoRole) {
|
|
345
|
+
const sid = String(row?.sessionId || '').trim();
|
|
346
|
+
const agentId = String(row?.agent_id || row?.agent?.id || '').trim();
|
|
347
|
+
const leader = String(ctoRole?.leader_agent_id || '').trim();
|
|
348
|
+
if (leader) return Boolean(agentId === leader || sid === leader || sid === `cto:${leader}`);
|
|
349
|
+
if (ctoRole) return false;
|
|
350
|
+
const role = normalizedRoleValue(row?.role || row?.metadata?.role || row?.sessionKind);
|
|
351
|
+
const roles = Array.isArray(row?.roles || row?.metadata?.roles)
|
|
352
|
+
? (row.roles || row.metadata.roles).map(normalizedRoleValue)
|
|
353
|
+
: String(row?.roles || row?.metadata?.roles || '').split(/[,\s]+/u).map(normalizedRoleValue);
|
|
354
|
+
if (role === 'cto' || roles.includes('cto') || row?.metadata?.cto === true) return true;
|
|
355
|
+
if (agentId === 'cto') return true;
|
|
356
|
+
if (/^cto[:._-]/iu.test(sid)) return true;
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function ctoRoleRow(ctoRole, hub, fallbackProject) {
|
|
361
|
+
if (!ctoRole) return null;
|
|
362
|
+
const status = ctoRole.status === 'active' ? 'active' : 'offline';
|
|
363
|
+
const leader = ctoRole.leader_agent_id || ctoRole.previous_leader_agent_id || 'unassigned';
|
|
364
|
+
return {
|
|
365
|
+
sessionId: ctoRole.leader_agent_id ? `cto:${ctoRole.leader_agent_id}` : `cto:${shortId(hub.id || fallbackProject || 'role')}`,
|
|
366
|
+
status,
|
|
367
|
+
cwd: hub.projectRoot || fallbackProject || 'local',
|
|
368
|
+
taskSummary: status === 'active'
|
|
369
|
+
? `CTO leader ${leader} · epoch ${ctoRole.leader_epoch || 0} · pending ${ctoRole.pending_count || 0}`
|
|
370
|
+
: `CTO offline · previous ${leader} · candidates ${ctoRole.live_candidate_count || 0}`,
|
|
343
371
|
synthetic: true,
|
|
344
|
-
|
|
372
|
+
roleStatus: ctoRole,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function buildProjectGroups(activeSessions, liveRows, hub, ctoRole = null) {
|
|
377
|
+
const groups = new Map();
|
|
378
|
+
const fallbackProject = hub.projectRoot || activeSessions[0]?.cwd || activeSessions[0]?.worktreePath || 'local';
|
|
379
|
+
for (const session of activeSessions) {
|
|
380
|
+
const group = getProjectGroup(groups, projectPathFor(session, hub, fallbackProject));
|
|
381
|
+
if (rowHasCtoRole(session, ctoRole)) group.ctos.push(session);
|
|
382
|
+
else group.workers.push(session);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const activeSessionIds = new Set(
|
|
386
|
+
activeSessions
|
|
387
|
+
.map(session => session.sessionId)
|
|
388
|
+
.filter(Boolean)
|
|
389
|
+
);
|
|
390
|
+
for (const worker of liveRows) {
|
|
391
|
+
if (activeSessionIds.has(worker.sessionId)) continue;
|
|
392
|
+
const group = getProjectGroup(groups, projectPathFor(worker, hub, fallbackProject));
|
|
393
|
+
if (rowHasCtoRole(worker, ctoRole)) group.ctos.push(worker);
|
|
394
|
+
else group.workers.push(worker);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (groups.size === 0 && fallbackProject) getProjectGroup(groups, fallbackProject);
|
|
398
|
+
const roleRow = ctoRoleRow(ctoRole, hub, fallbackProject);
|
|
399
|
+
if (roleRow) {
|
|
400
|
+
const project = projectPathFor(roleRow, hub, fallbackProject);
|
|
401
|
+
const group = getProjectGroup(groups, project);
|
|
402
|
+
const leader = String(ctoRole?.leader_agent_id || '').trim();
|
|
403
|
+
const existing = group.ctos.find(row => {
|
|
404
|
+
const sid = String(row?.sessionId || '').trim();
|
|
405
|
+
const agentId = String(row?.agent_id || row?.agent?.id || '').trim();
|
|
406
|
+
return sid === roleRow.sessionId || (leader && (sid === leader || agentId === leader));
|
|
407
|
+
});
|
|
408
|
+
if (existing) {
|
|
409
|
+
existing.roleStatus = ctoRole;
|
|
410
|
+
} else {
|
|
411
|
+
group.ctos.unshift(roleRow);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return [...groups.values()].sort((a, b) => a.projectPath.localeCompare(b.projectPath));
|
|
345
415
|
}
|
|
346
|
-
}
|
|
347
|
-
return [...groups.values()].sort((a, b) => a.projectPath.localeCompare(b.projectPath));
|
|
348
|
-
}
|
|
349
416
|
|
|
350
417
|
function explicitPromptText(row) {
|
|
351
418
|
return String(row?.promptText || row?.prompt || row?.userPrompt || row?.handoffPrompt || '').trim();
|
|
@@ -443,20 +510,24 @@
|
|
|
443
510
|
`;
|
|
444
511
|
}
|
|
445
512
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
513
|
+
function renderCtoChat(ctoRow, workers) {
|
|
514
|
+
const status = ctoRow.status || ctoRow.phase || 'active';
|
|
515
|
+
const title = compactText(ctoRow.sessionId || ctoRow.taskSummary || 'Session', 'Session');
|
|
516
|
+
const subtitle = compactText(hasPromptText(ctoRow) ? ctoRow.taskSummary || ctoRow.sessionKind || '' : displayPath(ctoRow.cwd || ctoRow.worktreePath || ''), '');
|
|
517
|
+
const role = ctoRow.roleStatus || null;
|
|
518
|
+
const roleHtml = role
|
|
519
|
+
? `<div class="detail-section"><div class="detail-label">Succession</div><div class="path-text">leader ${escapeHtml(role.leader_agent_id || 'none')} · epoch ${escapeHtml(role.leader_epoch || 0)} · pending ${escapeHtml(role.pending_count || 0)} · candidates ${escapeHtml(role.live_candidate_count || 0)}/${escapeHtml(role.candidate_count || 0)}</div></div>`
|
|
520
|
+
: '';
|
|
521
|
+
const promptHtml = hasPromptText(ctoRow)
|
|
522
|
+
? `<div class="detail-section"><div class="detail-label">Prompt</div>${renderChatBubble(ctoRow, { side: 'cto' })}</div>`
|
|
523
|
+
: '';
|
|
524
|
+
const agentRows = workers.map(worker => hasPromptText(worker) ? renderChatBubble(worker, { side: 'worker' }) : renderRuntimeCard(worker)).join('');
|
|
525
|
+
const agentsHtml = workers.length
|
|
526
|
+
? `<div class="detail-section"><div class="detail-label">Agents</div><div class="runtime-list">${agentRows}</div></div>`
|
|
527
|
+
: '';
|
|
528
|
+
const emptyHtml = roleHtml || promptHtml || agentsHtml ? '' : '<div class="detail-empty">No session details yet</div>';
|
|
529
|
+
return `
|
|
530
|
+
<details class="session-card" data-open-key="cto:${dataAttr(ctoRow.sessionId || title)}">
|
|
460
531
|
<summary class="session-summary">
|
|
461
532
|
<span class="agent-badge">T</span>
|
|
462
533
|
<div style="min-width:0;">
|
|
@@ -465,10 +536,11 @@
|
|
|
465
536
|
</div>
|
|
466
537
|
<div class="agent-state ${escapeHtml(status)}">${escapeHtml(status)}</div>
|
|
467
538
|
</summary>
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
539
|
+
<div class="session-detail">
|
|
540
|
+
${roleHtml}
|
|
541
|
+
${promptHtml}
|
|
542
|
+
${agentsHtml}
|
|
543
|
+
${emptyHtml}
|
|
472
544
|
</div>
|
|
473
545
|
</details>
|
|
474
546
|
`;
|
|
@@ -478,14 +550,15 @@
|
|
|
478
550
|
return `${count} ${count === 1 ? one : many}`;
|
|
479
551
|
}
|
|
480
552
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
553
|
+
function renderProjectGroup(group) {
|
|
554
|
+
const projectDisplayPath = displayPath(group.projectPath);
|
|
555
|
+
const projectLabel = projectName(group.projectPath);
|
|
556
|
+
const sessionCount = group.ctos.filter(row => !isSyntheticCto(row)).length;
|
|
557
|
+
const agentCount = group.workers.length;
|
|
558
|
+
const ctoCount = group.ctos.length;
|
|
559
|
+
const projectCount = [ctoCount ? pluralize(ctoCount, 'CTO') : '', sessionCount ? pluralize(sessionCount, 'session') : '', agentCount ? pluralize(agentCount, 'agent') : '']
|
|
560
|
+
.filter(Boolean)
|
|
561
|
+
.join(' · ') || 'empty';
|
|
489
562
|
let html = `
|
|
490
563
|
<details class="project-group" data-open-key="project:${dataAttr(group.projectPath)}">
|
|
491
564
|
<summary class="project-summary">
|
|
@@ -496,26 +569,49 @@
|
|
|
496
569
|
<div class="project-path">${escapeHtml(projectDisplayPath)}</div>
|
|
497
570
|
<div class="project-body">
|
|
498
571
|
`;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
572
|
+
const ctos = group.ctos.slice(0, 6);
|
|
573
|
+
const workers = group.workers.slice(0, 10);
|
|
574
|
+
for (const [index, ctoRow] of ctos.entries()) {
|
|
575
|
+
html += renderCtoChat(ctoRow, index === 0 ? workers : []);
|
|
576
|
+
}
|
|
577
|
+
if (ctos.length === 0 && workers.length > 0) {
|
|
578
|
+
const agentRows = workers.map(worker => hasPromptText(worker) ? renderChatBubble(worker, { side: 'worker' }) : renderRuntimeCard(worker)).join('');
|
|
579
|
+
html += `<div class="detail-section"><div class="detail-label">Agents</div><div class="runtime-list">${agentRows}</div></div>`;
|
|
580
|
+
}
|
|
581
|
+
html += '</div></details>';
|
|
582
|
+
return html;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function renderCtoRoleBanner(ctoRole) {
|
|
586
|
+
if (!ctoRole) return '';
|
|
587
|
+
const status = ctoRole.status || 'offline';
|
|
588
|
+
const leader = ctoRole.leader_agent_id || ctoRole.previous_leader_agent_id || 'none';
|
|
589
|
+
return `
|
|
590
|
+
<div class="section">
|
|
591
|
+
<div class="section-title">CTO Succession</div>
|
|
592
|
+
<div class="mini-card">
|
|
593
|
+
<div style="display:flex;align-items:center;gap:8px;justify-content:space-between;">
|
|
594
|
+
<div class="mini-title"><span class="agent-badge">T</span> ${escapeHtml(leader)}</div>
|
|
595
|
+
<div class="value-pill status-text ${escapeHtml(status)}">${escapeHtml(status)}</div>
|
|
596
|
+
</div>
|
|
597
|
+
<div class="path-text">epoch ${escapeHtml(ctoRole.leader_epoch || 0)} · pending ${escapeHtml(ctoRole.pending_count || 0)} · candidates ${escapeHtml(ctoRole.live_candidate_count || 0)}/${escapeHtml(ctoRole.candidate_count || 0)}${ctoRole.candidate_source ? ` · ${escapeHtml(ctoRole.candidate_source)}` : ''}</div>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
`;
|
|
601
|
+
}
|
|
507
602
|
|
|
508
603
|
function renderSessions(data) {
|
|
509
604
|
const container = document.getElementById('tab-sessions');
|
|
510
605
|
const openKeys = collectOpenDetailKeys(container);
|
|
511
606
|
const hub = data?.hub || {};
|
|
512
607
|
const sessions = Array.isArray(data?.sessions) ? data.sessions : [];
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
608
|
+
const cto = data?.cto || {};
|
|
609
|
+
const ctoRole = ctoRoleFromPayload(cto);
|
|
610
|
+
const live = Array.isArray(cto.live_sessions) ? cto.live_sessions : [];
|
|
611
|
+
const runtime = data?.runtime || {};
|
|
612
|
+
const activeSessions = sessions.filter(s => s.status !== 'stale');
|
|
613
|
+
const liveRows = liveFallbackRows(live);
|
|
614
|
+
const projectGroups = buildProjectGroups(activeSessions.slice(0, 10), liveRows, hub, ctoRole);
|
|
519
615
|
const activeCount = activeSessions.filter(s => s.status === 'active').length;
|
|
520
616
|
const idleCount = activeSessions.filter(s => s.status === 'idle').length;
|
|
521
617
|
const staleCount = sessions.filter(s => s.status === 'stale').length;
|
|
@@ -530,11 +626,12 @@
|
|
|
530
626
|
</div>
|
|
531
627
|
<div class="path-text">${projectGroups.length} ${projectGroups.length === 1 ? 'workspace' : 'workspaces'} · ${cto.active_shards?.length || 0} shards · ${renderCopyChip('Hub', hub.id || String(hub.pid || ''))}</div>
|
|
532
628
|
</div>
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
629
|
+
<div class="section">
|
|
630
|
+
<div class="section-title">Agent Mix</div>
|
|
631
|
+
<div class="models-list">${renderRuntimeClients(runtime)}</div>
|
|
632
|
+
</div>
|
|
633
|
+
${renderCtoRoleBanner(ctoRole)}
|
|
634
|
+
${renderCtoHygiene(cto.hygiene)}
|
|
538
635
|
<div class="section">
|
|
539
636
|
<div class="section-title">Workspaces</div>
|
|
540
637
|
<div class="agent-view">
|