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.
@@ -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
  }
@@ -112,4 +112,7 @@ export class AgentFeedClient {
112
112
  agentId,
113
113
  });
114
114
  }
115
+ async getSettings() {
116
+ return this.request("/api/settings");
117
+ }
115
118
  }
@@ -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
  }
@@ -7,6 +7,7 @@ export interface BuildArgsOptions {
7
7
  permissionMode: PermissionMode;
8
8
  extraAllowedTools?: string[];
9
9
  model?: string;
10
+ chrome?: boolean;
10
11
  }
11
12
  export interface CLIBackend {
12
13
  readonly name: BackendType;
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
- // Confirm yolo if any server config overrides to yolo (and CLI didn't already confirm)
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 ? ["inherit", "pipe", "inherit"] : "inherit",
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.)${statusTool}`;
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 ![name](/api/uploads/up_xxx.png), 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>`;
@@ -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: ![alt](/api/uploads/up_xxx.png), 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
+ ? `![${fileName}](${upload.url})`
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. Legacy values without colon default to claude. */
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
- /** Store as "backendType:sessionName" */
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
- this.map.set(k, v);
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 raw = this.map.get(postId);
20
- if (!raw)
20
+ const raws = this.map.get(postId);
21
+ if (!raws || raws.length === 0)
21
22
  return undefined;
22
- // Parse "backendType:sessionName" or plain "sessionName"
23
- const colonIdx = raw.indexOf(":");
24
- return colonIdx >= 0 ? raw.slice(colonIdx + 1) : raw;
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. Legacy values without colon default to claude. */
28
+ /** Get both backendType and sessionName for first entry (legacy compat) */
27
29
  getWithType(postId) {
28
- const raw = this.map.get(postId);
29
- if (!raw)
30
- return undefined;
31
- const colonIdx = raw.indexOf(":");
32
- if (colonIdx >= 0) {
33
- return {
34
- backendType: raw.slice(0, colonIdx),
35
- sessionName: raw.slice(colonIdx + 1),
36
- };
37
- }
38
- // Legacy: no colon → assume claude
39
- return { backendType: "claude", sessionName: raw };
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
- /** Store as "backendType:sessionName" */
51
+ /** Add backend session (legacy compat: set = add) */
43
52
  set(postId, backendType, sessionName) {
44
- this.map.set(postId, `${backendType}:${sessionName}`);
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, raw] of this.map) {
61
- // Match both "backendType:sessionName" and legacy "sessionName"
62
- const colonIdx = raw.indexOf(":");
63
- const name = colonIdx >= 0 ? raw.slice(colonIdx + 1) : raw;
64
- if (name === sessionName) {
65
- this.map.delete(postId);
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
  }
@@ -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
- const MAX_BOT_MENTIONS_PER_POST = 4;
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 botMentionCounts = new Map();
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(() => { wakeAttempts.clear(); botMentionCounts.clear(); }, 10 * 60 * 1000);
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 count = botMentionCounts.get(t.postId) ?? 0;
21
- if (count >= MAX_BOT_MENTIONS_PER_POST) {
22
- console.log(`Skipping bot mention on ${t.postId}: loop limit (${MAX_BOT_MENTIONS_PER_POST}) reached`);
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
- botMentionCounts.set(t.postId, count + 1);
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 postSession = postSessionStore?.getWithType(event.post_id);
49
- return [{
50
- triggerType: "thread_follow_up",
51
- eventId: event.id,
52
- feedId: event.feed_id,
53
- feedName: "",
54
- postId: event.post_id,
55
- content: event.content,
56
- authorName: event.author_name,
57
- authorIsBot: event.author_type === "bot",
58
- sessionName: postSession?.sessionName ?? "default",
59
- backendType: postSession?.backendType ?? defaultBackendType,
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
@@ -9,6 +9,7 @@ export interface AgentConfig {
9
9
  permission_mode: PermissionMode;
10
10
  allowed_tools: string[];
11
11
  model: string | null;
12
+ chrome: boolean;
12
13
  }
13
14
  export interface BackendAgent {
14
15
  backendType: BackendType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentfeed",
3
- "version": "0.1.11",
3
+ "version": "0.1.14",
4
4
  "description": "Worker daemon for AgentFeed - watches feeds and wakes AI agents via claude -p",
5
5
  "type": "module",
6
6
  "bin": {