agentfeed 0.1.11 → 0.1.14
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/api-client.d.ts +4 -0
- package/dist/api-client.js +3 -0
- package/dist/backends/claude.js +7 -1
- package/dist/backends/types.d.ts +1 -0
- package/dist/cli.js +10 -0
- package/dist/index.js +10 -8
- package/dist/invoker.d.ts +2 -0
- package/dist/invoker.js +17 -5
- package/dist/mcp-server.js +68 -0
- package/dist/post-session-store.d.ts +6 -2
- package/dist/post-session-store.js +53 -27
- package/dist/processor.d.ts +1 -0
- package/dist/processor.js +62 -8
- package/dist/trigger.js +30 -13
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/api-client.d.ts
CHANGED
|
@@ -30,4 +30,8 @@ export declare class AgentFeedClient {
|
|
|
30
30
|
}, agentId?: string): Promise<void>;
|
|
31
31
|
getAgentConfig(agentId: string): Promise<AgentConfig>;
|
|
32
32
|
reportSession(sessionName: string, claudeSessionId: string, agentId?: string): Promise<void>;
|
|
33
|
+
getSettings(): Promise<{
|
|
34
|
+
bot_mention_limit: number;
|
|
35
|
+
bot_mention_window_minutes: number;
|
|
36
|
+
}>;
|
|
33
37
|
}
|
package/dist/api-client.js
CHANGED
package/dist/backends/claude.js
CHANGED
|
@@ -24,7 +24,7 @@ export class ClaudeBackend {
|
|
|
24
24
|
fs.writeFileSync(this.mcpConfigPath, JSON.stringify(config, null, 2));
|
|
25
25
|
}
|
|
26
26
|
buildArgs(options) {
|
|
27
|
-
const { prompt, systemPrompt, sessionId, permissionMode, extraAllowedTools, model } = options;
|
|
27
|
+
const { prompt, systemPrompt, sessionId, permissionMode, extraAllowedTools, model, chrome } = options;
|
|
28
28
|
const args = [
|
|
29
29
|
"-p", prompt,
|
|
30
30
|
"--append-system-prompt", systemPrompt,
|
|
@@ -33,11 +33,17 @@ export class ClaudeBackend {
|
|
|
33
33
|
if (model) {
|
|
34
34
|
args.push("--model", model);
|
|
35
35
|
}
|
|
36
|
+
if (chrome) {
|
|
37
|
+
args.push("--chrome");
|
|
38
|
+
}
|
|
36
39
|
if (permissionMode === "yolo") {
|
|
37
40
|
args.push("--dangerously-skip-permissions");
|
|
38
41
|
}
|
|
39
42
|
else {
|
|
40
43
|
const allowedTools = ["mcp__agentfeed__*", ...(extraAllowedTools ?? [])];
|
|
44
|
+
if (chrome) {
|
|
45
|
+
allowedTools.push("mcp__claude-in-chrome__*");
|
|
46
|
+
}
|
|
41
47
|
for (const tool of allowedTools) {
|
|
42
48
|
args.push("--allowedTools", tool);
|
|
43
49
|
}
|
package/dist/backends/types.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -100,6 +100,16 @@ export function probeBackend(type) {
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
export function confirmYolo() {
|
|
103
|
+
// --yes flag skips the interactive confirmation
|
|
104
|
+
if (process.argv.includes("--yes") || process.argv.includes("-y")) {
|
|
105
|
+
console.log("");
|
|
106
|
+
console.log(" \x1b[33m⚠️ YOLO mode enabled. The agent can do literally anything.\x1b[0m");
|
|
107
|
+
console.log(" \x1b[33m No prompt sandboxing. No trust boundaries.\x1b[0m");
|
|
108
|
+
console.log(" \x1b[33m Prompt injection? Not your problem today.\x1b[0m");
|
|
109
|
+
console.log("");
|
|
110
|
+
console.log(" Continue? (y/N): y (auto-confirmed via --yes)");
|
|
111
|
+
return Promise.resolve(true);
|
|
112
|
+
}
|
|
103
113
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
104
114
|
return new Promise((resolve) => {
|
|
105
115
|
console.log("");
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { QueueStore } from "./queue-store.js";
|
|
|
10
10
|
import { PostSessionStore } from "./post-session-store.js";
|
|
11
11
|
import { AgentRegistryStore } from "./agent-registry-store.js";
|
|
12
12
|
import { createBackend } from "./backends/index.js";
|
|
13
|
-
import { handleTriggers } from "./processor.js";
|
|
13
|
+
import { handleTriggers, loadSettings } from "./processor.js";
|
|
14
14
|
import { getRequiredEnv, parsePermissionMode, parseAllowedTools, detectInstalledBackends, probeBackend, confirmYolo, migrateSessionFile, } from "./cli.js";
|
|
15
15
|
const serverUrl = getRequiredEnv("AGENTFEED_URL");
|
|
16
16
|
const apiKey = getRequiredEnv("AGENTFEED_API_KEY");
|
|
@@ -131,16 +131,12 @@ async function main() {
|
|
|
131
131
|
agentRegistry.set(agentName, agent.id);
|
|
132
132
|
}
|
|
133
133
|
backendAgents = Array.from(backendAgentMap.values());
|
|
134
|
-
//
|
|
134
|
+
// Server config yolo is pre-authorized by admin — no confirmation needed
|
|
135
|
+
// Only confirm if CLI explicitly requests yolo via --permission flag
|
|
135
136
|
if (permissionMode !== "yolo") {
|
|
136
137
|
const hasServerYolo = backendAgents.some((ba) => ba.config?.permission_mode === "yolo");
|
|
137
138
|
if (hasServerYolo) {
|
|
138
|
-
console.log("\nServer config has yolo permission for one or more agents.");
|
|
139
|
-
const confirmed = await confirmYolo();
|
|
140
|
-
if (!confirmed) {
|
|
141
|
-
console.log("Cancelled. Update agent permissions on the server to use safe mode.");
|
|
142
|
-
process.exit(0);
|
|
143
|
-
}
|
|
139
|
+
console.log("\nServer config has yolo permission for one or more agents (auto-confirmed by server settings).");
|
|
144
140
|
}
|
|
145
141
|
}
|
|
146
142
|
// Register Named Session agents for all backends
|
|
@@ -159,6 +155,12 @@ async function main() {
|
|
|
159
155
|
}
|
|
160
156
|
}
|
|
161
157
|
const deps = getProcessorDeps();
|
|
158
|
+
// Load bot mention limit settings from server
|
|
159
|
+
await loadSettings(client);
|
|
160
|
+
// Refresh settings every 5 minutes
|
|
161
|
+
setInterval(async () => {
|
|
162
|
+
await loadSettings(client);
|
|
163
|
+
}, 5 * 60 * 1000);
|
|
162
164
|
// Step 1: Startup scan for unprocessed items
|
|
163
165
|
console.log("Scanning for unprocessed items...");
|
|
164
166
|
const ownAgentIds = agentRegistry.getAllIds();
|
package/dist/invoker.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface InvokeOptions {
|
|
|
9
9
|
permissionMode: PermissionMode;
|
|
10
10
|
extraAllowedTools?: string[];
|
|
11
11
|
model?: string;
|
|
12
|
+
chrome?: boolean;
|
|
12
13
|
sessionId?: string;
|
|
13
14
|
agentId?: string;
|
|
14
15
|
timeoutMs?: number;
|
|
@@ -16,5 +17,6 @@ export interface InvokeOptions {
|
|
|
16
17
|
export interface InvokeResult {
|
|
17
18
|
exitCode: number;
|
|
18
19
|
sessionId?: string;
|
|
20
|
+
timedOut: boolean;
|
|
19
21
|
}
|
|
20
22
|
export declare function invokeAgent(backend: CLIBackend, options: InvokeOptions): Promise<InvokeResult>;
|
package/dist/invoker.js
CHANGED
|
@@ -42,6 +42,7 @@ export function invokeAgent(backend, options) {
|
|
|
42
42
|
permissionMode: options.permissionMode,
|
|
43
43
|
extraAllowedTools: options.extraAllowedTools,
|
|
44
44
|
model: options.model,
|
|
45
|
+
chrome: options.chrome,
|
|
45
46
|
});
|
|
46
47
|
const env = backend.buildEnv({
|
|
47
48
|
...agentfeedEnv,
|
|
@@ -55,12 +56,14 @@ export function invokeAgent(backend, options) {
|
|
|
55
56
|
console.log(`Invoking ${backend.name}...`);
|
|
56
57
|
const child = spawn(backend.binaryName, args, {
|
|
57
58
|
env,
|
|
58
|
-
stdio: isNewSession ? ["
|
|
59
|
+
stdio: isNewSession ? ["ignore", "pipe", "inherit"] : ["ignore", "inherit", "inherit"],
|
|
59
60
|
});
|
|
60
61
|
// Timeout watchdog
|
|
61
62
|
let killTimer = null;
|
|
63
|
+
let timedOut = false;
|
|
62
64
|
if (options.timeoutMs) {
|
|
63
65
|
killTimer = setTimeout(() => {
|
|
66
|
+
timedOut = true;
|
|
64
67
|
console.warn(`Agent timed out after ${options.timeoutMs / 1000}s, killing process...`);
|
|
65
68
|
child.kill("SIGTERM");
|
|
66
69
|
setTimeout(() => { if (!child.killed)
|
|
@@ -81,6 +84,7 @@ export function invokeAgent(backend, options) {
|
|
|
81
84
|
const sid = backend.parseSessionId(line);
|
|
82
85
|
if (sid) {
|
|
83
86
|
sessionId = sid;
|
|
87
|
+
console.log(`Session ID parsed: ${sid}`);
|
|
84
88
|
}
|
|
85
89
|
// Try to extract displayable text
|
|
86
90
|
const text = backend.parseStreamText(line);
|
|
@@ -104,7 +108,7 @@ export function invokeAgent(backend, options) {
|
|
|
104
108
|
if (isNewSession)
|
|
105
109
|
process.stdout.write("\n");
|
|
106
110
|
console.log(`Agent exited (code ${code ?? "unknown"})`);
|
|
107
|
-
resolve({ exitCode: code ?? 1, sessionId: sessionId ?? options.sessionId });
|
|
111
|
+
resolve({ exitCode: code ?? 1, sessionId: sessionId ?? options.sessionId, timedOut });
|
|
108
112
|
});
|
|
109
113
|
});
|
|
110
114
|
}
|
|
@@ -138,7 +142,15 @@ function buildSystemPrompt(options) {
|
|
|
138
142
|
- agentfeed_create_post - Create a new post in a feed
|
|
139
143
|
- agentfeed_get_comments - Get comments on a post (use since/author_type filters)
|
|
140
144
|
- agentfeed_post_comment - Post a comment (Korean and emoji supported!)
|
|
141
|
-
- agentfeed_download_file - Download and view uploaded files (images, etc.)
|
|
145
|
+
- agentfeed_download_file - Download and view uploaded files (images, etc.)
|
|
146
|
+
- agentfeed_upload_file - Upload a local file and get markdown URL${statusTool}`;
|
|
147
|
+
const chromeSection = options.chrome
|
|
148
|
+
? `\n\n# Chrome Browser
|
|
149
|
+
|
|
150
|
+
You have Chrome browser automation tools (mcp__claude-in-chrome__*) available.
|
|
151
|
+
Use these to navigate web pages, take screenshots, and interact with browser content.
|
|
152
|
+
To capture and share a web page: take a screenshot, save to /tmp, then upload via agentfeed_upload_file.`
|
|
153
|
+
: "";
|
|
142
154
|
const imageGuidance = `IMPORTANT: When content contains image URLs like , use agentfeed_download_file to view the image before responding about it.`;
|
|
143
155
|
if (options.permissionMode === "yolo") {
|
|
144
156
|
return `# AgentFeed
|
|
@@ -149,7 +161,7 @@ ${toolList}
|
|
|
149
161
|
|
|
150
162
|
Use these tools to interact with the feed. All content encoding is handled automatically.
|
|
151
163
|
|
|
152
|
-
${imageGuidance}`;
|
|
164
|
+
${imageGuidance}${chromeSection}`;
|
|
153
165
|
}
|
|
154
166
|
return `${SECURITY_POLICY}
|
|
155
167
|
|
|
@@ -161,7 +173,7 @@ ${toolList}
|
|
|
161
173
|
|
|
162
174
|
Use these tools to interact with the feed. All content encoding is handled automatically.
|
|
163
175
|
|
|
164
|
-
${imageGuidance}`;
|
|
176
|
+
${imageGuidance}${chromeSection}`;
|
|
165
177
|
}
|
|
166
178
|
function wrapUntrusted(text) {
|
|
167
179
|
return `<untrusted_content>\n${escapeXml(text)}\n</untrusted_content>`;
|
package/dist/mcp-server.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
1
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
5
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -82,6 +84,22 @@ const TOOLS = [
|
|
|
82
84
|
required: ["url"],
|
|
83
85
|
},
|
|
84
86
|
},
|
|
87
|
+
{
|
|
88
|
+
name: "agentfeed_upload_file",
|
|
89
|
+
description: "Upload a file to AgentFeed. Returns the URL that can be used in markdown. " +
|
|
90
|
+
"For images: , for others: [filename](/api/uploads/up_xxx.ext). " +
|
|
91
|
+
"Use this to share generated charts, images, or any files in posts and comments.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
file_path: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Absolute path to the file to upload (e.g. /tmp/chart.svg)",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
required: ["file_path"],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
85
103
|
{
|
|
86
104
|
name: "agentfeed_set_status",
|
|
87
105
|
description: "Report agent status (thinking/idle)",
|
|
@@ -184,6 +202,56 @@ async function handleToolCall(name, args, ctx) {
|
|
|
184
202
|
],
|
|
185
203
|
};
|
|
186
204
|
}
|
|
205
|
+
case "agentfeed_upload_file": {
|
|
206
|
+
const { file_path } = args;
|
|
207
|
+
const fileBuffer = await readFile(file_path);
|
|
208
|
+
const fileName = basename(file_path);
|
|
209
|
+
const ext = fileName.includes(".") ? fileName.split(".").pop().toLowerCase() : "";
|
|
210
|
+
const mimeTypes = {
|
|
211
|
+
svg: "image/svg+xml",
|
|
212
|
+
png: "image/png",
|
|
213
|
+
jpg: "image/jpeg",
|
|
214
|
+
jpeg: "image/jpeg",
|
|
215
|
+
gif: "image/gif",
|
|
216
|
+
webp: "image/webp",
|
|
217
|
+
pdf: "application/pdf",
|
|
218
|
+
json: "application/json",
|
|
219
|
+
csv: "text/csv",
|
|
220
|
+
txt: "text/plain",
|
|
221
|
+
md: "text/markdown",
|
|
222
|
+
html: "text/html",
|
|
223
|
+
mp4: "video/mp4",
|
|
224
|
+
webm: "video/webm",
|
|
225
|
+
};
|
|
226
|
+
const mimeType = mimeTypes[ext] || "application/octet-stream";
|
|
227
|
+
const blob = new Blob([fileBuffer], { type: mimeType });
|
|
228
|
+
const formData = new FormData();
|
|
229
|
+
formData.append("file", blob, fileName);
|
|
230
|
+
const uploadRes = await fetch(`${serverUrl}/api/uploads`, {
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers: {
|
|
233
|
+
Authorization: `Bearer ${client.apiKey}`,
|
|
234
|
+
Origin: serverUrl,
|
|
235
|
+
...(client.agentId ? { "X-Agent-Id": client.agentId } : {}),
|
|
236
|
+
},
|
|
237
|
+
body: formData,
|
|
238
|
+
});
|
|
239
|
+
if (!uploadRes.ok)
|
|
240
|
+
throw new Error(`Failed to upload file: ${uploadRes.status} ${await uploadRes.text()}`);
|
|
241
|
+
const upload = (await uploadRes.json());
|
|
242
|
+
const isImage = upload.mime_type.startsWith("image/");
|
|
243
|
+
const markdown = isImage
|
|
244
|
+
? ``
|
|
245
|
+
: `[${fileName}](${upload.url})`;
|
|
246
|
+
return {
|
|
247
|
+
content: [
|
|
248
|
+
{
|
|
249
|
+
type: "text",
|
|
250
|
+
text: `File uploaded successfully.\nURL: ${upload.url}\nMarkdown: ${markdown}\nSize: ${upload.size} bytes`,
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
187
255
|
case "agentfeed_set_status": {
|
|
188
256
|
const { status, feed_id, post_id } = args;
|
|
189
257
|
await client.setAgentStatus({ status, feed_id, post_id });
|
|
@@ -11,10 +11,14 @@ export declare class PostSessionStore extends PersistentStore {
|
|
|
11
11
|
protected deserialize(raw: string): void;
|
|
12
12
|
/** Get raw sessionName (legacy compat — for callers that don't need backendType) */
|
|
13
13
|
get(postId: string): string | undefined;
|
|
14
|
-
/** Get both backendType and sessionName
|
|
14
|
+
/** Get both backendType and sessionName for first entry (legacy compat) */
|
|
15
15
|
getWithType(postId: string): PostSessionInfo | undefined;
|
|
16
|
+
/** Get all backend sessions for a post */
|
|
17
|
+
getAll(postId: string): PostSessionInfo[];
|
|
16
18
|
private static readonly MAX_SIZE;
|
|
17
|
-
/**
|
|
19
|
+
/** Add backend session (legacy compat: set = add) */
|
|
18
20
|
set(postId: string, backendType: BackendType, sessionName: string): void;
|
|
21
|
+
/** Add backend session to post (prevents duplicates) */
|
|
22
|
+
add(postId: string, backendType: BackendType, sessionName: string): void;
|
|
19
23
|
removeBySessionName(sessionName: string): void;
|
|
20
24
|
}
|
|
@@ -11,37 +11,56 @@ export class PostSessionStore extends PersistentStore {
|
|
|
11
11
|
deserialize(raw) {
|
|
12
12
|
const data = JSON.parse(raw);
|
|
13
13
|
for (const [k, v] of Object.entries(data)) {
|
|
14
|
-
|
|
14
|
+
// Support both legacy single value and new array format
|
|
15
|
+
this.map.set(k, Array.isArray(v) ? v : [v]);
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
/** Get raw sessionName (legacy compat — for callers that don't need backendType) */
|
|
18
19
|
get(postId) {
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
20
|
+
const raws = this.map.get(postId);
|
|
21
|
+
if (!raws || raws.length === 0)
|
|
21
22
|
return undefined;
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
// Return first sessionName
|
|
24
|
+
const first = raws[0];
|
|
25
|
+
const colonIdx = first.indexOf(":");
|
|
26
|
+
return colonIdx >= 0 ? first.slice(colonIdx + 1) : first;
|
|
25
27
|
}
|
|
26
|
-
/** Get both backendType and sessionName
|
|
28
|
+
/** Get both backendType and sessionName for first entry (legacy compat) */
|
|
27
29
|
getWithType(postId) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
const all = this.getAll(postId);
|
|
31
|
+
return all.length > 0 ? all[0] : undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Get all backend sessions for a post */
|
|
34
|
+
getAll(postId) {
|
|
35
|
+
const raws = this.map.get(postId);
|
|
36
|
+
if (!raws)
|
|
37
|
+
return [];
|
|
38
|
+
return raws.map((raw) => {
|
|
39
|
+
const colonIdx = raw.indexOf(":");
|
|
40
|
+
if (colonIdx >= 0) {
|
|
41
|
+
return {
|
|
42
|
+
backendType: raw.slice(0, colonIdx),
|
|
43
|
+
sessionName: raw.slice(colonIdx + 1),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Legacy: no colon → assume claude
|
|
47
|
+
return { backendType: "claude", sessionName: raw };
|
|
48
|
+
});
|
|
40
49
|
}
|
|
41
50
|
static MAX_SIZE = 1000;
|
|
42
|
-
/**
|
|
51
|
+
/** Add backend session (legacy compat: set = add) */
|
|
43
52
|
set(postId, backendType, sessionName) {
|
|
44
|
-
this.
|
|
53
|
+
this.add(postId, backendType, sessionName);
|
|
54
|
+
}
|
|
55
|
+
/** Add backend session to post (prevents duplicates) */
|
|
56
|
+
add(postId, backendType, sessionName) {
|
|
57
|
+
const entry = `${backendType}:${sessionName}`;
|
|
58
|
+
const existing = this.map.get(postId) ?? [];
|
|
59
|
+
// Prevent duplicates
|
|
60
|
+
if (!existing.includes(entry)) {
|
|
61
|
+
existing.push(entry);
|
|
62
|
+
this.map.set(postId, existing);
|
|
63
|
+
}
|
|
45
64
|
// Evict oldest entries if over limit
|
|
46
65
|
if (this.map.size > PostSessionStore.MAX_SIZE) {
|
|
47
66
|
const iter = this.map.keys();
|
|
@@ -57,12 +76,19 @@ export class PostSessionStore extends PersistentStore {
|
|
|
57
76
|
}
|
|
58
77
|
removeBySessionName(sessionName) {
|
|
59
78
|
let changed = false;
|
|
60
|
-
for (const [postId,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
for (const [postId, raws] of this.map) {
|
|
80
|
+
const filtered = raws.filter((raw) => {
|
|
81
|
+
const colonIdx = raw.indexOf(":");
|
|
82
|
+
const name = colonIdx >= 0 ? raw.slice(colonIdx + 1) : raw;
|
|
83
|
+
return name !== sessionName;
|
|
84
|
+
});
|
|
85
|
+
if (filtered.length !== raws.length) {
|
|
86
|
+
if (filtered.length === 0) {
|
|
87
|
+
this.map.delete(postId);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
this.map.set(postId, filtered);
|
|
91
|
+
}
|
|
66
92
|
changed = true;
|
|
67
93
|
}
|
|
68
94
|
}
|
package/dist/processor.d.ts
CHANGED
|
@@ -18,4 +18,5 @@ export interface ProcessorDeps {
|
|
|
18
18
|
agentRegistry: AgentRegistryStore;
|
|
19
19
|
ensureSessionAgent: (sessionName: string, agentName: string, backendType: BackendType) => Promise<string>;
|
|
20
20
|
}
|
|
21
|
+
export declare function loadSettings(client: AgentFeedClient): Promise<void>;
|
|
21
22
|
export declare function handleTriggers(triggers: TriggerContext[], deps: ProcessorDeps): void;
|
package/dist/processor.js
CHANGED
|
@@ -4,25 +4,58 @@ const MAX_WAKE_ATTEMPTS = 3;
|
|
|
4
4
|
const MAX_CRASH_RETRIES = 3;
|
|
5
5
|
const AGENT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
6
|
const MAX_CONCURRENT = 5;
|
|
7
|
-
|
|
7
|
+
// Time-window based bot mention limits (loaded from server)
|
|
8
|
+
let botMentionLimit = 4;
|
|
9
|
+
let botMentionWindowMs = 5 * 60 * 1000; // 5 minutes
|
|
8
10
|
const wakeAttempts = new Map();
|
|
9
|
-
const
|
|
11
|
+
const botMentionTimestamps = new Map();
|
|
10
12
|
const runningKeys = new Set();
|
|
13
|
+
let retryTimer = null;
|
|
14
|
+
const RETRY_DELAY_MS = 3000;
|
|
11
15
|
// Periodic cleanup to prevent memory growth
|
|
12
|
-
setInterval(() => {
|
|
16
|
+
setInterval(() => {
|
|
17
|
+
wakeAttempts.clear();
|
|
18
|
+
// Clean up old timestamps beyond the window
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const [postId, timestamps] of botMentionTimestamps.entries()) {
|
|
21
|
+
const recent = timestamps.filter((ts) => now - ts < botMentionWindowMs);
|
|
22
|
+
if (recent.length === 0) {
|
|
23
|
+
botMentionTimestamps.delete(postId);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
botMentionTimestamps.set(postId, recent);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}, 10 * 60 * 1000);
|
|
13
30
|
function triggerKey(t) {
|
|
14
31
|
return `${t.backendType}:${t.sessionName}`;
|
|
15
32
|
}
|
|
33
|
+
export async function loadSettings(client) {
|
|
34
|
+
try {
|
|
35
|
+
const settings = await client.getSettings();
|
|
36
|
+
botMentionLimit = settings.bot_mention_limit;
|
|
37
|
+
botMentionWindowMs = settings.bot_mention_window_minutes * 60 * 1000;
|
|
38
|
+
console.log(`Loaded settings: bot_mention_limit=${botMentionLimit}, bot_mention_window_minutes=${settings.bot_mention_window_minutes}`);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error("Failed to load settings from server, using defaults:", err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
16
44
|
export function handleTriggers(triggers, deps) {
|
|
45
|
+
const now = Date.now();
|
|
17
46
|
for (const t of triggers) {
|
|
18
|
-
// Prevent bot-to-bot mention loops
|
|
47
|
+
// Prevent bot-to-bot mention loops (time-window based)
|
|
19
48
|
if (t.authorIsBot && t.triggerType === "mention") {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
49
|
+
const timestamps = botMentionTimestamps.get(t.postId) ?? [];
|
|
50
|
+
// Filter timestamps within the current window
|
|
51
|
+
const recentTimestamps = timestamps.filter((ts) => now - ts < botMentionWindowMs);
|
|
52
|
+
if (recentTimestamps.length >= botMentionLimit) {
|
|
53
|
+
console.log(`Skipping bot mention on ${t.postId}: loop limit (${botMentionLimit} mentions in ${botMentionWindowMs / 1000 / 60} minutes) reached`);
|
|
23
54
|
continue;
|
|
24
55
|
}
|
|
25
|
-
|
|
56
|
+
// Record this mention
|
|
57
|
+
recentTimestamps.push(now);
|
|
58
|
+
botMentionTimestamps.set(t.postId, recentTimestamps);
|
|
26
59
|
}
|
|
27
60
|
deps.queueStore.push(t);
|
|
28
61
|
console.log(`Queued trigger: ${t.triggerType} on ${t.postId} [${t.backendType}] (queue size: ${deps.queueStore.size})`);
|
|
@@ -34,6 +67,7 @@ function scheduleQueue(deps) {
|
|
|
34
67
|
if (queued.length === 0)
|
|
35
68
|
return;
|
|
36
69
|
const toRun = [];
|
|
70
|
+
let hasRequeued = false;
|
|
37
71
|
for (const t of queued) {
|
|
38
72
|
const attempts = wakeAttempts.get(t.eventId) ?? 0;
|
|
39
73
|
if (attempts >= MAX_WAKE_ATTEMPTS) {
|
|
@@ -44,12 +78,22 @@ function scheduleQueue(deps) {
|
|
|
44
78
|
if (runningKeys.size >= MAX_CONCURRENT || runningKeys.has(key)) {
|
|
45
79
|
// Same backend+session already running — re-queue
|
|
46
80
|
deps.queueStore.push(t);
|
|
81
|
+
hasRequeued = true;
|
|
47
82
|
}
|
|
48
83
|
else {
|
|
49
84
|
toRun.push(t);
|
|
50
85
|
runningKeys.add(key);
|
|
51
86
|
}
|
|
52
87
|
}
|
|
88
|
+
// Schedule retry for re-queued items so they don't wait for next SSE event
|
|
89
|
+
if (hasRequeued) {
|
|
90
|
+
if (retryTimer)
|
|
91
|
+
clearTimeout(retryTimer);
|
|
92
|
+
retryTimer = setTimeout(() => {
|
|
93
|
+
retryTimer = null;
|
|
94
|
+
scheduleQueue(deps);
|
|
95
|
+
}, RETRY_DELAY_MS);
|
|
96
|
+
}
|
|
53
97
|
for (const trigger of toRun) {
|
|
54
98
|
processItem(trigger, deps).catch((err) => {
|
|
55
99
|
console.error(`Error processing ${trigger.postId}:`, err);
|
|
@@ -86,6 +130,7 @@ async function processItem(trigger, deps) {
|
|
|
86
130
|
? ba.config.allowed_tools
|
|
87
131
|
: deps.extraAllowedTools;
|
|
88
132
|
const effectiveModel = ba.config?.model;
|
|
133
|
+
const effectiveChrome = ba.config?.chrome ?? false;
|
|
89
134
|
let retries = 0;
|
|
90
135
|
let success = false;
|
|
91
136
|
try {
|
|
@@ -100,6 +145,7 @@ async function processItem(trigger, deps) {
|
|
|
100
145
|
permissionMode: effectivePermission,
|
|
101
146
|
extraAllowedTools: effectiveTools,
|
|
102
147
|
model: effectiveModel ?? undefined,
|
|
148
|
+
chrome: effectiveChrome,
|
|
103
149
|
sessionId: ba.sessionStore.get(trigger.sessionName),
|
|
104
150
|
agentId: sessionAgentId,
|
|
105
151
|
timeoutMs: AGENT_TIMEOUT_MS,
|
|
@@ -107,14 +153,22 @@ async function processItem(trigger, deps) {
|
|
|
107
153
|
if (result.sessionId) {
|
|
108
154
|
ba.sessionStore.set(trigger.sessionName, result.sessionId);
|
|
109
155
|
deps.postSessionStore.set(trigger.postId, trigger.backendType, trigger.sessionName);
|
|
156
|
+
console.log(`Saved post-session mapping: ${trigger.postId} → [${trigger.backendType}] ${trigger.sessionName}`);
|
|
110
157
|
await deps.client.reportSession(trigger.sessionName, result.sessionId, sessionAgentId).catch((err) => {
|
|
111
158
|
console.warn("Failed to report session:", err);
|
|
112
159
|
});
|
|
113
160
|
}
|
|
161
|
+
else {
|
|
162
|
+
console.warn(`No session ID returned by ${trigger.backendType}, post-session mapping not saved`);
|
|
163
|
+
}
|
|
114
164
|
if (result.exitCode === 0) {
|
|
115
165
|
success = true;
|
|
116
166
|
break;
|
|
117
167
|
}
|
|
168
|
+
if (result.timedOut) {
|
|
169
|
+
console.warn("Agent timed out, skipping retry");
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
118
172
|
if (result.exitCode !== 0 && ba.sessionStore.get(trigger.sessionName)) {
|
|
119
173
|
console.log("Session may be stale, clearing and retrying as new session...");
|
|
120
174
|
ba.sessionStore.delete(trigger.sessionName);
|
package/dist/trigger.js
CHANGED
|
@@ -45,19 +45,36 @@ export function detectTriggers(event, backendAgents, followStore, postSessionSto
|
|
|
45
45
|
return mentions;
|
|
46
46
|
// Trigger 3: Comment in a followed thread (human-authored only — prevents bot loop)
|
|
47
47
|
if (!authorIsOwnBot && followStore?.has(event.post_id)) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
const postSessions = postSessionStore?.getAll(event.post_id) ?? [];
|
|
49
|
+
if (postSessions.length === 0) {
|
|
50
|
+
console.log(`Thread follow-up on ${event.post_id}: no post-session found, using default [${defaultBackendType}]`);
|
|
51
|
+
return [{
|
|
52
|
+
triggerType: "thread_follow_up",
|
|
53
|
+
eventId: event.id,
|
|
54
|
+
feedId: event.feed_id,
|
|
55
|
+
feedName: "",
|
|
56
|
+
postId: event.post_id,
|
|
57
|
+
content: event.content,
|
|
58
|
+
authorName: event.author_name,
|
|
59
|
+
authorIsBot: event.author_type === "bot",
|
|
60
|
+
sessionName: "default",
|
|
61
|
+
backendType: defaultBackendType,
|
|
62
|
+
}];
|
|
63
|
+
}
|
|
64
|
+
// Trigger ALL backends that participated in this thread
|
|
65
|
+
console.log(`Thread follow-up on ${event.post_id}: triggering ${postSessions.length} backend(s) [${postSessions.map(s => s.backendType).join(", ")}]`);
|
|
66
|
+
return postSessions.map((ps) => ({
|
|
67
|
+
triggerType: "thread_follow_up",
|
|
68
|
+
eventId: `${event.id}:${ps.backendType}`,
|
|
69
|
+
feedId: event.feed_id,
|
|
70
|
+
feedName: "",
|
|
71
|
+
postId: event.post_id,
|
|
72
|
+
content: event.content,
|
|
73
|
+
authorName: event.author_name,
|
|
74
|
+
authorIsBot: event.author_type === "bot",
|
|
75
|
+
sessionName: ps.sessionName,
|
|
76
|
+
backendType: ps.backendType,
|
|
77
|
+
}));
|
|
61
78
|
}
|
|
62
79
|
}
|
|
63
80
|
if (event.type === "post_created") {
|
package/dist/types.d.ts
CHANGED