@dmsdc-ai/aigentry-deliberation 0.0.38 → 0.0.40

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/lib/telepty.js ADDED
@@ -0,0 +1,868 @@
1
+ /**
2
+ * Telepty/Bus domain — envelope building, bus subscription, session health,
3
+ * pending turn request tracking, and process locator utilities.
4
+ *
5
+ * Extracted from index.js to keep the main entry point focused on MCP tool
6
+ * registration while this module owns the Telepty wire protocol.
7
+ */
8
+
9
+ import { z } from "zod";
10
+ import { createHash } from "crypto";
11
+ import { execFileSync } from "child_process";
12
+ import fs from "fs";
13
+ import path from "path";
14
+ import os from "os";
15
+ import WebSocket from "ws";
16
+
17
+ // ── Dependency injection ────────────────────────────────────────
18
+ // Functions that live in index.js but are needed here. Injected once
19
+ // via `initTeleptyDeps()` so we avoid circular imports.
20
+
21
+ let _deps = {
22
+ appendRuntimeLog: () => {},
23
+ normalizeSpeaker: (v) => v,
24
+ getProjectSlug: () => "unknown",
25
+ // Only needed by dispatchTeleptyTurnRequest:
26
+ resolveTransportForSpeaker: () => ({ transport: "manual", reason: "not_initialized" }),
27
+ generateTurnId: () => `t-${Date.now().toString(36)}`,
28
+ buildClipboardTurnPrompt: () => "",
29
+ };
30
+
31
+ export function initTeleptyDeps(deps) {
32
+ Object.assign(_deps, deps);
33
+ }
34
+
35
+ // ── Constants ───────────────────────────────────────────────────
36
+
37
+ const HOME = os.homedir();
38
+ export const TELEPTY_CONFIG_FILE = path.join(HOME, ".telepty", "config.json");
39
+ export const TELEPTY_DEFAULT_HOST = process.env.TELEPTY_HOST || "127.0.0.1";
40
+ export const TELEPTY_PORT = Number(process.env.TELEPTY_PORT || 3848);
41
+ export const TELEPTY_TRANSPORT_TIMEOUT_MS = 5_000;
42
+ export const TELEPTY_SEMANTIC_TIMEOUT_MS = 60_000;
43
+ export const TELEPTY_BUS_RECONNECT_MS = 5_000;
44
+ export const TELEPTY_SESSION_HEALTH_STALE_MS = 25_000;
45
+
46
+ // ── Zod schemas ─────────────────────────────────────────────────
47
+
48
+ export const StructuredActionableTaskSchema = z.object({
49
+ id: z.number(),
50
+ task: z.string(),
51
+ files: z.array(z.string()).optional(),
52
+ project: z.string().optional(),
53
+ priority: z.enum(["high", "medium", "low"]).optional(),
54
+ });
55
+
56
+ export const StructuredExperimentOutcomeSchema = z.object({
57
+ verdict: z.enum(["keep", "discard", "modify"]),
58
+ confidence: z.number().min(0).max(1).optional(),
59
+ measurement_window_hours: z.number().nonnegative().optional(),
60
+ patches: z.array(z.unknown()).optional(),
61
+ suggested_action: z.enum(["advance", "revert", "iterate"]).optional(),
62
+ });
63
+
64
+ export const StructuredSynthesisSchema = z.object({
65
+ summary: z.string(),
66
+ decisions: z.array(z.string()),
67
+ actionable_tasks: z.array(StructuredActionableTaskSchema),
68
+ experiment_outcome: StructuredExperimentOutcomeSchema.optional(),
69
+ });
70
+
71
+ export const StructuredExecutionContractSchema = z.object({
72
+ schema_version: z.number().int().positive(),
73
+ source_session_id: z.string().min(1),
74
+ deliberation_id: z.string().min(1),
75
+ summary: z.string(),
76
+ decisions: z.array(z.string()),
77
+ tasks: z.array(StructuredActionableTaskSchema),
78
+ experiment_outcome: StructuredExperimentOutcomeSchema.nullable().optional(),
79
+ unresolved_questions: z.array(z.string()),
80
+ artifact_refs: z.array(z.string()),
81
+ generated_from: z.object({
82
+ structured_synthesis_hash: z.string().length(40),
83
+ }),
84
+ });
85
+
86
+ export const TeleptyEnvelopeSchema = z.object({
87
+ version: z.number().int().positive().optional(),
88
+ message_id: z.string().min(1),
89
+ session_id: z.string().min(1),
90
+ project: z.string().min(1),
91
+ kind: z.string().min(1),
92
+ source: z.string().min(1),
93
+ source_host: z.string().min(1).optional(),
94
+ target: z.string().min(1),
95
+ reply_to: z.string().nullable().optional(),
96
+ trace: z.array(z.string()),
97
+ payload: z.unknown(),
98
+ ts: z.string().min(1),
99
+ });
100
+
101
+ export const TeleptyTurnRequestPayloadSchema = z.object({
102
+ turn_id: z.string().min(1),
103
+ round: z.number().int().positive(),
104
+ max_rounds: z.number().int().positive(),
105
+ speaker: z.string().min(1),
106
+ role: z.string().nullable().optional(),
107
+ prompt: z.string().min(1),
108
+ content: z.string().min(1).describe("PTY-compatible alias for prompt field"),
109
+ prompt_sha1: z.string().length(40),
110
+ history_entries: z.number().int().nonnegative().optional(),
111
+ transport_timeout_ms: z.number().int().positive(),
112
+ semantic_timeout_ms: z.number().int().positive(),
113
+ });
114
+
115
+ export const TeleptyTurnCompletedPayloadSchema = z.object({
116
+ turn_id: z.string().nullable().optional(),
117
+ speaker: z.string().min(1),
118
+ round: z.number().int().positive(),
119
+ max_rounds: z.number().int().positive(),
120
+ next_speaker: z.string().min(1),
121
+ next_round: z.number().int().positive(),
122
+ status: z.string().min(1),
123
+ total_responses: z.number().int().nonnegative(),
124
+ channel_used: z.string().nullable().optional(),
125
+ fallback_reason: z.string().nullable().optional(),
126
+ orchestrator_session_id: z.string().nullable().optional(),
127
+ });
128
+
129
+ export const TeleptyDeliberationCompletedPayloadSchema = z.object({
130
+ topic: z.string(),
131
+ synthesis: z.string(),
132
+ structured_synthesis: StructuredSynthesisSchema.nullable().optional(),
133
+ execution_contract: StructuredExecutionContractSchema.nullable().optional(),
134
+ });
135
+
136
+ export const TELEPTY_ENVELOPE_PAYLOAD_SCHEMAS = {
137
+ turn_request: TeleptyTurnRequestPayloadSchema,
138
+ turn_completed: TeleptyTurnCompletedPayloadSchema,
139
+ deliberation_completed: TeleptyDeliberationCompletedPayloadSchema,
140
+ };
141
+
142
+ // ── Module-level state ──────────────────────────────────────────
143
+
144
+ export const teleptyBusState = {
145
+ ws: null,
146
+ status: "idle",
147
+ connectPromise: null,
148
+ reconnectTimer: null,
149
+ lastError: null,
150
+ lastConnectedAt: null,
151
+ lastMessageAt: null,
152
+ healthBySession: new Map(),
153
+ };
154
+
155
+ export const pendingTeleptyTurnRequests = new Map();
156
+
157
+ // ── Functions ───────────────────────────────────────────────────
158
+
159
+ export function hashPromptText(value) {
160
+ return createHash("sha1").update(String(value || "")).digest("hex");
161
+ }
162
+
163
+ export function sortJsonValue(value) {
164
+ if (Array.isArray(value)) {
165
+ return value.map(sortJsonValue);
166
+ }
167
+ if (value && typeof value === "object") {
168
+ return Object.keys(value)
169
+ .sort()
170
+ .reduce((acc, key) => {
171
+ acc[key] = sortJsonValue(value[key]);
172
+ return acc;
173
+ }, {});
174
+ }
175
+ return value;
176
+ }
177
+
178
+ export function hashStructuredSynthesis(structured) {
179
+ return hashPromptText(JSON.stringify(sortJsonValue(structured || null)));
180
+ }
181
+
182
+ /**
183
+ * Build a deterministic execution contract from structured synthesis.
184
+ *
185
+ * Data model canonical roles:
186
+ * - structured_synthesis: human + reasoning canonical — rich context for
187
+ * human review (decisions rationale, experiment outcomes, full task descriptions).
188
+ * - execution_contract: automation canonical — minimal, deterministic task list
189
+ * derived from structured_synthesis via SHA-1 hash for provenance tracking.
190
+ * Consumers (inbox-watcher, devkit, registry, orchestrator) MUST prefer
191
+ * execution_contract when available; fall back to structured_synthesis only
192
+ * when execution_contract is absent.
193
+ */
194
+ export function buildExecutionContract({ state, structured }) {
195
+ if (!structured) return null;
196
+ return {
197
+ schema_version: 2,
198
+ source_session_id: state.id,
199
+ deliberation_id: state.id,
200
+ summary: structured.summary || "",
201
+ decisions: structured.decisions || [],
202
+ tasks: structured.actionable_tasks || [],
203
+ experiment_outcome: structured.experiment_outcome || null,
204
+ unresolved_questions: [],
205
+ artifact_refs: [],
206
+ generated_from: {
207
+ structured_synthesis_hash: hashStructuredSynthesis(structured),
208
+ },
209
+ };
210
+ }
211
+
212
+ export function createEnvelopeId(prefix = "env") {
213
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
214
+ }
215
+
216
+ export function validateTeleptyEnvelope(envelope) {
217
+ const parsed = TeleptyEnvelopeSchema.parse(envelope);
218
+ const payloadSchema = TELEPTY_ENVELOPE_PAYLOAD_SCHEMAS[parsed.kind];
219
+ if (payloadSchema) {
220
+ payloadSchema.parse(parsed.payload);
221
+ }
222
+ return parsed;
223
+ }
224
+
225
+ export function resolveTeleptySourceHost() {
226
+ const explicit = process.env.TELEPTY_SOURCE_HOST;
227
+ if (typeof explicit === "string" && explicit.trim()) {
228
+ return explicit.trim();
229
+ }
230
+ const hostname = os.hostname();
231
+ return typeof hostname === "string" && hostname.trim() ? hostname.trim() : undefined;
232
+ }
233
+
234
+ export function buildTeleptyEnvelope({ session_id, project, kind, source, source_host = resolveTeleptySourceHost(), target, reply_to = null, trace = [], payload, ts = new Date().toISOString(), message_id = createEnvelopeId(kind) }) {
235
+ return validateTeleptyEnvelope({
236
+ version: 1,
237
+ message_id,
238
+ session_id,
239
+ project,
240
+ kind,
241
+ source,
242
+ source_host,
243
+ target,
244
+ reply_to,
245
+ trace,
246
+ payload,
247
+ ts,
248
+ });
249
+ }
250
+
251
+ export function buildTeleptyTurnRequestEnvelope({ state, speaker, turnId, turnPrompt, includeHistoryEntries = 0, profile }) {
252
+ const role = (state.speaker_roles || {})[speaker] || null;
253
+ const target = profile?.telepty_host && !["127.0.0.1", "localhost"].includes(profile.telepty_host)
254
+ ? `${profile.telepty_session_id}@${profile.telepty_host}`
255
+ : profile?.telepty_session_id || speaker;
256
+ return buildTeleptyEnvelope({
257
+ session_id: state.id,
258
+ project: state.project || _deps.getProjectSlug(),
259
+ kind: "turn_request",
260
+ source: `deliberation:${state.id}`,
261
+ target,
262
+ reply_to: state.id,
263
+ trace: [
264
+ `project:${state.project || _deps.getProjectSlug()}`,
265
+ `speaker:${speaker}`,
266
+ `turn:${turnId}`,
267
+ ],
268
+ payload: {
269
+ turn_id: turnId,
270
+ round: state.current_round,
271
+ max_rounds: state.max_rounds,
272
+ speaker,
273
+ role,
274
+ prompt: turnPrompt,
275
+ content: turnPrompt,
276
+ prompt_sha1: hashPromptText(turnPrompt),
277
+ history_entries: includeHistoryEntries,
278
+ transport_timeout_ms: TELEPTY_TRANSPORT_TIMEOUT_MS,
279
+ semantic_timeout_ms: TELEPTY_SEMANTIC_TIMEOUT_MS,
280
+ },
281
+ });
282
+ }
283
+
284
+ export function buildTeleptyTurnCompletedEnvelope({ state, entry }) {
285
+ return buildTeleptyEnvelope({
286
+ session_id: state.id,
287
+ project: state.project || _deps.getProjectSlug(),
288
+ kind: "turn_completed",
289
+ source: `deliberation:${state.id}`,
290
+ target: "telepty-bus",
291
+ reply_to: state.orchestrator_session_id || state.id,
292
+ trace: [
293
+ `project:${state.project || _deps.getProjectSlug()}`,
294
+ `speaker:${entry.speaker}`,
295
+ `turn:${entry.turn_id || "none"}`,
296
+ ],
297
+ payload: {
298
+ turn_id: entry.turn_id || null,
299
+ speaker: entry.speaker,
300
+ round: entry.round,
301
+ max_rounds: state.max_rounds,
302
+ next_speaker: state.current_speaker || "none",
303
+ next_round: state.current_round,
304
+ status: state.status,
305
+ total_responses: Array.isArray(state.log) ? state.log.length : 0,
306
+ channel_used: entry.channel_used || null,
307
+ fallback_reason: entry.fallback_reason || null,
308
+ orchestrator_session_id: state.orchestrator_session_id || null,
309
+ },
310
+ });
311
+ }
312
+
313
+ export function buildTeleptySynthesisEnvelope({ state, synthesis, structured, executionContract }) {
314
+ const derivedExecutionContract =
315
+ executionContract !== undefined
316
+ ? executionContract
317
+ : (structured ? buildExecutionContract({ state, structured }) : (state.execution_contract || null));
318
+ return buildTeleptyEnvelope({
319
+ session_id: state.id,
320
+ project: state.project || _deps.getProjectSlug(),
321
+ kind: "deliberation_completed",
322
+ source: `deliberation:${state.id}`,
323
+ target: "telepty-bus",
324
+ reply_to: state.id,
325
+ trace: [
326
+ `project:${state.project || _deps.getProjectSlug()}`,
327
+ "stage:synthesis",
328
+ ],
329
+ payload: {
330
+ topic: state.topic,
331
+ synthesis,
332
+ structured_synthesis: structured || null,
333
+ execution_contract: derivedExecutionContract || null,
334
+ },
335
+ });
336
+ }
337
+
338
+ export function resolveTeleptyBusUrl(host = TELEPTY_DEFAULT_HOST) {
339
+ const url = new URL(`ws://${host}:${TELEPTY_PORT}/api/bus`);
340
+ const token = loadTeleptyAuthToken();
341
+ if (token) {
342
+ url.searchParams.set("token", token);
343
+ }
344
+ return url.toString();
345
+ }
346
+
347
+ export function cleanupPendingTeleptyTurn(messageId) {
348
+ const entry = pendingTeleptyTurnRequests.get(messageId);
349
+ if (!entry) return;
350
+ if (entry.transportTimer) clearTimeout(entry.transportTimer);
351
+ if (entry.semanticTimer) clearTimeout(entry.semanticTimer);
352
+ pendingTeleptyTurnRequests.delete(messageId);
353
+ }
354
+
355
+ export function registerPendingTeleptyTurnRequest({ envelope, profile, speaker }) {
356
+ const nowMs = Date.now();
357
+ const entry = {
358
+ message_id: envelope.message_id,
359
+ deliberation_session_id: envelope.session_id,
360
+ project: envelope.project,
361
+ speaker,
362
+ turn_id: envelope.payload.turn_id,
363
+ target_session_id: profile?.telepty_session_id || speaker,
364
+ target_host: profile?.telepty_host || TELEPTY_DEFAULT_HOST,
365
+ prompt_sha1: envelope.payload.prompt_sha1,
366
+ published_at: envelope.ts,
367
+ transport_status: "pending",
368
+ semantic_status: "pending",
369
+ transport_deadline_at: new Date(nowMs + TELEPTY_TRANSPORT_TIMEOUT_MS).toISOString(),
370
+ semantic_deadline_at: new Date(nowMs + TELEPTY_SEMANTIC_TIMEOUT_MS).toISOString(),
371
+ };
372
+ entry.transportPromise = new Promise(resolve => {
373
+ entry.resolveTransport = resolve;
374
+ });
375
+ entry.semanticPromise = new Promise(resolve => {
376
+ entry.resolveSemantic = resolve;
377
+ });
378
+ entry.transportTimer = setTimeout(() => {
379
+ if (entry.transport_status !== "pending") return;
380
+ entry.transport_status = "timeout";
381
+ _deps.appendRuntimeLog("WARN", `TELEPTY_TRANSPORT_TIMEOUT: ${entry.deliberation_session_id} | speaker: ${entry.speaker} | target: ${entry.target_session_id}`);
382
+ entry.resolveTransport?.({ ok: false, code: "transport_timeout" });
383
+ }, TELEPTY_TRANSPORT_TIMEOUT_MS);
384
+ entry.semanticTimer = setTimeout(() => {
385
+ if (entry.semantic_status !== "pending") return;
386
+ entry.semantic_status = "timeout";
387
+ _deps.appendRuntimeLog("WARN", `TELEPTY_SEMANTIC_TIMEOUT: ${entry.deliberation_session_id} | speaker: ${entry.speaker} | target: ${entry.target_session_id}`);
388
+ entry.resolveSemantic?.({ ok: false, code: "semantic_timeout" });
389
+ setTimeout(() => cleanupPendingTeleptyTurn(entry.message_id), 5_000);
390
+ }, TELEPTY_SEMANTIC_TIMEOUT_MS);
391
+ pendingTeleptyTurnRequests.set(entry.message_id, entry);
392
+ return entry;
393
+ }
394
+
395
+ export function ackPendingTeleptyTurn(event) {
396
+ const promptHash = hashPromptText(event?.content || "");
397
+ const targetSessionId = String(event?.target_agent || "");
398
+ const candidate = [...pendingTeleptyTurnRequests.values()]
399
+ .filter(entry =>
400
+ entry.transport_status === "pending"
401
+ && entry.target_session_id === targetSessionId
402
+ && entry.prompt_sha1 === promptHash
403
+ )
404
+ .sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0];
405
+ if (!candidate) return null;
406
+
407
+ candidate.transport_status = "ack";
408
+ candidate.inject_id = event.inject_id || null;
409
+ candidate.transport_acked_at = new Date().toISOString();
410
+ if (candidate.transportTimer) clearTimeout(candidate.transportTimer);
411
+ candidate.resolveTransport?.({
412
+ ok: true,
413
+ code: "inject_written",
414
+ inject_id: event.inject_id || null,
415
+ });
416
+ _deps.appendRuntimeLog("INFO", `TELEPTY_TRANSPORT_ACK: ${candidate.deliberation_session_id} | speaker: ${candidate.speaker} | target: ${candidate.target_session_id} | inject_id: ${event.inject_id || "n/a"}`);
417
+ return candidate;
418
+ }
419
+
420
+ export function completePendingTeleptySemantic({ sessionId, speaker, turnId }) {
421
+ const candidate = [...pendingTeleptyTurnRequests.values()]
422
+ .filter(entry =>
423
+ entry.semantic_status === "pending"
424
+ && entry.deliberation_session_id === sessionId
425
+ && _deps.normalizeSpeaker(entry.speaker) === _deps.normalizeSpeaker(speaker)
426
+ && (!turnId || !entry.turn_id || entry.turn_id === turnId)
427
+ )
428
+ .sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0];
429
+ if (!candidate) return null;
430
+
431
+ candidate.semantic_status = "completed";
432
+ candidate.semantic_completed_at = new Date().toISOString();
433
+ if (candidate.semanticTimer) clearTimeout(candidate.semanticTimer);
434
+ candidate.resolveSemantic?.({ ok: true, code: "responded" });
435
+ _deps.appendRuntimeLog("INFO", `TELEPTY_SEMANTIC_COMPLETE: ${candidate.deliberation_session_id} | speaker: ${candidate.speaker} | target: ${candidate.target_session_id}`);
436
+ setTimeout(() => cleanupPendingTeleptyTurn(candidate.message_id), 5_000);
437
+ return candidate;
438
+ }
439
+
440
+ export function updateTeleptySessionHealth(event) {
441
+ const sessionId = event?.session_id;
442
+ if (!sessionId) return null;
443
+ const health = {
444
+ session_id: sessionId,
445
+ payload: event.payload || {},
446
+ timestamp: event.timestamp || new Date().toISOString(),
447
+ seen_at: new Date().toISOString(),
448
+ };
449
+ teleptyBusState.healthBySession.set(sessionId, health);
450
+ return health;
451
+ }
452
+
453
+ export function getTeleptySessionHealth(sessionId, nowMs = Date.now()) {
454
+ const entry = teleptyBusState.healthBySession.get(sessionId);
455
+ if (!entry) return null;
456
+ const seenAtMs = Date.parse(entry.seen_at || entry.timestamp || "");
457
+ const ageMs = Number.isFinite(seenAtMs) ? nowMs - seenAtMs : null;
458
+ return {
459
+ ...entry,
460
+ age_ms: ageMs,
461
+ stale: Number.isFinite(ageMs) ? ageMs > TELEPTY_SESSION_HEALTH_STALE_MS : true,
462
+ };
463
+ }
464
+
465
+ export function handleTeleptyBusMessage(raw) {
466
+ let parsed = null;
467
+ try {
468
+ parsed = JSON.parse(String(raw));
469
+ } catch {
470
+ return null;
471
+ }
472
+ teleptyBusState.lastMessageAt = new Date().toISOString();
473
+ if (!parsed || typeof parsed !== "object") return null;
474
+
475
+ if (parsed.type === "inject_written") {
476
+ return ackPendingTeleptyTurn(parsed);
477
+ }
478
+ if (parsed.type === "session_health") {
479
+ return updateTeleptySessionHealth(parsed);
480
+ }
481
+ return parsed;
482
+ }
483
+
484
+ export async function ensureTeleptyBusSubscriber() {
485
+ if (teleptyBusState.ws && teleptyBusState.ws.readyState === WebSocket.OPEN) {
486
+ return { ok: true, status: "open" };
487
+ }
488
+ if (teleptyBusState.connectPromise) {
489
+ return teleptyBusState.connectPromise;
490
+ }
491
+
492
+ teleptyBusState.connectPromise = new Promise((resolve) => {
493
+ try {
494
+ let settled = false;
495
+ const finish = (result) => {
496
+ if (settled) return;
497
+ settled = true;
498
+ resolve(result);
499
+ };
500
+ teleptyBusState.status = "connecting";
501
+ const ws = new WebSocket(resolveTeleptyBusUrl());
502
+ teleptyBusState.ws = ws;
503
+
504
+ ws.once("open", () => {
505
+ teleptyBusState.status = "open";
506
+ teleptyBusState.lastConnectedAt = new Date().toISOString();
507
+ teleptyBusState.lastError = null;
508
+ _deps.appendRuntimeLog("INFO", "TELEPTY_BUS_CONNECTED");
509
+ finish({ ok: true, status: "open" });
510
+ });
511
+
512
+ ws.on("message", (data) => {
513
+ handleTeleptyBusMessage(data.toString());
514
+ });
515
+
516
+ ws.on("error", (err) => {
517
+ teleptyBusState.lastError = String(err?.message || err);
518
+ _deps.appendRuntimeLog("WARN", `TELEPTY_BUS_ERROR: ${teleptyBusState.lastError}`);
519
+ if (ws.readyState !== WebSocket.OPEN) {
520
+ teleptyBusState.status = "error";
521
+ teleptyBusState.ws = null;
522
+ teleptyBusState.connectPromise = null;
523
+ finish({ ok: false, status: "error", error: teleptyBusState.lastError });
524
+ }
525
+ });
526
+
527
+ ws.on("close", () => {
528
+ teleptyBusState.status = "closed";
529
+ teleptyBusState.ws = null;
530
+ teleptyBusState.connectPromise = null;
531
+ if (!settled) {
532
+ finish({ ok: false, status: "closed", error: teleptyBusState.lastError || "socket closed" });
533
+ }
534
+ if (!teleptyBusState.reconnectTimer) {
535
+ teleptyBusState.reconnectTimer = setTimeout(() => {
536
+ teleptyBusState.reconnectTimer = null;
537
+ ensureTeleptyBusSubscriber().catch(() => {});
538
+ }, TELEPTY_BUS_RECONNECT_MS);
539
+ }
540
+ });
541
+ } catch (err) {
542
+ teleptyBusState.status = "error";
543
+ teleptyBusState.lastError = String(err?.message || err);
544
+ teleptyBusState.connectPromise = null;
545
+ resolve({ ok: false, status: "error", error: teleptyBusState.lastError });
546
+ }
547
+ });
548
+
549
+ const result = await teleptyBusState.connectPromise;
550
+ if (!result.ok) {
551
+ teleptyBusState.connectPromise = null;
552
+ } else if (teleptyBusState.ws?.readyState === WebSocket.OPEN) {
553
+ teleptyBusState.connectPromise = null;
554
+ }
555
+ return result;
556
+ }
557
+
558
+ export async function callBrainIngest(executionContract) {
559
+ if (!executionContract) return { ok: false, reason: "no_contract" };
560
+ try {
561
+ const inboxDir = path.join(os.homedir(), ".aigentry", "inbox");
562
+ if (!fs.existsSync(inboxDir)) {
563
+ fs.mkdirSync(inboxDir, { recursive: true });
564
+ }
565
+ const fileName = `handoff-${executionContract.deliberation_id}.json`;
566
+ const filePath = path.join(inboxDir, fileName);
567
+ fs.writeFileSync(filePath, JSON.stringify(executionContract, null, 2), "utf8");
568
+ _deps.appendRuntimeLog("INFO", `BRAIN_INGEST: wrote handoff file ${filePath}`);
569
+ return { ok: true, path: filePath };
570
+ } catch (err) {
571
+ _deps.appendRuntimeLog("WARN", `BRAIN_INGEST: failed to write handoff file: ${err.message}`);
572
+ return { ok: false, error: err.message };
573
+ }
574
+ }
575
+
576
+ export async function notifyTeleptyBus(event) {
577
+ const host = process.env.TELEPTY_HOST || "localhost";
578
+ const port = process.env.TELEPTY_PORT || "3848";
579
+ const token = loadTeleptyAuthToken();
580
+ try {
581
+ const res = await fetch(`http://${host}:${port}/api/bus/publish`, {
582
+ method: "POST",
583
+ headers: {
584
+ "Content-Type": "application/json",
585
+ ...(token ? { "x-telepty-token": token } : {}),
586
+ },
587
+ body: JSON.stringify(event),
588
+ });
589
+ const data = await res.json().catch(() => null);
590
+ if (res.ok) {
591
+ _deps.appendRuntimeLog("INFO", `HANDOFF: Telepty bus notified: ${event.kind || event.type || "unknown"}`);
592
+ return { ok: true, delivered: data?.delivered ?? null };
593
+ }
594
+ return { ok: false, status: res.status, error: data?.error || `HTTP ${res.status}` };
595
+ } catch (err) {
596
+ _deps.appendRuntimeLog("WARN", `HANDOFF: Telepty bus notification failed: ${err.message}`);
597
+ return { ok: false, error: err.message };
598
+ }
599
+ }
600
+
601
+ export function getDefaultOrchestratorSessionId() {
602
+ // Check multiple env vars that may indicate an orchestrator context
603
+ const candidates = [
604
+ process.env.TELEPTY_SESSION_ID,
605
+ process.env.DELIBERATION_ORCHESTRATOR_ID,
606
+ process.env.ORCHESTRATOR_SESSION_ID,
607
+ ];
608
+ for (const value of candidates) {
609
+ if (typeof value === "string" && value.trim()) return value.trim();
610
+ }
611
+ return null;
612
+ }
613
+
614
+ export function buildTurnCompletionNotificationText(state, entry) {
615
+ const nextSpeaker = state.current_speaker || "none";
616
+ const turnId = entry.turn_id || "(none)";
617
+ if (state.status === "awaiting_synthesis") {
618
+ return [
619
+ `[deliberation turn complete]`,
620
+ `session_id: ${state.id}`,
621
+ `speaker: ${entry.speaker}`,
622
+ `turn_id: ${turnId}`,
623
+ `round: ${entry.round}/${state.max_rounds}`,
624
+ `status: awaiting_synthesis`,
625
+ `responses: ${state.log.length}`,
626
+ `all rounds complete; run deliberation_synthesize(session_id: "${state.id}")`,
627
+ `no further reply needed.`,
628
+ ].join("\n");
629
+ }
630
+
631
+ return [
632
+ `[deliberation turn complete]`,
633
+ `session_id: ${state.id}`,
634
+ `speaker: ${entry.speaker}`,
635
+ `turn_id: ${turnId}`,
636
+ `round: ${entry.round}/${state.max_rounds}`,
637
+ `status: ${state.status}`,
638
+ `next_speaker: ${nextSpeaker}`,
639
+ `next_round: ${state.current_round}/${state.max_rounds}`,
640
+ `responses: ${state.log.length}`,
641
+ `informational notification only.`,
642
+ `no further reply needed.`,
643
+ ].join("\n");
644
+ }
645
+
646
+ export async function notifyTeleptySessionInject({ targetSessionId, prompt, fromSessionId, replyToSessionId = null, host = TELEPTY_DEFAULT_HOST }) {
647
+ if (!targetSessionId || !prompt) return { ok: false, error: "missing target or prompt" };
648
+ const token = loadTeleptyAuthToken();
649
+ if (!token) return { ok: false, error: "telepty auth token unavailable" };
650
+
651
+ try {
652
+ const response = await fetch(`http://${host}:${TELEPTY_PORT}/api/sessions/${encodeURIComponent(targetSessionId)}/inject`, {
653
+ method: "POST",
654
+ headers: {
655
+ "Content-Type": "application/json",
656
+ "x-telepty-token": token,
657
+ },
658
+ body: JSON.stringify({
659
+ prompt,
660
+ from: fromSessionId || null,
661
+ reply_to: replyToSessionId || null,
662
+ deliberation_session_id: null,
663
+ thread_id: null,
664
+ }),
665
+ });
666
+ const data = await response.json().catch(() => null);
667
+ if (!response.ok) {
668
+ return { ok: false, error: data?.error || `HTTP ${response.status}` };
669
+ }
670
+ return { ok: true, inject_id: data?.inject_id || null };
671
+ } catch (err) {
672
+ return { ok: false, error: String(err?.message || err) };
673
+ }
674
+ }
675
+
676
+ export async function dispatchTeleptyTurnRequest({ state, speaker, prompt = null, includeHistoryEntries = 4, awaitSemantic = false }) {
677
+ const { profile } = _deps.resolveTransportForSpeaker(state, speaker);
678
+ const turnId = state.pending_turn_id || _deps.generateTurnId();
679
+ const turnPrompt = _deps.buildClipboardTurnPrompt(state, speaker, prompt, includeHistoryEntries);
680
+ const busReady = await ensureTeleptyBusSubscriber();
681
+ const envelope = buildTeleptyTurnRequestEnvelope({
682
+ state,
683
+ speaker,
684
+ turnId,
685
+ turnPrompt,
686
+ includeHistoryEntries,
687
+ profile,
688
+ });
689
+ const pending = registerPendingTeleptyTurnRequest({ envelope, profile, speaker });
690
+ const publishResult = await notifyTeleptyBus(envelope);
691
+ const health = profile?.telepty_session_id ? getTeleptySessionHealth(profile.telepty_session_id) : null;
692
+
693
+ // Direct inject into target session (bus broadcast alone does not deliver prompts)
694
+ const targetSessionId = profile?.telepty_session_id || speaker;
695
+ const targetHost = profile?.telepty_host || TELEPTY_DEFAULT_HOST;
696
+ const injectResult = await notifyTeleptySessionInject({
697
+ targetSessionId,
698
+ prompt: turnPrompt,
699
+ fromSessionId: `deliberation:${state.id}`,
700
+ replyToSessionId: state.id,
701
+ host: targetHost,
702
+ });
703
+
704
+ if (!injectResult.ok && !publishResult.ok) {
705
+ cleanupPendingTeleptyTurn(envelope.message_id);
706
+ return {
707
+ ok: false,
708
+ stage: "publish",
709
+ envelope,
710
+ turnPrompt,
711
+ publishResult,
712
+ injectResult,
713
+ busReady,
714
+ health,
715
+ };
716
+ }
717
+
718
+ // If direct inject succeeded, resolve transport immediately
719
+ if (injectResult.ok) {
720
+ pending.transport_status = "ack";
721
+ pending.inject_id = injectResult.inject_id || null;
722
+ pending.transport_acked_at = new Date().toISOString();
723
+ if (pending.transportTimer) clearTimeout(pending.transportTimer);
724
+ pending.resolveTransport?.({
725
+ ok: true,
726
+ code: "inject_written",
727
+ inject_id: injectResult.inject_id || null,
728
+ });
729
+ _deps.appendRuntimeLog("INFO", `TELEPTY_DIRECT_INJECT: ${state.id} | speaker: ${speaker} | target: ${targetSessionId}`);
730
+ }
731
+
732
+ const transportResult = await pending.transportPromise;
733
+ let semanticResult = null;
734
+ if (awaitSemantic && transportResult.ok) {
735
+ semanticResult = await pending.semanticPromise;
736
+ }
737
+
738
+ return {
739
+ ok: !awaitSemantic ? transportResult.ok : Boolean(semanticResult?.ok),
740
+ stage: awaitSemantic ? (semanticResult?.ok ? "semantic" : (semanticResult?.code || "semantic_timeout")) : (transportResult.ok ? "transport" : (transportResult?.code || "transport_timeout")),
741
+ envelope,
742
+ turnPrompt,
743
+ publishResult,
744
+ transportResult,
745
+ semanticResult,
746
+ busReady,
747
+ health,
748
+ };
749
+ }
750
+
751
+ export function loadTeleptyAuthToken() {
752
+ try {
753
+ const raw = fs.readFileSync(TELEPTY_CONFIG_FILE, "utf-8");
754
+ const parsed = JSON.parse(raw);
755
+ return typeof parsed?.authToken === "string" && parsed.authToken.trim()
756
+ ? parsed.authToken.trim()
757
+ : null;
758
+ } catch {
759
+ return null;
760
+ }
761
+ }
762
+
763
+ export function formatTeleptyHostLabel(host) {
764
+ return !host || host === "127.0.0.1" || host === "localhost" ? "Local" : host;
765
+ }
766
+
767
+ export async function collectTeleptySessions() {
768
+ const token = loadTeleptyAuthToken();
769
+ if (!token) {
770
+ return { sessions: [], note: "telepty auth token not found." };
771
+ }
772
+
773
+ const host = TELEPTY_DEFAULT_HOST;
774
+ try {
775
+ const res = await fetch(`http://${host}:${TELEPTY_PORT}/api/sessions`, {
776
+ headers: { "x-telepty-token": token },
777
+ signal: AbortSignal.timeout(1500),
778
+ });
779
+ if (!res.ok) {
780
+ return { sessions: [], note: `telepty daemon unavailable (${res.status}).` };
781
+ }
782
+ const sessions = await res.json();
783
+ if (!Array.isArray(sessions)) {
784
+ return { sessions: [], note: "telepty session response format was invalid." };
785
+ }
786
+ ensureTeleptyBusSubscriber().catch(() => {});
787
+ return {
788
+ sessions: sessions.map(session => ({ host, ...session })),
789
+ note: null,
790
+ };
791
+ } catch {
792
+ return { sessions: [], note: null };
793
+ }
794
+ }
795
+
796
+ export function scoreTeleptyProcessMatch(session, baseCommand = "", fullCommand = "") {
797
+ const base = String(baseCommand || "").toLowerCase();
798
+ const full = String(fullCommand || "").toLowerCase();
799
+ const wanted = String(session?.command || "").trim().toLowerCase();
800
+ let score = 0;
801
+
802
+ if (wanted && (base === wanted || full.startsWith(`${wanted} `) || full.includes(` ${wanted} `))) {
803
+ score += 10;
804
+ }
805
+ if (base === "node" || base === "telepty") {
806
+ score -= 2;
807
+ }
808
+ if (full.includes("mcp-deliberation") || full.includes("oh-my-claudecode") || full.includes("bridge/mcp-server")) {
809
+ score -= 3;
810
+ }
811
+ return score;
812
+ }
813
+
814
+ export function collectTeleptyProcessLocators(sessions = []) {
815
+ const wantedSessions = new Map(
816
+ sessions
817
+ .filter(session => session?.id)
818
+ .map(session => [String(session.id), session])
819
+ );
820
+ if (wantedSessions.size === 0) {
821
+ return new Map();
822
+ }
823
+
824
+ try {
825
+ const env = {
826
+ HOME: process.env.HOME,
827
+ PATH: process.env.PATH,
828
+ SHELL: process.env.SHELL,
829
+ USER: process.env.USER,
830
+ LOGNAME: process.env.LOGNAME,
831
+ TERM: process.env.TERM,
832
+ };
833
+ const raw = execFileSync("ps", ["eww", "-axo", "pid=,tty=,comm=,command="], {
834
+ encoding: "utf-8",
835
+ windowsHide: true,
836
+ timeout: 2500,
837
+ maxBuffer: 8 * 1024 * 1024,
838
+ env,
839
+ });
840
+
841
+ const best = new Map();
842
+ for (const line of String(raw).split("\n")) {
843
+ if (!line.includes("TELEPTY_SESSION_ID=")) continue;
844
+ const match = line.match(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/);
845
+ if (!match) continue;
846
+ const [, pid, tty, comm, command] = match;
847
+ const sessionIdMatch = command.match(/(?:^|\s)TELEPTY_SESSION_ID=([^\s]+)/);
848
+ const sessionId = sessionIdMatch?.[1];
849
+ if (!sessionId || !wantedSessions.has(sessionId)) continue;
850
+
851
+ const session = wantedSessions.get(sessionId);
852
+ const score = scoreTeleptyProcessMatch(session, comm, command);
853
+ const current = best.get(sessionId);
854
+ if (!current || score > current.score) {
855
+ best.set(sessionId, { pid: Number(pid), tty, score });
856
+ }
857
+ }
858
+
859
+ return new Map(
860
+ [...best.entries()].map(([sessionId, value]) => [
861
+ sessionId,
862
+ { pid: Number.isFinite(value.pid) ? value.pid : null, tty: value.tty || null },
863
+ ])
864
+ );
865
+ } catch {
866
+ return new Map();
867
+ }
868
+ }