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.
- package/dist/api-client.d.ts +4 -0
- package/dist/api-client.js +3 -0
- package/dist/index.js +10 -8
- package/dist/invoker.js +1 -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 +43 -8
- package/dist/trigger.js +30 -13
- 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/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.js
CHANGED
|
@@ -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,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
|
-
|
|
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();
|
|
11
13
|
let retryTimer = null;
|
|
12
14
|
const RETRY_DELAY_MS = 3000;
|
|
13
15
|
// Periodic cleanup to prevent memory growth
|
|
14
|
-
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);
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
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") {
|