@dmsdc-ai/aigentry-deliberation 0.0.40 → 0.0.42
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/index.js +113 -3
- package/lib/session.js +31 -3
- package/lib/telepty.js +45 -2
- package/lib/transport.js +62 -7
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -154,6 +154,8 @@ import {
|
|
|
154
154
|
} from "./lib/speaker-discovery.js";
|
|
155
155
|
import {
|
|
156
156
|
initSessionDeps,
|
|
157
|
+
DEFAULT_SESSION_TTL_MS,
|
|
158
|
+
isSessionExpired,
|
|
157
159
|
generateSessionId,
|
|
158
160
|
generateTurnId,
|
|
159
161
|
detectContextDirs,
|
|
@@ -354,6 +356,34 @@ function getSessionFile(sessionRef, projectSlug) {
|
|
|
354
356
|
return path.join(getSessionsDir(getSessionProject(sessionRef, projectSlug)), `${sessionId}.json`);
|
|
355
357
|
}
|
|
356
358
|
|
|
359
|
+
function getExecutionStatusFile(sessionId, projectSlug) {
|
|
360
|
+
return path.join(getProjectStateDir(projectSlug || getProjectSlug()), `exec-status-${sessionId}.json`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function loadExecutionStatus(sessionId, projectSlug) {
|
|
364
|
+
// Search across all projects if projectSlug not given
|
|
365
|
+
const projects = projectSlug
|
|
366
|
+
? [normalizeProjectSlug(projectSlug)]
|
|
367
|
+
: [getProjectSlug(), ...listStateProjects()];
|
|
368
|
+
for (const p of [...new Set(projects)]) {
|
|
369
|
+
const file = getExecutionStatusFile(sessionId, p);
|
|
370
|
+
try {
|
|
371
|
+
const data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
372
|
+
if (data && data.session_id === sessionId) return data;
|
|
373
|
+
} catch { /* not found */ }
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function saveExecutionStatus(sessionId, projectSlug, patch) {
|
|
379
|
+
const file = getExecutionStatusFile(sessionId, normalizeProjectSlug(projectSlug || getProjectSlug()));
|
|
380
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
381
|
+
const existing = (() => { try { return JSON.parse(fs.readFileSync(file, "utf-8")); } catch { return {}; } })();
|
|
382
|
+
const updated = { ...existing, ...patch, session_id: sessionId, updated_at: new Date().toISOString() };
|
|
383
|
+
fs.writeFileSync(file, JSON.stringify(updated, null, 2));
|
|
384
|
+
return updated;
|
|
385
|
+
}
|
|
386
|
+
|
|
357
387
|
function listStateProjects() {
|
|
358
388
|
if (!fs.existsSync(GLOBAL_STATE_DIR)) return [];
|
|
359
389
|
try {
|
|
@@ -690,11 +720,17 @@ server.tool(
|
|
|
690
720
|
(v) => (typeof v === "string" ? v === "true" : v),
|
|
691
721
|
z.boolean().optional()
|
|
692
722
|
).describe("If true, automatically create a handoff task in the inbox when synthesis completes. Enables the Autonomous Deliberation Handoff pattern."),
|
|
723
|
+
auto_synthesize: z.preprocess(
|
|
724
|
+
(v) => (typeof v === "string" ? v === "true" : v),
|
|
725
|
+
z.boolean().optional()
|
|
726
|
+
).describe("If true, automatically generate synthesis when all rounds complete. Lighter than auto_execute (no handoff)."),
|
|
693
727
|
mode: z.enum(["standard", "lite"]).default("standard").describe("Deliberation mode. 'lite' caps speakers to 3 and rounds to 2 for quick decisions."),
|
|
728
|
+
session_ttl_ms: z.number().int().min(60000).max(86400000).optional()
|
|
729
|
+
.describe("Session TTL in milliseconds. Sessions expire after this duration. Default: 7200000 (2 hours). Max: 86400000 (24 hours)."),
|
|
694
730
|
orchestrator_session_id: z.string().trim().min(1).max(128).optional()
|
|
695
731
|
.describe("Optional telepty session ID to notify on turn completion. Defaults to TELEPTY_SESSION_ID when available."),
|
|
696
732
|
},
|
|
697
|
-
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, mode, orchestrator_session_id }) => {
|
|
733
|
+
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, auto_synthesize, mode, session_ttl_ms, orchestrator_session_id }) => {
|
|
698
734
|
// ── First-time onboarding guard ──
|
|
699
735
|
const config = loadDeliberationConfig();
|
|
700
736
|
if (!config.setup_complete) {
|
|
@@ -884,7 +920,9 @@ server.tool(
|
|
|
884
920
|
speaker_roles: speaker_roles || (role_preset ? applyRolePreset(role_preset, speakerOrder) : {}),
|
|
885
921
|
degradation: degradationLevels,
|
|
886
922
|
auto_execute: auto_execute || false,
|
|
923
|
+
auto_synthesize: auto_synthesize || auto_execute || false,
|
|
887
924
|
mode: mode || "standard",
|
|
925
|
+
session_ttl_ms: session_ttl_ms || DEFAULT_SESSION_TTL_MS,
|
|
888
926
|
orchestrator_session_id: orchestrator_session_id || getDefaultOrchestratorSessionId() || null,
|
|
889
927
|
created: new Date().toISOString(),
|
|
890
928
|
updated: new Date().toISOString(),
|
|
@@ -955,7 +993,9 @@ server.tool(
|
|
|
955
993
|
appendRuntimeLog("INFO", `SESSION_CREATED: ${sessionId} | topic: ${topic.slice(0, 60)} | speakers: ${speakerOrder.join(",")} | rounds: ${rounds}`);
|
|
956
994
|
|
|
957
995
|
// Auto-handoff: kick off background orchestration
|
|
958
|
-
|
|
996
|
+
// auto_execute = full handoff (turns + synthesis + bus notification)
|
|
997
|
+
// auto_synthesize = lighter (turns + synthesis only, no bus notification)
|
|
998
|
+
if (auto_execute || auto_synthesize) {
|
|
959
999
|
// Fire-and-forget — runs in background
|
|
960
1000
|
runAutoHandoff(sessionId).catch(err => {
|
|
961
1001
|
appendRuntimeLog("ERROR", `AUTO_HANDOFF_SPAWN_ERROR: ${sessionId} | ${err.message}`);
|
|
@@ -1074,10 +1114,27 @@ server.tool(
|
|
|
1074
1114
|
return { content: [{ type: "text", text: t(`Session "${resolved}" not found.`, `세션 "${resolved}"을 찾을 수 없습니다.`, "en") }] };
|
|
1075
1115
|
}
|
|
1076
1116
|
|
|
1117
|
+
if (isSessionExpired(state)) {
|
|
1118
|
+
state.status = "expired";
|
|
1119
|
+
state.current_speaker = "none";
|
|
1120
|
+
state.expired_reason = "ttl_exceeded";
|
|
1121
|
+
saveSession(state);
|
|
1122
|
+
return {
|
|
1123
|
+
content: [{
|
|
1124
|
+
type: "text",
|
|
1125
|
+
text: `⏰ Session "${resolved}" has expired (TTL exceeded).\nTopic: ${state.topic}\nCreated: ${state.created}\n\nUse \`deliberation_reset(session_id: "${resolved}")\` to clean up, or start a new deliberation.`,
|
|
1126
|
+
}],
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const execStatus = loadExecutionStatus(state.id, state.project);
|
|
1131
|
+
const execLine = execStatus
|
|
1132
|
+
? `\n**Execution status:** ${execStatus.execution_status}${execStatus.tasks_total > 0 ? ` (${execStatus.tasks_done}/${execStatus.tasks_total} tasks)` : ""}${execStatus.note ? ` — ${execStatus.note}` : ""}`
|
|
1133
|
+
: "";
|
|
1077
1134
|
return {
|
|
1078
1135
|
content: [{
|
|
1079
1136
|
type: "text",
|
|
1080
|
-
text: `📋 **Forum Status** — ${state.id}\n\n**Project:** ${state.project}\n**Topic:** ${state.topic}\n**Status:** ${state.status === "active" ? "active" : state.status === "awaiting_synthesis" ? "awaiting synthesis" : state.status === "completed" ? "completed" : state.status} (Round ${state.current_round}/${state.max_rounds})\n**Participants:** ${state.speakers.join(", ")}\n**Current turn:** ${state.current_speaker}\n**Accumulated responses:** ${state.log.length}${state.degradation ? `\n\n**Environment status:**\n${formatDegradationReport(state.degradation)}` : ""}`,
|
|
1137
|
+
text: `📋 **Forum Status** — ${state.id}\n\n**Project:** ${state.project}\n**Topic:** ${state.topic}\n**Status:** ${state.status === "active" ? "active" : state.status === "awaiting_synthesis" ? "awaiting synthesis" : state.status === "completed" ? "completed" : state.status} (Round ${state.current_round}/${state.max_rounds})${execLine}\n**Participants:** ${state.speakers.join(", ")}\n**Current turn:** ${state.current_speaker}\n**Accumulated responses:** ${state.log.length}${state.degradation ? `\n\n**Environment status:**\n${formatDegradationReport(state.degradation)}` : ""}`,
|
|
1081
1138
|
}],
|
|
1082
1139
|
};
|
|
1083
1140
|
}
|
|
@@ -2040,6 +2097,14 @@ server.tool(
|
|
|
2040
2097
|
saveSession(loaded);
|
|
2041
2098
|
archivePath = archiveState(loaded);
|
|
2042
2099
|
cleanupSyncMarkdown(loaded);
|
|
2100
|
+
// Write execution_status sidecar (persists after session file is deleted)
|
|
2101
|
+
saveExecutionStatus(loaded.id, loaded.project, {
|
|
2102
|
+
execution_status: loaded.auto_execute ? "executing" : "pending",
|
|
2103
|
+
tasks_total: loaded.execution_contract?.tasks?.length ?? 0,
|
|
2104
|
+
tasks_done: 0,
|
|
2105
|
+
project: loaded.project,
|
|
2106
|
+
topic: loaded.topic,
|
|
2107
|
+
});
|
|
2043
2108
|
|
|
2044
2109
|
// Clean up the active session JSON file upon completion
|
|
2045
2110
|
const sessionFile = getSessionFile(loaded);
|
|
@@ -2094,6 +2159,51 @@ server.tool(
|
|
|
2094
2159
|
})
|
|
2095
2160
|
);
|
|
2096
2161
|
|
|
2162
|
+
server.tool(
|
|
2163
|
+
"deliberation_set_execution_status",
|
|
2164
|
+
"Update the execution status of a completed deliberation's handoff. Used by executor agents to report implementation progress.",
|
|
2165
|
+
{
|
|
2166
|
+
session_id: z.string().min(1).describe("Session ID of the completed deliberation"),
|
|
2167
|
+
status: z.enum(["pending", "executing", "implemented", "failed"]).describe("New execution status"),
|
|
2168
|
+
tasks_done: z.number().int().min(0).optional().describe("Number of tasks completed so far"),
|
|
2169
|
+
tasks_total: z.number().int().min(0).optional().describe("Total number of tasks (if known)"),
|
|
2170
|
+
note: z.string().max(200).optional().describe("Short progress note (max 200 chars)"),
|
|
2171
|
+
project: z.string().optional().describe("Project slug (auto-detected if omitted)"),
|
|
2172
|
+
},
|
|
2173
|
+
safeToolHandler("deliberation_set_execution_status", async ({ session_id, status, tasks_done, tasks_total, note, project }) => {
|
|
2174
|
+
const patch = { execution_status: status };
|
|
2175
|
+
if (tasks_done !== undefined) patch.tasks_done = tasks_done;
|
|
2176
|
+
if (tasks_total !== undefined) patch.tasks_total = tasks_total;
|
|
2177
|
+
if (note !== undefined) patch.note = note;
|
|
2178
|
+
|
|
2179
|
+
const saved = saveExecutionStatus(session_id, project, patch);
|
|
2180
|
+
|
|
2181
|
+
// Notify telepty bus so other MCP processes can react
|
|
2182
|
+
const statusEvent = {
|
|
2183
|
+
kind: "execution_status_update",
|
|
2184
|
+
session_id,
|
|
2185
|
+
project: project || getProjectSlug(),
|
|
2186
|
+
execution_status: status,
|
|
2187
|
+
tasks_done: saved.tasks_done ?? 0,
|
|
2188
|
+
tasks_total: saved.tasks_total ?? 0,
|
|
2189
|
+
note: note || null,
|
|
2190
|
+
timestamp: new Date().toISOString(),
|
|
2191
|
+
};
|
|
2192
|
+
notifyTeleptyBus(statusEvent).catch(() => {});
|
|
2193
|
+
appendRuntimeLog("INFO", `EXECUTION_STATUS: ${session_id} | status: ${status} | tasks: ${saved.tasks_done ?? 0}/${saved.tasks_total ?? 0}`);
|
|
2194
|
+
|
|
2195
|
+
const taskLine = (saved.tasks_total ?? 0) > 0
|
|
2196
|
+
? ` (${saved.tasks_done ?? 0}/${saved.tasks_total} tasks)`
|
|
2197
|
+
: "";
|
|
2198
|
+
return {
|
|
2199
|
+
content: [{
|
|
2200
|
+
type: "text",
|
|
2201
|
+
text: `✅ Execution status updated\n\n**Session:** ${session_id}\n**Status:** ${status}${taskLine}${note ? `\n**Note:** ${note}` : ""}`,
|
|
2202
|
+
}],
|
|
2203
|
+
};
|
|
2204
|
+
})
|
|
2205
|
+
);
|
|
2206
|
+
|
|
2097
2207
|
server.tool(
|
|
2098
2208
|
"deliberation_list",
|
|
2099
2209
|
"Return the list of past deliberation archives.",
|
package/lib/session.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
notifyTeleptyBus,
|
|
17
17
|
notifyTeleptySessionInject,
|
|
18
18
|
buildTeleptyTurnCompletedEnvelope,
|
|
19
|
+
buildTeleptyTurnRespondedEnvelope,
|
|
19
20
|
getDefaultOrchestratorSessionId,
|
|
20
21
|
buildTurnCompletionNotificationText,
|
|
21
22
|
} from "./telepty.js";
|
|
@@ -56,6 +57,19 @@ export function initSessionDeps(deps) {
|
|
|
56
57
|
Object.assign(_deps, deps);
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
// ── Session TTL ──────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export const DEFAULT_SESSION_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
63
|
+
|
|
64
|
+
export function isSessionExpired(state) {
|
|
65
|
+
if (!state || !state.created) return false;
|
|
66
|
+
// Only expire sessions that explicitly opted in to TTL
|
|
67
|
+
if (!state.session_ttl_ms) return false;
|
|
68
|
+
const createdAt = new Date(state.created).getTime();
|
|
69
|
+
if (isNaN(createdAt)) return false;
|
|
70
|
+
return Date.now() - createdAt > state.session_ttl_ms;
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
// ── Session ID generation ─────────────────────────────────────
|
|
60
74
|
|
|
61
75
|
export function generateSessionId(topic) {
|
|
@@ -157,6 +171,9 @@ export function findSessionRecord(sessionRef, { preferProject, activeOnly = fals
|
|
|
157
171
|
if (activeOnly && normalized.status !== "active" && normalized.status !== "awaiting_synthesis") {
|
|
158
172
|
return null;
|
|
159
173
|
}
|
|
174
|
+
if (activeOnly && isSessionExpired(normalized)) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
160
177
|
return { file, project, state: normalized };
|
|
161
178
|
}
|
|
162
179
|
|
|
@@ -171,6 +188,9 @@ export function findSessionRecord(sessionRef, { preferProject, activeOnly = fals
|
|
|
171
188
|
if (activeOnly && normalized.status !== "active" && normalized.status !== "awaiting_synthesis") {
|
|
172
189
|
continue;
|
|
173
190
|
}
|
|
191
|
+
if (activeOnly && isSessionExpired(normalized)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
174
194
|
return { file, project: normalized.project || project, state: normalized };
|
|
175
195
|
}
|
|
176
196
|
return null;
|
|
@@ -216,15 +236,16 @@ export function listActiveSessions(projectSlug) {
|
|
|
216
236
|
return null;
|
|
217
237
|
}
|
|
218
238
|
})
|
|
219
|
-
.filter(s => s && (s.status === "active" || s.status === "awaiting_synthesis"));
|
|
239
|
+
.filter(s => s && (s.status === "active" || s.status === "awaiting_synthesis") && !isSessionExpired(s));
|
|
220
240
|
});
|
|
221
241
|
}
|
|
222
242
|
|
|
223
243
|
export function resolveSessionId(sessionId) {
|
|
224
|
-
// Use session_id directly if provided
|
|
244
|
+
// Use session_id directly if provided — do not load or expire here;
|
|
245
|
+
// individual tool handlers check expiry when needed.
|
|
225
246
|
if (sessionId) return sessionId;
|
|
226
247
|
|
|
227
|
-
// Auto-select when only one active session
|
|
248
|
+
// Auto-select when only one active session (listActiveSessions now checks TTL)
|
|
228
249
|
const active = listActiveSessions();
|
|
229
250
|
if (active.length === 0) return null;
|
|
230
251
|
if (active.length === 1) return active[0].id;
|
|
@@ -556,6 +577,13 @@ export function submitDeliberationTurn({ session_id, speaker, content, turn_id,
|
|
|
556
577
|
speaker: normalizedSpeaker,
|
|
557
578
|
turnId: state.pending_turn_id || turn_id || null,
|
|
558
579
|
});
|
|
580
|
+
// Cross-process semantic completion: notify other MCP processes via bus
|
|
581
|
+
const turnRespondedEnvelope = buildTeleptyTurnRespondedEnvelope({
|
|
582
|
+
state,
|
|
583
|
+
speaker: normalizedSpeaker,
|
|
584
|
+
turnId: state.pending_turn_id || turn_id || null,
|
|
585
|
+
});
|
|
586
|
+
notifyTeleptyBus(turnRespondedEnvelope).catch(() => {});
|
|
559
587
|
_deps.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}${source_metadata?.source_machine_id ? ` | source_machine: ${source_metadata.source_machine_id}` : ""}`);
|
|
560
588
|
|
|
561
589
|
state.current_speaker = selectNextSpeaker(state);
|
package/lib/telepty.js
CHANGED
|
@@ -126,6 +126,14 @@ export const TeleptyTurnCompletedPayloadSchema = z.object({
|
|
|
126
126
|
orchestrator_session_id: z.string().nullable().optional(),
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
export const TeleptyTurnRespondedPayloadSchema = z.object({
|
|
130
|
+
session_id: z.string().min(1),
|
|
131
|
+
speaker: z.string().min(1),
|
|
132
|
+
turn_id: z.string().nullable().optional(),
|
|
133
|
+
round: z.number().int().positive(),
|
|
134
|
+
timestamp: z.string().min(1),
|
|
135
|
+
});
|
|
136
|
+
|
|
129
137
|
export const TeleptyDeliberationCompletedPayloadSchema = z.object({
|
|
130
138
|
topic: z.string(),
|
|
131
139
|
synthesis: z.string(),
|
|
@@ -136,6 +144,7 @@ export const TeleptyDeliberationCompletedPayloadSchema = z.object({
|
|
|
136
144
|
export const TELEPTY_ENVELOPE_PAYLOAD_SCHEMAS = {
|
|
137
145
|
turn_request: TeleptyTurnRequestPayloadSchema,
|
|
138
146
|
turn_completed: TeleptyTurnCompletedPayloadSchema,
|
|
147
|
+
turn_responded: TeleptyTurnRespondedPayloadSchema,
|
|
139
148
|
deliberation_completed: TeleptyDeliberationCompletedPayloadSchema,
|
|
140
149
|
};
|
|
141
150
|
|
|
@@ -310,6 +319,29 @@ export function buildTeleptyTurnCompletedEnvelope({ state, entry }) {
|
|
|
310
319
|
});
|
|
311
320
|
}
|
|
312
321
|
|
|
322
|
+
export function buildTeleptyTurnRespondedEnvelope({ state, speaker, turnId }) {
|
|
323
|
+
return buildTeleptyEnvelope({
|
|
324
|
+
session_id: state.id,
|
|
325
|
+
project: state.project || _deps.getProjectSlug(),
|
|
326
|
+
kind: "turn_responded",
|
|
327
|
+
source: `deliberation:${state.id}`,
|
|
328
|
+
target: "telepty-bus",
|
|
329
|
+
reply_to: state.id,
|
|
330
|
+
trace: [
|
|
331
|
+
`project:${state.project || _deps.getProjectSlug()}`,
|
|
332
|
+
`speaker:${speaker}`,
|
|
333
|
+
`turn:${turnId || "none"}`,
|
|
334
|
+
],
|
|
335
|
+
payload: {
|
|
336
|
+
session_id: state.id,
|
|
337
|
+
speaker,
|
|
338
|
+
turn_id: turnId || null,
|
|
339
|
+
round: state.current_round,
|
|
340
|
+
timestamp: new Date().toISOString(),
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
313
345
|
export function buildTeleptySynthesisEnvelope({ state, synthesis, structured, executionContract }) {
|
|
314
346
|
const derivedExecutionContract =
|
|
315
347
|
executionContract !== undefined
|
|
@@ -478,6 +510,14 @@ export function handleTeleptyBusMessage(raw) {
|
|
|
478
510
|
if (parsed.type === "session_health") {
|
|
479
511
|
return updateTeleptySessionHealth(parsed);
|
|
480
512
|
}
|
|
513
|
+
if (parsed.kind === "turn_responded" && parsed.payload) {
|
|
514
|
+
completePendingTeleptySemantic({
|
|
515
|
+
sessionId: parsed.payload.session_id,
|
|
516
|
+
speaker: parsed.payload.speaker,
|
|
517
|
+
turnId: parsed.payload.turn_id || null,
|
|
518
|
+
});
|
|
519
|
+
return parsed;
|
|
520
|
+
}
|
|
481
521
|
return parsed;
|
|
482
522
|
}
|
|
483
523
|
|
|
@@ -643,7 +683,7 @@ export function buildTurnCompletionNotificationText(state, entry) {
|
|
|
643
683
|
].join("\n");
|
|
644
684
|
}
|
|
645
685
|
|
|
646
|
-
export async function notifyTeleptySessionInject({ targetSessionId, prompt, fromSessionId, replyToSessionId = null, host = TELEPTY_DEFAULT_HOST }) {
|
|
686
|
+
export async function notifyTeleptySessionInject({ targetSessionId, prompt, fromSessionId, replyToSessionId = null, deliberationSessionId = null, turnId = null, host = TELEPTY_DEFAULT_HOST }) {
|
|
647
687
|
if (!targetSessionId || !prompt) return { ok: false, error: "missing target or prompt" };
|
|
648
688
|
const token = loadTeleptyAuthToken();
|
|
649
689
|
if (!token) return { ok: false, error: "telepty auth token unavailable" };
|
|
@@ -659,7 +699,8 @@ export async function notifyTeleptySessionInject({ targetSessionId, prompt, from
|
|
|
659
699
|
prompt,
|
|
660
700
|
from: fromSessionId || null,
|
|
661
701
|
reply_to: replyToSessionId || null,
|
|
662
|
-
deliberation_session_id: null,
|
|
702
|
+
deliberation_session_id: deliberationSessionId || null,
|
|
703
|
+
turn_id: turnId || null,
|
|
663
704
|
thread_id: null,
|
|
664
705
|
}),
|
|
665
706
|
});
|
|
@@ -698,6 +739,8 @@ export async function dispatchTeleptyTurnRequest({ state, speaker, prompt = null
|
|
|
698
739
|
prompt: turnPrompt,
|
|
699
740
|
fromSessionId: `deliberation:${state.id}`,
|
|
700
741
|
replyToSessionId: state.id,
|
|
742
|
+
deliberationSessionId: state.id,
|
|
743
|
+
turnId,
|
|
701
744
|
host: targetHost,
|
|
702
745
|
});
|
|
703
746
|
|
package/lib/transport.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
dispatchTeleptyTurnRequest,
|
|
18
18
|
buildTeleptySynthesisEnvelope,
|
|
19
19
|
notifyTeleptyBus,
|
|
20
|
+
notifyTeleptySessionInject,
|
|
20
21
|
callBrainIngest,
|
|
21
22
|
buildExecutionContract,
|
|
22
23
|
ensureTeleptyBusSubscriber,
|
|
@@ -932,6 +933,13 @@ export async function runUntilBlockedCore(sessionId, {
|
|
|
932
933
|
const { transport } = resolveTransportForSpeaker(state, speaker);
|
|
933
934
|
const callerSpeaker = detectCallerSpeaker();
|
|
934
935
|
if (transport === "cli_respond" && callerSpeaker && normalizeSpeaker(callerSpeaker) === normalizeSpeaker(speaker)) {
|
|
936
|
+
// Count how many remaining speakers can be auto-dispatched after the orchestrator responds
|
|
937
|
+
const remainingAutoSpeakers = (state.speakers || []).filter(s => {
|
|
938
|
+
if (normalizeSpeaker(s) === normalizeSpeaker(callerSpeaker)) return false;
|
|
939
|
+
const { transport: t } = resolveTransportForSpeaker(state, s);
|
|
940
|
+
return t === "cli_respond" || t === "browser_auto" || t === "telepty_bus";
|
|
941
|
+
}).length;
|
|
942
|
+
|
|
935
943
|
return {
|
|
936
944
|
ok: true,
|
|
937
945
|
status: "blocked",
|
|
@@ -939,6 +947,10 @@ export async function runUntilBlockedCore(sessionId, {
|
|
|
939
947
|
speaker,
|
|
940
948
|
transport,
|
|
941
949
|
turn_prompt: buildClipboardTurnPrompt(state, speaker, null, includeHistoryEntries),
|
|
950
|
+
remaining_auto_speakers: remainingAutoSpeakers,
|
|
951
|
+
hint: remainingAutoSpeakers > 0
|
|
952
|
+
? "Respond with deliberation_respond, then call run_until_blocked again to auto-progress remaining speakers."
|
|
953
|
+
: undefined,
|
|
942
954
|
steps,
|
|
943
955
|
};
|
|
944
956
|
}
|
|
@@ -1108,11 +1120,13 @@ Respond with EXACTLY this JSON structure:
|
|
|
1108
1120
|
|
|
1109
1121
|
/**
|
|
1110
1122
|
* Orchestrate full auto-handoff: run all turns -> synthesize -> inbox -> telepty.
|
|
1111
|
-
* Called as fire-and-forget from deliberation_start when auto_execute is true.
|
|
1123
|
+
* Called as fire-and-forget from deliberation_start when auto_execute or auto_synthesize is true.
|
|
1112
1124
|
*/
|
|
1113
1125
|
export async function runAutoHandoff(sessionId) {
|
|
1114
1126
|
_deps.appendRuntimeLog("INFO", `AUTO_HANDOFF_START: ${sessionId}`);
|
|
1115
1127
|
|
|
1128
|
+
const retryConfig = { maxRetries: 2, retryDelayMs: 10000 };
|
|
1129
|
+
|
|
1116
1130
|
try {
|
|
1117
1131
|
// Phase 1: Run all deliberation turns
|
|
1118
1132
|
let maxIterations = 100; // safety limit
|
|
@@ -1132,14 +1146,41 @@ export async function runAutoHandoff(sessionId) {
|
|
|
1132
1146
|
|
|
1133
1147
|
_deps.appendRuntimeLog("INFO", `AUTO_HANDOFF_TURN: ${sessionId} | speaker: ${speaker} | round: ${state.current_round}/${state.max_rounds}`);
|
|
1134
1148
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1149
|
+
let turnSucceeded = false;
|
|
1150
|
+
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
|
|
1151
|
+
const runResult = await runUntilBlockedCore(sessionId, { maxTurns: 1, includeHistoryEntries: 3 });
|
|
1152
|
+
const step = runResult.steps.at(-1) || null;
|
|
1153
|
+
|
|
1154
|
+
if (runResult.ok && runResult.status !== "blocked") {
|
|
1155
|
+
_deps.appendRuntimeLog("INFO", `AUTO_HANDOFF_TURN_OK: ${sessionId} | speaker: ${speaker} | ${step?.elapsedMs || 0}ms`);
|
|
1156
|
+
turnSucceeded = true;
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// self_turn blocks should break immediately — the orchestrator itself is the speaker
|
|
1161
|
+
if (runResult.block_reason === "self_turn") {
|
|
1162
|
+
_deps.appendRuntimeLog("WARN", `AUTO_HANDOFF_SELF_TURN: ${sessionId} | speaker: ${speaker} | breaking`);
|
|
1163
|
+
turnSucceeded = false;
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (attempt < retryConfig.maxRetries) {
|
|
1168
|
+
_deps.appendRuntimeLog("WARN", `AUTO_HANDOFF_RETRY: ${sessionId} | speaker: ${speaker} | attempt ${attempt + 1}/${retryConfig.maxRetries} | reason: ${runResult.block_reason || runResult.error || "unknown"} | retrying in ${retryConfig.retryDelayMs}ms`);
|
|
1169
|
+
await new Promise(r => setTimeout(r, retryConfig.retryDelayMs));
|
|
1170
|
+
} else {
|
|
1171
|
+
_deps.appendRuntimeLog("WARN", `AUTO_HANDOFF_SKIP: ${sessionId} | speaker: ${speaker} | exhausted ${retryConfig.maxRetries} retries | submitting placeholder`);
|
|
1172
|
+
// Submit a placeholder turn so the session can advance to the next speaker
|
|
1173
|
+
submitDeliberationTurn({
|
|
1174
|
+
session_id: sessionId,
|
|
1175
|
+
speaker,
|
|
1176
|
+
content: `[AUTO_SKIP] Speaker ${speaker} did not respond after ${retryConfig.maxRetries} retries.`,
|
|
1177
|
+
channel_used: "auto_skip",
|
|
1178
|
+
});
|
|
1179
|
+
turnSucceeded = true; // placeholder submitted, continue with next speaker
|
|
1180
|
+
}
|
|
1140
1181
|
}
|
|
1141
1182
|
|
|
1142
|
-
|
|
1183
|
+
if (!turnSucceeded) break;
|
|
1143
1184
|
}
|
|
1144
1185
|
|
|
1145
1186
|
// Phase 2: Generate structured synthesis
|
|
@@ -1206,6 +1247,20 @@ export async function runAutoHandoff(sessionId) {
|
|
|
1206
1247
|
_deps.appendRuntimeLog("INFO", `AUTO_HANDOFF_NOTIFIED: ${sessionId} | telepty event sent`);
|
|
1207
1248
|
}
|
|
1208
1249
|
|
|
1250
|
+
// Phase 5: Report final results to orchestrator
|
|
1251
|
+
const orchestratorSessionId = state.orchestrator_session_id;
|
|
1252
|
+
if (orchestratorSessionId) {
|
|
1253
|
+
const taskCount = structured.actionable_tasks?.length || 0;
|
|
1254
|
+
const decisionCount = structured.decisions?.length || 0;
|
|
1255
|
+
const reportText = `[deliberation_auto_complete] session: ${sessionId} | topic: ${state.topic} | decisions: ${decisionCount} | tasks: ${taskCount} | status: completed`;
|
|
1256
|
+
notifyTeleptySessionInject({
|
|
1257
|
+
targetSessionId: orchestratorSessionId,
|
|
1258
|
+
prompt: reportText,
|
|
1259
|
+
fromSessionId: `deliberation:${sessionId}`,
|
|
1260
|
+
}).catch(() => {});
|
|
1261
|
+
_deps.appendRuntimeLog("INFO", `AUTO_HANDOFF_REPORTED: ${sessionId} | orchestrator: ${orchestratorSessionId}`);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1209
1264
|
_deps.appendRuntimeLog("INFO", `AUTO_HANDOFF_COMPLETE: ${sessionId}`);
|
|
1210
1265
|
} catch (err) {
|
|
1211
1266
|
_deps.appendRuntimeLog("ERROR", `AUTO_HANDOFF_ERROR: ${sessionId} | ${err.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmsdc-ai/aigentry-deliberation",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.42",
|
|
4
4
|
"description": "MCP server for structured multi-AI discussions — deliberate across Claude, GPT, Gemini and more before committing to decisions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|