@dmsdc-ai/aigentry-deliberation 0.0.42 → 0.0.45
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/doctor.js +76 -0
- package/index.js +178 -32
- package/lib/entitlement.js +10 -13
- package/lib/transport.js +28 -3
- package/package.json +1 -1
package/doctor.js
CHANGED
|
@@ -62,6 +62,54 @@ const LOG_LOCATIONS = [
|
|
|
62
62
|
path.join(HOME, ".local", "lib", "mcp-deliberation", "runtime.log"),
|
|
63
63
|
];
|
|
64
64
|
|
|
65
|
+
// Runtime log size-check thresholds (env-configurable).
|
|
66
|
+
// Doctor DIAGNOSES only — it never mutates log files.
|
|
67
|
+
const LOG_SIZE_WARN_MB = Number(process.env.DELIBERATION_LOG_SIZE_WARN_MB) > 0
|
|
68
|
+
? Number(process.env.DELIBERATION_LOG_SIZE_WARN_MB)
|
|
69
|
+
: 50;
|
|
70
|
+
const LOG_SIZE_ERROR_MB = Number(process.env.DELIBERATION_LOG_SIZE_ERROR_MB) > 0
|
|
71
|
+
? Number(process.env.DELIBERATION_LOG_SIZE_ERROR_MB)
|
|
72
|
+
: 500;
|
|
73
|
+
|
|
74
|
+
function formatBytes(bytes) {
|
|
75
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "? B";
|
|
76
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
77
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
78
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
79
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Inspect ~/.local/lib/mcp-deliberation/ for runtime.log* files and report on
|
|
84
|
+
* total footprint. Returns { level, totalBytes, topFiles } without mutating
|
|
85
|
+
* anything (doctor is diagnostic-only).
|
|
86
|
+
*/
|
|
87
|
+
function checkRuntimeLogFootprint(installDir) {
|
|
88
|
+
const result = { level: "ok", totalBytes: 0, topFiles: [], dir: installDir };
|
|
89
|
+
try {
|
|
90
|
+
if (!fs.existsSync(installDir)) return result;
|
|
91
|
+
const entries = [];
|
|
92
|
+
for (const name of fs.readdirSync(installDir)) {
|
|
93
|
+
if (!/^runtime\.log(\.|$)/.test(name)) continue;
|
|
94
|
+
const p = path.join(installDir, name);
|
|
95
|
+
try {
|
|
96
|
+
const st = fs.statSync(p);
|
|
97
|
+
if (!st.isFile()) continue;
|
|
98
|
+
entries.push({ path: p, size: st.size });
|
|
99
|
+
result.totalBytes += st.size;
|
|
100
|
+
} catch { /* skip */ }
|
|
101
|
+
}
|
|
102
|
+
entries.sort((a, b) => b.size - a.size);
|
|
103
|
+
result.topFiles = entries.slice(0, 3);
|
|
104
|
+
if (result.totalBytes >= LOG_SIZE_ERROR_MB * 1024 * 1024) {
|
|
105
|
+
result.level = "error";
|
|
106
|
+
} else if (result.totalBytes >= LOG_SIZE_WARN_MB * 1024 * 1024) {
|
|
107
|
+
result.level = "warn";
|
|
108
|
+
}
|
|
109
|
+
} catch { /* ignore */ }
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
65
113
|
// ── TOML parser (minimal, mcp_servers only) ────────────────────
|
|
66
114
|
|
|
67
115
|
function parseMcpServersFromToml(content) {
|
|
@@ -381,6 +429,34 @@ function runDiagnostics() {
|
|
|
381
429
|
)
|
|
382
430
|
: path.join(HOME, ".local", "lib", "mcp-deliberation");
|
|
383
431
|
|
|
432
|
+
// Runtime log footprint check — diagnose only, never mutate.
|
|
433
|
+
const logCheck = checkRuntimeLogFootprint(installDir);
|
|
434
|
+
if (logCheck.totalBytes > 0) {
|
|
435
|
+
const sizeStr = formatBytes(logCheck.totalBytes);
|
|
436
|
+
if (logCheck.level === "error") {
|
|
437
|
+
totalIssues++;
|
|
438
|
+
console.log(` ❌ runtime.log footprint: ${sizeStr} (>= ${LOG_SIZE_ERROR_MB} MB ERROR threshold)`);
|
|
439
|
+
} else if (logCheck.level === "warn") {
|
|
440
|
+
totalIssues++;
|
|
441
|
+
console.log(` ⚠️ runtime.log footprint: ${sizeStr} (>= ${LOG_SIZE_WARN_MB} MB WARN threshold)`);
|
|
442
|
+
} else {
|
|
443
|
+
console.log(` ✅ runtime.log footprint: ${sizeStr}`);
|
|
444
|
+
}
|
|
445
|
+
if (logCheck.level !== "ok" && logCheck.topFiles.length > 0) {
|
|
446
|
+
console.log(` top offenders:`);
|
|
447
|
+
for (const f of logCheck.topFiles) {
|
|
448
|
+
console.log(` - ${formatBytes(f.size).padStart(10)} ${f.path}`);
|
|
449
|
+
}
|
|
450
|
+
console.log(` fix: upgrade to v0.0.45+ and let normal rotation / budget enforcement reclaim space. Immediate: rm ${path.join(installDir, 'runtime.log.old')} && : > ${path.join(installDir, 'runtime.log')}`);
|
|
451
|
+
allIssues.push({
|
|
452
|
+
config: "logs",
|
|
453
|
+
server: "runtime.log",
|
|
454
|
+
issue: logCheck.level === "error" ? "log dir >= 500 MB" : "log dir >= 50 MB",
|
|
455
|
+
fix: "upgrade to v0.0.45+ or manual cleanup",
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
384
460
|
const selfPath = path.join(installDir, "index.js");
|
|
385
461
|
if (checkPathExists(selfPath)) {
|
|
386
462
|
console.log(` ✅ Server file: ${selfPath}`);
|
package/index.js
CHANGED
|
@@ -413,22 +413,154 @@ function formatRuntimeError(error) {
|
|
|
413
413
|
return String(error);
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
|
|
416
|
+
// ── Runtime log configuration (env-overridable) ────────────────
|
|
417
|
+
const LOG_MAX_SIZE_MB = Number(process.env.DELIBERATION_LOG_MAX_SIZE_MB) > 0
|
|
418
|
+
? Number(process.env.DELIBERATION_LOG_MAX_SIZE_MB)
|
|
419
|
+
: 1;
|
|
420
|
+
const LOG_TOTAL_BUDGET_MB = Number(process.env.DELIBERATION_LOG_TOTAL_BUDGET_MB) > 0
|
|
421
|
+
? Number(process.env.DELIBERATION_LOG_TOTAL_BUDGET_MB)
|
|
422
|
+
: 10;
|
|
423
|
+
const LOG_DEDUP_MS = Number.isFinite(Number(process.env.DELIBERATION_LOG_DEDUP_MS)) && Number(process.env.DELIBERATION_LOG_DEDUP_MS) >= 0
|
|
424
|
+
? Number(process.env.DELIBERATION_LOG_DEDUP_MS)
|
|
425
|
+
: 1000;
|
|
426
|
+
const LOG_HARD_CAP_BYTES = LOG_MAX_SIZE_MB * 2 * 1024 * 1024; // race fallback: 2× per-file threshold
|
|
427
|
+
const LOG_TAIL_BYTES = 500 * 1024; // truncate to last 500 KB on hard-cap overflow
|
|
428
|
+
const LOG_PRE_UPGRADE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
429
|
+
|
|
430
|
+
// Module-level dedup state
|
|
431
|
+
let _dedupLastKey = null;
|
|
432
|
+
let _dedupLastWriteMs = 0;
|
|
433
|
+
let _dedupLastMessage = null;
|
|
434
|
+
let _dedupPendingCount = 0;
|
|
435
|
+
|
|
436
|
+
// Module-level upgrade-safety guard (once per process)
|
|
437
|
+
let _logUpgradeSafetyRan = false;
|
|
438
|
+
|
|
439
|
+
function _flushDedupToFile() {
|
|
440
|
+
if (_dedupPendingCount <= 0) return;
|
|
417
441
|
try {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
442
|
+
const elapsed = Date.now() - _dedupLastWriteMs;
|
|
443
|
+
const summary = `${new Date().toISOString()} [DEDUP] [${_dedupPendingCount}x in ${elapsed}ms] ${_dedupLastMessage || ""}\n`;
|
|
444
|
+
fs.appendFileSync(GLOBAL_RUNTIME_LOG, summary, "utf-8");
|
|
445
|
+
} catch { /* ignore */ }
|
|
446
|
+
_dedupPendingCount = 0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function _runLogUpgradeSafetyOnce() {
|
|
450
|
+
if (_logUpgradeSafetyRan) return;
|
|
451
|
+
_logUpgradeSafetyRan = true;
|
|
452
|
+
try {
|
|
453
|
+
const dir = path.dirname(GLOBAL_RUNTIME_LOG);
|
|
454
|
+
if (!fs.existsSync(dir)) return;
|
|
455
|
+
const markerPath = path.join(dir, ".log-upgrade-v0.0.45");
|
|
456
|
+
if (!fs.existsSync(markerPath)) {
|
|
457
|
+
let totalSize = 0;
|
|
458
|
+
const candidates = [];
|
|
459
|
+
for (const name of fs.readdirSync(dir)) {
|
|
460
|
+
if (!/^runtime\.log(\.|$)/.test(name)) continue;
|
|
461
|
+
if (name.startsWith("runtime.log.pre-")) continue;
|
|
462
|
+
const p = path.join(dir, name);
|
|
463
|
+
try {
|
|
464
|
+
const s = fs.statSync(p).size;
|
|
465
|
+
totalSize += s;
|
|
466
|
+
candidates.push(p);
|
|
467
|
+
} catch { /* skip */ }
|
|
468
|
+
}
|
|
469
|
+
if (totalSize > 1024 * 1024) {
|
|
470
|
+
const preBackup = GLOBAL_RUNTIME_LOG + ".pre-0.0.45";
|
|
471
|
+
try {
|
|
472
|
+
if (fs.existsSync(GLOBAL_RUNTIME_LOG) && !fs.existsSync(preBackup)) {
|
|
473
|
+
fs.renameSync(GLOBAL_RUNTIME_LOG, preBackup);
|
|
474
|
+
}
|
|
475
|
+
// Any other rotated files (runtime.log.old etc.) — removed so normal
|
|
476
|
+
// rotation can start fresh. The .pre-0.0.45 backup retains the latest.
|
|
477
|
+
for (const p of candidates) {
|
|
478
|
+
if (p === preBackup) continue;
|
|
479
|
+
if (!fs.existsSync(p)) continue;
|
|
480
|
+
try { fs.unlinkSync(p); } catch { /* ignore */ }
|
|
481
|
+
}
|
|
482
|
+
} catch { /* ignore */ }
|
|
483
|
+
}
|
|
484
|
+
try { fs.writeFileSync(markerPath, new Date().toISOString()); } catch { /* ignore */ }
|
|
485
|
+
}
|
|
486
|
+
// Expire .pre-0.0.45 after 7 days OR on budget overflow
|
|
421
487
|
try {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
488
|
+
const preBackup = GLOBAL_RUNTIME_LOG + ".pre-0.0.45";
|
|
489
|
+
if (fs.existsSync(preBackup)) {
|
|
490
|
+
const preStat = fs.statSync(preBackup);
|
|
491
|
+
let totalDir = preStat.size;
|
|
492
|
+
try {
|
|
493
|
+
for (const name of fs.readdirSync(dir)) {
|
|
494
|
+
if (!/^runtime\.log(\.|$)/.test(name)) continue;
|
|
495
|
+
if (name === "runtime.log.pre-0.0.45") continue;
|
|
496
|
+
try { totalDir += fs.statSync(path.join(dir, name)).size; } catch { /* skip */ }
|
|
497
|
+
}
|
|
498
|
+
} catch { /* skip */ }
|
|
499
|
+
const age = Date.now() - preStat.mtimeMs;
|
|
500
|
+
if (age > LOG_PRE_UPGRADE_EXPIRY_MS || totalDir > LOG_TOTAL_BUDGET_MB * 1024 * 1024) {
|
|
501
|
+
try { fs.unlinkSync(preBackup); } catch { /* ignore */ }
|
|
427
502
|
}
|
|
428
503
|
}
|
|
429
|
-
} catch { /* ignore
|
|
504
|
+
} catch { /* ignore */ }
|
|
505
|
+
} catch { /* ignore */ }
|
|
506
|
+
}
|
|
430
507
|
|
|
431
|
-
|
|
508
|
+
function _rotateOrTruncate() {
|
|
509
|
+
try {
|
|
510
|
+
if (!fs.existsSync(GLOBAL_RUNTIME_LOG)) return;
|
|
511
|
+
const stats = fs.statSync(GLOBAL_RUNTIME_LOG);
|
|
512
|
+
// Hard cap: if file exceeds 2× per-file threshold (race under concurrent writers),
|
|
513
|
+
// truncate in place to the last LOG_TAIL_BYTES to prevent runaway growth.
|
|
514
|
+
if (stats.size > LOG_HARD_CAP_BYTES) {
|
|
515
|
+
try {
|
|
516
|
+
const fd = fs.openSync(GLOBAL_RUNTIME_LOG, "r");
|
|
517
|
+
try {
|
|
518
|
+
const tailStart = Math.max(0, stats.size - LOG_TAIL_BYTES);
|
|
519
|
+
const buf = Buffer.alloc(stats.size - tailStart);
|
|
520
|
+
fs.readSync(fd, buf, 0, buf.length, tailStart);
|
|
521
|
+
fs.writeFileSync(GLOBAL_RUNTIME_LOG, buf, "utf-8");
|
|
522
|
+
} finally {
|
|
523
|
+
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
524
|
+
}
|
|
525
|
+
} catch { /* fall through */ }
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (stats.size > LOG_MAX_SIZE_MB * 1024 * 1024) {
|
|
529
|
+
const oldLog = GLOBAL_RUNTIME_LOG + ".old";
|
|
530
|
+
// Explicit cleanup: delete previous .old before rename (robustness over
|
|
531
|
+
// atomic-rename-overwrite assumption, especially under concurrent writers).
|
|
532
|
+
try {
|
|
533
|
+
if (fs.existsSync(oldLog)) fs.unlinkSync(oldLog);
|
|
534
|
+
} catch { /* ignore */ }
|
|
535
|
+
try { fs.renameSync(GLOBAL_RUNTIME_LOG, oldLog); } catch { /* ignore */ }
|
|
536
|
+
}
|
|
537
|
+
} catch { /* ignore rotation failures */ }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function appendRuntimeLog(level, message) {
|
|
541
|
+
try {
|
|
542
|
+
fs.mkdirSync(path.dirname(GLOBAL_RUNTIME_LOG), { recursive: true });
|
|
543
|
+
_runLogUpgradeSafetyOnce();
|
|
544
|
+
|
|
545
|
+
const safeMessage = String(message ?? "");
|
|
546
|
+
const key = `${level}:${safeMessage.slice(0, 200)}`;
|
|
547
|
+
const now = Date.now();
|
|
548
|
+
|
|
549
|
+
// Dedup: suppress repeated identical messages within window
|
|
550
|
+
if (_dedupLastKey === key && (now - _dedupLastWriteMs) < LOG_DEDUP_MS) {
|
|
551
|
+
_dedupPendingCount += 1;
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// New key or window expired — flush prior suppression summary first
|
|
556
|
+
if (_dedupPendingCount > 0) _flushDedupToFile();
|
|
557
|
+
_dedupLastKey = key;
|
|
558
|
+
_dedupLastWriteMs = now;
|
|
559
|
+
_dedupLastMessage = `[${level}] ${safeMessage}`;
|
|
560
|
+
|
|
561
|
+
_rotateOrTruncate();
|
|
562
|
+
|
|
563
|
+
const line = `${new Date(now).toISOString()} [${level}] ${safeMessage}\n`;
|
|
432
564
|
fs.appendFileSync(GLOBAL_RUNTIME_LOG, line, "utf-8");
|
|
433
565
|
} catch {
|
|
434
566
|
// ignore logging failures
|
|
@@ -582,34 +714,48 @@ for (const stream of [process.stdout, process.stderr]) {
|
|
|
582
714
|
});
|
|
583
715
|
}
|
|
584
716
|
|
|
717
|
+
// Module-level reentrance guard. Once a fatal handler has fired, subsequent
|
|
718
|
+
// invocations become no-ops. This breaks the EPIPE self-amplifying loop where
|
|
719
|
+
// writing the previous error's log line itself triggered another EPIPE.
|
|
720
|
+
let _hasHandledFatalError = false;
|
|
721
|
+
|
|
722
|
+
function _isBrokenStdioError(err) {
|
|
723
|
+
if (!err) return false;
|
|
724
|
+
const code = err.code;
|
|
725
|
+
if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED" || code === "ERR_STREAM_WRITE_AFTER_END") return true;
|
|
726
|
+
const message = String(err?.message ?? err ?? "");
|
|
727
|
+
return /EPIPE|write after end/i.test(message);
|
|
728
|
+
}
|
|
729
|
+
|
|
585
730
|
process.on("uncaughtException", (error) => {
|
|
586
|
-
|
|
587
|
-
if (error
|
|
731
|
+
if (_hasHandledFatalError) return;
|
|
732
|
+
if (_isBrokenStdioError(error)) {
|
|
733
|
+
_hasHandledFatalError = true;
|
|
734
|
+
try { _flushDedupToFile(); } catch { /* noop */ }
|
|
588
735
|
try { appendRuntimeLog("INFO", "Client disconnected (EPIPE). Shutting down."); } catch { /* noop */ }
|
|
589
|
-
process.exit(0);
|
|
590
|
-
|
|
591
|
-
const message = formatRuntimeError(error);
|
|
592
|
-
appendRuntimeLog("UNCAUGHT_EXCEPTION", message);
|
|
593
|
-
try {
|
|
594
|
-
process.stderr.write(`[mcp-deliberation] uncaughtException: ${message}\n`);
|
|
595
|
-
} catch {
|
|
596
|
-
// ignore stderr write failures
|
|
736
|
+
try { process.exit(0); } catch { /* noop */ }
|
|
737
|
+
return;
|
|
597
738
|
}
|
|
739
|
+
// Non-stdio fatal: log to file only. process.stderr.write was REMOVED here
|
|
740
|
+
// because it was the re-trigger source when stdio was the broken channel.
|
|
741
|
+
try { appendRuntimeLog("UNCAUGHT_EXCEPTION", formatRuntimeError(error)); } catch { /* noop */ }
|
|
598
742
|
});
|
|
599
743
|
|
|
600
744
|
process.on("unhandledRejection", (reason) => {
|
|
601
|
-
|
|
602
|
-
if (reason
|
|
745
|
+
if (_hasHandledFatalError) return;
|
|
746
|
+
if (_isBrokenStdioError(reason)) {
|
|
747
|
+
_hasHandledFatalError = true;
|
|
748
|
+
try { _flushDedupToFile(); } catch { /* noop */ }
|
|
603
749
|
try { appendRuntimeLog("INFO", "Client disconnected (EPIPE). Shutting down."); } catch { /* noop */ }
|
|
604
|
-
process.exit(0);
|
|
605
|
-
|
|
606
|
-
const message = formatRuntimeError(reason);
|
|
607
|
-
appendRuntimeLog("UNHANDLED_REJECTION", message);
|
|
608
|
-
try {
|
|
609
|
-
process.stderr.write(`[mcp-deliberation] unhandledRejection: ${message}\n`);
|
|
610
|
-
} catch {
|
|
611
|
-
// ignore stderr write failures
|
|
750
|
+
try { process.exit(0); } catch { /* noop */ }
|
|
751
|
+
return;
|
|
612
752
|
}
|
|
753
|
+
try { appendRuntimeLog("UNHANDLED_REJECTION", formatRuntimeError(reason)); } catch { /* noop */ }
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// Flush any pending dedup summary on graceful exit so the tail summary is not lost
|
|
757
|
+
process.on("exit", () => {
|
|
758
|
+
try { _flushDedupToFile(); } catch { /* noop */ }
|
|
613
759
|
});
|
|
614
760
|
|
|
615
761
|
// Read version from package.json (single source of truth)
|
|
@@ -3109,4 +3255,4 @@ if (__entryFile && path.resolve(__currentFile) === __entryFile) {
|
|
|
3109
3255
|
}
|
|
3110
3256
|
|
|
3111
3257
|
// ── Test exports (used by vitest) ──
|
|
3112
|
-
export { checkToolEntitlement, 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, buildTeleptyTurnCompletedEnvelope, buildTeleptySynthesisEnvelope, validateTeleptyEnvelope, registerPendingTeleptyTurnRequest, handleTeleptyBusMessage, completePendingTeleptySemantic, cleanupPendingTeleptyTurn, getTeleptySessionHealth, TELEPTY_TRANSPORT_TIMEOUT_MS, TELEPTY_SEMANTIC_TIMEOUT_MS };
|
|
3258
|
+
export { appendRuntimeLog, _flushDedupToFile, _isBrokenStdioError, checkToolEntitlement, 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, buildTeleptyTurnCompletedEnvelope, buildTeleptySynthesisEnvelope, validateTeleptyEnvelope, registerPendingTeleptyTurnRequest, handleTeleptyBusMessage, completePendingTeleptySemantic, cleanupPendingTeleptyTurn, getTeleptySessionHealth, TELEPTY_TRANSPORT_TIMEOUT_MS, TELEPTY_SEMANTIC_TIMEOUT_MS };
|
package/lib/entitlement.js
CHANGED
|
@@ -4,23 +4,20 @@ import os from "os";
|
|
|
4
4
|
|
|
5
5
|
const LICENSE_PATH = path.join(os.homedir(), ".aigentry", "license.json");
|
|
6
6
|
|
|
7
|
-
// Feature → tier mapping
|
|
7
|
+
// Feature → tier mapping (all features available to all tiers — open source)
|
|
8
8
|
const FEATURE_TIERS = {
|
|
9
9
|
"deliberation.core": ["free", "pro", "team"],
|
|
10
|
-
"deliberation.multi_session": ["pro", "team"],
|
|
11
|
-
"deliberation.browser_integration": ["pro", "team"],
|
|
12
|
-
"deliberation.auto_run": ["pro", "team"],
|
|
13
|
-
"deliberation.remote": ["pro", "team"],
|
|
14
|
-
"deliberation.decision_engine": ["pro", "team"],
|
|
15
|
-
"deliberation.code_review": ["pro", "team"],
|
|
16
|
-
"deliberation.unlimited_speakers": ["pro", "team"],
|
|
10
|
+
"deliberation.multi_session": ["free", "pro", "team"],
|
|
11
|
+
"deliberation.browser_integration": ["free", "pro", "team"],
|
|
12
|
+
"deliberation.auto_run": ["free", "pro", "team"],
|
|
13
|
+
"deliberation.remote": ["free", "pro", "team"],
|
|
14
|
+
"deliberation.decision_engine": ["free", "pro", "team"],
|
|
15
|
+
"deliberation.code_review": ["free", "pro", "team"],
|
|
16
|
+
"deliberation.unlimited_speakers": ["free", "pro", "team"],
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
// Free tier limits
|
|
20
|
-
const FREE_LIMITS = {
|
|
21
|
-
"deliberation.multi_session": 1,
|
|
22
|
-
"deliberation.unlimited_speakers": 3,
|
|
23
|
-
};
|
|
19
|
+
// Free tier limits (no limits — open source)
|
|
20
|
+
const FREE_LIMITS = {};
|
|
24
21
|
|
|
25
22
|
// Tool → feature mapping
|
|
26
23
|
const TOOL_FEATURE_MAP = {
|
package/lib/transport.js
CHANGED
|
@@ -1128,6 +1128,22 @@ export async function runAutoHandoff(sessionId) {
|
|
|
1128
1128
|
const retryConfig = { maxRetries: 2, retryDelayMs: 10000 };
|
|
1129
1129
|
|
|
1130
1130
|
try {
|
|
1131
|
+
// Pre-flight: if every speaker matches the orchestrator's CLI identity, there is
|
|
1132
|
+
// no one to auto-dispatch. Halt Phase 1 and skip Phase 2 synthesis so the session
|
|
1133
|
+
// remains `active` and the orchestrator can provide turns manually.
|
|
1134
|
+
{
|
|
1135
|
+
const initialState = loadSession(sessionId);
|
|
1136
|
+
const callerId = detectCallerSpeaker();
|
|
1137
|
+
if (initialState && callerId && Array.isArray(initialState.speakers) && initialState.speakers.length > 0) {
|
|
1138
|
+
const normalizedCaller = normalizeSpeaker(callerId);
|
|
1139
|
+
const allSelf = initialState.speakers.every(s => normalizeSpeaker(s) === normalizedCaller);
|
|
1140
|
+
if (allSelf) {
|
|
1141
|
+
_deps.appendRuntimeLog("WARN", `AUTO_HANDOFF_ALL_SELF_TURN: ${sessionId} | all speakers match caller identity "${normalizedCaller}" | halting auto-dispatch; orchestrator must proceed manually`);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1131
1147
|
// Phase 1: Run all deliberation turns
|
|
1132
1148
|
let maxIterations = 100; // safety limit
|
|
1133
1149
|
while (maxIterations-- > 0) {
|
|
@@ -1157,10 +1173,19 @@ export async function runAutoHandoff(sessionId) {
|
|
|
1157
1173
|
break;
|
|
1158
1174
|
}
|
|
1159
1175
|
|
|
1160
|
-
// self_turn
|
|
1176
|
+
// self_turn: orchestrator is the speaker. The defensive guard at runUntilBlockedCore
|
|
1177
|
+
// prevented a recursive CLI spawn; here we submit a visible placeholder so the session
|
|
1178
|
+
// can advance to the next speaker rather than aborting Phase 1 entirely.
|
|
1161
1179
|
if (runResult.block_reason === "self_turn") {
|
|
1162
|
-
_deps.appendRuntimeLog("WARN", `
|
|
1163
|
-
|
|
1180
|
+
_deps.appendRuntimeLog("WARN", `AUTO_HANDOFF_SELF_TURN_SKIP: ${sessionId} | speaker: ${speaker} | submitting placeholder and advancing`);
|
|
1181
|
+
submitDeliberationTurn({
|
|
1182
|
+
session_id: sessionId,
|
|
1183
|
+
speaker,
|
|
1184
|
+
content: `[SELF_TURN_SKIP] Speaker ${speaker} matches the orchestrator identity; auto-dispatch skipped to avoid recursive self-spawn. The orchestrator may contribute this speaker's input manually via deliberation_respond before synthesis.`,
|
|
1185
|
+
channel_used: "self_turn_skip",
|
|
1186
|
+
fallback_reason: "caller_identity_match",
|
|
1187
|
+
});
|
|
1188
|
+
turnSucceeded = true; // placeholder submitted, advance to next speaker
|
|
1164
1189
|
break;
|
|
1165
1190
|
}
|
|
1166
1191
|
|
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.45",
|
|
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",
|