instar 0.25.1 → 0.25.3
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +337 -100
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +30 -8
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +2 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts +3 -1
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +41 -3
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/messaging/slack/types.d.ts +1 -0
- package/dist/messaging/slack/types.d.ts.map +1 -1
- package/dist/monitoring/PresenceProxy.d.ts +5 -0
- package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
- package/dist/monitoring/PresenceProxy.js +21 -4
- package/dist/monitoring/PresenceProxy.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +59 -4
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/pre-push-gate.js +28 -0
- package/src/data/builtin-manifest.json +63 -63
- package/src/templates/scripts/slack-reply.sh +10 -0
- package/upgrades/0.25.2.md +23 -0
- package/upgrades/0.25.3.md +25 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA0PH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsnCD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA0PH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsnCD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAikHtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
|
package/dist/commands/server.js
CHANGED
|
@@ -1701,17 +1701,44 @@ export async function startServer(options) {
|
|
|
1701
1701
|
}
|
|
1702
1702
|
// If handle() returned false, fall through to relay
|
|
1703
1703
|
}
|
|
1704
|
-
// Relay to
|
|
1704
|
+
// Relay to messaging platform if adapter is available and session has a binding
|
|
1705
1705
|
if (classification.action === 'relay' || classification.action === 'auto-approve') {
|
|
1706
|
+
let relayed = false;
|
|
1707
|
+
// Try Telegram first
|
|
1706
1708
|
if (telegram) {
|
|
1707
1709
|
const topicId = telegram.getTopicForSession(prompt.sessionName);
|
|
1708
1710
|
if (topicId) {
|
|
1709
1711
|
try {
|
|
1710
1712
|
await telegram.relayPrompt(topicId, prompt);
|
|
1711
|
-
console.log(`[PromptGate] Relayed ${prompt.type} prompt to topic ${topicId}`);
|
|
1713
|
+
console.log(`[PromptGate] Relayed ${prompt.type} prompt to Telegram topic ${topicId}`);
|
|
1714
|
+
relayed = true;
|
|
1712
1715
|
}
|
|
1713
1716
|
catch (relayErr) {
|
|
1714
|
-
console.error(`[PromptGate]
|
|
1717
|
+
console.error(`[PromptGate] Telegram relay failed: ${relayErr instanceof Error ? relayErr.message : relayErr}`);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
// Try Slack if not already relayed via Telegram
|
|
1722
|
+
if (!relayed && _slackAdapter) {
|
|
1723
|
+
const channelId = _slackAdapter.getChannelForSession(prompt.sessionName);
|
|
1724
|
+
if (channelId) {
|
|
1725
|
+
try {
|
|
1726
|
+
const question = prompt.summary || 'Agent needs your input';
|
|
1727
|
+
const options = (prompt.options || []).map((opt, i) => ({
|
|
1728
|
+
label: opt.label.slice(0, 75),
|
|
1729
|
+
value: opt.key,
|
|
1730
|
+
primary: i === 0,
|
|
1731
|
+
}));
|
|
1732
|
+
if (options.length > 0) {
|
|
1733
|
+
await _slackAdapter.relayPrompt(channelId, prompt.id, question, options);
|
|
1734
|
+
}
|
|
1735
|
+
else {
|
|
1736
|
+
await _slackAdapter.sendToChannel(channelId, `⏳ *Agent needs your input:*\n${question}\n\n_Reply in this channel to respond._`);
|
|
1737
|
+
}
|
|
1738
|
+
console.log(`[PromptGate] Relayed ${prompt.type} prompt to Slack channel ${channelId}`);
|
|
1739
|
+
}
|
|
1740
|
+
catch (relayErr) {
|
|
1741
|
+
console.error(`[PromptGate] Slack relay failed: ${relayErr instanceof Error ? relayErr.message : relayErr}`);
|
|
1715
1742
|
}
|
|
1716
1743
|
}
|
|
1717
1744
|
}
|
|
@@ -1803,6 +1830,29 @@ export async function startServer(options) {
|
|
|
1803
1830
|
if (telemetryHeartbeat) {
|
|
1804
1831
|
telemetryHeartbeat.start();
|
|
1805
1832
|
}
|
|
1833
|
+
// Helper: resolve pending plan prompts when a prompt response arrives.
|
|
1834
|
+
// Calls the internal route endpoint to mark the plan prompt as resolved,
|
|
1835
|
+
// which unblocks the PreToolUse hook that's polling for the response.
|
|
1836
|
+
const resolvePlanPromptForSession = (sessionName, key) => {
|
|
1837
|
+
const port = config.port ?? 4042;
|
|
1838
|
+
const payload = JSON.stringify({ sessionName, key });
|
|
1839
|
+
const http = require('node:http');
|
|
1840
|
+
const req = http.request({
|
|
1841
|
+
hostname: '127.0.0.1',
|
|
1842
|
+
port,
|
|
1843
|
+
path: '/hooks/plan-prompt/resolve',
|
|
1844
|
+
method: 'POST',
|
|
1845
|
+
headers: {
|
|
1846
|
+
'Content-Type': 'application/json',
|
|
1847
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
1848
|
+
'Authorization': `Bearer ${config.authToken}`,
|
|
1849
|
+
},
|
|
1850
|
+
timeout: 2000,
|
|
1851
|
+
});
|
|
1852
|
+
req.on('error', () => { }); // Best-effort
|
|
1853
|
+
req.write(payload);
|
|
1854
|
+
req.end();
|
|
1855
|
+
};
|
|
1806
1856
|
// Set up Telegram if configured
|
|
1807
1857
|
// When --no-telegram is set (lifeline owns polling), create adapter in send-only mode
|
|
1808
1858
|
// so the server can still relay replies via /telegram/reply/:topicId
|
|
@@ -1831,6 +1881,8 @@ export async function startServer(options) {
|
|
|
1831
1881
|
console.warn(`[PromptGate] Skipping injection — session "${sessionName}" is no longer alive`);
|
|
1832
1882
|
return false;
|
|
1833
1883
|
}
|
|
1884
|
+
// Also resolve any pending plan prompt for this session (unblocks the hook)
|
|
1885
|
+
resolvePlanPromptForSession(sessionName, key);
|
|
1834
1886
|
return sessionManager.sendKey(sessionName, key);
|
|
1835
1887
|
};
|
|
1836
1888
|
telegram.onPromptTextResponse = (sessionName, text) => {
|
|
@@ -1871,6 +1923,8 @@ export async function startServer(options) {
|
|
|
1871
1923
|
console.warn(`[PromptGate] Skipping injection — session "${sessionName}" is no longer alive`);
|
|
1872
1924
|
return false;
|
|
1873
1925
|
}
|
|
1926
|
+
// Also resolve any pending plan prompt for this session (unblocks the hook)
|
|
1927
|
+
resolvePlanPromptForSession(sessionName, key);
|
|
1874
1928
|
return sessionManager.sendKey(sessionName, key);
|
|
1875
1929
|
};
|
|
1876
1930
|
telegram.onPromptTextResponse = (sessionName, text) => {
|
|
@@ -2301,14 +2355,14 @@ export async function startServer(options) {
|
|
|
2301
2355
|
lines.push('--- End Thread History ---');
|
|
2302
2356
|
lines.push('');
|
|
2303
2357
|
lines.push('CRITICAL: You MUST relay your response back to Slack after responding.');
|
|
2304
|
-
lines.push('Use the relay script:');
|
|
2358
|
+
lines.push('Use the relay script (write ONLY your reply text — do NOT pipe or cat this file into the script):');
|
|
2305
2359
|
lines.push('');
|
|
2306
2360
|
lines.push(`cat <<'EOF' | .claude/scripts/slack-reply.sh ${channelId}`);
|
|
2307
2361
|
lines.push('Your response text here');
|
|
2308
2362
|
lines.push('EOF');
|
|
2309
2363
|
lines.push('');
|
|
2310
2364
|
lines.push('Strip the [slack:] prefix before interpreting the message.');
|
|
2311
|
-
lines.push('Only relay conversational text — not tool output or internal reasoning.');
|
|
2365
|
+
lines.push('Only relay conversational text — not tool output, file contents, or internal reasoning.');
|
|
2312
2366
|
const contextData = lines.join('\n');
|
|
2313
2367
|
fs.writeFileSync(ctxPath, contextData);
|
|
2314
2368
|
// Transform [image:path] and [document:path] tags into explicit read instructions
|
|
@@ -2364,8 +2418,7 @@ export async function startServer(options) {
|
|
|
2364
2418
|
sessionManager.injectMessage(existingSession, bootstrapMessage);
|
|
2365
2419
|
// Track for stall detection
|
|
2366
2420
|
slackAdapter.trackMessageInjection(channelId, existingSession, message.content);
|
|
2367
|
-
// Delivery confirmation
|
|
2368
|
-
slackAdapter.sendToChannel(channelId, '✓ Delivered').catch(() => { });
|
|
2421
|
+
// Delivery confirmation via reaction only (no text message — the ✅ reaction is sufficient)
|
|
2369
2422
|
}
|
|
2370
2423
|
catch (injectErr) {
|
|
2371
2424
|
console.error(`[slack→session] Injection failed: ${injectErr instanceof Error ? injectErr.message : injectErr}`);
|
|
@@ -2491,6 +2544,21 @@ export async function startServer(options) {
|
|
|
2491
2544
|
slackAdapter.onIsSessionAlive = (tmuxSession) => {
|
|
2492
2545
|
return sessionManager.isSessionAlive(tmuxSession);
|
|
2493
2546
|
};
|
|
2547
|
+
// Wire prompt response callback — inject button presses into sessions
|
|
2548
|
+
slackAdapter.onPromptResponse = (channelId, promptId, value) => {
|
|
2549
|
+
// Look up which session is bound to this channel
|
|
2550
|
+
const sessionName = slackAdapter.getSessionForChannel(channelId);
|
|
2551
|
+
if (!sessionName) {
|
|
2552
|
+
console.warn(`[slack] Prompt response for channel ${channelId} but no session bound`);
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
if (!sessionManager.isSessionAlive(sessionName)) {
|
|
2556
|
+
console.warn(`[slack] Prompt response for dead session "${sessionName}"`);
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
sessionManager.sendKey(sessionName, value);
|
|
2560
|
+
console.log(`[slack] Prompt response injected: session="${sessionName}" key="${value}"`);
|
|
2561
|
+
};
|
|
2494
2562
|
// Standby commands will be wired after PresenceProxy is initialized (below)
|
|
2495
2563
|
}
|
|
2496
2564
|
catch (err) {
|
|
@@ -2574,19 +2642,34 @@ export async function startServer(options) {
|
|
|
2574
2642
|
sessionManager.startMonitoring();
|
|
2575
2643
|
// Proactive resume heartbeat: every 60s, update the topic→UUID mapping
|
|
2576
2644
|
// for all active topic-linked sessions. Ensures crash recovery via --resume.
|
|
2577
|
-
if (_topicResumeMap && telegram) {
|
|
2645
|
+
if (_topicResumeMap && (telegram || _slackAdapter)) {
|
|
2578
2646
|
const resumeHeartbeatInterval = setInterval(() => {
|
|
2579
2647
|
try {
|
|
2580
|
-
const topicSessions = telegram.getAllTopicSessions();
|
|
2581
|
-
// Enrich with authoritative Claude session IDs from SessionManager
|
|
2582
2648
|
const enriched = new Map();
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
const
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2649
|
+
// Telegram topic-session mappings
|
|
2650
|
+
if (telegram) {
|
|
2651
|
+
const topicSessions = telegram.getAllTopicSessions();
|
|
2652
|
+
for (const [topicId, sessionName] of topicSessions) {
|
|
2653
|
+
const sessions = sessionManager.listRunningSessions();
|
|
2654
|
+
const session = sessions.find(s => s.tmuxSession === sessionName);
|
|
2655
|
+
enriched.set(topicId, {
|
|
2656
|
+
sessionName,
|
|
2657
|
+
claudeSessionId: session?.claudeSessionId ?? undefined,
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
// Slack channel-session mappings (use synthetic IDs for compatibility)
|
|
2662
|
+
if (_slackAdapter) {
|
|
2663
|
+
const registry = _slackAdapter.getChannelRegistry();
|
|
2664
|
+
for (const [channelId, entry] of Object.entries(registry)) {
|
|
2665
|
+
const syntheticId = slackChannelToSyntheticId(channelId);
|
|
2666
|
+
const sessions = sessionManager.listRunningSessions();
|
|
2667
|
+
const session = sessions.find(s => s.tmuxSession === entry.sessionName);
|
|
2668
|
+
enriched.set(syntheticId, {
|
|
2669
|
+
sessionName: entry.sessionName,
|
|
2670
|
+
claudeSessionId: session?.claudeSessionId ?? undefined,
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2590
2673
|
}
|
|
2591
2674
|
_topicResumeMap?.refreshResumeMappings(enriched);
|
|
2592
2675
|
}
|
|
@@ -2601,19 +2684,21 @@ export async function startServer(options) {
|
|
|
2601
2684
|
// Save Claude session UUID before any session kill so the topic can be
|
|
2602
2685
|
// resumed later with --resume. This fires BEFORE the tmux session is
|
|
2603
2686
|
// destroyed, so the UUID can still be discovered from the JSONL mtime.
|
|
2604
|
-
if (_topicResumeMap
|
|
2687
|
+
if (_topicResumeMap) {
|
|
2605
2688
|
sessionManager.on('beforeSessionKill', (session) => {
|
|
2606
2689
|
try {
|
|
2607
|
-
// Save Telegram topic resume UUID
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2690
|
+
// Save Telegram topic resume UUID (if Telegram is configured)
|
|
2691
|
+
if (telegram) {
|
|
2692
|
+
const topicId = telegram.getTopicForSession(session.tmuxSession);
|
|
2693
|
+
if (topicId) {
|
|
2694
|
+
const uuid = _topicResumeMap.findUuidForSession(session.tmuxSession, session.claudeSessionId ?? undefined);
|
|
2695
|
+
if (uuid) {
|
|
2696
|
+
_topicResumeMap.save(topicId, uuid, session.tmuxSession);
|
|
2697
|
+
console.log(`[beforeSessionKill] Saved resume UUID ${uuid} for topic ${topicId} (session: ${session.name}, source: ${session.claudeSessionId ? 'hook' : 'mtime'})`);
|
|
2698
|
+
}
|
|
2614
2699
|
}
|
|
2615
2700
|
}
|
|
2616
|
-
// Save Slack channel resume UUID
|
|
2701
|
+
// Save Slack channel resume UUID (if Slack is configured)
|
|
2617
2702
|
if (_slackAdapter) {
|
|
2618
2703
|
const channelId = _slackAdapter.getChannelForSession(session.tmuxSession);
|
|
2619
2704
|
if (channelId) {
|
|
@@ -2740,53 +2825,94 @@ export async function startServer(options) {
|
|
|
2740
2825
|
watchdog = new SessionWatchdog(config, sessionManager, state);
|
|
2741
2826
|
watchdog.intelligence = sharedIntelligence ?? null;
|
|
2742
2827
|
watchdog.on('intervention', (event) => {
|
|
2828
|
+
const levelNames = ['Monitoring', 'Ctrl+C', 'SIGTERM', 'SIGKILL', 'Kill Session'];
|
|
2829
|
+
const levelName = levelNames[event.level] || `Level ${event.level}`;
|
|
2830
|
+
const msg = `🔧 Watchdog [${levelName}]: ${event.action}\nStuck: \`${event.stuckCommand.slice(0, 60)}\``;
|
|
2743
2831
|
if (telegram) {
|
|
2744
2832
|
const topicId = telegram.getTopicForSession(event.sessionName);
|
|
2745
|
-
if (topicId)
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2833
|
+
if (topicId)
|
|
2834
|
+
telegram.sendToTopic(topicId, msg).catch(() => { });
|
|
2835
|
+
}
|
|
2836
|
+
if (_slackAdapter) {
|
|
2837
|
+
const channelId = _slackAdapter.getChannelForSession(event.sessionName);
|
|
2838
|
+
if (channelId)
|
|
2839
|
+
_slackAdapter.sendToChannel(channelId, msg).catch(() => { });
|
|
2750
2840
|
}
|
|
2751
2841
|
});
|
|
2752
2842
|
watchdog.on('recovery', (sessionName, fromLevel) => {
|
|
2843
|
+
const msg = `✅ Watchdog: session recovered (was at escalation level ${fromLevel})`;
|
|
2753
2844
|
if (telegram) {
|
|
2754
2845
|
const topicId = telegram.getTopicForSession(sessionName);
|
|
2755
|
-
if (topicId)
|
|
2756
|
-
telegram.sendToTopic(topicId,
|
|
2757
|
-
|
|
2846
|
+
if (topicId)
|
|
2847
|
+
telegram.sendToTopic(topicId, msg).catch(() => { });
|
|
2848
|
+
}
|
|
2849
|
+
if (_slackAdapter) {
|
|
2850
|
+
const channelId = _slackAdapter.getChannelForSession(sessionName);
|
|
2851
|
+
if (channelId)
|
|
2852
|
+
_slackAdapter.sendToChannel(channelId, msg).catch(() => { });
|
|
2758
2853
|
}
|
|
2759
2854
|
});
|
|
2760
2855
|
watchdog.start();
|
|
2761
2856
|
console.log(pc.green(' Session Watchdog enabled'));
|
|
2762
2857
|
}
|
|
2763
2858
|
// StallTriageNurse — LLM-powered session recovery (uses shared intelligence)
|
|
2859
|
+
// Platform-aware: works with Telegram topics AND Slack channels
|
|
2764
2860
|
let triageNurse;
|
|
2765
|
-
if (config.monitoring.triage?.enabled && telegram) {
|
|
2861
|
+
if (config.monitoring.triage?.enabled && (telegram || _slackAdapter)) {
|
|
2766
2862
|
triageNurse = new StallTriageNurse({
|
|
2767
2863
|
captureSessionOutput: (name, lines) => sessionManager.captureOutput(name, lines),
|
|
2768
2864
|
isSessionAlive: (name) => sessionManager.isSessionAlive(name),
|
|
2769
2865
|
sendKey: (name, key) => sessionManager.sendKey(name, key),
|
|
2770
2866
|
sendInput: (name, text) => sessionManager.sendInput(name, text),
|
|
2771
2867
|
getTopicHistory: (topicId, limit) => {
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
timestamp:
|
|
2777
|
-
}
|
|
2868
|
+
// Check if this is a Slack synthetic ID
|
|
2869
|
+
const slackChId = slackProxyChannelMap.get(topicId);
|
|
2870
|
+
if (slackChId && _slackAdapter) {
|
|
2871
|
+
const msgs = _slackAdapter.getChannelMessages(slackChId, limit);
|
|
2872
|
+
return msgs.map(m => ({ text: m.text, fromUser: true, timestamp: new Date(parseFloat(m.ts) * 1000).toISOString() }));
|
|
2873
|
+
}
|
|
2874
|
+
if (telegram) {
|
|
2875
|
+
const entries = telegram.getTopicHistory(topicId, limit);
|
|
2876
|
+
return entries.map(e => ({ text: e.text, fromUser: e.fromUser, timestamp: e.timestamp }));
|
|
2877
|
+
}
|
|
2878
|
+
return [];
|
|
2879
|
+
},
|
|
2880
|
+
sendToTopic: async (topicId, text) => {
|
|
2881
|
+
const slackChId = slackProxyChannelMap.get(topicId);
|
|
2882
|
+
if (slackChId && _slackAdapter) {
|
|
2883
|
+
await _slackAdapter.sendToChannel(slackChId, text);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
if (telegram)
|
|
2887
|
+
await telegram.sendToTopic(topicId, text);
|
|
2888
|
+
},
|
|
2889
|
+
respawnSession: (name, topicId, options) => {
|
|
2890
|
+
if (telegram) {
|
|
2891
|
+
return respawnSessionForTopic(sessionManager, telegram, name, topicId, undefined, topicMemory, undefined, undefined, options);
|
|
2892
|
+
}
|
|
2893
|
+
// Slack respawn: kill and let next message trigger fresh session
|
|
2894
|
+
const stuckSession = sessionManager.listRunningSessions().find(s => s.tmuxSession === name);
|
|
2895
|
+
if (stuckSession)
|
|
2896
|
+
sessionManager.killSession(stuckSession.id);
|
|
2897
|
+
return Promise.resolve();
|
|
2898
|
+
},
|
|
2899
|
+
clearStallForTopic: (topicId) => {
|
|
2900
|
+
const slackChId = slackProxyChannelMap.get(topicId);
|
|
2901
|
+
if (slackChId && _slackAdapter) {
|
|
2902
|
+
_slackAdapter.clearStallTracking(slackChId);
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
if (telegram)
|
|
2906
|
+
telegram.clearStallTracking(topicId);
|
|
2778
2907
|
},
|
|
2779
|
-
sendToTopic: (topicId, text) => telegram.sendToTopic(topicId, text),
|
|
2780
|
-
respawnSession: (name, topicId, options) => respawnSessionForTopic(sessionManager, telegram, name, topicId, undefined, topicMemory, undefined, undefined, options),
|
|
2781
|
-
clearStallForTopic: (topicId) => telegram.clearStallTracking(topicId),
|
|
2782
2908
|
}, {
|
|
2783
2909
|
config: config.monitoring.triage,
|
|
2784
2910
|
state,
|
|
2785
2911
|
intelligence: sharedIntelligence,
|
|
2786
2912
|
});
|
|
2787
|
-
// Wire nurse into
|
|
2913
|
+
// Wire nurse into stall detection — both Telegram and Slack
|
|
2788
2914
|
// Note: presenceProxy may be set later — use late-binding check
|
|
2789
|
-
|
|
2915
|
+
const stallTriageHandler = async (topicId, sessionName, messageText, injectedAt) => {
|
|
2790
2916
|
// If PresenceProxy Tier 3 is actively handling this topic, defer to it
|
|
2791
2917
|
if (presenceProxy) {
|
|
2792
2918
|
const proxyState = presenceProxy.getState(topicId);
|
|
@@ -2801,27 +2927,58 @@ export async function startServer(options) {
|
|
|
2801
2927
|
const result = await triageNurse.triage(topicId, sessionName, messageText, injectedAt, 'telegram_stall');
|
|
2802
2928
|
return { resolved: result.resolved };
|
|
2803
2929
|
};
|
|
2930
|
+
if (telegram) {
|
|
2931
|
+
telegram.onStallDetected = stallTriageHandler;
|
|
2932
|
+
}
|
|
2804
2933
|
console.log(pc.green(' Stall Triage Nurse enabled'));
|
|
2805
2934
|
}
|
|
2806
2935
|
// TriageOrchestrator — next-gen session recovery with scoped Claude Code sessions
|
|
2936
|
+
// Platform-aware: works with Telegram topics AND Slack channels
|
|
2807
2937
|
let triageOrchestrator;
|
|
2808
|
-
if (config.monitoring.triageOrchestrator?.enabled && telegram) {
|
|
2938
|
+
if (config.monitoring.triageOrchestrator?.enabled && (telegram || _slackAdapter)) {
|
|
2809
2939
|
triageOrchestrator = new TriageOrchestrator({
|
|
2810
2940
|
captureSessionOutput: (name, lines) => sessionManager.captureOutput(name, lines),
|
|
2811
2941
|
isSessionAlive: (name) => sessionManager.isSessionAlive(name),
|
|
2812
2942
|
sendKey: (name, key) => sessionManager.sendKey(name, key),
|
|
2813
2943
|
sendInput: (name, text) => sessionManager.sendInput(name, text),
|
|
2814
2944
|
getTopicHistory: (topicId, limit) => {
|
|
2815
|
-
const
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2945
|
+
const slackChId = slackProxyChannelMap.get(topicId);
|
|
2946
|
+
if (slackChId && _slackAdapter) {
|
|
2947
|
+
const msgs = _slackAdapter.getChannelMessages(slackChId, limit);
|
|
2948
|
+
return msgs.map(m => ({ text: m.text, fromUser: true, timestamp: new Date(parseFloat(m.ts) * 1000).toISOString() }));
|
|
2949
|
+
}
|
|
2950
|
+
if (telegram) {
|
|
2951
|
+
const entries = telegram.getTopicHistory(topicId, limit);
|
|
2952
|
+
return entries.map(e => ({ text: e.text, fromUser: e.fromUser, timestamp: e.timestamp }));
|
|
2953
|
+
}
|
|
2954
|
+
return [];
|
|
2955
|
+
},
|
|
2956
|
+
sendToTopic: async (topicId, text) => {
|
|
2957
|
+
const slackChId = slackProxyChannelMap.get(topicId);
|
|
2958
|
+
if (slackChId && _slackAdapter) {
|
|
2959
|
+
await _slackAdapter.sendToChannel(slackChId, text);
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
if (telegram)
|
|
2963
|
+
await telegram.sendToTopic(topicId, text);
|
|
2964
|
+
},
|
|
2965
|
+
respawnSession: (name, topicId, options) => {
|
|
2966
|
+
if (telegram)
|
|
2967
|
+
return respawnSessionForTopic(sessionManager, telegram, name, topicId, undefined, topicMemory, undefined, undefined, options);
|
|
2968
|
+
const stuckSession = sessionManager.listRunningSessions().find(s => s.tmuxSession === name);
|
|
2969
|
+
if (stuckSession)
|
|
2970
|
+
sessionManager.killSession(stuckSession.id);
|
|
2971
|
+
return Promise.resolve();
|
|
2972
|
+
},
|
|
2973
|
+
clearStallForTopic: (topicId) => {
|
|
2974
|
+
const slackChId = slackProxyChannelMap.get(topicId);
|
|
2975
|
+
if (slackChId && _slackAdapter) {
|
|
2976
|
+
_slackAdapter.clearStallTracking(slackChId);
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
if (telegram)
|
|
2980
|
+
telegram.clearStallTracking(topicId);
|
|
2821
2981
|
},
|
|
2822
|
-
sendToTopic: (topicId, text) => telegram.sendToTopic(topicId, text),
|
|
2823
|
-
respawnSession: (name, topicId, options) => respawnSessionForTopic(sessionManager, telegram, name, topicId, undefined, topicMemory, undefined, undefined, options),
|
|
2824
|
-
clearStallForTopic: (topicId) => telegram.clearStallTracking(topicId),
|
|
2825
2982
|
spawnTriageSession: (name, options) => sessionManager.spawnTriageSession(name, options),
|
|
2826
2983
|
getTriageSessionUuid: (sessionName) => {
|
|
2827
2984
|
return _topicResumeMap?.findUuidForSession(sessionName) ?? undefined;
|
|
@@ -2862,33 +3019,37 @@ export async function startServer(options) {
|
|
|
2862
3019
|
state,
|
|
2863
3020
|
});
|
|
2864
3021
|
// TriageOrchestrator takes over stall detection from StallTriageNurse
|
|
2865
|
-
|
|
3022
|
+
const triageStallHandler = async (topicId, sessionName, messageText, injectedAt) => {
|
|
2866
3023
|
const result = await triageOrchestrator.activate(topicId, sessionName, 'stall_detector', messageText, injectedAt);
|
|
2867
3024
|
return { resolved: result.resolved };
|
|
2868
3025
|
};
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
origClearStall(
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
telegram.onGetTriageStatus = (topicId) => {
|
|
2877
|
-
const ts = triageOrchestrator.getTriageState(topicId);
|
|
2878
|
-
if (!ts)
|
|
2879
|
-
return null;
|
|
2880
|
-
return {
|
|
2881
|
-
active: true,
|
|
2882
|
-
classification: ts.classification,
|
|
2883
|
-
checkCount: ts.checkCount,
|
|
2884
|
-
lastCheck: new Date(ts.lastCheckAt).toISOString(),
|
|
3026
|
+
if (telegram) {
|
|
3027
|
+
telegram.onStallDetected = triageStallHandler;
|
|
3028
|
+
// Cancel triage when stall tracking clears (session responded)
|
|
3029
|
+
const origClearStall = telegram.clearStallTracking.bind(telegram);
|
|
3030
|
+
telegram.clearStallTracking = (topicId) => {
|
|
3031
|
+
origClearStall(topicId);
|
|
3032
|
+
triageOrchestrator.onTargetSessionResponded(topicId);
|
|
2885
3033
|
};
|
|
2886
|
-
|
|
3034
|
+
// Wire /triage command
|
|
3035
|
+
telegram.onGetTriageStatus = (topicId) => {
|
|
3036
|
+
const ts = triageOrchestrator.getTriageState(topicId);
|
|
3037
|
+
if (!ts)
|
|
3038
|
+
return null;
|
|
3039
|
+
return {
|
|
3040
|
+
active: true,
|
|
3041
|
+
classification: ts.classification,
|
|
3042
|
+
checkCount: ts.checkCount,
|
|
3043
|
+
lastCheck: new Date(ts.lastCheckAt).toISOString(),
|
|
3044
|
+
};
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
2887
3047
|
console.log(pc.green(' Triage Orchestrator enabled (replaces Stall Triage Nurse for stall detection)'));
|
|
2888
3048
|
}
|
|
2889
3049
|
// SessionRecovery — fast mechanical recovery (JSONL analysis, no LLM)
|
|
3050
|
+
// Platform-aware: works with Telegram topics AND Slack channels
|
|
2890
3051
|
let sessionRecovery;
|
|
2891
|
-
if (telegram) {
|
|
3052
|
+
if (telegram || _slackAdapter) {
|
|
2892
3053
|
sessionRecovery = new SessionRecovery({ enabled: true, projectDir: config.projectDir }, {
|
|
2893
3054
|
isSessionAlive: (name) => sessionManager.isSessionAlive(name),
|
|
2894
3055
|
getPanePid: (name) => {
|
|
@@ -2904,6 +3065,13 @@ export async function startServer(options) {
|
|
|
2904
3065
|
}
|
|
2905
3066
|
},
|
|
2906
3067
|
killSession: (name) => {
|
|
3068
|
+
// Route through SessionManager to fire beforeSessionKill hook
|
|
3069
|
+
const session = sessionManager.listRunningSessions().find(s => s.tmuxSession === name);
|
|
3070
|
+
if (session) {
|
|
3071
|
+
sessionManager.killSession(session.id);
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
// Fallback: direct tmux kill for untracked sessions
|
|
2907
3075
|
try {
|
|
2908
3076
|
const tmux = detectTmuxPath();
|
|
2909
3077
|
if (!tmux)
|
|
@@ -2913,32 +3081,86 @@ export async function startServer(options) {
|
|
|
2913
3081
|
catch { /* may already be dead */ }
|
|
2914
3082
|
},
|
|
2915
3083
|
respawnSession: async (topicId, _sessionName, recoveryPrompt) => {
|
|
2916
|
-
|
|
2917
|
-
|
|
3084
|
+
// Check Slack first (synthetic IDs are negative)
|
|
3085
|
+
const slackChId = slackProxyChannelMap.get(topicId);
|
|
3086
|
+
if (slackChId && _slackAdapter) {
|
|
3087
|
+
// Slack respawn: kill existing, next message triggers fresh session
|
|
3088
|
+
const session = sessionManager.listRunningSessions().find(s => s.tmuxSession === _sessionName);
|
|
3089
|
+
if (session)
|
|
3090
|
+
sessionManager.killSession(session.id);
|
|
2918
3091
|
return;
|
|
2919
|
-
|
|
3092
|
+
}
|
|
3093
|
+
if (telegram) {
|
|
3094
|
+
const targetSession = telegram.getSessionForTopic(topicId);
|
|
3095
|
+
if (!targetSession)
|
|
3096
|
+
return;
|
|
3097
|
+
await respawnSessionForTopic(sessionManager, telegram, targetSession, topicId, undefined, topicMemory, undefined, recoveryPrompt, { silent: true });
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
sendToTopic: async (topicId, message) => {
|
|
3101
|
+
const slackChId = slackProxyChannelMap.get(topicId);
|
|
3102
|
+
if (slackChId && _slackAdapter) {
|
|
3103
|
+
await _slackAdapter.sendToChannel(slackChId, message);
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
if (telegram)
|
|
3107
|
+
await telegram.sendToTopic(topicId, message);
|
|
2920
3108
|
},
|
|
2921
|
-
sendToTopic: async (topicId, message) => { await telegram.sendToTopic(topicId, message); },
|
|
2922
3109
|
});
|
|
2923
3110
|
console.log(pc.green(' Session Recovery enabled (mechanical fast-path)'));
|
|
2924
3111
|
}
|
|
2925
3112
|
// SessionMonitor — proactive session health monitoring
|
|
3113
|
+
// Platform-aware: monitors both Telegram and Slack sessions
|
|
2926
3114
|
let sessionMonitor;
|
|
2927
|
-
if (telegram) {
|
|
3115
|
+
if (telegram || _slackAdapter) {
|
|
2928
3116
|
sessionMonitor = new SessionMonitor({
|
|
2929
|
-
getActiveTopicSessions: () =>
|
|
3117
|
+
getActiveTopicSessions: () => {
|
|
3118
|
+
const sessions = new Map();
|
|
3119
|
+
// Telegram topic sessions
|
|
3120
|
+
if (telegram && telegram.getActiveTopicSessions) {
|
|
3121
|
+
const telegramSessions = telegram.getActiveTopicSessions();
|
|
3122
|
+
for (const [topicId, sessionName] of telegramSessions) {
|
|
3123
|
+
sessions.set(topicId, sessionName);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
// Slack channel sessions (using synthetic IDs)
|
|
3127
|
+
if (_slackAdapter) {
|
|
3128
|
+
const registry = _slackAdapter.getChannelRegistry();
|
|
3129
|
+
for (const [channelId, entry] of Object.entries(registry)) {
|
|
3130
|
+
const syntheticId = slackChannelToSyntheticId(channelId);
|
|
3131
|
+
sessions.set(syntheticId, entry.sessionName);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
return sessions;
|
|
3135
|
+
},
|
|
2930
3136
|
captureSessionOutput: (name, lines) => sessionManager.captureOutput(name, lines),
|
|
2931
3137
|
isSessionAlive: (name) => sessionManager.isSessionAlive(name),
|
|
2932
3138
|
getTopicHistory: (topicId, limit) => {
|
|
2933
|
-
const
|
|
2934
|
-
if (
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
3139
|
+
const slackChId = slackProxyChannelMap.get(topicId);
|
|
3140
|
+
if (slackChId && _slackAdapter) {
|
|
3141
|
+
const msgs = _slackAdapter.getChannelMessages(slackChId, limit);
|
|
3142
|
+
return msgs.map(m => ({ text: m.text, fromUser: true, timestamp: new Date(parseFloat(m.ts) * 1000).toISOString() }));
|
|
3143
|
+
}
|
|
3144
|
+
if (telegram) {
|
|
3145
|
+
const history = telegram.getMessageLog?.();
|
|
3146
|
+
if (!history)
|
|
3147
|
+
return [];
|
|
3148
|
+
return history
|
|
3149
|
+
.filter((m) => m.topicId === topicId)
|
|
3150
|
+
.slice(-limit)
|
|
3151
|
+
.map((m) => ({ text: m.text, fromUser: m.fromUser, timestamp: m.timestamp }));
|
|
3152
|
+
}
|
|
3153
|
+
return [];
|
|
3154
|
+
},
|
|
3155
|
+
sendToTopic: async (topicId, text) => {
|
|
3156
|
+
const slackChId = slackProxyChannelMap.get(topicId);
|
|
3157
|
+
if (slackChId && _slackAdapter) {
|
|
3158
|
+
await _slackAdapter.sendToChannel(slackChId, text);
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
if (telegram)
|
|
3162
|
+
await telegram.sendToTopic(topicId, text);
|
|
2940
3163
|
},
|
|
2941
|
-
sendToTopic: (topicId, text) => telegram.sendToTopic(topicId, text),
|
|
2942
3164
|
triggerTriage: triageOrchestrator
|
|
2943
3165
|
? async (topicId, sessionName, reason) => {
|
|
2944
3166
|
const result = await triageOrchestrator.activate(topicId, sessionName, 'watchdog', reason, Date.now());
|
|
@@ -4336,23 +4558,38 @@ export async function startServer(options) {
|
|
|
4336
4558
|
// 1. Resume entries are consumed (removed) on spawn
|
|
4337
4559
|
// 2. Proactive save may not have run yet
|
|
4338
4560
|
// 3. beforeSessionKill doesn't fire for bulk process exit
|
|
4339
|
-
if (_topicResumeMap
|
|
4561
|
+
if (_topicResumeMap) {
|
|
4340
4562
|
try {
|
|
4341
4563
|
const runningSessions = sessionManager.listRunningSessions();
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
const
|
|
4564
|
+
let saved = 0;
|
|
4565
|
+
// Save Telegram topic resume UUIDs
|
|
4566
|
+
if (telegram) {
|
|
4567
|
+
const topicSessions = telegram.getAllTopicSessions?.();
|
|
4568
|
+
if (topicSessions) {
|
|
4569
|
+
for (const [topicId, sessionName] of topicSessions) {
|
|
4570
|
+
const session = runningSessions.find(s => s.tmuxSession === sessionName);
|
|
4571
|
+
const uuid = _topicResumeMap.findUuidForSession(sessionName, session?.claudeSessionId ?? undefined);
|
|
4572
|
+
if (uuid) {
|
|
4573
|
+
_topicResumeMap.save(topicId, uuid, sessionName);
|
|
4574
|
+
saved++;
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
// Save Slack channel resume UUIDs
|
|
4580
|
+
if (_slackAdapter) {
|
|
4581
|
+
const registry = _slackAdapter.getChannelRegistry();
|
|
4582
|
+
for (const [channelId, entry] of Object.entries(registry)) {
|
|
4583
|
+
const session = runningSessions.find(s => s.tmuxSession === entry.sessionName);
|
|
4584
|
+
const uuid = _topicResumeMap.findUuidForSession(entry.sessionName, session?.claudeSessionId ?? undefined);
|
|
4348
4585
|
if (uuid) {
|
|
4349
|
-
|
|
4586
|
+
_slackAdapter.saveChannelResume(channelId, uuid, entry.sessionName);
|
|
4350
4587
|
saved++;
|
|
4351
4588
|
}
|
|
4352
4589
|
}
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
}
|
|
4590
|
+
}
|
|
4591
|
+
if (saved > 0) {
|
|
4592
|
+
console.log(`[shutdown] Saved ${saved} resume UUID(s) for active sessions`);
|
|
4356
4593
|
}
|
|
4357
4594
|
}
|
|
4358
4595
|
catch (err) {
|