@dmsdc-ai/aigentry-deliberation 0.0.40 → 0.0.41

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 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
- if (auto_execute) {
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
- const runResult = await runUntilBlockedCore(sessionId, { maxTurns: 1, includeHistoryEntries: 3 });
1136
- const step = runResult.steps.at(-1) || null;
1137
- if (!runResult.ok || runResult.status === "blocked") {
1138
- _deps.appendRuntimeLog("WARN", `AUTO_HANDOFF_TURN_BLOCKED: ${sessionId} | speaker: ${speaker} | ${runResult.block_reason || runResult.error || "unknown"}`);
1139
- break;
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
- _deps.appendRuntimeLog("INFO", `AUTO_HANDOFF_TURN_OK: ${sessionId} | speaker: ${speaker} | ${step?.elapsedMs || 0}ms`);
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.40",
3
+ "version": "0.0.41",
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",