agentfeed 0.1.12 → 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
  }
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.js CHANGED
@@ -84,6 +84,7 @@ export function invokeAgent(backend, options) {
84
84
  const sid = backend.parseSessionId(line);
85
85
  if (sid) {
86
86
  sessionId = sid;
87
+ console.log(`Session ID parsed: ${sid}`);
87
88
  }
88
89
  // Try to extract displayable text
89
90
  const text = backend.parseStreamText(line);
@@ -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,27 +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();
11
13
  let retryTimer = null;
12
14
  const RETRY_DELAY_MS = 3000;
13
15
  // Periodic cleanup to prevent memory growth
14
- 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);
15
30
  function triggerKey(t) {
16
31
  return `${t.backendType}:${t.sessionName}`;
17
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
+ }
18
44
  export function handleTriggers(triggers, deps) {
45
+ const now = Date.now();
19
46
  for (const t of triggers) {
20
- // Prevent bot-to-bot mention loops
47
+ // Prevent bot-to-bot mention loops (time-window based)
21
48
  if (t.authorIsBot && t.triggerType === "mention") {
22
- const count = botMentionCounts.get(t.postId) ?? 0;
23
- if (count >= MAX_BOT_MENTIONS_PER_POST) {
24
- 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`);
25
54
  continue;
26
55
  }
27
- botMentionCounts.set(t.postId, count + 1);
56
+ // Record this mention
57
+ recentTimestamps.push(now);
58
+ botMentionTimestamps.set(t.postId, recentTimestamps);
28
59
  }
29
60
  deps.queueStore.push(t);
30
61
  console.log(`Queued trigger: ${t.triggerType} on ${t.postId} [${t.backendType}] (queue size: ${deps.queueStore.size})`);
@@ -122,10 +153,14 @@ async function processItem(trigger, deps) {
122
153
  if (result.sessionId) {
123
154
  ba.sessionStore.set(trigger.sessionName, result.sessionId);
124
155
  deps.postSessionStore.set(trigger.postId, trigger.backendType, trigger.sessionName);
156
+ console.log(`Saved post-session mapping: ${trigger.postId} → [${trigger.backendType}] ${trigger.sessionName}`);
125
157
  await deps.client.reportSession(trigger.sessionName, result.sessionId, sessionAgentId).catch((err) => {
126
158
  console.warn("Failed to report session:", err);
127
159
  });
128
160
  }
161
+ else {
162
+ console.warn(`No session ID returned by ${trigger.backendType}, post-session mapping not saved`);
163
+ }
129
164
  if (result.exitCode === 0) {
130
165
  success = true;
131
166
  break;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentfeed",
3
- "version": "0.1.12",
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": {