@hoverlover/cc-discord 0.5.3 → 0.5.5
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/package.json +1 -1
- package/scripts/orchestrator.sh +77 -26
- package/server/db.ts +22 -0
- package/server/index.ts +12 -1
package/package.json
CHANGED
package/scripts/orchestrator.sh
CHANGED
|
@@ -97,7 +97,7 @@ discover_channels_lines() {
|
|
|
97
97
|
local response
|
|
98
98
|
response=$(curl -s --max-time 10 \
|
|
99
99
|
-H "x-api-token: ${RELAY_API_TOKEN}" \
|
|
100
|
-
"${RELAY_URL}/api/channels" 2>/dev/null) || {
|
|
100
|
+
"${RELAY_URL}/api/channels?include_threads=true" 2>/dev/null) || {
|
|
101
101
|
log "WARNING: Failed to reach relay at ${RELAY_URL}"
|
|
102
102
|
return
|
|
103
103
|
}
|
|
@@ -299,33 +299,84 @@ done < <(discover_channels_lines)
|
|
|
299
299
|
|
|
300
300
|
log "Initial spawn complete (${#KNOWN_CHANNEL_IDS[@]} channels). Entering health check loop."
|
|
301
301
|
|
|
302
|
-
#
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
# Check for unread messages with no active agent (e.g. new threads).
|
|
303
|
+
# Spawns agents immediately so users don't wait for the next health check.
|
|
304
|
+
UNSERVICED_CHECK_INTERVAL="${UNSERVICED_CHECK_INTERVAL:-5}"
|
|
305
305
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
306
|
+
check_unserviced() {
|
|
307
|
+
local response
|
|
308
|
+
response=$(curl -s --max-time 5 \
|
|
309
|
+
-H "x-api-token: ${RELAY_API_TOKEN}" \
|
|
310
|
+
"${RELAY_URL}/api/unserviced" 2>/dev/null) || return
|
|
311
|
+
|
|
312
|
+
local targets
|
|
313
|
+
targets=$(echo "$response" | bun -e "
|
|
314
|
+
const input = await Bun.stdin.text();
|
|
315
|
+
try {
|
|
316
|
+
const data = JSON.parse(input);
|
|
317
|
+
if (data.success && Array.isArray(data.targets)) {
|
|
318
|
+
for (const t of data.targets) {
|
|
319
|
+
// Only spawn for snowflake IDs (channels/threads), not agent names
|
|
320
|
+
if (/^\d{15,22}$/.test(t.toAgent)) {
|
|
321
|
+
console.log(t.toAgent);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch {}
|
|
326
|
+
" 2>/dev/null) || return
|
|
318
327
|
|
|
319
|
-
|
|
320
|
-
check_stuck_agents
|
|
328
|
+
[ -z "$targets" ] && return
|
|
321
329
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
start_channel_agent "$channel_id" "$channel_name"
|
|
330
|
+
while IFS= read -r target_id; do
|
|
331
|
+
[ -z "$target_id" ] && continue
|
|
332
|
+
_idx=$(find_channel_index "$target_id")
|
|
333
|
+
if [ "$_idx" -ge 0 ]; then
|
|
334
|
+
# Agent exists but hasn't polled yet — skip
|
|
335
|
+
continue
|
|
329
336
|
fi
|
|
330
|
-
|
|
337
|
+
log "Unserviced messages for ${target_id} — spawning agent"
|
|
338
|
+
start_channel_agent "$target_id" "thread-${target_id}"
|
|
339
|
+
done <<< "$targets"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
# Health check loop — runs unserviced check every UNSERVICED_CHECK_INTERVAL seconds,
|
|
343
|
+
# full health check every HEALTH_CHECK_INTERVAL seconds.
|
|
344
|
+
SECONDS_SINCE_HEALTH=0
|
|
345
|
+
while true; do
|
|
346
|
+
sleep "$UNSERVICED_CHECK_INTERVAL"
|
|
347
|
+
SECONDS_SINCE_HEALTH=$((SECONDS_SINCE_HEALTH + UNSERVICED_CHECK_INTERVAL))
|
|
348
|
+
|
|
349
|
+
# Fast check: spawn agents for unserviced threads/channels
|
|
350
|
+
check_unserviced
|
|
351
|
+
|
|
352
|
+
# Full health check on the slower interval
|
|
353
|
+
if [ "$SECONDS_SINCE_HEALTH" -ge "$HEALTH_CHECK_INTERVAL" ]; then
|
|
354
|
+
SECONDS_SINCE_HEALTH=0
|
|
355
|
+
|
|
356
|
+
# Check for dead agents and restart them
|
|
357
|
+
for i in "${!KNOWN_CHANNEL_IDS[@]}"; do
|
|
358
|
+
_pid="${KNOWN_CHANNEL_PIDS[$i]}"
|
|
359
|
+
if ! is_agent_alive "$_pid"; then
|
|
360
|
+
_name="${KNOWN_CHANNEL_NAMES[$i]}"
|
|
361
|
+
_cid="${KNOWN_CHANNEL_IDS[$i]}"
|
|
362
|
+
wait "$_pid" 2>/dev/null || true
|
|
363
|
+
log "Agent #${_name} (${_cid}) exited. Restarting in ${AGENT_RESTART_DELAY}s..."
|
|
364
|
+
sleep "$AGENT_RESTART_DELAY"
|
|
365
|
+
start_channel_agent "$_cid" "$_name"
|
|
366
|
+
fi
|
|
367
|
+
done
|
|
368
|
+
|
|
369
|
+
# Check for stuck agents (alive but not polling)
|
|
370
|
+
check_stuck_agents
|
|
371
|
+
|
|
372
|
+
# Check for new channels/threads
|
|
373
|
+
while IFS=' ' read -r channel_id channel_name; do
|
|
374
|
+
[ -z "$channel_id" ] && continue
|
|
375
|
+
_idx=$(find_channel_index "$channel_id")
|
|
376
|
+
if [ "$_idx" -lt 0 ]; then
|
|
377
|
+
log "New channel discovered: #${channel_name} (${channel_id})"
|
|
378
|
+
start_channel_agent "$channel_id" "$channel_name"
|
|
379
|
+
fi
|
|
380
|
+
done < <(discover_channels_lines)
|
|
381
|
+
fi
|
|
331
382
|
done
|
package/server/db.ts
CHANGED
|
@@ -227,6 +227,28 @@ export function insertTraceEvent(
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Find message targets (to_agent) that have unread messages but no agent
|
|
232
|
+
* has ever polled for them (no agent_activity record). These are typically
|
|
233
|
+
* new threads that need an agent spawned.
|
|
234
|
+
*/
|
|
235
|
+
export function getUnservicedTargets(sessionId: string): { toAgent: string; unreadCount: number; oldestAt: string }[] {
|
|
236
|
+
try {
|
|
237
|
+
const rows = db
|
|
238
|
+
.prepare(`
|
|
239
|
+
SELECT m.to_agent, COUNT(*) as unread_count, MIN(m.created_at) as oldest_at
|
|
240
|
+
FROM messages m
|
|
241
|
+
LEFT JOIN agent_activity a ON a.agent_id = m.to_agent AND a.session_id = m.session_id
|
|
242
|
+
WHERE m.session_id = ? AND m.read = 0 AND a.agent_id IS NULL
|
|
243
|
+
GROUP BY m.to_agent
|
|
244
|
+
`)
|
|
245
|
+
.all(sessionId) as any[];
|
|
246
|
+
return rows.map((r) => ({ toAgent: r.to_agent, unreadCount: r.unread_count, oldestAt: r.oldest_at }));
|
|
247
|
+
} catch {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
230
252
|
/**
|
|
231
253
|
* Get health status for all agents in a session.
|
|
232
254
|
* Returns each agent's last heartbeat time, status, and whether it
|
package/server/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
TYPING_MAX_MS,
|
|
28
28
|
validateConfig,
|
|
29
29
|
} from "./config.ts";
|
|
30
|
-
import { clearChannelModel, db, getAgentHealthAll, getChannelModel, isTraceThread, setChannelModel } from "./db.ts";
|
|
30
|
+
import { clearChannelModel, db, getAgentHealthAll, getChannelModel, getUnservicedTargets, isTraceThread, setChannelModel } from "./db.ts";
|
|
31
31
|
import { memoryStore } from "./memory.ts";
|
|
32
32
|
import { persistInboundDiscordMessage, persistOutboundDiscordMessage } from "./messages.ts";
|
|
33
33
|
import { startTraceFlushLoop, stopTraceFlushLoop } from "./trace-thread.ts";
|
|
@@ -253,6 +253,17 @@ app.get("/api/agent-health", (req: Request, res: Response) => {
|
|
|
253
253
|
}
|
|
254
254
|
});
|
|
255
255
|
|
|
256
|
+
app.get("/api/unserviced", (req: Request, res: Response) => {
|
|
257
|
+
try {
|
|
258
|
+
if (!requireAuth(req, res)) return;
|
|
259
|
+
const targets = getUnservicedTargets(DISCORD_SESSION_ID);
|
|
260
|
+
res.json({ success: true, targets });
|
|
261
|
+
} catch (err: unknown) {
|
|
262
|
+
console.error("[Relay] /api/unserviced failed:", err);
|
|
263
|
+
res.status(500).json({ success: false, error: (err as Error).message });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
256
267
|
app.post("/api/send", async (req: Request, res: Response) => {
|
|
257
268
|
try {
|
|
258
269
|
if (!requireAuth(req, res)) return;
|