@dmsdc-ai/aigentry-deliberation 0.0.33 → 0.0.35
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/README.md +37 -0
- package/clipboard.js +178 -0
- package/index.js +1621 -107
- package/install.js +18 -1
- package/model-router.js +14 -25
- package/package.json +2 -1
- package/skills/deliberation/SKILL.md +7 -6
- package/skills/deliberation-gate/SKILL.md +9 -5
package/index.js
CHANGED
|
@@ -68,10 +68,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
68
68
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
69
69
|
import { z } from "zod";
|
|
70
70
|
import { execFileSync, spawn } from "child_process";
|
|
71
|
+
import { createHash } from "crypto";
|
|
71
72
|
import fs from "fs";
|
|
72
73
|
import path from "path";
|
|
73
74
|
import { fileURLToPath } from "url";
|
|
74
75
|
import os from "os";
|
|
76
|
+
import WebSocket from "ws";
|
|
75
77
|
import { OrchestratedBrowserPort } from "./browser-control-port.js";
|
|
76
78
|
import { getModelSelectionForTurn } from "./model-router.js";
|
|
77
79
|
import { readClipboardText, writeClipboardText, hasClipboardImage, captureClipboardImage } from "./clipboard.js";
|
|
@@ -110,6 +112,13 @@ const DEFAULT_CLI_CANDIDATES = [
|
|
|
110
112
|
"continue",
|
|
111
113
|
];
|
|
112
114
|
const MAX_AUTO_DISCOVERED_SPEAKERS = 12;
|
|
115
|
+
const TELEPTY_CONFIG_FILE = path.join(HOME, ".telepty", "config.json");
|
|
116
|
+
const TELEPTY_DEFAULT_HOST = process.env.TELEPTY_HOST || "127.0.0.1";
|
|
117
|
+
const TELEPTY_PORT = Number(process.env.TELEPTY_PORT || 3848);
|
|
118
|
+
const TELEPTY_TRANSPORT_TIMEOUT_MS = 5_000;
|
|
119
|
+
const TELEPTY_SEMANTIC_TIMEOUT_MS = 60_000;
|
|
120
|
+
const TELEPTY_BUS_RECONNECT_MS = 5_000;
|
|
121
|
+
const TELEPTY_SESSION_HEALTH_STALE_MS = 25_000;
|
|
113
122
|
|
|
114
123
|
function loadDeliberationConfig() {
|
|
115
124
|
const configPath = path.join(INSTALL_DIR, "config.json");
|
|
@@ -126,6 +135,57 @@ function saveDeliberationConfig(config) {
|
|
|
126
135
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
127
136
|
}
|
|
128
137
|
|
|
138
|
+
const StructuredActionableTaskSchema = z.object({
|
|
139
|
+
id: z.number(),
|
|
140
|
+
task: z.string(),
|
|
141
|
+
files: z.array(z.string()).optional(),
|
|
142
|
+
project: z.string().optional(),
|
|
143
|
+
priority: z.enum(["high", "medium", "low"]).optional(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const StructuredSynthesisSchema = z.object({
|
|
147
|
+
summary: z.string(),
|
|
148
|
+
decisions: z.array(z.string()),
|
|
149
|
+
actionable_tasks: z.array(StructuredActionableTaskSchema),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const TeleptyEnvelopeSchema = z.object({
|
|
153
|
+
message_id: z.string().min(1),
|
|
154
|
+
session_id: z.string().min(1),
|
|
155
|
+
project: z.string().min(1),
|
|
156
|
+
kind: z.string().min(1),
|
|
157
|
+
source: z.string().min(1),
|
|
158
|
+
target: z.string().min(1),
|
|
159
|
+
reply_to: z.string().nullable().optional(),
|
|
160
|
+
trace: z.array(z.string()),
|
|
161
|
+
payload: z.unknown(),
|
|
162
|
+
ts: z.string().min(1),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const TeleptyTurnRequestPayloadSchema = z.object({
|
|
166
|
+
turn_id: z.string().min(1),
|
|
167
|
+
round: z.number().int().positive(),
|
|
168
|
+
max_rounds: z.number().int().positive(),
|
|
169
|
+
speaker: z.string().min(1),
|
|
170
|
+
role: z.string().nullable().optional(),
|
|
171
|
+
prompt: z.string().min(1),
|
|
172
|
+
prompt_sha1: z.string().length(40),
|
|
173
|
+
history_entries: z.number().int().nonnegative().optional(),
|
|
174
|
+
transport_timeout_ms: z.number().int().positive(),
|
|
175
|
+
semantic_timeout_ms: z.number().int().positive(),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const TeleptyDeliberationCompletedPayloadSchema = z.object({
|
|
179
|
+
topic: z.string(),
|
|
180
|
+
synthesis: z.string(),
|
|
181
|
+
structured_synthesis: StructuredSynthesisSchema.nullable().optional(),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const TELEPTY_ENVELOPE_PAYLOAD_SCHEMAS = {
|
|
185
|
+
turn_request: TeleptyTurnRequestPayloadSchema,
|
|
186
|
+
deliberation_completed: TeleptyDeliberationCompletedPayloadSchema,
|
|
187
|
+
};
|
|
188
|
+
|
|
129
189
|
const DEFAULT_BROWSER_APPS = ["Google Chrome", "Brave Browser", "Arc", "Microsoft Edge", "Safari"];
|
|
130
190
|
const DEFAULT_LLM_DOMAINS = [
|
|
131
191
|
"chatgpt.com",
|
|
@@ -345,33 +405,441 @@ const LOCKS_SUBDIR = ".locks";
|
|
|
345
405
|
const LOCK_RETRY_MS = 25;
|
|
346
406
|
const LOCK_TIMEOUT_MS = 8000;
|
|
347
407
|
const LOCK_STALE_MS = 60000;
|
|
408
|
+
const SPEAKER_SELECTION_FILE = "speaker-selection.json";
|
|
409
|
+
const SPEAKER_SELECTION_TTL_MS = 10 * 60 * 1000;
|
|
348
410
|
|
|
349
411
|
function getProjectSlug() {
|
|
350
412
|
return path.basename(process.cwd());
|
|
351
413
|
}
|
|
352
414
|
|
|
353
|
-
function
|
|
354
|
-
|
|
415
|
+
function normalizeProjectSlug(projectSlug) {
|
|
416
|
+
if (typeof projectSlug === "string" && projectSlug.trim()) {
|
|
417
|
+
return projectSlug.trim();
|
|
418
|
+
}
|
|
419
|
+
return getProjectSlug();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function getProjectStateDir(projectSlug = getProjectSlug()) {
|
|
423
|
+
return path.join(GLOBAL_STATE_DIR, normalizeProjectSlug(projectSlug));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function getSessionsDir(projectSlug = getProjectSlug()) {
|
|
427
|
+
return path.join(getProjectStateDir(projectSlug), "sessions");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function getSessionProject(sessionRef, fallbackProject = getProjectSlug()) {
|
|
431
|
+
if (sessionRef && typeof sessionRef === "object" && typeof sessionRef.project === "string" && sessionRef.project.trim()) {
|
|
432
|
+
return sessionRef.project.trim();
|
|
433
|
+
}
|
|
434
|
+
return normalizeProjectSlug(fallbackProject);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function getSessionFile(sessionRef, projectSlug) {
|
|
438
|
+
const sessionId = typeof sessionRef === "object" && sessionRef !== null
|
|
439
|
+
? sessionRef.id
|
|
440
|
+
: sessionRef;
|
|
441
|
+
return path.join(getSessionsDir(getSessionProject(sessionRef, projectSlug)), `${sessionId}.json`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function listStateProjects() {
|
|
445
|
+
if (!fs.existsSync(GLOBAL_STATE_DIR)) return [];
|
|
446
|
+
try {
|
|
447
|
+
return fs.readdirSync(GLOBAL_STATE_DIR, { withFileTypes: true })
|
|
448
|
+
.filter(entry => entry.isDirectory())
|
|
449
|
+
.map(entry => entry.name);
|
|
450
|
+
} catch {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function findSessionRecord(sessionRef, { preferProject, activeOnly = false } = {}) {
|
|
456
|
+
if (!sessionRef) return null;
|
|
457
|
+
|
|
458
|
+
if (typeof sessionRef === "object" && sessionRef !== null && sessionRef.id) {
|
|
459
|
+
const project = getSessionProject(sessionRef, preferProject);
|
|
460
|
+
const file = getSessionFile(sessionRef.id, project);
|
|
461
|
+
const state = readJsonFileSafe(file);
|
|
462
|
+
if (!state) return null;
|
|
463
|
+
const normalized = normalizeSessionActors(state);
|
|
464
|
+
if (activeOnly && normalized.status !== "active" && normalized.status !== "awaiting_synthesis") {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
return { file, project, state: normalized };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const sessionId = String(sessionRef);
|
|
471
|
+
const preferred = normalizeProjectSlug(preferProject);
|
|
472
|
+
const projects = [...new Set([preferred, ...listStateProjects()])];
|
|
473
|
+
for (const project of projects) {
|
|
474
|
+
const file = getSessionFile(sessionId, project);
|
|
475
|
+
const state = readJsonFileSafe(file);
|
|
476
|
+
if (!state) continue;
|
|
477
|
+
const normalized = normalizeSessionActors(state);
|
|
478
|
+
if (activeOnly && normalized.status !== "active" && normalized.status !== "awaiting_synthesis") {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
return { file, project: normalized.project || project, state: normalized };
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const teleptyBusState = {
|
|
487
|
+
ws: null,
|
|
488
|
+
status: "idle",
|
|
489
|
+
connectPromise: null,
|
|
490
|
+
reconnectTimer: null,
|
|
491
|
+
lastError: null,
|
|
492
|
+
lastConnectedAt: null,
|
|
493
|
+
lastMessageAt: null,
|
|
494
|
+
healthBySession: new Map(),
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const pendingTeleptyTurnRequests = new Map();
|
|
498
|
+
|
|
499
|
+
function hashPromptText(value) {
|
|
500
|
+
return createHash("sha1").update(String(value || "")).digest("hex");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function createEnvelopeId(prefix = "env") {
|
|
504
|
+
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function validateTeleptyEnvelope(envelope) {
|
|
508
|
+
const parsed = TeleptyEnvelopeSchema.parse(envelope);
|
|
509
|
+
const payloadSchema = TELEPTY_ENVELOPE_PAYLOAD_SCHEMAS[parsed.kind];
|
|
510
|
+
if (payloadSchema) {
|
|
511
|
+
payloadSchema.parse(parsed.payload);
|
|
512
|
+
}
|
|
513
|
+
return parsed;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function buildTeleptyEnvelope({ session_id, project, kind, source, target, reply_to = null, trace = [], payload, ts = new Date().toISOString(), message_id = createEnvelopeId(kind) }) {
|
|
517
|
+
return validateTeleptyEnvelope({
|
|
518
|
+
message_id,
|
|
519
|
+
session_id,
|
|
520
|
+
project,
|
|
521
|
+
kind,
|
|
522
|
+
source,
|
|
523
|
+
target,
|
|
524
|
+
reply_to,
|
|
525
|
+
trace,
|
|
526
|
+
payload,
|
|
527
|
+
ts,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function buildTeleptyTurnRequestEnvelope({ state, speaker, turnId, turnPrompt, includeHistoryEntries = 0, profile }) {
|
|
532
|
+
const role = (state.speaker_roles || {})[speaker] || null;
|
|
533
|
+
const target = profile?.telepty_host && !["127.0.0.1", "localhost"].includes(profile.telepty_host)
|
|
534
|
+
? `${profile.telepty_session_id}@${profile.telepty_host}`
|
|
535
|
+
: profile?.telepty_session_id || speaker;
|
|
536
|
+
return buildTeleptyEnvelope({
|
|
537
|
+
session_id: state.id,
|
|
538
|
+
project: state.project || getProjectSlug(),
|
|
539
|
+
kind: "turn_request",
|
|
540
|
+
source: `deliberation:${state.id}`,
|
|
541
|
+
target,
|
|
542
|
+
reply_to: state.id,
|
|
543
|
+
trace: [
|
|
544
|
+
`project:${state.project || getProjectSlug()}`,
|
|
545
|
+
`speaker:${speaker}`,
|
|
546
|
+
`turn:${turnId}`,
|
|
547
|
+
],
|
|
548
|
+
payload: {
|
|
549
|
+
turn_id: turnId,
|
|
550
|
+
round: state.current_round,
|
|
551
|
+
max_rounds: state.max_rounds,
|
|
552
|
+
speaker,
|
|
553
|
+
role,
|
|
554
|
+
prompt: turnPrompt,
|
|
555
|
+
prompt_sha1: hashPromptText(turnPrompt),
|
|
556
|
+
history_entries: includeHistoryEntries,
|
|
557
|
+
transport_timeout_ms: TELEPTY_TRANSPORT_TIMEOUT_MS,
|
|
558
|
+
semantic_timeout_ms: TELEPTY_SEMANTIC_TIMEOUT_MS,
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function buildTeleptySynthesisEnvelope({ state, synthesis, structured }) {
|
|
564
|
+
return buildTeleptyEnvelope({
|
|
565
|
+
session_id: state.id,
|
|
566
|
+
project: state.project || getProjectSlug(),
|
|
567
|
+
kind: "deliberation_completed",
|
|
568
|
+
source: `deliberation:${state.id}`,
|
|
569
|
+
target: "telepty-bus",
|
|
570
|
+
reply_to: state.id,
|
|
571
|
+
trace: [
|
|
572
|
+
`project:${state.project || getProjectSlug()}`,
|
|
573
|
+
"stage:synthesis",
|
|
574
|
+
],
|
|
575
|
+
payload: {
|
|
576
|
+
topic: state.topic,
|
|
577
|
+
synthesis,
|
|
578
|
+
structured_synthesis: structured || null,
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function resolveTeleptyBusUrl(host = TELEPTY_DEFAULT_HOST) {
|
|
584
|
+
const url = new URL(`ws://${host}:${TELEPTY_PORT}/api/bus`);
|
|
585
|
+
const token = loadTeleptyAuthToken();
|
|
586
|
+
if (token) {
|
|
587
|
+
url.searchParams.set("token", token);
|
|
588
|
+
}
|
|
589
|
+
return url.toString();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function cleanupPendingTeleptyTurn(messageId) {
|
|
593
|
+
const entry = pendingTeleptyTurnRequests.get(messageId);
|
|
594
|
+
if (!entry) return;
|
|
595
|
+
if (entry.transportTimer) clearTimeout(entry.transportTimer);
|
|
596
|
+
if (entry.semanticTimer) clearTimeout(entry.semanticTimer);
|
|
597
|
+
pendingTeleptyTurnRequests.delete(messageId);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function registerPendingTeleptyTurnRequest({ envelope, profile, speaker }) {
|
|
601
|
+
const nowMs = Date.now();
|
|
602
|
+
const entry = {
|
|
603
|
+
message_id: envelope.message_id,
|
|
604
|
+
deliberation_session_id: envelope.session_id,
|
|
605
|
+
project: envelope.project,
|
|
606
|
+
speaker,
|
|
607
|
+
turn_id: envelope.payload.turn_id,
|
|
608
|
+
target_session_id: profile?.telepty_session_id || speaker,
|
|
609
|
+
target_host: profile?.telepty_host || TELEPTY_DEFAULT_HOST,
|
|
610
|
+
prompt_sha1: envelope.payload.prompt_sha1,
|
|
611
|
+
published_at: envelope.ts,
|
|
612
|
+
transport_status: "pending",
|
|
613
|
+
semantic_status: "pending",
|
|
614
|
+
transport_deadline_at: new Date(nowMs + TELEPTY_TRANSPORT_TIMEOUT_MS).toISOString(),
|
|
615
|
+
semantic_deadline_at: new Date(nowMs + TELEPTY_SEMANTIC_TIMEOUT_MS).toISOString(),
|
|
616
|
+
};
|
|
617
|
+
entry.transportPromise = new Promise(resolve => {
|
|
618
|
+
entry.resolveTransport = resolve;
|
|
619
|
+
});
|
|
620
|
+
entry.semanticPromise = new Promise(resolve => {
|
|
621
|
+
entry.resolveSemantic = resolve;
|
|
622
|
+
});
|
|
623
|
+
entry.transportTimer = setTimeout(() => {
|
|
624
|
+
if (entry.transport_status !== "pending") return;
|
|
625
|
+
entry.transport_status = "timeout";
|
|
626
|
+
appendRuntimeLog("WARN", `TELEPTY_TRANSPORT_TIMEOUT: ${entry.deliberation_session_id} | speaker: ${entry.speaker} | target: ${entry.target_session_id}`);
|
|
627
|
+
entry.resolveTransport?.({ ok: false, code: "transport_timeout" });
|
|
628
|
+
}, TELEPTY_TRANSPORT_TIMEOUT_MS);
|
|
629
|
+
entry.semanticTimer = setTimeout(() => {
|
|
630
|
+
if (entry.semantic_status !== "pending") return;
|
|
631
|
+
entry.semantic_status = "timeout";
|
|
632
|
+
appendRuntimeLog("WARN", `TELEPTY_SEMANTIC_TIMEOUT: ${entry.deliberation_session_id} | speaker: ${entry.speaker} | target: ${entry.target_session_id}`);
|
|
633
|
+
entry.resolveSemantic?.({ ok: false, code: "semantic_timeout" });
|
|
634
|
+
setTimeout(() => cleanupPendingTeleptyTurn(entry.message_id), 5_000);
|
|
635
|
+
}, TELEPTY_SEMANTIC_TIMEOUT_MS);
|
|
636
|
+
pendingTeleptyTurnRequests.set(entry.message_id, entry);
|
|
637
|
+
return entry;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function ackPendingTeleptyTurn(event) {
|
|
641
|
+
const promptHash = hashPromptText(event?.content || "");
|
|
642
|
+
const targetSessionId = String(event?.target_agent || "");
|
|
643
|
+
const candidate = [...pendingTeleptyTurnRequests.values()]
|
|
644
|
+
.filter(entry =>
|
|
645
|
+
entry.transport_status === "pending"
|
|
646
|
+
&& entry.target_session_id === targetSessionId
|
|
647
|
+
&& entry.prompt_sha1 === promptHash
|
|
648
|
+
)
|
|
649
|
+
.sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0];
|
|
650
|
+
if (!candidate) return null;
|
|
651
|
+
|
|
652
|
+
candidate.transport_status = "ack";
|
|
653
|
+
candidate.inject_id = event.inject_id || null;
|
|
654
|
+
candidate.transport_acked_at = new Date().toISOString();
|
|
655
|
+
if (candidate.transportTimer) clearTimeout(candidate.transportTimer);
|
|
656
|
+
candidate.resolveTransport?.({
|
|
657
|
+
ok: true,
|
|
658
|
+
code: "inject_written",
|
|
659
|
+
inject_id: event.inject_id || null,
|
|
660
|
+
});
|
|
661
|
+
appendRuntimeLog("INFO", `TELEPTY_TRANSPORT_ACK: ${candidate.deliberation_session_id} | speaker: ${candidate.speaker} | target: ${candidate.target_session_id} | inject_id: ${event.inject_id || "n/a"}`);
|
|
662
|
+
return candidate;
|
|
355
663
|
}
|
|
356
664
|
|
|
357
|
-
function
|
|
358
|
-
|
|
665
|
+
function completePendingTeleptySemantic({ sessionId, speaker, turnId }) {
|
|
666
|
+
const candidate = [...pendingTeleptyTurnRequests.values()]
|
|
667
|
+
.filter(entry =>
|
|
668
|
+
entry.semantic_status === "pending"
|
|
669
|
+
&& entry.deliberation_session_id === sessionId
|
|
670
|
+
&& normalizeSpeaker(entry.speaker) === normalizeSpeaker(speaker)
|
|
671
|
+
&& (!turnId || !entry.turn_id || entry.turn_id === turnId)
|
|
672
|
+
)
|
|
673
|
+
.sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0];
|
|
674
|
+
if (!candidate) return null;
|
|
675
|
+
|
|
676
|
+
candidate.semantic_status = "completed";
|
|
677
|
+
candidate.semantic_completed_at = new Date().toISOString();
|
|
678
|
+
if (candidate.semanticTimer) clearTimeout(candidate.semanticTimer);
|
|
679
|
+
candidate.resolveSemantic?.({ ok: true, code: "responded" });
|
|
680
|
+
appendRuntimeLog("INFO", `TELEPTY_SEMANTIC_COMPLETE: ${candidate.deliberation_session_id} | speaker: ${candidate.speaker} | target: ${candidate.target_session_id}`);
|
|
681
|
+
setTimeout(() => cleanupPendingTeleptyTurn(candidate.message_id), 5_000);
|
|
682
|
+
return candidate;
|
|
359
683
|
}
|
|
360
684
|
|
|
361
|
-
function
|
|
362
|
-
|
|
685
|
+
function updateTeleptySessionHealth(event) {
|
|
686
|
+
const sessionId = event?.session_id;
|
|
687
|
+
if (!sessionId) return null;
|
|
688
|
+
const health = {
|
|
689
|
+
session_id: sessionId,
|
|
690
|
+
payload: event.payload || {},
|
|
691
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
692
|
+
seen_at: new Date().toISOString(),
|
|
693
|
+
};
|
|
694
|
+
teleptyBusState.healthBySession.set(sessionId, health);
|
|
695
|
+
return health;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function getTeleptySessionHealth(sessionId, nowMs = Date.now()) {
|
|
699
|
+
const entry = teleptyBusState.healthBySession.get(sessionId);
|
|
700
|
+
if (!entry) return null;
|
|
701
|
+
const seenAtMs = Date.parse(entry.seen_at || entry.timestamp || "");
|
|
702
|
+
const ageMs = Number.isFinite(seenAtMs) ? nowMs - seenAtMs : null;
|
|
703
|
+
return {
|
|
704
|
+
...entry,
|
|
705
|
+
age_ms: ageMs,
|
|
706
|
+
stale: Number.isFinite(ageMs) ? ageMs > TELEPTY_SESSION_HEALTH_STALE_MS : true,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function handleTeleptyBusMessage(raw) {
|
|
711
|
+
let parsed = null;
|
|
712
|
+
try {
|
|
713
|
+
parsed = JSON.parse(String(raw));
|
|
714
|
+
} catch {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
teleptyBusState.lastMessageAt = new Date().toISOString();
|
|
718
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
719
|
+
|
|
720
|
+
if (parsed.type === "inject_written") {
|
|
721
|
+
return ackPendingTeleptyTurn(parsed);
|
|
722
|
+
}
|
|
723
|
+
if (parsed.type === "session_health") {
|
|
724
|
+
return updateTeleptySessionHealth(parsed);
|
|
725
|
+
}
|
|
726
|
+
return parsed;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async function ensureTeleptyBusSubscriber() {
|
|
730
|
+
if (teleptyBusState.ws && teleptyBusState.ws.readyState === WebSocket.OPEN) {
|
|
731
|
+
return { ok: true, status: "open" };
|
|
732
|
+
}
|
|
733
|
+
if (teleptyBusState.connectPromise) {
|
|
734
|
+
return teleptyBusState.connectPromise;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
teleptyBusState.connectPromise = new Promise((resolve) => {
|
|
738
|
+
try {
|
|
739
|
+
let settled = false;
|
|
740
|
+
const finish = (result) => {
|
|
741
|
+
if (settled) return;
|
|
742
|
+
settled = true;
|
|
743
|
+
resolve(result);
|
|
744
|
+
};
|
|
745
|
+
teleptyBusState.status = "connecting";
|
|
746
|
+
const ws = new WebSocket(resolveTeleptyBusUrl());
|
|
747
|
+
teleptyBusState.ws = ws;
|
|
748
|
+
|
|
749
|
+
ws.once("open", () => {
|
|
750
|
+
teleptyBusState.status = "open";
|
|
751
|
+
teleptyBusState.lastConnectedAt = new Date().toISOString();
|
|
752
|
+
teleptyBusState.lastError = null;
|
|
753
|
+
appendRuntimeLog("INFO", "TELEPTY_BUS_CONNECTED");
|
|
754
|
+
finish({ ok: true, status: "open" });
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
ws.on("message", (data) => {
|
|
758
|
+
handleTeleptyBusMessage(data.toString());
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
ws.on("error", (err) => {
|
|
762
|
+
teleptyBusState.lastError = String(err?.message || err);
|
|
763
|
+
appendRuntimeLog("WARN", `TELEPTY_BUS_ERROR: ${teleptyBusState.lastError}`);
|
|
764
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
765
|
+
teleptyBusState.status = "error";
|
|
766
|
+
teleptyBusState.ws = null;
|
|
767
|
+
teleptyBusState.connectPromise = null;
|
|
768
|
+
finish({ ok: false, status: "error", error: teleptyBusState.lastError });
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
ws.on("close", () => {
|
|
773
|
+
teleptyBusState.status = "closed";
|
|
774
|
+
teleptyBusState.ws = null;
|
|
775
|
+
teleptyBusState.connectPromise = null;
|
|
776
|
+
if (!settled) {
|
|
777
|
+
finish({ ok: false, status: "closed", error: teleptyBusState.lastError || "socket closed" });
|
|
778
|
+
}
|
|
779
|
+
if (!teleptyBusState.reconnectTimer) {
|
|
780
|
+
teleptyBusState.reconnectTimer = setTimeout(() => {
|
|
781
|
+
teleptyBusState.reconnectTimer = null;
|
|
782
|
+
ensureTeleptyBusSubscriber().catch(() => {});
|
|
783
|
+
}, TELEPTY_BUS_RECONNECT_MS);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
} catch (err) {
|
|
787
|
+
teleptyBusState.status = "error";
|
|
788
|
+
teleptyBusState.lastError = String(err?.message || err);
|
|
789
|
+
teleptyBusState.connectPromise = null;
|
|
790
|
+
resolve({ ok: false, status: "error", error: teleptyBusState.lastError });
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
const result = await teleptyBusState.connectPromise;
|
|
795
|
+
if (!result.ok) {
|
|
796
|
+
teleptyBusState.connectPromise = null;
|
|
797
|
+
} else if (teleptyBusState.ws?.readyState === WebSocket.OPEN) {
|
|
798
|
+
teleptyBusState.connectPromise = null;
|
|
799
|
+
}
|
|
800
|
+
return result;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function notifyTeleptyBus(event) {
|
|
804
|
+
const host = process.env.TELEPTY_HOST || "localhost";
|
|
805
|
+
const port = process.env.TELEPTY_PORT || "3848";
|
|
806
|
+
const token = loadTeleptyAuthToken();
|
|
807
|
+
try {
|
|
808
|
+
const res = await fetch(`http://${host}:${port}/api/bus/publish`, {
|
|
809
|
+
method: "POST",
|
|
810
|
+
headers: {
|
|
811
|
+
"Content-Type": "application/json",
|
|
812
|
+
...(token ? { "x-telepty-token": token } : {}),
|
|
813
|
+
},
|
|
814
|
+
body: JSON.stringify(event),
|
|
815
|
+
});
|
|
816
|
+
const data = await res.json().catch(() => null);
|
|
817
|
+
if (res.ok) {
|
|
818
|
+
appendRuntimeLog("INFO", `HANDOFF: Telepty bus notified: ${event.kind || event.type || "unknown"}`);
|
|
819
|
+
return { ok: true, delivered: data?.delivered ?? null };
|
|
820
|
+
}
|
|
821
|
+
return { ok: false, status: res.status, error: data?.error || `HTTP ${res.status}` };
|
|
822
|
+
} catch (err) {
|
|
823
|
+
appendRuntimeLog("WARN", `HANDOFF: Telepty bus notification failed: ${err.message}`);
|
|
824
|
+
return { ok: false, error: err.message };
|
|
825
|
+
}
|
|
363
826
|
}
|
|
364
827
|
|
|
365
|
-
function getArchiveDir() {
|
|
366
|
-
const
|
|
367
|
-
|
|
828
|
+
function getArchiveDir(projectSlug = getProjectSlug()) {
|
|
829
|
+
const slug = normalizeProjectSlug(projectSlug);
|
|
830
|
+
const obsidianDir = path.join(OBSIDIAN_PROJECTS, slug, "deliberations");
|
|
831
|
+
if (fs.existsSync(path.join(OBSIDIAN_PROJECTS, slug))) {
|
|
368
832
|
return obsidianDir;
|
|
369
833
|
}
|
|
370
|
-
return path.join(getProjectStateDir(), "archive");
|
|
834
|
+
return path.join(getProjectStateDir(slug), "archive");
|
|
371
835
|
}
|
|
372
836
|
|
|
373
|
-
function getLocksDir() {
|
|
374
|
-
return path.join(getProjectStateDir(), LOCKS_SUBDIR);
|
|
837
|
+
function getLocksDir(projectSlug = getProjectSlug()) {
|
|
838
|
+
return path.join(getProjectStateDir(projectSlug), LOCKS_SUBDIR);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function getSpeakerSelectionFile(projectSlug = getProjectSlug()) {
|
|
842
|
+
return path.join(getProjectStateDir(projectSlug), SPEAKER_SELECTION_FILE);
|
|
375
843
|
}
|
|
376
844
|
|
|
377
845
|
function formatRuntimeError(error) {
|
|
@@ -429,6 +897,19 @@ function writeTextAtomic(filePath, text) {
|
|
|
429
897
|
fs.renameSync(tmp, filePath);
|
|
430
898
|
}
|
|
431
899
|
|
|
900
|
+
function readJsonFileSafe(filePath) {
|
|
901
|
+
try {
|
|
902
|
+
if (!fs.existsSync(filePath)) return null;
|
|
903
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
904
|
+
} catch {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function writeJsonFileAtomic(filePath, value) {
|
|
910
|
+
writeTextAtomic(filePath, JSON.stringify(value, null, 2));
|
|
911
|
+
}
|
|
912
|
+
|
|
432
913
|
function acquireFileLock(lockPath, {
|
|
433
914
|
timeoutMs = LOCK_TIMEOUT_MS,
|
|
434
915
|
retryMs = LOCK_RETRY_MS,
|
|
@@ -488,13 +969,20 @@ function withFileLock(lockPath, fn, options) {
|
|
|
488
969
|
}
|
|
489
970
|
}
|
|
490
971
|
|
|
491
|
-
function withProjectLock(fn, options) {
|
|
492
|
-
|
|
972
|
+
function withProjectLock(projectSlug, fn, options) {
|
|
973
|
+
if (typeof projectSlug === "function") {
|
|
974
|
+
return withFileLock(path.join(getLocksDir(), "_project.lock"), projectSlug, fn);
|
|
975
|
+
}
|
|
976
|
+
return withFileLock(path.join(getLocksDir(projectSlug), "_project.lock"), fn, options);
|
|
493
977
|
}
|
|
494
978
|
|
|
495
|
-
function withSessionLock(
|
|
979
|
+
function withSessionLock(sessionRef, fn, options) {
|
|
980
|
+
const sessionId = typeof sessionRef === "object" && sessionRef !== null ? sessionRef.id : sessionRef;
|
|
981
|
+
const explicitProject = typeof sessionRef === "object" && sessionRef !== null ? sessionRef.project : null;
|
|
982
|
+
const record = findSessionRecord(sessionRef, { preferProject: explicitProject || getProjectSlug() });
|
|
983
|
+
const projectSlug = explicitProject || record?.project || getProjectSlug();
|
|
496
984
|
const safeId = String(sessionId).replace(/[^a-zA-Z0-9가-힣._-]/g, "_");
|
|
497
|
-
return withFileLock(path.join(getLocksDir(), `${safeId}.lock`), fn, options);
|
|
985
|
+
return withFileLock(path.join(getLocksDir(projectSlug), `${safeId}.lock`), fn, options);
|
|
498
986
|
}
|
|
499
987
|
|
|
500
988
|
function normalizeSpeaker(raw) {
|
|
@@ -516,6 +1004,151 @@ function dedupeSpeakers(items = []) {
|
|
|
516
1004
|
return out;
|
|
517
1005
|
}
|
|
518
1006
|
|
|
1007
|
+
function createSelectionToken() {
|
|
1008
|
+
return `sel-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function issueSpeakerSelectionToken({ candidates, include_browser }) {
|
|
1012
|
+
const selectionState = {
|
|
1013
|
+
token: createSelectionToken(),
|
|
1014
|
+
phase: "candidates",
|
|
1015
|
+
created_at: new Date().toISOString(),
|
|
1016
|
+
include_browser: !!include_browser,
|
|
1017
|
+
candidate_speakers: dedupeSpeakers((candidates || []).map(c => typeof c === "string" ? c : c?.speaker)),
|
|
1018
|
+
};
|
|
1019
|
+
writeJsonFileAtomic(getSpeakerSelectionFile(), selectionState);
|
|
1020
|
+
return selectionState;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function loadSpeakerSelectionToken() {
|
|
1024
|
+
return readJsonFileSafe(getSpeakerSelectionFile());
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function clearSpeakerSelectionToken() {
|
|
1028
|
+
try {
|
|
1029
|
+
fs.unlinkSync(getSpeakerSelectionFile());
|
|
1030
|
+
} catch {
|
|
1031
|
+
// ignore missing file
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function validateSpeakerSelectionSnapshot({ selectionState, selection_token, speakers, includeBrowserSpeakers, nowMs = Date.now() }) {
|
|
1036
|
+
if (!selection_token) {
|
|
1037
|
+
return { ok: false, code: "missing_token" };
|
|
1038
|
+
}
|
|
1039
|
+
if (!selectionState?.token) {
|
|
1040
|
+
return { ok: false, code: "missing_selection_state" };
|
|
1041
|
+
}
|
|
1042
|
+
if (selectionState.token !== selection_token) {
|
|
1043
|
+
return { ok: false, code: "token_mismatch" };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const createdAtMs = Date.parse(selectionState.created_at || "");
|
|
1047
|
+
if (!Number.isFinite(createdAtMs) || (nowMs - createdAtMs) > SPEAKER_SELECTION_TTL_MS) {
|
|
1048
|
+
return { ok: false, code: "expired_token" };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (!!selectionState.include_browser !== !!includeBrowserSpeakers) {
|
|
1052
|
+
return { ok: false, code: "mode_mismatch" };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const availableSpeakers = new Set(dedupeSpeakers(selectionState.candidate_speakers || []));
|
|
1056
|
+
const requestedSpeakers = dedupeSpeakers(speakers || []);
|
|
1057
|
+
const missingSpeakers = requestedSpeakers.filter(speaker => !availableSpeakers.has(speaker));
|
|
1058
|
+
if (missingSpeakers.length > 0) {
|
|
1059
|
+
return { ok: false, code: "speaker_mismatch", missing_speakers: missingSpeakers };
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return { ok: true };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function confirmSpeakerSelectionToken({ selectionState, selection_token, speakers, includeBrowserSpeakers, nowMs = Date.now(), persist = true }) {
|
|
1066
|
+
const snapshotValidation = validateSpeakerSelectionSnapshot({
|
|
1067
|
+
selectionState,
|
|
1068
|
+
selection_token,
|
|
1069
|
+
speakers,
|
|
1070
|
+
includeBrowserSpeakers,
|
|
1071
|
+
nowMs,
|
|
1072
|
+
});
|
|
1073
|
+
if (!snapshotValidation.ok) {
|
|
1074
|
+
return snapshotValidation;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const confirmedSelection = {
|
|
1078
|
+
token: createSelectionToken(),
|
|
1079
|
+
phase: "confirmed",
|
|
1080
|
+
created_at: new Date(nowMs).toISOString(),
|
|
1081
|
+
include_browser: !!includeBrowserSpeakers,
|
|
1082
|
+
candidate_speakers: dedupeSpeakers(selectionState.candidate_speakers || []),
|
|
1083
|
+
selected_speakers: dedupeSpeakers(speakers || []),
|
|
1084
|
+
};
|
|
1085
|
+
if (persist) {
|
|
1086
|
+
writeJsonFileAtomic(getSpeakerSelectionFile(), confirmedSelection);
|
|
1087
|
+
}
|
|
1088
|
+
return { ok: true, selectionState: confirmedSelection };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function validateSpeakerSelectionRequest({ selectionState, selection_token, speakers, includeBrowserSpeakers, nowMs = Date.now() }) {
|
|
1092
|
+
const snapshotValidation = validateSpeakerSelectionSnapshot({
|
|
1093
|
+
selectionState,
|
|
1094
|
+
selection_token,
|
|
1095
|
+
speakers,
|
|
1096
|
+
includeBrowserSpeakers,
|
|
1097
|
+
nowMs,
|
|
1098
|
+
});
|
|
1099
|
+
if (!snapshotValidation.ok) {
|
|
1100
|
+
return snapshotValidation;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (selectionState.phase !== "confirmed" || !Array.isArray(selectionState.selected_speakers)) {
|
|
1104
|
+
return { ok: false, code: "selection_not_confirmed" };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const expectedSpeakers = dedupeSpeakers(selectionState.selected_speakers || []);
|
|
1108
|
+
const requestedSpeakers = dedupeSpeakers(speakers || []);
|
|
1109
|
+
if (
|
|
1110
|
+
expectedSpeakers.length !== requestedSpeakers.length
|
|
1111
|
+
|| expectedSpeakers.some(speaker => !requestedSpeakers.includes(speaker))
|
|
1112
|
+
) {
|
|
1113
|
+
return {
|
|
1114
|
+
ok: false,
|
|
1115
|
+
code: "selected_speakers_mismatch",
|
|
1116
|
+
expected_speakers: expectedSpeakers,
|
|
1117
|
+
requested_speakers: requestedSpeakers,
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return { ok: true };
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function hasExplicitBrowserParticipantSelection({ speakers, participant_types } = {}) {
|
|
1125
|
+
const manualSpeakers = Array.isArray(speakers) ? speakers : [];
|
|
1126
|
+
const hasBrowserSpeaker = manualSpeakers.some(speaker => {
|
|
1127
|
+
const normalized = normalizeSpeaker(speaker);
|
|
1128
|
+
return normalized?.startsWith("web-");
|
|
1129
|
+
});
|
|
1130
|
+
if (hasBrowserSpeaker) return true;
|
|
1131
|
+
|
|
1132
|
+
const overrides = participant_types && typeof participant_types === "object"
|
|
1133
|
+
? Object.entries(participant_types)
|
|
1134
|
+
: [];
|
|
1135
|
+
|
|
1136
|
+
return overrides.some(([speaker, type]) => {
|
|
1137
|
+
const normalized = normalizeSpeaker(speaker);
|
|
1138
|
+
return normalized?.startsWith("web-") || type === "browser" || type === "browser_auto";
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function resolveIncludeBrowserSpeakers({ include_browser_speakers, config, speakers, participant_types } = {}) {
|
|
1143
|
+
if (include_browser_speakers !== undefined && include_browser_speakers !== null) {
|
|
1144
|
+
return include_browser_speakers;
|
|
1145
|
+
}
|
|
1146
|
+
if (config?.include_browser_speakers !== undefined && config?.include_browser_speakers !== null) {
|
|
1147
|
+
return config.include_browser_speakers;
|
|
1148
|
+
}
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
519
1152
|
function resolveCliCandidates() {
|
|
520
1153
|
const fromEnv = (process.env.DELIBERATION_CLI_CANDIDATES || "")
|
|
521
1154
|
.split(/[,\s]+/)
|
|
@@ -531,6 +1164,125 @@ function resolveCliCandidates() {
|
|
|
531
1164
|
return dedupeSpeakers([...fromEnv, ...DEFAULT_CLI_CANDIDATES]);
|
|
532
1165
|
}
|
|
533
1166
|
|
|
1167
|
+
function loadTeleptyAuthToken() {
|
|
1168
|
+
try {
|
|
1169
|
+
const raw = fs.readFileSync(TELEPTY_CONFIG_FILE, "utf-8");
|
|
1170
|
+
const parsed = JSON.parse(raw);
|
|
1171
|
+
return typeof parsed?.authToken === "string" && parsed.authToken.trim()
|
|
1172
|
+
? parsed.authToken.trim()
|
|
1173
|
+
: null;
|
|
1174
|
+
} catch {
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function formatTeleptyHostLabel(host) {
|
|
1180
|
+
return !host || host === "127.0.0.1" || host === "localhost" ? "Local" : host;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async function collectTeleptySessions() {
|
|
1184
|
+
const token = loadTeleptyAuthToken();
|
|
1185
|
+
if (!token) {
|
|
1186
|
+
return { sessions: [], note: "telepty auth token not found." };
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const host = TELEPTY_DEFAULT_HOST;
|
|
1190
|
+
try {
|
|
1191
|
+
const res = await fetch(`http://${host}:${TELEPTY_PORT}/api/sessions`, {
|
|
1192
|
+
headers: { "x-telepty-token": token },
|
|
1193
|
+
signal: AbortSignal.timeout(1500),
|
|
1194
|
+
});
|
|
1195
|
+
if (!res.ok) {
|
|
1196
|
+
return { sessions: [], note: `telepty daemon unavailable (${res.status}).` };
|
|
1197
|
+
}
|
|
1198
|
+
const sessions = await res.json();
|
|
1199
|
+
if (!Array.isArray(sessions)) {
|
|
1200
|
+
return { sessions: [], note: "telepty session response format was invalid." };
|
|
1201
|
+
}
|
|
1202
|
+
ensureTeleptyBusSubscriber().catch(() => {});
|
|
1203
|
+
return {
|
|
1204
|
+
sessions: sessions.map(session => ({ host, ...session })),
|
|
1205
|
+
note: null,
|
|
1206
|
+
};
|
|
1207
|
+
} catch {
|
|
1208
|
+
return { sessions: [], note: null };
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function scoreTeleptyProcessMatch(session, baseCommand = "", fullCommand = "") {
|
|
1213
|
+
const base = String(baseCommand || "").toLowerCase();
|
|
1214
|
+
const full = String(fullCommand || "").toLowerCase();
|
|
1215
|
+
const wanted = String(session?.command || "").trim().toLowerCase();
|
|
1216
|
+
let score = 0;
|
|
1217
|
+
|
|
1218
|
+
if (wanted && (base === wanted || full.startsWith(`${wanted} `) || full.includes(` ${wanted} `))) {
|
|
1219
|
+
score += 10;
|
|
1220
|
+
}
|
|
1221
|
+
if (base === "node" || base === "telepty") {
|
|
1222
|
+
score -= 2;
|
|
1223
|
+
}
|
|
1224
|
+
if (full.includes("mcp-deliberation") || full.includes("oh-my-claudecode") || full.includes("bridge/mcp-server")) {
|
|
1225
|
+
score -= 3;
|
|
1226
|
+
}
|
|
1227
|
+
return score;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function collectTeleptyProcessLocators(sessions = []) {
|
|
1231
|
+
const wantedSessions = new Map(
|
|
1232
|
+
sessions
|
|
1233
|
+
.filter(session => session?.id)
|
|
1234
|
+
.map(session => [String(session.id), session])
|
|
1235
|
+
);
|
|
1236
|
+
if (wantedSessions.size === 0) {
|
|
1237
|
+
return new Map();
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
try {
|
|
1241
|
+
const env = {
|
|
1242
|
+
HOME: process.env.HOME,
|
|
1243
|
+
PATH: process.env.PATH,
|
|
1244
|
+
SHELL: process.env.SHELL,
|
|
1245
|
+
USER: process.env.USER,
|
|
1246
|
+
LOGNAME: process.env.LOGNAME,
|
|
1247
|
+
TERM: process.env.TERM,
|
|
1248
|
+
};
|
|
1249
|
+
const raw = execFileSync("ps", ["eww", "-axo", "pid=,tty=,comm=,command="], {
|
|
1250
|
+
encoding: "utf-8",
|
|
1251
|
+
windowsHide: true,
|
|
1252
|
+
timeout: 2500,
|
|
1253
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
1254
|
+
env,
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
const best = new Map();
|
|
1258
|
+
for (const line of String(raw).split("\n")) {
|
|
1259
|
+
if (!line.includes("TELEPTY_SESSION_ID=")) continue;
|
|
1260
|
+
const match = line.match(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/);
|
|
1261
|
+
if (!match) continue;
|
|
1262
|
+
const [, pid, tty, comm, command] = match;
|
|
1263
|
+
const sessionIdMatch = command.match(/(?:^|\s)TELEPTY_SESSION_ID=([^\s]+)/);
|
|
1264
|
+
const sessionId = sessionIdMatch?.[1];
|
|
1265
|
+
if (!sessionId || !wantedSessions.has(sessionId)) continue;
|
|
1266
|
+
|
|
1267
|
+
const session = wantedSessions.get(sessionId);
|
|
1268
|
+
const score = scoreTeleptyProcessMatch(session, comm, command);
|
|
1269
|
+
const current = best.get(sessionId);
|
|
1270
|
+
if (!current || score > current.score) {
|
|
1271
|
+
best.set(sessionId, { pid: Number(pid), tty, score });
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
return new Map(
|
|
1276
|
+
[...best.entries()].map(([sessionId, value]) => [
|
|
1277
|
+
sessionId,
|
|
1278
|
+
{ pid: Number.isFinite(value.pid) ? value.pid : null, tty: value.tty || null },
|
|
1279
|
+
])
|
|
1280
|
+
);
|
|
1281
|
+
} catch {
|
|
1282
|
+
return new Map();
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
534
1286
|
function commandExistsInPath(command) {
|
|
535
1287
|
if (!command || !/^[a-zA-Z0-9._-]+$/.test(command)) {
|
|
536
1288
|
return false;
|
|
@@ -1143,6 +1895,7 @@ function inferLlmProvider(url = "", title = "") {
|
|
|
1143
1895
|
async function collectSpeakerCandidates({ include_cli = true, include_browser = true } = {}) {
|
|
1144
1896
|
const candidates = [];
|
|
1145
1897
|
const seen = new Set();
|
|
1898
|
+
let browserNote = null;
|
|
1146
1899
|
|
|
1147
1900
|
const add = (candidate) => {
|
|
1148
1901
|
const speaker = normalizeSpeaker(candidate?.speaker);
|
|
@@ -1162,9 +1915,29 @@ async function collectSpeakerCandidates({ include_cli = true, include_browser =
|
|
|
1162
1915
|
live,
|
|
1163
1916
|
});
|
|
1164
1917
|
}
|
|
1918
|
+
|
|
1919
|
+
const { sessions: teleptySessions, note: teleptyNote } = await collectTeleptySessions();
|
|
1920
|
+
const locators = collectTeleptyProcessLocators(teleptySessions);
|
|
1921
|
+
for (const session of teleptySessions) {
|
|
1922
|
+
const locator = locators.get(session.id) || {};
|
|
1923
|
+
add({
|
|
1924
|
+
speaker: session.id,
|
|
1925
|
+
type: "telepty",
|
|
1926
|
+
label: session.id,
|
|
1927
|
+
telepty_session_id: session.id,
|
|
1928
|
+
telepty_host: session.host || TELEPTY_DEFAULT_HOST,
|
|
1929
|
+
command: session.command || "wrapped",
|
|
1930
|
+
cwd: session.cwd || null,
|
|
1931
|
+
active_clients: session.active_clients ?? null,
|
|
1932
|
+
runtime_pid: locator.pid ?? null,
|
|
1933
|
+
runtime_tty: locator.tty ?? null,
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
if (teleptyNote) {
|
|
1937
|
+
browserNote = browserNote ? `${browserNote} | ${teleptyNote}` : teleptyNote;
|
|
1938
|
+
}
|
|
1165
1939
|
}
|
|
1166
1940
|
|
|
1167
|
-
let browserNote = null;
|
|
1168
1941
|
if (include_browser) {
|
|
1169
1942
|
// Ensure CDP is available before probing browser tabs
|
|
1170
1943
|
const cdpStatus = await ensureCdpAvailable();
|
|
@@ -1308,6 +2081,7 @@ async function collectSpeakerCandidates({ include_cli = true, include_browser =
|
|
|
1308
2081
|
|
|
1309
2082
|
function formatSpeakerCandidatesReport({ candidates, browserNote }) {
|
|
1310
2083
|
const cli = candidates.filter(c => c.type === "cli");
|
|
2084
|
+
const telepty = candidates.filter(c => c.type === "telepty");
|
|
1311
2085
|
const detected = candidates.filter(c => c.type === "browser" && !c.auto_registered);
|
|
1312
2086
|
const autoReg = candidates.filter(c => c.type === "browser" && c.auto_registered);
|
|
1313
2087
|
|
|
@@ -1322,6 +2096,22 @@ function formatSpeakerCandidatesReport({ candidates, browserNote }) {
|
|
|
1322
2096
|
}).join("\n")}\n\n`;
|
|
1323
2097
|
}
|
|
1324
2098
|
|
|
2099
|
+
out += "### Telepty Sessions\n";
|
|
2100
|
+
if (telepty.length === 0) {
|
|
2101
|
+
out += "- (No active telepty sessions)\n\n";
|
|
2102
|
+
} else {
|
|
2103
|
+
out += `${telepty.map(c => {
|
|
2104
|
+
const parts = [
|
|
2105
|
+
`command: ${c.command || "wrapped"}`,
|
|
2106
|
+
c.telepty_host ? `host: ${formatTeleptyHostLabel(c.telepty_host)}` : null,
|
|
2107
|
+
Number.isFinite(c.runtime_pid) ? `pid: ${c.runtime_pid}` : null,
|
|
2108
|
+
c.runtime_tty ? `tty: ${c.runtime_tty}` : null,
|
|
2109
|
+
].filter(Boolean).join(", ");
|
|
2110
|
+
const cwdLine = c.cwd ? `\n cwd: ${c.cwd}` : "";
|
|
2111
|
+
return `- \`${c.speaker}\` (${parts})${cwdLine}`;
|
|
2112
|
+
}).join("\n")}\n\n`;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
1325
2115
|
out += "### Browser LLM (detected)\n";
|
|
1326
2116
|
if (detected.length === 0) {
|
|
1327
2117
|
out += "- (No LLM tabs detected in browser)\n";
|
|
@@ -1403,6 +2193,18 @@ function mapParticipantProfiles(speakers, candidates, typeOverrides) {
|
|
|
1403
2193
|
continue;
|
|
1404
2194
|
}
|
|
1405
2195
|
|
|
2196
|
+
if (candidate.type === "telepty") {
|
|
2197
|
+
profiles.push({
|
|
2198
|
+
speaker,
|
|
2199
|
+
type: "telepty",
|
|
2200
|
+
command: candidate.command || null,
|
|
2201
|
+
telepty_session_id: candidate.telepty_session_id || speaker,
|
|
2202
|
+
telepty_host: candidate.telepty_host || null,
|
|
2203
|
+
runtime_pid: Number.isFinite(candidate.runtime_pid) ? candidate.runtime_pid : null,
|
|
2204
|
+
});
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
1406
2208
|
const effectiveType = candidate.cdp_available ? "browser_auto" : "browser";
|
|
1407
2209
|
profiles.push({
|
|
1408
2210
|
speaker,
|
|
@@ -1420,6 +2222,7 @@ function mapParticipantProfiles(speakers, candidates, typeOverrides) {
|
|
|
1420
2222
|
|
|
1421
2223
|
const TRANSPORT_TYPES = {
|
|
1422
2224
|
cli: "cli_respond",
|
|
2225
|
+
telepty: "telepty_bus",
|
|
1423
2226
|
browser: "clipboard",
|
|
1424
2227
|
browser_auto: "browser_auto",
|
|
1425
2228
|
manual: "manual",
|
|
@@ -1461,6 +2264,9 @@ const CLI_INVOCATION_HINTS = {
|
|
|
1461
2264
|
|
|
1462
2265
|
function formatTransportGuidance(transport, state, speaker) {
|
|
1463
2266
|
const sid = state.id;
|
|
2267
|
+
const profile = (state.participant_profiles || []).find(
|
|
2268
|
+
p => normalizeSpeaker(p.speaker) === normalizeSpeaker(speaker)
|
|
2269
|
+
) || null;
|
|
1464
2270
|
switch (transport) {
|
|
1465
2271
|
case "cli_respond": {
|
|
1466
2272
|
const hint = CLI_INVOCATION_HINTS[speaker] || null;
|
|
@@ -1485,8 +2291,22 @@ function formatTransportGuidance(transport, state, speaker) {
|
|
|
1485
2291
|
`⛔ **No API calls**: This speaker responds only via web browser. Do not call LLMs via REST API or HTTP requests.`;
|
|
1486
2292
|
case "browser_auto":
|
|
1487
2293
|
return `Auto browser speaker. Proceed automatically with \`deliberation_browser_auto_turn(session_id: "${sid}")\`. Inputs directly to browser LLM via CDP and reads responses.\n\n⛔ **No API calls**: Proceeds only via CDP automation. No REST API or HTTP requests.`;
|
|
2294
|
+
case "telepty_bus":
|
|
2295
|
+
return `Telepty session speaker. This turn will be published on the telepty bus as a structured \`turn_request\` envelope for the target session to consume.\n\n` +
|
|
2296
|
+
`📡 **Bus delivery**: deliberation publishes a typed envelope instead of relying on raw PTY inject.\n` +
|
|
2297
|
+
`⏱️ **Timeouts**: transport ack waits ${TELEPTY_TRANSPORT_TIMEOUT_MS / 1000}s, semantic self-submit waits ${TELEPTY_SEMANTIC_TIMEOUT_MS / 1000}s.\n` +
|
|
2298
|
+
`⛔ **No proxy response**: the remote telepty session must answer for itself via \`deliberation_respond(...)\`.`;
|
|
1488
2299
|
case "manual":
|
|
1489
2300
|
default:
|
|
2301
|
+
if (profile?.type === "telepty" && profile.telepty_session_id) {
|
|
2302
|
+
const hostSuffix = profile.telepty_host && !["127.0.0.1", "localhost"].includes(profile.telepty_host)
|
|
2303
|
+
? `@${profile.telepty_host}`
|
|
2304
|
+
: "";
|
|
2305
|
+
const pidNote = Number.isFinite(profile.runtime_pid) ? ` (pid ${profile.runtime_pid})` : "";
|
|
2306
|
+
return `Telepty-managed session speaker${pidNote}. Send the [turn_prompt] below to \`telepty inject ${profile.telepty_session_id}${hostSuffix} "<prompt>"\`, then have that remote session self-submit via \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`.\n\n` +
|
|
2307
|
+
`📋 **Recommended path**: inject the prompt into the telepty session and let the remote session answer for itself.\n` +
|
|
2308
|
+
`⛔ **No proxy response**: Do not answer on behalf of this speaker from the orchestrator.`;
|
|
2309
|
+
}
|
|
1490
2310
|
return `Manual speaker. Get a response from the LLM's **web UI or CLI tool** and submit via \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`.\n\n` +
|
|
1491
2311
|
`📋 **Copy the [turn_prompt] section below** to the web UI.\n` +
|
|
1492
2312
|
`🖼️ **To include an image:** Copy the image to your clipboard and use \`include_clipboard_image: true\` in \`deliberation_respond\`.\n\n` +
|
|
@@ -1627,38 +2447,45 @@ function readContextFromDirs(dirs, maxChars = 15000) {
|
|
|
1627
2447
|
|
|
1628
2448
|
// ── State helpers ──────────────────────────────────────────────
|
|
1629
2449
|
|
|
1630
|
-
function ensureDirs() {
|
|
1631
|
-
fs.mkdirSync(getSessionsDir(), { recursive: true });
|
|
1632
|
-
fs.mkdirSync(getArchiveDir(), { recursive: true });
|
|
1633
|
-
fs.mkdirSync(getLocksDir(), { recursive: true });
|
|
2450
|
+
function ensureDirs(projectSlug = getProjectSlug()) {
|
|
2451
|
+
fs.mkdirSync(getSessionsDir(projectSlug), { recursive: true });
|
|
2452
|
+
fs.mkdirSync(getArchiveDir(projectSlug), { recursive: true });
|
|
2453
|
+
fs.mkdirSync(getLocksDir(projectSlug), { recursive: true });
|
|
1634
2454
|
}
|
|
1635
2455
|
|
|
1636
|
-
function loadSession(
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1639
|
-
return normalizeSessionActors(JSON.parse(fs.readFileSync(file, "utf-8")));
|
|
2456
|
+
function loadSession(sessionRef) {
|
|
2457
|
+
const record = findSessionRecord(sessionRef);
|
|
2458
|
+
return record?.state || null;
|
|
1640
2459
|
}
|
|
1641
2460
|
|
|
1642
2461
|
function saveSession(state) {
|
|
1643
|
-
ensureDirs();
|
|
2462
|
+
ensureDirs(state.project);
|
|
1644
2463
|
state.updated = new Date().toISOString();
|
|
1645
|
-
writeTextAtomic(getSessionFile(state
|
|
2464
|
+
writeTextAtomic(getSessionFile(state), JSON.stringify(state, null, 2));
|
|
1646
2465
|
syncMarkdown(state);
|
|
1647
2466
|
}
|
|
1648
2467
|
|
|
1649
|
-
function listActiveSessions() {
|
|
1650
|
-
const
|
|
1651
|
-
|
|
2468
|
+
function listActiveSessions(projectSlug) {
|
|
2469
|
+
const projects = projectSlug
|
|
2470
|
+
? [normalizeProjectSlug(projectSlug)]
|
|
2471
|
+
: [...new Set([getProjectSlug(), ...listStateProjects()])];
|
|
1652
2472
|
|
|
1653
|
-
return
|
|
1654
|
-
|
|
1655
|
-
.
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
2473
|
+
return projects.flatMap(project => {
|
|
2474
|
+
const dir = getSessionsDir(project);
|
|
2475
|
+
if (!fs.existsSync(dir)) return [];
|
|
2476
|
+
|
|
2477
|
+
return fs.readdirSync(dir)
|
|
2478
|
+
.filter(f => f.endsWith(".json"))
|
|
2479
|
+
.map(f => {
|
|
2480
|
+
try {
|
|
2481
|
+
const data = JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
|
2482
|
+
return normalizeSessionActors(data);
|
|
2483
|
+
} catch {
|
|
2484
|
+
return null;
|
|
2485
|
+
}
|
|
2486
|
+
})
|
|
2487
|
+
.filter(s => s && (s.status === "active" || s.status === "awaiting_synthesis"));
|
|
2488
|
+
});
|
|
1662
2489
|
}
|
|
1663
2490
|
|
|
1664
2491
|
function resolveSessionId(sessionId) {
|
|
@@ -1676,8 +2503,7 @@ function resolveSessionId(sessionId) {
|
|
|
1676
2503
|
|
|
1677
2504
|
function syncMarkdown(state) {
|
|
1678
2505
|
const filename = `deliberation-${state.id}.md`;
|
|
1679
|
-
|
|
1680
|
-
const mdPath = path.join(getProjectStateDir(), filename);
|
|
2506
|
+
const mdPath = path.join(getProjectStateDir(state.project), filename);
|
|
1681
2507
|
try {
|
|
1682
2508
|
writeTextAtomic(mdPath, stateToMarkdown(state));
|
|
1683
2509
|
} catch { /* ignore sync failures */ }
|
|
@@ -1685,8 +2511,7 @@ function syncMarkdown(state) {
|
|
|
1685
2511
|
|
|
1686
2512
|
function cleanupSyncMarkdown(state) {
|
|
1687
2513
|
const filename = `deliberation-${state.id}.md`;
|
|
1688
|
-
|
|
1689
|
-
const statePath = path.join(getProjectStateDir(), filename);
|
|
2514
|
+
const statePath = path.join(getProjectStateDir(state.project), filename);
|
|
1690
2515
|
try { fs.unlinkSync(statePath); } catch { /* ignore */ }
|
|
1691
2516
|
// Also clean up legacy files in CWD (from older versions)
|
|
1692
2517
|
const cwdPath = path.join(process.cwd(), filename);
|
|
@@ -1745,14 +2570,14 @@ tags: [deliberation]
|
|
|
1745
2570
|
}
|
|
1746
2571
|
|
|
1747
2572
|
function archiveState(state) {
|
|
1748
|
-
ensureDirs();
|
|
2573
|
+
ensureDirs(state.project);
|
|
1749
2574
|
const slug = state.topic
|
|
1750
2575
|
.replace(/[^a-zA-Z0-9가-힣\s-]/g, "")
|
|
1751
2576
|
.replace(/\s+/g, "-")
|
|
1752
2577
|
.slice(0, 30);
|
|
1753
2578
|
const ts = new Date().toISOString().slice(0, 16).replace(/:/g, "");
|
|
1754
2579
|
const filename = `deliberation-${ts}-${slug}.md`;
|
|
1755
|
-
const dest = path.join(getArchiveDir(), filename);
|
|
2580
|
+
const dest = path.join(getArchiveDir(state.project), filename);
|
|
1756
2581
|
writeTextAtomic(dest, stateToMarkdown(state));
|
|
1757
2582
|
return dest;
|
|
1758
2583
|
}
|
|
@@ -2261,24 +3086,152 @@ function closeAllMonitorTerminals() {
|
|
|
2261
3086
|
|
|
2262
3087
|
function multipleSessionsError() {
|
|
2263
3088
|
const active = listActiveSessions();
|
|
2264
|
-
const list = active.map(s => `- **${s.id}
|
|
3089
|
+
const list = active.map(s => `- **${s.id}** [${s.project || "unknown"}]: "${s.topic}" (Round ${s.current_round}/${s.max_rounds}, next: ${s.current_speaker})`).join("\n");
|
|
2265
3090
|
return t(`Multiple active sessions found. Please specify session_id:\n\n${list}`, `여러 활성 세션이 있습니다. session_id를 지정하세요:\n\n${list}`, "en");
|
|
2266
3091
|
}
|
|
2267
3092
|
|
|
2268
|
-
function
|
|
3093
|
+
function truncatePromptText(text, maxChars) {
|
|
3094
|
+
const value = String(text || "").trim();
|
|
3095
|
+
if (!value || !Number.isFinite(maxChars) || maxChars <= 0 || value.length <= maxChars) {
|
|
3096
|
+
return value;
|
|
3097
|
+
}
|
|
3098
|
+
const remaining = value.length - maxChars;
|
|
3099
|
+
return `${value.slice(0, maxChars).trimEnd()}\n...(truncated ${remaining} chars)`;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
function getPromptBudgetForSpeaker(speaker, includeHistoryEntries = 4) {
|
|
3103
|
+
const defaultBudget = {
|
|
3104
|
+
maxEntries: Math.max(0, includeHistoryEntries),
|
|
3105
|
+
maxCharsPerEntry: 1600,
|
|
3106
|
+
maxTotalChars: 6400,
|
|
3107
|
+
maxTopicChars: 3200,
|
|
3108
|
+
};
|
|
3109
|
+
switch (speaker) {
|
|
3110
|
+
case "codex":
|
|
3111
|
+
return {
|
|
3112
|
+
maxEntries: Math.min(Math.max(0, includeHistoryEntries), 3),
|
|
3113
|
+
maxCharsPerEntry: 1200,
|
|
3114
|
+
maxTotalChars: 3600,
|
|
3115
|
+
maxTopicChars: 2200,
|
|
3116
|
+
};
|
|
3117
|
+
case "gemini":
|
|
3118
|
+
return {
|
|
3119
|
+
maxEntries: Math.min(Math.max(0, includeHistoryEntries), 4),
|
|
3120
|
+
maxCharsPerEntry: 1400,
|
|
3121
|
+
maxTotalChars: 5600,
|
|
3122
|
+
maxTopicChars: 2800,
|
|
3123
|
+
};
|
|
3124
|
+
default:
|
|
3125
|
+
return defaultBudget;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
function formatRecentLogForPrompt(state, maxEntries = 4, options = {}) {
|
|
2269
3130
|
const entries = Array.isArray(state.log) ? state.log.slice(-Math.max(0, maxEntries)) : [];
|
|
2270
3131
|
if (entries.length === 0) {
|
|
2271
3132
|
return "(No previous responses yet)";
|
|
2272
3133
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
3134
|
+
const maxCharsPerEntry = options.maxCharsPerEntry || 1600;
|
|
3135
|
+
const maxTotalChars = options.maxTotalChars || maxCharsPerEntry * entries.length;
|
|
3136
|
+
const rendered = [];
|
|
3137
|
+
let usedChars = 0;
|
|
3138
|
+
|
|
3139
|
+
for (const entry of entries) {
|
|
3140
|
+
const header = `- ${entry.speaker} (Round ${entry.round})`;
|
|
3141
|
+
const remainingChars = Math.max(0, maxTotalChars - usedChars - header.length - 1);
|
|
3142
|
+
const entryBudget = Math.max(200, Math.min(maxCharsPerEntry, remainingChars || maxCharsPerEntry));
|
|
3143
|
+
const content = truncatePromptText(entry.content, entryBudget);
|
|
3144
|
+
const block = `${header}\n${content}`;
|
|
3145
|
+
rendered.push(block);
|
|
3146
|
+
usedChars += block.length + 2;
|
|
3147
|
+
if (usedChars >= maxTotalChars) {
|
|
3148
|
+
break;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
return rendered.join("\n\n");
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
function getCliAutoTurnTimeoutSec({ speaker, requestedTimeoutSec, promptLength, priorTurns }) {
|
|
3156
|
+
const requested = Number.isFinite(requestedTimeoutSec) ? requestedTimeoutSec : 120;
|
|
3157
|
+
if (speaker === "codex") {
|
|
3158
|
+
let recommended = Math.max(requested, priorTurns === 0 ? 240 : 180);
|
|
3159
|
+
if (promptLength > 6000) {
|
|
3160
|
+
recommended = Math.max(recommended, 300);
|
|
3161
|
+
}
|
|
3162
|
+
if (promptLength > 10000 || priorTurns >= 1) {
|
|
3163
|
+
recommended = Math.max(recommended, 420);
|
|
3164
|
+
}
|
|
3165
|
+
return recommended;
|
|
3166
|
+
}
|
|
3167
|
+
return priorTurns === 0 ? Math.max(requested, 180) : requested;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
function getCliExecArgs(speaker) {
|
|
3171
|
+
switch (speaker) {
|
|
3172
|
+
case "claude":
|
|
3173
|
+
return ["-p", "--output-format", "text"];
|
|
3174
|
+
case "codex":
|
|
3175
|
+
return [
|
|
3176
|
+
"exec",
|
|
3177
|
+
"--ephemeral",
|
|
3178
|
+
"-c", 'approval_policy="never"',
|
|
3179
|
+
"-c", 'sandbox_mode="read-only"',
|
|
3180
|
+
"-c", 'model_reasoning_effort="low"',
|
|
3181
|
+
"-",
|
|
3182
|
+
];
|
|
3183
|
+
case "gemini":
|
|
3184
|
+
return null;
|
|
3185
|
+
default:
|
|
3186
|
+
return null;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
function buildCliAutoTurnFailureText({ state, speaker, hint, err, effectiveTimeout, promptLength, priorTurns }) {
|
|
3191
|
+
const isTimeout = /CLI timeout \(/.test(String(err?.message || ""));
|
|
3192
|
+
if (!isTimeout) {
|
|
3193
|
+
return `❌ CLI auto-turn failed: ${err.message}\n\n**Speaker:** ${speaker}\n**CLI:** ${hint.cmd}\n\nYou can submit a manual response via deliberation_respond(speaker: "${speaker}", content: "...").`;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
const retryTimeout = speaker === "codex"
|
|
3197
|
+
? Math.min(Math.max(effectiveTimeout, 420), 600)
|
|
3198
|
+
: Math.min(effectiveTimeout + 60, 300);
|
|
3199
|
+
|
|
3200
|
+
return t(
|
|
3201
|
+
`⏱️ CLI auto-turn timed out.\n\n` +
|
|
3202
|
+
`**Speaker:** ${speaker}\n` +
|
|
3203
|
+
`**CLI:** ${hint.cmd}\n` +
|
|
3204
|
+
`**Timeout:** ${effectiveTimeout}s\n` +
|
|
3205
|
+
`**Prompt size:** ${promptLength} chars\n` +
|
|
3206
|
+
`**Prior turns by speaker:** ${priorTurns}\n` +
|
|
3207
|
+
`**Session state:** still waiting on ${speaker} for Round ${state.current_round}\n\n` +
|
|
3208
|
+
`This usually means the CLI stayed busy longer than the timeout. It does **not** necessarily mean the model is down.\n` +
|
|
3209
|
+
`${speaker === "codex" ? `Codex is the slowest CLI in recent deliberation logs, especially when recent_log contains long prior responses.\n` : ""}` +
|
|
3210
|
+
`Recommended next step: retry with \`deliberation_cli_auto_turn(session_id: "${state.id}", timeout_sec: ${retryTimeout})\`.\n` +
|
|
3211
|
+
`Manual fallback: \`deliberation_respond(session_id: "${state.id}", speaker: "${speaker}", content: "...")\`.`,
|
|
3212
|
+
`⏱️ CLI 자동 턴이 타임아웃되었습니다.\n\n` +
|
|
3213
|
+
`**Speaker:** ${speaker}\n` +
|
|
3214
|
+
`**CLI:** ${hint.cmd}\n` +
|
|
3215
|
+
`**Timeout:** ${effectiveTimeout}s\n` +
|
|
3216
|
+
`**Prompt 크기:** ${promptLength} chars\n` +
|
|
3217
|
+
`**이 speaker의 이전 발언 수:** ${priorTurns}\n` +
|
|
3218
|
+
`**세션 상태:** Round ${state.current_round}에서 아직 ${speaker} 응답을 기다리는 중\n\n` +
|
|
3219
|
+
`이건 보통 CLI가 제한 시간 안에 응답을 끝내지 못했다는 뜻입니다. 모델이 완전히 죽었다는 의미는 아닙니다.\n` +
|
|
3220
|
+
`${speaker === "codex" ? `최근 딜리버레이션 로그 기준으로 Codex는 이전 응답 전문이 길게 들어가면 가장 느린 편입니다.\n` : ""}` +
|
|
3221
|
+
`권장 조치: \`deliberation_cli_auto_turn(session_id: "${state.id}", timeout_sec: ${retryTimeout})\` 로 재시도하세요.\n` +
|
|
3222
|
+
`수동 대안: \`deliberation_respond(session_id: "${state.id}", speaker: "${speaker}", content: "...")\`.`,
|
|
3223
|
+
state?.lang
|
|
3224
|
+
);
|
|
2277
3225
|
}
|
|
2278
3226
|
|
|
2279
3227
|
function buildClipboardTurnPrompt(state, speaker, prompt, includeHistoryEntries = 4) {
|
|
2280
|
-
const
|
|
3228
|
+
const promptBudget = getPromptBudgetForSpeaker(speaker, includeHistoryEntries);
|
|
3229
|
+
const recent = formatRecentLogForPrompt(state, promptBudget.maxEntries, promptBudget);
|
|
2281
3230
|
const extraPrompt = prompt ? `\n[Additional instructions]\n${prompt}\n` : "";
|
|
3231
|
+
const topic = truncatePromptText(state.topic, promptBudget.maxTopicChars);
|
|
3232
|
+
const noToolRule = speaker === "codex"
|
|
3233
|
+
? `\n- Do not inspect files, run shell commands, browse, or call tools. Answer only from the provided discussion context.`
|
|
3234
|
+
: "";
|
|
2282
3235
|
|
|
2283
3236
|
// Role prompt injection
|
|
2284
3237
|
const speakerRole = (state.speaker_roles || {})[speaker] || "free";
|
|
@@ -2290,7 +3243,7 @@ function buildClipboardTurnPrompt(state, speaker, prompt, includeHistoryEntries
|
|
|
2290
3243
|
return `[deliberation_turn_request]
|
|
2291
3244
|
session_id: ${state.id}
|
|
2292
3245
|
project: ${state.project}
|
|
2293
|
-
topic: ${
|
|
3246
|
+
topic: ${topic}
|
|
2294
3247
|
round: ${state.current_round}/${state.max_rounds}
|
|
2295
3248
|
target_speaker: ${speaker}
|
|
2296
3249
|
required_turn: ${state.current_speaker}${roleSection}
|
|
@@ -2302,6 +3255,7 @@ ${recent}
|
|
|
2302
3255
|
[response_rule]
|
|
2303
3256
|
- Write only ${speaker}'s response for this turn reflecting the discussion context above
|
|
2304
3257
|
- Output markdown body only (no unnecessary headers/footers)${speakerRole !== "free" ? `\n- Analyze and respond from the perspective of assigned role (${speakerRole})` : ""}
|
|
3258
|
+
- Keep the response concise and decision-oriented${noToolRule}
|
|
2305
3259
|
- Must include one of [AGREE], [DISAGREE], or [CONDITIONAL: reason] at the end of response
|
|
2306
3260
|
[/response_rule]
|
|
2307
3261
|
[/deliberation_turn_request]
|
|
@@ -2375,6 +3329,11 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
|
|
|
2375
3329
|
role_drift: roleDrift || undefined,
|
|
2376
3330
|
attachments: attachments || undefined,
|
|
2377
3331
|
});
|
|
3332
|
+
completePendingTeleptySemantic({
|
|
3333
|
+
sessionId: state.id,
|
|
3334
|
+
speaker: normalizedSpeaker,
|
|
3335
|
+
turnId: state.pending_turn_id || turn_id || null,
|
|
3336
|
+
});
|
|
2378
3337
|
appendRuntimeLog("INFO", `TURN: ${state.id} | R${state.current_round} | speaker: ${normalizedSpeaker} | votes: ${votes.length > 0 ? votes.map(v => v.vote).join(",") : "none"} | channel: ${channel_used || "respond"} | attachments: ${attachments ? attachments.length : 0}`);
|
|
2379
3338
|
|
|
2380
3339
|
state.current_speaker = selectNextSpeaker(state);
|
|
@@ -2452,6 +3411,7 @@ server.tool(
|
|
|
2452
3411
|
session_id: z.string().trim().min(1).max(64).optional().describe("Explicit session ID to use. If omitted, one is generated from topic."),
|
|
2453
3412
|
rounds: z.coerce.number().optional().describe("Number of rounds (defaults to config setting, default 3)"),
|
|
2454
3413
|
first_speaker: z.string().trim().min(1).max(64).optional().describe("First speaker name (defaults to first item in speakers)"),
|
|
3414
|
+
selection_token: z.string().trim().min(1).max(128).optional().describe("Single-use token returned by deliberation_speaker_candidates. Required for fresh manual speaker selection."),
|
|
2455
3415
|
speakers: z.preprocess(
|
|
2456
3416
|
(v) => {
|
|
2457
3417
|
const parsed = typeof v === "string" ? JSON.parse(v) : v;
|
|
@@ -2468,14 +3428,18 @@ server.tool(
|
|
|
2468
3428
|
require_manual_speakers: z.preprocess(
|
|
2469
3429
|
(v) => (typeof v === "string" ? v === "true" : v),
|
|
2470
3430
|
z.boolean().optional()
|
|
2471
|
-
).describe("
|
|
3431
|
+
).describe("Deprecated toggle. Speakers are now always selected manually before start."),
|
|
2472
3432
|
auto_discover_speakers: z.preprocess(
|
|
2473
3433
|
(v) => (typeof v === "string" ? v === "true" : v),
|
|
2474
3434
|
z.boolean().optional()
|
|
2475
|
-
).describe("
|
|
3435
|
+
).describe("Deprecated toggle. Auto-discovery no longer auto-joins participants; use deliberation_speaker_candidates instead."),
|
|
3436
|
+
include_browser_speakers: z.preprocess(
|
|
3437
|
+
(v) => (typeof v === "string" ? v === "true" : v),
|
|
3438
|
+
z.boolean().optional()
|
|
3439
|
+
).describe("Whether browser speakers are allowed to participate. Defaults to false unless explicitly enabled."),
|
|
2476
3440
|
participant_types: z.preprocess(
|
|
2477
3441
|
(v) => (typeof v === "string" ? JSON.parse(v) : v),
|
|
2478
|
-
z.record(z.string(), z.enum(["cli", "browser", "browser_auto", "manual"])).optional()
|
|
3442
|
+
z.record(z.string(), z.enum(["cli", "telepty", "browser", "browser_auto", "manual"])).optional()
|
|
2479
3443
|
).describe("Per-speaker type override (e.g., {\"chatgpt\": \"browser_auto\"})"),
|
|
2480
3444
|
ordering_strategy: z.enum(["auto", "cyclic", "random", "weighted-random"]).optional()
|
|
2481
3445
|
.describe("Ordering strategy: auto (automatic based on speaker count), cyclic (sequential), random (random each turn), weighted-random (less spoken speakers first)"),
|
|
@@ -2485,8 +3449,12 @@ server.tool(
|
|
|
2485
3449
|
).describe("Per-speaker role assignment (e.g., {\"claude\": \"critic\", \"codex\": \"implementer\"})"),
|
|
2486
3450
|
role_preset: z.enum(["balanced", "debate", "research", "brainstorm", "review", "consensus"]).optional()
|
|
2487
3451
|
.describe("Role preset (balanced/debate/research/brainstorm/review/consensus). Ignored if speaker_roles is specified"),
|
|
3452
|
+
auto_execute: z.preprocess(
|
|
3453
|
+
(v) => (typeof v === "string" ? v === "true" : v),
|
|
3454
|
+
z.boolean().optional()
|
|
3455
|
+
).describe("If true, automatically create a handoff task in the inbox when synthesis completes. Enables the Autonomous Deliberation Handoff pattern."),
|
|
2488
3456
|
},
|
|
2489
|
-
safeToolHandler("deliberation_start", async ({ topic, session_id, rounds, first_speaker, speakers, speaker_instructions, require_manual_speakers, auto_discover_speakers, participant_types, ordering_strategy, speaker_roles, role_preset }) => {
|
|
3457
|
+
safeToolHandler("deliberation_start", async ({ topic, session_id, rounds, first_speaker, selection_token, speakers, speaker_instructions, require_manual_speakers, auto_discover_speakers, include_browser_speakers, participant_types, ordering_strategy, speaker_roles, role_preset, auto_execute }) => {
|
|
2490
3458
|
// ── First-time onboarding guard ──
|
|
2491
3459
|
const config = loadDeliberationConfig();
|
|
2492
3460
|
if (!config.setup_complete) {
|
|
@@ -2495,7 +3463,7 @@ server.tool(
|
|
|
2495
3463
|
return {
|
|
2496
3464
|
content: [{
|
|
2497
3465
|
type: "text",
|
|
2498
|
-
text: `🎉 **Welcome to Deliberation!**\n\nPlease configure basic settings before starting.\n\n**Currently detected speakers:**\n${candidateText}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nYou can set
|
|
3466
|
+
text: `🎉 **Welcome to Deliberation!**\n\nPlease configure basic settings before starting.\n\n**Currently detected speakers:**\n${candidateText}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nYou can set the remaining defaults with:\n\n\`\`\`\ndeliberation_cli_config(\n include_browser_speakers: false,\n default_rounds: 3,\n default_ordering: "auto"\n)\n\`\`\`\n\n**1. Speaker participation mode**\n - Always manual — participants are selected fresh at every start from the current candidate snapshot\n\n**2. Browser speakers** (\`include_browser_speakers\`)\n - \`false\` — CLI + telepty sessions only (recommended)\n - \`true\` — Include browser LLM speakers too\n\n**3. Default rounds** (\`default_rounds\`)\n - \`1\` — Quick consensus\n - \`3\` — Default (recommended)\n - \`5\` — Deep discussion\n\n**4. Ordering strategy** (\`default_ordering\`)\n - \`"auto"\` — cyclic for 2 speakers, weighted-random for 3+ (recommended)\n - \`"cyclic"\` — Fixed order\n - \`"random"\` — Random each turn\n - \`"weighted-random"\` — Less spoken speakers first`,
|
|
2499
3467
|
}],
|
|
2500
3468
|
};
|
|
2501
3469
|
}
|
|
@@ -2507,36 +3475,75 @@ server.tool(
|
|
|
2507
3475
|
return { content: [{ type: "text", text: `❌ Session "${session_id}" is already active. Please use a different ID or reset it first.` }] };
|
|
2508
3476
|
}
|
|
2509
3477
|
}
|
|
2510
|
-
const
|
|
3478
|
+
const explicitBrowserSelection = hasExplicitBrowserParticipantSelection({ speakers, participant_types });
|
|
3479
|
+
const includeBrowserSpeakers = resolveIncludeBrowserSpeakers({
|
|
3480
|
+
include_browser_speakers,
|
|
3481
|
+
config,
|
|
3482
|
+
speakers,
|
|
3483
|
+
participant_types,
|
|
3484
|
+
});
|
|
3485
|
+
if (explicitBrowserSelection && !includeBrowserSpeakers) {
|
|
3486
|
+
return {
|
|
3487
|
+
content: [{
|
|
3488
|
+
type: "text",
|
|
3489
|
+
text: `❌ Browser speakers are currently disabled.\n\nThis deliberation server now defaults to CLI-only participation to avoid browser timeouts blocking the session.\n\nTo include browser speakers, opt in explicitly:\n\`\`\`\ndeliberation_start(\n topic: "${topic.replace(/"/g, '\\"')}",\n speakers: ${JSON.stringify(speakers || ["claude", "codex"])},\n include_browser_speakers: true,\n require_manual_speakers: true\n)\n\`\`\`\n\nOr save it in config:\n\`deliberation_cli_config(include_browser_speakers: true)\``,
|
|
3490
|
+
}],
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
const candidateSnapshot = await collectSpeakerCandidates({
|
|
3495
|
+
include_cli: true,
|
|
3496
|
+
include_browser: includeBrowserSpeakers,
|
|
3497
|
+
});
|
|
2511
3498
|
|
|
2512
3499
|
// Resolve effective settings from config
|
|
2513
|
-
const effectiveRequireManual =
|
|
2514
|
-
const effectiveAutoDiscover =
|
|
3500
|
+
const effectiveRequireManual = true;
|
|
3501
|
+
const effectiveAutoDiscover = false;
|
|
2515
3502
|
rounds = rounds ?? config.default_rounds ?? 3;
|
|
2516
3503
|
const rawOrdering = ordering_strategy ?? config.default_ordering ?? "auto";
|
|
2517
3504
|
// Resolve "auto": 2 speakers → cyclic, 3+ → weighted-random
|
|
2518
3505
|
ordering_strategy = rawOrdering === "auto" ? undefined : rawOrdering; // resolved after speakers are known
|
|
2519
3506
|
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
3507
|
+
const manualSpeakersProvided = Array.isArray(speakers) && speakers.length > 0;
|
|
3508
|
+
let selectionValidation = { ok: true };
|
|
3509
|
+
if (effectiveRequireManual && manualSpeakersProvided) {
|
|
3510
|
+
selectionValidation = validateSpeakerSelectionRequest({
|
|
3511
|
+
selectionState: loadSpeakerSelectionToken(),
|
|
3512
|
+
selection_token,
|
|
3513
|
+
speakers,
|
|
3514
|
+
includeBrowserSpeakers,
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
const hasManualSpeakers = manualSpeakersProvided && (!effectiveRequireManual || selectionValidation.ok);
|
|
3518
|
+
|
|
3519
|
+
if (manualSpeakersProvided && effectiveRequireManual && !selectionValidation.ok) {
|
|
3520
|
+
const candidateText = formatSpeakerCandidatesReport(candidateSnapshot);
|
|
3521
|
+
const mismatchNote = selectionValidation.code === "speaker_mismatch"
|
|
3522
|
+
? `\n\nRequested speakers not in the latest candidate snapshot: ${(selectionValidation.missing_speakers || []).join(", ")}`
|
|
3523
|
+
: selectionValidation.code === "selected_speakers_mismatch"
|
|
3524
|
+
? `\n\nThis token is bound to a different speaker set.\nExpected: ${(selectionValidation.expected_speakers || []).join(", ")}\nRequested: ${(selectionValidation.requested_speakers || []).join(", ")}`
|
|
3525
|
+
: "";
|
|
3526
|
+
const confirmationNote = selectionValidation.code === "selection_not_confirmed"
|
|
3527
|
+
? "\n\nThe token you passed is only a candidate snapshot token. You must confirm the exact user-picked speakers before start."
|
|
3528
|
+
: "";
|
|
3529
|
+
return {
|
|
3530
|
+
content: [{
|
|
3531
|
+
type: "text",
|
|
3532
|
+
text: `Fresh participant selection is required before each deliberation start.${confirmationNote}${mismatchNote}\n\n1. Call \`deliberation_speaker_candidates(include_cli: true, include_browser: ${includeBrowserSpeakers ? "true" : "false"})\`\n2. Show the speaker list in the TUI and let the user choose participants\n3. Call \`deliberation_confirm_speakers(selection_token: "<candidate-token>", speakers: [...])\`\n4. Pass the returned confirmed \`selection_token\` into \`deliberation_start(..., selection_token: "...", speakers: [...])\`\n\n${candidateText}`,
|
|
3533
|
+
}],
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
2527
3536
|
|
|
2528
3537
|
if (!hasManualSpeakers && effectiveRequireManual) {
|
|
2529
3538
|
const candidateText = formatSpeakerCandidatesReport(candidateSnapshot);
|
|
2530
3539
|
const llmSuggested = Array.isArray(speakers) && speakers.length > 0
|
|
2531
|
-
? `\n\n💡 **LLM suggested speakers:** ${speakers.join(", ")}\
|
|
2532
|
-
: "";
|
|
2533
|
-
const configNote = configRequiresSelection
|
|
2534
|
-
? "\n\n⚙️ `require_speaker_selection: true` setting requires you to manually select speakers."
|
|
3540
|
+
? `\n\n💡 **LLM suggested speakers:** ${speakers.join(", ")}\nShow the candidate list in the TUI, let the user confirm, then call \`deliberation_confirm_speakers\` with the final speaker list.`
|
|
2535
3541
|
: "";
|
|
3542
|
+
const configNote = "\n\n⚙️ Manual speaker selection is enabled and requires a fresh confirmed `selection_token`.";
|
|
2536
3543
|
return {
|
|
2537
3544
|
content: [{
|
|
2538
3545
|
type: "text",
|
|
2539
|
-
text: `Speakers must be manually selected to start a deliberation.${configNote}${llmSuggested}\n\n${candidateText}\n\nExample:\n\
|
|
3546
|
+
text: `Speakers must be manually selected to start a deliberation.${configNote}${llmSuggested}\n\n${candidateText}\n\nExample:\n\n1. \`deliberation_speaker_candidates(...)\`\n2. User picks speakers in the TUI\n3. \`deliberation_confirm_speakers(selection_token: "<candidate-token>", speakers: ["claude", "codex", "gemini"])\`\n4. \`deliberation_start(\n topic: "${topic.replace(/"/g, '\\"')}",\n selection_token: "<confirmed-token>",\n rounds: ${rounds},\n speakers: ["claude", "codex", "gemini"],\n require_manual_speakers: true,\n first_speaker: "codex"\n)\`\n\nFirst call deliberation_speaker_candidates to check currently available speakers.`,
|
|
2540
3547
|
}],
|
|
2541
3548
|
};
|
|
2542
3549
|
}
|
|
@@ -2544,7 +3551,6 @@ server.tool(
|
|
|
2544
3551
|
let autoDiscoveredSpeakers = [];
|
|
2545
3552
|
let autoParticipantTypes = {};
|
|
2546
3553
|
if (!hasManualSpeakers && effectiveAutoDiscover) {
|
|
2547
|
-
// Include ALL candidates: CLI + browser
|
|
2548
3554
|
for (const c of candidateSnapshot.candidates) {
|
|
2549
3555
|
autoDiscoveredSpeakers.push(c.speaker);
|
|
2550
3556
|
if (c.type === "browser" && c.cdp_available) {
|
|
@@ -2573,14 +3579,17 @@ server.tool(
|
|
|
2573
3579
|
|| DEFAULT_SPEAKERS[0];
|
|
2574
3580
|
const speakerOrder = buildSpeakerOrder(selectedSpeakers, normalizedFirstSpeaker, "front");
|
|
2575
3581
|
|
|
3582
|
+
if (effectiveRequireManual) {
|
|
3583
|
+
clearSpeakerSelectionToken();
|
|
3584
|
+
}
|
|
3585
|
+
|
|
2576
3586
|
// Warn if only 1 speaker — deliberation requires 2+
|
|
2577
3587
|
if (speakerOrder.length < 2) {
|
|
2578
|
-
const candidateSnapshot = await collectSpeakerCandidates({ include_cli: true, include_browser: true });
|
|
2579
3588
|
const candidateText = formatSpeakerCandidatesReport(candidateSnapshot);
|
|
2580
3589
|
return {
|
|
2581
3590
|
content: [{
|
|
2582
3591
|
type: "text",
|
|
2583
|
-
text: `⚠️ Deliberation requires at least 2 speakers. Currently only ${speakerOrder.length} specified: ${speakerOrder.join(", ")}\n\nAvailable speaker candidates:\n${candidateText}\n\nExample:\ndeliberation_start(topic: "${topic.slice(0, 50)}...", speakers: ["claude", "codex", "
|
|
3592
|
+
text: `⚠️ Deliberation requires at least 2 speakers. Currently only ${speakerOrder.length} specified: ${speakerOrder.join(", ")}\n\nAvailable speaker candidates:\n${candidateText}\n\nExample:\ndeliberation_start(topic: "${topic.slice(0, 50)}...", speakers: ["claude", "codex", "gemini"])`,
|
|
2584
3593
|
}],
|
|
2585
3594
|
};
|
|
2586
3595
|
}
|
|
@@ -2601,7 +3610,7 @@ server.tool(
|
|
|
2601
3610
|
}
|
|
2602
3611
|
|
|
2603
3612
|
const participantMode = hasManualSpeakers
|
|
2604
|
-
? "
|
|
3613
|
+
? "user-selected"
|
|
2605
3614
|
: (autoDiscoveredSpeakers.length > 0 ? "auto-discovered (PATH)" : "default");
|
|
2606
3615
|
|
|
2607
3616
|
const degradationLevels = await detectDegradationLevels();
|
|
@@ -2624,6 +3633,7 @@ server.tool(
|
|
|
2624
3633
|
ordering_strategy: ordering_strategy || (speakerOrder.length <= 2 ? "cyclic" : "weighted-random"),
|
|
2625
3634
|
speaker_roles: speaker_roles || (role_preset ? applyRolePreset(role_preset, speakerOrder) : {}),
|
|
2626
3635
|
degradation: degradationLevels,
|
|
3636
|
+
auto_execute: auto_execute || false,
|
|
2627
3637
|
created: new Date().toISOString(),
|
|
2628
3638
|
updated: new Date().toISOString(),
|
|
2629
3639
|
};
|
|
@@ -2691,6 +3701,15 @@ server.tool(
|
|
|
2691
3701
|
}).join("\n");
|
|
2692
3702
|
|
|
2693
3703
|
appendRuntimeLog("INFO", `SESSION_CREATED: ${sessionId} | topic: ${topic.slice(0, 60)} | speakers: ${speakerOrder.join(",")} | rounds: ${rounds}`);
|
|
3704
|
+
|
|
3705
|
+
// Auto-handoff: kick off background orchestration
|
|
3706
|
+
if (auto_execute) {
|
|
3707
|
+
// Fire-and-forget — runs in background
|
|
3708
|
+
runAutoHandoff(sessionId).catch(err => {
|
|
3709
|
+
appendRuntimeLog("ERROR", `AUTO_HANDOFF_SPAWN_ERROR: ${sessionId} | ${err.message}`);
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
|
|
2694
3713
|
return {
|
|
2695
3714
|
content: [{
|
|
2696
3715
|
type: "text",
|
|
@@ -2702,15 +3721,66 @@ server.tool(
|
|
|
2702
3721
|
|
|
2703
3722
|
server.tool(
|
|
2704
3723
|
"deliberation_speaker_candidates",
|
|
2705
|
-
"Query available speaker candidates (local CLI + browser LLM tabs).",
|
|
3724
|
+
"Query available speaker candidates (local CLI + telepty active sessions + browser LLM tabs).",
|
|
2706
3725
|
{
|
|
2707
3726
|
include_cli: z.boolean().default(true).describe("Include local CLI candidates"),
|
|
2708
3727
|
include_browser: z.boolean().default(true).describe("Include browser LLM tab candidates"),
|
|
2709
3728
|
},
|
|
2710
3729
|
async ({ include_cli, include_browser }) => {
|
|
2711
3730
|
const snapshot = await collectSpeakerCandidates({ include_cli, include_browser });
|
|
3731
|
+
const selection = issueSpeakerSelectionToken({
|
|
3732
|
+
candidates: snapshot.candidates,
|
|
3733
|
+
include_browser,
|
|
3734
|
+
});
|
|
2712
3735
|
const text = formatSpeakerCandidatesReport(snapshot);
|
|
2713
|
-
return {
|
|
3736
|
+
return {
|
|
3737
|
+
content: [{
|
|
3738
|
+
type: "text",
|
|
3739
|
+
text: `${text}\n\n**Candidate token:** \`${selection.token}\`\nAfter the user picks participants in the TUI, call \`deliberation_confirm_speakers(selection_token: "${selection.token}", speakers: [...])\` to mint a confirmed start token. Raw candidate tokens cannot start a deliberation.\n\n${PRODUCT_DISCLAIMER}`,
|
|
3740
|
+
}],
|
|
3741
|
+
};
|
|
3742
|
+
}
|
|
3743
|
+
);
|
|
3744
|
+
|
|
3745
|
+
server.tool(
|
|
3746
|
+
"deliberation_confirm_speakers",
|
|
3747
|
+
"Bind a fresh candidate token to the exact CLI/telepty/browser speakers the user chose in the TUI.",
|
|
3748
|
+
{
|
|
3749
|
+
selection_token: z.string().trim().min(1).max(128).describe("Candidate token returned by deliberation_speaker_candidates."),
|
|
3750
|
+
speakers: z.array(z.string().trim().min(1)).min(1).describe("Exact speakers the user selected in the TUI."),
|
|
3751
|
+
},
|
|
3752
|
+
async ({ selection_token, speakers }) => {
|
|
3753
|
+
const selectionState = loadSpeakerSelectionToken();
|
|
3754
|
+
const includeBrowserSpeakers = !!selectionState?.include_browser;
|
|
3755
|
+
const confirmation = confirmSpeakerSelectionToken({
|
|
3756
|
+
selectionState,
|
|
3757
|
+
selection_token,
|
|
3758
|
+
speakers,
|
|
3759
|
+
includeBrowserSpeakers,
|
|
3760
|
+
});
|
|
3761
|
+
|
|
3762
|
+
if (!confirmation.ok) {
|
|
3763
|
+
const candidateText = formatSpeakerCandidatesReport(await collectSpeakerCandidates({
|
|
3764
|
+
include_cli: true,
|
|
3765
|
+
include_browser: includeBrowserSpeakers,
|
|
3766
|
+
}));
|
|
3767
|
+
const mismatchNote = confirmation.code === "speaker_mismatch"
|
|
3768
|
+
? `\n\nRequested speakers not in the latest candidate snapshot: ${(confirmation.missing_speakers || []).join(", ")}`
|
|
3769
|
+
: "";
|
|
3770
|
+
return {
|
|
3771
|
+
content: [{
|
|
3772
|
+
type: "text",
|
|
3773
|
+
text: `Speaker confirmation failed.${mismatchNote}\n\n1. Call \`deliberation_speaker_candidates\` for a fresh snapshot\n2. Let the user choose speakers in the TUI\n3. Call \`deliberation_confirm_speakers\` with that exact selection\n\n${candidateText}`,
|
|
3774
|
+
}],
|
|
3775
|
+
};
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
return {
|
|
3779
|
+
content: [{
|
|
3780
|
+
type: "text",
|
|
3781
|
+
text: `✅ Speaker selection confirmed.\n\n**Selected speakers:** ${confirmation.selectionState.selected_speakers.join(", ")}\n**Confirmed selection token:** \`${confirmation.selectionState.token}\`\n\nUse this exact token with the same speaker list in \`deliberation_start(..., selection_token: "...", speakers: [...])\`.\nIf the user changes the selection, call \`deliberation_speaker_candidates\` again for a fresh snapshot.`,
|
|
3782
|
+
}],
|
|
3783
|
+
};
|
|
2714
3784
|
}
|
|
2715
3785
|
);
|
|
2716
3786
|
|
|
@@ -2849,6 +3919,55 @@ server.tool(
|
|
|
2849
3919
|
|
|
2850
3920
|
let extra = "";
|
|
2851
3921
|
let turnPrompt = "";
|
|
3922
|
+
let manualFallbackPrompt = false;
|
|
3923
|
+
|
|
3924
|
+
if (transport === "telepty_bus") {
|
|
3925
|
+
turnPrompt = buildClipboardTurnPrompt(state, speaker, prompt, include_history_entries);
|
|
3926
|
+
const busReady = await ensureTeleptyBusSubscriber();
|
|
3927
|
+
const envelope = buildTeleptyTurnRequestEnvelope({
|
|
3928
|
+
state,
|
|
3929
|
+
speaker,
|
|
3930
|
+
turnId: turnId || generateTurnId(),
|
|
3931
|
+
turnPrompt,
|
|
3932
|
+
includeHistoryEntries: include_history_entries,
|
|
3933
|
+
profile,
|
|
3934
|
+
});
|
|
3935
|
+
const pending = registerPendingTeleptyTurnRequest({ envelope, profile, speaker });
|
|
3936
|
+
const publishResult = await notifyTeleptyBus(envelope);
|
|
3937
|
+
const health = profile?.telepty_session_id ? getTeleptySessionHealth(profile.telepty_session_id) : null;
|
|
3938
|
+
|
|
3939
|
+
if (!publishResult.ok) {
|
|
3940
|
+
cleanupPendingTeleptyTurn(envelope.message_id);
|
|
3941
|
+
manualFallbackPrompt = true;
|
|
3942
|
+
extra += `\n\n❌ Telepty bus publish failed: ${publishResult.error || publishResult.status || "unknown error"}\n` +
|
|
3943
|
+
`Fallback: use manual telepty inject for this turn.`;
|
|
3944
|
+
guidance = formatTransportGuidance("manual", state, speaker);
|
|
3945
|
+
} else {
|
|
3946
|
+
const transportResult = await pending.transportPromise;
|
|
3947
|
+
const healthLine = health
|
|
3948
|
+
? `\n**Session health:** alive=${health.payload?.alive === true ? "yes" : "no"}, pid=${health.payload?.pid || "n/a"}, age=${Math.max(0, Math.round((health.age_ms || 0) / 1000))}s${health.stale ? " (stale)" : ""}`
|
|
3949
|
+
: "";
|
|
3950
|
+
const transportLine = transportResult.ok
|
|
3951
|
+
? `✅ Transport ack received via \`inject_written\` within ${TELEPTY_TRANSPORT_TIMEOUT_MS / 1000}s.`
|
|
3952
|
+
: `⚠️ Transport ack not observed within ${TELEPTY_TRANSPORT_TIMEOUT_MS / 1000}s. The request was published, but delivery is still best-effort.`;
|
|
3953
|
+
const subscriberLine = busReady.ok
|
|
3954
|
+
? "- Bus subscriber: connected"
|
|
3955
|
+
: `- Bus subscriber: unavailable (${busReady.error || busReady.status || "unknown"})`;
|
|
3956
|
+
if (!transportResult.ok) {
|
|
3957
|
+
manualFallbackPrompt = true;
|
|
3958
|
+
}
|
|
3959
|
+
extra += `\n\n### Telepty Bus Dispatch\n` +
|
|
3960
|
+
`- Envelope: \`${envelope.message_id}\`\n` +
|
|
3961
|
+
`- Kind: \`${envelope.kind}\`\n` +
|
|
3962
|
+
`- Target: \`${envelope.target}\`\n` +
|
|
3963
|
+
`- Delivered subscribers: ${publishResult.delivered ?? "unknown"}\n` +
|
|
3964
|
+
`${subscriberLine}\n` +
|
|
3965
|
+
`- Transport timeout: ${TELEPTY_TRANSPORT_TIMEOUT_MS / 1000}s\n` +
|
|
3966
|
+
`- Semantic timeout: ${TELEPTY_SEMANTIC_TIMEOUT_MS / 1000}s${healthLine}\n\n` +
|
|
3967
|
+
`${transportLine}\n\n` +
|
|
3968
|
+
`The remote telepty session must still self-submit its response with \`deliberation_respond(session_id: "${state.id}", speaker: "${speaker}", ...)\` before the semantic timeout.`;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
2852
3971
|
|
|
2853
3972
|
if (transport === "browser_auto") {
|
|
2854
3973
|
// Auto-execute browser_auto_turn
|
|
@@ -2925,9 +4044,13 @@ server.tool(
|
|
|
2925
4044
|
extra += `\n\n### [turn_prompt]\n\`\`\`markdown\n${turnPrompt}\n\`\`\``;
|
|
2926
4045
|
}
|
|
2927
4046
|
|
|
4047
|
+
if (transport === "telepty_bus" && manualFallbackPrompt) {
|
|
4048
|
+
extra += `\n\n### [turn_prompt]\n\`\`\`markdown\n${turnPrompt}\n\`\`\``;
|
|
4049
|
+
}
|
|
4050
|
+
|
|
2928
4051
|
const profileInfo = profile
|
|
2929
4052
|
? `\n**Profile:** ${profile.type}${profile.url ? ` | ${profile.url}` : ""}${profile.command ? ` | command: ${profile.command}` : ""}`
|
|
2930
|
-
|
|
4053
|
+
: "";
|
|
2931
4054
|
|
|
2932
4055
|
return {
|
|
2933
4056
|
content: [{
|
|
@@ -3059,6 +4182,327 @@ server.tool(
|
|
|
3059
4182
|
})
|
|
3060
4183
|
);
|
|
3061
4184
|
|
|
4185
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
4186
|
+
// Auto-handoff orchestrator helpers
|
|
4187
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
4188
|
+
|
|
4189
|
+
/**
|
|
4190
|
+
* Run a single CLI auto-turn for the given session and speaker.
|
|
4191
|
+
* Returns { ok: true, response, elapsedMs } or { ok: false, error }.
|
|
4192
|
+
*/
|
|
4193
|
+
async function runCliAutoTurnCore(sessionId, speaker, timeoutSec = 120) {
|
|
4194
|
+
const state = loadSession(sessionId);
|
|
4195
|
+
if (!state || state.status !== "active") {
|
|
4196
|
+
return { ok: false, error: "Session not active" };
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
const { transport } = resolveTransportForSpeaker(state, speaker);
|
|
4200
|
+
if (transport !== "cli_respond") {
|
|
4201
|
+
return { ok: false, error: `Speaker "${speaker}" is not CLI type` };
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
const hint = CLI_INVOCATION_HINTS[speaker];
|
|
4205
|
+
if (!hint) return { ok: false, error: `No CLI hints for "${speaker}"` };
|
|
4206
|
+
if (!checkCliLiveness(hint.cmd)) return { ok: false, error: `CLI "${hint.cmd}" not available` };
|
|
4207
|
+
|
|
4208
|
+
const turnId = state.pending_turn_id || generateTurnId();
|
|
4209
|
+
const turnPrompt = buildClipboardTurnPrompt(state, speaker, null, 3);
|
|
4210
|
+
const speakerPriorTurns = state.log.filter(e => e.speaker === speaker).length;
|
|
4211
|
+
const effectiveTimeout = getCliAutoTurnTimeoutSec({
|
|
4212
|
+
speaker,
|
|
4213
|
+
requestedTimeoutSec: timeoutSec,
|
|
4214
|
+
promptLength: turnPrompt.length,
|
|
4215
|
+
priorTurns: speakerPriorTurns,
|
|
4216
|
+
});
|
|
4217
|
+
|
|
4218
|
+
const startTime = Date.now();
|
|
4219
|
+
try {
|
|
4220
|
+
const response = await new Promise((resolve, reject) => {
|
|
4221
|
+
const env = { ...process.env };
|
|
4222
|
+
if (hint.envPrefix?.includes("CLAUDECODE=")) delete env.CLAUDECODE;
|
|
4223
|
+
|
|
4224
|
+
let child;
|
|
4225
|
+
let stdout = "";
|
|
4226
|
+
let stderr = "";
|
|
4227
|
+
let settled = false;
|
|
4228
|
+
let forceKillTimer = null;
|
|
4229
|
+
|
|
4230
|
+
const resolveOnce = (v) => { if (!settled) { settled = true; if (forceKillTimer) clearTimeout(forceKillTimer); resolve(v); } };
|
|
4231
|
+
const rejectOnce = (e) => { if (!settled) { settled = true; if (forceKillTimer) clearTimeout(forceKillTimer); reject(e); } };
|
|
4232
|
+
|
|
4233
|
+
switch (speaker) {
|
|
4234
|
+
case "claude":
|
|
4235
|
+
child = spawn("claude", getCliExecArgs("claude"), { env, windowsHide: true });
|
|
4236
|
+
child.stdin.write(turnPrompt);
|
|
4237
|
+
child.stdin.end();
|
|
4238
|
+
break;
|
|
4239
|
+
case "codex":
|
|
4240
|
+
child = spawn("codex", getCliExecArgs("codex"), { env, windowsHide: true });
|
|
4241
|
+
child.stdin.write(turnPrompt);
|
|
4242
|
+
child.stdin.end();
|
|
4243
|
+
break;
|
|
4244
|
+
case "gemini":
|
|
4245
|
+
child = spawn("gemini", ["-p", turnPrompt], { env, windowsHide: true });
|
|
4246
|
+
break;
|
|
4247
|
+
default: {
|
|
4248
|
+
const flags = hint.flags ? hint.flags.split(/\s+/) : [];
|
|
4249
|
+
child = spawn(hint.cmd, [...flags, turnPrompt], { env, windowsHide: true });
|
|
4250
|
+
break;
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
const timer = setTimeout(() => {
|
|
4255
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
4256
|
+
forceKillTimer = setTimeout(() => { try { child.kill("SIGKILL"); } catch {} }, 5000);
|
|
4257
|
+
if (typeof forceKillTimer?.unref === "function") forceKillTimer.unref();
|
|
4258
|
+
rejectOnce(new Error(`CLI timeout (${effectiveTimeout}s)`));
|
|
4259
|
+
}, effectiveTimeout * 1000);
|
|
4260
|
+
|
|
4261
|
+
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
4262
|
+
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
4263
|
+
|
|
4264
|
+
child.on("close", (code) => {
|
|
4265
|
+
clearTimeout(timer);
|
|
4266
|
+
if (code !== 0 && !stdout.trim()) {
|
|
4267
|
+
rejectOnce(new Error(`CLI exit code ${code}: ${stderr.slice(0, 500)}`));
|
|
4268
|
+
} else {
|
|
4269
|
+
resolveOnce(stdout.trim());
|
|
4270
|
+
}
|
|
4271
|
+
});
|
|
4272
|
+
|
|
4273
|
+
child.on("error", (err) => rejectOnce(err));
|
|
4274
|
+
});
|
|
4275
|
+
|
|
4276
|
+
// Submit the turn
|
|
4277
|
+
submitDeliberationTurn({
|
|
4278
|
+
session_id: sessionId,
|
|
4279
|
+
speaker,
|
|
4280
|
+
content: response,
|
|
4281
|
+
turn_id: turnId,
|
|
4282
|
+
channel_used: "cli_auto",
|
|
4283
|
+
});
|
|
4284
|
+
|
|
4285
|
+
return { ok: true, response, elapsedMs: Date.now() - startTime };
|
|
4286
|
+
} catch (err) {
|
|
4287
|
+
return { ok: false, error: err.message };
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
/**
|
|
4292
|
+
* Generate structured synthesis by calling a CLI speaker with a synthesis prompt.
|
|
4293
|
+
*/
|
|
4294
|
+
async function generateAutoSynthesis(sessionId) {
|
|
4295
|
+
const state = loadSession(sessionId);
|
|
4296
|
+
if (!state) return null;
|
|
4297
|
+
|
|
4298
|
+
const historyText = state.log.map(e => `[${e.speaker}] ${e.content}`).join("\n\n---\n\n");
|
|
4299
|
+
|
|
4300
|
+
const synthesisPrompt = `You are a deliberation synthesizer. Analyze this discussion and produce ONLY a JSON response (no markdown, no explanation).
|
|
4301
|
+
|
|
4302
|
+
Topic: ${state.topic}
|
|
4303
|
+
Project: ${state.project}
|
|
4304
|
+
Rounds: ${state.max_rounds}
|
|
4305
|
+
|
|
4306
|
+
Discussion:
|
|
4307
|
+
${historyText}
|
|
4308
|
+
|
|
4309
|
+
Respond with EXACTLY this JSON structure:
|
|
4310
|
+
{
|
|
4311
|
+
"summary": "Brief summary of the outcome",
|
|
4312
|
+
"decisions": ["Decision 1", "Decision 2"],
|
|
4313
|
+
"actionable_tasks": [
|
|
4314
|
+
{"id": 1, "task": "What to do", "files": ["path/to/file.ts"], "project": "${state.project}", "priority": "high|medium|low"}
|
|
4315
|
+
],
|
|
4316
|
+
"markdown_synthesis": "# Full synthesis in markdown\\n\\n..."
|
|
4317
|
+
}`;
|
|
4318
|
+
|
|
4319
|
+
// Use the first available CLI speaker to generate synthesis
|
|
4320
|
+
const speaker = state.speakers.find(s => {
|
|
4321
|
+
const hint = CLI_INVOCATION_HINTS[s];
|
|
4322
|
+
return hint && checkCliLiveness(hint.cmd);
|
|
4323
|
+
});
|
|
4324
|
+
|
|
4325
|
+
if (!speaker) return null;
|
|
4326
|
+
|
|
4327
|
+
const hint = CLI_INVOCATION_HINTS[speaker];
|
|
4328
|
+
|
|
4329
|
+
try {
|
|
4330
|
+
const response = await new Promise((resolve, reject) => {
|
|
4331
|
+
const env = { ...process.env };
|
|
4332
|
+
if (hint.envPrefix?.includes("CLAUDECODE=")) delete env.CLAUDECODE;
|
|
4333
|
+
|
|
4334
|
+
let child;
|
|
4335
|
+
let stdout = "";
|
|
4336
|
+
|
|
4337
|
+
switch (speaker) {
|
|
4338
|
+
case "claude":
|
|
4339
|
+
child = spawn("claude", getCliExecArgs("claude"), { env, windowsHide: true });
|
|
4340
|
+
child.stdin.write(synthesisPrompt);
|
|
4341
|
+
child.stdin.end();
|
|
4342
|
+
break;
|
|
4343
|
+
case "codex":
|
|
4344
|
+
child = spawn("codex", getCliExecArgs("codex"), { env, windowsHide: true });
|
|
4345
|
+
child.stdin.write(synthesisPrompt);
|
|
4346
|
+
child.stdin.end();
|
|
4347
|
+
break;
|
|
4348
|
+
case "gemini":
|
|
4349
|
+
child = spawn("gemini", ["-p", synthesisPrompt], { env, windowsHide: true });
|
|
4350
|
+
break;
|
|
4351
|
+
default: {
|
|
4352
|
+
const flags = hint.flags ? hint.flags.split(/\s+/) : [];
|
|
4353
|
+
child = spawn(hint.cmd, [...flags, synthesisPrompt], { env, windowsHide: true });
|
|
4354
|
+
break;
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
const timer = setTimeout(() => {
|
|
4359
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
4360
|
+
reject(new Error("Synthesis generation timeout"));
|
|
4361
|
+
}, 180000); // 3 min timeout for synthesis
|
|
4362
|
+
|
|
4363
|
+
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
4364
|
+
child.on("close", (code) => {
|
|
4365
|
+
clearTimeout(timer);
|
|
4366
|
+
resolve(stdout.trim());
|
|
4367
|
+
});
|
|
4368
|
+
child.on("error", reject);
|
|
4369
|
+
});
|
|
4370
|
+
|
|
4371
|
+
// Extract JSON from response (may have markdown wrapping)
|
|
4372
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
4373
|
+
if (!jsonMatch) return { markdown_synthesis: response };
|
|
4374
|
+
|
|
4375
|
+
try {
|
|
4376
|
+
return JSON.parse(jsonMatch[0]);
|
|
4377
|
+
} catch {
|
|
4378
|
+
return { markdown_synthesis: response };
|
|
4379
|
+
}
|
|
4380
|
+
} catch (err) {
|
|
4381
|
+
appendRuntimeLog("ERROR", `AUTO_SYNTHESIS_FAILED: ${sessionId} | ${err.message}`);
|
|
4382
|
+
return null;
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
/**
|
|
4387
|
+
* Orchestrate full auto-handoff: run all turns -> synthesize -> inbox -> telepty.
|
|
4388
|
+
* Called as fire-and-forget from deliberation_start when auto_execute is true.
|
|
4389
|
+
*/
|
|
4390
|
+
async function runAutoHandoff(sessionId) {
|
|
4391
|
+
appendRuntimeLog("INFO", `AUTO_HANDOFF_START: ${sessionId}`);
|
|
4392
|
+
|
|
4393
|
+
try {
|
|
4394
|
+
// Phase 1: Run all deliberation turns
|
|
4395
|
+
let maxIterations = 100; // safety limit
|
|
4396
|
+
while (maxIterations-- > 0) {
|
|
4397
|
+
const state = loadSession(sessionId);
|
|
4398
|
+
if (!state) {
|
|
4399
|
+
appendRuntimeLog("ERROR", `AUTO_HANDOFF: Session ${sessionId} disappeared`);
|
|
4400
|
+
return;
|
|
4401
|
+
}
|
|
4402
|
+
if (state.status !== "active") {
|
|
4403
|
+
appendRuntimeLog("INFO", `AUTO_HANDOFF: Session ${sessionId} status=${state.status}, turns done`);
|
|
4404
|
+
break;
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
const speaker = state.current_speaker;
|
|
4408
|
+
if (speaker === "none") break;
|
|
4409
|
+
|
|
4410
|
+
appendRuntimeLog("INFO", `AUTO_HANDOFF_TURN: ${sessionId} | speaker: ${speaker} | round: ${state.current_round}/${state.max_rounds}`);
|
|
4411
|
+
|
|
4412
|
+
const result = await runCliAutoTurnCore(sessionId, speaker);
|
|
4413
|
+
if (!result.ok) {
|
|
4414
|
+
appendRuntimeLog("WARN", `AUTO_HANDOFF_TURN_FAIL: ${sessionId} | speaker: ${speaker} | ${result.error}`);
|
|
4415
|
+
// Skip this speaker, continue with next
|
|
4416
|
+
const freshState = loadSession(sessionId);
|
|
4417
|
+
if (freshState) {
|
|
4418
|
+
// Advance to next speaker manually
|
|
4419
|
+
const idx = freshState.speakers.indexOf(speaker);
|
|
4420
|
+
const nextIdx = (idx + 1) % freshState.speakers.length;
|
|
4421
|
+
freshState.current_speaker = freshState.speakers[nextIdx];
|
|
4422
|
+
if (nextIdx === 0) freshState.current_round++;
|
|
4423
|
+
if (freshState.current_round > freshState.max_rounds) {
|
|
4424
|
+
freshState.status = "awaiting_synthesis";
|
|
4425
|
+
freshState.current_speaker = "none";
|
|
4426
|
+
}
|
|
4427
|
+
saveSession(freshState);
|
|
4428
|
+
}
|
|
4429
|
+
continue;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
appendRuntimeLog("INFO", `AUTO_HANDOFF_TURN_OK: ${sessionId} | speaker: ${speaker} | ${result.elapsedMs}ms`);
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
// Phase 2: Generate structured synthesis
|
|
4436
|
+
appendRuntimeLog("INFO", `AUTO_HANDOFF_SYNTHESIZE: ${sessionId}`);
|
|
4437
|
+
let synthResult = await generateAutoSynthesis(sessionId);
|
|
4438
|
+
|
|
4439
|
+
// Phase 3: Call synthesize (reuse existing logic)
|
|
4440
|
+
const state = loadSession(sessionId);
|
|
4441
|
+
if (!state) return;
|
|
4442
|
+
|
|
4443
|
+
// Fallback: if synthesis generation failed, build a basic structure from the discussion
|
|
4444
|
+
if (!synthResult || (!synthResult.summary && !synthResult.actionable_tasks)) {
|
|
4445
|
+
appendRuntimeLog("WARN", `AUTO_HANDOFF_SYNTH_FALLBACK: ${sessionId} | Building fallback from discussion log`);
|
|
4446
|
+
const turns = state.log || [];
|
|
4447
|
+
const fallbackSummary = turns.length > 0
|
|
4448
|
+
? `Deliberation on "${state.topic}" completed with ${turns.length} turns from ${[...new Set(turns.map(t => t.speaker))].join(", ")}.`
|
|
4449
|
+
: `Deliberation on "${state.topic}" completed.`;
|
|
4450
|
+
synthResult = {
|
|
4451
|
+
summary: fallbackSummary,
|
|
4452
|
+
decisions: [`Discussed: ${state.topic}`],
|
|
4453
|
+
actionable_tasks: [],
|
|
4454
|
+
markdown_synthesis: `# Auto-generated synthesis (fallback)\n\n${fallbackSummary}\n\n## Discussion\n${turns.map(t => `**${t.speaker}**: ${typeof t.content === 'string' ? t.content.substring(0, 200) : '(no content)'}${t.content && t.content.length > 200 ? '...' : ''}`).join("\n\n")}`,
|
|
4455
|
+
};
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
const markdownSynthesis = synthResult?.markdown_synthesis ||
|
|
4459
|
+
`# Auto-generated synthesis\n\n${synthResult?.summary || "Deliberation completed."}\n\n## Decisions\n${(synthResult?.decisions || []).map(d => `- ${d}`).join("\n")}\n\n## Tasks\n${(synthResult?.actionable_tasks || []).map(t => `- [${t.priority}] ${t.task}`).join("\n")}`;
|
|
4460
|
+
|
|
4461
|
+
const structured = {
|
|
4462
|
+
summary: synthResult.summary || "",
|
|
4463
|
+
decisions: synthResult.decisions || [],
|
|
4464
|
+
actionable_tasks: synthResult.actionable_tasks || [],
|
|
4465
|
+
};
|
|
4466
|
+
|
|
4467
|
+
// Apply synthesis to session
|
|
4468
|
+
withSessionLock(sessionId, () => {
|
|
4469
|
+
const loaded = loadSession(sessionId);
|
|
4470
|
+
if (!loaded) return;
|
|
4471
|
+
loaded.synthesis = markdownSynthesis;
|
|
4472
|
+
loaded.structured_synthesis = structured;
|
|
4473
|
+
loaded.status = "completed";
|
|
4474
|
+
loaded.current_speaker = "none";
|
|
4475
|
+
saveSession(loaded);
|
|
4476
|
+
archiveState(loaded);
|
|
4477
|
+
cleanupSyncMarkdown(loaded);
|
|
4478
|
+
|
|
4479
|
+
const sessionFile = getSessionFile(loaded);
|
|
4480
|
+
try { if (fs.existsSync(sessionFile)) fs.unlinkSync(sessionFile); } catch {}
|
|
4481
|
+
});
|
|
4482
|
+
|
|
4483
|
+
closeMonitorTerminal(sessionId, getSessionWindowIds(state));
|
|
4484
|
+
|
|
4485
|
+
appendRuntimeLog("INFO", `AUTO_HANDOFF_SYNTHESIZED: ${sessionId}`);
|
|
4486
|
+
|
|
4487
|
+
// Phase 4: Notify telepty bus with full structured data for dustcraw to consume
|
|
4488
|
+
if (state.auto_execute) {
|
|
4489
|
+
const envelope = buildTeleptySynthesisEnvelope({
|
|
4490
|
+
state,
|
|
4491
|
+
synthesis: markdownSynthesis,
|
|
4492
|
+
structured,
|
|
4493
|
+
});
|
|
4494
|
+
await notifyTeleptyBus(envelope).catch(() => {});
|
|
4495
|
+
appendRuntimeLog("INFO", `AUTO_HANDOFF_NOTIFIED: ${sessionId} | telepty event sent`);
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
appendRuntimeLog("INFO", `AUTO_HANDOFF_COMPLETE: ${sessionId}`);
|
|
4499
|
+
} catch (err) {
|
|
4500
|
+
appendRuntimeLog("ERROR", `AUTO_HANDOFF_ERROR: ${sessionId} | ${err.message}`);
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
4505
|
+
|
|
3062
4506
|
server.tool(
|
|
3063
4507
|
"deliberation_cli_auto_turn",
|
|
3064
4508
|
"Automatically send a turn to a CLI speaker and collect the response.",
|
|
@@ -3107,10 +4551,7 @@ server.tool(
|
|
|
3107
4551
|
}] };
|
|
3108
4552
|
}
|
|
3109
4553
|
|
|
3110
|
-
// Dynamic timeout: first turn gets extra time for cold-start
|
|
3111
4554
|
const speakerPriorTurns = state.log.filter(e => e.speaker === speaker).length;
|
|
3112
|
-
const effectiveTimeout = speakerPriorTurns === 0 ? Math.max(timeout_sec, 180) : timeout_sec;
|
|
3113
|
-
|
|
3114
4555
|
const hint = CLI_INVOCATION_HINTS[speaker];
|
|
3115
4556
|
if (!hint) {
|
|
3116
4557
|
return { content: [{ type: "text", text: t(`No CLI invocation info for speaker "${speaker}". This speaker is not registered in CLI_INVOCATION_HINTS.`, `speaker "${speaker}"에 대한 CLI 호출 정보가 없습니다. CLI_INVOCATION_HINTS에 등록되지 않은 speaker입니다.`, state?.lang) }] };
|
|
@@ -3123,6 +4564,12 @@ server.tool(
|
|
|
3123
4564
|
|
|
3124
4565
|
const turnId = state.pending_turn_id || generateTurnId();
|
|
3125
4566
|
const turnPrompt = buildClipboardTurnPrompt(state, speaker, null, 3);
|
|
4567
|
+
const effectiveTimeout = getCliAutoTurnTimeoutSec({
|
|
4568
|
+
speaker,
|
|
4569
|
+
requestedTimeoutSec: timeout_sec,
|
|
4570
|
+
promptLength: turnPrompt.length,
|
|
4571
|
+
priorTurns: speakerPriorTurns,
|
|
4572
|
+
});
|
|
3126
4573
|
|
|
3127
4574
|
// Spawn CLI process
|
|
3128
4575
|
const startTime = Date.now();
|
|
@@ -3137,16 +4584,31 @@ server.tool(
|
|
|
3137
4584
|
let child;
|
|
3138
4585
|
let stdout = "";
|
|
3139
4586
|
let stderr = "";
|
|
4587
|
+
let settled = false;
|
|
4588
|
+
let forceKillTimer = null;
|
|
4589
|
+
|
|
4590
|
+
const resolveOnce = (value) => {
|
|
4591
|
+
if (settled) return;
|
|
4592
|
+
settled = true;
|
|
4593
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
4594
|
+
resolve(value);
|
|
4595
|
+
};
|
|
4596
|
+
const rejectOnce = (error) => {
|
|
4597
|
+
if (settled) return;
|
|
4598
|
+
settled = true;
|
|
4599
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
4600
|
+
reject(error);
|
|
4601
|
+
};
|
|
3140
4602
|
|
|
3141
4603
|
// Different invocation patterns per CLI
|
|
3142
4604
|
switch (speaker) {
|
|
3143
4605
|
case "claude":
|
|
3144
|
-
child = spawn("claude",
|
|
4606
|
+
child = spawn("claude", getCliExecArgs("claude"), { env, windowsHide: true });
|
|
3145
4607
|
child.stdin.write(turnPrompt);
|
|
3146
4608
|
child.stdin.end();
|
|
3147
4609
|
break;
|
|
3148
4610
|
case "codex":
|
|
3149
|
-
child = spawn("codex",
|
|
4611
|
+
child = spawn("codex", getCliExecArgs("codex"), { env, windowsHide: true });
|
|
3150
4612
|
child.stdin.write(turnPrompt);
|
|
3151
4613
|
child.stdin.end();
|
|
3152
4614
|
break;
|
|
@@ -3162,8 +4624,19 @@ server.tool(
|
|
|
3162
4624
|
}
|
|
3163
4625
|
|
|
3164
4626
|
const timer = setTimeout(() => {
|
|
3165
|
-
|
|
3166
|
-
|
|
4627
|
+
appendRuntimeLog("WARN", `CLI_TURN_TIMEOUT: ${resolved} | speaker: ${speaker} | cli: ${hint.cmd} | timeout: ${effectiveTimeout}s | prompt_len: ${turnPrompt.length} | prior_turns: ${speakerPriorTurns}`);
|
|
4628
|
+
try {
|
|
4629
|
+
child.kill("SIGTERM");
|
|
4630
|
+
} catch { /* ignore */ }
|
|
4631
|
+
forceKillTimer = setTimeout(() => {
|
|
4632
|
+
try {
|
|
4633
|
+
child.kill("SIGKILL");
|
|
4634
|
+
} catch { /* ignore */ }
|
|
4635
|
+
}, 5000);
|
|
4636
|
+
if (typeof forceKillTimer.unref === "function") {
|
|
4637
|
+
forceKillTimer.unref();
|
|
4638
|
+
}
|
|
4639
|
+
rejectOnce(new Error(`CLI timeout (${effectiveTimeout}s)`));
|
|
3167
4640
|
}, effectiveTimeout * 1000);
|
|
3168
4641
|
|
|
3169
4642
|
child.stdout.on("data", (data) => { stdout += data.toString(); });
|
|
@@ -3172,7 +4645,8 @@ server.tool(
|
|
|
3172
4645
|
child.on("close", (code) => {
|
|
3173
4646
|
clearTimeout(timer);
|
|
3174
4647
|
if (code !== 0 && !stdout.trim()) {
|
|
3175
|
-
|
|
4648
|
+
appendRuntimeLog("ERROR", `CLI_TURN_EXIT: ${resolved} | speaker: ${speaker} | cli: ${hint.cmd} | code: ${code} | stderr: ${stderr.slice(0, 200).replace(/\s+/g, " ")}`);
|
|
4649
|
+
rejectOnce(new Error(`CLI exit code ${code}: ${stderr.slice(0, 500)}`));
|
|
3176
4650
|
} else {
|
|
3177
4651
|
// Clean up output noise
|
|
3178
4652
|
let cleaned = stdout;
|
|
@@ -3183,12 +4657,12 @@ server.tool(
|
|
|
3183
4657
|
const codexLineIdx = lines.findIndex(l => l.trim() === "codex");
|
|
3184
4658
|
if (codexLineIdx !== -1) {
|
|
3185
4659
|
cleaned = lines.slice(codexLineIdx + 1)
|
|
3186
|
-
.filter(line => !/^(tokens used$|^[0-9,]
|
|
4660
|
+
.filter(line => !/^(tokens used$|^[0-9,]*$|^mcp:.*)/.test(line))
|
|
3187
4661
|
.join("\n");
|
|
3188
4662
|
} else {
|
|
3189
4663
|
// Fallback regex cleaning
|
|
3190
4664
|
cleaned = stdout.split("\n")
|
|
3191
|
-
.filter(line => !/^(OpenAI Codex|--------|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|mcp
|
|
4665
|
+
.filter(line => !/^(OpenAI Codex|--------|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|mcp:.*|thinking$|tokens used$|^[0-9,]*$)/.test(line))
|
|
3192
4666
|
.join("\n");
|
|
3193
4667
|
}
|
|
3194
4668
|
} else if (speaker === "gemini") {
|
|
@@ -3196,13 +4670,14 @@ server.tool(
|
|
|
3196
4670
|
.filter(line => !/^(Loaded cached|Error during discovery|\[MCP error\]| {4}at| {2}errno:| {2}code:| {2}syscall:| {2}path:| {2}spawnargs:|MCP issues detected|Server .* supports tool updates)/.test(line))
|
|
3197
4671
|
.join("\n");
|
|
3198
4672
|
}
|
|
3199
|
-
|
|
4673
|
+
resolveOnce(cleaned.trim());
|
|
3200
4674
|
}
|
|
3201
4675
|
});
|
|
3202
4676
|
|
|
3203
4677
|
child.on("error", (err) => {
|
|
3204
4678
|
clearTimeout(timer);
|
|
3205
|
-
|
|
4679
|
+
appendRuntimeLog("ERROR", `CLI_TURN_ERROR: ${resolved} | speaker: ${speaker} | cli: ${hint.cmd} | error: ${String(err.message || err).replace(/\s+/g, " ")}`);
|
|
4680
|
+
rejectOnce(err);
|
|
3206
4681
|
});
|
|
3207
4682
|
});
|
|
3208
4683
|
|
|
@@ -3234,7 +4709,15 @@ server.tool(
|
|
|
3234
4709
|
return {
|
|
3235
4710
|
content: [{
|
|
3236
4711
|
type: "text",
|
|
3237
|
-
text:
|
|
4712
|
+
text: buildCliAutoTurnFailureText({
|
|
4713
|
+
state,
|
|
4714
|
+
speaker,
|
|
4715
|
+
hint,
|
|
4716
|
+
err,
|
|
4717
|
+
effectiveTimeout,
|
|
4718
|
+
promptLength: turnPrompt.length,
|
|
4719
|
+
priorTurns: speakerPriorTurns,
|
|
4720
|
+
}),
|
|
3238
4721
|
}],
|
|
3239
4722
|
};
|
|
3240
4723
|
}
|
|
@@ -3495,12 +4978,22 @@ server.tool(
|
|
|
3495
4978
|
|
|
3496
4979
|
server.tool(
|
|
3497
4980
|
"deliberation_synthesize",
|
|
3498
|
-
"End the deliberation and submit a synthesis report.",
|
|
4981
|
+
"End the deliberation and submit a synthesis report. Optionally include structured actionable tasks for automated handoff.",
|
|
3499
4982
|
{
|
|
3500
4983
|
session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
|
|
3501
4984
|
synthesis: z.string().describe("Synthesis report (markdown)"),
|
|
4985
|
+
structured: z.preprocess(
|
|
4986
|
+
(v) => {
|
|
4987
|
+
if (typeof v === "string") {
|
|
4988
|
+
try { return JSON.parse(v); }
|
|
4989
|
+
catch { return v; }
|
|
4990
|
+
}
|
|
4991
|
+
return v;
|
|
4992
|
+
},
|
|
4993
|
+
StructuredSynthesisSchema.optional()
|
|
4994
|
+
).describe("Structured synthesis data for automated handoff. If omitted, only markdown synthesis is stored."),
|
|
3502
4995
|
},
|
|
3503
|
-
safeToolHandler("deliberation_synthesize", async ({ session_id, synthesis }) => {
|
|
4996
|
+
safeToolHandler("deliberation_synthesize", async ({ session_id, synthesis, structured }) => {
|
|
3504
4997
|
const resolved = resolveSessionId(session_id);
|
|
3505
4998
|
if (!resolved) {
|
|
3506
4999
|
return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
|
|
@@ -3518,13 +5011,15 @@ server.tool(
|
|
|
3518
5011
|
}
|
|
3519
5012
|
|
|
3520
5013
|
loaded.synthesis = synthesis;
|
|
5014
|
+
loaded.structured_synthesis = structured || null;
|
|
3521
5015
|
loaded.status = "completed";
|
|
3522
5016
|
loaded.current_speaker = "none";
|
|
3523
5017
|
saveSession(loaded);
|
|
3524
5018
|
archivePath = archiveState(loaded);
|
|
3525
5019
|
cleanupSyncMarkdown(loaded);
|
|
5020
|
+
|
|
3526
5021
|
// Clean up the active session JSON file upon completion
|
|
3527
|
-
const sessionFile = getSessionFile(loaded
|
|
5022
|
+
const sessionFile = getSessionFile(loaded);
|
|
3528
5023
|
try { if (fs.existsSync(sessionFile)) fs.unlinkSync(sessionFile); } catch { /* ignore */ }
|
|
3529
5024
|
state = loaded;
|
|
3530
5025
|
return null;
|
|
@@ -3534,10 +5029,20 @@ server.tool(
|
|
|
3534
5029
|
}
|
|
3535
5030
|
|
|
3536
5031
|
appendRuntimeLog("INFO", `SYNTHESIZED: ${resolved} | turns: ${state.log.length} | rounds: ${state.max_rounds}`);
|
|
5032
|
+
const synthesisEnvelope = buildTeleptySynthesisEnvelope({
|
|
5033
|
+
state,
|
|
5034
|
+
synthesis,
|
|
5035
|
+
structured,
|
|
5036
|
+
});
|
|
3537
5037
|
|
|
3538
5038
|
// Immediately force-close monitor terminal (including physical Terminal) on deliberation end
|
|
3539
5039
|
closeMonitorTerminal(state.id, getSessionWindowIds(state));
|
|
3540
5040
|
|
|
5041
|
+
// Notify telepty bus with full structured data for dustcraw to consume
|
|
5042
|
+
if (state.auto_execute) {
|
|
5043
|
+
notifyTeleptyBus(synthesisEnvelope).catch(() => {}); // fire-and-forget
|
|
5044
|
+
}
|
|
5045
|
+
|
|
3541
5046
|
return {
|
|
3542
5047
|
content: [{
|
|
3543
5048
|
type: "text",
|
|
@@ -3585,11 +5090,11 @@ server.tool(
|
|
|
3585
5090
|
// Reset specific session only
|
|
3586
5091
|
let toCloseIds = [];
|
|
3587
5092
|
const result = withSessionLock(session_id, () => {
|
|
3588
|
-
const
|
|
3589
|
-
if (!
|
|
5093
|
+
const state = loadSession(session_id);
|
|
5094
|
+
if (!state) {
|
|
3590
5095
|
return { content: [{ type: "text", text: t(`Session "${session_id}" not found.`, `세션 "${session_id}"을 찾을 수 없습니다.`, "en") }] };
|
|
3591
5096
|
}
|
|
3592
|
-
const
|
|
5097
|
+
const file = getSessionFile(state);
|
|
3593
5098
|
if (state && state.log.length > 0) {
|
|
3594
5099
|
archiveState(state);
|
|
3595
5100
|
}
|
|
@@ -3665,7 +5170,11 @@ server.tool(
|
|
|
3665
5170
|
require_speaker_selection: z.preprocess(
|
|
3666
5171
|
(v) => (typeof v === "string" ? v === "true" : v),
|
|
3667
5172
|
z.boolean().optional()
|
|
3668
|
-
).describe("
|
|
5173
|
+
).describe("Deprecated toggle. Speaker selection is now always manual; any provided value is normalized to true."),
|
|
5174
|
+
include_browser_speakers: z.preprocess(
|
|
5175
|
+
(v) => (typeof v === "string" ? v === "true" : v),
|
|
5176
|
+
z.boolean().optional()
|
|
5177
|
+
).describe("true: browser LLM speakers may join when requested, false: CLI + telepty candidate mode"),
|
|
3669
5178
|
default_rounds: z.coerce.number().int().min(1).max(10).optional()
|
|
3670
5179
|
.describe("Default number of rounds (1-10, default 3)"),
|
|
3671
5180
|
default_ordering: z.enum(["auto", "cyclic", "random", "weighted-random"]).optional()
|
|
@@ -3673,13 +5182,17 @@ server.tool(
|
|
|
3673
5182
|
chrome_profile: z.string().optional()
|
|
3674
5183
|
.describe("Chrome profile directory name for CDP (e.g., \"Default\", \"Profile 1\"). Stored for auto-launch."),
|
|
3675
5184
|
},
|
|
3676
|
-
safeToolHandler("deliberation_cli_config", async ({ enabled_clis, require_speaker_selection, default_rounds, default_ordering, chrome_profile }) => {
|
|
5185
|
+
safeToolHandler("deliberation_cli_config", async ({ enabled_clis, require_speaker_selection, include_browser_speakers, default_rounds, default_ordering, chrome_profile }) => {
|
|
3677
5186
|
const config = loadDeliberationConfig();
|
|
3678
5187
|
|
|
3679
5188
|
// Handle setup config updates
|
|
3680
5189
|
let configChanged = false;
|
|
3681
5190
|
if (require_speaker_selection !== undefined && require_speaker_selection !== null) {
|
|
3682
|
-
config.require_speaker_selection =
|
|
5191
|
+
config.require_speaker_selection = true;
|
|
5192
|
+
configChanged = true;
|
|
5193
|
+
}
|
|
5194
|
+
if (include_browser_speakers !== undefined && include_browser_speakers !== null) {
|
|
5195
|
+
config.include_browser_speakers = include_browser_speakers;
|
|
3683
5196
|
configChanged = true;
|
|
3684
5197
|
}
|
|
3685
5198
|
if (default_rounds !== undefined && default_rounds !== null) {
|
|
@@ -3708,7 +5221,7 @@ server.tool(
|
|
|
3708
5221
|
return {
|
|
3709
5222
|
content: [{
|
|
3710
5223
|
type: "text",
|
|
3711
|
-
text: `## Deliberation CLI Settings\n\n**Mode:** ${mode}\n**Speaker selection:** ${config.
|
|
5224
|
+
text: `## Deliberation CLI Settings\n\n**Mode:** ${mode}\n**Speaker selection:** manual only (fresh user selection required every start)\n**Browser speakers:** ${config.include_browser_speakers === true ? "enabled" : "disabled (CLI + telepty default)"}\n**Default rounds:** ${config.default_rounds || 3}\n**Ordering:** ${config.default_ordering || "auto"}\n**Chrome profile:** ${config.chrome_profile || "Default"} (env: DELIBERATION_CHROME_PROFILE)\n**Configured CLIs:** ${configured.length > 0 ? configured.join(", ") : "(none — full auto-detection)"}\n**Currently detected CLIs:** ${detected.join(", ") || "(none)"}\n**All supported CLIs:** ${DEFAULT_CLI_CANDIDATES.join(", ")}\n\nℹ️ Every start now requires two steps: \`deliberation_speaker_candidates\` for a fresh snapshot, then \`deliberation_confirm_speakers\` for the exact user-picked set. Telepty active sessions are included in the candidate list automatically.\n\nTo change defaults:\n\`deliberation_cli_config(include_browser_speakers: false, default_rounds: 3, default_ordering: "auto")\`\n\nTo enable browser speakers:\n\`deliberation_cli_config(include_browser_speakers: true)\`\n\nTo set Chrome profile for CDP:\n\`deliberation_cli_config(chrome_profile: "Profile 1")\`\n\nTo revert CLI filters to full auto-detection:\n\`deliberation_cli_config(enabled_clis: [])\``,
|
|
3712
5225
|
}],
|
|
3713
5226
|
};
|
|
3714
5227
|
}
|
|
@@ -4140,11 +5653,12 @@ server.tool(
|
|
|
4140
5653
|
const env = { ...process.env, NO_COLOR: "1" };
|
|
4141
5654
|
|
|
4142
5655
|
if (speaker === "claude") {
|
|
4143
|
-
|
|
5656
|
+
const args = getCliExecArgs("claude");
|
|
5657
|
+
proc = spawn("claude", args.includes("--no-input") ? args : [...args, "--no-input"], { env, windowsHide: true });
|
|
4144
5658
|
proc.stdin.write(opinionPrompt);
|
|
4145
5659
|
proc.stdin.end();
|
|
4146
5660
|
} else if (speaker === "codex") {
|
|
4147
|
-
proc = spawn("codex",
|
|
5661
|
+
proc = spawn("codex", getCliExecArgs("codex"), { env, windowsHide: true });
|
|
4148
5662
|
proc.stdin.write(opinionPrompt);
|
|
4149
5663
|
proc.stdin.end();
|
|
4150
5664
|
} else if (speaker === "gemini") {
|
|
@@ -4170,7 +5684,7 @@ server.tool(
|
|
|
4170
5684
|
const codexLineIdx = lines.findIndex(l => l.trim() === "codex");
|
|
4171
5685
|
if (codexLineIdx !== -1) {
|
|
4172
5686
|
cleaned = lines.slice(codexLineIdx + 1)
|
|
4173
|
-
.filter(line => !/^(tokens used$|^[0-9,]
|
|
5687
|
+
.filter(line => !/^(tokens used$|^[0-9,]*$|^mcp:.*)/.test(line))
|
|
4174
5688
|
.join("\n").trim();
|
|
4175
5689
|
}
|
|
4176
5690
|
} else if (speaker === "gemini") {
|
|
@@ -4513,4 +6027,4 @@ if (__entryFile && path.resolve(__currentFile) === __entryFile) {
|
|
|
4513
6027
|
}
|
|
4514
6028
|
|
|
4515
6029
|
// ── Test exports (used by vitest) ──
|
|
4516
|
-
export { selectNextSpeaker, loadRolePrompt, inferSuggestedRole, parseVotes, ROLE_KEYWORDS, ROLE_HEADING_MARKERS, loadRolePresets, applyRolePreset, detectDegradationLevels, formatDegradationReport, DEGRADATION_TIERS, DECISION_STAGES, STAGE_TRANSITIONS, createDecisionSession, advanceStage, buildConflictMap, parseOpinionFromResponse, buildOpinionPrompt, generateConflictQuestions, buildSynthesis, buildActionPlan, loadTemplates, matchTemplate };
|
|
6030
|
+
export { selectNextSpeaker, loadRolePrompt, inferSuggestedRole, parseVotes, ROLE_KEYWORDS, ROLE_HEADING_MARKERS, loadRolePresets, applyRolePreset, detectDegradationLevels, formatDegradationReport, DEGRADATION_TIERS, DECISION_STAGES, STAGE_TRANSITIONS, createDecisionSession, advanceStage, buildConflictMap, parseOpinionFromResponse, buildOpinionPrompt, generateConflictQuestions, buildSynthesis, buildActionPlan, loadTemplates, matchTemplate, hasExplicitBrowserParticipantSelection, resolveIncludeBrowserSpeakers, confirmSpeakerSelectionToken, validateSpeakerSelectionRequest, truncatePromptText, getPromptBudgetForSpeaker, formatRecentLogForPrompt, getCliAutoTurnTimeoutSec, getCliExecArgs, buildCliAutoTurnFailureText, buildClipboardTurnPrompt, getProjectStateDir, loadSession, saveSession, listActiveSessions, multipleSessionsError, findSessionRecord, mapParticipantProfiles, formatSpeakerCandidatesReport, buildTeleptyTurnRequestEnvelope, buildTeleptySynthesisEnvelope, validateTeleptyEnvelope, registerPendingTeleptyTurnRequest, handleTeleptyBusMessage, completePendingTeleptySemantic, cleanupPendingTeleptyTurn, getTeleptySessionHealth, TELEPTY_TRANSPORT_TIMEOUT_MS, TELEPTY_SEMANTIC_TIMEOUT_MS };
|