pilotlynx 0.1.2 → 0.2.0
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/README.md +40 -40
- package/dist/agents/improve.agent.d.ts +42 -1
- package/dist/agents/improve.agent.js +102 -7
- package/dist/agents/improve.agent.js.map +1 -1
- package/dist/agents/run.agent.d.ts +0 -1
- package/dist/agents/run.agent.js +10 -7
- package/dist/agents/run.agent.js.map +1 -1
- package/dist/cli.js +8 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.js +51 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/eval.d.ts +2 -0
- package/dist/commands/eval.js +47 -0
- package/dist/commands/eval.js.map +1 -0
- package/dist/commands/improve.js +83 -5
- package/dist/commands/improve.js.map +1 -1
- package/dist/commands/init.js +17 -27
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/insights.js +63 -0
- package/dist/commands/insights.js.map +1 -1
- package/dist/commands/logs.d.ts +1 -0
- package/dist/commands/logs.js +23 -1
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/relay.js +250 -124
- package/dist/commands/relay.js.map +1 -1
- package/dist/commands/run.js +21 -2
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/schedule.js +7 -1
- package/dist/commands/schedule.js.map +1 -1
- package/dist/commands/status.js +9 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/lib/agent-runner.d.ts +5 -1
- package/dist/lib/agent-runner.js +41 -1
- package/dist/lib/agent-runner.js.map +1 -1
- package/dist/lib/audit.d.ts +7 -0
- package/dist/lib/audit.js +63 -0
- package/dist/lib/audit.js.map +1 -0
- package/dist/lib/callbacks.d.ts +12 -1
- package/dist/lib/callbacks.js +147 -19
- package/dist/lib/callbacks.js.map +1 -1
- package/dist/lib/command-ops/doctor-ops.js +41 -6
- package/dist/lib/command-ops/doctor-ops.js.map +1 -1
- package/dist/lib/command-ops/eval-ops.d.ts +7 -0
- package/dist/lib/command-ops/eval-ops.js +111 -0
- package/dist/lib/command-ops/eval-ops.js.map +1 -0
- package/dist/lib/command-ops/improve-ops.d.ts +14 -1
- package/dist/lib/command-ops/improve-ops.js +369 -17
- package/dist/lib/command-ops/improve-ops.js.map +1 -1
- package/dist/lib/command-ops/run-ops.d.ts +8 -1
- package/dist/lib/command-ops/run-ops.js +25 -4
- package/dist/lib/command-ops/run-ops.js.map +1 -1
- package/dist/lib/command-ops/secrets-migration-ops.js +1 -1
- package/dist/lib/command-ops/secrets-migration-ops.js.map +1 -1
- package/dist/lib/command-ops/status-ops.d.ts +1 -0
- package/dist/lib/command-ops/status-ops.js +19 -7
- package/dist/lib/command-ops/status-ops.js.map +1 -1
- package/dist/lib/config.js +3 -3
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/cron.d.ts +1 -1
- package/dist/lib/cron.js +2 -2
- package/dist/lib/cron.js.map +1 -1
- package/dist/lib/global-config.js +1 -1
- package/dist/lib/global-config.js.map +1 -1
- package/dist/lib/observation.d.ts +60 -2
- package/dist/lib/observation.js +261 -13
- package/dist/lib/observation.js.map +1 -1
- package/dist/lib/registry.d.ts +0 -1
- package/dist/lib/registry.js +1 -1
- package/dist/lib/registry.js.map +1 -1
- package/dist/lib/relay/admin.d.ts +29 -0
- package/dist/lib/relay/admin.js +176 -0
- package/dist/lib/relay/admin.js.map +1 -0
- package/dist/lib/relay/bindings.d.ts +8 -0
- package/dist/lib/relay/bindings.js +50 -0
- package/dist/lib/relay/bindings.js.map +1 -0
- package/dist/lib/relay/config.d.ts +3 -3
- package/dist/lib/relay/config.js +18 -10
- package/dist/lib/relay/config.js.map +1 -1
- package/dist/lib/relay/context.d.ts +31 -0
- package/dist/lib/relay/context.js +118 -0
- package/dist/lib/relay/context.js.map +1 -0
- package/dist/lib/relay/db.d.ts +38 -0
- package/dist/lib/relay/db.js +252 -0
- package/dist/lib/relay/db.js.map +1 -0
- package/dist/lib/relay/executor.d.ts +2 -0
- package/dist/lib/relay/executor.js +108 -0
- package/dist/lib/relay/executor.js.map +1 -0
- package/dist/lib/relay/feedback.d.ts +29 -0
- package/dist/lib/relay/feedback.js +142 -0
- package/dist/lib/relay/feedback.js.map +1 -0
- package/dist/lib/relay/notifier.d.ts +30 -0
- package/dist/lib/relay/notifier.js +78 -0
- package/dist/lib/relay/notifier.js.map +1 -0
- package/dist/lib/relay/notify.d.ts +3 -4
- package/dist/lib/relay/notify.js +43 -159
- package/dist/lib/relay/notify.js.map +1 -1
- package/dist/lib/relay/platform.d.ts +52 -0
- package/dist/lib/relay/platform.js +5 -0
- package/dist/lib/relay/platform.js.map +1 -0
- package/dist/lib/relay/platforms/slack.d.ts +37 -0
- package/dist/lib/relay/platforms/slack.js +240 -0
- package/dist/lib/relay/platforms/slack.js.map +1 -0
- package/dist/lib/relay/platforms/telegram.d.ts +29 -0
- package/dist/lib/relay/platforms/telegram.js +193 -0
- package/dist/lib/relay/platforms/telegram.js.map +1 -0
- package/dist/lib/relay/poster.d.ts +24 -0
- package/dist/lib/relay/poster.js +136 -0
- package/dist/lib/relay/poster.js.map +1 -0
- package/dist/lib/relay/queue.d.ts +18 -0
- package/dist/lib/relay/queue.js +85 -0
- package/dist/lib/relay/queue.js.map +1 -0
- package/dist/lib/relay/router.d.ts +25 -2
- package/dist/lib/relay/router.js +259 -168
- package/dist/lib/relay/router.js.map +1 -1
- package/dist/lib/relay/service.d.ts +31 -7
- package/dist/lib/relay/service.js +281 -200
- package/dist/lib/relay/service.js.map +1 -1
- package/dist/lib/relay/types.d.ts +189 -34
- package/dist/lib/relay/types.js +68 -28
- package/dist/lib/relay/types.js.map +1 -1
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +17 -2
- package/dist/lib/sandbox.js.map +1 -1
- package/dist/lib/schedule.d.ts +4 -5
- package/dist/lib/schedule.js +7 -8
- package/dist/lib/schedule.js.map +1 -1
- package/dist/lib/secrets.js +11 -1
- package/dist/lib/secrets.js.map +1 -1
- package/dist/lib/types.d.ts +80 -0
- package/dist/lib/types.js +9 -1
- package/dist/lib/types.js.map +1 -1
- package/package.json +18 -18
- package/prompts/improve.yaml +114 -6
- package/prompts/relay.yaml +29 -0
- package/prompts/run.yaml +36 -2
- package/template/CLAUDE.md +34 -5
- package/template/RUNBOOK.md +5 -5
- package/template/evals/sample.json +16 -0
- package/template/memory/MEMORY.md +6 -0
- package/template/memory/procedures/.gitkeep +0 -0
- package/template/schedule.yaml +1 -1
- package/template/workflows/daily_feedback.ts +78 -2
- package/template/workflows/project_review.ts +51 -2
- package/prompts/relay-chat.yaml +0 -24
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class AgentPool {
|
|
2
|
+
private maxConcurrent;
|
|
3
|
+
private maxQueueDepth;
|
|
4
|
+
private maxMemoryMB;
|
|
5
|
+
private projectQueues;
|
|
6
|
+
private globalSemaphore;
|
|
7
|
+
private idleTimers;
|
|
8
|
+
constructor(maxConcurrent: number, maxQueueDepth: number, maxMemoryMB: number);
|
|
9
|
+
enqueue<T>(project: string, fn: () => Promise<T>): Promise<{
|
|
10
|
+
result: Promise<T>;
|
|
11
|
+
position: number;
|
|
12
|
+
}>;
|
|
13
|
+
getQueueDepth(project: string): number;
|
|
14
|
+
getActiveCount(): number;
|
|
15
|
+
shutdown(): Promise<void>;
|
|
16
|
+
private getOrCreateQueue;
|
|
17
|
+
private resetIdleTimer;
|
|
18
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import PQueue from 'p-queue';
|
|
2
|
+
const IDLE_TIMEOUT_MS = 30 * 60_000; // 30 minutes
|
|
3
|
+
export class AgentPool {
|
|
4
|
+
maxConcurrent;
|
|
5
|
+
maxQueueDepth;
|
|
6
|
+
maxMemoryMB;
|
|
7
|
+
projectQueues = new Map();
|
|
8
|
+
globalSemaphore;
|
|
9
|
+
idleTimers = new Map();
|
|
10
|
+
constructor(maxConcurrent, maxQueueDepth, maxMemoryMB) {
|
|
11
|
+
this.maxConcurrent = maxConcurrent;
|
|
12
|
+
this.maxQueueDepth = maxQueueDepth;
|
|
13
|
+
this.maxMemoryMB = maxMemoryMB;
|
|
14
|
+
this.globalSemaphore = new PQueue({ concurrency: maxConcurrent });
|
|
15
|
+
}
|
|
16
|
+
async enqueue(project, fn) {
|
|
17
|
+
// Check memory pressure
|
|
18
|
+
const rss = process.memoryUsage().rss;
|
|
19
|
+
if (rss > this.maxMemoryMB * 1024 * 1024) {
|
|
20
|
+
throw new Error(`Memory pressure: RSS ${Math.round(rss / 1024 / 1024)}MB exceeds limit ${this.maxMemoryMB}MB`);
|
|
21
|
+
}
|
|
22
|
+
const queue = this.getOrCreateQueue(project);
|
|
23
|
+
// Check queue depth
|
|
24
|
+
const depth = queue.size + queue.pending;
|
|
25
|
+
if (depth >= this.maxQueueDepth) {
|
|
26
|
+
throw new Error(`Queue full for project "${project}": ${depth}/${this.maxQueueDepth}`);
|
|
27
|
+
}
|
|
28
|
+
const position = depth;
|
|
29
|
+
// Reset idle timer for this project
|
|
30
|
+
this.resetIdleTimer(project);
|
|
31
|
+
const result = queue.add(async () => {
|
|
32
|
+
// Wait for a global semaphore slot before running
|
|
33
|
+
return this.globalSemaphore.add(() => fn());
|
|
34
|
+
});
|
|
35
|
+
return { result, position };
|
|
36
|
+
}
|
|
37
|
+
getQueueDepth(project) {
|
|
38
|
+
const queue = this.projectQueues.get(project);
|
|
39
|
+
if (!queue)
|
|
40
|
+
return 0;
|
|
41
|
+
return queue.size + queue.pending;
|
|
42
|
+
}
|
|
43
|
+
getActiveCount() {
|
|
44
|
+
return this.globalSemaphore.pending;
|
|
45
|
+
}
|
|
46
|
+
async shutdown() {
|
|
47
|
+
// Clear all idle timers
|
|
48
|
+
for (const timer of this.idleTimers.values()) {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
this.idleTimers.clear();
|
|
52
|
+
// Clear all project queues
|
|
53
|
+
for (const queue of this.projectQueues.values()) {
|
|
54
|
+
queue.clear();
|
|
55
|
+
}
|
|
56
|
+
this.projectQueues.clear();
|
|
57
|
+
// Wait for in-flight work to complete
|
|
58
|
+
await this.globalSemaphore.onIdle();
|
|
59
|
+
this.globalSemaphore.clear();
|
|
60
|
+
}
|
|
61
|
+
getOrCreateQueue(project) {
|
|
62
|
+
let queue = this.projectQueues.get(project);
|
|
63
|
+
if (!queue) {
|
|
64
|
+
queue = new PQueue({ concurrency: 1 });
|
|
65
|
+
this.projectQueues.set(project, queue);
|
|
66
|
+
}
|
|
67
|
+
return queue;
|
|
68
|
+
}
|
|
69
|
+
resetIdleTimer(project) {
|
|
70
|
+
const existing = this.idleTimers.get(project);
|
|
71
|
+
if (existing)
|
|
72
|
+
clearTimeout(existing);
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
const queue = this.projectQueues.get(project);
|
|
75
|
+
if (queue && queue.size === 0 && queue.pending === 0) {
|
|
76
|
+
this.projectQueues.delete(project);
|
|
77
|
+
this.idleTimers.delete(project);
|
|
78
|
+
}
|
|
79
|
+
}, IDLE_TIMEOUT_MS);
|
|
80
|
+
// Don't hold the process open for idle timers
|
|
81
|
+
timer.unref();
|
|
82
|
+
this.idleTimers.set(project, timer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.js","sourceRoot":"","sources":["../../../src/lib/relay/queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,2 +1,25 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { ChatPlatform, ChatMessage } from './platform.js';
|
|
3
|
+
import type { RelayConfig } from './types.js';
|
|
4
|
+
import { AgentPool } from './queue.js';
|
|
5
|
+
export declare class RelayRouter {
|
|
6
|
+
private db;
|
|
7
|
+
private pool;
|
|
8
|
+
private config;
|
|
9
|
+
private activeAbortControllers;
|
|
10
|
+
private startedAt;
|
|
11
|
+
constructor(db: Database.Database, pool: AgentPool, config: RelayConfig);
|
|
12
|
+
/**
|
|
13
|
+
* Main message handler — called by platform adapters on incoming messages.
|
|
14
|
+
*/
|
|
15
|
+
routeMessage(platform: ChatPlatform, msg: ChatMessage): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Reaction handler — called by platform adapters on reactions.
|
|
18
|
+
*/
|
|
19
|
+
routeReaction(platform: ChatPlatform, channelId: string, messageId: string, userId: string, emoji: string): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Slash command handler — called by platform adapters.
|
|
22
|
+
*/
|
|
23
|
+
routeCommand(platform: ChatPlatform, channelId: string, userId: string, command: string, args: string): Promise<string>;
|
|
24
|
+
private executeAndPost;
|
|
25
|
+
}
|
package/dist/lib/relay/router.js
CHANGED
|
@@ -1,191 +1,282 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
// ── Relay Router ──
|
|
2
|
+
// Central dispatch: routes platform messages to the right handler.
|
|
3
|
+
// Wires together: bindings, context, queue, executor, poster, admin, feedback.
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { lookupBinding } from './bindings.js';
|
|
6
|
+
import { cacheMessage, writePendingMessage, markPendingDone, recordRelayRun, updateRelayRun } from './db.js';
|
|
7
|
+
import { assembleContext } from './context.js';
|
|
8
|
+
import { executeRelayRun } from './executor.js';
|
|
9
|
+
import { formatResponse, addCostFooter } from './poster.js';
|
|
10
|
+
import { parseCommand, handleAdminCommand } from './admin.js';
|
|
11
|
+
import { classifyReaction, handleFeedback, appendRelayFeedback, saveFeedbackToMemory, isReactionRateLimited } from './feedback.js';
|
|
12
|
+
import { sendWebhookNotification } from './notify.js';
|
|
13
|
+
// ── Rate Limiting ──
|
|
14
|
+
const userMessageTimestamps = new Map();
|
|
15
|
+
function isUserRateLimited(userId, maxPerHour) {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const cutoff = now - 3_600_000;
|
|
18
|
+
let timestamps = userMessageTimestamps.get(userId);
|
|
19
|
+
if (!timestamps) {
|
|
20
|
+
timestamps = [];
|
|
21
|
+
userMessageTimestamps.set(userId, timestamps);
|
|
22
|
+
}
|
|
23
|
+
const recent = timestamps.filter((t) => t > cutoff);
|
|
24
|
+
userMessageTimestamps.set(userId, recent);
|
|
25
|
+
if (recent.length >= maxPerHour)
|
|
13
26
|
return true;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function formatLogsSummary(project) {
|
|
17
|
-
const logs = getRecentLogs(project, 7);
|
|
18
|
-
if (logs.length === 0)
|
|
19
|
-
return `No recent logs for "${project}".`;
|
|
20
|
-
const lines = logs.slice(-10).map(r => {
|
|
21
|
-
const icon = r.success ? '\u2705' : '\u274c';
|
|
22
|
-
const time = new Date(r.startedAt).toISOString().slice(0, 16).replace('T', ' ');
|
|
23
|
-
return `${icon} ${time} ${r.workflow} ($${r.costUsd.toFixed(4)})`;
|
|
24
|
-
});
|
|
25
|
-
return `Recent runs for *${project}* (last 7 days):\n${lines.join('\n')}`;
|
|
27
|
+
recent.push(now);
|
|
28
|
+
return false;
|
|
26
29
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
// ── Router ──
|
|
31
|
+
export class RelayRouter {
|
|
32
|
+
db;
|
|
33
|
+
pool;
|
|
34
|
+
config;
|
|
35
|
+
activeAbortControllers = new Map(); // conversationId → controller
|
|
36
|
+
startedAt = new Date();
|
|
37
|
+
constructor(db, pool, config) {
|
|
38
|
+
this.db = db;
|
|
39
|
+
this.pool = pool;
|
|
40
|
+
this.config = config;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Main message handler — called by platform adapters on incoming messages.
|
|
44
|
+
*/
|
|
45
|
+
async routeMessage(platform, msg) {
|
|
46
|
+
// Ignore bot messages
|
|
47
|
+
if (msg.isBot)
|
|
40
48
|
return;
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
// Cache the incoming message
|
|
50
|
+
cacheMessage(this.db, msg);
|
|
51
|
+
// Check for admin commands first
|
|
52
|
+
const parsed = parseCommand(msg.text);
|
|
53
|
+
if (parsed) {
|
|
54
|
+
const response = await handleAdminCommand({
|
|
55
|
+
db: this.db,
|
|
56
|
+
platform: platform.name,
|
|
57
|
+
channelId: msg.channelId,
|
|
58
|
+
userId: msg.userId,
|
|
59
|
+
config: this.config,
|
|
60
|
+
getQueueDepth: (p) => this.pool.getQueueDepth(p),
|
|
61
|
+
getActiveCount: () => this.pool.getActiveCount(),
|
|
62
|
+
startedAt: this.startedAt,
|
|
63
|
+
}, parsed.command, parsed.args);
|
|
64
|
+
// Handle special commands
|
|
65
|
+
if (parsed.command === 'cancel') {
|
|
66
|
+
const controller = this.activeAbortControllers.get(msg.conversationId);
|
|
67
|
+
if (controller) {
|
|
68
|
+
controller.abort();
|
|
69
|
+
this.activeAbortControllers.delete(msg.conversationId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await platform.sendMessage(msg.channelId, response, msg.conversationId);
|
|
44
73
|
return;
|
|
45
74
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!
|
|
49
|
-
|
|
50
|
-
await adapter.send(msg.chatId, `Your chat ID is \`${rawId}\`.\n\n` +
|
|
51
|
-
`Run this to connect:\n` +
|
|
52
|
-
`\`pilotlynx relay add-chat ${rawId} --project <name>\``);
|
|
75
|
+
// Look up channel binding
|
|
76
|
+
const project = lookupBinding(this.db, platform.name, msg.channelId);
|
|
77
|
+
if (!project) {
|
|
78
|
+
await platform.sendMessage(msg.channelId, 'This channel is not bound to a project. An admin can use `bind <project>` to set one up.', msg.conversationId);
|
|
53
79
|
return;
|
|
54
80
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
// Rate limit
|
|
82
|
+
if (isUserRateLimited(msg.userId, this.config.limits.userRatePerHour)) {
|
|
83
|
+
await platform.sendMessage(msg.channelId, 'You\'re sending messages too quickly. Please slow down.', msg.conversationId);
|
|
84
|
+
return;
|
|
59
85
|
}
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
// Write pending message for crash recovery
|
|
87
|
+
const pendingId = randomUUID();
|
|
88
|
+
writePendingMessage(this.db, {
|
|
89
|
+
id: pendingId,
|
|
90
|
+
platform: platform.name,
|
|
91
|
+
channelId: msg.channelId,
|
|
92
|
+
conversationId: msg.conversationId,
|
|
93
|
+
userId: msg.userId,
|
|
94
|
+
userName: msg.userName,
|
|
95
|
+
text: msg.text,
|
|
96
|
+
receivedAt: new Date().toISOString(),
|
|
97
|
+
status: 'pending',
|
|
98
|
+
});
|
|
99
|
+
// Enqueue the agent run
|
|
100
|
+
try {
|
|
101
|
+
const { result: runPromise, position } = await this.pool.enqueue(project, async () => {
|
|
102
|
+
return this.executeAndPost(platform, msg, project, pendingId);
|
|
103
|
+
});
|
|
104
|
+
// If queued (not immediate), notify the user
|
|
105
|
+
if (position > 0) {
|
|
106
|
+
await platform.sendMessage(msg.channelId, `Your request is queued (position ${position}). I'll respond shortly.`, msg.conversationId);
|
|
107
|
+
}
|
|
108
|
+
// Don't await the run promise — it executes in the background via the queue
|
|
109
|
+
runPromise.catch((err) => {
|
|
110
|
+
console.error(`[relay] Run failed for ${project}:`, err);
|
|
111
|
+
});
|
|
62
112
|
}
|
|
63
|
-
|
|
64
|
-
|
|
113
|
+
catch (err) {
|
|
114
|
+
// Queue full or memory pressure
|
|
115
|
+
markPendingDone(this.db, pendingId);
|
|
116
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
117
|
+
await platform.sendMessage(msg.channelId, `Cannot process request: ${errMsg}`, msg.conversationId);
|
|
65
118
|
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
await adapter.send(msg.chatId, HELP_TEXT);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Reaction handler — called by platform adapters on reactions.
|
|
122
|
+
*/
|
|
123
|
+
async routeReaction(platform, channelId, messageId, userId, emoji) {
|
|
124
|
+
// Rate limit reactions
|
|
125
|
+
if (isReactionRateLimited(userId, this.config.limits.reactionRatePerHour))
|
|
74
126
|
return;
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
127
|
+
// Check for cancellation reaction (stop_sign on a "Working on it..." message)
|
|
128
|
+
if (emoji === 'stop_sign' || emoji === 'octagonal_sign') {
|
|
129
|
+
// Find conversation for this message and abort
|
|
130
|
+
for (const [convId, controller] of this.activeAbortControllers) {
|
|
131
|
+
controller.abort();
|
|
132
|
+
this.activeAbortControllers.delete(convId);
|
|
133
|
+
await platform.sendMessage(channelId, 'Run cancelled.', convId);
|
|
134
|
+
break;
|
|
83
135
|
}
|
|
84
136
|
return;
|
|
85
137
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (!project) {
|
|
89
|
-
await adapter.send(msg.chatId, 'Usage: /status <project>\nNo default project configured.');
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
await adapter.send(msg.chatId, formatLogsSummary(project));
|
|
138
|
+
const feedbackType = classifyReaction(emoji);
|
|
139
|
+
if (!feedbackType)
|
|
93
140
|
return;
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
if (!chatConfig.allowRun) {
|
|
97
|
-
await adapter.send(msg.chatId, 'Run commands are disabled for this chat.');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
let resolvedProject;
|
|
101
|
-
let resolvedWorkflow;
|
|
102
|
-
if (parts.length === 3) {
|
|
103
|
-
resolvedProject = parts[1];
|
|
104
|
-
resolvedWorkflow = parts[2];
|
|
105
|
-
}
|
|
106
|
-
else if (parts.length === 2 && chatConfig.project) {
|
|
107
|
-
resolvedProject = chatConfig.project;
|
|
108
|
-
resolvedWorkflow = parts[1];
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
await adapter.send(msg.chatId, 'Usage: /run <project> <workflow>');
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
// Acquire run lock
|
|
115
|
-
const release = await acquireRunLock(resolvedProject);
|
|
116
|
-
if (!release) {
|
|
117
|
-
await adapter.send(msg.chatId, `\u23f3 Project "${resolvedProject}" is busy. Try again shortly.`);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
let typingInterval;
|
|
121
|
-
try {
|
|
122
|
-
await adapter.sendTyping(msg.chatId);
|
|
123
|
-
typingInterval = setInterval(() => {
|
|
124
|
-
adapter.sendTyping(msg.chatId).catch(() => { });
|
|
125
|
-
}, 4000);
|
|
126
|
-
const result = await executeRun(resolvedProject, resolvedWorkflow);
|
|
127
|
-
if (result.success) {
|
|
128
|
-
const duration = result.durationMs ? `${Math.round(result.durationMs / 1000)}s` : '?';
|
|
129
|
-
await adapter.send(msg.chatId, `\u2705 *${resolvedProject}/${resolvedWorkflow}* completed\n` +
|
|
130
|
-
`Duration: ${duration} | Cost: $${result.costUsd?.toFixed(4) ?? '?'}\n` +
|
|
131
|
-
`${result.record?.summary?.slice(0, 500) ?? ''}`);
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
await adapter.send(msg.chatId, `\u274c *${resolvedProject}/${resolvedWorkflow}* failed\n${result.error?.slice(0, 500) ?? 'Unknown error'}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
finally {
|
|
138
|
-
clearInterval(typingInterval);
|
|
139
|
-
await release();
|
|
140
|
-
}
|
|
141
|
+
const project = lookupBinding(this.db, platform.name, channelId);
|
|
142
|
+
if (!project)
|
|
141
143
|
return;
|
|
144
|
+
const signal = {
|
|
145
|
+
type: feedbackType,
|
|
146
|
+
platform: platform.name,
|
|
147
|
+
conversationId: messageId, // approximate — reactions target a message
|
|
148
|
+
messageId,
|
|
149
|
+
userId,
|
|
150
|
+
userName: userId, // resolved by platform adapter if available
|
|
151
|
+
timestamp: new Date().toISOString(),
|
|
152
|
+
};
|
|
153
|
+
// Try to get agent output summary from the most recent run for this project
|
|
154
|
+
const lastRun = this.db.prepare(`SELECT id, status FROM relay_runs
|
|
155
|
+
WHERE project = ? AND platform = ? AND channel_id = ?
|
|
156
|
+
ORDER BY started_at DESC LIMIT 1`).get(project, platform.name, channelId);
|
|
157
|
+
const outputSummary = lastRun?.status === 'completed' ? `(run ${lastRun.id})` : undefined;
|
|
158
|
+
const entry = handleFeedback(signal, project, outputSummary);
|
|
159
|
+
appendRelayFeedback(entry);
|
|
160
|
+
// Save starred/saved responses to project memory/ dir
|
|
161
|
+
if (feedbackType === 'save') {
|
|
162
|
+
saveFeedbackToMemory(project, entry, outputSummary);
|
|
163
|
+
}
|
|
164
|
+
if (feedbackType === 'negative') {
|
|
165
|
+
await platform.sendMessage(channelId, 'Got it — what went wrong? Reply in this thread and I\'ll note the feedback.', messageId);
|
|
142
166
|
}
|
|
143
|
-
await adapter.send(msg.chatId, `Unknown command: ${cmd}\nUse /help for available commands.`);
|
|
144
|
-
}
|
|
145
|
-
catch (err) {
|
|
146
|
-
await adapter.send(msg.chatId, `Command error: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
async function handleChat(msg, text, chatConfig, adapter) {
|
|
150
|
-
const project = chatConfig.project;
|
|
151
|
-
if (!project) {
|
|
152
|
-
await adapter.send(msg.chatId, 'No project assigned to this chat. Use /help for commands.');
|
|
153
|
-
return;
|
|
154
167
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Slash command handler — called by platform adapters.
|
|
170
|
+
*/
|
|
171
|
+
async routeCommand(platform, channelId, userId, command, args) {
|
|
172
|
+
return handleAdminCommand({
|
|
173
|
+
db: this.db,
|
|
174
|
+
platform: platform.name,
|
|
175
|
+
channelId,
|
|
176
|
+
userId,
|
|
177
|
+
config: this.config,
|
|
178
|
+
getQueueDepth: (p) => this.pool.getQueueDepth(p),
|
|
179
|
+
getActiveCount: () => this.pool.getActiveCount(),
|
|
180
|
+
startedAt: this.startedAt,
|
|
181
|
+
}, command, args);
|
|
166
182
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
// ── Internal ──
|
|
184
|
+
async executeAndPost(platform, msg, project, pendingId) {
|
|
185
|
+
const runId = randomUUID();
|
|
186
|
+
const abortController = new AbortController();
|
|
187
|
+
this.activeAbortControllers.set(msg.conversationId, abortController);
|
|
188
|
+
// Record the run start
|
|
189
|
+
recordRelayRun(this.db, {
|
|
190
|
+
id: runId,
|
|
191
|
+
platform: platform.name,
|
|
192
|
+
channelId: msg.channelId,
|
|
193
|
+
conversationId: msg.conversationId,
|
|
194
|
+
project,
|
|
195
|
+
userId: msg.userId,
|
|
196
|
+
startedAt: new Date().toISOString(),
|
|
197
|
+
status: 'running',
|
|
198
|
+
costUsd: 0,
|
|
199
|
+
inputTokens: 0,
|
|
200
|
+
outputTokens: 0,
|
|
201
|
+
durationMs: 0,
|
|
180
202
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
203
|
+
// Start streaming indicator
|
|
204
|
+
const stream = await platform.startStream(msg.channelId, msg.conversationId);
|
|
205
|
+
try {
|
|
206
|
+
// Assemble context
|
|
207
|
+
const { prompt, isStale } = await assembleContext(this.db, platform, msg.channelId, msg.conversationId, msg.text, msg.userName, project, this.config.context);
|
|
208
|
+
if (isStale) {
|
|
209
|
+
await platform.sendMessage(msg.channelId, `_Thread inactive for ${this.config.context.staleThreadDays}+ days. Starting fresh context._`, msg.conversationId);
|
|
210
|
+
}
|
|
211
|
+
// Execute agent run
|
|
212
|
+
const request = {
|
|
213
|
+
platform: platform.name,
|
|
214
|
+
channelId: msg.channelId,
|
|
215
|
+
conversationId: msg.conversationId,
|
|
216
|
+
userId: msg.userId,
|
|
217
|
+
userName: msg.userName,
|
|
218
|
+
project,
|
|
219
|
+
prompt,
|
|
220
|
+
abortSignal: abortController.signal,
|
|
221
|
+
onText: (text) => {
|
|
222
|
+
stream.append(text).catch(() => { });
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const result = await executeRelayRun(request);
|
|
226
|
+
// Stop streaming and post final response
|
|
227
|
+
await stream.stop();
|
|
228
|
+
// Format and split response
|
|
229
|
+
const maxLen = platform.capabilities.maxMessageLength;
|
|
230
|
+
const parts = formatResponse(result.text, maxLen);
|
|
231
|
+
for (const part of parts) {
|
|
232
|
+
await platform.sendMessage(msg.channelId, part, msg.conversationId);
|
|
233
|
+
}
|
|
234
|
+
// Git diff summary
|
|
235
|
+
if (result.gitDiffStat) {
|
|
236
|
+
await platform.sendMessage(msg.channelId, `\`\`\`\n${result.gitDiffStat}\n\`\`\``, msg.conversationId);
|
|
237
|
+
}
|
|
238
|
+
// Cost footer
|
|
239
|
+
const footer = addCostFooter(result);
|
|
240
|
+
await platform.sendMessage(msg.channelId, footer, msg.conversationId);
|
|
241
|
+
// Update run record
|
|
242
|
+
const completedAt = new Date().toISOString();
|
|
243
|
+
updateRelayRun(this.db, runId, {
|
|
244
|
+
completedAt,
|
|
245
|
+
status: result.success ? 'completed' : 'failed',
|
|
246
|
+
costUsd: result.costUsd,
|
|
247
|
+
inputTokens: result.inputTokens,
|
|
248
|
+
outputTokens: result.outputTokens,
|
|
249
|
+
durationMs: result.durationMs,
|
|
250
|
+
model: result.model,
|
|
251
|
+
});
|
|
252
|
+
// Emit relay webhook event
|
|
253
|
+
sendWebhookNotification({
|
|
254
|
+
event: result.success ? 'relay_run_complete' : 'relay_run_failed',
|
|
255
|
+
timestamp: completedAt,
|
|
256
|
+
project,
|
|
257
|
+
workflow: 'relay',
|
|
258
|
+
success: result.success,
|
|
259
|
+
summary: result.text.slice(0, 200),
|
|
260
|
+
costUsd: result.costUsd,
|
|
261
|
+
durationMs: result.durationMs,
|
|
262
|
+
model: result.model,
|
|
263
|
+
platform: platform.name,
|
|
264
|
+
channelId: msg.channelId,
|
|
265
|
+
}).catch(() => { });
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
await stream.stop();
|
|
269
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
270
|
+
await platform.sendMessage(msg.channelId, `Error: ${errMsg}`, msg.conversationId);
|
|
271
|
+
updateRelayRun(this.db, runId, {
|
|
272
|
+
completedAt: new Date().toISOString(),
|
|
273
|
+
status: 'failed',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
this.activeAbortControllers.delete(msg.conversationId);
|
|
278
|
+
markPendingDone(this.db, pendingId);
|
|
279
|
+
}
|
|
189
280
|
}
|
|
190
281
|
}
|
|
191
282
|
//# sourceMappingURL=router.js.map
|